1. Symptom
Survival hull upgrades did not appear when the player had “enough” credits relative to the dual-price rule. Tagged Survival kills also inflated credits/XP. Changing difficulty looked like it might be the cause because it shipped in the same period.
2. Player impact
Progression felt gated or random. Players could be paid twice for the same kill and still fail a hull check that used shop prices. Difficulty selection did not actually change those rewards — which is the intended design, and now tested.
3. Reproduction
Compare ship.price with getSurvivalProgressionPrice(ship): tests assert price === progressionPrice * 8. findNextSurvivalShip must stay null at threshold − 1 and return the next hull at threshold (progressionPrice * 2). Grant a Survival-tagged kill and confirm combat.js skips ambient credit/combo when enemy.userData.aiGmSurvival is set. Switch Story vs Extinction and compare milestone claims.
4. System ownership
Price fields: data/ships.js (survivalProgressionPrice set before ×8, price after). Threshold/milestones: survival-progression.js. Kill pay split: play/three/combat.js and combat-window-shims.js. Difficulty profiles: combat-difficulty.js (ten ids). Loot tables must not import difficulty.
5. Incorrect assumption
There is one “price” on a ship. Kill rewards can all flow through the same credit += path. Difficulty should scale rewards “because it is harder.”
6. Root cause
Displayed shop price includes SHIP_PRICE_GLOBAL_MULTIPLIER (8). Survival’s double-price hull rule must use the pre-multiplier survivalProgressionPrice. Ambient combat rewards still ran for enemies already paid by the Survival contract (userData.aiGmSurvival). Difficulty is a combat-stat multiplier, not an economy multiplier.
7. Minimal fix
Keep survivalProgressionPrice on the hull before the global ×8; gate findNextSurvivalShip on that field × 2. Skip ambient credits, combo, and XP when isKaelaSurvivalKill. Milestone credits (20k/40k/80k/160k at waves 5/10/15/20) claim once per operation via awardedWaves. Do not let difficultyId change kill/wave/milestone functions.
8. Regression test
tests/survival-progression-rewards.test.mjs: pre-multiplier threshold, no duplicate hull, milestone once, Story vs Extinction identical, all ten difficulties identical XP/credits, combat.js source asserts the aiGmSurvival skip.
9. Release validation
Development log 20 Aug 2026, v0.4.159 (threshold + duplicate kills + reward independence). Ten-level difficulty is logged the same day as a separate additive profile, not as a reward scaler.
10. Generalized lesson
Economy fields need names for each consumer (shop vs unlock vs repair). Shared += on credits is how duplicate rewards happen. Difficulty, loot, and progression are three knobs; coupling them in a prompt will “balance” the wrong one.
Simplified excerpt
Simplified / pseudocode for teaching. Not a dump of production files.
Two prices on one hull, simplified.
// Simplified teaching excerpt — not CONTRABAND production source.
let hull = basePrice;
if (tier >= 3) hull *= 3;
ship.survivalProgressionPrice = hull; // unlock / double-price rule
ship.price = hull * 8; // shipyard display
const unlockAt = ship.survivalProgressionPrice * 2;
if (enemy.userData.aiGmSurvival) {
// Survival contract already paid this kill — skip ambient credits / combo / XP
}Public evidence
- CONTRABAND development log, 20 Aug 2026, v0.4.159.
- survival-progression-rewards.test.mjs
- ships.js survivalProgressionPrice vs price; survival-progression.js; combat.js isKaelaSurvivalKill.