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 #643

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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 @@ -125,6 +126,13 @@ func (lt *ProjectileBehaviour) Critical() bool {
return lt.conf.Critical && !lt.collided
}

// 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)
}

// Tick runs the tick-based behaviour of a ProjectileBehaviour and returns the
// Movement within the tick. Tick handles the movement, collision and hitting
// of a projectile.
Expand Down Expand Up @@ -164,8 +172,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
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
}
102 changes: 96 additions & 6 deletions server/player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,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 @@ -575,9 +577,14 @@ func (p *Player) Hurt(dmg float64, src world.DamageSource) (float64, bool) {
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 {
if damageLeft > a {
p.SetAbsorption(0)
Expand Down Expand Up @@ -606,7 +613,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 @@ -623,6 +629,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
}

// FinalDamageFrom resolves the final damage received by the player if it is attacked by the source passed
// with the damage passed. FinalDamageFrom takes into account things such as the armour worn and the
// enchantments on the individual pieces.
Expand Down Expand Up @@ -929,8 +973,8 @@ func (p *Player) StopSprinting() {
if !p.sprinting.CAS(true, false) {
return
}
p.SetSpeed(p.Speed() / 1.3)

p.SetSpeed(p.Speed() / 1.3)
p.updateState()
}

Expand Down Expand Up @@ -1040,6 +1084,18 @@ 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.
TwistedAsylumMC marked this conversation as resolved.
Show resolved Hide resolved
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
}

// 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 @@ -1125,6 +1181,12 @@ func (p *Player) OnFireDuration() time.Duration {
return time.Duration(p.fireTicks.Load()) * time.Second / 20
}

// 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
}

// SetOnFire ...
func (p *Player) SetOnFire(duration time.Duration) {
ticks := int64(duration.Seconds() * 20)
Expand All @@ -1135,6 +1197,16 @@ func (p *Player) SetOnFire(duration time.Duration) {
p.updateState()
}

// 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()
}
}

// Extinguish ...
func (p *Player) Extinguish() {
p.SetOnFire(0)
Expand Down Expand Up @@ -1309,11 +1381,11 @@ func (p *Player) UseItem() {
return
}
p.SetHeldItems(p.subtractItem(i, 1), left)
w.PlaySound(p.Position().Add(mgl64.Vec3{0, 1.5}), sound.Burp{})

useCtx := p.useContext()
useCtx.NewItem = usable.Consume(w, p)
p.addNewItem(useCtx)
w.PlaySound(p.Position().Add(mgl64.Vec3{0, 1.5}), sound.Burp{})
}
}

Expand All @@ -1324,6 +1396,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 @@ -2256,6 +2329,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 @@ -2640,6 +2719,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 @@ -43,6 +43,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() {
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagSneaking)
if b, ok := e.(blocker); ok {
if _, ok = b.Blocking(); ok {
m.SetFlag(protocol.EntityDataKeyFlagsTwo, protocol.EntityDataFlagBlocking%64)
}
}
}
if sp, ok := e.(sprinter); ok && sp.Sprinting() {
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagSprinting)
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 @@ -744,6 +744,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 @@ -70,3 +70,6 @@ type GoatHorn struct {
// FireCharge is a sound played when a player lights a block on fire with a fire charge, or when a dispenser or a
// 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 }
Loading