fix: Economy is calculated server-side and not browser-side anymore
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"clickCooldownSeconds": 0,
|
"clickCooldownSeconds": 10,
|
||||||
"databaseWipeoutIntervalSeconds": 21600,
|
"databaseWipeoutIntervalSeconds": 21600,
|
||||||
"debugModeForTeams": true,
|
"debugModeForTeams": true,
|
||||||
"configReloadIntervalSeconds": 30,
|
"configReloadIntervalSeconds": 30,
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import {
|
|||||||
fetchGridForSeed,
|
fetchGridForSeed,
|
||||||
fetchAndApplyScores,
|
fetchAndApplyScores,
|
||||||
updateEconomyDisplay,
|
updateEconomyDisplay,
|
||||||
tickEconScore,
|
|
||||||
loadEconScores,
|
loadEconScores,
|
||||||
loadVictoryPoints,
|
loadVictoryPoints,
|
||||||
loadDbInfo,
|
loadDbInfo,
|
||||||
loadElementBonus,
|
loadElementBonus,
|
||||||
tickElementBonus,
|
|
||||||
refreshFromServer,
|
refreshFromServer,
|
||||||
refreshGridDisplay,
|
refreshGridDisplay,
|
||||||
loadPlayfieldMask,
|
loadPlayfieldMask,
|
||||||
@@ -61,8 +59,8 @@ function scheduleScorePoll() {
|
|||||||
clearTimeout(scorePollTimer);
|
clearTimeout(scorePollTimer);
|
||||||
scorePollTimer = window.setTimeout(async () => {
|
scorePollTimer = window.setTimeout(async () => {
|
||||||
await fetchAndApplyScores();
|
await fetchAndApplyScores();
|
||||||
tickEconScore(ECON_TICK_SECONDS);
|
await loadEconScores();
|
||||||
tickElementBonus();
|
await loadElementBonus();
|
||||||
scheduleScorePoll();
|
scheduleScorePoll();
|
||||||
}, ECON_TICK_SECONDS * 1_000);
|
}, ECON_TICK_SECONDS * 1_000);
|
||||||
}
|
}
|
||||||
|
|||||||
48
server/econTick.js
Normal file
48
server/econTick.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { getConfig } from "./configLoader.js";
|
||||||
|
import {
|
||||||
|
ensureSeedEpoch,
|
||||||
|
getGridCells,
|
||||||
|
addEconScore,
|
||||||
|
setElementBonus,
|
||||||
|
} from "./db/gameDb.js";
|
||||||
|
import { computeTeamIncome, computeTeamElementBonus } from "./helpers/economy.js";
|
||||||
|
|
||||||
|
const TICK_SECONDS = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the server-side economy tick loop.
|
||||||
|
* Runs every TICK_SECONDS, reads the current grid from the DB, computes
|
||||||
|
* income and element bonus for each team, and persists the results.
|
||||||
|
* This runs independently of any connected browser.
|
||||||
|
*/
|
||||||
|
export function startEconTick() {
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const worldSeed = await ensureSeedEpoch();
|
||||||
|
const rows = await getGridCells(worldSeed);
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
const resourceWorth = cfg.resourceWorth ?? { common: {}, rare: {} };
|
||||||
|
const elementWorth = cfg.elementWorth ?? {};
|
||||||
|
|
||||||
|
// ── Economic score deltas ─────────────────────────────────────────────
|
||||||
|
const blueIncome = computeTeamIncome("blue", rows, resourceWorth);
|
||||||
|
const redIncome = computeTeamIncome("red", rows, resourceWorth);
|
||||||
|
|
||||||
|
const blueDelta = blueIncome * TICK_SECONDS;
|
||||||
|
const redDelta = redIncome * TICK_SECONDS;
|
||||||
|
|
||||||
|
if (blueDelta > 0) await addEconScore(worldSeed, "blue", blueDelta);
|
||||||
|
if (redDelta > 0) await addEconScore(worldSeed, "red", redDelta);
|
||||||
|
|
||||||
|
// ── Element bonus (overwrites; reflects current grid state) ──────────
|
||||||
|
const blueBonus = computeTeamElementBonus("blue", rows, elementWorth);
|
||||||
|
const redBonus = computeTeamElementBonus("red", rows, elementWorth);
|
||||||
|
|
||||||
|
await setElementBonus(worldSeed, "blue", blueBonus);
|
||||||
|
await setElementBonus(worldSeed, "red", redBonus);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[econ tick]", e.message);
|
||||||
|
}
|
||||||
|
}, TICK_SECONDS * 1_000);
|
||||||
|
}
|
||||||
124
server/helpers/economy.js
Normal file
124
server/helpers/economy.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// Server-side economy helpers — mirrors public/src/economy.js
|
||||||
|
// Uses raw DB rows instead of a client-side cells Map.
|
||||||
|
|
||||||
|
// ── Label constants (mirrors public/src/planetEconomy.js) ─────────────────────
|
||||||
|
|
||||||
|
const resources = {
|
||||||
|
common: {
|
||||||
|
rock: "Roches communes",
|
||||||
|
wood: "Bois communs",
|
||||||
|
mineral: "Minérais communs",
|
||||||
|
stones: "Gemmes communes",
|
||||||
|
liquid: "Eau salée",
|
||||||
|
oil: "Fioul brut",
|
||||||
|
gas: "Gaz communs",
|
||||||
|
grain: "Céréales",
|
||||||
|
livestock: "Bétail commun",
|
||||||
|
fish: "Poissons commun",
|
||||||
|
plant: "Plantes communes",
|
||||||
|
goods: "Biens de consommation",
|
||||||
|
animals: "Animaux domestiques",
|
||||||
|
science: "Sites archéologiques",
|
||||||
|
factory: "Usines standards",
|
||||||
|
acid: "Acides pauvres",
|
||||||
|
},
|
||||||
|
rare: {
|
||||||
|
rock: "Roches rares",
|
||||||
|
wood: "Bois renforcés",
|
||||||
|
mineral: "Minérais rares",
|
||||||
|
stones: "Gemmes rares",
|
||||||
|
liquid: "Eau douce",
|
||||||
|
oil: "Fioul raffiné",
|
||||||
|
gas: "Gaz nobles",
|
||||||
|
grain: "Fruits",
|
||||||
|
livestock: "Bétail raffiné",
|
||||||
|
fish: "Poissons raffinés",
|
||||||
|
plant: "Plantes rares",
|
||||||
|
goods: "Biens de luxe",
|
||||||
|
animals: "Animaux exotiques",
|
||||||
|
science: "Artéfacts anciens",
|
||||||
|
factory: "Usines planétaires",
|
||||||
|
acid: "Acides riches",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const elements = {
|
||||||
|
common: "Matières premières",
|
||||||
|
petrol: "Hydrocarbures",
|
||||||
|
food: "Nourriture",
|
||||||
|
medic: "Médicaments",
|
||||||
|
science: "Science",
|
||||||
|
industry: "Industrie",
|
||||||
|
money: "Finance",
|
||||||
|
goods: "Biens",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Pre-built lookup maps ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** French resource label → { cat: "common"|"rare", key: string } */
|
||||||
|
const LABEL_TO_RESOURCE = new Map();
|
||||||
|
for (const [cat, entries] of Object.entries(resources)) {
|
||||||
|
for (const [key, label] of Object.entries(entries)) {
|
||||||
|
LABEL_TO_RESOURCE.set(label, { cat, key });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** French element label → config key (e.g. "Matières premières" → "common") */
|
||||||
|
const ELEMENT_LABEL_TO_KEY = Object.fromEntries(
|
||||||
|
Object.entries(elements).map(([key, label]) => [label, key])
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Income computation ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute total income per second for a team from DB grid rows.
|
||||||
|
*
|
||||||
|
* @param {string} team - "blue" or "red"
|
||||||
|
* @param {Array<{ has_planet: boolean, planet_json: object|null, discovered_by: string }>} rows
|
||||||
|
* @param {{ common: object, rare: object }} resourceWorth
|
||||||
|
* @returns {number} credits per second
|
||||||
|
*/
|
||||||
|
export function computeTeamIncome(team, rows, resourceWorth) {
|
||||||
|
let total = 0;
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.discovered_by !== team) continue;
|
||||||
|
if (!row.has_planet || !row.planet_json) continue;
|
||||||
|
const { naturalResources } = row.planet_json;
|
||||||
|
if (!naturalResources) continue;
|
||||||
|
for (const [label, pct] of Object.entries(naturalResources)) {
|
||||||
|
const info = LABEL_TO_RESOURCE.get(label);
|
||||||
|
if (!info) continue;
|
||||||
|
const worth = resourceWorth?.[info.cat]?.[info.key] ?? 0;
|
||||||
|
if (worth === 0) continue;
|
||||||
|
total += (pct / 100) * worth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Element bonus computation ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute cumulative element bonus (%) for a team from DB grid rows.
|
||||||
|
*
|
||||||
|
* @param {string} team - "blue" or "red"
|
||||||
|
* @param {Array<{ has_planet: boolean, planet_json: object|null, discovered_by: string }>} rows
|
||||||
|
* @param {object} elementWorth - { common: 1, petrol: 3, ... }
|
||||||
|
* @returns {number} bonus in percent
|
||||||
|
*/
|
||||||
|
export function computeTeamElementBonus(team, rows, elementWorth) {
|
||||||
|
let bonus = 0;
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.discovered_by !== team) continue;
|
||||||
|
if (!row.has_planet || !row.planet_json) continue;
|
||||||
|
const { production } = row.planet_json;
|
||||||
|
if (!production) continue;
|
||||||
|
for (const [elementLabel, pct] of Object.entries(production)) {
|
||||||
|
const elementKey = ELEMENT_LABEL_TO_KEY[elementLabel] ?? elementLabel;
|
||||||
|
const worth = elementWorth?.[elementKey] ?? 0;
|
||||||
|
if (worth === 0) continue;
|
||||||
|
bonus += (pct / 100) * worth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bonus;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { loadConfigFile, getConfig } from "./configLoader.js";
|
|||||||
import { initGameSchema, ensureSeedEpoch } from "./db/gameDb.js";
|
import { initGameSchema, ensureSeedEpoch } from "./db/gameDb.js";
|
||||||
import { initUsersSchema } from "./db/usersDb.js";
|
import { initUsersSchema } from "./db/usersDb.js";
|
||||||
import app from "./app.js";
|
import app from "./app.js";
|
||||||
|
import { startEconTick } from "./econTick.js";
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT ?? 8080);
|
const PORT = Number(process.env.PORT ?? 8080);
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
scheduleConfigPoll();
|
scheduleConfigPoll();
|
||||||
|
startEconTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => {
|
main().catch((e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user