This commit is contained in:
33333-33333 2026-06-09 22:32:14 +09:00
commit d6a55d9c13
20 changed files with 187 additions and 672 deletions

82
dist/game.js vendored

File diff suppressed because one or more lines are too long

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Chick Sorter v27.8</title>
<title>Chick Sorter v27.6</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
@ -36,7 +36,7 @@
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
<div class="panel-head">
<div>
<h1>CHICK SORTER <span>v27.8</span></h1>
<h1>CHICK SORTER <span>v27.6</span></h1>
<p>Build, expand, connect at least one valid EGG route, then start the next day.</p>
</div>
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
@ -54,8 +54,8 @@
<button id="buildMixerButton" class="tool-button" type="button"><strong>Mixer</strong><span>・・450</span></button>
<button id="buildTrashButton" class="tool-button" type="button"><strong>Waste Shredder</strong><span>・・240</span></button>
<button id="buildTruckButton" class="tool-button" type="button"><strong>Truck</strong><span>・・450</span></button>
<button id="buildMaintenanceRoomButton" class="tool-button repair" type="button"><strong>Maintenance Room</strong><span>¥400 / 2x2</span></button>
<button id="eraseButton" class="tool-button danger" type="button"><strong>Sell</strong><span>Sell or refund equipment</span></button>
<button id="hireRepairmanButton" class="tool-button repair" type="button"><strong>Hire Repairman</strong><span>・・100 / next day</span></button>
<div class="history-tools"><button id="undoButton" class="tool-button small" type="button"><strong>Undo</strong><span>Ctrl+Z</span></button><button id="redoButton" class="tool-button small" type="button"><strong>Redo</strong><span>Ctrl+Y</span></button></div>
</div>
<div id="buildStatus" class="mini-box">Build tools unlock after each day.</div>
@ -67,12 +67,6 @@
</div>
</div>
<details id="cardInventory" class="acquired-cards-panel" aria-label="Acquired cards">
<summary>CARDS <strong>0</strong><span>acquired</span></summary>
<div class="acquired-cards-list">No passive cards yet.</div>
</details>
<div id="manualScannerMonitor" class="scanner-monitor" aria-label="Offscreen manual scanner monitor"></div>
<div class="control-dock" aria-label="Sorting controls">

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "zunda-shiwake",
"version": "27.8.0",
"version": "27.10.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "zunda-shiwake",
"version": "27.8.0",
"version": "27.10.0",
"devDependencies": {
"esbuild": "^0.25.0"
}

View file

@ -1,6 +1,6 @@
{
"name": "zunda-shiwake",
"version": "27.8.0",
"version": "27.6.0",
"private": true,
"type": "module",
"scripts": {

View file

@ -1,7 +1,7 @@
// Central balance sheet for tuning gameplay.
// Edit this file first when adjusting prices, income, penalties, timing, card rates, or caps.
export const BALANCE = {
version: 'v27.8 repair routing and acquired-card UI',
version: 'v27.6 direct conveyor chick movement',
time: {
daySeconds: 60,
farmShutdownGraceSeconds: 10
@ -88,15 +88,6 @@ export const BALANCE = {
description: 'Choose one AUTO SCANNER on the map and raise it by 1 level.',
tags: ['SCANNER', 'ACTIVE']
},
{
id: 'upgradeMaintenanceRoom',
title: 'MAINTENANCE ROOM Improvement',
rarity: 'common',
type: 'equipmentUpgrade',
target: 'maintenanceRoom',
description: 'Choose one MAINTENANCE ROOM and add +1 repair worker. Each worker costs JPY 80 per day.',
tags: ['MAINTENANCE', 'ACTIVE']
},
{
id: 'upgradeMixer',
title: 'MIXER Improvement',
@ -376,7 +367,7 @@ export const BALANCE = {
trash: 1.00
},
repairman: {
dailyCost: 80,
dailyCost: 100,
secondsPerOnePercent: 0.5,
walkSpeed: 90
}
@ -402,10 +393,6 @@ export const BALANCE = {
id: 'autoScanner', type: 'scanner', kind: 'auto', name: 'Auto Scanner', shortName: 'Auto Scanner', price: 360,
buildable: true, upgradeable: false
},
maintenanceRoom: {
id: 'maintenanceRoom', type: 'maintenanceRoom', name: 'Maintenance Room', shortName: 'MAINT ROOM', price: 400,
buildable: true, upgradeable: true, footprint: { cols: 2, rows: 2 }
},
mixer: {
id: 'mixer', type: 'facility', name: 'Mixer', shortName: 'MIXER', price: 450,
buildable: true, upgradeable: true, incomeKey: 'mixer', body: { w: 164, h: 126 }

View file

@ -24,7 +24,7 @@ export const EGG_SPAWN_RANGES = BALANCE.production.eggSpawnRanges;
export const GRID = BALANCE.grid;
export const FACILITY_DEFS = BALANCE.facilities;
export const BUILD_TOOL_IDS = ['conveyor', 'boostConveyor', 'eggFarm', 'autoScanner', 'manualScanner', 'maintenanceRoom', 'mixer', 'trash', 'truck'];
export const BUILD_TOOL_IDS = ['conveyor', 'boostConveyor', 'eggFarm', 'autoScanner', 'manualScanner', 'mixer', 'trash', 'truck'];
export const MACHINE_FACILITY_IDS = ['mixer', 'trash', 'truck'];
export const INCOME_FACILITY_IDS = ['mixer', 'truck'];

View file

@ -49,27 +49,6 @@ export function createScanner(game, col, row, kind) {
};
}
export function createMaintenanceRoom(game, col, row, price) {
const def = FACILITY_DEFS.maintenanceRoom || {};
const cols = def.footprint?.cols || 2;
const rows = def.footprint?.rows || 2;
return {
type: 'facility',
id: 'maintenanceRoom',
baseId: 'maintenanceRoom',
name: def.name || 'Maintenance Room',
shortName: def.shortName || 'MAINT ROOM',
col, row, cols, rows,
x: GRID.x + col * GRID.cell,
y: GRID.y + row * GRID.cell,
w: cols * GRID.cell,
h: rows * GRID.cell,
level: 1,
price: price || def.price || 400,
builtSession: game.buildSession
};
}
export function nearestGridEdge(point) {
const colFloat = (point.x - GRID.x) / GRID.cell;
const rowFloat = (point.y - GRID.y) / GRID.cell;

View file

@ -147,7 +147,6 @@ export function createGame() {
manualCombo: { count: 0, lastBonus: 0 },
pendingRepairWorkerWages: 0,
repairman: { hiredForNextDay: false, active: false, x: GRID.x + GRID.cell * 0.5, y: GRID.y + BALANCE.grid.rows * GRID.cell + 84, target: null, repairedToday: 0 },
repairmen: [],
stats: newTurnStats(),
lastResult: null,
totals: newTotalStats()
@ -323,7 +322,6 @@ export function resetLayout(game) {
game.repairman.active = false;
game.repairman.hiredForNextDay = false;
}
game.repairmen = [];
game.conveyorTiles.clear();
game.conveyorMeta.clear();
game.blockedCells = new Set();

View file

@ -24,8 +24,8 @@ export const TEXT = {
autoMenu: 'Auto Menu'
},
status: {
build: tool => `BUILD: ${(tool === 'erase' ? 'SELL' : (tool || 'SELECT')).toUpperCase()} | Right/middle-drag pan | Hover equipment for info`,
sorting: 'Sorting: use scanner keys. Right/middle-drag to pan. Build after clearing the day.',
build: tool => `BUILD: ${(tool === 'erase' ? 'SELL' : (tool || 'SELECT')).toUpperCase()} | Right-drag pan | Hover equipment for info`,
sorting: 'Sorting: use scanner keys. Build after clearing the day.',
allPortsConnected: 'Start condition met: one EGG route reaches an exit.',
equipmentPanelEmpty: 'Hover equipment in Build phase.',
eventPanelRunning: 'Forced events appear in Build phase.'
@ -69,7 +69,6 @@ export function buildToolFlavorText(id) {
eggFarm: 'A tiny gatehouse producing questionable eggs on schedule.',
autoScanner: 'Slower than hands without upgrades.',
manualScanner: 'A manual checkpoint. The operator is the algorithm.',
maintenanceRoom: '2x2 repair base. Spawns repair workers during sorting.',
mixer: 'Male chicks become revenue here. Do not feed it poop.',
trash: 'A polite shredder for poop and other regrets.',
truck: 'Ships the target cargo. Wrong cargo still leaves a paper trail.'

View file

@ -13,7 +13,7 @@ import { createChickSystem } from './systems/chickSystem.js';
import { CARD_DEFS, conveyorSpeedForGame, createCardSystem, debugGrantCard } from './systems/cards.js';
import { floating, shockwave, sparkBurst, updateEffects } from './systems/effects.js';
import { rollContractOffer, activateAcceptedContract, clearActiveContract, resolveContract } from './systems/contracts.js';
import { ensureMaintenanceState, activateRepairmanForDay, deactivateRepairman, conveyorSpeedFactorForKey } from './systems/maintenance.js';
import { ensureMaintenanceState, hireRepairmanForNextDay, activateRepairmanForDay, deactivateRepairman, conveyorSpeedFactorForKey } from './systems/maintenance.js';
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
@ -27,7 +27,7 @@ const ui = {
modal: document.getElementById('modal'), modalTitle: document.getElementById('modalTitle'), modalBody: document.getElementById('modalBody'), modalActions: document.getElementById('modalActions'),
hoverTooltip: document.getElementById('hoverTooltip'),
manualScannerMonitor: document.getElementById('manualScannerMonitor'),
cardInventory: document.getElementById('cardInventory'),
hireRepairmanButton: document.getElementById('hireRepairmanButton'),
debug: {
panel: document.getElementById('debugPanel'),
infiniteCash: document.getElementById('debugInfiniteCash'),
@ -41,7 +41,7 @@ const ui = {
},
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'), boostConveyor: document.getElementById('buildBoostConveyorButton'), eggFarm: document.getElementById('buildEggFarmButton'), autoScanner: document.getElementById('buildAutoScannerButton'), manualScanner: document.getElementById('buildManualScannerButton'), maintenanceRoom: document.getElementById('buildMaintenanceRoomButton'), mixer: document.getElementById('buildMixerButton'), trash: document.getElementById('buildTrashButton'), truck: document.getElementById('buildTruckButton'), erase: document.getElementById('eraseButton'), undo: document.getElementById('undoButton'), redo: document.getElementById('redoButton')
nextTurn: document.getElementById('nextTurnButton'), conveyor: document.getElementById('buildConveyorButton'), boostConveyor: document.getElementById('buildBoostConveyorButton'), 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')
}
};
@ -53,7 +53,6 @@ function initializeStaticText() {
eggFarm: 'eggFarm',
autoScanner: 'autoScanner',
manualScanner: 'manualScanner',
maintenanceRoom: 'maintenanceRoom',
mixer: 'mixer',
trash: 'trash',
truck: 'truck'
@ -170,7 +169,7 @@ function startNextTurn() {
}
ensureMaintenanceState(game);
commitFactoryGraphForDay(game);
const repairActivation = activateRepairmanForDay(game);
activateRepairmanForDay(game);
const zundaBasisProfit = Math.max(0, Math.floor(game.lastResult?.profit ?? game.stats?.profit ?? 0));
const zunda = collectZundaTax(game, zundaBasisProfit);
const loanRepayment = collectLoanRepayments(game);
@ -194,21 +193,12 @@ function startNextTurn() {
game.stats = newTurnStats();
game.stats.zundaTax = 0;
game.stats.loanRepayment = game.lastStartFees?.loanRepayment || 0;
game.stats.repairWorkerWages = repairActivation.wages || 0;
const carriedStartCosts = game.stats.loanRepayment;
game.stats.repairWorkerWages = game.pendingRepairWorkerWages || 0;
const carriedStartCosts = game.stats.loanRepayment + game.stats.repairWorkerWages;
if (carriedStartCosts > 0) {
game.stats.penalty += carriedStartCosts;
game.stats.profit -= carriedStartCosts;
}
if (game.stats.repairWorkerWages > 0) {
game.cash -= game.stats.repairWorkerWages;
game.stats.penalty += game.stats.repairWorkerWages;
game.stats.profit -= game.stats.repairWorkerWages;
game.totals.repairWorkerWages = (game.totals.repairWorkerWages || 0) + game.stats.repairWorkerWages;
game.totals.penalty = (game.totals.penalty || 0) + game.stats.repairWorkerWages;
game.totals.profit = (game.totals.profit || 0) - game.stats.repairWorkerWages;
floating(game, canvas.width / 2 - game.view.x, 176 - game.view.y, `REPAIR WAGES -${yen(game.stats.repairWorkerWages)}`, THEME.danger);
}
game.pendingRepairWorkerWages = 0;
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; }
@ -223,6 +213,18 @@ function startNextTurn() {
}
function hireRepairman() {
ensureMaintenanceState(game);
const result = hireRepairmanForNextDay(game);
if (!result.ok) {
floating(game, canvas.width / 2 - game.view.x, 96 - game.view.y, result.reason || 'Cannot hire repairman.', THEME.danger);
build?.fail?.(result.reason || 'Cannot hire repairman.');
} else {
floating(game, canvas.width / 2 - game.view.x, 96 - game.view.y, `Repairman hired -${yen(result.cost)}`, THEME.green);
}
uiSystem.updatePanels();
uiSystem.updateUI();
}
function completeTurn() {
if (game.phase !== 'running') return;
@ -580,11 +582,10 @@ ui.manualScannerMonitor?.addEventListener('click', event => {
canvas.addEventListener('contextmenu', event => event.preventDefault());
canvas.addEventListener('wheel', zoomAt, { passive: false });
canvas.addEventListener('pointerdown', event => {
if (!['build', 'running'].includes(game.phase)) return;
if (game.phase !== 'build') return;
canvas.setPointerCapture(event.pointerId);
const world = canvasPoint(event);
if (event.button === 2 || event.button === 1) { startPan(event); return; }
if (game.phase !== 'build') return;
if (event.button === 2) { startPan(event); return; }
if (event.button !== 0) return;
if (game.cardTargetPick?.mode === 'autoScannerMenu') { cancelBuildAction(); return; }
if (game.cardTargetPick?.pending) { cardSystem.chooseTargetAtPoint(world); return; }
@ -619,9 +620,9 @@ canvas.addEventListener('pointerdown', event => {
build.startSelectionBox(event);
});
canvas.addEventListener('pointermove', event => {
if (game.pan && ((event.buttons & 2) || (event.buttons & 4))) { updatePan(event); return; }
if (game.phase !== 'build') { hideHoverTooltip(); return; }
updateHover(event);
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' || game.buildTool === 'boostConveyor') && (event.buttons & 1)) build.continueConveyorDrag(pointToCell(canvasPoint(event).x, canvasPoint(event).y));
@ -647,7 +648,8 @@ document.addEventListener('pointerdown', event => {
}, true);
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.boostConveyor.addEventListener('click', () => build.setBuildTool('boostConveyor')); 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.maintenanceRoom?.addEventListener('click', () => build.setBuildTool('maintenanceRoom')); 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.nextTurn.addEventListener('click', startNextTurn); ui.buttons.conveyor.addEventListener('click', () => build.setBuildTool('conveyor')); ui.buttons.boostConveyor.addEventListener('click', () => build.setBuildTool('boostConveyor')); 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.hireRepairmanButton?.addEventListener('click', hireRepairman);
ui.debug.setDay?.addEventListener('click', debugSetDay);
ui.debug.zeroTimer?.addEventListener('click', debugZeroTimer);
ui.debug.grantCard?.addEventListener('click', debugGrantSelectedCard);

View file

@ -308,9 +308,13 @@ function neighborPointsIntoCell(game, p, dirName) {
function branchExitDirs(game, k, dirs, connectedDirs = connectedConveyorDirs(game, k), branchMode = 'random') {
if (connectedDirs.length < 3) return dirs;
if (branchMode && branchMode !== 'random' && connectedDirs.includes(branchMode)) return [branchMode];
const p = parseKey(k);
const entrances = new Set(connectedDirs.filter(dir => neighborPointsIntoCell(game, p, dir)));
if (branchMode && branchMode !== 'random' && connectedDirs.includes(branchMode)) {
const selectedEntrances = new Set(entrances);
selectedEntrances.add(branchMode);
if (connectedDirs.filter(dir => !selectedEntrances.has(dir)).length >= 2) entrances.add(branchMode);
}
const exits = connectedDirs.filter(dir => !entrances.has(dir));
if (exits.length) return exits;
const fallback = dirs.filter(dir => !entrances.has(dir));
@ -321,8 +325,11 @@ function branchSelectableEntranceDirs(game, k, connectedDirs = connectedConveyor
if (connectedDirs.length < 3) return [];
const p = parseKey(k);
const physicalEntrances = new Set(connectedDirs.filter(dir => neighborPointsIntoCell(game, p, dir)));
const exits = connectedDirs.filter(dir => !physicalEntrances.has(dir));
return exits.length >= 2 ? exits : connectedDirs;
return connectedDirs.filter(dir => {
const entrances = new Set(physicalEntrances);
entrances.add(dir);
return connectedDirs.filter(item => !entrances.has(item)).length >= 2;
});
}
function effectiveConveyorDirs(game, k, meta) {
@ -373,12 +380,9 @@ function explicitConveyorMarkerDirs(game, k, meta) {
const neighborNames = DIRS
.filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr)))
.map(d => d.name);
const rawExplicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
.filter(name => DIRS.some(d => d.name === name));
const explicitReachable = rawExplicit.filter(name => neighborNames.includes(name));
const terminalVisual = meta.dir && rawExplicit.includes(meta.dir) ? [meta.dir] : [];
const visualSource = explicitReachable.length ? explicitReachable : (terminalVisual.length ? terminalVisual : effectiveConveyorDirs(game, k, meta));
const dirs = branchExitDirs(game, k, visualSource, neighborNames, meta.branchMode || 'random');
const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
.filter(name => neighborNames.includes(name));
const dirs = branchExitDirs(game, k, explicit.length ? explicit : effectiveConveyorDirs(game, k, meta), neighborNames, meta.branchMode || 'random');
return dirs
.map(name => DIRS.find(d => d.name === name))
.filter(Boolean);
@ -614,28 +618,9 @@ function drawFacilities(ctx, game) {
if (kind === 'mixer') drawMixer(ctx, game, f);
else if (kind === 'trash') drawTrash(ctx, game, f);
else if (kind === 'truck') drawTruck(ctx, game, f);
else if (kind === 'maintenanceRoom') drawMaintenanceRoom(ctx, game, f);
}
for (const f of Object.values(game.facilities || {})) drawFacilityReceiver(ctx, game, f);
}
function drawMaintenanceRoom(ctx, game, room) {
ctx.save();
ctx.fillStyle = '#f7fff5';
ctx.strokeStyle = THEME.ink;
ctx.lineWidth = 4;
rect(ctx, room.x, room.y, room.w, room.h, true, true);
ctx.fillStyle = THEME.greenSoft;
rect(ctx, room.x + 8, room.y + 8, room.w - 16, room.h - 16, true, false);
ctx.fillStyle = THEME.ink;
ctx.font = '900 14px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillText('MAINT', room.x + room.w / 2, room.y + 31);
ctx.font = '900 11px ui-monospace, monospace';
ctx.fillText(`WORKERS ${Math.max(1, Math.floor(room.level || 1))}`, room.x + room.w / 2, room.y + room.h - 18);
drawSelection(ctx, game, 'facility', room.id, room.x + room.w / 2, room.y + room.h / 2, room.w + 10, room.h + 10);
ctx.restore();
}
function drawExternalDuct(ctx, f) {
if (!f?.entry) return;
const c = cellCenter(f.entry.col, f.entry.row);
@ -850,39 +835,37 @@ function drawFloatingTexts(ctx, game) {
}
function drawRepairman(ctx, game) {
const workers = Array.isArray(game.repairmen) && game.repairmen.length ? game.repairmen : (game.repairman ? [game.repairman] : []);
const visibleWorkers = workers.filter(r => r?.active || (game.phase === 'build' && !!r?.hiredForNextDay));
if (!visibleWorkers.length) return;
const r = game.repairman;
const visible = !!r?.active || (game.phase === 'build' && !!r?.hiredForNextDay);
if (!visible) return;
ctx.save();
ctx.lineWidth = 3;
ctx.fillStyle = '#fff0d1';
ctx.strokeStyle = THEME.ink;
ctx.beginPath(); ctx.arc(r.x, r.y, 17, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.fillStyle = '#ffd6e7';
ctx.beginPath(); ctx.arc(r.x - 6, r.y - 2, 3.2, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(r.x + 6, r.y - 2, 3.2, 0, Math.PI * 2); ctx.fill();
const exploited = !!game.cardEffects?.laborExploitation;
for (const r of visibleWorkers) {
ctx.save();
ctx.lineWidth = 3;
ctx.fillStyle = '#fff0d1';
ctx.strokeStyle = THEME.ink;
ctx.beginPath(); ctx.arc(r.x, r.y, 17, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.fillStyle = '#ffd6e7';
ctx.beginPath(); ctx.arc(r.x - 6, r.y - 2, 3.2, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(r.x + 6, r.y - 2, 3.2, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = exploited ? THEME.danger : THEME.ink;
ctx.beginPath(); ctx.arc(r.x - 5, r.y - 5, exploited ? 2.8 : 2.2, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(r.x + 5, r.y - 5, exploited ? 2.8 : 2.2, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = THEME.ink;
ctx.lineWidth = exploited ? 3 : 2;
ctx.beginPath(); ctx.arc(r.x, r.y + (exploited ? 0 : 1), exploited ? 8 : 7, 0.10 * Math.PI, 0.90 * Math.PI); ctx.stroke();
ctx.fillStyle = THEME.green;
rect(ctx, r.x - 9, r.y + 14, 18, 8, true, true);
ctx.strokeStyle = THEME.ink;
ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(r.x - 11, r.y + 18); ctx.lineTo(r.x - 20, r.y + 24); ctx.stroke();
ctx.beginPath(); ctx.moveTo(r.x + 11, r.y + 18); ctx.lineTo(r.x + 20, r.y + 24); ctx.stroke();
ctx.fillStyle = THEME.ink;
ctx.font = '900 11px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillText(r.active ? 'FIX' : 'HIRED', r.x, r.y + 33);
if (r.target?.label) label(ctx, r.x, r.y - 24, r.target.label, THEME.green);
else if (!r.active && r.hiredForNextDay) label(ctx, r.x, r.y - 24, 'NEXT DAY', THEME.green);
ctx.restore();
}
ctx.fillStyle = exploited ? THEME.danger : THEME.ink;
ctx.beginPath(); ctx.arc(r.x - 5, r.y - 5, exploited ? 2.8 : 2.2, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(r.x + 5, r.y - 5, exploited ? 2.8 : 2.2, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = THEME.ink;
ctx.lineWidth = exploited ? 3 : 2;
ctx.beginPath(); ctx.arc(r.x, r.y + (exploited ? 0 : 1), exploited ? 8 : 7, 0.10 * Math.PI, 0.90 * Math.PI); ctx.stroke();
ctx.fillStyle = THEME.green;
rect(ctx, r.x - 9, r.y + 14, 18, 8, true, true);
ctx.strokeStyle = THEME.ink;
ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(r.x - 11, r.y + 18); ctx.lineTo(r.x - 20, r.y + 24); ctx.stroke();
ctx.beginPath(); ctx.moveTo(r.x + 11, r.y + 18); ctx.lineTo(r.x + 20, r.y + 24); ctx.stroke();
ctx.fillStyle = THEME.ink;
ctx.font = '900 11px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.fillText(r.active ? 'FIX' : 'HIRED', r.x, r.y + 33);
if (r.target?.label) label(ctx, r.x, r.y - 24, r.target.label, THEME.green);
else if (!r.active && r.hiredForNextDay) label(ctx, r.x, r.y - 24, 'NEXT DAY', THEME.green);
ctx.restore();
}
function drawCanvasHints(ctx, game, canvas) {

View file

@ -1,7 +1,7 @@
import { BALANCE } from '../core/balance.js';
import { DIRS, MACHINE_FACILITY_IDS, GRID, THEME } from '../core/config.js';
import { getSpawnRange } from '../core/state.js';
import { createEggFarm, createScanner, createFacility, createMaintenanceRoom } from '../core/entities.js';
import { createEggFarm, createScanner, createFacility } from '../core/entities.js';
import { generateBlockedCellsInRect } from '../core/mapGen.js';
import { expansionCost, expansionLot, expansionLots, lotFromChunk, lotBounds, lotContainsPoint, cellsInLot, isOwnedCell, countOwnedCells, shiftRowIndexedSet, shiftRowIndexedMap, ownChunk } from '../core/gridExpansion.js';
import { key, parseKey, pointToCell, cellCenter, yen, directionNameBetweenCells } from '../core/utils.js';
@ -12,7 +12,7 @@ import { autoScannerCooldownSeconds } from './cards.js';
import { record } from './history.js';
import { floating, eraseEffect } from './effects.js';
import { createSelectionSystem } from './selectionSystem.js';
import { ensureMaintenanceState, remainingPercent, performanceFactor, autoScannerDelayMultiplier, eggSpawnDelayMultiplier, facilityProcessingDelay, durabilityCapFor, repairmanDailyCost } from './maintenance.js';
import { ensureMaintenanceState, remainingPercent, performanceFactor, autoScannerDelayMultiplier, eggSpawnDelayMultiplier, facilityProcessingDelay, durabilityCapFor } from './maintenance.js';
export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanels }) {
function updatePanels() { if (onUpdatePanels) onUpdatePanels(); }
@ -45,10 +45,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
for (const farm of game.eggFarms || []) farm.row += rowDelta;
for (const scanner of game.scanners || []) scanner.row += rowDelta;
for (const facility of Object.values(game.facilities || {})) {
if (isMaintenanceRoom(facility)) {
facility.row += rowDelta;
syncMaintenanceRoomRect(facility);
}
if (facility.entry) facility.entry.row += rowDelta;
}
}
@ -125,33 +121,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
return Object.values(game.facilities).some(f => !ignoreIds.has(f.id) && rectsOverlap(rect, rectOfFacility(f), 12));
}
function facilityBaseId(f) { return f?.baseId || f?.id || ''; }
function isMaintenanceRoom(f) { return facilityBaseId(f) === 'maintenanceRoom'; }
function syncMaintenanceRoomRect(room) {
if (!room) return;
room.cols = room.cols || 2;
room.rows = room.rows || 2;
room.x = GRID.x + room.col * GRID.cell;
room.y = GRID.y + room.row * GRID.cell;
room.w = room.cols * GRID.cell;
room.h = room.rows * GRID.cell;
}
function maintenanceRoomCells(col, row, cols = 2, rows = 2) {
const cells = [];
for (let r = 0; r < rows; r += 1) for (let c = 0; c < cols; c += 1) cells.push({ col: col + c, row: row + r });
return cells;
}
function maintenanceRoomAtCell(col, row, ignoreIds = new Set()) {
return Object.values(game.facilities || {}).find(f => {
if (!isMaintenanceRoom(f) || ignoreIds.has(f.id)) return false;
const cols = f.cols || 2, rows = f.rows || 2;
return col >= f.col && col < f.col + cols && row >= f.row && row < f.row + rows;
}) || null;
}
function facilityEntryAtCell(col, row, ignoreIds = new Set()) {
return Object.values(game.facilities || {}).find(f => f.entry && !ignoreIds.has(f.id) && f.entry.col === col && f.entry.row === row) || null;
}
function isEquipmentCell(col, row, moving = null) {
if (!pointInGrid(col, row)) return false;
if (isBlockedCell(col, row)) return true;
@ -161,8 +130,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
const existingScanner = scannerAt(game, col, row);
if (existingScanner && !(moving?.type === 'scanner' && moving.ref?.id === existingScanner.id)) return true;
if (game.conveyorTiles.has(k) && !(moving?.type === 'conveyor' && moving.oldKey === k)) return true;
const ignore = moving?.type === 'facility' && moving.ref?.id ? new Set([moving.ref.id]) : new Set();
if (maintenanceRoomAtCell(col, row, ignore)) return true;
return false;
}
@ -176,7 +143,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
const existingScanner = scannerAt(game, cell.col, cell.row);
if (existingScanner && !(moving?.type === 'scanner' && moving.ref?.id === existingScanner.id)) return true;
if (game.conveyorTiles.has(k)) return true;
if (maintenanceRoomAtCell(cell.col, cell.row) || facilityEntryAtCell(cell.col, cell.row)) return true;
}
return false;
}
@ -305,8 +271,11 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
const connected = branchExitDirsForKey(k);
if (connected.length < 3) return [];
const physicalEntrances = new Set(connected.filter(dir => neighborPointsIntoCell(cell, dir)));
const exits = connected.filter(dir => !physicalEntrances.has(dir));
return exits.length >= 2 ? exits : connected;
return connected.filter(dir => {
const entrances = new Set(physicalEntrances);
entrances.add(dir);
return connected.filter(item => !entrances.has(item)).length >= 2;
});
}
function markConveyorDirection(from, to) {
@ -326,8 +295,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
}
function cycleSingleConveyorDirection(cell) {
const neighbors = conveyorNeighborCells(cell);
if (neighbors.length <= 1) return cycleIsolatedConveyorDirection(cell);
const path = conveyorRunPathFrom(cell);
if (path.length < 2) return cycleIsolatedConveyorDirection(cell);
record(game);
@ -415,17 +382,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
meta.outDirs = [dir];
meta.branchMode = 'random';
}
if (target.length >= 2) {
const last = target[target.length - 1];
const prev = target[target.length - 2];
const meta = game.conveyorMeta.get(key(last.col, last.row));
const dir = directionNameBetweenCells(prev, last);
if (meta && dir && !isBranchCell(last)) {
meta.dir = dir;
meta.outDirs = [dir];
meta.branchMode = 'random';
}
}
}
function conveyorToolDef() {
@ -438,7 +394,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
const k = key(col, row);
if (game.conveyorTiles.has(k)) return { ok: false, exists: true };
if (isBlockedCell(col, row)) return { ok: false, reason: 'Cannot build on blocked ground.' };
if (farmAt(game, col, row) || scannerAt(game, col, row) || maintenanceRoomAtCell(col, row)) return { ok: false, reason: TEXT.fail.cellOccupied };
if (farmAt(game, col, row) || scannerAt(game, col, row)) return { ok: false, reason: TEXT.fail.cellOccupied };
const toolDef = conveyorToolDef();
const kind = toolDef.id || 'conveyor';
const cost = buildPrice(kind, game);
@ -536,34 +492,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
conveyorDragRecordedDirectionEdit = false;
}
function maintenanceRoomPlacementBlocked(col, row, moving = null) {
const ignoreIds = moving?.type === 'facility' && moving.ref?.id ? new Set([moving.ref.id]) : new Set();
for (const cell of maintenanceRoomCells(col, row)) {
if (!pointInGrid(cell.col, cell.row) || isBlockedCell(cell.col, cell.row)) return true;
if (farmAt(game, cell.col, cell.row) || scannerAt(game, cell.col, cell.row)) return true;
if (game.conveyorTiles.has(key(cell.col, cell.row))) return true;
if (maintenanceRoomAtCell(cell.col, cell.row, ignoreIds)) return true;
if (facilityEntryAtCell(cell.col, cell.row, ignoreIds)) return true;
}
return false;
}
function buildMaintenanceRoom(col, row) {
if (maintenanceRoomPlacementBlocked(col, row)) return fail(TEXT.fail.cellOccupied);
const cost = buildPrice('maintenanceRoom', game);
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
record(game);
spendCash(game, cost);
const room = applyBuildQuality(createMaintenanceRoom(game, col, row, cost));
const id = game.facilities.maintenanceRoom ? `maintenanceRoom:${game.nextId++}` : 'maintenanceRoom';
room.id = id;
room.baseId = 'maintenanceRoom';
game.facilities[id] = room;
game.selected = { type: 'facility', id };
floating(game, room.x + room.w / 2, room.y + room.h / 2, `-${yen(cost)}`, THEME.ink);
updatePanels();
}
function buildAtCell(cell) {
if (!cell) return fail(TEXT.fail.outOfGrid);
const { col, row } = cell;
@ -573,7 +501,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
refreshRoutingAfterEdit(game);
return;
}
if (game.buildTool === 'maintenanceRoom') return buildMaintenanceRoom(col, row);
if (isBlockedCell(col, row)) return fail('Cannot build on blocked ground.');
if ((game.buildTool === 'manualScanner' || game.buildTool === 'autoScanner') ? scannerPlacementBlocked(col, row) : isEquipmentCell(col, row)) return fail(TEXT.fail.cellOccupied);
if (game.buildTool === 'eggFarm') buildFarm(col, row);
@ -583,7 +510,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
function buildAtPoint(p) {
const cell = pointToCell(p.x, p.y);
if (['conveyor', 'boostConveyor', 'eggFarm', 'manualScanner', 'autoScanner', 'maintenanceRoom'].includes(game.buildTool)) return buildAtCell(cell);
if (['conveyor', 'boostConveyor', 'eggFarm', 'manualScanner', 'autoScanner'].includes(game.buildTool)) return buildAtCell(cell);
if (MACHINE_FACILITY_IDS.includes(game.buildTool)) return buildFacility(p, game.buildTool);
}
@ -1001,15 +928,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
} else if (obj.type === 'facility') {
lines.push(`Price: ${yen(equipmentPrice(obj))}`);
{ const reason = lastProtectedSaleReason({ type: obj.type, ref: obj }); lines.push(reason ? `Sale: blocked (${reason})` : `Sale value: ${yen(resaleValueFor({ type: obj.type, ref: obj }, game).amount)}`); }
if (facilityKind === 'maintenanceRoom') {
const workers = Math.max(1, Math.floor(obj.level || 1));
lines.push(`Workers: ${workers}`);
lines.push(`Wage: ${yen(repairmanDailyCost(game))} / worker / day`);
lines.push(`Daily cost: ${yen(workers * repairmanDailyCost(game))}`);
lines.push(`Footprint: ${obj.cols || 2}x${obj.rows || 2} cells`);
return lines;
}
lines.push(['mixer', 'truck', 'trash'].includes(facilityKind) ? `Level: ${obj.level} / no cap` : `Level: ${obj.level}`);
lines.push(['mixer', 'truck', 'trash'].includes(obj.id) ? `Level: ${obj.level} / no cap` : `Level: ${obj.level}`);
if (facilityKind === 'truck') lines.push('Durability: none | Always normal');
if (['mixer', 'trash'].includes(facilityKind)) lines.push(`Durability: ${remainingPercent(obj)}% | Extra delay ${facilityProcessingDelay(game, obj).toFixed(2)}s`);
if (facilityKind === 'mixer') {
@ -1047,7 +966,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
if (obj.type === 'eggFarm') return 'A tiny gatehouse producing questionable eggs on schedule.';
if (obj.type === 'scanner' && obj.kind === 'auto') return 'Slower than hands without upgrades.';
if (obj.type === 'scanner') return 'A manual checkpoint. The operator is the algorithm.';
if (obj.type === 'facility' && (obj.baseId || obj.id) === 'maintenanceRoom') return 'Spawns repair workers at day start. Upgrade cards add workers.';
if (obj.type === 'facility' && (obj.baseId || obj.id) === 'mixer') return 'Male chicks become revenue here. Do not feed it poop.';
if (obj.type === 'facility' && (obj.baseId || obj.id) === 'trash') return 'Poop goes in. Sometimes coins come out, for reasons best left unaudited.';
if (obj.type === 'facility' && (obj.baseId || obj.id) === 'truck') return 'The shipping endpoint. Correct cargo pays; wrong cargo complains.';

View file

@ -3,7 +3,7 @@ import { nextSpawnDelay } from '../core/state.js';
import { cellCenter, key, yen } from '../core/utils.js';
import { applyPenalty, applyRevenue, upgradedMixerPrice, upgradedTruckPrice, shredderUpgradeCount, rawShredderUpgradeCount, shredderBonusChance, shredderBonusMaxCards } from './economy.js';
import { floating, shake, eraseEffect, shockwave, sparkBurst, smokeBurst } from './effects.js';
import { averageConveyorPerformance, autoScannerDelayMultiplier, ensureMaintenanceState, maintenanceRoomWorkerCount, repairAllEquipment, repairmanDailyCost } from './maintenance.js';
import { averageConveyorPerformance, autoScannerDelayMultiplier, ensureMaintenanceState, repairAllEquipment } from './maintenance.js';
import { scannerCenter, refreshRoutingAfterEdit } from './routing.js';
const COMMON_WEIGHT = CARD_BALANCE.commonWeight;
@ -118,10 +118,6 @@ function targetLabel(game, target) {
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`;
}
if (target.type === 'facility' && (target.baseId || target.id) === 'maintenanceRoom') {
const before = maintenanceRoomWorkerCount(target);
return `MAINT ROOM workers ${before} -> ${before + 1} | wage ${yen(repairmanDailyCost(game))}/worker/day`;
}
if (target.type === 'facility' && (target.baseId || target.id) === 'mixer') {
const beforeLevel = target.level;
const before = upgradedMixerPrice(game);
@ -165,7 +161,6 @@ export function targetsForCard(game, cardOrId) {
.filter(f => Math.max(0, Number(f.extraEggOutlet) || 0) < 3)
.map(f => ({ type: 'eggOutlet', id: f.id, col: f.col, row: f.row, farm: f }));
if (card.target === 'autoScanner') return game.scanners.filter(s => s.kind === 'auto');
if (card.target === 'maintenanceRoom') return Object.values(game.facilities || {}).filter(f => (f.baseId || f.id) === 'maintenanceRoom');
if (card.target === 'mixer') return Object.values(game.facilities || {}).filter(f => (f.baseId || f.id) === 'mixer');
if (card.target === 'truck') return Object.values(game.facilities || {}).filter(f => (f.baseId || f.id) === 'truck');
if (card.target === 'trash') return rawShredderUpgradeCount(game) < shredderBonusMaxCards()
@ -191,8 +186,7 @@ function cardAtCap(game, card) {
function repairmanCardVisible(game) {
const r = game.repairman || {};
const hasMaintenanceRoom = Object.values(game.facilities || {}).some(f => (f.baseId || f.id) === 'maintenanceRoom');
return !!(hasMaintenanceRoom || r.active || r.hiredForNextDay || game.pendingRepairWorkerWages > 0);
return !!(r.active || r.hiredForNextDay || game.pendingRepairWorkerWages > 0);
}
function availableCards(game) {
@ -436,11 +430,6 @@ function cardDescription(game, card) {
const after = autoScannerCooldownSeconds({ ...scanner, level: (scanner.level || 1) + 1 }, game);
return `Choose one AUTO SCANNER. Level +1. Cooldown ${before.toFixed(2)}s -> ${after.toFixed(2)}s.`;
}
if (card.id === 'upgradeMaintenanceRoom') {
const room = targetsForCard(game, card)[0] || { level: 1 };
const workers = maintenanceRoomWorkerCount(room);
return `Choose one MAINTENANCE ROOM. Repair workers ${workers} -> ${workers + 1}. Wage ${yen(repairmanDailyCost(game))} per worker per day.`;
}
if (card.id === 'upgradeMixer') {
const preview = incomeUpgradePreview(game, 'mixer', ECONOMY.income.mixer);
return `Choose one MIXER. Earning +10%. Male-chick income ${yen(preview.before)} -> ${yen(preview.after)}.`;

View file

@ -248,8 +248,12 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
function branchExitDirNames(cell, dirs, branchMode = 'random') {
const connected = conveyorNeighborDirNames(cell);
if (connected.length < 3) return dirs;
if (branchMode && branchMode !== 'random' && branchMode !== 'pass' && connected.includes(branchMode)) return [branchMode];
const entrances = new Set(connected.filter(dir => neighborPointsIntoCell(cell, dir)));
if (branchMode && branchMode !== 'random' && branchMode !== 'pass' && connected.includes(branchMode)) {
const selectedEntrances = new Set(entrances);
selectedEntrances.add(branchMode);
if (connected.filter(dir => !selectedEntrances.has(dir)).length >= 2) entrances.add(branchMode);
}
const exits = connected.filter(dir => !entrances.has(dir));
if (exits.length) return exits;
const fallback = dirs.filter(dir => !entrances.has(dir));

View file

@ -24,7 +24,7 @@ export function equipmentBasePrice(objOrHit) {
if (type === 'conveyor') return obj.price || FACILITY_DEFS[obj.kind]?.price || FACILITY_DEFS.conveyor.price;
if (type === 'eggFarm') return obj.price || FACILITY_DEFS.eggFarm.price;
if (type === 'scanner') return obj.price || (obj.kind === 'auto' ? FACILITY_DEFS.autoScanner.price : FACILITY_DEFS.manualScanner.price);
if (type === 'facility') return obj.price || FACILITY_DEFS[obj.baseId || obj.id]?.price || 0;
if (type === 'facility') return obj.price || FACILITY_DEFS[obj.id]?.price || 0;
return FACILITY_DEFS[obj.id]?.price || 0;
}

View file

@ -1,12 +1,10 @@
import { BALANCE } from '../core/balance.js';
import { GRID, THEME } from '../core/config.js';
import { cellCenter, parseKey, key, yen, pointToCell, inGrid } from '../core/utils.js';
import { isOwnedCell } from '../core/gridExpansion.js';
import { cellCenter, parseKey, key, yen } from '../core/utils.js';
import { spendCash } from './economy.js';
import { floating } from './effects.js';
const RULES = BALANCE.maintenance;
const REPAIR_INTEREST_WEAR_THRESHOLD = 0.20;
function effectCount(game, id) {
return Math.max(0, Number(game?.cardEffects?.[id]) || 0);
@ -32,44 +30,6 @@ export function repairmanDailyCost(game) {
return game?.cardEffects?.laborExploitation ? Math.ceil(base * 0.7) : base;
}
function facilityKind(target) {
return target?.baseId || target?.id || target;
}
export function maintenanceRooms(game) {
return Object.values(game?.facilities || {}).filter(f => facilityKind(f) === 'maintenanceRoom');
}
export function maintenanceRoomWorkerCount(room) {
return Math.max(1, Math.floor(Number(room?.level) || 1));
}
export function maintenanceWorkerCount(game) {
return maintenanceRooms(game).reduce((sum, room) => sum + maintenanceRoomWorkerCount(room), 0);
}
export function repairWorkerWageForDay(game) {
return maintenanceWorkerCount(game) * repairmanDailyCost(game);
}
function roomSpawnPoint(room, index = 0, total = 1) {
const cols = Math.max(1, Math.ceil(Math.sqrt(total)));
const row = Math.floor(index / cols);
const col = index % cols;
const spacing = 12;
return {
x: room.x + room.w / 2 + (col - (cols - 1) / 2) * spacing,
y: room.y + room.h / 2 + (row - Math.floor((total - 1) / cols) / 2) * spacing
};
}
function defaultRepairmanPosition() {
return {
x: GRID.x + GRID.cell * 0.5,
y: GRID.y + GRID.rows * GRID.cell + 84
};
}
function qualityMultiplierOf(objOrMeta) {
return Math.max(0.1, Number(objOrMeta?.durabilityBaseMultiplier) || 1);
}
@ -79,12 +39,12 @@ export function ensureMaintenanceState(game) {
game.repairman = {
hiredForNextDay: false,
active: false,
...defaultRepairmanPosition(),
x: GRID.x + GRID.cell * 0.5,
y: GRID.y + GRID.rows * GRID.cell + 84,
target: null,
repairedToday: 0
};
}
if (!Array.isArray(game.repairmen)) game.repairmen = [];
for (const [k, meta] of game.conveyorMeta || []) {
if (meta.uses == null) meta.uses = 0;
if (meta.maintenanceType == null) meta.maintenanceType = 'conveyor';
@ -99,9 +59,8 @@ export function ensureMaintenanceState(game) {
if (scanner.kind === 'auto') attachMaintenance(scanner, 'autoScanner', game);
}
for (const f of Object.values(game.facilities || {})) {
const id = facilityKind(f);
if (id === 'mixer') attachMaintenance(f, 'mixer', game);
if (id === 'trash') attachMaintenance(f, 'trash', game);
if (f.id === 'mixer') attachMaintenance(f, 'mixer', game);
if (f.id === 'trash') attachMaintenance(f, 'trash', game);
}
}
@ -182,6 +141,10 @@ export function eggSpawnDelayMultiplier(farm) {
return delayMultiplier(farm);
}
function facilityKind(target) {
return target?.baseId || target?.id || target;
}
function facilityFromTarget(game, target) {
if (!target) return null;
if (typeof target === 'object') return target;
@ -250,16 +213,14 @@ export function equipmentMaintenanceTargets(game) {
for (const [k, meta] of game.conveyorMeta || []) targets.push({ type: 'conveyor', key: k, meta, label: `${meta.kind === 'boostConveyor' ? 'Boost Belt' : 'Belt'} ${k}`, center: cellCenter(parseKey(k).col, parseKey(k).row) });
for (const farm of game.eggFarms || []) targets.push({ type: 'eggFarm', ref: farm, label: `EGG #${farm.id}`, center: cellCenter(farm.col, farm.row) });
for (const scanner of (game.scanners || []).filter(s => s.kind === 'auto')) targets.push({ type: 'autoScanner', ref: scanner, label: `AUTO #${scanner.id}`, center: cellCenter(scanner.col, scanner.row) });
for (const f of Object.values(game.facilities || {})) {
const id = facilityKind(f);
if (!['mixer', 'trash'].includes(id)) continue;
targets.push({ type: id, ref: f, label: f.shortName || f.name || id, center: { x: f.x + f.w / 2, y: f.y + f.h / 2 } });
for (const id of ['mixer', 'trash']) {
const f = game.facilities?.[id];
if (f) targets.push({ type: id, ref: f, label: f.shortName || f.name || id, center: { x: f.x + f.w / 2, y: f.y + f.h / 2 } });
}
return targets;
}
function targetWear(t) { return t.meta ? wearRatio(t) : wearRatio(t.ref); }
function targetNeedsRepair(t) { return targetWear(t) > REPAIR_INTEREST_WEAR_THRESHOLD; }
function targetUses(t) { return t.meta ? usesOf(t) : usesOf(t.ref); }
function targetDurability(t) { return t.meta ? durabilityOf(t) : durabilityOf(t.ref); }
function reduceTargetUses(t, amount) { setUses(t.meta ? t : t.ref, Math.max(0, targetUses(t) - amount)); }
@ -291,36 +252,14 @@ export function hireRepairmanForNextDay(game) {
export function activateRepairmanForDay(game) {
ensureMaintenanceState(game);
const rooms = maintenanceRooms(game);
const workers = [];
for (const room of rooms) {
const count = maintenanceRoomWorkerCount(room);
for (let i = 0; i < count; i += 1) {
const p = roomSpawnPoint(room, i, count);
workers.push({
id: `${room.id}:worker:${i}`,
homeRoomId: room.id,
hiredForNextDay: false,
active: true,
x: p.x,
y: p.y,
target: null,
repairedToday: 0
});
}
}
game.repairmen = workers;
const legacy = game.repairman || { ...defaultRepairmanPosition() };
game.repairman = workers[0] || { ...legacy, active: false, hiredForNextDay: false, target: null, repairedToday: 0 };
return { count: workers.length, wages: workers.length * repairmanDailyCost(game) };
game.repairman.active = !!game.repairman.hiredForNextDay;
game.repairman.hiredForNextDay = false;
game.repairman.target = null;
game.repairman.repairedToday = 0;
}
export function deactivateRepairman(game) {
ensureMaintenanceState(game);
for (const r of game.repairmen || []) {
r.active = false;
r.target = null;
}
game.repairman.active = false;
game.repairman.target = null;
}
@ -331,114 +270,29 @@ function resolveStoredTarget(game, stored) {
return targets.find(t => targetKey(t) === stored.key) || null;
}
function repairWalkableCell(game, col, row) {
return inGrid(col, row) && isOwnedCell(game, col, row) && !game.blockedCells?.has?.(key(col, row));
}
function nearestRepairWalkableCell(game, point) {
const direct = pointToCell(point.x, point.y);
if (direct && repairWalkableCell(game, direct.col, direct.row)) return direct;
let best = null;
let bestDist = Infinity;
for (let row = 0; row < GRID.rows; row += 1) {
for (let col = 0; col < GRID.cols; col += 1) {
if (!repairWalkableCell(game, col, row)) continue;
const c = cellCenter(col, row);
const dist = Math.hypot(c.x - point.x, c.y - point.y);
if (dist < bestDist) {
bestDist = dist;
best = { col, row };
}
}
}
return best;
}
function repairApproachPoint(_game, target) {
const ref = target?.ref || null;
if (ref?.entry && Number.isFinite(ref.entry.col) && Number.isFinite(ref.entry.row)) return cellCenter(ref.entry.col, ref.entry.row);
return target?.center || null;
}
function repairCellPath(game, fromPoint, toPoint) {
const start = nearestRepairWalkableCell(game, fromPoint);
const goal = nearestRepairWalkableCell(game, toPoint);
if (!start || !goal) return null;
const startKey = key(start.col, start.row);
const goalKey = key(goal.col, goal.row);
const queue = [start];
const cameFrom = new Map([[startKey, null]]);
for (let qi = 0; qi < queue.length; qi += 1) {
const cur = queue[qi];
const ck = key(cur.col, cur.row);
if (ck === goalKey) break;
for (const d of [{ dc: 1, dr: 0 }, { dc: -1, dr: 0 }, { dc: 0, dr: 1 }, { dc: 0, dr: -1 }]) {
const next = { col: cur.col + d.dc, row: cur.row + d.dr };
if (!repairWalkableCell(game, next.col, next.row)) continue;
const nk = key(next.col, next.row);
if (cameFrom.has(nk)) continue;
cameFrom.set(nk, ck);
queue.push(next);
}
}
if (!cameFrom.has(goalKey)) return null;
const cells = [];
let curKey = goalKey;
while (curKey) {
const p = parseKey(curKey);
cells.push(p);
curKey = cameFrom.get(curKey);
}
cells.reverse();
return cells;
}
function repairWaypoints(game, r, target) {
const approach = repairApproachPoint(game, target);
if (!approach) return null;
const cellPath = repairCellPath(game, { x: r.x, y: r.y }, approach);
if (!cellPath) return null;
const waypoints = cellPath.slice(1).map(c => cellCenter(c.col, c.row));
const last = waypoints[waypoints.length - 1] || cellCenter(cellPath[0].col, cellPath[0].row);
if (Math.hypot(last.x - approach.x, last.y - approach.y) > 2) waypoints.push(approach);
if (!waypoints.length) waypoints.push(approach);
return waypoints;
}
function canRepairmanReachTarget(game, r, target) {
return !!repairWaypoints(game, r || defaultRepairmanPosition(), target);
}
function chooseRepairTarget(game, r) {
function chooseRepairTarget(game) {
let best = null;
for (const t of equipmentMaintenanceTargets(game)) {
const w = targetWear(t);
if (!targetNeedsRepair(t)) continue;
if (r && !canRepairmanReachTarget(game, r, t)) continue;
if (w <= 0.001) continue;
if (!best || w > targetWear(best)) best = t;
}
return best;
}
function updateSingleRepairman(game, r, dt) {
export function updateRepairman(game, dt) {
ensureMaintenanceState(game);
const r = game.repairman;
if (!r.active || game.phase !== 'running') return;
let target = resolveStoredTarget(game, r.target);
if (target && (!targetNeedsRepair(target) || !canRepairmanReachTarget(game, r, target))) target = null;
target = target || chooseRepairTarget(game, r);
let target = resolveStoredTarget(game, r.target) || chooseRepairTarget(game);
if (!target) {
r.target = null;
return;
}
r.target = { key: targetKey(target), label: target.label };
const waypoints = repairWaypoints(game, r, target);
if (!waypoints?.length) {
r.target = null;
return;
}
const waypoint = waypoints[0];
const speed = RULES.repairman.walkSpeed;
const dx = waypoint.x - r.x;
const dy = waypoint.y - r.y;
const dx = target.center.x - r.x;
const dy = target.center.y - r.y;
const dist = Math.hypot(dx, dy);
if (dist > 8) {
const step = Math.min(dist, speed * dt);
@ -446,23 +300,14 @@ function updateSingleRepairman(game, r, dt) {
r.y += dy / dist * step;
return;
}
if (waypoints.length > 1 || Math.hypot((repairApproachPoint(game, target)?.x || waypoint.x) - r.x, (repairApproachPoint(game, target)?.y || waypoint.y) - r.y) > 10) return;
const repairWear = dt / (RULES.repairman.secondsPerOnePercent * 100);
const usesBefore = targetUses(target);
const maxAllowedUses = targetDurability(target) * REPAIR_INTEREST_WEAR_THRESHOLD;
const usesDelta = repairWear * targetDurability(target);
setUses(target.meta ? target : target.ref, Math.max(maxAllowedUses, targetUses(target) - usesDelta));
reduceTargetUses(target, usesDelta);
const repairedPct = Math.max(0, (usesBefore - targetUses(target)) / targetDurability(target) * 100);
r.repairedToday = (r.repairedToday || 0) + repairedPct;
if (repairedPct > 0 && Math.random() < 0.02) floating(game, r.x, r.y - 28, 'REPAIR', THEME.green);
if (!targetNeedsRepair(target)) r.target = null;
}
export function updateRepairman(game, dt) {
ensureMaintenanceState(game);
const workers = Array.isArray(game.repairmen) && game.repairmen.length ? game.repairmen : (game.repairman ? [game.repairman] : []);
for (const r of workers) updateSingleRepairman(game, r, dt);
if (workers.length) game.repairman = workers[0];
if (targetWear(target) <= 0.001) r.target = null;
}
function worstDegradation(game) {
@ -478,12 +323,5 @@ function worstDegradation(game) {
export function maintenanceSummary(game) {
const worst = worstDegradation(game);
const avgBeltPerformance = averageConveyorPerformance(game);
return {
worst,
avgBeltPerformance,
repairman: game.repairman || null,
repairmen: Array.isArray(game.repairmen) ? game.repairmen : [],
workers: maintenanceWorkerCount(game),
dailyWage: repairWorkerWageForDay(game)
};
return { worst, avgBeltPerformance, repairman: game.repairman || null };
}

View file

@ -141,8 +141,12 @@ function inferredConveyorOutDirNames(game, cellKey) {
const inferred = neighbors;
const dirs = explicit.length ? explicit : inferred;
if (neighbors.length < 3) return dirs;
if (meta.branchMode && meta.branchMode !== 'random' && neighbors.includes(meta.branchMode)) return [meta.branchMode];
const entrances = new Set(neighbors.filter(dir => neighborPointsIntoCell(game, p, dir)));
if (meta.branchMode && meta.branchMode !== 'random' && neighbors.includes(meta.branchMode)) {
const selectedEntrances = new Set(entrances);
selectedEntrances.add(meta.branchMode);
if (neighbors.filter(dir => !selectedEntrances.has(dir)).length >= 2) entrances.add(meta.branchMode);
}
const exits = neighbors.filter(dir => !entrances.has(dir));
if (exits.length) return exits;
const fallback = dirs.filter(dir => !entrances.has(dir));

View file

@ -13,25 +13,6 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
return `${item.type}:${item.id || item.ref?.id}`;
}
function facilityKind(f) { return f?.baseId || f?.id || ''; }
function isMaintenanceRoom(f) { return facilityKind(f) === 'maintenanceRoom'; }
function maintenanceRoomFootprint(room, col = room?.col, row = room?.row) {
const cells = [];
const cols = room?.cols || 2;
const rows = room?.rows || 2;
for (let r = 0; r < rows; r += 1) for (let c = 0; c < cols; c += 1) cells.push({ col: col + c, row: row + r });
return cells;
}
function syncMaintenanceRoomRect(room) {
if (!room) return;
room.cols = room.cols || 2;
room.rows = room.rows || 2;
room.x = GRID.x + room.col * GRID.cell;
room.y = GRID.y + room.row * GRID.cell;
room.w = room.cols * GRID.cell;
room.h = room.rows * GRID.cell;
}
function selectionFromHit(hit) {
if (!hit) return null;
if (hit.type === 'conveyor') return { type: 'conveyor', id: hit.oldKey };
@ -137,10 +118,7 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
return { ...sel, obj, oldKey: sel.id, currentKey: sel.id, meta: game.conveyorMeta.get(sel.id) || { price: buildPrice('conveyor'), builtSession: null }, col: cell.col, row: cell.row };
}
if (sel.type === 'eggFarm' || sel.type === 'scanner') return { ...sel, obj, col: obj.col, row: obj.row };
if (sel.type === 'facility') {
if (isMaintenanceRoom(obj)) return { ...sel, obj, col: obj.col, row: obj.row, cols: obj.cols || 2, rows: obj.rows || 2, maintenanceRoom: true, center: { x: obj.x + obj.w / 2, y: obj.y + obj.h / 2 } };
return { ...sel, obj, x: obj.x, y: obj.y, w: obj.w, h: obj.h, entry: obj.entry ? { ...obj.entry } : null, side: obj.side, center: { x: obj.x + obj.w / 2, y: obj.y + obj.h / 2 } };
}
if (sel.type === 'facility') return { ...sel, obj, x: obj.x, y: obj.y, w: obj.w, h: obj.h, entry: obj.entry ? { ...obj.entry } : null, side: obj.side, center: { x: obj.x + obj.w / 2, y: obj.y + obj.h / 2 } };
return null;
}
@ -172,17 +150,13 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
const scanner = scannerAt(game, col, row);
if (scanner && !selectedTokens.has(`scanner:${scanner.id}`)) return true;
if (game.conveyorTiles.has(k) && !selectedTokens.has(`conveyor:${k}`)) return true;
for (const f of Object.values(game.facilities || {})) {
if (!isMaintenanceRoom(f) || selectedTokens.has(`facility:${f.id}`)) continue;
if (maintenanceRoomFootprint(f).some(cell => cell.col === col && cell.row === row)) return true;
}
return false;
}
function facilityDragWouldOverlap(dx, dy, selectedTokens) {
const candidateRects = [];
for (const origin of game.groupDrag.origins) {
if (origin.type !== 'facility' || origin.maintenanceRoom) continue;
if (origin.type !== 'facility') continue;
const nextCenter = { x: origin.center.x + dx, y: origin.center.y + dy };
const { entry, side } = nearestGridEdge(nextCenter);
if (!pointInGrid(entry.col, entry.row)) return true;
@ -216,16 +190,15 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
const selectedTokens = new Set(game.groupDrag.selections.map(selectionToken));
const targetCells = new Set();
for (const origin of game.groupDrag.origins) {
if (origin.type === 'facility' && !origin.maintenanceRoom) continue;
if (origin.type === 'facility') continue;
const col = origin.col + dcol, row = origin.row + drow;
const cells = origin.maintenanceRoom ? maintenanceRoomFootprint(origin.obj, col, row) : (origin.type === 'scanner' ? scannerFootprintCells({ col, row }) : [{ col, row }]);
const cells = origin.type === 'scanner' ? scannerFootprintCells({ col, row }) : [{ col, row }];
for (const cell of cells) {
if (!pointInGrid(cell.col, cell.row)) return fail('Selection outside grid');
if (game.blockedCells?.has?.(key(cell.col, cell.row))) return fail('Selection hits blocked ground');
const tk = key(cell.col, cell.row);
if (targetCells.has(tk)) return fail('Selection overlap');
if (cellOccupiedByNonSelected(cell.col, cell.row, selectedTokens)) return fail('Cell occupied');
if (origin.maintenanceRoom && Object.values(game.facilities || {}).some(f => f.entry && !selectedTokens.has(`facility:${f.id}`) && f.entry.col === cell.col && f.entry.row === cell.row)) return fail('Cell occupied');
targetCells.add(tk);
}
}
@ -252,15 +225,9 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
origin.obj.col = origin.col + dcol;
origin.obj.row = origin.row + drow;
} else if (origin.type === 'facility') {
if (origin.maintenanceRoom) {
origin.obj.col = origin.col + dcol;
origin.obj.row = origin.row + drow;
syncMaintenanceRoomRect(origin.obj);
} else {
const nextCenter = { x: origin.center.x + dx, y: origin.center.y + dy };
const { entry, side } = nearestGridEdge(nextCenter);
layoutFacilityOnEdge(origin.obj, entry, side);
}
const nextCenter = { x: origin.center.x + dx, y: origin.center.y + dy };
const { entry, side } = nearestGridEdge(nextCenter);
layoutFacilityOnEdge(origin.obj, entry, side);
}
}
game.groupDrag.selections = game.groupDrag.origins.map(o => o.type === 'conveyor' ? { type: 'conveyor', id: o.currentKey || o.oldKey } : { type: o.type, id: o.id });

View file

@ -4,8 +4,8 @@ import { TEXT } from '../core/text.js';
import { routeFromFarmToScanner, factoryReady, facilityConnectionIssues } from './routing.js';
import { buildPrice, fairiesTributeInfo, finalScore, maleTruckPenalty, mixerPoopPenalty, truckPoopPenalty, zundaTaxInfo } from './economy.js';
import { truckTarget } from './contracts.js';
import { CARD_DEFS, conveyorSpeedForGame } from './cards.js';
import { maintenanceSummary } from './maintenance.js';
import { conveyorSpeedForGame } from './cards.js';
import { maintenanceSummary, repairmanDailyCost } from './maintenance.js';
export function createUISystem({ game, ui, build, startGame, beginCardDraft, activeQueuedChick }) {
function escapeHtml(value) {
@ -29,7 +29,6 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
updateBuildStatus();
updateContractPanel();
updateTurnSummary();
updateAcquiredCardsPanel();
updateHistoryButtons();
}
@ -57,6 +56,15 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
: '';
}
if (ui.buttons.erase) ui.buttons.erase.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending;
if (ui.hireRepairmanButton) {
const hired = !!game.repairman?.hiredForNextDay;
const cost = repairmanDailyCost(game);
const blocked = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || hired || game.cash < cost;
ui.hireRepairmanButton.disabled = blocked;
ui.hireRepairmanButton.classList.toggle('active', hired);
ui.hireRepairmanButton.classList.toggle('unaffordable', game.cash < cost);
ui.hireRepairmanButton.title = hired ? 'Repairman is hired for the next production phase.' : `Hire for ${yen(cost)} for one day. Repairs 2% durability per second.`;
}
const cashout = noAffordableBuildTools();
ui.buttons.nextTurn?.classList.toggle('cashout-emphasis', cashout);
ui.buttons.erase?.classList.toggle('cashout-emphasis', cashout);
@ -73,50 +81,6 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
: TEXT.status.sorting;
}
const ACQUIRED_CARD_IDS = new Set([
'bearing', 'legalWork', 'crowdedFarming', 'hatchingFeed', 'safetyCover',
'preventiveMaintenance', 'dudFilter', 'durabilityCoating', 'dudRefund',
'freeReroll', 'composter', 'scannerQueueSpacing', 'recyclingSubsidy',
'chemicalWeaponSubsidy', 'laborExploitation', 'usedMachine'
]);
function acquiredCardCount(card) {
const effects = game.cardEffects || {};
if (!ACQUIRED_CARD_IDS.has(card.id)) return 0;
if (card.id === 'usedMachine') return effects.usedMachineActive ? 1 : 0;
if (card.id === 'laborExploitation') return effects.laborExploitation ? 1 : 0;
return Math.max(0, Number(effects[card.id]) || 0);
}
function acquiredCardEntries() {
return CARD_DEFS
.filter(card => ACQUIRED_CARD_IDS.has(card.id))
.map(card => ({ card, count: acquiredCardCount(card) }))
.filter(item => item.count > 0);
}
function updateAcquiredCardsPanel() {
const panel = ui.cardInventory;
if (!panel) return;
const entries = acquiredCardEntries();
const total = entries.reduce((sum, item) => sum + item.count, 0);
const summary = panel.querySelector('summary');
const list = panel.querySelector('.acquired-cards-list');
if (summary) summary.innerHTML = `CARDS <strong>${total}</strong><span>${entries.length ? 'passive' : 'none'}</span>`;
if (!list) return;
if (!entries.length) {
list.innerHTML = '<p>No passive acquired cards yet. Consumed cards and equipment improvements are hidden.</p>';
panel.classList.add('empty');
return;
}
panel.classList.remove('empty');
list.innerHTML = entries.map(({ card, count }) => {
const tags = (card.tags || []).filter(tag => !['ACTIVE', 'ONE-SHOT'].includes(tag)).slice(0, 2);
return `<div class="acquired-card-chip ${escapeHtml(card.rarity || 'common')}"><b>${escapeHtml(card.title)}</b><strong>x${count}</strong>${tags.length ? `<small>${tags.map(escapeHtml).join(' / ')}</small>` : ''}</div>`;
}).join('');
}
function updateHistoryButtons() {
ui.buttons.undo.disabled = game.phase !== 'build' || game.undoStack.length === 0;
ui.buttons.redo.disabled = game.phase !== 'build' || game.redoStack.length === 0;
@ -131,8 +95,6 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
const tribute = fairiesTributeInfo(game, tributeDay);
const lastTribute = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.fairiesTribute || 0 : null;
const lastTributeReduction = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.reduction || 0 : 0;
const maint = maintenanceSummary(game);
const activeWorkers = Array.isArray(game.repairmen) ? game.repairmen.filter(r => r.active).length : (game.repairman?.active ? 1 : 0);
ui.turnSummary.innerHTML = [
`Seed: <b>${escapeHtml(game.runSeed || 'none')}</b> | Grid box: <b>${game.gridRows || 10}x${game.gridCols || 20}</b> | Owned: <b>${game.gridOwnedCells || game.ownedCells?.size || ((game.gridRows || 10) * (game.gridCols || 20))}</b> cells | Blocked: <b>${game.blockedCells?.size || 0}</b> cells`,
`Truck target: <b>${truckTarget(game).toUpperCase()}</b> | Male fine: <b>-${yen(maleTruckPenalty(game))}</b>`,
@ -141,7 +103,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
lastTribute != null
? `Fairies tribute paid before Build: <b>-${yen(lastTribute)}</b>${lastTributeReduction ? ` | Flattery -${yen(lastTributeReduction)}` : ''}`
: `Fairies tribute before Build: <b>-${yen(tribute.amount)}</b>${tribute.reduction ? ` | Flattery -${yen(tribute.reduction)}` : ''}`,
`Maintenance: <b>worst ${maint.worst.percent}% dirty (${maint.worst.label})</b> | Repair workers: <b>${activeWorkers || maint.workers}</b> | Daily wages: <b>-${yen(maint.dailyWage || 0)}</b>`,
`Maintenance: <b>worst ${maintenanceSummary(game).worst.percent}% dirty (${maintenanceSummary(game).worst.label})</b> | Repairman: <b>${game.repairman?.hiredForNextDay ? 'hired next day' : game.repairman?.active ? 'working' : 'none'}</b>`,
`Manual combo: <b>${game.manualCombo?.count || 0}</b> | Card draft: <b>${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'}</b> | Belt: <b>${displaySpeed()}px/s</b> | Farms with scanner route: <b>${connected}/${game.eggFarms.length}</b>`,
game.cardEffects?.usedMachineActive ? '<span class="cash-negative">Procurement: USED MACHINE MODE / no refunds / 60% durability</span>' : '',
issues.length ? `<span class="cash-negative">Blocked: ${issues[0]}</span>` : `<span class="cash-positive">${TEXT.status.allPortsConnected}</span>`
@ -193,7 +155,6 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
ui.phase.textContent = phaseLabel();
if (ui.comboCount) ui.comboCount.textContent = game.manualCombo?.count || 0;
updatePriorityStrip();
updateAcquiredCardsPanel();
ui.turnProfit.textContent = yen(game.stats.profit);
ui.turnProfit.classList.toggle('cash-negative', game.stats.profit < 0);
ui.turnProfit.classList.toggle('cash-positive', game.stats.profit > 0);

View file

@ -748,111 +748,3 @@ body { font-size: 20px; }
@media (max-width: 1180px) {
.scanner-monitor { bottom: 132px; width: min(520px, calc(100% - 16px)); left: 8px; }
}
/* v27.8 acquired passive-card panel */
.acquired-cards-panel {
position: absolute;
z-index: 7;
left: 12px;
top: 124px;
width: min(360px, calc(100% - 450px));
max-height: 42px;
overflow: hidden;
border: 3px solid var(--line);
background: rgba(255,255,255,.94);
box-shadow: 5px 5px 0 rgba(16,32,21,.16);
font-weight: 900;
pointer-events: auto;
transition: max-height .14s ease, background .14s ease;
}
.acquired-cards-panel[open],
.acquired-cards-panel:hover,
.acquired-cards-panel:focus-within {
max-height: 360px;
background: rgba(255,255,255,.98);
}
.acquired-cards-panel summary {
min-height: 36px;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
cursor: pointer;
list-style: none;
text-transform: uppercase;
letter-spacing: .05em;
font-size: 15px;
}
.acquired-cards-panel summary::-webkit-details-marker { display: none; }
.acquired-cards-panel summary::after {
content: 'show';
margin-left: auto;
color: var(--muted);
font-size: 11px;
}
.acquired-cards-panel[open] summary::after,
.acquired-cards-panel:hover summary::after { content: 'hide'; }
.acquired-cards-panel summary strong {
display: inline-grid;
place-items: center;
min-width: 28px;
height: 24px;
padding: 0 6px;
border: 2px solid var(--line);
background: var(--green-soft);
color: var(--green-dark);
}
.acquired-cards-panel summary span {
color: var(--muted);
font-size: 11px;
}
.acquired-cards-list {
display: grid;
grid-template-columns: 1fr;
gap: 6px;
max-height: 306px;
overflow: auto;
padding: 8px;
border-top: 3px solid var(--line);
}
.acquired-cards-list p {
margin: 0;
color: var(--muted);
font-size: 12px;
line-height: 1.35;
}
.acquired-card-chip {
display: grid;
grid-template-columns: 1fr auto;
gap: 2px 8px;
align-items: center;
border: 2px solid var(--line);
background: #f7fff5;
padding: 7px 8px;
font-size: 12px;
line-height: 1.2;
}
.acquired-card-chip.rare { background: #fff0d1; }
.acquired-card-chip.ultraRare { background: #ffe6f0; }
.acquired-card-chip b {
text-transform: uppercase;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.acquired-card-chip strong {
color: var(--green);
font-size: 13px;
}
.acquired-card-chip small {
grid-column: 1 / -1;
color: var(--muted);
font-size: 10px;
text-transform: uppercase;
}
.acquired-cards-panel.empty { opacity: .78; }
.game-shell.phase-title .acquired-cards-panel { display: none; }
@media (max-width: 1180px) {
.acquired-cards-panel { top: 252px; left: 8px; width: min(420px, calc(100% - 16px)); }
}