147 lines
5.1 KiB
JavaScript
147 lines
5.1 KiB
JavaScript
/**
|
||
* Galaxy Strike Online — Resource System
|
||
* GDD §5: Metal, Gas, Crystal, Energy Cells, Dark Matter
|
||
* Handles production formulas, storage caps, and economy tick integration.
|
||
*/
|
||
|
||
const RESOURCE_TYPES = ['metal', 'gas', 'crystal', 'energyCells', 'darkMatter'];
|
||
|
||
// GDD §5.1 base rates & storage
|
||
const RESOURCE_CONFIG = {
|
||
metal: { baseRate: 100, storageBase: 10000, icon: '⚙', color: '#9e9e9e', label: 'Metal' },
|
||
gas: { baseRate: 60, storageBase: 5000, icon: '☁', color: '#4fc3f7', label: 'Gas' },
|
||
crystal: { baseRate: 40, storageBase: 5000, icon: '💎', color: '#ce93d8', label: 'Crystal' },
|
||
energyCells: { baseRate: 200, storageBase: 8000, icon: '⚡', color: '#fff176', label: 'Energy Cells' },
|
||
darkMatter: { baseRate: 5, storageBase: 500, icon: '✦', color: '#b39ddb', label: 'Dark Matter' },
|
||
};
|
||
|
||
// Building → resource production mapping (GDD §5.2, §6.2)
|
||
const BUILDING_PRODUCTION = {
|
||
mining_facility: { metal: { base: 100, perLevel: 0.15 } },
|
||
gas_extractor: { gas: { base: 60, perLevel: 0.15 } },
|
||
crystal_refinery: { crystal: { base: 40, perLevel: 0.12 } },
|
||
power_reactor: { energyCells:{ base: 200, perLevel: 0.20 } },
|
||
// Dark matter only from endgame structures
|
||
};
|
||
|
||
// Building → storage bonus
|
||
const BUILDING_STORAGE = {
|
||
storage_depot: { all: 2000 },
|
||
command_center:{ all: 500 },
|
||
};
|
||
|
||
class ResourceSystem {
|
||
constructor() {
|
||
this.tickIntervalMs = 60000; // 60-second economy tick per GDD §18.2
|
||
}
|
||
|
||
/** Initialise resources for a new player */
|
||
initResources(playerData) {
|
||
if (playerData.resources) return; // already initialised
|
||
playerData.resources = {
|
||
metal: 500,
|
||
gas: 200,
|
||
crystal: 100,
|
||
energyCells: 300,
|
||
darkMatter: 0,
|
||
lastTick: Date.now(),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Calculate per-hour production rates from buildings.
|
||
* GDD formula: output = base_rate × (1 + 0.15 × level) × research_mult × sector_richness
|
||
*/
|
||
getProductionRates(playerData) {
|
||
const buildings = playerData.buildings || {};
|
||
const research = playerData.research?.effects || {};
|
||
const rates = { metal: 0, gas: 0, crystal: 0, energyCells: 0, darkMatter: 0 };
|
||
|
||
for (const [buildId, prod] of Object.entries(BUILDING_PRODUCTION)) {
|
||
const bld = buildings[buildId];
|
||
if (!bld || bld.level < 1) continue;
|
||
for (const [res, cfg] of Object.entries(prod)) {
|
||
const researchMult = 1 + ((research[`${res}Bonus`] || research['miningBonus'] || 0) / 100);
|
||
rates[res] += Math.floor(cfg.base * (1 + cfg.perLevel * bld.level) * researchMult);
|
||
}
|
||
}
|
||
return rates;
|
||
}
|
||
|
||
/** Calculate storage caps */
|
||
getStorageCaps(playerData) {
|
||
const buildings = playerData.buildings || {};
|
||
const caps = {};
|
||
for (const [res, cfg] of Object.entries(RESOURCE_CONFIG)) {
|
||
let cap = cfg.storageBase;
|
||
for (const [buildId, bonus] of Object.entries(BUILDING_STORAGE)) {
|
||
const bld = buildings[buildId];
|
||
if (!bld || bld.level < 1) continue;
|
||
if (bonus.all) cap += bonus.all * bld.level;
|
||
if (bonus[res]) cap += bonus[res] * bld.level;
|
||
}
|
||
caps[res] = cap;
|
||
}
|
||
return caps;
|
||
}
|
||
|
||
/**
|
||
* Economy tick — called server-side every 60s.
|
||
* Returns { produced, capped } summary.
|
||
*/
|
||
tick(playerData) {
|
||
this.initResources(playerData);
|
||
const now = Date.now();
|
||
const elapsed = (now - (playerData.resources.lastTick || now)) / 3600000; // hours
|
||
if (elapsed <= 0) return { produced: {}, capped: {} };
|
||
|
||
const rates = this.getProductionRates(playerData);
|
||
const caps = this.getStorageCaps(playerData);
|
||
const res = playerData.resources;
|
||
const produced = {};
|
||
const capped = {};
|
||
|
||
for (const r of RESOURCE_TYPES) {
|
||
const gain = Math.floor(rates[r] * elapsed);
|
||
if (gain <= 0) continue;
|
||
produced[r] = gain;
|
||
const before = res[r] || 0;
|
||
res[r] = Math.min(before + gain, caps[r]);
|
||
if (res[r] === caps[r] && before + gain > caps[r]) capped[r] = true;
|
||
}
|
||
res.lastTick = now;
|
||
return { produced, capped };
|
||
}
|
||
|
||
/** Spend resources — throws if insufficient */
|
||
spend(playerData, costs) {
|
||
this.initResources(playerData);
|
||
const res = playerData.resources;
|
||
for (const [r, amount] of Object.entries(costs)) {
|
||
if ((res[r] || 0) < amount) {
|
||
const label = RESOURCE_CONFIG[r]?.label || r;
|
||
throw new Error(`Insufficient ${label}: need ${amount}, have ${res[r] || 0}`);
|
||
}
|
||
}
|
||
for (const [r, amount] of Object.entries(costs)) {
|
||
res[r] = (res[r] || 0) - amount;
|
||
}
|
||
}
|
||
|
||
/** Add resources (from mining, loot, etc.) */
|
||
add(playerData, gains) {
|
||
this.initResources(playerData);
|
||
const caps = this.getStorageCaps(playerData);
|
||
const res = playerData.resources;
|
||
for (const [r, amount] of Object.entries(gains)) {
|
||
res[r] = Math.min((res[r] || 0) + amount, caps[r] || 999999);
|
||
}
|
||
}
|
||
|
||
getConfig() { return RESOURCE_CONFIG; }
|
||
getTypes() { return RESOURCE_TYPES; }
|
||
getBuildingProd() { return BUILDING_PRODUCTION; }
|
||
}
|
||
|
||
module.exports = { ResourceSystem, RESOURCE_CONFIG, RESOURCE_TYPES };
|