visual
|
|
@ -76,3 +76,39 @@ Implemented events:
|
||||||
- Fixed duplicate Emergency Loan button in the bankruptcy decision modal.
|
- Fixed duplicate Emergency Loan button in the bankruptcy decision modal.
|
||||||
- Fixed stale debug-removal CSS fragments left from the previous audit pass.
|
- Fixed stale debug-removal CSS fragments left from the previous audit pass.
|
||||||
- Fixed corrupted score-breakdown formula text.
|
- Fixed corrupted score-breakdown formula text.
|
||||||
|
|
||||||
|
## v27.15 dark factory visual patch
|
||||||
|
|
||||||
|
- Wired `assets/dark_factory/facilities/` sprites into canvas rendering.
|
||||||
|
- Added pixelated rendering for facility sprites and dark factory grid/floor colors.
|
||||||
|
- Changed CSS theme from green/white panels to dark industrial panels with amber/red/blue accents.
|
||||||
|
- Moved UI emphasis: status HUD across the top, event/status strip down the left, build/equipment panel on the right, sorting controls at the bottom.
|
||||||
|
- Kept user-supplied chick/poop slots unmodified. Chick art is not auto-requested; existing poop compatibility path remains.
|
||||||
|
- Debug UI remains intentionally present for balancing builds.
|
||||||
|
|
||||||
|
|
||||||
|
## v27.17 verified visual patch
|
||||||
|
|
||||||
|
- Versioned CSS and JavaScript launch filenames prevent the old visual bundle from being reused from cache.
|
||||||
|
- Removed all player-facing remnants of wage-based Repairman hiring.
|
||||||
|
- Maintenance Room remains a 2×2 buildable object and dispatches one Repairman per room.
|
||||||
|
- Conveyor slats now animate perpendicular to travel, with blue-green rubber on Boost Conveyors.
|
||||||
|
- DEBUG collapsed width reduced to 58 px.
|
||||||
|
- Facility sprite scale aligned to grid-object pixel density.
|
||||||
|
- Scanner ducts, blocked-cell coverage, chick sex colors/symbols, metal/grime decoration, and card texture icons verified in browser.
|
||||||
|
|
||||||
|
## v27.18 UI and routing patch
|
||||||
|
|
||||||
|
- Moved event, belt speed, combo, and truck target indicators to a non-overlapping lower-left group.
|
||||||
|
- Reworked T-junction branch mode as explicit output selection: random or a valid fixed exit.
|
||||||
|
- Lightened floor tiles, added deterministic green-fluid stains, increased conveyor arrow contrast, and centered blocked-cell X marks.
|
||||||
|
- Converted card icons into translucent background watermarks and normalized modal button styling.
|
||||||
|
- Replaced Maintenance Room art with storage/break-room pixel art.
|
||||||
|
|
||||||
|
|
||||||
|
## v27.19 land/UI/dynamite patch
|
||||||
|
|
||||||
|
- Starting blocked ratio: 3%.
|
||||||
|
- Purchased-lot blocked ratio: base 3% + 0.5 percentage points per purchase, max 10%.
|
||||||
|
- Dynamite target selection is map-driven and consumes the card correctly after any blast.
|
||||||
|
- Duplicate event/status information removed.
|
||||||
|
|
|
||||||
20
PATCH_VERIFICATION.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# v27.19 verification
|
||||||
|
|
||||||
|
## Automated checks
|
||||||
|
|
||||||
|
- All source JavaScript files pass `node --check`.
|
||||||
|
- Production bundle `dist/game-v27.19.js` builds successfully.
|
||||||
|
- Initial blocked-cell generation: 6 / 200 cells = 3%.
|
||||||
|
- Purchased-lot ratios: purchase 1 = 3.5%, purchase 2 = 4.0%, purchase 14+ = 10% cap.
|
||||||
|
- A 10x10 lot at the cap generates 10 blocked cells.
|
||||||
|
- Dynamite empty-space click leaves target mode active.
|
||||||
|
- Clicking a chosen blocked cell removes that exact cell and consumes one charge.
|
||||||
|
- Escape after at least one blast finishes and consumes the card, preventing reuse.
|
||||||
|
- Browser check: no console/page errors, duplicate right-side event panel absent, Status contains no event/belt/combo/truck-target duplicates.
|
||||||
|
- Browser check: NEXT DAY Fairies fee remains inside the button.
|
||||||
|
|
||||||
|
## Visual checks
|
||||||
|
|
||||||
|
- Green floor residue is sparse and splattered.
|
||||||
|
- Blocked cells use subdued black/yellow hazard striping.
|
||||||
|
- Initial Status reports 6 blocked cells.
|
||||||
55
README.md
|
|
@ -1,46 +1,25 @@
|
||||||
# Chick Sorter v27.14 Balancing Build
|
# Chick Sorter v27.19 — Land and UI Patch
|
||||||
|
|
||||||
Open `index.html` in a browser.
|
Serve this folder with a static HTTP server and open `index.html`.
|
||||||
|
|
||||||
Production uses the committed `dist/game.js` bundle. Edit source files under
|
The launch page uses cache-busted files:
|
||||||
`src/`, then run `npm install` once and `npm run build` after JavaScript
|
|
||||||
changes to regenerate the bundle.
|
|
||||||
|
|
||||||
## Static server cache setup
|
- `styles-v27.19.css?build=2719`
|
||||||
|
- `dist/game-v27.19.js?build=2719`
|
||||||
|
|
||||||
Serve `index.html`, the bundled JavaScript, and CSS with cache validation
|
A `v27.19 VERIFIED` badge appears near the upper-right when the current build is loaded.
|
||||||
instead of import query strings. This prevents stale files while keeping reloads
|
|
||||||
fast.
|
|
||||||
|
|
||||||
Use the example in `deploy/` for your HTTP server:
|
## Build
|
||||||
|
|
||||||
- nginx: include `deploy/nginx-cache.conf` inside the server block.
|
Run `npm install` once, then `npm run build` after editing JavaScript or `styles.css`.
|
||||||
- Apache: copy or include `deploy/apache-cache.htaccess` from the site root.
|
|
||||||
- Caddy: adapt `deploy/Caddyfile-cache`.
|
|
||||||
|
|
||||||
After deploying, purge any CDN or reverse-proxy cache once.
|
## v27.19 changes
|
||||||
|
|
||||||
## Current baseline
|
- Initial no-build tiles are 3% of the starting 20x10 land.
|
||||||
|
- Each purchased 10x10 lot increases its own no-build ratio by 0.5 percentage points, capped at 10%.
|
||||||
- Build, move, box-select, sell, and repair factory equipment during Build phase.
|
- Floor slime is sparse, deterministic splatter instead of frequent puddles.
|
||||||
- Start the next day only when at least one EGG route reaches a scanner and then any valid exit.
|
- No-build tiles use subdued black/yellow hazard striping.
|
||||||
- Fairies tribute is paid when pressing Next Day, using the previous day's net profit as part of the calculation.
|
- The duplicate right-side event panel was removed.
|
||||||
- Bankruptcy is checked at day-end settlement after shipment/event/subsidy revenue. Temporary negative cash during production does not immediately end the game.
|
- The Status panel no longer repeats event, belt speed, combo, or truck target data.
|
||||||
- Cards use English names and descriptions with numeric effect summaries.
|
- NEXT DAY displays the Fairies fee on a contained second line.
|
||||||
- `poop.png` is the only bundled image asset currently used by default; other art falls back to canvas drawing.
|
- Dynamite lets the player select any highlighted no-build tiles, up to three. Empty clicks do not cancel selection.
|
||||||
|
|
||||||
## Controls
|
|
||||||
|
|
||||||
- S1 manual scanner: `A` = left / `D` = right.
|
|
||||||
- S2 manual scanner: `←` = left / `→` = right.
|
|
||||||
- Build phase: left-drag empty map for box selection; drag selected equipment to move it.
|
|
||||||
- Right-drag: pan.
|
|
||||||
- `Ctrl+Z` / `Ctrl+Y`: undo / redo.
|
|
||||||
- `Esc`: cancel upgrade-card target selection.
|
|
||||||
|
|
||||||
## Cleanup in this build
|
|
||||||
|
|
||||||
- Removed duplicate `src/index.html` and `src/styles.css`; root `index.html` and `styles.css` are the single launch surface.
|
|
||||||
- Debug panel is intentionally present for balancing builds. Do not remove it during cleanup unless explicitly requested. Its card menu is grouped and target-consuming cards can be applied to selected targets.
|
|
||||||
- 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.
|
|
||||||
|
|
|
||||||
19
assets/README_ASSETS_DARK_FACTORY.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Assets organization (prepared, not yet wired into the game)
|
||||||
|
|
||||||
|
This patch adds a **dark factory** art set without changing runtime references yet.
|
||||||
|
|
||||||
|
## Folder guide
|
||||||
|
- `assets/dark_factory/facilities/` : new factory equipment sprites
|
||||||
|
- `assets/dark_factory/previews/` : visual overview sheet
|
||||||
|
- `assets/ui/` : future UI asset buckets (buttons / panels / icons / debug)
|
||||||
|
- `assets/chicks/` : reserved for user-supplied chick sprites
|
||||||
|
- `assets/poop/` : reserved for user-supplied poop sprites
|
||||||
|
- `assets/_legacy/` : copied backup of older loose files
|
||||||
|
- `assets/images/` : left untouched for compatibility with the current build
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Grid base unit: **16x16**
|
||||||
|
- Non-grid UI object base unit: **32x32**
|
||||||
|
- Color count intentionally limited for a coarse pixel-art look
|
||||||
|
- Sprites are supplied as individual PNG files with transparency
|
||||||
|
- This patch **does not yet hook the new art into the live game**
|
||||||
3
assets/_legacy/images/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Image assets
|
||||||
|
|
||||||
|
`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`.
|
||||||
BIN
assets/_legacy/images/poop.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
1
assets/chicks/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Reserved for user-supplied chick sprites. Recommended base: 16x16 world sprites / 32x32 UI icons.\n
|
||||||
1
assets/dark_factory/facilities/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Dark factory facility sprites. See ../manifest.json for file roles and dimensions.\n
|
||||||
|
After Width: | Height: | Size: 423 B |
|
After Width: | Height: | Size: 473 B |
|
After Width: | Height: | Size: 575 B |
|
After Width: | Height: | Size: 563 B |
|
After Width: | Height: | Size: 548 B |
|
After Width: | Height: | Size: 534 B |
|
After Width: | Height: | Size: 426 B |
|
After Width: | Height: | Size: 480 B |
BIN
assets/dark_factory/facilities/conveyor/conveyor_turn_ne_16.png
Normal file
|
After Width: | Height: | Size: 581 B |
BIN
assets/dark_factory/facilities/conveyor/conveyor_turn_nw_16.png
Normal file
|
After Width: | Height: | Size: 565 B |
BIN
assets/dark_factory/facilities/conveyor/conveyor_turn_se_16.png
Normal file
|
After Width: | Height: | Size: 558 B |
BIN
assets/dark_factory/facilities/conveyor/conveyor_turn_sw_16.png
Normal file
|
After Width: | Height: | Size: 539 B |
BIN
assets/dark_factory/facilities/machines/auto_scanner_32x48.png
Normal file
|
After Width: | Height: | Size: 850 B |
BIN
assets/dark_factory/facilities/machines/egg_farm_16.png
Normal file
|
After Width: | Height: | Size: 314 B |
BIN
assets/dark_factory/facilities/machines/manual_scanner_32x48.png
Normal file
|
After Width: | Height: | Size: 714 B |
BIN
assets/dark_factory/facilities/machines/mixer_32.png
Normal file
|
After Width: | Height: | Size: 557 B |
BIN
assets/dark_factory/facilities/machines/mixer_compact_16.png
Normal file
|
After Width: | Height: | Size: 475 B |
BIN
assets/dark_factory/facilities/machines/shredder_32.png
Normal file
|
After Width: | Height: | Size: 410 B |
BIN
assets/dark_factory/facilities/machines/shredder_compact_16.png
Normal file
|
After Width: | Height: | Size: 326 B |
BIN
assets/dark_factory/facilities/utility/maintenance_room_32.png
Normal file
|
After Width: | Height: | Size: 843 B |
BIN
assets/dark_factory/facilities/utility/obstacle_crate_16.png
Normal file
|
After Width: | Height: | Size: 387 B |
BIN
assets/dark_factory/facilities/utility/repairman_16.png
Normal file
|
After Width: | Height: | Size: 222 B |
BIN
assets/dark_factory/facilities/vehicles/truck_32.png
Normal file
|
After Width: | Height: | Size: 496 B |
BIN
assets/dark_factory/facilities/vehicles/truck_compact_16.png
Normal file
|
After Width: | Height: | Size: 401 B |
103
assets/dark_factory/manifest.json
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
{
|
||||||
|
"theme": "dark_factory",
|
||||||
|
"grid_unit_px": 16,
|
||||||
|
"ui_unit_px": 32,
|
||||||
|
"palette_note": "restricted palette / coarse pixel art",
|
||||||
|
"facilities": [
|
||||||
|
{
|
||||||
|
"id": "conveyor",
|
||||||
|
"files": [
|
||||||
|
"conveyor_straight_h_16.png",
|
||||||
|
"conveyor_straight_v_16.png",
|
||||||
|
"conveyor_turn_ne_16.png",
|
||||||
|
"conveyor_turn_nw_16.png",
|
||||||
|
"conveyor_turn_se_16.png",
|
||||||
|
"conveyor_turn_sw_16.png"
|
||||||
|
],
|
||||||
|
"size": "16x16",
|
||||||
|
"folder": "facilities/conveyor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "boost_conveyor",
|
||||||
|
"files": [
|
||||||
|
"boost_conveyor_straight_h_16.png",
|
||||||
|
"boost_conveyor_straight_v_16.png",
|
||||||
|
"boost_conveyor_turn_ne_16.png",
|
||||||
|
"boost_conveyor_turn_nw_16.png",
|
||||||
|
"boost_conveyor_turn_se_16.png",
|
||||||
|
"boost_conveyor_turn_sw_16.png"
|
||||||
|
],
|
||||||
|
"size": "16x16",
|
||||||
|
"folder": "facilities/conveyor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "egg_farm",
|
||||||
|
"files": [
|
||||||
|
"egg_farm_16.png"
|
||||||
|
],
|
||||||
|
"size": "16x16",
|
||||||
|
"folder": "facilities/machines"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "manual_scanner",
|
||||||
|
"files": [
|
||||||
|
"manual_scanner_32x48.png"
|
||||||
|
],
|
||||||
|
"size": "32x48",
|
||||||
|
"footprint": "2x3 tiles",
|
||||||
|
"folder": "facilities/machines"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "auto_scanner",
|
||||||
|
"files": [
|
||||||
|
"auto_scanner_32x48.png"
|
||||||
|
],
|
||||||
|
"size": "32x48",
|
||||||
|
"footprint": "2x3 tiles",
|
||||||
|
"folder": "facilities/machines"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mixer",
|
||||||
|
"files": [
|
||||||
|
"mixer_32.png",
|
||||||
|
"mixer_compact_16.png"
|
||||||
|
],
|
||||||
|
"size": "32x32 + 16x16 compact",
|
||||||
|
"folder": "facilities/machines"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shredder",
|
||||||
|
"files": [
|
||||||
|
"shredder_32.png",
|
||||||
|
"shredder_compact_16.png"
|
||||||
|
],
|
||||||
|
"size": "32x32 + 16x16 compact",
|
||||||
|
"folder": "facilities/machines"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "truck",
|
||||||
|
"files": [
|
||||||
|
"truck_32.png",
|
||||||
|
"truck_compact_16.png"
|
||||||
|
],
|
||||||
|
"size": "32x32 + 16x16 compact",
|
||||||
|
"folder": "facilities/vehicles"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "repairman",
|
||||||
|
"files": [
|
||||||
|
"repairman_16.png"
|
||||||
|
],
|
||||||
|
"size": "16x16",
|
||||||
|
"folder": "facilities/utility"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "obstacle_crate",
|
||||||
|
"files": [
|
||||||
|
"obstacle_crate_16.png"
|
||||||
|
],
|
||||||
|
"size": "16x16",
|
||||||
|
"folder": "facilities/utility"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1
assets/dark_factory/previews/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Preview sheet(s) for quick inspection. These are reference only, not runtime assets.\n
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
BIN
assets/images/chick_female.png
Normal file
|
After Width: | Height: | Size: 430 B |
BIN
assets/images/chick_male.png
Normal file
|
After Width: | Height: | Size: 385 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 378 B |
1
assets/poop/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Reserved for user-supplied poop sprites. Recommended base: 16x16 world sprites / 32x32 UI icons.\n
|
||||||
BIN
assets/poop/poop.png
Normal file
|
After Width: | Height: | Size: 378 B |
1
assets/ui/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Future UI asset buckets. Nothing is wired yet. Use 32x32 as the base size for non-grid UI objects.\n
|
||||||
59
dist/game-v27.17.js
vendored
Normal file
59
dist/game-v27.18.js
vendored
Normal file
51
dist/game-v27.19.js
vendored
Normal file
74
dist/game.js
vendored
30
index.html
|
|
@ -3,12 +3,16 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Chick Sorter v27.14</title>
|
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
|
||||||
<link rel="stylesheet" href="./styles.css" />
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
|
<title>Chick Sorter v27.19 — Land and UI Patch</title>
|
||||||
|
<link rel="stylesheet" href="./styles-v27.19.css?build=2719" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="app">
|
<main id="app">
|
||||||
<section id="gameShell" class="game-shell" aria-label="Game screen">
|
<section id="gameShell" class="game-shell" data-build="v27.19" aria-label="Game screen">
|
||||||
|
<div class="build-stamp" aria-label="loaded build">v27.19 VERIFIED</div>
|
||||||
<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">
|
||||||
|
|
@ -17,7 +21,6 @@
|
||||||
<div class="hud-card profit"><span>NET</span><strong id="turnProfit">¥0</strong></div>
|
<div class="hud-card profit"><span>NET</span><strong id="turnProfit">¥0</strong></div>
|
||||||
<div class="hud-card"><span>DAY</span><strong id="turn">1</strong></div>
|
<div class="hud-card"><span>DAY</span><strong id="turn">1</strong></div>
|
||||||
<div class="hud-card compact"><span>PHASE</span><strong id="phaseLabel">Title</strong></div>
|
<div class="hud-card compact"><span>PHASE</span><strong id="phaseLabel">Title</strong></div>
|
||||||
<div class="hud-card combo"><span>COMBO</span><strong id="comboCount">0</strong></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hud hud-top-right" aria-label="Day result details">
|
<div class="hud hud-top-right" aria-label="Day result details">
|
||||||
|
|
@ -27,16 +30,17 @@
|
||||||
<div class="hud-card"><span>POOP</span><strong id="poopCount">0</strong></div>
|
<div class="hud-card"><span>POOP</span><strong id="poopCount">0</strong></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="priority-strip" aria-label="Important day info">
|
<div class="left-bottom-status" aria-label="Important day info">
|
||||||
<div id="eventBrief" class="event-brief">NO ACTIVE EVENT</div>
|
<div id="eventBrief" class="event-brief status-chip">NO ACTIVE EVENT</div>
|
||||||
<div id="targetBrief" class="target-brief">TRUCK TARGET: FEMALE</div>
|
<div id="speedBrief" class="speed-brief status-chip">BELT: 52 px/s</div>
|
||||||
<div id="speedBrief" class="speed-brief">BELT: 52 px/s</div>
|
<div class="combo-brief status-chip"><span>COMBO</span><strong id="comboCount">0</strong></div>
|
||||||
|
<div id="targetBrief" class="target-brief status-chip">TRUCK TARGET: FEMALE</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
|
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
|
||||||
<div class="panel-head">
|
<div class="panel-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>CHICK SORTER <span>v27.14</span></h1>
|
<h1>CHICK SORTER <span>v27.19</span></h1>
|
||||||
<p>Build, expand, connect at least one valid EGG route, then start the next day.</p>
|
<p>Build, expand, connect at least one valid EGG route, then start the next day.</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
|
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
|
||||||
|
|
@ -55,7 +59,7 @@
|
||||||
<button id="buildTrashButton" class="tool-button" type="button"><strong>Waste Shredder</strong><span>¥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>¥450</span></button>
|
<button id="buildTruckButton" class="tool-button" type="button"><strong>Truck</strong><span>¥450</span></button>
|
||||||
<button id="eraseButton" class="tool-button danger" type="button"><strong>Sell</strong><span>Sell or refund equipment</span></button>
|
<button id="eraseButton" class="tool-button danger" type="button"><strong>Sell</strong><span>Sell or refund equipment</span></button>
|
||||||
<button id="hireRepairmanButton" class="tool-button repair" type="button"><strong>Hire Repairman</strong><span>¥100 / next day</span></button>
|
<button id="buildMaintenanceRoomButton" class="tool-button repair" type="button"><strong>Maintenance Room</strong><span>¥220 / 2×2</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>
|
||||||
|
|
@ -64,10 +68,6 @@
|
||||||
<h2>Status</h2>
|
<h2>Status</h2>
|
||||||
<div id="turnSummary" class="mini-box"></div>
|
<div id="turnSummary" class="mini-box"></div>
|
||||||
</section>
|
</section>
|
||||||
<section class="rules-section compact-rules">
|
|
||||||
<h2>Next Event</h2>
|
|
||||||
<div id="eventPanel" class="event-card empty"></div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -110,6 +110,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="./dist/game.js"></script>
|
<script type="module" src="./dist/game-v27.19.js?build=2719"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
58
package-lock.json
generated
|
|
@ -1,19 +1,19 @@
|
||||||
{
|
{
|
||||||
"name": "zunda-shiwake",
|
"name": "zunda-shiwake",
|
||||||
"version": "27.13.0",
|
"version": "27.19.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "zunda-shiwake",
|
"name": "zunda-shiwake",
|
||||||
"version": "27.13.0",
|
"version": "27.19.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.25.0"
|
"esbuild": "^0.25.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||||
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||||
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
|
|
@ -114,7 +114,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
|
|
@ -131,7 +131,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
|
|
@ -148,7 +148,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
|
|
@ -165,7 +165,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
|
|
@ -182,7 +182,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||||
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
|
|
@ -199,7 +199,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||||
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
|
|
@ -233,7 +233,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||||
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
|
|
@ -250,7 +250,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
|
|
@ -267,7 +267,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
|
|
@ -284,7 +284,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
|
|
@ -301,7 +301,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||||
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
|
|
@ -318,7 +318,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
|
|
@ -335,7 +335,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
|
|
@ -352,7 +352,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
|
|
@ -369,7 +369,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openharmony-arm64": {
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||||
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
|
|
@ -386,7 +386,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
|
|
@ -403,7 +403,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
|
|
@ -420,7 +420,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
|
|
@ -437,7 +437,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
|
|
@ -453,7 +453,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "zunda-shiwake",
|
"name": "zunda-shiwake",
|
||||||
"version": "27.14.0",
|
"version": "27.19.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
||||||
"build": "npm run clean && esbuild src/game.js --bundle --format=esm --target=es2020 --minify --outfile=dist/game.js"
|
"build": "npm run clean && node -e \"require('fs').copyFileSync('styles.css', 'styles-v27.19.css')\" && esbuild src/game.js --bundle --format=esm --target=es2020 --minify --outfile=dist/game-v27.19.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.25.0"
|
"esbuild": "^0.25.0"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Central balance sheet for tuning gameplay.
|
// Central balance sheet for tuning gameplay.
|
||||||
// Edit this file first when adjusting prices, income, penalties, timing, card rates, or caps.
|
// Edit this file first when adjusting prices, income, penalties, timing, card rates, or caps.
|
||||||
export const BALANCE = {
|
export const BALANCE = {
|
||||||
version: 'v27.14 debug restore consistency patch',
|
version: 'v27.19 LAND + UI PATCH',
|
||||||
time: {
|
time: {
|
||||||
daySeconds: 60,
|
daySeconds: 60,
|
||||||
farmShutdownGraceSeconds: 10
|
farmShutdownGraceSeconds: 10
|
||||||
|
|
@ -24,7 +24,9 @@ export const BALANCE = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
map: {
|
map: {
|
||||||
blockedRatio: 0.075
|
blockedRatio: 0.03,
|
||||||
|
blockedExpansionIncrement: 0.005,
|
||||||
|
blockedRatioMax: 0.10
|
||||||
},
|
},
|
||||||
economy: {
|
economy: {
|
||||||
income: {
|
income: {
|
||||||
|
|
@ -251,7 +253,7 @@ export const BALANCE = {
|
||||||
rarity: 'rare',
|
rarity: 'rare',
|
||||||
type: 'cellAction',
|
type: 'cellAction',
|
||||||
target: 'blockedCell',
|
target: 'blockedCell',
|
||||||
description: 'Remove any 3 unbuildable cells',
|
description: 'Choose and remove up to 3 unbuildable cells.',
|
||||||
tags: ['RISK', 'RARE', 'ONE-SHOT']
|
tags: ['RISK', 'RARE', 'ONE-SHOT']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -307,7 +309,7 @@ export const BALANCE = {
|
||||||
title: 'Motivational Exploitation',
|
title: 'Motivational Exploitation',
|
||||||
rarity: 'ultraRare',
|
rarity: 'ultraRare',
|
||||||
type: 'instant',
|
type: 'instant',
|
||||||
description: 'Repairman labor cost -30%. The repairman smiles with bloodshot eyes. One time only.',
|
description: 'Repair room work time -30%. The repairman smiles with bloodshot eyes. One time only.',
|
||||||
tags: ['MAINTENANCE', 'ULTRA RARE', 'PASSIVE']
|
tags: ['MAINTENANCE', 'ULTRA RARE', 'PASSIVE']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -374,7 +376,6 @@ export const BALANCE = {
|
||||||
trash: 1.00
|
trash: 1.00
|
||||||
},
|
},
|
||||||
repairman: {
|
repairman: {
|
||||||
dailyCost: 100,
|
|
||||||
secondsPerOnePercent: 0.5,
|
secondsPerOnePercent: 0.5,
|
||||||
walkSpeed: 90
|
walkSpeed: 90
|
||||||
}
|
}
|
||||||
|
|
@ -411,6 +412,10 @@ export const BALANCE = {
|
||||||
truck: {
|
truck: {
|
||||||
id: 'truck', type: 'facility', name: 'Truck', shortName: 'TRUCK', price: 450,
|
id: 'truck', type: 'facility', name: 'Truck', shortName: 'TRUCK', price: 450,
|
||||||
buildable: true, upgradeable: true, incomeKey: 'truck', body: { w: 176, h: 138 }
|
buildable: true, upgradeable: true, incomeKey: 'truck', body: { w: 176, h: 138 }
|
||||||
|
},
|
||||||
|
maintenanceRoom: {
|
||||||
|
id: 'maintenanceRoom', type: 'maintenanceRoom', name: 'Maintenance Room', shortName: 'REPAIR ROOM', price: 220,
|
||||||
|
buildable: true, upgradeable: false, footprint: { cols: 2, rows: 2 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export const EGG_SPAWN_RANGES = BALANCE.production.eggSpawnRanges;
|
||||||
export const GRID = BALANCE.grid;
|
export const GRID = BALANCE.grid;
|
||||||
export const FACILITY_DEFS = BALANCE.facilities;
|
export const FACILITY_DEFS = BALANCE.facilities;
|
||||||
|
|
||||||
export const BUILD_TOOL_IDS = ['conveyor', 'boostConveyor', 'eggFarm', 'autoScanner', 'manualScanner', 'mixer', 'trash', 'truck'];
|
export const BUILD_TOOL_IDS = ['conveyor', 'boostConveyor', 'eggFarm', 'autoScanner', 'manualScanner', 'mixer', 'trash', 'truck', 'maintenanceRoom'];
|
||||||
export const MACHINE_FACILITY_IDS = ['mixer', 'trash', 'truck'];
|
export const MACHINE_FACILITY_IDS = ['mixer', 'trash', 'truck'];
|
||||||
export const INCOME_FACILITY_IDS = ['mixer', 'truck'];
|
export const INCOME_FACILITY_IDS = ['mixer', 'truck'];
|
||||||
|
|
||||||
|
|
@ -40,20 +40,20 @@ export const DIRS = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const THEME = {
|
export const THEME = {
|
||||||
bg: '#eefbea',
|
bg: '#111318',
|
||||||
ink: '#102015',
|
ink: '#090a0d',
|
||||||
muted: '#526456',
|
muted: '#8f9bab',
|
||||||
green: '#22b94f',
|
green: '#7bd84f',
|
||||||
greenSoft: '#b7f5c6',
|
greenSoft: '#273c24',
|
||||||
mixerBlue: '#2477ff',
|
mixerBlue: '#4aa3ff',
|
||||||
truckPink: '#ff6aa8',
|
truckPink: '#ffb33e',
|
||||||
wasteGreen: '#22b94f',
|
wasteGreen: '#88d35a',
|
||||||
danger: '#c42424',
|
danger: '#e34a36',
|
||||||
warn: '#f0a020',
|
warn: '#ffb827',
|
||||||
white: '#ffffff',
|
white: '#e6edf2',
|
||||||
poop: '#7b4a23',
|
poop: '#7b4a23',
|
||||||
male: '#62a9ff',
|
male: '#4aa3ff',
|
||||||
female: '#ff8bc6'
|
female: '#ff6fae'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EFFECT_PRIORITY = {
|
export const EFFECT_PRIORITY = {
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,26 @@ export function createFacility(game, id, p, price) {
|
||||||
return layoutFacilityOnEdge(f, entry, side);
|
return layoutFacilityOnEdge(f, entry, side);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function maintenanceRoomFootprint(room) {
|
||||||
|
if (!room) return [];
|
||||||
|
return [
|
||||||
|
{ col: room.col, row: room.row },
|
||||||
|
{ col: room.col + 1, row: room.row },
|
||||||
|
{ col: room.col, row: room.row + 1 },
|
||||||
|
{ col: room.col + 1, row: room.row + 1 }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMaintenanceRoom(game, col, row) {
|
||||||
|
return {
|
||||||
|
type: 'maintenanceRoom', id: game.nextId++, col, row,
|
||||||
|
price: BUILD_COSTS.maintenanceRoom,
|
||||||
|
builtSession: game.buildSession,
|
||||||
|
wCells: 2, hCells: 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function rollChickSex(game) {
|
export function rollChickSex(game) {
|
||||||
return Math.random() < currentPoopRate(game) ? 'poop' : (Math.random() < 0.5 ? 'male' : 'female');
|
return Math.random() < currentPoopRate(game) ? 'poop' : (Math.random() < 0.5 ? 'male' : 'female');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,12 @@ function mulberry32(seed) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function blockedRatioForExpansionPurchase(purchaseCount, { base = 0.03, increment = 0.005, max = 0.10 } = {}) {
|
||||||
|
const count = Math.max(0, Math.floor(Number(purchaseCount) || 0));
|
||||||
|
return Number(Math.min(max, base + count * increment).toFixed(6));
|
||||||
|
}
|
||||||
|
|
||||||
export function createRunSeed() {
|
export function createRunSeed() {
|
||||||
return `RUN-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e6).toString(36)}`.toUpperCase();
|
return `RUN-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e6).toString(36)}`.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +44,7 @@ export function generateBlockedCells({ seed, targetRatio = 0.20, reserved = [] }
|
||||||
const j = Math.floor(rng() * (i + 1));
|
const j = Math.floor(rng() * (i + 1));
|
||||||
[candidates[i], candidates[j]] = [candidates[j], candidates[i]];
|
[candidates[i], candidates[j]] = [candidates[j], candidates[i]];
|
||||||
}
|
}
|
||||||
const target = Math.round(GRID.cols * GRID.rows * targetRatio);
|
const target = Math.round(GRID.cols * GRID.rows * targetRatio + 1e-9);
|
||||||
return new Set(candidates.slice(0, Math.min(target, candidates.length)).map(c => c.k));
|
return new Set(candidates.slice(0, Math.min(target, candidates.length)).map(c => c.k));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,6 +66,6 @@ export function generateBlockedCellsInRect({ seed, colStart = 0, colEnd = GRID.c
|
||||||
const j = Math.floor(rng() * (i + 1));
|
const j = Math.floor(rng() * (i + 1));
|
||||||
[candidates[i], candidates[j]] = [candidates[j], candidates[i]];
|
[candidates[i], candidates[j]] = [candidates[j], candidates[i]];
|
||||||
}
|
}
|
||||||
const target = Math.round((c1 - c0) * (r1 - r0) * targetRatio);
|
const target = Math.round((c1 - c0) * (r1 - r0) * targetRatio + 1e-9);
|
||||||
return new Set(candidates.slice(0, Math.min(target, candidates.length)).map(c => c.k));
|
return new Set(candidates.slice(0, Math.min(target, candidates.length)).map(c => c.k));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ export function newTurnStats() {
|
||||||
manualComboBonus: 0,
|
manualComboBonus: 0,
|
||||||
manualComboSuccess: 0,
|
manualComboSuccess: 0,
|
||||||
manualComboFailure: 0,
|
manualComboFailure: 0,
|
||||||
repairWorkerWages: 0,
|
|
||||||
fairiesTribute: 0,
|
fairiesTribute: 0,
|
||||||
chemicalWeaponSubsidy: 0,
|
chemicalWeaponSubsidy: 0,
|
||||||
rescueLoan: 0,
|
rescueLoan: 0,
|
||||||
|
|
@ -73,7 +72,6 @@ export function newTotalStats() {
|
||||||
manualComboBonus: 0,
|
manualComboBonus: 0,
|
||||||
manualComboSuccess: 0,
|
manualComboSuccess: 0,
|
||||||
manualComboFailure: 0,
|
manualComboFailure: 0,
|
||||||
repairWorkerWages: 0,
|
|
||||||
cardRerollCost: 0,
|
cardRerollCost: 0,
|
||||||
dudRefundIncome: 0,
|
dudRefundIncome: 0,
|
||||||
composterIncome: 0,
|
composterIncome: 0,
|
||||||
|
|
@ -129,6 +127,8 @@ export function createGame() {
|
||||||
eggFarms: [],
|
eggFarms: [],
|
||||||
scanners: [],
|
scanners: [],
|
||||||
facilities: {},
|
facilities: {},
|
||||||
|
maintenanceRooms: [],
|
||||||
|
repairmen: [],
|
||||||
branchCounters: new Map(),
|
branchCounters: new Map(),
|
||||||
routingVersion: 0,
|
routingVersion: 0,
|
||||||
factoryGraph: null,
|
factoryGraph: null,
|
||||||
|
|
@ -144,8 +144,7 @@ export function createGame() {
|
||||||
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
|
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
|
||||||
cardTargetPick: null,
|
cardTargetPick: null,
|
||||||
manualCombo: { count: 0, lastBonus: 0 },
|
manualCombo: { count: 0, lastBonus: 0 },
|
||||||
pendingRepairWorkerWages: 0,
|
repairman: { active: false, x: GRID.x + GRID.cell * 0.5, y: GRID.y + BALANCE.grid.rows * GRID.cell + 84, target: null, repairedToday: 0 },
|
||||||
repairman: { hiredForNextDay: false, active: false, x: GRID.x + GRID.cell * 0.5, y: GRID.y + BALANCE.grid.rows * GRID.cell + 84, target: null, repairedToday: 0 },
|
|
||||||
stats: newTurnStats(),
|
stats: newTurnStats(),
|
||||||
lastResult: null,
|
lastResult: null,
|
||||||
totals: newTotalStats()
|
totals: newTotalStats()
|
||||||
|
|
@ -319,10 +318,11 @@ export function resetLayout(game) {
|
||||||
game.repairman.y = GRID.y + GRID.rows * GRID.cell + 84;
|
game.repairman.y = GRID.y + GRID.rows * GRID.cell + 84;
|
||||||
game.repairman.target = null;
|
game.repairman.target = null;
|
||||||
game.repairman.active = false;
|
game.repairman.active = false;
|
||||||
game.repairman.hiredForNextDay = false;
|
|
||||||
}
|
}
|
||||||
game.conveyorTiles.clear();
|
game.conveyorTiles.clear();
|
||||||
game.conveyorMeta.clear();
|
game.conveyorMeta.clear();
|
||||||
|
game.maintenanceRooms = [];
|
||||||
|
game.repairmen = [];
|
||||||
game.blockedCells = new Set();
|
game.blockedCells = new Set();
|
||||||
installInitialConveyorBackbone(game);
|
installInitialConveyorBackbone(game);
|
||||||
game.facilities = defaultFacilities();
|
game.facilities = defaultFacilities();
|
||||||
|
|
@ -336,7 +336,7 @@ export function resetLayout(game) {
|
||||||
// installed. The protected footprint includes every default conveyor segment,
|
// installed. The protected footprint includes every default conveyor segment,
|
||||||
// scanner body/ports, farm output, and machine receiver. This guarantees that
|
// scanner body/ports, farm output, and machine receiver. This guarantees that
|
||||||
// the starter line is always continuous from Egg Farm -> S1 -> S2 -> exits even
|
// the starter line is always continuous from Egg Farm -> S1 -> S2 -> exits even
|
||||||
// when roughly 20% of the map is blocked.
|
// when roughly 3% of the initial map is blocked.
|
||||||
const reserved = protectedInitialCells(game);
|
const reserved = protectedInitialCells(game);
|
||||||
game.blockedCells = generateBlockedCells({ seed: game.runSeed, targetRatio: BALANCE.map.blockedRatio, reserved: [...reserved] });
|
game.blockedCells = generateBlockedCells({ seed: game.runSeed, targetRatio: BALANCE.map.blockedRatio, reserved: [...reserved] });
|
||||||
scrubBlockedCellsFromInitialBackbone(game);
|
scrubBlockedCellsFromInitialBackbone(game);
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ export const TEXT = {
|
||||||
sorting: 'Sorting: use scanner keys. Build after clearing the day.',
|
sorting: 'Sorting: use scanner keys. Build after clearing the day.',
|
||||||
allPortsConnected: 'Start condition met: one EGG route reaches an exit.',
|
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.'
|
|
||||||
},
|
},
|
||||||
routeLabels: {
|
routeLabels: {
|
||||||
mixer: 'MIXER',
|
mixer: 'MIXER',
|
||||||
|
|
@ -71,7 +70,8 @@ export function buildToolFlavorText(id) {
|
||||||
manualScanner: 'A manual checkpoint. The operator is the algorithm.',
|
manualScanner: 'A manual checkpoint. The operator is the algorithm.',
|
||||||
mixer: 'Male chicks become revenue here. Do not feed it poop.',
|
mixer: 'Male chicks become revenue here. Do not feed it poop.',
|
||||||
trash: 'A polite shredder for poop and other regrets.',
|
trash: 'A polite shredder for poop and other regrets.',
|
||||||
truck: 'Ships the target cargo. Wrong cargo still leaves a paper trail.'
|
truck: 'Ships the target cargo. Wrong cargo still leaves a paper trail.',
|
||||||
|
maintenanceRoom: '2×2 repair bay. Each room dispatches one repairman during the day.'
|
||||||
};
|
};
|
||||||
return flavors[id] || '';
|
return flavors[id] || '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
src/game.js
|
|
@ -13,7 +13,7 @@ import { createChickSystem } from './systems/chickSystem.js';
|
||||||
import { CARD_DEFS, cardById, conveyorSpeedForGame, createCardSystem, debugCardTargets, debugGrantCard } from './systems/cards.js';
|
import { CARD_DEFS, cardById, conveyorSpeedForGame, createCardSystem, debugCardTargets, debugGrantCard } from './systems/cards.js';
|
||||||
import { floating, shockwave, sparkBurst, updateEffects } from './systems/effects.js';
|
import { floating, shockwave, sparkBurst, updateEffects } from './systems/effects.js';
|
||||||
import { rollEventOffer, activateEventOffer, clearActiveEvent, resolveEvent, eventDaySeconds, fairiesTributeMultiplierFromEvent } from './systems/events.js';
|
import { rollEventOffer, activateEventOffer, clearActiveEvent, resolveEvent, eventDaySeconds, fairiesTributeMultiplierFromEvent } from './systems/events.js';
|
||||||
import { ensureMaintenanceState, hireRepairmanForNextDay, activateRepairmanForDay, deactivateRepairman, conveyorSpeedFactorForKey } from './systems/maintenance.js';
|
import { ensureMaintenanceState, activateRepairmanForDay, deactivateRepairman, conveyorSpeedFactorForKey } from './systems/maintenance.js';
|
||||||
|
|
||||||
const canvas = document.getElementById('gameCanvas');
|
const canvas = document.getElementById('gameCanvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
@ -23,11 +23,10 @@ const ui = {
|
||||||
money: document.getElementById('money'), turn: document.getElementById('turn'), timeLeft: document.getElementById('timeLeft'), phase: document.getElementById('phaseLabel'), comboCount: document.getElementById('comboCount'),
|
money: document.getElementById('money'), turn: document.getElementById('turn'), timeLeft: document.getElementById('timeLeft'), phase: document.getElementById('phaseLabel'), comboCount: document.getElementById('comboCount'),
|
||||||
eventBrief: document.getElementById('eventBrief'), targetBrief: document.getElementById('targetBrief'), speedBrief: document.getElementById('speedBrief'),
|
eventBrief: document.getElementById('eventBrief'), targetBrief: document.getElementById('targetBrief'), speedBrief: document.getElementById('speedBrief'),
|
||||||
turnProfit: document.getElementById('turnProfit'), mixerCount: document.getElementById('mixerCount'), truckFemaleCount: document.getElementById('truckFemaleCount'), truckMaleCount: document.getElementById('truckMaleCount'), poopCount: document.getElementById('poopCount'),
|
turnProfit: document.getElementById('turnProfit'), mixerCount: document.getElementById('mixerCount'), truckFemaleCount: document.getElementById('truckFemaleCount'), truckMaleCount: document.getElementById('truckMaleCount'), poopCount: document.getElementById('poopCount'),
|
||||||
buildStatus: document.getElementById('buildStatus'), turnSummary: document.getElementById('turnSummary'), eventPanel: document.getElementById('eventPanel'),
|
buildStatus: document.getElementById('buildStatus'), turnSummary: document.getElementById('turnSummary'),
|
||||||
modal: document.getElementById('modal'), modalTitle: document.getElementById('modalTitle'), modalBody: document.getElementById('modalBody'), modalActions: document.getElementById('modalActions'),
|
modal: document.getElementById('modal'), modalTitle: document.getElementById('modalTitle'), modalBody: document.getElementById('modalBody'), modalActions: document.getElementById('modalActions'),
|
||||||
hoverTooltip: document.getElementById('hoverTooltip'),
|
hoverTooltip: document.getElementById('hoverTooltip'),
|
||||||
manualScannerMonitor: document.getElementById('manualScannerMonitor'),
|
manualScannerMonitor: document.getElementById('manualScannerMonitor'),
|
||||||
hireRepairmanButton: document.getElementById('hireRepairmanButton'),
|
|
||||||
debug: {
|
debug: {
|
||||||
panel: document.getElementById('debugPanel'),
|
panel: document.getElementById('debugPanel'),
|
||||||
infiniteCash: document.getElementById('debugInfiniteCash'),
|
infiniteCash: document.getElementById('debugInfiniteCash'),
|
||||||
|
|
@ -42,7 +41,7 @@ const ui = {
|
||||||
},
|
},
|
||||||
buttons: {
|
buttons: {
|
||||||
s1Left: document.getElementById('scanner1MixerButton'), s1Right: document.getElementById('scanner1TruckButton'), s2Left: document.getElementById('scanner2MixerButton'), s2Right: document.getElementById('scanner2TruckButton'),
|
s1Left: document.getElementById('scanner1MixerButton'), s1Right: document.getElementById('scanner1TruckButton'), s2Left: document.getElementById('scanner2MixerButton'), s2Right: document.getElementById('scanner2TruckButton'),
|
||||||
nextTurn: document.getElementById('nextTurnButton'), conveyor: document.getElementById('buildConveyorButton'), boostConveyor: document.getElementById('buildBoostConveyorButton'), eggFarm: document.getElementById('buildEggFarmButton'), autoScanner: document.getElementById('buildAutoScannerButton'), manualScanner: document.getElementById('buildManualScannerButton'), mixer: document.getElementById('buildMixerButton'), trash: document.getElementById('buildTrashButton'), truck: document.getElementById('buildTruckButton'), erase: document.getElementById('eraseButton'), undo: document.getElementById('undoButton'), redo: document.getElementById('redoButton')
|
nextTurn: document.getElementById('nextTurnButton'), conveyor: document.getElementById('buildConveyorButton'), boostConveyor: document.getElementById('buildBoostConveyorButton'), eggFarm: document.getElementById('buildEggFarmButton'), autoScanner: document.getElementById('buildAutoScannerButton'), manualScanner: document.getElementById('buildManualScannerButton'), mixer: document.getElementById('buildMixerButton'), trash: document.getElementById('buildTrashButton'), truck: document.getElementById('buildTruckButton'), maintenanceRoom: document.getElementById('buildMaintenanceRoomButton'), erase: document.getElementById('eraseButton'), undo: document.getElementById('undoButton'), redo: document.getElementById('redoButton')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -56,7 +55,8 @@ function initializeStaticText() {
|
||||||
manualScanner: 'manualScanner',
|
manualScanner: 'manualScanner',
|
||||||
mixer: 'mixer',
|
mixer: 'mixer',
|
||||||
trash: 'trash',
|
trash: 'trash',
|
||||||
truck: 'truck'
|
truck: 'truck',
|
||||||
|
maintenanceRoom: 'maintenanceRoom'
|
||||||
};
|
};
|
||||||
for (const [buttonKey, toolId] of Object.entries(buttonToolMap)) {
|
for (const [buttonKey, toolId] of Object.entries(buttonToolMap)) {
|
||||||
if (ui.buttons[buttonKey]) ui.buttons[buttonKey].innerHTML = buildToolButtonHtml(toolId);
|
if (ui.buttons[buttonKey]) ui.buttons[buttonKey].innerHTML = buildToolButtonHtml(toolId);
|
||||||
|
|
@ -259,13 +259,10 @@ function startNextTurn() {
|
||||||
resetCameraToFactoryStart();
|
resetCameraToFactoryStart();
|
||||||
game.stats = newTurnStats();
|
game.stats = newTurnStats();
|
||||||
game.stats.fairiesTribute = game.lastStartFees?.fairiesTribute || 0;
|
game.stats.fairiesTribute = game.lastStartFees?.fairiesTribute || 0;
|
||||||
game.stats.repairWorkerWages = game.pendingRepairWorkerWages || 0;
|
if (game.stats.fairiesTribute > 0) {
|
||||||
const carriedStartCosts = game.stats.fairiesTribute + game.stats.repairWorkerWages;
|
game.stats.penalty += game.stats.fairiesTribute;
|
||||||
if (carriedStartCosts > 0) {
|
game.stats.profit -= game.stats.fairiesTribute;
|
||||||
game.stats.penalty += carriedStartCosts;
|
|
||||||
game.stats.profit -= carriedStartCosts;
|
|
||||||
}
|
}
|
||||||
game.pendingRepairWorkerWages = 0;
|
|
||||||
for (const scanner of game.scanners) { scanner.queue = []; scanner.cooldown = 0; }
|
for (const scanner of game.scanners) { scanner.queue = []; scanner.cooldown = 0; }
|
||||||
for (const farm of game.eggFarms) { farm.nextSpawn = chicks.currentSpawnDelay(farm); farm.lastInterval = farm.nextSpawn; farm.shutterProgress = 0; farm.shutterSparked = false; }
|
for (const farm of game.eggFarms) { farm.nextSpawn = chicks.currentSpawnDelay(farm); farm.lastInterval = farm.nextSpawn; farm.shutterProgress = 0; farm.shutterSparked = false; }
|
||||||
game.buildTool = null;
|
game.buildTool = null;
|
||||||
|
|
@ -279,19 +276,6 @@ function startNextTurn() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function hireRepairman() {
|
|
||||||
ensureMaintenanceState(game);
|
|
||||||
const result = hireRepairmanForNextDay(game);
|
|
||||||
if (!result.ok) {
|
|
||||||
floating(game, canvas.width / 2 - game.view.x, 96 - game.view.y, result.reason || 'Cannot hire repairman.', THEME.danger);
|
|
||||||
build?.fail?.(result.reason || 'Cannot hire repairman.');
|
|
||||||
} else {
|
|
||||||
floating(game, canvas.width / 2 - game.view.x, 96 - game.view.y, `Repairman hired -${yen(result.cost)}`, THEME.green);
|
|
||||||
}
|
|
||||||
uiSystem.updatePanels();
|
|
||||||
uiSystem.updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
function completeTurn() {
|
function completeTurn() {
|
||||||
if (game.phase !== 'running') return;
|
if (game.phase !== 'running') return;
|
||||||
const ship = settleTruckRevenue(game);
|
const ship = settleTruckRevenue(game);
|
||||||
|
|
@ -713,8 +697,7 @@ document.addEventListener('pointerdown', event => {
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
ui.buttons.s1Left.addEventListener('click', () => chicks.sortSlot(0, 'left')); ui.buttons.s1Right.addEventListener('click', () => chicks.sortSlot(0, 'right')); ui.buttons.s2Left.addEventListener('click', () => chicks.sortSlot(1, 'left')); ui.buttons.s2Right.addEventListener('click', () => chicks.sortSlot(1, 'right'));
|
ui.buttons.s1Left.addEventListener('click', () => chicks.sortSlot(0, 'left')); ui.buttons.s1Right.addEventListener('click', () => chicks.sortSlot(0, 'right')); ui.buttons.s2Left.addEventListener('click', () => chicks.sortSlot(1, 'left')); ui.buttons.s2Right.addEventListener('click', () => chicks.sortSlot(1, 'right'));
|
||||||
ui.buttons.nextTurn.addEventListener('click', startNextTurn); ui.buttons.conveyor.addEventListener('click', () => build.setBuildTool('conveyor')); ui.buttons.boostConveyor.addEventListener('click', () => build.setBuildTool('boostConveyor')); 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.boostConveyor.addEventListener('click', () => build.setBuildTool('boostConveyor')); 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.maintenanceRoom.addEventListener('click', () => build.setBuildTool('maintenanceRoom')); ui.buttons.erase.addEventListener('click', () => build.setBuildTool('erase'));
|
||||||
ui.hireRepairmanButton?.addEventListener('click', hireRepairman);
|
|
||||||
ui.debug.setDay?.addEventListener('click', debugSetDay);
|
ui.debug.setDay?.addEventListener('click', debugSetDay);
|
||||||
ui.debug.zeroTimer?.addEventListener('click', debugZeroTimer);
|
ui.debug.zeroTimer?.addEventListener('click', debugZeroTimer);
|
||||||
ui.debug.grantCard?.addEventListener('click', debugGrantSelectedCard);
|
ui.debug.grantCard?.addEventListener('click', debugGrantSelectedCard);
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,36 @@ 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.
|
||||||
// To use replacement art, set ENABLE_EXTERNAL_ART_ASSETS to true and place files under assets/images/.
|
// To use replacement art, set ENABLE_EXTERNAL_ART_ASSETS to true and place files under assets/images/.
|
||||||
const ENABLE_EXTERNAL_ART_ASSETS = false;
|
const ENABLE_EXTERNAL_ART_ASSETS = true;
|
||||||
const ASSET_PATHS = {
|
const ASSET_PATHS = {
|
||||||
|
// User-supplied chick/poop assets remain optional. Facility/UI art is wired below.
|
||||||
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: new URL('../../assets/images/poop.png', import.meta.url).href,
|
poop: './assets/images/poop.png',
|
||||||
conveyor: './assets/images/conveyor.png',
|
conveyorStraightH: './assets/dark_factory/facilities/conveyor/conveyor_straight_h_16.png',
|
||||||
scannerManual: './assets/images/scanner_manual.png',
|
conveyorStraightV: './assets/dark_factory/facilities/conveyor/conveyor_straight_v_16.png',
|
||||||
scannerAuto: './assets/images/scanner_auto.png',
|
conveyorTurnNe: './assets/dark_factory/facilities/conveyor/conveyor_turn_ne_16.png',
|
||||||
eggFarm: './assets/images/egg_farm.png',
|
conveyorTurnNw: './assets/dark_factory/facilities/conveyor/conveyor_turn_nw_16.png',
|
||||||
mixer: './assets/images/mixer.png',
|
conveyorTurnSe: './assets/dark_factory/facilities/conveyor/conveyor_turn_se_16.png',
|
||||||
shredder: './assets/images/shredder.png',
|
conveyorTurnSw: './assets/dark_factory/facilities/conveyor/conveyor_turn_sw_16.png',
|
||||||
truck: './assets/images/truck.png'
|
boostConveyorStraightH: './assets/dark_factory/facilities/conveyor/boost_conveyor_straight_h_16.png',
|
||||||
|
boostConveyorStraightV: './assets/dark_factory/facilities/conveyor/boost_conveyor_straight_v_16.png',
|
||||||
|
boostConveyorTurnNe: './assets/dark_factory/facilities/conveyor/boost_conveyor_turn_ne_16.png',
|
||||||
|
boostConveyorTurnNw: './assets/dark_factory/facilities/conveyor/boost_conveyor_turn_nw_16.png',
|
||||||
|
boostConveyorTurnSe: './assets/dark_factory/facilities/conveyor/boost_conveyor_turn_se_16.png',
|
||||||
|
boostConveyorTurnSw: './assets/dark_factory/facilities/conveyor/boost_conveyor_turn_sw_16.png',
|
||||||
|
scannerManual: './assets/dark_factory/facilities/machines/manual_scanner_32x48.png',
|
||||||
|
scannerAuto: './assets/dark_factory/facilities/machines/auto_scanner_32x48.png',
|
||||||
|
eggFarm: './assets/dark_factory/facilities/machines/egg_farm_16.png',
|
||||||
|
mixer: './assets/dark_factory/facilities/machines/mixer_32.png',
|
||||||
|
shredder: './assets/dark_factory/facilities/machines/shredder_32.png',
|
||||||
|
truck: './assets/dark_factory/facilities/vehicles/truck_32.png',
|
||||||
|
mixerCompact: './assets/dark_factory/facilities/machines/mixer_compact_16.png',
|
||||||
|
shredderCompact: './assets/dark_factory/facilities/machines/shredder_compact_16.png',
|
||||||
|
truckCompact: './assets/dark_factory/facilities/vehicles/truck_compact_16.png',
|
||||||
|
maintenanceRoom: './assets/dark_factory/facilities/utility/maintenance_room_32.png',
|
||||||
|
repairman: './assets/dark_factory/facilities/utility/repairman_16.png',
|
||||||
|
obstacleCrate: './assets/dark_factory/facilities/utility/obstacle_crate_16.png'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const assets = {};
|
export const assets = {};
|
||||||
|
|
@ -28,7 +46,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 || name === 'poop') && src) {
|
if (ENABLE_EXTERNAL_ART_ASSETS && src) {
|
||||||
img.onload = () => { img.loaded = true; };
|
img.onload = () => { img.loaded = true; };
|
||||||
img.src = src;
|
img.src = src;
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +56,7 @@ for (const [name, src] of Object.entries(ASSET_PATHS)) {
|
||||||
// Each draw* function first checks whether an image loaded; if not, it falls back to simple canvas shapes.
|
// Each draw* function first checks whether an image loaded; if not, it falls back to simple canvas shapes.
|
||||||
|
|
||||||
export function drawAll(ctx, canvas, game, helpers) {
|
export function drawAll(ctx, canvas, game, helpers) {
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
drawBackground(ctx, canvas);
|
drawBackground(ctx, canvas);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
@ -50,6 +69,7 @@ export function drawAll(ctx, canvas, game, helpers) {
|
||||||
drawExpansionOffers(ctx, game);
|
drawExpansionOffers(ctx, game);
|
||||||
drawConveyors(ctx, game);
|
drawConveyors(ctx, game);
|
||||||
drawScannerConnectors(ctx, game);
|
drawScannerConnectors(ctx, game);
|
||||||
|
drawMaintenanceRooms(ctx, game);
|
||||||
drawFacilities(ctx, game);
|
drawFacilities(ctx, game);
|
||||||
drawEggFarms(ctx, game);
|
drawEggFarms(ctx, game);
|
||||||
drawScanners(ctx, game);
|
drawScanners(ctx, game);
|
||||||
|
|
@ -108,17 +128,84 @@ function label(ctx, x, y, text, color = THEME.ink) {
|
||||||
}
|
}
|
||||||
function drawImageIfLoaded(ctx, image, x, y, w, h) {
|
function drawImageIfLoaded(ctx, image, x, y, w, h) {
|
||||||
if (!image?.loaded) return false;
|
if (!image?.loaded) return false;
|
||||||
ctx.drawImage(image, x, y, w, h);
|
const previous = ctx.imageSmoothingEnabled;
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
ctx.drawImage(image, Math.round(x), Math.round(y), Math.round(w), Math.round(h));
|
||||||
|
ctx.imageSmoothingEnabled = previous;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
function pixelRect(ctx, x, y, w, h, fill, stroke = null, lineWidth = 1) {
|
||||||
|
ctx.fillStyle = fill;
|
||||||
|
ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
|
||||||
|
if (stroke) {
|
||||||
|
ctx.strokeStyle = stroke;
|
||||||
|
ctx.lineWidth = lineWidth;
|
||||||
|
ctx.strokeRect(Math.round(x) + 0.5, Math.round(y) + 0.5, Math.round(w) - 1, Math.round(h) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function floorCellHash(col, row) {
|
||||||
|
let n = (Math.imul(col + 37, 73856093) ^ Math.imul(row + 71, 19349663)) >>> 0;
|
||||||
|
n ^= n >>> 13;
|
||||||
|
return n >>> 0;
|
||||||
|
}
|
||||||
|
function drawFloorSlime(ctx, x, y, col, row) {
|
||||||
|
const h = floorCellHash(col, row);
|
||||||
|
// Sparse, deterministic splashes: about 3% of owned floor cells.
|
||||||
|
if (h % 31 !== 0) return;
|
||||||
|
const ox = 10 + ((h >>> 4) % Math.max(1, GRID.cell - 22));
|
||||||
|
const oy = 10 + ((h >>> 9) % Math.max(1, GRID.cell - 22));
|
||||||
|
const angle = ((h >>> 14) % 16) * Math.PI / 16;
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = 0.46;
|
||||||
|
ctx.fillStyle = '#4d9638';
|
||||||
|
ctx.strokeStyle = 'rgba(30,66,27,.48)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
|
||||||
|
// Small irregular core rather than a large puddle.
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let i = 0; i < 8; i += 1) {
|
||||||
|
const a = angle + i * Math.PI / 4;
|
||||||
|
const r = 3.5 + ((h >>> (i + 2)) & 3);
|
||||||
|
const px = x + ox + Math.cos(a) * r * 1.35;
|
||||||
|
const py = y + oy + Math.sin(a) * r * .75;
|
||||||
|
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
|
||||||
|
}
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Several separated droplets produce a splattered silhouette.
|
||||||
|
const droplets = 3 + ((h >>> 24) % 4);
|
||||||
|
for (let i = 0; i < droplets; i += 1) {
|
||||||
|
const a = angle + i * 2.17 + ((h >>> (i + 8)) & 3) * .18;
|
||||||
|
const dist = 8 + ((h >>> (i + 11)) & 7);
|
||||||
|
const radius = 1.1 + ((h >>> (i + 17)) & 3) * .45;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x + ox + Math.cos(a) * dist, y + oy + Math.sin(a) * dist * .72, radius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.fillStyle = 'rgba(151,218,101,.40)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x + ox - 2, y + oy - 2, 1.7, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
function drawBackground(ctx, canvas) {
|
function drawBackground(ctx, canvas) {
|
||||||
ctx.fillStyle = THEME.bg;
|
ctx.fillStyle = THEME.bg;
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.strokeStyle = 'rgba(16, 32, 21, 0.035)';
|
ctx.globalAlpha = 0.85;
|
||||||
|
for (let y = 0; y < canvas.height; y += 16) {
|
||||||
|
for (let x = 0; x < canvas.width; x += 16) {
|
||||||
|
const even = ((x / 16 + y / 16) | 0) % 2 === 0;
|
||||||
|
ctx.fillStyle = even ? '#181b20' : '#20242b';
|
||||||
|
ctx.fillRect(x, y, 16, 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = 'rgba(255,184,39,.08)';
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
for (let x = 0; x < canvas.width; x += 32) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); }
|
for (let x = 0; x < canvas.width; x += 64) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); }
|
||||||
for (let y = 0; y < canvas.height; y += 32) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); }
|
for (let y = 0; y < canvas.height; y += 64) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); }
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
function ownedCellKeys(game) {
|
function ownedCellKeys(game) {
|
||||||
|
|
@ -131,15 +218,20 @@ function ownedCellKeys(game) {
|
||||||
function drawGrid(ctx, game) {
|
function drawGrid(ctx, game) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
const cells = ownedCellKeys(game);
|
const cells = ownedCellKeys(game);
|
||||||
ctx.fillStyle = 'rgba(255,255,255,.46)';
|
|
||||||
ctx.strokeStyle = 'rgba(16,32,21,.055)';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
for (const k of cells) {
|
for (const k of cells) {
|
||||||
const p = parseKey(k);
|
const p = parseKey(k);
|
||||||
const c = cellCenter(p.col, p.row);
|
const c = cellCenter(p.col, p.row);
|
||||||
rect(ctx, c.x - GRID.cell / 2, c.y - GRID.cell / 2, GRID.cell, GRID.cell, true, true);
|
const x = c.x - GRID.cell / 2;
|
||||||
|
const y = c.y - GRID.cell / 2;
|
||||||
|
const tone = ((p.col + p.row) % 2 === 0) ? '#59616d' : '#505965';
|
||||||
|
pixelRect(ctx, x, y, GRID.cell, GRID.cell, tone, 'rgba(190,199,210,.28)', 1);
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,.055)';
|
||||||
|
ctx.fillRect(Math.round(x + 2), Math.round(y + 2), Math.round(GRID.cell - 4), 2);
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,.18)';
|
||||||
|
ctx.fillRect(Math.round(x + 1), Math.round(y + GRID.cell - 3), Math.round(GRID.cell - 2), 2);
|
||||||
|
drawFloorSlime(ctx, x, y, p.col, p.row);
|
||||||
}
|
}
|
||||||
ctx.strokeStyle = 'rgba(16,32,21,.16)';
|
ctx.strokeStyle = 'rgba(255,184,39,.32)';
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
for (const k of cells) {
|
for (const k of cells) {
|
||||||
const p = parseKey(k);
|
const p = parseKey(k);
|
||||||
|
|
@ -154,14 +246,41 @@ function drawGrid(ctx, game) {
|
||||||
const p = parseKey(k);
|
const p = parseKey(k);
|
||||||
if (!isOwnedCell(game, p.col, p.row)) continue;
|
if (!isOwnedCell(game, p.col, p.row)) continue;
|
||||||
const c = cellCenter(p.col, p.row);
|
const c = cellCenter(p.col, p.row);
|
||||||
ctx.fillStyle = 'rgba(16,32,21,.30)';
|
const x = c.x - GRID.cell / 2;
|
||||||
ctx.strokeStyle = 'rgba(16,32,21,.60)';
|
const y = c.y - GRID.cell / 2;
|
||||||
|
// Muted floor-level hazard striping. It remains readable without dominating machines.
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.rect(Math.round(x + 1), Math.round(y + 1), GRID.cell - 2, GRID.cell - 2);
|
||||||
|
ctx.clip();
|
||||||
|
ctx.fillStyle = 'rgba(42,45,48,.42)';
|
||||||
|
ctx.fillRect(Math.round(x), Math.round(y), GRID.cell, GRID.cell);
|
||||||
|
ctx.lineWidth = 8;
|
||||||
|
ctx.strokeStyle = 'rgba(196,163,79,.38)';
|
||||||
|
for (let stripe = -GRID.cell; stripe < GRID.cell * 2; stripe += 16) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + stripe, y + GRID.cell);
|
||||||
|
ctx.lineTo(x + stripe + GRID.cell, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.lineWidth = 8;
|
||||||
|
ctx.strokeStyle = 'rgba(20,23,26,.32)';
|
||||||
|
for (let stripe = -GRID.cell + 8; stripe < GRID.cell * 2; stripe += 16) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x + stripe, y + GRID.cell);
|
||||||
|
ctx.lineTo(x + stripe + GRID.cell, y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
ctx.strokeStyle = 'rgba(17,19,22,.72)';
|
||||||
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);
|
ctx.strokeRect(Math.round(x) + 1, Math.round(y) + 1, GRID.cell - 2, GRID.cell - 2);
|
||||||
|
ctx.strokeStyle = 'rgba(224,194,112,.24)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.strokeRect(Math.round(x) + 4, Math.round(y) + 4, GRID.cell - 8, GRID.cell - 8);
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawExpansionOffers(ctx, game) {
|
function drawExpansionOffers(ctx, game) {
|
||||||
if (game.phase !== 'build' || game.cardDraft?.pending || game.cardTargetPick?.pending) return;
|
if (game.phase !== 'build' || game.cardDraft?.pending || game.cardTargetPick?.pending) return;
|
||||||
const cost = expansionCost(game);
|
const cost = expansionCost(game);
|
||||||
|
|
@ -220,6 +339,22 @@ function componentRatio(game, k) {
|
||||||
const data = id ? game.congestion.get(id) : null;
|
const data = id ? game.congestion.get(id) : null;
|
||||||
return data?.ratio || 0;
|
return data?.ratio || 0;
|
||||||
}
|
}
|
||||||
|
function conveyorAssetForTile(game, k, meta = {}) {
|
||||||
|
const dirs = connectedConveyorDirs(game, k).sort();
|
||||||
|
const boost = meta.kind === 'boostConveyor';
|
||||||
|
const prefix = boost ? 'boostConveyor' : 'conveyor';
|
||||||
|
const has = name => dirs.includes(name);
|
||||||
|
if (dirs.length === 2) {
|
||||||
|
if (has('left') && has('right')) return assets[`${prefix}StraightH`];
|
||||||
|
if (has('up') && has('down')) return assets[`${prefix}StraightV`];
|
||||||
|
if (has('up') && has('right')) return assets[`${prefix}TurnNe`];
|
||||||
|
if (has('up') && has('left')) return assets[`${prefix}TurnNw`];
|
||||||
|
if (has('down') && has('right')) return assets[`${prefix}TurnSe`];
|
||||||
|
if (has('down') && has('left')) return assets[`${prefix}TurnSw`];
|
||||||
|
}
|
||||||
|
if (dirs.length === 1) return has('up') || has('down') ? assets[`${prefix}StraightV`] : assets[`${prefix}StraightH`];
|
||||||
|
return has('up') || has('down') ? assets[`${prefix}StraightV`] : assets[`${prefix}StraightH`];
|
||||||
|
}
|
||||||
function drawConveyors(ctx, game) {
|
function drawConveyors(ctx, game) {
|
||||||
const edges = [];
|
const edges = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
@ -239,9 +374,9 @@ function drawConveyors(ctx, game) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||||
for (const layer of [
|
for (const layer of [
|
||||||
{ width: 33, color: THEME.ink, alpha: 1 },
|
{ width: 31, color: '#0b0c0f', alpha: 1 },
|
||||||
{ width: 25, color: THEME.white, alpha: 1 },
|
{ width: 23, color: '#444b56', alpha: 1 },
|
||||||
{ width: 17, color: THEME.greenSoft, alpha: 1 }
|
{ width: 15, color: '#171a1f', alpha: 1 }
|
||||||
]) {
|
]) {
|
||||||
ctx.globalAlpha = layer.alpha; ctx.strokeStyle = layer.color; ctx.lineWidth = layer.width;
|
ctx.globalAlpha = layer.alpha; ctx.strokeStyle = layer.color; ctx.lineWidth = layer.width;
|
||||||
for (const e of edges) { ctx.beginPath(); ctx.moveTo(e.a.x, e.a.y); ctx.lineTo(e.b.x, e.b.y); ctx.stroke(); }
|
for (const e of edges) { ctx.beginPath(); ctx.moveTo(e.a.x, e.a.y); ctx.lineTo(e.b.x, e.b.y); ctx.stroke(); }
|
||||||
|
|
@ -258,27 +393,80 @@ function drawConveyors(ctx, game) {
|
||||||
const c = cellCenter(...Object.values(parseKey(k)));
|
const c = cellCenter(...Object.values(parseKey(k)));
|
||||||
const meta = game.conveyorMeta.get(k) || {};
|
const meta = game.conveyorMeta.get(k) || {};
|
||||||
const ratio = componentRatio(game, k);
|
const ratio = componentRatio(game, k);
|
||||||
const baseFill = meta.kind === 'boostConveyor' ? '#fff2b8' : THEME.white;
|
const sprite = conveyorAssetForTile(game, k, meta);
|
||||||
ctx.fillStyle = ratio > 0.5 ? mixHex(baseFill, '#ffd6d6', Math.max(0, (ratio - .5) / .5)) : baseFill;
|
const congested = ratio > 0.5;
|
||||||
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 2;
|
ctx.fillStyle = congested ? 'rgba(196,36,36,.28)' : 'rgba(0,0,0,.35)';
|
||||||
rect(ctx, c.x - 8, c.y - 8, 16, 16, true, true);
|
ctx.strokeStyle = congested ? THEME.danger : 'rgba(255,184,39,.34)';
|
||||||
if (meta.kind === 'boostConveyor') {
|
ctx.lineWidth = congested ? 3 : 1;
|
||||||
ctx.save();
|
rect(ctx, c.x - 20, c.y - 20, 40, 40, true, true);
|
||||||
ctx.strokeStyle = THEME.warn;
|
if (!drawImageIfLoaded(ctx, sprite, c.x - 19, c.y - 19, 38, 38)) {
|
||||||
ctx.lineWidth = 3;
|
const baseFill = meta.kind === 'boostConveyor' ? '#315e92' : '#454b55';
|
||||||
ctx.beginPath(); ctx.moveTo(c.x - 15, c.y + 13); ctx.lineTo(c.x - 3, c.y - 13); ctx.lineTo(c.x + 2, c.y - 2); ctx.lineTo(c.x + 15, c.y - 13); ctx.stroke();
|
ctx.fillStyle = ratio > 0.5 ? mixHex(baseFill, '#a12c2c', Math.max(0, (ratio - .5) / .5)) : baseFill;
|
||||||
ctx.restore();
|
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 2;
|
||||||
|
rect(ctx, c.x - 18, c.y - 18, 36, 36, true, true);
|
||||||
}
|
}
|
||||||
drawDirtOverlay(ctx, c.x - 14, c.y - 14, 28, 28, { meta });
|
drawAnimatedConveyorStripes(ctx, c, k, meta, ratio);
|
||||||
|
drawDirtOverlay(ctx, c.x - 18, c.y - 18, 36, 36, { meta });
|
||||||
drawSelection(ctx, game, 'conveyor', k, c.x, c.y, 38, 38);
|
drawSelection(ctx, game, 'conveyor', k, c.x, c.y, 38, 38);
|
||||||
drawConveyorDirectionMarkers(ctx, c, directionMarkers.get(k) || []);
|
drawConveyorDirectionMarkers(ctx, c, directionMarkers.get(k) || []);
|
||||||
const connectedDirs = connectedConveyorDirs(game, k);
|
const connectedDirs = connectedConveyorDirs(game, k);
|
||||||
drawBranchModeIcon(ctx, c, meta, branchSelectableEntranceDirs(game, k, connectedDirs), connectedDirs);
|
drawBranchModeIcon(ctx, c, meta, branchSelectableExitDirs(game, k, connectedDirs), connectedDirs);
|
||||||
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);
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function drawAnimatedConveyorStripes(ctx, center, tileKey, meta = {}, ratio = 0) {
|
||||||
|
const names = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))];
|
||||||
|
const primary = names[0] || 'right';
|
||||||
|
const verticalTravel = primary === 'up' || primary === 'down';
|
||||||
|
const reverse = primary === 'left' || primary === 'up';
|
||||||
|
const boost = meta.kind === 'boostConveyor';
|
||||||
|
const t = typeof performance !== 'undefined' ? performance.now() / (boost ? 70 : 105) : 0;
|
||||||
|
const phase = ((reverse ? -t : t) % 10 + 10) % 10;
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.rect(center.x - 18, center.y - 18, 36, 36);
|
||||||
|
ctx.clip();
|
||||||
|
|
||||||
|
// Blue-green rubber for boost belts; dark steel/rubber for standard belts.
|
||||||
|
const rubber = boost ? 'rgba(20,126,124,.62)' : 'rgba(42,47,54,.72)';
|
||||||
|
ctx.fillStyle = rubber;
|
||||||
|
ctx.fillRect(center.x - 18, center.y - 18, 36, 36);
|
||||||
|
|
||||||
|
// Slats are always perpendicular to travel and move in the travel direction.
|
||||||
|
ctx.strokeStyle = ratio > 0.5 ? 'rgba(238,83,61,.90)' : 'rgba(184,190,198,.92)';
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
if (verticalTravel) {
|
||||||
|
for (let yy = center.y - 28 + phase; yy < center.y + 28; yy += 10) {
|
||||||
|
ctx.beginPath(); ctx.moveTo(center.x - 17, yy); ctx.lineTo(center.x + 17, yy); ctx.stroke();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let xx = center.x - 28 + phase; xx < center.x + 28; xx += 10) {
|
||||||
|
ctx.beginPath(); ctx.moveTo(xx, center.y - 17); ctx.lineTo(xx, center.y + 17); ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 0.55;
|
||||||
|
ctx.strokeStyle = '#050609';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
if (verticalTravel) {
|
||||||
|
for (let yy = center.y - 23 + phase; yy < center.y + 28; yy += 10) {
|
||||||
|
ctx.beginPath(); ctx.moveTo(center.x - 17, yy); ctx.lineTo(center.x + 17, yy); ctx.stroke();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let xx = center.x - 23 + phase; xx < center.x + 28; xx += 10) {
|
||||||
|
ctx.beginPath(); ctx.moveTo(xx, center.y - 17); ctx.lineTo(xx, center.y + 17); ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.strokeStyle = 'rgba(8,9,12,.92)';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
rect(ctx, center.x - 18, center.y - 18, 36, 36, false, true);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
function reachableConveyorDirs(game, k, meta) {
|
function reachableConveyorDirs(game, k, meta) {
|
||||||
const p = parseKey(k);
|
const p = parseKey(k);
|
||||||
const neighborNames = DIRS
|
const neighborNames = DIRS
|
||||||
|
|
@ -306,30 +494,27 @@ function neighborPointsIntoCell(game, p, dirName) {
|
||||||
return names.includes(d.opposite);
|
return names.includes(d.opposite);
|
||||||
}
|
}
|
||||||
|
|
||||||
function branchExitDirs(game, k, dirs, connectedDirs = connectedConveyorDirs(game, k), branchMode = 'random') {
|
function branchExitCandidates(game, k, dirs, connectedDirs = connectedConveyorDirs(game, k)) {
|
||||||
if (connectedDirs.length < 3) return dirs;
|
if (connectedDirs.length < 3) return dirs;
|
||||||
const p = parseKey(k);
|
const p = parseKey(k);
|
||||||
const entrances = new Set(connectedDirs.filter(dir => neighborPointsIntoCell(game, p, dir)));
|
const physicalEntrances = new Set(connectedDirs.filter(dir => neighborPointsIntoCell(game, p, dir)));
|
||||||
if (branchMode && branchMode !== 'random' && connectedDirs.includes(branchMode)) {
|
const physicalExits = connectedDirs.filter(dir => !physicalEntrances.has(dir));
|
||||||
const selectedEntrances = new Set(entrances);
|
if (physicalEntrances.size && physicalExits.length >= 2) return physicalExits;
|
||||||
selectedEntrances.add(branchMode);
|
const explicit = dirs.filter(dir => connectedDirs.includes(dir));
|
||||||
if (connectedDirs.filter(dir => !selectedEntrances.has(dir)).length >= 2) entrances.add(branchMode);
|
if (explicit.length >= 2) return [...new Set(explicit)];
|
||||||
}
|
return connectedDirs;
|
||||||
const exits = connectedDirs.filter(dir => !entrances.has(dir));
|
|
||||||
if (exits.length) return exits;
|
|
||||||
const fallback = dirs.filter(dir => !entrances.has(dir));
|
|
||||||
return fallback.length ? fallback : dirs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function branchSelectableEntranceDirs(game, k, connectedDirs = connectedConveyorDirs(game, k)) {
|
function branchExitDirs(game, k, dirs, connectedDirs = connectedConveyorDirs(game, k), branchMode = 'random') {
|
||||||
if (connectedDirs.length < 3) return [];
|
const candidates = branchExitCandidates(game, k, dirs, connectedDirs);
|
||||||
const p = parseKey(k);
|
if (connectedDirs.length >= 3 && branchMode && branchMode !== 'random' && candidates.includes(branchMode)) return [branchMode];
|
||||||
const physicalEntrances = new Set(connectedDirs.filter(dir => neighborPointsIntoCell(game, p, dir)));
|
return candidates;
|
||||||
return connectedDirs.filter(dir => {
|
}
|
||||||
const entrances = new Set(physicalEntrances);
|
|
||||||
entrances.add(dir);
|
function branchSelectableExitDirs(game, k, connectedDirs = connectedConveyorDirs(game, k)) {
|
||||||
return connectedDirs.filter(item => !entrances.has(item)).length >= 2;
|
const meta = game.conveyorMeta?.get?.(k) || {};
|
||||||
});
|
const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))];
|
||||||
|
return connectedDirs.length < 3 ? [] : branchExitCandidates(game, k, explicit, connectedDirs);
|
||||||
}
|
}
|
||||||
|
|
||||||
function effectiveConveyorDirs(game, k, meta) {
|
function effectiveConveyorDirs(game, k, meta) {
|
||||||
|
|
@ -351,12 +536,13 @@ function drawBranchModeIcon(ctx, center, meta, dirs, connectedDirs = dirs) {
|
||||||
ctx.lineJoin = 'round';
|
ctx.lineJoin = 'round';
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.fillStyle = 'rgba(255,255,255,.9)';
|
ctx.fillStyle = 'rgba(18,21,26,.94)';
|
||||||
ctx.strokeStyle = isFixed ? THEME.warn : THEME.green;
|
ctx.strokeStyle = isFixed ? THEME.warn : THEME.green;
|
||||||
rect(ctx, center.x - 17, center.y - 17, 34, 34, true, true);
|
ctx.lineWidth = 4;
|
||||||
ctx.strokeStyle = THEME.ink;
|
rect(ctx, center.x - 18, center.y - 18, 36, 36, true, true);
|
||||||
ctx.lineWidth = 2;
|
ctx.strokeStyle = '#ffd36a';
|
||||||
ctx.fillStyle = THEME.ink;
|
ctx.lineWidth = 3;
|
||||||
|
ctx.fillStyle = '#020304';
|
||||||
if (mode !== 'random') {
|
if (mode !== 'random') {
|
||||||
const d = DIRS.find(item => item.name === mode);
|
const d = DIRS.find(item => item.name === mode);
|
||||||
drawDirectionTriangle(ctx, center.x, center.y, d?.angle || 0, 9, 6);
|
drawDirectionTriangle(ctx, center.x, center.y, d?.angle || 0, 9, 6);
|
||||||
|
|
@ -400,10 +586,14 @@ function drawConveyorDirectionMarkers(ctx, center, dirs) {
|
||||||
const limited = dirs.slice(0, 4);
|
const limited = dirs.slice(0, 4);
|
||||||
if (!limited.length) return;
|
if (!limited.length) return;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.fillStyle = THEME.ink;
|
ctx.globalAlpha = 1;
|
||||||
ctx.globalAlpha = 0.86;
|
|
||||||
limited.forEach(d => {
|
limited.forEach(d => {
|
||||||
drawDirectionTriangle(ctx, center.x + d.dc * 12, center.y + d.dr * 12, d.angle, 6, 5);
|
const x = center.x + d.dc * 11;
|
||||||
|
const y = center.y + d.dr * 11;
|
||||||
|
ctx.fillStyle = 'rgba(255,205,83,.96)';
|
||||||
|
drawDirectionTriangle(ctx, x, y, d.angle, 9, 7);
|
||||||
|
ctx.fillStyle = '#020304';
|
||||||
|
drawDirectionTriangle(ctx, x, y, d.angle, 6.5, 4.6);
|
||||||
});
|
});
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
@ -413,6 +603,36 @@ function drawDirectionTriangle(ctx, x, y, angle, length = 11, halfWidth = 8) {
|
||||||
ctx.beginPath(); ctx.moveTo(length, 0); ctx.lineTo(-length * 0.72, -halfWidth); ctx.lineTo(-length * 0.72, halfWidth); ctx.closePath(); ctx.fill();
|
ctx.beginPath(); ctx.moveTo(length, 0); ctx.lineTo(-length * 0.72, -halfWidth); ctx.lineTo(-length * 0.72, halfWidth); ctx.closePath(); ctx.fill();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawScannerPortDuct(ctx, scanner, type, p, color) {
|
||||||
|
const c = scannerCenter(scanner);
|
||||||
|
let target = null;
|
||||||
|
if (type === 'inputA') target = { x: c.x, y: c.y - 67 };
|
||||||
|
if (type === 'left') target = { x: c.x - 49, y: c.y + 14 };
|
||||||
|
if (type === 'right') target = { x: c.x + 49, y: c.y + 14 };
|
||||||
|
if (!target) return;
|
||||||
|
const elbow = type === 'inputA'
|
||||||
|
? { x: p.x, y: target.y }
|
||||||
|
: { x: target.x, y: p.y };
|
||||||
|
ctx.save();
|
||||||
|
ctx.lineCap = 'square';
|
||||||
|
ctx.lineJoin = 'bevel';
|
||||||
|
ctx.strokeStyle = '#050609';
|
||||||
|
ctx.lineWidth = 15;
|
||||||
|
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(elbow.x, elbow.y); ctx.lineTo(target.x, target.y); ctx.stroke();
|
||||||
|
ctx.strokeStyle = '#596473';
|
||||||
|
ctx.lineWidth = 9;
|
||||||
|
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(elbow.x, elbow.y); ctx.lineTo(target.x, target.y); ctx.stroke();
|
||||||
|
ctx.strokeStyle = color || '#7b8795';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(elbow.x, elbow.y); ctx.lineTo(target.x, target.y); ctx.stroke();
|
||||||
|
ctx.fillStyle = color || '#7b8795';
|
||||||
|
ctx.strokeStyle = '#050609';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
rect(ctx, target.x - 9, target.y - 7, 18, 14, true, true);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
function drawScannerConnectors(ctx, game) {
|
function drawScannerConnectors(ctx, game) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
|
|
@ -428,11 +648,12 @@ function drawScannerConnectors(ctx, game) {
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (!item.p) continue;
|
if (!item.p) continue;
|
||||||
const p = cellCenter(item.p.col, item.p.row);
|
const p = cellCenter(item.p.col, item.p.row);
|
||||||
|
drawScannerPortDuct(ctx, scanner, item.type, p, item.color);
|
||||||
const connected = scannerPortHasConveyor(game, scanner, item.type || (item.label === 'IN' ? 'inputA' : null)) || game.conveyorTiles.has(key(item.p.col, item.p.row));
|
const connected = scannerPortHasConveyor(game, scanner, item.type || (item.label === 'IN' ? 'inputA' : null)) || game.conveyorTiles.has(key(item.p.col, item.p.row));
|
||||||
ctx.fillStyle = connected ? item.color : '#f4c2c2';
|
ctx.fillStyle = connected ? mixHex('#1a1d23', item.color, 0.24) : '#3a1e22';
|
||||||
ctx.strokeStyle = THEME.ink;
|
ctx.strokeStyle = connected ? item.color : THEME.danger;
|
||||||
rect(ctx, p.x - 18, p.y - 12, 36, 24, true, true);
|
rect(ctx, p.x - 18, p.y - 12, 36, 24, true, true);
|
||||||
ctx.fillStyle = connected && item.color === THEME.white ? THEME.ink : THEME.white;
|
ctx.fillStyle = THEME.white;
|
||||||
ctx.fillText(item.label, p.x, p.y + 4);
|
ctx.fillText(item.label, p.x, p.y + 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -598,7 +819,7 @@ function drawFacilityReceiver(ctx, game, facility) {
|
||||||
const c = cellCenter(f.entry.col, f.entry.row);
|
const c = cellCenter(f.entry.col, f.entry.row);
|
||||||
const info = receiverTitle(id, game);
|
const info = receiverTitle(id, game);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.fillStyle = THEME.white;
|
ctx.fillStyle = '#191c22';
|
||||||
ctx.strokeStyle = info.color;
|
ctx.strokeStyle = info.color;
|
||||||
ctx.lineWidth = 5;
|
ctx.lineWidth = 5;
|
||||||
rect(ctx, c.x - 22, c.y - 22, 44, 44, true, true);
|
rect(ctx, c.x - 22, c.y - 22, 44, 44, true, true);
|
||||||
|
|
@ -612,6 +833,90 @@ function drawFacilityReceiver(ctx, game, facility) {
|
||||||
ctx.fillText(info.title, c.x, c.y + 20);
|
ctx.fillText(info.title, c.x, c.y + 20);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function drawMetalPanel(ctx, x, y, w, h, fill = '#252b34', stroke = '#08090c', accent = '#606a78') {
|
||||||
|
ctx.save();
|
||||||
|
const g = ctx.createLinearGradient(x, y, x, y + h);
|
||||||
|
g.addColorStop(0, mixHex(fill, '#ffffff', 0.12));
|
||||||
|
g.addColorStop(0.55, fill);
|
||||||
|
g.addColorStop(1, mixHex(fill, '#000000', 0.28));
|
||||||
|
ctx.fillStyle = g;
|
||||||
|
ctx.strokeStyle = stroke;
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
rect(ctx, x, y, w, h, true, true);
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,.08)';
|
||||||
|
ctx.fillRect(x + 5, y + 5, Math.max(0, w - 10), 3);
|
||||||
|
ctx.strokeStyle = 'rgba(0,0,0,.34)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let yy = y + 11; yy < y + h - 8; yy += 13) { ctx.beginPath(); ctx.moveTo(x + 6, yy); ctx.lineTo(x + w - 6, yy); ctx.stroke(); }
|
||||||
|
ctx.fillStyle = accent;
|
||||||
|
for (const [rx, ry] of [[x + 8, y + 8], [x + w - 8, y + 8], [x + 8, y + h - 8], [x + w - 8, y + h - 8]]) {
|
||||||
|
ctx.beginPath(); ctx.arc(rx, ry, 2.2, 0, Math.PI * 2); ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFacilityDetails(ctx, f, accent = THEME.green) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = '#06070a'; ctx.lineWidth = 4;
|
||||||
|
ctx.beginPath(); ctx.moveTo(f.x + 12, f.y + f.h - 18); ctx.lineTo(f.x + f.w - 12, f.y + f.h - 18); ctx.stroke();
|
||||||
|
ctx.strokeStyle = accent; ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath(); ctx.moveTo(f.x + 16, f.y + 22); ctx.lineTo(f.x + f.w - 18, f.y + 22); ctx.stroke();
|
||||||
|
ctx.fillStyle = '#20242b'; ctx.strokeStyle = '#07080a'; ctx.lineWidth = 2;
|
||||||
|
rect(ctx, f.x + f.w - 44, f.y + 12, 30, 22, true, true);
|
||||||
|
ctx.fillStyle = accent; ctx.fillRect(f.x + f.w - 38, f.y + 18, 6, 6);
|
||||||
|
ctx.fillStyle = THEME.danger; ctx.fillRect(f.x + f.w - 27, f.y + 18, 6, 6);
|
||||||
|
ctx.strokeStyle = '#47515f'; ctx.lineWidth = 3;
|
||||||
|
ctx.beginPath(); ctx.moveTo(f.x + 12, f.y + f.h - 35); ctx.bezierCurveTo(f.x + 42, f.y + f.h - 52, f.x + 60, f.y + f.h - 20, f.x + 88, f.y + f.h - 38); ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFacilityTexture(ctx, image, f, maxCells = 1.45) {
|
||||||
|
if (!image?.loaded) return false;
|
||||||
|
// Keep external-machine sprites at roughly the same pixel density as a grid object.
|
||||||
|
const maxSize = GRID.cell * Math.min(1.45, maxCells);
|
||||||
|
const size = Math.max(42, Math.min(maxSize, f.w - 34, f.h - 42));
|
||||||
|
const x = f.x + Math.round((f.w - size) / 2);
|
||||||
|
const y = f.y + Math.round((f.h - size) / 2) + 2;
|
||||||
|
return drawImageIfLoaded(ctx, image, x, y, size, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
function roomBounds(room) {
|
||||||
|
const first = cellCenter(room.col, room.row);
|
||||||
|
return { x: first.x - GRID.cell / 2, y: first.y - GRID.cell / 2, w: GRID.cell * 2, h: GRID.cell * 2, cx: first.x + GRID.cell / 2, cy: first.y + GRID.cell / 2 };
|
||||||
|
}
|
||||||
|
function drawMaintenanceRooms(ctx, game) {
|
||||||
|
for (const room of game.maintenanceRooms || []) {
|
||||||
|
const b = roomBounds(room);
|
||||||
|
ctx.save();
|
||||||
|
drawMetalPanel(ctx, b.x + 2, b.y + 2, b.w - 4, b.h - 4, '#343b45', '#0a0b0e', '#7a6750');
|
||||||
|
if (!drawImageIfLoaded(ctx, assets.maintenanceRoom, b.x + 5, b.y + 5, b.w - 10, b.h - 10)) {
|
||||||
|
ctx.fillStyle = '#414a55'; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 3;
|
||||||
|
rect(ctx, b.x + 8, b.y + 8, b.w - 16, b.h - 16, true, true);
|
||||||
|
// Storage shelves, spare-parts crates, a workbench, lockers and a small break stool.
|
||||||
|
ctx.fillStyle = '#6e573e'; ctx.strokeStyle = '#111318'; ctx.lineWidth = 2;
|
||||||
|
rect(ctx, b.x + 13, b.y + 14, 24, 51, true, true);
|
||||||
|
ctx.strokeStyle = '#b58a52';
|
||||||
|
for (const yy of [b.y + 29, b.y + 44, b.y + 59]) { ctx.beginPath(); ctx.moveTo(b.x + 14, yy); ctx.lineTo(b.x + 36, yy); ctx.stroke(); }
|
||||||
|
ctx.fillStyle = '#515b68'; ctx.strokeStyle = '#111318';
|
||||||
|
rect(ctx, b.x + b.w - 34, b.y + 13, 20, 43, true, true);
|
||||||
|
ctx.fillStyle = '#d49d28'; ctx.fillRect(b.x + b.w - 21, b.y + 32, 3, 3);
|
||||||
|
ctx.fillStyle = '#735431'; ctx.strokeStyle = '#111318';
|
||||||
|
rect(ctx, b.x + 41, b.y + b.h - 31, 34, 14, true, true);
|
||||||
|
ctx.fillStyle = '#1b2027'; ctx.fillRect(b.x + 46, b.y + b.h - 39, 24, 8);
|
||||||
|
ctx.fillStyle = '#7b8795'; ctx.fillRect(b.x + 51, b.y + b.h - 37, 4, 5);
|
||||||
|
ctx.fillStyle = '#805b35'; ctx.strokeStyle = '#111318';
|
||||||
|
rect(ctx, b.x + b.w - 32, b.y + b.h - 28, 16, 12, true, true);
|
||||||
|
ctx.fillStyle = '#222831'; ctx.beginPath(); ctx.arc(b.x + b.w - 46, b.y + b.h - 20, 7, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillStyle = '#ddd0ad'; ctx.font = '900 8px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText('STORE / REST', b.cx, b.y + 11);
|
||||||
|
}
|
||||||
|
drawDirtOverlay(ctx, b.x + 5, b.y + 5, b.w - 10, b.h - 10, room);
|
||||||
|
drawSelection(ctx, game, 'maintenanceRoom', room.id, b.cx, b.cy, b.w + 6, b.h + 6);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function drawFacilities(ctx, game) {
|
function drawFacilities(ctx, game) {
|
||||||
for (const f of Object.values(game.facilities || {})) {
|
for (const f of Object.values(game.facilities || {})) {
|
||||||
const kind = facilityKind(f);
|
const kind = facilityKind(f);
|
||||||
|
|
@ -630,24 +935,29 @@ function drawExternalDuct(ctx, f) {
|
||||||
else if (f.side === 'top') { bx = c.x; by = f.y + f.h; }
|
else if (f.side === 'top') { bx = c.x; by = f.y + f.h; }
|
||||||
else if (f.side === 'bottom') { bx = c.x; by = f.y; }
|
else if (f.side === 'bottom') { bx = c.x; by = f.y; }
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.strokeStyle = THEME.ink;
|
ctx.strokeStyle = '#07080a';
|
||||||
ctx.lineWidth = 9;
|
ctx.lineWidth = 11;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.beginPath(); ctx.moveTo(c.x, c.y); ctx.lineTo(bx, by); ctx.stroke();
|
ctx.beginPath(); ctx.moveTo(c.x, c.y); ctx.lineTo(bx, by); ctx.stroke();
|
||||||
ctx.strokeStyle = THEME.white; ctx.lineWidth = 5;
|
ctx.strokeStyle = '#555f70'; ctx.lineWidth = 5;
|
||||||
ctx.beginPath(); ctx.moveTo(c.x, c.y); ctx.lineTo(bx, by); ctx.stroke();
|
ctx.beginPath(); ctx.moveTo(c.x, c.y); ctx.lineTo(bx, by); ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
function drawMixer(ctx, game, m = game.facilities.mixer) {
|
function drawMixer(ctx, game, m = game.facilities.mixer) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
drawExternalDuct(ctx, m);
|
drawExternalDuct(ctx, m);
|
||||||
if (drawImageIfLoaded(ctx, assets.mixer, m.x, m.y, m.w, m.h)) { drawDirtOverlay(ctx, m.x, m.y, m.w, m.h, m); drawSelection(ctx, game, 'facility', m.id, m.x + m.w / 2, m.y + m.h / 2, m.w + 12, m.h + 12); ctx.restore(); return; }
|
drawMetalPanel(ctx, m.x, m.y, m.w, m.h, '#2b333d', THEME.ink, THEME.mixerBlue);
|
||||||
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4; rect(ctx, m.x, m.y, m.w, m.h, true, true);
|
drawFacilityDetails(ctx, m, THEME.mixerBlue);
|
||||||
ctx.fillStyle = THEME.ink; ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`MIXER L${m.level}`, m.x + m.w / 2, m.y + 28);
|
ctx.fillStyle = THEME.white; ctx.font = '900 14px ui-monospace, monospace'; ctx.textAlign = 'left';
|
||||||
ctx.font = '900 12px ui-monospace, monospace'; ctx.fillText(`PAY ${yen(upgradedMixerPrice(game))}`, m.x + m.w / 2, m.y + 48);
|
ctx.fillText(`MIXER L${m.level}`, m.x + 14, m.y + 18);
|
||||||
|
ctx.fillStyle = THEME.muted; ctx.font = '900 10px ui-monospace, monospace';
|
||||||
|
ctx.fillText(`PAY ${yen(upgradedMixerPrice(game))}`, m.x + 14, m.y + 34);
|
||||||
|
const drew = drawFacilityTexture(ctx, assets.mixerCompact.loaded ? assets.mixerCompact : assets.mixer, m, 2.05);
|
||||||
|
if (!drew) {
|
||||||
|
ctx.strokeStyle = THEME.green; ctx.lineWidth = 5;
|
||||||
|
for (let i = 0; i < 3; i += 1) { ctx.beginPath(); ctx.arc(m.x + m.w / 2, m.y + m.h / 2 + 6, 11 + i * 8, 0, Math.PI * 1.5); ctx.stroke(); }
|
||||||
|
}
|
||||||
drawTargetBadge(ctx, m.x + 14, m.y + m.h - 40, m.w - 28, 'SEND MALE', 'male', 'safe meat route');
|
drawTargetBadge(ctx, m.x + 14, m.y + m.h - 40, m.w - 28, 'SEND MALE', 'male', 'safe meat route');
|
||||||
ctx.strokeStyle = THEME.green; ctx.lineWidth = 5;
|
|
||||||
for (let i = 0; i < 3; i += 1) { ctx.beginPath(); ctx.arc(m.x + m.w / 2, m.y + 78, 16 + i * 8, 0, Math.PI * 1.5); ctx.stroke(); }
|
|
||||||
drawDirtOverlay(ctx, m.x, m.y, m.w, m.h, m);
|
drawDirtOverlay(ctx, m.x, m.y, m.w, m.h, m);
|
||||||
drawSelection(ctx, game, 'facility', m.id, m.x + m.w / 2, m.y + m.h / 2, m.w + 12, m.h + 12);
|
drawSelection(ctx, game, 'facility', m.id, m.x + m.w / 2, m.y + m.h / 2, m.w + 12, m.h + 12);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
@ -655,10 +965,15 @@ function drawMixer(ctx, game, m = game.facilities.mixer) {
|
||||||
function drawTrash(ctx, game, t = game.facilities.trash) {
|
function drawTrash(ctx, game, t = game.facilities.trash) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
drawExternalDuct(ctx, t);
|
drawExternalDuct(ctx, t);
|
||||||
if (drawImageIfLoaded(ctx, assets.shredder, t.x, t.y, t.w, t.h)) { drawDirtOverlay(ctx, t.x, t.y, t.w, t.h, t); drawSelection(ctx, game, 'facility', t.id, t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12); ctx.restore(); return; }
|
drawMetalPanel(ctx, t.x, t.y, t.w, t.h, '#29342d', THEME.ink, THEME.wasteGreen);
|
||||||
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4; rect(ctx, t.x, t.y, t.w, t.h, true, true);
|
drawFacilityDetails(ctx, t, THEME.wasteGreen);
|
||||||
ctx.fillStyle = THEME.ink; ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`SHREDDER L${t.level}`, t.x + t.w / 2, t.y + 25);
|
ctx.fillStyle = THEME.white; ctx.font = '900 14px ui-monospace, monospace'; ctx.textAlign = 'left';
|
||||||
for (let i = 0; i < 7; i += 1) { ctx.fillRect(t.x + 34 + i * 16, t.y + 48, 7, t.h - 66); }
|
ctx.fillText(`SHREDDER L${t.level}`, t.x + 14, t.y + 18);
|
||||||
|
const drew = drawFacilityTexture(ctx, assets.shredderCompact.loaded ? assets.shredderCompact : assets.shredder, t, 1.85);
|
||||||
|
if (!drew) {
|
||||||
|
ctx.fillStyle = '#0a0b0e';
|
||||||
|
for (let i = 0; i < 7; i += 1) ctx.fillRect(t.x + 40 + i * 12, t.y + 42, 6, Math.max(20, t.h - 72));
|
||||||
|
}
|
||||||
drawTargetBadge(ctx, t.x + 14, t.y + t.h - 42, t.w - 28, 'SEND POOP', 'poop', 'shredder');
|
drawTargetBadge(ctx, t.x + 14, t.y + t.h - 42, t.w - 28, 'SEND POOP', 'poop', 'shredder');
|
||||||
drawDirtOverlay(ctx, t.x, t.y, t.w, t.h, t);
|
drawDirtOverlay(ctx, t.x, t.y, t.w, t.h, t);
|
||||||
drawSelection(ctx, game, 'facility', t.id, t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12);
|
drawSelection(ctx, game, 'facility', t.id, t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12);
|
||||||
|
|
@ -667,14 +982,18 @@ function drawTrash(ctx, game, t = game.facilities.trash) {
|
||||||
function drawTruck(ctx, game, t = game.facilities.truck) {
|
function drawTruck(ctx, game, 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)) { drawSelection(ctx, game, 'facility', t.id, t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12); ctx.restore(); return; }
|
drawMetalPanel(ctx, t.x, t.y, t.w, t.h, '#343128', THEME.ink, THEME.truckPink);
|
||||||
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4; rect(ctx, t.x, t.y, t.w, t.h, true, true);
|
drawFacilityDetails(ctx, t, THEME.truckPink);
|
||||||
ctx.fillStyle = THEME.ink; ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`TRUCK L${t.level}`, t.x + t.w / 2, t.y + 24);
|
ctx.fillStyle = THEME.white; ctx.font = '900 14px ui-monospace, monospace'; ctx.textAlign = 'left';
|
||||||
ctx.font = '900 11px ui-monospace, monospace'; ctx.fillText(`UNIT ${yen(upgradedTruckPrice(game))}`, t.x + t.w / 2, t.y + 42);
|
ctx.fillText(`TRUCK L${t.level}`, t.x + 14, t.y + 18);
|
||||||
|
ctx.fillStyle = THEME.muted; ctx.font = '900 10px ui-monospace, monospace';
|
||||||
|
ctx.fillText(`UNIT ${yen(upgradedTruckPrice(game))}`, t.x + 14, t.y + 34);
|
||||||
|
drawFacilityTexture(ctx, assets.truckCompact.loaded ? assets.truckCompact : assets.truck, t, 2.0);
|
||||||
const truckTargetType = targetForTruck(game);
|
const truckTargetType = targetForTruck(game);
|
||||||
drawTargetBadge(ctx, t.x + 15, t.y + 50, t.w - 30, `SEND ${truckTargetType.toUpperCase()}`, truckTargetType, game.eventOffer && !game.eventActive ? 'next event' : 'truck cargo');
|
drawTargetBadge(ctx, t.x + 15, t.y + 50, t.w - 30, `SEND ${truckTargetType.toUpperCase()}`, truckTargetType, game.eventOffer && !game.eventActive ? 'next event' : 'truck cargo');
|
||||||
ctx.fillStyle = THEME.greenSoft; rect(ctx, t.x + 13, t.y + 88, t.w - 26, t.h - 103, true, false);
|
ctx.fillStyle = 'rgba(39,60,36,.9)'; rect(ctx, t.x + 13, t.y + 88, t.w - 26, Math.max(18, t.h - 103), true, false);
|
||||||
for (const cargo of game.truckCargo) cargo.sex === 'poop' ? drawTinyPoop(ctx, t.x + cargo.x, t.y + cargo.y) : drawTinyChick(ctx, t.x + cargo.x, t.y + cargo.y, cargo.sex, cargo.sex === 'male');
|
for (const cargo of game.truckCargo) cargo.sex === 'poop' ? drawTinyPoop(ctx, t.x + cargo.x, t.y + cargo.y) : drawTinyChick(ctx, t.x + cargo.x, t.y + cargo.y, cargo.sex, cargo.sex === 'male');
|
||||||
|
drawDirtOverlay(ctx, t.x, t.y, t.w, t.h, t);
|
||||||
drawSelection(ctx, game, 'facility', t.id, t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12);
|
drawSelection(ctx, game, 'facility', t.id, t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
@ -695,13 +1014,32 @@ function drawChick(ctx, chick, active, game) {
|
||||||
if (chick.sex === 'poop') drawPoop(ctx, chick.x, y, chick.radius);
|
if (chick.sex === 'poop') drawPoop(ctx, chick.x, y, chick.radius);
|
||||||
else {
|
else {
|
||||||
const img = chick.sex === 'male' ? assets.chickMale : assets.chickFemale;
|
const img = chick.sex === 'male' ? assets.chickMale : assets.chickFemale;
|
||||||
if (!drawImageIfLoaded(ctx, img, chick.x - chick.radius, y - chick.radius, chick.radius * 2, chick.radius * 2)) {
|
const spriteDrawn = drawImageIfLoaded(ctx, img, chick.x - chick.radius, y - chick.radius, chick.radius * 2, chick.radius * 2);
|
||||||
ctx.fillStyle = chick.sex === 'male' ? THEME.male : THEME.female;
|
if (!spriteDrawn) {
|
||||||
|
const bodyColor = chick.sex === 'male' ? THEME.male : THEME.female;
|
||||||
|
const accent = chick.sex === 'male' ? '#164f8f' : '#8b2252';
|
||||||
|
ctx.fillStyle = bodyColor;
|
||||||
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 5;
|
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 5;
|
||||||
ctx.beginPath(); ctx.arc(chick.x, y, chick.radius, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
ctx.beginPath(); ctx.arc(chick.x, y, chick.radius, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,.24)'; ctx.beginPath(); ctx.arc(chick.x - 5, y - 7, chick.radius * .36, 0, Math.PI * 2); ctx.fill();
|
||||||
ctx.fillStyle = THEME.ink; ctx.beginPath(); ctx.arc(chick.x - 6, y - 5, 2.5, 0, Math.PI * 2); ctx.arc(chick.x + 6, y - 5, 2.5, 0, Math.PI * 2); ctx.fill();
|
ctx.fillStyle = THEME.ink; ctx.beginPath(); ctx.arc(chick.x - 6, y - 5, 2.5, 0, Math.PI * 2); ctx.arc(chick.x + 6, y - 5, 2.5, 0, Math.PI * 2); ctx.fill();
|
||||||
ctx.fillStyle = '#ffaa2e'; ctx.beginPath(); ctx.moveTo(chick.x, y + 2); ctx.lineTo(chick.x + 9, y + 6); ctx.lineTo(chick.x, y + 10); ctx.closePath(); ctx.fill();
|
ctx.fillStyle = '#ffaa2e'; ctx.beginPath(); ctx.moveTo(chick.x, y + 2); ctx.lineTo(chick.x + 9, y + 6); ctx.lineTo(chick.x, y + 10); ctx.closePath(); ctx.fill();
|
||||||
|
ctx.strokeStyle = accent; ctx.lineWidth = 3;
|
||||||
|
if (chick.sex === 'male') {
|
||||||
|
ctx.beginPath(); ctx.moveTo(chick.x - 8, y - chick.radius - 2); ctx.lineTo(chick.x + 8, y - chick.radius - 2); ctx.stroke();
|
||||||
|
} else {
|
||||||
|
ctx.beginPath(); ctx.moveTo(chick.x - 9, y - chick.radius + 2); ctx.lineTo(chick.x, y - chick.radius - 6); ctx.lineTo(chick.x + 9, y - chick.radius + 2); ctx.stroke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Symbols remain legible even when custom sprites are replaced.
|
||||||
|
ctx.fillStyle = chick.sex === 'male' ? '#dff1ff' : '#fff0f7';
|
||||||
|
ctx.strokeStyle = '#050609';
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.font = `900 ${Math.max(10, Math.round(chick.radius * .72))}px ui-monospace, monospace`;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
const sexMark = chick.sex === 'male' ? '♂' : '♀';
|
||||||
|
ctx.strokeText(sexMark, chick.x + chick.radius * .54, y + chick.radius * .70);
|
||||||
|
ctx.fillText(sexMark, chick.x + chick.radius * .54, y + chick.radius * .70);
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
@ -721,8 +1059,14 @@ function drawPoop(ctx, x, y, radius) {
|
||||||
}
|
}
|
||||||
function drawTinyPoop(ctx, x, y) { drawPoop(ctx, x, y, 7); }
|
function drawTinyPoop(ctx, x, y) { drawPoop(ctx, x, y, 7); }
|
||||||
function drawTinyChick(ctx, x, y, sex, warning = false) {
|
function drawTinyChick(ctx, x, y, sex, warning = false) {
|
||||||
ctx.save(); ctx.fillStyle = sex === 'male' ? THEME.male : THEME.female; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 2;
|
ctx.save();
|
||||||
|
const bodyColor = sex === 'male' ? THEME.male : THEME.female;
|
||||||
|
const accent = sex === 'male' ? '#164f8f' : '#8b2252';
|
||||||
|
ctx.fillStyle = bodyColor; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 2;
|
||||||
ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
ctx.beginPath(); ctx.arc(x, y, 7, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||||
|
ctx.strokeStyle = accent; ctx.lineWidth = 2;
|
||||||
|
if (sex === 'male') { ctx.beginPath(); ctx.moveTo(x - 4, y - 8); ctx.lineTo(x + 4, y - 8); ctx.stroke(); }
|
||||||
|
else { ctx.beginPath(); ctx.moveTo(x - 5, y - 7); ctx.lineTo(x, y - 11); ctx.lineTo(x + 5, y - 7); ctx.stroke(); }
|
||||||
if (warning) { ctx.fillStyle = THEME.danger; ctx.font = '900 12px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText('!', x, y + 15); }
|
if (warning) { ctx.fillStyle = THEME.danger; ctx.font = '900 12px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText('!', x, y + 15); }
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
@ -784,9 +1128,12 @@ function drawCardTargetOverlay(ctx, canvas, game) {
|
||||||
ctx.strokeStyle = THEME.ink;
|
ctx.strokeStyle = THEME.ink;
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
const remaining = Math.max(1, game.cardTargetPick?.remaining || 1);
|
const remaining = Math.max(1, game.cardTargetPick?.remaining || 1);
|
||||||
|
const dynamiteUsed = Math.max(0, game.cardTargetPick?.used || 0);
|
||||||
const msg = game.cardTargetPick?.mode === 'autoScannerMenu'
|
const msg = game.cardTargetPick?.mode === 'autoScannerMenu'
|
||||||
? 'AUTO SCANNER ROUTE SETTINGS. CLICK EMPTY AREA TO CLOSE.'
|
? 'AUTO SCANNER ROUTE SETTINGS. CLICK EMPTY AREA TO CLOSE.'
|
||||||
: isBlockedCellMode ? `CLICK BLOCKED CELL: ${remaining} LEFT. PRESS ESC TO CANCEL.` : 'CLICK AN UPGRADEABLE MACHINE. PRESS ESC TO CANCEL.';
|
: isBlockedCellMode
|
||||||
|
? `DYNAMITE: CLICK ANY BLOCKED TILE (${remaining} LEFT). ESC ${dynamiteUsed > 0 ? 'FINISHES' : 'CANCELS'}.`
|
||||||
|
: 'CLICK AN UPGRADEABLE MACHINE. PRESS ESC TO CANCEL.';
|
||||||
rect(ctx, (216 - (game.view?.x || 0)) / scale, (96 - (game.view?.y || 0)) / scale, 560 / scale, 42 / scale, true, true);
|
rect(ctx, (216 - (game.view?.x || 0)) / scale, (96 - (game.view?.y || 0)) / scale, 560 / scale, 42 / scale, true, true);
|
||||||
ctx.fillStyle = THEME.ink;
|
ctx.fillStyle = THEME.ink;
|
||||||
ctx.font = '900 12px ui-monospace, monospace';
|
ctx.font = '900 12px ui-monospace, monospace';
|
||||||
|
|
@ -835,10 +1182,19 @@ function drawFloatingTexts(ctx, game) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawRepairman(ctx, game) {
|
function drawRepairman(ctx, game) {
|
||||||
const r = game.repairman;
|
const list = (game.repairmen && game.repairmen.length) ? game.repairmen : (game.repairman ? [game.repairman] : []);
|
||||||
const visible = !!r?.active || (game.phase === 'build' && !!r?.hiredForNextDay);
|
for (const r of list) drawSingleRepairman(ctx, game, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSingleRepairman(ctx, game, r) {
|
||||||
|
const visible = !!r?.active;
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
if (drawImageIfLoaded(ctx, assets.repairman, r.x - 18, r.y - 18, 36, 36)) {
|
||||||
|
if (r.target?.label) label(ctx, r.x, r.y - 24, r.target.label, THEME.green);
|
||||||
|
ctx.restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
ctx.fillStyle = '#fff0d1';
|
ctx.fillStyle = '#fff0d1';
|
||||||
ctx.strokeStyle = THEME.ink;
|
ctx.strokeStyle = THEME.ink;
|
||||||
|
|
@ -862,19 +1218,15 @@ function drawRepairman(ctx, game) {
|
||||||
ctx.fillStyle = THEME.ink;
|
ctx.fillStyle = THEME.ink;
|
||||||
ctx.font = '900 11px ui-monospace, monospace';
|
ctx.font = '900 11px ui-monospace, monospace';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(r.active ? 'FIX' : 'HIRED', r.x, r.y + 33);
|
ctx.fillText('FIX', r.x, r.y + 33);
|
||||||
if (r.target?.label) label(ctx, r.x, r.y - 24, r.target.label, THEME.green);
|
if (r.target?.label) label(ctx, r.x, r.y - 24, r.target.label, THEME.green);
|
||||||
else if (!r.active && r.hiredForNextDay) label(ctx, r.x, r.y - 24, 'NEXT DAY', THEME.green);
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function drawCanvasHints(ctx, game, canvas) {
|
function drawCanvasHints(ctx, game, canvas) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'left'; ctx.fillStyle = THEME.ink;
|
ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'left'; ctx.fillStyle = THEME.ink;
|
||||||
// Visible version marker helps avoid browser/file-cache confusion when testing zips.
|
|
||||||
ctx.fillStyle = 'rgba(255,255,255,.92)'; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 3;
|
|
||||||
rect(ctx, canvas.width - 284, 18, 260, 34, true, true);
|
|
||||||
ctx.fillStyle = THEME.green; ctx.font = '900 12px ui-monospace, monospace'; ctx.fillText(VERSION, canvas.width - 270, 40);
|
|
||||||
ctx.font = '900 19px ui-monospace, monospace'; ctx.fillStyle = THEME.ink;
|
ctx.font = '900 19px ui-monospace, monospace'; ctx.fillStyle = THEME.ink;
|
||||||
const lines = [];
|
const lines = [];
|
||||||
if (game.phase === 'running') lines.push(game.timeLeft <= 0 ? 'TIME UP: FARMS SHUT. CLEAR LINE.' : 'A/D: S1 ←/→: S2');
|
if (game.phase === 'running') lines.push(game.timeLeft <= 0 ? 'TIME UP: FARMS SHUT. CLEAR LINE.' : 'A/D: S1 ←/→: S2');
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { BALANCE } from '../core/balance.js';
|
import { BALANCE } from '../core/balance.js';
|
||||||
import { DIRS, MACHINE_FACILITY_IDS, GRID, THEME } from '../core/config.js';
|
import { DIRS, MACHINE_FACILITY_IDS, GRID, THEME } from '../core/config.js';
|
||||||
import { getSpawnRange } from '../core/state.js';
|
import { getSpawnRange } from '../core/state.js';
|
||||||
import { createEggFarm, createScanner, createFacility } from '../core/entities.js';
|
import { createEggFarm, createScanner, createFacility, createMaintenanceRoom, maintenanceRoomFootprint } from '../core/entities.js';
|
||||||
import { generateBlockedCellsInRect } from '../core/mapGen.js';
|
import { generateBlockedCellsInRect, blockedRatioForExpansionPurchase } from '../core/mapGen.js';
|
||||||
import { expansionCost, expansionLot, expansionLots, lotFromChunk, lotBounds, lotContainsPoint, cellsInLot, isOwnedCell, countOwnedCells, shiftRowIndexedSet, shiftRowIndexedMap, ownChunk } from '../core/gridExpansion.js';
|
import { expansionCost, expansionLot, expansionLots, lotFromChunk, lotBounds, lotContainsPoint, cellsInLot, isOwnedCell, countOwnedCells, shiftRowIndexedSet, shiftRowIndexedMap, ownChunk } from '../core/gridExpansion.js';
|
||||||
import { key, parseKey, pointToCell, cellCenter, yen, directionNameBetweenCells } from '../core/utils.js';
|
import { key, parseKey, pointToCell, cellCenter, yen, directionNameBetweenCells } from '../core/utils.js';
|
||||||
import { TEXT, equipmentName } from '../core/text.js';
|
import { TEXT, equipmentName } from '../core/text.js';
|
||||||
|
|
@ -92,13 +92,18 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
game.gridExpansionPurchases = (game.gridExpansionPurchases || 0) + 1;
|
game.gridExpansionPurchases = (game.gridExpansionPurchases || 0) + 1;
|
||||||
game.gridExpansionPurchasesByDirection[direction] = (game.gridExpansionPurchasesByDirection[direction] || 0) + 1;
|
game.gridExpansionPurchasesByDirection[direction] = (game.gridExpansionPurchasesByDirection[direction] || 0) + 1;
|
||||||
syncGridSizeToGame();
|
syncGridSizeToGame();
|
||||||
|
const blockedRatio = blockedRatioForExpansionPurchase(game.gridExpansionPurchases, {
|
||||||
|
base: BALANCE.map.blockedRatio ?? 0.03,
|
||||||
|
increment: BALANCE.map.blockedExpansionIncrement ?? 0.005,
|
||||||
|
max: BALANCE.map.blockedRatioMax ?? 0.10
|
||||||
|
});
|
||||||
const blocked = generateBlockedCellsInRect({
|
const blocked = generateBlockedCellsInRect({
|
||||||
seed: `${game.runSeed}:grid-expansion:${lot.chunkX},${lot.chunkY}:${game.gridExpansionPurchases}`,
|
seed: `${game.runSeed}:grid-expansion:${lot.chunkX},${lot.chunkY}:${game.gridExpansionPurchases}`,
|
||||||
colStart: lot.colStart,
|
colStart: lot.colStart,
|
||||||
colEnd: lot.colStart + lot.cols,
|
colEnd: lot.colStart + lot.cols,
|
||||||
rowStart: lot.rowStart,
|
rowStart: lot.rowStart,
|
||||||
rowEnd: lot.rowStart + lot.rows,
|
rowEnd: lot.rowStart + lot.rows,
|
||||||
targetRatio: BALANCE.map.blockedRatio
|
targetRatio: blockedRatio
|
||||||
});
|
});
|
||||||
for (const k of blocked) game.blockedCells.add(k);
|
for (const k of blocked) game.blockedCells.add(k);
|
||||||
refreshRoutingAfterEdit(game);
|
refreshRoutingAfterEdit(game);
|
||||||
|
|
@ -106,7 +111,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
const b = lotBounds(lot);
|
const b = lotBounds(lot);
|
||||||
floating(game, b.x + b.w / 2, b.y + b.h / 2, `${direction.toUpperCase()} +${addedCells} CELLS -${yen(cost)}`, THEME.green);
|
floating(game, b.x + b.w / 2, b.y + b.h / 2, `${direction.toUpperCase()} +${addedCells} CELLS -${yen(cost)}`, THEME.green);
|
||||||
updatePanels();
|
updatePanels();
|
||||||
return { ok: true, direction, cost, addedCells, blocked: blocked.size, chunkX: lot.chunkX, chunkY: lot.chunkY };
|
return { ok: true, direction, cost, addedCells, blocked: blocked.size, blockedRatio, chunkX: lot.chunkX, chunkY: lot.chunkY };
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointInGrid(col, row) { return col >= 0 && col < GRID.cols && row >= 0 && row < GRID.rows && isOwnedCell(game, col, row); }
|
function pointInGrid(col, row) { return col >= 0 && col < GRID.cols && row >= 0 && row < GRID.rows && isOwnedCell(game, col, row); }
|
||||||
|
|
@ -129,10 +134,32 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
if (existingFarm && !(moving?.type === 'eggFarm' && moving.ref?.id === existingFarm.id)) return true;
|
if (existingFarm && !(moving?.type === 'eggFarm' && moving.ref?.id === existingFarm.id)) return true;
|
||||||
const existingScanner = scannerAt(game, col, row);
|
const existingScanner = scannerAt(game, col, row);
|
||||||
if (existingScanner && !(moving?.type === 'scanner' && moving.ref?.id === existingScanner.id)) return true;
|
if (existingScanner && !(moving?.type === 'scanner' && moving.ref?.id === existingScanner.id)) return true;
|
||||||
|
const existingRoom = maintenanceRoomAt(game, col, row);
|
||||||
|
if (existingRoom && !(moving?.type === 'maintenanceRoom' && moving.ref?.id === existingRoom.id)) return true;
|
||||||
if (game.conveyorTiles.has(k) && !(moving?.type === 'conveyor' && moving.oldKey === k)) return true;
|
if (game.conveyorTiles.has(k) && !(moving?.type === 'conveyor' && moving.oldKey === k)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maintenanceRoomAt(gameRef, col, row) {
|
||||||
|
return (gameRef.maintenanceRooms || []).find(room => maintenanceRoomFootprint(room).some(cell => cell.col === col && cell.row === row)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maintenanceRoomPlacementBlocked(col, row, moving = null) {
|
||||||
|
const draft = { col, row };
|
||||||
|
for (const cell of maintenanceRoomFootprint(draft)) {
|
||||||
|
if (!pointInGrid(cell.col, cell.row) || isBlockedCell(cell.col, cell.row)) return true;
|
||||||
|
const k = key(cell.col, cell.row);
|
||||||
|
const existingFarm = farmAt(game, cell.col, cell.row);
|
||||||
|
if (existingFarm) return true;
|
||||||
|
const existingScanner = scannerAt(game, cell.col, cell.row);
|
||||||
|
if (existingScanner) return true;
|
||||||
|
const existingRoom = maintenanceRoomAt(game, cell.col, cell.row);
|
||||||
|
if (existingRoom && !(moving?.type === 'maintenanceRoom' && moving.ref?.id === existingRoom.id)) return true;
|
||||||
|
if (game.conveyorTiles.has(k)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function scannerPlacementBlocked(col, row, moving = null) {
|
function scannerPlacementBlocked(col, row, moving = null) {
|
||||||
const draft = { col, row };
|
const draft = { col, row };
|
||||||
for (const cell of scannerFootprintCells(draft)) {
|
for (const cell of scannerFootprintCells(draft)) {
|
||||||
|
|
@ -153,6 +180,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
if (game.selected.type === 'scanner') return game.scanners.find(s => s.id === game.selected.id) || null;
|
if (game.selected.type === 'scanner') return game.scanners.find(s => s.id === game.selected.id) || null;
|
||||||
if (game.selected.type === 'conveyor') return game.conveyorTiles.has(game.selected.id) ? { type: 'conveyor', id: game.selected.id } : null;
|
if (game.selected.type === 'conveyor') return game.conveyorTiles.has(game.selected.id) ? { type: 'conveyor', id: game.selected.id } : null;
|
||||||
if (game.selected.type === 'facility') return game.facilities[game.selected.id] || null;
|
if (game.selected.type === 'facility') return game.facilities[game.selected.id] || null;
|
||||||
|
if (game.selected.type === 'maintenanceRoom') return (game.maintenanceRooms || []).find(r => r.id === game.selected.id) || null;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,6 +190,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
if (obj.type === 'scanner') return `${obj.kind === 'auto' ? equipmentName('autoScanner') : equipmentName('manualScanner')} #${obj.id}`;
|
if (obj.type === 'scanner') return `${obj.kind === 'auto' ? equipmentName('autoScanner') : equipmentName('manualScanner')} #${obj.id}`;
|
||||||
if (obj.type === 'conveyor') return game.conveyorMeta.get(obj.id)?.kind === 'boostConveyor' ? equipmentName('boostConveyor') : equipmentName('conveyor');
|
if (obj.type === 'conveyor') return game.conveyorMeta.get(obj.id)?.kind === 'boostConveyor' ? equipmentName('boostConveyor') : equipmentName('conveyor');
|
||||||
if (obj.type === 'facility') return obj.name;
|
if (obj.type === 'facility') return obj.name;
|
||||||
|
if (obj.type === 'maintenanceRoom') return `${equipmentName('maintenanceRoom')} #${obj.id}`;
|
||||||
return 'Equipment';
|
return 'Equipment';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,16 +295,18 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
return connected;
|
return connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
function branchSelectableEntranceDirsForKey(k) {
|
function branchSelectableExitDirsForKey(k) {
|
||||||
const cell = parseKey(k);
|
const cell = parseKey(k);
|
||||||
const connected = branchExitDirsForKey(k);
|
const connected = branchExitDirsForKey(k);
|
||||||
if (connected.length < 3) return [];
|
if (connected.length < 3) return [];
|
||||||
|
const meta = game.conveyorMeta.get(k) || {};
|
||||||
const physicalEntrances = new Set(connected.filter(dir => neighborPointsIntoCell(cell, dir)));
|
const physicalEntrances = new Set(connected.filter(dir => neighborPointsIntoCell(cell, dir)));
|
||||||
return connected.filter(dir => {
|
const physicalExits = connected.filter(dir => !physicalEntrances.has(dir));
|
||||||
const entrances = new Set(physicalEntrances);
|
if (physicalEntrances.size && physicalExits.length >= 2) return physicalExits;
|
||||||
entrances.add(dir);
|
const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
|
||||||
return connected.filter(item => !entrances.has(item)).length >= 2;
|
.filter(dir => connected.includes(dir));
|
||||||
});
|
if (explicit.length >= 2) return explicit;
|
||||||
|
return connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
function markConveyorDirection(from, to) {
|
function markConveyorDirection(from, to) {
|
||||||
|
|
@ -502,6 +533,10 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isBlockedCell(col, row)) return fail('Cannot build on blocked ground.');
|
if (isBlockedCell(col, row)) return fail('Cannot build on blocked ground.');
|
||||||
|
if (game.buildTool === 'maintenanceRoom') {
|
||||||
|
if (maintenanceRoomPlacementBlocked(col, row)) return fail(TEXT.fail.cellOccupied);
|
||||||
|
return buildMaintenanceRoom(col, row);
|
||||||
|
}
|
||||||
if ((game.buildTool === 'manualScanner' || game.buildTool === 'autoScanner') ? scannerPlacementBlocked(col, row) : isEquipmentCell(col, row)) return fail(TEXT.fail.cellOccupied);
|
if ((game.buildTool === 'manualScanner' || game.buildTool === 'autoScanner') ? scannerPlacementBlocked(col, row) : isEquipmentCell(col, row)) return fail(TEXT.fail.cellOccupied);
|
||||||
if (game.buildTool === 'eggFarm') buildFarm(col, row);
|
if (game.buildTool === 'eggFarm') buildFarm(col, row);
|
||||||
else if (game.buildTool === 'manualScanner') buildScanner(col, row, 'manual');
|
else if (game.buildTool === 'manualScanner') buildScanner(col, row, 'manual');
|
||||||
|
|
@ -510,7 +545,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
|
|
||||||
function buildAtPoint(p) {
|
function buildAtPoint(p) {
|
||||||
const cell = pointToCell(p.x, p.y);
|
const cell = pointToCell(p.x, p.y);
|
||||||
if (['conveyor', 'boostConveyor', 'eggFarm', 'manualScanner', 'autoScanner'].includes(game.buildTool)) return buildAtCell(cell);
|
if (['conveyor', 'boostConveyor', 'eggFarm', 'manualScanner', 'autoScanner', 'maintenanceRoom'].includes(game.buildTool)) return buildAtCell(cell);
|
||||||
if (MACHINE_FACILITY_IDS.includes(game.buildTool)) return buildFacility(p, game.buildTool);
|
if (MACHINE_FACILITY_IDS.includes(game.buildTool)) return buildFacility(p, game.buildTool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -542,6 +577,26 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
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 buildMaintenanceRoom(col, row) {
|
||||||
|
const cost = buildPrice('maintenanceRoom', game);
|
||||||
|
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||||
|
if (maintenanceRoomPlacementBlocked(col, row)) return fail(TEXT.fail.cellOccupied);
|
||||||
|
record(game);
|
||||||
|
spendCash(game, cost);
|
||||||
|
const room = applyBuildQuality(createMaintenanceRoom(game, col, row));
|
||||||
|
room.price = cost;
|
||||||
|
game.maintenanceRooms = game.maintenanceRooms || [];
|
||||||
|
game.maintenanceRooms.push(room);
|
||||||
|
game.selected = { type: 'maintenanceRoom', id: room.id };
|
||||||
|
const center = roomCenter(room);
|
||||||
|
floating(game, center.x, center.y, `-${yen(cost)}`, THEME.ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
function roomCenter(room) {
|
||||||
|
const first = cellCenter(room.col, room.row);
|
||||||
|
return { x: first.x + GRID.cell / 2, y: first.y + GRID.cell / 2 };
|
||||||
|
}
|
||||||
|
|
||||||
function buildFacility(p, id) {
|
function buildFacility(p, id) {
|
||||||
const cost = buildPrice(id, game);
|
const cost = buildPrice(id, game);
|
||||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||||
|
|
@ -603,6 +658,13 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
eraseEffect(game, f.x + f.w / 2, f.y + f.h / 2);
|
eraseEffect(game, f.x + f.w / 2, f.y + f.h / 2);
|
||||||
refreshRoutingAfterEdit(game);
|
refreshRoutingAfterEdit(game);
|
||||||
}
|
}
|
||||||
|
if (hit.type === 'maintenanceRoom') {
|
||||||
|
const room = hit.ref;
|
||||||
|
const center = roomCenter(room);
|
||||||
|
refundOrSell(hit, center.x, center.y);
|
||||||
|
game.maintenanceRooms = (game.maintenanceRooms || []).filter(r => r.id !== room.id);
|
||||||
|
eraseEffect(game, center.x, center.y);
|
||||||
|
}
|
||||||
game.selected = null;
|
game.selected = null;
|
||||||
game.multiSelected = [];
|
game.multiSelected = [];
|
||||||
updatePanels();
|
updatePanels();
|
||||||
|
|
@ -623,6 +685,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
if (farm) return { type: 'eggFarm', ref: farm };
|
if (farm) return { type: 'eggFarm', ref: farm };
|
||||||
const scanner = scannerAt(game, cell.col, cell.row);
|
const scanner = scannerAt(game, cell.col, cell.row);
|
||||||
if (scanner) return { type: 'scanner', ref: scanner };
|
if (scanner) return { type: 'scanner', ref: scanner };
|
||||||
|
const room = maintenanceRoomAt(game, cell.col, cell.row);
|
||||||
|
if (room) return { type: 'maintenanceRoom', ref: room };
|
||||||
const k = key(cell.col, cell.row);
|
const k = key(cell.col, cell.row);
|
||||||
if (game.conveyorTiles.has(k)) return { type: 'conveyor', oldKey: k, ref: { type: 'conveyor', id: k } };
|
if (game.conveyorTiles.has(k)) return { type: 'conveyor', oldKey: k, ref: { type: 'conveyor', id: k } };
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -641,6 +705,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
else if (obj.type === 'eggFarm') eraseAtPoint(cellCenter(obj.col, obj.row));
|
else if (obj.type === 'eggFarm') eraseAtPoint(cellCenter(obj.col, obj.row));
|
||||||
else if (obj.type === 'scanner') eraseAtPoint(scannerCenter(obj));
|
else if (obj.type === 'scanner') eraseAtPoint(scannerCenter(obj));
|
||||||
else if (obj.type === 'facility') eraseAtPoint({ x: obj.x + obj.w / 2, y: obj.y + obj.h / 2 });
|
else if (obj.type === 'facility') eraseAtPoint({ x: obj.x + obj.w / 2, y: obj.y + obj.h / 2 });
|
||||||
|
else if (obj.type === 'maintenanceRoom') eraseAtPoint(roomCenter(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
function modalButton(text, onClick, className = 'primary-button') {
|
function modalButton(text, onClick, className = 'primary-button') {
|
||||||
|
|
@ -798,6 +863,14 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
ui.modalActions.innerHTML = '';
|
ui.modalActions.innerHTML = '';
|
||||||
if (obj.type === 'scanner' && obj.kind === 'auto') ui.modalActions.appendChild(modalButton('Auto Menu', () => showAutoScannerMenu(obj), 'facility-action warn'));
|
if (obj.type === 'scanner' && obj.kind === 'auto') ui.modalActions.appendChild(modalButton('Auto Menu', () => showAutoScannerMenu(obj), 'facility-action warn'));
|
||||||
if (obj.type === 'scanner' && obj.kind === 'manual') ui.modalActions.appendChild(modalButton('Set Keys', () => showManualScannerMenu(obj), 'facility-action warn'));
|
if (obj.type === 'scanner' && obj.kind === 'manual') ui.modalActions.appendChild(modalButton('Set Keys', () => showManualScannerMenu(obj), 'facility-action warn'));
|
||||||
|
if (obj.type === 'conveyor' && isBranchKey(obj.id)) {
|
||||||
|
const mode = game.conveyorMeta.get(obj.id)?.branchMode || 'random';
|
||||||
|
ui.modalActions.appendChild(modalButton(`Branch: ${branchModeLabel(mode)}`, () => {
|
||||||
|
cycleBranchModeForKey(obj.id);
|
||||||
|
game.selected = { type: 'conveyor', id: obj.id };
|
||||||
|
showSelectedMenu();
|
||||||
|
}, 'facility-action warn branch-action'));
|
||||||
|
}
|
||||||
const hit = obj.type === 'conveyor' ? { type: 'conveyor', oldKey: obj.id, ref: obj } : { type: obj.type, ref: obj };
|
const hit = obj.type === 'conveyor' ? { type: 'conveyor', oldKey: obj.id, ref: obj } : { type: obj.type, ref: obj };
|
||||||
const saleReason = lastProtectedSaleReason(hit);
|
const saleReason = lastProtectedSaleReason(hit);
|
||||||
if (saleReason) {
|
if (saleReason) {
|
||||||
|
|
@ -829,6 +902,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
boxes.push({ id: `facility:${id}`, price: equipmentPrice({ type: 'facility', ref: f }), x: f.x, y: f.y, w: f.w, h: f.h });
|
boxes.push({ id: `facility:${id}`, price: equipmentPrice({ type: 'facility', ref: f }), x: f.x, y: f.y, w: f.w, h: f.h });
|
||||||
if (f.entry) { const c = cellCenter(f.entry.col, f.entry.row); boxes.push({ id: `facilityPort:${id}`, price: equipmentPrice({ type: 'facility', ref: f }), x: c.x - 18, y: c.y - 18, w: 36, h: 36 }); }
|
if (f.entry) { const c = cellCenter(f.entry.col, f.entry.row); boxes.push({ id: `facilityPort:${id}`, price: equipmentPrice({ type: 'facility', ref: f }), x: c.x - 18, y: c.y - 18, w: 36, h: 36 }); }
|
||||||
}
|
}
|
||||||
|
for (const room of game.maintenanceRooms || []) { const c = roomCenter(room); boxes.push({ id: `maintenanceRoom:${room.id}`, price: equipmentPrice({ type: 'maintenanceRoom', ref: room }), x: c.x - GRID.cell, y: c.y - GRID.cell, w: GRID.cell * 2, h: GRID.cell * 2 }); }
|
||||||
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 - 50, y: c.y - 73, w: 100, h: 146 }); }
|
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 - 50, y: c.y - 73, w: 100, h: 146 }); }
|
||||||
for (const k of game.conveyorTiles) {
|
for (const k of game.conveyorTiles) {
|
||||||
|
|
@ -869,19 +943,19 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
function cycleBranchModeAtPoint(p) {
|
function cycleBranchModeAtPoint(p) {
|
||||||
const k = branchSwitcherAtPoint(p);
|
const k = branchSwitcherAtPoint(p);
|
||||||
if (!k) return false;
|
if (!k) return false;
|
||||||
cycleBranchModeForKey(k);
|
return cycleBranchModeForKey(k);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cycleBranchModeForKey(k) {
|
function cycleBranchModeForKey(k) {
|
||||||
const meta = game.conveyorMeta.get(k);
|
const meta = game.conveyorMeta.get(k);
|
||||||
if (!meta) return false;
|
if (!meta) return false;
|
||||||
const reachable = branchSelectableEntranceDirsForKey(k);
|
const reachable = branchSelectableExitDirsForKey(k);
|
||||||
if (reachable.length < 2) return false;
|
if (reachable.length < 2) return false;
|
||||||
record(game);
|
record(game);
|
||||||
const current = BRANCH_MODES.includes(meta.branchMode) ? meta.branchMode : 'random';
|
const current = BRANCH_MODES.includes(meta.branchMode) ? meta.branchMode : 'random';
|
||||||
const modes = ['random', ...DIRS.map(d => d.name).filter(mode => reachable.includes(mode))];
|
const modes = ['random', ...DIRS.map(d => d.name).filter(mode => reachable.includes(mode))];
|
||||||
meta.branchMode = modes[(Math.max(0, modes.indexOf(current)) + 1) % modes.length];
|
meta.branchMode = modes[(Math.max(0, modes.indexOf(current)) + 1) % modes.length];
|
||||||
|
refreshRoutingAfterEdit(game);
|
||||||
game.selected = null;
|
game.selected = null;
|
||||||
game.multiSelected = [];
|
game.multiSelected = [];
|
||||||
updatePanels();
|
updatePanels();
|
||||||
|
|
@ -920,11 +994,16 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
lines.push(`Durability: ${remainingPercent({ meta })}% | Speed x${performanceFactor({ meta }).toFixed(2)}`);
|
lines.push(`Durability: ${remainingPercent({ meta })}% | Speed x${performanceFactor({ meta }).toFixed(2)}`);
|
||||||
if (isBranchKey(obj.id)) {
|
if (isBranchKey(obj.id)) {
|
||||||
lines.push(`Branch mode: ${branchModeLabel(meta?.branchMode || 'random')}`);
|
lines.push(`Branch mode: ${branchModeLabel(meta?.branchMode || 'random')}`);
|
||||||
lines.push('Use the Branch button below or the on-belt branch badge.');
|
lines.push('Click the Branch button or the large on-belt badge to cycle RANDOM / fixed exit.');
|
||||||
} else {
|
} else {
|
||||||
lines.push('Branch mode: needs 3+ conveyor connections.');
|
lines.push('Branch mode: needs 3+ conveyor connections.');
|
||||||
}
|
}
|
||||||
lines.push(`Sale value: ${yen(resaleValueFor({ type: 'conveyor', oldKey: obj.id, ref: obj }, game).amount)}`);
|
lines.push(`Sale value: ${yen(resaleValueFor({ type: 'conveyor', oldKey: obj.id, ref: obj }, game).amount)}`);
|
||||||
|
} else if (obj.type === 'maintenanceRoom') {
|
||||||
|
lines.push(`Price: ${yen(equipmentPrice({ type: 'maintenanceRoom', ref: obj }))}`);
|
||||||
|
lines.push(`Sale value: ${yen(resaleValueFor({ type: 'maintenanceRoom', ref: obj }, game).amount)}`);
|
||||||
|
lines.push('Size: 2×2 grid cells');
|
||||||
|
lines.push('Dispatch: 1 repairman during each production day');
|
||||||
} else if (obj.type === 'facility') {
|
} else if (obj.type === 'facility') {
|
||||||
lines.push(`Price: ${yen(equipmentPrice(obj))}`);
|
lines.push(`Price: ${yen(equipmentPrice(obj))}`);
|
||||||
{ const reason = lastProtectedSaleReason({ type: obj.type, ref: obj }); lines.push(reason ? `Sale: blocked (${reason})` : `Sale value: ${yen(resaleValueFor({ type: obj.type, ref: obj }, game).amount)}`); }
|
{ const reason = lastProtectedSaleReason({ type: obj.type, ref: obj }); lines.push(reason ? `Sale: blocked (${reason})` : `Sale value: ${yen(resaleValueFor({ type: obj.type, ref: obj }, game).amount)}`); }
|
||||||
|
|
@ -969,6 +1048,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
||||||
if (obj.type === 'facility' && (obj.baseId || obj.id) === 'mixer') return 'Male chicks become revenue here. Do not feed it poop.';
|
if (obj.type === 'facility' && (obj.baseId || obj.id) === 'mixer') return 'Male chicks become revenue here. Do not feed it poop.';
|
||||||
if (obj.type === 'facility' && (obj.baseId || obj.id) === 'trash') return 'Poop goes in. Sometimes coins come out, for reasons best left unaudited.';
|
if (obj.type === 'facility' && (obj.baseId || obj.id) === 'trash') return 'Poop goes in. Sometimes coins come out, for reasons best left unaudited.';
|
||||||
if (obj.type === 'facility' && (obj.baseId || obj.id) === 'truck') return 'The shipping endpoint. Correct cargo pays; wrong cargo complains.';
|
if (obj.type === 'facility' && (obj.baseId || obj.id) === 'truck') return 'The shipping endpoint. Correct cargo pays; wrong cargo complains.';
|
||||||
|
if (obj.type === 'maintenanceRoom') return 'A cramped 2×2 storage and break room with lockers, spare parts, a bench and one dispatched repairman.';
|
||||||
return 'Factory equipment.';
|
return 'Factory equipment.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -213,8 +213,7 @@ function cardAtCap(game, card) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function repairmanCardVisible(game) {
|
function repairmanCardVisible(game) {
|
||||||
const r = game.repairman || {};
|
return !!((game.maintenanceRooms || []).length || (game.repairmen || []).length || game.repairman?.active);
|
||||||
return !!(r.active || r.hiredForNextDay || game.pendingRepairWorkerWages > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function availableCards(game) {
|
function availableCards(game) {
|
||||||
|
|
@ -506,8 +505,9 @@ function cardDescription(game, card) {
|
||||||
if (card.id === 'flattery') return `Next Fairies tribute -50%. Held for next tribute: ${e.fairiesFlatteryNext || 0}.`;
|
if (card.id === 'flattery') return `Next Fairies tribute -50%. Held for next tribute: ${e.fairiesFlatteryNext || 0}.`;
|
||||||
if (card.id === 'usedMachine') return 'Enable Used Machines: future equipment costs 50%, has no SELL refund, and starts with 60% durability.';
|
if (card.id === 'usedMachine') return 'Enable Used Machines: future equipment costs 50%, has no SELL refund, and starts with 60% durability.';
|
||||||
if (card.id === 'newMachine') return 'Cancel Used Machines. Future equipment returns to normal price, refund, and 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 === 'laborExploitation') return 'Repairman dispatch paperwork is streamlined. Repair rooms keep operating; 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 === 'rescueLoan') return `At day-end bankruptcy check, gain ${yen(500)} if cash is below ${yen(0)}. Held: ${e.rescueLoanCharges || 0}.`;
|
||||||
|
if (card.id === 'dynamite') return 'Choose any blocked tiles on the map and destroy up to 3. Empty-space clicks are ignored; press Esc to finish after at least one blast.';
|
||||||
return card.description;
|
return card.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -610,7 +610,8 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
||||||
cardId: card.id,
|
cardId: card.id,
|
||||||
mode: card.target === 'blockedCell' ? 'blockedCell' : 'equipment',
|
mode: card.target === 'blockedCell' ? 'blockedCell' : 'equipment',
|
||||||
targetKeys: targets.map(targetKey),
|
targetKeys: targets.map(targetKey),
|
||||||
remaining: card.id === 'dynamite' ? Math.min(3, targets.length) : 1
|
remaining: card.id === 'dynamite' ? Math.min(3, targets.length) : 1,
|
||||||
|
used: 0
|
||||||
};
|
};
|
||||||
ui.modal.classList.remove('visible', 'equipment-popover');
|
ui.modal.classList.remove('visible', 'equipment-popover');
|
||||||
updatePanels();
|
updatePanels();
|
||||||
|
|
@ -630,6 +631,7 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
||||||
floating(game, c.x, c.y - 18, 'BOOM', THEME.danger);
|
floating(game, c.x, c.y - 18, 'BOOM', THEME.danger);
|
||||||
refreshRoutingAfterEdit(game);
|
refreshRoutingAfterEdit(game);
|
||||||
game.cardTargetPick.remaining = Math.max(0, (game.cardTargetPick.remaining || 1) - 1);
|
game.cardTargetPick.remaining = Math.max(0, (game.cardTargetPick.remaining || 1) - 1);
|
||||||
|
game.cardTargetPick.used = Math.max(0, game.cardTargetPick.used || 0) + 1;
|
||||||
game.cardTargetPick.targetKeys = targetsForCard(game, 'dynamite').map(targetKey);
|
game.cardTargetPick.targetKeys = targetsForCard(game, 'dynamite').map(targetKey);
|
||||||
updatePanels();
|
updatePanels();
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -639,15 +641,21 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
||||||
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) {
|
||||||
cancelTargetPick();
|
cancelTargetPick();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (card.id === 'dynamite') {
|
if (card.id === 'dynamite') {
|
||||||
|
// Empty-space clicks do nothing: the player may freely choose any highlighted tile.
|
||||||
|
if (!target) return true;
|
||||||
if (!applyDynamiteTarget(target)) return true;
|
if (!applyDynamiteTarget(target)) return true;
|
||||||
if ((game.cardTargetPick.remaining || 0) <= 0 || !targetsForCard(game, 'dynamite').length) completePickedCard(card);
|
if ((game.cardTargetPick.remaining || 0) <= 0 || !targetsForCard(game, 'dynamite').length) completePickedCard(card);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!target) {
|
||||||
|
cancelTargetPick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
applyEquipmentUpgrade(game, target);
|
applyEquipmentUpgrade(game, target);
|
||||||
completePickedCard(card);
|
completePickedCard(card);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -655,6 +663,15 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
||||||
|
|
||||||
function cancelTargetPick() {
|
function cancelTargetPick() {
|
||||||
if (!game.cardTargetPick?.pending) return;
|
if (!game.cardTargetPick?.pending) return;
|
||||||
|
const activePick = game.cardTargetPick;
|
||||||
|
const card = cardById(activePick.cardId);
|
||||||
|
// Once a dynamite charge has been used, Escape finishes the card instead of
|
||||||
|
// returning a partially consumed card to the draft. Before the first blast,
|
||||||
|
// Escape still cancels normally.
|
||||||
|
if (card?.id === 'dynamite' && (activePick.used || 0) > 0) {
|
||||||
|
completePickedCard(card);
|
||||||
|
return;
|
||||||
|
}
|
||||||
game.cardTargetPick = null;
|
game.cardTargetPick = null;
|
||||||
showDraft();
|
showDraft();
|
||||||
}
|
}
|
||||||
|
|
@ -678,6 +695,36 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
||||||
showDraft();
|
showDraft();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function cardIconFor(card) {
|
||||||
|
const tags = new Set(card.tags || []);
|
||||||
|
const byTarget = {
|
||||||
|
eggFarm: { type: 'image', src: './assets/dark_factory/facilities/machines/egg_farm_16.png', alt: 'EGG' },
|
||||||
|
eggOutlet: { type: 'image', src: './assets/dark_factory/facilities/machines/egg_farm_16.png', alt: 'EGG' },
|
||||||
|
autoScanner: { type: 'image', src: './assets/dark_factory/facilities/machines/auto_scanner_32x48.png', alt: 'SCAN' },
|
||||||
|
mixer: { type: 'image', src: './assets/dark_factory/facilities/machines/mixer_compact_16.png', alt: 'MIX' },
|
||||||
|
truck: { type: 'image', src: './assets/dark_factory/facilities/vehicles/truck_compact_16.png', alt: 'TRUCK' },
|
||||||
|
trash: { type: 'image', src: './assets/dark_factory/facilities/machines/shredder_compact_16.png', alt: 'SHR' },
|
||||||
|
blockedCell: { type: 'image', src: './assets/dark_factory/facilities/utility/obstacle_crate_16.png', alt: 'BLOCK' }
|
||||||
|
};
|
||||||
|
if (byTarget[card.target]) return byTarget[card.target];
|
||||||
|
if (card.id === 'bearing' || tags.has('CONVEYOR')) return { type: 'image', src: './assets/dark_factory/facilities/conveyor/conveyor_straight_h_16.png', alt: 'BELT' };
|
||||||
|
if (tags.has('SCANNER')) return { type: 'image', src: './assets/dark_factory/facilities/machines/auto_scanner_32x48.png', alt: 'SCAN' };
|
||||||
|
if (tags.has('EGG')) return { type: 'image', src: './assets/dark_factory/facilities/machines/egg_farm_16.png', alt: 'EGG' };
|
||||||
|
if (tags.has('POOP')) return { type: 'glyph', text: '💩' };
|
||||||
|
if (tags.has('MAINTENANCE')) return { type: 'image', src: './assets/dark_factory/facilities/utility/repairman_16.png', alt: 'FIX' };
|
||||||
|
if (tags.has('CARD')) return { type: 'glyph', text: '▣' };
|
||||||
|
if (tags.has('ECONOMY')) return { type: 'glyph', text: '¥' };
|
||||||
|
if (tags.has('RISK')) return { type: 'glyph', text: '!' };
|
||||||
|
return { type: 'glyph', text: '◆' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cardIconHtml(card) {
|
||||||
|
const icon = cardIconFor(card);
|
||||||
|
if (icon.type === 'image') return `<div class="card-icon image"><img src="${icon.src}" alt="${icon.alt || ''}" /></div>`;
|
||||||
|
return `<div class="card-icon glyph" aria-hidden="true">${icon.text || '◆'}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function cardButton(card, index) {
|
function cardButton(card, index) {
|
||||||
const b = document.createElement('button');
|
const b = document.createElement('button');
|
||||||
b.type = 'button';
|
b.type = 'button';
|
||||||
|
|
@ -685,7 +732,7 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
||||||
b.className = `card-choice ${kind}`;
|
b.className = `card-choice ${kind}`;
|
||||||
const rarity = card.type === 'dud' ? 'MISS' : (card.rarity === 'ultraRare' ? 'ULTRA RARE' : (card.rarity === 'rare' ? 'RARE' : 'COMMON'));
|
const rarity = card.type === 'dud' ? 'MISS' : (card.rarity === 'ultraRare' ? 'ULTRA RARE' : (card.rarity === 'rare' ? 'RARE' : 'COMMON'));
|
||||||
const tags = [rarity, ...(card.tags || []).filter(tag => tag !== rarity)].slice(0, 5);
|
const tags = [rarity, ...(card.tags || []).filter(tag => tag !== rarity)].slice(0, 5);
|
||||||
b.innerHTML = `<strong>${card.title}</strong><div class="card-tags">${tags.map(tag => `<span>${tag}</span>`).join('')}</div><small>${cardDescription(game, card)}</small>`;
|
b.innerHTML = `${cardIconHtml(card)}<div class="card-copy"><strong>${card.title}</strong><div class="card-tags">${tags.map(tag => `<span>${tag}</span>`).join('')}</div><small>${cardDescription(game, card)}</small></div>`;
|
||||||
b.addEventListener('click', () => chooseCard(card, b));
|
b.addEventListener('click', () => chooseCard(card, b));
|
||||||
b.dataset.index = String(index);
|
b.dataset.index = String(index);
|
||||||
return b;
|
return b;
|
||||||
|
|
|
||||||
|
|
@ -249,15 +249,13 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
||||||
const connected = conveyorNeighborDirNames(cell);
|
const connected = conveyorNeighborDirNames(cell);
|
||||||
if (connected.length < 3) return dirs;
|
if (connected.length < 3) return dirs;
|
||||||
const entrances = new Set(connected.filter(dir => neighborPointsIntoCell(cell, dir)));
|
const entrances = new Set(connected.filter(dir => neighborPointsIntoCell(cell, dir)));
|
||||||
if (branchMode && branchMode !== 'random' && branchMode !== 'pass' && connected.includes(branchMode)) {
|
const physicalExits = connected.filter(dir => !entrances.has(dir));
|
||||||
const selectedEntrances = new Set(entrances);
|
let candidates = entrances.size && physicalExits.length >= 2
|
||||||
selectedEntrances.add(branchMode);
|
? physicalExits
|
||||||
if (connected.filter(dir => !selectedEntrances.has(dir)).length >= 2) entrances.add(branchMode);
|
: [...new Set(dirs.filter(dir => connected.includes(dir)))];
|
||||||
}
|
if (candidates.length < 2) candidates = connected;
|
||||||
const exits = connected.filter(dir => !entrances.has(dir));
|
if (branchMode && branchMode !== 'random' && branchMode !== 'pass' && candidates.includes(branchMode)) return [branchMode];
|
||||||
if (exits.length) return exits;
|
return candidates;
|
||||||
const fallback = dirs.filter(dir => !entrances.has(dir));
|
|
||||||
return fallback.length ? fallback : dirs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function optionAvailableForBranch(chick, opt) {
|
function optionAvailableForBranch(chick, opt) {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export function equipmentBasePrice(objOrHit) {
|
||||||
if (type === 'eggFarm') return obj.price || FACILITY_DEFS.eggFarm.price;
|
if (type === 'eggFarm') return obj.price || FACILITY_DEFS.eggFarm.price;
|
||||||
if (type === 'scanner') return obj.price || (obj.kind === 'auto' ? FACILITY_DEFS.autoScanner.price : FACILITY_DEFS.manualScanner.price);
|
if (type === 'scanner') return obj.price || (obj.kind === 'auto' ? FACILITY_DEFS.autoScanner.price : FACILITY_DEFS.manualScanner.price);
|
||||||
if (type === 'facility') return obj.price || FACILITY_DEFS[obj.id]?.price || 0;
|
if (type === 'facility') return obj.price || FACILITY_DEFS[obj.id]?.price || 0;
|
||||||
|
if (type === 'maintenanceRoom') return obj.price || FACILITY_DEFS.maintenanceRoom.price;
|
||||||
return FACILITY_DEFS[obj.id]?.price || 0;
|
return FACILITY_DEFS[obj.id]?.price || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { BALANCE } from '../core/balance.js';
|
import { BALANCE } from '../core/balance.js';
|
||||||
import { GRID, THEME } from '../core/config.js';
|
import { GRID, THEME } from '../core/config.js';
|
||||||
import { cellCenter, parseKey, key, yen } from '../core/utils.js';
|
import { cellCenter, parseKey, key } from '../core/utils.js';
|
||||||
import { spendCash } from './economy.js';
|
|
||||||
import { floating } from './effects.js';
|
import { floating } from './effects.js';
|
||||||
|
|
||||||
const RULES = BALANCE.maintenance;
|
const RULES = BALANCE.maintenance;
|
||||||
|
|
@ -25,19 +24,25 @@ export function durabilityCapFor(game, type, qualityMultiplier = 1) {
|
||||||
return Math.max(1, Math.ceil(base * Math.max(0.1, qualityMultiplier || 1) * durabilityMultiplier(game)));
|
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) {
|
function qualityMultiplierOf(objOrMeta) {
|
||||||
return Math.max(0.1, Number(objOrMeta?.durabilityBaseMultiplier) || 1);
|
return Math.max(0.1, Number(objOrMeta?.durabilityBaseMultiplier) || 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maintenanceRoomCenter(room, index = 0) {
|
||||||
|
if (!room) return { x: GRID.x + GRID.cell * (0.5 + index * 0.55), y: GRID.y + GRID.rows * GRID.cell + 84 };
|
||||||
|
const x = GRID.x + room.col * GRID.cell + GRID.cell;
|
||||||
|
const y = GRID.y + room.row * GRID.cell + GRID.cell;
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
function repairmanHome(game, index = 0) {
|
||||||
|
const room = (game.maintenanceRooms || [])[index];
|
||||||
|
return maintenanceRoomCenter(room, index);
|
||||||
|
}
|
||||||
|
|
||||||
export function ensureMaintenanceState(game) {
|
export function ensureMaintenanceState(game) {
|
||||||
if (!game.repairman) {
|
if (!game.repairman) {
|
||||||
game.repairman = {
|
game.repairman = {
|
||||||
hiredForNextDay: false,
|
|
||||||
active: false,
|
active: false,
|
||||||
x: GRID.x + GRID.cell * 0.5,
|
x: GRID.x + GRID.cell * 0.5,
|
||||||
y: GRID.y + GRID.rows * GRID.cell + 84,
|
y: GRID.y + GRID.rows * GRID.cell + 84,
|
||||||
|
|
@ -45,6 +50,8 @@ export function ensureMaintenanceState(game) {
|
||||||
repairedToday: 0
|
repairedToday: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (!Array.isArray(game.maintenanceRooms)) game.maintenanceRooms = [];
|
||||||
|
if (!Array.isArray(game.repairmen)) game.repairmen = [];
|
||||||
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.maintenanceType == null) meta.maintenanceType = 'conveyor';
|
if (meta.maintenanceType == null) meta.maintenanceType = 'conveyor';
|
||||||
|
|
@ -240,28 +247,32 @@ export function repairAllEquipment(game, percent = 0.02) {
|
||||||
return repaired;
|
return repaired;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hireRepairmanForNextDay(game) {
|
|
||||||
ensureMaintenanceState(game);
|
|
||||||
if (game.repairman.hiredForNextDay) return { ok: false, reason: 'Repairman already hired.' };
|
|
||||||
const cost = repairmanDailyCost(game);
|
|
||||||
if (game.cash < cost) return { ok: false, reason: `Need ${yen(cost - game.cash)} more.` };
|
|
||||||
spendCash(game, cost);
|
|
||||||
game.pendingRepairWorkerWages = (game.pendingRepairWorkerWages || 0) + cost;
|
|
||||||
game.totals.repairWorkerWages = (game.totals.repairWorkerWages || 0) + cost;
|
|
||||||
game.repairman.hiredForNextDay = true;
|
|
||||||
return { ok: true, cost };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function activateRepairmanForDay(game) {
|
export function activateRepairmanForDay(game) {
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
game.repairman.active = !!game.repairman.hiredForNextDay;
|
const rooms = game.maintenanceRooms || [];
|
||||||
game.repairman.hiredForNextDay = false;
|
game.repairmen = rooms.map((room, index) => {
|
||||||
game.repairman.target = null;
|
const home = repairmanHome(game, index);
|
||||||
|
return {
|
||||||
|
id: `room-${room.id}`,
|
||||||
|
roomId: room.id,
|
||||||
|
active: true,
|
||||||
|
x: home.x,
|
||||||
|
y: home.y,
|
||||||
|
target: null,
|
||||||
|
repairedToday: 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const first = game.repairmen[0];
|
||||||
|
game.repairman.active = !!first;
|
||||||
|
game.repairman.target = first?.target || null;
|
||||||
game.repairman.repairedToday = 0;
|
game.repairman.repairedToday = 0;
|
||||||
|
if (first) { game.repairman.x = first.x; game.repairman.y = first.y; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivateRepairman(game) {
|
export function deactivateRepairman(game) {
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
|
for (const r of game.repairmen || []) { r.active = false; r.target = null; }
|
||||||
|
game.repairmen = [];
|
||||||
game.repairman.active = false;
|
game.repairman.active = false;
|
||||||
game.repairman.target = null;
|
game.repairman.target = null;
|
||||||
}
|
}
|
||||||
|
|
@ -284,14 +295,34 @@ function chooseRepairTarget(game) {
|
||||||
|
|
||||||
export function updateRepairman(game, dt) {
|
export function updateRepairman(game, dt) {
|
||||||
ensureMaintenanceState(game);
|
ensureMaintenanceState(game);
|
||||||
const r = game.repairman;
|
const repairmen = game.repairmen?.length ? game.repairmen : (game.repairman?.active ? [game.repairman] : []);
|
||||||
if (!r.active || game.phase !== 'running') return;
|
if (!repairmen.length || game.phase !== 'running') return;
|
||||||
let target = resolveStoredTarget(game, r.target) || chooseRepairTarget(game);
|
const assigned = new Set(repairmen.map(r => r.target?.key).filter(Boolean));
|
||||||
|
for (const r of repairmen) updateSingleRepairman(game, r, dt, assigned);
|
||||||
|
const first = repairmen[0];
|
||||||
|
if (first && game.repairman !== first) {
|
||||||
|
game.repairman.active = true;
|
||||||
|
game.repairman.x = first.x;
|
||||||
|
game.repairman.y = first.y;
|
||||||
|
game.repairman.target = first.target;
|
||||||
|
game.repairman.repairedToday = first.repairedToday || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSingleRepairman(game, r, dt, assigned) {
|
||||||
|
if (!r.active) return;
|
||||||
|
let target = resolveStoredTarget(game, r.target);
|
||||||
|
if (!target || targetWear(target) <= 0.001) {
|
||||||
|
if (r.target?.key) assigned.delete(r.target.key);
|
||||||
|
target = chooseRepairTargetAvoiding(game, assigned);
|
||||||
|
}
|
||||||
if (!target) {
|
if (!target) {
|
||||||
r.target = null;
|
r.target = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
r.target = { key: targetKey(target), label: target.label };
|
const tKey = targetKey(target);
|
||||||
|
assigned.add(tKey);
|
||||||
|
r.target = { key: tKey, label: target.label };
|
||||||
const speed = RULES.repairman.walkSpeed;
|
const speed = RULES.repairman.walkSpeed;
|
||||||
const dx = target.center.x - r.x;
|
const dx = target.center.x - r.x;
|
||||||
const dy = target.center.y - r.y;
|
const dy = target.center.y - r.y;
|
||||||
|
|
@ -302,16 +333,32 @@ export function updateRepairman(game, dt) {
|
||||||
r.y += dy / dist * step;
|
r.y += dy / dist * step;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const repairWear = dt / (RULES.repairman.secondsPerOnePercent * 100);
|
const workTimeMultiplier = game?.cardEffects?.laborExploitation ? 0.7 : 1;
|
||||||
|
const repairWear = dt / (RULES.repairman.secondsPerOnePercent * workTimeMultiplier * 100);
|
||||||
const usesBefore = targetUses(target);
|
const usesBefore = targetUses(target);
|
||||||
const usesDelta = repairWear * targetDurability(target);
|
const usesDelta = repairWear * targetDurability(target);
|
||||||
reduceTargetUses(target, usesDelta);
|
reduceTargetUses(target, usesDelta);
|
||||||
const repairedPct = Math.max(0, (usesBefore - targetUses(target)) / targetDurability(target) * 100);
|
const repairedPct = Math.max(0, (usesBefore - targetUses(target)) / targetDurability(target) * 100);
|
||||||
r.repairedToday = (r.repairedToday || 0) + repairedPct;
|
r.repairedToday = (r.repairedToday || 0) + repairedPct;
|
||||||
if (repairedPct > 0 && Math.random() < 0.02) floating(game, r.x, r.y - 28, 'REPAIR', THEME.green);
|
if (repairedPct > 0 && Math.random() < 0.02) floating(game, r.x, r.y - 28, 'REPAIR', THEME.green);
|
||||||
if (targetWear(target) <= 0.001) r.target = null;
|
if (targetWear(target) <= 0.001) {
|
||||||
|
assigned.delete(tKey);
|
||||||
|
r.target = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chooseRepairTargetAvoiding(game, assigned = new Set()) {
|
||||||
|
let best = null;
|
||||||
|
for (const t of equipmentMaintenanceTargets(game)) {
|
||||||
|
if (assigned.has(targetKey(t))) continue;
|
||||||
|
const w = targetWear(t);
|
||||||
|
if (w <= 0.001) continue;
|
||||||
|
if (!best || w > targetWear(best)) best = t;
|
||||||
|
}
|
||||||
|
return best || chooseRepairTarget(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function worstDegradation(game) {
|
function worstDegradation(game) {
|
||||||
let worst = 0;
|
let worst = 0;
|
||||||
let label = 'none';
|
let label = 'none';
|
||||||
|
|
@ -325,5 +372,5 @@ function worstDegradation(game) {
|
||||||
export function maintenanceSummary(game) {
|
export function maintenanceSummary(game) {
|
||||||
const worst = worstDegradation(game);
|
const worst = worstDegradation(game);
|
||||||
const avgBeltPerformance = averageConveyorPerformance(game);
|
const avgBeltPerformance = averageConveyorPerformance(game);
|
||||||
return { worst, avgBeltPerformance, repairman: game.repairman || null };
|
return { worst, avgBeltPerformance, repairman: game.repairman || null, repairmen: game.repairmen || [], rooms: game.maintenanceRooms || [] };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,19 +138,14 @@ function inferredConveyorOutDirNames(game, cellKey) {
|
||||||
.filter(d => game.conveyorTiles.has(graphNodeKey(p.col + d.dc, p.row + d.dr)))
|
.filter(d => game.conveyorTiles.has(graphNodeKey(p.col + d.dc, p.row + d.dr)))
|
||||||
.map(d => d.name);
|
.map(d => d.name);
|
||||||
const explicit = conveyorOutDirNames(game, cellKey).filter(dir => neighbors.includes(dir));
|
const explicit = conveyorOutDirNames(game, cellKey).filter(dir => neighbors.includes(dir));
|
||||||
const inferred = neighbors;
|
const dirs = explicit.length ? explicit : neighbors;
|
||||||
const dirs = explicit.length ? explicit : inferred;
|
|
||||||
if (neighbors.length < 3) return dirs;
|
if (neighbors.length < 3) return dirs;
|
||||||
const entrances = new Set(neighbors.filter(dir => neighborPointsIntoCell(game, p, dir)));
|
const entrances = new Set(neighbors.filter(dir => neighborPointsIntoCell(game, p, dir)));
|
||||||
if (meta.branchMode && meta.branchMode !== 'random' && neighbors.includes(meta.branchMode)) {
|
const physicalExits = neighbors.filter(dir => !entrances.has(dir));
|
||||||
const selectedEntrances = new Set(entrances);
|
let candidates = entrances.size && physicalExits.length >= 2 ? physicalExits : [...new Set(dirs)];
|
||||||
selectedEntrances.add(meta.branchMode);
|
if (candidates.length < 2) candidates = neighbors;
|
||||||
if (neighbors.filter(dir => !selectedEntrances.has(dir)).length >= 2) entrances.add(meta.branchMode);
|
if (meta.branchMode && meta.branchMode !== 'random' && candidates.includes(meta.branchMode)) return [meta.branchMode];
|
||||||
}
|
return candidates;
|
||||||
const exits = neighbors.filter(dir => !entrances.has(dir));
|
|
||||||
if (exits.length) return exits;
|
|
||||||
const fallback = dirs.filter(dir => !entrances.has(dir));
|
|
||||||
return fallback.length ? fallback : dirs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyFactoryGraph(game) {
|
function emptyFactoryGraph(game) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { GRID } from '../core/config.js';
|
import { GRID } from '../core/config.js';
|
||||||
import { key, parseKey, cellCenter } from '../core/utils.js';
|
import { key, parseKey, cellCenter } from '../core/utils.js';
|
||||||
import { nearestGridEdge, layoutFacilityOnEdge } from '../core/entities.js';
|
import { nearestGridEdge, layoutFacilityOnEdge, maintenanceRoomFootprint } from '../core/entities.js';
|
||||||
import { farmAt, scannerAt, scannerCenter, scannerFootprintCells, refreshRoutingAfterEdit } from './routing.js';
|
import { farmAt, scannerAt, scannerCenter, scannerFootprintCells, refreshRoutingAfterEdit } from './routing.js';
|
||||||
import { snapshot } from './history.js';
|
import { snapshot } from './history.js';
|
||||||
import { buildPrice } from './economy.js';
|
import { buildPrice } from './economy.js';
|
||||||
|
|
@ -10,6 +10,7 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
||||||
if (!item) return '';
|
if (!item) return '';
|
||||||
if (item.type === 'conveyor') return `conveyor:${item.id || item.oldKey}`;
|
if (item.type === 'conveyor') return `conveyor:${item.id || item.oldKey}`;
|
||||||
if (item.type === 'facility') return `facility:${item.id || item.ref?.id}`;
|
if (item.type === 'facility') return `facility:${item.id || item.ref?.id}`;
|
||||||
|
if (item.type === 'maintenanceRoom') return `maintenanceRoom:${item.id || item.ref?.id}`;
|
||||||
return `${item.type}:${item.id || item.ref?.id}`;
|
return `${item.type}:${item.id || item.ref?.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,6 +30,7 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
||||||
if (sel.type === 'eggFarm') return game.eggFarms.find(f => f.id === sel.id) || null;
|
if (sel.type === 'eggFarm') return game.eggFarms.find(f => f.id === sel.id) || null;
|
||||||
if (sel.type === 'scanner') return game.scanners.find(sc => sc.id === sel.id) || null;
|
if (sel.type === 'scanner') return game.scanners.find(sc => sc.id === sel.id) || null;
|
||||||
if (sel.type === 'facility') return game.facilities[sel.id] || null;
|
if (sel.type === 'facility') return game.facilities[sel.id] || null;
|
||||||
|
if (sel.type === 'maintenanceRoom') return (game.maintenanceRooms || []).find(r => r.id === sel.id) || null;
|
||||||
if (sel.type === 'conveyor') return game.conveyorTiles.has(sel.id) ? { type: 'conveyor', id: sel.id } : null;
|
if (sel.type === 'conveyor') return game.conveyorTiles.has(sel.id) ? { type: 'conveyor', id: sel.id } : null;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +44,10 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
||||||
}
|
}
|
||||||
for (const farm of game.eggFarms) items.push({ type: 'eggFarm', id: farm.id, ref: farm, center: cellCenter(farm.col, farm.row), cell: { col: farm.col, row: farm.row } });
|
for (const farm of game.eggFarms) items.push({ type: 'eggFarm', id: farm.id, ref: farm, center: cellCenter(farm.col, farm.row), cell: { col: farm.col, row: farm.row } });
|
||||||
for (const scanner of game.scanners) items.push({ type: 'scanner', id: scanner.id, ref: scanner, center: scannerCenter(scanner), cell: { col: scanner.col, row: scanner.row } });
|
for (const scanner of game.scanners) items.push({ type: 'scanner', id: scanner.id, ref: scanner, center: scannerCenter(scanner), cell: { col: scanner.col, row: scanner.row } });
|
||||||
|
for (const room of game.maintenanceRooms || []) {
|
||||||
|
const c = cellCenter(room.col, room.row);
|
||||||
|
items.push({ type: 'maintenanceRoom', id: room.id, ref: room, center: { x: c.x + GRID.cell / 2, y: c.y + GRID.cell / 2 }, cell: { col: room.col, row: room.row } });
|
||||||
|
}
|
||||||
for (const f of Object.values(game.facilities)) {
|
for (const f of Object.values(game.facilities)) {
|
||||||
const bodyCenter = { x: f.x + f.w / 2, y: f.y + f.h / 2 };
|
const bodyCenter = { x: f.x + f.w / 2, y: f.y + f.h / 2 };
|
||||||
const portCenter = f.entry ? cellCenter(f.entry.col, f.entry.row) : bodyCenter;
|
const portCenter = f.entry ? cellCenter(f.entry.col, f.entry.row) : bodyCenter;
|
||||||
|
|
@ -117,7 +123,7 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
||||||
const cell = parseKey(sel.id);
|
const cell = parseKey(sel.id);
|
||||||
return { ...sel, obj, oldKey: sel.id, currentKey: sel.id, meta: game.conveyorMeta.get(sel.id) || { price: buildPrice('conveyor'), builtSession: null }, col: cell.col, row: cell.row };
|
return { ...sel, obj, oldKey: sel.id, currentKey: sel.id, meta: game.conveyorMeta.get(sel.id) || { price: buildPrice('conveyor'), builtSession: null }, col: cell.col, row: cell.row };
|
||||||
}
|
}
|
||||||
if (sel.type === 'eggFarm' || sel.type === 'scanner') return { ...sel, obj, col: obj.col, row: obj.row };
|
if (sel.type === 'eggFarm' || sel.type === 'scanner' || sel.type === 'maintenanceRoom') return { ...sel, obj, col: obj.col, row: obj.row };
|
||||||
if (sel.type === 'facility') return { ...sel, obj, x: obj.x, y: obj.y, w: obj.w, h: obj.h, entry: obj.entry ? { ...obj.entry } : null, side: obj.side, center: { x: obj.x + obj.w / 2, y: obj.y + obj.h / 2 } };
|
if (sel.type === 'facility') return { ...sel, obj, x: obj.x, y: obj.y, w: obj.w, h: obj.h, entry: obj.entry ? { ...obj.entry } : null, side: obj.side, center: { x: obj.x + obj.w / 2, y: obj.y + obj.h / 2 } };
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -149,6 +155,8 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
||||||
if (farm && !selectedTokens.has(`eggFarm:${farm.id}`)) return true;
|
if (farm && !selectedTokens.has(`eggFarm:${farm.id}`)) return true;
|
||||||
const scanner = scannerAt(game, col, row);
|
const scanner = scannerAt(game, col, row);
|
||||||
if (scanner && !selectedTokens.has(`scanner:${scanner.id}`)) return true;
|
if (scanner && !selectedTokens.has(`scanner:${scanner.id}`)) return true;
|
||||||
|
const room = (game.maintenanceRooms || []).find(item => maintenanceRoomFootprint(item).some(cell => cell.col === col && cell.row === row));
|
||||||
|
if (room && !selectedTokens.has(`maintenanceRoom:${room.id}`)) return true;
|
||||||
if (game.conveyorTiles.has(k) && !selectedTokens.has(`conveyor:${k}`)) return true;
|
if (game.conveyorTiles.has(k) && !selectedTokens.has(`conveyor:${k}`)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -192,7 +200,7 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
||||||
for (const origin of game.groupDrag.origins) {
|
for (const origin of game.groupDrag.origins) {
|
||||||
if (origin.type === 'facility') continue;
|
if (origin.type === 'facility') continue;
|
||||||
const col = origin.col + dcol, row = origin.row + drow;
|
const col = origin.col + dcol, row = origin.row + drow;
|
||||||
const cells = origin.type === 'scanner' ? scannerFootprintCells({ col, row }) : [{ col, row }];
|
const cells = origin.type === 'scanner' ? scannerFootprintCells({ col, row }) : (origin.type === 'maintenanceRoom' ? maintenanceRoomFootprint({ col, row }) : [{ col, row }]);
|
||||||
for (const cell of cells) {
|
for (const cell of cells) {
|
||||||
if (!pointInGrid(cell.col, cell.row)) return fail('Selection outside grid');
|
if (!pointInGrid(cell.col, cell.row)) return fail('Selection outside grid');
|
||||||
if (game.blockedCells?.has?.(key(cell.col, cell.row))) return fail('Selection hits blocked ground');
|
if (game.blockedCells?.has?.(key(cell.col, cell.row))) return fail('Selection hits blocked ground');
|
||||||
|
|
@ -224,6 +232,9 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
||||||
} else if (origin.type === 'scanner') {
|
} else if (origin.type === 'scanner') {
|
||||||
origin.obj.col = origin.col + dcol;
|
origin.obj.col = origin.col + dcol;
|
||||||
origin.obj.row = origin.row + drow;
|
origin.obj.row = origin.row + drow;
|
||||||
|
} else if (origin.type === 'maintenanceRoom') {
|
||||||
|
origin.obj.col = origin.col + dcol;
|
||||||
|
origin.obj.row = origin.row + drow;
|
||||||
} else if (origin.type === 'facility') {
|
} else if (origin.type === 'facility') {
|
||||||
const nextCenter = { x: origin.center.x + dx, y: origin.center.y + dy };
|
const nextCenter = { x: origin.center.x + dx, y: origin.center.y + dy };
|
||||||
const { entry, side } = nearestGridEdge(nextCenter);
|
const { entry, side } = nearestGridEdge(nextCenter);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { routeFromFarmToScanner, factoryReady, facilityConnectionIssues } from '
|
||||||
import { buildPrice, fairiesTributeInfo, finalScore, maleTruckPenalty, mixerPoopPenalty, truckPoopPenalty } from './economy.js';
|
import { buildPrice, fairiesTributeInfo, finalScore, maleTruckPenalty, mixerPoopPenalty, truckPoopPenalty } from './economy.js';
|
||||||
import { truckTarget } from './events.js';
|
import { truckTarget } from './events.js';
|
||||||
import { conveyorSpeedForGame } from './cards.js';
|
import { conveyorSpeedForGame } from './cards.js';
|
||||||
import { maintenanceSummary, repairmanDailyCost } from './maintenance.js';
|
import { maintenanceSummary } from './maintenance.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) {
|
||||||
|
|
@ -27,7 +27,6 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
function updatePanels() {
|
function updatePanels() {
|
||||||
updateToolButtons();
|
updateToolButtons();
|
||||||
updateBuildStatus();
|
updateBuildStatus();
|
||||||
updateEventPanel();
|
|
||||||
updateTurnSummary();
|
updateTurnSummary();
|
||||||
updateHistoryButtons();
|
updateHistoryButtons();
|
||||||
}
|
}
|
||||||
|
|
@ -56,15 +55,6 @@ 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) {
|
|
||||||
const hired = !!game.repairman?.hiredForNextDay;
|
|
||||||
const cost = repairmanDailyCost(game);
|
|
||||||
const blocked = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || hired || game.cash < cost;
|
|
||||||
ui.hireRepairmanButton.disabled = blocked;
|
|
||||||
ui.hireRepairmanButton.classList.toggle('active', hired);
|
|
||||||
ui.hireRepairmanButton.classList.toggle('unaffordable', game.cash < cost);
|
|
||||||
ui.hireRepairmanButton.title = hired ? 'Repairman is hired for the next production phase.' : `Hire for ${yen(cost)} for one day. Repairs 2% durability per second.`;
|
|
||||||
}
|
|
||||||
const cashout = noAffordableBuildTools();
|
const cashout = noAffordableBuildTools();
|
||||||
ui.buttons.nextTurn?.classList.toggle('cashout-emphasis', cashout);
|
ui.buttons.nextTurn?.classList.toggle('cashout-emphasis', cashout);
|
||||||
ui.buttons.erase?.classList.toggle('cashout-emphasis', cashout);
|
ui.buttons.erase?.classList.toggle('cashout-emphasis', cashout);
|
||||||
|
|
@ -89,47 +79,14 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
function updateTurnSummary() {
|
function updateTurnSummary() {
|
||||||
const connected = game.eggFarms.filter(f => routeFromFarmToScanner(game, f)?.scannerId).length;
|
const connected = game.eggFarms.filter(f => routeFromFarmToScanner(game, f)?.scannerId).length;
|
||||||
const issues = facilityConnectionIssues(game);
|
const issues = facilityConnectionIssues(game);
|
||||||
const tributeBasisProfit = Math.max(0, Math.floor(game.lastResult?.profit ?? game.stats?.profit ?? 0));
|
|
||||||
const tributeDay = Math.max(1, Number(game.turn) || 1);
|
|
||||||
const tribute = fairiesTributeInfo(game, tributeDay, tributeBasisProfit);
|
|
||||||
ui.turnSummary.innerHTML = [
|
ui.turnSummary.innerHTML = [
|
||||||
`Seed: <b>${escapeHtml(game.runSeed || 'none')}</b> | Grid box: <b>${game.gridRows || 10}x${game.gridCols || 20}</b> | Owned: <b>${game.gridOwnedCells || game.ownedCells?.size || ((game.gridRows || 10) * (game.gridCols || 20))}</b> cells | Blocked: <b>${game.blockedCells?.size || 0}</b> cells`,
|
`Seed: <b>${escapeHtml(game.runSeed || 'none')}</b> | Grid box: <b>${game.gridRows || 10}x${game.gridCols || 20}</b> | Owned: <b>${game.gridOwnedCells || game.ownedCells?.size || ((game.gridRows || 10) * (game.gridCols || 20))}</b> cells | Blocked: <b>${game.blockedCells?.size || 0}</b> cells`,
|
||||||
`Truck target: <b>${truckTarget(game).toUpperCase()}</b> | Male fine: <b>-${yen(maleTruckPenalty(game))}</b>`,
|
`Penalties: <b>wrong shipment -${yen(maleTruckPenalty(game))}</b> | <b>Mixer poop -${yen(mixerPoopPenalty(game))}</b> | <b>Shipment poop -${yen(truckPoopPenalty(game))}</b>`,
|
||||||
`Poop fine: <b>Mixer -${yen(mixerPoopPenalty(game))}</b> / <b>Shipment -${yen(truckPoopPenalty(game))}</b>`,
|
`Maintenance: <b>worst ${maintenanceSummary(game).worst.percent}% dirty (${maintenanceSummary(game).worst.label})</b> | Rooms: <b>${(game.maintenanceRooms || []).length}</b> | Repairmen: <b>${(game.repairmen || []).length}</b>`,
|
||||||
`Fairies tribute on Next Day: <b>-${yen(tribute.amount)}</b> | base ${yen(tribute.base)} + profit share ${yen(tribute.profitTribute)} | basis net ${yen(tributeBasisProfit)} | exemption ${yen(tribute.profitExemption)}${tribute.reduction ? ` | reduction -${yen(tribute.reduction)}` : ''}`,
|
`Card draft: <b>${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'}</b> | Farms with scanner route: <b>${connected}/${game.eggFarms.length}</b>`,
|
||||||
`Maintenance: <b>worst ${maintenanceSummary(game).worst.percent}% dirty (${maintenanceSummary(game).worst.label})</b> | Repairman: <b>${game.repairman?.hiredForNextDay ? 'hired next day' : game.repairman?.active ? 'working' : 'none'}</b>`,
|
|
||||||
`Manual combo: <b>${game.manualCombo?.count || 0}</b> | Card draft: <b>${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'}</b> | Belt: <b>${displaySpeed()}px/s</b> | Farms with scanner route: <b>${connected}/${game.eggFarms.length}</b>`,
|
|
||||||
game.cardEffects?.usedMachineActive ? '<span class="cash-negative">Procurement: USED MACHINE MODE / no refunds / 60% durability</span>' : '',
|
game.cardEffects?.usedMachineActive ? '<span class="cash-negative">Procurement: USED MACHINE MODE / no refunds / 60% durability</span>' : '',
|
||||||
issues.length ? `<span class="cash-negative">Blocked: ${issues[0]}</span>` : `<span class="cash-positive">${TEXT.status.allPortsConnected}</span>`
|
issues.length ? `<span class="cash-negative">Blocked: ${issues[0]}</span>` : `<span class="cash-positive">${TEXT.status.allPortsConnected}</span>`
|
||||||
].join('<br>');
|
].filter(Boolean).join('<br>');
|
||||||
}
|
|
||||||
function updateEventPanel() {
|
|
||||||
if (!ui.eventPanel) return;
|
|
||||||
const offer = game.eventOffer;
|
|
||||||
if (game.phase !== 'build') {
|
|
||||||
ui.eventPanel.innerHTML = TEXT.status.eventPanelRunning;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!offer) {
|
|
||||||
ui.eventPanel.className = 'event-card empty';
|
|
||||||
const nextTurn = game.turn + 1;
|
|
||||||
if (nextTurn < DAILY_EVENT_FIRST_TURN) {
|
|
||||||
ui.eventPanel.innerHTML = `<strong>Event lock active.</strong><span>No irregular events during Days 1-7. First possible forced event: Day ${DAILY_EVENT_FIRST_TURN}.</span>`;
|
|
||||||
} else {
|
|
||||||
ui.eventPanel.innerHTML = '<strong>No event.</strong><span>Random forced events can appear later.</span>';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ui.eventPanel.className = 'event-card accepted';
|
|
||||||
ui.eventPanel.innerHTML = `
|
|
||||||
<div class="event-status">FORCED NEXT DAY</div>
|
|
||||||
<h3>${offer.title}</h3>
|
|
||||||
<div class="event-metrics">
|
|
||||||
<span>Effect: ${offer.route}</span>
|
|
||||||
<span>${offer.targetAmount ? `Target: ${offer.targetAmount}` : 'Target: none'}</span>
|
|
||||||
<span>${offer.reward ? `Reward: ${yen(offer.reward)}` : 'Reward: none'}</span>
|
|
||||||
<span>Time ${offer.daySeconds || 60}s / Output x${Number(offer.productionMultiplier || 1).toFixed(2)} / Poop +${Math.round((offer.poopRateAdd || 0) * 100)}pt</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
updateToolButtons();
|
updateToolButtons();
|
||||||
|
|
@ -172,7 +129,9 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
}
|
}
|
||||||
const nextTributeBasis = Math.max(0, Math.floor(game.lastResult?.profit ?? game.stats?.profit ?? 0));
|
const nextTributeBasis = Math.max(0, Math.floor(game.lastResult?.profit ?? game.stats?.profit ?? 0));
|
||||||
const nextTribute = fairiesTributeInfo(game, Math.max(1, Number(game.turn) || 1), nextTributeBasis).amount;
|
const nextTribute = fairiesTributeInfo(game, Math.max(1, Number(game.turn) || 1), nextTributeBasis).amount;
|
||||||
ui.buttons.nextTurn.textContent = game.phase === 'build' && nextTribute > 0 ? `Next Day - Fairies ${yen(nextTribute)}` : TEXT.actions.nextDay;
|
ui.buttons.nextTurn.innerHTML = game.phase === 'build' && nextTribute > 0
|
||||||
|
? `<span class="next-day-main">NEXT DAY</span><small class="next-day-fee">FAIRIES -${yen(nextTribute)}</small>`
|
||||||
|
: `<span class="next-day-main">${TEXT.actions.nextDay}</span>`;
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,7 +139,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
if (!ui.eventBrief || !ui.targetBrief || !ui.speedBrief) return;
|
if (!ui.eventBrief || !ui.targetBrief || !ui.speedBrief) return;
|
||||||
const target = truckTarget(game).toUpperCase();
|
const target = truckTarget(game).toUpperCase();
|
||||||
ui.targetBrief.textContent = `TRUCK TARGET: ${target}`;
|
ui.targetBrief.textContent = `TRUCK TARGET: ${target}`;
|
||||||
ui.speedBrief.textContent = `BELT: ${displaySpeed()} px/s | COMBO: ${game.manualCombo?.count || 0}`;
|
ui.speedBrief.textContent = `BELT SPEED: ${displaySpeed()} px/s`;
|
||||||
ui.speedBrief.title = `Conveyor speed does not increase by day. Degraded belts reduce speed. Cap: ${CONVEYOR_SPEED_MAX}px/s.`;
|
ui.speedBrief.title = `Conveyor speed does not increase by day. Degraded belts reduce speed. Cap: ${CONVEYOR_SPEED_MAX}px/s.`;
|
||||||
const active = game.eventActive;
|
const active = game.eventActive;
|
||||||
const offer = game.eventOffer;
|
const offer = game.eventOffer;
|
||||||
|
|
@ -235,7 +194,6 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
||||||
<div><strong>Event bonus</strong>${positive(r.eventBonus || 0)}</div>
|
<div><strong>Event bonus</strong>${positive(r.eventBonus || 0)}</div>
|
||||||
<div><strong>Penalties</strong>${negative(penalties)}</div>
|
<div><strong>Penalties</strong>${negative(penalties)}</div>
|
||||||
<div><strong>Fairies fee</strong>${negative(r.fairiesTribute?.amount || r.fairiesTribute || 0)}</div>
|
<div><strong>Fairies fee</strong>${negative(r.fairiesTribute?.amount || r.fairiesTribute || 0)}</div>
|
||||||
<div><strong>Repair worker wages</strong>${negative(r.repairWorkerWages || 0)}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="formula-box compact">Income ${yen(r.revenue)} - losses ${yen(r.penalty)} = <b class="${netClass}">${yen(r.profit)}</b></p>`;
|
<p class="formula-box compact">Income ${yen(r.revenue)} - losses ${yen(r.penalty)} = <b class="${netClass}">${yen(r.profit)}</b></p>`;
|
||||||
ui.modalActions.innerHTML = '';
|
ui.modalActions.innerHTML = '';
|
||||||
|
|
|
||||||
1130
styles-v27.17.css
Normal file
1253
styles-v27.18.css
Normal file
1279
styles-v27.19.css
Normal file
518
styles.css
|
|
@ -759,3 +759,521 @@ body { font-size: 20px; }
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.scanner-monitor { bottom: 132px; width: min(520px, calc(100% - 16px)); left: 8px; }
|
.scanner-monitor { bottom: 132px; width: min(520px, calc(100% - 16px)); left: 8px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
DARK FACTORY VISUAL PASS
|
||||||
|
- Facility sprites are now loaded from assets/dark_factory/.
|
||||||
|
- Debug UI is intentionally retained for balancing builds. Do not remove.
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
:root {
|
||||||
|
--bg: #0b0c0f;
|
||||||
|
--screen: #111318;
|
||||||
|
--panel: rgba(24, 27, 34, 0.96);
|
||||||
|
--panel-2: #20242b;
|
||||||
|
--panel-3: #2a303a;
|
||||||
|
--ink: #e6edf2;
|
||||||
|
--muted: #8f9bab;
|
||||||
|
--line: #050609;
|
||||||
|
--green: #7bd84f;
|
||||||
|
--green-soft: #273c24;
|
||||||
|
--white: #e6edf2;
|
||||||
|
--danger: #e34a36;
|
||||||
|
--warn: #ffb827;
|
||||||
|
--cyan: #4aa3ff;
|
||||||
|
--orange: #c86a2b;
|
||||||
|
--shadow: 6px 6px 0 rgba(0, 0, 0, 0.62);
|
||||||
|
}
|
||||||
|
html, body { background: #050609; color: var(--ink); }
|
||||||
|
body {
|
||||||
|
background:
|
||||||
|
linear-gradient(90deg, rgba(255,184,39,.035) 1px, transparent 1px) 0 0 / 32px 32px,
|
||||||
|
linear-gradient(rgba(255,184,39,.028) 1px, transparent 1px) 0 0 / 32px 32px,
|
||||||
|
#050609;
|
||||||
|
}
|
||||||
|
#app { width: min(100vw, 1720px); padding: 0; }
|
||||||
|
.game-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
border: 0;
|
||||||
|
background: var(--screen);
|
||||||
|
box-shadow: inset 0 0 0 4px #050609, inset 0 0 0 8px #303744;
|
||||||
|
}
|
||||||
|
#gameCanvas {
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 720px;
|
||||||
|
background: var(--screen);
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
.hud { gap: 4px; }
|
||||||
|
.hud-top-left {
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
right: 420px;
|
||||||
|
grid-template-columns: 1.25fr repeat(5, minmax(92px, .65fr));
|
||||||
|
}
|
||||||
|
.hud-top-right {
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
grid-template-columns: repeat(4, minmax(74px, auto));
|
||||||
|
}
|
||||||
|
.hud-card {
|
||||||
|
min-width: 74px;
|
||||||
|
padding: 7px 9px;
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
background: linear-gradient(#242a33, #171a20);
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,.7), inset 0 0 0 2px rgba(255,184,39,.10);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.hud-card span { color: var(--warn); font-size: 9px; letter-spacing: .12em; }
|
||||||
|
.hud-card strong { color: var(--ink); text-shadow: 2px 2px 0 #050609; }
|
||||||
|
.hud-card.cash strong, .hud-card.profit strong, .hud-card.combo strong, .cash-positive { color: var(--green) !important; }
|
||||||
|
.hud-card.time-critical { background: #3a1f1f; box-shadow: inset 0 0 0 3px var(--danger), 4px 4px 0 rgba(0,0,0,.7); }
|
||||||
|
.priority-strip {
|
||||||
|
top: 74px;
|
||||||
|
left: 10px;
|
||||||
|
width: clamp(250px, 21vw, 330px);
|
||||||
|
max-width: none;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.priority-strip > div {
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
background: linear-gradient(#222832, #15181e);
|
||||||
|
color: var(--ink);
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,.66), inset 0 0 0 2px rgba(255,184,39,.12);
|
||||||
|
}
|
||||||
|
.event-brief.active { background: #38251a; box-shadow: inset 0 0 0 3px var(--orange), 4px 4px 0 rgba(0,0,0,.66); }
|
||||||
|
.event-brief.next { background: #1c2c20; box-shadow: inset 0 0 0 3px var(--green), 4px 4px 0 rgba(0,0,0,.66); }
|
||||||
|
.event-brief.locked { background: #181b20; color: var(--muted); }
|
||||||
|
.target-brief { color: var(--warn); }
|
||||||
|
.speed-brief { color: var(--cyan); }
|
||||||
|
.build-panel {
|
||||||
|
top: 82px;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
width: clamp(320px, 24vw, 410px);
|
||||||
|
padding: 9px;
|
||||||
|
border: 4px solid var(--line);
|
||||||
|
background: linear-gradient(#222832, #14171d);
|
||||||
|
box-shadow: 6px 6px 0 rgba(0,0,0,.72), inset 0 0 0 3px rgba(255,184,39,.10);
|
||||||
|
scrollbar-color: var(--warn) #111318;
|
||||||
|
}
|
||||||
|
.panel-head {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 126px;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.panel-head h1 { color: var(--warn); text-shadow: 2px 2px 0 #050609; font-size: 17px; }
|
||||||
|
.panel-head h1 span { color: #050609; background: var(--warn); border-color: var(--line); }
|
||||||
|
.panel-head p { color: var(--muted); }
|
||||||
|
.panel-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.build-panel h2, .result-section-title { color: var(--warn); }
|
||||||
|
.large-tools {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.tool-button, .primary-button, .sort-button, .facility-action {
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
background: linear-gradient(#2a303a, #171a20);
|
||||||
|
color: var(--ink);
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,.70), inset 0 0 0 2px rgba(255,184,39,.08);
|
||||||
|
text-shadow: 1px 1px 0 #050609;
|
||||||
|
}
|
||||||
|
.tool-button { min-height: 48px; padding: 8px; }
|
||||||
|
.tool-button strong { color: var(--ink); }
|
||||||
|
.tool-button span { color: var(--muted); }
|
||||||
|
.tool-button.active {
|
||||||
|
background: linear-gradient(#33311d, #1e2117);
|
||||||
|
box-shadow: inset 0 0 0 3px var(--warn), 4px 4px 0 rgba(0,0,0,.70);
|
||||||
|
}
|
||||||
|
.tool-button.danger.active { background: linear-gradient(#4b1f1c, #241014); box-shadow: inset 0 0 0 3px var(--danger), 4px 4px 0 rgba(0,0,0,.70); }
|
||||||
|
.primary-button { background: linear-gradient(#8b2d23, #4b1714); color: #fff3d2; }
|
||||||
|
.primary-button:not(:disabled) { box-shadow: inset 0 0 0 3px #ffb827, 5px 5px 0 rgba(0,0,0,.72); }
|
||||||
|
.primary-button:disabled, .tool-button:disabled, .sort-button:disabled, .facility-action:disabled { opacity: .42; filter: grayscale(1); }
|
||||||
|
.mini-box, .event-card, .formula-box, .equipment-menu-lines, .route-card, .result-grid div, .muted-title {
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
background: #15181e;
|
||||||
|
color: var(--muted);
|
||||||
|
box-shadow: inset 0 0 0 2px rgba(255,184,39,.08);
|
||||||
|
}
|
||||||
|
.event-card h3, .event-card strong, .event-metrics span { color: var(--ink); }
|
||||||
|
.event-card.accepted { background: #1c2c20; box-shadow: inset 0 0 0 3px var(--green), 4px 4px 0 rgba(0,0,0,.66); }
|
||||||
|
.event-card.empty { background: #15181e; color: var(--muted); }
|
||||||
|
.event-metrics span { border-color: #3d4654; background: #20242b; }
|
||||||
|
.event-status { background: #2a303a; color: var(--warn); border-color: var(--line); }
|
||||||
|
.control-dock {
|
||||||
|
left: 50%;
|
||||||
|
right: auto;
|
||||||
|
bottom: 8px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
grid-template-columns: repeat(4, minmax(135px, 160px));
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 4px solid var(--line);
|
||||||
|
background: rgba(13,15,19,.96);
|
||||||
|
box-shadow: 5px 5px 0 rgba(0,0,0,.72), inset 0 0 0 2px rgba(255,184,39,.10);
|
||||||
|
}
|
||||||
|
.sort-button { min-height: 44px; font-size: 12px; }
|
||||||
|
.scanner-monitor {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
left: 10px;
|
||||||
|
top: 198px;
|
||||||
|
width: clamp(250px, 21vw, 330px);
|
||||||
|
}
|
||||||
|
.scanner-monitor-card {
|
||||||
|
border: 3px solid var(--line) !important;
|
||||||
|
background: linear-gradient(#222832, #15181e) !important;
|
||||||
|
color: var(--ink) !important;
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,.66), inset 0 0 0 2px rgba(255,184,39,.10) !important;
|
||||||
|
}
|
||||||
|
.scanner-monitor-card strong { color: var(--warn); }
|
||||||
|
.debug-panel {
|
||||||
|
left: auto;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 10px;
|
||||||
|
width: min(420px, calc(100% - 24px));
|
||||||
|
border-color: var(--line);
|
||||||
|
background: rgba(16,18,23,.97);
|
||||||
|
box-shadow: 5px 5px 0 rgba(0,0,0,.72), inset 0 0 0 2px rgba(227,74,54,.18);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.debug-panel summary { color: var(--danger); }
|
||||||
|
.debug-body { border-top-color: var(--line); }
|
||||||
|
.debug-body button, .debug-body input[type="number"], .debug-body select, .debug-readout {
|
||||||
|
border-color: var(--line);
|
||||||
|
background: #20242b;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.debug-readout { color: var(--muted); }
|
||||||
|
.modal { background: rgba(3,4,6,.72); }
|
||||||
|
.modal-card {
|
||||||
|
border-color: var(--line);
|
||||||
|
background: linear-gradient(#242a33, #15181e);
|
||||||
|
color: var(--ink);
|
||||||
|
box-shadow: 8px 8px 0 rgba(0,0,0,.72), inset 0 0 0 3px rgba(255,184,39,.12);
|
||||||
|
}
|
||||||
|
.modal-card h2 { color: var(--warn); text-shadow: 2px 2px 0 #050609; }
|
||||||
|
.modal-card ul, .modal-card p { color: var(--muted); }
|
||||||
|
.hover-tooltip {
|
||||||
|
border-color: var(--line) !important;
|
||||||
|
background: rgba(20,23,29,.98) !important;
|
||||||
|
color: var(--ink) !important;
|
||||||
|
box-shadow: 5px 5px 0 rgba(0,0,0,.72) !important;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar { width: 14px; height: 14px; }
|
||||||
|
::-webkit-scrollbar-track { background: #111318; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #333b48; border: 3px solid #111318; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #ffb827; }
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.hud-top-left { right: 8px; grid-template-columns: repeat(3, minmax(80px, 1fr)); }
|
||||||
|
.hud-top-right { top: 132px; left: 8px; right: auto; grid-template-columns: repeat(2, minmax(82px, auto)); }
|
||||||
|
.priority-strip { top: 226px; }
|
||||||
|
.scanner-monitor { top: 348px; }
|
||||||
|
.build-panel { left: 8px; right: 8px; bottom: 8px; width: auto; max-height: 46vh; }
|
||||||
|
.control-dock { grid-template-columns: repeat(2, 150px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* v27.16: compact collapsed debug panel + icon card layout */
|
||||||
|
.debug-panel:not([open]) {
|
||||||
|
width: 104px !important;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.debug-panel:not([open]) summary {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.debug-panel[open] {
|
||||||
|
width: min(420px, calc(100% - 24px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-choice {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 54px minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: start;
|
||||||
|
min-height: 150px;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(255,255,255,.08), rgba(0,0,0,.20)),
|
||||||
|
#20242b;
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
.card-choice::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 5px;
|
||||||
|
border: 1px solid rgba(255,184,39,.18);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.card-icon {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
background: #15181e;
|
||||||
|
box-shadow: inset 0 0 0 2px rgba(255,184,39,.10), 3px 3px 0 rgba(0,0,0,.35);
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
.card-icon img {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
object-fit: contain;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
.card-icon.glyph {
|
||||||
|
color: var(--warn, #ffb827);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.card-copy {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.card-choice strong {
|
||||||
|
color: var(--warn, #ffb827);
|
||||||
|
text-shadow: 1px 1px 0 #050609;
|
||||||
|
}
|
||||||
|
.card-choice small { color: var(--muted); }
|
||||||
|
.card-tags span {
|
||||||
|
background: #15181e;
|
||||||
|
color: var(--white);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
.card-choice.common { background: linear-gradient(145deg, #262c35, #171a20); }
|
||||||
|
.card-choice.rare {
|
||||||
|
background: linear-gradient(145deg, #3b3020, #191b20);
|
||||||
|
box-shadow: inset 0 0 0 3px #f0a020, 5px 5px 0 rgba(0,0,0,.50);
|
||||||
|
}
|
||||||
|
.card-choice.ultra-rare {
|
||||||
|
background: linear-gradient(145deg, #401d2a, #171a20);
|
||||||
|
box-shadow: inset 0 0 0 3px #c42424, 5px 5px 0 rgba(0,0,0,.55);
|
||||||
|
}
|
||||||
|
.card-choice.dud {
|
||||||
|
background: linear-gradient(145deg, #2b2b2b, #191919);
|
||||||
|
color: #878787;
|
||||||
|
}
|
||||||
|
.card-choice.dud .card-tags span { background: #222; color: #aaa; }
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.debug-panel:not([open]) {
|
||||||
|
width: 92px !important;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* v27.18 inherited cache-proof identity and final UI overrides */
|
||||||
|
.build-stamp {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 12;
|
||||||
|
right: 14px;
|
||||||
|
top: 76px;
|
||||||
|
padding: 5px 9px;
|
||||||
|
border: 2px solid #ffb827;
|
||||||
|
background: rgba(14, 17, 22, .94);
|
||||||
|
color: #80e067;
|
||||||
|
box-shadow: 3px 3px 0 rgba(0,0,0,.7);
|
||||||
|
font: 900 11px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.debug-panel:not([open]) {
|
||||||
|
width: 58px !important;
|
||||||
|
max-width: 58px !important;
|
||||||
|
height: 30px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.debug-panel:not([open]) summary {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 58px;
|
||||||
|
height: 27px;
|
||||||
|
padding: 5px 7px;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: clip;
|
||||||
|
}
|
||||||
|
.debug-panel[open] {
|
||||||
|
width: min(390px, calc(100% - 24px)) !important;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.card-choice {
|
||||||
|
border-width: 3px;
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,.55), inset 0 0 0 1px rgba(255,184,39,.14);
|
||||||
|
}
|
||||||
|
.card-choice:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 4px 6px 0 rgba(0,0,0,.65), inset 0 0 0 2px rgba(128,224,103,.24);
|
||||||
|
}
|
||||||
|
.card-icon img {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
filter: drop-shadow(2px 2px 0 rgba(0,0,0,.7));
|
||||||
|
}
|
||||||
|
#gameCanvas { image-rendering: pixelated; }
|
||||||
|
|
||||||
|
/* v27.18: status relocation, consistent industrial controls, card watermarks */
|
||||||
|
.hud-top-left { grid-template-columns: repeat(5, minmax(82px, auto)); }
|
||||||
|
|
||||||
|
.left-bottom-status {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 6;
|
||||||
|
left: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
width: min(310px, calc(100% - 20px));
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.left-bottom-status > div {
|
||||||
|
min-width: 0;
|
||||||
|
border: 3px solid var(--line);
|
||||||
|
background: rgba(22, 26, 32, .95);
|
||||||
|
color: var(--ink);
|
||||||
|
padding: 8px 10px;
|
||||||
|
box-shadow: 4px 4px 0 rgba(0,0,0,.68), inset 0 0 0 1px rgba(255,184,39,.10);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .025em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.left-bottom-status .event-brief { grid-column: 1 / -1; white-space: normal; min-height: 39px; }
|
||||||
|
.left-bottom-status .event-brief.active { background: #35221c; box-shadow: inset 0 0 0 3px var(--danger), 4px 4px 0 rgba(0,0,0,.68); }
|
||||||
|
.left-bottom-status .event-brief.next { background: #22301f; box-shadow: inset 0 0 0 3px var(--green), 4px 4px 0 rgba(0,0,0,.68); }
|
||||||
|
.left-bottom-status .event-brief.locked { background: #1c2027; color: var(--muted); }
|
||||||
|
.left-bottom-status .speed-brief { color: #d9e2e8; }
|
||||||
|
.left-bottom-status .target-brief { color: #ffcf68; }
|
||||||
|
.left-bottom-status .combo-brief { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
||||||
|
.left-bottom-status .combo-brief span { color: var(--muted); font-size: 10px; }
|
||||||
|
.left-bottom-status .combo-brief strong { color: var(--green); font-size: 22px; line-height: 1; }
|
||||||
|
|
||||||
|
/* Remove the remaining bright green/white control styling from modal actions. */
|
||||||
|
.modal-actions button,
|
||||||
|
.facility-action,
|
||||||
|
.card-target-button {
|
||||||
|
border: 3px solid #07080a;
|
||||||
|
background: linear-gradient(#353d48, #1b1f25);
|
||||||
|
color: #e6edf2;
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(255,184,39,.14), 4px 4px 0 rgba(0,0,0,.68);
|
||||||
|
}
|
||||||
|
.modal-actions button:hover:not(:disabled),
|
||||||
|
.facility-action:hover:not(:disabled),
|
||||||
|
.card-target-button:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(#424b58, #242930);
|
||||||
|
color: #fff0c9;
|
||||||
|
box-shadow: inset 0 0 0 2px rgba(255,184,39,.42), 4px 4px 0 rgba(0,0,0,.72);
|
||||||
|
}
|
||||||
|
.facility-action.warn,
|
||||||
|
.modal-actions .reroll-button {
|
||||||
|
background: linear-gradient(#554221, #2b2419);
|
||||||
|
color: #ffd36a;
|
||||||
|
box-shadow: inset 0 0 0 3px #b97810, 5px 5px 0 rgba(0,0,0,.72);
|
||||||
|
}
|
||||||
|
.modal-actions .reroll-button:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(#6b5227, #342919);
|
||||||
|
color: #fff0bd;
|
||||||
|
box-shadow: inset 0 0 0 3px #ffb827, 5px 5px 0 rgba(0,0,0,.76);
|
||||||
|
}
|
||||||
|
.facility-action.good { background: linear-gradient(#29422a, #19271a); color: #a9ed91; }
|
||||||
|
.facility-action.danger { background: linear-gradient(#57251f, #2d1715); color: #ffb1a3; }
|
||||||
|
.modal-actions button:disabled,
|
||||||
|
.facility-action:disabled,
|
||||||
|
.card-target-button:disabled { opacity: .42; filter: grayscale(.85); box-shadow: 3px 3px 0 rgba(0,0,0,.45); }
|
||||||
|
|
||||||
|
/* Icons are now translucent background art rather than separate foreground badges. */
|
||||||
|
.card-choice {
|
||||||
|
display: block;
|
||||||
|
min-height: 170px;
|
||||||
|
padding: 16px;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
.card-choice::before { z-index: 1; }
|
||||||
|
.card-copy { position: relative; z-index: 3; padding-right: 6px; }
|
||||||
|
.card-icon {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
right: -9px;
|
||||||
|
bottom: -12px;
|
||||||
|
width: 124px;
|
||||||
|
height: 124px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: .14;
|
||||||
|
transform: rotate(-7deg);
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
.card-icon img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: grayscale(.12) contrast(1.2) drop-shadow(3px 3px 0 rgba(0,0,0,.55));
|
||||||
|
}
|
||||||
|
.card-icon.glyph {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #f0b437;
|
||||||
|
font-size: 88px;
|
||||||
|
line-height: 1;
|
||||||
|
text-shadow: 4px 4px 0 rgba(0,0,0,.55);
|
||||||
|
}
|
||||||
|
.card-choice.rare .card-icon { opacity: .16; }
|
||||||
|
.card-choice.ultra-rare .card-icon { opacity: .17; }
|
||||||
|
.card-choice.dud .card-icon { opacity: .08; filter: grayscale(1); }
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.hud-top-left { grid-template-columns: repeat(3, minmax(80px, 1fr)); }
|
||||||
|
.left-bottom-status { width: 290px; left: 8px; bottom: 8px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.left-bottom-status { width: min(270px, calc(100% - 16px)); grid-template-columns: 1fr; }
|
||||||
|
.left-bottom-status .event-brief { grid-column: auto; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* v27.19: compact Next Day fee label and deduplicated side panel */
|
||||||
|
.panel-head { grid-template-columns: minmax(0, 1fr) minmax(112px, 126px); }
|
||||||
|
.primary-button#nextTurnButton {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 126px;
|
||||||
|
padding: 8px 7px;
|
||||||
|
white-space: normal;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.08;
|
||||||
|
display: grid;
|
||||||
|
place-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#nextTurnButton .next-day-main { display: block; font-size: 15px; letter-spacing: .035em; }
|
||||||
|
#nextTurnButton .next-day-fee {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
color: #ffd36a;
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1.15;
|
||||||
|
letter-spacing: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
|
||||||