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",
"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": {
"head": 200,
"chest": 200,
@ -21,7 +22,8 @@
{"_type": "Collision", "has": true},
{"_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": "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": {
@ -30,7 +32,8 @@
"foreground": "enemies/fg:rat_giant",
"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": {
"head": 50,
"chest": 50,

View file

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

View file

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

View file

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

View file

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

View file

@ -270,6 +270,7 @@ void System::combat(int attack_id) {
// player shouldn't hit theirself
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 {
.attacker=enemy.entity,
.host_state=host_state,
@ -286,10 +287,38 @@ void System::combat(int attack_id) {
animation::animate_entity(world, enemy.entity);
}
System::learn_by_doing(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() {
auto& level = GameDB::current_level();

View file

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

View file

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

View file

@ -21,15 +21,25 @@ namespace gui {
"[right_arm]"
"[left_arm]"
"[right_leg]"
"[left_leg]");
"[left_leg]"
"[=*%(200,100)attack|_|attack_rating]"
"[=*%(200,100)toughness|_|toughness_rating]");
$gui.set<Background>($gui.MAIN, {$gui.$parser, });
for(auto& [name, cell] : $gui.cells()) {
auto gui_id = $gui.entity(name);
$gui.set<Text>(gui_id, {guecs::to_wstring(name)});
$gui.set<Meter>(gui_id, {1.0f, THEME.DARK_MID, {}});
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<Meter>(gui_id, {1.0f, THEME.DARK_MID, {}});
}
}
$gui.init();
@ -50,10 +60,23 @@ namespace gui {
if(auto meter = $gui.get_if<Meter>(gui_id)) {
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;
}
// BUG: gross, make it stop
meter->bar.shape->setFillColor(meter->color);
}
$gui.show_text(key, fmt::format(L"{}: {}", guecs::to_wstring(key), value));
$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) {

View file

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

View file

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