#include "Game.h" #include "Creature.h" #include #include #include #include namespace { const char *DocumentPathFor(GameState state) { switch (state) { case START_MENU: return "assets/main-menu.rml"; case PAUSE_MENU: return "assets/pause-menu.rml"; case STATS_MENU: return "assets/stats-menu.rml"; case OPTIONS_MENU: return "assets/options-menu.rml"; case IN_GAME: return "assets/game-ui.rml"; } return nullptr; } bool IsMenuState(GameState state) { return state == OPTIONS_MENU || state == STATS_MENU; } Color ColorForTier(AppearanceTier tier) { switch (tier) { case AppearanceTier::Standard: return GRAY; case AppearanceTier::Subtle1: return YELLOW; case AppearanceTier::Subtle2: return ORANGE; case AppearanceTier::Obvious: return RED; } return WHITE; } } // namespace bool Game::Init(Rml::Context *context) { if (!context) return false; ctx = context; return true; } bool Game::SetupRmlDataBinding() { Rml::DataModelConstructor dmc = ctx->CreateDataModel("game_data"); if (!dmc) { TraceLog(LOG_ERROR, "Failed to construct data model."); return false; } dmc.Bind("game_state", &game_data.game_state); dmc.Bind("marshmallows_remaining", &game_data.marshmallows_remaining); dmc.Bind("creature_number", &game_data.creature_number); dmc.Bind("creature_count", &game_data.creature_count); dmc.Bind("creature_species", &game_data.creature_species); dmc.Bind("debug_appearance_tier", &game_data.debug_appearance_tier); dmc.Bind("debug_is_cryptid", &game_data.debug_is_cryptid); dmc.Bind("debug_behaves_normally", &game_data.debug_behaves_normally); dmc.Bind("has_fed_current", &game_data.has_fed_current); dmc.Bind("feed_reaction_text", &game_data.feed_reaction_text); dmc.Bind("score", &game_data.score); dmc.Bind("round_number", &game_data.round_number); dmc.Bind("round_over", &game_data.round_over); dmc.Bind("round_result_text", &game_data.round_result_text); dmc.Bind("time_remaining_seconds", &game_data.time_remaining_seconds); dmc.Bind("time_urgent", &game_data.time_urgent); dmc.BindEventCallback("start_game", [this](Rml::DataModelHandle, Rml::Event &, const Rml::VariantList &) { TrySetGameState(IN_GAME); }); dmc.BindEventCallback("pause_game", [this](Rml::DataModelHandle, Rml::Event &, const Rml::VariantList &) { TrySetGameState(PAUSE_MENU); }); dmc.BindEventCallback( "unpause_game", [this](Rml::DataModelHandle, Rml::Event &, const Rml::VariantList &) { TrySetGameState(IN_GAME); }); dmc.BindEventCallback( "open_options_menu", [this](Rml::DataModelHandle, Rml::Event &, const Rml::VariantList &) { TrySetGameState(OPTIONS_MENU); }); dmc.BindEventCallback( "end_current_game", [this](Rml::DataModelHandle, Rml::Event &, const Rml::VariantList &) { TrySetGameState(START_MENU); }); dmc.BindEventCallback("go_back", [this](Rml::DataModelHandle, Rml::Event &, const Rml::VariantList &) { GoBack(); }); // --- gameplay actions --- dmc.BindEventCallback("feed_creature", [this](Rml::DataModelHandle, Rml::Event &, const Rml::VariantList &) { FeedCreature(); }); dmc.BindEventCallback( "capture_creature", [this](Rml::DataModelHandle, Rml::Event &, const Rml::VariantList &) { CaptureCreature(); }); dmc.BindEventCallback( "release_creature", [this](Rml::DataModelHandle, Rml::Event &, const Rml::VariantList &) { ReleaseCreature(); }); dmc.BindEventCallback( "continue_next_round", [this](Rml::DataModelHandle, Rml::Event &, const Rml::VariantList &) { ContinueToNextRound(); }); modelHandle = dmc.GetModelHandle(); return true; } Rml::ElementDocument *Game::LoadOrGetDocument(GameState state) { if (auto it = documents.find(state); it != documents.end()) return it->second; const char *path = DocumentPathFor(state); if (!path) return nullptr; Rml::ElementDocument *doc = ctx->LoadDocument(path); if (!doc) { TraceLog(LOG_ERROR, "Failed to load document: %s", path); return nullptr; } documents[state] = doc; return doc; } void Game::ShowDocumentForState(GameState state) { if (activeDocument) { activeDocument->Hide(); activeDocument = nullptr; } if (Rml::ElementDocument *doc = LoadOrGetDocument(state)) { doc->Show(); activeDocument = doc; } } void Game::TrySetGameState(GameState newState) { GameState currentState = CurrentState(); if (IsMenuState(newState) && !IsMenuState(currentState)) { previousState = currentState; } switch (newState) { case START_MENU: if (currentState == IN_GAME) ResetGame(); break; case IN_GAME: if (currentState == START_MENU) StartNewGame(); break; default: break; } game_data.game_state = newState; ShowDocumentForState(newState); } void Game::GoBack() { if (IsMenuState(CurrentState())) { TrySetGameState(previousState); } } void Game::StartNewGame() { roundIndex = 0; game_data.score = 0; BeginRound(); } void Game::ResetGame() { roundIndex = 0; game_data.score = 0; } void Game::BeginRound() { RoundConfig cfg = MakeRoundConfig(roundIndex); currentRound = GenerateRound(cfg); game_data.round_number = roundIndex + 1; game_data.round_over = false; game_data.round_result_text = ""; creatureTimeLimit = TimeLimitForRound(roundIndex); RefreshCreatureBindings(); } float Game::TimeLimitForRound(int roundIndex) const { return std::max(15.0f, 40.0f - roundIndex * 1.0f); } void Game::ResetCreatureTimer() { creatureTimeRemaining = creatureTimeLimit; game_data.time_remaining_seconds = (int)std::ceil(creatureTimeRemaining); game_data.time_urgent = false; } void Game::UpdateCreatureTimer(float dt) { if (game_data.round_over) return; creatureTimeRemaining -= dt; int secondsLeft = (int)std::ceil(std::max(creatureTimeRemaining, 0.0f)); bool urgent = creatureTimeRemaining <= 2.0f; if (secondsLeft != game_data.time_remaining_seconds || urgent != game_data.time_urgent) { game_data.time_remaining_seconds = secondsLeft; game_data.time_urgent = urgent; if (modelHandle) { modelHandle.DirtyVariable("time_remaining_seconds"); modelHandle.DirtyVariable("time_urgent"); } } if (creatureTimeRemaining <= 0.0f) { ReleaseCreature(); } } void Game::RefreshCreatureBindings() { Creature *c = currentRound.CurrentCreature(); game_data.marshmallows_remaining = currentRound.marshmallowsRemaining; game_data.creature_count = currentRound.conf.creature_count; game_data.creature_number = currentRound.currentIndex + 1; if (c) { game_data.creature_species = SpeciesName(c->species); game_data.debug_appearance_tier = AppearanceTierName(c->appearance); game_data.debug_is_cryptid = c->is_cryptid; game_data.debug_behaves_normally = c->behaves_normally; game_data.has_fed_current = c->has_been_fed; game_data.feed_reaction_text = ""; } ResetCreatureTimer(); MarkDirty(); } void Game::MarkDirty() { if (!modelHandle) return; modelHandle.DirtyVariable("marshmallows_remaining"); modelHandle.DirtyVariable("creature_number"); modelHandle.DirtyVariable("creature_count"); modelHandle.DirtyVariable("creature_species"); modelHandle.DirtyVariable("debug_appearance_tier"); modelHandle.DirtyVariable("debug_is_cryptid"); modelHandle.DirtyVariable("debug_behaves_normally"); modelHandle.DirtyVariable("has_fed_current"); modelHandle.DirtyVariable("feed_reaction_text"); modelHandle.DirtyVariable("score"); modelHandle.DirtyVariable("round_number"); modelHandle.DirtyVariable("round_over"); modelHandle.DirtyVariable("round_result_text"); } void Game::FeedCreature() { if (game_data.round_over) return; if (!FeedCurrentCreature(currentRound)) return; Creature *c = currentRound.CurrentCreature(); if (c) { bool ate = WouldEatMarshmallow(*c); game_data.feed_reaction_text = std::string(SpeciesName(c->species)) + (ate ? " eats the marshmallow!" : " ignores it."); game_data.has_fed_current = true; } game_data.marshmallows_remaining = currentRound.marshmallowsRemaining; MarkDirty(); } void Game::CaptureCreature() { if (game_data.round_over) return; bool died = ResolveCurrentCreature(currentRound, true); if (currentRound.CurrentCreature() && !currentRound.CurrentCreature()->is_cryptid) { } Creature &resolved = currentRound.creatures[currentRound.currentIndex - 1]; game_data.score += resolved.is_cryptid ? 1 : -2; if (died) { EndRound(true, false); return; } if (currentRound.IsComplete()) { EndRound(false, currentRound.Passed()); return; } RefreshCreatureBindings(); } void Game::ReleaseCreature() { if (game_data.round_over) return; bool died = ResolveCurrentCreature(currentRound, false); if (died) { EndRound(true, false); return; } if (currentRound.IsComplete()) { EndRound(false, currentRound.Passed()); return; } RefreshCreatureBindings(); } void Game::EndRound(bool died, bool passed) { game_data.round_over = true; if (died) { game_data.round_result_text = "A cryptid got away. You did not survive."; } else if (passed) { game_data.round_result_text = "Round complete. Success rate: " + std::to_string((int)(currentRound.SuccessRate() * 100.0f)) + "%"; } else { game_data.round_result_text = "Too many mistakes. Round failed. Success rate: " + std::to_string((int)(currentRound.SuccessRate() * 100.0f)) + "%"; } MarkDirty(); } void Game::ContinueToNextRound() { if (!game_data.round_over) return; if (currentRound.Passed()) { roundIndex++; } BeginRound(); } void Game::Update() { HandleInput(); if (CurrentState() != IN_GAME) return; float dt = GetFrameTime(); UpdateCreatureTimer(dt); } void Game::Draw2D() { if (CurrentState() != IN_GAME) return; Creature *c = currentRound.CurrentCreature(); if (c && !game_data.round_over) { Color tierColor = ColorForTier(c->appearance); int size = 120; int x = GetScreenWidth() / 2 - size / 2; int y = GetScreenHeight() / 2 - size / 2; DrawRectangle(x, y, size, size, tierColor); DrawRectangleLines(x, y, size, size, BLACK); } } void Game::HandleInput() { if (!IsKeyReleased(KEY_ESCAPE)) return; switch (CurrentState()) { case IN_GAME: TrySetGameState(PAUSE_MENU); break; case PAUSE_MENU: TrySetGameState(IN_GAME); break; case OPTIONS_MENU: case STATS_MENU: GoBack(); break; default: break; } }