/** * 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 × season_mult × event_mult * @param {object} playerData * @param {object} [seasonBonuses] - from SeasonSystem.getSeasonBonuses() * @param {object} [activeEvent] - from GalaxyEventSystem.getEventStatus() */ getProductionRates(playerData, seasonBonuses = {}, activeEvent = null) { const buildings = playerData.buildings || {}; const research = playerData.research?.effects || {}; const rates = { metal: 0, gas: 0, crystal: 0, energyCells: 0, darkMatter: 0 }; // Season multipliers const seasonDarkMatterMult = 1 + (seasonBonuses.darkMatterBonus || 0); // Active event multipliers const eventBonuses = (activeEvent?.active && activeEvent?.bonuses) ? activeEvent.bonuses : {}; const eventDarkMatterMult = eventBonuses.darkMatterMultiplier || 1; 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); let rate = Math.floor(cfg.base * (1 + cfg.perLevel * bld.level) * researchMult); if (res === 'darkMatter') rate = Math.floor(rate * seasonDarkMatterMult * eventDarkMatterMult); rates[res] += rate; } } // Dark matter siphon building const dmSiphon = buildings['dark_matter_siphon']; if (dmSiphon && dmSiphon.level >= 1) { rates.darkMatter += Math.floor(5 * dmSiphon.level * seasonDarkMatterMult * eventDarkMatterMult); } 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, seasonBonuses = {}, activeEvent = null) { 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, seasonBonuses, activeEvent); 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 };