Bot harness hardening: XP, dodge, spawns, attack pace, quests, GM server commands - #3
Open
barrelin-oss wants to merge 63 commits into
Open
Bot harness hardening: XP, dodge, spawns, attack pace, quests, GM server commands#3barrelin-oss wants to merge 63 commits into
barrelin-oss wants to merge 63 commits into
Conversation
Adds a dedicated experience_update server->client message sent whenever a player gains XP (solo/party NPC kill XP, crusade rewards, login reward delivery). Carries experience_gained, new total experience and level; on level-up also levels_gained, new max_hp/max_mp/max_sp and unspent stat_points. Implemented as an experience_gain callback on player_system (fired from add_experience only when XP actually changes; silent at max level), wired in game_handlers so every add_experience call site is covered. Also fixes pre-existing MSVC build breaks: missing NOMINMAX include order in three bridge translation units and unguarded POSIX time functions (timegm/gmtime_r/localtime_r) in auth_system and a crusade test. Documented in docs/protocol/player.md, docs/JSON_PROTOCOL.md and docs/protocol/items.md; PROGRESS.md updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y, headless bot client - JSON party messages (invite/accept/leave/update) + handlers and docs - Fix loot gold credited to ECS entity id instead of player_id - npc_registry YAML: parse gold_min/gold_max; npcs.yaml exp/gold keys fixed - Perf: O(1) player lookup in find_aggro_target; sees_all counter gates the far-admin scan in get_players_who_can_see - Case-insensitive .amd loading (ARESDEN/ELVINE were silently skipped), lowercase map names; characters start in their nation town - Registration rate limit configurable (auth.max_registration_attempts) - mapdata configs for default/aresden/elvine (spawners, merchants, initial points validated by tools/bot/scan-map.mjs) - tools/bot: headless bot client (hunt/loot/shop/party), gen-bots, scanner - posix_time_compat shim (pre-existing local build fix, included as-is) WIP checkpoint requested by worktree session relaxed-bell-f49dbe for merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The modernized magic_type enum was missing six values used by the HBX Magic.cfg data (14, 19, 21, 25, 26, 27), so 15 spells were skipped at load as invalid magic_type - including all high-circle attack magic (Blizzard, Meteor-Strike, Lightning-Strike, Bloody-Shock-Wave). - Add create_dynamic(14), damage_linear(19), damage_area_no_center(21), damage_area_sp_down(25), armor_break(26), ice_linear(27) to the enum - Implement line targeting (find_line_targets): Bresenham trace from caster toward target up to 12 tiles, faction and safe-zone filtered - damage_area_sp_down drains SP from player targets (effect2 dice, parsed into new sp_drain field) - armor_break deals pure damage (ignores_defense now honored via deal_pure_damage) - Cancellation becomes an offensive dispel (debuff category, removes the target effects) instead of hitting the utility stub - Fix 4 placeholder rows in magic.yaml copied from Lightning-Strike in the original cfg: Cancellation type 28, Resurrection 32, Illusion-Movement and Mass-Illusion-Movement 16 - create_dynamic spells (Spike-Field, Ice-Storm, Cloud-Kill) stay skipped with an info log until the dynamic ground-object subsystem exists (tracked in PROGRESS.md) - New registry test covering the legacy types; 2522 tests pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
loot_tables.yaml and shops.yaml were authored against a different item numbering than items.yaml - 22 referenced IDs did not exist (375+ validation warnings per boot) and several existing IDs pointed at the wrong item entirely (bosses dropping Tomato/Hoe/Garlic/Carrot as placeholders for Ice/Merien gear; potion shops selling a MagicNecklace; blacksmiths selling Dagger variants labeled ShortSword/MainGauche). Audited every reference against items.yaml using the intent recorded in the line comments: - Remapped 16 references whose intended item exists under another ID (BlackShadowSword 926, The_Devastator 923, BarbarianHammer 928, KlonessAxe 929, StormBringer 924, GiantSword 46, Flameberge+1 55, MagicWand(MS20) 256, KnecklaceOfStoneGolem 647, SapphireRing 336 ...) - Removed 32 pool entries whose intended item does not exist in this item set (AncientTablets, CritCandy, SSS/E.S.W/I.M.C manuals, XelimaCap/Hat/Helm, NecklaceOfXelima, DragonWand MS40, HolyBlade, GiantBattleHammer) plus the vegetable placeholders - Rewrote ~50 stale comments to the real item names (no behavior change) - shops.yaml: ShopKeeper-E/W sell RedPotion/BluePotion/GreenPotion (91/93/95); Gandlf/William sell Dagger/ShortSword/MainGauche (1/8/12) All references now resolve; no in-pool duplicates; 2522 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entity ids from the shared entity_manager are not player ids; casting one
to the other made get_player() return nullptr (or the wrong player) and
silently skipped guarded blocks. Standard fix: get_player_by_entity().
- magic_system: 21 lookup sites (mana/HP/SP costs, silenced/level/stat
checks, range and safe-zone checks, damage/heal scaling, SP drain,
resurrection, debuff resist); AOE/line target finders now push the real
p.ecs_entity instead of fabricating entity{player_id.value, 0}
- application: periodic heal/mana_drain/mana_restore effect ticks never
applied to players
- game_handlers_combat: spell-cast broadcast target resolution; respawn
invulnerability was keyed on a fabricated entity and never matched
- player_system::remove_player: effect cleanup used entity{id.value},
leaking active effects on logout
- wave4 legacy handlers: attack/cast fabricated caster entities from
player_id, so spell knowledge/cooldowns could never match the JSON path
- tests: fixtures now resolve the real ecs_entity (old fabricated form
only passed because fixtures spawn no NPCs, keeping counters aligned)
Legitimate uses kept: admin API player_id from the wire, inventory/trade
entity_id{pid.value} keying (consistent), spatial entity_id{index}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ng spells
- magic_system.cpp: 20x get_player(player_id{entity.id}) -> get_player_by_entity;
mana cost, range checks, INT scaling and player-target effects were silently
skipped (ECS entity ids are not player ids)
- auth_handlers: grant qualifying spells on first login (magic_data '[]')
- tools/bot: mage role (Magic-Missile at range, self-Heal, mana potions),
combat-aware potion use, resting state that waits for natural regen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tick_ms + hp/mp/sp_interval_ms wired through server_config into player_system_config (set_config was never called before — defaults only). Amounts keep the legacy formulas; intervals control how fast a full roll lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New world::dynamic_object_system managing temporary tile objects,
modernized from the legacy CDynamicObject system. The three create_
dynamic (type 14) spells now load and cast:
- Spike-Field: 25 spike traps in a 5x5 area; 2d4 physical damage when
an entity steps on a trap tile (movement-triggered, owner-immune)
- Ice-Storm: single field ticking 3d3+5 ice damage in 5x5 every ~1s,
applies freeze (20s) to players
- Cloud-Kill: single cloud ticking 1d8 poison damage in 3x3 (power 40
from cfg), applies poison DoT to players
Field data is parsed from magic.yaml effect3 {object type, rx, ry}
(legacy Magic.cfg effect10/11/12 columns). Spawn validation: walkable
tile, not a safe zone, one object per tile; area ticks skip players in
safe zones. Damage is attributed to the caster so kill credit flows
through the normal combat/death pipeline.
Protocol: new dynamic_object_spawn / dynamic_object_removed broadcasts
to visible players, plus re-send of visible fields on enter_game and
teleport. Documented in docs/protocol/combat.md and JSON_PROTOCOL.md.
Not ported (documented in PROGRESS.md): weather shortening fire
duration, fire-ice mutual duration reduction, coal fire spreading,
NPC-move spike triggers.
8 new tests; registry test updated for type-14 loading; 2530 tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the magic type cycle: sp_down_area(5), sp_up_area(7), create(10), possession(15) and tremor(22) join the enum, removing the last invalid magic_type skips at boot (66/66 spells loadable). - Staminar-Drain: direct area SP drain (effect1 average); the sp_down_spot single-target variant now drains SP too instead of applying an empty effect. Celebrating-Light (zero dice) is a harmless visual cast. - Staminar-Recovery / Great-Staminar-Recov.: area ally SP restore; healing path branches on sp_up_spot/sp_up_area to restore SP instead of HP (sp_up_spot previously healed HP by mistake) - Create-Food: drops a random basic food (Baguette/Meat/Fish) at the caster via item_ops::drop_loot + ground item broadcast - Tremor: area earthquake damage 3d4+3 (legacy knockback not ported, documented) - Possession: accepted as no-op (legacy ground-item ownership does not exist in the modern server) - Fixed two more entity-id-as-player-id lookups found while wiring: on_spell_cast caster resolution (game_handlers_combat.cpp) and the healing apply_heal pid (magic_system.cpp) New registry test covering the five types; 2531 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
recipes.yaml and fishing.yaml referenced potions from a different item set (same class as the loot/shop audit). Remapped by function, results are name-referenced: HealthPotion -> RedPotion, ManaPotion -> BluePotion, RevitalizingPotion -> GreenPotion (and Big variants); fishing rare catch SuperPowerGreenPotion -> SuperGreenPotion (391). Eliminates the last 7 registry warnings at startup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Overlap with helbreathx-52 (20 magic_system lookup sites, on_spell_cast caster, healing apply_heal pid) resolved in favor of master. Remaining delta: AOE/line target fabrication, damage/heal scaling lookups, SP drain, debuff resists, application.cpp effect ticks, respawn invulnerability, logout effect cleanup, wave4 legacy handlers, test fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hunger_state::consume used std::min<int8_t>(100, level + amount), which converts the sum to int8_t BEFORE comparing. Eating near full wrapped around (100 + 30 -> -126), so is_starving() became true and update_regeneration skipped the entire tick - HP, MP and SP froze. Clamp in int instead, in both consume and decay. The bug was unreachable until now because no shop sold food: ShopKeeper-W and ShopKeeper-E only stocked potions, so nothing in the game could restore hunger. Add Meat (item 99) to both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 50-bot run froze after ~13 minutes: bots stayed alive and walked
around with mobs in sight but stopped fighting entirely. Three causes,
each masking the next.
Hunger: characters persist hunger_level across sessions and bots never
ate, so they logged in starving with regeneration fully blocked
server-side. Track hunger from enter_game and hunger_update, buy food
with top priority, and eat via autoEat(). A starving bot with no food
now goes shopping *before* the flee branch - fleeing recovers nothing
while regen is off - and walks toward its nation's town when no
merchant is visible, instead of freezing in place.
MP: the server sends mp/max_mp inside stat_update (send_vitals_update),
not the mp_update the bot was listening for. Mage MP only ever moved on
a successful cast, so mages sat at "MP 0" forever and rested for a
threshold that could never be reached. Read mp/max_mp from stat_update.
Purchases never worked: the shop catalog exposes item_id but
shop_buy_request requires item_template_id, so every buy silently
failed - which is why every bot ran with "pots 0" and no weapon.
Normalize the catalog and log failed purchases so this cannot hide
again. inventory_item_update (v1) also sends the item flattened while
add/v2 wrap it in {item:{...}}; the handler threw and bought items
vanished. Accept both shapes.
Also: cap resting at 60s so a stalled regen can never freeze a bot
again, and buy a weapon (50 gold) before potions - a weaponless bot
deals ~1 damage and never earns its way out.
Verified over a 17-minute run: 544 kills at a steady 25-45/min with no
decay, 0 deaths, 0 protocol errors, no hunger value outside 0-100.
Previously the run peaked at 35/min and flatlined to 0 by minute 13.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
handle_shop_buy (the v1 path) created the item with create_from_template
and added it to the inventory, but never called set_owner. The owner
stayed entity_id{}, and item_ops::equip_item rejects any item whose
owner != player with "Item not owned by player" - so every weapon bought
through this path was impossible to equip. The v2 path already got this
right via item_ops::shop_buy.
handle_player_equip also discarded result.error and replied with a bare
false ack, which made a rejected equip indistinguishable from a dropped
packet. Log the reason. That log is what surfaced the second cause here:
bots were retrying a 0-durability weapon every 5 seconds forever, with
the server answering "Item is broken" into the void.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The legend at the top of shops.yaml described a category scheme that no item in items.yaml ever used (20=accessory_ring, 30=consumable_potion, ...), while items carry the legacy numbering (46=accessories, 21=potions, 5=shields, 6=body armor, 31=food). Every buy_categories list was matching against numbers that do not exist, so the general shopkeepers refused to buy back potions and had no buyer at all for accessories - the most common and most valuable loot. Rewrite the legend from the actual distribution in items.yaml and fix the four shops. Category 42 is deliberately left out of every list: it holds Gold itself alongside guild tickets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bots ended every long run broke (avg 31 gold), unarmed, and hitting for ~1 damage. sellableJunk() only ever returned spare weapons, and only from the second one on, so 20-30 looted items sat in inventory as dead weight. It now returns everything not equipped and not reserved (best spare weapon when nothing is equipped, potions and food up to the buy limits), routes each item to a merchant that actually buys its type, and runs before any purchase while the bot is broke - otherwise a bot with 21 gold and 15 items spent the 21 on food and never liquidated anything. Two protocol mismatches kept bought items invisible to the bot: - inventory_item_msg (what inventory_item_update carries, and that is how every shop purchase arrives) uses item_type/equip_pos as legacy numbers and carries no price or damage, while serialize_item (inventory_data, item_add) uses type/equip_pos strings plus price and damage. A bought weapon never satisfied `it.type === "weapon"`, so the bot believed it was still unarmed and rebought one every shop cooldown. mergeItem() now folds both shapes together and keeps the richer fields. - equipment_change, the authoritative confirmation, arrived 83 times per run and fell through to the unknown-broadcast default, so this.equipment was never updated from it. Also: skip weapons at 0 durability (the server rejects them as broken and the bot retried the same one every 5s forever), let essential needs override the "no shopping with mobs nearby" guard and walk to town when no merchant is in sight, and buy a weapon before potions. Measured over a mature 50-bot run: avg gold 31 -> 2358 for warriors, 53231 gold raised across 45 sales, 48-53 kills/min sustained, 0 protocol errors. Weapon adoption is better but still incomplete - 12 warriors hold enough gold and never trigger a blacksmith trip; that is unsolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gandlf and William sell Dagger, ShortSword and MainGauche for 50 gold each, but the Dagger requires level 10 and the other two require nothing. The shop catalog only carried name, price and base_price, so a client picking "the cheapest affordable weapon" always landed on the Dagger and then could not equip it - with no way to know why. Send level_limit and category with each catalog entry. handle_player_equip had six early returns that replied with a bare false ack and no log, which is what made this take so long to find: a rejected equip looked exactly like a dropped packet. Log each reason. The one that cracked it was "stat/level requirements not met". Same treatment for the post-purchase inventory_item_update in handle_shop_buy: if the inventory, the entry, or the message could not be built, the client silently never learned it owned the item. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Warriors were sitting on thousands of gold without a weapon, hitting for ~1 damage. Two independent causes. They bought weapons they could not use. All three blacksmith weapons cost 50 gold, so "cheapest affordable" always chose the Dagger, which requires level 10 - most bots are level 1-8. The bot then retried equipping it every 5 seconds forever while the server answered "stat/level requirements not met" into the void, and rebought another one each shop cooldown. Filter catalog candidates by level_limit (now sent by the server), and mirror the server's check_requirements in canUse() so unusable looted weapons stop counting as owned weapons - they become sellable junk and stop blocking the purchase of one that fits. Character stats come from enter_game. equipBestWeapon() also sat after restingTick(), which returns early. A mage at 0 MP rests almost continuously and never reached the line, and neither did anyone recovering. Equipping costs nothing and matters most to whoever is fleeing, so it now runs before the rest and flee checks. Warriors carrying a usable weapon went from 18/30 to 23/30 in a mature 50-bot run, with 8 successful equips and no rejections after the fix. Kill rate held at 52-54/min. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fleeing stayed true until HP reached recoverHpThreshold. With regen stalled - starving, no potions, gold at zero - that threshold never arrives and the bot circles forever, the same deadlock restingTick had. Cap the flee at 45s with a 20s cooldown, gated by shouldFlee() so low HP cannot immediately re-trigger it. The cooldown also blocks resting: the first version passed its test but was useless in practice, because a bot leaving the flee at 12/50 HP was grabbed by restingTick in the very same tick and parked again 200ms later. Blocking both makes the window usable - the bot now targets a mob 200ms after the cap instead. Verified twice: forced with a 3s cap (fired at 3.07s, re-fled after 8.15s of cooldown) and in production at 45s (fired at 45.2s, then went straight back to hunting). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
player_system::add_stat_point already had the right stat indices (0=str 1=dex 2=vit 3=int 4=mag 5=cha) but nothing ever called it: no protocol message, no handler, no caller anywhere. It was dead code, and the 3 points awarded per level were permanently stuck. Add stat_point_request/stat_point_response with range validation, wire the handler, and send a stat_update after so the client sees the derived stats change. Also send stat_points in the enter_game character payload. The count lived only in the persistence struct, so a client learned its unspent points solely from the next level-up's experience_update - characters logged in blind to a backlog they already had. One bot turned out to be sitting on 20 unspent points. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Warriors raise INT only until Protection-From-Magic becomes reachable (int_req 32 in magic.yaml) and never touch MAG; past that DEX is the priority and STR follows in second. Mages keep pushing INT, with MAG trailing to cover spell costs. Two caveats worth stating. STR here is a stand-in: the spec calls for "enough STR for max weapon speed", but weapons carry no str_req in this server and the attack rate is a flat 100ms, so there is no threshold to aim at - it is encoded as a ratio (STR about half of DEX) and should become a real threshold once weapon speed exists. And reaching INT 32 does not by itself grant the spell: spells are only granted at first login from INT/MAG, still marked TODO in auth_handlers. Verified both branches. The INT branch ran on its own (113 allocations, no failures); the DEX/STR branch was forced by temporarily lowering the target to 12, which warriors had already passed - DEX climbed 13 to 17 while STR held at 14 - then the target was restored to 32. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Spells were only ever granted at first login, from the character's INT and MAG, with a TODO in auth_handlers saying to replace it with a real flow. That made stat allocation pointless for magic: a warrior born at INT 14 could raise INT forever and still never gain a spell. Add learn_spell_request/learn_spell_response. The handler reuses validate_npc_interaction for range, then checks that this NPC teaches the spell, that the player meets int_req/mag_req, has the gold, and does not already know it. On success it deducts, learns, and returns the updated gold and spell list. The gold price was already in magic.yaml as "cost" but nothing read it - only mana_cost was parsed. Carry it through as gold_cost in both spell structs and the registry-to-runtime conversion, and expose the spell catalogue (int_req, mag_req, cost, known) in the shop interact response so a client can decide before asking. The catalogue hangs off the ShopKeepers rather than a dedicated teacher: npc_category is derived from npc_type and every NPC becomes a merchant, so there is no way to mark a trainer in config today. Moving it later is just config - add the NPC, spawn it, move the spells: block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Warriors go after Create-Food and Protection-From-Magic, mages after the full set. A reachable spell - INT and gold both sufficient - becomes a reason to visit the shop, since that is the only way the allocated INT turns into anything useful. This closes the loop end to end for the first time: level up, three stat points, INT rises, crosses int_req, the bot walks to the teacher, pays, and learns. Verified with warriors born at INT 14 who crossed 18 and bought Create-Food - impossible before, when spells were frozen at whatever first login granted. A mature run produced 5 Create-Food, 1 Invisibility and 1 Protection-From-Magic, the last being the warrior target the INT policy aims at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The items table has had a name column all along, defaulting to '', but the INSERT never listed it and item_row had no such field. Every item was stored nameless and the name was reconstructed from template_id on load. The game did not care, but it left the table opaque: answering "what are the bots carrying" meant joining template_id against items.yaml by hand. Carry the name through item_row and write it, for the bank rows as well as the inventory ones - that path was missing it too. Verified after a full autosave: 330 of 330 rows now carry a name matching their template_id. The template stays the source of truth; the stored name is for inspection, noted in the field comment so nobody later treats the column as authoritative and drifts from a renamed template. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
npcs.yaml is generated by tools/convert from the legacy .cfg format and names two fields defense_ratio and size. The registry read "defense" and "body_size" - names that appear zero times in the file - so both were always 0 across all 85 NPCs. The obvious fix, renaming defense_ratio to defense, would have been wrong. In the legacy format defense_ratio is not damage absorption: it is the denominator of the hit chance, calc_hit_chance(hit_rate, dodge_rate). npc.defense in this codebase is a damage-reduction percentage capped at 80, so feeding it values of 10-450 would have made every mob absorb 50-80% of incoming damage. It belongs in dodge_rate. Two links in that chain were broken: the registry never read the value, and npc_system never copied dodge_rate from the template to the spawned NPC. With dodge_rate at 0, calc_hit_chance did max(1, 0) and every attack in the game - players and mobs alike - landed at the 99% cap. npc.defense stays 0 on purpose: the legacy format has no absorption field, so there is nothing to map onto it. Hit chance now varies by mob as intended - roughly 87% against a Slime (dodge 20), 58% against a Giant-Ant (30) and 23% against an Orc (75) at current bot stats. Kill rate held at 21-49/min with no errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
attack_result_msg carried a single hit flag, and hit=false was used for both a swing that missed and roughly a dozen rejections - out of range, target dead, attacking too fast, not in combat mode. A client could not tell them apart, so measuring an actual hit rate was impossible; the dodge_rate figures in the previous commit were arithmetic, not observation. Add resolved (the server actually rolled the attack) and dodged (the target evaded, as opposed to the attacker missing), and set them on the one path that resolves combat. Measured against the computed expectations right after: Slime 86% vs ~87% predicted, Giant-Ant 58% vs ~58%, Orc 25% vs ~23%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Only hits were logged, so a miss was indistinguishable from a swing that never happened. Count resolved swings and hits, log the miss with whether it was a dodge, and carry the running rate in the status line. Confirms the dodge_rate wiring with observation rather than arithmetic: Slime 86%, Giant-Ant 58%, Orc 25%, against predictions of 87/58/23. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The single respawn timer was reset by every death and never fired under 50 bots, so spawners stopped repopulating. A serial queue (one respawn per respawn_time_ms) fixed that but capped each spawner at 12/min, which became the kill-rate ceiling of the bot runs. Each death now carries its own due time, as the legacy spot-mob-generator did, and respawn_time_ms is read from mapdata (spot_mob_generator.respawn_time_ms, default 60 s). The test maps also grade the field: Slimes everywhere, Giant-Ants and Orcs only on the side away from the initial points, because 20 Orcs on the spawn tile killed level-1 characters in about 12 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
autoPotion() fired a use_item_request every 200 ms tick before the server had answered with the new HP: 481 of 3159 potion uses in the last run were repeats of the same HP, and the buy-drink-buy loop replaced hunting (1374 kills/h down to 11). potionReady() now requires potionCooldownMs and a changed value (or potionSettleMs elapsed), with separate timers for red and blue potions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The server only refused swings closer than 100 ms; Item.cfg field 19
("Speed", 0..15) was loaded but never used. combat/attack_timing.h now
paces every swing: base_ms + speed * speed_step_ms, plus str_penalty_ms
per STR point below the weapon str_speed_req (derived as
speed * str_per_speed when the item declares none), clamped to
[min_ms, max_ms]. str_speed_req never blocks equipping; that stays with
str_requirement.
Every player_attack_response carries attack_interval_ms, refused swings
included, so clients pace themselves without knowing the formula. The
knobs live in the new attack_speed section of server.yaml. Bots adopt the
reported interval and no longer count attack_too_fast as a swing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gm_command_context gains config, broadcast_all and request_shutdown hooks injected from application.cpp, so the admin module still does not depend on application.h. /reloadconfig re-reads server.yaml from the boot path and reports which sections apply live; /shutdown [seconds] [reason] broadcasts warnings at 5 min, 60 s, 30 s and 10 s on the shutdown_countdown scheduler tag shared with the admin web API, /shutdown cancel aborts, and /shutdown alone stops at once. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Recent Changes for the attack pace, party XP fix, potion cooldown, spawn rescheduling, city hall quests and GM server commands. Quest NPCs and server management are marked done. Object interaction (doors, chests) is dropped: the original game has neither, only the Heldenian gate doors that the war system already owns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Once a bot hit its carry limit the server put the item back ("Too heavy
to carry", pickup_result success=false) but the bot ignored that reply,
deleted the ground item optimistically, saw it re-broadcast and tried
again every 200 ms tick without ever fighting again. With respawn
working, drops piled up and bots froze one by one: run 3 went from 281
to 42 kills per 5 minutes with bots at full HP next to 84 mobs.
Failed pickups now blacklist that ground item for lootSkipMs and two in
a row switch the bot to gold-only looting for overweightMs. The bot also
tracks inventory_weight_update: above lootWeightCap of max weight only
gold is picked up, above sellWeightRatio it goes to sell whatever junk it
has, however little.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Logs in with an admin account, runs /reloadconfig (and the /reload alias), schedules /shutdown 20, waits for the 20 s and 10 s system-chat warnings, cancels, checks the usage errors, then issues /shutdown 0 and verifies the connection is closed by a clean server exit. 16 checks; --no-final-shutdown skips the last step. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hunger_update, skill_update and skill_progress shared one case that did this.hunger = d.level, so every skill progress message (level 5, 8...) became "hunger 5". The bot then believed it was starving, ate constantly and spent its gold on food: run 4 had 187 Meat purchases against 5 swords and a third of the warriors unarmed with ~20 gold, while the server-side hunger drains 1 point per minute. HANDOFF items 6.2 and 6.3 trace back to this. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…reachable targets Status lines carry the aiTick branch in effect (acao=...), stepTowards logs when it gives up after 9 refused moves, and a shop trip that does not reach the merchant in shopTripMaxMs is abandoned and logged. That showed why kill rates decayed: bots stood for minutes against blocked_terrain (mobs that wandered outside the walkable area) and blocked_occupied (crowds around the merchant tiles) without a log line. stepTowards now tries the neighbouring directions when the direct step is refused and keeps a successful detour for detourSteps steps instead of turning back into the same obstacle; a target abandoned as unreachable is skipped for avoidTargetMs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ix::WebSocketServer was built with port and host only, so it kept the library defaults: maxConnections = 128 and a TCP backlog of 5. websocket.max_connections in server.yaml never reached it, and the 129th client was dropped during the handshake (close 1006) with nothing in the server log. Bots 129-200 of the scale test all died that way. The server now passes the configured limit and a backlog of 256 and logs both at startup; bots reconnect after reconnectMs when the socket closes outside a shutdown. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
application.cpp turned a mapdata spawner rect into center + radius = max(w, h) / 2, and spawn_point::get_spawn_position sampled a square of that radius: a 50x70 rect spawned NPCs up to 10 tiles past its narrow sides, behind walls. Bots piled up at x=195-199 in Aresden trying to reach mobs born there; with 200 bots that alone took the kill rate from 686 to 43 per 5 minutes. spawn_point now carries radius_x/radius_y and samples inside the rect (the square radius still applies when unset). spawn_npc_at set the NPC home to the rect center, so with wander_range 5 an NPC born near the edge of a 70-tile generator never wandered again. Home is now the tile it spawned on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With every spawn now inside the generator rect the test arena got denser, and warriors stood in pockets of 3-4 mobs drinking a potion every 7 s until they died: 470 potions and 14 deaths in 25 minutes of run 9. shouldFlee() now also triggers below swarmFleeHp (60% HP) when swarmFleeCount (3) monsters are adjacent, reusing the existing flee/recover flow. Run 10 on the same arena: 1142 kills in 15 minutes, 2 potions, 0 deaths. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NPCs born near the edge of a spot-mob generator drifted wander_range tiles past it, into pockets behind walls where nobody could reach them. They never died there, so the reachable population of the area shrank over every run and bots ended up chasing mobs through walls: run 10 went from 492 to 80 kills per 5 minutes in 20 minutes with zero deaths. ai_runtime_state carries the generator rect (home_min/home_max, set in spawn_npc_at from the spawn point half extents) and process_wander_state refuses steps outside it. Chasing still leaves the rect and return_home brings the NPC back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A reconnect kept the previous session's entities and ground items, so bots chased ghost mobs (58 "visible" with 60 in the whole map) and died to the real ones: deaths went from 7 to 19 in the 5 minutes after the first server restart under load. resetSession() now clears entities, ground items, target, loot and party state on close; inventory and equipment are resent by the server on enter. With NPCs kept inside their generator rect and roaming properly, the test arenas were too dense for 25 bots per city; Slimes were doing most of the killing. Generators are now 30 Slimes, 20 Giant-Ants and 10 Orcs per city. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…b generator tools/convert/mapdata-legacy.mjs converts the legacy MapData/*.txt (Helbreath 3.82, centuu/HelbreathServer) into bin/mapdata/<map>.yaml: 233 spot-mob generators, 824 teleports, 48 safe zones, initial, fish, mineral and way points, level limits, random_mob_generator with the legacy maximum-object as max_mobs, and the interior maps' fixed NPCs as 1-tile generators. Installed for every map with an .amd; aresden and elvine keep the bot test arenas, their legacy versions sit next to them as *.legacy.yaml. Spawners may name the NPC (npc_name) and the server prefers it over the legacy numeric type, whose mapping only knew 22 types. Boot registers 209 spawn points; 24 are skipped for NPC templates missing from npcs.yaml. npc_system::update_random_mobs is the legacy MobGenerator: nothing called spawn_random_mob() periodically, so the 24 maps that rely on the random generator stayed empty. Each check tops every enabled map up by a small batch of free-roaming mobs of its level, up to min(map max_mobs, random_mob_cap). tools/bot/map-peek.mjs teleports the admin character to a map tile, takes one step and lists the NPCs in view. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The GM teleport does not push player_teleport, so entities seen on the origin map stayed in the list and inflated the destination count. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…onsters Every side-0 template with action_limit 0 was classified as a town NPC, so Rabbit, Cat and Unicorn were is_friendly(): attacks on them were refused and the random mob generator, which only counts is_monster() roamers, never counted them. elvfarm (level 1) reached 305 roaming NPCs against a cap of 150 because 160 were rabbits and cats. They are now monsters with is_aggressive = false; Guard-Neutral (side 0) is a guard. Town NPCs are action_limit 2/6 and unaffected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/teleport, /goto, /summonplayer, the admin web tool teleport and the apocalypse teleports called player_system::execute_teleport directly, which moves the player server-side only: the client got no player_teleport, no destination entities, teleporters, ground items or environment, so a GM kept looking at the old map. They now route through game_handlers::execute_player_teleport (public, returns the error string) via gm_command_context::teleport_player, admin_web_handlers::set_teleport_fn and application::teleport_player_synced. Without a bridge (unit tests) the GM commands fall back to the bare move. tools/bot/hunt-peek.mjs teleports next to the first NPC with a given name in view and sends one attack; map-peek no longer needs to clear the view by hand. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
npcs.yaml gains Giant-Crayfish, Giant-Lizard, McGaffin, Perry, Devlin, Gail, YW-Aresden and Sor-Aresden, mapped column by column from the legacy NPC.cfg (centuu, 3.82); exp scaled to this file's range. Boot registers 221 spawn points, 12 skipped (legacy types 7/8/9 with no name in NPC.cfg). Verified in game on dglv2, druncncity, cmdhall_1 and procella. dialogs.yaml: McGaffin, Perry and Devlin offer open_quests/claim_rewards; Gail offers the crusade job selection. map-peek/hunt-peek heal GmSmoke first and stop when the teleport is refused instead of reporting the origin map. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reading the centuu MapData with the alphabetical 3.51 table put 140 Abaddons in procella, Giant-Crayfish in maze and Sor-Aresden in druncncity. The converter now defaults to the Korean table taken from the generator comments (71 Fire-Wyvern, 72 Barlog, 73 Tentocle, 74 Centaurus, 75 Giant-Lizard, 76 Minotaurs, 80 Giant-Plant, 81 MasterMage-Orc, 82 Nizie; 70 Claw-Turtle is an assumption, marked in the YAML); --intl-types keeps the NPC.cfg numbering. NPC.cfg names are translated to this distribution's spelling (Giant-Cray-Fish, Lizard, Minotaurus, Master-Mage-Orc), so the duplicate Giant-Crayfish and Giant-Lizard templates are gone again. Verified in game on procella, maze and druncncity. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ide alone tools/cerca/cerca.mjs sees every Bash/PowerShell command and every Write/Edit path and exits 2 with a reason for: rewriting published history or pushing to the read-only upstream, merging or releasing through gh, discarding local work (hard reset, checkout --, restore, clean, branch -D), privilege elevation, piping downloads into a shell, package publish, dropping or truncating database objects, stopping PostgreSQL, killing processes that are not the project's, recursive deletes outside the workspace, and writes to credential files, the fence itself and the settings that install it. Idea from GitArika/agents-orchestrator (hooks/cerca.sh): a hook is not overridable by an existing allow rule and sees the whole command line. cerca.test.mjs gives every rule a blocking case and an innocent neighbour. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tools/convert/merge-legacy-items.ts adds to items.yaml the items of a 3.x Item*.cfg that have no equivalent here (same id with the same type/equip_pos/sprite/frame is the same item under another label; long names within two edits are spelling variants): 103 new items from the centuu 3.82 files, legacy ids kept when free, 13 renumbered from 1001. Existing ids never change. merge-legacy-npcs.mjs adds the 27 missing templates (Ice-Golem, the gates, 24 faction war units). spot_mob_mapping.h knows type 28 (Troll), so quests 21/22 load. Bots: a refused request arrives as type error with error_code; the harness mirrors it into data.error and, on dead, asks for a respawn; a character that logs in dead respawns on enter. quest-smoke teleports to the arena city hall before looking for Kennedy instead of trusting the saved position. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
player_interact_response for an NPC dialog wrote each option's action as the raw enum number while dialog_choice_response and docs/protocol/npc.md use names (goto_node, open_quests, ...). npc::dialog_action_name() is now the one place that spells an action, used by both responses. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…up first No NPC ever got the banker/warehouse category (town templates all became merchant), so player_interact_request never opened the bank. Howard and Tom, the warehouse keepers, are warehouse now and a click on them returns the bank contents. /setgold pushes inventory_gold_update to the target, so the client's gold matches without relogging. quest-smoke abandons quests left by another session before measuring: the officer lists active quests regardless of level, and a client that accepted one made the level-1 check fail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
get_characters_response gains "equipment": {slot: {appr, color}}, the
shape of the entity spawn, read from character_equipment + items in one
query per account and resolved through the item registry, so the character
select can draw the figure dressed instead of naked.
npcs.yaml: 19 templates on sprites 100-112 (our numbering; the packs come
from the Helbreath Olympia client): Scarecrow, Ghost, Princess, Bat, the
two officers, Guard-Archer/Axe/Sword per side, the three chests and the
Black-Beholder. The spawners (scarecrows on the farms, bats and ghosts in
dglv2) live in bin/mapdata, which is not versioned.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…chanics docs/olympia-reference.md catalogues the Helbreath Olympia client folder (data files, 36 patch notes, client strings) and describes its mechanics in porting order: specialties, elites, daily quests, achievements, enchanting with shards, talents and rebirth. The quest loader takes giver/giver_map (a named NPC instead of the city hall officer), name/description, a second kill target, gathered items and a gather-only row type. quests.yaml 200-231 are the 33 Olympia quests (31 ported; the two elite-kill ones wait for elites) given by ten persons from persons.json, added to npcs.yaml, dialogs.yaml and placed on their maps. Exp scaled /50, gold /5. Checked live at every person. items.yaml gains description on the 69 items whose Olympia tooltip text applies here. Scarecrows, bats and ghosts get spawners on the farms and dglv2. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, achievements Specialties (monster mastery): kills per monster type unlock the ladder of bonuses of specialties.yaml against that monster (damage, damage taken, hit ratio, drop rate), applied in build_combat_context and the loot roll, saved in characters.specialty_data, announced with specialty_update and a system chat line. Elites: elite_chance on a spawner makes some spawns "Elite " with 5x HP, double dice, 5x exp, 3x gold and 3x drop chances; the wire carries "elite": true; kill objectives take elite/elite2 and the two Olympia quests that needed them are in. Daily quests: period_hours on a row is a cooldown after the turn-in, kept per template in the journal; accept answers on_cooldown and the list hides them until ready. Treasure chests: treasure_chests.yaml puts a bronze/silver/gold chest on a random walkable tile of a listed map every interval; opening it gives the tier's gold and the drops of loot_tables.yaml 109-111; silver announced to the map, gold to everyone. Achievements: achievements.yaml counters (kills, per type, elites, quests, gold, chests, level, specialty levels) unlock points and titles, saved in characters.achievement_data, announced with achievement_unlocked. Tests for the three systems and the loader flags. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The template's description (the Olympia tooltip texts, plus the potions) is copied to the instance and serialized; the client shows it in the inventory tooltip. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- player_attack_request accepts attack_type "super"/3 and the client's
per-weapon codes 20-27. With a charge left the hit is a guaranteed
critical, the charge is spent and the action broadcast says
"super_attack" (nearby clients play the shout). New push
super_attack_update {charges}: after enter_game_response, after each
super attack and on level-up; charges follow legacy level / 10.
- item.weapon was never filled, so every item serialized with
weapon_type "none". Derived now from the template's legacy appearance
value (weapon_type_from_appearance), with a test.
- send_stat_update carries the current hp/mp/sp too: drinking a potion
only sends that message, and the client never saw the heal.
- NPC templates with action_limit 3/4/8 get the stationary AI flag
(never set before). The Olympia Scarecrow is a training dummy now
(action_limit 3): stands still, hits back with nothing.
- Docs: combat.md (attack types, super_attack_update), items-v2.md,
JSON_PROTOCOL.md, PROGRESS.md. Tests for the parsing and the message.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Items carry the template's gender_limit (gender_requirement, serialized as gender_req) and player_equip_request refuses a piece whose gender does not match the character, as legacy did for the (M)/(W) armor sets. Test requirement_check_test.gendered_armor; docs and PROGRESS updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
36 commits from a bot-driven hardening pass: a headless 50-bot harness (
tools/bot) runs against the server as its integration and load test, and nearly everything below was found by it rather than by unit tests.Server fixes
party_member::current_mapwas never set, somembers_in_map()matched nobody anddistribute_npc_kill_expreturned silently. Eligibility now resolves each member through the live player record.dodge_ratewas never wired:defense_ratiofrom the legacy cfg is the hit-chance denominator, not absorption; it stayed 0 so every swing landed at the 99% cap. Measured rates after the fix match the formula (Slime 90%, Giant-Ant 61%, Orc 21%).int8_toverflow (100 + 30became-126), shop-bought items without an owner (unequippable), shopbuy_categoriesnumbering, loot/shop references to items absent from the item set, missing legacy magic types (all 66 spells load), entity-id vs player-id confusion across magic/combat/effects.respawn_time_msis read from mapdata and each death schedules its own respawn, as the legacy spot-mob-generator did. The old single timer was reset by every death and never fired under load.New mechanics
combat/attack_timing.h):base_ms + speed * speed_step_ms, plus a penalty per STR point below the weapon'sstr_speed_req. Item.cfg field 19 ("Speed") was loaded but unused. Everyplayer_attack_responsenow carriesattack_interval_ms; knobs live in the newattack_speedsection ofserver.yaml.quests.yaml(hunt / go-place),quest_list/accept/abandon/complete/journalmessages plus thequest_updatepush (docs/protocol/quest.md), dialog actionsopen_quests/claim_rewardson Kennedy and William, rewards paid from the bridge, kill hook from the NPC death callback.stat_point_request), spells can be learned when INT allows,experience_updateon XP gain, party JSON protocol, dynamic ground-field spells (Spike-Field, Ice-Storm, Cloud-Kill)./reloadconfigand/shutdown [seconds] [reason] | cancelon the same paths as the admin web API.Bot harness
tools/bot/bot.mjs(50 autonomous warriors/mages: hunting, potions, eating, shopping, equipping, selling, parties, stat allocation, spell learning, quests) plusquest-smoke.mjs, an 18-check end-to-end smoke of the quest flow against a running server.Test plan
hgserver_tests: 2551 tests pass.node tools/bot/quest-smoke.mjsagainst a running server (needs an admin account): 18/18.🤖 Generated with Claude Code