Skip to main content

Heroes (the companion-hero interweave)

Heroes (the companion-hero interweave)

Status: SHIPPED — foundation completed 2026-06-28. Migrated from the old Dark Citadel hero system, which the new game's rewrite dropped (~1,004 authored hero abilities + missions + arena/tower battles

Last updated: September 29, 2026

Dark Tower — Heroes (the companion-hero interweave)

Status: SHIPPED — foundation completed 2026-06-28. Migrated from the old Dark Citadel hero system, which the new game's rewrite dropped (~1,004 authored hero abilities + missions + arena/tower battles were lost; see legacy-feature-migration-audit.md §3.3). This doc is the design home for the hero layer.

The interweave (the core idea)

In the old game one companion was BOTH a travel companion AND a combat hero — the same entity, two views. The new game kept only the travel-companion view (resource/combat perks that auto-fire during story combat). This migration brings the HERO view back onto the same entity rather than a parallel hero type, exactly as the audit recommended:

  • The player's roster is the existing six companion slots (companionOne..companionSix on ). Every recruited companion is also a hero.
  • Hero state rides on the companion (DarkTowerCompanion): heroLevel, heroXp, missionFinish. These are PLAYER state on the owned companion (zero/null on the content catalog row).
  • Hero rarity and derived attack / defense / health / power are COMPUTED, not stored, in — so no companion content row needs a new column.

Rarity (derived, no content change)

A companion's hero rarity is derived from its combat special (DarkTowerCompanionSpecial), bumped one grade if it is clue-gated (the premium roster the player worked to unlock). Grades use the existing DarkTowerItemRarity ladder (GRAY < GREEN < BLUE < PURPLE < ORANGE < EMERALD; the reserved WHITE/INDIGO/DANTRINE/RED endgame grades fall through to the base multiplier).

Special Base grade
ASSASSIN, DRAGONS_BREATH, CRITICAL_BUFFER, HEALER PURPLE
DAMAGE_BUFFER, FIRE/ACID/POISON_ATTACK, ELEMENTAL_HAND_CROSSBOW BLUE
HIT/ARMOR/SKILL/COMBAT_BUFFER, DAZE/DISARM_ATTACK GREEN
none GRAY

Rarity power multipliers (x100): GRAY 100, GREEN 130, BLUE 170, PURPLE 230, ORANGE 300, EMERALD 400.

Stats, leveling, and the curve

  • Stats = base × rarity multiplier × level scale. Level one is the rarity-scaled base; each level past one adds ten percent of that base. Bases: health 100, attack 20, defense 10.
  • Power (for team comparison) = attack×5 + defense×3 + health.
  • Leveling: a hero earns heroXp on missions and (future) in the arena. The threshold to advance is xpForNextLevel = 100 × current level (escalating). Leveling spends the threshold and carries the remainder. Code: / xpForNextLevel.

Hero missions (the idle layer)

A hero can be sent on a timed mission (reusing the same real-time countdown model as research training):

Concern Behavior
Send "send hero N" stamps missionFinish = now + missionMinutes(rarity) (15 min base, scaled by rarity).
Send all "Send all heroes" (also "send heroes", "send my heroes", "launch heroes") on the hero hall and the missions menu sends every idle hero at once and names each one with its return time. "Send heroes" is not a synonym of the Hero missions option, which only reads the mission list ().
Status the missions menu shows each hero idle / away (minutes remaining) / returned.
Collect "collect missions" awards missionGold(rarity, level) (300 gold base, scaled by rarity and level) and missionXp(level) (50 base × level) to every returned hero, then clears the timer.

Missions are the steady hero-xp and gold source that drives leveling. Hermetic-test note: the math is JUnit-pinned (); the live countdown is not asserted against a wall clock.

Player-Feature Review — (2026-07-16)

