hehehe
This commit is contained in:
parent
16bde0b87d
commit
b54f36c0e8
19 changed files with 1768 additions and 422 deletions
25
index.html
25
index.html
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Chick Sorter v27.0</title>
|
<title>Chick Sorter v27.6</title>
|
||||||
<link rel="stylesheet" href="./styles.css" />
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
<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"><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 compact"><span>PHASE</span><strong id="phaseLabel">Title</strong></div>
|
||||||
|
<div class="hud-card combo"><span>COMBO</span><strong id="comboCount">0</strong></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hud hud-top-right" aria-label="Day result details">
|
<div class="hud hud-top-right" aria-label="Day result details">
|
||||||
|
|
@ -35,8 +36,8 @@
|
||||||
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
|
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>CHICK SORTER <span>v27.0</span></h1>
|
<h1>CHICK SORTER <span>v27.6</span></h1>
|
||||||
<p>Build, drag, connect at least one valid EGG route, then start the next day.</p>
|
<p>Build, expand, connect at least one valid EGG route, then start the next day.</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
|
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -54,6 +55,7 @@
|
||||||
<button id="buildTruckButton" class="tool-button" type="button"><strong>Truck</strong><span>¥450</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="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="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>
|
||||||
<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 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>
|
||||||
<div id="buildStatus" class="mini-box">Build tools unlock after each day.</div>
|
<div id="buildStatus" class="mini-box">Build tools unlock after each day.</div>
|
||||||
|
|
@ -70,6 +72,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="manualScannerMonitor" class="scanner-monitor" aria-label="Offscreen manual scanner monitor"></div>
|
||||||
|
|
||||||
<div class="control-dock" aria-label="Sorting controls">
|
<div class="control-dock" aria-label="Sorting controls">
|
||||||
<button id="scanner1MixerButton" class="sort-button" type="button">S1 A -> Mixer</button>
|
<button id="scanner1MixerButton" class="sort-button" type="button">S1 A -> Mixer</button>
|
||||||
<button id="scanner1TruckButton" class="sort-button" type="button">S1 D -> S2</button>
|
<button id="scanner1TruckButton" class="sort-button" type="button">S1 D -> S2</button>
|
||||||
|
|
@ -77,6 +81,19 @@
|
||||||
<button id="scanner2TruckButton" class="sort-button" type="button">S2 -> Truck</button>
|
<button id="scanner2TruckButton" class="sort-button" type="button">S2 -> Truck</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="hoverTooltip" class="hover-tooltip" aria-hidden="true"></div>
|
<div id="hoverTooltip" class="hover-tooltip" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<details id="debugPanel" class="debug-panel">
|
||||||
|
<summary>DBG</summary>
|
||||||
|
<div class="debug-body">
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<div id="debugReadout" class="debug-readout"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
@ -88,6 +105,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="./src/game.js?v=27.0-cleanup"></script>
|
<script type="module" src="./src/game.js?v=27.6-direct-conveyor-chicks"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Central balance sheet for tuning gameplay.
|
// Central balance sheet for tuning gameplay.
|
||||||
// Edit this file first when adjusting prices, income, penalties, timing, card rates, or caps.
|
// Edit this file first when adjusting prices, income, penalties, timing, card rates, or caps.
|
||||||
export const BALANCE = {
|
export const BALANCE = {
|
||||||
version: 'v27.0 cleanup build',
|
version: 'v27.6 direct conveyor chick movement',
|
||||||
time: {
|
time: {
|
||||||
daySeconds: 60,
|
daySeconds: 60,
|
||||||
farmShutdownGraceSeconds: 10
|
farmShutdownGraceSeconds: 10
|
||||||
|
|
@ -10,11 +10,18 @@ export const BALANCE = {
|
||||||
starting: 250
|
starting: 250
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
x: 190,
|
x: 170,
|
||||||
y: 142,
|
y: 78,
|
||||||
cols: 22,
|
cols: 20,
|
||||||
rows: 13,
|
rows: 10,
|
||||||
cell: 46
|
cell: 46,
|
||||||
|
expansion: {
|
||||||
|
colsPerPurchase: 10,
|
||||||
|
rowsPerPurchase: 10,
|
||||||
|
costBase: 1000,
|
||||||
|
costMultiplier: 4 / 3,
|
||||||
|
truncateUnit: 100
|
||||||
|
}
|
||||||
},
|
},
|
||||||
map: {
|
map: {
|
||||||
blockedRatio: 0.075
|
blockedRatio: 0.075
|
||||||
|
|
@ -62,12 +69,232 @@ export const BALANCE = {
|
||||||
bearingSpeedMultiplier: 1.075
|
bearingSpeedMultiplier: 1.075
|
||||||
},
|
},
|
||||||
cards: {
|
cards: {
|
||||||
|
definitions: [
|
||||||
|
{
|
||||||
|
id: 'upgradeEgg',
|
||||||
|
title: 'EGG Improvement',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'equipmentUpgrade',
|
||||||
|
target: 'eggFarm',
|
||||||
|
description: 'Choose one EGG FARM on the map and raise it by 1 level.',
|
||||||
|
tags: ['EGG', 'ACTIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'upgradeAutoScanner',
|
||||||
|
title: 'AUTO SCANNER Improvement',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'equipmentUpgrade',
|
||||||
|
target: 'autoScanner',
|
||||||
|
description: 'Choose one AUTO SCANNER on the map and raise it by 1 level.',
|
||||||
|
tags: ['SCANNER', 'ACTIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'upgradeMixer',
|
||||||
|
title: 'MIXER Improvement',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'equipmentUpgrade',
|
||||||
|
target: 'mixer',
|
||||||
|
description: 'Choose MIXER on the map and raise it by 1 level.',
|
||||||
|
tags: ['ECONOMY', 'ACTIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'upgradeTruck',
|
||||||
|
title: 'TRUCK Improvement',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'equipmentUpgrade',
|
||||||
|
target: 'truck',
|
||||||
|
description: 'Choose TRUCK on the map and raise it by 1 level.',
|
||||||
|
tags: ['ECONOMY', 'ACTIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'upgradeTrash',
|
||||||
|
title: 'SHREDDER Improvement',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'equipmentUpgrade',
|
||||||
|
target: 'trash',
|
||||||
|
description: 'Choose SHREDDER and raise it by 1 level. Bonus chance increases by the current upgrade count. Max 30 upgrades.',
|
||||||
|
tags: ['POOP', 'ACTIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'crowdedFarming',
|
||||||
|
title: 'Crowded Farming Contract',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'EGG production interval -5%. Equipment deterioration +4%. Max 10 cards.',
|
||||||
|
tags: ['EGG', 'RISK', 'PASSIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hatchingFeed',
|
||||||
|
title: 'Hatching Feed',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'EGG production interval -4%. Poop rate +3%. No card limit.',
|
||||||
|
tags: ['EGG', 'POOP', 'PASSIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'safetyCover',
|
||||||
|
title: 'Safety Cover',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Explosion damage -5%. No card limit.',
|
||||||
|
tags: ['RISK', 'PASSIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'preventiveMaintenance',
|
||||||
|
title: 'Preventive Maintenance Manual',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'All equipment deterioration speed -5%. No card limit.',
|
||||||
|
tags: ['MAINTENANCE', 'PASSIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dudFilter',
|
||||||
|
title: 'DUD Filter',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'DUD chance -5 percentage points. Stacks until DUD chance reaches 0%.',
|
||||||
|
tags: ['CARD', 'PASSIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'durabilityCoating',
|
||||||
|
title: 'Durability Coating',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'All equipment maximum durability +3%. No card limit.',
|
||||||
|
tags: ['MAINTENANCE', 'PASSIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bearing',
|
||||||
|
title: 'High-Quality Bearing',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Conveyor speed +7.5%.',
|
||||||
|
tags: ['CONVEYOR', 'PASSIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'extraEggOutlet',
|
||||||
|
title: 'Extra Egg Outlet',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'equipmentUpgrade',
|
||||||
|
target: 'eggOutlet',
|
||||||
|
description: 'Choose one EGG FARM and add +1 output port to that EGG. Max +3 per EGG.',
|
||||||
|
tags: ['EGG', 'RARE', 'ACTIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'recyclingSubsidy',
|
||||||
|
title: 'Recycling Subsidy',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'SELL refund rate +10 points. Max 3 cards; normal SELL can reach 80%.',
|
||||||
|
tags: ['ECONOMY', 'RARE', 'PASSIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dynamite',
|
||||||
|
title: 'Dynamite',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'cellAction',
|
||||||
|
target: 'blockedCell',
|
||||||
|
description: 'Remove any 3 blocked cells with a burst effect.',
|
||||||
|
tags: ['RISK', 'RARE', 'ONE-SHOT']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'usedMachine',
|
||||||
|
title: 'Used Machines',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Future equipment costs half. New purchases have no refund and 60% durability. New Machines cancels it.',
|
||||||
|
tags: ['ECONOMY', 'RISK', 'RARE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'newMachine',
|
||||||
|
title: 'New Machines',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Cancel Used Machines and restore normal prices, refunds, and durability.',
|
||||||
|
tags: ['ECONOMY', 'ONE-SHOT']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'flattery',
|
||||||
|
title: 'Flattery',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Next Fairies tribute -50%.',
|
||||||
|
tags: ['ECONOMY', 'RARE', 'ONE-SHOT']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'loan',
|
||||||
|
title: 'Loan',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Gain JPY 300 per elapsed day now. Pay JPY 400 per day for that many days starting tomorrow.',
|
||||||
|
tags: ['ECONOMY', 'RISK', 'RARE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'extraCards',
|
||||||
|
title: 'Extra Cards',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Draw 2 cards now and choose both.',
|
||||||
|
tags: ['RARE', 'ACTIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'legalWork',
|
||||||
|
title: 'Legal Work',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'ZUNDA TAX exemption +¥250. The 95% cap point also moves up by ¥250.',
|
||||||
|
tags: ['ECONOMY', 'RARE', 'PASSIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'chemicalWeaponSubsidy',
|
||||||
|
title: 'Chemical Weapons Subsidy',
|
||||||
|
rarity: 'ultraRare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Each day, gain JPY 2000 per 100 poop shipped by truck. No card limit.',
|
||||||
|
tags: ['POOP', 'ULTRA RARE', 'PASSIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'laborExploitation',
|
||||||
|
title: 'Motivational Exploitation',
|
||||||
|
rarity: 'ultraRare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Repairman labor cost -30%. The repairman smiles with bloodshot eyes. One time only.',
|
||||||
|
tags: ['MAINTENANCE', 'ULTRA RARE', 'PASSIVE']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rescueLoan',
|
||||||
|
title: 'Emergency Loan',
|
||||||
|
rarity: 'ultraRare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'If cash is below JPY 0 at day-end settlement, gain JPY 500. Consumed on use.',
|
||||||
|
tags: ['ECONOMY', 'ULTRA RARE', 'ONE-SHOT']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
defaultEffects: {
|
||||||
|
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: []
|
||||||
|
},
|
||||||
commonWeight: 8,
|
commonWeight: 8,
|
||||||
rareWeight: 2,
|
rareWeight: 2,
|
||||||
ultraRareWeight: 0.45,
|
ultraRareWeight: 0.45,
|
||||||
baseDraftSize: 3,
|
baseDraftSize: 3,
|
||||||
maxDudsPerDraft: 2,
|
maxDudsPerDraft: 2,
|
||||||
dudChancePerCard: 0.30,
|
dudChancePerCard: 0.30,
|
||||||
|
dudChanceReductionPerFilter: 0.05,
|
||||||
extraCardsAddedPicks: 2,
|
extraCardsAddedPicks: 2,
|
||||||
rerollCost: {
|
rerollCost: {
|
||||||
dayDivisor: 2,
|
dayDivisor: 2,
|
||||||
|
|
@ -94,8 +321,8 @@ export const BALANCE = {
|
||||||
},
|
},
|
||||||
repairman: {
|
repairman: {
|
||||||
dailyCost: 100,
|
dailyCost: 100,
|
||||||
secondsPerOnePercent: 1 / 3,
|
secondsPerOnePercent: 0.5,
|
||||||
walkSpeed: 140
|
walkSpeed: 90
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
facilities: {
|
facilities: {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ export const CONVEYOR_SPEED_MAX = BALANCE.conveyor.maxSpeed;
|
||||||
export const BEARING_SPEED_MULTIPLIER = BALANCE.conveyor.bearingSpeedMultiplier;
|
export const BEARING_SPEED_MULTIPLIER = BALANCE.conveyor.bearingSpeedMultiplier;
|
||||||
|
|
||||||
export const CARD_BALANCE = BALANCE.cards;
|
export const CARD_BALANCE = BALANCE.cards;
|
||||||
|
export const CARD_DEFS = BALANCE.cards.definitions;
|
||||||
|
export const CARD_DEFAULT_EFFECTS = BALANCE.cards.defaultEffects;
|
||||||
export const EGG_SPAWN_RANGES = BALANCE.production.eggSpawnRanges;
|
export const EGG_SPAWN_RANGES = BALANCE.production.eggSpawnRanges;
|
||||||
export const GRID = BALANCE.grid;
|
export const GRID = BALANCE.grid;
|
||||||
export const FACILITY_DEFS = BALANCE.facilities;
|
export const FACILITY_DEFS = BALANCE.facilities;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export function createEggFarm(game, col, row) {
|
||||||
const farm = {
|
const farm = {
|
||||||
type: 'eggFarm', id: game.nextId++, col, row,
|
type: 'eggFarm', id: game.nextId++, col, row,
|
||||||
level: 1, nextSpawn: 2, lastInterval: 2,
|
level: 1, nextSpawn: 2, lastInterval: 2,
|
||||||
|
extraEggOutlet: 0,
|
||||||
price: BUILD_COSTS.eggFarm,
|
price: BUILD_COSTS.eggFarm,
|
||||||
builtSession: game.buildSession
|
builtSession: game.buildSession
|
||||||
};
|
};
|
||||||
|
|
@ -97,15 +98,17 @@ export function rollChickSex(game) {
|
||||||
|
|
||||||
export function createChick(game, routeData) {
|
export function createChick(game, routeData) {
|
||||||
const sex = rollChickSex(game);
|
const sex = rollChickSex(game);
|
||||||
const start = routeData.route[0];
|
const route = (routeData.route || []).map(p => ({ x: p.x, y: p.y }));
|
||||||
|
const start = route[0];
|
||||||
return {
|
return {
|
||||||
id: game.nextId++, sex,
|
id: game.nextId++, sex,
|
||||||
x: start.x, y: start.y,
|
x: start.x, y: start.y,
|
||||||
route: routeData.route,
|
route,
|
||||||
targetIndex: 1,
|
targetIndex: route.length > 1 ? 1 : route.length,
|
||||||
stage: 'input',
|
stage: routeData.stage || 'belt',
|
||||||
scannerId: routeData.scannerId,
|
scannerId: routeData.scannerId ?? null,
|
||||||
nextScannerId: null,
|
nextScannerId: null,
|
||||||
|
pendingScannerId: null,
|
||||||
radius: sex === 'poop' ? 15 : 17,
|
radius: sex === 'poop' ? 15 : 17,
|
||||||
bob: Math.random() * Math.PI * 2,
|
bob: Math.random() * Math.PI * 2,
|
||||||
queueIndex: -1,
|
queueIndex: -1,
|
||||||
|
|
|
||||||
160
src/core/gridExpansion.js
Normal file
160
src/core/gridExpansion.js
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { BALANCE } from './balance.js';
|
||||||
|
import { GRID } from './config.js';
|
||||||
|
import { key, parseKey } from './utils.js';
|
||||||
|
|
||||||
|
export const GRID_CHUNK_SIZE = 10;
|
||||||
|
const INITIAL_CHUNKS_X = 2;
|
||||||
|
const INITIAL_CHUNKS_Y = 1;
|
||||||
|
|
||||||
|
function chunkKey(chunkX, chunkY) { return `${chunkX},${chunkY}`; }
|
||||||
|
function parseChunkKey(value) {
|
||||||
|
const [chunkX, chunkY] = String(value).split(',').map(Number);
|
||||||
|
return { chunkX, chunkY };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expansionCost(game) {
|
||||||
|
const cfg = BALANCE.grid.expansion || {};
|
||||||
|
const purchaseCount = Math.max(1, (game.gridExpansionPurchases || 0) + 1);
|
||||||
|
const raw = (cfg.costBase || 1000) * purchaseCount * (cfg.costMultiplier || (4 / 3));
|
||||||
|
const unit = Math.max(1, cfg.truncateUnit || 100);
|
||||||
|
return Math.floor(raw / unit) * unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetOwnedCells(game) {
|
||||||
|
game.ownedCells = new Set();
|
||||||
|
game.ownedChunks = new Set();
|
||||||
|
game.gridChunkOriginY = 0;
|
||||||
|
game.gridExpansionPurchases = 0;
|
||||||
|
game.gridExpansionPurchasesByDirection = { right: 0, up: 0 };
|
||||||
|
for (let chunkY = 0; chunkY < INITIAL_CHUNKS_Y; chunkY += 1) {
|
||||||
|
for (let chunkX = 0; chunkX < INITIAL_CHUNKS_X; chunkX += 1) game.ownedChunks.add(chunkKey(chunkX, chunkY));
|
||||||
|
}
|
||||||
|
for (let row = 0; row < BALANCE.grid.rows; row += 1) {
|
||||||
|
for (let col = 0; col < BALANCE.grid.cols; col += 1) game.ownedCells.add(key(col, row));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureOwnedChunks(game) {
|
||||||
|
if (game.ownedChunks?.size) return game.ownedChunks;
|
||||||
|
game.ownedChunks = new Set();
|
||||||
|
if (game.ownedCells?.size) {
|
||||||
|
const originY = game.gridChunkOriginY || 0;
|
||||||
|
for (const k of game.ownedCells) {
|
||||||
|
const p = parseKey(k);
|
||||||
|
const chunkX = Math.floor(p.col / GRID_CHUNK_SIZE);
|
||||||
|
const chunkY = Math.floor(p.row / GRID_CHUNK_SIZE) + originY;
|
||||||
|
game.ownedChunks.add(chunkKey(chunkX, chunkY));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let chunkY = 0; chunkY < INITIAL_CHUNKS_Y; chunkY += 1) {
|
||||||
|
for (let chunkX = 0; chunkX < INITIAL_CHUNKS_X; chunkX += 1) game.ownedChunks.add(chunkKey(chunkX, chunkY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return game.ownedChunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOwnedCell(game, col, row) {
|
||||||
|
if (!game?.ownedCells || game.ownedCells.size === 0) return col >= 0 && col < GRID.cols && row >= 0 && row < GRID.rows;
|
||||||
|
return game.ownedCells.has(key(col, row));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countOwnedCells(game) {
|
||||||
|
return game?.ownedCells?.size || (GRID.cols * GRID.rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lotFromChunk(game, chunkX, chunkY, direction = 'up') {
|
||||||
|
const originY = game.gridChunkOriginY || 0;
|
||||||
|
return {
|
||||||
|
direction,
|
||||||
|
key: chunkKey(chunkX, chunkY),
|
||||||
|
chunkX,
|
||||||
|
chunkY,
|
||||||
|
colStart: chunkX * GRID_CHUNK_SIZE,
|
||||||
|
rowStart: (chunkY - originY) * GRID_CHUNK_SIZE,
|
||||||
|
cols: GRID_CHUNK_SIZE,
|
||||||
|
rows: GRID_CHUNK_SIZE
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expansionLots(game) {
|
||||||
|
const owned = ensureOwnedChunks(game);
|
||||||
|
const candidates = new Map();
|
||||||
|
for (const k of owned) {
|
||||||
|
const { chunkX, chunkY } = parseChunkKey(k);
|
||||||
|
const right = { chunkX: chunkX + 1, chunkY, direction: 'right' };
|
||||||
|
const up = { chunkX, chunkY: chunkY - 1, direction: 'up' };
|
||||||
|
for (const candidate of [right, up]) {
|
||||||
|
if (candidate.chunkX < 0 || candidate.chunkY > 0) continue;
|
||||||
|
const ck = chunkKey(candidate.chunkX, candidate.chunkY);
|
||||||
|
if (owned.has(ck) || candidates.has(ck)) continue;
|
||||||
|
candidates.set(ck, lotFromChunk(game, candidate.chunkX, candidate.chunkY, candidate.direction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...candidates.values()].sort((a, b) => {
|
||||||
|
const dir = (a.direction === b.direction) ? 0 : (a.direction === 'right' ? -1 : 1);
|
||||||
|
if (dir) return dir;
|
||||||
|
if (a.chunkY !== b.chunkY) return a.chunkY - b.chunkY;
|
||||||
|
return a.chunkX - b.chunkX;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expansionLot(game, direction = 'right') {
|
||||||
|
const lots = expansionLots(game);
|
||||||
|
return lots.find(lot => lot.direction === direction) || lots[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lotBounds(lot) {
|
||||||
|
return {
|
||||||
|
x: GRID.x + lot.colStart * GRID.cell,
|
||||||
|
y: GRID.y + lot.rowStart * GRID.cell,
|
||||||
|
w: lot.cols * GRID.cell,
|
||||||
|
h: lot.rows * GRID.cell
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expansionButtonBounds(lot) {
|
||||||
|
const b = lotBounds(lot);
|
||||||
|
return {
|
||||||
|
x: b.x + b.w / 2 - 116,
|
||||||
|
y: b.y + b.h / 2 - 36,
|
||||||
|
w: 232,
|
||||||
|
h: 72,
|
||||||
|
cx: b.x + b.w / 2,
|
||||||
|
cy: b.y + b.h / 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lotContainsPoint(lot, p) {
|
||||||
|
const b = expansionButtonBounds(lot);
|
||||||
|
return p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cellsInLot(lot) {
|
||||||
|
const cells = [];
|
||||||
|
for (let row = lot.rowStart; row < lot.rowStart + lot.rows; row += 1) {
|
||||||
|
for (let col = lot.colStart; col < lot.colStart + lot.cols; col += 1) cells.push({ col, row });
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shiftRowIndexedSet(set, rowDelta) {
|
||||||
|
const next = new Set();
|
||||||
|
for (const k of set || []) {
|
||||||
|
const p = parseKey(k);
|
||||||
|
next.add(key(p.col, p.row + rowDelta));
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shiftRowIndexedMap(map, rowDelta) {
|
||||||
|
const next = new Map();
|
||||||
|
for (const [k, v] of map || []) {
|
||||||
|
const p = parseKey(k);
|
||||||
|
next.set(key(p.col, p.row + rowDelta), v);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ownChunk(game, chunkX, chunkY) {
|
||||||
|
ensureOwnedChunks(game).add(chunkKey(chunkX, chunkY));
|
||||||
|
}
|
||||||
|
|
@ -41,3 +41,25 @@ export function generateBlockedCells({ seed, targetRatio = 0.20, reserved = [] }
|
||||||
const target = Math.round(GRID.cols * GRID.rows * targetRatio);
|
const target = Math.round(GRID.cols * GRID.rows * targetRatio);
|
||||||
return new Set(candidates.slice(0, Math.min(target, candidates.length)).map(c => c.k));
|
return new Set(candidates.slice(0, Math.min(target, candidates.length)).map(c => c.k));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateBlockedCellsInRect({ seed, colStart = 0, colEnd = GRID.cols, rowStart = 0, rowEnd = GRID.rows, targetRatio = 0.20, reserved = [] } = {}) {
|
||||||
|
const rng = mulberry32(hashSeed(seed || createRunSeed()));
|
||||||
|
const reservedSet = new Set(reserved.map(p => typeof p === 'string' ? p : key(p.col, p.row)));
|
||||||
|
const candidates = [];
|
||||||
|
const c0 = Math.max(0, Math.floor(colStart));
|
||||||
|
const c1 = Math.max(c0, Math.floor(colEnd));
|
||||||
|
const r0 = Math.max(0, Math.floor(rowStart));
|
||||||
|
const r1 = Math.max(r0, Math.floor(rowEnd));
|
||||||
|
for (let row = r0; row < r1; row += 1) {
|
||||||
|
for (let col = c0; col < c1; col += 1) {
|
||||||
|
const k = key(col, row);
|
||||||
|
if (!reservedSet.has(k)) candidates.push({ col, row, k });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = candidates.length - 1; i > 0; i -= 1) {
|
||||||
|
const j = Math.floor(rng() * (i + 1));
|
||||||
|
[candidates[i], candidates[j]] = [candidates[j], candidates[i]];
|
||||||
|
}
|
||||||
|
const target = Math.round((c1 - c0) * (r1 - r0) * targetRatio);
|
||||||
|
return new Set(candidates.slice(0, Math.min(target, candidates.length)).map(c => c.k));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { STARTING_CASH, TURN_SECONDS, FARM_SHUTDOWN_GRACE_SECONDS, BUILD_COSTS, FACILITY_DEFS, GRID, EGG_SPAWN_RANGES } from './config.js';
|
import { STARTING_CASH, TURN_SECONDS, FARM_SHUTDOWN_GRACE_SECONDS, BUILD_COSTS, FACILITY_DEFS, GRID, EGG_SPAWN_RANGES } from './config.js';
|
||||||
import { BALANCE } from './balance.js';
|
import { BALANCE } from './balance.js';
|
||||||
import { createRunSeed, generateBlockedCells } from './mapGen.js';
|
import { createRunSeed, generateBlockedCells } from './mapGen.js';
|
||||||
import { randomBetween, key, inGrid } from './utils.js';
|
import { resetOwnedCells } from './gridExpansion.js';
|
||||||
|
import { randomBetween, key, inGrid, directionNameBetweenCells } from './utils.js';
|
||||||
|
|
||||||
export function newTurnStats() {
|
export function newTurnStats() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -96,6 +97,12 @@ export function createGame() {
|
||||||
lastTimestamp: 0,
|
lastTimestamp: 0,
|
||||||
nextId: 10,
|
nextId: 10,
|
||||||
runSeed: createRunSeed(),
|
runSeed: createRunSeed(),
|
||||||
|
gridCols: BALANCE.grid.cols,
|
||||||
|
gridRows: BALANCE.grid.rows,
|
||||||
|
gridExpansionPurchases: 0,
|
||||||
|
gridExpansionPurchasesByDirection: { right: 0, up: 0 },
|
||||||
|
gridChunkOriginY: 0,
|
||||||
|
ownedCells: new Set(),
|
||||||
blockedCells: new Set(),
|
blockedCells: new Set(),
|
||||||
buildTool: null,
|
buildTool: null,
|
||||||
buildSession: 0,
|
buildSession: 0,
|
||||||
|
|
@ -106,7 +113,7 @@ export function createGame() {
|
||||||
selectionBox: null,
|
selectionBox: null,
|
||||||
groupDrag: null,
|
groupDrag: null,
|
||||||
pan: null,
|
pan: null,
|
||||||
view: { x: 0, y: 0 },
|
view: { x: 0, y: 0, scale: 1 },
|
||||||
hover: null,
|
hover: null,
|
||||||
chicks: [],
|
chicks: [],
|
||||||
effects: [],
|
effects: [],
|
||||||
|
|
@ -128,12 +135,12 @@ export function createGame() {
|
||||||
lastExplodedComponent: new Map(),
|
lastExplodedComponent: new Map(),
|
||||||
contractOffer: null,
|
contractOffer: null,
|
||||||
contractActive: null,
|
contractActive: null,
|
||||||
cardEffects: { bearing: 0, legalWork: 0, fairiesFlatteryNext: 0, crowdedFarming: 0, extraEggOutlet: 0, hatchingFeed: 0, safetyCover: 0, recyclingSubsidy: 0, chemicalWeaponSubsidy: 0, preventiveMaintenance: 0, 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, laborExploitation: 0, usedMachineActive: false, rescueLoanCharges: 0, loans: [] },
|
||||||
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
|
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
|
||||||
cardTargetPick: null,
|
cardTargetPick: null,
|
||||||
manualCombo: { count: 0, lastBonus: 0 },
|
manualCombo: { count: 0, lastBonus: 0 },
|
||||||
pendingRepairWorkerWages: 0,
|
pendingRepairWorkerWages: 0,
|
||||||
repairman: { hiredForNextDay: false, active: false, x: GRID.x + GRID.cell * 0.5, y: GRID.y + GRID.rows * GRID.cell + 84, target: null, repairedToday: 0 },
|
repairman: { hiredForNextDay: false, active: false, x: GRID.x + GRID.cell * 0.5, y: GRID.y + BALANCE.grid.rows * GRID.cell + 84, target: null, repairedToday: 0 },
|
||||||
stats: newTurnStats(),
|
stats: newTurnStats(),
|
||||||
lastResult: null,
|
lastResult: null,
|
||||||
totals: newTotalStats()
|
totals: newTotalStats()
|
||||||
|
|
@ -165,34 +172,34 @@ function facilityBodyForEntry(id, entry, side) {
|
||||||
|
|
||||||
export function defaultFacilities() {
|
export function defaultFacilities() {
|
||||||
return {
|
return {
|
||||||
// Machine bodies sit outside the build grid. Only these receiver cells sit on the grid edge.
|
// Initial grid is 20 columns × 10 rows. Machine bodies sit outside the starter grid.
|
||||||
mixer: facilityBodyForEntry('mixer', { col: 0, row: 5 }, 'left'),
|
mixer: facilityBodyForEntry('mixer', { col: 0, row: 4 }, 'left'),
|
||||||
trash: facilityBodyForEntry('trash', { col: 10, row: 12 }, 'bottom'),
|
trash: facilityBodyForEntry('trash', { col: 12, row: 9 }, 'bottom'),
|
||||||
truck: facilityBodyForEntry('truck', { col: 17, row: 12 }, 'bottom')
|
truck: facilityBodyForEntry('truck', { col: 18, row: 9 }, 'bottom')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_CONVEYOR_BACKBONE = Object.freeze([
|
const INITIAL_CONVEYOR_BACKBONE = Object.freeze([
|
||||||
// Egg Farm -> S1 top input. S1 starts lower so the farm is not pressed against it.
|
// Egg Farm -> S1 top input. The S1/S2 rows were moved down so unrelated
|
||||||
[4, 1], [4, 2], [4, 3], [4, 4],
|
// 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],
|
||||||
// S1 left output -> Mixer receiver on the left grid edge.
|
// S1 left output -> Mixer receiver on the left grid edge.
|
||||||
[3, 5], [2, 5], [1, 5], [0, 5],
|
[7, 4], [6, 4], [5, 4], [4, 4], [3, 4], [2, 4], [1, 4], [0, 4],
|
||||||
// S1 right output -> S2 top input.
|
// S1 right output -> S2 top input.
|
||||||
[5, 5], [6, 5], [7, 5], [8, 5], [9, 5], [10, 5], [11, 5],
|
[9, 4], [10, 4], [11, 4], [12, 4], [13, 4], [14, 4], [15, 4], [15, 5], [15, 6],
|
||||||
[11, 6], [11, 7],
|
|
||||||
// S2 left output -> Waste Shredder receiver on bottom grid edge.
|
// S2 left output -> Waste Shredder receiver on bottom grid edge.
|
||||||
[10, 8], [10, 9], [10, 10], [10, 11], [10, 12],
|
[14, 7], [13, 7], [12, 7], [12, 8], [12, 9],
|
||||||
// S2 right output -> Truck receiver on the bottom grid edge.
|
// S2 right output -> Truck receiver on the bottom grid edge.
|
||||||
[12, 8], [13, 8], [14, 8], [15, 8], [16, 8], [17, 8], [17, 9], [17, 10], [17, 11], [17, 12]
|
[16, 7], [17, 7], [18, 7], [18, 8], [18, 9]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const INITIAL_SCANNERS = Object.freeze([
|
const INITIAL_SCANNERS = Object.freeze([
|
||||||
{ type: 'scanner', id: 1, kind: 'manual', slot: 0, role: 0, col: 4, row: 5, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard', keys: { left: { key: 'a', code: 'KeyA', label: 'A' }, right: { key: 'd', code: 'KeyD', label: 'D' } } },
|
{ type: 'scanner', id: 1, kind: 'manual', slot: 0, role: 0, col: 8, row: 4, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard', keys: { left: { key: 'a', code: 'KeyA', label: 'A' }, right: { key: 'd', code: 'KeyD', label: 'D' } } },
|
||||||
{ type: 'scanner', id: 2, kind: 'manual', slot: 1, role: 1, col: 11, row: 8, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard', keys: { left: { key: 'arrowleft', code: 'ArrowLeft', label: 'Left' }, right: { key: 'arrowright', code: 'ArrowRight', label: 'Right' } } }
|
{ type: 'scanner', id: 2, kind: 'manual', slot: 1, role: 1, col: 15, row: 7, level: 1, queue: [], cooldown: 0, price: BUILD_COSTS.manualScanner, builtSession: null, autoMode: 'standard', keys: { left: { key: 'arrowleft', code: 'ArrowLeft', label: 'Left' }, right: { key: 'arrowright', code: 'ArrowRight', label: 'Right' } } }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const INITIAL_EGG_FARMS = Object.freeze([
|
const INITIAL_EGG_FARMS = Object.freeze([
|
||||||
{ type: 'eggFarm', id: 1, col: 4, row: 0, level: 1, nextSpawn: 2, lastInterval: 2, spawnCounter: 0, price: BUILD_COSTS.eggFarm, builtSession: null }
|
{ type: 'eggFarm', id: 1, col: 1, row: 0, level: 1, extraEggOutlet: 0, nextSpawn: 2, lastInterval: 2, spawnCounter: 0, price: BUILD_COSTS.eggFarm, builtSession: null }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function addConveyorTile(game, col, row, options = {}) {
|
function addConveyorTile(game, col, row, options = {}) {
|
||||||
|
|
@ -205,11 +212,34 @@ function addConveyorTile(game, col, row, options = {}) {
|
||||||
builtSession: null,
|
builtSession: null,
|
||||||
uses: 0,
|
uses: 0,
|
||||||
durability: BALANCE.maintenance.durability.conveyor,
|
durability: BALANCE.maintenance.durability.conveyor,
|
||||||
maintenanceType: 'conveyor'
|
maintenanceType: 'conveyor',
|
||||||
|
dir: options.dir || null,
|
||||||
|
outDirs: Array.isArray(options.outDirs) ? [...options.outDirs] : []
|
||||||
});
|
});
|
||||||
|
} 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])];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureConveyorMeta(game, col, row) {
|
||||||
|
const k = key(col, row);
|
||||||
|
if (!game.conveyorMeta.has(k)) addConveyorTile(game, col, row);
|
||||||
|
return game.conveyorMeta.get(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
function markInitialConveyorDirection(game, from, to) {
|
||||||
|
const dir = directionNameBetweenCells(from, to);
|
||||||
|
if (!dir) return;
|
||||||
|
const fromMeta = ensureConveyorMeta(game, from.col, from.row);
|
||||||
|
fromMeta.outDirs = Array.isArray(fromMeta.outDirs) ? fromMeta.outDirs : [];
|
||||||
|
if (!fromMeta.outDirs.includes(dir)) fromMeta.outDirs.push(dir);
|
||||||
|
fromMeta.dir = dir;
|
||||||
|
const toMeta = ensureConveyorMeta(game, to.col, to.row);
|
||||||
|
if (!toMeta.dir) toMeta.dir = dir;
|
||||||
|
}
|
||||||
|
|
||||||
function scannerPortCells(scanner) {
|
function scannerPortCells(scanner) {
|
||||||
return [
|
return [
|
||||||
{ col: scanner.col, row: scanner.row - 1 },
|
{ col: scanner.col, row: scanner.row - 1 },
|
||||||
|
|
@ -243,7 +273,9 @@ function protectedInitialCells(game) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function installInitialConveyorBackbone(game) {
|
function installInitialConveyorBackbone(game) {
|
||||||
for (const [col, row] of INITIAL_CONVEYOR_BACKBONE) addConveyorTile(game, col, row);
|
const cells = INITIAL_CONVEYOR_BACKBONE.map(([col, row]) => ({ col, row }));
|
||||||
|
for (const { col, row } of cells) addConveyorTile(game, col, row);
|
||||||
|
for (let i = 0; i < cells.length - 1; i += 1) markInitialConveyorDirection(game, cells[i], cells[i + 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrubBlockedCellsFromInitialBackbone(game) {
|
function scrubBlockedCellsFromInitialBackbone(game) {
|
||||||
|
|
@ -252,6 +284,21 @@ function scrubBlockedCellsFromInitialBackbone(game) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetLayout(game) {
|
export function resetLayout(game) {
|
||||||
|
GRID.x = BALANCE.grid.x;
|
||||||
|
GRID.y = BALANCE.grid.y;
|
||||||
|
GRID.cols = BALANCE.grid.cols;
|
||||||
|
GRID.rows = BALANCE.grid.rows;
|
||||||
|
game.gridCols = GRID.cols;
|
||||||
|
game.gridRows = GRID.rows;
|
||||||
|
resetOwnedCells(game);
|
||||||
|
game.gridOwnedCells = game.ownedCells.size;
|
||||||
|
if (game.repairman) {
|
||||||
|
game.repairman.x = GRID.x + GRID.cell * 0.5;
|
||||||
|
game.repairman.y = GRID.y + GRID.rows * GRID.cell + 84;
|
||||||
|
game.repairman.target = null;
|
||||||
|
game.repairman.active = false;
|
||||||
|
game.repairman.hiredForNextDay = false;
|
||||||
|
}
|
||||||
game.conveyorTiles.clear();
|
game.conveyorTiles.clear();
|
||||||
game.conveyorMeta.clear();
|
game.conveyorMeta.clear();
|
||||||
game.blockedCells = new Set();
|
game.blockedCells = new Set();
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,18 @@ export function pointToCell(x, y) {
|
||||||
}
|
}
|
||||||
export function distance(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); }
|
export function distance(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); }
|
||||||
export function sameCell(a, b) { return a && b && a.col === b.col && a.row === b.row; }
|
export function sameCell(a, b) { return a && b && a.col === b.col && a.row === b.row; }
|
||||||
|
|
||||||
|
export function directionNameBetweenCells(a, b) {
|
||||||
|
if (!a || !b) return null;
|
||||||
|
const dc = Math.sign((b.col ?? 0) - (a.col ?? 0));
|
||||||
|
const dr = Math.sign((b.row ?? 0) - (a.row ?? 0));
|
||||||
|
if (Math.abs((b.col ?? 0) - (a.col ?? 0)) + Math.abs((b.row ?? 0) - (a.row ?? 0)) !== 1) return null;
|
||||||
|
if (dc === 1 && dr === 0) return 'right';
|
||||||
|
if (dc === -1 && dr === 0) return 'left';
|
||||||
|
if (dc === 0 && dr === 1) return 'down';
|
||||||
|
if (dc === 0 && dr === -1) return 'up';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
export function lerp(a, b, t) { return a + (b - a) * t; }
|
export function lerp(a, b, t) { return a + (b - a) * t; }
|
||||||
export function hexToRgb(hex) {
|
export function hexToRgb(hex) {
|
||||||
const s = hex.replace('#', '');
|
const s = hex.replace('#', '');
|
||||||
|
|
|
||||||
301
src/game.js
301
src/game.js
|
|
@ -1,7 +1,8 @@
|
||||||
import { TURN_SECONDS, THEME } from './core/config.js';
|
import { GRID, TURN_SECONDS, THEME } from './core/config.js';
|
||||||
import { buildToolButtonHtml } from './core/text.js';
|
import { buildToolButtonHtml } from './core/text.js';
|
||||||
import { createGame, resetLayout, newTurnStats } from './core/state.js';
|
import { createGame, resetLayout, newTurnStats } from './core/state.js';
|
||||||
import { clamp, pointToCell, cellCenter, yen } from './core/utils.js';
|
import { clamp, pointToCell, cellCenter, key, yen } from './core/utils.js';
|
||||||
|
import { expansionLots, lotBounds } from './core/gridExpansion.js';
|
||||||
import { commitFactoryGraphForDay, facilityConnectionIssues } from './systems/routing.js';
|
import { commitFactoryGraphForDay, facilityConnectionIssues } from './systems/routing.js';
|
||||||
import { applyRevenue, collectChemicalWeaponSubsidy, collectFairiesTribute, collectLoanRepayments, collectZundaTax, settleTruckRevenue } from './systems/economy.js';
|
import { applyRevenue, collectChemicalWeaponSubsidy, collectFairiesTribute, collectLoanRepayments, collectZundaTax, settleTruckRevenue } from './systems/economy.js';
|
||||||
import { undo, redo } from './systems/history.js';
|
import { undo, redo } from './systems/history.js';
|
||||||
|
|
@ -9,7 +10,7 @@ import { drawAll } from './render/draw.js';
|
||||||
import { createBuildSystem } from './systems/buildSystem.js';
|
import { createBuildSystem } from './systems/buildSystem.js';
|
||||||
import { createUISystem } from './systems/uiSystem.js';
|
import { createUISystem } from './systems/uiSystem.js';
|
||||||
import { createChickSystem } from './systems/chickSystem.js';
|
import { createChickSystem } from './systems/chickSystem.js';
|
||||||
import { conveyorSpeedForGame, createCardSystem } from './systems/cards.js';
|
import { CARD_DEFS, conveyorSpeedForGame, createCardSystem, debugGrantCard } from './systems/cards.js';
|
||||||
import { floating, shockwave, sparkBurst, updateEffects } from './systems/effects.js';
|
import { floating, shockwave, sparkBurst, updateEffects } from './systems/effects.js';
|
||||||
import { rollContractOffer, activateAcceptedContract, clearActiveContract, resolveContract } from './systems/contracts.js';
|
import { rollContractOffer, activateAcceptedContract, clearActiveContract, resolveContract } from './systems/contracts.js';
|
||||||
import { ensureMaintenanceState, hireRepairmanForNextDay, activateRepairmanForDay, deactivateRepairman, conveyorSpeedFactorForKey } from './systems/maintenance.js';
|
import { ensureMaintenanceState, hireRepairmanForNextDay, activateRepairmanForDay, deactivateRepairman, conveyorSpeedFactorForKey } from './systems/maintenance.js';
|
||||||
|
|
@ -19,13 +20,25 @@ const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
const ui = {
|
const ui = {
|
||||||
shell: document.getElementById('gameShell'),
|
shell: document.getElementById('gameShell'),
|
||||||
money: document.getElementById('money'), turn: document.getElementById('turn'), timeLeft: document.getElementById('timeLeft'), phase: document.getElementById('phaseLabel'),
|
money: document.getElementById('money'), turn: document.getElementById('turn'), timeLeft: document.getElementById('timeLeft'), phase: document.getElementById('phaseLabel'), comboCount: document.getElementById('comboCount'),
|
||||||
eventBrief: document.getElementById('eventBrief'), targetBrief: document.getElementById('targetBrief'), speedBrief: document.getElementById('speedBrief'),
|
eventBrief: document.getElementById('eventBrief'), targetBrief: document.getElementById('targetBrief'), speedBrief: document.getElementById('speedBrief'),
|
||||||
turnProfit: document.getElementById('turnProfit'), mixerCount: document.getElementById('mixerCount'), truckFemaleCount: document.getElementById('truckFemaleCount'), truckMaleCount: document.getElementById('truckMaleCount'), poopCount: document.getElementById('poopCount'),
|
turnProfit: document.getElementById('turnProfit'), mixerCount: document.getElementById('mixerCount'), truckFemaleCount: document.getElementById('truckFemaleCount'), truckMaleCount: document.getElementById('truckMaleCount'), poopCount: document.getElementById('poopCount'),
|
||||||
buildStatus: document.getElementById('buildStatus'), turnSummary: document.getElementById('turnSummary'), contractPanel: document.getElementById('contractPanel'),
|
buildStatus: document.getElementById('buildStatus'), turnSummary: document.getElementById('turnSummary'), contractPanel: document.getElementById('contractPanel'),
|
||||||
modal: document.getElementById('modal'), modalTitle: document.getElementById('modalTitle'), modalBody: document.getElementById('modalBody'), modalActions: document.getElementById('modalActions'),
|
modal: document.getElementById('modal'), modalTitle: document.getElementById('modalTitle'), modalBody: document.getElementById('modalBody'), modalActions: document.getElementById('modalActions'),
|
||||||
hoverTooltip: document.getElementById('hoverTooltip'),
|
hoverTooltip: document.getElementById('hoverTooltip'),
|
||||||
|
manualScannerMonitor: document.getElementById('manualScannerMonitor'),
|
||||||
hireRepairmanButton: document.getElementById('hireRepairmanButton'),
|
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'),
|
||||||
|
cardSelect: document.getElementById('debugCardSelect'),
|
||||||
|
grantCard: document.getElementById('debugGrantCardButton'),
|
||||||
|
grantAllCards: document.getElementById('debugGrantAllCardsButton'),
|
||||||
|
readout: document.getElementById('debugReadout')
|
||||||
|
},
|
||||||
buttons: {
|
buttons: {
|
||||||
s1Left: document.getElementById('scanner1MixerButton'), s1Right: document.getElementById('scanner1TruckButton'), s2Left: document.getElementById('scanner2MixerButton'), s2Right: document.getElementById('scanner2TruckButton'),
|
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'), 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')
|
||||||
|
|
@ -48,6 +61,15 @@ function initializeStaticText() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initializeDebugPanel() {
|
||||||
|
if (!ui.debug?.panel) return;
|
||||||
|
if (ui.debug.cardSelect) {
|
||||||
|
ui.debug.cardSelect.innerHTML = CARD_DEFS
|
||||||
|
.map(card => `<option value="${card.id}">${card.title}</option>`)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const game = createGame();
|
const game = createGame();
|
||||||
let build;
|
let build;
|
||||||
let uiSystem;
|
let uiSystem;
|
||||||
|
|
@ -62,13 +84,60 @@ function currentConveyorSpeed(conveyorKey = null) {
|
||||||
function startGame() {
|
function startGame() {
|
||||||
Object.assign(game, createGame());
|
Object.assign(game, createGame());
|
||||||
game.phase = 'running';
|
game.phase = 'running';
|
||||||
game.view = { x: 0, y: 0 };
|
game.view = { x: 0, y: 0, scale: 1 };
|
||||||
resetLayout(game);
|
resetLayout(game);
|
||||||
|
resetCameraToFactoryStart();
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
uiSystem.hideModal();
|
uiSystem.hideModal();
|
||||||
chicks.updateCongestion();
|
chicks.updateCongestion();
|
||||||
uiSystem.updatePanels();
|
uiSystem.updatePanels();
|
||||||
uiSystem.updateUI();
|
uiSystem.updateUI();
|
||||||
|
updateDebugReadout();
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugSetDay() {
|
||||||
|
const day = Math.max(1, Math.floor(Number(ui.debug.dayInput?.value) || 1));
|
||||||
|
game.turn = day;
|
||||||
|
if (ui.debug.dayInput) ui.debug.dayInput.value = String(day);
|
||||||
|
floating(game, canvas.width / 2 - game.view.x, 86 - game.view.y, `DAY ${day}`, THEME.green);
|
||||||
|
uiSystem.updatePanels();
|
||||||
|
uiSystem.updateUI();
|
||||||
|
updateDebugReadout(`Day set to ${day}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugGrantSelectedCard() {
|
||||||
|
const id = ui.debug.cardSelect?.value;
|
||||||
|
const result = debugGrantCard(game, id);
|
||||||
|
uiSystem.updatePanels();
|
||||||
|
uiSystem.updateUI();
|
||||||
|
updateDebugReadout(result.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugGrantAllCards() {
|
||||||
|
const results = CARD_DEFS.map(card => debugGrantCard(game, card));
|
||||||
|
const ok = results.filter(result => result.ok).length;
|
||||||
|
uiSystem.updatePanels();
|
||||||
|
uiSystem.updateUI();
|
||||||
|
updateDebugReadout(`Granted ${ok}/${CARD_DEFS.length} cards.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDebugState() {
|
||||||
|
if (ui.debug?.infiniteCash?.checked && game.cash < 999999) {
|
||||||
|
game.cash = 999999;
|
||||||
|
game.totals.maxCash = Math.max(game.totals.maxCash, game.cash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDebugReadout(message = '') {
|
||||||
|
if (!ui.debug?.readout) return;
|
||||||
|
ui.debug.readout.textContent = [
|
||||||
|
message,
|
||||||
|
`D${game.turn}`,
|
||||||
|
yen(game.cash),
|
||||||
|
`Combo ${game.manualCombo?.count || 0}`,
|
||||||
|
`Rescue ${game.cardEffects?.rescueLoanCharges || 0}`,
|
||||||
|
`Chem ${game.cardEffects?.chemicalWeaponSubsidy || 0}`
|
||||||
|
].filter(Boolean).join(' | ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function startNextTurn() {
|
function startNextTurn() {
|
||||||
|
|
@ -87,8 +156,8 @@ function startNextTurn() {
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
commitFactoryGraphForDay(game);
|
commitFactoryGraphForDay(game);
|
||||||
activateRepairmanForDay(game);
|
activateRepairmanForDay(game);
|
||||||
const zundaBasisCash = game.cash;
|
const zundaBasisProfit = Math.max(0, Math.floor(game.lastResult?.profit ?? game.stats?.profit ?? 0));
|
||||||
const zunda = collectZundaTax(game, zundaBasisCash);
|
const zunda = collectZundaTax(game, zundaBasisProfit);
|
||||||
const loanRepayment = collectLoanRepayments(game);
|
const loanRepayment = collectLoanRepayments(game);
|
||||||
game.lastStartFees = { zundaTax: zunda.tax || 0, loanRepayment: loanRepayment.amount || 0, fairiesTribute: 0 };
|
game.lastStartFees = { zundaTax: zunda.tax || 0, loanRepayment: loanRepayment.amount || 0, fairiesTribute: 0 };
|
||||||
if (zunda.tax > 0) floating(game, canvas.width / 2 - game.view.x, 116 - game.view.y, `ZUNDA TAX -${yen(zunda.tax)}`, THEME.danger);
|
if (zunda.tax > 0) floating(game, canvas.width / 2 - game.view.x, 116 - game.view.y, `ZUNDA TAX -${yen(zunda.tax)}`, THEME.danger);
|
||||||
|
|
@ -106,12 +175,12 @@ function startNextTurn() {
|
||||||
game.floatingTexts = [];
|
game.floatingTexts = [];
|
||||||
game.shake = { time: 0, strength: 0 };
|
game.shake = { time: 0, strength: 0 };
|
||||||
game.truckCargo = [];
|
game.truckCargo = [];
|
||||||
game.view = { x: 0, y: 0 };
|
resetCameraToFactoryStart();
|
||||||
game.stats = newTurnStats();
|
game.stats = newTurnStats();
|
||||||
game.stats.zundaTax = game.lastStartFees?.zundaTax || 0;
|
game.stats.zundaTax = 0;
|
||||||
game.stats.loanRepayment = game.lastStartFees?.loanRepayment || 0;
|
game.stats.loanRepayment = game.lastStartFees?.loanRepayment || 0;
|
||||||
game.stats.repairWorkerWages = game.pendingRepairWorkerWages || 0;
|
game.stats.repairWorkerWages = game.pendingRepairWorkerWages || 0;
|
||||||
const carriedStartCosts = game.stats.zundaTax + game.stats.loanRepayment + game.stats.repairWorkerWages;
|
const carriedStartCosts = game.stats.loanRepayment + game.stats.repairWorkerWages;
|
||||||
if (carriedStartCosts > 0) {
|
if (carriedStartCosts > 0) {
|
||||||
game.stats.penalty += carriedStartCosts;
|
game.stats.penalty += carriedStartCosts;
|
||||||
game.stats.profit -= carriedStartCosts;
|
game.stats.profit -= carriedStartCosts;
|
||||||
|
|
@ -234,21 +303,96 @@ function update(timestamp) {
|
||||||
if (!game.lastTimestamp) game.lastTimestamp = timestamp;
|
if (!game.lastTimestamp) game.lastTimestamp = timestamp;
|
||||||
const dt = Math.min((timestamp - game.lastTimestamp) / 1000, 0.05);
|
const dt = Math.min((timestamp - game.lastTimestamp) / 1000, 0.05);
|
||||||
game.lastTimestamp = timestamp;
|
game.lastTimestamp = timestamp;
|
||||||
|
applyDebugState();
|
||||||
if (game.phase === 'running') chicks.updateRunning(dt, { closeFarmShutters, completeTurn });
|
if (game.phase === 'running') chicks.updateRunning(dt, { closeFarmShutters, completeTurn });
|
||||||
updateEffects(game, dt, canvas, build.equipmentHitBoxes);
|
updateEffects(game, dt, canvas, build.equipmentHitBoxes);
|
||||||
chicks.updateCongestion();
|
chicks.updateCongestion();
|
||||||
drawAll(ctx, canvas, game, { activeQueuedChick: chicks.activeQueuedChick, selectedObject: build.selectedObject, selectedTitle: build.selectedTitle });
|
drawAll(ctx, canvas, game, { activeQueuedChick: chicks.activeQueuedChick, selectedObject: build.selectedObject, selectedTitle: build.selectedTitle });
|
||||||
uiSystem.updateUI();
|
uiSystem.updateUI();
|
||||||
|
updateManualScannerMonitor();
|
||||||
|
updateDebugReadout();
|
||||||
requestAnimationFrame(update);
|
requestAnimationFrame(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build/edit/selection operations live in src/systems/buildSystem.js.
|
// Build/edit/selection operations live in src/systems/buildSystem.js.
|
||||||
// HUD, panels, modal screens, and gameover rendering live in src/systems/uiSystem.js.
|
// HUD, panels, modal screens, and gameover rendering live in src/systems/uiSystem.js.
|
||||||
// Hitbox generation lives in buildSystem.js.
|
// Hitbox generation lives in buildSystem.js.
|
||||||
|
function viewScale() { return game.view?.scale || 1; }
|
||||||
|
function ensureView() {
|
||||||
|
if (!game.view) game.view = { x: 0, y: 0, scale: 1 };
|
||||||
|
if (game.view.scale == null) game.view.scale = 1;
|
||||||
|
}
|
||||||
function rawCanvasPoint(event) { const r = canvas.getBoundingClientRect(); return { x: (event.clientX - r.left) * canvas.width / r.width, y: (event.clientY - r.top) * canvas.height / r.height }; }
|
function rawCanvasPoint(event) { const r = canvas.getBoundingClientRect(); return { x: (event.clientX - r.left) * canvas.width / r.width, y: (event.clientY - r.top) * canvas.height / r.height }; }
|
||||||
function canvasPoint(event) { const p = rawCanvasPoint(event); return { x: p.x - game.view.x, y: p.y - game.view.y }; }
|
function canvasPoint(event) { const p = rawCanvasPoint(event); const s = viewScale(); return { x: (p.x - (game.view?.x || 0)) / s, y: (p.y - (game.view?.y || 0)) / s }; }
|
||||||
function startPan(event) { const p = rawCanvasPoint(event); game.pan = { start: p, viewX: game.view.x, viewY: game.view.y }; }
|
function worldToScreen(p) { const s = viewScale(); return { x: p.x * s + (game.view?.x || 0), y: p.y * s + (game.view?.y || 0) }; }
|
||||||
function updatePan(event) { if (!game.pan) return; const p = rawCanvasPoint(event); game.view.x = clamp(game.pan.viewX + p.x - game.pan.start.x, -620, 420); game.view.y = clamp(game.pan.viewY + p.y - game.pan.start.y, -340, 240); }
|
function cameraWorldBounds() {
|
||||||
|
const bounds = {
|
||||||
|
left: GRID.x - 90,
|
||||||
|
top: GRID.y - 130,
|
||||||
|
right: GRID.x + GRID.cols * GRID.cell + 260,
|
||||||
|
bottom: GRID.y + GRID.rows * GRID.cell + 220
|
||||||
|
};
|
||||||
|
if (game.phase === 'build') {
|
||||||
|
for (const lot of expansionLots(game)) {
|
||||||
|
const b = lotBounds(lot);
|
||||||
|
bounds.left = Math.min(bounds.left, b.x - 60);
|
||||||
|
bounds.top = Math.min(bounds.top, b.y - 80);
|
||||||
|
bounds.right = Math.max(bounds.right, b.x + b.w + 80);
|
||||||
|
bounds.bottom = Math.max(bounds.bottom, b.y + b.h + 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedClampRange(min, max) {
|
||||||
|
if (min <= max) return [min, max];
|
||||||
|
const mid = (min + max) / 2;
|
||||||
|
return [mid, mid];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampCamera() {
|
||||||
|
ensureView();
|
||||||
|
const s = viewScale();
|
||||||
|
const b = cameraWorldBounds();
|
||||||
|
const [minX, maxX] = normalizedClampRange(canvas.width - 160 - b.right * s, 110 - b.left * s);
|
||||||
|
const [minY, maxY] = normalizedClampRange(canvas.height - 150 - b.bottom * s, 150 - b.top * s);
|
||||||
|
game.view.x = clamp(game.view.x, minX, maxX);
|
||||||
|
game.view.y = clamp(game.view.y, minY, maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCameraToFactoryStart() {
|
||||||
|
ensureView();
|
||||||
|
game.view.scale = 1;
|
||||||
|
const farm = game.eggFarms?.[0];
|
||||||
|
const anchor = farm ? cellCenter(farm.col, farm.row) : { x: GRID.x, y: GRID.y };
|
||||||
|
// Keep the initial EGG and the left-side Mixer fully visible. The previous
|
||||||
|
// anchor placed the EGG at 150,150, which clipped the Mixer body at the left edge.
|
||||||
|
game.view.x = 280 - anchor.x;
|
||||||
|
game.view.y = 190 - anchor.y;
|
||||||
|
clampCamera();
|
||||||
|
}
|
||||||
|
function startPan(event) { const p = rawCanvasPoint(event); ensureView(); game.pan = { start: p, viewX: game.view.x, viewY: game.view.y }; }
|
||||||
|
function updatePan(event) {
|
||||||
|
if (!game.pan) return;
|
||||||
|
const p = rawCanvasPoint(event);
|
||||||
|
ensureView();
|
||||||
|
game.view.x = game.pan.viewX + p.x - game.pan.start.x;
|
||||||
|
game.view.y = game.pan.viewY + p.y - game.pan.start.y;
|
||||||
|
clampCamera();
|
||||||
|
}
|
||||||
|
function zoomAt(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
ensureView();
|
||||||
|
const raw = rawCanvasPoint(event);
|
||||||
|
const before = canvasPoint(event);
|
||||||
|
const factor = Math.exp(-event.deltaY * 0.0012);
|
||||||
|
const nextScale = clamp(viewScale() * factor, 0.55, 1.8);
|
||||||
|
game.view.scale = nextScale;
|
||||||
|
game.view.x = raw.x - before.x * nextScale;
|
||||||
|
game.view.y = raw.y - before.y * nextScale;
|
||||||
|
clampCamera();
|
||||||
|
hideHoverTooltip();
|
||||||
|
}
|
||||||
function selectionForHit(hit) { if (!hit) return null; if (hit.type === 'conveyor') return { type: 'conveyor', id: hit.oldKey }; return { type: hit.type, id: hit.ref.id }; }
|
function selectionForHit(hit) { if (!hit) return null; if (hit.type === 'conveyor') return { type: 'conveyor', id: hit.oldKey }; return { type: hit.type, id: hit.ref.id }; }
|
||||||
function objectFromHit(hit) {
|
function objectFromHit(hit) {
|
||||||
if (!hit) return null;
|
if (!hit) return null;
|
||||||
|
|
@ -298,7 +442,115 @@ function clickSelect(event) {
|
||||||
if (obj?.type === 'scanner' && obj.kind === 'manual') build.showManualScannerMenu(obj);
|
if (obj?.type === 'scanner' && obj.kind === 'manual') build.showManualScannerMenu(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value).replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeQueuedForScanner(scanner) {
|
||||||
|
const id = scanner?.queue?.[0];
|
||||||
|
return id == null ? null : game.chicks.find(chick => chick.id === id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function chickDisplay(chick) {
|
||||||
|
if (!chick) return '<span class="scanner-current empty">WAIT</span>';
|
||||||
|
const label = chick.sex === 'poop' ? '💩 POOP' : chick.sex === 'male' ? '♂ MALE' : '♀ FEMALE';
|
||||||
|
return `<span class="scanner-current ${chick.sex}">${label}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function miniScannerGrid(scanner, chick) {
|
||||||
|
const farmCells = new Set(game.eggFarms.map(farm => key(farm.col, farm.row)));
|
||||||
|
const scannerCells = new Map(game.scanners.map(s => [key(s.col, s.row), s]));
|
||||||
|
const facilityEntryCells = new Set(Object.values(game.facilities || {}).map(f => f.entry ? key(f.entry.col, f.entry.row) : null).filter(Boolean));
|
||||||
|
const activeSex = chick?.sex || '';
|
||||||
|
const activeLabel = activeSex === 'poop' ? '💩' : activeSex === 'male' ? '♂' : activeSex === 'female' ? '♀' : '';
|
||||||
|
const cells = [];
|
||||||
|
for (let row = scanner.row - 2; row <= scanner.row + 2; row += 1) {
|
||||||
|
for (let col = scanner.col - 2; col <= scanner.col + 2; col += 1) {
|
||||||
|
const k = key(col, row);
|
||||||
|
const classes = ['mini-cell'];
|
||||||
|
let label = '';
|
||||||
|
if (col === scanner.col && row === scanner.row) {
|
||||||
|
classes.push('scanner');
|
||||||
|
if (activeSex) classes.push(activeSex);
|
||||||
|
label = activeLabel || 'S';
|
||||||
|
} else if (row === scanner.row - 1 && col === scanner.col) {
|
||||||
|
classes.push('input');
|
||||||
|
label = 'IN';
|
||||||
|
} else if (farmCells.has(k)) {
|
||||||
|
classes.push('egg');
|
||||||
|
label = 'E';
|
||||||
|
} else if (scannerCells.has(k)) {
|
||||||
|
classes.push('scanner-other');
|
||||||
|
label = 'S';
|
||||||
|
} else if (facilityEntryCells.has(k)) {
|
||||||
|
classes.push('port');
|
||||||
|
label = 'P';
|
||||||
|
} else if (game.conveyorTiles.has(k)) {
|
||||||
|
classes.push('belt');
|
||||||
|
label = '•';
|
||||||
|
} else if (game.blockedCells?.has?.(k)) {
|
||||||
|
classes.push('blocked');
|
||||||
|
label = '×';
|
||||||
|
}
|
||||||
|
cells.push(`<span class="${classes.join(' ')}">${label}</span>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `<div class="scanner-mini-grid" aria-label="scanner vicinity">${cells.join('')}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scannerIsOffscreen(scanner) {
|
||||||
|
const screen = worldToScreen(cellCenter(scanner.col, scanner.row));
|
||||||
|
const margin = 76;
|
||||||
|
return screen.x < -margin || screen.y < -margin || screen.x > canvas.width + margin || screen.y > canvas.height + margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateManualScannerMonitor() {
|
||||||
|
if (!ui.manualScannerMonitor) return;
|
||||||
|
if (game.phase !== 'running') {
|
||||||
|
ui.manualScannerMonitor.classList.remove('visible');
|
||||||
|
ui.manualScannerMonitor.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scanners = game.scanners.filter(scanner => scanner.kind === 'manual' && scannerIsOffscreen(scanner));
|
||||||
|
if (!scanners.length) {
|
||||||
|
ui.manualScannerMonitor.classList.remove('visible');
|
||||||
|
ui.manualScannerMonitor.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ui.manualScannerMonitor.innerHTML = scanners.map(scanner => {
|
||||||
|
const chick = activeQueuedForScanner(scanner);
|
||||||
|
const left = scanner.keys?.left?.label || (scanner.slot === 0 ? 'A' : 'Left');
|
||||||
|
const right = scanner.keys?.right?.label || (scanner.slot === 0 ? 'D' : 'Right');
|
||||||
|
const leftDest = scanner.role === 0 ? 'MIXER' : 'WASTE';
|
||||||
|
const rightDest = scanner.role === 0 ? 'S2' : 'TRUCK';
|
||||||
|
const disabled = chick ? '' : ' disabled';
|
||||||
|
return `
|
||||||
|
<section class="scanner-monitor-card">
|
||||||
|
<header><strong>S${(scanner.slot ?? 0) + 1}</strong><span>OFFSCREEN / Q:${scanner.queue.length}</span></header>
|
||||||
|
<div class="scanner-mini-map">
|
||||||
|
<span>IN</span><b>${chickDisplay(chick)}</b><span>SCANNER</span>
|
||||||
|
</div>
|
||||||
|
${miniScannerGrid(scanner, chick)}
|
||||||
|
<div class="scanner-monitor-actions">
|
||||||
|
<button type="button" data-scanner-id="${scanner.id}" data-side="left"${disabled}>${escapeHtml(left)} → ${leftDest}</button>
|
||||||
|
<button type="button" data-scanner-id="${scanner.id}" data-side="right"${disabled}>${escapeHtml(right)} → ${rightDest}</button>
|
||||||
|
</div>
|
||||||
|
</section>`;
|
||||||
|
}).join('');
|
||||||
|
ui.manualScannerMonitor.classList.add('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.manualScannerMonitor?.addEventListener('click', event => {
|
||||||
|
const button = event.target.closest('button[data-scanner-id]');
|
||||||
|
if (!button || button.disabled) return;
|
||||||
|
const scanner = game.scanners.find(s => s.id === Number(button.dataset.scannerId));
|
||||||
|
if (!scanner) return;
|
||||||
|
chicks.sortScanner(scanner, button.dataset.side);
|
||||||
|
updateManualScannerMonitor();
|
||||||
|
});
|
||||||
|
|
||||||
canvas.addEventListener('contextmenu', event => event.preventDefault());
|
canvas.addEventListener('contextmenu', event => event.preventDefault());
|
||||||
|
canvas.addEventListener('wheel', zoomAt, { passive: false });
|
||||||
canvas.addEventListener('pointerdown', event => {
|
canvas.addEventListener('pointerdown', event => {
|
||||||
if (game.phase !== 'build') return;
|
if (game.phase !== 'build') return;
|
||||||
canvas.setPointerCapture(event.pointerId);
|
canvas.setPointerCapture(event.pointerId);
|
||||||
|
|
@ -306,8 +558,14 @@ canvas.addEventListener('pointerdown', event => {
|
||||||
if (event.button === 2) { startPan(event); return; }
|
if (event.button === 2) { startPan(event); return; }
|
||||||
if (event.button !== 0) return;
|
if (event.button !== 0) return;
|
||||||
if (game.cardTargetPick?.pending) { cardSystem.chooseTargetAtPoint(world); 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 (game.buildTool === 'erase') { build.eraseAtPoint(world); return; }
|
||||||
if (game.buildTool) {
|
if (game.buildTool) {
|
||||||
|
if (game.buildTool === 'conveyor') {
|
||||||
|
build.beginConveyorDrag(pointToCell(world.x, world.y));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const hit = build.equipmentAtPoint(world);
|
const hit = build.equipmentAtPoint(world);
|
||||||
if (hit) { game.buildTool = null; clickSelect(event); return; }
|
if (hit) { game.buildTool = null; clickSelect(event); return; }
|
||||||
build.buildAtPoint(world);
|
build.buildAtPoint(world);
|
||||||
|
|
@ -322,7 +580,7 @@ canvas.addEventListener('pointermove', event => {
|
||||||
if (game.pan && (event.buttons & 2)) updatePan(event);
|
if (game.pan && (event.buttons & 2)) updatePan(event);
|
||||||
if (game.groupDrag && (event.buttons & 1)) build.updateGroupDrag(event);
|
if (game.groupDrag && (event.buttons & 1)) build.updateGroupDrag(event);
|
||||||
if (game.selectionBox && (event.buttons & 1)) build.updateSelectionBox(event);
|
if (game.selectionBox && (event.buttons & 1)) build.updateSelectionBox(event);
|
||||||
if (game.buildTool === 'conveyor' && (event.buttons & 1)) build.buildAtCell(pointToCell(canvasPoint(event).x, canvasPoint(event).y));
|
if (game.buildTool === 'conveyor' && (event.buttons & 1)) build.continueConveyorDrag(pointToCell(canvasPoint(event).x, canvasPoint(event).y));
|
||||||
if (game.buildTool === 'erase' && (event.buttons & 1)) build.eraseAtPoint(canvasPoint(event));
|
if (game.buildTool === 'erase' && (event.buttons & 1)) build.eraseAtPoint(canvasPoint(event));
|
||||||
});
|
});
|
||||||
canvas.addEventListener('pointerup', event => {
|
canvas.addEventListener('pointerup', event => {
|
||||||
|
|
@ -331,11 +589,12 @@ canvas.addEventListener('pointerup', event => {
|
||||||
if (!game.groupDrag.committed) clickSelect(event);
|
if (!game.groupDrag.committed) clickSelect(event);
|
||||||
build.finishGroupDrag();
|
build.finishGroupDrag();
|
||||||
}
|
}
|
||||||
|
if (game.buildTool === 'conveyor') build.endConveyorDrag?.();
|
||||||
game.groupDrag = null; game.pan = null;
|
game.groupDrag = null; game.pan = null;
|
||||||
try { canvas.releasePointerCapture(event.pointerId); } catch (_) { /* noop */ }
|
try { canvas.releasePointerCapture(event.pointerId); } catch (_) { /* noop */ }
|
||||||
});
|
});
|
||||||
canvas.addEventListener('pointerleave', hideHoverTooltip);
|
canvas.addEventListener('pointerleave', hideHoverTooltip);
|
||||||
canvas.addEventListener('pointercancel', () => { game.groupDrag = null; game.selectionBox = null; game.pan = null; hideHoverTooltip(); });
|
canvas.addEventListener('pointercancel', () => { build?.endConveyorDrag?.(); game.groupDrag = null; game.selectionBox = null; game.pan = null; hideHoverTooltip(); });
|
||||||
document.addEventListener('pointerdown', event => {
|
document.addEventListener('pointerdown', event => {
|
||||||
if (!ui.modal.classList.contains('visible') || !ui.modal.classList.contains('equipment-popover')) return;
|
if (!ui.modal.classList.contains('visible') || !ui.modal.classList.contains('equipment-popover')) return;
|
||||||
if (event.target.closest('#modal .modal-card')) return;
|
if (event.target.closest('#modal .modal-card')) return;
|
||||||
|
|
@ -345,7 +604,11 @@ document.addEventListener('pointerdown', event => {
|
||||||
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.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.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.hireRepairmanButton?.addEventListener('click', hireRepairman);
|
||||||
ui.buttons.undo.addEventListener('click', () => { if (undo(game)) uiSystem.updatePanels(); }); ui.buttons.redo.addEventListener('click', () => { if (redo(game)) uiSystem.updatePanels(); });
|
ui.expandGridButton?.addEventListener('click', () => { build?.buyGridExpansion?.('right'); clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); });
|
||||||
|
ui.debug.setDay?.addEventListener('click', debugSetDay);
|
||||||
|
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(); } });
|
||||||
function scannerKeyMatches(scanner, side, event) {
|
function scannerKeyMatches(scanner, side, event) {
|
||||||
const binding = scanner.keys?.[side];
|
const binding = scanner.keys?.[side];
|
||||||
if (!binding) return false;
|
if (!binding) return false;
|
||||||
|
|
@ -361,8 +624,8 @@ window.addEventListener('keydown', event => {
|
||||||
if (scannerKeyMatches(scanner, 'right', event)) { event.preventDefault(); chicks.sortScanner(scanner, 'right'); return; }
|
if (scannerKeyMatches(scanner, 'right', event)) { event.preventDefault(); chicks.sortScanner(scanner, 'right'); return; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && name === 'z') { event.preventDefault(); if (undo(game)) uiSystem.updatePanels(); }
|
if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && name === '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)) uiSystem.updatePanels(); }
|
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(); cardSystem.cancelTargetPick(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -372,4 +635,4 @@ build = createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanels: () =>
|
||||||
chicks = createChickSystem({ game, currentConveyorSpeed, onGameOverCheck: () => uiSystem?.checkGameOver() });
|
chicks = createChickSystem({ game, currentConveyorSpeed, onGameOverCheck: () => uiSystem?.checkGameOver() });
|
||||||
cardSystem = createCardSystem({ game, ui, onUpdatePanels: () => uiSystem?.updatePanels() });
|
cardSystem = createCardSystem({ game, ui, onUpdatePanels: () => uiSystem?.updatePanels() });
|
||||||
uiSystem = createUISystem({ game, ui, build, startGame, beginCardDraft: () => cardSystem.showDraft(), activeQueuedChick: chicks.activeQueuedChick });
|
uiSystem = createUISystem({ game, ui, build, startGame, beginCardDraft: () => cardSystem.showDraft(), activeQueuedChick: chicks.activeQueuedChick });
|
||||||
initializeStaticText(); resetLayout(game); ensureMaintenanceState(game); chicks.updateCongestion(); uiSystem.updatePanels(); uiSystem.updateUI(); uiSystem.showTitle(); requestAnimationFrame(update);
|
initializeStaticText(); initializeDebugPanel(); resetLayout(game); ensureMaintenanceState(game); chicks.updateCongestion(); uiSystem.updatePanels(); uiSystem.updateUI(); uiSystem.showTitle(); updateDebugReadout(); requestAnimationFrame(update);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { GRID, THEME, EFFECT_PRIORITY, VERSION } from '../core/config.js';
|
import { DIRS, GRID, THEME, EFFECT_PRIORITY, VERSION } from '../core/config.js';
|
||||||
|
import { expansionCost, expansionLots, lotBounds, expansionButtonBounds, isOwnedCell } from '../core/gridExpansion.js';
|
||||||
import { key, parseKey, pointToCell, cellCenter, mixHex, randomBetween, yen } from '../core/utils.js';
|
import { key, parseKey, pointToCell, cellCenter, mixHex, randomBetween, yen } from '../core/utils.js';
|
||||||
import { getConveyorNeighbors, routeFromFarmToScanner, outputRoute, scannerCenter, scannerConnector, scannerOutputs, destinationLabel, destinationColor, scannerPortHasConveyor, disconnectedBuildWarnings } from '../systems/routing.js';
|
import { getConveyorNeighbors, routeFromFarmToScanner, outputRoute, scannerCenter, scannerConnector, scannerOutputs, destinationLabel, destinationColor, scannerPortHasConveyor, disconnectedBuildWarnings } from '../systems/routing.js';
|
||||||
import { upgradedMixerPrice, upgradedTruckPrice } from '../systems/economy.js';
|
import { upgradedMixerPrice, upgradedTruckPrice } from '../systems/economy.js';
|
||||||
|
|
@ -42,8 +43,11 @@ export function drawAll(ctx, canvas, game, helpers) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
const sx = game.shake.time > 0 ? randomBetween(-game.shake.strength, game.shake.strength) : 0;
|
const sx = game.shake.time > 0 ? randomBetween(-game.shake.strength, game.shake.strength) : 0;
|
||||||
const sy = game.shake.time > 0 ? randomBetween(-game.shake.strength, game.shake.strength) : 0;
|
const sy = game.shake.time > 0 ? randomBetween(-game.shake.strength, game.shake.strength) : 0;
|
||||||
ctx.translate(game.view.x + sx, game.view.y + sy);
|
const scale = game.view?.scale || 1;
|
||||||
|
ctx.translate((game.view?.x || 0) + sx, (game.view?.y || 0) + sy);
|
||||||
|
ctx.scale(scale, scale);
|
||||||
drawGrid(ctx, game);
|
drawGrid(ctx, game);
|
||||||
|
drawExpansionOffers(ctx, game);
|
||||||
drawConveyors(ctx, game);
|
drawConveyors(ctx, game);
|
||||||
drawScannerConnectors(ctx, game);
|
drawScannerConnectors(ctx, game);
|
||||||
drawFacilities(ctx, game);
|
drawFacilities(ctx, game);
|
||||||
|
|
@ -85,11 +89,11 @@ function drawDisconnectedWarnings(ctx, game) {
|
||||||
ctx.fillStyle = THEME.danger;
|
ctx.fillStyle = THEME.danger;
|
||||||
ctx.strokeStyle = THEME.white;
|
ctx.strokeStyle = THEME.white;
|
||||||
ctx.lineWidth = 4;
|
ctx.lineWidth = 4;
|
||||||
ctx.font = '900 18px ui-monospace, monospace';
|
ctx.font = '900 16px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.strokeText('!', b.cx, b.y + 24);
|
ctx.strokeText('!', b.cx, b.y + 24);
|
||||||
ctx.fillText('!', b.cx, b.y + 24);
|
ctx.fillText('!', b.cx, b.y + 24);
|
||||||
ctx.font = '900 9px ui-monospace, monospace';
|
ctx.font = '900 11px ui-monospace, monospace';
|
||||||
ctx.strokeText('NO ROUTE', b.cx, b.y + b.h - 8);
|
ctx.strokeText('NO ROUTE', b.cx, b.y + b.h - 8);
|
||||||
ctx.fillText('NO ROUTE', b.cx, b.y + b.h - 8);
|
ctx.fillText('NO ROUTE', b.cx, b.y + b.h - 8);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
@ -100,7 +104,7 @@ function rect(ctx, x, y, w, h, fill = true, stroke = true) {
|
||||||
ctx.beginPath(); ctx.rect(x, y, w, h); if (fill) ctx.fill(); if (stroke) ctx.stroke();
|
ctx.beginPath(); ctx.rect(x, y, w, h); if (fill) ctx.fill(); if (stroke) ctx.stroke();
|
||||||
}
|
}
|
||||||
function label(ctx, x, y, text, color = THEME.ink) {
|
function label(ctx, x, y, text, color = THEME.ink) {
|
||||||
ctx.save(); ctx.font = '900 10px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.lineWidth = 4; ctx.strokeStyle = THEME.white; ctx.fillStyle = color; ctx.strokeText(text, x, y); ctx.fillText(text, x, y); ctx.restore();
|
ctx.save(); ctx.font = '900 12px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.lineWidth = 4; ctx.strokeStyle = THEME.white; ctx.fillStyle = color; ctx.strokeText(text, x, y); ctx.fillText(text, x, y); ctx.restore();
|
||||||
}
|
}
|
||||||
function drawImageIfLoaded(ctx, image, x, y, w, h) {
|
function drawImageIfLoaded(ctx, image, x, y, w, h) {
|
||||||
if (!image?.loaded) return false;
|
if (!image?.loaded) return false;
|
||||||
|
|
@ -117,22 +121,38 @@ function drawBackground(ctx, canvas) {
|
||||||
for (let y = 0; y < canvas.height; y += 32) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); }
|
for (let y = 0; y < canvas.height; y += 32) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); }
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
function ownedCellKeys(game) {
|
||||||
|
if (game.ownedCells?.size) return [...game.ownedCells];
|
||||||
|
const cells = [];
|
||||||
|
for (let row = 0; row < GRID.rows; row += 1) for (let col = 0; col < GRID.cols; col += 1) cells.push(key(col, row));
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
function drawGrid(ctx, game) {
|
function drawGrid(ctx, game) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
const cells = ownedCellKeys(game);
|
||||||
ctx.fillStyle = 'rgba(255,255,255,.46)';
|
ctx.fillStyle = 'rgba(255,255,255,.46)';
|
||||||
ctx.strokeStyle = 'rgba(16,32,21,.055)';
|
ctx.strokeStyle = 'rgba(16,32,21,.055)';
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
rect(ctx, GRID.x - 10, GRID.y - 10, GRID.cols * GRID.cell + 20, GRID.rows * GRID.cell + 20, true, true);
|
for (const k of cells) {
|
||||||
for (let c = 0; c <= GRID.cols; c += 1) {
|
const p = parseKey(k);
|
||||||
const x = GRID.x + c * GRID.cell;
|
const c = cellCenter(p.col, p.row);
|
||||||
ctx.beginPath(); ctx.moveTo(x, GRID.y); ctx.lineTo(x, GRID.y + GRID.rows * GRID.cell); ctx.stroke();
|
rect(ctx, c.x - GRID.cell / 2, c.y - GRID.cell / 2, GRID.cell, GRID.cell, true, true);
|
||||||
}
|
}
|
||||||
for (let r = 0; r <= GRID.rows; r += 1) {
|
ctx.strokeStyle = 'rgba(16,32,21,.16)';
|
||||||
const y = GRID.y + r * GRID.cell;
|
ctx.lineWidth = 3;
|
||||||
ctx.beginPath(); ctx.moveTo(GRID.x, y); ctx.lineTo(GRID.x + GRID.cols * GRID.cell, y); ctx.stroke();
|
for (const k of cells) {
|
||||||
|
const p = parseKey(k);
|
||||||
|
const c = cellCenter(p.col, p.row);
|
||||||
|
const x = c.x - GRID.cell / 2, y = c.y - GRID.cell / 2;
|
||||||
|
if (!isOwnedCell(game, p.col - 1, p.row)) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y + GRID.cell); ctx.stroke(); }
|
||||||
|
if (!isOwnedCell(game, p.col + 1, p.row)) { ctx.beginPath(); ctx.moveTo(x + GRID.cell, y); ctx.lineTo(x + GRID.cell, y + GRID.cell); ctx.stroke(); }
|
||||||
|
if (!isOwnedCell(game, p.col, p.row - 1)) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + GRID.cell, y); ctx.stroke(); }
|
||||||
|
if (!isOwnedCell(game, p.col, p.row + 1)) { ctx.beginPath(); ctx.moveTo(x, y + GRID.cell); ctx.lineTo(x + GRID.cell, y + GRID.cell); ctx.stroke(); }
|
||||||
}
|
}
|
||||||
for (const k of game.blockedCells || []) {
|
for (const k of game.blockedCells || []) {
|
||||||
const p = parseKey(k);
|
const p = parseKey(k);
|
||||||
|
if (!isOwnedCell(game, p.col, p.row)) continue;
|
||||||
const c = cellCenter(p.col, p.row);
|
const c = cellCenter(p.col, p.row);
|
||||||
ctx.fillStyle = 'rgba(16,32,21,.30)';
|
ctx.fillStyle = 'rgba(16,32,21,.30)';
|
||||||
ctx.strokeStyle = 'rgba(16,32,21,.60)';
|
ctx.strokeStyle = 'rgba(16,32,21,.60)';
|
||||||
|
|
@ -142,6 +162,40 @@ function drawGrid(ctx, game) {
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawExpansionOffers(ctx, game) {
|
||||||
|
if (game.phase !== 'build' || game.cardDraft?.pending || game.cardTargetPick?.pending) return;
|
||||||
|
const cost = expansionCost(game);
|
||||||
|
const affordable = game.cash >= cost;
|
||||||
|
for (const lot of expansionLots(game)) {
|
||||||
|
const b = lotBounds(lot);
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = affordable ? 'rgba(183,245,198,.34)' : 'rgba(255,255,255,.22)';
|
||||||
|
ctx.strokeStyle = affordable ? THEME.green : THEME.muted;
|
||||||
|
ctx.lineWidth = 5;
|
||||||
|
ctx.setLineDash([16, 10]);
|
||||||
|
rect(ctx, b.x, b.y, b.w, b.h, true, true);
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
const button = expansionButtonBounds(lot);
|
||||||
|
const cx = button.cx;
|
||||||
|
const cy = button.cy;
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,.94)';
|
||||||
|
ctx.strokeStyle = THEME.ink;
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
rect(ctx, button.x, button.y, button.w, button.h, true, true);
|
||||||
|
ctx.fillStyle = affordable ? THEME.green : THEME.danger;
|
||||||
|
ctx.font = '900 16px ui-monospace, monospace';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(lot.direction === 'right' ? 'BUY RIGHT LOT' : 'BUY UPPER LOT', cx, cy - 12);
|
||||||
|
ctx.fillStyle = THEME.ink;
|
||||||
|
ctx.font = '900 16px ui-monospace, monospace';
|
||||||
|
ctx.fillText(`${yen(cost)} / 10×10`, cx, cy + 15);
|
||||||
|
ctx.fillStyle = THEME.muted;
|
||||||
|
ctx.font = '900 11px ui-monospace, monospace';
|
||||||
|
ctx.fillText('blocked tiles included', cx, cy + 30);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function drawDirtOverlay(ctx, x, y, w, h, target, radius = 0) {
|
function drawDirtOverlay(ctx, x, y, w, h, target, radius = 0) {
|
||||||
const wear = wearRatio(target);
|
const wear = wearRatio(target);
|
||||||
if (wear < 0.50) return;
|
if (wear < 0.50) return;
|
||||||
|
|
@ -236,8 +290,22 @@ function addMarkerFromRoute(game, markers, route) {
|
||||||
if (!list.some(x => Math.abs(Math.sin((x - angle) / 2)) < 0.01)) list.push(angle);
|
if (!list.some(x => Math.abs(Math.sin((x - angle) / 2)) < 0.01)) list.push(angle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function explicitConveyorAngles(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)]
|
||||||
|
.map(name => DIRS.find(d => d.name === name)?.angle)
|
||||||
|
.filter(angle => Number.isFinite(angle));
|
||||||
|
}
|
||||||
|
|
||||||
function collectConveyorDirectionMarkers(game) {
|
function collectConveyorDirectionMarkers(game) {
|
||||||
const markers = new Map();
|
const markers = new Map();
|
||||||
|
for (const [k, meta] of game.conveyorMeta || []) {
|
||||||
|
const angles = explicitConveyorAngles(meta);
|
||||||
|
if (angles.length) markers.set(k, angles);
|
||||||
|
}
|
||||||
for (const farm of game.eggFarms) addMarkerFromRoute(game, markers, routeFromFarmToScanner(game, farm)?.route);
|
for (const farm of game.eggFarms) addMarkerFromRoute(game, markers, routeFromFarmToScanner(game, farm)?.route);
|
||||||
for (const scanner of game.scanners) {
|
for (const scanner of game.scanners) {
|
||||||
for (const side of ['left', 'right']) addMarkerFromRoute(game, markers, outputRoute(game, side, scannerCenter(scanner), scanner.id)?.route);
|
for (const side of ['left', 'right']) addMarkerFromRoute(game, markers, outputRoute(game, side, scannerCenter(scanner), scanner.id)?.route);
|
||||||
|
|
@ -266,7 +334,7 @@ function drawDirectionTriangle(ctx, x, y, angle, length = 11, halfWidth = 8) {
|
||||||
function drawScannerConnectors(ctx, game) {
|
function drawScannerConnectors(ctx, game) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.font = '900 10px ui-monospace, monospace';
|
ctx.font = '900 12px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
for (const scanner of game.scanners) {
|
for (const scanner of game.scanners) {
|
||||||
const outputs = scannerOutputs(scanner);
|
const outputs = scannerOutputs(scanner);
|
||||||
|
|
@ -294,13 +362,18 @@ function drawEggFarm(ctx, farm, game) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
if (drawImageIfLoaded(ctx, assets.eggFarm, c.x - 29, c.y - 29, 58, 58)) {
|
if (drawImageIfLoaded(ctx, assets.eggFarm, c.x - 29, c.y - 29, 58, 58)) {
|
||||||
drawDirtOverlay(ctx, c.x - 29, c.y - 29, 58, 58, farm);
|
drawDirtOverlay(ctx, c.x - 29, c.y - 29, 58, 58, farm);
|
||||||
|
const outlets = 1 + Math.max(0, Number(farm.extraEggOutlet) || 0);
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,.92)'; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 2;
|
||||||
|
rect(ctx, c.x + 9, c.y + 12, 30, 18, true, true);
|
||||||
|
ctx.fillStyle = THEME.ink; ctx.font = '900 10px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`O${outlets}`, c.x + 24, c.y + 24);
|
||||||
drawSelection(ctx, game, 'eggFarm', farm.id, c.x, c.y, 68, 68);
|
drawSelection(ctx, game, 'eggFarm', farm.id, c.x, c.y, 68, 68);
|
||||||
ctx.restore(); return;
|
ctx.restore(); return;
|
||||||
}
|
}
|
||||||
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4;
|
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4;
|
||||||
rect(ctx, c.x - 28, c.y - 28, 56, 56, true, true);
|
rect(ctx, c.x - 28, c.y - 28, 56, 56, true, true);
|
||||||
ctx.fillStyle = THEME.green; ctx.font = '900 15px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText('EGG', c.x, c.y - 4);
|
ctx.fillStyle = THEME.green; ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText('EGG', c.x, c.y - 4);
|
||||||
ctx.font = '900 9px ui-monospace, monospace'; ctx.fillStyle = THEME.ink; ctx.fillText(`L${farm.level}`, c.x, c.y + 13); ctx.fillText(`${farm.nextSpawn.toFixed(1)}s`, c.x, c.y + 26);
|
const outlets = 1 + Math.max(0, Number(farm.extraEggOutlet) || 0);
|
||||||
|
ctx.font = '900 11px ui-monospace, monospace'; ctx.fillStyle = THEME.ink; ctx.fillText(`L${farm.level} O${outlets}`, c.x, c.y + 13); ctx.fillText(`${farm.nextSpawn.toFixed(1)}s`, c.x, c.y + 26);
|
||||||
const shutter = Math.max(0, Math.min(1, farm.shutterProgress || 0));
|
const shutter = Math.max(0, Math.min(1, farm.shutterProgress || 0));
|
||||||
if (shutter > 0) {
|
if (shutter > 0) {
|
||||||
const h = 56 * shutter;
|
const h = 56 * shutter;
|
||||||
|
|
@ -308,7 +381,7 @@ function drawEggFarm(ctx, farm, game) {
|
||||||
rect(ctx, c.x - 28, c.y - 28, 56, h, true, false);
|
rect(ctx, c.x - 28, c.y - 28, 56, h, true, false);
|
||||||
ctx.strokeStyle = THEME.white; ctx.lineWidth = 2;
|
ctx.strokeStyle = THEME.white; ctx.lineWidth = 2;
|
||||||
for (let yy = c.y - 24; yy < c.y - 28 + h; yy += 8) { ctx.beginPath(); ctx.moveTo(c.x - 24, yy); ctx.lineTo(c.x + 24, yy); ctx.stroke(); }
|
for (let yy = c.y - 24; yy < c.y - 28 + h; yy += 8) { ctx.beginPath(); ctx.moveTo(c.x - 24, yy); ctx.lineTo(c.x + 24, yy); ctx.stroke(); }
|
||||||
if (shutter >= 1) { ctx.fillStyle = THEME.white; ctx.font = '900 10px ui-monospace, monospace'; ctx.fillText('SHUT', c.x, c.y + 4); }
|
if (shutter >= 1) { ctx.fillStyle = THEME.white; ctx.font = '900 12px ui-monospace, monospace'; ctx.fillText('SHUT', c.x, c.y + 4); }
|
||||||
}
|
}
|
||||||
drawDirtOverlay(ctx, c.x - 28, c.y - 28, 56, 56, farm);
|
drawDirtOverlay(ctx, c.x - 28, c.y - 28, 56, 56, farm);
|
||||||
drawSelection(ctx, game, 'eggFarm', farm.id, c.x, c.y, 68, 68);
|
drawSelection(ctx, game, 'eggFarm', farm.id, c.x, c.y, 68, 68);
|
||||||
|
|
@ -340,9 +413,9 @@ function drawScanner(ctx, scanner, game) {
|
||||||
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4;
|
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4;
|
||||||
rect(ctx, c.x - 52, c.y - 36, 104, 72, true, true);
|
rect(ctx, c.x - 52, c.y - 36, 104, 72, true, true);
|
||||||
ctx.fillStyle = scanner.kind === 'auto' ? THEME.green : THEME.ink;
|
ctx.fillStyle = scanner.kind === 'auto' ? THEME.green : THEME.ink;
|
||||||
ctx.font = '900 13px ui-monospace, monospace'; ctx.textAlign = 'center';
|
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 - 14);
|
||||||
ctx.font = '900 9px ui-monospace, monospace';
|
ctx.font = '900 11px ui-monospace, monospace';
|
||||||
const leftKey = scanner.keys?.left?.label || (scanner.slot === 0 ? 'A' : 'Left');
|
const leftKey = scanner.keys?.left?.label || (scanner.slot === 0 ? 'A' : 'Left');
|
||||||
const rightKey = scanner.keys?.right?.label || (scanner.slot === 0 ? 'D' : 'Right');
|
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 + 3);
|
||||||
|
|
@ -350,11 +423,11 @@ function drawScanner(ctx, scanner, game) {
|
||||||
if (scanner.kind === 'manual') {
|
if (scanner.kind === 'manual') {
|
||||||
drawManualKeyboardIcon(ctx, scanner, c);
|
drawManualKeyboardIcon(ctx, scanner, c);
|
||||||
ctx.fillStyle = q > 0 ? THEME.green : THEME.muted;
|
ctx.fillStyle = q > 0 ? THEME.green : THEME.muted;
|
||||||
ctx.font = '900 8px ui-monospace, monospace';
|
ctx.font = '900 10px ui-monospace, monospace';
|
||||||
ctx.fillText(`Q:${q}`, c.x, c.y + 32);
|
ctx.fillText(`Q:${q}`, c.x, c.y + 32);
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = q > 0 ? THEME.green : THEME.muted;
|
ctx.fillStyle = q > 0 ? THEME.green : THEME.muted;
|
||||||
ctx.font = '900 10px ui-monospace, monospace';
|
ctx.font = '900 12px ui-monospace, monospace';
|
||||||
ctx.fillText(`Q:${q} CD:${scanner.cooldown.toFixed(1)}`, c.x, c.y + 25);
|
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);
|
drawDirtOverlay(ctx, c.x - 52, c.y - 36, 104, 72, scanner);
|
||||||
}
|
}
|
||||||
|
|
@ -378,7 +451,7 @@ function drawManualKeyboardIcon(ctx, scanner, c) {
|
||||||
ctx.fillStyle = '#f7fff5';
|
ctx.fillStyle = '#f7fff5';
|
||||||
ctx.strokeStyle = THEME.ink;
|
ctx.strokeStyle = THEME.ink;
|
||||||
rect(ctx, baseX - 4, baseY - 4, 68, 25, true, true);
|
rect(ctx, baseX - 4, baseY - 4, 68, 25, true, true);
|
||||||
ctx.font = '900 13px ui-monospace, monospace';
|
ctx.font = '900 19px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
for (let i = 0; i < labels.length; i += 1) {
|
for (let i = 0; i < labels.length; i += 1) {
|
||||||
const keyDef = labels[i];
|
const keyDef = labels[i];
|
||||||
|
|
@ -397,7 +470,7 @@ function drawManualKeyboardIcon(ctx, scanner, c) {
|
||||||
ctx.fillText(keyDef.text, x + 14, y + 13);
|
ctx.fillText(keyDef.text, x + 14, y + 13);
|
||||||
}
|
}
|
||||||
ctx.fillStyle = THEME.muted;
|
ctx.fillStyle = THEME.muted;
|
||||||
ctx.font = '900 6px ui-monospace, monospace';
|
ctx.font = '900 8px ui-monospace, monospace';
|
||||||
ctx.fillText('KEYBOARD', c.x, baseY - 7);
|
ctx.fillText('KEYBOARD', c.x, baseY - 7);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
@ -417,12 +490,12 @@ function drawTargetBadge(ctx, x, y, w, title, type, subtitle = '') {
|
||||||
rect(ctx, x, y, w, 32, true, true);
|
rect(ctx, x, y, w, 32, true, true);
|
||||||
drawItemIcon(ctx, x + 18, y + 16, type);
|
drawItemIcon(ctx, x + 18, y + 16, type);
|
||||||
ctx.fillStyle = THEME.ink;
|
ctx.fillStyle = THEME.ink;
|
||||||
ctx.font = '900 10px ui-monospace, monospace';
|
ctx.font = '900 12px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.fillText(title, x + 34, y + 13);
|
ctx.fillText(title, x + 34, y + 13);
|
||||||
if (subtitle) {
|
if (subtitle) {
|
||||||
ctx.fillStyle = THEME.muted;
|
ctx.fillStyle = THEME.muted;
|
||||||
ctx.font = '900 8px ui-monospace, monospace';
|
ctx.font = '900 10px ui-monospace, monospace';
|
||||||
ctx.fillText(subtitle, x + 34, y + 25);
|
ctx.fillText(subtitle, x + 34, y + 25);
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
@ -447,7 +520,7 @@ function drawFacilityReceiver(ctx, game, id) {
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
rect(ctx, c.x - 18, c.y - 18, 36, 36, false, true);
|
rect(ctx, c.x - 18, c.y - 18, 36, 36, false, true);
|
||||||
drawItemIcon(ctx, c.x, c.y - 3, info.type);
|
drawItemIcon(ctx, c.x, c.y - 3, info.type);
|
||||||
ctx.font = '900 8px ui-monospace, monospace';
|
ctx.font = '900 10px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillStyle = THEME.ink;
|
ctx.fillStyle = THEME.ink;
|
||||||
ctx.fillText(info.title, c.x, c.y + 20);
|
ctx.fillText(info.title, c.x, c.y + 20);
|
||||||
|
|
@ -484,8 +557,8 @@ function drawMixer(ctx, game) {
|
||||||
drawExternalDuct(ctx, m);
|
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', 'mixer', 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.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 17px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`MIXER L${m.level}`, m.x + m.w / 2, m.y + 28);
|
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 10px ui-monospace, monospace'; ctx.fillText(`PAY ${yen(upgradedMixerPrice(game))}`, m.x + m.w / 2, m.y + 48);
|
ctx.font = '900 12px ui-monospace, monospace'; ctx.fillText(`PAY ${yen(upgradedMixerPrice(game))}`, m.x + m.w / 2, m.y + 48);
|
||||||
drawTargetBadge(ctx, m.x + 14, m.y + m.h - 40, m.w - 28, 'SEND MALE', 'male', 'safe meat route');
|
drawTargetBadge(ctx, m.x + 14, m.y + m.h - 40, m.w - 28, 'SEND MALE', 'male', 'safe meat route');
|
||||||
ctx.strokeStyle = THEME.green; ctx.lineWidth = 5;
|
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(); }
|
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(); }
|
||||||
|
|
@ -499,7 +572,7 @@ function drawTrash(ctx, game) {
|
||||||
drawExternalDuct(ctx, t);
|
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', 'trash', 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.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 15px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`SHREDDER L${t.level}`, t.x + t.w / 2, t.y + 25);
|
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); }
|
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');
|
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);
|
drawDirtOverlay(ctx, t.x, t.y, t.w, t.h, t);
|
||||||
|
|
@ -512,7 +585,7 @@ function drawTruck(ctx, game) {
|
||||||
drawExternalDuct(ctx, t);
|
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', 'truck', 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.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 15px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`TRUCK L${t.level}`, t.x + t.w / 2, t.y + 24);
|
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);
|
ctx.font = '900 11px ui-monospace, monospace'; ctx.fillText(`UNIT ${yen(upgradedTruckPrice(game))}`, t.x + t.w / 2, t.y + 42);
|
||||||
const truckTargetType = targetForTruck(game);
|
const truckTargetType = targetForTruck(game);
|
||||||
drawTargetBadge(ctx, t.x + 15, t.y + 50, t.w - 30, `SEND ${truckTargetType.toUpperCase()}`, truckTargetType, game.contractOffer && !game.contractActive ? 'next event' : 'truck cargo');
|
drawTargetBadge(ctx, t.x + 15, t.y + 50, t.w - 30, `SEND ${truckTargetType.toUpperCase()}`, truckTargetType, game.contractOffer && !game.contractActive ? 'next event' : 'truck cargo');
|
||||||
|
|
@ -566,7 +639,7 @@ function drawTinyPoop(ctx, x, y) { drawPoop(ctx, x, y, 7); }
|
||||||
function drawTinyChick(ctx, x, y, sex, warning = false) {
|
function drawTinyChick(ctx, x, y, sex, warning = false) {
|
||||||
ctx.save(); ctx.fillStyle = sex === 'male' ? THEME.male : THEME.female; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 2;
|
ctx.save(); ctx.fillStyle = sex === 'male' ? THEME.male : THEME.female; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 2;
|
||||||
ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||||
if (warning) { ctx.fillStyle = THEME.danger; ctx.font = '900 10px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText('!', x, y + 15); }
|
if (warning) { ctx.fillStyle = THEME.danger; ctx.font = '900 12px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText('!', x, y + 15); }
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
function selectionToken(type, id) { return `${type}:${id}`; }
|
function selectionToken(type, id) { return `${type}:${id}`; }
|
||||||
|
|
@ -587,7 +660,7 @@ function drawSelection(ctx, game, type, id, x, y, w, h) {
|
||||||
function drawSelectedTooltip(ctx, game, selectedObject, selectedTitle) {
|
function drawSelectedTooltip(ctx, game, selectedObject, selectedTitle) {
|
||||||
if (game.phase !== 'build') return;
|
if (game.phase !== 'build') return;
|
||||||
if ((game.multiSelected || []).length > 1) {
|
if ((game.multiSelected || []).length > 1) {
|
||||||
ctx.save(); ctx.fillStyle = 'rgba(255,255,255,.96)'; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 3; rect(ctx, 54 - game.view.x, 66 - game.view.y, 214, 42, true, true); ctx.fillStyle = THEME.ink; ctx.font = '900 11px ui-monospace, monospace'; ctx.fillText(`${game.multiSelected.length} ITEMS SELECTED`, 65 - game.view.x, 83 - game.view.y); ctx.fillStyle = THEME.muted; ctx.font = '900 9px ui-monospace, monospace'; ctx.fillText('drag any selected item to move group', 65 - game.view.x, 99 - game.view.y); ctx.restore();
|
ctx.save(); ctx.fillStyle = 'rgba(255,255,255,.96)'; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 3; rect(ctx, 54 - game.view.x, 66 - game.view.y, 214, 42, true, true); ctx.fillStyle = THEME.ink; ctx.font = '900 11px ui-monospace, monospace'; ctx.fillText(`${game.multiSelected.length} ITEMS SELECTED`, 65 - game.view.x, 83 - game.view.y); ctx.fillStyle = THEME.muted; ctx.font = '900 11px ui-monospace, monospace'; ctx.fillText('drag any selected item to move group', 65 - game.view.x, 99 - game.view.y); ctx.restore();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Single equipment inspection is handled by the DOM hover tooltip.
|
// Single equipment inspection is handled by the DOM hover tooltip.
|
||||||
|
|
@ -602,7 +675,8 @@ function drawCardTargetOverlay(ctx, canvas, game) {
|
||||||
// so every non-candidate object is reliably dimmed even after panning.
|
// so every non-candidate object is reliably dimmed even after panning.
|
||||||
ctx.fillStyle = 'rgba(0,0,0,.66)';
|
ctx.fillStyle = 'rgba(0,0,0,.66)';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.rect(-game.view.x, -game.view.y, canvas.width, canvas.height);
|
const scale = game.view?.scale || 1;
|
||||||
|
ctx.rect(-(game.view?.x || 0) / scale, -(game.view?.y || 0) / scale, canvas.width / scale, canvas.height / scale);
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const b = item.bounds;
|
const b = item.bounds;
|
||||||
const pad = 8;
|
const pad = 8;
|
||||||
|
|
@ -614,11 +688,11 @@ function drawCardTargetOverlay(ctx, canvas, game) {
|
||||||
ctx.fillStyle = 'rgba(255,255,255,.95)';
|
ctx.fillStyle = 'rgba(255,255,255,.95)';
|
||||||
ctx.strokeStyle = THEME.ink;
|
ctx.strokeStyle = THEME.ink;
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
rect(ctx, 260 - game.view.x, 96 - game.view.y, 420, 42, true, true);
|
rect(ctx, (260 - (game.view?.x || 0)) / scale, (96 - (game.view?.y || 0)) / scale, 420 / scale, 42 / scale, true, true);
|
||||||
ctx.fillStyle = THEME.danger;
|
ctx.fillStyle = THEME.danger;
|
||||||
ctx.font = '900 12px ui-monospace, monospace';
|
ctx.font = '900 12px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(isBlockedCellMode ? 'NO BLOCKED CELL. PRESS ESC.' : 'NO VALID UPGRADE TARGET. PRESS ESC.', 470 - game.view.x, 122 - game.view.y);
|
ctx.fillText(isBlockedCellMode ? 'NO BLOCKED CELL. PRESS ESC.' : 'NO VALID UPGRADE TARGET. PRESS ESC.', (470 - (game.view?.x || 0)) / scale, (122 - (game.view?.y || 0)) / scale);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -627,11 +701,11 @@ function drawCardTargetOverlay(ctx, canvas, game) {
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
const remaining = Math.max(1, game.cardTargetPick?.remaining || 1);
|
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 = isBlockedCellMode ? `CLICK BLOCKED CELL: ${remaining} LEFT. PRESS ESC TO CANCEL.` : 'CLICK AN UPGRADEABLE MACHINE. PRESS ESC TO CANCEL.';
|
||||||
rect(ctx, 216 - game.view.x, 96 - game.view.y, 560, 42, true, true);
|
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.fillStyle = THEME.ink;
|
||||||
ctx.font = '900 12px ui-monospace, monospace';
|
ctx.font = '900 12px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(msg, 496 - game.view.x, 122 - game.view.y);
|
ctx.fillText(msg, (496 - (game.view?.x || 0)) / scale, (122 - (game.view?.y || 0)) / scale);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -669,7 +743,7 @@ function drawEffects(ctx, game) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function drawFloatingTexts(ctx, game) {
|
function drawFloatingTexts(ctx, game) {
|
||||||
ctx.save(); ctx.font = '900 15px ui-monospace, monospace'; ctx.textAlign = 'center';
|
ctx.save(); ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center';
|
||||||
for (const t of game.floatingTexts) { const alpha = Math.max(0, t.life / t.maxLife); ctx.globalAlpha = alpha; ctx.lineWidth = 5; ctx.strokeStyle = THEME.white; ctx.fillStyle = t.color; ctx.strokeText(t.text, t.x, t.y); ctx.fillText(t.text, t.x, t.y); }
|
for (const t of game.floatingTexts) { const alpha = Math.max(0, t.life / t.maxLife); ctx.globalAlpha = alpha; ctx.lineWidth = 5; ctx.strokeStyle = THEME.white; ctx.fillStyle = t.color; ctx.strokeText(t.text, t.x, t.y); ctx.fillText(t.text, t.x, t.y); }
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
@ -700,7 +774,7 @@ function drawRepairman(ctx, game) {
|
||||||
ctx.beginPath(); ctx.moveTo(r.x - 11, r.y + 18); ctx.lineTo(r.x - 20, r.y + 24); ctx.stroke();
|
ctx.beginPath(); ctx.moveTo(r.x - 11, r.y + 18); ctx.lineTo(r.x - 20, r.y + 24); ctx.stroke();
|
||||||
ctx.beginPath(); ctx.moveTo(r.x + 11, r.y + 18); ctx.lineTo(r.x + 20, r.y + 24); ctx.stroke();
|
ctx.beginPath(); ctx.moveTo(r.x + 11, r.y + 18); ctx.lineTo(r.x + 20, r.y + 24); ctx.stroke();
|
||||||
ctx.fillStyle = THEME.ink;
|
ctx.fillStyle = THEME.ink;
|
||||||
ctx.font = '900 9px ui-monospace, monospace';
|
ctx.font = '900 11px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(r.active ? 'FIX' : 'HIRED', r.x, r.y + 33);
|
ctx.fillText(r.active ? 'FIX' : 'HIRED', r.x, r.y + 33);
|
||||||
if (r.target?.label) label(ctx, r.x, r.y - 24, r.target.label, THEME.green);
|
if (r.target?.label) label(ctx, r.x, r.y - 24, r.target.label, THEME.green);
|
||||||
|
|
@ -710,12 +784,12 @@ function drawRepairman(ctx, game) {
|
||||||
|
|
||||||
function drawCanvasHints(ctx, game, canvas) {
|
function drawCanvasHints(ctx, game, canvas) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = '900 13px ui-monospace, monospace'; ctx.textAlign = 'left'; ctx.fillStyle = THEME.ink;
|
ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'left'; ctx.fillStyle = THEME.ink;
|
||||||
// Visible version marker helps avoid browser/file-cache confusion when testing zips.
|
// Visible version marker helps avoid browser/file-cache confusion when testing zips.
|
||||||
ctx.fillStyle = 'rgba(255,255,255,.92)'; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 3;
|
ctx.fillStyle = 'rgba(255,255,255,.92)'; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 3;
|
||||||
rect(ctx, canvas.width - 284, 18, 260, 34, true, true);
|
rect(ctx, canvas.width - 284, 18, 260, 34, true, true);
|
||||||
ctx.fillStyle = THEME.green; ctx.font = '900 12px ui-monospace, monospace'; ctx.fillText(VERSION, canvas.width - 270, 40);
|
ctx.fillStyle = THEME.green; ctx.font = '900 12px ui-monospace, monospace'; ctx.fillText(VERSION, canvas.width - 270, 40);
|
||||||
ctx.font = '900 13px ui-monospace, monospace'; ctx.fillStyle = THEME.ink;
|
ctx.font = '900 19px ui-monospace, monospace'; ctx.fillStyle = THEME.ink;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
if (game.phase === 'running') lines.push(game.timeLeft <= 0 ? 'TIME UP: FARMS SHUT. CLEAR LINE.' : 'A/D: S1 ←/→: S2');
|
if (game.phase === 'running') lines.push(game.timeLeft <= 0 ? 'TIME UP: FARMS SHUT. CLEAR LINE.' : 'A/D: S1 ←/→: S2');
|
||||||
if (game.phase === 'build') lines.push(`BUILD: ${game.buildTool || 'SELECT'} | connect 1 EGG route to any exit`);
|
if (game.phase === 'build') lines.push(`BUILD: ${game.buildTool || 'SELECT'} | connect 1 EGG route to any exit`);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
import { BALANCE } from '../core/balance.js';
|
||||||
import { MACHINE_FACILITY_IDS, GRID, THEME } from '../core/config.js';
|
import { MACHINE_FACILITY_IDS, GRID, THEME } from '../core/config.js';
|
||||||
import { getSpawnRange } from '../core/state.js';
|
import { getSpawnRange } from '../core/state.js';
|
||||||
import { createEggFarm, createScanner, createFacility } from '../core/entities.js';
|
import { createEggFarm, createScanner, createFacility } from '../core/entities.js';
|
||||||
import { key, parseKey, pointToCell, cellCenter, yen } from '../core/utils.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 { TEXT, equipmentName } from '../core/text.js';
|
||||||
import { farmAt, scannerAt, scannerCenter, routeFromFarmToScanner, refreshRoutingAfterEdit, disconnectedBuildWarnings } from './routing.js';
|
import { farmAt, scannerAt, scannerCenter, routeFromFarmToScanner, refreshRoutingAfterEdit, disconnectedBuildWarnings } from './routing.js';
|
||||||
import { buildPrice, equipmentBasePrice, incomeMultiplier, mixerPoopPenalty, refundCash, spendCash, truckPoopPenalty, upgradedMixerPrice, upgradedTruckPrice, resaleValueFor, shredderUpgradeCount, shredderBonusChance, shredderBonusMaxCards } from './economy.js';
|
import { buildPrice, equipmentBasePrice, incomeMultiplier, mixerPoopPenalty, refundCash, spendCash, truckPoopPenalty, upgradedMixerPrice, upgradedTruckPrice, resaleValueFor, shredderUpgradeCount, shredderBonusChance, shredderBonusMaxCards } from './economy.js';
|
||||||
|
|
@ -14,8 +17,95 @@ import { ensureMaintenanceState, remainingPercent, performanceFactor, autoScanne
|
||||||
export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanels }) {
|
export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanels }) {
|
||||||
function updatePanels() { if (onUpdatePanels) onUpdatePanels(); }
|
function updatePanels() { if (onUpdatePanels) onUpdatePanels(); }
|
||||||
|
|
||||||
function pointInGrid(col, row) { return col >= 0 && col < GRID.cols && row >= 0 && row < GRID.rows; }
|
let lastConveyorBuildCell = null;
|
||||||
function isBlockedCell(col, row) { return game.blockedCells?.has?.(key(col, row)); }
|
let conveyorDragRecordedDirectionEdit = false;
|
||||||
|
|
||||||
|
function gridExpansionCost() { return expansionCost(game); }
|
||||||
|
|
||||||
|
function syncGridSizeToGame() {
|
||||||
|
game.gridCols = GRID.cols;
|
||||||
|
game.gridRows = GRID.rows;
|
||||||
|
game.gridOwnedCells = countOwnedCells(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shiftWorldRowsDown(rowDelta) {
|
||||||
|
if (!rowDelta) return;
|
||||||
|
GRID.y -= rowDelta * GRID.cell;
|
||||||
|
GRID.rows += rowDelta;
|
||||||
|
game.gridChunkOriginY = (game.gridChunkOriginY || 0) - Math.floor(rowDelta / 10);
|
||||||
|
game.ownedCells = shiftRowIndexedSet(game.ownedCells, rowDelta);
|
||||||
|
game.blockedCells = shiftRowIndexedSet(game.blockedCells, rowDelta);
|
||||||
|
game.conveyorTiles = shiftRowIndexedSet(game.conveyorTiles, rowDelta);
|
||||||
|
game.conveyorMeta = shiftRowIndexedMap(game.conveyorMeta, rowDelta);
|
||||||
|
for (const farm of game.eggFarms || []) farm.row += rowDelta;
|
||||||
|
for (const scanner of game.scanners || []) scanner.row += rowDelta;
|
||||||
|
for (const facility of Object.values(game.facilities || {})) {
|
||||||
|
if (facility.entry) facility.entry.row += rowDelta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOwnedLotCells(lot) {
|
||||||
|
game.ownedCells = game.ownedCells || new Set();
|
||||||
|
for (const cell of cellsInLot(lot)) game.ownedCells.add(key(cell.col, cell.row));
|
||||||
|
}
|
||||||
|
|
||||||
|
function expansionOfferAtPoint(p) {
|
||||||
|
if (game.phase !== 'build' || game.cardDraft?.pending || game.cardTargetPick?.pending) return null;
|
||||||
|
return expansionLots(game).find(lot => lot && lotContainsPoint(lot, p)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExpansionRequest(request = 'right') {
|
||||||
|
if (request && typeof request === 'object' && Number.isFinite(request.chunkX) && Number.isFinite(request.chunkY)) return request;
|
||||||
|
const direction = ['right', 'up'].includes(request) ? request : 'right';
|
||||||
|
return expansionLot(game, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buyGridExpansion(request = 'right') {
|
||||||
|
const offer = resolveExpansionRequest(request);
|
||||||
|
if (game.phase !== 'build' || game.cardDraft?.pending || game.cardTargetPick?.pending) return { ok: false, reason: 'Cannot expand now.' };
|
||||||
|
if (!offer) return { ok: false, reason: 'Invalid expansion lot.' };
|
||||||
|
const direction = offer.direction || 'right';
|
||||||
|
const cost = gridExpansionCost();
|
||||||
|
if (game.cash < cost) {
|
||||||
|
const reason = `Need ${yen(cost - game.cash)} more.`;
|
||||||
|
fail(reason);
|
||||||
|
return { ok: false, reason };
|
||||||
|
}
|
||||||
|
record(game);
|
||||||
|
spendCash(game, cost);
|
||||||
|
if (!game.gridExpansionPurchasesByDirection) game.gridExpansionPurchasesByDirection = { right: 0, up: 0 };
|
||||||
|
const originY = game.gridChunkOriginY || 0;
|
||||||
|
if (offer.chunkY < originY) {
|
||||||
|
const chunksToAdd = originY - offer.chunkY;
|
||||||
|
shiftWorldRowsDown(chunksToAdd * (BALANCE.grid.expansion?.rowsPerPurchase || 10));
|
||||||
|
}
|
||||||
|
const lot = lotFromChunk(game, offer.chunkX, offer.chunkY, direction);
|
||||||
|
if (lot.colStart + lot.cols > GRID.cols) GRID.cols = lot.colStart + lot.cols;
|
||||||
|
if (lot.rowStart + lot.rows > GRID.rows) GRID.rows = lot.rowStart + lot.rows;
|
||||||
|
addOwnedLotCells(lot);
|
||||||
|
ownChunk(game, lot.chunkX, lot.chunkY);
|
||||||
|
game.gridExpansionPurchases = (game.gridExpansionPurchases || 0) + 1;
|
||||||
|
game.gridExpansionPurchasesByDirection[direction] = (game.gridExpansionPurchasesByDirection[direction] || 0) + 1;
|
||||||
|
syncGridSizeToGame();
|
||||||
|
const blocked = generateBlockedCellsInRect({
|
||||||
|
seed: `${game.runSeed}:grid-expansion:${lot.chunkX},${lot.chunkY}:${game.gridExpansionPurchases}`,
|
||||||
|
colStart: lot.colStart,
|
||||||
|
colEnd: lot.colStart + lot.cols,
|
||||||
|
rowStart: lot.rowStart,
|
||||||
|
rowEnd: lot.rowStart + lot.rows,
|
||||||
|
targetRatio: BALANCE.map.blockedRatio
|
||||||
|
});
|
||||||
|
for (const k of blocked) game.blockedCells.add(k);
|
||||||
|
refreshRoutingAfterEdit(game);
|
||||||
|
const addedCells = lot.cols * lot.rows;
|
||||||
|
const b = lotBounds(lot);
|
||||||
|
floating(game, b.x + b.w / 2, b.y + b.h / 2, `${direction.toUpperCase()} +${addedCells} CELLS -${yen(cost)}`, THEME.green);
|
||||||
|
updatePanels();
|
||||||
|
return { ok: true, direction, cost, addedCells, blocked: blocked.size, chunkX: lot.chunkX, chunkY: lot.chunkY };
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointInGrid(col, row) { return col >= 0 && col < GRID.cols && row >= 0 && row < GRID.rows && isOwnedCell(game, col, row); }
|
||||||
|
function isBlockedCell(col, row) { return !isOwnedCell(game, col, row) || game.blockedCells?.has?.(key(col, row)); }
|
||||||
|
|
||||||
function rectOfFacility(f) { return { x: f.x, y: f.y, w: f.w, h: f.h }; }
|
function rectOfFacility(f) { return { x: f.x, y: f.y, w: f.w, h: f.h }; }
|
||||||
function rectsOverlap(a, b, margin = 10) {
|
function rectsOverlap(a, b, margin = 10) {
|
||||||
|
|
@ -93,24 +183,133 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
floating(game, canvas.width / 2 - game.view.x, 86 - game.view.y, reason.toUpperCase(), THEME.danger);
|
floating(game, canvas.width / 2 - game.view.x, 86 - game.view.y, reason.toUpperCase(), THEME.danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function conveyorMetaForCell(cell) {
|
||||||
|
if (!cell) return null;
|
||||||
|
const k = key(cell.col, cell.row);
|
||||||
|
return game.conveyorMeta.get(k) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOutDirs(meta) {
|
||||||
|
if (!meta) return [];
|
||||||
|
if (!Array.isArray(meta.outDirs)) meta.outDirs = [];
|
||||||
|
meta.outDirs = [...new Set(meta.outDirs.filter(dir => ['right', 'left', 'down', 'up'].includes(dir)))];
|
||||||
|
return meta.outDirs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function markConveyorDirection(from, to) {
|
||||||
|
const dir = directionNameBetweenCells(from, to);
|
||||||
|
if (!dir) return false;
|
||||||
|
const fromKey = key(from.col, from.row);
|
||||||
|
const toKey = key(to.col, to.row);
|
||||||
|
const fromMeta = game.conveyorMeta.get(fromKey);
|
||||||
|
const toMeta = game.conveyorMeta.get(toKey);
|
||||||
|
if (!fromMeta || !toMeta) return false;
|
||||||
|
const outDirs = normalizeOutDirs(fromMeta);
|
||||||
|
const changed = !outDirs.includes(dir) || fromMeta.dir !== dir || !toMeta.dir;
|
||||||
|
if (!outDirs.includes(dir)) outDirs.push(dir);
|
||||||
|
fromMeta.dir = dir;
|
||||||
|
if (!toMeta.dir) toMeta.dir = dir;
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConveyorCell(cell, incomingDir = null) {
|
||||||
|
if (!cell) return { ok: false, reason: TEXT.fail.outOfGrid };
|
||||||
|
const { col, row } = cell;
|
||||||
|
const k = key(col, row);
|
||||||
|
if (game.conveyorTiles.has(k)) return { ok: false, exists: true };
|
||||||
|
if (isBlockedCell(col, row)) return { ok: false, reason: 'Cannot build on blocked ground.' };
|
||||||
|
if (farmAt(game, col, row) || scannerAt(game, col, row)) return { ok: false, reason: TEXT.fail.cellOccupied };
|
||||||
|
const cost = buildPrice('conveyor', game);
|
||||||
|
if (game.cash < cost) return { ok: false, reason: TEXT.fail.notEnoughCash };
|
||||||
|
record(game);
|
||||||
|
spendCash(game, cost);
|
||||||
|
game.conveyorTiles.add(k);
|
||||||
|
const quality = buildQualityPatch();
|
||||||
|
game.conveyorMeta.set(k, {
|
||||||
|
price: cost,
|
||||||
|
builtSession: game.buildSession,
|
||||||
|
uses: 0,
|
||||||
|
durability: durabilityCapFor(game, 'conveyor', quality.durabilityBaseMultiplier),
|
||||||
|
maintenanceType: 'conveyor',
|
||||||
|
dir: incomingDir || null,
|
||||||
|
outDirs: [],
|
||||||
|
...quality
|
||||||
|
});
|
||||||
|
game.selected = { type: 'conveyor', id: k };
|
||||||
|
floating(game, cellCenter(col, row).x, cellCenter(col, row).y, `-${yen(cost)}`, THEME.ink);
|
||||||
|
return { ok: true, cell, key: k };
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConveyorDragCell(cell) {
|
||||||
|
if (!cell) return fail(TEXT.fail.outOfGrid);
|
||||||
|
const currentKey = key(cell.col, cell.row);
|
||||||
|
const exists = game.conveyorTiles.has(currentKey);
|
||||||
|
const previous = lastConveyorBuildCell;
|
||||||
|
if (previous && previous.col === cell.col && previous.row === cell.row) return;
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
if (previous && markConveyorDirection(previous, cell)) {
|
||||||
|
if (!conveyorDragRecordedDirectionEdit) {
|
||||||
|
// Direction edits are intentionally free; record once per drag gesture so undo can restore a major reroute.
|
||||||
|
record(game);
|
||||||
|
conveyorDragRecordedDirectionEdit = true;
|
||||||
|
}
|
||||||
|
refreshRoutingAfterEdit(game);
|
||||||
|
}
|
||||||
|
game.selected = { type: 'conveyor', id: currentKey };
|
||||||
|
lastConveyorBuildCell = { col: cell.col, row: cell.row };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const incomingDir = previous ? directionNameBetweenCells(previous, cell) : null;
|
||||||
|
const result = buildConveyorCell(cell, incomingDir);
|
||||||
|
if (!result.ok) return fail(result.reason || TEXT.fail.cellOccupied);
|
||||||
|
if (previous) markConveyorDirection(previous, cell);
|
||||||
|
lastConveyorBuildCell = { col: cell.col, row: cell.row };
|
||||||
|
refreshRoutingAfterEdit(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stepTowardCell(from, to) {
|
||||||
|
if (!from || !to) return to;
|
||||||
|
const dc = to.col - from.col;
|
||||||
|
const dr = to.row - from.row;
|
||||||
|
if (dc === 0 && dr === 0) return from;
|
||||||
|
if (Math.abs(dc) >= Math.abs(dr)) return { col: from.col + Math.sign(dc), row: from.row };
|
||||||
|
return { col: from.col, row: from.row + Math.sign(dr) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginConveyorDrag(cell) {
|
||||||
|
conveyorDragRecordedDirectionEdit = false;
|
||||||
|
lastConveyorBuildCell = null;
|
||||||
|
handleConveyorDragCell(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
function continueConveyorDrag(cell) {
|
||||||
|
if (!cell) return;
|
||||||
|
if (!lastConveyorBuildCell) return handleConveyorDragCell(cell);
|
||||||
|
let cursor = lastConveyorBuildCell;
|
||||||
|
let guard = 0;
|
||||||
|
while ((cursor.col !== cell.col || cursor.row !== cell.row) && guard < 64) {
|
||||||
|
const next = stepTowardCell(cursor, cell);
|
||||||
|
handleConveyorDragCell(next);
|
||||||
|
cursor = lastConveyorBuildCell || next;
|
||||||
|
guard += 1;
|
||||||
|
if (!lastConveyorBuildCell) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function endConveyorDrag() {
|
||||||
|
lastConveyorBuildCell = null;
|
||||||
|
conveyorDragRecordedDirectionEdit = false;
|
||||||
|
}
|
||||||
|
|
||||||
function buildAtCell(cell) {
|
function buildAtCell(cell) {
|
||||||
if (!cell) return fail(TEXT.fail.outOfGrid);
|
if (!cell) return fail(TEXT.fail.outOfGrid);
|
||||||
const { col, row } = cell;
|
const { col, row } = cell;
|
||||||
if (game.buildTool === 'conveyor') {
|
if (game.buildTool === 'conveyor') {
|
||||||
if (game.conveyorTiles.has(key(col, row))) return fail(TEXT.fail.cellOccupied);
|
const result = buildConveyorCell(cell, null);
|
||||||
if (isBlockedCell(col, row)) return fail('Cannot build on blocked ground.');
|
if (!result.ok) return fail(result.reason || TEXT.fail.cellOccupied);
|
||||||
if (farmAt(game, col, row) || scannerAt(game, col, row)) return fail(TEXT.fail.cellOccupied);
|
|
||||||
const cost = buildPrice('conveyor', game);
|
|
||||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
|
||||||
record(game);
|
|
||||||
spendCash(game, cost);
|
|
||||||
const k = key(col, row);
|
|
||||||
game.conveyorTiles.add(k);
|
|
||||||
const quality = buildQualityPatch();
|
|
||||||
game.conveyorMeta.set(k, { price: cost, builtSession: game.buildSession, uses: 0, durability: durabilityCapFor(game, 'conveyor', quality.durabilityBaseMultiplier), maintenanceType: 'conveyor', ...quality });
|
|
||||||
game.selected = { type: 'conveyor', id: k };
|
|
||||||
refreshRoutingAfterEdit(game);
|
refreshRoutingAfterEdit(game);
|
||||||
floating(game, cellCenter(col, row).x, cellCenter(col, row).y, `-${yen(cost)}`, THEME.ink);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isBlockedCell(col, row)) return fail('Cannot build on blocked ground.');
|
if (isBlockedCell(col, row)) return fail('Cannot build on blocked ground.');
|
||||||
|
|
@ -374,8 +573,9 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
function positionEquipmentPopover(obj) {
|
function positionEquipmentPopover(obj) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const p = selectedPopoverWorldPoint(obj);
|
const p = selectedPopoverWorldPoint(obj);
|
||||||
const screenX = rect.left + ((p.x + game.view.x) / canvas.width) * rect.width + 18;
|
const scale = game.view?.scale || 1;
|
||||||
const screenY = rect.top + ((p.y + game.view.y) / canvas.height) * rect.height - 12;
|
const screenX = rect.left + ((p.x * scale + (game.view?.x || 0)) / canvas.width) * rect.width + 18;
|
||||||
|
const screenY = rect.top + ((p.y * scale + (game.view?.y || 0)) / canvas.height) * rect.height - 12;
|
||||||
const maxX = Math.max(12, window.innerWidth - 274);
|
const maxX = Math.max(12, window.innerWidth - 274);
|
||||||
const maxY = Math.max(12, window.innerHeight - 220);
|
const maxY = Math.max(12, window.innerHeight - 220);
|
||||||
ui.modal.style.setProperty('--popover-x', `${Math.max(12, Math.min(maxX, screenX))}px`);
|
ui.modal.style.setProperty('--popover-x', `${Math.max(12, Math.min(maxX, screenX))}px`);
|
||||||
|
|
@ -410,6 +610,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
|
|
||||||
function setBuildTool(tool) {
|
function setBuildTool(tool) {
|
||||||
if (game.phase !== 'build') return;
|
if (game.phase !== 'build') return;
|
||||||
|
endConveyorDrag();
|
||||||
game.buildTool = game.buildTool === tool ? null : tool;
|
game.buildTool = game.buildTool === tool ? null : tool;
|
||||||
game.selected = null;
|
game.selected = null;
|
||||||
game.multiSelected = [];
|
game.multiSelected = [];
|
||||||
|
|
@ -457,7 +658,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
lines.push(`Cell: ${obj.id}`);
|
lines.push(`Cell: ${obj.id}`);
|
||||||
lines.push(`Congestion: ${c ? Math.floor(c.ratio * 100) : 0}%`);
|
lines.push(`Congestion: ${c ? Math.floor(c.ratio * 100) : 0}%`);
|
||||||
lines.push(`Durability: ${remainingPercent({ meta })}% | Speed x${performanceFactor({ meta }).toFixed(2)}`);
|
lines.push(`Durability: ${remainingPercent({ meta })}% | Speed x${performanceFactor({ meta }).toFixed(2)}`);
|
||||||
lines.push('Cross conveyors force straight travel. Branches use round-robin. Merges merge normally.');
|
lines.push('Flow follows drawn directions. Branches choose randomly; full branches are avoided when possible.');
|
||||||
lines.push(`Sale value: ${yen(resaleValueFor({ type: 'conveyor', oldKey: obj.id, ref: obj }, game).amount)}`);
|
lines.push(`Sale value: ${yen(resaleValueFor({ type: 'conveyor', oldKey: obj.id, ref: obj }, game).amount)}`);
|
||||||
} else if (obj.type === 'facility') {
|
} else if (obj.type === 'facility') {
|
||||||
lines.push(`Price: ${yen(equipmentPrice(obj))}`);
|
lines.push(`Price: ${yen(equipmentPrice(obj))}`);
|
||||||
|
|
@ -515,6 +716,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
finishGroupDrag: selection.finishGroupDrag,
|
finishGroupDrag: selection.finishGroupDrag,
|
||||||
selectedObject, selectedTitle, equipmentPrice, selectedUpgradeCost,
|
selectedObject, selectedTitle, equipmentPrice, selectedUpgradeCost,
|
||||||
selectedInfoLines, flavorText, disconnectedWarningFor,
|
selectedInfoLines, flavorText, disconnectedWarningFor,
|
||||||
|
gridExpansionCost, buyGridExpansion, expansionOfferAtPoint,
|
||||||
|
beginConveyorDrag, continueConveyorDrag, endConveyorDrag,
|
||||||
removeSelected, switchScannerRole, showManualScannerMenu,
|
removeSelected, switchScannerRole, showManualScannerMenu,
|
||||||
showAutoScannerMenu, showSelectedMenu, setBuildTool, fail,
|
showAutoScannerMenu, showSelectedMenu, setBuildTool, fail,
|
||||||
isEquipmentCell, equipmentHitBoxes,
|
isEquipmentCell, equipmentHitBoxes,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { AUTO_SCANNER_COOLDOWN, AUTO_SCANNER_UPGRADE_RATE, AUTO_SCANNER_MIN_COOLDOWN, BEARING_SPEED_MULTIPLIER, CARD_BALANCE, CONVEYOR_SPEED, CONVEYOR_SPEED_MAX, ECONOMY, EGG_SPAWN_RANGES, GRID, THEME } from '../core/config.js';
|
import { AUTO_SCANNER_COOLDOWN, AUTO_SCANNER_UPGRADE_RATE, AUTO_SCANNER_MIN_COOLDOWN, BEARING_SPEED_MULTIPLIER, CARD_BALANCE, CARD_DEFAULT_EFFECTS, CARD_DEFS as CARD_DEFINITIONS, CONVEYOR_SPEED, CONVEYOR_SPEED_MAX, ECONOMY, EGG_SPAWN_RANGES, GRID, THEME } from '../core/config.js';
|
||||||
import { nextSpawnDelay } from '../core/state.js';
|
import { nextSpawnDelay } from '../core/state.js';
|
||||||
import { cellCenter, key, yen } from '../core/utils.js';
|
import { cellCenter, key, yen } from '../core/utils.js';
|
||||||
import { applyPenalty, applyRevenue, upgradedMixerPrice, upgradedTruckPrice, shredderUpgradeCount, rawShredderUpgradeCount, shredderBonusChance, shredderBonusMaxCards } from './economy.js';
|
import { applyPenalty, applyRevenue, upgradedMixerPrice, upgradedTruckPrice, shredderUpgradeCount, rawShredderUpgradeCount, shredderBonusChance, shredderBonusMaxCards } from './economy.js';
|
||||||
|
|
@ -11,221 +11,7 @@ const RARE_WEIGHT = CARD_BALANCE.rareWeight;
|
||||||
const ULTRA_RARE_WEIGHT = CARD_BALANCE.ultraRareWeight || 0.45;
|
const ULTRA_RARE_WEIGHT = CARD_BALANCE.ultraRareWeight || 0.45;
|
||||||
const BASE_DRAFT_SIZE = CARD_BALANCE.baseDraftSize;
|
const BASE_DRAFT_SIZE = CARD_BALANCE.baseDraftSize;
|
||||||
|
|
||||||
const DEFAULT_EFFECTS = Object.freeze({
|
export const CARD_DEFS = CARD_DEFINITIONS;
|
||||||
bearing: 0,
|
|
||||||
legalWork: 0,
|
|
||||||
fairiesFlatteryNext: 0,
|
|
||||||
crowdedFarming: 0,
|
|
||||||
extraEggOutlet: 0,
|
|
||||||
hatchingFeed: 0,
|
|
||||||
safetyCover: 0,
|
|
||||||
recyclingSubsidy: 0,
|
|
||||||
chemicalWeaponSubsidy: 0,
|
|
||||||
preventiveMaintenance: 0,
|
|
||||||
durabilityCoating: 0,
|
|
||||||
laborExploitation: 0,
|
|
||||||
usedMachineActive: false,
|
|
||||||
rescueLoanCharges: 0,
|
|
||||||
loans: []
|
|
||||||
});
|
|
||||||
|
|
||||||
export const CARD_DEFS = [
|
|
||||||
{
|
|
||||||
id: 'upgradeEgg',
|
|
||||||
title: 'EGG Improvement',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'equipmentUpgrade',
|
|
||||||
target: 'eggFarm',
|
|
||||||
description: 'Choose one EGG FARM on the map and raise it by 1 level.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'upgradeAutoScanner',
|
|
||||||
title: 'AUTO SCANNER Improvement',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'equipmentUpgrade',
|
|
||||||
target: 'autoScanner',
|
|
||||||
description: 'Choose one AUTO SCANNER on the map and raise it by 1 level.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'upgradeMixer',
|
|
||||||
title: 'MIXER Improvement',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'equipmentUpgrade',
|
|
||||||
target: 'mixer',
|
|
||||||
description: 'Choose MIXER on the map and raise it by 1 level.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'upgradeTruck',
|
|
||||||
title: 'TRUCK Improvement',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'equipmentUpgrade',
|
|
||||||
target: 'truck',
|
|
||||||
description: 'Choose TRUCK on the map and raise it by 1 level.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'upgradeTrash',
|
|
||||||
title: 'SHREDDER Improvement',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'equipmentUpgrade',
|
|
||||||
target: 'trash',
|
|
||||||
description: 'Choose SHREDDER and raise it by 1 level. Bonus chance increases by the current upgrade count. Max 30 upgrades.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'crowdedFarming',
|
|
||||||
title: 'Crowded Farming Contract',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'EGG production interval -5%. Equipment deterioration +4%. Max 10 cards.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'hatchingFeed',
|
|
||||||
title: 'Hatching Feed',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'EGG production interval -4%. Poop rate +3%. No card limit.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'safetyCover',
|
|
||||||
title: 'Safety Cover',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'Explosion damage -5%. No card limit.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'preventiveMaintenance',
|
|
||||||
title: 'Preventive Maintenance Manual',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'All equipment deterioration speed -5%. No card limit.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'durabilityCoating',
|
|
||||||
title: 'Durability Coating',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'All equipment maximum durability +3%. No card limit.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bearing',
|
|
||||||
title: 'High-Quality Bearing',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'Conveyor speed +7.5%.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'extraEggOutlet',
|
|
||||||
title: 'Extra Egg Outlet',
|
|
||||||
rarity: 'rare',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'EGG output ports +1. Each connected belt can receive eggs. Max 3 cards.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'recyclingSubsidy',
|
|
||||||
title: 'Recycling Subsidy',
|
|
||||||
rarity: 'rare',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'SELL refund rate +10 points. Max 3 cards; normal SELL can reach 80%.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dynamite',
|
|
||||||
title: 'Dynamite',
|
|
||||||
rarity: 'rare',
|
|
||||||
type: 'cellAction',
|
|
||||||
target: 'blockedCell',
|
|
||||||
description: 'Remove any 3 blocked cells with a burst effect.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'usedMachine',
|
|
||||||
title: 'Used Machines',
|
|
||||||
rarity: 'rare',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'Future equipment costs half. New purchases have no refund and 60% durability. New Machines cancels it.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'newMachine',
|
|
||||||
title: 'New Machines',
|
|
||||||
rarity: 'common',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'Cancel Used Machines and restore normal prices, refunds, and durability.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'flattery',
|
|
||||||
title: 'Flattery',
|
|
||||||
rarity: 'rare',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'Next Fairies tribute -50%.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'loan',
|
|
||||||
title: 'Loan',
|
|
||||||
rarity: 'rare',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'Gain JPY 300 per elapsed day now. Pay JPY 400 per day for that many days starting tomorrow.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'extraCards',
|
|
||||||
title: 'Extra Cards',
|
|
||||||
rarity: 'rare',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'Draw 2 cards now and choose both.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'legalWork',
|
|
||||||
title: 'Legal Work',
|
|
||||||
rarity: 'rare',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'ZUNDA TAX exemption +¥250. The 95% cap point also moves up by ¥250.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'chemicalWeaponSubsidy',
|
|
||||||
title: 'Chemical Weapons Subsidy',
|
|
||||||
rarity: 'ultraRare',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'Each day, gain JPY 2000 per 100 poop shipped by truck. No card limit.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'laborExploitation',
|
|
||||||
title: 'Motivational Exploitation',
|
|
||||||
rarity: 'ultraRare',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'Repairman labor cost -30%. The repairman smiles with bloodshot eyes. One time only.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rescueLoan',
|
|
||||||
title: 'Emergency Loan',
|
|
||||||
rarity: 'ultraRare',
|
|
||||||
type: 'instant',
|
|
||||||
description: 'If cash is below JPY 0 at day-end settlement, gain JPY 500. Consumed on use.'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const CARD_TAGS = {
|
|
||||||
upgradeEgg: ['EGG', 'ACTIVE'],
|
|
||||||
upgradeAutoScanner: ['SCANNER', 'ACTIVE'],
|
|
||||||
upgradeMixer: ['ECONOMY', 'ACTIVE'],
|
|
||||||
upgradeTruck: ['ECONOMY', 'ACTIVE'],
|
|
||||||
upgradeTrash: ['POOP', 'ACTIVE'],
|
|
||||||
crowdedFarming: ['EGG', 'RISK', 'PASSIVE'],
|
|
||||||
hatchingFeed: ['EGG', 'POOP', 'PASSIVE'],
|
|
||||||
safetyCover: ['RISK', 'PASSIVE'],
|
|
||||||
preventiveMaintenance: ['MAINTENANCE', 'PASSIVE'],
|
|
||||||
durabilityCoating: ['MAINTENANCE', 'PASSIVE'],
|
|
||||||
bearing: ['CONVEYOR', 'PASSIVE'],
|
|
||||||
extraEggOutlet: ['EGG', 'RARE', 'PASSIVE'],
|
|
||||||
recyclingSubsidy: ['ECONOMY', 'RARE', 'PASSIVE'],
|
|
||||||
dynamite: ['RISK', 'RARE', 'ONE-SHOT'],
|
|
||||||
usedMachine: ['ECONOMY', 'RISK', 'RARE'],
|
|
||||||
newMachine: ['ECONOMY', 'ONE-SHOT'],
|
|
||||||
flattery: ['ECONOMY', 'RARE', 'ONE-SHOT'],
|
|
||||||
loan: ['ECONOMY', 'RISK', 'RARE'],
|
|
||||||
extraCards: ['RARE', 'ACTIVE'],
|
|
||||||
legalWork: ['ECONOMY', 'RARE', 'PASSIVE'],
|
|
||||||
chemicalWeaponSubsidy: ['POOP', 'ULTRA RARE', 'PASSIVE'],
|
|
||||||
laborExploitation: ['MAINTENANCE', 'ULTRA RARE', 'PASSIVE'],
|
|
||||||
rescueLoan: ['ECONOMY', 'ULTRA RARE', 'ONE-SHOT']
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const card of CARD_DEFS) card.tags = CARD_TAGS[card.id] || [];
|
|
||||||
|
|
||||||
function effectCount(game, id) {
|
function effectCount(game, id) {
|
||||||
const effects = ensureCardState(game);
|
const effects = ensureCardState(game);
|
||||||
|
|
@ -233,8 +19,8 @@ function effectCount(game, id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureCardState(game) {
|
export function ensureCardState(game) {
|
||||||
if (!game.cardEffects) game.cardEffects = { ...DEFAULT_EFFECTS, loans: [] };
|
if (!game.cardEffects) game.cardEffects = { ...CARD_DEFAULT_EFFECTS, loans: [] };
|
||||||
for (const [k, v] of Object.entries(DEFAULT_EFFECTS)) {
|
for (const [k, v] of Object.entries(CARD_DEFAULT_EFFECTS)) {
|
||||||
if (game.cardEffects[k] == null) game.cardEffects[k] = Array.isArray(v) ? [] : v;
|
if (game.cardEffects[k] == null) game.cardEffects[k] = Array.isArray(v) ? [] : v;
|
||||||
}
|
}
|
||||||
if (game.cardEffects.flattery != null) {
|
if (game.cardEffects.flattery != null) {
|
||||||
|
|
@ -254,8 +40,8 @@ export function eggProductionDelayMultiplier(game) {
|
||||||
return crowded * feed;
|
return crowded * feed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extraEggOutletCount(game) {
|
export function extraEggOutletCount(_game, farm = null) {
|
||||||
return Math.min(3, effectCount(game, 'extraEggOutlet'));
|
return Math.min(3, Math.max(0, Number(farm?.extraEggOutlet) || 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function conveyorSpeedForGame(game, conveyorKey = null) {
|
export function conveyorSpeedForGame(game, conveyorKey = null) {
|
||||||
|
|
@ -281,11 +67,48 @@ export function cardById(id) {
|
||||||
return CARD_DEFS.find(card => card.id === id) || null;
|
return CARD_DEFS.find(card => card.id === id) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function debugGrantCard(game, cardOrId) {
|
||||||
|
const card = typeof cardOrId === 'string' ? cardById(cardOrId) : cardOrId;
|
||||||
|
ensureCardState(game);
|
||||||
|
if (!card || card.type === 'dud') return { ok: false, reason: 'Card not found.' };
|
||||||
|
if (card.id === 'extraCards') {
|
||||||
|
game.cardDraft = game.cardDraft || { pending: false, choices: [], rerolls: 0, picksRemaining: 0 };
|
||||||
|
game.cardDraft.choices.push(...dealCards(game, 2));
|
||||||
|
return { ok: true, reason: `${card.title} added draft cards.` };
|
||||||
|
}
|
||||||
|
if (card.type === 'instant') {
|
||||||
|
applyInstantCard(game, card);
|
||||||
|
return { ok: true, reason: `${card.title} granted.` };
|
||||||
|
}
|
||||||
|
if (card.type === 'equipmentUpgrade') {
|
||||||
|
const target = targetsForCard(game, card)[0];
|
||||||
|
if (!target) return { ok: false, reason: `No valid target for ${card.title}.` };
|
||||||
|
applyEquipmentUpgrade(game, target);
|
||||||
|
return { ok: true, reason: `${card.title} applied.` };
|
||||||
|
}
|
||||||
|
if (card.type === 'cellAction' && card.target === 'blockedCell') {
|
||||||
|
const targets = targetsForCard(game, card).slice(0, 3);
|
||||||
|
if (!targets.length) return { ok: false, reason: 'No blocked cells to remove.' };
|
||||||
|
for (const target of targets) {
|
||||||
|
game.blockedCells.delete(key(target.col, target.row));
|
||||||
|
const c = cellCenter(target.col, target.row);
|
||||||
|
eraseEffect(game, c.x, c.y);
|
||||||
|
}
|
||||||
|
refreshRoutingAfterEdit(game);
|
||||||
|
return { ok: true, reason: `${card.title} removed ${targets.length} blocked cells.` };
|
||||||
|
}
|
||||||
|
return { ok: false, reason: `${card.title} cannot be debug-granted.` };
|
||||||
|
}
|
||||||
|
|
||||||
function targetLabel(game, target) {
|
function targetLabel(game, target) {
|
||||||
if (target.type === 'blockedCell') return `Blocked cell ${target.col},${target.row}`;
|
if (target.type === 'blockedCell') return `Blocked cell ${target.col},${target.row}`;
|
||||||
if (target.type === 'eggFarm') {
|
if (target.type === 'eggFarm') {
|
||||||
return `EGG #${target.id} L${target.level} -> L${target.level + 1}`;
|
return `EGG #${target.id} L${target.level} -> L${target.level + 1}`;
|
||||||
}
|
}
|
||||||
|
if (target.type === 'eggOutlet') {
|
||||||
|
const held = Math.max(0, Number(target.farm?.extraEggOutlet) || 0);
|
||||||
|
return `EGG #${target.id} OUTLET ${1 + held} -> ${2 + held}`;
|
||||||
|
}
|
||||||
if (target.type === 'scanner') {
|
if (target.type === 'scanner') {
|
||||||
return `AUTO #${target.id} L${target.level} -> L${target.level + 1} | CD ${autoScannerCooldownSeconds(target, game).toFixed(1)}s -> ${autoScannerCooldownSeconds({ ...target, level: target.level + 1 }, game).toFixed(1)}s`;
|
return `AUTO #${target.id} L${target.level} -> L${target.level + 1} | CD ${autoScannerCooldownSeconds(target, game).toFixed(1)}s -> ${autoScannerCooldownSeconds({ ...target, level: target.level + 1 }, game).toFixed(1)}s`;
|
||||||
}
|
}
|
||||||
|
|
@ -328,6 +151,9 @@ export function targetsForCard(game, cardOrId) {
|
||||||
if (!card) return [];
|
if (!card) return [];
|
||||||
if (card.target === 'blockedCell') return blockedCellTargets(game);
|
if (card.target === 'blockedCell') return blockedCellTargets(game);
|
||||||
if (card.target === 'eggFarm') return game.eggFarms.filter(f => (f.level || 1) < 4);
|
if (card.target === 'eggFarm') return game.eggFarms.filter(f => (f.level || 1) < 4);
|
||||||
|
if (card.target === 'eggOutlet') return game.eggFarms
|
||||||
|
.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 === 'autoScanner') return game.scanners.filter(s => s.kind === 'auto');
|
||||||
if (card.target === 'mixer') return game.facilities.mixer ? [game.facilities.mixer] : [];
|
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 === 'truck') return game.facilities.truck ? [game.facilities.truck] : [];
|
||||||
|
|
@ -338,16 +164,26 @@ export function targetsForCard(game, cardOrId) {
|
||||||
function cardAtCap(game, card) {
|
function cardAtCap(game, card) {
|
||||||
const effects = ensureCardState(game);
|
const effects = ensureCardState(game);
|
||||||
if (card.id === 'crowdedFarming') return (effects.crowdedFarming || 0) >= 10;
|
if (card.id === 'crowdedFarming') return (effects.crowdedFarming || 0) >= 10;
|
||||||
if (card.id === 'extraEggOutlet') return (effects.extraEggOutlet || 0) >= 3;
|
|
||||||
if (card.id === 'recyclingSubsidy') return (effects.recyclingSubsidy || 0) >= 3;
|
if (card.id === 'recyclingSubsidy') return (effects.recyclingSubsidy || 0) >= 3;
|
||||||
|
if (card.id === 'dudFilter') {
|
||||||
|
const base = Math.max(0, Math.min(1, CARD_BALANCE.dudChancePerCard || 0));
|
||||||
|
const step = Math.max(0.0001, Math.min(1, CARD_BALANCE.dudChanceReductionPerFilter || 0.05));
|
||||||
|
return (effects.dudFilter || 0) >= Math.ceil(base / step);
|
||||||
|
}
|
||||||
if (card.id === 'laborExploitation') return !!effects.laborExploitation;
|
if (card.id === 'laborExploitation') return !!effects.laborExploitation;
|
||||||
if (card.id === 'usedMachine') return !!effects.usedMachineActive;
|
if (card.id === 'usedMachine') return !!effects.usedMachineActive;
|
||||||
if (card.id === 'newMachine') return !effects.usedMachineActive;
|
if (card.id === 'newMachine') return !effects.usedMachineActive;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function repairmanCardVisible(game) {
|
||||||
|
const r = game.repairman || {};
|
||||||
|
return !!(r.active || r.hiredForNextDay || game.pendingRepairWorkerWages > 0);
|
||||||
|
}
|
||||||
|
|
||||||
function availableCards(game) {
|
function availableCards(game) {
|
||||||
return CARD_DEFS.filter(card => {
|
return CARD_DEFS.filter(card => {
|
||||||
|
if (card.id === 'laborExploitation' && !repairmanCardVisible(game)) return false;
|
||||||
if (cardAtCap(game, card)) return false;
|
if (cardAtCap(game, card)) return false;
|
||||||
if ((card.type === 'equipmentUpgrade' || card.type === 'cellAction') && targetsForCard(game, card).length <= 0) return false;
|
if ((card.type === 'equipmentUpgrade' || card.type === 'cellAction') && targetsForCard(game, card).length <= 0) return false;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -382,11 +218,18 @@ function dudCard() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyDudsToChoices(choices) {
|
function dudChanceForGame(game) {
|
||||||
|
const effects = ensureCardState(game);
|
||||||
|
const base = Math.max(0, Math.min(1, CARD_BALANCE.dudChancePerCard || 0));
|
||||||
|
const step = Math.max(0, Math.min(1, CARD_BALANCE.dudChanceReductionPerFilter || 0));
|
||||||
|
return Math.max(0, base - step * Math.max(0, effects.dudFilter || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDudsToChoices(game, choices) {
|
||||||
const result = [...choices];
|
const result = [...choices];
|
||||||
let duds = 0;
|
let duds = 0;
|
||||||
const maxDuds = Math.max(0, CARD_BALANCE.maxDudsPerDraft || 0);
|
const maxDuds = Math.max(0, CARD_BALANCE.maxDudsPerDraft || 0);
|
||||||
const chance = Math.max(0, Math.min(1, CARD_BALANCE.dudChancePerCard || 0));
|
const chance = dudChanceForGame(game);
|
||||||
for (let i = 0; i < result.length && duds < maxDuds; i += 1) {
|
for (let i = 0; i < result.length && duds < maxDuds; i += 1) {
|
||||||
if (Math.random() < chance) {
|
if (Math.random() < chance) {
|
||||||
result[i] = dudCard();
|
result[i] = dudCard();
|
||||||
|
|
@ -409,13 +252,14 @@ export function dealCards(game, count = BASE_DRAFT_SIZE) {
|
||||||
const i = pool.findIndex(card => card.id === picked.id);
|
const i = pool.findIndex(card => card.id === picked.id);
|
||||||
if (i >= 0) pool.splice(i, 1);
|
if (i >= 0) pool.splice(i, 1);
|
||||||
}
|
}
|
||||||
return applyDudsToChoices(choices);
|
return applyDudsToChoices(game, choices);
|
||||||
}
|
}
|
||||||
|
|
||||||
function targetKey(target) {
|
function targetKey(target) {
|
||||||
if (!target) return '';
|
if (!target) return '';
|
||||||
if (target.type === 'blockedCell') return `blockedCell:${target.id}`;
|
if (target.type === 'blockedCell') return `blockedCell:${target.id}`;
|
||||||
if (target.type === 'eggFarm') return `eggFarm:${target.id}`;
|
if (target.type === 'eggFarm') return `eggFarm:${target.id}`;
|
||||||
|
if (target.type === 'eggOutlet') return `eggOutlet:${target.id}`;
|
||||||
if (target.type === 'scanner') return `scanner:${target.id}`;
|
if (target.type === 'scanner') return `scanner:${target.id}`;
|
||||||
if (target.type === 'facility') return `facility:${target.id}`;
|
if (target.type === 'facility') return `facility:${target.id}`;
|
||||||
return `${target.type}:${target.id}`;
|
return `${target.type}:${target.id}`;
|
||||||
|
|
@ -427,7 +271,7 @@ function boundsForTarget(target) {
|
||||||
const c = cellCenter(target.col, target.row);
|
const c = cellCenter(target.col, target.row);
|
||||||
return { x: c.x - GRID.cell * 0.48, y: c.y - GRID.cell * 0.48, w: GRID.cell * 0.96, h: GRID.cell * 0.96, cx: c.x, cy: c.y };
|
return { x: c.x - GRID.cell * 0.48, y: c.y - GRID.cell * 0.48, w: GRID.cell * 0.96, h: GRID.cell * 0.96, cx: c.x, cy: c.y };
|
||||||
}
|
}
|
||||||
if (target.type === 'eggFarm') {
|
if (target.type === 'eggFarm' || target.type === 'eggOutlet') {
|
||||||
const c = cellCenter(target.col, target.row);
|
const c = cellCenter(target.col, target.row);
|
||||||
return { x: c.x - 34, y: c.y - 34, w: 68, h: 68, cx: c.x, cy: c.y };
|
return { x: c.x - 34, y: c.y - 34, w: 68, h: 68, cx: c.x, cy: c.y };
|
||||||
}
|
}
|
||||||
|
|
@ -460,6 +304,15 @@ function targetAtPoint(game, p) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyEquipmentUpgrade(game, target) {
|
function applyEquipmentUpgrade(game, target) {
|
||||||
|
if (target.type === 'eggOutlet') {
|
||||||
|
const farm = target.farm || game.eggFarms.find(f => f.id === target.id);
|
||||||
|
if (!farm) return;
|
||||||
|
farm.extraEggOutlet = Math.min(3, Math.max(0, Number(farm.extraEggOutlet) || 0) + 1);
|
||||||
|
const c = cellCenter(farm.col, farm.row);
|
||||||
|
floating(game, c.x, c.y - 30, `OUTLET ${1 + farm.extraEggOutlet}`, THEME.green);
|
||||||
|
refreshRoutingAfterEdit(game);
|
||||||
|
return;
|
||||||
|
}
|
||||||
target.level = (target.level || 1) + 1;
|
target.level = (target.level || 1) + 1;
|
||||||
if (target.type === 'eggFarm') {
|
if (target.type === 'eggFarm') {
|
||||||
target.nextSpawn = nextSpawnDelay(target);
|
target.nextSpawn = nextSpawnDelay(target);
|
||||||
|
|
@ -484,12 +337,12 @@ function applyInstantCard(game, card) {
|
||||||
if (card.id === 'bearing') inc('bearing');
|
if (card.id === 'bearing') inc('bearing');
|
||||||
if (card.id === 'legalWork') inc('legalWork');
|
if (card.id === 'legalWork') inc('legalWork');
|
||||||
if (card.id === 'crowdedFarming') effects.crowdedFarming = Math.min(10, (effects.crowdedFarming || 0) + 1);
|
if (card.id === 'crowdedFarming') effects.crowdedFarming = Math.min(10, (effects.crowdedFarming || 0) + 1);
|
||||||
if (card.id === 'extraEggOutlet') effects.extraEggOutlet = Math.min(3, (effects.extraEggOutlet || 0) + 1);
|
|
||||||
if (card.id === 'hatchingFeed') inc('hatchingFeed');
|
if (card.id === 'hatchingFeed') inc('hatchingFeed');
|
||||||
if (card.id === 'safetyCover') inc('safetyCover');
|
if (card.id === 'safetyCover') inc('safetyCover');
|
||||||
if (card.id === 'recyclingSubsidy') effects.recyclingSubsidy = Math.min(3, (effects.recyclingSubsidy || 0) + 1);
|
if (card.id === 'recyclingSubsidy') effects.recyclingSubsidy = Math.min(3, (effects.recyclingSubsidy || 0) + 1);
|
||||||
if (card.id === 'chemicalWeaponSubsidy') inc('chemicalWeaponSubsidy');
|
if (card.id === 'chemicalWeaponSubsidy') inc('chemicalWeaponSubsidy');
|
||||||
if (card.id === 'preventiveMaintenance') inc('preventiveMaintenance');
|
if (card.id === 'preventiveMaintenance') inc('preventiveMaintenance');
|
||||||
|
if (card.id === 'dudFilter') inc('dudFilter');
|
||||||
if (card.id === 'durabilityCoating') { inc('durabilityCoating'); ensureMaintenanceState(game); }
|
if (card.id === 'durabilityCoating') { inc('durabilityCoating'); ensureMaintenanceState(game); }
|
||||||
if (card.id === 'flattery') inc('fairiesFlatteryNext');
|
if (card.id === 'flattery') inc('fairiesFlatteryNext');
|
||||||
if (card.id === 'usedMachine') effects.usedMachineActive = true;
|
if (card.id === 'usedMachine') effects.usedMachineActive = true;
|
||||||
|
|
@ -560,12 +413,24 @@ function cardDescription(game, card) {
|
||||||
const next = Math.min(10, (e.crowdedFarming || 0) + 1);
|
const next = Math.min(10, (e.crowdedFarming || 0) + 1);
|
||||||
return `Held ${e.crowdedFarming || 0}/10. After pick: EGG interval ${formatPercent(Math.max(0.5, 1 - 0.05 * next))}, deterioration ${formatPercent(Math.min(1.4, 1 + 0.04 * next))}.`;
|
return `Held ${e.crowdedFarming || 0}/10. After pick: EGG interval ${formatPercent(Math.max(0.5, 1 - 0.05 * next))}, deterioration ${formatPercent(Math.min(1.4, 1 + 0.04 * next))}.`;
|
||||||
}
|
}
|
||||||
if (card.id === 'extraEggOutlet') return `Held ${e.extraEggOutlet || 0}/3. After pick: up to ${1 + Math.min(3, (e.extraEggOutlet || 0) + 1)} EGG output ports.`;
|
if (card.id === 'extraEggOutlet') {
|
||||||
|
const targets = targetsForCard(game, card);
|
||||||
|
const held = Math.max(0, Number(targets[0]?.farm?.extraEggOutlet) || 0);
|
||||||
|
return `Choose one EGG. Output ports ${1 + held} -> ${2 + held}. Max 4 ports per EGG.`;
|
||||||
|
}
|
||||||
if (card.id === 'hatchingFeed') return `Held ${e.hatchingFeed || 0}. After pick: EGG interval ${formatPercent(Math.pow(0.96, (e.hatchingFeed || 0) + 1))}, poop rate ${formatPercent(Math.pow(1.03, (e.hatchingFeed || 0) + 1))}.`;
|
if (card.id === 'hatchingFeed') return `Held ${e.hatchingFeed || 0}. After pick: EGG interval ${formatPercent(Math.pow(0.96, (e.hatchingFeed || 0) + 1))}, poop rate ${formatPercent(Math.pow(1.03, (e.hatchingFeed || 0) + 1))}.`;
|
||||||
if (card.id === 'safetyCover') return `Held ${e.safetyCover || 0}. After pick: explosion damage ${formatPercent(Math.pow(0.95, (e.safetyCover || 0) + 1))}.`;
|
if (card.id === 'safetyCover') return `Held ${e.safetyCover || 0}. After pick: explosion damage ${formatPercent(Math.pow(0.95, (e.safetyCover || 0) + 1))}.`;
|
||||||
if (card.id === 'recyclingSubsidy') return `Held ${e.recyclingSubsidy || 0}/3. After pick: normal SELL refund ${Math.round(Math.min(0.8, 0.5 + 0.1 * ((e.recyclingSubsidy || 0) + 1)) * 100)}%.`;
|
if (card.id === 'recyclingSubsidy') return `Held ${e.recyclingSubsidy || 0}/3. After pick: normal SELL refund ${Math.round(Math.min(0.8, 0.5 + 0.1 * ((e.recyclingSubsidy || 0) + 1)) * 100)}%.`;
|
||||||
if (card.id === 'chemicalWeaponSubsidy') return `Held ${e.chemicalWeaponSubsidy || 0}. After pick: ${yen(2000 * ((e.chemicalWeaponSubsidy || 0) + 1))} per 100 truck-shipped poop each day.`;
|
if (card.id === 'chemicalWeaponSubsidy') return `Held ${e.chemicalWeaponSubsidy || 0}. After pick: ${yen(2000 * ((e.chemicalWeaponSubsidy || 0) + 1))} per 100 truck-shipped poop each day.`;
|
||||||
if (card.id === 'preventiveMaintenance') return `Held ${e.preventiveMaintenance || 0}. After pick: deterioration speed ${formatPercent(Math.pow(0.95, (e.preventiveMaintenance || 0) + 1))}.`;
|
if (card.id === 'preventiveMaintenance') return `Held ${e.preventiveMaintenance || 0}. After pick: deterioration speed ${formatPercent(Math.pow(0.95, (e.preventiveMaintenance || 0) + 1))}.`;
|
||||||
|
if (card.id === 'dudFilter') {
|
||||||
|
const base = Math.max(0, Math.min(1, CARD_BALANCE.dudChancePerCard || 0));
|
||||||
|
const step = Math.max(0, Math.min(1, CARD_BALANCE.dudChanceReductionPerFilter || 0.05));
|
||||||
|
const held = Math.max(0, e.dudFilter || 0);
|
||||||
|
const before = Math.max(0, base - step * held);
|
||||||
|
const after = Math.max(0, base - step * (held + 1));
|
||||||
|
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 === 'durabilityCoating') return `Held ${e.durabilityCoating || 0}. After pick: maximum durability ${formatPercent(Math.pow(1.03, (e.durabilityCoating || 0) + 1))}.`;
|
||||||
if (card.id === 'flattery') return `Next Fairies tribute -50%. Held for next tribute: ${e.fairiesFlatteryNext || 0}.`;
|
if (card.id === 'flattery') return `Next Fairies tribute -50%. Held for next tribute: ${e.fairiesFlatteryNext || 0}.`;
|
||||||
if (card.id === 'usedMachine') return 'Enable Used Machines: future equipment costs 50%, has no SELL refund, and starts with 60% durability.';
|
if (card.id === 'usedMachine') return 'Enable Used Machines: future equipment costs 50%, has no SELL refund, and starts with 60% durability.';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { GRID, THEME } from '../core/config.js';
|
import { DIRS, GRID, THEME } from '../core/config.js';
|
||||||
import { createChick } from '../core/entities.js';
|
import { createChick } from '../core/entities.js';
|
||||||
import { nextSpawnDelay } from '../core/state.js';
|
import { nextSpawnDelay } from '../core/state.js';
|
||||||
import { key, parseKey, pointToCell, cellCenter, randomBetween, yen } from '../core/utils.js';
|
import { key, parseKey, pointToCell, cellCenter, randomBetween, yen, inGrid } from '../core/utils.js';
|
||||||
import { scannerById, scannerBySlot, scannerCenter, routeFromFarmToScanner, outputRoute, destinationLabel, destinationColor, nearestConveyorKey, buildConveyorComponents, autoSideFor } from './routing.js';
|
import { scannerById, scannerBySlot, scannerCenter, scannerConnector, nearestConveyorKey, buildConveyorComponents, autoSideFor, ensureFactoryGraph } from './routing.js';
|
||||||
import { applyMixerIncome, applyMixerPoopFine, applyTruckPoopFine, applyWrongTruckFine, upgradedTruckPrice, applyShredderBonus, applyRevenue } from './economy.js';
|
import { applyMixerIncome, applyMixerPoopFine, applyTruckPoopFine, applyWrongTruckFine, upgradedTruckPrice, applyShredderBonus, applyRevenue } from './economy.js';
|
||||||
import { autoScannerCooldownSeconds, eggProductionDelayMultiplier, extraEggOutletCount } from './cards.js';
|
import { autoScannerCooldownSeconds, eggProductionDelayMultiplier, extraEggOutletCount } from './cards.js';
|
||||||
import { productionMultiplier, truckTarget, isTargetTruckCargo, shouldFineMaleTruck } from './contracts.js';
|
import { productionMultiplier, truckTarget, isTargetTruckCargo, shouldFineMaleTruck } from './contracts.js';
|
||||||
|
|
@ -17,35 +17,23 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
|
|
||||||
function spawnChick(farm) {
|
function spawnChick(farm) {
|
||||||
const farmCenter = cellCenter(farm.col, farm.row);
|
const farmCenter = cellCenter(farm.col, farm.row);
|
||||||
const maxOutputs = 1 + extraEggOutletCount(game);
|
const maxOutputs = 1 + extraEggOutletCount(game, farm);
|
||||||
const routes = [];
|
const starts = farmOutputStarts(farm, maxOutputs);
|
||||||
const usedStarts = new Set();
|
if (!starts.length) {
|
||||||
const maxAttempts = Math.max(maxOutputs * 5, 5);
|
|
||||||
for (let attempt = 0; attempt < maxAttempts && routes.length < maxOutputs; attempt += 1) {
|
|
||||||
const data = routeFromFarmToScanner(game, farm, true);
|
|
||||||
if (!data) break;
|
|
||||||
const start = data.route?.[0];
|
|
||||||
if (!start) continue;
|
|
||||||
const startKey = `${Math.round(start.x)}:${Math.round(start.y)}`;
|
|
||||||
if (usedStarts.has(startKey) && routes.length > 0) continue;
|
|
||||||
usedStarts.add(startKey);
|
|
||||||
routes.push(data);
|
|
||||||
}
|
|
||||||
if (!routes.length) {
|
|
||||||
suppressUnconnectedFarmSpawn(farm, farmCenter);
|
suppressUnconnectedFarmSpawn(farm, farmCenter);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let spawned = 0;
|
let spawned = 0;
|
||||||
let blocked = 0;
|
let blocked = 0;
|
||||||
for (const data of routes) {
|
for (const startCell of starts) {
|
||||||
const start = data.route[0];
|
const start = cellCenter(startCell.col, startCell.row);
|
||||||
const blocker = spawnTileBlocker(start);
|
const blocker = spawnTileBlocker(start);
|
||||||
if (blocker) {
|
if (blocker) {
|
||||||
blocker.stoppedTimer = 0.85;
|
blocker.stoppedTimer = 0.85;
|
||||||
blocked += 1;
|
blocked += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const chick = createChick(game, data);
|
const chick = createChick(game, { stage: 'belt', scannerId: null, route: [start] });
|
||||||
if (chick.sex === 'poop') countPoopSpawned(game);
|
if (chick.sex === 'poop') countPoopSpawned(game);
|
||||||
game.chicks.push(chick);
|
game.chicks.push(chick);
|
||||||
recordFarmProduction(game, farm);
|
recordFarmProduction(game, farm);
|
||||||
|
|
@ -66,6 +54,15 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function farmOutputStarts(farm, limit) {
|
||||||
|
const starts = DIRS
|
||||||
|
.map(d => ({ col: farm.col + d.dc, row: farm.row + d.dr }))
|
||||||
|
.filter(p => inGrid(p.col, p.row) && game.conveyorTiles.has(key(p.col, p.row)));
|
||||||
|
const scored = starts.map(p => ({ p, blocked: targetBlockedByChick(null, cellCenter(p.col, p.row)) ? 1 : 0, roll: Math.random() }));
|
||||||
|
scored.sort((a, b) => a.blocked - b.blocked || a.roll - b.roll);
|
||||||
|
return scored.slice(0, Math.max(1, limit || 1)).map(item => item.p);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function spawnTileBlocker(start) {
|
function spawnTileBlocker(start) {
|
||||||
return game.chicks.find(ch => ch.stage !== 'flying' && Math.hypot(ch.x - start.x, ch.y - start.y) < GRID.cell * 0.68) || null;
|
return game.chicks.find(ch => ch.stage !== 'flying' && Math.hypot(ch.x - start.x, ch.y - start.y) < GRID.cell * 0.68) || null;
|
||||||
|
|
@ -138,32 +135,132 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
}
|
}
|
||||||
moveAlongRoute(chick, dt);
|
moveAlongRoute(chick, dt);
|
||||||
if (chick.route && chick.targetIndex < chick.route.length) return;
|
if (chick.route && chick.targetIndex < chick.route.length) return;
|
||||||
if (chick.stage === 'input' || chick.stage === 'toScanner') {
|
if (chick.pendingScannerId) {
|
||||||
const scanner = scannerById(game, chick.stage === 'input' ? chick.scannerId : chick.nextScannerId);
|
const scanner = scannerById(game, chick.pendingScannerId);
|
||||||
if (!scanner) {
|
chick.pendingScannerId = null;
|
||||||
removeChick(index, 'NO SCANNER');
|
if (!scanner) removeChick(index, 'NO SCANNER');
|
||||||
return;
|
else enqueueChick(chick, scanner, chick.route);
|
||||||
}
|
|
||||||
enqueueChick(chick, scanner, chick.route);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (chick.stage === 'toMixer') resolveMixer(index);
|
advanceByCurrentConveyor(chick, index);
|
||||||
else if (chick.stage === 'toTruck') resolveTruck(index);
|
|
||||||
else if (chick.stage === 'toTrash') resolveTrash(index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockedByFrontChick(chick) {
|
function blockedByFrontChick(chick) {
|
||||||
if (!chick.route || chick.targetIndex >= chick.route.length) return false;
|
if (!chick.route || chick.targetIndex >= chick.route.length) return false;
|
||||||
const next = chick.route[chick.targetIndex];
|
const next = chick.route[chick.targetIndex];
|
||||||
|
return targetBlockedByChick(chick, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetBlockedByChick(chick, target) {
|
||||||
|
if (!target) return false;
|
||||||
for (const other of game.chicks) {
|
for (const other of game.chicks) {
|
||||||
if (other.id === chick.id || other.stage === 'flying') continue;
|
if (chick && other.id === chick.id) continue;
|
||||||
if (Math.hypot(other.x - next.x, other.y - next.y) < GRID.cell * 0.55 && routeProgress(other) >= routeProgress(chick)) return true;
|
if (other.stage === 'flying') continue;
|
||||||
|
if (Math.hypot(other.x - target.x, other.y - target.y) < GRID.cell * 0.55) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function routeProgress(chick) {
|
function appendPoint(points, p) {
|
||||||
return chick.targetIndex || 0;
|
if (!p) return;
|
||||||
|
const last = points[points.length - 1];
|
||||||
|
if (!last || Math.hypot(last.x - p.x, last.y - p.y) > 1) points.push({ x: p.x, y: p.y });
|
||||||
|
}
|
||||||
|
|
||||||
|
function conveyorOutDirNames(cellKey) {
|
||||||
|
const meta = game.conveyorMeta?.get?.(cellKey) || {};
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
function dirByName(name) {
|
||||||
|
return DIRS.find(d => d.name === name) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scannerAtBodyCell(col, row) {
|
||||||
|
return game.scanners.find(scanner => scanner.col === col && scanner.row === row) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scannerReceivingFrom(cell, dirName) {
|
||||||
|
const d = dirByName(dirName);
|
||||||
|
if (!d) return null;
|
||||||
|
const scanner = scannerAtBodyCell(cell.col + d.dc, cell.row + d.dr);
|
||||||
|
if (!scanner) return null;
|
||||||
|
const input = scannerConnector(scanner, 'inputA');
|
||||||
|
return input && input.col === cell.col && input.row === cell.row ? scanner : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function facilityAtEntryCell(cell) {
|
||||||
|
return Object.entries(game.facilities || {}).find(([, f]) => f?.entry && f.entry.col === cell.col && f.entry.row === cell.row) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dirExitsToFacility(facility, dirName) {
|
||||||
|
return (facility.side === 'left' && dirName === 'left')
|
||||||
|
|| (facility.side === 'right' && dirName === 'right')
|
||||||
|
|| (facility.side === 'top' && dirName === 'up')
|
||||||
|
|| (facility.side === 'bottom' && dirName === 'down');
|
||||||
|
}
|
||||||
|
|
||||||
|
function movementOptionFromDir(cell, dirName) {
|
||||||
|
const d = dirByName(dirName);
|
||||||
|
if (!d) return null;
|
||||||
|
const next = { col: cell.col + d.dc, row: cell.row + d.dr };
|
||||||
|
if (inGrid(next.col, next.row) && game.conveyorTiles.has(key(next.col, next.row))) {
|
||||||
|
return { type: 'conveyor', dirName, target: cellCenter(next.col, next.row), nextCell: next };
|
||||||
|
}
|
||||||
|
const scanner = scannerReceivingFrom(cell, dirName);
|
||||||
|
if (scanner) return { type: 'scanner', dirName, target: scannerCenter(scanner), scanner };
|
||||||
|
const facilityPair = facilityAtEntryCell(cell);
|
||||||
|
if (facilityPair && dirExitsToFacility(facilityPair[1], dirName)) {
|
||||||
|
return { type: 'facility', dirName, facilityId: facilityPair[0], facility: facilityPair[1], target: cellCenter(cell.col, cell.row) };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickMovementOption(chick, cell, dirs) {
|
||||||
|
const candidates = dirs.map(dir => movementOptionFromDir(cell, dir)).filter(Boolean);
|
||||||
|
if (!candidates.length) return null;
|
||||||
|
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');
|
||||||
|
if (!pool.length) return null;
|
||||||
|
return pool[Math.floor(Math.random() * pool.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSingleSegmentRoute(chick, target) {
|
||||||
|
const route = [];
|
||||||
|
appendPoint(route, { x: chick.x, y: chick.y });
|
||||||
|
appendPoint(route, target);
|
||||||
|
chick.route = route.length ? route : [{ x: chick.x, y: chick.y }];
|
||||||
|
chick.targetIndex = chick.route.length > 1 ? 1 : chick.route.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function advanceByCurrentConveyor(chick, index) {
|
||||||
|
const cell = pointToCell(chick.x, chick.y);
|
||||||
|
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 dirs = conveyorOutDirNames(currentKey);
|
||||||
|
if (!dirs.length) {
|
||||||
|
chick.stoppedTimer = 0.25;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const option = pickMovementOption(chick, cell, dirs);
|
||||||
|
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);
|
||||||
|
else removeChick(index, 'DONE');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (option.type === 'scanner') chick.pendingScannerId = option.scanner.id;
|
||||||
|
setSingleSegmentRoute(chick, option.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
function conveyorKeyFromPoint(p) {
|
function conveyorKeyFromPoint(p) {
|
||||||
|
|
@ -341,13 +438,35 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
return bonus;
|
return bonus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scannerOutputStartCell(scanner, side) {
|
||||||
|
const connector = scannerConnector(scanner, side);
|
||||||
|
if (!connector) return null;
|
||||||
|
const connectorKey = key(connector.col, connector.row);
|
||||||
|
if (game.conveyorTiles.has(connectorKey)) return { ...connector };
|
||||||
|
const graph = ensureFactoryGraph(game);
|
||||||
|
const keys = (graph.scannerPorts.get(scanner.id)?.[side]?.keys || []).filter(Boolean);
|
||||||
|
if (!keys.length) return null;
|
||||||
|
const parsed = keys.map(parseKey);
|
||||||
|
const exact = parsed.find(p => p.col === connector.col && p.row === connector.row);
|
||||||
|
return exact || parsed[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scannerReleaseRoute(chick, scanner, side, startCell) {
|
||||||
|
const route = [];
|
||||||
|
appendPoint(route, { x: chick.x, y: chick.y });
|
||||||
|
const connector = scannerConnector(scanner, side);
|
||||||
|
if (connector) appendPoint(route, cellCenter(connector.col, connector.row));
|
||||||
|
appendPoint(route, cellCenter(startCell.col, startCell.row));
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
function sortChickByIndex(index, side, auto) {
|
function sortChickByIndex(index, side, auto) {
|
||||||
const chick = game.chicks[index];
|
const chick = game.chicks[index];
|
||||||
if (!chick || chick.stage !== 'queued') return false;
|
if (!chick || chick.stage !== 'queued') return false;
|
||||||
const scanner = scannerById(game, chick.scannerId);
|
const scanner = scannerById(game, chick.scannerId);
|
||||||
if (!scanner) return false;
|
if (!scanner) return false;
|
||||||
const plan = outputRoute(game, side, { x: chick.x, y: chick.y }, scanner.id, true);
|
const startCell = scannerOutputStartCell(scanner, side);
|
||||||
if (!plan) {
|
if (!startCell) {
|
||||||
if (!auto && scanner.kind === 'manual') resetManualCombo(scanner);
|
if (!auto && scanner.kind === 'manual') resetManualCombo(scanner);
|
||||||
floating(game, chick.x, chick.y - 16, side === 'left' ? 'NO LEFT BELT' : 'NO RIGHT BELT', THEME.danger);
|
floating(game, chick.x, chick.y - 16, side === 'left' ? 'NO LEFT BELT' : 'NO RIGHT BELT', THEME.danger);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -357,14 +476,17 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
else resetManualCombo(scanner);
|
else resetManualCombo(scanner);
|
||||||
}
|
}
|
||||||
scanner.queue = scanner.queue.filter(id => id !== chick.id);
|
scanner.queue = scanner.queue.filter(id => id !== chick.id);
|
||||||
chick.stage = plan.destination === 'scanner' ? 'toScanner' : `to${plan.destination[0].toUpperCase()}${plan.destination.slice(1)}`;
|
chick.stage = 'belt';
|
||||||
chick.route = plan.route;
|
chick.route = scannerReleaseRoute(chick, scanner, side, startCell);
|
||||||
chick.targetIndex = 1;
|
chick.targetIndex = chick.route.length > 1 ? 1 : chick.route.length;
|
||||||
chick.nextScannerId = plan.nextScannerId || null;
|
chick.scannerId = null;
|
||||||
|
chick.nextScannerId = null;
|
||||||
|
chick.pendingScannerId = null;
|
||||||
chick.queueRoute = null;
|
chick.queueRoute = null;
|
||||||
chick.queueIndex = -1;
|
chick.queueIndex = -1;
|
||||||
scannerPulse(game, scannerCenter(scanner).x, scannerCenter(scanner).y, auto ? THEME.green : destinationColor(plan.destination));
|
const pulseColor = auto ? THEME.green : (side === 'left' ? THEME.mixerBlue : THEME.truckPink);
|
||||||
floating(game, chick.x, chick.y - 20, auto ? 'AUTO' : destinationLabel(plan.destination), auto ? THEME.green : destinationColor(plan.destination));
|
scannerPulse(game, scannerCenter(scanner).x, scannerCenter(scanner).y, pulseColor);
|
||||||
|
floating(game, chick.x, chick.y - 20, auto ? 'AUTO' : side.toUpperCase(), pulseColor);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -230,12 +230,15 @@ export function settleTruckRevenue(game) {
|
||||||
return { base, adjusted, target, targetCount, unitPrice };
|
return { base, adjusted, target, targetCount, unitPrice };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectZundaTax(game, basisCash = game.cash) {
|
export function collectZundaTax(game, basisProfit = 0) {
|
||||||
const info = zundaTaxInfo(basisCash, game);
|
const info = zundaTaxInfo(basisProfit, game);
|
||||||
if (info.tax <= 0) return info;
|
if (info.tax <= 0) return info;
|
||||||
game.stats.zundaTax += info.tax;
|
// ZUNDA TAX is settled between days. It reduces cash and lifetime totals,
|
||||||
game.totals.zundaTax += info.tax;
|
// but it is intentionally excluded from the next day's NET display.
|
||||||
applyPenalty(game, info.tax);
|
game.totals.zundaTax = (game.totals.zundaTax || 0) + info.tax;
|
||||||
|
game.totals.penalty = (game.totals.penalty || 0) + info.tax;
|
||||||
|
game.totals.profit = (game.totals.profit || 0) - info.tax;
|
||||||
|
game.cash -= info.tax;
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { BALANCE } from '../core/balance.js';
|
||||||
|
import { GRID } from '../core/config.js';
|
||||||
export function cleanScanner(scanner) {
|
export function cleanScanner(scanner) {
|
||||||
return { ...scanner, keys: scanner.keys ? { left: { ...scanner.keys.left }, right: { ...scanner.keys.right } } : null, queue: [], cooldown: scanner.cooldown || 0 };
|
return { ...scanner, keys: scanner.keys ? { left: { ...scanner.keys.left }, right: { ...scanner.keys.right } } : null, queue: [], cooldown: scanner.cooldown || 0 };
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +9,16 @@ export function snapshot(game) {
|
||||||
stats: game.stats,
|
stats: game.stats,
|
||||||
totals: game.totals,
|
totals: game.totals,
|
||||||
nextId: game.nextId,
|
nextId: game.nextId,
|
||||||
|
gridX: GRID.x,
|
||||||
|
gridY: GRID.y,
|
||||||
|
gridCols: game.gridCols || GRID.cols,
|
||||||
|
gridRows: game.gridRows || GRID.rows,
|
||||||
|
gridExpansionPurchases: game.gridExpansionPurchases || 0,
|
||||||
|
gridExpansionPurchasesByDirection: game.gridExpansionPurchasesByDirection || { right: 0, up: 0 },
|
||||||
|
gridChunkOriginY: game.gridChunkOriginY || 0,
|
||||||
|
ownedCells: [...(game.ownedCells || [])],
|
||||||
|
ownedChunks: [...(game.ownedChunks || [])],
|
||||||
|
blockedCells: [...(game.blockedCells || [])],
|
||||||
selected: game.selected,
|
selected: game.selected,
|
||||||
multiSelected: game.multiSelected || [],
|
multiSelected: game.multiSelected || [],
|
||||||
buildTool: game.buildTool,
|
buildTool: game.buildTool,
|
||||||
|
|
@ -28,10 +40,24 @@ export function restore(game, text) {
|
||||||
game.stats = data.stats;
|
game.stats = data.stats;
|
||||||
game.totals = data.totals;
|
game.totals = data.totals;
|
||||||
game.nextId = data.nextId;
|
game.nextId = data.nextId;
|
||||||
|
GRID.x = data.gridX ?? BALANCE.grid.x;
|
||||||
|
GRID.y = data.gridY ?? BALANCE.grid.y;
|
||||||
|
GRID.cols = data.gridCols || BALANCE.grid.cols;
|
||||||
|
GRID.rows = data.gridRows || BALANCE.grid.rows;
|
||||||
|
game.gridCols = GRID.cols;
|
||||||
|
game.gridRows = GRID.rows;
|
||||||
|
game.gridExpansionPurchases = data.gridExpansionPurchases || 0;
|
||||||
|
game.gridExpansionPurchasesByDirection = data.gridExpansionPurchasesByDirection || { right: 0, up: 0 };
|
||||||
|
game.gridChunkOriginY = data.gridChunkOriginY || 0;
|
||||||
|
game.ownedCells = new Set(data.ownedCells || []);
|
||||||
|
game.ownedChunks = new Set(data.ownedChunks || []);
|
||||||
|
game.gridOwnedCells = game.ownedCells.size || (GRID.cols * GRID.rows);
|
||||||
|
game.blockedCells = new Set(data.blockedCells || []);
|
||||||
game.selected = data.selected;
|
game.selected = data.selected;
|
||||||
game.multiSelected = data.multiSelected || [];
|
game.multiSelected = data.multiSelected || [];
|
||||||
game.buildTool = data.buildTool;
|
game.buildTool = data.buildTool;
|
||||||
game.view = data.view || { x: 0, y: 0 };
|
game.view = data.view || { x: 0, y: 0, scale: 1 };
|
||||||
|
if (game.view.scale == null) game.view.scale = 1;
|
||||||
game.facilities = data.facilities || {};
|
game.facilities = data.facilities || {};
|
||||||
game.eggFarms = data.eggFarms || [];
|
game.eggFarms = data.eggFarms || [];
|
||||||
game.scanners = (data.scanners || []).map(s => ({ ...s, queue: [], cooldown: s.cooldown || 0 }));
|
game.scanners = (data.scanners || []).map(s => ({ ...s, queue: [], cooldown: s.cooldown || 0 }));
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,14 @@ export function refreshRoutingAfterEdit(game) {
|
||||||
function graphNodeKey(col, row) { return key(col, row); }
|
function graphNodeKey(col, row) { return key(col, row); }
|
||||||
function graphCell(k) { return parseKey(k); }
|
function graphCell(k) { return parseKey(k); }
|
||||||
|
|
||||||
|
function conveyorOutDirNames(game, cellKey) {
|
||||||
|
const meta = game.conveyorMeta?.get?.(cellKey) || {};
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
function emptyFactoryGraph(game) {
|
function emptyFactoryGraph(game) {
|
||||||
return {
|
return {
|
||||||
version: game.routingVersion || 0,
|
version: game.routingVersion || 0,
|
||||||
|
|
@ -132,7 +140,9 @@ export function buildFactoryGraph(game) {
|
||||||
|
|
||||||
for (const k of graph.cells) {
|
for (const k of graph.cells) {
|
||||||
const p = graphCell(k);
|
const p = graphCell(k);
|
||||||
for (const d of DIRS) {
|
const explicitDirs = conveyorOutDirNames(game, k);
|
||||||
|
const dirs = explicitDirs.length ? DIRS.filter(d => explicitDirs.includes(d.name)) : DIRS;
|
||||||
|
for (const d of dirs) {
|
||||||
const nk = graphNodeKey(p.col + d.dc, p.row + d.dr);
|
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 });
|
if (graph.cells.has(nk)) graph.adjacency.get(k).push({ key: nk, dir: d.name });
|
||||||
}
|
}
|
||||||
|
|
@ -270,38 +280,68 @@ export function bfsAllRoutes(game, start, isGoal) {
|
||||||
if (isGoal.cacheKey && graph.pathCache.has(cacheKey)) return graph.pathCache.get(cacheKey).map(path => path.map(p => ({ ...p })));
|
if (isGoal.cacheKey && graph.pathCache.has(cacheKey)) return graph.pathCache.get(cacheKey).map(path => path.map(p => ({ ...p })));
|
||||||
|
|
||||||
const startState = `${startK}|none`;
|
const startState = `${startK}|none`;
|
||||||
const queue = [{ key: startK, col: start.col, row: start.row, incoming: 'none' }];
|
const queue = [{ key: startK, col: start.col, row: start.row, incoming: 'none', depth: 0 }];
|
||||||
const visited = new Set([startState]);
|
const depthByState = new Map([[startState, 0]]);
|
||||||
const parent = new Map();
|
const parents = new Map();
|
||||||
const found = [];
|
const found = [];
|
||||||
|
let bestGoalDepth = Infinity;
|
||||||
|
const maxParentsPerState = 8;
|
||||||
|
const maxGoalStates = 24;
|
||||||
|
const maxPaths = 80;
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const cur = queue.shift();
|
const cur = queue.shift();
|
||||||
|
if (cur.depth > bestGoalDepth) break;
|
||||||
const curStateK = `${cur.key}|${cur.incoming}`;
|
const curStateK = `${cur.key}|${cur.incoming}`;
|
||||||
if (isGoal(cur)) found.push(curStateK);
|
if (isGoal(cur)) {
|
||||||
|
found.push(curStateK);
|
||||||
|
bestGoalDepth = cur.depth;
|
||||||
|
if (found.length >= maxGoalStates) break;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (const n of graph.adjacency.get(cur.key) || []) {
|
for (const n of graph.adjacency.get(cur.key) || []) {
|
||||||
const next = graphCell(n.key);
|
const next = graphCell(n.key);
|
||||||
const outDir = dirBetween(cur, next);
|
const outDir = dirBetween(cur, next);
|
||||||
if (cur.incoming !== 'none' && isCrossInGraph(graph, cur.key) && outDir !== cur.incoming) continue;
|
if (cur.incoming !== 'none' && isCrossInGraph(graph, cur.key) && outDir !== cur.incoming) continue;
|
||||||
|
const nextDepth = cur.depth + 1;
|
||||||
|
if (nextDepth > bestGoalDepth) continue;
|
||||||
const nextStateK = `${n.key}|${outDir}`;
|
const nextStateK = `${n.key}|${outDir}`;
|
||||||
if (visited.has(nextStateK)) continue;
|
const knownDepth = depthByState.get(nextStateK);
|
||||||
visited.add(nextStateK);
|
if (knownDepth == null) {
|
||||||
parent.set(nextStateK, curStateK);
|
depthByState.set(nextStateK, nextDepth);
|
||||||
queue.push({ key: n.key, col: next.col, row: next.row, incoming: outDir });
|
parents.set(nextStateK, [curStateK]);
|
||||||
|
queue.push({ key: n.key, col: next.col, row: next.row, incoming: outDir, depth: nextDepth });
|
||||||
|
} else if (knownDepth === nextDepth) {
|
||||||
|
const list = parents.get(nextStateK) || [];
|
||||||
|
if (list.length < maxParentsPerState && !list.includes(curStateK)) list.push(curStateK);
|
||||||
|
parents.set(nextStateK, list);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const paths = found.map(goalState => {
|
|
||||||
const reversed = [];
|
const paths = [];
|
||||||
let cursor = goalState;
|
const expand = (state, suffix) => {
|
||||||
while (cursor) {
|
if (paths.length >= maxPaths) return;
|
||||||
const [cellK] = cursor.split('|');
|
const [cellK] = state.split('|');
|
||||||
reversed.push(graphCell(cellK));
|
const nextSuffix = [graphCell(cellK), ...suffix];
|
||||||
if (cellK === startK) break;
|
if (state === startState || cellK === startK) {
|
||||||
cursor = parent.get(cursor);
|
paths.push(nextSuffix);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return reversed.reverse();
|
for (const parent of parents.get(state) || []) expand(parent, nextSuffix);
|
||||||
});
|
};
|
||||||
if (isGoal.cacheKey) graph.pathCache.set(cacheKey, paths.map(path => path.map(p => ({ ...p }))));
|
for (const goalState of found) expand(goalState, []);
|
||||||
return paths;
|
|
||||||
|
const unique = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const path of paths) {
|
||||||
|
const sig = path.map(p => graphNodeKey(p.col, p.row)).join('>');
|
||||||
|
if (seen.has(sig)) continue;
|
||||||
|
seen.add(sig);
|
||||||
|
unique.push(path);
|
||||||
|
}
|
||||||
|
if (isGoal.cacheKey) graph.pathCache.set(cacheKey, unique.map(path => path.map(p => ({ ...p }))));
|
||||||
|
return unique;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildConveyorComponents(game) {
|
export function buildConveyorComponents(game) {
|
||||||
|
|
@ -363,6 +403,25 @@ function routeWithScannerTarget(fromPoint, connector, cells, targetScanner) {
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chickCellOccupancyScore(game, cell, depth = 0) {
|
||||||
|
const c = cellCenter(cell.col, cell.row);
|
||||||
|
let score = 0;
|
||||||
|
for (const chick of game.chicks || []) {
|
||||||
|
if (chick.stage === 'flying' || chick.stage === 'queued') continue;
|
||||||
|
const d = Math.hypot(chick.x - c.x, chick.y - c.y);
|
||||||
|
if (d < GRID.cell * 0.50) score += depth <= 1 ? 100 : Math.max(1, 12 - depth);
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
function candidateCongestionScore(game, candidate) {
|
||||||
|
const cells = candidate.cells || [];
|
||||||
|
let score = 0;
|
||||||
|
const limit = Math.min(cells.length, 8);
|
||||||
|
for (let i = 0; i < limit; i += 1) score += chickCellOccupancyScore(game, cells[i], i);
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
export function chooseRoundRobin(game, id, candidates, advance = false) {
|
export function chooseRoundRobin(game, id, candidates, advance = false) {
|
||||||
if (!candidates.length) return null;
|
if (!candidates.length) return null;
|
||||||
const sorted = [...candidates].sort((a, b) => {
|
const sorted = [...candidates].sort((a, b) => {
|
||||||
|
|
@ -376,9 +435,14 @@ export function chooseRoundRobin(game, id, candidates, advance = false) {
|
||||||
const kb = b.key || JSON.stringify(b.route || b.cells || b);
|
const kb = b.key || JSON.stringify(b.route || b.cells || b);
|
||||||
return ka.localeCompare(kb);
|
return ka.localeCompare(kb);
|
||||||
});
|
});
|
||||||
|
if (!advance || sorted.length === 1) return sorted[0];
|
||||||
|
const bestBias = sorted[0].roleBias ?? 0;
|
||||||
|
let pool = sorted.filter(item => (item.roleBias ?? 0) === bestBias);
|
||||||
|
const bestScore = Math.min(...pool.map(item => candidateCongestionScore(game, item)));
|
||||||
|
pool = pool.filter(item => candidateCongestionScore(game, item) === bestScore);
|
||||||
const n = game.branchCounters.get(id) || 0;
|
const n = game.branchCounters.get(id) || 0;
|
||||||
if (advance) game.branchCounters.set(id, n + 1);
|
game.branchCounters.set(id, n + 1);
|
||||||
return sorted[n % sorted.length];
|
return pool[Math.floor(Math.random() * pool.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scannerRules(scanner) {
|
export function scannerRules(scanner) {
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,7 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
||||||
if (origin.type !== 'facility') continue;
|
if (origin.type !== 'facility') continue;
|
||||||
const nextCenter = { x: origin.center.x + dx, y: origin.center.y + dy };
|
const nextCenter = { x: origin.center.x + dx, y: origin.center.y + dy };
|
||||||
const { entry, side } = nearestGridEdge(nextCenter);
|
const { entry, side } = nearestGridEdge(nextCenter);
|
||||||
|
if (!pointInGrid(entry.col, entry.row)) return true;
|
||||||
if (game.blockedCells?.has?.(key(entry.col, entry.row))) return true;
|
if (game.blockedCells?.has?.(key(entry.col, entry.row))) return true;
|
||||||
const draft = { ...origin.obj, entry: { ...entry }, side };
|
const draft = { ...origin.obj, entry: { ...entry }, side };
|
||||||
layoutFacilityOnEdge(draft, entry, side);
|
layoutFacilityOnEdge(draft, entry, side);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
hasBuildableOption = true;
|
hasBuildableOption = true;
|
||||||
if (game.cash >= buildPrice(id, game)) return false;
|
if (game.cash >= buildPrice(id, game)) return false;
|
||||||
}
|
}
|
||||||
|
if (build?.gridExpansionCost && game.cash >= build.gridExpansionCost()) return false;
|
||||||
return hasBuildableOption;
|
return hasBuildableOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,6 +61,15 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
if (ui.buttons.erase) ui.buttons.erase.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending;
|
if (ui.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) {
|
if (ui.hireRepairmanButton) {
|
||||||
const hired = !!game.repairman?.hiredForNextDay;
|
const hired = !!game.repairman?.hiredForNextDay;
|
||||||
const cost = repairmanDailyCost(game);
|
const cost = repairmanDailyCost(game);
|
||||||
|
|
@ -67,7 +77,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
ui.hireRepairmanButton.disabled = blocked;
|
ui.hireRepairmanButton.disabled = blocked;
|
||||||
ui.hireRepairmanButton.classList.toggle('active', hired);
|
ui.hireRepairmanButton.classList.toggle('active', hired);
|
||||||
ui.hireRepairmanButton.classList.toggle('unaffordable', game.cash < cost);
|
ui.hireRepairmanButton.classList.toggle('unaffordable', game.cash < cost);
|
||||||
ui.hireRepairmanButton.title = hired ? 'Repairman is hired for the next production phase.' : `Hire for ${yen(cost)} for one day. Repairs 3% durability per second.`;
|
ui.hireRepairmanButton.title = hired ? 'Repairman is hired for the next production phase.' : `Hire for ${yen(cost)} for one day. Repairs 2% durability per second.`;
|
||||||
}
|
}
|
||||||
const cashout = noAffordableBuildTools();
|
const cashout = noAffordableBuildTools();
|
||||||
ui.buttons.nextTurn?.classList.toggle('cashout-emphasis', cashout);
|
ui.buttons.nextTurn?.classList.toggle('cashout-emphasis', cashout);
|
||||||
|
|
@ -93,21 +103,22 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
function updateTurnSummary() {
|
function updateTurnSummary() {
|
||||||
const connected = game.eggFarms.filter(f => routeFromFarmToScanner(game, f)?.scannerId).length;
|
const connected = game.eggFarms.filter(f => routeFromFarmToScanner(game, f)?.scannerId).length;
|
||||||
const issues = facilityConnectionIssues(game);
|
const issues = facilityConnectionIssues(game);
|
||||||
const tax = zundaTaxInfo(game.cash, game);
|
const zundaBasisProfit = Math.max(0, Math.floor(game.lastResult?.profit ?? game.stats?.profit ?? 0));
|
||||||
|
const tax = zundaTaxInfo(zundaBasisProfit, game);
|
||||||
const tributeDay = game.phase === 'running' ? Math.max(1, (game.totals.turnsCompleted || 0) + 1) : Math.max(1, Number(game.turn) || 1);
|
const tributeDay = game.phase === 'running' ? Math.max(1, (game.totals.turnsCompleted || 0) + 1) : Math.max(1, Number(game.turn) || 1);
|
||||||
const tribute = fairiesTributeInfo(game, tributeDay);
|
const tribute = fairiesTributeInfo(game, tributeDay);
|
||||||
const lastTribute = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.fairiesTribute || 0 : null;
|
const lastTribute = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.fairiesTribute || 0 : null;
|
||||||
const lastTributeReduction = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.reduction || 0 : 0;
|
const lastTributeReduction = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.reduction || 0 : 0;
|
||||||
ui.turnSummary.innerHTML = [
|
ui.turnSummary.innerHTML = [
|
||||||
`Seed: <b>${escapeHtml(game.runSeed || 'none')}</b> | Blocked: <b>${game.blockedCells?.size || 0}</b> cells`,
|
`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`,
|
||||||
`Truck target: <b>${truckTarget(game).toUpperCase()}</b> | Male fine: <b>-${yen(maleTruckPenalty(game))}</b>`,
|
`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>`,
|
`Poop fine: <b>Mixer -${yen(mixerPoopPenalty(game))}</b> / <b>Shipment -${yen(truckPoopPenalty(game))}</b>`,
|
||||||
`ZUNDA TAX on Next Day: <b>-${yen(tax.tax)}</b> | 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)} × ${tax.ratePercent}% | exemption ${yen(tax.exemption)} | 95% at ${yen(tax.maxRateCash)}`,
|
||||||
lastTribute != null
|
lastTribute != null
|
||||||
? `Fairies tribute paid before Build: <b>-${yen(lastTribute)}</b>${lastTributeReduction ? ` | Flattery -${yen(lastTributeReduction)}` : ''}`
|
? `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)}` : ''}`,
|
: `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>`,
|
`Maintenance: <b>worst ${maintenanceSummary(game).worst.percent}% dirty (${maintenanceSummary(game).worst.label})</b> | Repairman: <b>${game.repairman?.hiredForNextDay ? 'hired next day' : game.repairman?.active ? 'working' : 'none'}</b>`,
|
||||||
`Manual combo: <b>${game.manualCombo?.count || 0}</b> | Card draft: <b>${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'}</b> | Belt: <b>${displaySpeed()}px/s</b> | Farms with scanner route: <b>${connected}/${game.eggFarms.length}</b>`,
|
`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>`,
|
||||||
game.cardEffects?.usedMachineActive ? '<span class="cash-negative">Procurement: USED MACHINE MODE / no refunds / 60% durability</span>' : '',
|
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>`
|
issues.length ? `<span class="cash-negative">Blocked: ${issues[0]}</span>` : `<span class="cash-positive">${TEXT.status.allPortsConnected}</span>`
|
||||||
].join('<br>');
|
].join('<br>');
|
||||||
|
|
@ -156,6 +167,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
: `${Math.max(0, game.timeLeft).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();
|
ui.phase.textContent = phaseLabel();
|
||||||
|
if (ui.comboCount) ui.comboCount.textContent = game.manualCombo?.count || 0;
|
||||||
updatePriorityStrip();
|
updatePriorityStrip();
|
||||||
ui.turnProfit.textContent = yen(game.stats.profit);
|
ui.turnProfit.textContent = yen(game.stats.profit);
|
||||||
ui.turnProfit.classList.toggle('cash-negative', game.stats.profit < 0);
|
ui.turnProfit.classList.toggle('cash-negative', game.stats.profit < 0);
|
||||||
|
|
@ -178,7 +190,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
ui.buttons.s2Left.textContent = `S2 ${s2.keys?.left?.label || 'Left'} -> Waste`;
|
ui.buttons.s2Left.textContent = `S2 ${s2.keys?.left?.label || 'Left'} -> Waste`;
|
||||||
ui.buttons.s2Right.textContent = `S2 ${s2.keys?.right?.label || 'Right'} -> Truck`;
|
ui.buttons.s2Right.textContent = `S2 ${s2.keys?.right?.label || 'Right'} -> Truck`;
|
||||||
}
|
}
|
||||||
const nextTax = zundaTaxInfo(game.cash, game).tax;
|
const nextTax = zundaTaxInfo(Math.max(0, Math.floor(game.lastResult?.profit ?? game.stats?.profit ?? 0)), game).tax;
|
||||||
ui.buttons.nextTurn.textContent = game.phase === 'build' && nextTax > 0 ? `Next Day - ZUNDA ${yen(nextTax)}` : TEXT.actions.nextDay;
|
ui.buttons.nextTurn.textContent = game.phase === 'build' && nextTax > 0 ? `Next Day - ZUNDA ${yen(nextTax)}` : TEXT.actions.nextDay;
|
||||||
ui.buttons.nextTurn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || !factoryReady(game);
|
ui.buttons.nextTurn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || !factoryReady(game);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
227
styles.css
227
styles.css
|
|
@ -20,7 +20,7 @@ h1, h2, p { margin: 0; }
|
||||||
.game-shell { position: relative; width: 100%; min-height: min(100vh, 920px); border: 4px solid var(--line); border-top: 0; background: var(--screen); overflow: hidden; box-shadow: var(--shadow); }
|
.game-shell { position: relative; width: 100%; min-height: min(100vh, 920px); border: 4px solid var(--line); border-top: 0; background: var(--screen); overflow: hidden; box-shadow: var(--shadow); }
|
||||||
#gameCanvas { display: block; width: 100%; height: min(calc(100vh - 10px), 900px); min-height: 720px; background: var(--screen); touch-action: none; }
|
#gameCanvas { display: block; width: 100%; height: min(calc(100vh - 10px), 900px); min-height: 720px; background: var(--screen); touch-action: none; }
|
||||||
.hud { position: absolute; z-index: 4; display: grid; gap: 6px; pointer-events: none; }
|
.hud { position: absolute; z-index: 4; display: grid; gap: 6px; pointer-events: none; }
|
||||||
.hud-top-left { top: 10px; left: 10px; grid-template-columns: repeat(5, minmax(86px, auto)); }
|
.hud-top-left { top: 10px; left: 10px; grid-template-columns: repeat(6, minmax(82px, auto)); }
|
||||||
.hud-top-right { top: 10px; right: 10px; grid-template-columns: repeat(4, minmax(78px, auto)); }
|
.hud-top-right { top: 10px; right: 10px; grid-template-columns: repeat(4, minmax(78px, auto)); }
|
||||||
.hud-card { min-width: 82px; padding: 7px 9px; border: 3px solid var(--line); background: rgba(255,255,255,.88); box-shadow: 4px 4px 0 rgba(16,32,21,.16); }
|
.hud-card { min-width: 82px; padding: 7px 9px; border: 3px solid var(--line); background: rgba(255,255,255,.88); box-shadow: 4px 4px 0 rgba(16,32,21,.16); }
|
||||||
.hud-card.primary { min-width: 116px; padding: 10px 12px; background: rgba(255,255,255,.96); }
|
.hud-card.primary { min-width: 116px; padding: 10px 12px; background: rgba(255,255,255,.96); }
|
||||||
|
|
@ -29,7 +29,7 @@ h1, h2, p { margin: 0; }
|
||||||
.hud-card.time-critical { background: #fff0d1; box-shadow: inset 0 0 0 4px var(--danger), 4px 4px 0 rgba(16,32,21,.16); }
|
.hud-card.time-critical { background: #fff0d1; box-shadow: inset 0 0 0 4px var(--danger), 4px 4px 0 rgba(16,32,21,.16); }
|
||||||
.hud-card span { display: block; font-size: 9px; letter-spacing: .1em; color: var(--muted); margin-bottom: 4px; }
|
.hud-card span { display: block; font-size: 9px; letter-spacing: .1em; color: var(--muted); margin-bottom: 4px; }
|
||||||
.hud-card strong { font-size: clamp(15px, 1.3vw, 22px); line-height: 1; color: var(--ink); }
|
.hud-card strong { font-size: clamp(15px, 1.3vw, 22px); line-height: 1; color: var(--ink); }
|
||||||
.hud-card.cash strong, .hud-card.profit strong, .cash-positive { color: var(--green) !important; }
|
.hud-card.cash strong, .hud-card.profit strong, .hud-card.combo strong, .cash-positive { color: var(--green) !important; }
|
||||||
.cash-negative { color: var(--danger) !important; }
|
.cash-negative { color: var(--danger) !important; }
|
||||||
|
|
||||||
.priority-strip { position: absolute; z-index: 5; top: 76px; left: 10px; display: grid; grid-template-columns: minmax(280px, 1.2fr) minmax(170px, .75fr) minmax(130px, .55fr); gap: 8px; max-width: calc(100% - 420px); pointer-events: none; }
|
.priority-strip { position: absolute; z-index: 5; top: 76px; left: 10px; display: grid; grid-template-columns: minmax(280px, 1.2fr) minmax(170px, .75fr) minmax(130px, .55fr); gap: 8px; max-width: calc(100% - 420px); pointer-events: none; }
|
||||||
|
|
@ -66,6 +66,76 @@ h1, h2, p { margin: 0; }
|
||||||
.facility-action.danger { background: #ffdada; }
|
.facility-action.danger { background: #ffdada; }
|
||||||
.control-dock { position: absolute; z-index: 7; left: 50%; bottom: 12px; transform: translateX(-50%); display: grid; grid-template-columns: repeat(4, 150px); gap: 8px; padding: 7px; border: 3px solid var(--line); background: rgba(255,255,255,.90); }
|
.control-dock { position: absolute; z-index: 7; left: 50%; bottom: 12px; transform: translateX(-50%); display: grid; grid-template-columns: repeat(4, 150px); gap: 8px; padding: 7px; border: 3px solid var(--line); background: rgba(255,255,255,.90); }
|
||||||
.sort-button { min-height: 46px; font-size: 12px; }
|
.sort-button { min-height: 46px; font-size: 12px; }
|
||||||
|
.debug-panel {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
width: min(292px, calc(100% - 20px));
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
background: rgba(255,255,255,.94);
|
||||||
|
box-shadow: 4px 4px 0 rgba(16,32,21,.16);
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
.debug-panel summary {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
.debug-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 74px;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 6px;
|
||||||
|
border-top: 3px solid var(--line);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
.debug-body label,
|
||||||
|
.debug-body button,
|
||||||
|
.debug-body select,
|
||||||
|
.debug-body input {
|
||||||
|
min-width: 0;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
.debug-check,
|
||||||
|
.debug-card {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.debug-day {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 34px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.debug-body button {
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
background: var(--white);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 900;
|
||||||
|
padding: 4px 5px;
|
||||||
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
.debug-body input[type="number"],
|
||||||
|
.debug-body select {
|
||||||
|
width: 100%;
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
background: var(--white);
|
||||||
|
padding: 3px 4px;
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
.debug-readout {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
background: #f7fff5;
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 5px;
|
||||||
|
line-height: 1.2;
|
||||||
|
max-height: 48px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
.modal { position: fixed; inset: 0; z-index: 20; display: none; align-items: center; justify-content: center; padding: 22px; background: rgba(16,32,21,.45); }
|
.modal { position: fixed; inset: 0; z-index: 20; display: none; align-items: center; justify-content: center; padding: 22px; background: rgba(16,32,21,.45); }
|
||||||
.modal.visible { display: flex; }
|
.modal.visible { display: flex; }
|
||||||
.modal-card { width: min(860px, 96vw); border: 4px solid var(--line); padding: 24px; background: var(--white); box-shadow: var(--shadow); }
|
.modal-card { width: min(860px, 96vw); border: 4px solid var(--line); padding: 24px; background: var(--white); box-shadow: var(--shadow); }
|
||||||
|
|
@ -479,3 +549,156 @@ body { font-size: 18px; }
|
||||||
.build-panel { width: auto; }
|
.build-panel { width: auto; }
|
||||||
.control-dock { grid-template-columns: repeat(2, 172px); }
|
.control-dock { grid-template-columns: repeat(2, 172px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* v27.1 expansion/readability pass */
|
||||||
|
body { font-size: 20px; }
|
||||||
|
.panel-head h1 { font-size: 22px; }
|
||||||
|
.panel-head h1 span { font-size: 13px; }
|
||||||
|
.panel-head p { font-size: 14px; line-height: 1.35; }
|
||||||
|
.build-panel h2 { font-size: 15px; }
|
||||||
|
.priority-strip > div { font-size: 16px; }
|
||||||
|
.hud-card span { font-size: 13px; }
|
||||||
|
.hud-card strong { font-size: clamp(22px, 1.8vw, 30px); }
|
||||||
|
.mini-box, .contract-card { font-size: 15px; line-height: 1.5; }
|
||||||
|
.compact-rules .mini-box { font-size: 14px; }
|
||||||
|
.tool-button strong { font-size: 16px; line-height: 1.15; }
|
||||||
|
.tool-button span { font-size: 13px; line-height: 1.2; }
|
||||||
|
.tool-button .tool-flavor {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
width: min(320px, calc(100vw - 40px));
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.tool-button .tool-flavor::before { left: auto; right: 18px; }
|
||||||
|
.tool-button.unaffordable .tool-flavor,
|
||||||
|
.tool-button.unaffordable:hover .tool-flavor,
|
||||||
|
.tool-button:disabled .tool-flavor,
|
||||||
|
.tool-button:disabled:hover .tool-flavor { display: none; }
|
||||||
|
.facility-action { font-size: 12px; }
|
||||||
|
.primary-button { font-size: 17px; }
|
||||||
|
#nextTurnButton { font-size: 19px; }
|
||||||
|
.card-choice strong { font-size: 19px; }
|
||||||
|
.card-choice span { font-size: 12px; }
|
||||||
|
.card-choice small { font-size: 16px; line-height: 1.45; }
|
||||||
|
.muted-card-note { font-size: 16px; }
|
||||||
|
.modal-card p, .modal-card li { font-size: 17px; }
|
||||||
|
.modal.equipment-popover .modal-card h2 { font-size: 16px; }
|
||||||
|
.modal.equipment-popover .modal-actions button { font-size: 11px; }
|
||||||
|
.equipment-menu-lines.compact, .formula-box.compact { font-size: 11px; }
|
||||||
|
.hover-tooltip strong { font-size: 18px; }
|
||||||
|
.hover-tooltip span { font-size: 15px; }
|
||||||
|
.sort-button { font-size: 17px; }
|
||||||
|
|
||||||
|
/* v27.3 offscreen scanner monitor / expansion UI pass */
|
||||||
|
.scanner-monitor {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 8;
|
||||||
|
left: 12px;
|
||||||
|
bottom: 92px;
|
||||||
|
display: none;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
width: min(560px, calc(100% - 24px));
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.scanner-monitor.visible { display: grid; }
|
||||||
|
.scanner-monitor-card {
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
background: rgba(255,255,255,.95);
|
||||||
|
box-shadow: 5px 5px 0 rgba(16,32,21,.18);
|
||||||
|
padding: 9px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
.scanner-monitor-card header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.scanner-monitor-card header strong { font-size: 20px; color: var(--green); }
|
||||||
|
.scanner-monitor-card header span { font-size: 12px; color: var(--muted); }
|
||||||
|
.scanner-mini-map {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 42px 1fr 82px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.scanner-mini-map > span,
|
||||||
|
.scanner-mini-map > b {
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
background: #f7fff5;
|
||||||
|
padding: 7px 6px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
min-height: 36px;
|
||||||
|
}
|
||||||
|
.scanner-current { display: block; color: var(--ink); }
|
||||||
|
.scanner-current.male { color: #2477ff; }
|
||||||
|
.scanner-current.female { color: #d62a85; }
|
||||||
|
.scanner-current.poop { color: #7b4a23; }
|
||||||
|
.scanner-current.empty { color: var(--muted); }
|
||||||
|
|
||||||
|
.scanner-mini-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 27px);
|
||||||
|
gap: 3px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
background: #f6f1df;
|
||||||
|
}
|
||||||
|
.scanner-mini-grid .mini-cell {
|
||||||
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid rgba(22,38,25,.28);
|
||||||
|
background: #fffdfa;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.scanner-mini-grid .mini-cell.belt { background: #e7f7e4; color: var(--green-dark); }
|
||||||
|
.scanner-mini-grid .mini-cell.input { background: #fff3bf; color: var(--ink); font-size: 9px; }
|
||||||
|
.scanner-mini-grid .mini-cell.scanner { background: var(--green); color: #fff; border-color: var(--line); font-size: 16px; }
|
||||||
|
.scanner-mini-grid .mini-cell.scanner.male { background: #2477ff; }
|
||||||
|
.scanner-mini-grid .mini-cell.scanner.female { background: #d62a85; }
|
||||||
|
.scanner-mini-grid .mini-cell.scanner.poop { background: #7b4a23; }
|
||||||
|
.scanner-mini-grid .mini-cell.scanner-other { background: #b7f5c6; color: var(--ink); }
|
||||||
|
.scanner-mini-grid .mini-cell.egg { background: #fff7c4; color: #7a5a00; }
|
||||||
|
.scanner-mini-grid .mini-cell.port { background: #dbe7ff; color: #27427d; }
|
||||||
|
.scanner-mini-grid .mini-cell.blocked { background: #d7cfc1; color: #6d4d35; }
|
||||||
|
|
||||||
|
.scanner-monitor-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
.scanner-monitor-actions button {
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
background: var(--green-soft);
|
||||||
|
color: var(--ink);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 900;
|
||||||
|
min-height: 44px;
|
||||||
|
box-shadow: 3px 3px 0 rgba(16,32,21,.18);
|
||||||
|
}
|
||||||
|
.scanner-monitor-actions button:disabled {
|
||||||
|
background: #edf3ec;
|
||||||
|
color: var(--muted);
|
||||||
|
opacity: .62;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.scanner-monitor { bottom: 132px; width: min(520px, calc(100% - 16px)); left: 8px; }
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue