@bongos/core 1.19.335 → 1.19.337
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +26 -61
- package/.devcontainer/post-create.sh +3 -7
- package/docs/adr/0011-terrain-id-rendering-model.md +2 -1
- package/docs/adr/0012-ground-object-cell-split.md +2 -1
- package/modules/economy/achievements.js +5 -4
- package/modules/economy/credits.js +3 -1
- package/modules/economy/policy.js +22 -0
- package/modules/provisioning/routes/provisioning.js +12 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness-checks-identity.js +0 -1
- package/scripts/gds/fitness-ratchets.js +13 -3
- package/scripts/gds/run-unit-tests.js +10 -10
- package/src/module-api.js +5 -1
- package/tests/provisioning_github_app_routes.mjs +40 -1
- package/tests/search_isolation.mjs +4 -1
- package/scripts/gds/seed-v3-tasks.js +0 -1981
- package/scripts/snapshots/backfill-history.mjs +0 -292
- package/scripts/snapshots/maybe-render-on-deploy.sh +0 -66
- package/scripts/snapshots/render-pre-art-eras.mjs +0 -329
- package/scripts/snapshots/render-world.mjs +0 -322
- package/tests/anti_grid.mjs +0 -89
- package/tests/autotile_variants.mjs +0 -256
- package/tests/autotile_xverify.mjs +0 -133
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
// scripts/snapshots/backfill-history.mjs
|
|
2
|
-
//
|
|
3
|
-
// One-shot historical backfill: walk every commit that touched graphics-
|
|
4
|
-
// affecting files and render a snapshot from each commit's tree.
|
|
5
|
-
//
|
|
6
|
-
// Strategy:
|
|
7
|
-
// 1. Ask git for every commit touching `public/assets/tiles/`, `public/game/world/`,
|
|
8
|
-
// or `src/world/{map,terrain}.js` (in chronological order, oldest first).
|
|
9
|
-
// 2. For each commit, `git archive` the working tree into a temp dir.
|
|
10
|
-
// The renderer + its node_modules come from the CURRENT worktree (the
|
|
11
|
-
// renderer didn't exist in old commits) — only the world DATA comes
|
|
12
|
-
// from the historical commit.
|
|
13
|
-
// 3. Run the renderer with --sha + --timestamp + --label so each snapshot
|
|
14
|
-
// is stamped correctly.
|
|
15
|
-
//
|
|
16
|
-
// Pre-Phase-D commits (before INITIAL_MAP existed in its two-grid form) are
|
|
17
|
-
// skipped — they would crash the renderer. We start the timeline at Phase D
|
|
18
|
-
// and note the gap in CURATED_LABELS below.
|
|
19
|
-
//
|
|
20
|
-
// Usage:
|
|
21
|
-
// node scripts/snapshots/backfill-history.mjs # render all known visual milestones
|
|
22
|
-
// node scripts/snapshots/backfill-history.mjs --dry-run # just list what would be rendered
|
|
23
|
-
|
|
24
|
-
import { execSync, spawnSync } from 'node:child_process';
|
|
25
|
-
import { createHash } from 'node:crypto';
|
|
26
|
-
import { mkdtempSync, rmSync, mkdirSync, existsSync, cpSync, symlinkSync, writeFileSync, readFileSync, unlinkSync } from 'node:fs';
|
|
27
|
-
import { tmpdir } from 'node:os';
|
|
28
|
-
import { resolve, dirname } from 'node:path';
|
|
29
|
-
import { fileURLToPath } from 'node:url';
|
|
30
|
-
|
|
31
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
const ROOT = resolve(HERE, '../..');
|
|
33
|
-
// The status surface moved to modules/status-ui/public/ in BV1.R84 (task 1635).
|
|
34
|
-
const SNAPSHOTS_DIR = resolve(ROOT, 'modules/status-ui/public/snapshots');
|
|
35
|
-
|
|
36
|
-
function sh(cmd, opts = {}) {
|
|
37
|
-
return execSync(cmd, { cwd: ROOT, encoding: 'utf8', ...opts }).trim();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Curated labels for known visual milestones. Anything not listed gets the
|
|
41
|
-
// commit subject as its label.
|
|
42
|
-
const CURATED_LABELS = {
|
|
43
|
-
'5354daa': 'Phase D — the first world',
|
|
44
|
-
'6abe01c': 'The coast is surveyed — sand meets sea',
|
|
45
|
-
'865d64c': 'A door appears, and a path laid to it',
|
|
46
|
-
'7ac06df': 'A wanderer arrives — first sight of Hyperion at his door',
|
|
47
|
-
'13b796d': 'The grass turns green',
|
|
48
|
-
'358f2cd': 'Rocks scattered through the meadow; the container takes its full form',
|
|
49
|
-
'86cee70': 'Container wall refined (7.78 → 8.9)',
|
|
50
|
-
'0e630c8': 'Multi-variant interior tiles',
|
|
51
|
-
'6fc7c09': 'Hyperion stands taller — vendor poses refined',
|
|
52
|
-
'1c6654a': 'Container wall regenerated (atlas drift fix)',
|
|
53
|
-
'd438644': 'The container door painted solid',
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// Pre-Phase-D commits to NOT attempt — these crash the renderer (no two-grid map yet).
|
|
57
|
-
const SKIP_BEFORE = '5354daa'; // Phase D — first renderable commit.
|
|
58
|
-
|
|
59
|
-
function isAncestor(maybeAncestor, descendant) {
|
|
60
|
-
try {
|
|
61
|
-
execSync(`git merge-base --is-ancestor ${maybeAncestor} ${descendant}`, { cwd: ROOT, stdio: 'ignore' });
|
|
62
|
-
return true;
|
|
63
|
-
} catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function listGraphicsCommits() {
|
|
69
|
-
// %H short SHA + ISO date + subject. Reverse so oldest first.
|
|
70
|
-
const raw = sh(
|
|
71
|
-
`git log --reverse --format='%H%x09%aI%x09%s' --no-merges -- ` +
|
|
72
|
-
`'public/assets/tiles/' 'public/game/world/' 'src/world/map.js' 'src/world/terrain.js'`
|
|
73
|
-
);
|
|
74
|
-
return raw.split('\n').filter(Boolean).map((line) => {
|
|
75
|
-
const [sha, timestamp, subject] = line.split('\t');
|
|
76
|
-
return { sha, timestamp, subject };
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function commitChangedGraphics(sha) {
|
|
81
|
-
// Did THIS commit change any tile PNGs or world code?
|
|
82
|
-
// For some commits the path filter above matches a config-only change
|
|
83
|
-
// (e.g. manifest tweak) that doesn't actually re-render differently. We
|
|
84
|
-
// still render — let's accept a few duplicates rather than over-filter.
|
|
85
|
-
try {
|
|
86
|
-
const out = sh(`git diff-tree --no-commit-id --name-only -r ${sha} -- public/assets/tiles/ public/game/world/ src/world/map.js src/world/terrain.js`);
|
|
87
|
-
return out.length > 0;
|
|
88
|
-
} catch {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function setupRenderTree(sha, tmpRoot) {
|
|
94
|
-
// 1. Extract historical world data from the commit.
|
|
95
|
-
mkdirSync(tmpRoot, { recursive: true });
|
|
96
|
-
// git archive paths: tile assets + world code on both sides + the manifest.
|
|
97
|
-
// Use a wide net so we cover any restructuring across the history.
|
|
98
|
-
const archivePaths = [
|
|
99
|
-
'public/assets/tiles',
|
|
100
|
-
'public/game/world',
|
|
101
|
-
'src/world',
|
|
102
|
-
].filter((p) => {
|
|
103
|
-
// Skip non-existent paths at this commit (some commits predate src/world).
|
|
104
|
-
try {
|
|
105
|
-
execSync(`git ls-tree ${sha} -- ${p}`, { cwd: ROOT, stdio: 'ignore' });
|
|
106
|
-
return true;
|
|
107
|
-
} catch {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
if (archivePaths.length === 0) return false;
|
|
112
|
-
|
|
113
|
-
sh(`git archive ${sha} ${archivePaths.map((p) => `'${p}'`).join(' ')} | tar -x -C '${tmpRoot}'`);
|
|
114
|
-
|
|
115
|
-
// 2. Copy renderer + helpers from CURRENT worktree.
|
|
116
|
-
const rendererSrc = resolve(ROOT, 'scripts/snapshots/render-world.mjs');
|
|
117
|
-
const rendererDst = resolve(tmpRoot, 'scripts/snapshots/render-world.mjs');
|
|
118
|
-
mkdirSync(dirname(rendererDst), { recursive: true });
|
|
119
|
-
cpSync(rendererSrc, rendererDst);
|
|
120
|
-
|
|
121
|
-
// 3. Symlink node_modules from current worktree (sharp is required).
|
|
122
|
-
const nmSrc = resolve(ROOT, 'node_modules');
|
|
123
|
-
const nmDst = resolve(tmpRoot, 'node_modules');
|
|
124
|
-
if (!existsSync(nmDst)) symlinkSync(nmSrc, nmDst);
|
|
125
|
-
|
|
126
|
-
// 4. Some historical commits may have a different map.js shape (pre-ADR-0012)
|
|
127
|
-
// that crashes the renderer. We don't try to compensate — those commits
|
|
128
|
-
// are filtered upstream by SKIP_BEFORE.
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function shortLabelFor(entry) {
|
|
133
|
-
const short = entry.sha.slice(0, 7);
|
|
134
|
-
return CURATED_LABELS[short] || entry.subject;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function main() {
|
|
138
|
-
const dryRun = process.argv.includes('--dry-run');
|
|
139
|
-
|
|
140
|
-
// Resolve the canonical short SHA of SKIP_BEFORE for ancestor checks.
|
|
141
|
-
const skipBeforeFull = sh(`git rev-parse ${SKIP_BEFORE}`);
|
|
142
|
-
|
|
143
|
-
const commits = listGraphicsCommits();
|
|
144
|
-
console.log(`[backfill] found ${commits.length} commits touching graphics paths`);
|
|
145
|
-
|
|
146
|
-
const candidates = [];
|
|
147
|
-
for (const c of commits) {
|
|
148
|
-
if (!isAncestor(c.sha, 'HEAD')) {
|
|
149
|
-
// Not in current main lineage (e.g. a branch that was never merged).
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (c.sha !== skipBeforeFull && !isAncestor(skipBeforeFull, c.sha)) {
|
|
153
|
-
// Earlier than Phase D — renderer won't work.
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (!commitChangedGraphics(c.sha)) continue;
|
|
157
|
-
candidates.push(c);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Always include current HEAD as the final snapshot if it isn't in the list.
|
|
161
|
-
const head = sh('git rev-parse HEAD');
|
|
162
|
-
const headTs = sh('git log -1 --format=%aI HEAD');
|
|
163
|
-
const headSubj = sh('git log -1 --format=%s HEAD');
|
|
164
|
-
if (!candidates.find((c) => c.sha === head)) {
|
|
165
|
-
candidates.push({ sha: head, timestamp: headTs, subject: headSubj });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
console.log(`[backfill] will render ${candidates.length} historical snapshots:`);
|
|
169
|
-
for (const c of candidates) {
|
|
170
|
-
const lbl = shortLabelFor(c);
|
|
171
|
-
console.log(` ${c.sha.slice(0, 7)} ${c.timestamp.slice(0, 16)} ${lbl}`);
|
|
172
|
-
}
|
|
173
|
-
if (dryRun) {
|
|
174
|
-
console.log('[backfill] --dry-run: not rendering');
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
mkdirSync(SNAPSHOTS_DIR, { recursive: true });
|
|
179
|
-
|
|
180
|
-
let rendered = 0;
|
|
181
|
-
let failed = 0;
|
|
182
|
-
for (const c of candidates) {
|
|
183
|
-
const short = c.sha.slice(0, 7);
|
|
184
|
-
const label = shortLabelFor(c);
|
|
185
|
-
|
|
186
|
-
// Skip work if the snapshot already exists in the manifest with the same sha.
|
|
187
|
-
// (Re-runs are idempotent.)
|
|
188
|
-
const manifestPath = resolve(SNAPSHOTS_DIR, 'manifest.json');
|
|
189
|
-
if (existsSync(manifestPath)) {
|
|
190
|
-
try {
|
|
191
|
-
const m = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
192
|
-
if (Array.isArray(m.snapshots) && m.snapshots.find((s) => s.sha === c.sha)) {
|
|
193
|
-
console.log(`[backfill] skip ${short} (already in manifest)`);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
} catch { /* ignore */ }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const tmp = mkdtempSync(resolve(tmpdir(), `otb-snap-${short}-`));
|
|
200
|
-
console.log(`\n[backfill] rendering ${short} → ${tmp}`);
|
|
201
|
-
try {
|
|
202
|
-
const ok = setupRenderTree(c.sha, tmp);
|
|
203
|
-
if (!ok) {
|
|
204
|
-
console.log(`[backfill] skip ${short} — no archivable paths`);
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const result = spawnSync('node', [
|
|
208
|
-
resolve(tmp, 'scripts/snapshots/render-world.mjs'),
|
|
209
|
-
'--sha', c.sha,
|
|
210
|
-
'--timestamp', c.timestamp,
|
|
211
|
-
'--label', label,
|
|
212
|
-
'--id', short,
|
|
213
|
-
'--no-manifest',
|
|
214
|
-
'--out', resolve(SNAPSHOTS_DIR, `${short}.png`),
|
|
215
|
-
], { cwd: tmp, stdio: 'inherit' });
|
|
216
|
-
if (result.status !== 0) {
|
|
217
|
-
throw new Error(`renderer exit ${result.status}`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Update the canonical manifest in the main worktree directly.
|
|
221
|
-
let m = { version: 2, snapshots: [] };
|
|
222
|
-
if (existsSync(manifestPath)) {
|
|
223
|
-
try { m = JSON.parse(readFileSync(manifestPath, 'utf8')); } catch { /* fall back */ }
|
|
224
|
-
}
|
|
225
|
-
if (!Array.isArray(m.snapshots)) m.snapshots = [];
|
|
226
|
-
const entry = {
|
|
227
|
-
id: short,
|
|
228
|
-
sha: c.sha,
|
|
229
|
-
timestamp: c.timestamp,
|
|
230
|
-
label,
|
|
231
|
-
image: `${short}.png`,
|
|
232
|
-
};
|
|
233
|
-
const i = m.snapshots.findIndex((s) => s.id === entry.id);
|
|
234
|
-
if (i >= 0) m.snapshots[i] = entry;
|
|
235
|
-
else m.snapshots.push(entry);
|
|
236
|
-
m.snapshots.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
237
|
-
m.version = 2;
|
|
238
|
-
writeFileSync(manifestPath, JSON.stringify(m, null, 2) + '\n');
|
|
239
|
-
console.log(`[backfill] manifest: ${m.snapshots.length} entries`);
|
|
240
|
-
|
|
241
|
-
rendered++;
|
|
242
|
-
} catch (e) {
|
|
243
|
-
console.error(`[backfill] FAIL ${short}: ${e.message}`);
|
|
244
|
-
failed++;
|
|
245
|
-
} finally {
|
|
246
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
console.log(`\n[backfill] done — ${rendered} rendered, ${failed} failed`);
|
|
251
|
-
|
|
252
|
-
// ---- Dedup pass ----
|
|
253
|
-
// Many commits change graphics code or atlas plumbing without changing the
|
|
254
|
-
// rendered output. Group snapshots by PNG hash; keep only the earliest
|
|
255
|
-
// commit (by timestamp) in each group. The later identical entries get
|
|
256
|
-
// their PNG file removed and their manifest entry dropped.
|
|
257
|
-
const manifestPath = resolve(SNAPSHOTS_DIR, 'manifest.json');
|
|
258
|
-
if (!existsSync(manifestPath)) return;
|
|
259
|
-
const m = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
260
|
-
if (!Array.isArray(m.snapshots)) return;
|
|
261
|
-
|
|
262
|
-
const byHash = new Map();
|
|
263
|
-
for (const snap of m.snapshots) {
|
|
264
|
-
const imgPath = resolve(SNAPSHOTS_DIR, snap.image);
|
|
265
|
-
if (!existsSync(imgPath)) continue;
|
|
266
|
-
const hash = createHash('md5').update(readFileSync(imgPath)).digest('hex');
|
|
267
|
-
const existing = byHash.get(hash);
|
|
268
|
-
if (!existing || (snap.timestamp || '') < (existing.timestamp || '')) {
|
|
269
|
-
byHash.set(hash, snap);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
const keepIds = new Set(Array.from(byHash.values()).map((s) => s.id));
|
|
273
|
-
const dropped = [];
|
|
274
|
-
for (const snap of m.snapshots) {
|
|
275
|
-
if (!keepIds.has(snap.id)) {
|
|
276
|
-
dropped.push(snap.id);
|
|
277
|
-
const imgPath = resolve(SNAPSHOTS_DIR, snap.image);
|
|
278
|
-
if (existsSync(imgPath)) {
|
|
279
|
-
try { unlinkSync(imgPath); } catch { /* ignore */ }
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
m.snapshots = m.snapshots.filter((s) => keepIds.has(s.id));
|
|
284
|
-
m.snapshots.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
285
|
-
writeFileSync(manifestPath, JSON.stringify(m, null, 2) + '\n');
|
|
286
|
-
if (dropped.length > 0) {
|
|
287
|
-
console.log(`[backfill] dedup: removed ${dropped.length} duplicates: ${dropped.join(', ')}`);
|
|
288
|
-
}
|
|
289
|
-
console.log(`[backfill] manifest now has ${m.snapshots.length} unique snapshots`);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
await main();
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# scripts/snapshots/maybe-render-on-deploy.sh
|
|
3
|
-
#
|
|
4
|
-
# Called from ~/deploy.sh on the droplet. Decides whether the just-pulled
|
|
5
|
-
# commit changed the world's visible graphics; if so, renders a new
|
|
6
|
-
# snapshot tagged with the commit sha + author timestamp + subject.
|
|
7
|
-
#
|
|
8
|
-
# State: the previous deployed sha is stored at ~/.amazonprimea-last-deploy.
|
|
9
|
-
# If it's missing (first run), we still render a snapshot of HEAD so the
|
|
10
|
-
# timeline isn't empty.
|
|
11
|
-
#
|
|
12
|
-
# Add to ~/deploy.sh right after `sudo systemctl restart amazonprimea`:
|
|
13
|
-
#
|
|
14
|
-
# bash scripts/snapshots/maybe-render-on-deploy.sh "$PREV_SHA"
|
|
15
|
-
#
|
|
16
|
-
# where $PREV_SHA is captured BEFORE the git fetch + reset --hard. A simple
|
|
17
|
-
# pattern that works inside deploy.sh:
|
|
18
|
-
#
|
|
19
|
-
# PREV_SHA="$(git rev-parse HEAD)"
|
|
20
|
-
# git fetch origin && git reset --hard origin/main
|
|
21
|
-
# ...
|
|
22
|
-
# bash scripts/snapshots/maybe-render-on-deploy.sh "$PREV_SHA"
|
|
23
|
-
|
|
24
|
-
set -euo pipefail
|
|
25
|
-
|
|
26
|
-
GRAPHICS_PATHS=(
|
|
27
|
-
'public/assets/tiles'
|
|
28
|
-
'public/game/world'
|
|
29
|
-
'src/world/map.js'
|
|
30
|
-
'src/world/terrain.js'
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
34
|
-
cd "$REPO_ROOT"
|
|
35
|
-
|
|
36
|
-
PREV="${1:-}"
|
|
37
|
-
NEW="$(git rev-parse HEAD)"
|
|
38
|
-
SHORT="$(git rev-parse --short=7 HEAD)"
|
|
39
|
-
SUBJECT="$(git log -1 --format=%s HEAD)"
|
|
40
|
-
TIMESTAMP="$(git log -1 --format=%aI HEAD)"
|
|
41
|
-
|
|
42
|
-
if [ -z "$PREV" ] || [ "$PREV" = "$NEW" ]; then
|
|
43
|
-
# First run, or no commits since last deploy.
|
|
44
|
-
if [ -z "$PREV" ]; then
|
|
45
|
-
echo "[snapshot] first run — rendering HEAD snapshot"
|
|
46
|
-
else
|
|
47
|
-
echo "[snapshot] no commits since $PREV — skipping"
|
|
48
|
-
exit 0
|
|
49
|
-
fi
|
|
50
|
-
else
|
|
51
|
-
if git diff --name-only "$PREV..$NEW" -- "${GRAPHICS_PATHS[@]}" | read -r _; then
|
|
52
|
-
echo "[snapshot] graphics changed between $PREV..$SHORT — rendering"
|
|
53
|
-
else
|
|
54
|
-
echo "[snapshot] no graphics changes between $PREV..$SHORT — skipping"
|
|
55
|
-
exit 0
|
|
56
|
-
fi
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
node scripts/snapshots/render-world.mjs \
|
|
60
|
-
--sha "$NEW" \
|
|
61
|
-
--timestamp "$TIMESTAMP" \
|
|
62
|
-
--label "$SUBJECT" \
|
|
63
|
-
--id "$SHORT" \
|
|
64
|
-
--out "modules/status-ui/public/snapshots/$SHORT.png"
|
|
65
|
-
|
|
66
|
-
echo "[snapshot] done — $SHORT"
|
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
// scripts/snapshots/render-pre-art-eras.mjs
|
|
2
|
-
//
|
|
3
|
-
// Captures the two snapshot-worthy moments BEFORE the pixel-art pipeline
|
|
4
|
-
// existed, which the main renderer cannot reach:
|
|
5
|
-
//
|
|
6
|
-
// 1. "Before the container washed ashore" — the hello-world page that
|
|
7
|
-
// lived at amazonprimea.com from 2026-05-08 19:16 (commit faa8c77)
|
|
8
|
-
// until Phase 1 replaced it ~44 minutes later. Black + gold,
|
|
9
|
-
// monospace, three lines of text.
|
|
10
|
-
//
|
|
11
|
-
// 2. "The first playable world" — the Phaser scaffold from Phase 7
|
|
12
|
-
// (commit 0b13bb4, 2026-05-08 20:21). No real tile art yet —
|
|
13
|
-
// procedural-canvas tiles painted in tilePalette.js at boot. We
|
|
14
|
-
// replicate those colors here so the snapshot matches what a player
|
|
15
|
-
// would have seen.
|
|
16
|
-
//
|
|
17
|
-
// Output: writes 2 PNGs into modules/status-ui/public/snapshots/ and inserts both
|
|
18
|
-
// into manifest.json (alongside whatever's already there). Idempotent.
|
|
19
|
-
// (The status surface moved to modules/status-ui/public/ in BV1.R84 / task 1635.)
|
|
20
|
-
|
|
21
|
-
import sharp from 'sharp';
|
|
22
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
23
|
-
import { resolve, dirname } from 'node:path';
|
|
24
|
-
import { fileURLToPath } from 'node:url';
|
|
25
|
-
|
|
26
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
27
|
-
const ROOT = resolve(HERE, '../..');
|
|
28
|
-
const SNAPSHOTS_DIR = resolve(ROOT, 'modules/status-ui/public/snapshots');
|
|
29
|
-
const MANIFEST_PATH = resolve(SNAPSHOTS_DIR, 'manifest.json');
|
|
30
|
-
|
|
31
|
-
const TILE_SIZE = 32;
|
|
32
|
-
const WORLD_COLS = 30;
|
|
33
|
-
const WORLD_ROWS = 18;
|
|
34
|
-
const WIDTH = WORLD_COLS * TILE_SIZE; // 960
|
|
35
|
-
const HEIGHT = WORLD_ROWS * TILE_SIZE; // 576
|
|
36
|
-
|
|
37
|
-
// ---------- Phase 7 procedural-canvas tile generators -----------------------
|
|
38
|
-
//
|
|
39
|
-
// Mirrors the original public/game/world/tilePalette.js from commit 0b13bb4.
|
|
40
|
-
// Each generator returns a 32×32 PNG Buffer. We hand-build them via sharp
|
|
41
|
-
// composites of small colored rectangles, matching the original colors.
|
|
42
|
-
|
|
43
|
-
function rect(x, y, w, h, color) {
|
|
44
|
-
return {
|
|
45
|
-
input: {
|
|
46
|
-
create: {
|
|
47
|
-
width: w,
|
|
48
|
-
height: h,
|
|
49
|
-
channels: 4,
|
|
50
|
-
background: color,
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
left: x,
|
|
54
|
-
top: y,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function speckle(color, count) {
|
|
59
|
-
// Original used (i*7)%32, (i*13)%32 — 1×1 pixels at deterministic spots.
|
|
60
|
-
const pieces = [];
|
|
61
|
-
for (let i = 0; i < count; i++) {
|
|
62
|
-
pieces.push(rect((i * 7) % TILE_SIZE, (i * 13) % TILE_SIZE, 1, 1, color));
|
|
63
|
-
}
|
|
64
|
-
return pieces;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function makeTile(layers) {
|
|
68
|
-
return sharp({
|
|
69
|
-
create: { width: TILE_SIZE, height: TILE_SIZE, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
|
|
70
|
-
})
|
|
71
|
-
.composite(layers)
|
|
72
|
-
.png()
|
|
73
|
-
.toBuffer();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function buildPhase7Tiles() {
|
|
77
|
-
return {
|
|
78
|
-
sand: await makeTile([
|
|
79
|
-
rect(0, 0, TILE_SIZE, TILE_SIZE, '#e6c97a'),
|
|
80
|
-
...speckle('#c8a85a', 18),
|
|
81
|
-
...speckle('#f3dfa1', 10),
|
|
82
|
-
]),
|
|
83
|
-
wet_sand: await makeTile([
|
|
84
|
-
rect(0, 0, TILE_SIZE, TILE_SIZE, '#a88947'),
|
|
85
|
-
...speckle('#7a6230', 22),
|
|
86
|
-
...speckle('#c5a25a', 8),
|
|
87
|
-
]),
|
|
88
|
-
sea: await makeTile([
|
|
89
|
-
rect(0, 0, TILE_SIZE, TILE_SIZE, '#2b6da3'),
|
|
90
|
-
rect(0, 8, TILE_SIZE, 4, '#3884bd'),
|
|
91
|
-
rect(0, 20, TILE_SIZE, 2, '#4ea0d3'),
|
|
92
|
-
...speckle('#cce8f7', 6),
|
|
93
|
-
]),
|
|
94
|
-
grass: await makeTile([
|
|
95
|
-
rect(0, 0, TILE_SIZE, TILE_SIZE, '#5b7a3a'),
|
|
96
|
-
...speckle('#3f5828', 16),
|
|
97
|
-
...speckle('#7f9c50', 8),
|
|
98
|
-
]),
|
|
99
|
-
rock: await makeTile([
|
|
100
|
-
rect(0, 0, TILE_SIZE, TILE_SIZE, '#5b7a3a'),
|
|
101
|
-
rect(6, 6, 22, 22, '#8c8c8c'),
|
|
102
|
-
rect(6, 22, 22, 6, '#6e6e6e'),
|
|
103
|
-
rect(8, 8, 8, 4, '#a8a8a8'),
|
|
104
|
-
]),
|
|
105
|
-
container: await makeTile([
|
|
106
|
-
rect(0, 0, TILE_SIZE, TILE_SIZE, '#8a3a1f'),
|
|
107
|
-
// Original drew vertical ridges every 4 pixels via two thin rects.
|
|
108
|
-
...[0, 4, 8, 12, 16, 20, 24, 28].flatMap((x) => [
|
|
109
|
-
rect(x, 0, 1, TILE_SIZE, '#5e2511'),
|
|
110
|
-
rect(x + 1, 0, 1, TILE_SIZE, '#a8482a'),
|
|
111
|
-
]),
|
|
112
|
-
rect(0, 0, TILE_SIZE, 2, '#3e1a0c'),
|
|
113
|
-
rect(0, TILE_SIZE - 2, TILE_SIZE, 2, '#3e1a0c'),
|
|
114
|
-
]),
|
|
115
|
-
container_door: await makeTile([
|
|
116
|
-
rect(0, 0, TILE_SIZE, TILE_SIZE, '#8a3a1f'),
|
|
117
|
-
rect(6, 4, 20, 26, '#1a0e06'),
|
|
118
|
-
rect(8, 6, 16, 22, '#3a2010'),
|
|
119
|
-
rect(20, 16, 2, 4, '#d9b34a'), // door handle
|
|
120
|
-
]),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Phase 7 map layout (mirrors public/game/world/map.js at commit 0b13bb4).
|
|
125
|
-
function phase7TileAt(x, y) {
|
|
126
|
-
if (y >= WORLD_ROWS - 4) return 'sea';
|
|
127
|
-
if (y === WORLD_ROWS - 5) return 'wet_sand';
|
|
128
|
-
if (y >= 6) return 'sand';
|
|
129
|
-
// Top band: scattered grass + occasional rocks (deterministic).
|
|
130
|
-
if ((x * 37 + y * 53) % 11 === 0) return 'rock';
|
|
131
|
-
return 'grass';
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Procedural player sprite (down-facing) — mirrors playerSprite.js paintDown
|
|
135
|
-
// at commit 0b13bb4. Returned as a 32×32 PNG buffer.
|
|
136
|
-
async function buildPhase7Player() {
|
|
137
|
-
const svg = Buffer.from(`<?xml version="1.0"?>
|
|
138
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">
|
|
139
|
-
<rect x="6" y="28" width="20" height="3" fill="#0e0c08" fill-opacity="0.4"/>
|
|
140
|
-
<rect x="11" y="22" width="4" height="7" fill="#4a3618"/>
|
|
141
|
-
<rect x="17" y="22" width="4" height="7" fill="#4a3618"/>
|
|
142
|
-
<rect x="9" y="14" width="14" height="9" fill="#7a5a2a"/>
|
|
143
|
-
<rect x="9" y="22" width="14" height="1" fill="#c8a85a"/>
|
|
144
|
-
<rect x="11" y="6" width="10" height="8" fill="#e8c39a"/>
|
|
145
|
-
<rect x="12" y="13" width="8" height="1" fill="#b08858"/>
|
|
146
|
-
<rect x="10" y="5" width="12" height="4" fill="#3a2918"/>
|
|
147
|
-
<rect x="10" y="8" width="2" height="2" fill="#3a2918"/>
|
|
148
|
-
<rect x="20" y="8" width="2" height="2" fill="#3a2918"/>
|
|
149
|
-
<rect x="13" y="10" width="2" height="2" fill="#1a1208"/>
|
|
150
|
-
<rect x="17" y="10" width="2" height="2" fill="#1a1208"/>
|
|
151
|
-
<rect x="7" y="16" width="2" height="5" fill="#e8c39a"/>
|
|
152
|
-
<rect x="23" y="16" width="2" height="5" fill="#e8c39a"/>
|
|
153
|
-
</svg>`);
|
|
154
|
-
return sharp(svg).png().toBuffer();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Procedural vendor sprite — mirrors buildVendorSprite at commit 0b13bb4.
|
|
158
|
-
async function buildPhase7Vendor() {
|
|
159
|
-
const svg = Buffer.from(`<?xml version="1.0"?>
|
|
160
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">
|
|
161
|
-
<rect x="8" y="4" width="16" height="26" fill="#3a2914" fill-opacity="0.67"/>
|
|
162
|
-
<rect x="7" y="3" width="18" height="28" fill="#8a6a3a" fill-opacity="0.33"/>
|
|
163
|
-
<rect x="7" y="28" width="18" height="3" fill="#0e0c08" fill-opacity="0.53"/>
|
|
164
|
-
<rect x="9" y="14" width="14" height="14" fill="#3a2814"/>
|
|
165
|
-
<rect x="11" y="16" width="10" height="10" fill="#1f1407"/>
|
|
166
|
-
<rect x="9" y="14" width="14" height="1" fill="#c8a85a"/>
|
|
167
|
-
<rect x="9" y="27" width="14" height="1" fill="#c8a85a"/>
|
|
168
|
-
<rect x="14" y="22" width="4" height="3" fill="#d8b890"/>
|
|
169
|
-
<rect x="11" y="12" width="10" height="5" fill="#d6cfb6"/>
|
|
170
|
-
<rect x="12" y="16" width="8" height="2" fill="#a8a08a"/>
|
|
171
|
-
<rect x="12" y="7" width="8" height="6" fill="#e8c39a"/>
|
|
172
|
-
<rect x="13" y="9" width="2" height="2" fill="#0e0c08"/>
|
|
173
|
-
<rect x="17" y="9" width="2" height="2" fill="#0e0c08"/>
|
|
174
|
-
<rect x="14" y="9" width="1" height="1" fill="#f5e8b8"/>
|
|
175
|
-
<rect x="18" y="9" width="1" height="1" fill="#f5e8b8"/>
|
|
176
|
-
</svg>`);
|
|
177
|
-
return sharp(svg).png().toBuffer();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async function renderPhase7Placeholder(outPath) {
|
|
181
|
-
const tiles = await buildPhase7Tiles();
|
|
182
|
-
const playerPng = await buildPhase7Player();
|
|
183
|
-
const vendorPng = await buildPhase7Vendor();
|
|
184
|
-
|
|
185
|
-
const composites = [];
|
|
186
|
-
for (let y = 0; y < WORLD_ROWS; y++) {
|
|
187
|
-
for (let x = 0; x < WORLD_COLS; x++) {
|
|
188
|
-
const key = phase7TileAt(x, y);
|
|
189
|
-
composites.push({
|
|
190
|
-
input: tiles[key],
|
|
191
|
-
left: x * TILE_SIZE,
|
|
192
|
-
top: y * TILE_SIZE,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Container overlay: 4 wide × 2 tall at (13, 8). Door at (13, 9).
|
|
198
|
-
for (let dy = 0; dy < 2; dy++) {
|
|
199
|
-
for (let dx = 0; dx < 4; dx++) {
|
|
200
|
-
composites.push({
|
|
201
|
-
input: tiles.container,
|
|
202
|
-
left: (13 + dx) * TILE_SIZE,
|
|
203
|
-
top: (8 + dy) * TILE_SIZE,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
composites.push({
|
|
208
|
-
input: tiles.container_door,
|
|
209
|
-
left: 13 * TILE_SIZE,
|
|
210
|
-
top: 9 * TILE_SIZE,
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Vendor at his canonical (13, 9) door cell; player at spawn (10, 10).
|
|
214
|
-
composites.push({ input: vendorPng, left: 13 * TILE_SIZE, top: 9 * TILE_SIZE });
|
|
215
|
-
composites.push({ input: playerPng, left: 10 * TILE_SIZE, top: 10 * TILE_SIZE });
|
|
216
|
-
|
|
217
|
-
await sharp({
|
|
218
|
-
create: { width: WIDTH, height: HEIGHT, channels: 3, background: { r: 0, g: 0, b: 0 } },
|
|
219
|
-
})
|
|
220
|
-
.composite(composites)
|
|
221
|
-
.png({ compressionLevel: 9 })
|
|
222
|
-
.toFile(outPath);
|
|
223
|
-
console.log(`[pre-art] wrote ${outPath} (Phase 7 procedural placeholder)`);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ---------- Hello-world page → PNG ------------------------------------------
|
|
227
|
-
//
|
|
228
|
-
// Mirrors server.js at commit faa8c77 — black bg, gold-tinted serif/mono
|
|
229
|
-
// type, three lines of copy. We render to PNG via sharp's SVG support.
|
|
230
|
-
|
|
231
|
-
async function renderHelloWorld(outPath) {
|
|
232
|
-
// Mirror server.js at commit faa8c77 — original body CSS was:
|
|
233
|
-
// max-width: 640px; margin: 80px auto; padding: 24px;
|
|
234
|
-
// font-family: ui-monospace, "Courier New", monospace;
|
|
235
|
-
// background: #0e0c08; color: #e8d59a; line-height: 1.6;
|
|
236
|
-
// h1 { font-size: 2.4em; letter-spacing: 0.05em; color: #f5e8b8; }
|
|
237
|
-
// .meta { margin-top: 56px; font-size: 0.82em; color: #6b5d3d; }
|
|
238
|
-
//
|
|
239
|
-
// Layout: 640-wide content block centered in viewport, text LEFT-aligned
|
|
240
|
-
// inside it. 80px top margin + 24px padding from the content frame.
|
|
241
|
-
const contentW = 640;
|
|
242
|
-
const x0 = (WIDTH - contentW) / 2 + 24; // left edge of text inside the padded frame
|
|
243
|
-
const baseFs = 16; // browser default 1em
|
|
244
|
-
const h1Fs = baseFs * 2.4; // ~38px
|
|
245
|
-
const bodyFs = baseFs; // 16px
|
|
246
|
-
const metaFs = baseFs * 0.82; // ~13px
|
|
247
|
-
const line = baseFs * 1.6; // 26px (line-height)
|
|
248
|
-
const top = 80 + 24; // body margin-top + padding-top
|
|
249
|
-
const h1Y = top + h1Fs; // baseline of h1
|
|
250
|
-
const gap = h1Fs * 0.4; // h1 margin-bottom
|
|
251
|
-
const p1Y = h1Y + gap + bodyFs;
|
|
252
|
-
const p2Y = p1Y + line;
|
|
253
|
-
const metaY = p2Y + 56 + bodyFs; // .meta has margin-top: 56px
|
|
254
|
-
|
|
255
|
-
const svg = Buffer.from(`<?xml version="1.0" encoding="UTF-8"?>
|
|
256
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}">
|
|
257
|
-
<rect width="${WIDTH}" height="${HEIGHT}" fill="#0e0c08"/>
|
|
258
|
-
<g font-family="ui-monospace, 'Courier New', 'Menlo', monospace">
|
|
259
|
-
<text x="${x0}" y="${h1Y}"
|
|
260
|
-
fill="#f5e8b8" font-size="${h1Fs}"
|
|
261
|
-
letter-spacing="0.05em">Amazonprimea</text>
|
|
262
|
-
<text x="${x0}" y="${p1Y}"
|
|
263
|
-
fill="#e8d59a" font-size="${bodyFs}">The container has not yet washed ashore.</text>
|
|
264
|
-
<text x="${x0}" y="${p2Y}"
|
|
265
|
-
fill="#e8d59a" font-size="${bodyFs}">Check back soon.</text>
|
|
266
|
-
<text x="${x0}" y="${metaY}"
|
|
267
|
-
fill="#6b5d3d" font-size="${metaFs}">— Off the Boats / Hermeslines</text>
|
|
268
|
-
</g>
|
|
269
|
-
</svg>`);
|
|
270
|
-
|
|
271
|
-
await sharp(svg)
|
|
272
|
-
.png({ compressionLevel: 9 })
|
|
273
|
-
.toFile(outPath);
|
|
274
|
-
console.log(`[pre-art] wrote ${outPath} (hello-world page)`);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ---------- manifest -------------------------------------------------------
|
|
278
|
-
|
|
279
|
-
function upsertSnapshot(manifest, entry) {
|
|
280
|
-
if (!Array.isArray(manifest.snapshots)) manifest.snapshots = [];
|
|
281
|
-
const idx = manifest.snapshots.findIndex((s) => s.id === entry.id);
|
|
282
|
-
if (idx >= 0) manifest.snapshots[idx] = { ...manifest.snapshots[idx], ...entry };
|
|
283
|
-
else manifest.snapshots.push(entry);
|
|
284
|
-
manifest.snapshots.sort((a, b) => (a.timestamp || '').localeCompare(b.timestamp || ''));
|
|
285
|
-
manifest.version = 2;
|
|
286
|
-
return manifest;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ---------- main -----------------------------------------------------------
|
|
290
|
-
|
|
291
|
-
const PRE_ART_SNAPSHOTS = [
|
|
292
|
-
{
|
|
293
|
-
id: 'hello-world',
|
|
294
|
-
sha: 'faa8c77e5342b1baa4fc6cbb0a83e7561d303840',
|
|
295
|
-
timestamp: '2026-05-08T19:16:33-04:00',
|
|
296
|
-
label: 'The container has not yet washed ashore',
|
|
297
|
-
image: 'hello-world.png',
|
|
298
|
-
render: renderHelloWorld,
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
id: 'phase7-placeholder',
|
|
302
|
-
sha: '0b13bb44100cead07eb780998c71be6b06f51cc8',
|
|
303
|
-
timestamp: '2026-05-08T20:21:06-04:00',
|
|
304
|
-
label: 'The first playable world — colored tiles, no art yet',
|
|
305
|
-
image: 'phase7-placeholder.png',
|
|
306
|
-
render: renderPhase7Placeholder,
|
|
307
|
-
},
|
|
308
|
-
];
|
|
309
|
-
|
|
310
|
-
mkdirSync(SNAPSHOTS_DIR, { recursive: true });
|
|
311
|
-
|
|
312
|
-
let manifest = { version: 2, snapshots: [] };
|
|
313
|
-
if (existsSync(MANIFEST_PATH)) {
|
|
314
|
-
try { manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8')); } catch { /* ignore */ }
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
for (const snap of PRE_ART_SNAPSHOTS) {
|
|
318
|
-
await snap.render(resolve(SNAPSHOTS_DIR, snap.image));
|
|
319
|
-
upsertSnapshot(manifest, {
|
|
320
|
-
id: snap.id,
|
|
321
|
-
sha: snap.sha,
|
|
322
|
-
timestamp: snap.timestamp,
|
|
323
|
-
label: snap.label,
|
|
324
|
-
image: snap.image,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
|
|
329
|
-
console.log(`[pre-art] manifest now has ${manifest.snapshots.length} entries`);
|