Lesson 22

Game economy: rewards should create decisions, not chores.

Coins, upgrades and rewards are pacing tools. A good economy asks the player to choose, then makes that choice visible in gameplay.

IntermediateBalancingUpgrades35-55 minutes

1. Define what money is for

Before writing formulas, decide what the currency does. In a tower defense game, gold buys towers and upgrades. In a courier game, coins buy fuel, protection and handling upgrades. In a dungeon game, shards might buy health, attack or keys. If currency buys everything, decisions blur. If it buys one strategic category, choices become clearer.

const upgrades = [
    { id: "tank", name: "Larger Fuel Tank", cost: 24, effect: { maxFuel: 140 } },
    { id: "cushion", name: "Cargo Cushion", cost: 18, effect: { landingDamage: 0.55 } },
    { id: "stabilizer", name: "Storm Stabilizer", cost: 30, effect: { boostEnabled: true } },
    { id: "lantern", name: "Glow Lantern", cost: 22, effect: { coinBonus: 2 } }
];

Source pattern: Supagames Little Sky Courier and Guardian Towers store upgrade definitions as data with costs, ownership and gameplay effects.

The data should explain the design. If two upgrades do the same thing, merge them or make one affect a different play style.

2. Cost curves should match session length

A 10-minute game needs faster economy than a 30-minute game. If the player earns 4 coins per minute and the first meaningful upgrade costs 100, the system is dead. Estimate expected earnings, then set costs so the first purchase happens early enough to teach the shop.

function upgradeCost(baseCost, level) {
    return Math.round(baseCost * Math.pow(1.55, level - 1));
}

function rewardForWave(wave, enemyType) {
    const base = enemyType === "boss" ? 100 : enemyType === "tank" ? 25 : 10;
    return Math.round(base * (1 + wave * 0.1));
}

Exponential costs work when the game has many repeat purchases. Fixed costs work better for handcrafted upgrades. Use the simplest curve that creates real choices.

3. Show affordability in the UI

The player should know what can be bought now, what is owned and what is locked behind more currency. A hidden affordability rule feels like a bug. Update buttons whenever gold changes.

function updateShopButtons() {
    for (const button of document.querySelectorAll("[data-upgrade]")) {
        const id = button.getAttribute("data-upgrade");
        const item = upgrades.find(u => u.id === id);
        const owned = save.upgrades[id];
        const affordable = save.coins >= item.cost;
        button.disabled = owned || !affordable;
        button.textContent = owned ? "Owned" : "Buy - " + item.cost;
    }
}

Do not rely only on color. Disabled state, text and price all help. This also improves mobile play because hover hints do not exist on touch screens.

4. Apply upgrades through one function

If upgrade effects are scattered across the game, bugs appear when loading saves or restarting levels. Create a function that derives player stats from base stats and owned upgrades. Call it after load, purchase and reset.

function applyUpgrades(player, save) {
    player.maxFuel = save.upgrades.tank ? 140 : 100;
    player.landingDamageMultiplier = save.upgrades.cushion ? 0.55 : 1;
    player.canBoost = !!save.upgrades.stabilizer;
    player.coinValue = save.upgrades.lantern ? 6 : 4;
}

function purchaseUpgrade(id) {
    const item = upgrades.find(u => u.id === id);
    if (!item || save.upgrades[id] || save.coins < item.cost) return false;
    save.coins -= item.cost;
    save.upgrades[id] = true;
    applyUpgrades(player, save);
    persistSave();
    return true;
}

This makes save migration safer. Old saves can load missing upgrade fields as false and still derive consistent stats.

5. Balance around player choices

If every upgrade is required, there is no economy. If no upgrade matters, there is no reward. A good set has tradeoffs: more safety, more speed, more income, more damage, more range. Let different players choose different routes and still finish.

const towerTypes = {
    arrow: { cost: 50, range: 130, damage: 12, fireRate: 500 },
    cannon: { cost: 100, range: 110, damage: 35, fireRate: 1200, aoe: 40 },
    ice: { cost: 75, range: 120, damage: 6, fireRate: 800, slow: 0.4 },
    sniper: { cost: 150, range: 220, damage: 80, fireRate: 2000 }
};

These are not just prices. They are play styles. Fast cheap damage, area damage, control and long-range burst all solve different problems.

6. Economy QA checklist

  • Can the player buy the first useful upgrade early?
  • Does each upgrade change gameplay visibly?
  • Can the game be completed without one mandatory purchase?
  • Are rewards large enough to feel meaningful but not so large that choices vanish?
  • Do save, reset and restart keep upgrade effects consistent?
  • Does the UI explain cost and ownership clearly?

The economy is the rhythm between effort and possibility. Tune it like pacing, not accounting.

7. Avoid punishment loops

Economies become frustrating when failure removes the tools needed to recover. If a player loses all coins on defeat, then cannot buy the upgrade needed to survive, the game enters a punishment loop. A softer approach is to keep permanent upgrades, reduce only temporary rewards or add a small comeback bonus after repeated failure.

function rewardAfterRun(result) {
    const base = result.victory ? 40 : 12;
    const comeback = result.lossStreak >= 2 ? 8 : 0;
    save.coins += base + comeback + result.bonusCoins;
    save.lossStreak = result.victory ? 0 : result.lossStreak + 1;
    persistSave();
}

Comeback systems should be subtle. They are not there to play the game for the user; they keep a rough attempt from making the next attempt impossible.

Previous: Procedural dungeons. Next: Mobile-first layout.