using Godot; namespace Game; public partial class HotReloadSmoke : Node { private const string Version = "flappy-physics-v1"; private const float WorldWidth = 540.0f; private const float WorldHeight = 360.0f; private const float GroundY = 320.0f; private const float BirdX = 108.0f; private const float BirdSize = 40.0f; private const float Gravity = 960.0f; private const float FlapVelocity = -330.0f; private const float PipeSpeed = 170.0f; private const float PipeWidth = 60.0f; private const float GapHeight = 160.0f; private const float ResetPipeX = 560.0f; private const float PipeRecycleX = -80.0f; private const int MinGapCenter = 118; private const int MaxGapCenter = 235; private static readonly Color AliveBirdColor = new Color(1.0f, 0.95f, 0.18f, 1.0f); private static readonly Color GameOverBirdColor = new Color(1.0f, 0.32f, 0.22f, 1.0f); [Export(PropertyHint.Range, "1,10,1")] public int FlapPower { get; set; } = 5; private float elapsed; private float birdY; private float velocityY; private float pipeX; private int gapCenter; private int score; private bool gameOver; private bool passedPipe; private bool processLogged; private bool runtimeReloadedObject; public override void _Ready() { if (birdY <= 0.0f || pipeX <= 0.0f) { ResetGame(); } ApplyGameState(); GD.Print("LeanCLR flappy reload: version = " + Version); GD.Print("LeanCLR flappy reload: score = " + score.ToString()); GD.Print("LeanCLR flappy reload: bird y = " + ((int)birdY).ToString()); GD.Print("LeanCLR flappy reload: velocity y = " + ((int)velocityY).ToString()); GD.Print("LeanCLR flappy reload: active marker = " + FileAccess.GetFileAsString("res://leanclr/live_reload.txt").Trim()); } public override void _Input(InputEvent event_) { if (!IsGameplayObjectActive()) { return; } if (event_ != null && event_.IsPressed() && !event_.IsEcho()) { ForceFlap(); } } public void ForceFlap() { if (gameOver) { ResetGame(); GD.Print("LeanCLR flappy input: restart"); return; } velocityY = FlapVelocity - FlapPower * 10.0f; GD.Print("LeanCLR flappy input: flap velocity = " + ((int)velocityY).ToString()); } public Variant CaptureHotReloadState() { Dictionary state = new Dictionary(); state[new Variant("elapsed")] = new Variant(elapsed); state[new Variant("birdY")] = new Variant(birdY); state[new Variant("velocityY")] = new Variant(velocityY); state[new Variant("pipeX")] = new Variant(pipeX); state[new Variant("gapCenter")] = new Variant(gapCenter); state[new Variant("score")] = new Variant(score); state[new Variant("gameOver")] = new Variant(gameOver); state[new Variant("passedPipe")] = new Variant(passedPipe); state[new Variant("processLogged")] = new Variant(processLogged); GD.Print("LeanCLR hot reload state: captured score = " + score.ToString() + " y = " + ((int)birdY).ToString()); return new Variant(state); } public void RestoreHotReloadState(Variant stateVariant) { runtimeReloadedObject = true; Dictionary state = stateVariant.AsDictionary(); if (state != null) { elapsed = ReadFloat(state, "elapsed", elapsed); birdY = ReadFloat(state, "birdY", birdY); velocityY = ReadFloat(state, "velocityY", velocityY); pipeX = ReadFloat(state, "pipeX", pipeX); gapCenter = ReadInt(state, "gapCenter", gapCenter); score = ReadInt(state, "score", score); gameOver = ReadBool(state, "gameOver", gameOver); passedPipe = ReadBool(state, "passedPipe", passedPipe); processLogged = ReadBool(state, "processLogged", processLogged); } GD.Print("LeanCLR hot reload state: restored score = " + score.ToString() + " y = " + ((int)birdY).ToString()); } public void OnHotReloaded() { runtimeReloadedObject = true; ApplyGameState(); GD.Print("LeanCLR hot reload state: active score after reload = " + score.ToString() + " y = " + ((int)birdY).ToString()); } public override void _Process(double delta) { if (!IsGameplayObjectActive()) { return; } float dt = Clamp((float)delta, 0.0f, 0.033f); elapsed += dt; if (!gameOver) { velocityY += Gravity * dt; birdY += velocityY * dt; pipeX -= PipeSpeed * dt; if (pipeX < PipeRecycleX) { pipeX = ResetPipeX; gapCenter = NextGapCenter(); passedPipe = false; } if (!passedPipe && pipeX + PipeWidth < BirdX) { passedPipe = true; score++; GD.Print("LeanCLR flappy score: " + score.ToString()); } if (CheckCollision()) { gameOver = true; GD.Print("LeanCLR flappy collision: game over score = " + score.ToString()); } } ApplyGameState(); if (!processLogged) { processLogged = true; GD.Print("LeanCLR flappy process: playable physics tick delta = " + delta.ToString()); } } private void ResetGame() { elapsed = 0.0f; birdY = 146.0f; velocityY = 0.0f; pipeX = 420.0f; gapCenter = 172; score = 0; gameOver = false; passedPipe = false; processLogged = false; } private bool CheckCollision() { if (birdY < 0.0f || birdY + BirdSize > GroundY) { return true; } bool overlapsPipeX = BirdX + BirdSize > pipeX && BirdX < pipeX + PipeWidth; if (!overlapsPipeX) { return false; } float gapTop = gapCenter - GapHeight * 0.5f; float gapBottom = gapCenter + GapHeight * 0.5f; return birdY < gapTop || birdY + BirdSize > gapBottom; } private void ApplyGameState() { TextureRect bird = GetNodeOrNull("../GameWorld/Bird"); ColorRect pipeTop = GetNodeOrNull("../GameWorld/PipeTop"); ColorRect pipeBottom = GetNodeOrNull("../GameWorld/PipeBottom"); Label title = GetNodeOrNull