Three hero-system asks from the 2026-07-10 game-admin review, validated against this doc's design intent:

  • Mission-return notification (ACCEPT, tuned scope; fb 9865). The missions menu already shows each hero idle / away (minutes remaining) / returned on demand, but nothing announces completion. Rather than a new proactive Alexa-push scheduler — which this game consistently DEFERS elsewhere pending retention data (see post-story-loop.md PS5: "an Alexa-reminder / pending-voice nudge... DEFERRED. If retention data justifies it") — the design-aligned fix is a SESSION-LOAD announcement mirroring the existing non-intercepting "You have N unread messages" pattern ( § Pending Messages): on the player's next turn/session, if any hero's missionFinish has passed, say so. No scheduler, no new infra class. SHIPPED (2026-07-17) — see § Hero Missions Return Announcement below.
  • Uniform mission times so heroes return together (DECLINE; fb 9932). missionMinutes(rarity) is deliberately scaled by rarity — the same rarity-scaling instinct used throughout Dark Tower (loot, gold, hero stats). Flattening mission time to a constant would remove the one place rarity affects PACING rather than just power. collect missions already collects every hero whose timer has elapsed in one action regardless of stagger, so the practical friction this ask cites is already minimal.
  • Auto level-up for the hero system (ALREADY-EXISTS; fb 9700). Hero level is a fully DERIVED stat — canLevelUp / heroLevel compute directly off accumulated heroXp, with no player choice or command involved, so hero leveling is already automatic. (The CHARACTER's own upgrade slots — Powers / Stats / Skills / Companions / Gods, see progression.md § Claiming Earned Upgrades — are deliberately player-chosen instead, and that stays unchanged: auto-selecting those would remove the build-identity choice core to the RPG systems.)

Full disposition table + rationale: index.md § Design Decisions Log — .

Hero mission-return announcement (SHIPPED, 2026-07-17)

The session-load announcement described above is live: runs at the top of every Dark Tower turn (, alongside the existing houseKeepingStory call) — mirroring 's non-intercepting pattern exactly: it APPENDS a short voice line and never blocks, replaces, or short-circuits the current card's own text or options.

  • Filter. .unannouncedReturnedHeroes(companions, now) returns every companion whose missionFinish has passed AND whose missionReturnAnnounced flag is still false. Pure and side-effect-free (pinned by JUnit DarkTowerHeroUtilitiesTest#unannouncedReturnedHeroes_filtersCompletedAndUnannounced — completed+unannounced surfaces once, an already-announced completion never re-surfaces, in-progress/idle heroes never surface).
  • Once per completion. The housekeeping wrapper stamps missionReturnAnnounced = true on every returned hero it names, so the SAME completion is never announced twice. (sending a hero on a new mission) and collectMissions (collecting a finished one) both reset the flag to false, so the NEXT completion is announced again.
  • Voice line. One hero: "Your hero X has returned from its mission. Say collect missions to gather its rewards." Multiple: names roll up via (Bella, Max, and 1 other) then "have returned from their missions. Say collect missions to gather their rewards."
  • Test coverage. game_test.scenario_tests id 2851 was authored via the canonical add_scenario_test tool but is quarantined skip_test=true / HARNESS_LIMITATION: assigns the shared Caffeine-cached companion catalog singleton () directly into the test player's slot — the same documented limitation hit ("the scenario harness cannot seed hero progress"). Mutating missionFinish on that shared instance to fake a past completion would leak into every other scenario_test seeding the same companion type in the same suite run. No clock injection exists for either. The JUnit test above is the real guard.

Direct power-name NLU shortcuts (SHIPPED, 2026-07-17; extends fb 553 / )

fb 9864 asked to say a power's name directly instead of "use power" then the name — already proven for SNEAK_ATTACK (backstab/sneak attack). Surveyed all 148 DarkTowerPowerTypes and scoped the batch to every class's own classPowerTrained power (dark_citadel.rule_dark_citadel_common, rule_type='CLASS' — 52 classes, 51 unique powers) plus SECOND_WIND (the only non-class power with real game_process.processed_input_aggregation usage; class-power usage volume was too thin — 5 distinct DARK_TOWER_ACTUALLY_USE_POWER keys logged total — to derive a separate "most-used" list).

  • 15 of the 51 class powers already had a direct-match row (ACID_RAIN, CHAIN_LIGHTNING, DWARVEN_CRUSH, FIRE_STORM, LAY_ON_HANDS, LIFE_DRAIN, MISTY_STEP, MULTI_ATTACK, POISON_CLOUD, PSYCHIC_BLAST, SNEAK_ATTACK, SUMMON_WOLF, WEAKENING_BLOW, WHIRLWIND_ATTACK, WILD_SPELL).
  • HEAL was excluded — bare "heal" already direct-matches DARK_TOWER_REST. DODGE was excluded — bare "dodge" already direct-matches the cross-game BASELINE_COMBAT_DEFEND (BASELINE_COMMANDS process_context). Adding a power-shortcut on either word would shadow an established command.
  • The remaining 40 powers got two new game_process.process_one_shot_values rows each (direct_match=1, action_key='DARK_TOWER_ACTUALLY_USE_POWER', process_context='DARK_TOWER', value_type= the power enum) — the bare display name and "use <name>", apostrophe-free to match 's input sanitizer (replaceAll("[.,:!;?']", "")). Phrasing was taken from the display name in rule_dark_citadel_common (rule_type='POWER'), which for several powers differs sharply from the enum key (GRAND_STRATEGIST_POWER → "Gradiose Dreams", MASTER_STRATEGIST_POWER → "Black Hole", STRATEGIST_POWER → "Icy Grip", VAMPIRE_CONSUME → "Vampiric Consumption") and matches the existing process_alternate_synonyms POWER_\<enum\> fuzzy-match entries already curated for these exact powers.
  • 80 rows total. No collision check hit an existing process_one_shot_values or process_alternate_synonyms entry for any chosen phrase (checked system-wide, not just DARK_TOWER context) other than HEAL/DODGE above.
  • game_test.process_tests is auto-generated only (testing-strategy.md: "DO NOT hand-author. Auto-generated from processed_input_aggregations real-usage stream") — no rows were hand-inserted there. Routing was instead verified live via POST /api/process/process-text (see the verification note in the queue-task result). The real-usage harvester () will pick these phrases up organically once players say them in production.

The hub (reusing the wired arena action)

The hero layer is the hero hall, layered onto the already-wired DARK_TOWER_ENTER_THE_ARENA action () with key-dispatch (HEROES / ROSTER / HERO_n / LEVEL_n / MISSIONS / SEND_n / COLLECT_MISSIONS / BATTLE) — no new action or process_action_handlers registry row.

Entry decision (preserve existing behavior): the no-key path keeps its original meaning — every arena phrase (enter the arena, arena, pvp, ...) goes STRAIGHT to combat, unchanged, so the live combat engine and the arena scenario tests are untouched (scenario_test 2462 guards this). The hero hall HUB is reachable via the heroes option in the command list () AND, as of the 2026-06-29 migration, by a cold-word heroes (also hero, hero hall, my heroes, hero hub, plus hero missions / hero mission added 2026-07-16) from any state: a dedicated DARK_TOWER_HEROES action routes straight to , which opens the hub. A dedicated action was the clean fix because the one-shot phrase table (game_process.process_one_shot_values) carries a parse-hint value_type, not a literal dispatch key, so it cannot inject a fixed HEROES key into the shared arena action. The action's process_action_handlers row ships enabled=0 and is flipped to 1 at deploy.

State-dependent resolution (report 230 F3, 2026-07-16). Both entry paths reach the SAME showArenaHub / mission screens, but the resolved ACTION differs by state — which is why the reviewer saw DARK_TOWER_ENTER_THE_ARENA in telemetry and could not reproduce it black-box. When a Dark Tower screen renders a heroes / hero missions OPTION (the command list and the free-explore grid, both intentionally reusing the wired arena action with a HEROES / MISSIONS key), Tier 2 OPTIONS matches the utterance to that option and resolves it to DARK_TOWER_ENTER_THE_ARENA (with the hub key) BEFORE the Tier 4 cold-word ... -> DARK_TOWER_HEROES row runs. With no such option on screen, Tier 4 wins and resolves to DARK_TOWER_HEROES. Both land the player on the hero hall. The 2026-07-16 fix added the missing hero missions / hero mission cold-word process_one_shot_values rows (the only real gap: cold, they FALLBACKed; heroes / hero hall already routed via DARK_TOWER_HEROES since the 2026-06-29 migration).

Code

  • backend/src/main/java/llitd/utils/darktower/hero/.java — pure rarity/stat/xp/ mission math (JUnit: ).
  • backend/src/main/java/llitd/engines/handlers/darktower/activity/.java — the hub (roster, detail, level, missions, battle) via enterArena key-dispatch.
  • backend/src/main/java/llitd/model/darktower/details/DarkTowerCompanion.java — heroLevel / heroXp / missionFinish fields.
  • backend/src/main/java/llitd/engines/handlers/common/baseline/.java — the heroes shortcut command.
  • backend/src/main/java/llitd/engines/wrappers/darktower/tools/.java — announceReturnedHeroMissions, the session-load mission-return announcement.
  • backend/src/main/java/llitd/engines/wrappers/.java — calls announceReturnedHeroMissions from continueTheJourney every turn.

Still to migrate (the broader old system — future slices)

The old hero system was larger; these are the remaining slices, deliberately deferred so the live combat engine and cross-player state are not rewritten in one push:

  • Hero army battle + ranked arena ladder — SHIPPED 2026-06-29. The hero hall's "lead heroes into battle" (ARMY key) resolves the combined hero team power against a rank-scaled enemy via the shared exploding-d20 contest; a per-type rank persists in towerStats (BASIC_ARENA) and scales the enemy each win, paying scaling gold + hero XP. . (Cross-player PvP arena below is still future.)
  • PvP arena — fight another player's defense team (cross-player; needs a target list + persistence).
  • Hero abilities / quick points — the 1,004 authored hero_powers + the DarkTowerHeroPointsLevel (NONE/LOW/MEDIUM/HIGH/MAJOR) allocation tiers (currently an orphan enum, ready to wire).
  • Troop command — heroes leading the trained troops (GAME_SOLDIER..GAME_CANNONS from research) into army battles.

Refs

  • legacy-feature-migration-audit.md §3.3 — the Heroes audit (the interweave, old refs, bring-back plan).
  • progression.md — Research troop training + building upgrades (the sibling migrated systems; troops are the army heroes will eventually command).
  • memory feedback_isp_wiring_leverages_existing_isps — the ISP-leverage rule.