wip first

This commit is contained in:
2026-04-08 16:29:50 -04:00
commit 2ce04c7bbb
7 changed files with 3711 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
dist

12
index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Canopy</title>
<script type="module" src="/src/main.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

987
package-lock.json generated Normal file
View File

@@ -0,0 +1,987 @@
{
"name": "canopy-game",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "canopy-game",
"version": "0.0.1",
"devDependencies": {
"vite": "^5.4.19"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.9",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.60.1",
"@rollup/rollup-android-arm64": "4.60.1",
"@rollup/rollup-darwin-arm64": "4.60.1",
"@rollup/rollup-darwin-x64": "4.60.1",
"@rollup/rollup-freebsd-arm64": "4.60.1",
"@rollup/rollup-freebsd-x64": "4.60.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
"@rollup/rollup-linux-arm-musleabihf": "4.60.1",
"@rollup/rollup-linux-arm64-gnu": "4.60.1",
"@rollup/rollup-linux-arm64-musl": "4.60.1",
"@rollup/rollup-linux-loong64-gnu": "4.60.1",
"@rollup/rollup-linux-loong64-musl": "4.60.1",
"@rollup/rollup-linux-ppc64-gnu": "4.60.1",
"@rollup/rollup-linux-ppc64-musl": "4.60.1",
"@rollup/rollup-linux-riscv64-gnu": "4.60.1",
"@rollup/rollup-linux-riscv64-musl": "4.60.1",
"@rollup/rollup-linux-s390x-gnu": "4.60.1",
"@rollup/rollup-linux-x64-gnu": "4.60.1",
"@rollup/rollup-linux-x64-musl": "4.60.1",
"@rollup/rollup-openbsd-x64": "4.60.1",
"@rollup/rollup-openharmony-arm64": "4.60.1",
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
"@rollup/rollup-win32-x64-gnu": "4.60.1",
"@rollup/rollup-win32-x64-msvc": "4.60.1",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}

14
package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "canopy-game",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^5.4.19"
}
}

504
plan.md Normal file
View File

@@ -0,0 +1,504 @@
# Canopy Refactor And Rules Expansion Plan
## Goals
- Convert the game from JavaScript to TypeScript.
- Refactor the single-file implementation into small rules and UI modules.
- Fix the turn-order leverage problem with a public initiative seat draft.
- Add a public weather/effect card draft-ban phase to break symmetry.
- Add an alternate per-turn income mode with extra positional costs for balance.
## Recommended Foundation
- Use plain TypeScript with typed domain models and pure rules modules.
- Do not migrate to a board-game framework during this feature pass.
- Keep rendering simple and browser-native for now.
- Treat the game as a deterministic rules engine with DOM rendering layered on top.
Why this approach:
- The game is turn-based and rules-heavy, not entity-heavy.
- ECS-style libraries are a poor fit for this problem.
- `boardgame.io` is the main browser board-game framework, but it would expand scope too much for this refactor.
- If formal phase handling becomes painful later, `XState` is the best follow-up addition.
## Card System Goals
The weather/effect cards should:
- be readable from a TV screen
- be resolved with perfect information
- create asymmetric incentives each round
- be strong enough to disrupt mirror play
- avoid too much arithmetic or long text
- mostly modify incentives, not replace the core game
## Card Market Structure
Recommended round flow:
1. Determine round initiative order.
2. Reveal `playerCount + 2` public cards.
3. In initiative order, each player takes exactly one action:
- draft one card for the round, or
- ban one card from the row
4. Drafted cards become active global rules for the round.
5. Banned cards are removed.
6. Undrafted cards are discarded.
Recommended limits:
- Start with 8 to 14 total cards.
- Keep active cards per round low, ideally 1 to 3.
- Prefer cards with one sentence and one number.
## Candidate Weather / Effect Cards
These are written to fit a large shared screen. The title should be the main thing players read; the rule text should stay short.
### Economy / Position Cards
- **Leaf Surge**: Each leaf gains `+1` energy.
- **Branching Season**: Each branch beyond your first gives `+1` energy.
- **West Light**: Left third columns give `+1` energy.
- **East Light**: Right third columns give `+1` energy.
- **High Noon**: Center third columns give `+1` energy.
- **Sun Ladder**: Straight-up growth costs `-1` once per turn.
- **Storehouse**: Banking is enabled this round.
- **Shade Cloth**: Your covered nodes still count for `1` energy.
### Conflict / Interaction Cards
- **Sap Theft**: When your branch crosses an opponent branch, gain `+1` energy.
- **Hedge Trimmer**: Branches above the lowest player height are cut down before scoring.
- **Disease Sweep**: Before each turn, that player loses `1` energy.
- **Stalemate**: Contested columns give no energy.
- **Split Light**: Contested columns give half energy to each tied player.
- **Shared Light**: Contested columns give full energy to every tied player.
### Additional Cards Worth Considering
- **Root Pulse**: Root shifts cost `0` this round.
- **Still Soil**: Root shifts are disabled this round.
- **Thin Air**: Diagonal growth costs `+1`.
- **Tailwind**: Diagonal growth costs `-1` once per turn.
- **Canopy Tax**: The tallest branch of each player costs `+1` to extend.
- **Undergrowth**: The first growth each turn costs `0`.
- **Dry Season**: Banking loses `1` energy when used.
- **Tall Reward**: Your tallest leaf gains `+2` energy.
- **Wide Reward**: If you control the most columns, gain `+2` energy.
- **Twig Bloom**: New leaves created this round gain `+1` energy when scored.
- **Heavy Shade**: Columns with 2 or more players present count as contested.
- **Sunbreak**: Empty columns next to your branch give `+1` energy.
## Best Initial Card Set
For the first implementation, avoid the cards that require the most new geometry or crossing logic. Start with cards that reuse existing board evaluation.
### Phase 1 card set
- **Leaf Surge**: Each leaf gains `+1` energy.
- **Branching Season**: Each branch beyond your first gives `+1` energy.
- **West Light**: Left third columns give `+1` energy.
- **East Light**: Right third columns give `+1` energy.
- **High Noon**: Center third columns give `+1` energy.
- **Sun Ladder**: Straight-up growth costs `-1` once per turn.
- **Storehouse**: Banking is enabled this round.
- **Stalemate**: Contested columns give no energy.
- **Split Light**: Contested columns give half energy to each tied player.
- **Shared Light**: Contested columns give full energy to every tied player.
### Phase 2 card set
Add later after the rules core is stable:
- **Shade Cloth**
- **Sap Theft**
- **Hedge Trimmer**
- **Disease Sweep**
- **Root Pulse**
- **Still Soil**
Why split it this way:
- Phase 1 mostly changes scoring and move costs.
- Phase 2 needs new concepts like coverage protection, branch crossing checks, pruning, or per-turn drain.
## Rules Clarifications To Lock Down
### Initiative seat draft
- Players use a pure seat-selection token, not growth bidding.
- Players choose specific seat numbers.
- Seats are drafted publicly in bidding order, so no seat conflict resolution is needed.
- Bidding order is determined by setup rule:
- rotating from a random starting anchor, or
- lowest lifetime growth income first
### Lifetime growth income
Track all growth ever gained.
This should include:
- starting growth at game start
- round-end sunlight income
- per-turn sunlight income
- seat bonus growth
- card-generated growth
- random event growth like sunbeam bonuses
- recovered or protected growth if the game records that as gain
This should not double-count:
- banked energy carried into a later round after it was already counted when first gained
Implementation note:
- Add a dedicated `lifetimeGrowthIncome` stat to each player.
- Update it only at the moment growth is awarded.
### Banking
Current open issue:
- Banking should likely become conditional on a round card rather than always-on.
Recommended rule:
- By default, banking is disabled.
- `Storehouse` enables banking for that round.
Why:
- This helps reduce the economy snowball.
- It gives the weather deck a meaningful strategic lever.
### Contested columns
The current game reads column ownership by the top intercepting node. Weather cards that talk about contested columns require a clear definition.
Recommended definition:
- A column is contested if 2 or more players have a node in that column and they are tied under the active contest rule.
Open design task:
- Decide whether contest is based on equal top row, any shared presence, or special card-defined presence.
Recommended default:
- Contest means two or more players share the best topmost position for that column.
This keeps the rule closest to current sunlight logic.
### Leaves and branches
These terms need firm technical definitions before implementation.
Recommended definitions:
- `leaf`: a node with no children
- `branch`: a root-to-leaf path, or more simply for scoring, each leaf represents one branch
Recommended implementation simplification:
- Use `leaf count` instead of true branch count in the first pass.
- Rename card text if needed to match the actual rule.
Example:
- Instead of `Branching Season: Each branch beyond your first gives +1 energy`, consider `Leaf Burst: Each leaf after your first gives +1 energy`.
This is much easier to explain and compute.
### Branch crossing
`Sap Theft` needs a precise crossing rule.
Risk:
- The current data model stores branch segments, but crossing detection between diagonal edges is new logic.
Recommendation:
- Do not include crossing-based cards in phase 1.
- If added later, count only actual geometric edge intersections between different players' segments.
### Hedge Trimmer
This card is flavorful but potentially destructive and swingy.
Risks:
- Requires pruning nodes and edges from the board.
- Can invalidate pending move assumptions.
- Needs a clear “same level” definition.
Recommendation:
- Delay to phase 2.
- Define it as: `Before scoring, all nodes above the shortest tallest-player height are removed.`
- Only apply between turns or at round end, never mid-turn.
### Disease Sweep
This can be implemented in more than one way.
Choices:
- lose growth before each turn
- lose sunlight before end-of-round tally
- kill twigs as the current disease event does
Recommendation:
- For a card, use the simple economy version: `Before each turn, that player loses 1 energy.`
- Keep the existing random disease event separate.
## Implementation Risks
### Player identity and turn order are currently coupled
Current risk:
- `player.id` is treated as both a stable identity and an array index.
Plan:
- Keep `players` in stable id order.
- Introduce `turnOrder: PlayerId[]` for seats and active order.
- Update turn flow and render helpers to use `turnOrder` where needed.
### The game needs explicit phases
Current risk:
- The code uses ad hoc flow control plus animation locks.
Plan:
- Add a typed phase model:
- `initiative_bid`
- `weather_draft`
- `turn`
- `round_end`
- `game_over`
### The code is currently one large file
Current risk:
- Rules, state, rendering, and DOM events are tightly mixed.
Plan:
- Convert to TypeScript first.
- Extract pure rules before feature work.
### Per-turn income changes the economy deeply
Current risk:
- Existing logic assumes income is only awarded between rounds.
Plan:
- Keep `round_end` income as the stable default.
- Add `per_turn` as an alternate rules path after the initiative and weather phases are working.
- Add positional surcharges to slow runaway chaining.
### TV readability
Current risk:
- Too many active modifiers will be hard to track.
Plan:
- Cap active cards per round.
- Keep card titles short.
- Show active effects in one compact panel with icons or one-line summaries.
### Banking may conflict with weather cards
Current risk:
- If banking is always available and also appears on a card, the card has no value.
Plan:
- Make banking conditional on round effects, or convert the card into a banking bonus instead of availability.
### Randomness vs strategy
Current risk:
- Too much randomness in the weather row can obscure good play.
Plan:
- Start with a small curated deck.
- Reveal a modest row each round.
- Consider a rotating deck instead of full shuffle if needed after testing.
## Proposed TypeScript Module Structure
- `src/main.ts`
- `src/constants.ts`
- `src/state/types.ts`
- `src/state/createGame.ts`
- `src/state/reducer.ts`
- `src/rules/board.ts`
- `src/rules/moves.ts`
- `src/rules/scoring.ts`
- `src/rules/initiative.ts`
- `src/rules/weather.ts`
- `src/rules/turnFlow.ts`
- `src/ui/renderBoard.ts`
- `src/ui/renderSidebar.ts`
- `src/ui/renderScoreboard.ts`
- `src/ui/renderModals.ts`
- `src/app/events.ts`
## Proposed Type Model
Core types to introduce early:
- `PlayerId`
- `NodeKey`
- `Setup`
- `GameState`
- `Player`
- `Phase`
- `TurnMove`
- `WeatherCardId`
- `WeatherCardDefinition`
- `ActiveRoundEffect`
New setup fields:
- `initiativeMode: "fixed" | "bid"`
- `biddingOrderRule: "rotating" | "lowest_growth_income"`
- `incomeTiming: "round_end" | "per_turn"`
- `weatherDraftEnabled: boolean`
New player fields:
- `lifetimeGrowthIncome: number`
New game state fields:
- `phase`
- `turnOrder`
- `seatChoices`
- `initiativeAnchorPlayerId`
- `weatherRow`
- `draftedWeather`
- `bannedWeather`
- `activeRoundEffects`
## Execution Order
### Milestone 1: TypeScript conversion and safe refactor
1. Add TypeScript and config.
2. Convert `src/main.js` to `src/main.ts` without changing behavior.
3. Extract constants and types.
4. Extract pure board, move, and scoring helpers.
5. Keep current gameplay working exactly as-is.
### Milestone 2: Turn-order refactor
1. Separate stable player identity from seat order.
2. Introduce `turnOrder`.
3. Update active-player logic to walk `turnOrder`.
4. Add explicit `phase` handling.
### Milestone 3: Initiative draft phase
1. Add setup controls for initiative mode and bidding-order rule.
2. Add lifetime growth income tracking.
3. Implement rotating bid order.
4. Implement lowest-growth-income bid order.
5. Add public seat selection modal.
6. Apply seat growth bonuses.
### Milestone 4: Weather draft-ban phase
1. Define weather card data in `rules/weather.ts`.
2. Add setup toggle for weather phase.
3. Implement weather row generation.
4. Implement draft-or-ban action in seat order.
5. Render active effects clearly.
6. Apply phase 1 weather cards in scoring and move cost logic.
### Milestone 5: Per-turn income mode
1. Add setup control for income timing.
2. Implement payout to the active player immediately after their turn.
3. Keep round-end mode intact.
4. Verify banking and pass logic still make sense.
### Milestone 6: Distance-based balancing
1. Add root-distance surcharge support.
2. Start with a simple formula such as:
- `extraCost = floor((abs(dx) + abs(dy)) / 4)`
3. Apply only in `per_turn` mode at first.
4. Tune after playtesting.
### Milestone 7: Phase 2 weather cards
1. Add crossing-based cards if still desired.
2. Add hedge-trimming/pruning cards.
3. Add disease drain cards.
4. Rebalance text and values based on playtests.
## Testing Plan
Add rule-level tests as soon as the core logic is modularized.
Priority coverage:
- legal growth moves
- root shift legality
- scoring by column
- contested column resolution modes
- seat draft ordering
- lifetime growth income tracking
- weather draft resolution
- weather effect application
- per-turn payout behavior
- distance surcharge cost calculation
## Recommended First Shipping Configuration
- initiative mode: `bid`
- bidding order rule: `rotating`
- income timing: `round_end`
- weather draft: `enabled`
- banking: enabled only through `Storehouse`
- weather deck: only phase 1 cards
Why this first:
- It directly attacks the symmetry problem.
- It fixes turn-order leverage without also changing all economy timing at once.
- It keeps the new concepts readable for group play.
## Summary
The most important design shift is this:
- turn order becomes a public seat draft
- each round gains a public asymmetric card market
- mirror play is no longer automatically safe because players can deny and reshape incentives before growth begins
The best implementation strategy is:
- TypeScript first
- modular rules second
- initiative phase third
- weather card system fourth
- per-turn economy mode after the game flow is stable

1508
src/main.js Normal file

File diff suppressed because it is too large Load Diff

684
src/styles.css Normal file
View File

@@ -0,0 +1,684 @@
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background:
radial-gradient(circle at top, rgba(48, 72, 104, 0.35), transparent 42%),
linear-gradient(180deg, #0b1220 0%, #070b13 100%);
color: #f4f7fb;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: transparent;
}
button,
input {
font: inherit;
}
input[type="number"],
input[type="text"] {
width: 100%;
min-height: 2.8rem;
padding: 0.7rem 0.85rem;
border-radius: 0.9rem;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.05);
color: #f4f7fb;
}
button {
border: 0;
cursor: pointer;
}
#app {
min-height: 100vh;
}
.layout {
min-height: 100vh;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.85rem;
}
.scoreboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.75rem;
}
.scoreboard--bottom {
align-items: end;
}
.score-card {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1.25rem;
padding: 0.8rem 1rem;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.score-card.active {
border-color: color-mix(in srgb, var(--player-color) 55%, white);
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08), 0 0 24px var(--player-glow);
}
.score-card__head,
.score-card__numbers,
.panel__title-row,
.button-row,
.setup-grid,
.toggle-row,
.active-turn {
display: flex;
align-items: center;
}
.score-card__head,
.panel__title-row,
.button-row,
.toggle-row {
justify-content: space-between;
}
.score-card__head h2,
.panel h1,
.panel h2,
.active-turn h2 {
margin: 0;
}
.score-card__numbers {
margin-top: 0.65rem;
gap: 1rem;
}
.score-card__numbers div {
display: grid;
gap: 0.15rem;
}
.score-card__numbers span,
.eyebrow,
label span,
.log-list p,
.status-panel p,
.active-turn p {
color: rgba(231, 238, 247, 0.72);
}
.score-card__numbers strong {
font-size: 1.35rem;
}
.score-value {
display: inline-block;
}
.score-value.changed {
animation: score-pop 0.7s ease;
}
.player-dot {
width: 0.95rem;
height: 0.95rem;
border-radius: 999px;
background: var(--player-color);
box-shadow: 0 0 18px var(--player-glow);
}
.game-area {
flex: 1;
display: grid;
grid-template-columns: minmax(0, 1.9fr) minmax(320px, 0.85fr);
gap: 0.85rem;
min-height: 0;
align-items: start;
}
.board-shell,
.panel {
border-radius: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(9, 16, 29, 0.72);
backdrop-filter: blur(20px);
}
.board-shell {
min-height: 0;
padding: 0.8rem;
}
.board {
position: relative;
width: 100%;
height: auto;
max-height: calc(100vh - 10.5rem);
aspect-ratio: var(--board-columns) / var(--board-rows);
display: grid;
gap: 0.32rem;
margin: 0 auto;
}
.board__lines {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.18));
z-index: 1;
}
.board__fx {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 4;
}
.cell {
position: relative;
background: rgba(255, 255, 255, 0.03);
border-radius: 0.8rem;
border: 1px solid rgba(255, 255, 255, 0.05);
overflow: hidden;
z-index: 2;
}
.cell__shade {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), var(--column-tint));
}
.cell__root-ring {
position: absolute;
inset: 18% 18%;
border: 1px dashed rgba(255, 255, 255, 0.28);
border-radius: 999px;
}
.cell__node,
.cell__target-label {
position: absolute;
inset: 50% auto auto 50%;
transform: translate(-50%, -50%);
}
.cell__node {
width: min(2.5vw, 1.6rem);
height: min(2.5vw, 1.6rem);
min-width: 1rem;
min-height: 1rem;
border-radius: 50%;
background: var(--node-color);
box-shadow: 0 0 0 0.2rem rgba(255, 255, 255, 0.06), 0 0 1.2rem var(--node-glow);
}
.cell.selected {
border-color: rgba(255, 255, 255, 0.55);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22);
}
.cell.pending {
border-color: rgba(255, 255, 255, 0.28);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12), 0 0 18px rgba(255, 255, 255, 0.08);
}
.cell.pending .cell__node {
box-shadow: 0 0 0 0.22rem rgba(255, 255, 255, 0.08), 0 0 1.4rem var(--node-glow), 0 0 2rem rgba(255, 255, 255, 0.08);
}
.board__drop-layer {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
z-index: 5;
}
.board__energy-layer {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 4;
}
.board__energy-cell {
position: absolute;
border-radius: 0.8rem;
background:
radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.95), color-mix(in srgb, var(--flash-color) 70%, #ffe08a) 34%, rgba(255, 224, 138, 0.18) 72%, transparent 100%),
linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12), 0 0 18px color-mix(in srgb, var(--flash-color) 55%, #ffe08a);
opacity: 0;
}
.board__energy-cell--sunlight {
inset: 12%;
background:
linear-gradient(180deg, rgba(255, 244, 214, 0.05), rgba(255, 244, 214, 0.008)),
radial-gradient(circle at 50% 50%, rgba(255, 242, 196, 0.11), color-mix(in srgb, var(--flash-color) 10%, #ffe3a3) 42%, rgba(255, 221, 128, 0.03) 72%, transparent 100%);
box-shadow: inset 0 0 0 1px rgba(255, 245, 224, 0.02), 0 0 6px color-mix(in srgb, var(--flash-color) 8%, #ffe3a3);
}
.board--sunlight .board__energy-cell--sunlight,
.board--branches .board__energy-cell,
.board--bonus .board__energy-cell--bonus {
animation: energy-cell-flash 0.48s ease forwards;
animation-delay: var(--flash-delay, 0ms);
}
.board__energy-cell--bonus {
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18), 0 0 22px rgba(255, 216, 94, 0.9);
}
.board--bonus .board__drop--bonus {
animation: sunlight-drop 0.85s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
}
.board__drop-core,
.board__drop-spark {
position: absolute;
inset: 0;
border-radius: 999px;
}
.board__drop-core {
background: radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.98), color-mix(in srgb, var(--drop-color) 72%, #ffe38a) 45%, rgba(255, 227, 138, 0.18) 100%);
box-shadow: 0 0 18px color-mix(in srgb, var(--drop-color) 40%, #ffe38a), 0 0 32px rgba(255, 236, 174, 0.65);
}
.board__drop-spark {
inset: 35%;
border: 1px solid rgba(255, 248, 220, 0.95);
opacity: 0.9;
}
.board__drop-spark--a { transform: translate(-0.8rem, -0.15rem) scale(0.55); }
.board__drop-spark--b { transform: translate(0.75rem, -0.1rem) scale(0.45); }
.board__drop-spark--c { transform: translate(0.1rem, -0.75rem) scale(0.35); }
.board__root-burst,
.board__disease-mark,
.board__sunbeam-burst {
opacity: 0;
}
.board__root-burst circle,
.board__sunbeam-burst {
fill: color-mix(in srgb, var(--burst-color, #ffd85e) 70%, white);
stroke: rgba(255, 255, 255, 0.65);
stroke-width: 0.35;
}
.board__root-burst text {
fill: #08111c;
font-size: 2.1px;
font-weight: 800;
}
.board__disease-mark circle {
fill: rgba(162, 255, 142, 0.2);
stroke: rgba(162, 255, 142, 0.9);
stroke-width: 0.35;
}
.board__disease-mark path {
stroke: rgba(162, 255, 142, 1);
stroke-width: 0.5;
stroke-linecap: round;
}
.board--branches .board__root-burst,
.board--events .board__root-burst,
.board--events .board__disease-mark,
.board--bonus .board__sunbeam-burst,
.board--events .board__sunbeam-burst {
animation: pop-fade 0.8s ease forwards;
animation-delay: var(--trace-delay, 0ms);
}
.board__sunbeam-burst text {
fill: #08111c;
font-size: 2.1px;
font-weight: 800;
}
.cell.target {
border-color: rgba(255, 255, 255, 0.22);
background: rgba(255, 255, 255, 0.05);
}
.cell.target:hover {
transform: translateY(-1px);
}
.cell__target-label {
width: 2rem;
height: 2rem;
display: grid;
place-items: center;
border-radius: 999px;
background: rgba(255, 255, 255, 0.09);
border: 1px solid rgba(255, 255, 255, 0.18);
font-weight: 700;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 1rem;
}
.panel {
padding: 1rem;
}
.controls-panel {
display: grid;
gap: 0.8rem;
}
.panel__actions {
display: flex;
gap: 0.55rem;
}
.modal-backdrop {
position: fixed;
inset: 0;
display: grid;
place-items: center;
padding: 1.25rem;
background: rgba(3, 8, 16, 0.72);
backdrop-filter: blur(14px);
z-index: 20;
}
.modal {
width: min(1180px, 100%);
max-height: min(92vh, 980px);
overflow: auto;
}
.modal-setup-grid,
.modal-grid {
display: grid;
gap: 0.9rem;
}
.modal-setup-grid {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.modal-grid {
margin-top: 0.9rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.modal-actions {
margin-top: 0.9rem;
justify-content: flex-end;
}
.eyebrow {
margin: 0 0 0.3rem;
font-size: 0.82rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.setup-grid {
gap: 1rem;
align-items: end;
}
.setup-grid label {
flex: 1;
display: grid;
gap: 0.35rem;
}
.seed-editor {
display: grid;
gap: 0.65rem;
padding: 0.8rem;
border-radius: 1rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.seed-row {
display: grid;
gap: 0.35rem;
}
.order-row,
.order-row__label,
.order-row__actions {
display: flex;
align-items: center;
}
.order-row {
justify-content: space-between;
gap: 0.75rem;
}
.order-row__label {
gap: 0.6rem;
font-weight: 600;
}
.order-row__actions {
gap: 0.4rem;
}
.mini-button {
min-height: 2rem;
padding: 0.35rem 0.65rem;
border-radius: 0.7rem;
background: rgba(255, 255, 255, 0.08);
color: #f4f7fb;
border: 1px solid rgba(255, 255, 255, 0.1);
font-weight: 600;
}
.seed-help {
margin: 0;
color: rgba(231, 238, 247, 0.72);
}
.randomize-button {
width: 100%;
}
input[type="range"] {
width: 100%;
}
.active-turn {
flex-direction: column;
align-items: flex-start;
gap: 0.35rem;
padding: 0.8rem;
border-radius: 1rem;
background: linear-gradient(135deg, color-mix(in srgb, var(--player-color) 22%, rgba(255, 255, 255, 0.04)), rgba(255, 255, 255, 0.04));
border: 1px solid color-mix(in srgb, var(--player-color) 35%, rgba(255, 255, 255, 0.08));
}
.root-shift-row {
display: flex;
gap: 0.55rem;
flex-wrap: wrap;
}
.root-shift-button {
min-height: 2.2rem;
}
.button-row {
gap: 0.75rem;
}
.button-row button,
.ghost-button {
min-height: 2.7rem;
padding: 0.65rem 0.9rem;
border-radius: 0.95rem;
background: #f4f7fb;
color: #0a1020;
font-weight: 700;
}
.ghost-button,
#finish-game {
background: rgba(255, 255, 255, 0.08);
color: #f4f7fb;
border: 1px solid rgba(255, 255, 255, 0.1);
}
button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.log-list {
display: grid;
gap: 0.55rem;
max-height: 18vh;
overflow: auto;
}
.log-list p,
.status-panel p,
.active-turn p {
margin: 0;
}
.event-note {
color: #ffd577;
}
@keyframes sunlight-drop {
0% {
opacity: 0;
top: -0.9rem;
transform: scale(0.65);
}
12% {
opacity: 1;
}
85% {
opacity: 1;
top: calc(var(--drop-end) - 0.55rem);
transform: scale(1);
}
100% {
opacity: 0;
top: calc(var(--drop-end) - 0.55rem);
transform: scale(1.25);
}
}
@keyframes pop-fade {
0% {
opacity: 0;
transform: scale(0.65);
}
25% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes energy-cell-flash {
0% {
opacity: 0;
}
20% {
opacity: 0.98;
}
100% {
opacity: 0;
}
}
@keyframes score-pop {
0% {
transform: scale(0.88);
color: #fff7d6;
text-shadow: 0 0 0 rgba(255, 235, 153, 0);
}
35% {
transform: scale(1.18);
color: #ffe480;
text-shadow: 0 0 16px rgba(255, 228, 128, 0.9);
}
100% {
transform: scale(1);
color: inherit;
text-shadow: 0 0 0 rgba(255, 235, 153, 0);
}
}
@media (max-width: 1100px) {
.game-area {
grid-template-columns: 1fr;
}
.modal-grid {
grid-template-columns: 1fr;
}
.board {
max-height: none;
width: 100%;
}
}
@media (max-width: 720px) {
.layout {
padding: 1rem;
}
.scoreboard {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.score-card__numbers {
gap: 1rem;
}
.cell__node {
width: 1rem;
height: 1rem;
}
}