Files are now in a src directory and I'm using a src/meson.build and tests/meson.build to specify what to build.

This commit is contained in:
Zed A. Shaw 2026-02-27 10:49:19 -05:00
parent 4778677647
commit 1d4ae911b9
108 changed files with 94 additions and 83 deletions

273
src/boss/fight.cpp Normal file
View file

@ -0,0 +1,273 @@
#define FSM_DEBUG 1
#include "boss/fight.hpp"
#include "boss/system.hpp"
#include "game_level.hpp"
#include <iostream>
#include "rand.hpp"
#include "events.hpp"
namespace boss {
using namespace DinkyECS;
Fight::Fight(shared_ptr<World> world, Entity boss_id, Entity player_id) :
$world(world),
$boss_id(boss_id),
$battle(System::create_battle($world, $boss_id)),
$ui(world, boss_id, player_id),
$host(player_id)
{
$host_combat = $world->get_if<components::Combat>(player_id);
dbc::check($host_combat,
fmt::format("No combat for host with player_id={}", player_id));
$ui.init();
}
void Fight::event(game::Event ev, std::any data) {
// if the mouse event is handled the done, this may be wrong later
if(handle_mouse(ev)) return;
switch($state) {
FSM_STATE(State, START, ev, data);
FSM_STATE(State, PLAYER_REQUESTS, ev, data);
FSM_STATE(State, EXEC_PLAN, ev, data);
FSM_STATE(State, ANIMATE, ev, data);
FSM_STATE(State, END, ev, data);
}
}
void Fight::START(game::Event ev, std::any) {
using enum game::Event;
switch(ev) {
case COMBAT_START:
$ui.status(L"PLAYER REQUESTS", L"COMMIT");
$battle.ap_refresh();
state(State::PLAYER_REQUESTS);
break;
default:
break;
}
}
void Fight::PLAYER_REQUESTS(game::Event ev, std::any data) {
using enum game::Event;
switch(ev) {
// this is only if using the debug X key to skip it
case BOSS_END:
state(State::END);
break;
case COMBAT_START:
System::plan_battle($battle, $world, $boss_id);
$ui.status(L"EXEC PLAN", L"START");
state(State::EXEC_PLAN);
break;
case ATTACK:
if($battle.player_request("kill_enemy")) {
fmt::println("player requests kill_enemy {} vs. {}",
$host_combat->ap, $battle.player_pending_ap());
} else {
fmt::println("NO MORE ACTION!");
}
break;
default:
break;
}
}
void Fight::EXEC_PLAN(game::Event ev, std::any data) {
using enum game::Event;
switch(ev) {
// this is only if using the debug X key to skip it
case BOSS_END:
state(State::END);
break;
case COMBAT_START:
$ui.status(L"EXEC PLAN", L"ANIMATE");
next_combat();
break;
case COMBAT:
do_combat(data);
state(State::ANIMATE);
break;
default:
break;
}
}
void Fight::ANIMATE(game::Event ev, std::any data) {
using enum game::Event;
switch(ev) {
case ANIMATION_END:
$ui.status(L"ANIMATE END", L"NEXT");
next_combat();
break;
default:
// ignored
break;
}
}
void Fight::END(game::Event ev, std::any) {
fmt::println("BOSS_FIGHT:END event {}", (int)ev);
}
void Fight::next_combat() {
if(auto action = $battle.next()) {
fmt::println("next combat has action");
System::combat(*action, $world, $boss_id, 0);
state(State::EXEC_PLAN);
} else if(player_dead()) {
fmt::println("player died");
$ui.status(L"YOU DIED", L"DEAD");
state(State::END);
} else {
fmt::println("end combat next turn");
$ui.status(L"PLAYER REQUESTS", L"COMMIT");
$ui.reset_camera();
$battle.ap_refresh();
$battle.clear_requests();
state(State::PLAYER_REQUESTS);
}
}
void Fight::do_combat(std::any data) {
using combat::BattleHostState;
dbc::check(data.type() == typeid(components::CombatResult),
fmt::format("Boss Fight wrong any data={}", data.type().name()));
auto result = std::any_cast<components::CombatResult>(data);
switch(result.host_state) {
case BattleHostState::agree:
case BattleHostState::disagree: {
std::string player_move = Random::uniform(0, 1) == 0 ? "player1" : "player3";
$ui.move_actor("player", player_move);
if(result.player_did > 0) {
$ui.animate_actor("player", "attack");
} else {
// NO need a no animation event
$ui.animate_actor("player", "idle");
}
$ui.damage("player", "boss", result.player_did);
} break;
case BattleHostState::not_host: {
std::string boss_move = Random::uniform(0, 1) == 0 ? "boss5" : "boss6";
$ui.move_actor("boss", boss_move);
if(result.enemy_did > 0) {
$ui.animate_actor("boss", "attack");
} else {
// NO need a no animation event
$ui.animate_actor("boss", "idle");
}
$ui.damage("boss", "player", result.enemy_did);
} break;
case BattleHostState::out_of_ap:
// NO need an out of AP animation
$ui.animate_actor("player", "idle");
break;
}
}
void Fight::run_systems() {
$ui.update_stats();
$battle.set($host, "tough_personality", false);
$battle.set($host, "have_healing", false);
$battle.set($host, "health_good", $host_combat->hp > 100);
}
void Fight::render(sf::RenderWindow& window) {
$ui.render();
}
void Fight::update() {
$ui.update();
}
bool Fight::handle_mouse(game::Event ev) {
using enum game::Event;
$mouse_pos = $window->mapPixelToCoords($router.position);
switch(ev) {
case MOUSE_CLICK: {
$ui.mouse($mouse_pos.x, $mouse_pos.y, guecs::NO_MODS);
} break;
case MOUSE_MOVE: {
$ui.mouse($mouse_pos.x, $mouse_pos.y, {1 << guecs::ModBit::hover});
} break;
case MOUSE_DRAG:
dbc::log("mouse drag");
break;
case MOUSE_DRAG_START:
dbc::log("mouse START drag");
break;
case MOUSE_DROP:
dbc::log("mouse DROP");
break;
default:
return false;
}
// switch/default didn't happen so handled it.
return true;
}
bool Fight::player_dead() {
return $host_combat->hp <= 0;
}
void Fight::init_fight() {
System::initialize_actor_ai(*$world, $boss_id);
run_systems();
}
void Fight::set_window(sf::RenderWindow* window) {
$window = window;
$ui.set_window(window);
}
bool Fight::handle_keyboard_mouse() {
dbc::check($window != nullptr, "you didn't set_window");
while(const auto ev = $window->pollEvent()) {
auto gui_ev = $router.process_event(ev);
if(gui_ev == game::Event::KEY_PRESS) {
using KEY = sf::Keyboard::Scan;
switch($router.scancode) {
// REALLY? just go to state end or use another event
case KEY::X:
event(game::Event::BOSS_END, {});
break;
default:
fmt::println("key press!");
}
} else if(gui_ev == game::Event::QUIT) {
return true;
} else {
event(gui_ev, {});
}
}
return in_state(State::END);
}
bool Fight::handle_world_events() {
if($world->has_event<game::Event>()) {
while($world->has_event<game::Event>()) {
auto [evt, entity, data] = $world->recv<game::Event>();
event(game::Event(evt), data);
run_systems();
}
}
return in_state(State::END);
}
}

