2026-06-07 18:06:35 +09:00
|
|
|
import { STARTING_CASH, TURN_SECONDS, FARM_SHUTDOWN_GRACE_SECONDS, BUILD_COSTS, FACILITY_PRICES, GRID } from './config.js';
|
2026-06-07 15:05:24 +09:00
|
|
|
import { randomBetween } 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,
|
2026-06-07 18:06:35 +09:00
|
|
|
mixerRevenue: 0,
|
|
|
|
|
truckRevenue: 0,
|
|
|
|
|
fairiesFee: 0,
|
|
|
|
|
mixerPoopFine: 0,
|
|
|
|
|
truckPoopFine: 0,
|
|
|
|
|
maleTruckFine: 0,
|
2026-06-07 15:05:24 +09:00
|
|
|
explosionDamage: 0,
|
|
|
|
|
correct: 0,
|
|
|
|
|
mistakes: 0,
|
|
|
|
|
contractBonus: 0,
|
|
|
|
|
contractResult: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
contractBonus: 0,
|
2026-06-07 18:06:35 +09:00
|
|
|
fairiesFee: 0,
|
|
|
|
|
mixerPoopFine: 0,
|
|
|
|
|
truckPoopFine: 0,
|
|
|
|
|
maleTruckFine: 0,
|
2026-06-07 15:05:24 +09:00
|
|
|
contractSuccess: 0,
|
|
|
|
|
contractFailed: 0
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-07 15:05:24 +09:00
|
|
|
lastTimestamp: 0,
|
|
|
|
|
nextId: 10,
|
|
|
|
|
buildTool: null,
|
|
|
|
|
buildSession: 0,
|
|
|
|
|
undoStack: [],
|
|
|
|
|
redoStack: [],
|
|
|
|
|
selected: null,
|
|
|
|
|
multiSelected: [],
|
|
|
|
|
selectionBox: null,
|
|
|
|
|
groupDrag: null,
|
|
|
|
|
pan: null,
|
|
|
|
|
view: { x: 0, y: 0 },
|
|
|
|
|
debug: false,
|
|
|
|
|
chicks: [],
|
|
|
|
|
effects: [],
|
|
|
|
|
floatingTexts: [],
|
|
|
|
|
shake: { time: 0, strength: 0 },
|
|
|
|
|
truckCargo: [],
|
|
|
|
|
conveyorTiles: new Set(),
|
|
|
|
|
conveyorMeta: new Map(),
|
|
|
|
|
eggFarms: [],
|
|
|
|
|
scanners: [],
|
|
|
|
|
facilities: {},
|
|
|
|
|
branchCounters: new Map(),
|
|
|
|
|
mixerHalfTimer: 0,
|
|
|
|
|
congestion: new Map(),
|
|
|
|
|
lastExplodedComponent: new Map(),
|
|
|
|
|
contractOffer: null,
|
|
|
|
|
contractActive: null,
|
|
|
|
|
stats: newTurnStats(),
|
|
|
|
|
lastResult: null,
|
|
|
|
|
totals: newTotalStats()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getSpawnRange(farm) {
|
|
|
|
|
const ranges = [[1.95, 4.35], [1.55, 3.45], [1.25, 2.80], [1.05, 2.35]];
|
|
|
|
|
return ranges[(farm?.level || 1) - 1] || ranges[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function nextSpawnDelay(farm) {
|
|
|
|
|
const [min, max] = getSpawnRange(farm);
|
|
|
|
|
return randomBetween(min, max);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function facilityBodyForEntry(id, entry, side) {
|
|
|
|
|
const sizes = {
|
|
|
|
|
mixer: { w: 164, h: 126, name: 'Mixer', price: FACILITY_PRICES.mixer },
|
|
|
|
|
trash: { w: 180, h: 100, name: 'Waste Shredder', price: FACILITY_PRICES.trash },
|
|
|
|
|
truck: { w: 176, h: 138, name: 'Truck', price: FACILITY_PRICES.truck }
|
|
|
|
|
};
|
|
|
|
|
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 };
|
|
|
|
|
const spec = sizes[id];
|
|
|
|
|
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 18:06:35 +09:00
|
|
|
mixer: facilityBodyForEntry('mixer', { col: 0, row: 4 }, 'left'),
|
|
|
|
|
trash: facilityBodyForEntry('trash', { col: 10, row: 12 }, 'bottom'),
|
2026-06-07 15:05:24 +09:00
|
|
|
truck: facilityBodyForEntry('truck', { col: 21, row: 7 }, 'right')
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resetLayout(game) {
|
|
|
|
|
game.conveyorTiles.clear();
|
|
|
|
|
game.conveyorMeta.clear();
|
|
|
|
|
const initial = [
|
2026-06-07 18:06:35 +09:00
|
|
|
// Egg Farm -> S1 top input. Shorter starter approach lane.
|
|
|
|
|
[4, 1], [4, 2], [4, 3],
|
2026-06-07 15:05:24 +09:00
|
|
|
// S1 left output -> Mixer receiver on the left grid edge.
|
2026-06-07 18:06:35 +09:00
|
|
|
[3, 4], [2, 4], [1, 4], [0, 4],
|
|
|
|
|
// S1 right output -> S2 top input.
|
|
|
|
|
[5, 4], [6, 4], [7, 4], [8, 4], [9, 4],
|
|
|
|
|
[9, 5], [10, 5], [11, 5], [11, 6],
|
2026-06-07 15:05:24 +09:00
|
|
|
// S2 left output -> Waste Shredder receiver on bottom grid edge.
|
2026-06-07 18:06:35 +09:00
|
|
|
[10, 7], [10, 8], [10, 9], [10, 10], [10, 11], [10, 12],
|
2026-06-07 15:05:24 +09:00
|
|
|
// S2 right output -> Truck receiver on the right grid edge.
|
2026-06-07 18:06:35 +09:00
|
|
|
// Restored a longer lane so S2-to-truck transport is readable and less congested.
|
|
|
|
|
[12, 7], [13, 7], [14, 7], [15, 7], [16, 7], [17, 7], [18, 7], [19, 7], [20, 7], [21, 7]
|
2026-06-07 15:05:24 +09:00
|
|
|
]; for (const [col, row] of initial) {
|
|
|
|
|
const k = `${col},${row}`;
|
|
|
|
|
game.conveyorTiles.add(k);
|
|
|
|
|
game.conveyorMeta.set(k, { price: BUILD_COSTS.conveyor, builtSession: null });
|
|
|
|
|
}
|
|
|
|
|
game.facilities = defaultFacilities();
|
|
|
|
|
game.scanners = [
|
2026-06-07 18:06:35 +09:00
|
|
|
{ type: 'scanner', id: 1, kind: 'manual', slot: 0, role: 0, col: 4, row: 4, 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: 7, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard' }
|
2026-06-07 15:05:24 +09:00
|
|
|
];
|
|
|
|
|
const farm = { type: 'eggFarm', id: 1, col: 4, row: 0, level: 1, nextSpawn: 2, lastInterval: 2, spawnCounter: 0, price: BUILD_COSTS.eggFarm, builtSession: null };
|
|
|
|
|
farm.nextSpawn = nextSpawnDelay(farm);
|
|
|
|
|
farm.lastInterval = farm.nextSpawn;
|
|
|
|
|
game.eggFarms = [farm];
|
|
|
|
|
}
|