Skip to main content

Exploration

Exploration

Status: IN PROGRESS — Tracked open issues; see Known Issues section

Last updated: October 30, 2026

Dark Tower -- Exploration & Activities

Status: IN PROGRESS — Tracked open issues; see Known Issues section

Subsystem: Free Explore zones, mines, training facilities, fishing, teleport, dungeons, crafting Primary sources: , , DarkTowerFreeExplorePoint, DarkTowerTeleportType,


Free Explore World

The Free Explore system provides an open-world layer on top of the card-based narrative. Cards with freeExplorePoint set belong to the world map. Each zone type provides different activities, resources, or strategic advantages.

Entering Free Explore

The Free Explore world is reached from the main menu (card 1) "free explore" option, which routes to the world-map hub (card 50: world, space, ocean, guild hall, keep). Players can also open it directly by voice from anywhere in the story with "free explore", "explore", "explore the world", "world map", or "go exploring" — these route to DARK_TOWER_FREE_EXPLORE (), which jumps to the hub card. The same entry is surfaced as a "free explore" option under the EXPLORE section of the Dark-Tower-specific command list (). The direct command is refused during combat. Guarded by scenario_test 1858.

Zone Types (31 Defined)

Source: llitd.model.darktower.enums.character.DarkTowerFreeExplorePoint

God Prayer Points (8)

Zone Purpose
ELYSIUM_PRAYER_POINT Pray to Elysium
MERRIS_PRAYER_POINT Pray to Merris
DESTIN_PRAYER_POINT Pray to Destin
TORRENT_PRAYER_POINT Pray to Torrent
PERRIK_PRAYER_POINT Pray to Perrik
APOLLO_PRAYER_POINT Pray to Apollo
VENUS_PRAYER_POINT Pray to Venus
POSEIDON_PRAYER_POINT Pray to Poseidon

Minimum level 25 required to pray. Each god has exactly one prayer point on the world map.

Invasion Assault Points (5)

Zone Reward Type
INVASION_ASSAULT_GOLD_POINT 2x gold mine rewards
INVASION_ASSAULT_WOOD_POINT 2x wood mine rewards
INVASION_ASSAULT_IRON_POINT 2x iron mine rewards
INVASION_ASSAULT_TOKEN_POINT 2x token mine rewards
INVASION_ASSAULT_ITEM_POINT 2x item mine rewards

Keep Lookup Points (8)

Zone Structure
KEEP_LOOKUP_POINT_STABLES Stables building
KEEP_LOOKUP_POINT_ARMORY Armory building
KEEP_LOOKUP_POINT_BAKEHOUSE Bakehouse building
KEEP_LOOKUP_POINT_WALLS Walls fortification
KEEP_LOOKUP_POINT_CHAPEL Chapel building
KEEP_LOOKUP_POINT_MOAT Moat fortification
KEEP_LOOKUP_POINT_BARRACKS Barracks building
KEEP_LOOKUP_POINT_SILO Silo storage

NPC Purchase Points (1)

Zone Card Keeper
NPC_ARISTOTLE_PURCHASE_POINT 2763, the guild stock room (C-4) Aristotle, the stock room's manager

The point marks the card where a named keeper keeps their wares; the card's own options carry the exits and the keeper's line. No handler reads it, and it gates nothing: the shop opens with buy from any card. It is appended after NONE because the card cache writes this enum by ordinal. A point a card names that the enum does not define loads as a null freeExplorePoint (READ_UNKNOWN_ENUM_VALUES_AS_NULL), so the card plays as a plain card; guarded by .

Mines and training facilities remain intended content. The Java handlers (, ) and DarkTowerFreeExplorePoint enum values are live. Do not delete the handlers while this section still lists the points.

Training facilities are routed: the action DARK_TOWER_RECRUIT_UNITS dispatches to , and "recruit" or "recruit " says it in DARK_TOWER context. The handler reads the card's freeExplorePoint, so a player recruits only on a card that carries one of the five facility points; anywhere else it says they must be at a training facility. What is still missing is the cards: a FREE_EXPLORE card per facility, carrying its freeExplorePoint and a "Recruit" option whose freeTextDeclarationAction is DARK_TOWER_RECRUIT_UNITS, with a neighbouring card's option leading to it. That is a game-content import into dark_citadel_card.

Mines are not routed yet: no action, dispatch row or one-shot reaches , and no card carries a mine point.

Mines (4)

Zone Resource
GOLD_MINE Gold currency
SPECIAL_TOKEN_MINE Special tokens (rare)
RESOURCE_ONE_MINE Wood (wooden logs)
RESOURCE_TWO_MINE Iron (iron sheets)

Training Facilities (5)

Zone Unit Type
SOLDIER_TRAINING_FACILITY Soldiers
ARCHER_TRAINING_FACILITY Archers
KNIGHT_TRAINING_FACILITY Knights
ARTILLERY_TRAINING_FACILITY Artillery
CANNON_TRAINING_FACILITY Cannons

Free-Explore Combat Rewards

Source: DarkTowerBasicFeatures.grantFreeExploreKillGold, ,

Roaming enemies on the open-world map are fought via free-explore cards (typeOfCard = FREE_EXPLORE). Roughly half of all enemies (146 of 297), including all six elementals, carry the ENEMY_REWARD death reward, which grants only experience and no gold — so defeating them left the player with nothing tangible (feedback 8742).

When an enemy dies on a FREE_EXPLORE card and its death reward grants no gold, combat now queues a supplemental gold drop:

base gold = 15 + max(1, enemyLevel) * 4

queued through the standard pending-reward pipeline, so it is announced and difficulty-boosted (EASY x2 .. HELLSCAPE x4), gold-find-buffed, and new-player-capped exactly like every other reward (see economy.md Free-Explore Combat Gold Supplement). Scoped to FREE_EXPLORE cards (story/dungeon balance untouched) and to no-gold death rewards (so ENEMY_GOLD_REWARD enemies are never double-paid). Scales by the enemy's level, not player level, to avoid a high-level trash-farming faucet, and stays below the gold-mine base (100). Guarded by scenario_test 2072 (3 grounded Slime Monsters on check-free free-explore card 3902; combat starts with no skill check, so the win is deterministic).

Mine Resource Collection

Source:

Mine Reward Formulas

Mine Formula
Gold 100 + (level * 10) + random(0-49)
Special Token 5 + (level / 5) + random(0-2)
Wood 20 + (level * 2) + random(0-9)
Iron 20 + (level * 2) + random(0-9)

Reward Scaling Examples

Level Gold Range Token Range Wood/Iron Range
1 110-159 5-7 22-31
10 200-249 7-9 40-49
50 600-649 15-17 120-129
100 1100-1149 25-27 220-229
500 5100-5149 105-107 1020-1029

Guard Condition

The handler checks isMinePointType() before allowing collection. If the current card is not a mine zone: "You must be at a mine to collect resources."

Training Facility Recruitment

Source: From index documentation (handler not directly read)

Base recruitment: 10 + (level * 2)

Multiplier by facility:
  Soldier / Archer:    1.0x
  Knight:              0.5x
  Artillery / Cannon:  0.3x
Level Soldiers Knights Artillery
1 12 6 3-4
50 110 55 33
100 210 105 63

Fishing System

Source:

Overview

Fishing is a multi-phase activity available at cards with collectibleToFishOrHunt set to a fish collectible type.

Go Fishing is always offered on such a card. offers it (or Go Hunting) on every turn at the card, beside paragon or anything else, and on the turn after a missed command as well. Its synonyms ("fish", "go fish", "fishing") exist only on that option, so a turn that left it out sent "go fishing" to another option, such as the card's own "done fishing", and the story moved on without a cast.

A net or reel option's key leads with its action. Picking a net carries WAIT-<net>-<bonus>-<fish> and reeling carries REEL-.... handleFishing reads that first word before any other, because a fish name can hold one of the words it looks for (CATFISH and SWORD_FISH hold FISH, which reads as start).

Resolution fix: fishings catch path had been broken because the collectible wrapper-service registry never resolved any collectible. The rule rows store primaryEnumKey with a COLLECTIBLE_ prefix (COLLECTIBLE_COD) while the enum constants are unprefixed (COD), so every primaryEnumKey deserialized to null and getCollectible(type) missed for every collectible — the fish/hunt sub-type checks then saw null and replied "cannot fish or hunt." A normalizing @JsonCreator on DarkTowerCollectibleTypes now strips the prefix so both forms resolve. This un-broke the net-selection flow (un-quarantined scenario_tests 963/964) and is guarded by 2431.

Phase Flow

1. startFishing() -- Player selects "fish" at a fishing card
   - Blocked if in combat or other action
   - Checks for fishing net in inventory
   - If no net: offers to buy FLIMSY_NET from shop
   - Player picks net: FLIMSY, FABRIC, WIRE, or WELDED_MESH
   - Initial bonus starts at 5

2. keepWaiting() -- Tug phase (repeatable)
   Roll 1d3:
     1: Big tug   -> bonus += rollDice(200) + 100  (range +101 to +300)
     2: Bad tug   -> bonus -= (rollDice(100) + 50)  (range -51 to -150)
     3: Neutral   -> no change

   Player chooses:
     "Reel In"    -> proceed to reelItIn()
     "Keep Waiting" -> another tug phase
     "Give Up"    -> CONTINUE (stop fishing)