61
src/boss/fight.hpp Normal file
View file

@ -0,0 +1,61 @@
#pragma once
#include "simplefsm.hpp"
#include "dinkyecs.hpp"
#include "boss/ui.hpp"
#include "events.hpp"
#include "battle.hpp"
#include "gui/event_router.hpp"
#include <memory>
#include <any>
namespace boss {
using std::shared_ptr;
enum class State {
START=__LINE__,
PLAYER_REQUESTS=__LINE__,
EXEC_PLAN=__LINE__,
ANIMATE=__LINE__,
END=__LINE__
};
class Fight : public DeadSimpleFSM<State, game::Event> {
public:
sf::RenderWindow* $window = nullptr;
shared_ptr<DinkyECS::World> $world = nullptr;
DinkyECS::Entity $boss_id = DinkyECS::NONE;
combat::BattleEngine $battle;
boss::UI $ui;
DinkyECS::Entity $host = DinkyECS::NONE;
components::Combat* $host_combat = nullptr;
gui::routing::Router $router{};
sf::Vector2f $mouse_pos{};
Fight(shared_ptr<DinkyECS::World> world,
DinkyECS::Entity boss_id,
DinkyECS::Entity player_id);
void set_window(sf::RenderWindow* window);
bool handle_mouse(game::Event ev);
void event(game::Event ev, std::any data);
void START(game::Event ev, std::any data);
void PLAYER_REQUESTS(game::Event ev, std::any data);
void EXEC_PLAN(game::Event ev, std::any data);
void ANIMATE(game::Event ev, std::any data);
void END(game::Event ev, std::any data);
void render(sf::RenderWindow& window);
void update();
bool handle_world_events();
void run_systems();
bool player_dead();
void init_fight();
void do_combat(std::any data);
void next_combat();
sf::Vector2f mouse_position();
bool handle_keyboard_mouse();
};
}

