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, zundaTax: 0, fairiesTribute: 0, loanRepayment: 0, chemicalWeaponSubsidy: 0, rescueLoan: 0, cardRerollCost: 0, mixerPoopFine: 0, truckPoopFine: 0, maleTruckFine: 0, explosionDamage: 0, shredderBonus: 0, correct: 0, mistakes: 0, contractBonus: 0, contractResult: 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, contractBonus: 0, zundaTax: 0, fairiesTribute: 0, loanRepayment: 0, chemicalWeaponSubsidy: 0, rescueLoan: 0, manualComboBonus: 0, manualComboSuccess: 0, manualComboFailure: 0, repairWorkerWages: 0, cardRerollCost: 0, mixerPoopFine: 0, truckPoopFine: 0, maleTruckFine: 0, contractSuccess: 0, contractFailed: 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(), contractOffer: null, contractActive: null, cardEffects: { bearing: 0, legalWork: 0, fairiesFlatteryNext: 0, crowdedFarming: 0, extraEggOutlet: 0, hatchingFeed: 0, safetyCover: 0, recyclingSubsidy: 0, chemicalWeaponSubsidy: 0, preventiveMaintenance: 0, dudFilter: 0, durabilityCoating: 0, laborExploitation: 0, usedMachineActive: false, rescueLoanCharges: 0, loans: [] }, 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], [8, 3], // 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. [9, 4], [10, 4], [11, 4], [12, 4], [13, 4], [14, 4], [15, 4], [15, 5], [15, 6], // 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. [16, 7], [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, extraEggOutlet: 0, 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', dir: options.dir || null, outDirs: Array.isArray(options.outDirs) ? [...options.outDirs] : [] }); } 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])]; } } 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 - 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) { 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; }