twe
This commit is contained in:
parent
1c383282dd
commit
383a46955c
18 changed files with 755 additions and 254 deletions
60
dist/game.js
vendored
60
dist/game.js
vendored
File diff suppressed because one or more lines are too long
28
index.html
28
index.html
|
|
@ -12,9 +12,9 @@
|
|||
<canvas id="gameCanvas" width="1440" height="900" aria-label="Chick Sorter game canvas"></canvas>
|
||||
|
||||
<div class="hud hud-top-left" aria-label="Run status">
|
||||
<div class="hud-card cash"><span>CASH</span><strong id="money">¥250</strong></div>
|
||||
<div class="hud-card cash"><span>CASH</span><strong id="money">・・250</strong></div>
|
||||
<div class="hud-card"><span>TIME</span><strong id="timeLeft">60.0s</strong></div>
|
||||
<div class="hud-card profit"><span>NET</span><strong id="turnProfit">¥0</strong></div>
|
||||
<div class="hud-card profit"><span>NET</span><strong id="turnProfit">・・0</strong></div>
|
||||
<div class="hud-card"><span>DAY</span><strong id="turn">1</strong></div>
|
||||
<div class="hud-card compact"><span>PHASE</span><strong id="phaseLabel">Title</strong></div>
|
||||
<div class="hud-card combo"><span>COMBO</span><strong id="comboCount">0</strong></div>
|
||||
|
|
@ -46,25 +46,20 @@
|
|||
<section class="equipment-section">
|
||||
<h2>Add / Edit Equipment</h2>
|
||||
<div class="large-tools">
|
||||
<button id="buildConveyorButton" class="tool-button" type="button"><strong>Conveyor</strong><span>¥30 / tile</span></button>
|
||||
<button id="buildEggFarmButton" class="tool-button" type="button"><strong>Egg Farm</strong><span>¥250</span></button>
|
||||
<button id="buildAutoScannerButton" class="tool-button" type="button"><strong>Auto Scanner</strong><span>¥360 / 2.25s cooldown</span></button>
|
||||
<button id="buildManualScannerButton" class="tool-button" type="button"><strong>Manual Scanner</strong><span>¥260</span></button>
|
||||
<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="buildConveyorButton" class="tool-button" type="button"><strong>Conveyor</strong><span>・・30 / tile</span></button>
|
||||
<button id="buildBoostConveyorButton" class="tool-button" type="button"><strong>Boost Conveyor</strong><span>・・50 / tile</span></button>
|
||||
<button id="buildEggFarmButton" class="tool-button" type="button"><strong>Egg Farm</strong><span>・・250</span></button>
|
||||
<button id="buildAutoScannerButton" class="tool-button" type="button"><strong>Auto Scanner</strong><span>¥360 / 3.5s cooldown</span></button>
|
||||
<button id="buildManualScannerButton" class="tool-button" type="button"><strong>Manual Scanner</strong><span>・・260</span></button>
|
||||
<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="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>
|
||||
<button id="expandGridButton" class="tool-button expand" type="button"><strong>Expand Grid</strong><span>¥1,300 / +10×10</span><small class="tool-flavor">Canvas buttons buy adjacent right or upper 10×10 lots. New ground includes blocked no-build tiles.</small></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>
|
||||
</section>
|
||||
|
||||
<section class="contract-section">
|
||||
<h2>Irregular One-Day Event</h2>
|
||||
<div id="contractPanel" class="contract-card empty">Irregular forced events appear during Build phase.</div>
|
||||
</section>
|
||||
<section class="rules-section compact-rules">
|
||||
<h2>Status</h2>
|
||||
<div id="turnSummary" class="mini-box"></div>
|
||||
|
|
@ -88,6 +83,7 @@
|
|||
<label class="debug-check"><input id="debugInfiniteCash" type="checkbox" /> Infinite money</label>
|
||||
<label class="debug-day">Day <input id="debugDayInput" type="number" min="1" step="1" value="1" /></label>
|
||||
<button id="debugSetDayButton" type="button">Set Day</button>
|
||||
<button id="debugZeroTimerButton" type="button">Timer 0</button>
|
||||
<label class="debug-card">Card <select id="debugCardSelect"></select></label>
|
||||
<button id="debugGrantCardButton" type="button">Grant Card</button>
|
||||
<button id="debugGrantAllCardsButton" type="button">All Cards</button>
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export const BALANCE = {
|
|||
},
|
||||
production: {
|
||||
poopRate: 0.08,
|
||||
autoScannerCooldown: 2.25,
|
||||
autoScannerCooldown: 3.5,
|
||||
autoScannerUpgradeRate: 0.95,
|
||||
autoScannerMinCooldown: 0.5,
|
||||
eggSpawnRanges: [
|
||||
|
|
@ -171,6 +171,40 @@ export const BALANCE = {
|
|||
description: 'Conveyor speed +7.5%.',
|
||||
tags: ['CONVEYOR', 'PASSIVE']
|
||||
},
|
||||
{
|
||||
id: 'sparePartsBin',
|
||||
title: 'SPARE PARTS BIN',
|
||||
rarity: 'common',
|
||||
type: 'instant',
|
||||
description: 'Restores 2% durability to every machine. A box of almost-compatible parts.',
|
||||
tags: ['MAINTENANCE', 'ACTIVE'],
|
||||
weightBase: 100,
|
||||
weightLossPerCopy: 2
|
||||
},
|
||||
{
|
||||
id: 'dudRefund',
|
||||
title: 'DUD REFUND',
|
||||
rarity: 'common',
|
||||
type: 'instant',
|
||||
description: 'DUDs now come with paperwork. Gain JPY 50 per copy when you pick a DUD.',
|
||||
tags: ['CARD', 'ECONOMY', 'PASSIVE']
|
||||
},
|
||||
{
|
||||
id: 'freeReroll',
|
||||
title: 'FREE REROLL',
|
||||
rarity: 'common',
|
||||
type: 'instant',
|
||||
description: 'The first bad ideas are free.',
|
||||
tags: ['CARD', 'PASSIVE']
|
||||
},
|
||||
{
|
||||
id: 'composter',
|
||||
title: 'COMPOSTER',
|
||||
rarity: 'common',
|
||||
type: 'instant',
|
||||
description: 'Turns shredder waste into suspicious fertilizer. Gain JPY 3 per copy when SHREDDER processes poop.',
|
||||
tags: ['POOP', 'ECONOMY', 'PASSIVE']
|
||||
},
|
||||
{
|
||||
id: 'extraEggOutlet',
|
||||
title: 'Extra Egg Outlet',
|
||||
|
|
@ -283,6 +317,10 @@ export const BALANCE = {
|
|||
preventiveMaintenance: 0,
|
||||
dudFilter: 0,
|
||||
durabilityCoating: 0,
|
||||
sparePartsBin: 0,
|
||||
dudRefund: 0,
|
||||
freeReroll: 0,
|
||||
composter: 0,
|
||||
laborExploitation: 0,
|
||||
usedMachineActive: false,
|
||||
rescueLoanCharges: 0,
|
||||
|
|
@ -330,6 +368,10 @@ export const BALANCE = {
|
|||
id: 'conveyor', type: 'conveyor', name: 'Conveyor', shortName: 'Belt', price: 30,
|
||||
buildable: true, upgradeable: false
|
||||
},
|
||||
boostConveyor: {
|
||||
id: 'boostConveyor', type: 'conveyor', name: 'Boost Conveyor', shortName: 'BOOST BELT', price: 50,
|
||||
buildable: true, upgradeable: false, speedMultiplier: 2
|
||||
},
|
||||
eggFarm: {
|
||||
id: 'eggFarm', type: 'eggFarm', name: 'Egg Farm', shortName: 'EGG', price: 250,
|
||||
buildable: true, upgradeable: true, maxLevel: 4, upgradeCosts: [null, 300, 720, 1600]
|
||||
|
|
|
|||
|
|
@ -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', 'eggFarm', 'autoScanner', 'manualScanner', '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'];
|
||||
|
||||
|
|
|
|||
|
|
@ -33,17 +33,19 @@ export function createEggFarm(game, col, row) {
|
|||
|
||||
export function createScanner(game, col, row, kind) {
|
||||
const cost = kind === 'auto' ? FACILITY_DEFS.autoScanner.price : FACILITY_DEFS.manualScanner.price;
|
||||
const manualCount = game.scanners.filter(s => s.kind === 'manual').length;
|
||||
const usedManualSlots = new Set(game.scanners.filter(s => s.kind === 'manual').map(s => s.slot));
|
||||
let manualSlot = 0;
|
||||
while (usedManualSlots.has(manualSlot)) manualSlot += 1;
|
||||
return {
|
||||
type: 'scanner', id: game.nextId++, kind,
|
||||
slot: kind === 'manual' ? manualCount : null,
|
||||
role: manualCount % 2,
|
||||
slot: kind === 'manual' ? manualSlot : null,
|
||||
role: kind === 'manual' ? manualSlot % 2 : 0,
|
||||
col, row, level: 1,
|
||||
queue: [], cooldown: 0,
|
||||
price: cost,
|
||||
builtSession: game.buildSession,
|
||||
autoMode: 'standard',
|
||||
keys: kind === 'manual' ? manualKeysForSlot(manualCount) : null
|
||||
keys: kind === 'manual' ? manualKeysForSlot(manualSlot) : null
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ export function newTurnStats() {
|
|||
chemicalWeaponSubsidy: 0,
|
||||
rescueLoan: 0,
|
||||
cardRerollCost: 0,
|
||||
dudRefundIncome: 0,
|
||||
composterIncome: 0,
|
||||
sparePartsRepair: 0,
|
||||
mixerPoopFine: 0,
|
||||
truckPoopFine: 0,
|
||||
maleTruckFine: 0,
|
||||
|
|
@ -74,6 +77,9 @@ export function newTotalStats() {
|
|||
manualComboFailure: 0,
|
||||
repairWorkerWages: 0,
|
||||
cardRerollCost: 0,
|
||||
dudRefundIncome: 0,
|
||||
composterIncome: 0,
|
||||
sparePartsRepair: 0,
|
||||
mixerPoopFine: 0,
|
||||
truckPoopFine: 0,
|
||||
maleTruckFine: 0,
|
||||
|
|
@ -135,7 +141,7 @@ export function createGame() {
|
|||
lastExplodedComponent: new Map(),
|
||||
contractOffer: null,
|
||||
contractActive: null,
|
||||
cardEffects: { bearing: 0, legalWork: 0, fairiesFlatteryNext: 0, crowdedFarming: 0, extraEggOutlet: 0, hatchingFeed: 0, safetyCover: 0, recyclingSubsidy: 0, chemicalWeaponSubsidy: 0, preventiveMaintenance: 0, dudFilter: 0, durabilityCoating: 0, laborExploitation: 0, usedMachineActive: false, rescueLoanCharges: 0, loans: [] },
|
||||
cardEffects: { bearing: 0, legalWork: 0, fairiesFlatteryNext: 0, crowdedFarming: 0, extraEggOutlet: 0, hatchingFeed: 0, safetyCover: 0, recyclingSubsidy: 0, chemicalWeaponSubsidy: 0, preventiveMaintenance: 0, dudFilter: 0, durabilityCoating: 0, sparePartsBin: 0, dudRefund: 0, freeReroll: 0, composter: 0, laborExploitation: 0, usedMachineActive: false, rescueLoanCharges: 0, loans: [] },
|
||||
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
|
||||
cardTargetPick: null,
|
||||
manualCombo: { count: 0, lastBonus: 0 },
|
||||
|
|
@ -182,15 +188,15 @@ export function defaultFacilities() {
|
|||
const INITIAL_CONVEYOR_BACKBONE = Object.freeze([
|
||||
// Egg Farm -> S1 top input. The S1/S2 rows were moved down so unrelated
|
||||
// belt streams keep at least one blank tile between side-by-side flows.
|
||||
[1, 1], [2, 1], [3, 1], [4, 1], [5, 1], [6, 1], [7, 1], [8, 1], [8, 2], [8, 3],
|
||||
[1, 1], [2, 1], [3, 1], [4, 1], [5, 1], [6, 1], [7, 1], [8, 1], [8, 2],
|
||||
// S1 left output -> Mixer receiver on the left grid edge.
|
||||
[7, 4], [6, 4], [5, 4], [4, 4], [3, 4], [2, 4], [1, 4], [0, 4],
|
||||
// S1 right output -> S2 top input.
|
||||
[9, 4], [10, 4], [11, 4], [12, 4], [13, 4], [14, 4], [15, 4], [15, 5], [15, 6],
|
||||
[10, 4], [11, 4], [12, 4], [13, 4], [14, 4], [15, 4], [15, 5],
|
||||
// S2 left output -> Waste Shredder receiver on bottom grid edge.
|
||||
[14, 7], [13, 7], [12, 7], [12, 8], [12, 9],
|
||||
// S2 right output -> Truck receiver on the bottom grid edge.
|
||||
[16, 7], [17, 7], [18, 7], [18, 8], [18, 9]
|
||||
[17, 7], [18, 7], [18, 8], [18, 9]
|
||||
]);
|
||||
|
||||
const INITIAL_SCANNERS = Object.freeze([
|
||||
|
|
@ -213,13 +219,19 @@ function addConveyorTile(game, col, row, options = {}) {
|
|||
uses: 0,
|
||||
durability: BALANCE.maintenance.durability.conveyor,
|
||||
maintenanceType: 'conveyor',
|
||||
kind: options.kind || 'conveyor',
|
||||
speedMultiplier: options.speedMultiplier || 1,
|
||||
dir: options.dir || null,
|
||||
outDirs: Array.isArray(options.outDirs) ? [...options.outDirs] : []
|
||||
outDirs: Array.isArray(options.outDirs) ? [...options.outDirs] : [],
|
||||
branchMode: options.branchMode || 'random'
|
||||
});
|
||||
} else if (options.dir || options.outDirs) {
|
||||
const meta = game.conveyorMeta.get(k);
|
||||
if (options.dir) meta.dir = options.dir;
|
||||
if (Array.isArray(options.outDirs)) meta.outDirs = [...new Set([...(meta.outDirs || []), ...options.outDirs])];
|
||||
if (!meta.branchMode) meta.branchMode = 'random';
|
||||
if (!meta.kind) meta.kind = 'conveyor';
|
||||
if (!meta.speedMultiplier) meta.speedMultiplier = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -242,9 +254,20 @@ function markInitialConveyorDirection(game, from, to) {
|
|||
|
||||
function scannerPortCells(scanner) {
|
||||
return [
|
||||
{ col: scanner.col, row: scanner.row - 1 },
|
||||
{ col: scanner.col, row: scanner.row - 2 },
|
||||
{ col: scanner.col - 1, row: scanner.row },
|
||||
{ col: scanner.col + 1, row: scanner.row }
|
||||
{ col: scanner.col + 2, row: scanner.row }
|
||||
].filter(p => inGrid(p.col, p.row));
|
||||
}
|
||||
|
||||
function scannerBodyCells(scanner) {
|
||||
return [
|
||||
{ col: scanner.col, row: scanner.row - 1 },
|
||||
{ col: scanner.col + 1, row: scanner.row - 1 },
|
||||
{ col: scanner.col, row: scanner.row },
|
||||
{ col: scanner.col + 1, row: scanner.row },
|
||||
{ col: scanner.col, row: scanner.row + 1 },
|
||||
{ col: scanner.col + 1, row: scanner.row + 1 }
|
||||
].filter(p => inGrid(p.col, p.row));
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +284,7 @@ function protectedInitialCells(game) {
|
|||
const reserved = new Set([...game.conveyorTiles]);
|
||||
const add = p => { if (p && inGrid(p.col, p.row)) reserved.add(key(p.col, p.row)); };
|
||||
for (const scanner of game.scanners) {
|
||||
add(scanner);
|
||||
for (const p of scannerBodyCells(scanner)) add(p);
|
||||
for (const p of scannerPortCells(scanner)) add(p);
|
||||
}
|
||||
for (const farm of game.eggFarms) {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export function equipmentName(id) {
|
|||
export function buildToolPriceText(id) {
|
||||
const def = FACILITY_DEFS[id];
|
||||
if (!def) return '';
|
||||
if (id === 'conveyor') return `${yen(def.price)} / tile`;
|
||||
if (id === 'conveyor' || id === 'boostConveyor') return `${yen(def.price)} / tile`;
|
||||
if (id === 'autoScanner') return `${yen(def.price)} / ${AUTO_SCANNER_COOLDOWN.toFixed(1)}s cooldown`;
|
||||
if (id === 'trash') return yen(def.price);
|
||||
return yen(def.price);
|
||||
|
|
@ -65,8 +65,9 @@ export function buildToolPriceText(id) {
|
|||
export function buildToolFlavorText(id) {
|
||||
const flavors = {
|
||||
conveyor: 'A narrow green belt. Routes decide whether chicks live, ship, or become invoices.',
|
||||
boostConveyor: 'Moves chicks twice as fast. Wears down like a normal belt.',
|
||||
eggFarm: 'A tiny gatehouse producing questionable eggs on schedule.',
|
||||
autoScanner: 'An automated judge. Faster than hands, still very sure of itself.',
|
||||
autoScanner: 'Slower than hands without upgrades.',
|
||||
manualScanner: 'A manual checkpoint. The operator is the algorithm.',
|
||||
mixer: 'Male chicks become revenue here. Do not feed it poop.',
|
||||
trash: 'A polite shredder for poop and other regrets.',
|
||||
|
|
|
|||
61
src/game.js
61
src/game.js
|
|
@ -28,12 +28,12 @@ const ui = {
|
|||
hoverTooltip: document.getElementById('hoverTooltip'),
|
||||
manualScannerMonitor: document.getElementById('manualScannerMonitor'),
|
||||
hireRepairmanButton: document.getElementById('hireRepairmanButton'),
|
||||
expandGridButton: document.getElementById('expandGridButton'),
|
||||
debug: {
|
||||
panel: document.getElementById('debugPanel'),
|
||||
infiniteCash: document.getElementById('debugInfiniteCash'),
|
||||
dayInput: document.getElementById('debugDayInput'),
|
||||
setDay: document.getElementById('debugSetDayButton'),
|
||||
zeroTimer: document.getElementById('debugZeroTimerButton'),
|
||||
cardSelect: document.getElementById('debugCardSelect'),
|
||||
grantCard: document.getElementById('debugGrantCardButton'),
|
||||
grantAllCards: document.getElementById('debugGrantAllCardsButton'),
|
||||
|
|
@ -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'), 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')
|
||||
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')
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -49,6 +49,7 @@ const ui = {
|
|||
function initializeStaticText() {
|
||||
const buttonToolMap = {
|
||||
conveyor: 'conveyor',
|
||||
boostConveyor: 'boostConveyor',
|
||||
eggFarm: 'eggFarm',
|
||||
autoScanner: 'autoScanner',
|
||||
manualScanner: 'manualScanner',
|
||||
|
|
@ -105,9 +106,22 @@ function debugSetDay() {
|
|||
updateDebugReadout(`Day set to ${day}.`);
|
||||
}
|
||||
|
||||
function debugZeroTimer() {
|
||||
game.timeLeft = 0;
|
||||
if (game.phase === 'running') closeFarmShutters();
|
||||
floating(game, canvas.width / 2 - game.view.x, 86 - game.view.y, 'TIMER 0', THEME.warn);
|
||||
uiSystem.updatePanels();
|
||||
uiSystem.updateUI();
|
||||
updateDebugReadout('Timer set to 0.');
|
||||
}
|
||||
|
||||
function debugGrantSelectedCard() {
|
||||
const id = ui.debug.cardSelect?.value;
|
||||
const result = debugGrantCard(game, id);
|
||||
if (ui.debug.grantCard) {
|
||||
ui.debug.grantCard.textContent = result.ok ? 'Granted' : 'Grant failed';
|
||||
window.setTimeout(() => { if (ui.debug.grantCard) ui.debug.grantCard.textContent = 'Grant Card'; }, 900);
|
||||
}
|
||||
uiSystem.updatePanels();
|
||||
uiSystem.updateUI();
|
||||
updateDebugReadout(result.reason);
|
||||
|
|
@ -440,6 +454,19 @@ function clickSelect(event) {
|
|||
uiSystem.updatePanels();
|
||||
const obj = build.selectedObject();
|
||||
if (obj?.type === 'scanner' && obj.kind === 'manual') build.showManualScannerMenu(obj);
|
||||
if (obj?.type === 'scanner' && obj.kind === 'auto') build.showAutoScannerMenu(obj);
|
||||
}
|
||||
|
||||
function cancelBuildAction() {
|
||||
game.buildTool = null;
|
||||
game.selected = null;
|
||||
game.multiSelected = [];
|
||||
game.selectionBox = null;
|
||||
if (game.cardTargetPick?.mode === 'autoScannerMenu') game.cardTargetPick = null;
|
||||
uiSystem.hideModal();
|
||||
hideHoverTooltip();
|
||||
uiSystem.updatePanels();
|
||||
uiSystem.updateUI();
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
|
|
@ -557,13 +584,21 @@ canvas.addEventListener('pointerdown', event => {
|
|||
const world = canvasPoint(event);
|
||||
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; }
|
||||
const expansionOffer = build.expansionOfferAtPoint?.(world);
|
||||
if (expansionOffer) { build.buyGridExpansion(expansionOffer); clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); return; }
|
||||
if (game.buildTool === 'erase') { build.eraseAtPoint(world); return; }
|
||||
if (build.cycleBranchModeAtPoint?.(world)) { uiSystem.updatePanels(); uiSystem.updateUI(); return; }
|
||||
if (game.buildTool === 'erase') {
|
||||
if (!build.equipmentAtPoint(world)) { cancelBuildAction(); return; }
|
||||
build.eraseAtPoint(world);
|
||||
return;
|
||||
}
|
||||
if (game.buildTool) {
|
||||
if (game.buildTool === 'conveyor') {
|
||||
build.beginConveyorDrag(pointToCell(world.x, world.y));
|
||||
const cell = pointToCell(world.x, world.y);
|
||||
if (!cell && !['mixer', 'trash', 'truck'].includes(game.buildTool)) { cancelBuildAction(); return; }
|
||||
if (game.buildTool === 'conveyor' || game.buildTool === 'boostConveyor') {
|
||||
build.beginConveyorDrag(cell);
|
||||
return;
|
||||
}
|
||||
const hit = build.equipmentAtPoint(world);
|
||||
|
|
@ -580,7 +615,7 @@ canvas.addEventListener('pointermove', 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' && (event.buttons & 1)) build.continueConveyorDrag(pointToCell(canvasPoint(event).x, canvasPoint(event).y));
|
||||
if ((game.buildTool === 'conveyor' || game.buildTool === 'boostConveyor') && (event.buttons & 1)) build.continueConveyorDrag(pointToCell(canvasPoint(event).x, canvasPoint(event).y));
|
||||
if (game.buildTool === 'erase' && (event.buttons & 1)) build.eraseAtPoint(canvasPoint(event));
|
||||
});
|
||||
canvas.addEventListener('pointerup', event => {
|
||||
|
|
@ -589,7 +624,7 @@ canvas.addEventListener('pointerup', event => {
|
|||
if (!game.groupDrag.committed) clickSelect(event);
|
||||
build.finishGroupDrag();
|
||||
}
|
||||
if (game.buildTool === 'conveyor') build.endConveyorDrag?.();
|
||||
if (game.buildTool === 'conveyor' || game.buildTool === 'boostConveyor') build.endConveyorDrag?.();
|
||||
game.groupDrag = null; game.pan = null;
|
||||
try { canvas.releasePointerCapture(event.pointerId); } catch (_) { /* noop */ }
|
||||
});
|
||||
|
|
@ -598,14 +633,14 @@ canvas.addEventListener('pointercancel', () => { build?.endConveyorDrag?.(); gam
|
|||
document.addEventListener('pointerdown', event => {
|
||||
if (!ui.modal.classList.contains('visible') || !ui.modal.classList.contains('equipment-popover')) return;
|
||||
if (event.target.closest('#modal .modal-card')) return;
|
||||
uiSystem.hideModal();
|
||||
cancelBuildAction();
|
||||
}, 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.eggFarm.addEventListener('click', () => build.setBuildTool('eggFarm')); ui.buttons.autoScanner.addEventListener('click', () => build.setBuildTool('autoScanner')); ui.buttons.manualScanner.addEventListener('click', () => build.setBuildTool('manualScanner')); ui.buttons.mixer.addEventListener('click', () => build.setBuildTool('mixer')); ui.buttons.trash.addEventListener('click', () => build.setBuildTool('trash')); ui.buttons.truck.addEventListener('click', () => build.setBuildTool('truck')); ui.buttons.erase.addEventListener('click', () => build.setBuildTool('erase'));
|
||||
ui.buttons.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.expandGridButton?.addEventListener('click', () => { build?.buyGridExpansion?.('right'); clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); });
|
||||
ui.debug.setDay?.addEventListener('click', debugSetDay);
|
||||
ui.debug.zeroTimer?.addEventListener('click', debugZeroTimer);
|
||||
ui.debug.grantCard?.addEventListener('click', debugGrantSelectedCard);
|
||||
ui.debug.grantAllCards?.addEventListener('click', debugGrantAllCards);
|
||||
ui.buttons.undo.addEventListener('click', () => { if (undo(game)) { clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); } }); ui.buttons.redo.addEventListener('click', () => { if (redo(game)) { clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); } });
|
||||
|
|
@ -626,7 +661,11 @@ window.addEventListener('keydown', event => {
|
|||
}
|
||||
if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && name === 'z') { event.preventDefault(); if (undo(game)) { clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); } }
|
||||
if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && (name === 'y' || (event.shiftKey && name === 'z'))) { event.preventDefault(); if (redo(game)) { clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); } }
|
||||
if (event.key === 'Escape' && game.cardTargetPick?.pending) { event.preventDefault(); cardSystem.cancelTargetPick(); }
|
||||
if (event.key === 'Escape' && game.cardTargetPick?.pending) {
|
||||
event.preventDefault();
|
||||
if (game.cardTargetPick.mode === 'autoScannerMenu') cancelBuildAction();
|
||||
else cardSystem.cancelTargetPick();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -254,18 +254,79 @@ function drawConveyors(ctx, game) {
|
|||
const directionMarkers = collectConveyorDirectionMarkers(game);
|
||||
for (const k of game.conveyorTiles) {
|
||||
const c = cellCenter(...Object.values(parseKey(k)));
|
||||
const meta = game.conveyorMeta.get(k) || {};
|
||||
const ratio = componentRatio(game, k);
|
||||
ctx.fillStyle = ratio > 0.5 ? mixHex(THEME.white, '#ffd6d6', Math.max(0, (ratio - .5) / .5)) : THEME.white;
|
||||
const baseFill = meta.kind === 'boostConveyor' ? '#fff2b8' : THEME.white;
|
||||
ctx.fillStyle = ratio > 0.5 ? mixHex(baseFill, '#ffd6d6', Math.max(0, (ratio - .5) / .5)) : baseFill;
|
||||
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 2;
|
||||
rect(ctx, c.x - 8, c.y - 8, 16, 16, true, true);
|
||||
drawDirtOverlay(ctx, c.x - 14, c.y - 14, 28, 28, { meta: game.conveyorMeta.get(k) });
|
||||
if (meta.kind === 'boostConveyor') {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = THEME.warn;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath(); ctx.moveTo(c.x - 15, c.y + 13); ctx.lineTo(c.x - 3, c.y - 13); ctx.lineTo(c.x + 2, c.y - 2); ctx.lineTo(c.x + 15, c.y - 13); ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
drawDirtOverlay(ctx, c.x - 14, c.y - 14, 28, 28, { meta });
|
||||
drawSelection(ctx, game, 'conveyor', k, c.x, c.y, 38, 38);
|
||||
drawConveyorDirectionMarkers(ctx, c, directionMarkers.get(k) || []);
|
||||
drawBranchModeLabel(ctx, c, meta, reachableConveyorDirs(game, k, meta));
|
||||
if (ratio > 0.5) label(ctx, c.x, c.y - 15, `${Math.floor(ratio * 100)}%`, THEME.danger);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function branchModeText(mode) {
|
||||
return { random: 'RND', pass: 'PASS', up: 'UP', right: 'RT', down: 'DN', left: 'LF' }[mode || 'random'] || 'RND';
|
||||
}
|
||||
|
||||
function neighborPointsIntoCell(game, p, dirName) {
|
||||
const d = DIRS.find(item => item.name === dirName);
|
||||
if (!d) return false;
|
||||
const meta = game.conveyorMeta?.get?.(key(p.col + d.dc, p.row + d.dr));
|
||||
if (!meta) return false;
|
||||
const names = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))];
|
||||
return names.includes(d.opposite);
|
||||
}
|
||||
|
||||
function reachableConveyorDirs(game, k, meta) {
|
||||
const p = parseKey(k);
|
||||
const neighborNames = DIRS
|
||||
.filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr)))
|
||||
.map(d => d.name);
|
||||
const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
|
||||
.filter(name => neighborNames.includes(name) && !neighborPointsIntoCell(game, p, name));
|
||||
const inferred = neighborNames.filter(name => !neighborPointsIntoCell(game, p, name));
|
||||
return explicit.length ? explicit : inferred;
|
||||
}
|
||||
|
||||
function effectiveConveyorDirs(game, k, meta) {
|
||||
const p = parseKey(k);
|
||||
const neighborNames = DIRS
|
||||
.filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr)))
|
||||
.map(d => d.name);
|
||||
const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
|
||||
.filter(name => neighborNames.includes(name) && !neighborPointsIntoCell(game, p, name));
|
||||
const inferred = neighborNames.filter(name => !neighborPointsIntoCell(game, p, name));
|
||||
return explicit.length ? explicit : inferred;
|
||||
}
|
||||
|
||||
function drawBranchModeLabel(ctx, center, meta, dirs) {
|
||||
if (dirs.length < 3) return;
|
||||
ctx.save();
|
||||
ctx.font = '900 11px ui-monospace, monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.fillStyle = 'rgba(255,255,255,.96)';
|
||||
const mode = meta.branchMode === 'pass' && dirs.length >= 4 ? 'pass' : (dirs.includes(meta.branchMode) ? meta.branchMode : 'random');
|
||||
ctx.strokeStyle = mode !== 'random' ? THEME.warn : THEME.green;
|
||||
rect(ctx, center.x - 24, center.y + 11, 48, 24, true, true);
|
||||
ctx.fillStyle = mode !== 'random' ? THEME.warn : THEME.ink;
|
||||
const text = branchModeText(mode);
|
||||
ctx.fillText(text, center.x, center.y + 27);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function cellKeyFromPoint(game, p) {
|
||||
const cell = pointToCell(p.x, p.y);
|
||||
if (!cell) return null;
|
||||
|
|
@ -290,12 +351,9 @@ function addMarkerFromRoute(game, markers, route) {
|
|||
if (!list.some(x => Math.abs(Math.sin((x - angle) / 2)) < 0.01)) list.push(angle);
|
||||
}
|
||||
}
|
||||
function explicitConveyorAngles(meta) {
|
||||
function explicitConveyorAngles(game, k, meta) {
|
||||
if (!meta) return [];
|
||||
const names = [];
|
||||
if (Array.isArray(meta.outDirs)) names.push(...meta.outDirs);
|
||||
if (meta.dir) names.push(meta.dir);
|
||||
return [...new Set(names)]
|
||||
return effectiveConveyorDirs(game, k, meta)
|
||||
.map(name => DIRS.find(d => d.name === name)?.angle)
|
||||
.filter(angle => Number.isFinite(angle));
|
||||
}
|
||||
|
|
@ -303,7 +361,7 @@ function explicitConveyorAngles(meta) {
|
|||
function collectConveyorDirectionMarkers(game) {
|
||||
const markers = new Map();
|
||||
for (const [k, meta] of game.conveyorMeta || []) {
|
||||
const angles = explicitConveyorAngles(meta);
|
||||
const angles = explicitConveyorAngles(game, k, meta);
|
||||
if (angles.length) markers.set(k, angles);
|
||||
}
|
||||
for (const farm of game.eggFarms) addMarkerFromRoute(game, markers, routeFromFarmToScanner(game, farm)?.route);
|
||||
|
|
@ -399,39 +457,39 @@ function drawScanner(ctx, scanner, game) {
|
|||
ctx.lineWidth = Math.min(14, 5 + Math.floor(combo / 10));
|
||||
ctx.shadowColor = ctx.strokeStyle;
|
||||
ctx.shadowBlur = Math.min(28, 8 + combo);
|
||||
rect(ctx, c.x - 60, c.y - 44, 120, 88, false, true);
|
||||
rect(ctx, c.x - 54, c.y - 76, 108, 152, false, true);
|
||||
ctx.restore();
|
||||
}
|
||||
const img = scanner.kind === 'auto' ? assets.scannerAuto : assets.scannerManual;
|
||||
if (drawImageIfLoaded(ctx, img, c.x - 54, c.y - 38, 108, 76)) {
|
||||
if (scanner.kind === 'auto') drawDirtOverlay(ctx, c.x - 54, c.y - 38, 108, 76, scanner);
|
||||
if (drawImageIfLoaded(ctx, img, c.x - 50, c.y - 73, 100, 146)) {
|
||||
if (scanner.kind === 'auto') drawDirtOverlay(ctx, c.x - 50, c.y - 73, 100, 146, scanner);
|
||||
else drawManualKeyboardIcon(ctx, scanner, c);
|
||||
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 116, 84);
|
||||
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 108, 154);
|
||||
ctx.restore(); return;
|
||||
}
|
||||
ctx.fillStyle = scanner.kind === 'auto' ? '#d8ffe2' : THEME.white;
|
||||
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4;
|
||||
rect(ctx, c.x - 52, c.y - 36, 104, 72, true, true);
|
||||
rect(ctx, c.x - 50, c.y - 73, 100, 146, true, true);
|
||||
ctx.fillStyle = scanner.kind === 'auto' ? THEME.green : THEME.ink;
|
||||
ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center';
|
||||
ctx.fillText(`${scanner.kind === 'auto' ? 'AUTO' : `S${(scanner.slot ?? 0) + 1}`}`, c.x, c.y - 14);
|
||||
ctx.fillText(`${scanner.kind === 'auto' ? 'AUTO' : `S${(scanner.slot ?? 0) + 1}`}`, c.x, c.y - 48);
|
||||
ctx.font = '900 11px ui-monospace, monospace';
|
||||
const leftKey = scanner.keys?.left?.label || (scanner.slot === 0 ? 'A' : 'Left');
|
||||
const rightKey = scanner.keys?.right?.label || (scanner.slot === 0 ? 'D' : 'Right');
|
||||
ctx.fillText(scanner.role === 0 ? `${leftKey}:M ${rightKey}:NEXT` : `${leftKey}:WASTE ${rightKey}:TRUCK`, c.x, c.y + 3);
|
||||
ctx.fillText(scanner.role === 0 ? `${leftKey}:M ${rightKey}:NEXT` : `${leftKey}:WASTE ${rightKey}:TRUCK`, c.x, c.y - 28);
|
||||
const q = scanner.queue.length;
|
||||
if (scanner.kind === 'manual') {
|
||||
drawManualKeyboardIcon(ctx, scanner, c);
|
||||
ctx.fillStyle = q > 0 ? THEME.green : THEME.muted;
|
||||
ctx.font = '900 10px ui-monospace, monospace';
|
||||
ctx.fillText(`Q:${q}`, c.x, c.y + 32);
|
||||
ctx.fillText(`Q:${q}`, c.x, c.y + 54);
|
||||
} else {
|
||||
ctx.fillStyle = q > 0 ? THEME.green : THEME.muted;
|
||||
ctx.font = '900 12px ui-monospace, monospace';
|
||||
ctx.fillText(`Q:${q} CD:${scanner.cooldown.toFixed(1)}`, c.x, c.y + 25);
|
||||
drawDirtOverlay(ctx, c.x - 52, c.y - 36, 104, 72, scanner);
|
||||
ctx.fillText(`Q:${q} CD:${scanner.cooldown.toFixed(1)}`, c.x, c.y + 54);
|
||||
drawDirtOverlay(ctx, c.x - 50, c.y - 73, 100, 146, scanner);
|
||||
}
|
||||
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 116, 84);
|
||||
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 108, 154);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
|
|
@ -445,7 +503,7 @@ function drawManualKeyboardIcon(ctx, scanner, c) {
|
|||
labels[1].text = scanner.keys?.right?.label || labels[1].text;
|
||||
const pressedSide = scanner.keyPressTime > 0 ? scanner.keyPressSide : null;
|
||||
const baseX = c.x - 30;
|
||||
const baseY = c.y + 8;
|
||||
const baseY = c.y + 18;
|
||||
ctx.save();
|
||||
ctx.lineWidth = 3;
|
||||
ctx.fillStyle = '#f7fff5';
|
||||
|
|
@ -506,9 +564,13 @@ function receiverTitle(id, game) {
|
|||
if (id === 'truck') return { title: `IN: ${targetForTruck(game).toUpperCase()}`, type: targetForTruck(game), color: THEME.truckPink };
|
||||
return { title: 'IN', type: 'female', color: THEME.green };
|
||||
}
|
||||
function drawFacilityReceiver(ctx, game, id) {
|
||||
const f = game.facilities[id];
|
||||
function facilityKind(fOrId) {
|
||||
return typeof fOrId === 'string' ? fOrId : (fOrId?.baseId || fOrId?.id || '');
|
||||
}
|
||||
function drawFacilityReceiver(ctx, game, facility) {
|
||||
const f = typeof facility === 'string' ? game.facilities[facility] : facility;
|
||||
if (!f?.entry) return;
|
||||
const id = facilityKind(f);
|
||||
const c = cellCenter(f.entry.col, f.entry.row);
|
||||
const info = receiverTitle(id, game);
|
||||
ctx.save();
|
||||
|
|
@ -527,12 +589,13 @@ function drawFacilityReceiver(ctx, game, id) {
|
|||
ctx.restore();
|
||||
}
|
||||
function drawFacilities(ctx, game) {
|
||||
if (game.facilities.mixer) drawMixer(ctx, game);
|
||||
if (game.facilities.trash) drawTrash(ctx, game);
|
||||
if (game.facilities.truck) drawTruck(ctx, game);
|
||||
drawFacilityReceiver(ctx, game, 'mixer');
|
||||
drawFacilityReceiver(ctx, game, 'trash');
|
||||
drawFacilityReceiver(ctx, game, 'truck');
|
||||
for (const f of Object.values(game.facilities || {})) {
|
||||
const kind = facilityKind(f);
|
||||
if (kind === 'mixer') drawMixer(ctx, game, f);
|
||||
else if (kind === 'trash') drawTrash(ctx, game, f);
|
||||
else if (kind === 'truck') drawTruck(ctx, game, f);
|
||||
}
|
||||
for (const f of Object.values(game.facilities || {})) drawFacilityReceiver(ctx, game, f);
|
||||
}
|
||||
function drawExternalDuct(ctx, f) {
|
||||
if (!f?.entry) return;
|
||||
|
|
@ -551,11 +614,10 @@ function drawExternalDuct(ctx, f) {
|
|||
ctx.beginPath(); ctx.moveTo(c.x, c.y); ctx.lineTo(bx, by); ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
function drawMixer(ctx, game) {
|
||||
const m = game.facilities.mixer;
|
||||
function drawMixer(ctx, game, m = game.facilities.mixer) {
|
||||
ctx.save();
|
||||
drawExternalDuct(ctx, m);
|
||||
if (drawImageIfLoaded(ctx, assets.mixer, m.x, m.y, m.w, m.h)) { drawDirtOverlay(ctx, m.x, m.y, m.w, m.h, m); drawSelection(ctx, game, 'facility', 'mixer', m.x + m.w / 2, m.y + m.h / 2, m.w + 12, m.h + 12); ctx.restore(); return; }
|
||||
if (drawImageIfLoaded(ctx, assets.mixer, m.x, m.y, m.w, m.h)) { drawDirtOverlay(ctx, m.x, m.y, m.w, m.h, m); drawSelection(ctx, game, 'facility', m.id, m.x + m.w / 2, m.y + m.h / 2, m.w + 12, m.h + 12); ctx.restore(); return; }
|
||||
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4; rect(ctx, m.x, m.y, m.w, m.h, true, true);
|
||||
ctx.fillStyle = THEME.ink; ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`MIXER L${m.level}`, m.x + m.w / 2, m.y + 28);
|
||||
ctx.font = '900 12px ui-monospace, monospace'; ctx.fillText(`PAY ${yen(upgradedMixerPrice(game))}`, m.x + m.w / 2, m.y + 48);
|
||||
|
|
@ -563,27 +625,25 @@ function drawMixer(ctx, game) {
|
|||
ctx.strokeStyle = THEME.green; ctx.lineWidth = 5;
|
||||
for (let i = 0; i < 3; i += 1) { ctx.beginPath(); ctx.arc(m.x + m.w / 2, m.y + 78, 16 + i * 8, 0, Math.PI * 1.5); ctx.stroke(); }
|
||||
drawDirtOverlay(ctx, m.x, m.y, m.w, m.h, m);
|
||||
drawSelection(ctx, game, 'facility', 'mixer', m.x + m.w / 2, m.y + m.h / 2, m.w + 12, m.h + 12);
|
||||
drawSelection(ctx, game, 'facility', m.id, m.x + m.w / 2, m.y + m.h / 2, m.w + 12, m.h + 12);
|
||||
ctx.restore();
|
||||
}
|
||||
function drawTrash(ctx, game) {
|
||||
const t = game.facilities.trash;
|
||||
function drawTrash(ctx, game, t = game.facilities.trash) {
|
||||
ctx.save();
|
||||
drawExternalDuct(ctx, t);
|
||||
if (drawImageIfLoaded(ctx, assets.shredder, t.x, t.y, t.w, t.h)) { drawDirtOverlay(ctx, t.x, t.y, t.w, t.h, t); drawSelection(ctx, game, 'facility', 'trash', t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12); ctx.restore(); return; }
|
||||
if (drawImageIfLoaded(ctx, assets.shredder, t.x, t.y, t.w, t.h)) { drawDirtOverlay(ctx, t.x, t.y, t.w, t.h, t); drawSelection(ctx, game, 'facility', t.id, t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12); ctx.restore(); return; }
|
||||
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4; rect(ctx, t.x, t.y, t.w, t.h, true, true);
|
||||
ctx.fillStyle = THEME.ink; ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`SHREDDER L${t.level}`, t.x + t.w / 2, t.y + 25);
|
||||
for (let i = 0; i < 7; i += 1) { ctx.fillRect(t.x + 34 + i * 16, t.y + 48, 7, t.h - 66); }
|
||||
drawTargetBadge(ctx, t.x + 14, t.y + t.h - 42, t.w - 28, 'SEND POOP', 'poop', 'shredder');
|
||||
drawDirtOverlay(ctx, t.x, t.y, t.w, t.h, t);
|
||||
drawSelection(ctx, game, 'facility', 'trash', t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12);
|
||||
drawSelection(ctx, game, 'facility', t.id, t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12);
|
||||
ctx.restore();
|
||||
}
|
||||
function drawTruck(ctx, game) {
|
||||
const t = game.facilities.truck;
|
||||
function drawTruck(ctx, game, t = game.facilities.truck) {
|
||||
ctx.save();
|
||||
drawExternalDuct(ctx, t);
|
||||
if (drawImageIfLoaded(ctx, assets.truck, t.x, t.y, t.w, t.h)) { drawSelection(ctx, game, 'facility', 'truck', t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12); ctx.restore(); return; }
|
||||
if (drawImageIfLoaded(ctx, assets.truck, t.x, t.y, t.w, t.h)) { drawSelection(ctx, game, 'facility', t.id, t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12); ctx.restore(); return; }
|
||||
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4; rect(ctx, t.x, t.y, t.w, t.h, true, true);
|
||||
ctx.fillStyle = THEME.ink; ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`TRUCK L${t.level}`, t.x + t.w / 2, t.y + 24);
|
||||
ctx.font = '900 11px ui-monospace, monospace'; ctx.fillText(`UNIT ${yen(upgradedTruckPrice(game))}`, t.x + t.w / 2, t.y + 42);
|
||||
|
|
@ -591,7 +651,7 @@ function drawTruck(ctx, game) {
|
|||
drawTargetBadge(ctx, t.x + 15, t.y + 50, t.w - 30, `SEND ${truckTargetType.toUpperCase()}`, truckTargetType, game.contractOffer && !game.contractActive ? 'next event' : 'truck cargo');
|
||||
ctx.fillStyle = THEME.greenSoft; rect(ctx, t.x + 13, t.y + 88, t.w - 26, t.h - 103, true, false);
|
||||
for (const cargo of game.truckCargo) cargo.sex === 'poop' ? drawTinyPoop(ctx, t.x + cargo.x, t.y + cargo.y) : drawTinyChick(ctx, t.x + cargo.x, t.y + cargo.y, cargo.sex, cargo.sex === 'male');
|
||||
drawSelection(ctx, game, 'facility', 'truck', t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12);
|
||||
drawSelection(ctx, game, 'facility', t.id, t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12);
|
||||
ctx.restore();
|
||||
}
|
||||
function drawChicks(ctx, game, activeQueuedChick) {
|
||||
|
|
@ -700,7 +760,9 @@ function drawCardTargetOverlay(ctx, canvas, game) {
|
|||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = 3;
|
||||
const remaining = Math.max(1, game.cardTargetPick?.remaining || 1);
|
||||
const msg = isBlockedCellMode ? `CLICK BLOCKED CELL: ${remaining} LEFT. PRESS ESC TO CANCEL.` : 'CLICK AN UPGRADEABLE MACHINE. PRESS ESC TO CANCEL.';
|
||||
const msg = game.cardTargetPick?.mode === 'autoScannerMenu'
|
||||
? 'AUTO SCANNER ROUTE SETTINGS. CLICK EMPTY AREA TO CLOSE.'
|
||||
: isBlockedCellMode ? `CLICK BLOCKED CELL: ${remaining} LEFT. PRESS ESC TO CANCEL.` : 'CLICK AN UPGRADEABLE MACHINE. PRESS ESC TO CANCEL.';
|
||||
rect(ctx, (216 - (game.view?.x || 0)) / scale, (96 - (game.view?.y || 0)) / scale, 560 / scale, 42 / scale, true, true);
|
||||
ctx.fillStyle = THEME.ink;
|
||||
ctx.font = '900 12px ui-monospace, monospace';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { BALANCE } from '../core/balance.js';
|
||||
import { MACHINE_FACILITY_IDS, GRID, THEME } from '../core/config.js';
|
||||
import { DIRS, MACHINE_FACILITY_IDS, GRID, THEME } from '../core/config.js';
|
||||
import { getSpawnRange } from '../core/state.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';
|
||||
import { TEXT, equipmentName } from '../core/text.js';
|
||||
import { farmAt, scannerAt, scannerCenter, routeFromFarmToScanner, refreshRoutingAfterEdit, disconnectedBuildWarnings } from './routing.js';
|
||||
import { farmAt, scannerAt, scannerCenter, scannerFootprintCells, routeFromFarmToScanner, refreshRoutingAfterEdit, disconnectedBuildWarnings } from './routing.js';
|
||||
import { buildPrice, equipmentBasePrice, incomeMultiplier, mixerPoopPenalty, refundCash, spendCash, truckPoopPenalty, upgradedMixerPrice, upgradedTruckPrice, resaleValueFor, shredderUpgradeCount, shredderBonusChance, shredderBonusMaxCards } from './economy.js';
|
||||
import { autoScannerCooldownSeconds } from './cards.js';
|
||||
import { record } from './history.js';
|
||||
|
|
@ -19,6 +19,11 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
let lastConveyorBuildCell = null;
|
||||
let conveyorDragRecordedDirectionEdit = false;
|
||||
let conveyorDragStartCell = null;
|
||||
let conveyorDragMoved = false;
|
||||
const BRANCH_MODES = ['random', 'pass', 'up', 'right', 'down', 'left'];
|
||||
const BRANCH_LABELS = { random: 'RND', pass: 'PASS', up: 'UP', right: 'RT', down: 'DN', left: 'LF' };
|
||||
const BRANCH_MARKER = { w: 48, h: 24, y: 23 };
|
||||
|
||||
function gridExpansionCost() { return expansionCost(game); }
|
||||
|
||||
|
|
@ -128,6 +133,20 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
return false;
|
||||
}
|
||||
|
||||
function scannerPlacementBlocked(col, row, moving = null) {
|
||||
const draft = { col, row };
|
||||
for (const cell of scannerFootprintCells(draft)) {
|
||||
if (!pointInGrid(cell.col, cell.row) || isBlockedCell(cell.col, cell.row)) return true;
|
||||
const k = key(cell.col, cell.row);
|
||||
const existingFarm = farmAt(game, cell.col, cell.row);
|
||||
if (existingFarm) return true;
|
||||
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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function selectedObject() {
|
||||
if (!game.selected) return null;
|
||||
if (game.selected.type === 'eggFarm') return game.eggFarms.find(f => f.id === game.selected.id) || null;
|
||||
|
|
@ -141,12 +160,13 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
if (!obj) return 'Equipment';
|
||||
if (obj.type === 'eggFarm') return `${equipmentName('eggFarm')} #${obj.id}`;
|
||||
if (obj.type === 'scanner') return `${obj.kind === 'auto' ? equipmentName('autoScanner') : equipmentName('manualScanner')} #${obj.id}`;
|
||||
if (obj.type === 'conveyor') return equipmentName('conveyor');
|
||||
if (obj.type === 'conveyor') return game.conveyorMeta.get(obj.id)?.kind === 'boostConveyor' ? equipmentName('boostConveyor') : equipmentName('conveyor');
|
||||
if (obj.type === 'facility') return obj.name;
|
||||
return 'Equipment';
|
||||
}
|
||||
|
||||
function equipmentPrice(hit) {
|
||||
if (hit?.type === 'conveyor') return game.conveyorMeta?.get(hit.oldKey || hit.ref?.id)?.price || equipmentBasePrice(hit);
|
||||
return equipmentBasePrice(hit);
|
||||
}
|
||||
|
||||
|
|
@ -171,8 +191,9 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
if (hit?.type === 'eggFarm' || obj.type === 'eggFarm') {
|
||||
return game.eggFarms.length <= 1 ? 'Cannot sell the last EGG FARM.' : '';
|
||||
}
|
||||
if ((hit?.type === 'facility' || obj.type === 'facility') && ['mixer', 'trash', 'truck'].includes(obj.id)) {
|
||||
const count = Object.values(game.facilities).filter(f => f.id === obj.id).length;
|
||||
if ((hit?.type === 'facility' || obj.type === 'facility') && ['mixer', 'trash', 'truck'].includes(obj.baseId || obj.id)) {
|
||||
const kind = obj.baseId || obj.id;
|
||||
const count = Object.values(game.facilities).filter(f => (f.baseId || f.id) === kind).length;
|
||||
if (count <= 1) return `Cannot sell the last ${obj.shortName || obj.name || obj.id}.`;
|
||||
}
|
||||
return '';
|
||||
|
|
@ -193,9 +214,47 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
if (!meta) return [];
|
||||
if (!Array.isArray(meta.outDirs)) meta.outDirs = [];
|
||||
meta.outDirs = [...new Set(meta.outDirs.filter(dir => ['right', 'left', 'down', 'up'].includes(dir)))];
|
||||
if (!BRANCH_MODES.includes(meta.branchMode)) meta.branchMode = 'random';
|
||||
if (!meta.kind) meta.kind = 'conveyor';
|
||||
if (!meta.speedMultiplier) meta.speedMultiplier = meta.kind === 'boostConveyor' ? 2 : 1;
|
||||
return meta.outDirs;
|
||||
}
|
||||
|
||||
function conveyorNeighborOutDirs(cell) {
|
||||
if (!cell) return [];
|
||||
return DIRS.filter(d => game.conveyorTiles.has(key(cell.col + d.dc, cell.row + d.dr))).map(d => d.name);
|
||||
}
|
||||
|
||||
function neighborPointsIntoCell(cell, dirName) {
|
||||
const d = DIRS.find(item => item.name === dirName);
|
||||
if (!cell || !d) return false;
|
||||
const nk = key(cell.col + d.dc, cell.row + d.dr);
|
||||
const meta = game.conveyorMeta.get(nk);
|
||||
if (!meta) return false;
|
||||
const incoming = d.opposite;
|
||||
const dirs = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))];
|
||||
return dirs.includes(incoming);
|
||||
}
|
||||
|
||||
function reachableOutDirsForKey(k) {
|
||||
if (!game.conveyorTiles.has(k)) return [];
|
||||
const cell = parseKey(k);
|
||||
const meta = game.conveyorMeta.get(k);
|
||||
const explicit = normalizeOutDirs(meta);
|
||||
const neighbors = conveyorNeighborOutDirs(cell);
|
||||
const explicitReachable = [...new Set([...explicit, meta?.dir].filter(Boolean))]
|
||||
.filter(dir => neighbors.includes(dir) && !neighborPointsIntoCell(cell, dir));
|
||||
const inferredExits = neighbors.filter(dir => !neighborPointsIntoCell(cell, dir));
|
||||
const reachable = explicitReachable.length ? explicitReachable : inferredExits;
|
||||
if (meta) {
|
||||
meta.outDirs = explicit.filter(dir => neighbors.includes(dir));
|
||||
if (meta.dir && !neighbors.includes(meta.dir)) meta.dir = meta.outDirs[0] || null;
|
||||
if (meta.branchMode !== 'random' && meta.branchMode !== 'pass' && !reachable.includes(meta.branchMode)) meta.branchMode = 'random';
|
||||
if (meta.branchMode === 'pass' && reachable.length < 4) meta.branchMode = 'random';
|
||||
}
|
||||
return reachable;
|
||||
}
|
||||
|
||||
function markConveyorDirection(from, to) {
|
||||
const dir = directionNameBetweenCells(from, to);
|
||||
if (!dir) return false;
|
||||
|
|
@ -212,6 +271,38 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
return changed;
|
||||
}
|
||||
|
||||
function availableConveyorOutDirs(cell) {
|
||||
if (!cell) return [];
|
||||
return reachableOutDirsForKey(key(cell.col, cell.row));
|
||||
}
|
||||
|
||||
function cycleSingleConveyorDirection(cell) {
|
||||
if (!cell) return false;
|
||||
const k = key(cell.col, cell.row);
|
||||
const meta = game.conveyorMeta.get(k);
|
||||
if (!meta) return false;
|
||||
const choices = availableConveyorOutDirs(cell);
|
||||
if (!choices.length) return false;
|
||||
const explicit = normalizeOutDirs(meta);
|
||||
const current = meta.dir && choices.includes(meta.dir) ? meta.dir : explicit.find(dir => choices.includes(dir));
|
||||
const next = choices[(Math.max(-1, choices.indexOf(current)) + 1) % choices.length];
|
||||
if (!next) return false;
|
||||
record(game);
|
||||
meta.dir = next;
|
||||
meta.outDirs = [next];
|
||||
meta.branchMode = 'random';
|
||||
game.selected = { type: 'conveyor', id: k };
|
||||
const c = cellCenter(cell.col, cell.row);
|
||||
floating(game, c.x, c.y - 22, branchModeLabel(next), THEME.ink);
|
||||
refreshRoutingAfterEdit(game);
|
||||
updatePanels();
|
||||
return true;
|
||||
}
|
||||
|
||||
function conveyorToolDef() {
|
||||
return BALANCE.facilities[game.buildTool === 'boostConveyor' ? 'boostConveyor' : 'conveyor'];
|
||||
}
|
||||
|
||||
function buildConveyorCell(cell, incomingDir = null) {
|
||||
if (!cell) return { ok: false, reason: TEXT.fail.outOfGrid };
|
||||
const { col, row } = cell;
|
||||
|
|
@ -219,7 +310,9 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
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)) return { ok: false, reason: TEXT.fail.cellOccupied };
|
||||
const cost = buildPrice('conveyor', game);
|
||||
const toolDef = conveyorToolDef();
|
||||
const kind = toolDef.id || 'conveyor';
|
||||
const cost = buildPrice(kind, game);
|
||||
if (game.cash < cost) return { ok: false, reason: TEXT.fail.notEnoughCash };
|
||||
record(game);
|
||||
spendCash(game, cost);
|
||||
|
|
@ -231,8 +324,11 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
uses: 0,
|
||||
durability: durabilityCapFor(game, 'conveyor', quality.durabilityBaseMultiplier),
|
||||
maintenanceType: 'conveyor',
|
||||
kind,
|
||||
speedMultiplier: toolDef.speedMultiplier || 1,
|
||||
dir: incomingDir || null,
|
||||
outDirs: [],
|
||||
branchMode: 'random',
|
||||
...quality
|
||||
});
|
||||
game.selected = { type: 'conveyor', id: k };
|
||||
|
|
@ -246,6 +342,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
const exists = game.conveyorTiles.has(currentKey);
|
||||
const previous = lastConveyorBuildCell;
|
||||
if (previous && previous.col === cell.col && previous.row === cell.row) return;
|
||||
if (previous) conveyorDragMoved = true;
|
||||
|
||||
if (exists) {
|
||||
if (previous && markConveyorDirection(previous, cell)) {
|
||||
|
|
@ -280,6 +377,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function beginConveyorDrag(cell) {
|
||||
conveyorDragRecordedDirectionEdit = false;
|
||||
conveyorDragStartCell = cell ? { col: cell.col, row: cell.row } : null;
|
||||
conveyorDragMoved = false;
|
||||
lastConveyorBuildCell = null;
|
||||
handleConveyorDragCell(cell);
|
||||
}
|
||||
|
|
@ -299,6 +398,11 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
}
|
||||
|
||||
function endConveyorDrag() {
|
||||
if (!conveyorDragMoved && conveyorDragStartCell && game.conveyorTiles.has(key(conveyorDragStartCell.col, conveyorDragStartCell.row))) {
|
||||
cycleSingleConveyorDirection(conveyorDragStartCell);
|
||||
}
|
||||
conveyorDragStartCell = null;
|
||||
conveyorDragMoved = false;
|
||||
lastConveyorBuildCell = null;
|
||||
conveyorDragRecordedDirectionEdit = false;
|
||||
}
|
||||
|
|
@ -306,14 +410,14 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
function buildAtCell(cell) {
|
||||
if (!cell) return fail(TEXT.fail.outOfGrid);
|
||||
const { col, row } = cell;
|
||||
if (game.buildTool === 'conveyor') {
|
||||
if (game.buildTool === 'conveyor' || game.buildTool === 'boostConveyor') {
|
||||
const result = buildConveyorCell(cell, null);
|
||||
if (!result.ok) return fail(result.reason || TEXT.fail.cellOccupied);
|
||||
refreshRoutingAfterEdit(game);
|
||||
return;
|
||||
}
|
||||
if (isBlockedCell(col, row)) return fail('Cannot build on blocked ground.');
|
||||
if (isEquipmentCell(col, row)) return fail(TEXT.fail.cellOccupied);
|
||||
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);
|
||||
else if (game.buildTool === 'manualScanner') buildScanner(col, row, 'manual');
|
||||
else if (game.buildTool === 'autoScanner') buildScanner(col, row, 'auto');
|
||||
|
|
@ -321,7 +425,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function buildAtPoint(p) {
|
||||
const cell = pointToCell(p.x, p.y);
|
||||
if (['conveyor', 'eggFarm', 'manualScanner', 'autoScanner'].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);
|
||||
}
|
||||
|
||||
|
|
@ -342,6 +446,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
if (kind === 'manual' && game.scanners.filter(s => s.kind === 'manual').length >= 8) return fail('Manual Scanner limit reached (8 max).');
|
||||
const cost = kind === 'auto' ? buildPrice('autoScanner', game) : buildPrice('manualScanner', game);
|
||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||
if (scannerPlacementBlocked(col, row)) return fail(TEXT.fail.cellOccupied);
|
||||
record(game);
|
||||
spendCash(game, cost);
|
||||
const scanner = applyBuildQuality(createScanner(game, col, row, kind));
|
||||
|
|
@ -354,16 +459,18 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function buildFacility(p, id) {
|
||||
const cost = buildPrice(id, game);
|
||||
if (game.facilities[id]) return fail(TEXT.fail.facilityExists);
|
||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||
const f = applyBuildQuality(createFacility(game, id, p, cost));
|
||||
const storageId = game.facilities[id] ? `${id}:${game.nextId++}` : id;
|
||||
f.baseId = id;
|
||||
f.id = storageId;
|
||||
if (f.entry && isBlockedCell(f.entry.col, f.entry.row)) return fail('Receiver cell is blocked.');
|
||||
if (facilityOverlaps(f)) return fail(TEXT.fail.facilityOverlap);
|
||||
record(game);
|
||||
spendCash(game, cost);
|
||||
game.facilities[id] = f;
|
||||
game.facilities[storageId] = f;
|
||||
refreshRoutingAfterEdit(game);
|
||||
game.selected = { type: 'facility', id };
|
||||
game.selected = { type: 'facility', id: storageId };
|
||||
floating(game, p.x, p.y - 14, `-${yen(cost)}`, THEME.ink);
|
||||
}
|
||||
|
||||
|
|
@ -371,7 +478,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
const resale = resaleValueFor(hit, game);
|
||||
if (resale.amount <= 0) return;
|
||||
refundCash(game, resale.amount);
|
||||
const label = resale.sameBuild ? 'REFUND' : 'SOLD 50%';
|
||||
const label = resale.sameBuild ? 'REFUND' : 'SOLD';
|
||||
floating(game, x, y - 16, `${label} +${yen(resale.amount)}`, THEME.green);
|
||||
}
|
||||
|
||||
|
|
@ -461,6 +568,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
}
|
||||
|
||||
function hideModal() {
|
||||
if (game.cardTargetPick?.mode === 'autoScannerMenu') game.cardTargetPick = null;
|
||||
ui.modal.classList.remove('visible', 'equipment-popover');
|
||||
ui.modal.style.removeProperty('--popover-x');
|
||||
ui.modal.style.removeProperty('--popover-y');
|
||||
|
|
@ -468,13 +576,26 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function showAutoScannerMenu(scanner) {
|
||||
ui.modalTitle.textContent = 'Auto Scanner';
|
||||
ui.modalBody.innerHTML = `<p>Standard auto scanner.</p><ul><li>Role 0: male left, others right.</li><li>Role 1: poop left, others right.</li><li>Cooldown: ${autoScannerCooldownSeconds(scanner).toFixed(1)}s.</li></ul>`;
|
||||
game.cardTargetPick = {
|
||||
pending: true,
|
||||
mode: 'autoScannerMenu',
|
||||
cardId: null,
|
||||
targetKeys: [`scanner:${scanner.id}`],
|
||||
remaining: 1
|
||||
};
|
||||
ui.modalBody.innerHTML = `
|
||||
<div class="equipment-menu-lines compact">
|
||||
<p>Left / right output routing for this AUTO scanner.</p>
|
||||
<p>Current: ${scanner.role === 0 ? 'Male left / others right' : 'Poop left / others right'}</p>
|
||||
<p>Cooldown: ${autoScannerCooldownSeconds(scanner).toFixed(1)}s.</p>
|
||||
</div>`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
const r0 = modalButton('Set Role 0', () => { record(game); scanner.role = 0; hideModal(); updatePanels(); });
|
||||
const r1 = modalButton('Set Role 1', () => { record(game); scanner.role = 1; hideModal(); updatePanels(); });
|
||||
ui.modalActions.append(r0, r1);
|
||||
const r0 = modalButton('Male Left / Others Right', () => { record(game); scanner.role = 0; hideModal(); updatePanels(); }, scanner.role === 0 ? 'facility-action warn' : 'facility-action');
|
||||
const r1 = modalButton('Poop Left / Others Right', () => { record(game); scanner.role = 1; hideModal(); updatePanels(); }, scanner.role === 1 ? 'facility-action warn' : 'facility-action');
|
||||
ui.modalActions.append(r0, r1, modalButton('Close', hideModal, 'facility-action'));
|
||||
positionEquipmentPopover(scanner);
|
||||
ui.modal.classList.add('visible', 'equipment-popover');
|
||||
updatePanels();
|
||||
}
|
||||
|
||||
function formatKeyBinding(binding) {
|
||||
|
|
@ -507,7 +628,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
<div class="equipment-menu-lines compact">
|
||||
<p>Left route key: <b>${left}</b></p>
|
||||
<p>Right route key: <b>${right}</b></p>
|
||||
<p>Role: ${scanner.role === 0 ? 'Male left / others right' : 'Poop left / others right'}</p>
|
||||
${message ? `<p class="cash-positive">${message}</p>` : ''}
|
||||
${conflict ? `<p class="cash-negative">${conflict}</p>` : ''}
|
||||
</div>`;
|
||||
|
|
@ -625,14 +745,64 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
if (f.entry) { const c = cellCenter(f.entry.col, f.entry.row); boxes.push({ id: `facilityPort:${id}`, price: equipmentPrice({ type: 'facility', ref: f }), x: c.x - 18, y: c.y - 18, w: 36, h: 36 }); }
|
||||
}
|
||||
for (const farm of game.eggFarms) { const c = cellCenter(farm.col, farm.row); boxes.push({ id: `eggFarm:${farm.id}`, price: equipmentPrice({ type: 'eggFarm', ref: farm }), x: c.x - 24, y: c.y - 24, w: 48, h: 48 }); }
|
||||
for (const scanner of game.scanners) { const c = scannerCenter(scanner); boxes.push({ id: `scanner:${scanner.id}`, price: equipmentPrice({ type: 'scanner', ref: scanner }), x: c.x - 52, y: c.y - 36, w: 104, h: 72 }); }
|
||||
for (const k of game.conveyorTiles) { const p = parseKey(k); const c = cellCenter(p.col, p.row); boxes.push({ id: `conveyor:${k}`, price: buildPrice('conveyor', game), x: c.x - 22, y: c.y - 22, w: 44, h: 44 }); }
|
||||
for (const scanner of game.scanners) { const c = scannerCenter(scanner); boxes.push({ id: `scanner:${scanner.id}`, price: equipmentPrice({ type: 'scanner', ref: scanner }), x: c.x - 50, y: c.y - 73, w: 100, h: 146 }); }
|
||||
for (const k of game.conveyorTiles) {
|
||||
const p = parseKey(k);
|
||||
const c = cellCenter(p.col, p.row);
|
||||
const meta = game.conveyorMeta.get(k) || {};
|
||||
boxes.push({ id: `conveyor:${k}`, price: meta.price || buildPrice(meta.kind === 'boostConveyor' ? 'boostConveyor' : 'conveyor', game), x: c.x - 22, y: c.y - 22, w: 44, h: 44 });
|
||||
}
|
||||
return boxes;
|
||||
}
|
||||
|
||||
function branchModeLabel(mode) {
|
||||
return BRANCH_LABELS[mode] || BRANCH_LABELS.random;
|
||||
}
|
||||
|
||||
function branchOutDirsForKey(k) {
|
||||
return reachableOutDirsForKey(k);
|
||||
}
|
||||
|
||||
function isBranchKey(k) {
|
||||
const dirs = branchOutDirsForKey(k);
|
||||
return dirs.length >= 3;
|
||||
}
|
||||
|
||||
function branchSwitcherAtPoint(p) {
|
||||
if (game.phase !== 'build' || game.cardDraft?.pending || game.cardTargetPick?.pending) return null;
|
||||
for (const k of game.conveyorTiles) {
|
||||
if (!isBranchKey(k)) continue;
|
||||
const cell = parseKey(k);
|
||||
const c = cellCenter(cell.col, cell.row);
|
||||
const left = c.x - BRANCH_MARKER.w / 2;
|
||||
const top = c.y + BRANCH_MARKER.y - BRANCH_MARKER.h / 2;
|
||||
if (p.x >= left && p.x <= left + BRANCH_MARKER.w && p.y >= top && p.y <= top + BRANCH_MARKER.h) return k;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function cycleBranchModeAtPoint(p) {
|
||||
const k = branchSwitcherAtPoint(p);
|
||||
if (!k) return false;
|
||||
const meta = game.conveyorMeta.get(k);
|
||||
if (!meta) return false;
|
||||
const reachable = reachableOutDirsForKey(k);
|
||||
if (reachable.length < 2) return false;
|
||||
record(game);
|
||||
const current = BRANCH_MODES.includes(meta.branchMode) ? meta.branchMode : 'random';
|
||||
const modes = ['random', ...(reachable.length >= 4 ? ['pass'] : []), ...DIRS.map(d => d.name).filter(mode => reachable.includes(mode))];
|
||||
meta.branchMode = modes[(Math.max(0, modes.indexOf(current)) + 1) % modes.length];
|
||||
game.selected = { type: 'conveyor', id: k };
|
||||
const c = cellCenter(...Object.values(parseKey(k)));
|
||||
floating(game, c.x, c.y - 22, branchModeLabel(meta.branchMode), THEME.ink);
|
||||
updatePanels();
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectedInfoLines(obj) {
|
||||
ensureMaintenanceState(game);
|
||||
const lines = [];
|
||||
const facilityKind = obj?.type === 'facility' ? (obj.baseId || obj.id) : '';
|
||||
if (obj.type === 'eggFarm') {
|
||||
const [min, max] = getSpawnRange(obj);
|
||||
lines.push(`Price: ${yen(equipmentPrice(obj))}`);
|
||||
|
|
@ -644,7 +814,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
lines.push(`Price: ${yen(equipmentPrice(obj))}`);
|
||||
lines.push(`Sale value: ${yen(resaleValueFor({ type: obj.type, ref: obj }, game).amount)}`);
|
||||
lines.push(`Type: ${obj.kind.toUpperCase()} / Standard`);
|
||||
lines.push(`Role: ${obj.role === 0 ? 'Male left / Others right' : 'Poop left / Others right'}`);
|
||||
if (obj.kind === 'auto') lines.push(`Role: ${obj.role === 0 ? 'Male left / Others right' : 'Poop left / Others right'}`);
|
||||
lines.push(`Queue: ${obj.queue.length}`);
|
||||
if (obj.kind === 'auto') lines.push(`Cooldown: ${obj.cooldown.toFixed(1)}s / ${autoScannerCooldownSeconds(obj, game).toFixed(1)}s`);
|
||||
if (obj.kind === 'auto') lines.push(`Durability: ${remainingPercent(obj)}% | Delay x${autoScannerDelayMultiplier(obj).toFixed(2)}`);
|
||||
|
|
@ -656,25 +826,27 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
const c = comp ? game.congestion.get(comp) : null;
|
||||
lines.push(`Price: ${yen(equipmentPrice({ type: 'conveyor', oldKey: obj.id, ref: obj }))}`);
|
||||
lines.push(`Cell: ${obj.id}`);
|
||||
lines.push(`Type: ${meta?.kind === 'boostConveyor' ? 'BOOST BELT' : 'Normal Belt'} | Speed x${Number(meta?.speedMultiplier || 1).toFixed(1)}`);
|
||||
lines.push(`Congestion: ${c ? Math.floor(c.ratio * 100) : 0}%`);
|
||||
lines.push(`Durability: ${remainingPercent({ meta })}% | Speed x${performanceFactor({ meta }).toFixed(2)}`);
|
||||
lines.push('Flow follows drawn directions. Branches choose randomly; full branches are avoided when possible.');
|
||||
lines.push(`Branch mode: ${branchModeLabel(meta?.branchMode || 'random')}`);
|
||||
lines.push('Click the branch marker to cycle Random / Up / Right / Down / Left.');
|
||||
lines.push(`Sale value: ${yen(resaleValueFor({ type: 'conveyor', oldKey: obj.id, ref: obj }, game).amount)}`);
|
||||
} 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)}`); }
|
||||
lines.push(['mixer', 'truck', 'trash'].includes(obj.id) ? `Level: ${obj.level} / no cap` : `Level: ${obj.level}`);
|
||||
if (obj.id === 'truck') lines.push('Durability: none | Always normal');
|
||||
if (['mixer', 'trash'].includes(obj.id)) lines.push(`Durability: ${remainingPercent(obj)}% | Extra delay ${facilityProcessingDelay(game, obj.id).toFixed(2)}s`);
|
||||
if (obj.id === 'mixer') {
|
||||
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') {
|
||||
lines.push(`Income: ${yen(upgradedMixerPrice(game))} per chick | upgrade x${incomeMultiplier(game, 'mixer').toFixed(3)}`);
|
||||
lines.push(`Poop fine: -${yen(mixerPoopPenalty(game))}`);
|
||||
}
|
||||
if (obj.id === 'truck') {
|
||||
if (facilityKind === 'truck') {
|
||||
lines.push(`Income: ${yen(upgradedTruckPrice(game))} per target cargo | upgrade x${incomeMultiplier(game, 'truck').toFixed(3)}`);
|
||||
lines.push(`Poop shipment fine: -${yen(truckPoopPenalty(game))}`);
|
||||
}
|
||||
if (obj.id === 'trash') {
|
||||
if (facilityKind === 'trash') {
|
||||
const cards = shredderUpgradeCount(game);
|
||||
const chance = shredderBonusChance(cards);
|
||||
lines.push(`Bonus cards: ${cards}/${shredderBonusMaxCards()}`);
|
||||
|
|
@ -696,13 +868,14 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function flavorText(obj) {
|
||||
if (!obj) return '';
|
||||
if (obj.type === 'conveyor' && game.conveyorMeta.get(obj.id)?.kind === 'boostConveyor') return 'Moves chicks twice as fast. Wears down like a normal belt.';
|
||||
if (obj.type === 'conveyor') return 'A stubborn belt tile. It only cares about the next cell.';
|
||||
if (obj.type === 'eggFarm') return 'A tiny gatehouse producing questionable eggs on schedule.';
|
||||
if (obj.type === 'scanner' && obj.kind === 'auto') return 'An automated judge. Faster than hands, still very sure of itself.';
|
||||
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.id === 'mixer') return 'Male chicks become revenue here. Do not feed it poop.';
|
||||
if (obj.type === 'facility' && obj.id === 'trash') return 'Poop goes in. Sometimes coins come out, for reasons best left unaudited.';
|
||||
if (obj.type === 'facility' && obj.id === 'truck') return 'The shipping endpoint. Correct cargo pays; wrong cargo complains.';
|
||||
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.';
|
||||
return 'Factory equipment.';
|
||||
}
|
||||
|
||||
|
|
@ -718,6 +891,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
selectedInfoLines, flavorText, disconnectedWarningFor,
|
||||
gridExpansionCost, buyGridExpansion, expansionOfferAtPoint,
|
||||
beginConveyorDrag, continueConveyorDrag, endConveyorDrag,
|
||||
branchSwitcherAtPoint, cycleBranchModeAtPoint,
|
||||
removeSelected, switchScannerRole, showManualScannerMenu,
|
||||
showAutoScannerMenu, showSelectedMenu, setBuildTool, fail,
|
||||
isEquipmentCell, equipmentHitBoxes,
|
||||
|
|
|
|||
|
|
@ -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 } from './maintenance.js';
|
||||
import { averageConveyorPerformance, autoScannerDelayMultiplier, ensureMaintenanceState, repairAllEquipment } from './maintenance.js';
|
||||
import { scannerCenter, refreshRoutingAfterEdit } from './routing.js';
|
||||
|
||||
const COMMON_WEIGHT = CARD_BALANCE.commonWeight;
|
||||
|
|
@ -31,6 +31,7 @@ export function ensureCardState(game) {
|
|||
if (game.cardEffects.drawBonus != null) delete game.cardEffects.drawBonus;
|
||||
if (!game.cardDraft) game.cardDraft = { pending: false, choices: [], rerolls: 0, picksRemaining: 0 };
|
||||
if (game.cardDraft.picksRemaining == null) game.cardDraft.picksRemaining = game.cardDraft.pending ? 1 : 0;
|
||||
if (game.cardDraft.freeRerolls == null) game.cardDraft.freeRerolls = 0;
|
||||
return game.cardEffects;
|
||||
}
|
||||
|
||||
|
|
@ -47,8 +48,9 @@ export function extraEggOutletCount(_game, farm = null) {
|
|||
export function conveyorSpeedForGame(game, conveyorKey = null) {
|
||||
const effects = ensureCardState(game);
|
||||
const speed = CONVEYOR_SPEED * Math.pow(BEARING_SPEED_MULTIPLIER, Math.max(0, effects.bearing || 0));
|
||||
const beltMultiplier = conveyorKey ? Math.max(0.1, Number(game?.conveyorMeta?.get?.(conveyorKey)?.speedMultiplier) || 1) : 1;
|
||||
const perf = conveyorKey ? 1 : averageConveyorPerformance(game);
|
||||
return Math.min(CONVEYOR_SPEED_MAX, speed * perf);
|
||||
return Math.min(CONVEYOR_SPEED_MAX, speed * perf) * beltMultiplier;
|
||||
}
|
||||
|
||||
export function autoScannerCooldownSeconds(scanner, game = null) {
|
||||
|
|
@ -155,9 +157,11 @@ 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 === 'mixer') return game.facilities.mixer ? [game.facilities.mixer] : [];
|
||||
if (card.target === 'truck') return game.facilities.truck ? [game.facilities.truck] : [];
|
||||
if (card.target === 'trash') return game.facilities.trash && rawShredderUpgradeCount(game) < shredderBonusMaxCards() ? [game.facilities.trash] : [];
|
||||
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()
|
||||
? Object.values(game.facilities || {}).filter(f => (f.baseId || f.id) === 'trash')
|
||||
: [];
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -190,17 +194,22 @@ function availableCards(game) {
|
|||
});
|
||||
}
|
||||
|
||||
function rarityWeight(card) {
|
||||
function cardWeight(game, card) {
|
||||
if (card.weightBase != null) {
|
||||
const held = effectCount(game, card.id);
|
||||
return Math.max(0, Number(card.weightBase) - Math.max(0, Number(card.weightLossPerCopy) || 0) * held);
|
||||
}
|
||||
if (card.rarity === 'ultraRare') return ULTRA_RARE_WEIGHT;
|
||||
if (card.rarity === 'rare') return RARE_WEIGHT;
|
||||
return COMMON_WEIGHT;
|
||||
}
|
||||
|
||||
function weightedPick(pool) {
|
||||
const total = pool.reduce((sum, card) => sum + rarityWeight(card), 0);
|
||||
function weightedPick(game, pool) {
|
||||
const total = pool.reduce((sum, card) => sum + cardWeight(game, card), 0);
|
||||
if (total <= 0) return pool[Math.floor(Math.random() * pool.length)];
|
||||
let roll = Math.random() * total;
|
||||
for (const card of pool) {
|
||||
roll -= rarityWeight(card);
|
||||
roll -= cardWeight(game, card);
|
||||
if (roll <= 0) return card;
|
||||
}
|
||||
return pool[pool.length - 1];
|
||||
|
|
@ -247,7 +256,7 @@ export function dealCards(game, count = BASE_DRAFT_SIZE) {
|
|||
let pool = [...source];
|
||||
while (choices.length < count && source.length) {
|
||||
if (!pool.length) pool = [...source];
|
||||
const picked = weightedPick(pool);
|
||||
const picked = weightedPick(game, pool);
|
||||
choices.push(picked);
|
||||
const i = pool.findIndex(card => card.id === picked.id);
|
||||
if (i >= 0) pool.splice(i, 1);
|
||||
|
|
@ -277,7 +286,7 @@ function boundsForTarget(target) {
|
|||
}
|
||||
if (target.type === 'scanner') {
|
||||
const c = scannerCenter(target);
|
||||
return { x: c.x - 58, y: c.y - 42, w: 116, h: 84, cx: c.x, cy: c.y };
|
||||
return { x: c.x - 54, y: c.y - 77, w: 108, h: 154, cx: c.x, cy: c.y };
|
||||
}
|
||||
if (target.type === 'facility') {
|
||||
return { x: target.x, y: target.y, w: target.w, h: target.h, cx: target.x + target.w / 2, cy: target.y + target.h / 2 };
|
||||
|
|
@ -287,6 +296,13 @@ function boundsForTarget(target) {
|
|||
|
||||
export function cardTargetBounds(game) {
|
||||
if (!game.cardTargetPick?.pending) return [];
|
||||
if (game.cardTargetPick.mode === 'autoScannerMenu') {
|
||||
const allowed = new Set(game.cardTargetPick.targetKeys || []);
|
||||
return game.scanners
|
||||
.filter(scanner => scanner.kind === 'auto' && allowed.has(targetKey(scanner)))
|
||||
.map(scanner => ({ key: targetKey(scanner), label: 'AUTO ROUTE SETTINGS', target: scanner, bounds: boundsForTarget(scanner) }))
|
||||
.filter(item => item.bounds);
|
||||
}
|
||||
const allowed = new Set(game.cardTargetPick.targetKeys || []);
|
||||
return targetsForCard(game, game.cardTargetPick.cardId)
|
||||
.filter(target => allowed.has(targetKey(target)))
|
||||
|
|
@ -344,6 +360,16 @@ function applyInstantCard(game, card) {
|
|||
if (card.id === 'preventiveMaintenance') inc('preventiveMaintenance');
|
||||
if (card.id === 'dudFilter') inc('dudFilter');
|
||||
if (card.id === 'durabilityCoating') { inc('durabilityCoating'); ensureMaintenanceState(game); }
|
||||
if (card.id === 'sparePartsBin') {
|
||||
inc('sparePartsBin');
|
||||
const repaired = repairAllEquipment(game, 0.02);
|
||||
game.stats.sparePartsRepair = (game.stats.sparePartsRepair || 0) + repaired;
|
||||
game.totals.sparePartsRepair = (game.totals.sparePartsRepair || 0) + repaired;
|
||||
floating(game, GRID.x + GRID.cols * GRID.cell / 2, GRID.y - 30, 'REPAIR +2%', THEME.green);
|
||||
}
|
||||
if (card.id === 'dudRefund') inc('dudRefund');
|
||||
if (card.id === 'freeReroll') inc('freeReroll');
|
||||
if (card.id === 'composter') inc('composter');
|
||||
if (card.id === 'flattery') inc('fairiesFlatteryNext');
|
||||
if (card.id === 'usedMachine') effects.usedMachineActive = true;
|
||||
if (card.id === 'newMachine') effects.usedMachineActive = false;
|
||||
|
|
@ -432,6 +458,10 @@ function cardDescription(game, card) {
|
|||
return `Held ${held}. DUD chance per card ${formatPercent(before)} -> ${formatPercent(after)}.`;
|
||||
}
|
||||
if (card.id === 'durabilityCoating') return `Held ${e.durabilityCoating || 0}. After pick: maximum durability ${formatPercent(Math.pow(1.03, (e.durabilityCoating || 0) + 1))}.`;
|
||||
if (card.id === 'sparePartsBin') return `Held ${e.sparePartsBin || 0}. Immediately restores 2% durability to every maintained machine. Offer weight: ${Math.max(0, 100 - 2 * Math.max(0, e.sparePartsBin || 0))}.`;
|
||||
if (card.id === 'dudRefund') return `Held ${e.dudRefund || 0}. After pick: DUD clicks award ${yen(50 * ((e.dudRefund || 0) + 1))}.`;
|
||||
if (card.id === 'freeReroll') return `Held ${e.freeReroll || 0}. After pick: each card screen starts with ${(e.freeReroll || 0) + 1} free reroll${(e.freeReroll || 0) + 1 === 1 ? '' : 's'}.`;
|
||||
if (card.id === 'composter') return `Held ${e.composter || 0}. After pick: SHREDDER poop grants ${yen(3 * ((e.composter || 0) + 1))}.`;
|
||||
if (card.id === 'flattery') return `Next Fairies tribute -50%. Held for next tribute: ${e.fairiesFlatteryNext || 0}.`;
|
||||
if (card.id === 'usedMachine') return 'Enable Used Machines: future equipment costs 50%, has no SELL refund, and starts with 60% durability.';
|
||||
if (card.id === 'newMachine') return 'Cancel Used Machines. Future equipment returns to normal price, refund, and durability.';
|
||||
|
|
@ -456,7 +486,7 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
function prepareDraft() {
|
||||
ensureCardState(game);
|
||||
game.cardTargetPick = null;
|
||||
game.cardDraft = { pending: true, choices: dealCards(game, BASE_DRAFT_SIZE), rerolls: 0, picksRemaining: 1 };
|
||||
game.cardDraft = { pending: true, choices: dealCards(game, BASE_DRAFT_SIZE), rerolls: 0, freeRerolls: effectCount(game, 'freeReroll'), picksRemaining: 1 };
|
||||
}
|
||||
|
||||
function finishDraft() {
|
||||
|
|
@ -496,6 +526,13 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
}
|
||||
|
||||
function chooseDud(card, buttonEl) {
|
||||
const refund = effectCount(game, 'dudRefund') * 50;
|
||||
if (refund > 0) {
|
||||
const amount = applyRevenue(game, refund);
|
||||
game.stats.dudRefundIncome = (game.stats.dudRefundIncome || 0) + amount;
|
||||
game.totals.dudRefundIncome = (game.totals.dudRefundIncome || 0) + amount;
|
||||
floating(game, GRID.x + GRID.cols * GRID.cell / 2, GRID.y - 30, `DUD REFUND +${yen(amount)}`, THEME.green);
|
||||
}
|
||||
shake(game, 34, 0.62);
|
||||
if (document.body?.animate) {
|
||||
document.body.animate([
|
||||
|
|
@ -566,8 +603,7 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
const card = cardById(game.cardTargetPick.cardId);
|
||||
const target = targetAtPoint(game, p);
|
||||
if (!card || !target) {
|
||||
const label = game.cardTargetPick.mode === 'blockedCell' ? 'SELECT BLOCKED CELL' : 'SELECT UPGRADE TARGET';
|
||||
floating(game, p.x, p.y - 18, label, THEME.danger);
|
||||
cancelTargetPick();
|
||||
return true;
|
||||
}
|
||||
if (card.id === 'dynamite') {
|
||||
|
|
@ -587,6 +623,14 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
}
|
||||
|
||||
function doReroll() {
|
||||
const free = Math.max(0, game.cardDraft?.freeRerolls || 0);
|
||||
if (free > 0) {
|
||||
game.cardDraft.freeRerolls = free - 1;
|
||||
game.cardDraft.rerolls += 1;
|
||||
redrawChoices(BASE_DRAFT_SIZE);
|
||||
showDraft();
|
||||
return;
|
||||
}
|
||||
const cost = rerollCost(game);
|
||||
if (game.cash < cost) return;
|
||||
applyPenalty(game, cost);
|
||||
|
|
@ -617,6 +661,7 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
if (!game.cardDraft.choices?.length) redrawChoices(BASE_DRAFT_SIZE);
|
||||
const choices = game.cardDraft.choices || [];
|
||||
const cost = rerollCost(game);
|
||||
const free = Math.max(0, game.cardDraft?.freeRerolls || 0);
|
||||
const remaining = Math.max(1, game.cardDraft.picksRemaining || 1);
|
||||
ui.modalTitle.textContent = 'Choose Upgrade Card';
|
||||
ui.modalBody.innerHTML = `<p class="muted-card-note">Choose ${remaining} card${remaining === 1 ? '' : 's'} before Build phase. Dud cards can be flicked away without consuming a pick. Equipment cards target machines; Dynamite targets blocked cells.</p><div id="cardChoices" class="card-choices"></div>`;
|
||||
|
|
@ -626,11 +671,18 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
const reroll = document.createElement('button');
|
||||
reroll.type = 'button';
|
||||
reroll.className = 'facility-action warn reroll-button';
|
||||
reroll.textContent = `Reroll ${yen(cost)}`;
|
||||
reroll.disabled = game.cash < cost;
|
||||
reroll.title = reroll.disabled ? 'Not enough cash' : `Reroll count today: ${(game.cardDraft.rerolls || 0) + 1}`;
|
||||
reroll.textContent = free > 0 ? `Reroll (FREE x${free})` : `Reroll ${yen(cost)}`;
|
||||
reroll.disabled = free <= 0 && game.cash < cost;
|
||||
reroll.title = free > 0 ? `${free} free reroll${free === 1 ? '' : 's'} remaining.` : (reroll.disabled ? 'Not enough cash' : '');
|
||||
reroll.addEventListener('click', doReroll);
|
||||
ui.modalActions.appendChild(reroll);
|
||||
const skip = document.createElement('button');
|
||||
skip.type = 'button';
|
||||
skip.className = 'facility-action';
|
||||
skip.textContent = 'No Card';
|
||||
skip.title = 'Skip this upgrade pick and enter Build phase.';
|
||||
skip.addEventListener('click', finishDraft);
|
||||
ui.modalActions.appendChild(skip);
|
||||
ui.modal.classList.remove('equipment-popover');
|
||||
ui.modal.classList.add('visible');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { DIRS, GRID, THEME } from '../core/config.js';
|
||||
import { createChick } from '../core/entities.js';
|
||||
import { nextSpawnDelay } from '../core/state.js';
|
||||
import { key, parseKey, pointToCell, cellCenter, randomBetween, yen, inGrid } from '../core/utils.js';
|
||||
import { key, parseKey, pointToCell, cellCenter, randomBetween, yen, inGrid, directionNameBetweenCells } from '../core/utils.js';
|
||||
import { scannerById, scannerBySlot, scannerCenter, scannerConnector, nearestConveyorKey, buildConveyorComponents, autoSideFor, ensureFactoryGraph } from './routing.js';
|
||||
import { applyMixerIncome, applyMixerPoopFine, applyTruckPoopFine, applyWrongTruckFine, upgradedTruckPrice, applyShredderBonus, applyRevenue } from './economy.js';
|
||||
import { autoScannerCooldownSeconds, eggProductionDelayMultiplier, extraEggOutletCount } from './cards.js';
|
||||
|
|
@ -104,7 +104,6 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
}
|
||||
updateScannerQueues(dt);
|
||||
updateCongestion();
|
||||
if (game.timeLeft > 0 || game.shutdownTimeLeft > 0) checkCongestionExplosions();
|
||||
if (game.timeLeft <= 0) {
|
||||
if (game.chicks.length === 0) completeTurn();
|
||||
else if (game.shutdownTimeLeft <= 0) blowOffRemainingForCleanup();
|
||||
|
|
@ -172,7 +171,14 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
const dirs = [];
|
||||
if (Array.isArray(meta.outDirs)) dirs.push(...meta.outDirs);
|
||||
if (meta.dir) dirs.push(meta.dir);
|
||||
return [...new Set(dirs)].filter(dir => DIRS.some(d => d.name === dir));
|
||||
const explicit = [...new Set(dirs)].filter(dir => DIRS.some(d => d.name === dir));
|
||||
const cell = parseKey(cellKey);
|
||||
const inferred = DIRS
|
||||
.filter(d => movementOptionFromDir(cell, d.name))
|
||||
.map(d => d.name);
|
||||
const cleanExplicit = explicit.filter(dir => inferred.includes(dir) && !neighborPointsIntoCell(cell, dir));
|
||||
const inferredExits = inferred.filter(dir => !neighborPointsIntoCell(cell, dir));
|
||||
return cleanExplicit.length ? cleanExplicit : inferredExits;
|
||||
}
|
||||
|
||||
function dirByName(name) {
|
||||
|
|
@ -180,7 +186,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
}
|
||||
|
||||
function scannerAtBodyCell(col, row) {
|
||||
return game.scanners.find(scanner => scanner.col === col && scanner.row === row) || null;
|
||||
return game.scanners.find(scanner => scanner.col <= col && col <= scanner.col + 1 && scanner.row - 1 <= row && row <= scanner.row + 1) || null;
|
||||
}
|
||||
|
||||
function scannerReceivingFrom(cell, dirName) {
|
||||
|
|
@ -196,6 +202,10 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
return Object.entries(game.facilities || {}).find(([, f]) => f?.entry && f.entry.col === cell.col && f.entry.row === cell.row) || null;
|
||||
}
|
||||
|
||||
function facilityKind(f) {
|
||||
return f?.baseId || f?.id || '';
|
||||
}
|
||||
|
||||
function dirExitsToFacility(facility, dirName) {
|
||||
return (facility.side === 'left' && dirName === 'left')
|
||||
|| (facility.side === 'right' && dirName === 'right')
|
||||
|
|
@ -219,9 +229,32 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
return null;
|
||||
}
|
||||
|
||||
function pickMovementOption(chick, cell, dirs) {
|
||||
function neighborPointsIntoCell(cell, dirName) {
|
||||
const d = dirByName(dirName);
|
||||
if (!d) return false;
|
||||
const meta = game.conveyorMeta?.get?.(key(cell.col + d.dc, cell.row + d.dr));
|
||||
if (!meta) return false;
|
||||
const names = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))];
|
||||
return names.includes(d.opposite);
|
||||
}
|
||||
|
||||
function optionAvailableForBranch(chick, opt) {
|
||||
return opt && (opt.type === 'facility' || !targetBlockedByChick(chick, opt.target));
|
||||
}
|
||||
|
||||
function pickMovementOption(chick, cell, dirs, branchMode = 'random') {
|
||||
const candidates = dirs.map(dir => movementOptionFromDir(cell, dir)).filter(Boolean);
|
||||
if (!candidates.length) return null;
|
||||
if (branchMode === 'pass') {
|
||||
const previous = chick.prevConveyorCell ? parseKey(chick.prevConveyorCell) : null;
|
||||
const forward = previous ? directionNameBetweenCells(previous, cell) : null;
|
||||
const pass = forward ? candidates.find(opt => opt.dirName === forward) : null;
|
||||
if (optionAvailableForBranch(chick, pass)) return pass;
|
||||
}
|
||||
if (branchMode && branchMode !== 'random') {
|
||||
const preferred = candidates.find(opt => opt.dirName === branchMode);
|
||||
if (optionAvailableForBranch(chick, preferred)) return preferred;
|
||||
}
|
||||
const moving = candidates.filter(opt => opt.type !== 'facility');
|
||||
const empty = moving.filter(opt => !targetBlockedByChick(chick, opt.target));
|
||||
const pool = empty.length ? empty : candidates.filter(opt => opt.type === 'facility');
|
||||
|
|
@ -242,24 +275,27 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
if (!cell) return removeChick(index, 'OFF GRID');
|
||||
const currentKey = key(cell.col, cell.row);
|
||||
if (!game.conveyorTiles.has(currentKey)) return removeChick(index, 'OFF BELT');
|
||||
const meta = game.conveyorMeta?.get(currentKey) || {};
|
||||
const dirs = conveyorOutDirNames(currentKey);
|
||||
if (!dirs.length) {
|
||||
chick.stoppedTimer = 0.25;
|
||||
return;
|
||||
}
|
||||
const option = pickMovementOption(chick, cell, dirs);
|
||||
const option = pickMovementOption(chick, cell, dirs, meta.branchMode || 'random');
|
||||
if (!option) {
|
||||
chick.stoppedTimer = 0.25;
|
||||
return;
|
||||
}
|
||||
if (option.type === 'facility') {
|
||||
if (option.facilityId === 'mixer') resolveMixer(index);
|
||||
else if (option.facilityId === 'truck') resolveTruck(index);
|
||||
else if (option.facilityId === 'trash') resolveTrash(index);
|
||||
const kind = facilityKind(option.facility);
|
||||
if (kind === 'mixer') resolveMixer(index, option.facility);
|
||||
else if (kind === 'truck') resolveTruck(index, option.facility);
|
||||
else if (kind === 'trash') resolveTrash(index, option.facility);
|
||||
else removeChick(index, 'DONE');
|
||||
return;
|
||||
}
|
||||
if (option.type === 'scanner') chick.pendingScannerId = option.scanner.id;
|
||||
chick.prevConveyorCell = currentKey;
|
||||
setSingleSegmentRoute(chick, option.target);
|
||||
}
|
||||
|
||||
|
|
@ -497,8 +533,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
return chick;
|
||||
}
|
||||
|
||||
function resolveMixer(index) {
|
||||
const mixer = game.facilities.mixer;
|
||||
function resolveMixer(index, mixer = game.facilities.mixer) {
|
||||
if (mixer?.processingCooldown > 0) {
|
||||
const chick = game.chicks[index];
|
||||
if (chick) chick.stoppedTimer = 0.25;
|
||||
|
|
@ -508,8 +543,8 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
if (!chick) return;
|
||||
const { x, y } = chick;
|
||||
countMixer(game);
|
||||
recordFacilityProcess(game, 'mixer');
|
||||
setFacilityCooldownAfterProcess(game, 'mixer');
|
||||
recordFacilityProcess(game, mixer);
|
||||
setFacilityCooldownAfterProcess(game, mixer);
|
||||
if (chick.sex === 'poop') {
|
||||
const penalty = applyMixerPoopFine(game);
|
||||
countPoopDestination(game, 'mixer');
|
||||
|
|
@ -525,11 +560,11 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
floating(game, x, y - 18, `+${yen(amount)}`, THEME.green);
|
||||
}
|
||||
|
||||
function resolveTruck(index) {
|
||||
function resolveTruck(index, truck = game.facilities.truck) {
|
||||
const chick = takeChick(index);
|
||||
if (!chick) return;
|
||||
const { x, y } = chick;
|
||||
addTruckCargo(chick.sex);
|
||||
addTruckCargo(chick.sex, truck);
|
||||
truckLoadEffect(game, x, y, chick.sex);
|
||||
countTruckCargo(game, chick.sex);
|
||||
const target = truckTarget(game);
|
||||
|
|
@ -567,8 +602,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
}
|
||||
}
|
||||
|
||||
function resolveTrash(index) {
|
||||
const trash = game.facilities.trash;
|
||||
function resolveTrash(index, trash = game.facilities.trash) {
|
||||
if (trash?.processingCooldown > 0) {
|
||||
const chick = game.chicks[index];
|
||||
if (chick) chick.stoppedTimer = 0.25;
|
||||
|
|
@ -579,13 +613,20 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
const { x, y } = chick;
|
||||
shredEffect(game, x, y, chick.sex);
|
||||
countTrash(game);
|
||||
recordFacilityProcess(game, 'trash');
|
||||
setFacilityCooldownAfterProcess(game, 'trash');
|
||||
recordFacilityProcess(game, trash);
|
||||
setFacilityCooldownAfterProcess(game, trash);
|
||||
const bonus = applyShredderBonus(game);
|
||||
const bonusText = bonus.amount > 0 ? ` +${yen(bonus.amount)}` : '';
|
||||
if (chick.sex === 'poop') {
|
||||
countPoopDestination(game, 'trash');
|
||||
countCorrect(game);
|
||||
const compost = Math.max(0, Number(game.cardEffects?.composter) || 0) * 3;
|
||||
if (compost > 0) {
|
||||
const compostAmount = applyRevenue(game, compost);
|
||||
game.stats.composterIncome = (game.stats.composterIncome || 0) + compostAmount;
|
||||
game.totals.composterIncome = (game.totals.composterIncome || 0) + compostAmount;
|
||||
floating(game, x, y - 36, `COMPOST +${yen(compostAmount)}`, THEME.green);
|
||||
}
|
||||
floating(game, x, y - 18, `CLEAN${bonusText}`, bonus.amount > 0 ? THEME.green : THEME.green);
|
||||
} else {
|
||||
countMistake(game);
|
||||
|
|
@ -599,8 +640,8 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
floating(game, chick.x, chick.y - 12, label, THEME.muted);
|
||||
}
|
||||
|
||||
function addTruckCargo(sex) {
|
||||
const t = game.facilities.truck;
|
||||
function addTruckCargo(sex, truck = game.facilities.truck) {
|
||||
const t = truck || game.facilities.truck;
|
||||
if (!t) return;
|
||||
game.truckCargo.push({ sex, x: randomBetween(22, t.w - 22), y: randomBetween(66, t.h - 24) });
|
||||
if (game.truckCargo.length > 45) game.truckCargo.shift();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function explosionDamageMultiplier(game) {
|
|||
export function equipmentBasePrice(objOrHit) {
|
||||
const obj = objOrHit?.ref || objOrHit || {};
|
||||
const type = objOrHit?.type || obj.type;
|
||||
if (type === 'conveyor') return FACILITY_DEFS.conveyor.price;
|
||||
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.id]?.price || 0;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ export function restore(game, text) {
|
|||
if (game.cardEffects.drawBonus != null) delete game.cardEffects.drawBonus;
|
||||
game.cardDraft = data.cardDraft || { pending: false, choices: [], rerolls: 0, picksRemaining: 0 };
|
||||
if (game.cardDraft.picksRemaining == null) game.cardDraft.picksRemaining = game.cardDraft.pending ? 1 : 0;
|
||||
if (game.cardDraft.freeRerolls == null) game.cardDraft.freeRerolls = 0;
|
||||
game.cardTargetPick = data.cardTargetPick || null;
|
||||
game.groupDrag = null;
|
||||
game.selectionBox = null;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ export function ensureMaintenanceState(game) {
|
|||
for (const [k, meta] of game.conveyorMeta || []) {
|
||||
if (meta.uses == null) meta.uses = 0;
|
||||
if (meta.maintenanceType == null) meta.maintenanceType = 'conveyor';
|
||||
if (!meta.kind) meta.kind = 'conveyor';
|
||||
if (!meta.speedMultiplier) meta.speedMultiplier = meta.kind === 'boostConveyor' ? 2 : 1;
|
||||
if (!meta.branchMode) meta.branchMode = 'random';
|
||||
const desired = durabilityCapFor(game, 'conveyor', qualityMultiplierOf(meta));
|
||||
if (meta.durability == null || meta.durability < desired) meta.durability = desired;
|
||||
}
|
||||
|
|
@ -138,9 +141,20 @@ export function eggSpawnDelayMultiplier(farm) {
|
|||
return delayMultiplier(farm);
|
||||
}
|
||||
|
||||
export function facilityProcessingDelay(game, id) {
|
||||
function facilityKind(target) {
|
||||
return target?.baseId || target?.id || target;
|
||||
}
|
||||
|
||||
function facilityFromTarget(game, target) {
|
||||
if (!target) return null;
|
||||
if (typeof target === 'object') return target;
|
||||
return game.facilities?.[target] || null;
|
||||
}
|
||||
|
||||
export function facilityProcessingDelay(game, target) {
|
||||
ensureMaintenanceState(game);
|
||||
const f = game.facilities?.[id];
|
||||
const f = facilityFromTarget(game, target);
|
||||
const id = facilityKind(f || target);
|
||||
if (!f || !['mixer', 'trash'].includes(id)) return 0;
|
||||
attachMaintenance(f, id);
|
||||
const t = Math.max(0, delayMultiplier(f) - 1);
|
||||
|
|
@ -169,25 +183,26 @@ export function recordAutoScan(game, scanner) {
|
|||
scanner.maintenance.uses = Math.min(scanner.maintenance.durability, (scanner.maintenance.uses || 0) + degradationUseMultiplier(game));
|
||||
}
|
||||
|
||||
export function recordFacilityProcess(game, id) {
|
||||
export function recordFacilityProcess(game, target) {
|
||||
ensureMaintenanceState(game);
|
||||
const f = game.facilities?.[id];
|
||||
const f = facilityFromTarget(game, target);
|
||||
const id = facilityKind(f || target);
|
||||
if (!f || !['mixer', 'trash'].includes(id)) return;
|
||||
attachMaintenance(f, id);
|
||||
f.maintenance.uses = Math.min(f.maintenance.durability, (f.maintenance.uses || 0) + degradationUseMultiplier(game));
|
||||
}
|
||||
|
||||
export function setFacilityCooldownAfterProcess(game, id) {
|
||||
const f = game.facilities?.[id];
|
||||
export function setFacilityCooldownAfterProcess(game, target) {
|
||||
const f = facilityFromTarget(game, target);
|
||||
if (!f) return 0;
|
||||
const delay = facilityProcessingDelay(game, id);
|
||||
const delay = facilityProcessingDelay(game, f);
|
||||
f.processingCooldown = Math.max(f.processingCooldown || 0, delay);
|
||||
return delay;
|
||||
}
|
||||
|
||||
export function updateProcessingCooldowns(game, dt) {
|
||||
for (const id of ['mixer', 'trash']) {
|
||||
const f = game.facilities?.[id];
|
||||
for (const f of Object.values(game.facilities || {})) {
|
||||
if (!['mixer', 'trash'].includes(f.baseId || f.id)) continue;
|
||||
if (f?.processingCooldown > 0) f.processingCooldown = Math.max(0, f.processingCooldown - dt);
|
||||
}
|
||||
}
|
||||
|
|
@ -195,7 +210,7 @@ export function updateProcessingCooldowns(game, dt) {
|
|||
export function equipmentMaintenanceTargets(game) {
|
||||
ensureMaintenanceState(game);
|
||||
const targets = [];
|
||||
for (const [k, meta] of game.conveyorMeta || []) targets.push({ type: 'conveyor', key: k, meta, label: `Belt ${k}`, center: cellCenter(parseKey(k).col, parseKey(k).row) });
|
||||
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 id of ['mixer', 'trash']) {
|
||||
|
|
@ -211,6 +226,18 @@ function targetDurability(t) { return t.meta ? durabilityOf(t) : durabilityOf(t.
|
|||
function reduceTargetUses(t, amount) { setUses(t.meta ? t : t.ref, Math.max(0, targetUses(t) - amount)); }
|
||||
function targetKey(t) { return t.type === 'conveyor' ? `conveyor:${t.key}` : `${t.type}:${t.ref?.id || t.ref?.type || t.type}`; }
|
||||
|
||||
export function repairAllEquipment(game, percent = 0.02) {
|
||||
ensureMaintenanceState(game);
|
||||
const pct = Math.max(0, Number(percent) || 0);
|
||||
let repaired = 0;
|
||||
for (const t of equipmentMaintenanceTargets(game)) {
|
||||
const before = targetUses(t);
|
||||
reduceTargetUses(t, targetDurability(t) * pct);
|
||||
repaired += Math.max(0, before - targetUses(t)) / targetDurability(t) * 100;
|
||||
}
|
||||
return repaired;
|
||||
}
|
||||
|
||||
export function hireRepairmanForNextDay(game) {
|
||||
ensureMaintenanceState(game);
|
||||
if (game.repairman.hiredForNextDay) return { ok: false, reason: 'Repairman already hired.' };
|
||||
|
|
|
|||
|
|
@ -5,11 +5,29 @@ import { key, parseKey, inGrid, cellCenter, distance, sameCell } from '../core/u
|
|||
// -----------------------------------------------------------------------------
|
||||
// Object lookup
|
||||
// -----------------------------------------------------------------------------
|
||||
export function scannerCenter(scanner) { return cellCenter(scanner.col, scanner.row); }
|
||||
export function scannerFootprintCells(scanner) {
|
||||
if (!scanner) return [];
|
||||
return [
|
||||
{ col: scanner.col, row: scanner.row - 1 },
|
||||
{ col: scanner.col + 1, row: scanner.row - 1 },
|
||||
{ col: scanner.col, row: scanner.row },
|
||||
{ col: scanner.col + 1, row: scanner.row },
|
||||
{ col: scanner.col, row: scanner.row + 1 },
|
||||
{ col: scanner.col + 1, row: scanner.row + 1 }
|
||||
];
|
||||
}
|
||||
export function scannerCenter(scanner) {
|
||||
const c = cellCenter(scanner.col, scanner.row);
|
||||
return { x: c.x + GRID.cell / 2, y: c.y };
|
||||
}
|
||||
export function farmAt(game, col, row) { return game.eggFarms.find(f => f.col === col && f.row === row) || null; }
|
||||
export function scannerAt(game, col, row) { return game.scanners.find(s => s.col === col && s.row === row) || null; }
|
||||
export function scannerAt(game, col, row) { return game.scanners.find(s => scannerFootprintCells(s).some(p => p.col === col && p.row === row)) || null; }
|
||||
export function scannerById(game, id) { return game.scanners.find(s => s.id === id) || null; }
|
||||
export function scannerBySlot(game, slot) { return game.scanners.find(s => s.kind === 'manual' && s.slot === slot) || null; }
|
||||
function facilityKind(f) { return f?.baseId || f?.id || ''; }
|
||||
function facilityEntriesForKind(game, kind) {
|
||||
return Object.entries(game.facilities || {}).filter(([, f]) => facilityKind(f) === kind);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Port definitions. Ports prefer the exact connector cell, but also accept
|
||||
|
|
@ -17,10 +35,10 @@ export function scannerBySlot(game, slot) { return game.scanners.find(s => s.kin
|
|||
// -----------------------------------------------------------------------------
|
||||
export function scannerConnector(scanner, type) {
|
||||
return {
|
||||
inputA: { col: scanner.col, row: scanner.row - 1 },
|
||||
inputA: { col: scanner.col, row: scanner.row - 2 },
|
||||
inputB: null,
|
||||
left: { col: scanner.col - 1, row: scanner.row },
|
||||
right: { col: scanner.col + 1, row: scanner.row }
|
||||
right: { col: scanner.col + 2, row: scanner.row }
|
||||
}[type];
|
||||
}
|
||||
|
||||
|
|
@ -54,21 +72,22 @@ function graphPortConveyorCells(game, point, blocked = [], preferred = []) {
|
|||
// -----------------------------------------------------------------------------
|
||||
export function scannerInputCells(game, scanner) {
|
||||
const connector = scannerConnector(scanner, 'inputA');
|
||||
return visualPortConveyorCells(game, connector, [ { col: scanner.col, row: scanner.row } ]);
|
||||
return visualPortConveyorCells(game, connector, scannerFootprintCells(scanner));
|
||||
}
|
||||
|
||||
function outputStartCells(game, scanner, side) {
|
||||
const connector = scannerConnector(scanner, side);
|
||||
return visualPortConveyorCells(game, connector, [ { col: scanner.col, row: scanner.row } ]);
|
||||
return visualPortConveyorCells(game, connector, scannerFootprintCells(scanner));
|
||||
}
|
||||
|
||||
export function facilityEntryPoint(game, dest) {
|
||||
const f = game.facilities[dest];
|
||||
const f = game.facilities[dest] || facilityEntriesForKind(game, dest)[0]?.[1];
|
||||
if (!f) return null;
|
||||
if (f.entry) return cellCenter(f.entry.col, f.entry.row);
|
||||
if (dest === 'mixer') return { x: f.x + f.w, y: f.y + f.h * 0.52 };
|
||||
if (dest === 'truck') return { x: f.x, y: f.y + f.h * 0.52 };
|
||||
if (dest === 'trash') return { x: f.x + f.w / 2, y: f.y + 8 };
|
||||
const kind = facilityKind(f) || dest;
|
||||
if (kind === 'mixer') return { x: f.x + f.w, y: f.y + f.h * 0.52 };
|
||||
if (kind === 'truck') return { x: f.x, y: f.y + f.h * 0.52 };
|
||||
if (kind === 'trash') return { x: f.x + f.w / 2, y: f.y + 8 };
|
||||
return { x: f.x + f.w / 2, y: f.y + f.h / 2 };
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +121,25 @@ function conveyorOutDirNames(game, cellKey) {
|
|||
return [...new Set(dirs)].filter(dir => DIRS.some(d => d.name === dir));
|
||||
}
|
||||
|
||||
function neighborPointsIntoCell(game, p, dirName) {
|
||||
const d = DIRS.find(item => item.name === dirName);
|
||||
if (!d) return false;
|
||||
const meta = game.conveyorMeta?.get?.(graphNodeKey(p.col + d.dc, p.row + d.dr));
|
||||
if (!meta) return false;
|
||||
const names = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))];
|
||||
return names.includes(d.opposite);
|
||||
}
|
||||
|
||||
function inferredConveyorOutDirNames(game, cellKey) {
|
||||
const p = graphCell(cellKey);
|
||||
const neighbors = DIRS
|
||||
.filter(d => game.conveyorTiles.has(graphNodeKey(p.col + d.dc, p.row + d.dr)))
|
||||
.map(d => d.name);
|
||||
const explicit = conveyorOutDirNames(game, cellKey).filter(dir => neighbors.includes(dir) && !neighborPointsIntoCell(game, p, dir));
|
||||
const inferred = neighbors.filter(dir => !neighborPointsIntoCell(game, p, dir));
|
||||
return explicit.length ? explicit : inferred;
|
||||
}
|
||||
|
||||
function emptyFactoryGraph(game) {
|
||||
return {
|
||||
version: game.routingVersion || 0,
|
||||
|
|
@ -140,8 +178,8 @@ export function buildFactoryGraph(game) {
|
|||
|
||||
for (const k of graph.cells) {
|
||||
const p = graphCell(k);
|
||||
const explicitDirs = conveyorOutDirNames(game, k);
|
||||
const dirs = explicitDirs.length ? DIRS.filter(d => explicitDirs.includes(d.name)) : DIRS;
|
||||
const outDirs = inferredConveyorOutDirNames(game, k);
|
||||
const dirs = DIRS.filter(d => outDirs.includes(d.name));
|
||||
for (const d of dirs) {
|
||||
const nk = graphNodeKey(p.col + d.dc, p.row + d.dr);
|
||||
if (graph.cells.has(nk)) graph.adjacency.get(k).push({ key: nk, dir: d.name });
|
||||
|
|
@ -191,7 +229,7 @@ function indexGraphPorts(game, graph) {
|
|||
const ports = {};
|
||||
for (const type of ['inputA', 'left', 'right']) {
|
||||
const cell = scannerConnector(scanner, type);
|
||||
const cells = graphPortConveyorCells(game, cell, [ { col: scanner.col, row: scanner.row } ])
|
||||
const cells = graphPortConveyorCells(game, cell, scannerFootprintCells(scanner))
|
||||
.filter(p => graph.cells.has(graphNodeKey(p.col, p.row)));
|
||||
const cellKeys = cells.map(p => graphNodeKey(p.col, p.row));
|
||||
const connected = cellKeys.length > 0;
|
||||
|
|
@ -265,13 +303,13 @@ function dirBetween(a, b) {
|
|||
return DIRS.find(d => d.dc === dc && d.dr === dr)?.name || null;
|
||||
}
|
||||
|
||||
function isCrossInGraph(graph, cellKey) {
|
||||
function isPassCross(game, graph, cellKey) {
|
||||
if (game.conveyorMeta?.get?.(cellKey)?.branchMode !== 'pass') return false;
|
||||
const ns = graph.adjacency.get(cellKey) || [];
|
||||
const names = new Set(ns.map(n => n.dir));
|
||||
return ns.length === 4 && names.has('left') && names.has('right') && names.has('up') && names.has('down');
|
||||
}
|
||||
|
||||
// A four-way cross conveyor is an overpass/crossing. Chicks never turn there.
|
||||
export function bfsAllRoutes(game, start, isGoal) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
const startK = graphNodeKey(start.col, start.row);
|
||||
|
|
@ -302,7 +340,7 @@ export function bfsAllRoutes(game, start, isGoal) {
|
|||
for (const n of graph.adjacency.get(cur.key) || []) {
|
||||
const next = graphCell(n.key);
|
||||
const outDir = dirBetween(cur, next);
|
||||
if (cur.incoming !== 'none' && isCrossInGraph(graph, cur.key) && outDir !== cur.incoming) continue;
|
||||
if (cur.incoming !== 'none' && isPassCross(game, graph, cur.key) && outDir !== cur.incoming) continue;
|
||||
const nextDepth = cur.depth + 1;
|
||||
if (nextDepth > bestGoalDepth) continue;
|
||||
const nextStateK = `${n.key}|${outDir}`;
|
||||
|
|
@ -524,20 +562,23 @@ function routeToNextScanner(game, scanner, side, fromPoint, connector, starts, a
|
|||
|
||||
function routeToFacility(game, scanner, side, fromPoint, connector, starts, dest, advance) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
const port = graph.facilityPorts.get(dest);
|
||||
if (!port?.connected) return null;
|
||||
const ports = [...graph.facilityPorts.entries()]
|
||||
.filter(([id, port]) => port?.connected && facilityKind(game.facilities?.[id]) === dest);
|
||||
if (!ports.length) return null;
|
||||
const candidates = [];
|
||||
for (const start of starts) {
|
||||
const goalKeys = (port.keys || [port.key]).filter(Boolean);
|
||||
if (!goalKeys.length) continue;
|
||||
const isGoal = p => goalKeys.includes(graphNodeKey(p.col, p.row));
|
||||
isGoal.cacheKey = `toFacility:${start.col},${start.row}:${dest}:v${graph.version}`;
|
||||
const routes = bfsAllRoutes(game, start, isGoal);
|
||||
for (const cells of routes) candidates.push({ key: `start${key(start.col, start.row)}:${dest}:${cells.length}:${key(cells[cells.length - 1].col, cells[cells.length - 1].row)}`, cells });
|
||||
for (const [facilityId, port] of ports) {
|
||||
const goalKeys = (port.keys || [port.key]).filter(Boolean);
|
||||
if (!goalKeys.length) continue;
|
||||
const isGoal = p => goalKeys.includes(graphNodeKey(p.col, p.row));
|
||||
isGoal.cacheKey = `toFacility:${start.col},${start.row}:${facilityId}:v${graph.version}`;
|
||||
const routes = bfsAllRoutes(game, start, isGoal);
|
||||
for (const cells of routes) candidates.push({ key: `start${key(start.col, start.row)}:${facilityId}:${cells.length}:${key(cells[cells.length - 1].col, cells[cells.length - 1].row)}`, cells, facilityId });
|
||||
}
|
||||
}
|
||||
const chosen = chooseRoundRobin(game, `scanner:${scanner.id}:${side}:${dest}`, candidates, advance);
|
||||
if (!chosen) return null;
|
||||
return { destination: dest, route: routeWithConnector(fromPoint, connector, chosen.cells, facilityEntryPoint(game, dest)) };
|
||||
return { destination: dest, facilityId: chosen.facilityId, route: routeWithConnector(fromPoint, connector, chosen.cells, facilityEntryPoint(game, chosen.facilityId)) };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -579,8 +620,7 @@ export function eggFarmHasValidRoute(game, farm) {
|
|||
|
||||
export function facilityHasValidRoute(game, dest) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
if (!graph.facilityPorts.get(dest)?.connected) return false;
|
||||
return game.scanners.some(scanner => scannerOutputReachesDestination(game, scanner, dest));
|
||||
return [...graph.facilityPorts.entries()].some(([id, port]) => port?.connected && facilityKind(game.facilities?.[id]) === dest);
|
||||
}
|
||||
|
||||
export function disconnectedBuildWarnings(game) {
|
||||
|
|
@ -590,8 +630,10 @@ export function disconnectedBuildWarnings(game) {
|
|||
if (!eggFarmHasValidRoute(game, farm)) warnings.push({ type: 'eggFarm', id: farm.id, ref: farm, message: 'This Egg will not produce chicks' });
|
||||
}
|
||||
for (const id of MACHINE_FACILITY_IDS) {
|
||||
const facility = game.facilities?.[id];
|
||||
if (facility && !facilityHasValidRoute(game, id)) warnings.push({ type: 'facility', id, ref: facility, message: 'This facility cannot receive items' });
|
||||
for (const [facilityId, facility] of facilityEntriesForKind(game, id)) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
if (!graph.facilityPorts.get(facilityId)?.connected) warnings.push({ type: 'facility', id: facilityId, ref: facility, message: 'This facility cannot receive items' });
|
||||
}
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
|
@ -605,7 +647,7 @@ export function minimumStartConnectionIssues(game) {
|
|||
if (outputRouteReachesFacility(game, scanner)) return [];
|
||||
}
|
||||
const farmCount = game.eggFarms?.length || 0;
|
||||
const exitCount = ['mixer', 'trash', 'truck'].filter(id => graph.facilityPorts.get(id)?.connected).length;
|
||||
const exitCount = [...graph.facilityPorts.entries()].filter(([id, port]) => port?.connected && ['mixer', 'trash', 'truck'].includes(facilityKind(game.facilities?.[id]))).length;
|
||||
if (!farmCount) return ['Build at least one EGG before starting the next day.'];
|
||||
if (!graph.metrics.farmOutputs) return ['Connect at least one EGG to a conveyor.'];
|
||||
if (!game.scanners?.length) return ['Build at least one scanner and connect it to an EGG route.'];
|
||||
|
|
@ -624,12 +666,14 @@ export function validateFactoryGraph(game, graph = ensureFactoryGraph(game), opt
|
|||
const labels = { mixer: 'Mixer', trash: 'Shredder', truck: 'Truck' };
|
||||
|
||||
for (const id of MACHINE_FACILITY_IDS) {
|
||||
const f = game.facilities[id];
|
||||
if (!f) {
|
||||
const facilities = facilityEntriesForKind(game, id);
|
||||
if (!facilities.length) {
|
||||
issues.push(`${labels[id] || id} is missing`);
|
||||
continue;
|
||||
}
|
||||
if (!graph.facilityPorts.get(id)?.connected) issues.push(`${labels[id] || id} receiver has no conveyor`);
|
||||
for (const [facilityId] of facilities) {
|
||||
if (!graph.facilityPorts.get(facilityId)?.connected) issues.push(`${labels[id] || id} receiver has no conveyor`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const scanner of game.scanners) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { GRID } from '../core/config.js';
|
||||
import { key, parseKey, cellCenter } from '../core/utils.js';
|
||||
import { nearestGridEdge, layoutFacilityOnEdge } from '../core/entities.js';
|
||||
import { farmAt, scannerAt, scannerCenter, refreshRoutingAfterEdit } from './routing.js';
|
||||
import { farmAt, scannerAt, scannerCenter, scannerFootprintCells, refreshRoutingAfterEdit } from './routing.js';
|
||||
import { snapshot } from './history.js';
|
||||
import { buildPrice } from './economy.js';
|
||||
|
||||
|
|
@ -192,12 +192,15 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
|||
for (const origin of game.groupDrag.origins) {
|
||||
if (origin.type === 'facility') continue;
|
||||
const col = origin.col + dcol, row = origin.row + drow;
|
||||
if (!pointInGrid(col, row)) return fail('Selection outside grid');
|
||||
if (game.blockedCells?.has?.(key(col, row))) return fail('Selection hits blocked ground');
|
||||
const tk = key(col, row);
|
||||
if (targetCells.has(tk)) return fail('Selection overlap');
|
||||
if (cellOccupiedByNonSelected(col, row, selectedTokens)) return fail('Cell occupied');
|
||||
targetCells.add(tk);
|
||||
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');
|
||||
targetCells.add(tk);
|
||||
}
|
||||
}
|
||||
if (facilityDragWouldOverlap(dx, dy, selectedTokens)) return fail('Facility overlap');
|
||||
commitGroupDragHistory();
|
||||
|
|
|
|||
|
|
@ -18,11 +18,9 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
if (game.phase !== 'build' || game.cardDraft?.pending || game.cardTargetPick?.pending) return false;
|
||||
let hasBuildableOption = false;
|
||||
for (const id of BUILD_TOOL_IDS) {
|
||||
if (MACHINE_FACILITY_IDS.includes(id) && !!game.facilities[id]) continue;
|
||||
hasBuildableOption = true;
|
||||
if (game.cash >= buildPrice(id, game)) return false;
|
||||
}
|
||||
if (build?.gridExpansionCost && game.cash >= build.gridExpansionCost()) return false;
|
||||
return hasBuildableOption;
|
||||
}
|
||||
|
||||
|
|
@ -44,32 +42,20 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
if (!btn) continue;
|
||||
const price = buildPrice(id, game);
|
||||
const priceSpan = btn.querySelector('span');
|
||||
if (priceSpan) priceSpan.textContent = id === 'conveyor' ? `${yen(price)} / tile` : yen(price);
|
||||
const uniqueAlreadyBuilt = MACHINE_FACILITY_IDS.includes(id) && !!game.facilities[id];
|
||||
if (priceSpan) priceSpan.textContent = (id === 'conveyor' || id === 'boostConveyor') ? `${yen(price)} / tile` : yen(price);
|
||||
const manualLimitReached = id === 'manualScanner' && game.scanners.filter(s => s.kind === 'manual').length >= 8;
|
||||
const unaffordable = game.cash < price;
|
||||
btn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || unaffordable || uniqueAlreadyBuilt || manualLimitReached;
|
||||
btn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || unaffordable || manualLimitReached;
|
||||
if (btn.disabled && game.buildTool === id) game.buildTool = null;
|
||||
btn.classList.toggle('unaffordable', unaffordable);
|
||||
btn.classList.toggle('already-built', uniqueAlreadyBuilt || manualLimitReached);
|
||||
btn.classList.toggle('already-built', manualLimitReached);
|
||||
btn.title = unaffordable
|
||||
? `Need ${yen(price - game.cash)} more`
|
||||
: manualLimitReached
|
||||
? 'Manual Scanner limit reached (8 max)'
|
||||
: uniqueAlreadyBuilt
|
||||
? `${FACILITY_DEFS[id]?.name || id} already exists`
|
||||
: '';
|
||||
}
|
||||
if (ui.buttons.erase) ui.buttons.erase.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending;
|
||||
if (ui.expandGridButton && build?.gridExpansionCost) {
|
||||
const cost = build.gridExpansionCost();
|
||||
const priceSpan = ui.expandGridButton.querySelector('span');
|
||||
if (priceSpan) priceSpan.textContent = `${yen(cost)} / +10×10`;
|
||||
const blocked = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || game.cash < cost;
|
||||
ui.expandGridButton.disabled = blocked;
|
||||
ui.expandGridButton.classList.toggle('unaffordable', game.cash < cost);
|
||||
ui.expandGridButton.title = game.cash < cost ? `Need ${yen(cost - game.cash)} more` : `Add the next right 10×10 lot, or click an upper/right lot button on the canvas. Next purchase count: ${(game.gridExpansionPurchases || 0) + 1}`;
|
||||
}
|
||||
if (ui.hireRepairmanButton) {
|
||||
const hired = !!game.repairman?.hiredForNextDay;
|
||||
const cost = repairmanDailyCost(game);
|
||||
|
|
@ -110,15 +96,15 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
const lastTribute = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.fairiesTribute || 0 : null;
|
||||
const lastTributeReduction = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.reduction || 0 : 0;
|
||||
ui.turnSummary.innerHTML = [
|
||||
`Seed: <b>${escapeHtml(game.runSeed || 'none')}</b> | Grid box: <b>${game.gridRows || 10}×${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`,
|
||||
`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>`,
|
||||
`Poop fine: <b>Mixer -${yen(mixerPoopPenalty(game))}</b> / <b>Shipment -${yen(truckPoopPenalty(game))}</b>`,
|
||||
`ZUNDA TAX on Next Day: <b>-${yen(tax.tax)}</b> | basis net ${yen(zundaBasisProfit)} | taxable ${yen(tax.taxable)} × ${tax.ratePercent}% | exemption ${yen(tax.exemption)} | 95% at ${yen(tax.maxRateCash)}`,
|
||||
`ZUNDA TAX on Next Day: <b>-${yen(tax.tax)}</b> | basis net ${yen(zundaBasisProfit)} | taxable ${yen(tax.taxable)} x${tax.ratePercent}% | exemption ${yen(tax.exemption)} | 95% at ${yen(tax.maxRateCash)}`,
|
||||
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 ${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> | Next expansion: <b>${yen(build?.gridExpansionCost ? build.gridExpansionCost() : 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>`,
|
||||
`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>`
|
||||
].join('<br>');
|
||||
|
|
@ -165,7 +151,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
ui.timeLeft.textContent = game.phase === 'running' && game.timeLeft <= 0 && game.shutdownTimeLeft > 0
|
||||
? `+${game.shutdownTimeLeft.toFixed(1)}s`
|
||||
: `${Math.max(0, game.timeLeft).toFixed(1)}s`;
|
||||
ui.timeLeft.closest?.('.hud-card')?.classList.toggle('time-critical', game.phase === 'running' && (game.timeLeft <= 10 || (game.timeLeft <= 0 && game.shutdownTimeLeft > 0)));
|
||||
ui.timeLeft.closest?.('.hud-card')?.classList.toggle('time-critical', game.phase === 'running' && (game.timeLeft <= 10 || (game.timeLeft <= 0 && game.shutdownTimeLeft > 0)));
|
||||
ui.phase.textContent = phaseLabel();
|
||||
if (ui.comboCount) ui.comboCount.textContent = game.manualCombo?.count || 0;
|
||||
updatePriorityStrip();
|
||||
|
|
@ -228,7 +214,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
|
||||
function showTitle() {
|
||||
ui.modalTitle.textContent = TEXT.versionTitle;
|
||||
ui.modalBody.innerHTML = `<div class="route-cards"><div class="route-card"><span class="route-icon male" aria-label="male chick">♂</span><strong>Male</strong><b>-></b><em>Mixer</em><small>S1: A</small></div><div class="route-card"><span class="route-icon female" aria-label="female chick">♀</span><strong>Female</strong><b>-></b><em>Truck</em><small>S1: D, then S2: right</small></div><div class="route-card"><span class="route-icon poop" aria-label="poop">💩</span><strong>Poop</strong><b>-></b><em>Shredder</em><small>S1: D, then S2: left</small></div></div><p>Build phase: connect at least one EGG through a scanner to Mixer, Shredder, or Truck before Next Day. One day is one run.</p>`;
|
||||
ui.modalBody.innerHTML = `<div class="route-cards"><div class="route-card"><span class="route-icon male" aria-label="male chick">髫ィ蜷カ繝サ/span><strong>Male</strong><b>-></b><em>Mixer</em><small>S1: A</small></div><div class="route-card"><span class="route-icon female" aria-label="female chick">髫ィ蜊債</span><strong>Female</strong><b>-></b><em>Truck</em><small>S1: D, then S2: right</small></div><div class="route-card"><span class="route-icon poop" aria-label="poop">・滓ィ頑c</span><strong>Poop</strong><b>-></b><em>Shredder</em><small>S1: D, then S2: left</small></div></div><p>Build phase: connect at least one EGG through a scanner to Mixer, Shredder, or Truck before Next Day. One day is one run.</p>`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
ui.modalActions.appendChild(button(TEXT.actions.startGame, startGame, 'primary-button'));
|
||||
ui.modal.classList.remove('equipment-popover', 'gameover-modal');
|
||||
|
|
@ -247,6 +233,8 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
<div><strong>Chick shipment income</strong>${positive(r.chickShipmentIncome || 0)}</div>
|
||||
<div><strong>Poop shipment income</strong>${positive(r.poopShipmentIncome || 0)}</div>
|
||||
<div><strong>Mixer income</strong>${positive(r.mixerRevenue || 0)}</div>
|
||||
<div><strong>Composter income</strong>${positive(r.composterIncome || 0)}</div>
|
||||
<div><strong>DUD refund income</strong>${positive(r.dudRefundIncome || 0)}</div>
|
||||
<div><strong>Manual combo bonus</strong>${positive(r.manualComboBonus || 0)}</div>
|
||||
<div><strong>Chemical subsidy</strong>${positive(r.chemicalWeaponSubsidy || 0)}</div>
|
||||
<div><strong>Contract bonus</strong>${positive(r.contractBonus || 0)}</div>
|
||||
|
|
@ -289,7 +277,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
ui.modalBody.innerHTML = `<p class="gameover-reason">Your cash went negative.</p>
|
||||
<div class="result-grid gameover-summary"><div><strong>Final Score</strong><span>${fs.score}</span></div><div><strong>Survival</strong><span>${game.totals.turnsCompleted} days</span></div><div><strong>Final Cash</strong><span>${yen(game.cash)}</span></div><div><strong>Accuracy</strong><span>${accuracy}%</span></div></div>
|
||||
<details class="gameover-details"><summary>Score breakdown</summary>
|
||||
<div class="formula-box compact"><strong>Formula</strong><br>Base = Cash + Total revenue + Factory value × 0.5 + Contract bonus × 0.5 = ${yen(fs.base)}<br>Daily earned = ceil(Base ÷ Days) = ${yen(fs.dailyEarned)}<br>Score = max(0, floor(Base + Daily earned + Correct×5 - Total outflow - Day penalty)) = ${fs.score}</div>
|
||||
<div class="formula-box compact"><strong>Formula</strong><br>Base = Cash + Total revenue + Factory value 繝サ繝サ繝サ0.5 + Contract bonus 繝サ繝サ繝サ0.5 = ${yen(fs.base)}<br>Daily earned = ceil(Base 繝サ繝サ・ス・キ Days) = ${yen(fs.dailyEarned)}<br>Score = max(0, floor(Base + Daily earned + Correct繝サ繝サ繝サ - Total outflow - Day penalty)) = ${fs.score}</div>
|
||||
<div class="result-grid gameover-breakdown"><div><strong>Factory Value</strong><span>${yen(fs.factoryValue)}</span></div><div><strong>Total Revenue</strong><span>${yen(game.totals.revenue)}</span></div><div><strong>Daily Earned</strong><span>${yen(fs.dailyEarned)} / day</span></div><div><strong>Total Outflow</strong><span>${yen(game.totals.penalty)}</span></div><div><strong>ZUNDA TAX</strong><span>${yen(game.totals.zundaTax)}</span></div><div><strong>Fairies Tribute</strong><span>${yen(game.totals.fairiesTribute || 0)}</span></div><div><strong>Correct Sorts</strong><span>${game.totals.correct}</span></div><div><strong>Auto Sorted</strong><span>${game.totals.autoSorted}</span></div><div><strong>Poop Control</strong><span>${game.totals.poopTrash} trashed / ${game.totals.poopTruck} shipped / ${game.totals.poopMixer} mixer</span></div><div><strong>Explosion Penalty</strong><span>${yen(game.totals.explosionDamage)}</span></div><div><strong>Contract Bonus</strong><span>${yen(game.totals.contractBonus)}</span></div><div><strong>Day Penalty</strong><span>-${fs.dayPenalty}</span></div><div><strong>Contracts</strong><span>${game.totals.contractSuccess} success / ${game.totals.contractFailed} failed</span></div></div>
|
||||
</details>`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue