1 Commits

Author SHA1 Message Date
e289af808a Basic gameplay loop, need to balance values 2026-08-25 23:09:11 -05:00
10 changed files with 382 additions and 50 deletions

View File

@@ -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

View File

@@ -6,6 +6,37 @@
</head>
<body data-model="game_data">
<h1>Hello from RmlUi!</h1>
<div id="hud" data-if="!round_over">
<p>Round {{round_number}} — Creature {{creature_number}} / {{creature_count}}</p>
<p>Species: {{creature_species}}</p>
<p>Marshmallows: {{marshmallows_remaining}}</p>
<p>Score: {{score}}</p>
<p class="timer" data-class-urgent="time_urgent">Time left: {{time_remaining_seconds}}s</p>
<p>------------------------------------------------------------------------------------------</p>
<!-- <p><strong>DEBUG</strong></p>
<p>Appearance tier: {{debug_appearance_tier}}</p>
<p>Is cryptid: {{debug_is_cryptid}}</p>
<p>Behaves normally: {{debug_behaves_normally}}</p>
<p>------------------------------------------------------------------------------------------</p> -->
<button class="menu-btn" data-event-click="feed_creature">Offer Marshmallow</button>
<button class="menu-btn" data-event-click="capture_creature">Capture</button>
<button class="menu-btn" data-event-click="release_creature">Carry On</button>
<button class="menu-btn" data-event-click="pause_game">Pause</button>
<p>{{feed_reaction_text}}</p>
</div>
<div id="round-end" data-if="round_over">
<h1>Round Over</h1>
<p>{{round_result_text}}</p>
<p>Score: {{score}}</p>
<button class="menu-btn" data-event-click="continue_next_round">Continue</button>
<button class="menu-btn" data-event-click="end_current_game">Return to Main Menu</button>
</div>
</body>
</rml>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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";
}

View File

@@ -17,4 +17,7 @@ struct Creature {
};
bool SpeciesEatsWhenNormal(Species species);
bool WouldEatMarshmallow(const Creature &c);
bool WouldEatMarshmallow(const Creature &c);
const char *SpeciesName(Species species);
const char *AppearanceTierName(AppearanceTier tier);

View File

@@ -1,6 +1,8 @@
#include "Game.h"
#include "Creature.h"
#include <RmlUi/Core/DataModelHandle.h>
#include <RmlUi/Core/Types.h>
#include <cmath>
#include <raylib.h>
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() {

View File

@@ -1,8 +1,10 @@
#pragma once
#include "Round.h"
#include <RmlUi/Core/Context.h>
#include <RmlUi/Core/DataModelHandle.h>
#include <RmlUi/Core/ElementDocument.h>
#include <RmlUi/Core/Types.h>
#include <raylib.h>
#include <unordered_map>
@@ -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<GameState, Rml::ElementDocument *> 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;
};

View File

@@ -35,11 +35,17 @@ float IncorrectBehaviourChance(AppearanceTier tier) {
}
Species RandomSpecies() { return static_cast<Species>(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;

View File

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