zunda_shiwake/src/core/state.js
2026-06-11 01:09:49 +09:00

347 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { resetOwnedCells } from './gridExpansion.js';
import { randomBetween, key, inGrid, directionNameBetweenCells } from './utils.js';
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,
mixerRevenue: 0,
truckRevenue: 0,
chickShipmentIncome: 0,
poopShipmentIncome: 0,
manualComboBonus: 0,
manualComboSuccess: 0,
manualComboFailure: 0,
repairWorkerWages: 0,
fairiesTribute: 0,
chemicalWeaponSubsidy: 0,
rescueLoan: 0,
cardRerollCost: 0,
dudRefundIncome: 0,
composterIncome: 0,
sparePartsRepair: 0,
mixerPoopFine: 0,
truckPoopFine: 0,
maleTruckFine: 0,
explosionDamage: 0,
shredderBonus: 0,
correct: 0,
mistakes: 0,
eventBonus: 0,
explosions: 0,
eventResult: null,
conveyorPasses: 0,
eggProduced: 0
};
}
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,
shredderBonus: 0,
eventBonus: 0,
explosions: 0,
fairiesTribute: 0,
chemicalWeaponSubsidy: 0,
rescueLoan: 0,
manualComboBonus: 0,
manualComboSuccess: 0,
manualComboFailure: 0,
repairWorkerWages: 0,
cardRerollCost: 0,
dudRefundIncome: 0,
composterIncome: 0,
sparePartsRepair: 0,
mixerPoopFine: 0,
truckPoopFine: 0,
maleTruckFine: 0,
eventSuccess: 0,
eventFailed: 0,
conveyorPasses: 0,
eggProduced: 0
};
}
export function createGame() {
return {
phase: 'title',
cash: STARTING_CASH,
turn: 1,
timeLeft: TURN_SECONDS,
shutdownTimeLeft: 0,
shutdownGraceSeconds: FARM_SHUTDOWN_GRACE_SECONDS,
cleanupTimer: null,
cleanupBlown: false,
lastTimestamp: 0,
nextId: 10,
runSeed: createRunSeed(),
gridCols: BALANCE.grid.cols,
gridRows: BALANCE.grid.rows,
gridExpansionPurchases: 0,
gridExpansionPurchasesByDirection: { right: 0, up: 0 },
gridChunkOriginY: 0,
ownedCells: new Set(),
blockedCells: new Set(),
buildTool: null,
buildSession: 0,
undoStack: [],
redoStack: [],
selected: null,
multiSelected: [],
selectionBox: null,
groupDrag: null,
pan: null,
view: { x: 0, y: 0, scale: 1 },
hover: null,
chicks: [],
effects: [],
floatingTexts: [],
shake: { time: 0, strength: 0 },
truckCargo: [],
conveyorTiles: new Set(),
conveyorMeta: new Map(),
eggFarms: [],
scanners: [],
facilities: {},
branchCounters: new Map(),
routingVersion: 0,
factoryGraph: null,
factoryGraphCommittedVersion: -1,
factoryGraphSnapshot: null,
mixerHalfTimer: 0,
congestion: new Map(),
lastExplodedComponent: new Map(),
eventOffer: null,
eventActive: null,
pendingFairiesTributeMultiplier: 1,
cardEffects: { bearing: 0, legalWork: 0, fairiesFlatteryNext: 0, autoScannerTuning: 0, poopRateBooster: 0, crowdedFarming: 0, hatchingFeed: 0, safetyCover: 0, recyclingSubsidy: 0, chemicalWeaponSubsidy: 0, preventiveMaintenance: 0, dudFilter: 0, durabilityCoating: 0, sparePartsBin: 0, dudRefund: 0, freeReroll: 0, composter: 0, scannerQueueSpacing: 0, laborExploitation: 0, usedMachineActive: false, rescueLoanCharges: 0 },
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
cardTargetPick: null,
manualCombo: { count: 0, lastBonus: 0 },
pendingRepairWorkerWages: 0,
repairman: { hiredForNextDay: false, active: false, x: GRID.x + GRID.cell * 0.5, y: GRID.y + BALANCE.grid.rows * GRID.cell + 84, target: null, repairedToday: 0 },
stats: newTurnStats(),
lastResult: null,
totals: newTotalStats()
};
}
export function getSpawnRange(farm) {
return EGG_SPAWN_RANGES[(farm?.level || 1) - 1] || EGG_SPAWN_RANGES[EGG_SPAWN_RANGES.length - 1] || [2, 4];
}
export function nextSpawnDelay(farm) {
const [min, max] = getSpawnRange(farm);
return randomBetween(min, max);
}
function facilityBodyForEntry(id, entry, side) {
const def = FACILITY_DEFS[id];
const spec = { ...def.body, name: def.name, price: def.price };
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 {
// Initial grid is 20 columns × 10 rows. Machine bodies sit outside the starter grid.
mixer: facilityBodyForEntry('mixer', { col: 0, row: 4 }, 'left'),
trash: facilityBodyForEntry('trash', { col: 12, row: 9 }, 'bottom'),
truck: facilityBodyForEntry('truck', { col: 18, row: 9 }, 'bottom')
};
}
const INITIAL_CONVEYOR_BACKBONE = Object.freeze([
// Egg Farm -> S1 top input. The S1/S2 rows were moved down so unrelated
// belt streams keep at least one blank tile between side-by-side flows.
[1, 1], [2, 1], [3, 1], [4, 1], [5, 1], [6, 1], [7, 1], [8, 1], [8, 2],
// S1 left output -> Mixer receiver on the left grid edge.
[7, 4], [6, 4], [5, 4], [4, 4], [3, 4], [2, 4], [1, 4], [0, 4],
// S1 right output -> S2 top input.
[10, 4], [11, 4], [12, 4], [13, 4], [14, 4], [15, 4], [15, 5],
// S2 left output -> Waste Shredder receiver on bottom grid edge.
[14, 7], [13, 7], [12, 7], [12, 8], [12, 9],
// S2 right output -> Truck receiver on the bottom grid edge.
[17, 7], [18, 7], [18, 8], [18, 9]
]);
const INITIAL_SCANNERS = Object.freeze([
{ type: 'scanner', id: 1, kind: 'manual', slot: 0, role: 0, col: 8, row: 4, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard', keys: { left: { key: 'a', code: 'KeyA', label: 'A' }, right: { key: 'd', code: 'KeyD', label: 'D' } } },
{ type: 'scanner', id: 2, kind: 'manual', slot: 1, role: 1, col: 15, row: 7, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard', keys: { left: { key: 'arrowleft', code: 'ArrowLeft', label: 'Left' }, right: { key: 'arrowright', code: 'ArrowRight', label: 'Right' } } }
]);
const INITIAL_EGG_FARMS = Object.freeze([
{ type: 'eggFarm', id: 1, col: 1, 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',
kind: options.kind || 'conveyor',
speedMultiplier: options.speedMultiplier || 1,
dir: options.dir || null,
outDirs: Array.isArray(options.outDirs) ? [...options.outDirs] : [],
branchMode: options.branchMode || 'random'
});
} else if (options.dir || options.outDirs) {
const meta = game.conveyorMeta.get(k);
if (options.dir) meta.dir = options.dir;
if (Array.isArray(options.outDirs)) meta.outDirs = [...new Set([...(meta.outDirs || []), ...options.outDirs])];
if (!meta.branchMode) meta.branchMode = 'random';
if (!meta.kind) meta.kind = 'conveyor';
if (!meta.speedMultiplier) meta.speedMultiplier = 1;
}
}
function ensureConveyorMeta(game, col, row) {
const k = key(col, row);
if (!game.conveyorMeta.has(k)) addConveyorTile(game, col, row);
return game.conveyorMeta.get(k);
}
function markInitialConveyorDirection(game, from, to) {
const dir = directionNameBetweenCells(from, to);
if (!dir) return;
const fromMeta = ensureConveyorMeta(game, from.col, from.row);
fromMeta.outDirs = Array.isArray(fromMeta.outDirs) ? fromMeta.outDirs : [];
if (!fromMeta.outDirs.includes(dir)) fromMeta.outDirs.push(dir);
fromMeta.dir = dir;
const toMeta = ensureConveyorMeta(game, to.col, to.row);
if (!toMeta.dir) toMeta.dir = dir;
}
function scannerPortCells(scanner) {
return [
{ col: scanner.col, row: scanner.row - 2 },
{ col: scanner.col - 1, row: scanner.row },
{ col: scanner.col + 2, row: scanner.row }
].filter(p => inGrid(p.col, p.row));
}
function scannerBodyCells(scanner) {
return [
{ col: scanner.col, row: scanner.row - 1 },
{ col: scanner.col + 1, row: scanner.row - 1 },
{ col: scanner.col, row: scanner.row },
{ col: scanner.col + 1, row: scanner.row },
{ col: scanner.col, row: scanner.row + 1 },
{ col: scanner.col + 1, row: scanner.row + 1 }
].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) {
for (const p of scannerBodyCells(scanner)) add(p);
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) {
const cells = INITIAL_CONVEYOR_BACKBONE.map(([col, row]) => ({ col, row }));
for (const { col, row } of cells) addConveyorTile(game, col, row);
for (let i = 0; i < cells.length - 1; i += 1) markInitialConveyorDirection(game, cells[i], cells[i + 1]);
}
function scrubBlockedCellsFromInitialBackbone(game) {
const protectedCells = protectedInitialCells(game);
for (const k of protectedCells) game.blockedCells.delete(k);
}
export function resetLayout(game) {
GRID.x = BALANCE.grid.x;
GRID.y = BALANCE.grid.y;
GRID.cols = BALANCE.grid.cols;
GRID.rows = BALANCE.grid.rows;
game.gridCols = GRID.cols;
game.gridRows = GRID.rows;
resetOwnedCells(game);
game.gridOwnedCells = game.ownedCells.size;
if (game.repairman) {
game.repairman.x = GRID.x + GRID.cell * 0.5;
game.repairman.y = GRID.y + GRID.rows * GRID.cell + 84;
game.repairman.target = null;
game.repairman.active = false;
game.repairman.hiredForNextDay = false;
}
game.conveyorTiles.clear();
game.conveyorMeta.clear();
game.blockedCells = new Set();
installInitialConveyorBackbone(game);
game.facilities = defaultFacilities();
game.scanners = INITIAL_SCANNERS.map(scanner => ({ ...scanner, keys: scanner.keys ? { left: { ...scanner.keys.left }, right: { ...scanner.keys.right } } : null, 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;
}