You can now take damage to your head.

This commit is contained in:
Zed A. Shaw 2026-03-29 23:54:21 -04:00
parent cbd4b858ac
commit d22eaa554d
7 changed files with 64 additions and 49 deletions

View file

@ -6,7 +6,7 @@
"foreground": "enemies/fg:player", "foreground": "enemies/fg:player",
"background": "color:transparent" "background": "color:transparent"
}, },
{"_type": "Combat", "hp": 200, "max_hp": 200, "ap": 0, "max_ap": 12, "ap_delta": 6, "damage": 50, "dead": false}, {"_type": "Combat", "ap": 0, "max_ap": 12, "ap_delta": 6, "damage": 50, "dead": false},
{"_type": "Motion", "dx": 0, "dy": 0, "random": false}, {"_type": "Motion", "dx": 0, "dy": 0, "random": false},
{"_type": "Collision", "has": true}, {"_type": "Collision", "has": true},
{"_type": "EnemyConfig", "ai_script": "Host::actions", "ai_start_name": "Host::initial_state", "ai_goal_name": "Host::final_state"}, {"_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:rat_giant", "foreground": "enemies/fg:rat_giant",
"background": "color:transparent" "background": "color:transparent"
}, },
{"_type": "Combat", "hp": 50, "max_hp": 50, "ap": 0, "max_ap": 12, "ap_delta": 6,"damage": 2, "dead": false}, {"_type": "Combat", "ap": 0, "max_ap": 12, "ap_delta": 6,"damage": 2, "dead": false},
{"_type": "Collision", "has": true}, {"_type": "Collision", "has": true},
{"_type": "Motion", "dx": 0, "dy": 0, "random": false}, {"_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"}, {"_type": "EnemyConfig", "ai_script": "Enemy::actions", "ai_start_name": "Enemy::initial_state", "ai_goal_name": "Enemy::final_state"},

View file

@ -8,9 +8,30 @@ namespace components {
if(attack) { if(attack) {
my_dmg = Random::uniform<int>(1, damage); my_dmg = Random::uniform<int>(1, damage);
target.hp -= my_dmg; target.hit_limb(my_dmg);
} }
return my_dmg; return my_dmg;
} }
void Combat::hit_limb(int my_dmg) {
body_parts["head"] -= my_dmg;
}
bool Combat::is_dead() {
return body_parts["head"] < 0;
}
bool Combat::almost_dead() {
return body_parts["head"] < 20;
}
bool Combat::can_heal() {
return body_parts["head"] < 50;
}
void Combat::apply_healing(Curative& cure) {
int new_hp = body_parts["head"] + cure.hp;
body_parts["head"] = std::min(new_hp, 50);
}
} }

View file

@ -83,10 +83,6 @@ namespace components {
std::string ai_goal_name; std::string ai_goal_name;
}; };
struct Curative {
int hp = 10;
};
struct Sprite { struct Sprite {
string name; string name;
float scale; float scale;
@ -106,12 +102,17 @@ namespace components {
std::vector<std::array<std::string, 4>> beats; std::vector<std::array<std::string, 4>> beats;
}; };
struct Curative {
int hp = 10;
};
struct Combat { struct Combat {
int hp;
int max_hp;
int ap_delta; int ap_delta;
int max_ap; int max_ap;
int damage; int damage;
std::unordered_map<std::string, int> body_parts{
{"head", 50},
};
// everyone starts at 0 but ap_delta is added each round // everyone starts at 0 but ap_delta is added each round
int ap = 0; int ap = 0;
@ -120,6 +121,11 @@ namespace components {
bool dead = false; bool dead = false;
int attack(Combat &target); int attack(Combat &target);
void hit_limb(int my_dmg);
bool is_dead();
bool almost_dead();
bool can_heal();
void apply_healing(Curative& cure);
}; };
struct LightSource { struct LightSource {
@ -156,7 +162,7 @@ namespace components {
ENROLL_COMPONENT(EnemyConfig, ai_script, ai_start_name, ai_goal_name); ENROLL_COMPONENT(EnemyConfig, ai_script, ai_start_name, ai_goal_name);
ENROLL_COMPONENT(Personality, hearing_distance, tough); ENROLL_COMPONENT(Personality, hearing_distance, tough);
ENROLL_COMPONENT(Motion, dx, dy, random); ENROLL_COMPONENT(Motion, dx, dy, random);
ENROLL_COMPONENT(Combat, hp, max_hp, ap_delta, max_ap, damage, dead); ENROLL_COMPONENT(Combat, ap_delta, max_ap, damage, dead);
ENROLL_COMPONENT(Device, config, events); ENROLL_COMPONENT(Device, config, events);
ENROLL_COMPONENT(Storyboard, image, audio, layout, beats); ENROLL_COMPONENT(Storyboard, image, audio, layout, beats);
ENROLL_COMPONENT(Sound, attack, death); ENROLL_COMPONENT(Sound, attack, death);

View file

@ -188,7 +188,7 @@ void System::death() {
world.query<Combat>([&](auto ent, auto &combat) { world.query<Combat>([&](auto ent, auto &combat) {
// bring out yer dead // bring out yer dead
if(combat.hp <= 0 && !combat.dead) { if(combat.is_dead() && !combat.dead) {
combat.dead = true; combat.dead = true;
if(ent != player.entity) { if(ent != player.entity) {
// we won't change out the player's components later // we won't change out the player's components later
@ -196,7 +196,7 @@ void System::death() {
} }
// we need to send this event for everything that dies // we need to send this event for everything that dies
world.send<game::Event>(game::Event::DEATH, ent, {}); world.send<game::Event>(game::Event::DEATH, ent, {});
} else if(float(combat.hp) / float(combat.max_hp) < 0.5f) { } else if(combat.almost_dead()) {
// if enemies are below 50% health they are marked with bad health // if enemies are below 50% health they are marked with bad health
if(world.has<ai::EntityAI>(ent)) { if(world.has<ai::EntityAI>(ent)) {
auto& enemy_ai = world.get<ai::EntityAI>(ent); auto& enemy_ai = world.get<ai::EntityAI>(ent);
@ -479,23 +479,14 @@ void System::use_item(const string& slot_name) {
auto& inventory = world.get<inventory::Model>(level.player); auto& inventory = world.get<inventory::Model>(level.player);
auto& player_combat = world.get<Combat>(level.player); auto& player_combat = world.get<Combat>(level.player);
if(player_combat.hp >= player_combat.max_hp) return; if(!player_combat.can_heal()) return;
if(!inventory.has(slot_name)) return; if(!inventory.has(slot_name)) return;
auto what = inventory.get(slot_name); auto what = inventory.get(slot_name);
if(auto curative = world.get_if<Curative>(what)) { if(auto curative = world.get_if<Curative>(what)) {
inventory.remove(what); inventory.remove(what);
player_combat.apply_healing(*curative);
player_combat.hp += curative->hp;
if(player_combat.hp > player_combat.max_hp) {
player_combat.hp = player_combat.max_hp;
}
dbc::log($F("player health now {}",
player_combat.hp));
world.remove<Curative>(what); world.remove<Curative>(what);
} else { } else {
dbc::log($F("no usable item at {}", what)); dbc::log($F("no usable item at {}", what));

View file

@ -15,13 +15,13 @@ namespace gui {
void BodyUI::init(size_t x, size_t y, size_t width, size_t height) { void BodyUI::init(size_t x, size_t y, size_t width, size_t height) {
$gui.position(x, y, width, height); $gui.position(x, y, width, height);
$gui.layout( $gui.layout(
"[body_head]" "[head]"
"[body_chest]" "[chest]"
"[body_right_arm]" "[right_arm]"
"[body_left_arm]" "[left_arm]"
"[body_stomach]" "[stomach]"
"[body_left_leg]" "[left_leg]"
"[body_right_leg]"); "[right_leg]");
$gui.set<Background>($gui.MAIN, {$gui.$parser, }); $gui.set<Background>($gui.MAIN, {$gui.$parser, });
@ -42,7 +42,16 @@ namespace gui {
void BodyUI::update() { void BodyUI::update() {
auto world = GameDB::current_world(); auto world = GameDB::current_world();
auto player = world->get_the<components::Player>(); auto& player = world->get_the<components::Player>();
auto& player_combat = world->get<components::Combat>(player.entity);
for(auto& [key, value] : player_combat.body_parts) {
auto gui_id = $gui.entity(key);
if(auto meter = $gui.get_if<Meter>(gui_id)) {
meter->percent = float(value) / 50.0;
}
}
} }
void BodyUI::render(sf::RenderWindow &window) { void BodyUI::render(sf::RenderWindow &window) {

View file

@ -48,7 +48,6 @@ namespace gui {
auto map = level.map; auto map = level.map;
std::wstring stats = $F(L"STATS\n" std::wstring stats = $F(L"STATS\n"
L"HP: {}\n"
L"mean:{:>8.5}\n" L"mean:{:>8.5}\n"
L"sdev: {:>8.5}\n" L"sdev: {:>8.5}\n"
L"min: {:>8.5}\n" L"min: {:>8.5}\n"
@ -58,7 +57,7 @@ namespace gui {
L"VSync? {}\n" L"VSync? {}\n"
L"FR Limit: {}\n" L"FR Limit: {}\n"
L"Debug? {}\n\n", L"Debug? {}\n\n",
player_combat.hp, $stats.mean(), $stats.stddev(), $stats.min, $stats.mean(), $stats.stddev(), $stats.min,
$stats.max, $stats.n, level.index, map->width(), map->height(), $stats.max, $stats.n, level.index, map->width(), map->height(),
VSYNC, FRAME_LIMIT, DEBUG_BUILD); VSYNC, FRAME_LIMIT, DEBUG_BUILD);
@ -74,9 +73,6 @@ namespace gui {
if(active) { if(active) {
auto& level = GameDB::current_level(); auto& level = GameDB::current_level();
// it's on now, enable things // it's on now, enable things
auto player = level.world->get_the<components::Player>();
auto& player_combat = level.world->get<components::Combat>(player.entity);
player_combat.hp = player_combat.max_hp;
$gui.show_text("debug_text", L"STATS"); $gui.show_text("debug_text", L"STATS");
} else { } else {
// it's off now, close it // it's off now, close it

View file

@ -25,20 +25,17 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") {
DinkyECS::Entity host = 0; DinkyECS::Entity host = 0;
ai::EntityAI host_ai("Host::actions", host_start, host_goal); ai::EntityAI host_ai("Host::actions", host_start, host_goal);
components::Combat host_combat{ components::Combat host_combat{.ap_delta=6, .max_ap=12, .damage=20};
.hp=100, .max_hp=100, .ap_delta=6, .max_ap=12, .damage=20};
battle.add_enemy({host, &host_ai, &host_combat, true}); battle.add_enemy({host, &host_ai, &host_combat, true});
DinkyECS::Entity axe_ranger = 1; DinkyECS::Entity axe_ranger = 1;
ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal); ai::EntityAI axe_ai("Enemy::actions", ai_start, ai_goal);
components::Combat axe_combat{ components::Combat axe_combat{.ap_delta=8, .max_ap=12, .damage=20};
.hp=20, .max_hp=20, .ap_delta=8, .max_ap=12, .damage=20};
battle.add_enemy({axe_ranger, &axe_ai, &axe_combat}); battle.add_enemy({axe_ranger, &axe_ai, &axe_combat});
DinkyECS::Entity rat = 2; DinkyECS::Entity rat = 2;
ai::EntityAI rat_ai("Enemy::actions", ai_start, ai_goal); ai::EntityAI rat_ai("Enemy::actions", ai_start, ai_goal);
components::Combat rat_combat{ components::Combat rat_combat{.ap_delta=2, .max_ap=10, .damage=10};
.hp=10, .max_hp=10, .ap_delta=2, .max_ap=10, .damage=10};
battle.add_enemy({rat, &rat_ai, &rat_combat}); battle.add_enemy({rat, &rat_ai, &rat_combat});
battle.set_all("enemy_found", true); battle.set_all("enemy_found", true);
@ -50,8 +47,8 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") {
battle.set(host, "have_healing", false); battle.set(host, "have_healing", false);
battle.set(host, "tough_personality", false); battle.set(host, "tough_personality", false);
while(host_combat.hp > 0) { while(!host_combat.is_dead()) {
battle.set(host, "health_good", host_combat.hp > 20); battle.set(host, "health_good", host_combat.almost_dead());
battle.player_request("use_healing"); battle.player_request("use_healing");
battle.player_request("kill_enemy"); battle.player_request("kill_enemy");
@ -62,11 +59,6 @@ TEST_CASE("battle operations fantasy", "[combat-battle]") {
while(auto act = battle.next()) { while(auto act = battle.next()) {
auto& [enemy, wants_to, cost, enemy_state] = *act; auto& [enemy, wants_to, cost, enemy_state] = *act;
// fmt::println(">>>>> entity: {} wants to {} cost={}; has {} HP; {} ap",
// enemy.entity, wants_to,
// cost, enemy.combat->hp,
// enemy.combat->ap);
switch(enemy_state) { switch(enemy_state) {
case BattleHostState::agree: case BattleHostState::agree:
// fmt::println("HOST and PLAYER requests match {}, doing it.", wants_to); // fmt::println("HOST and PLAYER requests match {}, doing it.", wants_to);