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  }