zunda_shiwake/src/core/state.js

266 lines
8.9 KiB
JavaScript
Raw Normal View History

2026-06-08 13:18:33 +09:00
import { STARTING_CASH, TURN_SECONDS, FARM_SHUTDOWN_GRACE_SECONDS, BUILD_COSTS, FACILITY_DEFS, GRID, EGG_SPAWN_RANGES } from './config.js';
import { BALANCE } from './balance.js';
import { createRunSeed, generateBlockedCells } from './mapGen.js';
import { randomBetween, key, inGrid } from './utils.js';
2026-06-07 15:05:24 +09:00
export function newTurnStats() {
return {
mixerCount: 0,
truckFemale: 0,
truckMale: 0,
trashCount: 0,
poopSpawned: 0,
poopTruck: 0,
poopTrash: 0,
poopMixer: 0,
autoSorted: 0,
revenue: 0,
penalty: 0,
profit: 0,
pendingTruckRevenue: 0,
2026-06-07 18:06:35 +09:00
mixerRevenue: 0,
truckRevenue: 0,
2026-06-07 20:36:52 +09:00
zundaTax: 0,
2026-06-07 22:04:17 +09:00
fairiesTribute: 0,
2026-06-08 18:01:20 +09:00
loanRepayment: 0,
chemicalWeaponSubsidy: 0,
rescueLoan: 0,
2026-06-07 22:04:17 +09:00
cardRerollCost: 0,
2026-06-07 18:06:35 +09:00
mixerPoopFine: 0,
truckPoopFine: 0,
maleTruckFine: 0,
2026-06-07 15:05:24 +09:00
explosionDamage: 0,
2026-06-08 13:18:33 +09:00
shredderBonus: 0,
2026-06-07 15:05:24 +09:00
correct: 0,
mistakes: 0,
contractBonus: 0,
2026-06-08 13:18:33 +09:00
contractResult: null,
conveyorPasses: 0,
eggProduced: 0
2026-06-07 15:05:24 +09:00
};
}
export function newTotalStats() {
return {
turnsCompleted: 0,
processed: 0,
correct: 0,
mistakes: 0,
revenue: 0,
penalty: 0,
profit: 0,
maxCash: STARTING_CASH,
autoSorted: 0,
poopTruck: 0,
poopTrash: 0,
poopMixer: 0,
explosionDamage: 0,
2026-06-08 13:18:33 +09:00
shredderBonus: 0,
2026-06-07 15:05:24 +09:00
contractBonus: 0,
2026-06-07 20:36:52 +09:00
zundaTax: 0,
2026-06-07 22:04:17 +09:00
fairiesTribute: 0,
2026-06-08 18:01:20 +09:00
loanRepayment: 0,
chemicalWeaponSubsidy: 0,
rescueLoan: 0,
2026-06-07 22:04:17 +09:00
cardRerollCost: 0,
2026-06-07 18:06:35 +09:00
mixerPoopFine: 0,
truckPoopFine: 0,
maleTruckFine: 0,
2026-06-07 15:05:24 +09:00
contractSuccess: 0,
2026-06-08 13:18:33 +09:00
contractFailed: 0,
conveyorPasses: 0,
eggProduced: 0
2026-06-07 15:05:24 +09:00
};
}
export function createGame() {
return {
phase: 'title',
cash: STARTING_CASH,
turn: 1,
timeLeft: TURN_SECONDS,
2026-06-07 18:06:35 +09:00
shutdownTimeLeft: 0,
shutdownGraceSeconds: FARM_SHUTDOWN_GRACE_SECONDS,
2026-06-08 13:18:33 +09:00
cleanupTimer: null,
cleanupBlown: false,
2026-06-07 15:05:24 +09:00
lastTimestamp: 0,
nextId: 10,
2026-06-08 13:18:33 +09:00
runSeed: createRunSeed(),
blockedCells: new Set(),
2026-06-07 15:05:24 +09:00
buildTool: null,
buildSession: 0,
undoStack: [],
redoStack: [],
selected: null,
multiSelected: [],
selectionBox: null,
groupDrag: null,
pan: null,
view: { x: 0, y: 0 },
2026-06-08 13:18:33 +09:00
hover: null,
2026-06-07 15:05:24 +09:00
chicks: [],
effects: [],
floatingTexts: [],
shake: { time: 0, strength: 0 },
truckCargo: [],
conveyorTiles: new Set(),
conveyorMeta: new Map(),
eggFarms: [],
scanners: [],
facilities: {},
branchCounters: new Map(),
2026-06-08 13:18:33 +09:00
routingVersion: 0,
factoryGraph: null,
factoryGraphCommittedVersion: -1,
factoryGraphSnapshot: null,
2026-06-07 15:05:24 +09:00
mixerHalfTimer: 0,
congestion: new Map(),
lastExplodedComponent: new Map(),
contractOffer: null,
contractActive: null,
2026-06-08 18:01:20 +09:00
cardEffects: { bearing: 0, legalWork: 0, fairiesFlatteryNext: 0, crowdedFarming: 0, extraEggOutlet: 0, hatchingFeed: 0, safetyCover: 0, recyclingSubsidy: 0, chemicalWeaponSubsidy: 0, preventiveMaintenance: 0, durabilityCoating: 0, laborExploitation: 0, usedMachineActive: false, rescueLoanCharges: 0, loans: [] },
2026-06-07 22:04:17 +09:00
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
cardTargetPick: null,
2026-06-08 13:18:33 +09:00
repairman: { hiredForNextDay: false, active: false, x: GRID.x + GRID.cell * 0.5, y: GRID.y + GRID.rows * GRID.cell + 84, target: null, repairedToday: 0 },
2026-06-07 15:05:24 +09:00
stats: newTurnStats(),
lastResult: null,
totals: newTotalStats()
};
}
export function getSpawnRange(farm) {
2026-06-08 13:18:33 +09:00
return EGG_SPAWN_RANGES[(farm?.level || 1) - 1] || EGG_SPAWN_RANGES[EGG_SPAWN_RANGES.length - 1] || [2, 4];
2026-06-07 15:05:24 +09:00
}
export function nextSpawnDelay(farm) {
const [min, max] = getSpawnRange(farm);
return randomBetween(min, max);
}
function facilityBodyForEntry(id, entry, side) {
2026-06-07 20:36:52 +09:00
const def = FACILITY_DEFS[id];
const spec = { ...def.body, name: def.name, price: def.price };
2026-06-07 15:05:24 +09:00
const g = { left: GRID.x, right: GRID.x + GRID.cols * GRID.cell, top: GRID.y, bottom: GRID.y + GRID.rows * GRID.cell };
const c = { x: GRID.x + entry.col * GRID.cell + GRID.cell / 2, y: GRID.y + entry.row * GRID.cell + GRID.cell / 2 };
let x = c.x - spec.w / 2;
let y = c.y - spec.h / 2;
if (side === 'left') { x = g.left - spec.w - 18; y = c.y - spec.h / 2; }
else if (side === 'right') { x = g.right + 18; y = c.y - spec.h / 2; }
else if (side === 'bottom') { x = c.x - spec.w / 2; y = g.bottom + 18; }
else if (side === 'top') { x = c.x - spec.w / 2; y = g.top - spec.h - 18; }
return { type: 'facility', id, name: spec.name, x, y, w: spec.w, h: spec.h, level: 1, price: spec.price, entry: { ...entry }, side };
}
export function defaultFacilities() {
return {
// Machine bodies sit outside the build grid. Only these receiver cells sit on the grid edge.
2026-06-07 20:36:52 +09:00
mixer: facilityBodyForEntry('mixer', { col: 0, row: 5 }, 'left'),
2026-06-07 18:06:35 +09:00
trash: facilityBodyForEntry('trash', { col: 10, row: 12 }, 'bottom'),
2026-06-07 20:36:52 +09:00
truck: facilityBodyForEntry('truck', { col: 17, row: 12 }, 'bottom')
2026-06-07 15:05:24 +09:00
};
}
2026-06-08 13:18:33 +09:00
const INITIAL_CONVEYOR_BACKBONE = Object.freeze([
// Egg Farm -> S1 top input. S1 starts lower so the farm is not pressed against it.
[4, 1], [4, 2], [4, 3], [4, 4],
// S1 left output -> Mixer receiver on the left grid edge.
[3, 5], [2, 5], [1, 5], [0, 5],
// S1 right output -> S2 top input.
[5, 5], [6, 5], [7, 5], [8, 5], [9, 5], [10, 5], [11, 5],
[11, 6], [11, 7],
// S2 left output -> Waste Shredder receiver on bottom grid edge.
[10, 8], [10, 9], [10, 10], [10, 11], [10, 12],
// S2 right output -> Truck receiver on the bottom grid edge.
[12, 8], [13, 8], [14, 8], [15, 8], [16, 8], [17, 8], [17, 9], [17, 10], [17, 11], [17, 12]
]);
const INITIAL_SCANNERS = Object.freeze([
{ type: 'scanner', id: 1, kind: 'manual', slot: 0, role: 0, col: 4, row: 5, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard' },
{ type: 'scanner', id: 2, kind: 'manual', slot: 1, role: 1, col: 11, row: 8, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard' }
]);
const INITIAL_EGG_FARMS = Object.freeze([
{ type: 'eggFarm', id: 1, col: 4, row: 0, level: 1, nextSpawn: 2, lastInterval: 2, spawnCounter: 0, price: BUILD_COSTS.eggFarm, builtSession: null }
]);
function addConveyorTile(game, col, row, options = {}) {
if (!inGrid(col, row)) return;
const k = key(col, row);
game.conveyorTiles.add(k);
if (!game.conveyorMeta.has(k) || options.forceMeta) {
game.conveyorMeta.set(k, {
price: BUILD_COSTS.conveyor,
builtSession: null,
uses: 0,
durability: BALANCE.maintenance.durability.conveyor,
maintenanceType: 'conveyor'
});
}
}
function scannerPortCells(scanner) {
return [
{ col: scanner.col, row: scanner.row - 1 },
{ col: scanner.col - 1, row: scanner.row },
{ col: scanner.col + 1, row: scanner.row }
].filter(p => inGrid(p.col, p.row));
}
function farmOutputCells(farm) {
return [
{ col: farm.col, row: farm.row + 1 },
{ col: farm.col - 1, row: farm.row },
{ col: farm.col + 1, row: farm.row },
{ col: farm.col, row: farm.row - 1 }
].filter(p => inGrid(p.col, p.row));
}
function protectedInitialCells(game) {
const reserved = new Set([...game.conveyorTiles]);
const add = p => { if (p && inGrid(p.col, p.row)) reserved.add(key(p.col, p.row)); };
for (const scanner of game.scanners) {
add(scanner);
for (const p of scannerPortCells(scanner)) add(p);
}
for (const farm of game.eggFarms) {
add(farm);
for (const p of farmOutputCells(farm)) add(p);
}
for (const f of Object.values(game.facilities)) add(f.entry);
return reserved;
}
function installInitialConveyorBackbone(game) {
for (const [col, row] of INITIAL_CONVEYOR_BACKBONE) addConveyorTile(game, col, row);
}
function scrubBlockedCellsFromInitialBackbone(game) {
const protectedCells = protectedInitialCells(game);
for (const k of protectedCells) game.blockedCells.delete(k);
}
2026-06-07 15:05:24 +09:00
export function resetLayout(game) {
game.conveyorTiles.clear();
game.conveyorMeta.clear();
2026-06-08 13:18:33 +09:00
game.blockedCells = new Set();
installInitialConveyorBackbone(game);
2026-06-07 15:05:24 +09:00
game.facilities = defaultFacilities();
2026-06-08 13:18:33 +09:00
game.scanners = INITIAL_SCANNERS.map(scanner => ({ ...scanner, queue: [] }));
game.eggFarms = INITIAL_EGG_FARMS.map(farm => {
const nextSpawn = nextSpawnDelay(farm);
return { ...farm, nextSpawn, lastInterval: nextSpawn };
});
// Generate no-build cells only after the complete initial factory footprint is
// installed. The protected footprint includes every default conveyor segment,
// scanner body/ports, farm output, and machine receiver. This guarantees that
// the starter line is always continuous from Egg Farm -> S1 -> S2 -> exits even
// when roughly 20% of the map is blocked.
const reserved = protectedInitialCells(game);
game.blockedCells = generateBlockedCells({ seed: game.runSeed, targetRatio: BALANCE.map.blockedRatio, reserved: [...reserved] });
scrubBlockedCellsFromInitialBackbone(game);
installInitialConveyorBackbone(game);
game.routingVersion = (game.routingVersion || 0) + 1;
game.factoryGraph = null;
game.factoryGraphCommittedVersion = -1;
2026-06-07 15:05:24 +09:00
}