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);