140
src/boss/system.cpp Normal file
View file

@ -0,0 +1,140 @@
#include "boss/system.hpp"
#include <fmt/core.h>
#include "components.hpp"
#include "game_level.hpp"
#include "ai.hpp"
#include "battle.hpp"
namespace boss {
using namespace components;
using namespace combat;
void System::load_config() {
fmt::println("load it");
}
void System::initialize_actor_ai(DinkyECS::World& world, DinkyECS::Entity entity_id) {
dbc::check(world.has<EnemyConfig>(entity_id), "boss doesn't have an AI EnemyConfig");
auto& config = world.get<EnemyConfig>(entity_id);
auto ai_start = ai::load_state(config.ai_start_name);
auto ai_goal = ai::load_state(config.ai_goal_name);
ai::EntityAI boss_ai(config.ai_script, ai_start, ai_goal);
boss_ai.set_state("enemy_found", true);
boss_ai.set_state("in_combat", true);
boss_ai.set_state("tough_personality", true);
boss_ai.set_state("health_good", true);
world.set<ai::EntityAI>(entity_id, boss_ai);
}
shared_ptr<boss::Fight> System::create_bossfight() {
// need to copy so we can clone it and keep things working, even
// if we don't use things like map and lighting
auto level = GameDB::current_level();
dbc::check(level.world != nullptr, "Starter world for boss fights can't be null.");
level.world = GameDB::clone_load_world(level.world);
auto& config = level.world->get_the<GameConfig>();
auto boss_names = config.bosses.keys();
auto& level_name = boss_names[level.index % boss_names.size()];
auto& boss_data = config.bosses[level_name];
auto boss_id = level.world->entity();
components::configure_entity(*level.world, boss_id, boss_data["components"]);
initialize_actor_ai(*level.world, boss_id);
dbc::check(level.world->has<ai::EntityAI>(boss_id), "boss doesn't have an AI");
initialize_actor_ai(*level.world, level.player);
dbc::check(level.world->has<ai::EntityAI>(level.player), "player/host doesn't have an AI");
GameDB::register_level(level);
return make_shared<boss::Fight>(level.world, boss_id, level.player);
}
BattleEngine System::create_battle(std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id) {
auto& level = GameDB::current_level();
auto player_combat = world->get_if<Combat>(level.player);
dbc::check(player_combat != nullptr, "No Combat for player.");
auto boss_combat = world->get_if<Combat>(boss_id);
dbc::check(boss_combat != nullptr, "No Combat for Boss.");
// BUG: should I reset AP here?
player_combat->ap = player_combat->max_ap;
boss_combat->ap = boss_combat->max_ap;
auto boss_ai = world->get_if<ai::EntityAI>(boss_id);
dbc::check(boss_ai != nullptr, "boss doesn't have an AI");
auto host_ai = world->get_if<ai::EntityAI>(boss_id);
dbc::check(host_ai != nullptr, "host doesn't have an AI");
BattleEngine battle;
battle.add_enemy({boss_id, boss_ai, boss_combat, false});
battle.add_enemy({level.player, host_ai, player_combat, true});
battle.set_all("enemy_found", true);
battle.set_all("in_combat", true);
battle.set(boss_id, "tough_personality", true);
return battle;
}
void System::plan_battle(BattleEngine& battle, std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id) {
// BUG: make this loop the list of entities in the battle then
// use their world state to configure the plan
battle.plan();
}
void System::combat(BattleResult& action, std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id, int attack_id) {
fmt::println("COMBAT >>> enemy={}, wants_to={}, cost={}, host_state={}",
int(action.enemy.entity), action.wants_to, action.cost, int(action.host_state));
auto& level = GameDB::current_level();
auto& player_combat = world->get<Combat>(level.player);
auto& boss_combat = world->get<Combat>(boss_id);
auto& [enemy, wants_to, cost, host_state] = action;
components::CombatResult result {
.attacker=enemy.entity,
.host_state=host_state,
.player_did=0,
.enemy_did=0
};
switch(host_state) {
case BattleHostState::agree:
// BUG: this is hard coding only one boss, how to select targets?
if(wants_to == "kill_enemy") {
result.player_did = player_combat.attack(boss_combat);
}
break;
case BattleHostState::disagree:
fmt::println("HOST DISAGREES! {}", wants_to);
if(wants_to == "kill_enemy") {
result.player_did = player_combat.attack(boss_combat);
}
break;
case BattleHostState::not_host:
dbc::log("kill_enemy");
if(wants_to == "kill_enemy") {
result.enemy_did = enemy.combat->attack(player_combat);
}
break;
case BattleHostState::out_of_ap:
fmt::println("OUT OF AP {}", wants_to);
break;
}
world->send<game::Event>(game::Event::COMBAT, enemy.entity, result);
}
}

