tweak
This commit is contained in:
parent
82a6b2c787
commit
650e44aff3
20 changed files with 1357 additions and 262 deletions
82
dist/game.js
vendored
82
dist/game.js
vendored
File diff suppressed because one or more lines are too long
12
index.html
12
index.html
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Chick Sorter v27.6</title>
|
||||
<title>Chick Sorter v27.8</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h1>CHICK SORTER <span>v27.6</span></h1>
|
||||
<h1>CHICK SORTER <span>v27.8</span></h1>
|
||||
<p>Build, expand, connect at least one valid EGG route, then start the next day.</p>
|
||||
</div>
|
||||
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
|
||||
|
|
@ -54,8 +54,8 @@
|
|||
<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="buildMaintenanceRoomButton" class="tool-button repair" type="button"><strong>Maintenance Room</strong><span>¥400 / 2x2</span></button>
|
||||
<button id="eraseButton" class="tool-button danger" type="button"><strong>Sell</strong><span>Sell or refund equipment</span></button>
|
||||
<button id="hireRepairmanButton" class="tool-button repair" type="button"><strong>Hire Repairman</strong><span>・・100 / next day</span></button>
|
||||
<div class="history-tools"><button id="undoButton" class="tool-button small" type="button"><strong>Undo</strong><span>Ctrl+Z</span></button><button id="redoButton" class="tool-button small" type="button"><strong>Redo</strong><span>Ctrl+Y</span></button></div>
|
||||
</div>
|
||||
<div id="buildStatus" class="mini-box">Build tools unlock after each day.</div>
|
||||
|
|
@ -67,6 +67,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<details id="cardInventory" class="acquired-cards-panel" aria-label="Acquired cards">
|
||||
<summary>CARDS <strong>0</strong><span>acquired</span></summary>
|
||||
<div class="acquired-cards-list">No passive cards yet.</div>
|
||||
</details>
|
||||
|
||||
<div id="manualScannerMonitor" class="scanner-monitor" aria-label="Offscreen manual scanner monitor"></div>
|
||||
|
||||
<div class="control-dock" aria-label="Sorting controls">
|
||||
|
|
|
|||
496
package-lock.json
generated
Normal file
496
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
{
|
||||
"name": "zunda-shiwake",
|
||||
"version": "27.8.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "zunda-shiwake",
|
||||
"version": "27.8.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "zunda-shiwake",
|
||||
"version": "27.6.0",
|
||||
"version": "27.8.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Central balance sheet for tuning gameplay.
|
||||
// Edit this file first when adjusting prices, income, penalties, timing, card rates, or caps.
|
||||
export const BALANCE = {
|
||||
version: 'v27.6 direct conveyor chick movement',
|
||||
version: 'v27.8 repair routing and acquired-card UI',
|
||||
time: {
|
||||
daySeconds: 60,
|
||||
farmShutdownGraceSeconds: 10
|
||||
|
|
@ -88,6 +88,15 @@ export const BALANCE = {
|
|||
description: 'Choose one AUTO SCANNER on the map and raise it by 1 level.',
|
||||
tags: ['SCANNER', 'ACTIVE']
|
||||
},
|
||||
{
|
||||
id: 'upgradeMaintenanceRoom',
|
||||
title: 'MAINTENANCE ROOM Improvement',
|
||||
rarity: 'common',
|
||||
type: 'equipmentUpgrade',
|
||||
target: 'maintenanceRoom',
|
||||
description: 'Choose one MAINTENANCE ROOM and add +1 repair worker. Each worker costs JPY 80 per day.',
|
||||
tags: ['MAINTENANCE', 'ACTIVE']
|
||||
},
|
||||
{
|
||||
id: 'upgradeMixer',
|
||||
title: 'MIXER Improvement',
|
||||
|
|
@ -367,7 +376,7 @@ export const BALANCE = {
|
|||
trash: 1.00
|
||||
},
|
||||
repairman: {
|
||||
dailyCost: 100,
|
||||
dailyCost: 80,
|
||||
secondsPerOnePercent: 0.5,
|
||||
walkSpeed: 90
|
||||
}
|
||||
|
|
@ -393,6 +402,10 @@ export const BALANCE = {
|
|||
id: 'autoScanner', type: 'scanner', kind: 'auto', name: 'Auto Scanner', shortName: 'Auto Scanner', price: 360,
|
||||
buildable: true, upgradeable: false
|
||||
},
|
||||
maintenanceRoom: {
|
||||
id: 'maintenanceRoom', type: 'maintenanceRoom', name: 'Maintenance Room', shortName: 'MAINT ROOM', price: 400,
|
||||
buildable: true, upgradeable: true, footprint: { cols: 2, rows: 2 }
|
||||
},
|
||||
mixer: {
|
||||
id: 'mixer', type: 'facility', name: 'Mixer', shortName: 'MIXER', price: 450,
|
||||
buildable: true, upgradeable: true, incomeKey: 'mixer', body: { w: 164, h: 126 }
|
||||
|
|
|
|||
|
|
@ -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', 'boostConveyor', 'eggFarm', 'autoScanner', 'manualScanner', 'mixer', 'trash', 'truck'];
|
||||
export const BUILD_TOOL_IDS = ['conveyor', 'boostConveyor', 'eggFarm', 'autoScanner', 'manualScanner', 'maintenanceRoom', 'mixer', 'trash', 'truck'];
|
||||
export const MACHINE_FACILITY_IDS = ['mixer', 'trash', 'truck'];
|
||||
export const INCOME_FACILITY_IDS = ['mixer', 'truck'];
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,27 @@ export function createScanner(game, col, row, kind) {
|
|||
};
|
||||
}
|
||||
|
||||
export function createMaintenanceRoom(game, col, row, price) {
|
||||
const def = FACILITY_DEFS.maintenanceRoom || {};
|
||||
const cols = def.footprint?.cols || 2;
|
||||
const rows = def.footprint?.rows || 2;
|
||||
return {
|
||||
type: 'facility',
|
||||
id: 'maintenanceRoom',
|
||||
baseId: 'maintenanceRoom',
|
||||
name: def.name || 'Maintenance Room',
|
||||
shortName: def.shortName || 'MAINT ROOM',
|
||||
col, row, cols, rows,
|
||||
x: GRID.x + col * GRID.cell,
|
||||
y: GRID.y + row * GRID.cell,
|
||||
w: cols * GRID.cell,
|
||||
h: rows * GRID.cell,
|
||||
level: 1,
|
||||
price: price || def.price || 400,
|
||||
builtSession: game.buildSession
|
||||
};
|
||||
}
|
||||
|
||||
export function nearestGridEdge(point) {
|
||||
const colFloat = (point.x - GRID.x) / GRID.cell;
|
||||
const rowFloat = (point.y - GRID.y) / GRID.cell;
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ export function createGame() {
|
|||
manualCombo: { count: 0, lastBonus: 0 },
|
||||
pendingRepairWorkerWages: 0,
|
||||
repairman: { hiredForNextDay: false, active: false, x: GRID.x + GRID.cell * 0.5, y: GRID.y + BALANCE.grid.rows * GRID.cell + 84, target: null, repairedToday: 0 },
|
||||
repairmen: [],
|
||||
stats: newTurnStats(),
|
||||
lastResult: null,
|
||||
totals: newTotalStats()
|
||||
|
|
@ -322,6 +323,7 @@ export function resetLayout(game) {
|
|||
game.repairman.active = false;
|
||||
game.repairman.hiredForNextDay = false;
|
||||
}
|
||||
game.repairmen = [];
|
||||
game.conveyorTiles.clear();
|
||||
game.conveyorMeta.clear();
|
||||
game.blockedCells = new Set();
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ export const TEXT = {
|
|||
autoMenu: 'Auto Menu'
|
||||
},
|
||||
status: {
|
||||
build: tool => `BUILD: ${(tool === 'erase' ? 'SELL' : (tool || 'SELECT')).toUpperCase()} | Right-drag pan | Hover equipment for info`,
|
||||
sorting: 'Sorting: use scanner keys. Build after clearing the day.',
|
||||
build: tool => `BUILD: ${(tool === 'erase' ? 'SELL' : (tool || 'SELECT')).toUpperCase()} | Right/middle-drag pan | Hover equipment for info`,
|
||||
sorting: 'Sorting: use scanner keys. Right/middle-drag to pan. Build after clearing the day.',
|
||||
allPortsConnected: 'Start condition met: one EGG route reaches an exit.',
|
||||
equipmentPanelEmpty: 'Hover equipment in Build phase.',
|
||||
eventPanelRunning: 'Forced events appear in Build phase.'
|
||||
|
|
@ -69,6 +69,7 @@ export function buildToolFlavorText(id) {
|
|||
eggFarm: 'A tiny gatehouse producing questionable eggs on schedule.',
|
||||
autoScanner: 'Slower than hands without upgrades.',
|
||||
manualScanner: 'A manual checkpoint. The operator is the algorithm.',
|
||||
maintenanceRoom: '2x2 repair base. Spawns repair workers during sorting.',
|
||||
mixer: 'Male chicks become revenue here. Do not feed it poop.',
|
||||
trash: 'A polite shredder for poop and other regrets.',
|
||||
truck: 'Ships the target cargo. Wrong cargo still leaves a paper trail.'
|
||||
|
|
|
|||
45
src/game.js
45
src/game.js
|
|
@ -13,7 +13,7 @@ import { createChickSystem } from './systems/chickSystem.js';
|
|||
import { CARD_DEFS, conveyorSpeedForGame, createCardSystem, debugGrantCard } from './systems/cards.js';
|
||||
import { floating, shockwave, sparkBurst, updateEffects } from './systems/effects.js';
|
||||
import { rollContractOffer, activateAcceptedContract, clearActiveContract, resolveContract } from './systems/contracts.js';
|
||||
import { ensureMaintenanceState, hireRepairmanForNextDay, activateRepairmanForDay, deactivateRepairman, conveyorSpeedFactorForKey } from './systems/maintenance.js';
|
||||
import { ensureMaintenanceState, activateRepairmanForDay, deactivateRepairman, conveyorSpeedFactorForKey } from './systems/maintenance.js';
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
|
@ -27,7 +27,7 @@ const ui = {
|
|||
modal: document.getElementById('modal'), modalTitle: document.getElementById('modalTitle'), modalBody: document.getElementById('modalBody'), modalActions: document.getElementById('modalActions'),
|
||||
hoverTooltip: document.getElementById('hoverTooltip'),
|
||||
manualScannerMonitor: document.getElementById('manualScannerMonitor'),
|
||||
hireRepairmanButton: document.getElementById('hireRepairmanButton'),
|
||||
cardInventory: document.getElementById('cardInventory'),
|
||||
debug: {
|
||||
panel: document.getElementById('debugPanel'),
|
||||
infiniteCash: document.getElementById('debugInfiniteCash'),
|
||||
|
|
@ -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'), boostConveyor: document.getElementById('buildBoostConveyorButton'), eggFarm: document.getElementById('buildEggFarmButton'), autoScanner: document.getElementById('buildAutoScannerButton'), manualScanner: document.getElementById('buildManualScannerButton'), mixer: document.getElementById('buildMixerButton'), trash: document.getElementById('buildTrashButton'), truck: document.getElementById('buildTruckButton'), erase: document.getElementById('eraseButton'), undo: document.getElementById('undoButton'), redo: document.getElementById('redoButton')
|
||||
nextTurn: document.getElementById('nextTurnButton'), conveyor: document.getElementById('buildConveyorButton'), boostConveyor: document.getElementById('buildBoostConveyorButton'), eggFarm: document.getElementById('buildEggFarmButton'), autoScanner: document.getElementById('buildAutoScannerButton'), manualScanner: document.getElementById('buildManualScannerButton'), maintenanceRoom: document.getElementById('buildMaintenanceRoomButton'), mixer: document.getElementById('buildMixerButton'), trash: document.getElementById('buildTrashButton'), truck: document.getElementById('buildTruckButton'), erase: document.getElementById('eraseButton'), undo: document.getElementById('undoButton'), redo: document.getElementById('redoButton')
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -53,6 +53,7 @@ function initializeStaticText() {
|
|||
eggFarm: 'eggFarm',
|
||||
autoScanner: 'autoScanner',
|
||||
manualScanner: 'manualScanner',
|
||||
maintenanceRoom: 'maintenanceRoom',
|
||||
mixer: 'mixer',
|
||||
trash: 'trash',
|
||||
truck: 'truck'
|
||||
|
|
@ -169,7 +170,7 @@ function startNextTurn() {
|
|||
}
|
||||
ensureMaintenanceState(game);
|
||||
commitFactoryGraphForDay(game);
|
||||
activateRepairmanForDay(game);
|
||||
const repairActivation = activateRepairmanForDay(game);
|
||||
const zundaBasisProfit = Math.max(0, Math.floor(game.lastResult?.profit ?? game.stats?.profit ?? 0));
|
||||
const zunda = collectZundaTax(game, zundaBasisProfit);
|
||||
const loanRepayment = collectLoanRepayments(game);
|
||||
|
|
@ -193,12 +194,21 @@ function startNextTurn() {
|
|||
game.stats = newTurnStats();
|
||||
game.stats.zundaTax = 0;
|
||||
game.stats.loanRepayment = game.lastStartFees?.loanRepayment || 0;
|
||||
game.stats.repairWorkerWages = game.pendingRepairWorkerWages || 0;
|
||||
const carriedStartCosts = game.stats.loanRepayment + game.stats.repairWorkerWages;
|
||||
game.stats.repairWorkerWages = repairActivation.wages || 0;
|
||||
const carriedStartCosts = game.stats.loanRepayment;
|
||||
if (carriedStartCosts > 0) {
|
||||
game.stats.penalty += carriedStartCosts;
|
||||
game.stats.profit -= carriedStartCosts;
|
||||
}
|
||||
if (game.stats.repairWorkerWages > 0) {
|
||||
game.cash -= game.stats.repairWorkerWages;
|
||||
game.stats.penalty += game.stats.repairWorkerWages;
|
||||
game.stats.profit -= game.stats.repairWorkerWages;
|
||||
game.totals.repairWorkerWages = (game.totals.repairWorkerWages || 0) + game.stats.repairWorkerWages;
|
||||
game.totals.penalty = (game.totals.penalty || 0) + game.stats.repairWorkerWages;
|
||||
game.totals.profit = (game.totals.profit || 0) - game.stats.repairWorkerWages;
|
||||
floating(game, canvas.width / 2 - game.view.x, 176 - game.view.y, `REPAIR WAGES -${yen(game.stats.repairWorkerWages)}`, THEME.danger);
|
||||
}
|
||||
game.pendingRepairWorkerWages = 0;
|
||||
for (const scanner of game.scanners) { scanner.queue = []; scanner.cooldown = 0; }
|
||||
for (const farm of game.eggFarms) { farm.nextSpawn = chicks.currentSpawnDelay(farm); farm.lastInterval = farm.nextSpawn; farm.shutterProgress = 0; farm.shutterSparked = false; }
|
||||
|
|
@ -213,18 +223,6 @@ function startNextTurn() {
|
|||
}
|
||||
|
||||
|
||||
function hireRepairman() {
|
||||
ensureMaintenanceState(game);
|
||||
const result = hireRepairmanForNextDay(game);
|
||||
if (!result.ok) {
|
||||
floating(game, canvas.width / 2 - game.view.x, 96 - game.view.y, result.reason || 'Cannot hire repairman.', THEME.danger);
|
||||
build?.fail?.(result.reason || 'Cannot hire repairman.');
|
||||
} else {
|
||||
floating(game, canvas.width / 2 - game.view.x, 96 - game.view.y, `Repairman hired -${yen(result.cost)}`, THEME.green);
|
||||
}
|
||||
uiSystem.updatePanels();
|
||||
uiSystem.updateUI();
|
||||
}
|
||||
|
||||
function completeTurn() {
|
||||
if (game.phase !== 'running') return;
|
||||
|
|
@ -582,10 +580,11 @@ ui.manualScannerMonitor?.addEventListener('click', event => {
|
|||
canvas.addEventListener('contextmenu', event => event.preventDefault());
|
||||
canvas.addEventListener('wheel', zoomAt, { passive: false });
|
||||
canvas.addEventListener('pointerdown', event => {
|
||||
if (game.phase !== 'build') return;
|
||||
if (!['build', 'running'].includes(game.phase)) return;
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
const world = canvasPoint(event);
|
||||
if (event.button === 2) { startPan(event); return; }
|
||||
if (event.button === 2 || event.button === 1) { startPan(event); return; }
|
||||
if (game.phase !== 'build') return;
|
||||
if (event.button !== 0) return;
|
||||
if (game.cardTargetPick?.mode === 'autoScannerMenu') { cancelBuildAction(); return; }
|
||||
if (game.cardTargetPick?.pending) { cardSystem.chooseTargetAtPoint(world); return; }
|
||||
|
|
@ -597,7 +596,6 @@ canvas.addEventListener('pointerdown', event => {
|
|||
if (!game.buildTool && clickedHit?.type === 'conveyor' && clickedCell && build.cycleSingleConveyorDirection?.(clickedCell)) {
|
||||
uiSystem.updatePanels();
|
||||
uiSystem.updateUI();
|
||||
build.showSelectedMenu?.();
|
||||
return;
|
||||
}
|
||||
if (game.buildTool === 'erase') {
|
||||
|
|
@ -621,9 +619,9 @@ canvas.addEventListener('pointerdown', event => {
|
|||
build.startSelectionBox(event);
|
||||
});
|
||||
canvas.addEventListener('pointermove', event => {
|
||||
if (game.pan && ((event.buttons & 2) || (event.buttons & 4))) { updatePan(event); return; }
|
||||
if (game.phase !== 'build') { hideHoverTooltip(); return; }
|
||||
updateHover(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' || game.buildTool === 'boostConveyor') && (event.buttons & 1)) build.continueConveyorDrag(pointToCell(canvasPoint(event).x, canvasPoint(event).y));
|
||||
|
|
@ -649,8 +647,7 @@ document.addEventListener('pointerdown', event => {
|
|||
}, 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.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.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.maintenanceRoom?.addEventListener('click', () => build.setBuildTool('maintenanceRoom')); 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.debug.setDay?.addEventListener('click', debugSetDay);
|
||||
ui.debug.zeroTimer?.addEventListener('click', debugZeroTimer);
|
||||
ui.debug.grantCard?.addEventListener('click', debugGrantSelectedCard);
|
||||
|
|
|
|||
|
|
@ -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, cellCenter, mixHex, randomBetween, yen } from '../core/utils.js';
|
||||
import { getConveyorNeighbors, scannerCenter, scannerConnector, scannerOutputs, destinationLabel, destinationColor, scannerPortHasConveyor, disconnectedBuildWarnings } from '../systems/routing.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);
|
||||
|
|
@ -270,25 +272,13 @@ function drawConveyors(ctx, game) {
|
|||
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) || []);
|
||||
drawBranchModeLabel(ctx, c, meta, reachableConveyorDirs(game, k, meta), connectedConveyorDirs(game, 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 branchModeText(mode) {
|
||||
return { random: 'RND', pass: 'PASS', up: 'UP', right: 'RT', down: 'DN', left: 'LF' }[mode || 'random'] || 'RND';
|
||||
}
|
||||
|
||||
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 reachableConveyorDirs(game, k, meta) {
|
||||
const p = parseKey(k);
|
||||
const neighborNames = DIRS
|
||||
|
|
@ -307,6 +297,34 @@ function connectedConveyorDirs(game, k) {
|
|||
.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;
|
||||
if (branchMode && branchMode !== 'random' && connectedDirs.includes(branchMode)) return [branchMode];
|
||||
const p = parseKey(k);
|
||||
const entrances = new Set(connectedDirs.filter(dir => neighborPointsIntoCell(game, p, dir)));
|
||||
const exits = connectedDirs.filter(dir => !entrances.has(dir));
|
||||
if (exits.length) return exits;
|
||||
const fallback = dirs.filter(dir => !entrances.has(dir));
|
||||
return fallback.length ? fallback : dirs;
|
||||
}
|
||||
|
||||
function branchSelectableEntranceDirs(game, k, connectedDirs = connectedConveyorDirs(game, k)) {
|
||||
if (connectedDirs.length < 3) return [];
|
||||
const p = parseKey(k);
|
||||
const physicalEntrances = new Set(connectedDirs.filter(dir => neighborPointsIntoCell(game, p, dir)));
|
||||
const exits = connectedDirs.filter(dir => !physicalEntrances.has(dir));
|
||||
return exits.length >= 2 ? exits : connectedDirs;
|
||||
}
|
||||
|
||||
function effectiveConveyorDirs(game, k, meta) {
|
||||
const p = parseKey(k);
|
||||
const neighborNames = DIRS
|
||||
|
|
@ -318,73 +336,70 @@ function effectiveConveyorDirs(game, k, meta) {
|
|||
return explicit.length ? explicit : inferred;
|
||||
}
|
||||
|
||||
function drawBranchModeLabel(ctx, center, meta, dirs, connectedDirs = dirs) {
|
||||
function drawBranchModeIcon(ctx, center, meta, dirs, connectedDirs = dirs) {
|
||||
if (connectedDirs.length < 3 || dirs.length < 2) return;
|
||||
ctx.save();
|
||||
const mode = meta.branchMode === 'pass' && dirs.length >= 4 ? 'pass' : (dirs.includes(meta.branchMode) ? meta.branchMode : 'random');
|
||||
const mode = dirs.includes(meta.branchMode) ? meta.branchMode : 'random';
|
||||
const isFixed = mode !== 'random';
|
||||
const text = branchModeText(mode);
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = 4;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.fillStyle = 'rgba(255,255,255,.9)';
|
||||
ctx.strokeStyle = isFixed ? THEME.warn : THEME.green;
|
||||
rect(ctx, center.x - 28, center.y - 19, 56, 38, true, true);
|
||||
rect(ctx, center.x - 17, center.y - 17, 34, 34, true, true);
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(center.x - 18, center.y);
|
||||
ctx.lineTo(center.x + 18, center.y);
|
||||
ctx.moveTo(center.x, center.y - 13);
|
||||
ctx.lineTo(center.x, center.y + 13);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = isFixed ? THEME.warn : THEME.green;
|
||||
ctx.beginPath();
|
||||
ctx.arc(center.x, center.y, 12, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
ctx.font = '900 9px ui-monospace, monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = THEME.ink;
|
||||
ctx.fillText(text, center.x, center.y + 0.5);
|
||||
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 explicitConveyorAngles(game, k, meta) {
|
||||
function explicitConveyorMarkerDirs(game, k, meta) {
|
||||
if (!meta) return [];
|
||||
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 = explicit.length ? explicit : effectiveConveyorDirs(game, k, meta).slice(0, 1);
|
||||
const rawExplicit = [...new Set([...(Array.isArray(meta.outDirs) ? meta.outDirs : []), meta.dir].filter(Boolean))]
|
||||
.filter(name => DIRS.some(d => d.name === name));
|
||||
const explicitReachable = rawExplicit.filter(name => neighborNames.includes(name));
|
||||
const terminalVisual = meta.dir && rawExplicit.includes(meta.dir) ? [meta.dir] : [];
|
||||
const visualSource = explicitReachable.length ? explicitReachable : (terminalVisual.length ? terminalVisual : effectiveConveyorDirs(game, k, meta));
|
||||
const dirs = branchExitDirs(game, k, visualSource, neighborNames, meta.branchMode || 'random');
|
||||
return dirs
|
||||
.map(name => DIRS.find(d => d.name === name)?.angle)
|
||||
.filter(angle => Number.isFinite(angle));
|
||||
.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(game, k, meta);
|
||||
if (angles.length) markers.set(k, angles);
|
||||
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();
|
||||
}
|
||||
|
|
@ -599,9 +614,28 @@ function drawFacilities(ctx, game) {
|
|||
if (kind === 'mixer') drawMixer(ctx, game, f);
|
||||
else if (kind === 'trash') drawTrash(ctx, game, f);
|
||||
else if (kind === 'truck') drawTruck(ctx, game, f);
|
||||
else if (kind === 'maintenanceRoom') drawMaintenanceRoom(ctx, game, f);
|
||||
}
|
||||
for (const f of Object.values(game.facilities || {})) drawFacilityReceiver(ctx, game, f);
|
||||
}
|
||||
function drawMaintenanceRoom(ctx, game, room) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = '#f7fff5';
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = 4;
|
||||
rect(ctx, room.x, room.y, room.w, room.h, true, true);
|
||||
ctx.fillStyle = THEME.greenSoft;
|
||||
rect(ctx, room.x + 8, room.y + 8, room.w - 16, room.h - 16, true, false);
|
||||
ctx.fillStyle = THEME.ink;
|
||||
ctx.font = '900 14px ui-monospace, monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('MAINT', room.x + room.w / 2, room.y + 31);
|
||||
ctx.font = '900 11px ui-monospace, monospace';
|
||||
ctx.fillText(`WORKERS ${Math.max(1, Math.floor(room.level || 1))}`, room.x + room.w / 2, room.y + room.h - 18);
|
||||
drawSelection(ctx, game, 'facility', room.id, room.x + room.w / 2, room.y + room.h / 2, room.w + 10, room.h + 10);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawExternalDuct(ctx, f) {
|
||||
if (!f?.entry) return;
|
||||
const c = cellCenter(f.entry.col, f.entry.row);
|
||||
|
|
@ -816,37 +850,39 @@ function drawFloatingTexts(ctx, game) {
|
|||
}
|
||||
|
||||
function drawRepairman(ctx, game) {
|
||||
const r = game.repairman;
|
||||
const visible = !!r?.active || (game.phase === 'build' && !!r?.hiredForNextDay);
|
||||
if (!visible) return;
|
||||
ctx.save();
|
||||
ctx.lineWidth = 3;
|
||||
ctx.fillStyle = '#fff0d1';
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.beginPath(); ctx.arc(r.x, r.y, 17, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = '#ffd6e7';
|
||||
ctx.beginPath(); ctx.arc(r.x - 6, r.y - 2, 3.2, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(r.x + 6, r.y - 2, 3.2, 0, Math.PI * 2); ctx.fill();
|
||||
const workers = Array.isArray(game.repairmen) && game.repairmen.length ? game.repairmen : (game.repairman ? [game.repairman] : []);
|
||||
const visibleWorkers = workers.filter(r => r?.active || (game.phase === 'build' && !!r?.hiredForNextDay));
|
||||
if (!visibleWorkers.length) return;
|
||||
const exploited = !!game.cardEffects?.laborExploitation;
|
||||
ctx.fillStyle = exploited ? THEME.danger : THEME.ink;
|
||||
ctx.beginPath(); ctx.arc(r.x - 5, r.y - 5, exploited ? 2.8 : 2.2, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(r.x + 5, r.y - 5, exploited ? 2.8 : 2.2, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = exploited ? 3 : 2;
|
||||
ctx.beginPath(); ctx.arc(r.x, r.y + (exploited ? 0 : 1), exploited ? 8 : 7, 0.10 * Math.PI, 0.90 * Math.PI); ctx.stroke();
|
||||
ctx.fillStyle = THEME.green;
|
||||
rect(ctx, r.x - 9, r.y + 14, 18, 8, true, true);
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath(); ctx.moveTo(r.x - 11, r.y + 18); ctx.lineTo(r.x - 20, r.y + 24); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(r.x + 11, r.y + 18); ctx.lineTo(r.x + 20, r.y + 24); ctx.stroke();
|
||||
ctx.fillStyle = THEME.ink;
|
||||
ctx.font = '900 11px ui-monospace, monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(r.active ? 'FIX' : 'HIRED', r.x, r.y + 33);
|
||||
if (r.target?.label) label(ctx, r.x, r.y - 24, r.target.label, THEME.green);
|
||||
else if (!r.active && r.hiredForNextDay) label(ctx, r.x, r.y - 24, 'NEXT DAY', THEME.green);
|
||||
ctx.restore();
|
||||
for (const r of visibleWorkers) {
|
||||
ctx.save();
|
||||
ctx.lineWidth = 3;
|
||||
ctx.fillStyle = '#fff0d1';
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.beginPath(); ctx.arc(r.x, r.y, 17, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = '#ffd6e7';
|
||||
ctx.beginPath(); ctx.arc(r.x - 6, r.y - 2, 3.2, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(r.x + 6, r.y - 2, 3.2, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = exploited ? THEME.danger : THEME.ink;
|
||||
ctx.beginPath(); ctx.arc(r.x - 5, r.y - 5, exploited ? 2.8 : 2.2, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.beginPath(); ctx.arc(r.x + 5, r.y - 5, exploited ? 2.8 : 2.2, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = exploited ? 3 : 2;
|
||||
ctx.beginPath(); ctx.arc(r.x, r.y + (exploited ? 0 : 1), exploited ? 8 : 7, 0.10 * Math.PI, 0.90 * Math.PI); ctx.stroke();
|
||||
ctx.fillStyle = THEME.green;
|
||||
rect(ctx, r.x - 9, r.y + 14, 18, 8, true, true);
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath(); ctx.moveTo(r.x - 11, r.y + 18); ctx.lineTo(r.x - 20, r.y + 24); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(r.x + 11, r.y + 18); ctx.lineTo(r.x + 20, r.y + 24); ctx.stroke();
|
||||
ctx.fillStyle = THEME.ink;
|
||||
ctx.font = '900 11px ui-monospace, monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(r.active ? 'FIX' : 'HIRED', r.x, r.y + 33);
|
||||
if (r.target?.label) label(ctx, r.x, r.y - 24, r.target.label, THEME.green);
|
||||
else if (!r.active && r.hiredForNextDay) label(ctx, r.x, r.y - 24, 'NEXT DAY', THEME.green);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
function drawCanvasHints(ctx, game, canvas) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { BALANCE } from '../core/balance.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 { createEggFarm, createScanner, createFacility, createMaintenanceRoom } 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';
|
||||
|
|
@ -12,7 +12,7 @@ import { autoScannerCooldownSeconds } from './cards.js';
|
|||
import { record } from './history.js';
|
||||
import { floating, eraseEffect } from './effects.js';
|
||||
import { createSelectionSystem } from './selectionSystem.js';
|
||||
import { ensureMaintenanceState, remainingPercent, performanceFactor, autoScannerDelayMultiplier, eggSpawnDelayMultiplier, facilityProcessingDelay, durabilityCapFor } from './maintenance.js';
|
||||
import { ensureMaintenanceState, remainingPercent, performanceFactor, autoScannerDelayMultiplier, eggSpawnDelayMultiplier, facilityProcessingDelay, durabilityCapFor, repairmanDailyCost } from './maintenance.js';
|
||||
|
||||
export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanels }) {
|
||||
function updatePanels() { if (onUpdatePanels) onUpdatePanels(); }
|
||||
|
|
@ -21,9 +21,9 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
let conveyorDragRecordedDirectionEdit = false;
|
||||
let conveyorDragStartCell = null;
|
||||
let conveyorDragMoved = false;
|
||||
const BRANCH_MODES = ['random', 'pass', 'up', 'right', 'down', 'left'];
|
||||
const BRANCH_LABELS = { random: 'RND', pass: 'PASS', up: 'UP', right: 'RT', down: 'DN', left: 'LF' };
|
||||
const BRANCH_MARKER = { w: 56, h: 38, y: 0 };
|
||||
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); }
|
||||
|
||||
|
|
@ -45,6 +45,10 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
for (const farm of game.eggFarms || []) farm.row += rowDelta;
|
||||
for (const scanner of game.scanners || []) scanner.row += rowDelta;
|
||||
for (const facility of Object.values(game.facilities || {})) {
|
||||
if (isMaintenanceRoom(facility)) {
|
||||
facility.row += rowDelta;
|
||||
syncMaintenanceRoomRect(facility);
|
||||
}
|
||||
if (facility.entry) facility.entry.row += rowDelta;
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +125,33 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
return Object.values(game.facilities).some(f => !ignoreIds.has(f.id) && rectsOverlap(rect, rectOfFacility(f), 12));
|
||||
}
|
||||
|
||||
function facilityBaseId(f) { return f?.baseId || f?.id || ''; }
|
||||
function isMaintenanceRoom(f) { return facilityBaseId(f) === 'maintenanceRoom'; }
|
||||
function syncMaintenanceRoomRect(room) {
|
||||
if (!room) return;
|
||||
room.cols = room.cols || 2;
|
||||
room.rows = room.rows || 2;
|
||||
room.x = GRID.x + room.col * GRID.cell;
|
||||
room.y = GRID.y + room.row * GRID.cell;
|
||||
room.w = room.cols * GRID.cell;
|
||||
room.h = room.rows * GRID.cell;
|
||||
}
|
||||
function maintenanceRoomCells(col, row, cols = 2, rows = 2) {
|
||||
const cells = [];
|
||||
for (let r = 0; r < rows; r += 1) for (let c = 0; c < cols; c += 1) cells.push({ col: col + c, row: row + r });
|
||||
return cells;
|
||||
}
|
||||
function maintenanceRoomAtCell(col, row, ignoreIds = new Set()) {
|
||||
return Object.values(game.facilities || {}).find(f => {
|
||||
if (!isMaintenanceRoom(f) || ignoreIds.has(f.id)) return false;
|
||||
const cols = f.cols || 2, rows = f.rows || 2;
|
||||
return col >= f.col && col < f.col + cols && row >= f.row && row < f.row + rows;
|
||||
}) || null;
|
||||
}
|
||||
function facilityEntryAtCell(col, row, ignoreIds = new Set()) {
|
||||
return Object.values(game.facilities || {}).find(f => f.entry && !ignoreIds.has(f.id) && f.entry.col === col && f.entry.row === row) || null;
|
||||
}
|
||||
|
||||
function isEquipmentCell(col, row, moving = null) {
|
||||
if (!pointInGrid(col, row)) return false;
|
||||
if (isBlockedCell(col, row)) return true;
|
||||
|
|
@ -130,6 +161,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
const existingScanner = scannerAt(game, col, row);
|
||||
if (existingScanner && !(moving?.type === 'scanner' && moving.ref?.id === existingScanner.id)) return true;
|
||||
if (game.conveyorTiles.has(k) && !(moving?.type === 'conveyor' && moving.oldKey === k)) return true;
|
||||
const ignore = moving?.type === 'facility' && moving.ref?.id ? new Set([moving.ref.id]) : new Set();
|
||||
if (maintenanceRoomAtCell(col, row, ignore)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -143,6 +176,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
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;
|
||||
if (maintenanceRoomAtCell(cell.col, cell.row) || facilityEntryAtCell(cell.col, cell.row)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -225,6 +259,10 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
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;
|
||||
|
|
@ -249,12 +287,28 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
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' && meta.branchMode !== 'pass' && !reachable.includes(meta.branchMode)) meta.branchMode = 'random';
|
||||
if (meta.branchMode === 'pass' && reachable.length < 4) meta.branchMode = 'random';
|
||||
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)));
|
||||
const exits = connected.filter(dir => !physicalEntrances.has(dir));
|
||||
return exits.length >= 2 ? exits : connected;
|
||||
}
|
||||
|
||||
function markConveyorDirection(from, to) {
|
||||
const dir = directionNameBetweenCells(from, to);
|
||||
if (!dir) return false;
|
||||
|
|
@ -271,35 +325,109 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
return changed;
|
||||
}
|
||||
|
||||
function availableConveyorOutDirs(cell) {
|
||||
if (!cell) return [];
|
||||
const neighbors = conveyorNeighborOutDirs(cell);
|
||||
return neighbors.length ? neighbors : DIRS.map(d => d.name);
|
||||
function cycleSingleConveyorDirection(cell) {
|
||||
const neighbors = conveyorNeighborCells(cell);
|
||||
if (neighbors.length <= 1) return cycleIsolatedConveyorDirection(cell);
|
||||
const path = conveyorRunPathFrom(cell);
|
||||
if (path.length < 2) return cycleIsolatedConveyorDirection(cell);
|
||||
record(game);
|
||||
applyRunDirection(path);
|
||||
refreshRoutingAfterEdit(game);
|
||||
updatePanels();
|
||||
return true;
|
||||
}
|
||||
|
||||
function cycleSingleConveyorDirection(cell) {
|
||||
function cycleIsolatedConveyorDirection(cell) {
|
||||
if (!cell) return false;
|
||||
const k = key(cell.col, cell.row);
|
||||
const meta = game.conveyorMeta.get(k);
|
||||
if (!meta) return false;
|
||||
const choices = availableConveyorOutDirs(cell);
|
||||
if (!choices.length) return false;
|
||||
const explicit = normalizeOutDirs(meta);
|
||||
const current = meta.dir && choices.includes(meta.dir) ? meta.dir : explicit.find(dir => choices.includes(dir));
|
||||
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];
|
||||
if (!next) return false;
|
||||
record(game);
|
||||
meta.dir = next;
|
||||
meta.outDirs = [next];
|
||||
meta.branchMode = 'random';
|
||||
game.selected = { type: 'conveyor', id: k };
|
||||
const c = cellCenter(cell.col, cell.row);
|
||||
floating(game, c.x, c.y - 22, branchModeLabel(next), THEME.ink);
|
||||
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';
|
||||
}
|
||||
if (target.length >= 2) {
|
||||
const last = target[target.length - 1];
|
||||
const prev = target[target.length - 2];
|
||||
const meta = game.conveyorMeta.get(key(last.col, last.row));
|
||||
const dir = directionNameBetweenCells(prev, last);
|
||||
if (meta && dir && !isBranchCell(last)) {
|
||||
meta.dir = dir;
|
||||
meta.outDirs = [dir];
|
||||
meta.branchMode = 'random';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function conveyorToolDef() {
|
||||
return BALANCE.facilities[game.buildTool === 'boostConveyor' ? 'boostConveyor' : 'conveyor'];
|
||||
}
|
||||
|
|
@ -310,7 +438,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
const k = key(col, row);
|
||||
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 };
|
||||
if (farmAt(game, col, row) || scannerAt(game, col, row) || maintenanceRoomAtCell(col, row)) return { ok: false, reason: TEXT.fail.cellOccupied };
|
||||
const toolDef = conveyorToolDef();
|
||||
const kind = toolDef.id || 'conveyor';
|
||||
const cost = buildPrice(kind, game);
|
||||
|
|
@ -408,6 +536,34 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
conveyorDragRecordedDirectionEdit = false;
|
||||
}
|
||||
|
||||
function maintenanceRoomPlacementBlocked(col, row, moving = null) {
|
||||
const ignoreIds = moving?.type === 'facility' && moving.ref?.id ? new Set([moving.ref.id]) : new Set();
|
||||
for (const cell of maintenanceRoomCells(col, row)) {
|
||||
if (!pointInGrid(cell.col, cell.row) || isBlockedCell(cell.col, cell.row)) return true;
|
||||
if (farmAt(game, cell.col, cell.row) || scannerAt(game, cell.col, cell.row)) return true;
|
||||
if (game.conveyorTiles.has(key(cell.col, cell.row))) return true;
|
||||
if (maintenanceRoomAtCell(cell.col, cell.row, ignoreIds)) return true;
|
||||
if (facilityEntryAtCell(cell.col, cell.row, ignoreIds)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function buildMaintenanceRoom(col, row) {
|
||||
if (maintenanceRoomPlacementBlocked(col, row)) return fail(TEXT.fail.cellOccupied);
|
||||
const cost = buildPrice('maintenanceRoom', game);
|
||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||
record(game);
|
||||
spendCash(game, cost);
|
||||
const room = applyBuildQuality(createMaintenanceRoom(game, col, row, cost));
|
||||
const id = game.facilities.maintenanceRoom ? `maintenanceRoom:${game.nextId++}` : 'maintenanceRoom';
|
||||
room.id = id;
|
||||
room.baseId = 'maintenanceRoom';
|
||||
game.facilities[id] = room;
|
||||
game.selected = { type: 'facility', id };
|
||||
floating(game, room.x + room.w / 2, room.y + room.h / 2, `-${yen(cost)}`, THEME.ink);
|
||||
updatePanels();
|
||||
}
|
||||
|
||||
function buildAtCell(cell) {
|
||||
if (!cell) return fail(TEXT.fail.outOfGrid);
|
||||
const { col, row } = cell;
|
||||
|
|
@ -417,6 +573,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
refreshRoutingAfterEdit(game);
|
||||
return;
|
||||
}
|
||||
if (game.buildTool === 'maintenanceRoom') return buildMaintenanceRoom(col, row);
|
||||
if (isBlockedCell(col, row)) return fail('Cannot build on blocked ground.');
|
||||
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);
|
||||
|
|
@ -426,7 +583,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function buildAtPoint(p) {
|
||||
const cell = pointToCell(p.x, p.y);
|
||||
if (['conveyor', 'boostConveyor', 'eggFarm', 'manualScanner', 'autoScanner'].includes(game.buildTool)) return buildAtCell(cell);
|
||||
if (['conveyor', 'boostConveyor', 'eggFarm', 'manualScanner', 'autoScanner', 'maintenanceRoom'].includes(game.buildTool)) return buildAtCell(cell);
|
||||
if (MACHINE_FACILITY_IDS.includes(game.buildTool)) return buildFacility(p, game.buildTool);
|
||||
}
|
||||
|
||||
|
|
@ -714,9 +871,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
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'));
|
||||
if (obj.type === 'conveyor' && isBranchKey(obj.id)) {
|
||||
ui.modalActions.appendChild(modalButton(`Branch: ${branchModeLabel(game.conveyorMeta.get(obj.id)?.branchMode || 'random')}`, () => { cycleBranchModeForKey(obj.id); showSelectedMenu(); }, 'facility-action warn'));
|
||||
}
|
||||
const hit = obj.type === 'conveyor' ? { type: 'conveyor', oldKey: obj.id, ref: obj } : { type: obj.type, ref: obj };
|
||||
const saleReason = lastProtectedSaleReason(hit);
|
||||
if (saleReason) {
|
||||
|
|
@ -764,12 +918,12 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
}
|
||||
|
||||
function branchOutDirsForKey(k) {
|
||||
return reachableOutDirsForKey(k);
|
||||
return branchExitDirsForKey(k);
|
||||
}
|
||||
|
||||
function isBranchKey(k) {
|
||||
if (!game.conveyorTiles.has(k)) return false;
|
||||
return conveyorNeighborOutDirs(parseKey(k)).length >= 3;
|
||||
return isBranchCell(parseKey(k));
|
||||
}
|
||||
|
||||
function branchSwitcherAtPoint(p) {
|
||||
|
|
@ -788,21 +942,21 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
function cycleBranchModeAtPoint(p) {
|
||||
const k = branchSwitcherAtPoint(p);
|
||||
if (!k) return false;
|
||||
return cycleBranchModeForKey(k);
|
||||
cycleBranchModeForKey(k);
|
||||
return true;
|
||||
}
|
||||
|
||||
function cycleBranchModeForKey(k) {
|
||||
const meta = game.conveyorMeta.get(k);
|
||||
if (!meta) return false;
|
||||
const reachable = reachableOutDirsForKey(k);
|
||||
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', ...(reachable.length >= 4 ? ['pass'] : []), ...DIRS.map(d => d.name).filter(mode => reachable.includes(mode))];
|
||||
const modes = ['random', ...DIRS.map(d => d.name).filter(mode => reachable.includes(mode))];
|
||||
meta.branchMode = modes[(Math.max(0, modes.indexOf(current)) + 1) % modes.length];
|
||||
game.selected = { type: 'conveyor', id: k };
|
||||
const c = cellCenter(...Object.values(parseKey(k)));
|
||||
floating(game, c.x, c.y - 22, branchModeLabel(meta.branchMode), THEME.ink);
|
||||
game.selected = null;
|
||||
game.multiSelected = [];
|
||||
updatePanels();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -841,13 +995,21 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
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 exits.');
|
||||
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 (facilityKind === 'maintenanceRoom') {
|
||||
const workers = Math.max(1, Math.floor(obj.level || 1));
|
||||
lines.push(`Workers: ${workers}`);
|
||||
lines.push(`Wage: ${yen(repairmanDailyCost(game))} / worker / day`);
|
||||
lines.push(`Daily cost: ${yen(workers * repairmanDailyCost(game))}`);
|
||||
lines.push(`Footprint: ${obj.cols || 2}x${obj.rows || 2} cells`);
|
||||
return lines;
|
||||
}
|
||||
lines.push(['mixer', 'truck', 'trash'].includes(facilityKind) ? `Level: ${obj.level} / no cap` : `Level: ${obj.level}`);
|
||||
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') {
|
||||
|
|
@ -885,6 +1047,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
if (obj.type === 'eggFarm') return 'A tiny gatehouse producing questionable eggs on schedule.';
|
||||
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.baseId || obj.id) === 'maintenanceRoom') return 'Spawns repair workers at day start. Upgrade cards add workers.';
|
||||
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.';
|
||||
|
|
|
|||
|
|
@ -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, repairAllEquipment } from './maintenance.js';
|
||||
import { averageConveyorPerformance, autoScannerDelayMultiplier, ensureMaintenanceState, maintenanceRoomWorkerCount, repairAllEquipment, repairmanDailyCost } from './maintenance.js';
|
||||
import { scannerCenter, refreshRoutingAfterEdit } from './routing.js';
|
||||
|
||||
const COMMON_WEIGHT = CARD_BALANCE.commonWeight;
|
||||
|
|
@ -118,6 +118,10 @@ 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.baseId || target.id) === 'maintenanceRoom') {
|
||||
const before = maintenanceRoomWorkerCount(target);
|
||||
return `MAINT ROOM workers ${before} -> ${before + 1} | wage ${yen(repairmanDailyCost(game))}/worker/day`;
|
||||
}
|
||||
if (target.type === 'facility' && (target.baseId || target.id) === 'mixer') {
|
||||
const beforeLevel = target.level;
|
||||
const before = upgradedMixerPrice(game);
|
||||
|
|
@ -161,6 +165,7 @@ 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 === 'maintenanceRoom') return Object.values(game.facilities || {}).filter(f => (f.baseId || f.id) === 'maintenanceRoom');
|
||||
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()
|
||||
|
|
@ -186,7 +191,8 @@ function cardAtCap(game, card) {
|
|||
|
||||
function repairmanCardVisible(game) {
|
||||
const r = game.repairman || {};
|
||||
return !!(r.active || r.hiredForNextDay || game.pendingRepairWorkerWages > 0);
|
||||
const hasMaintenanceRoom = Object.values(game.facilities || {}).some(f => (f.baseId || f.id) === 'maintenanceRoom');
|
||||
return !!(hasMaintenanceRoom || r.active || r.hiredForNextDay || game.pendingRepairWorkerWages > 0);
|
||||
}
|
||||
|
||||
function availableCards(game) {
|
||||
|
|
@ -430,6 +436,11 @@ function cardDescription(game, card) {
|
|||
const after = autoScannerCooldownSeconds({ ...scanner, level: (scanner.level || 1) + 1 }, game);
|
||||
return `Choose one AUTO SCANNER. Level +1. Cooldown ${before.toFixed(2)}s -> ${after.toFixed(2)}s.`;
|
||||
}
|
||||
if (card.id === 'upgradeMaintenanceRoom') {
|
||||
const room = targetsForCard(game, card)[0] || { level: 1 };
|
||||
const workers = maintenanceRoomWorkerCount(room);
|
||||
return `Choose one MAINTENANCE ROOM. Repair workers ${workers} -> ${workers + 1}. Wage ${yen(repairmanDailyCost(game))} per worker per day.`;
|
||||
}
|
||||
if (card.id === 'upgradeMixer') {
|
||||
const preview = incomeUpgradePreview(game, 'mixer', ECONOMY.income.mixer);
|
||||
return `Choose one MIXER. Earning +10%. Male-chick income ${yen(preview.before)} -> ${yen(preview.after)}.`;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { DIRS, GRID, THEME } from '../core/config.js';
|
||||
import { createChick } from '../core/entities.js';
|
||||
import { nextSpawnDelay } from '../core/state.js';
|
||||
import { key, parseKey, pointToCell, cellCenter, randomBetween, yen, inGrid, directionNameBetweenCells } from '../core/utils.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, scannerQueueSpacingMultiplier } from './cards.js';
|
||||
|
|
@ -239,6 +239,23 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
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;
|
||||
if (branchMode && branchMode !== 'random' && branchMode !== 'pass' && connected.includes(branchMode)) return [branchMode];
|
||||
const entrances = new Set(connected.filter(dir => neighborPointsIntoCell(cell, dir)));
|
||||
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));
|
||||
}
|
||||
|
|
@ -246,16 +263,6 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
function pickMovementOption(chick, cell, dirs, branchMode = 'random') {
|
||||
const candidates = dirs.map(dir => movementOptionFromDir(cell, dir)).filter(Boolean);
|
||||
if (!candidates.length) return null;
|
||||
if (branchMode === 'pass') {
|
||||
const previous = chick.prevConveyorCell ? parseKey(chick.prevConveyorCell) : null;
|
||||
const forward = previous ? directionNameBetweenCells(previous, cell) : null;
|
||||
const pass = forward ? candidates.find(opt => opt.dirName === forward) : null;
|
||||
if (optionAvailableForBranch(chick, pass)) return pass;
|
||||
}
|
||||
if (branchMode && branchMode !== 'random') {
|
||||
const preferred = candidates.find(opt => opt.dirName === branchMode);
|
||||
if (optionAvailableForBranch(chick, preferred)) return preferred;
|
||||
}
|
||||
const moving = candidates.filter(opt => opt.type !== 'facility');
|
||||
const empty = moving.filter(opt => !targetBlockedByChick(chick, opt.target));
|
||||
const pool = empty.length ? empty : candidates.filter(opt => opt.type === 'facility');
|
||||
|
|
@ -277,7 +284,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
const currentKey = key(cell.col, cell.row);
|
||||
if (!game.conveyorTiles.has(currentKey)) return removeChick(index, 'OFF BELT');
|
||||
const meta = game.conveyorMeta?.get(currentKey) || {};
|
||||
const dirs = conveyorOutDirNames(currentKey);
|
||||
const dirs = branchExitDirNames(cell, conveyorOutDirNames(currentKey), meta.branchMode || 'random');
|
||||
if (!dirs.length) {
|
||||
chick.stoppedTimer = 0.25;
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function equipmentBasePrice(objOrHit) {
|
|||
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;
|
||||
if (type === 'facility') return obj.price || FACILITY_DEFS[obj.baseId || obj.id]?.price || 0;
|
||||
return FACILITY_DEFS[obj.id]?.price || 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { BALANCE } from '../core/balance.js';
|
||||
import { GRID, THEME } from '../core/config.js';
|
||||
import { cellCenter, parseKey, key, yen } from '../core/utils.js';
|
||||
import { cellCenter, parseKey, key, yen, pointToCell, inGrid } from '../core/utils.js';
|
||||
import { isOwnedCell } from '../core/gridExpansion.js';
|
||||
import { spendCash } from './economy.js';
|
||||
import { floating } from './effects.js';
|
||||
|
||||
const RULES = BALANCE.maintenance;
|
||||
const REPAIR_INTEREST_WEAR_THRESHOLD = 0.20;
|
||||
|
||||
function effectCount(game, id) {
|
||||
return Math.max(0, Number(game?.cardEffects?.[id]) || 0);
|
||||
|
|
@ -30,6 +32,44 @@ export function repairmanDailyCost(game) {
|
|||
return game?.cardEffects?.laborExploitation ? Math.ceil(base * 0.7) : base;
|
||||
}
|
||||
|
||||
function facilityKind(target) {
|
||||
return target?.baseId || target?.id || target;
|
||||
}
|
||||
|
||||
export function maintenanceRooms(game) {
|
||||
return Object.values(game?.facilities || {}).filter(f => facilityKind(f) === 'maintenanceRoom');
|
||||
}
|
||||
|
||||
export function maintenanceRoomWorkerCount(room) {
|
||||
return Math.max(1, Math.floor(Number(room?.level) || 1));
|
||||
}
|
||||
|
||||
export function maintenanceWorkerCount(game) {
|
||||
return maintenanceRooms(game).reduce((sum, room) => sum + maintenanceRoomWorkerCount(room), 0);
|
||||
}
|
||||
|
||||
export function repairWorkerWageForDay(game) {
|
||||
return maintenanceWorkerCount(game) * repairmanDailyCost(game);
|
||||
}
|
||||
|
||||
function roomSpawnPoint(room, index = 0, total = 1) {
|
||||
const cols = Math.max(1, Math.ceil(Math.sqrt(total)));
|
||||
const row = Math.floor(index / cols);
|
||||
const col = index % cols;
|
||||
const spacing = 12;
|
||||
return {
|
||||
x: room.x + room.w / 2 + (col - (cols - 1) / 2) * spacing,
|
||||
y: room.y + room.h / 2 + (row - Math.floor((total - 1) / cols) / 2) * spacing
|
||||
};
|
||||
}
|
||||
|
||||
function defaultRepairmanPosition() {
|
||||
return {
|
||||
x: GRID.x + GRID.cell * 0.5,
|
||||
y: GRID.y + GRID.rows * GRID.cell + 84
|
||||
};
|
||||
}
|
||||
|
||||
function qualityMultiplierOf(objOrMeta) {
|
||||
return Math.max(0.1, Number(objOrMeta?.durabilityBaseMultiplier) || 1);
|
||||
}
|
||||
|
|
@ -39,12 +79,12 @@ export function ensureMaintenanceState(game) {
|
|||
game.repairman = {
|
||||
hiredForNextDay: false,
|
||||
active: false,
|
||||
x: GRID.x + GRID.cell * 0.5,
|
||||
y: GRID.y + GRID.rows * GRID.cell + 84,
|
||||
...defaultRepairmanPosition(),
|
||||
target: null,
|
||||
repairedToday: 0
|
||||
};
|
||||
}
|
||||
if (!Array.isArray(game.repairmen)) game.repairmen = [];
|
||||
for (const [k, meta] of game.conveyorMeta || []) {
|
||||
if (meta.uses == null) meta.uses = 0;
|
||||
if (meta.maintenanceType == null) meta.maintenanceType = 'conveyor';
|
||||
|
|
@ -59,8 +99,9 @@ export function ensureMaintenanceState(game) {
|
|||
if (scanner.kind === 'auto') attachMaintenance(scanner, 'autoScanner', game);
|
||||
}
|
||||
for (const f of Object.values(game.facilities || {})) {
|
||||
if (f.id === 'mixer') attachMaintenance(f, 'mixer', game);
|
||||
if (f.id === 'trash') attachMaintenance(f, 'trash', game);
|
||||
const id = facilityKind(f);
|
||||
if (id === 'mixer') attachMaintenance(f, 'mixer', game);
|
||||
if (id === 'trash') attachMaintenance(f, 'trash', game);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -141,10 +182,6 @@ export function eggSpawnDelayMultiplier(farm) {
|
|||
return delayMultiplier(farm);
|
||||
}
|
||||
|
||||
function facilityKind(target) {
|
||||
return target?.baseId || target?.id || target;
|
||||
}
|
||||
|
||||
function facilityFromTarget(game, target) {
|
||||
if (!target) return null;
|
||||
if (typeof target === 'object') return target;
|
||||
|
|
@ -213,14 +250,16 @@ export function equipmentMaintenanceTargets(game) {
|
|||
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']) {
|
||||
const f = game.facilities?.[id];
|
||||
if (f) targets.push({ type: id, ref: f, label: f.shortName || f.name || id, center: { x: f.x + f.w / 2, y: f.y + f.h / 2 } });
|
||||
for (const f of Object.values(game.facilities || {})) {
|
||||
const id = facilityKind(f);
|
||||
if (!['mixer', 'trash'].includes(id)) continue;
|
||||
targets.push({ type: id, ref: f, label: f.shortName || f.name || id, center: { x: f.x + f.w / 2, y: f.y + f.h / 2 } });
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
function targetWear(t) { return t.meta ? wearRatio(t) : wearRatio(t.ref); }
|
||||
function targetNeedsRepair(t) { return targetWear(t) > REPAIR_INTEREST_WEAR_THRESHOLD; }
|
||||
function targetUses(t) { return t.meta ? usesOf(t) : usesOf(t.ref); }
|
||||
function targetDurability(t) { return t.meta ? durabilityOf(t) : durabilityOf(t.ref); }
|
||||
function reduceTargetUses(t, amount) { setUses(t.meta ? t : t.ref, Math.max(0, targetUses(t) - amount)); }
|
||||
|
|
@ -252,14 +291,36 @@ export function hireRepairmanForNextDay(game) {
|
|||
|
||||
export function activateRepairmanForDay(game) {
|
||||
ensureMaintenanceState(game);
|
||||
game.repairman.active = !!game.repairman.hiredForNextDay;
|
||||
game.repairman.hiredForNextDay = false;
|
||||
game.repairman.target = null;
|
||||
game.repairman.repairedToday = 0;
|
||||
const rooms = maintenanceRooms(game);
|
||||
const workers = [];
|
||||
for (const room of rooms) {
|
||||
const count = maintenanceRoomWorkerCount(room);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const p = roomSpawnPoint(room, i, count);
|
||||
workers.push({
|
||||
id: `${room.id}:worker:${i}`,
|
||||
homeRoomId: room.id,
|
||||
hiredForNextDay: false,
|
||||
active: true,
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
target: null,
|
||||
repairedToday: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
game.repairmen = workers;
|
||||
const legacy = game.repairman || { ...defaultRepairmanPosition() };
|
||||
game.repairman = workers[0] || { ...legacy, active: false, hiredForNextDay: false, target: null, repairedToday: 0 };
|
||||
return { count: workers.length, wages: workers.length * repairmanDailyCost(game) };
|
||||
}
|
||||
|
||||
export function deactivateRepairman(game) {
|
||||
ensureMaintenanceState(game);
|
||||
for (const r of game.repairmen || []) {
|
||||
r.active = false;
|
||||
r.target = null;
|
||||
}
|
||||
game.repairman.active = false;
|
||||
game.repairman.target = null;
|
||||
}
|
||||
|
|
@ -270,29 +331,114 @@ function resolveStoredTarget(game, stored) {
|
|||
return targets.find(t => targetKey(t) === stored.key) || null;
|
||||
}
|
||||
|
||||
function chooseRepairTarget(game) {
|
||||
function repairWalkableCell(game, col, row) {
|
||||
return inGrid(col, row) && isOwnedCell(game, col, row) && !game.blockedCells?.has?.(key(col, row));
|
||||
}
|
||||
|
||||
function nearestRepairWalkableCell(game, point) {
|
||||
const direct = pointToCell(point.x, point.y);
|
||||
if (direct && repairWalkableCell(game, direct.col, direct.row)) return direct;
|
||||
let best = null;
|
||||
let bestDist = Infinity;
|
||||
for (let row = 0; row < GRID.rows; row += 1) {
|
||||
for (let col = 0; col < GRID.cols; col += 1) {
|
||||
if (!repairWalkableCell(game, col, row)) continue;
|
||||
const c = cellCenter(col, row);
|
||||
const dist = Math.hypot(c.x - point.x, c.y - point.y);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = { col, row };
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function repairApproachPoint(_game, target) {
|
||||
const ref = target?.ref || null;
|
||||
if (ref?.entry && Number.isFinite(ref.entry.col) && Number.isFinite(ref.entry.row)) return cellCenter(ref.entry.col, ref.entry.row);
|
||||
return target?.center || null;
|
||||
}
|
||||
|
||||
function repairCellPath(game, fromPoint, toPoint) {
|
||||
const start = nearestRepairWalkableCell(game, fromPoint);
|
||||
const goal = nearestRepairWalkableCell(game, toPoint);
|
||||
if (!start || !goal) return null;
|
||||
const startKey = key(start.col, start.row);
|
||||
const goalKey = key(goal.col, goal.row);
|
||||
const queue = [start];
|
||||
const cameFrom = new Map([[startKey, null]]);
|
||||
for (let qi = 0; qi < queue.length; qi += 1) {
|
||||
const cur = queue[qi];
|
||||
const ck = key(cur.col, cur.row);
|
||||
if (ck === goalKey) break;
|
||||
for (const d of [{ dc: 1, dr: 0 }, { dc: -1, dr: 0 }, { dc: 0, dr: 1 }, { dc: 0, dr: -1 }]) {
|
||||
const next = { col: cur.col + d.dc, row: cur.row + d.dr };
|
||||
if (!repairWalkableCell(game, next.col, next.row)) continue;
|
||||
const nk = key(next.col, next.row);
|
||||
if (cameFrom.has(nk)) continue;
|
||||
cameFrom.set(nk, ck);
|
||||
queue.push(next);
|
||||
}
|
||||
}
|
||||
if (!cameFrom.has(goalKey)) return null;
|
||||
const cells = [];
|
||||
let curKey = goalKey;
|
||||
while (curKey) {
|
||||
const p = parseKey(curKey);
|
||||
cells.push(p);
|
||||
curKey = cameFrom.get(curKey);
|
||||
}
|
||||
cells.reverse();
|
||||
return cells;
|
||||
}
|
||||
|
||||
function repairWaypoints(game, r, target) {
|
||||
const approach = repairApproachPoint(game, target);
|
||||
if (!approach) return null;
|
||||
const cellPath = repairCellPath(game, { x: r.x, y: r.y }, approach);
|
||||
if (!cellPath) return null;
|
||||
const waypoints = cellPath.slice(1).map(c => cellCenter(c.col, c.row));
|
||||
const last = waypoints[waypoints.length - 1] || cellCenter(cellPath[0].col, cellPath[0].row);
|
||||
if (Math.hypot(last.x - approach.x, last.y - approach.y) > 2) waypoints.push(approach);
|
||||
if (!waypoints.length) waypoints.push(approach);
|
||||
return waypoints;
|
||||
}
|
||||
|
||||
function canRepairmanReachTarget(game, r, target) {
|
||||
return !!repairWaypoints(game, r || defaultRepairmanPosition(), target);
|
||||
}
|
||||
|
||||
function chooseRepairTarget(game, r) {
|
||||
let best = null;
|
||||
for (const t of equipmentMaintenanceTargets(game)) {
|
||||
const w = targetWear(t);
|
||||
if (w <= 0.001) continue;
|
||||
if (!targetNeedsRepair(t)) continue;
|
||||
if (r && !canRepairmanReachTarget(game, r, t)) continue;
|
||||
if (!best || w > targetWear(best)) best = t;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
export function updateRepairman(game, dt) {
|
||||
ensureMaintenanceState(game);
|
||||
const r = game.repairman;
|
||||
function updateSingleRepairman(game, r, dt) {
|
||||
if (!r.active || game.phase !== 'running') return;
|
||||
let target = resolveStoredTarget(game, r.target) || chooseRepairTarget(game);
|
||||
let target = resolveStoredTarget(game, r.target);
|
||||
if (target && (!targetNeedsRepair(target) || !canRepairmanReachTarget(game, r, target))) target = null;
|
||||
target = target || chooseRepairTarget(game, r);
|
||||
if (!target) {
|
||||
r.target = null;
|
||||
return;
|
||||
}
|
||||
r.target = { key: targetKey(target), label: target.label };
|
||||
const waypoints = repairWaypoints(game, r, target);
|
||||
if (!waypoints?.length) {
|
||||
r.target = null;
|
||||
return;
|
||||
}
|
||||
const waypoint = waypoints[0];
|
||||
const speed = RULES.repairman.walkSpeed;
|
||||
const dx = target.center.x - r.x;
|
||||
const dy = target.center.y - r.y;
|
||||
const dx = waypoint.x - r.x;
|
||||
const dy = waypoint.y - r.y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist > 8) {
|
||||
const step = Math.min(dist, speed * dt);
|
||||
|
|
@ -300,14 +446,23 @@ export function updateRepairman(game, dt) {
|
|||
r.y += dy / dist * step;
|
||||
return;
|
||||
}
|
||||
if (waypoints.length > 1 || Math.hypot((repairApproachPoint(game, target)?.x || waypoint.x) - r.x, (repairApproachPoint(game, target)?.y || waypoint.y) - r.y) > 10) return;
|
||||
const repairWear = dt / (RULES.repairman.secondsPerOnePercent * 100);
|
||||
const usesBefore = targetUses(target);
|
||||
const maxAllowedUses = targetDurability(target) * REPAIR_INTEREST_WEAR_THRESHOLD;
|
||||
const usesDelta = repairWear * targetDurability(target);
|
||||
reduceTargetUses(target, usesDelta);
|
||||
setUses(target.meta ? target : target.ref, Math.max(maxAllowedUses, targetUses(target) - usesDelta));
|
||||
const repairedPct = Math.max(0, (usesBefore - targetUses(target)) / targetDurability(target) * 100);
|
||||
r.repairedToday = (r.repairedToday || 0) + repairedPct;
|
||||
if (repairedPct > 0 && Math.random() < 0.02) floating(game, r.x, r.y - 28, 'REPAIR', THEME.green);
|
||||
if (targetWear(target) <= 0.001) r.target = null;
|
||||
if (!targetNeedsRepair(target)) r.target = null;
|
||||
}
|
||||
|
||||
export function updateRepairman(game, dt) {
|
||||
ensureMaintenanceState(game);
|
||||
const workers = Array.isArray(game.repairmen) && game.repairmen.length ? game.repairmen : (game.repairman ? [game.repairman] : []);
|
||||
for (const r of workers) updateSingleRepairman(game, r, dt);
|
||||
if (workers.length) game.repairman = workers[0];
|
||||
}
|
||||
|
||||
function worstDegradation(game) {
|
||||
|
|
@ -323,5 +478,12 @@ function worstDegradation(game) {
|
|||
export function maintenanceSummary(game) {
|
||||
const worst = worstDegradation(game);
|
||||
const avgBeltPerformance = averageConveyorPerformance(game);
|
||||
return { worst, avgBeltPerformance, repairman: game.repairman || null };
|
||||
return {
|
||||
worst,
|
||||
avgBeltPerformance,
|
||||
repairman: game.repairman || null,
|
||||
repairmen: Array.isArray(game.repairmen) ? game.repairmen : [],
|
||||
workers: maintenanceWorkerCount(game),
|
||||
dailyWage: repairWorkerWageForDay(game)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,12 +133,20 @@ function neighborPointsIntoCell(game, p, dirName) {
|
|||
|
||||
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;
|
||||
return explicit.length ? explicit : inferred;
|
||||
const dirs = explicit.length ? explicit : inferred;
|
||||
if (neighbors.length < 3) return dirs;
|
||||
if (meta.branchMode && meta.branchMode !== 'random' && neighbors.includes(meta.branchMode)) return [meta.branchMode];
|
||||
const entrances = new Set(neighbors.filter(dir => neighborPointsIntoCell(game, p, dir)));
|
||||
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) {
|
||||
|
|
@ -306,13 +314,6 @@ function dirBetween(a, b) {
|
|||
return DIRS.find(d => d.dc === dc && d.dr === dr)?.name || null;
|
||||
}
|
||||
|
||||
function isPassCross(game, graph, cellKey) {
|
||||
if (game.conveyorMeta?.get?.(cellKey)?.branchMode !== 'pass') return false;
|
||||
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');
|
||||
}
|
||||
|
||||
export function bfsAllRoutes(game, start, isGoal) {
|
||||
const graph = ensureFactoryGraph(game);
|
||||
const startK = graphNodeKey(start.col, start.row);
|
||||
|
|
@ -343,7 +344,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' && isPassCross(game, graph, cur.key) && outDir !== cur.incoming) continue;
|
||||
const nextDepth = cur.depth + 1;
|
||||
if (nextDepth > bestGoalDepth) continue;
|
||||
const nextStateK = `${n.key}|${outDir}`;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,25 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
|||
return `${item.type}:${item.id || item.ref?.id}`;
|
||||
}
|
||||
|
||||
function facilityKind(f) { return f?.baseId || f?.id || ''; }
|
||||
function isMaintenanceRoom(f) { return facilityKind(f) === 'maintenanceRoom'; }
|
||||
function maintenanceRoomFootprint(room, col = room?.col, row = room?.row) {
|
||||
const cells = [];
|
||||
const cols = room?.cols || 2;
|
||||
const rows = room?.rows || 2;
|
||||
for (let r = 0; r < rows; r += 1) for (let c = 0; c < cols; c += 1) cells.push({ col: col + c, row: row + r });
|
||||
return cells;
|
||||
}
|
||||
function syncMaintenanceRoomRect(room) {
|
||||
if (!room) return;
|
||||
room.cols = room.cols || 2;
|
||||
room.rows = room.rows || 2;
|
||||
room.x = GRID.x + room.col * GRID.cell;
|
||||
room.y = GRID.y + room.row * GRID.cell;
|
||||
room.w = room.cols * GRID.cell;
|
||||
room.h = room.rows * GRID.cell;
|
||||
}
|
||||
|
||||
function selectionFromHit(hit) {
|
||||
if (!hit) return null;
|
||||
if (hit.type === 'conveyor') return { type: 'conveyor', id: hit.oldKey };
|
||||
|
|
@ -118,7 +137,10 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
|||
return { ...sel, obj, oldKey: sel.id, currentKey: sel.id, meta: game.conveyorMeta.get(sel.id) || { price: buildPrice('conveyor'), builtSession: null }, col: cell.col, row: cell.row };
|
||||
}
|
||||
if (sel.type === 'eggFarm' || sel.type === 'scanner') return { ...sel, obj, col: obj.col, row: obj.row };
|
||||
if (sel.type === 'facility') return { ...sel, obj, x: obj.x, y: obj.y, w: obj.w, h: obj.h, entry: obj.entry ? { ...obj.entry } : null, side: obj.side, center: { x: obj.x + obj.w / 2, y: obj.y + obj.h / 2 } };
|
||||
if (sel.type === 'facility') {
|
||||
if (isMaintenanceRoom(obj)) return { ...sel, obj, col: obj.col, row: obj.row, cols: obj.cols || 2, rows: obj.rows || 2, maintenanceRoom: true, center: { x: obj.x + obj.w / 2, y: obj.y + obj.h / 2 } };
|
||||
return { ...sel, obj, x: obj.x, y: obj.y, w: obj.w, h: obj.h, entry: obj.entry ? { ...obj.entry } : null, side: obj.side, center: { x: obj.x + obj.w / 2, y: obj.y + obj.h / 2 } };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -150,13 +172,17 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
|||
const scanner = scannerAt(game, col, row);
|
||||
if (scanner && !selectedTokens.has(`scanner:${scanner.id}`)) return true;
|
||||
if (game.conveyorTiles.has(k) && !selectedTokens.has(`conveyor:${k}`)) return true;
|
||||
for (const f of Object.values(game.facilities || {})) {
|
||||
if (!isMaintenanceRoom(f) || selectedTokens.has(`facility:${f.id}`)) continue;
|
||||
if (maintenanceRoomFootprint(f).some(cell => cell.col === col && cell.row === row)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function facilityDragWouldOverlap(dx, dy, selectedTokens) {
|
||||
const candidateRects = [];
|
||||
for (const origin of game.groupDrag.origins) {
|
||||
if (origin.type !== 'facility') continue;
|
||||
if (origin.type !== 'facility' || origin.maintenanceRoom) continue;
|
||||
const nextCenter = { x: origin.center.x + dx, y: origin.center.y + dy };
|
||||
const { entry, side } = nearestGridEdge(nextCenter);
|
||||
if (!pointInGrid(entry.col, entry.row)) return true;
|
||||
|
|
@ -190,15 +216,16 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
|||
const selectedTokens = new Set(game.groupDrag.selections.map(selectionToken));
|
||||
const targetCells = new Set();
|
||||
for (const origin of game.groupDrag.origins) {
|
||||
if (origin.type === 'facility') continue;
|
||||
if (origin.type === 'facility' && !origin.maintenanceRoom) continue;
|
||||
const col = origin.col + dcol, row = origin.row + drow;
|
||||
const cells = origin.type === 'scanner' ? scannerFootprintCells({ col, row }) : [{ col, row }];
|
||||
const cells = origin.maintenanceRoom ? maintenanceRoomFootprint(origin.obj, col, row) : (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');
|
||||
if (origin.maintenanceRoom && Object.values(game.facilities || {}).some(f => f.entry && !selectedTokens.has(`facility:${f.id}`) && f.entry.col === cell.col && f.entry.row === cell.row)) return fail('Cell occupied');
|
||||
targetCells.add(tk);
|
||||
}
|
||||
}
|
||||
|
|
@ -225,9 +252,15 @@ export function createSelectionSystem({ game, canvasPoint, updatePanels, equipme
|
|||
origin.obj.col = origin.col + dcol;
|
||||
origin.obj.row = origin.row + drow;
|
||||
} else if (origin.type === 'facility') {
|
||||
const nextCenter = { x: origin.center.x + dx, y: origin.center.y + dy };
|
||||
const { entry, side } = nearestGridEdge(nextCenter);
|
||||
layoutFacilityOnEdge(origin.obj, entry, side);
|
||||
if (origin.maintenanceRoom) {
|
||||
origin.obj.col = origin.col + dcol;
|
||||
origin.obj.row = origin.row + drow;
|
||||
syncMaintenanceRoomRect(origin.obj);
|
||||
} else {
|
||||
const nextCenter = { x: origin.center.x + dx, y: origin.center.y + dy };
|
||||
const { entry, side } = nearestGridEdge(nextCenter);
|
||||
layoutFacilityOnEdge(origin.obj, entry, side);
|
||||
}
|
||||
}
|
||||
}
|
||||
game.groupDrag.selections = game.groupDrag.origins.map(o => o.type === 'conveyor' ? { type: 'conveyor', id: o.currentKey || o.oldKey } : { type: o.type, id: o.id });
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import { TEXT } from '../core/text.js';
|
|||
import { routeFromFarmToScanner, factoryReady, facilityConnectionIssues } from './routing.js';
|
||||
import { buildPrice, fairiesTributeInfo, finalScore, maleTruckPenalty, mixerPoopPenalty, truckPoopPenalty, zundaTaxInfo } from './economy.js';
|
||||
import { truckTarget } from './contracts.js';
|
||||
import { conveyorSpeedForGame } from './cards.js';
|
||||
import { maintenanceSummary, repairmanDailyCost } from './maintenance.js';
|
||||
import { CARD_DEFS, conveyorSpeedForGame } from './cards.js';
|
||||
import { maintenanceSummary } from './maintenance.js';
|
||||
|
||||
export function createUISystem({ game, ui, build, startGame, beginCardDraft, activeQueuedChick }) {
|
||||
function escapeHtml(value) {
|
||||
|
|
@ -29,6 +29,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
updateBuildStatus();
|
||||
updateContractPanel();
|
||||
updateTurnSummary();
|
||||
updateAcquiredCardsPanel();
|
||||
updateHistoryButtons();
|
||||
}
|
||||
|
||||
|
|
@ -56,15 +57,6 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
: '';
|
||||
}
|
||||
if (ui.buttons.erase) ui.buttons.erase.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending;
|
||||
if (ui.hireRepairmanButton) {
|
||||
const hired = !!game.repairman?.hiredForNextDay;
|
||||
const cost = repairmanDailyCost(game);
|
||||
const blocked = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || hired || game.cash < cost;
|
||||
ui.hireRepairmanButton.disabled = blocked;
|
||||
ui.hireRepairmanButton.classList.toggle('active', hired);
|
||||
ui.hireRepairmanButton.classList.toggle('unaffordable', game.cash < cost);
|
||||
ui.hireRepairmanButton.title = hired ? 'Repairman is hired for the next production phase.' : `Hire for ${yen(cost)} for one day. Repairs 2% durability per second.`;
|
||||
}
|
||||
const cashout = noAffordableBuildTools();
|
||||
ui.buttons.nextTurn?.classList.toggle('cashout-emphasis', cashout);
|
||||
ui.buttons.erase?.classList.toggle('cashout-emphasis', cashout);
|
||||
|
|
@ -81,6 +73,50 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
: TEXT.status.sorting;
|
||||
}
|
||||
|
||||
|
||||
const ACQUIRED_CARD_IDS = new Set([
|
||||
'bearing', 'legalWork', 'crowdedFarming', 'hatchingFeed', 'safetyCover',
|
||||
'preventiveMaintenance', 'dudFilter', 'durabilityCoating', 'dudRefund',
|
||||
'freeReroll', 'composter', 'scannerQueueSpacing', 'recyclingSubsidy',
|
||||
'chemicalWeaponSubsidy', 'laborExploitation', 'usedMachine'
|
||||
]);
|
||||
|
||||
function acquiredCardCount(card) {
|
||||
const effects = game.cardEffects || {};
|
||||
if (!ACQUIRED_CARD_IDS.has(card.id)) return 0;
|
||||
if (card.id === 'usedMachine') return effects.usedMachineActive ? 1 : 0;
|
||||
if (card.id === 'laborExploitation') return effects.laborExploitation ? 1 : 0;
|
||||
return Math.max(0, Number(effects[card.id]) || 0);
|
||||
}
|
||||
|
||||
function acquiredCardEntries() {
|
||||
return CARD_DEFS
|
||||
.filter(card => ACQUIRED_CARD_IDS.has(card.id))
|
||||
.map(card => ({ card, count: acquiredCardCount(card) }))
|
||||
.filter(item => item.count > 0);
|
||||
}
|
||||
|
||||
function updateAcquiredCardsPanel() {
|
||||
const panel = ui.cardInventory;
|
||||
if (!panel) return;
|
||||
const entries = acquiredCardEntries();
|
||||
const total = entries.reduce((sum, item) => sum + item.count, 0);
|
||||
const summary = panel.querySelector('summary');
|
||||
const list = panel.querySelector('.acquired-cards-list');
|
||||
if (summary) summary.innerHTML = `CARDS <strong>${total}</strong><span>${entries.length ? 'passive' : 'none'}</span>`;
|
||||
if (!list) return;
|
||||
if (!entries.length) {
|
||||
list.innerHTML = '<p>No passive acquired cards yet. Consumed cards and equipment improvements are hidden.</p>';
|
||||
panel.classList.add('empty');
|
||||
return;
|
||||
}
|
||||
panel.classList.remove('empty');
|
||||
list.innerHTML = entries.map(({ card, count }) => {
|
||||
const tags = (card.tags || []).filter(tag => !['ACTIVE', 'ONE-SHOT'].includes(tag)).slice(0, 2);
|
||||
return `<div class="acquired-card-chip ${escapeHtml(card.rarity || 'common')}"><b>${escapeHtml(card.title)}</b><strong>x${count}</strong>${tags.length ? `<small>${tags.map(escapeHtml).join(' / ')}</small>` : ''}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function updateHistoryButtons() {
|
||||
ui.buttons.undo.disabled = game.phase !== 'build' || game.undoStack.length === 0;
|
||||
ui.buttons.redo.disabled = game.phase !== 'build' || game.redoStack.length === 0;
|
||||
|
|
@ -95,6 +131,8 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
const tribute = fairiesTributeInfo(game, tributeDay);
|
||||
const lastTribute = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.fairiesTribute || 0 : null;
|
||||
const lastTributeReduction = game.lastBuildFees?.turn === tributeDay ? game.lastBuildFees.reduction || 0 : 0;
|
||||
const maint = maintenanceSummary(game);
|
||||
const activeWorkers = Array.isArray(game.repairmen) ? game.repairmen.filter(r => r.active).length : (game.repairman?.active ? 1 : 0);
|
||||
ui.turnSummary.innerHTML = [
|
||||
`Seed: <b>${escapeHtml(game.runSeed || 'none')}</b> | Grid box: <b>${game.gridRows || 10}x${game.gridCols || 20}</b> | Owned: <b>${game.gridOwnedCells || game.ownedCells?.size || ((game.gridRows || 10) * (game.gridCols || 20))}</b> cells | Blocked: <b>${game.blockedCells?.size || 0}</b> cells`,
|
||||
`Truck target: <b>${truckTarget(game).toUpperCase()}</b> | Male fine: <b>-${yen(maleTruckPenalty(game))}</b>`,
|
||||
|
|
@ -103,7 +141,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
lastTribute != null
|
||||
? `Fairies tribute paid before Build: <b>-${yen(lastTribute)}</b>${lastTributeReduction ? ` | Flattery -${yen(lastTributeReduction)}` : ''}`
|
||||
: `Fairies tribute before Build: <b>-${yen(tribute.amount)}</b>${tribute.reduction ? ` | Flattery -${yen(tribute.reduction)}` : ''}`,
|
||||
`Maintenance: <b>worst ${maintenanceSummary(game).worst.percent}% dirty (${maintenanceSummary(game).worst.label})</b> | Repairman: <b>${game.repairman?.hiredForNextDay ? 'hired next day' : game.repairman?.active ? 'working' : 'none'}</b>`,
|
||||
`Maintenance: <b>worst ${maint.worst.percent}% dirty (${maint.worst.label})</b> | Repair workers: <b>${activeWorkers || maint.workers}</b> | Daily wages: <b>-${yen(maint.dailyWage || 0)}</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>`
|
||||
|
|
@ -155,6 +193,7 @@ export function createUISystem({ game, ui, build, startGame, beginCardDraft, act
|
|||
ui.phase.textContent = phaseLabel();
|
||||
if (ui.comboCount) ui.comboCount.textContent = game.manualCombo?.count || 0;
|
||||
updatePriorityStrip();
|
||||
updateAcquiredCardsPanel();
|
||||
ui.turnProfit.textContent = yen(game.stats.profit);
|
||||
ui.turnProfit.classList.toggle('cash-negative', game.stats.profit < 0);
|
||||
ui.turnProfit.classList.toggle('cash-positive', game.stats.profit > 0);
|
||||
|
|
|
|||
108
styles.css
108
styles.css
|
|
@ -748,3 +748,111 @@ body { font-size: 20px; }
|
|||
@media (max-width: 1180px) {
|
||||
.scanner-monitor { bottom: 132px; width: min(520px, calc(100% - 16px)); left: 8px; }
|
||||
}
|
||||
|
||||
/* v27.8 acquired passive-card panel */
|
||||
.acquired-cards-panel {
|
||||
position: absolute;
|
||||
z-index: 7;
|
||||
left: 12px;
|
||||
top: 124px;
|
||||
width: min(360px, calc(100% - 450px));
|
||||
max-height: 42px;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--line);
|
||||
background: rgba(255,255,255,.94);
|
||||
box-shadow: 5px 5px 0 rgba(16,32,21,.16);
|
||||
font-weight: 900;
|
||||
pointer-events: auto;
|
||||
transition: max-height .14s ease, background .14s ease;
|
||||
}
|
||||
.acquired-cards-panel[open],
|
||||
.acquired-cards-panel:hover,
|
||||
.acquired-cards-panel:focus-within {
|
||||
max-height: 360px;
|
||||
background: rgba(255,255,255,.98);
|
||||
}
|
||||
.acquired-cards-panel summary {
|
||||
min-height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
font-size: 15px;
|
||||
}
|
||||
.acquired-cards-panel summary::-webkit-details-marker { display: none; }
|
||||
.acquired-cards-panel summary::after {
|
||||
content: 'show';
|
||||
margin-left: auto;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.acquired-cards-panel[open] summary::after,
|
||||
.acquired-cards-panel:hover summary::after { content: 'hide'; }
|
||||
.acquired-cards-panel summary strong {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
min-width: 28px;
|
||||
height: 24px;
|
||||
padding: 0 6px;
|
||||
border: 2px solid var(--line);
|
||||
background: var(--green-soft);
|
||||
color: var(--green-dark);
|
||||
}
|
||||
.acquired-cards-panel summary span {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.acquired-cards-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
max-height: 306px;
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
border-top: 3px solid var(--line);
|
||||
}
|
||||
.acquired-cards-list p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.acquired-card-chip {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 2px 8px;
|
||||
align-items: center;
|
||||
border: 2px solid var(--line);
|
||||
background: #f7fff5;
|
||||
padding: 7px 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.acquired-card-chip.rare { background: #fff0d1; }
|
||||
.acquired-card-chip.ultraRare { background: #ffe6f0; }
|
||||
.acquired-card-chip b {
|
||||
text-transform: uppercase;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.acquired-card-chip strong {
|
||||
color: var(--green);
|
||||
font-size: 13px;
|
||||
}
|
||||
.acquired-card-chip small {
|
||||
grid-column: 1 / -1;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.acquired-cards-panel.empty { opacity: .78; }
|
||||
.game-shell.phase-title .acquired-cards-panel { display: none; }
|
||||
@media (max-width: 1180px) {
|
||||
.acquired-cards-panel { top: 252px; left: 8px; width: min(420px, calc(100% - 16px)); }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue