zunda_shiwake/src/game.js
2026-06-07 18:06:35 +09:00

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);