428 lines
11 KiB
C++
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;
|
|
}
|
|
} |