3. reelItIn() -- Catch resolution
   If bonus > 900: Fish escapes ("waited too long"), net consumed
   Otherwise:
     catchRoll = rollDice(1000)
     threshold = min(900, bonus + netBonus - rarityPenalty + fishingBonus)
     If catchRoll <= threshold: CATCH TARGET FISH
     Else: catch random GRAY rarity fish instead

Net Capture Bonus

netBonus in the threshold formula is DarkTowerItem.chanceCaptureBonus, an OPTIONAL rule field. No ITEM rule row sets it -- all 44 items, including all four nets, omit the key -- so it resolves to null and contributes 0. Consequence: the four nets are currently mechanically identical for catch chance and differ only in shop price. Both consumers must therefore read an unset bonus as zero: does so via .orElse(0L), and now does the same -- unboxing it directly threw a on every reel-in and killed the turn with the INTERNAL_ERROR response. Giving each net a distinct capture bonus is an open content/balance decision, not a code gap.

Rarity Penalties

Source: switch statement

Fish Rarity Penalty
WHITE -200
RED -200
DANTRINE -200
INDIGO -200
BLACK -300
ORANGE -100
BLUE -50
All others 0

Catch Amount

baseCatch = rollDice(3)  // 1-3 fish
fishingYieldStacks = effectStacks(EFFECT_FISHING_YIELD_BOOST)
totalCatch = baseCatch + fishingYieldStacks

Fishing Bonus Calculation

Source:

fishingBonus = effectStacks(EFFECT_FISHING_YIELD_BOOST) * 50

Each stack of the fishing yield buff adds +50 to the catch threshold AND +1 to fish per catch.

Daily Fishing Bonus

First fish of the day: +100 special tokens (18-hour cooldown between bonuses).

Bonus Random Item

On catching a gray-rarity fish: 1 in 20 chance (5%) of finding a random item reward, throttled to once per 10 minutes.

Net Consumption

Nets are consumed on each catch attempt, regardless of success or failure. If the fish escapes (bonus > 900), the net is still consumed.

Hunting System

Source:

Hunting is the land-animal counterpart to fishing, available at a card whose collectibleToFishOrHunt is a land-animal collectible (a quarry whose specificSubType is ANIMAL_MAMMAL_TYPES, ANIMAL_BIRD_TYPES, ANIMAL_INSECT_LIZARD, or ANIMAL_MONSTER_TYPES). The same DARK_TOWER_FISHING START entry (and the new hunt / go hunting / track NLU) drives it; startFishing inspects the quarry sub-type and dispatches a fish target to the net loop and a land-animal target to startHunting.

Flow

Unlike fishings multi-tug net loop, hunting is a single stalk-and-take roll:

threshold = HUNT_BASE_CHANCE (450) + level * HUNT_LEVEL_SCALE (9) - rarityPenalty(quarry)
success if rollDice(1000) <= threshold
  • Success: add 1-3 of the quarry collectible to inventory; the first hunt of the day grants +100 special tokens (18-hour cooldown), mirroring fishing.
  • Miss: the animal catches the hunters scent and escapes (nothing gained, nothing consumed).
  • Rarity penalty bands mirror fishing (WHITE/RED/DANTRINE/INDIGO -200, BLACK -300, ORANGE -100, BLUE -50).

A higher-level hunter reliably takes level-appropriate quarry while a novice has a real chance to be eluded. The wrapper surfaces the affordance as Go Hunting (vs Go Fishing) on a hunting card. Guarded by scenario_test 2429 (deterministic catch at seeded level 100).

Content note: the authored card corpus currently sets collectibleToFishOrHunt only to fish collectibles, so no card routes a real player to hunting yet. The hunting SYSTEM is complete and tested (via the setFishHuntTarget test seeder); surfacing it to players is a content step — author hunting grounds (free-explore cards with a land-animal collectibleToFishOrHunt).

Combat Capture System

Source:

Overview

During combat (level 3 or higher) the player can capture a living enemy that carries a captureTarget collectible, turning a fight into a creature for their collection. Capturable enemies are mostly the ANIMAL subtype. Capture follows a pick, then hold, then throw flow that mirrors the fishing tug loop: the longer you hold the net the better your odds, but the enemy strikes you each turn you wait.

Phase Flow

1. attemptCapture() -- Player says "capture" in combat
   - Blocked if not in combat, below level 3, or no capturable enemy is present
   - Lists each distinct capturable enemy as a target option
     (key START-<ENEMY>-NONE-10, starting bonus 10)

2. openHoldStep() -- Player picks a target
   - Surfaces the live capture odds for that target BEFORE any throw, including the
     enemy-health band narration, so the chance is seen to respond to how wounded the
     enemy is (feedback 8658)
   - Offers "Hold the net" (HOLD-<ENEMY>-<NET>-<BONUS>) and "Throw now"
     (THROW-<ENEMY>-<NET>-<BONUS>), plus a none/continue option
   - No enemy strike on this first entry -- the player only just chose a target

3. keepHoldingNet() -- Player chooses "Hold the net" (repeatable)
   - Charge rises by HOLD_CHARGE_PER_TURN (+100, about ten percentage points)
   - The enemy takes one free strike (enemyAttacksAndStillAlive); if it drops the
     player below zero hit points the last-stand flow takes over
   - Otherwise re-presents the hold-or-throw menu with the higher carried bonus

4. processThrowNetKey() / executeNetThrow() -- Player chooses "Throw now"
   - Resolves the capture at the carried bonus: rollDice(1000) <= captureChance
   - Success: capture the collectible, grant the companion-unlock clue, consume the
     net, remove the enemy; first capture of the day grants +100 special tokens
   - Failure: the enemy shakes the net off
   - Either way the enemy then attacks (processEnemyAttacksAfterThrow)

Capture Chance

Source: (measured in thousandths, shown to the player as a whole percent)

chance = enemy.captureChance
       + heldBonus            // +100 per turn held; +200 more with the CAPTURE_MASTER power
       + net.chanceCaptureBonus   // 0 for the bare-hands NONE net
       + timeoutBonuses       // +50 if no net thrown in 15 min, +100 if none in 2 min
       + enemySubTypeModifier // see table
       + healthPercentModifier    // see table -- the dominant term
chance = clamp(chance, 30, 970)   // floor 30 = 3 percent, ceiling 970 = 97 percent

Enemy Sub-Type Modifier

Sub-Type Modifier
DRAGON -500
HUMANOID -100
UNDEAD -70
ANIMAL +50
All others 0

Enemy-Health Modifier

The single biggest lever -- capture a wounded enemy, not a fresh one. Each band narrates a line so the player hears the odds respond to damage.

Enemy Health % Modifier Narration
> 80% -1400 "drastically reduces your chances"
> 60% -1000 "greatly reduces your chances"
> 40% -700 "reduces your chances"
> 20% -350 "reduces your chances by a small amount"
<= 20% +50 "much easier to capture"

At full health the -1400 penalty pins the chance to the 30 (3 percent) floor for most enemies, so holding alone cannot make a fresh enemy a likely capture -- the player is meant to weaken it first. Because the held bonus and the health band are both shown in the hold step preview, the announced chance visibly differs between a high-health and a low-health enemy (proven by the hermetic DARK_TOWER scenario test for the hold option plus JUnit for the high-vs-low difference).

Hold-step copy must match the floor behavior (UX). When the enemy-health band pins the chance to the 3 percent floor, the hold-step preview must NOT promise the percentage will rise from holding -- doing so contradicts the player's experience (holding while a fresh enemy attacks leaves the displayed percent at 3). presentHoldMenu() computes whether one more hold (adding HOLD_CHARGE_PER_TURN then re-clamping) would actually raise the displayed whole percent: if yes it offers "Hold the net to charge it higher"; if no (the floored full-health case) it tells the player the enemy is too healthy and to wound it in battle first -- and, for ANIMALS, that they can weaken it. This keeps the spoken guidance truthful against the intentional weaken-first formula without changing any math. Source: feedback 9061 / 9062 / 8961 / 8962 / 8963 ("the percentage never changes no matter what you do"); guarded by DARK_TOWER scenario test 2193 (sibling of 2070, which guards the unchanged menu shape and health-responsiveness narration).

Weaken Option

Source: -- "weaken" in combat. On a successful ANIMAL_HANDLING HARD check, halves the weakest capturable animal's current hit points. Only animals can be weakened, not monsters. Either way the enemy then attacks.

Net

The combat-capture flow uses a bare-hands net (NONE): no net item is consumed and no net bonus applies, so the "net" is narrative. Net-item selection was retired and endWithPickingNet is inert.

Teleport System

Source: DarkTowerTeleportType

Teleport locations are gated by clues. The baseTeleportMenu() method filters available locations by discovered clues. Players unlock fast travel by finding location clues during exploration.

Available teleport types are defined in the DarkTowerTeleportType enum and mapped to specific card IDs.

  • Opening the menu. Saying "fast travel" or "teleport" (DARK_TOWER_FAST_TRAVEL, DARK_TOWER_FAST_TRAVEL_BASIC) opens the menu, and so does the "fast travel" option on the main hub (DARK_TOWER_TELEPORT_MENU). Both reach .
  • Rows. Each location is a dark_citadel.rule_dark_citadel_common row with rule_type = 'TELEPORT_LOCATION', a text rule_id (TELEPORT_1 to TELEPORT_11) and the location JSON in rule_data. A location's darkTowerClueTypes must name a DarkTowerClueTypes member; a name that is not one is refused, never read as no requirement. Regression: , .

Dungeons

Three dungeon types are available from the world map:

