hm
This commit is contained in:
parent
33cad83ccb
commit
16bde0b87d
20 changed files with 460 additions and 988 deletions
|
|
@ -1,27 +0,0 @@
|
|||
# Chick Sorter v11.0 Routing / Compact Upgrades
|
||||
|
||||
Open `index.html` in a browser.
|
||||
|
||||
## v11.0 changes
|
||||
|
||||
- Egg Farm price is now JPY 300.
|
||||
- Initial S2-to-Truck conveyor lane is longer.
|
||||
- Egg Farm shutters reopen in Build phase after each day.
|
||||
- Mixer and Truck upgrades have no level cap. Each upgrade increases income and matching fines by `ceil(base × 1.05^upgrade count)`.
|
||||
- Manual Scanner and Conveyor upgrade choices are removed.
|
||||
- Equipment click menu is a compact bubble instead of a large overlay.
|
||||
- Explosion damage is `ceil(equipment price / 30)`.
|
||||
- Spawn blocking now highlights the stopped chick instead of showing `WAIT`.
|
||||
- Scanner/facility routing is more tolerant after manual conveyor dragging.
|
||||
|
||||
## Controls
|
||||
|
||||
Sorting:
|
||||
- S1: A = Mixer, D = S2
|
||||
- S2: Left Arrow = Shredder, Right Arrow = Truck
|
||||
|
||||
Build phase:
|
||||
- Left drag empty area: box select
|
||||
- Drag selected equipment: move selection
|
||||
- Right drag: pan
|
||||
- Ctrl+Z / Ctrl+Y: undo / redo
|
||||
|
|
@ -26,14 +26,7 @@ export const BUILD_TOOL_IDS = ['conveyor', 'eggFarm', 'autoScanner', 'manualScan
|
|||
export const MACHINE_FACILITY_IDS = ['mixer', 'trash', 'truck'];
|
||||
export const INCOME_FACILITY_IDS = ['mixer', 'truck'];
|
||||
|
||||
// Compatibility exports. New code should prefer FACILITY_DEFS and ECONOMY.
|
||||
export const BUILD_COSTS = Object.fromEntries(Object.entries(FACILITY_DEFS).map(([id, def]) => [id, def.price]));
|
||||
export const FACILITY_PRICES = Object.fromEntries(MACHINE_FACILITY_IDS.map(id => [id, FACILITY_DEFS[id].price]));
|
||||
export const MIXER_PRICE = ECONOMY.income.mixer;
|
||||
export const TRUCK_PRICE = ECONOMY.income.truck;
|
||||
export const POOP_FINE = ECONOMY.poopFine;
|
||||
export const ZUNDA_TAX = ECONOMY.zundaTax;
|
||||
export const INCOME_UPGRADE_RATE = ECONOMY.incomeUpgradeRate;
|
||||
|
||||
export const DIRS = [
|
||||
{ name: 'right', dc: 1, dr: 0, angle: 0, opposite: 'left' },
|
||||
|
|
|
|||
|
|
@ -2,6 +2,22 @@ import { BUILD_COSTS, FACILITY_DEFS, GRID } from './config.js';
|
|||
import { nextSpawnDelay } from './state.js';
|
||||
import { currentPoopRate } from '../systems/contracts.js';
|
||||
|
||||
const DEFAULT_MANUAL_KEYS = [
|
||||
[{ key: 'a', code: 'KeyA', label: 'A' }, { key: 'd', code: 'KeyD', label: 'D' }],
|
||||
[{ key: 'arrowleft', code: 'ArrowLeft', label: 'Left' }, { key: 'arrowright', code: 'ArrowRight', label: 'Right' }],
|
||||
[{ key: 'j', code: 'KeyJ', label: 'J' }, { key: 'l', code: 'KeyL', label: 'L' }],
|
||||
[{ key: 'f', code: 'KeyF', label: 'F' }, { key: 'h', code: 'KeyH', label: 'H' }],
|
||||
[{ key: 'q', code: 'KeyQ', label: 'Q' }, { key: 'e', code: 'KeyE', label: 'E' }],
|
||||
[{ key: 'z', code: 'KeyZ', label: 'Z' }, { key: 'x', code: 'KeyX', label: 'X' }],
|
||||
[{ key: 'u', code: 'KeyU', label: 'U' }, { key: 'o', code: 'KeyO', label: 'O' }],
|
||||
[{ key: 'n', code: 'KeyN', label: 'N' }, { key: 'm', code: 'KeyM', label: 'M' }]
|
||||
];
|
||||
|
||||
function manualKeysForSlot(slot) {
|
||||
const pair = DEFAULT_MANUAL_KEYS[slot] || DEFAULT_MANUAL_KEYS[0];
|
||||
return { left: { ...pair[0] }, right: { ...pair[1] } };
|
||||
}
|
||||
|
||||
export function createEggFarm(game, col, row) {
|
||||
const farm = {
|
||||
type: 'eggFarm', id: game.nextId++, col, row,
|
||||
|
|
@ -25,7 +41,8 @@ export function createScanner(game, col, row, kind) {
|
|||
queue: [], cooldown: 0,
|
||||
price: cost,
|
||||
builtSession: game.buildSession,
|
||||
autoMode: 'standard'
|
||||
autoMode: 'standard',
|
||||
keys: kind === 'manual' ? manualKeysForSlot(manualCount) : null
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ export function newTurnStats() {
|
|||
pendingTruckRevenue: 0,
|
||||
mixerRevenue: 0,
|
||||
truckRevenue: 0,
|
||||
chickShipmentIncome: 0,
|
||||
poopShipmentIncome: 0,
|
||||
manualComboBonus: 0,
|
||||
manualComboSuccess: 0,
|
||||
manualComboFailure: 0,
|
||||
repairWorkerWages: 0,
|
||||
zundaTax: 0,
|
||||
fairiesTribute: 0,
|
||||
loanRepayment: 0,
|
||||
|
|
@ -62,6 +68,10 @@ export function newTotalStats() {
|
|||
loanRepayment: 0,
|
||||
chemicalWeaponSubsidy: 0,
|
||||
rescueLoan: 0,
|
||||
manualComboBonus: 0,
|
||||
manualComboSuccess: 0,
|
||||
manualComboFailure: 0,
|
||||
repairWorkerWages: 0,
|
||||
cardRerollCost: 0,
|
||||
mixerPoopFine: 0,
|
||||
truckPoopFine: 0,
|
||||
|
|
@ -121,6 +131,8 @@ export function createGame() {
|
|||
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: [] },
|
||||
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
|
||||
cardTargetPick: null,
|
||||
manualCombo: { count: 0, lastBonus: 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 },
|
||||
stats: newTurnStats(),
|
||||
lastResult: null,
|
||||
|
|
@ -175,8 +187,8 @@ const INITIAL_CONVEYOR_BACKBONE = 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' },
|
||||
{ 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' }
|
||||
{ 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: 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' } } }
|
||||
]);
|
||||
|
||||
const INITIAL_EGG_FARMS = Object.freeze([
|
||||
|
|
@ -245,7 +257,7 @@ export function resetLayout(game) {
|
|||
game.blockedCells = new Set();
|
||||
installInitialConveyorBackbone(game);
|
||||
game.facilities = defaultFacilities();
|
||||
game.scanners = INITIAL_SCANNERS.map(scanner => ({ ...scanner, queue: [] }));
|
||||
game.scanners = INITIAL_SCANNERS.map(scanner => ({ ...scanner, keys: scanner.keys ? { left: { ...scanner.keys.left }, right: { ...scanner.keys.right } } : null, queue: [] }));
|
||||
game.eggFarms = INITIAL_EGG_FARMS.map(farm => {
|
||||
const nextSpawn = nextSpawnDelay(farm);
|
||||
return { ...farm, nextSpawn, lastInterval: nextSpawn };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { AUTO_SCANNER_COOLDOWN, BUILD_TOOL_IDS, FACILITY_DEFS, VERSION } from './config.js';
|
||||
import { AUTO_SCANNER_COOLDOWN, FACILITY_DEFS, VERSION } from './config.js';
|
||||
import { yen } from './utils.js';
|
||||
|
||||
export const TEXT = {
|
||||
|
|
@ -10,6 +10,7 @@ export const TEXT = {
|
|||
build: 'Build',
|
||||
running: 'Sorting',
|
||||
clearing: 'Clearing',
|
||||
loanDecision: 'Emergency Loan',
|
||||
gameover: 'Game Over'
|
||||
},
|
||||
actions: {
|
||||
|
|
@ -52,10 +53,6 @@ export function equipmentName(id) {
|
|||
return FACILITY_DEFS[id]?.name || String(id || '').replace(/^[a-z]/, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
export function shortEquipmentName(id) {
|
||||
return FACILITY_DEFS[id]?.shortName || equipmentName(id);
|
||||
}
|
||||
|
||||
export function buildToolPriceText(id) {
|
||||
const def = FACILITY_DEFS[id];
|
||||
if (!def) return '';
|
||||
|
|
@ -83,10 +80,6 @@ export function buildToolButtonHtml(id) {
|
|||
return `<strong>${equipmentName(id)}</strong><span>${buildToolPriceText(id)}</span>${flavor ? `<small class="tool-flavor">${flavor}</small>` : ''}`;
|
||||
}
|
||||
|
||||
export function buildToolButtonIds() {
|
||||
return BUILD_TOOL_IDS;
|
||||
}
|
||||
|
||||
export function routeLabel(dest) {
|
||||
return TEXT.routeLabels[dest] || String(dest).toUpperCase();
|
||||
}
|
||||
|
|
|
|||
78
src/game.js
78
src/game.js
|
|
@ -3,7 +3,7 @@ import { buildToolButtonHtml } from './core/text.js';
|
|||
import { createGame, resetLayout, newTurnStats } from './core/state.js';
|
||||
import { clamp, pointToCell, cellCenter, yen } from './core/utils.js';
|
||||
import { commitFactoryGraphForDay, facilityConnectionIssues } from './systems/routing.js';
|
||||
import { applyRescueLoanIfNeeded, 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 { drawAll } from './render/draw.js';
|
||||
import { createBuildSystem } from './systems/buildSystem.js';
|
||||
|
|
@ -108,6 +108,15 @@ function startNextTurn() {
|
|||
game.truckCargo = [];
|
||||
game.view = { x: 0, y: 0 };
|
||||
game.stats = newTurnStats();
|
||||
game.stats.zundaTax = game.lastStartFees?.zundaTax || 0;
|
||||
game.stats.loanRepayment = game.lastStartFees?.loanRepayment || 0;
|
||||
game.stats.repairWorkerWages = game.pendingRepairWorkerWages || 0;
|
||||
const carriedStartCosts = game.stats.zundaTax + game.stats.loanRepayment + game.stats.repairWorkerWages;
|
||||
if (carriedStartCosts > 0) {
|
||||
game.stats.penalty += carriedStartCosts;
|
||||
game.stats.profit -= carriedStartCosts;
|
||||
}
|
||||
game.pendingRepairWorkerWages = 0;
|
||||
for (const scanner of game.scanners) { scanner.queue = []; scanner.cooldown = 0; }
|
||||
for (const farm of game.eggFarms) { farm.nextSpawn = chicks.currentSpawnDelay(farm); farm.lastInterval = farm.nextSpawn; farm.shutterProgress = 0; farm.shutterSparked = false; }
|
||||
game.buildTool = null;
|
||||
|
|
@ -139,17 +148,33 @@ function completeTurn() {
|
|||
const ship = settleTruckRevenue(game);
|
||||
const contract = resolveContract(game);
|
||||
const chemical = collectChemicalWeaponSubsidy(game);
|
||||
if (ship.adjusted > 0) {
|
||||
const truck = game.facilities.truck;
|
||||
const x = truck ? truck.x + truck.w / 2 : canvas.width / 2 - game.view.x;
|
||||
const y = truck ? truck.y + 26 : 96 - game.view.y;
|
||||
floating(game, x, y, `${ship.target === 'poop' ? 'POOP SHIP' : 'SHIP'} +${yen(ship.adjusted)}`, THEME.green);
|
||||
}
|
||||
if (contract?.bonus > 0) floating(game, canvas.width / 2 - game.view.x, 96 - game.view.y, `CONTRACT +${yen(contract.bonus)}`, THEME.green);
|
||||
if (chemical.amount > 0) floating(game, canvas.width / 2 - game.view.x, 116 - game.view.y, `CHEM SUBSIDY +${yen(chemical.amount)}`, THEME.green);
|
||||
clearActiveContract(game);
|
||||
const settlementDay = Math.max(1, (game.totals.turnsCompleted || 0) + 1);
|
||||
const tribute = collectFairiesTribute(game, settlementDay);
|
||||
game.lastBuildFees = { fairiesTribute: tribute.amount || 0, reduction: tribute.reduction || 0, turn: settlementDay };
|
||||
if (tribute.amount > 0) floating(game, canvas.width / 2 - game.view.x, 146 - game.view.y, `FAIRIES -${yen(tribute.amount)}`, THEME.danger);
|
||||
const rescue = applyRescueLoanIfNeeded(game);
|
||||
if (rescue.amount > 0) floating(game, canvas.width / 2 - game.view.x, 176 - game.view.y, `RESCUE +${yen(rescue.amount)}`, THEME.green);
|
||||
game.totals.turnsCompleted += 1;
|
||||
game.lastResult = { ...game.stats, ship, contract, chemical, rescue, fairiesTribute: tribute, turn: game.turn, cash: game.cash };
|
||||
if (game.cash < 0) { game.phase = 'gameover'; uiSystem.showGameOver(); return; }
|
||||
game.lastResult = { ...game.stats, ship, contract, chemical, rescue: { amount: 0, used: 0, remaining: game.cardEffects?.rescueLoanCharges || 0 }, fairiesTribute: tribute, turn: game.turn, cash: game.cash };
|
||||
if (game.cash < 0) {
|
||||
if ((game.cardEffects?.rescueLoanCharges || 0) > 0) {
|
||||
game.phase = 'loanDecision';
|
||||
uiSystem.showEmergencyLoanDecision(useEmergencyLoan, acceptGameOver);
|
||||
return;
|
||||
}
|
||||
game.phase = 'gameover'; uiSystem.showGameOver(); return;
|
||||
}
|
||||
enterBuildPhase();
|
||||
}
|
||||
|
||||
function enterBuildPhase() {
|
||||
game.phase = 'build';
|
||||
game.truckCargo = [];
|
||||
game.chicks = [];
|
||||
|
|
@ -174,6 +199,24 @@ function completeTurn() {
|
|||
uiSystem.showTurnResult(game.lastResult);
|
||||
}
|
||||
|
||||
function useEmergencyLoan() {
|
||||
if ((game.cardEffects?.rescueLoanCharges || 0) <= 0) return acceptGameOver();
|
||||
game.cardEffects.rescueLoanCharges -= 1;
|
||||
game.stats.rescueLoan = (game.stats.rescueLoan || 0) + 500;
|
||||
game.totals.rescueLoan = (game.totals.rescueLoan || 0) + 500;
|
||||
const amount = applyRevenue(game, 500);
|
||||
game.lastResult = { ...(game.lastResult || {}), ...game.stats, rescue: { amount: ((game.lastResult?.rescue?.amount || 0) + amount), used: ((game.lastResult?.rescue?.used || 0) + 1), remaining: game.cardEffects.rescueLoanCharges }, cash: game.cash };
|
||||
floating(game, canvas.width / 2 - game.view.x, 176 - game.view.y, `RESCUE +${yen(amount)}`, THEME.green);
|
||||
if (game.cash >= 0) enterBuildPhase();
|
||||
else if ((game.cardEffects?.rescueLoanCharges || 0) > 0) { game.phase = 'loanDecision'; uiSystem.showEmergencyLoanDecision(useEmergencyLoan, acceptGameOver); }
|
||||
else acceptGameOver();
|
||||
}
|
||||
|
||||
function acceptGameOver() {
|
||||
game.phase = 'gameover';
|
||||
uiSystem.showGameOver();
|
||||
}
|
||||
|
||||
function closeFarmShutters() {
|
||||
if (game.shutdownTimeLeft <= 0) game.shutdownTimeLeft = game.shutdownGraceSeconds || 10;
|
||||
for (const farm of game.eggFarms) {
|
||||
|
|
@ -226,8 +269,9 @@ function showHoverTooltip(event, hit) {
|
|||
if (!obj) return hideHoverTooltip();
|
||||
const title = build.selectedTitle(obj);
|
||||
const flavor = build.flavorText ? build.flavorText(obj) : '';
|
||||
const warning = build.disconnectedWarningFor ? build.disconnectedWarningFor(obj) : '';
|
||||
const lines = build.selectedInfoLines(obj).slice(0, 7);
|
||||
ui.hoverTooltip.innerHTML = `<strong>${title}</strong>${flavor ? `<em>${flavor}</em>` : ''}${lines.map(line => `<span>${line}</span>`).join('')}`;
|
||||
ui.hoverTooltip.innerHTML = `<strong>${title}</strong>${warning ? `<em class="warning">${warning}</em>` : (flavor ? `<em>${flavor}</em>` : '')}${lines.map(line => `<span>${line}</span>`).join('')}`;
|
||||
const x = Math.min(window.innerWidth - 330, event.clientX + 18);
|
||||
const y = Math.min(window.innerHeight - 220, event.clientY + 18);
|
||||
ui.hoverTooltip.style.left = `${Math.max(8, x)}px`;
|
||||
|
|
@ -250,6 +294,8 @@ function clickSelect(event) {
|
|||
game.selected = sel;
|
||||
game.multiSelected = [sel];
|
||||
uiSystem.updatePanels();
|
||||
const obj = build.selectedObject();
|
||||
if (obj?.type === 'scanner' && obj.kind === 'manual') build.showManualScannerMenu(obj);
|
||||
}
|
||||
|
||||
canvas.addEventListener('contextmenu', event => event.preventDefault());
|
||||
|
|
@ -300,7 +346,25 @@ ui.buttons.s1Left.addEventListener('click', () => chicks.sortSlot(0, 'left')); u
|
|||
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.buttons.undo.addEventListener('click', () => { if (undo(game)) uiSystem.updatePanels(); }); ui.buttons.redo.addEventListener('click', () => { if (redo(game)) uiSystem.updatePanels(); });
|
||||
window.addEventListener('keydown', event => { if (event.repeat) return; const name = event.key.toLowerCase(); if (name === 'a') { event.preventDefault(); chicks.sortSlot(0, 'left'); } if (name === 'd') { event.preventDefault(); chicks.sortSlot(0, 'right'); } if (event.key === 'ArrowLeft') { event.preventDefault(); chicks.sortSlot(1, 'left'); } if (event.key === 'ArrowRight') { event.preventDefault(); chicks.sortSlot(1, 'right'); } 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 === 'y' || (event.shiftKey && name === 'z'))) { event.preventDefault(); if (redo(game)) uiSystem.updatePanels(); } if (event.key === 'Escape' && game.cardTargetPick?.pending) { event.preventDefault(); cardSystem.cancelTargetPick(); } });
|
||||
function scannerKeyMatches(scanner, side, event) {
|
||||
const binding = scanner.keys?.[side];
|
||||
if (!binding) return false;
|
||||
return (binding.code && event.code === binding.code) || (binding.key && event.key.toLowerCase() === binding.key);
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', event => {
|
||||
if (event.repeat) return;
|
||||
const name = event.key.toLowerCase();
|
||||
if (game.phase === 'running') {
|
||||
for (const scanner of game.scanners.filter(s => s.kind === 'manual')) {
|
||||
if (scannerKeyMatches(scanner, 'left', event)) { event.preventDefault(); chicks.sortScanner(scanner, 'left'); 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 === 'y' || (event.shiftKey && name === 'z'))) { event.preventDefault(); if (redo(game)) uiSystem.updatePanels(); }
|
||||
if (event.key === 'Escape' && game.cardTargetPick?.pending) { event.preventDefault(); cardSystem.cancelTargetPick(); }
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
106
src/index.html
106
src/index.html
|
|
@ -1,106 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Chick Sorter v27.0</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main id="app">
|
||||
<section id="gameShell" class="game-shell" aria-label="Game screen">
|
||||
<canvas id="gameCanvas" width="1440" height="900" aria-label="Chick Sorter game canvas"></canvas>
|
||||
|
||||
<div class="hud hud-top-left" aria-label="Run status">
|
||||
<div class="hud-card cash"><span>CASH</span><strong id="money">¥250</strong></div>
|
||||
<div class="hud-card"><span>TIME</span><strong id="timeLeft">60.0s</strong></div>
|
||||
<div class="hud-card profit"><span>NET</span><strong id="turnProfit">¥0</strong></div>
|
||||
<div class="hud-card"><span>DAY</span><strong id="turn">1</strong></div>
|
||||
<div class="hud-card compact"><span>PHASE</span><strong id="phaseLabel">Title</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="hud hud-top-right" aria-label="Day result details">
|
||||
<div class="hud-card"><span>MIXER</span><strong id="mixerCount">0</strong></div>
|
||||
<div class="hud-card"><span>TRUCK F</span><strong id="truckFemaleCount">0</strong></div>
|
||||
<div class="hud-card"><span>WRONG</span><strong id="truckMaleCount">0</strong></div>
|
||||
<div class="hud-card"><span>POOP</span><strong id="poopCount">0</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="priority-strip" aria-label="Important day info">
|
||||
<div id="eventBrief" class="event-brief">NO ACTIVE EVENT</div>
|
||||
<div id="targetBrief" class="target-brief">TRUCK TARGET: FEMALE</div>
|
||||
<div id="speedBrief" class="speed-brief">BELT: 52 px/s</div>
|
||||
</div>
|
||||
|
||||
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h1>CHICK SORTER <span>v27.0</span></h1>
|
||||
<p>Build, drag, connect at least one valid EGG route, then start the next day.</p>
|
||||
</div>
|
||||
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-grid">
|
||||
<section class="equipment-section">
|
||||
<h2>Add / Edit Equipment</h2>
|
||||
<div class="large-tools">
|
||||
<button id="buildConveyorButton" class="tool-button" type="button"><strong>Conveyor</strong><span>¥30 / tile</span></button>
|
||||
<button id="buildEggFarmButton" class="tool-button" type="button"><strong>Egg Farm</strong><span>¥250</span></button>
|
||||
<button id="buildAutoScannerButton" class="tool-button" type="button"><strong>Auto Scanner</strong><span>¥360 / 2.25s cooldown</span></button>
|
||||
<button id="buildManualScannerButton" class="tool-button" type="button"><strong>Manual Scanner</strong><span>¥260</span></button>
|
||||
<button id="buildMixerButton" class="tool-button" type="button"><strong>Mixer</strong><span>¥450</span></button>
|
||||
<button id="buildTrashButton" class="tool-button" type="button"><strong>Waste Shredder</strong><span>¥240</span></button>
|
||||
<button id="buildTruckButton" class="tool-button" type="button"><strong>Truck</strong><span>¥450</span></button>
|
||||
<button id="eraseButton" class="tool-button danger" type="button"><strong>Sell</strong><span>Sell or refund equipment</span></button>
|
||||
<button id="hireRepairmanButton" class="tool-button repair" type="button"><strong>Hire Repairman</strong><span>¥100 / next day</span></button>
|
||||
<div class="history-tools"><button id="undoButton" class="tool-button small" type="button"><strong>Undo</strong><span>Ctrl+Z</span></button><button id="redoButton" class="tool-button small" type="button"><strong>Redo</strong><span>Ctrl+Y</span></button></div>
|
||||
</div>
|
||||
<div id="buildStatus" class="mini-box">Build tools unlock after each day.</div>
|
||||
</section>
|
||||
|
||||
<section class="contract-section">
|
||||
<h2>Irregular One-Day Event</h2>
|
||||
<div id="contractPanel" class="contract-card empty">Irregular forced events appear during Build phase.</div>
|
||||
</section>
|
||||
<section class="rules-section compact-rules">
|
||||
<h2>Status</h2>
|
||||
<div id="turnSummary" class="mini-box"></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-dock" aria-label="Sorting controls">
|
||||
<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="scanner2MixerButton" class="sort-button" type="button">S2 -> Waste</button>
|
||||
<button id="scanner2TruckButton" class="sort-button" type="button">S2 -> Truck</button>
|
||||
</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><input id="debugInfiniteCash" type="checkbox" /> Infinite cash</label>
|
||||
<label>Day <input id="debugDayInput" type="number" min="1" step="1" value="1" /></label>
|
||||
<button id="debugSetDayButton" type="button">Set Day</button>
|
||||
<label>Upgrade <select id="debugUpgradeSelect"></select></label>
|
||||
<button id="debugApplyUpgradeButton" type="button">Free Buy</button>
|
||||
<div id="debugObservations" class="debug-readout"></div>
|
||||
<div id="debugEventLog" class="debug-readout event-log"></div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="modal" class="modal visible">
|
||||
<div class="modal-card">
|
||||
<h2 id="modalTitle">Chick Sorter</h2>
|
||||
<div id="modalBody"></div>
|
||||
<div id="modalActions" class="modal-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./game.js?v=27.0-combo-settlement"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { GRID, THEME, EFFECT_PRIORITY, VERSION } from '../core/config.js';
|
||||
import { key, parseKey, pointToCell, cellCenter, mixHex, randomBetween, yen } from '../core/utils.js';
|
||||
import { getConveyorNeighbors, routeFromFarmToScanner, outputRoute, scannerCenter, scannerConnector, scannerOutputs, destinationLabel, destinationColor, scannerPortHasConveyor } 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 { cardTargetBounds } from '../systems/cards.js';
|
||||
import { wearRatio } from '../systems/maintenance.js';
|
||||
|
|
@ -49,6 +49,7 @@ export function drawAll(ctx, canvas, game, helpers) {
|
|||
drawFacilities(ctx, game);
|
||||
drawEggFarms(ctx, game);
|
||||
drawScanners(ctx, game);
|
||||
drawDisconnectedWarnings(ctx, game);
|
||||
drawChicks(ctx, game, helpers.activeQueuedChick);
|
||||
drawRepairman(ctx, game);
|
||||
drawCardTargetOverlay(ctx, canvas, game);
|
||||
|
|
@ -60,6 +61,41 @@ export function drawAll(ctx, canvas, game, helpers) {
|
|||
drawCanvasHints(ctx, game, canvas);
|
||||
}
|
||||
|
||||
function warningBounds(item) {
|
||||
if (item.type === 'eggFarm') {
|
||||
const c = cellCenter(item.ref.col, item.ref.row);
|
||||
return { x: c.x - 34, y: c.y - 34, w: 68, h: 68, cx: c.x, cy: c.y };
|
||||
}
|
||||
const f = item.ref;
|
||||
return { x: f.x, y: f.y, w: f.w, h: f.h, cx: f.x + f.w / 2, cy: f.y + f.h / 2 };
|
||||
}
|
||||
|
||||
function drawDisconnectedWarnings(ctx, game) {
|
||||
if (game.phase !== 'build') return;
|
||||
const warnings = disconnectedBuildWarnings(game);
|
||||
for (const item of warnings) {
|
||||
const b = warningBounds(item);
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'rgba(196,36,36,.22)';
|
||||
ctx.strokeStyle = THEME.danger;
|
||||
ctx.lineWidth = 5;
|
||||
ctx.setLineDash([10, 6]);
|
||||
rect(ctx, b.x, b.y, b.w, b.h, true, true);
|
||||
ctx.setLineDash([]);
|
||||
ctx.fillStyle = THEME.danger;
|
||||
ctx.strokeStyle = THEME.white;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.font = '900 18px ui-monospace, monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.strokeText('!', b.cx, b.y + 24);
|
||||
ctx.fillText('!', b.cx, b.y + 24);
|
||||
ctx.font = '900 9px ui-monospace, monospace';
|
||||
ctx.strokeText('NO ROUTE', b.cx, b.y + b.h - 8);
|
||||
ctx.fillText('NO ROUTE', b.cx, b.y + b.h - 8);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
@ -282,6 +318,17 @@ function drawScanners(ctx, game) { for (const s of game.scanners) drawScanner(ct
|
|||
function drawScanner(ctx, scanner, game) {
|
||||
const c = scannerCenter(scanner);
|
||||
ctx.save();
|
||||
if (scanner.kind === 'manual' && (game.manualCombo?.count || 0) >= 10) {
|
||||
const combo = game.manualCombo.count;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = Math.min(0.55, 0.16 + combo / 120);
|
||||
ctx.strokeStyle = combo >= 30 ? THEME.warn : THEME.green;
|
||||
ctx.lineWidth = Math.min(14, 5 + Math.floor(combo / 10));
|
||||
ctx.shadowColor = ctx.strokeStyle;
|
||||
ctx.shadowBlur = Math.min(28, 8 + combo);
|
||||
rect(ctx, c.x - 60, c.y - 44, 120, 88, false, true);
|
||||
ctx.restore();
|
||||
}
|
||||
const img = scanner.kind === 'auto' ? assets.scannerAuto : assets.scannerManual;
|
||||
if (drawImageIfLoaded(ctx, img, c.x - 54, c.y - 38, 108, 76)) {
|
||||
if (scanner.kind === 'auto') drawDirtOverlay(ctx, c.x - 54, c.y - 38, 108, 76, scanner);
|
||||
|
|
@ -296,7 +343,9 @@ function drawScanner(ctx, scanner, game) {
|
|||
ctx.font = '900 13px ui-monospace, monospace'; ctx.textAlign = 'center';
|
||||
ctx.fillText(`${scanner.kind === 'auto' ? 'AUTO' : `S${(scanner.slot ?? 0) + 1}`}`, c.x, c.y - 14);
|
||||
ctx.font = '900 9px ui-monospace, monospace';
|
||||
ctx.fillText(scanner.role === 0 ? 'A/L:M D/R:NEXT' : 'L:WASTE R:TRUCK', c.x, c.y + 3);
|
||||
const leftKey = scanner.keys?.left?.label || (scanner.slot === 0 ? 'A' : 'Left');
|
||||
const rightKey = scanner.keys?.right?.label || (scanner.slot === 0 ? 'D' : 'Right');
|
||||
ctx.fillText(scanner.role === 0 ? `${leftKey}:M ${rightKey}:NEXT` : `${leftKey}:WASTE ${rightKey}:TRUCK`, c.x, c.y + 3);
|
||||
const q = scanner.queue.length;
|
||||
if (scanner.kind === 'manual') {
|
||||
drawManualKeyboardIcon(ctx, scanner, c);
|
||||
|
|
@ -319,6 +368,8 @@ function drawManualKeyboardIcon(ctx, scanner, c) {
|
|||
const labels = slot === 0
|
||||
? [{ side: 'left', text: 'A' }, { side: 'right', text: 'D' }]
|
||||
: [{ side: 'left', text: '←' }, { side: 'right', text: '→' }];
|
||||
labels[0].text = scanner.keys?.left?.label || labels[0].text;
|
||||
labels[1].text = scanner.keys?.right?.label || labels[1].text;
|
||||
const pressedSide = scanner.keyPressTime > 0 ? scanner.keyPressSide : null;
|
||||
const baseX = c.x - 30;
|
||||
const baseY = c.y + 8;
|
||||
|
|
|
|||
623
src/styles.css
623
src/styles.css
|
|
@ -1,623 +0,0 @@
|
|||
:root {
|
||||
--bg: #f2fff0;
|
||||
--screen: #eefbea;
|
||||
--panel: rgba(245, 255, 242, 0.96);
|
||||
--ink: #102015;
|
||||
--muted: #526456;
|
||||
--line: #102015;
|
||||
--green: #22b94f;
|
||||
--green-soft: #b7f5c6;
|
||||
--white: #ffffff;
|
||||
--danger: #c42424;
|
||||
--shadow: 8px 8px 0 rgba(16, 32, 21, 0.22);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { min-height: 100%; }
|
||||
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; overflow-x: hidden; }
|
||||
button { font: inherit; }
|
||||
h1, h2, p { margin: 0; }
|
||||
#app { width: min(100vw, 1680px); margin: 0 auto; padding: 0 8px 12px; }
|
||||
.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; }
|
||||
.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-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.primary { min-width: 116px; padding: 10px 12px; background: rgba(255,255,255,.96); }
|
||||
.hud-card.primary span { font-size: 9px; }
|
||||
.hud-card.primary strong { font-size: clamp(22px, 2vw, 32px); }
|
||||
.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 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; }
|
||||
.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 > div { border: 3px solid var(--line); background: rgba(255,255,255,.94); padding: 8px 10px; font-weight: 900; text-transform: uppercase; box-shadow: 4px 4px 0 rgba(16,32,21,.14); font-size: 12px; letter-spacing: .03em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.event-brief.active { background: #fff0d1; box-shadow: inset 0 0 0 3px var(--danger), 4px 4px 0 rgba(16,32,21,.14); }
|
||||
.event-brief.next { background: var(--green-soft); box-shadow: inset 0 0 0 3px var(--green), 4px 4px 0 rgba(16,32,21,.14); }
|
||||
.event-brief.locked { background: #edf3ec; color: var(--muted); }
|
||||
.target-brief { color: var(--green); }
|
||||
.speed-brief { color: var(--ink); }
|
||||
|
||||
.build-panel { position: absolute; z-index: 6; top: 118px; right: 12px; bottom: 12px; width: clamp(278px, 23vw, 360px); min-height: 0; padding: 10px; border: 4px solid var(--line); background: var(--panel); box-shadow: var(--shadow); overflow: auto; }
|
||||
.panel-head { display: grid; gap: 8px; margin-bottom: 10px; }
|
||||
.panel-head h1 { font-size: 15px; letter-spacing: .06em; }
|
||||
.panel-head h1 span { font-size: 9px; color: var(--white); background: var(--green); border: 2px solid var(--line); padding: 1px 6px; }
|
||||
.panel-head p { color: var(--muted); margin-top: 4px; font-size: 9px; line-height: 1.25; }
|
||||
.panel-grid { display: grid; grid-template-columns: 1fr; gap: 10px; }
|
||||
.build-panel h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .1em; margin-bottom: 7px; }
|
||||
.large-tools { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
||||
.history-tools { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; grid-column: 1 / -1; }
|
||||
.tool-button.small { min-height: 40px; padding: 6px; }
|
||||
.tool-button, .primary-button, .sort-button, .facility-action { border: 3px solid var(--line); background: var(--white); color: var(--ink); font-weight: 900; text-transform: uppercase; cursor: pointer; box-shadow: 4px 4px 0 rgba(16,32,21,.18); }
|
||||
.tool-button { min-height: 50px; text-align: left; padding: 8px; }
|
||||
.tool-button strong { display: block; font-size: 12px; }
|
||||
.tool-button span { display: block; margin-top: 4px; font-size: 9px; color: var(--muted); }
|
||||
.tool-button.active { background: var(--green-soft); box-shadow: inset 0 0 0 3px var(--green), 4px 4px 0 rgba(16,32,21,.18); }
|
||||
.tool-button.danger.active { background: #ffdada; box-shadow: inset 0 0 0 3px var(--danger), 4px 4px 0 rgba(16,32,21,.18); }
|
||||
.primary-button { padding: 10px 12px; background: var(--green); color: var(--white); white-space: nowrap; }
|
||||
.primary-button:disabled, .tool-button:disabled, .sort-button:disabled, .facility-action:disabled { opacity: .42; cursor: not-allowed; }
|
||||
.mini-box, .facility-panel-empty, .facility-card { border: 3px solid var(--line); background: var(--white); padding: 12px; line-height: 1.45; color: var(--muted); font-size: 11px; }
|
||||
.facility-card h3 { color: var(--ink); font-size: 12px; margin: 0 0 6px; }
|
||||
.facility-card p { margin: 4px 0; }
|
||||
.facility-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||
.facility-action { padding: 7px 9px; font-size: 9px; }
|
||||
.facility-action.good { background: var(--green-soft); }
|
||||
.facility-action.warn { background: #fff0d1; }
|
||||
.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); }
|
||||
.sort-button { min-height: 46px; font-size: 12px; }
|
||||
.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-card { width: min(860px, 96vw); border: 4px solid var(--line); padding: 24px; background: var(--white); box-shadow: var(--shadow); }
|
||||
.modal-card h2 { font-size: 26px; text-transform: uppercase; margin-bottom: 12px; }
|
||||
.modal-card p { line-height: 1.55; }
|
||||
.modal-card ul { margin: 12px 0 0; padding-left: 22px; line-height: 1.55; color: var(--muted); }
|
||||
.modal-actions { margin-top: 18px; display: flex; justify-content: flex-end; gap: 10px; flex-wrap: wrap; }
|
||||
.route-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 14px 0; }
|
||||
.route-card { border: 3px solid var(--line); background: #f7fff5; padding: 14px; display: grid; grid-template-columns: 44px 1fr 28px 1fr; align-items: center; gap: 4px 8px; }
|
||||
.route-card .route-icon { grid-row: 1 / span 2; font-size: 30px; text-align: center; }
|
||||
.route-card strong, .route-card em { font-style: normal; font-weight: 900; text-transform: uppercase; }
|
||||
.route-card b { font-size: 24px; color: var(--green); text-align: center; }
|
||||
.route-card small { grid-column: 2 / span 3; color: var(--muted); font-weight: 900; }
|
||||
.result-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 12px; }
|
||||
.result-grid div { border: 3px solid var(--line); padding: 10px; background: #f7fff5; }
|
||||
.result-grid strong { display: block; font-size: 11px; color: var(--muted); text-transform: uppercase; margin-bottom: 6px; }
|
||||
.result-grid span { font-size: 16px; font-weight: 900; color: var(--ink); }
|
||||
.game-shell:not(.phase-build) .build-panel { display: none; }
|
||||
.game-shell.phase-running .hud-top-right { display: none; }
|
||||
.game-shell:not(.phase-running) .control-dock { display: none; }
|
||||
@media (max-width: 1180px) {
|
||||
#app { padding: 0; }
|
||||
.game-shell { min-height: 100vh; }
|
||||
#gameCanvas { height: 100vh; }
|
||||
.hud-top-left, .hud-top-right { grid-template-columns: repeat(2, minmax(76px, auto)); }
|
||||
.priority-strip { top: 150px; left: 8px; right: 8px; max-width: none; grid-template-columns: 1fr; }
|
||||
.build-panel { top: auto; left: 8px; right: 8px; bottom: 8px; width: auto; max-height: 42vh; }
|
||||
.large-tools { grid-template-columns: 1fr 1fr; }
|
||||
.control-dock { grid-template-columns: repeat(2, 150px); }
|
||||
.route-cards, .result-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.contract-card { border: 3px solid var(--line); background: #f7fff5; padding: 12px; line-height: 1.35; color: var(--ink); font-size: 11px; box-shadow: 4px 4px 0 rgba(16,32,21,.14); }
|
||||
.contract-card.empty { color: var(--muted); background: rgba(255,255,255,.72); }
|
||||
.contract-card.empty strong { display: block; color: var(--ink); margin-bottom: 4px; }
|
||||
.contract-card h3 { margin: 2px 0 7px; font-size: 14px; text-transform: uppercase; letter-spacing: .04em; }
|
||||
.contract-card p { margin: 5px 0; }
|
||||
.contract-card.accepted { background: var(--green-soft); box-shadow: inset 0 0 0 3px var(--green), 4px 4px 0 rgba(16,32,21,.14); }
|
||||
.contract-card.declined { background: #f1f1f1; color: var(--muted); }
|
||||
.contract-status { display: inline-block; padding: 2px 7px; border: 2px solid var(--line); background: var(--white); font-size: 9px; font-weight: 900; letter-spacing: .1em; margin-bottom: 4px; }
|
||||
.contract-metrics { display: grid; grid-template-columns: 1fr; gap: 4px; margin: 8px 0; }
|
||||
.contract-metrics span { display: block; border: 2px solid rgba(16,32,21,.55); background: rgba(255,255,255,.82); padding: 5px; font-weight: 900; color: var(--ink); }
|
||||
.contract-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
|
||||
.result-section-title { margin: 16px 0 8px; font-size: 12px; font-weight: 900; text-transform: uppercase; letter-spacing: .12em; color: var(--green); }
|
||||
.result-section-title:first-child { margin-top: 0; }
|
||||
.muted-title { color: var(--muted); border: 3px solid var(--line); padding: 10px; background: #f7fff5; }
|
||||
|
||||
.compact-rules .mini-box { padding: 8px; font-size: 9px; }
|
||||
.primary-button:disabled { filter: grayscale(1); background: #c9d2ca; color: #526456; }
|
||||
.formula-box {
|
||||
border: 3px solid var(--line);
|
||||
background: #f7fff5;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
line-height: 1.5;
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.equipment-menu-lines {
|
||||
border: 3px solid var(--line);
|
||||
background: #ffffff;
|
||||
padding: 10px;
|
||||
margin: 8px 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.equipment-menu-lines p { margin: 4px 0; }
|
||||
|
||||
/* v11 compact clicked-equipment bubble */
|
||||
.modal.equipment-popover {
|
||||
background: transparent;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.modal.equipment-popover .modal-card {
|
||||
position: absolute;
|
||||
left: var(--popover-x, 16px);
|
||||
top: var(--popover-y, 16px);
|
||||
width: min(250px, calc(100vw - 24px));
|
||||
max-height: min(280px, calc(100vh - 24px));
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
border-width: 3px;
|
||||
pointer-events: auto;
|
||||
box-shadow: 6px 6px 0 rgba(16,32,21,.18);
|
||||
}
|
||||
.modal.equipment-popover .modal-card h2 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.modal.equipment-popover .modal-actions {
|
||||
margin-top: 8px;
|
||||
gap: 6px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.modal.equipment-popover .modal-actions button {
|
||||
padding: 5px 7px;
|
||||
font-size: 9px;
|
||||
}
|
||||
.equipment-menu-lines.compact {
|
||||
padding: 7px;
|
||||
margin: 6px 0;
|
||||
font-size: 9px;
|
||||
}
|
||||
.formula-box.compact {
|
||||
padding: 7px;
|
||||
margin: 6px 0;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
/* v15 pre-build card draft */
|
||||
.card-choices {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 220px));
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.card-choice {
|
||||
min-height: 132px;
|
||||
border: 3px solid var(--line);
|
||||
background: #f7fff5;
|
||||
color: var(--ink);
|
||||
box-shadow: 5px 5px 0 rgba(16,32,21,.16);
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-weight: 900;
|
||||
}
|
||||
.card-choice:hover { transform: translate(-1px, -1px); box-shadow: 6px 6px 0 rgba(16,32,21,.18); }
|
||||
.card-choice strong { display: block; font-size: 14px; text-transform: uppercase; line-height: 1.2; }
|
||||
.card-choice span { display: inline-block; margin: 8px 0; padding: 2px 7px; border: 2px solid var(--line); background: var(--white); font-size: 9px; letter-spacing: .1em; }
|
||||
.card-choice small { display: block; color: var(--muted); font-size: 11px; line-height: 1.35; }
|
||||
.card-choice.rare { background: #fff0d1; box-shadow: inset 0 0 0 3px #f0a020, 5px 5px 0 rgba(16,32,21,.16); }
|
||||
.card-choice.ultra-rare { background: #ffe6f0; box-shadow: inset 0 0 0 3px #c42424, 5px 5px 0 rgba(16,32,21,.18); }
|
||||
.card-choice.ultra-rare span { background: #c42424; color: #fff; }
|
||||
.muted-card-note { color: var(--muted); font-weight: 700; }
|
||||
.card-targets { display: grid; gap: 8px; margin-top: 14px; }
|
||||
.card-target-button { border: 3px solid var(--line); background: #f7fff5; padding: 10px 12px; text-align: left; cursor: pointer; font-weight: 900; box-shadow: 4px 4px 0 rgba(16,32,21,.14); }
|
||||
.card-target-button:hover { background: var(--green-soft); }
|
||||
|
||||
/* v19 readability pass */
|
||||
body { font-size: 16px; }
|
||||
.hud-card span { font-size: 11px; }
|
||||
.hud-card strong { font-size: clamp(18px, 1.5vw, 25px); }
|
||||
.priority-strip > div { font-size: 14px; }
|
||||
.panel-head h1 { font-size: 18px; }
|
||||
.panel-head p { font-size: 11px; }
|
||||
.build-panel h2 { font-size: 13px; }
|
||||
.tool-button strong { font-size: 14px; }
|
||||
.tool-button span { font-size: 11px; }
|
||||
.mini-box, .facility-panel-empty, .facility-card, .contract-card { font-size: 12px; }
|
||||
.facility-card h3 { font-size: 14px; }
|
||||
.sort-button { font-size: 14px; }
|
||||
.modal-card h2 { font-size: 29px; }
|
||||
.card-choice strong { font-size: 16px; }
|
||||
.card-choice small { font-size: 13px; }
|
||||
|
||||
.tool-button.unaffordable, .tool-button.already-built { opacity: .42; filter: grayscale(.7); }
|
||||
|
||||
.hover-tooltip {
|
||||
position: fixed;
|
||||
z-index: 30;
|
||||
width: min(320px, calc(100vw - 20px));
|
||||
display: none;
|
||||
border: 3px solid var(--line);
|
||||
background: rgba(255,255,255,.96);
|
||||
color: var(--ink);
|
||||
box-shadow: 6px 6px 0 rgba(16,32,21,.18);
|
||||
padding: 10px;
|
||||
pointer-events: none;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.hover-tooltip.visible { display: block; }
|
||||
.hover-tooltip strong { display: block; font-size: 15px; text-transform: uppercase; margin-bottom: 5px; }
|
||||
.hover-tooltip em { display: block; font-style: normal; color: var(--green); font-weight: 900; margin-bottom: 7px; }
|
||||
.hover-tooltip span { display: block; color: var(--muted); font-size: 12px; }
|
||||
|
||||
.debug-panel {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
z-index: 12;
|
||||
border: 3px solid var(--line);
|
||||
background: rgba(255,255,255,.94);
|
||||
box-shadow: 4px 4px 0 rgba(16,32,21,.16);
|
||||
max-width: 330px;
|
||||
}
|
||||
.debug-panel summary {
|
||||
cursor: pointer;
|
||||
padding: 5px 8px;
|
||||
font-weight: 900;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
.debug-body {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
padding: 8px;
|
||||
border-top: 3px solid var(--line);
|
||||
font-size: 12px;
|
||||
}
|
||||
.debug-body label { display: grid; gap: 3px; font-weight: 900; color: var(--muted); }
|
||||
.debug-body input, .debug-body select, .debug-body button {
|
||||
border: 2px solid var(--line);
|
||||
background: var(--white);
|
||||
color: var(--ink);
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
padding: 5px 6px;
|
||||
}
|
||||
.debug-body button { cursor: pointer; background: var(--green-soft); }
|
||||
|
||||
.card-choice.dud {
|
||||
background: #eeeeee;
|
||||
color: #777;
|
||||
filter: grayscale(1);
|
||||
border-style: dashed;
|
||||
box-shadow: 5px 5px 0 rgba(16,32,21,.10);
|
||||
}
|
||||
.card-choice.dud span { background: #f8f8f8; }
|
||||
.card-choice.flung {
|
||||
pointer-events: none;
|
||||
transform: translate(var(--fling-x, 480px), var(--fling-y, -320px)) rotate(var(--fling-r, 68deg)) scale(.68);
|
||||
opacity: 0;
|
||||
transition: transform .34s cubic-bezier(.18,.84,.26,.99), opacity .28s ease-in;
|
||||
}
|
||||
|
||||
/* v20 Phase 1 observability */
|
||||
.debug-readout {
|
||||
border: 2px solid var(--line);
|
||||
background: rgba(247,255,245,.92);
|
||||
padding: 6px;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.debug-readout strong {
|
||||
display: block;
|
||||
color: var(--ink);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
}
|
||||
.debug-readout span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.debug-readout.event-log {
|
||||
max-height: 160px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
.route-card .route-icon.male,
|
||||
.route-card .route-icon.female,
|
||||
.route-card .route-icon.poop {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 999px;
|
||||
border: 3px solid var(--line, #102015);
|
||||
background: var(--white, #fff);
|
||||
line-height: 1;
|
||||
}
|
||||
.route-card .route-icon.male { color: #2477ff; background: #d9ecff; }
|
||||
.route-card .route-icon.female { color: #d62a85; background: #ffe0f0; }
|
||||
.route-card .route-icon.poop { color: #7b4a23; background: #fff0d1; font-size: 24px; }
|
||||
.restart-button { min-width: 160px; }
|
||||
.restart-fallback-note { margin-top: 12px; color: var(--muted, #526456); font-weight: 700; }
|
||||
|
||||
/* v24.4 hover-only build-button flavor / cashout guidance / compact game-over */
|
||||
.tool-button .tool-flavor {
|
||||
display: block;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
margin-top: 0;
|
||||
color: var(--muted);
|
||||
font-size: 8px;
|
||||
line-height: 1.22;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
transition: max-height .12s ease, opacity .12s ease, margin-top .12s ease;
|
||||
}
|
||||
.tool-button:hover .tool-flavor {
|
||||
max-height: 42px;
|
||||
opacity: 1;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.large-tools .tool-button { min-height: 58px; }
|
||||
.large-tools .tool-button:hover { min-height: 78px; }
|
||||
.history-tools .tool-button.small { min-height: 40px; }
|
||||
.primary-button.cashout-emphasis,
|
||||
.tool-button.cashout-emphasis {
|
||||
opacity: 1 !important;
|
||||
filter: none !important;
|
||||
animation: cashoutPulse 0.9s ease-in-out infinite alternate;
|
||||
}
|
||||
.primary-button.cashout-emphasis {
|
||||
box-shadow: inset 0 0 0 4px #fff0d1, 6px 6px 0 rgba(16,32,21,.22);
|
||||
}
|
||||
.tool-button.cashout-emphasis {
|
||||
background: #fff0d1;
|
||||
box-shadow: inset 0 0 0 3px var(--danger), 6px 6px 0 rgba(16,32,21,.20);
|
||||
}
|
||||
@keyframes cashoutPulse {
|
||||
from { transform: translate(0, 0); }
|
||||
to { transform: translate(-1px, -1px); }
|
||||
}
|
||||
.modal.gameover-modal { padding: 12px; }
|
||||
.modal.gameover-modal .modal-card {
|
||||
width: min(640px, 92vw);
|
||||
max-height: min(82vh, 720px);
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
.modal.gameover-modal .modal-card h2 {
|
||||
font-size: 22px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.gameover-reason { font-weight: 900; color: var(--danger); margin-bottom: 8px; }
|
||||
.gameover-summary {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.gameover-breakdown {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.modal.gameover-modal .result-grid div { padding: 7px; }
|
||||
.modal.gameover-modal .result-grid strong { font-size: 9px; margin-bottom: 3px; }
|
||||
.modal.gameover-modal .result-grid span { font-size: 12px; }
|
||||
.gameover-details {
|
||||
margin-top: 10px;
|
||||
border: 3px solid var(--line);
|
||||
background: #f7fff5;
|
||||
padding: 8px;
|
||||
}
|
||||
.gameover-details summary {
|
||||
cursor: pointer;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.modal.gameover-modal .formula-box.compact { font-size: 9px; line-height: 1.35; }
|
||||
@media (max-width: 720px) {
|
||||
.gameover-summary, .gameover-breakdown { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
|
||||
/* v26.0 readability and stable build-menu tooltip pass */
|
||||
body { font-size: 18px; }
|
||||
.build-panel { width: clamp(330px, 26vw, 420px); }
|
||||
.panel-head h1 { font-size: 20px; }
|
||||
.panel-head h1 span { font-size: 12px; }
|
||||
.panel-head p { font-size: 13px; line-height: 1.35; }
|
||||
.build-panel h2 { font-size: 14px; }
|
||||
.priority-strip > div { font-size: 15px; }
|
||||
.hud-card span { font-size: 12px; }
|
||||
.hud-card strong { font-size: clamp(20px, 1.7vw, 28px); }
|
||||
.mini-box, .contract-card, .facility-panel-empty, .facility-card { font-size: 14px; line-height: 1.45; }
|
||||
.compact-rules .mini-box { font-size: 13px; }
|
||||
.tool-button {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
min-height: 62px;
|
||||
padding: 10px;
|
||||
}
|
||||
.large-tools .tool-button { min-height: 62px; }
|
||||
.large-tools .tool-button:hover { min-height: 62px; }
|
||||
.tool-button strong { font-size: 15px; line-height: 1.15; }
|
||||
.tool-button span { font-size: 12px; }
|
||||
.tool-button .tool-flavor {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 60;
|
||||
left: 0;
|
||||
top: calc(100% + 7px);
|
||||
width: min(300px, calc(100vw - 40px));
|
||||
max-height: none;
|
||||
opacity: 1;
|
||||
overflow: visible;
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border: 3px solid var(--line);
|
||||
background: rgba(255,255,255,.98);
|
||||
color: var(--ink);
|
||||
box-shadow: 5px 5px 0 rgba(16,32,21,.18);
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
pointer-events: none;
|
||||
transition: none;
|
||||
}
|
||||
.tool-button .tool-flavor::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: -10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-left: 3px solid var(--line);
|
||||
border-top: 3px solid var(--line);
|
||||
background: rgba(255,255,255,.98);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.tool-button:hover .tool-flavor {
|
||||
display: block;
|
||||
max-height: none;
|
||||
opacity: 1;
|
||||
margin: 0;
|
||||
}
|
||||
.primary-button { font-size: 16px; padding: 13px 16px; }
|
||||
#nextTurnButton { min-height: 56px; font-size: 18px; }
|
||||
@keyframes cashoutPulse {
|
||||
from { box-shadow: inset 0 0 0 3px var(--danger), 4px 4px 0 rgba(16,32,21,.18); filter: brightness(1); }
|
||||
to { box-shadow: inset 0 0 0 5px #fff0d1, 4px 4px 0 rgba(16,32,21,.20); filter: brightness(1.08); }
|
||||
}
|
||||
.card-choices { grid-template-columns: repeat(auto-fill, minmax(230px, 270px)); gap: 14px; }
|
||||
.card-choice { min-height: 164px; padding: 15px; }
|
||||
.card-choice strong { font-size: 18px; }
|
||||
.card-choice span { font-size: 11px; }
|
||||
.card-choice small { font-size: 15px; line-height: 1.4; }
|
||||
.muted-card-note { font-size: 15px; line-height: 1.45; }
|
||||
.modal-card { width: min(980px, 96vw); }
|
||||
.modal-card p, .modal-card li { font-size: 16px; }
|
||||
.modal-actions .reroll-button {
|
||||
order: 99;
|
||||
flex: 1 1 100%;
|
||||
min-height: 64px;
|
||||
margin-top: 8px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
background: #fff0d1;
|
||||
box-shadow: inset 0 0 0 4px var(--green), 6px 6px 0 rgba(16,32,21,.20);
|
||||
}
|
||||
.modal-actions .reroll-button:disabled { box-shadow: 4px 4px 0 rgba(16,32,21,.12); }
|
||||
.hover-tooltip strong { font-size: 17px; }
|
||||
.hover-tooltip span { font-size: 14px; }
|
||||
.sort-button { font-size: 16px; min-height: 56px; }
|
||||
.control-dock { grid-template-columns: repeat(4, 172px); }
|
||||
@media (max-width: 1180px) {
|
||||
.build-panel { width: auto; }
|
||||
.control-dock { grid-template-columns: repeat(2, 172px); }
|
||||
}
|
||||
|
||||
/* v27 combo / tags / settlement clarity */
|
||||
.card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.card-tags i {
|
||||
display: inline-block;
|
||||
border: 2px solid var(--line);
|
||||
background: rgba(255,255,255,.86);
|
||||
color: var(--ink);
|
||||
font-style: normal;
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
padding: 1px 6px;
|
||||
letter-spacing: .05em;
|
||||
}
|
||||
.settlement-hero {
|
||||
border: 4px solid var(--line);
|
||||
background: #f7fff5;
|
||||
padding: 14px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: 5px 5px 0 rgba(16,32,21,.14);
|
||||
}
|
||||
.settlement-hero.positive { box-shadow: inset 0 0 0 4px var(--green), 5px 5px 0 rgba(16,32,21,.14); }
|
||||
.settlement-hero.negative { box-shadow: inset 0 0 0 4px var(--danger), 5px 5px 0 rgba(16,32,21,.14); }
|
||||
.settlement-hero strong { display: block; font-size: 34px; line-height: 1; color: var(--green); }
|
||||
.settlement-hero.negative strong { color: var(--danger); }
|
||||
.settlement-hero span { display: block; margin-top: 8px; font-weight: 900; color: var(--muted); }
|
||||
.settlement-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.settlement-grid div {
|
||||
border: 3px solid var(--line);
|
||||
background: rgba(255,255,255,.9);
|
||||
padding: 9px;
|
||||
}
|
||||
.settlement-grid strong { display: block; font-size: 11px; text-transform: uppercase; color: var(--muted); }
|
||||
.settlement-grid span { display: block; margin-top: 5px; font-size: 20px; font-weight: 900; color: var(--ink); }
|
||||
.settlement-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border: 3px solid var(--line);
|
||||
background: #fff;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.settlement-table td {
|
||||
border-bottom: 2px solid rgba(16,32,21,.18);
|
||||
padding: 7px 9px;
|
||||
font-size: 15px;
|
||||
font-weight: 900;
|
||||
}
|
||||
.settlement-table td:last-child { text-align: right; }
|
||||
.settlement-table tr.income td:last-child { color: var(--green); }
|
||||
.settlement-table tr.cost td:last-child { color: var(--danger); }
|
||||
.settlement-notes {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
border: 3px solid var(--line);
|
||||
background: #f7fff5;
|
||||
padding: 10px;
|
||||
color: var(--muted);
|
||||
font-weight: 800;
|
||||
}
|
||||
.key-config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.key-config-grid label {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
font-weight: 900;
|
||||
color: var(--muted);
|
||||
}
|
||||
.key-config-grid input,
|
||||
.key-config-grid select {
|
||||
border: 3px solid var(--line);
|
||||
background: var(--white);
|
||||
color: var(--ink);
|
||||
padding: 7px 8px;
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
}
|
||||
.modal.rescue-modal .modal-card {
|
||||
width: min(560px, 92vw);
|
||||
}
|
||||
.hover-tooltip .cash-negative { color: var(--danger) !important; font-weight: 900; }
|
||||
@media (max-width: 720px) {
|
||||
.settlement-grid { grid-template-columns: 1fr 1fr; }
|
||||
.settlement-hero strong { font-size: 26px; }
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { getSpawnRange } from '../core/state.js';
|
|||
import { createEggFarm, createScanner, createFacility } from '../core/entities.js';
|
||||
import { key, parseKey, pointToCell, cellCenter, yen } from '../core/utils.js';
|
||||
import { TEXT, equipmentName } from '../core/text.js';
|
||||
import { farmAt, scannerAt, scannerCenter, routeFromFarmToScanner, refreshRoutingAfterEdit } 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 { autoScannerCooldownSeconds } from './cards.js';
|
||||
import { record } from './history.js';
|
||||
|
|
@ -88,10 +88,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
return '';
|
||||
}
|
||||
|
||||
function canSellHit(hit) {
|
||||
return !lastProtectedSaleReason(hit);
|
||||
}
|
||||
|
||||
function fail(reason) {
|
||||
ui.buildStatus.textContent = reason;
|
||||
floating(game, canvas.width / 2 - game.view.x, 86 - game.view.y, reason.toUpperCase(), THEME.danger);
|
||||
|
|
@ -144,6 +140,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
}
|
||||
|
||||
function buildScanner(col, row, kind) {
|
||||
if (kind === 'manual' && game.scanners.filter(s => s.kind === 'manual').length >= 8) return fail('Manual Scanner limit reached (8 max).');
|
||||
const cost = kind === 'auto' ? buildPrice('autoScanner', game) : buildPrice('manualScanner', game);
|
||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||
record(game);
|
||||
|
|
@ -281,11 +278,88 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
ui.modal.classList.add('visible', 'equipment-popover');
|
||||
}
|
||||
|
||||
function formatKeyBinding(binding) {
|
||||
return binding?.label || binding?.key || '?';
|
||||
}
|
||||
|
||||
function bindingFromEvent(event) {
|
||||
const label = event.key === ' ' ? 'Space' : event.key.length === 1 ? event.key.toUpperCase() : event.key.replace(/^Arrow/, '');
|
||||
return { key: event.key.toLowerCase(), code: event.code, label };
|
||||
}
|
||||
|
||||
function keyConflict(scanner, side, binding) {
|
||||
for (const s of game.scanners.filter(item => item.kind === 'manual')) {
|
||||
for (const candidateSide of ['left', 'right']) {
|
||||
if (s.id === scanner.id && candidateSide === side) continue;
|
||||
const existing = s.keys?.[candidateSide];
|
||||
if (existing && (existing.code === binding.code || existing.key === binding.key)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function showManualScannerMenu(scanner, message = '') {
|
||||
if (!scanner || scanner.kind !== 'manual') return;
|
||||
ui.modalTitle.textContent = `Manual Scanner #${scanner.id}`;
|
||||
const left = formatKeyBinding(scanner.keys?.left);
|
||||
const right = formatKeyBinding(scanner.keys?.right);
|
||||
const conflict = manualKeyConflicts(scanner);
|
||||
ui.modalBody.innerHTML = `
|
||||
<div class="equipment-menu-lines compact">
|
||||
<p>Left route key: <b>${left}</b></p>
|
||||
<p>Right route key: <b>${right}</b></p>
|
||||
<p>Role: ${scanner.role === 0 ? 'Male left / others right' : 'Poop left / others right'}</p>
|
||||
${message ? `<p class="cash-positive">${message}</p>` : ''}
|
||||
${conflict ? `<p class="cash-negative">${conflict}</p>` : ''}
|
||||
</div>`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
ui.modalActions.appendChild(modalButton('Set Left Key', () => captureManualKey(scanner, 'left'), 'facility-action warn'));
|
||||
ui.modalActions.appendChild(modalButton('Set Right Key', () => captureManualKey(scanner, 'right'), 'facility-action warn'));
|
||||
ui.modalActions.appendChild(modalButton('Close', hideModal, 'facility-action'));
|
||||
positionEquipmentPopover(scanner);
|
||||
ui.modal.classList.add('visible', 'equipment-popover');
|
||||
}
|
||||
|
||||
function manualKeyConflicts(scanner) {
|
||||
const own = [
|
||||
{ side: 'left', binding: scanner.keys?.left },
|
||||
{ side: 'right', binding: scanner.keys?.right }
|
||||
];
|
||||
if (own[0].binding && own[1].binding && (own[0].binding.code === own[1].binding.code || own[0].binding.key === own[1].binding.key)) return 'Left and right keys conflict.';
|
||||
for (const item of own) {
|
||||
if (!item.binding) continue;
|
||||
for (const other of game.scanners.filter(s => s.kind === 'manual' && s.id !== scanner.id)) {
|
||||
if (other.keys?.left && (other.keys.left.code === item.binding.code || other.keys.left.key === item.binding.key)) return `Key conflicts with Manual Scanner #${other.id} left.`;
|
||||
if (other.keys?.right && (other.keys.right.code === item.binding.code || other.keys.right.key === item.binding.key)) return `Key conflicts with Manual Scanner #${other.id} right.`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function captureManualKey(scanner, side) {
|
||||
ui.modalBody.innerHTML = `<div class="equipment-menu-lines compact"><p>Press a key for ${side.toUpperCase()} route.</p><p>Esc cancels.</p></div>`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
const onKey = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
window.removeEventListener('keydown', onKey, true);
|
||||
if (event.key === 'Escape') return showManualScannerMenu(scanner);
|
||||
const binding = bindingFromEvent(event);
|
||||
const conflict = keyConflict(scanner, side, binding);
|
||||
record(game);
|
||||
scanner.keys = scanner.keys || { left: null, right: null };
|
||||
scanner.keys[side] = binding;
|
||||
updatePanels();
|
||||
showManualScannerMenu(scanner, conflict ? `${binding.label} assigned, but another scanner already uses it.` : `${side.toUpperCase()} set to ${binding.label}.`);
|
||||
};
|
||||
window.addEventListener('keydown', onKey, true);
|
||||
}
|
||||
|
||||
function switchScannerRole() {
|
||||
const obj = selectedObject();
|
||||
if (!obj || obj.type !== 'scanner') return;
|
||||
if (obj.kind === 'auto') return showAutoScannerMenu(obj);
|
||||
updatePanels();
|
||||
return showManualScannerMenu(obj);
|
||||
}
|
||||
|
||||
function selectedPopoverWorldPoint(obj) {
|
||||
|
|
@ -318,6 +392,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
${['mixer', 'truck'].includes(obj.id) ? '<p class="formula-box compact">Income/Fine = ceil(base × 1.05^upgrades)</p>' : ''}`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
if (obj.type === 'scanner' && obj.kind === 'auto') ui.modalActions.appendChild(modalButton('Auto Menu', () => showAutoScannerMenu(obj), 'facility-action warn'));
|
||||
if (obj.type === 'scanner' && obj.kind === 'manual') ui.modalActions.appendChild(modalButton('Set Keys', () => showManualScannerMenu(obj), 'facility-action warn'));
|
||||
const hit = obj.type === 'conveyor' ? { type: 'conveyor', oldKey: obj.id, ref: obj } : { type: obj.type, ref: obj };
|
||||
const saleReason = lastProtectedSaleReason(hit);
|
||||
if (saleReason) {
|
||||
|
|
@ -410,6 +485,13 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
return lines;
|
||||
}
|
||||
|
||||
function disconnectedWarningFor(obj) {
|
||||
if (!obj || game.phase !== 'build') return '';
|
||||
const match = disconnectedBuildWarnings(game).find(item => item.type === obj.type && item.id === obj.id);
|
||||
if (!match) return '';
|
||||
return match.message || 'Not connected to conveyor route';
|
||||
}
|
||||
|
||||
|
||||
function flavorText(obj) {
|
||||
if (!obj) return '';
|
||||
|
|
@ -432,8 +514,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
updateGroupDrag: selection.updateGroupDrag,
|
||||
finishGroupDrag: selection.finishGroupDrag,
|
||||
selectedObject, selectedTitle, equipmentPrice, selectedUpgradeCost,
|
||||
selectedInfoLines, flavorText,
|
||||
removeSelected, switchScannerRole,
|
||||
selectedInfoLines, flavorText, disconnectedWarningFor,
|
||||
removeSelected, switchScannerRole, showManualScannerMenu,
|
||||
showAutoScannerMenu, showSelectedMenu, setBuildTool, fail,
|
||||
isEquipmentCell, equipmentHitBoxes,
|
||||
routeFromFarmToScanner
|
||||
|
|
|
|||
|
|
@ -199,6 +199,34 @@ export const CARD_DEFS = [
|
|||
}
|
||||
];
|
||||
|
||||
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) {
|
||||
const effects = ensureCardState(game);
|
||||
return Math.max(0, Number(effects[id]) || 0);
|
||||
|
|
@ -226,10 +254,6 @@ export function eggProductionDelayMultiplier(game) {
|
|||
return crowded * feed;
|
||||
}
|
||||
|
||||
export function poopRateCardMultiplier(game) {
|
||||
return Math.pow(1.03, effectCount(game, 'hatchingFeed'));
|
||||
}
|
||||
|
||||
export function extraEggOutletCount(game) {
|
||||
return Math.min(3, effectCount(game, 'extraEggOutlet'));
|
||||
}
|
||||
|
|
@ -247,13 +271,6 @@ export function autoScannerCooldownSeconds(scanner, game = null) {
|
|||
return game ? Math.max(AUTO_SCANNER_MIN_COOLDOWN, base * autoScannerDelayMultiplier(scanner)) : base;
|
||||
}
|
||||
|
||||
export function flatteryReductionForTurn(game, turn = game.turn) {
|
||||
const day = Math.max(1, Math.floor(Number(turn) || 1));
|
||||
const base = Math.ceil(day * 10);
|
||||
const charges = Math.max(0, ensureCardState(game).fairiesFlatteryNext || 0);
|
||||
return charges > 0 ? Math.ceil(base * (1 - Math.pow(0.5, charges))) : 0;
|
||||
}
|
||||
|
||||
export function rerollCost(game) {
|
||||
const draft = game.cardDraft || { rerolls: 0 };
|
||||
const nextRerollCount = Math.max(1, (draft.rerolls || 0) + 1);
|
||||
|
|
@ -480,13 +497,7 @@ function applyInstantCard(game, card) {
|
|||
if (card.id === 'laborExploitation') effects.laborExploitation = 1;
|
||||
if (card.id === 'rescueLoan') {
|
||||
inc('rescueLoanCharges');
|
||||
if (game.cash < 0) {
|
||||
effects.rescueLoanCharges = Math.max(0, (effects.rescueLoanCharges || 0) - 1);
|
||||
applyRevenue(game, 500);
|
||||
floating(game, GRID.x + GRID.cols * GRID.cell / 2, GRID.y - 30, '+¥500 RESCUE', THEME.green);
|
||||
} else {
|
||||
floating(game, GRID.x + GRID.cols * GRID.cell / 2, GRID.y - 30, 'RESCUE HELD', THEME.green);
|
||||
}
|
||||
floating(game, GRID.x + GRID.cols * GRID.cell / 2, GRID.y - 30, 'RESCUE HELD', THEME.green);
|
||||
}
|
||||
if (card.id === 'loan') {
|
||||
const day = Math.max(1, Math.floor(Number(game.turn) || 1));
|
||||
|
|
@ -497,11 +508,6 @@ function applyInstantCard(game, card) {
|
|||
}
|
||||
}
|
||||
|
||||
function formatMultiplier(value) {
|
||||
const rounded = Math.round(Number(value || 0) * 100) / 100;
|
||||
return `x${rounded.toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1')}`;
|
||||
}
|
||||
|
||||
function formatPercent(value) {
|
||||
return `${Math.round(Number(value || 0) * 100)}%`;
|
||||
}
|
||||
|
|
@ -732,7 +738,8 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
const kind = card.type === 'dud' ? 'dud' : (card.rarity === 'ultraRare' ? 'ultra-rare' : (card.rarity === 'rare' ? 'rare' : 'common'));
|
||||
b.className = `card-choice ${kind}`;
|
||||
const rarity = card.type === 'dud' ? 'MISS' : (card.rarity === 'ultraRare' ? 'ULTRA RARE' : (card.rarity === 'rare' ? 'RARE' : 'COMMON'));
|
||||
b.innerHTML = `<strong>${card.title}</strong><span>${rarity}</span><small>${cardDescription(game, card)}</small>`;
|
||||
const tags = [rarity, ...(card.tags || []).filter(tag => tag !== rarity)].slice(0, 5);
|
||||
b.innerHTML = `<strong>${card.title}</strong><div class="card-tags">${tags.map(tag => `<span>${tag}</span>`).join('')}</div><small>${cardDescription(game, card)}</small>`;
|
||||
b.addEventListener('click', () => chooseCard(card, b));
|
||||
b.dataset.index = String(index);
|
||||
return b;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { createChick } from '../core/entities.js';
|
|||
import { nextSpawnDelay } from '../core/state.js';
|
||||
import { key, parseKey, pointToCell, cellCenter, randomBetween, yen } from '../core/utils.js';
|
||||
import { scannerById, scannerBySlot, scannerCenter, routeFromFarmToScanner, outputRoute, destinationLabel, destinationColor, nearestConveyorKey, buildConveyorComponents, autoSideFor } from './routing.js';
|
||||
import { applyMixerIncome, applyMixerPoopFine, applyTruckPoopFine, applyWrongTruckFine, upgradedTruckPrice, applyShredderBonus } from './economy.js';
|
||||
import { applyMixerIncome, applyMixerPoopFine, applyTruckPoopFine, applyWrongTruckFine, upgradedTruckPrice, applyShredderBonus, applyRevenue } from './economy.js';
|
||||
import { autoScannerCooldownSeconds, eggProductionDelayMultiplier, extraEggOutletCount } from './cards.js';
|
||||
import { productionMultiplier, truckTarget, isTargetTruckCargo, shouldFineMaleTruck } from './contracts.js';
|
||||
import { floating, shake, spawnPulse, scannerPulse, meatEffect, sludgeEffect, shredEffect, truckLoadEffect, shockwave, sparkBurst, smokeBurst, flyingDebris, rageEffect } from './effects.js';
|
||||
|
|
@ -284,7 +284,11 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
function sortSlot(slot, side) {
|
||||
if (game.phase !== 'running') return;
|
||||
const scanner = scannerBySlot(game, slot);
|
||||
if (!scanner) return;
|
||||
sortScanner(scanner, side);
|
||||
}
|
||||
|
||||
function sortScanner(scanner, side) {
|
||||
if (game.phase !== 'running' || !scanner || scanner.kind !== 'manual') return;
|
||||
scanner.keyPressTime = 0.18;
|
||||
scanner.keyPressSide = side;
|
||||
if (!scanner.queue.length) return;
|
||||
|
|
@ -292,6 +296,51 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
if (index >= 0) sortChickByIndex(index, side, false);
|
||||
}
|
||||
|
||||
function manualScanSucceeded(scanner, chick, side) {
|
||||
return scanner.kind === 'manual' && autoSideFor(scanner, chick) === side;
|
||||
}
|
||||
|
||||
function resetManualCombo(scanner) {
|
||||
const previous = game.manualCombo?.count || 0;
|
||||
if (!game.manualCombo) game.manualCombo = { count: 0, lastBonus: 0 };
|
||||
game.manualCombo.count = 0;
|
||||
game.manualCombo.lastBonus = 0;
|
||||
game.stats.manualComboFailure = (game.stats.manualComboFailure || 0) + 1;
|
||||
game.totals.manualComboFailure = (game.totals.manualComboFailure || 0) + 1;
|
||||
if (previous >= 10) {
|
||||
const c = scannerCenter(scanner);
|
||||
shake(game, 10, 0.25);
|
||||
floating(game, c.x, c.y - 36, 'COMBO RESET', THEME.danger);
|
||||
}
|
||||
}
|
||||
|
||||
function awardManualCombo(scanner) {
|
||||
if (!game.manualCombo) game.manualCombo = { count: 0, lastBonus: 0 };
|
||||
game.manualCombo.count = Math.max(0, game.manualCombo.count || 0) + 1;
|
||||
game.stats.manualComboSuccess = (game.stats.manualComboSuccess || 0) + 1;
|
||||
game.totals.manualComboSuccess = (game.totals.manualComboSuccess || 0) + 1;
|
||||
const combo = game.manualCombo.count;
|
||||
game.manualCombo.lastBonus = 0;
|
||||
if (combo < 10) return 0;
|
||||
const trials = Math.max(1, Math.floor(combo / 10));
|
||||
const manualScannerCount = game.scanners.filter(s => s.kind === 'manual').length;
|
||||
const chance = Math.min(1, (20 + manualScannerCount * 10) / 100);
|
||||
let successfulTrials = 0;
|
||||
for (let i = 0; i < trials; i += 1) {
|
||||
if (Math.random() < chance) successfulTrials += 1;
|
||||
}
|
||||
const bonus = Math.floor(successfulTrials * Math.max(1, game.turn || 1) * 5 / 4);
|
||||
if (bonus > 0) {
|
||||
applyRevenue(game, bonus);
|
||||
game.stats.manualComboBonus = (game.stats.manualComboBonus || 0) + bonus;
|
||||
game.totals.manualComboBonus = (game.totals.manualComboBonus || 0) + bonus;
|
||||
game.manualCombo.lastBonus = bonus;
|
||||
const c = scannerCenter(scanner);
|
||||
floating(game, c.x, c.y - 42, `COMBO +${yen(bonus)}`, THEME.green);
|
||||
}
|
||||
return bonus;
|
||||
}
|
||||
|
||||
function sortChickByIndex(index, side, auto) {
|
||||
const chick = game.chicks[index];
|
||||
if (!chick || chick.stage !== 'queued') return false;
|
||||
|
|
@ -299,9 +348,14 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
if (!scanner) return false;
|
||||
const plan = outputRoute(game, side, { x: chick.x, y: chick.y }, scanner.id, true);
|
||||
if (!plan) {
|
||||
if (!auto && scanner.kind === 'manual') resetManualCombo(scanner);
|
||||
floating(game, chick.x, chick.y - 16, side === 'left' ? 'NO LEFT BELT' : 'NO RIGHT BELT', THEME.danger);
|
||||
return false;
|
||||
}
|
||||
if (!auto && scanner.kind === 'manual') {
|
||||
if (manualScanSucceeded(scanner, chick, side)) awardManualCombo(scanner);
|
||||
else resetManualCombo(scanner);
|
||||
}
|
||||
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.route = plan.route;
|
||||
|
|
@ -502,25 +556,12 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
floating(game, fc.x, fc.y - 22, '100% JAM EXPLOSION', THEME.danger);
|
||||
}
|
||||
|
||||
function explodeRoute(route, extras, label) {
|
||||
const victims = game.chicks.filter(ch => route.some(p => Math.hypot(ch.x - p.x, ch.y - p.y) < GRID.cell));
|
||||
game.chicks = game.chicks.filter(ch => !victims.some(v => v.id === ch.id));
|
||||
for (const v of victims) flyingDebris(game, v.sex, v.x, v.y);
|
||||
for (const e of extras) flyingDebris(game, e.sex, e.x, e.y);
|
||||
for (const p of route) {
|
||||
shockwave(game, p.x, p.y, THEME.danger);
|
||||
sparkBurst(game, p.x, p.y, 4);
|
||||
smokeBurst(game, p.x, p.y, 3);
|
||||
}
|
||||
shake(game, 18, .75);
|
||||
if (route[0]) floating(game, route[0].x, route[0].y - 18, label, THEME.danger);
|
||||
}
|
||||
|
||||
return {
|
||||
activeQueuedChick,
|
||||
currentSpawnDelay,
|
||||
sortSlot,
|
||||
updateCongestion,
|
||||
sortScanner,
|
||||
updateRunning
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { TURN_SECONDS, POOP_RATE, CONTRACT_EVENT_CHANCE, CONTRACT_EVENT_FIRST_TU
|
|||
import { getSpawnRange } from '../core/state.js';
|
||||
import { applyRevenue } from './economy.js';
|
||||
|
||||
export const CONTRACT_TARGETS = [
|
||||
const CONTRACT_TARGETS = [
|
||||
{
|
||||
target: 'female',
|
||||
title: 'Premium Female Shipment',
|
||||
|
|
@ -107,10 +107,6 @@ export function isTargetTruckCargo(game, sex) {
|
|||
return truckTarget(game) === sex;
|
||||
}
|
||||
|
||||
export function isPoopSoilActive(game) {
|
||||
return truckTarget(game) !== 'poop';
|
||||
}
|
||||
|
||||
export function shouldFineMaleTruck(game) {
|
||||
return truckTarget(game) !== 'male';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,10 +82,6 @@ export function shredderBonusChance(gameOrCount) {
|
|||
return Math.min(1, percent / 100);
|
||||
}
|
||||
|
||||
export function shredderBonusExpectedPerItem(gameOrCount) {
|
||||
return shredderBonusChance(gameOrCount);
|
||||
}
|
||||
|
||||
export function rollShredderBonus(game) {
|
||||
const cards = shredderUpgradeCount(game);
|
||||
const chance = shredderBonusChance(cards);
|
||||
|
|
@ -117,10 +113,6 @@ export function zundaTaxInfo(cash, game = null) {
|
|||
return { profit, exemption, maxRateCash, taxable, rate, ratePercent: Math.round(rate * 100), tax, step, legalWork };
|
||||
}
|
||||
|
||||
export function zundaTaxForCash(cash, game = null) {
|
||||
return zundaTaxInfo(cash, game).tax;
|
||||
}
|
||||
|
||||
export function fairiesTributeInfo(game, turn = game.turn) {
|
||||
const day = Math.max(1, Math.floor(Number(turn) || 1));
|
||||
const base = Math.ceil(day * ECONOMY.fairiesTribute.perDay);
|
||||
|
|
@ -146,18 +138,7 @@ export function explosionDamageForPrice(price, game = null) {
|
|||
return Math.max(1, Math.ceil(base * explosionDamageMultiplier(game)));
|
||||
}
|
||||
|
||||
export function upgradeCostFor(obj) {
|
||||
if (!obj) return null;
|
||||
if (obj.type === 'eggFarm') return FACILITY_DEFS.eggFarm.upgradeCosts[obj.level] || null;
|
||||
if (obj.type === 'facility' && INCOME_FACILITY_IDS.includes(obj.id)) {
|
||||
const level = obj.level || 1;
|
||||
if (level <= 1) return 300;
|
||||
return Math.ceil(900 * Math.pow(1.65, level - 2));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function positivePayout(_game, amount) {
|
||||
function positivePayout(_game, amount) {
|
||||
return Math.max(0, Math.ceil(amount));
|
||||
}
|
||||
|
||||
|
|
@ -243,6 +224,8 @@ export function settleTruckRevenue(game) {
|
|||
const adjusted = positivePayout(game, base);
|
||||
game.stats.pendingTruckRevenue = adjusted;
|
||||
game.stats.truckRevenue = adjusted;
|
||||
if (target === 'poop') game.stats.poopShipmentIncome = adjusted;
|
||||
else game.stats.chickShipmentIncome = adjusted;
|
||||
if (adjusted > 0) applyRevenue(game, adjusted);
|
||||
return { base, adjusted, target, targetCount, unitPrice };
|
||||
}
|
||||
|
|
@ -269,21 +252,6 @@ export function collectFairiesTribute(game, turn = game.turn) {
|
|||
return info;
|
||||
}
|
||||
|
||||
export function applyRescueLoanIfNeeded(game) {
|
||||
if (!game?.cardEffects || game.cash >= 0) return { amount: 0, used: 0, remaining: Math.max(0, game?.cardEffects?.rescueLoanCharges || 0) };
|
||||
let used = 0;
|
||||
let amount = 0;
|
||||
while (game.cash < 0 && (game.cardEffects.rescueLoanCharges || 0) > 0) {
|
||||
game.cardEffects.rescueLoanCharges -= 1;
|
||||
used += 1;
|
||||
amount += 500;
|
||||
game.stats.rescueLoan = (game.stats.rescueLoan || 0) + 500;
|
||||
game.totals.rescueLoan = (game.totals.rescueLoan || 0) + 500;
|
||||
applyRevenue(game, 500);
|
||||
}
|
||||
return { amount, used, remaining: Math.max(0, game.cardEffects.rescueLoanCharges || 0) };
|
||||
}
|
||||
|
||||
export function collectChemicalWeaponSubsidy(game) {
|
||||
const cards = effectCount(game, 'chemicalWeaponSubsidy');
|
||||
const poopTruck = Math.max(0, game?.stats?.poopTruck || 0);
|
||||
|
|
@ -337,6 +305,3 @@ export function finalScore(game) {
|
|||
));
|
||||
return { score, factoryValue: value, base, dailyEarned, dayPenalty, correctBonus, explosionPenalty, days };
|
||||
}
|
||||
|
||||
export const MIXER_PRICE = ECONOMY.income.mixer;
|
||||
export const TRUCK_PRICE = ECONOMY.income.truck;
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
const MAX_EVENTS = 160;
|
||||
|
||||
export function emitGameEvent(game, type, payload = {}) {
|
||||
if (!game) return null;
|
||||
if (!Array.isArray(game.eventLog)) game.eventLog = [];
|
||||
const id = game.nextEventId || 1;
|
||||
game.nextEventId = id + 1;
|
||||
const event = {
|
||||
id,
|
||||
at: Date.now(),
|
||||
day: game.turn || 1,
|
||||
phase: game.phase || 'unknown',
|
||||
type,
|
||||
payload: sanitizePayload(payload)
|
||||
};
|
||||
game.eventLog.push(event);
|
||||
if (game.eventLog.length > MAX_EVENTS) game.eventLog.splice(0, game.eventLog.length - MAX_EVENTS);
|
||||
return event;
|
||||
}
|
||||
|
||||
export function recentGameEvents(game, limit = 8) {
|
||||
const events = Array.isArray(game?.eventLog) ? game.eventLog : [];
|
||||
return events.slice(Math.max(0, events.length - limit)).reverse();
|
||||
}
|
||||
|
||||
export function clearGameEvents(game) {
|
||||
if (game) game.eventLog = [];
|
||||
}
|
||||
|
||||
function sanitizePayload(payload) {
|
||||
if (!payload || typeof payload !== 'object') return payload;
|
||||
const out = {};
|
||||
for (const [k, v] of Object.entries(payload)) {
|
||||
if (typeof v === 'number' || typeof v === 'string' || typeof v === 'boolean' || v == null) out[k] = v;
|
||||
else if (Array.isArray(v)) out[k] = v.slice(0, 12).map(x => typeof x === 'object' ? JSON.stringify(x).slice(0, 80) : x);
|
||||
else out[k] = JSON.stringify(v).slice(0, 120);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
export function cleanScanner(scanner) {
|
||||
return { ...scanner, queue: [], cooldown: scanner.cooldown || 0 };
|
||||
return { ...scanner, keys: scanner.keys ? { left: { ...scanner.keys.left }, right: { ...scanner.keys.right } } : null, queue: [], cooldown: scanner.cooldown || 0 };
|
||||
}
|
||||
export function snapshot(game) {
|
||||
return JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -101,10 +101,6 @@ export function performanceFactor(target) {
|
|||
return Math.max(RULES.minimumPerformance, 1 - t * (1 - RULES.minimumPerformance));
|
||||
}
|
||||
|
||||
export function degradationPercent(target) {
|
||||
return Math.round(wearRatio(target) * 100);
|
||||
}
|
||||
|
||||
export function conveyorTarget(game, conveyorKey) {
|
||||
const meta = game.conveyorMeta?.get(conveyorKey);
|
||||
return meta ? { type: 'conveyor', key: conveyorKey, meta } : null;
|
||||
|
|
@ -221,6 +217,8 @@ export function hireRepairmanForNextDay(game) {
|
|||
const cost = repairmanDailyCost(game);
|
||||
if (game.cash < cost) return { ok: false, reason: `Need ${yen(cost - game.cash)} more.` };
|
||||
spendCash(game, cost);
|
||||
game.pendingRepairWorkerWages = (game.pendingRepairWorkerWages || 0) + cost;
|
||||
game.totals.repairWorkerWages = (game.totals.repairWorkerWages || 0) + cost;
|
||||
game.repairman.hiredForNextDay = true;
|
||||
return { ok: true, cost };
|
||||
}
|
||||
|
|
@ -285,7 +283,7 @@ export function updateRepairman(game, dt) {
|
|||
if (targetWear(target) <= 0.001) r.target = null;
|
||||
}
|
||||
|
||||
export function worstDegradation(game) {
|
||||
function worstDegradation(game) {
|
||||
let worst = 0;
|
||||
let label = 'none';
|
||||
for (const t of equipmentMaintenanceTargets(game)) {
|
||||
|
|
|
|||
|
|
@ -29,14 +29,6 @@ function adjacentCells(point) {
|
|||
return DIRS.map(d => ({ col: point.col + d.dc, row: point.row + d.dr })).filter(p => inGrid(p.col, p.row));
|
||||
}
|
||||
|
||||
function conveyorAt(game, point) {
|
||||
return !!point && inGrid(point.col, point.row) && game.conveyorTiles.has(key(point.col, point.row));
|
||||
}
|
||||
|
||||
function exactConveyorCell(game, point) {
|
||||
return conveyorAt(game, point) ? [{ ...point }] : [];
|
||||
}
|
||||
|
||||
function visualPortConveyorCells(game, point, blocked = [], preferred = []) {
|
||||
if (!point || !inGrid(point.col, point.row)) return [];
|
||||
const result = [];
|
||||
|
|
@ -57,21 +49,6 @@ function graphPortConveyorCells(game, point, blocked = [], preferred = []) {
|
|||
return exact.length ? exact : cells;
|
||||
}
|
||||
|
||||
// Kept for explicit non-port use.
|
||||
export function exactOrAdjacentConveyorCells(game, point, blocked = [], preferred = []) {
|
||||
if (!point || !inGrid(point.col, point.row)) return [];
|
||||
const result = [];
|
||||
const add = p => {
|
||||
if (!p || !inGrid(p.col, p.row) || !game.conveyorTiles.has(key(p.col, p.row))) return;
|
||||
if (blocked.some(b => b && sameCell(p, b))) return;
|
||||
if (!result.some(x => sameCell(x, p))) result.push({ ...p, viaTolerance: !sameCell(p, point) });
|
||||
};
|
||||
add(point);
|
||||
for (const p of preferred) add(p);
|
||||
for (const p of adjacentCells(point)) add(p);
|
||||
return result;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Strict connection tests
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -95,34 +72,11 @@ export function facilityEntryPoint(game, dest) {
|
|||
return { x: f.x + f.w / 2, y: f.y + f.h / 2 };
|
||||
}
|
||||
|
||||
export function facilityEntryCell(game, dest) {
|
||||
return game.facilities[dest]?.entry || null;
|
||||
}
|
||||
|
||||
export function facilityEndpointCells(game, dest) {
|
||||
const f = game.facilities[dest];
|
||||
if (!f?.entry) return [];
|
||||
return visualPortConveyorCells(game, f.entry);
|
||||
}
|
||||
|
||||
export function portHasConveyor(game, port) {
|
||||
return visualPortConveyorCells(game, port).length > 0;
|
||||
}
|
||||
|
||||
export function scannerPortHasConveyor(game, scanner, type) {
|
||||
if (type === 'inputA') return scannerInputCells(game, scanner).length > 0;
|
||||
return outputStartCells(game, scanner, type).length > 0;
|
||||
}
|
||||
|
||||
export function getInputScanner(game, col, row) {
|
||||
return game.scanners.find(scanner => scannerInputCells(game, scanner).some(p => sameCell({ col, row }, p))) || null;
|
||||
}
|
||||
export function isInputConnector(game, col, row) { return !!getInputScanner(game, col, row); }
|
||||
export function isOutputConnector(game, col, row) {
|
||||
return game.scanners.some(scanner => sameCell({ col, row }, scannerConnector(scanner, 'left')) || sameCell({ col, row }, scannerConnector(scanner, 'right')));
|
||||
}
|
||||
export function inputCellsForScanner(game, scanner) { return scannerInputCells(game, scanner); }
|
||||
|
||||
export function refreshRoutingAfterEdit(game) {
|
||||
game.branchCounters?.clear?.();
|
||||
game.routeCache = null;
|
||||
|
|
@ -271,7 +225,7 @@ export function commitFactoryGraphForDay(game) {
|
|||
return graph;
|
||||
}
|
||||
|
||||
export function graphSummary(graph) {
|
||||
function graphSummary(graph) {
|
||||
if (!graph) return null;
|
||||
return {
|
||||
version: graph.version,
|
||||
|
|
@ -296,11 +250,6 @@ export function getConveyorNeighbors(game, col, row) {
|
|||
return (graph.adjacency.get(k) || []).map(n => ({ ...graphCell(n.key), dir: DIRS.find(d => d.name === n.dir) || { name: n.dir, dc: 0, dr: 0 } }));
|
||||
}
|
||||
|
||||
export function getAdjacentConveyors(game, col, row) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
return adjacentCells({ col, row }).filter(p => graph.cells.has(graphNodeKey(p.col, p.row)));
|
||||
}
|
||||
|
||||
function dirBetween(a, b) {
|
||||
const dc = b.col - a.col, dr = b.row - a.row;
|
||||
return DIRS.find(d => d.dc === dc && d.dr === dr)?.name || null;
|
||||
|
|
@ -453,13 +402,6 @@ export function destinationColor(dest) {
|
|||
return { mixer: '#2477ff', truck: '#ff6aa8', trash: '#22b94f', 'scanner-role-1': '#526456', scanner: '#526456', input: '#526456' }[dest] || '#102015';
|
||||
}
|
||||
|
||||
export function isFacilityEndpoint(game, p, dest, connector) {
|
||||
if (sameCell(p, connector)) return false;
|
||||
const graph = ensureFactoryGraph(game);
|
||||
const port = graph.facilityPorts.get(dest);
|
||||
return !!port?.connected && (port.keys || [port.key]).includes(graphNodeKey(p.col, p.row));
|
||||
}
|
||||
|
||||
export function routeFromFarmToScanner(game, farm, advance = false) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
const starts = (graph.farmOutputs.get(farm.id) || []).map(graphCell);
|
||||
|
|
@ -552,6 +494,44 @@ function outputRouteReachesFacility(game, scanner, visited = new Set()) {
|
|||
return false;
|
||||
}
|
||||
|
||||
function scannerOutputReachesDestination(game, scanner, dest, visited = new Set()) {
|
||||
if (!scanner || visited.has(scanner.id)) return false;
|
||||
visited.add(scanner.id);
|
||||
for (const side of ['left', 'right']) {
|
||||
const plan = outputRoute(game, side, scannerCenter(scanner), scanner.id, false);
|
||||
if (!plan) continue;
|
||||
if (plan.destination === dest) return true;
|
||||
if (plan.destination === 'scanner') {
|
||||
const next = scannerById(game, plan.nextScannerId);
|
||||
if (scannerOutputReachesDestination(game, next, dest, visited)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function eggFarmHasValidRoute(game, farm) {
|
||||
return !!routeFromFarmToScanner(game, farm, false)?.scannerId;
|
||||
}
|
||||
|
||||
export function facilityHasValidRoute(game, dest) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
if (!graph.facilityPorts.get(dest)?.connected) return false;
|
||||
return game.scanners.some(scanner => scannerOutputReachesDestination(game, scanner, dest));
|
||||
}
|
||||
|
||||
export function disconnectedBuildWarnings(game) {
|
||||
if (game.phase !== 'build') return [];
|
||||
const warnings = [];
|
||||
for (const farm of game.eggFarms || []) {
|
||||
if (!eggFarmHasValidRoute(game, farm)) warnings.push({ type: 'eggFarm', id: farm.id, ref: farm, message: 'This Egg will not produce chicks' });
|
||||
}
|
||||
for (const id of MACHINE_FACILITY_IDS) {
|
||||
const facility = game.facilities?.[id];
|
||||
if (facility && !facilityHasValidRoute(game, id)) warnings.push({ type: 'facility', id, ref: facility, message: 'This facility cannot receive items' });
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
||||
export function minimumStartConnectionIssues(game) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
for (const farm of game.eggFarms || []) {
|
||||
|
|
|
|||
|
|
@ -45,14 +45,17 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
const priceSpan = btn.querySelector('span');
|
||||
if (priceSpan) priceSpan.textContent = id === 'conveyor' ? `${yen(price)} / tile` : yen(price);
|
||||
const uniqueAlreadyBuilt = MACHINE_FACILITY_IDS.includes(id) && !!game.facilities[id];
|
||||
const manualLimitReached = id === 'manualScanner' && game.scanners.filter(s => s.kind === 'manual').length >= 8;
|
||||
const unaffordable = game.cash < price;
|
||||
btn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || unaffordable || uniqueAlreadyBuilt;
|
||||
btn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || unaffordable || uniqueAlreadyBuilt || manualLimitReached;
|
||||
if (btn.disabled && game.buildTool === id) game.buildTool = null;
|
||||
btn.classList.toggle('unaffordable', unaffordable);
|
||||
btn.classList.toggle('already-built', uniqueAlreadyBuilt);
|
||||
btn.classList.toggle('already-built', uniqueAlreadyBuilt || manualLimitReached);
|
||||
btn.title = unaffordable
|
||||
? `Need ${yen(price - game.cash)} more`
|
||||
: uniqueAlreadyBuilt
|
||||
: manualLimitReached
|
||||
? 'Manual Scanner limit reached (8 max)'
|
||||
: uniqueAlreadyBuilt
|
||||
? `${FACILITY_DEFS[id]?.name || id} already exists`
|
||||
: '';
|
||||
}
|
||||
|
|
@ -104,7 +107,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
? `Fairies tribute paid before Build: <b>-${yen(lastTribute)}</b>${lastTributeReduction ? ` | Flattery -${yen(lastTributeReduction)}` : ''}`
|
||||
: `Fairies tribute before Build: <b>-${yen(tribute.amount)}</b>${tribute.reduction ? ` | Flattery -${yen(tribute.reduction)}` : ''}`,
|
||||
`Maintenance: <b>worst ${maintenanceSummary(game).worst.percent}% dirty (${maintenanceSummary(game).worst.label})</b> | Repairman: <b>${game.repairman?.hiredForNextDay ? 'hired next day' : game.repairman?.active ? 'working' : 'none'}</b>`,
|
||||
`Card draft: <b>${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'}</b> | Belt: <b>${displaySpeed()}px/s</b> | Farms with scanner route: <b>${connected}/${game.eggFarms.length}</b>`,
|
||||
`Manual combo: <b>${game.manualCombo?.count || 0}</b> | Card draft: <b>${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'}</b> | Belt: <b>${displaySpeed()}px/s</b> | Farms with scanner route: <b>${connected}/${game.eggFarms.length}</b>`,
|
||||
game.cardEffects?.usedMachineActive ? '<span class="cash-negative">Procurement: USED MACHINE MODE / no refunds / 60% durability</span>' : '',
|
||||
issues.length ? `<span class="cash-negative">Blocked: ${issues[0]}</span>` : `<span class="cash-positive">${TEXT.status.allPortsConnected}</span>`
|
||||
].join('<br>');
|
||||
|
|
@ -143,6 +146,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
ui.shell.classList.toggle('phase-build', game.phase === 'build');
|
||||
ui.shell.classList.toggle('phase-gameover', game.phase === 'gameover');
|
||||
ui.shell.classList.toggle('phase-title', game.phase === 'title');
|
||||
ui.shell.classList.toggle('phase-loanDecision', game.phase === 'loanDecision');
|
||||
ui.money.textContent = yen(game.cash);
|
||||
ui.money.classList.toggle('cash-negative', game.cash < 0);
|
||||
ui.money.classList.toggle('cash-positive', game.cash > STARTING_CASH);
|
||||
|
|
@ -164,6 +168,16 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
ui.buttons.s1Right.disabled = game.phase !== 'running' || !activeQueuedChick(0);
|
||||
ui.buttons.s2Left.disabled = game.phase !== 'running' || !activeQueuedChick(1);
|
||||
ui.buttons.s2Right.disabled = game.phase !== 'running' || !activeQueuedChick(1);
|
||||
const s1 = game.scanners.find(s => s.kind === 'manual' && s.slot === 0);
|
||||
const s2 = game.scanners.find(s => s.kind === 'manual' && s.slot === 1);
|
||||
if (s1) {
|
||||
ui.buttons.s1Left.textContent = `S1 ${s1.keys?.left?.label || 'A'} -> Mixer`;
|
||||
ui.buttons.s1Right.textContent = `S1 ${s1.keys?.right?.label || 'D'} -> S2`;
|
||||
}
|
||||
if (s2) {
|
||||
ui.buttons.s2Left.textContent = `S2 ${s2.keys?.left?.label || 'Left'} -> Waste`;
|
||||
ui.buttons.s2Right.textContent = `S2 ${s2.keys?.right?.label || 'Right'} -> Truck`;
|
||||
}
|
||||
const nextTax = zundaTaxInfo(game.cash, game).tax;
|
||||
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);
|
||||
|
|
@ -173,7 +187,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
if (!ui.eventBrief || !ui.targetBrief || !ui.speedBrief) return;
|
||||
const target = truckTarget(game).toUpperCase();
|
||||
ui.targetBrief.textContent = `TRUCK TARGET: ${target}`;
|
||||
ui.speedBrief.textContent = `BELT: ${displaySpeed()} px/s`;
|
||||
ui.speedBrief.textContent = `BELT: ${displaySpeed()} px/s | COMBO: ${game.manualCombo?.count || 0}`;
|
||||
ui.speedBrief.title = `Conveyor speed does not increase by day. Degraded belts reduce speed. Cap: ${CONVEYOR_SPEED_MAX}px/s.`;
|
||||
const active = game.contractActive;
|
||||
const offer = game.contractOffer;
|
||||
|
|
@ -194,6 +208,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
|
||||
function phaseLabel() {
|
||||
if (game.phase === 'running') return game.timeLeft <= 0 ? TEXT.phases.clearing : TEXT.phases.running;
|
||||
if (game.phase === 'loanDecision') return TEXT.phases.loanDecision;
|
||||
if (game.phase === 'build') return TEXT.phases.build;
|
||||
if (game.phase === 'gameover') return TEXT.phases.gameover;
|
||||
return TEXT.phases.title;
|
||||
|
|
@ -209,16 +224,50 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
}
|
||||
|
||||
function showTurnResult(r) {
|
||||
const positive = value => `<span class="${value > 0 ? 'cash-positive' : ''}">${value > 0 ? '+' : ''}${yen(value)}</span>`;
|
||||
const negative = value => `<span class="${value > 0 ? 'cash-negative' : ''}">${value > 0 ? '-' : ''}${yen(value)}</span>`;
|
||||
const netClass = r.profit >= 0 ? 'cash-positive' : 'cash-negative';
|
||||
const penalties = (r.mixerPoopFine || 0) + (r.truckPoopFine || 0) + (r.maleTruckFine || 0) + (r.explosionDamage || 0) + (r.cardRerollCost || 0);
|
||||
ui.modalTitle.textContent = `Day ${r.turn} Settlement`;
|
||||
ui.modalBody.innerHTML = `
|
||||
<p class="formula-box compact">${yen(r.revenue)} - ${yen(r.penalty)} = ${yen(r.profit)}</p>
|
||||
<p><strong>Final profit: ${yen(r.profit)}</strong></p>`;
|
||||
<div class="settlement-net ${netClass}"><span>Net result</span><strong>${yen(r.profit)}</strong></div>
|
||||
<div class="result-grid settlement-grid">
|
||||
<div><strong>Chick shipment income</strong>${positive(r.chickShipmentIncome || 0)}</div>
|
||||
<div><strong>Poop shipment income</strong>${positive(r.poopShipmentIncome || 0)}</div>
|
||||
<div><strong>Mixer income</strong>${positive(r.mixerRevenue || 0)}</div>
|
||||
<div><strong>Manual combo bonus</strong>${positive(r.manualComboBonus || 0)}</div>
|
||||
<div><strong>Chemical subsidy</strong>${positive(r.chemicalWeaponSubsidy || 0)}</div>
|
||||
<div><strong>Contract bonus</strong>${positive(r.contractBonus || 0)}</div>
|
||||
<div><strong>Penalties</strong>${negative(penalties)}</div>
|
||||
<div><strong>Fairies fee</strong>${negative(r.fairiesTribute?.amount || r.fairiesTribute || 0)}</div>
|
||||
<div><strong>Repair worker wages</strong>${negative(r.repairWorkerWages || 0)}</div>
|
||||
<div><strong>Loan repayments</strong>${negative(r.loanRepayment || 0)}</div>
|
||||
</div>
|
||||
<p class="formula-box compact">Income ${yen(r.revenue)} - losses ${yen(r.penalty)} = <b class="${netClass}">${yen(r.profit)}</b></p>`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
ui.modalActions.appendChild(button('Choose Upgrade Card', beginCardDraft || hideModal, 'primary-button'));
|
||||
ui.modal.classList.remove('equipment-popover', 'gameover-modal');
|
||||
ui.modal.classList.add('visible');
|
||||
}
|
||||
|
||||
function showEmergencyLoanDecision(onUse, onDecline) {
|
||||
const charges = Math.max(0, game.cardEffects?.rescueLoanCharges || 0);
|
||||
ui.modalTitle.textContent = 'Emergency Loan Available';
|
||||
ui.modalBody.innerHTML = `
|
||||
<p class="gameover-reason">End-of-day cash is below ${yen(0)}.</p>
|
||||
<div class="result-grid settlement-grid">
|
||||
<div><strong>Current money</strong><span class="cash-negative">${yen(game.cash)}</span></div>
|
||||
<div><strong>Emergency Loan</strong><span class="cash-positive">+${yen(500)}</span></div>
|
||||
<div><strong>Cards remaining</strong><span>${charges}</span></div>
|
||||
</div>
|
||||
<p class="formula-box compact">Use one Emergency Loan to gain ${yen(500)} immediately. If cash is still negative, another decision will be shown if another card remains.</p>`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
ui.modalActions.appendChild(button('Accept Game Over', onDecline, 'facility-action danger'));
|
||||
ui.modalActions.appendChild(button('Use Emergency Loan', onUse, 'primary-button'));
|
||||
ui.modal.classList.remove('equipment-popover', 'gameover-modal');
|
||||
ui.modal.classList.add('visible');
|
||||
}
|
||||
|
||||
|
||||
function showGameOver() {
|
||||
const processed = game.totals.processed;
|
||||
|
|
@ -256,5 +305,5 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
// Bankruptcy is intentionally checked only in completeTurn(), after all day-end revenue, tribute, and rescue-loan effects are settled.
|
||||
}
|
||||
|
||||
return { updatePanels, updateUI, showTitle, showTurnResult, showGameOver, hideModal, checkGameOver };
|
||||
return { updatePanels, updateUI, showTitle, showTurnResult, showEmergencyLoanDecision, showGameOver, hideModal, checkGameOver };
|
||||
}
|
||||
|
|
|
|||
19
styles.css
19
styles.css
|
|
@ -202,6 +202,8 @@ h1, h2, p { margin: 0; }
|
|||
.card-choice:hover { transform: translate(-1px, -1px); box-shadow: 6px 6px 0 rgba(16,32,21,.18); }
|
||||
.card-choice strong { display: block; font-size: 14px; text-transform: uppercase; line-height: 1.2; }
|
||||
.card-choice span { display: inline-block; margin: 8px 0; padding: 2px 7px; border: 2px solid var(--line); background: var(--white); font-size: 9px; letter-spacing: .1em; }
|
||||
.card-tags { display: flex; flex-wrap: wrap; gap: 4px; margin: 7px 0; }
|
||||
.card-tags span { margin: 0; padding: 2px 5px; font-size: 8px; line-height: 1.1; letter-spacing: .04em; }
|
||||
.card-choice small { display: block; color: var(--muted); font-size: 11px; line-height: 1.35; }
|
||||
.card-choice.rare { background: #fff0d1; box-shadow: inset 0 0 0 3px #f0a020, 5px 5px 0 rgba(16,32,21,.16); }
|
||||
.card-choice.ultra-rare { background: #ffe6f0; box-shadow: inset 0 0 0 3px #c42424, 5px 5px 0 rgba(16,32,21,.18); }
|
||||
|
|
@ -245,6 +247,7 @@ body { font-size: 16px; }
|
|||
.hover-tooltip.visible { display: block; }
|
||||
.hover-tooltip strong { display: block; font-size: 15px; text-transform: uppercase; margin-bottom: 5px; }
|
||||
.hover-tooltip em { display: block; font-style: normal; color: var(--green); font-weight: 900; margin-bottom: 7px; }
|
||||
.hover-tooltip em.warning { color: var(--danger); }
|
||||
.hover-tooltip span { display: block; color: var(--muted); font-size: 12px; }
|
||||
|
||||
|
||||
|
|
@ -334,6 +337,22 @@ body { font-size: 16px; }
|
|||
margin-bottom: 8px;
|
||||
}
|
||||
.gameover-reason { font-weight: 900; color: var(--danger); margin-bottom: 8px; }
|
||||
.settlement-net {
|
||||
border: 3px solid var(--line);
|
||||
background: #f7fff5;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.settlement-net strong { font-size: 24px; }
|
||||
.settlement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
|
||||
.settlement-grid div { padding: 8px; }
|
||||
.settlement-grid span { display: block; font-size: 14px; }
|
||||
.gameover-summary {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue