diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..697e9ef99a262678f510fcd3c366b7a9a7b21c2d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +dist/phase2-unreal/ReferenceSource/NeonReflections/3087.webp filter=lfs diff=lfs merge=lfs -text +dist/phase2-unreal/ReferenceSource/NeonReflections/3089.webp filter=lfs diff=lfs merge=lfs -text +dist/phase2-unreal/ReferenceSource/NeonReflections/3090.webp filter=lfs diff=lfs merge=lfs -text +dist/phase2-unreal/ReferenceSource/NeonReflections/3092.jpg filter=lfs diff=lfs merge=lfs -text +dist/phase2-unreal/ReferenceSource/Opening/3099.mp4 filter=lfs diff=lfs merge=lfs -text +phase2-unreal/ReferenceSource/NeonReflections/3087.webp filter=lfs diff=lfs merge=lfs -text +phase2-unreal/ReferenceSource/NeonReflections/3089.webp filter=lfs diff=lfs merge=lfs -text +phase2-unreal/ReferenceSource/NeonReflections/3090.webp filter=lfs diff=lfs merge=lfs -text +phase2-unreal/ReferenceSource/NeonReflections/3092.jpg filter=lfs diff=lfs merge=lfs -text +phase2-unreal/ReferenceSource/Opening/3099.mp4 filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000000000000000000000000000000000000..420440cf74623113685c001b61d6afe90619b6cd --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,43 @@ +name: GitWorld Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Build browser game + run: npm run build + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2dbe14e4366df2efb8365af9e9d5aa470a23549 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.DS_Store +Thumbs.db +*.log diff --git a/Launch-GitWorld.cmd b/Launch-GitWorld.cmd new file mode 100644 index 0000000000000000000000000000000000000000..ce5d87cccb334e2b3d46532546ed2b1b2e84ac96 --- /dev/null +++ b/Launch-GitWorld.cmd @@ -0,0 +1,21 @@ +@echo off +setlocal + +set "PROJECT=%~dp0phase2-unreal\GitWorldUnreal.uproject" +set "EDITOR=" + +for %%P in ( + "%ProgramFiles%\Epic Games\UE_5.6\Engine\Binaries\Win64\UnrealEditor.exe" + "%ProgramFiles%\Epic Games\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe" + "%ProgramFiles%\Epic Games\UE_5.4\Engine\Binaries\Win64\UnrealEditor.exe" + "%ProgramFiles%\Epic Games\UE_5.3\Engine\Binaries\Win64\UnrealEditor.exe" + "%ProgramFiles%\Epic Games\UE_5.2\Engine\Binaries\Win64\UnrealEditor.exe" +) do if not defined EDITOR if exist "%%~P" set "EDITOR=%%~P" + +if defined EDITOR ( + start "GitWorld Unreal" "%EDITOR%" "%PROJECT%" -game -vr + exit /b 0 +) + +start "GitWorld Unreal" "%PROJECT%" +exit /b 0 diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..732c0b80dbf89c8bd3d5c51e33a57893e04406ae --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# GitWorld — browser-playable repository world + +GitWorld is a static browser game where the simulated repository `SNAPKITTYWEST / pax-coder` becomes a walkable cyberpunk city. Code is represented by buildings and files, commits are construction events, issues are quests, pull requests are gates, and the three named swarm agents are the construction crew. + +## Play online + +[Open the GitWorld game on GitHub Pages](https://snapkittywest.github.io/gitworld/) + +The opening scene, `3099.mp4`, and the four Neon Reflections scene plates are packaged into the Pages artifact. No Unreal installation is required to play the browser build. + +## Run + +```bash +npm test +npm run build +``` + +After the build, open `dist/index.html` from a local static file viewer. The app uses no backend, runtime API, or CDN dependency; its local opening video and scene plates are copied into `dist/phase2-unreal/ReferenceSource/`. `src/main.ts` is intentionally browser-compatible TypeScript-flavored JavaScript so the zero-dependency build can copy it directly to `dist/main.js`. + +## Controls + +- `WASD` or arrow keys: walk +- `Enter` / `E`: inspect the nearest district +- `T`: open the terminal phone +- `1`–`8`: equip a tool +- `Esc`: close an overlay +- Touch arrows: move on small screens + +All repository values are simulated and labeled in the interface. diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000000000000000000000000000000000000..957bdf158deb509ccb86982fa5f868a19146315a --- /dev/null +++ b/dist/index.html @@ -0,0 +1,122 @@ + + + + + + + + GitWorld — the world you walk into + + + +
+ + +
+ VRCHAT WORLD · NEON REFLECTIONS +

GITWORLD

+

Walk into the code. Every repository is a living world.

+
OPENING SCENE READYLOCAL BROWSER RENDERVR-STYLE VIEW
+
+ Loading 3099.mp4… +
+
+
+ + +
+
GitWorldTHE WORLD YOU WALK INTO
+
⌘ K
+
BROWSER-LOCAL SIMULATION
+
+ + + +
+
ENTER REPOSITORY

SNAPKITTYWEST / pax-coder

PublicBRANCH / main
+
+
+
REALITY DECKNEON REFLECTIONS · BROWSER SCENE PLATES
+
+ + + + +
+
+
WALKABLE CITYNORTHWASD · ARROWS TO NAVIGATE
+ +
WASDwalkEenter
+ +
+ + + +
+
CONTEXTUAL INSPECTPHYSICAL SOFTWARE
+
+
+
+ + + + + +
+
SEE SOFTWARE AS PHYSICAL OBJECTSCLICK A TOOL OR BUILDING TO INTERACT
+
+ + + + + + + + +
+
+ +
+
+
+ +
+ + + diff --git a/dist/main.js b/dist/main.js new file mode 100644 index 0000000000000000000000000000000000000000..1a8c2020f5b3f57c3e047881b27f544b944237ef --- /dev/null +++ b/dist/main.js @@ -0,0 +1,167 @@ +import { EVENT_TYPES, TOOLBELT, commandTranscript } from './shared.js'; +import { + advanceWorldState, + createWorldState, + getNearbyDistrict, + renderMinimap, + renderWorld, + worldReducer +} from './world-architect.js'; +import { bindUi, renderHud } from './ui-reality.js'; + +const app = document; +const worldViewport = app.getElementById('world-viewport'); +const minimap = app.getElementById('minimap-viewport'); +let state = createWorldState(); + +function assetPath(relativePath) { + const root = './phase2-unreal/ReferenceSource/'; + const UrlConstructor = window.URL; + return new UrlConstructor(root + relativePath, window.location.href).href; +} + +function setupBrowserScene() { + const opening = app.getElementById('opening-scene'); + const video = app.getElementById('opening-video'); + const enter = app.querySelector('[data-action="enter-game"]'); + const sound = app.querySelector('[data-action="toggle-opening-sound"]'); + const status = app.getElementById('opening-status'); + if (!opening || !video || !enter) return; + + const enterWorld = () => { + opening.classList.add('is-dismissed'); + opening.setAttribute('aria-hidden', 'true'); + video.pause(); + worldViewport?.focus(); + }; + + try { + video.src = assetPath('Opening/3099.mp4'); + video.poster = assetPath('NeonReflections/3090.webp'); + video.load(); + const openingFallback = window.setTimeout(() => { + if (video.readyState < 2) enterWorld(); + }, 2500); + video.play().catch(() => { + if (status) status.textContent = 'Opening scene ready · press ENTER THE WORLD'; + }); + video.addEventListener('playing', () => { + window.clearTimeout(openingFallback); + if (status) status.textContent = 'Playing 3099.mp4 · opening scene'; + }, { once: true }); + video.addEventListener('error', () => { + if (status) status.textContent = 'Opening media unavailable · entering browser scene'; + window.setTimeout(enterWorld, 700); + }, { once: true }); + } catch { + if (status) status.textContent = 'Opening media unavailable · entering browser scene'; + enterWorld(); + } + + enter.addEventListener('click', enterWorld); + sound?.addEventListener('click', () => { + video.muted = !video.muted; + sound.textContent = video.muted ? 'SOUND OFF' : 'SOUND ON'; + }); + + try { + app.querySelectorAll('[data-reference-image]').forEach((plate) => { + plate.setAttribute('poster', assetPath(plate.dataset.referenceImage || '')); + }); + } catch { + // Reference plates are optional presentation assets; never block gameplay. + } +} + +function dispatch(event) { + state = worldReducer(state, event); + window.gitWorldState = state; + render(); +} + +function render() { + const nearby = getNearbyDistrict(state); + if (worldViewport) renderWorld(worldViewport, state, (districtId) => dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId })); + if (minimap) renderMinimap(minimap, state); + renderHud(app, state, nearby); +} + +function move(dx, dy) { + dispatch({ type: EVENT_TYPES.PLAYER_MOVE, dx, dy }); +} + +function useToolByKey(key) { + const tool = TOOLBELT.find((candidate) => candidate.key === key); + if (!tool) return; + dispatch({ type: EVENT_TYPES.USE_TOOL, toolId: tool.id, message: `${tool.name}: ${tool.verb}.` }); +} + +function inspectNearest() { + const nearby = getNearbyDistrict(state); + if (nearby.inRange) { + dispatch({ type: EVENT_TYPES.ENTER_DISTRICT, districtId: nearby.district.id }); + } else { + dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId: nearby.district.id }); + } +} + +function onKeyDown(event) { + const target = event.target; + const typing = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement; + if (typing && event.key !== 'Escape') return; + const key = event.key.toLowerCase(); + const movement = { + w: [0, -1], arrowup: [0, -1], + s: [0, 1], arrowdown: [0, 1], + a: [-1, 0], arrowleft: [-1, 0], + d: [1, 0], arrowright: [1, 0] + }[key]; + if (movement) { + event.preventDefault(); + move(movement[0], movement[1]); + return; + } + if (key === 'e' || event.key === 'Enter') { + event.preventDefault(); + inspectNearest(); + return; + } + if (key === 't') { + event.preventDefault(); + dispatch({ type: EVENT_TYPES.OPEN_TERMINAL }); + return; + } + if (key === 'i') { + event.preventDefault(); + dispatch({ type: EVENT_TYPES.OPEN_INSPECTOR }); + return; + } + if (/^[1-8]$/.test(event.key)) { + event.preventDefault(); + useToolByKey(event.key); + return; + } + if (event.key === 'Escape') { + dispatch(state.terminal.open ? { type: EVENT_TYPES.CLOSE_TERMINAL } : { type: EVENT_TYPES.CLOSE_INSPECTOR }); + } +} + +bindUi(app, dispatch); +app.addEventListener('keydown', onKeyDown); +window.addEventListener('resize', render); +window.gitWorldState = state; +render(); +setupBrowserScene(); + +// Fixed-step browser-local simulation: the world advances on a deterministic clock, +// while the render layer simply reflects the latest serializable state. +window.setInterval(() => { + const advanced = advanceWorldState(state); + state = advanced.state; + window.gitWorldState = state; + render(); +}, 1600); + +// Keep the transcript helper reachable for the UI's delegated terminal buttons in +// environments where the app is embedded without a module bundler. +window.gitWorldCommandTranscript = commandTranscript; diff --git a/dist/phase2-unreal/ReferenceSource/NeonReflections/3087.webp b/dist/phase2-unreal/ReferenceSource/NeonReflections/3087.webp new file mode 100644 index 0000000000000000000000000000000000000000..a2eb546bd032e0fa50538440a793c3bb64ede310 --- /dev/null +++ b/dist/phase2-unreal/ReferenceSource/NeonReflections/3087.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aff14460f1c28ebe7ee2e341399587ef13c636948dbbced9b4b017b2d72e39c4 +size 278024 diff --git a/dist/phase2-unreal/ReferenceSource/NeonReflections/3089.webp b/dist/phase2-unreal/ReferenceSource/NeonReflections/3089.webp new file mode 100644 index 0000000000000000000000000000000000000000..e4f59cae6ca2b8c42977019a750a7cd363a50768 --- /dev/null +++ b/dist/phase2-unreal/ReferenceSource/NeonReflections/3089.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06ef3e7ce562608dbb1c4685fb65d423a66921479d0cd5897584c76f9f17b951 +size 280306 diff --git a/dist/phase2-unreal/ReferenceSource/NeonReflections/3090.webp b/dist/phase2-unreal/ReferenceSource/NeonReflections/3090.webp new file mode 100644 index 0000000000000000000000000000000000000000..1cdaf0e3033442661d45eb539de6b1b51a75f19c --- /dev/null +++ b/dist/phase2-unreal/ReferenceSource/NeonReflections/3090.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbf068da5c13c53daa91e2b273121eb731318d2746f49aa50fa5fa5ff57ac1a0 +size 327290 diff --git a/dist/phase2-unreal/ReferenceSource/NeonReflections/3092.jpg b/dist/phase2-unreal/ReferenceSource/NeonReflections/3092.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7e31e473e734930d830a782cd24dbbe430109a41 --- /dev/null +++ b/dist/phase2-unreal/ReferenceSource/NeonReflections/3092.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90addbd5567b5b37b98db73d7791896947621501b507e014e4902ab08497ad65 +size 191235 diff --git a/dist/phase2-unreal/ReferenceSource/Opening/3099.mp4 b/dist/phase2-unreal/ReferenceSource/Opening/3099.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..d8ff7560bb5b876fda518d7415eb325c27fad9b3 --- /dev/null +++ b/dist/phase2-unreal/ReferenceSource/Opening/3099.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eaaea396db131ac4681bd51cb4dc258ba7cae15efeebab77d752e335ca486ae0 +size 5190235 diff --git a/dist/phase2-unreal/ReferenceSource/README.md b/dist/phase2-unreal/ReferenceSource/README.md new file mode 100644 index 0000000000000000000000000000000000000000..48dab7f4194fc658195d2267673f0bf9fea477b6 --- /dev/null +++ b/dist/phase2-unreal/ReferenceSource/README.md @@ -0,0 +1,7 @@ +# GitWorld Phase 3 reference set + +The four user-selected files in `NeonReflections/` are wired as visual reference plates for the `pax-coder` GitWorld sandbox. They establish the neon social-hub, portal, avatar, and VR interface look for the Unreal scene. + +They are intentionally not passed to the photogrammetry reconstruction script: the frames are generated/stylized renders from different compositions, not a camera sweep around one real subject. RealityCapture would need a separate folder of overlapping photographs to produce a valid 3D mesh. + +The Unreal Editor script imports the plates, creates unlit display materials, places them on vertical gallery planes around the repository world, and assigns the imported textures to the world actor’s `ReferenceImages` property. This gives the project a visible scene using the supplied art while preserving the separate true-photogrammetry path. diff --git a/dist/phase2-unreal/ReferenceSource/manifest.json b/dist/phase2-unreal/ReferenceSource/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..b846c1e379cbec24448b9e2f3a649a68943b80f8 --- /dev/null +++ b/dist/phase2-unreal/ReferenceSource/manifest.json @@ -0,0 +1,34 @@ +{ + "referenceSetId": "neon-reflections-v15", + "repositoryWorld": "pax-coder", + "sourceType": "visual-reference-plates", + "photogrammetryReady": false, + "reason": "The supplied frames depict different stylized scenes and a character turntable; they are not overlapping views of one physical location.", + "openingScene": { + "file": "Opening/3099.mp4", + "role": "gitworld-opening-cinematic", + "autoplay": true + }, + "assets": [ + { + "file": "3087.webp", + "role": "arcade-portal-reference", + "placement": "east-gallery" + }, + { + "file": "3089.webp", + "role": "social-hub-pov-reference", + "placement": "north-gallery" + }, + { + "file": "3090.webp", + "role": "social-hub-spawn-reference", + "placement": "west-gallery" + }, + { + "file": "3092.jpg", + "role": "avatar-turntable-reference", + "placement": "avatar-gallery" + } + ] +} diff --git a/dist/shared.js b/dist/shared.js new file mode 100644 index 0000000000000000000000000000000000000000..e7c80eacb2e8ff21a414f37e96c9b17d1952dff0 --- /dev/null +++ b/dist/shared.js @@ -0,0 +1,90 @@ +export const EVENT_TYPES = Object.freeze({ + PLAYER_MOVE: 'player.move', + SELECT_DISTRICT: 'district.selected', + SELECT_FILE: 'file.selected', + SELECT_COLLECTION: 'collection.selected', + SELECT_RECORD: 'record.selected', + OPEN_INSPECTOR: 'inspector.opened', + CLOSE_INSPECTOR: 'inspector.closed', + OPEN_TERMINAL: 'terminal.opened', + CLOSE_TERMINAL: 'terminal.closed', + RUN_COMMAND: 'terminal.command', + USE_TOOL: 'tool.used', + ENTER_DISTRICT: 'district.entered', + TICK: 'world.tick', + AGENT_PROGRESS: 'agent.progress', + CONSTRUCTION: 'construction.updated', + BUILD_STARTED: 'build.started', + BUILD_COMPLETED: 'build.completed', + TEST_STARTED: 'test.started', + TEST_COMPLETED: 'test.completed', + DEPLOYMENT_STARTED: 'deployment.started', + DEPLOYMENT_COMPLETED: 'deployment.completed', + COMMIT: 'repository.commit', + ISSUE: 'repository.issue', + PULL_REQUEST: 'repository.pull_request' +}); + +export const TOOLBELT = Object.freeze([ + { id: 'terminal', key: '1', icon: '▣', name: 'Terminal Phone', verb: 'Open shell', color: '#69f6cf' }, + { id: 'bash', key: '2', icon: '⚒', name: 'Bash Hammer', verb: 'Run operation', color: '#ffbd69' }, + { id: 'git', key: '3', icon: '◈', name: 'Git Backpack', verb: 'View history', color: '#ff7ac8' }, + { id: 'docker', key: '4', icon: '▦', name: 'Docker Box', verb: 'Open container', color: '#6dafff' }, + { id: 'code', key: '5', icon: '⌘', name: 'Code Tablet', verb: 'Read source', color: '#b79cff' }, + { id: 'ssh', key: '6', icon: '⚿', name: 'SSH Key', verb: 'Unlock gate', color: '#ffdd67' }, + { id: 'database', key: '7', icon: '▤', name: 'Database Vault', verb: 'Inspect data', color: '#9fe870' }, + { id: 'deploy', key: '8', icon: '↗', name: 'Deploy Rocket', verb: 'Launch build', color: '#ff8b62' } +]); + +export const EVENT_LABELS = Object.freeze({ + [EVENT_TYPES.AGENT_PROGRESS]: 'agent progress', + [EVENT_TYPES.CONSTRUCTION]: 'construction update', + [EVENT_TYPES.BUILD_STARTED]: 'build started', + [EVENT_TYPES.BUILD_COMPLETED]: 'build completed', + [EVENT_TYPES.TEST_STARTED]: 'tests started', + [EVENT_TYPES.TEST_COMPLETED]: 'tests passed', + [EVENT_TYPES.DEPLOYMENT_STARTED]: 'deployment queued', + [EVENT_TYPES.DEPLOYMENT_COMPLETED]: 'deployment complete', + [EVENT_TYPES.COMMIT]: 'commit landed', + [EVENT_TYPES.ISSUE]: 'issue quest', + [EVENT_TYPES.PULL_REQUEST]: 'pull request gate' +}); + +export const COLLECTIONS = Object.freeze(['files', 'commits', 'issues', 'prs', 'build']); + +export const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); + +export function formatTime(tick) { + const seconds = String((tick * 7) % 60).padStart(2, '0'); + const minutes = String(Math.floor(tick * 7 / 60)).padStart(2, '0'); + return `00:${minutes}:${seconds}`; +} + +export function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +export function makeEvent(type, tick, detail = {}) { + return { + id: `${type}-${tick}`, + type, + tick, + at: formatTime(tick), + detail + }; +} + +export function commandTranscript(command, state) { + const prompt = '$'; + const selected = state.selectedFileId || 'README.md'; + const lines = { + ls: ['src/', 'tests/', 'docs/', '.github/', 'Cargo.toml', 'README.md'], + 'git status': ['On branch main', 'Your branch is up to date with origin/main.', 'working tree clean'], + 'git log': ['8f3a9c1 Fix kernel bounds', '2h4d7e2 Add proof sketch', '9ac1bd9 Update docs'], + 'cargo test': ['Compiling pax-coder v0.8.2', 'running 142 tests', 'test result: ok. 142 passed; 0 failed'], + './build.sh': ['[world] assembling repository tower', '[ci] verifying artifacts', 'BUILD #812 completed successfully'], + help: ['Try: ls, git status, git log, cargo test, ./build.sh'] + }; + const output = lines[command] || [`command not found: ${command}`, 'Try `help` for local commands.']; + return [`${prompt} ${command}`, ...output, `${prompt} _`]; +} diff --git a/dist/src/main.ts b/dist/src/main.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ab15afdd201e777e7bc464c9ebdd936bde615ff --- /dev/null +++ b/dist/src/main.ts @@ -0,0 +1,107 @@ +import { EVENT_TYPES, TOOLBELT, commandTranscript } from './shared.js'; +import { + advanceWorldState, + createWorldState, + getNearbyDistrict, + renderMinimap, + renderWorld, + worldReducer +} from './world-architect.js'; +import { bindUi, renderHud } from './ui-reality.js'; + +const app = document; +const worldViewport = app.getElementById('world-viewport'); +const minimap = app.getElementById('minimap-viewport'); +let state = createWorldState(); + +function dispatch(event) { + state = worldReducer(state, event); + window.gitWorldState = state; + render(); +} + +function render() { + const nearby = getNearbyDistrict(state); + if (worldViewport) renderWorld(worldViewport, state, (districtId) => dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId })); + if (minimap) renderMinimap(minimap, state); + renderHud(app, state, nearby); +} + +function move(dx, dy) { + dispatch({ type: EVENT_TYPES.PLAYER_MOVE, dx, dy }); +} + +function useToolByKey(key) { + const tool = TOOLBELT.find((candidate) => candidate.key === key); + if (!tool) return; + dispatch({ type: EVENT_TYPES.USE_TOOL, toolId: tool.id, message: `${tool.name}: ${tool.verb}.` }); +} + +function inspectNearest() { + const nearby = getNearbyDistrict(state); + if (nearby.inRange) { + dispatch({ type: EVENT_TYPES.ENTER_DISTRICT, districtId: nearby.district.id }); + } else { + dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId: nearby.district.id }); + } +} + +function onKeyDown(event) { + const target = event.target; + const typing = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement; + if (typing && event.key !== 'Escape') return; + const key = event.key.toLowerCase(); + const movement = { + w: [0, -1], arrowup: [0, -1], + s: [0, 1], arrowdown: [0, 1], + a: [-1, 0], arrowleft: [-1, 0], + d: [1, 0], arrowright: [1, 0] + }[key]; + if (movement) { + event.preventDefault(); + move(movement[0], movement[1]); + return; + } + if (key === 'e' || event.key === 'Enter') { + event.preventDefault(); + inspectNearest(); + return; + } + if (key === 't') { + event.preventDefault(); + dispatch({ type: EVENT_TYPES.OPEN_TERMINAL }); + return; + } + if (key === 'i') { + event.preventDefault(); + dispatch({ type: EVENT_TYPES.OPEN_INSPECTOR }); + return; + } + if (/^[1-8]$/.test(event.key)) { + event.preventDefault(); + useToolByKey(event.key); + return; + } + if (event.key === 'Escape') { + dispatch(state.terminal.open ? { type: EVENT_TYPES.CLOSE_TERMINAL } : { type: EVENT_TYPES.CLOSE_INSPECTOR }); + } +} + +bindUi(app, dispatch); +app.addEventListener('keydown', onKeyDown); +window.addEventListener('resize', render); +window.gitWorldState = state; +render(); + +// Fixed-step browser-local simulation: the world advances on a deterministic clock, +// while the render layer simply reflects the latest serializable state. +window.setInterval(() => { + const advanced = advanceWorldState(state); + state = advanced.state; + window.gitWorldState = state; + render(); +}, 1600); + +// Keep the transcript helper reachable for the UI's delegated terminal buttons in +// environments where the app is embedded without a module bundler. +window.gitWorldCommandTranscript = commandTranscript; diff --git a/dist/src/shared.js b/dist/src/shared.js new file mode 100644 index 0000000000000000000000000000000000000000..e7c80eacb2e8ff21a414f37e96c9b17d1952dff0 --- /dev/null +++ b/dist/src/shared.js @@ -0,0 +1,90 @@ +export const EVENT_TYPES = Object.freeze({ + PLAYER_MOVE: 'player.move', + SELECT_DISTRICT: 'district.selected', + SELECT_FILE: 'file.selected', + SELECT_COLLECTION: 'collection.selected', + SELECT_RECORD: 'record.selected', + OPEN_INSPECTOR: 'inspector.opened', + CLOSE_INSPECTOR: 'inspector.closed', + OPEN_TERMINAL: 'terminal.opened', + CLOSE_TERMINAL: 'terminal.closed', + RUN_COMMAND: 'terminal.command', + USE_TOOL: 'tool.used', + ENTER_DISTRICT: 'district.entered', + TICK: 'world.tick', + AGENT_PROGRESS: 'agent.progress', + CONSTRUCTION: 'construction.updated', + BUILD_STARTED: 'build.started', + BUILD_COMPLETED: 'build.completed', + TEST_STARTED: 'test.started', + TEST_COMPLETED: 'test.completed', + DEPLOYMENT_STARTED: 'deployment.started', + DEPLOYMENT_COMPLETED: 'deployment.completed', + COMMIT: 'repository.commit', + ISSUE: 'repository.issue', + PULL_REQUEST: 'repository.pull_request' +}); + +export const TOOLBELT = Object.freeze([ + { id: 'terminal', key: '1', icon: '▣', name: 'Terminal Phone', verb: 'Open shell', color: '#69f6cf' }, + { id: 'bash', key: '2', icon: '⚒', name: 'Bash Hammer', verb: 'Run operation', color: '#ffbd69' }, + { id: 'git', key: '3', icon: '◈', name: 'Git Backpack', verb: 'View history', color: '#ff7ac8' }, + { id: 'docker', key: '4', icon: '▦', name: 'Docker Box', verb: 'Open container', color: '#6dafff' }, + { id: 'code', key: '5', icon: '⌘', name: 'Code Tablet', verb: 'Read source', color: '#b79cff' }, + { id: 'ssh', key: '6', icon: '⚿', name: 'SSH Key', verb: 'Unlock gate', color: '#ffdd67' }, + { id: 'database', key: '7', icon: '▤', name: 'Database Vault', verb: 'Inspect data', color: '#9fe870' }, + { id: 'deploy', key: '8', icon: '↗', name: 'Deploy Rocket', verb: 'Launch build', color: '#ff8b62' } +]); + +export const EVENT_LABELS = Object.freeze({ + [EVENT_TYPES.AGENT_PROGRESS]: 'agent progress', + [EVENT_TYPES.CONSTRUCTION]: 'construction update', + [EVENT_TYPES.BUILD_STARTED]: 'build started', + [EVENT_TYPES.BUILD_COMPLETED]: 'build completed', + [EVENT_TYPES.TEST_STARTED]: 'tests started', + [EVENT_TYPES.TEST_COMPLETED]: 'tests passed', + [EVENT_TYPES.DEPLOYMENT_STARTED]: 'deployment queued', + [EVENT_TYPES.DEPLOYMENT_COMPLETED]: 'deployment complete', + [EVENT_TYPES.COMMIT]: 'commit landed', + [EVENT_TYPES.ISSUE]: 'issue quest', + [EVENT_TYPES.PULL_REQUEST]: 'pull request gate' +}); + +export const COLLECTIONS = Object.freeze(['files', 'commits', 'issues', 'prs', 'build']); + +export const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); + +export function formatTime(tick) { + const seconds = String((tick * 7) % 60).padStart(2, '0'); + const minutes = String(Math.floor(tick * 7 / 60)).padStart(2, '0'); + return `00:${minutes}:${seconds}`; +} + +export function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +export function makeEvent(type, tick, detail = {}) { + return { + id: `${type}-${tick}`, + type, + tick, + at: formatTime(tick), + detail + }; +} + +export function commandTranscript(command, state) { + const prompt = '$'; + const selected = state.selectedFileId || 'README.md'; + const lines = { + ls: ['src/', 'tests/', 'docs/', '.github/', 'Cargo.toml', 'README.md'], + 'git status': ['On branch main', 'Your branch is up to date with origin/main.', 'working tree clean'], + 'git log': ['8f3a9c1 Fix kernel bounds', '2h4d7e2 Add proof sketch', '9ac1bd9 Update docs'], + 'cargo test': ['Compiling pax-coder v0.8.2', 'running 142 tests', 'test result: ok. 142 passed; 0 failed'], + './build.sh': ['[world] assembling repository tower', '[ci] verifying artifacts', 'BUILD #812 completed successfully'], + help: ['Try: ls, git status, git log, cargo test, ./build.sh'] + }; + const output = lines[command] || [`command not found: ${command}`, 'Try `help` for local commands.']; + return [`${prompt} ${command}`, ...output, `${prompt} _`]; +} diff --git a/dist/src/ui-reality.js b/dist/src/ui-reality.js new file mode 100644 index 0000000000000000000000000000000000000000..ad113d9c9c7aa786f35a3598ef9488cd054a68ac --- /dev/null +++ b/dist/src/ui-reality.js @@ -0,0 +1,210 @@ +import { + COLLECTIONS, + EVENT_LABELS, + EVENT_TYPES, + TOOLBELT, + commandTranscript, + formatTime +} from './shared.js'; + +function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[character])); +} + +function byId(root, id) { + return root.getElementById(id); +} + +function setText(root, id, value) { + const element = byId(root, id); + if (element) element.textContent = value; +} + +function selectedDistrict(state) { + return state.districts.find((district) => district.id === state.selectedDistrictId) || state.districts[0]; +} + +function selectedFile(state) { + const district = selectedDistrict(state); + return district.files.find((file) => file.id === state.selectedFileId) || district.files[0]; +} + +function eventMessage(event) { + return event.detail?.message || EVENT_LABELS[event.type] || event.type; +} + +function renderAgents(root, state) { + const agentList = byId(root, 'agent-list'); + if (!agentList) return; + agentList.innerHTML = state.agents.map((agent) => `
  • + ${agent.name.slice(0, 2)}${escapeHtml(agent.name)}${escapeHtml(agent.role)}${escapeHtml(agent.task)}● ${escapeHtml(agent.status)} +
  • `).join(''); + setText(root, 'active-agent-count', String(state.agents.length)); +} + +function renderEvents(root, state) { + const feed = byId(root, 'event-feed'); + if (!feed) return; + feed.innerHTML = state.events.slice(0, 7).map((event) => `
  • ${escapeHtml(eventMessage(event))}

  • `).join(''); + setText(root, 'event-count', `${state.events.length} EVENTS`); +} + +function renderToolbelt(root, state) { + const toolbelt = byId(root, 'toolbelt'); + if (!toolbelt) return; + toolbelt.innerHTML = TOOLBELT.map((tool) => ``).join(''); +} + +function renderStatus(root, state) { + setText(root, 'repo-visibility', state.repository.visibility); + setText(root, 'repo-branch', `BRANCH / ${state.repository.branch}`); + setText(root, 'build-status', state.repository.build); + setText(root, 'test-status', state.repository.tests); + setText(root, 'deploy-status', state.repository.deployment); + setText(root, 'world-clock', formatTime(state.tick)); + setText(root, 'player-location', state.view === 'district' ? `INSIDE ${selectedDistrict(state).path}` : 'SPAWN PLAZA'); + setText(root, 'feedback-line', state.feedback); + const playerLevel = Math.min(99, 12 + Math.floor(state.tick / 3)); + setText(root, 'player-level', `LEVEL ${playerLevel} DEV`); + const xp = Math.min(100, 34 + state.tick * 2); + const xpBar = byId(root, 'xp-fill'); + if (xpBar) xpBar.style.width = `${xp}%`; + setText(root, 'xp-label', `${xp} / 100 XP`); +} + +function renderFiles(state) { + const district = selectedDistrict(state); + return `
    DIRECTORY${escapeHtml(district.path)}${escapeHtml(district.stat)}
    ${district.files.map((file) => ``).join('')}
    `; +} + +function renderCommits(state) { + const commits = state.repository.commits || []; + return `
    HISTORY ROADRECENT COMMITS${commits.length} simulated events
    ${commits.map((commit) => ``).join('')}
    `; +} + +function renderIssues(state) { + const issues = state.repository.issues || []; + return `
    QUEST BOARDOPEN ISSUESQuests waiting in the city
    ${issues.map((issue) => ``).join('')}
    `; +} + +function renderPullRequests(state) { + const prs = state.repository.pullRequests || []; + return `
    MERGE GATEPULL REQUESTSDoors into the next branch
    ${prs.map((pr) => ``).join('')}
    `; +} + +function renderBuild(state) { + return `
    AUTOMATION CENTERBUILD & TESTAll values are simulated browser-local signals
    BUILD #812${escapeHtml(state.repository.build)}
    INSPECTORS${escapeHtml(state.repository.tests)}
    PAGES RUNWAY${escapeHtml(state.repository.deployment)}
    SOURCECOMPILETESTDEPLOY
    `; +} + +function renderRecord(state) { + const record = state.selectedRecord; + if (!record) return renderBuild(state); + if (record.type === 'commit') { + const item = (state.repository.commits || []).find((candidate) => candidate.id === record.id); + return `
    COMMIT EVENT${escapeHtml(item?.hash || record.id)}Historical event made physical
    ${escapeHtml(item?.message || 'Commit selected')}

    This simulated commit is a construction event. The repository builder routes its changed files into the city.

    author · ${escapeHtml(item?.author || 'builder')}
    `; + } + if (record.type === 'issue') { + const item = (state.repository.issues || []).find((candidate) => candidate.id === record.id); + return `
    ISSUE QUEST${escapeHtml(item?.title || record.id)}${escapeHtml(item?.status || 'OPEN')}
    Quest objective

    ${escapeHtml(item?.detail || 'Inspect the open issue in the quest district.')}

    marker active · browser-local simulation
    `; + } + const item = (state.repository.pullRequests || []).find((candidate) => candidate.id === record.id); + return `
    PULL REQUEST DOOR${escapeHtml(item?.title || record.id)}${escapeHtml(item?.status || 'REVIEW')}
    Merge gate

    ${escapeHtml(item?.detail || 'The gate is waiting for review.')}

    checks passing · browser-local simulation
    `; +} + +function renderInspector(root, state, nearby) { + const panel = byId(root, 'inspector-content'); + if (!panel) return; + const district = selectedDistrict(state); + const file = selectedFile(state); + const collection = state.selectedCollection; + let content = ''; + if (state.activePanel === 'file' || (state.activePanel === 'collection' && collection === 'files')) { + content = `
    ${escapeHtml(file.kind)} OBJECT

    ${escapeHtml(file.name)}

    ${escapeHtml(file.detail)}

    ${escapeHtml(file.code)}
    `; + } else if (state.activePanel === 'record') { + content = renderRecord(state); + } else if (state.activePanel === 'collection') { + content = collection === 'commits' ? renderCommits(state) : collection === 'issues' ? renderIssues(state) : collection === 'prs' ? renderPullRequests(state) : renderBuild(state); + } else if (state.activePanel === 'inspector' || state.activePanel === 'world') { + const canEnter = nearby?.district?.id === district.id && nearby.inRange; + content = `
    ${state.view === 'district' ? 'CURRENT DISTRICT' : 'NEAREST DISTRICT'}

    ${escapeHtml(district.title)}

    ${escapeHtml(district.summary)}

    ${escapeHtml(district.path)}${escapeHtml(district.stat)}${Math.round((state.construction[district.id] || 0) * 100)}% BUILT
    ${district.files.slice(0, 3).map((item) => ``).join('')}
    `; + } else { + content = `
    ${escapeHtml(district.path)}

    Repository objects

    Select a building, tool, or activity to inspect the physical software around you.

    `; + } + panel.innerHTML = content; + const nearbyPrompt = byId(root, 'nearby-prompt'); + if (nearbyPrompt) { + nearbyPrompt.innerHTML = nearby?.district && nearby.inRange ? `NEARBY${escapeHtml(nearby.district.name)}PRESS E / ENTER TO ENTER` : `WALKABLE CITY${escapeHtml(state.player.facing.toUpperCase())}WASD · ARROWS TO NAVIGATE`; + nearbyPrompt.classList.toggle('is-ready', Boolean(nearby?.inRange)); + } +} + +function renderTerminal(root, state) { + const overlay = byId(root, 'terminal-phone'); + if (!overlay) return; + overlay.classList.toggle('is-open', state.terminal.open); + overlay.setAttribute('aria-hidden', String(!state.terminal.open)); + const output = byId(root, 'terminal-output'); + if (output) output.innerHTML = state.terminal.lines.map((line) => `
    ${escapeHtml(line)}
    `).join(''); + const input = byId(root, 'terminal-input'); + if (input && input.value !== state.terminal.command) input.value = ''; +} + +function renderSearch(root) { + const search = byId(root, 'repo-search'); + if (!search) return; + const query = search.value.trim().toLowerCase(); + root.querySelectorAll('.world-building').forEach((building) => { + building.classList.toggle('is-search-hidden', Boolean(query) && !building.textContent.toLowerCase().includes(query)); + }); +} + +export function renderHud(root, state, nearby) { + renderAgents(root, state); + renderEvents(root, state); + renderToolbelt(root, state); + renderStatus(root, state); + renderInspector(root, state, nearby); + renderTerminal(root, state); + renderSearch(root); + const drawer = byId(root, 'inspector-drawer'); + if (drawer) drawer.classList.toggle('is-open', state.activePanel !== 'world' && state.activePanel !== 'terminal'); + const shell = byId(root, 'game-shell'); + if (shell) shell.dataset.view = state.view; + setText(root, 'selected-tool-label', (TOOLBELT.find((tool) => tool.id === state.activeTool) || TOOLBELT[0]).name); +} + +export function bindUi(root, dispatch) { + root.addEventListener('click', (event) => { + const target = event.target.closest('[data-action], [data-district-id]'); + if (!target) return; + const action = target.dataset.action || (target.dataset.districtId ? 'select-district' : ''); + if (action === 'select-district') dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId: target.dataset.districtId }); + if (action === 'open-inspector') dispatch({ type: EVENT_TYPES.OPEN_INSPECTOR }); + if (action === 'enter-district') dispatch({ type: EVENT_TYPES.ENTER_DISTRICT, districtId: target.dataset.districtId }); + if (action === 'close-inspector') dispatch({ type: EVENT_TYPES.CLOSE_INSPECTOR }); + if (action === 'select-file') dispatch({ type: EVENT_TYPES.SELECT_FILE, fileId: target.dataset.fileId }); + if (action === 'select-collection') dispatch({ type: EVENT_TYPES.SELECT_COLLECTION, collection: target.dataset.collection }); + if (action === 'commit-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'commit', recordId: target.dataset.commitId }); + if (action === 'issue-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'issue', recordId: target.dataset.issueId }); + if (action === 'pr-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'pr', recordId: target.dataset.prId }); + if (action === 'move') dispatch({ type: EVENT_TYPES.PLAYER_MOVE, dx: target.dataset.dx, dy: target.dataset.dy }); + if (action === 'tool') { + const tool = TOOLBELT.find((item) => item.id === target.dataset.toolId); + dispatch({ type: EVENT_TYPES.USE_TOOL, toolId: target.dataset.toolId, message: tool?.verb ? `${tool.name}: ${tool.verb}.` : undefined }); + } + if (action === 'open-terminal') dispatch({ type: EVENT_TYPES.OPEN_TERMINAL }); + if (action === 'close-terminal') dispatch({ type: EVENT_TYPES.CLOSE_TERMINAL }); + if (action === 'command') dispatch({ type: EVENT_TYPES.RUN_COMMAND, command: target.dataset.command, lines: commandTranscript(target.dataset.command, window.gitWorldState || { selectedFileId: 'README.md' }) }); + }); + + root.addEventListener('submit', (event) => { + if (!event.target.matches('#terminal-form')) return; + event.preventDefault(); + const input = byId(root, 'terminal-input'); + const command = input?.value.trim() || ''; + if (command) dispatch({ type: EVENT_TYPES.RUN_COMMAND, command, lines: commandTranscript(command, window.gitWorldState || { selectedFileId: 'README.md' }) }); + }); + + const search = byId(root, 'repo-search'); + if (search) search.addEventListener('input', () => renderSearch(root)); +} diff --git a/dist/src/world-architect.js b/dist/src/world-architect.js new file mode 100644 index 0000000000000000000000000000000000000000..f7b03b2938be412d195cab79d2108fe56467b307 --- /dev/null +++ b/dist/src/world-architect.js @@ -0,0 +1,451 @@ +import { + clamp, + clone, + EVENT_TYPES, + makeEvent +} from './shared.js'; + +export const WALKABLE_BOUNDS = Object.freeze({ minX: 70, maxX: 1530, minY: 90, maxY: 850 }); + +export const DISTRICTS = Object.freeze([ + { + id: 'src', + name: 'SRC DISTRICT', + path: '/src', + title: 'Central Kernel Tower', + glyph: '⌘', + accent: '#62f2d2', + x: 565, + y: 230, + width: 255, + height: 220, + summary: 'The living source tree: modules, kernel code, and the repository entry point.', + stat: '42 files · 213 commits', + files: [ + { id: 'main-rs', name: 'main.rs', kind: 'Rust', size: '8.4 KB', detail: 'Repository bootstrap and agent scheduler.', code: 'fn main() {\n let world = GitWorld::spawn();\n world.walk();\n}' }, + { id: 'kernel-cu', name: 'kernel.cu', kind: 'CUDA', size: '24.1 KB', detail: 'The compute floor is under construction.', code: '__global__ void kernel_step(Agent agent) {\n agent.advance();\n}' }, + { id: 'lib-rs', name: 'lib.rs', kind: 'Rust', size: '12.7 KB', detail: 'Shared repository primitives and event contracts.', code: 'pub mod events;\npub mod districts;\npub mod builders;' }, + { id: 'mod-rs', name: 'mod.rs', kind: 'Rust', size: '2.1 KB', detail: 'District module registry.', code: 'pub const DISTRICTS: &[&str] = &[\n "src", "tests", "docs"\n];' } + ] + }, + { + id: 'tests', + name: 'TEST DISTRICT', + path: '/tests', + title: 'Test Factory', + glyph: '✓', + accent: '#91e85e', + x: 230, + y: 370, + width: 235, + height: 185, + summary: 'Inspection robots verify each build before the city opens another street.', + stat: '142 passing · 0 failing', + files: [ + { id: 'world-test', name: 'world.test.js', kind: 'Test', size: '9.7 KB', detail: 'Deterministic movement and event checks.', code: 'assert.equal(world.tick, 12);\nassert.equal(events.length, 4);' }, + { id: 'contracts-test', name: 'contracts.test.js', kind: 'Test', size: '6.2 KB', detail: 'Shared UI/world event contract checks.', code: 'assert.ok(EVENT_TYPES.PLAYER_MOVE);\nassert.ok(EVENT_TYPES.BUILD_COMPLETED);' }, + { id: 'fixtures', name: 'fixtures/', kind: 'Directory', size: '18 items', detail: 'Small simulated repository fixtures.', code: 'fixture("pax-coder");' } + ] + }, + { + id: 'docs', + name: 'DOCS LIBRARY', + path: '/docs', + title: 'Documentation Library', + glyph: '▤', + accent: '#ffcf68', + x: 1085, + y: 245, + width: 240, + height: 205, + summary: 'A warm archive of architecture decisions, guides, and the city map legend.', + stat: '18 guides · 96% coverage', + files: [ + { id: 'architecture', name: 'architecture.md', kind: 'Markdown', size: '14.2 KB', detail: 'The frontend event-driven architecture.', code: '# GitWorld architecture\nInput -> simulation -> state -> HUD' }, + { id: 'contributing', name: 'contributing.md', kind: 'Markdown', size: '4.8 KB', detail: 'How builders add a district.', code: '1. Choose a street\n2. Add a contract\n3. Run the inspectors' }, + { id: 'readme', name: 'README.md', kind: 'Markdown', size: '8.3 KB', detail: 'The repository welcome sign.', code: '# pax-coder\nA simulated repository world.' } + ] + }, + { + id: 'issues', + name: 'ISSUE QUESTS', + path: '/issues', + title: 'Issue Quest Board', + glyph: '!', + accent: '#ff9f58', + x: 865, + y: 470, + width: 215, + height: 160, + summary: 'Open issues become quests with markers, owners, and a clear next action.', + stat: '3 open quests · 1 in progress', + files: [ + { id: 'issue-201', name: '#201 bug in proof', kind: 'Issue', size: 'open', detail: 'Reproduce the proof boundary on the kernel floor.', code: 'status: open\nowner: proof-agent' }, + { id: 'issue-202', name: '#202 add test', kind: 'Issue', size: 'open', detail: 'Add a regression test for agent movement.', code: 'status: open\nowner: test-agent' }, + { id: 'issue-203', name: '#203 docs update', kind: 'Issue', size: 'queued', detail: 'Document the deploy launch sequence.', code: 'status: queued\nowner: doc-agent' } + ] + }, + { + id: 'prs', + name: 'PR GATE', + path: '/pulls', + title: 'Pull Request Gate', + glyph: '↔', + accent: '#d37cff', + x: 600, + y: 475, + width: 210, + height: 180, + summary: 'A glowing merge door: review the change before opening the next branch road.', + stat: '2 open · review required', + files: [ + { id: 'pr-88', name: '#88 merge kernel fix', kind: 'Pull request', size: 'review', detail: 'Two approvals still needed before merge.', code: 'checks: passing\nreviews: 1 / 3' }, + { id: 'pr-91', name: '#91 docs refresh', kind: 'Pull request', size: 'ready', detail: 'Ready to merge after the documentation quest.', code: 'checks: passing\nreviews: 3 / 3' } + ] + }, + { + id: 'actions', + name: 'ACTIONS FACTORY', + path: '/.github', + title: 'Automation Center', + glyph: '⚙', + accent: '#66a9ff', + x: 1250, + y: 485, + width: 235, + height: 195, + summary: 'CI factories turn commits into tested artifacts and light the deploy runway.', + stat: 'BUILD #812 · passing', + files: [ + { id: 'ci-yml', name: 'ci.yml', kind: 'Actions', size: '5.2 KB', detail: 'Build, test, and artifact inspection pipeline.', code: 'jobs:\n build:\n test:\n deploy:' }, + { id: 'pages-yml', name: 'pages.yml', kind: 'Actions', size: '2.4 KB', detail: 'Static Pages deployment path.', code: 'deploy-pages: enabled' }, + { id: 'release', name: 'release.yml', kind: 'Actions', size: '3.0 KB', detail: 'Packages the next city snapshot.', code: 'tag: v0.8.2' } + ] + } +]); + +const INITIAL_AGENTS = [ + { id: 'world-architect', name: 'WORLD ARCHITECT', role: 'World geometry', task: 'Building /src district', status: 'BUILDING', progress: 78, color: '#62f2d2' }, + { id: 'repository-builder', name: 'REPOSITORY BUILDER', role: 'Repository mapping', task: 'Mapping 42 files', status: 'MAPPING', progress: 64, color: '#ffcf68' }, + { id: 'reality-integrator', name: 'REALITY/UI INTEGRATOR', role: 'Interaction layer', task: 'Connecting terminal', status: 'WIRING', progress: 52, color: '#d37cff' } +]; + +const INITIAL_EVENTS = [ + { id: 'seed-1', type: EVENT_TYPES.COMMIT, tick: 0, at: '00:00:00', detail: { message: '8f3a9c1 Fix kernel bounds', districtId: 'src' } }, + { id: 'seed-2', type: EVENT_TYPES.AGENT_PROGRESS, tick: 0, at: '00:00:00', detail: { message: 'WORLD ARCHITECT placed a new tower window.', agentId: 'world-architect' } }, + { id: 'seed-3', type: EVENT_TYPES.TEST_COMPLETED, tick: 0, at: '00:00:00', detail: { message: '142 inspectors passed the street check.', districtId: 'tests' } } +]; + +const SIMULATION_SEQUENCE = [ + { type: EVENT_TYPES.AGENT_PROGRESS, detail: { agentId: 'world-architect', message: 'WORLD ARCHITECT raised the /src tower scaffold.' } }, + { type: EVENT_TYPES.AGENT_PROGRESS, detail: { agentId: 'repository-builder', message: 'REPOSITORY BUILDER mapped kernel.cu into the compute floor.' } }, + { type: EVENT_TYPES.CONSTRUCTION, detail: { districtId: 'src', amount: 0.03, message: 'A new source window flickers online.' } }, + { type: EVENT_TYPES.TEST_STARTED, detail: { districtId: 'tests', message: 'Inspection robots started a deterministic street sweep.' } }, + { type: EVENT_TYPES.TEST_COMPLETED, detail: { districtId: 'tests', message: '142 tests passed. The test district lights turn green.' } }, + { type: EVENT_TYPES.BUILD_STARTED, detail: { districtId: 'actions', message: 'BUILD #813 started in the automation factory.' } }, + { type: EVENT_TYPES.AGENT_PROGRESS, detail: { agentId: 'reality-integrator', message: 'REALITY/UI INTEGRATOR wired a contextual inspect panel.' } }, + { type: EVENT_TYPES.BUILD_COMPLETED, detail: { districtId: 'actions', message: 'BUILD #813 completed and artifacts are staged.' } }, + { type: EVENT_TYPES.COMMIT, detail: { districtId: 'src', message: '2h4d7e2 Add proof sketch' } }, + { type: EVENT_TYPES.ISSUE, detail: { districtId: 'issues', message: '#202 quest marker activated: add test' } }, + { type: EVENT_TYPES.PULL_REQUEST, detail: { districtId: 'prs', message: '#91 merge gate is ready for review.' } }, + { type: EVENT_TYPES.DEPLOYMENT_STARTED, detail: { districtId: 'actions', message: 'Deploy rocket is fueling on the Pages runway.' } }, + { type: EVENT_TYPES.DEPLOYMENT_COMPLETED, detail: { districtId: 'actions', message: 'Static city snapshot deployed to the local launch pad.' } } +]; + +export function createWorldState(seed = 20260828) { + return { + seed, + tick: 0, + repository: { + owner: 'SNAPKITTYWEST', + name: 'pax-coder', + branch: 'main', + visibility: 'Public', + simulated: true, + stars: 213, + forks: 45, + languages: ['Rust', 'CUDA', 'Markdown'], + build: 'PASSING', + tests: '142 PASS', + deployment: 'STAGED', + commits: [ + { id: '8f3a9c1', hash: '8f3a9c1', message: 'Fix kernel bounds', author: 'AhmadParr' }, + { id: '2h4d7e2', hash: '2h4d7e2', message: 'Add proof sketch', author: 'proof-agent' }, + { id: '9ac1bd9', hash: '9ac1bd9', message: 'Update docs', author: 'doc-agent' }, + { id: '7d0c812', hash: '7d0c812', message: 'Wire agent event bus', author: 'world-architect' } + ], + issues: [ + { id: '201', title: '#201 Bug in proof', detail: 'Reproduce the proof boundary on the kernel floor.', status: 'OPEN' }, + { id: '202', title: '#202 Add test', detail: 'Add a regression test for agent movement.', status: 'OPEN' }, + { id: '203', title: '#203 Docs update', detail: 'Document the deploy launch sequence.', status: 'QUEUED' } + ], + pullRequests: [ + { id: '88', title: '#88 Merge kernel fix', detail: 'Two approvals still needed before merge.', status: 'REVIEW' }, + { id: '91', title: '#91 Docs refresh', detail: 'Ready to merge after the documentation quest.', status: 'READY' } + ] + }, + districts: clone(DISTRICTS), + agents: clone(INITIAL_AGENTS), + events: clone(INITIAL_EVENTS), + construction: { src: 0.78, tests: 1, docs: 0.96, issues: 0.7, prs: 0.82, actions: 0.88 }, + player: { x: 800, y: 735, speed: 18, facing: 'north', step: 0 }, + selectedDistrictId: 'src', + selectedFileId: 'main-rs', + selectedCollection: 'files', + selectedRecord: null, + activeTool: 'terminal', + activePanel: 'world', + terminal: { open: false, lines: ['$ local shell connected', '$ type `help` for commands'], command: '' }, + feedback: 'Walk toward a district or click a glowing building.', + unlocked: false, + view: 'city' + }; +} + +function pushEvent(state, event) { + state.events = [event, ...state.events].slice(0, 14); +} + +function findDistrict(state, districtId) { + return state.districts.find((district) => district.id === districtId) || state.districts[0]; +} + +function selectedFile(state) { + const district = findDistrict(state, state.selectedDistrictId); + return district.files.find((file) => file.id === state.selectedFileId) || district.files[0]; +} + +function circlesOverlap(x, y, district, margin = 28) { + const left = district.x - margin; + const right = district.x + district.width + margin; + const top = district.y - margin; + const bottom = district.y + district.height + margin; + return x >= left && x <= right && y >= top && y <= bottom; +} + +export function getNearbyDistrict(state, range = 185) { + let nearest = null; + let distance = Number.POSITIVE_INFINITY; + for (const district of state.districts) { + const centerX = district.x + district.width / 2; + const centerY = district.y + district.height / 2; + const nextDistance = Math.hypot(state.player.x - centerX, state.player.y - centerY); + if (nextDistance < distance) { + nearest = district; + distance = nextDistance; + } + } + return { district: nearest, distance, inRange: distance <= range }; +} + +export function worldReducer(previous, event) { + const state = clone(previous); + switch (event.type) { + case EVENT_TYPES.PLAYER_MOVE: { + const dx = Number(event.dx) || 0; + const dy = Number(event.dy) || 0; + const magnitude = Math.hypot(dx, dy) || 1; + const nextX = clamp(state.player.x + (dx / magnitude) * state.player.speed, WALKABLE_BOUNDS.minX, WALKABLE_BOUNDS.maxX); + const nextY = clamp(state.player.y + (dy / magnitude) * state.player.speed, WALKABLE_BOUNDS.minY, WALKABLE_BOUNDS.maxY); + if (!state.districts.some((district) => circlesOverlap(nextX, nextY, district))) { + state.player.x = nextX; + state.player.y = nextY; + state.player.step = (state.player.step + 1) % 4; + } + state.player.facing = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'east' : 'west') : (dy > 0 ? 'south' : 'north'); + return state; + } + case EVENT_TYPES.SELECT_DISTRICT: { + const district = findDistrict(state, event.districtId); + state.selectedDistrictId = district.id; + state.selectedFileId = district.files[0]?.id || null; + state.feedback = `${district.name} selected. Press Enter or E to inspect.`; + return state; + } + case EVENT_TYPES.SELECT_FILE: + state.selectedFileId = event.fileId; + state.activePanel = 'file'; + state.feedback = `Inspecting ${selectedFile(state).name}`; + return state; + case EVENT_TYPES.SELECT_COLLECTION: + state.selectedCollection = event.collection; + state.activePanel = 'collection'; + return state; + case EVENT_TYPES.SELECT_RECORD: + state.selectedRecord = { type: event.recordType, id: event.recordId }; + state.activePanel = 'record'; + return state; + case EVENT_TYPES.OPEN_INSPECTOR: + state.activePanel = 'inspector'; + state.feedback = `Inspecting ${findDistrict(state, state.selectedDistrictId).path}`; + return state; + case EVENT_TYPES.CLOSE_INSPECTOR: + state.activePanel = 'world'; + return state; + case EVENT_TYPES.ENTER_DISTRICT: { + const district = findDistrict(state, event.districtId || state.selectedDistrictId); + state.selectedDistrictId = district.id; + state.selectedFileId = district.files[0]?.id || null; + state.activePanel = 'inspector'; + state.view = 'district'; + state.feedback = `Entered ${district.path}. Code is physical here.`; + pushEvent(state, makeEvent(EVENT_TYPES.ENTER_DISTRICT, state.tick, { message: `Player entered ${district.path}.`, districtId: district.id })); + return state; + } + case EVENT_TYPES.OPEN_TERMINAL: + state.terminal.open = true; + state.activeTool = 'terminal'; + state.activePanel = 'terminal'; + state.feedback = 'Terminal phone online. Commands are simulated locally.'; + return state; + case EVENT_TYPES.CLOSE_TERMINAL: + state.terminal.open = false; + if (state.activePanel === 'terminal') state.activePanel = 'world'; + return state; + case EVENT_TYPES.RUN_COMMAND: { + const command = String(event.command || '').trim().toLowerCase(); + if (!command) return state; + state.terminal.command = command; + state.terminal.lines = [...state.terminal.lines, ...event.lines].slice(-28); + state.feedback = 'Ran `' + command + '` in the simulated terminal.'; + pushEvent(state, makeEvent(EVENT_TYPES.USE_TOOL, state.tick, { message: `Terminal ran ${command}.`, toolId: 'terminal' })); + return state; + } + case EVENT_TYPES.USE_TOOL: { + state.activeTool = event.toolId; + if (event.toolId === 'terminal') state.terminal.open = true; + if (event.toolId === 'git') { state.selectedCollection = 'commits'; state.activePanel = 'collection'; } + if (event.toolId === 'docker') { state.selectedCollection = 'build'; state.activePanel = 'collection'; } + if (event.toolId === 'code') state.activePanel = 'file'; + if (event.toolId === 'ssh') { state.unlocked = true; state.activePanel = 'inspector'; } + if (event.toolId === 'database') { state.selectedCollection = 'files'; state.activePanel = 'collection'; } + if (event.toolId === 'deploy') { state.repository.deployment = 'LAUNCHING'; state.selectedCollection = 'build'; state.activePanel = 'collection'; } + state.feedback = event.message || `Equipped ${event.toolId}.`; + pushEvent(state, makeEvent(EVENT_TYPES.USE_TOOL, state.tick, { message: state.feedback, toolId: event.toolId })); + return state; + } + case EVENT_TYPES.TICK: + state.tick += 1; + return state; + case EVENT_TYPES.AGENT_PROGRESS: { + const agent = state.agents.find((candidate) => candidate.id === event.detail.agentId); + if (agent) agent.progress = Math.min(100, agent.progress + 2); + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + } + case EVENT_TYPES.CONSTRUCTION: { + const districtId = event.detail.districtId; + state.construction[districtId] = clamp((state.construction[districtId] || 0) + (event.detail.amount || 0), 0, 1); + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + } + case EVENT_TYPES.BUILD_STARTED: + state.repository.build = 'RUNNING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.BUILD_COMPLETED: + state.repository.build = 'PASSING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.TEST_STARTED: + state.repository.tests = 'RUNNING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.TEST_COMPLETED: + state.repository.tests = '142 PASS'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.DEPLOYMENT_STARTED: + state.repository.deployment = 'LAUNCHING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.DEPLOYMENT_COMPLETED: + state.repository.deployment = 'STAGED'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.COMMIT: + case EVENT_TYPES.ISSUE: + case EVENT_TYPES.PULL_REQUEST: + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + default: + return state; + } +} + +export function advanceWorldState(state) { + const ticked = worldReducer(state, { type: EVENT_TYPES.TICK }); + const plan = SIMULATION_SEQUENCE[(ticked.tick - 1) % SIMULATION_SEQUENCE.length]; + const event = makeEvent(plan.type, ticked.tick, plan.detail); + return { state: worldReducer(ticked, event), event }; +} + +function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[character])); +} + +function windowBlocks(count, color) { + return Array.from({ length: count }, (_, index) => ``).join(''); +} + +export function renderWorld(viewport, state, onBuildingSelect) { + const viewportWidth = viewport.clientWidth || 960; + const viewportHeight = viewport.clientHeight || 680; + const centerX = viewportWidth / 2; + const centerY = viewportHeight * 0.56; + const translateX = centerX - state.player.x * 0.88; + const translateY = centerY - state.player.y * 0.88; + const buildingMarkup = state.districts.map((district) => { + const selected = district.id === state.selectedDistrictId ? ' is-selected' : ''; + const progress = Math.round((state.construction[district.id] || 0) * 100); + return ``; + }).join(''); + + viewport.innerHTML = `
    + + +
    +
    +
    MAIN
    +
    HISTORY
    +
    BRANCH / ALT
    +
    SPAWN PLAZA
    + ${buildingMarkup} + + +
    +
    +
    !
    +
    PULL REQUESTOPEN GATE
    +
    + +
    +
    SIMULATED REPOSITORYSNAPKITTYWEST/ pax-coder★ 213 · ⑂ 45 · ◉ PUBLIC
    +
    BUILD AGENTS3 ACTIVEWATCH THE CREW WORK
    +
    +
    `; + + viewport.querySelectorAll('[data-district-id]').forEach((building) => { + building.addEventListener('click', () => onBuildingSelect(building.dataset.districtId)); + }); +} + +export function renderMinimap(root, state) { + const districts = state.districts.map((district) => ``).join(''); + root.innerHTML = `
    ${districts}
    LIVE MAP${state.selectedDistrictId.toUpperCase()}
    `; +} diff --git a/dist/styles.css b/dist/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..fc6e263b46b9a981781ffc229642663d9bf95cd4 --- /dev/null +++ b/dist/styles.css @@ -0,0 +1,108 @@ +:root { + color-scheme: dark; + --night: #040b14; + --sky: #071829; + --panel: rgba(5, 16, 29, .92); + --panel-solid: #091827; + --line: #164666; + --line-hot: #2a7195; + --white: #e7f5ff; + --muted: #8daabe; + --cyan: #64f4e1; + --blue: #68acff; + --green: #9ff254; + --gold: #ffc95b; + --orange: #ff9d54; + --pink: #e887ff; + --red: #ff6877; + --mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + --sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { box-sizing: border-box; } +html { min-width: 320px; background: var(--night); } +body { margin: 0; min-width: 320px; overflow-x: hidden; color: var(--white); font-family: var(--sans); background: var(--night); } +button, input { font: inherit; } +button { color: inherit; cursor: pointer; } +button:focus-visible, input:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; } +.game-shell { position: relative; min-height: 100vh; isolation: isolate; padding: 14px 15px 10px; background: #050e1b; overflow: hidden; } +.game-shell::before { content: ""; position: fixed; z-index: -4; inset: 0; pointer-events: none; background-image: radial-gradient(circle at 50% 15%, rgba(33, 91, 129, .2), transparent 38%), linear-gradient(180deg, #07182a 0, #050d18 58%, #071322 100%); } +.game-shell::after { content: ""; position: fixed; z-index: -3; inset: 0; pointer-events: none; opacity: .34; background-image: radial-gradient(circle, rgba(102, 198, 255, .8) 0 1px, transparent 1.5px); background-size: 113px 137px; background-position: 25px 18px; } +.ambient { position: fixed; z-index: -2; width: 390px; height: 390px; border-radius: 50%; pointer-events: none; filter: blur(46px); opacity: .16; }.ambient-one { top: 6%; left: 31%; background: #216cbb; }.ambient-two { right: -90px; bottom: 8%; background: #8e2aa8; } +.panel-glass { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 16px 36px rgba(0, 0, 0, .28), inset 0 1px 0 rgba(118, 215, 255, .08); backdrop-filter: blur(10px); } +.topbar { position: relative; z-index: 5; display: grid; grid-template-columns: 230px minmax(260px, 1fr) 260px; align-items: center; gap: 30px; width: min(1500px, 100%); margin: 0 auto; padding: 0 2px 13px; } +.brand-lockup { display: flex; align-items: center; gap: 11px; text-transform: uppercase; }.brand-lockup > span:last-child { display: grid; gap: 2px; }.brand-lockup strong { color: var(--cyan); font: 700 19px var(--mono); letter-spacing: -.04em; text-shadow: 0 0 16px rgba(100, 244, 225, .45); }.brand-lockup small { color: var(--muted); font: 8px var(--mono); letter-spacing: .18em; }.brand-mark { width: 32px; height: 32px; display: grid; place-items: center; color: var(--cyan); border: 1px solid var(--cyan); border-radius: 6px; font-size: 20px; box-shadow: 0 0 16px rgba(100, 244, 225, .22); } +.repo-search-wrap { position: relative; display: flex; align-items: center; }.repo-search-wrap label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); }.repo-search-wrap input { width: 100%; height: 34px; padding: 0 44px 0 16px; color: var(--white); border: 1px solid #355675; border-radius: 8px; background: rgba(6, 14, 25, .72); box-shadow: inset 0 0 12px rgba(31, 120, 191, .1); }.repo-search-wrap input::placeholder { color: #7c9ab5; }.repo-search-wrap kbd { position: absolute; right: 7px; color: var(--cyan); font: 14px var(--mono); pointer-events: none; }.topbar-actions { display: flex; align-items: center; justify-content: flex-end; gap: 11px; }.simulated-pill { display: flex; align-items: center; gap: 7px; color: var(--green); font: 8px var(--mono); letter-spacing: .06em; }.simulated-pill i, .live-dot { width: 7px; height: 7px; display: inline-block; border-radius: 50%; background: var(--green); box-shadow: 0 0 10px var(--green); }.icon-button { width: 29px; height: 29px; padding: 0; border: 1px solid var(--line-hot); border-radius: 6px; background: rgba(9, 27, 45, .8); color: var(--cyan); font-size: 17px; line-height: 1; }.icon-button:hover { border-color: var(--cyan); background: rgba(27, 79, 102, .7); } + +.left-rail { position: absolute; z-index: 4; top: 72px; left: 15px; width: 254px; padding: 17px 18px; }.panel-title { display: grid; gap: 1px; padding-bottom: 12px; border-bottom: 1px solid rgba(61, 141, 177, .35); }.section-kicker { color: var(--cyan); font: 10px var(--mono); letter-spacing: .14em; }.panel-title h1 { margin: 2px 0 0; color: var(--white); font: 700 19px var(--mono); letter-spacing: .02em; }.panel-title i { width: 56px; height: 2px; margin-top: 9px; background: var(--cyan); box-shadow: 0 0 8px var(--cyan); }.welcome-copy { margin: 14px 0 0; color: #d2e3ed; font-size: 14px; line-height: 1.45; }.welcome-copy.muted { margin-top: 6px; color: var(--muted); font-size: 12px; }.welcome-copy strong { color: var(--cyan); }.rail-divider { height: 1px; margin: 16px 0 13px; background: var(--line); }.component-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }.component-heading > span { color: var(--cyan); font: 12px var(--mono); letter-spacing: .05em; }.component-heading small { color: #6d8da7; font: 8px var(--mono); }.component-list { display: grid; gap: 9px; margin: 13px 0 0; padding: 0; list-style: none; }.component-list li { display: flex; align-items: center; gap: 9px; color: #bfd3df; font-size: 12px; }.component-list li b { width: 20px; height: 20px; display: grid; place-items: center; color: var(--cyan); border: 1px solid #2e7798; border-radius: 5px; font-size: 13px; }.component-list strong { color: var(--white); }.how-it-works { margin-top: 18px; padding-top: 13px; border-top: 1px solid var(--line); }.how-it-works ol { display: grid; gap: 5px; margin: 10px 0 0; padding-left: 21px; color: var(--muted); font: 10px/1.4 var(--mono); } + +.world-stage { position: relative; min-height: 720px; width: min(1500px, 100%); margin: 0 auto; padding: 9px 287px 0 274px; }.stage-heading { position: relative; z-index: 3; display: flex; justify-content: space-between; align-items: flex-end; gap: 14px; padding: 5px 18px 10px; }.stage-heading h2 { margin: 4px 0 0; color: var(--white); font: 700 21px var(--mono); letter-spacing: -.03em; text-shadow: 0 0 19px rgba(148, 218, 255, .28); }.stage-heading h2 b { color: var(--cyan); font-weight: 500; }.stage-state { display: flex; gap: 10px; color: var(--green); font: 9px var(--mono); }.stage-state span + span { padding-left: 10px; border-left: 1px solid var(--line); color: var(--muted); }.world-viewport { position: relative; height: 690px; overflow: hidden; border: 1px solid #195174; border-radius: 13px; background: #071725; box-shadow: inset 0 0 50px rgba(5, 43, 69, .55), 0 20px 50px rgba(0, 0, 0, .34); }.world-viewport::after { content: ""; position: absolute; z-index: 3; inset: 0; pointer-events: none; border: 1px solid rgba(117, 212, 255, .08); border-radius: 12px; box-shadow: inset 0 0 35px rgba(0, 0, 0, .62); } +.world-canvas { position: absolute; width: 1600px; height: 960px; transform-origin: 0 0; transition: transform .25s ease; }.world-ground { position: absolute; inset: 0; overflow: hidden; background: linear-gradient(180deg, #0b2435 0 40%, #102f3b 40% 46%, #132c31 46% 100%); }.world-ground::before { content: ""; position: absolute; left: -8%; right: -8%; bottom: -25%; height: 64%; transform: perspective(540px) rotateX(57deg); transform-origin: bottom; background-image: linear-gradient(rgba(49, 129, 134, .25) 1px, transparent 1px), linear-gradient(90deg, rgba(49, 129, 134, .22) 1px, transparent 1px); background-size: 42px 42px; opacity: .58; }.world-ground::after { content: ""; position: absolute; inset: 0; pointer-events: none; background-image: radial-gradient(circle at 15% 22%, rgba(155, 235, 255, .8) 0 1px, transparent 1.5px); background-size: 87px 73px; opacity: .55; }.ground-grid { position: absolute; inset: 0; background: linear-gradient(105deg, transparent 49.7%, rgba(36, 117, 126, .26) 50%, transparent 50.3%); opacity: .28; }.distant-skyline { position: absolute; z-index: 0; top: 74px; left: 0; right: 0; height: 220px; display: flex; align-items: flex-end; justify-content: space-around; padding: 0 65px; opacity: .8; }.distant-skyline i { display: block; width: 43px; height: 120px; background: #0d2d42; border: 1px solid #1d6383; box-shadow: 9px -24px 0 -8px #0d2c42, inset 9px 16px 0 -8px rgba(114, 218, 255, .28); }.distant-skyline i:nth-child(2n) { height: 172px; width: 66px; }.distant-skyline i:nth-child(3n) { height: 92px; background: #112949; }.moon-orb { position: absolute; z-index: 0; top: 36px; right: 210px; width: 56px; height: 56px; border-radius: 50%; background: #98cbdf; box-shadow: 0 0 36px rgba(100, 216, 255, .45); opacity: .8; }.moon-orb::after { content: ""; position: absolute; inset: 9px 13px 13px 7px; border-radius: 50%; box-shadow: 20px 2px 0 -14px #5a8caa, 10px 27px 0 -13px #6598ac; } +.road { position: absolute; z-index: 1; background: #183e45; border: 2px solid #3b6c67; box-shadow: inset 0 0 0 3px rgba(5, 25, 35, .35), 0 0 22px rgba(32, 153, 157, .12); }.road span { position: absolute; color: rgba(116, 219, 211, .5); font: 10px var(--mono); letter-spacing: .2em; }.road-main { left: -120px; top: 531px; width: 1850px; height: 111px; transform: rotate(-2deg); }.road-main span { top: 47px; left: 215px; }.road-cross { left: 657px; top: 232px; width: 122px; height: 610px; transform: rotate(16deg); }.road-cross span { top: 300px; left: 27px; transform: rotate(-90deg); }.road-branch { left: 975px; top: 406px; width: 700px; height: 73px; transform: rotate(26deg); border-color: #6b5687; background: #2b3145; }.road-branch span { top: 27px; left: 30px; color: rgba(204, 156, 255, .65); }.plaza { position: absolute; z-index: 2; left: 683px; top: 590px; width: 188px; height: 122px; border: 3px solid #a17d51; background: #4b4b3b; transform: rotate(45deg); box-shadow: 0 0 0 7px rgba(32, 68, 64, .7), inset 0 0 0 2px #c6a15f; }.plaza span { position: absolute; left: 45px; top: 48px; transform: rotate(-45deg); color: #f5d894; font: 11px var(--mono); white-space: nowrap; } + +.world-building { position: absolute; z-index: 3; left: var(--building-x); top: var(--building-y); width: var(--building-w); height: var(--building-h); padding: 0; border: 1px solid color-mix(in srgb, var(--district-accent) 65%, #1e475b); border-radius: 5px; background: #102531; box-shadow: 8px 13px 0 rgba(4, 13, 19, .45), 0 0 24px color-mix(in srgb, var(--district-accent) 24%, transparent), inset 0 0 0 3px rgba(5, 17, 27, .62); transform: perspective(440px) rotateX(4deg); transition: transform .2s ease, filter .2s ease, border-color .2s ease; overflow: hidden; text-align: left; }.world-building:hover, .world-building.is-selected { z-index: 5; filter: brightness(1.24); transform: perspective(440px) rotateX(4deg) translateY(-5px); border-color: var(--district-accent); }.world-building.is-search-hidden { opacity: .16; filter: grayscale(1); }.building-cap { position: absolute; top: -22px; left: 13px; width: calc(100% - 26px); height: 24px; border: 1px solid var(--district-accent); background: #152f3a; transform: skewX(-23deg); box-shadow: inset 0 0 14px color-mix(in srgb, var(--district-accent) 33%, transparent); }.building-facade { position: absolute; inset: 32px 11px 31px; display: grid; grid-template-columns: repeat(5, 1fr); grid-auto-rows: 16px; gap: 9px 10px; padding: 9px; background: repeating-linear-gradient(90deg, rgba(53, 111, 116, .16) 0 22px, transparent 22px 40px), #17313a; border: 1px solid rgba(98, 166, 168, .55); }.building-window { height: 9px; background: var(--district-accent); opacity: calc(.25 + (var(--construction) / 130)); box-shadow: 0 0 7px var(--district-accent); animation: window-blink 4s infinite alternate; animation-delay: calc(var(--window-index) * -140ms); }.building-sign { position: absolute; z-index: 1; top: 7px; left: 9px; right: 9px; min-height: 23px; display: flex; align-items: center; gap: 6px; padding: 3px 7px; color: var(--white); background: rgba(7, 20, 29, .92); border-left: 3px solid var(--district-accent); font: 11px var(--mono); letter-spacing: .03em; }.building-sign b { color: var(--district-accent); font-size: 18px; }.building-status { position: absolute; bottom: 7px; left: 12px; color: var(--district-accent); font: 9px var(--mono); letter-spacing: .1em; }.building-floor { position: absolute; left: -8px; right: -8px; bottom: -13px; height: 18px; background: #081a25; border-top: 2px solid var(--district-accent); transform: skewX(-22deg); }.building-issues { transform: perspective(440px) rotateX(4deg) rotate(-1deg); }.building-prs { box-shadow: 8px 13px 0 rgba(4, 13, 19, .45), 0 0 30px rgba(232, 135, 255, .34); }.building-prs .building-sign { color: var(--pink); }.construction-crane { position: absolute; z-index: 2; width: 13px; height: 236px; border-left: 5px solid var(--gold); opacity: .75; }.construction-crane i { position: absolute; top: 22px; left: 0; width: 145px; height: 5px; background: var(--gold); }.construction-crane b { position: absolute; top: 26px; left: 118px; width: 2px; height: 130px; background: var(--gold); }.construction-crane em { position: absolute; top: 151px; left: 109px; width: 20px; height: 20px; border: 2px solid var(--orange); border-radius: 50%; animation: crane-pulse 2s infinite; }.crane-one { left: 480px; top: 115px; transform: rotate(-2deg); }.crane-two { right: 173px; top: 107px; transform: scale(.82) rotate(3deg); } +.agent-bot { position: absolute; z-index: 5; width: 24px; height: 31px; border: 2px solid var(--green); background: #182d33; box-shadow: 0 0 14px rgba(159, 242, 84, .48); animation: bot-walk 4s ease-in-out infinite; }.agent-bot::after { content: ""; position: absolute; left: 4px; right: 4px; top: 6px; height: 7px; background: var(--green); box-shadow: 0 0 7px var(--green); }.agent-bot i, .agent-bot b { position: absolute; bottom: -8px; width: 4px; height: 8px; background: var(--green); }.agent-bot i { left: 2px; }.agent-bot b { right: 2px; }.bot-a { left: 763px; top: 437px; }.bot-b { left: 1086px; top: 453px; animation-delay: -2s; border-color: var(--gold); }.bot-b::after { background: var(--gold); box-shadow: 0 0 7px var(--gold); }.bot-b i, .bot-b b { background: var(--gold); }.quest-marker { position: absolute; z-index: 6; left: 1006px; top: 552px; width: 43px; height: 43px; display: grid; place-items: center; color: var(--gold); border: 4px solid var(--gold); border-radius: 50%; font: bold 28px var(--mono); box-shadow: 0 0 16px rgba(255, 201, 91, .6); animation: quest-pulse 1.8s infinite; }.pr-gate { position: absolute; z-index: 4; left: 695px; top: 415px; width: 130px; height: 76px; display: grid; place-items: center; align-content: center; gap: 6px; color: var(--pink); border: 3px solid var(--pink); border-radius: 5px; background: #2c173d; box-shadow: 0 0 23px rgba(232, 135, 255, .55), inset 0 0 16px rgba(232, 135, 255, .2); transform: perspective(240px) rotateX(8deg); }.pr-gate span { font: 9px var(--mono); }.pr-gate b { font-size: 11px; }.player-avatar { position: absolute; z-index: 8; width: 42px; height: 74px; transform: translate(-50%, -78%); transition: left .13s linear, top .13s linear; filter: drop-shadow(0 7px 3px rgba(0, 0, 0, .55)); }.avatar-shadow { position: absolute; left: 1px; bottom: -3px; width: 40px; height: 11px; border-radius: 50%; background: rgba(0, 0, 0, .48); }.avatar-legs { position: absolute; left: 9px; bottom: 5px; width: 24px; height: 28px; display: flex; gap: 3px; }.avatar-legs i { flex: 1; background: #152d4b; border: 2px solid #09192d; }.avatar-body { position: absolute; left: 7px; bottom: 25px; width: 29px; height: 29px; background: #171b24; border: 2px solid #060d19; }.avatar-body::after { content: "⌘"; position: absolute; inset: 2px; display: grid; place-items: center; color: var(--white); font-size: 17px; }.avatar-pack { position: absolute; z-index: -1; left: -7px; top: 2px; width: 11px; height: 22px; background: #3a2a20; border: 1px solid #9a6d38; }.avatar-head { position: absolute; left: 10px; top: 3px; width: 23px; height: 22px; background: #5f402d; border: 2px solid #09111c; border-radius: 3px 3px 8px 8px; }.avatar-head::before { content: ""; position: absolute; left: -2px; right: -2px; top: -3px; height: 9px; background: #171515; border-radius: 5px 5px 0 0; }.world-sign { position: absolute; z-index: 6; display: grid; gap: 2px; padding: 11px 15px; background: rgba(5, 20, 30, .91); border: 2px solid var(--cyan); box-shadow: 0 0 19px rgba(100, 244, 225, .22), inset 0 0 0 3px rgba(11, 56, 65, .7); transform: perspective(340px) rotateX(4deg); }.world-sign small { color: var(--green); font: 9px var(--mono); }.world-sign strong { color: var(--cyan); font: 17px var(--mono); letter-spacing: .06em; }.world-sign b { color: #bfe6ee; font: 13px var(--mono); }.world-sign em { color: var(--gold); font: 10px var(--mono); font-style: normal; }.repo-sign { left: 631px; top: 155px; width: 255px; transform: perspective(340px) rotateX(4deg) rotate(-1deg); }.build-sign { right: 88px; top: 201px; width: 260px; border-color: var(--green); }.build-sign strong { color: var(--green); }.build-sign b { color: var(--white); } + +.nearby-prompt { position: absolute; z-index: 5; left: 300px; bottom: 65px; display: grid; gap: 2px; min-width: 192px; padding: 9px 13px; border-left: 2px solid var(--cyan); background: rgba(5, 19, 30, .84); }.nearby-prompt span, .nearby-prompt small { color: var(--muted); font: 9px var(--mono); letter-spacing: .1em; }.nearby-prompt strong { color: var(--cyan); font: 13px var(--mono); }.nearby-prompt.is-ready { border-color: var(--gold); }.nearby-prompt.is-ready strong { color: var(--gold); }.city-reticle { position: absolute; z-index: 4; left: 50%; top: 53%; width: 45px; height: 45px; transform: translate(-50%, -50%); pointer-events: none; opacity: .35; }.city-reticle i, .city-reticle b { position: absolute; display: block; background: var(--cyan); }.city-reticle i { left: 21px; top: 0; width: 1px; height: 45px; }.city-reticle b { left: 0; top: 21px; width: 45px; height: 1px; }.movement-help { position: absolute; z-index: 5; right: 306px; bottom: 63px; display: flex; align-items: center; gap: 5px; color: var(--muted); font: 9px var(--mono); }.movement-help kbd, .terminal-form input, .repo-search-wrap kbd { font-family: var(--mono); }.movement-help kbd { padding: 3px 5px; border: 1px solid #38556a; border-radius: 4px; color: var(--white); background: #112334; }.movement-help span { margin-right: 6px; } +.feedback-line { position: absolute; z-index: 5; left: 300px; right: 306px; bottom: 14px; display: flex; align-items: center; gap: 8px; min-height: 29px; padding: 0 11px; border-top: 1px solid #1d4d64; background: rgba(5, 17, 27, .82); color: var(--muted); font: 10px var(--mono); }.feedback-line i { width: 6px; height: 6px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px var(--green); }.feedback-line time { margin-left: auto; color: var(--cyan); } + +.right-rail { position: absolute; z-index: 4; top: 72px; right: 15px; width: 276px; display: grid; gap: 10px; }.map-panel, .agents-panel, .event-panel { padding: 12px; }.panel-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-bottom: 9px; color: var(--cyan); border-bottom: 1px solid rgba(61, 141, 177, .35); font: 11px var(--mono); letter-spacing: .08em; }.panel-heading > span { display: flex; align-items: center; gap: 7px; }.panel-heading small { color: var(--muted); font: 8px var(--mono); }.count-badge { display: inline-grid; place-items: center; width: 21px; height: 17px; color: var(--green); border: 1px solid var(--green); border-radius: 4px; font: 9px var(--mono); }.minimap-viewport { position: relative; height: 153px; margin: 11px 0 9px; overflow: hidden; border: 1px solid #22536e; border-radius: 6px; background: #1c3b2b; }.minimap-grid { position: absolute; inset: 0; background-color: #173b2e; background-image: linear-gradient(25deg, transparent 40%, rgba(19, 75, 63, .75) 41% 47%, transparent 48%), repeating-linear-gradient(90deg, rgba(91, 169, 90, .23) 0 3px, transparent 3px 17px), repeating-linear-gradient(0deg, rgba(70, 131, 71, .2) 0 3px, transparent 3px 21px); }.map-road { position: absolute; background: #7a7251; opacity: .8; }.map-road-a { left: -12%; top: 52%; width: 125%; height: 7px; transform: rotate(-15deg); }.map-road-b { left: 42%; top: -12%; width: 8px; height: 125%; transform: rotate(24deg); }.minimap-district { position: absolute; width: 13px; height: 13px; padding: 0; border: 2px solid #081a24; transform: translate(-50%, -50%); box-shadow: 0 0 5px currentColor; }.minimap-district:hover { transform: translate(-50%, -50%) scale(1.4); }.map-player { position: absolute; left: 50%; top: 50%; width: 0; height: 0; border-right: 7px solid transparent; border-bottom: 16px solid white; border-left: 7px solid transparent; filter: drop-shadow(0 0 4px var(--cyan)); }.map-caption { display: flex; justify-content: space-between; color: var(--muted); font: 9px var(--mono); }.map-caption b { color: var(--cyan); }.map-legend { display: flex; justify-content: space-between; color: var(--muted); font: 8px var(--mono); }.map-legend i { display: inline-block; width: 6px; height: 6px; margin-right: 3px; border-radius: 50%; }.legend-player { background: white; }.legend-building { background: var(--green); }.legend-road { background: #bca46b; }.agent-list { display: grid; gap: 10px; margin: 12px 0; padding: 0; list-style: none; }.agent-row { display: grid; grid-template-columns: 27px 1fr auto; gap: 7px; align-items: center; min-width: 0; }.agent-avatar { width: 26px; height: 26px; display: grid; place-items: center; color: var(--agent-color); border: 1px solid var(--agent-color); border-radius: 5px; font: 9px var(--mono); }.agent-copy { display: grid; gap: 1px; min-width: 0; }.agent-copy strong { overflow: hidden; color: var(--white); font: 9px var(--mono); text-overflow: ellipsis; white-space: nowrap; }.agent-copy small { color: var(--muted); font: 8px var(--mono); }.agent-copy em { overflow: hidden; color: var(--agent-color); font: 8px var(--mono); font-style: normal; text-overflow: ellipsis; white-space: nowrap; }.progress-line { display: block; height: 3px; margin-top: 2px; background: #173040; }.progress-line b { display: block; height: 100%; background: var(--agent-color); box-shadow: 0 0 6px var(--agent-color); transition: width .4s ease; }.agent-state { color: var(--green); font: 8px var(--mono); }.swarm-flow { display: grid; justify-items: center; gap: 3px; padding-top: 9px; border-top: 1px solid var(--line); color: var(--muted); font: 8px var(--mono); }.swarm-flow span:first-of-type { color: var(--cyan); }.swarm-flow i { color: var(--line-hot); font-style: normal; }.event-panel { max-height: 205px; overflow: hidden; }.event-feed { display: grid; gap: 7px; margin: 11px 0 0; padding: 0; overflow: auto; list-style: none; }.event-row { display: grid; grid-template-columns: 52px 8px 1fr; gap: 6px; align-items: start; color: var(--muted); font-size: 9px; }.event-row time { color: #668aa3; font: 8px var(--mono); }.event-row p { margin: 0; line-height: 1.3; }.event-dot { width: 6px; height: 6px; margin-top: 3px; border-radius: 50%; background: var(--green); box-shadow: 0 0 7px var(--green); }.event-repository-issue .event-dot, .event-repository-pull-request .event-dot { background: var(--orange); box-shadow: 0 0 7px var(--orange); }.event-build-started .event-dot, .event-deployment-started .event-dot { background: var(--gold); box-shadow: 0 0 7px var(--gold); } + +.inspector-drawer { position: absolute; z-index: 7; top: 385px; right: 307px; width: 330px; min-height: 300px; padding: 15px; opacity: 0; pointer-events: none; transform: translateX(24px); transition: opacity .22s ease, transform .22s ease; }.inspector-drawer.is-open { opacity: 1; pointer-events: auto; transform: translateX(0); }.drawer-heading { display: flex; justify-content: space-between; gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }.drawer-heading div { display: grid; gap: 3px; }.drawer-heading strong { color: var(--white); font: 13px var(--mono); }.inspector-content { min-height: 215px; padding: 14px 0 8px; }.inspect-header { display: grid; gap: 6px; }.inspect-header h2 { margin: 0; color: var(--white); font: 17px var(--mono); }.inspect-header p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.45; }.district-metrics { display: flex; flex-wrap: wrap; gap: 5px 11px; margin-top: 5px; color: var(--cyan); font: 9px var(--mono); }.district-metrics span + span { color: var(--muted); }.inspector-actions { display: flex; flex-wrap: wrap; gap: 7px; margin: 15px 0 10px; }.panel-action { padding: 7px 9px; border: 1px solid var(--line-hot); border-radius: 5px; background: #0c2435; color: var(--cyan); font: 9px var(--mono); }.panel-action:hover { border-color: var(--cyan); }.panel-action.primary { border-color: var(--cyan); background: rgba(43, 111, 117, .3); }.panel-action:disabled { cursor: not-allowed; opacity: .48; }.mini-file-list, .file-list, .commit-list, .issue-list, .pr-list { display: grid; gap: 5px; }.mini-file-list button, .file-row, .commit-row, .issue-row, .pr-row { display: grid; grid-template-columns: 20px 1fr auto; gap: 7px; align-items: center; padding: 6px 7px; border: 1px solid transparent; background: rgba(11, 37, 51, .72); color: var(--muted); text-align: left; }.mini-file-list button:hover, .file-row:hover, .file-row.is-active, .commit-row:hover, .issue-row:hover, .pr-row:hover { border-color: var(--line-hot); color: var(--white); }.mini-file-list b, .file-row b { color: var(--cyan); }.file-row strong, .commit-row strong, .issue-row strong, .pr-row strong { display: block; overflow: hidden; color: var(--white); font: 10px var(--mono); text-overflow: ellipsis; white-space: nowrap; }.file-row small, .commit-row small, .issue-row small, .pr-row small { display: block; color: var(--muted); font-size: 9px; }.collection-heading { display: grid; gap: 4px; margin-bottom: 10px; }.collection-heading span { color: var(--cyan); font: 9px var(--mono); letter-spacing: .1em; }.collection-heading strong { color: var(--white); font: 14px var(--mono); }.collection-heading small { color: var(--muted); font-size: 10px; }.commit-row, .issue-row, .pr-row { grid-template-columns: 15px 1fr auto; }.commit-node, .issue-marker, .pr-marker { width: 9px; height: 9px; display: grid; place-items: center; color: var(--green); border: 1px solid var(--green); border-radius: 50%; font: 8px var(--mono); }.issue-marker { color: var(--orange); border-color: var(--orange); }.pr-marker { color: var(--pink); border-color: var(--pink); border-radius: 2px; }.commit-row em, .issue-row em, .pr-row em { color: var(--green); font: 8px var(--mono); font-style: normal; }.issue-row em { color: var(--orange); }.pr-row em { color: var(--pink); }.code-preview { max-height: 145px; overflow: auto; margin: 13px 0; padding: 11px; color: var(--green); border: 1px solid #244f52; background: #07151c; font: 10px/1.45 var(--mono); white-space: pre-wrap; }.build-stack { display: grid; gap: 9px; color: var(--muted); font: 10px var(--mono); }.build-stack > div:not(.pipeline-steps) { display: flex; justify-content: space-between; gap: 10px; }.build-stack b { color: var(--green); font: 10px var(--mono); }.status-running, .status-launching { color: var(--gold) !important; }.pipeline-steps { display: flex; gap: 4px; padding-top: 10px; border-top: 1px solid var(--line); }.pipeline-steps i { flex: 1; padding: 5px 2px; border: 1px solid #27404c; color: #5e7a8a; font: 8px var(--mono); font-style: normal; text-align: center; }.pipeline-steps i.done { border-color: #3a764e; color: var(--green); }.pipeline-steps i.active { border-color: var(--cyan); color: var(--cyan); } +.drawer-tabs { display: flex; gap: 4px; padding-top: 9px; border-top: 1px solid var(--line); }.drawer-tabs button { flex: 1; padding: 5px 2px; border: 1px solid #234456; background: transparent; color: var(--muted); font: 8px var(--mono); }.drawer-tabs button:hover { color: var(--cyan); border-color: var(--cyan); } + +.terminal-phone { position: fixed; z-index: 10; right: 22px; bottom: 91px; width: 300px; min-height: 405px; padding: 17px 12px 13px; border: 3px solid #2e3c48; border-radius: 27px; background: #060b10; box-shadow: 0 18px 50px rgba(0,0,0,.65), 0 0 0 4px rgba(1, 6, 10, .75), inset 0 0 22px rgba(96, 177, 211, .08); opacity: 0; pointer-events: none; transform: translateY(14px) rotate(1deg); transition: opacity .25s ease, transform .25s ease; }.terminal-phone.is-open { opacity: 1; pointer-events: auto; transform: translateY(0) rotate(1deg); }.phone-speaker { position: absolute; top: 8px; left: 50%; width: 55px; height: 4px; transform: translateX(-50%); border-radius: 5px; background: #29343d; }.terminal-top { display: flex; justify-content: space-between; align-items: center; padding: 3px 4px 11px; color: #d3e6eb; font: 10px var(--mono); }.terminal-top .icon-button { width: 22px; height: 22px; }.terminal-screen { min-height: 344px; padding: 11px; border: 1px solid #213844; border-radius: 7px; background: #020608; }.terminal-header { display: grid; gap: 4px; padding-bottom: 10px; border-bottom: 1px solid #1c303b; }.terminal-header strong { color: var(--white); font: 13px var(--mono); }.terminal-header small { color: var(--muted); font: 8px var(--mono); }.terminal-output { height: 222px; overflow: auto; padding-top: 9px; color: #8bd35e; font: 9px/1.5 var(--mono); }.terminal-line.command { color: var(--cyan); }.terminal-form { display: flex; gap: 5px; align-items: center; padding: 8px 0; border-top: 1px solid #1c303b; color: var(--green); font: 10px var(--mono); }.terminal-form input { flex: 1; min-width: 0; padding: 3px; border: 0; outline: 0; background: transparent; color: var(--white); font: 10px var(--mono); }.terminal-form button { border: 0; background: transparent; color: var(--cyan); font-size: 17px; }.terminal-shortcuts { display: flex; flex-wrap: wrap; gap: 4px; }.terminal-shortcuts button { padding: 4px 5px; border: 1px solid #244655; border-radius: 3px; background: #091a25; color: var(--muted); font: 8px var(--mono); }.terminal-shortcuts button:hover { color: var(--cyan); border-color: var(--cyan); }.phone-home { width: 54px; height: 4px; margin: 11px auto 0; border-radius: 5px; background: #29343d; } +.toolbelt { position: absolute; z-index: 6; left: 50%; bottom: 61px; display: grid; grid-template-columns: repeat(8, 1fr); width: min(560px, calc(100% - 620px)); transform: translateX(-50%); overflow: hidden; }.tool-slot { min-width: 0; display: grid; place-items: center; gap: 2px; padding: 7px 4px 5px; border: 0; border-right: 1px solid #29495b; background: rgba(6, 18, 29, .94); color: var(--muted); }.tool-slot:last-child { border-right: 0; }.tool-slot:hover, .tool-slot.is-active { background: rgba(16, 57, 69, .96); color: var(--tool-color); box-shadow: inset 0 -2px 0 var(--tool-color); }.tool-slot b { color: var(--tool-color); font-size: 24px; line-height: 1; text-shadow: 0 0 9px var(--tool-color); }.tool-slot strong { overflow: hidden; max-width: 100%; color: var(--white); font: 8px var(--mono); text-align: center; text-overflow: ellipsis; white-space: nowrap; }.tool-slot small { color: var(--muted); font: 7px var(--mono); }.tool-key { position: absolute; margin: 0 0 -49px 0; color: #6a8ca3; font: 8px var(--mono); } +.software-shelf { position: relative; z-index: 8; width: min(1500px, 100%); margin: 8px auto 0; padding: 10px 14px 12px; border: 1px solid #17415b; border-radius: 10px; background: rgba(5, 17, 29, .92); }.shelf-title { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }.shelf-title span { color: var(--white); font: 700 13px var(--mono); letter-spacing: .04em; }.shelf-title small { color: var(--cyan); font: 8px var(--mono); }.object-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 6px; }.object-card { min-height: 105px; display: grid; place-items: center; align-content: center; gap: 3px; padding: 7px 4px; border: 1px solid #1b4056; border-radius: 6px; background: #071521; }.object-card:hover { border-color: var(--cyan); background: #0d2635; transform: translateY(-2px); }.object-card strong { color: var(--white); font: 8px var(--mono); text-align: center; }.object-card small { color: var(--muted); font: 8px var(--mono); }.object-art { display: grid; place-items: center; width: 51px; height: 58px; color: var(--cyan); font-size: 42px; text-shadow: 0 0 13px currentColor; }.hammer-art { color: var(--orange); }.backpack-art { color: #ff7561; }.box-art { color: var(--blue); }.tablet-art { color: var(--pink); }.key-art { color: var(--gold); }.vault-art { color: #a9b1b7; }.rocket-art { color: var(--orange); }.touch-controls { display: none; }.disclaimer { position: relative; z-index: 7; display: flex; justify-content: space-between; width: min(1500px, 100%); margin: 9px auto 0; color: #617f94; font: 8px var(--mono); letter-spacing: .08em; } + +@keyframes window-blink { 0%, 77% { opacity: .45; } 86% { opacity: 1; } 92%, 100% { opacity: .58; } } +@keyframes quest-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.09); box-shadow: 0 0 28px rgba(255, 201, 91, .88); } } +@keyframes crane-pulse { 0%, 100% { opacity: .35; transform: scale(.7); } 50% { opacity: 1; transform: scale(1.1); } } +@keyframes bot-walk { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(22px, -4px); } } + +@media (max-width: 1240px) { + .left-rail { width: 222px; }.right-rail { width: 245px; }.world-stage { padding-left: 235px; padding-right: 255px; }.world-viewport { height: 650px; }.toolbelt { width: min(520px, calc(100% - 500px)); }.inspector-drawer { right: 262px; }.feedback-line { left: 260px; right: 260px; }.movement-help { right: 260px; }.nearby-prompt { left: 260px; } +} + +@media (max-width: 980px) { + .topbar { grid-template-columns: 1fr minmax(230px, 1fr) auto; gap: 12px; }.left-rail { position: relative; top: auto; left: auto; width: min(100%, 650px); margin: 0 auto 10px; }.left-rail .how-it-works { display: none; }.world-stage { min-height: 600px; padding: 0; }.world-viewport { height: 650px; }.right-rail { position: relative; top: auto; right: auto; width: 100%; grid-template-columns: 1fr 1fr 1fr; margin: 10px auto 0; }.event-panel { max-height: 255px; }.inspector-drawer { position: fixed; top: auto; right: 15px; bottom: 160px; width: min(360px, calc(100% - 30px)); }.toolbelt { position: relative; left: auto; bottom: auto; width: min(640px, 100%); margin: 10px auto 0; transform: none; }.software-shelf { margin-top: 10px; }.feedback-line { left: 0; right: 0; bottom: 0; }.movement-help { right: 12px; bottom: 42px; }.nearby-prompt { left: 12px; bottom: 42px; }.disclaimer { margin-top: 8px; }.world-stage .stage-heading { padding-inline: 2px; } +} + +@media (max-width: 680px) { + .game-shell { padding: 10px 9px 9px; }.topbar { grid-template-columns: 1fr auto; }.repo-search-wrap { grid-column: 1 / -1; grid-row: 2; }.simulated-pill { display: none; }.brand-lockup strong { font-size: 17px; }.stage-heading { align-items: flex-start; flex-direction: column; gap: 6px; }.stage-heading h2 { font-size: 17px; }.world-viewport { height: 500px; }.world-canvas { transform: scale(.7) !important; transform-origin: 0 0; }.right-rail { grid-template-columns: 1fr; }.map-panel, .agents-panel { min-height: 0; }.event-panel { max-height: 210px; }.world-stage { min-height: 550px; }.world-stage .movement-help { display: none; }.nearby-prompt { bottom: 42px; }.toolbelt { grid-template-columns: repeat(4, 1fr); }.tool-slot { min-height: 54px; }.tool-slot b { font-size: 20px; }.tool-key { margin-bottom: -39px; }.object-grid { grid-template-columns: repeat(4, 1fr); }.object-card { min-height: 90px; }.object-art { width: 40px; height: 43px; font-size: 30px; }.shelf-title { align-items: flex-start; flex-direction: column; gap: 3px; }.disclaimer { flex-direction: column; gap: 4px; }.terminal-phone { right: 14px; bottom: 130px; width: min(300px, calc(100% - 28px)); }.inspector-drawer { right: 9px; bottom: 128px; width: calc(100% - 18px); }.left-rail { padding: 14px; }.component-list { grid-template-columns: 1fr 1fr; }.component-list li { font-size: 10px; }.component-list li b { width: 17px; height: 17px; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; } +} + +/* Browser-native opening scene and reality plates. These are local assets from + phase2-unreal/ReferenceSource, so the playable build stays self-contained. */ +.opening-scene { position: fixed; z-index: 100; inset: 0; display: grid; place-items: end start; overflow: hidden; background: #02050d; transition: opacity .55s ease, visibility .55s ease; } +.opening-scene.is-dismissed { opacity: 0; visibility: hidden; pointer-events: none; } +.opening-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; background: #050816; filter: saturate(1.15) contrast(1.05) brightness(.72); } +.opening-vignette { position: absolute; inset: 0; pointer-events: none; background: radial-gradient(circle at 55% 44%, transparent 0 24%, rgba(4, 5, 18, .22) 54%, rgba(2, 4, 12, .92) 100%), linear-gradient(90deg, rgba(2, 5, 13, .9), transparent 55%, rgba(2, 5, 13, .35)); } +.opening-copy { position: relative; z-index: 2; width: min(630px, calc(100% - 48px)); margin: 0 0 11vh 7vw; padding: 24px 28px 27px; border: 1px solid rgba(125, 232, 255, .35); border-radius: 16px; background: linear-gradient(135deg, rgba(4, 14, 32, .86), rgba(20, 10, 46, .72)); box-shadow: 0 0 42px rgba(32, 145, 255, .18), inset 0 0 30px rgba(111, 73, 255, .1); backdrop-filter: blur(7px); } +.opening-copy h1 { margin: 6px 0 7px; color: #f5fbff; font: 800 clamp(50px, 10vw, 104px)/.86 var(--mono); letter-spacing: -.08em; text-shadow: 0 0 28px rgba(181, 140, 255, .75); } +.opening-copy p { margin: 0 0 18px; color: #d5e6f4; font-size: clamp(16px, 2vw, 22px); } +.opening-meta { display: flex; flex-wrap: wrap; gap: 9px 18px; color: #9ab6cf; font: 10px var(--mono); letter-spacing: .06em; } +.opening-meta i { display: inline-block; width: 7px; height: 7px; margin-right: 5px; border-radius: 50%; background: #91e85e; box-shadow: 0 0 12px #91e85e; } +.opening-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: 24px; } +.opening-enter, .opening-sound { border: 1px solid rgba(125, 232, 255, .55); border-radius: 8px; color: #effbff; background: rgba(24, 88, 128, .52); font: 700 12px var(--mono); letter-spacing: .06em; cursor: pointer; } +.opening-enter { padding: 14px 17px; box-shadow: 0 0 18px rgba(125, 232, 255, .2); } +.opening-enter b { margin-left: 14px; color: #7de8ff; font-size: 19px; } +.opening-sound { padding: 13px 14px; color: #abc1d7; background: rgba(5, 10, 25, .45); } +.opening-enter:hover, .opening-sound:hover { border-color: #b58cff; box-shadow: 0 0 22px rgba(181, 140, 255, .3); } +.opening-copy > small { display: block; margin-top: 13px; color: #7e9bb6; font: 10px var(--mono); } +.reality-deck { position: absolute; z-index: 5; right: 27px; bottom: 27px; left: 27px; padding: 9px; border: 1px solid rgba(125, 232, 255, .24); border-radius: 10px; background: linear-gradient(90deg, rgba(4, 15, 29, .9), rgba(8, 19, 41, .72)); box-shadow: 0 0 28px rgba(12, 89, 125, .16); backdrop-filter: blur(5px); } +.reality-deck-heading { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 0 3px 7px; color: #9edff4; font: 10px var(--mono); letter-spacing: .07em; } +.reality-deck-heading small { color: #6c8aa4; font-size: 8px; } +.reality-deck-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 7px; } +.reality-plate { display: block; width: 100%; height: 72px; object-fit: cover; border: 1px solid rgba(125, 232, 255, .22); border-radius: 6px; background: #081322; filter: saturate(1.16) contrast(1.04); } +@media (max-width: 680px) { + .opening-copy { width: calc(100% - 24px); margin: 0 12px 24px; padding: 18px; } + .opening-copy h1 { font-size: 53px; } + .reality-deck { right: 12px; bottom: 12px; left: 12px; } + .reality-deck-heading small { display: none; } + .reality-plate { height: 48px; } +} diff --git a/dist/ui-reality.js b/dist/ui-reality.js new file mode 100644 index 0000000000000000000000000000000000000000..ad113d9c9c7aa786f35a3598ef9488cd054a68ac --- /dev/null +++ b/dist/ui-reality.js @@ -0,0 +1,210 @@ +import { + COLLECTIONS, + EVENT_LABELS, + EVENT_TYPES, + TOOLBELT, + commandTranscript, + formatTime +} from './shared.js'; + +function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[character])); +} + +function byId(root, id) { + return root.getElementById(id); +} + +function setText(root, id, value) { + const element = byId(root, id); + if (element) element.textContent = value; +} + +function selectedDistrict(state) { + return state.districts.find((district) => district.id === state.selectedDistrictId) || state.districts[0]; +} + +function selectedFile(state) { + const district = selectedDistrict(state); + return district.files.find((file) => file.id === state.selectedFileId) || district.files[0]; +} + +function eventMessage(event) { + return event.detail?.message || EVENT_LABELS[event.type] || event.type; +} + +function renderAgents(root, state) { + const agentList = byId(root, 'agent-list'); + if (!agentList) return; + agentList.innerHTML = state.agents.map((agent) => `
  • + ${agent.name.slice(0, 2)}${escapeHtml(agent.name)}${escapeHtml(agent.role)}${escapeHtml(agent.task)}● ${escapeHtml(agent.status)} +
  • `).join(''); + setText(root, 'active-agent-count', String(state.agents.length)); +} + +function renderEvents(root, state) { + const feed = byId(root, 'event-feed'); + if (!feed) return; + feed.innerHTML = state.events.slice(0, 7).map((event) => `
  • ${escapeHtml(eventMessage(event))}

  • `).join(''); + setText(root, 'event-count', `${state.events.length} EVENTS`); +} + +function renderToolbelt(root, state) { + const toolbelt = byId(root, 'toolbelt'); + if (!toolbelt) return; + toolbelt.innerHTML = TOOLBELT.map((tool) => ``).join(''); +} + +function renderStatus(root, state) { + setText(root, 'repo-visibility', state.repository.visibility); + setText(root, 'repo-branch', `BRANCH / ${state.repository.branch}`); + setText(root, 'build-status', state.repository.build); + setText(root, 'test-status', state.repository.tests); + setText(root, 'deploy-status', state.repository.deployment); + setText(root, 'world-clock', formatTime(state.tick)); + setText(root, 'player-location', state.view === 'district' ? `INSIDE ${selectedDistrict(state).path}` : 'SPAWN PLAZA'); + setText(root, 'feedback-line', state.feedback); + const playerLevel = Math.min(99, 12 + Math.floor(state.tick / 3)); + setText(root, 'player-level', `LEVEL ${playerLevel} DEV`); + const xp = Math.min(100, 34 + state.tick * 2); + const xpBar = byId(root, 'xp-fill'); + if (xpBar) xpBar.style.width = `${xp}%`; + setText(root, 'xp-label', `${xp} / 100 XP`); +} + +function renderFiles(state) { + const district = selectedDistrict(state); + return `
    DIRECTORY${escapeHtml(district.path)}${escapeHtml(district.stat)}
    ${district.files.map((file) => ``).join('')}
    `; +} + +function renderCommits(state) { + const commits = state.repository.commits || []; + return `
    HISTORY ROADRECENT COMMITS${commits.length} simulated events
    ${commits.map((commit) => ``).join('')}
    `; +} + +function renderIssues(state) { + const issues = state.repository.issues || []; + return `
    QUEST BOARDOPEN ISSUESQuests waiting in the city
    ${issues.map((issue) => ``).join('')}
    `; +} + +function renderPullRequests(state) { + const prs = state.repository.pullRequests || []; + return `
    MERGE GATEPULL REQUESTSDoors into the next branch
    ${prs.map((pr) => ``).join('')}
    `; +} + +function renderBuild(state) { + return `
    AUTOMATION CENTERBUILD & TESTAll values are simulated browser-local signals
    BUILD #812${escapeHtml(state.repository.build)}
    INSPECTORS${escapeHtml(state.repository.tests)}
    PAGES RUNWAY${escapeHtml(state.repository.deployment)}
    SOURCECOMPILETESTDEPLOY
    `; +} + +function renderRecord(state) { + const record = state.selectedRecord; + if (!record) return renderBuild(state); + if (record.type === 'commit') { + const item = (state.repository.commits || []).find((candidate) => candidate.id === record.id); + return `
    COMMIT EVENT${escapeHtml(item?.hash || record.id)}Historical event made physical
    ${escapeHtml(item?.message || 'Commit selected')}

    This simulated commit is a construction event. The repository builder routes its changed files into the city.

    author · ${escapeHtml(item?.author || 'builder')}
    `; + } + if (record.type === 'issue') { + const item = (state.repository.issues || []).find((candidate) => candidate.id === record.id); + return `
    ISSUE QUEST${escapeHtml(item?.title || record.id)}${escapeHtml(item?.status || 'OPEN')}
    Quest objective

    ${escapeHtml(item?.detail || 'Inspect the open issue in the quest district.')}

    marker active · browser-local simulation
    `; + } + const item = (state.repository.pullRequests || []).find((candidate) => candidate.id === record.id); + return `
    PULL REQUEST DOOR${escapeHtml(item?.title || record.id)}${escapeHtml(item?.status || 'REVIEW')}
    Merge gate

    ${escapeHtml(item?.detail || 'The gate is waiting for review.')}

    checks passing · browser-local simulation
    `; +} + +function renderInspector(root, state, nearby) { + const panel = byId(root, 'inspector-content'); + if (!panel) return; + const district = selectedDistrict(state); + const file = selectedFile(state); + const collection = state.selectedCollection; + let content = ''; + if (state.activePanel === 'file' || (state.activePanel === 'collection' && collection === 'files')) { + content = `
    ${escapeHtml(file.kind)} OBJECT

    ${escapeHtml(file.name)}

    ${escapeHtml(file.detail)}

    ${escapeHtml(file.code)}
    `; + } else if (state.activePanel === 'record') { + content = renderRecord(state); + } else if (state.activePanel === 'collection') { + content = collection === 'commits' ? renderCommits(state) : collection === 'issues' ? renderIssues(state) : collection === 'prs' ? renderPullRequests(state) : renderBuild(state); + } else if (state.activePanel === 'inspector' || state.activePanel === 'world') { + const canEnter = nearby?.district?.id === district.id && nearby.inRange; + content = `
    ${state.view === 'district' ? 'CURRENT DISTRICT' : 'NEAREST DISTRICT'}

    ${escapeHtml(district.title)}

    ${escapeHtml(district.summary)}

    ${escapeHtml(district.path)}${escapeHtml(district.stat)}${Math.round((state.construction[district.id] || 0) * 100)}% BUILT
    ${district.files.slice(0, 3).map((item) => ``).join('')}
    `; + } else { + content = `
    ${escapeHtml(district.path)}

    Repository objects

    Select a building, tool, or activity to inspect the physical software around you.

    `; + } + panel.innerHTML = content; + const nearbyPrompt = byId(root, 'nearby-prompt'); + if (nearbyPrompt) { + nearbyPrompt.innerHTML = nearby?.district && nearby.inRange ? `NEARBY${escapeHtml(nearby.district.name)}PRESS E / ENTER TO ENTER` : `WALKABLE CITY${escapeHtml(state.player.facing.toUpperCase())}WASD · ARROWS TO NAVIGATE`; + nearbyPrompt.classList.toggle('is-ready', Boolean(nearby?.inRange)); + } +} + +function renderTerminal(root, state) { + const overlay = byId(root, 'terminal-phone'); + if (!overlay) return; + overlay.classList.toggle('is-open', state.terminal.open); + overlay.setAttribute('aria-hidden', String(!state.terminal.open)); + const output = byId(root, 'terminal-output'); + if (output) output.innerHTML = state.terminal.lines.map((line) => `
    ${escapeHtml(line)}
    `).join(''); + const input = byId(root, 'terminal-input'); + if (input && input.value !== state.terminal.command) input.value = ''; +} + +function renderSearch(root) { + const search = byId(root, 'repo-search'); + if (!search) return; + const query = search.value.trim().toLowerCase(); + root.querySelectorAll('.world-building').forEach((building) => { + building.classList.toggle('is-search-hidden', Boolean(query) && !building.textContent.toLowerCase().includes(query)); + }); +} + +export function renderHud(root, state, nearby) { + renderAgents(root, state); + renderEvents(root, state); + renderToolbelt(root, state); + renderStatus(root, state); + renderInspector(root, state, nearby); + renderTerminal(root, state); + renderSearch(root); + const drawer = byId(root, 'inspector-drawer'); + if (drawer) drawer.classList.toggle('is-open', state.activePanel !== 'world' && state.activePanel !== 'terminal'); + const shell = byId(root, 'game-shell'); + if (shell) shell.dataset.view = state.view; + setText(root, 'selected-tool-label', (TOOLBELT.find((tool) => tool.id === state.activeTool) || TOOLBELT[0]).name); +} + +export function bindUi(root, dispatch) { + root.addEventListener('click', (event) => { + const target = event.target.closest('[data-action], [data-district-id]'); + if (!target) return; + const action = target.dataset.action || (target.dataset.districtId ? 'select-district' : ''); + if (action === 'select-district') dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId: target.dataset.districtId }); + if (action === 'open-inspector') dispatch({ type: EVENT_TYPES.OPEN_INSPECTOR }); + if (action === 'enter-district') dispatch({ type: EVENT_TYPES.ENTER_DISTRICT, districtId: target.dataset.districtId }); + if (action === 'close-inspector') dispatch({ type: EVENT_TYPES.CLOSE_INSPECTOR }); + if (action === 'select-file') dispatch({ type: EVENT_TYPES.SELECT_FILE, fileId: target.dataset.fileId }); + if (action === 'select-collection') dispatch({ type: EVENT_TYPES.SELECT_COLLECTION, collection: target.dataset.collection }); + if (action === 'commit-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'commit', recordId: target.dataset.commitId }); + if (action === 'issue-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'issue', recordId: target.dataset.issueId }); + if (action === 'pr-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'pr', recordId: target.dataset.prId }); + if (action === 'move') dispatch({ type: EVENT_TYPES.PLAYER_MOVE, dx: target.dataset.dx, dy: target.dataset.dy }); + if (action === 'tool') { + const tool = TOOLBELT.find((item) => item.id === target.dataset.toolId); + dispatch({ type: EVENT_TYPES.USE_TOOL, toolId: target.dataset.toolId, message: tool?.verb ? `${tool.name}: ${tool.verb}.` : undefined }); + } + if (action === 'open-terminal') dispatch({ type: EVENT_TYPES.OPEN_TERMINAL }); + if (action === 'close-terminal') dispatch({ type: EVENT_TYPES.CLOSE_TERMINAL }); + if (action === 'command') dispatch({ type: EVENT_TYPES.RUN_COMMAND, command: target.dataset.command, lines: commandTranscript(target.dataset.command, window.gitWorldState || { selectedFileId: 'README.md' }) }); + }); + + root.addEventListener('submit', (event) => { + if (!event.target.matches('#terminal-form')) return; + event.preventDefault(); + const input = byId(root, 'terminal-input'); + const command = input?.value.trim() || ''; + if (command) dispatch({ type: EVENT_TYPES.RUN_COMMAND, command, lines: commandTranscript(command, window.gitWorldState || { selectedFileId: 'README.md' }) }); + }); + + const search = byId(root, 'repo-search'); + if (search) search.addEventListener('input', () => renderSearch(root)); +} diff --git a/dist/world-architect.js b/dist/world-architect.js new file mode 100644 index 0000000000000000000000000000000000000000..f7b03b2938be412d195cab79d2108fe56467b307 --- /dev/null +++ b/dist/world-architect.js @@ -0,0 +1,451 @@ +import { + clamp, + clone, + EVENT_TYPES, + makeEvent +} from './shared.js'; + +export const WALKABLE_BOUNDS = Object.freeze({ minX: 70, maxX: 1530, minY: 90, maxY: 850 }); + +export const DISTRICTS = Object.freeze([ + { + id: 'src', + name: 'SRC DISTRICT', + path: '/src', + title: 'Central Kernel Tower', + glyph: '⌘', + accent: '#62f2d2', + x: 565, + y: 230, + width: 255, + height: 220, + summary: 'The living source tree: modules, kernel code, and the repository entry point.', + stat: '42 files · 213 commits', + files: [ + { id: 'main-rs', name: 'main.rs', kind: 'Rust', size: '8.4 KB', detail: 'Repository bootstrap and agent scheduler.', code: 'fn main() {\n let world = GitWorld::spawn();\n world.walk();\n}' }, + { id: 'kernel-cu', name: 'kernel.cu', kind: 'CUDA', size: '24.1 KB', detail: 'The compute floor is under construction.', code: '__global__ void kernel_step(Agent agent) {\n agent.advance();\n}' }, + { id: 'lib-rs', name: 'lib.rs', kind: 'Rust', size: '12.7 KB', detail: 'Shared repository primitives and event contracts.', code: 'pub mod events;\npub mod districts;\npub mod builders;' }, + { id: 'mod-rs', name: 'mod.rs', kind: 'Rust', size: '2.1 KB', detail: 'District module registry.', code: 'pub const DISTRICTS: &[&str] = &[\n "src", "tests", "docs"\n];' } + ] + }, + { + id: 'tests', + name: 'TEST DISTRICT', + path: '/tests', + title: 'Test Factory', + glyph: '✓', + accent: '#91e85e', + x: 230, + y: 370, + width: 235, + height: 185, + summary: 'Inspection robots verify each build before the city opens another street.', + stat: '142 passing · 0 failing', + files: [ + { id: 'world-test', name: 'world.test.js', kind: 'Test', size: '9.7 KB', detail: 'Deterministic movement and event checks.', code: 'assert.equal(world.tick, 12);\nassert.equal(events.length, 4);' }, + { id: 'contracts-test', name: 'contracts.test.js', kind: 'Test', size: '6.2 KB', detail: 'Shared UI/world event contract checks.', code: 'assert.ok(EVENT_TYPES.PLAYER_MOVE);\nassert.ok(EVENT_TYPES.BUILD_COMPLETED);' }, + { id: 'fixtures', name: 'fixtures/', kind: 'Directory', size: '18 items', detail: 'Small simulated repository fixtures.', code: 'fixture("pax-coder");' } + ] + }, + { + id: 'docs', + name: 'DOCS LIBRARY', + path: '/docs', + title: 'Documentation Library', + glyph: '▤', + accent: '#ffcf68', + x: 1085, + y: 245, + width: 240, + height: 205, + summary: 'A warm archive of architecture decisions, guides, and the city map legend.', + stat: '18 guides · 96% coverage', + files: [ + { id: 'architecture', name: 'architecture.md', kind: 'Markdown', size: '14.2 KB', detail: 'The frontend event-driven architecture.', code: '# GitWorld architecture\nInput -> simulation -> state -> HUD' }, + { id: 'contributing', name: 'contributing.md', kind: 'Markdown', size: '4.8 KB', detail: 'How builders add a district.', code: '1. Choose a street\n2. Add a contract\n3. Run the inspectors' }, + { id: 'readme', name: 'README.md', kind: 'Markdown', size: '8.3 KB', detail: 'The repository welcome sign.', code: '# pax-coder\nA simulated repository world.' } + ] + }, + { + id: 'issues', + name: 'ISSUE QUESTS', + path: '/issues', + title: 'Issue Quest Board', + glyph: '!', + accent: '#ff9f58', + x: 865, + y: 470, + width: 215, + height: 160, + summary: 'Open issues become quests with markers, owners, and a clear next action.', + stat: '3 open quests · 1 in progress', + files: [ + { id: 'issue-201', name: '#201 bug in proof', kind: 'Issue', size: 'open', detail: 'Reproduce the proof boundary on the kernel floor.', code: 'status: open\nowner: proof-agent' }, + { id: 'issue-202', name: '#202 add test', kind: 'Issue', size: 'open', detail: 'Add a regression test for agent movement.', code: 'status: open\nowner: test-agent' }, + { id: 'issue-203', name: '#203 docs update', kind: 'Issue', size: 'queued', detail: 'Document the deploy launch sequence.', code: 'status: queued\nowner: doc-agent' } + ] + }, + { + id: 'prs', + name: 'PR GATE', + path: '/pulls', + title: 'Pull Request Gate', + glyph: '↔', + accent: '#d37cff', + x: 600, + y: 475, + width: 210, + height: 180, + summary: 'A glowing merge door: review the change before opening the next branch road.', + stat: '2 open · review required', + files: [ + { id: 'pr-88', name: '#88 merge kernel fix', kind: 'Pull request', size: 'review', detail: 'Two approvals still needed before merge.', code: 'checks: passing\nreviews: 1 / 3' }, + { id: 'pr-91', name: '#91 docs refresh', kind: 'Pull request', size: 'ready', detail: 'Ready to merge after the documentation quest.', code: 'checks: passing\nreviews: 3 / 3' } + ] + }, + { + id: 'actions', + name: 'ACTIONS FACTORY', + path: '/.github', + title: 'Automation Center', + glyph: '⚙', + accent: '#66a9ff', + x: 1250, + y: 485, + width: 235, + height: 195, + summary: 'CI factories turn commits into tested artifacts and light the deploy runway.', + stat: 'BUILD #812 · passing', + files: [ + { id: 'ci-yml', name: 'ci.yml', kind: 'Actions', size: '5.2 KB', detail: 'Build, test, and artifact inspection pipeline.', code: 'jobs:\n build:\n test:\n deploy:' }, + { id: 'pages-yml', name: 'pages.yml', kind: 'Actions', size: '2.4 KB', detail: 'Static Pages deployment path.', code: 'deploy-pages: enabled' }, + { id: 'release', name: 'release.yml', kind: 'Actions', size: '3.0 KB', detail: 'Packages the next city snapshot.', code: 'tag: v0.8.2' } + ] + } +]); + +const INITIAL_AGENTS = [ + { id: 'world-architect', name: 'WORLD ARCHITECT', role: 'World geometry', task: 'Building /src district', status: 'BUILDING', progress: 78, color: '#62f2d2' }, + { id: 'repository-builder', name: 'REPOSITORY BUILDER', role: 'Repository mapping', task: 'Mapping 42 files', status: 'MAPPING', progress: 64, color: '#ffcf68' }, + { id: 'reality-integrator', name: 'REALITY/UI INTEGRATOR', role: 'Interaction layer', task: 'Connecting terminal', status: 'WIRING', progress: 52, color: '#d37cff' } +]; + +const INITIAL_EVENTS = [ + { id: 'seed-1', type: EVENT_TYPES.COMMIT, tick: 0, at: '00:00:00', detail: { message: '8f3a9c1 Fix kernel bounds', districtId: 'src' } }, + { id: 'seed-2', type: EVENT_TYPES.AGENT_PROGRESS, tick: 0, at: '00:00:00', detail: { message: 'WORLD ARCHITECT placed a new tower window.', agentId: 'world-architect' } }, + { id: 'seed-3', type: EVENT_TYPES.TEST_COMPLETED, tick: 0, at: '00:00:00', detail: { message: '142 inspectors passed the street check.', districtId: 'tests' } } +]; + +const SIMULATION_SEQUENCE = [ + { type: EVENT_TYPES.AGENT_PROGRESS, detail: { agentId: 'world-architect', message: 'WORLD ARCHITECT raised the /src tower scaffold.' } }, + { type: EVENT_TYPES.AGENT_PROGRESS, detail: { agentId: 'repository-builder', message: 'REPOSITORY BUILDER mapped kernel.cu into the compute floor.' } }, + { type: EVENT_TYPES.CONSTRUCTION, detail: { districtId: 'src', amount: 0.03, message: 'A new source window flickers online.' } }, + { type: EVENT_TYPES.TEST_STARTED, detail: { districtId: 'tests', message: 'Inspection robots started a deterministic street sweep.' } }, + { type: EVENT_TYPES.TEST_COMPLETED, detail: { districtId: 'tests', message: '142 tests passed. The test district lights turn green.' } }, + { type: EVENT_TYPES.BUILD_STARTED, detail: { districtId: 'actions', message: 'BUILD #813 started in the automation factory.' } }, + { type: EVENT_TYPES.AGENT_PROGRESS, detail: { agentId: 'reality-integrator', message: 'REALITY/UI INTEGRATOR wired a contextual inspect panel.' } }, + { type: EVENT_TYPES.BUILD_COMPLETED, detail: { districtId: 'actions', message: 'BUILD #813 completed and artifacts are staged.' } }, + { type: EVENT_TYPES.COMMIT, detail: { districtId: 'src', message: '2h4d7e2 Add proof sketch' } }, + { type: EVENT_TYPES.ISSUE, detail: { districtId: 'issues', message: '#202 quest marker activated: add test' } }, + { type: EVENT_TYPES.PULL_REQUEST, detail: { districtId: 'prs', message: '#91 merge gate is ready for review.' } }, + { type: EVENT_TYPES.DEPLOYMENT_STARTED, detail: { districtId: 'actions', message: 'Deploy rocket is fueling on the Pages runway.' } }, + { type: EVENT_TYPES.DEPLOYMENT_COMPLETED, detail: { districtId: 'actions', message: 'Static city snapshot deployed to the local launch pad.' } } +]; + +export function createWorldState(seed = 20260828) { + return { + seed, + tick: 0, + repository: { + owner: 'SNAPKITTYWEST', + name: 'pax-coder', + branch: 'main', + visibility: 'Public', + simulated: true, + stars: 213, + forks: 45, + languages: ['Rust', 'CUDA', 'Markdown'], + build: 'PASSING', + tests: '142 PASS', + deployment: 'STAGED', + commits: [ + { id: '8f3a9c1', hash: '8f3a9c1', message: 'Fix kernel bounds', author: 'AhmadParr' }, + { id: '2h4d7e2', hash: '2h4d7e2', message: 'Add proof sketch', author: 'proof-agent' }, + { id: '9ac1bd9', hash: '9ac1bd9', message: 'Update docs', author: 'doc-agent' }, + { id: '7d0c812', hash: '7d0c812', message: 'Wire agent event bus', author: 'world-architect' } + ], + issues: [ + { id: '201', title: '#201 Bug in proof', detail: 'Reproduce the proof boundary on the kernel floor.', status: 'OPEN' }, + { id: '202', title: '#202 Add test', detail: 'Add a regression test for agent movement.', status: 'OPEN' }, + { id: '203', title: '#203 Docs update', detail: 'Document the deploy launch sequence.', status: 'QUEUED' } + ], + pullRequests: [ + { id: '88', title: '#88 Merge kernel fix', detail: 'Two approvals still needed before merge.', status: 'REVIEW' }, + { id: '91', title: '#91 Docs refresh', detail: 'Ready to merge after the documentation quest.', status: 'READY' } + ] + }, + districts: clone(DISTRICTS), + agents: clone(INITIAL_AGENTS), + events: clone(INITIAL_EVENTS), + construction: { src: 0.78, tests: 1, docs: 0.96, issues: 0.7, prs: 0.82, actions: 0.88 }, + player: { x: 800, y: 735, speed: 18, facing: 'north', step: 0 }, + selectedDistrictId: 'src', + selectedFileId: 'main-rs', + selectedCollection: 'files', + selectedRecord: null, + activeTool: 'terminal', + activePanel: 'world', + terminal: { open: false, lines: ['$ local shell connected', '$ type `help` for commands'], command: '' }, + feedback: 'Walk toward a district or click a glowing building.', + unlocked: false, + view: 'city' + }; +} + +function pushEvent(state, event) { + state.events = [event, ...state.events].slice(0, 14); +} + +function findDistrict(state, districtId) { + return state.districts.find((district) => district.id === districtId) || state.districts[0]; +} + +function selectedFile(state) { + const district = findDistrict(state, state.selectedDistrictId); + return district.files.find((file) => file.id === state.selectedFileId) || district.files[0]; +} + +function circlesOverlap(x, y, district, margin = 28) { + const left = district.x - margin; + const right = district.x + district.width + margin; + const top = district.y - margin; + const bottom = district.y + district.height + margin; + return x >= left && x <= right && y >= top && y <= bottom; +} + +export function getNearbyDistrict(state, range = 185) { + let nearest = null; + let distance = Number.POSITIVE_INFINITY; + for (const district of state.districts) { + const centerX = district.x + district.width / 2; + const centerY = district.y + district.height / 2; + const nextDistance = Math.hypot(state.player.x - centerX, state.player.y - centerY); + if (nextDistance < distance) { + nearest = district; + distance = nextDistance; + } + } + return { district: nearest, distance, inRange: distance <= range }; +} + +export function worldReducer(previous, event) { + const state = clone(previous); + switch (event.type) { + case EVENT_TYPES.PLAYER_MOVE: { + const dx = Number(event.dx) || 0; + const dy = Number(event.dy) || 0; + const magnitude = Math.hypot(dx, dy) || 1; + const nextX = clamp(state.player.x + (dx / magnitude) * state.player.speed, WALKABLE_BOUNDS.minX, WALKABLE_BOUNDS.maxX); + const nextY = clamp(state.player.y + (dy / magnitude) * state.player.speed, WALKABLE_BOUNDS.minY, WALKABLE_BOUNDS.maxY); + if (!state.districts.some((district) => circlesOverlap(nextX, nextY, district))) { + state.player.x = nextX; + state.player.y = nextY; + state.player.step = (state.player.step + 1) % 4; + } + state.player.facing = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'east' : 'west') : (dy > 0 ? 'south' : 'north'); + return state; + } + case EVENT_TYPES.SELECT_DISTRICT: { + const district = findDistrict(state, event.districtId); + state.selectedDistrictId = district.id; + state.selectedFileId = district.files[0]?.id || null; + state.feedback = `${district.name} selected. Press Enter or E to inspect.`; + return state; + } + case EVENT_TYPES.SELECT_FILE: + state.selectedFileId = event.fileId; + state.activePanel = 'file'; + state.feedback = `Inspecting ${selectedFile(state).name}`; + return state; + case EVENT_TYPES.SELECT_COLLECTION: + state.selectedCollection = event.collection; + state.activePanel = 'collection'; + return state; + case EVENT_TYPES.SELECT_RECORD: + state.selectedRecord = { type: event.recordType, id: event.recordId }; + state.activePanel = 'record'; + return state; + case EVENT_TYPES.OPEN_INSPECTOR: + state.activePanel = 'inspector'; + state.feedback = `Inspecting ${findDistrict(state, state.selectedDistrictId).path}`; + return state; + case EVENT_TYPES.CLOSE_INSPECTOR: + state.activePanel = 'world'; + return state; + case EVENT_TYPES.ENTER_DISTRICT: { + const district = findDistrict(state, event.districtId || state.selectedDistrictId); + state.selectedDistrictId = district.id; + state.selectedFileId = district.files[0]?.id || null; + state.activePanel = 'inspector'; + state.view = 'district'; + state.feedback = `Entered ${district.path}. Code is physical here.`; + pushEvent(state, makeEvent(EVENT_TYPES.ENTER_DISTRICT, state.tick, { message: `Player entered ${district.path}.`, districtId: district.id })); + return state; + } + case EVENT_TYPES.OPEN_TERMINAL: + state.terminal.open = true; + state.activeTool = 'terminal'; + state.activePanel = 'terminal'; + state.feedback = 'Terminal phone online. Commands are simulated locally.'; + return state; + case EVENT_TYPES.CLOSE_TERMINAL: + state.terminal.open = false; + if (state.activePanel === 'terminal') state.activePanel = 'world'; + return state; + case EVENT_TYPES.RUN_COMMAND: { + const command = String(event.command || '').trim().toLowerCase(); + if (!command) return state; + state.terminal.command = command; + state.terminal.lines = [...state.terminal.lines, ...event.lines].slice(-28); + state.feedback = 'Ran `' + command + '` in the simulated terminal.'; + pushEvent(state, makeEvent(EVENT_TYPES.USE_TOOL, state.tick, { message: `Terminal ran ${command}.`, toolId: 'terminal' })); + return state; + } + case EVENT_TYPES.USE_TOOL: { + state.activeTool = event.toolId; + if (event.toolId === 'terminal') state.terminal.open = true; + if (event.toolId === 'git') { state.selectedCollection = 'commits'; state.activePanel = 'collection'; } + if (event.toolId === 'docker') { state.selectedCollection = 'build'; state.activePanel = 'collection'; } + if (event.toolId === 'code') state.activePanel = 'file'; + if (event.toolId === 'ssh') { state.unlocked = true; state.activePanel = 'inspector'; } + if (event.toolId === 'database') { state.selectedCollection = 'files'; state.activePanel = 'collection'; } + if (event.toolId === 'deploy') { state.repository.deployment = 'LAUNCHING'; state.selectedCollection = 'build'; state.activePanel = 'collection'; } + state.feedback = event.message || `Equipped ${event.toolId}.`; + pushEvent(state, makeEvent(EVENT_TYPES.USE_TOOL, state.tick, { message: state.feedback, toolId: event.toolId })); + return state; + } + case EVENT_TYPES.TICK: + state.tick += 1; + return state; + case EVENT_TYPES.AGENT_PROGRESS: { + const agent = state.agents.find((candidate) => candidate.id === event.detail.agentId); + if (agent) agent.progress = Math.min(100, agent.progress + 2); + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + } + case EVENT_TYPES.CONSTRUCTION: { + const districtId = event.detail.districtId; + state.construction[districtId] = clamp((state.construction[districtId] || 0) + (event.detail.amount || 0), 0, 1); + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + } + case EVENT_TYPES.BUILD_STARTED: + state.repository.build = 'RUNNING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.BUILD_COMPLETED: + state.repository.build = 'PASSING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.TEST_STARTED: + state.repository.tests = 'RUNNING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.TEST_COMPLETED: + state.repository.tests = '142 PASS'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.DEPLOYMENT_STARTED: + state.repository.deployment = 'LAUNCHING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.DEPLOYMENT_COMPLETED: + state.repository.deployment = 'STAGED'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.COMMIT: + case EVENT_TYPES.ISSUE: + case EVENT_TYPES.PULL_REQUEST: + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + default: + return state; + } +} + +export function advanceWorldState(state) { + const ticked = worldReducer(state, { type: EVENT_TYPES.TICK }); + const plan = SIMULATION_SEQUENCE[(ticked.tick - 1) % SIMULATION_SEQUENCE.length]; + const event = makeEvent(plan.type, ticked.tick, plan.detail); + return { state: worldReducer(ticked, event), event }; +} + +function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[character])); +} + +function windowBlocks(count, color) { + return Array.from({ length: count }, (_, index) => ``).join(''); +} + +export function renderWorld(viewport, state, onBuildingSelect) { + const viewportWidth = viewport.clientWidth || 960; + const viewportHeight = viewport.clientHeight || 680; + const centerX = viewportWidth / 2; + const centerY = viewportHeight * 0.56; + const translateX = centerX - state.player.x * 0.88; + const translateY = centerY - state.player.y * 0.88; + const buildingMarkup = state.districts.map((district) => { + const selected = district.id === state.selectedDistrictId ? ' is-selected' : ''; + const progress = Math.round((state.construction[district.id] || 0) * 100); + return ``; + }).join(''); + + viewport.innerHTML = `
    + + +
    +
    +
    MAIN
    +
    HISTORY
    +
    BRANCH / ALT
    +
    SPAWN PLAZA
    + ${buildingMarkup} + + +
    +
    +
    !
    +
    PULL REQUESTOPEN GATE
    +
    + +
    +
    SIMULATED REPOSITORYSNAPKITTYWEST/ pax-coder★ 213 · ⑂ 45 · ◉ PUBLIC
    +
    BUILD AGENTS3 ACTIVEWATCH THE CREW WORK
    +
    +
    `; + + viewport.querySelectorAll('[data-district-id]').forEach((building) => { + building.addEventListener('click', () => onBuildingSelect(building.dataset.districtId)); + }); +} + +export function renderMinimap(root, state) { + const districts = state.districts.map((district) => ``).join(''); + root.innerHTML = `
    ${districts}
    LIVE MAP${state.selectedDistrictId.toUpperCase()}
    `; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000000000000000000000000000000000000..5e6ca3b76ce3242dad5f4bc31dfa701bce6b2059 --- /dev/null +++ b/index.html @@ -0,0 +1,122 @@ + + + + + + + + GitWorld — the world you walk into + + + +
    + + +
    + VRCHAT WORLD · NEON REFLECTIONS +

    GITWORLD

    +

    Walk into the code. Every repository is a living world.

    +
    OPENING SCENE READYLOCAL BROWSER RENDERVR-STYLE VIEW
    +
    + Loading 3099.mp4… +
    +
    +
    + + +
    +
    GitWorldTHE WORLD YOU WALK INTO
    +
    ⌘ K
    +
    BROWSER-LOCAL SIMULATION
    +
    + + + +
    +
    ENTER REPOSITORY

    SNAPKITTYWEST / pax-coder

    PublicBRANCH / main
    +
    +
    +
    REALITY DECKNEON REFLECTIONS · BROWSER SCENE PLATES
    +
    + + + + +
    +
    +
    WALKABLE CITYNORTHWASD · ARROWS TO NAVIGATE
    + +
    WASDwalkEenter
    + +
    + + + +
    +
    CONTEXTUAL INSPECTPHYSICAL SOFTWARE
    +
    +
    +
    + + + + + +
    +
    SEE SOFTWARE AS PHYSICAL OBJECTSCLICK A TOOL OR BUILDING TO INTERACT
    +
    + + + + + + + + +
    +
    + +
    +
    +
    + +
    + + + diff --git a/launch.html b/launch.html new file mode 100644 index 0000000000000000000000000000000000000000..4d7074769d87359a7a7a692a65e0bb79a85168be --- /dev/null +++ b/launch.html @@ -0,0 +1,37 @@ + + + + + + GitWorld Launcher + + + +
    +
    SNAPKITTYWEST · local build
    +

    GitWorld

    +

    Choose the playable build. The browser build opens immediately. The Unreal project is wired with the opening scene, VR pawn, repository worlds, portals, and the 8K rendering profile.

    +
    +
    Play browser buildWalkable local prototype · opens now
    +
    Open Unreal projectLaunches the project through the installed Unreal association
    +
    Run Unreal VR launcherUses the local UE 5.x editor and starts with OpenXR VR
    +
    +

    Desktop fallback: in Unreal Play-in-Editor use WASD to move, E to interact, Space to teleport, and Esc to skip the opening.

    +
    + + diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..94383b1289c52702033840887871aa3a7942136a --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "gitworld-local-prototype", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "A browser-local walkable repository city prototype.", + "scripts": { + "build": "node scripts/build.mjs", + "test": "node --test tests/*.test.mjs", + "check": "node --check src/shared.js && node --check src/world-architect.js && node --check src/ui-reality.js && node --check scripts/build.mjs" + } +} diff --git a/phase2-unreal/.gitignore b/phase2-unreal/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..9fb979fe2ae86dd1a3cb9d7213125e5ebc9f68c2 --- /dev/null +++ b/phase2-unreal/.gitignore @@ -0,0 +1,10 @@ +Binaries/ +DerivedDataCache/ +Intermediate/ +Saved/ +Compute/build/ +RealityCapture/Output/ +Saved/Autosaves/ +ReferenceSource/Output/ +*.sln +*.suo diff --git a/phase2-unreal/Compute/CMakeLists.txt b/phase2-unreal/Compute/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..c50a4d11448979ab34a96b7b6c4cba483b86e0bf --- /dev/null +++ b/phase2-unreal/Compute/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.24) +project(GitWorldCuda LANGUAGES CXX CUDA) + +add_library(GitWorldCuda SHARED GitWorldBuildPass.cu) +target_compile_features(GitWorldCuda PRIVATE cxx_std_17) +set_target_properties(GitWorldCuda PROPERTIES + CUDA_SEPARABLE_COMPILATION ON + WINDOWS_EXPORT_ALL_SYMBOLS ON + OUTPUT_NAME "GitWorldCuda" +) diff --git a/phase2-unreal/Compute/GitWorldBuildPass.cu b/phase2-unreal/Compute/GitWorldBuildPass.cu new file mode 100644 index 0000000000000000000000000000000000000000..ebb8fe7661b3a71d53364adb1af95737b1f4e18c --- /dev/null +++ b/phase2-unreal/Compute/GitWorldBuildPass.cu @@ -0,0 +1,65 @@ +#include "GitWorldBuildPass.h" + +#include + +namespace +{ + __global__ void BuildFieldKernel(const float* Input, float* Output, int Count) + { + const int Index = blockIdx.x * blockDim.x + threadIdx.x; + if (Index < Count) + { + Output[Index] = Input[Index] * (1.0f + static_cast(Index) * 0.125f); + } + } +} + +extern "C" GITWORLD_CUDA_API int GW_CudaBuildPass( + const float* Input, + float* Output, + int Count, + int* DeviceIndex) +{ + if (Input == nullptr || Output == nullptr || Count <= 0 || DeviceIndex == nullptr) + { + return 0; + } + + int DeviceCount = 0; + if (cudaGetDeviceCount(&DeviceCount) != cudaSuccess || DeviceCount == 0) + { + return 0; + } + + *DeviceIndex = 0; + if (cudaSetDevice(*DeviceIndex) != cudaSuccess) + { + return 0; + } + + float* DeviceInput = nullptr; + float* DeviceOutput = nullptr; + const size_t Bytes = sizeof(float) * static_cast(Count); + if (cudaMalloc(&DeviceInput, Bytes) != cudaSuccess + || cudaMalloc(&DeviceOutput, Bytes) != cudaSuccess) + { + if (DeviceInput != nullptr) cudaFree(DeviceInput); + if (DeviceOutput != nullptr) cudaFree(DeviceOutput); + return 0; + } + + const bool CopiedIn = cudaMemcpy(DeviceInput, Input, Bytes, cudaMemcpyHostToDevice) == cudaSuccess; + if (CopiedIn) + { + BuildFieldKernel<<<(Count + 127) / 128, 128>>>(DeviceInput, DeviceOutput, Count); + } + const bool Computed = CopiedIn + && cudaGetLastError() == cudaSuccess + && cudaDeviceSynchronize() == cudaSuccess; + const bool CopiedOut = Computed + && cudaMemcpy(Output, DeviceOutput, Bytes, cudaMemcpyDeviceToHost) == cudaSuccess; + + cudaFree(DeviceInput); + cudaFree(DeviceOutput); + return CopiedOut ? 1 : 0; +} diff --git a/phase2-unreal/Compute/GitWorldBuildPass.h b/phase2-unreal/Compute/GitWorldBuildPass.h new file mode 100644 index 0000000000000000000000000000000000000000..e92dfe4569bdca07da0484481bdd0bbcc5527d5c --- /dev/null +++ b/phase2-unreal/Compute/GitWorldBuildPass.h @@ -0,0 +1,16 @@ +#pragma once + +#if defined(_WIN32) +#define GITWORLD_CUDA_API __declspec(dllexport) +#else +#define GITWORLD_CUDA_API +#endif + +extern "C" +{ + GITWORLD_CUDA_API int GW_CudaBuildPass( + const float* Input, + float* Output, + int Count, + int* DeviceIndex); +} diff --git a/phase2-unreal/Compute/README.md b/phase2-unreal/Compute/README.md new file mode 100644 index 0000000000000000000000000000000000000000..92c887291aba1b1ac86051ce8f220486f73aae87 --- /dev/null +++ b/phase2-unreal/Compute/README.md @@ -0,0 +1,12 @@ +# Optional CUDA build-field pass + +This DLL is not the Unreal renderer. Unreal renders the world through DX12, Nanite, Lumen, and hardware ray tracing when the project is run on a supported configuration. The DLL provides a small GPU compute pass for repository build-field weights and is loaded dynamically by `FGitWorldCudaBridge`. + +Build from a Visual Studio Developer Command Prompt with the CUDA toolkit installed: + +```text +cmake -S Compute -B Compute/build -G "Visual Studio 17 2022" -A x64 +cmake --build Compute/build --config Release +``` + +Copy `Compute/build/Release/GitWorldCuda.dll` into `Binaries/Win64/`, or set `GITWORLD_CUDA_DLL` to an absolute path before launching Unreal. If the DLL is absent or the device pass fails, the world remains playable using the deterministic CPU fallback. diff --git a/phase2-unreal/Config/DefaultEngine.ini b/phase2-unreal/Config/DefaultEngine.ini new file mode 100644 index 0000000000000000000000000000000000000000..94a8808a55247e161e58e97c622475e9ffff7e04 --- /dev/null +++ b/phase2-unreal/Config/DefaultEngine.ini @@ -0,0 +1,44 @@ +[/Script/EngineSettings.GameMapsSettings] +GameDefaultMap=/Engine/Maps/Entry +EditorStartupMap=/Engine/Maps/Entry +GlobalDefaultGameMode=/Script/GitWorldUnreal.GitWorldGameMode + +[/Script/Engine.RendererSettings] +r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=True +r.DynamicGlobalIlluminationMethod=1 +r.ReflectionMethod=1 +r.Lumen.HardwareRayTracing=1 +r.RayTracing=True +r.Nanite.ProjectEnabled=True +r.Shadow.Virtual.Enable=1 +r.GenerateMeshDistanceFields=True +r.AllowStaticLighting=False +r.DefaultFeature.MotionBlur=False +r.DefaultFeature.AntiAliasing=4 +r.TemporalAA.Upsampling=1 + +[DevOptions.Shaders] +TargetedRHIs=PCD3D_SM6 + +[/Script/Engine.Engine] ++ActiveGameNameRedirects=(OldGameName="TP_ThirdPerson",NewGameName="/Script/GitWorldUnreal") + +[/Script/Engine.GameUserSettings] +ResolutionSizeX=7680 +ResolutionSizeY=4320 +LastUserConfirmedResolutionSizeX=7680 +LastUserConfirmedResolutionSizeY=4320 +WindowPosX=-1 +WindowPosY=-1 +bUseHDRDisplayOutput=False +HDRDisplayOutputNits=1000 + +[ConsoleVariables] +r.ScreenPercentage=100 +r.SecondaryScreenPercentage.GameViewport=100 +r.DynamicRes.OperationMode=0 +r.ViewDistanceScale=2 +r.Shadow.Virtual.MaxPhysicalPages=4096 +r.Nanite.MaxPixelsPerEdge=1 +r.Streaming.PoolSize=0 +t.MaxFPS=60 diff --git a/phase2-unreal/Config/DefaultGame.ini b/phase2-unreal/Config/DefaultGame.ini new file mode 100644 index 0000000000000000000000000000000000000000..2956d24b4f3893908dd207600037a582ce1bdfac --- /dev/null +++ b/phase2-unreal/Config/DefaultGame.ini @@ -0,0 +1,2 @@ +[/Script/GitWorldUnreal.GitWorldGameMode] +GameSessionClass=/Script/Engine.GameSession diff --git a/phase2-unreal/Config/DefaultInput.ini b/phase2-unreal/Config/DefaultInput.ini new file mode 100644 index 0000000000000000000000000000000000000000..4bef5610c6445e27704e1fcb26e0319e81031156 --- /dev/null +++ b/phase2-unreal/Config/DefaultInput.ini @@ -0,0 +1,17 @@ +[/Script/Engine.InputSettings] ++AxisMappings=(AxisName="MoveForward",Key=W,Scale=1.0) ++AxisMappings=(AxisName="MoveForward",Key=S,Scale=-1.0) ++AxisMappings=(AxisName="MoveForward",Key=Gamepad_LeftY,Scale=1.0) ++AxisMappings=(AxisName="MoveForward",Key=MotionController_Left_Thumbstick_Y,Scale=1.0) ++AxisMappings=(AxisName="MoveRight",Key=D,Scale=1.0) ++AxisMappings=(AxisName="MoveRight",Key=A,Scale=-1.0) ++AxisMappings=(AxisName="MoveRight",Key=Gamepad_LeftX,Scale=1.0) ++AxisMappings=(AxisName="MoveRight",Key=MotionController_Left_Thumbstick_X,Scale=1.0) ++AxisMappings=(AxisName="Turn",Key=MouseX,Scale=1.0) ++AxisMappings=(AxisName="Turn",Key=Gamepad_RightX,Scale=1.0) ++ActionMappings=(ActionName="Interact",Key=E) ++ActionMappings=(ActionName="Interact",Key=MotionController_Right_Trigger) ++ActionMappings=(ActionName="Teleport",Key=SpaceBar) ++ActionMappings=(ActionName="Teleport",Key=MotionController_Right_Thumbstick) ++ActionMappings=(ActionName="SkipOpening",Key=Escape) ++ActionMappings=(ActionName="SkipOpening",Key=MotionController_Left_Menu) diff --git a/phase2-unreal/Content/RealityScene/README.md b/phase2-unreal/Content/RealityScene/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7e7461a20185f4ff8072792475823a6abd4caef6 --- /dev/null +++ b/phase2-unreal/Content/RealityScene/README.md @@ -0,0 +1,9 @@ +# Reality scene import target + +Import one FBX per repository here in Unreal: + +```text +/Game/RealityScene//.fbx +``` + +Assign the imported static mesh to `RealityCaptureMesh` on the matching `AGitWorldRepoWorld` actor. The actor keeps repository state isolated while the RealityCapture mesh supplies the physical scene surface. diff --git a/phase2-unreal/Content/VR/README.md b/phase2-unreal/Content/VR/README.md new file mode 100644 index 0000000000000000000000000000000000000000..329863818cb45fe54e503b19f725acaaae5259e3 --- /dev/null +++ b/phase2-unreal/Content/VR/README.md @@ -0,0 +1,12 @@ +# GitWorld VR mode + +The project enables OpenXR and uses `AGitWorldVRPawn` as the default pawn. The pawn provides head tracking, tracked left/right controllers, thumbstick or WASD movement, controller-directed teleport, portal interaction, and opening-video skip. + +Controls: + +- Left thumbstick / WASD: smooth movement +- Right trigger / E: interact with a portal or repository world +- Right thumbstick click / Space: aim and release to teleport +- Left menu / Escape: skip the `3099.mp4` opening sequence + +OpenXR must be installed/enabled by the Unreal Engine installation. The project’s 7680×4320 settings are for cinematic output; VR should use the headset’s native per-eye render target and a measured frame-time budget. diff --git a/phase2-unreal/EditorScripts/import_gitworld_opening.py b/phase2-unreal/EditorScripts/import_gitworld_opening.py new file mode 100644 index 0000000000000000000000000000000000000000..ec1a3957cca3881d3c5223aee5e2994bc8763cab --- /dev/null +++ b/phase2-unreal/EditorScripts/import_gitworld_opening.py @@ -0,0 +1,110 @@ +"""Run inside the Unreal Editor Python environment. + +Creates a Media Framework asset for 3099.mp4, builds an emissive screen +material, and places the AGitWorldOpeningScene actor in the current level. +""" + +import pathlib + +import unreal + + +PROJECT_DIR = pathlib.Path(unreal.Paths.project_dir()) +VIDEO_FILE = PROJECT_DIR / "ReferenceSource" / "Opening" / "3099.mp4" +MEDIA_PATH = "/Game/GitWorld/Media" +MATERIAL_PATH = "/Game/GitWorld/Materials" + + +def create_asset(asset_tools, name, package_path, asset_class, factory): + existing = unreal.EditorAssetLibrary.load_asset(package_path + "/" + name) + if existing: + return existing + return asset_tools.create_asset(name, package_path, asset_class, factory) + + +def create_media_assets(): + if not VIDEO_FILE.is_file(): + raise RuntimeError("Opening video not found: %s" % VIDEO_FILE) + + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + source = create_asset( + asset_tools, + "MS_GitWorldOpening", + MEDIA_PATH, + unreal.FileMediaSource, + unreal.FileMediaSourceFactoryNew(), + ) + source.set_editor_property("file_path", str(VIDEO_FILE)) + unreal.EditorAssetLibrary.save_loaded_asset(source) + + player = create_asset( + asset_tools, + "MP_GitWorldOpening", + MEDIA_PATH, + unreal.MediaPlayer, + unreal.MediaPlayerFactoryNew(), + ) + player.set_editor_property("loop", False) + player.set_editor_property("play_on_open", True) + player.set_editor_property("media_source", source) + unreal.EditorAssetLibrary.save_loaded_asset(player) + + texture = create_asset( + asset_tools, + "MT_GitWorldOpening", + MEDIA_PATH, + unreal.MediaTexture, + unreal.MediaTextureFactoryNew(), + ) + texture.set_editor_property("media_player", player) + unreal.EditorAssetLibrary.save_loaded_asset(texture) + return texture + + +def create_screen_material(texture): + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + material = create_asset( + asset_tools, + "M_GitWorldOpeningScreen", + MATERIAL_PATH, + unreal.Material, + unreal.MaterialFactoryNew(), + ) + material.set_editor_property("two_sided", True) + material.set_editor_property("shading_model", unreal.MaterialShadingModel.MSM_UNLIT) + sample = unreal.MaterialEditingLibrary.create_material_expression( + material, unreal.MaterialExpressionTextureSampleParameter2D, -400, 0 + ) + sample.set_editor_property("parameter_name", "OpeningVideo") + sample.set_editor_property("texture", texture) + unreal.MaterialEditingLibrary.connect_material_property( + sample, "RGB", unreal.MaterialProperty.MP_EMISSIVE_COLOR + ) + unreal.MaterialEditingLibrary.layout_material_expressions(material) + unreal.EditorAssetLibrary.save_loaded_asset(material) + return material + + +def place_opening_actor(material): + opening_class = unreal.load_class(None, "/Script/GitWorldUnreal.GitWorldOpeningScene") + if not opening_class: + raise RuntimeError("GitWorldOpeningScene class is unavailable; build the Unreal module first") + actor = unreal.EditorLevelLibrary.spawn_actor_from_class( + opening_class, unreal.Vector(0.0, 0.0, 1050.0), unreal.Rotator(0.0, 0.0, 0.0) + ) + actor.set_actor_label("GitWorld_Opening_3099") + actor.set_editor_property("opening_video_file", "ReferenceSource/Opening/3099.mp4") + actor.set_editor_property("screen_material", material) + actor.set_editor_property("b_play_on_begin", True) + return actor + + +def run(): + texture = create_media_assets() + material = create_screen_material(texture) + place_opening_actor(material) + unreal.EditorLevelLibrary.save_current_level() + unreal.log("GitWorld opening scene wired: 3099.mp4") + + +run() diff --git a/phase2-unreal/EditorScripts/import_gitworld_reference_scene.py b/phase2-unreal/EditorScripts/import_gitworld_reference_scene.py new file mode 100644 index 0000000000000000000000000000000000000000..7d68b500527de25d015912d90901f65bb32c57dd --- /dev/null +++ b/phase2-unreal/EditorScripts/import_gitworld_reference_scene.py @@ -0,0 +1,115 @@ +"""Run inside Unreal Editor's Python console or Execute Python Script. + +Imports the supplied Phase 3 reference plates and places them as emissive +gallery panels in the pax-coder repository sandbox. This is a visual reference +scene, not a photogrammetry reconstruction. +""" + +import pathlib + +import unreal + + +PROJECT_DIR = pathlib.Path(unreal.Paths.project_dir()) +SOURCE_DIR = PROJECT_DIR / "ReferenceSource" / "NeonReflections" +DESTINATION = "/Game/RealityScene/NeonReflections" +REPOSITORY_ID = "pax-coder" +REFERENCE_SET_ID = "neon-reflections-v15" +IMAGE_FILES = ["3087.webp", "3089.webp", "3090.webp", "3092.jpg"] + + +def import_reference_images(): + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + imported = [] + for file_name in IMAGE_FILES: + source_file = SOURCE_DIR / file_name + if not source_file.is_file(): + unreal.log_warning("Missing reference plate: %s" % source_file) + continue + task = unreal.AssetImportTask() + task.set_editor_property("filename", str(source_file)) + task.set_editor_property("destination_path", DESTINATION) + task.set_editor_property("destination_name", pathlib.Path(file_name).stem) + task.set_editor_property("automated", True) + task.set_editor_property("replace_existing", True) + task.set_editor_property("save", True) + asset_tools.import_asset_tasks([task]) + imported.extend(task.get_editor_property("imported_object_paths")) + return [unreal.load_asset(path) for path in imported] + + +def make_emissive_material(texture, name): + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + package_path = DESTINATION + "/Materials" + factory = unreal.MaterialFactoryNew() + material = asset_tools.create_asset(name, package_path, unreal.Material, factory) + if not material: + return None + material.set_editor_property("two_sided", True) + material.set_editor_property("shading_model", unreal.MaterialShadingModel.MSM_UNLIT) + sample = unreal.MaterialEditingLibrary.create_material_expression( + material, unreal.MaterialExpressionTextureSampleParameter2D, -400, 0 + ) + sample.set_editor_property("parameter_name", "ReferenceTexture") + sample.set_editor_property("texture", texture) + unreal.MaterialEditingLibrary.connect_material_property( + sample, "RGB", unreal.MaterialProperty.MP_EMISSIVE_COLOR + ) + unreal.MaterialEditingLibrary.layout_material_expressions(material) + unreal.EditorAssetLibrary.save_loaded_asset(material) + return material + + +def place_gallery_panel(mesh, material, location, rotation, scale, label): + actor = unreal.EditorLevelLibrary.spawn_actor_from_object( + mesh, unreal.Vector(*location), unreal.Rotator(*rotation) + ) + actor.set_actor_label(label) + actor.set_actor_scale3d(unreal.Vector(*scale)) + component = actor.get_component_by_class(unreal.StaticMeshComponent) + if component and material: + component.set_material(0, material) + return actor + + +def run(): + textures = import_reference_images() + if not textures: + raise RuntimeError("No reference plates imported from %s" % SOURCE_DIR) + + plane = unreal.load_asset("/Engine/BasicShapes/Plane.Plane") + if not plane: + raise RuntimeError("Engine plane mesh is unavailable") + + placements = [ + ((0.0, 1800.0, 900.0), (90.0, 0.0, 0.0), (12.0, 8.0), "NeonRef_North"), + ((0.0, -1800.0, 900.0), (-90.0, 0.0, 180.0), (12.0, 8.0), "NeonRef_South"), + ((1800.0, 0.0, 900.0), (90.0, 0.0, 90.0), (12.0, 8.0), "NeonRef_East"), + ((-1800.0, 0.0, 900.0), (90.0, 0.0, -90.0), (12.0, 8.0), "NeonRef_West"), + ] + + for texture, placement in zip(textures, placements): + material = make_emissive_material(texture, "MI_" + texture.get_name()) + place_gallery_panel(plane, material, *placement) + + repo_class = unreal.load_class(None, "/Script/GitWorldUnreal.GitWorldRepoWorld") + if repo_class: + repo_actor = next( + ( + actor + for actor in unreal.EditorLevelLibrary.get_all_level_actors() + if actor.is_a(repo_class) + and actor.get_editor_property("repository_id") == REPOSITORY_ID + ), + None, + ) + if repo_actor: + repo_actor.set_editor_property("reference_set_id", REFERENCE_SET_ID) + repo_actor.set_editor_property("reference_images", textures) + unreal.EditorAssetLibrary.save_loaded_asset(repo_actor) + + unreal.EditorLevelLibrary.save_current_level() + unreal.log("GitWorld Phase 3 reference gallery placed: %s" % REFERENCE_SET_ID) + + +run() diff --git a/phase2-unreal/GitWorldUnreal.uproject b/phase2-unreal/GitWorldUnreal.uproject new file mode 100644 index 0000000000000000000000000000000000000000..e080dc28a8706d50ce3f7eaaf3af0f7dcdc59b40 --- /dev/null +++ b/phase2-unreal/GitWorldUnreal.uproject @@ -0,0 +1,23 @@ +{ + "FileVersion": 3, + "EngineAssociation": "", + "Category": "Games", + "Description": "GitWorld Phase 2: walkable repository worlds with an optional CUDA build-field compute pass.", + "Modules": [ + { + "Name": "GitWorldUnreal", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ], + "Plugins": [ + { + "Name": "Niagara", + "Enabled": true + }, + { + "Name": "OpenXR", + "Enabled": true + } + ] +} diff --git a/phase2-unreal/README.md b/phase2-unreal/README.md new file mode 100644 index 0000000000000000000000000000000000000000..595faf112b775a7b62792e06d4d83c523ee46f4f --- /dev/null +++ b/phase2-unreal/README.md @@ -0,0 +1,42 @@ +# GitWorld Phase 2 — Unreal walkable worlds + +## Objective + +Move the completed browser-local GitWorld concept into a real-time Unreal project with a walkable third-person space, repository-owned sandbox state, visible build progress, and a high-resolution rendering profile. + +## Player experience + +The player spawns in a GitWorld map containing repository districts. Each repository is an `AGitWorldRepoWorld` actor with its own deterministic snapshot, geometry, events, and terminal transcript. Walking into an `AGitWorldRepoPortal` activates that repository in the world subsystem. Build ticks change district geometry and the sign above the world, so construction is visible in-world rather than represented only by UI. + +## Architecture + +- `GitWorldSimulation.*` — deterministic, headless repository state transitions and terminal output. +- `AGitWorldRepoWorld` — one sandboxed repository world; owns its snapshot, generated blocks, sign, and compute result. +- `AGitWorldRepoPortal` — overlap trigger that switches the active repository when the player walks in. +- `UGitWorldWorldSubsystem` — registry and active-world selection; no repository actor mutates another repository’s state. +- `FGitWorldCudaBridge` — optional dynamic CUDA DLL boundary with a deterministic CPU fallback. +- `AGitWorldGameMode` — temporary demo catalog for `pax-coder`, `agent-minefield`, and `cuda-lab`; replace with a data asset in Phase 3. + +## Rendering and 8K target + +`Config/DefaultEngine.ini` enables a DX12-oriented Nanite/Lumen profile, virtual shadow maps, hardware ray tracing, and a 7680×4320 game-user resolution target. Use `r.SetRes 7680x4320f` for a desktop viewport or Movie Render Queue for a high-resolution capture. A real 8K interactive session is a performance target, not a promise that every GPU can sustain 60 FPS at native resolution. + +The local machine currently exposes an RTX 3080 with 10,240 MiB VRAM. Unreal Editor and `nvcc` were not available on PATH during scaffold validation, so Unreal compilation, CUDA device execution, and native 8K frame-time measurement remain pending until those tools are installed. + +## Run when Unreal is installed + +1. Right-click `GitWorldUnreal.uproject` and generate Visual Studio project files. +2. Build `GitWorldUnrealEditor` in Development Editor / Win64. +3. Open the project, press Play, and walk with the default pawn. +4. Confirm the repository sign, build tick, portal activation, and terminal transcript. +5. If desired, build `Compute/GitWorldCuda.dll` and place it in `Binaries/Win64/` to enable the CUDA compute pass. + +## Local verification + +From the parent `gitworld` folder: + +```text +node tools/verify-phase2.mjs +``` + +This verifies the project manifest, Unreal source contract, isolated repository identifiers, CUDA boundary, and 8K settings without requiring Unreal or CUDA to be installed. diff --git a/phase2-unreal/RealityCapture/README.md b/phase2-unreal/RealityCapture/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2b24f8bfcc41c693ba89861afc561f2c82696c91 --- /dev/null +++ b/phase2-unreal/RealityCapture/README.md @@ -0,0 +1,36 @@ +# RealityCapture 1.5 → Unreal reality scene + +RealityCapture 1.5.1 is installed locally at: + +```text +C:\Program Files\Capturing Reality\RealityCapture\RealityCapture.exe +``` + +The reconstruction pipeline is in `Tools/reconstruct.ps1`. It creates one isolated RealityCapture project and FBX output per repository. The resulting FBX is imported into Unreal as a `UStaticMesh`, then assigned to the `RealityCaptureMesh` property of that repository’s `AGitWorldRepoWorld` actor. + +## Input requirement + +Provide a deliberate capture folder containing at least three overlapping photos of the same physical scene. The current Downloads folder contains mixed personal/media files, so it is not selected automatically. + +## Reconstruct a repository scene + +From this `phase2-unreal` directory: + +```powershell +& .\RealityCapture\Tools\reconstruct.ps1 ` + -RepositoryId pax-coder ` + -InputFolder 'D:\Captures\pax-coder-plaza' +``` + +The script runs `-addFolder`, `-align`, `-selectMaximalComponent`, automatic reconstruction-region setup, model calculation, texturing, FBX export, and project save. RealityCapture’s command-line workflow and these reconstruction/export commands are documented by Capturing Reality: [CLI](https://rchelp.capturingreality.com/en-US/tutorials/commandline.htm), [reconstruction examples](https://rchelp.capturingreality.com/en-US/tutorials/commandline_2.htm), and [model export](https://rchelp.capturingreality.com/en-US/tutorials/commandline_3.htm). + +## Import into Unreal + +1. Generate Visual Studio project files from `GitWorldUnreal.uproject`. +2. Import `RealityCapture/Output//.fbx` into `/Game/RealityScene/`. +3. Enable Nanite on the imported static mesh and create a collision setup appropriate for walking. +4. Assign the mesh to `RealityCaptureMesh` on the matching `AGitWorldRepoWorld` actor. +5. Adjust `RealitySceneTransform` for centimeters/origin alignment; keep the repo’s walkable bounds around the imported scene. +6. Press Play and walk through the physical scene while build blocks, signs, portals, and events remain attached to the repository sandbox. + +The Unreal renderer remains DX12/Nanite/Lumen. CUDA is an optional compute pass for build-field weights; RealityCapture’s photogrammetry reconstruction is run by its own installed application. diff --git a/phase2-unreal/RealityCapture/Tools/reconstruct.ps1 b/phase2-unreal/RealityCapture/Tools/reconstruct.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..b5f379562639ac40afad579422e694e9cbdc262b --- /dev/null +++ b/phase2-unreal/RealityCapture/Tools/reconstruct.ps1 @@ -0,0 +1,75 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidatePattern('^[a-z0-9][a-z0-9-]*$')] + [string]$RepositoryId, + + [Parameter(Mandatory = $true)] + [string]$InputFolder, + + [ValidateSet('Preview', 'Normal', 'High')] + [string]$Detail = 'Normal', + + [switch]$ValidateOnly +) + +$ErrorActionPreference = 'Stop' + +$realityCapture = 'C:\Program Files\Capturing Reality\RealityCapture\RealityCapture.exe' +$scriptRoot = Split-Path -Parent $PSScriptRoot +$outputRoot = Join-Path $scriptRoot 'Output' +$repositoryOutput = Join-Path $outputRoot $RepositoryId +$projectFile = Join-Path $repositoryOutput ($RepositoryId + '.rcproj') +$modelFile = Join-Path $repositoryOutput ($RepositoryId + '.fbx') + +if (-not (Test-Path -LiteralPath $realityCapture -PathType Leaf)) { + throw "RealityCapture 1.5 executable not found at $realityCapture" +} +if (-not (Test-Path -LiteralPath $InputFolder -PathType Container)) { + throw "Capture folder not found: $InputFolder" +} + +$imageExtensions = @('.jpg', '.jpeg', '.png', '.tif', '.tiff', '.webp') +$images = @(Get-ChildItem -LiteralPath $InputFolder -File -Recurse | Where-Object { + $imageExtensions -contains $_.Extension.ToLowerInvariant() +}) +if ($images.Count -lt 3) { + throw "RealityCapture needs at least 3 overlapping image files; found $($images.Count) in $InputFolder" +} + +New-Item -ItemType Directory -Path $repositoryOutput -Force | Out-Null + +if ($ValidateOnly) { + Write-Output "RealityCapture input PASS: $($images.Count) image files for $RepositoryId" + exit 0 +} + +$modelCommand = switch ($Detail) { + 'Preview' { '-calculatePreviewModel' } + 'High' { '-calculateHighModel' } + default { '-calculateNormalModel' } +} + +$arguments = @( + '-newScene', + '-addFolder', $InputFolder, + '-align', + '-selectMaximalComponent', + '-setReconstructionRegionAuto', + $modelCommand, + '-calculateTexture', + '-exportSelectedModel', $modelFile, + '-save', $projectFile, + '-quit' +) + +Write-Output "Starting RealityCapture 1.5 reconstruction for $RepositoryId with $($images.Count) images..." +& $realityCapture @arguments +if ($LASTEXITCODE -ne 0) { + throw "RealityCapture exited with code $LASTEXITCODE" +} +if (-not (Test-Path -LiteralPath $modelFile -PathType Leaf)) { + throw "RealityCapture completed without producing $modelFile" +} + +Write-Output "Reality scene ready: $modelFile" diff --git a/phase2-unreal/ReferenceSource/NeonReflections/3087.webp b/phase2-unreal/ReferenceSource/NeonReflections/3087.webp new file mode 100644 index 0000000000000000000000000000000000000000..a2eb546bd032e0fa50538440a793c3bb64ede310 --- /dev/null +++ b/phase2-unreal/ReferenceSource/NeonReflections/3087.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aff14460f1c28ebe7ee2e341399587ef13c636948dbbced9b4b017b2d72e39c4 +size 278024 diff --git a/phase2-unreal/ReferenceSource/NeonReflections/3089.webp b/phase2-unreal/ReferenceSource/NeonReflections/3089.webp new file mode 100644 index 0000000000000000000000000000000000000000..e4f59cae6ca2b8c42977019a750a7cd363a50768 --- /dev/null +++ b/phase2-unreal/ReferenceSource/NeonReflections/3089.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06ef3e7ce562608dbb1c4685fb65d423a66921479d0cd5897584c76f9f17b951 +size 280306 diff --git a/phase2-unreal/ReferenceSource/NeonReflections/3090.webp b/phase2-unreal/ReferenceSource/NeonReflections/3090.webp new file mode 100644 index 0000000000000000000000000000000000000000..1cdaf0e3033442661d45eb539de6b1b51a75f19c --- /dev/null +++ b/phase2-unreal/ReferenceSource/NeonReflections/3090.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbf068da5c13c53daa91e2b273121eb731318d2746f49aa50fa5fa5ff57ac1a0 +size 327290 diff --git a/phase2-unreal/ReferenceSource/NeonReflections/3092.jpg b/phase2-unreal/ReferenceSource/NeonReflections/3092.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7e31e473e734930d830a782cd24dbbe430109a41 --- /dev/null +++ b/phase2-unreal/ReferenceSource/NeonReflections/3092.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90addbd5567b5b37b98db73d7791896947621501b507e014e4902ab08497ad65 +size 191235 diff --git a/phase2-unreal/ReferenceSource/Opening/3099.mp4 b/phase2-unreal/ReferenceSource/Opening/3099.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..d8ff7560bb5b876fda518d7415eb325c27fad9b3 --- /dev/null +++ b/phase2-unreal/ReferenceSource/Opening/3099.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eaaea396db131ac4681bd51cb4dc258ba7cae15efeebab77d752e335ca486ae0 +size 5190235 diff --git a/phase2-unreal/ReferenceSource/README.md b/phase2-unreal/ReferenceSource/README.md new file mode 100644 index 0000000000000000000000000000000000000000..48dab7f4194fc658195d2267673f0bf9fea477b6 --- /dev/null +++ b/phase2-unreal/ReferenceSource/README.md @@ -0,0 +1,7 @@ +# GitWorld Phase 3 reference set + +The four user-selected files in `NeonReflections/` are wired as visual reference plates for the `pax-coder` GitWorld sandbox. They establish the neon social-hub, portal, avatar, and VR interface look for the Unreal scene. + +They are intentionally not passed to the photogrammetry reconstruction script: the frames are generated/stylized renders from different compositions, not a camera sweep around one real subject. RealityCapture would need a separate folder of overlapping photographs to produce a valid 3D mesh. + +The Unreal Editor script imports the plates, creates unlit display materials, places them on vertical gallery planes around the repository world, and assigns the imported textures to the world actor’s `ReferenceImages` property. This gives the project a visible scene using the supplied art while preserving the separate true-photogrammetry path. diff --git a/phase2-unreal/ReferenceSource/manifest.json b/phase2-unreal/ReferenceSource/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..b846c1e379cbec24448b9e2f3a649a68943b80f8 --- /dev/null +++ b/phase2-unreal/ReferenceSource/manifest.json @@ -0,0 +1,34 @@ +{ + "referenceSetId": "neon-reflections-v15", + "repositoryWorld": "pax-coder", + "sourceType": "visual-reference-plates", + "photogrammetryReady": false, + "reason": "The supplied frames depict different stylized scenes and a character turntable; they are not overlapping views of one physical location.", + "openingScene": { + "file": "Opening/3099.mp4", + "role": "gitworld-opening-cinematic", + "autoplay": true + }, + "assets": [ + { + "file": "3087.webp", + "role": "arcade-portal-reference", + "placement": "east-gallery" + }, + { + "file": "3089.webp", + "role": "social-hub-pov-reference", + "placement": "north-gallery" + }, + { + "file": "3090.webp", + "role": "social-hub-spawn-reference", + "placement": "west-gallery" + }, + { + "file": "3092.jpg", + "role": "avatar-turntable-reference", + "placement": "avatar-gallery" + } + ] +} diff --git a/phase2-unreal/Source/GitWorldUnreal.Target.cs b/phase2-unreal/Source/GitWorldUnreal.Target.cs new file mode 100644 index 0000000000000000000000000000000000000000..f10d0804dd810a2139bbaac40d58ca8a78e9cf04 --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal.Target.cs @@ -0,0 +1,12 @@ +using UnrealBuildTool; +using System.Collections.Generic; + +public class GitWorldUnrealTarget : TargetRules +{ + public GitWorldUnrealTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Game; + DefaultBuildSettings = BuildSettingsVersion.V5; + ExtraModuleNames.Add("GitWorldUnreal"); + } +} diff --git a/phase2-unreal/Source/GitWorldUnreal/GitWorldUnreal.Build.cs b/phase2-unreal/Source/GitWorldUnreal/GitWorldUnreal.Build.cs new file mode 100644 index 0000000000000000000000000000000000000000..2ba04d5f3dba8824c897b4d8655dfcda78166f20 --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/GitWorldUnreal.Build.cs @@ -0,0 +1,46 @@ +using UnrealBuildTool; +using System; +using System.IO; + +public class GitWorldUnreal : ModuleRules +{ + public GitWorldUnreal(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + PublicDependencyModuleNames.AddRange(new string[] + { + "Core", + "CoreUObject", + "Engine", + "InputCore", + "MediaAssets", + "HeadMountedDisplay" + }); + PrivateDependencyModuleNames.AddRange(new string[] { "Projects" }); + + // CUDA is deliberately optional. The game still builds and runs with the + // deterministic CPU implementation when the CUDA DLL is absent. + PublicDefinitions.Add("GITWORLD_WITH_CUDA=0"); + string cudaLibrary = Environment.GetEnvironmentVariable("GITWORLD_CUDA_DLL"); + if (!string.IsNullOrWhiteSpace(cudaLibrary)) + { + PublicDefinitions.Add("GITWORLD_CUDA_DLL_CONFIGURED=1"); + } + + string openingVideo = Path.GetFullPath(Path.Combine( + ModuleDirectory, + "..", + "..", + "..", + "ReferenceSource", + "Opening", + "3099.mp4")); + if (File.Exists(openingVideo)) + { + RuntimeDependencies.Add( + "$(ProjectDir)/ReferenceSource/Opening/3099.mp4", + openingVideo, + StagedFileType.NonUFS); + } + } +} diff --git a/phase2-unreal/Source/GitWorldUnreal/GitWorldUnreal.cpp b/phase2-unreal/Source/GitWorldUnreal/GitWorldUnreal.cpp new file mode 100644 index 0000000000000000000000000000000000000000..59b1d9b1f7d6967b4c6bac812a3669ba6b1dd1e0 --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/GitWorldUnreal.cpp @@ -0,0 +1,3 @@ +#include "Modules/ModuleManager.h" + +IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, GitWorldUnreal, "GitWorldUnreal"); diff --git a/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldCudaBridge.cpp b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldCudaBridge.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c925bd6aaa98f42b5d589705649a1b59341f81d3 --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldCudaBridge.cpp @@ -0,0 +1,72 @@ +#include "GitWorldCudaBridge.h" + +#include "HAL/PlatformProcess.h" +#include "HAL/PlatformMisc.h" +#include "Misc/Paths.h" + +FGitWorldCudaBridge::FGitWorldCudaBridge() +{ + FString LibraryPath = FPlatformMisc::GetEnvironmentVariable(TEXT("GITWORLD_CUDA_DLL")); + if (LibraryPath.IsEmpty()) + { + LibraryPath = FPaths::Combine(FPaths::ProjectDir(), TEXT("Binaries/Win64/GitWorldCuda.dll")); + } + + if (!FPaths::FileExists(LibraryPath)) + { + return; + } + + CudaLibrary = FPlatformProcess::GetDllHandle(*LibraryPath); + if (CudaLibrary != nullptr) + { + CudaBuildPass = reinterpret_cast( + FPlatformProcess::GetDllExport(CudaLibrary, TEXT("GW_CudaBuildPass"))); + } +} + +FGitWorldCudaBridge::~FGitWorldCudaBridge() +{ + if (CudaLibrary != nullptr) + { + FPlatformProcess::FreeDllHandle(CudaLibrary); + } +} + +FGitWorldComputeResult FGitWorldCudaBridge::RunBuildPass(const FGitWorldRepoSnapshot& Snapshot) const +{ + TArray Input; + Input.Reserve(Snapshot.Districts.Num()); + for (const FGitWorldDistrictDefinition& District : Snapshot.Districts) + { + Input.Add(static_cast(District.ArtifactCount)); + } + + FGitWorldComputeResult Result; + if (CudaBuildPass != nullptr && Input.Num() > 0) + { + TArray Output; + Output.SetNumZeroed(Input.Num()); + int32 DeviceIndex = -1; + if (CudaBuildPass(Input.GetData(), Output.GetData(), Input.Num(), &DeviceIndex) != 0) + { + Result.Backend = TEXT("CUDA build-field pass"); + Result.bCudaAvailable = true; + Result.DeviceIndex = DeviceIndex; + for (const float Value : Output) + { + Result.WorkScore += Value; + } + return Result; + } + } + + // Deterministic fallback keeps the world playable when nvcc/CUDA DLLs are absent. + Result.Backend = TEXT("CPU fallback"); + Result.bCudaAvailable = false; + for (int32 Index = 0; Index < Input.Num(); ++Index) + { + Result.WorkScore += Input[Index] * (1.0f + static_cast(Index) * 0.125f); + } + return Result; +} diff --git a/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldGameMode.cpp b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldGameMode.cpp new file mode 100644 index 0000000000000000000000000000000000000000..49ac726972783707f3b0cca09d4830a0f149b50b --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldGameMode.cpp @@ -0,0 +1,84 @@ +#include "GitWorldGameMode.h" + +#include "Engine/World.h" +#include "GameFramework/PlayerController.h" +#include "GitWorldRepoPortal.h" +#include "GitWorldRepoWorld.h" +#include "GitWorldOpeningScene.h" +#include "GitWorldVRPawn.h" +#include "Kismet/GameplayStatics.h" + +AGitWorldGameMode::AGitWorldGameMode() +{ + DefaultPawnClass = AGitWorldVRPawn::StaticClass(); + PlayerControllerClass = APlayerController::StaticClass(); +} + +void AGitWorldGameMode::BeginPlay() +{ + Super::BeginPlay(); + + TArray ExistingOpeningScenes; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), AGitWorldOpeningScene::StaticClass(), ExistingOpeningScenes); + if (ExistingOpeningScenes.Num() == 0) + { + AGitWorldOpeningScene* OpeningScene = GetWorld()->SpawnActorDeferred( + AGitWorldOpeningScene::StaticClass(), + FTransform(FVector(0.0f, 0.0f, 1050.0f)), + this, + nullptr, + ESpawnActorCollisionHandlingMethod::AlwaysSpawn); + if (OpeningScene) + { + OpeningScene->OpeningVideoFile = TEXT("ReferenceSource/Opening/3099.mp4"); + OpeningScene->bPlayOnBegin = true; + OpeningScene->FinishSpawning(FTransform(FVector(0.0f, 0.0f, 1050.0f))); + } + } + + // The demo map is data-driven: every entry becomes a separate actor-owned + // repository sandbox. Replace this catalog with a DataAsset in the next phase. + SpawnDemoRepositoryWorld(TEXT("pax-coder"), TEXT("pax-coder"), FVector(0.0f, 0.0f, 0.0f), 213); + SpawnDemoRepositoryWorld(TEXT("agent-minefield"), TEXT("agent-minefield"), FVector(6000.0f, 0.0f, 0.0f), 404); + SpawnDemoRepositoryWorld(TEXT("cuda-lab"), TEXT("cuda-lab"), FVector(-6000.0f, 0.0f, 0.0f), 808); +} + +void AGitWorldGameMode::SpawnDemoRepositoryWorld( + const FName& RepositoryId, + const FString& InDisplayName, + const FVector& Location, + int32 InSeed) +{ + if (!GetWorld()) + { + return; + } + + AGitWorldRepoWorld* RepoWorld = GetWorld()->SpawnActorDeferred( + AGitWorldRepoWorld::StaticClass(), + FTransform(Location), + this, + nullptr, + ESpawnActorCollisionHandlingMethod::AlwaysSpawn); + if (RepoWorld) + { + RepoWorld->RepositoryId = RepositoryId; + RepoWorld->DisplayName = InDisplayName; + RepoWorld->Owner = TEXT("SNAPKITTYWEST"); + RepoWorld->Seed = InSeed; + RepoWorld->FinishSpawning(FTransform(Location)); + } + + AGitWorldRepoPortal* Portal = GetWorld()->SpawnActorDeferred( + AGitWorldRepoPortal::StaticClass(), + FTransform(Location + FVector(-1600.0f, 0.0f, 180.0f)), + this, + nullptr, + ESpawnActorCollisionHandlingMethod::AlwaysSpawn); + if (Portal) + { + Portal->TargetRepositoryId = RepositoryId; + Portal->EntryMessage = FString::Printf(TEXT("Entered %s repository sandbox."), *InDisplayName); + Portal->FinishSpawning(FTransform(Location + FVector(-1600.0f, 0.0f, 180.0f))); + } +} diff --git a/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldOpeningScene.cpp b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldOpeningScene.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1f8cfd89b8a076f0749bb0c7bb694177352299b4 --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldOpeningScene.cpp @@ -0,0 +1,115 @@ +#include "GitWorldOpeningScene.h" + +#include "Components/StaticMeshComponent.h" +#include "Engine/StaticMesh.h" +#include "HAL/PlatformFileManager.h" +#include "MediaPlayer.h" +#include "MediaTexture.h" +#include "Misc/Paths.h" +#include "UObject/ConstructorHelpers.h" + +AGitWorldOpeningScene::AGitWorldOpeningScene() +{ + PrimaryActorTick.bCanEverTick = false; + + Screen = CreateDefaultSubobject(TEXT("OpeningScreen")); + RootComponent = Screen; + Screen->SetCollisionEnabled(ECollisionEnabled::NoCollision); + Screen->SetMobility(EComponentMobility::Movable); + Screen->SetRelativeRotation(FRotator(90.0f, 0.0f, 0.0f)); + Screen->SetRelativeScale3D(FVector(16.0f, 9.0f, 1.0f)); + + static ConstructorHelpers::FObjectFinder PlaneMesh(TEXT("/Engine/BasicShapes/Plane.Plane")); + if (PlaneMesh.Succeeded()) + { + Screen->SetStaticMesh(PlaneMesh.Object); + } +} + +void AGitWorldOpeningScene::OnConstruction(const FTransform& Transform) +{ + Super::OnConstruction(Transform); + ApplyScreenMaterial(); +} + +void AGitWorldOpeningScene::BeginPlay() +{ + Super::BeginPlay(); + MediaPlayer = NewObject(this, TEXT("GitWorldOpeningMediaPlayer")); + MediaTexture = NewObject(this, TEXT("GitWorldOpeningMediaTexture")); + MediaTexture->SetMediaPlayer(MediaPlayer); + MediaTexture->UpdateResource(); + MediaPlayer->OnMediaOpened.AddDynamic(this, &AGitWorldOpeningScene::HandleMediaOpened); + MediaPlayer->OnEndReached.AddDynamic(this, &AGitWorldOpeningScene::HandleMediaEnd); + ApplyScreenMaterial(); + + if (bPlayOnBegin) + { + PlayOpening(); + } +} + +void AGitWorldOpeningScene::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + if (MediaPlayer) + { + MediaPlayer->Close(); + } + Super::EndPlay(EndPlayReason); +} + +void AGitWorldOpeningScene::PlayOpening() +{ + if (!MediaPlayer) + { + return; + } + + FString FilePath = OpeningVideoFile; + if (FPaths::IsRelative(FilePath)) + { + FilePath = FPaths::Combine(FPaths::ProjectDir(), FilePath); + } + FilePath = FPaths::ConvertRelativePathToFull(FilePath); + if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*FilePath)) + { + MediaPlayer->OpenUrl(FilePath); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("GitWorld opening video not found: %s"), *FilePath); + OnOpeningFinished.Broadcast(); + } +} + +void AGitWorldOpeningScene::SkipOpening() +{ + if (MediaPlayer) + { + MediaPlayer->Close(); + } + OnOpeningFinished.Broadcast(); +} + +bool AGitWorldOpeningScene::IsOpeningPlaying() const +{ + return MediaPlayer != nullptr && MediaPlayer->IsPlaying(); +} + +void AGitWorldOpeningScene::HandleMediaOpened(const FString& OpenedUrl) +{ + UE_LOG(LogTemp, Log, TEXT("GitWorld opening scene playing: %s"), *OpenedUrl); +} + +void AGitWorldOpeningScene::HandleMediaEnd() +{ + OnOpeningFinished.Broadcast(); +} + +void AGitWorldOpeningScene::ApplyScreenMaterial() +{ + if (Screen && ScreenMaterial) + { + Screen->SetMaterial(0, ScreenMaterial); + } +} diff --git a/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldRepoPortal.cpp b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldRepoPortal.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f9894a9eda22cef262f76ee7150cc317b7b5ebec --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldRepoPortal.cpp @@ -0,0 +1,45 @@ +#include "GitWorldRepoPortal.h" + +#include "Components/BoxComponent.h" +#include "GameFramework/Pawn.h" +#include "GitWorldWorldSubsystem.h" + +AGitWorldRepoPortal::AGitWorldRepoPortal() +{ + PrimaryActorTick.bCanEverTick = false; + Trigger = CreateDefaultSubobject(TEXT("Trigger")); + RootComponent = Trigger; + Trigger->SetBoxExtent(FVector(180.0f, 260.0f, 180.0f)); + Trigger->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + Trigger->SetCollisionResponseToAllChannels(ECR_Overlap); + Trigger->SetGenerateOverlapEvents(true); +} + +void AGitWorldRepoPortal::BeginPlay() +{ + Super::BeginPlay(); + Trigger->OnComponentBeginOverlap.AddDynamic(this, &AGitWorldRepoPortal::HandleOverlap); +} + +void AGitWorldRepoPortal::HandleOverlap( + UPrimitiveComponent* OverlappedComponent, + AActor* OtherActor, + UPrimitiveComponent* OtherComp, + int32 OtherBodyIndex, + bool bFromSweep, + const FHitResult& SweepResult) +{ + if (!OtherActor || !OtherActor->IsA()) + { + return; + } + + if (UGitWorldWorldSubsystem* Subsystem = GetWorld()->GetSubsystem()) + { + if (Subsystem->ActivateRepository(TargetRepositoryId)) + { + OnRepositoryEntered.Broadcast(TargetRepositoryId); + UE_LOG(LogTemp, Log, TEXT("GitWorld portal: %s"), *EntryMessage); + } + } +} diff --git a/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldRepoWorld.cpp b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldRepoWorld.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fca48c58706e130b1faeddae9d85a1cb8aace1e0 --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldRepoWorld.cpp @@ -0,0 +1,175 @@ +#include "GitWorldRepoWorld.h" + +#include "Components/BoxComponent.h" +#include "Components/HierarchicalInstancedStaticMeshComponent.h" +#include "Components/StaticMeshComponent.h" +#include "Components/TextRenderComponent.h" +#include "Engine/StaticMesh.h" +#include "Engine/World.h" +#include "GitWorldSimulation.h" +#include "GitWorldWorldSubsystem.h" +#include "UObject/ConstructorHelpers.h" + +AGitWorldRepoWorld::AGitWorldRepoWorld() +{ + PrimaryActorTick.bCanEverTick = true; + + WorldRoot = CreateDefaultSubobject(TEXT("WorldRoot")); + RootComponent = WorldRoot; + + WalkableBounds = CreateDefaultSubobject(TEXT("WalkableBounds")); + WalkableBounds->SetupAttachment(WorldRoot); + WalkableBounds->SetBoxExtent(WorldExtent * 0.5f); + WalkableBounds->SetRelativeLocation(FVector(0.0f, 0.0f, WorldExtent.Z * 0.5f)); + WalkableBounds->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + WalkableBounds->SetGenerateOverlapEvents(true); + + BuildBlocks = CreateDefaultSubobject(TEXT("BuildBlocks")); + BuildBlocks->SetupAttachment(WorldRoot); + BuildBlocks->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + + RealityScene = CreateDefaultSubobject(TEXT("RealityScene")); + RealityScene->SetupAttachment(WorldRoot); + RealityScene->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + RealityScene->SetMobility(EComponentMobility::Static); + + RepositorySign = CreateDefaultSubobject(TEXT("RepositorySign")); + RepositorySign->SetupAttachment(WorldRoot); + RepositorySign->SetHorizontalAlignment(EHTA_Center); + RepositorySign->SetVerticalAlignment(EVerticalTextAlignment::EVRTA_TextCenter); + RepositorySign->SetTextRenderColor(FColor(96, 242, 255)); + RepositorySign->SetWorldSize(64.0f); + RepositorySign->SetRelativeLocation(FVector(0.0f, 0.0f, WorldExtent.Z + 320.0f)); + + static ConstructorHelpers::FObjectFinder CubeMesh(TEXT("/Engine/BasicShapes/Cube.Cube")); + if (CubeMesh.Succeeded()) + { + BuildBlocks->SetStaticMesh(CubeMesh.Object); + } +} + +void AGitWorldRepoWorld::OnConstruction(const FTransform& Transform) +{ + Super::OnConstruction(Transform); + WalkableBounds->SetBoxExtent(WorldExtent * 0.5f); + WalkableBounds->SetRelativeLocation(FVector(0.0f, 0.0f, WorldExtent.Z * 0.5f)); + RepositorySign->SetRelativeLocation(FVector(0.0f, 0.0f, WorldExtent.Z + 320.0f)); + RealityScene->SetStaticMesh(RealityCaptureMesh); + RealityScene->SetRelativeTransform(RealitySceneTransform); + Snapshot = GitWorldSimulation::MakeSnapshot(RepositoryId, Owner, DisplayName, Seed); + RebuildFromSnapshot(); +} + +void AGitWorldRepoWorld::BeginPlay() +{ + Super::BeginPlay(); + if (Snapshot.RepositoryId != RepositoryId) + { + Snapshot = GitWorldSimulation::MakeSnapshot(RepositoryId, Owner, DisplayName, Seed); + } + if (UGitWorldWorldSubsystem* Subsystem = GetWorld()->GetSubsystem()) + { + Subsystem->RegisterRepoWorld(this); + } + RebuildFromSnapshot(); +} + +void AGitWorldRepoWorld::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + if (UWorld* World = GetWorld()) + { + if (UGitWorldWorldSubsystem* Subsystem = World->GetSubsystem()) + { + Subsystem->UnregisterRepoWorld(this); + } + } + Super::EndPlay(EndPlayReason); +} + +void AGitWorldRepoWorld::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + const int32 PreviousTick = Snapshot.BuildTick; + Snapshot = GitWorldSimulation::AdvanceSnapshot(Snapshot, DeltaSeconds); + if (Snapshot.BuildTick != PreviousTick) + { + RebuildFromSnapshot(); + } +} + +void AGitWorldRepoWorld::RebuildFromSnapshot() +{ + if (Snapshot.RepositoryId.IsNone() || Snapshot.RepositoryId != RepositoryId) + { + Snapshot = GitWorldSimulation::MakeSnapshot(RepositoryId, Owner, DisplayName, Seed); + } + + ComputeResult = ComputeBridge.RunBuildPass(Snapshot); + RealityScene->SetStaticMesh(RealityCaptureMesh); + RealityScene->SetRelativeTransform(RealitySceneTransform); + BuildDistrictMeshes(); + UpdateRepositorySign(); +} + +bool AGitWorldRepoWorld::ContainsWorldLocation(const FVector& WorldLocation) const +{ + const FVector LocalLocation = GetActorTransform().InverseTransformPosition(WorldLocation); + return GitWorldSimulation::IsWalkable(LocalLocation, WorldExtent); +} + +TArray AGitWorldRepoWorld::RunLocalTerminalCommand(const FString& Command) const +{ + return GitWorldSimulation::TerminalTranscript(Snapshot, Command); +} + +void AGitWorldRepoWorld::BuildDistrictMeshes() +{ + if (!BuildBlocks) + { + return; + } + + BuildBlocks->ClearInstances(); + const float DistrictWidth = WorldExtent.X * 0.38f; + const float DistrictDepth = WorldExtent.Y * 0.38f; + + for (const FGitWorldDistrictDefinition& District : Snapshot.Districts) + { + const FVector Center( + District.GridLocation.X * DistrictWidth, + District.GridLocation.Y * DistrictDepth, + 0.0f); + const int32 BlockCount = FMath::Clamp(District.ArtifactCount, 1, 64); + for (int32 BlockIndex = 0; BlockIndex < BlockCount; ++BlockIndex) + { + const int32 Row = BlockIndex % 6; + const int32 Column = BlockIndex / 6; + const float Height = 140.0f + 30.0f * static_cast((Snapshot.BuildTick + BlockIndex) % 6); + const FVector Location = Center + FVector( + (static_cast(Row) - 2.5f) * 120.0f, + (static_cast(Column) - 1.5f) * 120.0f, + Height * 0.5f); + const FTransform InstanceTransform( + FRotator::ZeroRotator, + Location, + FVector(0.85f, 0.85f, Height / 100.0f)); + BuildBlocks->AddInstance(InstanceTransform); + } + } +} + +void AGitWorldRepoWorld::UpdateRepositorySign() +{ + if (!RepositorySign) + { + return; + } + + const FString SignText = FString::Printf( + TEXT("%s / %s\nBUILD TICK %03d\n%s"), + *Owner, + *DisplayName, + Snapshot.BuildTick, + *ComputeResult.Backend); + RepositorySign->SetText(FText::FromString(SignText)); +} diff --git a/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldSimulation.cpp b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldSimulation.cpp new file mode 100644 index 0000000000000000000000000000000000000000..19c13923cc0fa05b52c099efc9ef19b734a96c0e --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldSimulation.cpp @@ -0,0 +1,156 @@ +#include "GitWorldSimulation.h" + +namespace +{ + FGitWorldDistrictDefinition MakeDistrict( + const TCHAR* Id, + const TCHAR* Label, + const FVector2D& GridLocation, + int32 ArtifactCount) + { + FGitWorldDistrictDefinition District; + District.Id = FName(Id); + District.DisplayName = FText::FromString(Label); + District.GridLocation = GridLocation; + District.ArtifactCount = ArtifactCount; + District.BuildState = EGitWorldBuildState::Ready; + return District; + } + + FGitWorldAgentState MakeAgent(const TCHAR* Id, const TCHAR* Role) + { + FGitWorldAgentState Agent; + Agent.AgentId = FName(Id); + Agent.Role = Role; + Agent.State = EGitWorldBuildState::Building; + Agent.Progress = 0.0f; + return Agent; + } + + FString StateLabel(EGitWorldBuildState State) + { + switch (State) + { + case EGitWorldBuildState::Queued: return TEXT("QUEUED"); + case EGitWorldBuildState::Building: return TEXT("BUILDING"); + case EGitWorldBuildState::Verifying: return TEXT("VERIFYING"); + case EGitWorldBuildState::Ready: return TEXT("READY"); + case EGitWorldBuildState::Failed: return TEXT("FAILED"); + default: return TEXT("UNKNOWN"); + } + } +} + +namespace GitWorldSimulation +{ + FGitWorldRepoSnapshot MakeSnapshot( + const FName& RepositoryId, + const FString& Owner, + const FString& DisplayName, + int32 Seed) + { + FGitWorldRepoSnapshot Snapshot; + Snapshot.RepositoryId = RepositoryId; + Snapshot.Owner = Owner; + Snapshot.DisplayName = DisplayName; + Snapshot.Seed = Seed; + Snapshot.Districts = { + MakeDistrict(TEXT("src"), TEXT("Source District"), FVector2D(-0.72f, 0.32f), 12), + MakeDistrict(TEXT("tests"), TEXT("Test Gardens"), FVector2D(-0.34f, -0.34f), 8), + MakeDistrict(TEXT("docs"), TEXT("Documentation Spire"), FVector2D(0.34f, 0.36f), 7), + MakeDistrict(TEXT("issues"), TEXT("Issue Bazaar"), FVector2D(0.72f, -0.12f), 6), + MakeDistrict(TEXT("pulls"), TEXT("Pull Request Gate"), FVector2D(0.0f, 0.02f), 5), + MakeDistrict(TEXT("actions"), TEXT("Actions Foundry"), FVector2D(0.50f, -0.62f), 9) + }; + Snapshot.Agents = { + MakeAgent(TEXT("world-architect"), TEXT("World Architect")), + MakeAgent(TEXT("reality-ui"), TEXT("Reality / UI")), + MakeAgent(TEXT("build-verifier"), TEXT("Build Verifier")) + }; + FGitWorldBuildEvent Event; + Event.EventId = TEXT("BOOT"); + Event.Message = FString::Printf(TEXT("%s/%s world loaded from deterministic seed %d."), *Owner, *DisplayName, Seed); + Snapshot.RecentEvents.Add(Event); + return Snapshot; + } + + FGitWorldRepoSnapshot AdvanceSnapshot( + const FGitWorldRepoSnapshot& Current, + float DeltaSeconds) + { + FGitWorldRepoSnapshot Next = Current; + Next.SimulationTime += FMath::Max(0.0f, DeltaSeconds); + if (Next.SimulationTime < 1.0f || Next.Districts.Num() == 0) + { + return Next; + } + + Next.SimulationTime = FMath::Fmod(Next.SimulationTime, 1.0f); + ++Next.BuildTick; + const int32 DistrictIndex = (Next.BuildTick - 1) % Next.Districts.Num(); + FGitWorldDistrictDefinition& District = Next.Districts[DistrictIndex]; + const int32 Phase = Next.BuildTick % 4; + District.BuildState = Phase == 0 ? EGitWorldBuildState::Ready + : Phase == 1 ? EGitWorldBuildState::Building + : Phase == 2 ? EGitWorldBuildState::Verifying + : EGitWorldBuildState::Ready; + + for (int32 AgentIndex = 0; AgentIndex < Next.Agents.Num(); ++AgentIndex) + { + FGitWorldAgentState& Agent = Next.Agents[AgentIndex]; + Agent.Progress = FMath::Fmod((Next.BuildTick * 0.19f) + AgentIndex * 0.27f, 1.0f); + Agent.State = AgentIndex == (Next.BuildTick % Next.Agents.Num()) + ? EGitWorldBuildState::Building + : EGitWorldBuildState::Verifying; + } + + FGitWorldBuildEvent Event; + Event.Tick = Next.BuildTick; + Event.EventId = FName(*FString::Printf(TEXT("TICK_%04d"), Next.BuildTick)); + Event.Message = FString::Printf( + TEXT("%s entered %s: %s."), + *Next.Agents[Next.BuildTick % Next.Agents.Num()].AgentId.ToString(), + *District.DisplayName.ToString(), + *StateLabel(District.BuildState)); + Next.RecentEvents.Insert(Event, 0); + if (Next.RecentEvents.Num() > 8) + { + Next.RecentEvents.SetNum(8); + } + return Next; + } + + bool IsWalkable(const FVector& Location, const FVector& WorldExtent) + { + return FMath::Abs(Location.X) <= WorldExtent.X * 0.5f + && FMath::Abs(Location.Y) <= WorldExtent.Y * 0.5f + && Location.Z >= 0.0f + && Location.Z <= WorldExtent.Z; + } + + TArray TerminalTranscript( + const FGitWorldRepoSnapshot& Snapshot, + const FString& Command) + { + TArray Lines; + Lines.Add(FString::Printf(TEXT("gitworld@%s:~$ %s"), *Snapshot.DisplayName, *Command)); + if (Command.Contains(TEXT("status"))) + { + Lines.Add(TEXT("On branch main")); + Lines.Add(FString::Printf(TEXT("build tick %d; simulated workspace clean"), Snapshot.BuildTick)); + } + else if (Command.Contains(TEXT("log"))) + { + for (const FGitWorldBuildEvent& Event : Snapshot.RecentEvents) + { + Lines.Add(FString::Printf(TEXT("%s %s"), *Event.EventId.ToString(), *Event.Message)); + } + } + else + { + Lines.Add(TEXT("local simulation: command accepted")); + Lines.Add(TEXT("no network, shell, or remote repository access")); + } + return Lines; + } +} diff --git a/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldVRPawn.cpp b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldVRPawn.cpp new file mode 100644 index 0000000000000000000000000000000000000000..dc4d46b0bcf1165ccd353e6ddd2df040e086e15f --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldVRPawn.cpp @@ -0,0 +1,204 @@ +#include "GitWorldVRPawn.h" + +#include "Camera/CameraComponent.h" +#include "Components/CapsuleComponent.h" +#include "DrawDebugHelpers.h" +#include "EngineUtils.h" +#include "GameFramework/PlayerController.h" +#include "HeadMountedDisplayFunctionLibrary.h" +#include "MotionControllerComponent.h" +#include "GitWorldOpeningScene.h" +#include "GitWorldRepoPortal.h" +#include "GitWorldRepoWorld.h" +#include "GitWorldWorldSubsystem.h" + +AGitWorldVRPawn::AGitWorldVRPawn() +{ + PrimaryActorTick.bCanEverTick = true; + AutoPossessPlayer = EAutoReceiveInput::Player0; + + CollisionCapsule = CreateDefaultSubobject(TEXT("CollisionCapsule")); + RootComponent = CollisionCapsule; + CollisionCapsule->InitCapsuleSize(34.0f, 96.0f); + CollisionCapsule->SetCollisionProfileName(TEXT("Pawn")); + + VROrigin = CreateDefaultSubobject(TEXT("VROrigin")); + VROrigin->SetupAttachment(CollisionCapsule); + + Camera = CreateDefaultSubobject(TEXT("VRCamera")); + Camera->SetupAttachment(VROrigin); + Camera->bUsePawnControlRotation = false; + Camera->bLockToHmd = true; + + LeftController = CreateDefaultSubobject(TEXT("LeftController")); + LeftController->SetupAttachment(VROrigin); + LeftController->SetTrackingSource(EControllerHand::Left); + + RightController = CreateDefaultSubobject(TEXT("RightController")); + RightController->SetupAttachment(VROrigin); + RightController->SetTrackingSource(EControllerHand::Right); +} + +void AGitWorldVRPawn::BeginPlay() +{ + Super::BeginPlay(); + UHeadMountedDisplayFunctionLibrary::SetTrackingOrigin(EHMDTrackingOrigin::Floor); +} + +void AGitWorldVRPawn::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + + if (!PendingMove.IsNearlyZero()) + { + const FVector DesiredMove = PendingMove.GetClampedToMaxSize(1.0f) * MoveSpeed * DeltaSeconds; + FHitResult Sweep; + AddActorWorldOffset(DesiredMove, true, &Sweep); + PendingMove = FVector::ZeroVector; + } + + if (bTeleportAiming) + { + FHitResult Hit; + if (TraceTeleport(Hit)) + { + DrawDebugLine(GetControllerOrigin(), Hit.ImpactPoint, FColor(128, 242, 255), false, 0.0f, 0, 3.0f); + DrawDebugSphere(Hit.ImpactPoint, 28.0f, 16, FColor(208, 76, 255), false, 0.0f, 0, 2.0f); + } + } +} + +void AGitWorldVRPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) +{ + Super::SetupPlayerInputComponent(PlayerInputComponent); + PlayerInputComponent->BindAxis(TEXT("MoveForward"), this, &AGitWorldVRPawn::MoveForward); + PlayerInputComponent->BindAxis(TEXT("MoveRight"), this, &AGitWorldVRPawn::MoveRight); + PlayerInputComponent->BindAxis(TEXT("Turn"), this, &AGitWorldVRPawn::Turn); + PlayerInputComponent->BindAction(TEXT("Interact"), IE_Pressed, this, &AGitWorldVRPawn::InteractWithWorld); + PlayerInputComponent->BindAction(TEXT("Teleport"), IE_Pressed, this, &AGitWorldVRPawn::StartTeleport); + PlayerInputComponent->BindAction(TEXT("Teleport"), IE_Released, this, &AGitWorldVRPawn::CompleteTeleport); + PlayerInputComponent->BindAction(TEXT("SkipOpening"), IE_Pressed, this, &AGitWorldVRPawn::SkipOpening); +} + +void AGitWorldVRPawn::MoveForward(float Value) +{ + if (FMath::IsNearlyZero(Value) || !Camera) + { + return; + } + FVector Direction = Camera->GetForwardVector(); + Direction.Z = 0.0f; + PendingMove += Direction.GetSafeNormal() * Value; +} + +void AGitWorldVRPawn::MoveRight(float Value) +{ + if (FMath::IsNearlyZero(Value) || !Camera) + { + return; + } + FVector Direction = Camera->GetRightVector(); + Direction.Z = 0.0f; + PendingMove += Direction.GetSafeNormal() * Value; +} + +void AGitWorldVRPawn::Turn(float Value) +{ + if (!FMath::IsNearlyZero(Value) && !UHeadMountedDisplayFunctionLibrary::IsHeadMountedDisplayEnabled()) + { + AddActorWorldRotation(FRotator(0.0f, Value * 1.5f, 0.0f)); + } +} + +void AGitWorldVRPawn::StartTeleport() +{ + bTeleportAiming = true; +} + +void AGitWorldVRPawn::CompleteTeleport() +{ + if (!bTeleportAiming) + { + return; + } + bTeleportAiming = false; + + FHitResult Hit; + if (TraceTeleport(Hit)) + { + const FVector Destination = Hit.ImpactPoint + FVector(0.0f, 0.0f, CollisionCapsule->GetScaledCapsuleHalfHeight()); + SetActorLocation(Destination, true); + } +} + +bool AGitWorldVRPawn::TraceTeleport(FHitResult& Hit) const +{ + if (!GetWorld()) + { + return false; + } + const FVector Start = GetControllerOrigin(); + const FVector End = Start + GetControllerDirection() * TeleportDistance; + FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(GitWorldTeleport), false, this); + return GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, QueryParams) + && Hit.ImpactNormal.Z > 0.55f; +} + +FVector AGitWorldVRPawn::GetControllerOrigin() const +{ + return RightController && RightController->IsTracked() + ? RightController->GetComponentLocation() + : Camera->GetComponentLocation(); +} + +FVector AGitWorldVRPawn::GetControllerDirection() const +{ + return RightController && RightController->IsTracked() + ? RightController->GetForwardVector() + : Camera->GetForwardVector(); +} + +void AGitWorldVRPawn::InteractWithWorld() +{ + if (!GetWorld()) + { + return; + } + FHitResult Hit; + const FVector Start = GetControllerOrigin(); + const FVector End = Start + GetControllerDirection() * 1000.0f; + FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(GitWorldInteract), false, this); + if (!GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, QueryParams)) + { + return; + } + + if (AGitWorldRepoPortal* Portal = Cast(Hit.GetActor())) + { + if (UGitWorldWorldSubsystem* Subsystem = GetWorld()->GetSubsystem()) + { + Subsystem->ActivateRepository(Portal->TargetRepositoryId); + } + return; + } + + if (AGitWorldRepoWorld* RepoWorld = Cast(Hit.GetActor())) + { + if (UGitWorldWorldSubsystem* Subsystem = GetWorld()->GetSubsystem()) + { + Subsystem->ActivateRepository(RepoWorld->RepositoryId); + } + } +} + +void AGitWorldVRPawn::SkipOpening() +{ + if (!GetWorld()) + { + return; + } + for (TActorIterator It(GetWorld()); It; ++It) + { + It->SkipOpening(); + } +} diff --git a/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldWorldSubsystem.cpp b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldWorldSubsystem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ec3b193ae521e2129d1271b44f3ff6ab4d35e1c6 --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Private/GitWorldWorldSubsystem.cpp @@ -0,0 +1,58 @@ +#include "GitWorldWorldSubsystem.h" + +#include "GitWorldRepoWorld.h" + +void UGitWorldWorldSubsystem::RegisterRepoWorld(AGitWorldRepoWorld* RepoWorld) +{ + if (IsValid(RepoWorld)) + { + RegisteredWorlds.Add(RepoWorld->RepositoryId, RepoWorld); + if (ActiveRepository.IsNone()) + { + ActiveRepository = RepoWorld->RepositoryId; + } + } +} + +void UGitWorldWorldSubsystem::UnregisterRepoWorld(AGitWorldRepoWorld* RepoWorld) +{ + if (!IsValid(RepoWorld)) + { + return; + } + + RegisteredWorlds.Remove(RepoWorld->RepositoryId); + if (ActiveRepository == RepoWorld->RepositoryId) + { + ActiveRepository = NAME_None; + for (const TPair>& Pair : RegisteredWorlds) + { + ActiveRepository = Pair.Key; + break; + } + } +} + +bool UGitWorldWorldSubsystem::ActivateRepository(FName RepositoryId) +{ + if (RegisteredWorlds.Contains(RepositoryId)) + { + ActiveRepository = RepositoryId; + return true; + } + return false; +} + +AGitWorldRepoWorld* UGitWorldWorldSubsystem::GetActiveWorld() const +{ + const TObjectPtr* Found = RegisteredWorlds.Find(ActiveRepository); + return Found != nullptr ? Found->Get() : nullptr; +} + +TArray UGitWorldWorldSubsystem::GetRegisteredRepositoryIds() const +{ + TArray Result; + RegisteredWorlds.GetKeys(Result); + Result.Sort(FNameLexicalLess()); + return Result; +} diff --git a/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldCudaBridge.h b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldCudaBridge.h new file mode 100644 index 0000000000000000000000000000000000000000..420b66ddeeb3ba36945a95647cfd6f108b5206d1 --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldCudaBridge.h @@ -0,0 +1,22 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GitWorldData.h" + +class GITWORLDUNREAL_API FGitWorldCudaBridge +{ +public: + FGitWorldCudaBridge(); + ~FGitWorldCudaBridge(); + + FGitWorldCudaBridge(const FGitWorldCudaBridge&) = delete; + FGitWorldCudaBridge& operator=(const FGitWorldCudaBridge&) = delete; + + bool IsCudaAvailable() const { return CudaBuildPass != nullptr; } + FGitWorldComputeResult RunBuildPass(const FGitWorldRepoSnapshot& Snapshot) const; + +private: + void* CudaLibrary = nullptr; + using FCudaBuildPass = int (*)(const float* Input, float* Output, int Count, int* DeviceIndex); + FCudaBuildPass CudaBuildPass = nullptr; +}; diff --git a/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldData.h b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldData.h new file mode 100644 index 0000000000000000000000000000000000000000..f933565f36cc56bc57851746f8076db634d8894b --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldData.h @@ -0,0 +1,119 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GitWorldData.generated.h" + +UENUM(BlueprintType) +enum class EGitWorldBuildState : uint8 +{ + Queued, + Building, + Verifying, + Ready, + Failed +}; + +USTRUCT(BlueprintType) +struct FGitWorldDistrictDefinition +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + FName Id; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + FText DisplayName; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + FVector2D GridLocation = FVector2D::ZeroVector; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + int32 ArtifactCount = 1; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + EGitWorldBuildState BuildState = EGitWorldBuildState::Queued; +}; + +USTRUCT(BlueprintType) +struct FGitWorldAgentState +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + FName AgentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + FString Role; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + EGitWorldBuildState State = EGitWorldBuildState::Queued; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + float Progress = 0.0f; +}; + +USTRUCT(BlueprintType) +struct FGitWorldBuildEvent +{ + GENERATED_BODY() + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld") + int32 Tick = 0; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld") + FName EventId; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld") + FString Message; +}; + +USTRUCT(BlueprintType) +struct FGitWorldRepoSnapshot +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + FName RepositoryId = TEXT("pax-coder"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + FString Owner = TEXT("SNAPKITTYWEST"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + FString DisplayName = TEXT("pax-coder"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld") + int32 Seed = 213; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld") + int32 BuildTick = 0; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld") + float SimulationTime = 0.0f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld") + TArray Districts; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld") + TArray Agents; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld") + TArray RecentEvents; +}; + +USTRUCT(BlueprintType) +struct FGitWorldComputeResult +{ + GENERATED_BODY() + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|Compute") + FString Backend = TEXT("CPU fallback"); + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|Compute") + bool bCudaAvailable = false; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|Compute") + int32 DeviceIndex = -1; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|Compute") + float WorkScore = 0.0f; +}; diff --git a/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldGameMode.h b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldGameMode.h new file mode 100644 index 0000000000000000000000000000000000000000..8acdd567aa8537de46f91b28ea8fdb53142f463a --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldGameMode.h @@ -0,0 +1,22 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameModeBase.h" +#include "GitWorldGameMode.generated.h" + +UCLASS() +class GITWORLDUNREAL_API AGitWorldGameMode : public AGameModeBase +{ + GENERATED_BODY() + +public: + AGitWorldGameMode(); + virtual void BeginPlay() override; + +private: + void SpawnDemoRepositoryWorld( + const FName& RepositoryId, + const FString& DisplayName, + const FVector& Location, + int32 Seed); +}; diff --git a/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldOpeningScene.h b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldOpeningScene.h new file mode 100644 index 0000000000000000000000000000000000000000..35d0af4c0000e573368a1fdb3392aa50284085da --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldOpeningScene.h @@ -0,0 +1,64 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "GitWorldOpeningScene.generated.h" + +class UMediaPlayer; +class UMediaTexture; +class UMaterialInterface; +class UStaticMeshComponent; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGitWorldOpeningFinished); + +UCLASS(Blueprintable) +class GITWORLDUNREAL_API AGitWorldOpeningScene : public AActor +{ + GENERATED_BODY() + +public: + AGitWorldOpeningScene(); + + virtual void OnConstruction(const FTransform& Transform) override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Opening") + FString OpeningVideoFile = TEXT("ReferenceSource/Opening/3099.mp4"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Opening") + bool bPlayOnBegin = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Opening") + TObjectPtr ScreenMaterial; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|Opening") + TObjectPtr Screen; + + UPROPERTY(BlueprintAssignable, Category="GitWorld|Opening") + FGitWorldOpeningFinished OnOpeningFinished; + + UFUNCTION(BlueprintCallable, Category="GitWorld|Opening") + void PlayOpening(); + + UFUNCTION(BlueprintCallable, Category="GitWorld|Opening") + void SkipOpening(); + + UFUNCTION(BlueprintPure, Category="GitWorld|Opening") + bool IsOpeningPlaying() const; + +private: + UPROPERTY(Transient) + TObjectPtr MediaPlayer; + + UPROPERTY(Transient) + TObjectPtr MediaTexture; + + UFUNCTION() + void HandleMediaOpened(const FString& OpenedUrl); + + UFUNCTION() + void HandleMediaEnd(); + + void ApplyScreenMaterial(); +}; diff --git a/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldRepoPortal.h b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldRepoPortal.h new file mode 100644 index 0000000000000000000000000000000000000000..59e6d29af5b8008865ed4169fc2ac03a582fdedd --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldRepoPortal.h @@ -0,0 +1,42 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "GitWorldRepoPortal.generated.h" + +class UBoxComponent; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGitWorldRepositoryEntered, FName, RepositoryId); + +UCLASS(Blueprintable) +class GITWORLDUNREAL_API AGitWorldRepoPortal : public AActor +{ + GENERATED_BODY() + +public: + AGitWorldRepoPortal(); + + virtual void BeginPlay() override; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Portal") + FName TargetRepositoryId = TEXT("pax-coder"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Portal") + FString EntryMessage = TEXT("Repository world entered."); + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|Portal") + TObjectPtr Trigger; + + UPROPERTY(BlueprintAssignable, Category="GitWorld|Portal") + FGitWorldRepositoryEntered OnRepositoryEntered; + +private: + UFUNCTION() + void HandleOverlap( + UPrimitiveComponent* OverlappedComponent, + AActor* OtherActor, + UPrimitiveComponent* OtherComp, + int32 OtherBodyIndex, + bool bFromSweep, + const FHitResult& SweepResult); +}; diff --git a/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldRepoWorld.h b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldRepoWorld.h new file mode 100644 index 0000000000000000000000000000000000000000..c030cd6060abc01e3245aac8e4fbd7235e6c6a50 --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldRepoWorld.h @@ -0,0 +1,91 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "GitWorldData.h" +#include "GitWorldCudaBridge.h" +#include "GitWorldRepoWorld.generated.h" + +class UBoxComponent; +class UHierarchicalInstancedStaticMeshComponent; +class UStaticMesh; +class UStaticMeshComponent; +class UTextRenderComponent; +class UTexture2D; + +UCLASS(Blueprintable) +class GITWORLDUNREAL_API AGitWorldRepoWorld : public AActor +{ + GENERATED_BODY() + +public: + AGitWorldRepoWorld(); + + virtual void OnConstruction(const FTransform& Transform) override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + virtual void Tick(float DeltaSeconds) override; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Repository") + FName RepositoryId = TEXT("pax-coder"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Repository") + FString Owner = TEXT("SNAPKITTYWEST"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Repository") + FString DisplayName = TEXT("pax-coder"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Repository") + int32 Seed = 213; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Repository") + FVector WorldExtent = FVector(3600.0f, 2400.0f, 900.0f); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Reality Capture") + TObjectPtr RealityCaptureMesh; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Reality Capture") + FTransform RealitySceneTransform = FTransform::Identity; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Reality Capture") + FName ReferenceSetId = TEXT("neon-reflections-v15"); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|Reality Capture") + TArray> ReferenceImages; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|Repository") + FGitWorldRepoSnapshot Snapshot; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|Compute") + FGitWorldComputeResult ComputeResult; + + UFUNCTION(BlueprintCallable, Category="GitWorld|Repository") + void RebuildFromSnapshot(); + + UFUNCTION(BlueprintPure, Category="GitWorld|Repository") + bool ContainsWorldLocation(const FVector& WorldLocation) const; + + UFUNCTION(BlueprintPure, Category="GitWorld|Repository") + TArray RunLocalTerminalCommand(const FString& Command) const; + +private: + UPROPERTY(VisibleAnywhere, Category="GitWorld|Components") + TObjectPtr WorldRoot; + + UPROPERTY(VisibleAnywhere, Category="GitWorld|Components") + TObjectPtr WalkableBounds; + + UPROPERTY(VisibleAnywhere, Category="GitWorld|Components") + TObjectPtr BuildBlocks; + + UPROPERTY(VisibleAnywhere, Category="GitWorld|Components") + TObjectPtr RealityScene; + + UPROPERTY(VisibleAnywhere, Category="GitWorld|Components") + TObjectPtr RepositorySign; + + FGitWorldCudaBridge ComputeBridge; + + void BuildDistrictMeshes(); + void UpdateRepositorySign(); +}; diff --git a/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldSimulation.h b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldSimulation.h new file mode 100644 index 0000000000000000000000000000000000000000..8a3693dfcdd4ce902acc5e348b477895174c9d9b --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldSimulation.h @@ -0,0 +1,25 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GitWorldData.h" + +namespace GitWorldSimulation +{ + GITWORLDUNREAL_API FGitWorldRepoSnapshot MakeSnapshot( + const FName& RepositoryId, + const FString& Owner, + const FString& DisplayName, + int32 Seed); + + GITWORLDUNREAL_API FGitWorldRepoSnapshot AdvanceSnapshot( + const FGitWorldRepoSnapshot& Current, + float DeltaSeconds); + + GITWORLDUNREAL_API bool IsWalkable( + const FVector& Location, + const FVector& WorldExtent); + + GITWORLDUNREAL_API TArray TerminalTranscript( + const FGitWorldRepoSnapshot& Snapshot, + const FString& Command); +} diff --git a/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldVRPawn.h b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldVRPawn.h new file mode 100644 index 0000000000000000000000000000000000000000..ef1921190ab3a83fc29311154ee329ff671ebc7f --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldVRPawn.h @@ -0,0 +1,63 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Pawn.h" +#include "GitWorldVRPawn.generated.h" + +class UCameraComponent; +class UCapsuleComponent; +class UMotionControllerComponent; +class USceneComponent; + +UCLASS(Blueprintable) +class GITWORLDUNREAL_API AGitWorldVRPawn : public APawn +{ + GENERATED_BODY() + +public: + AGitWorldVRPawn(); + + virtual void BeginPlay() override; + virtual void Tick(float DeltaSeconds) override; + virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|VR") + float MoveSpeed = 240.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GitWorld|VR") + float TeleportDistance = 1800.0f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|VR") + TObjectPtr CollisionCapsule; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|VR") + TObjectPtr VROrigin; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|VR") + TObjectPtr Camera; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|VR") + TObjectPtr LeftController; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GitWorld|VR") + TObjectPtr RightController; + + UFUNCTION(BlueprintCallable, Category="GitWorld|VR") + void InteractWithWorld(); + + UFUNCTION(BlueprintCallable, Category="GitWorld|VR") + void SkipOpening(); + +private: + FVector PendingMove = FVector::ZeroVector; + bool bTeleportAiming = false; + + void MoveForward(float Value); + void MoveRight(float Value); + void Turn(float Value); + void StartTeleport(); + void CompleteTeleport(); + bool TraceTeleport(FHitResult& Hit) const; + FVector GetControllerOrigin() const; + FVector GetControllerDirection() const; +}; diff --git a/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldWorldSubsystem.h b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldWorldSubsystem.h new file mode 100644 index 0000000000000000000000000000000000000000..e26411df9a6d80ae4b621b543eb362b2bf62233f --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnreal/Public/GitWorldWorldSubsystem.h @@ -0,0 +1,36 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/WorldSubsystem.h" +#include "GitWorldWorldSubsystem.generated.h" + +class AGitWorldRepoWorld; + +UCLASS() +class GITWORLDUNREAL_API UGitWorldWorldSubsystem : public UWorldSubsystem +{ + GENERATED_BODY() + +public: + void RegisterRepoWorld(AGitWorldRepoWorld* RepoWorld); + void UnregisterRepoWorld(AGitWorldRepoWorld* RepoWorld); + + UFUNCTION(BlueprintCallable, Category="GitWorld") + bool ActivateRepository(FName RepositoryId); + + UFUNCTION(BlueprintPure, Category="GitWorld") + FName GetActiveRepository() const { return ActiveRepository; } + + UFUNCTION(BlueprintPure, Category="GitWorld") + AGitWorldRepoWorld* GetActiveWorld() const; + + UFUNCTION(BlueprintPure, Category="GitWorld") + TArray GetRegisteredRepositoryIds() const; + +private: + UPROPERTY() + TMap> RegisteredWorlds; + + UPROPERTY() + FName ActiveRepository; +}; diff --git a/phase2-unreal/Source/GitWorldUnrealEditor.Target.cs b/phase2-unreal/Source/GitWorldUnrealEditor.Target.cs new file mode 100644 index 0000000000000000000000000000000000000000..dd29eb3d83e76b204a467c29d083593cbb3ccd75 --- /dev/null +++ b/phase2-unreal/Source/GitWorldUnrealEditor.Target.cs @@ -0,0 +1,12 @@ +using UnrealBuildTool; +using System.Collections.Generic; + +public class GitWorldUnrealEditorTarget : TargetRules +{ + public GitWorldUnrealEditorTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Editor; + DefaultBuildSettings = BuildSettingsVersion.V5; + ExtraModuleNames.Add("GitWorldUnreal"); + } +} diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000000000000000000000000000000000000..1735c8461887adf3a67bfa41a221a68ce9778413 --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,55 @@ +import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); +const dist = join(root, 'dist'); +const referenceSource = join(root, 'phase2-unreal', 'ReferenceSource'); +const publishedReferenceSource = join(dist, 'phase2-unreal', 'ReferenceSource'); + +await mkdir(dist, { recursive: true }); +await cp(referenceSource, publishedReferenceSource, { recursive: true, force: true }); +await copyIfChanged(join(root, 'styles.css'), join(dist, 'styles.css')); +await copyIfChanged(join(root, 'src', 'main.ts'), join(dist, 'main.js')); +for (const moduleName of ['shared.js', 'world-architect.js', 'ui-reality.js']) { + await copyIfChanged(join(root, 'src', moduleName), join(dist, moduleName)); +} + +const builtHtml = (await BunlessHtml(await import('node:fs/promises'))) + .replace('./src/main.ts', './main.js'); +await writeIfChanged(join(dist, 'index.html'), builtHtml); +console.log(`GitWorld build ready: ${dist}`); + +async function copyIfChanged(source, destination) { + if (await filesMatch(source, destination)) { + return; + } + await cp(source, destination); +} + +async function writeIfChanged(destination, contents) { + try { + if ((await readFile(destination, 'utf8')) === contents) { + return; + } + } catch { + // The destination does not exist yet. + } + await writeFile(destination, contents, 'utf8'); +} + +async function filesMatch(left, right) { + try { + const [leftContents, rightContents] = await Promise.all([ + readFile(left), + readFile(right) + ]); + return leftContents.equals(rightContents); + } catch { + return false; + } +} + +async function BunlessHtml(fs) { + return fs.readFile(join(root, 'index.html'), 'utf8'); +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a8c2020f5b3f57c3e047881b27f544b944237ef --- /dev/null +++ b/src/main.ts @@ -0,0 +1,167 @@ +import { EVENT_TYPES, TOOLBELT, commandTranscript } from './shared.js'; +import { + advanceWorldState, + createWorldState, + getNearbyDistrict, + renderMinimap, + renderWorld, + worldReducer +} from './world-architect.js'; +import { bindUi, renderHud } from './ui-reality.js'; + +const app = document; +const worldViewport = app.getElementById('world-viewport'); +const minimap = app.getElementById('minimap-viewport'); +let state = createWorldState(); + +function assetPath(relativePath) { + const root = './phase2-unreal/ReferenceSource/'; + const UrlConstructor = window.URL; + return new UrlConstructor(root + relativePath, window.location.href).href; +} + +function setupBrowserScene() { + const opening = app.getElementById('opening-scene'); + const video = app.getElementById('opening-video'); + const enter = app.querySelector('[data-action="enter-game"]'); + const sound = app.querySelector('[data-action="toggle-opening-sound"]'); + const status = app.getElementById('opening-status'); + if (!opening || !video || !enter) return; + + const enterWorld = () => { + opening.classList.add('is-dismissed'); + opening.setAttribute('aria-hidden', 'true'); + video.pause(); + worldViewport?.focus(); + }; + + try { + video.src = assetPath('Opening/3099.mp4'); + video.poster = assetPath('NeonReflections/3090.webp'); + video.load(); + const openingFallback = window.setTimeout(() => { + if (video.readyState < 2) enterWorld(); + }, 2500); + video.play().catch(() => { + if (status) status.textContent = 'Opening scene ready · press ENTER THE WORLD'; + }); + video.addEventListener('playing', () => { + window.clearTimeout(openingFallback); + if (status) status.textContent = 'Playing 3099.mp4 · opening scene'; + }, { once: true }); + video.addEventListener('error', () => { + if (status) status.textContent = 'Opening media unavailable · entering browser scene'; + window.setTimeout(enterWorld, 700); + }, { once: true }); + } catch { + if (status) status.textContent = 'Opening media unavailable · entering browser scene'; + enterWorld(); + } + + enter.addEventListener('click', enterWorld); + sound?.addEventListener('click', () => { + video.muted = !video.muted; + sound.textContent = video.muted ? 'SOUND OFF' : 'SOUND ON'; + }); + + try { + app.querySelectorAll('[data-reference-image]').forEach((plate) => { + plate.setAttribute('poster', assetPath(plate.dataset.referenceImage || '')); + }); + } catch { + // Reference plates are optional presentation assets; never block gameplay. + } +} + +function dispatch(event) { + state = worldReducer(state, event); + window.gitWorldState = state; + render(); +} + +function render() { + const nearby = getNearbyDistrict(state); + if (worldViewport) renderWorld(worldViewport, state, (districtId) => dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId })); + if (minimap) renderMinimap(minimap, state); + renderHud(app, state, nearby); +} + +function move(dx, dy) { + dispatch({ type: EVENT_TYPES.PLAYER_MOVE, dx, dy }); +} + +function useToolByKey(key) { + const tool = TOOLBELT.find((candidate) => candidate.key === key); + if (!tool) return; + dispatch({ type: EVENT_TYPES.USE_TOOL, toolId: tool.id, message: `${tool.name}: ${tool.verb}.` }); +} + +function inspectNearest() { + const nearby = getNearbyDistrict(state); + if (nearby.inRange) { + dispatch({ type: EVENT_TYPES.ENTER_DISTRICT, districtId: nearby.district.id }); + } else { + dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId: nearby.district.id }); + } +} + +function onKeyDown(event) { + const target = event.target; + const typing = target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement; + if (typing && event.key !== 'Escape') return; + const key = event.key.toLowerCase(); + const movement = { + w: [0, -1], arrowup: [0, -1], + s: [0, 1], arrowdown: [0, 1], + a: [-1, 0], arrowleft: [-1, 0], + d: [1, 0], arrowright: [1, 0] + }[key]; + if (movement) { + event.preventDefault(); + move(movement[0], movement[1]); + return; + } + if (key === 'e' || event.key === 'Enter') { + event.preventDefault(); + inspectNearest(); + return; + } + if (key === 't') { + event.preventDefault(); + dispatch({ type: EVENT_TYPES.OPEN_TERMINAL }); + return; + } + if (key === 'i') { + event.preventDefault(); + dispatch({ type: EVENT_TYPES.OPEN_INSPECTOR }); + return; + } + if (/^[1-8]$/.test(event.key)) { + event.preventDefault(); + useToolByKey(event.key); + return; + } + if (event.key === 'Escape') { + dispatch(state.terminal.open ? { type: EVENT_TYPES.CLOSE_TERMINAL } : { type: EVENT_TYPES.CLOSE_INSPECTOR }); + } +} + +bindUi(app, dispatch); +app.addEventListener('keydown', onKeyDown); +window.addEventListener('resize', render); +window.gitWorldState = state; +render(); +setupBrowserScene(); + +// Fixed-step browser-local simulation: the world advances on a deterministic clock, +// while the render layer simply reflects the latest serializable state. +window.setInterval(() => { + const advanced = advanceWorldState(state); + state = advanced.state; + window.gitWorldState = state; + render(); +}, 1600); + +// Keep the transcript helper reachable for the UI's delegated terminal buttons in +// environments where the app is embedded without a module bundler. +window.gitWorldCommandTranscript = commandTranscript; diff --git a/src/shared.js b/src/shared.js new file mode 100644 index 0000000000000000000000000000000000000000..e7c80eacb2e8ff21a414f37e96c9b17d1952dff0 --- /dev/null +++ b/src/shared.js @@ -0,0 +1,90 @@ +export const EVENT_TYPES = Object.freeze({ + PLAYER_MOVE: 'player.move', + SELECT_DISTRICT: 'district.selected', + SELECT_FILE: 'file.selected', + SELECT_COLLECTION: 'collection.selected', + SELECT_RECORD: 'record.selected', + OPEN_INSPECTOR: 'inspector.opened', + CLOSE_INSPECTOR: 'inspector.closed', + OPEN_TERMINAL: 'terminal.opened', + CLOSE_TERMINAL: 'terminal.closed', + RUN_COMMAND: 'terminal.command', + USE_TOOL: 'tool.used', + ENTER_DISTRICT: 'district.entered', + TICK: 'world.tick', + AGENT_PROGRESS: 'agent.progress', + CONSTRUCTION: 'construction.updated', + BUILD_STARTED: 'build.started', + BUILD_COMPLETED: 'build.completed', + TEST_STARTED: 'test.started', + TEST_COMPLETED: 'test.completed', + DEPLOYMENT_STARTED: 'deployment.started', + DEPLOYMENT_COMPLETED: 'deployment.completed', + COMMIT: 'repository.commit', + ISSUE: 'repository.issue', + PULL_REQUEST: 'repository.pull_request' +}); + +export const TOOLBELT = Object.freeze([ + { id: 'terminal', key: '1', icon: '▣', name: 'Terminal Phone', verb: 'Open shell', color: '#69f6cf' }, + { id: 'bash', key: '2', icon: '⚒', name: 'Bash Hammer', verb: 'Run operation', color: '#ffbd69' }, + { id: 'git', key: '3', icon: '◈', name: 'Git Backpack', verb: 'View history', color: '#ff7ac8' }, + { id: 'docker', key: '4', icon: '▦', name: 'Docker Box', verb: 'Open container', color: '#6dafff' }, + { id: 'code', key: '5', icon: '⌘', name: 'Code Tablet', verb: 'Read source', color: '#b79cff' }, + { id: 'ssh', key: '6', icon: '⚿', name: 'SSH Key', verb: 'Unlock gate', color: '#ffdd67' }, + { id: 'database', key: '7', icon: '▤', name: 'Database Vault', verb: 'Inspect data', color: '#9fe870' }, + { id: 'deploy', key: '8', icon: '↗', name: 'Deploy Rocket', verb: 'Launch build', color: '#ff8b62' } +]); + +export const EVENT_LABELS = Object.freeze({ + [EVENT_TYPES.AGENT_PROGRESS]: 'agent progress', + [EVENT_TYPES.CONSTRUCTION]: 'construction update', + [EVENT_TYPES.BUILD_STARTED]: 'build started', + [EVENT_TYPES.BUILD_COMPLETED]: 'build completed', + [EVENT_TYPES.TEST_STARTED]: 'tests started', + [EVENT_TYPES.TEST_COMPLETED]: 'tests passed', + [EVENT_TYPES.DEPLOYMENT_STARTED]: 'deployment queued', + [EVENT_TYPES.DEPLOYMENT_COMPLETED]: 'deployment complete', + [EVENT_TYPES.COMMIT]: 'commit landed', + [EVENT_TYPES.ISSUE]: 'issue quest', + [EVENT_TYPES.PULL_REQUEST]: 'pull request gate' +}); + +export const COLLECTIONS = Object.freeze(['files', 'commits', 'issues', 'prs', 'build']); + +export const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); + +export function formatTime(tick) { + const seconds = String((tick * 7) % 60).padStart(2, '0'); + const minutes = String(Math.floor(tick * 7 / 60)).padStart(2, '0'); + return `00:${minutes}:${seconds}`; +} + +export function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +export function makeEvent(type, tick, detail = {}) { + return { + id: `${type}-${tick}`, + type, + tick, + at: formatTime(tick), + detail + }; +} + +export function commandTranscript(command, state) { + const prompt = '$'; + const selected = state.selectedFileId || 'README.md'; + const lines = { + ls: ['src/', 'tests/', 'docs/', '.github/', 'Cargo.toml', 'README.md'], + 'git status': ['On branch main', 'Your branch is up to date with origin/main.', 'working tree clean'], + 'git log': ['8f3a9c1 Fix kernel bounds', '2h4d7e2 Add proof sketch', '9ac1bd9 Update docs'], + 'cargo test': ['Compiling pax-coder v0.8.2', 'running 142 tests', 'test result: ok. 142 passed; 0 failed'], + './build.sh': ['[world] assembling repository tower', '[ci] verifying artifacts', 'BUILD #812 completed successfully'], + help: ['Try: ls, git status, git log, cargo test, ./build.sh'] + }; + const output = lines[command] || [`command not found: ${command}`, 'Try `help` for local commands.']; + return [`${prompt} ${command}`, ...output, `${prompt} _`]; +} diff --git a/src/ui-reality.js b/src/ui-reality.js new file mode 100644 index 0000000000000000000000000000000000000000..ad113d9c9c7aa786f35a3598ef9488cd054a68ac --- /dev/null +++ b/src/ui-reality.js @@ -0,0 +1,210 @@ +import { + COLLECTIONS, + EVENT_LABELS, + EVENT_TYPES, + TOOLBELT, + commandTranscript, + formatTime +} from './shared.js'; + +function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[character])); +} + +function byId(root, id) { + return root.getElementById(id); +} + +function setText(root, id, value) { + const element = byId(root, id); + if (element) element.textContent = value; +} + +function selectedDistrict(state) { + return state.districts.find((district) => district.id === state.selectedDistrictId) || state.districts[0]; +} + +function selectedFile(state) { + const district = selectedDistrict(state); + return district.files.find((file) => file.id === state.selectedFileId) || district.files[0]; +} + +function eventMessage(event) { + return event.detail?.message || EVENT_LABELS[event.type] || event.type; +} + +function renderAgents(root, state) { + const agentList = byId(root, 'agent-list'); + if (!agentList) return; + agentList.innerHTML = state.agents.map((agent) => `
  • + ${agent.name.slice(0, 2)}${escapeHtml(agent.name)}${escapeHtml(agent.role)}${escapeHtml(agent.task)}● ${escapeHtml(agent.status)} +
  • `).join(''); + setText(root, 'active-agent-count', String(state.agents.length)); +} + +function renderEvents(root, state) { + const feed = byId(root, 'event-feed'); + if (!feed) return; + feed.innerHTML = state.events.slice(0, 7).map((event) => `
  • ${escapeHtml(eventMessage(event))}

  • `).join(''); + setText(root, 'event-count', `${state.events.length} EVENTS`); +} + +function renderToolbelt(root, state) { + const toolbelt = byId(root, 'toolbelt'); + if (!toolbelt) return; + toolbelt.innerHTML = TOOLBELT.map((tool) => ``).join(''); +} + +function renderStatus(root, state) { + setText(root, 'repo-visibility', state.repository.visibility); + setText(root, 'repo-branch', `BRANCH / ${state.repository.branch}`); + setText(root, 'build-status', state.repository.build); + setText(root, 'test-status', state.repository.tests); + setText(root, 'deploy-status', state.repository.deployment); + setText(root, 'world-clock', formatTime(state.tick)); + setText(root, 'player-location', state.view === 'district' ? `INSIDE ${selectedDistrict(state).path}` : 'SPAWN PLAZA'); + setText(root, 'feedback-line', state.feedback); + const playerLevel = Math.min(99, 12 + Math.floor(state.tick / 3)); + setText(root, 'player-level', `LEVEL ${playerLevel} DEV`); + const xp = Math.min(100, 34 + state.tick * 2); + const xpBar = byId(root, 'xp-fill'); + if (xpBar) xpBar.style.width = `${xp}%`; + setText(root, 'xp-label', `${xp} / 100 XP`); +} + +function renderFiles(state) { + const district = selectedDistrict(state); + return `
    DIRECTORY${escapeHtml(district.path)}${escapeHtml(district.stat)}
    ${district.files.map((file) => ``).join('')}
    `; +} + +function renderCommits(state) { + const commits = state.repository.commits || []; + return `
    HISTORY ROADRECENT COMMITS${commits.length} simulated events
    ${commits.map((commit) => ``).join('')}
    `; +} + +function renderIssues(state) { + const issues = state.repository.issues || []; + return `
    QUEST BOARDOPEN ISSUESQuests waiting in the city
    ${issues.map((issue) => ``).join('')}
    `; +} + +function renderPullRequests(state) { + const prs = state.repository.pullRequests || []; + return `
    MERGE GATEPULL REQUESTSDoors into the next branch
    ${prs.map((pr) => ``).join('')}
    `; +} + +function renderBuild(state) { + return `
    AUTOMATION CENTERBUILD & TESTAll values are simulated browser-local signals
    BUILD #812${escapeHtml(state.repository.build)}
    INSPECTORS${escapeHtml(state.repository.tests)}
    PAGES RUNWAY${escapeHtml(state.repository.deployment)}
    SOURCECOMPILETESTDEPLOY
    `; +} + +function renderRecord(state) { + const record = state.selectedRecord; + if (!record) return renderBuild(state); + if (record.type === 'commit') { + const item = (state.repository.commits || []).find((candidate) => candidate.id === record.id); + return `
    COMMIT EVENT${escapeHtml(item?.hash || record.id)}Historical event made physical
    ${escapeHtml(item?.message || 'Commit selected')}

    This simulated commit is a construction event. The repository builder routes its changed files into the city.

    author · ${escapeHtml(item?.author || 'builder')}
    `; + } + if (record.type === 'issue') { + const item = (state.repository.issues || []).find((candidate) => candidate.id === record.id); + return `
    ISSUE QUEST${escapeHtml(item?.title || record.id)}${escapeHtml(item?.status || 'OPEN')}
    Quest objective

    ${escapeHtml(item?.detail || 'Inspect the open issue in the quest district.')}

    marker active · browser-local simulation
    `; + } + const item = (state.repository.pullRequests || []).find((candidate) => candidate.id === record.id); + return `
    PULL REQUEST DOOR${escapeHtml(item?.title || record.id)}${escapeHtml(item?.status || 'REVIEW')}
    Merge gate

    ${escapeHtml(item?.detail || 'The gate is waiting for review.')}

    checks passing · browser-local simulation
    `; +} + +function renderInspector(root, state, nearby) { + const panel = byId(root, 'inspector-content'); + if (!panel) return; + const district = selectedDistrict(state); + const file = selectedFile(state); + const collection = state.selectedCollection; + let content = ''; + if (state.activePanel === 'file' || (state.activePanel === 'collection' && collection === 'files')) { + content = `
    ${escapeHtml(file.kind)} OBJECT

    ${escapeHtml(file.name)}

    ${escapeHtml(file.detail)}

    ${escapeHtml(file.code)}
    `; + } else if (state.activePanel === 'record') { + content = renderRecord(state); + } else if (state.activePanel === 'collection') { + content = collection === 'commits' ? renderCommits(state) : collection === 'issues' ? renderIssues(state) : collection === 'prs' ? renderPullRequests(state) : renderBuild(state); + } else if (state.activePanel === 'inspector' || state.activePanel === 'world') { + const canEnter = nearby?.district?.id === district.id && nearby.inRange; + content = `
    ${state.view === 'district' ? 'CURRENT DISTRICT' : 'NEAREST DISTRICT'}

    ${escapeHtml(district.title)}

    ${escapeHtml(district.summary)}

    ${escapeHtml(district.path)}${escapeHtml(district.stat)}${Math.round((state.construction[district.id] || 0) * 100)}% BUILT
    ${district.files.slice(0, 3).map((item) => ``).join('')}
    `; + } else { + content = `
    ${escapeHtml(district.path)}

    Repository objects

    Select a building, tool, or activity to inspect the physical software around you.

    `; + } + panel.innerHTML = content; + const nearbyPrompt = byId(root, 'nearby-prompt'); + if (nearbyPrompt) { + nearbyPrompt.innerHTML = nearby?.district && nearby.inRange ? `NEARBY${escapeHtml(nearby.district.name)}PRESS E / ENTER TO ENTER` : `WALKABLE CITY${escapeHtml(state.player.facing.toUpperCase())}WASD · ARROWS TO NAVIGATE`; + nearbyPrompt.classList.toggle('is-ready', Boolean(nearby?.inRange)); + } +} + +function renderTerminal(root, state) { + const overlay = byId(root, 'terminal-phone'); + if (!overlay) return; + overlay.classList.toggle('is-open', state.terminal.open); + overlay.setAttribute('aria-hidden', String(!state.terminal.open)); + const output = byId(root, 'terminal-output'); + if (output) output.innerHTML = state.terminal.lines.map((line) => `
    ${escapeHtml(line)}
    `).join(''); + const input = byId(root, 'terminal-input'); + if (input && input.value !== state.terminal.command) input.value = ''; +} + +function renderSearch(root) { + const search = byId(root, 'repo-search'); + if (!search) return; + const query = search.value.trim().toLowerCase(); + root.querySelectorAll('.world-building').forEach((building) => { + building.classList.toggle('is-search-hidden', Boolean(query) && !building.textContent.toLowerCase().includes(query)); + }); +} + +export function renderHud(root, state, nearby) { + renderAgents(root, state); + renderEvents(root, state); + renderToolbelt(root, state); + renderStatus(root, state); + renderInspector(root, state, nearby); + renderTerminal(root, state); + renderSearch(root); + const drawer = byId(root, 'inspector-drawer'); + if (drawer) drawer.classList.toggle('is-open', state.activePanel !== 'world' && state.activePanel !== 'terminal'); + const shell = byId(root, 'game-shell'); + if (shell) shell.dataset.view = state.view; + setText(root, 'selected-tool-label', (TOOLBELT.find((tool) => tool.id === state.activeTool) || TOOLBELT[0]).name); +} + +export function bindUi(root, dispatch) { + root.addEventListener('click', (event) => { + const target = event.target.closest('[data-action], [data-district-id]'); + if (!target) return; + const action = target.dataset.action || (target.dataset.districtId ? 'select-district' : ''); + if (action === 'select-district') dispatch({ type: EVENT_TYPES.SELECT_DISTRICT, districtId: target.dataset.districtId }); + if (action === 'open-inspector') dispatch({ type: EVENT_TYPES.OPEN_INSPECTOR }); + if (action === 'enter-district') dispatch({ type: EVENT_TYPES.ENTER_DISTRICT, districtId: target.dataset.districtId }); + if (action === 'close-inspector') dispatch({ type: EVENT_TYPES.CLOSE_INSPECTOR }); + if (action === 'select-file') dispatch({ type: EVENT_TYPES.SELECT_FILE, fileId: target.dataset.fileId }); + if (action === 'select-collection') dispatch({ type: EVENT_TYPES.SELECT_COLLECTION, collection: target.dataset.collection }); + if (action === 'commit-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'commit', recordId: target.dataset.commitId }); + if (action === 'issue-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'issue', recordId: target.dataset.issueId }); + if (action === 'pr-detail') dispatch({ type: EVENT_TYPES.SELECT_RECORD, recordType: 'pr', recordId: target.dataset.prId }); + if (action === 'move') dispatch({ type: EVENT_TYPES.PLAYER_MOVE, dx: target.dataset.dx, dy: target.dataset.dy }); + if (action === 'tool') { + const tool = TOOLBELT.find((item) => item.id === target.dataset.toolId); + dispatch({ type: EVENT_TYPES.USE_TOOL, toolId: target.dataset.toolId, message: tool?.verb ? `${tool.name}: ${tool.verb}.` : undefined }); + } + if (action === 'open-terminal') dispatch({ type: EVENT_TYPES.OPEN_TERMINAL }); + if (action === 'close-terminal') dispatch({ type: EVENT_TYPES.CLOSE_TERMINAL }); + if (action === 'command') dispatch({ type: EVENT_TYPES.RUN_COMMAND, command: target.dataset.command, lines: commandTranscript(target.dataset.command, window.gitWorldState || { selectedFileId: 'README.md' }) }); + }); + + root.addEventListener('submit', (event) => { + if (!event.target.matches('#terminal-form')) return; + event.preventDefault(); + const input = byId(root, 'terminal-input'); + const command = input?.value.trim() || ''; + if (command) dispatch({ type: EVENT_TYPES.RUN_COMMAND, command, lines: commandTranscript(command, window.gitWorldState || { selectedFileId: 'README.md' }) }); + }); + + const search = byId(root, 'repo-search'); + if (search) search.addEventListener('input', () => renderSearch(root)); +} diff --git a/src/world-architect.js b/src/world-architect.js new file mode 100644 index 0000000000000000000000000000000000000000..f7b03b2938be412d195cab79d2108fe56467b307 --- /dev/null +++ b/src/world-architect.js @@ -0,0 +1,451 @@ +import { + clamp, + clone, + EVENT_TYPES, + makeEvent +} from './shared.js'; + +export const WALKABLE_BOUNDS = Object.freeze({ minX: 70, maxX: 1530, minY: 90, maxY: 850 }); + +export const DISTRICTS = Object.freeze([ + { + id: 'src', + name: 'SRC DISTRICT', + path: '/src', + title: 'Central Kernel Tower', + glyph: '⌘', + accent: '#62f2d2', + x: 565, + y: 230, + width: 255, + height: 220, + summary: 'The living source tree: modules, kernel code, and the repository entry point.', + stat: '42 files · 213 commits', + files: [ + { id: 'main-rs', name: 'main.rs', kind: 'Rust', size: '8.4 KB', detail: 'Repository bootstrap and agent scheduler.', code: 'fn main() {\n let world = GitWorld::spawn();\n world.walk();\n}' }, + { id: 'kernel-cu', name: 'kernel.cu', kind: 'CUDA', size: '24.1 KB', detail: 'The compute floor is under construction.', code: '__global__ void kernel_step(Agent agent) {\n agent.advance();\n}' }, + { id: 'lib-rs', name: 'lib.rs', kind: 'Rust', size: '12.7 KB', detail: 'Shared repository primitives and event contracts.', code: 'pub mod events;\npub mod districts;\npub mod builders;' }, + { id: 'mod-rs', name: 'mod.rs', kind: 'Rust', size: '2.1 KB', detail: 'District module registry.', code: 'pub const DISTRICTS: &[&str] = &[\n "src", "tests", "docs"\n];' } + ] + }, + { + id: 'tests', + name: 'TEST DISTRICT', + path: '/tests', + title: 'Test Factory', + glyph: '✓', + accent: '#91e85e', + x: 230, + y: 370, + width: 235, + height: 185, + summary: 'Inspection robots verify each build before the city opens another street.', + stat: '142 passing · 0 failing', + files: [ + { id: 'world-test', name: 'world.test.js', kind: 'Test', size: '9.7 KB', detail: 'Deterministic movement and event checks.', code: 'assert.equal(world.tick, 12);\nassert.equal(events.length, 4);' }, + { id: 'contracts-test', name: 'contracts.test.js', kind: 'Test', size: '6.2 KB', detail: 'Shared UI/world event contract checks.', code: 'assert.ok(EVENT_TYPES.PLAYER_MOVE);\nassert.ok(EVENT_TYPES.BUILD_COMPLETED);' }, + { id: 'fixtures', name: 'fixtures/', kind: 'Directory', size: '18 items', detail: 'Small simulated repository fixtures.', code: 'fixture("pax-coder");' } + ] + }, + { + id: 'docs', + name: 'DOCS LIBRARY', + path: '/docs', + title: 'Documentation Library', + glyph: '▤', + accent: '#ffcf68', + x: 1085, + y: 245, + width: 240, + height: 205, + summary: 'A warm archive of architecture decisions, guides, and the city map legend.', + stat: '18 guides · 96% coverage', + files: [ + { id: 'architecture', name: 'architecture.md', kind: 'Markdown', size: '14.2 KB', detail: 'The frontend event-driven architecture.', code: '# GitWorld architecture\nInput -> simulation -> state -> HUD' }, + { id: 'contributing', name: 'contributing.md', kind: 'Markdown', size: '4.8 KB', detail: 'How builders add a district.', code: '1. Choose a street\n2. Add a contract\n3. Run the inspectors' }, + { id: 'readme', name: 'README.md', kind: 'Markdown', size: '8.3 KB', detail: 'The repository welcome sign.', code: '# pax-coder\nA simulated repository world.' } + ] + }, + { + id: 'issues', + name: 'ISSUE QUESTS', + path: '/issues', + title: 'Issue Quest Board', + glyph: '!', + accent: '#ff9f58', + x: 865, + y: 470, + width: 215, + height: 160, + summary: 'Open issues become quests with markers, owners, and a clear next action.', + stat: '3 open quests · 1 in progress', + files: [ + { id: 'issue-201', name: '#201 bug in proof', kind: 'Issue', size: 'open', detail: 'Reproduce the proof boundary on the kernel floor.', code: 'status: open\nowner: proof-agent' }, + { id: 'issue-202', name: '#202 add test', kind: 'Issue', size: 'open', detail: 'Add a regression test for agent movement.', code: 'status: open\nowner: test-agent' }, + { id: 'issue-203', name: '#203 docs update', kind: 'Issue', size: 'queued', detail: 'Document the deploy launch sequence.', code: 'status: queued\nowner: doc-agent' } + ] + }, + { + id: 'prs', + name: 'PR GATE', + path: '/pulls', + title: 'Pull Request Gate', + glyph: '↔', + accent: '#d37cff', + x: 600, + y: 475, + width: 210, + height: 180, + summary: 'A glowing merge door: review the change before opening the next branch road.', + stat: '2 open · review required', + files: [ + { id: 'pr-88', name: '#88 merge kernel fix', kind: 'Pull request', size: 'review', detail: 'Two approvals still needed before merge.', code: 'checks: passing\nreviews: 1 / 3' }, + { id: 'pr-91', name: '#91 docs refresh', kind: 'Pull request', size: 'ready', detail: 'Ready to merge after the documentation quest.', code: 'checks: passing\nreviews: 3 / 3' } + ] + }, + { + id: 'actions', + name: 'ACTIONS FACTORY', + path: '/.github', + title: 'Automation Center', + glyph: '⚙', + accent: '#66a9ff', + x: 1250, + y: 485, + width: 235, + height: 195, + summary: 'CI factories turn commits into tested artifacts and light the deploy runway.', + stat: 'BUILD #812 · passing', + files: [ + { id: 'ci-yml', name: 'ci.yml', kind: 'Actions', size: '5.2 KB', detail: 'Build, test, and artifact inspection pipeline.', code: 'jobs:\n build:\n test:\n deploy:' }, + { id: 'pages-yml', name: 'pages.yml', kind: 'Actions', size: '2.4 KB', detail: 'Static Pages deployment path.', code: 'deploy-pages: enabled' }, + { id: 'release', name: 'release.yml', kind: 'Actions', size: '3.0 KB', detail: 'Packages the next city snapshot.', code: 'tag: v0.8.2' } + ] + } +]); + +const INITIAL_AGENTS = [ + { id: 'world-architect', name: 'WORLD ARCHITECT', role: 'World geometry', task: 'Building /src district', status: 'BUILDING', progress: 78, color: '#62f2d2' }, + { id: 'repository-builder', name: 'REPOSITORY BUILDER', role: 'Repository mapping', task: 'Mapping 42 files', status: 'MAPPING', progress: 64, color: '#ffcf68' }, + { id: 'reality-integrator', name: 'REALITY/UI INTEGRATOR', role: 'Interaction layer', task: 'Connecting terminal', status: 'WIRING', progress: 52, color: '#d37cff' } +]; + +const INITIAL_EVENTS = [ + { id: 'seed-1', type: EVENT_TYPES.COMMIT, tick: 0, at: '00:00:00', detail: { message: '8f3a9c1 Fix kernel bounds', districtId: 'src' } }, + { id: 'seed-2', type: EVENT_TYPES.AGENT_PROGRESS, tick: 0, at: '00:00:00', detail: { message: 'WORLD ARCHITECT placed a new tower window.', agentId: 'world-architect' } }, + { id: 'seed-3', type: EVENT_TYPES.TEST_COMPLETED, tick: 0, at: '00:00:00', detail: { message: '142 inspectors passed the street check.', districtId: 'tests' } } +]; + +const SIMULATION_SEQUENCE = [ + { type: EVENT_TYPES.AGENT_PROGRESS, detail: { agentId: 'world-architect', message: 'WORLD ARCHITECT raised the /src tower scaffold.' } }, + { type: EVENT_TYPES.AGENT_PROGRESS, detail: { agentId: 'repository-builder', message: 'REPOSITORY BUILDER mapped kernel.cu into the compute floor.' } }, + { type: EVENT_TYPES.CONSTRUCTION, detail: { districtId: 'src', amount: 0.03, message: 'A new source window flickers online.' } }, + { type: EVENT_TYPES.TEST_STARTED, detail: { districtId: 'tests', message: 'Inspection robots started a deterministic street sweep.' } }, + { type: EVENT_TYPES.TEST_COMPLETED, detail: { districtId: 'tests', message: '142 tests passed. The test district lights turn green.' } }, + { type: EVENT_TYPES.BUILD_STARTED, detail: { districtId: 'actions', message: 'BUILD #813 started in the automation factory.' } }, + { type: EVENT_TYPES.AGENT_PROGRESS, detail: { agentId: 'reality-integrator', message: 'REALITY/UI INTEGRATOR wired a contextual inspect panel.' } }, + { type: EVENT_TYPES.BUILD_COMPLETED, detail: { districtId: 'actions', message: 'BUILD #813 completed and artifacts are staged.' } }, + { type: EVENT_TYPES.COMMIT, detail: { districtId: 'src', message: '2h4d7e2 Add proof sketch' } }, + { type: EVENT_TYPES.ISSUE, detail: { districtId: 'issues', message: '#202 quest marker activated: add test' } }, + { type: EVENT_TYPES.PULL_REQUEST, detail: { districtId: 'prs', message: '#91 merge gate is ready for review.' } }, + { type: EVENT_TYPES.DEPLOYMENT_STARTED, detail: { districtId: 'actions', message: 'Deploy rocket is fueling on the Pages runway.' } }, + { type: EVENT_TYPES.DEPLOYMENT_COMPLETED, detail: { districtId: 'actions', message: 'Static city snapshot deployed to the local launch pad.' } } +]; + +export function createWorldState(seed = 20260828) { + return { + seed, + tick: 0, + repository: { + owner: 'SNAPKITTYWEST', + name: 'pax-coder', + branch: 'main', + visibility: 'Public', + simulated: true, + stars: 213, + forks: 45, + languages: ['Rust', 'CUDA', 'Markdown'], + build: 'PASSING', + tests: '142 PASS', + deployment: 'STAGED', + commits: [ + { id: '8f3a9c1', hash: '8f3a9c1', message: 'Fix kernel bounds', author: 'AhmadParr' }, + { id: '2h4d7e2', hash: '2h4d7e2', message: 'Add proof sketch', author: 'proof-agent' }, + { id: '9ac1bd9', hash: '9ac1bd9', message: 'Update docs', author: 'doc-agent' }, + { id: '7d0c812', hash: '7d0c812', message: 'Wire agent event bus', author: 'world-architect' } + ], + issues: [ + { id: '201', title: '#201 Bug in proof', detail: 'Reproduce the proof boundary on the kernel floor.', status: 'OPEN' }, + { id: '202', title: '#202 Add test', detail: 'Add a regression test for agent movement.', status: 'OPEN' }, + { id: '203', title: '#203 Docs update', detail: 'Document the deploy launch sequence.', status: 'QUEUED' } + ], + pullRequests: [ + { id: '88', title: '#88 Merge kernel fix', detail: 'Two approvals still needed before merge.', status: 'REVIEW' }, + { id: '91', title: '#91 Docs refresh', detail: 'Ready to merge after the documentation quest.', status: 'READY' } + ] + }, + districts: clone(DISTRICTS), + agents: clone(INITIAL_AGENTS), + events: clone(INITIAL_EVENTS), + construction: { src: 0.78, tests: 1, docs: 0.96, issues: 0.7, prs: 0.82, actions: 0.88 }, + player: { x: 800, y: 735, speed: 18, facing: 'north', step: 0 }, + selectedDistrictId: 'src', + selectedFileId: 'main-rs', + selectedCollection: 'files', + selectedRecord: null, + activeTool: 'terminal', + activePanel: 'world', + terminal: { open: false, lines: ['$ local shell connected', '$ type `help` for commands'], command: '' }, + feedback: 'Walk toward a district or click a glowing building.', + unlocked: false, + view: 'city' + }; +} + +function pushEvent(state, event) { + state.events = [event, ...state.events].slice(0, 14); +} + +function findDistrict(state, districtId) { + return state.districts.find((district) => district.id === districtId) || state.districts[0]; +} + +function selectedFile(state) { + const district = findDistrict(state, state.selectedDistrictId); + return district.files.find((file) => file.id === state.selectedFileId) || district.files[0]; +} + +function circlesOverlap(x, y, district, margin = 28) { + const left = district.x - margin; + const right = district.x + district.width + margin; + const top = district.y - margin; + const bottom = district.y + district.height + margin; + return x >= left && x <= right && y >= top && y <= bottom; +} + +export function getNearbyDistrict(state, range = 185) { + let nearest = null; + let distance = Number.POSITIVE_INFINITY; + for (const district of state.districts) { + const centerX = district.x + district.width / 2; + const centerY = district.y + district.height / 2; + const nextDistance = Math.hypot(state.player.x - centerX, state.player.y - centerY); + if (nextDistance < distance) { + nearest = district; + distance = nextDistance; + } + } + return { district: nearest, distance, inRange: distance <= range }; +} + +export function worldReducer(previous, event) { + const state = clone(previous); + switch (event.type) { + case EVENT_TYPES.PLAYER_MOVE: { + const dx = Number(event.dx) || 0; + const dy = Number(event.dy) || 0; + const magnitude = Math.hypot(dx, dy) || 1; + const nextX = clamp(state.player.x + (dx / magnitude) * state.player.speed, WALKABLE_BOUNDS.minX, WALKABLE_BOUNDS.maxX); + const nextY = clamp(state.player.y + (dy / magnitude) * state.player.speed, WALKABLE_BOUNDS.minY, WALKABLE_BOUNDS.maxY); + if (!state.districts.some((district) => circlesOverlap(nextX, nextY, district))) { + state.player.x = nextX; + state.player.y = nextY; + state.player.step = (state.player.step + 1) % 4; + } + state.player.facing = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'east' : 'west') : (dy > 0 ? 'south' : 'north'); + return state; + } + case EVENT_TYPES.SELECT_DISTRICT: { + const district = findDistrict(state, event.districtId); + state.selectedDistrictId = district.id; + state.selectedFileId = district.files[0]?.id || null; + state.feedback = `${district.name} selected. Press Enter or E to inspect.`; + return state; + } + case EVENT_TYPES.SELECT_FILE: + state.selectedFileId = event.fileId; + state.activePanel = 'file'; + state.feedback = `Inspecting ${selectedFile(state).name}`; + return state; + case EVENT_TYPES.SELECT_COLLECTION: + state.selectedCollection = event.collection; + state.activePanel = 'collection'; + return state; + case EVENT_TYPES.SELECT_RECORD: + state.selectedRecord = { type: event.recordType, id: event.recordId }; + state.activePanel = 'record'; + return state; + case EVENT_TYPES.OPEN_INSPECTOR: + state.activePanel = 'inspector'; + state.feedback = `Inspecting ${findDistrict(state, state.selectedDistrictId).path}`; + return state; + case EVENT_TYPES.CLOSE_INSPECTOR: + state.activePanel = 'world'; + return state; + case EVENT_TYPES.ENTER_DISTRICT: { + const district = findDistrict(state, event.districtId || state.selectedDistrictId); + state.selectedDistrictId = district.id; + state.selectedFileId = district.files[0]?.id || null; + state.activePanel = 'inspector'; + state.view = 'district'; + state.feedback = `Entered ${district.path}. Code is physical here.`; + pushEvent(state, makeEvent(EVENT_TYPES.ENTER_DISTRICT, state.tick, { message: `Player entered ${district.path}.`, districtId: district.id })); + return state; + } + case EVENT_TYPES.OPEN_TERMINAL: + state.terminal.open = true; + state.activeTool = 'terminal'; + state.activePanel = 'terminal'; + state.feedback = 'Terminal phone online. Commands are simulated locally.'; + return state; + case EVENT_TYPES.CLOSE_TERMINAL: + state.terminal.open = false; + if (state.activePanel === 'terminal') state.activePanel = 'world'; + return state; + case EVENT_TYPES.RUN_COMMAND: { + const command = String(event.command || '').trim().toLowerCase(); + if (!command) return state; + state.terminal.command = command; + state.terminal.lines = [...state.terminal.lines, ...event.lines].slice(-28); + state.feedback = 'Ran `' + command + '` in the simulated terminal.'; + pushEvent(state, makeEvent(EVENT_TYPES.USE_TOOL, state.tick, { message: `Terminal ran ${command}.`, toolId: 'terminal' })); + return state; + } + case EVENT_TYPES.USE_TOOL: { + state.activeTool = event.toolId; + if (event.toolId === 'terminal') state.terminal.open = true; + if (event.toolId === 'git') { state.selectedCollection = 'commits'; state.activePanel = 'collection'; } + if (event.toolId === 'docker') { state.selectedCollection = 'build'; state.activePanel = 'collection'; } + if (event.toolId === 'code') state.activePanel = 'file'; + if (event.toolId === 'ssh') { state.unlocked = true; state.activePanel = 'inspector'; } + if (event.toolId === 'database') { state.selectedCollection = 'files'; state.activePanel = 'collection'; } + if (event.toolId === 'deploy') { state.repository.deployment = 'LAUNCHING'; state.selectedCollection = 'build'; state.activePanel = 'collection'; } + state.feedback = event.message || `Equipped ${event.toolId}.`; + pushEvent(state, makeEvent(EVENT_TYPES.USE_TOOL, state.tick, { message: state.feedback, toolId: event.toolId })); + return state; + } + case EVENT_TYPES.TICK: + state.tick += 1; + return state; + case EVENT_TYPES.AGENT_PROGRESS: { + const agent = state.agents.find((candidate) => candidate.id === event.detail.agentId); + if (agent) agent.progress = Math.min(100, agent.progress + 2); + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + } + case EVENT_TYPES.CONSTRUCTION: { + const districtId = event.detail.districtId; + state.construction[districtId] = clamp((state.construction[districtId] || 0) + (event.detail.amount || 0), 0, 1); + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + } + case EVENT_TYPES.BUILD_STARTED: + state.repository.build = 'RUNNING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.BUILD_COMPLETED: + state.repository.build = 'PASSING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.TEST_STARTED: + state.repository.tests = 'RUNNING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.TEST_COMPLETED: + state.repository.tests = '142 PASS'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.DEPLOYMENT_STARTED: + state.repository.deployment = 'LAUNCHING'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.DEPLOYMENT_COMPLETED: + state.repository.deployment = 'STAGED'; + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + case EVENT_TYPES.COMMIT: + case EVENT_TYPES.ISSUE: + case EVENT_TYPES.PULL_REQUEST: + pushEvent(state, makeEvent(event.type, state.tick, event.detail)); + state.feedback = event.detail.message; + return state; + default: + return state; + } +} + +export function advanceWorldState(state) { + const ticked = worldReducer(state, { type: EVENT_TYPES.TICK }); + const plan = SIMULATION_SEQUENCE[(ticked.tick - 1) % SIMULATION_SEQUENCE.length]; + const event = makeEvent(plan.type, ticked.tick, plan.detail); + return { state: worldReducer(ticked, event), event }; +} + +function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, (character) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[character])); +} + +function windowBlocks(count, color) { + return Array.from({ length: count }, (_, index) => ``).join(''); +} + +export function renderWorld(viewport, state, onBuildingSelect) { + const viewportWidth = viewport.clientWidth || 960; + const viewportHeight = viewport.clientHeight || 680; + const centerX = viewportWidth / 2; + const centerY = viewportHeight * 0.56; + const translateX = centerX - state.player.x * 0.88; + const translateY = centerY - state.player.y * 0.88; + const buildingMarkup = state.districts.map((district) => { + const selected = district.id === state.selectedDistrictId ? ' is-selected' : ''; + const progress = Math.round((state.construction[district.id] || 0) * 100); + return ``; + }).join(''); + + viewport.innerHTML = `
    + + +
    +
    +
    MAIN
    +
    HISTORY
    +
    BRANCH / ALT
    +
    SPAWN PLAZA
    + ${buildingMarkup} + + +
    +
    +
    !
    +
    PULL REQUESTOPEN GATE
    +
    + +
    +
    SIMULATED REPOSITORYSNAPKITTYWEST/ pax-coder★ 213 · ⑂ 45 · ◉ PUBLIC
    +
    BUILD AGENTS3 ACTIVEWATCH THE CREW WORK
    +
    +
    `; + + viewport.querySelectorAll('[data-district-id]').forEach((building) => { + building.addEventListener('click', () => onBuildingSelect(building.dataset.districtId)); + }); +} + +export function renderMinimap(root, state) { + const districts = state.districts.map((district) => ``).join(''); + root.innerHTML = `
    ${districts}
    LIVE MAP${state.selectedDistrictId.toUpperCase()}
    `; +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..fc6e263b46b9a981781ffc229642663d9bf95cd4 --- /dev/null +++ b/styles.css @@ -0,0 +1,108 @@ +:root { + color-scheme: dark; + --night: #040b14; + --sky: #071829; + --panel: rgba(5, 16, 29, .92); + --panel-solid: #091827; + --line: #164666; + --line-hot: #2a7195; + --white: #e7f5ff; + --muted: #8daabe; + --cyan: #64f4e1; + --blue: #68acff; + --green: #9ff254; + --gold: #ffc95b; + --orange: #ff9d54; + --pink: #e887ff; + --red: #ff6877; + --mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + --sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { box-sizing: border-box; } +html { min-width: 320px; background: var(--night); } +body { margin: 0; min-width: 320px; overflow-x: hidden; color: var(--white); font-family: var(--sans); background: var(--night); } +button, input { font: inherit; } +button { color: inherit; cursor: pointer; } +button:focus-visible, input:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; } +.game-shell { position: relative; min-height: 100vh; isolation: isolate; padding: 14px 15px 10px; background: #050e1b; overflow: hidden; } +.game-shell::before { content: ""; position: fixed; z-index: -4; inset: 0; pointer-events: none; background-image: radial-gradient(circle at 50% 15%, rgba(33, 91, 129, .2), transparent 38%), linear-gradient(180deg, #07182a 0, #050d18 58%, #071322 100%); } +.game-shell::after { content: ""; position: fixed; z-index: -3; inset: 0; pointer-events: none; opacity: .34; background-image: radial-gradient(circle, rgba(102, 198, 255, .8) 0 1px, transparent 1.5px); background-size: 113px 137px; background-position: 25px 18px; } +.ambient { position: fixed; z-index: -2; width: 390px; height: 390px; border-radius: 50%; pointer-events: none; filter: blur(46px); opacity: .16; }.ambient-one { top: 6%; left: 31%; background: #216cbb; }.ambient-two { right: -90px; bottom: 8%; background: #8e2aa8; } +.panel-glass { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 16px 36px rgba(0, 0, 0, .28), inset 0 1px 0 rgba(118, 215, 255, .08); backdrop-filter: blur(10px); } +.topbar { position: relative; z-index: 5; display: grid; grid-template-columns: 230px minmax(260px, 1fr) 260px; align-items: center; gap: 30px; width: min(1500px, 100%); margin: 0 auto; padding: 0 2px 13px; } +.brand-lockup { display: flex; align-items: center; gap: 11px; text-transform: uppercase; }.brand-lockup > span:last-child { display: grid; gap: 2px; }.brand-lockup strong { color: var(--cyan); font: 700 19px var(--mono); letter-spacing: -.04em; text-shadow: 0 0 16px rgba(100, 244, 225, .45); }.brand-lockup small { color: var(--muted); font: 8px var(--mono); letter-spacing: .18em; }.brand-mark { width: 32px; height: 32px; display: grid; place-items: center; color: var(--cyan); border: 1px solid var(--cyan); border-radius: 6px; font-size: 20px; box-shadow: 0 0 16px rgba(100, 244, 225, .22); } +.repo-search-wrap { position: relative; display: flex; align-items: center; }.repo-search-wrap label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); }.repo-search-wrap input { width: 100%; height: 34px; padding: 0 44px 0 16px; color: var(--white); border: 1px solid #355675; border-radius: 8px; background: rgba(6, 14, 25, .72); box-shadow: inset 0 0 12px rgba(31, 120, 191, .1); }.repo-search-wrap input::placeholder { color: #7c9ab5; }.repo-search-wrap kbd { position: absolute; right: 7px; color: var(--cyan); font: 14px var(--mono); pointer-events: none; }.topbar-actions { display: flex; align-items: center; justify-content: flex-end; gap: 11px; }.simulated-pill { display: flex; align-items: center; gap: 7px; color: var(--green); font: 8px var(--mono); letter-spacing: .06em; }.simulated-pill i, .live-dot { width: 7px; height: 7px; display: inline-block; border-radius: 50%; background: var(--green); box-shadow: 0 0 10px var(--green); }.icon-button { width: 29px; height: 29px; padding: 0; border: 1px solid var(--line-hot); border-radius: 6px; background: rgba(9, 27, 45, .8); color: var(--cyan); font-size: 17px; line-height: 1; }.icon-button:hover { border-color: var(--cyan); background: rgba(27, 79, 102, .7); } + +.left-rail { position: absolute; z-index: 4; top: 72px; left: 15px; width: 254px; padding: 17px 18px; }.panel-title { display: grid; gap: 1px; padding-bottom: 12px; border-bottom: 1px solid rgba(61, 141, 177, .35); }.section-kicker { color: var(--cyan); font: 10px var(--mono); letter-spacing: .14em; }.panel-title h1 { margin: 2px 0 0; color: var(--white); font: 700 19px var(--mono); letter-spacing: .02em; }.panel-title i { width: 56px; height: 2px; margin-top: 9px; background: var(--cyan); box-shadow: 0 0 8px var(--cyan); }.welcome-copy { margin: 14px 0 0; color: #d2e3ed; font-size: 14px; line-height: 1.45; }.welcome-copy.muted { margin-top: 6px; color: var(--muted); font-size: 12px; }.welcome-copy strong { color: var(--cyan); }.rail-divider { height: 1px; margin: 16px 0 13px; background: var(--line); }.component-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }.component-heading > span { color: var(--cyan); font: 12px var(--mono); letter-spacing: .05em; }.component-heading small { color: #6d8da7; font: 8px var(--mono); }.component-list { display: grid; gap: 9px; margin: 13px 0 0; padding: 0; list-style: none; }.component-list li { display: flex; align-items: center; gap: 9px; color: #bfd3df; font-size: 12px; }.component-list li b { width: 20px; height: 20px; display: grid; place-items: center; color: var(--cyan); border: 1px solid #2e7798; border-radius: 5px; font-size: 13px; }.component-list strong { color: var(--white); }.how-it-works { margin-top: 18px; padding-top: 13px; border-top: 1px solid var(--line); }.how-it-works ol { display: grid; gap: 5px; margin: 10px 0 0; padding-left: 21px; color: var(--muted); font: 10px/1.4 var(--mono); } + +.world-stage { position: relative; min-height: 720px; width: min(1500px, 100%); margin: 0 auto; padding: 9px 287px 0 274px; }.stage-heading { position: relative; z-index: 3; display: flex; justify-content: space-between; align-items: flex-end; gap: 14px; padding: 5px 18px 10px; }.stage-heading h2 { margin: 4px 0 0; color: var(--white); font: 700 21px var(--mono); letter-spacing: -.03em; text-shadow: 0 0 19px rgba(148, 218, 255, .28); }.stage-heading h2 b { color: var(--cyan); font-weight: 500; }.stage-state { display: flex; gap: 10px; color: var(--green); font: 9px var(--mono); }.stage-state span + span { padding-left: 10px; border-left: 1px solid var(--line); color: var(--muted); }.world-viewport { position: relative; height: 690px; overflow: hidden; border: 1px solid #195174; border-radius: 13px; background: #071725; box-shadow: inset 0 0 50px rgba(5, 43, 69, .55), 0 20px 50px rgba(0, 0, 0, .34); }.world-viewport::after { content: ""; position: absolute; z-index: 3; inset: 0; pointer-events: none; border: 1px solid rgba(117, 212, 255, .08); border-radius: 12px; box-shadow: inset 0 0 35px rgba(0, 0, 0, .62); } +.world-canvas { position: absolute; width: 1600px; height: 960px; transform-origin: 0 0; transition: transform .25s ease; }.world-ground { position: absolute; inset: 0; overflow: hidden; background: linear-gradient(180deg, #0b2435 0 40%, #102f3b 40% 46%, #132c31 46% 100%); }.world-ground::before { content: ""; position: absolute; left: -8%; right: -8%; bottom: -25%; height: 64%; transform: perspective(540px) rotateX(57deg); transform-origin: bottom; background-image: linear-gradient(rgba(49, 129, 134, .25) 1px, transparent 1px), linear-gradient(90deg, rgba(49, 129, 134, .22) 1px, transparent 1px); background-size: 42px 42px; opacity: .58; }.world-ground::after { content: ""; position: absolute; inset: 0; pointer-events: none; background-image: radial-gradient(circle at 15% 22%, rgba(155, 235, 255, .8) 0 1px, transparent 1.5px); background-size: 87px 73px; opacity: .55; }.ground-grid { position: absolute; inset: 0; background: linear-gradient(105deg, transparent 49.7%, rgba(36, 117, 126, .26) 50%, transparent 50.3%); opacity: .28; }.distant-skyline { position: absolute; z-index: 0; top: 74px; left: 0; right: 0; height: 220px; display: flex; align-items: flex-end; justify-content: space-around; padding: 0 65px; opacity: .8; }.distant-skyline i { display: block; width: 43px; height: 120px; background: #0d2d42; border: 1px solid #1d6383; box-shadow: 9px -24px 0 -8px #0d2c42, inset 9px 16px 0 -8px rgba(114, 218, 255, .28); }.distant-skyline i:nth-child(2n) { height: 172px; width: 66px; }.distant-skyline i:nth-child(3n) { height: 92px; background: #112949; }.moon-orb { position: absolute; z-index: 0; top: 36px; right: 210px; width: 56px; height: 56px; border-radius: 50%; background: #98cbdf; box-shadow: 0 0 36px rgba(100, 216, 255, .45); opacity: .8; }.moon-orb::after { content: ""; position: absolute; inset: 9px 13px 13px 7px; border-radius: 50%; box-shadow: 20px 2px 0 -14px #5a8caa, 10px 27px 0 -13px #6598ac; } +.road { position: absolute; z-index: 1; background: #183e45; border: 2px solid #3b6c67; box-shadow: inset 0 0 0 3px rgba(5, 25, 35, .35), 0 0 22px rgba(32, 153, 157, .12); }.road span { position: absolute; color: rgba(116, 219, 211, .5); font: 10px var(--mono); letter-spacing: .2em; }.road-main { left: -120px; top: 531px; width: 1850px; height: 111px; transform: rotate(-2deg); }.road-main span { top: 47px; left: 215px; }.road-cross { left: 657px; top: 232px; width: 122px; height: 610px; transform: rotate(16deg); }.road-cross span { top: 300px; left: 27px; transform: rotate(-90deg); }.road-branch { left: 975px; top: 406px; width: 700px; height: 73px; transform: rotate(26deg); border-color: #6b5687; background: #2b3145; }.road-branch span { top: 27px; left: 30px; color: rgba(204, 156, 255, .65); }.plaza { position: absolute; z-index: 2; left: 683px; top: 590px; width: 188px; height: 122px; border: 3px solid #a17d51; background: #4b4b3b; transform: rotate(45deg); box-shadow: 0 0 0 7px rgba(32, 68, 64, .7), inset 0 0 0 2px #c6a15f; }.plaza span { position: absolute; left: 45px; top: 48px; transform: rotate(-45deg); color: #f5d894; font: 11px var(--mono); white-space: nowrap; } + +.world-building { position: absolute; z-index: 3; left: var(--building-x); top: var(--building-y); width: var(--building-w); height: var(--building-h); padding: 0; border: 1px solid color-mix(in srgb, var(--district-accent) 65%, #1e475b); border-radius: 5px; background: #102531; box-shadow: 8px 13px 0 rgba(4, 13, 19, .45), 0 0 24px color-mix(in srgb, var(--district-accent) 24%, transparent), inset 0 0 0 3px rgba(5, 17, 27, .62); transform: perspective(440px) rotateX(4deg); transition: transform .2s ease, filter .2s ease, border-color .2s ease; overflow: hidden; text-align: left; }.world-building:hover, .world-building.is-selected { z-index: 5; filter: brightness(1.24); transform: perspective(440px) rotateX(4deg) translateY(-5px); border-color: var(--district-accent); }.world-building.is-search-hidden { opacity: .16; filter: grayscale(1); }.building-cap { position: absolute; top: -22px; left: 13px; width: calc(100% - 26px); height: 24px; border: 1px solid var(--district-accent); background: #152f3a; transform: skewX(-23deg); box-shadow: inset 0 0 14px color-mix(in srgb, var(--district-accent) 33%, transparent); }.building-facade { position: absolute; inset: 32px 11px 31px; display: grid; grid-template-columns: repeat(5, 1fr); grid-auto-rows: 16px; gap: 9px 10px; padding: 9px; background: repeating-linear-gradient(90deg, rgba(53, 111, 116, .16) 0 22px, transparent 22px 40px), #17313a; border: 1px solid rgba(98, 166, 168, .55); }.building-window { height: 9px; background: var(--district-accent); opacity: calc(.25 + (var(--construction) / 130)); box-shadow: 0 0 7px var(--district-accent); animation: window-blink 4s infinite alternate; animation-delay: calc(var(--window-index) * -140ms); }.building-sign { position: absolute; z-index: 1; top: 7px; left: 9px; right: 9px; min-height: 23px; display: flex; align-items: center; gap: 6px; padding: 3px 7px; color: var(--white); background: rgba(7, 20, 29, .92); border-left: 3px solid var(--district-accent); font: 11px var(--mono); letter-spacing: .03em; }.building-sign b { color: var(--district-accent); font-size: 18px; }.building-status { position: absolute; bottom: 7px; left: 12px; color: var(--district-accent); font: 9px var(--mono); letter-spacing: .1em; }.building-floor { position: absolute; left: -8px; right: -8px; bottom: -13px; height: 18px; background: #081a25; border-top: 2px solid var(--district-accent); transform: skewX(-22deg); }.building-issues { transform: perspective(440px) rotateX(4deg) rotate(-1deg); }.building-prs { box-shadow: 8px 13px 0 rgba(4, 13, 19, .45), 0 0 30px rgba(232, 135, 255, .34); }.building-prs .building-sign { color: var(--pink); }.construction-crane { position: absolute; z-index: 2; width: 13px; height: 236px; border-left: 5px solid var(--gold); opacity: .75; }.construction-crane i { position: absolute; top: 22px; left: 0; width: 145px; height: 5px; background: var(--gold); }.construction-crane b { position: absolute; top: 26px; left: 118px; width: 2px; height: 130px; background: var(--gold); }.construction-crane em { position: absolute; top: 151px; left: 109px; width: 20px; height: 20px; border: 2px solid var(--orange); border-radius: 50%; animation: crane-pulse 2s infinite; }.crane-one { left: 480px; top: 115px; transform: rotate(-2deg); }.crane-two { right: 173px; top: 107px; transform: scale(.82) rotate(3deg); } +.agent-bot { position: absolute; z-index: 5; width: 24px; height: 31px; border: 2px solid var(--green); background: #182d33; box-shadow: 0 0 14px rgba(159, 242, 84, .48); animation: bot-walk 4s ease-in-out infinite; }.agent-bot::after { content: ""; position: absolute; left: 4px; right: 4px; top: 6px; height: 7px; background: var(--green); box-shadow: 0 0 7px var(--green); }.agent-bot i, .agent-bot b { position: absolute; bottom: -8px; width: 4px; height: 8px; background: var(--green); }.agent-bot i { left: 2px; }.agent-bot b { right: 2px; }.bot-a { left: 763px; top: 437px; }.bot-b { left: 1086px; top: 453px; animation-delay: -2s; border-color: var(--gold); }.bot-b::after { background: var(--gold); box-shadow: 0 0 7px var(--gold); }.bot-b i, .bot-b b { background: var(--gold); }.quest-marker { position: absolute; z-index: 6; left: 1006px; top: 552px; width: 43px; height: 43px; display: grid; place-items: center; color: var(--gold); border: 4px solid var(--gold); border-radius: 50%; font: bold 28px var(--mono); box-shadow: 0 0 16px rgba(255, 201, 91, .6); animation: quest-pulse 1.8s infinite; }.pr-gate { position: absolute; z-index: 4; left: 695px; top: 415px; width: 130px; height: 76px; display: grid; place-items: center; align-content: center; gap: 6px; color: var(--pink); border: 3px solid var(--pink); border-radius: 5px; background: #2c173d; box-shadow: 0 0 23px rgba(232, 135, 255, .55), inset 0 0 16px rgba(232, 135, 255, .2); transform: perspective(240px) rotateX(8deg); }.pr-gate span { font: 9px var(--mono); }.pr-gate b { font-size: 11px; }.player-avatar { position: absolute; z-index: 8; width: 42px; height: 74px; transform: translate(-50%, -78%); transition: left .13s linear, top .13s linear; filter: drop-shadow(0 7px 3px rgba(0, 0, 0, .55)); }.avatar-shadow { position: absolute; left: 1px; bottom: -3px; width: 40px; height: 11px; border-radius: 50%; background: rgba(0, 0, 0, .48); }.avatar-legs { position: absolute; left: 9px; bottom: 5px; width: 24px; height: 28px; display: flex; gap: 3px; }.avatar-legs i { flex: 1; background: #152d4b; border: 2px solid #09192d; }.avatar-body { position: absolute; left: 7px; bottom: 25px; width: 29px; height: 29px; background: #171b24; border: 2px solid #060d19; }.avatar-body::after { content: "⌘"; position: absolute; inset: 2px; display: grid; place-items: center; color: var(--white); font-size: 17px; }.avatar-pack { position: absolute; z-index: -1; left: -7px; top: 2px; width: 11px; height: 22px; background: #3a2a20; border: 1px solid #9a6d38; }.avatar-head { position: absolute; left: 10px; top: 3px; width: 23px; height: 22px; background: #5f402d; border: 2px solid #09111c; border-radius: 3px 3px 8px 8px; }.avatar-head::before { content: ""; position: absolute; left: -2px; right: -2px; top: -3px; height: 9px; background: #171515; border-radius: 5px 5px 0 0; }.world-sign { position: absolute; z-index: 6; display: grid; gap: 2px; padding: 11px 15px; background: rgba(5, 20, 30, .91); border: 2px solid var(--cyan); box-shadow: 0 0 19px rgba(100, 244, 225, .22), inset 0 0 0 3px rgba(11, 56, 65, .7); transform: perspective(340px) rotateX(4deg); }.world-sign small { color: var(--green); font: 9px var(--mono); }.world-sign strong { color: var(--cyan); font: 17px var(--mono); letter-spacing: .06em; }.world-sign b { color: #bfe6ee; font: 13px var(--mono); }.world-sign em { color: var(--gold); font: 10px var(--mono); font-style: normal; }.repo-sign { left: 631px; top: 155px; width: 255px; transform: perspective(340px) rotateX(4deg) rotate(-1deg); }.build-sign { right: 88px; top: 201px; width: 260px; border-color: var(--green); }.build-sign strong { color: var(--green); }.build-sign b { color: var(--white); } + +.nearby-prompt { position: absolute; z-index: 5; left: 300px; bottom: 65px; display: grid; gap: 2px; min-width: 192px; padding: 9px 13px; border-left: 2px solid var(--cyan); background: rgba(5, 19, 30, .84); }.nearby-prompt span, .nearby-prompt small { color: var(--muted); font: 9px var(--mono); letter-spacing: .1em; }.nearby-prompt strong { color: var(--cyan); font: 13px var(--mono); }.nearby-prompt.is-ready { border-color: var(--gold); }.nearby-prompt.is-ready strong { color: var(--gold); }.city-reticle { position: absolute; z-index: 4; left: 50%; top: 53%; width: 45px; height: 45px; transform: translate(-50%, -50%); pointer-events: none; opacity: .35; }.city-reticle i, .city-reticle b { position: absolute; display: block; background: var(--cyan); }.city-reticle i { left: 21px; top: 0; width: 1px; height: 45px; }.city-reticle b { left: 0; top: 21px; width: 45px; height: 1px; }.movement-help { position: absolute; z-index: 5; right: 306px; bottom: 63px; display: flex; align-items: center; gap: 5px; color: var(--muted); font: 9px var(--mono); }.movement-help kbd, .terminal-form input, .repo-search-wrap kbd { font-family: var(--mono); }.movement-help kbd { padding: 3px 5px; border: 1px solid #38556a; border-radius: 4px; color: var(--white); background: #112334; }.movement-help span { margin-right: 6px; } +.feedback-line { position: absolute; z-index: 5; left: 300px; right: 306px; bottom: 14px; display: flex; align-items: center; gap: 8px; min-height: 29px; padding: 0 11px; border-top: 1px solid #1d4d64; background: rgba(5, 17, 27, .82); color: var(--muted); font: 10px var(--mono); }.feedback-line i { width: 6px; height: 6px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px var(--green); }.feedback-line time { margin-left: auto; color: var(--cyan); } + +.right-rail { position: absolute; z-index: 4; top: 72px; right: 15px; width: 276px; display: grid; gap: 10px; }.map-panel, .agents-panel, .event-panel { padding: 12px; }.panel-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-bottom: 9px; color: var(--cyan); border-bottom: 1px solid rgba(61, 141, 177, .35); font: 11px var(--mono); letter-spacing: .08em; }.panel-heading > span { display: flex; align-items: center; gap: 7px; }.panel-heading small { color: var(--muted); font: 8px var(--mono); }.count-badge { display: inline-grid; place-items: center; width: 21px; height: 17px; color: var(--green); border: 1px solid var(--green); border-radius: 4px; font: 9px var(--mono); }.minimap-viewport { position: relative; height: 153px; margin: 11px 0 9px; overflow: hidden; border: 1px solid #22536e; border-radius: 6px; background: #1c3b2b; }.minimap-grid { position: absolute; inset: 0; background-color: #173b2e; background-image: linear-gradient(25deg, transparent 40%, rgba(19, 75, 63, .75) 41% 47%, transparent 48%), repeating-linear-gradient(90deg, rgba(91, 169, 90, .23) 0 3px, transparent 3px 17px), repeating-linear-gradient(0deg, rgba(70, 131, 71, .2) 0 3px, transparent 3px 21px); }.map-road { position: absolute; background: #7a7251; opacity: .8; }.map-road-a { left: -12%; top: 52%; width: 125%; height: 7px; transform: rotate(-15deg); }.map-road-b { left: 42%; top: -12%; width: 8px; height: 125%; transform: rotate(24deg); }.minimap-district { position: absolute; width: 13px; height: 13px; padding: 0; border: 2px solid #081a24; transform: translate(-50%, -50%); box-shadow: 0 0 5px currentColor; }.minimap-district:hover { transform: translate(-50%, -50%) scale(1.4); }.map-player { position: absolute; left: 50%; top: 50%; width: 0; height: 0; border-right: 7px solid transparent; border-bottom: 16px solid white; border-left: 7px solid transparent; filter: drop-shadow(0 0 4px var(--cyan)); }.map-caption { display: flex; justify-content: space-between; color: var(--muted); font: 9px var(--mono); }.map-caption b { color: var(--cyan); }.map-legend { display: flex; justify-content: space-between; color: var(--muted); font: 8px var(--mono); }.map-legend i { display: inline-block; width: 6px; height: 6px; margin-right: 3px; border-radius: 50%; }.legend-player { background: white; }.legend-building { background: var(--green); }.legend-road { background: #bca46b; }.agent-list { display: grid; gap: 10px; margin: 12px 0; padding: 0; list-style: none; }.agent-row { display: grid; grid-template-columns: 27px 1fr auto; gap: 7px; align-items: center; min-width: 0; }.agent-avatar { width: 26px; height: 26px; display: grid; place-items: center; color: var(--agent-color); border: 1px solid var(--agent-color); border-radius: 5px; font: 9px var(--mono); }.agent-copy { display: grid; gap: 1px; min-width: 0; }.agent-copy strong { overflow: hidden; color: var(--white); font: 9px var(--mono); text-overflow: ellipsis; white-space: nowrap; }.agent-copy small { color: var(--muted); font: 8px var(--mono); }.agent-copy em { overflow: hidden; color: var(--agent-color); font: 8px var(--mono); font-style: normal; text-overflow: ellipsis; white-space: nowrap; }.progress-line { display: block; height: 3px; margin-top: 2px; background: #173040; }.progress-line b { display: block; height: 100%; background: var(--agent-color); box-shadow: 0 0 6px var(--agent-color); transition: width .4s ease; }.agent-state { color: var(--green); font: 8px var(--mono); }.swarm-flow { display: grid; justify-items: center; gap: 3px; padding-top: 9px; border-top: 1px solid var(--line); color: var(--muted); font: 8px var(--mono); }.swarm-flow span:first-of-type { color: var(--cyan); }.swarm-flow i { color: var(--line-hot); font-style: normal; }.event-panel { max-height: 205px; overflow: hidden; }.event-feed { display: grid; gap: 7px; margin: 11px 0 0; padding: 0; overflow: auto; list-style: none; }.event-row { display: grid; grid-template-columns: 52px 8px 1fr; gap: 6px; align-items: start; color: var(--muted); font-size: 9px; }.event-row time { color: #668aa3; font: 8px var(--mono); }.event-row p { margin: 0; line-height: 1.3; }.event-dot { width: 6px; height: 6px; margin-top: 3px; border-radius: 50%; background: var(--green); box-shadow: 0 0 7px var(--green); }.event-repository-issue .event-dot, .event-repository-pull-request .event-dot { background: var(--orange); box-shadow: 0 0 7px var(--orange); }.event-build-started .event-dot, .event-deployment-started .event-dot { background: var(--gold); box-shadow: 0 0 7px var(--gold); } + +.inspector-drawer { position: absolute; z-index: 7; top: 385px; right: 307px; width: 330px; min-height: 300px; padding: 15px; opacity: 0; pointer-events: none; transform: translateX(24px); transition: opacity .22s ease, transform .22s ease; }.inspector-drawer.is-open { opacity: 1; pointer-events: auto; transform: translateX(0); }.drawer-heading { display: flex; justify-content: space-between; gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }.drawer-heading div { display: grid; gap: 3px; }.drawer-heading strong { color: var(--white); font: 13px var(--mono); }.inspector-content { min-height: 215px; padding: 14px 0 8px; }.inspect-header { display: grid; gap: 6px; }.inspect-header h2 { margin: 0; color: var(--white); font: 17px var(--mono); }.inspect-header p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.45; }.district-metrics { display: flex; flex-wrap: wrap; gap: 5px 11px; margin-top: 5px; color: var(--cyan); font: 9px var(--mono); }.district-metrics span + span { color: var(--muted); }.inspector-actions { display: flex; flex-wrap: wrap; gap: 7px; margin: 15px 0 10px; }.panel-action { padding: 7px 9px; border: 1px solid var(--line-hot); border-radius: 5px; background: #0c2435; color: var(--cyan); font: 9px var(--mono); }.panel-action:hover { border-color: var(--cyan); }.panel-action.primary { border-color: var(--cyan); background: rgba(43, 111, 117, .3); }.panel-action:disabled { cursor: not-allowed; opacity: .48; }.mini-file-list, .file-list, .commit-list, .issue-list, .pr-list { display: grid; gap: 5px; }.mini-file-list button, .file-row, .commit-row, .issue-row, .pr-row { display: grid; grid-template-columns: 20px 1fr auto; gap: 7px; align-items: center; padding: 6px 7px; border: 1px solid transparent; background: rgba(11, 37, 51, .72); color: var(--muted); text-align: left; }.mini-file-list button:hover, .file-row:hover, .file-row.is-active, .commit-row:hover, .issue-row:hover, .pr-row:hover { border-color: var(--line-hot); color: var(--white); }.mini-file-list b, .file-row b { color: var(--cyan); }.file-row strong, .commit-row strong, .issue-row strong, .pr-row strong { display: block; overflow: hidden; color: var(--white); font: 10px var(--mono); text-overflow: ellipsis; white-space: nowrap; }.file-row small, .commit-row small, .issue-row small, .pr-row small { display: block; color: var(--muted); font-size: 9px; }.collection-heading { display: grid; gap: 4px; margin-bottom: 10px; }.collection-heading span { color: var(--cyan); font: 9px var(--mono); letter-spacing: .1em; }.collection-heading strong { color: var(--white); font: 14px var(--mono); }.collection-heading small { color: var(--muted); font-size: 10px; }.commit-row, .issue-row, .pr-row { grid-template-columns: 15px 1fr auto; }.commit-node, .issue-marker, .pr-marker { width: 9px; height: 9px; display: grid; place-items: center; color: var(--green); border: 1px solid var(--green); border-radius: 50%; font: 8px var(--mono); }.issue-marker { color: var(--orange); border-color: var(--orange); }.pr-marker { color: var(--pink); border-color: var(--pink); border-radius: 2px; }.commit-row em, .issue-row em, .pr-row em { color: var(--green); font: 8px var(--mono); font-style: normal; }.issue-row em { color: var(--orange); }.pr-row em { color: var(--pink); }.code-preview { max-height: 145px; overflow: auto; margin: 13px 0; padding: 11px; color: var(--green); border: 1px solid #244f52; background: #07151c; font: 10px/1.45 var(--mono); white-space: pre-wrap; }.build-stack { display: grid; gap: 9px; color: var(--muted); font: 10px var(--mono); }.build-stack > div:not(.pipeline-steps) { display: flex; justify-content: space-between; gap: 10px; }.build-stack b { color: var(--green); font: 10px var(--mono); }.status-running, .status-launching { color: var(--gold) !important; }.pipeline-steps { display: flex; gap: 4px; padding-top: 10px; border-top: 1px solid var(--line); }.pipeline-steps i { flex: 1; padding: 5px 2px; border: 1px solid #27404c; color: #5e7a8a; font: 8px var(--mono); font-style: normal; text-align: center; }.pipeline-steps i.done { border-color: #3a764e; color: var(--green); }.pipeline-steps i.active { border-color: var(--cyan); color: var(--cyan); } +.drawer-tabs { display: flex; gap: 4px; padding-top: 9px; border-top: 1px solid var(--line); }.drawer-tabs button { flex: 1; padding: 5px 2px; border: 1px solid #234456; background: transparent; color: var(--muted); font: 8px var(--mono); }.drawer-tabs button:hover { color: var(--cyan); border-color: var(--cyan); } + +.terminal-phone { position: fixed; z-index: 10; right: 22px; bottom: 91px; width: 300px; min-height: 405px; padding: 17px 12px 13px; border: 3px solid #2e3c48; border-radius: 27px; background: #060b10; box-shadow: 0 18px 50px rgba(0,0,0,.65), 0 0 0 4px rgba(1, 6, 10, .75), inset 0 0 22px rgba(96, 177, 211, .08); opacity: 0; pointer-events: none; transform: translateY(14px) rotate(1deg); transition: opacity .25s ease, transform .25s ease; }.terminal-phone.is-open { opacity: 1; pointer-events: auto; transform: translateY(0) rotate(1deg); }.phone-speaker { position: absolute; top: 8px; left: 50%; width: 55px; height: 4px; transform: translateX(-50%); border-radius: 5px; background: #29343d; }.terminal-top { display: flex; justify-content: space-between; align-items: center; padding: 3px 4px 11px; color: #d3e6eb; font: 10px var(--mono); }.terminal-top .icon-button { width: 22px; height: 22px; }.terminal-screen { min-height: 344px; padding: 11px; border: 1px solid #213844; border-radius: 7px; background: #020608; }.terminal-header { display: grid; gap: 4px; padding-bottom: 10px; border-bottom: 1px solid #1c303b; }.terminal-header strong { color: var(--white); font: 13px var(--mono); }.terminal-header small { color: var(--muted); font: 8px var(--mono); }.terminal-output { height: 222px; overflow: auto; padding-top: 9px; color: #8bd35e; font: 9px/1.5 var(--mono); }.terminal-line.command { color: var(--cyan); }.terminal-form { display: flex; gap: 5px; align-items: center; padding: 8px 0; border-top: 1px solid #1c303b; color: var(--green); font: 10px var(--mono); }.terminal-form input { flex: 1; min-width: 0; padding: 3px; border: 0; outline: 0; background: transparent; color: var(--white); font: 10px var(--mono); }.terminal-form button { border: 0; background: transparent; color: var(--cyan); font-size: 17px; }.terminal-shortcuts { display: flex; flex-wrap: wrap; gap: 4px; }.terminal-shortcuts button { padding: 4px 5px; border: 1px solid #244655; border-radius: 3px; background: #091a25; color: var(--muted); font: 8px var(--mono); }.terminal-shortcuts button:hover { color: var(--cyan); border-color: var(--cyan); }.phone-home { width: 54px; height: 4px; margin: 11px auto 0; border-radius: 5px; background: #29343d; } +.toolbelt { position: absolute; z-index: 6; left: 50%; bottom: 61px; display: grid; grid-template-columns: repeat(8, 1fr); width: min(560px, calc(100% - 620px)); transform: translateX(-50%); overflow: hidden; }.tool-slot { min-width: 0; display: grid; place-items: center; gap: 2px; padding: 7px 4px 5px; border: 0; border-right: 1px solid #29495b; background: rgba(6, 18, 29, .94); color: var(--muted); }.tool-slot:last-child { border-right: 0; }.tool-slot:hover, .tool-slot.is-active { background: rgba(16, 57, 69, .96); color: var(--tool-color); box-shadow: inset 0 -2px 0 var(--tool-color); }.tool-slot b { color: var(--tool-color); font-size: 24px; line-height: 1; text-shadow: 0 0 9px var(--tool-color); }.tool-slot strong { overflow: hidden; max-width: 100%; color: var(--white); font: 8px var(--mono); text-align: center; text-overflow: ellipsis; white-space: nowrap; }.tool-slot small { color: var(--muted); font: 7px var(--mono); }.tool-key { position: absolute; margin: 0 0 -49px 0; color: #6a8ca3; font: 8px var(--mono); } +.software-shelf { position: relative; z-index: 8; width: min(1500px, 100%); margin: 8px auto 0; padding: 10px 14px 12px; border: 1px solid #17415b; border-radius: 10px; background: rgba(5, 17, 29, .92); }.shelf-title { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }.shelf-title span { color: var(--white); font: 700 13px var(--mono); letter-spacing: .04em; }.shelf-title small { color: var(--cyan); font: 8px var(--mono); }.object-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 6px; }.object-card { min-height: 105px; display: grid; place-items: center; align-content: center; gap: 3px; padding: 7px 4px; border: 1px solid #1b4056; border-radius: 6px; background: #071521; }.object-card:hover { border-color: var(--cyan); background: #0d2635; transform: translateY(-2px); }.object-card strong { color: var(--white); font: 8px var(--mono); text-align: center; }.object-card small { color: var(--muted); font: 8px var(--mono); }.object-art { display: grid; place-items: center; width: 51px; height: 58px; color: var(--cyan); font-size: 42px; text-shadow: 0 0 13px currentColor; }.hammer-art { color: var(--orange); }.backpack-art { color: #ff7561; }.box-art { color: var(--blue); }.tablet-art { color: var(--pink); }.key-art { color: var(--gold); }.vault-art { color: #a9b1b7; }.rocket-art { color: var(--orange); }.touch-controls { display: none; }.disclaimer { position: relative; z-index: 7; display: flex; justify-content: space-between; width: min(1500px, 100%); margin: 9px auto 0; color: #617f94; font: 8px var(--mono); letter-spacing: .08em; } + +@keyframes window-blink { 0%, 77% { opacity: .45; } 86% { opacity: 1; } 92%, 100% { opacity: .58; } } +@keyframes quest-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.09); box-shadow: 0 0 28px rgba(255, 201, 91, .88); } } +@keyframes crane-pulse { 0%, 100% { opacity: .35; transform: scale(.7); } 50% { opacity: 1; transform: scale(1.1); } } +@keyframes bot-walk { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(22px, -4px); } } + +@media (max-width: 1240px) { + .left-rail { width: 222px; }.right-rail { width: 245px; }.world-stage { padding-left: 235px; padding-right: 255px; }.world-viewport { height: 650px; }.toolbelt { width: min(520px, calc(100% - 500px)); }.inspector-drawer { right: 262px; }.feedback-line { left: 260px; right: 260px; }.movement-help { right: 260px; }.nearby-prompt { left: 260px; } +} + +@media (max-width: 980px) { + .topbar { grid-template-columns: 1fr minmax(230px, 1fr) auto; gap: 12px; }.left-rail { position: relative; top: auto; left: auto; width: min(100%, 650px); margin: 0 auto 10px; }.left-rail .how-it-works { display: none; }.world-stage { min-height: 600px; padding: 0; }.world-viewport { height: 650px; }.right-rail { position: relative; top: auto; right: auto; width: 100%; grid-template-columns: 1fr 1fr 1fr; margin: 10px auto 0; }.event-panel { max-height: 255px; }.inspector-drawer { position: fixed; top: auto; right: 15px; bottom: 160px; width: min(360px, calc(100% - 30px)); }.toolbelt { position: relative; left: auto; bottom: auto; width: min(640px, 100%); margin: 10px auto 0; transform: none; }.software-shelf { margin-top: 10px; }.feedback-line { left: 0; right: 0; bottom: 0; }.movement-help { right: 12px; bottom: 42px; }.nearby-prompt { left: 12px; bottom: 42px; }.disclaimer { margin-top: 8px; }.world-stage .stage-heading { padding-inline: 2px; } +} + +@media (max-width: 680px) { + .game-shell { padding: 10px 9px 9px; }.topbar { grid-template-columns: 1fr auto; }.repo-search-wrap { grid-column: 1 / -1; grid-row: 2; }.simulated-pill { display: none; }.brand-lockup strong { font-size: 17px; }.stage-heading { align-items: flex-start; flex-direction: column; gap: 6px; }.stage-heading h2 { font-size: 17px; }.world-viewport { height: 500px; }.world-canvas { transform: scale(.7) !important; transform-origin: 0 0; }.right-rail { grid-template-columns: 1fr; }.map-panel, .agents-panel { min-height: 0; }.event-panel { max-height: 210px; }.world-stage { min-height: 550px; }.world-stage .movement-help { display: none; }.nearby-prompt { bottom: 42px; }.toolbelt { grid-template-columns: repeat(4, 1fr); }.tool-slot { min-height: 54px; }.tool-slot b { font-size: 20px; }.tool-key { margin-bottom: -39px; }.object-grid { grid-template-columns: repeat(4, 1fr); }.object-card { min-height: 90px; }.object-art { width: 40px; height: 43px; font-size: 30px; }.shelf-title { align-items: flex-start; flex-direction: column; gap: 3px; }.disclaimer { flex-direction: column; gap: 4px; }.terminal-phone { right: 14px; bottom: 130px; width: min(300px, calc(100% - 28px)); }.inspector-drawer { right: 9px; bottom: 128px; width: calc(100% - 18px); }.left-rail { padding: 14px; }.component-list { grid-template-columns: 1fr 1fr; }.component-list li { font-size: 10px; }.component-list li b { width: 17px; height: 17px; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; } +} + +/* Browser-native opening scene and reality plates. These are local assets from + phase2-unreal/ReferenceSource, so the playable build stays self-contained. */ +.opening-scene { position: fixed; z-index: 100; inset: 0; display: grid; place-items: end start; overflow: hidden; background: #02050d; transition: opacity .55s ease, visibility .55s ease; } +.opening-scene.is-dismissed { opacity: 0; visibility: hidden; pointer-events: none; } +.opening-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; background: #050816; filter: saturate(1.15) contrast(1.05) brightness(.72); } +.opening-vignette { position: absolute; inset: 0; pointer-events: none; background: radial-gradient(circle at 55% 44%, transparent 0 24%, rgba(4, 5, 18, .22) 54%, rgba(2, 4, 12, .92) 100%), linear-gradient(90deg, rgba(2, 5, 13, .9), transparent 55%, rgba(2, 5, 13, .35)); } +.opening-copy { position: relative; z-index: 2; width: min(630px, calc(100% - 48px)); margin: 0 0 11vh 7vw; padding: 24px 28px 27px; border: 1px solid rgba(125, 232, 255, .35); border-radius: 16px; background: linear-gradient(135deg, rgba(4, 14, 32, .86), rgba(20, 10, 46, .72)); box-shadow: 0 0 42px rgba(32, 145, 255, .18), inset 0 0 30px rgba(111, 73, 255, .1); backdrop-filter: blur(7px); } +.opening-copy h1 { margin: 6px 0 7px; color: #f5fbff; font: 800 clamp(50px, 10vw, 104px)/.86 var(--mono); letter-spacing: -.08em; text-shadow: 0 0 28px rgba(181, 140, 255, .75); } +.opening-copy p { margin: 0 0 18px; color: #d5e6f4; font-size: clamp(16px, 2vw, 22px); } +.opening-meta { display: flex; flex-wrap: wrap; gap: 9px 18px; color: #9ab6cf; font: 10px var(--mono); letter-spacing: .06em; } +.opening-meta i { display: inline-block; width: 7px; height: 7px; margin-right: 5px; border-radius: 50%; background: #91e85e; box-shadow: 0 0 12px #91e85e; } +.opening-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-top: 24px; } +.opening-enter, .opening-sound { border: 1px solid rgba(125, 232, 255, .55); border-radius: 8px; color: #effbff; background: rgba(24, 88, 128, .52); font: 700 12px var(--mono); letter-spacing: .06em; cursor: pointer; } +.opening-enter { padding: 14px 17px; box-shadow: 0 0 18px rgba(125, 232, 255, .2); } +.opening-enter b { margin-left: 14px; color: #7de8ff; font-size: 19px; } +.opening-sound { padding: 13px 14px; color: #abc1d7; background: rgba(5, 10, 25, .45); } +.opening-enter:hover, .opening-sound:hover { border-color: #b58cff; box-shadow: 0 0 22px rgba(181, 140, 255, .3); } +.opening-copy > small { display: block; margin-top: 13px; color: #7e9bb6; font: 10px var(--mono); } +.reality-deck { position: absolute; z-index: 5; right: 27px; bottom: 27px; left: 27px; padding: 9px; border: 1px solid rgba(125, 232, 255, .24); border-radius: 10px; background: linear-gradient(90deg, rgba(4, 15, 29, .9), rgba(8, 19, 41, .72)); box-shadow: 0 0 28px rgba(12, 89, 125, .16); backdrop-filter: blur(5px); } +.reality-deck-heading { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 0 3px 7px; color: #9edff4; font: 10px var(--mono); letter-spacing: .07em; } +.reality-deck-heading small { color: #6c8aa4; font-size: 8px; } +.reality-deck-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 7px; } +.reality-plate { display: block; width: 100%; height: 72px; object-fit: cover; border: 1px solid rgba(125, 232, 255, .22); border-radius: 6px; background: #081322; filter: saturate(1.16) contrast(1.04); } +@media (max-width: 680px) { + .opening-copy { width: calc(100% - 24px); margin: 0 12px 24px; padding: 18px; } + .opening-copy h1 { font-size: 53px; } + .reality-deck { right: 12px; bottom: 12px; left: 12px; } + .reality-deck-heading small { display: none; } + .reality-plate { height: 48px; } +} diff --git a/tests/contract.test.mjs b/tests/contract.test.mjs new file mode 100644 index 0000000000000000000000000000000000000000..b603daa127f8a25522807edfc6e85f081884889c --- /dev/null +++ b/tests/contract.test.mjs @@ -0,0 +1,40 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); + +test('frontend wiring exposes the playable MVP surfaces', async () => { + const html = await readFile(join(root, 'index.html'), 'utf8'); + const shared = await readFile(join(root, 'src', 'shared.js'), 'utf8'); + for (const label of ['opening-scene', 'opening-video', 'world-viewport', 'reality-deck', 'minimap-viewport', 'agent-list', 'event-feed', 'toolbelt', 'terminal-phone', 'inspector-drawer', 'touch-controls']) { + assert.match(html, new RegExp(`id="${label}"`)); + } + const interfaceText = `${html}\n${shared}`; + for (const label of ['Terminal Phone', 'Bash Hammer', 'Git Backpack', 'Docker Box', 'Code Tablet', 'SSH Key', 'Database Vault', 'Deploy Rocket']) { + assert.match(interfaceText, new RegExp(label)); + } + assert.match(html, /SNAPKITTYWEST/); + assert.match(html, /SIMULATED REPOSITORY DATA/); + assert.match(html, /data-reference-image="NeonReflections\/3090\.webp"/); + assert.match(html, /data-action="enter-game"/); +}); + +test('the local app contains no runtime network or external asset dependency', async () => { + const files = ['index.html', 'styles.css', 'src/main.ts', 'src/shared.js', 'src/world-architect.js', 'src/ui-reality.js']; + const contents = await Promise.all(files.map((file) => readFile(join(root, file), 'utf8'))); + const source = contents.join('\n'); + assert.doesNotMatch(source, /fetch\s*\(|XMLHttpRequest|WebSocket|https?:\/\//i); + assert.doesNotMatch(source, / { + const packageJson = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')); + assert.equal(packageJson.scripts.test, 'node --test tests/*.test.mjs'); + assert.equal(packageJson.scripts.build, 'node scripts/build.mjs'); + assert.equal(packageJson.private, true); +}); diff --git a/tests/world.test.mjs b/tests/world.test.mjs new file mode 100644 index 0000000000000000000000000000000000000000..940efc5d379f9c274650591ec8b71df1e8878bd9 --- /dev/null +++ b/tests/world.test.mjs @@ -0,0 +1,63 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { EVENT_TYPES, commandTranscript } from '../src/shared.js'; +import { + DISTRICTS, + advanceWorldState, + createWorldState, + getNearbyDistrict, + worldReducer +} from '../src/world-architect.js'; + +test('initial world is deterministic and contains all repository districts', () => { + const first = createWorldState(); + const second = createWorldState(); + assert.deepEqual(first, second); + assert.equal(first.repository.simulated, true); + assert.deepEqual(first.districts.map((district) => district.id), ['src', 'tests', 'docs', 'issues', 'prs', 'actions']); + assert.equal(DISTRICTS.length, 6); + assert.equal(first.agents.length, 3); +}); + +test('movement is bounded and building collision preserves the previous position', () => { + let state = createWorldState(); + const initial = { ...state.player }; + for (let index = 0; index < 120; index += 1) state = worldReducer(state, { type: EVENT_TYPES.PLAYER_MOVE, dx: 0, dy: 1 }); + assert.ok(state.player.y <= 850); + for (let index = 0; index < 120; index += 1) state = worldReducer(state, { type: EVENT_TYPES.PLAYER_MOVE, dx: -1, dy: 0 }); + assert.ok(state.player.x >= 70); + const atStart = createWorldState(); + const blocked = worldReducer(atStart, { type: EVENT_TYPES.PLAYER_MOVE, dx: 0, dy: -1 }); + assert.notDeepEqual(blocked.player, initial); + assert.equal(blocked.player.step, 1); +}); + +test('the world emits a deterministic construction event on each simulation step', () => { + const start = createWorldState(); + const first = advanceWorldState(start); + const replay = advanceWorldState(createWorldState()); + assert.equal(first.state.tick, 1); + assert.deepEqual(first.event, replay.event); + assert.equal(first.event.type, EVENT_TYPES.AGENT_PROGRESS); + assert.ok(first.state.events[0].detail.message.includes('WORLD ARCHITECT')); +}); + +test('entering and inspecting a repository district updates the shared state', () => { + let state = createWorldState(); + state = worldReducer(state, { type: EVENT_TYPES.SELECT_DISTRICT, districtId: 'docs' }); + state = worldReducer(state, { type: EVENT_TYPES.OPEN_INSPECTOR }); + assert.equal(state.selectedDistrictId, 'docs'); + assert.equal(state.activePanel, 'inspector'); + state = worldReducer(state, { type: EVENT_TYPES.ENTER_DISTRICT, districtId: 'docs' }); + assert.equal(state.view, 'district'); + assert.equal(state.events[0].type, EVENT_TYPES.ENTER_DISTRICT); +}); + +test('nearby district detection and terminal transcript are browser-local', () => { + const state = createWorldState(); + const nearby = getNearbyDistrict(state); + assert.equal(nearby.inRange, false); + const transcript = commandTranscript('cargo test', state); + assert.ok(transcript.some((line) => line.includes('142 passed'))); + assert.ok(!transcript.some((line) => line.includes('network'))); +}); diff --git a/tools/verify-phase2.mjs b/tools/verify-phase2.mjs new file mode 100644 index 0000000000000000000000000000000000000000..875c73677fc48ff74b60efba12f86a2b349b8b23 --- /dev/null +++ b/tools/verify-phase2.mjs @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict'; +import { access, readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..', 'phase2-unreal'); + +const requiredFiles = [ + 'GitWorldUnreal.uproject', + 'Config/DefaultEngine.ini', + 'Source/GitWorldUnreal/GitWorldUnreal.Build.cs', + 'Source/GitWorldUnreal/Public/GitWorldData.h', + 'Source/GitWorldUnreal/Public/GitWorldRepoWorld.h', + 'Source/GitWorldUnreal/Public/GitWorldRepoPortal.h', + 'Source/GitWorldUnreal/Public/GitWorldWorldSubsystem.h', + 'Source/GitWorldUnreal/Public/GitWorldCudaBridge.h', + 'Source/GitWorldUnreal/Public/GitWorldOpeningScene.h', + 'Source/GitWorldUnreal/Public/GitWorldVRPawn.h', + 'RealityCapture/Tools/reconstruct.ps1', + 'Content/RealityScene/README.md', + 'ReferenceSource/manifest.json', + 'EditorScripts/import_gitworld_reference_scene.py', + 'EditorScripts/import_gitworld_opening.py', + 'Content/VR/README.md', + 'Compute/CMakeLists.txt', + 'Compute/GitWorldBuildPass.cu' +]; + +for (const file of requiredFiles) { + await access(join(root, file)); +} + +const project = JSON.parse(await readFile(join(root, 'GitWorldUnreal.uproject'), 'utf8')); +assert.equal(project.Modules[0].Name, 'GitWorldUnreal'); +assert.equal(project.Plugins.some((plugin) => plugin.Name === 'OpenXR' && plugin.Enabled === true), true); + +const buildRules = await readFile(join(root, 'Source', 'GitWorldUnreal', 'GitWorldUnreal.Build.cs'), 'utf8'); +assert.match(buildRules, /RuntimeDependencies\.Add/); +assert.match(buildRules, /3099\.mp4/); + +const engine = await readFile(join(root, 'Config', 'DefaultEngine.ini'), 'utf8'); +assert.match(engine, /r\.Nanite\.ProjectEnabled=(?:1|True)/); +assert.match(engine, /r\.Lumen\.HardwareRayTracing=(?:1|True)/); +assert.match(engine, /ResolutionSizeX=7680/); +assert.match(engine, /ResolutionSizeY=4320/); + +const sourceFiles = [ + 'Source/GitWorldUnreal/Public/GitWorldRepoWorld.h', + 'Source/GitWorldUnreal/Private/GitWorldSimulation.cpp', + 'Source/GitWorldUnreal/Private/GitWorldRepoWorld.cpp', + 'Source/GitWorldUnreal/Private/GitWorldRepoPortal.cpp', + 'Source/GitWorldUnreal/Private/GitWorldWorldSubsystem.cpp', + 'Source/GitWorldUnreal/Private/GitWorldCudaBridge.cpp', + 'Source/GitWorldUnreal/Private/GitWorldGameMode.cpp', + 'Source/GitWorldUnreal/Private/GitWorldOpeningScene.cpp', + 'Source/GitWorldUnreal/Private/GitWorldVRPawn.cpp' +]; +const source = (await Promise.all(sourceFiles.map((file) => readFile(join(root, file), 'utf8')))).join('\n'); +for (const required of ['AdvanceSnapshot', 'ActivateRepository', 'SpawnDemoRepositoryWorld', 'GW_CudaBuildPass', 'TerminalTranscript', 'RealityCaptureMesh', 'ReferenceSetId', 'ReferenceImages', 'OpenUrl', 'OnEndReached', 'SetTrackingOrigin', 'InteractWithWorld', 'CompleteTeleport']) { + assert.match(source, new RegExp(required)); +} +assert.doesNotMatch(source, /ironic-mirror|shadow-orchestrator/i); + +const captureScript = await readFile(join(root, 'RealityCapture', 'Tools', 'reconstruct.ps1'), 'utf8'); +for (const required of ['-addFolder', '-align', '-selectMaximalComponent', '-calculateTexture', '-exportSelectedModel']) { + assert.match(captureScript, new RegExp(required.replace('-', '\\-'))); +} + +const referenceManifest = JSON.parse(await readFile(join(root, 'ReferenceSource', 'manifest.json'), 'utf8')); +assert.equal(referenceManifest.photogrammetryReady, false); +assert.equal(referenceManifest.assets.length, 4); +assert.equal(referenceManifest.openingScene.file, 'Opening/3099.mp4'); +const editorScript = await readFile(join(root, 'EditorScripts', 'import_gitworld_reference_scene.py'), 'utf8'); +for (const required of ['AssetImportTask', 'create_material_expression', 'reference_images', 'save_current_level']) { + assert.match(editorScript, new RegExp(required)); +} +const openingScript = await readFile(join(root, 'EditorScripts', 'import_gitworld_opening.py'), 'utf8'); +for (const required of ['3099.mp4', 'FileMediaSourceFactoryNew', 'MediaPlayerFactoryNew', 'MediaTextureFactoryNew', 'GitWorldOpeningScene']) { + assert.match(openingScript, new RegExp(required)); +} + +const openingVideo = await readFile(join(root, 'ReferenceSource', 'Opening', '3099.mp4')); +assert.ok(openingVideo.length > 0); + +console.log(`Phase 3 contract PASS: ${requiredFiles.length} files present; reference plates and opening video wired; Unreal execution still requires the local editor.`); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..f035d6ce6209b440cdb809e629cc737169121535 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "allowJs": true, + "checkJs": false, + "noEmit": true, + "strict": true, + "lib": ["ES2022", "DOM"] + }, + "include": ["src/**/*.ts", "src/**/*.js"] +}