Started working on this 'arena tester' tool that would let me load an enemy and test them, but then realized I could just make it so I can spawn enemies in the game. I'm keeping the arena around as it will be useful later as a scriptable testing tool, but for now just spawn and test.

This commit is contained in:
Zed A. Shaw 2025-04-04 12:45:55 -04:00
parent b6c1eba1b3
commit 4f090159ab
14 changed files with 524 additions and 58 deletions

45
tools/arena.cpp Normal file
View file

@ -0,0 +1,45 @@
#include "arena_fsm.hpp"
#include "textures.hpp"
#include "sound.hpp"
#include "ai.hpp"
#include "animation.hpp"
#include <iostream>
int main(int argc, char* argv[]) {
try {
dbc::check(argc == 2, "USAGE: arena enemy_name");
std::string enemy_name{argv[1]};
textures::init();
sound::init();
ai::init("assets/ai.json");
animation::init();
sound::mute(false);
sound::play("ambient_1", true);
arena::FSM main(enemy_name);
main.event(arena::Event::STARTED);
while(main.active()) {
main.render();
// ZED: need to sort out how to deal with this in the FSM
if(main.in_state(arena::State::IDLE)) {
main.event(arena::Event::TICK);
}
main.keyboard_mouse();
main.handle_world_events();
}
return 0;
} catch(const std::system_error& e) {
std::cout << "WARNING: On OSX you'll get this error on shutdown.\n";
std::cout << "Caught system_error with code "
"[" << e.code() << "] meaning "
"[" << e.what() << "]\n";
}
}

120
tools/arena_fsm.cpp Normal file
View file

@ -0,0 +1,120 @@
#include "gui_fsm.hpp"
#include <iostream>
#include <chrono>
#include <numeric>
#include <functional>
#include "components.hpp"
#include <numbers>
#include "systems.hpp"
#include "events.hpp"
#include "sound.hpp"
#include <fmt/xchar.h>
#include "arena_fsm.hpp"
namespace arena {
using namespace components;
FSM::FSM(std::string enemy_name) :
$enemy_name(enemy_name),
$window(sf::VideoMode({SCREEN_WIDTH, SCREEN_HEIGHT}), "Arena Battle Tester"),
$font{FONT_FILE_NAME}
{
}
void FSM::event(Event ev) {
switch($state) {
FSM_STATE(State, START, ev);
FSM_STATE(State, IDLE, ev);
FSM_STATE(State, END, ev);
}
}
void FSM::START(Event ) {
run_systems();
$level = $level_mgr.current();
auto entity_id = $level_mgr.spawn_enemy($enemy_name);
$arena_ui = make_shared<ArenaUI>($level.world, entity_id);
$arena_ui->init();
state(State::IDLE);
}
void FSM::END(Event ev) {
dbc::log(fmt::format("END: received event after done: {}", int(ev)));
}
void FSM::IDLE(Event ev) {
using enum Event;
switch(ev) {
case QUIT:
$window.close();
state(State::END);
return; // done
case CLOSE:
dbc::log("Nothing to close.");
break;
case TICK:
// do nothing
break;
case ATTACK:
dbc::log("ATTACK!");
break;
default:
dbc::sentinel("unhandled event in IDLE");
}
}
void FSM::keyboard_mouse() {
while(const auto ev = $window.pollEvent()) {
if(ev->is<sf::Event::Closed>()) {
event(Event::QUIT);
}
if(const auto* mouse = ev->getIf<sf::Event::MouseButtonPressed>()) {
if(mouse->button == sf::Mouse::Button::Left) {
sf::Vector2f pos = $window.mapPixelToCoords(mouse->position);
(void)pos;
}
}
if(const auto* key = ev->getIf<sf::Event::KeyPressed>()) {
using KEY = sf::Keyboard::Scan;
switch(key->scancode) {
case KEY::Escape:
event(Event::CLOSE);
break;
case KEY::Space:
event(Event::ATTACK);
break;
default:
break; // ignored
}
}
}
}
void FSM::draw_gui() {
if($arena_ui != nullptr) {
$arena_ui->render($window);
}
}
void FSM::render() {
$window.clear();
draw_gui();
$window.display();
}
void FSM::run_systems() {
}
bool FSM::active() {
return !in_state(State::END);
}
void FSM::handle_world_events() {
}
}

52
tools/arena_fsm.hpp Normal file
View file

@ -0,0 +1,52 @@
#pragma once
#include "constants.hpp"
#include "stats.hpp"
#include "levelmanager.hpp"
#include "fsm.hpp"
#include "main_ui.hpp"
#include "combat_ui.hpp"
#include "status_ui.hpp"
#include "arena_ui.hpp"
#include "map_view.hpp"
#include "mini_map.hpp"
namespace arena {
enum class State {
START,
IDLE,
END
};
enum class Event {
STARTED=0,
TICK=1,
CLOSE = 7,
ATTACK = 10,
QUIT = 14
};
class FSM : public DeadSimpleFSM<State, Event> {
public:
std::string $enemy_name;
sf::RenderWindow $window;
sf::Font $font;
LevelManager $level_mgr;
GameLevel $level;
shared_ptr<arena::ArenaUI> $arena_ui = nullptr;
FSM(std::string enemy_name);
void event(Event ev);
void START(Event );
void IDLE(Event ev);
void END(Event ev);
void try_move(int dir, bool strafe);
void keyboard_mouse();
void draw_gui();
void render();
bool active();
void run_systems();
void handle_world_events();
};
}

