import { MODULE_CATALOG, SHIP_BASE_STATS, STARTER_LOADOUT, purchasableModules, } from './modules.js'; import { createSeededRandom } from '../world/seeded-random.js'; const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); export const createStarterProgression = () => ({ credits: 650, scrap: 0, loadout: { ...STARTER_LOADOUT }, inventory: [], }); export const deriveShipStats = (loadout = STARTER_LOADOUT) => { const stats = { ...SHIP_BASE_STATS }; for (const moduleId of Object.values(loadout)) { if (!moduleId) continue; const module = MODULE_CATALOG[moduleId]; if (!module) continue; for (const [key, value] of Object.entries(module.modifiers || {})) { stats[key] = (stats[key] ?? 0) + value; } } stats.maxHull = Math.max(1, stats.maxHull); stats.maxSpeed = Math.max(40, stats.maxSpeed); stats.acceleration = Math.max(10, stats.acceleration); stats.turnRate = Math.max(15, stats.turnRate); stats.reloadMs = Math.max(260, stats.reloadMs); stats.cannonDamage = Math.max(1, stats.cannonDamage); stats.windExposure = clamp(stats.windExposure, 0.35, 1.5); stats.cargoCapacity = Math.max(1, Math.floor(stats.cargoCapacity)); stats.salvageCapacity = Math.max(1, Math.floor(stats.salvageCapacity)); return stats; }; export const installModule = (progression, moduleId) => { const module = MODULE_CATALOG[moduleId]; if (!module) return { ...progression, installed: false, reason: 'unknown-module' }; const inventory = [...(progression.inventory || [])]; const inventoryIndex = inventory.indexOf(moduleId); if (inventoryIndex >= 0) inventory.splice(inventoryIndex, 1); const loadout = { ...(progression.loadout || STARTER_LOADOUT) }; const replaced = loadout[module.slot] || null; loadout[module.slot] = moduleId; if (replaced && MODULE_CATALOG[replaced]?.price > 0) inventory.push(replaced); return { ...progression, loadout, inventory, installed: true, replaced, installedModule: moduleId, }; }; export const buyAndInstallModule = (progression, moduleId) => { const module = MODULE_CATALOG[moduleId]; if (!module || module.price <= 0) return { ...progression, purchased: false, reason: 'not-for-sale' }; if ((progression.credits || 0) < module.price) { return { ...progression, purchased: false, reason: 'insufficient-credits' }; } const purchased = { ...progression, credits: progression.credits - module.price, inventory: [...(progression.inventory || []), moduleId], }; return { ...installModule(purchased, moduleId), purchased: true }; }; export const marketForPort = (port) => { const stock = purchasableModules().filter((module) => { if (module.slot === 'rudder') return true; if (port?.type === 'industrial') return ['engine', 'hull', 'utility', 'cargo'].includes(module.slot); if (port?.type === 'military') return ['weapon', 'hull', 'navigation'].includes(module.slot); if (port?.type === 'fuel') return ['engine', 'navigation', 'cargo'].includes(module.slot); return true; }); return stock.sort((a, b) => a.price - b.price); }; export const salvageForPirate = ({ seed = 'salvage', classKey = 'raider', wave = 1, sequence = 0, } = {}) => { const random = createSeededRandom(`${seed}:${classKey}:${wave}:${sequence}`); const chance = classKey === 'captain' ? 0.86 : classKey === 'frigate' ? 0.62 : 0.42; const modulePool = purchasableModules(); const module = random() < chance ? modulePool[Math.floor(random() * modulePool.length)] : null; const scrap = 1 + Math.floor(random() * (classKey === 'captain' ? 8 : classKey === 'frigate' ? 5 : 3)); const credits = 20 + Math.floor(random() * (35 + wave * 8)); return { moduleId: module?.id || null, scrap, credits, condition: module ? clamp(0.28 + random() * 0.67, 0.28, 0.95) : null, }; }; export const collectSalvage = (progression, salvage, stats = deriveShipStats(progression.loadout)) => { const inventory = [...(progression.inventory || [])]; let moduleCollected = false; if (salvage?.moduleId && inventory.length < stats.salvageCapacity) { inventory.push(salvage.moduleId); moduleCollected = true; } return { ...progression, inventory, scrap: (progression.scrap || 0) + Math.max(0, salvage?.scrap || 0), credits: (progression.credits || 0) + Math.max(0, salvage?.credits || 0), lastSalvage: { ...salvage, moduleCollected }, }; }; export const bestAffordableMarketUpgrade = (progression, port) => { const stats = deriveShipStats(progression.loadout); const candidates = marketForPort(port) .filter((module) => module.price <= (progression.credits || 0)) .filter((module) => progression.loadout?.[module.slot] !== module.id) .map((module) => { const next = deriveShipStats({ ...progression.loadout, [module.slot]: module.id }); const score = (next.maxHull - stats.maxHull) * 0.7 + (next.maxSpeed - stats.maxSpeed) + (next.acceleration - stats.acceleration) * 0.45 + (next.turnRate - stats.turnRate) * 0.35 + (stats.reloadMs - next.reloadMs) * 0.06 + (next.cannonDamage - stats.cannonDamage) * 5 + (stats.windExposure - next.windExposure) * 30 + (next.salvageCapacity - stats.salvageCapacity) * 8 + (next.cargoCapacity - stats.cargoCapacity) * 4; return { module, score }; }) .sort((a, b) => b.score - a.score); return candidates[0]?.module || null; };