20
src/boss/system.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include "dinkyecs.hpp"
#include <memory>
#include "boss/fight.hpp"
namespace boss {
namespace System {
void load_config();
std::shared_ptr<boss::Fight> create_bossfight();
void initialize_actor_ai(DinkyECS::World& world, DinkyECS::Entity boss_id);
combat::BattleEngine create_battle(std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id);
void plan_battle(combat::BattleEngine& battle, std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id);
void combat(combat::BattleResult& action, std::shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id, int attack_id);
}
}

130
src/boss/ui.cpp Normal file
View file

@ -0,0 +1,130 @@
#include "boss/ui.hpp"
#include "scene.hpp"
#include "constants.hpp"
#include "components.hpp"
#include <fmt/xchar.h>
#include "game_level.hpp"
#include "gui/guecstra.hpp"
#include "events.hpp"
namespace boss {
using namespace guecs;
using namespace DinkyECS;
UI::UI(shared_ptr<World> world, Entity boss_id, Entity player_id) :
$world(world),
$boss_id(boss_id),
$player_id(player_id),
$combat_ui(true),
$arena(world->get<components::AnimatedScene>($boss_id)),
$view_texture({BOSS_VIEW_WIDTH, BOSS_VIEW_HEIGHT}),
$view_sprite($view_texture.getTexture())
{
$arena.set_end_cb([&]() {
$world->send<game::Event>(game::Event::ANIMATION_END, DinkyECS::NONE, {});
});
$view_sprite.setPosition({BOSS_VIEW_X, BOSS_VIEW_Y});
}
void UI::init() {
$arena.init();
$actions.position(0,0, SCREEN_WIDTH-BOSS_VIEW_WIDTH, SCREEN_HEIGHT);
$actions.layout(
"[*%(100,400)combat]"
"[_]"
"[_]"
"[_]"
"[commit]"
"[*%(100,300)stats]"
"[_]"
"[_]");
auto commit = $actions.entity("commit");
$actions.set<Rectangle>(commit, {});
$actions.set<Text>(commit, {L"COMMIT"});
$actions.set<Effect>(commit, {});
$actions.set<Clickable>(commit,
guecs::make_action(commit, game::Event::COMBAT_START, {}));
auto stats = $actions.entity("stats");
$actions.set<Rectangle>(stats, {});
update_stats();
$actions.init();
auto& cell = $actions.cell_for("combat");
$combat_ui.init(cell.x, cell.y, cell.w, cell.h);
}
void UI::update_stats() {
auto& player_combat = $world->get<components::Combat>($player_id);
auto& boss_combat = $world->get<components::Combat>($boss_id);
std::wstring status = fmt::format(
L"--PLAYER--\nHP:{}/{}\nAP:{}/{}\n\n--BOSS--\nHP:{}/{}\nAP:{}/{}\n----\n",
player_combat.hp, player_combat.max_hp,
player_combat.ap, player_combat.max_ap,
boss_combat.hp, boss_combat.max_hp,
boss_combat.ap, boss_combat.max_ap);
$actions.show_text("stats", status);
}
void UI::set_window(sf::RenderWindow* window) {
$window = window;
}
void UI::render() {
$actions.render(*$window);
$combat_ui.render(*$window);
$arena.render($view_texture);
$view_texture.display();
$window->draw($view_sprite);
}
void UI::update() {
$arena.update();
}
bool UI::mouse(float x, float y, Modifiers mods) {
// BUG: arena is getting the _window_ coordinates, not the rendertexture
return $combat_ui.mouse(x, y, mods)
|| $actions.mouse(x, y, mods) || $arena.mouse(x, y, mods);
}
void UI::status(const std::wstring& msg, const std::wstring &button_msg) {
$arena.$ui.show_text("status", msg);
$actions.show_text("commit", button_msg);
}
void UI::move_actor(const std::string& actor, const std::string& cell_name) {
$arena.move_actor(actor, cell_name);
}
void UI::animate_actor(const std::string& actor, const std::string& form) {
$arena.animate_actor(actor, form);
}
void UI::damage(const string& actor, const std::string& target, int amount) {
if(amount > 0) {
$arena.attach_text(target, fmt::format("{}", amount));
$arena.apply_effect(actor, "lightning");
// USING SCALE
float scale = 0.8f;
std::string style = "pan";
$arena.zoom(target, style, scale);
} else {
$arena.attach_text(actor, "MISSED");
}
}
void UI::reset_camera() {
$arena.reset($view_texture);
}
}

