package block import ( "math" "math/rand/v2" "time" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/block/cube/trace" "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/particle" "github.com/df-mc/dragonfly/server/world/sound" "github.com/go-gl/mathgl/mgl64" ) // ExplosionConfig is the configuration for an explosion. The world, position, size, sound, particle, and more can all // be configured through this configuration. type ExplosionConfig struct { // Size is the size of the explosion, it is effectively the radius which entities/blocks will be affected within. Size float64 // RandSource is the source to use for the explosion "randomness". If set // to nil, RandSource defaults to a `rand.PCG`source seeded with // `time.Now().UnixNano()`. RandSource rand.Source // SpawnFire will cause the explosion to randomly start fires in 1/3 of all destroyed air blocks that are // above opaque blocks. SpawnFire bool // ItemDropChance specifies how item drops should be handled. By default, // 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 // 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. Sound world.Sound // Particle is the particle to spawn when the explosion is created. If set to nil, this will default to the particle // of a regular huge explosion. Particle world.Particle } // ExplodableEntity represents an entity that can be exploded. type ExplodableEntity interface { // Explode is called when an explosion occurs. The entity can then react to the explosion using the configuration // and impact provided. Explode(explosionPos mgl64.Vec3, impact float64, c ExplosionConfig) } // Explodable represents a block that can be exploded. type Explodable interface { // Explode is called when an explosion occurs. The block can react to the explosion using the configuration passed. Explode(explosionPos mgl64.Vec3, pos cube.Pos, tx *world.Tx, c ExplosionConfig) } // rays ... var rays = make([]mgl64.Vec3, 0, 1352) // init ... func init() { for x := 0.0; x < 16; x++ { for y := 0.0; y < 16; y++ { for z := 0.0; z < 16; z++ { if x != 0 && x != 15 && y != 0 && y != 15 && z != 0 && z != 15 { continue } rays = append(rays, mgl64.Vec3{x/15*2 - 1, y/15*2 - 1, z/15*2 - 1}.Normalize().Mul(0.3)) } } } } // Explode performs the explosion as specified by the configuration. func (c ExplosionConfig) Explode(tx *world.Tx, explosionPos mgl64.Vec3) { if c.Sound == nil { c.Sound = sound.Explosion{} } if c.Particle == nil { c.Particle = particle.HugeExplosion{} } if c.RandSource == nil { t := uint64(time.Now().UnixNano()) c.RandSource = rand.NewPCG(t, t) } if c.Size == 0 { c.Size = 4 } if c.ItemDropChance == 0 { c.ItemDropChance = 1.0 / c.Size } r, d := rand.New(c.RandSource), c.Size*2 box := cube.Box( math.Floor(explosionPos[0]-d-1), math.Floor(explosionPos[1]-d-1), math.Floor(explosionPos[2]-d-1), math.Ceil(explosionPos[0]+d+1), math.Ceil(explosionPos[1]+d+1), math.Ceil(explosionPos[2]+d+1), ) affectedEntities := make([]world.Entity, 0, 32) for e := range tx.EntitiesWithin(box.Grow(2)) { pos := e.Position() dist := pos.Sub(explosionPos).Len() if dist > d || dist == 0 { continue } affectedEntities = append(affectedEntities, e) } affectedBlocks := make([]cube.Pos, 0, 32) for _, ray := range rays { pos := explosionPos for blastForce := c.Size * (0.7 + r.Float64()*0.6); blastForce > 0.0; blastForce -= 0.225 { current := cube.PosFromVec3(pos) currentBlock := tx.Block(current) resistance := 0.0 if l, ok := tx.Liquid(current); ok { resistance = l.BlastResistance() } else if i, ok := currentBlock.(Breakable); ok { resistance = i.BreakInfo().BlastResistance } else if _, ok = currentBlock.(Air); !ok { // Completely stop the ray if the current block is not air and unbreakable. break } pos = pos.Add(ray) if blastForce -= (resistance/5 + 0.3) * 0.3; blastForce > 0 { affectedBlocks = append(affectedBlocks, current) } } } ctx := event.C(tx) spawnFire := c.SpawnFire itemDropChance := c.ItemDropChance if tx.World().Handler().HandleExplosion(ctx, explosionPos, &affectedEntities, &affectedBlocks, &itemDropChance, &spawnFire); ctx.Cancelled() { return } for _, e := range affectedEntities { if explodable, ok := e.(ExplodableEntity); ok { impact := (1 - e.Position().Sub(explosionPos).Len()/d) * exposure(tx, explosionPos, e) explodable.Explode(explosionPos, impact, c) } } for _, pos := range affectedBlocks { bl := tx.Block(pos) if explodable, ok := bl.(Explodable); ok { explodable.Explode(explosionPos, pos, tx, c) } else if breakable, ok := bl.(Breakable); ok { // Clear the block first so break handlers see the post-break world, this is required by things such as redstone updates. tx.SetBlock(pos, nil, nil) breakHandler := breakable.BreakInfo().BreakHandler if breakHandler != nil { breakHandler(pos, tx, nil) } if itemDropChance > r.Float64() { for _, drop := range breakable.BreakInfo().Drops(item.ToolNone{}, nil) { dropItem(tx, drop, pos.Vec3Centre()) } } } } if spawnFire { for _, pos := range affectedBlocks { if r.IntN(3) == 0 { if _, ok := tx.Block(pos).(Air); ok && tx.Block(pos.Side(cube.FaceDown)).Model().FaceSolid(pos, cube.FaceUp, tx) { Fire{}.Start(tx, pos) } } } } tx.AddParticle(explosionPos, c.Particle) tx.PlaySound(explosionPos, c.Sound) } // exposure returns the exposure of an explosion to an entity, used to calculate the impact of an explosion. func exposure(tx *world.Tx, origin mgl64.Vec3, e world.Entity) float64 { pos := e.Position() box := e.H().Type().BBox(e).Translate(pos) boxMin, boxMax := box.Min(), box.Max() diff := boxMax.Sub(boxMin).Mul(2.0).Add(mgl64.Vec3{1, 1, 1}) step := mgl64.Vec3{1.0 / diff[0], 1.0 / diff[1], 1.0 / diff[2]} if step[0] < 0.0 || step[1] < 0.0 || step[2] < 0.0 { return 0.0 } xOffset := (1.0 - math.Floor(diff[0])/diff[0]) / 2.0 zOffset := (1.0 - math.Floor(diff[2])/diff[2]) / 2.0 var checks, misses float64 for x := 0.0; x <= 1.0; x += step[0] { for y := 0.0; y <= 1.0; y += step[1] { for z := 0.0; z <= 1.0; z += step[2] { point := mgl64.Vec3{ lerp(x, boxMin[0], boxMax[0]) + xOffset, lerp(y, boxMin[1], boxMax[1]), lerp(z, boxMin[2], boxMax[2]) + zOffset, } var collided bool trace.TraverseBlocks(origin, point, func(pos cube.Pos) (cont bool) { _, collided = trace.BlockIntercept(pos, tx, tx.Block(pos), origin, point) return !collided }) if !collided { misses++ } checks++ } } } return misses / checks } // lerp returns the linear interpolation between a and b at t. func lerp(a, b, t float64) float64 { return b + a*(t-b) }