This commit is contained in:
33333-33333 2026-06-09 15:19:38 +09:00
commit 82a6b2c787
13 changed files with 215 additions and 103 deletions

46
dist/game.js vendored

File diff suppressed because one or more lines are too long

View file

@ -43,7 +43,7 @@ export const BALANCE = {
fairiesTribute: { fairiesTribute: {
perDay: 10 perDay: 10
}, },
incomeUpgradeRate: 1.05, incomeUpgradeRate: 1.10,
explosionDamageDivisor: 30, explosionDamageDivisor: 30,
maleTruckFinePerHalfDay: 30, maleTruckFinePerHalfDay: 30,
shredderBonus: { shredderBonus: {
@ -205,6 +205,14 @@ export const BALANCE = {
description: 'Turns shredder waste into suspicious fertilizer. Gain JPY 3 per copy when SHREDDER processes poop.', description: 'Turns shredder waste into suspicious fertilizer. Gain JPY 3 per copy when SHREDDER processes poop.',
tags: ['POOP', 'ECONOMY', 'PASSIVE'] tags: ['POOP', 'ECONOMY', 'PASSIVE']
}, },
{
id: 'scannerQueueSpacing',
title: 'Scanner Queue Spacing',
rarity: 'common',
type: 'instant',
description: 'Manual and Auto Scanner queue spacing -15%. No card limit.',
tags: ['SCANNER', 'PASSIVE']
},
{ {
id: 'extraEggOutlet', id: 'extraEggOutlet',
title: 'Extra Egg Outlet', title: 'Extra Egg Outlet',
@ -321,6 +329,7 @@ export const BALANCE = {
dudRefund: 0, dudRefund: 0,
freeReroll: 0, freeReroll: 0,
composter: 0, composter: 0,
scannerQueueSpacing: 0,
laborExploitation: 0, laborExploitation: 0,
usedMachineActive: false, usedMachineActive: false,
rescueLoanCharges: 0, rescueLoanCharges: 0,
@ -369,7 +378,7 @@ export const BALANCE = {
buildable: true, upgradeable: false buildable: true, upgradeable: false
}, },
boostConveyor: { boostConveyor: {
id: 'boostConveyor', type: 'conveyor', name: 'Boost Conveyor', shortName: 'BOOST BELT', price: 50, id: 'boostConveyor', type: 'conveyor', name: 'Boost Conveyor', shortName: 'BOOST BELT', price: 75,
buildable: true, upgradeable: false, speedMultiplier: 2 buildable: true, upgradeable: false, speedMultiplier: 2
}, },
eggFarm: { eggFarm: {

View file

@ -141,7 +141,7 @@ export function createGame() {
lastExplodedComponent: new Map(), lastExplodedComponent: new Map(),
contractOffer: null, contractOffer: null,
contractActive: 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, sparePartsBin: 0, dudRefund: 0, freeReroll: 0, composter: 0, laborExploitation: 0, usedMachineActive: false, rescueLoanCharges: 0, loans: [] }, 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, sparePartsBin: 0, dudRefund: 0, freeReroll: 0, composter: 0, scannerQueueSpacing: 0, laborExploitation: 0, usedMachineActive: false, rescueLoanCharges: 0, loans: [] },
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 }, cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
cardTargetPick: null, cardTargetPick: null,
manualCombo: { count: 0, lastBonus: 0 }, manualCombo: { count: 0, lastBonus: 0 },

View file

@ -3,7 +3,7 @@ import { buildToolButtonHtml } from './core/text.js';
import { createGame, resetLayout, newTurnStats } from './core/state.js'; import { createGame, resetLayout, newTurnStats } from './core/state.js';
import { clamp, pointToCell, cellCenter, key, yen } from './core/utils.js'; import { clamp, pointToCell, cellCenter, key, yen } from './core/utils.js';
import { expansionLots, lotBounds } from './core/gridExpansion.js'; import { expansionLots, lotBounds } from './core/gridExpansion.js';
import { commitFactoryGraphForDay, facilityConnectionIssues } from './systems/routing.js'; import { commitFactoryGraphForDay, facilityConnectionIssues, scannerConnector } from './systems/routing.js';
import { applyRevenue, collectChemicalWeaponSubsidy, collectFairiesTribute, collectLoanRepayments, collectZundaTax, settleTruckRevenue } from './systems/economy.js'; import { applyRevenue, collectChemicalWeaponSubsidy, collectFairiesTribute, collectLoanRepayments, collectZundaTax, settleTruckRevenue } from './systems/economy.js';
import { undo, redo } from './systems/history.js'; import { undo, redo } from './systems/history.js';
import { drawAll } from './render/draw.js'; import { drawAll } from './render/draw.js';
@ -385,10 +385,11 @@ function resetCameraToFactoryStart() {
game.view.y = 190 - anchor.y; game.view.y = 190 - anchor.y;
clampCamera(); clampCamera();
} }
function startPan(event) { const p = rawCanvasPoint(event); ensureView(); game.pan = { start: p, viewX: game.view.x, viewY: game.view.y }; } function startPan(event) { const p = rawCanvasPoint(event); ensureView(); game.pan = { start: p, viewX: game.view.x, viewY: game.view.y, moved: false }; }
function updatePan(event) { function updatePan(event) {
if (!game.pan) return; if (!game.pan) return;
const p = rawCanvasPoint(event); const p = rawCanvasPoint(event);
if (Math.hypot(p.x - game.pan.start.x, p.y - game.pan.start.y) > 5) game.pan.moved = true;
ensureView(); ensureView();
game.view.x = game.pan.viewX + p.x - game.pan.start.x; game.view.x = game.pan.viewX + p.x - game.pan.start.x;
game.view.y = game.pan.viewY + p.y - game.pan.start.y; game.view.y = game.pan.viewY + p.y - game.pan.start.y;
@ -487,6 +488,8 @@ function chickDisplay(chick) {
function miniScannerGrid(scanner, chick) { function miniScannerGrid(scanner, chick) {
const farmCells = new Set(game.eggFarms.map(farm => key(farm.col, farm.row))); const farmCells = new Set(game.eggFarms.map(farm => key(farm.col, farm.row)));
const scannerCells = new Map(game.scanners.map(s => [key(s.col, s.row), s])); const scannerCells = new Map(game.scanners.map(s => [key(s.col, s.row), s]));
const inputCell = scannerConnector(scanner, 'inputA');
const inputKey = inputCell ? key(inputCell.col, inputCell.row) : null;
const facilityEntryCells = new Set(Object.values(game.facilities || {}).map(f => f.entry ? key(f.entry.col, f.entry.row) : null).filter(Boolean)); const facilityEntryCells = new Set(Object.values(game.facilities || {}).map(f => f.entry ? key(f.entry.col, f.entry.row) : null).filter(Boolean));
const activeSex = chick?.sex || ''; const activeSex = chick?.sex || '';
const activeLabel = activeSex === 'poop' ? '💩' : activeSex === 'male' ? '♂' : activeSex === 'female' ? '♀' : ''; const activeLabel = activeSex === 'poop' ? '💩' : activeSex === 'male' ? '♂' : activeSex === 'female' ? '♀' : '';
@ -500,7 +503,7 @@ function miniScannerGrid(scanner, chick) {
classes.push('scanner'); classes.push('scanner');
if (activeSex) classes.push(activeSex); if (activeSex) classes.push(activeSex);
label = activeLabel || 'S'; label = activeLabel || 'S';
} else if (row === scanner.row - 1 && col === scanner.col) { } else if (k === inputKey) {
classes.push('input'); classes.push('input');
label = 'IN'; label = 'IN';
} else if (farmCells.has(k)) { } else if (farmCells.has(k)) {
@ -589,6 +592,14 @@ canvas.addEventListener('pointerdown', event => {
const expansionOffer = build.expansionOfferAtPoint?.(world); const expansionOffer = build.expansionOfferAtPoint?.(world);
if (expansionOffer) { build.buyGridExpansion(expansionOffer); clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); return; } if (expansionOffer) { build.buyGridExpansion(expansionOffer); clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); return; }
if (build.cycleBranchModeAtPoint?.(world)) { uiSystem.updatePanels(); uiSystem.updateUI(); return; } if (build.cycleBranchModeAtPoint?.(world)) { uiSystem.updatePanels(); uiSystem.updateUI(); return; }
const clickedCell = pointToCell(world.x, world.y);
const clickedHit = build.equipmentAtPoint(world);
if (!game.buildTool && clickedHit?.type === 'conveyor' && clickedCell && build.cycleSingleConveyorDirection?.(clickedCell)) {
uiSystem.updatePanels();
uiSystem.updateUI();
build.showSelectedMenu?.();
return;
}
if (game.buildTool === 'erase') { if (game.buildTool === 'erase') {
if (!build.equipmentAtPoint(world)) { cancelBuildAction(); return; } if (!build.equipmentAtPoint(world)) { cancelBuildAction(); return; }
build.eraseAtPoint(world); build.eraseAtPoint(world);
@ -624,6 +635,7 @@ canvas.addEventListener('pointerup', event => {
if (!game.groupDrag.committed) clickSelect(event); if (!game.groupDrag.committed) clickSelect(event);
build.finishGroupDrag(); build.finishGroupDrag();
} }
if (event.button === 2 && game.pan && !game.pan.moved && game.buildTool) cancelBuildAction();
if (game.buildTool === 'conveyor' || game.buildTool === 'boostConveyor') build.endConveyorDrag?.(); if (game.buildTool === 'conveyor' || game.buildTool === 'boostConveyor') build.endConveyorDrag?.();
game.groupDrag = null; game.pan = null; game.groupDrag = null; game.pan = null;
try { canvas.releasePointerCapture(event.pointerId); } catch (_) { /* noop */ } try { canvas.releasePointerCapture(event.pointerId); } catch (_) { /* noop */ }

View file

@ -1,7 +1,7 @@
import { DIRS, GRID, THEME, EFFECT_PRIORITY, VERSION } from '../core/config.js'; import { DIRS, GRID, THEME, EFFECT_PRIORITY, VERSION } from '../core/config.js';
import { expansionCost, expansionLots, lotBounds, expansionButtonBounds, isOwnedCell } from '../core/gridExpansion.js'; import { expansionCost, expansionLots, lotBounds, expansionButtonBounds, isOwnedCell } from '../core/gridExpansion.js';
import { key, parseKey, pointToCell, cellCenter, mixHex, randomBetween, yen } from '../core/utils.js'; import { key, parseKey, cellCenter, mixHex, randomBetween, yen } from '../core/utils.js';
import { getConveyorNeighbors, routeFromFarmToScanner, outputRoute, scannerCenter, scannerConnector, scannerOutputs, destinationLabel, destinationColor, scannerPortHasConveyor, disconnectedBuildWarnings } from '../systems/routing.js'; import { getConveyorNeighbors, scannerCenter, scannerConnector, scannerOutputs, destinationLabel, destinationColor, scannerPortHasConveyor, disconnectedBuildWarnings } from '../systems/routing.js';
import { upgradedMixerPrice, upgradedTruckPrice } from '../systems/economy.js'; import { upgradedMixerPrice, upgradedTruckPrice } from '../systems/economy.js';
import { cardTargetBounds } from '../systems/cards.js'; import { cardTargetBounds } from '../systems/cards.js';
import { wearRatio } from '../systems/maintenance.js'; import { wearRatio } from '../systems/maintenance.js';
@ -270,7 +270,7 @@ function drawConveyors(ctx, game) {
drawDirtOverlay(ctx, c.x - 14, c.y - 14, 28, 28, { meta }); drawDirtOverlay(ctx, c.x - 14, c.y - 14, 28, 28, { meta });
drawSelection(ctx, game, 'conveyor', k, c.x, c.y, 38, 38); drawSelection(ctx, game, 'conveyor', k, c.x, c.y, 38, 38);
drawConveyorDirectionMarkers(ctx, c, directionMarkers.get(k) || []); drawConveyorDirectionMarkers(ctx, c, directionMarkers.get(k) || []);
drawBranchModeLabel(ctx, c, meta, reachableConveyorDirs(game, k, meta)); drawBranchModeLabel(ctx, c, meta, reachableConveyorDirs(game, k, meta), connectedConveyorDirs(game, k));
if (ratio > 0.5) label(ctx, c.x, c.y - 15, `${Math.floor(ratio * 100)}%`, THEME.danger); if (ratio > 0.5) label(ctx, c.x, c.y - 15, `${Math.floor(ratio * 100)}%`, THEME.danger);
} }
ctx.restore(); ctx.restore();
@ -295,65 +295,74 @@ function reachableConveyorDirs(game, k, meta) {
.filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr))) .filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr)))
.map(d => d.name); .map(d => d.name);
const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))] const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
.filter(name => neighborNames.includes(name) && !neighborPointsIntoCell(game, p, name)); .filter(name => neighborNames.includes(name));
const inferred = neighborNames.filter(name => !neighborPointsIntoCell(game, p, name)); const inferred = neighborNames;
return explicit.length ? explicit : inferred; return explicit.length ? explicit : inferred;
} }
function connectedConveyorDirs(game, k) {
const p = parseKey(k);
return DIRS
.filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr)))
.map(d => d.name);
}
function effectiveConveyorDirs(game, k, meta) { function effectiveConveyorDirs(game, k, meta) {
const p = parseKey(k); const p = parseKey(k);
const neighborNames = DIRS const neighborNames = DIRS
.filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr))) .filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr)))
.map(d => d.name); .map(d => d.name);
const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))] const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
.filter(name => neighborNames.includes(name) && !neighborPointsIntoCell(game, p, name)); .filter(name => neighborNames.includes(name));
const inferred = neighborNames.filter(name => !neighborPointsIntoCell(game, p, name)); const inferred = neighborNames;
return explicit.length ? explicit : inferred; return explicit.length ? explicit : inferred;
} }
function drawBranchModeLabel(ctx, center, meta, dirs) { function drawBranchModeLabel(ctx, center, meta, dirs, connectedDirs = dirs) {
if (dirs.length < 3) return; if (connectedDirs.length < 3 || dirs.length < 2) return;
ctx.save(); ctx.save();
ctx.font = '900 11px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.lineWidth = 3;
ctx.fillStyle = 'rgba(255,255,255,.96)';
const mode = meta.branchMode === 'pass' && dirs.length >= 4 ? 'pass' : (dirs.includes(meta.branchMode) ? meta.branchMode : 'random'); const mode = meta.branchMode === 'pass' && dirs.length >= 4 ? 'pass' : (dirs.includes(meta.branchMode) ? meta.branchMode : 'random');
ctx.strokeStyle = mode !== 'random' ? THEME.warn : THEME.green; const isFixed = mode !== 'random';
rect(ctx, center.x - 24, center.y + 11, 48, 24, true, true);
ctx.fillStyle = mode !== 'random' ? THEME.warn : THEME.ink;
const text = branchModeText(mode); const text = branchModeText(mode);
ctx.fillText(text, center.x, center.y + 27); ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.lineWidth = 4;
ctx.fillStyle = 'rgba(255,255,255,.9)';
ctx.strokeStyle = isFixed ? THEME.warn : THEME.green;
rect(ctx, center.x - 28, center.y - 19, 56, 38, true, true);
ctx.strokeStyle = THEME.ink;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(center.x - 18, center.y);
ctx.lineTo(center.x + 18, center.y);
ctx.moveTo(center.x, center.y - 13);
ctx.lineTo(center.x, center.y + 13);
ctx.stroke();
ctx.fillStyle = isFixed ? THEME.warn : THEME.green;
ctx.beginPath();
ctx.arc(center.x, center.y, 12, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = THEME.ink;
ctx.lineWidth = 2;
ctx.stroke();
ctx.font = '900 9px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = THEME.ink;
ctx.fillText(text, center.x, center.y + 0.5);
ctx.restore(); ctx.restore();
} }
function cellKeyFromPoint(game, p) {
const cell = pointToCell(p.x, p.y);
if (!cell) return null;
const center = cellCenter(cell.col, cell.row);
if (Math.hypot(center.x - p.x, center.y - p.y) > GRID.cell * 0.38) return null;
const k = key(cell.col, cell.row);
return game.conveyorTiles.has(k) ? k : null;
}
function addMarkerFromRoute(game, markers, route) {
if (!route || route.length < 2) return;
for (let i = 0; i < route.length - 1; i += 1) {
const a = route[i], b = route[i + 1];
const ak = cellKeyFromPoint(game, a);
const bk = cellKeyFromPoint(game, b);
if (!ak || !bk || ak === bk) continue;
const ac = parseKey(ak), bc = parseKey(bk);
const dc = bc.col - ac.col, dr = bc.row - ac.row;
if (Math.abs(dc) + Math.abs(dr) !== 1) continue;
const angle = Math.atan2(dr, dc);
if (!markers.has(ak)) markers.set(ak, []);
const list = markers.get(ak);
if (!list.some(x => Math.abs(Math.sin((x - angle) / 2)) < 0.01)) list.push(angle);
}
}
function explicitConveyorAngles(game, k, meta) { function explicitConveyorAngles(game, k, meta) {
if (!meta) return []; if (!meta) return [];
return effectiveConveyorDirs(game, k, meta) const p = parseKey(k);
const neighborNames = DIRS
.filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr)))
.map(d => d.name);
const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
.filter(name => neighborNames.includes(name));
const dirs = explicit.length ? explicit : effectiveConveyorDirs(game, k, meta).slice(0, 1);
return dirs
.map(name => DIRS.find(d => d.name === name)?.angle) .map(name => DIRS.find(d => d.name === name)?.angle)
.filter(angle => Number.isFinite(angle)); .filter(angle => Number.isFinite(angle));
} }
@ -364,10 +373,6 @@ function collectConveyorDirectionMarkers(game) {
const angles = explicitConveyorAngles(game, k, meta); const angles = explicitConveyorAngles(game, k, meta);
if (angles.length) markers.set(k, angles); if (angles.length) markers.set(k, angles);
} }
for (const farm of game.eggFarms) addMarkerFromRoute(game, markers, routeFromFarmToScanner(game, farm)?.route);
for (const scanner of game.scanners) {
for (const side of ['left', 'right']) addMarkerFromRoute(game, markers, outputRoute(game, side, scannerCenter(scanner), scanner.id)?.route);
}
return markers; return markers;
} }
function drawConveyorDirectionMarkers(ctx, center, angles) { function drawConveyorDirectionMarkers(ctx, center, angles) {

View file

@ -23,7 +23,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
let conveyorDragMoved = false; let conveyorDragMoved = false;
const BRANCH_MODES = ['random', 'pass', 'up', 'right', 'down', 'left']; const BRANCH_MODES = ['random', 'pass', 'up', 'right', 'down', 'left'];
const BRANCH_LABELS = { random: 'RND', pass: 'PASS', up: 'UP', right: 'RT', down: 'DN', left: 'LF' }; const BRANCH_LABELS = { random: 'RND', pass: 'PASS', up: 'UP', right: 'RT', down: 'DN', left: 'LF' };
const BRANCH_MARKER = { w: 48, h: 24, y: 23 }; const BRANCH_MARKER = { w: 56, h: 38, y: 0 };
function gridExpansionCost() { return expansionCost(game); } function gridExpansionCost() { return expansionCost(game); }
@ -243,8 +243,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
const explicit = normalizeOutDirs(meta); const explicit = normalizeOutDirs(meta);
const neighbors = conveyorNeighborOutDirs(cell); const neighbors = conveyorNeighborOutDirs(cell);
const explicitReachable = [...new Set([...explicit, meta?.dir].filter(Boolean))] const explicitReachable = [...new Set([...explicit, meta?.dir].filter(Boolean))]
.filter(dir => neighbors.includes(dir) && !neighborPointsIntoCell(cell, dir)); .filter(dir => neighbors.includes(dir));
const inferredExits = neighbors.filter(dir => !neighborPointsIntoCell(cell, dir)); const inferredExits = neighbors;
const reachable = explicitReachable.length ? explicitReachable : inferredExits; const reachable = explicitReachable.length ? explicitReachable : inferredExits;
if (meta) { if (meta) {
meta.outDirs = explicit.filter(dir => neighbors.includes(dir)); meta.outDirs = explicit.filter(dir => neighbors.includes(dir));
@ -273,7 +273,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
function availableConveyorOutDirs(cell) { function availableConveyorOutDirs(cell) {
if (!cell) return []; if (!cell) return [];
return reachableOutDirsForKey(key(cell.col, cell.row)); const neighbors = conveyorNeighborOutDirs(cell);
return neighbors.length ? neighbors : DIRS.map(d => d.name);
} }
function cycleSingleConveyorDirection(cell) { function cycleSingleConveyorDirection(cell) {
@ -709,10 +710,13 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
ui.modalTitle.textContent = selectedTitle(obj); ui.modalTitle.textContent = selectedTitle(obj);
ui.modalBody.innerHTML = ` ui.modalBody.innerHTML = `
<div class="equipment-menu-lines compact">${lines.map(x => `<p>${x}</p>`).join('')}</div> <div class="equipment-menu-lines compact">${lines.map(x => `<p>${x}</p>`).join('')}</div>
${['mixer', 'truck'].includes(obj.id) ? '<p class="formula-box compact">Income/Fine = ceil(base × 1.05^upgrades)</p>' : ''}`; ${['mixer', 'truck'].includes(obj.baseId || obj.id) ? '<p class="formula-box compact">Income/Fine = ceil(base x 1.10^upgrades)</p>' : ''}`;
ui.modalActions.innerHTML = ''; ui.modalActions.innerHTML = '';
if (obj.type === 'scanner' && obj.kind === 'auto') ui.modalActions.appendChild(modalButton('Auto Menu', () => showAutoScannerMenu(obj), 'facility-action warn')); if (obj.type === 'scanner' && obj.kind === 'auto') ui.modalActions.appendChild(modalButton('Auto Menu', () => showAutoScannerMenu(obj), 'facility-action warn'));
if (obj.type === 'scanner' && obj.kind === 'manual') ui.modalActions.appendChild(modalButton('Set Keys', () => showManualScannerMenu(obj), 'facility-action warn')); if (obj.type === 'scanner' && obj.kind === 'manual') ui.modalActions.appendChild(modalButton('Set Keys', () => showManualScannerMenu(obj), 'facility-action warn'));
if (obj.type === 'conveyor' && isBranchKey(obj.id)) {
ui.modalActions.appendChild(modalButton(`Branch: ${branchModeLabel(game.conveyorMeta.get(obj.id)?.branchMode || 'random')}`, () => { cycleBranchModeForKey(obj.id); showSelectedMenu(); }, 'facility-action warn'));
}
const hit = obj.type === 'conveyor' ? { type: 'conveyor', oldKey: obj.id, ref: obj } : { type: obj.type, ref: obj }; const hit = obj.type === 'conveyor' ? { type: 'conveyor', oldKey: obj.id, ref: obj } : { type: obj.type, ref: obj };
const saleReason = lastProtectedSaleReason(hit); const saleReason = lastProtectedSaleReason(hit);
if (saleReason) { if (saleReason) {
@ -764,8 +768,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
} }
function isBranchKey(k) { function isBranchKey(k) {
const dirs = branchOutDirsForKey(k); if (!game.conveyorTiles.has(k)) return false;
return dirs.length >= 3; return conveyorNeighborOutDirs(parseKey(k)).length >= 3;
} }
function branchSwitcherAtPoint(p) { function branchSwitcherAtPoint(p) {
@ -784,6 +788,10 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
function cycleBranchModeAtPoint(p) { function cycleBranchModeAtPoint(p) {
const k = branchSwitcherAtPoint(p); const k = branchSwitcherAtPoint(p);
if (!k) return false; if (!k) return false;
return cycleBranchModeForKey(k);
}
function cycleBranchModeForKey(k) {
const meta = game.conveyorMeta.get(k); const meta = game.conveyorMeta.get(k);
if (!meta) return false; if (!meta) return false;
const reachable = reachableOutDirsForKey(k); const reachable = reachableOutDirsForKey(k);
@ -829,8 +837,12 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
lines.push(`Type: ${meta?.kind === 'boostConveyor' ? 'BOOST BELT' : 'Normal Belt'} | Speed x${Number(meta?.speedMultiplier || 1).toFixed(1)}`); lines.push(`Type: ${meta?.kind === 'boostConveyor' ? 'BOOST BELT' : 'Normal Belt'} | Speed x${Number(meta?.speedMultiplier || 1).toFixed(1)}`);
lines.push(`Congestion: ${c ? Math.floor(c.ratio * 100) : 0}%`); lines.push(`Congestion: ${c ? Math.floor(c.ratio * 100) : 0}%`);
lines.push(`Durability: ${remainingPercent({ meta })}% | Speed x${performanceFactor({ meta }).toFixed(2)}`); lines.push(`Durability: ${remainingPercent({ meta })}% | Speed x${performanceFactor({ meta }).toFixed(2)}`);
lines.push(`Branch mode: ${branchModeLabel(meta?.branchMode || 'random')}`); if (isBranchKey(obj.id)) {
lines.push('Click the branch marker to cycle Random / Up / Right / Down / Left.'); lines.push(`Branch mode: ${branchModeLabel(meta?.branchMode || 'random')}`);
lines.push('Use the Branch button below or the on-belt branch badge.');
} else {
lines.push('Branch mode: needs 3+ conveyor exits.');
}
lines.push(`Sale value: ${yen(resaleValueFor({ type: 'conveyor', oldKey: obj.id, ref: obj }, game).amount)}`); lines.push(`Sale value: ${yen(resaleValueFor({ type: 'conveyor', oldKey: obj.id, ref: obj }, game).amount)}`);
} else if (obj.type === 'facility') { } else if (obj.type === 'facility') {
lines.push(`Price: ${yen(equipmentPrice(obj))}`); lines.push(`Price: ${yen(equipmentPrice(obj))}`);
@ -891,7 +903,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
selectedInfoLines, flavorText, disconnectedWarningFor, selectedInfoLines, flavorText, disconnectedWarningFor,
gridExpansionCost, buyGridExpansion, expansionOfferAtPoint, gridExpansionCost, buyGridExpansion, expansionOfferAtPoint,
beginConveyorDrag, continueConveyorDrag, endConveyorDrag, beginConveyorDrag, continueConveyorDrag, endConveyorDrag,
branchSwitcherAtPoint, cycleBranchModeAtPoint, cycleSingleConveyorDirection,
branchSwitcherAtPoint, cycleBranchModeAtPoint, cycleBranchModeForKey,
removeSelected, switchScannerRole, showManualScannerMenu, removeSelected, switchScannerRole, showManualScannerMenu,
showAutoScannerMenu, showSelectedMenu, setBuildTool, fail, showAutoScannerMenu, showSelectedMenu, setBuildTool, fail,
isEquipmentCell, equipmentHitBoxes, isEquipmentCell, equipmentHitBoxes,

View file

@ -45,6 +45,10 @@ export function extraEggOutletCount(_game, farm = null) {
return Math.min(3, Math.max(0, Number(farm?.extraEggOutlet) || 0)); return Math.min(3, Math.max(0, Number(farm?.extraEggOutlet) || 0));
} }
export function scannerQueueSpacingMultiplier(game) {
return Math.pow(0.85, effectCount(game, 'scannerQueueSpacing'));
}
export function conveyorSpeedForGame(game, conveyorKey = null) { export function conveyorSpeedForGame(game, conveyorKey = null) {
const effects = ensureCardState(game); const effects = ensureCardState(game);
const speed = CONVEYOR_SPEED * Math.pow(BEARING_SPEED_MULTIPLIER, Math.max(0, effects.bearing || 0)); const speed = CONVEYOR_SPEED * Math.pow(BEARING_SPEED_MULTIPLIER, Math.max(0, effects.bearing || 0));
@ -114,7 +118,7 @@ function targetLabel(game, target) {
if (target.type === 'scanner') { if (target.type === 'scanner') {
return `AUTO #${target.id} L${target.level} -> L${target.level + 1} | CD ${autoScannerCooldownSeconds(target, game).toFixed(1)}s -> ${autoScannerCooldownSeconds({ ...target, level: target.level + 1 }, game).toFixed(1)}s`; return `AUTO #${target.id} L${target.level} -> L${target.level + 1} | CD ${autoScannerCooldownSeconds(target, game).toFixed(1)}s -> ${autoScannerCooldownSeconds({ ...target, level: target.level + 1 }, game).toFixed(1)}s`;
} }
if (target.type === 'facility' && target.id === 'mixer') { if (target.type === 'facility' && (target.baseId || target.id) === 'mixer') {
const beforeLevel = target.level; const beforeLevel = target.level;
const before = upgradedMixerPrice(game); const before = upgradedMixerPrice(game);
target.level = beforeLevel + 1; target.level = beforeLevel + 1;
@ -122,7 +126,7 @@ function targetLabel(game, target) {
target.level = beforeLevel; target.level = beforeLevel;
return `MIXER L${target.level} -> L${target.level + 1} | ${yen(before)} -> ${yen(after)}`; return `MIXER L${target.level} -> L${target.level + 1} | ${yen(before)} -> ${yen(after)}`;
} }
if (target.type === 'facility' && target.id === 'truck') { if (target.type === 'facility' && (target.baseId || target.id) === 'truck') {
const beforeLevel = target.level; const beforeLevel = target.level;
const before = upgradedTruckPrice(game); const before = upgradedTruckPrice(game);
target.level = beforeLevel + 1; target.level = beforeLevel + 1;
@ -370,6 +374,7 @@ function applyInstantCard(game, card) {
if (card.id === 'dudRefund') inc('dudRefund'); if (card.id === 'dudRefund') inc('dudRefund');
if (card.id === 'freeReroll') inc('freeReroll'); if (card.id === 'freeReroll') inc('freeReroll');
if (card.id === 'composter') inc('composter'); if (card.id === 'composter') inc('composter');
if (card.id === 'scannerQueueSpacing') inc('scannerQueueSpacing');
if (card.id === 'flattery') inc('fairiesFlatteryNext'); if (card.id === 'flattery') inc('fairiesFlatteryNext');
if (card.id === 'usedMachine') effects.usedMachineActive = true; if (card.id === 'usedMachine') effects.usedMachineActive = true;
if (card.id === 'newMachine') effects.usedMachineActive = false; if (card.id === 'newMachine') effects.usedMachineActive = false;
@ -400,6 +405,18 @@ function incomeAtLevel(base, level) {
return Math.ceil(base * Math.pow(ECONOMY.incomeUpgradeRate, Math.max(0, level - 1))); return Math.ceil(base * Math.pow(ECONOMY.incomeUpgradeRate, Math.max(0, level - 1)));
} }
function incomeUpgradePreview(game, id, base) {
const targets = targetsForCard(game, id === 'mixer' ? 'upgradeMixer' : 'upgradeTruck');
const target = targets[0];
if (!target) return { before: incomeAtLevel(base, 1), after: incomeAtLevel(base, 2) };
const before = id === 'mixer' ? upgradedMixerPrice(game) : upgradedTruckPrice(game);
const beforeLevel = target.level || 1;
target.level = beforeLevel + 1;
const after = id === 'mixer' ? upgradedMixerPrice(game) : upgradedTruckPrice(game);
target.level = beforeLevel;
return { before, after };
}
function cardDescription(game, card) { function cardDescription(game, card) {
const e = ensureCardState(game); const e = ensureCardState(game);
if (card.id === 'upgradeEgg') { if (card.id === 'upgradeEgg') {
@ -414,12 +431,12 @@ function cardDescription(game, card) {
return `Choose one AUTO SCANNER. Level +1. Cooldown ${before.toFixed(2)}s -> ${after.toFixed(2)}s.`; return `Choose one AUTO SCANNER. Level +1. Cooldown ${before.toFixed(2)}s -> ${after.toFixed(2)}s.`;
} }
if (card.id === 'upgradeMixer') { if (card.id === 'upgradeMixer') {
const level = game.facilities?.mixer?.level || 1; const preview = incomeUpgradePreview(game, 'mixer', ECONOMY.income.mixer);
return `MIXER level +1. Male-chick income ${yen(incomeAtLevel(ECONOMY.income.mixer, level))} -> ${yen(incomeAtLevel(ECONOMY.income.mixer, level + 1))}.`; return `Choose one MIXER. Earning +10%. Male-chick income ${yen(preview.before)} -> ${yen(preview.after)}.`;
} }
if (card.id === 'upgradeTruck') { if (card.id === 'upgradeTruck') {
const level = game.facilities?.truck?.level || 1; const preview = incomeUpgradePreview(game, 'truck', ECONOMY.income.truck);
return `TRUCK level +1. Correct shipment income ${yen(incomeAtLevel(ECONOMY.income.truck, level))} -> ${yen(incomeAtLevel(ECONOMY.income.truck, level + 1))}.`; return `Choose one TRUCK. Earning +10%. Correct shipment income ${yen(preview.before)} -> ${yen(preview.after)}.`;
} }
if (card.id === 'upgradeTrash') { if (card.id === 'upgradeTrash') {
const before = shredderUpgradeCount(game); const before = shredderUpgradeCount(game);
@ -462,6 +479,7 @@ function cardDescription(game, card) {
if (card.id === 'dudRefund') return `Held ${e.dudRefund || 0}. After pick: DUD clicks award ${yen(50 * ((e.dudRefund || 0) + 1))}.`; if (card.id === 'dudRefund') return `Held ${e.dudRefund || 0}. After pick: DUD clicks award ${yen(50 * ((e.dudRefund || 0) + 1))}.`;
if (card.id === 'freeReroll') return `Held ${e.freeReroll || 0}. After pick: each card screen starts with ${(e.freeReroll || 0) + 1} free reroll${(e.freeReroll || 0) + 1 === 1 ? '' : 's'}.`; if (card.id === 'freeReroll') return `Held ${e.freeReroll || 0}. After pick: each card screen starts with ${(e.freeReroll || 0) + 1} free reroll${(e.freeReroll || 0) + 1 === 1 ? '' : 's'}.`;
if (card.id === 'composter') return `Held ${e.composter || 0}. After pick: SHREDDER poop grants ${yen(3 * ((e.composter || 0) + 1))}.`; if (card.id === 'composter') return `Held ${e.composter || 0}. After pick: SHREDDER poop grants ${yen(3 * ((e.composter || 0) + 1))}.`;
if (card.id === 'scannerQueueSpacing') return `Held ${e.scannerQueueSpacing || 0}. After pick: Manual/Auto Scanner queue spacing ${formatPercent(Math.pow(0.85, (e.scannerQueueSpacing || 0) + 1))}.`;
if (card.id === 'flattery') return `Next Fairies tribute -50%. Held for next tribute: ${e.fairiesFlatteryNext || 0}.`; if (card.id === 'flattery') return `Next Fairies tribute -50%. Held for next tribute: ${e.fairiesFlatteryNext || 0}.`;
if (card.id === 'usedMachine') return 'Enable Used Machines: future equipment costs 50%, has no SELL refund, and starts with 60% durability.'; if (card.id === 'usedMachine') return 'Enable Used Machines: future equipment costs 50%, has no SELL refund, and starts with 60% durability.';
if (card.id === 'newMachine') return 'Cancel Used Machines. Future equipment returns to normal price, refund, and durability.'; if (card.id === 'newMachine') return 'Cancel Used Machines. Future equipment returns to normal price, refund, and durability.';

View file

@ -4,7 +4,7 @@ import { nextSpawnDelay } from '../core/state.js';
import { key, parseKey, pointToCell, cellCenter, randomBetween, yen, inGrid, directionNameBetweenCells } from '../core/utils.js'; import { key, parseKey, pointToCell, cellCenter, randomBetween, yen, inGrid, directionNameBetweenCells } from '../core/utils.js';
import { scannerById, scannerBySlot, scannerCenter, scannerConnector, nearestConveyorKey, buildConveyorComponents, autoSideFor, ensureFactoryGraph } from './routing.js'; import { scannerById, scannerBySlot, scannerCenter, scannerConnector, nearestConveyorKey, buildConveyorComponents, autoSideFor, ensureFactoryGraph } from './routing.js';
import { applyMixerIncome, applyMixerPoopFine, applyTruckPoopFine, applyWrongTruckFine, upgradedTruckPrice, applyShredderBonus, applyRevenue } from './economy.js'; import { applyMixerIncome, applyMixerPoopFine, applyTruckPoopFine, applyWrongTruckFine, upgradedTruckPrice, applyShredderBonus, applyRevenue } from './economy.js';
import { autoScannerCooldownSeconds, eggProductionDelayMultiplier, extraEggOutletCount } from './cards.js'; import { autoScannerCooldownSeconds, eggProductionDelayMultiplier, extraEggOutletCount, scannerQueueSpacingMultiplier } from './cards.js';
import { productionMultiplier, truckTarget, isTargetTruckCargo, shouldFineMaleTruck } from './contracts.js'; import { productionMultiplier, truckTarget, isTargetTruckCargo, shouldFineMaleTruck } from './contracts.js';
import { floating, shake, spawnPulse, scannerPulse, meatEffect, sludgeEffect, shredEffect, truckLoadEffect, shockwave, sparkBurst, smokeBurst, flyingDebris, rageEffect } from './effects.js'; import { floating, shake, spawnPulse, scannerPulse, meatEffect, sludgeEffect, shredEffect, truckLoadEffect, shockwave, sparkBurst, smokeBurst, flyingDebris, rageEffect } from './effects.js';
import { countAutoSorted, countCorrect, countMistake, countMixer, countPoopDestination, countPoopSpawned, countTrash, countTruckCargo } from './stats.js'; import { countAutoSorted, countCorrect, countMistake, countMixer, countPoopDestination, countPoopSpawned, countTrash, countTruckCargo } from './stats.js';
@ -104,6 +104,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
} }
updateScannerQueues(dt); updateScannerQueues(dt);
updateCongestion(); updateCongestion();
checkCongestionExplosions();
if (game.timeLeft <= 0) { if (game.timeLeft <= 0) {
if (game.chicks.length === 0) completeTurn(); if (game.chicks.length === 0) completeTurn();
else if (game.shutdownTimeLeft <= 0) blowOffRemainingForCleanup(); else if (game.shutdownTimeLeft <= 0) blowOffRemainingForCleanup();
@ -176,8 +177,8 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
const inferred = DIRS const inferred = DIRS
.filter(d => movementOptionFromDir(cell, d.name)) .filter(d => movementOptionFromDir(cell, d.name))
.map(d => d.name); .map(d => d.name);
const cleanExplicit = explicit.filter(dir => inferred.includes(dir) && !neighborPointsIntoCell(cell, dir)); const cleanExplicit = explicit.filter(dir => inferred.includes(dir));
const inferredExits = inferred.filter(dir => !neighborPointsIntoCell(cell, dir)); const inferredExits = inferred;
return cleanExplicit.length ? cleanExplicit : inferredExits; return cleanExplicit.length ? cleanExplicit : inferredExits;
} }
@ -354,7 +355,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
function positionScannerQueue(scanner) { function positionScannerQueue(scanner) {
scanner.queue = scanner.queue.filter(id => game.chicks.some(ch => ch.id === id && ch.stage === 'queued')); scanner.queue = scanner.queue.filter(id => game.chicks.some(ch => ch.id === id && ch.stage === 'queued'));
const spacing = GRID.cell * 0.86; const spacing = GRID.cell * 0.86 * scannerQueueSpacingMultiplier(game);
scanner.queue.forEach((id, idx) => { scanner.queue.forEach((id, idx) => {
const chick = game.chicks.find(ch => ch.id === id); const chick = game.chicks.find(ch => ch.id === id);
if (!chick) return; if (!chick) return;
@ -683,6 +684,8 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
} }
} }
if (comp.ratio >= 1) { if (comp.ratio >= 1) {
const victims = chicksInComponent(comp.id);
if (!victims.length || victims.some(chick => chick.stage !== 'queued' && chick.stoppedTimer <= 0.2)) continue;
const last = game.lastExplodedComponent.get(comp.id) || 0; const last = game.lastExplodedComponent.get(comp.id) || 0;
const now = performance.now(); const now = performance.now();
if (now - last > 900) { if (now - last > 900) {

View file

@ -34,7 +34,10 @@ export function buildPrice(id, game = null) {
} }
export function facilityUpgradeCount(game, id) { export function facilityUpgradeCount(game, id) {
return Math.max(0, (game.facilities?.[id]?.level || 1) - 1); if (!game?.facilities) return 0;
return Object.values(game.facilities)
.filter(f => (f?.baseId || f?.id) === id)
.reduce((sum, f) => sum + Math.max(0, (f.level || 1) - 1), 0);
} }
export function incomeMultiplier(game, id) { export function incomeMultiplier(game, id) {

View file

@ -64,7 +64,7 @@ export function restore(game, text) {
game.conveyorTiles = new Set(data.conveyorTiles || []); game.conveyorTiles = new Set(data.conveyorTiles || []);
game.conveyorMeta = new Map(data.conveyorMeta || []); game.conveyorMeta = new Map(data.conveyorMeta || []);
game.branchCounters = new Map(data.branchCounters || []); game.branchCounters = new Map(data.branchCounters || []);
game.cardEffects = data.cardEffects || { bearing: 0, legalWork: 0, flattery: 0 }; game.cardEffects = data.cardEffects || { bearing: 0, legalWork: 0, fairiesFlatteryNext: 0, scannerQueueSpacing: 0 };
if (game.cardEffects.drawBonus != null) delete game.cardEffects.drawBonus; if (game.cardEffects.drawBonus != null) delete game.cardEffects.drawBonus;
game.cardDraft = data.cardDraft || { pending: false, choices: [], rerolls: 0, picksRemaining: 0 }; game.cardDraft = data.cardDraft || { pending: false, choices: [], rerolls: 0, picksRemaining: 0 };
if (game.cardDraft.picksRemaining == null) game.cardDraft.picksRemaining = game.cardDraft.pending ? 1 : 0; if (game.cardDraft.picksRemaining == null) game.cardDraft.picksRemaining = game.cardDraft.pending ? 1 : 0;

View file

@ -72,7 +72,8 @@ function graphPortConveyorCells(game, point, blocked = [], preferred = []) {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export function scannerInputCells(game, scanner) { export function scannerInputCells(game, scanner) {
const connector = scannerConnector(scanner, 'inputA'); const connector = scannerConnector(scanner, 'inputA');
return visualPortConveyorCells(game, connector, scannerFootprintCells(scanner)); if (!connector || !inGrid(connector.col, connector.row)) return [];
return game.conveyorTiles.has(key(connector.col, connector.row)) ? [{ ...connector, viaTolerance: false }] : [];
} }
function outputStartCells(game, scanner, side) { function outputStartCells(game, scanner, side) {
@ -135,8 +136,8 @@ function inferredConveyorOutDirNames(game, cellKey) {
const neighbors = DIRS const neighbors = DIRS
.filter(d => game.conveyorTiles.has(graphNodeKey(p.col + d.dc, p.row + d.dr))) .filter(d => game.conveyorTiles.has(graphNodeKey(p.col + d.dc, p.row + d.dr)))
.map(d => d.name); .map(d => d.name);
const explicit = conveyorOutDirNames(game, cellKey).filter(dir => neighbors.includes(dir) && !neighborPointsIntoCell(game, p, dir)); const explicit = conveyorOutDirNames(game, cellKey).filter(dir => neighbors.includes(dir));
const inferred = neighbors.filter(dir => !neighborPointsIntoCell(game, p, dir)); const inferred = neighbors;
return explicit.length ? explicit : inferred; return explicit.length ? explicit : inferred;
} }
@ -229,8 +230,10 @@ function indexGraphPorts(game, graph) {
const ports = {}; const ports = {};
for (const type of ['inputA', 'left', 'right']) { for (const type of ['inputA', 'left', 'right']) {
const cell = scannerConnector(scanner, type); const cell = scannerConnector(scanner, type);
const cells = graphPortConveyorCells(game, cell, scannerFootprintCells(scanner)) const cells = type === 'inputA'
.filter(p => graph.cells.has(graphNodeKey(p.col, p.row))); ? (cell && graph.cells.has(graphNodeKey(cell.col, cell.row)) ? [{ ...cell }] : [])
: graphPortConveyorCells(game, cell, scannerFootprintCells(scanner))
.filter(p => graph.cells.has(graphNodeKey(p.col, p.row)));
const cellKeys = cells.map(p => graphNodeKey(p.col, p.row)); const cellKeys = cells.map(p => graphNodeKey(p.col, p.row));
const connected = cellKeys.length > 0; const connected = cellKeys.length > 0;
ports[type] = { type, cell, cells, keys: cellKeys, key: cellKeys[0] || null, connected }; ports[type] = { type, cell, cells, keys: cellKeys, key: cellKeys[0] || null, connected };

View file

@ -214,7 +214,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
function showTitle() { function showTitle() {
ui.modalTitle.textContent = TEXT.versionTitle; ui.modalTitle.textContent = TEXT.versionTitle;
ui.modalBody.innerHTML = `<div class="route-cards"><div class="route-card"><span class="route-icon male" aria-label="male chick">髫ィ蜷カ繝サ/span><strong>Male</strong><b>-></b><em>Mixer</em><small>S1: A</small></div><div class="route-card"><span class="route-icon female" aria-label="female chick">髫ィ蜊債€</span><strong>Female</strong><b>-></b><em>Truck</em><small>S1: D, then S2: right</small></div><div class="route-card"><span class="route-icon poop" aria-label="poop">・滓ィ頑c</span><strong>Poop</strong><b>-></b><em>Shredder</em><small>S1: D, then S2: left</small></div></div><p>Build phase: connect at least one EGG through a scanner to Mixer, Shredder, or Truck before Next Day. One day is one run.</p>`; ui.modalBody.innerHTML = `<div class="route-cards"><div class="route-card"><span class="route-icon male" aria-label="male chick">♂</span><strong>Male</strong><b>-></b><em>Mixer</em><small>S1: A</small></div><div class="route-card"><span class="route-icon female" aria-label="female chick">♀</span><strong>Female</strong><b>-></b><em>Truck</em><small>S1: D, then S2: right</small></div><div class="route-card"><span class="route-icon poop" aria-label="poop">💩</span><strong>Poop</strong><b>-></b><em>Shredder</em><small>S1: D, then S2: left</small></div></div><p>Build phase: connect at least one EGG through a scanner to Mixer, Shredder, or Truck before Next Day. One day is one run.</p>`;
ui.modalActions.innerHTML = ''; ui.modalActions.innerHTML = '';
ui.modalActions.appendChild(button(TEXT.actions.startGame, startGame, 'primary-button')); ui.modalActions.appendChild(button(TEXT.actions.startGame, startGame, 'primary-button'));
ui.modal.classList.remove('equipment-popover', 'gameover-modal'); ui.modal.classList.remove('equipment-popover', 'gameover-modal');

View file

@ -588,6 +588,52 @@ body { font-size: 20px; }
.modal.equipment-popover .modal-actions button { font-size: 11px; } .modal.equipment-popover .modal-actions button { font-size: 11px; }
.equipment-menu-lines.compact, .formula-box.compact { font-size: 11px; } .equipment-menu-lines.compact, .formula-box.compact { font-size: 11px; }
.hover-tooltip strong { font-size: 18px; } .hover-tooltip strong { font-size: 18px; }
/* Keep construction flavor text readable as a hover bubble, not button content. */
.large-tools .tool-button {
min-height: 62px;
position: relative;
overflow: visible;
}
.large-tools .tool-button:hover {
min-height: 62px;
}
.large-tools .tool-button .tool-flavor {
display: none;
position: absolute;
z-index: 90;
right: 0;
top: calc(100% + 8px);
width: min(320px, calc(100vw - 40px));
max-height: none;
opacity: 1;
overflow: visible;
margin: 0;
padding: 10px 12px;
border: 3px solid var(--line);
background: rgba(255,255,255,.98);
color: var(--ink);
box-shadow: 5px 5px 0 rgba(16,32,21,.18);
font-size: 14px;
line-height: 1.35;
pointer-events: none;
overflow-wrap: anywhere;
}
.large-tools .tool-button:hover .tool-flavor {
display: block;
}
.large-tools .tool-button .tool-flavor::before {
content: '';
position: absolute;
right: 18px;
top: -10px;
width: 16px;
height: 16px;
border-left: 3px solid var(--line);
border-top: 3px solid var(--line);
background: rgba(255,255,255,.98);
transform: rotate(45deg);
}
.hover-tooltip span { font-size: 15px; } .hover-tooltip span { font-size: 15px; }
.sort-button { font-size: 17px; } .sort-button { font-size: 17px; }