CHICK SORTER v27.0
-Build, drag, connect at least one valid EGG route, then start the next day.
-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 @@ - - -
- - -Build, drag, connect at least one valid EGG route, then start the next day.
-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}${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('${yen(r.revenue)} - ${yen(r.penalty)} = ${yen(r.profit)}
-Final profit: ${yen(r.profit)}
`; +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)}.
+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;