Real CONTRABAND development case

Survival progression: threshold, duplicates, difficulty

A Martinez AI Studios education product

Hull unlocks, kill pay, and ten difficulty profiles had to stay independent contracts.

Kaela Survival grants hulls when credits cross a double-price threshold. Shipyard display prices apply a global ×8 multiplier after a tier scale. Using the displayed price made unlocks eight times too expensive. Separately, kills tagged as Survival still received ambient credits, combo money, and XP — a double pay. Ten combat difficulty profiles (Story through Extinction) multiply enemy stats only; tests require identical milestone credits and kill/wave XP on every profile.

Category
Economy / progression
Project
CONTRABAND
Recorded
2026-08-20
Status
Verified · published

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.

Related flagship modules

Related notes