Skip to main content

Economy

Economy

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

Last updated: October 31, 2026

Dark Tower -- Equipment, Shop & Economy

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

Subsystem: Equipment slots, item types, rarity system, loot rolls, set bonuses, gem socketing, shop, currencies, rewards, punishments Primary sources: , , , , DarkTowerSetPerkTypes, DarkTowerItemTypes, DarkTowerItemRarity


Equipment Slots (15 Total)

Source:

Weapons (3 slots)

mainHand: DarkTowerWeapon     // Primary melee weapon
offHand: DarkTowerWeapon      // Secondary/shield slot
ranged: DarkTowerWeapon       // Ranged weapon

Armor (6 slots)

armorChest: DarkTowerArmor    // CHEST
armorHands: DarkTowerArmor    // GLOVES
armorHead: DarkTowerArmor     // HELMET
armorFeet: DarkTowerArmor     // FEET
armorLegs: DarkTowerArmor     // LEGS
armorShoulder: DarkTowerArmor // SHOULDERS

Trinkets (6 slots)

trinketHat: DarkTowerTrinket
trinketRing: DarkTowerTrinket
trinketNecklace: DarkTowerTrinket
trinketAuxiliary: DarkTowerTrinket
trinketBelt: DarkTowerTrinket
trinketCloak: DarkTowerTrinket

Armor Weight Types

Source: DarkTowerArmorWeightType

Weight Athletic Skill Penalty
HEAVY -32
MEDIUM -16
LIGHT -8
CLOTH 0

Item Rarity System

Source: DarkTowerItemRarity

Standard Rarities (found in world)

Rarity Color Notes
GRAY Gray Common, lowest tier
GREEN Green Uncommon
BLUE Blue Rare
PURPLE Purple Epic
ORANGE Orange Legendary, best standard drop

Special Rarities (restricted acquisition)

Rarity Color Source
BLACK Black Can only have ONE, not found in world
EMERALD Emerald Can have multiple, not found in world
WHITE White Waves content exclusive
INDIGO Indigo Invasions exclusive
DANTRINE Dantrine Dungeons exclusive
RED Red Raids exclusive

Loot System

Source:

Two-Phase Loot Approach

Phase 1: Roll for rarity (1d1000 against difficulty-weighted table)
Phase 2: Pick random item of that rarity from appropriate pool

This prevents bias where common items dominate rare items in mixed pools.

Rarity Distribution by Difficulty

Source: (1d1000 roll)

Rarity EASY HARD NIGHTMARE HELLSCAPE
ORANGE 0.5% (1-5) 5% (1-50) 20% (1-200) 40% (1-400)
PURPLE 1% (6-15) 20% (51-250) 30% (201-500) 30% (401-700)
BLUE 10% (16-115) 30% (251-550) 25% (501-750) 20% (701-900)
GREEN 50% (116-615) 30% (551-850) 15% (751-900) 8% (901-980)
GRAY 38.5% (616-1000) 15% (851-1000) 10% (901-1000) 2% (981-1000)

Item Type Distribution

Source: (1d20 roll)

Rolls 1-2:  ARMOR   (10%)
Rolls 3-4:  TRINKET (10%)
Rolls 5-6:  WEAPON  (10%)
Rolls 7-20: ITEM    (70%)   // Consumable items

Random Roll Processing

Source:

Max rolls per reward: 10 (hard cap)
Excess rolls: converted to bonus gold at 100 gold per roll

For weapons, armor, and trinkets: only unowned items are eligible (prevents duplicates). If no items of the target rarity exist, falls back to any available rarity for that difficulty.

Difficulty Overrides for Specific Rewards

Source:

Reward Type Forced Difficulty
GUILD_REWARD_MINOR, GUILD_REWARD_MAJOR HARD
GUILD_REWARD_EPIC NIGHTMARE
GUILD_REWARD_PURCHASE HELLSCAPE
DUNGEON, RAID, WAVES NIGHTMARE
All others Player's current difficulty

Currencies

Source: (currency moved to via CurrencyLedger)

Currency Source Uses
Gold Combat (incl. free-explore world kills), mines, rewards, excess rolls Shop, crafting, race changes
Diamonds Purchase-only — ISP packs ONLY (no free faucet). Ledger key special_tokens (grandfathered) Diamond store premium gear, enchantments, resets, character slots
Special Tokens Earned — login streak, token mine, keep-building, fishing/hunting/capture dailies, combat/raid/invasion rewards, achievements, milestones, warship. Ledger key dark_tower_tokens (DARK_TOWER_TOKEN) Token Shop, earned-currency sinks
Wood Resource mines, rewards Crafting, keep building
Iron Resource mines, rewards Crafting, keep building
Honor Tokens Achievements Special rewards

Diamonds and Special Tokens are TWO INDEPENDENT CurrencyLedger keys (epic 24205) — an earned faucet never changes Diamonds and a pack purchase never changes Special Tokens. See the § Currency architecture section below and currency-split-blueprint.md.

Currency operations use and (not direct field access).

Shop-Root ISP Entry (-05-24)

Diamonds are the real-money premium currency, sold via the Amazon ISP pack listing rendered by ($1 / $3 / $5 / $10 packs). The gold shop root () now surfaces a Buy Diamonds entry alongside the in-game-gold sinks (Weapons, Armor, Trinkets, Consumables, Token Shop, Title Shop, Permanent Upgrades). It routes through the unified buy router via BASELINE_BUY + , landing the player directly on the pack listing. Before this fix the shop root offered only gold sinks and players had no voice path to the USD pack listing — a monetization gap. See .

Currency Identity Reconciliation (2026-07-13)

Source: 2026-07-12 ISP monetization findings () P0 finding — one premium ledger carried three player-facing names (Diamonds / Special Tokens / gems), so a real paid Diamond grant could "sound missing" when the player later saw the same balance called something else.

Current state (confirmed, not guessed). There is exactly one storage location: CurrencyLedger key special_tokens (), written through with type "DIAMOND". / spendDiamonds() / canAffordDiamonds() all delegate straight to the specialMoney() / spendSpecialMoney() / canAffordSpecial() methods that back the same ledger — this is intentional (feedback 377/378/413 fixed a real bug where fragmented storage across "DIAMOND"/"diamonds"/ "special_tokens" keys caused a post-purchase balance of 0; carries a Do NOT add a "diamonds" constant guard comment for this reason). "Diamonds" is already the dominant player-facing name across the highest-stakes surfaces: ShortfallCurrencyType.SPECIAL_TOKENS displays "diamonds", the ISP pack listing, the Reset/Respec sink, and applyCurrencyPayout / getCurrencyBenefitsText in all say "Diamonds".

What fixed (display-only, no faucet/ledger change):

  • announced a token-priced purchase as "N tokens" while the shortfall path for the identical balance said "diamonds" — the exact "sounds missing" bug. Purchase confirmation and buildPriceText (category-browse price labels) now say "diamonds".
  • 's "what does X cost" readout labeled priceOfTokens items "tokens" while priceOfDiamonds items said "diamonds" for the same ledger — unified to "diamonds".
  • literally listed "gold, wood, iron, special tokens, and diamonds" as five currencies — merged to four ("gold, wood, iron, and diamonds").
  • The shop-root pitch () and the post-death upsell () both claimed Diamonds could "recruit a companion" — CONFIRMED false, live companions have no direct Diamond price (DarkTowerCompanion carries no diamond-price field; companions are earned via ). The shop-root pitch also claimed "hidden storylines" as a Diamond purchase — false, premium chapters unlock via the Champion Pass subscription, not Diamond spend. Both claims removed.
  • Deliberately left alone: the Token Shop's own "tokens" branding (, Token Shop section) — a dated, documented design decision (2026-05-12) to keep a token-only sink NAMED distinctly from Diamonds "to prevent coupling the gold/wood/iron shop into a shared multi-currency pool." Renaming it would reverse that decision and was out of scope for a trust/copy fix. Same for the earned-faucet reward-flavor lines (mine/fish/hunt/capture/collect "you gained N special tokens") — these feed the Token Shop's earned-currency framing and were left consistent with it.

Currency architecture — RESOLVED 2026-07-13 (owner decision, human_question 24178). The deeper fork is decided: Option A — SPLIT. Dark Tower runs a THREE-currency model — gold (common), Special Tokens (a distinct earned currency with its own ledger, fed by every earned faucet: login streak, token mine, fishing/hunting/capture dailies, achievements), and Diamonds (100% their OWN currency, purchase-only — credited ONLY by ISP packs; NO free faucet may ever inflate them). The Diamond store (premium gear, enchantments, resets) charges Diamonds only; the Token Shop and earned-currency sinks charge Special Tokens only. This is the intent economy-2.0-roadmap.md anticipates for the PvP marketplace (settlement currency must be unambiguous — Precondition E resolves as "split, Diamonds purchase-only").

The ledger/faucet split is IMPLEMENTED (epic 24205, 2026-07-13) — see currency-split-blueprint.md for the full build. Two independent CurrencyLedger keys now back Dark Tower premium currency:

  • Diamonds (purchase-only) stay on the physical key special_tokens, which GRANDFATHERS every prior balance as Diamonds — no data migration ran (owner decision: grandfather all as Diamonds, no migration), so no player lost value. Only (the ISP pack payout, now sourced PremiumCurrencySource.PURCHASE) credits it; NO free faucet touches it.
  • Special Tokens (earned) are a NEW currency DARK_TOWER_TOKEN on the NEW key dark_tower_tokens (registered in GameCurrencyRegistry as a secondary ledger, mirroring Blood Lineage LINEAGE_GEMS), starting at 0 for all players. EVERY earned faucet was re-pointed to it (amounts unchanged): daily login streak, token mine, keep-building armory/chapel, fishing/hunting/capture dailies, combat/raid/ invasion reward pipeline, achievements/collect, the Bounty-Board 7-day milestone, the post-story Ascendant's Boon + Hunter's Contracts milestones, the warship minigame, and the onboarding grant. The Token Shop reads/charges dark_tower_tokens; the Diamond store, enchant, reset, and character-slot sinks stay on Diamonds. .tokens()/addTokens()/spendTokens() back the token ledger; diamonds()/specialMoney()/spendDiamonds() back Diamonds.

Result: packs are the ONLY source of Diamonds, so a 7-day login streak can no longer out-earn a $1 pack. Option B (unify to one Diamond economy with reduced faucets) was rejected — it keeps Diamonds earnable. Regression: JUnit (TwoLedgerIndependence) + (two distinct ledger keys) + scenario_tests 2701/2702 (Token Shop reads the token ledger, not Diamonds).

Pack thresholds (P0, also ). The $3 and $10 packs granted 315 and 1,250 Diamonds — short of real Diamond-shop prices (cheapest gear record ~320; cheapest active enchantment 1,350+), so the pack copy's own claims were unaffordable. Fixed by raising the DT-specific grant, NOT the shared cross-game pack constant (ProductPackAmounts.THREE_DOLLAR/TEN_DOLLAR stay 315/1,250 for every other game): new ProductReferenceId.DARK_TOWER_THREE_DOLLAR_PACK / DARK_TOWER_TEN_DOLLAR_PACK literal enum entries (matching the real SMAPI product names) map to new ProductPackAmounts.DARK_TOWER_THREE_DOLLAR (320) / DARK_TOWER_TEN_DOLLAR (1,500) constants. $1 (100) and $5 (550) already crossed real thresholds and were left unchanged. pack copy was rewritten to name only reachable outcomes: the CONFIRMED-false "200 Diamonds is enough to enchant your weapon with fire or frost and still claim a powerful trinket" claim is replaced with the verified 100-Diamond second character slot () doubled to 200 on first purchase; the $3 tier names "real gear in the Diamond shop" instead of an unverified specific item. This is an economy-facing grant change — flagged here for owner review before the next deploy ships it (fixer agents do not deploy).

Regression: game_test.scenario_tests 2695 (pack listing truthful-outcome), 2696 (shop-root companion/storyline claim removed), 2697 (post-death companion claim removed), and 2413 (updated — the pre-existing "concrete pack copy" guard now asserts on "character slot" instead of the removed "trinket" claim).

Champion Pass Subscription — Entitlement Projection (2026-07-12 monetization audit)

The shared subscription () is Dark Tower's Champion Pass. It gates the sixth companion slot () and 11 subscriber-only card options via the derived clue DarkTowerClueTypes.CLUE_SUBSCRIBED, and gates all 26 premium companions (see heroes.md § ISP value) via clueRequired: CLUE_SUBSCRIBED on the companion content row. Eight other benefit checks (spin wheel, buy/sell, shop, story switch, upgrade, attack) call hasActiveSubscription directly instead. CLUE_SUBSCRIBED is projected onto every active subscriber every request by (called from , before the current card's options are filtered) — the single writer of that clue. Before this fix nothing ever set the clue, so the sixth companion slot and every CLUE_SUBSCRIBED-gated card option and companion silently never activated for any subscriber. See § Derived Subscription Projections for the general pattern and its single-writer requirement.

VIP point valuation. previously valued the subscription at a flat $15 (225 VIP points), 5-7x the real $1.99-$2.99 live price — overweighting a subscriber's VIP standing versus a real pack buyer. Fixed to price the subscription at its real $1.99 base with an explicit, documented 1.5x loyalty multiplier (30 points) for the recurring commitment, rather than a mispriced flat valuation.

Production reconciliation recipe. Confirms every active DT subscriber's character carries CLUE_SUBSCRIBED. Run post-deploy against prod to verify the fix landed (pre-fix baseline: 15/15 active subscribers, 0 carrying the clue, confirming the audit finding exactly):

SELECT x.customer_id, x.character_key,
       JSON_CONTAINS(cc.character_data, '"CLUE_SUBSCRIBED"', '$.cluesDiscovered') AS has_clue_subscribed
FROM (
  SELECT DISTINCT pt.customer_id, (ac.account_data->>'$.currentCharacterKey') AS character_key
  FROM game_log.purchase_transactions pt
  JOIN dark_citadel.account_dark_citadel ac
    ON ac.customer_id COLLATE utf8mb4_unicode_ci = pt.customer_id COLLATE utf8mb4_unicode_ci
  WHERE pt.game_type = 'DARK_TOWER' AND pt.product_ref = 'SUBSCRIPTION'
    AND pt.transaction_type = 'PURCHASE' AND pt.status = 'COMPLETED' AND pt.refunded_at IS NULL
    AND (pt.entitlement_ends_at IS NULL OR pt.entitlement_ends_at > NOW())
) x
LEFT JOIN dark_citadel.character_dark_citadel cc
  ON cc.character_key COLLATE utf8mb4_unicode_ci = x.character_key COLLATE utf8mb4_unicode_ci;

A row with has_clue_subscribed = 0 (or a NULL join, meaning the account has no character row at all) is a live entitlement/benefit gap and should be investigated — a character only picks up the projection on its next request (reconcileSubscriberClue runs on session load), so a subscriber who has not played since the fix deployed will still show 0 until their next session.

Consumable Items

Source: DarkTowerItemTypes

Item Purpose
ITEM_HEALING_POTION HP restoration in combat
ITEM_ARMOR_POTION Temporary armor boost
ITEM_DAMAGE_POTION Temporary damage boost
ITEM_SKILLED_POTION Temporary skill boost
ITEM_RECHARGE_POTION Power recharge
ITEM_SPEED_UP_BOOST_ONE 1-minute speed boost
ITEM_SPEED_UP_BOOST_FIVE 5-minute speed boost
ITEM_SPEED_UP_BOOST_SIXTY 60-minute speed boost
ITEM_FULL_RECHARGE_POTION Full power recharge
ITEM_SPIN_THE_WHEEL_GAME_TOKEN Mini-game token
ITEM_ORC_WAR_CHEST Requires key to open (lock-and-key mechanic)
ITEM_WAR_CHEST_KEY Opens Orc War Chest

Fishing Nets (4 Tiers)

Net Acquisition Capture Bonus
ITEM_FLIMSY_NET Buyable Low
ITEM_FABRIC_NET Buyable Medium
ITEM_WIRE_NET Craftable High
ITEM_WELDED_MESH_NET Craftable Highest

Nets are consumed on each fishing attempt regardless of success.

Crafting Bench

Status: SHIPPED. Source: engines/handlers/darktower/activity/

The crafting bench turns gathered materials into items. It is reached by the existing crafting NLU (craft, crafting, craft menu, start crafting, ...) which routes to DARK_TOWER_START_CRAFTING -> openCrafting. openCrafting lists each recipe with its material cost and the players current balances; selecting a recipe routes DARK_TOWER_CRAFT_ITEM -> craftItem, which validates affordability across every material the recipe needs, spends them, and grants the item (). Materials are the same currencies the mines and rewards grant: gold (), wood, and iron ().

Recipe Catalog (initial)

Recipe Produces Gold Wood Iron
Healing Potion ITEM_HEALING_POTION 100 - -
Damage Potion ITEM_DAMAGE_POTION 150 - -
Armor Potion ITEM_ARMOR_POTION 150 - -
Wire Fishing Net ITEM_WIRE_NET 200 10 8
Welded Mesh Fishing Net ITEM_WELDED_MESH_NET 500 20 18

The two craftable nets satisfy the "Craftable" acquisition noted in the Fishing Nets table above. The catalog is an in-code list (mirroring the Token Shop pattern) and is the single place to add recipes. Regression: scenario_test 2430 (craft a Wire Fishing Net from seeded gold+wood+iron). Test-only setWood / setIron seeders on the Dark Tower PlayRequest seed material balances.

Set Bonus System

Source: DarkTowerSetPerkTypes

Sets have up to 6 pieces. Bonuses activate at piece thresholds (2, 3, 4, 5, or 6 equipped).

Set Data Model

Source:

Each set contains:

  • Up to 6 gear piece references (gearPieceOne through gearPieceSix)
  • Up to 5 bonus tiers (2-piece through 6-piece), each with a DarkTowerSetPerkTypes and an amount value
  • lockedEarn: Whether the set can be earned through normal gameplay

13 Set Perk Types

Source: DarkTowerSetPerkTypes,

Stat Perks (permanent while set pieces equipped — amount is a percentage):

Perk Display Text Used In
HIT_BONUS "bonus to hit chance" getSetHitBoostPercent()
CRIT_BONUS "bonus to critical hit chance" getSetBonusCritChance()
CRIT_DAMAGE_BONUS "bonus to critical hit damage" getSetBonusCritDamage()
DAMAGE_BONUS_PERCENT "bonus to damage" getSetDamageBoostPercent()
ARMOR_CLASS_BONUS "bonus to armor class" getSetArmorBoostPercent()
HIT_POINT_BONUS_PERCENT "bonus to hit points" getSetBonusHitPoints()
COMPANION_BONUS_PERCENT "bonus to your companions power level" getSetBonusCompanionPoints()

All stat perk amounts are stored as percentage values in the set's setAmount*Piece fields and described to the player as "a {amount} percent bonus to...".

Triggered AOE Perks (proc on hit — amount is activation chance percentage):

Perk Display Text Cooldown
AOE_FIRE_SET_BONUS "chance to trigger a fire aoe effect on all enemies" 5 minutes
AOE_ICE_SET_BONUS "chance to trigger an ice aoe effect on all enemies" 5 minutes
AOE_ACID_SET_BONUS "chance to trigger an acid aoe effect on all enemies" 5 minutes
AOE_GRAVITY_BOMB_SET_BONUS "chance to trigger a gravity bomb to suck enemies up" 3 minutes
BEHEADING_SET_BONUS "chance to behead an enemy on hit" 3 minutes
AOE_DAZE_SET_BONUS "chance to daze your enemy on hit" No explicit cooldown

How Set Bonuses Are Calculated

Source:

  1. generateIdSet() collects all equipped gear enum keys (weapons, armor, trinkets)
  2. generateSetCountMap() counts how many pieces of each set are equipped
  3. For each set, checks each tier threshold (2-6 pieces) and sums matching perk amounts
  4. Total bonus = sum of all matching perks across all active sets

Example: If a player has 3 pieces of Set A (2-piece gives HIT_BONUS 5, 3-piece gives DAMAGE_BONUS_PERCENT 8), they get +5% hit and +8% damage from that set.

Set Tracking

Source:

Map<String, Long> activeSetBonuses  // Set ID -> number of pieces equipped

Gem Socketing

Source: DarkTowerGem, DarkTowerGemColors, DarkTowerGemCosmetics, DarkTowerGemTypes,

Gem Data Model

Source:

Each gem has the following bonus fields (all nullable, only non-null values apply):

Field Effect Used In
bonusToHit Flat hit chance bonus getGemsHitBoost()
bonusToDamage Flat damage bonus getGemsDamageBoost()
critChanceBonus Crit chance percentage getGemsCritChanceBoost()
critDamageMultiplier Crit damage multiplier getGemsCritDamageMultiplier()
healthBoostPercent HP percentage boost Displayed in learnAbout()
armorBonus Flat AC bonus getAllGemsArmorClassBoost()
darkTowerDamageTypesAdded Elemental damage type getDamageTypesAdded()
damageTypeRollAdded Elemental damage amount Paired with damage type
gemPowerEarned Special power granted DarkTowerPowerSpecial

Gem Socket Slots

Source:

Each piece of equipment has up to 4 gem slots (firstGem through fourthGem):

  • Weapons (3 weapons x 4 slots = up to 12 weapon gem slots)
  • Armor (6 armor pieces x 4 slots = up to 24 armor gem slots)
  • Trinkets (6 trinkets x 4 slots = up to 24 trinket gem slots)
  • Maximum total gem capacity: 60 gems across all gear

Gem Colors (5 Types)

Source: DarkTowerGemColors

Color Prefix Pattern
YELLOW GEM_YELLOW_* — typically crit gems
BLUE GEM_BLUE_* — typically damage/ice gems
RED GEM_RED_* — typically hit/fire gems
GREEN GEM_GREEN_* — typically health/poison gems
ONYX GEM_ONYX_* — typically armor/necrotic gems

