Code Farm economy rebalance: market saturation, infrastructure/defense/cosmetics, mastery track, secondary leaderboards, admin eras, underdog bonus and weekly contracts

Every purchase/upgrade path (new and pre-existing) is now race-safe against concurrent requests via atomic conditional SQL updates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:55:46 +02:00
co-authored by Claude Sonnet 5
parent 024edb5291
commit 34f76aad65
54 changed files with 3886 additions and 164 deletions
+7
View File
@@ -263,6 +263,13 @@
font-size: 0.8rem;
}
.game-lb-title {
flex-shrink: 0;
color: var(--accent);
font-size: 0.75rem;
font-style: italic;
}
.game-lb-score {
flex-shrink: 0;
color: var(--text-muted);
+106 -8
View File
@@ -11,7 +11,9 @@ export class GameFarm {
this.username = this.root.dataset.username || "";
this.stateUrl = this.mode === "view" ? `/game/farm/${this.username}` : "/game/state";
this.farm = null;
this.board = "score";
this._bindActions();
this._bindLeaderboardBoard();
this._startTicker();
this.load();
if (this.mode === "own") this._loadLeaderboard();
@@ -39,6 +41,15 @@ export class GameFarm {
});
}
_bindLeaderboardBoard() {
const select = this.root.querySelector("[data-lb-board]");
if (!select) return;
select.addEventListener("change", () => {
this.board = select.value;
this._loadLeaderboard();
});
}
async _submit(form) {
const action = form.dataset.gameAction;
const params = {};
@@ -68,8 +79,13 @@ export class GameFarm {
this._setHost("[data-game-grid-host]", this._gridHtml(farm));
if (this.mode === "own") {
this._setHost("[data-shop-host]", this._shopHtml(farm));
this._setHost("[data-defense-host]", this._defenseHtml(farm));
this._setHost("[data-infra-host]", this._infraHtml(farm));
this._setHost("[data-perk-host]", this._perksHtml(farm));
this._setHost("[data-legacy-host]", this._legacyHtml(farm));
this._setHost("[data-mastery-host]", this._masteryHtml(farm));
this._setHost("[data-analytics-host]", this._analyticsHtml(farm));
this._setHost("[data-cosmetics-host]", this._cosmeticsHtml(farm));
this._setHost("[data-daily-host]", this._dailyHtml(farm));
this._setHost("[data-quest-host]", this._questsHtml(farm));
}
@@ -94,6 +110,9 @@ export class GameFarm {
set("[data-hud-prestige]", farm.prestige);
set("[data-hud-prestige-mult]", `+${Math.round(farm.prestige_multiplier * 100 - 100)}% coins`);
set("[data-hud-stars]", farm.stars);
set("[data-hud-mastery]", farm.mastery_points);
set("[data-era-coins]", farm.era_coins);
set("[data-era-harvests]", farm.era_harvests);
const fill = this.root.querySelector("[data-hud-xp-fill]");
if (fill) {
const pct = farm.level_is_max || !farm.level_span ? 100 : Math.floor((100 * farm.level_into) / farm.level_span);
@@ -143,6 +162,70 @@ export class GameFarm {
.join("");
}
_masteryHtml(farm) {
return (farm.mastery || [])
.map((m) => {
const action = m.maxed
? `<span class="perk-maxed">Maxed</span>`
: `<form method="post" action="/game/mastery" data-game-action="mastery"><input type="hidden" name="key" value="${m.key}"><button type="submit" class="btn btn-sm">Upgrade (${m.cost} Mastery)</button></form>`;
return `<div class="perk-card"><span class="perk-icon" aria-hidden="true">${m.icon}</span><strong class="perk-name">${m.name}</strong><span class="perk-level">Lv ${m.level}/${m.max_level}</span><span class="perk-effect">${m.effect || m.description}</span>${action}</div>`;
})
.join("");
}
_analyticsHtml(farm) {
if (!farm.mastery_analytics_unlocked) return "";
return (
`<div class="shop-row"><div class="shop-info"><strong>Lifetime coins earned</strong><span>${farm.lifetime_coins_earned}c since Farm Analytics was unlocked.</span></div></div>` +
`<div class="shop-row"><div class="shop-info"><strong>Lifetime harvests</strong><span>${farm.lifetime_harvests} builds harvested since Farm Analytics was unlocked.</span></div></div>`
);
}
_infraHtml(farm) {
return (farm.infrastructure || [])
.map((infra) => {
let action;
if (infra.owned) {
action = `<span class="perk-maxed">Owned</span>`;
} else if (farm.prestige < infra.min_prestige) {
action = `<span class="perk-maxed">Requires prestige ${infra.min_prestige}</span>`;
} else {
action = `<form method="post" action="/game/infrastructure/buy" data-game-action="infrastructure"><input type="hidden" name="key" value="${infra.key}"><button type="submit" class="btn btn-sm" data-confirm="Buy ${infra.name} for ${infra.cost}c?">Buy (${infra.cost}c)</button></form>`;
}
return `<div class="perk-card"><span class="perk-icon" aria-hidden="true">${infra.icon}</span><strong class="perk-name">${infra.name}</strong><span class="perk-effect">${infra.description}</span>${action}</div>`;
})
.join("");
}
_defenseHtml(farm) {
const upkeep = farm.defense_upkeep_daily
? `Costs ${farm.defense_upkeep_daily}c/day upkeep - unpaid protection decays a level.`
: "No upkeep at level 0.";
const action = farm.defense_next_cost
? `<form method="post" action="/game/defense/upgrade" data-game-action="defense"><button type="submit" class="btn btn-sm btn-primary">Upgrade (${farm.defense_next_cost}c)</button></form>`
: `<span class="perk-maxed">Maxed</span>`;
return `<div class="shop-row"><div class="shop-info"><strong>Defense: ${farm.defense_tier_name} (level ${farm.defense_level})</strong><span>${upkeep}</span></div>${action}</div>`;
}
_cosmeticsHtml(farm) {
return (farm.cosmetics || [])
.map((c) => {
let action;
if (c.owned && c.kind === "title") {
action =
farm.active_title === c.key
? `<span class="perk-maxed">Equipped</span>`
: `<form method="post" action="/game/cosmetics/equip" data-game-action="cosmetic-equip"><input type="hidden" name="key" value="${c.key}"><button type="submit" class="btn btn-sm">Equip</button></form>`;
} else if (c.owned) {
action = `<span class="perk-maxed">Owned</span>`;
} else {
action = `<form method="post" action="/game/cosmetics/buy" data-game-action="cosmetic-buy"><input type="hidden" name="key" value="${c.key}"><button type="submit" class="btn btn-sm" data-confirm="Buy ${c.name} for ${c.cost_coins}c?">Buy (${c.cost_coins}c)</button></form>`;
}
return `<div class="perk-card"><span class="perk-icon" aria-hidden="true">${c.icon}</span><strong class="perk-name">${c.name}</strong><span class="perk-effect">${c.description}</span>${action}</div>`;
})
.join("");
}
_dailyHtml(farm) {
const action = farm.daily_available
? `<form method="post" action="/game/daily" data-game-action="daily"><button type="submit" class="btn btn-sm btn-primary">Claim +${farm.daily_reward}c</button></form>`
@@ -156,13 +239,14 @@ export class GameFarm {
return quests
.map((quest) => {
const pct = quest.goal ? Math.floor((100 * quest.progress) / quest.goal) : 0;
const reward = quest.reward_stars ? `+${quest.reward_stars} &#9733;` : `+${quest.reward_coins}c`;
let foot = "";
if (quest.can_claim) {
foot = `<form method="post" action="/game/quests/claim" data-game-action="claim-quest"><input type="hidden" name="quest" value="${quest.kind}"><button type="submit" class="btn btn-sm btn-primary">Claim</button></form>`;
foot = `<form method="post" action="/game/quests/claim" data-game-action="claim-quest"><input type="hidden" name="quest" value="${quest.kind}"><input type="hidden" name="scope" value="${quest.scope}"><button type="submit" class="btn btn-sm btn-primary">Claim</button></form>`;
} else if (quest.claimed) {
foot = `<span class="quest-claimed">Done</span>`;
}
return `<li class="quest-item${quest.claimed ? " quest-done" : ""}"><div class="quest-head"><span>${quest.label}</span><span class="quest-reward">+${quest.reward_coins}c</span></div><div class="quest-bar"><span class="quest-fill" style="--quest-pct:${pct}%"></span></div><div class="quest-foot"><span class="quest-progress">${quest.progress}/${quest.goal}</span>${foot}</div></li>`;
return `<li class="quest-item${quest.claimed ? " quest-done" : ""}"><div class="quest-head"><span>${quest.label}</span><span class="quest-reward">${reward}</span></div><div class="quest-bar"><span class="quest-fill" style="--quest-pct:${pct}%"></span></div><div class="quest-foot"><span class="quest-progress">${quest.progress}/${quest.goal}</span>${foot}</div></li>`;
})
.join("");
}
@@ -186,7 +270,13 @@ export class GameFarm {
.map((crop) => {
const disabled = crop.locked || crop.cost > farm.coins ? " disabled" : "";
const lock = crop.locked ? ` (Lv ${crop.min_level})` : "";
return `<option value="${crop.key}"${disabled}>${crop.icon} ${crop.name} - ${crop.cost}c${lock}</option>`;
const market =
crop.market_state === "saturated"
? " (saturated)"
: crop.market_state === "boosted"
? " (boosted)"
: "";
return `<option value="${crop.key}"${disabled}>${crop.icon} ${crop.name} - ${crop.cost}c${lock}${market}</option>`;
})
.join("");
body = `<form class="plot-plant-form" method="post" action="/game/plant" data-game-action="plant"><input type="hidden" name="slot" value="${plot.slot}"><select name="crop" class="plot-crop-select" aria-label="Choose a project to build">${options}</select><button type="submit" class="btn btn-sm btn-primary">Plant</button></form>`;
@@ -257,19 +347,27 @@ export class GameFarm {
const list = this.root.querySelector("[data-game-leaderboard]");
if (!list) return;
try {
const data = await Http.getJson("/game/leaderboard");
const data = await Http.getJson(`/game/leaderboard?board=${encodeURIComponent(this.board || "score")}`);
if (!data.entries || !data.entries.length) {
list.innerHTML = `<li class="game-lb-empty">No farmers yet.</li>`;
return;
}
const board = this.board || "score";
list.innerHTML = data.entries
.map(
(entry) =>
`<li class="game-lb-row${entry.username === this.username ? " game-lb-self" : ""}"><span class="game-lb-rank">#${entry.rank}</span><a class="game-lb-name" href="/game/farm/${entry.username}">${entry.username}</a><span class="game-lb-level">Lv ${entry.level}</span><span class="game-lb-score">${entry.score.toLocaleString()}</span></li>`
)
.map((entry) => {
const title = entry.title ? `<span class="game-lb-title">${entry.title}</span>` : "";
const value = this._leaderboardValue(board, entry);
return `<li class="game-lb-row${entry.username === this.username ? " game-lb-self" : ""}"><span class="game-lb-rank">#${entry.rank}</span><a class="game-lb-name" href="/game/farm/${entry.username}">${entry.username}</a>${title}<span class="game-lb-level">Lv ${entry.level}</span><span class="game-lb-score">${value}</span></li>`;
})
.join("");
} catch (error) {
return;
}
}
_leaderboardValue(board, entry) {
if (board === "raids") return `${entry.raid_avg.toLocaleString()}c/raid`;
if (board === "time_to_kernel") return this._format(entry.time_to_kernel_seconds);
return entry.score.toLocaleString();
}
}