133
tools/arena_ui.cpp Normal file
View file

@ -0,0 +1,133 @@
#include "arena_ui.hpp"
#include "easings.hpp"
#include "sound.hpp"
#include <fmt/xchar.h>
namespace arena {
using namespace guecs;
ArenaUI::ArenaUI(shared_ptr<DinkyECS::World> world, DinkyECS::Entity entity_id)
: $world(world),
$entity_id(entity_id),
$config(world->get_the<components::GameConfig>())
{
$status.position(0, 0, BOSS_VIEW_X, SCREEN_HEIGHT);
$status.layout(
"[main_status]"
"[(150)status_3|(150)status_4]"
"[(150)status_5|(150)status_6]"
"[(150)status_7|(150)status_8]");
$overlay.position(BOSS_VIEW_X, BOSS_VIEW_Y,
BOSS_VIEW_WIDTH, BOSS_VIEW_HEIGHT);
$overlay.layout("[_|=*%(200)enemy|_|_]");
$sounds = $world->get<components::Sound>($entity_id);
$combat = $world->get<components::Combat>($entity_id);
$sprite_config = $world->get<components::Sprite>($entity_id);
}
void ArenaUI::configure_sprite() {
$animation = $world->get<components::Animation>($entity_id);
$animation.frame_width = $sprite_config.width;
auto enemy_id = $overlay.entity("enemy");
auto& enemy_image = $overlay.get<Sprite>(enemy_id);
sf::IntRect frame_rect{{0,0},{$sprite_config.width, $sprite_config.height}};
enemy_image.sprite->setTextureRect(frame_rect);
}
void ArenaUI::configure_background() {
if($world->has<components::BossFight>($entity_id)) {
auto& boss = $world->get<components::BossFight>($entity_id);
$entity_background = textures::get(boss.background);
$entity_background.sprite->setPosition({BOSS_VIEW_X, BOSS_VIEW_Y});
$status.world().set_the<Background>({$status.$parser});
$entity_has_stage = true;
if(boss.stage) {
$entity_stage = textures::get(*boss.stage);
} else {
$entity_stage = textures::get("devils_fingers_background");
}
$entity_stage.sprite->setPosition({BOSS_VIEW_X, BOSS_VIEW_Y});
} else {
$entity_has_stage = false;
$entity_background = textures::get("devils_fingers_background");
$entity_background.sprite->setPosition({BOSS_VIEW_X, BOSS_VIEW_Y});
$status.world().set_the<Background>({$status.$parser});
}
}
void ArenaUI::configure_gui() {
for(auto& [name, cell] : $status.cells()) {
auto button = $status.entity(name);
$status.set<Rectangle>(button, {});
$status.set<Clickable>(button, {
[this, name](auto, auto){
dbc::log(fmt::format("STATUS: {}", name));
}
});
if(name == "main_status") {
$status.set<Textual>(button, {fmt::format(L"HP: {}", $combat.hp)});
} else {
$status.set<Label>(button, {L"Attack"});
}
}
$status.init();
for(auto& [name, cell] : $overlay.cells()) {
auto region = $overlay.entity(name);
$overlay.set<Clickable>(region, {
[this, name](auto, auto){
dbc::log(fmt::format("OVERLAY: {}", name));
}
});
if(name == "enemy") {
$overlay.set<Sprite>(region, {$sprite_config.name, 20});
}
}
$overlay.init();
configure_sprite();
}
void ArenaUI::init() {
// background must come first
configure_background();
configure_gui();
}
void ArenaUI::render(sf::RenderWindow& window) {
window.draw(*$entity_background.sprite);
if($entity_has_stage) {
window.draw(*$entity_stage.sprite);
}
$status.render(window);
$overlay.render(window);
}
bool ArenaUI::mouse(float x, float y) {
if($status.mouse(x, y)) {
dbc::log("STATUS button pressed");
}
if($overlay.mouse(x, y)) {
$animation.play();
sound::play("Sword_Hit_1");
$entity_hit = !$entity_hit;
$combat.hp--;
}
return false;
}
}

45
tools/arena_ui.hpp Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/Graphics/Font.hpp>
#include "guecs.hpp"
#include "textures.hpp"
#include "components.hpp"
#include <SFML/System/Clock.hpp>
// aspect ratio of art is 3/2 so 1.5
// possible sizes: 900/600; 1620/1080; 1800/1200
// To calculate it do short side * 1.5 so 1080 * 1.5 == 1620
//
// Side panel = 300/1080
namespace arena {
using std::string, std::shared_ptr;
class ArenaUI {
public:
sf::Clock $clock;
bool $entity_hit = false;
sf::Vector2f $entity_pos;
components::Combat $combat;
components::Sprite $sprite_config;
components::Sound $sounds;
components::Animation $animation;
guecs::UI $status;
guecs::UI $overlay;
textures::SpriteTexture $entity_background;
bool $entity_has_stage = false;
textures::SpriteTexture $entity_stage;
std::shared_ptr<DinkyECS::World> $world = nullptr;
DinkyECS::Entity $entity_id;
components::GameConfig& $config;
ArenaUI(shared_ptr<DinkyECS::World> world, DinkyECS::Entity entity_id);
void init();
void render(sf::RenderWindow& window);
bool mouse(float x, float y);
void configure_sprite();
void configure_background();
void configure_gui();
};
}