Private
Public Access
1
0

feat(gameplay): Adding % bonus for planet type

This commit is contained in:
gauvainboiche
2026-03-30 15:43:43 +02:00
parent c0f66d8cc0
commit 3b229755f8
10 changed files with 548 additions and 81 deletions

View File

@@ -65,3 +65,31 @@ export async function apiTickEconScores(seed, blue, red) {
if (!res.ok) throw new Error("econ_tick_failed");
return res.json();
}
export async function apiFetchElementBonus() {
const res = await fetch("/api/element-bonus");
if (!res.ok) throw new Error("element_bonus_fetch_failed");
return res.json();
}
export async function apiTickElementBonus(seed, blue, red) {
const res = await fetch("/api/element-bonus/tick", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ seed, blue, red }),
});
if (!res.ok) throw new Error("element_bonus_tick_failed");
return res.json();
}
export async function apiFetchDbInfo() {
const res = await fetch("/api/db-info");
if (!res.ok) throw new Error("db_info_fetch_failed");
return res.json();
}
export async function apiFetchVictoryPoints() {
const res = await fetch("/api/victory-points");
if (!res.ok) throw new Error("vp_fetch_failed");
return res.json();
}

View File

@@ -1,4 +1,4 @@
import { resources } from "./planetEconomy.js";
import { resources, elements } from "./planetEconomy.js";
// ── Sort state ────────────────────────────────────────────────────────────────
@@ -64,6 +64,46 @@ export function computeTeamIncome(team, cells, resourceWorth) {
return { total, byResource };
}
// ── Element bonus calculation ─────────────────────────────────────────────────
/**
* Reverse map: French element label → config key
* e.g. "Matières premières" → "common", "Hydrocarbures" → "petrol"
*/
const ELEMENT_LABEL_TO_KEY = Object.fromEntries(
Object.entries(elements).map(([key, label]) => [label, key])
);
/**
* Compute cumulative element bonus for a team based on their planets' production.
* planet.production stores French label strings as keys (values from elements const).
* bonus = sum_over_planets( sum_over_elements( elementShare% / 100 * elementWorth[key] ) )
*
* @param {string} team
* @param {Map<string, { discoveredBy: string, hasPlanet: boolean, planet: object|null }>} cells
* @param {object} elementWorth - { common: 1, petrol: 3, ... }
* @returns {number} bonus value (use as %)
*/
export function computeTeamElementBonus(team, cells, elementWorth) {
let bonus = 0;
for (const [, meta] of cells) {
if (meta.discoveredBy !== team) continue;
if (!meta.hasPlanet || !meta.planet) continue;
const { production } = meta.planet;
if (!production) continue;
for (const [elementLabel, pct] of Object.entries(production)) {
// production keys are French labels; map back to config key
const elementKey = ELEMENT_LABEL_TO_KEY[elementLabel] ?? elementLabel;
const worth = elementWorth?.[elementKey] ?? 0;
if (worth === 0) continue;
bonus += (pct / 100) * worth;
}
}
return bonus;
}
export { elements };
// ── Resource table for the sidebar ───────────────────────────────────────────
/**

View File

@@ -1,7 +1,7 @@
import { fnv1a32, hash2u32, mulberry32 } from "./rng.js";
import { formatPlanet, generatePlanet } from "./planetGeneration.js";
import { apiFetchConfig, apiFetchScores, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores } from "./api.js";
import { computeTeamIncome, renderResourceTable, setEconSort, getEconSort } from "./economy.js";
import { apiFetchConfig, apiFetchScores, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints } from "./api.js";
import { computeTeamIncome, computeTeamElementBonus, renderResourceTable, setEconSort, getEconSort } from "./economy.js";
// ── Constants ─────────────────────────────────────────────────────────────────
@@ -27,6 +27,7 @@ export const GAME_CONFIG = {
configReloadIntervalSeconds: 30,
worldSeed: "",
seedPeriodEndsAtUtc: "",
elementWorth: {},
resourceWorth: { common: {}, rare: {} },
};
window.GAME_CONFIG = GAME_CONFIG;
@@ -149,6 +150,12 @@ const nextPeriodEl = document.getElementById("nextPeriodUtc");
const resetCountEl = document.getElementById("refreshCountdown");
const scoreBlueEl = document.getElementById("scoreBlue");
const scoreRedEl = document.getElementById("scoreRed");
const vpBlueEl = document.getElementById("vpBlue");
const vpRedEl = document.getElementById("vpRed");
const dbCreatedAtEl = document.getElementById("dbCreatedAt");
const elemBonusBlueEl = document.getElementById("elemBonusBlue");
const elemBonusRedEl = document.getElementById("elemBonusRed");
const effectiveCooldownEl = document.getElementById("effectiveCooldown");
const incomeBlueEl = document.getElementById("incomeBlue");
const incomeRedEl = document.getElementById("incomeRed");
const resourceTableEl = document.getElementById("resourceTableBody");
@@ -188,6 +195,9 @@ export function applyConfigPayload(data) {
GAME_CONFIG.configReloadIntervalSeconds = Math.max(5, Number(data.configReloadIntervalSeconds) || 30);
GAME_CONFIG.worldSeed = String(data.worldSeed ?? "");
GAME_CONFIG.seedPeriodEndsAtUtc = String(data.seedPeriodEndsAtUtc ?? "");
if (data.elementWorth && typeof data.elementWorth === "object") {
GAME_CONFIG.elementWorth = data.elementWorth;
}
if (data.resourceWorth && typeof data.resourceWorth === "object") {
GAME_CONFIG.resourceWorth = data.resourceWorth;
}
@@ -232,8 +242,77 @@ export function lockTeamSwitcher() {
export async function fetchAndApplyScores() {
try {
const { blue, red } = await apiFetchScores();
scoreBlueEl.textContent = String(blue ?? 0);
scoreRedEl.textContent = String(red ?? 0);
if (scoreBlueEl) scoreBlueEl.textContent = String(blue ?? 0);
if (scoreRedEl) scoreRedEl.textContent = String(red ?? 0);
} catch { /* ignore */ }
}
export async function loadVictoryPoints() {
try {
const { blue, red } = await apiFetchVictoryPoints();
if (vpBlueEl) vpBlueEl.textContent = String(blue ?? 0);
if (vpRedEl) vpRedEl.textContent = String(red ?? 0);
} catch { /* ignore */ }
}
// ── Element bonus ─────────────────────────────────────────────────────────────
let elemBonusBlue = 0;
let elemBonusRed = 0;
function updateEffectiveCooldownDisplay() {
const base = GAME_CONFIG.clickCooldownSeconds;
const bonus = currentTeam === "blue" ? elemBonusBlue : elemBonusRed;
const effective = base / (1 + bonus / 100);
if (elemBonusBlueEl) elemBonusBlueEl.textContent = elemBonusBlue.toFixed(2);
if (elemBonusRedEl) elemBonusRedEl.textContent = elemBonusRed.toFixed(2);
if (effectiveCooldownEl) {
effectiveCooldownEl.textContent = effective < 1
? `${(effective * 1000).toFixed(0)}ms`
: `${effective.toFixed(2)}s`;
}
}
export async function loadElementBonus() {
try {
const { blue, red } = await apiFetchElementBonus();
elemBonusBlue = blue ?? 0;
elemBonusRed = red ?? 0;
updateEffectiveCooldownDisplay();
} catch { /* ignore */ }
}
export async function tickElementBonus() {
const worth = GAME_CONFIG.elementWorth;
const blueBonus = computeTeamElementBonus("blue", cells, worth);
const redBonus = computeTeamElementBonus("red", cells, worth);
try {
const result = await apiTickElementBonus(seedStr, blueBonus, redBonus);
elemBonusBlue = result.blue ?? blueBonus;
elemBonusRed = result.red ?? redBonus;
} catch {
elemBonusBlue = blueBonus;
elemBonusRed = redBonus;
}
updateEffectiveCooldownDisplay();
}
export function getEffectiveCooldown() {
const base = GAME_CONFIG.clickCooldownSeconds;
const bonus = currentTeam === "blue" ? elemBonusBlue : elemBonusRed;
return base / (1 + bonus / 100);
}
export async function loadDbInfo() {
try {
const { createdAt } = await apiFetchDbInfo();
if (dbCreatedAtEl && createdAt) {
const d = new Date(createdAt);
dbCreatedAtEl.textContent =
d.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" }) +
" " +
d.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" });
}
} catch { /* ignore */ }
}
@@ -385,7 +464,7 @@ function tickCooldown() {
}
export function startCooldown() {
const secs = GAME_CONFIG.clickCooldownSeconds;
const secs = getEffectiveCooldown();
if (secs <= 0) {
teamCooldownEndMs[currentTeam] = 0;
countdownWrap.classList.add("hidden");

View File

@@ -9,6 +9,10 @@ import {
updateEconomyDisplay,
tickEconScore,
loadEconScores,
loadVictoryPoints,
loadDbInfo,
loadElementBonus,
tickElementBonus,
refreshFromServer,
refreshGridDisplay,
loadPlayfieldMask,
@@ -58,6 +62,7 @@ function scheduleScorePoll() {
scorePollTimer = window.setTimeout(async () => {
await fetchAndApplyScores();
tickEconScore(ECON_TICK_SECONDS);
tickElementBonus();
scheduleScorePoll();
}, ECON_TICK_SECONDS * 1_000);
}
@@ -144,6 +149,9 @@ async function boot() {
await fetchGridForSeed(seedStr);
await fetchAndApplyScores();
await loadEconScores();
await loadVictoryPoints();
await loadDbInfo();
await loadElementBonus();
updateEconomyDisplay();
} catch {
hint.textContent = "API unavailable — start the Node server (docker-compose up --build).";
@@ -159,6 +167,9 @@ async function boot() {
scheduleConfigPoll();
scheduleScorePoll();
// Refresh VP every 30 s so new awards are reflected promptly
setInterval(loadVictoryPoints, 30_000);
// Refresh grid every second so all clients see new tiles promptly
setInterval(refreshGridDisplay, 1_000);
}