airship-phaser/src/progression/ship-loadout.js

155 lines
5.4 KiB
JavaScript

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;
};