Now the body_ui handles the toughness and attack rating, and applies colors to show your body part status.

This commit is contained in:
Zed A. Shaw 2026-04-03 23:42:49 -04:00
parent 009c5c1cd2
commit 831b46fa3f
11 changed files with 92 additions and 20 deletions

View file

@ -6,7 +6,8 @@
"foreground": "enemies/fg:player", "foreground": "enemies/fg:player",
"background": "color:transparent" "background": "color:transparent"
}, },
{"_type": "Combat", "max_hp": 200, "max_ap": 12, "ap_delta": 6, "damage": 20, {"_type": "Combat", "max_hp": 200, "max_ap": 12,
"ap_delta": 6, "damage": 20, "attack_rating": 0.5, "toughness_rating": 0.1,
"body_parts": { "body_parts": {
"head": 200, "head": 200,
"chest": 200, "chest": 200,
@ -21,7 +22,8 @@
{"_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"},
{"_type": "Personality", "hearing_distance": 5, "tough": false}, {"_type": "Personality", "hearing_distance": 5, "tough": false},
{"_type": "LightSource", "strength": 35, "radius": 2.0} {"_type": "LightSource", "strength": 35, "radius": 2.0},
{"_type": "LearnByDoing", "attacks": 0, "hits": 0, "next_attack_level": 10, "next_toughness_level": 10}
] ]
}, },
"SPIDER_BOT": { "SPIDER_BOT": {
@ -30,7 +32,8 @@
"foreground": "enemies/fg:rat_giant", "foreground": "enemies/fg:rat_giant",
"background": "color:transparent" "background": "color:transparent"
}, },
{"_type": "Combat", "max_hp": 50, "max_ap": 12, "ap_delta": 6,"damage": 30, {"_type": "Combat", "max_hp": 50, "max_ap": 12,
"ap_delta": 6,"damage": 20, "attack_rating": 0.6, "toughness_rating": 0.2,
"body_parts": { "body_parts": {
"head": 50, "head": 50,
"chest": 50, "chest": 50,

View file

@ -4,11 +4,14 @@
namespace components { namespace components {
int Combat::attack(Combat &target) { int Combat::attack(Combat &target) {
int attack = 1; float hit_prob = Random::uniform_real(0.0f, 1.0f);
int my_dmg = 0; int my_dmg = 0;
my_dmg = Random::uniform<int>(1, damage); 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); target.take_damage(my_dmg);
}
return my_dmg; return my_dmg;
} }

View file

@ -45,5 +45,6 @@ namespace game {
TRAP=__LINE__, TRAP=__LINE__,
UPDATE_SPRITE=__LINE__, UPDATE_SPRITE=__LINE__,
USE_ITEM=__LINE__, USE_ITEM=__LINE__,
LEVEL_UP=__LINE__,
}; };
} }

View file

@ -30,6 +30,7 @@ namespace components {
components::enroll<Sprite>(MAP); components::enroll<Sprite>(MAP);
components::enroll<Sound>(MAP); components::enroll<Sound>(MAP);
components::enroll<Collision>(MAP); components::enroll<Collision>(MAP);
components::enroll<LearnByDoing>(MAP);
MAP_configured = true; MAP_configured = true;
} }
} }

View file

@ -108,11 +108,21 @@ namespace components {
int hp = 10; int hp = 10;
}; };
struct LearnByDoing {
int attacks=0;
int hits=0;
int next_attack_level=10;
int next_toughness_level=10;
};
struct Combat { struct Combat {
int max_hp=1; int max_hp=1;
int ap_delta=1; int ap_delta=1;
int max_ap=1; int max_ap=1;
int damage=1; int damage=1;
float attack_rating=0.01;
float toughness_rating=0.01;
std::unordered_map<std::string, int> body_parts{ std::unordered_map<std::string, int> body_parts{
{"head", 10}, {"head", 10},
{"chest", 10}, {"chest", 10},
@ -177,11 +187,12 @@ 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, max_hp, ap_delta, max_ap, damage, body_parts); ENROLL_COMPONENT(Combat, max_hp, ap_delta, max_ap, damage, attack_rating, toughness_rating, body_parts);
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);
ENROLL_COMPONENT(Collision, has); ENROLL_COMPONENT(Collision, has);
ENROLL_COMPONENT(LearnByDoing, attacks, hits, next_attack_level, next_toughness_level);
template<typename COMPONENT> COMPONENT convert(nlohmann::json &data) { template<typename COMPONENT> COMPONENT convert(nlohmann::json &data) {
COMPONENT result; COMPONENT result;

View file

@ -270,6 +270,7 @@ void System::combat(int attack_id) {
// player shouldn't hit theirself // player shouldn't hit theirself
if(host_state != BattleHostState::not_host) continue; if(host_state != BattleHostState::not_host) continue;
// BUG: really this should be attacker and receiver so I don't need the above line
components::CombatResult result { components::CombatResult result {
.attacker=enemy.entity, .attacker=enemy.entity,
.host_state=host_state, .host_state=host_state,
@ -286,10 +287,38 @@ void System::combat(int attack_id) {
animation::animate_entity(world, enemy.entity); animation::animate_entity(world, enemy.entity);
} }
System::learn_by_doing(result);
world.send<game::Event>(game::Event::COMBAT, enemy.entity, result); world.send<game::Event>(game::Event::COMBAT, enemy.entity, result);
} }
} }
void System::learn_by_doing(components::CombatResult& result) {
auto& level = GameDB::current_level();
auto& world = *level.world;
auto& combat = world.get<components::Combat>(level.player);
auto& lbd = world.get<components::LearnByDoing>(level.player);
if(result.player_did > 0) {
lbd.attacks++;
}
if(result.enemy_did > 0) {
lbd.hits++;
}
if(lbd.attacks > lbd.next_attack_level) {
// they leveled up
combat.attack_rating += 0.01;
lbd.next_attack_level = lbd.next_attack_level * 4;
}
if(lbd.hits > lbd.next_toughness_level) {
// they leveled up
combat.toughness_rating += 0.01;
lbd.next_toughness_level = lbd.next_toughness_level * 4;
}
}
void System::collision() { void System::collision() {
auto& level = GameDB::current_level(); auto& level = GameDB::current_level();

View file

@ -31,6 +31,7 @@ namespace System {
void enemy_ai(); void enemy_ai();
void combat(int attack_id); void combat(int attack_id);
void learn_by_doing(components::CombatResult& result);
std::shared_ptr<sf::Shader> sprite_effect(Entity entity); std::shared_ptr<sf::Shader> sprite_effect(Entity entity);
void distribute_loot(Position target_pos); void distribute_loot(Position target_pos);

View file

@ -209,7 +209,7 @@ void WorldBuilder::configure_starting_items(DinkyECS::World &world) {
world.make_constant(healing); world.make_constant(healing);
auto sword = System::spawn_item(world, "SWORD_1"); auto sword = System::spawn_item(world, "SWORD_1");
inventory.add("hand_main", sword); inventory.add("weapon", sword);
world.make_constant(sword); world.make_constant(sword);
} }

View file

@ -21,16 +21,26 @@ namespace gui {
"[right_arm]" "[right_arm]"
"[left_arm]" "[left_arm]"
"[right_leg]" "[right_leg]"
"[left_leg]"); "[left_leg]"
"[=*%(200,100)attack|_|attack_rating]"
"[=*%(200,100)toughness|_|toughness_rating]");
$gui.set<Background>($gui.MAIN, {$gui.$parser, }); $gui.set<Background>($gui.MAIN, {$gui.$parser, });
for(auto& [name, cell] : $gui.cells()) { for(auto& [name, cell] : $gui.cells()) {
auto gui_id = $gui.entity(name); auto gui_id = $gui.entity(name);
if(name.starts_with("attack")) {
$gui.set<Rectangle>(gui_id, {});
$gui.set<Text>(gui_id, {L"Attack"});
} else if(name.starts_with("toughness")) {
$gui.set<Rectangle>(gui_id, {});
$gui.set<Text>(gui_id, {L"Toughness"});
} else {
$gui.set<Text>(gui_id, {guecs::to_wstring(name)}); $gui.set<Text>(gui_id, {guecs::to_wstring(name)});
$gui.set<Meter>(gui_id, {1.0f, THEME.DARK_MID, {}}); $gui.set<Meter>(gui_id, {1.0f, THEME.DARK_MID, {}});
} }
}
$gui.init(); $gui.init();
update(); update();
@ -50,10 +60,23 @@ namespace gui {
if(auto meter = $gui.get_if<Meter>(gui_id)) { if(auto meter = $gui.get_if<Meter>(gui_id)) {
meter->percent = float(value) / float(player_combat.max_hp); meter->percent = float(value) / float(player_combat.max_hp);
if(meter->percent < 0.2) {
meter->color = sf::Color(220, 10, 10, 255);
} else {
meter->color = THEME.DARK_MID;
} }
$gui.show_text(key, fmt::format(L"{}: {}", guecs::to_wstring(key), value)); // BUG: gross, make it stop
meter->bar.shape->setFillColor(meter->color);
} }
$gui.show_text(key, fmt::format(L"{}", guecs::to_wstring(key), value));
}
// update learn by doing
$gui.show_text("attack_rating", fmt::format(L"{}", int(player_combat.attack_rating * 100.0f)));
$gui.show_text("toughness_rating", fmt::format(L"{}", int(player_combat.toughness_rating * 100.0f)));
} }
void BodyUI::render(sf::RenderWindow &window) { void BodyUI::render(sf::RenderWindow &window) {

View file

@ -66,11 +66,11 @@ namespace gui {
void FSM::INTRO(Event ev) { void FSM::INTRO(Event ev) {
dbc::check($story != nullptr, "you forgot the stroy"); dbc::check($story != nullptr, "you forgot the stroy");
if($story->playing()) {
if(ev == game::Event::MOUSE_CLICK) { if(ev == game::Event::MOUSE_CLICK) {
$story->stop(); $story->stop();
} }
} else {
if(!$story->playing()) {
$story = nullptr; $story = nullptr;
state(State::IDLE); state(State::IDLE);
} }

View file

@ -16,14 +16,14 @@ namespace gui {
{ {
$gui.position(x, y, width, height); $gui.position(x, y, width, height);
$gui.layout( $gui.layout(
"[*%(100, 300)body_ui]" "[*%(100, 400)body_ui]"
"[_]"
"[_]" "[_]"
"[_]" "[_]"
"[=inv0|=inv1|=inv2]" "[=inv0|=inv1|=inv2]"
"[=inv3|=inv4|=inv5]" "[=inv3|=inv4|=inv5]"
"[=inv6|=inv7|=inv8]" "[=inv6|=inv7|=inv8]"
"[=hand_main]" "[=weapon]");
"[=hand_off]");
} }
void StatusUI::init() { void StatusUI::init() {