From 16bde0b87da6f8413c98865f69a0b6382caf297d Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Mon, 8 Jun 2026 18:26:53 +0900 Subject: [PATCH] hm --- src/README.md | 27 -- src/core/config.js | 7 - src/core/entities.js | 19 +- src/core/state.js | 18 +- src/core/text.js | 11 +- src/game.js | 78 ++++- src/index.html | 106 ------- src/render/draw.js | 55 +++- src/styles.css | 623 ------------------------------------- src/systems/buildSystem.js | 98 +++++- src/systems/cards.js | 55 ++-- src/systems/chickSystem.js | 73 ++++- src/systems/contracts.js | 6 +- src/systems/economy.js | 41 +-- src/systems/gameEvents.js | 39 --- src/systems/history.js | 2 +- src/systems/maintenance.js | 8 +- src/systems/routing.js | 98 +++--- src/systems/uiSystem.js | 65 +++- styles.css | 19 ++ 20 files changed, 460 insertions(+), 988 deletions(-) delete mode 100644 src/README.md delete mode 100644 src/index.html delete mode 100644 src/styles.css delete mode 100644 src/systems/gameEvents.js diff --git a/src/README.md b/src/README.md deleted file mode 100644 index 90ddd52..0000000 --- a/src/README.md +++ /dev/null @@ -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 diff --git a/src/core/config.js b/src/core/config.js index 23bcf64..68e13cf 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -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' }, diff --git a/src/core/entities.js b/src/core/entities.js index 7e9db82..fe24a10 100644 --- a/src/core/entities.js +++ b/src/core/entities.js @@ -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 }; } diff --git a/src/core/state.js b/src/core/state.js index 9e7e66e..ae7550a 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -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 }; diff --git a/src/core/text.js b/src/core/text.js index f854201..e387544 100644 --- a/src/core/text.js +++ b/src/core/text.js @@ -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 `${equipmentName(id)}${buildToolPriceText(id)}${flavor ? `${flavor}` : ''}`; } -export function buildToolButtonIds() { - return BUILD_TOOL_IDS; -} - export function routeLabel(dest) { return TEXT.routeLabels[dest] || String(dest).toUpperCase(); } diff --git a/src/game.js b/src/game.js index d4a35fa..d5779ed 100644 --- a/src/game.js +++ b/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 = `${title}${flavor ? `${flavor}` : ''}${lines.map(line => `${line}`).join('')}`; + ui.hoverTooltip.innerHTML = `${title}${warning ? `${warning}` : (flavor ? `${flavor}` : '')}${lines.map(line => `${line}`).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(); } +}); diff --git a/src/index.html b/src/index.html deleted file mode 100644 index a84c5b6..0000000 --- a/src/index.html +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - Chick Sorter v27.0 - - - -
-
- - -
-
CASH¥250
-
TIME60.0s
-
NET¥0
-
DAY1
-
PHASETitle
-
- -
-
MIXER0
-
TRUCK F0
-
WRONG0
-
POOP0
-
- -
-
NO ACTIVE EVENT
-
TRUCK TARGET: FEMALE
-
BELT: 52 px/s
-
- -
-
-
-

CHICK SORTER v27.0

-

Build, drag, connect at least one valid EGG route, then start the next day.

-
- -
- -
-
-

Add / Edit Equipment

-
- - - - - - - - - -
-
-
Build tools unlock after each day.
-
- -
-

Irregular One-Day Event

-
Irregular forced events appear during Build phase.
-
-
-

Status

-
-
-
-
- -
- - - - -
- - -
- DBG -
- - - - - -
-
-
-
-
-
- - - - - - diff --git a/src/render/draw.js b/src/render/draw.js index a8cebaa..99c8e73 100644 --- a/src/render/draw.js +++ b/src/render/draw.js @@ -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; diff --git a/src/styles.css b/src/styles.css deleted file mode 100644 index 29cafcf..0000000 --- a/src/styles.css +++ /dev/null @@ -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; } -} diff --git a/src/systems/buildSystem.js b/src/systems/buildSystem.js index 3d5804b..a9ee9d2 100644 --- a/src/systems/buildSystem.js +++ b/src/systems/buildSystem.js @@ -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 = ` +
+

Left route key: ${left}

+

Right route key: ${right}

+

Role: ${scanner.role === 0 ? 'Male left / others right' : 'Poop left / others right'}

+ ${message ? `

${message}

` : ''} + ${conflict ? `

${conflict}

` : ''} +
`; + 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 = `

Press a key for ${side.toUpperCase()} route.

Esc cancels.

`; + 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) ? '

Income/Fine = ceil(base × 1.05^upgrades)