Dungeon Card Count Entry
Lich Lair ~500 cards From world map
Goblin Dungeon ~400 cards From world map
Graveyard ~328 cards From world map

Entering a dungeon saves the world map position. Exiting returns to the saved position.

Dungeon rewards use forced NIGHTMARE difficulty for loot rolls (see ).

Activities Summary

Activity Key Mechanic Handler
Fishing Multi-phase timing game with nets
Mining Level-scaled resource collection
Arena Wave-based combat challenges, leaderboard --
Tower Defense Defense-style, score-based ranking Uses
Crafting Recipe-based item creation from gold, wood, and iron
Hunting Single-roll stalk-and-take for land-animal quarry; level-scaled catch chance

World Map Coordinates

Source: DarkTowerCard coordinate fields

Cards have 3D coordinates for world map positioning:

coordinatesX: Integer
coordinatesY: Integer
coordinatesZ: Integer

These are used for the visual world map (currently missing from frontend -- see index.md frontend gaps).

Bounty System

Source:

Map<DarkTowerEnemyTypes, KeyValueLongLong> activeBounties

Bounties track kill targets for specific enemy types. The KeyValueLongLong likely stores target count and current progress.

Bounty completion triggers COLLECT_BOUNTY_COMPLETION_REWARD.

Upgrade System

Source: DarkTowerUpgradeTypes

Upgrades are persistent improvements purchased with resources:

DarkTowerUpgradeTypes includes:
  GUILD_TAVERN  // +2% HP per level (used in calculateHitPoints)
  // Other upgrade types defined in the enum

Upgrade levels are queried via .

Guild System

Source: DarkTowerUpgradeTypes (guild upgrades), DarkTowerGuildRank, ChatGuild* classes,

Overview

Guilds in Dark Tower are a cross-system feature spanning the chat system (social) and the upgrade system (gameplay bonuses). Guild management (creation, joining, inviting, leaving) is handled by the chat system, while guild buildings provide gameplay bonuses via the upgrade system.

Guild Ranks

Source: llitd.model.darktower.enums.character.DarkTowerGuildRank

Rank Role
LEADER Guild founder/owner
MARSHAL Senior officer
LIEUTENANT Junior officer
MEMBER Standard member

Guild Operations (Chat System)

Source: llitd.engines.handlers.common.chat.guild.*

Operation Handler Description
Create guild Create a new guild
Join guild Request to join
Leave guild Leave current guild
Invite to guild Invite player
Accept invite Accept pending invite
Approve join request Approve/deny join requests
Guild settings Modify guild settings
Member management Promote/demote/remove members
Guild wars Guild vs guild competition

Guild Buildings (Upgrade System)

Source: llitd.model.darktower.enums.character.DarkTowerUpgradeTypes (all DarkTowerResearchType.GUILD)

Building Upgrade Key Effect
Guild Hall GUILD_HALL Core guild building
Guild Inn GUILD_INN Guild social space
Guild Headquarters GUILD_HEADQUARTERS Guild command center
Guild Prison GUILD_PRISON Guild justice system
Guild Warehouse GUILD_WAREHOUSE Increases guild shared inventory capacity
Guild Tavern GUILD_TAVERN +2% HP per level (confirmed in calculateHitPoints())

Guild Tavern HP Bonus (Verified)

Source: .java:106-110

final long guildTavernLevel = getUpgradeLevel(gameBasicProperties, DarkTowerUpgradeTypes.GUILD_TAVERN);
if (guildTavernLevel > 0) {
   basePercent[0] += guildTavernLevel * 0.02;  // +2% HP per level
}
Tavern Level HP Bonus
1 +2%
5 +10%
10 +20%
20 +40%

Guild Reward Loot Tiers

Source:

Reward Type Forced Difficulty
GUILD_REWARD_MINOR HARD
GUILD_REWARD_MAJOR HARD
GUILD_REWARD_EPIC NIGHTMARE
GUILD_REWARD_PURCHASE HELLSCAPE

Guild Wars System

Source: llitd.model.guildwars.*

The Guild Wars subsystem has dedicated models for seasons, contributions, and stats:

Model Purpose
GuildWarsSeason Season tracking with status enum
GuildWarsGuildStats Per-guild season performance
GuildWarsPlayerContribution Individual player contributions
GuildWarsContributionLog Contribution history
GuildWarsActionType Types of guild war actions
Contribution calculation logic

Wave Combat System

Source: DarkTowerStatus.WAVES, DarkTowerRewardTypes.WAVES, DarkTowerItemRarity.WHITE

Overview

Tower Waves is a standalone endurance mode reached from the waves menu (card 150) by saying "waves" or "start waves". It is a gauntlet of successive, escalating enemy waves: each wave is a small group that grows by one body per wave, the player does not heal between waves within a run, and the run ends when the player dies. The best wave reached is tracked as the rank and is kept as the player's BEST (never overwritten by a weaker later run). This is the "successive groups of enemies" + "score and ranking tracking" the design intends.

History (player_feedback 9065, owner decision 2026-06-20): an earlier rewrite collapsed the mode into a SINGLE dynamically-spawned mega-pack (8-to-10 zombies + 7-to-8 skeletons scaled by level), with no sequential waves, no rank readout, and a defeat that wrongly narrated "Victory!" plus a duplicated diamond upsell. Even a level-500 character could not clear the one giant fight, and the run had no payoff — the "I dont like the new version of waves" report. The owner directed: do NOT revert to the legacy authored wave-card chain (an old-game artifact); make the NEW mode compelling. It was rebuilt into the real escalating gauntlet described below.

How It Works (live mode)

Concern Behavior
Entry heals the player to full (a fresh run starts fresh; the no-heal rule is WITHIN a run), surfaces the best-wave goal, then spawns wave one. Below the run opens with a spoken warning (lowLevelWarning): a level 1 character clears wave one and falls in one round of wave two. It warns rather than refuses, because how far a run gets is the score and the player may still choose it.
Wave composition + scaling (pure, unit-tested): waveEnemyCount = WAVE_BASE_ENEMIES (3) + one per wave, capped at WAVE_MAX_ENEMIES (12); each normal wave draws a fresh 2-to-3 type mix from a depth-tiered pool (EARLY waves 1-5, MID 6-10, LATE 11-20, ELITE 21+, every entry verified-present in rule_dark_citadel_common so no enemy-not-found risk), rotated by wave so consecutive waves are not the same fight; every BOSS_EVERY (10th) wave is a single named BOSS on an escalating roster (Goblin King, Minotaur, ... Dragon, Tarrasque) escorted by up to BOSS_MAX_MINIONS (4) tier minions; waveStatDifficulty ramps with character level AND wave depth, accelerates past POST_RAMP_WAVE (10), and adds a boss premium so deep runs and bosses bite.
Wave advance on victory detects an active run (guarded to non-card combat) and calls spawnTowerWave(waveNumber+1) in place — the next, larger wave begins immediately with NO heal. Narrated "Wave N cleared. The next wave presses in. Wave N+1." Each wave also grants its own victory XP/reward.
Rank tracking Each cleared wave is recorded via into towerStats["TOWER_WAVES"] (highestWaveReached = best, totalWavesCompleted incremented). The rank is the BEST run.
Death / run end continueFailureActions reads the run state BEFORE clearing it and speaks the rank readout ("You held the line through N waves. Your best run is wave M. Say start waves to try again."), then ends the run. The duplicate post-death diamond upsell is suppressed in waves (the drop prompt already shows one nudge).
Run-state safety inTowerWaves / towerWaveNumber live on ; populateEnemies clears inTowerWaves at EVERY combat start (re-set by the two waves entry points), so a stale flag can never make a story or free-explore victory spawn a wave.
No Victory-on-loss combatOutcomeResolved (set when an outcome resolves, reset in populateEnemies) guards so the drop-to-zero path's 30 percent heal can no longer make handleCombatEnd re-check HP and wrongly fire the Victory branch on a loss. This also hardens the general fb-927 fix.

Regression shield: (pure JUnit) pins the wave-size, composition, stat ramp, and best-wave rank math — the live fight is RNG-driven so a value assertion on a real fight would be brittle (.claude/rules/hermetic-scenario-tests.md). game_test.scenario_tests 2294 (DARK_TOWER) asserts the deterministic entry: saying "waves" announces "Wave 1" and renders combat options (a small first wave, not the old mega-pack). Source feedback 9065.

Variety, bosses, and post-wave-ten escalation (2026-06-28, player Lauri): the original rebuilt mode fielded only zombies and skeletons (plus orcs from wave four) with no bosses and a single linear stat ramp — player Lauri (lauri2023@gmx.de, 2026-06-27) reported it as "the same fight ... four orcs and four skeletons ... boring after wave 10". waveEnemies now draws a fresh two-to-three type mix from a depth-tiered pool of verified-present enemies, every tenth wave is a named boss with an escalating roster, and waveStatDifficulty accelerates past wave ten and adds a boss premium. was extended to pin the variety, the boss cadence + escalation, and the steeper post-ten ramp. A boss/variety check cannot be a single-command scenario_test (reaching wave ten requires winning nine RNG fights), so JUnit stays the regression surface per the hermetic-test rule; scenario_test 2294 still guards the deterministic wave-one entry. Still deferred to a fast-follow: a per-milestone reward ladder (an item every tenth wave).

Legacy wave-cards (NOT the live mode): the authored WAVES_CARD chain (cards 600-899, ORC → ACOLYTES → BEAR → ...) is the predecessor's hand-authored gauntlet and is the old-game artifact the owner declined to revert to. The live mode is the dynamic -driven loop above, not these cards.

