tweak
This commit is contained in:
parent
650e44aff3
commit
d6a55d9c13
20 changed files with 187 additions and 672 deletions
82
dist/game.js
vendored
82
dist/game.js
vendored
File diff suppressed because one or more lines are too long
12
index.html
12
index.html
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "zunda-shiwake",
|
||||
"version": "27.8.0",
|
||||
"version": "27.6.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
|
|
|
|||
44
src/game.js
44
src/game.js
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
|
|
|
|||
|
|
@ -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)}.`;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
108
styles.css
108
styles.css
|
|
@ -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)); }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue