feat(changelog): add release ledger data
Lint and Build Checks / Lint, Test, and Build (push) Failing after 560h18m4s

This commit is contained in:
2026-06-16 11:19:24 -05:00
parent 13aa686fa8
commit d0ac1c5622
41 changed files with 821 additions and 29 deletions
+2
View File
@@ -0,0 +1,2 @@
VITE_LAST_BUILD_DATE=20260616
VITE_LAST_SUCCESSFUL_COMMIT=13aa686
+1
View File
@@ -1 +1,2 @@
npm run version:bump-build
npm run validate:commit
+11 -2
View File
@@ -61,13 +61,15 @@ __tests__/ # Jest tests
## Current App Behavior
- Home menu shows New Game, Saves, Settings, Policies, Credits, and Toolkit.
- Home menu shows New Game, Saves, Settings, Policies, Change Log, Credits, and Toolkit.
- Toolkit is shown only when the resolution setting is `browser`.
- Toolkit is clickable only when the socket status is connected.
- Route state persists through refreshes.
- Settings persist resolution, autosave, mute, and master/music/ambience/speech/SFX volume.
- Browser background music is implemented in `useBackgroundMusic.web.ts`.
- Change Log content is sourced from `src/data/changelog.json`.
- Credits Toolkit data is sourced from `src/data/credits.json` and synced through the socket server.
- Toolkit placeholder datasets live under `src/data/toolkit/` as empty arrays until schemas stabilize.
## Common Tasks
@@ -96,6 +98,13 @@ __tests__/ # Jest tests
4. Extend `src/socket/toolkitSocketClient.ts` and a store under `src/state/` for client state.
5. Keep Toolkit UI read-only unless editable forms are explicitly requested.
### Add or change data content
1. Read `src/data/AGENTS.md` first.
2. Keep app-imported JSON covered by a typed export and runtime validation.
3. Keep Toolkit placeholder resources as arrays in `src/data/toolkit/` until concrete schemas are requested.
4. Add tests for new data shape, ordering, or placeholder expectations.
### Work with assets
- Images: `src/assets/images/`
@@ -136,7 +145,7 @@ npm run start
- Socket changes: run tests plus `npm run dev:toolkit` and check `/health`
- Commit validation: `npm run validate:commit`
Husky runs `npm run validate:commit` on pre-commit.
Husky runs `npm run version:bump-build` and then `npm run validate:commit` on pre-commit. The version bump updates package metadata and public `.env` build fields for `version-yyyymmdd-last7commit` display.
## Commit Message Format
+11
View File
@@ -0,0 +1,11 @@
# Change Log
All notable project deliverables and build bumps are tracked here.
## 0.0.1
- Established the browser and Electron game shell.
- Added persisted settings, route state, Toolkit gating, and socket status.
- Added Toolkit planning surfaces and socket-backed Credits data.
- Added the first-pass gameplay phase scaffold with local saves.
+2 -2
View File
@@ -70,14 +70,14 @@ npm run web:build
npm run electron:build
```
Husky runs `npm run validate:commit` on commit. That command runs lint, Jest, and the Electron build path.
Husky increments the patch build version with `npm run version:bump-build`, writes public build metadata to `.env`, then runs `npm run validate:commit` on commit. Validation runs lint, Jest, and the Electron build path. The menu build label uses `version-yyyymmdd-last7commit`.
## App Structure
```text
App.tsx # Safe area, status bar, viewport, navigator root
src/navigation/ # Route metadata and lightweight app-state navigator
src/screens/ # Home, Settings, Policies, Toolkit, generic pages
src/screens/ # Home, Gameplay, Saves, Settings, Policies, Toolkit, generic pages
src/components/ # Shared React Native UI primitives
src/state/ # Zustand stores and persistence adapters
src/settings/ # Resolution and audio setting definitions
+59
View File
@@ -0,0 +1,59 @@
import {
changelogEntries,
getChangelogEntriesNewestFirst,
isChangelogEntries,
} from '../src/data';
describe('changelog data', () => {
it('provides valid changelog entries', () => {
expect(isChangelogEntries(changelogEntries)).toBe(true);
expect(changelogEntries.length).toBeGreaterThan(0);
expect(changelogEntries[0]).toEqual(
expect.objectContaining({
id: 'build-metadata-changelog-poc',
title: 'Build Metadata and Changelog Scaffold',
}),
);
});
it('sorts changelog entries newest first', () => {
const sortedEntries = getChangelogEntriesNewestFirst([
{
changes: ['Older change'],
createdAt: '2026-06-01',
id: 'older-entry',
summary: 'Older summary',
title: 'Older Entry',
version: '0.0.1',
},
{
changes: ['Newer change'],
createdAt: '2026-06-16',
id: 'newer-entry',
summary: 'Newer summary',
title: 'Newer Entry',
version: '0.0.2',
},
]);
expect(sortedEntries.map(entry => entry.id)).toEqual([
'newer-entry',
'older-entry',
]);
});
it('rejects malformed changelog data', () => {
expect(
isChangelogEntries([
{
changes: 'not-an-array',
createdAt: '2026-06-16',
id: 'bad',
summary: 'Bad entry',
title: 'Bad',
version: '0.0.1',
},
]),
).toBe(false);
});
});
+2
View File
@@ -14,6 +14,7 @@ describe('routeDefinitions', () => {
'newGame',
'saves',
'settings',
'changelog',
'policiesLegal',
'policiesPrivacy',
'policiesTerms',
@@ -49,6 +50,7 @@ describe('routeDefinitions', () => {
'saves',
'settings',
'policies',
'changelog',
'credits',
'toolkit',
]);
+18 -2
View File
@@ -3,6 +3,7 @@ import { ScrollView, Text } from 'react-native';
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
import { AppNavigator } from '../src/navigation/AppNavigator';
import { buildVersionLabel } from '../src/config/buildInfo';
import {
policyRoutes,
primaryMenuRoutes,
@@ -10,6 +11,7 @@ import {
toolkitRoutes,
} from '../src/navigation/routes';
import { defaultVolumeSettings } from '../src/settings/gameSettings';
import { ChangelogScreen } from '../src/screens/ChangelogScreen';
import { GameplayScreen } from '../src/screens/GameplayScreen';
import { HomeScreen } from '../src/screens/HomeScreen';
import { PageScreen } from '../src/screens/PageScreen';
@@ -168,8 +170,9 @@ describe('screen scaffold', () => {
expect(text).toContain('Saves');
expect(text).toContain('Settings');
expect(text).toContain('Policies');
expect(text).toContain('Change Log');
expect(text).toContain('Credits');
expect(text).toContain('Build v0.0.1');
expect(text).toContain(buildVersionLabel);
expect(text).not.toContain('Toolkit');
expect(renderer.root.findByProps({ testID: 'home-build-version' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'home-background' })).toBeTruthy();
@@ -178,6 +181,7 @@ describe('screen scaffold', () => {
expect(renderer.root.findByProps({ testID: 'menu-saves-icon' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'menu-settings-icon' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'menu-policies-icon' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'menu-changelog-icon' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'menu-credits-icon' })).toBeTruthy();
expect(renderer.root.findAllByProps({ testID: 'menu-toolkit' })).toHaveLength(0);
});
@@ -258,6 +262,17 @@ describe('screen scaffold', () => {
expect(renderer.root.findByProps({ testID: 'back-home-button' })).toBeTruthy();
});
it('renders changelog entries newest first', () => {
const renderer = render(<ChangelogScreen goHome={jest.fn()} />);
const text = getRenderedText(renderer);
expect(text).toContain('Build Metadata and Changelog Scaffold');
expect(text).toContain('0.0.1-20260616-13aa686');
expect(text).toContain('Seeded data-backed changelog content');
expect(renderer.root.findByProps({ testID: 'changelog-screen' })).toBeTruthy();
expect(renderer.root.findByProps({ testID: 'back-home-button' })).toBeTruthy();
});
it('renders gameplay phases and moves through the turn scaffold', () => {
act(() => {
useGameplayStore.getState().startNewGame();
@@ -501,7 +516,7 @@ describe('screen scaffold', () => {
expect(text).toContain('Jacob Mathison');
expect(text).toContain('React Native');
expect(text).toContain('Font Library contributors');
expect(text).toContain('Build v0.0.1');
expect(text).toContain(buildVersionLabel);
expect(text).toContain('Bundled fallback credits');
expect(renderer.root.findByProps({ testID: 'toolkit-credits-card' })).toBeTruthy();
expect(
@@ -619,6 +634,7 @@ describe('screen scaffold', () => {
it('does not use scroll containers for fixed landscape screens', () => {
const screens = [
<HomeScreen key="home" navigate={jest.fn()} />,
<ChangelogScreen key="changelog" goHome={jest.fn()} />,
<GameplayScreen key="gameplay" navigate={jest.fn()} />,
<PageScreen key="page" goHome={jest.fn()} routeName="newGame" />,
<PoliciesScreen key="policies" goHome={jest.fn()} navigate={jest.fn()} />,
+36
View File
@@ -0,0 +1,36 @@
const { readFileSync } = require('node:fs');
const path = require('node:path');
const toolkitPlaceholderFiles = [
'audio.json',
'credits.json',
'endings.json',
'factions.json',
'flags.json',
'lore.json',
'map.json',
'opening.json',
'petitioners.json',
'references.json',
'resources.json',
'rulings.json',
'seed.json',
'story.json',
];
describe('toolkit data placeholders', () => {
it('provides an empty array JSON file for every toolkit section', () => {
for (const fileName of toolkitPlaceholderFiles) {
const filePath = path.join(
process.cwd(),
'src',
'data',
'toolkit',
fileName,
);
const content = JSON.parse(readFileSync(filePath, 'utf8'));
expect(content).toEqual([]);
}
});
});
+17 -8
View File
@@ -4,7 +4,7 @@
**Name:** Bone Lord Bob — Throne of the Bone King
**Repo:** `jacob-mathison/blb-throne-of-the-bone-king` (Gitea: `http://100.79.253.19:3000/jacob-mathison/blb-throne-of-the-bone-king`)
**Type:** Cross-platform game shell — React Native UI shared across mobile, web, and desktop
**Status:** Early development / scaffolding (menu shell, settings, navigation)
**Status:** Early development / scaffolding (menu shell, settings, navigation, gameplay phase flow, saves, toolkit data placeholders)
**Machine manifest:** `manifest.llm.json` — targets, task routes, commands, architecture index, known gaps
## Part of
@@ -27,7 +27,7 @@ Primary day-to-day development currently runs through **web** (`npm run web`) an
- **Zustand** ^5.0.14 — app state + persistence
- **Vite** ^8 — web/Electron renderer bundler
- **Electron** ^42 — desktop shell
- **socket.io** / **socket.io-client** ^4.8 — declared; `socket/` not yet implemented
- **socket.io** / **socket.io-client** ^4.8 — Toolkit socket server and client sync
- **Jest** — unit/integration tests
- **ESLint + Prettier** — lint/format
- **Node** >= 22.12.0
@@ -47,13 +47,21 @@ App.tsx
**Navigation** is store-driven, not React Navigation:
- Route catalog: `src/navigation/routes.ts` (`RouteName`, menu groupings)
- Navigator: `src/navigation/AppNavigator.tsx` — reads/writes `useNavigationStore`, animates opacity on route change
- Screens: `src/screens/` (`HomeScreen`, `SettingsScreen`, `PoliciesScreen`, `ToolkitScreen`, `PageScreen`)
- Screens: `src/screens/` (`HomeScreen`, `GameplayScreen`, `SavesScreen`, `ChangelogScreen`, `SettingsScreen`, `PoliciesScreen`, `ToolkitScreen`, `PageScreen`)
**State** lives in Zustand stores under `src/state/`:
- `navigationStore` — current/pending route (persisted as `blb-navigation`)
- `gameSettingsStore` — resolution, volumes, autosave (persisted as `blb-game-settings`)
- `gameplayStore` — active run, day counter, and current gameplay phase (persisted as `blb-gameplay`)
- `saveGameStore` — local save slots for gameplay runs (persisted as `blb-save-games`)
- `createPersistOptions` — shared persist middleware wiring
**Data content** lives under `src/data/`:
- `changelog.json` + `changelog.ts` — app Change Log entries, sorted newest-first in the UI
- `credits.json` + `credits.ts` — Toolkit Credits fallback/source content
- `legal.json` — policy content
- `toolkit/*.json` — empty array placeholders for future Toolkit resources until schemas stabilize
**Settings** constants/helpers: `src/settings/gameSettings.ts`
Resolution presets include phone sizes plus a `browser` (unbounded) mode. Toolkit menu items are only shown when resolution is `browser`.
@@ -97,6 +105,7 @@ blb-throne-of-the-bone-king/
│ └── preload.ts # Preload bridge
├── src/
│ ├── components/ # Reusable UI
│ ├── data/ # Editable JSON content + typed exports
│ ├── navigation/ # Routes + AppNavigator
│ ├── screens/ # Screen-level views
│ ├── settings/ # Game settings constants/helpers
@@ -105,7 +114,7 @@ blb-throne-of-the-bone-king/
├── __tests__/ # Jest test suite
├── socket/ # Planned real-time layer (stub)
├── docs/ # Project docs (stub)
├── wiki/ # Wiki content (stub)
├── wiki/ # Wiki/reference images and loose design assets
├── vite.config.ts # Vite + react-native-web aliases
├── tsconfig.electron.json # Electron main-process TS config
├── metro.config.js # Metro bundler (mobile)
@@ -163,9 +172,9 @@ npm run test # Jest
## Known Gaps
Canonical list lives in `manifest.llm.json` → `knownGaps`. Current highlights:
- `socket/` is a stub (socket.io deps declared, not wired)
- `docs/` and `wiki/` are placeholder stubs
- `AGENTS.md` and `README.md` are stale relative to web/Electron targets
- `docs/` is still a placeholder stub
- `wiki/` currently stores reference images and loose wiki/design assets
- Toolkit placeholder JSON files are empty arrays until schemas are requested
## Notes for LLMs
- **Read `manifest.llm.json` for task routing** — use `taskRoutes` to pick the narrowest file set
@@ -175,6 +184,6 @@ Canonical list lives in `manifest.llm.json` → `knownGaps`. Current highlights:
- **Metro** bundles mobile; **Vite** bundles web/Electron renderer — not interchangeable
- **Navigation is Zustand-based** — do not add React Navigation unless explicitly requested
- **Persistence is platform-split** — extend `persistStorage.ts` / `persistStorage.web.ts`, not stores directly
- `socket/` is a stub — socket.io is installed but no server/client wiring yet
- `src/data/AGENTS.md` contains extra rules for JSON content and Toolkit placeholders
- iOS Pods and `node_modules/` are not committed; run `pod install` after pulling native dep changes
- `dist/` build artifacts are gitignored
+54 -4
View File
@@ -13,7 +13,7 @@
},
"universe": "Bone Lord Bob (BLB) - Mathison Projects Inc.",
"status": "early-development",
"summary": "Menu shell with persisted route navigation, settings, policies, credits, background music, browser/Electron rendering, and a browser-only socket-gated Toolkit for volatile gameplay/content systems."
"summary": "Menu shell with persisted route navigation, settings, policies, changelog, credits, gameplay phase scaffolding, local saves, build metadata display, background music, browser/Electron rendering, and a browser-only socket-gated Toolkit for volatile gameplay/content systems."
},
"principles": [
"Start with llm.txt for human-readable orientation.",
@@ -84,6 +84,11 @@
"path": "socket/README.md",
"type": "toolkit-socket-notes",
"purpose": "Socket folder notes."
},
{
"path": "src/data/AGENTS.md",
"type": "data-instructions",
"purpose": "Rules for editable JSON content and Toolkit placeholder resources."
}
],
"targets": [
@@ -149,7 +154,7 @@
"Sass ^1.101.0",
"Jest",
"ESLint + Prettier",
"Husky pre-commit validation",
"Husky pre-commit build version bump and validation",
"Node >= 22.12.0"
],
"scripts": {
@@ -166,7 +171,8 @@
"electron:package": "npm run electron:build && electron-builder",
"lint": "eslint .",
"test": "jest",
"validate:commit": "npm run lint && npm run test -- --runInBand && npm run electron:build"
"validate:commit": "npm run lint && npm run test -- --runInBand && npm run electron:build",
"version:bump-build": "node tools/bump-build-version.mjs"
},
"architecture": {
"rootComponent": "App.tsx",
@@ -195,6 +201,16 @@
"persistKey": "blb-game-settings",
"purpose": "Resolution, autosave, mute, and audio volume settings."
},
{
"file": "src/state/gameplayStore.ts",
"persistKey": "blb-gameplay",
"purpose": "Active gameplay run, day counter, phase state, and phase navigation."
},
{
"file": "src/state/saveGameStore.ts",
"persistKey": "blb-save-games",
"purpose": "Local save slots for gameplay runs."
},
{
"file": "src/state/toolkitSocketStore.ts",
"purpose": "Toolkit socket connection status."
@@ -229,6 +245,9 @@
},
"screens": [
"src/screens/HomeScreen.tsx",
"src/screens/GameplayScreen.tsx",
"src/screens/SavesScreen.tsx",
"src/screens/ChangelogScreen.tsx",
"src/screens/SettingsScreen.tsx",
"src/screens/PoliciesScreen.tsx",
"src/screens/ToolkitScreen.tsx",
@@ -248,12 +267,37 @@
"favicon": "src/assets/images/favicon.svg"
},
"data": {
"changelog": {
"json": "src/data/changelog.json",
"typedExport": "src/data/changelog.ts",
"sortOrder": "newest-created-first"
},
"credits": {
"json": "src/data/credits.json",
"typedExport": "src/data/credits.ts"
},
"legal": {
"json": "src/data/legal.json"
},
"toolkitPlaceholders": {
"directory": "src/data/toolkit/",
"shape": "empty arrays until schemas stabilize",
"files": [
"audio.json",
"credits.json",
"endings.json",
"factions.json",
"flags.json",
"lore.json",
"map.json",
"opening.json",
"petitioners.json",
"references.json",
"resources.json",
"rulings.json",
"seed.json",
"story.json"
]
}
},
"audio": {
@@ -399,6 +443,7 @@
"read": [
"llm.txt",
"manifest.llm.json",
"src/data/AGENTS.md",
"src/data/",
"socket/contentStore.cjs"
],
@@ -555,12 +600,17 @@
{
"command": "npm run validate:commit",
"purpose": "Pre-commit safety check: lint, Jest, and Electron build."
},
{
"command": "npm run version:bump-build",
"purpose": "Increment the package patch version, update public .env build metadata, and stage package/env metadata before a commit."
}
],
"knownGaps": [
"docs/ and wiki/ are placeholder README stubs.",
"docs/ is still a placeholder README stub.",
"No JSON Schema validator exists yet for manifest.llm.json.",
"Toolkit content panels are read-only scaffolds except for socket/store save plumbing.",
"Toolkit placeholder JSON files are intentionally empty arrays until content schemas are requested.",
"Native android/ and ios/ folders are not currently present in this checkout.",
"No database exists yet; editable content is intentionally JSON-backed until schemas stabilize."
],
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "BlbThroneOfTheBoneKing",
"version": "0.0.1",
"version": "0.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "BlbThroneOfTheBoneKing",
"version": "0.0.1",
"version": "0.0.2",
"dependencies": {
"@react-native-async-storage/async-storage": "^3.1.1",
"@react-native/new-app-screen": "0.85.3",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "BlbThroneOfTheBoneKing",
"version": "0.0.1",
"version": "0.0.2",
"private": true,
"main": "dist/electron/main.js",
"scripts": {
@@ -19,6 +19,7 @@
"start": "react-native start",
"test": "jest",
"validate:commit": "npm run lint && npm run test -- --runInBand && npm run electron:build",
"version:bump-build": "node tools/bump-build-version.mjs",
"web": "vite --host 127.0.0.1",
"web:build": "vite build",
"web:preview": "vite preview --host 127.0.0.1"
+3 -3
View File
@@ -71,10 +71,10 @@ const styles = StyleSheet.create({
borderColor: '#4d4432',
borderRadius: 8,
borderWidth: 1,
minHeight: 42,
minHeight: 38,
justifyContent: 'center',
paddingHorizontal: 14,
paddingVertical: 9,
paddingHorizontal: 12,
paddingVertical: 7,
width: '100%',
},
buttonPressed: {
+6 -2
View File
@@ -1,3 +1,7 @@
export const appVersion = '0.0.1';
import packageJson from '../../package.json';
export const buildVersionLabel = `Build v${appVersion}`;
export const appVersion = packageJson.version;
export const buildIdentifier = appVersion;
export const buildVersionLabel = `Build ${buildIdentifier}`;
+22 -2
View File
@@ -1,10 +1,30 @@
/// <reference types="vite/client" />
import packageJson from '../../package.json';
const viteAppVersion = import.meta.env.VITE_APP_VERSION;
const viteLastBuildDate = import.meta.env.VITE_LAST_BUILD_DATE;
const viteLastSuccessfulCommit = import.meta.env.VITE_LAST_SUCCESSFUL_COMMIT;
export const appVersion =
typeof viteAppVersion === 'string' && viteAppVersion.length > 0
? viteAppVersion
: '0.0.1';
: packageJson.version;
export const buildVersionLabel = `Build v${appVersion}`;
export const buildDate =
typeof viteLastBuildDate === 'string' && viteLastBuildDate.length > 0
? viteLastBuildDate
: null;
export const lastSuccessfulCommit =
typeof viteLastSuccessfulCommit === 'string' &&
viteLastSuccessfulCommit.length > 0
? viteLastSuccessfulCommit
: null;
export const buildIdentifier =
buildDate == null || lastSuccessfulCommit == null
? appVersion
: `${appVersion}-${buildDate}-${lastSuccessfulCommit}`;
export const buildVersionLabel = `Build ${buildIdentifier}`;
+19
View File
@@ -0,0 +1,19 @@
# AGENTS.md - Data Folder
This folder contains source-of-truth JSON content used by the app and Toolkit.
## Rules
- Keep JSON valid, stable, and reviewable.
- Add TypeScript exports and runtime validation when app code imports JSON directly.
- Keep Toolkit placeholder files in `toolkit/` as arrays until schemas stabilize.
- Do not put secrets, credentials, local machine paths, or private notes in this folder.
- Prefer additive schema changes. If a shape changes, update tests and any socket/content-store validation.
## Current Content
- `changelog.json`: app-facing changelog entries, newest records shown first in the Change Log screen.
- `credits.json`: Toolkit Credits source content.
- `legal.json`: policy/legal content.
- `toolkit/*.json`: empty array placeholders for future Toolkit authoring resources.
+14
View File
@@ -0,0 +1,14 @@
[
{
"id": "build-metadata-changelog-poc",
"version": "0.0.1-20260616-13aa686",
"createdAt": "2026-06-16",
"title": "Build Metadata and Changelog Scaffold",
"summary": "Proof-of-concept changelog record for tracking deliverables alongside the visible build identifier.",
"changes": [
"Added pre-commit build metadata for version, build date, and last successful commit.",
"Added a Change Log route to the main menu.",
"Seeded data-backed changelog content for future release notes."
]
}
]
+45
View File
@@ -0,0 +1,45 @@
import changelogJson from './changelog.json';
export type ChangelogEntry = Readonly<{
changes: string[];
createdAt: string;
id: string;
summary: string;
title: string;
version: string;
}>;
function isRecord(value: unknown): value is Record<string, unknown> {
return value != null && typeof value === 'object' && !Array.isArray(value);
}
function isChangelogEntry(value: unknown): value is ChangelogEntry {
return (
isRecord(value) &&
typeof value.id === 'string' &&
typeof value.version === 'string' &&
typeof value.createdAt === 'string' &&
typeof value.title === 'string' &&
typeof value.summary === 'string' &&
Array.isArray(value.changes) &&
value.changes.every(change => typeof change === 'string')
);
}
export function isChangelogEntries(value: unknown): value is ChangelogEntry[] {
return Array.isArray(value) && value.every(isChangelogEntry);
}
if (!isChangelogEntries(changelogJson)) {
throw new Error('src/data/changelog.json does not match ChangelogEntry[].');
}
export const changelogEntries: ChangelogEntry[] = changelogJson;
export function getChangelogEntriesNewestFirst(
entries: readonly ChangelogEntry[] = changelogEntries,
) {
return [...entries].sort((firstEntry, secondEntry) =>
secondEntry.createdAt.localeCompare(firstEntry.createdAt),
);
}
+6
View File
@@ -1,3 +1,9 @@
export {
changelogEntries,
getChangelogEntriesNewestFirst,
isChangelogEntries,
type ChangelogEntry,
} from './changelog';
export {
fallbackCreditsContent,
isCreditsContent,
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
[]
+4
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef } from 'react';
import { Animated, StyleSheet } from 'react-native';
import { useToolkitSocketConnection } from '../hooks/useToolkitSocketConnection';
import { ChangelogScreen } from '../screens/ChangelogScreen';
import { GameplayScreen } from '../screens/GameplayScreen';
import { HomeScreen } from '../screens/HomeScreen';
import { PageScreen } from '../screens/PageScreen';
@@ -112,6 +113,9 @@ function renderRoute(
if (routeName === 'home') {
return <HomeScreen navigate={navigate} />;
}
if (routeName === 'changelog') {
return <ChangelogScreen goHome={goHome} />;
}
if (routeName === 'gameplay') {
return <GameplayScreen navigate={navigate} />;
}
+6
View File
@@ -19,6 +19,10 @@ export const routeDefinitions = {
label: 'Policies',
title: 'Policies',
},
changelog: {
label: 'Change Log',
title: 'Change Log',
},
policiesLegal: {
label: 'Legal',
title: 'Legal',
@@ -112,6 +116,7 @@ export const primaryMenuRoutes = [
'saves',
'settings',
'policies',
'changelog',
'credits',
'toolkit',
] as const satisfies readonly RouteName[];
@@ -167,6 +172,7 @@ export const requiredPageRoutes = [
'newGame',
'saves',
'settings',
'changelog',
'policiesLegal',
'policiesPrivacy',
'policiesTerms',
+344
View File
@@ -0,0 +1,344 @@
import { StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MenuButton } from '../components/MenuButton';
import {
getChangelogEntriesNewestFirst,
type ChangelogEntry,
} from '../data';
import { defaultTextStyle, displayTextStyle } from '../styles/typography';
type ChangelogScreenProps = Readonly<{
goHome: () => void;
}>;
const visibleChangeLimit = 4;
export function ChangelogScreen({ goHome }: ChangelogScreenProps) {
const safeAreaInsets = useSafeAreaInsets();
const entries = getChangelogEntriesNewestFirst();
const latestEntry = entries[0];
return (
<View
style={[
styles.container,
{
paddingBottom: safeAreaInsets.bottom + 18,
paddingLeft: safeAreaInsets.left + 22,
paddingRight: safeAreaInsets.right + 22,
paddingTop: safeAreaInsets.top + 18,
},
]}
testID="changelog-screen">
<View style={styles.leftRail}>
<Text style={styles.eyebrow}>Bone Lord Bob</Text>
<Text style={styles.title}>Change Log</Text>
<Text style={styles.description}>
Newest release notes are pinned at the top, with older deliverables
settling toward the bottom.
</Text>
{latestEntry == null ? null : (
<View style={styles.latestPanel} testID="changelog-latest-panel">
<Text style={styles.panelLabel}>Latest Build</Text>
<Text style={styles.latestVersion}>{latestEntry.version}</Text>
<Text style={styles.latestDate}>{latestEntry.createdAt}</Text>
</View>
)}
<View style={styles.homeAction}>
<MenuButton label="Home" onPress={goHome} testID="back-home-button" />
</View>
</View>
<View style={styles.releaseBoard}>
<View style={styles.boardHeader}>
<View>
<Text style={styles.boardKicker}>Release Ledger</Text>
<Text style={styles.boardTitle}>Deliverable History</Text>
</View>
<Text style={styles.entryCount}>
{entries.length} {entries.length === 1 ? 'record' : 'records'}
</Text>
</View>
<View style={styles.timeline} testID="changelog-entry-list">
{entries.map((entry, index) => (
<ChangelogCard
entry={entry}
index={index}
key={entry.id}
latest={index === 0}
/>
))}
</View>
</View>
</View>
);
}
type ChangelogCardProps = Readonly<{
entry: ChangelogEntry;
index: number;
latest: boolean;
}>;
function ChangelogCard({ entry, index, latest }: ChangelogCardProps) {
return (
<View style={styles.timelineRow} testID={`changelog-entry-${entry.id}`}>
<View style={styles.timelineMarkerColumn}>
<View style={[styles.marker, latest ? styles.markerLatest : null]}>
<Text style={styles.markerText}>{index + 1}</Text>
</View>
<View style={styles.markerLine} />
</View>
<View style={[styles.entryCard, latest ? styles.entryCardLatest : null]}>
<View style={styles.entryHeader}>
<View style={styles.versionPill}>
<Text style={styles.version}>{entry.version}</Text>
</View>
<Text style={styles.date}>{entry.createdAt}</Text>
</View>
<Text style={styles.entryTitle}>{entry.title}</Text>
<Text style={styles.summary}>{entry.summary}</Text>
<View style={styles.changeList}>
{entry.changes.slice(0, visibleChangeLimit).map(change => (
<View key={change} style={styles.changeRow}>
<Text style={styles.changeBullet}></Text>
<Text style={styles.changeItem}>{change}</Text>
</View>
))}
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
boardHeader: {
alignItems: 'center',
borderBottomColor: '#37342d',
borderBottomWidth: 1,
flexDirection: 'row',
justifyContent: 'space-between',
paddingBottom: 10,
},
boardKicker: {
...defaultTextStyle,
color: '#d6b25e',
fontSize: 10,
fontWeight: '800',
letterSpacing: 0,
textTransform: 'uppercase',
},
boardTitle: {
...displayTextStyle,
color: '#f6f1e7',
fontSize: 18,
letterSpacing: 0,
lineHeight: 22,
},
changeBullet: {
color: '#d6b25e',
fontSize: 9,
lineHeight: 15,
},
changeItem: {
...defaultTextStyle,
color: '#c9c1b3',
flex: 1,
fontSize: 11,
lineHeight: 15,
},
changeList: {
gap: 5,
},
changeRow: {
flexDirection: 'row',
gap: 7,
},
container: {
backgroundColor: '#111315',
flex: 1,
flexDirection: 'row',
gap: 18,
overflow: 'hidden',
},
date: {
...defaultTextStyle,
color: '#80796c',
fontSize: 11,
fontWeight: '800',
letterSpacing: 0,
},
description: {
...defaultTextStyle,
color: '#c9c1b3',
fontSize: 14,
lineHeight: 20,
},
entryCard: {
backgroundColor: '#191b1f',
borderColor: '#37342d',
borderRadius: 8,
borderWidth: 1,
flex: 1,
gap: 8,
minWidth: 0,
padding: 12,
},
entryCardLatest: {
backgroundColor: '#1e1d1a',
borderColor: '#d6b25e',
},
entryCount: {
...defaultTextStyle,
backgroundColor: '#24272b',
borderColor: '#4d4432',
borderRadius: 6,
borderWidth: 1,
color: '#f5d98d',
fontSize: 11,
fontWeight: '800',
letterSpacing: 0,
paddingHorizontal: 8,
paddingVertical: 5,
textTransform: 'uppercase',
},
entryHeader: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
},
entryTitle: {
...displayTextStyle,
color: '#f6f1e7',
fontSize: 17,
letterSpacing: 0,
lineHeight: 21,
},
eyebrow: {
...defaultTextStyle,
color: '#d6b25e',
fontSize: 12,
fontWeight: '800',
letterSpacing: 0,
textTransform: 'uppercase',
},
homeAction: {
marginTop: 'auto',
width: 164,
},
latestDate: {
...defaultTextStyle,
color: '#80796c',
fontSize: 11,
fontWeight: '700',
letterSpacing: 0,
},
latestPanel: {
backgroundColor: '#191b1f',
borderColor: '#4d4432',
borderRadius: 8,
borderWidth: 1,
gap: 4,
padding: 12,
},
latestVersion: {
...defaultTextStyle,
color: '#f5d98d',
fontSize: 11,
fontWeight: '800',
letterSpacing: 0,
lineHeight: 15,
},
leftRail: {
gap: 12,
justifyContent: 'flex-start',
width: 232,
zIndex: 1,
},
marker: {
alignItems: 'center',
backgroundColor: '#24272b',
borderColor: '#4d4432',
borderRadius: 14,
borderWidth: 1,
height: 28,
justifyContent: 'center',
width: 28,
},
markerLatest: {
backgroundColor: '#3a2918',
borderColor: '#d6b25e',
},
markerLine: {
backgroundColor: '#37342d',
flex: 1,
marginVertical: 5,
width: 1,
},
markerText: {
...defaultTextStyle,
color: '#f5d98d',
fontSize: 11,
fontWeight: '800',
letterSpacing: 0,
},
panelLabel: {
...defaultTextStyle,
color: '#d6b25e',
fontSize: 10,
fontWeight: '800',
letterSpacing: 0,
textTransform: 'uppercase',
},
releaseBoard: {
backgroundColor: '#15171a',
borderColor: '#37342d',
borderRadius: 8,
borderWidth: 1,
flex: 1,
gap: 12,
padding: 14,
zIndex: 1,
},
summary: {
...defaultTextStyle,
color: '#d6b25e',
fontSize: 12,
lineHeight: 16,
},
timeline: {
flex: 1,
gap: 10,
justifyContent: 'flex-start',
},
timelineMarkerColumn: {
alignItems: 'center',
width: 28,
},
timelineRow: {
flexDirection: 'row',
gap: 10,
height: 170,
},
title: {
...displayTextStyle,
color: '#f6f1e7',
fontSize: 34,
letterSpacing: 0,
lineHeight: 40,
},
version: {
...displayTextStyle,
color: '#f5d98d',
fontSize: 12,
letterSpacing: 0,
},
versionPill: {
backgroundColor: '#3a2918',
borderColor: '#d6b25e',
borderRadius: 6,
borderWidth: 1,
paddingHorizontal: 8,
paddingVertical: 4,
},
});
+2 -1
View File
@@ -24,6 +24,7 @@ type HomeScreenProps = Readonly<{
}>;
const menuIcons: Record<PrimaryMenuRouteName, string> = {
changelog: '✎',
credits: '★',
newGame: '▶',
policies: '§',
@@ -155,7 +156,7 @@ const styles = StyleSheet.create({
textTransform: 'uppercase',
},
menu: {
gap: 8,
gap: 6,
width: 156,
},
menuBackgroundImage: {
+1
View File
@@ -12,6 +12,7 @@ import { defaultTextStyle } from '../styles/typography';
type PageRouteName = Exclude<
RouteName,
| 'gameplay'
| 'changelog'
| 'home'
| 'policies'
| 'saves'
+118
View File
@@ -0,0 +1,118 @@
import { readFile, writeFile } from 'node:fs/promises';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const envPath = new URL('../.env', import.meta.url);
const packagePath = new URL('../package.json', import.meta.url);
const packageLockPath = new URL('../package-lock.json', import.meta.url);
function getNextPatchVersion(version) {
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
if (match == null) {
throw new Error(`Expected package version to use #.#.# format, got "${version}".`);
}
const [, major, minor, patch] = match;
return `${major}.${minor}.${Number(patch) + 1}`;
}
async function readJson(filePath) {
return JSON.parse(await readFile(filePath, 'utf8'));
}
async function writeJson(filePath, json) {
await writeFile(filePath, `${JSON.stringify(json, null, 2)}\n`);
}
async function getLastSuccessfulCommit() {
const { stdout } = await execFileAsync('git', ['rev-parse', '--short=7', 'HEAD']);
return stdout.trim();
}
function getBuildDate() {
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${now.getFullYear()}${month}${day}`;
}
async function readEnvLines() {
try {
return (await readFile(envPath, 'utf8')).split(/\r?\n/);
} catch (error) {
if (error != null && typeof error === 'object' && 'code' in error) {
const nodeError = error;
if (nodeError.code === 'ENOENT') {
return [];
}
}
throw error;
}
}
async function writeEnvValues(values) {
const lines = await readEnvLines();
const remainingKeys = new Set(Object.keys(values));
const nextLines = lines
.filter((line, index) => line.length > 0 || index < lines.length - 1)
.map(line => {
const [key] = line.split('=', 1);
if (!remainingKeys.has(key)) {
return line;
}
remainingKeys.delete(key);
return `${key}=${values[key]}`;
});
for (const key of remainingKeys) {
nextLines.push(`${key}=${values[key]}`);
}
await writeFile(envPath, `${nextLines.join('\n')}\n`);
}
async function main() {
const packageJson = await readJson(packagePath);
const nextVersion = getNextPatchVersion(packageJson.version);
const buildDate = getBuildDate();
const lastSuccessfulCommit = await getLastSuccessfulCommit();
packageJson.version = nextVersion;
await writeJson(packagePath, packageJson);
const packageLockJson = await readJson(packageLockPath);
packageLockJson.version = nextVersion;
if (
packageLockJson.packages != null &&
packageLockJson.packages[''] != null
) {
packageLockJson.packages[''].version = nextVersion;
}
await writeJson(packageLockPath, packageLockJson);
await writeEnvValues({
VITE_LAST_BUILD_DATE: buildDate,
VITE_LAST_SUCCESSFUL_COMMIT: lastSuccessfulCommit,
});
await execFileAsync('git', ['add', '.env', 'package.json', 'package-lock.json']);
console.log(
`Updated build version to ${nextVersion}-${buildDate}-${lastSuccessfulCommit}`,
);
}
main().catch(error => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});

Before

Width:  |  Height:  |  Size: 5.1 MiB

After

Width:  |  Height:  |  Size: 5.1 MiB