Gem Types (130+ Types)

Source: DarkTowerGemTypes

Gems are organized by color and function:

Category Examples Count
Yellow crit gems GEM_YELLOW_CRIT_GEM_ONE through FOUR 4
Blue damage gems GEM_BLUE_DAMAGE_GEM_ONE through SIX 6
Blue ice gems GEM_BLUE_ICE_GEM_ONE through FOUR 4
Red fire gems GEM_RED_FIRE_GEM_ONE through FOUR 4
Red hit gems GEM_RED_HIT_GEM_ONE through FOUR 4
Green health gems GEM_GREEN_HEALTH_GEM_ONE through FOUR 4
Green poison gems GEM_GREEN_POISON_GEM_ONE through FOUR 4
Onyx armor gems GEM_ONYX_ARMOR_GEM_ONE through FOUR 4
Onyx necrotic gems GEM_ONYX_NECROTIC_GEM_ONE through FOUR 4
Named unique gems GEM_BLUE_GEM_OF_DESTRUCTION, GEM_RED_GEM_OF_RAGE, etc. ~90
Seasonal gems Christmas, Easter, Halloween, Hanukkah, Valentine's, St. Patrick's, Turkey 10
Elemental essence gems Fire, Water, Earth, Air (Elemental Shrine trial rewards) 4

Elemental Essence Gems (Special)

Source: DarkTowerGemTypes comments

Gem Source Bonus
GEM_ESSENCE_FIRE Fire elemental trial +15% fire damage, 10% fire resistance
GEM_ESSENCE_WATER Water elemental trial +15% ice damage, healing bonus
GEM_ESSENCE_EARTH Earth elemental trial +10% phys damage, +15% armor
GEM_ESSENCE_AIR Air elemental trial +15% lightning damage, +10% dodge

All 4 essences are required to claim the Orb of Elemental Mastery.

Gem Cosmetic Effects

Source: DarkTowerGemCosmetics

Some gems have visual/audio effects during combat:

Cosmetic Effect
AOE_STUN_ONE_ROUND Stun visual effect
SNOW_PUFF_OF_AIR Snow/frost effect
JACK_O_LANTERN_LAUGH Halloween audio effect
TURKEY_GOBBLE Thanksgiving audio effect

How Gem Bonuses Are Calculated

Source:

Gem bonuses are summed across all equipped gems:

  • getAllGemsArmorClassBoost() — sums armorBonus from all gems in armor + weapons + trinkets
  • getGemsDamageBoost(gems) — sums bonusToDamage from provided gem list
  • getGemsHitBoost(gems) — sums bonusToHit from provided gem list
  • getGemsCritChanceBoost(gems) — sums critChanceBonus from provided gem list
  • getGemsCritDamageMultiplier(gems) — sums critDamageMultiplier from provided gem list
  • getDamageTypesAdded(gems) — collects elemental damage type + roll pairs

Gem Purchasing

Gems have priceGold and priceSpecialTokens fields and can be sold for sellPrice. Specific values are data-driven (stored in DB, not hardcoded).

Reward System

Source:

Reward Types (114 types)

Source: DarkTowerRewardTypes

Recovery rewards (processed immediately, not queued):

Reward Effect
HEALTH_RECOVERY_MINOR Heal 10% HP
HEALTH_RECOVERY_REWARD Heal 20% HP
HEALTH_RECOVER_MAJOR Heal 30% HP
HEALTH_RECOVERY_FULL Heal to 100% HP
PERFECT_RECOVERY_REWARD Full heal + reset powers + clear all effects
EFFECT_RECOVERY_REWARD Remove all bad effects
POWER_RECOVERY_REWARD Reset special power usage

Standard rewards (queued in pendingRewards, processed by finalizeRewardsExternal()):

BASIC, EXPERIENCE, MONEY, ITEM, GEAR, MAJOR, SPECIAL, WAVES, RAID, DUNGEON, INVASION, etc.

Relationship rewards: ISABEL/WILLIAM/TRINITY/CUTHBERT/CHELSEA/NIGEL/LUCAS/RYLEE_RELATIONSHIP_IMPROVE

Seasonal rewards: HALLOWEEN_EXCLUSIVE, ST_PATTY_DAY, EASTER_DAY, HANUKKAH_EXCLUSIVE, VALENTINES_DAY, TURKEY_GEM, CHRISTMAS_GEM

Specific item rewards: Named weapon/trinket/gem rewards for specific drops

Pending Rewards Queue

Source:

List<DarkTowerReward> pendingRewards = new ArrayList<>();

Rewards are added when earned (combat victory, quest completion, etc.) and consumed when finalizeRewardsExternal() is called. Recovery rewards bypass the queue and apply immediately.

Free-Explore Combat Gold Supplement (feedback 8742)

Source: DarkTowerBasicFeatures.grantFreeExploreKillGold +

Roughly half of all enemies (146 of 297), including all six elementals, carry the ENEMY_REWARD death reward, which grants only experience (75) and no gold. On the open-world free-explore map that left a roaming-enemy kill paying out nothing tangible (feedback 8742: "it would be nice if you actually got something from the random stuff around the free explorer map when you defeated them like the elemental").

When an enemy dies on a FREE_EXPLORE card AND its death reward grants no gold, combat queues a supplemental gold reward through the standard pending-reward pipeline:

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

The base is then difficulty-boosted (EASY x2 .. HELLSCAPE x4), gold-find-buffed, and new-player-capped by finalMoney exactly like every other reward. Scoped two ways so the economy is not distorted:

  • FREE_EXPLORE cards only — story and dungeon balance is untouched.
  • No-gold death rewards only — enemies that already drop gold (e.g. ENEMY_GOLD_REWARD orcs at money 26) are never double-paid.

Scaling is by the slain enemy's level, not player level, so a high-level player farming low-level trash earns no extra; the base stays well below the gold-mine base (100) so mining remains the dedicated gold faucet. Guarded by scenario_test 2072 (3 grounded Slime Monsters on check-free free-explore card 3902) and JUnit .

Punishment System

Source:

Punishment Types

Source: DarkTowerPunishmentTypes

Category Types Amount
Combat debuffs WEAKNESS, BRITTLE, UNINTELLIGENT, UNWISE, INCREASED_DAMAGE Effect applied
Health loss HEALTH_LOSS_MINOR, HEALTH_LOSS_MIDDLE, HEALTH_LOSS_MAJOR % of HP
Money loss MONEY_LOSS_MINOR (cap 100g), MONEY_LOSS (cap 500g), MONEY_LOSS_MAJOR (cap 1000g) Capped amount
Insanity INSANITY_MINOR, INSANITY, INSANITY_MAJOR Insanity points
Relationship Per-NPC: WILLIAM/ISABEL/TRINITY/CUTHBERT/CHELSEA/NIGEL/LUCAS/RYLEE_RELATIONSHIP_HURT Relationship damage
Other MERCENARY_KILLED, INVADED_LOST Special

New Player Mercy

Source: constants

NEW_PLAYER_LEVEL_THRESHOLD = 5
NEW_PLAYER_MERCY_MULTIPLIER = 0.5  // 50% reduction

Players below level 5 receive 50% reduction on:

  • Health loss
  • Money loss
  • Insanity gain

A message "(New adventurer protection reduced the severity of this punishment.)" is appended.

Power-Based Resistance

MASTER_OF_DESTINY: % chance (from powerSpecialPercent) to resist punishment effects
GRAND_STRATEGIST_POWER: % chance (from powerSpecialPercent) to resist punishment effects

Both are checked independently. If either triggers, the effect is blocked.

Inventory Management

Source:

maxInventorySpace: int = 20              // Default, can increase
ownedItemsMap: Map<String, CharacterChoices>  // Consumable items with quantities
gearOwned: Set<String>                   // Unique gear pieces owned

Loot Tracking

Deque<LootLogEntry> recentLoot           // Last 200 loot events (circular buffer)

Each entry: itemId, lootType (BASIC/OVERRIDDEN), quantity, timestamp, acquired (true/false)

Shop System

Shop is accessible from main menu (card 0) only after session count > 2 (must have played 3+ sessions).

The shop uses FreeTextDeclarationAction.SHOP_BUY for purchase routing. Items are priced per their data definitions (not hard-coded in utilities).

Shop Root Layout

The shop root (, list SHOP_ROOT_CATEGORIES, which is the definition) is paged. The first page is the gear categories, the Token Shop and a Special Shop entry, whose label names what it opens, so a player hears the two counters without paging. The Special Shop opens a short menu of the two premium gold counters, Black Market and Permanent Upgrades (owner ruling on orchestrator#1019 T61 item 2, option C). Those counters, the Title Shop, the Enchantment Shop and Spend Diamonds are on the second page. A category named while the other page is shown still opens: the page option carries the other page's names (OptionSelector.carryOffPageNames). Prices are spoken in words, "Healing Potion for 350 gold", never in parentheses or with a bare g, and a category page's heading reads "Available items, page 1 of 2, level 1." A menu's none or Leave Shop puts the card's choices back with one line ("You leave the shop."), never an item lookup or "I did not catch that" (, OptionEscapes.spokenEscape).

Selling — Sell-Browse Flow

A bare "sell", or a top-level "sell " / "sell my X" / "sell weapon" said from the MAIN game (a story card), opens a sell-browse menu listing the player's own sellable inventory. It must NEVER route to the buy menu.

Top-level NLU (reach the sell flow from anywhere). The parent-game one-shot context routes the sell intent: the bare sell / sell item direct rows plus a prefix row sell (direct_match=0, DARK_TOWER) in game_process.process_one_shot_values map any "sell ..." utterance to FreeTextDeclarationAction.SHOP_SELL. then opens the sell-browse menu whenever the action is SHOP_SELL (it no longer depends only on the spoken value matching isSellBrowseTrigger), so "sell sword" / "sell my dagger" reach the sell flow from the main game, not only from inside the sell sub-menu. (sell is still excluded from isShopMenuTrigger.)

Carried (unequipped) inventory. showSellMenu enumerates gearOwned (weapons/armor/trinkets) plus ownedItemsMap (consumables/collectibles), keeping only registry-sellable items (DarkTowerShopRegistry.isSellable), and lists them as SELL_{type}_{enumKey} options (each labelled "{name} for {value} gold", most valuable first). Selecting one routes SHOP_SELL -> handleSellKey -> sellItem.

Equipped gear (sellable through a confirm). Worn starting gear lives only in the equipped slot fields (mainHand/offHand/ranged, the six armor slots, the six trinket slots) and is NOT in gearOwned, so the carried-inventory pass alone never surfaced it — a player holding an equipped weapon was wrongly told "nothing to sell". collectEquippedSellableInventory now enumerates every equipped slot, keeps the registry-sellable items, de-dupes a type held in two slots, and lists them as SELLEQUIP_{type}_{enumKey} options ("{name} equipped for {value} gold"). An item that is both equipped and in gearOwned is shown once, under the equipped section. Selecting an equipped item shows an unequip-and-sell confirm ("You have the {name} equipped. Unequip and sell it for {value} gold?"); "yes" routes SELLEQUIPYES_{type}_{enumKey} -> sellEquippedItem, which removes the item and nulls its slot directly (combat lazily re-equips a basic dagger when the main hand is empty, so the player is never left weaponless — it does NOT use removeWeapons, whose copy-count loop no-ops for worn starting gear that was never in gearOwned), then pays out and announces the sale. The page reserves room for equipped gear (EQUIPPED_SELL_LIMIT) so worn items stay reachable by voice.

Empty state. When the player has nothing sellable carried AND nothing sellable equipped, showSellMenu speaks a clear empty message ("You don't have anything to sell right now, not even gear you have equipped. ...") rather than silently opening the buy shop.

Shop Sort Order — Equipment Strongest-First (Option A, 2026-06-14)

Source: + getGearSortValue.

When a player browses an equipment category (Weapons, Armor, Trinkets), the shop lists items strongest-first — highest gear value on page 1, weakest on the last page. Consumables (the ITEM category) keep their existing cheapest-first gold-price sort.

Decision (human_question #21779, Option A — descending default). The shop previously sorted every category by getGearSortValue ascending, so a player opening Weapons saw the Dagger (gear score 11) on page 1 and the strongest affordable weapon buried on the last page. Feedback 8730 ("let me get the most powerful one") flagged that a player wanting the best gear they can afford had to page to the end. Flipping equipment to descending puts the best affordable gear on page 1, matching the dominant buy intent. Consumables are not power-ranked, so their cheapest-first sort is unchanged.

getGearSortValue still returns the ordering magnitude (gear value for equipment, gold price for consumables); the direction is chosen by the caller — Comparator.reversed() for the three equipment categories, natural order for consumables. Cosmetic / ordering-only change; no pricing, affordability, or purchase-routing behavior is affected.

Regression: game_test.scenario_tests — weapons category page 1 leads with the highest available gear score at the seeded level.

Auction House

Source: ,

The auction house runs automated cycles where randomized items are put up for bid. Players compete by placing bids through the game UI.

Cycle Mechanics

Property Value
Cycle length 118 minutes
Items per cycle 4 (4× randomBiddableItem() call)
Item selection roll 1d30

Item Type Selection (1d30)

Roll Item Type
1 Weapon
2 Armor
3 Item
Default Trinket

Starting Bids by Item Type

Type Starting Bid
Item 1,200 gold
Weapon 50,000 gold
Armor 30,000 gold
Collectible 2,500 gold
Trinket 25,000 gold

Bidding Rules

  • Bid increment: New bid must be at least 135% of the current top bid
  • Inventory check: Player must have space in inventory before bid is accepted
  • UI flow: shows current auction list, handles confirmation step
  • Storage: Auction state stored in Redis

FIXED (2026-07-17): makeBid previously mutated the bid amount but never called setTopBidPlayer or wrote the mutated bid back to auctionItemBids, so a "successfully placed" bid never actually held — and separately, winningItems was written keyed by the raw Long topBidPlayer field (refreshStoryAuctions) but read/compared by the String characterKey ('s held-bid filter, wonAnItem/collectWinningItems), so a genuinely-won item could never be found or collected. topBidPlayer and winningItems' key are now both String (the character key) throughout. Regression: game_test.scenario_tests 2879 (bid, confirm, held-bid display) and 2880 (collection), plus JUnit . Separately noted, NOT fixed in this task: refreshStoryAuctions (the auction-cycle rollover) has no @Scheduled caller or any other wiring anywhere in the codebase — the automated 118-minute auction cycle described above does not currently run in production; a test-only seeding path (/, wired through DarkTowerUiPlayEntry.PlayRequest) was added so the bid/collection flow could be regression-tested despite this. Needs a follow-up to wire a scheduler.

This is the AUTOMATED NPC-seller auction, not a player-to-player marketplace. The player-driven (PvP) auction-house revisit — price discovery, market-manipulation guardrails, listing UX, and the B3 bandwidth lane below — is scoped in economy-2.0-roadmap.md (the gate doc for queue ). Re-enabling the per-trade history store here is in scope for that build (price discovery needs it).


Spin the Wheel

Source: , DarkTowerSpinTheWheelBasics

Spin the Wheel is a token-based mini-game with randomized rewards. The player uses one token per spin.

Property Value
Token required ITEM_SPIN_THE_WHEEL_GAME_TOKEN
Action BASELINE_SPIN_THE_WHEEL
Difficulty Forced NIGHTMARE during play

Outcome Logic

  1. Roll 1d10
  2. Rolls 1–3 (30%) → Extreme table (1d300)
  3. Rolls 4–10 (70%) → Normal table (1d100)

Extreme table (1d300):

Rolls Reward
Jackpot range Jackpot reward
Bow range High-end bow
Troops range Troops reward
Free spin range Additional spin token(s)

Normal table (1d100):

Rolls Probability Reward
Good item 10% Good item
Minor troops 10% Minor troops
Troops 10% Troops
Two spins + minor 10% 2 spin tokens + minor reward
Minor reward 60% Minor reward

Design note: The forced NIGHTMARE difficulty means loot rolls from Spin the Wheel use the NIGHTMARE rarity table (20% Orange, 30% Purple, 25% Blue). This makes the mini-game a meaningful endgame reward avenue.


Orc War Chest

Source: , DarkTowerSpinTheWheelBasics

The Orc War Chest uses a lock-and-key mechanic: the player must have both a key and the chest item before opening.

Property Value
Key item ITEM_WAR_CHEST_KEY
Chest item ITEM_ORC_WAR_CHEST
Action BASELINE_OPEN_CHEST
Difficulty Forced NIGHTMARE during play

Both items are consumed on open. Uses the same DarkTowerSpinTheWheelBasics reward logic as Spin the Wheel, with additional special rewards at high roll values:

Special Reward Table (High Rolls)

Rolls Reward
100–110 Mega haul
111–120 Awesome haul
121–130 Great haul
150 Orcish Boom Stick (specific weapon)
151 Orcish Broad Sword (specific weapon)
152–161 30× speed boosters
162–163 5× Orc War Chest Keys

Enchantment System

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

Enchantments are persistent stat bonuses stored in activeEnchantments on the character. There are 38 enchantment types in 4 categories.

Categories

Category Count Acquisition
Purchasable 19 IAP / diamond shop
In-game earned 7 Gameplay rewards, achievements
Paragon 7 Paragon progression unlocks
Saddles (mounts) 5 Mount system

Purchasable Enchantments (19)

Range: ENCHANTMENT_DEMON_HUNTER through ENCHANTMENT_BATTLE_MASTER

These provide stat boosts that can be bought directly. Typical bonuses include flat HP, damage percent, AC, skill bonus, and crit chance depending on the enchantment type.

In-Game Earned Enchantments (7)

Range: ENCHANTMENT_CHAMPIONS through ENCHANTMENT_MIDDLE_GROUND

Earned through gameplay milestones (specific quest completions, achievement unlocks, etc.).

Paragon Enchantments (7)

Range: PARAGON_HIT_BONUS through PARAGON_COMPANION_BONUS

Unlocked via the paragon system. These provide bonuses to hit chance, damage, armor, skills, crits, HP, and companion effectiveness.

Saddle Enchantments (5)

Range: SADDLE_HORSE through SADDLE_RED_DRAGON

Associated with the mount system. Mount type determines saddle enchantment tier.

How Enchantments Apply

Enchantment bonuses feed into: HP (flat), AC (flat), skill checks, hit chance, damage, and crit calculations via the shared calculateEnchantmentBonus() helpers in .

An enchantment's experienceBoost is a percent added to all experience earned, summed across the player's discovered enchantments (). It joins the additive experience stack in , beside the difficulty boost, the xp-find buff and the warband bonus, so the experience the player hears is the experience granted. A fixed grant handed straight to addExperience (a quest's or a launch reward's set experience) is not boosted. The readout says it the same way: "plus N percent to all experience earnings". JUnit: .


Player-Facing Enchant Command — SHIPPED (2026-07-16)

Status: SHIPPED. Resolves the confirmed gap identified in the review (fb 9870/9871).

The 19 purchasable enchantments (ENCHANTMENT_DEMON_HUNTER through ENCHANTMENT_BATTLE_MASTER) are now reachable through a real Diamond-shop category, (backend/src/main/java/llitd/engines/handlers/darktower/shop/.java), mirroring the existing Token Shop / Title Shop / Black Market category pattern:

  • Browse: CATEGORY_ENCHANTMENT_SHOP lists the player's not-yet-owned purchasable enchantments, cheapest first, with diamond prices (subscriber discount applied to the displayed price).
  • Buy: BUY_ENCHANTMENT_{enumKey} spends Diamonds through the audited premium-currency ledger ( / spendDiamonds, routed through — never a raw currency mutation) and appends the purchased DarkTowerEnchantments catalog entry to the character's activeEnchantments list, so the bonus feeds into combat math (via the existing helpers) on the very next calculation.
  • Insufficient diamonds: routes through with ShortfallCurrencyType.SPECIAL_TOKENS, matching every other Diamond-priced shop sink.
  • Buy by name from anywhere: a buy that names an enchantment ("buy protector", "buy the protector pack") reaches the same purchase through the unified buy router, 's tryRouteSpecificInGamePurchase and , which reads the catalog's own names. Inside the shop only the exact option label bought, and everywhere else the router refused the name, so a player holding the Diamonds could not spend them (report 11051).
  • NLU: game_process.process_one_shot_values rows (process_context DARK_TOWER, action_key SHOP_BUY, value_type CATEGORY_ENCHANTMENT_SHOP) route enchant, enchant my weapon, enchant menu, enchant shop, enchantment shop, enchantments, browse enchantments, enchant weapon, buy enchantment/buy an enchantment/buy enchantments, and open enchantment shop to the new category from anywhere in a Dark Tower session, plus a discoverable "Enchantment Shop" option at the shop root (mirroring the Token Shop / Title Shop / Black Market menu entries).

The game's own marketing copy (, 's pack-listing pitch, 's post-death upsell, 's shop-root pitch) was verified during this build — all four lines were already generic ("Diamonds let you enchant weapons...") or already priced against the real $10-pack-covers-the-cheapest-enchantment threshold (1,350 diamonds, Angel Slayer), so none needed a copy correction; they are now factually true rather than aspirational.

Regression tests (game_test.scenario_tests, add_scenario_test): id 2820 (browse the enchant shop lists the catalog with diamond prices), id 2821 (happy-path purchase — 2000 diamonds seeded via setSpecialTokens, buy Demon Hunter's Enchantment for 1000, activeEnchantments non-empty), id 2822 (insufficient-diamonds rejection — 0 diamonds seeded, buy attempt renders the shortfall copy and activeEnchantments stays empty).

Fixed in passing: had a missing-space grammar bug ("costs1000 diamonds") in its price clause — never previously exercised with a non-zero diamondPrice on a live response, since the only prior caller (DarkTowerAssignParagon) only reads paragon-earned enchantments (diamondPrice 0). Added a readOutEnchantmentValue(enchantment, diamondCost) overload so a caller applying a subscriber discount states the actually-charged price rather than the catalog base price, and used it from the new purchase flow.

Known issue found during this build, not in scope, tracked separately below: the GEAR_ENCHANTMENTS content rows have a data-entry bug outside the 19 purchasable enchantments — the row named "Blog Middle Ground Membership" carries primaryEnumKey: ENCHANTMENT_LLITD_BLOG_HERO (a duplicate of the real "Blog Hero Membership" row's key) instead of ENCHANTMENT_MIDDLE_GROUND, and ENCHANTMENT_MEMORIAL has no content row at all. Both are in-game-earned enchantments (not diamond-purchasable), unaffected by this shop. See Known Issues below.

Diamond Spend Hub — SHIPPED (2026-07-17)

Status: SHIPPED. Resolves owner decision on human_idea 23993 (a diamond-store ask, fb 9793/9794/9933): diamond SPENDING already existed but was scattered across three unrelated handlers with no single place to browse "everywhere my diamonds go."

Source: engines/handlers/darktower/shop/.

What it consolidates (the real diamond sinks — verified in code, not assumed)

Before this build, a CanBePurchased.priceOfDiamonds() audit across DarkTowerWeapon / DarkTowerArmor / DarkTowerTrinket / DarkTowerItem / DarkTowerGem found every one of those overrides hardcoded to return null — no weapon, armor, trinket, consumable, or gem in the standard gold-shop categories has ever been diamond-priced, despite the shop-root pitch's long-standing "buy Diamonds ... for the Enchantment Shop and exclusive Diamond shop gear" claim (see Known Issues below). The actual live diamond sinks are exactly three:

Sink Cheapest price Handler
Reset and Respec (one build dimension) 5 diamonds — the cheapest real diamond sink in the game (see § Reset / Respec)
Reset everything at once 15 diamonds same
Buy a reset token / bundle of three 12 / 30 diamonds same
Enchantment Shop (19 purchasable enchantments) ~1,000+ diamonds (Angel Slayer priced against the $10 pack threshold)
Extra character slot 100 diamonds (second slot), then 250 / 500 / 1,000 / 2,000 / 5,000... via

The Black Market is intentionally NOT in the hub. Despite its "premium shop" framing (§ S1 above), every DarkTowerBlackMarketPricing constant (FEATURED_GEAR_BASE, PREMIUM_SLOT_BASE, CONSUMABLE_BUNDLE_BASE, REROLL_BASE) is gold-denominated — it is the fast gold claw-back sink paired with the richer combat income, not a diamond sink. The original task guidance for this build assumed it belonged in a diamond-spend hub; that assumption was checked against the code and found false, so it is excluded here. If a future change ever adds a genuine diamond-priced Black Market slot, add it to the hub then.

The hub itself

is a thin router — it implements no purchase logic of its own. It speaks the player's live diamond balance (), states the cheapest entry point (reset, 5 diamonds), and lists three options that hand off to the handler that already owns that sink:

  • Reset and Respec → FreeTextDeclarationAction.DARK_TOWER_RESET (opens the existing reset menu)
  • Enchantment Shop → SHOP_BUY / CATEGORY_ENCHANTMENT_SHOP (opens the existing enchant shop)
  • Buy a Character Slot → SHOP_BUY / BUY_CHARACTER_SLOT (the existing purchase flow)

Reachability — distinct from the pack-BUYING flow

The hub is reachable two ways, both landing on 's new CATEGORY_DIAMOND_SPEND_HUB branch:

  1. A permanent "Spend Diamonds" option in the shop root (showShopMenu), alongside Token Shop / Title Shop / Permanent Upgrades / Black Market / Enchantment Shop / Buy Diamonds.
  2. Top-level one-shot NLU phrases in game_process.process_one_shot_values (process_context=DARK_TOWER, action_key=SHOP_BUY, value_type=CATEGORY_DIAMOND_SPEND_HUB): "spend diamonds", "spend my diamonds", "diamond menu", "diamonds menu", "use my diamonds", "use diamonds", "diamond spend menu", "spend diamond hub", "open diamond menu".

These are deliberately distinct from the existing pack-BUYING flow, which stays completely unchanged: "diamond shop" / "diamond store" (the isShopMenuTrigger gold-shop-root phrases) and "buy diamonds" / "more diamonds" / "packs" / the DARK_TOWER_DIAMOND_SHOP action all still route to (the real-money ISP pack listing). A player who already holds diamonds and wants to browse where to spend them reaches the new hub; a player who wants to buy more diamonds reaches the unchanged pack listing.

Token Shop — Rotating Daily Sink

Status: SHIPPED (2026-05-12)

Purpose: Provide a token-only sink. Player feedback (fb 767) flagged that special tokens had no strong sink today; gold/wood/iron each had shop options but tokens did not. The Token Shop layers a curated, rotating token-priced inventory on top of the existing shop infrastructure.

Catalog (8 items)

Item Type Display Name Token Cost Rarity
ITEM_DAMAGE_POTION Damage Potion 5 common
ITEM_ARMOR_POTION Armor Potion 5 common
ITEM_SKILLED_POTION Skilled Potion 6 common
ITEM_FULL_RECHARGE_POTION Full Recharge Potion 8 uncommon
ITEM_SPEED_UP_BOOST_SIXTY Sixty Minute Speed Boost 12 uncommon
ITEM_WIRE_NET Wire Fishing Net 15 rare
ITEM_WAR_CHEST_KEY Orc War Chest Key 20 rare
ITEM_WELDED_MESH_NET Welded Mesh Fishing Net 30 epic

Four of these eight appear per UTC day, selected by deterministic shuffle.

Refresh Schedule

  • Cadence: 24 hours, aligned to UTC midnight.
  • Seed: LocalDate.now(ZoneOffset.UTC).toEpochDay() (a monotonically increasing long; rolls over at UTC midnight).
  • Persistence: No state stored — rotation is recomputed on every browse from the current date. No background job, no DB column, no Redis key.
  • Determinism: Same seed -> same Random sequence -> same shuffle order -> same first four items. Every player sees the same rotation; every test run on the same day sees the same rotation.

NLU Surface

Direct-match phrases routed to SHOP_BUY action with key CATEGORY_TOKEN_SHOP (process_one_shot_values, process_context=DARK_TOWER):

  • token shop
  • open token shop
  • open the token shop
  • browse tokens
  • browse the token shop
  • spend tokens
  • token store
  • tokens shop
  • visit token shop

Plus a permanent "Token Shop" option in the main shop menu (showShopMenu()), reachable from any "shop"/"store"/"merchant" entry point.

Reset / Respec — Diamond Sink

Status: SHIPPED 2026-06-28 (DC->DT feature migration). Source: engines/handlers/darktower/builtin/. Full mechanics in progression.md § Reset & Respec System.

The character build Reset (respec) is the game's diamond sink for build experimentation. A player clears a chosen build dimension to pick it again, paying in diamonds:

Reset Diamond cost
First respec (any dimension, one time only) FREE
One dimension (Powers / Classes / Stats / Skills / Companions), after the free first 5
Everything at once, after the free first 15
Buy one reset token (redeemed for a full from-scratch reset) 12
Buy a bundle of three reset tokens 30
  • Currency. "Diamonds" is the player-facing name for the Dark Tower special-tokens ledger ( delegates to specialMoney(); same CurrencyLedger "special_tokens" key). A reset spends via spendDiamonds and gates on canAffordDiamonds; a shortfall routes to the diamond purchase flow through (ShortfallCurrencyType.SPECIAL_TOKENS, display name "diamonds").
  • No-op guard. A reset never charges when the chosen dimension has nothing to clear.
  • Free first respec (onboarding grace). A new player's very first respec is FREE, regardless of dimension, so a first mis-pick is forgivable. The free reset is gated on respecCount == 0; respecCount (on ) increments on every reset that actually clears something, so every reset after the first is diamond-gated. The free reset never consumes a held reset token. Modeled on the legacy RESET_COOLDOWN_COUNT escalation, simplified to a single free first use.
  • Reset tokens (SHIPPED 2026-06-28; re-priced + choice added). A reset token is the diamond-purchasable respec currency. A single token costs RESET_TOKEN_DIAMOND_COST (12) diamonds — deliberately below the 15-diamond direct "Everything" cost so a token has real value — and a discounted bundle of RESET_TOKEN_BUNDLE_QUANTITY (3) costs RESET_TOKEN_BUNDLE_DIAMOND_COST (30), undercutting both three singles (36) and three direct resets (45). Buying either increments the resetTokens counter on . Token-versus-diamond is the player's choice, never auto-consumed: when a player with a held token chooses the full "Everything" reset, the handler presents an explicit choice — spend a reset token, or pay 15 diamonds. Per-dimension resets always pay diamonds directly. Tokens are stockpileable, the natural unit for a future real-money ISP bundle (sell a pack of reset tokens) -- route that through . The reset token is also surfaced in the Token Shop (the player's diamond sink) so it is reachable from the shop.
  • Why diamonds, not gold. In legacy Dark Citadel, reset cost a trivial escalating gold amount (at most 200). Re-pricing it as a diamond sink makes respec a deliberate, monetizable action and an ISP value lever; the free first respec is the onboarding-grace exception. Tunable via SINGLE_DIMENSION_DIAMOND_COST / EVERYTHING_DIAMOND_COST / RESET_TOKEN_DIAMOND_COST / RESET_TOKEN_BUNDLE_QUANTITY / RESET_TOKEN_BUNDLE_DIAMOND_COST in .

Title Shop — Rotating Cosmetic Sink

Status: SHIPPED (2026-05-16).

Purpose: Voice-native cosmetic gold sink for level 100+ players whose gold accumulates faster than existing sinks absorb. Round 3 of 19054 declined gold-for-stat (would either bypass paragon or duplicate it confusingly) but accepted in principle that gold lacks a strong late-game sink. The title shop solves the cosmetic bucket of that gap. Titles are deeply expandable (the catalog can grow forever) and voice-friendly because Alexa speaks the title in narrator copy — no rendering layer required.

Title Catalog (initial ~30 titles across 6 tiers)

Tier 1 — Common (10,000 gold, no gate)

Flavor titles, always eligible from level 1:

Title Voice example
The Brave "The Brave Sam steps forward"
The Curious --
The Wanderer --
The Steady --
The Bold --

Tier 2 — Uncommon (100,000 gold, level 25+)

Earned-feel titles, gated by basic level milestone:

Title Gate
Champion Level 25+
Defender Level 25+
Pathfinder Level 25+
Veteran Level 50+
Adventurer Level 25+

Tier 3 — Rare (1,000,000 gold, milestone-gated)

Achievement-anchored titles:

Title Gate
Dragonslayer Level 100+ AND any kill on DRAGON enemy type
Tower Conqueror Cleared Tower waves to round 50+ (towerStats)
Demon Hunter Level 100+ AND 1000+ DEMON kills (enemyKills)
Lich Bane Level 100+ AND 500+ UNDEAD kills (enemyKills)
Master Strategist Level 200+

Tier 4 — Epic (5,000,000 gold, devotion-gated)

One per god, gated by 100% devotion to that god:

Title Gate
Champion of Elysium Elysium devotion 100
Hand of Merris Merris devotion 100
Voice of Destin Destin devotion 100
Storm of Torrent Torrent devotion 100
Blood of Perrik Perrik devotion 100
Light of Apollo Apollo devotion 100
Touched by Venus Venus devotion 100
Tide of Poseidon Poseidon devotion 100

Tier 5 — Legendary (25,000,000 gold, paragon-gated)

Title Gate
The Paragon Level 200+
Master of the Tower Level 300+
Worldbreaker Level 400+
The Ascendant Level 500
Eternal Level 500 AND all 40 regular paragon points spent

Tier 6 — Mythic (100,000,000 gold, completion-gated)

The two prestige caps:

Title Gate
The Crowned Any raid completion AND any dungeon completion AND wave 75+ AND any auction win
The Indomitable Four gods simultaneously at 100% devotion

Cost Ladder Summary

Tier Per-title cost Tier total
1 Common 10,000 50,000
2 Uncommon 100,000 500,000
3 Rare 1,000,000 5,000,000
4 Epic 5,000,000 40,000,000
5 Legendary 25,000,000 125,000,000
6 Mythic 100,000,000 200,000,000
Total -- ~370,000,000

Full title collection is a ~370M gold completionist target, naturally pacing endgame consumption for many hundreds of hours of post-level-100 play.

Refresh Schedule

  • Cadence: 24 hours, aligned to UTC midnight.
  • Seed: LocalDate.now(ZoneOffset.UTC).toEpochDay().
  • Persistence: No state stored — rotation recomputed on every browse from current date.
  • Filter pipeline: pool = allTitles \ ownedTitles, then filter by meetsGate(player, title), then shuffle, then take first 4. If the eligible pool is smaller than 4 (player has bought most titles), show whatever is left.

NLU Surface

Direct-match phrases routed to handlers:

Browse the rotating shop (DARK_TOWER_TITLE_SHOP):

  • title shop / titles shop
  • open title shop / open the title shop
  • browse titles
  • buy a title / buy title

Set active title (DARK_TOWER_SET_TITLE — captures title name from utterance):

  • set my title to {title} / set title to {title}
  • use title {title} / wear title {title}
  • change my title to {title}

Clear active title (DARK_TOWER_CLEAR_TITLE):

  • remove my title / remove title
  • clear title / clear my title
  • no title

Inspect:

  • what is my title / what's my title
  • list my titles / show my titles (player's owned set)

Plus a permanent "Title Shop" option in the main shop menu (showShopMenu()), reachable from any "shop" / "store" / "merchant" entry point.

Voice Display Integration

  • New helper: returns currentTitle == null ? characterName : currentTitle + " " + characterName.
  • Call sites that switch to getDisplayName():
    • Welcome / greeting copy (main menu, return-from-combat, day rollover)
    • Achievement announcements (level up, paragon milestone, raid completion)
    • World events / world chat broadcasts (chat-shortcut commands from 19084)
    • Auction win announcements
  • Call sites that stay first-name-only:
    • NPC dialog in cards (close interpersonal voice)
    • Internal monologue / cinematic narration where formality would break tone
  • NPC / cinematic name token: card text usernamereplacementvalue is registered by to getSpokenName — the character name spoken whole, or the hero when the name is blank, a TestCharacter fixture, carries a digit, or is outside length 3-24. Titles are omitted. Live cards: 37211, 181208. Guard: on the Alexa render path.

Permanent Non-Combat Buffs — Always-On Gold Sink

Status: SHIPPED (2026-05-16).

Purpose: Companion to the Title Shop. Where titles are the cosmetic / prestige lane, permanent buffs are the QoL / utility lane. Three discrete sinks: inventory expansion, gold/xp/token find boosts. All three are deliberately capped low so they STACK with companion / devotion / set bonuses without ever replacing those sources. The combat-stat lane stays paragon-only; gold becomes the "smooth your loop" lane.

B1: Inventory Slot Expansion

Field Value
Starting slots 20 (maxInventorySpace default)
Hard cap 50
Cost formula 5000 * floor(1.5^(N - 21)) for slot N, capped at 1,000,000 per slot

Cost ladder (selected anchors):

Slot # Cost
21 5,000
25 ~25,000
30 ~190,000
35 ~1,000,000 (cap reached)
40 1,000,000
50 1,000,000

Total to buy all 30 expansion slots: ~10,000,000 gold.

B2: Permanent Gold-Find / XP-Find / Token-Find

Three independent buff tracks, one per DarkTowerFindBuff value. Each tier adds DarkTowerPermanentBuffPricing.FIND_BUFF_PERCENT_PER_TIER percent, and a track stops at FIND_BUFF_HARD_CAP_TIER. The gold cost of each step is findBuffCostForNextTier.

Tier Cost Cumulative bonus
1 50,000 +1%
2 250,000 +2%
3 1,000,000 +3%
4 5,000,000 +4%
5 25,000,000 +5%

Per-track total cost to max: ~31,300,000 gold. All three tracks maxed: ~94,000,000 gold.

Apply path — when a reward is granted:

  • Gold reward: finalAmount = baseAmount * (1 + companionBoost + devotionBoost + permanentGoldFindBonus) where each component is a fractional multiplier summed additively.
  • XP reward: same pattern via permanentXpFindBonus.
  • Token reward: same pattern via permanentTokenFindBonus.

Wired into existing reward paths in — multipliers compose with current companion/devotion/set sources.

Crediting a tier owed to a player goes through the admin grant, never a raw write to dark_citadel.character_dark_citadel: the session's cached character writes back over a raw write. POST /api/admin/grant takes gameKey dark_tower and a currencyType that is one DarkTowerFindBuff value's grantType() or fieldName(). amount is the number of tiers to add, not an absolute target. The grant raises the player's current character through and saveDurable, and writes the GRANT audit row with its product_ref first. That product_ref is refused only when the same customer already has a COMPLETED GRANT of the same currency under it, so one batch ref grants each player once. A save that throws writes the pre-tier back through the same cache, Redis included. The audit row is marked undelivered only when that write-back lands. A Dark Tower turn takes no lock the grant holds, so a turn that loaded the raised tier can still save it after that. The retry therefore reads the undelivered row's post_balance, the tier that attempt meant to set (PurchaseTransactionLogger.findUndeliveredGrant), and a character below it gets the step. A character at or above it may hold that grant, or its tier was raised another way since, by the shop () or a character switch. A tier cannot tell which, so that retry adds nothing, the row stays FAILED, and the answer is a failure that names both causes. Every later retry of the ref refuses the same way until an admin reconciles that transaction. When the write-back throws, the row stays delivered and the replay check refuses the retry. A credit past DarkTowerPermanentBuffPricing.FIND_BUFF_HARD_CAP_TIER is refused by the field's name before the audit row, and a deduct that would lower the tier is refused by that same name. See § Admin Grant + Cache Reset.

B3 (DEFERRED): Auction House Bandwidth

Not designed in this round. Considered: pay gold per cycle to view 8 or 12 auction items instead of 4. Deferred because the auction house itself is in 19054's "DEFER UNTIL DT 2.0 economy roadmap" bucket. UNBLOCKED 2026-06-09: the DT 2.0 economy roadmap is now authored (economy-2.0-roadmap.md § Precondition F) — B3 reframes as a permanent-buff ladder scaling both the browse window and a player's max concurrent listings, sequenced AFTER the core PvP marketplace ships and only once liquidity makes "see more listings" worth paying for.

NLU Surface

Inventory expansion (DARK_TOWER_INVENTORY_EXPAND):

  • buy inventory slot / buy inventory slots
  • expand my inventory / expand inventory
  • more inventory / bigger inventory
  • upgrade my bag

Find-buff purchases (one action per track):

  • buy gold find / upgrade gold find / more gold find
  • buy xp find / upgrade xp find / more experience find
  • buy token find / upgrade token find / more token find

Status / inspect (DARK_TOWER_INSPECT_BUFFS):

  • what are my permanent buffs / show my buffs
  • how much inventory do I have
  • what's my gold find

All three buffs and inventory expansion appear as menu options in a "Permanent Upgrades" submenu off the main shop.

Voice Integration

Purchase confirmations follow voice rules (no parens, no slashes, natural language):

  • "You now have 21 inventory slots. The next slot costs 7,500 gold."
  • "Your gold find is now 2 percent higher. Next tier costs 1 million gold."
  • "Permanent upgrades. Inventory slots, gold find, experience find, token find. What would you like?"

Combined Endgame Sink Summary

Sink Cost to fully max Sink type
Title Shop (30 titles) ~370,000,000 gold Cosmetic, prestige
Inventory expansion (20→50) ~10,000,000 gold QoL
Gold-find / XP-find / Token-find (3 tracks, 5 tiers each) ~94,000,000 gold QoL
Total combined deep-sink target ~474,000,000 gold --

For a level 500 player whose gold accumulation has nowhere to go, this represents a multi-hundred-hour completionist arc that does NOT touch paragon balance and does NOT bypass the existing combat-stat ceiling.


Reward Scaling + Matching Gold Sinks (Richer Earn, Quick Claw-Back)

Status: SHIPPED 2026-06-19 (design ). R1 combat clear bonus, R1b combat XP scaling, R2 skill-check gold, and the S1 Black Market are all live in game-engine code. Regression: game_test.scenario_tests 2222 (black market browse renders the rotation) + 2223 (reroll deducts gold), JUnit (clear-bonus + skill-check formulas) + (price/reroll ladder). The shop-root menu option count rose 10 to 11 with the new Black Market category (scenario_test 2112 assertion updated).

Origin: Player feedback 8985 / 9000 (player on DARK_TOWER), confirmed by PROVE and the owner decision on human_question 22397. The complaint, verbatim: "after each combat no matter how many enemies I kill I only get a 300 reward of 100 gold and experience per fight" and "Im only getting 100 gold and 300 experience after each combat session no matter how much I fight and Im not getting gold anywhere else just after combat Im not getting rewards for skill checks or anything."

Current reward model (PROVE 22374, verified in code)

A combat victory () grants the card's fixed combatSuccessReward — a DarkTowerRewardTypes enum whose backing DarkTowerReward row carries a card-authored, fixed gold value. That gold is summed in and passed through before being credited. finalMoney is the only multiplier on combat gold:

// .finalMoney (verbatim shape)
totalMultiplier = 100 + difficultyMoneyBoost + permanentGoldFindBonus;   // additive percentages
boosted         = percentOf(totalMultiplier, value);                      // value * totalMultiplier / 100
return applyNewPlayerCap(boosted, NEW_PLAYER_GOLD_CAP=100, gbp);          // capped to 100 if level < 5

difficultyMoneyBoost = DarkTowerDifficultyUtils.moneyBoost: EASY 100, HARD 150, NIGHTMARE 200, HELLSCAPE 300. Because it is added to a base 100, the effective gold multiplier is:

Difficulty moneyBoost finalMoney effective multiplier
EASY 100 ×2.0
HARD 150 ×2.5
NIGHTMARE 200 ×3.0
HELLSCAPE 300 ×4.0

This is the SAME multiplier the free-explore supplement uses (the doc's "EASY x2 .. HELLSCAPE x4" note). Important facts that shape this design:

  • The combat gold is card-fixed: it does not scale with enemy count or enemy/player level. A 1-enemy and a 5-enemy victory on the same card pay identically. This is the complaint's root cause.
  • finalMoney applies only difficulty money-boost + the permanent gold-find buff + the new-player cap. It does not apply companion goldPercentBoost or devotion gold-find (those are not in the gold path — a divergence from the older prose in the Permanent-Buff section, corrected here).
  • A player below level 5 has every gold reward batch capped at 100 gold (NEW_PLAYER_GOLD_CAP). The complaint's flat "100 gold" is consistent with a low-level player hitting this cap and/or a small card base — either way, no per-fight scaling exists.
  • Experience has no starter cap: finalExperience applies the difficulty boost, the permanent xp-find buff, the warband bonus and the enchantments' experience percent at every level, so combat XP below level 5 scales with the fight.
  • Skill-check gold structurally exists but is unused. already routes check.getSuccessReward() through addRewardReadout → finalMoney on success, so a check can pay gold today — but card authors have not attached gold-bearing success rewards to checks across the 612k-card corpus, so in practice checks pay nothing. The fix is a code auto-grant, not a 612k-card re-authoring pass.
  • A combat XP-by-enemy-level fallback already exists: when a victory's reward grants no XP, continueSuccessActions adds baseXp = max(10, sum(enemyLevel) * 5) from gameBasicProperties.getOrCreateEnemiesKilled(). It does NOT fire when the card reward already grants XP (the complaint's "300 XP" cards), so it is invisible to the complaining player.

Owner decision (human_question 22397)

Richer rewards are welcome — enemy-scaled combat gold and skill-check gold — on the condition that they are paired with new GOLD SINKS. The owner's principle, verbatim intent: players may earn a lot as long as there is a QUICK, rewarding way for the game to claw the gold back. This document is the economy arbiter for both halves: the richer earn (R1, R2) and the matching fast sink (S1, S2).

Why the existing sinks do NOT satisfy this on their own

The economy already has deep sinks — the Title Shop (~370M gold), Permanent Buffs (~94M), and Inventory expansion (~10M) total a ~474M-gold completionist arc (see Combined Endgame Sink Summary). But those are slow, long-horizon sinks aimed at the level-500 player whose gold has nowhere left to go. They do not give a mid-game player a quick, rewarding place to spend a fresh windfall, and they are not what the owner means by "claw it back quickly." The richer earn below is paired with a new short-horizon, high-velocity sink (the Black Market) that absorbs fresh income at the rate it is earned, at the level the player is at. The two horizons are complementary: Black Market = fast cycle; Title/Buff/Inventory = slow completion.


R1 — Combat Clear Bonus (enemy-count + enemy-level scaling)

A combat victory queues a NEW supplemental gold reward — the Combat Clear Bonus — computed from the enemies actually defeated, queued into pendingRewards before finalizeRewardsExternal so it rides the same finalMoney path (difficulty ×2–4 + permanent gold-find + new-player cap) as the card's fixed reward. The card base is never reduced; the bonus is purely additive.

COMBAT_GOLD_BASE_PER_ENEMY     = 8     // flat per defeated enemy   (pre-finalMoney)
COMBAT_GOLD_PER_ENEMY_LEVEL    = 3     // per level of the defeated enemy
COMBAT_GOLD_MAX_SCALED_ENEMIES = 10    // count cap (swarm/wave/boss-pack guard)

perEnemyGold(e)  = COMBAT_GOLD_BASE_PER_ENEMY + max(1, e.level) * COMBAT_GOLD_PER_ENEMY_LEVEL
clearBonus       = sum of perEnemyGold(e) over the highest-level
                   min(defeatedCount, COMBAT_GOLD_MAX_SCALED_ENEMIES) defeated enemies

Read the defeated set from gameBasicProperties.getOrCreateEnemiesKilled() (NOT currentEnemies, which clearCombat() empties before finalize — see Code Hooks). The clearBonus is a MONEY reward; finalMoney then applies the ×2–4 difficulty multiplier and the gold-find buff exactly like the card reward.

Design choices and why:

  • Scale by ENEMY level, not player level — mirrors the deliberate free-explore-supplement choice: a high-level player farming low-level trash earns almost nothing extra (a level-1 mob pays 8+1*3 = 11 pre-finalMoney), so the bonus rewards fighting level-appropriate content, not grinding weak mobs. Content difficulty rises with the player naturally, so enemy-level scaling is the progression curve — without a farm exploit.
  • Enemy-count cap of 10. Mass-enemy cards exist (a Gold Dragon dungeon boss rides with 50 Goblin Warlocks + 50 Harpies; arena/wave rounds reach 45 enemies — see combat.md). Counting only the highest-level 10 defeated enemies bounds the payout while still rewarding the real threats.
  • Scope: authored story + dungeon + wave/raid combat victories. Free-explore roaming kills keep their existing per-kill supplement (15 + max(1, enemyLevel) * 4 via grantFreeExploreKillGold, fired in removeDeadEnemies for FREE_EXPLORE cards with no-gold death rewards) and are excluded from the clear bonus so the two never double-pay.
  • Granted once per victory, not per swing or per round.

Worked examples (gold; raw = pre-finalMoney):

Fight clearBonus (raw) + card base (raw 50*) Then ×2.0 EASY ×3.0 NIGHTMARE
1 enemy, L10 1×(8+30)=38 88 176 264
3 enemies, L30 3×(8+90)=294 344 688 1,032
5 enemies, L100 5×(8+300)=1,540 1,590 3,180 4,770
8 enemies, L300 8×(8+900)=7,264 7,314 14,628 21,942
45 goblins, L5 (capped 10) 10×(8+15)=230 280 560 840

* 50 raw is a representative authored card base (× the EASY ×2 multiplier ≈ the "100 gold" the player reported). Real card bases vary; the clear bonus is the dominant new term either way.

R1b — Combat XP scaling (extend the existing fallback, curve-safe)

The same complaint flags flat 300 XP. Rather than a separate system, promote the existing enemy-level XP fallback into an additive bonus: grant xpClearBonus on every victory (not only when reward XP is zero), scaled by the same capped defeated-enemy set, kept small so it never destabilizes the exponential XP curve (level 100 needs ~8.1M XP for the level, level 300 needs ~9.3M — see progression.md). A few hundred bonus XP is meaningful at low level and a rounding error at high level, so the bonus self-limits — it cannot accelerate the late grind, only de-flatten early fights.

COMBAT_XP_BASE_PER_ENEMY  = 10
COMBAT_XP_PER_ENEMY_LEVEL = 2
xpClearBonus = sum over the same capped defeated-enemy set of
               (COMBAT_XP_BASE_PER_ENEMY + max(1, e.level) * COMBAT_XP_PER_ENEMY_LEVEL)

XP is the secondary lever; gold is the headline. If playtest shows leveling pace drifting, tune COMBAT_XP_* down first — the gold scaling is what the sink (S1) is balanced against. (The existing max(10, sum(enemyLevel)*5) no-XP fallback is subsumed: keep it as the floor when a card grants no XP, and add xpClearBonus on top when it does.)


R2 — Skill-Check Gold (auto-grant on option-check success)

A successful skill check on a card OPTION auto-grants a small gold reward, queued through the same reward pipeline so finalMoney applies the game-difficulty multiplier + gold-find. The path already exists (handleChecks → addRewardReadout(check.getSuccessReward())); the fix is to auto-grant a computed gold reward on success regardless of whether the card author attached one, since re-authoring gold onto checks across 612k cards is impractical.

SKILL_CHECK_GOLD_BASE        = 4      // pre-finalMoney
SKILL_CHECK_GOLD_PER_LEVEL   = 0.5
checkBandBonus: EASY +0, HARD +4, NIGHTMARE +10, HELLSCAPE +20   // the CHECK's difficulty band

skillCheckGoldRaw = round(SKILL_CHECK_GOLD_BASE + playerLevel * SKILL_CHECK_GOLD_PER_LEVEL + checkBandBonus)
// finalMoney then applies the GAME-difficulty money-boost (×2..×4) + gold-find + new-player cap

Two different difficulties compose deliberately: checkBandBonus rewards harder checks; finalMoney's difficultyMoneyBoost rewards the player's chosen game difficulty. Net paid examples:

Player / check / game-diff raw ×finalMoney paid
L10, EASY check, EASY game (×2.0) 4+5+0=9 ×2.0 ~18
L30, HARD check, HARD game (×2.5) 4+15+4=23 ×2.5 ~58
L100, NIGHTMARE check, NIGHTMARE game (×3.0) 4+50+10=64 ×3.0 ~192

Design choices and why:

  • Option checks only — NOT combat stance / "better footing" checks. Stances and the footing move are rolled inside combat and re-attemptable every round (combat.md § Stance System); paying gold per stance would be a spin-to-win farm. The reward is scoped to the authored DarkTowerCheck on a card option — exactly the "rewards for skill checks" the player asked for.
  • Success only (a fail already routes to nextCardIdOnFailure, pays nothing).
  • Anti-farm cooldown. Some checks live on loopable cards (free-explore, re-enterable hubs). The auto-grant fires at most once per (cardId, checkType) per cooldown window, tracked in actionUsage, mirroring the loot weakness-message 15-minute throttle.
  • Layers on top of any authored successReward — if a card author did attach a gold reward to a check, both apply (authored reward is the intent; the auto-grant is the baseline floor).

S1 — The Black Market (rotating high-cost premium shop) — the headline sink

The Black Market is a new shop category off the main shop root (alongside Weapons, Armor, Trinkets, Consumables, Token Shop, Title Shop, Permanent Upgrades, Buy Diamonds). It is the quick, rewarding claw-back the owner asked for: a frequently-rotating, small, hand-curated set of genuinely powerful items at steep, level-scaled gold prices, with a pay-gold reroll lever for players who want to spend a windfall right now.

What it stocks (4 slots per window)

Premium, real-power stock — the opposite of the cosmetic Title Shop:

Slot Stock Why it's a draw
1 — Featured Gear One high-rarity (ORANGE / PURPLE) weapon, armor, or trinket the player does not own, level-appropriate The headline buy: a real power upgrade now instead of grinding loot RNG
2 — Premium Gem One high-tier gem (damage / crit / armor). Shipped substitution: gems are socketed into gear, not held as standalone inventory, so this slot is a rotating war-chest / key cache that delivers comparable direct power via the NIGHTMARE-table reward. Direct combat power for an equipped slot
3 — Consumable Bundle A stack of premium combat consumables (damage / armor / skilled / full-recharge potions) OR an Orc War Chest Key Burns gold on combat velocity; the key chains into the NIGHTMARE-table chest reward
4 — Wildcard Rotates: 60-minute speed boosts, a premium companion unlock token, or a bonus reroll token Variety so the window always has a reason to look

Gear sold here uses the existing loot/gear ownership system — only unowned, eligible pieces appear, so every purchase is a real acquisition, never a dead cosmetic. This is what makes the sink rewarding rather than a gold-burn tax.

Fast rotation — the "quick" in quick claw-back

  • Refreshes every 4 hours (6 windows per UTC day) — far faster than the daily Token Shop and Title Shop. The faster cadence is the point: there is almost always something new worth buying, so fresh income flows back out instead of piling up.
  • Deterministic rotation: the window floor(epochSecondUTC / 14400) (4-hour bucket) picks each slot's offer, the same deterministic-rotation pattern the Token Shop and Title Shop already use (same QA + social-signal benefits; recomputed on every browse, no stored state, no background job).
  • Every window changes every slot. Each slot rotates through its offers (): windows run in epochs as long as the slot's offer count, each epoch shows every offer once in an order drawn from the epoch, and an epoch never opens on the offer the last one closed on. So no slot shows the same offer two windows running, which an independent draw per window did a third of the time, with three offers a slot. The featured gear slot rotates through the player's unowned premium pieces the same way; with one piece left it shows that piece, and with none the legendary supply cache.
  • The welcome line says when the next rotation is ("the next rotation is in 3 hours and 17 minutes"), so a player who has seen this window's stock knows when to come back.

Reroll for gold — the immediate claw-back lever

A player can pay gold to instantly reroll the current window's stock without waiting for the timer. This is the fastest sink in the design: a player sitting on a windfall can chase the gear they want this session.

REROLL_BASE            = 1,000
REROLL_LEVEL_SCALE     = 0.05      // +5% of base per player level
REROLL_GROWTH          = 2.0       // doubles each reroll within the same window
REROLL_MAX_PER_WINDOW  = 6         // soft ceiling; beyond this the cost is punitive by design

rerollCost(n) = round(REROLL_BASE * (1 + level * REROLL_LEVEL_SCALE) * REROLL_GROWTH^n)
                where n = rerolls already done this window (0-based)

A reroll steps every slot to its next offer, so each reroll changes every slot that has more than one offer; slots 2 to 4 always have three, so a paid reroll always buys a different shelf. Reroll count resets when the 4-hour window rolls. The escalating cost means a few rerolls are affordable on a windfall, but chasing a specific item is expensive — a deliberate gold drain, not a guaranteed buy.

Steep, level-scaled prices

Prices scale with player level so the sink keeps pace with the now-richer income (a level-100 player earns ~30× a level-10 player per fight, so the level-100 Black Market must cost ~that much more, or the sink stops biting):

FEATURED_GEAR_BASE     = 10,000    // featured gear, before rarity
GEM_BASE               = 4,000
CONSUMABLE_BUNDLE_BASE = 2,500
LEVEL_PRICE_SCALE      = 0.05      // +5% of base per level

price(item) = round(item.base * (1 + level * LEVEL_PRICE_SCALE) * rarityFactor)
              rarityFactor: PURPLE 1.0, ORANGE 1.8   (gear only)

Worked featured-gear price (ORANGE, rarityFactor 1.8): L10 ≈ 27,000; L30 ≈ 45,000; L100 ≈ 108,000; L300 ≈ 288,000. (A PURPLE featured item is ~half.) Sized so a featured purchase ≈ a fraction-to-one play session of the new combat income at that level — see the balance table.


S2 — Secondary sinks (rounding out the claw-back)

The Black Market is the headline; these reinforce it without heavy new systems:

  • Reroll cost (S1) is itself a pure, high-velocity gold sink — the single fastest claw-back lever.
  • Consumable velocity (existing, amplified). Richer income naturally drives more spending on combat potions and boosts, including the in-combat "buy healing potion" path already shipped. The Black Market's premium consumable bundle intensifies this; no new mechanic required.
  • Gold Gamble (optional, lighter secondary). A gold-priced single-roll gamble paralleling Spin the Wheel (which is token-only): pay a level-scaled gold price for one roll on a mostly-consumable payout table with an occasional gear hit. A pure "burn gold for a thrill" sink. Ship the Black Market first; add this only if the Black Market alone does not absorb high-level surplus.
  • Existing deep sinks unchanged. Title Shop / Permanent Buffs / Inventory expansion (~474M total) stay the long-horizon completion pile for the surplus the fast sinks don't catch — deliberately NOT re-priced.

Balance Math — target net economy curve

Assume an engaged play session ≈ 20 combats + ~10 option skill checks, fighting level-appropriate content at the difficulty a player of that level typically runs. Income is the new combat + skill-check gold, after the finalMoney difficulty multiplier (×2.0 EASY … ×3.0 NIGHTMARE; gold-find buff ignored for the baseline). Per-combat raw uses a representative card base of 50.

Level / difficulty Per-combat gold (raw → paid) Per-check (paid) Session income Was (card-only) Black Market featured (ORANGE) Session ≈
L10 EASY (×2.0), 3×L10 164 → 328 18 ~6,740 ~2,000 27,000 ~0.25 featured / ~3 rerolls
L30 HARD (×2.5), 3×L30 344 → 860 58 ~17,780 ~2,500 45,000 ~0.4 featured / ~3 rerolls
L100 NIGHTMARE (×3.0), 5×L100 1,590 → 4,770 192 ~97,320 ~3,000 108,000 ~0.9 featured / ~6 rerolls
L300 NIGHTMARE (×3.0), 8×L300 7,314 → 21,942 492 ~443,760 ~3,000 288,000 ~1.5 featured / many rerolls

(Session = 20×paid-combat + 10×paid-check. "Was" = the flat card base × the same multiplier, no scaling.)

Target net curve (the design contract):

  1. Combat income rises ~3–7× at mid-game vs the flat status quo — the felt "richer rewards" that closes the complaint. The increase is largest exactly where the player engages most (more enemies, higher difficulty, level-appropriate content).
  2. The Black Market is sized to absorb the MAJORITY of an engaged player's fresh income at their level. A featured purchase costs ~0.25–1.5 sessions of income; the reroll lever lets a determined player pour a whole windfall into one window (cost escalates but spend is uncapped). This is the owner's quick claw-back: gold cycles in and back out within a session or two, at the level the player is at.
  3. Net accumulation stays gently positive. After the fast sink takes its share, a small surplus funnels into the existing deep cosmetic/QoL sinks (~474M), preserving the long-horizon completion arc. The economy is never a closed loop that zeroes the player out (punishing) nor a runaway faucet (inflationary).
  4. High-level players (L200+) accumulate faster — by design. The deep sinks (Title tiers 5–6 = 25M–100M each) plus the uncapped reroll absorb the surplus; this is the intended endgame gold pile, not a balance failure. Black Market prices scale with level (×0.05/level) specifically so the fast sink keeps biting as income climbs.

Tuning knobs (single source of truth — change the constant, re-run the table): COMBAT_GOLD_BASE_PER_ENEMY, COMBAT_GOLD_PER_ENEMY_LEVEL, COMBAT_GOLD_MAX_SCALED_ENEMIES, SKILL_CHECK_GOLD_BASE/PER_LEVEL, FEATURED_GEAR_BASE, LEVEL_PRICE_SCALE, REROLL_BASE/GROWTH. The earn and sink constants are tuned as a pair: if combat income is raised, raise FEATURED_GEAR_BASE / LEVEL_PRICE_SCALE in the same change so the net curve above holds. Re-evaluate against game_log.combat_metrics after 7+ days of live data (the same data-driven-review discipline the Balance Analytics Instrumentation section mandates) — do not hand-tune from partial data.


Anti-Exploit Guards (collected)

Vector Guard
Farm low-level trash for combat gold Clear bonus scales by enemy level, not player level — a level-1 mob pays ~11 raw
Swarm/wave/boss-pack jackpot Enemy-count cap of 10 (highest-level enemies counted)
Double-pay on free-explore Clear bonus scoped to story/dungeon/wave victories; free-explore keeps its separate per-kill supplement
Spin stances for skill-check gold Skill-check gold is OPTION-checks only, never combat stance/footing checks
Loop a check card for gold One grant per (cardId, checkType) per cooldown window (actionUsage throttle)
Cheap-buy premium gear at low level Black Market prices scale ×0.05/level; reroll cost scales too
Infinite reroll Reroll cost doubles each reroll within the window; resets every 4h
New-player gold flood Clear bonus + skill-check gold flow through the existing finalMoney new-player cap (100 gold/batch under level 5)

Code Hooks (for the implementation follow-up — game-engine scope)

Implemented in queue . Slot-2 substitution: the design "Premium Gem" slot ships as a war-chest cache (an Orc War Chest plus its key) — Dark Tower gems are socketed into gear, not held as standalone inventory, so there is no clean stand-alone gem grant; the war chest delivers comparable direct power via the NIGHTMARE-table reward. The four live slots are Featured Gear (unowned ORANGE/PURPLE), War Chest Cache, Combat Supply Bundle, and a rotating Wildcard. Integration points (all live):

Piece Hook Pattern to mirror
R1 Combat Clear Bonus In , compute from gameBasicProperties.getOrCreateEnemiesKilled() (currentEnemies is already cleared) and queue a MONEY reward before finalizeRewardsExternal so it rides finalMoney + grantFreeExploreKillGold
R1b Combat XP Same victory hook; queue a small EXPERIENCE bonus from the same killed set, on top of (not instead of) the existing max(10, sum(enemyLevel)*5) no-XP floor existing addExperience path in continueSuccessActions
R2 Skill-Check Gold In , on isSpecificCheckSuccess, auto-queue a computed MONEY reward (independent of check.getSuccessReward()); cooldown via actionUsage the loot weakness-message 15-min throttle
S1 Black Market handler New — 4-hour deterministic rotation + reroll (rotation) + (gate/price)
S1 Shop wiring New CATEGORY_BLACK_MARKET in / handleCategoryKey existing Token Shop / Title Shop categories
S1 Pricing constants New constants util DarkTowerPermanentBuffPricing
NLU Direct-match phrases ("black market", "open black market", "reroll black market", "refresh the market") in game_process.process_one_shot_values, process_context=DARK_TOWER Token Shop NLU surface
Regression New game_test.scenario_tests per the per-commit rule: clear bonus scales with enemy count; option-check success grants gold; Black Market browse renders the rotation; reroll deducts gold authored via add_scenario_test

Black market rerolls and enchantment money

A reroll keeps its seed on the character, so the stock a player hears is the stock they buy from. The featured piece stays when it is the only premium piece left unowned, and the reroll says so; the other slots still change, which is what the reroll charges for. The combat bundle's first offer carries two exploding shards. getEnchantmentMoneyBoost adds its percent to finalMoney, so a gold enchantment pays on every gold reward.