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 andbuildPriceText(category-browse price labels) now say"diamonds". - 's "what does X cost" readout labeled
priceOfTokensitems"tokens"whilepriceOfDiamondsitems 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 (
DarkTowerCompanioncarries 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 sourcedPremiumCurrencySource.PURCHASE) credits it; NO free faucet touches it. - Special Tokens (earned) are a NEW currency
DARK_TOWER_TOKENon the NEW keydark_tower_tokens(registered inGameCurrencyRegistryas 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/chargesdark_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 (
gearPieceOnethroughgearPieceSix) - Up to 5 bonus tiers (2-piece through 6-piece), each with a
DarkTowerSetPerkTypesand 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:
generateIdSet()collects all equipped gear enum keys (weapons, armor, trinkets)generateSetCountMap()counts how many pieces of each set are equipped- For each set, checks each tier threshold (2-6 pieces) and sums matching perk amounts
- 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()— sumsarmorBonusfrom all gems in armor + weapons + trinketsgetGemsDamageBoost(gems)— sumsbonusToDamagefrom provided gem listgetGemsHitBoost(gems)— sumsbonusToHitfrom provided gem listgetGemsCritChanceBoost(gems)— sumscritChanceBonusfrom provided gem listgetGemsCritDamageMultiplier(gems)— sumscritDamageMultiplierfrom provided gem listgetDamageTypesAdded(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_REWARDorcs 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
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
- Roll 1d10
- Rolls 1–3 (30%) → Extreme table (1d300)
- 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_SHOPlists 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 purchasedDarkTowerEnchantmentscatalog entry to the character'sactiveEnchantmentslist, 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
tryRouteSpecificInGamePurchaseand , 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_valuesrows (process_contextDARK_TOWER, action_keySHOP_BUY, value_typeCATEGORY_ENCHANTMENT_SHOP) routeenchant,enchant my weapon,enchant menu,enchant shop,enchantment shop,enchantments,browse enchantments,enchant weapon,buy enchantment/buy an enchantment/buy enchantments, andopen enchantment shopto 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:
- A permanent "Spend Diamonds" option in the shop root (
showShopMenu), alongside Token Shop / Title Shop / Permanent Upgrades / Black Market / Enchantment Shop / Buy Diamonds. - 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
Randomsequence -> 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 shopopen token shopopen the token shopbrowse tokensbrowse the token shopspend tokenstoken storetokens shopvisit 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(); sameCurrencyLedger"special_tokens" key). A reset spends viaspendDiamondsand gates oncanAffordDiamonds; 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 legacyRESET_COOLDOWN_COUNTescalation, 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 ofRESET_TOKEN_BUNDLE_QUANTITY(3) costsRESET_TOKEN_BUNDLE_DIAMOND_COST(30), undercutting both three singles (36) and three direct resets (45). Buying either increments theresetTokenscounter 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_COSTin .
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 bymeetsGate(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 shopopen title shop/open the title shopbrowse titlesbuy 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 titleclear title/clear my titleno title
Inspect:
what is my title/what's my titlelist 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
usernamereplacementvalueis registered by togetSpokenName— the character name spoken whole, orthe herowhen 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 slotsexpand my inventory/expand inventorymore inventory/bigger inventoryupgrade my bag
Find-buff purchases (one action per track):
buy gold find/upgrade gold find/more gold findbuy xp find/upgrade xp find/more experience findbuy token find/upgrade token find/more token find
Status / inspect (DARK_TOWER_INSPECT_BUFFS):
what are my permanent buffs/show my buffshow much inventory do I havewhat'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.
finalMoneyapplies only difficulty money-boost + the permanent gold-find buff + the new-player cap. It does not apply companiongoldPercentBoostor 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:
finalExperienceapplies 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()throughaddRewardReadout→finalMoneyon 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,
continueSuccessActionsaddsbaseXp = max(10, sum(enemyLevel) * 5)fromgameBasicProperties.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 = 11pre-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) * 4viagrantFreeExploreKillGold, fired inremoveDeadEnemiesfor 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
DarkTowerCheckon 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 inactionUsage, 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):
- 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).
- 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.
- 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).
- 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.