diff --git a/.agent/current_task.md b/.agent/current_task.md index 45dfa19..303f025 100644 --- a/.agent/current_task.md +++ b/.agent/current_task.md @@ -1,30 +1,32 @@ -# Current Task: Entry/Exit Implementation & Camera Snapping +# Current Task: 3D Spatial Audio & SoundManager Implementation ## Status: COMPLETED ✅ ## What was done this session -### 1. Game.ts Decomposition ✅ -- **InventoryManager** moved to `src/systems/`. -- **GameSemanticAPI** extracted to `src/systems/`. -- MONOLITH reduced from 80KB to 40KB. +### 1. SoundManager Architecture ✅ +- Implemented `SoundManager.ts` using **Web Audio API**. +- Support for **3D Spatial Audio** (HRTF panning), **Convolution Reverb**, and **Delay** effects. +- Dynamic **Proximity EQ** (+6dB bass boost at 250Hz) and **Reverb Scaling**. -### 2. Universal Exit/Entry Components ✅ -- Implemented `Exit` and `Entry` components working on ANY object type (Entities, Triggerboxes, Quads). -- Supported **Same-Scene Teleportation** (empty `targetSceneId`). -- Automated collision-based transitions in `ComponentSystem`. -- Verified actor transfer and state persistence (NPCs & Player). +### 2. 2.5D Spatial Logic ✅ +- Developed a physically grounded 2.5D sound model: + * `Parallax 1.1` = Head Level (Z=0). + * `Parallax 1.0` = Foreground (Z=-400). + * `Parallax 0.0` = Infinity (Z=-10000). + * `Parallax < 0` = Behind Listener (+Z). +- Integrated **Camera Zoom** scaling: Z-depth is attenuated by `1/zoom`. -### 3. Camera Snap ✅ -- Implemented `scene.snapCameraToPlayer()` for instant positioning after transition. -- Fixed bug where camera would "stick" to old player reference after scene load. +### 3. Engine Integration ✅ +- Synchronized `SoundManager` update loop in `Game.ts`. +- Exposed complete Audio API through `ScriptAPI.ts` (`api.playSoundAttached`, `api.loadReverbIR`, etc.). +- Created a demo script and scene for visual/auditory validation. -### 4. Verification ✅ -- `npm run typecheck` passed. -- `npm run test` passed: 18 files, 205 tests. -- Restored storage/semantic runtime contracts after a partial API rewrite caused PUT/DROP and inventory regressions. +### 4. Documentation & Memory ✅ +- Wrote comprehensive technical documentation in `SoundSys.md`. +- Persisted architectural facts in `agent_memory`. ## Next Steps -1. **Feature Development**: Resume gameplay features as per `GDD.md`. -2. **Architecture Audit**: Review remaining "any" casts in `ComponentSystem.ts`. -3. **Browser QA**: Optionally verify instant camera snap visually in the editor/runtime. +1. **Performance Tuning**: Monitor `ConvolverNode` overhead in high-density scenes. +2. **SFX Library**: Start populating the `/public/sounds/` directory with production assets. +3. **Gameplay Mechanics**: Integrate sound triggers into common object prefabs (Doors, Switches). diff --git a/.agent/rules/kairo.md b/.agent/rules/kairo.md new file mode 100644 index 0000000..5b6bca7 --- /dev/null +++ b/.agent/rules/kairo.md @@ -0,0 +1,41 @@ +--- +trigger: always_on +--- + +## Kairo TaskOps Skill + +Use Kairo CLI for task tracking and status reconciliation in the Scanline Engine project. + +### Core Workflow + +1. **Check Context**: At the start of a session, run `kairo api list --proj quest` to understand the current backlog and active tasks. +2. **Reconcile**: If the user's request matches an existing task, update its status to `doing` using `kairo api update --id --status doing`. +3. **Track Progress**: For tasks that span multiple turns, keep the description updated with key findings. +4. **Wrap-up**: When a task is finished, mark it as `done` and store any durable knowledge (decisions, runbooks, incidents) in `agent_memory`. + +### CLI Command Reference + +- **List Project Tasks**: + `kairo api list --proj quest` (filters tasks carrying the `proj:quest` tag) +- **Create New Task**: + `kairo api create --title "[Quest] " --description "Owner: \nContext:
\nExpected outcome: " --tag "proj:quest" --tag "owner:codex" --tag "type:" --priority <0-3>` +- **Update Task Status**: + `kairo api update --id --status ` +- **Mark as Done**: + `kairo api update --id --status done` + +### Standard Tagging Taxonomy + +- `proj:quest`: Mandatory for all project-related tasks. +- `owner:`: Who is responsible for the task. +- `type:`: Category of work. +- `area:`: Specific subsystem. +- `source:`: Where the task originated. +- `status-meta:`: Additional status metadata. +- `session:`: The session date when the task was created or updated. + +### Integration with agent_memory + +- Kairo is for **tasks** (the "what" and "status"). +- `agent_memory` is for **durable knowledge** (the "how", "why", and "what was learned"). +- Always cross-reference: mention the Kairo task ID in memory entries and commit messages when relevant. diff --git a/.agent/rules/wrap-up.md b/.agent/rules/wrap-up.md new file mode 100644 index 0000000..ff91087 --- /dev/null +++ b/.agent/rules/wrap-up.md @@ -0,0 +1,24 @@ +--- +trigger: always_on +--- + +## Wrap-up Protocol + +When the user requests a `wrap-up` or `wrap-up-session`: + +1. **Summarize the Session**: Create a comprehensive summary of the current session, including: + - **Session Goal**: What was the primary objective? + - **What Was Implemented**: Detailed list of features, fixes, and changes. + - **Architecture/Runtime Decisions**: Any key decisions made and their rationale. + - **Parser/Mechanics/Scene Changes**: Specific updates to core subsystems. + - **Tests Run**: Outcomes of `npm test`, `npx vitest`, or specific manual checks. + - **Commits Created**: List hashes and messages of commits made during the session. + - **Current State**: Summary of the project status at the end of the session. + - **Remaining Work / Next Steps**: Actionable tasks for the next session. + - **Risks / Caveats**: Any known issues, technical debt, or open questions. +2. **Update `Sessions.md`**: Append the summary to the end of `Sessions.md` in the project root, using the established `## Session Entry - YYYY-MM-DD HH:mm [Timezone]` format. +3. **Persist Knowledge**: + - Store high-value, durable knowledge in `agent_memory` (facts, decisions, runbooks). + - Update `.agent/context.md` and `.agent/current_task.md` to reflect the new state. +4. **Kairo Cleanup**: Close completed tasks in Kairo and create follow-up tasks for the `Remaining Work`. +5. **Final Report**: Provide the user with a brief confirmation that the wrap-up is complete and point them to the updated `Sessions.md`. diff --git a/AGENTS.md b/AGENTS.md index 9a59094..1723bcc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,83 +1,256 @@ -# Project Instructions +# Scanline Engine Agent Protocol -- To get answers to previous session logs and project documentation, you can use your NotebookLM skill (Scanline Engine's notebook, URL: `https://notebooklm.google.com/notebook/9f146be7-7c4a-4bb0-b7b4-7f20079e85b0`). You can ask NotebookLM questions, and it will answer based on the project's entire knowledge base. -- Always consult NotebookLM for architecture/codebase recall first (if possible), instead of having to re-analyze the codebase each time. This saves tokens and allows us to do more. +## Mission -- If NotebookLM is not available, use local RAG as fallback. +Be a senior, autonomous engineering collaborator for Scanline Engine. Use project memory and knowledge synthesis before broad code scans, keep task state explicit, verify behavior in the repository and tests, and preserve durable knowledge for future agents. -- You ALWAYS record all important points, decisions, and insights you, and other developers, might need in the future in your memory (agent-memory-MCP) +## Startup Protocol -- Before implementing anything, check the contents of your memory for all related information. +At the start of a new session, before resuming nontrivial work, or before architecture-sensitive troubleshooting: -## Knowledge Recall Model +1. Read the user's newest request and classify it: quick answer, implementation, architecture-sensitive work, troubleshooting, continuation, review, or planning. +2. Recall `agent_memory` for relevant fresh facts, decisions, incidents, commits, runbooks, and known caveats. +3. If the task needs whole-project context, complete the NotebookLM readiness flow and ask for a structured subsystem overview before scanning large code areas. +4. Use `local_rag` as fallback or sidecar for fuzzy related-document discovery and indexed docs. +5. Check Kairo for active/high-priority `proj:quest` tasks when starting a new session, resuming work, or when the request may already have a follow-up. Do not check Kairo for tiny one-shot questions unless task status may matter. +6. Verify conclusions against the current repository before editing. +7. For substantial work, keep Kairo updated while working and store durable conclusions in `agent_memory` after validation. + +## Responsibility Model + +- Codex owns architecture judgment, risk assessment, code review, test selection, final integration, and user-facing recommendations. +- NotebookLM and `local_rag` provide recall and synthesis, not final truth. +- Gemini may perform bounded technical work only under explicit scope. +- The repository and tests are the current source of truth. + +## Knowledge Sources Use the project knowledge sources in this order, depending on the question: -1. `agent_memory` is the primary durable memory layer for precise facts, decisions, runbooks, incidents, commit context, and fresh conclusions from prior work. -2. NotebookLM is the broad architecture/documentation synthesis layer. Use it when a question needs whole-project context, but only after the NotebookLM readiness flow below succeeds. -3. `local_rag` is the local fallback/sidecar for fuzzy recall: semantic search, related-document discovery, and cases where the exact memory title, file name, or subsystem name is unknown. -4. The repository itself is the source of truth for current code. Use `rg`, file reads, and tests to verify behavior before editing. +1. `agent_memory`: primary durable memory for precise facts, decisions, runbooks, incidents, commit context, fresh conclusions, and known failures. +2. NotebookLM: broad architecture/documentation/session synthesis, after the CLI readiness flow succeeds. +3. `local_rag`: local fallback/sidecar for semantic search, related-document discovery, indexed memory exports, and mirrored project docs. +4. Repository: source of truth for current code and behavior. Use `rg`, file reads, and tests before editing. -## Gemini CLI Worker Rule +The Scanline Engine NotebookLM notebook is: -When Gemini CLI is installed, use it as an external helper for technical tasks wherever this is practical and safe. This is intended to increase throughput and reduce Codex token use. +`https://notebooklm.google.com/notebook/9f146be7-7c4a-4bb0-b7b4-7f20079e85b0` -- Prefer the `gemini-cli-agent` skill for this workflow. -- Use Gemini for bounded implementation chores, mechanical edits, small test-writing tasks, focused bug fixes, and independent read-only reviews. -- Run multiple Gemini CLI processes in parallel when tasks are independent and have disjoint file ownership. -- Codex remains responsible for project memory/NotebookLM/RAG recall, architecture decisions, prompt scoping, diff review, test selection, and final integration. -- Give Gemini strict prompts with allowed write scope, forbidden files, allowed commands, validation expectations, and an instruction to stop if the task exceeds scope. -- Do not delegate broad architecture/design decisions, project-knowledge recall, GDD interpretation, or open-ended refactors to Gemini. -- After Gemini edits, inspect `git status`/`git diff`, reject out-of-scope changes, and run relevant tests before considering the work complete. +## NotebookLM Architecture Recall -`local_rag` does not query live `agent_memory` directly. It indexes a file mirror: +NotebookLM is not just passive documentation search. Treat it as a free, high-context analysis assistant over the full project knowledge base: project documentation, exported session history (`Sessions.md`), memory exports, GDD/autotest docs, and other curated sources in the Scanline Engine notebook. -- exported durable memory docs under `docs/memory`; -- mirrored Quest root documentation under `docs/projects/Quest`. +Before architecture-sensitive, subsystem-level, gameplay/runtime, parser, scene, or troubleshooting-heavy work: -Important `local_rag` caveats: +1. Complete the NotebookLM CLI readiness flow. +2. Ask NotebookLM for a structured brief in the shape you need. +3. Use `agent_memory` after NotebookLM for precise fresh facts, incidents, commit hashes, decisions, and runbooks. +4. Use `local_rag` if NotebookLM is unavailable, noisy, or you need fuzzy related-document discovery. +5. Verify all conclusions against the current repository before editing. -- For `mcp__local_rag__summarize_project_context`, use `context: "Quest"`, not the full Windows path like `D:\GAMES\New folder\Quest`. Earlier misses happened because indexed memory docs use the `Quest` context label. -- Use `mcp__local_rag__semantic_search` when you are unsure what to ask for; it searches across indexed memory and project documentation. -- Use `mcp__local_rag__repo_list` with `path: "docs/projects/Quest"` to confirm the project documentation mirror is visible. -- Fresh `agent_memory` entries may not appear in `local_rag` until the memory export/mirror and RAG index are refreshed. For fresh facts, query `agent_memory` directly first. -- Project documentation is mirrored into `local_rag` by the local startup script at `C:\Users\Professional\.codex\tools\agent-memory-mcp\start-local-rag.ps1`; the mirrored files live at `C:\Users\Professional\.codex\tools\agent-memory-mcp\local-rag-data\docs\projects\Quest`. +Preferred subsystem overview prompt: + +```text +For Scanline Engine, give a subsystem overview for ``. + +Return: +- Current contract and intended behavior +- Key runtime/editor/parser files +- Relevant tests/autotests and how to run them +- Recent decisions, incidents, sessions, and commits +- Known caveats, regressions, or gotchas +- Recommended implementation checklist + +Do not give a generic explanation. Produce an engineering brief for implementation. +Keep it concise, actionable, and prefer exact file paths when known. +If evidence is uncertain, mark it as uncertain and say what repo files should verify it. +``` + +Bug analysis prompt: + +```text +For Scanline Engine, analyze ``. + +Return: +- Most likely subsystems involved +- Known similar incidents or prior fixes +- Files and functions to inspect first +- Tests that should reproduce or protect this behavior +- Risks and rollback considerations +``` + +Implementation brief prompt: + +```text +For Scanline Engine, prepare an implementation brief for ``. -## NotebookLM CLI Connectivity Rule +Return: +- Existing architecture to reuse +- Contract changes needed +- Minimal file/test plan +- Documentation/GDD updates needed +- Open questions for the user +``` + +NotebookLM usage rules: + +- Prefer specific output shapes over broad "summarize this" prompts. +- Use NotebookLM to synthesize prior knowledge before spending Codex context on large code or docs. +- Ask about `Sessions.md` explicitly when chronology, previous chat context, or "why was this done?" matters. +- NotebookLM answers are guidance; validate file paths, contracts, and behavior in the repo. + +## NotebookLM CLI Readiness + +*(Note: Substitute `` with your actual local checkout location.)* Use NotebookLM through the CLI first. Do not start with the NotebookLM MCP for normal project recall on this machine. Required readiness flow: 1. Run `python -m notebooklm auth check --json` only as a storage/cookie diagnostic. -2. Run `python -m notebooklm list --json`. This is the real auth check. -3. Run a project notebook smoke test: - - `python -m notebooklm ask "ping: reply with one short sentence confirming access" --notebook 9f146be7-7c4a-4bb0-b7b4-7f20079e85b0 --json` +2. Run `python -m notebooklm list --json`; this is the real auth check. +3. Run the project notebook smoke test: + `python -m notebooklm ask "ping: reply with one short sentence confirming access" --notebook 9f146be7-7c4a-4bb0-b7b4-7f20079e85b0 --json` 4. If `list` and the smoke-test `ask` work, reuse the current CLI auth and do not re-authorize. 5. If a real CLI command returns `Authentication expired or invalid` or redirects to Google sign-in, organize CLI re-auth with the user: - - start `python -m notebooklm login` in a visible terminal, preferably via: - `Start-Process powershell -ArgumentList @('-NoExit','-Command','Set-Location -LiteralPath "D:\GAMES\New folder\Quest"; python -m notebooklm login')` - - ask the user to complete Google login in the opened browser, wait for the NotebookLM homepage, then press Enter in that terminal; - - re-run `list` and the project smoke-test `ask`. + `Start-Process powershell -ArgumentList @('-NoExit','-Command','Set-Location -LiteralPath ""; python -m notebooklm login')` + Ask the user to complete Google login in the opened browser, wait for the NotebookLM homepage, then press Enter in that terminal. Re-run `list` and the project smoke test. Important caveats: - `auth check` can report `status: ok` while the server-side session is expired or revoked. Trust `list`/`ask`, not `auth check` alone. - Use explicit notebook IDs (`--notebook 9f146be7-7c4a-4bb0-b7b4-7f20079e85b0`) instead of `notebooklm use`, so separate agent sessions do not overwrite shared CLI context. -- MCP may still fail with `browserType.launchPersistentContext: Target page, context or browser has been closed`; treat that as an MCP/Chrome profile launch issue, not as a NotebookLM auth issue. Only troubleshoot MCP if the user explicitly asks for MCP repair. +- MCP may fail with `browserType.launchPersistentContext: Target page, context or browser has been closed`; treat that as an MCP/Chrome profile launch issue, not NotebookLM auth. Only troubleshoot MCP if the user asks. - If CLI auth is repaired and MCP state must be refreshed later, back up and copy `C:\Users\Professional\.notebooklm\storage_state.json` to `C:\Users\Professional\AppData\Local\notebooklm-mcp\Data\browser_state\state.json`. This does not fix MCP browser launch failures. +## Local RAG + +`local_rag` does not query live `agent_memory` directly. It indexes a file mirror: + +- exported durable memory docs under `docs/memory`; +- mirrored Quest root documentation under `docs/projects/Quest`. + +Rules and caveats: + +- Use `context: "Quest"` for `mcp__local_rag__summarize_project_context`, not the full Windows path. +- Use `mcp__local_rag__semantic_search` when you are unsure what to ask for. +- Use `mcp__local_rag__repo_list` with `path: "docs/projects/Quest"` to confirm the project documentation mirror is visible. +- Fresh `agent_memory` entries may not appear in `local_rag` until the memory export/mirror and RAG index are refreshed. Query `agent_memory` directly for fresh facts. +- Project documentation is mirrored by `C:\Users\Professional\.codex\tools\agent-memory-mcp\start-local-rag.ps1`. +- Mirrored project docs live at `C:\Users\Professional\.codex\tools\agent-memory-mcp\local-rag-data\docs\projects\Quest`. + +## Memory Policy + +Always record important points, decisions, and insights that future developers or agents may need. Store durable knowledge, not transient chatter. + +Store memory when: + +- a runtime/parser/gameplay/editor contract changes; +- a bug root cause or durable workaround is found; +- a repeatable workflow/runbook is discovered; +- a commit has lasting architectural or operational value; +- a test failure is known, reproduced, and tracked; +- a user accepts or rejects important behavior. + +Do not store: + +- raw logs without conclusions; +- temporary guesses that did not produce durable lessons; +- facts obvious from nearby code unless they connect to decisions, caveats, or tests. + +Use the right type: + +- `working`: short-lived current-task context. +- `episodic`: important events, chronology, commits, validations, incidents. +- `semantic`: stable facts about architecture, contracts, configuration, files, and environment. +- `procedural`: repeatable workflows, setup, troubleshooting, validation steps. +- `store_decision`, `store_runbook`, `store_incident`: use these structured APIs when the record fits. + +After major work, clean up or supersede stale temporary context when useful. + +## Kairo TaskOps + +Use Kairo as the shared task/action layer for work with an owner, status, and next step. `agent_memory` remains the durable knowledge layer. + +At session start or before substantial work: + +- Check Kairo for active/high-priority `proj:quest` tasks, especially `doing`, delegated, blocked, needs-acceptance, waiting, and high-priority tasks. +- Reconcile the user's current request with existing Kairo tasks before creating duplicates. +- If resuming work, update the relevant task instead of creating a new one. + +During and after work: + +- Create or update Kairo tasks for work that continues beyond the current response, needs user acceptance/manual action, is delegated, or comes from review/test follow-up. +- Keep active work in `doing`, delegated work clearly tagged, and blocked work marked with status metadata. +- Close your own completed tasks yourself: after validation or required acceptance, set them to `done`. +- When a task is completed, store durable conclusions in `agent_memory`. +- Do not create Kairo tasks for trivial internal steps, raw notes, architecture facts, or temporary debugging thoughts. + +Kairo conventions: + +- Sync repo: private GitHub repo `z-hunter/kairo-tasks-sync` (`git@github.com:z-hunter/kairo-tasks-sync.git`), local path `C:\Users\Professional\AppData\Roaming\kairo\tasks-sync`. +- Use `proj:quest`. +- Prefer tags: `owner:`, `type:`, `area:`, `source:`, `status-meta:`, `session:`. +- Priority: `0` blocker/urgent user action/regression risk, `1` important current-session work, `2` normal follow-up, `3` low-priority cleanup or someday. +- Title: start with `[Quest]`, use an action phrase, and mention owner only when delegated or user-facing. +- Description: include owner, context, expected outcome, acceptance criteria, relevant files/links, and source when useful. + +## Gemini CLI Worker Rule + +When Gemini CLI is installed, use it as an external helper for bounded technical tasks where practical and safe. Prefer the `gemini-cli-agent` skill. + +Good Gemini tasks: + +- bounded implementation chores; +- mechanical edits; +- small test-writing tasks; +- focused bug fixes; +- independent read-only reviews; +- multiple independent tasks with disjoint file ownership. + +Do not delegate: + +- architecture or product decisions; +- project-knowledge recall, GDD interpretation, or broad design; +- open-ended refactors; +- final integration, test selection, or user-facing recommendations. + +Gemini rules: + +- Local Gemini has access to `agent_memory` and Kairo. When relevant, create `owner:gemini` tasks and tell Gemini exactly which memory/task context to consult or update. +- Codex remains responsible for prompt scope, architecture judgment, diff review, test selection, and final integration. +- Give Gemini strict prompts with allowed write scope, forbidden files, allowed commands, validation expectations, and an instruction to stop if scope is exceeded. +- After Gemini edits, inspect `git status`/`git diff`, reject out-of-scope changes, and run relevant tests. + +## Implementation Discipline + +- Prefer existing repo patterns, helpers, and architecture over new abstractions. +- Keep edits scoped to the requested behavior and related contracts. +- Update `GDD.md` if gameplay/design behavior changes. +- Use structured APIs/parsers where available instead of ad hoc string manipulation. +- For runtime/scene/gameplay bugs, prefer diagnostic helpers or temporary probes that explain engine decisions, such as why `isWalkable` returned false, which object blocked a path, or which semantic rule selected a parser target. +- Do not revert user changes. Work with dirty files unless the user explicitly asks to revert them. + +## Validation Ladder + +Use the narrowest meaningful checks first, then broaden based on risk: + +1. Focused tests for touched files or newly added behavior. +2. Adjacent subsystem tests. +3. Integration/parser/autotests when contracts cross runtime/parser/scene/system boundaries. +4. `npm run typecheck` for TypeScript changes. +5. Full `npm test` when risk is broad or before major commits. + +If the full suite fails on an unrelated existing issue, reproduce the failing test if useful, create/update a Kairo follow-up, and report it clearly. + ## Project Standards -- **What is?**: A 2.5D retro-style adventure game engine, "Scanline Engine" (previously "Quest"), with AI-powered text parser. -- **Tech Stack**: React, Vite, Vanilla CSS. -- **Files**: - - Use `src/` for source code. - - `GDD.md` is the source of truth for game design. -- **Workflow**: - - Consult NotebookLM/RAG/GDD (in order of priority) before implementing gameplay features. - - Update `GDD.md` if design decisions change during implementation. - - Once the new functionality has been tested and accepted by the user, commit to memory all the most useful facts obtained during implementation that may be useful to developers in the future. +- Scanline Engine is a 2.5D retro-style adventure game engine with an AI-powered text parser. +- Tech stack: React, Vite, Vanilla CSS. +- Use `src/` for source code. +- `GDD.md` is the source of truth for game design. +- Consult NotebookLM/RAG/GDD before implementing gameplay features. +- Once new functionality is tested and accepted, store useful implementation facts in `agent_memory`. ## Autotests Recall Rule @@ -95,23 +268,16 @@ and especially on: - spatial hierarchy; - subscene behavior; -recall that this project has an autotest system on branch `autotests`. - -Before proceeding with substantial changes in those areas: +remember that this project has an autotest system on branch `autotests`. -- remember that autotests may already cover the contract you are touching; -- consult memory for the current autotest workflow and coverage; -- use `Autotests.md` for the current developer-facing description of: - - when to run autotests; - - how to run them; - - what is currently covered; - - how fixtures and test harnesses are structured. +Before substantial changes in those areas: -## Autotests Maintenance Rule +- consult memory for current autotest workflow and coverage; +- use `Autotests.md` for when/how to run autotests, current coverage, fixtures, and harness structure; +- check whether existing autotests already cover the contract you are touching. -When making significant functional changes or adding important new behavior in mechanics/runtime code: +When making significant functional changes: -- check whether existing autotests still describe the intended behavior; - update affected tests if the contract changed; -- add new tests when a new important gameplay/runtime/parser contract is introduced; -- update `Autotests.md` if the test system, fixtures, or coverage model changes in a meaningful way. +- add tests for new important gameplay/runtime/parser contracts; +- update `Autotests.md` if the test system, fixtures, or coverage model changes meaningfully. diff --git a/Commands.md b/Commands.md index 91ee36c..483c75d 100644 --- a/Commands.md +++ b/Commands.md @@ -101,6 +101,8 @@ For `PUT`, target resolution has priority: - unknown destinations such as `recirder` fail as target-not-found; - source items already stored in the selected destination are filtered out before building the batch. - relation targets such as `PUT cassette UNDER chair` resolve only to an existing `Inventory`/`Surface` slot for that relation; parser/runtime checks must not auto-create missing containers. +- `floor`/`ground` can resolve to a Walkbox pseudo-floor target for `PUT`/`DROP`, including `PUT item ON FLOOR` and `PUT item IN FLOOR`. +- For `LOOK floor` / `EXAMINE floor`, the parser first tries the current Walkbox pseudo-floor under the player, but only if it has the needed text field (`description` for `LOOK`, `details` for `EXAMINE`). Otherwise it falls back to a real visible/held object titled or synonymed `Floor`, then to `parser.look_default_object`. - for untitled technical storage nodes, the relation to the player-facing target is the first spatial relation from the nearest titled parent to that technical chain. A Surface inside an untitled `UNDER` child of `Chair` is therefore treated as `UNDER chair`, even if the Surface's internal placement relation is `ON`. - visible but currently unusable source items are reserved for diagnostics, not clarification. For example, a far cassette can produce a distance-specific failure, but it must not be offered as a selectable source option when a usable cassette is available. diff --git a/GDD.md b/GDD.md index d8b03ec..464cda1 100644 --- a/GDD.md +++ b/GDD.md @@ -48,7 +48,11 @@ - `#CLS` : очищает буфер консоли; - `#PEEK-ON` : включает режим отладки parser-mediator, при котором после каждой игровой команды в консоль выводятся `Context JSON`, `Action JSON` и `Result JSON`; - `#PEEK-OFF` : отключает этот режим. -- `#VALIDATE-SPATIAL` : запускает проверку текущей сцены через `SceneSpatialValidator` и выводит в консоль список spatial/container ошибок и предупреждений. Проверяются, в частности, циклы и битые spatial-ссылки, конфликтующие контейнеры с одинаковым relation, некорректные storage-ссылки `Inventory`/`Surface`, `hidden` у безымянных объектов и main inventory персонажа. +- `#PEEKLLM-ON` : включает режим отладки LLM-каскада, при котором после LLM-вызова в консоль выводятся полный prompt (`system` + `messages`) и полный raw response/error; +- `#PEEKLLM-OFF` : отключает этот режим. +- `#PEEKPN-ON` : включает узкий режим отладки Parser Notes, при котором после каждой команды выводятся только PN из полученного parser context и PN-создания/редактирования/очистки; +- `#PEEKPN-OFF` : отключает этот режим. +- `#VALIDATE-SPATIAL` : запускает проверку текущей сцены через `SceneSpatialValidator` и выводит в консоль список spatial/container ошибок и предупреждений. Проверяются, в частности, циклы и битые spatial-ссылки, конфликтующие контейнеры с одинаковым relation, некорректные storage-ссылки `Inventory`/`Surface`, `hidden` у безымянных объектов и main inventory персонажа. Эти детали выводятся в консоль браузера. В режиме редактора `SceneSpatialValidator` также автоматически запускается при загрузке и сохранении сцены. Эта проверка не блокирует работу и не исправляет сцену автоматически: если найдены проблемы, редактор показывает краткое уведомление, а подробный список issues выводится в browser console. Полный ручной отчёт можно получить командой `#VALIDATE-SPATIAL`. @@ -70,7 +74,7 @@ Parser обрабатывает пользовательский ввод кас **Каскад 2**: Средняя (или малая) языковая модель (LM), работающая локально или через API. (очень медленно и "дорого"). -Текущая техническая реализация Каскада 2 уже подключена как opt-in слой: `#LLM-ON` включает LLM cascade, `#LLM-OFF` выключает его. Временный provider — Claude Haiku через Anthropic Messages API и dev-server proxy `/api/llm`; в будущем этот provider должен быть заменяем на локальную малую LM без переписывания parser-а. +Текущая техническая реализация Каскада 2 уже подключена как opt-in слой: `#LLM-ON` включает LLM cascade, `#LLM-OFF` выключает его. Временный provider — Claude Haiku через Anthropic Messages API и dev-server proxy `/api/llm`; в будущем этот provider должен быть заменяем на локальную малую LM без переписывания parser-а. Prompt Каскада 2 разделён на provider-agnostic scene-static prefix и per-call dynamic suffix; Anthropic cache-control является деталью текущего provider-коннектора, а не parser mechanics. Каскад 1 обрабатывает почти весь нормальный пользовательский ввод, а LLM/SLM подключается только если ввод не распознан нижними каскадами, если стандартная команда дошла до `Game API` и получила `status = escalate`, либо если специально включён тестовый режим `#C1-OFF`. LLM может вернуть безопасный DSL-plan, который исполняет `Parser Core`, или короткий player-facing Game Master текст. В текущем v1 это не полноценный бесконечный agent loop: после post-API escalation допускается один дополнительный LLM-вызов, а затем parser завершает обработку обычным путём. @@ -247,6 +251,8 @@ Parser обрабатывает пользовательский ввод кас - _Actor_: объект, который помимо свойств Static имеет направление, в котором он повёрнут и, опционально, спрайты/анимации состояний (idle, walk, talk, etc), причём для каждого направления свой набор. Обычно Actor это NPC и анимированные объекты. Персонаж игрока также является разновидностью Actor. + В редакторе Actor может быть создан напрямую через меню/горячую клавишу, либо получен из обычного Static/Entity добавлением компонента `Actor` в панели Components. В текущей архитектуре этот компонент является authoring-маркером: при добавлении редактор превращает объект в Actor, а при удалении превращает его обратно в Static. + - _Quad_ : четырёхугольный объект, каждая вершина которого обладает отдельным параллаксом. Используется для создания псевдо 3d поверхностей и эффектов типа лучей света и теней. ### ID @@ -448,6 +454,19 @@ _Sort Mode_ (v0, v1, v2, v3, ignore) Кроме простых свойств, которые есть всегда, объекты сцены способны содержать _компоненты_ (структуры данных), которые могут быть добавлены и удалены в редакторе. Каждый объект может иметь один или несколько компонентов разных типов. Но не любой объект может содержать любой компонент. +#### Компонент Actor + +Есть у Actor и может быть добавлен к Static/Entity. + +`Actor` -- особый компонент-маркер редактора. В отличие от обычных компонентов, он меняет тип объекта: + +- если добавить `Actor` к Static/Entity, объект становится Actor и получает Actor-only свойства: направление, скорость движения, режим Player, визуальные состояния и группы анимаций; +- если удалить `Actor` у Actor, объект становится Static/Entity и теряет Actor-only свойства, визуальные состояния, группы анимаций и Actor-only компоненты вроде `Shadow`; +- перед удалением компонента `Actor` редактор показывает подтверждение с кнопками `Cancel` и `Proceed`, поскольку операция необратимо очищает Actor-настройки объекта; +- Static и Actor по-прежнему можно создавать напрямую из меню или горячими клавишами. Разница в том, что созданный напрямую Actor уже имеет компонент-маркер `Actor`. + +Важно: на уровне текущей runtime-архитектуры Actor всё ещё является расширением Entity (`Actor extends Entity`). Компонент `Actor` описывает поведение редактора и сериализации, а не означает, что вся Actor-логика уже перенесена в чистую компонентную систему. + #### Компоненты групп анимаций Есть только у Actor. @@ -580,7 +599,9 @@ Parser-команды `OPEN` и `CLOSE` используют тот же runtime - groups: список допустимых group id. Если пуст, поверхность принимает любые предметы; если задан, принимает только предметы хотя бы из одной подходящей группы; - items: список предметов на поверхности вместе с их runtime-координатами размещения. - Поверхности могут быть как у обычных объектов сцены, так и у объектов крупного плана (`Subscene`). Для `WalkBox` поверхность трактуется как пол, и предметы могут размещаться в пределах его доступной области. + Поверхности могут быть как у обычных объектов сцены, так и у объектов крупного плана (`Subscene`). Для `WalkBox` поверхность трактуется как pseudo-floor/pseudo-ground target: на неё можно класть предметы через `DROP`, `PUT ... ON FLOOR` и `PUT ... IN FLOOR`, а сообщения нормализуются к player-facing `floor`. + + Команды `LOOK FLOOR` и `EXAMINE FLOOR` сначала смотрят pseudo-floor, на котором стоит player. `LOOK` использует `description` текущего `WalkBox`, а `EXAMINE` использует его `details`. Если текущего pseudo-floor нет или у него нет нужного поля, parser ищет обычный titled-объект с Title/Synonym `Floor`. Если и такого объекта нет, возвращается стандартное сообщение `parser.look_default_object` для floor. Безымянная Surface считается техническим storage-узлом. В текстовых сообщениях `PUT`/`DROP` и relation-look важна не связь предмета с самой Surface, а первое spatial-отношение от ближайшего titled semantic parent к этой Surface. Например, если Surface лежит `in` верхнем ящике, а предмет технически размещён `on` Surface, игроку сообщается, что предмет помещён **into the upper drawer**. Если Surface лежит `under` стулом, предмет описывается как находящийся **under the Chair**. @@ -633,6 +654,14 @@ Static и Actor могут содержать скриптовые событи Системный prompt текущего LLM-каскада хранится в `public/text/system/parser-llm-system.md`. +LLM-каскад поддерживает Parser Notes (PN): runtime-only приватные заметки ведущего-парсера для текущей сцены или отдельных объектов. PN нужны для мелких фактов, придуманных при обработке неподдержанных, но правдоподобных действий игрока, чтобы следующие ответы оставались консистентными. Например, если при попытке послушать радио парсер решил, что в эфире сейчас только статика, он может записать это как PN объекта и учитывать при следующей команде. PN не являются текстовыми ассетами, не сохраняются в scene JSON и не показываются игроку напрямую; при `#PEEK-ON` debug-лог показывает создание, обновление, очистку и stale-пометку PN, а `#PEEKPN-ON` выводит только PN context и PN mutations с operation, targetType, id, полным текстом note и `needsCheck`, если заметка требует перепроверки. + +Если обычная runtime-операция реально затрагивает объект с PN или его содержимое (`TAKE`, `PUT`, `OPEN`, `CLOSE` и т.п.), движок не редактирует текст PN сам, а помечает её `parserNoteNeedsCheck: true`. LLM должна сверить такую заметку с текущей моделью мира и заменить или очистить её, если она устарела. + +LLM-контекст сцены также содержит короткую runtime-only мини-историю текущего визита: до 8 последних команд игрока и player-facing ответов парсера (`context.scene.recentTurns`). Ответы урезаются до 85 символов. Эта история нужна для локальной conversational continuity, не сохраняется в файлы сцен и очищается при новом входе в сцену после ухода. + +Промпты LLM должны оставаться на английском языке и использовать story-neutral формулировки вроде `player character`, а не имена конкретных героев. + Текстовые ассеты хранятся в Public\text\ в виде json файлов с именами, совпадающими с id сцен и объектов. - `public\text\scenes\.json` @@ -647,7 +676,10 @@ Static и Actor могут содержать скриптовые событи Текстовый asset содержит стандартные (используемые движком) поля, а также может содержать дополнительные (кастомные). -> сейчас стандартными текстовыми полями считаются `title` и `description`. +Для object TA стандартными player-facing полями считаются `title`, `description`, `details` и `takeFailure`. +`takeFailure` используется стандартным `TAKE`, когда объект распознан, но стандартная runtime-логика определила, что его нельзя взять и иначе вернула бы generic `parser.take_cannot`. Если поле задано непустой строкой, parser выводит этот текст напрямую и не запускает post-API LLM recovery для этого отказа. +Scene/object TA также поддерживают скрытое поле `lore`: оно не показывается игроку напрямую, но добавляется в context Stage 2 LLM cascade для истории места, визуального размещения объектов, описания персонажей и другого фонового знания. +Текстовые поля TA можно задавать строкой или массивом строк; массив строк склеивается через `\n`, а пустые элементы массива становятся пустыми строками в выводе. Кроме текстовых ассетов сцен и объектов, в проекте есть и **служебные TA** для строк самого движка, парсера, UI и скриптов. Они хранятся отдельно, в `public\text\system\`, разбиваются по доменам (`parser.json`, `engine.json`, `scripts.json`, etc) и адресуются по строковым ключам вида `parser.take_prompt` или `engine.click_you_see`. Служебные TA не имеют таблицы переадресации. Это просто словари строк, доступных по ключу. @@ -947,11 +979,14 @@ ScriptRegistry.register('interaction.lamp.use', ({ entity }) => { - `actor.setDirection(dir: 'up' | 'down' | 'left' | 'right')` - `actor.walkTo(x, y)` - `actor.moveTo(x, y)` +- `actor.getMoveResult()` - `actor.stop()` - `actor.setState(state)` - `actor.playAnimSet(id: string)` - `actor.resetAnimSet()` +`actor.moveTo(x, y)` строит маршрут до указанной точки через текущие правила проходимости сцены. Если точка недостижима, метод сразу возвращает результат со статусом `unreachable`. Если маршрут построен, метод возвращает `started`, а Actor начинает идти по точкам маршрута. После прибытия `actor.getMoveResult()` возвращает `arrived`; если во время движения маршрут внезапно оказался заблокирован из-за другого объекта или изменения сцены, Actor останавливается, а `actor.getMoveResult()` возвращает `blocked` с сообщением, что маршрут нужно пересмотреть. + Пример: ```typescript @@ -1103,6 +1138,8 @@ Prefab можно загрузить в текущую сцену из файл Также есть кнопка "Add Anim. Set" для добавления новой (пустой) группы анимаций. Если у Actor вообще нет групп анимаций, первая добавленная автоматически получает id 'idle' а вторая 'walk' (но это пользователь потом может отредактировать) +В разделе Components у Static/Entity доступен компонент `Actor`. Добавление этого компонента превращает выбранный объект в Actor и открывает Actor-свойства. У Actor этот компонент отображается как маркер типа. Удаление компонента `Actor` вызывает поп-ап подтверждения; при выборе `Proceed` объект становится Static и теряет Actor-only настройки, включая визуальные состояния, группы анимаций, направление, скорость движения, режим Player и Actor-only компоненты. + Если текущмй спрайт не задан, то Actor будет выглядеть как Static без спрайта, то есть как прямоугольник с заливкой цветом. Для Actor и Static можно задать размер их прямоугольника (ширина и высота) а также масштаб (умножитель размера). Например, можно уменьшать масштаб предмета, чтобы использовать для него спрайты более высокого разрешения без увеличения размера на экране. diff --git a/Parser.md b/Parser.md index f392d16..03f5f29 100644 --- a/Parser.md +++ b/Parser.md @@ -128,9 +128,11 @@ flowchart TD - `rawInput` и `normalizedInput` как metadata текущего цикла parser-а; - текущую сцену (`id`, `name`, `title`, `description`, `activeSubscene`); +- `context.scene.recentTurns`: до 8 последних player-facing команд/ответов текущего визита в сцену; - список текстово значимых объектов сцены; - отдельный список `knownEntities` для объектов, известных движку, но не раскрытых player-facing текстовому слою; - инвентарь игрока; +- `worldFacts`: краткие авторитетные факты о переноске, containment/location и TA-driven semantic relations; - spatial nodes and relation projection, derived from the runtime scene hierarchy; - `pending state`, если parser уже ждёт уточнение. @@ -153,6 +155,14 @@ flowchart TD - он не определяет target; - он лишь даёт parser-у картину мира. +`worldFacts` deliberately combine generic runtime facts and authored semantic facts: + +- generic: `Player carries ID card.`, `Boombox contains Compact cassette.`, `Compact cassette is inside Boombox.`; +- authored semantic: `Boombox already has Compact cassette loaded.`, `Car has Gasoline in the tank.`; +- semantic facts come from object Text Assets, not from hardcoded parser heuristics. + +For v1, semantic facts only improve LLM context. They do not create real command mechanics or runtime effects. + Scope slices intentionally separate knowledge from actionability: - `visible` means the player-facing text layer may refer to the object; @@ -210,6 +220,7 @@ Parser не должен самостоятельно обходить raw `.spa Примеры: - `LOOK` использует видимые объекты сцены и инвентарь; +- `LOOK floor` / `EXAMINE floor` имеют специальную floor-chain: сначала current walkbox pseudo-floor под player с нужным текстовым полем (`description` для `LOOK`, `details` для `EXAMINE`), затем обычные visible/held targets с настоящим Title/Synonym `Floor`, затем `parser.look_default_object`; - `TAKE` использует только берущиеся объекты сцены; - `EXAMINE` использует инвентарь, объекты активной subscene и объекты в пределах допустимой дистанции; - `GO TO` использует сценовые цели и достижимые сценовые объекты. @@ -370,6 +381,8 @@ flowchart TD - временная модель по умолчанию — `claude-haiku-4-5-20251001`; - ключ берётся только из `process.env.ANTHROPIC_API_KEY` на dev-server side; - prompt хранится в `public/text/system/parser-llm-system.md`; +- prompt собирается как provider-agnostic split: scene-static system prefix с базовыми инструкциями и scene/object/inventory text snapshot, плюс per-call dynamic user suffix с вводом, recentTurns, Parser Notes, worldFacts, spatial model, pending state, focused target и previousAttempt; +- scene-static prefix подготавливается при переключении сцены; Anthropic cache entry создаётся лениво первым реальным LLM-вызовом; - ответ модели нормализуется в существующий `ParserCascadeEnvelope`; - `plan` исполняется обычным `Parser Core`; - `final_response` и `clarification` в v1 конвертируются в `showText`; @@ -483,6 +496,23 @@ Parser сначала проверяет: - `context` - `scope` +В `context` также попадают runtime-only Parser Notes: + +- `context.scene.parserNote` +- `context.scene.parserNoteNeedsCheck` +- `context.entities[].parserNote` +- `context.entities[].parserNoteNeedsCheck` +- `context.knownEntities[].parserNote` +- `context.knownEntities[].parserNoteNeedsCheck` +- `context.inventory[].parserNote` +- `context.inventory[].parserNoteNeedsCheck` + +Parser Notes — это приватная память LLM/GM-каскада, а не player-facing текст. Они используются для мелких придуманных фактов, которые должны оставаться консистентными между командами, например состояние радиоприёма или след от действия игрока на объекте. + +Отдельно `context.scene.recentTurns` даёт LLM короткую историю текущего визита в сцену: последние 8 команд игрока и player-facing ответов парсера. Ответ хранится в укороченном виде до 85 символов. История runtime-only, не сериализуется в scene JSON и очищается при входе в сцену после ухода из неё, чтобы повторный визит начинался без старых turns. + +`parserNoteNeedsCheck: true` означает, что после записи PN объект или сцена были реально затронуты обычной runtime-операцией (`TAKE`, `PUT`, `OPEN`, `CLOSE`, удаление из inventory и т.п.). Parser не удаляет заметку автоматически: он оставляет её в контексте, но просит LLM сверить её с текущими `worldFacts`, `contents`, `location`, `spatialNodes` и `spatialRelations`. Если заметка устарела, LLM должна заменить её или очистить пустой PN-записью. + ### Step 4. Stage 1 runs sequentially - сначала `Stage 1.1`; @@ -652,10 +682,14 @@ Parser: - `title` - `description` - `details` +- `lore` +- `takeFailure` -Но и новое опциональное поле: +Также важны опциональные поля: - `synonyms` +- `semanticTags` +- `relationFacts` Пример: @@ -664,7 +698,8 @@ Parser: "title": "logo", "description": "You see Scanline Engine logo.", "details": "Extended description here.", - "synonyms": ["logotype", "emblem", "scanline symbol"] + "synonyms": ["logotype", "emblem", "scanline symbol"], + "semanticTags": ["signage", "brand_marker"] } ``` @@ -680,6 +715,89 @@ Parser: - используется действием `EXAMINE`; - тоже входит в стандартный шаблон нового object TA. +Поле `lore`: + +- является стандартным скрытым полем scene/object TA для Stage 2 LLM context; +- не выводится игроку действиями `LOOK`, `EXAMINE` или runtime UI; +- используется для истории места, визуальной компоновки сцены, описания персонажей и другого + контекста, который помогает LLM писать ответы в духе того, что игрок видит на экране. + +Текстовые поля object/scene TA можно задавать строкой или массивом строк. Массив +склеивается через `\n`, пустая строка внутри массива даёт пустую строку в выводе: + +```json +{ + "details": ["First paragraph.", "", "Second paragraph."] +} +``` + +Поле `takeFailure`: + +- является стандартным player-facing полем object TA; +- используется стандартным `TAKE`, когда runtime определил, что объект не является + берущимся предметом и иначе вернул бы generic `parser.take_cannot`; +- если задано непустой строкой, выводится напрямую и делает failed outcome + terminal (`recoverable: false`), поэтому post-API LLM recovery не запускается. + +Поля `semanticTags` и `relationFacts`: + +- являются authoring metadata для `ParserWorldModelBuilder`; +- генерируют `worldFacts` для Stage 2 LLM context; +- не являются player-facing текстом; +- не добавляют новые команды или runtime effects сами по себе. + +`semanticTags` задаются на объекте-ребёнке или любом объекте, которому нужен смысловой маркер: + +```json +{ + "title": "Compact cassette", + "synonyms": ["tape", "cassette"], + "semanticTags": ["media", "audio_media", "cassette"] +} +``` + +`relationFacts` задаются на parent/container/device объекте: + +```json +{ + "title": "Boombox", + "relationFacts": [ + { + "relation": "in", + "childTags": ["media", "audio_media"], + "fact": "{self} already has {child} loaded." + } + ] +} +``` + +Если объект с тегом `media` или `audio_media` находится `in` Boombox, в LLM context появится semantic world fact: + +```text +Boombox already has Compact cassette loaded. +``` + +Та же модель должна использоваться для других доменов: + +```json +{ + "title": "Car", + "relationFacts": [ + { + "relation": "in", + "childTags": ["fuel"], + "fact": "{self} has {child} in the tank." + } + ] +} +``` + +Supported placeholders in `fact`: + +- `{self}` - resolved title parent-объекта; +- `{child}` - resolved title child-объекта; +- `{relation}` - matched relation. + Стандартный шаблон нового object TA: ```json @@ -687,7 +805,11 @@ Parser: "title": "Object", "description": "You see nothing special.", "details": "", - "synonyms": [] + "lore": "", + "takeFailure": "", + "synonyms": [], + "semanticTags": [], + "relationFacts": [] } ``` @@ -978,6 +1100,8 @@ type ParserPlannedAction = | { type: 'ensureHeldEntity'; ref: string; noEffectMessage?: string } | { type: 'goToSceneById'; sceneId: string } | { type: 'removeInventoryEntity'; ref: string } + | { type: 'setSceneParserNote'; note: string } + | { type: 'setEntityParserNote'; entityId: string; note: string } | { type: 'showText'; message?: string; @@ -994,6 +1118,10 @@ type ParserPlannedAction = - generic clarification и validation на уровне `Parser Core`; - подстановки resolved entity titles в финальные сообщения через `paramsFromRefs`. +`setSceneParserNote` и `setEntityParserNote` пишут runtime-only Parser Notes. Они не выводят текст игроку, но возвращают `effects`, которые видны при `#PEEK-ON`. `setEntityParserNote` принимает `entityId` из текущего parser context; скрытые known entities не являются допустимой прямой целью, пока объект не видим или не находится в инвентаре. Пустая заметка очищает запись, непустая заменяет её целиком и ограничивается 600 символами. + +Запись PN через эти actions сбрасывает `parserNoteNeedsCheck`, потому что LLM только что переоценила заметку. Обычные действия, реально затрагивающие объект или его содержимое, наоборот помечают существующую PN как `needsCheck`, не меняя её текста. + ### Почему DSL должен быть ограниченным Это важно для безопасности и устойчивости архитектуры. @@ -1052,6 +1180,8 @@ sequenceDiagram - `#PEEK-ON` - `#PEEK-OFF` +- `#PEEKPN-ON` +- `#PEEKPN-OFF` - `#STAGE1-ON` - `#STAGE1-OFF` - `#STAGE2-ON` @@ -1082,6 +1212,27 @@ sequenceDiagram - итоговые outcomes. - raw LLM response, extracted JSON, accepted/filtered actions, provider/model, duration, token count and error reason. +### PEEKPN + +`#PEEKPN-ON` включает узкий debug-режим только для Parser Notes. Он не выводит общий `context/scope/envelope/result` дамп. + +После каждой команды parser выводит: + +- `--- PARSER NOTES CONTEXT ---`, если в полученном parser context уже есть PN; +- `--- PARSER NOTES MUTATIONS ---`, если команда создала, изменила, очистила PN или пометила существующую PN как `needsCheck`. + +Каждая запись содержит полный текст note без compact-обрезки: + +```json +{ + "operation": "context | created | updated | cleared | needsCheck", + "targetType": "scene | entity | inventory | focusedTarget", + "id": "object_or_scene_id", + "note": "full Parser Note text", + "needsCheck": true +} +``` + ### Stage toggles Можно изолированно тестировать разные уровни: @@ -1121,6 +1272,7 @@ Parser должен быть локализуемым без переписыв - `public/text/system/parser-lexicon.json` — stage1 lexicon и normalization vocabulary; - `public/text/system/parser-training.json` — training phrases для NLP-слоя. - `public/text/system/commands/*.json` — custom command assets; +- `public/text/objects/*.json` — object title/description/details/synonyms plus optional semantic tags and relation facts for LLM context; - `Commands.md` — формат и принципы command TA. Текущее применение: @@ -1271,22 +1423,25 @@ already execute against real runtime spatial data. `near` remains parser-recogni - `src/mechanics/LlmCascade.ts` - Stage 2 (`llm-v3`) - - builds the LLM request from `ParserContext`, `ParserScope`, and optional lower-cascade hints + - builds a scene-static prompt prefix and per-call dynamic prompt suffix from `ParserContext` and optional lower-cascade hints - loads and caches `public/text/system/parser-llm-system.md` - calls the configured `ILlmProvider` - extracts fenced or unfenced JSON from model output - normalizes `plan`, `final_response`, and `clarification` into parser envelopes - - records `LlmCascadeDebugInfo` for `#PEEK` + - records `LlmCascadeDebugInfo`, static prompt hash/cache eligibility, and provider cache usage for `#PEEK` / `#PEEKLLM` - `src/mechanics/llm/ILlmProvider.ts` - provider-agnostic LLM interface - - request/response/message types + - request/response/message/content-block types + - optional cache metadata on text blocks; unsupported providers may flatten or ignore it - streaming delta callback type - debug fields shared by current and future providers - `src/mechanics/llm/AnthropicProvider.ts` - temporary Anthropic Claude Haiku provider - posts Anthropic Messages payloads to the local `/api/llm` proxy + - maps provider-agnostic cache metadata to Anthropic `cache_control` + - parses streamed cache creation/read token usage when Anthropic reports it - parses streamed SSE chunks - supports injected `fetchImpl` for deterministic tests diff --git a/ScriptSys.md b/ScriptSys.md new file mode 100644 index 0000000..8effb86 --- /dev/null +++ b/ScriptSys.md @@ -0,0 +1,150 @@ +# Scripting System Guide + +The Scanline Engine supports a hot-reloadable scripting system for gameplay logic and debug tools. This guide covers the basics of creating scripts, attaching them to objects, and using the API. + +## Chapter 1: How to Create Your First Script + +In this tutorial, we will create a simple script that moves an actor when a player steps on a trigger. + +### 1.1 Create the Script File + +Scripts are written in TypeScript and placed in the `src/scripts/` directory. They are automatically loaded at startup. + +Create a new file called `my_first_script.ts` in `src/scripts/`: + +```typescript +import { ScriptRegistry } from '../core/ScriptRegistry'; + +// Register the script with a unique ID ('my_first_script') +ScriptRegistry.register('my_first_script', ({ api }) => { + api.log('Trigger activated! Moving the hero...'); + + // Get the actor named 'hero' + const hero = api.getActor('hero'); + + if (hero) { + // Make the hero walk to a specific coordinate + hero.walkTo(200, 150); + } else { + api.log('Hero not found!'); + } +}); +``` + +### 1.2 Attach the Script in the Editor + +Now that the script is registered, you need to attach it to an object in your scene so it can be executed. + +1. Open the **Scene Editor** (press `F1` in-game). +2. Create or select a **TriggerBox** object in the Hierarchy Panel. +3. In the Properties Panel for the TriggerBox, find the **script** property. +4. Enter `my_first_script` (the exact ID you used in `ScriptRegistry.register`). +5. Save the scene. + +### 1.3 Test the Script + +1. Return to the game view. +2. Walk the hero into the TriggerBox. +3. The console will display *"Trigger activated! Moving the hero..."* and the hero will automatically walk to coordinates (200, 150). + +> **Tip:** You can also test the script manually without a TriggerBox by opening the in-game console (`~`) and typing: `RUN my_first_script` + +--- + +## Chapter 2: API Reference + +The `ScriptContext` provides access to the `api` object. + +| Method | Description | +| :--------------------- | :------------------------------------------------------ | +| `api.log(message)` | Prints a message to the in-game console. | +| `api.wait(ms)` | Async helper that resolves after `ms` game-time milliseconds. Automatically pauses if scene is inactive. | +| `api.makeGlobal()` | Declares the script as global, meaning its timers will continue ticking regardless of the active scene. | +| `api.getQuad(name)` | Returns a `QuadObject` by name, or `null`. | +| `api.getActor(name)` | Returns an `Actor` instance by name, or `null`. | +| `api.getEntity(name)` | Returns a generic `Entity` instance by name, or `null`. | +| `api.saveCheckpoint()` | Saves the current scene state to the Undo History. | + +### 2.1 QuadObject Methods + +| Method | Description | +| :-------------------------------- | :---------------------------------------------------------------------------------------------------------------------- | +| `quad.setVertex(idx, x?, y?, p?)` | Updates vertex properties. Returns `true` if successful, `false` if bound/invalid. Pass `undefined` to skip a property. | + +### 2.2 Entity Properties (Common for all objects) + +| Property | Type | Description | +| :---------------- | :-------- | :---------------------------------------------------------------- | +| `x`, `y` | `number` | World coordinates. Setters automatically update the Scene Editor. | +| `parallax` | `number` | Depth plane factor (1.0 = Default, <1.0 = Far, >1.0 = Near). | +| `width`, `height` | `number` | Visual dimensions (scaled by model scale and depth). | +| `visible` | `boolean` | Toggles rendering. | +| `opacity` | `number` | Alpha value (0.0 to 1.0). | +| `blur` | `number` | Blur filter in pixels. | +| `blendMode` | `string` | Canvas `globalCompositeOperation` (e.g., 'screen', 'multiply'). | +| `color` | `string` | Fill color (used if sprite is missing). | + +**Methods:** + +- `setSprite(filename)`: Changes the object's visual sprite. + +### 2.3 Actor Methods (Player & NPC) + +| Method | Description | +| :------------------ | :----------------------------------------------------------- | +| `walkTo(x, y)` | Moves the actor to target coordinates, respecting walkboxes. | +| `moveTo(x, y)` | Teleports or moves the actor linearly (ignores walkboxes). | +| `stop()` | Stops current movement and sets state to 'idle'. | +| `playAnimSet(id)` | Changes the animation set (e.g., 'dance', 'run'). | +| `setDirection(dir)` | Sets facing direction: 'up', 'down', 'left', 'right'. | +| `setState(state)` | Sets actor state (e.g., 'idle', 'walk'). | + +--- + +--- + +## Chapter 3: Lifecycles, Scopes, and Async Timing + +Scanline Engine handles scripts securely, binding them to the lifecycle of the active scene. + +### 3.1 Pausing and Resuming +By default, scripts are **Scene-Bound**. When a script calls `api.setInterval`, `api.setTimeout`, or `await api.wait(1000)`, those timers are ticked by the engine's `update(deltaTime)` loop. +If the player transitions to a different scene, the engine **pauses** the timers for the inactive scene. When the player returns, the timers **resume** perfectly from where they left off. + +> **Editor Note:** If you manually reload a scene in the Scene Editor, all scripts bound to that scene are permanently stopped to prevent duplication. + +### 3.2 Global Scripts +If you want a script to continue running in the background across all scenes (e.g., a global clock, or ambient system), declare it as global at the start of your script: + +```typescript +ScriptRegistry.register('my_global_clock', ({ api }) => { + api.makeGlobal(); // Now this script ticks regardless of the active scene + + api.setInterval(() => { + // This runs every 1000ms, everywhere + }, 1000); +}); +``` + +### 3.3 Async/Await and Native Promises +You can use `async/await` in your scripts in combination with `api.wait()`. + +```typescript +ScriptRegistry.register('cutscene', async ({ api }) => { + const hero = api.getActor('hero'); + hero.walkTo(100, 100); + await api.wait(2000); // Wait 2 seconds (in game time) + hero.playAnimSet('dance'); +}); +``` + +> [!WARNING] +> **Native Promise Limitations** +> The engine can only pause its own timers (`api.wait`, `api.setInterval`). If you use native browser mechanisms like `fetch()` or `Promise.all()` over external events, those operations will continue running in the background even if the scene is inactive. Always try to build logic using the `api` methods to guarantee proper pause/resume behavior. + +--- + +## Chapter 4: Debugging & Undo + +- **Hot Reload**: Edits to scripts in `src/scripts/` are applied immediately without reloading the page. +- **Granular Undo**: Use `api.saveCheckpoint()` before making changes to allow users to `Undo` script actions step-by-step in the Editor. diff --git a/Sessions.md b/Sessions.md index cf406a3..64b849d 100644 --- a/Sessions.md +++ b/Sessions.md @@ -596,7 +596,7 @@ During the session the following checks were run successfully: ## Session Entry - 2026-04-19 12:13 +02:00 - 1. What was completed +1. What was completed - Decomposition of Game.ts: Successfully refactored the monolith from ~80KB to ~40KB by delegating responsibilities to specialized systems. @@ -609,20 +609,20 @@ During the session the following checks were run successfully: - Autotests & CI Consistency: Updated test fixtures and ensured the entire suite (202 tests) passes, confirming no regressions in parser or runtime logic. - 1. Current state +1. Current state - The core architecture is now modular and more scalable. - The IGame interface is fully updated to reflect the new delegation pattern. - The workspace is clean, and changes are committed to the scene-refact2 branch. - Browser runtime errors (SyntaxErrors due to improper type imports) have been fully resolved and verified. - 1. Next steps +1. Next steps - Feature Sprint: Resume development of gameplay features as defined in GDD.md. - Cleanup: Conduct a final audit of any remaining any casts in ComponentSystem.ts that can now be replaced with AnyComponent. - Tauri Prep: Proceed with the explicit workspace model for the desktop build as outlined in Tauri.md. - 1. Risks & Caveats +1. Risks & Caveats - Import Precision: Developers must use import type when bringing in IGame or GameActionOutcome in new files to avoid Vite build failures. @@ -752,3 +752,1232 @@ During the session the following checks were run successfully: - The local `gemini-cli-agent` skill lives outside the repo under the Codex skills directory; the repo only records the usage rule in `AGENTS.md`. - `npm install` was run to restore missing local `.bin` scripts after a temporary worktree/junction test setup disrupted the local dependency executable links. `package.json` and lockfiles remained unchanged. - NotebookLM source replacement still depends on CLI auth and may require the standard readiness flow if auth has expired. + +## Session Entry - 2026-04-27 01:27 +02:00 + +### Session Goals + +- Continue work on Scanline Engine after verifying NotebookLM access. +- Evaluate and implement the idea that Static/Entity objects can become Actors by adding an Actor component, and Actors can become Static again by removing it. +- Add a confirmation popup for removing the Actor component because that operation discards Actor-only data. +- Commit the completed improvement and verify follow-up review findings against the actual current code. + +### What Was Implemented + +- Verified NotebookLM CLI authorization using the project readiness flow: + - `python -m notebooklm auth check --json` succeeded. + - `python -m notebooklm list --json` succeeded and showed the `Scanline Engine` notebook. + - Targeted notebook smoke test succeeded for notebook `9f146be7-7c4a-4bb0-b7b4-7f20079e85b0`. +- Implemented Actor component conversion: + - Static/Entity objects can add an `Actor` component from the Components section. + - Adding the component replaces the scene object with an `Actor` instance at the same `scene.entities` index. + - Actor objects display an `Actor` marker component in the Components section. + - Removing the `Actor` component replaces the object with a normal `Entity`. + - Actor serialization now emits `{ type: 'Actor' }` in `components` for editor consistency. + - Static/Entity JSON that contains an Actor component marker now loads as an Actor, preserving compatibility with the new authoring model. +- Added a destructive confirmation dialog when removing the Actor component: + - Title: `Remove Actor Component`. + - Buttons: `Cancel` and `Proceed`. + - Proceed warns that the object becomes Static and loses Actor settings, including direction, player mode, move speed, visual states, animation sets, and Actor-only components. +- Added tests for: + - Entity -> Actor conversion preserving common properties and adding the Actor marker. + - Actor -> Entity conversion dropping Actor-only serialized data and removing Actor/Shadow components. + +### Important Architecture / Runtime Decisions + +- Actor component is currently a UI/editor conversion handle, not a full component-first runtime rewrite. +- Runtime continues to use the existing class split where `Actor extends Entity`, and systems that rely on `instanceof Actor`, `entity.type === 'Actor'`, Actor movement methods, player state, direction, and animation sets remain valid. +- Conversion helpers live on `SceneEditor` and perform the undo snapshot internally before mutating the scene: + - `convertEntityToActor()` + - `convertActorToEntity()` +- Review findings asking for additional `saveUndoState()` calls in `SectionComponents.tsx` were checked against current code and intentionally not applied: + - Both conversion helpers already call `this.saveUndoState()` before mutation. + - Adding UI-level undo snapshots would create duplicate undo entries for one conversion action. +- Removing Actor strips Actor-only state and also removes `Shadow`, which remains Actor-only for this slice. + +### Parser / Mechanics / Scene / Inventory Changes + +- No parser, command-resolution, inventory, subscene, or semantic API behavior was intentionally changed. +- Scene loading changed only insofar as Entity/Static JSON carrying the Actor marker is instantiated as `Actor`. +- Existing runtime Actor behavior is preserved rather than moved into a component system. + +### Tests Run and Outcomes + +- `npm run typecheck` + - Passed. +- `npm test` + - Passed. + - 21 test files passed. + - 244 tests passed. +- `npm run build` + - Passed during implementation. + - Vite emitted only existing-style warnings about chunk size and dynamic/static imports of `fileApi`. +- During wrap-up, `npm run typecheck` and `npm test` were re-run and passed again. + +### Commits Created + +- `ec18c3e2f9dc8102ea2f5483caad926328411a2c` - `Add Actor component conversion` + +### Current State + +- Branch: `scene-refact3`. +- Last commit: `ec18c3e Add Actor component conversion`. +- After the commit, additional uncommitted changes are present in the worktree. They were not made as part of the committed Actor conversion wrap-up and were intentionally left untouched: + - `public/scenes/home/room.json` + - `public/scenes/home/room_backup.json` + - `public/scenes/test_room (10).json` + - `public/scenes/test_room.json` + - `public/scenes/test_room1.json` + - `src/components/editor/properties/MultiSelectionProperties.tsx` + - `src/components/editor/properties/PropertiesPanel.tsx` + - `src/components/editor/properties/SectionComponents.tsx` + - `src/components/editor/properties/SectionIdentity.tsx` + - `src/entities/SceneObject.ts` + - `src/systems/GameSemanticAPI.ts` + - `public/text/objects/Sofa.json` + - `src/utils/GroupIds.ts` + - `tests/editor/group-id-normalization.test.ts` +- This wrap-up appends a new entry to `Sessions.md`, which is expected to remain as an additional documentation change unless separately committed. + +### Remaining Work / Next Recommended Steps + +- Manually QA the editor flow in the running app: + - create Static; + - add Actor component; + - verify Actor Properties appear; + - undo/redo the conversion; + - remove Actor component; + - verify Cancel does nothing and Proceed converts to Static; + - verify Actor-only fields and Shadow are removed after Proceed. +- Decide whether `GDD.md` should be updated to describe Actor as an editor-visible component marker while preserving the current runtime class split. +- Review the unrelated dirty files before any future commit so the Actor conversion commit remains isolated from group-id or scene-content work. + +### Risks / Caveats / Open Questions + +- Actor component is not yet a pure runtime component architecture. It is intentionally an authoring/conversion affordance over existing classes. +- If future work moves Actor behavior into a true component system, the current conversion helpers should become a migration bridge rather than the final architecture. +- The confirmation dialog prevents accidental loss, but once the user chooses Proceed, Actor-only settings are removed from the object data by design. + +## Session Entry - 2026-05-05 22:22 +02:00 + +### Session Goals + +- Improve `Actor.moveTo` so Actors can route around obstacles instead of moving only in a direct line. +- Make route outcomes usable by future AI/NPC logic: immediate unreachable result, arrival result, and route-blocked/replan-needed result. +- Ensure click-to-move uses the same route planning as scripted `moveTo`. +- Tune path following until it matches keyboard movement better in narrow passages. +- Improve the agent setup by documenting a stronger NotebookLM/Kairo/memory startup workflow in `AGENTS.md`. +- Preserve durable conclusions in `agent_memory`, Kairo, and this session log. + +### What Was Implemented + +- Added route planning to `src/entities/Actor.ts`. + - `Actor.moveTo(x, y)` now returns an `ActorMoveResult`. + - `Actor.moveToVisual(x, y)` also routes after converting the click/visual target into the Actor's parallax-corrected world target. + - `Actor.getMoveResult()` exposes the latest movement outcome for future AI/NPC polling. + - Movement results include statuses/codes such as `started`, `arrived`, `unreachable`, and `blocked` / `route_blocked`. +- Implemented path planning using `Scene.isWalkable` as the single source of truth for current collision and Walkbox semantics. + - Direct segment sampling is tried first. + - If direct movement is blocked, bounded grid A* builds a waypoint route. + - Route smoothing removes unnecessary intermediate waypoints when a segment is clear. + - Search cap is based on the generated grid area rather than a fixed 4000-iteration cap, which matters for large/complex Walkbox areas. + - Segment-clear checks were tightened to sample at `gridSize / 2` with a minimum step of 2. +- Added route-following axis-slide fallback. + - If a diagonal route step is blocked, Actor tries X-only or Y-only movement before reporting `route_blocked`. + - This mirrors keyboard movement and helps narrow passages where manual control can already pass. + - A zero-displacement axis fallback is not treated as progress, so true blocks still report `blocked`. +- Updated click-to-move tests so Walkbox clicks assert route planning rather than the old `visualTarget` behavior. +- Updated `GDD.md` to document `actor.getMoveResult()` and the new `moveTo` route/outcome contract. +- Added `tests/entities/actor-movement.test.ts`. + - Direct route. + - Route around blocking collider. + - Immediate unreachable destination. + - Dynamic blocker causing `route_blocked`. + - Axis-slide behavior for narrow/diagonal blocked steps. + - Large Walkbox route that would exceed the old fixed search cap. + +### Important Architecture / Runtime Decisions + +- `Scene.isWalkable` remains the single authoritative movement oracle. The path planner does not duplicate collision, Walkbox Add/Subtract/Invert, parallax, or dynamic-scene rules. +- `moveToVisual` must be kept in sync with `moveTo`, because `SceneInteraction.movePlayerToClick` uses `moveToVisual` for mouse click movement. +- AI/NPC movement should poll `actor.getMoveResult()` for `arrived`, `unreachable`, or `route_blocked` rather than inferring from `target`/`state` alone. +- Route movement should preserve keyboard parity in narrow spaces by attempting axis-separated progress before failing. +- NotebookLM should be used as a structured architecture-analysis assistant, not just a broad summarizer. `AGENTS.md` now includes explicit NotebookLM query templates and workflow. +- Kairo is now explicitly part of session startup/resume flow; agents should check active/high-priority `proj:quest` tasks and close their own completed tasks after validation/acceptance. + +### Parser / Mechanics / Scene / Inventory Changes + +- Runtime movement changed in `Actor`. +- Scene interaction changed only through test expectations and the use of routed `moveToVisual`; no parser behavior was intentionally changed. +- `GameSemanticAPI`, inventory, spatial text semantics, and parser command resolution were not intentionally changed. +- Existing `Scene.isWalkable` collision/Walkbox behavior was reused rather than modified. + +### Tests Run and Outcomes + +- `npm test -- tests/entities/actor-movement.test.ts` + - Passed during focused implementation. +- `npm test -- tests/entities/actor-movement.test.ts tests/scene/scene-interaction.test.ts` + - Passed. +- `npm test -- tests/game/navigation-and-spatial.test.ts tests/entities/actor-movement.test.ts tests/scene/scene-interaction.test.ts` + - Passed. + - Final relevant run: 3 files passed, 26 tests passed. +- `npm run typecheck` + - Passed. +- Full `npm test` + - Run during the session and failed on an unrelated existing parser world-model test: + - `tests/parser/world-model-context.test.ts` + - case: `omits scene duplicates whose stable id is already held from takable scope` + - actual issue: `compact_cassette` still appears in takable scope. + - The failure was reproduced with the single parser test file and tracked separately in Kairo. + +### Commits Created + +- `11d990d Add Actor route pathfinding` + - Adds routed `moveTo` / `moveToVisual`, movement result API, pathfinding tests, click-to-move regression coverage, and GDD documentation. +- `fbea8ff AI settings update` + - Adds NotebookLM structured recall workflow to `AGENTS.md`. +- `af53e2f AI settings update` + - Refactors `AGENTS.md` into a more useful operational startup protocol, including Startup Protocol, Responsibility Model, NotebookLM workflow, Kairo lifecycle, memory policy, validation ladder, and autotest rules. + +### Kairo / Memory Updates + +- Kairo task `[Quest] Implement Actor MoveTo route planning` was completed and marked `done`. +- Kairo follow-up created for unrelated parser duplicate held-item failure: + - `aaaaaaabtx4yd3f45sovk3pbwhktwukd` + - `[Quest] Fix duplicate held item leaking into parser takable scope` +- Durable `agent_memory` entries were stored for: + - final MoveTo pathfinding contract and caveats; + - click-to-move using `moveToVisual` route planning; + - large Walkbox search cap behavior; + - narrow passage axis-slide parity with keyboard movement; + - commit `11d990d`; + - NotebookLM structured recall workflow; + - `AGENTS.md` operational startup protocol refactor. + +### Current State + +- Branch: `scene-refact3`. +- Latest commit: `af53e2f AI settings update`. +- Worktree was clean before this wrap-up entry was appended. +- This wrap-up adds a new `Sessions.md` documentation change that should remain uncommitted unless the user wants to commit the session log. + +### Remaining Work / Next Recommended Steps + +- Investigate and fix the unrelated parser world-model duplicate held-item failure: + - `tests/parser/world-model-context.test.ts` + - `compact_cassette` appears in takable scope when the stable id is already held. +- Continue manual QA of click-to-move and scripted `moveTo` in real scenes with: + - large Walkbox polygons; + - foreground occluders such as the sofa; + - narrow passages; + - dynamic blockers. +- Consider adding an engine diagnostic helper such as `scene.explainWalkable(x, y, actor)` or route debug output that reports which object/Walkbox caused a blocked point. +- If route planning performance becomes an issue in large scenes, consider caching sampled walkability grids per route request, using a binary heap for A*, or coarser/finer adaptive grids. +- If future NPC AI relies heavily on `ActorMoveResult`, consider adding event/callback hooks in addition to polling. + +### Risks / Caveats / Open Questions + +- The path planner is intentionally conservative and depends on `Scene.isWalkable`; any existing `isWalkable` quirks will be inherited by pathfinding. +- `Actor.moveTo` now returns a result where old code ignored a `void` return. TypeScript accepted this, but scripts may need to start checking results for AI/NPC behavior. +- `moveToVisual` now clears `visualTarget` and stores world-route waypoints in `target`/`route`; tests were updated to reflect this. +- Large Walkbox pathfinding works after removing the fixed cap, but the new regression test shows a nontrivial runtime cost. Keep an eye on route planning latency in very large scenes. +- Full `npm test` is not green because of the unrelated parser duplicate held-item test. Focused movement/navigation/typecheck validation is green. + +## Session Entry - 2026-05-06 02:17 +02:00 + +### Session Goals + +- Improve the Stage 2 LLM parser context so it understands authored scene semantics such as "the cassette is already loaded in the boombox". +- Replace the temporary media-specific heuristic with a generic Text Asset driven model. +- Keep v1 scoped to LLM context only: no new runtime command verbs such as `PLAY`, `DRIVE`, or `FUEL`. +- Document the new TA authoring contract and commit the implementation. + +### What Was Implemented + +- Added structured object Text Asset support in `TextAssetManager`. + - Object TA can now contain `semanticTags: string[]`. + - Object TA can now contain `relationFacts: Array<{ relation, childTags, fact }>`. + - Added `getResolvedObjectStructuredListField` as a safe accessor for structured list fields without breaking existing string/list text fields. +- Extended parser world model context. + - `ParserEntityContext` now includes optional `semanticTags`. + - `ParserWorldModelBuilder` still emits generic facts such as `Boombox contains Compact cassette.` and `Compact cassette is inside Boombox.` + - It now additionally emits TA-driven semantic facts when a parent object's `relationFacts` match a child object's `semanticTags`. + - Supported semantic relation matching is currently `in`, `on`, `under`, and `behind`. + - `fact` templates support `{self}`, `{child}`, and `{relation}`. +- Removed the previous hardcoded media heuristic. + - The previous `boombox/recorder/cassette/disk` inference is gone. + - Loaded-media knowledge is now authored in object TA. +- Updated current scene Text Assets. + - `public/text/objects/boombox.json` now defines audio/media semantic tags and a relation fact for loaded media. + - `public/text/objects/test.json` and `test_1.json` now tag cassettes as `media`, `audio_media`, and `cassette`. +- Updated LLM prompt assets. + - `parser-llm.json` now gives generic `worldFacts` authority instructions. + - Media-specific prompt wording for PLAY/MUSIC/CASSETTE/RECORDER was removed. + - `parser-llm-system.md` now describes world facts as current location, containment, and Text Asset semantic relation facts. +- Updated documentation. + - `TextAssets.md` now documents `semanticTags`, `relationFacts`, examples, placeholders, supported relations, and v1 limitations. + - `Parser.md` now documents `worldFacts` as a mix of generic runtime facts and authored semantic facts, plus the object TA template changes. + +### Important Architecture Decisions + +- Semantic facts for the LLM are authored in Text Assets, not runtime components, parser code, or prompt-specific hacks. +- `worldFacts` are treated as concise authoritative state facts for the LLM. +- Semantic relation facts are context only in v1. They help the LLM avoid contradicting the scene but do not execute commands or mutate state. +- The same mechanism should be used for future domains: + - boombox + cassette -> loaded media; + - disk drive + floppy -> inserted disk; + - car + gasoline -> fueled vehicle; + - lamp + bulb -> installed component. +- The LLM prompt should remain generic and trust `worldFacts`; it should not contain per-domain rules such as "if recorder contains cassette...". + +### Parser / Mechanics / Scene / Inventory Changes + +- Parser mechanics changed only in world model context construction and LLM prompt preparation. +- Runtime gameplay effects, inventory rules, spatial placement behavior, scene transitions, and command execution were not changed. +- No real `PLAY`, `DRIVE`, `FUEL`, or similar command mechanic was added. +- `LlmCascade` prompt asset typing was widened to tolerate structured service/object text data while still reading only string and string-list prompt fields. + +### Tests Run and Outcomes + +- `npm test -- tests/parser/world-model-context.test.ts tests/parser/llm-cascade.test.ts` + - Passed: 2 files, 27 tests. +- `npm test -- tests/parser tests/integration/parser-game.test.ts` + - Passed: 9 files, 125 tests. +- `npm run typecheck` + - Passed. +- `git diff --check` + - Passed. +- Full `npm test` + - Passed: 23 files, 261 tests. + +### Commits Created + +- `51e64b6 Add TA-driven semantic facts for LLM parser context` + - Implements structured TA semantic fields, TA-driven semantic world facts, generic LLM world fact instructions, current boombox/cassette TA metadata, tests, and documentation. + +### Kairo / Memory Updates + +- Kairo task completed: + - `aaaaaaabtx5ixylx5tpg5nq3qygv5tss` + - `[Quest] Make parser LLM context expose explicit containment` +- Durable `agent_memory` entries stored for: + - the TA-driven semantic facts architecture; + - the documentation update; + - commit `51e64b6`. + +### Current State + +- Branch: `scene-refact3`. +- Latest code commit: `51e64b6 Add TA-driven semantic facts for LLM parser context`. +- Worktree was clean immediately after the commit. +- This wrap-up appends a new `Sessions.md` entry after the code commit. + +### Remaining Work / Next Recommended Steps + +- Manually smoke-test `#LLM-ON` with commands around: + - `play cassette`; + - `play music`; + - future non-media examples once authored, such as fuel/vehicle or disk/drive. +- Consider editor support for authoring `semanticTags` and `relationFacts` directly in the TA UI. +- Consider adding schema validation or linting for malformed `relationFacts`. +- If semantic facts become gameplay-critical later, design a separate runtime component/command contract instead of overloading LLM context facts. + +### Risks / Caveats / Open Questions + +- Semantic facts are only as correct as the TA authoring. A wrong tag or relation fact can mislead the LLM even though runtime state is unchanged. +- Facts should stay concise and factual; atmospheric sarcasm belongs in LLM responses, not TA semantic facts. +- Empty or missing `childTags` currently means the relation rule applies to any child in that relation. +- NotebookLM source replacement completed after this entry was written: fresh `Sessions.md`, `GDD.md`, `AgentMemory.md`, `Parser.md`, and `TextAssets.md` sources were uploaded and reached `ready` status in the Scanline Engine notebook. + +## Session Entry - 2026-05-07 18:01 Europe/Warsaw + +### Session Goals + +- Implement the GDD-described closed-console modal state for parser responses that exceed the two visible closed-console lines. +- Add word wrapping in the closed low-res console so long lines are not clipped at the right screen edge. +- Preserve forced line breaks (`\n`, CRLF/CR) from Text Assets and parser responses so TA descriptions can use paragraphs. +- Iterate on modal-console UX until it matches real gameplay behavior in `test_room`, especially `LOOK CITY`. + +### What Was Implemented + +- Added closed-console word wrapping and forced-newline preservation in `src/core/Console.ts`. + - Closed-console display lines are derived from buffer text by splitting CRLF/CR/`\n` into explicit paragraphs and wrapping words to the low-res console width. + - Very long unbroken words are split so they cannot overflow the canvas. +- Added closed modal state handling. + - `Console.isClosedModal` marks the continue-waiting state. + - Parser/player output that wraps beyond two closed-console lines enters modal state while the console is closed. + - Any normal key press or canvas click dismisses the modal. + - Backquote/tilde is an exception: it opens the full high-res console instead of merely dismissing the modal. +- Added parser-response batching. + - `Console.logResponse()` evaluates the modal threshold over the full player-facing parser response batch, not one physical buffer entry at a time. + - `Game.logResponse()` funnels real parser responses into that batch path, with a fallback for tests/stubs. + - `Parser.parse()` now sends player-facing response output through `game.logResponse(...)` when available. +- Fixed Enter propagation. + - The hidden parser input now stops `Enter` propagation after submitting a command, preventing the same key event from bubbling to global input and instantly dismissing a newly opened modal. +- Scoped modal rendering to the latest parser response. + - `Console` now stores a separate `closedModalDisplayLines` snapshot when a response triggers modal state. + - `Game.renderUI()` renders only `getClosedModalDisplayLines()` while modal, rather than expanding the whole closed-console history. + - Dismissing modal, opening the full console, clearing, or loading from JSON clears that modal snapshot. +- Kept technical parser logs out of the closed console. + - `ConsoleLine` now supports optional `showInClosed`. + - Parser debug/peek messages are written with `{ showInClosed: false }`, so they remain visible in the open console buffer but do not occupy the low-res gameplay screen. + - Command confirmations such as `Parser peek enabled.` from `#PEEK-ON` and `LLM prompt/response peek enabled.` from `#PEEKLLM-ON` remain normal visible output. +- Polished the continue prompt. + - `[Continue]` is right-aligned on the modal prompt row. + - Its blink cadence now uses the same `cursorBlink / 500` logic as the normal closed-console text cursor. + +### Important Architecture / Runtime Decisions + +- Closed modal is a transient view over the latest parser response, not a resized history viewer. +- The full console buffer remains the authoritative history for the open console; the modal snapshot is only for the low-res modal display. +- Parser/player responses and technical diagnostic logs now have different closed-console visibility semantics: + - player-facing parser responses can trigger and populate modal state; + - debug/peek logs are retained for the open console but hidden from the closed console. +- Tilde/backquote has higher priority than generic modal dismissal because it is the user's established gesture for opening the console. +- Forced text newlines are handled at display wrapping time, so existing JSON string escape behavior (`\n`, CRLF/CR) works without changing Text Asset schemas. + +### Parser / Mechanics / Scene / UI Changes + +- Parser: + - `src/mechanics/Parser.ts` now sends player-facing response arrays through `game.logResponse(...)`. + - Parser debug messages are logged with `showInClosed: false`. +- Console/runtime: + - `src/core/Console.ts` gained display-line wrapping, modal state, modal response snapshots, `logResponse`, `getClosedModalDisplayLines`, and `showInClosed` filtering. + - `src/core/Game.ts` renders dynamic-height closed modal output, right-aligned blinking `[Continue]`, and `logResponse`. + - `src/core/Input.ts` handles Backquote before generic modal dismissal. + - `src/core/IGame.ts` exposes optional `logResponse`. +- React UI: + - `src/components/UIOverlay.tsx` disables hidden parser input during modal state, stops submit `Enter` propagation, and mirrors the Backquote modal-open fallback. +- Tests: + - `tests/parser/preprocessor.test.ts` now covers closed-console wrapping, forced newlines, modal dismissal, multi-message parser response modal triggering, Backquote opening the full console, technical log filtering, and latest-response-only modal snapshots. + +### Validation / Tests Run + +- `codex-doctor -Fast` + - Passed: 17 checks, 0 warnings, 0 failures. +- NotebookLM CLI readiness: + - Initial `list` / smoke `ask` failed because auth had expired. + - `notebooklm-ready.ps1 -AutoLogin` repaired CLI auth and the project smoke test passed. +- Focused tests during implementation: + - `npm test -- tests/parser/preprocessor.test.ts` + - `npm test -- tests/parser/preprocessor.test.ts tests/parser/commands.test.ts tests/integration/parser-game.test.ts` + - `npm test -- tests/parser/preprocessor.test.ts tests/parser/llm-parser.test.ts` + - All passed after fixes. +- Typecheck: + - `npm run typecheck` + - Passed. +- Full suite: + - `npm test` + - Passed: 23 files, 268 tests. +- Whitespace: + - `git diff --check` + - Passed. Git reported expected LF-to-CRLF working-copy warnings only. +- Browser smoke checks with Playwright: + - `LOOK CITY` in `test_room` enters closed modal, disables input, preserves forced CR/newline, wraps text, and dismisses to the last two wrapped lines. + - Backquote from modal opens the full high-res console overlay. + - `#PEEK-ON` followed by `LOOK CITY` keeps `--- CONTEXT ---` in the full buffer but hides it from closed display, while `Parser peek enabled.` remains visible. + - Repeating `LOOK CITY` leaves history in the full buffer but modal renders only the latest response snapshot. + +### Commits Created During This Session + +- No git commit was created during this session. +- Latest commit at wrap-up time: + - `2fcb17e Fix spatial relation type narrowing` + +### Kairo / Memory Updates + +- Kairo task updated and closed: + - `aaaaaaabtybovqouo5bfcrh3gru6asfb` + - `[Quest] Implement closed-console modal multiline output` +- Durable `agent_memory` entries stored for: + - closed console modal multiline output implementation; + - parser response batching and Enter propagation fix; + - Backquote opening the full console from modal state; + - right-aligned `[Continue]` prompt and blink cadence; + - technical parser logs hidden from closed console; + - latest-response-only modal snapshot behavior. + +### Current State + +- Branch: `scene-refact3`. +- Worktree has uncommitted changes from this session in: + - `src/components/UIOverlay.tsx` + - `src/core/Console.ts` + - `src/core/Game.ts` + - `src/core/IGame.ts` + - `src/core/Input.ts` + - `src/mechanics/Parser.ts` + - `tests/parser/preprocessor.test.ts` +- There is also a pre-existing/user-owned dirty file not edited as part of this implementation: + - `public/scenes/test_room.json` +- `Sessions.md` is updated by this wrap-up entry. + +### Remaining Work / Next Recommended Steps + +- Commit the feature after user acceptance, including the source/test changes and this `Sessions.md` entry if desired. +- Consider adding a higher-level integration/UI test for closed-modal rendering if the project later gains browser-driven test infrastructure. +- Consider documenting the closed-console modal contract in `GDD.md` or a console/UI architecture doc if this behavior becomes a stable public editing/design rule. + +### Risks / Caveats / Open Questions + +- Closed modal currently caps visible modal output to available low-res screen height. Extremely long responses still show the tail of that response rather than true pagination. +- Technical logs are hidden only when logged with `showInClosed: false`; future debug producers should use the same flag if they should stay out of the closed console. +- The modal snapshot is intentionally transient and is not serialized into save/load state. +- The dirty `public/scenes/test_room.json` was left untouched because it appears unrelated/user-owned. + +### NotebookLM / RAG Refresh + +- NotebookLM source replacement completed after this entry was appended: + - `Sessions.md` + - `GDD.md` + - generated `AgentMemory.md` +- The local memory mirror was refreshed and the NotebookLM memory dump was regenerated as part of this wrap-up workflow. +- Fresh NotebookLM sources reached `ready` status in the Scanline Engine notebook. + +## Session Entry - 2026-05-08 00:40 +02:00 + +### Session Goals + +- Finish and stabilize the inventory item preview behavior. +- Make inventory click, `LOOK`, and `EXAMINE` semantics line up with the intended text channels: + - overlay is visual-only; + - console owns all text; + - click/`LOOK` use `description`; + - `EXAMINE` uses `details`. +- Ensure the closed console modal `[Continue]` state has higher click priority than the inventory preview overlay. +- Record the current repo state and durable behavior contract for future sessions. + +### What Was Implemented + +- Inventory overlay is now image-only: + - text rendering was removed from the overlay path; + - stale `.inventory-preview-text` styling was removed. +- Clicking a player inventory slot now behaves like `LOOK`: + - opens the inventory preview overlay with no preview text; + - logs the item `description` to the game console. +- `LOOK` on a held inventory item now opens the same image preview and returns/logs `description`. +- `EXAMINE` on a held inventory item now opens the image preview and returns/logs `details`. +- Inventory preview default text resolution no longer prefers `details`; explicit callers now decide whether preview text should exist, and the current player-facing overlay path passes `null`. +- Inventory overlay click handling now respects the closed-console modal: + - if the console is in `[Continue]` state, the first click calls `console.continueClosedModal()`; + - that click does not close the inventory preview; + - subsequent backdrop clicks close the preview normally; + - clicks on the preview card itself still do not close the overlay. + +### Important Decisions + +- Inventory preview overlay should be visual-only for current gameplay UX. Text belongs in the console. +- Clicking an inventory slot is equivalent to `LOOK` for text semantics, not `EXAMINE`. +- `EXAMINE` remains the detailed text command and uses `details`, including for held inventory items. +- The closed console modal has higher input priority than the inventory overlay. This avoids losing the preview when the player is only trying to dismiss `[Continue]`. + +### Parser / Mechanics / Inventory Changes + +- `src/components/inventory/PlayerInventoryPanel.tsx` + - click handler now opens preview with `null` preview text and logs `description` to console. +- `src/systems/GameSemanticAPI.ts` + - `lookEntity(...)` opens image-only preview for inventory items while returning `description`; + - `examineEntity(...)` opens image-only preview for inventory items while returning `details`. +- `src/systems/InventoryManager.ts` + - preview fallback text resolution was changed to use `description` instead of `details`, but the current overlay flow intentionally passes `null`. +- `src/components/UIOverlay.tsx` + - preview text rendering removed; + - overlay click handling checks `console.continueClosedModal()` before closing preview. + +### Tests Run + +- `npx vitest run tests/game/semantic-api.test.ts` + - Passed: 80 tests. +- `npx vitest run tests/game/semantic-api.test.ts tests/parser/commands.test.ts` + - Passed: 91 tests. +- `npm run typecheck` + - Passed. + +### Commits Created During This Session + +- Latest commit at wrap-up time: + - `1b43375 Imrovement: Click on Inventory Item now work as LOOK command instead of EXAMINE.` +- No additional commit was created by this wrap-up step. + +### Durable Memory Updates + +- Stored `agent_memory` decision: + - `5e584cec-59f7-4a32-943c-ade76c5cc271` + - `Inventory item LOOK/click/EXAMINE preview contract` + +### Current Worktree State + +- Worktree still has uncommitted changes at wrap-up time: + - `public/scenes/test_room.json` + - `public/text/system/parser-llm-system.md` + - `public/text/system/parser-llm.json` + - `src/mechanics/Parser.ts` + - `src/systems/GameSemanticAPI.ts` + - `tests/fixtures/parserFactory.ts` + - `tests/game/semantic-api.test.ts` + - `tests/integration/parser-game.test.ts` + - `public/text/objects/audio_cables.json` (untracked) +- The dirty tree includes ongoing hidden-object / relation-discovery changes beyond the inventory preview contract: + - direct `LOOK` / `EXAMINE` of hidden semantic targets is being constrained; + - relation `LOOK` can reveal `lookable` hidden contents; + - examining an anchor can reveal `examinable` hidden descendants; + - LLM instructions now distinguish hidden facts from visible target candidates and allow only indirect non-revealing hints. + +### Remaining Work / Next Steps + +- Review and commit the remaining dirty hidden-object / relation-discovery changes separately from the already committed inventory-preview behavior, if accepted. +- Consider a browser/UI-level test later for the overlay-vs-closed-modal click priority, since the current validation is mostly semantic/type-level. +- Optionally update `GDD.md` or a parser/UI behavior doc with the final inventory item contract: + - click/`LOOK` = image preview + `description` in console; + - `EXAMINE` = image preview + `details` in console; + - overlay itself renders no text. + +### Risks / Caveats + +- `UIOverlay.tsx` behavior depends on `console.continueClosedModal()` returning `true` only when the console is actually in modal `[Continue]` state. +- The current dirty worktree includes user/session changes outside the final inventory preview fix; future agents should inspect diffs carefully before committing. +- The latest commit message contains a typo: `Imrovement`. + +## Session Entry - 2026-05-08 01:18 +02:00 + +### Session Goals + +- Record only the work completed after the previous wrap-up. +- Commit the accepted inventory-preview focused-target parser behavior. +- Refresh durable memory and NotebookLM sources after the commit. + +### What Was Implemented + +- Inventory preview overlay now keeps the command input focused: + - `UIOverlay` prevents overlay mouse-down from blurring the hidden parser input; + - the player can continue typing commands while inspecting the item image. +- The currently previewed held inventory item is now the default parser target/item for commands that omit an explicit object: + - `LOOK` with no noun becomes `LOOK `; + - `EXAMINE`, `TAKE`, `OPEN`, `CLOSE`, and `GO TO` with a missing target use the preview item; + - `DROP`/`PUT` with a missing item use the preview item; + - custom commands fill the first missing entity argument from the preview item. +- The parser world model now exposes `context.focusedTarget` for the LLM cascade: + - includes preview item id, title, source, description/details, and synonyms when available; + - only appears when the previewed entity is still held in player inventory. +- LLM prompt assets now instruct the model to use `focusedTarget.title` as the default target/item when the player omitted an explicit object. +- Parser tests were updated so the fixture matches the current inventory `LOOK` contract (`description`, not `details`). + +### Important Decisions + +- The inventory preview item is a parser focus, not just a UI state. +- Focused-target defaulting is applied after stage 1, NLP, and LLM envelopes are produced, before core parser execution. +- Overlay remains image-only; all textual output remains in the console. + +### Tests Run + +- `npx vitest run tests/parser tests/integration/parser-game.test.ts tests/game/semantic-api.test.ts` + - Passed: 10 files, 219 tests. +- `npm run typecheck` + - Passed. +- Pre-commit hook ran on staged files: + - `prettier --write` + - `eslint --max-warnings=0 --fix` + +### Commits Created During This Session + +- `3689cda Make inventory preview item parser default target` + - Adds focused-target defaulting for parser plans. + - Adds `focusedTarget` to LLM context and prompt guidance. + - Keeps command input focused while inventory overlay is open. + - Adds parser/world-model regression tests. + +### Durable Memory Updates + +- Stored decision before commit: + - `b475a7c9-c4fa-4f67-a26e-36af2ee3a720` + - `Inventory preview focused target defaults parser command targets` +- Stored commit-context decision after commit: + - `64808676-4d76-4aec-8609-eae6f0c73fb6` + - `Commit 3689cda focused inventory preview parser default target` + +### Current State + +- Worktree is clean after commit. +- Latest commit: `3689cda`. + +### Remaining Work / Next Recommended Steps + +- Push or continue from `3689cda` as the clean checkpoint. +- Consider documenting the focused-target command rule in `GDD.md` / parser docs if this becomes a public design contract. + +### Risks / Caveats + +- Custom command defaulting currently fills the first missing entity argument only. Multi-argument commands still ask for later missing arguments. +- `LOOK` with no noun becomes focused-item `LOOK` only when a held inventory preview is open; otherwise existing scene-look behavior is preserved. + +## Session Entry - 2026-05-08 01:19 +02:00 + +### Session Goals + +- Final wrap-up and durable handoff after the hidden-object discovery fixes, LLM prompt clarification, and focused inventory-preview parser work. +- Refresh project memory / NotebookLM sources so future agents see the current contracts. + +### What Was Implemented + +- Hidden semantic object discovery was tightened and committed in `e8aa71a`. + - Direct `LOOK ` no longer reveals or describes hidden objects. + - Direct `EXAMINE ` also behaves as not found until the object is discovered. + - Relation `LOOK` reveals `hidden: "lookable"` descendants, for example `LOOK BEHIND BOOMBOX` revealing `audio_cables`. + - `EXAMINE ` reveals `hidden: "examinable"` descendants around that anchor, for example `EXAMINE BOOMBOX` revealing examinable `audio_cables` behind it. + - `test_room` now includes `audio_cables` behind the boombox with object text assets. +- The parser LLM prompt now treats hidden facts as real engine/world facts but not player-visible targets. + - Hidden objects from `hiddenKnown`, `worldKnown`, or world facts cannot be used as action targets/items/anchors. + - The LLM may use hidden facts only for indirect non-revealing sensory hints such as smell, rattling, vague shape, weight, or suspicious gaps. +- Focused inventory-preview parser behavior was committed in `3689cda`. + - An open held-item preview becomes the default parser target/item when the player omits an explicit object. + - LLM context now exposes `focusedTarget`. + - The inventory overlay preserves parser input focus. + +### Important Decisions + +- Hidden discovery state is runtime progress, not scene authoring data. + - `Scene.revealedHiddenEntities` should later be saved with game-state save/load, not in scene JSON. +- The player cannot directly name or examine an unknown hidden object. + - Discovery happens through contextual investigation of visible anchors/relations. +- LLM hidden knowledge is authorial context for atmosphere, not permission to reveal objects or route actions to them. + +### Parser / Mechanics / Runtime Changes + +- `Parser` no longer has direct semantic-hidden target resolution for `LOOK` or `EXAMINE`. +- `GameSemanticAPI.examineEntity()` no longer reveals the target before access checks. +- `GameSemanticAPI.examineEntity()` does reveal examinable hidden descendants after a visible anchor passes access checks. +- Parser test fixtures were updated to mirror the production semantic API reveal behavior. +- Parser world model / LLM context now supports inventory preview `focusedTarget`. + +### Tests Run And Outcomes + +- Hidden-object / LLM prompt focused validation: + - `npm test -- tests/game/semantic-api.test.ts tests/integration/parser-game.test.ts tests/parser/world-model-context.test.ts` + - Passed: 3 files, 157 tests. + - `npm test -- tests/parser/llm-parser.test.ts tests/parser/llm-cascade.test.ts tests/parser/world-model-context.test.ts tests/core/text-asset-manager.test.ts` + - Passed: 4 files, 39 tests. +- Broader validation: + - `npm run typecheck` passed. + - `npm test -- tests/parser tests/integration/parser-game.test.ts tests/game/semantic-api.test.ts` passed: 10 files, 215 tests. + - Full `npm test` passed: 24 files, 277 tests. + - `git diff --check` passed, with only expected Windows LF-to-CRLF warnings. +- Focused-target commit validation: + - `npx vitest run tests/parser tests/integration/parser-game.test.ts tests/game/semantic-api.test.ts` passed: 10 files, 219 tests. + - `npm run typecheck` passed. + +### Commits Created During This Session + +- `e8aa71a Fixed Hidden Items mechanic and imroved LLM prompt about it` + - Fixes hidden `lookable` / `examinable` discovery contracts. + - Adds `audio_cables` test-room content. + - Updates parser LLM hidden-fact prompt rules. +- `3689cda Make inventory preview item parser default target` + - Adds focused inventory-preview default targets and LLM `focusedTarget` context. + +### Durable Memory Updates + +- Stored and/or updated durable memory for: + - hidden object direct `LOOK` leak incident; + - future game-state save/load preserving `revealedHiddenEntities`; + - direct `EXAMINE` not revealing hidden semantic targets; + - LLM hidden facts being usable only for non-revealing sensory hints; + - inventory preview focused target defaulting. + +### Current State + +- Branch: `scene-refact3`. +- Branch is ahead of `origin/scene-refact3` by 1 according to the latest `git status`. +- Working tree has only `Sessions.md` modified for wrap-up source updates. +- Latest commit: `3689cda`. + +### Remaining Work / Next Recommended Steps + +- Commit the updated `Sessions.md` after NotebookLM source refresh if desired. +- Push `scene-refact3` when ready. +- Later game save/load work should include per-scene runtime state such as `revealedHiddenEntities`. +- Consider documenting the final hidden-object and focused-target contracts in `GDD.md` or parser docs if they become player-facing design rules. + +### Risks / Caveats + +- Commit messages contain typos: `imroved` / `Imrovement`. +- NotebookLM and local RAG sources can lag behind live `agent_memory`; this wrap-up refresh should reduce that gap. + +## Session Entry - 2026-05-09 01:16 +02:00 + +# Session Summary + +## Session Goal + +- Modernize project documentation (tech-spec.md). +- Decouple and expand the Scripting System documentation. +- Improve the Scene Editor's UX by implementing SVG component iconography in the properties panel and custom Select dropdown. + +## What Was Implemented + +### 1. Documentation Modernization (tech-spec.md) + +- Completely rewrote the specification to reflect the post-refactoring architecture (Game monolith decomposition, GameSemanticAPI, LLM Parser, A* Navigation). +- Added a comprehensive **Codebase Map** mapping subsystems to src/ directories. +- Integrated the **Tauri Native Build** documentation, including path resolution logic (std::env::current_exe()), Windows bundling specifics, and workspace requirements. + +### 2. Scripting System Documentation + +- Extracted the technical scripting API into a new, beginner-friendly standalone guide: ScriptSys.md. +- Added a "Chapter 1: How to Create Your First Script" tutorial with practical examples on registering a script and attaching it to a TriggerBox. +- tech-spec.md now references ScriptSys.md as the primary source for scripting. + +### 3. Component UI Updates + +- Implemented dynamic SVG icon rendering in the SectionComponents.tsx properties panel header using import.meta.glob and CSS mask-image. +- Fixed a typo in the icon filename (invetory.svg -> inventory.svg) to correctly match component names. +- Updated the custom { + if (el) el.indeterminate = sharedFilled === 'mixed'; + }} + onChange={(e) => { + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).filled = e.target.checked; + }); + }} + /> + Fill + + + )} + {quads.length > 0 && (
)} + + {scene.soundEnv && ( +
+
+
+ 3 + 3D SOUND ENV. +
+
+
+
+ + { + const val = parseFloat(e.target.value); + if (!isNaN(val)) { + scene.soundEnv.audioMaxDistance = val; + SoundManager.getInstance().setEnvironment(scene.soundEnv); + incrementObjectVersion(); + } + }} + /> +
+
+ + { + const val = parseFloat(e.target.value); + if (!isNaN(val)) { + scene.soundEnv.reverbMaxDist = val; + SoundManager.getInstance().setEnvironment(scene.soundEnv); + incrementObjectVersion(); + } + }} + /> +
+
+ + { + const val = parseFloat(e.target.value); + if (!isNaN(val)) { + scene.soundEnv.reverbMinPercent = Math.max(0, Math.min(1, val)); + SoundManager.getInstance().setEnvironment(scene.soundEnv); + incrementObjectVersion(); + } + }} + /> +
+
+ + { + const val = parseFloat(e.target.value); + if (!isNaN(val)) { + scene.soundEnv.zoomSensitivity = val; + SoundManager.getInstance().setEnvironment(scene.soundEnv); + incrementObjectVersion(); + } + }} + /> +
+
+ + { + const val = parseFloat(e.target.value); + if (!isNaN(val)) { + scene.soundEnv.pannerRefDistance = val; + SoundManager.getInstance().setEnvironment(scene.soundEnv); + incrementObjectVersion(); + } + }} + /> +
+
+ + { + const val = parseFloat(e.target.value); + if (!isNaN(val)) { + scene.soundEnv.pannerRolloffFactor = val; + SoundManager.getInstance().setEnvironment(scene.soundEnv); + incrementObjectVersion(); + } + }} + /> +
+
+ + +
+
+ + +
+
+ +
+ +
+ { + scene.soundEnv.defaultReverbIR = e.target.value; + SoundManager.getInstance().setEnvironment(scene.soundEnv); + incrementObjectVersion(); + }} + /> + +
+
+
+ )} ); }; diff --git a/src/components/editor/properties/SectionComponents.tsx b/src/components/editor/properties/SectionComponents.tsx index 348a18f..69bae1e 100644 --- a/src/components/editor/properties/SectionComponents.tsx +++ b/src/components/editor/properties/SectionComponents.tsx @@ -3,6 +3,16 @@ import { usePropertiesContext } from './PropertiesContext'; import { Select } from '../../common/Select'; import { SceneObject } from '../../../entities/SceneObject'; +import { Actor } from '../../../entities/Actor'; +import { Entity } from '../../../entities/Entity'; + +const iconModules = import.meta.glob('../../../assets/components-icon/*.svg', { eager: true }); +function getIconUrl(type: string): string | undefined { + const mod = (iconModules as Record)[ + `../../../assets/components-icon/${type.toLowerCase()}.svg` + ]; + return mod?.default; +} export const SectionComponents: React.FC = () => { const { game, obj, selectedObjectType, setSectionRef, incrementObjectVersion } = @@ -10,6 +20,11 @@ export const SectionComponents: React.FC = () => { const o = obj; const title = game.textAssets.getResolvedObjectField(o, 'title'); const hasTitle = !!title?.trim(); + const hasActorComponent = (o.components || []).some((comp: any) => comp?.type === 'Actor'); + const renderedComponents = + selectedObjectType === 'Actor' && !hasActorComponent + ? [{ type: 'Actor', __virtualActorComponent: true }, ...(o.components || [])] + : o.components || []; const relationOptions = [ { value: 'in', label: 'IN' }, { value: 'on', label: 'ON' }, @@ -112,6 +127,9 @@ export const SectionComponents: React.FC = () => { { value: 'Blocker', label: 'Blocker' }, { value: 'Exit', label: 'Exit (Transition)' }, { value: 'Entry', label: 'Entry (Spawn Point)' }, + ...(selectedObjectType === 'Static' || selectedObjectType === 'Entity' + ? [{ value: 'Actor', label: 'Actor' }] + : []), ...(selectedObjectType === 'Quad' ? [ { value: 'Backface', label: 'Backface' }, @@ -120,11 +138,17 @@ export const SectionComponents: React.FC = () => { ] : []), ...(selectedObjectType === 'Actor' ? [{ value: 'Shadow', label: 'Shadow' }] : []), - ]} + ].map((opt) => ({ ...opt, icon: getIconUrl(opt.value) }))} placeholder="+ Add Component" onChange={(value) => { const type = value; if (!type) return; + if (type === 'Actor') { + if (game.editor.selectedObject instanceof Entity) { + game.editor.convertEntityToActor(game.editor.selectedObject); + } + return; + } if (game.editor) game.editor.saveUndoState(); if (!o.components) o.components = []; const relation = hasTitle ? getNextAvailableContainerRelation() : null; @@ -224,10 +248,14 @@ export const SectionComponents: React.FC = () => { - {o.components && - o.components.map((comp: any, idx: number) => ( + {renderedComponents.map((comp: any, displayIdx: number) => { + const idx = + selectedObjectType === 'Actor' && !hasActorComponent ? displayIdx - 1 : displayIdx; + const isVirtualActorComponent = !!comp.__virtualActorComponent; + + return (
{ marginBottom: '5px', }} > - - {comp.type} - +
+ {getIconUrl(comp.type) && ( +
+ )} + {comp.type} +
+ {comp.type === 'Actor' && ( +
+ Enables Actor movement, direction, player mode, and animation sets. +
+ )} + {comp.type === 'Backface' && ( <>
{ type="text" className="e-input" style={{ width: '100%' }} - value={comp.idKey || ''} + value={comp.keyId || ''} onChange={(e) => { - comp.idKey = e.target.value; + comp.keyId = e.target.value; incrementObjectVersion(); }} /> @@ -1090,7 +1163,8 @@ export const SectionComponents: React.FC = () => { )}
- ))} + ); + })}
); }; diff --git a/src/components/editor/properties/SectionIdentity.tsx b/src/components/editor/properties/SectionIdentity.tsx index 83d666c..8cedf19 100644 --- a/src/components/editor/properties/SectionIdentity.tsx +++ b/src/components/editor/properties/SectionIdentity.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { usePropertiesContext } from './PropertiesContext'; import { Select } from '../../common/Select'; import { Entity } from '../../../entities/Entity'; -import { SceneObject } from '../../../entities/SceneObject'; -import { Triggerbox } from '../../../entities/Triggerbox'; +import { normalizeGroupIdList } from '../../../utils/GroupIds'; +import { findDuplicateSceneObjectName } from './SectionIdentityUtils'; interface SectionIdentityData { id?: string; @@ -55,6 +55,12 @@ export const SectionIdentity: React.FC = ({ incrementHierarchyVersion, } = usePropertiesContext(); const o = obj; + const identityValue = isScene ? o.id || '' : o.name || ''; + const [identityDraft, setIdentityDraft] = React.useState(identityValue); + + React.useEffect(() => { + setIdentityDraft(identityValue); + }, [identityValue]); return (
@@ -64,12 +70,9 @@ export const SectionIdentity: React.FC = ({ { - const val = e.target.value; - if (isScene) o.id = val; - else o.name = val; - incrementObjectVersion(); + setIdentityDraft(e.target.value); }} onBlur={(e) => { const rawVal = e.target.value; @@ -80,16 +83,17 @@ export const SectionIdentity: React.FC = ({ const scene = game?.sceneManager?.currentScene; if (!isScene && scene) { - const dupEntity = scene.entities.find( - (ent: Entity) => ent.name === finalVal && ent !== game?.editor?.selectedObject + const duplicate = findDuplicateSceneObjectName( + scene, + finalVal, + game?.editor?.selectedObject || o ); - const dupTrigger = scene.triggerboxes - ? scene.triggerboxes.find( - (tb: Triggerbox) => tb.name === finalVal && tb !== game?.editor?.selectedObject - ) - : null; - if (dupEntity || dupTrigger) { + if (!finalVal) { + console.warn('[PropertiesPanel] Empty Name rejected.'); + game.showMessage('Name cannot be empty!'); + isValid = false; + } else if (duplicate) { console.warn(`[PropertiesPanel] Duplicate Name '${finalVal}' rejected.`); game.showMessage(`Name '${finalVal}' already exists!`); isValid = false; @@ -98,13 +102,10 @@ export const SectionIdentity: React.FC = ({ if (isValid) { handleChange(field, finalVal); + setIdentityDraft(finalVal); } else { - const realObj = game?.editor?.selectedObject as SceneObject | null | undefined; - - if (!isScene && realObj) { - o.name = realObj.name; - incrementObjectVersion(); - } + setIdentityDraft(identityValue); + incrementObjectVersion(); } }} /> @@ -153,19 +154,13 @@ export const SectionIdentity: React.FC = ({ className="e-input" value={o.groupID || ''} onChange={(e) => { - const val = e.target.value; - const tokens = val.split(','); - const newTokens = tokens.map((t) => { - if (t.length === 0) return ''; - let clean = t; - const trimmed = t.trimStart(); - if (trimmed.length > 0 && !trimmed.startsWith('#')) { - const firstCharIdx = t.length - trimmed.length; - clean = t.substring(0, firstCharIdx) + '#' + trimmed; - } - return clean; - }); - handleChange('groupID', newTokens.join(',')); + handleChange( + 'groupID', + normalizeGroupIdList(e.target.value, { preserveEmptyTokens: true }) + ); + }} + onBlur={(e) => { + handleChange('groupID', normalizeGroupIdList(e.target.value)); }} />
diff --git a/src/components/editor/properties/SectionIdentityUtils.ts b/src/components/editor/properties/SectionIdentityUtils.ts new file mode 100644 index 0000000..9f87415 --- /dev/null +++ b/src/components/editor/properties/SectionIdentityUtils.ts @@ -0,0 +1,21 @@ +import type { SceneObject } from '../../../entities/SceneObject'; + +export function findDuplicateSceneObjectName( + scene: any, + name: string, + currentObject: unknown +): SceneObject | null { + const finalName = name.trim(); + if (!scene || !finalName) return null; + + const selected = currentObject as SceneObject | null | undefined; + const allObjects: SceneObject[] = [ + ...(scene.entities || []), + ...(scene.walkbox || []), + ...(scene.triggerboxes || []), + ]; + + return ( + allObjects.find((candidate) => candidate.name === finalName && candidate !== selected) || null + ); +} diff --git a/src/components/editor/properties/propertiesConstants.ts b/src/components/editor/properties/propertiesConstants.ts index 901bc04..51be524 100644 --- a/src/components/editor/properties/propertiesConstants.ts +++ b/src/components/editor/properties/propertiesConstants.ts @@ -107,6 +107,24 @@ export const PROPERTIES_LABEL_TOOLTIPS: Record = { Max: 'Maximum depth-scaling factor used at the front end of the scene.', 'Horizon Y': 'Y coordinate treated as the horizon for depth scaling.', 'Front Y': 'Y coordinate treated as the foreground limit for depth scaling.', + 'Max Distance': + 'The maximum distance (in pixels) beyond which the sound volume stops decreasing and remains at its minimum level. Useful for limiting the audible range of local sounds.', + 'Reverb Drown Dist': + 'The distance (based on depth/parallax) at which the dry sound is completely replaced by reverb. Higher values keep the sound "clear" further away; lower values make it "washy" and distant quickly.', + 'Reverb Min %': + 'The minimum amount of reverb present even when the sound is right next to the listener (at zero distance). 0.0 is completely dry; 1.0 is full reverb.', + 'Zoom Sensitivity': + 'Controls how much the camera zoom affects the perceived audio distance. 0 means zoom has no effect; 1.0 means audio distance scales 1:1 with optical zoom.', + 'Ref Distance': + 'The "Reference Distance" — the distance from the listener where volume begins to fall off. Below this value, the sound plays at 100% volume. Increase this for larger objects that should sound "close" over a wider area.', + 'Rolloff Factor': + 'Determines how quickly the volume decreases as the listener moves away from the source beyond the Reference Distance. Higher values cause a steeper, faster drop in volume.', + 'Panning Model': + 'The spatialization algorithm:\n- HRTF: High-quality, simulates human ear filtering (recommended).\n- Equal Power: Simple stereo panning without frequency filtering.', + 'Distance Model': + 'The formula used to calculate volume drop-off:\n- Linear: Steady, constant decrease.\n- Inverse: Natural-sounding decrease (logarithmic).\n- Exponential: Very sharp drop-off at a distance.', + 'Default Reverb IR': + 'Impulse response file used as the default reverb for all attached sounds in this scene. If empty, attached sounds will be dry by default.', 'UI Scale': 'Editor interface scale multiplier.', 'Game Zoom': 'Scales the game viewport inside the application window. Fit uses the largest size that still stays fully visible.', diff --git a/src/components/inventory/PlayerInventoryPanel.tsx b/src/components/inventory/PlayerInventoryPanel.tsx index 63571ef..e73369b 100644 --- a/src/components/inventory/PlayerInventoryPanel.tsx +++ b/src/components/inventory/PlayerInventoryPanel.tsx @@ -46,6 +46,9 @@ export const PlayerInventoryPanel: React.FC = ({ game
{inventoryItems.map((item: Entity) => { const title = game.textAssets.getResolvedObjectField(item, 'title') || item.name; + const objectDescription = game.textAssets.getResolvedObjectField(item, 'description'); + const runtimeDescription = typeof item.description === 'string' ? item.description : null; + const description = objectDescription || runtimeDescription; const isActive = previewedItem === item; return (