Compare commits
6 commits
b54f36c0e8
...
d6a55d9c13
| Author | SHA1 | Date | |
|---|---|---|---|
| d6a55d9c13 | |||
| 650e44aff3 | |||
| 82a6b2c787 | |||
| 383a46955c | |||
| 1c383282dd | |||
| 40b4752ed2 |
26 changed files with 1644 additions and 288 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules/
|
||||
18
README.md
18
README.md
|
|
@ -2,6 +2,24 @@
|
|||
|
||||
Open `index.html` in a browser.
|
||||
|
||||
Production uses the committed `dist/game.js` bundle. Edit source files under
|
||||
`src/`, then run `npm install` once and `npm run build` after JavaScript
|
||||
changes to regenerate the bundle.
|
||||
|
||||
## Static server cache setup
|
||||
|
||||
Serve `index.html`, the bundled JavaScript, and CSS with cache validation
|
||||
instead of import query strings. This prevents stale files while keeping reloads
|
||||
fast.
|
||||
|
||||
Use the example in `deploy/` for your HTTP server:
|
||||
|
||||
- nginx: include `deploy/nginx-cache.conf` inside the server block.
|
||||
- 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.
|
||||
|
||||
## Current baseline
|
||||
|
||||
- Build, move, box-select, sell, and repair factory equipment during Build phase.
|
||||
|
|
|
|||
9
deploy/Caddyfile-cache
Normal file
9
deploy/Caddyfile-cache
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Adapt these directives inside the Caddy site block that serves this static app.
|
||||
|
||||
encode gzip zstd
|
||||
|
||||
@html path /index.html
|
||||
header @html Cache-Control "no-cache"
|
||||
|
||||
@assets path *.js *.css
|
||||
header @assets Cache-Control "no-cache"
|
||||
17
deploy/apache-cache.htaccess
Normal file
17
deploy/apache-cache.htaccess
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Use this from the static site root when served by Apache with .htaccess enabled.
|
||||
|
||||
FileETag MTime Size
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
<Files "index.html">
|
||||
Header set Cache-Control "no-cache"
|
||||
</Files>
|
||||
|
||||
<FilesMatch "\.(js|css)$">
|
||||
Header set Cache-Control "no-cache"
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/css application/javascript text/javascript
|
||||
</IfModule>
|
||||
20
deploy/nginx-cache.conf
Normal file
20
deploy/nginx-cache.conf
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Include this in the nginx server block that serves this static app.
|
||||
#
|
||||
# Example:
|
||||
# server {
|
||||
# root /var/www/zunda-shiwake;
|
||||
# include /var/www/zunda-shiwake/deploy/nginx-cache.conf;
|
||||
# }
|
||||
|
||||
etag on;
|
||||
gzip on;
|
||||
gzip_types text/css application/javascript text/javascript;
|
||||
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
|
||||
location ~* \.(js|css)$ {
|
||||
add_header Cache-Control "no-cache";
|
||||
gzip_static on;
|
||||
}
|
||||
61
dist/game.js
vendored
Normal file
61
dist/game.js
vendored
Normal file
File diff suppressed because one or more lines are too long
30
index.html
30
index.html
|
|
@ -12,9 +12,9 @@
|
|||
<canvas id="gameCanvas" width="1440" height="900" aria-label="Chick Sorter game canvas"></canvas>
|
||||
|
||||
<div class="hud hud-top-left" aria-label="Run status">
|
||||
<div class="hud-card cash"><span>CASH</span><strong id="money">¥250</strong></div>
|
||||
<div class="hud-card cash"><span>CASH</span><strong id="money">・・250</strong></div>
|
||||
<div class="hud-card"><span>TIME</span><strong id="timeLeft">60.0s</strong></div>
|
||||
<div class="hud-card profit"><span>NET</span><strong id="turnProfit">¥0</strong></div>
|
||||
<div class="hud-card profit"><span>NET</span><strong id="turnProfit">・・0</strong></div>
|
||||
<div class="hud-card"><span>DAY</span><strong id="turn">1</strong></div>
|
||||
<div class="hud-card compact"><span>PHASE</span><strong id="phaseLabel">Title</strong></div>
|
||||
<div class="hud-card combo"><span>COMBO</span><strong id="comboCount">0</strong></div>
|
||||
|
|
@ -46,25 +46,20 @@
|
|||
<section class="equipment-section">
|
||||
<h2>Add / Edit Equipment</h2>
|
||||
<div class="large-tools">
|
||||
<button id="buildConveyorButton" class="tool-button" type="button"><strong>Conveyor</strong><span>¥30 / tile</span></button>
|
||||
<button id="buildEggFarmButton" class="tool-button" type="button"><strong>Egg Farm</strong><span>¥250</span></button>
|
||||
<button id="buildAutoScannerButton" class="tool-button" type="button"><strong>Auto Scanner</strong><span>¥360 / 2.25s cooldown</span></button>
|
||||
<button id="buildManualScannerButton" class="tool-button" type="button"><strong>Manual Scanner</strong><span>¥260</span></button>
|
||||
<button id="buildMixerButton" class="tool-button" type="button"><strong>Mixer</strong><span>¥450</span></button>
|
||||
<button id="buildTrashButton" class="tool-button" type="button"><strong>Waste Shredder</strong><span>¥240</span></button>
|
||||
<button id="buildTruckButton" class="tool-button" type="button"><strong>Truck</strong><span>¥450</span></button>
|
||||
<button id="buildConveyorButton" class="tool-button" type="button"><strong>Conveyor</strong><span>・・30 / tile</span></button>
|
||||
<button id="buildBoostConveyorButton" class="tool-button" type="button"><strong>Boost Conveyor</strong><span>・・50 / tile</span></button>
|
||||
<button id="buildEggFarmButton" class="tool-button" type="button"><strong>Egg Farm</strong><span>・・250</span></button>
|
||||
<button id="buildAutoScannerButton" class="tool-button" type="button"><strong>Auto Scanner</strong><span>¥360 / 3.5s cooldown</span></button>
|
||||
<button id="buildManualScannerButton" class="tool-button" type="button"><strong>Manual Scanner</strong><span>・・260</span></button>
|
||||
<button id="buildMixerButton" class="tool-button" type="button"><strong>Mixer</strong><span>・・450</span></button>
|
||||
<button id="buildTrashButton" class="tool-button" type="button"><strong>Waste Shredder</strong><span>・・240</span></button>
|
||||
<button id="buildTruckButton" class="tool-button" type="button"><strong>Truck</strong><span>・・450</span></button>
|
||||
<button id="eraseButton" class="tool-button danger" type="button"><strong>Sell</strong><span>Sell or refund equipment</span></button>
|
||||
<button id="hireRepairmanButton" class="tool-button repair" type="button"><strong>Hire Repairman</strong><span>¥100 / next day</span></button>
|
||||
<button id="expandGridButton" class="tool-button expand" type="button"><strong>Expand Grid</strong><span>¥1,300 / +10×10</span><small class="tool-flavor">Canvas buttons buy adjacent right or upper 10×10 lots. New ground includes blocked no-build tiles.</small></button>
|
||||
<button id="hireRepairmanButton" class="tool-button repair" type="button"><strong>Hire Repairman</strong><span>・・100 / next day</span></button>
|
||||
<div class="history-tools"><button id="undoButton" class="tool-button small" type="button"><strong>Undo</strong><span>Ctrl+Z</span></button><button id="redoButton" class="tool-button small" type="button"><strong>Redo</strong><span>Ctrl+Y</span></button></div>
|
||||
</div>
|
||||
<div id="buildStatus" class="mini-box">Build tools unlock after each day.</div>
|
||||
</section>
|
||||
|
||||
<section class="contract-section">
|
||||
<h2>Irregular One-Day Event</h2>
|
||||
<div id="contractPanel" class="contract-card empty">Irregular forced events appear during Build phase.</div>
|
||||
</section>
|
||||
<section class="rules-section compact-rules">
|
||||
<h2>Status</h2>
|
||||
<div id="turnSummary" class="mini-box"></div>
|
||||
|
|
@ -88,6 +83,7 @@
|
|||
<label class="debug-check"><input id="debugInfiniteCash" type="checkbox" /> Infinite money</label>
|
||||
<label class="debug-day">Day <input id="debugDayInput" type="number" min="1" step="1" value="1" /></label>
|
||||
<button id="debugSetDayButton" type="button">Set Day</button>
|
||||
<button id="debugZeroTimerButton" type="button">Timer 0</button>
|
||||
<label class="debug-card">Card <select id="debugCardSelect"></select></label>
|
||||
<button id="debugGrantCardButton" type="button">Grant Card</button>
|
||||
<button id="debugGrantAllCardsButton" type="button">All Cards</button>
|
||||
|
|
@ -105,6 +101,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./src/game.js?v=27.6-direct-conveyor-chicks"></script>
|
||||
<script type="module" src="./dist/game.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
496
package-lock.json
generated
Normal file
496
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
{
|
||||
"name": "zunda-shiwake",
|
||||
"version": "27.10.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "zunda-shiwake",
|
||||
"version": "27.10.0",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"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",
|
||||
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"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",
|
||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"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",
|
||||
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"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",
|
||||
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"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",
|
||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"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",
|
||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"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",
|
||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"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",
|
||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"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",
|
||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"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",
|
||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"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",
|
||||
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"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",
|
||||
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"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",
|
||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"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",
|
||||
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"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",
|
||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"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",
|
||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"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",
|
||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"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",
|
||||
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"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",
|
||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"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",
|
||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"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",
|
||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"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",
|
||||
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"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",
|
||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"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",
|
||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"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",
|
||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"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",
|
||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"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",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.12",
|
||||
"@esbuild/android-arm": "0.25.12",
|
||||
"@esbuild/android-arm64": "0.25.12",
|
||||
"@esbuild/android-x64": "0.25.12",
|
||||
"@esbuild/darwin-arm64": "0.25.12",
|
||||
"@esbuild/darwin-x64": "0.25.12",
|
||||
"@esbuild/freebsd-arm64": "0.25.12",
|
||||
"@esbuild/freebsd-x64": "0.25.12",
|
||||
"@esbuild/linux-arm": "0.25.12",
|
||||
"@esbuild/linux-arm64": "0.25.12",
|
||||
"@esbuild/linux-ia32": "0.25.12",
|
||||
"@esbuild/linux-loong64": "0.25.12",
|
||||
"@esbuild/linux-mips64el": "0.25.12",
|
||||
"@esbuild/linux-ppc64": "0.25.12",
|
||||
"@esbuild/linux-riscv64": "0.25.12",
|
||||
"@esbuild/linux-s390x": "0.25.12",
|
||||
"@esbuild/linux-x64": "0.25.12",
|
||||
"@esbuild/netbsd-arm64": "0.25.12",
|
||||
"@esbuild/netbsd-x64": "0.25.12",
|
||||
"@esbuild/openbsd-arm64": "0.25.12",
|
||||
"@esbuild/openbsd-x64": "0.25.12",
|
||||
"@esbuild/openharmony-arm64": "0.25.12",
|
||||
"@esbuild/sunos-x64": "0.25.12",
|
||||
"@esbuild/win32-arm64": "0.25.12",
|
||||
"@esbuild/win32-ia32": "0.25.12",
|
||||
"@esbuild/win32-x64": "0.25.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
package.json
Normal file
13
package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "zunda-shiwake",
|
||||
"version": "27.6.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ export const BALANCE = {
|
|||
fairiesTribute: {
|
||||
perDay: 10
|
||||
},
|
||||
incomeUpgradeRate: 1.05,
|
||||
incomeUpgradeRate: 1.10,
|
||||
explosionDamageDivisor: 30,
|
||||
maleTruckFinePerHalfDay: 30,
|
||||
shredderBonus: {
|
||||
|
|
@ -53,7 +53,7 @@ export const BALANCE = {
|
|||
},
|
||||
production: {
|
||||
poopRate: 0.08,
|
||||
autoScannerCooldown: 2.25,
|
||||
autoScannerCooldown: 3.5,
|
||||
autoScannerUpgradeRate: 0.95,
|
||||
autoScannerMinCooldown: 0.5,
|
||||
eggSpawnRanges: [
|
||||
|
|
@ -112,7 +112,7 @@ export const BALANCE = {
|
|||
rarity: 'common',
|
||||
type: 'equipmentUpgrade',
|
||||
target: 'trash',
|
||||
description: 'Choose SHREDDER and raise it by 1 level. Bonus chance increases by the current upgrade count. Max 30 upgrades.',
|
||||
description: 'Shred poops for a little money. Max 30 upgrades.',
|
||||
tags: ['POOP', 'ACTIVE']
|
||||
},
|
||||
{
|
||||
|
|
@ -171,6 +171,48 @@ export const BALANCE = {
|
|||
description: 'Conveyor speed +7.5%.',
|
||||
tags: ['CONVEYOR', 'PASSIVE']
|
||||
},
|
||||
{
|
||||
id: 'sparePartsBin',
|
||||
title: 'SPARE PARTS BIN',
|
||||
rarity: 'common',
|
||||
type: 'instant',
|
||||
description: 'Restores 2% durability to every machine. A box of almost-compatible parts.',
|
||||
tags: ['MAINTENANCE', 'ACTIVE'],
|
||||
weightBase: 100,
|
||||
weightLossPerCopy: 2
|
||||
},
|
||||
{
|
||||
id: 'dudRefund',
|
||||
title: 'DUD REFUND',
|
||||
rarity: 'common',
|
||||
type: 'instant',
|
||||
description: 'DUDs now come with paperwork. Gain JPY 50 per copy when you pick a DUD.',
|
||||
tags: ['CARD', 'ECONOMY', 'PASSIVE']
|
||||
},
|
||||
{
|
||||
id: 'freeReroll',
|
||||
title: 'FREE REROLL',
|
||||
rarity: 'common',
|
||||
type: 'instant',
|
||||
description: 'The first bad ideas are free.',
|
||||
tags: ['CARD', 'PASSIVE']
|
||||
},
|
||||
{
|
||||
id: 'composter',
|
||||
title: 'COMPOSTER',
|
||||
rarity: 'common',
|
||||
type: 'instant',
|
||||
description: 'Turns shredder waste into suspicious fertilizer. Gain JPY 3 per copy when SHREDDER processes poop.',
|
||||
tags: ['POOP', 'ECONOMY', 'PASSIVE']
|
||||
},
|
||||
{
|
||||
id: 'scannerQueueSpacing',
|
||||
title: 'Scanner Queue Spacing',
|
||||
rarity: 'common',
|
||||
type: 'instant',
|
||||
description: 'Manual and Auto Scanner queue spacing -15%. No card limit.',
|
||||
tags: ['SCANNER', 'PASSIVE']
|
||||
},
|
||||
{
|
||||
id: 'extraEggOutlet',
|
||||
title: 'Extra Egg Outlet',
|
||||
|
|
@ -194,7 +236,7 @@ export const BALANCE = {
|
|||
rarity: 'rare',
|
||||
type: 'cellAction',
|
||||
target: 'blockedCell',
|
||||
description: 'Remove any 3 blocked cells with a burst effect.',
|
||||
description: 'Remove any 3 unbuildable cells',
|
||||
tags: ['RISK', 'RARE', 'ONE-SHOT']
|
||||
},
|
||||
{
|
||||
|
|
@ -283,6 +325,11 @@ export const BALANCE = {
|
|||
preventiveMaintenance: 0,
|
||||
dudFilter: 0,
|
||||
durabilityCoating: 0,
|
||||
sparePartsBin: 0,
|
||||
dudRefund: 0,
|
||||
freeReroll: 0,
|
||||
composter: 0,
|
||||
scannerQueueSpacing: 0,
|
||||
laborExploitation: 0,
|
||||
usedMachineActive: false,
|
||||
rescueLoanCharges: 0,
|
||||
|
|
@ -330,6 +377,10 @@ export const BALANCE = {
|
|||
id: 'conveyor', type: 'conveyor', name: 'Conveyor', shortName: 'Belt', price: 30,
|
||||
buildable: true, upgradeable: false
|
||||
},
|
||||
boostConveyor: {
|
||||
id: 'boostConveyor', type: 'conveyor', name: 'Boost Conveyor', shortName: 'BOOST BELT', price: 75,
|
||||
buildable: true, upgradeable: false, speedMultiplier: 2
|
||||
},
|
||||
eggFarm: {
|
||||
id: 'eggFarm', type: 'eggFarm', name: 'Egg Farm', shortName: 'EGG', price: 250,
|
||||
buildable: true, upgradeable: true, maxLevel: 4, upgradeCosts: [null, 300, 720, 1600]
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const EGG_SPAWN_RANGES = BALANCE.production.eggSpawnRanges;
|
|||
export const GRID = BALANCE.grid;
|
||||
export const FACILITY_DEFS = BALANCE.facilities;
|
||||
|
||||
export const BUILD_TOOL_IDS = ['conveyor', 'eggFarm', 'autoScanner', 'manualScanner', 'mixer', 'trash', 'truck'];
|
||||
export const BUILD_TOOL_IDS = ['conveyor', 'boostConveyor', 'eggFarm', 'autoScanner', 'manualScanner', 'mixer', 'trash', 'truck'];
|
||||
export const MACHINE_FACILITY_IDS = ['mixer', 'trash', 'truck'];
|
||||
export const INCOME_FACILITY_IDS = ['mixer', 'truck'];
|
||||
|
||||
|
|
|
|||
|
|
@ -33,17 +33,19 @@ export function createEggFarm(game, col, row) {
|
|||
|
||||
export function createScanner(game, col, row, kind) {
|
||||
const cost = kind === 'auto' ? FACILITY_DEFS.autoScanner.price : FACILITY_DEFS.manualScanner.price;
|
||||
const manualCount = game.scanners.filter(s => s.kind === 'manual').length;
|
||||
const usedManualSlots = new Set(game.scanners.filter(s => s.kind === 'manual').map(s => s.slot));
|
||||
let manualSlot = 0;
|
||||
while (usedManualSlots.has(manualSlot)) manualSlot += 1;
|
||||
return {
|
||||
type: 'scanner', id: game.nextId++, kind,
|
||||
slot: kind === 'manual' ? manualCount : null,
|
||||
role: manualCount % 2,
|
||||
slot: kind === 'manual' ? manualSlot : null,
|
||||
role: kind === 'manual' ? manualSlot % 2 : 0,
|
||||
col, row, level: 1,
|
||||
queue: [], cooldown: 0,
|
||||
price: cost,
|
||||
builtSession: game.buildSession,
|
||||
autoMode: 'standard',
|
||||
keys: kind === 'manual' ? manualKeysForSlot(manualCount) : null
|
||||
keys: kind === 'manual' ? manualKeysForSlot(manualSlot) : null
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ export function newTurnStats() {
|
|||
chemicalWeaponSubsidy: 0,
|
||||
rescueLoan: 0,
|
||||
cardRerollCost: 0,
|
||||
dudRefundIncome: 0,
|
||||
composterIncome: 0,
|
||||
sparePartsRepair: 0,
|
||||
mixerPoopFine: 0,
|
||||
truckPoopFine: 0,
|
||||
maleTruckFine: 0,
|
||||
|
|
@ -74,6 +77,9 @@ export function newTotalStats() {
|
|||
manualComboFailure: 0,
|
||||
repairWorkerWages: 0,
|
||||
cardRerollCost: 0,
|
||||
dudRefundIncome: 0,
|
||||
composterIncome: 0,
|
||||
sparePartsRepair: 0,
|
||||
mixerPoopFine: 0,
|
||||
truckPoopFine: 0,
|
||||
maleTruckFine: 0,
|
||||
|
|
@ -135,7 +141,7 @@ export function createGame() {
|
|||
lastExplodedComponent: new Map(),
|
||||
contractOffer: null,
|
||||
contractActive: null,
|
||||
cardEffects: { bearing: 0, legalWork: 0, fairiesFlatteryNext: 0, crowdedFarming: 0, extraEggOutlet: 0, hatchingFeed: 0, safetyCover: 0, recyclingSubsidy: 0, chemicalWeaponSubsidy: 0, preventiveMaintenance: 0, dudFilter: 0, durabilityCoating: 0, laborExploitation: 0, usedMachineActive: false, rescueLoanCharges: 0, loans: [] },
|
||||
cardEffects: { bearing: 0, legalWork: 0, fairiesFlatteryNext: 0, crowdedFarming: 0, extraEggOutlet: 0, hatchingFeed: 0, safetyCover: 0, recyclingSubsidy: 0, chemicalWeaponSubsidy: 0, preventiveMaintenance: 0, dudFilter: 0, durabilityCoating: 0, sparePartsBin: 0, dudRefund: 0, freeReroll: 0, composter: 0, scannerQueueSpacing: 0, laborExploitation: 0, usedMachineActive: false, rescueLoanCharges: 0, loans: [] },
|
||||
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
|
||||
cardTargetPick: null,
|
||||
manualCombo: { count: 0, lastBonus: 0 },
|
||||
|
|
@ -182,15 +188,15 @@ export function defaultFacilities() {
|
|||
const INITIAL_CONVEYOR_BACKBONE = Object.freeze([
|
||||
// Egg Farm -> S1 top input. The S1/S2 rows were moved down so unrelated
|
||||
// belt streams keep at least one blank tile between side-by-side flows.
|
||||
[1, 1], [2, 1], [3, 1], [4, 1], [5, 1], [6, 1], [7, 1], [8, 1], [8, 2], [8, 3],
|
||||
[1, 1], [2, 1], [3, 1], [4, 1], [5, 1], [6, 1], [7, 1], [8, 1], [8, 2],
|
||||
// S1 left output -> Mixer receiver on the left grid edge.
|
||||
[7, 4], [6, 4], [5, 4], [4, 4], [3, 4], [2, 4], [1, 4], [0, 4],
|
||||
// S1 right output -> S2 top input.
|
||||
[9, 4], [10, 4], [11, 4], [12, 4], [13, 4], [14, 4], [15, 4], [15, 5], [15, 6],
|
||||
[10, 4], [11, 4], [12, 4], [13, 4], [14, 4], [15, 4], [15, 5],
|
||||
// S2 left output -> Waste Shredder receiver on bottom grid edge.
|
||||
[14, 7], [13, 7], [12, 7], [12, 8], [12, 9],
|
||||
// S2 right output -> Truck receiver on the bottom grid edge.
|
||||
[16, 7], [17, 7], [18, 7], [18, 8], [18, 9]
|
||||
[17, 7], [18, 7], [18, 8], [18, 9]
|
||||
]);
|
||||
|
||||
const INITIAL_SCANNERS = Object.freeze([
|
||||
|
|
@ -213,13 +219,19 @@ function addConveyorTile(game, col, row, options = {}) {
|
|||
uses: 0,
|
||||
durability: BALANCE.maintenance.durability.conveyor,
|
||||
maintenanceType: 'conveyor',
|
||||
kind: options.kind || 'conveyor',
|
||||
speedMultiplier: options.speedMultiplier || 1,
|
||||
dir: options.dir || null,
|
||||
outDirs: Array.isArray(options.outDirs) ? [...options.outDirs] : []
|
||||
outDirs: Array.isArray(options.outDirs) ? [...options.outDirs] : [],
|
||||
branchMode: options.branchMode || 'random'
|
||||
});
|
||||
} else if (options.dir || options.outDirs) {
|
||||
const meta = game.conveyorMeta.get(k);
|
||||
if (options.dir) meta.dir = options.dir;
|
||||
if (Array.isArray(options.outDirs)) meta.outDirs = [...new Set([...(meta.outDirs || []), ...options.outDirs])];
|
||||
if (!meta.branchMode) meta.branchMode = 'random';
|
||||
if (!meta.kind) meta.kind = 'conveyor';
|
||||
if (!meta.speedMultiplier) meta.speedMultiplier = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -242,9 +254,20 @@ function markInitialConveyorDirection(game, from, to) {
|
|||
|
||||
function scannerPortCells(scanner) {
|
||||
return [
|
||||
{ col: scanner.col, row: scanner.row - 1 },
|
||||
{ col: scanner.col, row: scanner.row - 2 },
|
||||
{ col: scanner.col - 1, row: scanner.row },
|
||||
{ col: scanner.col + 1, row: scanner.row }
|
||||
{ col: scanner.col + 2, row: scanner.row }
|
||||
].filter(p => inGrid(p.col, p.row));
|
||||
}
|
||||
|
||||
function scannerBodyCells(scanner) {
|
||||
return [
|
||||
{ col: scanner.col, row: scanner.row - 1 },
|
||||
{ col: scanner.col + 1, row: scanner.row - 1 },
|
||||
{ col: scanner.col, row: scanner.row },
|
||||
{ col: scanner.col + 1, row: scanner.row },
|
||||
{ col: scanner.col, row: scanner.row + 1 },
|
||||
{ col: scanner.col + 1, row: scanner.row + 1 }
|
||||
].filter(p => inGrid(p.col, p.row));
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +284,7 @@ function protectedInitialCells(game) {
|
|||
const reserved = new Set([...game.conveyorTiles]);
|
||||
const add = p => { if (p && inGrid(p.col, p.row)) reserved.add(key(p.col, p.row)); };
|
||||
for (const scanner of game.scanners) {
|
||||
add(scanner);
|
||||
for (const p of scannerBodyCells(scanner)) add(p);
|
||||
for (const p of scannerPortCells(scanner)) add(p);
|
||||
}
|
||||
for (const farm of game.eggFarms) {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export function equipmentName(id) {
|
|||
export function buildToolPriceText(id) {
|
||||
const def = FACILITY_DEFS[id];
|
||||
if (!def) return '';
|
||||
if (id === 'conveyor') return `${yen(def.price)} / tile`;
|
||||
if (id === 'conveyor' || id === 'boostConveyor') return `${yen(def.price)} / tile`;
|
||||
if (id === 'autoScanner') return `${yen(def.price)} / ${AUTO_SCANNER_COOLDOWN.toFixed(1)}s cooldown`;
|
||||
if (id === 'trash') return yen(def.price);
|
||||
return yen(def.price);
|
||||
|
|
@ -65,8 +65,9 @@ export function buildToolPriceText(id) {
|
|||
export function buildToolFlavorText(id) {
|
||||
const flavors = {
|
||||
conveyor: 'A narrow green belt. Routes decide whether chicks live, ship, or become invoices.',
|
||||
boostConveyor: 'Moves chicks twice as fast. Wears down like a normal belt.',
|
||||
eggFarm: 'A tiny gatehouse producing questionable eggs on schedule.',
|
||||
autoScanner: 'An automated judge. Faster than hands, still very sure of itself.',
|
||||
autoScanner: 'Slower than hands without upgrades.',
|
||||
manualScanner: 'A manual checkpoint. The operator is the algorithm.',
|
||||
mixer: 'Male chicks become revenue here. Do not feed it poop.',
|
||||
trash: 'A polite shredder for poop and other regrets.',
|
||||
|
|
|
|||
78
src/game.js
78
src/game.js
|
|
@ -3,7 +3,7 @@ import { buildToolButtonHtml } from './core/text.js';
|
|||
import { createGame, resetLayout, newTurnStats } from './core/state.js';
|
||||
import { clamp, pointToCell, cellCenter, key, yen } from './core/utils.js';
|
||||
import { expansionLots, lotBounds } from './core/gridExpansion.js';
|
||||
import { commitFactoryGraphForDay, facilityConnectionIssues } from './systems/routing.js';
|
||||
import { commitFactoryGraphForDay, facilityConnectionIssues, scannerConnector } from './systems/routing.js';
|
||||
import { applyRevenue, collectChemicalWeaponSubsidy, collectFairiesTribute, collectLoanRepayments, collectZundaTax, settleTruckRevenue } from './systems/economy.js';
|
||||
import { undo, redo } from './systems/history.js';
|
||||
import { drawAll } from './render/draw.js';
|
||||
|
|
@ -28,12 +28,12 @@ const ui = {
|
|||
hoverTooltip: document.getElementById('hoverTooltip'),
|
||||
manualScannerMonitor: document.getElementById('manualScannerMonitor'),
|
||||
hireRepairmanButton: document.getElementById('hireRepairmanButton'),
|
||||
expandGridButton: document.getElementById('expandGridButton'),
|
||||
debug: {
|
||||
panel: document.getElementById('debugPanel'),
|
||||
infiniteCash: document.getElementById('debugInfiniteCash'),
|
||||
dayInput: document.getElementById('debugDayInput'),
|
||||
setDay: document.getElementById('debugSetDayButton'),
|
||||
zeroTimer: document.getElementById('debugZeroTimerButton'),
|
||||
cardSelect: document.getElementById('debugCardSelect'),
|
||||
grantCard: document.getElementById('debugGrantCardButton'),
|
||||
grantAllCards: document.getElementById('debugGrantAllCardsButton'),
|
||||
|
|
@ -41,7 +41,7 @@ const ui = {
|
|||
},
|
||||
buttons: {
|
||||
s1Left: document.getElementById('scanner1MixerButton'), s1Right: document.getElementById('scanner1TruckButton'), s2Left: document.getElementById('scanner2MixerButton'), s2Right: document.getElementById('scanner2TruckButton'),
|
||||
nextTurn: document.getElementById('nextTurnButton'), conveyor: document.getElementById('buildConveyorButton'), 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'), erase: document.getElementById('eraseButton'), undo: document.getElementById('undoButton'), redo: document.getElementById('redoButton')
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -49,6 +49,7 @@ const ui = {
|
|||
function initializeStaticText() {
|
||||
const buttonToolMap = {
|
||||
conveyor: 'conveyor',
|
||||
boostConveyor: 'boostConveyor',
|
||||
eggFarm: 'eggFarm',
|
||||
autoScanner: 'autoScanner',
|
||||
manualScanner: 'manualScanner',
|
||||
|
|
@ -105,9 +106,22 @@ function debugSetDay() {
|
|||
updateDebugReadout(`Day set to ${day}.`);
|
||||
}
|
||||
|
||||
function debugZeroTimer() {
|
||||
game.timeLeft = 0;
|
||||
if (game.phase === 'running') closeFarmShutters();
|
||||
floating(game, canvas.width / 2 - game.view.x, 86 - game.view.y, 'TIMER 0', THEME.warn);
|
||||
uiSystem.updatePanels();
|
||||
uiSystem.updateUI();
|
||||
updateDebugReadout('Timer set to 0.');
|
||||
}
|
||||
|
||||
function debugGrantSelectedCard() {
|
||||
const id = ui.debug.cardSelect?.value;
|
||||
const result = debugGrantCard(game, id);
|
||||
if (ui.debug.grantCard) {
|
||||
ui.debug.grantCard.textContent = result.ok ? 'Granted' : 'Grant failed';
|
||||
window.setTimeout(() => { if (ui.debug.grantCard) ui.debug.grantCard.textContent = 'Grant Card'; }, 900);
|
||||
}
|
||||
uiSystem.updatePanels();
|
||||
uiSystem.updateUI();
|
||||
updateDebugReadout(result.reason);
|
||||
|
|
@ -371,10 +385,11 @@ function resetCameraToFactoryStart() {
|
|||
game.view.y = 190 - anchor.y;
|
||||
clampCamera();
|
||||
}
|
||||
function startPan(event) { const p = rawCanvasPoint(event); ensureView(); game.pan = { start: p, viewX: game.view.x, viewY: game.view.y }; }
|
||||
function startPan(event) { const p = rawCanvasPoint(event); ensureView(); game.pan = { start: p, viewX: game.view.x, viewY: game.view.y, moved: false }; }
|
||||
function updatePan(event) {
|
||||
if (!game.pan) return;
|
||||
const p = rawCanvasPoint(event);
|
||||
if (Math.hypot(p.x - game.pan.start.x, p.y - game.pan.start.y) > 5) game.pan.moved = true;
|
||||
ensureView();
|
||||
game.view.x = game.pan.viewX + p.x - game.pan.start.x;
|
||||
game.view.y = game.pan.viewY + p.y - game.pan.start.y;
|
||||
|
|
@ -440,6 +455,19 @@ function clickSelect(event) {
|
|||
uiSystem.updatePanels();
|
||||
const obj = build.selectedObject();
|
||||
if (obj?.type === 'scanner' && obj.kind === 'manual') build.showManualScannerMenu(obj);
|
||||
if (obj?.type === 'scanner' && obj.kind === 'auto') build.showAutoScannerMenu(obj);
|
||||
}
|
||||
|
||||
function cancelBuildAction() {
|
||||
game.buildTool = null;
|
||||
game.selected = null;
|
||||
game.multiSelected = [];
|
||||
game.selectionBox = null;
|
||||
if (game.cardTargetPick?.mode === 'autoScannerMenu') game.cardTargetPick = null;
|
||||
uiSystem.hideModal();
|
||||
hideHoverTooltip();
|
||||
uiSystem.updatePanels();
|
||||
uiSystem.updateUI();
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
|
|
@ -460,6 +488,8 @@ function chickDisplay(chick) {
|
|||
function miniScannerGrid(scanner, chick) {
|
||||
const farmCells = new Set(game.eggFarms.map(farm => key(farm.col, farm.row)));
|
||||
const scannerCells = new Map(game.scanners.map(s => [key(s.col, s.row), s]));
|
||||
const inputCell = scannerConnector(scanner, 'inputA');
|
||||
const inputKey = inputCell ? key(inputCell.col, inputCell.row) : null;
|
||||
const facilityEntryCells = new Set(Object.values(game.facilities || {}).map(f => f.entry ? key(f.entry.col, f.entry.row) : null).filter(Boolean));
|
||||
const activeSex = chick?.sex || '';
|
||||
const activeLabel = activeSex === 'poop' ? '💩' : activeSex === 'male' ? '♂' : activeSex === 'female' ? '♀' : '';
|
||||
|
|
@ -473,7 +503,7 @@ function miniScannerGrid(scanner, chick) {
|
|||
classes.push('scanner');
|
||||
if (activeSex) classes.push(activeSex);
|
||||
label = activeLabel || 'S';
|
||||
} else if (row === scanner.row - 1 && col === scanner.col) {
|
||||
} else if (k === inputKey) {
|
||||
classes.push('input');
|
||||
label = 'IN';
|
||||
} else if (farmCells.has(k)) {
|
||||
|
|
@ -557,13 +587,28 @@ canvas.addEventListener('pointerdown', event => {
|
|||
const world = canvasPoint(event);
|
||||
if (event.button === 2) { startPan(event); return; }
|
||||
if (event.button !== 0) return;
|
||||
if (game.cardTargetPick?.mode === 'autoScannerMenu') { cancelBuildAction(); return; }
|
||||
if (game.cardTargetPick?.pending) { cardSystem.chooseTargetAtPoint(world); return; }
|
||||
const expansionOffer = build.expansionOfferAtPoint?.(world);
|
||||
if (expansionOffer) { build.buyGridExpansion(expansionOffer); clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); return; }
|
||||
if (game.buildTool === 'erase') { build.eraseAtPoint(world); return; }
|
||||
if (build.cycleBranchModeAtPoint?.(world)) { uiSystem.updatePanels(); uiSystem.updateUI(); return; }
|
||||
const clickedCell = pointToCell(world.x, world.y);
|
||||
const clickedHit = build.equipmentAtPoint(world);
|
||||
if (!game.buildTool && clickedHit?.type === 'conveyor' && clickedCell && build.cycleSingleConveyorDirection?.(clickedCell)) {
|
||||
uiSystem.updatePanels();
|
||||
uiSystem.updateUI();
|
||||
return;
|
||||
}
|
||||
if (game.buildTool === 'erase') {
|
||||
if (!build.equipmentAtPoint(world)) { cancelBuildAction(); return; }
|
||||
build.eraseAtPoint(world);
|
||||
return;
|
||||
}
|
||||
if (game.buildTool) {
|
||||
if (game.buildTool === 'conveyor') {
|
||||
build.beginConveyorDrag(pointToCell(world.x, world.y));
|
||||
const cell = pointToCell(world.x, world.y);
|
||||
if (!cell && !['mixer', 'trash', 'truck'].includes(game.buildTool)) { cancelBuildAction(); return; }
|
||||
if (game.buildTool === 'conveyor' || game.buildTool === 'boostConveyor') {
|
||||
build.beginConveyorDrag(cell);
|
||||
return;
|
||||
}
|
||||
const hit = build.equipmentAtPoint(world);
|
||||
|
|
@ -580,7 +625,7 @@ canvas.addEventListener('pointermove', event => {
|
|||
if (game.pan && (event.buttons & 2)) updatePan(event);
|
||||
if (game.groupDrag && (event.buttons & 1)) build.updateGroupDrag(event);
|
||||
if (game.selectionBox && (event.buttons & 1)) build.updateSelectionBox(event);
|
||||
if (game.buildTool === 'conveyor' && (event.buttons & 1)) build.continueConveyorDrag(pointToCell(canvasPoint(event).x, canvasPoint(event).y));
|
||||
if ((game.buildTool === 'conveyor' || game.buildTool === 'boostConveyor') && (event.buttons & 1)) build.continueConveyorDrag(pointToCell(canvasPoint(event).x, canvasPoint(event).y));
|
||||
if (game.buildTool === 'erase' && (event.buttons & 1)) build.eraseAtPoint(canvasPoint(event));
|
||||
});
|
||||
canvas.addEventListener('pointerup', event => {
|
||||
|
|
@ -589,7 +634,8 @@ canvas.addEventListener('pointerup', event => {
|
|||
if (!game.groupDrag.committed) clickSelect(event);
|
||||
build.finishGroupDrag();
|
||||
}
|
||||
if (game.buildTool === 'conveyor') build.endConveyorDrag?.();
|
||||
if (event.button === 2 && game.pan && !game.pan.moved && game.buildTool) cancelBuildAction();
|
||||
if (game.buildTool === 'conveyor' || game.buildTool === 'boostConveyor') build.endConveyorDrag?.();
|
||||
game.groupDrag = null; game.pan = null;
|
||||
try { canvas.releasePointerCapture(event.pointerId); } catch (_) { /* noop */ }
|
||||
});
|
||||
|
|
@ -598,14 +644,14 @@ canvas.addEventListener('pointercancel', () => { build?.endConveyorDrag?.(); gam
|
|||
document.addEventListener('pointerdown', event => {
|
||||
if (!ui.modal.classList.contains('visible') || !ui.modal.classList.contains('equipment-popover')) return;
|
||||
if (event.target.closest('#modal .modal-card')) return;
|
||||
uiSystem.hideModal();
|
||||
cancelBuildAction();
|
||||
}, 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.nextTurn.addEventListener('click', startNextTurn); ui.buttons.conveyor.addEventListener('click', () => build.setBuildTool('conveyor')); ui.buttons.eggFarm.addEventListener('click', () => build.setBuildTool('eggFarm')); ui.buttons.autoScanner.addEventListener('click', () => build.setBuildTool('autoScanner')); ui.buttons.manualScanner.addEventListener('click', () => build.setBuildTool('manualScanner')); ui.buttons.mixer.addEventListener('click', () => build.setBuildTool('mixer')); ui.buttons.trash.addEventListener('click', () => build.setBuildTool('trash')); ui.buttons.truck.addEventListener('click', () => build.setBuildTool('truck')); ui.buttons.erase.addEventListener('click', () => build.setBuildTool('erase'));
|
||||
ui.buttons.nextTurn.addEventListener('click', startNextTurn); ui.buttons.conveyor.addEventListener('click', () => build.setBuildTool('conveyor')); ui.buttons.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.hireRepairmanButton?.addEventListener('click', hireRepairman);
|
||||
ui.expandGridButton?.addEventListener('click', () => { build?.buyGridExpansion?.('right'); clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); });
|
||||
ui.debug.setDay?.addEventListener('click', debugSetDay);
|
||||
ui.debug.zeroTimer?.addEventListener('click', debugZeroTimer);
|
||||
ui.debug.grantCard?.addEventListener('click', debugGrantSelectedCard);
|
||||
ui.debug.grantAllCards?.addEventListener('click', debugGrantAllCards);
|
||||
ui.buttons.undo.addEventListener('click', () => { if (undo(game)) { clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); } }); ui.buttons.redo.addEventListener('click', () => { if (redo(game)) { clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); } });
|
||||
|
|
@ -626,7 +672,11 @@ window.addEventListener('keydown', event => {
|
|||
}
|
||||
if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && name === 'z') { event.preventDefault(); if (undo(game)) { clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); } }
|
||||
if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && (name === 'y' || (event.shiftKey && name === 'z'))) { event.preventDefault(); if (redo(game)) { clampCamera(); uiSystem.updatePanels(); uiSystem.updateUI(); } }
|
||||
if (event.key === 'Escape' && game.cardTargetPick?.pending) { event.preventDefault(); cardSystem.cancelTargetPick(); }
|
||||
if (event.key === 'Escape' && game.cardTargetPick?.pending) {
|
||||
event.preventDefault();
|
||||
if (game.cardTargetPick.mode === 'autoScannerMenu') cancelBuildAction();
|
||||
else cardSystem.cancelTargetPick();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { DIRS, GRID, THEME, EFFECT_PRIORITY, VERSION } from '../core/config.js';
|
||||
import { expansionCost, expansionLots, lotBounds, expansionButtonBounds, isOwnedCell } from '../core/gridExpansion.js';
|
||||
import { key, parseKey, pointToCell, cellCenter, mixHex, randomBetween, yen } from '../core/utils.js';
|
||||
import { getConveyorNeighbors, routeFromFarmToScanner, outputRoute, scannerCenter, scannerConnector, scannerOutputs, destinationLabel, destinationColor, scannerPortHasConveyor, disconnectedBuildWarnings } from '../systems/routing.js';
|
||||
import { key, parseKey, cellCenter, mixHex, randomBetween, yen } from '../core/utils.js';
|
||||
import { scannerCenter, scannerConnector, scannerOutputs, destinationLabel, destinationColor, scannerPortHasConveyor, disconnectedBuildWarnings } from '../systems/routing.js';
|
||||
import { upgradedMixerPrice, upgradedTruckPrice } from '../systems/economy.js';
|
||||
import { cardTargetBounds } from '../systems/cards.js';
|
||||
import { wearRatio } from '../systems/maintenance.js';
|
||||
|
|
@ -225,8 +225,10 @@ function drawConveyors(ctx, game) {
|
|||
const seen = new Set();
|
||||
for (const k of game.conveyorTiles) {
|
||||
const { col, row } = parseKey(k);
|
||||
for (const n of getConveyorNeighbors(game, col, row)) {
|
||||
for (const d of DIRS) {
|
||||
const n = { col: col + d.dc, row: row + d.dr };
|
||||
const nk = key(n.col, n.row);
|
||||
if (!game.conveyorTiles.has(nk)) continue;
|
||||
const e = [k, nk].sort().join('|');
|
||||
if (seen.has(e)) continue;
|
||||
seen.add(e);
|
||||
|
|
@ -254,74 +256,154 @@ function drawConveyors(ctx, game) {
|
|||
const directionMarkers = collectConveyorDirectionMarkers(game);
|
||||
for (const k of game.conveyorTiles) {
|
||||
const c = cellCenter(...Object.values(parseKey(k)));
|
||||
const meta = game.conveyorMeta.get(k) || {};
|
||||
const ratio = componentRatio(game, k);
|
||||
ctx.fillStyle = ratio > 0.5 ? mixHex(THEME.white, '#ffd6d6', Math.max(0, (ratio - .5) / .5)) : THEME.white;
|
||||
const baseFill = meta.kind === 'boostConveyor' ? '#fff2b8' : THEME.white;
|
||||
ctx.fillStyle = ratio > 0.5 ? mixHex(baseFill, '#ffd6d6', Math.max(0, (ratio - .5) / .5)) : baseFill;
|
||||
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 2;
|
||||
rect(ctx, c.x - 8, c.y - 8, 16, 16, true, true);
|
||||
drawDirtOverlay(ctx, c.x - 14, c.y - 14, 28, 28, { meta: game.conveyorMeta.get(k) });
|
||||
if (meta.kind === 'boostConveyor') {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = THEME.warn;
|
||||
ctx.lineWidth = 3;
|
||||
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.restore();
|
||||
}
|
||||
drawDirtOverlay(ctx, c.x - 14, c.y - 14, 28, 28, { meta });
|
||||
drawSelection(ctx, game, 'conveyor', k, c.x, c.y, 38, 38);
|
||||
drawConveyorDirectionMarkers(ctx, c, directionMarkers.get(k) || []);
|
||||
const connectedDirs = connectedConveyorDirs(game, k);
|
||||
drawBranchModeIcon(ctx, c, meta, branchSelectableEntranceDirs(game, k, connectedDirs), connectedDirs);
|
||||
if (ratio > 0.5) label(ctx, c.x, c.y - 15, `${Math.floor(ratio * 100)}%`, THEME.danger);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function cellKeyFromPoint(game, p) {
|
||||
const cell = pointToCell(p.x, p.y);
|
||||
if (!cell) return null;
|
||||
const center = cellCenter(cell.col, cell.row);
|
||||
if (Math.hypot(center.x - p.x, center.y - p.y) > GRID.cell * 0.38) return null;
|
||||
const k = key(cell.col, cell.row);
|
||||
return game.conveyorTiles.has(k) ? k : null;
|
||||
function reachableConveyorDirs(game, k, meta) {
|
||||
const p = parseKey(k);
|
||||
const neighborNames = DIRS
|
||||
.filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr)))
|
||||
.map(d => d.name);
|
||||
const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
|
||||
.filter(name => neighborNames.includes(name));
|
||||
const inferred = neighborNames;
|
||||
return explicit.length ? explicit : inferred;
|
||||
}
|
||||
function addMarkerFromRoute(game, markers, route) {
|
||||
if (!route || route.length < 2) return;
|
||||
for (let i = 0; i < route.length - 1; i += 1) {
|
||||
const a = route[i], b = route[i + 1];
|
||||
const ak = cellKeyFromPoint(game, a);
|
||||
const bk = cellKeyFromPoint(game, b);
|
||||
if (!ak || !bk || ak === bk) continue;
|
||||
const ac = parseKey(ak), bc = parseKey(bk);
|
||||
const dc = bc.col - ac.col, dr = bc.row - ac.row;
|
||||
if (Math.abs(dc) + Math.abs(dr) !== 1) continue;
|
||||
const angle = Math.atan2(dr, dc);
|
||||
if (!markers.has(ak)) markers.set(ak, []);
|
||||
const list = markers.get(ak);
|
||||
if (!list.some(x => Math.abs(Math.sin((x - angle) / 2)) < 0.01)) list.push(angle);
|
||||
|
||||
function connectedConveyorDirs(game, k) {
|
||||
const p = parseKey(k);
|
||||
return DIRS
|
||||
.filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr)))
|
||||
.map(d => d.name);
|
||||
}
|
||||
|
||||
function neighborPointsIntoCell(game, p, dirName) {
|
||||
const d = DIRS.find(item => item.name === dirName);
|
||||
if (!d) return false;
|
||||
const meta = game.conveyorMeta?.get?.(key(p.col + d.dc, p.row + d.dr));
|
||||
if (!meta) return false;
|
||||
const names = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))];
|
||||
return names.includes(d.opposite);
|
||||
}
|
||||
|
||||
function branchExitDirs(game, k, dirs, connectedDirs = connectedConveyorDirs(game, k), branchMode = 'random') {
|
||||
if (connectedDirs.length < 3) return dirs;
|
||||
const p = parseKey(k);
|
||||
const entrances = new Set(connectedDirs.filter(dir => neighborPointsIntoCell(game, p, dir)));
|
||||
if (branchMode && branchMode !== 'random' && connectedDirs.includes(branchMode)) {
|
||||
const selectedEntrances = new Set(entrances);
|
||||
selectedEntrances.add(branchMode);
|
||||
if (connectedDirs.filter(dir => !selectedEntrances.has(dir)).length >= 2) entrances.add(branchMode);
|
||||
}
|
||||
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 explicitConveyorAngles(meta) {
|
||||
|
||||
function branchSelectableEntranceDirs(game, k, connectedDirs = connectedConveyorDirs(game, k)) {
|
||||
if (connectedDirs.length < 3) return [];
|
||||
const p = parseKey(k);
|
||||
const physicalEntrances = new Set(connectedDirs.filter(dir => neighborPointsIntoCell(game, p, dir)));
|
||||
return connectedDirs.filter(dir => {
|
||||
const entrances = new Set(physicalEntrances);
|
||||
entrances.add(dir);
|
||||
return connectedDirs.filter(item => !entrances.has(item)).length >= 2;
|
||||
});
|
||||
}
|
||||
|
||||
function effectiveConveyorDirs(game, k, meta) {
|
||||
const p = parseKey(k);
|
||||
const neighborNames = DIRS
|
||||
.filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr)))
|
||||
.map(d => d.name);
|
||||
const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
|
||||
.filter(name => neighborNames.includes(name));
|
||||
const inferred = neighborNames;
|
||||
return explicit.length ? explicit : inferred;
|
||||
}
|
||||
|
||||
function drawBranchModeIcon(ctx, center, meta, dirs, connectedDirs = dirs) {
|
||||
if (connectedDirs.length < 3 || dirs.length < 2) return;
|
||||
ctx.save();
|
||||
const mode = dirs.includes(meta.branchMode) ? meta.branchMode : 'random';
|
||||
const isFixed = mode !== 'random';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.fillStyle = 'rgba(255,255,255,.9)';
|
||||
ctx.strokeStyle = isFixed ? THEME.warn : THEME.green;
|
||||
rect(ctx, center.x - 17, center.y - 17, 34, 34, true, true);
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.fillStyle = THEME.ink;
|
||||
if (mode !== 'random') {
|
||||
const d = DIRS.find(item => item.name === mode);
|
||||
drawDirectionTriangle(ctx, center.x, center.y, d?.angle || 0, 9, 6);
|
||||
} else {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(center.x - 10, center.y);
|
||||
ctx.lineTo(center.x - 2, center.y);
|
||||
ctx.lineTo(center.x + 8, center.y - 8);
|
||||
ctx.moveTo(center.x - 2, center.y);
|
||||
ctx.lineTo(center.x + 8, center.y + 8);
|
||||
ctx.stroke();
|
||||
drawDirectionTriangle(ctx, center.x + 9, center.y - 9, -Math.PI / 4, 4, 3);
|
||||
drawDirectionTriangle(ctx, center.x + 9, center.y + 9, Math.PI / 4, 4, 3);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function explicitConveyorMarkerDirs(game, k, meta) {
|
||||
if (!meta) return [];
|
||||
const names = [];
|
||||
if (Array.isArray(meta.outDirs)) names.push(...meta.outDirs);
|
||||
if (meta.dir) names.push(meta.dir);
|
||||
return [...new Set(names)]
|
||||
.map(name => DIRS.find(d => d.name === name)?.angle)
|
||||
.filter(angle => Number.isFinite(angle));
|
||||
const p = parseKey(k);
|
||||
const neighborNames = DIRS
|
||||
.filter(d => game.conveyorTiles.has(key(p.col + d.dc, p.row + d.dr)))
|
||||
.map(d => d.name);
|
||||
const explicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
|
||||
.filter(name => neighborNames.includes(name));
|
||||
const dirs = branchExitDirs(game, k, explicit.length ? explicit : effectiveConveyorDirs(game, k, meta), neighborNames, meta.branchMode || 'random');
|
||||
return dirs
|
||||
.map(name => DIRS.find(d => d.name === name))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function collectConveyorDirectionMarkers(game) {
|
||||
const markers = new Map();
|
||||
for (const [k, meta] of game.conveyorMeta || []) {
|
||||
const angles = explicitConveyorAngles(meta);
|
||||
if (angles.length) markers.set(k, angles);
|
||||
}
|
||||
for (const farm of game.eggFarms) addMarkerFromRoute(game, markers, routeFromFarmToScanner(game, farm)?.route);
|
||||
for (const scanner of game.scanners) {
|
||||
for (const side of ['left', 'right']) addMarkerFromRoute(game, markers, outputRoute(game, side, scannerCenter(scanner), scanner.id)?.route);
|
||||
const dirs = explicitConveyorMarkerDirs(game, k, meta);
|
||||
if (dirs.length) markers.set(k, dirs);
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
function drawConveyorDirectionMarkers(ctx, center, angles) {
|
||||
const limited = angles.slice(0, 3);
|
||||
function drawConveyorDirectionMarkers(ctx, center, dirs) {
|
||||
const limited = dirs.slice(0, 4);
|
||||
if (!limited.length) return;
|
||||
ctx.save();
|
||||
ctx.fillStyle = THEME.ink;
|
||||
ctx.globalAlpha = 0.86;
|
||||
limited.forEach((angle, index) => {
|
||||
const offset = (index - (limited.length - 1) / 2) * 4;
|
||||
const nx = Math.cos(angle + Math.PI / 2), ny = Math.sin(angle + Math.PI / 2);
|
||||
drawDirectionTriangle(ctx, center.x + nx * offset, center.y + ny * offset, angle, 6, 5);
|
||||
limited.forEach(d => {
|
||||
drawDirectionTriangle(ctx, center.x + d.dc * 12, center.y + d.dr * 12, d.angle, 6, 5);
|
||||
});
|
||||
ctx.restore();
|
||||
}
|
||||
|
|
@ -399,39 +481,39 @@ function drawScanner(ctx, scanner, game) {
|
|||
ctx.lineWidth = Math.min(14, 5 + Math.floor(combo / 10));
|
||||
ctx.shadowColor = ctx.strokeStyle;
|
||||
ctx.shadowBlur = Math.min(28, 8 + combo);
|
||||
rect(ctx, c.x - 60, c.y - 44, 120, 88, false, true);
|
||||
rect(ctx, c.x - 54, c.y - 76, 108, 152, false, true);
|
||||
ctx.restore();
|
||||
}
|
||||
const img = scanner.kind === 'auto' ? assets.scannerAuto : assets.scannerManual;
|
||||
if (drawImageIfLoaded(ctx, img, c.x - 54, c.y - 38, 108, 76)) {
|
||||
if (scanner.kind === 'auto') drawDirtOverlay(ctx, c.x - 54, c.y - 38, 108, 76, scanner);
|
||||
if (drawImageIfLoaded(ctx, img, c.x - 50, c.y - 73, 100, 146)) {
|
||||
if (scanner.kind === 'auto') drawDirtOverlay(ctx, c.x - 50, c.y - 73, 100, 146, scanner);
|
||||
else drawManualKeyboardIcon(ctx, scanner, c);
|
||||
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 116, 84);
|
||||
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 108, 154);
|
||||
ctx.restore(); return;
|
||||
}
|
||||
ctx.fillStyle = scanner.kind === 'auto' ? '#d8ffe2' : THEME.white;
|
||||
ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4;
|
||||
rect(ctx, c.x - 52, c.y - 36, 104, 72, true, true);
|
||||
rect(ctx, c.x - 50, c.y - 73, 100, 146, true, true);
|
||||
ctx.fillStyle = scanner.kind === 'auto' ? THEME.green : THEME.ink;
|
||||
ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center';
|
||||
ctx.fillText(`${scanner.kind === 'auto' ? 'AUTO' : `S${(scanner.slot ?? 0) + 1}`}`, c.x, c.y - 14);
|
||||
ctx.fillText(`${scanner.kind === 'auto' ? 'AUTO' : `S${(scanner.slot ?? 0) + 1}`}`, c.x, c.y - 48);
|
||||
ctx.font = '900 11px ui-monospace, monospace';
|
||||
const leftKey = scanner.keys?.left?.label || (scanner.slot === 0 ? 'A' : 'Left');
|
||||
const rightKey = scanner.keys?.right?.label || (scanner.slot === 0 ? 'D' : 'Right');
|
||||
ctx.fillText(scanner.role === 0 ? `${leftKey}:M ${rightKey}:NEXT` : `${leftKey}:WASTE ${rightKey}:TRUCK`, c.x, c.y + 3);
|
||||
ctx.fillText(scanner.role === 0 ? `${leftKey}:M ${rightKey}:NEXT` : `${leftKey}:WASTE ${rightKey}:TRUCK`, c.x, c.y - 28);
|
||||
const q = scanner.queue.length;
|
||||
if (scanner.kind === 'manual') {
|
||||
drawManualKeyboardIcon(ctx, scanner, c);
|
||||
ctx.fillStyle = q > 0 ? THEME.green : THEME.muted;
|
||||
ctx.font = '900 10px ui-monospace, monospace';
|
||||
ctx.fillText(`Q:${q}`, c.x, c.y + 32);
|
||||
ctx.fillText(`Q:${q}`, c.x, c.y + 54);
|
||||
} else {
|
||||
ctx.fillStyle = q > 0 ? THEME.green : THEME.muted;
|
||||
ctx.font = '900 12px ui-monospace, monospace';
|
||||
ctx.fillText(`Q:${q} CD:${scanner.cooldown.toFixed(1)}`, c.x, c.y + 25);
|
||||
drawDirtOverlay(ctx, c.x - 52, c.y - 36, 104, 72, scanner);
|
||||
ctx.fillText(`Q:${q} CD:${scanner.cooldown.toFixed(1)}`, c.x, c.y + 54);
|
||||
drawDirtOverlay(ctx, c.x - 50, c.y - 73, 100, 146, scanner);
|
||||
}
|
||||
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 116, 84);
|
||||
drawSelection(ctx, game, 'scanner', scanner.id, c.x, c.y, 108, 154);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
|
|
@ -445,7 +527,7 @@ function drawManualKeyboardIcon(ctx, scanner, c) {
|
|||
labels[1].text = scanner.keys?.right?.label || labels[1].text;
|
||||
const pressedSide = scanner.keyPressTime > 0 ? scanner.keyPressSide : null;
|
||||
const baseX = c.x - 30;
|
||||
const baseY = c.y + 8;
|
||||
const baseY = c.y + 18;
|
||||
ctx.save();
|
||||
ctx.lineWidth = 3;
|
||||
ctx.fillStyle = '#f7fff5';
|
||||
|
|
@ -506,9 +588,13 @@ function receiverTitle(id, game) {
|
|||
if (id === 'truck') return { title: `IN: ${targetForTruck(game).toUpperCase()}`, type: targetForTruck(game), color: THEME.truckPink };
|
||||
return { title: 'IN', type: 'female', color: THEME.green };
|
||||
}
|
||||
function drawFacilityReceiver(ctx, game, id) {
|
||||
const f = game.facilities[id];
|
||||
function facilityKind(fOrId) {
|
||||
return typeof fOrId === 'string' ? fOrId : (fOrId?.baseId || fOrId?.id || '');
|
||||
}
|
||||
function drawFacilityReceiver(ctx, game, facility) {
|
||||
const f = typeof facility === 'string' ? game.facilities[facility] : facility;
|
||||
if (!f?.entry) return;
|
||||
const id = facilityKind(f);
|
||||
const c = cellCenter(f.entry.col, f.entry.row);
|
||||
const info = receiverTitle(id, game);
|
||||
ctx.save();
|
||||
|
|
@ -527,12 +613,13 @@ function drawFacilityReceiver(ctx, game, id) {
|
|||
ctx.restore();
|
||||
}
|
||||
function drawFacilities(ctx, game) {
|
||||
if (game.facilities.mixer) drawMixer(ctx, game);
|
||||
if (game.facilities.trash) drawTrash(ctx, game);
|
||||
if (game.facilities.truck) drawTruck(ctx, game);
|
||||
drawFacilityReceiver(ctx, game, 'mixer');
|
||||
drawFacilityReceiver(ctx, game, 'trash');
|
||||
drawFacilityReceiver(ctx, game, 'truck');
|
||||
for (const f of Object.values(game.facilities || {})) {
|
||||
const kind = facilityKind(f);
|
||||
if (kind === 'mixer') drawMixer(ctx, game, f);
|
||||
else if (kind === 'trash') drawTrash(ctx, game, f);
|
||||
else if (kind === 'truck') drawTruck(ctx, game, f);
|
||||
}
|
||||
for (const f of Object.values(game.facilities || {})) drawFacilityReceiver(ctx, game, f);
|
||||
}
|
||||
function drawExternalDuct(ctx, f) {
|
||||
if (!f?.entry) return;
|
||||
|
|
@ -551,11 +638,10 @@ function drawExternalDuct(ctx, f) {
|
|||
ctx.beginPath(); ctx.moveTo(c.x, c.y); ctx.lineTo(bx, by); ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
function drawMixer(ctx, game) {
|
||||
const m = game.facilities.mixer;
|
||||
function drawMixer(ctx, game, m = game.facilities.mixer) {
|
||||
ctx.save();
|
||||
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', 'mixer', m.x + m.w / 2, m.y + m.h / 2, m.w + 12, m.h + 12); ctx.restore(); return; }
|
||||
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; }
|
||||
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4; rect(ctx, m.x, m.y, m.w, m.h, true, true);
|
||||
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.font = '900 12px ui-monospace, monospace'; ctx.fillText(`PAY ${yen(upgradedMixerPrice(game))}`, m.x + m.w / 2, m.y + 48);
|
||||
|
|
@ -563,27 +649,25 @@ function drawMixer(ctx, game) {
|
|||
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);
|
||||
drawSelection(ctx, game, 'facility', 'mixer', 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();
|
||||
}
|
||||
function drawTrash(ctx, game) {
|
||||
const t = game.facilities.trash;
|
||||
function drawTrash(ctx, game, t = game.facilities.trash) {
|
||||
ctx.save();
|
||||
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', 'trash', t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12); ctx.restore(); return; }
|
||||
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; }
|
||||
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4; rect(ctx, t.x, t.y, t.w, t.h, true, true);
|
||||
ctx.fillStyle = THEME.ink; ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`SHREDDER L${t.level}`, t.x + t.w / 2, t.y + 25);
|
||||
for (let i = 0; i < 7; i += 1) { ctx.fillRect(t.x + 34 + i * 16, t.y + 48, 7, t.h - 66); }
|
||||
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);
|
||||
drawSelection(ctx, game, 'facility', 'trash', 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();
|
||||
}
|
||||
function drawTruck(ctx, game) {
|
||||
const t = game.facilities.truck;
|
||||
function drawTruck(ctx, game, t = game.facilities.truck) {
|
||||
ctx.save();
|
||||
drawExternalDuct(ctx, t);
|
||||
if (drawImageIfLoaded(ctx, assets.truck, t.x, t.y, t.w, t.h)) { drawSelection(ctx, game, 'facility', 'truck', t.x + t.w / 2, t.y + t.h / 2, t.w + 12, t.h + 12); ctx.restore(); return; }
|
||||
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; }
|
||||
ctx.fillStyle = THEME.white; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 4; rect(ctx, t.x, t.y, t.w, t.h, true, true);
|
||||
ctx.fillStyle = THEME.ink; ctx.font = '900 19px ui-monospace, monospace'; ctx.textAlign = 'center'; ctx.fillText(`TRUCK L${t.level}`, t.x + t.w / 2, t.y + 24);
|
||||
ctx.font = '900 11px ui-monospace, monospace'; ctx.fillText(`UNIT ${yen(upgradedTruckPrice(game))}`, t.x + t.w / 2, t.y + 42);
|
||||
|
|
@ -591,7 +675,7 @@ function drawTruck(ctx, game) {
|
|||
drawTargetBadge(ctx, t.x + 15, t.y + 50, t.w - 30, `SEND ${truckTargetType.toUpperCase()}`, truckTargetType, game.contractOffer && !game.contractActive ? 'next event' : 'truck cargo');
|
||||
ctx.fillStyle = THEME.greenSoft; rect(ctx, t.x + 13, t.y + 88, t.w - 26, 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');
|
||||
drawSelection(ctx, game, 'facility', 'truck', 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();
|
||||
}
|
||||
function drawChicks(ctx, game, activeQueuedChick) {
|
||||
|
|
@ -700,7 +784,9 @@ function drawCardTargetOverlay(ctx, canvas, game) {
|
|||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = 3;
|
||||
const remaining = Math.max(1, game.cardTargetPick?.remaining || 1);
|
||||
const msg = isBlockedCellMode ? `CLICK BLOCKED CELL: ${remaining} LEFT. PRESS ESC TO CANCEL.` : 'CLICK AN UPGRADEABLE MACHINE. PRESS ESC TO CANCEL.';
|
||||
const msg = game.cardTargetPick?.mode === 'autoScannerMenu'
|
||||
? '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.';
|
||||
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.font = '900 12px ui-monospace, monospace';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { BALANCE } from '../core/balance.js';
|
||||
import { 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 { createEggFarm, createScanner, createFacility } from '../core/entities.js';
|
||||
import { generateBlockedCellsInRect } from '../core/mapGen.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 { TEXT, equipmentName } from '../core/text.js';
|
||||
import { farmAt, scannerAt, scannerCenter, routeFromFarmToScanner, refreshRoutingAfterEdit, disconnectedBuildWarnings } from './routing.js';
|
||||
import { farmAt, scannerAt, scannerCenter, scannerFootprintCells, routeFromFarmToScanner, refreshRoutingAfterEdit, disconnectedBuildWarnings } from './routing.js';
|
||||
import { buildPrice, equipmentBasePrice, incomeMultiplier, mixerPoopPenalty, refundCash, spendCash, truckPoopPenalty, upgradedMixerPrice, upgradedTruckPrice, resaleValueFor, shredderUpgradeCount, shredderBonusChance, shredderBonusMaxCards } from './economy.js';
|
||||
import { autoScannerCooldownSeconds } from './cards.js';
|
||||
import { record } from './history.js';
|
||||
|
|
@ -19,6 +19,11 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
let lastConveyorBuildCell = null;
|
||||
let conveyorDragRecordedDirectionEdit = false;
|
||||
let conveyorDragStartCell = null;
|
||||
let conveyorDragMoved = false;
|
||||
const BRANCH_MODES = ['random', 'up', 'right', 'down', 'left'];
|
||||
const BRANCH_LABELS = { random: 'RND', up: 'UP', right: 'RT', down: 'DN', left: 'LF' };
|
||||
const BRANCH_MARKER = { w: 40, h: 40, y: 0 };
|
||||
|
||||
function gridExpansionCost() { return expansionCost(game); }
|
||||
|
||||
|
|
@ -128,6 +133,20 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
return false;
|
||||
}
|
||||
|
||||
function scannerPlacementBlocked(col, row, moving = null) {
|
||||
const draft = { col, row };
|
||||
for (const cell of scannerFootprintCells(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 && !(moving?.type === 'scanner' && moving.ref?.id === existingScanner.id)) return true;
|
||||
if (game.conveyorTiles.has(k)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function selectedObject() {
|
||||
if (!game.selected) return null;
|
||||
if (game.selected.type === 'eggFarm') return game.eggFarms.find(f => f.id === game.selected.id) || null;
|
||||
|
|
@ -141,12 +160,13 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
if (!obj) return 'Equipment';
|
||||
if (obj.type === 'eggFarm') return `${equipmentName('eggFarm')} #${obj.id}`;
|
||||
if (obj.type === 'scanner') return `${obj.kind === 'auto' ? equipmentName('autoScanner') : equipmentName('manualScanner')} #${obj.id}`;
|
||||
if (obj.type === 'conveyor') return 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;
|
||||
return 'Equipment';
|
||||
}
|
||||
|
||||
function equipmentPrice(hit) {
|
||||
if (hit?.type === 'conveyor') return game.conveyorMeta?.get(hit.oldKey || hit.ref?.id)?.price || equipmentBasePrice(hit);
|
||||
return equipmentBasePrice(hit);
|
||||
}
|
||||
|
||||
|
|
@ -171,8 +191,9 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
if (hit?.type === 'eggFarm' || obj.type === 'eggFarm') {
|
||||
return game.eggFarms.length <= 1 ? 'Cannot sell the last EGG FARM.' : '';
|
||||
}
|
||||
if ((hit?.type === 'facility' || obj.type === 'facility') && ['mixer', 'trash', 'truck'].includes(obj.id)) {
|
||||
const count = Object.values(game.facilities).filter(f => f.id === obj.id).length;
|
||||
if ((hit?.type === 'facility' || obj.type === 'facility') && ['mixer', 'trash', 'truck'].includes(obj.baseId || obj.id)) {
|
||||
const kind = obj.baseId || obj.id;
|
||||
const count = Object.values(game.facilities).filter(f => (f.baseId || f.id) === kind).length;
|
||||
if (count <= 1) return `Cannot sell the last ${obj.shortName || obj.name || obj.id}.`;
|
||||
}
|
||||
return '';
|
||||
|
|
@ -193,9 +214,70 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
if (!meta) return [];
|
||||
if (!Array.isArray(meta.outDirs)) meta.outDirs = [];
|
||||
meta.outDirs = [...new Set(meta.outDirs.filter(dir => ['right', 'left', 'down', 'up'].includes(dir)))];
|
||||
if (!BRANCH_MODES.includes(meta.branchMode)) meta.branchMode = 'random';
|
||||
if (!meta.kind) meta.kind = 'conveyor';
|
||||
if (!meta.speedMultiplier) meta.speedMultiplier = meta.kind === 'boostConveyor' ? 2 : 1;
|
||||
return meta.outDirs;
|
||||
}
|
||||
|
||||
function conveyorNeighborOutDirs(cell) {
|
||||
if (!cell) return [];
|
||||
return DIRS.filter(d => game.conveyorTiles.has(key(cell.col + d.dc, cell.row + d.dr))).map(d => d.name);
|
||||
}
|
||||
|
||||
function isBranchCell(cell) {
|
||||
return conveyorNeighborOutDirs(cell).length >= 3;
|
||||
}
|
||||
|
||||
function neighborPointsIntoCell(cell, dirName) {
|
||||
const d = DIRS.find(item => item.name === dirName);
|
||||
if (!cell || !d) return false;
|
||||
const nk = key(cell.col + d.dc, cell.row + d.dr);
|
||||
const meta = game.conveyorMeta.get(nk);
|
||||
if (!meta) return false;
|
||||
const incoming = d.opposite;
|
||||
const dirs = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))];
|
||||
return dirs.includes(incoming);
|
||||
}
|
||||
|
||||
function reachableOutDirsForKey(k) {
|
||||
if (!game.conveyorTiles.has(k)) return [];
|
||||
const cell = parseKey(k);
|
||||
const meta = game.conveyorMeta.get(k);
|
||||
const explicit = normalizeOutDirs(meta);
|
||||
const neighbors = conveyorNeighborOutDirs(cell);
|
||||
const explicitReachable = [...new Set([...explicit, meta?.dir].filter(Boolean))]
|
||||
.filter(dir => neighbors.includes(dir));
|
||||
const inferredExits = neighbors;
|
||||
const reachable = explicitReachable.length ? explicitReachable : inferredExits;
|
||||
if (meta) {
|
||||
meta.outDirs = explicit.filter(dir => neighbors.includes(dir));
|
||||
if (meta.dir && !neighbors.includes(meta.dir)) meta.dir = meta.outDirs[0] || null;
|
||||
if (meta.branchMode !== 'random' && !neighbors.includes(meta.branchMode)) meta.branchMode = 'random';
|
||||
}
|
||||
return reachable;
|
||||
}
|
||||
|
||||
function branchExitDirsForKey(k) {
|
||||
if (!game.conveyorTiles.has(k)) return [];
|
||||
const cell = parseKey(k);
|
||||
const connected = conveyorNeighborOutDirs(cell);
|
||||
if (!isBranchCell(cell)) return connected.length ? connected : reachableOutDirsForKey(k);
|
||||
return connected;
|
||||
}
|
||||
|
||||
function branchSelectableEntranceDirsForKey(k) {
|
||||
const cell = parseKey(k);
|
||||
const connected = branchExitDirsForKey(k);
|
||||
if (connected.length < 3) return [];
|
||||
const physicalEntrances = new Set(connected.filter(dir => neighborPointsIntoCell(cell, dir)));
|
||||
return connected.filter(dir => {
|
||||
const entrances = new Set(physicalEntrances);
|
||||
entrances.add(dir);
|
||||
return connected.filter(item => !entrances.has(item)).length >= 2;
|
||||
});
|
||||
}
|
||||
|
||||
function markConveyorDirection(from, to) {
|
||||
const dir = directionNameBetweenCells(from, to);
|
||||
if (!dir) return false;
|
||||
|
|
@ -212,6 +294,100 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
return changed;
|
||||
}
|
||||
|
||||
function cycleSingleConveyorDirection(cell) {
|
||||
const path = conveyorRunPathFrom(cell);
|
||||
if (path.length < 2) return cycleIsolatedConveyorDirection(cell);
|
||||
record(game);
|
||||
applyRunDirection(path);
|
||||
refreshRoutingAfterEdit(game);
|
||||
updatePanels();
|
||||
return true;
|
||||
}
|
||||
|
||||
function cycleIsolatedConveyorDirection(cell) {
|
||||
if (!cell) return false;
|
||||
const k = key(cell.col, cell.row);
|
||||
const meta = game.conveyorMeta.get(k);
|
||||
if (!meta || isBranchCell(cell)) return false;
|
||||
const choices = DIRS.map(d => d.name);
|
||||
const current = meta.dir && choices.includes(meta.dir) ? meta.dir : null;
|
||||
const next = choices[(Math.max(-1, choices.indexOf(current)) + 1) % choices.length];
|
||||
record(game);
|
||||
meta.dir = next;
|
||||
meta.outDirs = [next];
|
||||
meta.branchMode = 'random';
|
||||
refreshRoutingAfterEdit(game);
|
||||
updatePanels();
|
||||
return true;
|
||||
}
|
||||
|
||||
function conveyorNeighborCells(cell) {
|
||||
if (!cell) return [];
|
||||
return DIRS
|
||||
.map(d => ({ col: cell.col + d.dc, row: cell.row + d.dr }))
|
||||
.filter(p => game.conveyorTiles.has(key(p.col, p.row)));
|
||||
}
|
||||
|
||||
function conveyorRunPathFrom(cell) {
|
||||
if (!cell || !game.conveyorTiles.has(key(cell.col, cell.row)) || isBranchCell(cell)) return [];
|
||||
const neighbors = conveyorNeighborCells(cell);
|
||||
if (!neighbors.length || neighbors.length > 2) return [];
|
||||
const walk = (first) => {
|
||||
const path = [];
|
||||
const visited = new Set([key(cell.col, cell.row)]);
|
||||
let prev = cell;
|
||||
let cur = first;
|
||||
let guard = 0;
|
||||
while (cur && guard < 200) {
|
||||
guard += 1;
|
||||
const curKey = key(cur.col, cur.row);
|
||||
if (visited.has(curKey)) break;
|
||||
visited.add(curKey);
|
||||
path.push(cur);
|
||||
if (isBranchCell(cur)) break;
|
||||
const nexts = conveyorNeighborCells(cur).filter(p => !(p.col === prev.col && p.row === prev.row));
|
||||
if (nexts.length !== 1) break;
|
||||
prev = cur;
|
||||
cur = nexts[0];
|
||||
}
|
||||
return path;
|
||||
};
|
||||
const before = neighbors[0] ? walk(neighbors[0]).reverse() : [];
|
||||
const after = neighbors[1] ? walk(neighbors[1]) : [];
|
||||
return [...before, cell, ...after];
|
||||
}
|
||||
|
||||
function directionInMeta(meta, dir) {
|
||||
return !!meta && [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))].includes(dir);
|
||||
}
|
||||
|
||||
function applyRunDirection(path) {
|
||||
let forward = 0, backward = 0;
|
||||
for (let i = 0; i < path.length - 1; i += 1) {
|
||||
const a = path[i], b = path[i + 1];
|
||||
const fwd = directionNameBetweenCells(a, b);
|
||||
const back = directionNameBetweenCells(b, a);
|
||||
if (directionInMeta(game.conveyorMeta.get(key(a.col, a.row)), fwd)) forward += 1;
|
||||
if (directionInMeta(game.conveyorMeta.get(key(b.col, b.row)), back)) backward += 1;
|
||||
}
|
||||
const target = forward > backward ? [...path].reverse() : path;
|
||||
for (let i = 0; i < target.length - 1; i += 1) {
|
||||
const from = target[i];
|
||||
if (isBranchCell(from)) continue;
|
||||
const to = target[i + 1];
|
||||
const meta = game.conveyorMeta.get(key(from.col, from.row));
|
||||
const dir = directionNameBetweenCells(from, to);
|
||||
if (!meta || !dir) continue;
|
||||
meta.dir = dir;
|
||||
meta.outDirs = [dir];
|
||||
meta.branchMode = 'random';
|
||||
}
|
||||
}
|
||||
|
||||
function conveyorToolDef() {
|
||||
return BALANCE.facilities[game.buildTool === 'boostConveyor' ? 'boostConveyor' : 'conveyor'];
|
||||
}
|
||||
|
||||
function buildConveyorCell(cell, incomingDir = null) {
|
||||
if (!cell) return { ok: false, reason: TEXT.fail.outOfGrid };
|
||||
const { col, row } = cell;
|
||||
|
|
@ -219,7 +395,9 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
if (game.conveyorTiles.has(k)) return { ok: false, exists: true };
|
||||
if (isBlockedCell(col, row)) return { ok: false, reason: 'Cannot build on blocked ground.' };
|
||||
if (farmAt(game, col, row) || scannerAt(game, col, row)) return { ok: false, reason: TEXT.fail.cellOccupied };
|
||||
const cost = buildPrice('conveyor', game);
|
||||
const toolDef = conveyorToolDef();
|
||||
const kind = toolDef.id || 'conveyor';
|
||||
const cost = buildPrice(kind, game);
|
||||
if (game.cash < cost) return { ok: false, reason: TEXT.fail.notEnoughCash };
|
||||
record(game);
|
||||
spendCash(game, cost);
|
||||
|
|
@ -231,8 +409,11 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
uses: 0,
|
||||
durability: durabilityCapFor(game, 'conveyor', quality.durabilityBaseMultiplier),
|
||||
maintenanceType: 'conveyor',
|
||||
kind,
|
||||
speedMultiplier: toolDef.speedMultiplier || 1,
|
||||
dir: incomingDir || null,
|
||||
outDirs: [],
|
||||
branchMode: 'random',
|
||||
...quality
|
||||
});
|
||||
game.selected = { type: 'conveyor', id: k };
|
||||
|
|
@ -246,6 +427,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
const exists = game.conveyorTiles.has(currentKey);
|
||||
const previous = lastConveyorBuildCell;
|
||||
if (previous && previous.col === cell.col && previous.row === cell.row) return;
|
||||
if (previous) conveyorDragMoved = true;
|
||||
|
||||
if (exists) {
|
||||
if (previous && markConveyorDirection(previous, cell)) {
|
||||
|
|
@ -280,6 +462,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function beginConveyorDrag(cell) {
|
||||
conveyorDragRecordedDirectionEdit = false;
|
||||
conveyorDragStartCell = cell ? { col: cell.col, row: cell.row } : null;
|
||||
conveyorDragMoved = false;
|
||||
lastConveyorBuildCell = null;
|
||||
handleConveyorDragCell(cell);
|
||||
}
|
||||
|
|
@ -299,6 +483,11 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
}
|
||||
|
||||
function endConveyorDrag() {
|
||||
if (!conveyorDragMoved && conveyorDragStartCell && game.conveyorTiles.has(key(conveyorDragStartCell.col, conveyorDragStartCell.row))) {
|
||||
cycleSingleConveyorDirection(conveyorDragStartCell);
|
||||
}
|
||||
conveyorDragStartCell = null;
|
||||
conveyorDragMoved = false;
|
||||
lastConveyorBuildCell = null;
|
||||
conveyorDragRecordedDirectionEdit = false;
|
||||
}
|
||||
|
|
@ -306,14 +495,14 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
function buildAtCell(cell) {
|
||||
if (!cell) return fail(TEXT.fail.outOfGrid);
|
||||
const { col, row } = cell;
|
||||
if (game.buildTool === 'conveyor') {
|
||||
if (game.buildTool === 'conveyor' || game.buildTool === 'boostConveyor') {
|
||||
const result = buildConveyorCell(cell, null);
|
||||
if (!result.ok) return fail(result.reason || TEXT.fail.cellOccupied);
|
||||
refreshRoutingAfterEdit(game);
|
||||
return;
|
||||
}
|
||||
if (isBlockedCell(col, row)) return fail('Cannot build on blocked ground.');
|
||||
if (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);
|
||||
else if (game.buildTool === 'manualScanner') buildScanner(col, row, 'manual');
|
||||
else if (game.buildTool === 'autoScanner') buildScanner(col, row, 'auto');
|
||||
|
|
@ -321,7 +510,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function buildAtPoint(p) {
|
||||
const cell = pointToCell(p.x, p.y);
|
||||
if (['conveyor', 'eggFarm', 'manualScanner', 'autoScanner'].includes(game.buildTool)) return buildAtCell(cell);
|
||||
if (['conveyor', 'boostConveyor', 'eggFarm', 'manualScanner', 'autoScanner'].includes(game.buildTool)) return buildAtCell(cell);
|
||||
if (MACHINE_FACILITY_IDS.includes(game.buildTool)) return buildFacility(p, game.buildTool);
|
||||
}
|
||||
|
||||
|
|
@ -342,6 +531,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
if (kind === 'manual' && game.scanners.filter(s => s.kind === 'manual').length >= 8) return fail('Manual Scanner limit reached (8 max).');
|
||||
const cost = kind === 'auto' ? buildPrice('autoScanner', game) : buildPrice('manualScanner', game);
|
||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||
if (scannerPlacementBlocked(col, row)) return fail(TEXT.fail.cellOccupied);
|
||||
record(game);
|
||||
spendCash(game, cost);
|
||||
const scanner = applyBuildQuality(createScanner(game, col, row, kind));
|
||||
|
|
@ -354,16 +544,18 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function buildFacility(p, id) {
|
||||
const cost = buildPrice(id, game);
|
||||
if (game.facilities[id]) return fail(TEXT.fail.facilityExists);
|
||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||
const f = applyBuildQuality(createFacility(game, id, p, cost));
|
||||
const storageId = game.facilities[id] ? `${id}:${game.nextId++}` : id;
|
||||
f.baseId = id;
|
||||
f.id = storageId;
|
||||
if (f.entry && isBlockedCell(f.entry.col, f.entry.row)) return fail('Receiver cell is blocked.');
|
||||
if (facilityOverlaps(f)) return fail(TEXT.fail.facilityOverlap);
|
||||
record(game);
|
||||
spendCash(game, cost);
|
||||
game.facilities[id] = f;
|
||||
game.facilities[storageId] = f;
|
||||
refreshRoutingAfterEdit(game);
|
||||
game.selected = { type: 'facility', id };
|
||||
game.selected = { type: 'facility', id: storageId };
|
||||
floating(game, p.x, p.y - 14, `-${yen(cost)}`, THEME.ink);
|
||||
}
|
||||
|
||||
|
|
@ -371,7 +563,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
const resale = resaleValueFor(hit, game);
|
||||
if (resale.amount <= 0) return;
|
||||
refundCash(game, resale.amount);
|
||||
const label = resale.sameBuild ? 'REFUND' : 'SOLD 50%';
|
||||
const label = resale.sameBuild ? 'REFUND' : 'SOLD';
|
||||
floating(game, x, y - 16, `${label} +${yen(resale.amount)}`, THEME.green);
|
||||
}
|
||||
|
||||
|
|
@ -461,6 +653,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
}
|
||||
|
||||
function hideModal() {
|
||||
if (game.cardTargetPick?.mode === 'autoScannerMenu') game.cardTargetPick = null;
|
||||
ui.modal.classList.remove('visible', 'equipment-popover');
|
||||
ui.modal.style.removeProperty('--popover-x');
|
||||
ui.modal.style.removeProperty('--popover-y');
|
||||
|
|
@ -468,13 +661,26 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function showAutoScannerMenu(scanner) {
|
||||
ui.modalTitle.textContent = 'Auto Scanner';
|
||||
ui.modalBody.innerHTML = `<p>Standard auto scanner.</p><ul><li>Role 0: male left, others right.</li><li>Role 1: poop left, others right.</li><li>Cooldown: ${autoScannerCooldownSeconds(scanner).toFixed(1)}s.</li></ul>`;
|
||||
game.cardTargetPick = {
|
||||
pending: true,
|
||||
mode: 'autoScannerMenu',
|
||||
cardId: null,
|
||||
targetKeys: [`scanner:${scanner.id}`],
|
||||
remaining: 1
|
||||
};
|
||||
ui.modalBody.innerHTML = `
|
||||
<div class="equipment-menu-lines compact">
|
||||
<p>Left / right output routing for this AUTO scanner.</p>
|
||||
<p>Current: ${scanner.role === 0 ? 'Male left / others right' : 'Poop left / others right'}</p>
|
||||
<p>Cooldown: ${autoScannerCooldownSeconds(scanner).toFixed(1)}s.</p>
|
||||
</div>`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
const r0 = modalButton('Set Role 0', () => { record(game); scanner.role = 0; hideModal(); updatePanels(); });
|
||||
const r1 = modalButton('Set Role 1', () => { record(game); scanner.role = 1; hideModal(); updatePanels(); });
|
||||
ui.modalActions.append(r0, r1);
|
||||
const r0 = modalButton('Male Left / Others Right', () => { record(game); scanner.role = 0; hideModal(); updatePanels(); }, scanner.role === 0 ? 'facility-action warn' : 'facility-action');
|
||||
const r1 = modalButton('Poop Left / Others Right', () => { record(game); scanner.role = 1; hideModal(); updatePanels(); }, scanner.role === 1 ? 'facility-action warn' : 'facility-action');
|
||||
ui.modalActions.append(r0, r1, modalButton('Close', hideModal, 'facility-action'));
|
||||
positionEquipmentPopover(scanner);
|
||||
ui.modal.classList.add('visible', 'equipment-popover');
|
||||
updatePanels();
|
||||
}
|
||||
|
||||
function formatKeyBinding(binding) {
|
||||
|
|
@ -507,7 +713,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
<div class="equipment-menu-lines compact">
|
||||
<p>Left route key: <b>${left}</b></p>
|
||||
<p>Right route key: <b>${right}</b></p>
|
||||
<p>Role: ${scanner.role === 0 ? 'Male left / others right' : 'Poop left / others right'}</p>
|
||||
${message ? `<p class="cash-positive">${message}</p>` : ''}
|
||||
${conflict ? `<p class="cash-negative">${conflict}</p>` : ''}
|
||||
</div>`;
|
||||
|
|
@ -589,7 +794,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
ui.modalTitle.textContent = selectedTitle(obj);
|
||||
ui.modalBody.innerHTML = `
|
||||
<div class="equipment-menu-lines compact">${lines.map(x => `<p>${x}</p>`).join('')}</div>
|
||||
${['mixer', 'truck'].includes(obj.id) ? '<p class="formula-box compact">Income/Fine = ceil(base × 1.05^upgrades)</p>' : ''}`;
|
||||
${['mixer', 'truck'].includes(obj.baseId || obj.id) ? '<p class="formula-box compact">Income/Fine = ceil(base x 1.10^upgrades)</p>' : ''}`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
if (obj.type === 'scanner' && obj.kind === 'auto') ui.modalActions.appendChild(modalButton('Auto Menu', () => showAutoScannerMenu(obj), 'facility-action warn'));
|
||||
if (obj.type === 'scanner' && obj.kind === 'manual') ui.modalActions.appendChild(modalButton('Set Keys', () => showManualScannerMenu(obj), 'facility-action warn'));
|
||||
|
|
@ -625,14 +830,68 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
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 farm of game.eggFarms) { const c = cellCenter(farm.col, farm.row); boxes.push({ id: `eggFarm:${farm.id}`, price: equipmentPrice({ type: 'eggFarm', ref: farm }), x: c.x - 24, y: c.y - 24, w: 48, h: 48 }); }
|
||||
for (const scanner of game.scanners) { const c = scannerCenter(scanner); boxes.push({ id: `scanner:${scanner.id}`, price: equipmentPrice({ type: 'scanner', ref: scanner }), x: c.x - 52, y: c.y - 36, w: 104, h: 72 }); }
|
||||
for (const k of game.conveyorTiles) { const p = parseKey(k); const c = cellCenter(p.col, p.row); boxes.push({ id: `conveyor:${k}`, price: buildPrice('conveyor', game), x: c.x - 22, y: c.y - 22, w: 44, h: 44 }); }
|
||||
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) {
|
||||
const p = parseKey(k);
|
||||
const c = cellCenter(p.col, p.row);
|
||||
const meta = game.conveyorMeta.get(k) || {};
|
||||
boxes.push({ id: `conveyor:${k}`, price: meta.price || buildPrice(meta.kind === 'boostConveyor' ? 'boostConveyor' : 'conveyor', game), x: c.x - 22, y: c.y - 22, w: 44, h: 44 });
|
||||
}
|
||||
return boxes;
|
||||
}
|
||||
|
||||
function branchModeLabel(mode) {
|
||||
return BRANCH_LABELS[mode] || BRANCH_LABELS.random;
|
||||
}
|
||||
|
||||
function branchOutDirsForKey(k) {
|
||||
return branchExitDirsForKey(k);
|
||||
}
|
||||
|
||||
function isBranchKey(k) {
|
||||
if (!game.conveyorTiles.has(k)) return false;
|
||||
return isBranchCell(parseKey(k));
|
||||
}
|
||||
|
||||
function branchSwitcherAtPoint(p) {
|
||||
if (game.phase !== 'build' || game.cardDraft?.pending || game.cardTargetPick?.pending) return null;
|
||||
for (const k of game.conveyorTiles) {
|
||||
if (!isBranchKey(k)) continue;
|
||||
const cell = parseKey(k);
|
||||
const c = cellCenter(cell.col, cell.row);
|
||||
const left = c.x - BRANCH_MARKER.w / 2;
|
||||
const top = c.y + BRANCH_MARKER.y - BRANCH_MARKER.h / 2;
|
||||
if (p.x >= left && p.x <= left + BRANCH_MARKER.w && p.y >= top && p.y <= top + BRANCH_MARKER.h) return k;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function cycleBranchModeAtPoint(p) {
|
||||
const k = branchSwitcherAtPoint(p);
|
||||
if (!k) return false;
|
||||
cycleBranchModeForKey(k);
|
||||
return true;
|
||||
}
|
||||
|
||||
function cycleBranchModeForKey(k) {
|
||||
const meta = game.conveyorMeta.get(k);
|
||||
if (!meta) return false;
|
||||
const reachable = branchSelectableEntranceDirsForKey(k);
|
||||
if (reachable.length < 2) return false;
|
||||
record(game);
|
||||
const current = BRANCH_MODES.includes(meta.branchMode) ? meta.branchMode : 'random';
|
||||
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];
|
||||
game.selected = null;
|
||||
game.multiSelected = [];
|
||||
updatePanels();
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectedInfoLines(obj) {
|
||||
ensureMaintenanceState(game);
|
||||
const lines = [];
|
||||
const facilityKind = obj?.type === 'facility' ? (obj.baseId || obj.id) : '';
|
||||
if (obj.type === 'eggFarm') {
|
||||
const [min, max] = getSpawnRange(obj);
|
||||
lines.push(`Price: ${yen(equipmentPrice(obj))}`);
|
||||
|
|
@ -644,7 +903,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
lines.push(`Price: ${yen(equipmentPrice(obj))}`);
|
||||
lines.push(`Sale value: ${yen(resaleValueFor({ type: obj.type, ref: obj }, game).amount)}`);
|
||||
lines.push(`Type: ${obj.kind.toUpperCase()} / Standard`);
|
||||
lines.push(`Role: ${obj.role === 0 ? 'Male left / Others right' : 'Poop left / Others right'}`);
|
||||
if (obj.kind === 'auto') lines.push(`Role: ${obj.role === 0 ? 'Male left / Others right' : 'Poop left / Others right'}`);
|
||||
lines.push(`Queue: ${obj.queue.length}`);
|
||||
if (obj.kind === 'auto') lines.push(`Cooldown: ${obj.cooldown.toFixed(1)}s / ${autoScannerCooldownSeconds(obj, game).toFixed(1)}s`);
|
||||
if (obj.kind === 'auto') lines.push(`Durability: ${remainingPercent(obj)}% | Delay x${autoScannerDelayMultiplier(obj).toFixed(2)}`);
|
||||
|
|
@ -656,25 +915,31 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
const c = comp ? game.congestion.get(comp) : null;
|
||||
lines.push(`Price: ${yen(equipmentPrice({ type: 'conveyor', oldKey: obj.id, ref: obj }))}`);
|
||||
lines.push(`Cell: ${obj.id}`);
|
||||
lines.push(`Type: ${meta?.kind === 'boostConveyor' ? 'BOOST BELT' : 'Normal Belt'} | Speed x${Number(meta?.speedMultiplier || 1).toFixed(1)}`);
|
||||
lines.push(`Congestion: ${c ? Math.floor(c.ratio * 100) : 0}%`);
|
||||
lines.push(`Durability: ${remainingPercent({ meta })}% | Speed x${performanceFactor({ meta }).toFixed(2)}`);
|
||||
lines.push('Flow follows drawn directions. Branches choose randomly; full branches are avoided when possible.');
|
||||
if (isBranchKey(obj.id)) {
|
||||
lines.push(`Branch mode: ${branchModeLabel(meta?.branchMode || 'random')}`);
|
||||
lines.push('Use the Branch button below or the on-belt branch badge.');
|
||||
} else {
|
||||
lines.push('Branch mode: needs 3+ conveyor connections.');
|
||||
}
|
||||
lines.push(`Sale value: ${yen(resaleValueFor({ type: 'conveyor', oldKey: obj.id, ref: obj }, game).amount)}`);
|
||||
} else if (obj.type === 'facility') {
|
||||
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)}`); }
|
||||
lines.push(['mixer', 'truck', 'trash'].includes(obj.id) ? `Level: ${obj.level} / no cap` : `Level: ${obj.level}`);
|
||||
if (obj.id === 'truck') lines.push('Durability: none | Always normal');
|
||||
if (['mixer', 'trash'].includes(obj.id)) lines.push(`Durability: ${remainingPercent(obj)}% | Extra delay ${facilityProcessingDelay(game, obj.id).toFixed(2)}s`);
|
||||
if (obj.id === 'mixer') {
|
||||
if (facilityKind === 'truck') lines.push('Durability: none | Always normal');
|
||||
if (['mixer', 'trash'].includes(facilityKind)) lines.push(`Durability: ${remainingPercent(obj)}% | Extra delay ${facilityProcessingDelay(game, obj).toFixed(2)}s`);
|
||||
if (facilityKind === 'mixer') {
|
||||
lines.push(`Income: ${yen(upgradedMixerPrice(game))} per chick | upgrade x${incomeMultiplier(game, 'mixer').toFixed(3)}`);
|
||||
lines.push(`Poop fine: -${yen(mixerPoopPenalty(game))}`);
|
||||
}
|
||||
if (obj.id === 'truck') {
|
||||
if (facilityKind === 'truck') {
|
||||
lines.push(`Income: ${yen(upgradedTruckPrice(game))} per target cargo | upgrade x${incomeMultiplier(game, 'truck').toFixed(3)}`);
|
||||
lines.push(`Poop shipment fine: -${yen(truckPoopPenalty(game))}`);
|
||||
}
|
||||
if (obj.id === 'trash') {
|
||||
if (facilityKind === 'trash') {
|
||||
const cards = shredderUpgradeCount(game);
|
||||
const chance = shredderBonusChance(cards);
|
||||
lines.push(`Bonus cards: ${cards}/${shredderBonusMaxCards()}`);
|
||||
|
|
@ -696,13 +961,14 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function flavorText(obj) {
|
||||
if (!obj) return '';
|
||||
if (obj.type === 'conveyor' && game.conveyorMeta.get(obj.id)?.kind === 'boostConveyor') return 'Moves chicks twice as fast. Wears down like a normal belt.';
|
||||
if (obj.type === 'conveyor') return 'A stubborn belt tile. It only cares about the next cell.';
|
||||
if (obj.type === 'eggFarm') return 'A tiny gatehouse producing questionable eggs on schedule.';
|
||||
if (obj.type === 'scanner' && obj.kind === 'auto') return 'An automated judge. Faster than hands, still very sure of itself.';
|
||||
if (obj.type === 'scanner' && obj.kind === 'auto') return 'Slower than hands without upgrades.';
|
||||
if (obj.type === 'scanner') return 'A manual checkpoint. The operator is the algorithm.';
|
||||
if (obj.type === 'facility' && obj.id === 'mixer') return 'Male chicks become revenue here. Do not feed it poop.';
|
||||
if (obj.type === 'facility' && obj.id === 'trash') return 'Poop goes in. Sometimes coins come out, for reasons best left unaudited.';
|
||||
if (obj.type === 'facility' && obj.id === 'truck') return 'The shipping endpoint. Correct cargo pays; wrong cargo complains.';
|
||||
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) === 'truck') return 'The shipping endpoint. Correct cargo pays; wrong cargo complains.';
|
||||
return 'Factory equipment.';
|
||||
}
|
||||
|
||||
|
|
@ -718,6 +984,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
selectedInfoLines, flavorText, disconnectedWarningFor,
|
||||
gridExpansionCost, buyGridExpansion, expansionOfferAtPoint,
|
||||
beginConveyorDrag, continueConveyorDrag, endConveyorDrag,
|
||||
cycleSingleConveyorDirection,
|
||||
branchSwitcherAtPoint, cycleBranchModeAtPoint, cycleBranchModeForKey,
|
||||
removeSelected, switchScannerRole, showManualScannerMenu,
|
||||
showAutoScannerMenu, showSelectedMenu, setBuildTool, fail,
|
||||
isEquipmentCell, equipmentHitBoxes,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { nextSpawnDelay } from '../core/state.js';
|
|||
import { cellCenter, key, yen } from '../core/utils.js';
|
||||
import { applyPenalty, applyRevenue, upgradedMixerPrice, upgradedTruckPrice, shredderUpgradeCount, rawShredderUpgradeCount, shredderBonusChance, shredderBonusMaxCards } from './economy.js';
|
||||
import { floating, shake, eraseEffect, shockwave, sparkBurst, smokeBurst } from './effects.js';
|
||||
import { averageConveyorPerformance, autoScannerDelayMultiplier, ensureMaintenanceState } from './maintenance.js';
|
||||
import { averageConveyorPerformance, autoScannerDelayMultiplier, ensureMaintenanceState, repairAllEquipment } from './maintenance.js';
|
||||
import { scannerCenter, refreshRoutingAfterEdit } from './routing.js';
|
||||
|
||||
const COMMON_WEIGHT = CARD_BALANCE.commonWeight;
|
||||
|
|
@ -31,6 +31,7 @@ export function ensureCardState(game) {
|
|||
if (game.cardEffects.drawBonus != null) delete game.cardEffects.drawBonus;
|
||||
if (!game.cardDraft) game.cardDraft = { pending: false, choices: [], rerolls: 0, picksRemaining: 0 };
|
||||
if (game.cardDraft.picksRemaining == null) game.cardDraft.picksRemaining = game.cardDraft.pending ? 1 : 0;
|
||||
if (game.cardDraft.freeRerolls == null) game.cardDraft.freeRerolls = 0;
|
||||
return game.cardEffects;
|
||||
}
|
||||
|
||||
|
|
@ -44,11 +45,16 @@ export function extraEggOutletCount(_game, farm = null) {
|
|||
return Math.min(3, Math.max(0, Number(farm?.extraEggOutlet) || 0));
|
||||
}
|
||||
|
||||
export function scannerQueueSpacingMultiplier(game) {
|
||||
return Math.pow(0.85, effectCount(game, 'scannerQueueSpacing'));
|
||||
}
|
||||
|
||||
export function conveyorSpeedForGame(game, conveyorKey = null) {
|
||||
const effects = ensureCardState(game);
|
||||
const speed = CONVEYOR_SPEED * Math.pow(BEARING_SPEED_MULTIPLIER, Math.max(0, effects.bearing || 0));
|
||||
const beltMultiplier = conveyorKey ? Math.max(0.1, Number(game?.conveyorMeta?.get?.(conveyorKey)?.speedMultiplier) || 1) : 1;
|
||||
const perf = conveyorKey ? 1 : averageConveyorPerformance(game);
|
||||
return Math.min(CONVEYOR_SPEED_MAX, speed * perf);
|
||||
return Math.min(CONVEYOR_SPEED_MAX, speed * perf) * beltMultiplier;
|
||||
}
|
||||
|
||||
export function autoScannerCooldownSeconds(scanner, game = null) {
|
||||
|
|
@ -112,7 +118,7 @@ function targetLabel(game, target) {
|
|||
if (target.type === 'scanner') {
|
||||
return `AUTO #${target.id} L${target.level} -> L${target.level + 1} | CD ${autoScannerCooldownSeconds(target, game).toFixed(1)}s -> ${autoScannerCooldownSeconds({ ...target, level: target.level + 1 }, game).toFixed(1)}s`;
|
||||
}
|
||||
if (target.type === 'facility' && target.id === 'mixer') {
|
||||
if (target.type === 'facility' && (target.baseId || target.id) === 'mixer') {
|
||||
const beforeLevel = target.level;
|
||||
const before = upgradedMixerPrice(game);
|
||||
target.level = beforeLevel + 1;
|
||||
|
|
@ -120,7 +126,7 @@ function targetLabel(game, target) {
|
|||
target.level = beforeLevel;
|
||||
return `MIXER L${target.level} -> L${target.level + 1} | ${yen(before)} -> ${yen(after)}`;
|
||||
}
|
||||
if (target.type === 'facility' && target.id === 'truck') {
|
||||
if (target.type === 'facility' && (target.baseId || target.id) === 'truck') {
|
||||
const beforeLevel = target.level;
|
||||
const before = upgradedTruckPrice(game);
|
||||
target.level = beforeLevel + 1;
|
||||
|
|
@ -155,9 +161,11 @@ export function targetsForCard(game, cardOrId) {
|
|||
.filter(f => Math.max(0, Number(f.extraEggOutlet) || 0) < 3)
|
||||
.map(f => ({ type: 'eggOutlet', id: f.id, col: f.col, row: f.row, farm: f }));
|
||||
if (card.target === 'autoScanner') return game.scanners.filter(s => s.kind === 'auto');
|
||||
if (card.target === 'mixer') return game.facilities.mixer ? [game.facilities.mixer] : [];
|
||||
if (card.target === 'truck') return game.facilities.truck ? [game.facilities.truck] : [];
|
||||
if (card.target === 'trash') return game.facilities.trash && rawShredderUpgradeCount(game) < shredderBonusMaxCards() ? [game.facilities.trash] : [];
|
||||
if (card.target === 'mixer') return Object.values(game.facilities || {}).filter(f => (f.baseId || f.id) === 'mixer');
|
||||
if (card.target === 'truck') return Object.values(game.facilities || {}).filter(f => (f.baseId || f.id) === 'truck');
|
||||
if (card.target === 'trash') return rawShredderUpgradeCount(game) < shredderBonusMaxCards()
|
||||
? Object.values(game.facilities || {}).filter(f => (f.baseId || f.id) === 'trash')
|
||||
: [];
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -190,17 +198,22 @@ function availableCards(game) {
|
|||
});
|
||||
}
|
||||
|
||||
function rarityWeight(card) {
|
||||
function cardWeight(game, card) {
|
||||
if (card.weightBase != null) {
|
||||
const held = effectCount(game, card.id);
|
||||
return Math.max(0, Number(card.weightBase) - Math.max(0, Number(card.weightLossPerCopy) || 0) * held);
|
||||
}
|
||||
if (card.rarity === 'ultraRare') return ULTRA_RARE_WEIGHT;
|
||||
if (card.rarity === 'rare') return RARE_WEIGHT;
|
||||
return COMMON_WEIGHT;
|
||||
}
|
||||
|
||||
function weightedPick(pool) {
|
||||
const total = pool.reduce((sum, card) => sum + rarityWeight(card), 0);
|
||||
function weightedPick(game, pool) {
|
||||
const total = pool.reduce((sum, card) => sum + cardWeight(game, card), 0);
|
||||
if (total <= 0) return pool[Math.floor(Math.random() * pool.length)];
|
||||
let roll = Math.random() * total;
|
||||
for (const card of pool) {
|
||||
roll -= rarityWeight(card);
|
||||
roll -= cardWeight(game, card);
|
||||
if (roll <= 0) return card;
|
||||
}
|
||||
return pool[pool.length - 1];
|
||||
|
|
@ -247,7 +260,7 @@ export function dealCards(game, count = BASE_DRAFT_SIZE) {
|
|||
let pool = [...source];
|
||||
while (choices.length < count && source.length) {
|
||||
if (!pool.length) pool = [...source];
|
||||
const picked = weightedPick(pool);
|
||||
const picked = weightedPick(game, pool);
|
||||
choices.push(picked);
|
||||
const i = pool.findIndex(card => card.id === picked.id);
|
||||
if (i >= 0) pool.splice(i, 1);
|
||||
|
|
@ -277,7 +290,7 @@ function boundsForTarget(target) {
|
|||
}
|
||||
if (target.type === 'scanner') {
|
||||
const c = scannerCenter(target);
|
||||
return { x: c.x - 58, y: c.y - 42, w: 116, h: 84, cx: c.x, cy: c.y };
|
||||
return { x: c.x - 54, y: c.y - 77, w: 108, h: 154, cx: c.x, cy: c.y };
|
||||
}
|
||||
if (target.type === 'facility') {
|
||||
return { x: target.x, y: target.y, w: target.w, h: target.h, cx: target.x + target.w / 2, cy: target.y + target.h / 2 };
|
||||
|
|
@ -287,6 +300,13 @@ function boundsForTarget(target) {
|
|||
|
||||
export function cardTargetBounds(game) {
|
||||
if (!game.cardTargetPick?.pending) return [];
|
||||
if (game.cardTargetPick.mode === 'autoScannerMenu') {
|
||||
const allowed = new Set(game.cardTargetPick.targetKeys || []);
|
||||
return game.scanners
|
||||
.filter(scanner => scanner.kind === 'auto' && allowed.has(targetKey(scanner)))
|
||||
.map(scanner => ({ key: targetKey(scanner), label: 'AUTO ROUTE SETTINGS', target: scanner, bounds: boundsForTarget(scanner) }))
|
||||
.filter(item => item.bounds);
|
||||
}
|
||||
const allowed = new Set(game.cardTargetPick.targetKeys || []);
|
||||
return targetsForCard(game, game.cardTargetPick.cardId)
|
||||
.filter(target => allowed.has(targetKey(target)))
|
||||
|
|
@ -344,6 +364,17 @@ function applyInstantCard(game, card) {
|
|||
if (card.id === 'preventiveMaintenance') inc('preventiveMaintenance');
|
||||
if (card.id === 'dudFilter') inc('dudFilter');
|
||||
if (card.id === 'durabilityCoating') { inc('durabilityCoating'); ensureMaintenanceState(game); }
|
||||
if (card.id === 'sparePartsBin') {
|
||||
inc('sparePartsBin');
|
||||
const repaired = repairAllEquipment(game, 0.02);
|
||||
game.stats.sparePartsRepair = (game.stats.sparePartsRepair || 0) + repaired;
|
||||
game.totals.sparePartsRepair = (game.totals.sparePartsRepair || 0) + repaired;
|
||||
floating(game, GRID.x + GRID.cols * GRID.cell / 2, GRID.y - 30, 'REPAIR +2%', THEME.green);
|
||||
}
|
||||
if (card.id === 'dudRefund') inc('dudRefund');
|
||||
if (card.id === 'freeReroll') inc('freeReroll');
|
||||
if (card.id === 'composter') inc('composter');
|
||||
if (card.id === 'scannerQueueSpacing') inc('scannerQueueSpacing');
|
||||
if (card.id === 'flattery') inc('fairiesFlatteryNext');
|
||||
if (card.id === 'usedMachine') effects.usedMachineActive = true;
|
||||
if (card.id === 'newMachine') effects.usedMachineActive = false;
|
||||
|
|
@ -374,6 +405,18 @@ function incomeAtLevel(base, level) {
|
|||
return Math.ceil(base * Math.pow(ECONOMY.incomeUpgradeRate, Math.max(0, level - 1)));
|
||||
}
|
||||
|
||||
function incomeUpgradePreview(game, id, base) {
|
||||
const targets = targetsForCard(game, id === 'mixer' ? 'upgradeMixer' : 'upgradeTruck');
|
||||
const target = targets[0];
|
||||
if (!target) return { before: incomeAtLevel(base, 1), after: incomeAtLevel(base, 2) };
|
||||
const before = id === 'mixer' ? upgradedMixerPrice(game) : upgradedTruckPrice(game);
|
||||
const beforeLevel = target.level || 1;
|
||||
target.level = beforeLevel + 1;
|
||||
const after = id === 'mixer' ? upgradedMixerPrice(game) : upgradedTruckPrice(game);
|
||||
target.level = beforeLevel;
|
||||
return { before, after };
|
||||
}
|
||||
|
||||
function cardDescription(game, card) {
|
||||
const e = ensureCardState(game);
|
||||
if (card.id === 'upgradeEgg') {
|
||||
|
|
@ -388,12 +431,12 @@ function cardDescription(game, card) {
|
|||
return `Choose one AUTO SCANNER. Level +1. Cooldown ${before.toFixed(2)}s -> ${after.toFixed(2)}s.`;
|
||||
}
|
||||
if (card.id === 'upgradeMixer') {
|
||||
const level = game.facilities?.mixer?.level || 1;
|
||||
return `MIXER level +1. Male-chick income ${yen(incomeAtLevel(ECONOMY.income.mixer, level))} -> ${yen(incomeAtLevel(ECONOMY.income.mixer, level + 1))}.`;
|
||||
const preview = incomeUpgradePreview(game, 'mixer', ECONOMY.income.mixer);
|
||||
return `Choose one MIXER. Earning +10%. Male-chick income ${yen(preview.before)} -> ${yen(preview.after)}.`;
|
||||
}
|
||||
if (card.id === 'upgradeTruck') {
|
||||
const level = game.facilities?.truck?.level || 1;
|
||||
return `TRUCK level +1. Correct shipment income ${yen(incomeAtLevel(ECONOMY.income.truck, level))} -> ${yen(incomeAtLevel(ECONOMY.income.truck, level + 1))}.`;
|
||||
const preview = incomeUpgradePreview(game, 'truck', ECONOMY.income.truck);
|
||||
return `Choose one TRUCK. Earning +10%. Correct shipment income ${yen(preview.before)} -> ${yen(preview.after)}.`;
|
||||
}
|
||||
if (card.id === 'upgradeTrash') {
|
||||
const before = shredderUpgradeCount(game);
|
||||
|
|
@ -432,6 +475,11 @@ function cardDescription(game, card) {
|
|||
return `Held ${held}. DUD chance per card ${formatPercent(before)} -> ${formatPercent(after)}.`;
|
||||
}
|
||||
if (card.id === 'durabilityCoating') return `Held ${e.durabilityCoating || 0}. After pick: maximum durability ${formatPercent(Math.pow(1.03, (e.durabilityCoating || 0) + 1))}.`;
|
||||
if (card.id === 'sparePartsBin') return `Held ${e.sparePartsBin || 0}. Immediately restores 2% durability to every maintained machine. Offer weight: ${Math.max(0, 100 - 2 * Math.max(0, e.sparePartsBin || 0))}.`;
|
||||
if (card.id === 'dudRefund') return `Held ${e.dudRefund || 0}. After pick: DUD clicks award ${yen(50 * ((e.dudRefund || 0) + 1))}.`;
|
||||
if (card.id === 'freeReroll') return `Held ${e.freeReroll || 0}. After pick: each card screen starts with ${(e.freeReroll || 0) + 1} free reroll${(e.freeReroll || 0) + 1 === 1 ? '' : 's'}.`;
|
||||
if (card.id === 'composter') return `Held ${e.composter || 0}. After pick: SHREDDER poop grants ${yen(3 * ((e.composter || 0) + 1))}.`;
|
||||
if (card.id === 'scannerQueueSpacing') return `Held ${e.scannerQueueSpacing || 0}. After pick: Manual/Auto Scanner queue spacing ${formatPercent(Math.pow(0.85, (e.scannerQueueSpacing || 0) + 1))}.`;
|
||||
if (card.id === 'flattery') return `Next Fairies tribute -50%. Held for next tribute: ${e.fairiesFlatteryNext || 0}.`;
|
||||
if (card.id === 'usedMachine') return 'Enable Used Machines: future equipment costs 50%, has no SELL refund, and starts with 60% durability.';
|
||||
if (card.id === 'newMachine') return 'Cancel Used Machines. Future equipment returns to normal price, refund, and durability.';
|
||||
|
|
@ -456,7 +504,7 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
function prepareDraft() {
|
||||
ensureCardState(game);
|
||||
game.cardTargetPick = null;
|
||||
game.cardDraft = { pending: true, choices: dealCards(game, BASE_DRAFT_SIZE), rerolls: 0, picksRemaining: 1 };
|
||||
game.cardDraft = { pending: true, choices: dealCards(game, BASE_DRAFT_SIZE), rerolls: 0, freeRerolls: effectCount(game, 'freeReroll'), picksRemaining: 1 };
|
||||
}
|
||||
|
||||
function finishDraft() {
|
||||
|
|
@ -496,6 +544,13 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
}
|
||||
|
||||
function chooseDud(card, buttonEl) {
|
||||
const refund = effectCount(game, 'dudRefund') * 50;
|
||||
if (refund > 0) {
|
||||
const amount = applyRevenue(game, refund);
|
||||
game.stats.dudRefundIncome = (game.stats.dudRefundIncome || 0) + amount;
|
||||
game.totals.dudRefundIncome = (game.totals.dudRefundIncome || 0) + amount;
|
||||
floating(game, GRID.x + GRID.cols * GRID.cell / 2, GRID.y - 30, `DUD REFUND +${yen(amount)}`, THEME.green);
|
||||
}
|
||||
shake(game, 34, 0.62);
|
||||
if (document.body?.animate) {
|
||||
document.body.animate([
|
||||
|
|
@ -566,8 +621,7 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
const card = cardById(game.cardTargetPick.cardId);
|
||||
const target = targetAtPoint(game, p);
|
||||
if (!card || !target) {
|
||||
const label = game.cardTargetPick.mode === 'blockedCell' ? 'SELECT BLOCKED CELL' : 'SELECT UPGRADE TARGET';
|
||||
floating(game, p.x, p.y - 18, label, THEME.danger);
|
||||
cancelTargetPick();
|
||||
return true;
|
||||
}
|
||||
if (card.id === 'dynamite') {
|
||||
|
|
@ -587,6 +641,14 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
}
|
||||
|
||||
function doReroll() {
|
||||
const free = Math.max(0, game.cardDraft?.freeRerolls || 0);
|
||||
if (free > 0) {
|
||||
game.cardDraft.freeRerolls = free - 1;
|
||||
game.cardDraft.rerolls += 1;
|
||||
redrawChoices(BASE_DRAFT_SIZE);
|
||||
showDraft();
|
||||
return;
|
||||
}
|
||||
const cost = rerollCost(game);
|
||||
if (game.cash < cost) return;
|
||||
applyPenalty(game, cost);
|
||||
|
|
@ -617,6 +679,7 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
if (!game.cardDraft.choices?.length) redrawChoices(BASE_DRAFT_SIZE);
|
||||
const choices = game.cardDraft.choices || [];
|
||||
const cost = rerollCost(game);
|
||||
const free = Math.max(0, game.cardDraft?.freeRerolls || 0);
|
||||
const remaining = Math.max(1, game.cardDraft.picksRemaining || 1);
|
||||
ui.modalTitle.textContent = 'Choose Upgrade Card';
|
||||
ui.modalBody.innerHTML = `<p class="muted-card-note">Choose ${remaining} card${remaining === 1 ? '' : 's'} before Build phase. Dud cards can be flicked away without consuming a pick. Equipment cards target machines; Dynamite targets blocked cells.</p><div id="cardChoices" class="card-choices"></div>`;
|
||||
|
|
@ -626,11 +689,18 @@ export function createCardSystem({ game, ui, onUpdatePanels }) {
|
|||
const reroll = document.createElement('button');
|
||||
reroll.type = 'button';
|
||||
reroll.className = 'facility-action warn reroll-button';
|
||||
reroll.textContent = `Reroll ${yen(cost)}`;
|
||||
reroll.disabled = game.cash < cost;
|
||||
reroll.title = reroll.disabled ? 'Not enough cash' : `Reroll count today: ${(game.cardDraft.rerolls || 0) + 1}`;
|
||||
reroll.textContent = free > 0 ? `Reroll (FREE x${free})` : `Reroll ${yen(cost)}`;
|
||||
reroll.disabled = free <= 0 && game.cash < cost;
|
||||
reroll.title = free > 0 ? `${free} free reroll${free === 1 ? '' : 's'} remaining.` : (reroll.disabled ? 'Not enough cash' : '');
|
||||
reroll.addEventListener('click', doReroll);
|
||||
ui.modalActions.appendChild(reroll);
|
||||
const skip = document.createElement('button');
|
||||
skip.type = 'button';
|
||||
skip.className = 'facility-action';
|
||||
skip.textContent = 'No Card';
|
||||
skip.title = 'Skip this upgrade pick and enter Build phase.';
|
||||
skip.addEventListener('click', finishDraft);
|
||||
ui.modalActions.appendChild(skip);
|
||||
ui.modal.classList.remove('equipment-popover');
|
||||
ui.modal.classList.add('visible');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { nextSpawnDelay } from '../core/state.js';
|
|||
import { key, parseKey, pointToCell, cellCenter, randomBetween, yen, inGrid } from '../core/utils.js';
|
||||
import { scannerById, scannerBySlot, scannerCenter, scannerConnector, nearestConveyorKey, buildConveyorComponents, autoSideFor, ensureFactoryGraph } from './routing.js';
|
||||
import { applyMixerIncome, applyMixerPoopFine, applyTruckPoopFine, applyWrongTruckFine, upgradedTruckPrice, applyShredderBonus, applyRevenue } from './economy.js';
|
||||
import { autoScannerCooldownSeconds, eggProductionDelayMultiplier, extraEggOutletCount } from './cards.js';
|
||||
import { autoScannerCooldownSeconds, eggProductionDelayMultiplier, extraEggOutletCount, scannerQueueSpacingMultiplier } from './cards.js';
|
||||
import { productionMultiplier, truckTarget, isTargetTruckCargo, shouldFineMaleTruck } from './contracts.js';
|
||||
import { floating, shake, spawnPulse, scannerPulse, meatEffect, sludgeEffect, shredEffect, truckLoadEffect, shockwave, sparkBurst, smokeBurst, flyingDebris, rageEffect } from './effects.js';
|
||||
import { countAutoSorted, countCorrect, countMistake, countMixer, countPoopDestination, countPoopSpawned, countTrash, countTruckCargo } from './stats.js';
|
||||
|
|
@ -104,7 +104,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
}
|
||||
updateScannerQueues(dt);
|
||||
updateCongestion();
|
||||
if (game.timeLeft > 0 || game.shutdownTimeLeft > 0) checkCongestionExplosions();
|
||||
checkCongestionExplosions();
|
||||
if (game.timeLeft <= 0) {
|
||||
if (game.chicks.length === 0) completeTurn();
|
||||
else if (game.shutdownTimeLeft <= 0) blowOffRemainingForCleanup();
|
||||
|
|
@ -172,7 +172,14 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
const dirs = [];
|
||||
if (Array.isArray(meta.outDirs)) dirs.push(...meta.outDirs);
|
||||
if (meta.dir) dirs.push(meta.dir);
|
||||
return [...new Set(dirs)].filter(dir => DIRS.some(d => d.name === dir));
|
||||
const explicit = [...new Set(dirs)].filter(dir => DIRS.some(d => d.name === dir));
|
||||
const cell = parseKey(cellKey);
|
||||
const inferred = DIRS
|
||||
.filter(d => movementOptionFromDir(cell, d.name))
|
||||
.map(d => d.name);
|
||||
const cleanExplicit = explicit.filter(dir => inferred.includes(dir));
|
||||
const inferredExits = inferred;
|
||||
return cleanExplicit.length ? cleanExplicit : inferredExits;
|
||||
}
|
||||
|
||||
function dirByName(name) {
|
||||
|
|
@ -180,7 +187,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
}
|
||||
|
||||
function scannerAtBodyCell(col, row) {
|
||||
return game.scanners.find(scanner => scanner.col === col && scanner.row === row) || null;
|
||||
return game.scanners.find(scanner => scanner.col <= col && col <= scanner.col + 1 && scanner.row - 1 <= row && row <= scanner.row + 1) || null;
|
||||
}
|
||||
|
||||
function scannerReceivingFrom(cell, dirName) {
|
||||
|
|
@ -196,6 +203,10 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
return Object.entries(game.facilities || {}).find(([, f]) => f?.entry && f.entry.col === cell.col && f.entry.row === cell.row) || null;
|
||||
}
|
||||
|
||||
function facilityKind(f) {
|
||||
return f?.baseId || f?.id || '';
|
||||
}
|
||||
|
||||
function dirExitsToFacility(facility, dirName) {
|
||||
return (facility.side === 'left' && dirName === 'left')
|
||||
|| (facility.side === 'right' && dirName === 'right')
|
||||
|
|
@ -219,7 +230,41 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
return null;
|
||||
}
|
||||
|
||||
function pickMovementOption(chick, cell, dirs) {
|
||||
function neighborPointsIntoCell(cell, dirName) {
|
||||
const d = dirByName(dirName);
|
||||
if (!d) return false;
|
||||
const meta = game.conveyorMeta?.get?.(key(cell.col + d.dc, cell.row + d.dr));
|
||||
if (!meta) return false;
|
||||
const names = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))];
|
||||
return names.includes(d.opposite);
|
||||
}
|
||||
|
||||
function conveyorNeighborDirNames(cell) {
|
||||
return DIRS
|
||||
.filter(d => inGrid(cell.col + d.dc, cell.row + d.dr) && game.conveyorTiles.has(key(cell.col + d.dc, cell.row + d.dr)))
|
||||
.map(d => d.name);
|
||||
}
|
||||
|
||||
function branchExitDirNames(cell, dirs, branchMode = 'random') {
|
||||
const connected = conveyorNeighborDirNames(cell);
|
||||
if (connected.length < 3) return dirs;
|
||||
const entrances = new Set(connected.filter(dir => neighborPointsIntoCell(cell, dir)));
|
||||
if (branchMode && branchMode !== 'random' && branchMode !== 'pass' && connected.includes(branchMode)) {
|
||||
const selectedEntrances = new Set(entrances);
|
||||
selectedEntrances.add(branchMode);
|
||||
if (connected.filter(dir => !selectedEntrances.has(dir)).length >= 2) entrances.add(branchMode);
|
||||
}
|
||||
const exits = connected.filter(dir => !entrances.has(dir));
|
||||
if (exits.length) return exits;
|
||||
const fallback = dirs.filter(dir => !entrances.has(dir));
|
||||
return fallback.length ? fallback : dirs;
|
||||
}
|
||||
|
||||
function optionAvailableForBranch(chick, opt) {
|
||||
return opt && (opt.type === 'facility' || !targetBlockedByChick(chick, opt.target));
|
||||
}
|
||||
|
||||
function pickMovementOption(chick, cell, dirs, branchMode = 'random') {
|
||||
const candidates = dirs.map(dir => movementOptionFromDir(cell, dir)).filter(Boolean);
|
||||
if (!candidates.length) return null;
|
||||
const moving = candidates.filter(opt => opt.type !== 'facility');
|
||||
|
|
@ -242,24 +287,27 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
if (!cell) return removeChick(index, 'OFF GRID');
|
||||
const currentKey = key(cell.col, cell.row);
|
||||
if (!game.conveyorTiles.has(currentKey)) return removeChick(index, 'OFF BELT');
|
||||
const dirs = conveyorOutDirNames(currentKey);
|
||||
const meta = game.conveyorMeta?.get(currentKey) || {};
|
||||
const dirs = branchExitDirNames(cell, conveyorOutDirNames(currentKey), meta.branchMode || 'random');
|
||||
if (!dirs.length) {
|
||||
chick.stoppedTimer = 0.25;
|
||||
return;
|
||||
}
|
||||
const option = pickMovementOption(chick, cell, dirs);
|
||||
const option = pickMovementOption(chick, cell, dirs, meta.branchMode || 'random');
|
||||
if (!option) {
|
||||
chick.stoppedTimer = 0.25;
|
||||
return;
|
||||
}
|
||||
if (option.type === 'facility') {
|
||||
if (option.facilityId === 'mixer') resolveMixer(index);
|
||||
else if (option.facilityId === 'truck') resolveTruck(index);
|
||||
else if (option.facilityId === 'trash') resolveTrash(index);
|
||||
const kind = facilityKind(option.facility);
|
||||
if (kind === 'mixer') resolveMixer(index, option.facility);
|
||||
else if (kind === 'truck') resolveTruck(index, option.facility);
|
||||
else if (kind === 'trash') resolveTrash(index, option.facility);
|
||||
else removeChick(index, 'DONE');
|
||||
return;
|
||||
}
|
||||
if (option.type === 'scanner') chick.pendingScannerId = option.scanner.id;
|
||||
chick.prevConveyorCell = currentKey;
|
||||
setSingleSegmentRoute(chick, option.target);
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +366,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
|
||||
function positionScannerQueue(scanner) {
|
||||
scanner.queue = scanner.queue.filter(id => game.chicks.some(ch => ch.id === id && ch.stage === 'queued'));
|
||||
const spacing = GRID.cell * 0.86;
|
||||
const spacing = GRID.cell * 0.86 * scannerQueueSpacingMultiplier(game);
|
||||
scanner.queue.forEach((id, idx) => {
|
||||
const chick = game.chicks.find(ch => ch.id === id);
|
||||
if (!chick) return;
|
||||
|
|
@ -497,8 +545,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
return chick;
|
||||
}
|
||||
|
||||
function resolveMixer(index) {
|
||||
const mixer = game.facilities.mixer;
|
||||
function resolveMixer(index, mixer = game.facilities.mixer) {
|
||||
if (mixer?.processingCooldown > 0) {
|
||||
const chick = game.chicks[index];
|
||||
if (chick) chick.stoppedTimer = 0.25;
|
||||
|
|
@ -508,8 +555,8 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
if (!chick) return;
|
||||
const { x, y } = chick;
|
||||
countMixer(game);
|
||||
recordFacilityProcess(game, 'mixer');
|
||||
setFacilityCooldownAfterProcess(game, 'mixer');
|
||||
recordFacilityProcess(game, mixer);
|
||||
setFacilityCooldownAfterProcess(game, mixer);
|
||||
if (chick.sex === 'poop') {
|
||||
const penalty = applyMixerPoopFine(game);
|
||||
countPoopDestination(game, 'mixer');
|
||||
|
|
@ -525,11 +572,11 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
floating(game, x, y - 18, `+${yen(amount)}`, THEME.green);
|
||||
}
|
||||
|
||||
function resolveTruck(index) {
|
||||
function resolveTruck(index, truck = game.facilities.truck) {
|
||||
const chick = takeChick(index);
|
||||
if (!chick) return;
|
||||
const { x, y } = chick;
|
||||
addTruckCargo(chick.sex);
|
||||
addTruckCargo(chick.sex, truck);
|
||||
truckLoadEffect(game, x, y, chick.sex);
|
||||
countTruckCargo(game, chick.sex);
|
||||
const target = truckTarget(game);
|
||||
|
|
@ -567,8 +614,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
}
|
||||
}
|
||||
|
||||
function resolveTrash(index) {
|
||||
const trash = game.facilities.trash;
|
||||
function resolveTrash(index, trash = game.facilities.trash) {
|
||||
if (trash?.processingCooldown > 0) {
|
||||
const chick = game.chicks[index];
|
||||
if (chick) chick.stoppedTimer = 0.25;
|
||||
|
|
@ -579,13 +625,20 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
const { x, y } = chick;
|
||||
shredEffect(game, x, y, chick.sex);
|
||||
countTrash(game);
|
||||
recordFacilityProcess(game, 'trash');
|
||||
setFacilityCooldownAfterProcess(game, 'trash');
|
||||
recordFacilityProcess(game, trash);
|
||||
setFacilityCooldownAfterProcess(game, trash);
|
||||
const bonus = applyShredderBonus(game);
|
||||
const bonusText = bonus.amount > 0 ? ` +${yen(bonus.amount)}` : '';
|
||||
if (chick.sex === 'poop') {
|
||||
countPoopDestination(game, 'trash');
|
||||
countCorrect(game);
|
||||
const compost = Math.max(0, Number(game.cardEffects?.composter) || 0) * 3;
|
||||
if (compost > 0) {
|
||||
const compostAmount = applyRevenue(game, compost);
|
||||
game.stats.composterIncome = (game.stats.composterIncome || 0) + compostAmount;
|
||||
game.totals.composterIncome = (game.totals.composterIncome || 0) + compostAmount;
|
||||
floating(game, x, y - 36, `COMPOST +${yen(compostAmount)}`, THEME.green);
|
||||
}
|
||||
floating(game, x, y - 18, `CLEAN${bonusText}`, bonus.amount > 0 ? THEME.green : THEME.green);
|
||||
} else {
|
||||
countMistake(game);
|
||||
|
|
@ -599,8 +652,8 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
floating(game, chick.x, chick.y - 12, label, THEME.muted);
|
||||
}
|
||||
|
||||
function addTruckCargo(sex) {
|
||||
const t = game.facilities.truck;
|
||||
function addTruckCargo(sex, truck = game.facilities.truck) {
|
||||
const t = truck || game.facilities.truck;
|
||||
if (!t) return;
|
||||
game.truckCargo.push({ sex, x: randomBetween(22, t.w - 22), y: randomBetween(66, t.h - 24) });
|
||||
if (game.truckCargo.length > 45) game.truckCargo.shift();
|
||||
|
|
@ -642,6 +695,8 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
}
|
||||
}
|
||||
if (comp.ratio >= 1) {
|
||||
const victims = chicksInComponent(comp.id);
|
||||
if (!victims.length || victims.some(chick => chick.stage !== 'queued' && chick.stoppedTimer <= 0.2)) continue;
|
||||
const last = game.lastExplodedComponent.get(comp.id) || 0;
|
||||
const now = performance.now();
|
||||
if (now - last > 900) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function explosionDamageMultiplier(game) {
|
|||
export function equipmentBasePrice(objOrHit) {
|
||||
const obj = objOrHit?.ref || objOrHit || {};
|
||||
const type = objOrHit?.type || obj.type;
|
||||
if (type === 'conveyor') return FACILITY_DEFS.conveyor.price;
|
||||
if (type === 'conveyor') return obj.price || FACILITY_DEFS[obj.kind]?.price || FACILITY_DEFS.conveyor.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 === 'facility') return obj.price || FACILITY_DEFS[obj.id]?.price || 0;
|
||||
|
|
@ -34,7 +34,10 @@ export function buildPrice(id, game = null) {
|
|||
}
|
||||
|
||||
export function facilityUpgradeCount(game, id) {
|
||||
return Math.max(0, (game.facilities?.[id]?.level || 1) - 1);
|
||||
if (!game?.facilities) return 0;
|
||||
return Object.values(game.facilities)
|
||||
.filter(f => (f?.baseId || f?.id) === id)
|
||||
.reduce((sum, f) => sum + Math.max(0, (f.level || 1) - 1), 0);
|
||||
}
|
||||
|
||||
export function incomeMultiplier(game, id) {
|
||||
|
|
|
|||
|
|
@ -64,10 +64,11 @@ export function restore(game, text) {
|
|||
game.conveyorTiles = new Set(data.conveyorTiles || []);
|
||||
game.conveyorMeta = new Map(data.conveyorMeta || []);
|
||||
game.branchCounters = new Map(data.branchCounters || []);
|
||||
game.cardEffects = data.cardEffects || { bearing: 0, legalWork: 0, flattery: 0 };
|
||||
game.cardEffects = data.cardEffects || { bearing: 0, legalWork: 0, fairiesFlatteryNext: 0, scannerQueueSpacing: 0 };
|
||||
if (game.cardEffects.drawBonus != null) delete game.cardEffects.drawBonus;
|
||||
game.cardDraft = data.cardDraft || { pending: false, choices: [], rerolls: 0, picksRemaining: 0 };
|
||||
if (game.cardDraft.picksRemaining == null) game.cardDraft.picksRemaining = game.cardDraft.pending ? 1 : 0;
|
||||
if (game.cardDraft.freeRerolls == null) game.cardDraft.freeRerolls = 0;
|
||||
game.cardTargetPick = data.cardTargetPick || null;
|
||||
game.groupDrag = null;
|
||||
game.selectionBox = null;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ export function ensureMaintenanceState(game) {
|
|||
for (const [k, meta] of game.conveyorMeta || []) {
|
||||
if (meta.uses == null) meta.uses = 0;
|
||||
if (meta.maintenanceType == null) meta.maintenanceType = 'conveyor';
|
||||
if (!meta.kind) meta.kind = 'conveyor';
|
||||
if (!meta.speedMultiplier) meta.speedMultiplier = meta.kind === 'boostConveyor' ? 2 : 1;
|
||||
if (!meta.branchMode) meta.branchMode = 'random';
|
||||
const desired = durabilityCapFor(game, 'conveyor', qualityMultiplierOf(meta));
|
||||
if (meta.durability == null || meta.durability < desired) meta.durability = desired;
|
||||
}
|
||||
|
|
@ -138,9 +141,20 @@ export function eggSpawnDelayMultiplier(farm) {
|
|||
return delayMultiplier(farm);
|
||||
}
|
||||
|
||||
export function facilityProcessingDelay(game, id) {
|
||||
function facilityKind(target) {
|
||||
return target?.baseId || target?.id || target;
|
||||
}
|
||||
|
||||
function facilityFromTarget(game, target) {
|
||||
if (!target) return null;
|
||||
if (typeof target === 'object') return target;
|
||||
return game.facilities?.[target] || null;
|
||||
}
|
||||
|
||||
export function facilityProcessingDelay(game, target) {
|
||||
ensureMaintenanceState(game);
|
||||
const f = game.facilities?.[id];
|
||||
const f = facilityFromTarget(game, target);
|
||||
const id = facilityKind(f || target);
|
||||
if (!f || !['mixer', 'trash'].includes(id)) return 0;
|
||||
attachMaintenance(f, id);
|
||||
const t = Math.max(0, delayMultiplier(f) - 1);
|
||||
|
|
@ -169,25 +183,26 @@ export function recordAutoScan(game, scanner) {
|
|||
scanner.maintenance.uses = Math.min(scanner.maintenance.durability, (scanner.maintenance.uses || 0) + degradationUseMultiplier(game));
|
||||
}
|
||||
|
||||
export function recordFacilityProcess(game, id) {
|
||||
export function recordFacilityProcess(game, target) {
|
||||
ensureMaintenanceState(game);
|
||||
const f = game.facilities?.[id];
|
||||
const f = facilityFromTarget(game, target);
|
||||
const id = facilityKind(f || target);
|
||||
if (!f || !['mixer', 'trash'].includes(id)) return;
|
||||
attachMaintenance(f, id);
|
||||
f.maintenance.uses = Math.min(f.maintenance.durability, (f.maintenance.uses || 0) + degradationUseMultiplier(game));
|
||||
}
|
||||
|
||||
export function setFacilityCooldownAfterProcess(game, id) {
|
||||
const f = game.facilities?.[id];
|
||||
export function setFacilityCooldownAfterProcess(game, target) {
|
||||
const f = facilityFromTarget(game, target);
|
||||
if (!f) return 0;
|
||||
const delay = facilityProcessingDelay(game, id);
|
||||
const delay = facilityProcessingDelay(game, f);
|
||||
f.processingCooldown = Math.max(f.processingCooldown || 0, delay);
|
||||
return delay;
|
||||
}
|
||||
|
||||
export function updateProcessingCooldowns(game, dt) {
|
||||
for (const id of ['mixer', 'trash']) {
|
||||
const f = game.facilities?.[id];
|
||||
for (const f of Object.values(game.facilities || {})) {
|
||||
if (!['mixer', 'trash'].includes(f.baseId || f.id)) continue;
|
||||
if (f?.processingCooldown > 0) f.processingCooldown = Math.max(0, f.processingCooldown - dt);
|
||||
}
|
||||
}
|
||||
|
|
@ -195,7 +210,7 @@ export function updateProcessingCooldowns(game, dt) {
|
|||
export function equipmentMaintenanceTargets(game) {
|
||||
ensureMaintenanceState(game);
|
||||
const targets = [];
|
||||
for (const [k, meta] of game.conveyorMeta || []) targets.push({ type: 'conveyor', key: k, meta, label: `Belt ${k}`, center: cellCenter(parseKey(k).col, parseKey(k).row) });
|
||||
for (const [k, meta] of game.conveyorMeta || []) targets.push({ type: 'conveyor', key: k, meta, label: `${meta.kind === 'boostConveyor' ? 'Boost Belt' : 'Belt'} ${k}`, center: cellCenter(parseKey(k).col, parseKey(k).row) });
|
||||
for (const farm of game.eggFarms || []) targets.push({ type: 'eggFarm', ref: farm, label: `EGG #${farm.id}`, center: cellCenter(farm.col, farm.row) });
|
||||
for (const scanner of (game.scanners || []).filter(s => s.kind === 'auto')) targets.push({ type: 'autoScanner', ref: scanner, label: `AUTO #${scanner.id}`, center: cellCenter(scanner.col, scanner.row) });
|
||||
for (const id of ['mixer', 'trash']) {
|
||||
|
|
@ -211,6 +226,18 @@ function targetDurability(t) { return t.meta ? durabilityOf(t) : durabilityOf(t.
|
|||
function reduceTargetUses(t, amount) { setUses(t.meta ? t : t.ref, Math.max(0, targetUses(t) - amount)); }
|
||||
function targetKey(t) { return t.type === 'conveyor' ? `conveyor:${t.key}` : `${t.type}:${t.ref?.id || t.ref?.type || t.type}`; }
|
||||
|
||||
export function repairAllEquipment(game, percent = 0.02) {
|
||||
ensureMaintenanceState(game);
|
||||
const pct = Math.max(0, Number(percent) || 0);
|
||||
let repaired = 0;
|
||||
for (const t of equipmentMaintenanceTargets(game)) {
|
||||
const before = targetUses(t);
|
||||
reduceTargetUses(t, targetDurability(t) * pct);
|
||||
repaired += Math.max(0, before - targetUses(t)) / targetDurability(t) * 100;
|
||||
}
|
||||
return repaired;
|
||||
}
|
||||
|
||||
export function hireRepairmanForNextDay(game) {
|
||||
ensureMaintenanceState(game);
|
||||
if (game.repairman.hiredForNextDay) return { ok: false, reason: 'Repairman already hired.' };
|
||||
|
|
|
|||
|
|
@ -5,11 +5,29 @@ import { key, parseKey, inGrid, cellCenter, distance, sameCell } from '../core/u
|
|||
// -----------------------------------------------------------------------------
|
||||
// Object lookup
|
||||
// -----------------------------------------------------------------------------
|
||||
export function scannerCenter(scanner) { return cellCenter(scanner.col, scanner.row); }
|
||||
export function scannerFootprintCells(scanner) {
|
||||
if (!scanner) return [];
|
||||
return [
|
||||
{ col: scanner.col, row: scanner.row - 1 },
|
||||
{ col: scanner.col + 1, row: scanner.row - 1 },
|
||||
{ col: scanner.col, row: scanner.row },
|
||||
{ col: scanner.col + 1, row: scanner.row },
|
||||
{ col: scanner.col, row: scanner.row + 1 },
|
||||
{ col: scanner.col + 1, row: scanner.row + 1 }
|
||||
];
|
||||
}
|
||||
export function scannerCenter(scanner) {
|
||||
const c = cellCenter(scanner.col, scanner.row);
|
||||
return { x: c.x + GRID.cell / 2, y: c.y };
|
||||
}
|
||||
export function farmAt(game, col, row) { return game.eggFarms.find(f => f.col === col && f.row === row) || null; }
|
||||
export function scannerAt(game, col, row) { return game.scanners.find(s => s.col === col && s.row === row) || null; }
|
||||
export function scannerAt(game, col, row) { return game.scanners.find(s => scannerFootprintCells(s).some(p => p.col === col && p.row === row)) || null; }
|
||||
export function scannerById(game, id) { return game.scanners.find(s => s.id === id) || null; }
|
||||
export function scannerBySlot(game, slot) { return game.scanners.find(s => s.kind === 'manual' && s.slot === slot) || null; }
|
||||
function facilityKind(f) { return f?.baseId || f?.id || ''; }
|
||||
function facilityEntriesForKind(game, kind) {
|
||||
return Object.entries(game.facilities || {}).filter(([, f]) => facilityKind(f) === kind);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Port definitions. Ports prefer the exact connector cell, but also accept
|
||||
|
|
@ -17,10 +35,10 @@ export function scannerBySlot(game, slot) { return game.scanners.find(s => s.kin
|
|||
// -----------------------------------------------------------------------------
|
||||
export function scannerConnector(scanner, type) {
|
||||
return {
|
||||
inputA: { col: scanner.col, row: scanner.row - 1 },
|
||||
inputA: { col: scanner.col, row: scanner.row - 2 },
|
||||
inputB: null,
|
||||
left: { col: scanner.col - 1, row: scanner.row },
|
||||
right: { col: scanner.col + 1, row: scanner.row }
|
||||
right: { col: scanner.col + 2, row: scanner.row }
|
||||
}[type];
|
||||
}
|
||||
|
||||
|
|
@ -54,21 +72,23 @@ function graphPortConveyorCells(game, point, blocked = [], preferred = []) {
|
|||
// -----------------------------------------------------------------------------
|
||||
export function scannerInputCells(game, scanner) {
|
||||
const connector = scannerConnector(scanner, 'inputA');
|
||||
return visualPortConveyorCells(game, connector, [ { col: scanner.col, row: scanner.row } ]);
|
||||
if (!connector || !inGrid(connector.col, connector.row)) return [];
|
||||
return game.conveyorTiles.has(key(connector.col, connector.row)) ? [{ ...connector, viaTolerance: false }] : [];
|
||||
}
|
||||
|
||||
function outputStartCells(game, scanner, side) {
|
||||
const connector = scannerConnector(scanner, side);
|
||||
return visualPortConveyorCells(game, connector, [ { col: scanner.col, row: scanner.row } ]);
|
||||
return visualPortConveyorCells(game, connector, scannerFootprintCells(scanner));
|
||||
}
|
||||
|
||||
export function facilityEntryPoint(game, dest) {
|
||||
const f = game.facilities[dest];
|
||||
const f = game.facilities[dest] || facilityEntriesForKind(game, dest)[0]?.[1];
|
||||
if (!f) return null;
|
||||
if (f.entry) return cellCenter(f.entry.col, f.entry.row);
|
||||
if (dest === 'mixer') return { x: f.x + f.w, y: f.y + f.h * 0.52 };
|
||||
if (dest === 'truck') return { x: f.x, y: f.y + f.h * 0.52 };
|
||||
if (dest === 'trash') return { x: f.x + f.w / 2, y: f.y + 8 };
|
||||
const kind = facilityKind(f) || dest;
|
||||
if (kind === 'mixer') return { x: f.x + f.w, y: f.y + f.h * 0.52 };
|
||||
if (kind === 'truck') return { x: f.x, y: f.y + f.h * 0.52 };
|
||||
if (kind === 'trash') return { x: f.x + f.w / 2, y: f.y + 8 };
|
||||
return { x: f.x + f.w / 2, y: f.y + f.h / 2 };
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +122,37 @@ function conveyorOutDirNames(game, cellKey) {
|
|||
return [...new Set(dirs)].filter(dir => DIRS.some(d => d.name === dir));
|
||||
}
|
||||
|
||||
function neighborPointsIntoCell(game, p, dirName) {
|
||||
const d = DIRS.find(item => item.name === dirName);
|
||||
if (!d) return false;
|
||||
const meta = game.conveyorMeta?.get?.(graphNodeKey(p.col + d.dc, p.row + d.dr));
|
||||
if (!meta) return false;
|
||||
const names = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))];
|
||||
return names.includes(d.opposite);
|
||||
}
|
||||
|
||||
function inferredConveyorOutDirNames(game, cellKey) {
|
||||
const p = graphCell(cellKey);
|
||||
const meta = game.conveyorMeta?.get?.(cellKey) || {};
|
||||
const neighbors = DIRS
|
||||
.filter(d => game.conveyorTiles.has(graphNodeKey(p.col + d.dc, p.row + d.dr)))
|
||||
.map(d => d.name);
|
||||
const explicit = conveyorOutDirNames(game, cellKey).filter(dir => neighbors.includes(dir));
|
||||
const inferred = neighbors;
|
||||
const dirs = explicit.length ? explicit : inferred;
|
||||
if (neighbors.length < 3) return dirs;
|
||||
const entrances = new Set(neighbors.filter(dir => neighborPointsIntoCell(game, p, dir)));
|
||||
if (meta.branchMode && meta.branchMode !== 'random' && neighbors.includes(meta.branchMode)) {
|
||||
const selectedEntrances = new Set(entrances);
|
||||
selectedEntrances.add(meta.branchMode);
|
||||
if (neighbors.filter(dir => !selectedEntrances.has(dir)).length >= 2) entrances.add(meta.branchMode);
|
||||
}
|
||||
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) {
|
||||
return {
|
||||
version: game.routingVersion || 0,
|
||||
|
|
@ -140,8 +191,8 @@ export function buildFactoryGraph(game) {
|
|||
|
||||
for (const k of graph.cells) {
|
||||
const p = graphCell(k);
|
||||
const explicitDirs = conveyorOutDirNames(game, k);
|
||||
const dirs = explicitDirs.length ? DIRS.filter(d => explicitDirs.includes(d.name)) : DIRS;
|
||||
const outDirs = inferredConveyorOutDirNames(game, k);
|
||||
const dirs = DIRS.filter(d => outDirs.includes(d.name));
|
||||
for (const d of dirs) {
|
||||
const nk = graphNodeKey(p.col + d.dc, p.row + d.dr);
|
||||
if (graph.cells.has(nk)) graph.adjacency.get(k).push({ key: nk, dir: d.name });
|
||||
|
|
@ -191,8 +242,10 @@ function indexGraphPorts(game, graph) {
|
|||
const ports = {};
|
||||
for (const type of ['inputA', 'left', 'right']) {
|
||||
const cell = scannerConnector(scanner, type);
|
||||
const cells = graphPortConveyorCells(game, cell, [ { col: scanner.col, row: scanner.row } ])
|
||||
.filter(p => graph.cells.has(graphNodeKey(p.col, p.row)));
|
||||
const cells = type === 'inputA'
|
||||
? (cell && graph.cells.has(graphNodeKey(cell.col, cell.row)) ? [{ ...cell }] : [])
|
||||
: graphPortConveyorCells(game, cell, scannerFootprintCells(scanner))
|
||||
.filter(p => graph.cells.has(graphNodeKey(p.col, p.row)));
|
||||
const cellKeys = cells.map(p => graphNodeKey(p.col, p.row));
|
||||
const connected = cellKeys.length > 0;
|
||||
ports[type] = { type, cell, cells, keys: cellKeys, key: cellKeys[0] || null, connected };
|
||||
|
|
@ -265,13 +318,6 @@ function dirBetween(a, b) {
|
|||
return DIRS.find(d => d.dc === dc && d.dr === dr)?.name || null;
|
||||
}
|
||||
|
||||
function isCrossInGraph(graph, cellKey) {
|
||||
const ns = graph.adjacency.get(cellKey) || [];
|
||||
const names = new Set(ns.map(n => n.dir));
|
||||
return ns.length === 4 && names.has('left') && names.has('right') && names.has('up') && names.has('down');
|
||||
}
|
||||
|
||||
// A four-way cross conveyor is an overpass/crossing. Chicks never turn there.
|
||||
export function bfsAllRoutes(game, start, isGoal) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
const startK = graphNodeKey(start.col, start.row);
|
||||
|
|
@ -302,7 +348,6 @@ export function bfsAllRoutes(game, start, isGoal) {
|
|||
for (const n of graph.adjacency.get(cur.key) || []) {
|
||||
const next = graphCell(n.key);
|
||||
const outDir = dirBetween(cur, next);
|
||||
if (cur.incoming !== 'none' && isCrossInGraph(graph, cur.key) && outDir !== cur.incoming) continue;
|
||||
const nextDepth = cur.depth + 1;
|
||||
if (nextDepth > bestGoalDepth) continue;
|
||||
const nextStateK = `${n.key}|${outDir}`;
|
||||
|
|
@ -524,20 +569,23 @@ function routeToNextScanner(game, scanner, side, fromPoint, connector, starts, a
|
|||
|
||||
function routeToFacility(game, scanner, side, fromPoint, connector, starts, dest, advance) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
const port = graph.facilityPorts.get(dest);
|
||||
if (!port?.connected) return null;
|
||||
const ports = [...graph.facilityPorts.entries()]
|
||||
.filter(([id, port]) => port?.connected && facilityKind(game.facilities?.[id]) === dest);
|
||||
if (!ports.length) return null;
|
||||
const candidates = [];
|
||||
for (const start of starts) {
|
||||
const goalKeys = (port.keys || [port.key]).filter(Boolean);
|
||||
if (!goalKeys.length) continue;
|
||||
const isGoal = p => goalKeys.includes(graphNodeKey(p.col, p.row));
|
||||
isGoal.cacheKey = `toFacility:${start.col},${start.row}:${dest}:v${graph.version}`;
|
||||
const routes = bfsAllRoutes(game, start, isGoal);
|
||||
for (const cells of routes) candidates.push({ key: `start${key(start.col, start.row)}:${dest}:${cells.length}:${key(cells[cells.length - 1].col, cells[cells.length - 1].row)}`, cells });
|
||||
for (const [facilityId, port] of ports) {
|
||||
const goalKeys = (port.keys || [port.key]).filter(Boolean);
|
||||
if (!goalKeys.length) continue;
|
||||
const isGoal = p => goalKeys.includes(graphNodeKey(p.col, p.row));
|
||||
isGoal.cacheKey = `toFacility:${start.col},${start.row}:${facilityId}:v${graph.version}`;
|
||||
const routes = bfsAllRoutes(game, start, isGoal);
|
||||
for (const cells of routes) candidates.push({ key: `start${key(start.col, start.row)}:${facilityId}:${cells.length}:${key(cells[cells.length - 1].col, cells[cells.length - 1].row)}`, cells, facilityId });
|
||||
}
|
||||
}
|
||||
const chosen = chooseRoundRobin(game, `scanner:${scanner.id}:${side}:${dest}`, candidates, advance);
|
||||
if (!chosen) return null;
|
||||
return { destination: dest, route: routeWithConnector(fromPoint, connector, chosen.cells, facilityEntryPoint(game, dest)) };
|
||||
return { destination: dest, facilityId: chosen.facilityId, route: routeWithConnector(fromPoint, connector, chosen.cells, facilityEntryPoint(game, chosen.facilityId)) };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -579,8 +627,7 @@ export function eggFarmHasValidRoute(game, farm) {
|
|||
|
||||
export function facilityHasValidRoute(game, dest) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
if (!graph.facilityPorts.get(dest)?.connected) return false;
|
||||
return game.scanners.some(scanner => scannerOutputReachesDestination(game, scanner, dest));
|
||||
return [...graph.facilityPorts.entries()].some(([id, port]) => port?.connected && facilityKind(game.facilities?.[id]) === dest);
|
||||
}
|
||||
|
||||
export function disconnectedBuildWarnings(game) {
|
||||
|
|
@ -590,8 +637,10 @@ export function disconnectedBuildWarnings(game) {
|
|||
if (!eggFarmHasValidRoute(game, farm)) warnings.push({ type: 'eggFarm', id: farm.id, ref: farm, message: 'This Egg will not produce chicks' });
|
||||
}
|
||||
for (const id of MACHINE_FACILITY_IDS) {
|
||||
const facility = game.facilities?.[id];
|
||||
if (facility && !facilityHasValidRoute(game, id)) warnings.push({ type: 'facility', id, ref: facility, message: 'This facility cannot receive items' });
|
||||
for (const [facilityId, facility] of facilityEntriesForKind(game, id)) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
if (!graph.facilityPorts.get(facilityId)?.connected) warnings.push({ type: 'facility', id: facilityId, ref: facility, message: 'This facility cannot receive items' });
|
||||
}
|
||||
}
|
||||
return warnings;
|
||||
}
|
||||
|
|
@ -605,7 +654,7 @@ export function minimumStartConnectionIssues(game) {
|
|||
if (outputRouteReachesFacility(game, scanner)) return [];
|
||||
}
|
||||
const farmCount = game.eggFarms?.length || 0;
|
||||
const exitCount = ['mixer', 'trash', 'truck'].filter(id => graph.facilityPorts.get(id)?.connected).length;
|
||||
const exitCount = [...graph.facilityPorts.entries()].filter(([id, port]) => port?.connected && ['mixer', 'trash', 'truck'].includes(facilityKind(game.facilities?.[id]))).length;
|
||||
if (!farmCount) return ['Build at least one EGG before starting the next day.'];
|
||||
if (!graph.metrics.farmOutputs) return ['Connect at least one EGG to a conveyor.'];
|
||||
if (!game.scanners?.length) return ['Build at least one scanner and connect it to an EGG route.'];
|
||||
|
|
@ -624,12 +673,14 @@ export function validateFactoryGraph(game, graph = ensureFactoryGraph(game), opt
|
|||
const labels = { mixer: 'Mixer', trash: 'Shredder', truck: 'Truck' };
|
||||
|
||||
for (const id of MACHINE_FACILITY_IDS) {
|
||||
const f = game.facilities[id];
|
||||
if (!f) {
|
||||
const facilities = facilityEntriesForKind(game, id);
|
||||
if (!facilities.length) {
|
||||
issues.push(`${labels[id] || id} is missing`);
|
||||
continue;
|
||||
}
|
||||
if (!graph.facilityPorts.get(id)?.connected) issues.push(`${labels[id] || id} receiver has no conveyor`);
|
||||
for (const [facilityId] of facilities) {
|
||||
if (!graph.facilityPorts.get(facilityId)?.connected) issues.push(`${labels[id] || id} receiver has no conveyor`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const scanner of game.scanners) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { GRID } from '../core/config.js';
|
||||
import { key, parseKey, cellCenter } from '../core/utils.js';
|
||||
import { nearestGridEdge, layoutFacilityOnEdge } from '../core/entities.js';
|
||||
import { farmAt, scannerAt, scannerCenter, refreshRoutingAfterEdit } from './routing.js';
|
||||
import { farmAt, scannerAt, scannerCenter, scannerFootprintCells, refreshRoutingAfterEdit } from './routing.js';
|
||||
import { snapshot } from './history.js';
|
||||
import { buildPrice } from './economy.js';
|
||||
|
||||
|
|
@ -192,12 +192,15 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
|||
for (const origin of game.groupDrag.origins) {
|
||||
if (origin.type === 'facility') continue;
|
||||
const col = origin.col + dcol, row = origin.row + drow;
|
||||
if (!pointInGrid(col, row)) return fail('Selection outside grid');
|
||||
if (game.blockedCells?.has?.(key(col, row))) return fail('Selection hits blocked ground');
|
||||
const tk = key(col, row);
|
||||
if (targetCells.has(tk)) return fail('Selection overlap');
|
||||
if (cellOccupiedByNonSelected(col, row, selectedTokens)) return fail('Cell occupied');
|
||||
targetCells.add(tk);
|
||||
const cells = origin.type === 'scanner' ? scannerFootprintCells({ col, row }) : [{ col, row }];
|
||||
for (const cell of cells) {
|
||||
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');
|
||||
const tk = key(cell.col, cell.row);
|
||||
if (targetCells.has(tk)) return fail('Selection overlap');
|
||||
if (cellOccupiedByNonSelected(cell.col, cell.row, selectedTokens)) return fail('Cell occupied');
|
||||
targetCells.add(tk);
|
||||
}
|
||||
}
|
||||
if (facilityDragWouldOverlap(dx, dy, selectedTokens)) return fail('Facility overlap');
|
||||
commitGroupDragHistory();
|
||||
|
|
|
|||
|
|
@ -18,11 +18,9 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
if (game.phase !== 'build' || game.cardDraft?.pending || game.cardTargetPick?.pending) return false;
|
||||
let hasBuildableOption = false;
|
||||
for (const id of BUILD_TOOL_IDS) {
|
||||
if (MACHINE_FACILITY_IDS.includes(id) && !!game.facilities[id]) continue;
|
||||
hasBuildableOption = true;
|
||||
if (game.cash >= buildPrice(id, game)) return false;
|
||||
}
|
||||
if (build?.gridExpansionCost && game.cash >= build.gridExpansionCost()) return false;
|
||||
return hasBuildableOption;
|
||||
}
|
||||
|
||||
|
|
@ -44,32 +42,20 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
if (!btn) continue;
|
||||
const price = buildPrice(id, game);
|
||||
const priceSpan = btn.querySelector('span');
|
||||
if (priceSpan) priceSpan.textContent = id === 'conveyor' ? `${yen(price)} / tile` : yen(price);
|
||||
const uniqueAlreadyBuilt = MACHINE_FACILITY_IDS.includes(id) && !!game.facilities[id];
|
||||
if (priceSpan) priceSpan.textContent = (id === 'conveyor' || id === 'boostConveyor') ? `${yen(price)} / tile` : yen(price);
|
||||
const manualLimitReached = id === 'manualScanner' && game.scanners.filter(s => s.kind === 'manual').length >= 8;
|
||||
const unaffordable = game.cash < price;
|
||||
btn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || unaffordable || uniqueAlreadyBuilt || manualLimitReached;
|
||||
btn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || unaffordable || manualLimitReached;
|
||||
if (btn.disabled && game.buildTool === id) game.buildTool = null;
|
||||
btn.classList.toggle('unaffordable', unaffordable);
|
||||
btn.classList.toggle('already-built', uniqueAlreadyBuilt || manualLimitReached);
|
||||
btn.classList.toggle('already-built', manualLimitReached);
|
||||
btn.title = unaffordable
|
||||
? `Need ${yen(price - game.cash)} more`
|
||||
: manualLimitReached
|
||||
? 'Manual Scanner limit reached (8 max)'
|
||||
: uniqueAlreadyBuilt
|
||||
? `${FACILITY_DEFS[id]?.name || id} already exists`
|
||||
: '';
|
||||
}
|
||||
if (ui.buttons.erase) ui.buttons.erase.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending;
|
||||
if (ui.expandGridButton && build?.gridExpansionCost) {
|
||||
const cost = build.gridExpansionCost();
|
||||
const priceSpan = ui.expandGridButton.querySelector('span');
|
||||
if (priceSpan) priceSpan.textContent = `${yen(cost)} / +10×10`;
|
||||
const blocked = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || game.cash < cost;
|
||||
ui.expandGridButton.disabled = blocked;
|
||||
ui.expandGridButton.classList.toggle('unaffordable', game.cash < cost);
|
||||
ui.expandGridButton.title = game.cash < cost ? `Need ${yen(cost - game.cash)} more` : `Add the next right 10×10 lot, or click an upper/right lot button on the canvas. Next purchase count: ${(game.gridExpansionPurchases || 0) + 1}`;
|
||||
}
|
||||
if (ui.hireRepairmanButton) {
|
||||
const hired = !!game.repairman?.hiredForNextDay;
|
||||
const cost = repairmanDailyCost(game);
|
||||
|
|
@ -110,15 +96,15 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
const lastTribute = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.fairiesTribute || 0 : null;
|
||||
const lastTributeReduction = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.reduction || 0 : 0;
|
||||
ui.turnSummary.innerHTML = [
|
||||
`Seed: <b>${escapeHtml(game.runSeed || 'none')}</b> | Grid box: <b>${game.gridRows || 10}×${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>`,
|
||||
`Poop fine: <b>Mixer -${yen(mixerPoopPenalty(game))}</b> / <b>Shipment -${yen(truckPoopPenalty(game))}</b>`,
|
||||
`ZUNDA TAX on Next Day: <b>-${yen(tax.tax)}</b> | basis net ${yen(zundaBasisProfit)} | taxable ${yen(tax.taxable)} × ${tax.ratePercent}% | exemption ${yen(tax.exemption)} | 95% at ${yen(tax.maxRateCash)}`,
|
||||
`ZUNDA TAX on Next Day: <b>-${yen(tax.tax)}</b> | basis net ${yen(zundaBasisProfit)} | taxable ${yen(tax.taxable)} x${tax.ratePercent}% | exemption ${yen(tax.exemption)} | 95% at ${yen(tax.maxRateCash)}`,
|
||||
lastTribute != null
|
||||
? `Fairies tribute paid before Build: <b>-${yen(lastTribute)}</b>${lastTributeReduction ? ` | Flattery -${yen(lastTributeReduction)}` : ''}`
|
||||
: `Fairies tribute before Build: <b>-${yen(tribute.amount)}</b>${tribute.reduction ? ` | Flattery -${yen(tribute.reduction)}` : ''}`,
|
||||
`Maintenance: <b>worst ${maintenanceSummary(game).worst.percent}% dirty (${maintenanceSummary(game).worst.label})</b> | Repairman: <b>${game.repairman?.hiredForNextDay ? 'hired next day' : game.repairman?.active ? 'working' : 'none'}</b>`,
|
||||
`Manual combo: <b>${game.manualCombo?.count || 0}</b> | Next expansion: <b>${yen(build?.gridExpansionCost ? build.gridExpansionCost() : 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>`,
|
||||
`Manual combo: <b>${game.manualCombo?.count || 0}</b> | Card draft: <b>${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'}</b> | Belt: <b>${displaySpeed()}px/s</b> | Farms with scanner route: <b>${connected}/${game.eggFarms.length}</b>`,
|
||||
game.cardEffects?.usedMachineActive ? '<span class="cash-negative">Procurement: USED MACHINE MODE / no refunds / 60% durability</span>' : '',
|
||||
issues.length ? `<span class="cash-negative">Blocked: ${issues[0]}</span>` : `<span class="cash-positive">${TEXT.status.allPortsConnected}</span>`
|
||||
].join('<br>');
|
||||
|
|
@ -165,7 +151,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
ui.timeLeft.textContent = game.phase === 'running' && game.timeLeft <= 0 && game.shutdownTimeLeft > 0
|
||||
? `+${game.shutdownTimeLeft.toFixed(1)}s`
|
||||
: `${Math.max(0, game.timeLeft).toFixed(1)}s`;
|
||||
ui.timeLeft.closest?.('.hud-card')?.classList.toggle('time-critical', game.phase === 'running' && (game.timeLeft <= 10 || (game.timeLeft <= 0 && game.shutdownTimeLeft > 0)));
|
||||
ui.timeLeft.closest?.('.hud-card')?.classList.toggle('time-critical', game.phase === 'running' && (game.timeLeft <= 10 || (game.timeLeft <= 0 && game.shutdownTimeLeft > 0)));
|
||||
ui.phase.textContent = phaseLabel();
|
||||
if (ui.comboCount) ui.comboCount.textContent = game.manualCombo?.count || 0;
|
||||
updatePriorityStrip();
|
||||
|
|
@ -247,6 +233,8 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
<div><strong>Chick shipment income</strong>${positive(r.chickShipmentIncome || 0)}</div>
|
||||
<div><strong>Poop shipment income</strong>${positive(r.poopShipmentIncome || 0)}</div>
|
||||
<div><strong>Mixer income</strong>${positive(r.mixerRevenue || 0)}</div>
|
||||
<div><strong>Composter income</strong>${positive(r.composterIncome || 0)}</div>
|
||||
<div><strong>DUD refund income</strong>${positive(r.dudRefundIncome || 0)}</div>
|
||||
<div><strong>Manual combo bonus</strong>${positive(r.manualComboBonus || 0)}</div>
|
||||
<div><strong>Chemical subsidy</strong>${positive(r.chemicalWeaponSubsidy || 0)}</div>
|
||||
<div><strong>Contract bonus</strong>${positive(r.contractBonus || 0)}</div>
|
||||
|
|
@ -289,7 +277,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
ui.modalBody.innerHTML = `<p class="gameover-reason">Your cash went negative.</p>
|
||||
<div class="result-grid gameover-summary"><div><strong>Final Score</strong><span>${fs.score}</span></div><div><strong>Survival</strong><span>${game.totals.turnsCompleted} days</span></div><div><strong>Final Cash</strong><span>${yen(game.cash)}</span></div><div><strong>Accuracy</strong><span>${accuracy}%</span></div></div>
|
||||
<details class="gameover-details"><summary>Score breakdown</summary>
|
||||
<div class="formula-box compact"><strong>Formula</strong><br>Base = Cash + Total revenue + Factory value × 0.5 + Contract bonus × 0.5 = ${yen(fs.base)}<br>Daily earned = ceil(Base ÷ Days) = ${yen(fs.dailyEarned)}<br>Score = max(0, floor(Base + Daily earned + Correct×5 - Total outflow - Day penalty)) = ${fs.score}</div>
|
||||
<div class="formula-box compact"><strong>Formula</strong><br>Base = Cash + Total revenue + Factory value 繝サ繝サ繝サ0.5 + Contract bonus 繝サ繝サ繝サ0.5 = ${yen(fs.base)}<br>Daily earned = ceil(Base 繝サ繝サ・ス・キ Days) = ${yen(fs.dailyEarned)}<br>Score = max(0, floor(Base + Daily earned + Correct繝サ繝サ繝サ - Total outflow - Day penalty)) = ${fs.score}</div>
|
||||
<div class="result-grid gameover-breakdown"><div><strong>Factory Value</strong><span>${yen(fs.factoryValue)}</span></div><div><strong>Total Revenue</strong><span>${yen(game.totals.revenue)}</span></div><div><strong>Daily Earned</strong><span>${yen(fs.dailyEarned)} / day</span></div><div><strong>Total Outflow</strong><span>${yen(game.totals.penalty)}</span></div><div><strong>ZUNDA TAX</strong><span>${yen(game.totals.zundaTax)}</span></div><div><strong>Fairies Tribute</strong><span>${yen(game.totals.fairiesTribute || 0)}</span></div><div><strong>Correct Sorts</strong><span>${game.totals.correct}</span></div><div><strong>Auto Sorted</strong><span>${game.totals.autoSorted}</span></div><div><strong>Poop Control</strong><span>${game.totals.poopTrash} trashed / ${game.totals.poopTruck} shipped / ${game.totals.poopMixer} mixer</span></div><div><strong>Explosion Penalty</strong><span>${yen(game.totals.explosionDamage)}</span></div><div><strong>Contract Bonus</strong><span>${yen(game.totals.contractBonus)}</span></div><div><strong>Day Penalty</strong><span>-${fs.dayPenalty}</span></div><div><strong>Contracts</strong><span>${game.totals.contractSuccess} success / ${game.totals.contractFailed} failed</span></div></div>
|
||||
</details>`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
|
|
|
|||
46
styles.css
46
styles.css
|
|
@ -588,6 +588,52 @@ body { font-size: 20px; }
|
|||
.modal.equipment-popover .modal-actions button { font-size: 11px; }
|
||||
.equipment-menu-lines.compact, .formula-box.compact { font-size: 11px; }
|
||||
.hover-tooltip strong { font-size: 18px; }
|
||||
|
||||
/* Keep construction flavor text readable as a hover bubble, not button content. */
|
||||
.large-tools .tool-button {
|
||||
min-height: 62px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
.large-tools .tool-button:hover {
|
||||
min-height: 62px;
|
||||
}
|
||||
.large-tools .tool-button .tool-flavor {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 90;
|
||||
right: 0;
|
||||
top: calc(100% + 8px);
|
||||
width: min(320px, calc(100vw - 40px));
|
||||
max-height: none;
|
||||
opacity: 1;
|
||||
overflow: visible;
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border: 3px solid var(--line);
|
||||
background: rgba(255,255,255,.98);
|
||||
color: var(--ink);
|
||||
box-shadow: 5px 5px 0 rgba(16,32,21,.18);
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
pointer-events: none;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.large-tools .tool-button:hover .tool-flavor {
|
||||
display: block;
|
||||
}
|
||||
.large-tools .tool-button .tool-flavor::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: -10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-left: 3px solid var(--line);
|
||||
border-top: 3px solid var(--line);
|
||||
background: rgba(255,255,255,.98);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.hover-tooltip span { font-size: 15px; }
|
||||
.sort-button { font-size: 17px; }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue