github.com/df-mc/dragonfly@v0.9.13/server/entity/projectile.go (about) 1 package entity 2 3 import ( 4 "github.com/df-mc/dragonfly/server/block" 5 "github.com/df-mc/dragonfly/server/block/cube" 6 "github.com/df-mc/dragonfly/server/block/cube/trace" 7 "github.com/df-mc/dragonfly/server/item" 8 "github.com/df-mc/dragonfly/server/item/potion" 9 "github.com/df-mc/dragonfly/server/world" 10 "github.com/go-gl/mathgl/mgl64" 11 "math" 12 "math/rand" 13 "time" 14 ) 15 16 // ProjectileBehaviourConfig allows the configuration of projectiles. Calling 17 // ProjectileBehaviourConfig.New() creates a ProjectileBehaviour using these 18 // settings. 19 type ProjectileBehaviourConfig struct { 20 // Gravity is the amount of Y velocity subtracted every tick. 21 Gravity float64 22 // Drag is used to reduce all axes of the velocity every tick. Velocity is 23 // multiplied with (1-Drag) every tick. 24 Drag float64 25 // Damage specifies the base damage dealt by the Projectile. If set to a 26 // negative number, entities hit are not hurt at all and are not knocked 27 // back. The base damage is multiplied with the velocity of the projectile 28 // to calculate the final damage of the projectile. 29 Damage float64 30 // Potion is the potion effect that is applied to an entity when the 31 // projectile hits it. 32 Potion potion.Potion 33 // KnockBackForceAddend is the additional horizontal velocity that is 34 // applied to an entity when it is hit by the projectile. 35 KnockBackForceAddend float64 36 // KnockBackHeightAddend is the additional vertical velocity that is applied 37 // to an entity when it is hit by the projectile. 38 KnockBackHeightAddend float64 39 // Particle is a particle that is spawned when the projectile hits a 40 // target, either a block or an entity. No particle is spawned if left nil. 41 Particle world.Particle 42 // ParticleCount is the amount of particles that should be spawned if 43 // Particle is not nil. ParticleCount will be set to 1 if Particle is not 44 // nil and ParticleCount is 0. 45 ParticleCount int 46 // Sound is a sound that is played when the projectile hits a target, either 47 // a block or an entity. No sound is played if left nil. 48 Sound world.Sound 49 // Critical specifies if the projectile is critical. This spawns critical 50 // hit particles behind the projectile and causes it to deal up to 50% more 51 // damage. 52 Critical bool 53 // Hit is a function that is called when the projectile Ent hits a target 54 // (the trace.Result). The target is either of the type trace.EntityResult 55 // or trace.BlockResult. Hit may be set to run additional behaviour when a 56 // projectile hits a target. 57 Hit func(e *Ent, target trace.Result) 58 // SurviveBlockCollision specifies if a projectile with this 59 // ProjectileBehaviour should survive collision with a block. If set to 60 // false, the projectile will break when hitting a block (like a snowball). 61 // If set to true, the projectile will survive like an arrow does. 62 SurviveBlockCollision bool 63 // BlockCollisionVelocityMultiplier is the multiplier used to modify the 64 // velocity of a projectile that has SurviveBlockCollision set to true. The 65 // default, 0, will cause the projectile to lose its velocity completely. A 66 // multiplier such as 0.5 will reduce the projectile's velocity, but retain 67 // half of it after inverting the axis on which the projectile collided. 68 BlockCollisionVelocityMultiplier float64 69 // DisablePickup specifies if picking up the projectile should be disabled, 70 // which is relevant in the case SurviveBlockCollision is set to true. Some 71 // projectiles, such as arrows, cannot be picked up if they are shot by 72 // monsters like skeletons. 73 DisablePickup bool 74 // PickupItem is the item that is given to a player when it picks up this 75 // projectile. If left as an empty item.Stack, no item is given upon pickup. 76 PickupItem item.Stack 77 } 78 79 // New creates a new ProjectileBehaviour using conf. The owner passed may be nil 80 // if the projectile does not have one. 81 func (conf ProjectileBehaviourConfig) New(owner world.Entity) *ProjectileBehaviour { 82 if conf.ParticleCount == 0 && conf.Particle != nil { 83 conf.ParticleCount = 1 84 } 85 return &ProjectileBehaviour{conf: conf, owner: owner, mc: &MovementComputer{ 86 Gravity: conf.Gravity, 87 Drag: conf.Drag, 88 DragBeforeGravity: true, 89 }} 90 } 91 92 // ProjectileBehaviour implements the behaviour of projectiles. Its specifics 93 // may be configured using ProjectileBehaviourConfig. 94 type ProjectileBehaviour struct { 95 conf ProjectileBehaviourConfig 96 owner world.Entity 97 mc *MovementComputer 98 ageCollided int 99 close bool 100 101 collisionPos cube.Pos 102 collided bool 103 } 104 105 // Owner returns the owner of the projectile. 106 func (lt *ProjectileBehaviour) Owner() world.Entity { 107 return lt.owner 108 } 109 110 // Explode adds velocity to a projectile to blast it away from the explosion's 111 // source. 112 func (lt *ProjectileBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, _ block.ExplosionConfig) { 113 e.vel = e.vel.Add(e.pos.Sub(src).Normalize().Mul(impact)) 114 } 115 116 // Potion returns the potion.Potion that is applied to an entity if hit by the 117 // projectile. 118 func (lt *ProjectileBehaviour) Potion() potion.Potion { 119 return lt.conf.Potion 120 } 121 122 // Critical returns true if ProjectileBehaviourConfig.Critical was set to true 123 // and if the projectile has not collided. 124 func (lt *ProjectileBehaviour) Critical() bool { 125 return lt.conf.Critical && !lt.collided 126 } 127 128 // Tick runs the tick-based behaviour of a ProjectileBehaviour and returns the 129 // Movement within the tick. Tick handles the movement, collision and hitting 130 // of a projectile. 131 func (lt *ProjectileBehaviour) Tick(e *Ent) *Movement { 132 if lt.close { 133 _ = e.Close() 134 return nil 135 } 136 w := e.World() 137 138 e.mu.Lock() 139 if lt.collided && lt.tickAttached(e) { 140 e.mu.Unlock() 141 142 if lt.ageCollided > 1200 { 143 lt.close = true 144 } 145 return nil 146 } 147 before, vel := e.pos, e.vel 148 m, result := lt.tickMovement(e) 149 e.pos, e.vel = m.pos, m.vel 150 151 lt.collisionPos, lt.collided, lt.ageCollided = cube.Pos{}, false, 0 152 e.mu.Unlock() 153 154 if result == nil { 155 return m 156 } 157 158 for i := 0; i < lt.conf.ParticleCount; i++ { 159 w.AddParticle(result.Position(), lt.conf.Particle) 160 } 161 if lt.conf.Sound != nil { 162 w.PlaySound(result.Position(), lt.conf.Sound) 163 } 164 165 switch r := result.(type) { 166 case trace.EntityResult: 167 if l, ok := r.Entity().(Living); ok && lt.conf.Damage >= 0 { 168 lt.hitEntity(l, e, before, vel) 169 } 170 case trace.BlockResult: 171 bpos := r.BlockPosition() 172 if t, ok := w.Block(bpos).(block.TNT); ok && e.OnFireDuration() > 0 { 173 t.Ignite(bpos, w) 174 } 175 if lt.conf.SurviveBlockCollision { 176 lt.hitBlockSurviving(e, r, m) 177 return m 178 } 179 } 180 if lt.conf.Hit != nil { 181 lt.conf.Hit(e, result) 182 } 183 184 lt.close = true 185 return m 186 } 187 188 // tickAttached performs the attached logic for a projectile. It checks if the 189 // projectile is still attached to a block and if it can be picked up. 190 func (lt *ProjectileBehaviour) tickAttached(e *Ent) bool { 191 w := e.World() 192 boxes := w.Block(lt.collisionPos).Model().BBox(lt.collisionPos, w) 193 box := e.Type().BBox(e).Translate(e.pos) 194 195 for _, bb := range boxes { 196 if box.IntersectsWith(bb.Translate(lt.collisionPos.Vec3()).Grow(0.05)) { 197 if lt.ageCollided > 5 && !lt.conf.DisablePickup { 198 lt.tryPickup(e) 199 } 200 lt.ageCollided++ 201 return true 202 } 203 } 204 return false 205 } 206 207 // tryPickup checks for nearby projectile collectors and closes the entity if 208 // one was found. 209 func (lt *ProjectileBehaviour) tryPickup(e *Ent) { 210 w := e.World() 211 translated := e.Type().BBox(e).Translate(e.pos) 212 grown := translated.GrowVec3(mgl64.Vec3{1, 0.5, 1}) 213 ignore := func(other world.Entity) bool { 214 return e == other 215 } 216 for _, other := range w.EntitiesWithin(translated.Grow(2), ignore) { 217 if !other.Type().BBox(other).Translate(other.Position()).IntersectsWith(grown) { 218 continue 219 } 220 collector, ok := other.(Collector) 221 if !ok { 222 continue 223 } 224 // A collector was within range to pick up the entity. 225 lt.close = true 226 for _, viewer := range w.Viewers(e.pos) { 227 viewer.ViewEntityAction(e, PickedUpAction{Collector: collector}) 228 } 229 if lt.conf.PickupItem.Empty() { 230 return 231 } 232 _ = collector.Collect(lt.conf.PickupItem) 233 } 234 } 235 236 // hitBlockSurviving is called if 237 // ProjectileBehaviourConfig.SurviveBlockCollision is set to true and the 238 // projectile collides with a block. If the resulting velocity is roughly 0, 239 // it sets the projectile as having collided with the block. 240 func (lt *ProjectileBehaviour) hitBlockSurviving(e *Ent, r trace.BlockResult, m *Movement) { 241 e.mu.Lock() 242 // Create an epsilon for deciding if the projectile has slowed down enough 243 // for us to consider it as having collided for the final time. We take the 244 // square root because FloatEqualThreshold squares it, which is not what we 245 // want. 246 eps := math.Sqrt(0.1 * (1 - lt.conf.BlockCollisionVelocityMultiplier)) 247 if mgl64.FloatEqualThreshold(e.vel.Len(), 0, eps) { 248 e.vel = mgl64.Vec3{} 249 lt.collisionPos, lt.collided = r.BlockPosition(), true 250 e.mu.Unlock() 251 252 for _, v := range e.World().Viewers(m.pos) { 253 v.ViewEntityAction(e, ArrowShakeAction{Duration: time.Millisecond * 350}) 254 v.ViewEntityState(e) 255 } 256 return 257 } 258 e.mu.Unlock() 259 } 260 261 // hitEntity is called when a projectile hits a Living. It deals damage to the 262 // entity and knocks it back. Additionally, it applies any potion effects and 263 // fire if applicable. 264 func (lt *ProjectileBehaviour) hitEntity(l Living, e *Ent, origin, vel mgl64.Vec3) { 265 src := ProjectileDamageSource{Projectile: e, Owner: lt.owner} 266 dmg := math.Ceil(lt.conf.Damage * vel.Len()) 267 if lt.conf.Critical { 268 dmg += rand.Float64() * dmg / 2 269 } 270 if _, vulnerable := l.Hurt(lt.conf.Damage, src); vulnerable { 271 l.KnockBack(origin, 0.45+lt.conf.KnockBackForceAddend, 0.3608+lt.conf.KnockBackHeightAddend) 272 273 for _, eff := range lt.conf.Potion.Effects() { 274 l.AddEffect(eff) 275 } 276 if flammable, ok := l.(Flammable); ok && e.OnFireDuration() > 0 { 277 flammable.SetOnFire(time.Second * 5) 278 } 279 } 280 } 281 282 // tickMovement ticks the movement of a projectile. It updates the position and 283 // rotation of the projectile based on its velocity and updates the velocity 284 // based on gravity and drag. 285 func (lt *ProjectileBehaviour) tickMovement(e *Ent) (*Movement, trace.Result) { 286 w, pos, vel := e.World(), e.pos, e.vel 287 viewers := w.Viewers(pos) 288 289 velBefore := vel 290 vel = lt.mc.applyHorizontalForces(w, pos, lt.mc.applyVerticalForces(vel)) 291 rot := cube.Rotation{ 292 mgl64.RadToDeg(math.Atan2(vel[0], vel[2])), 293 mgl64.RadToDeg(math.Atan2(vel[1], math.Hypot(vel[0], vel[2]))), 294 } 295 296 var ( 297 end = pos.Add(vel) 298 hit trace.Result 299 ok bool 300 ) 301 if !mgl64.FloatEqual(end.Sub(pos).LenSqr(), 0) { 302 if hit, ok = trace.Perform(pos, end, w, e.Type().BBox(e).Grow(1.0), lt.ignores(e)); ok { 303 if _, ok := hit.(trace.BlockResult); ok { 304 // Undo the gravity because the velocity as a result of gravity 305 // at the point of collision should be 0. 306 vel[1] = (vel[1] + lt.mc.Gravity) / (1 - lt.mc.Drag) 307 x, y, z := vel.Mul(lt.conf.BlockCollisionVelocityMultiplier).Elem() 308 // Calculate multipliers for all coordinates: 1 for the ones that 309 // weren't on the same axis as the one collided with, -1 for the one 310 // that was on that axis to deflect the projectile. 311 mx, my, mz := hit.Face().Axis().Vec3().Mul(-2).Add(mgl64.Vec3{1, 1, 1}).Elem() 312 313 vel = mgl64.Vec3{x * mx, y * my, z * mz} 314 } else { 315 vel = zeroVec3 316 } 317 end = hit.Position() 318 } 319 } 320 return &Movement{v: viewers, e: e, pos: end, vel: vel, dpos: end.Sub(pos), dvel: vel.Sub(velBefore), rot: rot}, hit 321 } 322 323 // ignores returns a function to ignore entities in trace.Perform that are 324 // either a spectator, not living, the entity itself or its owner in the first 325 // 5 ticks. 326 func (lt *ProjectileBehaviour) ignores(e *Ent) func(other world.Entity) bool { 327 return func(other world.Entity) (ignored bool) { 328 g, ok := other.(interface{ GameMode() world.GameMode }) 329 _, living := other.(Living) 330 return (ok && !g.GameMode().HasCollision()) || e == other || !living || (e.age < time.Second/4 && lt.owner == other) 331 } 332 }