Skip to content

Instantly share code, notes, and snippets.

@thatcosmonaut
Last active September 28, 2023 18:12
Show Gist options
  • Save thatcosmonaut/d40bf4b0f9d2fd180e89f506826357b3 to your computer and use it in GitHub Desktop.
Save thatcosmonaut/d40bf4b0f9d2fd180e89f506826357b3 to your computer and use it in GitHub Desktop.
Samurai Gunn 2 MotionSystem
using System;
using System.Collections.Generic;
using MoonTools.ECS;
using MoonWorks.Math.Fixed;
using SamuraiGunn2.Content;
namespace SamuraiGunn2
{
public class MotionSystem : MoonTools.ECS.System
{
Filter DashFilter;
Filter SolidColliderFilter;
Filter SweepMovementFilter;
Filter InstantMovementFilter;
Filter InstantMovementIgnoreTimeDilationFilter;
Filter UnbreakableFilter;
Filter CanBeDamagedFilter;
Filter CanBeBrokenFilter;
Filter EffectColliderFilter;
Filter EffectColliderAnimationFilter;
Filter MotionRampFilter;
Filter GrassFilter;
Filter SpriteRotationFilter;
Filter SpriteRotationDampingFilter;
Filter DestroyOnBoundsFilter;
Filter PreviousPositionFilter;
private Filter RadialExplosionFilter;
Filter PreviousPositionForInterpolationFilter;
Filter PreviousSpriteRotationForInterpolationFilter;
Filter PreviousSpriteScaleForInterpolationFilter;
SpatialHash<Entity> SolidSpatialHash = new SpatialHash<Entity>(32);
SpatialHash<Entity> ExplodableSpatialHash = new SpatialHash<Entity>(32);
private SpatialHash<Entity> UnbreakableSpatialHash = new SpatialHash<Entity>(32);
// deferred actions so that weird timing results don't occur
HashSet<Entity> EntitiesToDisableEffectCollision;
HashSet<Entity> EntitiesToDestroy;
private HashSet<(Entity, Entity)> BulletsToDeactivate;
private HashSet<(Entity, Entity)> BreaksToProcess = new HashSet<(Entity, Entity)>();
// zero-dimension rectangle is an exact point check
// nice for things like pushing an origin out of the wall
private Rectangle Point = new Rectangle(0, 0, 0, 0);
private Rectangle SinglePixelRectangle = new Rectangle(0, 0, 1, 1);
ContinuousSpatialHash EffectColliderHash = new ContinuousSpatialHash(32);
EffectSpawner EffectSpawner;
GameplaySpawner GameplaySpawner;
ColliderSpawner ColliderSpawner;
BulletTransformer BulletTransformer;
CharacterManipulator CharacterManipulator;
public MotionSystem(World world) : base(world)
{
DashFilter = FilterBuilder
.Include<Position2D>()
.Include<Velocity2D>()
.Include<Dashing>()
.Build();
SolidColliderFilter = FilterBuilder
.Include<Position2D>()
.Include<SolidCollider>()
.Include<Solid>()
.Build();
SweepMovementFilter = FilterBuilder
.Include<Position2D>()
.Include<Velocity2D>()
.Include<SolidCollider>()
.Include<UseSweepMovement>()
.Build();
InstantMovementFilter = FilterBuilder
.Include<Position2D>()
.Include<Velocity2D>()
.Exclude<UseSweepMovement>()
.Exclude<IgnoreTimeDilation>()
.Build();
InstantMovementIgnoreTimeDilationFilter = FilterBuilder
.Include<Position2D>()
.Include<Velocity2D>()
.Include<IgnoreTimeDilation>()
.Exclude<UseSweepMovement>()
.Build();
UnbreakableFilter = FilterBuilder
.Include<Position2D>()
.Include<SolidCollider>()
.Include<Unbreakable>()
.Build();
CanBeDamagedFilter = FilterBuilder
.Include<Position2D>()
.Include<CanBeDamaged>()
.Build();
CanBeBrokenFilter = FilterBuilder
.Include<Position2D>()
.Include<CanBeBroken>()
.Build();
EffectColliderFilter = FilterBuilder
.Include<Position2D>()
.Include<EffectCollider>()
.Build();
PreviousPositionFilter = FilterBuilder
.Include<Position2D>()
.Include<PreviousPosition>()
.Build();
EffectColliderAnimationFilter = FilterBuilder
.Include<EffectColliderAnimation>()
.Build();
MotionRampFilter = FilterBuilder
.Include<Velocity2D>()
.Include<MotionRamp>()
.Exclude<Freeze>()
.Build();
SpriteRotationFilter = FilterBuilder
.Include<SpriteAngularVelocity>()
.Include<SpriteRotation>()
.Exclude<Freeze>()
.Exclude<NoUpdate>()
.Build();
SpriteRotationDampingFilter = FilterBuilder
.Include<SpriteAngularVelocity>()
.Include<SpriteAngularDamping>()
.Include<SpriteRotation>()
.Exclude<Freeze>()
.Exclude<NoUpdate>()
.Build();
RadialExplosionFilter = FilterBuilder.Include<RadialExplosion>().Build();
GrassFilter = FilterBuilder.Include<Position2D>().Include<GrassCollision>().Build();
DestroyOnBoundsFilter = FilterBuilder.Include<DestroyOnBounds>().Build();
PreviousPositionForInterpolationFilter = FilterBuilder
.Include<Position2D>()
.Build();
PreviousSpriteRotationForInterpolationFilter = FilterBuilder
.Include<SpriteRotation>()
.Build();
PreviousSpriteScaleForInterpolationFilter = FilterBuilder
.Include<SpriteScale>()
.Build();
EntitiesToDisableEffectCollision = new HashSet<Entity>(16);
EntitiesToDestroy = new HashSet<Entity>(16);
BulletsToDeactivate = new HashSet<(Entity, Entity)>(16);
GameplaySpawner = new GameplaySpawner(world);
EffectSpawner = new EffectSpawner(world);
ColliderSpawner = new ColliderSpawner(world);
BulletTransformer = new BulletTransformer(world);
CharacterManipulator = new CharacterManipulator(world);
EffectColliderFilter.RegisterAddCallback(RegisterEffectColliderEntity);
EffectColliderFilter.RegisterRemoveCallback(DeregisterEffectColliderEntity);
SolidColliderFilter.RegisterAddCallback(AddSolidToSpatialHash);
SolidColliderFilter.RegisterRemoveCallback(RemoveFromSolidSpatialHash);
CanBeDamagedFilter.RegisterAddCallback(AddToExplodableHash);
CanBeDamagedFilter.RegisterRemoveCallback(RemoveFromExplodableHash);
CanBeBrokenFilter.RegisterAddCallback(AddToExplodableHash);
CanBeBrokenFilter.RegisterRemoveCallback(RemoveFromExplodableHash);
UnbreakableFilter.RegisterAddCallback(AddUnbreakableToSpatialHash);
UnbreakableFilter.RegisterRemoveCallback(RemoveUnbreakableFromSpatialHash);
}
public override void Update(TimeSpan delta)
{
// hud elements that move during the StopAction effect
var undilatedDelta = ReadMessage<DeltaTimeMessage>().UndilatedDelta;
foreach(var entity in InstantMovementIgnoreTimeDilationFilter.Entities)
{
var position = Get<Position2D>(entity);
var velocity = Get<Velocity2D>(entity).Velocity;
var movement = GetMovement(entity, velocity, undilatedDelta);
Set(entity, position + movement);
}
// track interpolation
foreach (var entity in PreviousPositionForInterpolationFilter.Entities)
{
Set(entity, new PreviousPositionForInterpolation(Get<Position2D>(entity)));
}
foreach (var entity in PreviousSpriteRotationForInterpolationFilter.Entities)
{
Set(entity, new PreviousSpriteRotationForInterpolation(Get<SpriteRotation>(entity).Rotation));
}
foreach (var entity in PreviousSpriteScaleForInterpolationFilter.Entities)
{
Set(entity, new PreviousSpriteScaleForInterpolation(Get<SpriteScale>(entity).Value));
}
if (Some<StopAction>())
{
return;
}
// push bullets outside of walls on spawn
foreach(var message in ReadMessages<MoveOutOfWall>())
{
var entity = message.Entity;
var position = Get<Position2D>(entity);
Set(entity, GetPositionOutsideOfUnbreakable(message.Collider, position, message.Xstep, message.Ystep, message.MaxDistance));
}
var dt = ReadMessage<DeltaTimeMessage>().DilatedDelta;
var levelBoundaries = GetSingleton<LevelBoundaries>();
foreach(var relation in Relations<ClashFX>())
{
Unrelate<ClashFX>(relation.Item1, relation.Item2);
}
// destroy orphaned colliders
foreach (var entity in EffectColliderFilter.Entities)
{
if (InRelationCount<HasEffectCollider>(entity) == 0)
{
Destroy(entity);
}
}
foreach (var entity in PreviousPositionFilter.Entities)
{
var position = Get<Position2D>(entity);
Set(entity, new PreviousPosition(position));
}
foreach (var entity in DashFilter.Entities)
{
var dashing = Get<Dashing>(entity);
Set(entity, new Velocity2D(dashing.Direction * dashing.Speed));
}
// overlap tests
foreach (var (damager, damageReceiver) in Relations<CantDamageUntilNotOverlapping>())
{
if (!EffectColliderHash.Check(damager, damageReceiver))
{
Unrelate<CantDamageUntilNotOverlapping>(damager, damageReceiver);
}
}
foreach (var (deflector, deflected) in Relations<CantDeflectUntilNotOverlapping>())
{
if (!EffectColliderHash.Check(deflector, deflected))
{
Unrelate<CantDeflectUntilNotOverlapping>(deflector, deflected);
}
}
foreach (var (tumbler, tumbleReceiver) in
Relations<CantTumbleTransferUntilNotOverlapping>())
{
if (!EffectColliderHash.Check(tumbler, tumbleReceiver))
{
Unrelate<CantTumbleTransferUntilNotOverlapping>(tumbler, tumbleReceiver);
}
}
// update collider animations
foreach (var entity in EffectColliderAnimationFilter.Entities)
{
var position = Get<Position2D>(entity);
var effectColliderAnimation = Get<EffectColliderAnimation>(entity);
effectColliderAnimation = effectColliderAnimation.Update(dt);
Set(entity, effectColliderAnimation);
// calculate directional rotation if necessary
if (Has<Direction>(entity))
{
var direction = Get<Direction>(entity).Value;
var rotatedRectangle =
effectColliderAnimation.CurrentCollider.TransformAim(
effectColliderAnimation.FlipX ? -1 : 1,
effectColliderAnimation.FlipY ? -1 : 1,
(int) direction.Y);
Set(entity, new EffectCollider(rotatedRectangle));
}
else
{
Set(entity, new EffectCollider(effectColliderAnimation.CurrentCollider));
}
// have to remove and re-add from sweep and prune because size changed
//SweepAndPrune.Remove(entity);
EffectColliderHash.Remove(entity);
var rectangle = Get<EffectCollider>(entity).Collider.Transform(position);
EffectColliderHash.Insert(InRelationSingleton<HasEffectCollider>(entity), entity, rectangle);
//SweepAndPrune.Add(entity, rectangle);
}
// Now, move everything, taking into account solid collisions to find the actual movement
foreach (var entity in SweepMovementFilter.Entities)
{
var position = Get<Position2D>(entity);
var velocity = Get<Velocity2D>(entity).Velocity;
var movement = GetMovement(entity, velocity, dt);
var projectedPosition = position + movement;
bool collidedHorizontal = false;
int collidedHorizontalX = -999;
bool collidedVertical = false;
int collidedVerticalY = -999;
var pointOfImpact = Position2D.Zero; // only valid if collision occurred
// if a moving object is solid, we need to pixel sweep
int mostRecentValidXPosition = position.X;
int mostRecentValidYPosition = position.Y;
var collider = Get<SolidCollider>(entity).Collider;
bool checkForGapsBelow = false;
if (Has<OnGround>(entity) && Has<StepDownLedgesWhenHoldingDown>(entity) && !Has<Tumbling>(entity))
{
if (HasInRelation<Controls>(entity))
{
var playerEntity = InRelationSingleton<Controls>(entity);
if (SomeMessageWithEntity<InputMessage>(playerEntity))
{
checkForGapsBelow = ReadMessageWithEntity<InputMessage>(playerEntity).Down.IsDown;
}
}
}
// first, check horizontal positions in between current position and destination
foreach (var i in IntegerEnumerator.IntegersBetween(position.X, projectedPosition.X))
{
var updatedPosition = new Position2D(i, position.Y);
if (levelBoundaries.WrapHorizontal && Has<Wrap>(entity))
{
levelBoundaries.WrappedPosition(updatedPosition, out updatedPosition);
}
// if holding down and moving over a gap, fall into that gap and stop sweeping x
if (checkForGapsBelow)
{
var belowPosition = updatedPosition + new Position2D(0, 4);
if (!CheckSolidOverlap(entity, belowPosition, collider))
{
projectedPosition += new Position2D(0, 2);
Send(new PlayStaticSoundMessage(Content.StaticAudio.DevUI_Tap));
Send(new PlayStaticSoundMessage(Random.Unsynced.Choose(StaticAudioArrays.FootStep)));
if (Has<CanEdgeDrop>(entity))
{
var edgeDrop = Get<CanEdgeDrop>(entity);
var wallDustSprite = Content.SpriteAnimations.WallDust;
Send(new EffectMessage
{
Position = updatedPosition,
FlipX = movement.X <= 0,
SpriteAnimation = new SpriteAnimation(wallDustSprite, 30, false),
Velocity = new Vector2(Fix64.Zero, -edgeDrop.Speed * Fix64.FromFraction(12, 10)),
UseVelocity = true,
UseLevelPalette = true,
ReceiveLight = true
});
velocity.X /= 2;
velocity.Y = edgeDrop.Speed;
}
Remove<OnGround>(entity);
}
}
if ((Has<CannotMoveThroughSolid>(entity) && CheckSolidOverlap(entity, updatedPosition, collider)) ||
(Has<CannotMoveThroughUnbreakable>(entity) && CheckUnbreakableOverlap(entity, updatedPosition, collider)))
{
collidedHorizontal = true;
collidedHorizontalX = mostRecentValidXPosition + (Fix64.Sign(movement.X) >= 0 ? collider.Right : collider.Left);
// set movement as integer to avoid rollover
movement.X = new Fix64(mostRecentValidXPosition - position.X);
position = position.SetX(position.X);
pointOfImpact = position.SetX(collidedHorizontalX);
// special case for things like bullets overlapping bamboo
CheckSolidBreaks(entity, collider, updatedPosition);
break;
}
// constrain level boundaries without "wall" collision
if (Has<ConstrainPositionToLevelBoundaries>(entity) && !levelBoundaries.WrapHorizontal)
{
var colliderRight = updatedPosition.X + collider.Right;
var colliderLeft = updatedPosition.X + collider.Left;
if (
(movement.X > 0 && colliderRight > levelBoundaries.BoundaryRectangle.X + levelBoundaries.BoundaryRectangle.W) ||
(movement.X < 0 && colliderLeft < levelBoundaries.BoundaryRectangle.X)
) {
movement.X = new Fix64(mostRecentValidXPosition - position.X);
position = position.SetX(position.X); // truncates X coord
break;
}
}
mostRecentValidXPosition = i;
}
// check vertical positions in between current position and destination
foreach (var i in IntegerEnumerator.IntegersBetween(position.Y, projectedPosition.Y))
{
var updatedPosition = new Position2D(mostRecentValidXPosition, i);
if (levelBoundaries.WrapVertical && Has<Wrap>(entity))
{
levelBoundaries.WrappedPosition(updatedPosition, out updatedPosition);
}
if ((Has<CannotMoveThroughSolid>(entity) && CheckSolidOverlap(entity, updatedPosition, collider)) ||
(Has<CannotMoveThroughUnbreakable>(entity) && CheckUnbreakableOverlap(entity, updatedPosition, collider)))
{
// Nudge over to gap when moving upwards and blocked
var nudgeToGap = false;
if (movement.Y < 0 && Has<NudgeToGapsWhenMovingUpwards>(entity))
{
var distance = Get<NudgeToGapsWhenMovingUpwards>(entity).Distance;
for (int horizontalDistance = 1; horizontalDistance <= distance; horizontalDistance += 1)
{
var checkPositionRight = updatedPosition + new Position2D(horizontalDistance, 0);
var checkPositionLeft = updatedPosition + new Position2D(-horizontalDistance, 0);
if (!CheckSolidOverlap(entity, checkPositionRight, collider))
{
position = position.SetX(position.X + (int)(horizontalDistance));
mostRecentValidXPosition = position.X;
nudgeToGap = true;
break;
}
else if (!CheckSolidOverlap(entity, checkPositionLeft, collider))
{
position = position.SetX(position.X - (int)(horizontalDistance));
mostRecentValidXPosition = position.X;
nudgeToGap = true;
break;
}
}
}
if (!nudgeToGap)
{
collidedVertical = true;
collidedVerticalY = mostRecentValidYPosition + (Fix64.Sign(movement.Y) > 0 ? collider.Bottom : collider.Top);
// set movement as integer to avoid rollover
movement.Y = new Fix64(mostRecentValidYPosition - position.Y);
position = position.SetY(position.Y); // truncates Y coord
pointOfImpact = position.SetY(collidedVerticalY);
// special case for things like bullets overlapping bamboo
CheckSolidBreaks(entity, collider, updatedPosition);
break;
}
}
// constrain level boundaries without "wall" collision
if (Has<ConstrainPositionToLevelBoundaries>(entity) && !levelBoundaries.WrapVertical)
{
var colliderBottom = updatedPosition.Y + collider.Bottom;
var colliderTop = updatedPosition.Y + collider.Top;
if (
(movement.Y > 0 && colliderBottom > levelBoundaries.BoundaryRectangle.Y + levelBoundaries.BoundaryRectangle.H) ||
(movement.Y < 0 && colliderTop < levelBoundaries.BoundaryRectangle.Y)
) {
movement.Y = new Fix64(mostRecentValidYPosition - position.Y);
position = position.SetY(position.Y); // truncates Y coord
break;
}
}
mostRecentValidYPosition = i;
}
var newPosition = position + movement;
if (Has<Wrap>(entity))
{
levelBoundaries.WrappedPosition(newPosition, out newPosition);
}
// do solid collision processing
var solidCollided = collidedHorizontal || collidedVertical;
// ground / wall / ceiling checks
var onCeiling = CheckSolidOverlap(entity, newPosition + new Position2D(0, -2), collider); // -2 for some fudge room
if (onCeiling)
{
Set(entity, new OnCeiling());
}
else
{
Remove<OnCeiling>(entity);
}
// check if on the ground
var onGround = velocity.Y >= 0 && !Has<Tumbling>(entity) && CheckSolidOverlap(entity, newPosition + new Position2D(0, 1), collider);
if (onGround)
{
// FIXME: is this necessary?
newPosition = newPosition.SetY(newPosition.Y); // truncate Y
}
if (Has<LandOnGroundEffects>(entity))
{
if (onGround && !Has<OnGround>(entity) && !Has<Dashing>(entity))
{
var landingTime = Fix64.FromFraction(1, 60) + Fix64.Abs(velocity.Y) * dt * dt * new Fix64(2);
Set(entity, new Landing(landingTime));
// land flash fx
Send(new EffectMessage
{
Position = newPosition + new Vector2(0, -10),
FlipX = Random.Synced.CoinFlip(),
SpriteAnimation = new SpriteAnimation(Content.SpriteAnimations.FX_Flash_Land, 30, false)
});
var dustSprite = Content.SpriteAnimations.FX_Dust_Land;
// heavy landing
if (velocity.Y > Fix64.FromFraction(39, 10) * new Fix64(60))
{
Set(entity, new Shake(new Vector2(velocity.Y / new Fix64(60) / new Fix64(4) * new Fix64(Random.Synced.Flip()), Fix64.Zero), Fix64.FromFraction(13, 10), Frames.ToSeconds(2)));
if (velocity.Y > Fix64.FromFraction(66, 10) * new Fix64(60))
{
Set(entity, new Shake(new Vector2(velocity.Y / new Fix64(60) / new Fix64(3) * new Fix64(Random.Synced.Flip()), Fix64.Zero), Fix64.FromFraction(12, 10), Frames.ToSeconds(2)));
for (var i = -1; i <= 1; i += 2) // (-1, 1)
{
Send(new EffectMessage
{
Position = newPosition + new Vector2(i * 2, 0),
FlipX = i == -1,
SpriteAnimation = new SpriteAnimation(dustSprite, Random.Synced.Range(9, 18), false),
Velocity = new Vector2(Fix64.Abs(velocity.Y) / new Fix64(6) * new Fix64(i), Fix64.Zero),
UseVelocity = true,
MotionDampFactor = new Vector2(Fix64.FromFraction(105, 100)),
UseMotionDamp = true,
ReceiveLight = true
});
}
}
}
// medium + heavy landing
if (velocity.Y > Fix64.FromFraction(266, 100) * new Fix64(60))
{
for (var i = -1; i <= 1; i += 2) // (-1, 1)
{
Send(new EffectMessage
{
Position = newPosition + new Vector2(i * 2, 0),
FlipX = i == -1,
SpriteAnimation = new SpriteAnimation(dustSprite, 60, false),
Velocity = new Vector2(Fix64.Abs(velocity.Y) / new Fix64(4) * new Fix64(i), Fix64.Zero),
UseVelocity = true,
MotionDampFactor = new Vector2(Fix64.FromFraction(105, 100)),
UseMotionDamp = true,
ReceiveLight = true
});
}
}
}
}
if (onGround && Has<FadeBlendWithBackgroundAfterHittingGround>(entity))
{
var fadeParams = Get<FadeBlendWithBackgroundAfterHittingGround>(entity);
Set(entity, new FadeBlendWithBackgroundTimer(fadeParams.TargetLerpFactor, fadeParams.DelayTime, fadeParams.FadeDuration));
Remove<FadeBlendWithBackgroundAfterHittingGround>(entity);
}
if (onGround && !Has<OnGround>(entity))
{
Set(entity, new OnGround());
}
else if (!onGround && Has<OnGround>(entity))
{
Remove<OnGround>(entity);
}
// check if on the wall
var onRight = velocity.X >= 0 && CheckSolidOverlap(entity, newPosition + new Position2D(1, 0), collider);
if (onRight && !Has<OnWallRight>(entity))
{
Set(entity, new OnWallRight());
}
else if (!onRight && Has<OnWallRight>(entity))
{
Remove<OnWallRight>(entity);
}
var onLeft = velocity.X <= 0 && CheckSolidOverlap(entity, newPosition + new Position2D(-1, 0), collider);
if (onLeft && !Has<OnWallLeft>(entity))
{
Set(entity, new OnWallLeft());
}
else if (!onLeft && Has<OnWallLeft>(entity))
{
Remove<OnWallLeft>(entity);
}
if (onGround)
{
if (Has<SlowOnGround>(entity))
{
velocity.X *= Get<SlowOnGround>(entity).Factor;
}
if (Has<SetDepthOnGround>(entity))
{
var depth = Get<SetDepthOnGround>(entity).Depth;
Set(entity, new DrawDepth(depth));
}
if (Has<RollOnGround>(entity))
{
var rollOnGround = Get<RollOnGround>(entity);
Set(entity, new SpriteAngularVelocity(velocity.X * rollOnGround.Factor));
if (!Has<SpriteRotation>(entity))
{
Set(entity, new SpriteRotation(Fix64.Zero));
}
}
if (!Has<DontZeroYVelocityOnGround>(entity) && !Has<BounceOnSolidCollision>(entity))
{
velocity.Y = Fix64.Zero;
}
}
if (Has<CanFling>(entity) && (onGround || onLeft || onRight))
{
Set(entity, new FlingIsReady());
}
if (solidCollided)
{
if (collidedHorizontal && Has<SlowOnHorizontalSolidCollision>(entity))
{
velocity.X *= Get<SlowOnHorizontalSolidCollision>(entity).Factor;
}
if (Has<DestroyOnSolidCollision>(entity))
{
EntitiesToDestroy.Add(entity);
}
if (Has<FreezeOnSolidCollision>(entity))
{
var freeze = Get<FreezeOnSolidCollision>(entity);
Set(entity, new Freeze(freeze.Time));
}
if (Has<DampenSpriteAngularVelocityOnSolidCollision>(entity))
{
var dampAngle = Get<DampenSpriteAngularVelocityOnSolidCollision>(entity);
var angularVelocity = Get<SpriteAngularVelocity>(entity).Value;
angularVelocity /= dampAngle.DampenFactor;
Set(entity, new SpriteAngularVelocity(angularVelocity));
}
if (Has<AccelerateSpriteAngularVelocityOnSolidCollision>(entity))
{
var accelerateSpriteAngle = Get<AccelerateSpriteAngularVelocityOnSolidCollision>(entity);
var angularVelocity = Get<SpriteAngularVelocity>(entity).Value;
if (accelerateSpriteAngle.FlipRandomly)
{
int flip = 0;
if (Has<Rollback>(entity))
{
flip = Random.Synced.Flip();
}
else
{
flip = Random.Unsynced.Flip();
}
angularVelocity += accelerateSpriteAngle.Acceleration * flip;
}
else
{
angularVelocity += accelerateSpriteAngle.Acceleration;
}
Set(entity, new SpriteAngularVelocity(angularVelocity));
}
if (Has<DamageOnSolidCollision>(entity) &&
((collidedHorizontal && Fix64.Abs(velocity.X) >= Fix64.Abs(velocity.Y / 2)) ||
(collidedVertical && Fix64.Abs(velocity.Y) >= Fix64.Abs(velocity.X / 2))))
{
var direction = Vector2.Zero;
if (collidedHorizontal)
{
direction = new Vector2(velocity.X > 0 ? 1 : -1, 0);
}
else
{
direction = new Vector2(0, velocity.Y > 0 ? 1 : -1);
}
Remove<LandOnGroundEffects>(entity);
CharacterManipulator.DeadlyTumblingCancel(entity);
Set(entity, new Shake(Vector2.Rotate(direction, Fix64.PiOver2), Fix64.One, Frames.ToSeconds(2)));
Set(entity, new SlammedIntoWall(direction * new Fix64(3), Fix64.Zero, Frames.ToSeconds(5)));
}
else if (Has<BounceOnSolidCollision>(entity))
{
var bounce = Get<BounceOnSolidCollision>(entity);
if (collidedHorizontal)
{
// make tumbling impact feel good on horizontal collision:
if (Has<Tumbling>(entity))
{
TumbleFX(
entity,
new Position2D(collidedHorizontalX, position.Y - 7),
Fix64.ToRadians(new Fix64(Math.Sign((int)velocity.X) * -90))
);
}
velocity.X = -velocity.X / bounce.MotionDamp.X;
}
if (collidedVertical)
{
var collisionStartVerticalVelocity = velocity.Y;
// make tumbling impact feel good on vertical collision:
if (Has<Tumbling>(entity))
{
TumbleFX(
entity,
new Position2D(position.X, collidedVerticalY),
Fix64.ToRadians(new Fix64(90 + Math.Sign((int)velocity.Y) * -90))
);
}
velocity.Y /= -bounce.MotionDamp.Y;
// minimum tumble speed
if (Has<Tumbling>(entity))
{
if (collisionStartVerticalVelocity > 0) // just collided with ground
{
velocity.Y -= 120;
}
velocity = Vector2.Normalize(velocity) * Fix64.Max(new Fix64(3 * 60), velocity.Length());
}
// Stutter Protection
if (Fix64.Abs(velocity.Y) < new Fix64(60))
{
velocity.Y = Fix64.Zero;
}
// Erratic Bounce
if (Has<ErraticBounce>(entity))
{
var accel = Fix64.Abs(velocity.Y * Get<ErraticBounce>(entity).VerticalToHorizontalFactor);
velocity.X += Random.Synced.Mirror(accel);
}
}
}
// zero out y velocity on a ceiling collision
else if (collidedVertical && Fix64.Sign(velocity.Y) == -1)
{
velocity.Y = Fix64.Zero;
}
else if (HasOutRelation<KilledBy>(entity))
{
CharacterManipulator.ClearKilledBy(entity);
}
if (HasOutRelation<InGasBubble>(entity) && (collidedVertical || collidedHorizontal))
{
EffectSpawner.GasBubblePopFX(entity);
Set(entity, new Freeze(Frames.ToSeconds(6)));
Set(entity, new Shake(Vector2.Rotate(Vector2.Normalize(Vector2.Max(Vector2.One, velocity)), Fix64.ToRadians(new Fix64(90))) * new Fix64(2), Fix64.FromFraction(11, 10), Frames.ToSeconds(2)));
}
if (Has<PlaySoundOnSolidCollision>(entity) && velocity.Length() > new Fix64(60))
{
var playSound = Get<PlaySoundOnSolidCollision>(entity);
var sound = Random.Unsynced.Choose(GetBounceSounds(playSound.BounceSoundType));
// Volume
var volume = playSound.Volume;
if (Has<DampenBounceSoundOnCollision>(entity))
{
var damp = Get<DampenBounceSoundOnCollision>(entity);
volume *= damp.VolumeFactor;
// decrease volumeFactor by step
var newVolumeFactor = MoonWorks.Math.MathHelper.Approach(damp.VolumeFactor, damp.VolumeFactorMin, damp.Step);
Set(entity, new DampenBounceSoundOnCollision(newVolumeFactor, damp.VolumeFactorMin, damp.Step));
}
// Pitch
var pitch = playSound.Pitch + Random.Unsynced.Mirror(playSound.PitchVariance);
if (collidedVertical) // pitch up slightly if a vertical bounce
{
pitch += Music.HalfStep;
}
// Play Sound
Send(new PlayStaticSoundMessage(sound, volume, pitch));
}
if (Has<MotionRampOnSolidCollision>(entity))
{
var motionRamp = Get<MotionRampOnSolidCollision>(entity).MotionRamp;
Set(entity, new MotionRamp(motionRamp.Value, motionRamp.Rate));
}
if (Has<MotionDampOnSolidCollision>(entity))
{
var damping = Get<MotionDampOnSolidCollision>(entity).damping;
if (collidedHorizontal)
{
velocity.X /= damping.X;
}
if (collidedVertical)
{
velocity.Y /= damping.Y;
}
}
if (Has<StopOnSolidCollision>(entity))
{
if (collidedHorizontal || collidedVertical)
{
velocity = Vector2.Zero;
}
}
if (Has<RemoveAccelerateTowardRelationOnSolidCollision>(entity))
{
if (collidedHorizontal || collidedVertical)
{
foreach(var target in OutRelations<AccelerateToward>(entity))
{
Unrelate<AccelerateToward>(entity, target);
}
}
}
if (HasOutRelation<TongueFlingOnSolidCollision>(entity))
{
if (collidedHorizontal || collidedVertical)
{
var characterEntity = OutRelationSingleton<TongueFlingOnSolidCollision>(entity);
Unrelate<TongueFlingOnSolidCollision>(entity, characterEntity);
Remove<TongueGrabOut>(characterEntity);
Set(characterEntity, new TongueGrabFling());
}
}
// Bullet Hit Spark Effects
if (Has<SpawnBulletHitSparkOnSolidCollision>(entity))
{
SpawnBulletHitSparks(entity, pointOfImpact);
}
// SUPER BULLET hit wall spark effects
if (Has<SpawnSuperBulletHitSparkOnSolidCollision>(entity))
{
SpawnSuperBulletHitSparks(entity, pointOfImpact);
}
if (Has<FlameTrapOnImpact>(entity))
{
GameplaySpawner.SpawnFlameTrap(OutRelationSingleton<BelongsToPlayer>(entity), pointOfImpact, new Fix64(3), Has<SuperPowered>(entity), onGround, onCeiling, onRight, onLeft);
}
if (Has<ExplodesOnImpact>(entity))
{
GameplaySpawner.SpawnExplosionPops(
OutRelationSingleton<BelongsToPlayer>(entity),
pointOfImpact
);
}
}
Set(entity, new Velocity2D(velocity));
UpdatePosition(entity, levelBoundaries, position, position + movement);
}
// move the unconstrained entities
foreach (var entity in InstantMovementFilter.Entities)
{
var position = Get<Position2D>(entity);
var velocity = Get<Velocity2D>(entity).Velocity;
var movement = GetMovement(entity, velocity, dt);
UpdatePosition(entity, levelBoundaries, position, position + movement);
}
// After movement processing is done, process secondary effects
ProcessDeflectInteractions();
// Replace bullet colliders
foreach (var message in ReadMessages<DeferReplaceBulletColliderMessage>())
{
// Destroy all existing collider entities related to this bullet
var entity = message.EntityID;
foreach (var colliderEntity in OutRelations<HasEffectCollider>(entity))
{
Destroy(colliderEntity);
}
// Update collider on this bullet
ColliderSpawner.AddBulletCollider(entity, message.Position, message.AimDirection, message.FacingX);
}
// deflection can disable colliders, so do that now
foreach (var entity in EntitiesToDisableEffectCollision)
{
foreach(var colliderEntity in OutRelations<HasEffectCollider>(entity))
{
Destroy(colliderEntity);
}
}
ProcessDeadlyThrowInteractions();
ProcessCancelDeadlyTumbleInteractions();
ProcessDamageInteractions();
ProcessBreakInteractions();
ProcessMakeHarmlessInteractions();
ProcessCutInteractions();
ProcessObstructInteractions();
ProcessKickInteractions();
ProcessRemoveAmmoInteractions();
ProcessPushGrassInteractions(dt);
ProcessTumbleInteractions();
ProcessPaintInteractions();
ProcessSuperFusions();
ProcessSwallowInteractions();
ProcessOtomoPowerBoostInteractions();
// process explosions
foreach (var explosionEntity in RadialExplosionFilter.Entities)
{
var position = Get<Position2D>(explosionEntity);
var radialExplosion = Get<RadialExplosion>(explosionEntity);
if (Trigger.OnTime(radialExplosion.Time, radialExplosion.ExplosionTriggerTime, dt))
{
var radiusSquared = radialExplosion.Radius * radialExplosion.Radius;
var topLeft = position + new Position2D(
-radialExplosion.Radius,
-radialExplosion.Radius);
var aabb = new Rectangle(topLeft, radialExplosion.Radius * 2, radialExplosion.Radius * 2);
foreach (var (entity, rectangle) in ExplodableSpatialHash.Retrieve(aabb))
{
var rectPosition = rectangle.Position;
var squareDistance = Vector2.DistanceSquared(position, rectPosition);
if (squareDistance < radiusSquared)
{
if (Has<CanBeDamaged>(entity))
{
ProcessDamage(explosionEntity, entity);
}
else if (Has<CanBeBroken>(entity))
{
ProcessBreak(
explosionEntity,
position,
entity,
Get<Position2D>(entity));
}
}
}
}
}
// process other motion-related activities
foreach (var entity in MotionRampFilter.Entities)
{
var motionRamp = Get<MotionRamp>(entity).Update(dt);
if (motionRamp.Value == Fix64.One)
{
Remove<MotionRamp>(entity);
}
else
{
Set(entity, motionRamp);
}
}
foreach (var (follower, target) in Relations<LerpFollow>())
{
var lerpFollow = GetRelationData<LerpFollow>(follower, target);
var offset = lerpFollow.Offset;
var t = lerpFollow.LerpTime;
var position = Get<Position2D>(follower);
var targetPosition = Get<Position2D>(target);
var newPosition = new Position2D(
(int) MoonWorks.Math.Easing.Lerp(position.X, targetPosition.X, (float) (dt / t)),
(int) MoonWorks.Math.Easing.Lerp(position.Y, targetPosition.Y, (float) (dt / t))
);
Set(follower, newPosition);
}
foreach (var entity in SpriteRotationFilter.Entities)
{
var spriteRotation = Get<SpriteRotation>(entity).Rotation;
var spriteAngularVelocity = Get<SpriteAngularVelocity>(entity).Value;
spriteRotation += spriteAngularVelocity * dt;
Set(entity, new SpriteRotation(spriteRotation));
}
foreach (var entity in SpriteRotationDampingFilter.Entities)
{
var spriteAngularVelocity = Get<SpriteAngularVelocity>(entity).Value;
var spriteAngularDamping = Get<SpriteAngularDamping>(entity).Damping;
Fix64 newVelocity;
// FIXME: why does this use multiply for damping but everything else uses divide
if (Some<TimeDilation>())
{
// use time dilation directly to avoid dt remultiply error
var timeDilation = GetSingleton<TimeDilation>();
newVelocity = spriteAngularVelocity * Fix64.Pow(spriteAngularDamping, timeDilation.Factor);
}
else
{
newVelocity = spriteAngularVelocity * spriteAngularDamping;
}
Set(entity, new SpriteAngularVelocity(newVelocity));
}
foreach (var entity in DestroyOnBoundsFilter.Entities)
{
var position = Get<Position2D>(entity);
// FIXME: magic values!!
if (System.Math.Abs(position.X) > 1000 || System.Math.Abs(position.Y) > 1000)
{
Destroy(entity);
}
}
foreach (var (bulletEntity, newOwnerEntity) in BulletsToDeactivate)
{
MakeBulletInactive(bulletEntity, newOwnerEntity);
}
foreach (var entity in EntitiesToDestroy)
{
Destroy(entity);
}
EntitiesToDestroy.Clear();
EntitiesToDisableEffectCollision.Clear();
BulletsToDeactivate.Clear();
BreaksToProcess.Clear();
}
private void ProcessDeflectInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<CanDeflect>(first) && Has<CanBeDeflected>(second))
{
ProcessDeflect(first, second);
}
if (Has<CanDeflect>(second) && Has<CanBeDeflected>(first))
{
ProcessDeflect(second, first);
}
}
}
private void ProcessObstructInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<CanObstruct>(first) && Has<CanBeObstructed>(second))
{
ProcessObstruct(first, second);
}
if (Has<CanObstruct>(second) && Has<CanBeObstructed>(first))
{
ProcessObstruct(second, first);
}
}
}
private void ProcessDamageInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<CanDamage>(first) && Has<CanBeDamaged>(second))
{
ProcessDamage(first, second);
}
if (Has<CanDamage>(second) && Has<CanBeDamaged>(first))
{
ProcessDamage(second, first);
}
}
}
private void ProcessKickInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<CanKick>(first) && Has<CanBeKicked>(second))
{
ProcessKick(first, second);
}
if (Has<CanKick>(second) && Has<CanBeKicked>(first))
{
ProcessKick(second, first);
}
}
}
private void ProcessMakeHarmlessInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
var firstPosition = Get<Position2D>(first);
var secondPosition = Get<Position2D>(second);
if (Has<CanMakeHarmless>(first) && Has<CanBecomeHarmless>(second))
{
ProcessBecomeHarmless(first, firstPosition, second, secondPosition);
}
if (Has<CanMakeHarmless>(second) && Has<CanBecomeHarmless>(first))
{
ProcessBecomeHarmless(second, secondPosition, first, firstPosition);
}
}
}
private void ProcessCancelDeadlyTumbleInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<DamageOnSolidCollision>(first) && Has<CanCancelDeadlyTumbling>(second))
{
ProcessCancelDeadlyTumble(first, second);
}
if (Has<DamageOnSolidCollision>(second) && Has<CanCancelDeadlyTumbling>(first))
{
ProcessCancelDeadlyTumble(second, first);
}
}
}
private void ProcessRemoveAmmoInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<CanRemoveAmmo>(first) && Has<Ammo>(second))
{
ProcessRemoveAmmo(first, second);
}
if (Has<CanRemoveAmmo>(second) && Has<Ammo>(first))
{
ProcessRemoveAmmo(second, first);
}
}
}
private void ProcessPushGrassInteractions(Fix64 dt)
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<CanPushGrass>(first) && Has<GrassCollision>(second))
{
ProcessPushGrass(first, second, dt);
}
if (Has<CanPushGrass>(second) && Has<GrassCollision>(first))
{
ProcessPushGrass(second, first, dt);
}
}
}
private void ProcessCutInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
var firstPosition = Get<Position2D>(first);
var secondPosition = Get<Position2D>(second);
if (Has<CanCut>(first) && Has<CanBeCut>(second))
{
ProcessCut(first, firstPosition, second, secondPosition);
}
if (Has<CanCut>(second) && Has<CanBeCut>(first))
{
ProcessCut(second, secondPosition, first, firstPosition);
}
}
}
private void ProcessBreakInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<CanBreak>(first) && Has<CanBeBroken>(second))
{
BreaksToProcess.Add((first, second));
}
if (Has<CanBreak>(second) && Has<CanBeBroken>(first))
{
BreaksToProcess.Add((second, first));
}
}
foreach (var (first, second) in BreaksToProcess)
{
var firstPosition = Get<Position2D>(first);
var secondPosition = Get<Position2D>(second);
ProcessBreak(first, firstPosition, second, secondPosition);
}
}
private void ProcessTumbleInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
// One character is tumbling and the other is not
// Transfer the tumbling motion to the one who is not tumbling
if (Has<Tumbling>(first) && Has<CanBeTumbled>(second) && !Has<Tumbling>(second))
{
ProcessTumbleTransfer(first, second);
}
if (Has<Tumbling>(second) && Has <CanBeTumbled>(first) && !Has<Tumbling>(first))
{
ProcessTumbleTransfer(second, first);
}
// Both characters are tumbling, so they bounce off of eachother
if (Has<Tumbling>(first) && Has<Tumbling>(second) && first != second)
{
ProcessTumbleReflect(first, second);
}
}
}
private void ProcessPaintInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<CanPaint>(first) && Has<CanBePainted>(second))
{
ProcessPaint(first, second);
}
if (Has<CanPaint>(second) && Has<CanBePainted>(first))
{
ProcessPaint(second, first);
}
}
}
private void ProcessSuperFusions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<CanSuperFuse>(first) &&
Has<CanSuperFuse>(second) &&
first != second)
{
ProcessSuperFuse(first, second);
}
}
}
private void ProcessSwallowInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<CanSwallow>(first) && Has<CanBeSwallowed>(second))
{
ProcessSwallow(first, second);
}
if (Has<CanSwallow>(second) && Has<CanBeSwallowed>(first))
{
ProcessSwallow(second, first);
}
}
}
private void ProcessOtomoPowerBoostInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<CanBeOtomoPowerBoosted>(first) && Has<CanOtomoPowerBoost>(second))
{
ProcessOtomoPowerBoost(first, Has<SuperPowered>(second));
}
if (Has<CanBeOtomoPowerBoosted>(second) && Has<CanOtomoPowerBoost>(first))
{
ProcessOtomoPowerBoost(second, Has<SuperPowered>(first));
}
}
}
private void ProcessDeadlyThrowInteractions()
{
foreach (var (first, second) in EffectColliderHash.Pairs)
{
if (Has<Dashing>(first) && Has<Dashing>(second) && Has<CanDeadlyThrowOnDash>(first) && Has<CanDeadlyThrowOnDash>(second))
{
ProcessDeadlyThrowClash(first, second);
}
else
{
if (Has<Dashing>(first) && Has<CanDeadlyThrowOnDash>(first) && Has<CanBeDeadlyThrown>(second) && !Related<BeingDeadlyGrabbedBy>(first, second))
{
ProcessDeadlyThrow(first, second);
}
if (Has<Dashing>(second) && Has<CanDeadlyThrowOnDash>(second) && Has<CanBeDeadlyThrown>(first) && !Related<BeingDeadlyGrabbedBy>(second, first))
{
ProcessDeadlyThrow(second, first);
}
}
}
}
private void ProcessCancelDeadlyTumble(Entity first, Entity second)
{
Remove<DamageOnSolidCollision>(first);
Set(first, new Freeze(Frames.ToSeconds(2)));
Set(first, new MotionRamp(new Fix64(4)));
Set(first, new Tumbling(Fix64.Zero, Get<CanBeTumbled>(first).DefaultTime));
}
private void ProcessDeadlyThrowClash(Entity first, Entity second)
{
var firstToSecondDirection = Vector2.Normalize(Get<Position2D>(second) - Get<Position2D>(first));
var speed = new Fix64(300);
var freezeTime = Frames.ToSeconds(10);
Set(first, new Freeze(freezeTime));
Set(second, new Freeze(freezeTime));
var shakeDir = Vector2.Rotate(firstToSecondDirection, Fix64.PiOver4 * Random.Unsynced.Flip());
Set(first, new Shake(shakeDir * new Fix64(2), Fix64.FromFraction(11, 10), Frames.ToSeconds(2)));
Set(second, new Shake(shakeDir * new Fix64(2), Fix64.FromFraction(11, 10), Frames.ToSeconds(2)));
CharacterManipulator.TumblingStart(first, Get<CanBeTumbled>(first).DefaultTime, -firstToSecondDirection * speed);
CharacterManipulator.TumblingStart(second, Get<CanBeTumbled>(first).DefaultTime, firstToSecondDirection * speed);
Remove<Dashing>(first);
Remove<Dashing>(second);
Relate(first, second, new CantTumbleTransferUntilNotOverlapping());
var firstPosition = Get<Position2D>(first);
var secondPosition = Get<Position2D>(second);
// Big Flash FX
Send(new EffectMessage
{
Position = new Position2D(
(firstPosition.X + secondPosition.X) / 2,
((firstPosition.Y + secondPosition.Y) / 2) - 7
),
Angle = Random.Unsynced.Cardinal(),
FlipX = Random.Unsynced.CoinFlip(),
FlipY = Random.Unsynced.CoinFlip(),
SpriteAnimation = new SpriteAnimation(
Content.SpriteAnimations.FX_Clash_Super,
60,
false
)
});
}
private void ProcessDeadlyThrow(Entity thrower, Entity thrown)
{
CharacterManipulator.DeadlyTumblingCancel(thrown);
Remove<Dashing>(thrown);
Remove<CannotMoveThroughSolid>(thrown);
Remove<UseSweepMovement>(thrown);
Remove<Dashing>(thrower);
var dashDir = Vector2.Normalize(Get<Velocity2D>(thrower).Velocity);
var throwerVelocity = Get<Velocity2D>(thrower).Velocity;
var offset = Get<CanBeDeadlyThrown>(thrown).Offset;
var freezeTime = Frames.ToSeconds(12);
Set(thrower, new Velocity2D(throwerVelocity / new Fix64(4)));
Set(thrower, new Freeze(freezeTime));
Set(thrown, new Shake(dashDir * new Fix64(2), Fix64.FromFraction(11, 10), Frames.ToSeconds(2)));
Set(thrown, new Freeze(freezeTime));
Set(thrown, new Velocity2D(Vector2.Zero));
var thrownPosition = Get<Position2D>(thrown);
var throwerPositionOffset = Get<Position2D>(thrower) + (dashDir * new Fix64(4));
var newPosition = new Position2D(
(thrownPosition.X + throwerPositionOffset.X) / 2,
(thrownPosition.Y + throwerPositionOffset.Y) / 2
);
Set(thrown, newPosition);
Set(thrown, new CanBreak());
Set(thrown, new CannotMoveThroughUnbreakable());
var throwAnticipationTime = Fix64.FromFraction(1, 2);
Relate(thrown, thrower, new BeingDeadlyGrabbedBy(Fix64.Zero, throwAnticipationTime, dashDir));
Relate(thrown, thrower, new CantDamage());
foreach(var weapon in InRelations<MeleeAttackOf>(thrown))
{
Remove<CanDamage>(weapon);
EntitiesToDestroy.Add(weapon);
}
foreach(var weapon in InRelations<MeleeAttackOf>(thrower))
{
Remove<CanDamage>(weapon);
EntitiesToDestroy.Add(weapon);
}
}
private void ProcessDamage(Entity damagerEntity, Entity damagedEntity)
{
if (Related<CantDamage>(damagerEntity, damagedEntity) ||
Related<CantDamageUntilNotOverlapping>(damagerEntity, damagedEntity) ||
Has<HarmlessTimer>(damagerEntity))
{
return;
}
var damagerPosition = Get<Position2D>(damagerEntity);
var damagedPosition = Get<Position2D>(damagedEntity);
var damagerVector = new Vector2(damagedPosition.X - damagerPosition.X, damagedPosition.Y - damagerPosition.Y);
// explosion
if (Has<ExplodesOnImpact>(damagerEntity))
{
GameplaySpawner.SpawnExplosionPops(
OutRelationSingleton<BelongsToPlayer>(damagerEntity),
damagerPosition
);
EntitiesToDestroy.Add(damagerEntity);
}
Vector2 damagerDirection;
if (Has<Direction>(damagerEntity))
{
damagerDirection = Get<Direction>(damagerEntity).Value;
}
else
{
damagerDirection = Vector2.Normalize(damagerVector);
}
bool cancelDamage = false;
if (EntitiesToDestroy.Contains(damagedEntity))
{
cancelDamage = true;
}
if (Has<NoDamageOnDashing>(damagerEntity) && Has<Dashing>(damagedEntity))
{
cancelDamage = true;
}
if (Related<Follow>(damagerEntity, damagedEntity))
{
cancelDamage = true;
}
if (Has<NoDamageIfHorizontalMotionGreaterThanVertical>(damagerEntity) && Has<Velocity2D>(damagedEntity))
{
var velocity = Get<Velocity2D>(damagedEntity);
if (Fix64.Abs(velocity.Velocity.X) > Fix64.Abs(velocity.Velocity.Y))
{
cancelDamage = true;
}
}
if (Has<NoDamageIfMovingUpward>(damagerEntity) && Has<Velocity2D>(damagedEntity))
{
var velocity = Get<Velocity2D>(damagedEntity);
if (velocity.Velocity.Y < 0)
{
cancelDamage = true;
}
}
if (Has<PreventCollisionEffectOnUnbreakableLinecast>(damagerEntity))
{
if (ThroughUnbreakableCheck(damagerEntity, damagerPosition, damagedPosition))
{
cancelDamage = true;
}
}
if (cancelDamage)
{
if (Has<CantInteractAfterDamageAvoided>(damagerEntity))
{
Relate<CantDamageUntilNotOverlapping>(damagerEntity, damagedEntity, new CantDamageUntilNotOverlapping());
}
return;
}
var damagerVelocity = Vector2.Zero;
if (Has<Velocity2D>(damagerEntity))
{
damagerVelocity = Get<Velocity2D>(damagerEntity).Velocity;
Set(damagerEntity, new MotionRamp(new Fix64(4)));
}
if (Has<CanBeBeheaded>(damagedEntity) && Has<CanBehead>(damagerEntity))
{
if (Get<Direction>(damagerEntity).Value.X != Fix64.Zero)
{
Entity beheader;
if (HasInRelation<Owns>(damagerEntity))
{
beheader = InRelationSingleton<Owns>(damagerEntity);
}
else
{
beheader = damagerEntity;
}
var beheaderOwnerPosition = Get<Position2D>(beheader);
var beheadedPosition = Get<Position2D>(damagedEntity);
if (beheaderOwnerPosition.Y <= beheadedPosition.Y - 12)
{
var headSprite = Get<CanBeBeheaded>(damagedEntity).HeadSpriteID
.SpriteAnimationInfo;
Send(
new SpawnHeadMessage(
beheadedPosition + new Position2D(0, -8),
headSprite,
Get<PaletteSprite>(damagedEntity),
Get<PaletteIndex>(damagedEntity),
beheaderOwnerPosition.X <=
beheadedPosition.X,
Get<FacingDirection>(damagedEntity).Right));
}
}
}
CharacterManipulator.SetKilledBy(damagedEntity, OutRelationSingleton<BelongsToPlayer>(damagerEntity));
Send(new DestroyMessage(damagedEntity, damagerDirection));
if (Has<DamageOnDamaging>(damagerEntity))
{
Relate(damagerEntity, InRelationSingleton<Controls>(damagedEntity), new KilledBy());
Send(new DestroyMessage(damagerEntity, -damagerDirection));
}
}
private void ProcessDeflect(Entity deflector, Entity deflected)
{
if (Related<CantDeflectUntilNotOverlapping>(deflector, deflected)) { return; }
if (Has<ChangeDirectionOnDeflect>(deflected))
{
var position = Get<Position2D>(deflected);
var velocity = Get<Velocity2D>(deflected).Velocity;
var deflectedInitialDirection = Get<Direction>(deflected).Value;
var deflectorDirection = Get<Direction>(deflector).Value;
var speed = velocity.Length();
// Speed Up
// Speed in the original game was increased by 30 per hit
// Multiplying the speed creates a proportional increase which is more exciting
speed *= Fix64.FromFraction(105, 100);
Vector2 newVelocity = Vector2.Zero;
if (deflectorDirection.X > 0)
{
newVelocity = new Vector2(speed, Fix64.Zero);
}
else if (deflectorDirection.X < 0)
{
newVelocity = new Vector2(-speed, Fix64.Zero);
}
else if (deflectorDirection.Y > 0)
{
newVelocity = new Vector2(Fix64.Zero, speed);
}
else if (deflectorDirection.Y < 0)
{
newVelocity = new Vector2(Fix64.Zero, -speed);
}
// aim should not be zero, so if it is, something is very wrong
var deflectorPosition = Get<Position2D>(deflector);
Position2D newProjectilePosition = position;
// Set Bullet Position to Relative Deflector Position, ie: going up? Set X to sword X, going left? Set Y to Sword Y
if (Fix64.Abs(newVelocity.X) > 0)
{
newProjectilePosition = new Position2D(position.X, deflectorPosition.Y);
}
if (Fix64.Abs(newVelocity.Y) > 0)
{
newProjectilePosition = new Position2D(deflectorPosition.X, position.Y);
}
newProjectilePosition += newVelocity / new Fix64(60);
var speedProportion = speed / new Fix64(350);
var shakeAmount = Fix64.FromFraction(11 + (int) speedProportion, 10);
Fix64 freezeTime = Frames.ToSeconds(3 + (int) speedProportion);
// Affect the deflector (sword, whip, weapon, etc)
if (Has<SpriteAnimation>(deflector)) // Advance the sword animation before freeze to emphasize hit. We don't want to freeze on a smear frame
{
var delfectorSprite = Get<SpriteAnimation>(deflector);
Set(deflector, delfectorSprite.Update(Fix64.FromFraction(2, 60)));
}
Set(deflector, new Freeze(freezeTime));
Set(deflector, new Shake(-deflectorDirection * new Fix64(2), shakeAmount, Frames.ToSeconds(2)));
// TODO: if bullet can bounce, allow it to bounce again
// TODO: if bullet can be driven by Yellow Devil, it can no longer be driven
// Affect the deflector's owner (Player Character)
var superDeflection = Has<SuperPowered>(deflected);
var deflectSpriteColor = superDeflection ? Colors.SuperDeflect : Colors.BulletYellow;
foreach(var owner in InRelations<Owns>(deflector))
{
if (Has<Dashing>(owner))
{
// Turn bullet super
if (!Has<SuperPowered>(deflected))
{
superDeflection = true;
deflectSpriteColor = MoonWorks.Graphics.Color.White;
Set(deflected, new Freeze(freezeTime + freezeTime / 2)); // extra freeze when turning super
BulletTransformer.TransformToSuperBullet(deflected);
}
}
// TODO: ghost gets flying charge back
// TODO: KD gets this ammo
// TODO: Slug can angle this reflected bullet
Set(owner, new Freeze(freezeTime));
Set(owner, new Shake(-deflectorDirection, shakeAmount, Frames.ToSeconds(2)));
Set(owner, new MotionRamp(new Fix64(4)));
Set(owner, new FlingIsReady());
// Push the deflector's owner back
if (Has<Velocity2D>(owner))
{
var ownerVelocity = Get<Velocity2D>(owner).Velocity;
if (deflectorDirection.Y > 0)
{
ownerVelocity.Y /= new Fix64(2);
}
var pushBackSpeed = Fix64.FromFraction(19, 10) * new Fix64(60);
var pushBackAngle = deflectorDirection.Angle();
var pushBackVector = Vector2.Rotate(-Vector2.UnitX * pushBackSpeed, pushBackAngle);
Set(owner, new Velocity2D(
ownerVelocity + pushBackVector
));
}
}
// Turn into Wobble bullet
if ((Has<WobbleOnSameDirectionDeflect>(deflected) && deflectorDirection == Get<Direction>(deflected).Value) ||
Has<WobbleDeflectedOnDeflect>(deflector))
{
Send(new PlayStaticSoundMessage(Content.StaticAudio.Bullet_Slow1));
// fractional motion ramp here to push the slow bullet farther for a bit and avoid collision w owner
Set(deflected, new MotionRamp(Fix64.FromFraction(1, 2)));
Remove<WobbleOnSameDirectionDeflect>(deflector);
var startingSpeed = newVelocity.Length();
newVelocity /= new Fix64(3);
if (newVelocity.Length() < BulletSpeeds.WobbleMinimum)
{
newVelocity = Vector2.Normalize(newVelocity) * BulletSpeeds.WobbleMinimum;
}
var storedSpeed = (startingSpeed - newVelocity.Length()) * Fix64.FromFraction(3, 2);
if (Has<Wobble>(deflected))
{
storedSpeed += Get<Wobble>(deflected).StoredSpeed; // Add to stored speed if already stored
}
BulletTransformer.TransformToWobbleBullet(deflected, storedSpeed);
superDeflection = false;
// Redirect the bullet if it was a stance switch
if (Has<WobbleDeflectedOnDeflect>(deflector))
{
foreach(var owner in InRelations<Owns>(deflector))
{
if (Has<Aim>(owner))
{
var aimDirection = Vector2.Normalize(Get<Aim>(owner).OrdinalAimAxes);
newVelocity = newVelocity.Length() * aimDirection;
// if aiming same direction as bullet, put on top of player so they don't die
if (deflectedInitialDirection == aimDirection)
{
newProjectilePosition = deflectorPosition;
}
// if aiming up or down, push bullet forward in character side facing direction
else if (aimDirection.Y != Fix64.Zero)
{
newProjectilePosition += new Position2D(new Fix64(8) * Get<FacingDirection>(owner).ToFix64Sign(), Fix64.Zero);
}
}
}
}
}
else
{
// if not turning into wobble bullet, normal motion ramp
Set(deflected, new MotionRamp(new Fix64(2)));
}
// Release stored speed of a super wobble
if (superDeflection && Has<Wobble>(deflected))
{
// TODO: FX for releasing stored speed
var storedSpeed = Get<Wobble>(deflected).StoredSpeed;
newVelocity = Vector2.Normalize(newVelocity) * (newVelocity.Length() + storedSpeed);
Remove<Wobble>(deflected);
var distortionRippleEntity = CreateEntity();
Set(distortionRippleEntity, newProjectilePosition);
Set(distortionRippleEntity, new ShockwaveDistortion(0, 20));
Set(distortionRippleEntity, new FusionExplosionEffectTimer(Fix64.FromFraction(1, 1)));
Set(distortionRippleEntity, new DestroyOnTransition());
}
// Affect the deflected (bullet, fireball, projectile, etc)
Set(deflected, new Shake(Vector2.Rotate(-deflectorDirection * Fix64.FromFraction(3, 2), Fix64.PiOver2), shakeAmount, Frames.ToSeconds(2)));
Set(deflected, new Freeze(freezeTime));
Set(deflected, new Direction(Vector2.Normalize(newVelocity)));
Set(deflected, new Velocity2D(newVelocity));
Set(deflected, new SpriteRotation(Vector2.Normalize(newVelocity).Angle()));
Set(deflected, newProjectilePosition);
if (Has<ResetWrapOnDeflect>(deflected))
{
Set(deflected, new Wrap());
}
// FX
Position2D fxPosition = (Position2D) (newProjectilePosition - (newVelocity / new Fix64(60)));
EffectSpawner.SpawnDeflectFX(fxPosition, newVelocity.Angle(), deflectSpriteColor);
// Line Sparks
int maxLineSparks = 6;
foreach(var lineSpark in Random.Unsynced.LinearCongruentialGenerator(maxLineSparks))
{
// OG Speed = range(5, 21)
Fix64 t = new Fix64(lineSpark) / new Fix64(maxLineSparks);
Fix64 sparkSpeed = Fix64.Lerp(new Fix64(6 * 60), new Fix64(13 * 60), t);
var sparkVector = Vector2.Rotate(Vector2.UnitX * sparkSpeed, newVelocity.Angle() + Random.Unsynced.Mirror(Fix64.ToRadians(new Fix64(45))));
EffectSpawner.SpawnLineSpark(
fxPosition + (sparkVector / new Fix64(60)),
sparkVector,
superDeflection? Colors.SuperDeflect : Colors.GetRandomHotSparkColor()
);
}
// Sprite Sparks
if (superDeflection)
{
int maxSparks = 8;
foreach(var spark in Random.Unsynced.LinearCongruentialGenerator(maxSparks))
{
Fix64 t = new Fix64(spark) / new Fix64(maxSparks);
var sparkSpeed = newVelocity.Length() * Fix64.Lerp(Fix64.FromFraction(1, 8), new Fix64(2), t);
var sparkVector = Vector2.Rotate(Vector2.UnitX * sparkSpeed, newVelocity.Angle() + Random.Unsynced.Mirror(Fix64.ToRadians(new Fix64(45))));
EffectSpawner.SpawnSuperSparks(fxPosition + (sparkVector / new Fix64(30)), sparkVector);
}
}
else
{
int maxSparks = 6;
foreach(var spark in Random.Unsynced.LinearCongruentialGenerator(maxSparks))
{
Fix64 t = new Fix64(spark) / new Fix64(maxSparks);
var sparkSpeed = newVelocity.Length() * Fix64.Lerp(Fix64.FromFraction(1, 8), Fix64.FromFraction(3, 2), t);
var sparkVector = Vector2.Rotate(Vector2.UnitX * sparkSpeed, newVelocity.Angle() + Random.Unsynced.Mirror(Fix64.ToRadians(new Fix64(45))));
EffectSpawner.SpawnSparks(fxPosition + (sparkVector / new Fix64(30)), sparkVector);
}
}
// Add to times deflected
var timesDeflected = 0;
if (Has<TimesDeflected>(deflected))
{
timesDeflected = Get<TimesDeflected>(deflected).Amount;
Set(deflected, new TimesDeflected(Get<TimesDeflected>(deflected).Amount + 1));
}
else
{
Set(deflected, new TimesDeflected(1));
}
// Update followers (blue flames on Otomo's super)
foreach (var follower in InRelations<Follow>(deflected))
{
Set(follower, new SpriteRotation(newVelocity.Angle()));
}
// SFX
var pitch = Math.Min(Music.HalfStep * 12, Music.HalfStep * timesDeflected);
Send(new PlayStaticSoundMessage(Random.Unsynced.Choose(StaticAudioArrays.Sword_ReflectBullet), 1, pitch));
Send(new DeferReplaceBulletColliderMessage(deflected.ID, newProjectilePosition, Vector2.Normalize(newVelocity), -1));
}
if (Has<TumbleDeflectorOwnerOnDeflect>(deflected))
{
// See if you got launched
bool launchDeflector = true;
bool superClash = true;
if (Has<TimeAlive>(deflected) && Has<TimeAlive>(deflector))
{
var deflectedTime = Get<TimeAlive>(deflected).Time;
var deflectorTime = Get<TimeAlive>(deflector).Time;
if (deflectorTime < deflectedTime)
{
launchDeflector = false;
}
if (deflectedTime != deflectorTime)
{
superClash = false;
}
if (HasInRelation<Owns>(deflector))
{
var deflectorOwner = InRelationSingleton<Owns>(deflector);
if (HasInRelation<Owns>(deflected))
{
var deflectedOwner = InRelationSingleton<Owns>(deflected);
if (!Has<Dashing>(deflectedOwner) && Has<Dashing>(deflectorOwner))
{
launchDeflector = false;
}
}
}
}
if (Has<CancelTumbleOwnerOnDeflectIfDashing>(deflected))
{
if (HasInRelation<Owns>(deflector))
{
var owner = InRelationSingleton<Owns>(deflector);
if (Has<Dashing>(owner))
{
launchDeflector = false;
}
}
}
if (launchDeflector)
{
if (HasInRelation<Owns>(deflector))
{
var owner = InRelationSingleton<Owns>(deflector);
// Dampen velocity
var newVelocity = Get<Velocity2D>(owner).Velocity;
var motionDamping = new Fix64(3);
newVelocity /= motionDamping;
// Get effect collider or solid collider center
Position2D ownerCenter = Get<Position2D>(owner) + Get<CanBeTumbled>(owner).DeflectionOriginOffset;
Position2D deflectedCenter = Get<Position2D>(deflected);
// Launch owner away from deflection
var ownerToDeflectedDirection = Vector2.Normalize(ownerCenter - deflectedCenter);
var speed = new Fix64(9 * 60);
var ownerToDeflectedVelocity = ownerToDeflectedDirection * speed;
newVelocity += ownerToDeflectedVelocity + new Vector2(0, -200);
// Update components
CharacterManipulator.TumblingStart(owner, Get<CanBeTumbled>(owner).DefaultTime, newVelocity);
}
}
// Clash FX
if (!Related<ClashFX>(deflected, deflector))
{
Relate(deflected, deflector, new ClashFX());
Relate(deflector, deflected, new ClashFX());
// Affect Characters
if (HasInRelation<Owns>(deflected))
{
Set(
InRelationSingleton<Owns>(deflected),
new Freeze(Frames.ToSeconds(4)));
}
if (HasInRelation<Owns>(deflector))
{
Set(
InRelationSingleton<Owns>(deflector),
new Freeze(Frames.ToSeconds(4)));
}
var positionDeflected = Get<Position2D>(deflected);
var positionDeflector = Get<Position2D>(deflector);
var position = new Position2D(
(positionDeflected.X + positionDeflector.X) / 2,
(positionDeflected.Y + positionDeflector.Y) / 2
);
// light
var light = CreateEntity();
Set(light, position);
Set(light, new RadialLight(500, new Fix64(70), superClash ? Colors.ClashLightSuper : Colors.ClashLightDefault, 4, 1f, 0.4f));
Set(light, new LightFade(500, Fix64.Zero, Frames.ToSeconds(15)));
Set(light, new DestroyOnTransition());
// Big Flash FX
Send(new EffectMessage
{
Position = position,
Angle = Random.Unsynced.Cardinal(),
FlipX = Random.Unsynced.CoinFlip(),
FlipY = Random.Unsynced.CoinFlip(),
SpriteAnimation = new SpriteAnimation(
superClash ? Content.SpriteAnimations.FX_Clash_Super : Content.SpriteAnimations.FX_Clash,
30,
false
)
});
// Sprite Sparks
if (superClash)
{
int maxSpriteSparks = 20;
foreach (var spriteSpark in Random.Unsynced.LinearCongruentialGenerator(maxSpriteSparks))
{
var t = (Fix64) spriteSpark / (Fix64) maxSpriteSparks;
EffectSpawner.SpawnSuperSparks(
position,
Vector2.Rotate(Vector2.UnitX * Fix64.Lerp(new Fix64(300), new Fix64(700), t), Random.Unsynced.Fixed(Fix64.PiTimes2))
);
}
}
else
{
var maxSpriteSparks = 15;
foreach (var spriteSpark in Random.Unsynced.LinearCongruentialGenerator(maxSpriteSparks))
{
var t = (Fix64) spriteSpark / (Fix64) maxSpriteSparks;
EffectSpawner.SpawnSparks(
position,
Vector2.Rotate(Vector2.UnitX * Fix64.Lerp(new Fix64(300), new Fix64(600), t), Random.Unsynced.Fixed(Fix64.PiTimes2))
);
}
}
// Line Sparks
for (var i = 0; i <= 15; i++)
{
EffectSpawner.SpawnLineSpark(
position,
Vector2.Rotate(new Vector2(new Fix64(8) + Random.Unsynced.Fixed(12) * new Fix64(60), Fix64.Zero), Random.Unsynced.Fixed(Fix64.PiTimes2)),
Random.Unsynced.Int(4) == 0 ? MoonWorks.Graphics.Color.White : superClash ? Colors.Super : Colors.GetRandomHotSparkColor()
);
}
// Delayed Directional Particles
var clashParticleColor = superClash ? Colors.ClashLightSuper : Colors.BulletYellow;
if (Has<Direction>(deflected))
{
var direction = -Get<Direction>(deflected).Value;
EffectSpawner.SpawnSwordFlashParticleTrio(position, direction.Angle(), clashParticleColor);
}
if (Has<Direction>(deflector))
{
var direction = -Get<Direction>(deflector).Value;
EffectSpawner.SpawnSwordFlashParticleTrio(position, direction.Angle(), clashParticleColor);
}
}
}
if (Has<DisableOnDeflect>(deflected))
{
EntitiesToDisableEffectCollision.Add(deflected);
Set(deflected, new DestroyTimer(Fix64.FromFraction(1, 5)));
var spriteAnimation = Get<SpriteAnimation>(deflected);
Set(deflected, new SpriteAnimation(spriteAnimation.SpriteAnimationInfo, 0, false, spriteAnimation.RawFrameIndex));
Set(deflected, new DestroyTimer(Frames.ToSeconds(2)));
UnrelateAll<Follow>(deflected);
}
else
{
Relate(deflector, deflected, new CantDeflectUntilNotOverlapping());
}
if (Has<ChangeOwnerOnDeflect>(deflected))
{
if (HasInRelation<Owns>(deflected))
{
var previousOwner = InRelationSingleton<Owns>(deflected);
Unrelate<Owns>(previousOwner, deflected);
}
Entity newOwner;
if (HasInRelation<Owns>(deflector))
{
newOwner = InRelationSingleton<Owns>(deflector);
}
else
{
newOwner = deflector;
}
Relate(newOwner, deflected, new Owns());
Relate(deflected, newOwner, new CantDamageUntilNotOverlapping());
}
}
private void ProcessObstruct(Entity obstructer, Entity obstructed)
{
if (Has<StunOwnerOwnerOnObstruct>(obstructed))
{
var position = Get<Position2D>(obstructed);
var direction = Get<Direction>(obstructed);
if (HasInRelation<Owns>(obstructed))
{
var owner = InRelationSingleton<Owns>(obstructed);
if (HasInRelation<Owns>(owner))
{
var ownerOwner = InRelationSingleton<Owns>(owner);
// Sparks that come out of the collision spot
for (var i = 0; i < 4; i++)
{
Position2D sparkPosition = position + (direction.Value * new Fix64(32));
var sparkExtraAngle = Random.Synced.Mirror(Fix64.ToRadians(new Fix64(60)));
var sparkSpeed = Frames.ToPerSecondSpeed(new Fix64(Random.Synced.Range(5, 10)));
var sparkVelocity = Vector2.Normalize(-direction.Value) * sparkSpeed;
sparkVelocity = Vector2.Rotate(sparkVelocity, sparkExtraAngle);
MoonWorks.Graphics.Color color;
var randomColorIndex = Random.Synced.Int(3);
if (randomColorIndex == 0)
{
color = MoonWorks.Graphics.Color.White;
}
else
{
color = Colors.GetRandomHotSparkColor();
}
var startPosition = new Position2D(
sparkPosition.X + Random.Synced.Mirror(5),
sparkPosition.Y + Random.Synced.Mirror(5)
);
var xStep = (int) -direction.Value.X * 2;
var yStep = (int) -direction.Value.Y * 2;
startPosition = GetPositionOutsideOfUnbreakable(
SinglePixelRectangle,
startPosition,
xStep,
yStep,
32
);
Send(new EffectMessage
{
Position = startPosition,
SpriteAnimation = new SpriteAnimation(Content.SpriteAnimations.FX_Spark, Random.Synced.Range(15, 30), false, Random.Synced.Range(0, 1)),
Velocity = sparkVelocity,
UseVelocity = true,
UseFallForce = true,
FallForce = Frames.ToPerSecondAcceleration(Fix64.FromFraction(1, 10)),
MotionDampFactor = new Vector2(Fix64.FromFraction(11, 10)),
UseMotionDamp = true,
UseColorBlend = true,
ColorBlend = color
});
}
// Shake sword
Set(owner, new Shake(new Vector2(direction.Value.Y * new Fix64(4), direction.Value.X * new Fix64(4)), Fix64.FromFraction(11, 10), Frames.ToSeconds(2)));
// Character Effects
Set(ownerOwner, new Shake(new Vector2(direction.Value.Y * new Fix64(2), direction.Value.X * new Fix64(2)), Fix64.FromFraction(13, 10), Frames.ToSeconds(2)));
Set(ownerOwner, new MotionRamp(new Fix64(4)));
Set(ownerOwner, new Freeze(Frames.ToSeconds(3)));
Set(ownerOwner, new Velocity2D(-direction.Value));
Send(new PlayStaticSoundMessage(Content.StaticAudio.Sword_Collision_Ring, .5f, Random.Unsynced.Mirror(.1f)));
}
}
}
/* Obstruct is processed after damage, so a sword going through
* a character to hit a wall will still hit the character,
* but the sword will be disabled next frame.
*/
if (Has<CancelCollisionEffectsOnObstruct>(obstructed))
{
if (HasInRelation<Owns>(obstructed))
{
var owner = InRelationSingleton<Owns>(obstructed);
CancelCollisionEffects(owner);
}
CancelCollisionEffects(obstructed);
}
if (Has<DestroyWhenObstructed>(obstructed))
{
EntitiesToDestroy.Add(obstructed);
}
Remove<CanBeObstructed>(obstructed);
}
private void ProcessKick(Entity kicker, Entity kicked)
{
var velocity = Get<Velocity2D>(kicked).Velocity;
var canBeKicked = Get<CanBeKicked>(kicked);
if (velocity.Length() >= canBeKicked.SpeedWindow) { return; }
var kickerVelocity = Get<Velocity2D>(kicker).Velocity;
velocity.X += kickerVelocity.X * canBeKicked.SpeedXFactor * (canBeKicked.RandomXSpeed ? Random.Synced.Range(Fix64.Zero, Fix64.One) : Fix64.One);
var yspdRatio = Fix64.Min(Fix64.Abs(kickerVelocity.X / 2), Fix64.One);
velocity.Y = (canBeKicked.RandomYSpeed ? canBeKicked.SpeedY * Random.Synced.Range(Fix64.Zero, Fix64.One) : canBeKicked.SpeedY) * yspdRatio;
Set(kicked, new Velocity2D(velocity));
if (Has<SpriteAngularVelocity>(kicked))
{
Set(kicked, new SpriteAngularVelocity(Random.Unsynced.Flip() * Fix64.Min(velocity.X / 10, new Fix64(5))));
}
}
private void ProcessBecomeHarmless(Entity makingHarmless, Position2D makingHarmlessPosition, Entity becomingHarmless, Position2D becomingHarmlessPosition)
{
if (
Has<PreventCollisionEffectOnUnbreakableLinecast>(makingHarmless) &&
ThroughUnbreakableCheck(makingHarmless, makingHarmlessPosition, becomingHarmlessPosition)
) {
return;
}
var canBecomeHarmless = Get<CanBecomeHarmless>(becomingHarmless);
Set(becomingHarmless, new HarmlessTimer(canBecomeHarmless.Time));
}
private void ProcessRemoveAmmo(Entity remover, Entity removed)
{
Set(removed, new Ammo(0));
}
private void ProcessPushGrass(Entity pusher, Entity grass, Fix64 dt)
{
var grassDistortion = Get<GrassDistortion>(grass);
var pushGrassParameters = Get<CanPushGrass>(pusher);
var velocity = Get<Velocity2D>(pusher).Velocity;
if (System.Math.Abs((float) velocity.X) > pushGrassParameters.SpeedThreshold)
{
var pushFactor = pushGrassParameters.PushFactor;
var wind = ((float) velocity.X * (float) dt) * pushFactor;
var windFactor = (wind + grassDistortion.WindFactor) / 2;
Set(grass, new GrassDistortion(
grassDistortion.DistortAngleOne,
grassDistortion.DistortAngleTwo,
grassDistortion.DistortSpeedOne,
grassDistortion.DistortSpeedTwo,
grassDistortion.CircleDistortDistance,
wind,
windFactor
));
}
}
private void ProcessCut(Entity cutter, Position2D cutterPosition, Entity gotCut, Position2D gotCutPosition)
{
if (
Has<PreventCollisionEffectOnUnbreakableLinecast>(cutter) &&
ThroughUnbreakableCheck(cutter, cutterPosition, gotCutPosition)
) {
return;
}
if (Has<LoseHeightOnCut>(gotCut))
{
if (Related<CantCutGrass>(cutter, gotCut)) { return; }
var height = Get<Height>(gotCut).Value;
var cutterY = cutterPosition.Y;
var heightDecrease = 0;
/*
if (cutterY + 2 < grassTransform.Position.Y + 16 && cutterY + 2 >= grassTransform.Position.Y + 16 - height)
{
heightDecrease = Math.Abs((grassTransform.Position.Y + 16) - (cutterY + 2));
}
else if (Random.Unsynced.Int(5) == 0)
{
heightDecrease = 1;
}
*/
heightDecrease = Random.Unsynced.Range(0, 4);
if (heightDecrease > 0)
{
// TODO: play cut sound
// TODO: grass clip particles
for (var i = 0; i < 3; i += 1)
{
var grassClipEntity = CreateEntity();
Set(grassClipEntity, new Position2D(gotCutPosition.X + Random.Unsynced.Int(16), gotCutPosition.Y + 16 - height + Random.Unsynced.Mirror(1)));
if (Random.Unsynced.CoinFlip())
{
Set(grassClipEntity, new InvertSpriteX());
}
if (Random.Unsynced.CoinFlip())
{
Set(grassClipEntity, new InvertSpriteY());
}
Set(grassClipEntity, new Velocity2D(new Fix64(60) * new Vector2(new Fix64(Random.Unsynced.Mirror(2)), (-Fix64.One - Random.Unsynced.Fixed(3)))));
Set(grassClipEntity, new SpriteAngularVelocity((Fix64) MoonWorks.Math.MathHelper.ToRadians(60 * Random.Unsynced.Flip() * (1 + Random.Unsynced.Float(2)))));
Set(grassClipEntity, new SpriteRotation(Random.Unsynced.Fixed(Fix64.PiTimes2)));
Set(grassClipEntity, new FallForce(Frames.ToPerSecondAcceleration(Fix64.FromFraction(1, 5))));
Set(grassClipEntity, new SolidCollider(SinglePixelRectangle));
Set(grassClipEntity, new SpriteAnimation(Content.SpriteAnimations.FX_Grass_Debris, 12 + Random.Unsynced.Int(30), false));
Set(grassClipEntity, new DestroyOnAnimationFinish());
Set(grassClipEntity, new DrawAsGrassClip());
}
height -= heightDecrease;
if (height <= 1)
{
EntitiesToDestroy.Add(gotCut);
}
else
{
Set(gotCut, new Height(height));
}
}
Relate(cutter, gotCut, new CantCutGrass());
}
}
private void ProcessBreak(Entity breaker, Position2D breakerPosition, Entity broken, Position2D brokenPosition)
{
if (Has<PreventCollisionEffectOnUnbreakableLinecast>(breaker))
{
var originOffset = Get<CanBeBroken>(broken).OriginOffset;
if (ThroughUnbreakableCheck(breaker, breakerPosition, brokenPosition + originOffset))
{
return;
}
}
Vector2 direction;
if (Has<Direction>(breaker))
{
direction = Get<Direction>(breaker).Value;
}
else
{
direction = Vector2.Normalize(brokenPosition - breakerPosition);
}
if (Has<FlameTrapOnImpact>(breaker) && Has<SuperPowered>(breaker))
{
// These are flame traps that are spawned when breaking through bamboo,
// they last shorter than normal flame traps and are spawned at the bamboo's center,
// rather than the bullet's point of impact
var originOffset = Get<CanBeBroken>(broken).OriginOffset;
var position = brokenPosition + originOffset;
GameplaySpawner.SpawnFlameTrap(
OutRelationSingleton<BelongsToPlayer>(breaker),
position,
new Fix64(2),
false,
direction.Y >= Fix64.FromFraction(7, 10),
direction.Y <= -Fix64.FromFraction(7, 10),
direction.X >= Fix64.FromFraction(7, 10),
direction.X <= -Fix64.FromFraction(7, 10)
);
}
if (Has<ExplodesOnImpact>(breaker) && Has<SuperPowered>(breaker))
{
GameplaySpawner.SpawnExplosionPops(
OutRelationSingleton<BelongsToPlayer>(breaker),
Get<Position2D>(breaker)
);
}
if (Has<DestroyWhenBroken>(broken))
{
Send(new DestroyMessage(broken, direction));
}
}
private void ProcessTumbleTransfer(Entity tumbler, Entity tumbleReceiver)
{
if (Related<CantTumbleTransferUntilNotOverlapping>(tumbler, tumbleReceiver) ||
Related<CantTumbleTransferUntilNotOverlapping>(tumbleReceiver, tumbler))
{
return;
}
var tumblerVelocity = Get<Velocity2D>(tumbler).Velocity;
var tumbleReceiverVelocity = Get<Velocity2D>(tumbleReceiver).Velocity;
var remainingTumbleTime = Get<Tumbling>(tumbler).EndTime - Get<Tumbling>(tumbler).Time;
CharacterManipulator.TumblingEnd(tumbler);
CharacterManipulator.TumblingStart(tumbleReceiver, remainingTumbleTime, tumbleReceiverVelocity + tumblerVelocity);
Set(tumbler, new Velocity2D((Vector2.Zero)));
Relate(tumbleReceiver, tumbler, new CantTumbleTransferUntilNotOverlapping());
// FX
var tumblerPosition = Get<Position2D>(tumbler);
var tumbleReceiverPosition = Get<Position2D>(tumbleReceiver);
var tumbleRecieverColor = Data.CharacterColors.Lookup((Character) Get<CharacterID>(tumbleReceiver).ID);
Position2D midpoint = new Position2D(
(tumblerPosition.X + tumbleReceiverPosition.X) / 2,
(tumblerPosition.Y + tumbleReceiverPosition.Y) / 2
);
EffectSpawner.SpawnTumbleTransferHitSpark(midpoint + new Position2D(0, -7), tumblerVelocity.Angle() + Fix64.PiOver2, tumbleRecieverColor);
// Motion FX
var freezeTime = Frames.ToSeconds(5);
var shake = Vector2.Rotate(Vector2.UnitX, tumblerVelocity.Angle() + (Random.Unsynced.Flip() * Fix64.PiOver2)) * new Fix64(2);
var shakeDamping = Fix64.FromFraction(12, 10);
var shakeTriggerTime = Frames.ToSeconds(2);
Set(tumbler, new MotionRamp(new Fix64(4)));
Set(tumbler, new Shake(shake, shakeDamping, shakeTriggerTime));
Set(tumbler, new Freeze(freezeTime + Frames.ToSeconds(2)));
Set(tumbleReceiver, new Freeze(freezeTime));
Set(tumbleReceiver, new Shake(shake, shakeDamping, shakeTriggerTime));
Set(tumbleReceiver, new MotionRamp(Fix64.FromFraction(7, 10)));
// SFX
float tumbleVelocityFactor = (float) tumblerVelocity.LengthSquared() / 362404f;
Send(new PlayStaticSoundMessage(Content.StaticAudio.Sword_Collision1, 1, .7f + Random.Unsynced.Mirror(Music.HalfStep)));
Send(new PlayStaticSoundMessage(Content.StaticAudio.Character_Tumble_Collision, 1, tumbleVelocityFactor - Music.HalfStep * 5 + Random.Unsynced.Mirror(Music.HalfStep)));
// Destroy sword if it was out when tumbled into
foreach (var entity in OutRelations<Owns>(tumbleReceiver))
{
if (Has<DestroyWhenOwnerTumbles>(entity))
{
EntitiesToDestroy.Add(entity);
}
}
}
private void ProcessTumbleReflect(Entity first, Entity second)
{
if (Related<CantTumbleTransferUntilNotOverlapping>(first, second) ||
Related<CantTumbleTransferUntilNotOverlapping>(second, first))
{
return;
}
var firstVelocity = Get<Velocity2D>(first);
var secondVelocity = Get<Velocity2D>(second);
var firstPosition = Get<Position2D>(first);
var secondPosition = Get<Position2D>(second);
var angleBetween = firstPosition - secondPosition;
// TODO: would be nice to push the characters apart by 8 pixels in the angle of contact
// so they arent overlapping, but we can't directly affect Transform because we don't want them to get stuck in walls
// this would be like an immediate sweep method
// FX
Position2D midpoint = new Position2D(
(firstPosition.X + secondPosition.X) / 2,
(firstPosition.Y + secondPosition.Y) / 2
);
var firstColor = Data.CharacterColors.Lookup((Character) Get<CharacterID>(first).ID);
var secondColor = Data.CharacterColors.Lookup((Character) Get<CharacterID>(second).ID);
EffectSpawner.SpawnTumbleTransferHitSpark(midpoint + new Position2D(0, -7), secondVelocity.Velocity.Angle() + Fix64.PiOver2, firstColor);
EffectSpawner.SpawnTumbleTransferHitSpark(midpoint + new Position2D(0, -7), firstVelocity.Velocity.Angle() + Fix64.PiOver2, secondColor);
// Motion FX
var freezeTime = Frames.ToSeconds(7);
var shake = Vector2.Rotate(Vector2.UnitX, angleBetween.Angle() + (Random.Unsynced.Flip() * Fix64.PiOver2)) * new Fix64(3);
var shakeDamping = Fix64.FromFraction(11, 10);
var shakeTriggerTime = Frames.ToSeconds(2);
var motionRampAmount = Fix64.FromFraction(5, 10);
Set(first, new Freeze(freezeTime));
Set(first, new MotionRamp(motionRampAmount));
Set(first, new Shake(shake, shakeDamping, shakeTriggerTime));
Set(second, new Freeze(freezeTime));
Set(second, new MotionRamp(motionRampAmount));
Set(second, new Shake(shake, shakeDamping, shakeTriggerTime));
// SFX
float tumbleVelocityFactor = (float) ((firstVelocity.Velocity.LengthSquared() + secondVelocity.Velocity.LengthSquared()) / 2) / 362404f;
Send(new PlayStaticSoundMessage(Content.StaticAudio.Sword_Collision1, 1, Random.Unsynced.Mirror(Music.HalfStep)));
Send(new PlayStaticSoundMessage(Content.StaticAudio.Character_Tumble_Collision, 1, tumbleVelocityFactor - 1 - Music.HalfStep * 5 + Random.Unsynced.Mirror(Music.HalfStep)));
// swap velocities
Set(first, secondVelocity);
Set(second, firstVelocity);
Relate(first, second, new CantTumbleTransferUntilNotOverlapping());
Relate(second, first, new CantTumbleTransferUntilNotOverlapping());
}
private void ProcessPaint(Entity painter, Entity painted)
{
var painterPosition = Get<Position2D>(painter);
var painterVelocity = Get<Velocity2D>(painter).Velocity;
var scale = Fix64.FromFraction(1, 5);
var entity = CreateEntity();
Set(entity, painterPosition + painterVelocity / new Fix64(30));
Set(entity, new SpriteRotation(Random.Synced.Fixed(Fix64.PiTimes2)));
Set(entity, new SpriteScale(new Vector2(scale)));
Set(entity, new SpriteAnimation(Content.SpriteAnimations.Stamp_Blood, 0, false, Random.Synced.Int(Content.SpriteAnimations.Stamp_Blood.Frames.Length)));
Set(entity, new ScaleTarget(Vector2.One));
Set(entity, new ScaleRate(new Vector2(scale * new Fix64(60))));
Set(entity, new DrawAsGroundBlood());
Set(entity, new DestroyOnTransition());
Relate(entity, painted, new Paints());
Relate(entity, painted, new Follow());
if (Has<DestroyWhenPainting>(painter))
{
EntitiesToDestroy.Add(painter);
}
}
private void ProcessSuperFuse(Entity entityA, Entity entityB)
{
var positionA = Get<Position2D>(entityA);
var positionB = Get<Position2D>(entityB);
var midpoint = new Position2D(
(positionA.X + positionB.X) / 2,
(positionA.Y + positionB.Y) / 2);
var fusionEntity = CreateEntity();
Set(fusionEntity, midpoint);
Set(fusionEntity, new SpriteAnimation(Content.SpriteAnimations.FX_AmmoGetOrb));
Set(fusionEntity, new FusionWarmUpEffectTimer(Fix64.One));
Set(fusionEntity, new PinchDistortion(0f, DamageRadius.SuperBulletExplosion));
Set(fusionEntity, new DrawDepth(-1999));
Set(fusionEntity, new DrawAsGameplayEffect());
Set(fusionEntity, new DestroyOnTransition());
EffectSpawner.SpawnSuperFusionCore(midpoint);
// TODO: any way we can get this to render above distortion and also not in distortion? Currently there's a doubling effect
EffectSpawner.SpawnSuperFusionHitSpark(midpoint, Vector2.Normalize(positionA - positionB));
if (HasOutRelation<BelongsToPlayer>(entityA))
{
Relate(fusionEntity, OutRelationSingleton<BelongsToPlayer>(entityA), new BelongsToPlayer());
}
if (HasOutRelation<BelongsToPlayer>(entityB))
{
Relate(fusionEntity, OutRelationSingleton<BelongsToPlayer>(entityB), new BelongsToPlayer());
}
BulletsToDeactivate.Add((entityA, fusionEntity));
BulletsToDeactivate.Add((entityB, fusionEntity));
Set(entityA, new FusionInitialVelocity(Get<Velocity2D>(entityA).Velocity));
Set(entityB, new FusionInitialVelocity(Get<Velocity2D>(entityB).Velocity));
}
private void ProcessSwallow(Entity swallower, Entity toBeSwallowed)
{
// Don't eat yourself
if (HasOutRelation<TongueOf>(swallower))
{
if (toBeSwallowed == OutRelationSingleton<TongueOf>(swallower))
return;
}
// TODO: grab multiple objects
if (!HasOutRelation<Grabbed>(swallower))
{
var velocity = Get<Velocity2D>(swallower).Velocity;
Set(swallower, new Velocity2D(velocity / new Fix64(3)));
Set(swallower, new Freeze(Frames.ToSeconds(4)));
if (HasOutRelation<TongueOf>(swallower))
{
var tongueOwnerEntity = OutRelationSingleton<TongueOf>(swallower);
Remove<TongueGrabOut>(tongueOwnerEntity);
Remove<TongueGrabFling>(tongueOwnerEntity);
Set(tongueOwnerEntity, new TongueGrabGrabbed());
}
Set(toBeSwallowed, new Freeze(Frames.ToSeconds(8)));
Set(toBeSwallowed, new Shake(Vector2.UnitX, Fix64.One, Frames.ToSeconds(2)));
Remove<Tumbling>(toBeSwallowed);
Relate(toBeSwallowed, swallower, new Follow(new Position2D(0, 7)));
Relate(swallower, toBeSwallowed, new Grabbed());
}
}
private void ProcessOtomoPowerBoost(Entity boostedEntity, bool superPowered)
{
GameplaySpawner.SetOtomoPowerBoost(boostedEntity, superPowered);
// Extra dash frames if dashing
if (Has<Dashing>(boostedEntity) && !Has<CharacterFlameTrailEffect>(boostedEntity))
{
var dashing = Get<Dashing>(boostedEntity);
Set(boostedEntity, new Dashing(dashing.Time + Frames.ToSeconds(3), dashing.Direction, dashing.ShootingDirection, dashing.Speed));
}
}
private Vector2 GetMovement(Entity entity, Vector2 velocity, Fix64 dt)
{
var movement = velocity * dt;
if (Has<Freeze>(entity))
{
// Freeze means NO movement
movement = Vector2.Zero;
}
else
{
if (Has<MotionRamp>(entity))
{
var motionRamp = Get<MotionRamp>(entity).Value;
movement /= motionRamp;
}
if (Has<Landing>(entity) || Has<JumpAnticipation>(entity))
{
movement /= new Fix64(2);
}
}
// Additional movement unaffected by motion modifiers
if (Has<MoveIgnoringModifiersTimer>(entity))
{
movement += Get<MoveIgnoringModifiersTimer>(entity).Movement * dt;
}
return movement;
}
private void UpdatePosition(Entity entity, in LevelBoundaries levelBoundaries, in Position2D oldPosition, Position2D newPosition, bool wrapped = false)
{
if (Has<Wrap>(entity))
{
var wrapCount = Get<Wrap>(entity).Count;
if (levelBoundaries.WrappedPosition(newPosition, out newPosition))
{
wrapped = true;
wrapCount += 1;
Set(entity, new Wrap(wrapCount));
if (Has<DestroyOnWrapCount>(entity))
{
if (wrapCount >= Get<DestroyOnWrapCount>(entity).Count)
{
EntitiesToDestroy.Add(entity);
}
}
}
}
if (wrapped)
{
Set(entity, new PreviousPositionForInterpolation(newPosition));
}
if (Has<ResetPositionOnBoundary>(entity))
{
var border = 16;
bool reset = false;
if (oldPosition.X < -border)
{
newPosition = newPosition.SetX(oldPosition.X + levelBoundaries.BoundaryRectangle.W + (border * 2));
reset = true;
}
else if (oldPosition.X > levelBoundaries.BoundaryRectangle.W + border)
{
newPosition = newPosition.SetX(oldPosition.X - (levelBoundaries.BoundaryRectangle.W + (border * 2)));
reset = true;
}
else if (oldPosition.Y < -border)
{
newPosition = newPosition.SetY(oldPosition.Y + levelBoundaries.BoundaryRectangle.H + (border * 2));
reset = true;
}
else if (oldPosition.Y > levelBoundaries.BoundaryRectangle.H + border)
{
newPosition = newPosition.SetY(oldPosition.Y - (levelBoundaries.BoundaryRectangle.H + (border * 2)));
reset = true;
}
if (reset)
{
if (Has<ChangeVelocityFromWindOnReset>(entity))
{
var speedFactorX = Random.Unsynced.Range(new Fix64(1), new Fix64(2));
var speedFactorY = Random.Unsynced.Range(new Fix64(1), new Fix64(2));
Set(entity, new VelocityFromWind(speedFactorX, speedFactorY));
}
}
Set(entity, new PreviousPositionForInterpolation(newPosition));
}
var moveX = newPosition.X - oldPosition.X;
var moveY = newPosition.Y - oldPosition.Y;
if (Has<EffectCollider>(entity))
{
EffectColliderHash.Update(entity, moveX, moveY);
}
if (Has<CanBeDamaged>(entity) || Has<CanBeBroken>(entity))
{
AddToExplodableHash(entity);
}
Set(entity, newPosition);
if (HasInRelation<Follow>(entity))
{
RecursivelyMoveFollowers(entity, newPosition, levelBoundaries, wrapped);
}
}
private void RecursivelyMoveFollowers(Entity entity, in Position2D newPosition, in LevelBoundaries levelBoundaries, bool wrapped)
{
foreach (var follower in InRelations<Follow>(entity))
{
var position = Get<Position2D>(follower);
UpdatePosition(follower, levelBoundaries, position, newPosition.Truncated + GetRelationData<Follow>(follower, entity).Offset, wrapped);
}
}
// TODO: might be a way to optimize this by checking if the initial rectangle overlaps the boundary first
private bool CheckSolidOverlap(Entity entity, in Position2D checkPosition, in Rectangle collider)
{
if (CheckSolidAtPosition(entity, checkPosition, collider))
{
return true;
}
if (Has<Wrap>(entity))
{
var levelBoundaries = GetSingleton<LevelBoundaries>();
if (levelBoundaries.WrapHorizontal)
{
if (CheckSolidAtPosition(entity, checkPosition + new Position2D(levelBoundaries.BoundaryRectangle.W, 0), collider))
{
return true;
}
if (CheckSolidAtPosition(entity, checkPosition + new Position2D(-levelBoundaries.BoundaryRectangle.W, 0), collider))
{
return true;
}
}
if (levelBoundaries.WrapVertical)
{
if (CheckSolidAtPosition(entity, checkPosition + new Position2D(0, levelBoundaries.BoundaryRectangle.H), collider))
{
return true;
}
if (CheckSolidAtPosition(entity, checkPosition + new Position2D(0, -levelBoundaries.BoundaryRectangle.H), collider))
{
return true;
}
}
}
return false;
}
private bool CheckUnbreakableOverlap(Entity entity, in Position2D checkPosition, in Rectangle collider)
{
if (CheckUnbreakableAtPosition(entity, checkPosition, collider))
{
return true;
}
if (Has<Wrap>(entity))
{
var levelBoundaries = GetSingleton<LevelBoundaries>();
if (levelBoundaries.WrapHorizontal)
{
if (CheckUnbreakableAtPosition(entity, checkPosition + new Position2D(levelBoundaries.BoundaryRectangle.W, 0), collider))
{
return true;
}
if (CheckUnbreakableAtPosition(entity, checkPosition + new Position2D(-levelBoundaries.BoundaryRectangle.W, 0), collider))
{
return true;
}
}
if (levelBoundaries.WrapVertical)
{
if (CheckUnbreakableAtPosition(entity, checkPosition + new Position2D(0, levelBoundaries.BoundaryRectangle.H), collider))
{
return true;
}
if (CheckUnbreakableAtPosition(entity, checkPosition + new Position2D(0, -levelBoundaries.BoundaryRectangle.H), collider))
{
return true;
}
}
}
return false;
}
private bool CheckSolidAtPosition(in Entity entity, in Position2D checkPosition, in Rectangle collider)
{
var updatedRectangle = collider.Transform(checkPosition);
return Collision.Any(SolidSpatialHash, entity, updatedRectangle);
}
private bool CheckSolidAtPosition(in Position2D checkPosition, in Rectangle collider)
{
var updatedRectangle = collider.Transform(checkPosition);
return Collision.Any(SolidSpatialHash, updatedRectangle);
}
private bool CheckUnbreakableAtPosition(in Entity entity, in Position2D checkPosition, in Rectangle collider)
{
var updatedRectangle = collider.Transform(checkPosition);
return Collision.Any(UnbreakableSpatialHash, entity, updatedRectangle);
}
private bool CheckUnbreakableAtPosition(in Position2D checkPosition, in Rectangle collider)
{
var updatedRectangle = collider.Transform(checkPosition);
return Collision.Any(UnbreakableSpatialHash, updatedRectangle);
}
// useful for when things spawn inside a wall but you don't want them to
// note that this might be slightly expensive, so dont go tooooo crazy with it
private Position2D GetPositionOutsideOfWall(Position2D startPosition, int xStep, int yStep, int maxSearchDistance)
{
if (xStep != 0 && yStep != 0)
{
var maxStep = Math.Max(xStep, yStep);
for (var i = 0; i < maxSearchDistance; i += Math.Abs(maxStep))
{
if (CheckSolidAtPosition(startPosition, Point))
{
startPosition = new Position2D(
startPosition.X + xStep,
startPosition.Y + yStep);
}
else
{
break;
}
}
}
else if (xStep != 0)
{
for (var j = 0; j < maxSearchDistance; j += Math.Abs(xStep))
{
if (CheckSolidAtPosition(startPosition, Point))
{
startPosition = startPosition.SetX(startPosition.X + xStep);
}
else
{
break;
}
}
}
else if (yStep != 0)
{
for (var j = 0; j < maxSearchDistance; j += Math.Abs(yStep))
{
if (CheckSolidAtPosition(startPosition, Point))
{
startPosition = startPosition.SetY(startPosition.Y + yStep);
}
else
{
break;
}
}
}
return startPosition;
}
private Position2D GetPositionOutsideOfUnbreakable(Rectangle collider, Position2D startPosition, int xStep, int yStep, int maxSearchDistance)
{
if (xStep != 0 && yStep != 0)
{
var maxStep = Math.Max(xStep, yStep);
for (var i = 0; i < maxSearchDistance; i += Math.Abs(maxStep))
{
if (CheckUnbreakableAtPosition(startPosition, collider))
{
startPosition = new Position2D(
startPosition.X + xStep,
startPosition.Y + yStep);
}
else
{
break;
}
}
}
else if (xStep != 0)
{
for (var j = 0; j < maxSearchDistance; j += Math.Abs(xStep))
{
if (CheckUnbreakableAtPosition(startPosition, collider))
{
startPosition = startPosition.SetX(startPosition.X + xStep);
}
else
{
break;
}
}
}
else if (yStep != 0)
{
for (var j = 0; j < maxSearchDistance; j += Math.Abs(yStep))
{
if (CheckUnbreakableAtPosition(startPosition, collider))
{
startPosition = startPosition.SetY(startPosition.Y + yStep);
}
else
{
break;
}
}
}
return startPosition;
}
private Position2D GetPositionOutsideOfUnbreakable(
Position2D startPosition,
int xStep,
int yStep,
int maxSearchDistance)
{
return GetPositionOutsideOfUnbreakable(
Point,
startPosition,
xStep,
yStep,
maxSearchDistance);
}
private Position2D GetClosestWrappedPosition(Entity entity, Position2D position, Position2D targetPosition)
{
if (!Has<Wrap>(entity))
{
return position;
}
var levelBoundaries = GetSingleton<LevelBoundaries>();
var closestPosition = position;
var distanceSquared = Vector2.DistanceSquared(position, targetPosition);
if (levelBoundaries.WrapHorizontal)
{
var leftWrapPosition = position.SetX(
position.X - levelBoundaries.BoundaryRectangle.W);
var leftDistance = Vector2.DistanceSquared(
leftWrapPosition,
targetPosition);
if (leftDistance < distanceSquared)
{
distanceSquared = leftDistance;
closestPosition = leftWrapPosition;
}
var rightWrapPosition = position.SetX(
position.X + levelBoundaries.BoundaryRectangle.W);
var rightDistance = Vector2.DistanceSquared(
rightWrapPosition,
targetPosition);
if (rightDistance < distanceSquared)
{
distanceSquared = rightDistance;
closestPosition = rightWrapPosition;
}
}
if (levelBoundaries.WrapVertical)
{
var upWrapPosition = position.SetY(
position.Y - levelBoundaries.BoundaryRectangle.H);
var upDistance = Vector2.DistanceSquared(
upWrapPosition,
targetPosition);
if (upDistance < distanceSquared)
{
distanceSquared = upDistance;
closestPosition = upWrapPosition;
}
var downWrapPosition = position.SetY(
position.Y + levelBoundaries.BoundaryRectangle.H);
var downDistance = Vector2.DistanceSquared(
downWrapPosition,
targetPosition);
if (downDistance < distanceSquared)
{
distanceSquared = downDistance;
closestPosition = downWrapPosition;
}
}
return closestPosition;
}
private void CheckSolidBreaks(Entity entity, Rectangle collider, Position2D updatedPosition)
{
if (Has<CanBreak>(entity))
{
var updatedCollider = collider.Transform(updatedPosition);
foreach (var (otherEntity, otherCollider) in SolidSpatialHash.Retrieve(entity, updatedCollider))
{
if (Has<CanBeBroken>(otherEntity) && Rectangle.TestOverlap(updatedCollider, otherCollider))
{
BreaksToProcess.Add((entity, otherEntity));
}
}
if (Has<Wrap>(entity))
{
var levelBoundaries = GetSingleton<LevelBoundaries>();
if (levelBoundaries.WrapHorizontal)
{
var wrappedPosition = updatedPosition.SetX(
updatedPosition.X - levelBoundaries.BoundaryRectangle.W);
updatedCollider = collider.Transform(wrappedPosition);
foreach (var (otherEntity, otherCollider) in SolidSpatialHash.Retrieve(entity, updatedCollider))
{
if (Has<CanBeBroken>(otherEntity) && Rectangle.TestOverlap(updatedCollider, otherCollider))
{
BreaksToProcess.Add((entity, otherEntity));
}
}
wrappedPosition = updatedPosition.SetX(
updatedPosition.X + levelBoundaries.BoundaryRectangle.W);
updatedCollider = collider.Transform(wrappedPosition);
foreach (var (otherEntity, otherCollider) in SolidSpatialHash.Retrieve(entity, updatedCollider))
{
if (Has<CanBeBroken>(otherEntity) && Rectangle.TestOverlap(updatedCollider, otherCollider))
{
BreaksToProcess.Add((entity, otherEntity));
}
}
}
if (levelBoundaries.WrapVertical)
{
var wrappedPosition = updatedPosition.SetY(
updatedPosition.Y - levelBoundaries.BoundaryRectangle.H);
updatedCollider = collider.Transform(wrappedPosition);
foreach (var (otherEntity, otherCollider) in SolidSpatialHash.Retrieve(entity, updatedCollider))
{
if (Has<CanBeBroken>(otherEntity) && Rectangle.TestOverlap(updatedCollider, otherCollider))
{
BreaksToProcess.Add((entity, otherEntity));
}
}
wrappedPosition = updatedPosition.SetY(
updatedPosition.Y + levelBoundaries.BoundaryRectangle.H);
updatedCollider = collider.Transform(wrappedPosition);
foreach (var (otherEntity, otherCollider) in SolidSpatialHash.Retrieve(entity, updatedCollider))
{
if (Has<CanBeBroken>(otherEntity) && Rectangle.TestOverlap(updatedCollider, otherCollider))
{
BreaksToProcess.Add((entity, otherEntity));
}
}
}
}
}
}
private void CancelCollisionEffects(Entity entity)
{
Remove<CanDamage>(entity);
Remove<CanDeflect>(entity);
Remove<CanObstruct>(entity);
Remove<CanCut>(entity);
Remove<CanBreak>(entity);
Remove<CanMakeHarmless>(entity);
}
private StaticSoundID[] GetBounceSounds(BounceSoundType bounceSoundType)
{
switch (bounceSoundType)
{
default:
case BounceSoundType.AmmoShell:
return StaticAudioArrays.AmmoShell;
case BounceSoundType.Bamboo:
return StaticAudioArrays.GrassCut;
}
}
private void RegisterEffectColliderEntity(Entity entity)
{
var position = Get<Position2D>(entity);
var rectangle = Get<EffectCollider>(entity).Collider.Transform(position);
EffectColliderHash.Insert(InRelationSingleton<HasEffectCollider>(entity), entity, rectangle);
}
private void DeregisterEffectColliderEntity(Entity entity)
{
EffectColliderHash.Remove(entity);
}
private void AddSolidToSpatialHash(Entity entity)
{
var position = Get<Position2D>(entity);
var rectangle = Get<SolidCollider>(entity).Collider.Transform(position);
SolidSpatialHash.Insert(entity, rectangle);
}
private void RemoveFromSolidSpatialHash(Entity entity)
{
SolidSpatialHash.Remove(entity);
}
private void AddUnbreakableToSpatialHash(Entity entity)
{
var position = Get<Position2D>(entity);
var rectangle = Get<SolidCollider>(entity).Collider.Transform(position);
UnbreakableSpatialHash.Insert(entity, rectangle);
}
private void RemoveUnbreakableFromSpatialHash(Entity entity)
{
UnbreakableSpatialHash.Remove(entity);
}
private void AddToExplodableHash(Entity entity)
{
var position = Get<Position2D>(entity);
if (Has<CanBeBroken>(entity))
{
position += Get<CanBeBroken>(entity).OriginOffset;
}
ExplodableSpatialHash.Insert(entity, new Rectangle(position, 1, 1));
}
private void RemoveFromExplodableHash(Entity entity)
{
ExplodableSpatialHash.Remove(entity);
}
private void TumbleFX(Entity entity, Position2D hitFlashSpawnPosition, Fix64 hitFlashRotation)
{
// Hit flash FX on top of character
Send(new EffectMessage
{
Position = hitFlashSpawnPosition,
Angle = hitFlashRotation,
FlipX = Random.Unsynced.CoinFlip(),
SpriteAnimation = new SpriteAnimation(Content.SpriteAnimations.FX_Flash_Jump, 60, false),
UseDepth = true,
Depth = 0
});
// Stick to wall
// TODO: make freeze a factor of the dot product of character velocity and wall normal
Set(entity, new Freeze(Frames.ToSeconds(FXTiming.TumbleHitWallFreezeFrames)));
// Shake when hitting the wall
Set(entity, new Shake(new Vector2(Fix64.Zero, new Fix64(FXTiming.TumbleHitWallShakeAmount)), Fix64.FromFraction(15, 10), Frames.ToSeconds(2)));
// Slow to fast movement for 4 frames after hitting wall
Set(entity, new MotionRamp(new Fix64(2)));
Send(new PlayStaticSoundMessage(Content.StaticAudio.Character_Tumble_Collision));
}
private void SpawnBulletHitSparks(Entity bullet, Position2D position)
{
var directionVector = Get<Direction>(bullet).Value;
// Bullet wall impact sprite
var sprite = Content.SpriteAnimations.FX_BulletHitWall;
var spriteSparks = Content.SpriteAnimations.FX_Spark;
Send(new EffectMessage
{
Position = position,
Angle = directionVector.Angle() + Fix64.Pi,
FlipY = Random.Unsynced.CoinFlip(),
SpriteAnimation = new SpriteAnimation(sprite, 30, false),
});
// Sprite Sparks
for(var i = 0; i < 6; i ++)
{
Send(new EffectMessage
{
Position = position,
Angle = Random.Unsynced.Int(4) * Fix64.PiOver4,
FlipX = Random.Unsynced.CoinFlip(),
FlipY = Random.Unsynced.CoinFlip(),
SpriteAnimation = new SpriteAnimation(spriteSparks, 20 + (i * 2), false),
UseVelocity = true,
Velocity = Vector2.Rotate(Vector2.UnitX * Random.Unsynced.Range(new Fix64(2), new Fix64(7)) * new Fix64(60), Fix64.Pi + directionVector.Angle() + Random.Unsynced.Mirror(Fix64.ToRadians(new Fix64(30)))),
UseMotionDamp = true,
MotionDampFactor = new Vector2(Fix64.FromFraction(11, 10))
});
}
// Line Sparks
for(var i = 0; i < 6; i ++)
{
EffectSpawner.SpawnLineSpark(
position,
Vector2.Rotate(Vector2.UnitX * Random.Unsynced.Range(new Fix64(5), new Fix64(15)) * new Fix64(60), Fix64.Pi + directionVector.Angle() + Random.Unsynced.Mirror(Fix64.PiOver4)),
Colors.GetRandomHotSparkColor()
);
}
}
private void SpawnSuperBulletHitSparks(Entity bullet, Position2D position)
{
var directionVector = Get<Direction>(bullet).Value;
// Bullet wall impact sprite
var sprite = Content.SpriteAnimations.FX_BulletHitWall_Blue;
var spriteSparks = Content.SpriteAnimations.FX_Spark_Blue;
Send(new EffectMessage
{
Position = position,
Angle = directionVector.Angle() + Fix64.Pi,
FlipY = Random.Unsynced.CoinFlip(),
SpriteAnimation = new SpriteAnimation(sprite, 30, false),
});
// Sprite Sparks
for(var i = 0; i < 8; i ++)
{
Send(new EffectMessage
{
Position = position,
Angle = Random.Unsynced.Int(4) * Fix64.PiOver4,
FlipX = Random.Unsynced.CoinFlip(),
FlipY = Random.Unsynced.CoinFlip(),
SpriteAnimation = new SpriteAnimation(spriteSparks, 10 + (i * 2), false),
UseVelocity = true,
Velocity = Vector2.Rotate(Vector2.UnitX * Random.Unsynced.Range(new Fix64(2), new Fix64(9)) * new Fix64(60), Fix64.Pi + directionVector.Angle() + Random.Unsynced.Mirror(Fix64.ToRadians(new Fix64(30)))),
UseMotionDamp = true,
MotionDampFactor = new Vector2(Fix64.FromFraction(11, 10))
});
}
// Line Sparks
for(var i = 0; i < 8; i ++)
{
MoonWorks.Graphics.Color color;
var randomColorIndex = Random.Synced.Int(3);
if (randomColorIndex == 0)
{
color = MoonWorks.Graphics.Color.White;
}
else
{
color = Colors.Super;
}
EffectSpawner.SpawnLineSpark(
position,
Vector2.Rotate(Vector2.UnitX * Random.Unsynced.Range(new Fix64(3), new Fix64(16)) * new Fix64(60), Fix64.Pi + directionVector.Angle() + Random.Unsynced.Mirror(Fix64.PiOver4)),
color
);
}
}
private void MakeBulletInactive(Entity bullet, Entity newOwner)
{
// deactivate
Remove<Velocity2D>(bullet);
Remove<DrawAsProjectile>(bullet);
// remove collision
foreach (var effectColliderEntity in OutRelations<HasEffectCollider>(bullet))
{
Destroy(effectColliderEntity);
}
// designate that this inactive bullet belongs to the owner
Relate(bullet, newOwner, new InactiveBulletOf());
}
private bool ThroughUnbreakableCheck(Entity entity, Position2D actorPosition, Position2D receiverPosition)
{
var closestPosition = GetClosestWrappedPosition(entity, actorPosition, receiverPosition);
return Collision.LineTest(UnbreakableSpatialHash, new Line(closestPosition, receiverPosition));
}
}
}
using MoonWorks.Math.Fixed;
namespace SamuraiGunn2
{
public struct Position2D : System.IEquatable<Position2D>
{
private Fix64 RawX;
private Fix64 RawY;
public int X { get; }
public int Y { get; }
public Position2D Truncated => new Position2D(X, Y);
public static Position2D Zero => new Position2D(0, 0);
public Position2D(int x, int y)
{
RawX = new Fix64(x);
RawY = new Fix64(y);
X = x;
Y = y;
}
public Position2D(Fix64 x, Fix64 y)
{
RawX = x;
RawY = y;
X = (int) Fix64.Round(RawX);
Y = (int) Fix64.Round(RawY);
}
public Vector2 ToVector2()
{
return new Vector2
{
X = RawX,
Y = RawY
};
}
public Position2D SetX(int x)
{
return new Position2D(new Fix64(x), RawY);
}
public Position2D SetY(int y)
{
return new Position2D(RawX, new Fix64(y));
}
public static implicit operator Vector2(Position2D positionVector)
{
return positionVector.ToVector2();
}
public static explicit operator Position2D(Vector2 vector)
{
return new Position2D(vector.X, vector.Y);
}
public static Position2D operator +(Position2D a, Position2D b)
{
return new Position2D(a.RawX + b.RawX, a.RawY + b.RawY);
}
public static Position2D operator +(Position2D a, Vector2 b)
{
return new Position2D(a.RawX + b.X, a.RawY + b.Y);
}
public static Vector2 operator -(Position2D a, Position2D b)
{
return new Position2D(a.RawX - b.RawX, a.RawY - b.RawY);
}
public static Vector2 operator -(Position2D a, Vector2 b)
{
return new Position2D(a.RawX - b.X, a.RawY - b.Y);
}
public override bool Equals(object other)
{
if (other is Position2D otherPosition)
{
return Equals(otherPosition);
}
return false;
}
public bool Equals(Position2D other)
{
return
X == other.X &&
Y == other.Y;
}
public override int GetHashCode()
{
return System.HashCode.Combine(X, Y);
}
public static bool operator ==(Position2D a, Position2D b)
{
return a.Equals(b);
}
public static bool operator !=(Position2D a, Position2D b)
{
return !(a == b);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment