195 lines
12 KiB
JavaScript
195 lines
12 KiB
JavaScript
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 { 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');
|
|
|
|
const ui = {
|
|
shell: document.getElementById('gameShell'),
|
|
money: document.getElementById('money'), turn: document.getElementById('turn'), timeLeft: document.getElementById('timeLeft'), phase: document.getElementById('phaseLabel'),
|
|
eventBrief: document.getElementById('eventBrief'), targetBrief: document.getElementById('targetBrief'), speedBrief: document.getElementById('speedBrief'),
|
|
turnProfit: document.getElementById('turnProfit'), mixerCount: document.getElementById('mixerCount'), truckFemaleCount: document.getElementById('truckFemaleCount'), truckMaleCount: document.getElementById('truckMaleCount'), poopCount: document.getElementById('poopCount'),
|
|
buildStatus: document.getElementById('buildStatus'), turnSummary: document.getElementById('turnSummary'), contractPanel: document.getElementById('contractPanel'), facilityPanel: document.getElementById('facilityPanel'),
|
|
modal: document.getElementById('modal'), modalTitle: document.getElementById('modalTitle'), modalBody: document.getElementById('modalBody'), modalActions: document.getElementById('modalActions'),
|
|
buttons: {
|
|
s1Left: document.getElementById('scanner1MixerButton'), s1Right: document.getElementById('scanner1TruckButton'), s2Left: document.getElementById('scanner2MixerButton'), s2Right: document.getElementById('scanner2TruckButton'),
|
|
nextTurn: document.getElementById('nextTurnButton'), conveyor: document.getElementById('buildConveyorButton'), eggFarm: document.getElementById('buildEggFarmButton'), autoScanner: document.getElementById('buildAutoScannerButton'), manualScanner: document.getElementById('buildManualScannerButton'), mixer: document.getElementById('buildMixerButton'), trash: document.getElementById('buildTrashButton'), truck: document.getElementById('buildTruckButton'), erase: document.getElementById('eraseButton'), undo: document.getElementById('undoButton'), redo: document.getElementById('redoButton')
|
|
}
|
|
};
|
|
|
|
const game = createGame();
|
|
let build;
|
|
let uiSystem;
|
|
let chicks;
|
|
|
|
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();
|
|
chicks.updateCongestion();
|
|
uiSystem.updatePanels();
|
|
uiSystem.updateUI();
|
|
}
|
|
|
|
function startNextTurn() {
|
|
if (game.phase !== 'build') return;
|
|
const issues = facilityConnectionIssues(game);
|
|
if (issues.length) {
|
|
build.fail(`Cannot start: ${issues[0]}`);
|
|
uiSystem.updatePanels();
|
|
return;
|
|
}
|
|
activateAcceptedContract(game);
|
|
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 = chicks.currentSpawnDelay(farm); farm.lastInterval = farm.nextSpawn; farm.shutterProgress = 0; farm.shutterSparked = false; }
|
|
game.buildTool = null;
|
|
game.selected = null;
|
|
game.groupDrag = null;
|
|
game.selectionBox = null;
|
|
game.pan = null;
|
|
chicks.updateCongestion();
|
|
uiSystem.updatePanels();
|
|
uiSystem.updateUI();
|
|
}
|
|
|
|
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.phase = 'build';
|
|
game.truckCargo = [];
|
|
game.chicks = [];
|
|
game.shutdownTimeLeft = 0;
|
|
game.buildSession += 1;
|
|
game.undoStack = [];
|
|
game.redoStack = [];
|
|
game.timeLeft = 0;
|
|
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;
|
|
game.selectionBox = null;
|
|
game.pan = null;
|
|
uiSystem.updatePanels();
|
|
uiSystem.showTurnResult(game.lastResult);
|
|
}
|
|
|
|
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 +10s', THEME.ink);
|
|
}
|
|
}
|
|
|
|
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') chicks.updateRunning(dt, { closeFarmShutters, completeTurn });
|
|
updateEffects(game, dt, canvas, build.equipmentHitBoxes);
|
|
chicks.updateCongestion();
|
|
drawAll(ctx, canvas, game, { activeQueuedChick: chicks.activeQueuedChick, selectedObject: build.selectedObject, selectedTitle: build.selectedTitle });
|
|
uiSystem.updateUI();
|
|
requestAnimationFrame(update);
|
|
}
|
|
|
|
// 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.
|
|
function rawCanvasPoint(event) { const r = canvas.getBoundingClientRect(); return { x: (event.clientX - r.left) * canvas.width / r.width, y: (event.clientY - r.top) * canvas.height / r.height }; }
|
|
function canvasPoint(event) { const p = rawCanvasPoint(event); return { x: p.x - game.view.x, y: p.y - game.view.y }; }
|
|
function startPan(event) { const p = rawCanvasPoint(event); game.pan = { start: p, viewX: game.view.x, viewY: game.view.y }; }
|
|
function updatePan(event) { if (!game.pan) return; const p = rawCanvasPoint(event); game.view.x = clamp(game.pan.viewX + p.x - game.pan.start.x, -620, 420); game.view.y = clamp(game.pan.viewY + p.y - game.pan.start.y, -340, 240); }
|
|
function selectionForHit(hit) { if (!hit) return null; if (hit.type === 'conveyor') return { type: 'conveyor', id: hit.oldKey }; return { type: hit.type, id: hit.ref.id }; }
|
|
function clickSelect(event) {
|
|
const p = canvasPoint(event), hit = build.equipmentAtPoint(p);
|
|
const sel = selectionForHit(hit);
|
|
if (!sel) { game.selected = null; game.multiSelected = []; uiSystem.updatePanels(); return; }
|
|
game.selected = sel;
|
|
game.multiSelected = [sel];
|
|
uiSystem.updatePanels();
|
|
build.showSelectedMenu();
|
|
}
|
|
|
|
canvas.addEventListener('contextmenu', event => event.preventDefault());
|
|
canvas.addEventListener('pointerdown', event => {
|
|
if (game.phase !== 'build') return;
|
|
canvas.setPointerCapture(event.pointerId);
|
|
const world = canvasPoint(event);
|
|
if (event.button === 2) { startPan(event); return; }
|
|
if (event.button !== 0) return;
|
|
if (game.buildTool === 'erase') { build.eraseAtPoint(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);
|
|
});
|
|
canvas.addEventListener('pointermove', event => {
|
|
if (game.phase !== 'build') return;
|
|
if (game.pan && (event.buttons & 2)) updatePan(event);
|
|
if (game.groupDrag && (event.buttons & 1)) build.updateGroupDrag(event);
|
|
if (game.selectionBox && (event.buttons & 1)) build.updateSelectionBox(event);
|
|
if (game.buildTool === 'conveyor' && (event.buttons & 1)) build.buildAtCell(pointToCell(canvasPoint(event).x, canvasPoint(event).y));
|
|
if (game.buildTool === 'erase' && (event.buttons & 1)) build.eraseAtPoint(canvasPoint(event));
|
|
});
|
|
canvas.addEventListener('pointerup', event => {
|
|
if (game.selectionBox) build.finishSelectionBox();
|
|
else if (game.groupDrag) {
|
|
if (!game.groupDrag.committed) clickSelect(event);
|
|
build.finishGroupDrag();
|
|
}
|
|
game.groupDrag = null; game.pan = null;
|
|
try { canvas.releasePointerCapture(event.pointerId); } catch (_) { /* noop */ }
|
|
});
|
|
canvas.addEventListener('pointercancel', () => { game.groupDrag = null; game.selectionBox = null; game.pan = null; });
|
|
|
|
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(); 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() });
|
|
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);
|