Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implemented shields #888

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions server/entity/projectile.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/potion"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/sound"
"github.com/go-gl/mathgl/mgl64"
"math"
"math/rand"
Expand Down Expand Up @@ -164,8 +165,19 @@ func (lt *ProjectileBehaviour) Tick(e *Ent) *Movement {

switch r := result.(type) {
case trace.EntityResult:
if l, ok := r.Entity().(Living); ok && lt.conf.Damage >= 0 {
lt.hitEntity(l, e, before, vel)
if l, ok := r.Entity().(Living); ok {
if blocker, ok := l.(blocker); ok {
if blocking, _ := blocker.Blocking(); blocking {
w.PlaySound(l.Position(), sound.ShieldBlock{})
m.vel = vel.Mul(-0.25)
m.dvel = m.vel.Sub(vel)
lt.close = false
return m
}
}
if lt.conf.Damage >= 0 {
lt.hitEntity(l, e, before, vel)
}
}
case trace.BlockResult:
bpos := r.BlockPosition()
Expand Down Expand Up @@ -330,3 +342,10 @@ func (lt *ProjectileBehaviour) ignores(e *Ent) func(other world.Entity) bool {
return (ok && !g.GameMode().HasCollision()) || e == other || !living || (e.age < time.Second/4 && lt.owner == other)
}
}

// blocker represents an entity that can block attacks with a shield.
type blocker interface {
// Blocking returns two different booleans, the first being if the entity is holding a shield in either hand, and the
// second being if the entity is using the shield.
Blocking() (holding bool, using bool)
}
1 change: 1 addition & 0 deletions server/item/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func init() {
world.RegisterItem(Salmon{})
world.RegisterItem(Scute{})
world.RegisterItem(Shears{})
world.RegisterItem(Shield{})
world.RegisterItem(ShulkerShell{})
world.RegisterItem(Slimeball{})
world.RegisterItem(Snowball{})
Expand Down
30 changes: 30 additions & 0 deletions server/item/shield.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package item

// Shield is a tool used for protecting the player against attacks.
type Shield struct{}

// MaxCount ...
func (Shield) MaxCount() int {
return 1
}

// RepairableBy ...
func (Shield) RepairableBy(i Stack) bool {
if planks, ok := i.Item().(interface{ RepairsWoodTools() bool }); ok {
return planks.RepairsWoodTools()
}
return false
}

// DurabilityInfo ...
func (s Shield) DurabilityInfo() DurabilityInfo {
return DurabilityInfo{
MaxDurability: 336,
BrokenItem: simpleItem(Stack{}),
}
}

// EncodeItem ...
func (Shield) EncodeItem() (name string, meta int16) {
return "minecraft:shield", 0
}
97 changes: 94 additions & 3 deletions server/player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ type Player struct {
invisible, immobile, onGround, usingItem atomic.Bool
usingSince atomic.Int64

glideTicks atomic.Int64
fireTicks atomic.Int64
blockingDelayTicks atomic.Int64
glideTicks atomic.Int64
fireTicks atomic.Int64

fallDistance atomic.Float64

breathing bool
Expand Down Expand Up @@ -581,8 +583,14 @@ func (p *Player) Hurt(dmg float64, src world.DamageSource) (float64, bool) {
if dmg < 0 {
return 0, true
}
w, pos := p.World(), p.Position()

totalDamage := p.FinalDamageFrom(dmg, src)
if ok, _ := p.Blocking(); ok && src.ReducedByArmour() {
if p.tryShieldBlock(dmg, totalDamage, src) {
return 0, false
}
}
damageLeft := totalDamage

if a := p.Absorption(); a > 0 {
Expand Down Expand Up @@ -628,7 +636,6 @@ func (p *Player) Hurt(dmg float64, src world.DamageSource) (float64, bool) {
}
}

w, pos := p.World(), p.Position()
for _, viewer := range p.viewers() {
viewer.ViewEntityAction(p, entity.HurtAction{})
}
Expand All @@ -645,6 +652,44 @@ func (p *Player) Hurt(dmg float64, src world.DamageSource) (float64, bool) {
return totalDamage, true
}

// tryShieldBlock tries to block an attack with a shield. If the attack was blocked, true is returned. If the
// attack was not blocked, false is returned.
func (p *Player) tryShieldBlock(dmg float64, totalDamage float64, src world.DamageSource) (affected bool) {
w, pos := p.World(), p.Position()

if src, ok := src.(entity.AttackDamageSource); ok {
diff := p.Position().Sub(src.Attacker.Position())
diff[1] = 0
if diff.Dot(p.Rotation().Vec3()) >= 0.0 {
return false
}
}
w.PlaySound(pos, sound.ShieldBlock{})
p.SetBlockingDelay(time.Millisecond * 250)

if src, ok := src.(entity.AttackDamageSource); ok {
if l, ok := src.Attacker.(entity.Living); ok {
l.KnockBack(pos, 0.5, 0.4)
}
if a, ok := src.Attacker.(*Player); ok {
held, _ := a.HeldItems()
if _, ok := held.Item().(item.Axe); ok {
p.SetBlockingDelay(time.Second * 5)
}
}
}
if dmg >= 3.0 {
i := int(math.Ceil(totalDamage))
held, other := p.HeldItems()
if _, ok := held.Item().(item.Shield); ok {
p.SetHeldItems(p.damageItem(held, i), other)
} else {
p.SetHeldItems(held, p.damageItem(other, i))
}
}
return true
}

// applyTotemEffects is an unexported function that is used to handle totem effects.
func (p *Player) applyTotemEffects() {
p.addHealth(2 - p.Health())
Expand Down Expand Up @@ -1081,6 +1126,34 @@ func (p *Player) StopFlying() {
p.session().SendGameMode(p.GameMode())
}

// Blocking returns true if the player is currently blocking with a shield. The first boolean is true if the player was
// holding a shield. The second boolean is true if the player was performing the necessary actions in order to block.
func (p *Player) Blocking() (holding bool, using bool) {
held, other := p.HeldItems()
_, heldShield := held.Item().(item.Shield)
_, otherShield := other.Item().(item.Shield)
if p.BlockingDelay() > 0 || !p.sneaking.Load() || p.usingItem.Load() {
return heldShield || otherShield, false
}
return heldShield || otherShield, true
}

// BlockingDelay returns the remaining duration of the player's shield blocking delay. If this value is larger than 0,
// they should not be able to use their shield.
func (p *Player) BlockingDelay() time.Duration {
return time.Duration(p.blockingDelayTicks.Load()) * time.Second / 20
}

// SetBlockingDelay updates the current blocking delay to the new duration provided. If the player is currently blocking
// with a shield, they will no longer be blocking and cannot do so until the duration is over.
func (p *Player) SetBlockingDelay(duration time.Duration) {
blocking, _ := p.Blocking()
p.blockingDelayTicks.Store(int64(duration.Seconds() * 20))
if blocking {
p.updateState()
}
}

// Jump makes the player jump if they are on ground. It exhausts the player by 0.05 food points, an additional 0.15
// is exhausted if the player is sprint jumping.
func (p *Player) Jump() {
Expand Down Expand Up @@ -1365,6 +1438,7 @@ func (p *Player) UseItem() {
// the item started being used.
func (p *Player) ReleaseItem() {
if !p.usingItem.CAS(true, false) || !p.canRelease() || !p.GameMode().AllowsInteraction() {
p.updateState()
return
}
ctx := p.useContext()
Expand Down Expand Up @@ -2298,6 +2372,12 @@ func (p *Player) Tick(w *world.World, current int64) {
p.Hurt(1, block.FireDamageSource{})
}
}
if p.BlockingDelay() > 0 {
p.blockingDelayTicks.Sub(1)
if p.BlockingDelay() <= 0 {
p.SetBlockingDelay(0)
}
}

if current%4 == 0 && p.usingItem.Load() {
held, _ := p.HeldItems()
Expand Down Expand Up @@ -2703,6 +2783,17 @@ func (p *Player) SwingArm() {
if p.Dead() {
return
}

duration := time.Millisecond * 300
if e, ok := p.Effect(effect.Haste{}); ok {
duration -= time.Duration(e.Level()) * time.Millisecond * 50
} else if e, ok = p.Effect(effect.MiningFatigue{}); ok {
duration += time.Duration(e.Level()*2) * time.Millisecond * 50
}
if duration > 0 {
p.SetBlockingDelay(duration)
}

for _, v := range p.viewers() {
v.ViewEntityAction(p, entity.SwingArmAction{})
}
Expand Down
9 changes: 9 additions & 0 deletions server/session/entity_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ func (s *Session) parseEntityMetadata(e world.Entity) protocol.EntityMetadata {

func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) {
if sn, ok := e.(sneaker); ok && sn.Sneaking() {
if b, ok := e.(blocker); ok {
if _, ok = b.Blocking(); ok {
m.SetFlag(protocol.EntityDataKeyFlagsTwo, protocol.EntityDataFlagBlocking%64)
}
}
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagSneaking)
}
if sp, ok := e.(sprinter); ok && sp.Sprinting() {
Expand Down Expand Up @@ -182,6 +187,10 @@ type glider interface {
Gliding() bool
}

type blocker interface {
Blocking() (bool, bool)
}

type breather interface {
Breathing() bool
AirSupply() time.Duration
Expand Down
2 changes: 2 additions & 0 deletions server/session/world.go
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,8 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool)
}
case sound.MusicDiscEnd:
pk.SoundType = packet.SoundEventRecordNull
case sound.ShieldBlock:
pk.SoundType = packet.SoundEventShieldBlock
case sound.FireCharge:
s.writePacket(&packet.LevelEvent{
EventType: packet.LevelEventSoundBlazeFireball,
Expand Down
3 changes: 3 additions & 0 deletions server/world/sound/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,8 @@ type GoatHorn struct {
// blaze shoots a fireball.
type FireCharge struct{ sound }

// ShieldBlock is a sound played when a player blocks an attack using a shield.
type ShieldBlock struct{ sound }

// Totem is a sound played when a player uses a totem.
type Totem struct{ sound }
Loading