From d0ac1c5622f7f846d656f75890cfaed58e2d27ac Mon Sep 17 00:00:00 2001 From: Jacob Mathison Date: Tue, 16 Jun 2026 11:19:24 -0500 Subject: [PATCH] feat(changelog): add release ledger data --- .env | 2 + .husky/pre-commit | 1 + AGENTS.md | 13 +- CHANGELOG.md | 11 + README.md | 4 +- __tests__/changelogData.test.ts | 59 +++ __tests__/navigationRoutes.test.ts | 2 + __tests__/screenScaffold.test.tsx | 20 +- __tests__/toolkitDataPlaceholders.test.js | 36 ++ llm.txt | 25 +- manifest.llm.json | 58 ++- package-lock.json | 4 +- package.json | 3 +- src/components/MenuButton.tsx | 6 +- src/config/buildInfo.ts | 8 +- src/config/buildInfo.web.ts | 24 +- src/data/AGENTS.md | 19 + src/data/changelog.json | 14 + src/data/changelog.ts | 45 +++ src/data/index.ts | 6 + src/data/toolkit/audio.json | 1 + src/data/toolkit/credits.json | 1 + src/data/toolkit/endings.json | 1 + src/data/toolkit/factions.json | 1 + src/data/toolkit/flags.json | 1 + src/data/toolkit/lore.json | 1 + src/data/toolkit/map.json | 1 + src/data/toolkit/opening.json | 1 + src/data/toolkit/petitioners.json | 1 + src/data/toolkit/references.json | 1 + src/data/toolkit/resources.json | 1 + src/data/toolkit/rulings.json | 1 + src/data/toolkit/seed.json | 1 + src/data/toolkit/story.json | 1 + src/navigation/AppNavigator.tsx | 4 + src/navigation/routes.ts | 6 + src/screens/ChangelogScreen.tsx | 344 ++++++++++++++++++ src/screens/HomeScreen.tsx | 3 +- src/screens/PageScreen.tsx | 1 + tools/bump-build-version.mjs | 118 ++++++ .../Throne of the Bone King.png | Bin 41 files changed, 821 insertions(+), 29 deletions(-) create mode 100644 .env create mode 100644 CHANGELOG.md create mode 100644 __tests__/changelogData.test.ts create mode 100644 __tests__/toolkitDataPlaceholders.test.js create mode 100644 src/data/AGENTS.md create mode 100644 src/data/changelog.json create mode 100644 src/data/changelog.ts create mode 100644 src/data/toolkit/audio.json create mode 100644 src/data/toolkit/credits.json create mode 100644 src/data/toolkit/endings.json create mode 100644 src/data/toolkit/factions.json create mode 100644 src/data/toolkit/flags.json create mode 100644 src/data/toolkit/lore.json create mode 100644 src/data/toolkit/map.json create mode 100644 src/data/toolkit/opening.json create mode 100644 src/data/toolkit/petitioners.json create mode 100644 src/data/toolkit/references.json create mode 100644 src/data/toolkit/resources.json create mode 100644 src/data/toolkit/rulings.json create mode 100644 src/data/toolkit/seed.json create mode 100644 src/data/toolkit/story.json create mode 100644 src/screens/ChangelogScreen.tsx create mode 100644 tools/bump-build-version.mjs rename {Game Assets 1 => wiki/Game Assets 1}/Throne of the Bone King.png (100%) diff --git a/.env b/.env new file mode 100644 index 0000000..5c1a29f --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +VITE_LAST_BUILD_DATE=20260616 +VITE_LAST_SUCCESSFUL_COMMIT=13aa686 diff --git a/.husky/pre-commit b/.husky/pre-commit index c2cc48f..9da317e 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,2 @@ +npm run version:bump-build npm run validate:commit diff --git a/AGENTS.md b/AGENTS.md index f71278d..0d34c23 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..57ec14e --- /dev/null +++ b/CHANGELOG.md @@ -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. + diff --git a/README.md b/README.md index 11661e3..5af1318 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/__tests__/changelogData.test.ts b/__tests__/changelogData.test.ts new file mode 100644 index 0000000..894360b --- /dev/null +++ b/__tests__/changelogData.test.ts @@ -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); + }); +}); diff --git a/__tests__/navigationRoutes.test.ts b/__tests__/navigationRoutes.test.ts index e1baf19..322d5bc 100644 --- a/__tests__/navigationRoutes.test.ts +++ b/__tests__/navigationRoutes.test.ts @@ -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', ]); diff --git a/__tests__/screenScaffold.test.tsx b/__tests__/screenScaffold.test.tsx index 628b4ef..b76de49 100644 --- a/__tests__/screenScaffold.test.tsx +++ b/__tests__/screenScaffold.test.tsx @@ -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(); + 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 = [ , + , , , , diff --git a/__tests__/toolkitDataPlaceholders.test.js b/__tests__/toolkitDataPlaceholders.test.js new file mode 100644 index 0000000..8cd3998 --- /dev/null +++ b/__tests__/toolkitDataPlaceholders.test.js @@ -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([]); + } + }); +}); diff --git a/llm.txt b/llm.txt index d5819f5..6982944 100644 --- a/llm.txt +++ b/llm.txt @@ -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 diff --git a/manifest.llm.json b/manifest.llm.json index 29aff09..9d918bf 100644 --- a/manifest.llm.json +++ b/manifest.llm.json @@ -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." ], diff --git a/package-lock.json b/package-lock.json index 86419e5..5009666 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 19f0952..e397b7d 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/MenuButton.tsx b/src/components/MenuButton.tsx index 14cb652..db1b86c 100644 --- a/src/components/MenuButton.tsx +++ b/src/components/MenuButton.tsx @@ -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: { diff --git a/src/config/buildInfo.ts b/src/config/buildInfo.ts index 3b1f868..3ac31c9 100644 --- a/src/config/buildInfo.ts +++ b/src/config/buildInfo.ts @@ -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}`; diff --git a/src/config/buildInfo.web.ts b/src/config/buildInfo.web.ts index e816cce..6065cd2 100644 --- a/src/config/buildInfo.web.ts +++ b/src/config/buildInfo.web.ts @@ -1,10 +1,30 @@ /// +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}`; diff --git a/src/data/AGENTS.md b/src/data/AGENTS.md new file mode 100644 index 0000000..55524cf --- /dev/null +++ b/src/data/AGENTS.md @@ -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. + diff --git a/src/data/changelog.json b/src/data/changelog.json new file mode 100644 index 0000000..e2c59f8 --- /dev/null +++ b/src/data/changelog.json @@ -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." + ] + } +] diff --git a/src/data/changelog.ts b/src/data/changelog.ts new file mode 100644 index 0000000..4e17063 --- /dev/null +++ b/src/data/changelog.ts @@ -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 { + 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), + ); +} diff --git a/src/data/index.ts b/src/data/index.ts index 9e9b1c9..982b1c3 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1,3 +1,9 @@ +export { + changelogEntries, + getChangelogEntriesNewestFirst, + isChangelogEntries, + type ChangelogEntry, +} from './changelog'; export { fallbackCreditsContent, isCreditsContent, diff --git a/src/data/toolkit/audio.json b/src/data/toolkit/audio.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/audio.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/credits.json b/src/data/toolkit/credits.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/credits.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/endings.json b/src/data/toolkit/endings.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/endings.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/factions.json b/src/data/toolkit/factions.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/factions.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/flags.json b/src/data/toolkit/flags.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/flags.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/lore.json b/src/data/toolkit/lore.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/lore.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/map.json b/src/data/toolkit/map.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/map.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/opening.json b/src/data/toolkit/opening.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/opening.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/petitioners.json b/src/data/toolkit/petitioners.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/petitioners.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/references.json b/src/data/toolkit/references.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/references.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/resources.json b/src/data/toolkit/resources.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/resources.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/rulings.json b/src/data/toolkit/rulings.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/rulings.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/seed.json b/src/data/toolkit/seed.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/seed.json @@ -0,0 +1 @@ +[] diff --git a/src/data/toolkit/story.json b/src/data/toolkit/story.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/data/toolkit/story.json @@ -0,0 +1 @@ +[] diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index c59541c..1748852 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -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 ; } + if (routeName === 'changelog') { + return ; + } if (routeName === 'gameplay') { return ; } diff --git a/src/navigation/routes.ts b/src/navigation/routes.ts index 4855d67..2712c1d 100644 --- a/src/navigation/routes.ts +++ b/src/navigation/routes.ts @@ -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', diff --git a/src/screens/ChangelogScreen.tsx b/src/screens/ChangelogScreen.tsx new file mode 100644 index 0000000..d8a7e12 --- /dev/null +++ b/src/screens/ChangelogScreen.tsx @@ -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 ( + + + Bone Lord Bob + Change Log + + Newest release notes are pinned at the top, with older deliverables + settling toward the bottom. + + {latestEntry == null ? null : ( + + Latest Build + {latestEntry.version} + {latestEntry.createdAt} + + )} + + + + + + + + Release Ledger + Deliverable History + + + {entries.length} {entries.length === 1 ? 'record' : 'records'} + + + + {entries.map((entry, index) => ( + + ))} + + + + ); +} + +type ChangelogCardProps = Readonly<{ + entry: ChangelogEntry; + index: number; + latest: boolean; +}>; + +function ChangelogCard({ entry, index, latest }: ChangelogCardProps) { + return ( + + + + {index + 1} + + + + + + + {entry.version} + + {entry.createdAt} + + {entry.title} + {entry.summary} + + {entry.changes.slice(0, visibleChangeLimit).map(change => ( + + + {change} + + ))} + + + + ); +} + +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, + }, +}); diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 18e19cc..8edd24d 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -24,6 +24,7 @@ type HomeScreenProps = Readonly<{ }>; const menuIcons: Record = { + changelog: '✎', credits: '★', newGame: '▶', policies: '§', @@ -155,7 +156,7 @@ const styles = StyleSheet.create({ textTransform: 'uppercase', }, menu: { - gap: 8, + gap: 6, width: 156, }, menuBackgroundImage: { diff --git a/src/screens/PageScreen.tsx b/src/screens/PageScreen.tsx index fcf9f56..f5149d2 100644 --- a/src/screens/PageScreen.tsx +++ b/src/screens/PageScreen.tsx @@ -12,6 +12,7 @@ import { defaultTextStyle } from '../styles/typography'; type PageRouteName = Exclude< RouteName, | 'gameplay' + | 'changelog' | 'home' | 'policies' | 'saves' diff --git a/tools/bump-build-version.mjs b/tools/bump-build-version.mjs new file mode 100644 index 0000000..ac9e805 --- /dev/null +++ b/tools/bump-build-version.mjs @@ -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; +}); diff --git a/Game Assets 1/Throne of the Bone King.png b/wiki/Game Assets 1/Throne of the Bone King.png similarity index 100% rename from Game Assets 1/Throne of the Bone King.png rename to wiki/Game Assets 1/Throne of the Bone King.png