Skip to content
Draft
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
4 changes: 4 additions & 0 deletions server/block/explosion.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ type ExplosionConfig struct {
// the item drop chance is 1/Size. If negative, no items will be dropped by
// the explosion. If set to 1 or higher, all items are dropped.
ItemDropChance float64
// UnblockableByShield specifies if the explosion damage should not be blockable by shields.
UnblockableByShield bool
// Source is the entity that caused the explosion, if known.
Source world.Entity

// Sound is the sound to play when the explosion is created. If set to nil, this will default to the sound of a
// regular explosion.
Expand Down
45 changes: 37 additions & 8 deletions server/block/tnt.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,31 @@ type TNT struct {
// ProjectileHit ...
func (t TNT) ProjectileHit(pos cube.Pos, tx *world.Tx, e world.Entity, _ cube.Face) {
if f, ok := e.(flammableEntity); ok && f.OnFireDuration() > 0 {
t.Ignite(pos, tx, nil)
spawnTnt(pos, tx, time.Second*4, tntIgnitionSourceHandle(e), true)
}
}

// Activate ...
func (t TNT) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, ctx *item.UseContext) bool {
held, _ := u.HeldItems()
if _, ok := held.Enchantment(enchantment.FireAspect); ok {
t.Ignite(pos, tx, nil)
t.Ignite(pos, tx, u)
ctx.DamageItem(1)
return true
}
return false
}

// Ignite ...
func (t TNT) Ignite(pos cube.Pos, tx *world.Tx, _ world.Entity) bool {
spawnTnt(pos, tx, time.Second*4)
func (t TNT) Ignite(pos cube.Pos, tx *world.Tx, source world.Entity) bool {
sourceHandle := entityHandle(source)
spawnTnt(pos, tx, time.Second*4, sourceHandle, true)
return true
}

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

// BreakInfo ...
Expand All @@ -66,9 +67,37 @@ func (t TNT) EncodeBlock() (name string, properties map[string]interface{}) {
}

// spawnTnt creates a new TNT entity at the given position with the given fuse duration.
func spawnTnt(pos cube.Pos, tx *world.Tx, fuse time.Duration) {
type ownerEntity interface {
ProjectileOwner() *world.EntityHandle
}

func tntIgnitionSourceHandle(source world.Entity) *world.EntityHandle {
if source == nil {
return nil
}
if o, ok := source.(ownerEntity); ok {
if owner := o.ProjectileOwner(); owner != nil {
return owner
}
}
return source.H()
}

func tntExplosionSourceHandle(c ExplosionConfig) *world.EntityHandle {
return entityHandle(c.Source)
}

func entityHandle(e world.Entity) *world.EntityHandle {
if e == nil {
return nil
}
return e.H()
}

func spawnTnt(pos cube.Pos, tx *world.Tx, fuse time.Duration, source *world.EntityHandle, blockableByShield bool) {
tx.PlaySound(pos.Vec3Centre(), sound.TNT{})
tx.SetBlock(pos, nil, nil)
opts := world.EntitySpawnOpts{Position: pos.Vec3Centre()}
tx.AddEntity(tx.World().EntityRegistry().Config().TNT(opts, fuse))
conf := tx.World().EntityRegistry().Config()
tx.AddEntity(conf.TNTWithSource(opts, fuse, source, blockableByShield))
}
202 changes: 202 additions & 0 deletions server/block/tnt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package block

import (
"testing"
"time"

"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
)

type tntSourceEntity struct {
h *world.EntityHandle
owner *world.EntityHandle
pos mgl64.Vec3
}

func (e tntSourceEntity) Close() error { return nil }
func (e tntSourceEntity) H() *world.EntityHandle { return e.h }
func (e tntSourceEntity) Position() mgl64.Vec3 { return e.pos }
func (e tntSourceEntity) Rotation() cube.Rotation {
return cube.Rotation{}
}
func (e tntSourceEntity) ProjectileOwner() *world.EntityHandle { return e.owner }

type tntTestEntityType struct{}

func (tntTestEntityType) Open(_ *world.Tx, h *world.EntityHandle, data *world.EntityData) world.Entity {
return tntSourceEntity{h: h}.withPosition(data.Pos)
}
func (tntTestEntityType) EncodeEntity() string { return "dragonfly:test_entity" }
func (tntTestEntityType) BBox(world.Entity) cube.BBox { return cube.Box(0, 0, 0, 0, 0, 0) }
func (tntTestEntityType) DecodeNBT(map[string]any, *world.EntityData) {}
func (tntTestEntityType) EncodeNBT(*world.EntityData) map[string]any { return nil }
func (tntTestEntityType) Apply(data *world.EntityData) {}

func (e tntSourceEntity) withPosition(pos mgl64.Vec3) tntSourceEntity {
e.pos = pos
return e
}
func newTNTTestHandle() *world.EntityHandle {
return world.EntitySpawnOpts{}.New(tntTestEntityType{}, tntTestEntityType{})
}

func TestTNTIgnitionSourcePrefersProjectileOwner(t *testing.T) {
owner := newTNTTestHandle()
projectile := tntSourceEntity{h: newTNTTestHandle(), owner: owner}

if got := tntIgnitionSourceHandle(projectile); got != owner {
t.Fatalf("expected TNT ignition source to use projectile owner handle %v, got %v", owner, got)
}
}

func TestTNTIgnitionSourceFallsBackToIgnitingEntity(t *testing.T) {
projectile := tntSourceEntity{h: newTNTTestHandle()}

if got := tntIgnitionSourceHandle(projectile); got != projectile.H() {
t.Fatalf("expected TNT ignition source to fall back to igniting entity handle %v, got %v", projectile.H(), got)
}
}

func TestTNTExplosionSourceUsesExplosionConfigSource(t *testing.T) {
source := tntSourceEntity{h: newTNTTestHandle()}

if got := tntExplosionSourceHandle(ExplosionConfig{Source: source}); got != source.H() {
t.Fatalf("expected chained TNT source to use explosion config source handle %v, got %v", source.H(), got)
}
}

func TestTNTSpawnCanCreateShieldBlockableTNTWithoutSource(t *testing.T) {
var blockable bool
var source *world.EntityHandle
registry := world.EntityRegistryConfig{
TNT: func(opts world.EntitySpawnOpts, fuse time.Duration) *world.EntityHandle {
return opts.New(tntTestEntityType{}, tntTestEntityType{})
},
TNTWithSource: func(opts world.EntitySpawnOpts, fuse time.Duration, src *world.EntityHandle, blockableByShield bool) *world.EntityHandle {
source, blockable = src, blockableByShield
return opts.New(tntTestEntityType{}, tntTestEntityType{})
},
}.New([]world.EntityType{tntTestEntityType{}})
w := world.Config{Entities: registry}.New()
defer func() {
_ = w.Close()
}()

<-w.Exec(func(tx *world.Tx) {
spawnTnt(cube.Pos{}, tx, time.Second, nil, true)
})
if source != nil {
t.Fatalf("expected no TNT source entity, got %T", source)
}
if !blockable {
t.Fatal("expected source-less environmental TNT to be shield blockable")
}
}

func TestTNTIgniteWithoutSourceIsShieldBlockable(t *testing.T) {
var blockable bool
var source *world.EntityHandle
registry := world.EntityRegistryConfig{
TNT: func(opts world.EntitySpawnOpts, fuse time.Duration) *world.EntityHandle {
return opts.New(tntTestEntityType{}, tntTestEntityType{})
},
TNTWithSource: func(opts world.EntitySpawnOpts, fuse time.Duration, src *world.EntityHandle, blockableByShield bool) *world.EntityHandle {
source, blockable = src, blockableByShield
return opts.New(tntTestEntityType{}, tntTestEntityType{})
},
}.New([]world.EntityType{tntTestEntityType{}})
w := world.Config{Entities: registry}.New()
defer func() {
_ = w.Close()
}()

<-w.Exec(func(tx *world.Tx) {
TNT{}.Ignite(cube.Pos{}, tx, nil)
})
if source != nil {
t.Fatalf("expected no TNT source entity, got %T", source)
}
if !blockable {
t.Fatal("expected source-less TNT ignition to be shield-blockable")
}
}

func TestTNTIgniteWithSourceIsShieldBlockable(t *testing.T) {
source := tntSourceEntity{h: newTNTTestHandle()}
var blockable bool
var gotSource *world.EntityHandle
registry := world.EntityRegistryConfig{
TNT: func(opts world.EntitySpawnOpts, fuse time.Duration) *world.EntityHandle {
return opts.New(tntTestEntityType{}, tntTestEntityType{})
},
TNTWithSource: func(opts world.EntitySpawnOpts, fuse time.Duration, src *world.EntityHandle, blockableByShield bool) *world.EntityHandle {
gotSource, blockable = src, blockableByShield
return opts.New(tntTestEntityType{}, tntTestEntityType{})
},
}.New([]world.EntityType{tntTestEntityType{}})
w := world.Config{Entities: registry}.New()
defer func() {
_ = w.Close()
}()

<-w.Exec(func(tx *world.Tx) {
TNT{}.Ignite(cube.Pos{}, tx, source)
})
if gotSource != source.H() {
t.Fatalf("expected TNT source entity %v, got %v", source.H(), gotSource)
}
if !blockable {
t.Fatal("expected source-aware TNT ignition to stay shield-blockable for other players")
}
}

func TestTNTSpawnPreservesUnavailableSourceHandle(t *testing.T) {
wantSource := newTNTTestHandle()
var gotSource *world.EntityHandle
registry := world.EntityRegistryConfig{
TNT: func(opts world.EntitySpawnOpts, fuse time.Duration) *world.EntityHandle {
return opts.New(tntTestEntityType{}, tntTestEntityType{})
},
TNTWithSource: func(opts world.EntitySpawnOpts, fuse time.Duration, src *world.EntityHandle, blockableByShield bool) *world.EntityHandle {
gotSource = src
return opts.New(tntTestEntityType{}, tntTestEntityType{})
},
}.New([]world.EntityType{tntTestEntityType{}})
w := world.Config{Entities: registry}.New()
defer func() {
_ = w.Close()
}()

<-w.Exec(func(tx *world.Tx) {
spawnTnt(cube.Pos{}, tx, time.Second, wantSource, true)
})
if gotSource != wantSource {
t.Fatalf("expected unavailable source handle %v to be passed through, got %v", wantSource, gotSource)
}
}

func TestTNTSpawnPreservesSourceLessUnblockableExplosion(t *testing.T) {
var blockable bool
registry := world.EntityRegistryConfig{
TNT: func(opts world.EntitySpawnOpts, fuse time.Duration) *world.EntityHandle {
return opts.New(tntTestEntityType{}, tntTestEntityType{})
},
TNTWithSource: func(opts world.EntitySpawnOpts, fuse time.Duration, src *world.EntityHandle, blockableByShield bool) *world.EntityHandle {
blockable = blockableByShield
return opts.New(tntTestEntityType{}, tntTestEntityType{})
},
}.New([]world.EntityType{tntTestEntityType{}})
w := world.Config{Entities: registry}.New()
defer func() {
_ = w.Close()
}()

<-w.Exec(func(tx *world.Tx) {
TNT{}.Explode(cube.Pos{}.Vec3Centre(), cube.Pos{}, tx, ExplosionConfig{UnblockableByShield: true})
})
if blockable {
t.Fatal("expected source-less unblockable explosion to prime shield-unblockable TNT")
}
}
12 changes: 11 additions & 1 deletion server/entity/damage.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/enchantment"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
)

type (
Expand Down Expand Up @@ -45,7 +46,16 @@ type (
}

// ExplosionDamageSource is used for damage caused by an explosion.
ExplosionDamageSource struct{}
ExplosionDamageSource struct {
// Origin is the position from which the explosion damage originated.
Origin mgl64.Vec3
// HasOrigin is true if Origin is a meaningful explosion source position.
HasOrigin bool
// BlockableByShield is true if the explosion damage may be blocked by a shield.
BlockableByShield bool
// Source is the entity that caused the explosion, if known.
Source world.Entity
}
)

func (FallDamageSource) ReducedByArmour() bool { return false }
Expand Down
15 changes: 15 additions & 0 deletions server/entity/ent.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ func (e *Ent) Behaviour() Behaviour {
return e.data.Data.(Behaviour)
}

// ProjectileOwner returns the entity that owns this Ent, if it has projectile behaviour.
func (e *Ent) ProjectileOwner() *world.EntityHandle {
if projectile, ok := e.Behaviour().(*ProjectileBehaviour); ok {
return projectile.Owner()
}
return nil
}

// MarkShieldBlocked marks the Ent's behaviour as shield-blocked if it supports projectile shield block state.
func (e *Ent) MarkShieldBlocked() {
if marker, ok := e.Behaviour().(interface{ MarkShieldBlocked() }); ok {
marker.MarkShieldBlocked()
}
}

// Explode propagates the explosion behaviour of the underlying Behaviour.
func (e *Ent) Explode(src mgl64.Vec3, impact float64, conf block.ExplosionConfig) {
if expl, ok := e.Behaviour().(interface {
Expand Down
Loading