155 lines
5.4 KiB
JavaScript
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;
|
|
};
|