diff --git a/Commands.md b/Commands.md index 483c75d4..79ec6283 100644 --- a/Commands.md +++ b/Commands.md @@ -2,7 +2,7 @@ ## Summary -`Commands` in `Scanline` are parser-level action specifications that describe **custom gameplay commands** without hardcoding one-off logic into `Parser.ts`. +`Commands` in `Scanline` are authored action specifications that describe **custom gameplay commands** without hardcoding one-off logic into `Parser.ts`. The goal is to let us add commands such as: - `TELEPORT WITH ID CARD` @@ -10,7 +10,7 @@ The goal is to let us add commands such as: - `REPAIR BOOMBOX WITH SOLDERING IRON` - `USE ITEM ON TARGET` -while reusing the same generic parser systems for: +while reusing the same generic parser and runtime systems for: - target resolution - ambiguity clarification - missing-argument clarification @@ -36,15 +36,15 @@ Instead: - each custom command is described by data - `Parser Core` executes a generic plan -This is also the shared execution foundation for the LLM cascade: -- lower layers, custom commands, mocked scenarios, and LLM outputs can emit the same plan format -- `Core` stays the only place where parser plans are executed against gameplay rules +This is also the shared execution foundation for the LLM cascade and other actor-aware clients: +- lower layers, custom commands, mocked scenarios, NPC Puppet Master plans, and LLM outputs can emit the same plan format +- parser-produced plans still execute through `Parser Core`, while non-parser actor plans use the shared actor-aware executor instead of going through text parsing again --- ## Position In The Architecture -Custom command assets belong to the **parser layer**, not to `Game`. +Custom command assets are authored content, not hardcoded `Game` logic. Their execution is shared runtime behavior, even when the initiating client is the parser. They are: - language-aware @@ -63,8 +63,8 @@ The flow is: 2. Stage 1 tries built-in parser logic 3. Stage 1 also checks custom command assets 4. A matching command asset produces a parser envelope / plan -5. `Parser Core` resolves arguments and executes the plan -6. `Game API` performs the actual world operations +5. `Parser Core` resolves arguments and executes the plan for parser-originated input +6. `Game API` / shared actor-aware runtime performs the actual world operations --- diff --git a/GDD.md b/GDD.md index 43070a5e..64c432a4 100644 --- a/GDD.md +++ b/GDD.md @@ -58,7 +58,7 @@ ## Парсер - посредник -Парсер в нашей игре -- это "мозг", который, общаясь с игроком на естественном языке, играет роль **посредника** между игровым движком и игроком, своеобразного гейм-мастера. Он принимает пользовательский ввод, наряду с контекстом (информацией о сцене, находящихся в ней предметах и NPC, доступныx действиях и состояниях). Затем парсер обрабатывает это и даёт команды игровому движку через API, опционально получает возвращаемые API значения и составляет сообщения для пользователя. +Парсер в нашей игре -- это "мозг", который, общаясь с игроком на естественном языке, играет роль **посредника** между игровым движком и игроком, своеобразного гейм-мастера. Он принимает пользовательский ввод, наряду с контекстом (информацией о сцене, находящихся в ней предметах и NPC, доступныx действиях и состояниях). Затем парсер обрабатывает это и даёт команды игровому движку через API, опционально получает возвращаемые API значения и составляет сообщения для пользователя. При этом сама семантическая часть исполнения действий постепенно выведена в общий actor-aware runtime слой: player parser, Puppet Master и другие клиенты могут использовать одни и те же разрешённые world actions и authored commands, не проходя через текстовый парсер заново. ---json---> | | | | | | ---text--> | | ---json--> | | @@ -625,7 +625,7 @@ Parser-команды `OPEN` и `CLOSE` используют тот же runtime Скриптовое API позволяет получать и устанавливать любые State любого объекта, но не позволяет создавать новые. Пользователь должен явно создать их в реакторе. -Parser command DSL тоже может менять уже созданные State через data-driven action. Такие команды не создают State автоматически: если State отсутствует или тип значения не совпадает, действие считается неуспешным. State текущих объектов сцены и inventory-предметов включаются в parser context и worldFacts, чтобы runtime-изменения были видны LLM без попадания в stale static cache. +Parser command DSL и общий actor-aware command runtime тоже могут менять уже созданные State через data-driven action. Такие команды не создают State автоматически: если State отсутствует или тип значения не совпадает, действие считается неуспешным. State текущих объектов сцены и inventory-предметов включаются в parser context и worldFacts, чтобы runtime-изменения были видны LLM без попадания в stale static cache. Runtime-изменения State должны идти через общий State event path (Script API и parser actions используют его автоматически). Если значение реально изменилось, движок проверяет `interactions` объекта и запускает скрипты по ключам `state:` и `state:=`. В контекст скрипта передаются `entity` и `args`: `stateId`, `previousValue`, `value`, `valueType`, `source`. Низкоуровневый `ComponentSystem.setStateValue` остаётся helper-ом без script side effects для нормализации, редактора и тестов. @@ -660,7 +660,7 @@ Static и Actor могут содержать скриптовые событи > Примечание: События _Always_ и _OnCollide_ зарезервированы в дизайне, но на текущий момент технически не реализованы в движке. -Parser command DSL поддерживает переиспользуемые runtime actions для authored-команд: проверку наличия точного объекта в scope (`requireEntityAvailable`), проверку одного из нескольких допустимых объектов (`requireAnyEntityAvailable`), изменение State (`setEntityState`), включение/выключение группы объектов (`setGroupDisabled`) и запуск/остановку скриптов (`runScript`/`stopScript`). Текущий пример: `TURN ON TV` / `TURN TV ON` требует видимый `tv` и пульт `tv_rc` у игрока или рядом; `TURN OFF TV` / `TURN TV OFF` требует пульт или reachable+visible `tv`. При успехе команды меняют только `tv.power` и выводят текст, а визуальные блики включаются/выключаются обычным State script event `state:power -> tv_power_changed`. +Parser command DSL и shared actor-aware command runtime поддерживают переиспользуемые runtime actions для authored-команд: проверку наличия точного объекта в scope (`requireEntityAvailable`), проверку одного из нескольких допустимых объектов (`requireAnyEntityAvailable`), изменение State (`setEntityState`), включение/выключение группы объектов (`setGroupDisabled`) и запуск/остановку скриптов (`runScript`/`stopScript`). Текущий пример: `TURN ON TV` / `TURN TV ON` требует видимый `tv` и пульт `tv_rc` у игрока или рядом; `TURN OFF TV` / `TURN TV OFF` требует пульт или reachable+visible `tv`. При успехе команды меняют только `tv.power` и выводят текст, а визуальные блики включаются/выключаются обычным State script event `state:power -> tv_power_changed`. Эти же authored commands могут быть выполнены и другими Actor, например через Puppet Master, если команда присутствует в их runtime context. ## Текстовые ассеты (TA) @@ -670,7 +670,7 @@ Parser command DSL поддерживает переиспользуемые run 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 должна сверить такую заметку с текущей моделью мира и заменить или очистить её, если она устарела. +Если обычная runtime-операция реально затрагивает объект с PN или его содержимое (`TAKE`, `PUT`, `OPEN`, `CLOSE` и т.п.), движок не редактирует текст PN сам, а помечает её `parserNoteNeedsCheck: true`. LLM должна сверить такую заметку с текущей моделью мира и заменить или очистить её, если она устарела. Этот механизм относится ко всем Actor-инициированным действиям, а не только к командам игрока. LLM-контекст сцены также содержит короткую runtime-only мини-историю текущего визита: до 8 последних команд игрока и player-facing ответов парсера (`context.scene.recentTurns`). Ответы урезаются до 85 символов. Эта история нужна для локальной conversational continuity, не сохраняется в файлы сцен и очищается при новом входе в сцену после ухода. diff --git a/NPCsys.md b/NPCsys.md new file mode 100644 index 00000000..b023cf36 --- /dev/null +++ b/NPCsys.md @@ -0,0 +1,61 @@ +Одной из главных особенностей (USP) игры является система умных NPC, контролируемых ИИ (LLM), которые отыгрывают свои роли, и с которыми можно общаться естественным языком. ИИ управляет перемещениями Actor, и его действиями, поэтому теоретически NPC могут всё то же самое, что и персонаж игрока. +Чтобы у Actor появилась управляемая ИИ "личность", ему надо присвоить компонент "NPC". + +За управление NPC и диалоги с ними отвечает "кукловод" -- модуль Puppet Master (PM) на базе LLM. Он отыгрывает, "role-play" каждого NPC используя: + +* Свой промпт с инструкцией; +* Scene Context -- описание сцены от World Builder, сходное с тем, получает Parser но гораздо более компактное (мы используем кэш, если LLM провайдер поддердивает, так что части 1-2 для сцены в оптимальном варианте токенизируются один раз). +* Описание каждого NPC из их TA (поле "Lore") +* Список текущих целей каждого NPC из их TA (новое поле "Objectives") +* Память NPC о предыдущих событиях (см. ниже) + +Примечание: подаваемый на вход LLM промпт надо оптимизировать с учётом того, что повторяющиеся данные могут кэшироваться провайдером LLM. + +## Диалоговая модель + +Любой NPC в игре может произносить реплики, которые слышат все остальные NPC, находящиеся в текущей локации (включая персонажа игрока). Если игрок хочет что-то сказать, в самом начале своей команды он должен указать символ "-". Весь остаток строки будет считаться текстом реплики. Можно также использовать команду SAY. На самом деле "- Hello" это просто более удобная форма команды "SAY Hello". +Свои и чужие реплики игрок видит в консоли другим цветом (отличающимся от цвета команд и системных сообщений), в формате "Имя: Реплика\n". + +### Лог сцены, реплики, действия + +Есть временный текстовый *лог сцены*, куда попадают все *реплики* и *действия*, совершённые NPC и игроком в этой сцене, до того, как они будут обработаны ИИ. +В качестве "действий" выступают строки текста в квадратных скобках, типа "[вежливо улыбается]", или "[внимательно просматривает пропуск Майлза и наконец возвращает обратно]". Эти описания действий могут генерироваться: + + 1. LLM в ходе написания реплик для персонажей как часть этих реплик или "flawor text". + 2. движком для значимых действий. Это просто сокращённые и обезличенные аналоги некоторых сообщений, которые игрок получает от парсера в ответ на свои команды, вызывающие изменение мира или состояния. + +КОМАНДА | ЧТО ВИДИТ ИГРОК В КОНСОЛИ | ЧТО ПИШЕТСЯ В ЛОГ СЦЕНЫ +-------------------------------- | ------------------------------ | ----------------------- +GIVE ID TO SECURITY OFFICER |"Ты протянул пропуск охраннику" | "[Майлз передал пропуск охраннику]" | + +* Hello there! |You: Hello there! | Miles: Hello there! + +Все записиси в логе имеют метку времени (на основании текущего игрового времени). Они хранятся временно, в течении заданного интервала, например 10 минут, по истечении которого удаляются. Также в логе хранится контрольная отметка времени последней обработки лога со стороны +Лог сцен сохраняется вместе с игрой. + +В целом, лог сцены похож на облегчённую версию того, что игрок видит в своей консоли. Но, в отличии от консоли, в лог попадают исключительно значимые для мира и модели события, произошедшие конкретно в этой сцене. +В лог попадают не вообще все события, а лишь те, что известны хотя бы одному NPC, в том числе те, про которые игроку не известно. Записи лога, известные не всем, имеют спец. аттрибут со списком NPC которые знают про событие. + +При появлении новых реплик PM вызывается для обработки сразу всех присутствующих в сцене NPC одним вызовом. Он обрабатывает ислючительно те реплики, что в логе находятся ПОСЛЕ контрольной отметки времени (хотя видит весь лог сцены). После обработки контрольная отметка обновляется. +За одну сессию PM их NPC могут сказать только одну реплику. + +В ходе сессии обработки PM решает какие из NPC сцены должны что-то ответить, пишет их ответы (возможно вместе c flawor text) а также strucured Plan действий, котоыре те должны совершить. Если NPC имеет что сказать, как минимум plan будет включать команду SAY. Если NPC должен реально взаимодействовать с миром, PM может использовать `MOVE_TO`, `TAKE`, `PUT`, `COMMAND` и fallback `USE`; при наличии подходящей authored-команды на объекте предпочтителен `COMMAND`, потому что именно он исполняет авторский runtime-контракт без лишней текстовой маршрутизации. `PUT` используется для выкладывания, dropping и перемещения предметов `on`/`in`/`under`/`behind` цели через общий actor-aware runtime. + +Plan выполняется отдельно, и в целом это унифицировано для всех Actor, в том числе персонажа игрока. То есть, когда игрок что-то говорит, перемещается по клику мыши или поднимает предмет, его Actor самом деле отрабатывает точно такой же Plan, что и другие. C точки зрения модели мира все Actor равноправны, в том числе персонаж игрока. + +## NPC Memory + +Это персистентная область заметок NPC, куда PM может заносить самые значимые фразы, факты, сведения, которые NPC необходимо помнить. Эти заметки хранятся независимо от того, в какой сцене NPC находится. +Если бы этой области не было, NPC бы помнили лишь то, что есть в логе сцены, и даже это бы забывали перейдя в дпугую локацию. + NPC Memory сохранятся вместе с игрой. + +## Вызов PM + +PM вызывается: + + 1. Для пакетной обраюотки NPC когда в логе сцены имеются необработанные реплики/действия. При этом, он не вызывается когда у фразы/действия нет NPC-слушателей. Допустим, если в сцене есть игрок и NPC, и NPC произнёс реплику, эту реплика не считается основанием для вызова PM, поскоьку тот NPC, котоырй её произнёс, не является её слушателем и не должен на неё реагировать (это вызвало бы бесконечную петлю), а игрок это не NPC и PM ему не нужен. То же самое, если в сцене есть лишь один игрок, который произносит реплику, либо совершает действие, которое видеть/слышать некому. + + 2. Для индивидуальной обработки одного NPC по событию. Такими событиями могут быть: + *Конец отработки перемещения по MoveTo для NPC, задавнного PM. То есть, NPC либо дошёл до нужной точки, либо столкнулся с препятствием, и есть необходимость обработать ситуация и возможно дать ему другое задание. + * По таймеру, заданному PM для этого персонажа (WAIT) + * При приближении объектов с заданными тэгами на заданное расстояние (например, ИИ кошки активируется и будет реагировать, если к ней приблится любая собака). diff --git a/Parser.md b/Parser.md index c1f33572..c4c47551 100644 --- a/Parser.md +++ b/Parser.md @@ -22,7 +22,7 @@ - **вся языковая интерпретация живёт внутри parser-а**; - `Game` и runtime не понимают язык игрока и не резолвят текстовые цели; - `Game` только исполняет операции над уже понятными сущностями и возвращает structured outcomes; -- parser — не единственный клиент `Game API`: тем же shared API пользуются UI, scripts и игровая логика. +- parser — не единственный клиент `Game API`: тем же shared API пользуются UI, scripts, игровая логика и actor-aware runtime исполнители вроде Puppet Master. --- @@ -572,7 +572,7 @@ Parser Notes — это приватная память LLM/GM-каскада, Принцип: - parser — один из клиентов `Game API`, а не его единственный владелец; -- тем же API могут пользоваться UI, scripts и игровая логика; +- тем же API могут пользоваться UI, scripts, игровая логика и actor-aware runtime исполнители; - parser передаёт в `Game` уже resolved цели; - `Game` не подбирает объекты по тексту; - `Game` не делает disambiguation; diff --git a/Sessions.md b/Sessions.md index 2182b9ea..49930a9e 100644 --- a/Sessions.md +++ b/Sessions.md @@ -2353,3 +2353,87 @@ Refining 3D Spatial Audio for the engine, ensuring that sound triggering, pannin - `Sessions.md` already had unrelated pre-existing content before this entry. - `public/scenes/home/room.json` also has unrelated pre-existing edits and was intentionally left out of the commit. - The fix is ingress-based; if prompt regressions continue, a second output-repair guard may still be worth considering later. + +--- + +## Session Entry - 2026-06-02 11:39 +02:00 + +### Session Goals +- Implement the major Puppet Master / Actor Actions feature slice so NPCs can do real world actions instead of only narrating intent. +- Make NPC movement respect the same walkability/collider constraints as the player while still allowing zero-collider objects to remain nonblocking. +- Let NPCs approach reachable positions near target objects instead of trying to walk onto object centers outside walkboxes. +- Reduce Puppet Master context noise by exposing only semantically meaningful scene objects plus special technical floor fallback objects. +- Add actor-aware command execution so authored commands can be executed by any Actor without routing NPCs through the text parser or LLM parser cascade. +- Update the documentation and durable knowledge sources after the architecture changed. + +### What Was Implemented +- Added real Puppet Master action execution for NPC plans beyond speech/objective updates, including movement completion and action completion loops. +- Added `TAKE` support for NPCs so they can actually pick up takeable visible entities into their own inventory. +- Added `COMMAND` support for PM plans, allowing NPCs to execute authored command plans by `commandId`. +- Added fallback `USE itemId ON targetId` support for actor plans while preserving existing player `USE` no-effect fallback behavior. +- Added shared actor-aware command execution through the new actor command/runtime path so player parser and PM can converge on the same underlying world actions. +- Added per-object command affordances to NPC world context: objects such as `tv` can list theoretically applicable authored commands like `turn_tv_on` and `turn_tv_off`, including compact prerequisites and state effects. +- Updated the PM prompt so authored `COMMAND` is preferred when listed on an object, while generic `USE` remains fallback. +- Added guardrails to prevent PM from claiming unsupported physical/state changes as already done. +- Added the continuation trigger for PM plans that update memory/objectives without scheduling follow-up action, preventing NPCs from getting stuck after setting a goal. +- Reduced NPC context noise by filtering visible entities to titled semantic objects, with an exception for technical `floor` fallback objects that correspond to walkable floor/storage placement. +- Kept zero-collider objects intentionally nonblocking, while nonzero colliders block NPC movement the same way they block the player. + +### Important Architecture / Runtime Decisions +- Authored command execution is now shared actor-aware runtime behavior, not a parser-only concern. +- NPCs must not send natural-language `RUN_COMMAND` text into the real parser pipeline. PM emits structured DSL steps such as `COMMAND` and `USE`; the engine executes already-authored command plans as data. +- `COMMAND` is preferred when a visible entity exposes a suitable authored command affordance because it can perform real state changes and side effects. +- `USE` is a generic fallback action and should not guess complex authored intent when a matching `COMMAND` exists. +- `held`, `reachable`, `visible`, and command prerequisites are evaluated relative to the acting Actor, not implicitly relative to the player. +- PM world context should list commands on the specific objects they can target rather than as a global loose command list. +- "Theoretically executable" command affordance means the command can target the entity by authored command structure, even if prerequisites are not currently satisfied. +- Actor-aware `PUT` was identified as the next required PM action after the test NPC tried to place the TV remote on the desk but could only narrate intent or mistakenly retry `TAKE`. + +### Parser / Mechanics / Scene / NPC Changes +- `ActorCommandExecutor` / actor-facing command runtime became the shared place for authored command execution and fallback use behavior. +- `ActorPlanExecutor` was extended to handle PM `COMMAND` and `USE` action steps. +- `NpcWorldModelBuilder` now exposes compact command affordances on visible semantic entities. +- `NpcPuppetMaster` prompt and validation now understand `COMMAND` and `USE`. +- Player `USE X ON Y` was kept stable while being moved through the shared actor-facing path. +- PM context now includes item locations such as `TV remote` being `in NPC` or `on floor`, letting the model reason about possession and placement requests. +- The TV test path became the canonical validation scenario: Linda can take the remote, turn the TV on, turn it off, and understand command affordances on `tv`. + +### Documentation / Session-Handoff Work +- Ran a Gemini-assisted audit to find documentation that still described authored commands and semantic execution as player/parser-only. +- Updated `Commands.md` so authored commands are described as shared runtime content rather than parser-only assets. +- Updated `Parser.md` to clarify that `Game API` has actor-aware clients, including Puppet Master-style runtime execution. +- Updated `tech-spec.md` so `GameSemanticAPI` is framed as actor-aware semantic execution instead of only parser command resolution. +- Updated `GDD.md` to explain that semantic command execution now lives in a shared actor-aware runtime layer. +- Updated `NPCsys.md` to document PM `MOVE_TO`, `TAKE`, `COMMAND`, fallback `USE`, and the current `PUT` gap. +- Updated `public/text/system/parser-llm-system.md` to keep the player GM prompt aligned with the shared authored-command runtime model. +- Updated `public/text/system/npc-pm-system.md` first to document the `PUT` limitation, then implemented actor-aware `PUT` and updated the prompt again so NPCs can place/drop items for real. +- Removed the redundant local `dist/text/system/parser-llm-system.md` copy so `public/text/system/parser-llm-system.md` remains the single source of truth. +- Synced the shared memory mirror, regenerated curated `AgentMemory.md`, and replaced stale NotebookLM `Sessions.md`, `GDD.md`, and `AgentMemory.md` sources. + +### Tests / Validation +- Focused NPC Puppet Master and parser command tests passed during the actor actions implementation. +- Full test suite passed after the actor-actions code slice: `37 files`, `450 tests passed`. +- TypeScript validation passed with `npm run typecheck`. +- `git diff --check` passed on the documentation/wrap-up edits. +- Manual PM log testing confirmed: + - NPC movement no longer walks onto the TV/outside walkbox when nonzero colliders and reachability are respected. + - Linda can take the TV remote, execute `turn_tv_on`, execute `turn_tv_off`, and update objectives/memory in response. + - The missing `PUT` action is now visible as a real capability gap rather than a command-execution failure. + +### Commits +- `b584cda` - `feat: let NPCs run authored actor commands` + - Added shared actor-aware command execution, PM `COMMAND`/`USE`, per-object command affordances, player `USE` regression preservation, and tests. +- No new commit was created during the final documentation/wrap-up step; the documentation refresh is still in the working tree. + +### Remaining Work / Next Steps +- Commit the documentation refresh and updated session entry as part of the actor actions feature handoff. +- Implement actor-aware `PUT` so NPCs can place/drop/give items instead of only taking and using them. +- Update PM prompt and tests once `PUT` lands so NPCs do not overpromise item placement. +- Consider adding `currentlyUseful` / state-match hints to command affordances so objects like `tv` can expose both `turn_tv_on` and `turn_tv_off` while still helping the model choose the state-relevant one. +- Continue broadening actor parity so player and NPC actions converge on the same semantic runtime contracts. + +### Risks / Caveats +- Actor-aware `PUT` is now implemented for PM plans; future placement work should build on this shared semantic runtime path. +- The documentation refresh is not yet committed, so the working tree contains expected modified docs and the updated `Sessions.md`. +- `public/text/system/parser-llm-system.md` is the canonical source; `dist/` should remain a generated build artifact only. +- The actor command architecture intentionally avoids feeding NPC natural-language commands into the player parser to prevent extra LLM calls, player-centric context, console noise, and recursion. diff --git a/public/assets/girl.png b/public/assets/girl.png new file mode 100644 index 00000000..955d0921 Binary files /dev/null and b/public/assets/girl.png differ diff --git a/public/scenes/test_room.json b/public/scenes/test_room.json index 6762de85..f8b22485 100644 --- a/public/scenes/test_room.json +++ b/public/scenes/test_room.json @@ -59,6 +59,11 @@ "id": "test_1", "x": 555, "y": 214 + }, + { + "id": "tv_rc", + "x": 326, + "y": 247 } ] } @@ -1006,23 +1011,23 @@ "capacity": 9007199254740991, "groups": [], "protected": false, - "items": ["test", "tv_rc"], + "items": ["test"], "relation": "in" } ], "layer": 0, "visible": true, "hidden": false, - "parallax": 1.0409334104436379, - "x": 839.2252670877428, - "y": 248.0374520832024, - "width": 119.88, - "height": 289.34, - "baseWidth": 162, - "baseHeight": 391, + "parallax": 1.039692645910645, + "x": 743.936959170698, + "y": 246.12667470239316, + "width": 71.03999999999999, + "height": 290.08, + "baseWidth": 96, + "baseHeight": 392, "colliderWidth": 88, "colliderHeight": 4, - "spriteName": "miles_ds-idle-down.json", + "spriteName": "miles_ds-idle-right.json", "color": "#00ffff", "scale": 0.74, "refScale": 0.74, @@ -1034,7 +1039,7 @@ "blur": 0, "isPlayer": true, "speed": 0.24, - "direction": "down", + "direction": "left", "animSets": { "idle": { "id": "idle", @@ -1417,7 +1422,7 @@ "hidden": false, "parallax": 1.0791811402100033, "x": 223.0428633931454, - "y": 306.93895592340493, + "y": 306.938955923405, "width": 1008.8000000000001, "height": 90.39999999999999, "baseWidth": 1261, @@ -1574,29 +1579,29 @@ "visible": true, "hidden": false, "parallax": 1, - "x": 776.9860395378569, - "y": 228.97896068838534, + "x": 681.6156395767036, + "y": 227.06665183791887, "ignoreScaling": false, "vertices": [ { - "x": 776.9860395378569, - "y": 228.97896068838534, - "p": 1.0285224905588148 + "x": 681.6156395767036, + "y": 227.06665183791887, + "p": 1.0272476933667722 }, { - "x": 871.0127356977046, - "y": 227.92308025108423, - "p": 1.0278611077054283 + "x": 774.8363353771408, + "y": 226.0209967142044, + "p": 1.0265927557234915 }, { - "x": 920.0042056975744, - "y": 259.5373424015853, - "p": 1.0484008716893412 + "x": 848.8943259125697, + "y": 257.31881232273514, + "p": 1.046960267742036 }, { - "x": 866.4916523175558, - "y": 258.11861792043845, - "p": 1.0474796220262588 + "x": 794.2593990178761, + "y": 255.91434237135977, + "p": 1.0460482742671169 } ], "color": "#2b019d", @@ -1842,7 +1847,7 @@ "id": "power", "valueType": "string", "initialValue": "off", - "value": "on", + "value": "off", "parserNoteTextAssets": { "off": "power_off", "on": "power_on" @@ -2083,7 +2088,7 @@ "name": "tv_anim", "type": "Entity", "locked": false, - "disabled": false, + "disabled": true, "groupID": "#tv_glow", "customName": "", "textRedirects": {}, @@ -2120,7 +2125,7 @@ "name": "tv_anim_glow_1", "type": "Quad", "locked": false, - "disabled": false, + "disabled": true, "groupID": "#tv_glow", "customName": "", "textRedirects": {}, @@ -2158,7 +2163,7 @@ ], "color": "#52779aff", "sortMode": "ignore", - "opacity": 0.28553271858866913, + "opacity": 0.24953602443603395, "blendMode": "lighter", "isGrid": false, "gridLinesX": 5, @@ -2172,7 +2177,7 @@ "name": "tv_anim_glow", "type": "Quad", "locked": false, - "disabled": false, + "disabled": true, "groupID": "#tv_glow", "customName": "", "textRedirects": {}, @@ -2210,7 +2215,7 @@ ], "color": "#52779aff", "sortMode": "ignore", - "opacity": 0.28553271858866913, + "opacity": 0.24953602443603395, "blendMode": "lighter", "isGrid": false, "gridLinesX": 5, @@ -2224,7 +2229,7 @@ "name": "tv_aim_glow3", "type": "Quad", "locked": false, - "disabled": false, + "disabled": true, "groupID": "#tv_glow", "customName": "", "textRedirects": {}, @@ -2262,7 +2267,7 @@ ], "color": "#7c62ac", "sortMode": "ignore", - "opacity": 0.28553271858866913, + "opacity": 0.24953602443603395, "blendMode": "screen", "isGrid": false, "gridLinesX": 5, @@ -2294,17 +2299,17 @@ "relation": "in" }, "parallax": 1, - "x": 839.2252670877428, - "y": 248.0374520832024, - "width": 15.368666227146264, - "height": 21.839683585944687, + "x": 743.936959170698, + "y": 246.12667470239316, + "width": 15.333005334145621, + "height": 21.78900758010167, "baseWidth": 19.69986357435198, "baseHeight": 27.994542974079128, "colliderWidth": 0, "colliderHeight": 0, "spriteName": null, "color": "#AAAAAA", - "scale": 0.7801407440788234, + "scale": 0.7783305339285831, "refScale": 0.8, "modelScale": 0.8, "ignoreScaling": false, @@ -2328,24 +2333,24 @@ } ], "layer": 0, - "visible": false, + "visible": true, "hidden": false, "spatial": { - "parentNodeId": "Hero_1", - "relation": "in" + "parentNodeId": "Walk_main", + "relation": "on" }, "parallax": 1, - "x": 839.2252670877428, - "y": 248.0374520832024, - "width": 25.35457418256176, - "height": 37.3979969192786, + "x": 326, + "y": 247, + "width": 25.322631578947366, + "height": 37.350881578947366, "baseWidth": 199.99999999999997, "baseHeight": 295, "colliderWidth": 0, "colliderHeight": 0, "spriteName": "tv_rc", "color": "#AAAAAA", - "scale": 0.1267728709128088, + "scale": 0.12661315789473684, "refScale": 0.13, "modelScale": 0.13, "ignoreScaling": false, @@ -2353,6 +2358,54 @@ "opacity": 1, "blendMode": "source-over", "blur": 0 + }, + { + "name": "NPC", + "type": "Actor", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Actor" + }, + { + "type": "NPC", + "enabled": true, + "memory": "Hero_1 asked me to take the remote and turn on the TV. I'm helping them get the Muppet Show started.", + "objectives": ["TV is ON"], + "objectivesInitializedFromTA": true + } + ], + "layer": 0, + "visible": true, + "hidden": false, + "parallax": 1.0746753892935719, + "x": 1168.0012191544956, + "y": 300.00009951210046, + "width": 146.9613259668508, + "height": 282.37569060773495, + "baseWidth": 146.9613259668508, + "baseHeight": 282.37569060773495, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "girl", + "color": "#36d87fff", + "scale": 1, + "refScale": 1, + "modelScale": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0, + "isPlayer": false, + "speed": 0.1, + "direction": "down", + "animSets": {} } ], "folders": [ @@ -2441,5 +2494,82 @@ "panningModel": "HRTF", "distanceModel": "linear", "defaultReverbIR": "/sounds/ir/room_drum_medium.wav" + }, + "sceneLog": { + "entries": [ + { + "id": "1780248398951-1", + "kind": "speech", + "timestamp": 1780248398951, + "actorId": "Hero_1", + "displayName": "Hero_1", + "text": "Hello", + "knownByNpcIds": ["NPC"] + }, + { + "id": "1780248400642-2", + "kind": "speech", + "timestamp": 1780248400642, + "actorId": "NPC", + "displayName": "Linda", + "text": "Oh, hi there! I was just about to put on the Muppet Show. Have you seen it?", + "knownByNpcIds": [] + }, + { + "id": "1780248413407-3", + "kind": "speech", + "timestamp": 1780248413407, + "actorId": "Hero_1", + "displayName": "Hero_1", + "text": "Not Yet", + "knownByNpcIds": ["NPC"] + }, + { + "id": "1780248415193-4", + "kind": "speech", + "timestamp": 1780248415193, + "actorId": "NPC", + "displayName": "Linda", + "text": "Oh, you should really stick around and watch it with me then! It's starting soon.", + "knownByNpcIds": [] + }, + { + "id": "1780248447688-5", + "kind": "speech", + "timestamp": 1780248447688, + "actorId": "Hero_1", + "displayName": "Hero_1", + "text": "Ok. Come closer to me", + "knownByNpcIds": ["NPC"] + }, + { + "id": "1780248449472-6", + "kind": "speech", + "timestamp": 1780248449472, + "actorId": "NPC", + "displayName": "Linda", + "text": "Sure! Let's sit together and watch the show. It's about to start!", + "knownByNpcIds": [] + }, + { + "id": "1780248520675-7", + "kind": "speech", + "timestamp": 1780248520675, + "actorId": "Hero_1", + "displayName": "Hero_1", + "text": "Please take the rc and turn tv on", + "knownByNpcIds": ["NPC"] + }, + { + "id": "1780248522411-8", + "kind": "speech", + "timestamp": 1780248522411, + "actorId": "NPC", + "displayName": "Linda", + "text": "Of course! Let me grab that remote for you.", + "knownByNpcIds": [] + } + ], + "lastPmProcessedAt": 1780248522411 } } diff --git a/public/sprites/girl.json b/public/sprites/girl.json new file mode 100644 index 00000000..498717fd --- /dev/null +++ b/public/sprites/girl.json @@ -0,0 +1,9 @@ +{ + "id": "girl", + "imageFile": "public/assets/girl.png", + "x": 0, + "y": 0, + "width": 100, + "height": 222, + "frames": 1 +} diff --git a/public/text/objects/NPC.json b/public/text/objects/NPC.json new file mode 100644 index 00000000..6e53b1a3 --- /dev/null +++ b/public/text/objects/NPC.json @@ -0,0 +1,9 @@ +{ + "title": "Linda", + "description": "You see a young woman.", + "details": "She has short, messy brown hair. She is wearing a t-shirt with a faded band logo and jeans. A worn-looking messenger bag is slung over her shoulder. She appears to be waiting for someone or something.", + "lore": "Linda wants to watch her favorite TV show -- Muppet show.", + "objectives": ["TV is ON"], + "takeFailure": "Linda is quite heavy. Besides, what would you do with her?", + "synonyms": ["girl"] +} diff --git a/public/text/system/commands/use_on.json b/public/text/system/commands/use_on.json index 564fe18e..c4fb1fcb 100644 --- a/public/text/system/commands/use_on.json +++ b/public/text/system/commands/use_on.json @@ -32,12 +32,10 @@ { "type": "resolveArgumentEntity", "arg": "item", "saveAs": "use_item" }, { "type": "resolveArgumentEntity", "arg": "target", "saveAs": "use_target" }, { - "type": "showText", - "messageId": "no_effect_pair", - "paramsFromRefs": { - "item": "use_item", - "target": "use_target" - } + "type": "actorUseOn", + "itemRef": "use_item", + "targetRef": "use_target", + "noEffectMessageId": "no_effect_pair" } ], "messages": { diff --git a/public/text/system/npc-pm-system.md b/public/text/system/npc-pm-system.md new file mode 100644 index 00000000..1725a5a8 --- /dev/null +++ b/public/text/system/npc-pm-system.md @@ -0,0 +1,41 @@ +You are the Puppet Master for NPCs in a retro adventure game. + +You role-play the NPCs listed in the context. Each NPC has its own knowledge, lore, objectives, and memory. Do not let one NPC use facts that are only available to another NPC. + +Respond with exactly one JSON object and no extra text: + +{ +"kind": "pm_response", +"plans": [ +{ +"npcId": "real_npc_id", +"steps": [ +{ "type": "SAY", "text": "short in-character line" }, +{ "type": "PUT", "itemId": "object_id", "targetId": "object_id_or_null", "relation": "on" }, +{ "type": "COMMAND", "commandId": "authored_command_id", "arguments": {} }, +{ "type": "OBJECTIVES_SET", "objectives": ["current goal"] } +], +"memory": "optional durable note for that NPC" +} +] +} + +Supported steps: + +- SAY: make the NPC speak once. +- MEMORY_SET: replace that NPC's durable memory note. +- OBJECTIVES_SET: replace that NPC's current runtime objectives. Use an empty array only when the NPC intentionally has no current objectives. +- WAIT: pause this NPC for a number of milliseconds; when the timer elapses, you will be called again for that NPC with a wait_elapsed trigger. +- MOVE_TO: move this NPC to a point or visible entity. Use either `{ "type": "MOVE_TO", "x": 100, "y": 200 }` or `{ "type": "MOVE_TO", "targetId": "object_id" }`. With `targetId`, the engine moves the NPC to the nearest walkable position from which the target can be approached/reached, not onto the object's center. When movement ends, you will be called again for that NPC with a move_completed trigger containing the move result. +- TAKE: make this NPC take a visible takeable entity into their own inventory. Use `{ "type": "TAKE", "targetId": "object_id" }`. Use this only after the NPC is close enough to reach the target. When the action finishes, you will be called again for that NPC with an action_completed trigger containing the action result. +- PUT: make this NPC place or drop a held or reachable entity. Use `{ "type": "PUT", "itemId": "held_or_reachable_item_id", "targetId": "target_object_id", "relation": "on" }`. Use `targetId: null` to drop/place it on the current reachable floor. Valid relations are `on`, `in`, `under`, `behind`, or `null`. When the action finishes, you will be called again for that NPC with an action_completed trigger containing the action result. +- COMMAND: execute an authored command listed on a visible entity. Use `{ "type": "COMMAND", "commandId": "turn_tv_on", "arguments": {} }`. Prefer this when an entity lists a suitable command because authored commands can perform real state changes and side effects. When the action finishes, you will be called again for that NPC with an action_completed trigger containing the command result. +- USE: fallback item-on-target action. Use `{ "type": "USE", "itemId": "held_or_reachable_item_id", "targetId": "target_object_id" }` only when no listed authored COMMAND fits. When the action finishes, you will be called again for that NPC with an action_completed trigger containing the action result. + +In the current engine slice, reliable actions are SAY, MEMORY_SET, OBJECTIVES_SET, WAIT, MOVE_TO, TAKE, PUT, COMMAND, and USE. Use MOVE_TO when physical repositioning matters for the NPC's current objective. Use TAKE when the NPC should actually pick up a takeable object. Use PUT when the NPC should place, drop, or move an item into/on/under/behind a target. Prefer COMMAND when the target entity lists a relevant command; use USE only as a generic fallback. + +OBJECTIVES_SET and MEMORY_SET only update internal NPC state. They do not perform work and they do not by themselves create a future movement/completion event. If you set a new objective that requires physical action, include the next concrete WAIT or MOVE_TO step in the same plan whenever possible. + +Do not claim that an unsupported physical action has already happened. In this slice you cannot actually OPEN or press buttons unless a supported COMMAND or other step explicitly does it. You may say what the NPC is about to do, move toward the relevant object, TAKE a takeable object, PUT an item somewhere, run a listed COMMAND, use a held item on a target, wait, or update memory/objectives honestly. + +Keep speech concise, in character, and responsive to the unread scene log. If no NPC should respond, return an empty plans array. diff --git a/public/text/system/parser-llm-system.md b/public/text/system/parser-llm-system.md index e96b8bd1..f0165ae8 100644 --- a/public/text/system/parser-llm-system.md +++ b/public/text/system/parser-llm-system.md @@ -9,7 +9,7 @@ You bring the world to life. You interpret what the player wants, respond with v ## Responsibilities - Generate short atmospheric responses when the player needs narration, reaction, refusal, flavor, or a harmless no-result attempt. -- Interpret commands the simpler parser layers could not understand. +- Interpret player commands the simpler parser layers could not understand. - Map creative phrasing to concrete game actions when the action is a faithful executable equivalent: it preserves the player's object, target, and intended world result even if the low-level verb is not literal. - If the player's intent is recognized but no faithful executable equivalent fits that intent, invent a short atmospheric and logical Game Master response instead of calling a merely adjacent or unrelated standard action. - Seed NPC-style responses when the player tries to talk to or interact with characters. @@ -179,7 +179,7 @@ For a game command: { "kind": "plan", "actions": [ { "type": "..." } ] } ``` -You may act as Game Master by using either `Direct Game Master world actions` or `Available authored parser commands`. Use action objects with the fields at the top level exactly like the `action` examples; do not wrap action fields inside a `fields` object. Use `runCustomCommand` when an authored command is the best fit, especially for equivalent wording, shortened phrases, or reordered phrases. Use direct world actions when the authored command is not quite right, when you need a different sequence, or when direct State/group/script control is the more faithful response. Prefer real state-changing actions over merely narrating a successful state change with `showText` or `final_response`. If your player-facing text says an object turned on/off, opened/closed, started/stopped, or otherwise changed persistent state, include the corresponding direct world action or authored command in the same plan. Objects may list `state:` interactions; those are authored scripts that run automatically after a matching `setEntityState`, so setting the State is enough unless another explicit effect is also needed. +You may act as Game Master by using either `Direct Game Master world actions` or `Available authored parser commands`. Use action objects with the fields at the top level exactly like the `action` examples; do not wrap action fields inside a `fields` object. Use `runCustomCommand` when an authored command is the best fit, especially for equivalent wording, shortened phrases, or reordered phrases. Use direct world actions when the authored command is not quite right, when you need a different sequence, or when direct State/group/script control is the more faithful response. Prefer real state-changing actions over merely narrating a successful state change with `showText` or `final_response`. If your player-facing text says an object turned on/off, opened/closed, started/stopped, or otherwise changed persistent state, include the corresponding direct world action or authored command in the same plan. Objects may list `state:` interactions; those are authored scripts that run automatically after a matching `setEntityState`, so setting the State is enough unless another explicit effect is also needed. Authored command execution is shared runtime behavior; the player parser is one client of it, not the only place where those actions may be executed. For conversation, atmosphere, reactions, or when no safe action fits and you are not creating or updating a persistent Parser Note: diff --git a/src/components/ConsoleOverlay.tsx b/src/components/ConsoleOverlay.tsx index d507db91..d792b06b 100644 --- a/src/components/ConsoleOverlay.tsx +++ b/src/components/ConsoleOverlay.tsx @@ -124,7 +124,14 @@ export const ConsoleOverlay: React.FC = ({ game }) => { key={i} style={{ marginBottom: '4px', - color: line.type === 'command' ? '#aaa' : line.type === 'error' ? '#f55' : '#fff', + color: + line.type === 'command' + ? '#aaa' + : line.type === 'error' + ? '#f55' + : line.type === 'dialogue' + ? '#7dd3fc' + : '#fff', whiteSpace: 'pre-wrap', overflowWrap: 'break-word', userSelect: 'text', diff --git a/src/components/UIOverlay.tsx b/src/components/UIOverlay.tsx index 6727646f..cbb7acfb 100644 --- a/src/components/UIOverlay.tsx +++ b/src/components/UIOverlay.tsx @@ -269,16 +269,7 @@ export const UIOverlay: React.FC = ({ game }) => { if (firstWord.startsWith('#')) { game.console.processCommand(val); } else { - const preprocessed = game.console.preprocessGameplayInput(val); - - // 1. Log Command to Buffer - game.console.log(preprocessed, 'command'); - - // 2. Add to History - game.console.addHistory(preprocessed); - - // 3. Send to gameplay parser - void game.parser.parse(preprocessed); + void game.submitGameplayInput(val); } e.currentTarget.value = ''; diff --git a/src/components/editor/properties/SectionComponents.tsx b/src/components/editor/properties/SectionComponents.tsx index a4762a1f..a1a78757 100644 --- a/src/components/editor/properties/SectionComponents.tsx +++ b/src/components/editor/properties/SectionComponents.tsx @@ -218,7 +218,12 @@ export const SectionComponents: React.FC = () => { { value: 'WalkBox', label: 'WalkBox (Collider)' }, ] : []), - ...(selectedObjectType === 'Actor' ? [{ value: 'Shadow', label: 'Shadow' }] : []), + ...(selectedObjectType === 'Actor' + ? [ + { value: 'NPC', label: 'NPC' }, + { value: 'Shadow', label: 'Shadow' }, + ] + : []), ].map((opt) => ({ ...opt, icon: getIconUrl(opt.value) }))} placeholder="+ Add Component" onChange={(value) => { @@ -320,6 +325,18 @@ export const SectionComponents: React.FC = () => { offsetY: 0, triggerId: '', }); + } else if (type === 'NPC') { + const initialObjectives = game.textAssets.getResolvedObjectListField( + o, + 'objectives' + ); + o.components.push({ + type: 'NPC', + enabled: true, + memory: '', + objectives: initialObjectives, + objectivesInitializedFromTA: true, + }); } else if (type === '3d-parallax') { o.components.push({ type: '3d-parallax' }); } else if (type === 'WalkBox') { @@ -424,6 +441,71 @@ export const SectionComponents: React.FC = () => { )} + {comp.type === 'NPC' && ( + <> +
+ Enables Puppet Master dialogue, NPC memory, and runtime objectives. +
+
+ +
+
+ +