hmm
This commit is contained in:
parent
2c7e525c53
commit
33cad83ccb
22 changed files with 1714 additions and 1010 deletions
159
README.md
159
README.md
|
|
@ -1,145 +1,28 @@
|
||||||
# Chick Sorter v23.0 Phase 2 Routefix Hotfix
|
# Chick Sorter v27.0 Cleanup Build
|
||||||
|
|
||||||
|
Open `index.html` in a browser.
|
||||||
|
|
||||||
This build changes the SHREDDER Improvement payout rule.
|
## Current baseline
|
||||||
|
|
||||||
## v18 changes
|
- Build, move, box-select, sell, and repair factory equipment during Build phase.
|
||||||
- `SHREDDER Improvement` now uses one payout roll per processed item.
|
- Start the next day only when at least one EGG route reaches a scanner and then any valid exit.
|
||||||
- Probability after n cards: `((√n / 2) × n)%`.
|
- Fairies tribute is paid after day-end settlement and before the next Build phase.
|
||||||
- Maximum SHREDDER Improvement cards counted: 30.
|
- Bankruptcy is checked only at that day-end transition. Temporary negative cash during production does not immediately end the game.
|
||||||
- At 30 cards, the chance is about 82.16%, so the expected payout is about ¥0.8216 per processed item.
|
- Cards use English names and descriptions with numeric effect summaries.
|
||||||
|
- `poop.png` is the only bundled image asset currently used by default; other art falls back to canvas drawing.
|
||||||
|
|
||||||
## SHREDDER Improvement expected value
|
## Controls
|
||||||
|
|
||||||
```text
|
- S1 manual scanner: `A` = left / `D` = right.
|
||||||
n = min(SHREDDER Improvement cards, 30)
|
- S2 manual scanner: `←` = left / `→` = right.
|
||||||
p = ((√n / 2) × n)% = n√n / 200
|
- Build phase: left-drag empty map for box selection; drag selected equipment to move it.
|
||||||
Per item payout X ~ Bernoulli(p)
|
- Right-drag: pan.
|
||||||
E[X] = p yen = n√n / 200 yen
|
- `Ctrl+Z` / `Ctrl+Y`: undo / redo.
|
||||||
```
|
- `Esc`: cancel upgrade-card target selection.
|
||||||
|
|
||||||
For k shredded items, expected total bonus is `k × n√n / 200` yen.
|
## Cleanup in this build
|
||||||
|
|
||||||
---
|
- Removed duplicate `src/index.html` and `src/styles.css`; root `index.html` and `styles.css` are the single launch surface.
|
||||||
|
- Removed the old debug panel, debug hotkey, debug event log, and free-upgrade/debug-cash code.
|
||||||
# Previous build notes
|
- Removed the obsolete right-side selected-facility panel code left behind after the UI removal.
|
||||||
|
- Removed stale historical README files and old changelog text that no longer matched the current specification.
|
||||||
# Chick Sorter v17.0 Shredder Card and Cleanup
|
|
||||||
|
|
||||||
This build adjusts selling constraints, timeout cleanup, settlement UI, and adds the SHREDDER improvement card.
|
|
||||||
|
|
||||||
## v17 changes
|
|
||||||
- The last EGG FARM, MIXER, SHREDDER, and TRUCK can no longer be sold.
|
|
||||||
- Non-last equipment still follows the existing rule: same Build phase removal refunds 100%, older equipment sells for 50%.
|
|
||||||
- When the day timer and shutdown grace expire, remaining chicks/poop are blown off the conveyors with no equipment damage.
|
|
||||||
- Settlement appears 1 second after that cleanup.
|
|
||||||
- The day settlement modal is reduced to two lines: one simple formula and final profit.
|
|
||||||
- Added `SHREDDER Improvement` card.
|
|
||||||
- Each card adds one independent trial per shredded item.
|
|
||||||
- Each trial has a 1/3 chance to pay ¥1.
|
|
||||||
- Expected value per processed item after n cards: n / 3 yen.
|
|
||||||
- EGG FARM price remains ¥250.
|
|
||||||
|
|
||||||
## SHREDDER Improvement expected value
|
|
||||||
|
|
||||||
```text
|
|
||||||
Per item payout X ~ Binomial(n, 1/3)
|
|
||||||
E[X] = n × 1/3 = n/3 yen
|
|
||||||
```
|
|
||||||
|
|
||||||
For k shredded items, expected total bonus is `k × n / 3` yen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Previous notes
|
|
||||||
|
|
||||||
This build refines the v15 card and tax system.
|
|
||||||
|
|
||||||
## v16.1 changes
|
|
||||||
- Fixed equipment improvement target-picking overlay by importing the target bounds helper into the renderer.
|
|
||||||
- Non-candidate equipment now receives a full dark overlay, and valid upgrade candidates are explicitly white-highlighted.
|
|
||||||
- Added a fallback message when a target picker has no valid target, so the game no longer appears frozen.
|
|
||||||
|
|
||||||
## v16 changes
|
|
||||||
|
|
||||||
- `Extra Cards` is now immediate.
|
|
||||||
- Selecting it consumes the current card pick, then grants 2 more card selections in the same draft.
|
|
||||||
- It no longer increases future draft size.
|
|
||||||
- Equipment improvement cards now target machines on the map.
|
|
||||||
- Valid targets are highlighted in white.
|
|
||||||
- Non-target areas are darkened.
|
|
||||||
- Click a highlighted machine to apply the upgrade.
|
|
||||||
- `Esc` cancels target selection and returns to the card draft.
|
|
||||||
- ZUNDA TAX and Fairies tribute no longer contaminate the next day’s profit display.
|
|
||||||
- They are still paid when `Next Day` is pressed.
|
|
||||||
- They still affect cash and total outflow.
|
|
||||||
- They are not counted inside the new day’s `Turn Profit`.
|
|
||||||
- ZUNDA TAX exemption / Legal Work behavior was rechecked and kept explicit.
|
|
||||||
- Base exemption: ¥500.
|
|
||||||
- `Legal Work`: exemption +¥250 and 95% cap point +¥250.
|
|
||||||
- Tax rate: 5% from the first yen above the exemption.
|
|
||||||
- +5% per ¥500 taxable band.
|
|
||||||
- Maximum rate: 95%.
|
|
||||||
- EGG FARM price remains ¥250.
|
|
||||||
|
|
||||||
## Active ZUNDA TAX formula
|
|
||||||
|
|
||||||
```text
|
|
||||||
taxable = max(0, cash - exemption)
|
|
||||||
rate = min(95%, ceil(taxable / 500) × 5%)
|
|
||||||
ZUNDA TAX = ceil(taxable × rate)
|
|
||||||
```
|
|
||||||
|
|
||||||
With no `Legal Work`, `exemption = ¥500`, so ¥501 starts at 5% and ¥10,000 reaches 95%.
|
|
||||||
|
|
||||||
## v19 changes
|
|
||||||
- Added collapsed bottom-left debug panel: infinite cash, day override, free upgrade purchase.
|
|
||||||
- Added `src/core/balance.js` as the primary balance-tuning sheet for prices, income, penalties, timing, card rates, and caps.
|
|
||||||
- Increased UI text size.
|
|
||||||
- Auto Scanner base cooldown is now 2.25 seconds.
|
|
||||||
- High-Quality Bearing now adds +7.5% conveyor speed per card.
|
|
||||||
- Equipment click popover removed; hover now shows flavor text and equipment information.
|
|
||||||
- ERASE button renamed to SELL.
|
|
||||||
- Card drafts can contain up to 2 dud cards; each slot has a 30% dud chance. Dud cards can be clicked away without spending a pick.
|
|
||||||
|
|
||||||
## v20 Phase 1 structural changes
|
|
||||||
- Added a canonical FactoryGraph in `src/systems/routing.js`.
|
|
||||||
- Build/edit operations invalidate the graph.
|
|
||||||
- Route planning, connection validation, congestion component lookup, and debug metrics now read from the same graph model.
|
|
||||||
- Scanner and facility ports are exact grid cells; visual adjacency is not accepted as connectivity.
|
|
||||||
- `Next Day` now commits the current FactoryGraph before fees and day start.
|
|
||||||
- The committed snapshot records conveyor cells, edges, components, port counts, farm outputs, and validation issues.
|
|
||||||
- Added `src/systems/gameEvents.js`.
|
|
||||||
- Major lifecycle, cash, build/sell, move, cleanup, and jam events are stored in a bounded run log.
|
|
||||||
- Expanded the debug panel from cheats only into an observability panel.
|
|
||||||
- Shows FactoryGraph size, components, port status, farm outputs, dead ends, active/queued items, max congestion, and recent events.
|
|
||||||
- This is a structural pass, not a content/balance pass. It is meant to make later map generation, market contracts, route diagnostics, replay, and deeper factory simulation safer to add.
|
|
||||||
|
|
||||||
## v21 Phase 2 structural changes
|
|
||||||
|
|
||||||
Implemented the Phase 2 foundation without the market board or programmable scanner logic.
|
|
||||||
|
|
||||||
- Added seeded no-build terrain generation. Roughly 20% of grid cells become blocked each run while preserving the initial critical route.
|
|
||||||
- Added equipment degradation based on actual use.
|
|
||||||
- Truck and Manual Scanner are exempt and always operate normally.
|
|
||||||
- Conveyor tiles degrade from item pass counts and reduce local belt speed.
|
|
||||||
- Egg Farms degrade from egg production and gradually increase spawn interval.
|
|
||||||
- Mixer, Waste Shredder, and Auto Scanner degrade from processing counts and add delay.
|
|
||||||
- Equipment keeps full performance until 50% durability remains; at 0% durability it performs at half speed.
|
|
||||||
- Added dirty overlay rendering for degraded equipment.
|
|
||||||
- Added a one-day repairman hire button. The repairman costs ¥100 for the next production phase, walks freely across the map, and repairs 1% degradation every 3 seconds.
|
|
||||||
- Expanded debug/hover readouts with blocked-cell and degradation information.
|
|
||||||
|
|
||||||
Market contracts and deeper scanner-programming logic are intentionally left for later Phase 2 work.
|
|
||||||
|
|
||||||
## v22 Phase 2 starter-route protection
|
|
||||||
|
|
||||||
No-build generation now protects the complete initial factory footprint before blocked cells are placed. The protected footprint includes the default Egg Farm, scanner bodies, scanner input/output ports, machine receiver cells, and every starter conveyor segment. After blocked-cell generation, the starter backbone is scrubbed from the blocked set and reinstalled as a final guard.
|
|
||||||
|
|
||||||
Result: even with roughly 20% no-build cells, the initial line remains continuous from Egg Farm to S1, S1 to S2, and S2 to both Shredder and Truck exits.
|
|
||||||
|
|
||||||
|
|
||||||
## v23 hotfix
|
|
||||||
|
|
||||||
- Fixed the startup ReferenceError caused by binding the repairman button to a missing `hireRepairman` wrapper.
|
|
||||||
- Disabled optional external art probing by default, preventing 404 console noise for placeholder image filenames. Canvas fallback art remains active.
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,3 @@
|
||||||
# Image asset folder
|
# Image assets
|
||||||
|
|
||||||
The game currently uses simple canvas placeholders. You can override them by adding images with these filenames:
|
`poop.png` is bundled and loaded by default. Other equipment/chick art is optional and currently falls back to canvas drawing unless external art loading is enabled in `src/render/draw.js`.
|
||||||
|
|
||||||
- `chick_male.png`
|
|
||||||
- `chick_female.png`
|
|
||||||
- `tarinai.png`
|
|
||||||
- `conveyor.png`
|
|
||||||
- `scanner_manual.png`
|
|
||||||
- `scanner_auto.png`
|
|
||||||
- `egg_farm.png`
|
|
||||||
- `mixer.png`
|
|
||||||
- `shredder.png`
|
|
||||||
- `truck.png`
|
|
||||||
|
|
||||||
The render code checks whether each image loads and falls back to simple shapes when it does not.
|
|
||||||
|
|
|
||||||
BIN
assets/images/poop.png
Normal file
BIN
assets/images/poop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
29
index.html
29
index.html
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Chick Sorter v21.0 Phase 2 Map / Degradation</title>
|
<title>Chick Sorter v27.0</title>
|
||||||
<link rel="stylesheet" href="./styles.css" />
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -35,8 +35,8 @@
|
||||||
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
|
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>CHICK SORTER <span>v21.0</span></h1>
|
<h1>CHICK SORTER <span>v27.0</span></h1>
|
||||||
<p>Build, drag, connect all receiver ports, then start the next day.</p>
|
<p>Build, drag, connect at least one valid EGG route, then start the next day.</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
|
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -63,13 +63,7 @@
|
||||||
<h2>Irregular One-Day Event</h2>
|
<h2>Irregular One-Day Event</h2>
|
||||||
<div id="contractPanel" class="contract-card empty">Irregular forced events appear during Build phase.</div>
|
<div id="contractPanel" class="contract-card empty">Irregular forced events appear during Build phase.</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="rules-section compact-rules">
|
||||||
<section class="facility-section">
|
|
||||||
<h2>Selected Facility</h2>
|
|
||||||
<div id="facilityPanel" class="facility-panel-empty">Click equipment in Build phase.</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="rules-section compact-rules">
|
|
||||||
<h2>Status</h2>
|
<h2>Status</h2>
|
||||||
<div id="turnSummary" class="mini-box"></div>
|
<div id="turnSummary" class="mini-box"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -83,19 +77,6 @@
|
||||||
<button id="scanner2TruckButton" class="sort-button" type="button">S2 -> Truck</button>
|
<button id="scanner2TruckButton" class="sort-button" type="button">S2 -> Truck</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="hoverTooltip" class="hover-tooltip" aria-hidden="true"></div>
|
<div id="hoverTooltip" class="hover-tooltip" aria-hidden="true"></div>
|
||||||
|
|
||||||
<details id="debugPanel" class="debug-panel">
|
|
||||||
<summary>DBG</summary>
|
|
||||||
<div class="debug-body">
|
|
||||||
<label><input id="debugInfiniteCash" type="checkbox" /> Infinite cash</label>
|
|
||||||
<label>Day <input id="debugDayInput" type="number" min="1" step="1" value="1" /></label>
|
|
||||||
<button id="debugSetDayButton" type="button">Set Day</button>
|
|
||||||
<label>Upgrade <select id="debugUpgradeSelect"></select></label>
|
|
||||||
<button id="debugApplyUpgradeButton" type="button">Free Buy</button>
|
|
||||||
<div id="debugObservations" class="debug-readout"></div>
|
|
||||||
<div id="debugEventLog" class="debug-readout event-log"></div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
@ -107,6 +88,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="./src/game.js?v=23.0-phase2-routefix-hotfix"></script>
|
<script type="module" src="./src/game.js?v=27.0-cleanup"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Central balance sheet for tuning gameplay.
|
// Central balance sheet for tuning gameplay.
|
||||||
// Edit this file first when adjusting prices, income, penalties, timing, card rates, or caps.
|
// Edit this file first when adjusting prices, income, penalties, timing, card rates, or caps.
|
||||||
export const BALANCE = {
|
export const BALANCE = {
|
||||||
version: 'v23.0 Phase 2 Routefix Hotfix',
|
version: 'v27.0 cleanup build',
|
||||||
time: {
|
time: {
|
||||||
daySeconds: 60,
|
daySeconds: 60,
|
||||||
farmShutdownGraceSeconds: 10
|
farmShutdownGraceSeconds: 10
|
||||||
|
|
@ -17,7 +17,7 @@ export const BALANCE = {
|
||||||
cell: 46
|
cell: 46
|
||||||
},
|
},
|
||||||
map: {
|
map: {
|
||||||
blockedRatio: 0.20
|
blockedRatio: 0.075
|
||||||
},
|
},
|
||||||
economy: {
|
economy: {
|
||||||
income: {
|
income: {
|
||||||
|
|
@ -64,6 +64,7 @@ export const BALANCE = {
|
||||||
cards: {
|
cards: {
|
||||||
commonWeight: 8,
|
commonWeight: 8,
|
||||||
rareWeight: 2,
|
rareWeight: 2,
|
||||||
|
ultraRareWeight: 0.45,
|
||||||
baseDraftSize: 3,
|
baseDraftSize: 3,
|
||||||
maxDudsPerDraft: 2,
|
maxDudsPerDraft: 2,
|
||||||
dudChancePerCard: 0.30,
|
dudChancePerCard: 0.30,
|
||||||
|
|
@ -81,11 +82,11 @@ export const BALANCE = {
|
||||||
fullPerformanceUntilWear: 0.50,
|
fullPerformanceUntilWear: 0.50,
|
||||||
minimumPerformance: 0.50,
|
minimumPerformance: 0.50,
|
||||||
durability: {
|
durability: {
|
||||||
conveyor: 260,
|
conveyor: 1300,
|
||||||
eggFarm: 95,
|
eggFarm: 475,
|
||||||
autoScanner: 170,
|
autoScanner: 850,
|
||||||
mixer: 155,
|
mixer: 775,
|
||||||
trash: 190
|
trash: 950
|
||||||
},
|
},
|
||||||
processingDelayMaxSeconds: {
|
processingDelayMaxSeconds: {
|
||||||
mixer: 1.20,
|
mixer: 1.20,
|
||||||
|
|
@ -93,7 +94,7 @@ export const BALANCE = {
|
||||||
},
|
},
|
||||||
repairman: {
|
repairman: {
|
||||||
dailyCost: 100,
|
dailyCost: 100,
|
||||||
secondsPerOnePercent: 3,
|
secondsPerOnePercent: 1 / 3,
|
||||||
walkSpeed: 140
|
walkSpeed: 140
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,12 @@ export function createFacility(game, id, p, price) {
|
||||||
return layoutFacilityOnEdge(f, entry, side);
|
return layoutFacilityOnEdge(f, entry, side);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function rollChickSex(game) {
|
||||||
|
return Math.random() < currentPoopRate(game) ? 'poop' : (Math.random() < 0.5 ? 'male' : 'female');
|
||||||
|
}
|
||||||
|
|
||||||
export function createChick(game, routeData) {
|
export function createChick(game, routeData) {
|
||||||
const sex = Math.random() < currentPoopRate(game) ? 'poop' : (Math.random() < 0.5 ? 'male' : 'female');
|
const sex = rollChickSex(game);
|
||||||
const start = routeData.route[0];
|
const start = routeData.route[0];
|
||||||
return {
|
return {
|
||||||
id: game.nextId++, sex,
|
id: game.nextId++, sex,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ export function newTurnStats() {
|
||||||
truckRevenue: 0,
|
truckRevenue: 0,
|
||||||
zundaTax: 0,
|
zundaTax: 0,
|
||||||
fairiesTribute: 0,
|
fairiesTribute: 0,
|
||||||
|
loanRepayment: 0,
|
||||||
|
chemicalWeaponSubsidy: 0,
|
||||||
|
rescueLoan: 0,
|
||||||
cardRerollCost: 0,
|
cardRerollCost: 0,
|
||||||
mixerPoopFine: 0,
|
mixerPoopFine: 0,
|
||||||
truckPoopFine: 0,
|
truckPoopFine: 0,
|
||||||
|
|
@ -56,6 +59,9 @@ export function newTotalStats() {
|
||||||
contractBonus: 0,
|
contractBonus: 0,
|
||||||
zundaTax: 0,
|
zundaTax: 0,
|
||||||
fairiesTribute: 0,
|
fairiesTribute: 0,
|
||||||
|
loanRepayment: 0,
|
||||||
|
chemicalWeaponSubsidy: 0,
|
||||||
|
rescueLoan: 0,
|
||||||
cardRerollCost: 0,
|
cardRerollCost: 0,
|
||||||
mixerPoopFine: 0,
|
mixerPoopFine: 0,
|
||||||
truckPoopFine: 0,
|
truckPoopFine: 0,
|
||||||
|
|
@ -79,7 +85,6 @@ export function createGame() {
|
||||||
cleanupBlown: false,
|
cleanupBlown: false,
|
||||||
lastTimestamp: 0,
|
lastTimestamp: 0,
|
||||||
nextId: 10,
|
nextId: 10,
|
||||||
nextEventId: 1,
|
|
||||||
runSeed: createRunSeed(),
|
runSeed: createRunSeed(),
|
||||||
blockedCells: new Set(),
|
blockedCells: new Set(),
|
||||||
buildTool: null,
|
buildTool: null,
|
||||||
|
|
@ -92,8 +97,6 @@ export function createGame() {
|
||||||
groupDrag: null,
|
groupDrag: null,
|
||||||
pan: null,
|
pan: null,
|
||||||
view: { x: 0, y: 0 },
|
view: { x: 0, y: 0 },
|
||||||
debug: false,
|
|
||||||
debugInfiniteCash: false,
|
|
||||||
hover: null,
|
hover: null,
|
||||||
chicks: [],
|
chicks: [],
|
||||||
effects: [],
|
effects: [],
|
||||||
|
|
@ -115,13 +118,12 @@ export function createGame() {
|
||||||
lastExplodedComponent: new Map(),
|
lastExplodedComponent: new Map(),
|
||||||
contractOffer: null,
|
contractOffer: null,
|
||||||
contractActive: null,
|
contractActive: null,
|
||||||
cardEffects: { bearing: 0, legalWork: 0, flattery: 0 },
|
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 },
|
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
|
||||||
cardTargetPick: null,
|
cardTargetPick: null,
|
||||||
repairman: { hiredForNextDay: false, active: false, x: GRID.x + GRID.cell * 0.5, y: GRID.y + GRID.rows * GRID.cell + 84, target: null, repairedToday: 0 },
|
repairman: { hiredForNextDay: false, active: false, x: GRID.x + GRID.cell * 0.5, y: GRID.y + GRID.rows * GRID.cell + 84, target: null, repairedToday: 0 },
|
||||||
stats: newTurnStats(),
|
stats: newTurnStats(),
|
||||||
lastResult: null,
|
lastResult: null,
|
||||||
eventLog: [],
|
|
||||||
totals: newTotalStats()
|
totals: newTotalStats()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const TEXT = {
|
||||||
status: {
|
status: {
|
||||||
build: tool => `BUILD: ${(tool === 'erase' ? 'SELL' : (tool || 'SELECT')).toUpperCase()} | Right-drag pan | Hover equipment for info`,
|
build: tool => `BUILD: ${(tool === 'erase' ? 'SELL' : (tool || 'SELECT')).toUpperCase()} | Right-drag pan | Hover equipment for info`,
|
||||||
sorting: 'Sorting: use scanner keys. Build after clearing the day.',
|
sorting: 'Sorting: use scanner keys. Build after clearing the day.',
|
||||||
allPortsConnected: 'All required ports connected.',
|
allPortsConnected: 'Start condition met: one EGG route reaches an exit.',
|
||||||
equipmentPanelEmpty: 'Hover equipment in Build phase.',
|
equipmentPanelEmpty: 'Hover equipment in Build phase.',
|
||||||
eventPanelRunning: 'Forced events appear in Build phase.'
|
eventPanelRunning: 'Forced events appear in Build phase.'
|
||||||
},
|
},
|
||||||
|
|
@ -65,8 +65,22 @@ export function buildToolPriceText(id) {
|
||||||
return yen(def.price);
|
return yen(def.price);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildToolFlavorText(id) {
|
||||||
|
const flavors = {
|
||||||
|
conveyor: 'A narrow green belt. Routes decide whether chicks live, ship, or become invoices.',
|
||||||
|
eggFarm: 'A tiny gatehouse producing questionable eggs on schedule.',
|
||||||
|
autoScanner: 'An automated judge. Faster than hands, still very sure of itself.',
|
||||||
|
manualScanner: 'A manual checkpoint. The operator is the algorithm.',
|
||||||
|
mixer: 'Male chicks become revenue here. Do not feed it poop.',
|
||||||
|
trash: 'A polite shredder for poop and other regrets.',
|
||||||
|
truck: 'Ships the target cargo. Wrong cargo still leaves a paper trail.'
|
||||||
|
};
|
||||||
|
return flavors[id] || '';
|
||||||
|
}
|
||||||
|
|
||||||
export function buildToolButtonHtml(id) {
|
export function buildToolButtonHtml(id) {
|
||||||
return `<strong>${equipmentName(id)}</strong><span>${buildToolPriceText(id)}</span>`;
|
const flavor = buildToolFlavorText(id);
|
||||||
|
return `<strong>${equipmentName(id)}</strong><span>${buildToolPriceText(id)}</span>${flavor ? `<small class="tool-flavor">${flavor}</small>` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildToolButtonIds() {
|
export function buildToolButtonIds() {
|
||||||
|
|
|
||||||
126
src/game.js
126
src/game.js
|
|
@ -1,18 +1,17 @@
|
||||||
import { TURN_SECONDS, THEME } from './core/config.js';
|
import { TURN_SECONDS, THEME } from './core/config.js';
|
||||||
import { buildToolButtonHtml } from './core/text.js';
|
import { buildToolButtonHtml } from './core/text.js';
|
||||||
import { createGame, resetLayout, newTurnStats, nextSpawnDelay } from './core/state.js';
|
import { createGame, resetLayout, newTurnStats } from './core/state.js';
|
||||||
import { clamp, pointToCell, cellCenter, yen } from './core/utils.js';
|
import { clamp, pointToCell, cellCenter, yen } from './core/utils.js';
|
||||||
import { commitFactoryGraphForDay, facilityConnectionIssues } from './systems/routing.js';
|
import { commitFactoryGraphForDay, facilityConnectionIssues } from './systems/routing.js';
|
||||||
import { collectFairiesTribute, collectZundaTax, settleTruckRevenue } from './systems/economy.js';
|
import { applyRescueLoanIfNeeded, collectChemicalWeaponSubsidy, collectFairiesTribute, collectLoanRepayments, collectZundaTax, settleTruckRevenue } from './systems/economy.js';
|
||||||
import { undo, redo } from './systems/history.js';
|
import { undo, redo } from './systems/history.js';
|
||||||
import { drawAll } from './render/draw.js';
|
import { drawAll } from './render/draw.js';
|
||||||
import { createBuildSystem } from './systems/buildSystem.js';
|
import { createBuildSystem } from './systems/buildSystem.js';
|
||||||
import { createUISystem } from './systems/uiSystem.js';
|
import { createUISystem } from './systems/uiSystem.js';
|
||||||
import { createChickSystem } from './systems/chickSystem.js';
|
import { createChickSystem } from './systems/chickSystem.js';
|
||||||
import { conveyorSpeedForGame, createCardSystem, ensureCardState } from './systems/cards.js';
|
import { conveyorSpeedForGame, createCardSystem } from './systems/cards.js';
|
||||||
import { floating, shockwave, sparkBurst, updateEffects } from './systems/effects.js';
|
import { floating, shockwave, sparkBurst, updateEffects } from './systems/effects.js';
|
||||||
import { rollContractOffer, activateAcceptedContract, clearActiveContract, resolveContract } from './systems/contracts.js';
|
import { rollContractOffer, activateAcceptedContract, clearActiveContract, resolveContract } from './systems/contracts.js';
|
||||||
import { clearGameEvents, emitGameEvent } from './systems/gameEvents.js';
|
|
||||||
import { ensureMaintenanceState, hireRepairmanForNextDay, activateRepairmanForDay, deactivateRepairman, conveyorSpeedFactorForKey } from './systems/maintenance.js';
|
import { ensureMaintenanceState, hireRepairmanForNextDay, activateRepairmanForDay, deactivateRepairman, conveyorSpeedFactorForKey } from './systems/maintenance.js';
|
||||||
|
|
||||||
const canvas = document.getElementById('gameCanvas');
|
const canvas = document.getElementById('gameCanvas');
|
||||||
|
|
@ -23,17 +22,9 @@ const ui = {
|
||||||
money: document.getElementById('money'), turn: document.getElementById('turn'), timeLeft: document.getElementById('timeLeft'), phase: document.getElementById('phaseLabel'),
|
money: document.getElementById('money'), turn: document.getElementById('turn'), timeLeft: document.getElementById('timeLeft'), phase: document.getElementById('phaseLabel'),
|
||||||
eventBrief: document.getElementById('eventBrief'), targetBrief: document.getElementById('targetBrief'), speedBrief: document.getElementById('speedBrief'),
|
eventBrief: document.getElementById('eventBrief'), targetBrief: document.getElementById('targetBrief'), speedBrief: document.getElementById('speedBrief'),
|
||||||
turnProfit: document.getElementById('turnProfit'), mixerCount: document.getElementById('mixerCount'), truckFemaleCount: document.getElementById('truckFemaleCount'), truckMaleCount: document.getElementById('truckMaleCount'), poopCount: document.getElementById('poopCount'),
|
turnProfit: document.getElementById('turnProfit'), mixerCount: document.getElementById('mixerCount'), truckFemaleCount: document.getElementById('truckFemaleCount'), truckMaleCount: document.getElementById('truckMaleCount'), poopCount: document.getElementById('poopCount'),
|
||||||
buildStatus: document.getElementById('buildStatus'), turnSummary: document.getElementById('turnSummary'), contractPanel: document.getElementById('contractPanel'), facilityPanel: document.getElementById('facilityPanel'),
|
buildStatus: document.getElementById('buildStatus'), turnSummary: document.getElementById('turnSummary'), contractPanel: document.getElementById('contractPanel'),
|
||||||
modal: document.getElementById('modal'), modalTitle: document.getElementById('modalTitle'), modalBody: document.getElementById('modalBody'), modalActions: document.getElementById('modalActions'),
|
modal: document.getElementById('modal'), modalTitle: document.getElementById('modalTitle'), modalBody: document.getElementById('modalBody'), modalActions: document.getElementById('modalActions'),
|
||||||
hoverTooltip: document.getElementById('hoverTooltip'),
|
hoverTooltip: document.getElementById('hoverTooltip'),
|
||||||
debugPanel: document.getElementById('debugPanel'),
|
|
||||||
debugInfiniteCash: document.getElementById('debugInfiniteCash'),
|
|
||||||
debugDayInput: document.getElementById('debugDayInput'),
|
|
||||||
debugSetDayButton: document.getElementById('debugSetDayButton'),
|
|
||||||
debugUpgradeSelect: document.getElementById('debugUpgradeSelect'),
|
|
||||||
debugApplyUpgradeButton: document.getElementById('debugApplyUpgradeButton'),
|
|
||||||
debugObservations: document.getElementById('debugObservations'),
|
|
||||||
debugEventLog: document.getElementById('debugEventLog'),
|
|
||||||
hireRepairmanButton: document.getElementById('hireRepairmanButton'),
|
hireRepairmanButton: document.getElementById('hireRepairmanButton'),
|
||||||
buttons: {
|
buttons: {
|
||||||
s1Left: document.getElementById('scanner1MixerButton'), s1Right: document.getElementById('scanner1TruckButton'), s2Left: document.getElementById('scanner2MixerButton'), s2Right: document.getElementById('scanner2TruckButton'),
|
s1Left: document.getElementById('scanner1MixerButton'), s1Right: document.getElementById('scanner1TruckButton'), s2Left: document.getElementById('scanner2MixerButton'), s2Right: document.getElementById('scanner2TruckButton'),
|
||||||
|
|
@ -74,10 +65,6 @@ function startGame() {
|
||||||
game.view = { x: 0, y: 0 };
|
game.view = { x: 0, y: 0 };
|
||||||
resetLayout(game);
|
resetLayout(game);
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
clearGameEvents(game);
|
|
||||||
emitGameEvent(game, 'RunStarted', { cash: game.cash });
|
|
||||||
game.debugInfiniteCash = !!ui.debugInfiniteCash?.checked;
|
|
||||||
refreshDebugUpgradeSelect();
|
|
||||||
uiSystem.hideModal();
|
uiSystem.hideModal();
|
||||||
chicks.updateCongestion();
|
chicks.updateCongestion();
|
||||||
uiSystem.updatePanels();
|
uiSystem.updatePanels();
|
||||||
|
|
@ -87,7 +74,7 @@ function startGame() {
|
||||||
function startNextTurn() {
|
function startNextTurn() {
|
||||||
if (game.phase !== 'build') return;
|
if (game.phase !== 'build') return;
|
||||||
if (game.cardDraft?.pending || game.cardTargetPick?.pending) {
|
if (game.cardDraft?.pending || game.cardTargetPick?.pending) {
|
||||||
build.fail(game.cardTargetPick?.pending ? 'Choose highlighted upgrade target first.' : 'Choose an upgrade card first.');
|
build.fail(game.cardTargetPick?.pending ? 'Choose upgrade target first.' : 'Choose an upgrade card first.');
|
||||||
uiSystem.updatePanels();
|
uiSystem.updatePanels();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -98,18 +85,16 @@ function startNextTurn() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
const graph = commitFactoryGraphForDay(game);
|
commitFactoryGraphForDay(game);
|
||||||
emitGameEvent(game, 'FactoryGraphCommitted', { cells: graph.metrics.conveyorCells, edges: graph.metrics.edges, components: graph.metrics.components });
|
|
||||||
activateRepairmanForDay(game);
|
activateRepairmanForDay(game);
|
||||||
const zundaBasisCash = game.cash;
|
const zundaBasisCash = game.cash;
|
||||||
const zunda = collectZundaTax(game, zundaBasisCash);
|
const zunda = collectZundaTax(game, zundaBasisCash);
|
||||||
const tribute = collectFairiesTribute(game, game.turn);
|
const loanRepayment = collectLoanRepayments(game);
|
||||||
game.lastStartFees = { zundaTax: zunda.tax || 0, fairiesTribute: tribute.amount || 0 };
|
game.lastStartFees = { zundaTax: zunda.tax || 0, loanRepayment: loanRepayment.amount || 0, fairiesTribute: 0 };
|
||||||
if (zunda.tax > 0) floating(game, canvas.width / 2 - game.view.x, 116 - game.view.y, `ZUNDA TAX -${yen(zunda.tax)}`, THEME.danger);
|
if (zunda.tax > 0) floating(game, canvas.width / 2 - game.view.x, 116 - game.view.y, `ZUNDA TAX -${yen(zunda.tax)}`, THEME.danger);
|
||||||
if (tribute.amount > 0) floating(game, canvas.width / 2 - game.view.x, 146 - game.view.y, `FAIRIES -${yen(tribute.amount)}`, THEME.danger);
|
if (loanRepayment.amount > 0) floating(game, canvas.width / 2 - game.view.x, 146 - game.view.y, `LOAN -${yen(loanRepayment.amount)}`, THEME.danger);
|
||||||
if (game.cash < 0) { game.phase = 'gameover'; uiSystem.showGameOver(); return; }
|
// Negative cash during the next production day is allowed; bankruptcy is checked only at day-end settlement before Build phase.
|
||||||
activateAcceptedContract(game);
|
activateAcceptedContract(game);
|
||||||
emitGameEvent(game, 'StartDay', { day: game.turn + 1, zundaTax: zunda.tax || 0, fairiesTribute: tribute.amount || 0 });
|
|
||||||
game.phase = 'running';
|
game.phase = 'running';
|
||||||
game.turn += 1;
|
game.turn += 1;
|
||||||
game.timeLeft = TURN_SECONDS;
|
game.timeLeft = TURN_SECONDS;
|
||||||
|
|
@ -153,10 +138,17 @@ function completeTurn() {
|
||||||
if (game.phase !== 'running') return;
|
if (game.phase !== 'running') return;
|
||||||
const ship = settleTruckRevenue(game);
|
const ship = settleTruckRevenue(game);
|
||||||
const contract = resolveContract(game);
|
const contract = resolveContract(game);
|
||||||
|
const chemical = collectChemicalWeaponSubsidy(game);
|
||||||
|
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);
|
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.totals.turnsCompleted += 1;
|
||||||
game.lastResult = { ...game.stats, ship, contract, turn: game.turn, cash: game.cash };
|
game.lastResult = { ...game.stats, ship, contract, chemical, rescue, fairiesTribute: tribute, turn: game.turn, cash: game.cash };
|
||||||
emitGameEvent(game, 'EndDay', { day: game.turn, revenue: game.stats.revenue, penalty: game.stats.penalty, profit: game.stats.profit, cash: game.cash });
|
|
||||||
if (game.cash < 0) { game.phase = 'gameover'; uiSystem.showGameOver(); return; }
|
if (game.cash < 0) { game.phase = 'gameover'; uiSystem.showGameOver(); return; }
|
||||||
game.phase = 'build';
|
game.phase = 'build';
|
||||||
game.truckCargo = [];
|
game.truckCargo = [];
|
||||||
|
|
@ -192,7 +184,6 @@ function closeFarmShutters() {
|
||||||
shockwave(game, c.x, c.y, THEME.ink);
|
shockwave(game, c.x, c.y, THEME.ink);
|
||||||
sparkBurst(game, c.x, c.y + 10, 10);
|
sparkBurst(game, c.x, c.y + 10, 10);
|
||||||
floating(game, c.x, c.y - 34, 'SHUT +10s', THEME.ink);
|
floating(game, c.x, c.y - 34, 'SHUT +10s', THEME.ink);
|
||||||
emitGameEvent(game, 'FarmShutterClosed', { farmId: farm.id });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,7 +191,6 @@ function update(timestamp) {
|
||||||
if (!game.lastTimestamp) game.lastTimestamp = timestamp;
|
if (!game.lastTimestamp) game.lastTimestamp = timestamp;
|
||||||
const dt = Math.min((timestamp - game.lastTimestamp) / 1000, 0.05);
|
const dt = Math.min((timestamp - game.lastTimestamp) / 1000, 0.05);
|
||||||
game.lastTimestamp = timestamp;
|
game.lastTimestamp = timestamp;
|
||||||
if (game.debugInfiniteCash && game.cash < 999999) { game.cash = 999999; game.totals.maxCash = Math.max(game.totals.maxCash, game.cash); }
|
|
||||||
if (game.phase === 'running') chicks.updateRunning(dt, { closeFarmShutters, completeTurn });
|
if (game.phase === 'running') chicks.updateRunning(dt, { closeFarmShutters, completeTurn });
|
||||||
updateEffects(game, dt, canvas, build.equipmentHitBoxes);
|
updateEffects(game, dt, canvas, build.equipmentHitBoxes);
|
||||||
chicks.updateCongestion();
|
chicks.updateCongestion();
|
||||||
|
|
@ -310,86 +300,12 @@ 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.buttons.nextTurn.addEventListener('click', startNextTurn); ui.buttons.conveyor.addEventListener('click', () => build.setBuildTool('conveyor')); ui.buttons.eggFarm.addEventListener('click', () => build.setBuildTool('eggFarm')); ui.buttons.autoScanner.addEventListener('click', () => build.setBuildTool('autoScanner')); ui.buttons.manualScanner.addEventListener('click', () => build.setBuildTool('manualScanner')); ui.buttons.mixer.addEventListener('click', () => build.setBuildTool('mixer')); ui.buttons.trash.addEventListener('click', () => build.setBuildTool('trash')); ui.buttons.truck.addEventListener('click', () => build.setBuildTool('truck')); ui.buttons.erase.addEventListener('click', () => build.setBuildTool('erase'));
|
||||||
ui.hireRepairmanButton?.addEventListener('click', hireRepairman);
|
ui.hireRepairmanButton?.addEventListener('click', hireRepairman);
|
||||||
ui.buttons.undo.addEventListener('click', () => { if (undo(game)) uiSystem.updatePanels(); }); ui.buttons.redo.addEventListener('click', () => { if (redo(game)) uiSystem.updatePanels(); });
|
ui.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(); } if (name === 'f1') { event.preventDefault(); game.debug = !game.debug; } });
|
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 debugUpgradeOptions() {
|
|
||||||
const options = [
|
|
||||||
{ value: 'effect:bearing', label: `Bearing +1 (${game.cardEffects?.bearing || 0})` },
|
|
||||||
{ value: 'effect:legalWork', label: `Legal Work +1 (${game.cardEffects?.legalWork || 0})` },
|
|
||||||
{ value: 'effect:flattery', label: `Flattery +1 (${game.cardEffects?.flattery || 0})` }
|
|
||||||
];
|
|
||||||
for (const farm of game.eggFarms) options.push({ value: `eggFarm:${farm.id}`, label: `EGG #${farm.id} L${farm.level}` });
|
|
||||||
for (const scanner of game.scanners.filter(s => s.kind === 'auto')) options.push({ value: `scanner:${scanner.id}`, label: `AUTO #${scanner.id} L${scanner.level}` });
|
|
||||||
for (const id of ['mixer', 'trash', 'truck']) {
|
|
||||||
const f = game.facilities[id];
|
|
||||||
if (f) options.push({ value: `facility:${id}`, label: `${f.shortName || f.name} L${f.level}` });
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshDebugUpgradeSelect() {
|
|
||||||
if (!ui.debugUpgradeSelect) return;
|
|
||||||
const previous = ui.debugUpgradeSelect.value;
|
|
||||||
const options = debugUpgradeOptions();
|
|
||||||
ui.debugUpgradeSelect.innerHTML = options.map(o => `<option value="${o.value}">${o.label}</option>`).join('');
|
|
||||||
if (options.some(o => o.value === previous)) ui.debugUpgradeSelect.value = previous;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDebugUpgrade() {
|
|
||||||
ensureCardState(game);
|
|
||||||
const value = ui.debugUpgradeSelect?.value || '';
|
|
||||||
const [kind, id] = value.split(':');
|
|
||||||
if (kind === 'effect') {
|
|
||||||
game.cardEffects[id] = Math.max(0, game.cardEffects[id] || 0) + 1;
|
|
||||||
floating(game, canvas.width / 2 - game.view.x, 112 - game.view.y, `${id.toUpperCase()} +1`, THEME.green);
|
|
||||||
} else if (kind === 'eggFarm') {
|
|
||||||
const farm = game.eggFarms.find(f => String(f.id) === id);
|
|
||||||
if (farm) {
|
|
||||||
farm.level = Math.min(4, (farm.level || 1) + 1);
|
|
||||||
farm.nextSpawn = nextSpawnDelay(farm);
|
|
||||||
farm.lastInterval = farm.nextSpawn;
|
|
||||||
floating(game, cellCenter(farm.col, farm.row).x, cellCenter(farm.col, farm.row).y - 34, `LV ${farm.level}`, THEME.green);
|
|
||||||
}
|
|
||||||
} else if (kind === 'scanner') {
|
|
||||||
const scanner = game.scanners.find(sc => String(sc.id) === id);
|
|
||||||
if (scanner) {
|
|
||||||
scanner.level = (scanner.level || 1) + 1;
|
|
||||||
const c = cellCenter(scanner.col, scanner.row);
|
|
||||||
floating(game, c.x, c.y - 42, `LV ${scanner.level}`, THEME.green);
|
|
||||||
}
|
|
||||||
} else if (kind === 'facility') {
|
|
||||||
const f = game.facilities[id];
|
|
||||||
if (f) {
|
|
||||||
if (id === 'trash') f.level = Math.min(31, (f.level || 1) + 1);
|
|
||||||
else f.level = (f.level || 1) + 1;
|
|
||||||
floating(game, f.x + f.w / 2, f.y + 20, `LV ${f.level}`, THEME.green);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
refreshDebugUpgradeSelect();
|
|
||||||
uiSystem?.updatePanels();
|
|
||||||
}
|
|
||||||
|
|
||||||
function wireDebugControls() {
|
|
||||||
if (!ui.debugPanel) return;
|
|
||||||
refreshDebugUpgradeSelect();
|
|
||||||
ui.debugPanel.addEventListener('toggle', refreshDebugUpgradeSelect);
|
|
||||||
ui.debugInfiniteCash?.addEventListener('change', () => {
|
|
||||||
game.debugInfiniteCash = !!ui.debugInfiniteCash.checked;
|
|
||||||
if (game.debugInfiniteCash) game.cash = Math.max(game.cash, 999999);
|
|
||||||
uiSystem?.updateUI();
|
|
||||||
});
|
|
||||||
ui.debugSetDayButton?.addEventListener('click', () => {
|
|
||||||
game.turn = Math.max(1, Math.floor(Number(ui.debugDayInput?.value) || 1));
|
|
||||||
uiSystem?.updatePanels();
|
|
||||||
uiSystem?.updateUI();
|
|
||||||
});
|
|
||||||
ui.debugApplyUpgradeButton?.addEventListener('click', applyDebugUpgrade);
|
|
||||||
ui.debugUpgradeSelect?.addEventListener('pointerdown', refreshDebugUpgradeSelect);
|
|
||||||
}
|
|
||||||
|
|
||||||
build = createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanels: () => uiSystem?.updatePanels() });
|
build = createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanels: () => uiSystem?.updatePanels() });
|
||||||
chicks = createChickSystem({ game, currentConveyorSpeed, onGameOverCheck: () => uiSystem?.checkGameOver() });
|
chicks = createChickSystem({ game, currentConveyorSpeed, onGameOverCheck: () => uiSystem?.checkGameOver() });
|
||||||
cardSystem = createCardSystem({ game, ui, onUpdatePanels: () => uiSystem?.updatePanels() });
|
cardSystem = createCardSystem({ game, ui, onUpdatePanels: () => uiSystem?.updatePanels() });
|
||||||
uiSystem = createUISystem({ game, ui, build, startGame, beginCardDraft: () => cardSystem.showDraft(), activeQueuedChick: chicks.activeQueuedChick });
|
uiSystem = createUISystem({ game, ui, build, startGame, beginCardDraft: () => cardSystem.showDraft(), activeQueuedChick: chicks.activeQueuedChick });
|
||||||
wireDebugControls(); initializeStaticText(); resetLayout(game); ensureMaintenanceState(game); refreshDebugUpgradeSelect(); chicks.updateCongestion(); uiSystem.updatePanels(); uiSystem.updateUI(); uiSystem.showTitle(); requestAnimationFrame(update);
|
initializeStaticText(); resetLayout(game); ensureMaintenanceState(game); chicks.updateCongestion(); uiSystem.updatePanels(); uiSystem.updateUI(); uiSystem.showTitle(); requestAnimationFrame(update);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Chick Sorter v11.0 Routing / Compact Upgrades</title>
|
<title>Chick Sorter v27.0</title>
|
||||||
<link rel="stylesheet" href="./styles.css" />
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -12,9 +12,9 @@
|
||||||
<canvas id="gameCanvas" width="1440" height="900" aria-label="Chick Sorter game canvas"></canvas>
|
<canvas id="gameCanvas" width="1440" height="900" aria-label="Chick Sorter game canvas"></canvas>
|
||||||
|
|
||||||
<div class="hud hud-top-left" aria-label="Run status">
|
<div class="hud hud-top-left" aria-label="Run status">
|
||||||
<div class="hud-card cash"><span>CASH</span><strong id="money">JPY 250</strong></div>
|
<div class="hud-card cash"><span>CASH</span><strong id="money">¥250</strong></div>
|
||||||
<div class="hud-card"><span>TIME</span><strong id="timeLeft">60.0s</strong></div>
|
<div class="hud-card"><span>TIME</span><strong id="timeLeft">60.0s</strong></div>
|
||||||
<div class="hud-card profit"><span>NET</span><strong id="turnProfit">JPY 0</strong></div>
|
<div class="hud-card profit"><span>NET</span><strong id="turnProfit">¥0</strong></div>
|
||||||
<div class="hud-card"><span>DAY</span><strong id="turn">1</strong></div>
|
<div class="hud-card"><span>DAY</span><strong id="turn">1</strong></div>
|
||||||
<div class="hud-card compact"><span>PHASE</span><strong id="phaseLabel">Title</strong></div>
|
<div class="hud-card compact"><span>PHASE</span><strong id="phaseLabel">Title</strong></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -35,8 +35,8 @@
|
||||||
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
|
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>CHICK SORTER <span>v11.0</span></h1>
|
<h1>CHICK SORTER <span>v27.0</span></h1>
|
||||||
<p>Build, drag, connect all receiver ports, then start the next day.</p>
|
<p>Build, drag, connect at least one valid EGG route, then start the next day.</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
|
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -45,14 +45,15 @@
|
||||||
<section class="equipment-section">
|
<section class="equipment-section">
|
||||||
<h2>Add / Edit Equipment</h2>
|
<h2>Add / Edit Equipment</h2>
|
||||||
<div class="large-tools">
|
<div class="large-tools">
|
||||||
<button id="buildConveyorButton" class="tool-button" type="button"><strong>Conveyor</strong><span>JPY 30 / tile</span></button>
|
<button id="buildConveyorButton" class="tool-button" type="button"><strong>Conveyor</strong><span>¥30 / tile</span></button>
|
||||||
<button id="buildEggFarmButton" class="tool-button" type="button"><strong>Egg Farm</strong><span>JPY 300</span></button>
|
<button id="buildEggFarmButton" class="tool-button" type="button"><strong>Egg Farm</strong><span>¥250</span></button>
|
||||||
<button id="buildAutoScannerButton" class="tool-button" type="button"><strong>Auto Scanner</strong><span>JPY 360 / 4.5s cooldown</span></button>
|
<button id="buildAutoScannerButton" class="tool-button" type="button"><strong>Auto Scanner</strong><span>¥360 / 2.25s cooldown</span></button>
|
||||||
<button id="buildManualScannerButton" class="tool-button" type="button"><strong>Manual Scanner</strong><span>JPY 260</span></button>
|
<button id="buildManualScannerButton" class="tool-button" type="button"><strong>Manual Scanner</strong><span>¥260</span></button>
|
||||||
<button id="buildMixerButton" class="tool-button" type="button"><strong>Mixer</strong><span>JPY 450</span></button>
|
<button id="buildMixerButton" class="tool-button" type="button"><strong>Mixer</strong><span>¥450</span></button>
|
||||||
<button id="buildTrashButton" class="tool-button" type="button"><strong>Waste Shredder</strong><span>JPY 240</span></button>
|
<button id="buildTrashButton" class="tool-button" type="button"><strong>Waste Shredder</strong><span>¥240</span></button>
|
||||||
<button id="buildTruckButton" class="tool-button" type="button"><strong>Truck</strong><span>JPY 450</span></button>
|
<button id="buildTruckButton" class="tool-button" type="button"><strong>Truck</strong><span>¥450</span></button>
|
||||||
<button id="eraseButton" class="tool-button danger" type="button"><strong>Erase</strong><span>Remove any equipment</span></button>
|
<button id="eraseButton" class="tool-button danger" type="button"><strong>Sell</strong><span>Sell or refund equipment</span></button>
|
||||||
|
<button id="hireRepairmanButton" class="tool-button repair" type="button"><strong>Hire Repairman</strong><span>¥100 / next day</span></button>
|
||||||
<div class="history-tools"><button id="undoButton" class="tool-button small" type="button"><strong>Undo</strong><span>Ctrl+Z</span></button><button id="redoButton" class="tool-button small" type="button"><strong>Redo</strong><span>Ctrl+Y</span></button></div>
|
<div class="history-tools"><button id="undoButton" class="tool-button small" type="button"><strong>Undo</strong><span>Ctrl+Z</span></button><button id="redoButton" class="tool-button small" type="button"><strong>Redo</strong><span>Ctrl+Y</span></button></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="buildStatus" class="mini-box">Build tools unlock after each day.</div>
|
<div id="buildStatus" class="mini-box">Build tools unlock after each day.</div>
|
||||||
|
|
@ -62,13 +63,7 @@
|
||||||
<h2>Irregular One-Day Event</h2>
|
<h2>Irregular One-Day Event</h2>
|
||||||
<div id="contractPanel" class="contract-card empty">Irregular forced events appear during Build phase.</div>
|
<div id="contractPanel" class="contract-card empty">Irregular forced events appear during Build phase.</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="rules-section compact-rules">
|
||||||
<section class="facility-section">
|
|
||||||
<h2>Selected Facility</h2>
|
|
||||||
<div id="facilityPanel" class="facility-panel-empty">Click equipment in Build phase.</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="rules-section compact-rules">
|
|
||||||
<h2>Status</h2>
|
<h2>Status</h2>
|
||||||
<div id="turnSummary" class="mini-box"></div>
|
<div id="turnSummary" class="mini-box"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -81,6 +76,20 @@
|
||||||
<button id="scanner2MixerButton" class="sort-button" type="button">S2 -> Waste</button>
|
<button id="scanner2MixerButton" class="sort-button" type="button">S2 -> Waste</button>
|
||||||
<button id="scanner2TruckButton" class="sort-button" type="button">S2 -> Truck</button>
|
<button id="scanner2TruckButton" class="sort-button" type="button">S2 -> Truck</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="hoverTooltip" class="hover-tooltip" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<details id="debugPanel" class="debug-panel">
|
||||||
|
<summary>DBG</summary>
|
||||||
|
<div class="debug-body">
|
||||||
|
<label><input id="debugInfiniteCash" type="checkbox" /> Infinite cash</label>
|
||||||
|
<label>Day <input id="debugDayInput" type="number" min="1" step="1" value="1" /></label>
|
||||||
|
<button id="debugSetDayButton" type="button">Set Day</button>
|
||||||
|
<label>Upgrade <select id="debugUpgradeSelect"></select></label>
|
||||||
|
<button id="debugApplyUpgradeButton" type="button">Free Buy</button>
|
||||||
|
<div id="debugObservations" class="debug-readout"></div>
|
||||||
|
<div id="debugEventLog" class="debug-readout event-log"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
@ -92,6 +101,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="./game.js?v=11.0-routing-compact-upgrades"></script>
|
<script type="module" src="./game.js?v=27.0-combo-settlement"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { key, parseKey, pointToCell, cellCenter, mixHex, randomBetween, yen } fr
|
||||||
import { getConveyorNeighbors, routeFromFarmToScanner, outputRoute, scannerCenter, scannerConnector, scannerOutputs, destinationLabel, destinationColor, scannerPortHasConveyor } from '../systems/routing.js';
|
import { getConveyorNeighbors, routeFromFarmToScanner, outputRoute, scannerCenter, scannerConnector, scannerOutputs, destinationLabel, destinationColor, scannerPortHasConveyor } from '../systems/routing.js';
|
||||||
import { upgradedMixerPrice, upgradedTruckPrice } from '../systems/economy.js';
|
import { upgradedMixerPrice, upgradedTruckPrice } from '../systems/economy.js';
|
||||||
import { cardTargetBounds } from '../systems/cards.js';
|
import { cardTargetBounds } from '../systems/cards.js';
|
||||||
import { wearRatio, remainingPercent } from '../systems/maintenance.js';
|
import { wearRatio } from '../systems/maintenance.js';
|
||||||
|
|
||||||
// Optional art overlay hook.
|
// Optional art overlay hook.
|
||||||
// Keep external art probing disabled by default so a clean checkout does not emit 404s for missing PNGs.
|
// Keep external art probing disabled by default so a clean checkout does not emit 404s for missing PNGs.
|
||||||
|
|
@ -12,7 +12,7 @@ const ENABLE_EXTERNAL_ART_ASSETS = false;
|
||||||
const ASSET_PATHS = {
|
const ASSET_PATHS = {
|
||||||
chickMale: './assets/images/chick_male.png',
|
chickMale: './assets/images/chick_male.png',
|
||||||
chickFemale: './assets/images/chick_female.png',
|
chickFemale: './assets/images/chick_female.png',
|
||||||
poop: './assets/images/tarinai.png',
|
poop: new URL('../../assets/images/poop.png', import.meta.url).href,
|
||||||
conveyor: './assets/images/conveyor.png',
|
conveyor: './assets/images/conveyor.png',
|
||||||
scannerManual: './assets/images/scanner_manual.png',
|
scannerManual: './assets/images/scanner_manual.png',
|
||||||
scannerAuto: './assets/images/scanner_auto.png',
|
scannerAuto: './assets/images/scanner_auto.png',
|
||||||
|
|
@ -27,7 +27,7 @@ for (const [name, src] of Object.entries(ASSET_PATHS)) {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.loaded = false;
|
img.loaded = false;
|
||||||
img.onerror = () => { img.loaded = false; };
|
img.onerror = () => { img.loaded = false; };
|
||||||
if (ENABLE_EXTERNAL_ART_ASSETS && src) {
|
if ((ENABLE_EXTERNAL_ART_ASSETS || name === 'poop') && src) {
|
||||||
img.onload = () => { img.loaded = true; };
|
img.onload = () => { img.loaded = true; };
|
||||||
img.src = src;
|
img.src = src;
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +56,6 @@ export function drawAll(ctx, canvas, game, helpers) {
|
||||||
drawSelectionBox(ctx, game);
|
drawSelectionBox(ctx, game);
|
||||||
drawEffects(ctx, game);
|
drawEffects(ctx, game);
|
||||||
drawFloatingTexts(ctx, game);
|
drawFloatingTexts(ctx, game);
|
||||||
if (game.debug) drawDebug(ctx, game);
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
drawCanvasHints(ctx, game, canvas);
|
drawCanvasHints(ctx, game, canvas);
|
||||||
}
|
}
|
||||||
|
|
@ -103,17 +102,13 @@ function drawGrid(ctx, game) {
|
||||||
ctx.strokeStyle = 'rgba(16,32,21,.60)';
|
ctx.strokeStyle = 'rgba(16,32,21,.60)';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
rect(ctx, c.x - GRID.cell / 2 + 4, c.y - GRID.cell / 2 + 4, GRID.cell - 8, GRID.cell - 8, true, true);
|
rect(ctx, c.x - GRID.cell / 2 + 4, c.y - GRID.cell / 2 + 4, GRID.cell - 8, GRID.cell - 8, true, true);
|
||||||
ctx.fillStyle = 'rgba(255,255,255,.70)';
|
|
||||||
ctx.font = '900 9px ui-monospace, monospace';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText('NO BUILD', c.x, c.y + 3);
|
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawDirtOverlay(ctx, x, y, w, h, target, radius = 0) {
|
function drawDirtOverlay(ctx, x, y, w, h, target, radius = 0) {
|
||||||
const wear = wearRatio(target);
|
const wear = wearRatio(target);
|
||||||
if (wear <= 0.01) return;
|
if (wear < 0.50) return;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalAlpha = Math.min(0.62, 0.12 + wear * 0.54);
|
ctx.globalAlpha = Math.min(0.62, 0.12 + wear * 0.54);
|
||||||
ctx.fillStyle = '#6b4a23';
|
ctx.fillStyle = '#6b4a23';
|
||||||
|
|
@ -174,6 +169,7 @@ function drawConveyors(ctx, game) {
|
||||||
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 2;
|
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 2;
|
||||||
rect(ctx, c.x - 8, c.y - 8, 16, 16, true, true);
|
rect(ctx, c.x - 8, c.y - 8, 16, 16, true, true);
|
||||||
drawDirtOverlay(ctx, c.x - 14, c.y - 14, 28, 28, { meta: game.conveyorMeta.get(k) });
|
drawDirtOverlay(ctx, c.x - 14, c.y - 14, 28, 28, { meta: game.conveyorMeta.get(k) });
|
||||||
|
drawSelection(ctx, game, 'conveyor', k, c.x, c.y, 38, 38);
|
||||||
drawConveyorDirectionMarkers(ctx, c, directionMarkers.get(k) || []);
|
drawConveyorDirectionMarkers(ctx, c, directionMarkers.get(k) || []);
|
||||||
if (ratio > 0.5) label(ctx, c.x, c.y - 15, `${Math.floor(ratio * 100)}%`, THEME.danger);
|
if (ratio > 0.5) label(ctx, c.x, c.y - 15, `${Math.floor(ratio * 100)}%`, THEME.danger);
|
||||||
}
|
}
|
||||||
|
|
@ -289,6 +285,7 @@ function drawScanner(ctx, scanner, game) {
|
||||||
const img = scanner.kind === 'auto' ? assets.scannerAuto : assets.scannerManual;
|
const img = scanner.kind === 'auto' ? assets.scannerAuto : assets.scannerManual;
|
||||||
if (drawImageIfLoaded(ctx, img, c.x - 54, c.y - 38, 108, 76)) {
|
if (drawImageIfLoaded(ctx, img, c.x - 54, c.y - 38, 108, 76)) {
|
||||||
if (scanner.kind === 'auto') drawDirtOverlay(ctx, c.x - 54, c.y - 38, 108, 76, scanner);
|
if (scanner.kind === 'auto') drawDirtOverlay(ctx, c.x - 54, c.y - 38, 108, 76, scanner);
|
||||||
|
else drawManualKeyboardIcon(ctx, scanner, c);
|
||||||
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 116, 84);
|
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 116, 84);
|
||||||
ctx.restore(); return;
|
ctx.restore(); return;
|
||||||
}
|
}
|
||||||
|
|
@ -297,17 +294,63 @@ function drawScanner(ctx, scanner, game) {
|
||||||
rect(ctx, c.x - 52, c.y - 36, 104, 72, true, true);
|
rect(ctx, c.x - 52, c.y - 36, 104, 72, true, true);
|
||||||
ctx.fillStyle = scanner.kind === 'auto' ? THEME.green : THEME.ink;
|
ctx.fillStyle = scanner.kind === 'auto' ? THEME.green : THEME.ink;
|
||||||
ctx.font = '900 13px ui-monospace, monospace'; ctx.textAlign = 'center';
|
ctx.font = '900 13px ui-monospace, monospace'; ctx.textAlign = 'center';
|
||||||
ctx.fillText(`${scanner.kind === 'auto' ? 'AUTO' : `S${(scanner.slot ?? 0) + 1}`}`, c.x, c.y - 12);
|
ctx.fillText(`${scanner.kind === 'auto' ? 'AUTO' : `S${(scanner.slot ?? 0) + 1}`}`, c.x, c.y - 14);
|
||||||
ctx.font = '900 10px ui-monospace, monospace';
|
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 + 7);
|
ctx.fillText(scanner.role === 0 ? 'A/L:M D/R:NEXT' : 'L:WASTE R:TRUCK', c.x, c.y + 3);
|
||||||
const q = scanner.queue.length;
|
const q = scanner.queue.length;
|
||||||
|
if (scanner.kind === 'manual') {
|
||||||
|
drawManualKeyboardIcon(ctx, scanner, c);
|
||||||
ctx.fillStyle = q > 0 ? THEME.green : THEME.muted;
|
ctx.fillStyle = q > 0 ? THEME.green : THEME.muted;
|
||||||
ctx.fillText(`Q:${q}${scanner.kind === 'auto' ? ` CD:${scanner.cooldown.toFixed(1)}` : ''}`, c.x, c.y + 25);
|
ctx.font = '900 8px ui-monospace, monospace';
|
||||||
if (scanner.kind === 'auto') drawDirtOverlay(ctx, c.x - 52, c.y - 36, 104, 72, scanner);
|
ctx.fillText(`Q:${q}`, c.x, c.y + 32);
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = q > 0 ? THEME.green : THEME.muted;
|
||||||
|
ctx.font = '900 10px ui-monospace, monospace';
|
||||||
|
ctx.fillText(`Q:${q} CD:${scanner.cooldown.toFixed(1)}`, c.x, c.y + 25);
|
||||||
|
drawDirtOverlay(ctx, c.x - 52, c.y - 36, 104, 72, scanner);
|
||||||
|
}
|
||||||
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 116, 84);
|
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 116, 84);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawManualKeyboardIcon(ctx, scanner, c) {
|
||||||
|
if (scanner.kind !== 'manual') return;
|
||||||
|
const slot = scanner.slot ?? 0;
|
||||||
|
const labels = slot === 0
|
||||||
|
? [{ side: 'left', text: 'A' }, { side: 'right', text: 'D' }]
|
||||||
|
: [{ side: 'left', text: '←' }, { side: 'right', text: '→' }];
|
||||||
|
const pressedSide = scanner.keyPressTime > 0 ? scanner.keyPressSide : null;
|
||||||
|
const baseX = c.x - 30;
|
||||||
|
const baseY = c.y + 8;
|
||||||
|
ctx.save();
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.fillStyle = '#f7fff5';
|
||||||
|
ctx.strokeStyle = THEME.ink;
|
||||||
|
rect(ctx, baseX - 4, baseY - 4, 68, 25, true, true);
|
||||||
|
ctx.font = '900 13px ui-monospace, monospace';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
for (let i = 0; i < labels.length; i += 1) {
|
||||||
|
const keyDef = labels[i];
|
||||||
|
const pressed = pressedSide === keyDef.side;
|
||||||
|
const x = baseX + i * 32;
|
||||||
|
const y = baseY + (pressed ? 3 : 0);
|
||||||
|
ctx.fillStyle = pressed ? THEME.greenSoft : THEME.white;
|
||||||
|
ctx.strokeStyle = THEME.ink;
|
||||||
|
ctx.lineWidth = pressed ? 4 : 3;
|
||||||
|
rect(ctx, x, y, 28, 17, true, true);
|
||||||
|
if (!pressed) {
|
||||||
|
ctx.fillStyle = 'rgba(16,32,21,.18)';
|
||||||
|
rect(ctx, x + 3, y + 17, 28, 3, true, false);
|
||||||
|
}
|
||||||
|
ctx.fillStyle = THEME.ink;
|
||||||
|
ctx.fillText(keyDef.text, x + 14, y + 13);
|
||||||
|
}
|
||||||
|
ctx.fillStyle = THEME.muted;
|
||||||
|
ctx.font = '900 6px ui-monospace, monospace';
|
||||||
|
ctx.fillText('KEYBOARD', c.x, baseY - 7);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
function targetForTruck(game) {
|
function targetForTruck(game) {
|
||||||
return game.contractActive?.target || game.contractOffer?.target || 'female';
|
return game.contractActive?.target || game.contractOffer?.target || 'female';
|
||||||
}
|
}
|
||||||
|
|
@ -416,7 +459,7 @@ function drawTruck(ctx, game) {
|
||||||
const t = game.facilities.truck;
|
const t = game.facilities.truck;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
drawExternalDuct(ctx, t);
|
drawExternalDuct(ctx, t);
|
||||||
if (drawImageIfLoaded(ctx, assets.truck, t.x, t.y, t.w, t.h)) { ctx.restore(); return; }
|
if (drawImageIfLoaded(ctx, assets.truck, t.x, t.y, t.w, t.h)) { drawSelection(ctx, game, 'facility', 'truck', t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12); ctx.restore(); return; }
|
||||||
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4; rect(ctx, t.x, t.y, t.w, t.h, true, true);
|
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4; rect(ctx, t.x, t.y, t.w, t.h, true, true);
|
||||||
ctx.fillStyle = THEME.ink; ctx.font = '900 15px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`TRUCK L${t.level}`, t.x + t.w / 2, t.y + 24);
|
ctx.fillStyle = THEME.ink; ctx.font = '900 15px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`TRUCK L${t.level}`, t.x + t.w / 2, t.y + 24);
|
||||||
ctx.font = '900 11px ui-monospace, monospace'; ctx.fillText(`UNIT ${yen(upgradedTruckPrice(game))}`, t.x + t.w / 2, t.y + 42);
|
ctx.font = '900 11px ui-monospace, monospace'; ctx.fillText(`UNIT ${yen(upgradedTruckPrice(game))}`, t.x + t.w / 2, t.y + 42);
|
||||||
|
|
@ -482,7 +525,13 @@ function isSelected(game, type, id) {
|
||||||
}
|
}
|
||||||
function drawSelection(ctx, game, type, id, x, y, w, h) {
|
function drawSelection(ctx, game, type, id, x, y, w, h) {
|
||||||
if (!isSelected(game, type, id)) return;
|
if (!isSelected(game, type, id)) return;
|
||||||
ctx.save(); ctx.strokeStyle = THEME.green; ctx.lineWidth = 4; ctx.setLineDash([8, 5]); ctx.strokeRect(x - w / 2, y - h / 2, w, h); ctx.restore();
|
ctx.save();
|
||||||
|
ctx.fillStyle = 'rgba(34,185,79,.18)';
|
||||||
|
ctx.strokeStyle = THEME.green;
|
||||||
|
ctx.lineWidth = (game.multiSelected || []).length > 1 ? 6 : 4;
|
||||||
|
ctx.setLineDash((game.multiSelected || []).length > 1 ? [] : [8, 5]);
|
||||||
|
rect(ctx, x - w / 2, y - h / 2, w, h, true, true);
|
||||||
|
ctx.restore();
|
||||||
}
|
}
|
||||||
function drawSelectedTooltip(ctx, game, selectedObject, selectedTitle) {
|
function drawSelectedTooltip(ctx, game, selectedObject, selectedTitle) {
|
||||||
if (game.phase !== 'build') return;
|
if (game.phase !== 'build') return;
|
||||||
|
|
@ -501,7 +550,15 @@ function drawCardTargetOverlay(ctx, canvas, game) {
|
||||||
// The canvas is already translated by the camera. Offset the dark layer back to screen space
|
// The canvas is already translated by the camera. Offset the dark layer back to screen space
|
||||||
// so every non-candidate object is reliably dimmed even after panning.
|
// so every non-candidate object is reliably dimmed even after panning.
|
||||||
ctx.fillStyle = 'rgba(0,0,0,.66)';
|
ctx.fillStyle = 'rgba(0,0,0,.66)';
|
||||||
ctx.fillRect(-game.view.x, -game.view.y, canvas.width, canvas.height);
|
ctx.beginPath();
|
||||||
|
ctx.rect(-game.view.x, -game.view.y, canvas.width, canvas.height);
|
||||||
|
for (const item of items) {
|
||||||
|
const b = item.bounds;
|
||||||
|
const pad = 8;
|
||||||
|
ctx.rect(b.x - pad, b.y - pad, b.w + pad * 2, b.h + pad * 2);
|
||||||
|
}
|
||||||
|
ctx.fill('evenodd');
|
||||||
|
const isBlockedCellMode = game.cardTargetPick?.mode === 'blockedCell';
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
ctx.fillStyle = 'rgba(255,255,255,.95)';
|
ctx.fillStyle = 'rgba(255,255,255,.95)';
|
||||||
ctx.strokeStyle = THEME.ink;
|
ctx.strokeStyle = THEME.ink;
|
||||||
|
|
@ -510,38 +567,20 @@ function drawCardTargetOverlay(ctx, canvas, game) {
|
||||||
ctx.fillStyle = THEME.danger;
|
ctx.fillStyle = THEME.danger;
|
||||||
ctx.font = '900 12px ui-monospace, monospace';
|
ctx.font = '900 12px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText('NO VALID UPGRADE TARGET. PRESS ESC.', 470 - game.view.x, 122 - game.view.y);
|
ctx.fillText(isBlockedCellMode ? 'NO BLOCKED CELL. PRESS ESC.' : 'NO VALID UPGRADE TARGET. PRESS ESC.', 470 - game.view.x, 122 - game.view.y);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const item of items) {
|
ctx.fillStyle = 'rgba(255,255,255,.95)';
|
||||||
const b = item.bounds;
|
|
||||||
ctx.save();
|
|
||||||
ctx.shadowColor = THEME.white;
|
|
||||||
ctx.shadowBlur = 20;
|
|
||||||
ctx.fillStyle = 'rgba(255,255,255,.32)';
|
|
||||||
ctx.strokeStyle = THEME.white;
|
|
||||||
ctx.lineWidth = 8;
|
|
||||||
ctx.setLineDash([10, 6]);
|
|
||||||
rect(ctx, b.x - 10, b.y - 10, b.w + 20, b.h + 20, true, true);
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
ctx.strokeStyle = THEME.green;
|
|
||||||
ctx.lineWidth = 4;
|
|
||||||
ctx.strokeRect(b.x - 4, b.y - 4, b.w + 8, b.h + 8);
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(255,255,255,.96)';
|
|
||||||
ctx.strokeStyle = THEME.ink;
|
ctx.strokeStyle = THEME.ink;
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
const labelW = Math.min(300, Math.max(150, item.label.length * 7));
|
const remaining = Math.max(1, game.cardTargetPick?.remaining || 1);
|
||||||
rect(ctx, b.cx - labelW / 2, b.y - 34, labelW, 24, true, true);
|
const msg = isBlockedCellMode ? `CLICK BLOCKED CELL: ${remaining} LEFT. PRESS ESC TO CANCEL.` : 'CLICK AN UPGRADEABLE MACHINE. PRESS ESC TO CANCEL.';
|
||||||
|
rect(ctx, 216 - game.view.x, 96 - game.view.y, 560, 42, true, true);
|
||||||
ctx.fillStyle = THEME.ink;
|
ctx.fillStyle = THEME.ink;
|
||||||
ctx.font = '900 10px ui-monospace, monospace';
|
ctx.font = '900 12px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(item.label, b.cx, b.y - 18);
|
ctx.fillText(msg, 496 - game.view.x, 122 - game.view.y);
|
||||||
}
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -586,17 +625,35 @@ function drawFloatingTexts(ctx, game) {
|
||||||
|
|
||||||
function drawRepairman(ctx, game) {
|
function drawRepairman(ctx, game) {
|
||||||
const r = game.repairman;
|
const r = game.repairman;
|
||||||
if (!r?.active) return;
|
const visible = !!r?.active || (game.phase === 'build' && !!r?.hiredForNextDay);
|
||||||
|
if (!visible) return;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
ctx.lineWidth = 3;
|
||||||
ctx.fillStyle = '#fff0d1';
|
ctx.fillStyle = '#fff0d1';
|
||||||
ctx.strokeStyle = THEME.ink;
|
ctx.strokeStyle = THEME.ink;
|
||||||
|
ctx.beginPath(); ctx.arc(r.x, r.y, 17, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||||
|
ctx.fillStyle = '#ffd6e7';
|
||||||
|
ctx.beginPath(); ctx.arc(r.x - 6, r.y - 2, 3.2, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.beginPath(); ctx.arc(r.x + 6, r.y - 2, 3.2, 0, Math.PI * 2); ctx.fill();
|
||||||
|
const exploited = !!game.cardEffects?.laborExploitation;
|
||||||
|
ctx.fillStyle = exploited ? THEME.danger : THEME.ink;
|
||||||
|
ctx.beginPath(); ctx.arc(r.x - 5, r.y - 5, exploited ? 2.8 : 2.2, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.beginPath(); ctx.arc(r.x + 5, r.y - 5, exploited ? 2.8 : 2.2, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.strokeStyle = THEME.ink;
|
||||||
|
ctx.lineWidth = exploited ? 3 : 2;
|
||||||
|
ctx.beginPath(); ctx.arc(r.x, r.y + (exploited ? 0 : 1), exploited ? 8 : 7, 0.10 * Math.PI, 0.90 * Math.PI); ctx.stroke();
|
||||||
|
ctx.fillStyle = THEME.green;
|
||||||
|
rect(ctx, r.x - 9, r.y + 14, 18, 8, true, true);
|
||||||
|
ctx.strokeStyle = THEME.ink;
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
ctx.beginPath(); ctx.arc(r.x, r.y, 15, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
ctx.beginPath(); ctx.moveTo(r.x - 11, r.y + 18); ctx.lineTo(r.x - 20, r.y + 24); ctx.stroke();
|
||||||
|
ctx.beginPath(); ctx.moveTo(r.x + 11, r.y + 18); ctx.lineTo(r.x + 20, r.y + 24); ctx.stroke();
|
||||||
ctx.fillStyle = THEME.ink;
|
ctx.fillStyle = THEME.ink;
|
||||||
ctx.font = '900 10px ui-monospace, monospace';
|
ctx.font = '900 9px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText('R', r.x, r.y + 4);
|
ctx.fillText(r.active ? 'FIX' : 'HIRED', r.x, r.y + 33);
|
||||||
if (r.target?.label) label(ctx, r.x, r.y - 22, r.target.label, THEME.green);
|
if (r.target?.label) label(ctx, r.x, r.y - 24, r.target.label, THEME.green);
|
||||||
|
else if (!r.active && r.hiredForNextDay) label(ctx, r.x, r.y - 24, 'NEXT DAY', THEME.green);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -610,13 +667,8 @@ function drawCanvasHints(ctx, game, canvas) {
|
||||||
ctx.font = '900 13px ui-monospace, monospace'; ctx.fillStyle = THEME.ink;
|
ctx.font = '900 13px ui-monospace, monospace'; ctx.fillStyle = THEME.ink;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
if (game.phase === 'running') lines.push(game.timeLeft <= 0 ? 'TIME UP: FARMS SHUT. CLEAR LINE.' : 'A/D: S1 ←/→: S2');
|
if (game.phase === 'running') lines.push(game.timeLeft <= 0 ? 'TIME UP: FARMS SHUT. CLEAR LINE.' : 'A/D: S1 ←/→: S2');
|
||||||
if (game.phase === 'build') lines.push(`BUILD: ${game.buildTool || 'SELECT'} | connect all receiver ports`);
|
if (game.phase === 'build') lines.push(`BUILD: ${game.buildTool || 'SELECT'} | connect 1 EGG route to any exit`);
|
||||||
let y = 76;
|
let y = 76;
|
||||||
for (const line of lines) { ctx.strokeStyle = THEME.white; ctx.lineWidth = 4; ctx.strokeText(line, 24, y); ctx.fillText(line, 24, y); y += 20; }
|
for (const line of lines) { ctx.strokeStyle = THEME.white; ctx.lineWidth = 4; ctx.strokeText(line, 24, y); ctx.fillText(line, 24, y); y += 20; }
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
function drawDebug(ctx, game) {
|
|
||||||
ctx.save(); ctx.font = '900 9px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillStyle = THEME.danger;
|
|
||||||
for (const k of game.conveyorTiles) { const { col, row } = parseKey(k); const c = cellCenter(col, row); const comp = game.componentLookup?.get(k) || '?'; ctx.fillText(`${col},${row}/C${comp}`, c.x, c.y + 24); }
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
910
src/styles.css
910
src/styles.css
|
|
@ -1,367 +1,135 @@
|
||||||
:root {
|
:root {
|
||||||
--bg: #020303;
|
--bg: #f2fff0;
|
||||||
--screen: #050607;
|
--screen: #eefbea;
|
||||||
--panel: rgba(10, 12, 14, 0.94);
|
--panel: rgba(245, 255, 242, 0.96);
|
||||||
--panel-solid: #0b0d0f;
|
--ink: #102015;
|
||||||
--panel-2: #111417;
|
--muted: #526456;
|
||||||
--line: rgba(132, 140, 148, 0.18);
|
--line: #102015;
|
||||||
--line-strong: rgba(177, 185, 193, 0.62);
|
--green: #22b94f;
|
||||||
--text: #d7dadd;
|
--green-soft: #b7f5c6;
|
||||||
--muted: #8d949b;
|
--white: #ffffff;
|
||||||
--accent: #b4bac0;
|
--danger: #c42424;
|
||||||
--accent-2: #b99561;
|
--shadow: 8px 8px 0 rgba(16, 32, 21, 0.22);
|
||||||
--danger: #d66d6d;
|
|
||||||
--good: #9fd27d;
|
|
||||||
--shadow: 0 22px 70px rgba(0, 0, 0, 0.70);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
html, body { min-height: 100%; }
|
html, body { min-height: 100%; }
|
||||||
body {
|
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; }
|
||||||
margin: 0;
|
|
||||||
background:
|
|
||||||
linear-gradient(90deg, rgba(255,255,255,0.012) 1px, transparent 1px),
|
|
||||||
linear-gradient(180deg, rgba(255,255,255,0.010) 1px, transparent 1px),
|
|
||||||
#020303;
|
|
||||||
background-size: 24px 24px;
|
|
||||||
color: var(--text);
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
button { font: inherit; }
|
button { font: inherit; }
|
||||||
h1, h2, p { margin: 0; }
|
h1, h2, p { margin: 0; }
|
||||||
|
#app { width: min(100vw, 1680px); margin: 0 auto; padding: 0 8px 12px; }
|
||||||
#app {
|
.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); }
|
||||||
width: min(100vw, 1680px);
|
#gameCanvas { display: block; width: 100%; height: min(calc(100vh - 10px), 900px); min-height: 720px; background: var(--screen); touch-action: none; }
|
||||||
margin: 0 auto;
|
.hud { position: absolute; z-index: 4; display: grid; gap: 6px; pointer-events: none; }
|
||||||
padding: 0 8px 12px;
|
.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); }
|
||||||
.game-shell {
|
.hud-card.primary { min-width: 116px; padding: 10px 12px; background: rgba(255,255,255,.96); }
|
||||||
position: relative;
|
.hud-card.primary span { font-size: 9px; }
|
||||||
width: 100%;
|
.hud-card.primary strong { font-size: clamp(22px, 2vw, 32px); }
|
||||||
margin-top: 0;
|
.hud-card.time-critical { background: #fff0d1; box-shadow: inset 0 0 0 4px var(--danger), 4px 4px 0 rgba(16,32,21,.16); }
|
||||||
min-height: min(100vh, 920px);
|
.hud-card span { display: block; font-size: 9px; letter-spacing: .1em; color: var(--muted); margin-bottom: 4px; }
|
||||||
border: 1px solid #30363d;
|
.hud-card strong { font-size: clamp(15px, 1.3vw, 22px); line-height: 1; color: var(--ink); }
|
||||||
border-top: 0;
|
.hud-card.cash strong, .hud-card.profit strong, .cash-positive { color: var(--green) !important; }
|
||||||
border-radius: 0;
|
|
||||||
background: var(--screen);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-shell::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
background: linear-gradient(180deg, rgba(255,255,255,0.035), transparent 90px);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#gameCanvas {
|
|
||||||
position: relative;
|
|
||||||
z-index: 0;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: min(calc(100vh - 10px), 900px);
|
|
||||||
min-height: 660px;
|
|
||||||
background: #050607;
|
|
||||||
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(78px, auto));
|
|
||||||
}
|
|
||||||
.hud-top-right {
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
grid-template-columns: repeat(4, minmax(78px, auto));
|
|
||||||
}
|
|
||||||
.hud-card {
|
|
||||||
min-width: 78px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
border: 1px solid rgba(82, 91, 100, 0.76);
|
|
||||||
border-radius: 0;
|
|
||||||
background: rgba(9, 11, 13, 0.92);
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.018), 0 8px 20px rgba(0, 0, 0, 0.26);
|
|
||||||
backdrop-filter: blur(3px);
|
|
||||||
}
|
|
||||||
.hud-card span {
|
|
||||||
display: block;
|
|
||||||
font-size: 9px;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: 0.09em;
|
|
||||||
color: #7c858e;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.hud-card strong {
|
|
||||||
font-size: clamp(15px, 1.35vw, 21px);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
.hud-card.cash strong { color: #d7b27d; }
|
|
||||||
.hud-card.compact strong { font-size: clamp(13px, 1.1vw, 18px); }
|
|
||||||
.hud-card.profit strong { color: var(--good); }
|
|
||||||
|
|
||||||
.cash-negative { color: var(--danger) !important; }
|
.cash-negative { color: var(--danger) !important; }
|
||||||
.cash-positive { color: var(--good) !important; }
|
|
||||||
|
|
||||||
.upgrade-panel {
|
.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; }
|
||||||
position: absolute;
|
.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; }
|
||||||
z-index: 5;
|
.event-brief.active { background: #fff0d1; box-shadow: inset 0 0 0 3px var(--danger), 4px 4px 0 rgba(16,32,21,.14); }
|
||||||
left: 10px;
|
.event-brief.next { background: var(--green-soft); box-shadow: inset 0 0 0 3px var(--green), 4px 4px 0 rgba(16,32,21,.14); }
|
||||||
right: 10px;
|
.event-brief.locked { background: #edf3ec; color: var(--muted); }
|
||||||
bottom: 64px;
|
.target-brief { color: var(--green); }
|
||||||
display: grid;
|
.speed-brief { color: var(--ink); }
|
||||||
gap: 9px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #343a40;
|
|
||||||
border-radius: 0;
|
|
||||||
background: rgba(8, 10, 12, 0.94);
|
|
||||||
box-shadow: 0 20px 55px rgba(0, 0, 0, 0.56);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
.panel-head {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
.panel-head h1 {
|
|
||||||
font-size: 18px;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
}
|
|
||||||
.panel-head h1 span {
|
|
||||||
color: #050607;
|
|
||||||
font-size: 11px;
|
|
||||||
background: #b4bac0;
|
|
||||||
border-radius: 0;
|
|
||||||
padding: 2px 7px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.panel-head p,
|
|
||||||
.mini-box,
|
|
||||||
.upgrade-desc { color: var(--muted); }
|
|
||||||
.panel-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 260px minmax(420px, 1fr) 340px;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.upgrade-panel h2 {
|
|
||||||
color: var(--accent);
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.10em;
|
|
||||||
margin-bottom: 7px;
|
|
||||||
}
|
|
||||||
.tool-list,
|
|
||||||
.upgrade-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 7px;
|
|
||||||
}
|
|
||||||
.tool-list { grid-template-columns: 1fr 1fr; }
|
|
||||||
.tool-button,
|
|
||||||
.primary-button,
|
|
||||||
.sort-button,
|
|
||||||
.buy-button {
|
|
||||||
border-radius: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 850;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.035em;
|
|
||||||
transition: transform 0.06s ease, filter 0.12s ease, opacity 0.12s ease, border-color 0.12s ease;
|
|
||||||
}
|
|
||||||
.tool-button:active,
|
|
||||||
.primary-button:active,
|
|
||||||
.sort-button:active,
|
|
||||||
.buy-button:active { transform: translateY(1px); }
|
|
||||||
.tool-button:disabled,
|
|
||||||
.primary-button:disabled,
|
|
||||||
.sort-button:disabled,
|
|
||||||
.buy-button:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.38;
|
|
||||||
}
|
|
||||||
.tool-button {
|
|
||||||
color: var(--text);
|
|
||||||
background: #111417;
|
|
||||||
border: 1px solid rgba(82, 91, 100, 0.76);
|
|
||||||
padding: 8px 9px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
.tool-button.active {
|
|
||||||
border-color: #b4bac0;
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(180, 186, 192, 0.35);
|
|
||||||
}
|
|
||||||
.tool-button.danger.active {
|
|
||||||
border-color: var(--danger);
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(214, 109, 109, 0.30);
|
|
||||||
}
|
|
||||||
.primary-button {
|
|
||||||
border: 1px solid #b4bac0;
|
|
||||||
padding: 9px 16px;
|
|
||||||
background: #b4bac0;
|
|
||||||
color: #050607;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.upgrade-list {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
.upgrade-item {
|
|
||||||
border: 1px solid #30363d;
|
|
||||||
border-radius: 0;
|
|
||||||
padding: 8px;
|
|
||||||
background: #0f1215;
|
|
||||||
}
|
|
||||||
.upgrade-head {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.upgrade-head strong { font-size: 12px; }
|
|
||||||
.upgrade-level { color: var(--muted); font-size: 11px; white-space: nowrap; }
|
|
||||||
.upgrade-desc { font-size: 11px; line-height: 1.28; margin-bottom: 7px; }
|
|
||||||
.buy-button {
|
|
||||||
color: var(--text);
|
|
||||||
background: #1a1f24;
|
|
||||||
border: 1px solid #4b535b;
|
|
||||||
padding: 6px 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
.buy-button:not(:disabled):hover,
|
|
||||||
.tool-button:not(:disabled):hover,
|
|
||||||
.sort-button:not(:disabled):hover { filter: brightness(1.14); }
|
|
||||||
.mini-box {
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid #30363d;
|
|
||||||
border-radius: 0;
|
|
||||||
background: #080a0c;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.45;
|
|
||||||
}
|
|
||||||
.mini-box strong { color: var(--text); }
|
|
||||||
|
|
||||||
.control-dock {
|
.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; }
|
||||||
position: absolute;
|
.panel-head { display: grid; gap: 8px; margin-bottom: 10px; }
|
||||||
z-index: 7;
|
.panel-head h1 { font-size: 15px; letter-spacing: .06em; }
|
||||||
left: 50%;
|
.panel-head h1 span { font-size: 9px; color: var(--white); background: var(--green); border: 2px solid var(--line); padding: 1px 6px; }
|
||||||
bottom: 10px;
|
.panel-head p { color: var(--muted); margin-top: 4px; font-size: 9px; line-height: 1.25; }
|
||||||
transform: translateX(-50%);
|
.panel-grid { display: grid; grid-template-columns: 1fr; gap: 10px; }
|
||||||
display: grid;
|
.build-panel h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .1em; margin-bottom: 7px; }
|
||||||
grid-template-columns: repeat(4, 150px);
|
.large-tools { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
||||||
gap: 8px;
|
.history-tools { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; grid-column: 1 / -1; }
|
||||||
padding: 6px;
|
.tool-button.small { min-height: 40px; padding: 6px; }
|
||||||
border: 1px solid #343a40;
|
.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); }
|
||||||
background: rgba(8, 10, 12, 0.92);
|
.tool-button { min-height: 50px; text-align: left; padding: 8px; }
|
||||||
}
|
.tool-button strong { display: block; font-size: 12px; }
|
||||||
.sort-button {
|
.tool-button span { display: block; margin-top: 4px; font-size: 9px; color: var(--muted); }
|
||||||
border: 1px solid #4b535b;
|
.tool-button.active { background: var(--green-soft); box-shadow: inset 0 0 0 3px var(--green), 4px 4px 0 rgba(16,32,21,.18); }
|
||||||
min-height: 46px;
|
.tool-button.danger.active { background: #ffdada; box-shadow: inset 0 0 0 3px var(--danger), 4px 4px 0 rgba(16,32,21,.18); }
|
||||||
font-size: 12px;
|
.primary-button { padding: 10px 12px; background: var(--green); color: var(--white); white-space: nowrap; }
|
||||||
color: #f0f1f2;
|
.primary-button:disabled, .tool-button:disabled, .sort-button:disabled, .facility-action:disabled { opacity: .42; cursor: not-allowed; }
|
||||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.025);
|
.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; }
|
||||||
.sort-button.mixer {
|
.facility-card p { margin: 4px 0; }
|
||||||
background: #1b2025;
|
.facility-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||||
}
|
.facility-action { padding: 7px 9px; font-size: 9px; }
|
||||||
.sort-button.truck {
|
.facility-action.good { background: var(--green-soft); }
|
||||||
background: #2a2118;
|
.facility-action.warn { background: #fff0d1; }
|
||||||
border-color: #705a3c;
|
.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 {
|
.modal { position: fixed; inset: 0; z-index: 20; display: none; align-items: center; justify-content: center; padding: 22px; background: rgba(16,32,21,.45); }
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 20;
|
|
||||||
display: none;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 22px;
|
|
||||||
background: rgba(0, 0, 0, 0.72);
|
|
||||||
}
|
|
||||||
.modal.visible { display: flex; }
|
.modal.visible { display: flex; }
|
||||||
.modal-card {
|
.modal-card { width: min(860px, 96vw); border: 4px solid var(--line); padding: 24px; background: var(--white); box-shadow: var(--shadow); }
|
||||||
width: min(720px, 96vw);
|
.modal-card h2 { font-size: 26px; text-transform: uppercase; margin-bottom: 12px; }
|
||||||
border: 1px solid #4b535b;
|
.modal-card p { line-height: 1.55; }
|
||||||
border-radius: 0;
|
.modal-card ul { margin: 12px 0 0; padding-left: 22px; line-height: 1.55; color: var(--muted); }
|
||||||
padding: 22px;
|
.modal-actions { margin-top: 18px; display: flex; justify-content: flex-end; gap: 10px; flex-wrap: wrap; }
|
||||||
background: #090b0d;
|
.route-cards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 14px 0; }
|
||||||
box-shadow: 0 34px 110px rgba(0, 0, 0, 0.78);
|
.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; }
|
||||||
.modal-card h2 {
|
.route-card strong, .route-card em { font-style: normal; font-weight: 900; text-transform: uppercase; }
|
||||||
font-size: 24px;
|
.route-card b { font-size: 24px; color: var(--green); text-align: center; }
|
||||||
margin-bottom: 12px;
|
.route-card small { grid-column: 2 / span 3; color: var(--muted); font-weight: 900; }
|
||||||
letter-spacing: 0.06em;
|
.result-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 12px; }
|
||||||
text-transform: uppercase;
|
.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; }
|
||||||
.modal-card p { color: var(--text); line-height: 1.55; }
|
.result-grid span { font-size: 16px; font-weight: 900; color: var(--ink); }
|
||||||
.modal-card ul {
|
.game-shell:not(.phase-build) .build-panel { display: none; }
|
||||||
margin: 12px 0 0;
|
.game-shell.phase-running .hud-top-right { display: none; }
|
||||||
padding-left: 22px;
|
.game-shell:not(.phase-running) .control-dock { display: none; }
|
||||||
color: var(--muted);
|
@media (max-width: 1180px) {
|
||||||
line-height: 1.55;
|
|
||||||
}
|
|
||||||
.modal-card strong { color: var(--text); }
|
|
||||||
.modal-actions {
|
|
||||||
margin-top: 18px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
#app { padding: 0; }
|
#app { padding: 0; }
|
||||||
.game-shell { min-height: 100vh; }
|
.game-shell { min-height: 100vh; }
|
||||||
#gameCanvas { height: 100vh; min-height: 680px; }
|
#gameCanvas { height: 100vh; }
|
||||||
.hud-top-left,
|
.hud-top-left, .hud-top-right { grid-template-columns: repeat(2, minmax(76px, auto)); }
|
||||||
.hud-top-right { grid-template-columns: repeat(2, minmax(74px, auto)); }
|
.priority-strip { top: 150px; left: 8px; right: 8px; max-width: none; grid-template-columns: 1fr; }
|
||||||
.panel-grid { grid-template-columns: 1fr; }
|
.build-panel { top: auto; left: 8px; right: 8px; bottom: 8px; width: auto; max-height: 42vh; }
|
||||||
.upgrade-list { grid-template-columns: 1fr; }
|
.large-tools { grid-template-columns: 1fr 1fr; }
|
||||||
.upgrade-panel { max-height: 44vh; overflow: auto; }
|
|
||||||
.control-dock { grid-template-columns: repeat(2, 150px); }
|
.control-dock { grid-template-columns: repeat(2, 150px); }
|
||||||
|
.route-cards, .result-grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* v6 visibility pass: play-first layering */
|
.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); }
|
||||||
.game-shell:not(.phase-build) .upgrade-panel {
|
.contract-card.empty { color: var(--muted); background: rgba(255,255,255,.72); }
|
||||||
display: none;
|
.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; }
|
||||||
.game-shell.phase-running .hud-top-right {
|
.contract-card p { margin: 5px 0; }
|
||||||
display: none;
|
.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); }
|
||||||
.game-shell:not(.phase-running) .control-dock {
|
.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; }
|
||||||
display: none;
|
.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); }
|
||||||
.game-shell.phase-build .upgrade-panel {
|
.contract-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
|
||||||
bottom: 10px;
|
.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; }
|
||||||
.game-shell.phase-running .hud-card {
|
.muted-title { color: var(--muted); border: 3px solid var(--line); padding: 10px; background: #f7fff5; }
|
||||||
background: rgba(5, 7, 8, 0.84);
|
|
||||||
border-color: rgba(124, 133, 142, 0.42);
|
.compact-rules .mini-box { padding: 8px; font-size: 9px; }
|
||||||
}
|
.primary-button:disabled { filter: grayscale(1); background: #c9d2ca; color: #526456; }
|
||||||
.formula-box {
|
.formula-box {
|
||||||
border: 1px solid #30363d;
|
border: 3px solid var(--line);
|
||||||
background: #101418;
|
background: #f7fff5;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: var(--text);
|
color: var(--ink);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.equipment-menu-lines {
|
.equipment-menu-lines {
|
||||||
border: 1px solid #30363d;
|
border: 3px solid var(--line);
|
||||||
background: #080a0c;
|
background: #ffffff;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|
@ -381,12 +149,13 @@ h1, h2, p { margin: 0; }
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: var(--popover-x, 16px);
|
left: var(--popover-x, 16px);
|
||||||
top: var(--popover-y, 16px);
|
top: var(--popover-y, 16px);
|
||||||
width: min(320px, calc(100vw - 24px));
|
width: min(250px, calc(100vw - 24px));
|
||||||
max-height: min(360px, calc(100vh - 24px));
|
max-height: min(280px, calc(100vh - 24px));
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
|
border-width: 3px;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
box-shadow: 6px 6px 0 rgba(0,0,0,.28);
|
box-shadow: 6px 6px 0 rgba(16,32,21,.18);
|
||||||
}
|
}
|
||||||
.modal.equipment-popover .modal-card h2 {
|
.modal.equipment-popover .modal-card h2 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
@ -398,16 +167,457 @@ h1, h2, p { margin: 0; }
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
.modal.equipment-popover .modal-actions button {
|
.modal.equipment-popover .modal-actions button {
|
||||||
padding: 7px 9px;
|
padding: 5px 7px;
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
.equipment-menu-lines.compact {
|
.equipment-menu-lines.compact {
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
.formula-box.compact {
|
.formula-box.compact {
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
font-size: 10px;
|
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; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { MACHINE_FACILITY_IDS, GRID, THEME } from '../core/config.js';
|
import { MACHINE_FACILITY_IDS, GRID, THEME } from '../core/config.js';
|
||||||
import { BALANCE } from '../core/balance.js';
|
|
||||||
import { getSpawnRange } from '../core/state.js';
|
import { getSpawnRange } from '../core/state.js';
|
||||||
import { createEggFarm, createScanner, createFacility } from '../core/entities.js';
|
import { createEggFarm, createScanner, createFacility } from '../core/entities.js';
|
||||||
import { key, parseKey, pointToCell, cellCenter, yen } from '../core/utils.js';
|
import { key, parseKey, pointToCell, cellCenter, yen } from '../core/utils.js';
|
||||||
|
|
@ -10,8 +9,7 @@ import { autoScannerCooldownSeconds } from './cards.js';
|
||||||
import { record } from './history.js';
|
import { record } from './history.js';
|
||||||
import { floating, eraseEffect } from './effects.js';
|
import { floating, eraseEffect } from './effects.js';
|
||||||
import { createSelectionSystem } from './selectionSystem.js';
|
import { createSelectionSystem } from './selectionSystem.js';
|
||||||
import { emitGameEvent } from './gameEvents.js';
|
import { ensureMaintenanceState, remainingPercent, performanceFactor, autoScannerDelayMultiplier, eggSpawnDelayMultiplier, facilityProcessingDelay, durabilityCapFor } from './maintenance.js';
|
||||||
import { ensureMaintenanceState, remainingPercent, degradationPercent, performanceFactor, autoScannerDelayMultiplier, eggSpawnDelayMultiplier, facilityProcessingDelay } from './maintenance.js';
|
|
||||||
|
|
||||||
export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanels }) {
|
export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanels }) {
|
||||||
function updatePanels() { if (onUpdatePanels) onUpdatePanels(); }
|
function updatePanels() { if (onUpdatePanels) onUpdatePanels(); }
|
||||||
|
|
@ -62,6 +60,22 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
return equipmentBasePrice(hit);
|
return equipmentBasePrice(hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function usedMachineMode() {
|
||||||
|
return !!game.cardEffects?.usedMachineActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQualityPatch() {
|
||||||
|
return usedMachineMode()
|
||||||
|
? { usedMachine: true, noRefund: true, durabilityBaseMultiplier: 0.6 }
|
||||||
|
: { usedMachine: false, noRefund: false, durabilityBaseMultiplier: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBuildQuality(obj) {
|
||||||
|
const patch = buildQualityPatch();
|
||||||
|
Object.assign(obj, patch);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
function lastProtectedSaleReason(hit) {
|
function lastProtectedSaleReason(hit) {
|
||||||
const obj = hit?.ref || hit || {};
|
const obj = hit?.ref || hit || {};
|
||||||
if (hit?.type === 'eggFarm' || obj.type === 'eggFarm') {
|
if (hit?.type === 'eggFarm' || obj.type === 'eggFarm') {
|
||||||
|
|
@ -90,16 +104,16 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
if (game.conveyorTiles.has(key(col, row))) return fail(TEXT.fail.cellOccupied);
|
if (game.conveyorTiles.has(key(col, row))) return fail(TEXT.fail.cellOccupied);
|
||||||
if (isBlockedCell(col, row)) return fail('Cannot build on blocked ground.');
|
if (isBlockedCell(col, row)) return fail('Cannot build on blocked ground.');
|
||||||
if (farmAt(game, col, row) || scannerAt(game, col, row)) return fail(TEXT.fail.cellOccupied);
|
if (farmAt(game, col, row) || scannerAt(game, col, row)) return fail(TEXT.fail.cellOccupied);
|
||||||
const cost = buildPrice('conveyor');
|
const cost = buildPrice('conveyor', game);
|
||||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||||
record(game);
|
record(game);
|
||||||
spendCash(game, cost);
|
spendCash(game, cost);
|
||||||
const k = key(col, row);
|
const k = key(col, row);
|
||||||
game.conveyorTiles.add(k);
|
game.conveyorTiles.add(k);
|
||||||
game.conveyorMeta.set(k, { price: cost, builtSession: game.buildSession, uses: 0, durability: BALANCE.maintenance.durability.conveyor, maintenanceType: 'conveyor' });
|
const quality = buildQualityPatch();
|
||||||
|
game.conveyorMeta.set(k, { price: cost, builtSession: game.buildSession, uses: 0, durability: durabilityCapFor(game, 'conveyor', quality.durabilityBaseMultiplier), maintenanceType: 'conveyor', ...quality });
|
||||||
game.selected = { type: 'conveyor', id: k };
|
game.selected = { type: 'conveyor', id: k };
|
||||||
refreshRoutingAfterEdit(game);
|
refreshRoutingAfterEdit(game);
|
||||||
emitGameEvent(game, 'BuildEquipment', { type: 'conveyor', cell: k, cost });
|
|
||||||
floating(game, cellCenter(col, row).x, cellCenter(col, row).y, `-${yen(cost)}`, THEME.ink);
|
floating(game, cellCenter(col, row).x, cellCenter(col, row).y, `-${yen(cost)}`, THEME.ink);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -117,36 +131,36 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFarm(col, row) {
|
function buildFarm(col, row) {
|
||||||
const cost = buildPrice('eggFarm');
|
const cost = buildPrice('eggFarm', game);
|
||||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||||
record(game);
|
record(game);
|
||||||
spendCash(game, cost);
|
spendCash(game, cost);
|
||||||
const farm = createEggFarm(game, col, row);
|
const farm = applyBuildQuality(createEggFarm(game, col, row));
|
||||||
|
farm.price = cost;
|
||||||
game.eggFarms.push(farm);
|
game.eggFarms.push(farm);
|
||||||
refreshRoutingAfterEdit(game);
|
refreshRoutingAfterEdit(game);
|
||||||
game.selected = { type: 'eggFarm', id: farm.id };
|
game.selected = { type: 'eggFarm', id: farm.id };
|
||||||
emitGameEvent(game, 'BuildEquipment', { type: 'eggFarm', id: farm.id, col, row, cost });
|
|
||||||
floating(game, cellCenter(col, row).x, cellCenter(col, row).y, `-${yen(cost)}`, THEME.ink);
|
floating(game, cellCenter(col, row).x, cellCenter(col, row).y, `-${yen(cost)}`, THEME.ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildScanner(col, row, kind) {
|
function buildScanner(col, row, kind) {
|
||||||
const cost = kind === 'auto' ? buildPrice('autoScanner') : buildPrice('manualScanner');
|
const cost = kind === 'auto' ? buildPrice('autoScanner', game) : buildPrice('manualScanner', game);
|
||||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||||
record(game);
|
record(game);
|
||||||
spendCash(game, cost);
|
spendCash(game, cost);
|
||||||
const scanner = createScanner(game, col, row, kind);
|
const scanner = applyBuildQuality(createScanner(game, col, row, kind));
|
||||||
|
scanner.price = cost;
|
||||||
game.scanners.push(scanner);
|
game.scanners.push(scanner);
|
||||||
refreshRoutingAfterEdit(game);
|
refreshRoutingAfterEdit(game);
|
||||||
game.selected = { type: 'scanner', id: scanner.id };
|
game.selected = { type: 'scanner', id: scanner.id };
|
||||||
emitGameEvent(game, 'BuildEquipment', { type: kind + 'Scanner', id: scanner.id, col, row, cost });
|
|
||||||
floating(game, cellCenter(col, row).x, cellCenter(col, row).y, `-${yen(cost)}`, THEME.ink);
|
floating(game, cellCenter(col, row).x, cellCenter(col, row).y, `-${yen(cost)}`, THEME.ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFacility(p, id) {
|
function buildFacility(p, id) {
|
||||||
const cost = buildPrice(id);
|
const cost = buildPrice(id, game);
|
||||||
if (game.facilities[id]) return fail(TEXT.fail.facilityExists);
|
if (game.facilities[id]) return fail(TEXT.fail.facilityExists);
|
||||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||||
const f = createFacility(game, id, p, cost);
|
const f = applyBuildQuality(createFacility(game, id, p, cost));
|
||||||
if (f.entry && isBlockedCell(f.entry.col, f.entry.row)) return fail('Receiver cell is blocked.');
|
if (f.entry && isBlockedCell(f.entry.col, f.entry.row)) return fail('Receiver cell is blocked.');
|
||||||
if (facilityOverlaps(f)) return fail(TEXT.fail.facilityOverlap);
|
if (facilityOverlaps(f)) return fail(TEXT.fail.facilityOverlap);
|
||||||
record(game);
|
record(game);
|
||||||
|
|
@ -154,7 +168,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
game.facilities[id] = f;
|
game.facilities[id] = f;
|
||||||
refreshRoutingAfterEdit(game);
|
refreshRoutingAfterEdit(game);
|
||||||
game.selected = { type: 'facility', id };
|
game.selected = { type: 'facility', id };
|
||||||
emitGameEvent(game, 'BuildEquipment', { type: id, cost });
|
|
||||||
floating(game, p.x, p.y - 14, `-${yen(cost)}`, THEME.ink);
|
floating(game, p.x, p.y - 14, `-${yen(cost)}`, THEME.ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,7 +185,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
const saleReason = lastProtectedSaleReason(hit);
|
const saleReason = lastProtectedSaleReason(hit);
|
||||||
if (saleReason) return fail(saleReason);
|
if (saleReason) return fail(saleReason);
|
||||||
record(game);
|
record(game);
|
||||||
emitGameEvent(game, 'SellEquipment', { type: hit.type, id: hit.oldKey || hit.ref?.id || hit.ref?.type || 'unknown', value: resaleValueFor(hit, game).amount });
|
|
||||||
if (hit.type === 'conveyor') {
|
if (hit.type === 'conveyor') {
|
||||||
const c = parseKey(hit.oldKey);
|
const c = parseKey(hit.oldKey);
|
||||||
const center = cellCenter(c.col, c.row);
|
const center = cellCenter(c.col, c.row);
|
||||||
|
|
@ -338,7 +350,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
}
|
}
|
||||||
for (const farm of game.eggFarms) { const c = cellCenter(farm.col, farm.row); boxes.push({ id: `eggFarm:${farm.id}`, price: equipmentPrice({ type: 'eggFarm', ref: farm }), x: c.x - 24, y: c.y - 24, w: 48, h: 48 }); }
|
for (const farm of game.eggFarms) { const c = cellCenter(farm.col, farm.row); boxes.push({ id: `eggFarm:${farm.id}`, price: equipmentPrice({ type: 'eggFarm', ref: farm }), x: c.x - 24, y: c.y - 24, w: 48, h: 48 }); }
|
||||||
for (const scanner of game.scanners) { const c = scannerCenter(scanner); boxes.push({ id: `scanner:${scanner.id}`, price: equipmentPrice({ type: 'scanner', ref: scanner }), x: c.x - 52, y: c.y - 36, w: 104, h: 72 }); }
|
for (const scanner of game.scanners) { const c = scannerCenter(scanner); boxes.push({ id: `scanner:${scanner.id}`, price: equipmentPrice({ type: 'scanner', ref: scanner }), x: c.x - 52, y: c.y - 36, w: 104, h: 72 }); }
|
||||||
for (const k of game.conveyorTiles) { const p = parseKey(k); const c = cellCenter(p.col, p.row); boxes.push({ id: `conveyor:${k}`, price: buildPrice('conveyor'), x: c.x - 22, y: c.y - 22, w: 44, h: 44 }); }
|
for (const k of game.conveyorTiles) { const p = parseKey(k); const c = cellCenter(p.col, p.row); boxes.push({ id: `conveyor:${k}`, price: buildPrice('conveyor', game), x: c.x - 22, y: c.y - 22, w: 44, h: 44 }); }
|
||||||
return boxes;
|
return boxes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -366,7 +378,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
const meta = game.conveyorMeta.get(obj.id);
|
const meta = game.conveyorMeta.get(obj.id);
|
||||||
const comp = game.componentLookup?.get(obj.id);
|
const comp = game.componentLookup?.get(obj.id);
|
||||||
const c = comp ? game.congestion.get(comp) : null;
|
const c = comp ? game.congestion.get(comp) : null;
|
||||||
lines.push(`Price: ${yen(buildPrice('conveyor'))}`);
|
lines.push(`Price: ${yen(equipmentPrice({ type: 'conveyor', oldKey: obj.id, ref: obj }))}`);
|
||||||
lines.push(`Cell: ${obj.id}`);
|
lines.push(`Cell: ${obj.id}`);
|
||||||
lines.push(`Congestion: ${c ? Math.floor(c.ratio * 100) : 0}%`);
|
lines.push(`Congestion: ${c ? Math.floor(c.ratio * 100) : 0}%`);
|
||||||
lines.push(`Durability: ${remainingPercent({ meta })}% | Speed x${performanceFactor({ meta }).toFixed(2)}`);
|
lines.push(`Durability: ${remainingPercent({ meta })}% | Speed x${performanceFactor({ meta }).toFixed(2)}`);
|
||||||
|
|
@ -391,7 +403,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
const chance = shredderBonusChance(cards);
|
const chance = shredderBonusChance(cards);
|
||||||
lines.push(`Bonus cards: ${cards}/${shredderBonusMaxCards()}`);
|
lines.push(`Bonus cards: ${cards}/${shredderBonusMaxCards()}`);
|
||||||
lines.push(`Bonus chance: ${(chance * 100).toFixed(2)}% for ${yen(1)} per processed item.`);
|
lines.push(`Bonus chance: ${(chance * 100).toFixed(2)}% for ${yen(1)} per processed item.`);
|
||||||
lines.push(`Expected value: ${chance.toFixed(3)}円 per item.`);
|
lines.push(`Expected value: ${chance.toFixed(3)} JPY per item.`);
|
||||||
}
|
}
|
||||||
if (obj.entry) lines.push(`Receiver: edge cell ${obj.entry.col},${obj.entry.row} (${obj.side})`);
|
if (obj.entry) lines.push(`Receiver: edge cell ${obj.entry.col},${obj.entry.row} (${obj.side})`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,34 @@
|
||||||
import { AUTO_SCANNER_COOLDOWN, AUTO_SCANNER_UPGRADE_RATE, AUTO_SCANNER_MIN_COOLDOWN, BEARING_SPEED_MULTIPLIER, CARD_BALANCE, CONVEYOR_SPEED, CONVEYOR_SPEED_MAX, GRID, THEME } from '../core/config.js';
|
import { AUTO_SCANNER_COOLDOWN, AUTO_SCANNER_UPGRADE_RATE, AUTO_SCANNER_MIN_COOLDOWN, BEARING_SPEED_MULTIPLIER, CARD_BALANCE, CONVEYOR_SPEED, CONVEYOR_SPEED_MAX, ECONOMY, EGG_SPAWN_RANGES, GRID, THEME } from '../core/config.js';
|
||||||
import { nextSpawnDelay } from '../core/state.js';
|
import { nextSpawnDelay } from '../core/state.js';
|
||||||
import { cellCenter, yen } from '../core/utils.js';
|
import { cellCenter, key, yen } from '../core/utils.js';
|
||||||
import { applyPenalty, upgradedMixerPrice, upgradedTruckPrice, shredderUpgradeCount, rawShredderUpgradeCount, shredderBonusChance, shredderBonusMaxCards } from './economy.js';
|
import { applyPenalty, applyRevenue, upgradedMixerPrice, upgradedTruckPrice, shredderUpgradeCount, rawShredderUpgradeCount, shredderBonusChance, shredderBonusMaxCards } from './economy.js';
|
||||||
import { floating } from './effects.js';
|
import { floating, shake, eraseEffect, shockwave, sparkBurst, smokeBurst } from './effects.js';
|
||||||
import { averageConveyorPerformance, autoScannerDelayMultiplier } from './maintenance.js';
|
import { averageConveyorPerformance, autoScannerDelayMultiplier, ensureMaintenanceState } from './maintenance.js';
|
||||||
import { scannerCenter } from './routing.js';
|
import { scannerCenter, refreshRoutingAfterEdit } from './routing.js';
|
||||||
|
|
||||||
const COMMON_WEIGHT = CARD_BALANCE.commonWeight;
|
const COMMON_WEIGHT = CARD_BALANCE.commonWeight;
|
||||||
const RARE_WEIGHT = CARD_BALANCE.rareWeight;
|
const RARE_WEIGHT = CARD_BALANCE.rareWeight;
|
||||||
|
const ULTRA_RARE_WEIGHT = CARD_BALANCE.ultraRareWeight || 0.45;
|
||||||
const BASE_DRAFT_SIZE = CARD_BALANCE.baseDraftSize;
|
const BASE_DRAFT_SIZE = CARD_BALANCE.baseDraftSize;
|
||||||
|
|
||||||
|
const DEFAULT_EFFECTS = Object.freeze({
|
||||||
|
bearing: 0,
|
||||||
|
legalWork: 0,
|
||||||
|
fairiesFlatteryNext: 0,
|
||||||
|
crowdedFarming: 0,
|
||||||
|
extraEggOutlet: 0,
|
||||||
|
hatchingFeed: 0,
|
||||||
|
safetyCover: 0,
|
||||||
|
recyclingSubsidy: 0,
|
||||||
|
chemicalWeaponSubsidy: 0,
|
||||||
|
preventiveMaintenance: 0,
|
||||||
|
durabilityCoating: 0,
|
||||||
|
laborExploitation: 0,
|
||||||
|
usedMachineActive: false,
|
||||||
|
rescueLoanCharges: 0,
|
||||||
|
loans: []
|
||||||
|
});
|
||||||
|
|
||||||
export const CARD_DEFS = [
|
export const CARD_DEFS = [
|
||||||
{
|
{
|
||||||
id: 'upgradeEgg',
|
id: 'upgradeEgg',
|
||||||
|
|
@ -49,7 +68,42 @@ export const CARD_DEFS = [
|
||||||
rarity: 'common',
|
rarity: 'common',
|
||||||
type: 'equipmentUpgrade',
|
type: 'equipmentUpgrade',
|
||||||
target: 'trash',
|
target: 'trash',
|
||||||
description: 'Choose SHREDDER on the map and raise it by 1 level. Per processed item: ((√cards / 2) × cards)% chance to pay ¥1. Max 30 cards.'
|
description: 'Choose SHREDDER and raise it by 1 level. Bonus chance increases by the current upgrade count. Max 30 upgrades.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'crowdedFarming',
|
||||||
|
title: 'Crowded Farming Contract',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'EGG production interval -5%. Equipment deterioration +4%. Max 10 cards.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hatchingFeed',
|
||||||
|
title: 'Hatching Feed',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'EGG production interval -4%. Poop rate +3%. No card limit.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'safetyCover',
|
||||||
|
title: 'Safety Cover',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Explosion damage -5%. No card limit.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'preventiveMaintenance',
|
||||||
|
title: 'Preventive Maintenance Manual',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'All equipment deterioration speed -5%. No card limit.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'durabilityCoating',
|
||||||
|
title: 'Durability Coating',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'All equipment maximum durability +3%. No card limit.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bearing',
|
id: 'bearing',
|
||||||
|
|
@ -58,6 +112,56 @@ export const CARD_DEFS = [
|
||||||
type: 'instant',
|
type: 'instant',
|
||||||
description: 'Conveyor speed +7.5%.'
|
description: 'Conveyor speed +7.5%.'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'extraEggOutlet',
|
||||||
|
title: 'Extra Egg Outlet',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'EGG output ports +1. Each connected belt can receive eggs. Max 3 cards.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'recyclingSubsidy',
|
||||||
|
title: 'Recycling Subsidy',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'SELL refund rate +10 points. Max 3 cards; normal SELL can reach 80%.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dynamite',
|
||||||
|
title: 'Dynamite',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'cellAction',
|
||||||
|
target: 'blockedCell',
|
||||||
|
description: 'Remove any 3 blocked cells with a burst effect.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'usedMachine',
|
||||||
|
title: 'Used Machines',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Future equipment costs half. New purchases have no refund and 60% durability. New Machines cancels it.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'newMachine',
|
||||||
|
title: 'New Machines',
|
||||||
|
rarity: 'common',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Cancel Used Machines and restore normal prices, refunds, and durability.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'flattery',
|
||||||
|
title: 'Flattery',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Next Fairies tribute -50%.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'loan',
|
||||||
|
title: 'Loan',
|
||||||
|
rarity: 'rare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Gain JPY 300 per elapsed day now. Pay JPY 400 per day for that many days starting tomorrow.'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'extraCards',
|
id: 'extraCards',
|
||||||
title: 'Extra Cards',
|
title: 'Extra Cards',
|
||||||
|
|
@ -73,22 +177,63 @@ export const CARD_DEFS = [
|
||||||
description: 'ZUNDA TAX exemption +¥250. The 95% cap point also moves up by ¥250.'
|
description: 'ZUNDA TAX exemption +¥250. The 95% cap point also moves up by ¥250.'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'flattery',
|
id: 'chemicalWeaponSubsidy',
|
||||||
title: 'Flattery',
|
title: 'Chemical Weapons Subsidy',
|
||||||
rarity: 'rare',
|
rarity: 'ultraRare',
|
||||||
type: 'instant',
|
type: 'instant',
|
||||||
description: 'Fairies tribute reduction card.'
|
description: 'Each day, gain JPY 2000 per 100 poop shipped by truck. No card limit.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'laborExploitation',
|
||||||
|
title: 'Motivational Exploitation',
|
||||||
|
rarity: 'ultraRare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'Repairman labor cost -30%. The repairman smiles with bloodshot eyes. One time only.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rescueLoan',
|
||||||
|
title: 'Emergency Loan',
|
||||||
|
rarity: 'ultraRare',
|
||||||
|
type: 'instant',
|
||||||
|
description: 'If cash is below JPY 0 at day-end settlement, gain JPY 500. Consumed on use.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function effectCount(game, id) {
|
||||||
|
const effects = ensureCardState(game);
|
||||||
|
return Math.max(0, Number(effects[id]) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
export function ensureCardState(game) {
|
export function ensureCardState(game) {
|
||||||
if (!game.cardEffects) game.cardEffects = { bearing: 0, legalWork: 0, flattery: 0 };
|
if (!game.cardEffects) game.cardEffects = { ...DEFAULT_EFFECTS, loans: [] };
|
||||||
|
for (const [k, v] of Object.entries(DEFAULT_EFFECTS)) {
|
||||||
|
if (game.cardEffects[k] == null) game.cardEffects[k] = Array.isArray(v) ? [] : v;
|
||||||
|
}
|
||||||
|
if (game.cardEffects.flattery != null) {
|
||||||
|
game.cardEffects.fairiesFlatteryNext = Math.max(0, game.cardEffects.fairiesFlatteryNext || 0) + Math.max(0, game.cardEffects.flattery || 0);
|
||||||
|
delete game.cardEffects.flattery;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(game.cardEffects.loans)) game.cardEffects.loans = [];
|
||||||
if (game.cardEffects.drawBonus != null) delete game.cardEffects.drawBonus;
|
if (game.cardEffects.drawBonus != null) delete game.cardEffects.drawBonus;
|
||||||
if (!game.cardDraft) game.cardDraft = { pending: false, choices: [], rerolls: 0, picksRemaining: 0 };
|
if (!game.cardDraft) game.cardDraft = { pending: false, choices: [], rerolls: 0, picksRemaining: 0 };
|
||||||
if (game.cardDraft.picksRemaining == null) game.cardDraft.picksRemaining = game.cardDraft.pending ? 1 : 0;
|
if (game.cardDraft.picksRemaining == null) game.cardDraft.picksRemaining = game.cardDraft.pending ? 1 : 0;
|
||||||
return game.cardEffects;
|
return game.cardEffects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function eggProductionDelayMultiplier(game) {
|
||||||
|
const crowded = Math.max(0.5, 1 - 0.05 * Math.min(10, effectCount(game, 'crowdedFarming')));
|
||||||
|
const feed = Math.pow(0.96, effectCount(game, 'hatchingFeed'));
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
|
||||||
export function conveyorSpeedForGame(game, conveyorKey = null) {
|
export function conveyorSpeedForGame(game, conveyorKey = null) {
|
||||||
const effects = ensureCardState(game);
|
const effects = ensureCardState(game);
|
||||||
const speed = CONVEYOR_SPEED * Math.pow(BEARING_SPEED_MULTIPLIER, Math.max(0, effects.bearing || 0));
|
const speed = CONVEYOR_SPEED * Math.pow(BEARING_SPEED_MULTIPLIER, Math.max(0, effects.bearing || 0));
|
||||||
|
|
@ -103,10 +248,10 @@ export function autoScannerCooldownSeconds(scanner, game = null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flatteryReductionForTurn(game, turn = game.turn) {
|
export function flatteryReductionForTurn(game, turn = game.turn) {
|
||||||
const effects = ensureCardState(game);
|
|
||||||
const day = Math.max(1, Math.floor(Number(turn) || 1));
|
const day = Math.max(1, Math.floor(Number(turn) || 1));
|
||||||
const perCard = Math.floor(10 * day / 10);
|
const base = Math.ceil(day * 10);
|
||||||
return Math.max(0, perCard * Math.max(0, effects.flattery || 0));
|
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) {
|
export function rerollCost(game) {
|
||||||
|
|
@ -120,6 +265,7 @@ export function cardById(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function targetLabel(game, target) {
|
function targetLabel(game, target) {
|
||||||
|
if (target.type === 'blockedCell') return `Blocked cell ${target.col},${target.row}`;
|
||||||
if (target.type === 'eggFarm') {
|
if (target.type === 'eggFarm') {
|
||||||
return `EGG #${target.id} L${target.level} -> L${target.level + 1}`;
|
return `EGG #${target.id} L${target.level} -> L${target.level + 1}`;
|
||||||
}
|
}
|
||||||
|
|
@ -152,10 +298,18 @@ function targetLabel(game, target) {
|
||||||
return `${target.name || target.id} L${target.level || 1} -> L${(target.level || 1) + 1}`;
|
return `${target.name || target.id} L${target.level || 1} -> L${(target.level || 1) + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function blockedCellTargets(game) {
|
||||||
|
return [...(game.blockedCells || [])].map(k => {
|
||||||
|
const [col, row] = String(k).split(',').map(Number);
|
||||||
|
return { type: 'blockedCell', id: k, col, row };
|
||||||
|
}).filter(t => Number.isFinite(t.col) && Number.isFinite(t.row));
|
||||||
|
}
|
||||||
|
|
||||||
export function targetsForCard(game, cardOrId) {
|
export function targetsForCard(game, cardOrId) {
|
||||||
const card = typeof cardOrId === 'string' ? cardById(cardOrId) : cardOrId;
|
const card = typeof cardOrId === 'string' ? cardById(cardOrId) : cardOrId;
|
||||||
ensureCardState(game);
|
ensureCardState(game);
|
||||||
if (!card) return [];
|
if (!card) return [];
|
||||||
|
if (card.target === 'blockedCell') return blockedCellTargets(game);
|
||||||
if (card.target === 'eggFarm') return game.eggFarms.filter(f => (f.level || 1) < 4);
|
if (card.target === 'eggFarm') return game.eggFarms.filter(f => (f.level || 1) < 4);
|
||||||
if (card.target === 'autoScanner') return game.scanners.filter(s => s.kind === 'auto');
|
if (card.target === 'autoScanner') return game.scanners.filter(s => s.kind === 'auto');
|
||||||
if (card.target === 'mixer') return game.facilities.mixer ? [game.facilities.mixer] : [];
|
if (card.target === 'mixer') return game.facilities.mixer ? [game.facilities.mixer] : [];
|
||||||
|
|
@ -164,15 +318,36 @@ export function targetsForCard(game, cardOrId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cardAtCap(game, card) {
|
||||||
|
const effects = ensureCardState(game);
|
||||||
|
if (card.id === 'crowdedFarming') return (effects.crowdedFarming || 0) >= 10;
|
||||||
|
if (card.id === 'extraEggOutlet') return (effects.extraEggOutlet || 0) >= 3;
|
||||||
|
if (card.id === 'recyclingSubsidy') return (effects.recyclingSubsidy || 0) >= 3;
|
||||||
|
if (card.id === 'laborExploitation') return !!effects.laborExploitation;
|
||||||
|
if (card.id === 'usedMachine') return !!effects.usedMachineActive;
|
||||||
|
if (card.id === 'newMachine') return !effects.usedMachineActive;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function availableCards(game) {
|
function availableCards(game) {
|
||||||
return CARD_DEFS.filter(card => card.type !== 'equipmentUpgrade' || targetsForCard(game, card).length > 0);
|
return CARD_DEFS.filter(card => {
|
||||||
|
if (cardAtCap(game, card)) return false;
|
||||||
|
if ((card.type === 'equipmentUpgrade' || card.type === 'cellAction') && targetsForCard(game, card).length <= 0) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function rarityWeight(card) {
|
||||||
|
if (card.rarity === 'ultraRare') return ULTRA_RARE_WEIGHT;
|
||||||
|
if (card.rarity === 'rare') return RARE_WEIGHT;
|
||||||
|
return COMMON_WEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
function weightedPick(pool) {
|
function weightedPick(pool) {
|
||||||
const total = pool.reduce((sum, card) => sum + (card.rarity === 'rare' ? RARE_WEIGHT : COMMON_WEIGHT), 0);
|
const total = pool.reduce((sum, card) => sum + rarityWeight(card), 0);
|
||||||
let roll = Math.random() * total;
|
let roll = Math.random() * total;
|
||||||
for (const card of pool) {
|
for (const card of pool) {
|
||||||
roll -= card.rarity === 'rare' ? RARE_WEIGHT : COMMON_WEIGHT;
|
roll -= rarityWeight(card);
|
||||||
if (roll <= 0) return card;
|
if (roll <= 0) return card;
|
||||||
}
|
}
|
||||||
return pool[pool.length - 1];
|
return pool[pool.length - 1];
|
||||||
|
|
@ -183,7 +358,7 @@ function dudCard() {
|
||||||
dudSerial += 1;
|
dudSerial += 1;
|
||||||
return {
|
return {
|
||||||
id: `dud:${Date.now()}:${dudSerial}`,
|
id: `dud:${Date.now()}:${dudSerial}`,
|
||||||
title: 'スカ',
|
title: 'Dud',
|
||||||
rarity: 'dud',
|
rarity: 'dud',
|
||||||
type: 'dud',
|
type: 'dud',
|
||||||
description: 'Click to flick this dud away. It does not consume a pick.'
|
description: 'Click to flick this dud away. It does not consume a pick.'
|
||||||
|
|
@ -222,6 +397,7 @@ export function dealCards(game, count = BASE_DRAFT_SIZE) {
|
||||||
|
|
||||||
function targetKey(target) {
|
function targetKey(target) {
|
||||||
if (!target) return '';
|
if (!target) return '';
|
||||||
|
if (target.type === 'blockedCell') return `blockedCell:${target.id}`;
|
||||||
if (target.type === 'eggFarm') return `eggFarm:${target.id}`;
|
if (target.type === 'eggFarm') return `eggFarm:${target.id}`;
|
||||||
if (target.type === 'scanner') return `scanner:${target.id}`;
|
if (target.type === 'scanner') return `scanner:${target.id}`;
|
||||||
if (target.type === 'facility') return `facility:${target.id}`;
|
if (target.type === 'facility') return `facility:${target.id}`;
|
||||||
|
|
@ -230,6 +406,10 @@ function targetKey(target) {
|
||||||
|
|
||||||
function boundsForTarget(target) {
|
function boundsForTarget(target) {
|
||||||
if (!target) return null;
|
if (!target) return null;
|
||||||
|
if (target.type === 'blockedCell') {
|
||||||
|
const c = cellCenter(target.col, target.row);
|
||||||
|
return { x: c.x - GRID.cell * 0.48, y: c.y - GRID.cell * 0.48, w: GRID.cell * 0.96, h: GRID.cell * 0.96, cx: c.x, cy: c.y };
|
||||||
|
}
|
||||||
if (target.type === 'eggFarm') {
|
if (target.type === 'eggFarm') {
|
||||||
const c = cellCenter(target.col, target.row);
|
const c = cellCenter(target.col, target.row);
|
||||||
return { x: c.x - 34, y: c.y - 34, w: 68, h: 68, cx: c.x, cy: c.y };
|
return { x: c.x - 34, y: c.y - 34, w: 68, h: 68, cx: c.x, cy: c.y };
|
||||||
|
|
@ -283,15 +463,112 @@ function applyEquipmentUpgrade(game, target) {
|
||||||
|
|
||||||
function applyInstantCard(game, card) {
|
function applyInstantCard(game, card) {
|
||||||
const effects = ensureCardState(game);
|
const effects = ensureCardState(game);
|
||||||
if (card.id === 'bearing') effects.bearing += 1;
|
const inc = id => { effects[id] = Math.max(0, effects[id] || 0) + 1; };
|
||||||
if (card.id === 'legalWork') effects.legalWork += 1;
|
if (card.id === 'bearing') inc('bearing');
|
||||||
if (card.id === 'flattery') effects.flattery += 1;
|
if (card.id === 'legalWork') inc('legalWork');
|
||||||
|
if (card.id === 'crowdedFarming') effects.crowdedFarming = Math.min(10, (effects.crowdedFarming || 0) + 1);
|
||||||
|
if (card.id === 'extraEggOutlet') effects.extraEggOutlet = Math.min(3, (effects.extraEggOutlet || 0) + 1);
|
||||||
|
if (card.id === 'hatchingFeed') inc('hatchingFeed');
|
||||||
|
if (card.id === 'safetyCover') inc('safetyCover');
|
||||||
|
if (card.id === 'recyclingSubsidy') effects.recyclingSubsidy = Math.min(3, (effects.recyclingSubsidy || 0) + 1);
|
||||||
|
if (card.id === 'chemicalWeaponSubsidy') inc('chemicalWeaponSubsidy');
|
||||||
|
if (card.id === 'preventiveMaintenance') inc('preventiveMaintenance');
|
||||||
|
if (card.id === 'durabilityCoating') { inc('durabilityCoating'); ensureMaintenanceState(game); }
|
||||||
|
if (card.id === 'flattery') inc('fairiesFlatteryNext');
|
||||||
|
if (card.id === 'usedMachine') effects.usedMachineActive = true;
|
||||||
|
if (card.id === 'newMachine') effects.usedMachineActive = false;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (card.id === 'loan') {
|
||||||
|
const day = Math.max(1, Math.floor(Number(game.turn) || 1));
|
||||||
|
const amount = 300 * day;
|
||||||
|
applyRevenue(game, amount);
|
||||||
|
effects.loans.push({ daysRemaining: day, dailyPayment: 400, borrowedOnTurn: game.turn || 1 });
|
||||||
|
floating(game, GRID.x + GRID.cols * GRID.cell / 2, GRID.y - 30, `LOAN +${yen(amount)}`, THEME.green);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eggRangeLabel(level) {
|
||||||
|
const range = EGG_SPAWN_RANGES[Math.max(0, Math.min(EGG_SPAWN_RANGES.length - 1, level - 1))] || EGG_SPAWN_RANGES[0] || [0, 0];
|
||||||
|
return `${range[0].toFixed(2)}-${range[1].toFixed(2)}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function incomeAtLevel(base, level) {
|
||||||
|
return Math.ceil(base * Math.pow(ECONOMY.incomeUpgradeRate, Math.max(0, level - 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function cardDescription(game, card) {
|
function cardDescription(game, card) {
|
||||||
if (card.id === 'flattery') {
|
const e = ensureCardState(game);
|
||||||
const reduction = flatteryReductionForTurn({ ...game, cardEffects: { ...ensureCardState(game), flattery: 1 } }, game.turn);
|
if (card.id === 'upgradeEgg') {
|
||||||
return `Fairies tribute reduction: ${yen(reduction)}`;
|
const targets = targetsForCard(game, card);
|
||||||
|
const level = Math.min(3, Math.max(1, targets[0]?.level || 1));
|
||||||
|
return `Choose one EGG. Level +1. Production interval ${eggRangeLabel(level)} -> ${eggRangeLabel(level + 1)}.`;
|
||||||
|
}
|
||||||
|
if (card.id === 'upgradeAutoScanner') {
|
||||||
|
const scanner = targetsForCard(game, card)[0] || { level: 1 };
|
||||||
|
const before = autoScannerCooldownSeconds(scanner, game);
|
||||||
|
const after = autoScannerCooldownSeconds({ ...scanner, level: (scanner.level || 1) + 1 }, game);
|
||||||
|
return `Choose one AUTO SCANNER. Level +1. Cooldown ${before.toFixed(2)}s -> ${after.toFixed(2)}s.`;
|
||||||
|
}
|
||||||
|
if (card.id === 'upgradeMixer') {
|
||||||
|
const level = game.facilities?.mixer?.level || 1;
|
||||||
|
return `MIXER level +1. Male-chick income ${yen(incomeAtLevel(ECONOMY.income.mixer, level))} -> ${yen(incomeAtLevel(ECONOMY.income.mixer, level + 1))}.`;
|
||||||
|
}
|
||||||
|
if (card.id === 'upgradeTruck') {
|
||||||
|
const level = game.facilities?.truck?.level || 1;
|
||||||
|
return `TRUCK level +1. Correct shipment income ${yen(incomeAtLevel(ECONOMY.income.truck, level))} -> ${yen(incomeAtLevel(ECONOMY.income.truck, level + 1))}.`;
|
||||||
|
}
|
||||||
|
if (card.id === 'upgradeTrash') {
|
||||||
|
const before = shredderUpgradeCount(game);
|
||||||
|
const after = Math.min(shredderBonusMaxCards(), before + 1);
|
||||||
|
return `SHREDDER level +1. Bonus chance per item ${formatPercent(shredderBonusChance(before))} -> ${formatPercent(shredderBonusChance(after))}.`;
|
||||||
|
}
|
||||||
|
if (card.id === 'bearing') {
|
||||||
|
const before = Math.min(CONVEYOR_SPEED_MAX, CONVEYOR_SPEED * Math.pow(BEARING_SPEED_MULTIPLIER, Math.max(0, e.bearing || 0)));
|
||||||
|
const after = Math.min(CONVEYOR_SPEED_MAX, CONVEYOR_SPEED * Math.pow(BEARING_SPEED_MULTIPLIER, Math.max(0, (e.bearing || 0) + 1)));
|
||||||
|
return `Conveyor speed +7.5%. After pick: ${before.toFixed(1)} -> ${after.toFixed(1)} px/s.`;
|
||||||
|
}
|
||||||
|
if (card.id === 'legalWork') {
|
||||||
|
const before = Math.max(0, e.legalWork || 0);
|
||||||
|
return `ZUNDA TAX exemption +${yen(250)}. After pick: exemption bonus ${yen(250 * (before + 1))}.`;
|
||||||
|
}
|
||||||
|
if (card.id === 'crowdedFarming') {
|
||||||
|
const next = Math.min(10, (e.crowdedFarming || 0) + 1);
|
||||||
|
return `Held ${e.crowdedFarming || 0}/10. After pick: EGG interval ${formatPercent(Math.max(0.5, 1 - 0.05 * next))}, deterioration ${formatPercent(Math.min(1.4, 1 + 0.04 * next))}.`;
|
||||||
|
}
|
||||||
|
if (card.id === 'extraEggOutlet') return `Held ${e.extraEggOutlet || 0}/3. After pick: up to ${1 + Math.min(3, (e.extraEggOutlet || 0) + 1)} EGG output ports.`;
|
||||||
|
if (card.id === 'hatchingFeed') return `Held ${e.hatchingFeed || 0}. After pick: EGG interval ${formatPercent(Math.pow(0.96, (e.hatchingFeed || 0) + 1))}, poop rate ${formatPercent(Math.pow(1.03, (e.hatchingFeed || 0) + 1))}.`;
|
||||||
|
if (card.id === 'safetyCover') return `Held ${e.safetyCover || 0}. After pick: explosion damage ${formatPercent(Math.pow(0.95, (e.safetyCover || 0) + 1))}.`;
|
||||||
|
if (card.id === 'recyclingSubsidy') return `Held ${e.recyclingSubsidy || 0}/3. After pick: normal SELL refund ${Math.round(Math.min(0.8, 0.5 + 0.1 * ((e.recyclingSubsidy || 0) + 1)) * 100)}%.`;
|
||||||
|
if (card.id === 'chemicalWeaponSubsidy') return `Held ${e.chemicalWeaponSubsidy || 0}. After pick: ${yen(2000 * ((e.chemicalWeaponSubsidy || 0) + 1))} per 100 truck-shipped poop each day.`;
|
||||||
|
if (card.id === 'preventiveMaintenance') return `Held ${e.preventiveMaintenance || 0}. After pick: deterioration speed ${formatPercent(Math.pow(0.95, (e.preventiveMaintenance || 0) + 1))}.`;
|
||||||
|
if (card.id === 'durabilityCoating') return `Held ${e.durabilityCoating || 0}. After pick: maximum durability ${formatPercent(Math.pow(1.03, (e.durabilityCoating || 0) + 1))}.`;
|
||||||
|
if (card.id === 'flattery') return `Next Fairies tribute -50%. Held for next tribute: ${e.fairiesFlatteryNext || 0}.`;
|
||||||
|
if (card.id === 'usedMachine') return 'Enable Used Machines: future equipment costs 50%, has no SELL refund, and starts with 60% durability.';
|
||||||
|
if (card.id === 'newMachine') return 'Cancel Used Machines. Future equipment returns to normal price, refund, and durability.';
|
||||||
|
if (card.id === 'laborExploitation') return 'Repairman labor cost -30% permanently. One time only.';
|
||||||
|
if (card.id === 'rescueLoan') return `At day-end bankruptcy check, gain ${yen(500)} if cash is below ${yen(0)}. Held: ${e.rescueLoanCharges || 0}.`;
|
||||||
|
if (card.id === 'loan') {
|
||||||
|
const day = Math.max(1, Math.floor(Number(game.turn) || 1));
|
||||||
|
return `Gain ${yen(300 * day)} now. Starting tomorrow, pay ${yen(400)} per day for ${day} days.`;
|
||||||
}
|
}
|
||||||
return card.description;
|
return card.description;
|
||||||
}
|
}
|
||||||
|
|
@ -348,18 +625,34 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseDud(card, buttonEl) {
|
function chooseDud(card, buttonEl) {
|
||||||
if (buttonEl) buttonEl.classList.add('flung');
|
shake(game, 34, 0.62);
|
||||||
|
if (document.body?.animate) {
|
||||||
|
document.body.animate([
|
||||||
|
{ transform: 'translate(0, 0)' },
|
||||||
|
{ transform: 'translate(-10px, 7px)' },
|
||||||
|
{ transform: 'translate(12px, -8px)' },
|
||||||
|
{ transform: 'translate(-8px, -5px)' },
|
||||||
|
{ transform: 'translate(9px, 6px)' },
|
||||||
|
{ transform: 'translate(0, 0)' }
|
||||||
|
], { duration: 260, easing: 'steps(5, end)' });
|
||||||
|
}
|
||||||
|
if (buttonEl) {
|
||||||
|
buttonEl.style.setProperty('--fling-x', `${260 + Math.random() * 260}px`);
|
||||||
|
buttonEl.style.setProperty('--fling-y', `${-260 - Math.random() * 170}px`);
|
||||||
|
buttonEl.style.setProperty('--fling-r', `${(Math.random() < 0.5 ? -1 : 1) * (52 + Math.random() * 38)}deg`);
|
||||||
|
buttonEl.classList.add('flung');
|
||||||
|
}
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
removeOneChoice(game, card);
|
removeOneChoice(game, card);
|
||||||
refillChoicesIfNeeded();
|
refillChoicesIfNeeded();
|
||||||
showDraft();
|
showDraft();
|
||||||
}, 190);
|
}, 340);
|
||||||
}
|
}
|
||||||
|
|
||||||
function chooseCard(card, buttonEl = null) {
|
function chooseCard(card, buttonEl = null) {
|
||||||
if (card.type === 'dud') return chooseDud(card, buttonEl);
|
if (card.type === 'dud') return chooseDud(card, buttonEl);
|
||||||
if (card.id === 'extraCards') return chooseExtraCards(card);
|
if (card.id === 'extraCards') return chooseExtraCards(card);
|
||||||
if (card.type === 'equipmentUpgrade') return startMapTargetPicker(card);
|
if (card.type === 'equipmentUpgrade' || card.type === 'cellAction') return startMapTargetPicker(card);
|
||||||
applyInstantCard(game, card);
|
applyInstantCard(game, card);
|
||||||
completePickedCard(card);
|
completePickedCard(card);
|
||||||
}
|
}
|
||||||
|
|
@ -370,18 +663,45 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
||||||
game.cardTargetPick = {
|
game.cardTargetPick = {
|
||||||
pending: true,
|
pending: true,
|
||||||
cardId: card.id,
|
cardId: card.id,
|
||||||
targetKeys: targets.map(targetKey)
|
mode: card.target === 'blockedCell' ? 'blockedCell' : 'equipment',
|
||||||
|
targetKeys: targets.map(targetKey),
|
||||||
|
remaining: card.id === 'dynamite' ? Math.min(3, targets.length) : 1
|
||||||
};
|
};
|
||||||
ui.modal.classList.remove('visible', 'equipment-popover');
|
ui.modal.classList.remove('visible', 'equipment-popover');
|
||||||
updatePanels();
|
updatePanels();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyDynamiteTarget(target) {
|
||||||
|
if (!target || target.type !== 'blockedCell') return false;
|
||||||
|
const k = key(target.col, target.row);
|
||||||
|
if (!game.blockedCells?.has(k)) return false;
|
||||||
|
game.blockedCells.delete(k);
|
||||||
|
const c = cellCenter(target.col, target.row);
|
||||||
|
eraseEffect(game, c.x, c.y);
|
||||||
|
shockwave(game, c.x, c.y, THEME.danger, 62);
|
||||||
|
sparkBurst(game, c.x, c.y, 18, THEME.warn);
|
||||||
|
smokeBurst(game, c.x, c.y, 8);
|
||||||
|
shake(game, 16, 0.35);
|
||||||
|
floating(game, c.x, c.y - 18, 'BOOM', THEME.danger);
|
||||||
|
refreshRoutingAfterEdit(game);
|
||||||
|
game.cardTargetPick.remaining = Math.max(0, (game.cardTargetPick.remaining || 1) - 1);
|
||||||
|
game.cardTargetPick.targetKeys = targetsForCard(game, 'dynamite').map(targetKey);
|
||||||
|
updatePanels();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function chooseTargetAtPoint(p) {
|
function chooseTargetAtPoint(p) {
|
||||||
if (!game.cardTargetPick?.pending) return false;
|
if (!game.cardTargetPick?.pending) return false;
|
||||||
const card = cardById(game.cardTargetPick.cardId);
|
const card = cardById(game.cardTargetPick.cardId);
|
||||||
const target = targetAtPoint(game, p);
|
const target = targetAtPoint(game, p);
|
||||||
if (!card || !target) {
|
if (!card || !target) {
|
||||||
floating(game, p.x, p.y - 18, 'SELECT WHITE TARGET', THEME.danger);
|
const label = game.cardTargetPick.mode === 'blockedCell' ? 'SELECT BLOCKED CELL' : 'SELECT UPGRADE TARGET';
|
||||||
|
floating(game, p.x, p.y - 18, label, THEME.danger);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (card.id === 'dynamite') {
|
||||||
|
if (!applyDynamiteTarget(target)) return true;
|
||||||
|
if ((game.cardTargetPick.remaining || 0) <= 0 || !targetsForCard(game, 'dynamite').length) completePickedCard(card);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
applyEquipmentUpgrade(game, target);
|
applyEquipmentUpgrade(game, target);
|
||||||
|
|
@ -409,9 +729,9 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
||||||
function cardButton(card, index) {
|
function cardButton(card, index) {
|
||||||
const b = document.createElement('button');
|
const b = document.createElement('button');
|
||||||
b.type = 'button';
|
b.type = 'button';
|
||||||
const kind = card.type === 'dud' ? 'dud' : (card.rarity === 'rare' ? 'rare' : 'common');
|
const kind = card.type === 'dud' ? 'dud' : (card.rarity === 'ultraRare' ? 'ultra-rare' : (card.rarity === 'rare' ? 'rare' : 'common'));
|
||||||
b.className = `card-choice ${kind}`;
|
b.className = `card-choice ${kind}`;
|
||||||
const rarity = card.type === 'dud' ? 'MISS' : (card.rarity === 'rare' ? 'RARE' : 'COMMON');
|
const rarity = card.type === 'dud' ? 'MISS' : (card.rarity === 'ultraRare' ? 'ULTRA RARE' : (card.rarity === 'rare' ? 'RARE' : 'COMMON'));
|
||||||
b.innerHTML = `<strong>${card.title}</strong><span>${rarity}</span><small>${cardDescription(game, card)}</small>`;
|
b.innerHTML = `<strong>${card.title}</strong><span>${rarity}</span><small>${cardDescription(game, card)}</small>`;
|
||||||
b.addEventListener('click', () => chooseCard(card, b));
|
b.addEventListener('click', () => chooseCard(card, b));
|
||||||
b.dataset.index = String(index);
|
b.dataset.index = String(index);
|
||||||
|
|
@ -427,13 +747,13 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
||||||
const cost = rerollCost(game);
|
const cost = rerollCost(game);
|
||||||
const remaining = Math.max(1, game.cardDraft.picksRemaining || 1);
|
const remaining = Math.max(1, game.cardDraft.picksRemaining || 1);
|
||||||
ui.modalTitle.textContent = 'Choose Upgrade Card';
|
ui.modalTitle.textContent = 'Choose Upgrade Card';
|
||||||
ui.modalBody.innerHTML = `<p class="muted-card-note">Choose ${remaining} card${remaining === 1 ? '' : 's'} before Build phase. Each shown card has a 30% dud chance, capped at 2 duds per draft. Click dud cards to flick them away. Equipment cards are applied by selecting a white-highlighted machine on the map.</p><div id="cardChoices" class="card-choices"></div>`;
|
ui.modalBody.innerHTML = `<p class="muted-card-note">Choose ${remaining} card${remaining === 1 ? '' : 's'} before Build phase. Dud cards can be flicked away without consuming a pick. Equipment cards target machines; Dynamite targets blocked cells.</p><div id="cardChoices" class="card-choices"></div>`;
|
||||||
const wrap = ui.modalBody.querySelector('#cardChoices');
|
const wrap = ui.modalBody.querySelector('#cardChoices');
|
||||||
choices.forEach((card, index) => wrap.appendChild(cardButton(card, index)));
|
choices.forEach((card, index) => wrap.appendChild(cardButton(card, index)));
|
||||||
ui.modalActions.innerHTML = '';
|
ui.modalActions.innerHTML = '';
|
||||||
const reroll = document.createElement('button');
|
const reroll = document.createElement('button');
|
||||||
reroll.type = 'button';
|
reroll.type = 'button';
|
||||||
reroll.className = 'facility-action warn';
|
reroll.className = 'facility-action warn reroll-button';
|
||||||
reroll.textContent = `Reroll ${yen(cost)}`;
|
reroll.textContent = `Reroll ${yen(cost)}`;
|
||||||
reroll.disabled = game.cash < cost;
|
reroll.disabled = game.cash < cost;
|
||||||
reroll.title = reroll.disabled ? 'Not enough cash' : `Reroll count today: ${(game.cardDraft.rerolls || 0) + 1}`;
|
reroll.title = reroll.disabled ? 'Not enough cash' : `Reroll count today: ${(game.cardDraft.rerolls || 0) + 1}`;
|
||||||
|
|
|
||||||
|
|
@ -4,38 +4,68 @@ import { nextSpawnDelay } from '../core/state.js';
|
||||||
import { key, parseKey, pointToCell, cellCenter, randomBetween, yen } from '../core/utils.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 { 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 } from './economy.js';
|
||||||
import { autoScannerCooldownSeconds } from './cards.js';
|
import { autoScannerCooldownSeconds, eggProductionDelayMultiplier, extraEggOutletCount } from './cards.js';
|
||||||
import { productionMultiplier, truckTarget, isTargetTruckCargo, shouldFineMaleTruck } from './contracts.js';
|
import { productionMultiplier, truckTarget, isTargetTruckCargo, shouldFineMaleTruck } from './contracts.js';
|
||||||
import { floating, shake, spawnPulse, scannerPulse, meatEffect, sludgeEffect, shredEffect, truckLoadEffect, shockwave, sparkBurst, smokeBurst, flyingDebris, rageEffect } from './effects.js';
|
import { floating, shake, spawnPulse, scannerPulse, meatEffect, sludgeEffect, shredEffect, truckLoadEffect, shockwave, sparkBurst, smokeBurst, flyingDebris, rageEffect } from './effects.js';
|
||||||
import { countAutoSorted, countCorrect, countMistake, countMixer, countPoopDestination, countPoopSpawned, countTrash, countTruckCargo } from './stats.js';
|
import { countAutoSorted, countCorrect, countMistake, countMixer, countPoopDestination, countPoopSpawned, countTrash, countTruckCargo } from './stats.js';
|
||||||
import { emitGameEvent } from './gameEvents.js';
|
|
||||||
import { eggSpawnDelayMultiplier, recordFarmProduction, recordConveyorPass, recordAutoScan, recordFacilityProcess, updateProcessingCooldowns, setFacilityCooldownAfterProcess, updateRepairman } from './maintenance.js';
|
import { eggSpawnDelayMultiplier, recordFarmProduction, recordConveyorPass, recordAutoScan, recordFacilityProcess, updateProcessingCooldowns, setFacilityCooldownAfterProcess, updateRepairman } from './maintenance.js';
|
||||||
|
|
||||||
export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck }) {
|
export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck }) {
|
||||||
function currentSpawnDelay(farm) {
|
function currentSpawnDelay(farm) {
|
||||||
return nextSpawnDelay(farm) * eggSpawnDelayMultiplier(farm) / productionMultiplier(game);
|
return nextSpawnDelay(farm) * eggSpawnDelayMultiplier(farm) * eggProductionDelayMultiplier(game) / productionMultiplier(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnChick(farm) {
|
function spawnChick(farm) {
|
||||||
const data = routeFromFarmToScanner(game, farm, true);
|
|
||||||
const farmCenter = cellCenter(farm.col, farm.row);
|
const farmCenter = cellCenter(farm.col, farm.row);
|
||||||
if (!data) {
|
const maxOutputs = 1 + extraEggOutletCount(game);
|
||||||
floating(game, farmCenter.x, farmCenter.y - 18, 'NO BELT', THEME.danger);
|
const routes = [];
|
||||||
return false;
|
const usedStarts = new Set();
|
||||||
|
const maxAttempts = Math.max(maxOutputs * 5, 5);
|
||||||
|
for (let attempt = 0; attempt < maxAttempts && routes.length < maxOutputs; attempt += 1) {
|
||||||
|
const data = routeFromFarmToScanner(game, farm, true);
|
||||||
|
if (!data) break;
|
||||||
|
const start = data.route?.[0];
|
||||||
|
if (!start) continue;
|
||||||
|
const startKey = `${Math.round(start.x)}:${Math.round(start.y)}`;
|
||||||
|
if (usedStarts.has(startKey) && routes.length > 0) continue;
|
||||||
|
usedStarts.add(startKey);
|
||||||
|
routes.push(data);
|
||||||
}
|
}
|
||||||
|
if (!routes.length) {
|
||||||
|
suppressUnconnectedFarmSpawn(farm, farmCenter);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let spawned = 0;
|
||||||
|
let blocked = 0;
|
||||||
|
for (const data of routes) {
|
||||||
const start = data.route[0];
|
const start = data.route[0];
|
||||||
const blocker = spawnTileBlocker(start);
|
const blocker = spawnTileBlocker(start);
|
||||||
if (blocker) {
|
if (blocker) {
|
||||||
blocker.stoppedTimer = 0.85;
|
blocker.stoppedTimer = 0.85;
|
||||||
return false;
|
blocked += 1;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
const chick = createChick(game, data);
|
const chick = createChick(game, data);
|
||||||
if (chick.sex === 'poop') countPoopSpawned(game);
|
if (chick.sex === 'poop') countPoopSpawned(game);
|
||||||
game.chicks.push(chick);
|
game.chicks.push(chick);
|
||||||
recordFarmProduction(game, farm);
|
recordFarmProduction(game, farm);
|
||||||
|
spawned += 1;
|
||||||
|
}
|
||||||
|
if (spawned > 0) {
|
||||||
spawnPulse(game, farmCenter.x, farmCenter.y);
|
spawnPulse(game, farmCenter.x, farmCenter.y);
|
||||||
|
if (spawned > 1) floating(game, farmCenter.x, farmCenter.y - 28, `x${spawned}`, THEME.green);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
return blocked === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function suppressUnconnectedFarmSpawn(farm, farmCenter) {
|
||||||
|
if ((farm.noRouteNoticeCooldown || 0) <= 0) {
|
||||||
|
floating(game, farmCenter.x, farmCenter.y - 24, 'NO BELT: PAUSED', THEME.warn);
|
||||||
|
farm.noRouteNoticeCooldown = 2.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function spawnTileBlocker(start) {
|
function spawnTileBlocker(start) {
|
||||||
return game.chicks.find(ch => ch.stage !== 'flying' && Math.hypot(ch.x - start.x, ch.y - start.y) < GRID.cell * 0.68) || null;
|
return game.chicks.find(ch => ch.stage !== 'flying' && Math.hypot(ch.x - start.x, ch.y - start.y) < GRID.cell * 0.68) || null;
|
||||||
|
|
@ -59,6 +89,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
}
|
}
|
||||||
if (producing && game.timeLeft > 0) {
|
if (producing && game.timeLeft > 0) {
|
||||||
for (const farm of game.eggFarms) {
|
for (const farm of game.eggFarms) {
|
||||||
|
if (farm.noRouteNoticeCooldown > 0) farm.noRouteNoticeCooldown = Math.max(0, farm.noRouteNoticeCooldown - dt);
|
||||||
farm.nextSpawn -= dt;
|
farm.nextSpawn -= dt;
|
||||||
if (farm.nextSpawn <= 0) {
|
if (farm.nextSpawn <= 0) {
|
||||||
const spawned = spawnChick(farm);
|
const spawned = spawnChick(farm);
|
||||||
|
|
@ -81,7 +112,6 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
if (game.chicks.length === 0) completeTurn();
|
if (game.chicks.length === 0) completeTurn();
|
||||||
else if (game.shutdownTimeLeft <= 0) blowOffRemainingForCleanup();
|
else if (game.shutdownTimeLeft <= 0) blowOffRemainingForCleanup();
|
||||||
}
|
}
|
||||||
onGameOverCheck();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function blowOffRemainingForCleanup() {
|
function blowOffRemainingForCleanup() {
|
||||||
|
|
@ -94,7 +124,6 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
for (const scanner of game.scanners) scanner.queue = [];
|
for (const scanner of game.scanners) scanner.queue = [];
|
||||||
updateCongestion();
|
updateCongestion();
|
||||||
if (victims.length) {
|
if (victims.length) {
|
||||||
emitGameEvent(game, 'CleanupBlowoff', { count: victims.length });
|
|
||||||
const cx = victims.reduce((sum, chick) => sum + chick.x, 0) / victims.length;
|
const cx = victims.reduce((sum, chick) => sum + chick.x, 0) / victims.length;
|
||||||
const cy = victims.reduce((sum, chick) => sum + chick.y, 0) / victims.length;
|
const cy = victims.reduce((sum, chick) => sum + chick.y, 0) / victims.length;
|
||||||
shockwave(game, cx, cy, THEME.warn, 58);
|
shockwave(game, cx, cy, THEME.warn, 58);
|
||||||
|
|
@ -229,6 +258,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
function updateScannerQueues(dt) {
|
function updateScannerQueues(dt) {
|
||||||
for (const scanner of game.scanners) {
|
for (const scanner of game.scanners) {
|
||||||
if (scanner.cooldown > 0) scanner.cooldown = Math.max(0, scanner.cooldown - dt);
|
if (scanner.cooldown > 0) scanner.cooldown = Math.max(0, scanner.cooldown - dt);
|
||||||
|
if (scanner.keyPressTime > 0) scanner.keyPressTime = Math.max(0, scanner.keyPressTime - dt);
|
||||||
positionScannerQueue(scanner);
|
positionScannerQueue(scanner);
|
||||||
if (scanner.kind !== 'auto' || scanner.cooldown > 0 || !scanner.queue.length) continue;
|
if (scanner.kind !== 'auto' || scanner.cooldown > 0 || !scanner.queue.length) continue;
|
||||||
const id = scanner.queue[0];
|
const id = scanner.queue[0];
|
||||||
|
|
@ -254,7 +284,10 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
function sortSlot(slot, side) {
|
function sortSlot(slot, side) {
|
||||||
if (game.phase !== 'running') return;
|
if (game.phase !== 'running') return;
|
||||||
const scanner = scannerBySlot(game, slot);
|
const scanner = scannerBySlot(game, slot);
|
||||||
if (!scanner || !scanner.queue.length) return;
|
if (!scanner) return;
|
||||||
|
scanner.keyPressTime = 0.18;
|
||||||
|
scanner.keyPressSide = side;
|
||||||
|
if (!scanner.queue.length) return;
|
||||||
const index = game.chicks.findIndex(c => c.id === scanner.queue[0]);
|
const index = game.chicks.findIndex(c => c.id === scanner.queue[0]);
|
||||||
if (index >= 0) sortChickByIndex(index, side, false);
|
if (index >= 0) sortChickByIndex(index, side, false);
|
||||||
}
|
}
|
||||||
|
|
@ -307,7 +340,6 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
countMistake(game);
|
countMistake(game);
|
||||||
sludgeEffect(game, x, y);
|
sludgeEffect(game, x, y);
|
||||||
floating(game, x, y - 18, `-${yen(penalty)}`, THEME.danger);
|
floating(game, x, y - 18, `-${yen(penalty)}`, THEME.danger);
|
||||||
onGameOverCheck();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const amount = applyMixerIncome(game);
|
const amount = applyMixerIncome(game);
|
||||||
|
|
@ -315,7 +347,6 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
else countMistake(game);
|
else countMistake(game);
|
||||||
meatEffect(game, x, y);
|
meatEffect(game, x, y);
|
||||||
floating(game, x, y - 18, `+${yen(amount)}`, THEME.green);
|
floating(game, x, y - 18, `+${yen(amount)}`, THEME.green);
|
||||||
onGameOverCheck();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTruck(index) {
|
function resolveTruck(index) {
|
||||||
|
|
@ -333,7 +364,6 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
if (isTarget) countCorrect(game);
|
if (isTarget) countCorrect(game);
|
||||||
else countMistake(game);
|
else countMistake(game);
|
||||||
floating(game, x, y - 18, `-${yen(penalty)}`, THEME.danger);
|
floating(game, x, y - 18, `-${yen(penalty)}`, THEME.danger);
|
||||||
onGameOverCheck();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (chick.sex === 'female') {
|
if (chick.sex === 'female') {
|
||||||
|
|
@ -355,7 +385,6 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
const penalty = applyWrongTruckFine(game);
|
const penalty = applyWrongTruckFine(game);
|
||||||
countMistake(game);
|
countMistake(game);
|
||||||
floating(game, x, y - 18, `-${yen(penalty)}`, THEME.danger);
|
floating(game, x, y - 18, `-${yen(penalty)}`, THEME.danger);
|
||||||
onGameOverCheck();
|
|
||||||
} else {
|
} else {
|
||||||
countMistake(game);
|
countMistake(game);
|
||||||
floating(game, x, y - 18, 'REJECT', THEME.warn);
|
floating(game, x, y - 18, 'REJECT', THEME.warn);
|
||||||
|
|
@ -471,7 +500,6 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
const first = parseKey(comp.cells[0]);
|
const first = parseKey(comp.cells[0]);
|
||||||
const fc = cellCenter(first.col, first.row);
|
const fc = cellCenter(first.col, first.row);
|
||||||
floating(game, fc.x, fc.y - 22, '100% JAM EXPLOSION', THEME.danger);
|
floating(game, fc.x, fc.y - 22, '100% JAM EXPLOSION', THEME.danger);
|
||||||
emitGameEvent(game, 'JamExplosion', { component: comp.id, victims: victims.length });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function explodeRoute(route, extras, label) {
|
function explodeRoute(route, extras, label) {
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,9 @@ export function productionMultiplier(game) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function currentPoopRate(game) {
|
export function currentPoopRate(game) {
|
||||||
return Math.min(0.9, POOP_RATE * (game.contractActive?.poopMultiplier || 1));
|
const feedCards = Math.max(0, Number(game?.cardEffects?.hatchingFeed) || 0);
|
||||||
|
const feedMultiplier = Math.pow(1.03, feedCards);
|
||||||
|
return Math.min(0.95, POOP_RATE * (game.contractActive?.poopMultiplier || 1) * feedMultiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function truckTarget(game) {
|
export function truckTarget(game) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
import { ECONOMY, FACILITY_DEFS, INCOME_FACILITY_IDS } from '../core/config.js';
|
import { ECONOMY, FACILITY_DEFS, INCOME_FACILITY_IDS } from '../core/config.js';
|
||||||
import { factoryValue, targetTruckCount, truckTarget } from './contracts.js';
|
import { factoryValue, targetTruckCount, truckTarget } from './contracts.js';
|
||||||
import { emitGameEvent } from './gameEvents.js';
|
|
||||||
|
function effectCount(game, id) {
|
||||||
|
return Math.max(0, Number(game?.cardEffects?.[id]) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function usedMachineActive(game) {
|
||||||
|
return !!game?.cardEffects?.usedMachineActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resaleRate(game, sameBuild) {
|
||||||
|
if (sameBuild) return 1;
|
||||||
|
return Math.min(0.8, 0.5 + 0.1 * Math.min(3, effectCount(game, 'recyclingSubsidy')));
|
||||||
|
}
|
||||||
|
|
||||||
|
function explosionDamageMultiplier(game) {
|
||||||
|
return Math.pow(0.95, effectCount(game, 'safetyCover'));
|
||||||
|
}
|
||||||
|
|
||||||
export function equipmentBasePrice(objOrHit) {
|
export function equipmentBasePrice(objOrHit) {
|
||||||
const obj = objOrHit?.ref || objOrHit || {};
|
const obj = objOrHit?.ref || objOrHit || {};
|
||||||
|
|
@ -12,8 +28,9 @@ export function equipmentBasePrice(objOrHit) {
|
||||||
return FACILITY_DEFS[obj.id]?.price || 0;
|
return FACILITY_DEFS[obj.id]?.price || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildPrice(id) {
|
export function buildPrice(id, game = null) {
|
||||||
return FACILITY_DEFS[id]?.price || 0;
|
const base = FACILITY_DEFS[id]?.price || 0;
|
||||||
|
return usedMachineActive(game) ? Math.ceil(base * 0.5) : base;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function facilityUpgradeCount(game, id) {
|
export function facilityUpgradeCount(game, id) {
|
||||||
|
|
@ -107,11 +124,11 @@ export function zundaTaxForCash(cash, game = null) {
|
||||||
export function fairiesTributeInfo(game, turn = game.turn) {
|
export function fairiesTributeInfo(game, turn = game.turn) {
|
||||||
const day = Math.max(1, Math.floor(Number(turn) || 1));
|
const day = Math.max(1, Math.floor(Number(turn) || 1));
|
||||||
const base = Math.ceil(day * ECONOMY.fairiesTribute.perDay);
|
const base = Math.ceil(day * ECONOMY.fairiesTribute.perDay);
|
||||||
const flatteryCount = Math.max(0, game?.cardEffects?.flattery || 0);
|
const flatteryCount = Math.max(0, game?.cardEffects?.fairiesFlatteryNext || game?.cardEffects?.flattery || 0);
|
||||||
const perCardReduction = Math.floor(10 * day / 10);
|
const multiplier = flatteryCount > 0 ? Math.pow(0.5, flatteryCount) : 1;
|
||||||
const reduction = Math.min(base, flatteryCount * perCardReduction);
|
const amount = Math.max(0, Math.ceil(base * multiplier));
|
||||||
const amount = Math.max(0, base - reduction);
|
const reduction = Math.max(0, base - amount);
|
||||||
return { day, base, flatteryCount, perCardReduction, reduction, amount };
|
return { day, base, flatteryCount, perCardReduction: Math.ceil(base * 0.5), reduction, amount };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resaleValueFor(hit, game) {
|
export function resaleValueFor(hit, game) {
|
||||||
|
|
@ -119,11 +136,14 @@ export function resaleValueFor(hit, game) {
|
||||||
const meta = hit?.type === 'conveyor' ? game?.conveyorMeta?.get(hit.oldKey || obj.id) : obj;
|
const meta = hit?.type === 'conveyor' ? game?.conveyorMeta?.get(hit.oldKey || obj.id) : obj;
|
||||||
const price = meta?.price || equipmentBasePrice(hit || obj);
|
const price = meta?.price || equipmentBasePrice(hit || obj);
|
||||||
const sameBuild = meta?.builtSession === game?.buildSession;
|
const sameBuild = meta?.builtSession === game?.buildSession;
|
||||||
return { amount: Math.max(0, Math.ceil(price * (sameBuild ? 1 : 0.5))), sameBuild, price };
|
if (meta?.noRefund || obj?.noRefund) return { amount: 0, sameBuild, price, rate: 0, noRefund: true };
|
||||||
|
const rate = resaleRate(game, sameBuild);
|
||||||
|
return { amount: Math.max(0, Math.ceil(price * rate)), sameBuild, price, rate, noRefund: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function explosionDamageForPrice(price) {
|
export function explosionDamageForPrice(price, game = null) {
|
||||||
return Math.max(1, Math.ceil((Number(price) || 0) / ECONOMY.explosionDamageDivisor));
|
const base = Math.max(1, Math.ceil((Number(price) || 0) / ECONOMY.explosionDamageDivisor));
|
||||||
|
return Math.max(1, Math.ceil(base * explosionDamageMultiplier(game)));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function upgradeCostFor(obj) {
|
export function upgradeCostFor(obj) {
|
||||||
|
|
@ -149,7 +169,6 @@ export function applyCashDelta(game, delta) {
|
||||||
if (delta >= 0) { game.stats.revenue += delta; game.totals.revenue += delta; }
|
if (delta >= 0) { game.stats.revenue += delta; game.totals.revenue += delta; }
|
||||||
else { game.stats.penalty += Math.abs(delta); game.totals.penalty += Math.abs(delta); }
|
else { game.stats.penalty += Math.abs(delta); game.totals.penalty += Math.abs(delta); }
|
||||||
game.totals.maxCash = Math.max(game.totals.maxCash, game.cash);
|
game.totals.maxCash = Math.max(game.totals.maxCash, game.cash);
|
||||||
emitGameEvent(game, 'CashDelta', { delta, before, after: game.cash });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyRevenue(game, amount) {
|
export function applyRevenue(game, amount) {
|
||||||
|
|
@ -175,7 +194,6 @@ export function refundCash(game, amount) {
|
||||||
game.stats.profit += amount;
|
game.stats.profit += amount;
|
||||||
game.totals.profit += amount;
|
game.totals.profit += amount;
|
||||||
game.totals.maxCash = Math.max(game.totals.maxCash, game.cash);
|
game.totals.maxCash = Math.max(game.totals.maxCash, game.cash);
|
||||||
emitGameEvent(game, 'CashRefund', { amount, before, after: game.cash });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyMixerIncome(game) {
|
export function applyMixerIncome(game) {
|
||||||
|
|
@ -210,7 +228,7 @@ export function applyWrongTruckFine(game) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyExplosionDamage(game, price) {
|
export function applyExplosionDamage(game, price) {
|
||||||
const amount = explosionDamageForPrice(price);
|
const amount = explosionDamageForPrice(price, game);
|
||||||
game.stats.explosionDamage += amount;
|
game.stats.explosionDamage += amount;
|
||||||
game.totals.explosionDamage += amount;
|
game.totals.explosionDamage += amount;
|
||||||
applyPenalty(game, amount);
|
applyPenalty(game, amount);
|
||||||
|
|
@ -240,6 +258,10 @@ export function collectZundaTax(game, basisCash = game.cash) {
|
||||||
|
|
||||||
export function collectFairiesTribute(game, turn = game.turn) {
|
export function collectFairiesTribute(game, turn = game.turn) {
|
||||||
const info = fairiesTributeInfo(game, turn);
|
const info = fairiesTributeInfo(game, turn);
|
||||||
|
if (game.cardEffects) {
|
||||||
|
game.cardEffects.fairiesFlatteryNext = 0;
|
||||||
|
if (game.cardEffects.flattery != null) delete game.cardEffects.flattery;
|
||||||
|
}
|
||||||
if (info.amount <= 0) return info;
|
if (info.amount <= 0) return info;
|
||||||
game.stats.fairiesTribute = (game.stats.fairiesTribute || 0) + info.amount;
|
game.stats.fairiesTribute = (game.stats.fairiesTribute || 0) + info.amount;
|
||||||
game.totals.fairiesTribute = (game.totals.fairiesTribute || 0) + info.amount;
|
game.totals.fairiesTribute = (game.totals.fairiesTribute || 0) + info.amount;
|
||||||
|
|
@ -247,6 +269,57 @@ export function collectFairiesTribute(game, turn = game.turn) {
|
||||||
return info;
|
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);
|
||||||
|
const blocks = Math.floor(poopTruck / 100);
|
||||||
|
const amount = cards > 0 && blocks > 0 ? blocks * 2000 * cards : 0;
|
||||||
|
if (amount > 0) {
|
||||||
|
game.stats.chemicalWeaponSubsidy = (game.stats.chemicalWeaponSubsidy || 0) + amount;
|
||||||
|
game.totals.chemicalWeaponSubsidy = (game.totals.chemicalWeaponSubsidy || 0) + amount;
|
||||||
|
applyRevenue(game, amount);
|
||||||
|
}
|
||||||
|
return { cards, poopTruck, blocks, amount };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectLoanRepayments(game) {
|
||||||
|
const effects = game?.cardEffects;
|
||||||
|
if (!effects || !Array.isArray(effects.loans) || !effects.loans.length) return { amount: 0, paidLoans: 0, remainingLoans: 0 };
|
||||||
|
let amount = 0;
|
||||||
|
let paidLoans = 0;
|
||||||
|
const nextLoans = [];
|
||||||
|
for (const loan of effects.loans) {
|
||||||
|
if ((loan.daysRemaining || 0) <= 0) continue;
|
||||||
|
const payment = Math.max(0, Math.ceil(loan.dailyPayment || 400));
|
||||||
|
amount += payment;
|
||||||
|
paidLoans += 1;
|
||||||
|
const daysRemaining = Math.max(0, (loan.daysRemaining || 0) - 1);
|
||||||
|
if (daysRemaining > 0) nextLoans.push({ ...loan, daysRemaining });
|
||||||
|
}
|
||||||
|
effects.loans = nextLoans;
|
||||||
|
if (amount > 0) {
|
||||||
|
game.stats.loanRepayment = (game.stats.loanRepayment || 0) + amount;
|
||||||
|
game.totals.loanRepayment = (game.totals.loanRepayment || 0) + amount;
|
||||||
|
applyPenalty(game, amount);
|
||||||
|
}
|
||||||
|
return { amount, paidLoans, remainingLoans: nextLoans.length };
|
||||||
|
}
|
||||||
|
|
||||||
export function finalScore(game) {
|
export function finalScore(game) {
|
||||||
const value = factoryValue(game);
|
const value = factoryValue(game);
|
||||||
const days = Math.max(1, game.totals.turnsCompleted || game.turn || 1);
|
const days = Math.max(1, game.totals.turnsCompleted || game.turn || 1);
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,37 @@ import { GRID, THEME } from '../core/config.js';
|
||||||
import { cellCenter, parseKey, key, yen } from '../core/utils.js';
|
import { cellCenter, parseKey, key, yen } from '../core/utils.js';
|
||||||
import { spendCash } from './economy.js';
|
import { spendCash } from './economy.js';
|
||||||
import { floating } from './effects.js';
|
import { floating } from './effects.js';
|
||||||
import { emitGameEvent } from './gameEvents.js';
|
|
||||||
|
|
||||||
const RULES = BALANCE.maintenance;
|
const RULES = BALANCE.maintenance;
|
||||||
|
|
||||||
|
function effectCount(game, id) {
|
||||||
|
return Math.max(0, Number(game?.cardEffects?.[id]) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function degradationUseMultiplier(game) {
|
||||||
|
const crowded = Math.min(1.4, 1 + 0.04 * Math.min(10, effectCount(game, 'crowdedFarming')));
|
||||||
|
const preventive = Math.pow(0.95, effectCount(game, 'preventiveMaintenance'));
|
||||||
|
return crowded * preventive;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function durabilityMultiplier(game) {
|
||||||
|
return Math.pow(1.03, effectCount(game, 'durabilityCoating'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function durabilityCapFor(game, type, qualityMultiplier = 1) {
|
||||||
|
const base = RULES.durability[type] || 1000;
|
||||||
|
return Math.max(1, Math.ceil(base * Math.max(0.1, qualityMultiplier || 1) * durabilityMultiplier(game)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function repairmanDailyCost(game) {
|
||||||
|
const base = RULES.repairman.dailyCost;
|
||||||
|
return game?.cardEffects?.laborExploitation ? Math.ceil(base * 0.7) : base;
|
||||||
|
}
|
||||||
|
|
||||||
|
function qualityMultiplierOf(objOrMeta) {
|
||||||
|
return Math.max(0.1, Number(objOrMeta?.durabilityBaseMultiplier) || 1);
|
||||||
|
}
|
||||||
|
|
||||||
export function ensureMaintenanceState(game) {
|
export function ensureMaintenanceState(game) {
|
||||||
if (!game.repairman) {
|
if (!game.repairman) {
|
||||||
game.repairman = {
|
game.repairman = {
|
||||||
|
|
@ -20,25 +47,27 @@ export function ensureMaintenanceState(game) {
|
||||||
}
|
}
|
||||||
for (const [k, meta] of game.conveyorMeta || []) {
|
for (const [k, meta] of game.conveyorMeta || []) {
|
||||||
if (meta.uses == null) meta.uses = 0;
|
if (meta.uses == null) meta.uses = 0;
|
||||||
if (meta.durability == null) meta.durability = RULES.durability.conveyor;
|
|
||||||
if (meta.maintenanceType == null) meta.maintenanceType = 'conveyor';
|
if (meta.maintenanceType == null) meta.maintenanceType = 'conveyor';
|
||||||
|
const desired = durabilityCapFor(game, 'conveyor', qualityMultiplierOf(meta));
|
||||||
|
if (meta.durability == null || meta.durability < desired) meta.durability = desired;
|
||||||
}
|
}
|
||||||
for (const farm of game.eggFarms || []) attachMaintenance(farm, 'eggFarm');
|
for (const farm of game.eggFarms || []) attachMaintenance(farm, 'eggFarm', game);
|
||||||
for (const scanner of game.scanners || []) {
|
for (const scanner of game.scanners || []) {
|
||||||
if (scanner.kind === 'auto') attachMaintenance(scanner, 'autoScanner');
|
if (scanner.kind === 'auto') attachMaintenance(scanner, 'autoScanner', game);
|
||||||
}
|
}
|
||||||
for (const f of Object.values(game.facilities || {})) {
|
for (const f of Object.values(game.facilities || {})) {
|
||||||
if (f.id === 'mixer') attachMaintenance(f, 'mixer');
|
if (f.id === 'mixer') attachMaintenance(f, 'mixer', game);
|
||||||
if (f.id === 'trash') attachMaintenance(f, 'trash');
|
if (f.id === 'trash') attachMaintenance(f, 'trash', game);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachMaintenance(obj, type) {
|
function attachMaintenance(obj, type, game = null) {
|
||||||
if (!obj) return;
|
if (!obj) return;
|
||||||
if (!obj.maintenance) obj.maintenance = {};
|
if (!obj.maintenance) obj.maintenance = {};
|
||||||
obj.maintenance.type = type;
|
obj.maintenance.type = type;
|
||||||
if (obj.maintenance.uses == null) obj.maintenance.uses = 0;
|
if (obj.maintenance.uses == null) obj.maintenance.uses = 0;
|
||||||
if (obj.maintenance.durability == null) obj.maintenance.durability = RULES.durability[type] || 1000;
|
const desired = game ? durabilityCapFor(game, type, qualityMultiplierOf(obj)) : (RULES.durability[type] || 1000);
|
||||||
|
if (obj.maintenance.durability == null || obj.maintenance.durability < desired) obj.maintenance.durability = desired;
|
||||||
}
|
}
|
||||||
|
|
||||||
function usesOf(target) {
|
function usesOf(target) {
|
||||||
|
|
@ -126,14 +155,14 @@ export function recordConveyorPass(game, conveyorKey) {
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
const meta = game.conveyorMeta?.get(conveyorKey);
|
const meta = game.conveyorMeta?.get(conveyorKey);
|
||||||
if (!meta) return;
|
if (!meta) return;
|
||||||
meta.uses = Math.min(meta.durability || RULES.durability.conveyor, (meta.uses || 0) + 1);
|
meta.uses = Math.min(meta.durability || RULES.durability.conveyor, (meta.uses || 0) + degradationUseMultiplier(game));
|
||||||
game.stats.conveyorPasses = (game.stats.conveyorPasses || 0) + 1;
|
game.stats.conveyorPasses = (game.stats.conveyorPasses || 0) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function recordFarmProduction(game, farm) {
|
export function recordFarmProduction(game, farm) {
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
attachMaintenance(farm, 'eggFarm');
|
attachMaintenance(farm, 'eggFarm');
|
||||||
farm.maintenance.uses = Math.min(farm.maintenance.durability, (farm.maintenance.uses || 0) + 1);
|
farm.maintenance.uses = Math.min(farm.maintenance.durability, (farm.maintenance.uses || 0) + degradationUseMultiplier(game));
|
||||||
game.stats.eggProduced = (game.stats.eggProduced || 0) + 1;
|
game.stats.eggProduced = (game.stats.eggProduced || 0) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +170,7 @@ export function recordAutoScan(game, scanner) {
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
if (!scanner || scanner.kind !== 'auto') return;
|
if (!scanner || scanner.kind !== 'auto') return;
|
||||||
attachMaintenance(scanner, 'autoScanner');
|
attachMaintenance(scanner, 'autoScanner');
|
||||||
scanner.maintenance.uses = Math.min(scanner.maintenance.durability, (scanner.maintenance.uses || 0) + 1);
|
scanner.maintenance.uses = Math.min(scanner.maintenance.durability, (scanner.maintenance.uses || 0) + degradationUseMultiplier(game));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function recordFacilityProcess(game, id) {
|
export function recordFacilityProcess(game, id) {
|
||||||
|
|
@ -149,7 +178,7 @@ export function recordFacilityProcess(game, id) {
|
||||||
const f = game.facilities?.[id];
|
const f = game.facilities?.[id];
|
||||||
if (!f || !['mixer', 'trash'].includes(id)) return;
|
if (!f || !['mixer', 'trash'].includes(id)) return;
|
||||||
attachMaintenance(f, id);
|
attachMaintenance(f, id);
|
||||||
f.maintenance.uses = Math.min(f.maintenance.durability, (f.maintenance.uses || 0) + 1);
|
f.maintenance.uses = Math.min(f.maintenance.durability, (f.maintenance.uses || 0) + degradationUseMultiplier(game));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setFacilityCooldownAfterProcess(game, id) {
|
export function setFacilityCooldownAfterProcess(game, id) {
|
||||||
|
|
@ -189,11 +218,10 @@ function targetKey(t) { return t.type === 'conveyor' ? `conveyor:${t.key}` : `${
|
||||||
export function hireRepairmanForNextDay(game) {
|
export function hireRepairmanForNextDay(game) {
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
if (game.repairman.hiredForNextDay) return { ok: false, reason: 'Repairman already hired.' };
|
if (game.repairman.hiredForNextDay) return { ok: false, reason: 'Repairman already hired.' };
|
||||||
const cost = RULES.repairman.dailyCost;
|
const cost = repairmanDailyCost(game);
|
||||||
if (game.cash < cost) return { ok: false, reason: `Need ${yen(cost - game.cash)} more.` };
|
if (game.cash < cost) return { ok: false, reason: `Need ${yen(cost - game.cash)} more.` };
|
||||||
spendCash(game, cost);
|
spendCash(game, cost);
|
||||||
game.repairman.hiredForNextDay = true;
|
game.repairman.hiredForNextDay = true;
|
||||||
emitGameEvent(game, 'HireRepairman', { cost });
|
|
||||||
return { ok: true, cost };
|
return { ok: true, cost };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,12 +231,10 @@ export function activateRepairmanForDay(game) {
|
||||||
game.repairman.hiredForNextDay = false;
|
game.repairman.hiredForNextDay = false;
|
||||||
game.repairman.target = null;
|
game.repairman.target = null;
|
||||||
game.repairman.repairedToday = 0;
|
game.repairman.repairedToday = 0;
|
||||||
if (game.repairman.active) emitGameEvent(game, 'RepairmanStarted', { day: game.turn });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivateRepairman(game) {
|
export function deactivateRepairman(game) {
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
if (game.repairman.active) emitGameEvent(game, 'RepairmanStopped', { repairedPercent: Math.round(game.repairman.repairedToday || 0) });
|
|
||||||
game.repairman.active = false;
|
game.repairman.active = false;
|
||||||
game.repairman.target = null;
|
game.repairman.target = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ export function scannerById(game, id) { return game.scanners.find(s => s.id ===
|
||||||
export function scannerBySlot(game, slot) { return game.scanners.find(s => s.kind === 'manual' && s.slot === slot) || null; }
|
export function scannerBySlot(game, slot) { return game.scanners.find(s => s.kind === 'manual' && s.slot === slot) || null; }
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Port definitions: only exact receiver/output cells are valid connections.
|
// Port definitions. Ports prefer the exact connector cell, but also accept
|
||||||
// Visual adjacency is deliberately not treated as a connection.
|
// visually touching adjacent conveyor cells so drawn connections and routing match.
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
export function scannerConnector(scanner, type) {
|
export function scannerConnector(scanner, type) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -37,8 +37,27 @@ function exactConveyorCell(game, point) {
|
||||||
return conveyorAt(game, point) ? [{ ...point }] : [];
|
return conveyorAt(game, point) ? [{ ...point }] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kept for explicit non-port use. Ports and validation intentionally use exact
|
function visualPortConveyorCells(game, point, blocked = [], preferred = []) {
|
||||||
// cells only; visual adjacency is not treated as a connection.
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphPortConveyorCells(game, point, blocked = [], preferred = []) {
|
||||||
|
const cells = visualPortConveyorCells(game, point, blocked, preferred);
|
||||||
|
const exact = cells.filter(p => !p.viaTolerance);
|
||||||
|
return exact.length ? exact : cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kept for explicit non-port use.
|
||||||
export function exactOrAdjacentConveyorCells(game, point, blocked = [], preferred = []) {
|
export function exactOrAdjacentConveyorCells(game, point, blocked = [], preferred = []) {
|
||||||
if (!point || !inGrid(point.col, point.row)) return [];
|
if (!point || !inGrid(point.col, point.row)) return [];
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
@ -57,11 +76,13 @@ export function exactOrAdjacentConveyorCells(game, point, blocked = [], preferre
|
||||||
// Strict connection tests
|
// Strict connection tests
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
export function scannerInputCells(game, scanner) {
|
export function scannerInputCells(game, scanner) {
|
||||||
return exactConveyorCell(game, scannerConnector(scanner, 'inputA'));
|
const connector = scannerConnector(scanner, 'inputA');
|
||||||
|
return visualPortConveyorCells(game, connector, [ { col: scanner.col, row: scanner.row } ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function outputStartCells(game, scanner, side) {
|
function outputStartCells(game, scanner, side) {
|
||||||
return exactConveyorCell(game, scannerConnector(scanner, side));
|
const connector = scannerConnector(scanner, side);
|
||||||
|
return visualPortConveyorCells(game, connector, [ { col: scanner.col, row: scanner.row } ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function facilityEntryPoint(game, dest) {
|
export function facilityEntryPoint(game, dest) {
|
||||||
|
|
@ -81,11 +102,11 @@ export function facilityEntryCell(game, dest) {
|
||||||
export function facilityEndpointCells(game, dest) {
|
export function facilityEndpointCells(game, dest) {
|
||||||
const f = game.facilities[dest];
|
const f = game.facilities[dest];
|
||||||
if (!f?.entry) return [];
|
if (!f?.entry) return [];
|
||||||
return exactConveyorCell(game, f.entry);
|
return visualPortConveyorCells(game, f.entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function portHasConveyor(game, port) {
|
export function portHasConveyor(game, port) {
|
||||||
return exactConveyorCell(game, port).length > 0;
|
return visualPortConveyorCells(game, port).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scannerPortHasConveyor(game, scanner, type) {
|
export function scannerPortHasConveyor(game, scanner, type) {
|
||||||
|
|
@ -206,21 +227,24 @@ function indexGraphPorts(game, graph) {
|
||||||
const ports = {};
|
const ports = {};
|
||||||
for (const type of ['inputA', 'left', 'right']) {
|
for (const type of ['inputA', 'left', 'right']) {
|
||||||
const cell = scannerConnector(scanner, type);
|
const cell = scannerConnector(scanner, type);
|
||||||
const cellKey = cell ? graphNodeKey(cell.col, cell.row) : null;
|
const cells = graphPortConveyorCells(game, cell, [ { col: scanner.col, row: scanner.row } ])
|
||||||
const connected = !!cellKey && graph.cells.has(cellKey);
|
.filter(p => graph.cells.has(graphNodeKey(p.col, p.row)));
|
||||||
ports[type] = { type, cell, key: connected ? cellKey : null, connected };
|
const cellKeys = cells.map(p => graphNodeKey(p.col, p.row));
|
||||||
|
const connected = cellKeys.length > 0;
|
||||||
|
ports[type] = { type, cell, cells, keys: cellKeys, key: cellKeys[0] || null, connected };
|
||||||
graph.metrics.scannerPorts += 1;
|
graph.metrics.scannerPorts += 1;
|
||||||
if (connected) graph.metrics.connectedScannerPorts += 1;
|
if (connected) graph.metrics.connectedScannerPorts += 1;
|
||||||
if (type === 'inputA' && connected) graph.scannerInputs.set(cellKey, scanner.id);
|
if (type === 'inputA') for (const cellKey of cellKeys) graph.scannerInputs.set(cellKey, scanner.id);
|
||||||
}
|
}
|
||||||
graph.scannerPorts.set(scanner.id, ports);
|
graph.scannerPorts.set(scanner.id, ports);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [id, f] of Object.entries(game.facilities)) {
|
for (const [id, f] of Object.entries(game.facilities)) {
|
||||||
if (!f.entry) continue;
|
if (!f.entry) continue;
|
||||||
const cellKey = graphNodeKey(f.entry.col, f.entry.row);
|
const cells = graphPortConveyorCells(game, f.entry).filter(p => graph.cells.has(graphNodeKey(p.col, p.row)));
|
||||||
const connected = graph.cells.has(cellKey);
|
const cellKeys = cells.map(p => graphNodeKey(p.col, p.row));
|
||||||
graph.facilityPorts.set(id, { id, cell: { ...f.entry }, key: connected ? cellKey : null, connected });
|
const connected = cellKeys.length > 0;
|
||||||
|
graph.facilityPorts.set(id, { id, cell: { ...f.entry }, cells, keys: cellKeys, key: cellKeys[0] || null, connected });
|
||||||
graph.metrics.facilityPorts += 1;
|
graph.metrics.facilityPorts += 1;
|
||||||
if (connected) graph.metrics.connectedFacilityPorts += 1;
|
if (connected) graph.metrics.connectedFacilityPorts += 1;
|
||||||
}
|
}
|
||||||
|
|
@ -263,21 +287,6 @@ export function graphSummary(graph) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function factoryGraphMetrics(game) {
|
|
||||||
const graph = ensureFactoryGraph(game);
|
|
||||||
const routeIssues = validateFactoryGraph(game, graph, { includeRoutes: true });
|
|
||||||
const maxCongestion = game.congestion?.size ? Math.max(...[...game.congestion.values()].map(c => c.ratio || 0), 0) : 0;
|
|
||||||
const queued = game.scanners.reduce((sum, scanner) => sum + (scanner.queue?.length || 0), 0);
|
|
||||||
return {
|
|
||||||
...graphSummary(graph),
|
|
||||||
issues: routeIssues,
|
|
||||||
activeItems: game.chicks.length,
|
|
||||||
queuedItems: queued,
|
|
||||||
movingItems: Math.max(0, game.chicks.length - queued),
|
|
||||||
maxCongestion
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Conveyor graph and pathfinding
|
// Conveyor graph and pathfinding
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
@ -387,9 +396,33 @@ function routeWithConnector(fromPoint, connector, cells, extraEnd = null) {
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routeToScannerCenter(cells, scanner) {
|
||||||
|
const points = routePoints(cells);
|
||||||
|
const input = scannerConnector(scanner, 'inputA');
|
||||||
|
if (input) appendPoint(points, cellCenter(input.col, input.row));
|
||||||
|
appendPoint(points, scannerCenter(scanner));
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
function routeWithScannerTarget(fromPoint, connector, cells, targetScanner) {
|
||||||
|
const points = [{ x: fromPoint.x, y: fromPoint.y }];
|
||||||
|
appendPoint(points, cellCenter(connector.col, connector.row));
|
||||||
|
for (const p of routePoints(cells)) appendPoint(points, p);
|
||||||
|
const input = scannerConnector(targetScanner, 'inputA');
|
||||||
|
if (input) appendPoint(points, cellCenter(input.col, input.row));
|
||||||
|
appendPoint(points, scannerCenter(targetScanner));
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
export function chooseRoundRobin(game, id, candidates, advance = false) {
|
export function chooseRoundRobin(game, id, candidates, advance = false) {
|
||||||
if (!candidates.length) return null;
|
if (!candidates.length) return null;
|
||||||
const sorted = [...candidates].sort((a, b) => {
|
const sorted = [...candidates].sort((a, b) => {
|
||||||
|
const ba = a.roleBias ?? 0;
|
||||||
|
const bb = b.roleBias ?? 0;
|
||||||
|
if (ba !== bb) return ba - bb;
|
||||||
|
const la = a.cells?.length ?? a.route?.length ?? Number.POSITIVE_INFINITY;
|
||||||
|
const lb = b.cells?.length ?? b.route?.length ?? Number.POSITIVE_INFINITY;
|
||||||
|
if (la !== lb) return la - lb;
|
||||||
const ka = a.key || JSON.stringify(a.route || a.cells || a);
|
const ka = a.key || JSON.stringify(a.route || a.cells || a);
|
||||||
const kb = b.key || JSON.stringify(b.route || b.cells || b);
|
const kb = b.key || JSON.stringify(b.route || b.cells || b);
|
||||||
return ka.localeCompare(kb);
|
return ka.localeCompare(kb);
|
||||||
|
|
@ -424,7 +457,7 @@ export function isFacilityEndpoint(game, p, dest, connector) {
|
||||||
if (sameCell(p, connector)) return false;
|
if (sameCell(p, connector)) return false;
|
||||||
const graph = ensureFactoryGraph(game);
|
const graph = ensureFactoryGraph(game);
|
||||||
const port = graph.facilityPorts.get(dest);
|
const port = graph.facilityPorts.get(dest);
|
||||||
return !!port?.connected && graphNodeKey(p.col, p.row) === port.key;
|
return !!port?.connected && (port.keys || [port.key]).includes(graphNodeKey(p.col, p.row));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function routeFromFarmToScanner(game, farm, advance = false) {
|
export function routeFromFarmToScanner(game, farm, advance = false) {
|
||||||
|
|
@ -444,7 +477,7 @@ export function routeFromFarmToScanner(game, farm, advance = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const chosen = chooseRoundRobin(game, `farm:${farm.id}`, candidates, advance);
|
const chosen = chooseRoundRobin(game, `farm:${farm.id}`, candidates, advance);
|
||||||
if (chosen) return { scannerId: chosen.scanner.id, route: routePoints(chosen.cells, scannerCenter(chosen.scanner)) };
|
if (chosen) return { scannerId: chosen.scanner.id, route: routeToScannerCenter(chosen.cells, chosen.scanner) };
|
||||||
const fallback = chooseRoundRobin(game, `farm:${farm.id}:output`, starts.map(p => ({ key: key(p.col, p.row), cells: [p] })), advance);
|
const fallback = chooseRoundRobin(game, `farm:${farm.id}:output`, starts.map(p => ({ key: key(p.col, p.row), cells: [p] })), advance);
|
||||||
return fallback ? { scannerId: null, route: routePoints(fallback.cells) } : null;
|
return fallback ? { scannerId: null, route: routePoints(fallback.cells) } : null;
|
||||||
}
|
}
|
||||||
|
|
@ -455,7 +488,7 @@ export function outputRoute(game, side, fromPoint, scannerId, advance = false) {
|
||||||
const graph = ensureFactoryGraph(game);
|
const graph = ensureFactoryGraph(game);
|
||||||
const port = graph.scannerPorts.get(scanner.id)?.[side];
|
const port = graph.scannerPorts.get(scanner.id)?.[side];
|
||||||
const connector = scannerConnector(scanner, side);
|
const connector = scannerConnector(scanner, side);
|
||||||
const starts = port?.connected ? [graphCell(port.key)] : [];
|
const starts = port?.connected ? (port.keys || [port.key]).filter(Boolean).map(graphCell) : [];
|
||||||
if (!connector || !starts.length) return null;
|
if (!connector || !starts.length) return null;
|
||||||
const dest = scannerOutputs(scanner)[side];
|
const dest = scannerOutputs(scanner)[side];
|
||||||
if (dest === 'scanner-role-1') return routeToNextScanner(game, scanner, side, fromPoint, connector, starts, advance);
|
if (dest === 'scanner-role-1') return routeToNextScanner(game, scanner, side, fromPoint, connector, starts, advance);
|
||||||
|
|
@ -464,21 +497,23 @@ export function outputRoute(game, side, fromPoint, scannerId, advance = false) {
|
||||||
|
|
||||||
function routeToNextScanner(game, scanner, side, fromPoint, connector, starts, advance) {
|
function routeToNextScanner(game, scanner, side, fromPoint, connector, starts, advance) {
|
||||||
const graph = ensureFactoryGraph(game);
|
const graph = ensureFactoryGraph(game);
|
||||||
const targets = game.scanners.filter(s => s.role === 1 && s.id !== scanner.id && graph.scannerPorts.get(s.id)?.inputA?.connected);
|
const targets = game.scanners.filter(s => s.id !== scanner.id && graph.scannerPorts.get(s.id)?.inputA?.connected);
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
for (const start of starts) {
|
for (const start of starts) {
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
const targetKey = graph.scannerPorts.get(target.id)?.inputA?.key;
|
const targetKey = graph.scannerPorts.get(target.id)?.inputA?.key;
|
||||||
if (!targetKey) continue;
|
if (!targetKey) continue;
|
||||||
const isGoal = p => graphNodeKey(p.col, p.row) === targetKey;
|
const targetKeys = (graph.scannerPorts.get(target.id)?.inputA?.keys || [targetKey]).filter(Boolean);
|
||||||
|
const isGoal = p => targetKeys.includes(graphNodeKey(p.col, p.row));
|
||||||
isGoal.cacheKey = `toScanner:${start.col},${start.row}:${target.id}:v${graph.version}`;
|
isGoal.cacheKey = `toScanner:${start.col},${start.row}:${target.id}:v${graph.version}`;
|
||||||
const routes = bfsAllRoutes(game, start, isGoal);
|
const routes = bfsAllRoutes(game, start, isGoal);
|
||||||
for (const cells of routes) candidates.push({ key: `start${key(start.col, start.row)}:s${target.id}:${cells.length}:${key(cells[cells.length - 1].col, cells[cells.length - 1].row)}`, target, cells });
|
const roleBias = target.role === 1 ? 0 : 1;
|
||||||
|
for (const cells of routes) candidates.push({ key: `${roleBias}:start${key(start.col, start.row)}:s${target.id}:${cells.length}:${key(cells[cells.length - 1].col, cells[cells.length - 1].row)}`, target, cells, roleBias });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const chosen = chooseRoundRobin(game, `scanner:${scanner.id}:${side}:toRole1`, candidates, advance);
|
const chosen = chooseRoundRobin(game, `scanner:${scanner.id}:${side}:toRole1`, candidates, advance);
|
||||||
if (!chosen) return null;
|
if (!chosen) return null;
|
||||||
return { destination: 'scanner', nextScannerId: chosen.target.id, route: routeWithConnector(fromPoint, connector, chosen.cells, scannerCenter(chosen.target)) };
|
return { destination: 'scanner', nextScannerId: chosen.target.id, route: routeWithScannerTarget(fromPoint, connector, chosen.cells, chosen.target) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function routeToFacility(game, scanner, side, fromPoint, connector, starts, dest, advance) {
|
function routeToFacility(game, scanner, side, fromPoint, connector, starts, dest, advance) {
|
||||||
|
|
@ -487,7 +522,9 @@ function routeToFacility(game, scanner, side, fromPoint, connector, starts, dest
|
||||||
if (!port?.connected) return null;
|
if (!port?.connected) return null;
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
for (const start of starts) {
|
for (const start of starts) {
|
||||||
const isGoal = p => graphNodeKey(p.col, p.row) === port.key;
|
const goalKeys = (port.keys || [port.key]).filter(Boolean);
|
||||||
|
if (!goalKeys.length) continue;
|
||||||
|
const isGoal = p => goalKeys.includes(graphNodeKey(p.col, p.row));
|
||||||
isGoal.cacheKey = `toFacility:${start.col},${start.row}:${dest}:v${graph.version}`;
|
isGoal.cacheKey = `toFacility:${start.col},${start.row}:${dest}:v${graph.version}`;
|
||||||
const routes = bfsAllRoutes(game, start, isGoal);
|
const routes = bfsAllRoutes(game, start, isGoal);
|
||||||
for (const cells of routes) candidates.push({ key: `start${key(start.col, start.row)}:${dest}:${cells.length}:${key(cells[cells.length - 1].col, cells[cells.length - 1].row)}`, cells });
|
for (const cells of routes) candidates.push({ key: `start${key(start.col, start.row)}:${dest}:${cells.length}:${key(cells[cells.length - 1].col, cells[cells.length - 1].row)}`, cells });
|
||||||
|
|
@ -500,6 +537,38 @@ function routeToFacility(game, scanner, side, fromPoint, connector, starts, dest
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Connection validation: uses FactoryGraph as the single source of truth.
|
// Connection validation: uses FactoryGraph as the single source of truth.
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
function outputRouteReachesFacility(game, scanner, 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 (['mixer', 'trash', 'truck'].includes(plan.destination)) return true;
|
||||||
|
if (plan.destination === 'scanner') {
|
||||||
|
const next = scannerById(game, plan.nextScannerId);
|
||||||
|
if (outputRouteReachesFacility(game, next, visited)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function minimumStartConnectionIssues(game) {
|
||||||
|
const graph = ensureFactoryGraph(game);
|
||||||
|
for (const farm of game.eggFarms || []) {
|
||||||
|
const route = routeFromFarmToScanner(game, farm, false);
|
||||||
|
if (!route?.scannerId) continue;
|
||||||
|
const scanner = scannerById(game, route.scannerId);
|
||||||
|
if (outputRouteReachesFacility(game, scanner)) return [];
|
||||||
|
}
|
||||||
|
const farmCount = game.eggFarms?.length || 0;
|
||||||
|
const exitCount = ['mixer', 'trash', 'truck'].filter(id => graph.facilityPorts.get(id)?.connected).length;
|
||||||
|
if (!farmCount) return ['Build at least one EGG before starting the next day.'];
|
||||||
|
if (!graph.metrics.farmOutputs) return ['Connect at least one EGG to a conveyor.'];
|
||||||
|
if (!game.scanners?.length) return ['Build at least one scanner and connect it to an EGG route.'];
|
||||||
|
if (!exitCount) return ['Connect at least one receiver exit: Mixer, Shredder, or Truck.'];
|
||||||
|
return ['Connect at least one EGG through a scanner to any valid exit.'];
|
||||||
|
}
|
||||||
|
|
||||||
function scannerName(scanner) {
|
function scannerName(scanner) {
|
||||||
if (scanner.kind === 'manual') return `S${(scanner.slot ?? 0) + 1}`;
|
if (scanner.kind === 'manual') return `S${(scanner.slot ?? 0) + 1}`;
|
||||||
return `Auto Scanner #${scanner.id}`;
|
return `Auto Scanner #${scanner.id}`;
|
||||||
|
|
@ -545,7 +614,7 @@ export function validateFactoryGraph(game, graph = ensureFactoryGraph(game), opt
|
||||||
}
|
}
|
||||||
|
|
||||||
export function facilityConnectionIssues(game) {
|
export function facilityConnectionIssues(game) {
|
||||||
return validateFactoryGraph(game, ensureFactoryGraph(game), { includeRoutes: true });
|
return minimumStartConnectionIssues(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function factoryReady(game) {
|
export function factoryReady(game) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { nearestGridEdge, layoutFacilityOnEdge } from '../core/entities.js';
|
||||||
import { farmAt, scannerAt, scannerCenter, refreshRoutingAfterEdit } from './routing.js';
|
import { farmAt, scannerAt, scannerCenter, refreshRoutingAfterEdit } from './routing.js';
|
||||||
import { snapshot } from './history.js';
|
import { snapshot } from './history.js';
|
||||||
import { buildPrice } from './economy.js';
|
import { buildPrice } from './economy.js';
|
||||||
import { emitGameEvent } from './gameEvents.js';
|
|
||||||
|
|
||||||
export function createSelectionSystem({ game, canvasPoint, updatePanels, equipmentAtPoint, fail, pointInGrid, rectOfFacility, rectsOverlap }) {
|
export function createSelectionSystem({ game, canvasPoint, updatePanels, equipmentAtPoint, fail, pointInGrid, rectOfFacility, rectsOverlap }) {
|
||||||
function selectionToken(item) {
|
function selectionToken(item) {
|
||||||
|
|
@ -236,7 +235,6 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
||||||
|
|
||||||
function finishGroupDrag() {
|
function finishGroupDrag() {
|
||||||
if (!game.groupDrag) return;
|
if (!game.groupDrag) return;
|
||||||
if (game.groupDrag.committed) emitGameEvent(game, 'MoveEquipment', { count: game.groupDrag.selections?.length || 0 });
|
|
||||||
refreshRoutingAfterEdit(game);
|
refreshRoutingAfterEdit(game);
|
||||||
game.groupDrag = null;
|
game.groupDrag = null;
|
||||||
updatePanels();
|
updatePanels();
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import { BUILD_TOOL_IDS, CONVEYOR_SPEED_MAX, CONTRACT_EVENT_FIRST_TURN, FACILITY_DEFS, MACHINE_FACILITY_IDS, STARTING_CASH } from '../core/config.js';
|
import { BUILD_TOOL_IDS, CONVEYOR_SPEED_MAX, CONTRACT_EVENT_FIRST_TURN, FACILITY_DEFS, MACHINE_FACILITY_IDS, STARTING_CASH } from '../core/config.js';
|
||||||
import { yen } from '../core/utils.js';
|
import { yen } from '../core/utils.js';
|
||||||
import { TEXT } from '../core/text.js';
|
import { TEXT } from '../core/text.js';
|
||||||
import { routeFromFarmToScanner, factoryReady, facilityConnectionIssues, factoryGraphMetrics } from './routing.js';
|
import { routeFromFarmToScanner, factoryReady, facilityConnectionIssues } from './routing.js';
|
||||||
import { buildPrice, fairiesTributeInfo, finalScore, maleTruckPenalty, mixerPoopPenalty, truckPoopPenalty, upgradedMixerPrice, zundaTaxInfo } from './economy.js';
|
import { buildPrice, fairiesTributeInfo, finalScore, maleTruckPenalty, mixerPoopPenalty, truckPoopPenalty, zundaTaxInfo } from './economy.js';
|
||||||
import { truckTarget } from './contracts.js';
|
import { truckTarget } from './contracts.js';
|
||||||
import { conveyorSpeedForGame } from './cards.js';
|
import { conveyorSpeedForGame } from './cards.js';
|
||||||
import { recentGameEvents } from './gameEvents.js';
|
import { maintenanceSummary, repairmanDailyCost } from './maintenance.js';
|
||||||
import { maintenanceSummary } from './maintenance.js';
|
|
||||||
import { BALANCE } from '../core/balance.js';
|
|
||||||
|
|
||||||
export function createUISystem({ game, ui, build, startGame, beginCardDraft, activeQueuedChick }) {
|
export function createUISystem({ game, ui, build, startGame, beginCardDraft, activeQueuedChick }) {
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
|
|
@ -16,14 +14,23 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
|
|
||||||
function displaySpeed() { return Math.round(conveyorSpeedForGame(game)); }
|
function displaySpeed() { return Math.round(conveyorSpeedForGame(game)); }
|
||||||
|
|
||||||
|
function noAffordableBuildTools() {
|
||||||
|
if (game.phase !== 'build' || game.cardDraft?.pending || game.cardTargetPick?.pending) return false;
|
||||||
|
let hasBuildableOption = false;
|
||||||
|
for (const id of BUILD_TOOL_IDS) {
|
||||||
|
if (MACHINE_FACILITY_IDS.includes(id) && !!game.facilities[id]) continue;
|
||||||
|
hasBuildableOption = true;
|
||||||
|
if (game.cash >= buildPrice(id, game)) return false;
|
||||||
|
}
|
||||||
|
return hasBuildableOption;
|
||||||
|
}
|
||||||
|
|
||||||
function updatePanels() {
|
function updatePanels() {
|
||||||
updateToolButtons();
|
updateToolButtons();
|
||||||
updateBuildStatus();
|
updateBuildStatus();
|
||||||
updateFacilityPanel();
|
|
||||||
updateContractPanel();
|
updateContractPanel();
|
||||||
updateTurnSummary();
|
updateTurnSummary();
|
||||||
updateHistoryButtons();
|
updateHistoryButtons();
|
||||||
updateDebugPanel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateToolButtons() {
|
function updateToolButtons() {
|
||||||
|
|
@ -34,7 +41,9 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
for (const id of BUILD_TOOL_IDS) {
|
for (const id of BUILD_TOOL_IDS) {
|
||||||
const btn = ui.buttons[id];
|
const btn = ui.buttons[id];
|
||||||
if (!btn) continue;
|
if (!btn) continue;
|
||||||
const price = buildPrice(id);
|
const price = buildPrice(id, game);
|
||||||
|
const priceSpan = btn.querySelector('span');
|
||||||
|
if (priceSpan) priceSpan.textContent = id === 'conveyor' ? `${yen(price)} / tile` : yen(price);
|
||||||
const uniqueAlreadyBuilt = MACHINE_FACILITY_IDS.includes(id) && !!game.facilities[id];
|
const uniqueAlreadyBuilt = MACHINE_FACILITY_IDS.includes(id) && !!game.facilities[id];
|
||||||
const unaffordable = game.cash < price;
|
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;
|
||||||
|
|
@ -50,18 +59,26 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
if (ui.buttons.erase) ui.buttons.erase.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending;
|
if (ui.buttons.erase) ui.buttons.erase.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending;
|
||||||
if (ui.hireRepairmanButton) {
|
if (ui.hireRepairmanButton) {
|
||||||
const hired = !!game.repairman?.hiredForNextDay;
|
const hired = !!game.repairman?.hiredForNextDay;
|
||||||
const cost = BALANCE.maintenance.repairman.dailyCost;
|
const cost = repairmanDailyCost(game);
|
||||||
const blocked = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || hired || game.cash < cost;
|
const blocked = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || hired || game.cash < cost;
|
||||||
ui.hireRepairmanButton.disabled = blocked;
|
ui.hireRepairmanButton.disabled = blocked;
|
||||||
ui.hireRepairmanButton.classList.toggle('active', hired);
|
ui.hireRepairmanButton.classList.toggle('active', hired);
|
||||||
ui.hireRepairmanButton.classList.toggle('unaffordable', game.cash < cost);
|
ui.hireRepairmanButton.classList.toggle('unaffordable', game.cash < cost);
|
||||||
ui.hireRepairmanButton.title = hired ? 'Repairman is hired for the next production phase.' : `Hire for ${yen(cost)} for one day.`;
|
ui.hireRepairmanButton.title = hired ? 'Repairman is hired for the next production phase.' : `Hire for ${yen(cost)} for one day. Repairs 3% durability per second.`;
|
||||||
}
|
}
|
||||||
|
const cashout = noAffordableBuildTools();
|
||||||
|
ui.buttons.nextTurn?.classList.toggle('cashout-emphasis', cashout);
|
||||||
|
ui.buttons.erase?.classList.toggle('cashout-emphasis', cashout);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBuildStatus() {
|
function updateBuildStatus() {
|
||||||
|
const cashout = noAffordableBuildTools();
|
||||||
ui.buildStatus.textContent = game.phase === 'build'
|
ui.buildStatus.textContent = game.phase === 'build'
|
||||||
? (game.cardTargetPick?.pending ? 'Select a white-highlighted machine on the map.' : (game.cardDraft?.pending ? 'Choose upgrade card before building.' : TEXT.status.build(game.buildTool)))
|
? (game.cardTargetPick?.pending
|
||||||
|
? 'Select an upgrade target on the map.'
|
||||||
|
: (game.cardDraft?.pending
|
||||||
|
? 'Choose upgrade card before building.'
|
||||||
|
: (cashout ? 'No affordable equipment. Start the next day or sell equipment to recover cash.' : TEXT.status.build(game.buildTool))))
|
||||||
: TEXT.status.sorting;
|
: TEXT.status.sorting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,45 +91,24 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
const connected = game.eggFarms.filter(f => routeFromFarmToScanner(game, f)?.scannerId).length;
|
const connected = game.eggFarms.filter(f => routeFromFarmToScanner(game, f)?.scannerId).length;
|
||||||
const issues = facilityConnectionIssues(game);
|
const issues = facilityConnectionIssues(game);
|
||||||
const tax = zundaTaxInfo(game.cash, game);
|
const tax = zundaTaxInfo(game.cash, game);
|
||||||
const tribute = fairiesTributeInfo(game, game.turn);
|
const tributeDay = game.phase === 'running' ? Math.max(1, (game.totals.turnsCompleted || 0) + 1) : Math.max(1, Number(game.turn) || 1);
|
||||||
|
const tribute = fairiesTributeInfo(game, tributeDay);
|
||||||
|
const lastTribute = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.fairiesTribute || 0 : null;
|
||||||
|
const lastTributeReduction = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.reduction || 0 : 0;
|
||||||
ui.turnSummary.innerHTML = [
|
ui.turnSummary.innerHTML = [
|
||||||
`Seed: <b>${escapeHtml(game.runSeed || 'none')}</b> | Blocked: <b>${game.blockedCells?.size || 0}</b> cells`,
|
`Seed: <b>${escapeHtml(game.runSeed || 'none')}</b> | Blocked: <b>${game.blockedCells?.size || 0}</b> cells`,
|
||||||
`Truck target: <b>${truckTarget(game).toUpperCase()}</b> | Male fine: <b>-${yen(maleTruckPenalty(game))}</b>`,
|
`Truck target: <b>${truckTarget(game).toUpperCase()}</b> | Male fine: <b>-${yen(maleTruckPenalty(game))}</b>`,
|
||||||
`Poop fine: <b>Mixer -${yen(mixerPoopPenalty(game))}</b> / <b>Shipment -${yen(truckPoopPenalty(game))}</b>`,
|
`Poop fine: <b>Mixer -${yen(mixerPoopPenalty(game))}</b> / <b>Shipment -${yen(truckPoopPenalty(game))}</b>`,
|
||||||
`ZUNDA TAX on Next Day: <b>-${yen(tax.tax)}</b> | taxable ${yen(tax.taxable)} × ${tax.ratePercent}% | exemption ${yen(tax.exemption)} | 95% at ${yen(tax.maxRateCash)}`,
|
`ZUNDA TAX on Next Day: <b>-${yen(tax.tax)}</b> | taxable ${yen(tax.taxable)} × ${tax.ratePercent}% | exemption ${yen(tax.exemption)} | 95% at ${yen(tax.maxRateCash)}`,
|
||||||
`Fairies tribute on Next Day: <b>-${yen(tribute.amount)}</b>${tribute.reduction ? ` | flattery -${yen(tribute.reduction)}` : ''}`,
|
lastTribute != null
|
||||||
|
? `Fairies tribute paid before Build: <b>-${yen(lastTribute)}</b>${lastTributeReduction ? ` | Flattery -${yen(lastTributeReduction)}` : ''}`
|
||||||
|
: `Fairies tribute before Build: <b>-${yen(tribute.amount)}</b>${tribute.reduction ? ` | Flattery -${yen(tribute.reduction)}` : ''}`,
|
||||||
`Maintenance: <b>worst ${maintenanceSummary(game).worst.percent}% dirty (${maintenanceSummary(game).worst.label})</b> | Repairman: <b>${game.repairman?.hiredForNextDay ? 'hired next day' : game.repairman?.active ? 'working' : 'none'}</b>`,
|
`Maintenance: <b>worst ${maintenanceSummary(game).worst.percent}% dirty (${maintenanceSummary(game).worst.label})</b> | Repairman: <b>${game.repairman?.hiredForNextDay ? 'hired next day' : game.repairman?.active ? 'working' : 'none'}</b>`,
|
||||||
`Card draft: <b>${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'}</b> | Belt: <b>${displaySpeed()}px/s</b> | Farms connected: <b>${connected}/${game.eggFarms.length}</b>`,
|
`Card draft: <b>${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'}</b> | Belt: <b>${displaySpeed()}px/s</b> | Farms with scanner route: <b>${connected}/${game.eggFarms.length}</b>`,
|
||||||
|
game.cardEffects?.usedMachineActive ? '<span class="cash-negative">Procurement: USED MACHINE MODE / no refunds / 60% durability</span>' : '',
|
||||||
issues.length ? `<span class="cash-negative">Blocked: ${issues[0]}</span>` : `<span class="cash-positive">${TEXT.status.allPortsConnected}</span>`
|
issues.length ? `<span class="cash-negative">Blocked: ${issues[0]}</span>` : `<span class="cash-positive">${TEXT.status.allPortsConnected}</span>`
|
||||||
].join('<br>');
|
].join('<br>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateDebugPanel() {
|
|
||||||
if (!ui.debugObservations && !ui.debugEventLog) return;
|
|
||||||
let m = null;
|
|
||||||
try { m = factoryGraphMetrics(game); } catch (_err) { m = null; }
|
|
||||||
if (ui.debugObservations) {
|
|
||||||
if (!m) ui.debugObservations.innerHTML = '<strong>FactoryGraph</strong><span>not available</span>';
|
|
||||||
else ui.debugObservations.innerHTML = [
|
|
||||||
'<strong>FactoryGraph</strong>',
|
|
||||||
`<span>v${m.version} | cells ${m.conveyorCells} | edges ${m.edges} | components ${m.components}</span>`,
|
|
||||||
`<span>ports scanner ${m.scannerPorts} / facility ${m.facilityPorts} | farm outputs ${m.farmOutputs}</span>`,
|
|
||||||
`<span>dead ends ${m.deadEnds} | crosses ${m.crosses} | max jam ${Math.round((m.maxCongestion || 0) * 100)}%</span>`,
|
|
||||||
`<span>items moving ${m.movingItems} / queued ${m.queuedItems} / total ${m.activeItems}</span>`,
|
|
||||||
`<span>blocked cells ${(game.blockedCells?.size || 0)} | worst dirt ${maintenanceSummary(game).worst.percent}% ${escapeHtml(maintenanceSummary(game).worst.label)}</span>`,
|
|
||||||
(m.issues?.length ? `<span class="cash-negative">issue: ${m.issues[0]}</span>` : '<span class="cash-positive">graph valid</span>')
|
|
||||||
].join('');
|
|
||||||
}
|
|
||||||
if (ui.debugEventLog) {
|
|
||||||
const events = recentGameEvents(game, 8);
|
|
||||||
ui.debugEventLog.innerHTML = ['<strong>Event Log</strong>', ...events.map(ev => {
|
|
||||||
const payload = Object.entries(ev.payload || {}).map(([k, v]) => `${escapeHtml(k)}:${escapeHtml(v)}`).join(' ');
|
|
||||||
return `<span>D${ev.day} ${escapeHtml(ev.type)}${payload ? ` | ${payload}` : ''}</span>`;
|
|
||||||
})].join('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateContractPanel() {
|
function updateContractPanel() {
|
||||||
if (!ui.contractPanel) return;
|
if (!ui.contractPanel) return;
|
||||||
const offer = game.contractOffer;
|
const offer = game.contractOffer;
|
||||||
|
|
@ -141,20 +137,6 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
<span>Output x${offer.productionMultiplier} / Poop x${offer.poopMultiplier}</span>
|
<span>Output x${offer.productionMultiplier} / Poop x${offer.poopMultiplier}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFacilityPanel() {
|
|
||||||
const obj = build.selectedObject();
|
|
||||||
if (!obj) {
|
|
||||||
ui.facilityPanel.className = 'facility-panel-empty';
|
|
||||||
ui.facilityPanel.innerHTML = TEXT.status.equipmentPanelEmpty;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ui.facilityPanel.className = 'facility-card';
|
|
||||||
const lines = build.selectedInfoLines(obj);
|
|
||||||
if (obj.type === 'facility' && obj.id === 'trash') lines.push('Items are shredded; no stored contents are visualized.');
|
|
||||||
ui.facilityPanel.innerHTML = `<h3>${build.selectedTitle(obj)}</h3>${lines.map(x => `<p>${x}</p>`).join('')}<p><b>Upgrades come from pre-Build card choices.</b></p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
updateToolButtons();
|
updateToolButtons();
|
||||||
ui.shell.classList.toggle('phase-running', game.phase === 'running');
|
ui.shell.classList.toggle('phase-running', game.phase === 'running');
|
||||||
|
|
@ -171,7 +153,6 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
ui.timeLeft.closest?.('.hud-card')?.classList.toggle('time-critical', game.phase === 'running' && (game.timeLeft <= 10 || (game.timeLeft <= 0 && game.shutdownTimeLeft > 0)));
|
ui.timeLeft.closest?.('.hud-card')?.classList.toggle('time-critical', game.phase === 'running' && (game.timeLeft <= 10 || (game.timeLeft <= 0 && game.shutdownTimeLeft > 0)));
|
||||||
ui.phase.textContent = phaseLabel();
|
ui.phase.textContent = phaseLabel();
|
||||||
updatePriorityStrip();
|
updatePriorityStrip();
|
||||||
updateDebugPanel();
|
|
||||||
ui.turnProfit.textContent = yen(game.stats.profit);
|
ui.turnProfit.textContent = yen(game.stats.profit);
|
||||||
ui.turnProfit.classList.toggle('cash-negative', game.stats.profit < 0);
|
ui.turnProfit.classList.toggle('cash-negative', game.stats.profit < 0);
|
||||||
ui.turnProfit.classList.toggle('cash-positive', game.stats.profit > 0);
|
ui.turnProfit.classList.toggle('cash-positive', game.stats.profit > 0);
|
||||||
|
|
@ -184,9 +165,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
ui.buttons.s2Left.disabled = game.phase !== 'running' || !activeQueuedChick(1);
|
ui.buttons.s2Left.disabled = game.phase !== 'running' || !activeQueuedChick(1);
|
||||||
ui.buttons.s2Right.disabled = game.phase !== 'running' || !activeQueuedChick(1);
|
ui.buttons.s2Right.disabled = game.phase !== 'running' || !activeQueuedChick(1);
|
||||||
const nextTax = zundaTaxInfo(game.cash, game).tax;
|
const nextTax = zundaTaxInfo(game.cash, game).tax;
|
||||||
const nextTribute = fairiesTributeInfo(game, game.turn).amount;
|
ui.buttons.nextTurn.textContent = game.phase === 'build' && nextTax > 0 ? `Next Day - ZUNDA ${yen(nextTax)}` : TEXT.actions.nextDay;
|
||||||
const nextFees = nextTax + nextTribute;
|
|
||||||
ui.buttons.nextTurn.textContent = game.phase === 'build' && nextFees > 0 ? `Next Day - ZUNDA ${yen(nextTax)} / Fairies ${yen(nextTribute)}` : TEXT.actions.nextDay;
|
|
||||||
ui.buttons.nextTurn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || !factoryReady(game);
|
ui.buttons.nextTurn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || !factoryReady(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,10 +201,10 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
|
|
||||||
function showTitle() {
|
function showTitle() {
|
||||||
ui.modalTitle.textContent = TEXT.versionTitle;
|
ui.modalTitle.textContent = TEXT.versionTitle;
|
||||||
ui.modalBody.innerHTML = `<div class="route-cards"><div class="route-card"><span class="route-icon">M</span><strong>Male</strong><b>-></b><em>Mixer</em><small>S1: A</small></div><div class="route-card"><span class="route-icon">F</span><strong>Female</strong><b>-></b><em>Truck</em><small>S1: D, then S2: right</small></div><div class="route-card"><span class="route-icon">P</span><strong>Poop</strong><b>-></b><em>Shredder</em><small>S1: D, then S2: left</small></div></div><p>Build phase: connect all receiver ports before Next Day. One day is one run.</p>`;
|
ui.modalBody.innerHTML = `<div class="route-cards"><div class="route-card"><span class="route-icon male" aria-label="male chick">♂</span><strong>Male</strong><b>-></b><em>Mixer</em><small>S1: A</small></div><div class="route-card"><span class="route-icon female" aria-label="female chick">♀</span><strong>Female</strong><b>-></b><em>Truck</em><small>S1: D, then S2: right</small></div><div class="route-card"><span class="route-icon poop" aria-label="poop">💩</span><strong>Poop</strong><b>-></b><em>Shredder</em><small>S1: D, then S2: left</small></div></div><p>Build phase: connect at least one EGG through a scanner to Mixer, Shredder, or Truck before Next Day. One day is one run.</p>`;
|
||||||
ui.modalActions.innerHTML = '';
|
ui.modalActions.innerHTML = '';
|
||||||
ui.modalActions.appendChild(button(TEXT.actions.startGame, startGame, 'primary-button'));
|
ui.modalActions.appendChild(button(TEXT.actions.startGame, startGame, 'primary-button'));
|
||||||
ui.modal.classList.remove('equipment-popover');
|
ui.modal.classList.remove('equipment-popover', 'gameover-modal');
|
||||||
ui.modal.classList.add('visible');
|
ui.modal.classList.add('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,7 +215,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
<p><strong>Final profit: ${yen(r.profit)}</strong></p>`;
|
<p><strong>Final profit: ${yen(r.profit)}</strong></p>`;
|
||||||
ui.modalActions.innerHTML = '';
|
ui.modalActions.innerHTML = '';
|
||||||
ui.modalActions.appendChild(button('Choose Upgrade Card', beginCardDraft || hideModal, 'primary-button'));
|
ui.modalActions.appendChild(button('Choose Upgrade Card', beginCardDraft || hideModal, 'primary-button'));
|
||||||
ui.modal.classList.remove('equipment-popover');
|
ui.modal.classList.remove('equipment-popover', 'gameover-modal');
|
||||||
ui.modal.classList.add('visible');
|
ui.modal.classList.add('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,13 +225,16 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
const accuracy = processed ? Math.round(game.totals.correct / processed * 100) : 0;
|
const accuracy = processed ? Math.round(game.totals.correct / processed * 100) : 0;
|
||||||
const fs = finalScore(game);
|
const fs = finalScore(game);
|
||||||
ui.modalTitle.textContent = TEXT.phases.gameover;
|
ui.modalTitle.textContent = TEXT.phases.gameover;
|
||||||
ui.modalBody.innerHTML = `<p>Your cash went negative.</p>
|
ui.modalBody.innerHTML = `<p class="gameover-reason">Your cash went negative.</p>
|
||||||
<div class="formula-box"><strong>Final score formula</strong><br>Base = Cash + Total revenue + Factory value × 0.5 + Contract bonus × 0.5 = ${yen(fs.base)}<br>Daily earned = ceil(Base ÷ Days) = ceil(${yen(fs.base)} ÷ ${fs.days}) = ${yen(fs.dailyEarned)}<br>Score = max(0, floor(Base + Daily earned + Correct×5 - Total outflow - Day penalty)) = ${fs.score}</div>
|
<div class="result-grid gameover-summary"><div><strong>Final Score</strong><span>${fs.score}</span></div><div><strong>Survival</strong><span>${game.totals.turnsCompleted} days</span></div><div><strong>Final Cash</strong><span>${yen(game.cash)}</span></div><div><strong>Accuracy</strong><span>${accuracy}%</span></div></div>
|
||||||
<div class="result-grid"><div><strong>Final Score</strong><span>${fs.score}</span></div><div><strong>Survival</strong><span>${game.totals.turnsCompleted} days</span></div><div><strong>Final Cash</strong><span>${yen(game.cash)}</span></div><div><strong>Factory Value</strong><span>${yen(fs.factoryValue)}</span></div><div><strong>Total Revenue</strong><span>${yen(game.totals.revenue)}</span></div><div><strong>Daily Earned</strong><span>${yen(fs.dailyEarned)} / day</span></div><div><strong>Total Outflow</strong><span>${yen(game.totals.penalty)}</span></div><div><strong>ZUNDA TAX</strong><span>${yen(game.totals.zundaTax)}</span></div><div><strong>Fairies Tribute</strong><span>${yen(game.totals.fairiesTribute || 0)}</span></div><div><strong>Correct Sorts</strong><span>${game.totals.correct}</span></div><div><strong>Accuracy</strong><span>${accuracy}%</span></div><div><strong>Auto Sorted</strong><span>${game.totals.autoSorted}</span></div><div><strong>Poop Control</strong><span>${game.totals.poopTrash} trashed / ${game.totals.poopTruck} shipped / ${game.totals.poopMixer} mixer</span></div><div><strong>Explosion Penalty</strong><span>${yen(game.totals.explosionDamage)}</span></div><div><strong>Contract Bonus</strong><span>${yen(game.totals.contractBonus)}</span></div><div><strong>Day Penalty</strong><span>-${fs.dayPenalty}</span></div><div><strong>Contracts</strong><span>${game.totals.contractSuccess} success / ${game.totals.contractFailed} failed</span></div></div>`;
|
<details class="gameover-details"><summary>Score breakdown</summary>
|
||||||
|
<div class="formula-box compact"><strong>Formula</strong><br>Base = Cash + Total revenue + Factory value × 0.5 + Contract bonus × 0.5 = ${yen(fs.base)}<br>Daily earned = ceil(Base ÷ Days) = ${yen(fs.dailyEarned)}<br>Score = max(0, floor(Base + Daily earned + Correct×5 - Total outflow - Day penalty)) = ${fs.score}</div>
|
||||||
|
<div class="result-grid gameover-breakdown"><div><strong>Factory Value</strong><span>${yen(fs.factoryValue)}</span></div><div><strong>Total Revenue</strong><span>${yen(game.totals.revenue)}</span></div><div><strong>Daily Earned</strong><span>${yen(fs.dailyEarned)} / day</span></div><div><strong>Total Outflow</strong><span>${yen(game.totals.penalty)}</span></div><div><strong>ZUNDA TAX</strong><span>${yen(game.totals.zundaTax)}</span></div><div><strong>Fairies Tribute</strong><span>${yen(game.totals.fairiesTribute || 0)}</span></div><div><strong>Correct Sorts</strong><span>${game.totals.correct}</span></div><div><strong>Auto Sorted</strong><span>${game.totals.autoSorted}</span></div><div><strong>Poop Control</strong><span>${game.totals.poopTrash} trashed / ${game.totals.poopTruck} shipped / ${game.totals.poopMixer} mixer</span></div><div><strong>Explosion Penalty</strong><span>${yen(game.totals.explosionDamage)}</span></div><div><strong>Contract Bonus</strong><span>${yen(game.totals.contractBonus)}</span></div><div><strong>Day Penalty</strong><span>-${fs.dayPenalty}</span></div><div><strong>Contracts</strong><span>${game.totals.contractSuccess} success / ${game.totals.contractFailed} failed</span></div></div>
|
||||||
|
</details>`;
|
||||||
ui.modalActions.innerHTML = '';
|
ui.modalActions.innerHTML = '';
|
||||||
ui.modalActions.appendChild(button(TEXT.actions.restart, startGame, 'primary-button'));
|
ui.modalActions.appendChild(button(TEXT.actions.restart || 'Restart', startGame, 'primary-button restart-button'));
|
||||||
ui.modal.classList.remove('equipment-popover');
|
ui.modal.classList.remove('equipment-popover');
|
||||||
ui.modal.classList.add('visible');
|
ui.modal.classList.add('visible', 'gameover-modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
function button(text, onClick, className = 'primary-button') {
|
function button(text, onClick, className = 'primary-button') {
|
||||||
|
|
@ -265,16 +247,13 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideModal() {
|
function hideModal() {
|
||||||
ui.modal.classList.remove('visible', 'equipment-popover');
|
ui.modal.classList.remove('visible', 'equipment-popover', 'gameover-modal');
|
||||||
ui.modal.style.removeProperty('--popover-x');
|
ui.modal.style.removeProperty('--popover-x');
|
||||||
ui.modal.style.removeProperty('--popover-y');
|
ui.modal.style.removeProperty('--popover-y');
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkGameOver() {
|
function checkGameOver() {
|
||||||
if (game.phase !== 'gameover' && game.cash < 0) {
|
// Bankruptcy is intentionally checked only in completeTurn(), after all day-end revenue, tribute, and rescue-loan effects are settled.
|
||||||
game.phase = 'gameover';
|
|
||||||
showGameOver();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { updatePanels, updateUI, showTitle, showTurnResult, showGameOver, hideModal, checkGameOver };
|
return { updatePanels, updateUI, showTitle, showTurnResult, showGameOver, hideModal, checkGameOver };
|
||||||
|
|
|
||||||
262
styles.css
262
styles.css
|
|
@ -58,9 +58,7 @@ h1, h2, p { margin: 0; }
|
||||||
.tool-button.danger.active { background: #ffdada; box-shadow: inset 0 0 0 3px var(--danger), 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 { 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; }
|
.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; }
|
.mini-box { 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-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||||||
.facility-action { padding: 7px 9px; font-size: 9px; }
|
.facility-action { padding: 7px 9px; font-size: 9px; }
|
||||||
.facility-action.good { background: var(--green-soft); }
|
.facility-action.good { background: var(--green-soft); }
|
||||||
|
|
@ -184,7 +182,9 @@ h1, h2, p { margin: 0; }
|
||||||
/* v15 pre-build card draft */
|
/* v15 pre-build card draft */
|
||||||
.card-choices {
|
.card-choices {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(190px, 220px));
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
@ -204,6 +204,8 @@ h1, h2, p { margin: 0; }
|
||||||
.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 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 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.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; }
|
.muted-card-note { color: var(--muted); font-weight: 700; }
|
||||||
.card-targets { display: grid; gap: 8px; margin-top: 14px; }
|
.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 { 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); }
|
||||||
|
|
@ -219,8 +221,7 @@ body { font-size: 16px; }
|
||||||
.build-panel h2 { font-size: 13px; }
|
.build-panel h2 { font-size: 13px; }
|
||||||
.tool-button strong { font-size: 14px; }
|
.tool-button strong { font-size: 14px; }
|
||||||
.tool-button span { font-size: 11px; }
|
.tool-button span { font-size: 11px; }
|
||||||
.mini-box, .facility-panel-empty, .facility-card, .contract-card { font-size: 12px; }
|
.mini-box, .contract-card { font-size: 12px; }
|
||||||
.facility-card h3 { font-size: 14px; }
|
|
||||||
.sort-button { font-size: 14px; }
|
.sort-button { font-size: 14px; }
|
||||||
.modal-card h2 { font-size: 29px; }
|
.modal-card h2 { font-size: 29px; }
|
||||||
.card-choice strong { font-size: 16px; }
|
.card-choice strong { font-size: 16px; }
|
||||||
|
|
@ -246,40 +247,6 @@ body { font-size: 16px; }
|
||||||
.hover-tooltip em { display: block; font-style: normal; color: var(--green); font-weight: 900; margin-bottom: 7px; }
|
.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; }
|
.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 {
|
.card-choice.dud {
|
||||||
background: #eeeeee;
|
background: #eeeeee;
|
||||||
|
|
@ -291,34 +258,205 @@ body { font-size: 16px; }
|
||||||
.card-choice.dud span { background: #f8f8f8; }
|
.card-choice.dud span { background: #f8f8f8; }
|
||||||
.card-choice.flung {
|
.card-choice.flung {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform: translate(180px, -60px) rotate(22deg);
|
transform: translate(var(--fling-x, 480px), var(--fling-y, -320px)) rotate(var(--fling-r, 68deg)) scale(.68);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: transform .18s ease-in, opacity .18s ease-in;
|
transition: transform .34s cubic-bezier(.18,.84,.26,.99), opacity .28s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* v20 Phase 1 observability */
|
|
||||||
.debug-readout {
|
|
||||||
border: 2px solid var(--line);
|
.route-card .route-icon.male,
|
||||||
background: rgba(247,255,245,.92);
|
.route-card .route-icon.female,
|
||||||
padding: 6px;
|
.route-card .route-icon.poop {
|
||||||
display: grid;
|
display: inline-grid;
|
||||||
gap: 3px;
|
place-items: center;
|
||||||
line-height: 1.25;
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 3px solid var(--line, #102015);
|
||||||
|
background: var(--white, #fff);
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.debug-readout strong {
|
.route-card .route-icon.male { color: #2477ff; background: #d9ecff; }
|
||||||
display: block;
|
.route-card .route-icon.female { color: #d62a85; background: #ffe0f0; }
|
||||||
color: var(--ink);
|
.route-card .route-icon.poop { color: #7b4a23; background: #fff0d1; font-size: 24px; }
|
||||||
font-size: 12px;
|
.restart-button { min-width: 160px; }
|
||||||
text-transform: uppercase;
|
.restart-fallback-note { margin-top: 12px; color: var(--muted, #526456); font-weight: 700; }
|
||||||
letter-spacing: .08em;
|
|
||||||
}
|
/* v24.4 hover-only build-button flavor / cashout guidance / compact game-over */
|
||||||
.debug-readout span {
|
.tool-button .tool-flavor {
|
||||||
display: block;
|
display: block;
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: 8px;
|
||||||
overflow-wrap: anywhere;
|
line-height: 1.22;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
transition: max-height .12s ease, opacity .12s ease, margin-top .12s ease;
|
||||||
}
|
}
|
||||||
.debug-readout.event-log {
|
.tool-button:hover .tool-flavor {
|
||||||
max-height: 160px;
|
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;
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* v27.0 cleanup 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 { 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); }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue