Last active
September 28, 2023 18:12
-
-
Save thatcosmonaut/d40bf4b0f9d2fd180e89f506826357b3 to your computer and use it in GitHub Desktop.
Samurai Gunn 2 MotionSystem
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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