Pending content step (apply at deploy): the waves menu card 150 still reads "only your most recent rank will count as your total value" — that contradicts the best-wave tracking this mode now does. Update card 150's rules text (prod dark_citadel_card, then evict STORY_CARD_LIST_CACHE_V2) to describe the BEST-run rank when the code deploys, so the menu copy matches behavior.

Reward detail Status
Reward type DarkTowerRewardTypes.WAVES — forced NIGHTMARE difficulty loot
Exclusive rarity DarkTowerItemRarity.WHITE — items only obtainable from waves
Wave stat tracking towerStats in — best wave + completion count (key TOWER_WAVES)

Invasion System

Source: DarkTowerStatus.INVASION_WON, DarkTowerStatus.INVASION_LOST, , DarkTowerFreeExplorePoint.INVASION_ASSAULT_*

Overview

Invasions are event-based content where players compete at invasion assault points on the world map. The system provides enhanced resource rewards (2x base mine rewards) at contested points.

PvP Keep Assault -- the migration target (player Lauri 2026-06-27; design locked 2026-06-28)

The implementation documented below is the ORPHANED VESTIGE. The old Dark Citadel had a real player-versus-player invasion: pick a live rival, travel to an assault point, and steal their gold through an attack-versus-defense contest. Player Lauri (lauri2023@gmx.de) asked for it back -- "steal gold from other players' ships or keeps ... improve the rates." This is the spec for the PvP-only migration into Dark Tower.

Locked design decisions (owner, 2026-06-28):

  • Scope: PvP-only. Build the steal-from-other-players half. The old PvE half (AI armies raid YOUR keep for event points) is DEFERRED, not built now.
  • "Ships" = a naval-skinned assault point. Ships never existed as a mechanic (Warship is a separate Battleship minigame). A ship target is a sea-themed flavor of the SAME keep-assault mechanic -- one system, two skins (keep / ship). No Warship tie-in.
  • Steal rates: moderate retune. The old GOLD protected% was 1.0 (100 percent), so the unprotected pool was always zero and every target -- rich or poor -- yielded only the flat floor, which felt pointless. Retune to the moderate band below.

Steal math (ZERO-SUM, pure + unit-tested in ): the steal is a TRANSFER, not a faucet -- the attacker gains EXACTLY what the victim loses, so an invasion can never mint new gold into the economy (owner-approved 2026-06-29, refining the earlier asymmetric-minting design).

  • EXPOSED_PCT = 40 -- only 40 percent of the victim's gold is exposed to a steal; 60 percent is safe.
  • ATTACKER_TAKE_PCT = 80 -- a winning attacker takes 80 percent of the exposed pool, so the net steal is stolen = round(victimGold * 0.40 * 0.80) = round(victimGold * 0.32).
  • FLAT_FLOOR = 15000 gold -- a minimum steal, but ONLY from a victim who actually holds at least 15,000 gold (so a broke target yields almost nothing instead of paying a floor out of thin air); the steal is never more than the victim has.
  • The victim loses exactly stolen and the attacker gains exactly stolen, so gold_minted is always 0. A failed assault steals nothing.
  • Worked example: a 1,000,000-gold target -> attacker gains 320,000 AND victim loses 320,000; a 10,000-gold target -> 3,200 each way; a 0-gold target -> 0.

The assault contest: an attack-versus-defense roll (port the exploding-d20 from the old ). Keep the resolver PURE (takes the two scores / seeded rolls, returns the winner) so it is unit-testable like the wave math; the handler supplies the RNG.

