diff --git a/.github/workflows/deploy-game.yml b/.github/workflows/deploy-game.yml
new file mode 100644
index 0000000..691e507
--- /dev/null
+++ b/.github/workflows/deploy-game.yml
@@ -0,0 +1,142 @@
+name: Deploy game
+
+# The game skin, deployed on its own Worker.
+#
+# It exists so that the game and the console do not share a blast radius.
+# `shell-online-app` serves the session list, the terminal and the vault; while
+# the game shipped inside it, a bad game build was a bad console build. This
+# builds and deploys only the game, only when the game changed.
+#
+# There is no database step and no migration step, because there is nothing to
+# migrate: the Worker this deploys serves static files and holds no bindings,
+# no secrets and no cron. The game's tables belong to the console's Worker,
+# which is what answers /api/*, and they are migrated by deploy-app.yml.
+#
+# Read docs/deploy-game.md before switching the route on. Until a route exists
+# this deploys something reachable by nothing, on purpose.
+
+on:
+ push:
+ branches: [main]
+ # Only what this Worker is built from. A change to the console, the CLI or
+ # the relay must not redeploy the game, which is the entire point.
+ paths:
+ - "app/src/game/**"
+ - "app/src/styles/game.css"
+ - "app/public/game/**"
+ - "app/game.html"
+ - "app/vite.game.config.ts"
+ - "app/wrangler.game.jsonc"
+ - ".github/workflows/deploy-game.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+# One at a time. Two overlapping deploys of the same Worker is a race over
+# which build is left serving.
+concurrency:
+ group: deploy-game
+ cancel-in-progress: false
+
+env:
+ # Pinned, like the console's: a deploy should not change because a new
+ # wrangler was published this morning.
+ WRANGLER_VERSION: "4.131.0"
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ environment: production
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ # A dispatch runs against whatever ref was chosen. Production is built
+ # from main, so anything else is a mistake worth catching before it ships.
+ - name: Refuse to deploy anything but main
+ env:
+ REF: ${{ github.ref }}
+ run: |
+ set -eu
+ if [ "$REF" != "refs/heads/main" ]; then
+ echo "this workflow deploys main; asked for $REF" >&2
+ exit 1
+ fi
+
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: 22
+ cache: npm
+ cache-dependency-path: app/package-lock.json
+
+ - run: npm ci
+ working-directory: app
+
+ # Fails here, naming what is missing, rather than further in where the
+ # symptom no longer resembles the cause. Values arrive through the
+ # environment rather than ${{ }} inside the script, so nothing configured
+ # elsewhere can be read as shell.
+ - name: Check the deploy has what it needs
+ env:
+ VITE_OIDC_ISSUER: ${{ vars.VITE_OIDC_ISSUER }}
+ VITE_OIDC_CLIENT_ID: ${{ vars.VITE_OIDC_CLIENT_ID }}
+ VITE_FIREBASE_API_KEY: ${{ vars.VITE_FIREBASE_API_KEY }}
+ VITE_FIREBASE_AUTH_DOMAIN: ${{ vars.VITE_FIREBASE_AUTH_DOMAIN }}
+ VITE_FIREBASE_PROJECT_ID: ${{ vars.VITE_FIREBASE_PROJECT_ID }}
+ VITE_FIREBASE_APP_ID: ${{ vars.VITE_FIREBASE_APP_ID }}
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ run: |
+ set -eu
+ missing=
+ for name in CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID; do
+ eval "value=\${$name-}"
+ if [ -z "$value" ]; then missing="$missing $name"; fi
+ done
+ # The client is compiled against one sign-in configuration or the
+ # other, exactly as the console is. Neither is a build that cannot
+ # sign anybody in.
+ oidc=0; firebase=0
+ if [ -n "${VITE_OIDC_ISSUER-}" ] && [ -n "${VITE_OIDC_CLIENT_ID-}" ]; then oidc=1; fi
+ if [ -n "${VITE_FIREBASE_API_KEY-}" ] && [ -n "${VITE_FIREBASE_PROJECT_ID-}" ]; then firebase=1; fi
+ if [ "$oidc" = 0 ] && [ "$firebase" = 0 ]; then
+ missing="$missing SIGN_IN_CONFIGURATION"
+ fi
+ if [ -n "$missing" ]; then
+ echo "the game deploy is missing:$missing" >&2
+ exit 1
+ fi
+
+ # The same tests the console runs, narrowed to what this Worker ships.
+ # A game that does not typecheck or breaks its own readability rules is
+ # not deployed, however green the console is.
+ - name: Check the game
+ working-directory: app
+ run: |
+ set -eu
+ npm run typecheck
+ npx vitest run src/game
+ node scripts/check-game-ui.mjs
+
+ - name: Build the game
+ working-directory: app
+ env:
+ VITE_OIDC_ISSUER: ${{ vars.VITE_OIDC_ISSUER }}
+ VITE_OIDC_CLIENT_ID: ${{ vars.VITE_OIDC_CLIENT_ID }}
+ VITE_FIREBASE_API_KEY: ${{ vars.VITE_FIREBASE_API_KEY }}
+ VITE_FIREBASE_AUTH_DOMAIN: ${{ vars.VITE_FIREBASE_AUTH_DOMAIN }}
+ VITE_FIREBASE_PROJECT_ID: ${{ vars.VITE_FIREBASE_PROJECT_ID }}
+ VITE_FIREBASE_APP_ID: ${{ vars.VITE_FIREBASE_APP_ID }}
+ VITE_FIREBASE_MESSAGING_SENDER_ID: ${{ vars.VITE_FIREBASE_MESSAGING_SENDER_ID }}
+ VITE_RELAY_URL: ${{ vars.VITE_RELAY_URL }}
+ run: npm run build:game
+
+ - name: Deploy the game Worker
+ working-directory: app
+ env:
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ run: npx --yes wrangler@${{ env.WRANGLER_VERSION }} deploy --config wrangler.game.jsonc
diff --git a/.gitignore b/.gitignore
index 1965590..af36fa7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,5 @@ worker-configuration.d.ts
# Marketing renders are maintained outside the source tree.
/promo/
+
+app/dist-game/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ad69e4..423b994 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,39 @@ All notable user-visible changes are recorded here. Versions follow [Semantic Ve
## Unreleased
+### Added
+
+- A game skin over the web app, reached from a controller at the right of the
+ top bar and left again through a pause screen whose last item is "Quit to
+ boring UI". The Marches are nine holdings spread over open country, each one
+ a rename of a part of the product: the Forge is where features are built, the
+ Watch is where faults are met, the Chronicle is the audit log, the Vault
+ holds session passwords the service cannot open. Live sessions are wrights
+ standing in them, their class is the harness each one runs, and what they are
+ doing is read from what the session is called. Click one to see where it is
+ posted, which machine it came from and how long it has been out. Drag to
+ move, wheel to zoom, and a road book in the pause menu rides you to any
+ holding.
+
+ Levels come from work that has already happened, counted by the service from
+ your own sessions: how many ran and finished, on how many days, from how many
+ machines. Marks come from levelling, and the pedlar sells cloth and dye and
+ nothing that changes a number.
+
+ The elixir vial shows what statistics gathering has cost, itemised by run and
+ by machine. It is off until you turn it on, and the notice explaining what
+ your machine would read — and what it never reads — is one press from the
+ vial. The reading happens on your machine rather than on the service, because
+ sessions are encrypted end to end and the service holds no key; `shell stats`
+ prints exactly what would be sent, and sends nothing.
+
+ It costs the session list nothing: the whole game is one lazily imported
+ chunk, and the build fails if any of it reaches the bundle everybody else
+ downloads. It honours reduced motion — on the map as well as in the
+ interface — offers a safe-area inset for televisions, an interface-size
+ slider and colourblind palettes, and is navigable with a keyboard or a pad
+ throughout.
+
## [0.16.0] — 2026-09-15
### Changed
diff --git a/app/game-preview.html b/app/game-preview.html
new file mode 100644
index 0000000..d83c762
--- /dev/null
+++ b/app/game-preview.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Shell Keep — preview
+
+
+
+
+
+
+
diff --git a/app/game.html b/app/game.html
new file mode 100644
index 0000000..8b9a0ca
--- /dev/null
+++ b/app/game.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+ Shell Keep
+
+
+
+
+
+
diff --git a/app/package-lock.json b/app/package-lock.json
index b7c2b50..43f31aa 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -15,6 +15,8 @@
"jose": "^6.2.12",
"oidc-client-ts": "^3.5.0",
"pg": "^8.23.0",
+ "pixi-viewport": "^6.0.3",
+ "pixi.js": "^8.20.1",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router-dom": "^7.18.3"
@@ -601,7 +603,6 @@
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.16.2.tgz",
"integrity": "sha512-fyCLPahl3r0Zb7Wzm+JLWcOOH3SaDgAyJzJ8EWIDiv51aNiv+2Pjbpa6myEuP7iXxjmJqrcbVm0GUF7h229T1Q==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@firebase/component": "0.7.5",
"@firebase/logger": "0.5.2",
@@ -669,7 +670,6 @@
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.18.tgz",
"integrity": "sha512-77H57WuGEsgrv59HdtuhHG+eQdP8di6myz7O93yW6yTZlR/tLSmsCxCq5vHp6AWWDwSV0EVtKUvizXQiQZVBWA==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@firebase/app": "0.16.2",
"@firebase/component": "0.7.5",
@@ -686,7 +686,6 @@
"resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.6.tgz",
"integrity": "sha512-yPLahy7Esfu2w/yme3msVK4xTkDXQqq6szfQn8yVOQpCKiT5GVFjqNpLbuz6NkX0WuTwUidkmPewW3r4xkvpeg==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@firebase/logger": "0.5.2"
}
@@ -1161,7 +1160,6 @@
"integrity": "sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA==",
"hasInstallScript": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"tslib": "^2.1.0"
},
@@ -1580,6 +1578,12 @@
"react-dom": ">= 16.8"
}
},
+ "node_modules/@pixi/colord": {
+ "version": "2.9.6",
+ "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz",
+ "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==",
+ "license": "MIT"
+ },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -1917,6 +1921,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/earcut": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-3.0.0.tgz",
+ "integrity": "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==",
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -1951,7 +1961,6 @@
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -2002,7 +2011,6 @@
"integrity": "sha512-toMg6PZGCIa/lQNCDoASrfb1ly4hsUKXFtFYC9kD4t78o5Y6LyNJU7AENt8eHPr3quYdxaxK7hj2mnbFfUk9NA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/istanbul-lib-coverage": "^1.0.0",
@@ -2087,6 +2095,21 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@webgpu/types": {
+ "version": "0.1.72",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.72.tgz",
+ "integrity": "sha512-0cF7RFM2edNoiIS1ODJp0/Gzv4/xSXhwoR0YCza+OWpJWtn4wmo9DvK91aLlH9+uUnwIriP7ZiC3WitmyhuzBw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.15",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz",
+ "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/@xterm/addon-fit": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
@@ -2527,6 +2550,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/earcut": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz",
+ "integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==",
+ "license": "ISC"
+ },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -2779,6 +2808,12 @@
"@types/estree": "^1.0.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
"node_modules/expect-type": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
@@ -3006,6 +3041,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/gifuct-js": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz",
+ "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==",
+ "license": "MIT",
+ "dependencies": {
+ "js-binary-schema-parser": "^2.0.3"
+ }
+ },
"node_modules/globalthis": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
@@ -3576,6 +3620,12 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/ismobilejs": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz",
+ "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==",
+ "license": "MIT"
+ },
"node_modules/jose": {
"version": "6.2.12",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
@@ -3585,6 +3635,12 @@
"url": "https://github.com/sponsors/panva"
}
},
+ "node_modules/js-binary-schema-parser": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz",
+ "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==",
+ "license": "MIT"
+ },
"node_modules/js-tokens": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
@@ -4198,6 +4254,12 @@
"node": ">=4"
}
},
+ "node_modules/parse-svg-path": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.2.0.tgz",
+ "integrity": "sha512-Tf7FFIrguPKQwzD4pWnYkR2VOv3raoHeKED80Bm+BYHI3KxC8KsgsGC5+fSMzAGDA6UEk4bHvmi+RsjmL3khpg==",
+ "license": "MIT"
+ },
"node_modules/path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
@@ -4233,7 +4295,6 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz",
"integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"pg-connection-string": "^2.14.0",
"pg-pool": "^3.14.0",
@@ -4361,6 +4422,41 @@
"node": ">=4"
}
},
+ "node_modules/pixi-viewport": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/pixi-viewport/-/pixi-viewport-6.0.3.tgz",
+ "integrity": "sha512-2+qPJ0/n+8hQYhWvY+795+x9y3MiUrCOWacK0DY53whowWaGdx9iDocy7z1pBwjkZhC52YvrJQuZKK0sdVLtBw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pixi.js": ">=8"
+ }
+ },
+ "node_modules/pixi.js": {
+ "version": "8.20.1",
+ "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.20.1.tgz",
+ "integrity": "sha512-akLVBMLvQbaEViqsmVraK0Njer1q4Q4lm4iNKuzvBiFrV8q+6/+HluJN3sWIwO663IBeQtUUS/CaXFJZs6ffXQ==",
+ "license": "MIT",
+ "workspaces": [
+ "examples",
+ "playground"
+ ],
+ "dependencies": {
+ "@pixi/colord": "^2.9.6",
+ "@types/earcut": "^3.0.0",
+ "@webgpu/types": "^0.1.69",
+ "@xmldom/xmldom": "^0.8.15",
+ "earcut": "^3.0.2",
+ "eventemitter3": "^5.0.1",
+ "gifuct-js": "^2.1.2",
+ "ismobilejs": "^1.1.1",
+ "parse-svg-path": "^0.2.0",
+ "tiny-lru": "^11.4.7"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/pixijs"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -4476,7 +4572,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4486,7 +4581,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -5145,6 +5239,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tiny-lru": {
+ "version": "11.4.7",
+ "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz",
+ "integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/tinybench": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz",
@@ -5204,7 +5307,6 @@
"integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "~0.28.0"
},
@@ -5352,7 +5454,6 @@
"integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.5",
@@ -5431,7 +5532,6 @@
"integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/mocker": "5.0.0",
diff --git a/app/package.json b/app/package.json
index d8b838c..c6304b1 100644
--- a/app/package.json
+++ b/app/package.json
@@ -5,12 +5,13 @@
"type": "module",
"scripts": {
"build": "tsc -b && vite build && npm run build:server && node scripts/check-bundle.mjs",
+ "build:game": "tsc -b && vite build --config vite.game.config.ts",
"build:server": "esbuild server/index.ts --bundle --platform=node --target=node22 --format=esm --packages=external --outfile=dist-server/index.js && cp -R server/lib/migrations dist-server/migrations",
"check:protocol": "node scripts/check-protocol.mjs",
"db:import": "tsx server/import-json.ts",
"db:migrate": "tsx server/migrate.ts",
- "db:verify": "tsx server/verify-schema.ts",
"db:up": "docker run -d --name shell-online-pg -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=shell_online -p 5433:5432 postgres:16-alpine",
+ "db:verify": "tsx server/verify-schema.ts",
"dev": "vite",
"dev:accounts": "tsx watch --env-file-if-exists=.env.local server/index.ts",
"dev:all": "npm-run-all --parallel dev dev:accounts",
@@ -18,12 +19,13 @@
"preview": "vite preview",
"render:deploy-config": "node scripts/render-deploy-config.mjs",
"start": "node dist-server/index.js",
- "test": "vitest run && node scripts/check-protocol.mjs",
+ "test": "vitest run && node scripts/check-protocol.mjs && node scripts/check-game-ui.mjs",
"test:pg": "TEST_DATABASE_URL=${TEST_DATABASE_URL:-postgres://postgres:dev@localhost:5433/shell_online} vitest run",
"test:watch": "vitest",
"typecheck": "tsc -b --force",
+ "verify:bundle": "node scripts/check-bundle.mjs",
"verify:deploy": "node scripts/verify-deploy.mjs",
- "verify:bundle": "node scripts/check-bundle.mjs"
+ "verify:game-ui": "node scripts/check-game-ui.mjs"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
@@ -33,6 +35,8 @@
"jose": "^6.2.12",
"oidc-client-ts": "^3.5.0",
"pg": "^8.23.0",
+ "pixi-viewport": "^6.0.3",
+ "pixi.js": "^8.20.1",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router-dom": "^7.18.3"
diff --git a/app/public/fonts/OFL-Pirata-One.txt b/app/public/fonts/OFL-Pirata-One.txt
new file mode 100644
index 0000000..9556702
--- /dev/null
+++ b/app/public/fonts/OFL-Pirata-One.txt
@@ -0,0 +1,93 @@
+Copyright (c) 2012, Rodrigo Fuenzalida, Nicolas Massi (www.taip.com.ar / abc.taip.com.ar), with Reserved Font Name 'Pirata'
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/app/public/fonts/pirata-one.woff2 b/app/public/fonts/pirata-one.woff2
new file mode 100644
index 0000000..3a3accf
Binary files /dev/null and b/app/public/fonts/pirata-one.woff2 differ
diff --git a/app/public/game/fx-circle_05.png b/app/public/game/fx-circle_05.png
new file mode 100644
index 0000000..8de3f69
Binary files /dev/null and b/app/public/game/fx-circle_05.png differ
diff --git a/app/public/game/fx-flare_01.png b/app/public/game/fx-flare_01.png
new file mode 100644
index 0000000..fb452a1
Binary files /dev/null and b/app/public/game/fx-flare_01.png differ
diff --git a/app/public/game/fx-light_01.png b/app/public/game/fx-light_01.png
new file mode 100644
index 0000000..71f8f0f
Binary files /dev/null and b/app/public/game/fx-light_01.png differ
diff --git a/app/public/game/fx-magic_05.png b/app/public/game/fx-magic_05.png
new file mode 100644
index 0000000..a46b215
Binary files /dev/null and b/app/public/game/fx-magic_05.png differ
diff --git a/app/public/game/fx-smoke_01.png b/app/public/game/fx-smoke_01.png
new file mode 100644
index 0000000..1cd418e
Binary files /dev/null and b/app/public/game/fx-smoke_01.png differ
diff --git a/app/public/game/fx-smoke_04.png b/app/public/game/fx-smoke_04.png
new file mode 100644
index 0000000..74a2695
Binary files /dev/null and b/app/public/game/fx-smoke_04.png differ
diff --git a/app/public/game/fx-spark_04.png b/app/public/game/fx-spark_04.png
new file mode 100644
index 0000000..98d0f0a
Binary files /dev/null and b/app/public/game/fx-spark_04.png differ
diff --git a/app/public/game/fx-star_04.png b/app/public/game/fx-star_04.png
new file mode 100644
index 0000000..5e241c6
Binary files /dev/null and b/app/public/game/fx-star_04.png differ
diff --git a/app/public/game/kingdom/banner-a.png b/app/public/game/kingdom/banner-a.png
new file mode 100644
index 0000000..dc813e1
Binary files /dev/null and b/app/public/game/kingdom/banner-a.png differ
diff --git a/app/public/game/kingdom/banner-b.png b/app/public/game/kingdom/banner-b.png
new file mode 100644
index 0000000..5bf2e4c
Binary files /dev/null and b/app/public/game/kingdom/banner-b.png differ
diff --git a/app/public/game/kingdom/banner-c.png b/app/public/game/kingdom/banner-c.png
new file mode 100644
index 0000000..f9a0279
Binary files /dev/null and b/app/public/game/kingdom/banner-c.png differ
diff --git a/app/public/game/kingdom/banner-d.png b/app/public/game/kingdom/banner-d.png
new file mode 100644
index 0000000..4a7cda4
Binary files /dev/null and b/app/public/game/kingdom/banner-d.png differ
diff --git a/app/public/game/kingdom/castle.svg b/app/public/game/kingdom/castle.svg
new file mode 100755
index 0000000..cccfe6f
--- /dev/null
+++ b/app/public/game/kingdom/castle.svg
@@ -0,0 +1,531 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/kingdom/goblet.svg b/app/public/game/kingdom/goblet.svg
new file mode 100755
index 0000000..d34af99
--- /dev/null
+++ b/app/public/game/kingdom/goblet.svg
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/kingdom/grass-band-a.png b/app/public/game/kingdom/grass-band-a.png
new file mode 100644
index 0000000..aa5b17c
Binary files /dev/null and b/app/public/game/kingdom/grass-band-a.png differ
diff --git a/app/public/game/kingdom/grass-band-b.png b/app/public/game/kingdom/grass-band-b.png
new file mode 100644
index 0000000..c2807cd
Binary files /dev/null and b/app/public/game/kingdom/grass-band-b.png differ
diff --git a/app/public/game/kingdom/grass-band-c.png b/app/public/game/kingdom/grass-band-c.png
new file mode 100644
index 0000000..63f98bc
Binary files /dev/null and b/app/public/game/kingdom/grass-band-c.png differ
diff --git a/app/public/game/kingdom/grass-band-d.png b/app/public/game/kingdom/grass-band-d.png
new file mode 100644
index 0000000..a1bb70a
Binary files /dev/null and b/app/public/game/kingdom/grass-band-d.png differ
diff --git a/app/public/game/kingdom/grass-tuft-a.png b/app/public/game/kingdom/grass-tuft-a.png
new file mode 100644
index 0000000..497dfe5
Binary files /dev/null and b/app/public/game/kingdom/grass-tuft-a.png differ
diff --git a/app/public/game/kingdom/grass-tuft-b.png b/app/public/game/kingdom/grass-tuft-b.png
new file mode 100644
index 0000000..a2f5525
Binary files /dev/null and b/app/public/game/kingdom/grass-tuft-b.png differ
diff --git a/app/public/game/kingdom/hero-banner.png b/app/public/game/kingdom/hero-banner.png
new file mode 100644
index 0000000..1a0e345
Binary files /dev/null and b/app/public/game/kingdom/hero-banner.png differ
diff --git a/app/public/game/kingdom/lance.svg b/app/public/game/kingdom/lance.svg
new file mode 100755
index 0000000..d74a277
--- /dev/null
+++ b/app/public/game/kingdom/lance.svg
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/kingdom/map.svg b/app/public/game/kingdom/map.svg
new file mode 100755
index 0000000..560adde
--- /dev/null
+++ b/app/public/game/kingdom/map.svg
@@ -0,0 +1,396 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/kingdom/pine-broad.svg b/app/public/game/kingdom/pine-broad.svg
new file mode 100644
index 0000000..875891f
--- /dev/null
+++ b/app/public/game/kingdom/pine-broad.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/kingdom/pine-dark.svg b/app/public/game/kingdom/pine-dark.svg
new file mode 100644
index 0000000..260829f
--- /dev/null
+++ b/app/public/game/kingdom/pine-dark.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/public/game/kingdom/pine-light-a.svg b/app/public/game/kingdom/pine-light-a.svg
new file mode 100644
index 0000000..a76f65a
--- /dev/null
+++ b/app/public/game/kingdom/pine-light-a.svg
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/kingdom/pine-light-b.svg b/app/public/game/kingdom/pine-light-b.svg
new file mode 100644
index 0000000..eed3d6e
--- /dev/null
+++ b/app/public/game/kingdom/pine-light-b.svg
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/kingdom/pine-tall.svg b/app/public/game/kingdom/pine-tall.svg
new file mode 100644
index 0000000..581b525
--- /dev/null
+++ b/app/public/game/kingdom/pine-tall.svg
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/kingdom/shield.svg b/app/public/game/kingdom/shield.svg
new file mode 100755
index 0000000..10d0cfe
--- /dev/null
+++ b/app/public/game/kingdom/shield.svg
@@ -0,0 +1,326 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/kingdom/siege.png b/app/public/game/kingdom/siege.png
new file mode 100644
index 0000000..9454f7e
Binary files /dev/null and b/app/public/game/kingdom/siege.png differ
diff --git a/app/public/game/kingdom/torch.svg b/app/public/game/kingdom/torch.svg
new file mode 100755
index 0000000..de83906
--- /dev/null
+++ b/app/public/game/kingdom/torch.svg
@@ -0,0 +1,361 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/kingdom/tower.svg b/app/public/game/kingdom/tower.svg
new file mode 100755
index 0000000..29047ae
--- /dev/null
+++ b/app/public/game/kingdom/tower.svg
@@ -0,0 +1,243 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/kingdom/wagon.svg b/app/public/game/kingdom/wagon.svg
new file mode 100755
index 0000000..29a0b2e
--- /dev/null
+++ b/app/public/game/kingdom/wagon.svg
@@ -0,0 +1,400 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/public/game/medieval-rts.json b/app/public/game/medieval-rts.json
new file mode 100644
index 0000000..93c2dd1
--- /dev/null
+++ b/app/public/game/medieval-rts.json
@@ -0,0 +1,2534 @@
+{
+ "frames": {
+ "Environment_01": {
+ "frame": {
+ "x": 1029,
+ "y": 636,
+ "w": 38,
+ "h": 92
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 38,
+ "h": 92
+ },
+ "sourceSize": {
+ "w": 38,
+ "h": 92
+ }
+ },
+ "Environment_02": {
+ "frame": {
+ "x": 1030,
+ "y": 728,
+ "w": 37,
+ "h": 66
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 37,
+ "h": 66
+ },
+ "sourceSize": {
+ "w": 37,
+ "h": 66
+ }
+ },
+ "Environment_03": {
+ "frame": {
+ "x": 1023,
+ "y": 0,
+ "w": 53,
+ "h": 96
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 53,
+ "h": 96
+ },
+ "sourceSize": {
+ "w": 53,
+ "h": 96
+ }
+ },
+ "Environment_04": {
+ "frame": {
+ "x": 838,
+ "y": 736,
+ "w": 28,
+ "h": 30
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 28,
+ "h": 30
+ },
+ "sourceSize": {
+ "w": 28,
+ "h": 30
+ }
+ },
+ "Environment_05": {
+ "frame": {
+ "x": 766,
+ "y": 736,
+ "w": 72,
+ "h": 30
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 72,
+ "h": 30
+ },
+ "sourceSize": {
+ "w": 72,
+ "h": 30
+ }
+ },
+ "Environment_06": {
+ "frame": {
+ "x": 804,
+ "y": 603,
+ "w": 36,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 36,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 36,
+ "h": 32
+ }
+ },
+ "Environment_07": {
+ "frame": {
+ "x": 976,
+ "y": 963,
+ "w": 64,
+ "h": 56
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 56
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 56
+ }
+ },
+ "Environment_08": {
+ "frame": {
+ "x": 950,
+ "y": 535,
+ "w": 76,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 76,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 76,
+ "h": 64
+ }
+ },
+ "Environment_09": {
+ "frame": {
+ "x": 950,
+ "y": 465,
+ "w": 76,
+ "h": 70
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 76,
+ "h": 70
+ },
+ "sourceSize": {
+ "w": 76,
+ "h": 70
+ }
+ },
+ "Environment_10": {
+ "frame": {
+ "x": 950,
+ "y": 395,
+ "w": 76,
+ "h": 70
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 76,
+ "h": 70
+ },
+ "sourceSize": {
+ "w": 76,
+ "h": 70
+ }
+ },
+ "Environment_11": {
+ "frame": {
+ "x": 950,
+ "y": 325,
+ "w": 76,
+ "h": 70
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 76,
+ "h": 70
+ },
+ "sourceSize": {
+ "w": 76,
+ "h": 70
+ }
+ },
+ "Environment_12": {
+ "frame": {
+ "x": 903,
+ "y": 255,
+ "w": 40,
+ "h": 46
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 40,
+ "h": 46
+ },
+ "sourceSize": {
+ "w": 40,
+ "h": 46
+ }
+ },
+ "Environment_13": {
+ "frame": {
+ "x": 768,
+ "y": 603,
+ "w": 36,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 36,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 36,
+ "h": 32
+ }
+ },
+ "Environment_14": {
+ "frame": {
+ "x": 951,
+ "y": 190,
+ "w": 64,
+ "h": 56
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 56
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 56
+ }
+ },
+ "Environment_15": {
+ "frame": {
+ "x": 900,
+ "y": 950,
+ "w": 76,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 76,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 76,
+ "h": 64
+ }
+ },
+ "Environment_16": {
+ "frame": {
+ "x": 900,
+ "y": 1014,
+ "w": 76,
+ "h": 70
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 76,
+ "h": 70
+ },
+ "sourceSize": {
+ "w": 76,
+ "h": 70
+ }
+ },
+ "Environment_17": {
+ "frame": {
+ "x": 951,
+ "y": 120,
+ "w": 76,
+ "h": 70
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 76,
+ "h": 70
+ },
+ "sourceSize": {
+ "w": 76,
+ "h": 70
+ }
+ },
+ "Environment_18": {
+ "frame": {
+ "x": 950,
+ "y": 255,
+ "w": 76,
+ "h": 70
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 76,
+ "h": 70
+ },
+ "sourceSize": {
+ "w": 76,
+ "h": 70
+ }
+ },
+ "Environment_19": {
+ "frame": {
+ "x": 863,
+ "y": 255,
+ "w": 40,
+ "h": 47
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 40,
+ "h": 47
+ },
+ "sourceSize": {
+ "w": 40,
+ "h": 47
+ }
+ },
+ "Environment_20": {
+ "frame": {
+ "x": 1026,
+ "y": 318,
+ "w": 43,
+ "h": 96
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 43,
+ "h": 96
+ },
+ "sourceSize": {
+ "w": 43,
+ "h": 96
+ }
+ },
+ "Environment_21": {
+ "frame": {
+ "x": 1076,
+ "y": 0,
+ "w": 28,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 28,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 28,
+ "h": 64
+ }
+ },
+ "Structure_01": {
+ "frame": {
+ "x": 957,
+ "y": 699,
+ "w": 72,
+ "h": 84
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 72,
+ "h": 84
+ },
+ "sourceSize": {
+ "w": 72,
+ "h": 84
+ }
+ },
+ "Structure_02": {
+ "frame": {
+ "x": 384,
+ "y": 1024,
+ "w": 120,
+ "h": 76
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 120,
+ "h": 76
+ },
+ "sourceSize": {
+ "w": 120,
+ "h": 76
+ }
+ },
+ "Structure_03": {
+ "frame": {
+ "x": 608,
+ "y": 1024,
+ "w": 104,
+ "h": 68
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 104,
+ "h": 68
+ },
+ "sourceSize": {
+ "w": 104,
+ "h": 68
+ }
+ },
+ "Structure_04": {
+ "frame": {
+ "x": 812,
+ "y": 1013,
+ "w": 88,
+ "h": 72
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 88,
+ "h": 72
+ },
+ "sourceSize": {
+ "w": 88,
+ "h": 72
+ }
+ },
+ "Structure_05": {
+ "frame": {
+ "x": 862,
+ "y": 399,
+ "w": 88,
+ "h": 84
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 88,
+ "h": 84
+ },
+ "sourceSize": {
+ "w": 88,
+ "h": 84
+ }
+ },
+ "Structure_06": {
+ "frame": {
+ "x": 128,
+ "y": 1024,
+ "w": 120,
+ "h": 76
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 120,
+ "h": 76
+ },
+ "sourceSize": {
+ "w": 120,
+ "h": 76
+ }
+ },
+ "Structure_07": {
+ "frame": {
+ "x": 504,
+ "y": 1024,
+ "w": 104,
+ "h": 68
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 104,
+ "h": 68
+ },
+ "sourceSize": {
+ "w": 104,
+ "h": 68
+ }
+ },
+ "Structure_08": {
+ "frame": {
+ "x": 959,
+ "y": 0,
+ "w": 64,
+ "h": 80
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 80
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 80
+ }
+ },
+ "Structure_09": {
+ "frame": {
+ "x": 760,
+ "y": 893,
+ "w": 112,
+ "h": 120
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 112,
+ "h": 120
+ },
+ "sourceSize": {
+ "w": 112,
+ "h": 120
+ }
+ },
+ "Structure_10": {
+ "frame": {
+ "x": 862,
+ "y": 555,
+ "w": 88,
+ "h": 80
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 88,
+ "h": 80
+ },
+ "sourceSize": {
+ "w": 88,
+ "h": 80
+ }
+ },
+ "Structure_11": {
+ "frame": {
+ "x": 863,
+ "y": 175,
+ "w": 88,
+ "h": 80
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 88,
+ "h": 80
+ },
+ "sourceSize": {
+ "w": 88,
+ "h": 80
+ }
+ },
+ "Structure_12": {
+ "frame": {
+ "x": 1026,
+ "y": 514,
+ "w": 40,
+ "h": 72
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 40,
+ "h": 72
+ },
+ "sourceSize": {
+ "w": 40,
+ "h": 72
+ }
+ },
+ "Structure_13": {
+ "frame": {
+ "x": 768,
+ "y": 0,
+ "w": 103,
+ "h": 103
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 103,
+ "h": 103
+ },
+ "sourceSize": {
+ "w": 103,
+ "h": 103
+ }
+ },
+ "Structure_14": {
+ "frame": {
+ "x": 640,
+ "y": 768,
+ "w": 125,
+ "h": 125
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 125,
+ "h": 125
+ },
+ "sourceSize": {
+ "w": 125,
+ "h": 125
+ }
+ },
+ "Structure_15": {
+ "frame": {
+ "x": 640,
+ "y": 640,
+ "w": 126,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 126,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 126,
+ "h": 128
+ }
+ },
+ "Structure_16": {
+ "frame": {
+ "x": 869,
+ "y": 736,
+ "w": 88,
+ "h": 120
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 88,
+ "h": 120
+ },
+ "sourceSize": {
+ "w": 88,
+ "h": 120
+ }
+ },
+ "Structure_17": {
+ "frame": {
+ "x": 871,
+ "y": 0,
+ "w": 88,
+ "h": 120
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 88,
+ "h": 120
+ },
+ "sourceSize": {
+ "w": 88,
+ "h": 120
+ }
+ },
+ "Structure_18": {
+ "frame": {
+ "x": 862,
+ "y": 303,
+ "w": 88,
+ "h": 96
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 88,
+ "h": 96
+ },
+ "sourceSize": {
+ "w": 88,
+ "h": 96
+ }
+ },
+ "Structure_19": {
+ "frame": {
+ "x": 765,
+ "y": 768,
+ "w": 104,
+ "h": 120
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 104,
+ "h": 120
+ },
+ "sourceSize": {
+ "w": 104,
+ "h": 120
+ }
+ },
+ "Structure_20": {
+ "frame": {
+ "x": 766,
+ "y": 640,
+ "w": 104,
+ "h": 96
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 104,
+ "h": 96
+ },
+ "sourceSize": {
+ "w": 104,
+ "h": 96
+ }
+ },
+ "Structure_21": {
+ "frame": {
+ "x": 640,
+ "y": 893,
+ "w": 120,
+ "h": 120
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 120,
+ "h": 120
+ },
+ "sourceSize": {
+ "w": 120,
+ "h": 120
+ }
+ },
+ "Structure_22": {
+ "frame": {
+ "x": 976,
+ "y": 883,
+ "w": 64,
+ "h": 80
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 80
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 80
+ }
+ },
+ "Structure_23": {
+ "frame": {
+ "x": 862,
+ "y": 483,
+ "w": 88,
+ "h": 72
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 88,
+ "h": 72
+ },
+ "sourceSize": {
+ "w": 88,
+ "h": 72
+ }
+ },
+ "Tile_01": {
+ "frame": {
+ "x": 0,
+ "y": 640,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_02": {
+ "frame": {
+ "x": 0,
+ "y": 768,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_03": {
+ "frame": {
+ "x": 0,
+ "y": 896,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_04": {
+ "frame": {
+ "x": 128,
+ "y": 128,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_05": {
+ "frame": {
+ "x": 128,
+ "y": 384,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_06": {
+ "frame": {
+ "x": 128,
+ "y": 640,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_07": {
+ "frame": {
+ "x": 128,
+ "y": 768,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_08": {
+ "frame": {
+ "x": 1026,
+ "y": 190,
+ "w": 60,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 60,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 60,
+ "h": 128
+ }
+ },
+ "Tile_09": {
+ "frame": {
+ "x": 0,
+ "y": 1024,
+ "w": 128,
+ "h": 60
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 60
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 60
+ }
+ },
+ "Tile_10": {
+ "frame": {
+ "x": 256,
+ "y": 128,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_11": {
+ "frame": {
+ "x": 256,
+ "y": 256,
+ "w": 128,
+ "h": 94
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 94
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 94
+ }
+ },
+ "Tile_12": {
+ "frame": {
+ "x": 256,
+ "y": 606,
+ "w": 128,
+ "h": 94
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 94
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 94
+ }
+ },
+ "Tile_13": {
+ "frame": {
+ "x": 256,
+ "y": 700,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_14": {
+ "frame": {
+ "x": 640,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_15": {
+ "frame": {
+ "x": 640,
+ "y": 128,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_16": {
+ "frame": {
+ "x": 640,
+ "y": 384,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_17": {
+ "frame": {
+ "x": 640,
+ "y": 512,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_18": {
+ "frame": {
+ "x": 384,
+ "y": 640,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_19": {
+ "frame": {
+ "x": 0,
+ "y": 128,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_20": {
+ "frame": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_21": {
+ "frame": {
+ "x": 384,
+ "y": 512,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_22": {
+ "frame": {
+ "x": 768,
+ "y": 303,
+ "w": 94,
+ "h": 86
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 94,
+ "h": 86
+ },
+ "sourceSize": {
+ "w": 94,
+ "h": 86
+ }
+ },
+ "Tile_23": {
+ "frame": {
+ "x": 870,
+ "y": 635,
+ "w": 86,
+ "h": 94
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 86,
+ "h": 94
+ },
+ "sourceSize": {
+ "w": 86,
+ "h": 94
+ }
+ },
+ "Tile_24": {
+ "frame": {
+ "x": 712,
+ "y": 1013,
+ "w": 100,
+ "h": 72
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 100,
+ "h": 72
+ },
+ "sourceSize": {
+ "w": 100,
+ "h": 72
+ }
+ },
+ "Tile_25": {
+ "frame": {
+ "x": 768,
+ "y": 389,
+ "w": 94,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 94,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 94,
+ "h": 128
+ }
+ },
+ "Tile_26": {
+ "frame": {
+ "x": 768,
+ "y": 175,
+ "w": 95,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 95,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 95,
+ "h": 128
+ }
+ },
+ "Tile_27": {
+ "frame": {
+ "x": 384,
+ "y": 896,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_28": {
+ "frame": {
+ "x": 512,
+ "y": 640,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_29": {
+ "frame": {
+ "x": 0,
+ "y": 384,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_30": {
+ "frame": {
+ "x": 0,
+ "y": 256,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_31": {
+ "frame": {
+ "x": 512,
+ "y": 128,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_32": {
+ "frame": {
+ "x": 512,
+ "y": 256,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_33": {
+ "frame": {
+ "x": 512,
+ "y": 384,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_34": {
+ "frame": {
+ "x": 640,
+ "y": 256,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_35": {
+ "frame": {
+ "x": 512,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_36": {
+ "frame": {
+ "x": 872,
+ "y": 856,
+ "w": 86,
+ "h": 94
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 86,
+ "h": 94
+ },
+ "sourceSize": {
+ "w": 86,
+ "h": 94
+ }
+ },
+ "Tile_37": {
+ "frame": {
+ "x": 768,
+ "y": 517,
+ "w": 94,
+ "h": 86
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 94,
+ "h": 86
+ },
+ "sourceSize": {
+ "w": 94,
+ "h": 86
+ }
+ },
+ "Tile_38": {
+ "frame": {
+ "x": 768,
+ "y": 103,
+ "w": 100,
+ "h": 72
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 100,
+ "h": 72
+ },
+ "sourceSize": {
+ "w": 100,
+ "h": 72
+ }
+ },
+ "Tile_39": {
+ "frame": {
+ "x": 956,
+ "y": 599,
+ "w": 72,
+ "h": 100
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 72,
+ "h": 100
+ },
+ "sourceSize": {
+ "w": 72,
+ "h": 100
+ }
+ },
+ "Tile_40": {
+ "frame": {
+ "x": 958,
+ "y": 783,
+ "w": 72,
+ "h": 100
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 72,
+ "h": 100
+ },
+ "sourceSize": {
+ "w": 72,
+ "h": 100
+ }
+ },
+ "Tile_41": {
+ "frame": {
+ "x": 512,
+ "y": 896,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_42": {
+ "frame": {
+ "x": 512,
+ "y": 768,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_43": {
+ "frame": {
+ "x": 256,
+ "y": 478,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_44": {
+ "frame": {
+ "x": 512,
+ "y": 512,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_45": {
+ "frame": {
+ "x": 256,
+ "y": 350,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_46": {
+ "frame": {
+ "x": 256,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_47": {
+ "frame": {
+ "x": 128,
+ "y": 896,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_48": {
+ "frame": {
+ "x": 128,
+ "y": 512,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_49": {
+ "frame": {
+ "x": 128,
+ "y": 256,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_50": {
+ "frame": {
+ "x": 384,
+ "y": 768,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_51": {
+ "frame": {
+ "x": 128,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_52": {
+ "frame": {
+ "x": 0,
+ "y": 512,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_53": {
+ "frame": {
+ "x": 384,
+ "y": 384,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_54": {
+ "frame": {
+ "x": 384,
+ "y": 256,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_55": {
+ "frame": {
+ "x": 384,
+ "y": 128,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_56": {
+ "frame": {
+ "x": 384,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_57": {
+ "frame": {
+ "x": 256,
+ "y": 956,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Tile_58": {
+ "frame": {
+ "x": 256,
+ "y": 828,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "Unit_01": {
+ "frame": {
+ "x": 1067,
+ "y": 96,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ },
+ "Unit_02": {
+ "frame": {
+ "x": 909,
+ "y": 120,
+ "w": 41,
+ "h": 52
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 41,
+ "h": 52
+ },
+ "sourceSize": {
+ "w": 41,
+ "h": 52
+ }
+ },
+ "Unit_03": {
+ "frame": {
+ "x": 1030,
+ "y": 794,
+ "w": 36,
+ "h": 50
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 36,
+ "h": 50
+ },
+ "sourceSize": {
+ "w": 36,
+ "h": 50
+ }
+ },
+ "Unit_04": {
+ "frame": {
+ "x": 1026,
+ "y": 464,
+ "w": 40,
+ "h": 50
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 40,
+ "h": 50
+ },
+ "sourceSize": {
+ "w": 40,
+ "h": 50
+ }
+ },
+ "Unit_05": {
+ "frame": {
+ "x": 1066,
+ "y": 794,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ },
+ "Unit_06": {
+ "frame": {
+ "x": 1058,
+ "y": 994,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ },
+ "Unit_07": {
+ "frame": {
+ "x": 1058,
+ "y": 1042,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ },
+ "Unit_08": {
+ "frame": {
+ "x": 868,
+ "y": 120,
+ "w": 41,
+ "h": 52
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 41,
+ "h": 52
+ },
+ "sourceSize": {
+ "w": 41,
+ "h": 52
+ }
+ },
+ "Unit_09": {
+ "frame": {
+ "x": 1040,
+ "y": 844,
+ "w": 36,
+ "h": 50
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 36,
+ "h": 50
+ },
+ "sourceSize": {
+ "w": 36,
+ "h": 50
+ }
+ },
+ "Unit_10": {
+ "frame": {
+ "x": 1027,
+ "y": 96,
+ "w": 40,
+ "h": 50
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 40,
+ "h": 50
+ },
+ "sourceSize": {
+ "w": 40,
+ "h": 50
+ }
+ },
+ "Unit_11": {
+ "frame": {
+ "x": 1066,
+ "y": 462,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ },
+ "Unit_12": {
+ "frame": {
+ "x": 1068,
+ "y": 558,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ },
+ "Unit_13": {
+ "frame": {
+ "x": 1066,
+ "y": 414,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ },
+ "Unit_14": {
+ "frame": {
+ "x": 1017,
+ "y": 1019,
+ "w": 41,
+ "h": 52
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 41,
+ "h": 52
+ },
+ "sourceSize": {
+ "w": 41,
+ "h": 52
+ }
+ },
+ "Unit_15": {
+ "frame": {
+ "x": 1040,
+ "y": 894,
+ "w": 36,
+ "h": 50
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 36,
+ "h": 50
+ },
+ "sourceSize": {
+ "w": 36,
+ "h": 50
+ }
+ },
+ "Unit_16": {
+ "frame": {
+ "x": 1026,
+ "y": 414,
+ "w": 40,
+ "h": 50
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 40,
+ "h": 50
+ },
+ "sourceSize": {
+ "w": 40,
+ "h": 50
+ }
+ },
+ "Unit_17": {
+ "frame": {
+ "x": 1066,
+ "y": 510,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ },
+ "Unit_18": {
+ "frame": {
+ "x": 1067,
+ "y": 636,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ },
+ "Unit_19": {
+ "frame": {
+ "x": 1067,
+ "y": 684,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ },
+ "Unit_20": {
+ "frame": {
+ "x": 976,
+ "y": 1019,
+ "w": 41,
+ "h": 52
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 41,
+ "h": 52
+ },
+ "sourceSize": {
+ "w": 41,
+ "h": 52
+ }
+ },
+ "Unit_21": {
+ "frame": {
+ "x": 1040,
+ "y": 944,
+ "w": 36,
+ "h": 50
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 36,
+ "h": 50
+ },
+ "sourceSize": {
+ "w": 36,
+ "h": 50
+ }
+ },
+ "Unit_22": {
+ "frame": {
+ "x": 1028,
+ "y": 586,
+ "w": 40,
+ "h": 50
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 40,
+ "h": 50
+ },
+ "sourceSize": {
+ "w": 40,
+ "h": 50
+ }
+ },
+ "Unit_23": {
+ "frame": {
+ "x": 1067,
+ "y": 732,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ },
+ "Unit_24": {
+ "frame": {
+ "x": 1069,
+ "y": 318,
+ "w": 33,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 33,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 33,
+ "h": 48
+ }
+ }
+ },
+ "meta": {
+ "image": "medieval-rts.png",
+ "format": "RGBA8888",
+ "size": {
+ "w": 1104,
+ "h": 1100
+ },
+ "scale": "1",
+ "app": "kenney.nl medieval-rts, CC0"
+ }
+}
diff --git a/app/public/game/medieval-rts.png b/app/public/game/medieval-rts.png
new file mode 100644
index 0000000..e67ff83
Binary files /dev/null and b/app/public/game/medieval-rts.png differ
diff --git a/app/public/game/ui/frame-heavy.png b/app/public/game/ui/frame-heavy.png
new file mode 100644
index 0000000..007278c
Binary files /dev/null and b/app/public/game/ui/frame-heavy.png differ
diff --git a/app/public/game/ui/frame-panel.png b/app/public/game/ui/frame-panel.png
new file mode 100644
index 0000000..fded4b3
Binary files /dev/null and b/app/public/game/ui/frame-panel.png differ
diff --git a/app/public/game/ui/pointer-press.svg b/app/public/game/ui/pointer-press.svg
new file mode 100644
index 0000000..911e305
--- /dev/null
+++ b/app/public/game/ui/pointer-press.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/app/public/game/ui/pointer.svg b/app/public/game/ui/pointer.svg
new file mode 100644
index 0000000..2dcb091
--- /dev/null
+++ b/app/public/game/ui/pointer.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/qa.html b/app/qa.html
new file mode 100644
index 0000000..a9f732e
--- /dev/null
+++ b/app/qa.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ shell.online — QA
+
+
+
+
+
+
+
diff --git a/app/scripts/check-bundle.mjs b/app/scripts/check-bundle.mjs
index 535fec0..238b4a6 100644
--- a/app/scripts/check-bundle.mjs
+++ b/app/scripts/check-bundle.mjs
@@ -85,4 +85,59 @@ if (blank.length > 0) {
process.exit(1);
}
+// The game skin must not be in what the corporate view downloads.
+//
+// src/game carries a renderer, a sprite atlas and a stylesheet of its own, for
+// a screen most sessions never open. It is reached through a dynamic import so
+// that none of it is fetched until somebody asks for it, and that is the sort
+// of property which holds right up until a convenient-looking direct import
+// puts it back. So it is checked rather than trusted.
+//
+// What is checked is everything the browser loads before first paint: the entry
+// script, plus the chunks Vite preloads alongside it because the entry imports
+// them statically. A lazily-imported chunk appears in neither, which is exactly
+// the point.
+const KEEP_MARKER = "__SHELL_KEEP__";
+
+async function eagerChunks(dir) {
+ let html;
+ try {
+ html = await readFile(join(dir, "index.html"), "utf8");
+ } catch {
+ // No index.html means this is not a client build; nothing to check.
+ return [];
+ }
+ const paths = new Set();
+ for (const [, src] of html.matchAll(/