diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dd20e1..91813ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,8 @@ add_executable(gamejam src/main.cpp src/Game.cpp src/Application.cpp + src/Creature.cpp + src/Round.cpp src/backend/RaylibSystemInterface.cpp src/backend/RaylibRenderInterface.cpp src/backend/RaylibInput.cpp diff --git a/assets/game-ui.rml b/assets/game-ui.rml index 9fe074b..d62b3e6 100644 --- a/assets/game-ui.rml +++ b/assets/game-ui.rml @@ -6,6 +6,37 @@ -

Hello from RmlUi!

+ +
+

Round {{round_number}} — Creature {{creature_number}} / {{creature_count}}

+

Species: {{creature_species}}

+

Marshmallows: {{marshmallows_remaining}}

+

Score: {{score}}

+

Time left: {{time_remaining_seconds}}s

+ +

------------------------------------------------------------------------------------------

+ + + + + + + +

{{feed_reaction_text}}

+
+ +
+

Round Over

+

{{round_result_text}}

+

Score: {{score}}

+ + +
+ \ No newline at end of file diff --git a/assets/globals.rcss b/assets/globals.rcss index 43e34be..645d0cb 100644 --- a/assets/globals.rcss +++ b/assets/globals.rcss @@ -11,12 +11,14 @@ body { } h1 { - font-size: 24px; - margin-bottom: 10px; + font-size: 24dp; + margin-bottom: 15dp; } p { - margin-bottom: 15px; + margin-bottom: 15dp; + display: block; + font-size: 18dp; } .hidden { @@ -39,4 +41,9 @@ p { .menu-btn:active { background-color: #2e4e70; +} + +.timer.urgent { + color: #ff4444; + font-weight: bold; } \ No newline at end of file diff --git a/src/Application.cpp b/src/Application.cpp index dbaeb23..60f61d1 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -24,8 +24,7 @@ bool Application::Init(int width, int height, const char *title) { return false; } - Rml::DataModelHandle game_data_model_handle; - if (!game.SetupRmlDataBinding(game_data_model_handle)) { + if (!game.SetupRmlDataBinding()) { TraceLog(LOG_ERROR, "Failed to initialize data bindings for 'game_data'."); return false; } diff --git a/src/Creature.cpp b/src/Creature.cpp index 425db35..77356c7 100644 --- a/src/Creature.cpp +++ b/src/Creature.cpp @@ -18,3 +18,31 @@ bool WouldEatMarshmallow(const Creature &c) { bool n = SpeciesEatsWhenNormal(c.species); return c.behaves_normally ? n : !n; } + +const char *SpeciesName(Species species) { + switch (species) { + case Species::Raccoon: + return "Raccoon"; + case Species::Opossum: + return "Opossum"; + case Species::Fox: + return "Fox"; + case Species::Deer: + return "Deer"; + } + return "Unknown"; +} + +const char *AppearanceTierName(AppearanceTier tier) { + switch (tier) { + case AppearanceTier::Standard: + return "Standard"; + case AppearanceTier::Subtle1: + return "Subtle 1"; + case AppearanceTier::Subtle2: + return "Subtle 2"; + case AppearanceTier::Obvious: + return "Obvious"; + } + return "Unknown"; +} \ No newline at end of file diff --git a/src/Creature.h b/src/Creature.h index 079f204..4cd2c81 100644 --- a/src/Creature.h +++ b/src/Creature.h @@ -17,4 +17,7 @@ struct Creature { }; bool SpeciesEatsWhenNormal(Species species); -bool WouldEatMarshmallow(const Creature &c); \ No newline at end of file +bool WouldEatMarshmallow(const Creature &c); + +const char *SpeciesName(Species species); +const char *AppearanceTierName(AppearanceTier tier); \ No newline at end of file diff --git a/src/Game.cpp b/src/Game.cpp index 6ad5d18..e745928 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -1,6 +1,8 @@ #include "Game.h" +#include "Creature.h" #include #include +#include #include namespace { @@ -25,6 +27,20 @@ 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) { @@ -34,18 +50,29 @@ bool Game::Init(Rml::Context *context) { return true; } -bool Game::SetupRmlDataBinding(Rml::DataModelHandle &model_handle) { +bool Game::SetupRmlDataBinding() { Rml::DataModelConstructor dmc = ctx->CreateDataModel("game_data"); if (!dmc) { TraceLog(LOG_ERROR, "Failed to construct data model."); return false; } - // ---------- Member bindings ---------- - dmc.Bind("game_state", &game_data.game_state); - - // ---------- Event callback bindings ---------- + 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 &) { @@ -79,7 +106,30 @@ bool Game::SetupRmlDataBinding(Rml::DataModelHandle &model_handle) { [this](Rml::DataModelHandle, Rml::Event &, const Rml::VariantList &) { GoBack(); }); - model_handle = dmc.GetModelHandle(); + // --- 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; } @@ -144,14 +194,192 @@ void Game::GoBack() { } void Game::StartNewGame() { - // TODO + roundIndex = 0; + game_data.score = 0; - // --- placeholder testing stuff --- - playerPos = {GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f}; + BeginRound(); } void Game::ResetGame() { - // TODO + 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() { @@ -159,32 +387,24 @@ void Game::Update() { if (CurrentState() != IN_GAME) return; - // TODO - // --- placeholder testing stuff --- float dt = GetFrameTime(); - - if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) - playerPos.y -= playerSpeed * dt; - if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) - playerPos.y += playerSpeed * dt; - if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) - playerPos.x -= playerSpeed * dt; - if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) - playerPos.x += playerSpeed * dt; + UpdateCreatureTimer(dt); } void Game::Draw2D() { if (CurrentState() != IN_GAME) return; - // TODO - // --- placeholder testing stuff --- - // DrawText("Hello from raylib!", 50, 50, 20, RAYWHITE); - if (CurrentState() != IN_GAME) - return; - DrawCircleV(playerPos, 24.0f, BLUE); - DrawText("WASD/Arrows to move, ESC to pause", 20, 20, 20, RAYWHITE); + 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() { diff --git a/src/Game.h b/src/Game.h index c196177..b055622 100644 --- a/src/Game.h +++ b/src/Game.h @@ -1,8 +1,10 @@ #pragma once +#include "Round.h" #include #include #include +#include #include #include @@ -10,6 +12,27 @@ enum GameState { START_MENU, PAUSE_MENU, STATS_MENU, OPTIONS_MENU, IN_GAME }; struct GameData { int game_state = START_MENU; + + int marshmallows_remaining = 0; + int creature_number = 0; + int creature_count = 0; + + Rml::String creature_species; + Rml::String debug_appearance_tier; + bool debug_is_cryptid = false; + bool debug_behaves_normally = false; + + bool has_fed_current = false; + Rml::String feed_reaction_text; + + int time_remaining_seconds = 0; + bool time_urgent = false; + + int score = 0; + int round_number = 1; + + bool round_over = false; + Rml::String round_result_text; }; class Game { @@ -17,7 +40,7 @@ public: GameData game_data; bool Init(Rml::Context *context); - bool SetupRmlDataBinding(Rml::DataModelHandle &model_handle); + bool SetupRmlDataBinding(); void Update(); void Draw2D(); @@ -32,18 +55,35 @@ public: void StartNewGame(); void ResetGame(); + void FeedCreature(); + void CaptureCreature(); + void ReleaseCreature(); + void ContinueToNextRound(); + private: Rml::ElementDocument *LoadOrGetDocument(GameState state); void ShowDocumentForState(GameState state); void HandleInput(); + void BeginRound(); + void RefreshCreatureBindings(); + void EndRound(bool died, bool passed); + void MarkDirty(); + Rml::Context *ctx = nullptr; std::unordered_map documents; Rml::ElementDocument *activeDocument = nullptr; + Rml::DataModelHandle modelHandle; GameState previousState = START_MENU; - // --- placeholder testing stuff --- - Vector2 playerPos{}; - float playerSpeed = 300.0f; + Round currentRound; + int roundIndex = 0; + + void UpdateCreatureTimer(float dt); + void ResetCreatureTimer(); + float TimeLimitForRound(int roundIndex) const; + + float creatureTimeLimit = 40.0f; + float creatureTimeRemaining = 0.0f; }; \ No newline at end of file diff --git a/src/round.cpp b/src/Round.cpp similarity index 90% rename from src/round.cpp rename to src/Round.cpp index 735bf1a..b28b121 100644 --- a/src/round.cpp +++ b/src/Round.cpp @@ -35,11 +35,17 @@ float IncorrectBehaviourChance(AppearanceTier tier) { } Species RandomSpecies() { return static_cast(GetRandomValue(0, 3)); } + +int ComputeCryptidCount(int creatureCount, float cryptidRatio) { + return std::clamp((int)std::round(creatureCount * cryptidRatio), 1, + creatureCount - 1); +} } // namespace RoundConfig MakeRoundConfig(int roundIndex) { RoundConfig cfg; cfg.creature_count = std::min(5 + roundIndex * 2, 20); + cfg.cryptid_ratio = 0.35f; float rampedStandard = std::min(0.35f + roundIndex * 0.03f, 0.70f); float remaining = 1.0f - rampedStandard; @@ -49,11 +55,8 @@ RoundConfig MakeRoundConfig(int roundIndex) { cfg.w_subtle2 = remaining * (0.20f / baseRemaining); cfg.w_obvious = remaining * (0.15f / baseRemaining); - cfg.cryptid_ratio = 0.35f; - float ambiguousFraction = - (1.0f - cfg.cryptid_ratio) + cfg.cryptid_ratio * cfg.w_standard; - float ambiguousCount = cfg.creature_count * ambiguousFraction; - cfg.marshmallow_supply = std::max(2, (int)std::round(ambiguousCount * 0.85f)); + int cryptidCount = ComputeCryptidCount(cfg.creature_count, cfg.cryptid_ratio); + cfg.marshmallow_supply = cryptidCount; return cfg; } @@ -64,8 +67,7 @@ Round GenerateRound(const RoundConfig &config) { round.marshmallowsRemaining = config.marshmallow_supply; int cryptidCount = - std::clamp((int)std::round(config.creature_count * config.cryptid_ratio), - 1, config.creature_count - 1); + ComputeCryptidCount(config.creature_count, config.cryptid_ratio); for (int i = 0; i < config.creature_count; ++i) { Creature c; diff --git a/src/Round.h b/src/Round.h index 6294d57..07aedc1 100644 --- a/src/Round.h +++ b/src/Round.h @@ -8,10 +8,10 @@ struct RoundConfig { float cryptid_ratio = 0.35f; // Appearance weights - float w_standard = 0.35f; - float w_subtle1 = 0.30f; - float w_subtle2 = 0.20f; - float w_obvious = 0.15f; + float w_standard = 0.30f; + float w_subtle1 = 0.35f; + float w_subtle2 = 0.30f; + float w_obvious = 0.10f; }; RoundConfig MakeRoundConfig(int roundIndex);