This commit is contained in:
33333-33333 2026-06-07 18:06:35 +09:00
commit b454ffbd40
21 changed files with 1349 additions and 712 deletions

View file

@ -1,15 +1,15 @@
import { VERSION, TURN_SECONDS, STARTING_CASH, POOP_RATE, CONVEYOR_SPEED, CONVEYOR_SPEED_GROWTH, AUTO_SCANNER_COOLDOWN, BUILD_COSTS, FACILITY_PRICES, GRID, THEME } from './core/config.js';
import { createGame, resetLayout, newTurnStats, newTotalStats, nextSpawnDelay, getSpawnRange } from './core/state.js';
import { createChick } from './core/entities.js';
import { key, parseKey, clamp, randomBetween, pointToCell, cellCenter, distance, yen } from './core/utils.js';
import { farmAt, scannerAt, scannerById, scannerBySlot, scannerCenter, scannerConnector, routeFromFarmToScanner, outputRoute, scannerOutputs, destinationLabel, destinationColor, nearestConveyorKey, buildConveyorComponents, factoryReady, facilityConnectionIssues } from './systems/routing.js';
import { MIXER_PRICE, TRUCK_PRICE, MIXER_HALF_SECONDS, maleTruckPenalty, truckPayoutMultiplier, positivePayout, applyCashDelta, spendCash, refundCash, settleTruckRevenue, finalScore } from './systems/economy.js';
import { snapshot, record, undo, redo } from './systems/history.js';
import { TURN_SECONDS, CONVEYOR_SPEED, CONVEYOR_SPEED_MAX, THEME } from './core/config.js';
import { createGame, resetLayout, newTurnStats } from './core/state.js';
import { clamp, pointToCell, cellCenter } from './core/utils.js';
import { facilityConnectionIssues } from './systems/routing.js';
import { applyCashDelta, collectFairiesFee, settleTruckRevenue } from './systems/economy.js';
import { undo, redo } from './systems/history.js';
import { drawAll } from './render/draw.js';
import { createBuildSystem } from './systems/buildSystem.js';
import { createUISystem } from './systems/uiSystem.js';
import { floating, shake, spawnPulse, scannerPulse, meatEffect, sludgeEffect, shredEffect, truckLoadEffect, shockwave, sparkBurst, smokeBurst, flyingDebris, eraseEffect, rageEffect, updateEffects } from './systems/effects.js';
import { rollContractOffer, activateAcceptedContract, clearActiveContract, productionMultiplier, resolveContract, truckTarget, isTargetTruckCargo, shouldFineMaleTruck } from './systems/contracts.js';
import { createChickSystem } from './systems/chickSystem.js';
import { floating, shockwave, sparkBurst, updateEffects } from './systems/effects.js';
import { rollContractOffer, activateAcceptedContract, clearActiveContract, resolveContract } from './systems/contracts.js';
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
@ -30,18 +30,17 @@ const ui = {
const game = createGame();
let build;
let uiSystem;
let chicks;
function conveyorSpeedForTurn(turn) {
return CONVEYOR_SPEED * Math.pow(CONVEYOR_SPEED_GROWTH, Math.max(0, turn - 1));
}
function currentConveyorSpeed() { return conveyorSpeedForTurn(game.turn); }
function currentConveyorSpeed() { return Math.min(CONVEYOR_SPEED_MAX, CONVEYOR_SPEED); }
function startGame() {
Object.assign(game, createGame());
game.phase = 'running';
game.view = { x: 0, y: 0 };
resetLayout(game);
uiSystem.hideModal();
updateCongestion();
chicks.updateCongestion();
uiSystem.updatePanels();
uiSystem.updateUI();
}
@ -58,17 +57,22 @@ function startNextTurn() {
game.phase = 'running';
game.turn += 1;
game.timeLeft = TURN_SECONDS;
game.shutdownTimeLeft = 0;
game.chicks = [];
game.effects = [];
game.floatingTexts = [];
game.shake = { time: 0, strength: 0 };
game.truckCargo = [];
game.view = { x: 0, y: 0 };
game.stats = newTurnStats();
for (const scanner of game.scanners) { scanner.queue = []; scanner.cooldown = 0; }
for (const farm of game.eggFarms) { farm.nextSpawn = currentSpawnDelay(farm); farm.lastInterval = farm.nextSpawn; farm.shutterProgress = 0; farm.shutterSparked = false; }
for (const farm of game.eggFarms) { farm.nextSpawn = chicks.currentSpawnDelay(farm); farm.lastInterval = farm.nextSpawn; farm.shutterProgress = 0; farm.shutterSparked = false; }
game.buildTool = null;
game.selected = null;
updateCongestion();
game.groupDrag = null;
game.selectionBox = null;
game.pan = null;
chicks.updateCongestion();
uiSystem.updatePanels();
uiSystem.updateUI();
}
@ -77,17 +81,21 @@ function completeTurn() {
if (game.phase !== 'running') return;
const ship = settleTruckRevenue(game);
const contract = resolveContract(game, applyCashDelta);
const fairiesFee = collectFairiesFee(game);
clearActiveContract(game);
game.totals.turnsCompleted += 1;
game.lastResult = { ...game.stats, ship, contract, fairiesFee, turn: game.turn, cash: game.cash };
if (game.cash < 0) { game.phase = 'gameover'; uiSystem.showGameOver(); return; }
game.lastResult = { ...game.stats, ship, contract, turn: game.turn, cash: game.cash };
game.phase = 'build';
game.truckCargo = [];
game.chicks = [];
game.shutdownTimeLeft = 0;
game.buildSession += 1;
game.undoStack = [];
game.redoStack = [];
game.timeLeft = 0;
game.totals.turnsCompleted += 1;
for (const scanner of game.scanners) scanner.queue = [];
for (const farm of game.eggFarms) { farm.shutterProgress = 0; farm.shutterSparked = false; }
game.contractOffer = rollContractOffer(game);
game.buildTool = null;
game.groupDrag = null;
@ -98,273 +106,30 @@ function completeTurn() {
}
function closeFarmShutters() {
if (game.shutdownTimeLeft <= 0) game.shutdownTimeLeft = game.shutdownGraceSeconds || 10;
for (const farm of game.eggFarms) {
if (farm.shutterSparked) continue;
farm.shutterProgress = 0.02;
farm.shutterSparked = true;
const c = cellCenter(farm.col, farm.row);
shockwave(game, c.x, c.y, THEME.ink);
sparkBurst(game, c.x, c.y + 10, 10);
floating(game, c.x, c.y - 34, 'SHUT', THEME.ink);
floating(game, c.x, c.y - 34, 'SHUT +10s', THEME.ink);
}
}
function spawnChick(farm) {
const data = routeFromFarmToScanner(game, farm, true);
if (!data) { floating(game, cellCenter(farm.col, farm.row).x, cellCenter(farm.col, farm.row).y - 18, 'NO ROUTE', THEME.danger); return false; }
const chick = createChick(game, data);
if (chick.sex === 'poop') game.stats.poopSpawned += 1;
const start = data.route[0];
if (isSpawnBlocked(start)) { explodeRoute(data.route, [{ sex: chick.sex, x: start.x, y: start.y }], 'LINE JAM!'); return false; }
game.chicks.push(chick);
spawnPulse(game, cellCenter(farm.col, farm.row).x, cellCenter(farm.col, farm.row).y);
return true;
}
function isSpawnBlocked(start) { return game.chicks.some(ch => ch.stage !== 'flying' && Math.hypot(ch.x - start.x, ch.y - start.y) < GRID.cell * 0.68); }
function currentSpawnDelay(farm) { return nextSpawnDelay(farm) / productionMultiplier(game); }
function update(timestamp) {
if (!game.lastTimestamp) game.lastTimestamp = timestamp;
const dt = Math.min((timestamp - game.lastTimestamp) / 1000, 0.05);
game.lastTimestamp = timestamp;
if (game.phase === 'running') updateRunning(dt);
if (game.phase === 'running') chicks.updateRunning(dt, { closeFarmShutters, completeTurn });
updateEffects(game, dt, canvas, build.equipmentHitBoxes);
updateCongestion();
drawAll(ctx, canvas, game, { activeQueuedChick, selectedObject: build.selectedObject, selectedTitle: build.selectedTitle });
chicks.updateCongestion();
drawAll(ctx, canvas, game, { activeQueuedChick: chicks.activeQueuedChick, selectedObject: build.selectedObject, selectedTitle: build.selectedTitle });
uiSystem.updateUI();
requestAnimationFrame(update);
}
function updateRunning(dt) {
const producing = game.timeLeft > 0;
game.timeLeft = Math.max(0, game.timeLeft - dt);
if (producing && game.timeLeft <= 0) closeFarmShutters();
if (game.timeLeft <= 0) {
for (const farm of game.eggFarms) farm.shutterProgress = Math.min(1, (farm.shutterProgress || 0) + dt * 2.8);
}
if (game.mixerHalfTimer > 0) game.mixerHalfTimer = Math.max(0, game.mixerHalfTimer - dt);
if (producing && game.timeLeft > 0) {
for (const farm of game.eggFarms) {
farm.nextSpawn -= dt;
if (farm.nextSpawn <= 0) { spawnChick(farm); farm.nextSpawn = currentSpawnDelay(farm); farm.lastInterval = farm.nextSpawn; }
}
}
for (let i = game.chicks.length - 1; i >= 0; i -= 1) {
const chick = game.chicks[i];
chick.bob += dt * 7;
if (chick.stage === 'queued') continue;
updateRoutedChick(chick, i, dt);
}
updateScannerQueues(dt);
updateCongestion();
checkCongestionExplosions(dt);
if (game.timeLeft <= 0 && game.chicks.length === 0) completeTurn();
uiSystem.checkGameOver();
}
function updateRoutedChick(chick, index, dt) {
if (blockedByFrontChick(chick)) return;
moveAlongRoute(chick, dt);
if (chick.route && chick.targetIndex < chick.route.length) return;
if (chick.stage === 'input' || chick.stage === 'toScanner') {
const scanner = scannerById(game, chick.stage === 'input' ? chick.scannerId : chick.nextScannerId);
if (!scanner) { removeChick(index, 'NO SCANNER'); return; }
enqueueChick(chick, scanner, chick.route);
return;
}
if (chick.stage === 'toMixer') resolveMixer(index);
else if (chick.stage === 'toTruck') resolveTruck(index);
else if (chick.stage === 'toTrash') resolveTrash(index);
}
function blockedByFrontChick(chick) {
if (!chick.route || chick.targetIndex >= chick.route.length) return false;
const next = chick.route[chick.targetIndex];
for (const other of game.chicks) {
if (other.id === chick.id || other.stage === 'flying') continue;
if (Math.hypot(other.x - next.x, other.y - next.y) < GRID.cell * 0.55 && routeProgress(other) >= routeProgress(chick)) return true;
}
return false;
}
function routeProgress(chick) { return chick.targetIndex || 0; }
function moveAlongRoute(chick, dt) {
let remaining = currentConveyorSpeed() * dt;
while (remaining > 0 && chick.targetIndex < chick.route.length) {
const target = chick.route[chick.targetIndex];
const dx = target.x - chick.x, dy = target.y - chick.y;
const dist = Math.hypot(dx, dy);
if (dist <= 0.001) { chick.targetIndex += 1; continue; }
if (remaining >= dist) { chick.x = target.x; chick.y = target.y; chick.targetIndex += 1; remaining -= dist; }
else { chick.x += dx / dist * remaining; chick.y += dy / dist * remaining; remaining = 0; }
}
}
function enqueueChick(chick, scanner, routeOverride) {
chick.stage = 'queued';
chick.route = null;
chick.queueRoute = routeOverride ? routeOverride.map(p => ({ x: p.x, y: p.y })) : [scannerCenter(scanner)];
chick.scannerId = scanner.id;
if (!scanner.queue.includes(chick.id)) scanner.queue.push(chick.id);
positionScannerQueue(scanner);
}
function positionScannerQueue(scanner) {
scanner.queue = scanner.queue.filter(id => game.chicks.some(ch => ch.id === id && ch.stage === 'queued'));
const spacing = GRID.cell * 0.86;
scanner.queue.forEach((id, idx) => {
const chick = game.chicks.find(ch => ch.id === id);
if (!chick) return;
setPositionFromRouteEnd(chick, chick.queueRoute || [scannerCenter(scanner)], idx * spacing);
chick.queueIndex = idx;
});
}
function setPositionFromRouteEnd(chick, route, distanceBack) {
if (!route || !route.length) return;
if (route.length === 1 || distanceBack <= 0) { const end = route[route.length - 1]; chick.x = end.x; chick.y = end.y; return; }
let remain = distanceBack;
for (let i = route.length - 1; i > 0; i -= 1) {
const a = route[i - 1], b = route[i];
const len = Math.hypot(b.x - a.x, b.y - a.y);
if (remain <= len) { const t = 1 - remain / len; chick.x = a.x + (b.x - a.x) * t; chick.y = a.y + (b.y - a.y) * t; return; }
remain -= len;
}
const start = route[0]; chick.x = start.x; chick.y = start.y;
}
function updateScannerQueues(dt) {
for (const scanner of game.scanners) {
if (scanner.cooldown > 0) scanner.cooldown = Math.max(0, scanner.cooldown - dt);
positionScannerQueue(scanner);
if (scanner.kind !== 'auto' || scanner.cooldown > 0 || !scanner.queue.length) continue;
const id = scanner.queue[0];
const index = game.chicks.findIndex(c => c.id === id);
if (index < 0) { scanner.queue.shift(); continue; }
if (sortChickByIndex(index, autoSideFor(scanner, game.chicks[index]), true)) { scanner.cooldown = AUTO_SCANNER_COOLDOWN; game.stats.autoSorted += 1; game.totals.autoSorted += 1; }
}
}
function autoSideFor(scanner, chick) {
if (scanner.role === 0) return chick.sex === 'male' ? 'left' : 'right';
return chick.sex === 'poop' ? 'left' : 'right';
}
function activeQueuedChick(slot) {
const scanner = scannerBySlot(game, slot);
if (!scanner || !scanner.queue.length) return null;
return game.chicks.find(c => c.id === scanner.queue[0]) || null;
}
function sortSlot(slot, side) {
if (game.phase !== 'running') return;
const scanner = scannerBySlot(game, slot);
if (!scanner || !scanner.queue.length) return;
const index = game.chicks.findIndex(c => c.id === scanner.queue[0]);
if (index >= 0) sortChickByIndex(index, side, false);
}
function sortChickByIndex(index, side, auto) {
const chick = game.chicks[index];
if (!chick || chick.stage !== 'queued') return false;
const scanner = scannerById(game, chick.scannerId);
if (!scanner) return false;
const plan = outputRoute(game, side, { x: chick.x, y: chick.y }, scanner.id, true);
if (!plan) { floating(game, chick.x, chick.y - 16, side === 'left' ? 'NO LEFT BELT' : 'NO RIGHT BELT', THEME.danger); return false; }
scanner.queue = scanner.queue.filter(id => id !== chick.id);
chick.stage = plan.destination === 'scanner' ? 'toScanner' : `to${plan.destination[0].toUpperCase()}${plan.destination.slice(1)}`;
chick.route = plan.route; chick.targetIndex = 1; chick.nextScannerId = plan.nextScannerId || null; chick.queueRoute = null; chick.queueIndex = -1;
scannerPulse(game, scannerCenter(scanner).x, scannerCenter(scanner).y, auto ? THEME.green : destinationColor(plan.destination));
floating(game, chick.x, chick.y - 20, auto ? 'AUTO' : destinationLabel(plan.destination), auto ? THEME.green : destinationColor(plan.destination));
return true;
}
function resolveMixer(index) {
const chick = game.chicks[index]; if (!chick) return;
const { x, y } = chick; game.chicks.splice(index, 1); game.stats.mixerCount += 1; game.totals.processed += 1;
if (chick.sex === 'poop') { game.mixerHalfTimer = MIXER_HALF_SECONDS; game.stats.poopMixer += 1; game.totals.poopMixer += 1; game.stats.mistakes += 1; game.totals.mistakes += 1; sludgeEffect(game, x, y); floating(game, x, y - 18, `HALF PAY ${MIXER_HALF_SECONDS}s`, THEME.ink); return; }
const amount = positivePayout(game, MIXER_PRICE); applyCashDelta(game, amount); if (chick.sex === 'male') { game.stats.correct += 1; game.totals.correct += 1; } else { game.stats.mistakes += 1; game.totals.mistakes += 1; } meatEffect(game, x, y); floating(game, x, y - 18, `+${yen(amount)}`, THEME.green); uiSystem.checkGameOver();
}
function resolveTruck(index) {
const chick = game.chicks[index]; if (!chick) return;
const { x, y } = chick; game.chicks.splice(index, 1); addTruckCargo(chick.sex); truckLoadEffect(game, x, y, chick.sex); game.totals.processed += 1;
const target = truckTarget(game);
const isTarget = isTargetTruckCargo(game, chick.sex);
if (chick.sex === 'poop') {
game.stats.poopTruck += 1; game.totals.poopTruck += 1;
if (isTarget) { game.stats.correct += 1; game.totals.correct += 1; floating(game, x, y - 18, `+${yen(positivePayout(game, TRUCK_PRICE))}`, THEME.green); }
else { game.stats.mistakes += 1; game.totals.mistakes += 1; floating(game, x, y - 18, `SOIL ${Math.round(truckPayoutMultiplier(game) * 100)}%`, THEME.ink); }
return;
}
if (chick.sex === 'female') {
game.stats.truckFemale += 1;
if (isTarget) { const displayAmount = positivePayout(game, Math.floor(TRUCK_PRICE * truckPayoutMultiplier(game))); game.stats.correct += 1; game.totals.correct += 1; floating(game, x, y - 18, `+${yen(displayAmount)}`, THEME.green); }
else { game.stats.mistakes += 1; game.totals.mistakes += 1; floating(game, x, y - 18, target === 'male' ? 'REJECT' : 'NO SALE', THEME.warn); }
return;
}
game.stats.truckMale += 1;
if (isTarget) { game.stats.correct += 1; game.totals.correct += 1; floating(game, x, y - 18, `+${yen(positivePayout(game, TRUCK_PRICE))}`, THEME.green); return; }
if (shouldFineMaleTruck(game)) { const penalty = maleTruckPenalty(game); applyCashDelta(game, -penalty); game.stats.mistakes += 1; game.totals.mistakes += 1; floating(game, x, y - 18, `-${yen(penalty)}`, THEME.danger); uiSystem.checkGameOver(); }
else { game.stats.mistakes += 1; game.totals.mistakes += 1; floating(game, x, y - 18, 'REJECT', THEME.warn); }
}
function resolveTrash(index) {
const chick = game.chicks[index]; if (!chick) return;
const { x, y } = chick; game.chicks.splice(index, 1); shredEffect(game, x, y, chick.sex); game.stats.trashCount += 1; game.totals.processed += 1;
if (chick.sex === 'poop') { game.stats.poopTrash += 1; game.totals.poopTrash += 1; game.stats.correct += 1; game.totals.correct += 1; floating(game, x, y - 18, 'CLEAN', THEME.green); }
else { game.stats.mistakes += 1; game.totals.mistakes += 1; floating(game, x, y - 18, 'WASTE', THEME.ink); }
}
function removeChick(index, label) { const chick = game.chicks[index]; if (!chick) return; game.chicks.splice(index, 1); floating(game, chick.x, chick.y - 12, label, THEME.muted); }
function addTruckCargo(sex) { const t = game.facilities.truck; if (!t) return; game.truckCargo.push({ sex, x: randomBetween(22, t.w - 22), y: randomBetween(66, t.h - 24) }); if (game.truckCargo.length > 45) game.truckCargo.shift(); }
function conveyorKeyAtChick(chick) {
const cell = pointToCell(chick.x, chick.y);
if (cell) {
const k = key(cell.col, cell.row);
if (game.conveyorTiles.has(k)) return k;
}
// Fallback only when the chick is visually still on a conveyor center.
const nearest = nearestConveyorKey(game, chick.x, chick.y);
if (!nearest) return null;
const p = parseKey(nearest);
const c = cellCenter(p.col, p.row);
return Math.hypot(chick.x - c.x, chick.y - c.y) <= GRID.cell * 0.55 ? nearest : null;
}
function updateCongestion() {
const { components, cellToComponent } = buildConveyorComponents(game);
for (const chick of game.chicks) {
if (chick.stage === 'flying') continue;
const k = conveyorKeyAtChick(chick);
if (!k) continue;
const id = cellToComponent.get(k);
const comp = components.get(id);
if (comp) comp.count += 1;
}
for (const comp of components.values()) comp.ratio = comp.count / comp.capacity;
game.congestion = components;
game.componentLookup = cellToComponent;
}
function checkCongestionExplosions(dt) {
for (const comp of game.congestion.values()) {
if (comp.ratio > 0.8 && comp.ratio < 1) {
for (const chick of chicksInComponent(comp.id).slice(0, 3)) if (Math.random() < 0.06) rageEffect(game, chick.x, chick.y - chick.radius - 8);
}
if (comp.ratio >= 1) {
const last = game.lastExplodedComponent.get(comp.id) || 0;
const now = performance.now();
if (now - last > 900) { game.lastExplodedComponent.set(comp.id, now); explodeComponent(comp); }
}
}
}
function chicksInComponent(id) {
return game.chicks.filter(ch => { const k = conveyorKeyAtChick(ch); return k && game.componentLookup?.get(k) === id; });
}
function explodeComponent(comp) {
const victims = chicksInComponent(comp.id);
if (!victims.length) return;
for (const v of victims) { flyingDebris(game, v.sex, v.x, v.y); }
game.chicks = game.chicks.filter(ch => !victims.some(v => v.id === ch.id));
for (const scanner of game.scanners) scanner.queue = scanner.queue.filter(id => game.chicks.some(c => c.id === id));
for (const k of comp.cells) { const p = parseKey(k); const c = cellCenter(p.col, p.row); shockwave(game, c.x, c.y, THEME.danger); sparkBurst(game, c.x, c.y, 6); smokeBurst(game, c.x, c.y, 4); }
shake(game, 18, .75); const first = parseKey(comp.cells[0]); const fc = cellCenter(first.col, first.row); floating(game, fc.x, fc.y - 22, '100% JAM EXPLOSION', THEME.danger);
}
function explodeRoute(route, extras, label) {
const victims = game.chicks.filter(ch => route.some(p => Math.hypot(ch.x - p.x, ch.y - p.y) < GRID.cell));
game.chicks = game.chicks.filter(ch => !victims.some(v => v.id === ch.id));
for (const v of victims) flyingDebris(game, v.sex, v.x, v.y);
for (const e of extras) flyingDebris(game, e.sex, e.x, e.y);
for (const p of route) { shockwave(game, p.x, p.y, THEME.danger); sparkBurst(game, p.x, p.y, 4); smokeBurst(game, p.x, p.y, 3); }
shake(game, 18, .75); if (route[0]) floating(game, route[0].x, route[0].y - 18, label, THEME.danger);
}
// Build/edit/selection operations live in src/systems/buildSystem.js.
// HUD, panels, modal screens, and gameover rendering live in src/systems/uiSystem.js.
// Hitbox generation lives in buildSystem.js.
@ -380,7 +145,7 @@ function clickSelect(event) {
game.selected = sel;
game.multiSelected = [sel];
uiSystem.updatePanels();
if (hit.type === 'scanner' && hit.ref.kind === 'auto') build.showAutoScannerMenu(hit.ref);
build.showSelectedMenu();
}
canvas.addEventListener('contextmenu', event => event.preventDefault());
@ -391,7 +156,12 @@ canvas.addEventListener('pointerdown', event => {
if (event.button === 2) { startPan(event); return; }
if (event.button !== 0) return;
if (game.buildTool === 'erase') { build.eraseAtPoint(world); return; }
if (game.buildTool) { build.buildAtPoint(world); return; }
if (game.buildTool) {
const hit = build.equipmentAtPoint(world);
if (hit) { game.buildTool = null; clickSelect(event); return; }
build.buildAtPoint(world);
return;
}
if (build.startGroupDrag(event)) return;
build.startSelectionBox(event);
});
@ -414,11 +184,12 @@ canvas.addEventListener('pointerup', event => {
});
canvas.addEventListener('pointercancel', () => { game.groupDrag = null; game.selectionBox = null; game.pan = null; });
ui.buttons.s1Left.addEventListener('click', () => sortSlot(0, 'left')); ui.buttons.s1Right.addEventListener('click', () => sortSlot(0, 'right')); ui.buttons.s2Left.addEventListener('click', () => sortSlot(1, 'left')); ui.buttons.s2Right.addEventListener('click', () => sortSlot(1, 'right'));
ui.buttons.s1Left.addEventListener('click', () => chicks.sortSlot(0, 'left')); ui.buttons.s1Right.addEventListener('click', () => chicks.sortSlot(0, 'right')); ui.buttons.s2Left.addEventListener('click', () => chicks.sortSlot(1, 'left')); ui.buttons.s2Right.addEventListener('click', () => chicks.sortSlot(1, 'right'));
ui.buttons.nextTurn.addEventListener('click', startNextTurn); ui.buttons.conveyor.addEventListener('click', () => build.setBuildTool('conveyor')); ui.buttons.eggFarm.addEventListener('click', () => build.setBuildTool('eggFarm')); ui.buttons.autoScanner.addEventListener('click', () => build.setBuildTool('autoScanner')); ui.buttons.manualScanner.addEventListener('click', () => build.setBuildTool('manualScanner')); ui.buttons.mixer.addEventListener('click', () => build.setBuildTool('mixer')); ui.buttons.trash.addEventListener('click', () => build.setBuildTool('trash')); ui.buttons.truck.addEventListener('click', () => build.setBuildTool('truck')); ui.buttons.erase.addEventListener('click', () => build.setBuildTool('erase'));
ui.buttons.undo.addEventListener('click', () => { if (undo(game)) uiSystem.updatePanels(); }); ui.buttons.redo.addEventListener('click', () => { if (redo(game)) uiSystem.updatePanels(); });
window.addEventListener('keydown', event => { if (event.repeat) return; const name = event.key.toLowerCase(); if (name === 'a') { event.preventDefault(); sortSlot(0, 'left'); } if (name === 'd') { event.preventDefault(); sortSlot(0, 'right'); } if (event.key === 'ArrowLeft') { event.preventDefault(); sortSlot(1, 'left'); } if (event.key === 'ArrowRight') { event.preventDefault(); sortSlot(1, 'right'); } if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && name === 'z') { event.preventDefault(); if (undo(game)) uiSystem.updatePanels(); } if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && (name === 'y' || (event.shiftKey && name === 'z'))) { event.preventDefault(); if (redo(game)) uiSystem.updatePanels(); } if (name === 'f1') { event.preventDefault(); game.debug = !game.debug; } });
window.addEventListener('keydown', event => { if (event.repeat) return; const name = event.key.toLowerCase(); if (name === 'a') { event.preventDefault(); chicks.sortSlot(0, 'left'); } if (name === 'd') { event.preventDefault(); chicks.sortSlot(0, 'right'); } if (event.key === 'ArrowLeft') { event.preventDefault(); chicks.sortSlot(1, 'left'); } if (event.key === 'ArrowRight') { event.preventDefault(); chicks.sortSlot(1, 'right'); } if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && name === 'z') { event.preventDefault(); if (undo(game)) uiSystem.updatePanels(); } if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && (name === 'y' || (event.shiftKey && name === 'z'))) { event.preventDefault(); if (redo(game)) uiSystem.updatePanels(); } if (name === 'f1') { event.preventDefault(); game.debug = !game.debug; } });
build = createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanels: () => uiSystem?.updatePanels() });
uiSystem = createUISystem({ game, ui, build, startGame, activeQueuedChick });
resetLayout(game); updateCongestion(); uiSystem.updatePanels(); uiSystem.updateUI(); uiSystem.showTitle(); requestAnimationFrame(update);
chicks = createChickSystem({ game, currentConveyorSpeed, onGameOverCheck: () => uiSystem?.checkGameOver() });
uiSystem = createUISystem({ game, ui, build, startGame, activeQueuedChick: chicks.activeQueuedChick });
resetLayout(game); chicks.updateCongestion(); uiSystem.updatePanels(); uiSystem.updateUI(); uiSystem.showTitle(); requestAnimationFrame(update);