Progression
Status: COMPLETE — Full design documented
Last updated: October 31, 2026
Dark Tower -- Progression & Character System
Status: COMPLETE — Full design documented
Subsystem: XP/leveling, character creation, classes, races, stats, powers, skills, paragon, companions, gods Primary sources: , , , enums in
llitd.model.darktower.enums.character.*
XP and Leveling
Source:
XP Curve (Exponential)
XP required per level:
Levels 1-10: round(1000 * 1.2^(level-1)) // Fast early: 1000, 1200, 1440, 1728...
Levels 10-100: round(5000 * 1.15^(level-10)) // Exponential mid: starts ~6,192 for level 11
Levels 100+: round(100000 * 1.12^(level-100)) // Paragon: starts ~2.6M for level 101
XP Requirements Table
| Level | XP for This Level | Cumulative XP |
|---|---|---|
| 2 | 1,000 | 1,000 |
| 5 | 1,728 | ~6,100 |
| 10 | 5,160 | ~25,000 |
| 11 | 6,192 | ~31,000 |
| 25 | ~41,000 | ~260,000 |
| 50 | ~819,000 | ~6.2M |
| 100 | ~8.1M | ~74M |
| 101 | ~100,000 | ~74.1M |
| 200 | ~9.3M | ~100M+ |
| 500 | Level cap | -- |
Level Calculation
calculateLevelFromValue(xp):
Iterates from level 1 to 500 (hard cap)
Subtracts each level's XP requirement
Returns the level where remaining XP is insufficient for next
Level Cap
Maximum level: 500 (enforced in the while loop: while (level < 500))
Legacy migrate note: characters imported from the old Dark Citadel skill do not land at raw legacy levels (those are uncapped and can exceed 3000). Migration scales to a ceiling of 400 (80% of the DT cap). See character-migration.md §1.3.
Claiming Earned Upgrades (the "upgrade" command)
Upgrades are slot-based, not point-based (Powers at level 5/50/100, Stat at 15, Skill at 35, Companions at 10/20/30/40, Gods at 25+; mandatory PRIMARY_CLASS / SECONDARY_CLASS first). A slot earned by leveling stays open until the player fills it. Two paths reach the picker:
- Auto-prompt — after a combat round,
endAfterRoundcallshasUpgradeToSpend, which defers a non-mandatory prompt while the player is in combat or in the first three sessions so it never interrupts a fight or the opening tutorial. - Explicit request — when the player says "upgrade",
attemptToUpgrade(userInitiated)resolves the slot directly and HONORS the request even when the auto-prompt would defer: an open earned slot opens the picker regardless of session count, and a slot blocked only by active combat is EXPLAINED ("finish your current battle first, say upgrade once the fight is over") rather than reported as "no upgrades to spend". Mandatory class slots still surface during combat so a classless character is never trapped. An explicit request with nothing earned still speaks the helpful "no upgrades to spend ... you are level N" line.
The misleading "no upgrades to spend" for an established player who had a long-open slot was fb 9009 (fixed 2026-06-15; scenario_test 2105).
The upgrade filter picker paginates any oversized option list. When a chosen filter -- notably the companion "All" filter for a leveled roster -- yields more options than the 35-tag voice cap can number, the picker shows one page of UPGRADE_OPTION_PAGE_SIZE options plus "More Options" and "Previous Options" nav that re-enter the same filter at the adjacent page (the page index rides in the filter key), so every option stays reachable instead of dead-ending past the cap (OVER_CAP_OPTIONS; real-player error_log 187213, ; scenario_test 2395). Mirrors the viewCompanions paging.
Reset & Respec System
Status: SHIPPED 2026-06-28 (DC->DT feature migration). Source: engines/handlers/darktower/builtin/.
A player can clear part of their chosen build so they can pick it again -- the respec feature carried over (and re-monetized) from the legacy Dark Citadel game, where it was a near-free gold action. The player says "reset" to open the Reset menu, or names a dimension directly ("reset powers"). Each reset is charged in diamonds (the player-facing name for Dark Tower special tokens -- see economy.md § Reset / Respec — Diamond Sink).
What can be reset
| Option | Clears | Diamond cost |
|---|---|---|
| Powers | the chosen power slots (chosenPowerFirst/Second/Third) |
5 |
| Classes | primary + secondary class and their derived power/stat/skill fields | 5 |
| Stats | the chosen stat (chosenStatFirst) |
5 |
| Skills | the chosen skill (chosenSkill) |
5 |
| Companions | the fresh companion picks (the starter Douglas is re-granted; hero-progressed and on-mission companions are preserved) | 5 |
| Everything | all of the above at once | 15 |
The very first respec is FREE (any dimension, one time only — see Mechanics); the costs above apply to every reset after the first.
What is preserved (never reset)
Level, experience, gear (equipped and owned), race, paragon, currency, clues, and story progress. Reset clears ONLY the level-up chosen build slots. Gear-derived and class-derived powers/stats/skills re-derive automatically on the next read, so a reset simply frees the chosen slots for the player to re-pick from the upgrade menu.
Mechanics and invariants
- Free first respec (onboarding grace). A new player's very first respec is FREE for any
dimension, so a first mis-pick is forgivable. Gated on
respecCount == 0;respecCountincrements on every reset that actually clears something, so every later reset is diamond-gated. The free reset never consumes a held reset token. Modeled on legacyRESET_COOLDOWN_COUNT, simplified to a single free first use. - Diamond charge plus shortfall. After the free first reset, a reset checks
canAffordDiamonds(cost); on shortfall it routes the player to the diamond purchase flow via (ShortfallCurrencyType.SPECIAL_TOKENS, player-facing "diamonds"). It NEVER charges for a no-op -- if the player has nothing to reset in the chosen dimension, it says so and skips the charge. - Reset token versus diamonds is the player's choice. A full "Everything" reset can be paid with a held reset token OR diamonds. When the player holds a token, the handler presents an explicit choice rather than auto-consuming the token. A single token costs 12 diamonds (below the 15-diamond direct cost), a bundle of three costs 30; both surface in the reset menu, and the single token is also offered in the Token Shop. See economy.md § Reset / Respec — Diamond Sink.
- Companion hero progress is never reset away. A recruited companion is also a combat hero — its hero level, hero experience, and any in-flight timed mission ride on the companion object. A companion reset re-rolls the fresh companion picks but PRESERVES any hero-progressed or on-mission companion in its slot (a wiped slot is never re-derived, so wiping it would silently destroy that earned progress). A respec re-rolls a build slot; it never deletes timed-mission hero progress.
- Class-skill teardown. Class power and stat derive from the class object at read time, but the
class skill is stored on its own field (
classSkill/secondaryClassSkill). Clearing a class therefore nulls all eight class fields, or a phantom class skill would linger in the skill list. - Hit-point recompute. Class, the toughness stat, the extra-hit-points power, and companion health boosts all feed max hit points, so any reset recomputes max HP via (the same call level-up uses) and clamps current HP down to the new max. A reset never heals.
- Starter companion. Resetting companions clears the wipeable slots and re-grants the
always-available starter (Douglas the Dog) via
addBasicCompanionswhen slot one ends up empty, so the player is never left with an empty party (hero-progressed and on-mission companions stay in place). - Blocked states. Reset is gated by
blockedFromGeneralActions-- no resetting mid-combat or mid-special-state.
Relationship to "change class"
Reset is the sanctioned re-roll. The free "change class" upgrade path remains a no-op for an already-classed character (see the Multi-Character Support note that classes are permanent via the free upgrade path) -- that guard exists so a player does not sideline their main chasing a free class swap. The paid Reset is the deliberate, diamond-gated way to clear a class (or any build dimension) and pick again from scratch.
NLU and wiring
DARK_TOWER_RESET is dispatched to (bound to Alexa
StartOverIntent and the Dark Tower reset command surface). Menu options re-enter DARK_TOWER_RESET
carrying the dimension key (POWER / CLASS / STAT / SKILL / COMPANION / ALL), read via
getKeyOrValue.
Regression
game_test.scenario_tests -- reset a seeded build dimension and assert the chosen slot is cleared and
diamonds are deducted (authored via add_scenario_test).
Starting Loadout
Source: Character creation flow (derived from defaults and creation handler)
| Attribute | Value | Notes |
|---|---|---|
| HP | computed | maxHp = (45 + level*4) * class multiplier + bonuses (e.g., Warrior L1 Fighter type x1.4 = 69). getCharacter() initializes a placeholder 100/100; .calculateHitPoints recomputes max once class is picked AND clamps currentHp <= maxHp (invariant — DT-2026-0508-012/010/021). |
| Level | 1 | |
| Experience | 900 | Almost enough for level 2 (need 1000) |
| Main Hand | Sword (WEAPON_SWORD_STANDARD) |
Starting melee weapon, equipped in startGame() |
| Ranged | Bow and Arrows (WEAPON_BOW_STANDARD) |
Starting ranged weapon, equipped in startGame() |
| Chest Armor | Leather Armor | ARMOR_LEATHER (CHEST slot), equipped in startGame() |
| Health Potions | 3 | ITEM_HEALING_POTION in ownedItemsMap, usable in combat, cap 3 |
| Gold | 2,000 | |
| Diamonds | 200 | |
| Wood | 600 | |
| Iron | 500 | |
| Insanity | 0 | |
| Start Card | 30000 | Main Story start |
| Inventory | 20 slots | maxInventorySpace = 20 (default) |
| Companions | Douglas Your Dog | Slot one: COMPANION_PET_DOG (COMBAT_BUFFER special, granted via addBasicCompanions()) |
| Difficulty | EASY | Default |
Character Structure
Source:
Identity
characterKey: String // Unique character identifier
characterName: String // Set once on creation
pendingCharacterName: String // During name confirmation flow
awaitingNameConfirmation: Boolean // Waiting for yes/no
Build (Dual-Class System)
Primary class:
primaryClass: DarkTowerClass
classPower: DarkTowerPower
classStat: DarkTowerStat
classSkill: DarkTowerSkill
Secondary class:
secondaryClass: DarkTowerClass
secondaryClassPower: DarkTowerPower
secondaryClassStat: DarkTowerStat
secondaryClassSkill: DarkTowerSkill
Additional choices:
chosenStatFirst: DarkTowerStat
chosenPowerFirst/Second/Third: DarkTowerPower // Up to 3 chosen powers
chosenSkill: DarkTowerSkill
chosenRace: DarkTowerRace
Gear-Provided Build Extensions
gearPowers: List<DarkTowerPower> // Powers granted by equipment
gearSkills: List<DarkTowerSkill> // Skills granted by equipment
gearStats: List<DarkTowerStat> // Stats granted by equipment
activeEnchantments: List<DarkTowerEnchantments> // Active enchantment effects
activeSetBonuses: Map<String, Long> // Set ID -> piece count
Classes
Source: DarkTowerClassTypes
Class Categories
| Category | Types | HP Bonus |
|---|---|---|
| Fighter/Leader | FIGHTER classes, LEADER classes | +40% HP |
| Psion/Healer | PSION, HEALER classes | +20% HP |
| Scoundrel/Caster | SCOUNDREL, CASTER classes | +10% HP |
| Other | All others | +3% HP |
Sub-Class Types
Source: DarkTowerSubClassTypes
FIGHTER, LEADER, SCOUNDREL, CASTER, PSION, HEALER (used for HP calculation grouping via DarkTowerClass.getClassType())
Full Class List (57 classes)
Basic: BARBARIAN, BARD, CAPTAIN, CLERIC, KNIGHT, MEDIC, MONK, PALADIN, PILOT, PIRATE, PSYCHIC, RANGER, ROGUE, SMUGGLER, SOLDIER, SPACE_MARINE, WARLOCK, WARMONGER, WARRIOR, WILD_MAGE, WIZARD, SPACE_PIRATE, BEAST_MASTER, DRAGON_LORD, SHAMAN, DRUID
Race-Specific: DWARVEN_BATTLEMASTER, ELVEN_STRATEGIST, ORCISH_RAIDER, WEREWOLF_SHADOW_WALKER, VAMPIRE_LORD
God-Specific: ELYSIUMS_CHAMPION, POSEIDONS_GUIDE, PERRIKS_GLADIATOR, MERRIS_PROTECTOR
Advanced: HAWK_TAMER, DRAGON_TAMER, MONSTER_TAMER, BLOOD_KNIGHT, DEATH_KNIGHT, ARCANE_ARCHER, LORE_MASTER, ASSASSIN, ARCANE_TRICKSTER, PSYCHIC_KNIGHT, SOUL_THIEF, MARKSMAN, HEAVY_GUNNER, DEMOLITIONS_EXPERT, TINKERER, NECROMANCER, WHIRLING_DERVISH, WITCH_DOCTOR, VIKING
Strategic: STRATEGIST, GRAND_STRATEGIST, MASTER_STRATEGIST
Races (14 Types)
Source: DarkTowerRaceTypes
HUMAN, ELF, HALF_ELF, DWARF, ORC, HALF_ORC, GOBLIN, GNOME, HALF_ANGEL, HALF_DRAGON, HALF_DEMON, VAMPIRE, WEREWOLF, CYBORG
Each race provides:
raceHitBoost: Flat hit chance bonus (applied twice if level >= 100)raceArmorBoost: Flat AC bonus (applied twice if level >= 100)raceDamageBoost: Percentage damage bonusraceSkillBoostAll: Skill bonus multiplied by level bonus
Stats (18 Types)
Source: DarkTowerStatTypes
Primary Stats
CHARISMA, CONSTITUTION, DEFENSE, DEXTERITY, INTELLIGENCE, STRENGTH, WISDOM
Secondary Stats
TOUGHNESS, AGILITY, WILLPOWER, FELLOWSHIP, WEAPON_SKILL, BALLISTIC_SKILL, MAGICAL_SKILL, PSYCHIC_SKILL, RESEARCH_MASTERY, TRAINING_MASTERY
Stat Impact Summary
| Stat | Combat Effect |
|---|---|
| STRENGTH | +5 melee hit, +30% melee damage |
| DEXTERITY | +5 ranged hit |
| AGILITY | +30% ranged damage |
| DEFENSE | +2 AC |
| TOUGHNESS | +35% HP |
Powers (148 Types)
Source: DarkTowerPowerTypes
Players can choose up to 3 powers plus class powers. Powers provide diverse combat abilities:
Key Combat Powers
| Power | Effect |
|---|---|
| ORCISH_RAGE | +45% damage |
| WEAPON_MASTER | +30% melee damage |
| MASTER_ARCHER | +30% ranged damage |
| DWARVEN_PROWESS | +40% melee damage, +2 hit |
| ELVEN_ARCHERY | +40% ranged damage, +2 hit |
| BARDIC_KNOWLEDGE | +13 skill bonus, +10 min success floor |
| FORTUNATE_STREAK | Re-roll one failed skill check |
| MASTER_OF_DESTINY | % chance to resist punishment effects |
| GRAND_STRATEGIST_POWER | % chance to resist punishment effects |
| ESCAPE_COMBAT (special) | +25 flee chance bonus |
| EXTRA_HIT_POINTS (special) | +35% HP |
Hunter Powers (Enemy-Type Bonus)
| Power | Target Type | Damage Bonus |
|---|---|---|
| DEMON_HUNTER | DEMON enemies | +25% |
| UNDEAD_HUNTER | UNDEAD enemies | +25% |
| HUMANOID_HUNTER | HUMANOID enemies | +25% |
Skills (39 Types)
Source: DarkTowerSkillTypes
ANIMAL_HANDLING, ARCANA, ATHLETICS, BLUFF, CHEMISTRY, CLIMB, COMMON_LORE, COMPUTERS, DEMOLITION, DIPLOMACY, DISCERN_CIPHER, ENDURANCE, HISTORY, INITIATIVE, INSIGHT, INTERROGATE, INTIMIDATE, INVESTIGATE, LEADERSHIP, LINGUISTICS, LUCK, MEDICINE, OCCULT_LORE, PERCEPTION, PERFORM, PILOTING, PSIONICS, REACTION, RELIGION, SCHOLASTIC_LORE, SLEIGHT_OF_HAND, STEALTH, STREETWISE, SURVIVAL, SWIM, WARFARE
Meta types: ANY, ALL, NONE
Skill check bonus: +16 for owning the skill, +16 if skill matches stat = +32 total if fully matched.
Paragon System
Source: paragon fields, DarkTowerParagonTypes, DarkTowerAssignParagon,
Earning Paragon Points
Points = ceil((level - 100) / 10.0) // 1 point per 10 levels above 100
Level 100: 0 points
Level 110: 1 point
Level 200: 10 points
Level 500: 40 points (maximum regular paragon points)
Level 500+: Special paragon points unlock (1 per 500 levels)
Assigning Paragon Points
Saying paragon opens assignment. Dispatch is DB-driven: DARK_TOWER_ASSIGN_PARAGON and DARK_TOWER_ASSIGN_PARAGON_SPECIAL both resolve to darkTowerAssignParagon (process_action_handlers rows 280 and 299, both enabled).
Normal points first. If the character has any unspent normal point, the menu is the six stat options (armor, damage, skill, crit chance, hit points, critical damage) — including at level 500 and above. Specials do not pre-empt that menu. A 500-plus character with unspent normals can assign them.
Specials stay, and they record themselves. Special points are offered only when a special remains and no normal remains. Each special is a PARAGON-category enchantment (PARAGON_HIT_BONUS and its siblings). Picking one grants that enchantment onto activeEnchantments and records the matching CLUE_PARAGON_* clue. specialParagonAssigned counts those clues, so the spend sticks and a later visit does not offer the same special. assignParagonSpecial is live code; commented-out grant and option loops are not kept. If the catalog has no unowned special left to pick, the handler does not render an empty menu.
Error does not grant. If assignParagonSpecial is invoked with no special left to spend, it speaks the error and returns without adding a clue or an enchantment.
Paragon Stat Caps
Source: paragon fields
| Paragon Stat | Field | Max | Notes |
|---|---|---|---|
| Damage % | paragonDamagePercent |
20 | |
| Armor % | paragonArmorPercent |
10 | |
| Skill % | paragonSkillPercent |
10 | |
| Crit Chance % | paragonCriticalChancePercent |
10 | |
| Crit Damage % | paragonCriticalDamagePercent |
Unlimited | Deep endgame sink |
| Hit Points % | paragonHitPointsPercent |
10 | Each point = +1% max HP |
Note: DarkTowerParagonTypes has 6 values: ARMOR, DAMAGE, CRITICAL, CRITICAL_DAMAGE, SKILL, HIT_POINTS.
Design Intent
Crit damage is the only uncapped paragon stat. Players who push to level 500 can dump all remaining points into crit damage for meaningful endgame scaling. This rewards sustained play without breaking early/mid game balance.
Companion System
Source: companion fields
Slots
companionOne through companionSix (6 companion slots)
mount: DarkTowerEnemy // Mount is technically an enemy type
Companion Impact
Source: DarkTowerCompanion model fields,
Companions provide two categories of bonuses:
Passive Stat Bonuses (From Companion Data Model)
Each companion has data-driven percentage boosts stored in DarkTowerCompanion:
| Field | Effect | Typical Values |
|---|---|---|
healthBoostPercent |
Added to HP percentage multiplier | Per-companion, data-defined |
goldPercentBoost |
Increases gold earnings | Per-companion, data-defined |
experiencePercentBoost |
Increases XP earnings | Per-companion, data-defined |
tokenPercentBoost |
Increases special token earnings | Per-companion, data-defined |
companionPotionBoost |
Increases potion effectiveness in combat | Per-companion, data-defined |
Combat Ability Scaling (Calculated from Level)
Source: — all values scale with companionEffectiveLevel = characterLevel + companionBoostedLevelPerks
| Ability Type | Chance Range | Effect Range | Notes |
|---|---|---|---|
| BLOCK | 15-30% | -- | Block one attack per round (capped at 30%) |
| DAMAGE (attack chance) | 20-40% | -- | Chance to attack each round, for every companion whatever its special. The lead companion, the first in the party, attacks every round, so a party is never silent; only the rest roll, because every companion certain to hit would end most fights before the player acts, the damage being a percent of the enemy's health |
| DAMAGE_DONE | 5 + up to 50 | -- | Percent of max damage, percentOfCapped(8, level, 50) + 5 |
| HEAL (heal chance) | 15-30% | -- | Chance to heal each round |
| HEAL_DONE | 3 + up to 20 | -- | Percent of max HP healed, percentOfCapped(3, level, 20) + 3 |
| SKILL (boost chance) | 20-40% | -- | Chance to boost specific skill checks |
| SKILL_DONE | 4 + up to 35 | -- | Percent boost to skill check, percentOfCapped(5, level, 35) + 4 |
Special Ability Effectiveness (By Companion Type)
Source:
| Special Type | Effect Scaling | Max |
|---|---|---|
| ASSASSIN | level / 10 kills |
10 kills |
| HEALER | Full heal (effectiveness = level) | -- |
| HIT_BUFFER / ARMOR_BUFFER | level / 8 + 3 flat bonus |
15 |
| DAMAGE_BUFFER / SKILL_BUFFER | level / 4 + 8 percent bonus |
30% |
| CRITICAL_BUFFER | level * 1.25 + 10 crit damage |
50% |
| DRAGONS_BREATH | level + 8 AOE fire damage, plus a 6-round fire DoT applied twice (non-stacking; the double application keeps the higher of two duration rolls) |
Uncapped |
| DAZE/DISARM/COMBAT_BUFFER | level / 8 + 1 count |
5 |
| ACID/FIRE/POISON/CROSSBOW | level / 8 + 1 DOT stacks |
5 |
Special Ability Activation Chance
Source:
| Special Type | Chance | Scaling |
|---|---|---|
| ASSASSIN, HEALER | 100% | Always activates |
| DAZE/DISARM/HIT_BUFFER/DAMAGE_BUFFER/ARMOR_BUFFER/SKILL_BUFFER/DRAGONS_BREATH/COMBAT_BUFFER | 5-15% | 5 + percentOf(5, level), capped 15% |
| ACID/FIRE/POISON/CROSSBOW/CRITICAL_BUFFER | 10-25% | 10 + percentOf(8, level), capped 25% |
Companion Level Boosting
Source:
Base companion bonus is +10 to effective level. Additional bonuses:
| Source | Bonus |
|---|---|
| Base | +10 |
| Early-game aid (character level < 10) | +(10 - characterLevel), decays by 1 per level to 0 at level 10 |
| SIGNIFICANT_COMPANION power | +5 + (3 * weapon boost) |
| RALLY power | +5 + (3 * weapon boost) |
| Auxiliary trinket (companion-specific) | +15 |
| Necklace trinket (companion-specific) | +15 |
| Enchantment companion bonus | Variable |
| BLUE rarity companion | +10 |
| PURPLE rarity companion | +20 |
| EMERALD rarity companion | +30 |
Starter Companion
New characters receive Douglas Your Dog (COMPANION_PET_DOG) in slot one via addBasicCompanions(), called during startGame() on first character creation (both the real-name confirmation path and the quick-start path). The companion has COMBAT_BUFFER special and no clue requirement.
Because early fights are the hardest for a brand-new player who has only this one starter companion, the starter (and any companion held below level 10) receives a decaying early-game effective-level bonus (see the Early-game aid row in Companion Level Boosting above). The bonus is largest at level 1 and reaches zero by level 10, so it eases the opening fights without affecting mid or late game balance.
The starter is a free companion that sits outside the level-gated slot accounting. Earned companion slots unlock at levels 10, 20, 30, and 40, so a level-1 character has zero earned slots. therefore exempts the starter (COMPANION_PET_DOG) when it reconciles filled-versus-available companion slots — without that exemption the starter is counted as occupying earned slot one and is stripped the first time abilities reconcile after creation, leaving the player with no companion (DT-2026-0603-003, regression scenario_test 1855).
Viewing and Choosing Companions
Players inspect their party and pick a new companion through , wired to the DARK_TOWER_PICK_CHAT_COMPANION action. NLU phrases that reach it include "choose companion", "pick companion", "select companion", "my companions", "view companions", "who is traveling with me", "my party", and "characters to travel with". The handler speaks the current roster, reports companion-slot status (slots unlock at levels 10, 20, 30, 40), and -- when a slot is open -- lists every currently selectable companion as a pick option that reuses the upgrade-apply path (DARK_TOWER_UPGRADE with key ITEM_COMPANION_{enumKey}). Companions gated behind a capture clue (for example the exploding creatures, which require CLUE_FOUND_EXPLODING_*) are not selectable until captured; when nothing is selectable the handler explains that path instead of showing an empty list.
The DARK_TOWER_PICK_CHAT_COMPANION action previously had no row in process_action_handlers, so all of those phrases dead-ended with "that command isn't available" -- a player could neither see nor choose companions outside combat (the in-combat status sheet was the only place companions surfaced). Regression: game_test.scenario_tests 1836.
Dismissing and Swapping a Companion
The same menu offers Dismiss a Companion and Swap a Companion whenever the party holds a companion that can leave. They are free; a full reset of every slot stays the paid reset, and a full party is offered it beside the swap. Both run through viewCompanions, with the verb in the option key or in the player's words. Only "dismiss Helga" reaches the change by name from anywhere outside combat, through its one free-form row; "release companion" and the swap, change and switch words are exact rows that open the list, because bare swap routes to using an item.
- Dismiss names who can leave, or acts at once on a named companion: the companion goes and its slot opens. A name is whole words of the companion's name, so a word that only fits inside one asks who.
- Swap does the same and opens the companion picker on the same turn. With no other companion unlocked it changes nothing and says so, so a swap never leaves a player with an empty slot and no one to fill it.
- The starter stays.
COMPANION_PET_DOGis free and sits outside the slot budget, so sending it away frees nothing. - A companion on a mission stays until the mission is collected, and does not swing in combat while it is away.
- Hero progress is asked about first. A companion with hero level or hero experience ( is false) is named with the level it would lose, and only a yes sends it away, the same rule the reset holds to.
- Blocked mid-combat by
blockedFromGeneralActions, as the reset is.
Gods System
Source: DarkTowerGodTypes, DarkTowerGodDomain
8 Gods
| God | Associated Domain(s) |
|---|---|
| ELYSIUM | -- |
| MERRIS | -- |
| DESTIN | -- |
| TORRENT | -- |
| PERRIK | -- |
| APOLLO | -- |
| VENUS | -- |
| POSEIDON | -- |
Domain Conflict System
Source: DarkTowerGodDomain
5 opposing domain pairs -- gods with opposing domains cannot coexist in active slots:
| Domain | Opposes |
|---|---|
| LIGHT | DARK |
| ORDER | CHAOS |
| LIFE | DEATH |
| WAR | PEACE |
| NATURE | ARCANE |
conflictsWith(other): return this.getOpposingDomain() == other
God Data Model
Source:
Each god has:
primaryEnumKey:DarkTowerGodTypesidentifiername: Display namedescription: Lore textprayerTypes:DarkTowerEffectTypes— the effect granted when prayingweaponOfGod:DarkTowerWeaponTypes— the sacred weapon associated with this goddomain:DarkTowerGodDomain— one of 10 domains (affects passive bonuses)
God Slots
- Active slots: 4 maximum (
activeGodOnethroughactiveGodFour) - Level requirement: 25 (minimum to pray, per
getMinimumLevel()) - Devotion tracking:
Map<String, Integer> devotionLevels(god name -> devotion 0-100) - Prayer locations: 8 free explore points (one per god, e.g.,
ELYSIUM_PRAYER_POINT) - Sacred weapons: Each god has an associated weapon (5% drop chance at max devotion at own temple)
Prayer System
Source:
Praying does the following:
- Grants the primary god's prayer effect (
addEffect(prayerTypes)) - Increases devotion by +5 per prayer (capped at 100)
- Sacred weapon drop: At a god's own temple with 100% devotion, 5% chance to receive the sacred weapon (one-time only, checked via
isOwned())
Devotion System
Source:
Devotion is a 0-100 scale per god, tracked in .
Devotion Tiers
| Range | Tier | Significance |
|---|---|---|
| 90-100 | Zealot | Full passive bonuses, can request blessings (95% success) |
| 70-89 | Devout | Strong bonuses, can request blessings (70-89% success) |
| 50-69 | Faithful | Moderate passive bonuses |
| 30-49 | Follower | Minor passive bonuses |
| 10-29 | Novice | Minimal passive bonuses |
| 0-9 | Agnostic | No bonuses |
Divine Blessings (Active Ability)
Source:
Players with 70%+ devotion (Devout or Zealot tier) can request blessings from their primary god:
- Success rate: Equal to devotion level (capped at 95%)
- Devotion cost: -10% (Devout) or -5% (Zealot)
- Effect: Grants the god's prayer effect with a domain-themed narrative
- Each domain provides a thematic blessing (e.g., WAR = damage boost, PEACE = defense boost, LIFE = regeneration)
Devotion Passive Bonuses
Source:
The primary god's domain determines which passive bonuses scale with devotion. Bonuses are percentage multipliers applied to gameplay systems:
| Domain | Primary Bonus | Novice (30%) | Follower (50%) | Devout (70%) | Zealot (90%) |
|---|---|---|---|---|---|
| ARCANE | Experience | +5% | +10% | +15% | +25% |
| NATURE | Gold find | +10% | +20% | +30% | +50% |
| WAR | Damage | +5% | +10% | +15% | +20% |
| DARK | Damage | +5% | +10% | +15% | +20% |
| CHAOS | Damage | +5% | +10% | +15% | +20% |
| PEACE | Defense | +5% | +10% | +15% | +20% |
| LIGHT | Defense | +5% | +10% | +15% | +20% |
| LIFE | Healing | +10% | +20% | +35% | +50% |
| All others | XP (minor) | +2% | +5% | +8% | +12% |
| All others | Gold (minor) | +2% | +5% | +8% | +12% |
Critical Hit Bonus (Domain-Specific)
| Domain | 30% Dev | 50% Dev | 70% Dev | 90% Dev |
|---|---|---|---|---|
| CHAOS | +5% | +7% | +10% | +15% |
| ORDER | -- | +3% | +5% | +8% |
God Registration
Source:
- Must be at a temple (
KEEP_LOOKUP_POINTwith god prayer point) - God is assigned to first available slot (1-4)
- Domain conflict check prevents opposing domains from coexisting
- If all slots full, must remove a god first ()
- Removing a god applies a devotion penalty (percentage reduction)
Multi-Character Support
Each account can have multiple characters with independent progress. The character management menu supports:
- Add new character
- Delete character
- Switch between characters
Legacy Dark Citadel character migration (LOCKED design — not shipped)
Players with a linked email can eventually migrate one merged character from the legacy Dark Citadel skill into Dark Tower. Canonical design: character-migration.md. Non-negotiables:
- One-shot + confirm — post-migrate Dark Citadel play does not transfer again.
- Level scale — legacy is uncapped (live max ~3400); DT hard cap is 500. Migrated level = scaled progress with ceiling 400 (80% of DT cap). See scaling constants in the migration doc.
- Permanent season currency — soft currency never lands in a competitive
CURRENT_SEASONbucket. - Free migration slot — successful migrate may bump
maxCharacterSlotsby 1 so the new character fits without a diamond charge. - Discord — migration problems/bug reports point players at Discord + report bug.
This is player-state migration (different project from legacy-feature-migration-audit.md, which brings back systems like heroes/invasions/research).
Delete is a hard delete of the persisted row. removes the character key from the account's characterKeys list and calls , which evicts the Redis cache entry AND deletes the dark_citadel.character_dark_citadel row. Character membership has no FK — a character exists only because some account's characterKeys JSON array names it — so a cache-only eviction (the pre-fix behavior) left the row permanently orphaned with nothing pointing at it once the key was dropped from the list, indistinguishable from live progress by any timestamp-based orphan check.
Each character maintains separate: state, inventory, story progress, clues, equipment, level, XP.
Character Switch and Create Recovery (no data loss)
Creating a character moves the account currentCharacterKey to the new level-1 hero; the prior character is never deleted, only out of view. A player who creates a second character can perceive total loss of their leveled main (player feedback 9016 and 9014), so the recovery UX below is mandatory.
- Create (.createNewGameCharacter): after the creation confirmation, the player is told their previous character is safe, named with its level, and given the exact phrase, say switch character, to return to it.
- Switch (.switchPlayer): names the character switched to with its level so the player can see when they have rotated back to their main, and points at saying switch character again to keep cycling.
- Discoverability: switch character is a tier-4 DARK_TOWER NLU command, with recovery synonyms such as my characters, other character, character menu, and character list, and it is listed in the help command menu whenever the account owns more than one character.
- Change class is not a re-roll (.handleUpgradeAction): classes are permanent once chosen. When a fully-classed character says change class, the game explains classes cannot be changed and that creating a new character is not required, so players never sideline their main chasing a class change. An open class slot, a new hero primary or an unlocked secondary, still opens the picker. Resetting is distinct from changing: the paid Reset feature (see the Reset & Respec System section above) deliberately clears a chosen class (and other build dimensions) for a diamond cost so the player can pick again from scratch -- the free change-class path stays a no-op, but the paid reset is the sanctioned re-roll. A full "Everything" reset may also be paid with a reset token -- a diamond-purchasable, stockpileable respec currency ("buy reset token"); see economy.md for the token economics.
Regression coverage: JUnit DarkTowerPlayerGeneratorTest covers the create and switch announcements; the multi-character precondition is not seedable in the single-command scenario framework (see scenario id 125), so scenario_tests 2102 and 2103 delegate to that JUnit class.
Slot Purchase & Persistence
- Source: ,
.{createNewGameCharacter,switchPlayer,purchaseCharacterSlot}, . - Max slots: Stored on (default 1). Progressive diamond pricing per : slot 2 = 100, 3 = 250, 4 = 500, 5 = 1000, 6 = 2000, 7 = 5000, then
10,000 * (slotCount - 6). - Save-before-reload rule: Any handler that mutates (characterKeys, currentCharacterKey, maxCharacterSlots) MUST call before
finalizeCharacter/populateDarkTowerPropertiesruns. The populate path re-fetches from Redis; without the prior save, in-memory mutations are wiped and saved character records become orphaned (no account'scharacterKeyslist references them). Regression: fb 941 / .
Insanity System
Source: , DarkTowerStatus.INSANITY
Mechanic
Insanity is a percentage (0-100) stored in insanityLevel.
Escalation (between INSANITY-status cards):
0-50 insanity: +1d6 per card transition
51-80 insanity: +1d4 per card transition
81-100 insanity: +1d2 per card transition
At 100%: Clears to 0, redirects to card 15653 ("Landing Point Insane").
Exit reset: Leaving INSANITY-status cards (moving to non-INSANITY card) resets insanity to 0.
Other sources: Punishment types INSANITY, INSANITY_MINOR, INSANITY_MAJOR can add insanity. Reward types INSANITY_HEAL, INSANITY_HEAL_MINOR, INSANITY_HEAL_MAJOR reduce it.
Difficulty scaling: INSANITY-status cards get +0 to +7 difficulty bonus based on current insanity level.
Notification throttling: 10-minute cooldown on insanity messages. Four tiers: LOW (0-10), MINOR (11-50), MAJOR (51-90), CRITICAL (91+).
Active Effects
Source:
Map<DarkTowerEffectTypes, Long> activeEffects // Effect type -> stack count
Persistent effects (survive between combats) tracked by type and stack count. Different from combat-only positives/negatives which are cleared after each fight.
Learn Codex (the "research menu")
Source: , DarkTowerLearnableRegistry, FreeTextDeclarationAction.DARK_TOWER_LEARN
The Learn Codex is the in-game encyclopedia. Saying "learn" opens a category menu, and the player can browse a category ("learn weapons") or look up any object by name ("learn about longsword"). This is the system player Lauri (lauri2023@gmx.de, 2026-06-27) called the "research menu" -- a place to read up on the game's content. It is DISTINCT from the Research System below (research slots and the perk tree): the codex is read-only knowledge, the Research System is a progression mechanic.
How it works
| Concern | Behavior |
|---|---|
| Entry | "learn" with no key -> handleCategorySelection lists the browsable categories. |
| Categories | The DarkTowerFeatureTypes the registry can enumerate: Weapons, Armor, Enemies, Items, Skills, Classes, Companions, Trinkets, Gods, Gems, Powers, Stats (the last three restored 2026-06-28). Each routes through DarkTowerLearnableRegistry.findByType. |
| Direct lookup | "learn about {name}" -> findAllMatchesByKey ranks matches (exact enum, name, plural, contains) and reads the best, or lists choices on a tie. |
| Filtered lookup | a filter key ("ARMOR_GLOVES", "RED_GEM") -> findByFilter via each object's CanBeFiltered.matchesFilter. |
| Pagination | five per page; canonical More and Previous options (page-in-key, option-lists-and-pagination.md vocabulary); boundary guard re-renders page one past the end; "choose different category" returns to the menu. |
| What is learnable | any object implementing both CanBeLearnedAbout and CanBeDiscoveredByKey. |
Category parity (2026-06-28, player Lauri)
The rebuilt codex menu had dropped three browsable categories the registry could already serve:
Gems, Powers, and Stats. All three model types (DarkTowerGem, DarkTowerPower, DarkTowerStat)
implement CanBeLearnedAbout and have wrapper getters (getAllStoryGems / getAllStoryPowers /
getAllStoryStats), so they were reachable by direct name lookup but missing from the browse menu.
They are now registered in findByType, added to getAllLearnableObjects (so cross-type key search
covers them), and listed in the category menu.
Collectibles and Races browse (2026-06-28): both are now browsable. DarkTowerCollectible
gained CanBeLearnedAbout (it already had CanBeDiscoveredByKey) and DarkTowerRace gained both
learnable interfaces; both are registered in findByType + getAllLearnableObjects and listed in
the category menu, sourced from getAllStoryCollectibles / getAllStoryRaces.
Gear Sets and Effects browse (2026-06-28): both are now browsable. DarkTowerSet and
DarkTowerEffect gained the learnable interfaces and are registered in findByType (STORY_SET /
EFFECT) + getAllLearnableObjects and listed in the menu, sourced from getAllStorySets /
getAllStoryEffects. The codex now browses 16 categories -- every DarkTowerFeatureTypes with
backing content.
Not applicable: RECIPE remains a DarkTowerFeatureTypes value with NO backing content -- the
wrapper service has no getAllStoryRecipes and there is no DarkTowerRecipe model, so there is
nothing to browse. Wiring it would require first creating the recipe content model + wrapper getter. This is the codex half of the larger weapon-categorization work (a player-defined weapon
category and sort restore is the matching open item -- see economy/exploration combat-filter notes).
Weapon type browse (2026-06-28, player Lauri)
Lauri also asked for "weapon types ... make your own categories." The codex's weapon category now opens as a TYPE PICKER -- Bladed, Blunt, Ranged, Other -- instead of paging the entire ~480-weapon list, so the player can browse by kind the way Dark Citadel allowed.
The bug it fixes: DarkTowerWeapon.matchesFilter derived the type bucket from weaponClassType, which
is HANDEDNESS (one-handed / two-handed / shield), so it produced ONE_HANDED_WEAPONS and never matched
the codex's BLADED_WEAPONS / BLUNT_WEAPONS / RANGED_WEAPONS / OTHER_WEAPONS keys -- weapon-type
browsing was dead. The category is now DERIVED from the weapon's range and damage type:
| Bucket | Rule |
|---|---|
RANGED_WEAPONS |
darkTowerWeaponRangeType == RANGED (wins first: a piercing bow is still ranged) |
BLADED_WEAPONS |
melee and damageType is SLASHING or PIERCING |
BLUNT_WEAPONS |
melee and damageType is BLUDGEONING |
OTHER_WEAPONS |
everything else (elemental or shield melee weapons) |
Both fields are populated on every weapon content row, so the four buckets are well-filled. A second
fix went in alongside: parsed the page with a fixed
three-way split("_"), which broke on any underscore-bearing filter key (BLADED_WEAPONS,
ARMOR_GLOVES) -- "More Bladed Weapons" would have paged nothing. It now parses the page off the END.
Still deferred: player-DEFINED named weapon categories (a personal favorites / custom-bucket system)
and the rarity / gear-score SORT selector were both lost in the rewrite and are net-new builds, not part
of this fix. The voice readout of a weapon's learnAbout() still speaks the raw handedness enum
("Type: ONE_HANDED") -- a separate voice-polish item.
Research System
Source:
Map<DarkTowerResearchSlots, DarkTowerResearchDetails> researchSlots
Research slots are keyed by DarkTowerResearchSlots enum, each containing research progress details. Unlocks abilities/perks via a research tree with prerequisites.
Research Stations: Buy-Only, Not Captured
Status: LOCKED (2026-05-12). Research stations are unlocked through paragon progression and slot purchases, NOT by capturing world-map zones. The earlier capture-point design (where players competed for PLAYER_RESEARCH_BOOST and similar capture-point zone types out in the free-explore world) is permanently retired.
Rationale. Player feedback rounds 718-721 (Round 3 of work.task.id=19054) flagged that capture-based research stations would let strong players hoard contested zones and lock weaker players out of progression. The capture-point system was already disabled in code; this lock formalizes the buy-only model as the canonical design. deleted the 6 dead PLAYER_*_BOOST enum values from DarkTowerFreeExplorePoint, the dead isCaptureWorldPointType() utility, and supporting comment stubs.
What players see. No change in prod. Research slots remain VIP-gated and purchasable. No capture mechanic for research exists. Do not reintroduce capture-style world-map contention for research progression — if a future design wants player-vs-player resource competition, route it through invasion assault points (which are designed for that) or guild systems, not research.
Research: troop training -- SHIPPED (2026-06-28)
The first migrated research slice is TROOP TRAINING (the old "Train Soldiers"), built on a pure cost
util. From the research menu ("research") a player picks "train troops" and trains any of five troop
types -- soldiers, archers, artillery, knights, cannons -- each a DarkTowerResearchType.TRAINING slot.
| Concern | Behavior |
|---|---|
| Start | "train soldiers" charges an escalating GOLD cost (, triangular in the slot's completed count) and stamps timeUntilFinished = now + trainingMinutes. |
| Status | the training menu shows each slot: idle (cost + minutes for the next batch), in progress (minutes remaining), or ready. |
| Collect | "collect troops" awards troopsPerBatch of the slot's troop collectible for every finished batch, raises the slot's amountResearched, and clears the timer -- so the next batch costs more, takes longer, and yields slightly more. |
| Defaults (tunable) | base 500 gold / 5 minutes / 5 troops at count zero, all in the tunable block. |
is pure + JUnit-pinned (); the real-time countdown is not asserted against a live clock (hermetic-test rule). Code: + .
ISP value (leverages the EXISTING catalog, owner guidance 2026-06-28): training makes the army REAL,
which makes the EXISTING VIP-gated extra slots worthwhile -- RESEARCH_SLOT_VIP and
BUILDING_RESEARCH_SLOT_VIP (gated by CLUE_VIP_SECOND_* clues a VIP/ISP purchase grants) give a player
MORE concurrent research and training capacity, so owning the existing VIP ISP now buys real value. The
win is boosting the EXISTING ISP, not a new product (memory feedback_isp_wiring_leverages_existing_isps).
Keep building upgrades -- ALSO SHIPPED (2026-06-28): a player upgrades any of the eight keep
buildings (stables, armory, bakehouse, walls, chapel, moat, barracks, silo) via "upgrade buildings"
then "upgrade walls". Each upgrade charges escalating gold (),
runs a real-time countdown, and on collect PERSISTS a higher level in
, read by -- so the
keep handler's reward scaling now actually improves (the previously-dead read-side is live). Levels are
PER-BUILDING and permanent (the "better than Dark Citadel" choice: a building never loses its level);
the two building research SLOTS cap how many upgrade AT ONCE -- one for everyone, a second unlocked by
the EXISTING VIP building slot -- so the VIP ISP buys real concurrency value (the ISP win is on the
existing catalog, not a new product). getUpgradeLevel reads buildingLevels first with a
backward-compatible fallback to the old slot scan, so existing characters (empty buildingLevels) see
no change until they upgrade. JUnit pins buildingCost / buildingMinutes.
Still to build (the lifecycle spec below): the 2 research-slot PERK projects (character upgrades),
plus character-stat / army research effects. SHIPPED 2026-06-29 (see the subsection below): the speed-up consumable, content-driven RESEARCH_BUILDING costs debiting gold, wood, and iron, the guild tavern as a raisable building, and an interim troop keep-defense hit-point bonus.
Research 4X completion -- SHIPPED (2026-06-29)
Four migration gaps closed, all built on the existing research backbone:
- Speed-up consumable. The research menu's "speed up" option opens a speed-up menu listing every
in-progress training or building timer. Picking one consumes an owned
ITEM_SPEED_UP_BOOST_*(one, five, or sixty minutes) against that slot'stimeUntilFinished-- subtracting the boost minutes, or finishing the slot now if the boost covers the rest. It prefers the smallest owned boost that finishes the remaining time, otherwise the largest, so a sixty-minute boost is not wasted on a one-minute job. Wired in with new key branches onDARK_TOWER_RESEARCH(no new action), reusinghasItem/removeItems. This closes the sink the menu already advertised. - Guild tavern is raisable.
GUILD_TAVERNjoined the upgradeable keep-building set, so the one building with a live combat read-side -- adds two percent hit points per tavern level -- can now actually be levelled through the building menu. - Data-driven building costs (
RESEARCH_BUILDINGrule_type). Per-building costs moved out of the single hardcoded base into content: each keep building has a row indark_citadel.rule_dark_citadel_commonwithrule_type='RESEARCH_BUILDING'and arule_dataJSON ofbaseGold/baseWood/baseIron/minutesToLevel, loaded by the newDarkTowerResearchBuildingRegistry(the same single-table rule pattern as enemies / weapons). A building upgrade now debits gold AND wood (WOODEN_LOG) AND iron (IRON_SHEET) -- the resources the keep already produces. Gold escalates triangularly; wood, iron, and minutes escalate linearly. A building with no row falls back to a default def, so existing characters always upgrade. The JavaDarkTowerUpgradeTypesenum stays the stable KEY (therule_id). Rows authored on prod and local. - The building menu is a summary. "Upgrade buildings" says what is ready, what is upgrading, the cheapest upgrade the player can start now in gold, wood and iron, and how many can start; "building costs" reads every building's level and next cost ().
- Troop sink -- interim keep defense. Trained troops (soldiers, archers, artillery, knights,
cannons) now give a small, capped hit-point bonus read live in
calculateHitPoints(DarkTowerResearchBuildingRegistry.keepDefenseHpPercent: about one tenth of a percent per troop, capped at ten percent at one hundred troops), so an army has an interim defensive payoff. Hero troop-command remains the future full sink.
Pure cost + troop curves are JUnit-pinned (); the real-time countdowns are not asserted against a live clock (hermetic-test rule). Still open after this slice: the 2 research-slot PERK projects (character-stat upgrades) and concrete army / character research effects.
Research lifecycle -- the migration target (owner-flagged not-migrated; design TBD 2026-06-28)
The slots map, the DarkTowerResearchSlots / DarkTowerResearchType / DarkTowerResearchDetails
model, and the hub all EXIST, but the system is SCAFFOLDING ONLY:
is informational (it mentions open slots and routes to Character / Perks) -- there is no way to START a
research project, run its countdown, spend a speed-up, or COLLECT it.
already READS a buildingLevel for reward scaling, but nothing ever WRITES it, so that read-side is
dead. This is the owner's "research system" gap, distinct from the Learn Codex (which Lauri called the
"research menu" -- see the Learn Codex section above).
Was (old Dark Citadel 4X layer): an async build / train / research layer -- nine slots (5 training,
2 research, 2 building; VIP unlocks extras). Lifecycle: pay the cost up front -> a real-time countdown
-> optional speed-up items -> collect and level up. Old refs: engines/handlers/story/research/**
(, , buildings/AStoryUpgradeBasic,
troops/); service/story/;
utils/story/ (triangular cost formula :39-47); story_character.research_time.
Build stages (each its own focused change -- NOT yet built):
- (pure: the triangular
calculateCost(level, base)plus aminutesToLevelcurve) + JUnit. Cost and time scale with the target level. - Content: port the building / research defs to a
RESEARCH_BUILDINGrule_type inrule_dark_citadel_common(baseGold/baseTokens/baseWood/baseIron/minutesToLevel), so costs and effects are data, not hardcode. - The slot lifecycle on
DarkTowerResearchDetails: START (debit the cost through the cache-writeback money path, stamptimeUntilFinished), STATUS (time remaining), COLLECT (once the clock has passed, apply the level-up), plus a speed-up consumable. - Concrete effects: the first and simplest is leveling a building -> write
buildingLevel, which the existing read-side picks up automatically (the audit's "activating the write lights up the read-side"). Then character-stat research, army (+armor / +attack), and troops. - Rewrite the hub from informational to actionable + register the start / collect / speed-up NLU actions.
Open design decisions (owner's call before building):
- Cost + time curve -- how steep is the triangular cost, and how long are the countdowns (minutes that scale how)? This sets the whole pace of the 4X layer.
- What research grants -- which concrete boosts (building reward scaling, character stats, army combat bonus, troop counts) and how large.
- ISP integration -- the Step-3 idea: an EXTRA research slot (or a research speed-up) as a diamond-purchasable ISP perk, the same lever as the reset-token idea. Routes through .
Why this is a careful, dedicated build: it is a multi-component async system (cost util + content +
slot lifecycle + effects + NLU) whose pacing knobs are owner-taste calls, and the real-time countdown
makes it awkward to test hermetically (a collect test must seed a past timeUntilFinished). Build it
deliberately once the curve and effects are decided, not rushed.
Statistics Tracking
Source: stats fields, written by DarkTowerStatsUtility.
Only the trackers with a live writer remain. The trackers whose data nothing ever read (chapter starts/ends, aggregate combat damage, item use, crafting, playtime, session count) were removed as write-only dead code, along with tower-wave tracking — the "tower defense" handler runs a single combat, not a multi-wave loop, so there was no wave-completion event to record.
| Tracker | Key Format | Purpose | Consumer |
|---|---|---|---|
enemyKills |
{enemyType}_{difficulty} |
Kill counts by type | Inspect screen + title gate |
actionUsage |
action_name | Special action use count; CAPTURE counts successful captures |
Power-recharge cap + crowned-title gate + the 25-capture milestone |
checkStats |
{checkType}_{difficulty} |
Skill check pass/fail counts | Recorded (no surfaced consumer yet) |
cardVisits / cardTransitions |
card_id / {fromCard}_{toCard} |
Card visit + navigation tracking | Recorded (no surfaced consumer yet) |
fishingStats |
fish_type | Times caught | Recorded (no surfaced consumer yet) |
recentLoot |
Deque |
Last 200 loot events (circular) | "last rewards" command (populated directly by the reward path, not via a tracker) |
towerStats and globalStats getters remain on the cache (the title gate still reads towerStats.highestWaveReached) but have no writer; they stay empty until a real tower-wave loop and a session/playtime timer exist. Rebuild the removed trackers from this table when a consumer for their data is added.
Power Progression & Balance Curve
New characters unlock powers via level gates, class training, and clue rewards. The balance guardrails in combat.md (Rules 1-5) govern in-fight behaviour; the progression rules below govern which powers a player may access at a given level.
Level Gates (from DB `rule_data.levelRequired`)
| Level | Powers Unlocked (sample) |
|---|---|
| 1 | Class power only (e.g. SNEAK_ATTACK for Rogue) |
| 5 | INVISIBILITY_TRIGGER |
| 6 | CHARM_MONSTERS |
| 10 | DODGE, FLIGHT_TRIGGER, CHANCE_INVISIBILITY |
| 15 | DOMINATE_MONSTER |
No levelRequired gate is lower than the combat caps permit. Low-level powers (e.g. starting class SNEAK_ATTACK which is already UP_ONLY capped at 2 uses/combat) are intentionally less powerful than level-15 abilities.
Power Curve — Newbie Powers Should Never Outperform Late-Game
When adding a power, check:
- Is this power accessible at level < 10? Its per-combat ceiling must be strictly lower than a comparable level-15+ power.
- Does it match the power-tier of the cost? Class-locked and clue-locked powers should be stronger than picked powers at the same level.
- Does an early-access version exist that is functionally equivalent to a late-access version? If so, the early version must be weaker (lower damage multiplier, narrower enemy cap, shorter duration).
The Sneak-Attack Family (Post-Audit Status)
Three powers implement stealth-like behaviour. The audit confirmed each stays balanced under the new caps:
| Power | Level | Special | Per-Combat Cap | Notes |
|---|---|---|---|---|
SNEAK_ATTACK (Backstab) |
1 (class) | UP_ONLY |
2 | Massive damage but enemies still on the field next round |
ASSASSINATE |
1 (class) | UP_ONLY |
2 | Same pattern; higher damage multiplier reflects class cost |
INVISIBILITY_TRIGGER |
5 | INVISIBLE_BOOST |
2 + enemy-count gate | Now capped at ≤2 enemies AND ≤2 uses per combat |
CHANCE_INVISIBILITY |
10 | INVISIBLE_BUFF (passive 25%) |
— | Passive proc; remains uncapped because it cannot be deliberately chained |
Crucially, resting, defeating the last enemy, or fleeing all clear actionUsage counters via clearCombat() — so caps do not bleed across encounters. The player is never permanently denied a power, only prevented from looping it within one fight.
Class-Gated Power Picker — Withering Touch Gap ( review, fb 9683)
Status: FIXED (2026-07-16).
Player report: Withering Touch is missing from the reset/upgrade Powers picker for the Warlock class
("the wall class" in the player's phrasing). Content-data check (dark_citadel.rule_dark_citadel_common)
confirmed the prerequisite chain SHOULD resolve cleanly:
POWER_WITHERING_TOUCH—sub_type: CASTER,canBePicked: 1,darkTowerPowerTypesRequired: LIFE_DRAIN.CLASS_WARLOCK—sub_type: CASTER,classPowerTrained: LIFE_DRAIN.
A Warlock trains LIFE_DRAIN as its class power, which is the exact prerequisite Withering Touch requires, so it should surface as pickable once the player has that class power. The content row exists and was correctly gated — the gap was a picker/filter bug, not missing content.
Root cause. DarkTowerPower.hasPrerequisitePower (backing canBeSelectedBy, the filter every power
in the "All" picker is checked against) only scanned the character's chosen/gear power slots
(chosenPowerFirst/Second/Third + gearPowers) for the required prerequisite power. A class's trained
power (e.g. Warlock -> LIFE_DRAIN) is never materialized onto /
secondaryClassPower — those fields are only ever nulled by Reset, never written by class selection — so
a class-granted prerequisite could never be recognized, even though combat's own "has this power" check
() correctly reads primaryClass.getClassPowerTrained(). Any power
whose prerequisite is a class-trained power (not a picked one) was unreachable in the picker.
Fix. DarkTowerPower.hasPrerequisitePower now also checks
character.getPrimaryClass().getClassPowerTrained() and
character.getSecondaryClass().getClassPowerTrained() against darkTowerPowerTypesRequired, matching
the source set already uses for combat. The private powerStream
helper backing isAlreadyChosen/hasPrerequisitePower was also extended to include gearPowers (it
previously covered only the three chosen slots), for consistency with the same canonical power-source set.
Verified live (Warlock, level 30): "upgrade" -> "all" -> "more options" now lists Withering Touch among the 30 selectable powers (was 29, missing it, before the fix); selecting it by name confirms "You've chosen Withering Touch." Regression guard: scenario_test 2819.
Title System
Status: SHIPPED (2026-05-16) — and the title cache fields are live; regression coverage is the fb-917 suite (scenario_tests 1181-1195). Full design in economy.md § Title Shop.
Identity Extension
Two new character cache fields support active and owned titles:
currentTitle: String // Active title, prefixed to character name in narrator copy
ownedTitles: Set<String> // Permanently unlocked titles, purchased from Title Shop
Display Name Pattern
When narrator copy references the player, it should call , which returns currentTitle + " " + characterName when a title is set, or just characterName otherwise.
Use getDisplayName() in: welcome / greeting copy, achievement announcements, level-up announcements, paragon milestone announcements, world events, chat broadcasts (chat-shortcut commands from 19084), auction win announcements.
Stay first-name-only in: NPC card dialog (close interpersonal voice), internal monologue / cinematic narration where formal address would break tone.
Acquisition
Titles are acquired exclusively through the Title Shop (gold cost + milestone gate). They cannot be earned directly; the underlying milestone (level threshold, devotion 100%, raid clear, enemy kill count) is the eligibility check that lets a title appear in the rotation, but the player still pays gold to claim it. This keeps gold-for-prestige distinct from gold-for-stat (declined in round 3).
Cross-References
- Catalog, pricing ladder, rotation mechanic, NLU surface, regression tests: see economy.md.
- Voice rules for displayed names: see voice-tags.md.
Paragon points begin at level fifty
Paragon points begin at PARAGON_START_LEVEL, fifty, and a point comes every ten levels past it: max(0, (level - 50) / 10). A player below it hears the level they start at. is the definition.