Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b0e8671
Add the game skin's frame: a lazy route, a way in, and a way out
Sep 16, 2026
304a496
Draw the keep: a sprite format, a renderer, and the first of the art
Sep 16, 2026
09a64df
Make the dev server render again, and add a door onto the keep
Sep 16, 2026
ce19a72
Move the camera above the holding, warm the palette, and fill the window
Sep 16, 2026
78b97ae
Let the whole app be run signed in, so the game can be tested inside it
Sep 16, 2026
ce9e655
Put the garrison on the field, and give the place a history
Sep 16, 2026
52e810d
Give the garrison something to do, and somewhere to see how it went
Sep 16, 2026
64845ad
Make the garrison real, and make it move
Sep 16, 2026
9e21234
Add the one decision the game asks for, a pedlar, and a Chronicle
Sep 16, 2026
e68f258
Save the keep on the service, so it follows you to another machine
Sep 16, 2026
412dc99
Make the keep survive a phone, and a panel survive gaining a line
Sep 16, 2026
62f3df3
Record the game skin in the changelog
Sep 16, 2026
ad60b2d
Rebuild the world on PixiJS: a real map, isometric, with the lore on …
Sep 16, 2026
84b79bb
Put people on the Marches, make them clickable, and stop them shaking
Sep 16, 2026
c99eeba
Make a wright clickable, and stop the field filling with duplicates
Sep 16, 2026
593f3c2
Frame the interface in brass, and put the lore on boards you can read
Sep 16, 2026
68b27c7
Hold the camera on the country when you zoom out
Sep 16, 2026
320267b
Earn levels from work that happened, not from the battle on screen
Sep 16, 2026
fba9fea
Open on the whole country, not on the quiet village in the middle
Sep 16, 2026
be973f4
Make the Marches four times the country, with nine holdings on it
Sep 16, 2026
9b99f74
Put woods, boulder fields and deadfall on the Marches
Sep 16, 2026
6c0c49c
Raise dust off the heels of anything walking
Sep 16, 2026
8dbd858
Say what the gathering reads, and show what it has cost
Sep 16, 2026
4fb24cf
Gather the statistics on the machine, where the plaintext is
Sep 16, 2026
e29c370
Bring the game's own notes back in line with what it is
Sep 16, 2026
632e548
Close the Marches in with a wood, so the map is not a floating polygon
Sep 16, 2026
1c3719e
Make the border wood old growth: massive trees, boulders, and ruins i…
Sep 16, 2026
6a52f7c
Stop the ground cropping the treeline, and the far wood being one fla…
Sep 16, 2026
511fc66
Make every team member a hero, and every session their soldier
Sep 16, 2026
6a39a55
Give heroes a shield, a name and two bars, and soldiers their class
Sep 16, 2026
e6be4dd
Audit: four faults, a dead function, and a layer pointing the wrong way
Sep 16, 2026
9752380
Give every hero a colour, and the map something to watch
Sep 16, 2026
b58f3a0
Put each harness's own mark over its soldiers, instead of spelling th…
Sep 16, 2026
97ba7ba
Break the HUD strip into four corners, and give it a crest
Sep 16, 2026
a29449d
Stand the inspect card beside the figure, and walk it along with them
Sep 16, 2026
9ebf5d6
Add the Barrow, letter the signposts, and make the shop reachable
Sep 16, 2026
0087ea6
Draw the Unmade as insects, and answer a click where it was made
Sep 16, 2026
9b35fd2
Fly banners at the camps, and bring the medieval pack onto the map
Sep 16, 2026
bcbc485
Answer a click anywhere on the body, and raise a castle over the Keep
Sep 16, 2026
1c66829
Record the body hitbox, the turquoise rim and the landmarks in the spec
Sep 16, 2026
7de8553
Bend the roads, line them with fences and lamps, and light the map fo…
Sep 16, 2026
e24fe4c
Stand everything on its own ground, and stop the fences piling up
Sep 17, 2026
6d12e1d
Turn the lamps up, lift the dusk, and put some daylight back in the wood
Sep 17, 2026
96c3bc9
Put the map in one perspective, stop people walking through walls, an…
Sep 17, 2026
4423694
Tank the heroes, lay the castle on the map's angle, and thin the fences
Sep 17, 2026
c898d2a
Sell the figure rather than the colour
Sep 17, 2026
5ac4f15
Build the castle out of pieces that were isometric to begin with
Sep 17, 2026
ea21e7c
Merge remote-tracking branch 'origin/main' into game-skin
Sep 17, 2026
f1a2722
Fix the CI break, cap what the field draws, and tidy comments that ha…
Sep 17, 2026
9e93bd4
Give the game its own Worker, its own pipeline, and no route yet
Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions .github/workflows/deploy-game.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ worker-configuration.d.ts

# Marketing renders are maintained outside the source tree.
/promo/

app/dist-game/
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions app/game-preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#0d1009" />
<title>Shell Keep — preview</title>
<!--
A development-only door onto the game skin.

The real route is /game inside the app, behind sign-in. This page mounts
the same component with no authentication around it so the interface can
be looked at on a machine that has no provider configured.

It is not an input to `vite build` -- that builds index.html and nothing
else -- so this exists while `npm run dev` is running and in no
deployment. See src/game/preview.tsx.
-->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/game/preview.tsx"></script>
</body>
</html>
16 changes: 16 additions & 0 deletions app/game.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="referrer" content="no-referrer" />
<meta name="robots" content="noindex, nofollow" />
<meta name="theme-color" content="#161914" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<title>Shell Keep</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/game/main.tsx"></script>
</body>
</html>
Loading
Loading