Skip to content

Commit 0e8dae4

Browse files
committed
Implement vanilla shield blocking
1 parent a2f6175 commit 0e8dae4

21 files changed

Lines changed: 1038 additions & 28 deletions

server/block/explosion.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ type ExplosionConfig struct {
3030
// the item drop chance is 1/Size. If negative, no items will be dropped by
3131
// the explosion. If set to 1 or higher, all items are dropped.
3232
ItemDropChance float64
33+
// UnblockableByShield specifies if the explosion damage should not be blockable by shields.
34+
UnblockableByShield bool
35+
// Source is the entity that caused the explosion, if known.
36+
Source world.Entity
3337

3438
// Sound is the sound to play when the explosion is created. If set to nil, this will default to the sound of a
3539
// regular explosion.

server/block/tnt.go

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,30 @@ type TNT struct {
1919
// ProjectileHit ...
2020
func (t TNT) ProjectileHit(pos cube.Pos, tx *world.Tx, e world.Entity, _ cube.Face) {
2121
if f, ok := e.(flammableEntity); ok && f.OnFireDuration() > 0 {
22-
t.Ignite(pos, tx, nil)
22+
spawnTnt(pos, tx, time.Second*4, tntIgnitionSourceHandle(e))
2323
}
2424
}
2525

2626
// Activate ...
2727
func (t TNT) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, ctx *item.UseContext) bool {
2828
held, _ := u.HeldItems()
2929
if _, ok := held.Enchantment(enchantment.FireAspect); ok {
30-
t.Ignite(pos, tx, nil)
30+
t.Ignite(pos, tx, u)
3131
ctx.DamageItem(1)
3232
return true
3333
}
3434
return false
3535
}
3636

3737
// Ignite ...
38-
func (t TNT) Ignite(pos cube.Pos, tx *world.Tx, _ world.Entity) bool {
39-
spawnTnt(pos, tx, time.Second*4)
38+
func (t TNT) Ignite(pos cube.Pos, tx *world.Tx, source world.Entity) bool {
39+
spawnTnt(pos, tx, time.Second*4, entityHandle(source))
4040
return true
4141
}
4242

4343
// Explode ...
44-
func (t TNT) Explode(_ mgl64.Vec3, pos cube.Pos, tx *world.Tx, _ ExplosionConfig) {
45-
spawnTnt(pos, tx, time.Second/2+time.Duration(rand.IntN(int(time.Second+time.Second/2))))
44+
func (t TNT) Explode(_ mgl64.Vec3, pos cube.Pos, tx *world.Tx, c ExplosionConfig) {
45+
spawnTnt(pos, tx, time.Second/2+time.Duration(rand.IntN(int(time.Second+time.Second/2))), tntExplosionSourceHandle(c))
4646
}
4747

4848
// BreakInfo ...
@@ -66,9 +66,43 @@ func (t TNT) EncodeBlock() (name string, properties map[string]interface{}) {
6666
}
6767

6868
// spawnTnt creates a new TNT entity at the given position with the given fuse duration.
69-
func spawnTnt(pos cube.Pos, tx *world.Tx, fuse time.Duration) {
69+
type ownerEntity interface {
70+
ProjectileOwner() *world.EntityHandle
71+
}
72+
73+
func tntIgnitionSourceHandle(source world.Entity) *world.EntityHandle {
74+
if source == nil {
75+
return nil
76+
}
77+
if o, ok := source.(ownerEntity); ok {
78+
if owner := o.ProjectileOwner(); owner != nil {
79+
return owner
80+
}
81+
}
82+
return source.H()
83+
}
84+
85+
func tntExplosionSourceHandle(c ExplosionConfig) *world.EntityHandle {
86+
return entityHandle(c.Source)
87+
}
88+
89+
func entityHandle(e world.Entity) *world.EntityHandle {
90+
if e == nil {
91+
return nil
92+
}
93+
return e.H()
94+
}
95+
96+
func spawnTnt(pos cube.Pos, tx *world.Tx, fuse time.Duration, source *world.EntityHandle) {
7097
tx.PlaySound(pos.Vec3Centre(), sound.TNT{})
7198
tx.SetBlock(pos, nil, nil)
7299
opts := world.EntitySpawnOpts{Position: pos.Vec3Centre()}
73-
tx.AddEntity(tx.World().EntityRegistry().Config().TNT(opts, fuse))
100+
conf := tx.World().EntityRegistry().Config()
101+
if source != nil && conf.TNTWithSource != nil {
102+
if e, ok := source.Entity(tx); ok {
103+
tx.AddEntity(conf.TNTWithSource(opts, fuse, e))
104+
return
105+
}
106+
}
107+
tx.AddEntity(conf.TNT(opts, fuse))
74108
}

server/block/tnt_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package block
2+
3+
import (
4+
"testing"
5+
6+
"github.com/df-mc/dragonfly/server/block/cube"
7+
"github.com/df-mc/dragonfly/server/world"
8+
"github.com/go-gl/mathgl/mgl64"
9+
)
10+
11+
type tntSourceEntity struct {
12+
h *world.EntityHandle
13+
owner *world.EntityHandle
14+
}
15+
16+
func (e tntSourceEntity) Close() error { return nil }
17+
func (e tntSourceEntity) H() *world.EntityHandle { return e.h }
18+
func (e tntSourceEntity) Position() mgl64.Vec3 { return mgl64.Vec3{} }
19+
func (e tntSourceEntity) Rotation() cube.Rotation {
20+
return cube.Rotation{}
21+
}
22+
func (e tntSourceEntity) ProjectileOwner() *world.EntityHandle { return e.owner }
23+
24+
type tntTestEntityType struct{}
25+
26+
func (tntTestEntityType) Open(*world.Tx, *world.EntityHandle, *world.EntityData) world.Entity {
27+
return nil
28+
}
29+
func (tntTestEntityType) EncodeEntity() string { return "dragonfly:test_entity" }
30+
func (tntTestEntityType) BBox(world.Entity) cube.BBox { return cube.Box(0, 0, 0, 0, 0, 0) }
31+
func (tntTestEntityType) DecodeNBT(map[string]any, *world.EntityData) {}
32+
func (tntTestEntityType) EncodeNBT(*world.EntityData) map[string]any { return nil }
33+
func (tntTestEntityType) Apply(data *world.EntityData) {}
34+
func newTNTTestHandle() *world.EntityHandle {
35+
return world.EntitySpawnOpts{}.New(tntTestEntityType{}, tntTestEntityType{})
36+
}
37+
38+
func TestTNTIgnitionSourcePrefersProjectileOwner(t *testing.T) {
39+
owner := newTNTTestHandle()
40+
projectile := tntSourceEntity{h: newTNTTestHandle(), owner: owner}
41+
42+
if got := tntIgnitionSourceHandle(projectile); got != owner {
43+
t.Fatalf("expected TNT ignition source to use projectile owner handle %v, got %v", owner, got)
44+
}
45+
}
46+
47+
func TestTNTIgnitionSourceFallsBackToIgnitingEntity(t *testing.T) {
48+
projectile := tntSourceEntity{h: newTNTTestHandle()}
49+
50+
if got := tntIgnitionSourceHandle(projectile); got != projectile.H() {
51+
t.Fatalf("expected TNT ignition source to fall back to igniting entity handle %v, got %v", projectile.H(), got)
52+
}
53+
}
54+
55+
func TestTNTExplosionSourceUsesExplosionConfigSource(t *testing.T) {
56+
source := tntSourceEntity{h: newTNTTestHandle()}
57+
58+
if got := tntExplosionSourceHandle(ExplosionConfig{Source: source}); got != source.H() {
59+
t.Fatalf("expected chained TNT source to use explosion config source handle %v, got %v", source.H(), got)
60+
}
61+
}

server/entity/damage.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"github.com/df-mc/dragonfly/server/item"
55
"github.com/df-mc/dragonfly/server/item/enchantment"
66
"github.com/df-mc/dragonfly/server/world"
7+
"github.com/go-gl/mathgl/mgl64"
78
)
89

910
type (
@@ -42,10 +43,21 @@ type (
4243
// Projectile and Owner are the world.Entity that dealt the damage and
4344
// the one that fired the projectile respectively.
4445
Projectile, Owner world.Entity
46+
// OnShieldBlock is called if the projectile damage is blocked by a shield.
47+
OnShieldBlock func()
4548
}
4649

4750
// ExplosionDamageSource is used for damage caused by an explosion.
48-
ExplosionDamageSource struct{}
51+
ExplosionDamageSource struct {
52+
// Origin is the position from which the explosion damage originated.
53+
Origin mgl64.Vec3
54+
// HasOrigin is true if Origin is a meaningful explosion source position.
55+
HasOrigin bool
56+
// BlockableByShield is true if the explosion damage may be blocked by a shield.
57+
BlockableByShield bool
58+
// Source is the entity that caused the explosion, if known.
59+
Source world.Entity
60+
}
4961
)
5062

5163
func (FallDamageSource) ReducedByArmour() bool { return false }

server/entity/ent.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ func (e *Ent) Behaviour() Behaviour {
4040
return e.data.Data.(Behaviour)
4141
}
4242

43+
// ProjectileOwner returns the entity that owns this Ent, if its Behaviour tracks one.
44+
func (e *Ent) ProjectileOwner() *world.EntityHandle {
45+
if owner, ok := e.Behaviour().(interface{ Owner() *world.EntityHandle }); ok {
46+
return owner.Owner()
47+
}
48+
return nil
49+
}
50+
4351
// Explode propagates the explosion behaviour of the underlying Behaviour.
4452
func (e *Ent) Explode(src mgl64.Vec3, impact float64, conf block.ExplosionConfig) {
4553
if expl, ok := e.Behaviour().(interface {

server/entity/projectile.go

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,13 @@ func (lt *ProjectileBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
166166
tx.PlaySound(result.Position(), lt.conf.Sound)
167167
}
168168

169+
deflected := false
169170
switch r := result.(type) {
170171
case trace.EntityResult:
171172
if l, ok := r.Entity().(Living); ok && lt.conf.Damage >= 0 {
172-
lt.hitEntity(l, e, vel)
173+
if lt.hitEntity(l, e, vel) {
174+
deflected = true
175+
}
173176
}
174177
case trace.BlockResult:
175178
bpos := r.BlockPosition()
@@ -184,6 +187,13 @@ func (lt *ProjectileBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
184187
if lt.conf.Hit != nil {
185188
lt.conf.Hit(e, tx, result)
186189
}
190+
if deflected {
191+
m.pos = e.Position()
192+
m.vel = e.Velocity()
193+
m.dpos = m.pos.Sub(result.Position())
194+
m.dvel = m.vel.Sub(vel)
195+
return m
196+
}
187197

188198
lt.close = true
189199
return m
@@ -257,14 +267,21 @@ func (lt *ProjectileBehaviour) hitBlockSurviving(e *Ent, r trace.BlockResult, m
257267
// hitEntity is called when a projectile hits a Living. It deals damage to the
258268
// entity and knocks it back. Additionally, it applies any potion effects and
259269
// fire if applicable.
260-
func (lt *ProjectileBehaviour) hitEntity(l Living, e *Ent, vel mgl64.Vec3) {
261-
owner, _ := lt.conf.Owner.Entity(e.tx)
262-
src := ProjectileDamageSource{Projectile: e, Owner: owner}
270+
func (lt *ProjectileBehaviour) hitEntity(l Living, e *Ent, vel mgl64.Vec3) bool {
271+
var owner world.Entity
272+
if lt.conf.Owner != nil {
273+
owner, _ = lt.conf.Owner.Entity(e.tx)
274+
}
275+
blocked := false
276+
src := ProjectileDamageSource{Projectile: e, Owner: owner, OnShieldBlock: func() { blocked = true }}
263277
dmg := math.Ceil(lt.conf.Damage * vel.Len())
264278
if lt.conf.Critical {
265279
dmg += rand.Float64() * dmg / 2
266280
}
267-
if _, vulnerable := l.Hurt(dmg, src); vulnerable {
281+
if _, vulnerable := l.Hurt(dmg, src); blocked {
282+
lt.deflect(e, vel)
283+
return true
284+
} else if vulnerable {
268285
l.KnockBack(l.Position().Sub(vel), 0.45+lt.conf.KnockBackForceAddend, 0.3608+lt.conf.KnockBackHeightAddend)
269286

270287
for _, eff := range lt.conf.Potion.Effects() {
@@ -278,6 +295,16 @@ func (lt *ProjectileBehaviour) hitEntity(l Living, e *Ent, vel mgl64.Vec3) {
278295
flammable.SetOnFire(time.Second * 5)
279296
}
280297
}
298+
return false
299+
}
300+
301+
func (lt *ProjectileBehaviour) deflect(e *Ent, vel mgl64.Vec3) {
302+
if vel.Len() == 0 {
303+
return
304+
}
305+
reflected := vel.Mul(-1)
306+
e.SetVelocity(reflected)
307+
e.data.Pos = e.Position().Add(reflected.Normalize().Mul(0.05))
281308
}
282309

283310
// tickMovement ticks the movement of a projectile. It updates the position and

server/entity/projectile_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package entity
2+
3+
import (
4+
"testing"
5+
6+
"github.com/df-mc/dragonfly/server/block/cube"
7+
"github.com/df-mc/dragonfly/server/entity/effect"
8+
"github.com/df-mc/dragonfly/server/world"
9+
"github.com/go-gl/mathgl/mgl64"
10+
)
11+
12+
type projectileShieldTarget struct {
13+
pos mgl64.Vec3
14+
blocked bool
15+
vulnerable bool
16+
}
17+
18+
func (t *projectileShieldTarget) Close() error { return nil }
19+
func (t *projectileShieldTarget) H() *world.EntityHandle { return nil }
20+
func (t *projectileShieldTarget) Position() mgl64.Vec3 { return t.pos }
21+
func (t *projectileShieldTarget) Rotation() cube.Rotation { return cube.Rotation{} }
22+
func (t *projectileShieldTarget) Health() float64 { return 20 }
23+
func (t *projectileShieldTarget) MaxHealth() float64 { return 20 }
24+
func (t *projectileShieldTarget) SetMaxHealth(float64) {}
25+
func (t *projectileShieldTarget) Dead() bool { return false }
26+
func (t *projectileShieldTarget) Heal(float64, world.HealingSource) {}
27+
func (t *projectileShieldTarget) KnockBack(mgl64.Vec3, float64, float64) {}
28+
func (t *projectileShieldTarget) Velocity() mgl64.Vec3 { return mgl64.Vec3{} }
29+
func (t *projectileShieldTarget) SetVelocity(mgl64.Vec3) {}
30+
func (t *projectileShieldTarget) AddEffect(effect.Effect) {}
31+
func (t *projectileShieldTarget) RemoveEffect(effect.Type) {}
32+
func (t *projectileShieldTarget) Effects() []effect.Effect { return nil }
33+
func (t *projectileShieldTarget) Speed() float64 { return 0 }
34+
func (t *projectileShieldTarget) SetSpeed(float64) {}
35+
36+
func (t *projectileShieldTarget) Hurt(_ float64, src world.DamageSource) (float64, bool) {
37+
if s, ok := src.(ProjectileDamageSource); ok && t.blocked && s.OnShieldBlock != nil {
38+
s.OnShieldBlock()
39+
}
40+
return 0, t.vulnerable
41+
}
42+
43+
func TestProjectileDeflectsAfterShieldBlock(t *testing.T) {
44+
pos := mgl64.Vec3{0, 0, 1}
45+
projectile := &Ent{data: &world.EntityData{Pos: pos}}
46+
behaviour := &ProjectileBehaviour{conf: ProjectileBehaviourConfig{Damage: 2}}
47+
velocity := mgl64.Vec3{0, 0, -1}
48+
49+
blocked := behaviour.hitEntity(&projectileShieldTarget{blocked: true}, projectile, velocity)
50+
if !blocked {
51+
t.Fatal("expected shield-blocked projectile hit to be handled as a deflection")
52+
}
53+
if got, want := projectile.Velocity(), velocity.Mul(-1); got != want {
54+
t.Fatalf("expected deflected projectile velocity %v, got %v", want, got)
55+
}
56+
if !projectile.Position().Sub(pos).Normalize().ApproxEqual(projectile.Velocity().Normalize()) {
57+
t.Fatalf("expected projectile to move away from blocker after deflection, position changed from %v to %v with velocity %v", pos, projectile.Position(), projectile.Velocity())
58+
}
59+
}
60+
61+
func TestProjectileDeflectsZeroDamageShieldBlock(t *testing.T) {
62+
pos := mgl64.Vec3{0, 0, 1}
63+
projectile := &Ent{data: &world.EntityData{Pos: pos}}
64+
behaviour := &ProjectileBehaviour{conf: ProjectileBehaviourConfig{Damage: 0}}
65+
velocity := mgl64.Vec3{0, 0, -1}
66+
67+
blocked := behaviour.hitEntity(&projectileShieldTarget{blocked: true}, projectile, velocity)
68+
if !blocked {
69+
t.Fatal("expected zero damage shield-blocked projectile hit to be handled as a deflection")
70+
}
71+
if got, want := projectile.Velocity(), velocity.Mul(-1); got != want {
72+
t.Fatalf("expected deflected projectile velocity %v, got %v", want, got)
73+
}
74+
}

server/entity/register.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var DefaultRegistry = conf.New([]world.EntityType{
2929

3030
var conf = world.EntityRegistryConfig{
3131
TNT: NewTNT,
32+
TNTWithSource: NewTNTWithSource,
3233
Egg: NewEgg,
3334
Snowball: NewSnowball,
3435
BottleOfEnchanting: NewBottleOfEnchanting,

0 commit comments

Comments
 (0)