362 lines
13 KiB
Go
362 lines
13 KiB
Go
|
|
package entity
|
||
|
|
|
||
|
|
import (
|
||
|
|
"iter"
|
||
|
|
"math"
|
||
|
|
"math/rand/v2"
|
||
|
|
"slices"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/df-mc/dragonfly/server/block"
|
||
|
|
"github.com/df-mc/dragonfly/server/block/cube"
|
||
|
|
"github.com/df-mc/dragonfly/server/block/cube/trace"
|
||
|
|
"github.com/df-mc/dragonfly/server/entity/effect"
|
||
|
|
"github.com/df-mc/dragonfly/server/item"
|
||
|
|
"github.com/df-mc/dragonfly/server/item/potion"
|
||
|
|
"github.com/df-mc/dragonfly/server/world"
|
||
|
|
"github.com/go-gl/mathgl/mgl64"
|
||
|
|
)
|
||
|
|
|
||
|
|
// ProjectileBehaviourConfig allows the configuration of projectiles. Calling
|
||
|
|
// ProjectileBehaviourConfig.New() creates a ProjectileBehaviour using these
|
||
|
|
// settings.
|
||
|
|
type ProjectileBehaviourConfig struct {
|
||
|
|
Owner *world.EntityHandle
|
||
|
|
// Gravity is the amount of Y velocity subtracted every tick.
|
||
|
|
Gravity float64
|
||
|
|
// Drag is used to reduce all axes of the velocity every tick. Velocity is
|
||
|
|
// multiplied with (1-Drag) every tick.
|
||
|
|
Drag float64
|
||
|
|
// Damage specifies the base damage dealt by the Projectile. If set to a
|
||
|
|
// negative number, entities hit are not hurt at all and are not knocked
|
||
|
|
// back. The base damage is multiplied with the velocity of the projectile
|
||
|
|
// to calculate the final damage of the projectile.
|
||
|
|
Damage float64
|
||
|
|
// Potion is the potion effect that is applied to an entity when the
|
||
|
|
// projectile hits it.
|
||
|
|
Potion potion.Potion
|
||
|
|
// KnockBackForceAddend is the additional horizontal velocity that is
|
||
|
|
// applied to an entity when it is hit by the projectile.
|
||
|
|
KnockBackForceAddend float64
|
||
|
|
// KnockBackHeightAddend is the additional vertical velocity that is applied
|
||
|
|
// to an entity when it is hit by the projectile.
|
||
|
|
KnockBackHeightAddend float64
|
||
|
|
// Particle is a particle that is spawned when the projectile hits a
|
||
|
|
// target, either a block or an entity. No particle is spawned if left nil.
|
||
|
|
Particle world.Particle
|
||
|
|
// ParticleCount is the amount of particles that should be spawned if
|
||
|
|
// Particle is not nil. ParticleCount will be set to 1 if Particle is not
|
||
|
|
// nil and ParticleCount is 0.
|
||
|
|
ParticleCount int
|
||
|
|
// Sound is a sound that is played when the projectile hits a target, either
|
||
|
|
// a block or an entity. No sound is played if left nil.
|
||
|
|
Sound world.Sound
|
||
|
|
// Critical specifies if the projectile is critical. This spawns critical
|
||
|
|
// hit particles behind the projectile and causes it to deal up to 50% more
|
||
|
|
// damage.
|
||
|
|
Critical bool
|
||
|
|
// Hit is a function that is called when the projectile Ent hits a target
|
||
|
|
// (the trace.Result). The target is either of the type trace.EntityResult
|
||
|
|
// or trace.BlockResult. Hit may be set to run additional behaviour when a
|
||
|
|
// projectile hits a target.
|
||
|
|
Hit func(e *Ent, tx *world.Tx, target trace.Result)
|
||
|
|
// SurviveBlockCollision specifies if a projectile with this
|
||
|
|
// ProjectileBehaviour should survive collision with a block. If set to
|
||
|
|
// false, the projectile will break when hitting a block (like a snowball).
|
||
|
|
// If set to true, the projectile will survive like an arrow does.
|
||
|
|
SurviveBlockCollision bool
|
||
|
|
// BlockCollisionVelocityMultiplier is the multiplier used to modify the
|
||
|
|
// velocity of a projectile that has SurviveBlockCollision set to true. The
|
||
|
|
// default, 0, will cause the projectile to lose its velocity completely. A
|
||
|
|
// multiplier such as 0.5 will reduce the projectile's velocity, but retain
|
||
|
|
// half of it after inverting the axis on which the projectile collided.
|
||
|
|
BlockCollisionVelocityMultiplier float64
|
||
|
|
// DisablePickup specifies if picking up the projectile should be disabled,
|
||
|
|
// which is relevant in the case SurviveBlockCollision is set to true. Some
|
||
|
|
// projectiles, such as arrows, cannot be picked up if they are shot by
|
||
|
|
// monsters like skeletons.
|
||
|
|
DisablePickup bool
|
||
|
|
// PickupItem is the item that is given to a player when it picks up this
|
||
|
|
// projectile. If left as an empty item.Stack, no item is given upon pickup.
|
||
|
|
PickupItem item.Stack
|
||
|
|
// CollisionPosition specifies the position that the projectile is stuck
|
||
|
|
// in. If non-empty, the entity will not move.
|
||
|
|
CollisionPosition cube.Pos
|
||
|
|
// PiercingLevel is the crossbow Piercing enchantment level. The projectile
|
||
|
|
// passes through PiercingLevel entities and damages PiercingLevel+1 in
|
||
|
|
// total. A value of 0 means no piercing.
|
||
|
|
PiercingLevel int
|
||
|
|
}
|
||
|
|
|
||
|
|
func (conf ProjectileBehaviourConfig) Apply(data *world.EntityData) {
|
||
|
|
data.Data = conf.New()
|
||
|
|
}
|
||
|
|
|
||
|
|
// New creates a new ProjectileBehaviour using conf. The owner passed may be nil
|
||
|
|
// if the projectile does not have one.
|
||
|
|
func (conf ProjectileBehaviourConfig) New() *ProjectileBehaviour {
|
||
|
|
if conf.ParticleCount == 0 && conf.Particle != nil {
|
||
|
|
conf.ParticleCount = 1
|
||
|
|
}
|
||
|
|
return &ProjectileBehaviour{conf: conf, collided: conf.CollisionPosition != cube.Pos{}, collisionPos: conf.CollisionPosition, mc: &MovementComputer{
|
||
|
|
Gravity: conf.Gravity,
|
||
|
|
Drag: conf.Drag,
|
||
|
|
DragBeforeGravity: true,
|
||
|
|
}}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ProjectileBehaviour implements the behaviour of projectiles. Its specifics
|
||
|
|
// may be configured using ProjectileBehaviourConfig.
|
||
|
|
type ProjectileBehaviour struct {
|
||
|
|
conf ProjectileBehaviourConfig
|
||
|
|
mc *MovementComputer
|
||
|
|
ageCollided int
|
||
|
|
close bool
|
||
|
|
|
||
|
|
collisionPos cube.Pos
|
||
|
|
collided bool
|
||
|
|
|
||
|
|
collidedEntities []*world.EntityHandle
|
||
|
|
}
|
||
|
|
|
||
|
|
// Owner returns the owner of the projectile.
|
||
|
|
func (lt *ProjectileBehaviour) Owner() *world.EntityHandle {
|
||
|
|
return lt.conf.Owner
|
||
|
|
}
|
||
|
|
|
||
|
|
// Explode adds velocity to a projectile to blast it away from the explosion's
|
||
|
|
// source.
|
||
|
|
func (lt *ProjectileBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, _ block.ExplosionConfig) {
|
||
|
|
e.data.Vel = e.Velocity().Add(e.Position().Sub(src).Normalize().Mul(impact))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Potion returns the potion.Potion that is applied to an entity if hit by the
|
||
|
|
// projectile.
|
||
|
|
func (lt *ProjectileBehaviour) Potion() potion.Potion {
|
||
|
|
return lt.conf.Potion
|
||
|
|
}
|
||
|
|
|
||
|
|
// Critical returns true if ProjectileBehaviourConfig.Critical was set to true
|
||
|
|
// and if the projectile has not collided.
|
||
|
|
func (lt *ProjectileBehaviour) Critical() bool {
|
||
|
|
return lt.conf.Critical && !lt.collided
|
||
|
|
}
|
||
|
|
|
||
|
|
// Tick runs the tick-based behaviour of a ProjectileBehaviour and returns the
|
||
|
|
// Movement within the tick. Tick handles the movement, collision and hitting
|
||
|
|
// of a projectile.
|
||
|
|
func (lt *ProjectileBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
|
||
|
|
if lt.close {
|
||
|
|
_ = e.Close()
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
if lt.collided && lt.tickAttached(e, tx) {
|
||
|
|
if lt.ageCollided > 1200 {
|
||
|
|
lt.close = true
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
vel := e.Velocity()
|
||
|
|
m, result := lt.tickMovement(e, tx)
|
||
|
|
e.data.Pos, e.data.Vel = m.pos, m.vel
|
||
|
|
|
||
|
|
lt.collisionPos, lt.collided, lt.ageCollided = cube.Pos{}, false, 0
|
||
|
|
|
||
|
|
if result == nil {
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
for i := 0; i < lt.conf.ParticleCount; i++ {
|
||
|
|
tx.AddParticle(result.Position(), lt.conf.Particle)
|
||
|
|
}
|
||
|
|
if lt.conf.Sound != nil {
|
||
|
|
tx.PlaySound(result.Position(), lt.conf.Sound)
|
||
|
|
}
|
||
|
|
|
||
|
|
switch r := result.(type) {
|
||
|
|
case trace.EntityResult:
|
||
|
|
if l, ok := r.Entity().(Living); ok {
|
||
|
|
if lt.conf.Damage >= 0 {
|
||
|
|
lt.hitEntity(l, e, vel)
|
||
|
|
}
|
||
|
|
lt.collidedEntities = append(lt.collidedEntities, l.H())
|
||
|
|
}
|
||
|
|
case trace.BlockResult:
|
||
|
|
bpos := r.BlockPosition()
|
||
|
|
if h, ok := tx.Block(bpos).(block.ProjectileHitter); ok {
|
||
|
|
h.ProjectileHit(bpos, tx, e, r.Face())
|
||
|
|
}
|
||
|
|
if lt.conf.SurviveBlockCollision {
|
||
|
|
lt.hitBlockSurviving(e, r, m, tx)
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
lt.close = true
|
||
|
|
}
|
||
|
|
if lt.conf.Hit != nil {
|
||
|
|
lt.conf.Hit(e, tx, result)
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(lt.collidedEntities) > lt.conf.PiercingLevel {
|
||
|
|
lt.close = true
|
||
|
|
}
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
// tickAttached performs the attached logic for a projectile. It checks if the
|
||
|
|
// projectile is still attached to a block and if it can be picked up.
|
||
|
|
func (lt *ProjectileBehaviour) tickAttached(e *Ent, tx *world.Tx) bool {
|
||
|
|
boxes := tx.Block(lt.collisionPos).Model().BBox(lt.collisionPos, tx)
|
||
|
|
box := e.H().Type().BBox(e).Translate(e.Position())
|
||
|
|
|
||
|
|
for _, bb := range boxes {
|
||
|
|
if box.IntersectsWith(bb.Translate(lt.collisionPos.Vec3()).Grow(0.05)) {
|
||
|
|
if lt.ageCollided > 5 && !lt.conf.DisablePickup {
|
||
|
|
lt.tryPickup(e, tx)
|
||
|
|
}
|
||
|
|
lt.ageCollided++
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// tryPickup checks for nearby projectile collectors and closes the entity if
|
||
|
|
// one was found.
|
||
|
|
func (lt *ProjectileBehaviour) tryPickup(e *Ent, tx *world.Tx) {
|
||
|
|
translated := e.H().Type().BBox(e).Translate(e.Position())
|
||
|
|
grown := translated.GrowVec3(mgl64.Vec3{1, 0.5, 1})
|
||
|
|
for other := range tx.EntitiesWithin(translated.Grow(2)) {
|
||
|
|
if !other.H().Type().BBox(other).Translate(other.Position()).IntersectsWith(grown) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
collector, ok := other.(Collector)
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if _, ok := collector.Collect(lt.conf.PickupItem); !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
// A collector was within range and able to pick up the entity.
|
||
|
|
lt.close = true
|
||
|
|
for _, viewer := range tx.Viewers(e.Position()) {
|
||
|
|
viewer.ViewEntityAction(e, PickedUpAction{Collector: collector})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// hitBlockSurviving is called if
|
||
|
|
// ProjectileBehaviourConfig.SurviveBlockCollision is set to true and the
|
||
|
|
// projectile collides with a block. If the resulting velocity is roughly 0,
|
||
|
|
// it sets the projectile as having collided with the block.
|
||
|
|
func (lt *ProjectileBehaviour) hitBlockSurviving(e *Ent, r trace.BlockResult, m *Movement, tx *world.Tx) {
|
||
|
|
// Create an epsilon for deciding if the projectile has slowed down enough
|
||
|
|
// for us to consider it as having collided for the final time. We take the
|
||
|
|
// square root because FloatEqualThreshold squares it, which is not what we
|
||
|
|
// want.
|
||
|
|
eps := math.Sqrt(0.1 * (1 - lt.conf.BlockCollisionVelocityMultiplier))
|
||
|
|
if mgl64.FloatEqualThreshold(e.Velocity().Len(), 0, eps) {
|
||
|
|
e.SetVelocity(mgl64.Vec3{})
|
||
|
|
lt.collisionPos, lt.collided = r.BlockPosition(), true
|
||
|
|
|
||
|
|
for _, v := range tx.Viewers(m.pos) {
|
||
|
|
v.ViewEntityAction(e, ArrowShakeAction{Duration: time.Millisecond * 350})
|
||
|
|
v.ViewEntityState(e)
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// hitEntity is called when a projectile hits a Living. It deals damage to the
|
||
|
|
// entity and knocks it back. Additionally, it applies any potion effects and
|
||
|
|
// fire if applicable.
|
||
|
|
func (lt *ProjectileBehaviour) hitEntity(l Living, e *Ent, vel mgl64.Vec3) {
|
||
|
|
owner, _ := lt.conf.Owner.Entity(e.tx)
|
||
|
|
src := ProjectileDamageSource{Projectile: e, Owner: owner}
|
||
|
|
dmg := math.Ceil(lt.conf.Damage * vel.Len())
|
||
|
|
if lt.conf.Critical {
|
||
|
|
dmg += rand.Float64() * dmg / 2
|
||
|
|
}
|
||
|
|
// TODO: Piercing arrows should bypass shield blocking when shields are implemented.
|
||
|
|
if _, vulnerable := l.Hurt(dmg, src); vulnerable {
|
||
|
|
l.KnockBack(l.Position().Sub(vel), 0.45+lt.conf.KnockBackForceAddend, 0.3608+lt.conf.KnockBackHeightAddend)
|
||
|
|
|
||
|
|
for _, eff := range lt.conf.Potion.Effects() {
|
||
|
|
if lasting, ok := eff.Type().(effect.LastingType); ok {
|
||
|
|
l.AddEffect(effect.New(lasting, eff.Level(), time.Duration(float64(eff.Duration())/8)))
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
l.AddEffect(eff)
|
||
|
|
}
|
||
|
|
if flammable, ok := l.(Flammable); ok && e.OnFireDuration() > 0 {
|
||
|
|
flammable.SetOnFire(time.Second * 5)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// tickMovement ticks the movement of a projectile. It updates the position and
|
||
|
|
// rotation of the projectile based on its velocity and updates the velocity
|
||
|
|
// based on gravity and drag.
|
||
|
|
func (lt *ProjectileBehaviour) tickMovement(e *Ent, tx *world.Tx) (*Movement, trace.Result) {
|
||
|
|
pos, vel := e.Position(), e.Velocity()
|
||
|
|
viewers := tx.Viewers(pos)
|
||
|
|
|
||
|
|
velBefore := vel
|
||
|
|
vel = lt.mc.applyHorizontalForces(tx, pos, lt.mc.applyVerticalForces(vel))
|
||
|
|
rot := cube.Rotation{
|
||
|
|
mgl64.RadToDeg(math.Atan2(vel[0], vel[2])),
|
||
|
|
mgl64.RadToDeg(math.Atan2(vel[1], math.Hypot(vel[0], vel[2]))),
|
||
|
|
}
|
||
|
|
|
||
|
|
var (
|
||
|
|
end = pos.Add(vel)
|
||
|
|
hit trace.Result
|
||
|
|
ok bool
|
||
|
|
)
|
||
|
|
if !mgl64.FloatEqual(end.Sub(pos).LenSqr(), 0) {
|
||
|
|
if hit, ok = trace.Perform(pos, end, tx, e.H().Type().BBox(e).Grow(1.0), lt.ignores(e)); ok {
|
||
|
|
if _, ok := hit.(trace.BlockResult); ok {
|
||
|
|
// Undo the gravity because the velocity as a result of gravity
|
||
|
|
// at the point of collision should be 0.
|
||
|
|
vel[1] = (vel[1] + lt.mc.Gravity) / (1 - lt.mc.Drag)
|
||
|
|
x, y, z := vel.Mul(lt.conf.BlockCollisionVelocityMultiplier).Elem()
|
||
|
|
// Calculate multipliers for all coordinates: 1 for the ones that
|
||
|
|
// weren't on the same axis as the one collided with, -1 for the one
|
||
|
|
// that was on that axis to deflect the projectile.
|
||
|
|
mx, my, mz := hit.Face().Axis().Vec3().Mul(-2).Add(mgl64.Vec3{1, 1, 1}).Elem()
|
||
|
|
|
||
|
|
vel = mgl64.Vec3{x * mx, y * my, z * mz}
|
||
|
|
} else if lt.conf.PiercingLevel == 0 {
|
||
|
|
vel = zeroVec3
|
||
|
|
}
|
||
|
|
end = hit.Position()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return &Movement{v: viewers, e: e, pos: end, vel: vel, dpos: end.Sub(pos), dvel: vel.Sub(velBefore), rot: rot}, hit
|
||
|
|
}
|
||
|
|
|
||
|
|
// ignores returns a function to ignore entities in trace.Perform that are
|
||
|
|
// either a spectator, not living, the entity itself, its owner in the first
|
||
|
|
// 5 ticks, or an entity it already collided with.
|
||
|
|
func (lt *ProjectileBehaviour) ignores(e *Ent) trace.EntityFilter {
|
||
|
|
return func(seq iter.Seq[world.Entity]) iter.Seq[world.Entity] {
|
||
|
|
return func(yield func(world.Entity) bool) {
|
||
|
|
for other := range seq {
|
||
|
|
g, ok := other.(interface{ GameMode() world.GameMode })
|
||
|
|
spectator := ok && !g.GameMode().HasCollision()
|
||
|
|
itself := e.H() == other.H()
|
||
|
|
_, living := other.(Living)
|
||
|
|
owner := e.data.Age < time.Second/4 && lt.conf.Owner == other.H()
|
||
|
|
collidedEntity := slices.Contains(lt.collidedEntities, other.H())
|
||
|
|
if spectator || itself || !living || owner || collidedEntity {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !yield(other) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|