' : ''}`; 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 diff --git a/src/systems/cards.js b/src/systems/cards.js index e897fb3..10dbf56 100644 --- a/src/systems/cards.js +++ b/src/systems/cards.js @@ -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 = `${card.title}${rarity}${cardDescription(game, card)}`; + const tags = [rarity, ...(card.tags || []).filter(tag => tag !== rarity)].slice(0, 5); + b.innerHTML = `${card.title}
${tags.map(tag => `${tag}`).join('')}
${cardDescription(game, card)}`; b.addEventListener('click', () => chooseCard(card, b)); b.dataset.index = String(index); return b; diff --git a/src/systems/chickSystem.js b/src/systems/chickSystem.js index 5849327..43814ff 100644 --- a/src/systems/chickSystem.js +++ b/src/systems/chickSystem.js @@ -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 }; } diff --git a/src/systems/contracts.js b/src/systems/contracts.js index 8f208d4..2920238 100644 --- a/src/systems/contracts.js +++ b/src/systems/contracts.js @@ -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'; } diff --git a/src/systems/economy.js b/src/systems/economy.js index d79f639..addc196 100644 --- a/src/systems/economy.js +++ b/src/systems/economy.js @@ -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; diff --git a/src/systems/gameEvents.js b/src/systems/gameEvents.js deleted file mode 100644 index 65dd54e..0000000 --- a/src/systems/gameEvents.js +++ /dev/null @@ -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; -} diff --git a/src/systems/history.js b/src/systems/history.js index d71d435..4f9ca52 100644 --- a/src/systems/history.js +++ b/src/systems/history.js @@ -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({ diff --git a/src/systems/maintenance.js b/src/systems/maintenance.js index 32ebbf2..a5bec62 100644 --- a/src/systems/maintenance.js +++ b/src/systems/maintenance.js @@ -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)) { diff --git a/src/systems/routing.js b/src/systems/routing.js index 08c8289..80d3449 100644 --- a/src/systems/routing.js +++ b/src/systems/routing.js @@ -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 || []) { diff --git a/src/systems/uiSystem.js b/src/systems/uiSystem.js index ee6bfe6..8fb2fac 100644 --- a/src/systems/uiSystem.js +++ b/src/systems/uiSystem.js @@ -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: -${yen(lastTribute)}${lastTributeReduction ? ` | Flattery -${yen(lastTributeReduction)}` : ''}` : `Fairies tribute before Build: -${yen(tribute.amount)}${tribute.reduction ? ` | Flattery -${yen(tribute.reduction)}` : ''}`, `Maintenance: worst ${maintenanceSummary(game).worst.percent}% dirty (${maintenanceSummary(game).worst.label}) | Repairman: ${game.repairman?.hiredForNextDay ? 'hired next day' : game.repairman?.active ? 'working' : 'none'}`, - `Card draft: ${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'} | Belt: ${displaySpeed()}px/s | Farms with scanner route: ${connected}/${game.eggFarms.length}`, + `Manual combo: ${game.manualCombo?.count || 0} | Card draft: ${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'} | Belt: ${displaySpeed()}px/s | Farms with scanner route: ${connected}/${game.eggFarms.length}`, game.cardEffects?.usedMachineActive ? 'Procurement: USED MACHINE MODE / no refunds / 60% durability' : '', issues.length ? `Blocked: ${issues[0]}` : `${TEXT.status.allPortsConnected}` ].join('
'); @@ -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 => `${value > 0 ? '+' : ''}${yen(value)}`; + const negative = value => `${value > 0 ? '-' : ''}${yen(value)}`; + 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 = ` -

${yen(r.revenue)} - ${yen(r.penalty)} = ${yen(r.profit)}

-

Final profit: ${yen(r.profit)}

`; +
Net result${yen(r.profit)}
+
+
Chick shipment income${positive(r.chickShipmentIncome || 0)}
+
Poop shipment income${positive(r.poopShipmentIncome || 0)}
+
Mixer income${positive(r.mixerRevenue || 0)}
+
Manual combo bonus${positive(r.manualComboBonus || 0)}
+
Chemical subsidy${positive(r.chemicalWeaponSubsidy || 0)}
+
Contract bonus${positive(r.contractBonus || 0)}
+
Penalties${negative(penalties)}
+
Fairies fee${negative(r.fairiesTribute?.amount || r.fairiesTribute || 0)}
+
Repair worker wages${negative(r.repairWorkerWages || 0)}
+
Loan repayments${negative(r.loanRepayment || 0)}
+
+

Income ${yen(r.revenue)} - losses ${yen(r.penalty)} = ${yen(r.profit)}

`; 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 = ` +

End-of-day cash is below ${yen(0)}.

+
+
Current money${yen(game.cash)}
+
Emergency Loan+${yen(500)}
+
Cards remaining${charges}
+
+

Use one Emergency Loan to gain ${yen(500)} immediately. If cash is still negative, another decision will be shown if another card remains.

`; + 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 }; } diff --git a/styles.css b/styles.css index dae2754..aaadd72 100644 --- a/styles.css +++ b/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;