Files
BGG2026/src/Game.cpp

428 lines
11 KiB
C++

#include "Game.h"
#include "Creature.h"
#include <RmlUi/Core/DataModelHandle.h>
#include <RmlUi/Core/Types.h>
#include <cmath>
#include <raylib.h>
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;
}
}