44
src/boss/ui.hpp Normal file
View file

@ -0,0 +1,44 @@
#pragma once
#include <memory>
#include <guecs/ui.hpp>
#include "gui/combat_ui.hpp"
#include "scene.hpp"
namespace components {
struct Animation;
}
// needed to break an include cycle
namespace GameDB {
struct Level;
}
namespace boss {
using std::shared_ptr;
struct UI {
sf::RenderWindow* $window = nullptr;
shared_ptr<DinkyECS::World> $world = nullptr;
DinkyECS::Entity $boss_id = DinkyECS::NONE;
DinkyECS::Entity $player_id = DinkyECS::NONE;
gui::CombatUI $combat_ui;
scene::Engine $arena;
guecs::UI $actions;
sf::RenderTexture $view_texture;
sf::Sprite $view_sprite;
UI(shared_ptr<DinkyECS::World> world, DinkyECS::Entity boss_id, DinkyECS::Entity player_id);
void init();
void set_window(sf::RenderWindow* window);
void render();
void update();
bool mouse(float x, float y, guecs::Modifiers mods);
void status(const std::wstring& msg, const std::wstring &button_msg);
void move_actor(const std::string& actor, const std::string& cell_name);
void animate_actor(const std::string& actor, const std::string& form);
void update_stats();
void damage(const std::string& actor, const std::string& target, int amount);
void reset_camera();
};
}