PlaySim.cs
namespace PixelPusher;
public static class PlaySim
{
public const float ViewW = 320f;
public const float ViewH = 180f;
public const float BodyW = 10f;
public const float BodyH = 14f;
const float CoyoteMax = 0.09f;
const float BufferMax = 0.1f;
public static bool Active;
public static float Px;
public static float Py;
public static float Vx;
public static float Vy;
public static bool Grounded;
public static bool FaceLeft;
public static int Coins;
public static int CoinTotal;
public static int Deaths;
public static float TimePlayed;
public static bool Won;
public static bool Dead;
public static float CamX;
public static float CamY;
public static float Anim;
public static int Version;
public static bool Walk3D;
public static float Pz;
public static string SignText = "";
static StageDoc _stage;
static TilesetDoc _tiles;
static float _coyote;
static float _buffer;
static float _spawnX;
static float _spawnY;
static float _deadT;
static readonly HashSet<int> _got = new();
public static bool Collected( int tx, int ty ) => _got.Contains( ty * 1024 + tx );
public static string TimeText
{
get
{
var t = TimePlayed;
var m = (int)(t / 60f);
var s = t - m * 60f;
return $"{m}:{s:00.00}";
}
}
public static void Start( StageDoc stage, bool fromPlaytest, int dropX = -1, int dropY = -1, bool walk3d = false )
{
_stage = stage ?? Desk.Stage;
_tiles = Desk.Tileset;
if ( _stage is null )
return;
Active = true;
Won = false;
Dead = false;
Walk3D = walk3d;
Coins = 0;
Deaths = 0;
TimePlayed = 0;
Vx = 0;
Vy = 0;
Pz = WorldStudio.SlabDepth * 0.4f;
Anim = 0;
SignText = "";
_got.Clear();
CoinTotal = _stage.Entities.Count( e => e.Kind == "coin" );
var start = _stage.Find( "start" );
if ( fromPlaytest && dropX >= 0 && dropY >= 0 )
{
_spawnX = dropX * _stage.TileSize + 3;
_spawnY = dropY * _stage.TileSize;
}
else if ( start is not null )
{
_spawnX = start.Tx * _stage.TileSize + 3;
_spawnY = start.Ty * _stage.TileSize;
}
else
{
_spawnX = 16;
_spawnY = 16;
}
Respawn();
Version++;
}
public static void Tick()
{
if ( !Active || _stage is null )
return;
if ( GameFlow.Screen is not (GameScreen.Play2D or GameScreen.Play3D) )
return;
if ( Won )
return;
if ( Dead )
{
_deadT -= Time.Delta;
if ( _deadT <= 0f )
Respawn();
return;
}
var dt = Math.Min( Time.Delta, 1f / 30f );
TimePlayed += dt;
Anim += dt;
var wasGrounded = Grounded;
var run = _stage.MaxRun;
var accel = 2400f;
var friction = 1800f;
var grav = _stage.Gravity;
var jump = _stage.JumpVel;
var ax = 0f;
if ( Input.Down( "Left" ) ) ax -= 1f;
if ( Input.Down( "Right" ) ) ax += 1f;
if ( ax < 0 ) FaceLeft = true;
else if ( ax > 0 ) FaceLeft = false;
if ( ax != 0 )
Vx = Approach( Vx, ax * run, accel * dt );
else
Vx = Approach( Vx, 0, friction * dt );
Vy += grav * dt;
if ( Vy > 900f )
Vy = 900f;
if ( Grounded )
_coyote = CoyoteMax;
else
_coyote -= dt;
if ( Input.Pressed( "Jump" ) )
_buffer = BufferMax;
else
_buffer -= dt;
if ( _buffer > 0f && _coyote > 0f )
{
Vy = -jump;
Grounded = false;
_coyote = 0;
_buffer = 0;
ChipVoices.Jump();
}
if ( !Input.Down( "Jump" ) && Vy < -40f )
Vy *= 0.55f;
MoveAxis( Vx * dt, 0 );
Grounded = false;
MoveAxis( 0, Vy * dt );
if ( !wasGrounded && Grounded )
ChipVoices.Land();
if ( Walk3D )
MoveDepth( dt );
TouchEntities();
if ( Py > _stage.Height * _stage.TileSize + 32 )
Kill();
if ( Walk3D )
{
var tx = Px + Pz * 0.55f - ViewW * 0.5f;
var ty = Py - Pz * 0.28f - ViewH * 0.6f;
CamX = CamX + (tx - CamX) * Math.Min( 1f, 8f * dt );
CamY = CamY + (ty - CamY) * Math.Min( 1f, 8f * dt );
WorldStudio.CamX = CamX;
WorldStudio.CamY = CamY;
}
else
{
var look = FaceLeft ? -28f : 28f;
var tx = Px + BodyW * 0.5f - ViewW * 0.5f + look;
var ty = Py + BodyH * 0.5f - ViewH * 0.65f;
CamX = CamX + (tx - CamX) * Math.Min( 1f, 8f * dt );
CamY = CamY + (ty - CamY) * Math.Min( 1f, 8f * dt );
}
ClampCam();
Version++;
}
static void MoveDepth( float dt )
{
var az = 0f;
if ( Input.Down( "Forward" ) ) az += 1f;
if ( Input.Down( "Backward" ) ) az -= 1f;
if ( az == 0 )
return;
var next = Pz + az * _stage.MaxRun * 0.55f * dt;
Pz = Math.Clamp( next, 2f, WorldStudio.SlabDepth - 6f );
}
static void MoveAxis( float dx, float dy )
{
Px += dx;
Py += dy;
var ts = _stage.TileSize;
var left = Px;
var right = Px + BodyW;
var top = Py;
var bot = Py + BodyH;
var x0 = Math.Max( 0, (int)MathF.Floor( left / ts ) );
var x1 = Math.Min( _stage.Width - 1, (int)MathF.Floor( (right - 0.001f) / ts ) );
var y0 = Math.Max( 0, (int)MathF.Floor( top / ts ) );
var y1 = Math.Min( _stage.Height - 1, (int)MathF.Floor( (bot - 0.001f) / ts ) );
for ( var ty = y0; ty <= y1; ty++ )
for ( var tx = x0; tx <= x1; tx++ )
{
var id = _stage.TileAt( tx, ty );
if ( id == 0 )
continue;
var solid = _tiles is not null && _tiles.IsSolid( id );
var oneWay = _tiles is not null && _tiles.IsOneWay( id );
var hazard = _tiles is not null && _tiles.IsHazard( id );
var spring = _tiles is not null && _tiles.IsSpring( id );
var tileL = tx * ts;
var tileR = tileL + ts;
var tileT = ty * ts;
var tileB = tileT + ts;
if ( hazard && Overlaps( left, top, right, bot, tileL + 2, tileT + 2, tileR - 2, tileB - 2 ) )
{
Kill();
return;
}
if ( spring && dy > 0 && bot > tileT && top < tileT + 6 && right > tileL + 2 && left < tileR - 2 )
{
Py = tileT - BodyH;
Vy = -_stage.JumpVel * 1.35f;
Grounded = false;
continue;
}
if ( oneWay )
{
if ( dy <= 0 )
continue;
if ( bot - dy > tileT + 4 )
continue;
if ( right <= tileL + 1 || left >= tileR - 1 )
continue;
Py = tileT - BodyH;
Vy = 0;
Grounded = true;
bot = Py + BodyH;
continue;
}
if ( !solid )
continue;
if ( !Overlaps( left, top, right, bot, tileL, tileT, tileR, tileB ) )
continue;
if ( dx > 0 )
{
Px = tileL - BodyW;
Vx = 0;
left = Px;
right = Px + BodyW;
}
else if ( dx < 0 )
{
Px = tileR;
Vx = 0;
left = Px;
right = Px + BodyW;
}
else if ( dy > 0 )
{
Py = tileT - BodyH;
Vy = 0;
Grounded = true;
top = Py;
bot = Py + BodyH;
}
else if ( dy < 0 )
{
Py = tileB;
Vy = 0;
top = Py;
bot = Py + BodyH;
}
}
}
static void TouchEntities()
{
var ts = _stage.TileSize;
var left = Px;
var right = Px + BodyW;
var top = Py;
var bot = Py + BodyH;
var reading = "";
foreach ( var e in _stage.Entities )
{
var el = e.Tx * ts + 2;
var et = e.Ty * ts + 2;
var er = el + ts - 4;
var eb = et + ts - 4;
if ( !Overlaps( left, top, right, bot, el, et, er, eb ) )
continue;
if ( e.Kind == "coin" )
{
var key = e.Ty * 1024 + e.Tx;
if ( _got.Add( key ) )
{
Coins++;
ChipVoices.Coin();
GameFlow.Bump();
}
}
else if ( e.Kind == "spring" && Vy >= 0 )
{
Vy = -_stage.JumpVel * 1.35f;
Grounded = false;
ChipVoices.Spring();
}
else if ( e.Kind == "check" )
{
_spawnX = e.Tx * ts + 3;
_spawnY = e.Ty * ts;
}
else if ( e.Kind == "sign" )
{
reading = string.IsNullOrWhiteSpace( e.Data ) ? "..." : e.Data;
}
else if ( e.Kind == "exit" )
{
if ( _stage.Goal == "grab" && Coins < CoinTotal )
{
reading = "GRAB EVERY COIN FIRST";
continue;
}
Won = true;
Active = true;
ChipVoices.Clear();
GameFlow.OnStageCleared();
return;
}
}
if ( reading != SignText )
{
SignText = reading;
GameFlow.Bump();
}
}
static void Kill()
{
if ( Dead || Won )
return;
Dead = true;
Deaths++;
_deadT = 0.55f;
Vx = 0;
Vy = 0;
ChipVoices.Hurt();
GameFlow.Bump();
}
static void Respawn()
{
Dead = false;
Px = _spawnX;
Py = _spawnY;
Vx = 0;
Vy = 0;
Grounded = false;
Pz = WorldStudio.SlabDepth * 0.4f;
CamX = Px - ViewW * 0.5f;
CamY = Py - ViewH * 0.65f;
ClampCam();
}
static void ClampCam()
{
var worldW = _stage.Width * _stage.TileSize;
var worldH = _stage.Height * _stage.TileSize;
CamX = Math.Clamp( CamX, 0, Math.Max( 0, worldW - ViewW ) );
CamY = Math.Clamp( CamY, 0, Math.Max( 0, worldH - ViewH ) );
}
static float Approach( float v, float t, float max )
{
if ( v < t )
return Math.Min( v + max, t );
return Math.Max( v - max, t );
}
static bool Overlaps( float al, float at, float ar, float ab, float bl, float bt, float br, float bb )
=> al < br && ar > bl && at < bb && ab > bt;
}