From a22564bb58b8f4f7fcf6590f230ef1065e9ba953 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Sun, 24 May 2026 11:59:51 -0400 Subject: [PATCH] Synced the combat from under the dome. --- assets/bosses.json | 3 ++- assets/enemies.json | 12 ++++----- assets/scenes.json | 2 ++ src/combat/combat.cpp | 55 ++++++++++++++++++++++++++++++++++++++--- src/game/components.hpp | 28 ++++++++++++++------- src/game/systems.cpp | 5 ++-- tests/battle.cpp | 18 +++++++++++--- wraps/magic_enum.wrap | 10 ++++++++ 8 files changed, 106 insertions(+), 27 deletions(-) create mode 100644 assets/scenes.json create mode 100644 wraps/magic_enum.wrap diff --git a/assets/bosses.json b/assets/bosses.json index 7e31746..580c79c 100644 --- a/assets/bosses.json +++ b/assets/bosses.json @@ -10,6 +10,7 @@ "[floor3|player1|player2|player3|player4|_]", "[floor4|player5|player6|player7|player8|_]" ], + "buttons": [], "background": "test_background", "actors": [ { @@ -62,7 +63,7 @@ } ] }, - {"_type": "Combat", "hp": 200, "max_hp": 200, "ap": 0, "ap_delta": 9, "max_ap": 20, "damage": 20, "dead": false}, + {"_type": "Combat", "hp": 200, "max_hp": 200, "ap": 0, "ap_delta": 9, "max_ap": 20, "damage": 20, "dead": false, "attack_rating": 0.80, "toughness_rating": 0.30}, {"_type": "Sound", "attack": "Marmot_Scream_1", "death": "Creature_Death_1"}, {"_type": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"} ] diff --git a/assets/enemies.json b/assets/enemies.json index 26d8b72..b241fdd 100644 --- a/assets/enemies.json +++ b/assets/enemies.json @@ -6,7 +6,7 @@ "foreground": "enemies/fg:player", "background": "color:transparent" }, - {"_type": "Combat", "hp": 200, "max_hp": 200, "ap": 0, "max_ap": 12, "ap_delta": 6, "damage": 50, "dead": false}, + {"_type": "Combat", "hp": 200, "max_hp": 200, "ap": 0, "max_ap": 12, "ap_delta": 6, "damage": 50, "dead": false, "attack_rating": 0.60, "toughness_rating": 0.0}, {"_type": "Motion", "dx": 0, "dy": 0, "random": false}, {"_type": "Collision", "has": true}, {"_type": "EnemyConfig", "ai_script": "Host::actions", "ai_start_name": "Host::initial_state", "ai_goal_name": "Host::final_state"}, @@ -20,7 +20,7 @@ "foreground": "enemies/fg:gold_savior", "background": "color:transparent" }, - {"_type": "Combat", "hp": 20, "max_hp": 20, "ap": 0, "max_ap": 12, "ap_delta": 6, "damage": 1, "dead": false}, + {"_type": "Combat", "hp": 20, "max_hp": 20, "ap": 0, "max_ap": 12, "ap_delta": 6, "damage": 1, "dead": false, "attack_rating": 0.60, "toughness_rating": 0.10}, {"_type": "Collision", "has": true}, {"_type": "Motion", "dx": 0, "dy": 0, "random": false}, {"_type": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"}, @@ -35,7 +35,7 @@ "foreground": "enemies/fg:knight", "background": "color:transparent" }, - {"_type": "Combat", "hp": 20, "max_hp": 20, "ap": 0, "max_ap": 12, "ap_delta": 6,"damage": 1, "dead": false}, + {"_type": "Combat", "hp": 20, "max_hp": 20, "ap": 0, "max_ap": 12, "ap_delta": 6,"damage": 1, "dead": false, "attack_rating": 0.60, "toughness_rating": 0.20}, {"_type": "Collision", "has": true}, {"_type": "Motion", "dx": 0, "dy": 0, "random": false}, {"_type": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"}, @@ -50,7 +50,7 @@ "foreground": "enemies/fg:axe_ranger", "background": "color:transparent" }, - {"_type": "Combat", "hp": 40, "max_hp": 40, "ap": 0, "max_ap": 12, "ap_delta": 6,"damage": 10, "dead": false}, + {"_type": "Combat", "hp": 40, "max_hp": 40, "ap": 0, "max_ap": 12, "ap_delta": 6,"damage": 10, "dead": false, "attack_rating": 0.60, "toughness_rating": 0.5}, {"_type": "Collision", "has": true}, {"_type": "Motion", "dx": 0, "dy": 0, "random": true}, {"_type": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"}, @@ -65,7 +65,7 @@ "foreground": "enemies/fg:rat_giant", "background": "color:transparent" }, - {"_type": "Combat", "hp": 50, "max_hp": 50, "ap": 0, "max_ap": 12, "ap_delta": 6,"damage": 2, "dead": false}, + {"_type": "Combat", "hp": 50, "max_hp": 50, "ap": 0, "max_ap": 12, "ap_delta": 6,"damage": 2, "dead": false, "attack_rating": 0.60, "toughness_rating": 0.0}, {"_type": "Collision", "has": true}, {"_type": "Motion", "dx": 0, "dy": 0, "random": false}, {"_type": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"}, @@ -80,7 +80,7 @@ "foreground": "enemies/fg:spider_giant", "background": "color:transparent" }, - {"_type": "Combat", "hp": 20, "max_hp": 20, "ap": 0, "max_ap": 12, "ap_delta": 6,"damage": 20, "dead": false}, + {"_type": "Combat", "hp": 20, "max_hp": 20, "ap": 0, "max_ap": 12, "ap_delta": 6,"damage": 20, "dead": false, "attack_rating": 0.60, "toughness_rating": 0.10}, {"_type": "Collision", "has": true}, {"_type": "Motion", "dx": 0, "dy": 0, "random": false}, {"_type": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"}, diff --git a/assets/scenes.json b/assets/scenes.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/assets/scenes.json @@ -0,0 +1,2 @@ +{ +} diff --git a/src/combat/combat.cpp b/src/combat/combat.cpp index 042895b..81d1003 100644 --- a/src/combat/combat.cpp +++ b/src/combat/combat.cpp @@ -3,14 +3,61 @@ namespace components { int Combat::attack(Combat &target) { - int attack = Random::uniform(0,1); + float hit_prob = Random::uniform_real(0.0f, 1.0f); + int my_dmg = 0; - if(attack) { - my_dmg = Random::uniform(1, damage); - target.hp -= my_dmg; + if(hit_prob < attack_rating) { + my_dmg = std::ceil(Random::uniform_real(1.0f, float(damage)) * (1.0f - toughness_rating)); + target.take_damage(my_dmg); } + INVARIANT(); return my_dmg; } + + void Combat::take_damage(int my_dmg) { + // catch this bug + dbc::check(hp >= 0, "HP went negative"); + + // don't hit dead parts + if(hp == 0) return; + + // don't go below 0 + hp = std::max(0, hp - my_dmg); + + has_died = hp <= 0; + INVARIANT(); + } + + bool Combat::less_than(int level) { + INVARIANT(); + // originally this checked main body parts like + // head, stomach, and chest + return hp <= level; + } + + bool Combat::is_dead() { + INVARIANT(); + return has_died; + } + + bool Combat::almost_dead() { + return less_than(max_hp / 4); + } + + bool Combat::can_heal() { + INVARIANT(); + return hp < max_hp; + } + + void Combat::apply_healing(Curative& cure) { + INVARIANT(); + hp += std::min(hp + cure.hp, max_hp); + } + + void Combat::INVARIANT() { + dbc::check(!(hp <= 0 && has_died == false), "entity hp <= 0 && has_died==false, they should be dead"); + dbc::check(!(hp > 0 && has_died == true), "entity has hp > 0 but is marked dead, should be alive"); + } } diff --git a/src/game/components.hpp b/src/game/components.hpp index 3d62c8b..78f16c0 100644 --- a/src/game/components.hpp +++ b/src/game/components.hpp @@ -101,6 +101,7 @@ namespace components { std::vector layout; json actors; json fixtures; + json buttons; }; struct Storyboard { @@ -111,19 +112,28 @@ namespace components { }; struct Combat { - int hp; - int max_hp; - int ap_delta; - int max_ap; - int damage; + int hp=1; + int max_hp=1; + int ap_delta=1; + int max_ap=1; + int damage=1; + float attack_rating=0.1; + float toughness_rating=0.1; // everyone starts at 0 but ap_delta is added each round int ap = 0; /* NOTE: This is used to _mark_ entities as dead, to detect ones that have just died. Don't make attack automatically set it.*/ - bool dead = false; + bool has_died = false; int attack(Combat &target); + bool less_than(int level); + void take_damage(int my_dmg); + bool is_dead(); + bool almost_dead(); + bool can_heal(); + void apply_healing(Curative& cure); + void INVARIANT(); }; struct LightSource { @@ -152,7 +162,7 @@ namespace components { using ComponentMap = std::unordered_map; ENROLL_COMPONENT(Tile, display, foreground, background); - ENROLL_COMPONENT(AnimatedScene, background, layout, actors, fixtures); + ENROLL_COMPONENT(AnimatedScene, background, layout, actors, fixtures, buttons); ENROLL_COMPONENT(Sprite, name, scale); ENROLL_COMPONENT(Curative, hp); ENROLL_COMPONENT(LightSource, strength, radius); @@ -160,7 +170,7 @@ namespace components { ENROLL_COMPONENT(EnemyConfig, ai_script, ai_start_name, ai_goal_name); ENROLL_COMPONENT(Personality, hearing_distance, tough); ENROLL_COMPONENT(Motion, dx, dy, random); - ENROLL_COMPONENT(Combat, hp, max_hp, ap_delta, max_ap, damage, dead); + ENROLL_COMPONENT(Combat, hp, max_hp, ap_delta, max_ap, damage, attack_rating, toughness_rating); ENROLL_COMPONENT(Device, config, events); ENROLL_COMPONENT(Storyboard, image, audio, layout, beats); ENROLL_COMPONENT(Sound, attack, death); @@ -184,7 +194,7 @@ namespace components { template void enroll(ComponentMap &m) { m[NameOf::name] = [](DinkyECS::World& world, DinkyECS::Entity ent, nlohmann::json &j) { - COMPONENT c; + COMPONENT c{}; from_json(j, c); world.set(ent, c); }; diff --git a/src/game/systems.cpp b/src/game/systems.cpp index af05116..efb0bb5 100644 --- a/src/game/systems.cpp +++ b/src/game/systems.cpp @@ -192,8 +192,7 @@ void System::death() { world.query([&](auto ent, auto &combat) { // bring out yer dead - if(combat.hp <= 0 && !combat.dead) { - combat.dead = true; + if(combat.is_dead()) { if(ent != player.entity) { // we won't change out the player's components later dead_things.push_back(ent); @@ -301,7 +300,7 @@ void System::collision() { for(auto entity : nearby) { if(world.has(entity)) { auto combat = world.get(entity); - if(!combat.dead) { + if(!combat.is_dead()) { combat_count++; world.send(game::Event::COMBAT_START, entity, entity); } diff --git a/tests/battle.cpp b/tests/battle.cpp index ecdd3da..71eefd9 100644 --- a/tests/battle.cpp +++ b/tests/battle.cpp @@ -16,7 +16,7 @@ using namespace combat; using namespace boss; using namespace components; -TEST_CASE("battle operations fantasy", "[combat-battle]") { +TEST_CASE("battle operations fantasy", "[fail]") { ai::reset(); ai::init("ai"); @@ -53,7 +53,10 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") { battle.set(host, "have_healing", false); battle.set(host, "tough_personality", false); - while(host_combat.hp > 0) { + int host_combat_loop = 0; + for(host_combat_loop = 0; host_combat_loop < 1000; host_combat_loop++) { + fmt::println("host HP is {}", host_combat.hp); + if(host_combat.is_dead()) break; battle.set(host, "health_good", host_combat.hp > 20); battle.player_request("use_healing"); @@ -61,8 +64,12 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") { battle.ap_refresh(); battle.plan(); + int battle_count = 0; + + for(int battle_count = 0; battle_count < 1000; battle_count++) { + auto act = battle.next(); + if(!act) break; - while(auto act = battle.next()) { auto& [enemy, wants_to, cost, enemy_state] = *act; // fmt::println(">>>>> entity: {} wants to {} cost={}; has {} HP; {} ap", @@ -81,6 +88,7 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") { break; case BattleHostState::not_host: if(wants_to == "kill_enemy") { + fmt::println("ATTACK!"); enemy.combat->attack(host_combat); } break; @@ -91,9 +99,11 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") { } REQUIRE(!battle.next()); + dbc::check(battle_count < 1000, "infinite battle loop"); } -} + dbc::check(host_combat_loop < 1000, "infinite host combat loop, host won't die!"); +} TEST_CASE("boss/systems.cpp works", "[combat-battle]") { components::init(); diff --git a/wraps/magic_enum.wrap b/wraps/magic_enum.wrap new file mode 100644 index 0000000..031a625 --- /dev/null +++ b/wraps/magic_enum.wrap @@ -0,0 +1,10 @@ +[wrap-file] +directory = magic_enum-0.9.7 +source_url = https://github.com/Neargye/magic_enum/archive/refs/tags/v0.9.7.tar.gz +source_filename = magic_enum-v0.9.7.tar.gz +source_hash = b403d3dad4ef542fdc3024fa37d3a6cedb4ad33c72e31b6d9bab89dcaf69edf7 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/magic_enum_0.9.7-1/magic_enum-v0.9.7.tar.gz +wrapdb_version = 0.9.7-1 + +[provide] +magic_enum = magic_enum_dep