Build stages — stages 1-6 SHIPPED 2026-06-29 (PvP gold-steal MVP):

  1. ✅ (pure: the ZERO-SUM steal math + the attack-versus-defense contest resolver + the 6-hour anti-spam cooldown) + JUnit .
  2. ✅ Target selection — the attacker's guild roster IS the rival pool (a social gate: no guild means no targets). walks the guild members, resolves each chat_ref to the player's Alexa customer id (linked players via chat_skill_xref, unlinked via chat_account.email_uniq), keeps only loadable Dark Tower players, and the chosen target's customer id is stored on . (Chosen over a global "find any rival" query -- a guild gives a social reason to raid and a bounded, consenting target set.)
  3. ✅ The assault flow — runs the contest (attacker level + hero-team power vs the victim's, exploding-d20), CREDITS the attacker the exact stolen amount and DEBITS the victim through (the live cache write-back path under the per-customer currency lock, never raw SQL). The attacker credit is applied directly to the seasonal stash, bypassing the double-money-weekend multiplier in addMoney so the transfer stays zero-sum.
  4. ✅ Cooldown — stamps a 6-hour per-attacker cooldown (stamped on BOTH a win and a loss); isCooldownReady / minutesUntilReady gate and narrate it.
  5. ✅ Log table dark_citadel.invasion_attack_log (mirror of story_log.story_invasions_attack_log) + — one row per attempt (scores, won, gold_stolen, gold_minted=0).
  6. ✅ NLU — the DARK_TOWER_INVASION action routes to (key-dispatch: no key lists targets + cooldown status, TARGET_n selects a rival, ASSAULT resolves). Cold-words invade / invasion / invasions / steal gold / raid a keep / invade a keep / raid a player (bare raid is left to DARK_TOWER_RAID_STATUS). Action + one_shots seeded prod enabled=0 / local enabled=1; the prod handler was flipped to enabled=1 on 2026-07-04 (config-drift remediation, report_findings 1085 / ) once the invasion code was confirmed live on prod — prod invasion is now live.
  7. ☐ Naval skin (DEFERRED) — a sea-themed "ship" flavor of the same keep-assault mechanic (one system, two skins). The mechanic is live; the skin is content/copy, a fast-follow.

Code: service/darktower/invasion/, engines/handlers/darktower/invasion/ , mappers/darktower/. The card-spawned INVASION_ASSAULT_* world-map points are NOT used by the PvP flow (they stay the orphan reward vestige); the invasion is reached by the invade voice command + the guild roster instead.

Why this is a careful, dedicated build (not a quick slice): stage 3 mutates a SECOND real player's persisted gold balance. Done wrong it corrupts a live player's economy, and it cannot be tested hermetically with a single player. Build it deliberately with the cross-player writeback path verified, not rushed.

What Exists

Component Status
Card status types INVASION_WON / INVASION_LOST — post-event narrative cards
Assault points (5) Gold, Wood, Iron, Token, Item points on world map
PvP invasion (live) + — the SHIPPED PvP keep-assault gold-steal, reached by the invade command, targeting guild rivals (see the Build stages above).
Invasion point handler (orphan) — captures a flat reward from a world-map assault point. STILL ORPHANED: zero call sites; PvE self-reward only, no PvP theft. The live PvP flow above replaced its intent; this vestige is unused.
Exclusive rarity DarkTowerItemRarity.INDIGO — items only obtainable from invasions
Punishment type INVADED_LOST — penalty for losing an invasion

Invasion Assault Point Rewards

Source: reward formulas

Point Type Formula
Gold 200 + (level * 20) + random(0-99)
Wood 40 + (level * 4) + random(0-19)
Iron 40 + (level * 4) + random(0-19)
Token 10 + (level / 3) + random(0-4)
Item Same as Token (awards special tokens)

These are approximately 2x the standard mine rewards.

Keep Building System

Source: , DarkTowerFreeExplorePoint.KEEP_LOOKUP_POINT_*, DarkTowerUpgradeTypes

Overview

The keep is the player's stronghold containing 8 functional buildings. Each building is visited at a world map location (KEEP_LOOKUP_POINT_*) and provides scaled resource/unit rewards based on character level and building upgrade level.

Keep Buildings (8 Types)

Source:

Building Upgrade Key Primary Reward Building Bonus
Stables STABLES Knights (cavalry) +3 knights/level
Armory ARMORY Iron + special tokens +5 iron, +2 tokens/level
Bakehouse BAKEHOUSE Gold + wood +25 gold, +5 wood/level
Walls WALLS Soldiers + archers +4 defenders/level
Chapel CHAPEL Special tokens + gold +2 tokens, +25 gold/level
Moat MOAT Artillery + cannons +2 siege/level
Barracks BARRACKS Mixed troops (soldiers/archers/knights) +6 troops/level (multiplied by type)
Silo SILO Wood + iron + gold +8 resources/level

Reward Formulas

All rewards follow this pattern:

reward = baseReward + (characterLevel * charScaler) + (buildingLevel * buildingBonus) + random(0 to variance)

Example — Barracks at level 50, building level 5:

Soldiers: (15 + 50*2 + 5*6 + random(0-7)) * 1.0 = ~145
Archers:  (15 + 50*2 + 5*6 + random(0-7)) * 0.8 = ~116
Knights:  (15 + 50*2 + 5*6 + random(0-7)) * 0.4 = ~58

Capture Points

Status: REMOVED 2026-05-12. Capture-point zone types and the isCaptureWorldPointType() utility were deleted from DarkTowerFreeExplorePoint and after the round-3 design decision (fb 718-721) to lock in the buy-only research-station model. Research stations are progression-gated purchases, not contested world-map zones — see progression.md § Research System.

Rest cooldown and city markets

A rest starts its cooldown only when it really rests the player; a refused rest costs nothing (). City card 8120 carries a market and a farm walk as free-explore cards 617200 to 617204; the other cities have none yet.