diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3bbb85b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# Project Instructions + +## Autotests Recall Rule + +When working on mechanics/runtime-related code or architecture-sensitive changes in: +- `src/mechanics` +- `src/scene` +- `src/systems` +- `src/core` + +and especially on: +- parser behavior; +- `Game` semantic API behavior; +- 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 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. + +## Autotests Maintenance Rule + +When making significant functional changes or adding important new behavior in mechanics/runtime code: +- 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. diff --git a/Autotests.md b/Autotests.md new file mode 100644 index 0000000..831d99b --- /dev/null +++ b/Autotests.md @@ -0,0 +1,399 @@ +# Autotests + +## Purpose + +This document describes the current automated test setup on the `autotests` branch. + +The first iteration is intentionally narrow: +- deterministic parser behavior; +- parser core contracts; +- direct `Game` semantic API behavior; +- scene runtime behavior around spatial hierarchy and subscenes; +- one thin parser + game integration layer. + +This setup is meant to protect the most fragile gameplay contracts without introducing heavy browser or UI end-to-end coverage. + +## Current Stack + +- Test runner: `vitest` +- Environment: `node` +- Command: + +```bash +npm run test +``` + +Type safety check: + +```bash +npm run typecheck +``` + +## Design Principles + +The current autotest system is built around a few constraints: + +- Tests should not depend on large mutable game-content scenes. +- Tests should use small deterministic fixtures. +- Tests should target architecture layers directly: + - parser; + - parser core; + - scene runtime; + - subscene behavior. +- Tests should be readable enough to act as executable architecture documentation. + +Out of scope for this iteration: +- full browser Playwright coverage; +- full UI/canvas assertions; +- LLM-stage testing; +- using live content scenes as the main source of truth. + +## File Layout + +```text +tests/ + fixtures/ + gameSemanticFactory.ts + gameFactory.ts + parserFactory.ts + sceneFactory.ts + textAssetFactory.ts + game/ + navigation-and-spatial.test.ts + semantic-api.test.ts + parser/ + commands.test.ts + core.test.ts + resolution.test.ts + scene/ + spatial-index.test.ts + subscene-activation.test.ts + subscene-cleanup.test.ts + integration/ + parser-game.test.ts +vitest.config.ts +``` + +## Fixture System + +The tests use programmatic fixtures instead of real scene files. + +### `tests/fixtures/textAssetFactory.ts` + +Provides a minimal in-memory text layer for tests: +- object titles, descriptions, details, synonyms; +- scene title and description; +- parser service strings; +- parser lexicon; +- parser training data; +- parser command specs. + +Use this when a test needs stable text assets without relying on `public/text/...`. + +### `tests/fixtures/gameFactory.ts` + +Provides a minimal `IGame`-compatible harness: +- captured player-facing messages; +- captured logs; +- captured played sounds; +- minimal `sceneManager`; +- minimal `textAssets`. + +This is the base semantic harness used by scene and parser tests. + +### `tests/fixtures/gameSemanticFactory.ts` + +Builds on top of `gameFactory.ts` and exposes the real `Game` semantic API methods through `Game.prototype`, while still avoiding full `Game` construction and UI bootstrap. + +This fixture exists specifically for direct `Game`-layer contract tests. + +Use it when the goal is to test: +- `lookScene` +- `lookEntity` +- `examineEntity` +- `showInventory` +- `removeInventoryEntity` +- `goToSceneTarget` +- `goToScene` +- `goToEntity` +- `describeSpatialRelation` + +without pulling parser behavior into the assertion. + +### `tests/fixtures/sceneFactory.ts` + +Builds a tiny `Scene` on top of the test game harness. + +Helpers include: +- `addEntity(...)` +- `addPlayer(...)` +- `addTriggerbox(...)` +- `addWalkbox(...)` + +This is the preferred way to build small deterministic runtime worlds for tests. + +### `tests/fixtures/parserFactory.ts` + +Builds a real `Parser` instance on top of the fixture game and scene. + +It wires the parser to a small semantic gameplay harness for: +- `lookScene` +- `lookEntity` +- `examineEntity` +- `takeEntity` +- `showInventory` +- `goToSceneTarget` +- `goToScene` +- `goToEntity` +- `removeInventoryEntity` +- `describeSpatialRelation` + +It also exposes: + +```ts +await fixture.run('look under chair') +``` + +which returns captured: +- `messages` +- `logs` +- `pendingIntent` + +This is the preferred entry point for parser-side tests. + +## Current Test Coverage + +### Scene Runtime + +#### `tests/scene/spatial-index.test.ts` + +Covers: +- parent/child spatial indexing; +- grouping by relation: + - `in` + - `on` + - `under` + - `behind` +- direct-child lookup staying non-recursive; +- legacy fallback: + - `parentNodeId + relation:null` behaves as `in` + +#### `tests/scene/subscene-activation.test.ts` + +Covers: +- direct entity child activation; +- direct triggerbox child activation; +- nested subscene becoming available; +- grandchildren not auto-activating; +- coexistence of: + - `targetGroupId` + - direct spatial children + +#### `tests/scene/subscene-cleanup.test.ts` + +Covers: +- `Switch` reset on subscene close; +- `sound1` playback path; +- cleanup for spatially included objects, not only group-based ones. + +### Parser + +#### `tests/parser/resolution.test.ts` + +Covers: +- exact resolution; +- synonym match; +- partial match; +- ambiguity clarification; +- deterministic tie-break: + - inventory first; + - nearest scene object when titles are indistinguishable. + +#### `tests/parser/commands.test.ts` + +Covers: +- `teleport` +- `teleport with id` +- wrong item for teleport -> no effect; +- `use id on boombox` +- missing-argument prompts for custom commands. + +#### `tests/parser/core.test.ts` + +Covers: +- pre-API handoff path; +- post-API escalation path; +- linear plan stopping after failure; +- core behavior independent of UI. + +### Game + +#### `tests/game/semantic-api.test.ts` + +Covers: +- `lookScene`; +- `lookEntity`; +- `examineEntity`; +- `showInventory`; +- `removeInventoryEntity`. + +This layer verifies `Game` as the shared semantic gameplay API, separate from parser parsing. + +#### `tests/game/navigation-and-spatial.test.ts` + +Covers: +- `goToSceneTarget`; +- `goToScene`; +- `goToEntity`; +- `describeSpatialRelation`. + +This layer is especially useful for validating the shared boundary between parser and world/game semantics. + +### Thin Integration + +#### `tests/integration/parser-game.test.ts` + +Covers a small end-to-end slice on tiny fixtures: +- `look under chair` +- far-but-visible `examine` + +This layer is intentionally small. + +## How To Run + +Run all tests: + +```bash +npm run test +``` + +Run typecheck: + +```bash +npm run typecheck +``` + +Run a specific test file with Vitest directly: + +```bash +npx vitest run tests/parser/commands.test.ts +``` + +Run tests in watch mode: + +```bash +npx vitest +``` + +## How To Add A New Test + +### Add a parser test + +If the behavior belongs to parser resolution, parser commands, or parser core: +- use `createParserFixture()` +- build the smallest world needed +- run parser input through `fixture.run(...)` +- assert on: + - player-facing messages; + - pending intent; + - scene/inventory side effects. + +Example: + +```ts +const fixture = createParserFixture(); +fixture.addPlayer(); +fixture.addEntity('chair', { title: 'Chair', description: 'A chair.' }); + +const result = await fixture.run('look chair'); + +expect(result.messages.at(-1)).toBe('A chair.'); +``` + +### Add a scene runtime test + +If the behavior belongs to scene/spatial/subscene runtime: +- use `createSceneFixture()` +- build the smallest spatial structure possible +- call runtime helpers or component activation directly +- assert on: + - enabled/disabled state; + - `activeSubscene`; + - `subsceneEntities`; + - switch state; + - played sounds. + +### Add a new parser command fixture + +If a test needs custom command data: +- reuse the default command fixtures already provided; +- or override command assets through: + +```ts +fixture.textAssets.setParserCommands([...]); +``` + +This keeps tests independent from `public/text/system/commands/*.json`. + +## Why Programmatic Fixtures Instead Of Real Scenes + +The current system intentionally avoids large real content scenes because they: +- change frequently during content work; +- contain noise unrelated to the tested contract; +- make failures harder to localize. + +Programmatic fixtures keep failures small and readable. + +Real JSON scene fixtures may still be useful later for: +- serialization tests; +- loader tests; +- migration tests. + +They are not necessary for the first iteration. + +## Current Limitations + +- No browser/UI/canvas assertions yet. +- No Playwright layer yet. +- No direct tests for console preprocessor behavior yet. +- No LLM-stage tests yet. +- Parser NLP stage is not the focus of the current suite. +- The direct `Game` tests use a semantic fixture layered on `Game.prototype`, not full `Game` construction. + +## Recommended Next Iteration + +The next useful expansions would be: + +1. Add tests for console-preprocessor behavior: + - `I` + - `X` + - `L` + - `#STAGE1-ON/OFF` + - `#STAGE2-ON/OFF` + +2. Add more parser-core scenarios: + - clarification continuation loops; + - more plan-state transitions; + - more validation branches. + +3. Add tests for console/preprocessor behavior: + - `I` + - `X` + - `L` + - `#STAGE1-ON/OFF` + - `#STAGE2-ON/OFF` + +4. Add tiny serialization/load fixtures if scene loading itself needs coverage. + +5. Add a very small browser smoke layer only if a runtime contract cannot be tested elsewhere. + +## Practical Rule + +When adding a test, prefer this order: + +1. scene/runtime test +2. parser test +3. thin integration test +4. browser/UI test + +If a lower layer can prove the contract, do not jump to a higher one. diff --git a/Commands.md b/Commands.md new file mode 100644 index 0000000..54d07de --- /dev/null +++ b/Commands.md @@ -0,0 +1,562 @@ +# Commands + +## Summary + +`Commands` in `Scanline` are parser-level 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` +- `UNLOCK DOOR WITH KEY` +- `REPAIR BOOMBOX WITH SOLDERING IRON` +- `USE ITEM ON TARGET` + +while reusing the same generic parser systems for: +- target resolution +- ambiguity clarification +- missing-argument clarification +- no-effect handling +- linear plan execution in `Parser Core` + +This document describes the first draft of the **custom command asset format** and how it maps into the parser architecture. + +--- + +## Why Custom Command Assets Exist + +Many story-specific commands are not generic enough to justify new built-in parser verbs, but they still need: +- natural language recognition +- reusable clarification behavior +- structured execution +- optional custom text + +If each of these is implemented as a custom branch in `Parser.ts`, the parser becomes hard to maintain. + +Instead: +- the parser provides the shared machinery +- each custom command is described by data +- `Parser Core` executes a generic plan + +This is also the best preparation for the future LLM cascade: +- lower layers and mocked scenarios can emit the same plan format +- `Core` can be hardened before real LLM integration + +--- + +## Position In The Architecture + +Custom command assets belong to the **parser layer**, not to `Game`. + +They are: +- language-aware +- target-aware +- clarification-aware +- plan-oriented + +They are not: +- runtime world logic +- arbitrary scripts +- direct `Game API` calls + +The flow is: + +1. Player input arrives +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 + +--- + +## Guiding Principles + +1. Custom commands should be described by **data**, not ad-hoc parser code. +2. Clarification rules should stay **generic** whenever possible. +3. The command system should reuse: + - `ParserWorldModel` + - scope slices + - pending clarification + - unified envelope + - `Parser Core` +4. Command-specific messages should be **overrides**, not separate parser logic. +5. Plans should remain **linear and constrained** in the first version. +6. Multi-argument commands should be expressed through the same generic machinery, not special parser branches. +7. Words like `with`, `on`, `to`, `in`, `under` should usually be treated as grammar hints for binding arguments or relations, not as standalone commands. + +This now has a concrete parser-side consequence: +- built-in `LOOK` / `EXAMINE` can already recognize relation markers such as `under`, `in`, `behind`, `near`; +- custom commands keep using the same idea through grammar markers like `separatorsBefore`; +- full execution of relation semantics still depends on future runtime scene-relation data. + +--- + +## Command Asset Location + +Proposed location: + +- `public/text/system/commands/.json` + +Examples: +- `public/text/system/commands/teleport_with.json` +- `public/text/system/commands/unlock_with.json` + +These files are parser text assets, similar in spirit to: +- `public/text/system/parser.json` +- `public/text/system/parser-lexicon.json` +- `public/text/system/parser-training.json` + +--- + +## First-Draft Command Asset Format + +Example: + +```json +{ + "id": "teleport_with", + "phrases": ["teleport with", "teleport"], + "arguments": [ + { + "name": "item", + "kind": "entity", + "required": true, + "scopes": ["held", "takable"], + "validation": { + "allowedTitles": ["your ID card"] + }, + "messages": { + "missing": "Teleport with what?", + "ambiguous": "Which item do you want to teleport with: {options}?", + "notFound": "You don't have anything like that.", + "noEffect": "That doesn't work." + } + } + ], + "plan": [ + { "type": "resolveArgumentEntity", "arg": "item", "saveAs": "teleport_item" }, + { "type": "ensureHeldEntity", "ref": "teleport_item", "noEffectMessageId": "no_effect" }, + { "type": "goToSceneById", "sceneId": "test1" }, + { "type": "removeInventoryEntity", "ref": "teleport_item" }, + { "type": "showText", "messageId": "success" } + ], + "messages": { + "success": "You vanish in a flash and arrive somewhere else." + } +} +``` + +--- + +## Field Reference + +### `id` + +Unique command id. + +Example: + +```json +"id": "teleport_with" +``` + +Used for: +- debugging +- command registry +- future analytics / tracing + +### `phrases` + +List of trigger phrases recognized by lower parser layers. + +Example: + +```json +"phrases": ["teleport with"] +``` + +Notes: +- first draft should keep this simple +- exact phrase matching is enough for v1 +- later this can evolve into richer grammar or language-pack integration +- in most cases, `phrases` should represent the verb-level command (`use`, `unlock`, `teleport`), while prepositions like `with` or `on` are handled by argument grammar + +### `arguments` + +Describes the arguments required by the command. + +Example: + +```json +{ + "name": "item", + "kind": "entity", + "required": true, + "scopes": ["held", "takable"] +} +``` + +First-draft fields: +- `name` +- `kind` +- `required` +- `scopes` +- optional `validation` +- optional `messages` + +For v1 we only need: +- `kind: "entity"` + +### `validation` + +Optional command-specific acceptance rules that run **after normal parser resolution**. + +This is important: +- resolution and ambiguity should remain generic +- command validation should decide whether the resolved object is valid for this command + +Example: + +```json +"validation": { + "allowedTitles": ["your ID card"] +} +``` + +First-draft validation fields: +- `allowedEntityIds` +- `allowedTitles` +- `allowedSynonyms` + +If validation fails, parser should use the command-specific `noEffect` message when available, or fall back to the standard parser no-effect message. + +### `plan` + +Linear list of parser-planned actions. + +This is the core of the command asset. + +The plan is: +- declarative +- validated by `Parser Core` +- executed one step at a time + +No arbitrary code is allowed here. + +### `messages` + +Optional command-specific message overrides. + +These should be used only when generic parser messages are not enough. + +The parser should still have shared defaults for: +- missing argument +- ambiguity +- target not found +- no effect +- generic failure + +--- + +## Standard vs Custom Messages + +The command system should not require every command to reinvent the same UX. + +The parser should provide generic standard flows for: + +- missing argument +- ambiguous target +- target not found +- no effect +- generic failure + +Examples of generic messages: +- `Use what?` +- `Which item do you mean: ...?` +- `You don't see any ... here.` +- `That doesn't work.` +- `Nothing happens.` + +Command assets may override those when the scene needs more specific flavour text. + +This keeps parser UX consistent while still allowing authored exceptions. + +--- + +## Relationship To Pending Clarification + +Custom commands should reuse the same pending clarification machinery as built-in commands. + +That means: +- if an argument is missing, parser asks a question +- if multiple candidates match, parser asks which one +- the next input can continue the same command + +This is important: +- we should not build a second clarification system just for custom commands +- clarification may happen for any individual argument in a multi-argument command + +--- + +## Relationship To Scope + +Argument resolution should always happen through parser scope. + +Example: + +```json +"scopes": ["held", "takable"] +``` + +This means: +- the parser may look in inventory +- then among takeable scene objects + +The command asset does not bypass scope rules. +It only says which scope slices are legal for that argument. + +### Multi-Argument Commands + +Commands may declare more than one argument. + +Important distinction: +- the **command** is usually the verb or verb phrase (`use`, `unlock`, `teleport`) +- words like `with`, `on`, `to`, `in`, `under` are usually **argument-binding markers** +- they help parser assign roles to arguments, but they are not usually separate commands in themselves + +For v1, arguments after the first may define `separatorsBefore`, for example: + +```json +{ + "name": "target", + "kind": "entity", + "required": true, + "scopes": ["visible", "held", "examinable"], + "separatorsBefore": ["on"] +} +``` + +With this, input like: + +```text +use key on door +``` + +is parsed as: +- `item = key` +- `target = door` + +So for parser architecture purposes, `USE` is the command, while `ON` is a grammar hint that introduces the next argument. + +If the separator is missing: +- earlier arguments keep the remaining text they can claim +- later required arguments may remain unresolved +- the usual parser clarification flow asks for the missing argument + +--- + +## Relationship To DSL + +Custom command assets are one of the producers of the unified parser DSL. + +They are not a separate execution system. + +Built-in commands and future LLM outputs should converge on the same general model: +- envelope +- plan +- `Parser Core` +- structured outcomes + +This is why `TELEPORT WITH` is useful as a test scenario: +- it exercises a richer plan +- without needing a real LLM yet + +--- + +## First-Draft Planned Actions Needed For `TELEPORT WITH` + +To support the first realistic custom command scenario, the first DSL expansion should include: + +```ts +type ParserPlannedAction = + | { type: 'resolveArgumentEntity'; arg: string; saveAs: string } + | { type: 'ensureHeldEntity'; ref: string } + | { type: 'goToSceneById'; sceneId: string } + | { type: 'removeInventoryEntity'; ref: string } + | { + type: 'showText'; + textKey?: string; + messageId?: string; + params?: Record; + paramsFromRefs?: Record; + }; +``` + +These actions are intentionally generic. + +They are useful not only for teleportation, but later for: +- unlocking +- repairing +- giving +- consuming +- scripted inventory-driven actions + +`paramsFromRefs` allows `showText` to interpolate values from resolved plan state. + +Example: + +```json +{ + "type": "showText", + "messageId": "no_effect_pair", + "paramsFromRefs": { + "item": "use_item", + "target": "use_target" + } +} +``` + +--- + +## Plan State + +To support command plans, `Parser Core` needs a small plan-state dictionary. + +Example: + +```ts +type ParserPlanState = Record; +``` + +Use: +- `saveAs` writes into plan state +- later actions use `ref` to read from it + +Example: +- resolve `item` and save as `teleport_item` +- later remove `teleport_item` from inventory + +--- + +## Required Shared Game API Support + +For the first real custom command plan, the shared `Game API` will likely need: + +- `removeInventoryEntity(entity)` + +This is not specific to teleportation. +It will also be useful for: +- consuming items +- giving items away +- one-use puzzle items +- future `use X on Y` flows + +This should live in shared gameplay API, not inside parser-only logic. + +--- + +## First Example: `TELEPORT WITH` + +Planned parser behavior: + +Input: + +```text +teleport with id card +``` + +Expected flow: + +1. Match custom command spec `teleport_with` +2. Resolve `item` inside `held + takable` +3. If missing: + - ask `Teleport with what?` +4. If ambiguous: + - ask which item +5. If the resolved item is not valid for this command: + - report generic or command-specific no-effect +6. If found in scene but not held: + - try to pick it up +7. If item still unavailable: + - report failure +8. If item is available: + - go to scene `test1` + - remove item from inventory + - show success message + +This gives us a realistic multi-step scenario while still using the lower cascade. + +--- + +## Second Example: `USE X ON Y` + +This is the first generic multi-argument command supported by the current system. + +Example command asset shape: + +```json +{ + "id": "use_on", + "phrases": ["use"], + "arguments": [ + { + "name": "item", + "kind": "entity", + "required": true, + "scopes": ["held", "takable"] + }, + { + "name": "target", + "kind": "entity", + "required": true, + "scopes": ["visible", "held", "examinable"], + "separatorsBefore": ["on"] + } + ], + "plan": [ + { "type": "resolveArgumentEntity", "arg": "item", "saveAs": "use_item" }, + { "type": "ensureHeldEntity", "ref": "use_item" }, + { "type": "resolveArgumentEntity", "arg": "target", "saveAs": "use_target" }, + { + "type": "showText", + "messageId": "no_effect_pair", + "paramsFromRefs": { + "item": "use_item", + "target": "use_target" + } + } + ] +} +``` + +This command is useful as a parser-system milestone because it exercises: +- multi-argument parsing +- per-argument clarification +- shared scope rules +- plan-state reuse +- dynamic final messaging from resolved refs + +--- + +## Implementation Order + +1. Add command-spec types +2. Add command asset loading to parser text layer +3. Extend parser DSL and plan state +4. Add shared API support such as `removeInventoryEntity(...)` +5. Add custom command matching in stage1 +6. Implement `teleport_with.json` +7. Run smoke tests + +--- + +## Future Expansion + +Later, command assets may grow to support: +- multiple arguments +- typed targets like `entity`, `scene`, `inventory-item` +- richer scope policies +- optional conditions +- richer message overrides +- LLM-generated plans that still reuse the same execution model + +But the first version should stay deliberately small and stable. diff --git a/GDD.md b/GDD.md index 44c6807..3f18bc4 100644 --- a/GDD.md +++ b/GDD.md @@ -8,12 +8,12 @@ # Текстовый интерфейс -Наша игра продолжает традиции классических Adventure, которые когда-то были полностью текстовыми. Кроме того, нарратив связан с темой компьютеров. Поэтому у нас есть классическая "консоль терминала" со строкой ввода команд и областью вывода сообщений над ней (буфер консоли). В этот буфер выводятся все введенные пользователем команды и все игровые сообщения (за исключением неигровых, служебных уведомлений движка и редактора сцены, которые выводятся через toast notifications). +Наша игра продолжает традиции классических Adventure, которые изначально когда-то были полностью текстовыми. Кроме того, нарратив связан с темой компьютеров. Поэтому у нас есть классическая "консоль терминала" со строкой ввода команд и областью вывода сообщений над ней (буфер консоли). В этот буфер выводятся все введенные пользователем команды и все игровые сообщения (за исключением неигровых, служебных уведомлений движка и редактора сцены, которые выводятся через toast notifications). Есть два основных формата пользовательского ввода: - **Команда**: указание что нужно сделать, напр. "открой дверь ключом"; - **Реплика**: прямой текст, который произносит герой и который "слышат" все NPC находяшиеся в сцене. Начинается с символа "-". - > Режим команд является основным, и пользователь может давать команду парсеру сегенерировать реплику за него, напр. "Расскажи Линде про то, что видел по дороге". +> Режим команд является основным, и пользователь может давать команду парсеру сегенерировать реплику за него, напр. "Расскажи Линде про то, что видел по дороге". ## Виртуальная консоль @@ -23,8 +23,8 @@ - **закрытое модальное**; - **открытое**. В _закрытом_ состоянии пользователь видит только последние две строки буфера консоли в нижней части экрана, и под ними строку ввода команды. - При нажатии на специальную клавишу на клавиатуре (тильда ~) консоль _открывается_ поверх картинки, почти на весь игровой экран, накладываясь на него с небольшой полупрозрачностью. При этом, в закрытом виде консоль и строка ввода интегрированы в игровую картинку, то есть рисуются на low-res 2d канвасе и поверх накладывается CRT фильтр. В открытом же виде консоль рисуется поверх игровой картинки, в том же слое, что UI редактора, в высоком разрешении, без CRT фильтра,чтобы пользователям было комфортно читать текст. Строка ввода работает и в открытом состоянии, так что пользователи могут вводить команды в консоль не закрывая её. - Для показа важных сообщений, которые не влазят в 2 строки закрытой консоли, она может переходить в _модальный_ режим, когда командная строка убирается, а если текст сообщения не помещается и в три строки, то высота области буфера увеличивается на нужное число строк, чтобы текст сообщения выводился поверх картинки. В модальном режиме после текста сообщения всегда идёт надпись "[Continue]" и ожидается нажатие любой клавиши или клик мыши, после чего происходит переход в обычный режим. + При нажатии на специальную клавишу на клавиатуре (тильда ~) консоль _открывается_ поверх картинки, почти на весь игровой экран, накладываясь на него с небольшой полупрозрачностью. При этом, в закрытом виде консоль и строка ввода интегрированы в игровую картинку, то есть рисуются на low-res канвасе и поверх накладывается CRT фильтр. В открытом же виде консоль рисуется поверх игровой картинки, в том же слое, что UI редактора, в высоком разрешении, без CRT фильтра,чтобы пользователям было комфортно читать текст. Строка ввода работает и в открытом состоянии, так что пользователи могут вводить команды в консоль не закрывая её. + Для показа важных сообщений, которые не влазят в 2 строки закрытой консоли, она может переходить в _модальный_ режим, когда командная строка убирается, а если текст сообщения не помещается и в три строки, то высота области буфера увеличивается на нужное число строк, чтобы текст сообщения выводился поверх картинки. В модальном режиме после текста сообщения всегда идёт мигающая надпись "[Continue]" и ожидается нажатие любой клавиши или клик мыши, после чего происходит переход в обычный режим. Открытая консоль не переходит в модальный режим. Текст открытой консоли можно прокручивать колесом мыши или клавишами Page Up/Down чтобы увидеть более ранние сообщения. Буфер должен быть достаточно большим, порядка 150 Kb. При сохранении игры в файл буфер сохраняется вместе с игрой. @@ -34,26 +34,257 @@ Доступна история команд (хранится порядка 50 последних команд), переключаться между которыми можно курсорными стрелками вверх/вниз с зажатой клавишей . История команд сохранятеся вместе с игрой. Также можно быстро очистить строку ввода, нажав +. +### Служебные команды консоли + +Помимо внутриигровых команд, игровая консоль поддерживает _служебные команды_, позволяющие разработчикам игры запускать скрипты, получать отладочную информацию и тп. Служебные команды консоли имеют специальный формат и начинаются с символа `#`. Они перехватываются препроцессором консоли до игрового парсера, доступны только тогда, когда доступен редактор сцены, то есть в пользовательском билде игры их нет. + +Поддерживаются следующие служебные команды: + +- `#RUN [args...]` : запускает скрипт из реестра скриптов; +- `#HALT` : останавливает все запущенные скрипты; +- `#HALT ` : останавливает конкретный скрипт; +- `#HELP` : выводит список доступных служебных команд; +- `#CLS` : очищает буфер консоли; +- `#PEEK-ON` : включает режим отладки parser-mediator, при котором после каждой игровой команды в консоль выводятся `Context JSON`, `Action JSON` и `Result JSON`; +- `#PEEK-OFF` : отключает этот режим. + ## Парсер - посредник -Парсер играет роль **посредника** между движком игры и игроком, своеобразного гейм-мастера. Он принимает пользовательский ввод, наряду с контекстом (информацией о сцене, находящихся в ней предметах и NPC, доступны действиях и состояниях). Затем парсер обрабатывает это и даёт команды игровому движку через API, опционально получает возвращаемые API значения и составляет сообщения для пользователя. +Парсер играет роль **посредника** между движком игры и игроком, своеобразного гейм-мастера. Он принимает пользовательский ввод, наряду с контекстом (информацией о сцене, находящихся в ней предметах и NPC, доступныx действиях и состояниях). Затем парсер обрабатывает это и даёт команды игровому движку через API, опционально получает возвращаемые API значения и составляет сообщения для пользователя. + + ---json---> | | | | +| | ---text--> | | ---json--> | | +| | <--text--- | | <--------- | | +| | + +```mermaid +flowchart LR + +User["User"] +Parser["Parser
(Game Master)"] +Context["Game Context
(Scene, Objects, NPCs, States)"] +API["Game Engine API"] +Preprocessor["Preprocessor"] + +User -- "text input" --> Preprocessor +Preprocessor -- "json" --> Parser +Context -- "json context" --> Parser +Parser -- "json commands" --> API +API -- "state / results" --> Parser +Parser -- "text response" --> User +``` - ---json--> | | | | -| | ---text--> | | ---json--> | | -| | <--text--- | | <--------- | | -| | Parser обрабатывает пользовательский ввод каскадно, если каскад не смог обработать команду, она передаётся следующему: -1. Простой разбор через regexps, распознающий самые элементарные команды типа look [at], take [up], drop, use <> with <>, i (inventory), а также служебные команды save, load, run, etc. (мгновенно); +1. Простой разбор через regexps, распознающий самые элементарные команды типа look [at], take [up], drop, use <> with <>, i (inventory): (мгновенно); 2. Малая локальная нейросеть, например nlp.js (быстро, порядка 50 ms); -3. Средняя (или малая) языковая модель, работающая локально или через API. (очень медленно и "дорого") +3. Средняя (или малая) языковая модель (LM), работающая локально или через API. (очень медленно и "дорого") Первые два каскада парсят почти весь нормальный пользовательский ввод, а LLM/SLM подключается только если ввод не распознан ни одним из них, либо надо сгенерировать текст, в том числе при прямых диалогах с NPC. В любом случае, на выходе парсера получается json с набором вызовов API. Затем парсер анализирует их результат (то, что возвращают методы API) и осуществляет какое-то действие: либо отправляет новый json c вызовами API и уходит на новую итерацию, либо отправляет пользователю итоговое сообщение в консоль и заканчивает обработку команды. > В данный момент Parser находится в зачаточном состоянии, реализован только первый каскад, и мы отлаживаем на нём взаимодействие с API. Впоследствии остальные каскады будут использовать ту же систему. +### Текущее состояние архитектуры parser v1 + +На текущем этапе parser уже начал переход от прямого command handler к форме **parser-mediator**, но пока остаётся только в первой, самой простой итерации. + +Сейчас это устроено так: + +- строка ввода сначала разделяется на **console commands** и **gameplay commands**; служебные команды консоли в формате `#...` не проходят через gameplay parser; +- gameplay parser получает пользовательский ввод и собирает **Context JSON** — упрощённый снимок текстово значимой части текущей сцены и инвентаря; +- parser хранит собственное runtime-состояние, включая pending clarification, если предыдущая команда не была завершена и требует уточнения; +- первый каскад остаётся простым regexp/switch parser и строит **Action JSON**, но теперь это план вызовов semantic runtime API (`game.look`, `game.take`, `game.showInventory`, `game.goTo`); +- отдельный tool layer / API adapter исполняет эти вызовы и возвращает parser структурированный **Result JSON** в виде outcomes, а не готового сценарного ответа; +- parser анализирует outcomes и сам решает, что делать дальше: завершить ответ игроку, сохранить pending state, передать кейс на старший каскад или выполнить следующую команду API. + +Таким образом, даже при очень простом первом каскаде уже существует правильная форма взаимодействия: + +`input -> context json -> action json -> engine execution -> result json -> player response` + +В первой версии через этот путь проходят только базовые команды: + +- `LOOK` +- `LOOK ` +- `LOOK AROUND / HERE / SCENE` +- `TAKE / GET / PICKUP ` +- `INV / INVENTORY / I` +- `GO / WALK / MOVE ` +- `GO TO ` + +Для `TAKE` и `GO` уже поддерживается базовый pending clarification: если обязательная цель не указана, parser может задать уточняющий вопрос и ожидать следующий ввод как продолжение текущей команды. + +Команда `USE`, speech input (реплики с `-`) и старшие каскады пока в эту версию не входят. + +### Текущая реализация tool layer / API adapter + +На этом этапе parser уже не перекладывает на исполнитель решение о том, что говорить игроку. Он использует runtime API как набор инструментов и сам остаётся оркестратором команды. + +Сейчас первый каскад parser может строить action-план из вызовов следующих semantic API-методов: + +- `game.look(target?)` +- `game.take(target?)` +- `game.showInventory()` +- `game.goTo(target?)` + +Каждый такой вызов возвращает structured outcome: + +- `status`: `ok | failed | needs_clarification | escalate` +- `code`: машинный код результата +- `message`: fallback-текст, который parser может вывести игроку, переиспользовать или проигнорировать +- `data`: структурированные данные +- `effects`: список побочных эффектов (`scene_changed`, `moved_to_inventory`, `script_executed` и тп) +- `recoverable`: можно ли пытаться продолжать обработку + +Примеры `Action JSON`, которые parser может передать tool layer: + +```json +{ + "stage": "regex-v1", + "actions": [ + { "type": "callGameMethod", "method": "look", "args": [null] } + ], + "debug": { + "rawInput": "look", + "normalizedInput": "LOOK", + "verb": "LOOK", + "noun": "" + } +} +``` + +```json +{ + "stage": "regex-v1", + "actions": [ + { "type": "callGameMethod", "method": "look", "args": ["lamp"] } + ], + "debug": { + "rawInput": "look lamp", + "normalizedInput": "LOOK LAMP", + "verb": "LOOK", + "noun": "lamp" + } +} +``` + +```json +{ + "stage": "regex-v1", + "actions": [ + { "type": "callGameMethod", "method": "take", "args": ["key"] } + ], + "debug": { + "rawInput": "take key", + "normalizedInput": "TAKE KEY", + "verb": "TAKE", + "noun": "key" + } +} +``` + +Пример pending-resolution, когда parser ждёт уточнение и трактует следующий ввод как продолжение предыдущей команды: + +```json +{ + "stage": "pending-resolution", + "actions": [ + { "type": "callGameMethod", "method": "take", "args": ["key"] } + ], + "debug": { + "rawInput": "key", + "normalizedInput": "KEY", + "verb": "TAKE", + "noun": "key", + "pendingIntent": "take" + } +} +``` + +Примеры `Result JSON`, которые tool layer возвращает parser: + +```json +{ + "type": "outcomes", + "handled": true, + "outcomes": [ + { + "status": "ok", + "code": "scene_description", + "message": "You are in New Scene." + } + ], + "actionsExecuted": ["look"] +} +``` + +```json +{ + "type": "outcomes", + "handled": true, + "outcomes": [ + { + "status": "ok", + "code": "item_taken", + "message": "You picked up the key.", + "effects": ["moved_to_inventory"] + } + ], + "actionsExecuted": ["take"] +} +``` + +```json +{ + "type": "outcomes", + "handled": true, + "outcomes": [ + { + "status": "needs_clarification", + "code": "missing_destination", + "message": "Where do you want to go?" + } + ], + "actionsExecuted": ["goTo"] +} +``` + +```json +{ + "type": "handoff", + "handled": false, + "outcomes": [], + "actionsExecuted": [], + "reason": "unsupported_by_stage1", + "debug": { + "actionJson": "...", + "action": { "type": "handoff", "reason": "unsupported_by_stage1" } + } +} +``` + +После получения `Result JSON` parser на текущем этапе может сделать одно из четырёх действий: + +1. Вывести игроку `outcome.message` как итог ответа. +2. Сохранить pending state и задать уточняющий вопрос, если `status = needs_clarification`. +3. Передать кейс на старший каскад, если `status = escalate` или stage1 не смог построить plan. +4. При `handoff` выдать игроку fallback-сообщение и, в debug-режиме, напечатать в консоль служебный отчёт (`Context JSON`, `Action JSON`, `Result JSON`) для следующего каскада. + +### Ближайшее направление развития + +Первый приоритет сейчас — не усложнение распознавания языка, а укрепление parser именно как посредника. + +Ближайшие шаги развития: + +1. Расширять и стабилизировать JSON-контракты между context builder, first-stage parser, tool layer и response builder. +2. Постепенно добавлять новые semantic API-методы (`game.use`, `game.open`, `game.talkTo`, etc), чтобы parser не реализовывал типовые действия через ad hoc low-level логику. +3. Укреплять pending clarification и parser session state, чтобы он мог вести короткий диалог с игроком внутри одной незавершённой команды. +4. После стабилизации этого контура подключать второй каскад, который будет получать тот же Context JSON и тот же Result JSON, но уже пытаться разбирать более свободный ввод и строить многошаговые планы. +5. Затем подключать тяжёлый LLM/SLM каскад и speech/dialogue routing, чтобы parser мог не только генерировать тексты, но и строить plan’ы для сложных команд. + +Иными словами, ближайшая цель — сделать первый каскад не умнее, а **архитектурно правильнее**, чтобы все последующие каскады подключались к уже работающему посреднику. + # Основные элементы игры ## Спрайт @@ -69,17 +300,26 @@ Parser обрабатывает пользовательский ввод кас Сцена это отдельная локация, в которой находится персонаж игрока, и другие объекты. Может занимать один физический экран, либо быть больше его. Сцена может содержать _объекты_ следующих типов: -- _WalkBox_: замкнутый многоугольник, определяющий область, в которой можно перемещаться персонажем игрока (или NPC). Несколько WalkBox могут быть на одной сцене и взаимодействовать друг с другом, в зависимости от их типа: add, substract, invert; -- _TriggerBox_: замкнутый многоугольник, определяющий область, активирующую какие-то события и сюжетную логику, например коллайдер, попав в который персонаж игрока проваливается в люк, переносится в другую сцену, запускает диалог с NPC и т.п; +- _WalkBox_: замкнутый многоугольник, определяющий служебную область, в которой можно перемещаться персонажем игрока (или NPC). Несколько WalkBox могут быть на одной сцене и взаимодействовать друг с другом, в зависимости от их типа: add, substract, invert; + +- _TriggerBox_: замкнутый многоугольник, определяющий служебную область, активирующую какие-то события и сюжетную логику, например коллайдер, попав в который персонаж игрока проваливается в люк, переносится в другую сцену, запускает диалог с NPC и т.п; + - _Static_: прямоугольник с координатами X/Y, размерами X/Y, цветом заполнения и опционально спрайтом/анимацией, отображающимся вместо прямоугольника. Спрайт можно переключать на лету. В основном Static это фоны, декоративные элементы и предметы, которые не перемещаются. + - _Actor_: объект, который помимо свойств Static имеет направление, в котором он повёрнут и, опционально, спрайты/анимации состояний (idle, walk, talk, etc), причём для каждого направления свой набор. Обычно Actor это NPC и анимированные объекты. Персонаж игрока также является разновидностью Actor. +- _Quad_ : четырёхугольный объект, каждая вершина которого обладает отдельным параллаксом. Используется для создания псевдо 3d поверхностей и эффектов типа лучей света и теней. + +### ID + Каждая сцена и каждый объект имеют свой уникальный _ID_, который используется для ссылок на них. При этом: 1. id (содержимое поля id/file) для сцен, спрайтов, и также префабов (т.е. сохранённых объектов) может включать один или несколько обратных слешей "\". При сохранении такого объекта слеши работают как маркеры подпапок (относительно дефолтной папки для данного типа объектов), например "home\room1" сохранится как файл room.json в папке home. 2. При загрузке такого объекта его id не читается из файла, а формируется с учётом пути относительно дефолтной папки и имени файла, таким образом этот объект загрузится c id "home\room1" а не "room1". Соответственно, если пользователь нажмёт "Save", объект сохранится не в дефолтной папке а в подпапке home как room1. 3. При завершении загрузки объекта сформированный id дополнительно проверяется на предмет совпадения с уже имеющимися в сцене. Если это не уникальный id, то он дополняется до уникального. -4. API при создании объекта или загрузки сцены получает id, трактует его как имя файла с возможным учётом подпапок и загружает его оттуда. +4. Игровой движок при создании объекта или загрузки сцены получает id, трактует его как имя файла с возможным учётом подпапок и загружает его оттуда. + +### Свойства сцены Сцена может поддерживать _Depth-scaling_ -- масштабирование объектов, имитирующее 3d перспективу, когда объекты, находящиеся "дальше от камеры" (то есть, выше по оси Y), становятся меньше. Настройки масштабирования для каждой сцены свои. Кроме того, объекты типа Static и Actor имеют свойство, запрещающее их Depth-scaling. Если Depth-scaling объекта запрещен, то он не изменяет свой размер при изменении Y, даже если Depth-scaling включен для сцены. Это полезно, например, для сцен, где персонаж лезет вертикально вверх по пожарной лестнице, и не должен уменьшаться по мере подъёма, поскольку не удаляется от камеры. @@ -87,21 +327,23 @@ Parser обрабатывает пользовательский ввод кас Сцена имеет свойство, определяющее _положение "камеры"_ (viewport), т.е. задаёт какая область сцены будет отображаться на экране и с каким зумом. Например, при приближении персонажа игрока к краю экрана сцена скроллится. По умолчанию камера позиционируется на персонаже игрока, но позиционированием можно управлять и динамически, например если игрок выходит из дома на улицу, то масштаб изображения может уменьшиться кастомной логикой (скриптом) этой сцены, отдалив камеру чтобы передать ощущение большого открытого пространства. Чтобы облегчить манипуляции с камерой, сцена имеет два значения параметра zoom: дефолтный и текущий. Дефолтный zoom задаётся при создании сцены и применяется при её загрузке, а текущий -- изменяется динамически во время игры и при редактировании. -Важно отметить, что все свойства сцены и всех объектов должны быть доступны для изменения не только в редакторе, но и динамически прямо во время игры, со стороны игровой логики (скриптов). Примерно как свойства в Unity или Unreal Engine. +Важно отметить, что все свойства сцены и всех объектов доступны для изменения не только в редакторе, но и динамически прямо во время игры, со стороны игровой логики (скриптов). Примерно как свойства в Unity или Unreal Engine. + +## Объекты сцены -## Структура классов +### Структура классов -С точки зрения кода класс _SceneObject_ является прародительским для всех объектов сцены в игре, включая Static, Actor, а также полигональные объекты TriggerBox и WalkBox. +С точки зрения ООП класс _SceneObject_ является прародителем для всех объектов сцены в игре, включая Static, Actor, Quad, а также полигональные служебные области TriggerBox и WalkBox, которые в игре не видны. SceneObject ├── PolygonObject -│ ├── Walkbox -│ └── Triggerbox +│ ├── Walkbox +│ └── Triggerbox ├── QuadObject └── Entity (≈ Static) -└── Actor + └── Actor -## Свойства объектов SceneObject +### Свойства объектов SceneObject Эти свойства наследуются всеми объектами в игре: @@ -119,9 +361,53 @@ _Disabled_ : (boolean) _Locked_ : (boolean) Объект может быть заблокирован (Locked) для редактирования в редакторе сцены. Заблокированные объекты нельзя выбрать или переместить кликом мыши на экране (они становятся "прозрачными" для кликов), но их всё ещё можно выбрать в списке объектов. В режиме игры это свойство игнорируется. -## Коллайдеры (Collision Box) +#### Пространственная вложенность (Spatial) + +Объекты сцены могут быть логически вложены друг в друга. Это относится не к рендерингу и не к parser-у, а к **модели мира**, которой владеет движок игры. Parser только получает эту структуру как часть контекста и использует её при разборе команд вроде `LOOK UNDER TABLE` или `LOOK IN DRAWER`. + +У любого подходящего объекта сцены может быть optional spatial placement: + +- `parentNodeId`: ID родительского узла; +- `relation`: тип вложения относительно родителя. + +Поддерживаются следующие типы отношений: + +- `in` : внутри; +- `on` : на; +- `under` : под; +- `behind` : за. + +Примеры: + +- ключ лежит **под** столом; +- записка лежит **в** верхнем ящике; +- монета лежит **за** картиной. -Объекты типа Static и Actor имеют свойства `Collider Width` и `Collider Height`, задающие размер прямоугольной области столкновения, которая по X центрирована по объекту, а по Y нижняя граница прямоугольника коллайдера приходится на нижнюю границу спрайта/прямоугольника объекта. То есть, при увеличении высоты коллайдера он растёт вверх, а при увеличении ширины он растёт в обе стороны от центра объекта. +Spatial-вложенность не определяет сама по себе: + +- видимость объекта; +- доступность объекта для взаимодействия; +- порядок отрисовки. + +Это отдельные системы. + +Spatial-система нужна для: + +- описания структуры сцены на уровне мира; +- parser-команд с пространственными уточнениями; +- point-and-click логики и скриптов, которым важно понимать, где предмет находится логически; +- редактора сцены, где вложенность должна быть видна и редактируема. + +В редакторе у объектов сцены должны быть доступны: + +- выбор родительского объекта; +- выбор типа отношения (`in`, `on`, `under`, `behind`). + +В списке иерархии сцены вложенные объекты отображаются под своим родителем со сдвигом вправо. + +#### Коллайдеры (Collision Box) + +Объекты Entity (Static и Actor имеют свойства `Collider Width` и `Collider Height`, задающие размер прямоугольной области столкновения, которая по X центрирована по объекту, а по Y нижняя граница прямоугольника коллайдера приходится на нижнюю границу спрайта/прямоугольника объекта. То есть, при увеличении высоты коллайдера он растёт вверх, а при увеличении ширины он растёт в обе стороны от центра объекта. - Если размеры коллайдера больше 0, этот объект является препятствием для других объектов (Actor), имеющих коллайдер. - Коллайдер взаимодействует с WalkBox: @@ -129,7 +415,7 @@ _Locked_ : (boolean) - В режиме _Invert_: коллайдер объекта должен полностью находиться внутри разрешенной зоны. - Если размеры коллайдера равны 0, объект считается проходимым, не сталкивается с другими и игнорирует WalkBox. -## Свойства Static +### Свойства Static _Parallax_ Управляет их перемещением при движении камеры. При значении 1 они движутся так же, как другие объекты, при 0.5 движутся вдвое медленней, при 0 остаются вообще неподвижными, а при значениях >1, соответственно, движутся быстрее чем обычные объекты. Это позволяет делать параллаксные фоны с эффектом глубины. Например, спрайт с небом не движется при скроллинге сцены, спрайт с отдалёнными домами движется вдвое медленней, чем остальная сцена, а деревья на переднем плане -- чуть быстрее. @@ -144,7 +430,7 @@ _Visual Effects_ - **Blend Mode**: Режим наложения цвета (Normal, Multiply, Screen, Overlay, etc). - **Blur**: Эффект размытия (в пикселях). -## Свойства Actor +### Свойства Actor Actor это расширение Static. Помимо текущего спрайта, как у Static, имеет направление и (опционально) визуальное состояние. @@ -157,7 +443,7 @@ Actor может иметь сколько угодно групп анимац Для скриптов есть возможность через API переключать состояние. Например, если переключить на группу "talk", то персонаж будет воспроизводить анимации разговора в зависимости от того, куда он повёрнут. Он будет сохранять эту анимацию до тех пор, пока ему не придёт команда перемещаться, тогда он переключится на walk а после остановки автоматически на idle. -## Свойства Quad +### Свойства Quad _Quad_ это примитив, определяемый четырьмя вершинами. В отличие от Static, это не прямоугольник, а произвольный четырёхугольник. Основное назначение -- создание поверхностей и стен с учётом 2.5D перспективы, а также эффектов тени и освещения. @@ -165,15 +451,15 @@ _Vertices_ Quad имеет 4 вершины. У каждой вершины свои координаты X, Y и свой коэффициент Parallax (P). Это позволяет создавать объекты, которые корректно деформируются при движении камеры, имитируя 3D перспективу. Например, "пол" будет иметь вершины с разным параллаксом: ближние к камере P > 1, дальние P < 1. _Retro Grid Mode_ -Quad может отображаться как "сетка" (Retro-Grid), что соответствует стилистике ретро-футуризма 80х. Настраивается цвет линий, толщина и количество ячеек сетки. Этот режим не отменяет заливку цветом и может использоваться одновременно с ней. +Quad может отображаться как сетка линий (Retro-Grid) в стиле компьютерной графики 80х. Настраивается цвет линий, толщина и количество ячеек сетки. Этот режим не отменяет заливку цветом и может использоваться одновременно с ней. +Помимо эстетической, Retro-Grid несёт и функциональную роль, играя роль сетки для выравнивания объектов относительно друг-друга. Её узлы могут служить точками привязки (когда объект перетаскивается с зажатым ) наряду с вершинами Quad и Entity. _Sort Mode_ (v0, v1, v2, v3, ignore) Определяет точку сортировки (Z-Sort) для объекта. Поскольку Quad может быть сильно вытянут в глубину (наподобие пола), его центр может быть некорректной точкой для сортировки относительно других объектов (например, персонажа стоящего на этом полу). Режим сортировки позволяет привязать Z-индекс к конкретной вершине (например, самой дальней). ## Компонентная система -Кроме простых свойств, которые есть всегда, объекты сцены могут содержать компоненты (структуры данных), которые могут быть добавлены и удалены в редакторе. Каждый объект может иметь один или несколько компонентов разных типов. Но не любой объект может содержать любой компонент. -Каждый компонент имеет уникальный id компонента. +Кроме простых свойств, которые есть всегда, объекты сцены способны содержать _компоненты_ (структуры данных), которые могут быть добавлены и удалены в редакторе. Каждый объект может иметь один или несколько компонентов разных типов. Но не любой объект может содержать любой компонент. #### Компоненты групп анимаций @@ -213,6 +499,29 @@ _Sort Mode_ (v0, v1, v2, v3, ignore) Технически это просто включение (Enable) указанных целей. Для выхода обратно в главную сцену игрок должен кликнуть за пределами объектов этой группы. Пока открыта Subscene передвижения персонажа игрока заблокированы. + В spatial-модели мира `Subscene` также может выступать как **виртуальный spatial node**, то есть узел, в который могут быть вложены другие объекты или подчинённые subscene. Это позволяет описывать, например: + + - `Стол -> Ящики стола -> Верхний ящик -> Записка` + - или `Стол -> (under) -> Ключ` + + Таким образом, вложенность может быть: + + - прямой: объект вложен в другой объект; + - опосредованной: объект вложен в `Subscene`, которая сама вложена в объект. + + При активации `Subscene` движок включает все объекты, **непосредственно** вложенные в неё через spatial hierarchy. Это относится к любым подходящим объектам сцены: + + - `Entity`; + - `Triggerbox`; + - вложенным `Subscene`. + + При этом раскрывается только **один уровень** вложенности: + + - если `Subscene A` содержит `Subscene B`, то при открытии `A` активируется `B`; + - но содержимое `B` не активируется автоматически, пока пользователь не откроет уже саму `B`. + + Старый механизм `targetGroupId` сохраняется для совместимости и может работать вместе с spatial-вложенностью, но spatial-модель считается источником истины для структуры содержимого. + > Если триггер Subscene относится к переключаемой им группе объектов (имеет такой Group ID как Group_id_1 или Group_id_2), то он не дизейблит самого себя, чтобы не сломать логику переключения. Но другие объекты, которые находятся в этой группе, включая триггеры, будут дизейблиться. - _Switch_ (Target_ID_1, Target_ID_2, state (1 or 2), [Name], [id_key], [sound_1], [sound_2]) : Триггер, который при клике мышью или по команде "открыть Name" подменяет одни объекты другими, а при повторном клике или команде "закрыть Name" подменяет обратно. Например, игрок кликает по двери и она открывается, а при повторном клике закрывается. Технически "открытие/закрытие" это включение одной и выключение другой группы объектов сцены (или subscene). @@ -266,68 +575,351 @@ Static и Actor могут содержать скриптовые событи > Примечание: События _Always_ и _OnCollide_ зарезервированы в дизайне, но на текущий момент технически не реализованы в движке. + + +## Текстовые ассеты (TA) + +Наша игра в значительной степени текстовая. Каждый объект или сцена имеет название, а также описания, выдаваемые при различных действиях с объектами, например по методу look ("You see _a desk_"). Есть также тексты, предназначенные не для пользователя, а для SLM/LLM: описывающие возможные действия c предметами, промпты для задания нужной атмосферы и тп. Всё это удобно хранить в виде json файлов. Когда игра загружает сцену и объекты, она читает и текстовые ассеты с ними связанные. + +Текстовые ассеты хранятся в Public\text\ в виде json файлов с именами, совпадающими с id сцен и объектов. + +- `public\text\scenes\.json` +- `public\text\objects\.json` + +Поскольку ID могут быть составными, ссылаясь на файлы в подпапках, соответствующие TA тоже могут находиться в подпапках. Например: 'public\text\scenes\home\room.json' для сцены 'home\room' + +Для сцен соответствующие TA создаются автоматически: + - при создании новой сцены: если нет TA, файл создаётся для дефолтного id новой сцены + - при сохранении сцены: если нет TA с новым id, но был для старого, старый TA копируется в новый + + +Текстовый asset содержит стандартные (используемые движком) поля, а также может содержать дополнительные (кастомные). + +> сейчас стандартными текстовыми полями считаются `title` и `description`. + + +Кроме текстовых ассетов сцен и объектов, в проекте есть и **служебные TA** для строк самого движка, парсера, UI и скриптов. Они хранятся отдельно, в `public\text\system\`, разбиваются по доменам (`parser.json`, `engine.json`, `scripts.json`, etc) и адресуются по строковым ключам вида `parser.take_prompt` или `engine.click_you_see`. +Служебные TA не имеют таблицы переадресации. Это просто словари строк, доступных по ключу. +В строках служебных TA допускаются именованные плейсхолдеры, например `{item}` или `{title}`, которые заполняются вызывающим кодом. + # Игровая логика (API & scripting system) Поскольку у нас игра, основанная на сюжетной логике, то нам нужно реагировать на события, такие как: -- столкновения Actor между собой, попадание их в TriggerBox, -- условия, такие как наличие у игрока предмета в инветаре, текущей сцены, присутствия в ней NPC, состояния какой-то внутренней переменной -- команды игрока -- реплики игрока в диалоге NPC и ответы NPC на них +- столкновения Actor между собой, попадание их в TriggerBox; +- условия, такие как наличие у игрока предмета в инветаре, текущей сцены, присутствия в ней NPC, состояния какой-то внутренней переменной; +- команды игрока; +- реплики игрока в диалоге NPC и ответы NPC на них; +- и тд. При этом в качестве реакции на это может потребоваться: -- изменение свойств сцены/объекта (например, скрыть/показать объект, приблизить или отдалить камеру) -- изменение состояния игры (например, изменить внутреннюю переменную timeOfDay на 'night') -- работа с инвентарём игрока (например, взять/забрать предмет) -- перенос игрока или NPC в другую сцену -- работа с анимациями объектов (например, персонаж садится на стул) +- изменение свойств сцены/объекта (например, скрыть/показать объект, приблизить или отдалить камеру); +- изменение состояния игры (например, изменить внутреннюю переменную timeOfDay на 'night'); +- работа с инвентарём игрока (например, взять/забрать предмет); +- перенос игрока или NPC в другую сцену; +- работа с анимациями объектов (например, персонаж садится на стул); - и тд. -Комплексный пример: игрок подходит к стене, на которой есть кнопка. Если игрок находится рядом с кнопкой и отдаёт команду нажать на неё, сверху спускается лестница, после чего становится доступна новая команда: "лезь по лестнице". Когда игрок переходит в режим лазания по лестнице, то обычный WalkBox отключается, а включается WalkBox для лестницы, который позволяет персонажу перемещаться лишь вверх и вниз. Кроме того, у персонажа игрока заменяются анимации walk для ходьбы вверх и вниз на анимации лазания вверх и вниз по лестнице, а ещё устанавливается запрет на Depth-scaling, чтобы поднимаясь по лестнице персонаж не уменьшался в размере. Когда игрок долазит до TriggerBox вверху лестницы, он оказывается в другой сцене, при этом свойства его персонажа сбрасываются на дефолтные, то есть он вновь масштабируется и ходит, а не лазит. +Комплексный пример: игрок подходит к стене, на которой есть кнопка. Если игрок отдаёт команду "push the button", сверху спускается лестница, после чего становится доступна новая команда: "climb the ladder". Когда игрок переходит в режим лазания по лестнице, то обычный WalkBox отключается, а включается WalkBox для лестницы, который позволяет персонажу перемещаться лишь вверх и вниз. Кроме того, у персонажа игрока заменяются анимации walk для ходьбы вверх и вниз на анимации лазания вверх и вниз по лестнице, а ещё устанавливается запрет на Depth-scaling, чтобы поднимаясь по лестнице персонаж не уменьшался в размере. Когда игрок долазит до TriggerBox вверху лестницы, он оказывается в другой сцене, при этом свойства его персонажа сбрасываются на дефолтные, то есть он вновь масштабируется и ходит, а не лазит. Очевидно, что это требует какой-то системы скриптов. Для этого мы используем тот же язык, на котором написан движок, то есть Typescript с паттерном Script Registry и API для взаимодействия с игрой. -> Парсер и UI используют этот же API. Например, если пользователь ввёл команду Look <объект> или кликнул наэтот объект, вызовется game.look(object_id) +> Парсер и UI используют этот же API. Например, если пользователь ввёл команду Look <объект> или кликнул на этот объект, вызовется game.look(target_id) ## API -Все скрипты регистрируются в `ScriptRegistry` и получают объект контекста `ScriptContext` со следующими аргументами: +Все скрипты регистрируются в `ScriptRegistry`. При выполнении скрипт получает объект `ScriptContext` со следующими полями: + +- `game`: основной экземпляр игры (`Game.instance`); +- `entity`: объект, на котором сработал скрипт, если он есть; +- `api`: экземпляр `ScriptAPI`, то есть компактная script-oriented обёртка над частью runtime API; +- `args`: опциональные дополнительные аргументы. + +Базовый шаблон скрипта: + +```typescript +ScriptRegistry.register('demo.test', ({ game, entity, api, args }) => { + game.showMessage('Script started'); +}); +``` + +### Видимость и модель доступа -- `game`: Ссылка на основной экземпляр игры (`Game.instance`). -- `entity`: Ссылка на объект, на котором сработал скрипт (Entity/Actor/Triggerbox). -- `args`: Опциональные дополнительные аргументы. +Важно различать **контекст скрипта** и **контекст браузерной консоли**. -### Основные методы +Штатный игровой скрипт работает только с тем, что передано в `ScriptContext` или доступно через эти ссылки. Нормальный способ доступа к сцене и объектам из скрипта: -#### Game +- `game` +- `entity` +- `api` +- `game.sceneManager.currentScene` +- `api.getEntity(name)` +- `api.getActor(name)` +- `api.getQuad(name)` +- `game.sceneManager.currentScene?.findEntity(name)` -- `game.showMessage(text: string)`: Выводит сообщение в игровую консоль/UI. -- `game.playSound(filename: string)`: Проигрывает звуковой файл из папки `public/sounds`. -- `game.sceneManager.switchTo(sceneId: string)`: Загружает и переключает на указанную сцену. +Вызовы вида: + +```typescript +Hero.walkTo(100, 100); +``` -- 'game.look()' +не являются нормальным способом использования API. Такой синтаксис относится к debug-видимости объектов в `window` для браузерной консоли. Он может быть полезен для отладки, но не должен использоваться как опора для игровых скриптов. -#### Entity / Actor +### Доступ через `game` -- `entity.setSprite(filename: string)`: Меняет спрайт объекта. -- `entity.description = "..."`: Меняет описание объекта (для команды look). -- `actor.setDirection(dir: 'up'|'down'|'left'|'right')`: Поворачивает персонажа. -- `actor.playAnimSet(id: string)`: Переключает набор анимаций (например, на 'talk'). -- `actor.resetAnimSet()`: Возвращает набор анимаций к дефолтному ('idle'/'walk'). -- `actor.walkTo(x, y)`: Заставляет персонажа идти в указанную точку (с учетом Walkbox). -- `actor.stop()`: Останавливает движение. +`game` — это базовый runtime API. Им пользуются не только скрипты, но и parser, компонентные системы и сам движок. + +Основные методы и свойства, полезные в скриптах: + +- `game.showMessage(text: string)`: выводит сообщение в игровую консоль; +- `game.log(text: string)`: выводит сообщение напрямую в буфер консоли; +- `game.text(key: string, params?: Record)`: получает строку из служебного TA по ключу; +- `game.look(target?: string | null)`: semantic API для осмотра сцены или объекта; +- `game.take(target?: string | null)`: semantic API для подбора предмета; +- `game.showInventory()`: semantic API для получения описания инвентаря; +- `game.goTo(target?: string | null)`: semantic API для перехода в сцену или перемещения игрока к объекту; +- `game.playSound(filename: string)`: проигрывает звук из `public/sounds`; +- `game.sceneManager.currentScene`: ссылка на текущую сцену; +- `game.sceneManager.switchTo(sceneId: string)`: переключает игру на другую сцену; +- `game.inventory`: массив предметов в инвентаре игрока. + +Пример: + +```typescript +ScriptRegistry.register('door.locked', ({ game }) => { + game.showMessage(game.text('engine.locked_needs', { item: 'keycard' })); +}); +``` + +`game.look / game.take / game.showInventory / game.goTo` на текущем этапе возвращают **structured outcome**, а не только текст. В нём есть: + +- `status`: `ok | failed | needs_clarification | escalate` +- `code`: машинный код результата +- `message`: fallback-текст, который parser может показать игроку или использовать как промежуточный ответ +- `data`: структурированные данные о результате +- `effects`: список побочных эффектов (`scene_changed`, `moved_to_inventory`, `script_executed`, etc) +- `recoverable`: можно ли продолжать обработку + +Пример: + +```typescript +const outcome = game.look('lamp'); + +if (outcome.status === 'ok' && outcome.message) { + game.showMessage(outcome.message); +} +``` + +#### Подробности по `game.goTo()` + +`game.goTo(target?: string | null)` в текущей реализации является **semantic API-заготовкой** для команд перемещения высокого уровня. Это ещё не полноценный travel planner, но уже единая точка входа, через которую parser может пробовать реализовывать команды вида `GO TO OFFICE`. + +Поведение метода сейчас такое: + +1. Если цель не указана, метод не падает и не завершает команду ошибкой, а возвращает outcome: + - `status = needs_clarification` + - `code = missing_destination` + - `message = "Where do you want to go?"` + +2. Если цель указана, метод сначала пытается найти **сцену** среди уже загруженных сцен. Поиск выполняется по: + - `scene.id` + - `scene.name` + - `scene title` из связанного TA + +3. Если сцена найдена: + - вызывается `game.sceneManager.switchTo(scene.id)` + - возвращается outcome со статусом `ok` + - в `message` попадает описание сцены (`description` из TA или fallback) + - в `effects` может быть `scene_changed` + +4. Если сцена не найдена, метод пытается найти **объект текущей сцены** через `scene.findEntity(target)`. + +5. Если объект найден и у него есть координаты: + - персонажу игрока отдаётся команда идти к этому объекту + - возвращается outcome со статусом `ok` + - в `message` используется fallback вроде `You go to .` + - в `effects` может быть `player_move_started` + +6. Если не найдена ни сцена, ни объект: + - возвращается outcome со статусом `failed` + - `code = destination_not_found` + - `message = "You can't get to from here."` + +Важно: текущий `game.goTo()` **не** делает сложного планирования и не проверяет сюжетные условия высокого уровня. Например, он пока не умеет сам решать кейсы вида: + +- доступен ли сейчас офис по сюжету; +- есть ли у игрока машина; +- есть ли ключи; +- надо ли выполнить цепочку промежуточных действий перед переходом. + +Именно такие кейсы в будущем должен будет разруливать parser-оркестратор и старшие каскады. Тогда `game.goTo()` станет для них не “полным решателем задачи”, а semantic runtime инструментом, который либо может выполнить шаг, либо возвращает structured outcome, на основании которого parser решает, что делать дальше. + +Пример: + +```typescript +const outcome = game.goTo('office'); + +if (outcome.status === 'needs_clarification' && outcome.message) { + game.showMessage(outcome.message); +} +``` + +### Доступ через `api` + +`api` — это удобная script-side обёртка. Она не заменяет `game`, а сокращает наиболее частые операции. + +Методы `ScriptAPI`: + +- `api.log(text: string)`: выводит текст в игровую консоль; +- `api.text(key: string, params?: Record)`: получает строку из служебного TA; +- `api.getEntity(name: string)`: возвращает объект сцены по имени; +- `api.getActor(name: string)`: возвращает `Actor` по имени; +- `api.getQuad(name: string)`: возвращает `QuadObject` по имени; +- `api.setTimeout(...)`, `api.clearTimeout(...)`: таймеры; +- `api.setInterval(...)`, `api.clearInterval(...)`: интервалы; +- `api.saveCheckpoint()`: сохраняет текущее состояние сцены в undo history редактора. + +`api.text(...)` и `game.text(...)` по сути делают одно и то же. Разница только в форме доступа: + +- `game.text(...)` — базовый runtime метод; +- `api.text(...)` — его сокращённая обёртка для скриптов. + +Ни `game.text(...)`, ни `api.text(...)` не выводят текст сами по себе. Они только возвращают строку. + +Примеры: + +```typescript +api.log(api.text('scripts.puzzle_solved')); + +const lamp = api.getEntity('lamp'); +const hero = api.getActor('Hero'); +const floor = api.getQuad('floor_main'); +``` + +api.getQuad(name) по сути делает: + +1. берёт game.sceneManager.currentScene +2. ищет объект через scene.findEntity(name) +3. проверяет obj.type === 'Quad' +4. возвращает объект или null + +Упрощённый эквивалент: + +```typescript +function getQuad(game, name) { + const scene = game.sceneManager.currentScene; + if (!scene) return null; + const obj = scene.findEntity(name); + if (obj && obj.type === 'Quad') { + return obj; + } +} +``` + +### Работа с текущей сценой + +Текущая сцена доступна как: + +```typescript +const scene = game.sceneManager.currentScene; +``` + +Основные полезные методы и свойства сцены: + +- `scene.findEntity(name)`: ищет объект по `id`, `customName` или `title` из TA; +- `scene.resolveTarget(targetStr)`: разрешает цель по `id`, `#group` или смешанному списку целей; +- `scene.setTextRedirect(field, targetField)`: устанавливает runtime-переадресацию стандартного текстового поля сцены на кастомное поле из её TA; +- `scene.clearTextRedirect(field)`: сбрасывает переадресацию; +- `scene.activeSubscene`: текущее состояние Subscene; +- `scene.player`: ссылка на персонажа игрока, если он есть. + +#### Text Redirects + +Каждая сцена и объект _в рантайме_ имеют _таблицу переадресации полей_ TA, позволяющую стандартным полям динамически ссылаться на кастомные поля из того же TA. Если переадресации нет, используется стандартное поле. Если целевое поле отсутствует, движок делает fallback на стандартное поле. + +Например, если мы хотим, чтобы описание сцены зависело от времени суток, можно хранить в TA поля `description`, `description_morning`, `description_evening` и переключать `description` скриптом: + +```typescript +const scene = game.sceneManager.currentScene; +scene?.setTextRedirect('description', 'description_evening'); +``` + +Сброс: + +```typescript +scene?.clearTextRedirect('description'); +``` + +Таблица переадресации, как и другие runtime-изменения сцены, сохраняется вместе с сохранённой игрой. + +### Работа с `entity` + +Если скрипт вызван событием конкретного объекта, он получает его в `entity`. + +Основные операции, доступные на уровне `SceneObject`: + +- `entity.setTextRedirect(field, targetField)` +- `entity.clearTextRedirect(field)` +- `entity.description = '...'` +- `entity.customName = '...'` +- `entity.disabled = true/false` +- `entity.visible = true/false` +- `entity.groupID = '#tag'` +- `entity.layer = number` +- `entity.locked = true/false` + +Если `entity` является `Entity` или `Actor`, также доступны типичные визуальные и пространственные свойства: + +- `entity.x`, `entity.y` +- `entity.scale` +- `entity.parallax` +- `entity.opacity` +- `entity.blur` +- `entity.blendMode` +- `entity.setSprite(filename: string, keepSize?: boolean)` + +Пример: + +```typescript +ScriptRegistry.register('interaction.lamp.use', ({ entity }) => { + entity.visible = false; + entity.setTextRedirect('description', 'description_broken'); +}); +``` + +### Работа с `Actor` + +Если объект является `Actor`, для него доступны методы управления движением и анимацией: + +- `actor.setDirection(dir: 'up' | 'down' | 'left' | 'right')` +- `actor.walkTo(x, y)` +- `actor.moveTo(x, y)` +- `actor.stop()` +- `actor.setState(state)` +- `actor.playAnimSet(id: string)` +- `actor.resetAnimSet()` + +Пример: + +```typescript +ScriptRegistry.register('npc.go_to_door', ({ api }) => { + const hero = api.getActor('Hero'); + hero?.walkTo(180, 140); +}); +``` ### Пример скрипта ```typescript ScriptRegistry.register('interaction.pillar.key', ({ game, entity }) => { - game.showMessage('You insert the key into a hidden slot in the pillar.'); + game.showMessage(game.text('scripts.pillar_key_inserted')); game.playSound('secret_reveal.wav'); // Change pillar appearance entity.setSprite('pillar_open'); - entity.description = 'The pillar is open.'; + entity.description = game.text('scripts.pillar_open_description'); }); ``` @@ -353,11 +945,11 @@ export function registerUserScripts() { } ``` -## Текстовые ресурсы -Наша игра в значительной степени текстовая. Каждый объект или сцена имеет название, а также описания, выдаваемые при различных дейсвиях с объектами, например по методу look ("You see _a desk_"). Есть также тексты, предназначенные не для пользователя, а для SLM/LLM: описывающие возможные действия c предметами, промпты для задания нужной атмосферы и тп. Всё это удобно хранить в виде текстового файла, или файлов. Когда игра загружает сцену и объекты, она читает и текстовые ассеты с ними связанные. -# Редактор cцены + + +# Редактор cцены ################################# Используется для создания/редактирования cцен и объектов. Включается по нажатию клавиши F1. Визуально отображается как набор UI элементов за пределами пользовательского игрового экрана: @@ -371,7 +963,17 @@ export function registerUserScripts() { Визуально это oldschool кнопки с F-клавишами в стиле Norton Commander: F1 Game F2 Save F3 Load F4 New F5 Sprite Edit F9 Settings -При нажатии Shift: F2 Save As... +Нижнее меню является **динамическим smart-меню**: + +- в обычном состоянии оно показывает базовые F-клавиши редактора; +- при удержании модификаторов (`Shift`, `Alt`, `Ctrl`) оно временно перестраивается и показывает набор горячих клавиш, доступных с этим модификатором; +- подписи в таком режиме могут зависеть от текущего состояния редактора. Например, при удержании `Alt` для выделенного объекта может показываться `D Disable` или `D Enable` в зависимости от того, отключён объект или нет. + +Пример: + +- при нажатии `Shift`: `F2 Save As...`, а также `1 Save Sel. 1` и `2 Save Sel. 2` для сохранения текущего выделения в два быстрых selection slots +- при нажатии `Alt`: `F2 Save As...`, а также контекстные действия вроде `L Lock/Unlock` и `D Disable/Enable` +- при нажатии `Ctrl`: операции редактирования вроде `Undo`, `Redo`, `Duplicate`, `Copy`, `Paste`, `Save Prefab`, `Load Prefab` Пользователь может нажимать на них мышкой или использовать клавиатурные сочетания. @@ -462,17 +1064,18 @@ Prefab можно загрузить в текущую сцену из файл ### 1. Общие (General) -| Сочетание | Действие | Описание | -+-----------+-------------------+---------- | -| **F1** | Toggle Editor | Открыть/Закрыть редактор сцены | -| **F5** | Sprite Editor | Открыть/Закрыть редактор спрайтов | -| **F9** | Settings | Открыть/Закрыть настройки игры | -| **F2** | Smart Save | Быстрое сохранение сцены (по текущему пути) | -| **Shift+F2** | Save As... | Сохранить сцену как (открывает диалог) | -| **F3** | Load Scene | Загрузить сцену | -| **F4** | New Scene | Создать новую сцену | -| **Alt + L** | Lock Object | Заблокировать/разблокировать объект | -| **Ctrl+Z** | Undo/Redo | Отменить последнее действие | +| Сочетание | Действие | Описание | +| ------------ | ------------- | ----------------------------------------- | +| **F1** | Toggle Editor | Открыть/Закрыть редактор сцены | +| **F5** | Sprite Editor | Открыть/Закрыть редактор спрайтов | +| **F9** | Settings | Открыть/Закрыть настройки игры | +| **F2** | Smart Save | Быстрое сохранение сцены (ID = имя файла) | +| **Shift+F2** | Save As... | Сохранить сцену как... (открывает диалог) | +| **F3** | Load Scene | Загрузить сцену | +| **F4** | New Scene | Создать новую сцену | +| **Alt+L** | Lock Object | Заблокировать/разблокировать объект | +| **Ctrl+Z** | Undo | Отменить последнее действие | +| **Ctrl+R** | Redo | Вернуть отменённое действие | ### 2. Работа с объектами (Object Manipulation) @@ -485,10 +1088,16 @@ Prefab можно загрузить в текущую сцену из файл | **Ctrl + V** | Paste | Вставить объект/группу из буфера в позицию курсора (или в центр вида) | | **Ctrl + S** | Save Prefab | Сохранить текущее выделение (single/group) в файл Prefab | | **Ctrl + O** | Load Prefab | Загрузить prefab и вставить в позицию курсора (или в центр вида) | +| **Shift + 1** | Save Sel. 1 | Сохранить текущее single/group выделение в быстрый слот 1, если курсор над канвасом или Hierarchy | +| **Shift + 2** | Save Sel. 2 | Сохранить текущее single/group выделение в быстрый слот 2, если курсор над канвасом или Hierarchy | +| **1** | Restore Sel. 1 | Восстановить сохранённое выделение из быстрого слота 1, если курсор над канвасом или Hierarchy | +| **2** | Restore Sel. 2 | Восстановить сохранённое выделение из быстрого слота 2, если курсор над канвасом или Hierarchy | | **Insert** | Assign Sprite | Назначить спрайт выбранному объекту | | **Space** | Select Scene | Выбрать настройки сцены (и снять выделение с объектов), если курсор над канвасом | | **LMB Drag (empty area)** | Marquee Select | Выделить несколько объектов прямоугольной областью | +Быстрые selection slots запоминают именно текущий набор выделенных объектов по их ID в текущей сцене. Если слот пуст, или сохранённые объекты больше не существуют, редактор показывает пользователю Toast Notification. + ### 3. Создание объектов (Creation) | Клавиша | Действие | Описание | @@ -669,3 +1278,50 @@ ctx.fillStyle = Theme.backgroundColor; // Получает значение --ui 3. **Данные**: - В `QuadObject` добавляются свойства: `textureName` (string), `tileX` (number), `tileY` (number). - В редактор добавляется выбор текстуры и настройка тайлинга. + +## Отладочный profiler памяти сцен + +В движок встроен debug-profiler для оценки веса сцен и кэшей. Он доступен из DevTools Console через глобальный объект: + +```js +window.__QUEST_DEBUG__ +``` + +Доступные методы: + +```js +await __QUEST_DEBUG__.profileCurrentSceneMemory() +await __QUEST_DEBUG__.profileScenes(['test_room', 'home\\room']) +``` + +`profileCurrentSceneMemory()` снимает снимок по текущей сцене, а `profileScenes([...])` последовательно загружает перечисленные сцены и печатает `console.table(...)` с их метриками. + +Основные поля profiler-отчёта: + +- `weightUnits` — итоговый вес сцены в условных единицах cache policy. +- `graphWeightUnits` — вклад структуры сцены (объекты, компоненты, полигоны). +- `textureWeightUnits` — вклад текстур, доминирующий в общей оценке. +- `estimatedTextureMb` / `textureMb` — грубая оценка объёма уникальных изображений сцены. +- `jsHeapMb` — текущий объём JS heap после загрузки сцены. +- `jsHeapDeltaMb` / `deltaMb` — прирост JS heap между шагами batch-профилирования. +- `bytesPerUnit` / `kbPerUnit` — приблизительная стоимость одной unit-единицы в JS heap. +- `imageCacheMb` — текущий общий вес image cache. + +Пример использования: + +```js +const current = await __QUEST_DEBUG__.profileCurrentSceneMemory() +console.log(current.weightUnits, current.estimatedTextureMb) +``` + +```js +const batch = await __QUEST_DEBUG__.profileScenes(['test_room', 'home\\room']) +console.table(batch.map((row) => ({ + scene: row.sceneId, + units: row.weightUnits, + texMb: row.estimatedTextureMb, + heapDeltaMb: row.jsHeapDeltaMb, +}))) +``` + +Profiler предназначен для инженерной оценки и калибровки cache policy. Его числа не являются точным измерением всей RAM приложения: `jsHeap` отражает память JavaScript, а `estimatedTextureMb` является вычисляемой оценкой по размерам изображений. diff --git a/Parser.md b/Parser.md new file mode 100644 index 0000000..eda2d89 --- /dev/null +++ b/Parser.md @@ -0,0 +1,1190 @@ +# Parser + +## Summary + +`Parser` в `Blue Signal` — это отдельный оркестратор между игроком и движком `Scanline`. + +Он не является простым обработчиком команд. Его роль ближе к **Game Master**: +- принять ввод игрока; +- увидеть текущую картину мира через `context`; +- выбрать подходящий каскад распознавания; +- разрешить цели команды внутри собственной модели мира; +- вызвать допустимые игровые API; +- проанализировать outcomes; +- либо ответить игроку, +- либо задать уточнение, +- либо передать кейс более сильному каскаду, +- либо сделать следующую итерацию исполнения. + +Главный принцип: +- **вся языковая интерпретация живёт внутри parser-а**; +- `Game` и runtime не понимают язык игрока и не резолвят текстовые цели; +- `Game` только исполняет операции над уже понятными сущностями и возвращает structured outcomes; +- parser — не единственный клиент `Game API`: тем же shared API пользуются UI, scripts и игровая логика. + +--- + +## Design Goals + +Parser должен: +- быть единственной точкой интерпретации пользовательского текста; +- иметь собственную "картину мира", пригодную для текстового анализа; +- использовать движок как набор инструментов, а не как место принятия языковых решений; +- поддерживать несколько каскадов понимания ввода; +- уметь вести короткий диалог с игроком внутри одной незавершённой команды; +- быть локализуемым без переписывания логики; +- со временем уметь переходить от простого command parser-а к полноценному orchestrator/GM. + +--- + +## High-Level Architecture + +Ключевой момент: +- `Player Input` и `Parser Context` — это **две отдельные сущности**; +- `ParserWorldModelBuilder` работает только от состояния игры; +- ввод игрока проходит каскады **последовательно**, а не параллельно. + +```mermaid +flowchart TD + GS[Game state] + CB[ParserWorldModelBuilder] + CTX[Parser context] + U[Input] + L1[Stage1 Regex] + L2[Stage1 NLP] + S2[Stage2 LLM] + CORE[Parser Core] + API[Game API] + Q[Question] + R[Response] + + GS --> CB + CB --> CTX + + U --> L1 + CTX --> L1 + + L1 --> CORE + L1 --> L2 + + CTX --> L2 + L2 --> CORE + L2 --> S2 + + CTX --> S2 + S2 --> CORE + + CORE --> S2 + CORE --> Q + Q --> U + + CORE --> API + API --> CORE + + CORE --> R +``` + +Что важно в этой схеме: +- ввод игрока всегда сначала идёт в `Stage 1.1`; +- `Stage 1.2` включается только по handoff от `Stage 1.1`; +- следующий каскад включается только после провала всего первого каскада; +- `Core` получает два разных типа входа: + - распознанные данные от каскадов; + - outcomes от `Game API`. + +--- + +## Layers + +### 1. Raw Game State + +Это реальное состояние runtime: +- текущая сцена; +- объекты сцены; +- инвентарь игрока; +- активная subscene; +- registry сцен; +- состояния объектов и компонентов; +- player actor. + +Это не parser-слой. Это слой движка. + +### 2. ParserWorldModelBuilder + +`ParserWorldModelBuilder` не использует ввод игрока для определения intent или target. + +Он получает состояние игры, а также metadata текущего parser-цикла, и строит единый `ParserWorldModel`: +- `context` +- `scope` + +Текущий context включает: +- `rawInput` и `normalizedInput` как metadata текущего цикла parser-а; +- текущую сцену (`id`, `name`, `title`, `description`, `activeSubscene`); +- список текстово значимых объектов сцены; +- инвентарь игрока; +- spatial nodes and relation projection, derived from the runtime scene hierarchy; +- `pending state`, если parser уже ждёт уточнение. + +Текущий scope включает: +- `visible` +- `held` +- `takable` +- `reachable` +- `examinable` +- `subscene` +- `sceneTargets` + +Важно: +- `ParserWorldModelBuilder` не интерпретирует пользовательский ввод; +- он не выбирает intent; +- он не определяет target; +- он лишь даёт parser-у картину мира. + +Spatial-проекция приходит из `Game` уже в терминах world model. В частности, `Subscene` раскрывает для runtime и parser-а только **непосредственный** уровень вложенности за одну активацию: parser не должен сам вычислять рекурсивное раскрытие поддерева. + +Пример context: + +```json +{ + "rawInput": "look logo", + "normalizedInput": "LOOK LOGO", + "scene": { + "id": "test_room", + "name": "New Scene", + "title": "New Scene", + "description": "You are in New Scene." + }, + "entities": [ + { + "id": "logo_1", + "type": "Entity", + "title": "logo", + "description": "You see Scanline Engine logo.", + "details": null, + "interactions": [] + } + ], + "inventory": [], + "spatialNodes": [], + "spatialRelations": [], + "pending": null +} +``` + +### 3. Scope Model + +`Scope` — это не отдельный каскад и не отдельный runtime subsystem, а структурированная часть `ParserWorldModel`. + +То есть: +- `context` = всё, что parser знает о мире; +- `scope` = какая часть этого мира доступна для конкретного класса действий. + +Примеры: +- `LOOK` использует видимые объекты сцены и инвентарь; +- `TAKE` использует только берущиеся объекты сцены; +- `EXAMINE` использует инвентарь, объекты активной subscene и объекты в пределах допустимой дистанции; +- `GO TO` использует сценовые цели и достижимые сценовые объекты. + +Текущая модель scope: + +```ts +type ParserScope = { + visible: Entity[]; + held: Entity[]; + takable: Entity[]; + reachable: Entity[]; + examinable: Entity[]; + subscene: Entity[]; + sceneTargets: SceneDescriptor[]; +}; +``` + +Ключевой принцип: +- scope должен быть общим для всех каскадов; +- каскады различаются тем, **как они понимают ввод**; +- они не должны различаться тем, **как они понимают мир**. + +--- + +## Cascades + +## Stage 1 + +Stage 1 на самом деле состоит из двух внутренних уровней. + +### Stage 1.1 — Regex Parser + +Это быстрый, детерминированный, дешёвый слой. + +Он: +- пытается распознать canonical-команду; +- выделяет базовый `intent`; +- нормализует или очищает `target phrase`; +- для `LOOK` / `EXAMINE` умеет извлекать relation grammar вроде `under`, `in`, `behind`, `near`; +- собирает унифицированный envelope для `Core`. + +Подходит для: +- `LOOK` +- `LOOK LOGO` +- `LOOK UNDER TABLE` +- `EXAMINE BOOMBOX` +- `EXAMINE IN DRAWER` +- `TAKE KEY` +- `INV` +- `GO TO OFFICE` + +Важно: +- relation-aware parsing уже начинается на уровне `Stage 1.1`; +- пока runtime не хранит явные object relations, `Core` умеет только: + - распознать relation-query; + - разрешить anchor-object через обычный resolution/clarification flow; + - и затем вернуть честный fallback, что spatial relation пока не может быть определена. + +### Stage 1.2 — NLP Layer + +Этот слой включается только если `Stage 1.1` не справился. + +Он: +- определяет `intent` по более свободному вводу; +- оценивает confidence; +- очищает `target phrase`; +- собирает тот же унифицированный envelope для `Core`, что и `Stage 1.1`. + +Он полезен для: +- `look at the lamp` +- `pick up the key` +- `what do i have?` +- `go over to the office` + +Важно: +- глагол или verb phrase обычно определяет саму команду; +- слова вроде `with`, `on`, `to`, `in`, `under` обычно являются не отдельными командами, а grammar hints для связывания аргументов или relation semantics. + +Важно: +- `Stage 1.2` не занимается world reasoning; +- не должен сам принимать игровые решения; +- не должен сам резолвить сложные semantic target-и; +- не генерирует player-facing ответы. + +### Детальная схема Stage 1 + +```mermaid +flowchart TD + U[Player Input] + CTX[Parser Context] + + U --> R1[Stage 1.1 Regex Parser] + CTX --> R1 + + R1 --> R1A{Intent recognized?} + R1A -->|yes| R1B[Extract or normalize target phrase] + R1B --> R1C[Build cascade envelope] + R1C --> CORE[Parser Core] + + R1A -->|no| N1[Stage 1.2 NLP Layer] + CTX --> N1 + + N1 --> N1A[Classify intent] + N1A --> N1B{Confidence high enough?} + N1B -->|yes| N1C[Extract or normalize target phrase] + N1C --> N1D[Build cascade envelope] + N1D --> CORE + + N1B -->|no| H[Handoff to next cascade] +``` + +Что важно: +- `intent` определяется внутри уровня каскада; +- `target phrase` выделяется и очищается там же; +- затем уровень собирает единый envelope/protocol для `Core`; +- в `Core` приходит уже не сырой ввод, а первичная интерпретация команды. + +## Stage 2 — LLM / Future + +Следующий каскад — старший, LLM-based. + +Его роль намного шире: +- понимать сложные смысловые соответствия; +- строить многошаговые планы; +- генерировать player-facing тексты, когда lower layers не справились; +- задавать сложные уточнения; +- работать как настоящий Game Master. + +Например: +- `look logotype` -> понять, что речь о `logo`; +- `go to office` -> построить цепочку действий; +- `examine the thing under the desk` -> понять relation и target. + +В отличие от первых двух уровней, stage2 не обязан возвращать только `intent`. + +--- + +## Parser Core + +`Parser Core` — центральный оркестратор всей системы. + +Он получает: +- cascade envelope от активного каскада; +- outcomes от `Game API` по отдельному каналу. + +Именно `Core` принимает решения: +- достаточно ли данных для обработки команды; +- нужно ли звать следующий каскад; +- нужно ли задать clarification; +- какой API-блок вызвать; +- нужно ли сделать следующую итерацию; +- какой итоговый ответ показать игроку. + +### Детальная схема Core + +```mermaid +flowchart TD + IN[Cascade envelope] + OUT[API outcomes] + CORE[Parser Core] + RES[Resolve and validate] + DEC[Decision] + PLAN[Build API block] + API[Game API] + POST[Analyze outcomes] + CLAR[Clarification] + ASK[Missing argument] + ESC[Escalate] + LOOP[Next API step] + RESP[Final response] + Q[Question] + M[Message] + + IN --> CORE + OUT --> CORE + + CORE --> RES + RES --> DEC + + DEC --> PLAN + DEC --> CLAR + DEC --> ASK + DEC --> ESC + + PLAN --> API + API --> OUT + + CORE --> POST + POST --> RESP + POST --> CLAR + POST --> ESC + POST --> LOOP + + LOOP --> API + CLAR --> Q + ASK --> Q + RESP --> M +``` + +Самое важное утверждение: +- `Core` может эскалировать **до API**, если уже видит, что intent/target/данных недостаточно; +- `Core` может эскалировать **после API**, если полученных outcomes недостаточно для завершения сценария. + +Именно это делает parser не просто parser-ом, а оркестратором. + +--- + +## Action Flow + +### Step 1. Input arrives + +Игрок вводит текст. + +### Step 2. Pending clarification is checked + +Parser сначала проверяет: +- не является ли ввод продолжением уже незавершённой команды; +- или это новая команда. + +### Step 3. World model is built + +`ParserWorldModelBuilder` строит `ParserWorldModel` из состояния игры: +- `context` +- `scope` + +### Step 4. Stage 1 runs sequentially + +- сначала `Stage 1.1`; +- если не справился, `Stage 1.2`; +- если весь первый каскад не справился, handoff на следующий каскад. + +### Step 5. Core resolves, validates, and decides + +`Core` получает cascade envelope, применяет context/scope, и решает: +- можно ли продолжать; +- нужен ли API call block; +- нужен ли clarification; +- нужна ли эскалация выше. + +### Step 6. API block executes + +Если `Core` решил исполнять, он формирует блок API вызовов. + +### Step 7. Outcomes return to Core + +`Game API` возвращает structured outcomes. + +### Step 8. Core either completes or iterates + +`Core` может: +- завершить ответ; +- задать уточнение; +- передать кейс следующему каскаду; +- построить следующий API block и продолжить цикл. + +--- + +## Game API Contract + +`Game API` — это shared gameplay API движка. + +Он не принадлежит одному только parser-у и не занимается языком игрока. + +Текущий semantic API: +- `lookScene(scene?)` +- `lookEntity(entity)` +- `examineEntity(entity)` +- `takeEntity(entity)` +- `removeInventoryEntity(entity)` +- `showInventory()` +- `goToSceneTarget(rawTarget)` +- `goToScene(sceneId)` +- `goToEntity(entity)` +- `getSeeMessage(target)` +- `describeSpatialRelation(anchorNodeId, relation)` + +Принцип: +- parser — один из клиентов `Game API`, а не его единственный владелец; +- тем же API могут пользоваться UI, scripts и игровая логика; +- parser передаёт в `Game` уже resolved цели; +- `Game` не подбирает объекты по тексту; +- `Game` не делает disambiguation; +- `Game` не разбирает user input. + +Уточнение по UI: +- текущий UI-клик по объекту считается корректным, если он показывает player-facing `title` объекта в консоли; +- UI-клик не обязан вызывать `lookEntity(...)`; +- это presentation-level behavior, а не parser semantics; +- в будущем parser-side `LOOK` тоже может использовать схожее перечисление видимых названий объектов, не требуя маршрутизации UI через parser. + +Следствие: +- на `Scanline` можно сделать не только parser-driven игру; +- при расширении полномочий UI на этом же API можно построить чистый point-and-click quest. + +### Что делает Game + +`Game` отвечает за: +- реальные операции в мире; +- валидацию игровых ограничений; +- structured outcomes. + +Например: +- `takeEntity(entity)` проверяет дистанцию и возможность взять предмет; +- `examineEntity(entity)` проверяет доступность examine; +- `goToSceneTarget(rawTarget)` оставляет `Game` знание о registry сцен и валидности перехода; +- `describeSpatialRelation(anchorNodeId, relation)` формирует player-facing spatial response на основе runtime world model; +- `goToEntity(entity)` запускает movement; +- `lookEntity(entity)` возвращает краткое описание с учётом spatial parent context, если он есть. + +То есть: +- parser отвечает за язык и выбор цели; +- `Game` отвечает за допустимость и исполнение операции. + +--- + +## Current Envelope And Actions + +Сейчас lower cascades (`Stage 1.1` и `Stage 1.2`) уже отдают единый `ParserCascadeEnvelope`. + +Текущие `ParserToolAction`: +- `lookScene` +- `lookTarget` +- `examineTarget` +- `takeTarget` +- `showInventory` +- `goToTarget` + +Текущий envelope имеет вид: +- `output.kind = 'plan'` +- `output.kind = 'handoff_up'` + +То есть: +- handoff больше не кодируется отдельным fake-action; +- `Parser Core` принимает envelope напрямую и сам решает, что это значит до API. + +--- + +## Target Resolution + +### Current Resolution Model + +Сейчас target resolution уже принадлежит parser-у. + +Parser: +- ищет цели в собственной модели мира; +- использует только player-facing `title`, а не технические `id`; +- может использовать опциональные `synonyms`, если они заданы в TA объекта; +- исключает `disabled` объекты сцены; +- поддерживает partial matching; +- поддерживает clarification при неоднозначности. + +При этом parser полезно различает: +- **command verb**: например `use`, `unlock`, `look`, `teleport` +- **grammar markers / relations**: например `with`, `on`, `to`, `in`, `under` + +Эти слова не обязательно являются частью самой команды. +Чаще они помогают parser-у: +- назначать роли аргументам; +- выбирать relation-aware scope; +- понимать структуру одной и той же команды в разных формулировках. + +### Object TA fields relevant to target resolution + +Для object TA важны не только: +- `title` +- `description` +- `details` + +Но и новое опциональное поле: +- `synonyms` + +Пример: + +```json +{ + "title": "logo", + "description": "You see Scanline Engine logo.", + "details": "Extended description here.", + "synonyms": ["logotype", "emblem", "scanline symbol"] +} +``` + +Поле `synonyms`: +- является parser-owned text knowledge; +- помогает точнее определять target без обращения к LLM; +- должно входить в шаблон нового object TA, даже если список пустой. + +Поле `details`: +- является стандартным полем object TA; +- используется действием `EXAMINE`; +- тоже входит в стандартный шаблон нового object TA. + +Стандартный шаблон нового object TA: + +```json +{ + "title": "Object", + "description": "You see nothing special.", + "details": "", + "synonyms": [] +} +``` + +### Inventory-aware resolution + +Инвентарь является частью доступного текстового мира для non-movement действий. + +Сейчас: +- `LOOK` может находить предметы в инвентаре; +- `EXAMINE` может находить предметы в инвентаре; +- `TAKE` и `GO TO` inventory не используют. + +### EXAMINE + +`EXAMINE` — отдельное действие, отличное от `LOOK`. + +- `LOOK` использует обычное краткое описание (`description`); +- `EXAMINE` использует расширенное описание (`details`). + +Если `details` отсутствует: +- lower layer не обязан это придумывать; +- `Game.examineEntity()` может вернуть `escalate`; +- старший каскад решит, что делать дальше. + +### Access rules for EXAMINE + +Игрок может examine объект, если он: +- лежит в инвентаре; +- находится в активной subscene; +- находится достаточно близко, по той же дистанции, что и `TAKE`. + +Это правило относится к игровому миру, а не к языку, поэтому применяется на стороне `Game.examineEntity()`. + +--- + +## Pending Clarification + +Parser может задавать вопросы, если ввода недостаточно. + +Примеры: +- `TAKE` -> `Take what?` +- `EXAMINE` -> `Examine what?` +- `GO TO` -> `Where do you want to go?` +- ambiguity -> `Which one do you mean ...?` + +Важно: +- parser задаёт ambiguity-question только если может показать игроку действительно различимые варианты; +- если несколько кандидатов имеют один и тот же player-facing `title`, parser не должен зацикливать уточнение; +- в таком случае применяется детерминированный tie-break: + - сначала предметы в инвентаре; + - если их несколько, по порядку инвентаря; + - иначе ближайший объект сцены. + +Parser хранит `pendingState`: +- intent +- question +- originalInput + +Следующий ввод: +- либо трактуется как продолжение текущей команды; +- либо отменяет pending flow, если выглядит как новая команда. + +```mermaid +sequenceDiagram + participant P as Player + participant R as Parser + participant G as Game + + P->>R: TAKE + R->>G: takeTarget(null) + G-->>R: needs_clarification + R-->>P: Take what? + + P->>R: key + R->>R: resolve as continuation of TAKE + R->>G: takeEntity(key) + G-->>R: ok / failed + R-->>P: final response +``` + +--- + +## Unified Cascade Output Model + +Первые два уровня parser-а по сути формируют пакет данных для одного и того же `Core`. + +То есть: +- `Stage 1.1` и `Stage 1.2` — это не два разных parser-а; +- это два разных способа превратить ввод игрока в данные для `Core`. + +Главный архитектурный вывод: +- protocol взаимодействия с `Core` должен быть единым для всех каскадов; +- нижние каскады могут использовать только простой subset этого protocol; +- старший каскад может использовать более богатые формы того же protocol. + +Это важно, потому что: +- позволяет отлаживать `Core` и execution loop без реальной LLM; +- позволяет мокать сложные LLM-сценарии через `Stage 1`; +- позволяет стабилизировать orchestration до подключения непредсказуемой модели. + +--- + +## Unified Parser DSL (First Draft) + +Будущий старший каскад (LLM) должен уметь возвращать не только `intent`, но и richer instructions. + +Однако он не должен: +- напрямую вызывать `Game API`; +- исполнять произвольный код; +- писать свободный JS; +- обходить `Parser Core`. + +Поэтому нужен **ограниченный parser DSL**. + +Важно: +- этот DSL не должен быть "особым форматом только для Stage 2"; +- это должен быть общий protocol общения cascade layers с `Core`; +- `Stage 1.1` и `Stage 1.2` просто используют его более простой subset. + +### Общий смысл DSL + +LLM возвращает не код, а допустимый план шагов. + +`Core`: +- валидирует этот план; +- исполняет шаги по одному; +- собирает outcomes; +- при необходимости повторно зовёт старший каскад. + +### Богатые выходы каскада + +Каскадный уровень должен уметь возвращать не только `intent`, но и: +- `plan` +- `clarification` +- `final_response` +- `handoff_up` + +То есть `Core` должен уметь принимать richer cascade outputs. + +### Первый вариант envelope + +```ts +type CascadeEnvelope = + | { + stage: 'regex-v1' | 'nlp-v2' | 'llm-v3'; + output: { + kind: 'intent'; + intent: string; + target?: string | null; + }; + } + | { + stage: 'regex-v1' | 'nlp-v2' | 'llm-v3'; + output: { + kind: 'plan'; + actions: ParserPlannedAction[]; + }; + } + | { + stage: 'regex-v1' | 'nlp-v2' | 'llm-v3'; + output: { + kind: 'clarification'; + question: string; + missing: string; + }; + } + | { + stage: 'regex-v1' | 'nlp-v2' | 'llm-v3'; + output: { + kind: 'final_response'; + message: string; + }; + } + | { + stage: 'regex-v1' | 'nlp-v2' | 'llm-v3'; + output: { + kind: 'handoff_up'; + reason: string; + }; + }; +``` + +### Первый вариант `ParserPlannedAction` + +Целевой DSL может быть богаче, но текущая реализация уже поддерживает полезный ограниченный subset: + +```ts +type ParserPlannedAction = + | { + type: 'resolveArgumentEntity'; + commandId: string; + arg: string; + query: string | null; + scopes: ParserScopeSlice[]; + saveAs: string; + messages?: ParserCommandArgumentMessages; + validation?: ParserCommandArgumentValidation; + } + | { type: 'ensureHeldEntity'; ref: string; noEffectMessage?: string } + | { type: 'goToSceneById'; sceneId: string } + | { type: 'removeInventoryEntity'; ref: string } + | { + type: 'showText'; + message?: string; + textKey?: string; + params?: Record; + paramsFromRefs?: Record; + }; +``` + +Этого уже хватает для: +- `TELEPORT WITH item`; +- двухаргументных custom commands вроде `USE X ON Y`; +- generic clarification и validation на уровне `Parser Core`; +- подстановки resolved entity titles в финальные сообщения через `paramsFromRefs`. + +### Почему DSL должен быть ограниченным + +Это важно для безопасности и устойчивости архитектуры. + +Ни один каскад не должен: +- писать произвольный код; +- обращаться к внутренностям runtime напрямую; +- вносить неконтролируемые side effects. + +Поэтому DSL должен быть: +- декларативным; +- ограниченным; +- валидируемым `Core`-ом; +- исполняемым только через разрешённые игровые API. + +### Важный принцип DSL + +Первый вариант DSL лучше делать **линейным**, без встроенных `if/else` и циклов. + +То есть: +- каскад предлагает список шагов; +- `Core` исполняет их по одному; +- при неожиданном outcome `Core` останавливает план и снова зовёт следующий подходящий уровень. + +Это проще и надёжнее, чем сразу делать полноценный mini-language. + +### Пример планового потока + +```mermaid +sequenceDiagram + participant P as Player + participant C as Parser Core + participant L as Stage 2 LLM + participant G as Game API + + P->>C: go to office + C->>L: unresolved complex command + context + L-->>C: plan(actions[]) + C->>G: execute action 1 + G-->>C: outcome 1 + C->>G: execute action 2 + G-->>C: outcome 2 + C->>L: outcomes summary / interrupted plan + L-->>C: clarification or new plan or final response + C-->>P: message or question +``` + +--- + +## Parser Debugging + +Для отладки используются служебные команды консоли: +- `#PEEK-ON` +- `#PEEK-OFF` +- `#STAGE1-ON` +- `#STAGE1-OFF` +- `#STAGE2-ON` +- `#STAGE2-OFF` + +### PEEK + +При `#PEEK-ON` parser выводит: +- `context=...` +- `scope=...` +- `envelope=...` +- `core=...` +- `result=...` +- `nlp=...` при участии NLP-слоя + +Это даёт возможность смотреть отдельно: +- world model snapshot; +- scope slices; +- cascade output; +- решение `Core`; +- итоговые outcomes. + +### Stage toggles + +Можно изолированно тестировать разные уровни: +- `#STAGE1-ON` / `#STAGE1-OFF` управляют `Stage 1.1`; +- `#STAGE2-ON` / `#STAGE2-OFF` управляют `Stage 1.2`; +- это полезно для отладки `Core` и DSL без реальной LLM. + +--- + +## Language Assets + +Parser должен быть локализуемым без переписывания логики. + +### Что должно жить в text assets + +Всё language-specific: +- player-facing parser strings; +- clarification prompts; +- NLP training phrases; +- stage1 lexicon и normalisation vocabulary: + - verbs; + - aliases; + - articles; + - polite prefixes; + - prepositional phrases and grammar markers. + +Текущая раскладка: +- `public/text/system/parser.json` — player-facing parser strings; +- `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; +- `Commands.md` — формат и принципы command TA. + +Текущее применение: +- `Stage 1.1` использует `parser-lexicon.json` для: + - command aliases; + - command-word detection; + - target normalization; + - scene-look special words (`around`, `here`, `scene`); +- `Stage 1.2` использует: + - `parser-training.json` как training corpus для `NLP.js`; + - `parser-lexicon.json` для той же target normalization, что и у `Stage 1.1`. + +То есть stage1 и stage2 уже питаются от одного и того же language pack, а не от независимых словарей в коде. + +### Что остаётся в коде + +- internal intent ids (`look`, `take`, `examine`, `goTo`); +- parser action ids (`lookTarget`, `takeTarget`, etc); +- `Game API` contracts; +- dev/system console commands вроде `#RUN`, `#PEEK`. + +### Предпочтительный формат language assets + +Language assets лучше хранить как **структурированные словари**, а не как сырые regex-строки. + +Пример: + +```json +{ + "stage1Aliases": { + "look": ["look"], + "examine": ["examine", "inspect", "check"], + "take": ["take", "get", "pickup", "pick up"], + "goTo": ["go", "walk", "move"], + "showInventory": ["inventory", "inv"] + }, + "normalizationPrefixes": { + "look": ["look at", "tell me about", "what is", "describe"], + "examine": ["take a closer look at", "look closely at", "examine", "inspect", "check"], + "take": ["pick up", "take", "get", "grab"], + "goTo": ["go over to", "go to", "walk to", "move to", "go", "walk", "move"] + }, + "articles": ["the", "a", "an", "my"], + "politePrefixes": ["please", "could you", "can you", "would you", "i want to"], + "lookSceneWords": ["around", "here", "scene"] +} +``` + +--- + +## Why Stage 1.2 Still Matters + +NLP-слой полезен, но не является фундаментом parser-а. + +Его роль: +- сделать ввод менее хрупким; +- поддержать более естественные формулировки; +- выдавать тот же internal package, что и regex layer. + +Фундамент parser-а — это: +- `Context Builder`; +- `Scope`; +- `Relations`; +- `Parser Core`. + +То есть: +- `Stage 1.1` = strict command parser; +- `Stage 1.2` = language comfort layer; +- `Stage 2` = semantic reasoning / Game Master layer. + +--- + +## Future: Relations and World Understanding + +Следующий важный шаг — richer world model. + +Например: +- `key under table` +- `note in drawer` +- `coin behind the picture` + +Тогда parser сможет различать: +- `look table` +- `look under table` +- `examine drawer` +- `look in drawer` + +Runtime relation model is now owned by `Game` / scene data, not by parser. Parser consumes a projection of that model through `ParserWorldModelBuilder`. + +Parser-facing relation projection: + +```ts +type ParserRelation = { + anchorNodeId: string; + relation: 'on' | 'under' | 'in' | 'behind'; + childNodeIds: string[]; +}; +``` + +Current state: +- `LOOK UNDER X` +- `LOOK IN X` +- `LOOK BEHIND X` + +already execute against real runtime spatial data. `near` remains parser-recognized but is intentionally not executed yet because its runtime semantics are still undefined. + +Именно richer context/scope/relations дадут parser-у настоящую "картину мира". + +--- + +## Technical Organization + +Текущие роли по коду: + +- `src/mechanics/Parser.ts` + - главный orchestrator parser-а + - stage orchestration + - target resolution + - pending clarification + - unified envelope intake + - `Parser Core` + - pre-API decision making + - linear plan execution + - response building + +- `src/mechanics/ParserWorldModelBuilder.ts` + - строит `ParserWorldModel` + - собирает `ParserContext` + - собирает `ParserScope` + - добавляет parser-facing данные по: + - scene entities + - inventory + - subscene + - scene registry + - object `synonyms` + +- `src/mechanics/NlpCascade.ts` + - Stage 1.2 (`NLP.js`) + - intent recognition + target cleanup + - возвращает тот же `ParserCascadeEnvelope`, что и regex-слой + +- `src/mechanics/parserLanguage.ts` + - stage1 lexicon helpers + - target normalization + - parser language-pack access helpers + +- `src/mechanics/parserCommands.ts` + - parser custom command matching + - phrase matching + - multi-argument splitting through `separatorsBefore` + +- `src/mechanics/parserTypes.ts` + - parser-facing types + - `ParserWorldModel` + - `ParserScope` + - `ParserCascadeEnvelope` + - `ParserCoreDecision` + - `ParserToolAction` + - `ParserCommandSpec` + - `ParserPlanState` + +- `src/core/Game.ts` + - semantic runtime tools + - world operations on resolved scene/entity targets + - access checks and structured outcomes + +- `src/core/IGame.ts` + - shared `Game API` contract used by parser and other clients + +- `src/core/TextAssetManager.ts` + - service text assets + - scene/object text resolution + - parser lexicon assets + - parser training assets + - parser command assets + - object fields such as `details` + - object list fields such as `synonyms` + +- `src/core/Console.ts` + - console command handling before gameplay parser + - gameplay input preprocessor + - stage toggles: + - `#STAGE1-ON/OFF` + - `#STAGE2-ON/OFF` + - parser debug toggle: + - `#PEEK-ON/OFF` + +- `src/components/UIOverlay.tsx` + - entry point from UI input to console preprocessor and gameplay parser + +### Code Map By Architecture Block + +| Architecture block | Main files | Key methods / responsibilities | +|---|---|---| +| Player input entry | `src/components/UIOverlay.tsx`, `src/core/Console.ts` | `UIOverlay` routes typed input into `console.preprocessGameplayInput(...)` before parser execution | +| Console preprocessor | `src/core/Console.ts` | `preprocessGameplayInput(...)`, stage toggles, shorthand expansion | +| World model builder | `src/mechanics/ParserWorldModelBuilder.ts` | `build(...)` returns `{ context, scope }` | +| Stage 1.1 regex | `src/mechanics/Parser.ts`, `src/mechanics/parserLanguage.ts` | `runStage1(...)`, `matchStage1Intent(...)`, `normalizeTargetForIntent(...)` | +| Custom command matching | `src/mechanics/parserCommands.ts`, `src/mechanics/Parser.ts` | `matchParserCommandSpec(...)`, `buildCustomCommandEnvelope(...)`, multi-argument extraction | +| Stage 1.2 NLP | `src/mechanics/NlpCascade.ts` | `parse(...)`, training on parser language assets, envelope generation | +| Parser Core | `src/mechanics/Parser.ts` | `runParserCore(...)`, `makeCoreDecision(...)`, `executeCoreDecision(...)`, `executeCorePlan(...)` | +| Scope-driven resolution | `src/mechanics/Parser.ts` | `resolveLookTarget(...)`, `resolveExamineTarget(...)`, `resolveTakeTarget(...)`, `resolveGoToTarget(...)`, `resolveEntityTargetInCandidates(...)` | +| Shared gameplay API | `src/core/Game.ts`, `src/core/IGame.ts` | `lookScene(...)`, `lookEntity(...)`, `examineEntity(...)`, `takeEntity(...)`, `goToScene(...)`, `goToEntity(...)`, `showInventory()`, `removeInventoryEntity(...)` | +| Text assets | `src/core/TextAssetManager.ts`, `public/text/system/*.json` | `getParserLexicon()`, `getParserTraining()`, `getParserCommands()`, `getResolvedObjectField(...)`, `getResolvedObjectListField(...)` | +| Parser debugging | `src/mechanics/Parser.ts`, `src/core/Console.ts` | `#PEEK`, stage toggles, debug output for `scope/envelope/core/result/nlp` | + +### Separation of concerns + +```mermaid +flowchart TD + A[Parser] -->|resolved target| B[Game API] + B -->|outcome| A + + A --> C[Language interpretation] + A --> D[Clarification] + A --> E[Target resolution] + A --> F[Plan orchestration] + + B --> G[World rules] + B --> H[Movement] + B --> I[Inventory mutation] + B --> J[Script execution] +``` + +Главное правило: +- parser понимает язык и управляет сценарием обработки; +- `Game` исполняет допустимые действия в игровом мире. + +--- + +## Current State vs Target State + +### Уже реализовано + +- parser-mediator v1; +- первый каскад с двумя уровнями (`regex` + `NLP.js`); +- unified cascade envelope model; +- `ParserWorldModelBuilder`; +- explicit scope slices; +- parser-owned target resolution; +- inventory-aware `LOOK` / `EXAMINE`; +- отдельный `EXAMINE` + `details`; +- pending clarification; +- parser debug via `#PEEK`; +- stage toggles via console; +- Game API с resolved targets; +- linear plan execution in `Parser Core` for non-LLM producers; +- parser custom command assets via `public/text/system/commands/*.json`; +- first generic multi-step command `TELEPORT WITH`; +- first generic two-argument command path `USE X ON Y`; +- базовая groundwork for future stage-2 DSL. + +### Дальше + +- parser relations (`on`, `under`, `in`, `behind`, ...); +- richer stage-2 (LLM) handoff; +- полноценный DSL execution loop; +- более сложные semantic actions (`use`, `open`, `talkTo`, ...); +- richer dialog/session state. + +--- + +## Core Principles Recap + +1. Parser — единственный слой, интерпретирующий язык игрока. +2. `Game API` — общий gameplay API для parser-а, UI, scripts и игровой логики. +3. `Game` и runtime не должны парсить текст и резолвить текстовые цели. +4. `ParserWorldModelBuilder` строит world model только из состояния игры. +5. `Player Input` и `Parser Context` — отдельные входы parser-а. +6. Stage processing последовательный, а не параллельный. +7. Первый каскад имеет два внутренних уровня: `regex`, затем `NLP`. +8. Оба уровня Stage 1 работают по одной логике: `intent -> target -> envelope`. +9. Все каскады подают данные в один и тот же `Parser Core`. +10. `Core` может эскалировать как до API, так и после API. +11. `Core` — центр clarification, orchestration, iteration и final response. +12. DSL/protocol общения с `Core` должен быть единым для всех каскадов, даже если нижние уровни используют только простой subset. +13. Object TA содержит стандартные parser-relevant поля `title`, `description`, `details`; также может содержать опциональное поле `synonyms` для повышения точности target resolution. +14. Player-facing messages никогда не должны показывать технические `id`. +15. Всё language-specific должно жить в text assets. +16. Console preprocessor работает до gameplay parser-а и отвечает за shorthand-ы и stage toggles. +17. UI-клик по объекту может показывать `title` напрямую, не вызывая parser semantics `LOOK`. + +Эта архитектура делает parser фундаментом для постепенного перехода от классического IF-style command parser-а к полноценному Game Master и orchestrator. diff --git a/ParserSmoke.md b/ParserSmoke.md new file mode 100644 index 0000000..ea359a4 --- /dev/null +++ b/ParserSmoke.md @@ -0,0 +1,143 @@ +# Parser Smoke Checks + +These checks are intended for quick manual regression testing of the current parser stack in `Scanline`, before the future LLM cascade exists. + +Run them in a scene that contains at least: +- one visible object with `title` +- one object with `details` +- one takeable item +- one inventory item after pickup +- one object or scene target addressable via `synonyms` + +## Baseline + +1. Enable parser debug when needed: + - `#PEEK-ON` +2. Ensure both lower layers are enabled: + - `#STAGE1-ON` + - `#STAGE2-ON` + +Expected: +- parser accepts commands normally +- `#PEEK` shows `context`, `scope`, `envelope`, `core`, `result` + +## Stage Toggles + +1. Disable stage 1: + - `#STAGE1-OFF` +2. Enter a phrase that only NLP should understand: + - `go over to the office` +3. Re-enable stage 1: + - `#STAGE1-ON` +4. Disable stage 2: + - `#STAGE2-OFF` +5. Retry the same NLP-only phrase. +6. Re-enable stage 2: + - `#STAGE2-ON` + +Expected: +- with `#STAGE1-OFF`, stage 1 bypasses and stage 2 can still parse +- with `#STAGE2-OFF`, stage 1 handoff does not reach NLP + +## Clarification Flow + +1. `take` +2. respond with a target, for example: + - `key` + +Repeat for: +- `examine` +- `go to` + +Expected: +- parser asks clarification question +- second input is treated as continuation +- `pendingState` clears after completion or failure + +## Inventory-Aware Resolution + +1. Pick up a visible item: + - `take id` +2. Check inventory: + - `i` +3. Look at carried item: + - `look id` + - `look id card` +4. Examine carried item: + - `x id` + +Expected: +- carried item appears in inventory +- `LOOK` and `EXAMINE` can resolve inventory items +- `TAKE` and `GO TO` do not use inventory as target space + +## Synonyms + +Use an object that has `synonyms` in its TA. + +Examples: +- `look logotype` +- `look recorder` +- `take radio` +- `go to tape recorder` + +Expected: +- parser resolves by `title` or `synonyms` +- ambiguity clarification appears if multiple candidates match + +## EXAMINE vs LOOK + +1. `look boombox` +2. `x boombox` + +Expected: +- `LOOK` returns short `description` +- `EXAMINE` returns `details` +- if `details` are missing, parser escalates instead of inventing text locally + +## Pre-API vs Post-API Escalation + +Pre-API example: +- use input unsupported by stage 1 and stage 2 disabled + +Post-API example: +- `x logo` when object has no `details` + +Expected: +- pre-API escalation appears in `core` +- post-API escalation appears in `result.outcomes` + +## Console Preprocessor + +1. `i` +2. `l logo` +3. `x boombox` + +Control case: +- `where i am?` + +Expected: +- `i` becomes `INVENTORY` +- `l ...` becomes `LOOK ...` +- `x ...` becomes `EXAMINE ...` +- normal sentences containing `i` are not rewritten to inventory commands + +## Scope Checks + +With `#PEEK-ON`, verify: +- `visible` contains visible scene entities +- `held` contains inventory entities +- `takable` contains only takeable scene entities +- `examinable` contains held, reachable, and subscene entities +- `sceneTargets` contains registered destination scenes + +## Current Success Criteria + +The smoke run is considered healthy when: +- no parser exceptions appear in the console +- `#PEEK` shows coherent `scope`, `envelope`, and `core` data +- clarification flows work +- inventory-aware resolution works +- synonym resolution works +- stage toggles work +- `LOOK` and `EXAMINE` remain distinct diff --git a/TextAssets.md b/TextAssets.md new file mode 100644 index 0000000..31c7817 --- /dev/null +++ b/TextAssets.md @@ -0,0 +1,71 @@ +# Text Assets + +## V1 decision + +We start with a minimal text asset system for scene and object descriptions. + +Text assets are stored separately from scene and prefab JSON files: + +- `public/text/scenes/.json` +- `public/text/objects/.json` + +Since scene/object IDs according to GDD can contain paths like "building\room", which means that the 'room.json scene' is located in the 'building' folder, there may be subfolders inside these folders. + +## Main rules + +- Scene text asset is created automatically when a scene is created or first saved, if it does not exist yet. +- Object text asset is stored independently from scenes and prefabs, because objects may exist outside a scene or move between scenes. +- Missing text asset files are not errors; runtime falls back to existing built-in fields. +- Text assets contain only data, not code. +- Dynamic text changes are controlled by scripts through runtime properties of scenes and objects. + +## Minimal fields + +Scene asset: + +- `title` +- `description` + +Object asset: + +- `title` +- `description` + +## Custom text variants + +Text assets may also contain custom named fields in the same JSON file, for example: + +- `description_morning` +- `description_evening` +- `title_locked` + +These are alternative text values that can be activated at runtime. + +## Runtime redirection + +The redirection table does not live inside text asset JSON files. + +Instead, each scene and object may have a runtime property such as `textRedirects` that remaps standard text fields to custom fields from the same text asset. + +Example: + +```json +{ + "description": "description_evening" +} +``` + +Meaning: + +- when runtime asks for `description`, it should use `description_evening` from the text asset; +- if no redirect is set, the default `description` field is used; +- if redirect points to a missing field, runtime should fall back to the standard field. + +Scripts do not generate text themselves. They only change which named text field is currently active. + +## Runtime integration + +- `title` maps to the user-facing object or scene name. +- `description` maps to the basic text used by parser/runtime for `look` or `look around`. +- Existing runtime fields remain as fallback and for backward compatibility. +- Parser and UI should read only the resolved standard fields, not custom variant names directly. diff --git a/package-lock.json b/package-lock.json index 214dce5..9b10881 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "temp_vite", "version": "0.0.0", "dependencies": { + "@nlpjs/core": "^5.0.0-alpha.5", + "@nlpjs/lang-en-min": "^5.0.0-alpha.5", + "@nlpjs/nlp": "^5.0.0-alpha.5", "react": "^19.2.0", "react-dom": "^19.2.0", "zustand": "^5.0.9" @@ -30,7 +33,8 @@ "prettier": "^3.3.3", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "vite": "^7.2.4", + "vitest": "^4.1.0" } }, "node_modules/@babel/code-frame": { @@ -1017,6 +1021,102 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nlpjs/core": { + "version": "5.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@nlpjs/core/-/core-5.0.0-alpha.5.tgz", + "integrity": "sha512-6KER6Jsy/KLVNor2isRsYV5XjVtYVaYLLqEmQt69Nmw2tIYyzzhzPxZPmOaU++OWfiUaZWGMgNueJHAmK6sApw==", + "license": "MIT" + }, + "node_modules/@nlpjs/lang-en-min": { + "version": "5.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@nlpjs/lang-en-min/-/lang-en-min-5.0.0-alpha.5.tgz", + "integrity": "sha512-a3k8VrJ6jOp9XSoz25kJR1N/kMaGjzb/B7dS1S5bqCvGY9sm+Xr4C9PCTZwEP4WgD2QZLCLEauiiKXM59zxwDQ==", + "license": "MIT", + "dependencies": { + "@nlpjs/core": "^5.0.0-alpha.5" + } + }, + "node_modules/@nlpjs/language-min": { + "version": "5.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@nlpjs/language-min/-/language-min-5.0.0-alpha.5.tgz", + "integrity": "sha512-O4tnPFqiSxK2Ukv0WD+NhviaBSjW8xq2GJFzQo3dKEief0JJQVK0McYzkahgyInhy0J/q4z9KGFzGs5VEVgwfA==", + "license": "MIT" + }, + "node_modules/@nlpjs/ner": { + "version": "5.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@nlpjs/ner/-/ner-5.0.0-alpha.5.tgz", + "integrity": "sha512-abtdWNXqEmWmRwRFCLnv72g2wMgcmWcEes6kL895GmevS/NseOE8j87ufTwPuk77Fgcga/z794eQaFU7Wn2HTA==", + "license": "MIT", + "dependencies": { + "@nlpjs/core": "^5.0.0-alpha.5", + "@nlpjs/language-min": "^5.0.0-alpha.5", + "@nlpjs/similarity": "^5.0.0-alpha.5" + } + }, + "node_modules/@nlpjs/neural": { + "version": "5.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@nlpjs/neural/-/neural-5.0.0-alpha.5.tgz", + "integrity": "sha512-QayWLhzfGardmIwJXEK7ojb6O2GCwJl5CQhwy8VI/zZtCvZ/l7ABKI0/tU0qhbPXWPg3v0PR3k5yLSBcX4XSPg==", + "license": "MIT" + }, + "node_modules/@nlpjs/nlg": { + "version": "5.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@nlpjs/nlg/-/nlg-5.0.0-alpha.5.tgz", + "integrity": "sha512-+gaNb1dcqzocR3CNdnvk+Tt5eAnWXCrnWmW99i6KB8FaKSMi8pzPmR1fZwCGVqhqlSRWAGLSRpEMpZ2qQuKjDQ==", + "license": "MIT", + "dependencies": { + "@nlpjs/core": "^5.0.0-alpha.5" + } + }, + "node_modules/@nlpjs/nlp": { + "version": "5.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@nlpjs/nlp/-/nlp-5.0.0-alpha.5.tgz", + "integrity": "sha512-K/MfazhTOWNjdn9Xy2PydYtziPHN5ZHFBfOKa5acWt26fdGiORBDL7ee2UqfM+Y1poZ9qaqbDHRi0O/R6YwpkA==", + "license": "MIT", + "dependencies": { + "@nlpjs/core": "^5.0.0-alpha.5", + "@nlpjs/ner": "^5.0.0-alpha.5", + "@nlpjs/nlg": "^5.0.0-alpha.5", + "@nlpjs/nlu": "^5.0.0-alpha.5", + "@nlpjs/sentiment": "^5.0.0-alpha.5", + "@nlpjs/slot": "^5.0.0-alpha.5" + } + }, + "node_modules/@nlpjs/nlu": { + "version": "5.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@nlpjs/nlu/-/nlu-5.0.0-alpha.5.tgz", + "integrity": "sha512-3DwDgGlpusuFXa3+8YFzm8BAzFT49PzRolUJDDOoMMhrF5s+J21XmtWPssFZjNfHjuqjssQRx4k+sJ9/QGfzXQ==", + "license": "MIT", + "dependencies": { + "@nlpjs/core": "^5.0.0-alpha.5", + "@nlpjs/language-min": "^5.0.0-alpha.5", + "@nlpjs/neural": "^5.0.0-alpha.5", + "@nlpjs/similarity": "^5.0.0-alpha.5" + } + }, + "node_modules/@nlpjs/sentiment": { + "version": "5.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@nlpjs/sentiment/-/sentiment-5.0.0-alpha.5.tgz", + "integrity": "sha512-yNk3lnZ7g1MFCnrm9TcZnFL1sK4IsBkBXzyw7gygG8lTlNZgPsumODo449fqWc1SHN2sz1K/dmSslBb6Iq2IcA==", + "license": "MIT", + "dependencies": { + "@nlpjs/core": "^5.0.0-alpha.5", + "@nlpjs/language-min": "^5.0.0-alpha.5", + "@nlpjs/neural": "^5.0.0-alpha.5" + } + }, + "node_modules/@nlpjs/similarity": { + "version": "5.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@nlpjs/similarity/-/similarity-5.0.0-alpha.5.tgz", + "integrity": "sha512-xgEjMXnwieZTXV/cRHnsFL72s6EvFxohOU2jWhXMgtaiV6bpglnJJTzvtFl8ZeLCzqfZqY3pfGmsvtWaR3vujw==", + "license": "MIT" + }, + "node_modules/@nlpjs/slot": { + "version": "5.0.0-alpha.5", + "resolved": "https://registry.npmjs.org/@nlpjs/slot/-/slot-5.0.0-alpha.5.tgz", + "integrity": "sha512-weia63mK3NXpUlkzVYx9fmv+SiAtVNrX8KC3wOqZtJl+Krg/Feo+xdmKPDT/VDtjz+v+je/4Xj85oAfPbHXepA==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1383,6 +1483,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1428,6 +1535,24 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1767,6 +1892,119 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1860,6 +2098,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1967,6 +2215,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2142,6 +2400,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -2427,6 +2692,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2468,6 +2743,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3102,6 +3387,16 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3241,6 +3536,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -3340,6 +3646,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3671,6 +3984,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -3724,6 +4044,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -3823,6 +4157,23 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3872,6 +4223,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4106,6 +4467,101 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4122,6 +4578,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 34ea177..50fcdc9 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", + "test": "vitest run", "typecheck": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.node.json --noEmit", "prepare": "husky", "format": "prettier --write .", @@ -14,6 +15,9 @@ "preview": "vite preview" }, "dependencies": { + "@nlpjs/core": "^5.0.0-alpha.5", + "@nlpjs/lang-en-min": "^5.0.0-alpha.5", + "@nlpjs/nlp": "^5.0.0-alpha.5", "react": "^19.2.0", "react-dom": "^19.2.0", "zustand": "^5.0.9" @@ -26,23 +30,24 @@ "@vitejs/plugin-react": "^5.1.1", "baseline-browser-mapping": "^2.9.19", "eslint": "^9.39.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", "husky": "^9.0.11", "lint-staged": "^15.2.10", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.1", "prettier": "^3.3.3", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "vite": "^7.2.4", + "vitest": "^4.1.0" }, "lint-staged": { "*.{ts,tsx,js,jsx}": [ "prettier --write", "eslint --max-warnings=0 --fix" ], - "*.{json,md,css,scss}": "prettier --write" + "*.{json,css,scss}": "prettier --write" } } diff --git a/progress.md b/progress.md new file mode 100644 index 0000000..5865e6a --- /dev/null +++ b/progress.md @@ -0,0 +1,31 @@ +Original prompt: Давай временно переключимся с парсера к архитектуре приложения и придумаем способ автоматического управления загруженными сценами, чтобы не получалось так, что каждая открытая сцена остаётся в RAM. Возможно, разумно хранить несколько наиболее часто загружаемых сцен, а редко загружаемые из памяти выгружать. И вообще мониторить кол-во памяти занятое сценами, и регулировать кол-во сцен в кэше в зависимости от этого + +- Решение: вводим Scene Registry + adaptive scene cache. +- Scene cache policy: единая для runtime и editor. +- Budget: по estimated scene weight, не по browser memory API. +- UI: в нижней строке editor слева показывать `MEM x | y`, где `x` — estimated memory, `y` — число сцен в cache. +- Важно не трогать локальные артефакты пользователя: `public/scenes/bug.json`, `public/scenes/ttt.json`, `public/scenes/test_room.json`, `public/text/scenes/new_scene.json`, `tasks.md`, `.nvimlog`. +- Реализовано: SceneManager разделён на registry + cache metadata; добавлены estimated weight, eviction, cache stats и registry scan по `public/scenes`. +- Реализовано: `game.goTo()` теперь ищет сцену через `sceneRegistry`, а не только среди живых scene instances. +- Реализовано: bottom menu editor показывает `MEM x | y` слева. +- Реализовано: save/save-as синхронизирует scene registration, чтобы cache/registry не теряли сцену после смены id. +- Проверки: `npm run -s typecheck` и `npm run -s build` проходят. +- Ограничение среды: локальный `npm run dev` smoke test в этой сессии блокируется sandbox-ошибкой `vite -> esbuild spawn EPERM`, поэтому браузерный прогон не был надёжно выполнен. +- Новое: добавлен debug-profiler сцен в `SceneManager`. +- `profileCurrentSceneMemory()` снимает snapshot по текущей сцене: weight units, heap snapshot, texture estimate, bytes-per-unit. +- `profileScenes([...])` прогоняет серию сцен, по очереди загружает их и печатает `console.table(...)` с `deltaMb`, `textureMb`, `kbPerUnit`. +- Debug API проброшен в `window.__QUEST_DEBUG__`: + - `__QUEST_DEBUG__.profileCurrentSceneMemory()` + - `__QUEST_DEBUG__.profileScenes([...])` + - `__QUEST_DEBUG__.game` +- Browser smoke (MCP): `__QUEST_DEBUG__.profileCurrentSceneMemory()` и `__QUEST_DEBUG__.profileScenes(['test_room'])` отрабатывают, новых console errors нет. +- Новый этап: scene cache и image cache сведены в согласованную texture-first модель. +- Scene weight теперь должен доминирующе учитывать texture bytes, а старый graph-weight используется как малый корректирующий вклад. +- Budgets scene cache подняты в 3 раза; image cache получил отдельный budget по device class. +- В `GDD.md` в конец раздела `Техническая реализация` добавлено описание profiler и примеры вызова через `window.__QUEST_DEBUG__`. +- Проверка browser loop: skill client не запустился, потому что в окружении нет пакета 'playwright'; для smoke test использован встроенный browser MCP. + +- New task: make editor bottom menu modifier-aware (Ctrl/Alt/Shift), with dynamic labels such as D ENABLE/DISABLE based on selected object state. +- Implemented state-driven EditorBottomMenu with modifier modes; added Alt/Shift Save As path and Ctrl action set; pending browser smoke verification. + +- Verified in browser: bottom menu now switches between base / Alt / Ctrl / Shift layouts, and Alt mode updates D from Disable to Enable after toggling selected object disabled state.\n diff --git a/public/scenes/bug.json b/public/scenes/bug.json deleted file mode 100644 index d939684..0000000 --- a/public/scenes/bug.json +++ /dev/null @@ -1,678 +0,0 @@ -{ - "id": "bug", - "name": "Test Room", - "filename": "bug", - "walkbox": [ - { - "type": "Walkbox", - "name": "Walk_997", - "locked": false, - "disabled": false, - "layer": 0, - "groupID": null, - "customName": "", - "interactions": {}, - "components": [], - "visible": true, - "poly": [ - { - "x": -79, - "y": 346 - }, - { - "x": -81, - "y": 272 - }, - { - "x": 153, - "y": 272 - }, - { - "x": 153, - "y": 346 - } - ], - "mode": "Add" - }, - { - "type": "Walkbox", - "name": "Walk_176", - "locked": false, - "disabled": false, - "layer": 0, - "groupID": null, - "customName": "", - "interactions": {}, - "components": [], - "visible": true, - "poly": [ - { - "x": 82, - "y": 192 - }, - { - "x": 407, - "y": 195 - }, - { - "x": 408, - "y": 203 - }, - { - "x": 470, - "y": 207 - }, - { - "x": 596, - "y": 220 - }, - { - "x": 624, - "y": 211 - }, - { - "x": 680, - "y": 211 - }, - { - "x": 680, - "y": 297 - }, - { - "x": -234, - "y": 291 - }, - { - "x": -210, - "y": 247 - }, - { - "x": -111, - "y": 249 - }, - { - "x": 77, - "y": 194 - } - ], - "mode": "Add" - } - ], - "triggerboxes": [ - { - "type": "Triggerbox", - "name": "Trig_sub_D", - "locked": false, - "disabled": false, - "layer": 0, - "groupID": null, - "customName": "", - "interactions": {}, - "components": [ - { - "type": "Subscene", - "targetGroupId": "#D", - "name": "" - } - ], - "visible": true, - "poly": [ - { - "x": 27, - "y": 210 - }, - { - "x": 28, - "y": 104 - }, - { - "x": 84, - "y": 94 - }, - { - "x": 83, - "y": 136 - }, - { - "x": 54, - "y": 145 - }, - { - "x": 50, - "y": 160 - }, - { - "x": 76, - "y": 170 - }, - { - "x": 79, - "y": 194 - } - ], - "script": "" - }, - { - "type": "Triggerbox", - "name": "sub_sw_d1", - "locked": false, - "disabled": false, - "layer": 0, - "groupID": "#D ", - "customName": "", - "interactions": {}, - "components": [ - { - "type": "Switch", - "groupId1": "nil", - "groupId2": "#D1", - "state": 1, - "idKey": "", - "sound1": "drawer_open.wav", - "sound2": "drawer_close.wav" - } - ], - "visible": true, - "poly": [ - { - "x": -156.29411764705887, - "y": -171.3529411764706 - }, - { - "x": 425, - "y": -171 - }, - { - "x": 414, - "y": -95 - }, - { - "x": -143, - "y": -95 - } - ], - "script": "" - }, - { - "type": "Triggerbox", - "name": "Trig_834", - "locked": false, - "disabled": false, - "layer": 0, - "groupID": null, - "customName": "", - "interactions": {}, - "components": [], - "visible": true, - "poly": [], - "script": "" - }, - { - "type": "Triggerbox", - "name": "sub_sw_d2", - "locked": false, - "disabled": false, - "layer": 0, - "groupID": "#D ", - "customName": "", - "interactions": {}, - "components": [ - { - "type": "Switch", - "groupId1": "nil", - "groupId2": "#D2", - "state": 1, - "idKey": "", - "sound1": "drawer_open.wav", - "sound2": "drawer_close.wav" - } - ], - "visible": true, - "poly": [ - { - "x": -156.29411764705878, - "y": -87.03921568627447 - }, - { - "x": 424.9999999999999, - "y": -86.68627450980387 - }, - { - "x": 413.9999999999999, - "y": -10.68627450980393 - }, - { - "x": -142.99999999999994, - "y": -10.68627450980393 - } - ], - "script": "" - } - ], - "scaling": { - "enabled": true, - "min": 0.91, - "max": 1, - "horizon": 193, - "front": 269 - }, - "entities": [ - { - "type": "Entity", - "name": "CityView", - "x": 54, - "y": 225, - "width": 821.6, - "height": 551.2, - "baseWidth": 1264, - "baseHeight": 848, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": "window-view.json", - "color": "#00ff00", - "scale": 0.65, - "modelScale": 0.65, - "layer": -2, - "parallax": 0.2, - "ignoreScaling": true, - "animationSpeed": 150, - "locked": true, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - }, - { - "type": "Entity", - "name": "room", - "x": 199, - "y": 297, - "width": 884.8, - "height": 593.5999999999999, - "baseWidth": 1264, - "baseHeight": 848, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": "room2", - "color": "#888888", - "scale": 0.7, - "modelScale": 0.7, - "layer": -1, - "parallax": 1, - "ignoreScaling": true, - "animationSpeed": 150, - "locked": true, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - }, - { - "type": "Entity", - "name": "Chair", - "x": 100, - "y": 249, - "width": 116.89999999999999, - "height": 198.79999999999998, - "baseWidth": 167, - "baseHeight": 284, - "colliderWidth": 78, - "colliderHeight": 18, - "spriteName": "chair.json", - "color": "#00ff00", - "scale": 0.7, - "modelScale": 0.7, - "layer": 0, - "parallax": 1, - "ignoreScaling": true, - "animationSpeed": 150, - "locked": true, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - }, - { - "type": "Actor", - "name": "Hero_1", - "x": 190.75771978135336, - "y": 255.02787403588582, - "width": 69.86457651889262, - "height": 285.28035411881154, - "baseWidth": 96, - "baseHeight": 392, - "colliderWidth": 88, - "colliderHeight": 4, - "spriteName": "miles_ds-idle-right.json", - "color": "#00ffff", - "scale": 0.7277560054051315, - "modelScale": 0.74, - "layer": 0, - "parallax": 1, - "ignoreScaling": false, - "animationSpeed": 30, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, - "isPlayer": true, - "speed": 0.24, - "direction": "left", - "animSets": { - "idle": { - "id": "idle", - "up": "miles_ds-idle-up.json", - "down": "miles_ds-idle-down.json", - "left": null, - "right": "miles_ds-idle-right.json" - }, - "walk": { - "id": "walk", - "up": "miles_ds-walk-up.json", - "down": "miles_ds-walk-down.json", - "left": null, - "right": "miles_ds-walk-right.json" - } - } - }, - { - "type": "Entity", - "name": "Black_1", - "x": -328, - "y": 307, - "width": 171.2340644206598, - "height": 637.1050459736764, - "baseWidth": 155.6673312915089, - "baseHeight": 579.1864054306149, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": null, - "color": "#000000", - "scale": 1.1, - "modelScale": 1.1, - "layer": 0, - "parallax": 1, - "ignoreScaling": false, - "animationSpeed": 150, - "locked": true, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - }, - { - "type": "Entity", - "name": "sofa", - "x": 195, - "y": 293, - "width": 882.6999999999999, - "height": 79.1, - "baseWidth": 1261, - "baseHeight": 113, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": "sofa.json", - "color": "#00ff00", - "scale": 0.7, - "modelScale": 0.7, - "layer": 1, - "parallax": 1, - "ignoreScaling": false, - "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - }, - { - "type": "Entity", - "name": "sub_D_main", - "x": 135, - "y": 310, - "width": 614.4, - "height": 484.79999999999995, - "baseWidth": 1024, - "baseHeight": 808, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": "sub_drawers_main", - "color": "#00ff00", - "scale": 0.6, - "modelScale": 0.6, - "layer": 3, - "parallax": 1, - "ignoreScaling": true, - "animationSpeed": 150, - "locked": true, - "disabled": false, - "customName": "", - "groupID": "#D", - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - }, - { - "type": "Entity", - "name": "sub_D2_body", - "x": 134, - "y": 311, - "width": 716.8, - "height": 475.29999999999995, - "baseWidth": 1024, - "baseHeight": 679, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": "sub_drawers_d2.json", - "color": "#00ff00", - "scale": 0.7, - "modelScale": 0.7, - "layer": 4, - "parallax": 1, - "ignoreScaling": true, - "animationSpeed": 150, - "locked": false, - "disabled": true, - "customName": "", - "groupID": "#D2", - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - }, - { - "type": "Entity", - "name": "sub_D1_body", - "x": 136, - "y": -4, - "width": 614.4, - "height": 177.6, - "baseWidth": 1024, - "baseHeight": 296, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": "sub_drawers_d1_body.json", - "color": "#00ff00", - "scale": 0.6, - "modelScale": 0.6, - "layer": 5, - "parallax": 1, - "ignoreScaling": true, - "animationSpeed": 150, - "locked": false, - "disabled": true, - "customName": "", - "groupID": "#D1", - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - }, - { - "type": "Entity", - "name": "miles_id", - "x": 93, - "y": 0, - "width": 150.4436263347707, - "height": 93.2343137254902, - "baseWidth": 165.32266630194582, - "baseHeight": 102.45528980823099, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": "sub_drawers_d1_id.json", - "color": "#AAAAAA", - "scale": 0.91, - "modelScale": 1, - "layer": 5, - "parallax": 1, - "ignoreScaling": false, - "animationSpeed": 150, - "locked": false, - "disabled": true, - "customName": "your ID card", - "groupID": "#D1", - "components": [ - { - "type": "Item", - "ignoreDistance": true - } - ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - }, - { - "type": "Entity", - "name": "sub_D1_stuff", - "x": 320, - "y": 184, - "width": 979, - "height": 412, - "baseWidth": 979, - "baseHeight": 412, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": "sub_drawers_d1_items.json", - "color": "#00ff00", - "scale": 1, - "modelScale": 1, - "layer": 5, - "parallax": 1, - "ignoreScaling": true, - "animationSpeed": 150, - "locked": false, - "disabled": true, - "customName": "", - "groupID": "#D1", - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - }, - { - "type": "Entity", - "name": "sub_D_main_top", - "x": 135, - "y": -176, - "width": 614.4, - "height": 129.6, - "baseWidth": 1024, - "baseHeight": 216, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": "sub_drawers_top.json", - "color": "#00ff00", - "scale": 0.6, - "modelScale": 0.6, - "layer": 6, - "parallax": 1, - "ignoreScaling": true, - "animationSpeed": 150, - "locked": true, - "disabled": false, - "customName": "", - "groupID": "#D", - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - }, - { - "type": "Entity", - "name": "sub_D1_fasade", - "x": 135, - "y": 66, - "width": 614.4, - "height": 105.6, - "baseWidth": 1024, - "baseHeight": 176, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": "sub_drawers_d1_facade.json", - "color": "#00ff00", - "scale": 0.6, - "modelScale": 0.6, - "layer": 6, - "parallax": 1, - "ignoreScaling": true, - "animationSpeed": 150, - "locked": false, - "disabled": true, - "customName": "", - "groupID": "#D1", - "components": [ - { - "type": "Subtrigger", - "target": "sub_sw_d1" - } - ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0 - } - ], - "camera": { - "x": 297, - "y": 29, - "zoom": 0.51 - }, - "autoCenter": false, - "cameraSpeed": 1.5, - "camDeadzoneX": 200, - "camDeadzoneY": -21, - "camMinX": 143, - "camMaxY": 45 -} diff --git a/public/scenes/home/room.json b/public/scenes/home/room.json index 7585f5c..086ae22 100644 --- a/public/scenes/home/room.json +++ b/public/scenes/home/room.json @@ -1,6 +1,8 @@ { "id": "home\\room", "name": "Test Room", + "description": "You are in Test Room.", + "textRedirects": {}, "filename": "home/room", "walkbox": [ { @@ -10,6 +12,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, @@ -41,6 +44,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, @@ -106,6 +110,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -159,6 +164,7 @@ "disabled": true, "groupID": "#D ", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -171,7 +177,7 @@ "sound2": "drawer_close.wav" } ], - "layer": 0, + "layer": 1, "visible": true, "poly": [ { @@ -200,6 +206,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, @@ -214,6 +221,7 @@ "disabled": true, "groupID": "#D ", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -226,7 +234,7 @@ "sound2": "drawer_close.wav" } ], - "layer": 0, + "layer": 1, "visible": true, "poly": [ { @@ -264,6 +272,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": -2, @@ -294,6 +303,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": -1, @@ -324,6 +334,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, @@ -354,23 +365,24 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, "visible": true, - "x": 249.76314957424822, - "y": 295.45481146309464, - "width": 119.88, - "height": 289.34, - "baseWidth": 162, - "baseHeight": 391, + "x": 241.58710837405346, + "y": 236.58147331007746, + "width": 119.33276583023533, + "height": 278.4431202705491, + "baseWidth": 168, + "baseHeight": 392, "colliderWidth": 88, "colliderHeight": 4, - "spriteName": "miles_ds-idle-down.json", + "spriteName": "miles_ds-idle-up.json", "color": "#00ffff", - "scale": 0.74, + "scale": 0.7103140823228293, "modelScale": 0.74, - "parallax": 1.0715390924595392, + "parallax": 1.033097301991213, "ignoreScaling": false, "animationSpeed": 30, "opacity": 1, @@ -378,7 +390,7 @@ "blur": 0, "isPlayer": true, "speed": 0.24, - "direction": "down", + "direction": "up", "animSets": { "idle": { "id": "idle", @@ -403,6 +415,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, @@ -433,9 +446,10 @@ "disabled": true, "groupID": "#D", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], - "layer": 3, + "layer": 0, "visible": true, "x": 135, "y": 310, @@ -463,8 +477,14 @@ "disabled": true, "groupID": "#D2", "customName": "", + "textRedirects": {}, "interactions": {}, - "components": [], + "components": [ + { + "type": "Subtrigger", + "target": "sub_sw_d2" + } + ], "layer": 4, "visible": true, "x": 134, @@ -493,9 +513,10 @@ "disabled": true, "groupID": "#D1", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], - "layer": 5, + "layer": 4, "visible": true, "x": 136, "y": -4, @@ -523,6 +544,7 @@ "disabled": true, "groupID": "#D1", "customName": "your ID card", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -558,6 +580,7 @@ "disabled": true, "groupID": "#D1", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 5, @@ -588,6 +611,7 @@ "disabled": true, "groupID": "#D", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 6, @@ -618,6 +642,7 @@ "disabled": true, "groupID": "#D1", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -653,6 +678,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -706,12 +732,13 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, "visible": true, - "x": 222.90433731748038, - "y": 306.92845155295413, + "x": 222.90433731748033, + "y": 306.928451552954, "width": 1008.8000000000001, "height": 90.39999999999999, "baseWidth": 1261, @@ -722,7 +749,7 @@ "color": "#36d87fff", "scale": 0.8, "modelScale": 0.8, - "parallax": 1.079038203629382, + "parallax": 1.0790382036293817, "ignoreScaling": false, "animationSpeed": 150, "opacity": 1, @@ -732,6 +759,37 @@ "speed": 0.1, "direction": "down", "animSets": {} + }, + { + "name": "boombox", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -152, + "y": -11, + "width": 112, + "height": 47, + "baseWidth": 123.07692307692307, + "baseHeight": 51.64835164835165, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#AAAAAA", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 0, + "blendMode": "source-over", + "blur": 0 } ], "camera": { diff --git a/public/scenes/home/room_backup.json b/public/scenes/home/room_backup.json index 7b52df5..a37a3dc 100644 --- a/public/scenes/home/room_backup.json +++ b/public/scenes/home/room_backup.json @@ -1,6 +1,8 @@ { "id": "home\\room_backup", "name": "Test Room", + "description": "You are in Test Room.", + "textRedirects": {}, "filename": "home/room_backup", "walkbox": [ { @@ -10,6 +12,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, @@ -41,6 +44,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, @@ -106,6 +110,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -159,6 +164,7 @@ "disabled": true, "groupID": "#D ", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -171,7 +177,7 @@ "sound2": "drawer_close.wav" } ], - "layer": 0, + "layer": 1, "visible": true, "poly": [ { @@ -200,6 +206,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, @@ -214,6 +221,7 @@ "disabled": true, "groupID": "#D ", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -226,7 +234,7 @@ "sound2": "drawer_close.wav" } ], - "layer": 0, + "layer": 1, "visible": true, "poly": [ { @@ -264,6 +272,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": -2, @@ -294,6 +303,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": -1, @@ -324,6 +334,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, @@ -354,12 +365,13 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, "visible": true, "x": 249.76314957424822, - "y": 295.45481146309464, + "y": 295.45481146309453, "width": 119.88, "height": 289.34, "baseWidth": 162, @@ -403,6 +415,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, @@ -433,9 +446,10 @@ "disabled": true, "groupID": "#D", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], - "layer": 3, + "layer": 0, "visible": true, "x": 135, "y": 310, @@ -463,8 +477,14 @@ "disabled": true, "groupID": "#D2", "customName": "", + "textRedirects": {}, "interactions": {}, - "components": [], + "components": [ + { + "type": "Subtrigger", + "target": "sub_sw_d2" + } + ], "layer": 4, "visible": true, "x": 134, @@ -493,9 +513,10 @@ "disabled": true, "groupID": "#D1", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], - "layer": 5, + "layer": 4, "visible": true, "x": 136, "y": -4, @@ -523,6 +544,7 @@ "disabled": true, "groupID": "#D1", "customName": "your ID card", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -558,6 +580,7 @@ "disabled": true, "groupID": "#D1", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 5, @@ -588,6 +611,7 @@ "disabled": true, "groupID": "#D", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 6, @@ -618,6 +642,7 @@ "disabled": true, "groupID": "#D1", "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -653,6 +678,7 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { @@ -706,12 +732,13 @@ "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, "visible": true, - "x": 222.90433731748038, - "y": 306.92845155295413, + "x": 222.90433731748033, + "y": 306.92845155295396, "width": 1008.8000000000001, "height": 90.39999999999999, "baseWidth": 1261, @@ -722,7 +749,7 @@ "color": "#36d87fff", "scale": 0.8, "modelScale": 0.8, - "parallax": 1.079038203629382, + "parallax": 1.0790382036293817, "ignoreScaling": false, "animationSpeed": 150, "opacity": 1, diff --git a/public/scenes/logo.json b/public/scenes/logo.json new file mode 100644 index 0000000..f6efb28 --- /dev/null +++ b/public/scenes/logo.json @@ -0,0 +1,513 @@ +{ + "id": "logo", + "name": "New Scene", + "description": "You are in New Scene.", + "textRedirects": {}, + "filename": "logo", + "walkbox": [], + "triggerboxes": [], + "scaling": { + "enabled": true, + "min": 0.4, + "max": 1, + "horizon": 78, + "front": 421 + }, + "entities": [ + { + "name": "Quad_201", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": "#g,#a", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "3d-parallax" + } + ], + "layer": -1, + "visible": true, + "x": 334.7566652973353, + "y": 147.45349435034427, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -1449, + "y": 432, + "p": 0.2, + "binding": { + "targetName": "Quad_204", + "type": "vertex", + "index": 3 + } + }, + { + "x": 1528, + "y": 428, + "p": 0.2, + "binding": { + "targetName": "Quad_204", + "type": "vertex", + "index": 2 + } + }, + { + "x": 3915.4050968588786, + "y": 457.44748933663504, + "p": 1 + }, + { + "x": -3349.5949031411214, + "y": 457.44748933663504, + "p": 1 + } + ], + "color": "#000219", + "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", + "isGrid": true, + "gridLinesX": 29, + "gridLinesY": 7, + "lineWidth": 7.4, + "gridColor": "#136382", + "filled": true, + "blur": 0 + }, + { + "name": "Quad_1", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": "#a", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -1, + "visible": true, + "x": 363.1753332849686, + "y": -1342.0347499102, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -1387, + "y": -476, + "p": 0.2, + "binding": { + "targetName": "Quad", + "type": "vertex", + "index": 3 + } + }, + { + "x": 1652, + "y": -488, + "p": 0.2, + "binding": { + "targetName": "Quad", + "type": "vertex", + "index": 2 + } + }, + { + "x": 3933.8237648465147, + "y": -1042.0407549239112, + "p": 1 + }, + { + "x": -3331.1762351534853, + "y": -1042.0407549239112, + "p": 1 + } + ], + "color": "#000219", + "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", + "isGrid": true, + "gridLinesX": 29, + "gridLinesY": 7, + "lineWidth": 7.4, + "gridColor": "#136382", + "filled": true, + "blur": 0 + }, + { + "name": "miles_ds", + "type": "Actor", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Shadow", + "shadowQuadId": "shadow", + "offsetX": -80, + "offsetY": -0.79232867459731, + "triggerId": "#g" + } + ], + "layer": 0, + "visible": true, + "x": -11.507373210367716, + "y": 437.34328111928414, + "width": 66.7529345238604, + "height": 272.57448263909663, + "baseWidth": 96, + "baseHeight": 392, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "miles_ds-idle-right.json", + "color": "#e27fa5", + "scale": 0.6953430679568792, + "modelScale": 1.03, + "parallax": 0.4538289363137056, + "ignoreScaling": false, + "animationSpeed": 30, + "opacity": 1, + "blendMode": "source-over", + "blur": 0, + "isPlayer": true, + "speed": 0.35, + "direction": "left", + "animSets": { + "idle": { + "id": "idle", + "up": "miles_ds-idle-up.json", + "down": "miles_ds-idle-down.json", + "left": null, + "right": "miles_ds-idle-right.json" + }, + "walk": { + "id": "walk", + "up": "miles_ds-walk-up.json", + "down": "miles_ds-walk-down.json", + "left": null, + "right": "miles_ds-walk-right.json" + } + } + }, + { + "name": "shadow", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -67.17651736805739, + "y": 437.33802751880216, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -67.17651736805739, + "y": 437.33802751880216, + "p": 0.4523541075273739 + }, + { + "x": 5.443172833624047, + "y": 437.2945106625292, + "p": 0.452503989220326 + }, + { + "x": -5.703139435760795, + "y": 438.52482078346884, + "p": 0.48592782666534984 + }, + { + "x": -73.48973431343629, + "y": 438.4581791995139, + "p": 0.48411737462461735 + } + ], + "color": "#000000", + "sortMode": "ignore", + "opacity": 1, + "blendMode": "multiply", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 4 + }, + { + "name": "logo_1", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -71, + "y": 237, + "width": 439.65753424657544, + "height": 438.3150684931505, + "baseWidth": 392.5513698630138, + "baseHeight": 391.35273972602727, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "scanline_logo", + "color": "#AAAAAA", + "scale": 1.1199999999999999, + "modelScale": 2.8, + "parallax": 0.2, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 0.6, + "blendMode": "lighter", + "blur": 0 + }, + { + "name": "logo_2", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -64, + "y": 346, + "width": 655.1999999999999, + "height": 668.64, + "baseWidth": 585, + "baseHeight": 597, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "scanline_logo", + "color": "#00ca4c", + "scale": 1.1199999999999999, + "modelScale": 2.8, + "parallax": 0.2, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 0.35, + "blendMode": "screen", + "blur": 32 + }, + { + "name": "Quad_204", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -2, + "visible": true, + "x": -306.3073195313695, + "y": 194.6463347631958, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -1132, + "y": 408, + "p": 0.2 + }, + { + "x": 1152, + "y": 388, + "p": 0.2 + }, + { + "x": 1528, + "y": 428, + "p": 0.2, + "binding": { + "targetName": "Quad_201", + "type": "vertex", + "index": 1 + } + }, + { + "x": -1449, + "y": 432, + "p": 0.2, + "binding": { + "targetName": "Quad_201", + "type": "vertex", + "index": 0 + } + } + ], + "color": "#ee00ff", + "sortMode": "ignore", + "opacity": 1, + "blendMode": "difference", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 8 + }, + { + "name": "Quad", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -2, + "visible": true, + "x": 31.056518423854072, + "y": -469.3074715804131, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -914, + "y": -424, + "p": 0.2 + }, + { + "x": 1249, + "y": -424, + "p": 0.2 + }, + { + "x": 1652, + "y": -488, + "p": 0.2, + "binding": { + "targetName": "Quad_1", + "type": "vertex", + "index": 1 + } + }, + { + "x": -1387, + "y": -476, + "p": 0.2, + "binding": { + "targetName": "Quad_1", + "type": "vertex", + "index": 0 + } + } + ], + "color": "#ee00ff", + "sortMode": "ignore", + "opacity": 1, + "blendMode": "difference", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 8 + }, + { + "name": "Quad_646", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -1359.1857676091972, + "y": -530.9597165247759, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": -1387, + "y": -476, + "p": 0.2, + "binding": { + "targetName": "Quad_1", + "type": "vertex", + "index": 0 + } + }, + { + "x": 1550.7, + "y": -487.6, + "p": 0.2, + "binding": { + "targetName": "Quad_1", + "type": "grid", + "gridU": 0.9666666666666667, + "gridV": 0 + } + }, + { + "x": 1528, + "y": 428, + "p": 0.2, + "binding": { + "targetName": "Quad_201", + "type": "vertex", + "index": 1 + } + }, + { + "x": -1449, + "y": 432, + "p": 0.2, + "binding": { + "targetName": "Quad_201", + "type": "vertex", + "index": 0 + } + } + ], + "color": "#020440", + "sortMode": "ignore", + "opacity": 0.55, + "blendMode": "source-over", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 20 + } + ], + "camera": { + "x": 165.57094204403754, + "y": -263.4392820164094, + "zoom": 0.1500946352969992 + }, + "autoCenter": true, + "cameraSpeed": 5, + "camDeadzoneX": 50, + "camDeadzoneY": 30, + "camMaxY": -370 +} diff --git a/public/scenes/quad4.json b/public/scenes/quad4.json index d3b7062..93922bb 100644 --- a/public/scenes/quad4.json +++ b/public/scenes/quad4.json @@ -1,34 +1,40 @@ { "id": "quad4", "name": "Test Room", + "description": "You are in Test Room.", + "textRedirects": {}, "filename": "quad4", "walkbox": [], "triggerboxes": [ { - "type": "Triggerbox", "name": "Trig_137", + "type": "Triggerbox", "locked": false, "disabled": false, - "layer": 0, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], + "layer": 0, "visible": true, + "spatial": {}, "poly": [], "script": "" }, { - "type": "Triggerbox", "name": "Trig_29", + "type": "Triggerbox", "locked": false, "disabled": false, - "layer": 0, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], + "layer": 0, "visible": true, + "spatial": {}, "poly": [], "script": "" } @@ -42,8 +48,18 @@ }, "entities": [ { - "type": "Entity", "name": "bg", + "type": "Entity", + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -10, + "visible": true, + "spatial": {}, "x": 21, "y": 307, "width": 796.32, @@ -56,38 +72,30 @@ "color": "#AAAAAA", "scale": 0.63, "modelScale": 0.7, - "layer": -10, "parallax": 0.1, "ignoreScaling": false, "animationSpeed": 150, - "locked": true, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Quad", "name": "sunbeam1", - "x": 19.66326736878159, - "y": 8.75, - "color": "#c95618", - "layer": -1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 0.1, - "blendMode": "lighter", - "blur": 0, + "components": [], + "layer": -1, + "visible": true, + "spatial": {}, + "x": 19.66326736878159, + "y": 8.75, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 2.34, @@ -110,17 +118,31 @@ "p": 0.64 } ], + "color": "#c95618", "sortMode": "ignore", + "opacity": 0.1, + "blendMode": "lighter", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Entity", "name": "Static_3", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -1, + "visible": true, + "spatial": {}, "x": 79, "y": 62, "width": 164.69142857142856, @@ -133,23 +155,26 @@ "color": "#AAAAAA", "scale": 0.14076190476190475, "modelScale": 0.2, - "layer": -1, "parallax": 0.65, "ignoreScaling": false, "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Entity", "name": "Static_4", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -1, + "visible": true, + "spatial": {}, "x": -75, "y": 62, "width": 164.69142857142856, @@ -162,38 +187,30 @@ "color": "#AAAAAA", "scale": 0.14076190476190475, "modelScale": 0.2, - "layer": -1, "parallax": 0.65, "ignoreScaling": false, "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Quad", "name": "glass", - "x": 50.35518543794477, - "y": 56.98544526105304, - "color": "#827c3a", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 0.9, - "blendMode": "overlay", - "blur": 0, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": 50.35518543794477, + "y": 56.98544526105304, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -161.15, @@ -216,32 +233,35 @@ "p": 0.65 } ], + "color": "#827c3a", "sortMode": "ignore", + "opacity": 0.9, + "blendMode": "overlay", "isGrid": false, "gridLinesX": 2, "gridLinesY": 2, "lineWidth": 1.6, "gridColor": "#1d1616", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_72", - "x": -92.63369640031912, - "y": -65.95591650446437, - "color": "#947171", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": -92.63369640031912, + "y": -65.95591650446437, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -199.36280668861116, @@ -264,32 +284,35 @@ "p": 1 } ], + "color": "#947171", "sortMode": "v2", + "opacity": 1, + "blendMode": "source-over", "isGrid": true, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 0.5, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "wall_2", - "x": 1.5, - "y": -5.5, - "color": "#855d38", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": 1.5, + "y": -5.5, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 201.75257762598838, @@ -312,32 +335,35 @@ "p": 1 } ], + "color": "#855d38", "sortMode": "v1", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "sunbeam3", - "x": 52.16326736878159, - "y": 140.25, - "color": "#c95618", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 0.1, - "blendMode": "lighter", - "blur": 0, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": 52.16326736878159, + "y": 140.25, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 98.68, @@ -360,17 +386,31 @@ "p": 1 } ], + "color": "#c95618", "sortMode": "v0", + "opacity": 0.1, + "blendMode": "lighter", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Entity", "name": "Static_2", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, "x": 58, "y": 54, "width": 55.44680072970669, @@ -383,38 +423,30 @@ "color": "#4a1215", "scale": 0.7, "modelScale": 1, - "layer": 0, "parallax": 0.7, "ignoreScaling": false, "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Quad", "name": "tabletop1_2", - "x": -5.0976058382019325, - "y": 11.589932781834104, - "color": "#1c495e", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": -5.0976058382019325, + "y": 11.589932781834104, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -1.8424309747189085, @@ -437,27 +469,27 @@ "p": 0.75 } ], + "color": "#1c495e", "sortMode": "v3", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_1", - "x": -391.5, - "y": 119.5, - "color": "#283743", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Backface", @@ -469,10 +501,13 @@ "cullingType": "render" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": 0, + "visible": false, + "spatial": {}, + "x": -391.5, + "y": 119.5, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -449, @@ -495,27 +530,27 @@ "p": 1 } ], + "color": "#283743", "sortMode": "v1", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "floor", - "x": 0.5995731785992522, - "y": 18.186447655482453, - "color": "#5b3a1a", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "3d-parallax" @@ -525,10 +560,13 @@ "mode": "Invert" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": 0, + "visible": true, + "spatial": {}, + "x": 0.5995731785992522, + "y": 18.186447655482453, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -338, @@ -551,32 +589,35 @@ "p": 1 } ], + "color": "#5b3a1a", "sortMode": "v0", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 9, "gridLinesY": 9, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "shadow_3", - "x": 45.71298290802447, - "y": 163.46124773339642, - "color": "#535246", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "multiply", - "blur": 5, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": 45.71298290802447, + "y": 163.46124773339642, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 48.23546261048095, @@ -599,32 +640,35 @@ "p": 0.77 } ], + "color": "#535246", "sortMode": "v1", + "opacity": 1, + "blendMode": "multiply", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 5 }, { - "type": "Quad", "name": "lightspot1_1", - "x": -240.36825872762117, - "y": 130.71254989539653, - "color": "#c95618", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": "#lightSpots", - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 0.2, - "blendMode": "lighter", - "blur": 2, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": -240.36825872762117, + "y": 130.71254989539653, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -315, @@ -647,32 +691,35 @@ "p": 1 } ], + "color": "#c95618", "sortMode": "v1", + "opacity": 0.2, + "blendMode": "lighter", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 2 }, { - "type": "Quad", "name": "lightspot2", - "x": 55.331970640694806, - "y": 141.72490207880932, - "color": "#c95618", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": "#lightSpots", - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 0.2, - "blendMode": "lighter", - "blur": 2, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": 55.331970640694806, + "y": 141.72490207880932, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 6, @@ -695,32 +742,35 @@ "p": 1 } ], + "color": "#c95618", "sortMode": "v1", + "opacity": 0.2, + "blendMode": "lighter", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 2 }, { - "type": "Quad", "name": "lightspot1", - "x": -94.35007280706921, - "y": 132.21254989539653, - "color": "#c95618", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": "#lightSpots", - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 0.2, - "blendMode": "lighter", - "blur": 2, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": -94.35007280706921, + "y": 132.21254989539653, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -139.12445128696197, @@ -743,32 +793,35 @@ "p": 1 } ], + "color": "#c95618", "sortMode": "v0", + "opacity": 0.2, + "blendMode": "lighter", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 2 }, { - "type": "Quad", "name": "Quad_303", - "x": 181.47444229637748, - "y": 65.98012041511447, - "color": "#1d2272", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": 181.47444229637748, + "y": 65.98012041511447, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 155.662541981541, @@ -791,17 +844,31 @@ "p": 0.685 } ], + "color": "#1d2272", "sortMode": "v3", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Entity", "name": "Static_231", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, "x": 15, "y": 78, "width": 28.70935763453951, @@ -814,38 +881,30 @@ "color": "#381a1a", "scale": 0.07508698412698413, "modelScale": 0.1, - "layer": 0, "parallax": 0.748, "ignoreScaling": false, "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Quad", "name": "shadow_2", - "x": -135.3242980143822, - "y": 162.32192586595625, - "color": "#535246", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "multiply", - "blur": 5, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": -135.3242980143822, + "y": 162.32192586595625, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -132.80181831192573, @@ -868,32 +927,35 @@ "p": 0.7 } ], + "color": "#535246", "sortMode": "v2", + "opacity": 1, + "blendMode": "multiply", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 5 }, { - "type": "Quad", "name": "shadow_1", - "x": 29.72577036587564, - "y": 152.95900020457972, - "color": "#535246", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "multiply", - "blur": 5, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": 29.72577036587564, + "y": 152.95900020457972, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 32.248250068332126, @@ -916,27 +978,27 @@ "p": 0.77 } ], + "color": "#535246", "sortMode": "v3", + "opacity": 1, + "blendMode": "multiply", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 5 }, { - "type": "Quad", "name": "Quad_2", - "x": -189.21706697024933, - "y": 58.32628202579792, - "color": "#ba814b", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Backface", @@ -948,10 +1010,13 @@ "cullingType": "render" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": 0, + "visible": true, + "spatial": {}, + "x": -189.21706697024933, + "y": 58.32628202579792, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -164.72899239988428, @@ -974,32 +1039,35 @@ "p": 0.75 } ], + "color": "#ba814b", "sortMode": "v3", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_891", - "x": -207, - "y": 76.75, - "color": "#474747", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": -207, + "y": 76.75, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -182.04473703579987, @@ -1022,27 +1090,27 @@ "p": 0.75 } ], + "color": "#474747", "sortMode": "v2", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_204", - "x": -206.21706697024933, - "y": 61.32628202579792, - "color": "#636363", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Backface", @@ -1067,10 +1135,13 @@ "mode": "Subtract" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": 0, + "visible": false, + "spatial": {}, + "x": -206.21706697024933, + "y": 61.32628202579792, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -181.59999999999997, @@ -1093,27 +1164,27 @@ "p": 0.75 } ], + "color": "#636363", "sortMode": "v3", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_4", - "x": -207.71706697024933, - "y": 79.32628202579792, - "color": "#888888", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Backface", @@ -1125,10 +1196,13 @@ "cullingType": "render" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": 0, + "visible": true, + "spatial": {}, + "x": -207.71706697024933, + "y": 79.32628202579792, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -212, @@ -1151,32 +1225,35 @@ "p": 1 } ], + "color": "#888888", "sortMode": "v2", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "lightspot1_2", - "x": -301.32662389675636, - "y": 144.63442346911063, - "color": "#c95618", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 0.2, - "blendMode": "lighter", - "blur": 2, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": -301.32662389675636, + "y": 144.63442346911063, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -449, @@ -1199,75 +1276,78 @@ "p": 1 } ], + "color": "#c95618", "sortMode": "v0", + "opacity": 0.2, + "blendMode": "lighter", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 2 }, { - "type": "Quad", "name": "shadow_hero", - "x": -96.83532994201772, - "y": 90.04937274515352, - "color": "#535246", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 0.5, - "blendMode": "multiply", - "blur": 5, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, + "x": -58.38940219735827, + "y": 90.05888362114656, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { - "x": -96.83532994201772, - "y": 90.04937274515352, - "p": 0.8010489022254632 + "x": -58.38940219735827, + "y": 90.05888362114656, + "p": 0.8018414752248832 }, { - "x": -71.65959761167679, - "y": 90.15046873444555, - "p": 0.8021390174531002 + "x": -33.0497420477009, + "y": 90.17377655121713, + "p": 0.8030099773830363 }, { - "x": -130.56026019579616, - "y": 130.143715311925, + "x": -101.38069836073447, + "y": 132.64466865932502, "p": 0.9969905705682787 }, { - "x": -134.2968063271847, - "y": 118.33601955695332, - "p": 0.9539817613221357 + "x": -103.81194334935691, + "y": 120.404686934742, + "p": 0.9658771764546956 } ], + "color": "#535246", "sortMode": "v2", + "opacity": 0.5, + "blendMode": "multiply", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 5 }, { - "type": "Quad", "name": "Quad_8", - "x": -189.21706697024933, - "y": 58.32628202579792, - "color": "#ba814b", - "layer": 1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Backface", @@ -1279,10 +1359,13 @@ "cullingType": "render" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": 1, + "visible": true, + "spatial": {}, + "x": -189.21706697024933, + "y": 58.32628202579792, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -183.61963904840187, @@ -1305,27 +1388,27 @@ "p": 0.85 } ], + "color": "#ba814b", "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_9", - "x": -214.2170669702493, - "y": 63.88183758135348, - "color": "#646464", - "layer": 1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Backface", @@ -1337,10 +1420,13 @@ "cullingType": "render" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": 1, + "visible": false, + "spatial": {}, + "x": -214.2170669702493, + "y": 63.88183758135348, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -208.61963904840184, @@ -1363,32 +1449,35 @@ "p": 0.85 } ], + "color": "#646464", "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_225", - "x": -188.1239844818758, - "y": 46.14256198347107, - "color": "#292828", - "layer": 1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 1, + "visible": true, + "spatial": {}, + "x": -188.1239844818758, + "y": 46.14256198347107, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -211.45932857364383, @@ -1411,32 +1500,35 @@ "p": 1 } ], + "color": "#292828", "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_6", - "x": 179.97444229637748, - "y": 67.48012041511447, - "color": "#191d5d", - "layer": 1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 1, + "visible": true, + "spatial": {}, + "x": 179.97444229637748, + "y": 67.48012041511447, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 154.162541981541, @@ -1459,32 +1551,35 @@ "p": 0.67 } ], + "color": "#191d5d", "sortMode": "v3", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "tabletop1", - "x": -20.53908115474743, - "y": 12.675865196363418, - "color": "#2c647d", - "layer": 1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 1, + "visible": true, + "spatial": {}, + "x": -20.53908115474743, + "y": 12.675865196363418, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 10, @@ -1507,17 +1602,31 @@ "p": 0.75 } ], + "color": "#2c647d", "sortMode": "v1", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Entity", "name": "table-top-dice", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 1, + "visible": true, + "spatial": {}, "x": 65, "y": 40, "width": 65.1612685556661, @@ -1530,38 +1639,30 @@ "color": "#1d4853", "scale": 0.7, "modelScale": 1, - "layer": 1, "parallax": 0.7, "ignoreScaling": false, "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Quad", "name": "tabletop1_1", - "x": 12.960918845252571, - "y": 13.175865196363418, - "color": "#2c647d", - "layer": 1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 1, + "visible": true, + "spatial": {}, + "x": 12.960918845252571, + "y": 13.175865196363418, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 35, @@ -1584,17 +1685,31 @@ "p": 0.7 } ], + "color": "#2c647d", "sortMode": "v3", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Entity", "name": "Static_386", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 1, + "visible": true, + "spatial": {}, "x": 15, "y": 54, "width": 33.18208520382986, @@ -1607,38 +1722,30 @@ "color": "#1d4853", "scale": 0.7, "modelScale": 1, - "layer": 1, "parallax": 0.75, "ignoreScaling": false, "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Quad", "name": "sunbeam_fuse", - "x": -71.60279117198134, - "y": 140.47520024754013, - "color": "#c95618", - "layer": 1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 0.2, - "blendMode": "screen", - "blur": 5, + "components": [], + "layer": 1, + "visible": true, + "spatial": {}, + "x": -71.60279117198134, + "y": 140.47520024754013, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -137.70178586019702, @@ -1661,32 +1768,35 @@ "p": 0.7 } ], + "color": "#c95618", "sortMode": "v3", + "opacity": 0.2, + "blendMode": "screen", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 5 }, { - "type": "Quad", "name": "shadow", - "x": 7.712982908024468, - "y": 164.46124773339642, - "color": "#535246", - "layer": 1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "multiply", - "blur": 5, + "components": [], + "layer": 1, + "visible": true, + "spatial": {}, + "x": 7.712982908024468, + "y": 164.46124773339642, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 10.235462610480951, @@ -1709,17 +1819,31 @@ "p": 0.77 } ], + "color": "#535246", "sortMode": "v1", + "opacity": 1, + "blendMode": "multiply", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 5 }, { - "type": "Entity", "name": "plant", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "spatial": {}, "x": -121, "y": 69, "width": 62.735725714285714, @@ -1732,23 +1856,26 @@ "color": "#AAAAAA", "scale": 0.1452215873015873, "modelScale": 0.2, - "layer": 0, "parallax": 0.648, "ignoreScaling": false, "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Entity", "name": "chair", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 1, + "visible": true, + "spatial": {}, "x": 58, "y": 77, "width": 35.06554666666667, @@ -1761,33 +1888,22 @@ "color": "#AAAAAA", "scale": 0.20997333333333335, "modelScale": 0.28, - "layer": 1, "parallax": 0.69, "ignoreScaling": false, "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Quad", "name": "Quad_7", - "x": -233.8281780813605, - "y": 74.88183758135345, - "color": "#666666", - "layer": 1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Backface", @@ -1799,10 +1915,13 @@ "cullingType": "render" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": 1, + "visible": false, + "spatial": {}, + "x": -233.8281780813605, + "y": 74.88183758135345, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -238.11111111111117, @@ -1825,37 +1944,27 @@ "p": 1 } ], + "color": "#666666", "sortMode": "v2", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Actor", "name": "miles_ds", - "x": -93.86496524392734, - "y": 90.88536850626807, - "width": 25.013972096100225, - "height": 102.14038605907592, - "baseWidth": 96, - "baseHeight": 392, - "colliderWidth": 32, - "colliderHeight": 6, - "spriteName": "miles_ds-idle-right.json", - "color": "#00ffff", - "scale": 0.26056220933437735, - "modelScale": 0.33, - "layer": 1, - "parallax": 0.8055746633180803, - "ignoreScaling": false, - "animationSpeed": 30, + "type": "Actor", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Shadow", @@ -1865,7 +1974,24 @@ "triggerId": "#lightSpots" } ], - "interactions": {}, + "layer": 1, + "visible": true, + "spatial": {}, + "x": -55.25117461216202, + "y": 90.89600096647854, + "width": 25.013972096100225, + "height": 102.14038605907592, + "baseWidth": 96, + "baseHeight": 392, + "colliderWidth": 32, + "colliderHeight": 6, + "spriteName": "miles_ds-idle-right.json", + "color": "#00ffff", + "scale": 0.26056220933437735, + "modelScale": 0.33, + "parallax": 0.8064607016689532, + "ignoreScaling": false, + "animationSpeed": 30, "opacity": 1, "blendMode": "source-over", "blur": 0, @@ -1890,18 +2016,14 @@ } }, { - "type": "Quad", "name": "Quad_5", - "x": -207.71706697024933, - "y": 79.32628202579792, - "color": "#b47e4b", - "layer": 1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Backface", @@ -1913,10 +2035,13 @@ "cullingType": "render" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": 1, + "visible": true, + "spatial": {}, + "x": -207.71706697024933, + "y": 79.32628202579792, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -212, @@ -1939,32 +2064,35 @@ "p": 1 } ], + "color": "#b47e4b", "sortMode": "v2", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_3", - "x": -245.72222222222223, - "y": 133.91666666666663, - "color": "#474747", - "layer": 1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 1, + "visible": true, + "spatial": {}, + "x": -245.72222222222223, + "y": 133.91666666666663, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -241, @@ -1987,32 +2115,35 @@ "p": 1 } ], + "color": "#474747", "sortMode": "v2", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "w4_1", - "x": -158.13739111340288, - "y": -247.54630050191656, - "color": "#0a0a0a", - "layer": 2, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 2, + "visible": true, + "spatial": {}, + "x": -158.13739111340288, + "y": -247.54630050191656, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -289, @@ -2035,32 +2166,35 @@ "p": 1 } ], + "color": "#0a0a0a", "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#1c9245", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "bas1", - "x": -173.87012979571483, - "y": 224.39393470676015, - "color": "#0a0a0a", - "layer": 2, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 2, + "visible": true, + "spatial": {}, + "x": -173.87012979571483, + "y": 224.39393470676015, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -449, @@ -2083,32 +2217,35 @@ "p": 1 } ], + "color": "#0a0a0a", "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "bas2", - "x": 251.2525776259888, - "y": -6.459972841696704, - "color": "#0a0a0a", - "layer": 2, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 2, + "visible": true, + "spatial": {}, + "x": 251.2525776259888, + "y": -6.459972841696704, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 201.75257762598838, @@ -2131,27 +2268,27 @@ "p": 1 } ], + "color": "#0a0a0a", "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_521", - "x": -187.12107701481986, - "y": 57.681818181818166, - "color": "#855d38", - "layer": 2, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Backface", @@ -2163,10 +2300,13 @@ "cullingType": "render" } ], - "interactions": {}, - "opacity": 0.75, - "blendMode": "source-over", - "blur": 2, + "layer": 2, + "visible": true, + "spatial": {}, + "x": -187.12107701481986, + "y": 57.681818181818166, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -210.302895196638, @@ -2189,32 +2329,35 @@ "p": 1 } ], + "color": "#855d38", "sortMode": "ignore", + "opacity": 0.75, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 2 }, { - "type": "Quad", "name": "sunbeam2", - "x": 98.16326736878159, - "y": 142.75, - "color": "#c95618", - "layer": 2, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": true, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 0.5, - "blendMode": "screen", - "blur": 10, + "components": [], + "layer": 2, + "visible": true, + "spatial": {}, + "x": 98.16326736878159, + "y": 142.75, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 88.25, @@ -2237,13 +2380,17 @@ "p": 0.64 } ], + "color": "#c95618", "sortMode": "v3", + "opacity": 0.5, + "blendMode": "screen", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 10 } ], "camera": { diff --git a/public/scenes/test1.json b/public/scenes/test1.json index fd67ccc..1211f4a 100644 --- a/public/scenes/test1.json +++ b/public/scenes/test1.json @@ -1,6 +1,8 @@ { "id": "test1", "name": "New Scene", + "description": "You are in New Scene.", + "textRedirects": {}, "filename": "test1", "walkbox": [], "triggerboxes": [], @@ -13,28 +15,26 @@ }, "entities": [ { - "type": "Quad", "name": "Table", - "x": 59.59895324707031, - "y": 38.5, - "color": "#3b3b3b", - "layer": -1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "WalkBox", "mode": "Subtract" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "multiply", - "blur": 4, + "layer": -1, + "visible": true, + "x": 59.59895324707031, + "y": 38.5, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 12.319771405705986, @@ -81,45 +81,51 @@ } } ], + "color": "#3b3b3b", "sortMode": "v3", + "opacity": 1, + "blendMode": "multiply", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 4 }, { - "type": "Actor", "name": "Hero", - "x": 342.2897696485943, - "y": 6.8788407954938435, - "width": 81.3795553528816, - "height": 196.41608730232534, - "baseWidth": 162, - "baseHeight": 391, + "type": "Actor", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -202.22446023938141, + "y": 56.1156452815223, + "width": 54.53940368071831, + "height": 222.70256502959978, + "baseWidth": 96, + "baseHeight": 392, "colliderWidth": 67, "colliderHeight": 9, - "spriteName": "miles_ds-idle-down.json", + "spriteName": "miles_ds-idle-right.json", "color": "#00ffff", - "scale": 0.5023429342770469, + "scale": 0.5681187883408158, "modelScale": 0.73, - "layer": 0, - "parallax": 0.6772222218538513, + "parallax": 0.7836801774993183, "ignoreScaling": false, "animationSpeed": 30, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0, "isPlayer": true, "speed": 0.24, - "direction": "down", + "direction": "right", "animSets": { "idle": { "id": "idle", @@ -138,27 +144,25 @@ } }, { - "type": "Quad", "name": "Floor", - "x": -102.34295904141581, - "y": 154.6256011960822, - "color": "#888888", - "layer": -1, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "3d-parallax" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": -1, + "visible": true, + "x": -102.34295904141581, + "y": 154.6256011960822, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -217.4024508165162, @@ -181,32 +185,34 @@ "p": 1 } ], + "color": "#888888", "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", "isGrid": true, "gridLinesX": 7, "gridLinesY": 7, "lineWidth": 1, "gridColor": "#ffffff", - "filled": false + "filled": false, + "blur": 0 }, { - "type": "Quad", "name": "Table_1", - "x": 76.7688832240514, - "y": -8.859439815848802, - "color": "#888888", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 0, + "visible": true, + "x": 76.7688832240514, + "y": -8.859439815848802, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 19.489701382687066, @@ -229,32 +235,34 @@ "p": 0.7999999999999999 } ], + "color": "#888888", "sortMode": "v3", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_390", - "x": -301.0427737310781, - "y": -19.020837617886528, - "color": "#888888", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 0, + "visible": true, + "x": -301.0427737310781, + "y": -19.020837617886528, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": -329.6782468829987, @@ -283,119 +291,127 @@ "p": 0.8 } ], + "color": "#888888", "sortMode": "v1", + "opacity": 1, + "blendMode": "source-over", "isGrid": true, "gridLinesX": 0, "gridLinesY": 0, "lineWidth": 2, "gridColor": "#ffffff", - "filled": false + "filled": false, + "blur": 0 }, { + "name": "Chair", "type": "Entity", - "name": "Static_356", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, "x": 228.3733428469584, "y": 44.85723027846018, - "width": 74.5754043558174, - "height": 126.82284333564156, + "width": 73.86837489927758, + "height": 125.62046988859183, "baseWidth": 167, "baseHeight": 284, "colliderWidth": 59, "colliderHeight": 28, "spriteName": "chair", "color": "#AAAAAA", - "scale": 0.44655930751986467, + "scale": 0.442325598199267, "modelScale": 0.6, - "layer": 0, "parallax": 0.7, "ignoreScaling": false, "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Entity", "name": "Static_261", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, "x": -436, "y": 58, - "width": 212.99109421060265, - "height": 322.2891557134119, + "width": 211.7236974198895, + "height": 320.37138425378015, "baseWidth": 269.4226327944573, "baseHeight": 407.67898383371823, "colliderWidth": 0, "colliderHeight": 0, "spriteName": null, "color": "#000000", - "scale": 0.790546406593442, + "scale": 0.7858422851261113, "modelScale": 1, - "layer": 0, "parallax": 0.8, "ignoreScaling": false, "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Entity", "name": "Static_332", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, "x": -175.66857200445347, "y": -2.59236790543932, - "width": 105.59413541296372, - "height": 120.1588437457863, + "width": 104.2060667709911, + "height": 118.57931736009331, "baseWidth": 145, "baseHeight": 165, "colliderWidth": 0, "colliderHeight": 0, "spriteName": "office-plant", "color": "#AAAAAA", - "scale": 0.7282354166411291, + "scale": 0.718662529455111, "modelScale": 1.1, - "layer": 0, "parallax": 0.63, "ignoreScaling": false, "animationSpeed": 150, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, "opacity": 1, "blendMode": "source-over", "blur": 0 }, { - "type": "Quad", "name": "Quad_72", - "x": 518.7507530972048, - "y": -123.06448817274526, - "color": "#3b494e", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, - "components": [], + "customName": "", + "textRedirects": {}, "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "components": [], + "layer": 0, + "visible": true, + "x": 518.7507530972048, + "y": -123.06448817274526, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 436, @@ -430,27 +446,27 @@ } } ], + "color": "#3b494e", "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_1", - "x": 347.11252701368375, - "y": -181.43675419702421, - "color": "#606b8a", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Backface", @@ -462,10 +478,12 @@ "cullingType": "render" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": 0, + "visible": true, + "x": 347.11252701368375, + "y": -181.43675419702421, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 413, @@ -504,27 +522,27 @@ } } ], + "color": "#606b8a", "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 }, { - "type": "Quad", "name": "Quad_2", - "x": 304.53304051582234, - "y": -222.5633211224302, - "color": "#606b8a", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, + "type": "Quad", "locked": false, "disabled": false, - "customName": "", "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, "components": [ { "type": "Backface", @@ -536,10 +554,12 @@ "cullingType": "render" } ], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, + "layer": 0, + "visible": true, + "x": 304.53304051582234, + "y": -222.5633211224302, + "parallax": 1, + "ignoreScaling": false, "vertices": [ { "x": 413, @@ -577,13 +597,52 @@ } } ], + "color": "#606b8a", "sortMode": "ignore", + "opacity": 1, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", - "filled": true + "filled": true, + "blur": 0 + }, + { + "name": "Chair", + "type": "Actor", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 618.6579311226798, + "y": -607.9714361625997, + "width": 15, + "height": 24, + "baseWidth": 25, + "baseHeight": 40, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#36d87fff", + "scale": 0.6, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0, + "isPlayer": false, + "speed": 0.1, + "direction": "down", + "animSets": {} } ], "camera": { diff --git a/public/scenes/test_room (10).json b/public/scenes/test_room (10).json index 775a7bb..6151972 100644 --- a/public/scenes/test_room (10).json +++ b/public/scenes/test_room (10).json @@ -1,119 +1,1115 @@ { "id": "test_room (10)", "name": "Test Room", + "description": "You are in Test Room.", + "textRedirects": {}, "filename": "test_room (10)", - "walkbox": [], - "triggerboxes": [], + "walkbox": [ + { + "name": "Walk_997", + "type": "Walkbox", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "poly": [ + { + "x": -79, + "y": 346 + }, + { + "x": -81, + "y": 272 + }, + { + "x": 153, + "y": 272 + }, + { + "x": 153, + "y": 346 + } + ], + "mode": "Add" + }, + { + "name": "Walk_176", + "type": "Walkbox", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "poly": [ + { + "x": 82, + "y": 192 + }, + { + "x": 407, + "y": 195 + }, + { + "x": 408, + "y": 203 + }, + { + "x": 470, + "y": 207 + }, + { + "x": 596, + "y": 220 + }, + { + "x": 624, + "y": 211 + }, + { + "x": 680, + "y": 211 + }, + { + "x": 680, + "y": 297 + }, + { + "x": -234, + "y": 291 + }, + { + "x": -210, + "y": 247 + }, + { + "x": -111, + "y": 249 + }, + { + "x": 77, + "y": 194 + } + ], + "mode": "Add" + } + ], + "triggerboxes": [ + { + "name": "Trig_sub_D", + "type": "Triggerbox", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Subscene", + "targetGroupId": "#D" + } + ], + "layer": 0, + "visible": true, + "poly": [ + { + "x": 27, + "y": 210 + }, + { + "x": 28, + "y": 104 + }, + { + "x": 84, + "y": 94 + }, + { + "x": 83, + "y": 136 + }, + { + "x": 54, + "y": 145 + }, + { + "x": 50, + "y": 160 + }, + { + "x": 76, + "y": 170 + }, + { + "x": 79, + "y": 194 + } + ], + "script": "" + }, + { + "name": "sub_sw_d1", + "type": "Triggerbox", + "locked": false, + "disabled": true, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Switch", + "groupId1": "nil", + "groupId2": "#D1", + "state": 1, + "idKey": "", + "sound1": "drawer_open.wav", + "sound2": "drawer_close.wav" + } + ], + "layer": 0, + "visible": true, + "spatial": { + "parentNodeId": "Trig_sub_D", + "relation": "in" + }, + "poly": [ + { + "x": -156.29411764705887, + "y": -171.3529411764706 + }, + { + "x": 425, + "y": -171 + }, + { + "x": 414, + "y": -95 + }, + { + "x": -143, + "y": -95 + } + ], + "script": "" + }, + { + "name": "Trig_834", + "type": "Triggerbox", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "poly": [], + "script": "" + }, + { + "name": "sub_sw_d2", + "type": "Triggerbox", + "locked": false, + "disabled": true, + "groupID": "#D ", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Switch", + "groupId1": "nil", + "groupId2": "#D2", + "state": 1, + "idKey": "", + "sound1": "drawer_open.wav", + "sound2": "drawer_close.wav" + } + ], + "layer": 0, + "visible": true, + "spatial": { + "parentNodeId": "Trig_sub_D", + "relation": "in" + }, + "poly": [ + { + "x": -156.29411764705878, + "y": -87.03921568627447 + }, + { + "x": 424.9999999999999, + "y": -86.68627450980387 + }, + { + "x": 413.9999999999999, + "y": -10.68627450980393 + }, + { + "x": -142.99999999999994, + "y": -10.68627450980393 + } + ], + "script": "" + }, + { + "name": "Desk", + "type": "Triggerbox", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "poly": [ + { + "x": -210, + "y": 211 + }, + { + "x": -192, + "y": 213 + }, + { + "x": -192, + "y": 197 + }, + { + "x": -171, + "y": 189 + }, + { + "x": -159, + "y": 211 + }, + { + "x": -136, + "y": 217 + }, + { + "x": -131, + "y": 242 + }, + { + "x": -109, + "y": 243 + }, + { + "x": -110, + "y": 146 + }, + { + "x": -18, + "y": 129 + }, + { + "x": -16, + "y": 201 + }, + { + "x": 26, + "y": 207 + }, + { + "x": 82, + "y": 193 + }, + { + "x": 87, + "y": 95 + }, + { + "x": -70, + "y": 78 + }, + { + "x": -236, + "y": 115 + }, + { + "x": -235, + "y": 173 + }, + { + "x": -218, + "y": 182 + }, + { + "x": -212, + "y": 211 + } + ], + "script": "" + } + ], "scaling": { - "enabled": false, - "min": 0.5, + "enabled": true, + "min": 0.91, "max": 1, - "horizon": 78, - "front": 300 + "horizon": 193, + "front": 269 }, "entities": [ { + "name": "CityView", "type": "Entity", - "name": "Static_832", - "x": 66, - "y": 39, - "width": 3.5, - "height": 8, - "baseWidth": 3.5, - "baseHeight": 8, - "spriteName": null, + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -2, + "visible": true, + "spatial": { + "parentNodeId": "window1", + "relation": "in" + }, + "x": 119.67896209456934, + "y": 234, + "width": 821.6, + "height": 551.2, + "baseWidth": 1264, + "baseHeight": 848, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "window-view.json", "color": "#00ff00", - "scale": 1, - "layer": 0, - "parallax": 0.5, - "ignoreScaling": false + "scale": 0.65, + "modelScale": 0.65, + "parallax": 0.4, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 }, { + "name": "room", "type": "Entity", - "name": "Pillar", - "x": 177, - "y": 85, - "width": 96.44819819819818, - "height": 3.6103603603603602, - "baseWidth": 96.44819819819818, - "baseHeight": 3.6103603603603602, - "spriteName": null, + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -1, + "visible": true, + "x": 199, + "y": 297, + "width": 884.8, + "height": 593.5999999999999, + "baseWidth": 1264, + "baseHeight": 848, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "room2", "color": "#888888", - "scale": 1, - "layer": -6, + "scale": 0.7, + "modelScale": 0.7, "parallax": 1, - "ignoreScaling": false + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 }, { + "name": "Chair", "type": "Entity", - "name": "Key", - "x": 73, - "y": 80, - "width": 1.272623975326678, - "height": 1.272623975326678, - "baseWidth": 1.272623975326678, - "baseHeight": 1.272623975326678, - "spriteName": null, - "color": "#ffff00", - "scale": 1, + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], "layer": 0, + "visible": true, + "x": 100, + "y": 249, + "width": 116.89999999999999, + "height": 198.79999999999998, + "baseWidth": 167, + "baseHeight": 284, + "colliderWidth": 78, + "colliderHeight": 18, + "spriteName": "chair.json", + "color": "#00ff00", + "scale": 0.7, + "modelScale": 0.7, "parallax": 1, - "ignoreScaling": false + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 }, { - "type": "Player", - "name": "Player", - "x": 150.91451042912607, - "y": 81.31452047584774, - "width": 15.234510253628654, - "height": 25.390850422714422, - "baseWidth": 15.234510253628654, - "baseHeight": 25.390850422714422, - "spriteName": "assets/hero.png", + "name": "Hero_1", + "type": "Actor", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Shadow", + "shadowQuadId": "shadow", + "offsetX": -70, + "offsetY": -25, + "triggerId": "room" + } + ], + "layer": 0, + "visible": true, + "x": 232.86186143649797, + "y": 223.85074125381902, + "width": 71.03999999999999, + "height": 290.08, + "baseWidth": 96, + "baseHeight": 392, + "colliderWidth": 88, + "colliderHeight": 4, + "spriteName": "miles_ds-idle-right.json", "color": "#00ffff", + "scale": 0.74, + "modelScale": 0.74, + "parallax": 1.024795357575403, + "ignoreScaling": true, + "animationSpeed": 30, + "opacity": 1, + "blendMode": "source-over", + "blur": 0, + "isPlayer": true, + "speed": 0.24, + "direction": "left", + "animSets": { + "idle": { + "id": "idle", + "up": "miles_ds-idle-up.json", + "down": "miles_ds-idle-down.json", + "left": null, + "right": "miles_ds-idle-right.json" + }, + "walk": { + "id": "walk", + "up": "miles_ds-walk-up.json", + "down": "miles_ds-walk-down.json", + "left": null, + "right": "miles_ds-walk-right.json" + } + } + }, + { + "name": "Black_1", + "type": "Entity", + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -328, + "y": 307, + "width": 171.2340644206598, + "height": 637.1050459736764, + "baseWidth": 155.6673312915089, + "baseHeight": 579.1864054306149, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#000000", + "scale": 1.1, + "modelScale": 1.1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D_main", + "type": "Entity", + "locked": true, + "disabled": true, + "groupID": "#D", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 135, + "y": 310, + "width": 614.4, + "height": 484.79999999999995, + "baseWidth": 1024, + "baseHeight": 808, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_main", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D2_body", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D2", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Subtrigger", + "target": "sub_sw_d2" + } + ], + "layer": 3, + "visible": true, + "x": 134, + "y": 311, + "width": 614.4, + "height": 407.4, + "baseWidth": 1024, + "baseHeight": 679, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d2.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D1_body", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 4, + "visible": true, + "x": 136, + "y": -4, + "width": 614.4, + "height": 177.6, + "baseWidth": 1024, + "baseHeight": 296, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_body.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "miles_id", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "your ID card", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Item", + "ignoreDistance": true + } + ], + "layer": 5, + "visible": true, + "spatial": { + "parentNodeId": "Drawer1", + "relation": "in" + }, + "x": 93, + "y": 0, + "width": 150.4436263347707, + "height": 93.2343137254902, + "baseWidth": 165.32266630194582, + "baseHeight": 102.45528980823099, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_id.json", + "color": "#AAAAAA", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D1_stuff", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 5, + "visible": true, + "spatial": { + "parentNodeId": "Drawer1", + "relation": "in" + }, + "x": 320, + "y": 184, + "width": 979, + "height": 412, + "baseWidth": 979, + "baseHeight": 412, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_items.json", + "color": "#00ff00", "scale": 1, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D_main_top", + "type": "Entity", + "locked": true, + "disabled": true, + "groupID": "#D", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 6, + "visible": true, + "x": 135, + "y": -176, + "width": 614.4, + "height": 129.6, + "baseWidth": 1024, + "baseHeight": 216, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_top.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D1_fasade", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Subtrigger", + "target": "sub_sw_d1" + } + ], + "layer": 6, + "visible": true, + "x": 135, + "y": 66, + "width": 614.4, + "height": 105.6, + "baseWidth": 1024, + "baseHeight": 176, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_facade.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "floor-parallax", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "3d-parallax" + } + ], "layer": 0, + "visible": true, + "x": -146.13725490196006, + "y": 289.60784313725503, "parallax": 1, - "ignoreScaling": false + "ignoreScaling": false, + "vertices": [ + { + "x": -205.9411764705876, + "y": 185.68627450980406, + "p": 1 + }, + { + "x": 708, + "y": 186, + "p": 1 + }, + { + "x": 766, + "y": 339, + "p": 1.1 + }, + { + "x": -196, + "y": 339, + "p": 1.1 + } + ], + "color": "#52779aff", + "sortMode": "ignore", + "opacity": 0, + "blendMode": "source-over", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 0 }, { + "name": "Actor_562", "type": "Actor", - "name": "Actor_508", - "x": 131, - "y": 87, - "width": 4.162162162162162, - "height": 14.047297297297298, - "baseWidth": 4.162162162162162, - "baseHeight": 14.047297297297298, + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 222.9043373174802, + "y": 306.9284515529539, + "width": 1008.8000000000001, + "height": 90.39999999999999, + "baseWidth": 1261, + "baseHeight": 112.99999999999999, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sofa", + "color": "#36d87fff", + "scale": 0.8, + "modelScale": 0.8, + "parallax": 1.0790382036293817, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0, + "isPlayer": false, + "speed": 0.1, + "direction": "down", + "animSets": {} + }, + { + "name": "boombox", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -152, + "y": -11, + "width": 112, + "height": 47, + "baseWidth": 123.07692307692307, + "baseHeight": 51.64835164835165, + "colliderWidth": 0, + "colliderHeight": 0, "spriteName": null, - "color": "#0000ff", - "scale": 1, + "color": "#AAAAAA", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 0, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "test", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Item" + } + ], "layer": 0, + "visible": true, + "spatial": { + "parentNodeId": "Chair", + "relation": "under" + }, + "x": 98, + "y": 243, + "width": 31.114048235454423, + "height": 35.128764136803376, + "baseWidth": 32.10246627605941, + "baseHeight": 36.24471998909933, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#AAAAAA", + "scale": 0.9692105263157895, + "modelScale": 1, "parallax": 1, - "ignoreScaling": false + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 }, { + "name": "Static_649", "type": "Entity", - "name": "Static_385", - "x": 198, - "y": 135, - "width": 15, - "height": 34, - "baseWidth": 15, - "baseHeight": 34, + "locked": false, + "disabled": true, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 15, + "visible": true, + "spatial": { + "parentNodeId": "Drawer3", + "relation": "in" + }, + "x": 138, + "y": 101, + "width": 492, + "height": 58, + "baseWidth": 540.6593406593406, + "baseHeight": 63.73626373626374, + "colliderWidth": 0, + "colliderHeight": 0, "spriteName": null, - "color": "#00ff00", - "scale": 1, + "color": "#b83ed0", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "Drawer1", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], "layer": 0, - "parallax": 1.5, - "ignoreScaling": false + "visible": true, + "spatial": { + "parentNodeId": "Desk", + "relation": "in" + }, + "x": 90.66834500947778, + "y": 120, + "width": 27.3, + "height": 27.3, + "baseWidth": 30, + "baseHeight": 30, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#AAAAAA", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 0, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "window1", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -2, + "visible": true, + "x": 56, + "y": -192, + "width": 27.3, + "height": 27.3, + "baseWidth": 30, + "baseHeight": 30, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#AAAAAA", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "shadow", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 177.40625891347847, + "y": 204.99379689971948, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": 177.40625891347847, + "y": 204.99379689971948, + "p": 1.0124869315719702 + }, + { + "x": 273.3546785024226, + "y": 203.45494893614816, + "p": 1.0114617012261051 + }, + { + "x": 262.5537137492114, + "y": 249.54350071101484, + "p": 1.0415316998111208 + }, + { + "x": 212.71215117336098, + "y": 247.59187519264492, + "p": 1.0402645533908195 + } + ], + "color": "#000975", + "sortMode": "ignore", + "opacity": 0.6, + "blendMode": "multiply", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 7 } ], "camera": { - "x": 0, - "y": 0, - "zoom": 1 + "x": 297, + "y": 29, + "zoom": 0.51 }, "autoCenter": false, - "cameraSpeed": 5 + "cameraSpeed": 1.5, + "camDeadzoneX": 200, + "camDeadzoneY": -21, + "camMinX": 143, + "camMaxY": 45 } diff --git a/public/scenes/test_room.json b/public/scenes/test_room.json index f0460bc..cf76365 100644 --- a/public/scenes/test_room.json +++ b/public/scenes/test_room.json @@ -1,158 +1,506 @@ { "id": "test_room", - "name": "New Scene", + "name": "Test Room", + "description": "You are in Test Room.", + "textRedirects": {}, "filename": "test_room", - "walkbox": [], - "triggerboxes": [], - "scaling": { - "enabled": true, - "min": 0.4, - "max": 1, - "horizon": 78, - "front": 421 - }, - "entities": [ + "walkbox": [ { - "name": "Quad_201", - "type": "Quad", + "name": "Walk_997", + "type": "Walkbox", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "poly": [ + { + "x": -79, + "y": 346 + }, + { + "x": -81, + "y": 272 + }, + { + "x": 153, + "y": 272 + }, + { + "x": 153, + "y": 346 + } + ], + "mode": "Add" + }, + { + "name": "Walk_176", + "type": "Walkbox", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "poly": [ + { + "x": 82, + "y": 192 + }, + { + "x": 407, + "y": 195 + }, + { + "x": 408, + "y": 203 + }, + { + "x": 470, + "y": 207 + }, + { + "x": 596, + "y": 220 + }, + { + "x": 624, + "y": 211 + }, + { + "x": 680, + "y": 211 + }, + { + "x": 680, + "y": 297 + }, + { + "x": -234, + "y": 291 + }, + { + "x": -210, + "y": 247 + }, + { + "x": -111, + "y": 249 + }, + { + "x": 77, + "y": 194 + } + ], + "mode": "Add" + } + ], + "triggerboxes": [ + { + "name": "Trig_sub_D", + "type": "Triggerbox", "locked": false, "disabled": false, - "groupID": "#g,#a", + "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { - "type": "3d-parallax" + "type": "Subscene", + "targetGroupId": "#D" } ], - "layer": -1, + "layer": 0, "visible": true, - "x": 334.7566652973353, - "y": 147.45349435034427, - "parallax": 1, - "ignoreScaling": false, - "vertices": [ + "poly": [ { - "x": -1431.0063050419365, - "y": 419.98108487419046, - "p": 0.2 + "x": 27, + "y": 210 }, { - "x": 1588.9936949580635, - "y": 415.98108487419046, - "p": 0.2 + "x": 28, + "y": 104 }, { - "x": 3915.4050968588786, - "y": 457.44748933663504, - "p": 1 + "x": 84, + "y": 94 }, { - "x": -3349.5949031411214, - "y": 457.44748933663504, - "p": 1 + "x": 83, + "y": 136 + }, + { + "x": 54, + "y": 145 + }, + { + "x": 50, + "y": 160 + }, + { + "x": 76, + "y": 170 + }, + { + "x": 79, + "y": 194 } ], - "color": "#888888", - "sortMode": "ignore", - "opacity": 1, - "blendMode": "source-over", - "isGrid": true, - "gridLinesX": 29, - "gridLinesY": 7, - "lineWidth": 4, - "gridColor": "#0f6719", - "filled": false, - "blur": 0 + "script": "" }, { - "name": "Quad_1", - "type": "Quad", + "name": "sub_sw_d1", + "type": "Triggerbox", + "locked": false, + "disabled": true, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Switch", + "groupId1": "nil", + "groupId2": "#D1", + "state": 1, + "idKey": "", + "sound1": "drawer_open.wav", + "sound2": "drawer_close.wav" + } + ], + "layer": 0, + "visible": true, + "spatial": { + "parentNodeId": "Trig_sub_D", + "relation": "in" + }, + "poly": [ + { + "x": -156.29411764705887, + "y": -171.3529411764706 + }, + { + "x": 425, + "y": -171 + }, + { + "x": 414, + "y": -95 + }, + { + "x": -143, + "y": -95 + } + ], + "script": "" + }, + { + "name": "Trig_834", + "type": "Triggerbox", "locked": false, "disabled": false, - "groupID": "#a", + "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], - "layer": -1, + "layer": 0, "visible": true, - "x": 363.1753332849686, - "y": -1342.0347499102, - "parallax": 1, - "ignoreScaling": false, - "vertices": [ + "poly": [], + "script": "" + }, + { + "name": "sub_sw_d2", + "type": "Triggerbox", + "locked": false, + "disabled": true, + "groupID": "#D ", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ { - "x": -1382.5263247292278, - "y": -605.2106827614945, - "p": 0.2 + "type": "Switch", + "groupId1": "nil", + "groupId2": "#D2", + "state": 1, + "idKey": "", + "sound1": "drawer_open.wav", + "sound2": "drawer_close.wav" + } + ], + "layer": 0, + "visible": true, + "spatial": { + "parentNodeId": "Trig_sub_D", + "relation": "in" + }, + "poly": [ + { + "x": -156.29411764705878, + "y": -87.03921568627447 }, { - "x": 1692.4736752707722, - "y": -565.2106827614945, - "p": 0.2 + "x": 424.9999999999999, + "y": -86.68627450980387 }, { - "x": 3933.8237648465147, - "y": -1042.0407549239112, - "p": 1 + "x": 413.9999999999999, + "y": -10.68627450980393 }, { - "x": -3331.1762351534853, - "y": -1042.0407549239112, - "p": 1 + "x": -142.99999999999994, + "y": -10.68627450980393 } ], + "script": "" + }, + { + "name": "Desk", + "type": "Triggerbox", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "poly": [ + { + "x": -210, + "y": 211 + }, + { + "x": -192, + "y": 213 + }, + { + "x": -192, + "y": 197 + }, + { + "x": -171, + "y": 189 + }, + { + "x": -159, + "y": 211 + }, + { + "x": -136, + "y": 217 + }, + { + "x": -131, + "y": 242 + }, + { + "x": -109, + "y": 243 + }, + { + "x": -110, + "y": 146 + }, + { + "x": -18, + "y": 129 + }, + { + "x": -16, + "y": 201 + }, + { + "x": 26, + "y": 207 + }, + { + "x": 82, + "y": 193 + }, + { + "x": 87, + "y": 95 + }, + { + "x": -70, + "y": 78 + }, + { + "x": -236, + "y": 115 + }, + { + "x": -235, + "y": 173 + }, + { + "x": -218, + "y": 182 + }, + { + "x": -212, + "y": 211 + } + ], + "script": "" + } + ], + "scaling": { + "enabled": true, + "min": 0.91, + "max": 1, + "horizon": 193, + "front": 269 + }, + "entities": [ + { + "name": "CityView", + "type": "Entity", + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -2, + "visible": true, + "spatial": { + "parentNodeId": "window1", + "relation": "in" + }, + "x": 119.67896209456934, + "y": 234, + "width": 821.6, + "height": 551.2, + "baseWidth": 1264, + "baseHeight": 848, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "window-view.json", + "color": "#00ff00", + "scale": 0.65, + "modelScale": 0.65, + "parallax": 0.4, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "room", + "type": "Entity", + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -1, + "visible": true, + "x": 199, + "y": 297, + "width": 884.8, + "height": 593.5999999999999, + "baseWidth": 1264, + "baseHeight": 848, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "room2", "color": "#888888", - "sortMode": "ignore", + "scale": 0.7, + "modelScale": 0.7, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, "opacity": 1, "blendMode": "source-over", - "isGrid": true, - "gridLinesX": 29, - "gridLinesY": 7, - "lineWidth": 4, - "gridColor": "#0f6719", - "filled": false, "blur": 0 }, { - "name": "miles_ds", + "name": "Chair", + "type": "Entity", + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 100, + "y": 249, + "width": 116.89999999999999, + "height": 198.79999999999998, + "baseWidth": 167, + "baseHeight": 284, + "colliderWidth": 78, + "colliderHeight": 18, + "spriteName": "chair.json", + "color": "#00ff00", + "scale": 0.7, + "modelScale": 0.7, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "Hero_1", "type": "Actor", "locked": false, "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [ { "type": "Shadow", "shadowQuadId": "shadow", - "offsetX": -80, - "offsetY": -0.79232867459731, - "triggerId": "#g" + "offsetX": -70, + "offsetY": -25, + "triggerId": "room" } ], "layer": 0, "visible": true, - "x": 159.0196542021097, - "y": 443.2970635781322, - "width": 143.88497017161473, - "height": 347.27792183395906, + "x": 306.351468358703, + "y": 227.84447142883158, + "width": 119.88, + "height": 289.34, "baseWidth": 162, "baseHeight": 391, - "colliderWidth": 0, - "colliderHeight": 0, + "colliderWidth": 88, + "colliderHeight": 4, "spriteName": "miles_ds-idle-down.json", "color": "#00ffff", - "scale": 0.8881788282198441, - "modelScale": 1.03, - "parallax": 0.7269997060619294, - "ignoreScaling": false, + "scale": 0.74, + "modelScale": 0.74, + "parallax": 1.0273839007520702, + "ignoreScaling": true, "animationSpeed": 30, "opacity": 1, "blendMode": "source-over", "blur": 0, "isPlayer": true, - "speed": 0.35, + "speed": 0.24, "direction": "down", "animSets": { "idle": { @@ -172,123 +520,596 @@ } }, { - "name": "shadow", + "name": "Black_1", + "type": "Entity", + "locked": true, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": -328, + "y": 307, + "width": 171.2340644206598, + "height": 637.1050459736764, + "baseWidth": 155.6673312915089, + "baseHeight": 579.1864054306149, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#000000", + "scale": 1.1, + "modelScale": 1.1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D_main", + "type": "Entity", + "locked": true, + "disabled": true, + "groupID": "#D", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 135, + "y": 310, + "width": 614.4, + "height": 484.79999999999995, + "baseWidth": 1024, + "baseHeight": 808, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_main", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D2_body", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D2", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Subtrigger", + "target": "sub_sw_d2" + } + ], + "layer": 3, + "visible": true, + "x": 134, + "y": 311, + "width": 614.4, + "height": 407.4, + "baseWidth": 1024, + "baseHeight": 679, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d2.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D1_body", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 4, + "visible": true, + "x": 136, + "y": -4, + "width": 614.4, + "height": 177.6, + "baseWidth": 1024, + "baseHeight": 296, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_body.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "miles_id", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "your ID card", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Item", + "ignoreDistance": true + } + ], + "layer": 5, + "visible": true, + "spatial": { + "parentNodeId": "Drawer1", + "relation": "in" + }, + "x": 93, + "y": 0, + "width": 150.4436263347707, + "height": 93.2343137254902, + "baseWidth": 165.32266630194582, + "baseHeight": 102.45528980823099, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_id.json", + "color": "#AAAAAA", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D1_stuff", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 5, + "visible": true, + "spatial": { + "parentNodeId": "Drawer1", + "relation": "in" + }, + "x": 320, + "y": 184, + "width": 979, + "height": 412, + "baseWidth": 979, + "baseHeight": 412, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_items.json", + "color": "#00ff00", + "scale": 1, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D_main_top", + "type": "Entity", + "locked": true, + "disabled": true, + "groupID": "#D", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 6, + "visible": true, + "x": 135, + "y": -176, + "width": 614.4, + "height": 129.6, + "baseWidth": 1024, + "baseHeight": 216, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_top.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "sub_D1_fasade", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": "#D1", + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Subtrigger", + "target": "sub_sw_d1" + } + ], + "layer": 6, + "visible": true, + "x": 135, + "y": 66, + "width": 614.4, + "height": 105.6, + "baseWidth": 1024, + "baseHeight": 176, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sub_drawers_d1_facade.json", + "color": "#00ff00", + "scale": 0.6, + "modelScale": 0.6, + "parallax": 1, + "ignoreScaling": true, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "floor-parallax", "type": "Quad", "locked": false, "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, - "components": [], + "components": [ + { + "type": "3d-parallax" + } + ], "layer": 0, "visible": true, - "x": 87.7396949124423, - "y": 443.2105923504103, + "x": -146.13725490196006, + "y": 289.60784313725503, "parallax": 1, "ignoreScaling": false, "vertices": [ { - "x": 87.7396949124423, - "y": 443.2105923504103, - "p": 0.7253314403158566 + "x": -205.9411764705876, + "y": 185.68627450980406, + "p": 1 }, { - "x": 196.48465665453705, - "y": 443.21612487072593, - "p": 0.7254381777171315 + "x": 708, + "y": 186, + "p": 1 }, { - "x": 197.82134195961072, - "y": 449.4326708990447, - "p": 0.8453722999813154 + "x": 766, + "y": 339, + "p": 1.1 }, { - "x": 95.34720342352023, - "y": 449.095953404058, - "p": 0.8388761014446592 + "x": -196, + "y": 339, + "p": 1.1 } ], - "color": "#000000", + "color": "#52779aff", "sortMode": "ignore", - "opacity": 1, - "blendMode": "multiply", + "opacity": 0, + "blendMode": "source-over", "isGrid": false, "gridLinesX": 5, "gridLinesY": 5, "lineWidth": 1, "gridColor": "#ffffff", "filled": true, - "blur": 4 + "blur": 0 }, { - "name": "logo", + "name": "Actor_562", + "type": "Actor", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 222.9043373174802, + "y": 306.9284515529539, + "width": 1008.8000000000001, + "height": 90.39999999999999, + "baseWidth": 1261, + "baseHeight": 112.99999999999999, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": "sofa", + "color": "#36d87fff", + "scale": 0.8, + "modelScale": 0.8, + "parallax": 1.0790382036293817, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0, + "isPlayer": false, + "speed": 0.1, + "direction": "down", + "animSets": {} + }, + { + "name": "boombox", "type": "Entity", "locked": false, "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, "visible": true, - "x": 91, - "y": 392, - "width": 706.7755102040817, - "height": 721.2734693877551, - "baseWidth": 585, - "baseHeight": 597, + "x": -152, + "y": -11, + "width": 112, + "height": 47, + "baseWidth": 123.07692307692307, + "baseHeight": 51.64835164835165, "colliderWidth": 0, "colliderHeight": 0, - "spriteName": "scanline_logo", + "spriteName": null, "color": "#AAAAAA", - "scale": 1.2081632653061225, - "modelScale": 2.8, - "parallax": 0.2, + "scale": 0.91, + "modelScale": 1, + "parallax": 1, "ignoreScaling": false, "animationSpeed": 150, - "opacity": 0.6, - "blendMode": "lighter", + "opacity": 0, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "test", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [ + { + "type": "Item" + } + ], + "layer": 0, + "visible": true, + "spatial": { + "parentNodeId": "Chair", + "relation": "under" + }, + "x": 98, + "y": 243, + "width": 31.114048235454423, + "height": 35.128764136803376, + "baseWidth": 32.10246627605941, + "baseHeight": 36.24471998909933, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#AAAAAA", + "scale": 0.9692105263157895, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "Static_649", + "type": "Entity", + "locked": false, + "disabled": true, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 15, + "visible": true, + "spatial": { + "parentNodeId": "Drawer3", + "relation": "in" + }, + "x": 138, + "y": 101, + "width": 492, + "height": 58, + "baseWidth": 540.6593406593406, + "baseHeight": 63.73626373626374, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#b83ed0", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 1, + "blendMode": "source-over", "blur": 0 }, { - "name": "logo_1", + "name": "Drawer1", "type": "Entity", "locked": false, "disabled": false, "groupID": null, "customName": "", + "textRedirects": {}, "interactions": {}, "components": [], "layer": 0, "visible": true, - "x": 96, - "y": 340, - "width": 655.1999999999999, - "height": 668.64, - "baseWidth": 585, - "baseHeight": 597, + "spatial": { + "parentNodeId": "Desk", + "relation": "in" + }, + "x": 90.66834500947778, + "y": 120, + "width": 27.3, + "height": 27.3, + "baseWidth": 30, + "baseHeight": 30, "colliderWidth": 0, "colliderHeight": 0, - "spriteName": "scanline_logo", + "spriteName": null, "color": "#AAAAAA", - "scale": 1.1199999999999999, - "modelScale": 2.8, - "parallax": 0.2, + "scale": 0.91, + "modelScale": 1, + "parallax": 1, + "ignoreScaling": false, + "animationSpeed": 150, + "opacity": 0, + "blendMode": "source-over", + "blur": 0 + }, + { + "name": "window1", + "type": "Entity", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": -2, + "visible": true, + "x": 56, + "y": -192, + "width": 27.3, + "height": 27.3, + "baseWidth": 30, + "baseHeight": 30, + "colliderWidth": 0, + "colliderHeight": 0, + "spriteName": null, + "color": "#AAAAAA", + "scale": 0.91, + "modelScale": 1, + "parallax": 1, "ignoreScaling": false, "animationSpeed": 150, "opacity": 1, "blendMode": "source-over", - "blur": 32 + "blur": 0 + }, + { + "name": "shadow", + "type": "Quad", + "locked": false, + "disabled": false, + "groupID": null, + "customName": "", + "textRedirects": {}, + "interactions": {}, + "components": [], + "layer": 0, + "visible": true, + "x": 251.56334401080537, + "y": 208.4319364013629, + "parallax": 1, + "ignoreScaling": false, + "vertices": [ + { + "x": 251.56334401080537, + "y": 208.4319364013629, + "p": 1.014733918039131 + }, + { + "x": 347.5117635997495, + "y": 206.89308843779156, + "p": 1.013708687693266 + }, + { + "x": 336.71084835449676, + "y": 252.9816450467687, + "p": 1.0437788529717444 + }, + { + "x": 286.86923627068785, + "y": 251.03001469428833, + "p": 1.0425115398579803 + } + ], + "color": "#000975", + "sortMode": "ignore", + "opacity": 0.6, + "blendMode": "multiply", + "isGrid": false, + "gridLinesX": 5, + "gridLinesY": 5, + "lineWidth": 1, + "gridColor": "#ffffff", + "filled": true, + "blur": 7 } ], "camera": { - "x": 165.57094204403754, - "y": -263.4392820164094, - "zoom": 0.1500946352969992 + "x": 297, + "y": 29, + "zoom": 0.51 }, - "autoCenter": true, - "cameraSpeed": 5, - "camDeadzoneX": 50, - "camDeadzoneY": 30, - "camMaxY": -370 + "autoCenter": false, + "cameraSpeed": 1.5, + "camDeadzoneX": 200, + "camDeadzoneY": -21, + "camMinX": 143, + "camMaxY": 45 } diff --git a/public/scenes/ttt.json b/public/scenes/ttt.json deleted file mode 100644 index e8c841a..0000000 --- a/public/scenes/ttt.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "id": "ttt", - "name": "New Scene", - "filename": "ttt", - "walkbox": [], - "triggerboxes": [], - "scaling": { - "enabled": true, - "min": 0.5, - "max": 1, - "horizon": 150, - "front": 300 - }, - "entities": [ - { - "type": "Quad", - "name": "Table_1", - "x": -22.401046752929688, - "y": 102.5, - "color": "#888888", - "layer": 0, - "parallax": 1, - "ignoreScaling": false, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, - "vertices": [ - { - "x": 12.319771405705986, - "y": 3.4308825168171477, - "p": 0.6666666666666669, - "binding": { - "targetName": "Quad_460", - "type": "grid", - "gridU": 0.3333333333333333, - "gridV": 0.16666666666666666 - } - }, - { - "x": 145.0142158501505, - "y": 3.4308825168171495, - "p": 0.6666666666666667, - "binding": { - "targetName": "Quad_460", - "type": "grid", - "gridU": 0.5, - "gridV": 0.16666666666666666 - } - }, - { - "x": 147.8475491834838, - "y": 65.0975491834838, - "p": 0.8, - "binding": { - "targetName": "Quad_460", - "type": "grid", - "gridU": 0.5, - "gridV": 0.5 - } - }, - { - "x": -9.569117483182907, - "y": 65.09754918348382, - "p": 0.7999999999999999, - "binding": { - "targetName": "Quad_460", - "type": "grid", - "gridU": 0.3333333333333333, - "gridV": 0.5 - } - } - ], - "sortMode": "v3", - "isGrid": false, - "gridLinesX": 5, - "gridLinesY": 5, - "lineWidth": 1, - "gridColor": "#ffffff", - "filled": true - }, - { - "type": "Actor", - "name": "miles_ds", - "x": -44.31537914386294, - "y": 27.151810240650562, - "width": 35.04, - "height": 143.07999999999998, - "baseWidth": 96, - "baseHeight": 392, - "colliderWidth": 0, - "colliderHeight": 0, - "spriteName": "miles_ds-idle-right.json", - "color": "#00ffff", - "scale": 0.365, - "modelScale": 0.73, - "layer": 0, - "parallax": 1, - "ignoreScaling": false, - "animationSpeed": 30, - "locked": false, - "disabled": false, - "customName": "", - "groupID": null, - "components": [], - "interactions": {}, - "opacity": 1, - "blendMode": "source-over", - "blur": 0, - "isPlayer": true, - "speed": 0.24, - "direction": "right", - "animSets": { - "idle": { - "id": "idle", - "up": "miles_ds-idle-up.json", - "down": "miles_ds-idle-down.json", - "left": null, - "right": "miles_ds-idle-right.json" - }, - "walk": { - "id": "walk", - "up": "miles_ds-walk-up.json", - "down": "miles_ds-walk-down.json", - "left": null, - "right": "miles_ds-walk-right.json" - } - } - } - ], - "camera": { - "x": 0, - "y": 0, - "zoom": 1 - }, - "autoCenter": false, - "cameraSpeed": 5, - "camDeadzoneX": 50, - "camDeadzoneY": 30 -} diff --git a/public/text/objects/Chair.json b/public/text/objects/Chair.json new file mode 100644 index 0000000..00bfe18 --- /dev/null +++ b/public/text/objects/Chair.json @@ -0,0 +1,4 @@ +{ + "title": "Chair", + "description": "Just an old ordinary office chair." +} diff --git a/public/text/objects/CityView.json b/public/text/objects/CityView.json new file mode 100644 index 0000000..aa81c71 --- /dev/null +++ b/public/text/objects/CityView.json @@ -0,0 +1,6 @@ +{ + "title": "City view", + "description": "You look out over San Vesper as it comes awake by slow degrees. In the distance, the towers of Civic Crown catch the first pale gold of dawn, their edges softened by bay fog and the city's lingering smog. \rFrom here, the skyline feels strangely remote: clean lines, cold light, a promise that belongs to someone else. Morning spills through the bare window glass without obstruction, and for a moment the city looks almost gentle, though never quite harmless.", + "details": "", + "synonyms": ["view", "landscape", "city"] +} diff --git a/public/text/objects/Desk.json b/public/text/objects/Desk.json new file mode 100644 index 0000000..807d07c --- /dev/null +++ b/public/text/objects/Desk.json @@ -0,0 +1,6 @@ +{ + "title": "Desk", + "description": "A large desk where the computer and everything connected to it reigns supreme. It is crowded in the way only a real working desk can be. The desk has three drawers.", + "details": "", + "synonyms": ["table", "workplace"] +} diff --git a/public/text/objects/DeskDrawer-1.json b/public/text/objects/DeskDrawer-1.json new file mode 100644 index 0000000..b559281 --- /dev/null +++ b/public/text/objects/DeskDrawer-1.json @@ -0,0 +1,6 @@ +{ + "title": "upper drawer", + "description": "Yoy see upper drawer of the desk", + "details": "", + "synonyms": ["drawer 1", "first drawer"] +} diff --git a/public/text/objects/Drawer1.json b/public/text/objects/Drawer1.json new file mode 100644 index 0000000..31bfa38 --- /dev/null +++ b/public/text/objects/Drawer1.json @@ -0,0 +1,6 @@ +{ + "title": "upper drawer", + "description": "", + "details": "", + "synonyms": [] +} diff --git a/public/text/objects/Static_723.json b/public/text/objects/Static_723.json new file mode 100644 index 0000000..41cf279 --- /dev/null +++ b/public/text/objects/Static_723.json @@ -0,0 +1,4 @@ +{ + "title": "Static_723", + "description": "You see nothing special." +} diff --git a/public/text/objects/Trig_sub_D.json b/public/text/objects/Trig_sub_D.json new file mode 100644 index 0000000..59c2c02 --- /dev/null +++ b/public/text/objects/Trig_sub_D.json @@ -0,0 +1,6 @@ +{ + "title": "Desk drawers", + "description": "The desk have 3 drawers.", + "details": "", + "synonyms": [] +} diff --git a/public/text/objects/boombox.json b/public/text/objects/boombox.json new file mode 100644 index 0000000..9adb742 --- /dev/null +++ b/public/text/objects/boombox.json @@ -0,0 +1,6 @@ +{ + "title": "Boombox", + "description": "The Sharp - small but toothy radio and cassette recorder, connected to the computer.", + "details": "The Sharp GF-7 boombox is connected to the computer using regular audio cables. You used to use it to store programs on cassette tapes, but now you have a floppy disk drive for that. And yet you store your software archives on tapes. Some Commodore programs can also output sound to it. And with the ability to record songs from the radio or capture live audio using the external microphones, the possibilities are endless. Everything works fine, but the magnetic head needs to be adjusted frequently with a screwdriver, and the cassette deck needs to be secured with duct tape.", + "synonyms": ["recorder", "radio", "tape recorder", "Sharp", "GF-7"] +} diff --git a/public/text/objects/key1.json b/public/text/objects/key1.json new file mode 100644 index 0000000..990ffce --- /dev/null +++ b/public/text/objects/key1.json @@ -0,0 +1,4 @@ +{ + "title": "golden key", + "description": "You see GODEN key" +} diff --git a/public/text/objects/key1_1.json b/public/text/objects/key1_1.json new file mode 100644 index 0000000..2d37a83 --- /dev/null +++ b/public/text/objects/key1_1.json @@ -0,0 +1,4 @@ +{ + "title": "silver key", + "description": "You see SILVER key" +} diff --git a/public/text/objects/key_g.json b/public/text/objects/key_g.json new file mode 100644 index 0000000..a15f1ab --- /dev/null +++ b/public/text/objects/key_g.json @@ -0,0 +1,4 @@ +{ + "title": "golden key", + "description": "You see GOLDEN key." +} diff --git a/public/text/objects/key_s.json b/public/text/objects/key_s.json new file mode 100644 index 0000000..c000852 --- /dev/null +++ b/public/text/objects/key_s.json @@ -0,0 +1,4 @@ +{ + "title": "silver key", + "description": "You see nothing special." +} diff --git a/public/text/objects/logo_1.json b/public/text/objects/logo_1.json new file mode 100644 index 0000000..cc3720e --- /dev/null +++ b/public/text/objects/logo_1.json @@ -0,0 +1,4 @@ +{ + "title": "logo", + "description": "You see Scanline Engine logo." +} diff --git a/public/text/objects/miles_id.json b/public/text/objects/miles_id.json new file mode 100644 index 0000000..442b305 --- /dev/null +++ b/public/text/objects/miles_id.json @@ -0,0 +1,4 @@ +{ + "title": "your ID card", + "description": "You see nothing special." +} diff --git a/public/text/objects/room.json b/public/text/objects/room.json new file mode 100644 index 0000000..da6fbc4 --- /dev/null +++ b/public/text/objects/room.json @@ -0,0 +1,6 @@ +{ + "title": "room", + "description": "You see nothing special.", + "details": "", + "synonyms": [] +} diff --git a/public/text/objects/test.json b/public/text/objects/test.json new file mode 100644 index 0000000..5d77306 --- /dev/null +++ b/public/text/objects/test.json @@ -0,0 +1,5 @@ +{ + "title": "Someone ID card", + "description": "You see someone's ID", + "synonyms": [] +} diff --git a/public/text/objects/window1.json b/public/text/objects/window1.json new file mode 100644 index 0000000..4f2f056 --- /dev/null +++ b/public/text/objects/window1.json @@ -0,0 +1,6 @@ +{ + "title": "Window", + "description": "The windows in this building are large, as was fashionable in the mid-century. They are rarely opened due to smog, noise, and the heavy steel frames that often jam.", + "details": "", + "synonyms": [] +} diff --git a/public/text/scenes/home/room.json b/public/text/scenes/home/room.json new file mode 100644 index 0000000..0343556 --- /dev/null +++ b/public/text/scenes/home/room.json @@ -0,0 +1,4 @@ +{ + "title": "Test Room", + "description": "You are in Test Room." +} diff --git a/public/text/scenes/home/room_backup.json b/public/text/scenes/home/room_backup.json new file mode 100644 index 0000000..0343556 --- /dev/null +++ b/public/text/scenes/home/room_backup.json @@ -0,0 +1,4 @@ +{ + "title": "Test Room", + "description": "You are in Test Room." +} diff --git a/public/text/scenes/logo.json b/public/text/scenes/logo.json new file mode 100644 index 0000000..faa8b8b --- /dev/null +++ b/public/text/scenes/logo.json @@ -0,0 +1,4 @@ +{ + "title": "New Scene", + "description": "You are in New Scene." +} diff --git a/public/text/scenes/new_scene.json b/public/text/scenes/new_scene.json new file mode 100644 index 0000000..4d88f2e --- /dev/null +++ b/public/text/scenes/new_scene.json @@ -0,0 +1,4 @@ +{ + "title": "Room A", + "description": "You are in Room A." +} diff --git a/public/text/scenes/quad4.json b/public/text/scenes/quad4.json new file mode 100644 index 0000000..fa67f05 --- /dev/null +++ b/public/text/scenes/quad4.json @@ -0,0 +1,4 @@ +{ + "title": "Test Room", + "description": "You are in Test Room." +} \ No newline at end of file diff --git a/public/text/scenes/test1.json b/public/text/scenes/test1.json new file mode 100644 index 0000000..faa8b8b --- /dev/null +++ b/public/text/scenes/test1.json @@ -0,0 +1,4 @@ +{ + "title": "New Scene", + "description": "You are in New Scene." +} diff --git a/public/text/scenes/test_room (10).json b/public/text/scenes/test_room (10).json new file mode 100644 index 0000000..fa67f05 --- /dev/null +++ b/public/text/scenes/test_room (10).json @@ -0,0 +1,4 @@ +{ + "title": "Test Room", + "description": "You are in Test Room." +} \ No newline at end of file diff --git a/public/text/scenes/test_room.json b/public/text/scenes/test_room.json new file mode 100644 index 0000000..8d9cc7f --- /dev/null +++ b/public/text/scenes/test_room.json @@ -0,0 +1,4 @@ +{ + "title": "Mile's Home", + "description": "You look toward the working corner of the apartment. The desk stands against the wall perpendicular to the window, with a shelf of books above it that makes the space feel close, personal, and lived in. It is cozy, but not tidy. There is the usual kind of programmer's clutter everywhere: cables coiled and tangled across the floor, Post-it notes and scraps of paper with old reminders, crumpled printouts of BASIC and assembler code, five-inch floppy disks, and the small debris of long hours spent thinking at a machine. Across the room, against the wall opposite the window, sits the sofa, set so you could watch the television or turn and look out at the city. The whole room feels warm, familiar, and comfortably disordered, like a place you genuinely likes to work in." +} diff --git a/public/text/system/commands/index.json b/public/text/system/commands/index.json new file mode 100644 index 0000000..486c353 --- /dev/null +++ b/public/text/system/commands/index.json @@ -0,0 +1,3 @@ +{ + "commands": ["teleport_with", "use_on"] +} diff --git a/public/text/system/commands/teleport_with.json b/public/text/system/commands/teleport_with.json new file mode 100644 index 0000000..2aa69a5 --- /dev/null +++ b/public/text/system/commands/teleport_with.json @@ -0,0 +1,31 @@ +{ + "id": "teleport_with", + "phrases": ["teleport with", "teleport"], + "arguments": [ + { + "name": "item", + "kind": "entity", + "required": true, + "scopes": ["held", "takable"], + "validation": { + "allowedTitles": ["your ID card"] + }, + "messages": { + "missing": "Teleport with what?", + "ambiguous": "Which item do you want to teleport with: {options}?", + "notFound": "You don't have anything like that.", + "noEffect": "That doesn't work." + } + } + ], + "plan": [ + { "type": "resolveArgumentEntity", "arg": "item", "saveAs": "teleport_item" }, + { "type": "ensureHeldEntity", "ref": "teleport_item", "noEffectMessageId": "no_effect" }, + { "type": "goToSceneById", "sceneId": "test1" }, + { "type": "removeInventoryEntity", "ref": "teleport_item" }, + { "type": "showText", "messageId": "success" } + ], + "messages": { + "success": "You vanish in a flash and arrive somewhere else." + } +} diff --git a/public/text/system/commands/use_on.json b/public/text/system/commands/use_on.json new file mode 100644 index 0000000..564fe18 --- /dev/null +++ b/public/text/system/commands/use_on.json @@ -0,0 +1,46 @@ +{ + "id": "use_on", + "phrases": ["use"], + "arguments": [ + { + "name": "item", + "kind": "entity", + "required": true, + "scopes": ["held", "reachable"], + "messages": { + "missing": "Use what on what?", + "ambiguous": "Which item do you mean: {options}?", + "notFound": "You don't see anything like that here.", + "noEffect": "That doesn't work." + } + }, + { + "name": "target", + "kind": "entity", + "required": true, + "scopes": ["held", "reachable"], + "separatorsBefore": ["on", "with"], + "messages": { + "missing": "Use it on what?", + "ambiguous": "Which target do you mean: {options}?", + "notFound": "You don't see anything like that here.", + "noEffect": "That doesn't work." + } + } + ], + "plan": [ + { "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" + } + } + ], + "messages": { + "no_effect_pair": "Using the {item} on the {target} does nothing." + } +} diff --git a/public/text/system/engine.json b/public/text/system/engine.json new file mode 100644 index 0000000..81d53ea --- /dev/null +++ b/public/text/system/engine.json @@ -0,0 +1,7 @@ +{ + "click_you_see": "You see {title}", + "too_far_generic": "You are too far away.", + "too_far_from_entity": "You are too far away from the {target}.", + "locked_needs": "Locked. Needs {item}", + "locked_generic": "Locked." +} diff --git a/public/text/system/parser-lexicon.json b/public/text/system/parser-lexicon.json new file mode 100644 index 0000000..609985a --- /dev/null +++ b/public/text/system/parser-lexicon.json @@ -0,0 +1,57 @@ +{ + "stage1Aliases": { + "look": ["look"], + "examine": ["examine", "inspect", "check"], + "take": ["take", "get", "pickup", "pick up"], + "goTo": ["go", "walk", "move"], + "showInventory": ["inventory", "inv"] + }, + "normalizationPrefixes": { + "look": ["look at", "look", "tell me about", "what is that", "what is", "describe"], + "examine": [ + "take a closer look at", + "look closely at", + "examine at", + "examine", + "inspect at", + "inspect", + "check at", + "check" + ], + "take": ["pick up", "take", "get", "grab"], + "goTo": [ + "go over to", + "walk over to", + "move over to", + "go to", + "walk to", + "move to", + "go", + "walk", + "move", + "head to", + "travel to", + "head", + "travel" + ], + "showInventory": [] + }, + "politePrefixes": [ + "please", + "could you", + "can you", + "would you", + "i want to", + "i would like to", + "i'd like to" + ], + "articles": ["the", "a", "an", "my"], + "lookSceneWords": ["around", "here", "scene"], + "relationMarkers": { + "on": ["on"], + "under": ["under", "beneath"], + "in": ["in", "inside", "into"], + "behind": ["behind"], + "near": ["near", "next to", "by"] + } +} diff --git a/public/text/system/parser-training.json b/public/text/system/parser-training.json new file mode 100644 index 0000000..f504a0c --- /dev/null +++ b/public/text/system/parser-training.json @@ -0,0 +1,87 @@ +{ + "look": [ + "look", + "look chair", + "look logo", + "look lamp", + "look key", + "look door", + "look at the lamp", + "look at the chair", + "look at the logo", + "tell me about the door", + "what is that lamp", + "what is the chair", + "what is the logo", + "describe the office door", + "look over the note" + ], + "examine": [ + "examine", + "examine chair", + "examine logo", + "inspect chair", + "inspect the logo", + "check the card", + "check chair", + "inspect the note", + "examine the lamp", + "inspect the desk", + "check the key", + "look closely at the logo", + "take a closer look at the chair" + ], + "take": [ + "take", + "take key", + "take card", + "take note", + "get key", + "pickup key", + "pick up key", + "take the key", + "pick up the key", + "grab the card", + "take the id card", + "pick up linda card", + "grab the note", + "i want to take the key", + "please pick up the card" + ], + "goTo": [ + "go", + "go office", + "go logo", + "walk office", + "walk logo", + "move office", + "move logo", + "go to the office", + "go to office", + "walk to the office", + "walk to the logo", + "move to the lamp", + "move to logo", + "head to the door", + "go over to the desk", + "go over to the office", + "go over to the logo", + "travel to the office", + "walk over to the card reader", + "move over to the console" + ], + "showInventory": [ + "inventory", + "inv", + "items", + "my items", + "show inventory", + "what do i have", + "what am i carrying", + "check my inventory", + "show me my inventory", + "list my items", + "what items do i have", + "open inventory" + ] +} diff --git a/public/text/system/parser.json b/public/text/system/parser.json new file mode 100644 index 0000000..0613008 --- /dev/null +++ b/public/text/system/parser.json @@ -0,0 +1,32 @@ +{ + "look_default_scene": "You are in {scene}.", + "look_scene_contents": "Here is {items}.", + "look_default_object": "You see nothing special about the {target}.", + "look_not_found": "You don't see any {target} here.", + "look_which_one": "Which one do you mean: {options}?", + "look_relation_prompt": "Look where?", + "examine_prompt": "Examine what?", + "examine_which_one": "Which one do you want to examine: {options}?", + "examine_relation_prompt": "Examine what area?", + "relation_empty": "You see nothing {relation} the {target}.", + "relation_contents": "{Relation} the {target} you see: {items}.", + "relation_location": "It is {relation} the {target}.", + "relation_not_supported": "You can't determine what is {relation} the {target} from here.", + "take_prompt": "Take what?", + "take_which_one": "Which item do you mean: {options}?", + "take_pickup_success": "You picked up the {item}.", + "take_cannot": "You cannot take that.", + "inventory_empty": "You are not carrying anything.", + "inventory_items": "You are carrying: {items}", + "go_to_prompt": "Where do you want to go?", + "go_to_which_one": "Where exactly do you want to go: {options}?", + "go_to_not_found": "You can't get to {target} from here.", + "go_to_success": "You go to {target}.", + "use_prompt": "Use what?", + "use_format_prompt": "Use what on what? (Format: USE ITEM ON TARGET)", + "use_missing_item": "You don't have the {item}.", + "use_no_effect_pair": "Using the {item} on the {target} does nothing.", + "use_no_effect_single": "You try to use the {target}, but nothing happens.", + "command_no_effect": "That doesn't work.", + "parse_unknown": "I don't understand." +} diff --git a/public/text/system/scripts.json b/public/text/system/scripts.json new file mode 100644 index 0000000..4fd9d57 --- /dev/null +++ b/public/text/system/scripts.json @@ -0,0 +1,6 @@ +{ + "pillar_key_inserted": "You insert the key into a hidden slot in the pillar.", + "pillar_compartment_opened": "Click! A secret compartment opens!", + "pillar_open_description": "The pillar is open, revealing a secret compartment.", + "test_audio_playing": "Playing test sound..." +} diff --git a/src/components/ConsoleOverlay.tsx b/src/components/ConsoleOverlay.tsx index 89ceb69..988bfab 100644 --- a/src/components/ConsoleOverlay.tsx +++ b/src/components/ConsoleOverlay.tsx @@ -135,28 +135,6 @@ const InputMirror: React.FC<{ game: Game }> = ({ game }) => { const input = game.getCommandInput(); if (!input) return; - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter') { - const command = input.value; - if (command.trim()) { - // Send to Game Console Processing - if (game.console) { - game.console.processCommand(command); - } else { - // Fallback purely for parser if console not active? - // Actually, if we are in ConsoleOverlay, we want Console logic. - // The original game parser logic might still listen to 'Enter' globally? - // Let's ensure we don't double submit. - // Game.ts -> onKeyDown usually handles parser. - // We might need to coordinate who consumes the input. - // For now, let's assume this is the Console input. - } - input.value = ''; - setVal(''); - } - } - }; - const update = () => { if (input && input.value !== val) { setVal(input.value); @@ -164,11 +142,9 @@ const InputMirror: React.FC<{ game: Game }> = ({ game }) => { requestAnimationFrame(update); }; - input.addEventListener('keydown', handleKeyDown); const rAF = requestAnimationFrame(update); return () => { - input.removeEventListener('keydown', handleKeyDown); cancelAnimationFrame(rAF); }; }, [game, val]); diff --git a/src/components/FileBrowser.tsx b/src/components/FileBrowser.tsx index d54627b..dd3157e 100644 --- a/src/components/FileBrowser.tsx +++ b/src/components/FileBrowser.tsx @@ -281,52 +281,12 @@ export const FileBrowser: React.FC = ({ }, [onCancel]); return ( -
-
-
-
+
+
+
+

{title || (mode === 'save' ? 'Save File' : 'Load File')}

- {currentPath} + {currentPath}
-
+
{isLoading &&
Loading...
} - {error &&
Error: {error}
} + {error &&
Error: {error}
} {!isLoading && !error && @@ -380,32 +327,20 @@ export const FileBrowser: React.FC = ({ if (item.isUp) handleUp(); else handleDoubleClick(item); }} - style={{ - padding: '5px', - cursor: 'pointer', - backgroundColor: isSelected ? 'var(--ui-selection-bg)' : 'transparent', - color: item.isDir - ? '#ffff00' - : isSelected - ? 'var(--ui-selection-text)' - : 'var(--ui-main-color)', - border: isSelected - ? '1px solid var(--ui-selection-bg)' - : '1px solid transparent', - }} + className={`file-browser-item ${item.isDir ? 'is-dir' : ''} ${isSelected ? 'is-selected' : ''}`} > {item.isDir ? '📁' : '📄'} {item.name}
); })} {!isLoading && displayItems.length === 0 && ( -
Directory is empty
+
Directory is empty
)}
-
- +
+ = ({ autoFocus />
-
- -
diff --git a/src/components/GameCanvas.tsx b/src/components/GameCanvas.tsx index 0263c7c..5ade7a6 100644 --- a/src/components/GameCanvas.tsx +++ b/src/components/GameCanvas.tsx @@ -8,15 +8,16 @@ interface GameCanvasProps { export const GameCanvas: React.FC = ({ onGameInit }) => { const canvasRef = useRef(null); const uiCanvasRef = useRef(null); + const editorOverlayCanvasRef = useRef(null); const gameRef = useRef(null); const containerRef = useRef(null); useEffect(() => { - if (canvasRef.current && uiCanvasRef.current && !gameRef.current) { + if (canvasRef.current && uiCanvasRef.current && editorOverlayCanvasRef.current && !gameRef.current) { // Initialize Game with BOTH canvases // canvasRef -> WebGL (CRT) // uiCanvasRef -> 2D (UI/Input) - const game = new Game(canvasRef.current, uiCanvasRef.current); + const game = new Game(canvasRef.current, uiCanvasRef.current, editorOverlayCanvasRef.current); gameRef.current = game; // Start Game Loop @@ -44,6 +45,11 @@ export const GameCanvas: React.FC = ({ onGameInit }) => { canvasRef.current.width = clientWidth * dpr; canvasRef.current.height = clientHeight * dpr; + if (editorOverlayCanvasRef.current) { + editorOverlayCanvasRef.current.width = clientWidth * dpr; + editorOverlayCanvasRef.current.height = clientHeight * dpr; + } + // Notify game of resize gameRef.current.resize(canvasRef.current.width, canvasRef.current.height); } @@ -127,6 +133,22 @@ export const GameCanvas: React.FC = ({ onGameInit }) => { pointerEvents: 'auto', }} /> + +
); }; diff --git a/src/components/UIOverlay.tsx b/src/components/UIOverlay.tsx index c0d8821..8627245 100644 --- a/src/components/UIOverlay.tsx +++ b/src/components/UIOverlay.tsx @@ -17,6 +17,13 @@ export const UIOverlay: React.FC = ({ game }) => { onConfirm: (f: string) => void; extension?: string; title?: string; + onCancel?: () => void; + } | null>(null); + const [choiceDialog, setChoiceDialog] = useState<{ + title: string; + message: string; + options: Array<{ id: string; label: string; variant?: 'primary' | 'danger' | 'neutral' }>; + onResolve: (choiceId: string | null) => void; } | null>(null); // Console History State @@ -35,8 +42,11 @@ export const UIOverlay: React.FC = ({ game }) => { game.onMessage = (text) => setMessage({ id: Date.now(), text }); // Bind File Browser Request - game.onRequestFileBrowser = (mode, dir, onConfirm, extension, title) => { - setFileBrowser({ open: true, mode, dir, onConfirm, extension, title }); + game.onRequestFileBrowser = (mode, dir, onConfirm, extension, title, onCancel) => { + setFileBrowser({ open: true, mode, dir, onConfirm, extension, title, onCancel }); + }; + game.onRequestChoiceDialog = (title, dialogMessage, options, onResolve) => { + setChoiceDialog({ title, message: dialogMessage, options, onResolve }); }; // Initialize UI bindings @@ -62,6 +72,21 @@ export const UIOverlay: React.FC = ({ game }) => { } }, [game]); + useEffect(() => { + if (!game) return; + if (editorEnabled || isConsoleOpen) return; + + const timer = window.setTimeout(() => { + const input = parserInputRef.current; + if (!input || input.disabled) return; + input.focus(); + const len = input.value.length; + input.setSelectionRange(len, len); + }, 0); + + return () => window.clearTimeout(timer); + }, [game, editorEnabled, isConsoleOpen]); + useEffect(() => { if (message) { const timer = setTimeout(() => { @@ -78,6 +103,36 @@ export const UIOverlay: React.FC = ({ game }) => { } }; + const handleBrowserCancel = () => { + if (fileBrowser?.onCancel) { + fileBrowser.onCancel(); + } + setFileBrowser(null); + }; + + const handleChoiceResolve = React.useCallback((choiceId: string | null) => { + setChoiceDialog((currentDialog) => { + if (currentDialog) { + currentDialog.onResolve(choiceId); + } + return null; + }); + }, []); + + useEffect(() => { + if (!choiceDialog) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + handleChoiceResolve(null); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [choiceDialog, handleChoiceResolve]); + return ( <>
@@ -112,14 +167,22 @@ export const UIOverlay: React.FC = ({ game }) => { // GDD: "Input command... displayed in buffer... then sent to parser" if (val && game) { - // 1. Log Command to Buffer - game.console.log(val, 'command'); + const firstWord = val.split(/\s+/)[0] || ''; + + if (firstWord.startsWith('#')) { + game.console.processCommand(val); + } else { + const preprocessed = game.console.preprocessGameplayInput(val); - // 2. Add to History - game.console.addHistory(val); + // 1. Log Command to Buffer + game.console.log(preprocessed, 'command'); - // 3. Send to Parser (Parser handles command casing, Console handles arguments) - game.parser.parse(val); + // 2. Add to History + game.console.addHistory(preprocessed); + + // 3. Send to gameplay parser + void game.parser.parse(preprocessed); + } e.currentTarget.value = ''; setHistoryIndex(-1); // Reset history index on submit @@ -197,6 +260,26 @@ export const UIOverlay: React.FC = ({ game }) => {
)} + {choiceDialog && ( +
+
+
{choiceDialog.title}
+
{choiceDialog.message}
+
+ {choiceDialog.options.map((option) => ( + + ))} +
+
+
+ )} + {/* Virtual Console Overlay (High Res, Open State) */} {game && } @@ -217,7 +300,7 @@ export const UIOverlay: React.FC = ({ game }) => { mode={fileBrowser.mode} directory={fileBrowser.dir} onConfirm={handleBrowserConfirm} - onCancel={() => setFileBrowser(null)} + onCancel={handleBrowserCancel} extension={fileBrowser.extension} title={fileBrowser.title} /> diff --git a/src/components/editor/EditorBottomMenu.tsx b/src/components/editor/EditorBottomMenu.tsx index 633ad1b..26f78bf 100644 --- a/src/components/editor/EditorBottomMenu.tsx +++ b/src/components/editor/EditorBottomMenu.tsx @@ -2,19 +2,110 @@ import React from 'react'; import { useGame } from '../../hooks/useGame'; import { useEditorStore } from '../../store/editorStore'; +type ModifierMode = 'base' | 'shift' | 'alt' | 'ctrl'; + +type MenuEntry = { + hotkey: string; + text: string; + action: string; + enabled?: boolean; +}; + export const EditorBottomMenu: React.FC = () => { const game = useGame(); - const { toggle, toggleSpriteEditor } = useEditorStore(); + const { + enabled: editorEnabled, + toggle, + toggleSpriteEditor, + selectedObjectId, + selectedObjectType, + selectedObjectKeys, + objectVersion, + } = useEditorStore(); const [fps, setFps] = React.useState(0); + const [sceneMem, setSceneMem] = React.useState(0); + const [sceneCount, setSceneCount] = React.useState(0); + const [modifierMode, setModifierMode] = React.useState('base'); React.useEffect(() => { const interval = setInterval(() => { setFps(game.fps); + const stats = game.sceneManager.getSceneCacheStats(); + setSceneMem(stats.estimatedMemory); + setSceneCount(stats.loadedScenes); }, 500); return () => clearInterval(interval); }, [game]); + React.useEffect(() => { + const getModifierMode = (e?: KeyboardEvent): ModifierMode => { + const altGraph = !!e?.getModifierState?.('AltGraph') || e?.key === 'AltGraph'; + const ctrl = !!e?.ctrlKey; + const alt = !!e?.altKey; + const shift = !!e?.shiftKey; + if (altGraph) return 'alt'; + if (ctrl && !alt) return 'ctrl'; + if (alt) return 'alt'; + if (shift) return 'shift'; + return 'base'; + }; + + const syncModifierMode = (e?: KeyboardEvent) => { + setModifierMode(getModifierMode(e)); + }; + + const isMenuModifierKey = (e: KeyboardEvent) => + e.key === 'Alt' || e.key === 'AltGraph' || e.code === 'AltLeft' || e.code === 'AltRight'; + + const handleKeyDown = (e: KeyboardEvent) => { + if (editorEnabled && isMenuModifierKey(e)) { + e.preventDefault(); + } + syncModifierMode(e); + }; + + const handleKeyUp = (e: KeyboardEvent) => { + if (editorEnabled && isMenuModifierKey(e)) { + e.preventDefault(); + } + syncModifierMode(e); + }; + + const handleBlur = () => setModifierMode('base'); + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + window.addEventListener('blur', handleBlur); + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('keyup', handleKeyUp); + window.removeEventListener('blur', handleBlur); + }; + }, [editorEnabled]); + + const selectedObject = game.editor?.selectedObject || null; + const hasObjectSelection = + (selectedObjectType === 'MULTI' && + Array.isArray(selectedObjectKeys) && + selectedObjectKeys.length > 0) || + (!!selectedObject && selectedObjectType !== 'SCENE' && selectedObjectType !== 'SETTINGS'); + const hasSingleSelectedObject = + !!selectedObject && selectedObjectType !== 'SCENE' && selectedObjectType !== 'SETTINGS'; + void selectedObjectId; + void objectVersion; + + const lockActionText = hasSingleSelectedObject + ? selectedObject.locked + ? 'Unlock' + : 'Lock' + : 'Lock'; + const disableActionText = hasSingleSelectedObject + ? selectedObject.disabled + ? 'Enable' + : 'Disable' + : 'Disable'; + const handleAction = (action: string) => { const editor = game.editor; @@ -25,9 +116,15 @@ export const EditorBottomMenu: React.FC = () => { case 'F2': editor.saveScene(false); break; - case 'ShiftF2': + case 'SaveAs': editor.saveScene(true); break; + case 'SaveSelectionSlot1': + editor.saveSelectionSlot(0); + break; + case 'SaveSelectionSlot2': + editor.saveSelectionSlot(1); + break; case 'F3': editor.promptLoadScene(); break; @@ -40,24 +137,101 @@ export const EditorBottomMenu: React.FC = () => { case 'F9': editor.selectObject('SETTINGS'); break; + case 'Undo': + editor.undo(); + break; + case 'Redo': + editor.redo(); + break; + case 'Duplicate': + editor.duplicateSelectedObject(); + break; + case 'Copy': + editor.copySelectedObjectToClipboard(); + break; + case 'Paste': + void editor.pasteObjectFromClipboard(); + break; + case 'SaveSelection': + editor.persistenceManager.saveObject(); + break; + case 'LoadSelection': + editor.persistenceManager.loadObject('cursor'); + break; + case 'ToggleLock': + if (editor.selectedObject) { + editor.selectedObject.locked = !editor.selectedObject.locked; + useEditorStore.getState().incrementObjectVersion(); + useEditorStore.getState().incrementHierarchyVersion(); + } + break; + case 'ToggleDisabled': + if (editor.selectedObject) { + editor.selectedObject.disabled = !editor.selectedObject.disabled; + useEditorStore.getState().incrementObjectVersion(); + useEditorStore.getState().incrementHierarchyVersion(); + } + break; } }; - const keys = [ - { label: 'F1 Game', action: 'F1' }, - { label: 'F2 Save', action: 'F2' }, - { label: 'F3 Load', action: 'F3' }, - { label: 'F4 New', action: 'F4' }, - { label: 'F5 Sprite', action: 'F5' }, - { label: 'F9 Settings', action: 'F9' }, + const baseKeys: MenuEntry[] = [ + { hotkey: 'F1', text: 'Game', action: 'F1' }, + { hotkey: 'F2', text: 'Save', action: 'F2' }, + { hotkey: 'F3', text: 'Load', action: 'F3' }, + { hotkey: 'F4', text: 'New', action: 'F4' }, + { hotkey: 'F5', text: 'Sprite', action: 'F5' }, + { hotkey: 'F9', text: 'Settings', action: 'F9' }, ]; + const shiftKeys: MenuEntry[] = [ + { hotkey: 'F2', text: 'Save As', action: 'SaveAs' }, + { hotkey: '1', text: 'Save Sel. 1', action: 'SaveSelectionSlot1', enabled: hasObjectSelection }, + { hotkey: '2', text: 'Save Sel. 2', action: 'SaveSelectionSlot2', enabled: hasObjectSelection }, + ]; + + const altKeys: MenuEntry[] = [ + { hotkey: 'F2', text: 'Save As', action: 'SaveAs' }, + { hotkey: 'L', text: lockActionText, action: 'ToggleLock', enabled: hasSingleSelectedObject }, + { + hotkey: 'D', + text: disableActionText, + action: 'ToggleDisabled', + enabled: hasSingleSelectedObject, + }, + ]; + + const ctrlKeys: MenuEntry[] = [ + { hotkey: 'Z', text: 'Undo', action: 'Undo' }, + { hotkey: 'Y', text: 'Redo', action: 'Redo' }, + { hotkey: 'D', text: 'Duplicate', action: 'Duplicate', enabled: hasObjectSelection }, + { hotkey: 'C', text: 'Copy', action: 'Copy', enabled: hasObjectSelection }, + { hotkey: 'V', text: 'Paste', action: 'Paste', enabled: true }, + { hotkey: 'S', text: 'Save Prefab', action: 'SaveSelection', enabled: hasObjectSelection }, + { hotkey: 'O', text: 'Load Prefab', action: 'LoadSelection', enabled: true }, + ]; + + const keys = + modifierMode === 'ctrl' + ? ctrlKeys + : modifierMode === 'alt' + ? altKeys + : modifierMode === 'shift' + ? shiftKeys + : baseKeys; + return (
+
{`MEM ${sceneMem} | ${sceneCount}`}
{keys.map((k) => ( - ))}
FPS: {fps}
diff --git a/src/components/editor/HierarchyPanel.tsx b/src/components/editor/HierarchyPanel.tsx index 574b2dd..7e9a2ff 100644 --- a/src/components/editor/HierarchyPanel.tsx +++ b/src/components/editor/HierarchyPanel.tsx @@ -46,6 +46,61 @@ export const HierarchyPanel: React.FC = () => { const filteredTriggers = [...(scene?.triggerboxes || [])].filter((item: any) => matchesFilter(item) ); + const filteredObjects = React.useMemo( + () => [...filteredEntities, ...filteredWalkboxes, ...filteredTriggers], + [filteredEntities, filteredWalkboxes, filteredTriggers] + ); + const filteredObjectOrder = React.useMemo( + () => new Map(filteredObjects.map((item: any, index: number) => [item.name, index])), + [filteredObjects] + ); + const hierarchicalObjects = React.useMemo(() => { + const objectByName = new Map(filteredObjects.map((item: any) => [item.name, item])); + const childrenByParent = new Map(); + const roots: any[] = []; + + const pushChild = (parentId: string, item: any) => { + const children = childrenByParent.get(parentId) || []; + children.push(item); + childrenByParent.set(parentId, children); + }; + + filteredObjects.forEach((item: any) => { + const parentId = + typeof item?.spatial?.parentNodeId === 'string' ? item.spatial.parentNodeId.trim() : ''; + if (parentId && parentId !== item.name && objectByName.has(parentId)) { + pushChild(parentId, item); + } else { + roots.push(item); + } + }); + + const sortBySceneOrder = (items: any[]) => + [...items].sort( + (left, right) => + (filteredObjectOrder.get(left.name) ?? Number.MAX_SAFE_INTEGER) - + (filteredObjectOrder.get(right.name) ?? Number.MAX_SAFE_INTEGER) + ); + + const ordered: Array<{ item: any; depth: number }> = []; + const visited = new Set(); + + const walk = (item: any, depth: number) => { + if (!item || visited.has(item.name)) return; + visited.add(item.name); + ordered.push({ item, depth }); + const children = sortBySceneOrder(childrenByParent.get(item.name) || []); + children.forEach((child) => walk(child, depth + 1)); + }; + + sortBySceneOrder(roots).forEach((item) => walk(item, 0)); + + sortBySceneOrder(filteredObjects) + .filter((item) => !visited.has(item.name)) + .forEach((item) => walk(item, 0)); + + return ordered; + }, [filteredObjects, filteredObjectOrder]); // Helper to resolve display ID for an item, matching how it's identified in the UI const getDisplayId = (item: any): string => { @@ -84,7 +139,10 @@ export const HierarchyPanel: React.FC = () => { return; } - const allItems = ['SCENE', ...filteredEntities, ...filteredWalkboxes, ...filteredTriggers]; + const allItems = [ + 'SCENE', + ...hierarchicalObjects.map((entry) => entry.item), + ]; if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { e.preventDefault(); @@ -126,9 +184,7 @@ export const HierarchyPanel: React.FC = () => { window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [ - filteredEntities, - filteredWalkboxes, - filteredTriggers, + hierarchicalObjects, hierarchyVersion, selectedObjectId, game.editor, @@ -302,15 +358,30 @@ export const HierarchyPanel: React.FC = () => { Scene
- {/* Entities */} - {filteredEntities.map((ent: any) => { - const isSelected = isItemSelected(ent); + {hierarchicalObjects.map(({ item, depth }, i) => { + const isSelected = isItemSelected(item); + const icon = + item.type === 'Actor' + ? '👤' + : item.type === 'Quad' + ? '▰' + : item.type === 'Walkbox' + ? '👣' + : item.type === 'Triggerbox' + ? '⚡' + : '📦'; + const label = + item.type === 'Walkbox' + ? item.name || `Walkbox ${i}` + : item.type === 'Triggerbox' + ? item.name || `Trigger ${i}` + : item.name; return (
{ justifyContent: 'space-between', }} onClick={(e) => { - if (e.ctrlKey) game.editor.toggleObjectSelection(ent); - else game.editor.selectObject(ent); + if (e.ctrlKey) game.editor.toggleObjectSelection(item); + else game.editor.selectObject(item); }} - onDoubleClick={() => centerCameraOn(ent)} + onDoubleClick={() => centerCameraOn(item)} >
- - {ent.type === 'Actor' ? '👤' : ent.type === 'Quad' ? '▰' : '📦'} - - {ent.name} -
- {ent.locked && 🔒} -
- ); - })} - - {/* Walkboxes */} - {filteredWalkboxes.map((wb: any, i: number) => { - const isSelected = isItemSelected(wb); - return ( -
{ - if (e.ctrlKey) game.editor.toggleObjectSelection(wb); - else game.editor.selectObject(wb); - }} - onDoubleClick={() => centerCameraOn(wb)} - > -
- - 👣 - - {wb.name || `Walkbox ${i}`} -
- {wb.locked && 🔒} -
- ); - })} - - {/* Triggers */} - {filteredTriggers.map((tb: any, i: number) => { - const isSelected = isItemSelected(tb); - return ( -
{ - if (e.ctrlKey) game.editor.toggleObjectSelection(tb); - else game.editor.selectObject(tb); - }} - onDoubleClick={() => centerCameraOn(tb)} - > -
{ : 'grayscale(100%) sepia(100%) hue-rotate(75deg) saturate(400%)', marginRight: '6px', display: 'inline-block', - textDecoration: tb.disabled ? 'line-through' : 'none', + textDecoration: item.disabled ? 'line-through' : 'none', }} > - ⚡ + {icon} - {tb.name || `Trigger ${i}`} + {label}
- {tb.locked && 🔒} + {item.locked && 🔒}
); })} diff --git a/src/components/editor/PropertiesPanel.tsx b/src/components/editor/PropertiesPanel.tsx index 7d0b6d2..62c24bd 100644 --- a/src/components/editor/PropertiesPanel.tsx +++ b/src/components/editor/PropertiesPanel.tsx @@ -4,6 +4,140 @@ import { useGame } from '../../hooks/useGame'; import { Select } from '../../components/common/Select'; import { QuadObject } from '../../entities/QuadObject'; import { Entity } from '../../entities/Entity'; +import { Triggerbox } from '../../entities/Triggerbox'; + +const PROPERTIES_LABEL_TOOLTIPS: Record = { + 'Group #ID': + 'Comma-separated tags used to address this object or selection from triggers, switches, scripts, and subscenes.', + Parent: + 'Chooses the direct spatial parent of this object. The object will be treated as attached to that parent instead of the root scene.', + Relation: + 'Defines how this object is attached to its parent in the spatial hierarchy: in, on, under, or behind.', + 'Group X': + 'Moves the whole selected group horizontally while preserving the relative layout between the selected objects.', + 'Group Y': + 'Moves the whole selected group vertically while preserving the relative layout between the selected objects.', + 'Group Scale': + 'Scales the whole selected group around its shared center while keeping the objects aligned with each other.', + X: 'Horizontal position in scene space.', + Y: 'Vertical position in scene space.', + H: 'Visible height of the object rectangle.', + W: 'Visible width of the object rectangle.', + Scale: + 'Overall size multiplier. For polygon objects it scales the current shape around its center; for sprite objects it changes their model scale.', + Layer: + 'Render and interaction layer. Higher layers are treated as being in front of lower ones.', + Parallax: + 'Camera parallax factor. Values around 1 move with the scene, while lower or higher values create foreground or background depth drift.', + 'Collider H': + 'Collision height used for walkbox and obstacle interaction. Set to 0 to make the object non-blocking.', + 'Collider W': + 'Collision width used for walkbox and obstacle interaction. Set to 0 to make the object non-blocking.', + 'Disable Depth-scaling': + 'Keeps the object at a fixed visual size instead of letting the scene depth-scaling system resize it by Y position.', + 'Fill Color': + 'Base fill color for the object when no sprite is used, or the tint/fill color used by this visual mode.', + 'Blend Mode': + 'Canvas blend mode used to combine this object with the scene behind it.', + Opacity: + 'Visual transparency. 0% keeps the object fully opaque; 100% makes it invisible and excluded from rendering.', + Blur: 'Blur radius in pixels. 0 px is sharp; higher values make the object softer.', + Sprite: + 'Sprite asset used to render this object. Leave empty to keep the plain filled rectangle look.', + Mode: + 'Selects the behavior mode for this object or component.', + 'Depth Sort mode': + 'Chooses which quad rule is used for Y sorting, or disables Y sorting so layer order stays fully manual.', + 'Grid X': + 'Number of vertical subdivisions in the retro grid effect.', + 'Grid Y': + 'Number of horizontal subdivisions in the retro grid effect.', + Width: 'Line width or stroke width used by the current visual effect.', + 'Grid Color': 'Color used to draw the retro grid lines.', + ID: 'Unique identifier used by the engine, scripts, references, and file operations.', + 'ID/File': + 'Unique scene identifier and file path key. Slashes create subfolders when the scene is saved.', + Title: + 'Text-asset title shown to the player and used by the text layer as the friendly name for this object or scene.', + 'Key Item ID': + 'Inventory item ID required to unlock or activate this interaction.', + Description: 'Player-facing short description used by text interactions and subscene presentation.', + 'Target ID(s)': + 'One or more target group IDs or object IDs affected by this component or interaction.', + 'Target ID(s) (Optional)': + 'Optional target IDs affected by this component. Leave empty when the component should only provide auxiliary behavior.', + 'Target Trigger (Name/ID)': + 'Name or ID of the triggerbox that this helper area should activate as if it were clicked directly.', + 'Target(s) 1': + 'Targets used when the switch is in state 1, usually the closed or default state.', + 'Target(s) 2': + 'Targets used when the switch is in state 2, usually the open or alternate state.', + 'Sound 1': 'Sound played when the switch moves into state 1.', + 'Sound 2': 'Sound played when the switch moves into state 2.', + State: 'Current switch state used as the starting state in the editor and at runtime.', + 'Shadow Quad ID': + 'Quad that receives or shapes this shadow effect.', + 'Offset X': 'Horizontal offset applied by the component or effect.', + 'Offset Y': 'Vertical offset applied by the component or effect.', + 'Trigger ID(s) (Zone)': + 'Trigger IDs that enable, disable, or otherwise gate this component in specific zones.', + Axis: 'Axis constrained by the component or comparison rule.', + Op: 'Comparison operator used by the current component or condition.', + 'Culling Type': + 'Chooses how the object is culled or hidden when it falls outside the active visibility rule.', + 'Vert A (0-3)': 'First quad vertex index used by this link or rule.', + 'Vert B (0-3)': 'Second quad vertex index used by this link or rule.', + Direction: 'Default facing direction for the actor.', + 'Move Speed': 'Actor movement speed in scene units per step.', + 'Anim Speed (ms)': 'Frame duration for sprite animation playback, in milliseconds.', + 'Cam X': 'Current camera X position in scene space.', + 'Cam Y': 'Current camera Y position in scene space.', + Zoom: 'Current scene camera zoom.', + 'Auto-Center on Player': + 'Automatically keeps the camera centered on the player instead of relying on manual camera values.', + 'Cam Spd': 'Camera follow speed when auto-centering or camera tracking is active.', + 'Dead X': 'Horizontal deadzone before camera follow begins.', + 'Dead Y': 'Vertical deadzone before camera follow begins.', + 'Min X': 'Minimum allowed X value for this camera range.', + 'Max X': 'Maximum allowed X value for this camera range.', + 'Min Y': 'Minimum allowed Y value for this camera range.', + 'Max Y': 'Maximum allowed Y value for this camera range.', + 'Def X': 'Default camera X used when the scene loads or resets.', + 'Def Y': 'Default camera Y used when the scene loads or resets.', + 'Def Zoom': 'Default camera zoom used when the scene loads or resets.', + 'Enable Depth Scaling': + 'Turns scene depth scaling on or off so objects can grow or shrink according to their Y position.', + Min: 'Minimum depth-scaling factor used at the horizon end of the scene.', + 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.', + 'UI Scale': 'Editor interface scale multiplier.', + Curvature: 'Strength of the CRT screen curvature effect.', + Vignette: 'Darkening applied toward the screen edges.', + 'Scanline Count': 'Number of scanlines used by the CRT filter.', + 'Scanline Intensity': 'Visibility strength of the CRT scanlines.', + 'RGB Split': 'Amount of RGB channel separation in the CRT effect.', + Bloom: 'Glow intensity added by the CRT effect.', + 'Phosphor / Grain': 'Amount of phosphor persistence and grain noise.', + 'Enable CRT Filter': 'Turns the CRT post-processing effect on or off.', + 'Bezel Glow': 'Adds a glow around the virtual CRT bezel.', + 'Lock Object': 'Prevents accidental editing of this object in the editor. Hotkey: Alt-L.', + Disabled: 'Disables the object so it does not participate in the scene. Hotkey: Alt-D.', + 'Retro Grid': + 'Enables the retro grid line overlay for this quad. It is also useful for alignment, because objects can snap to grid nodes while Alt is held.', +}; + +const normalizeTooltipLabelText = (rawText: string): string => { + const text = rawText.replace(/\s+/g, ' ').trim(); + if (!text) return ''; + if (text.startsWith('Opacity')) return 'Opacity'; + if (text.startsWith('Blur')) return 'Blur'; + if (text.startsWith('UI Scale')) return 'UI Scale'; + if (text.startsWith('State')) return 'State'; + if (text.startsWith('Mode:')) return 'Mode'; + if (text === 'Disable Depth Scaling') return 'Disable Depth-scaling'; + return text; +}; export const PropertiesPanel: React.FC = () => { const game = useGame(); @@ -12,14 +146,31 @@ export const PropertiesPanel: React.FC = () => { selectedObjectType, incrementHierarchyVersion, incrementObjectVersion, + objectVersion, mode, selectedVertexIndex, } = useEditorStore(); const [groupIdDraft, setGroupIdDraft] = React.useState(''); + const [multiSpatialParentDraft, setMultiSpatialParentDraft] = React.useState(''); + const [multiSpatialRelationDraft, setMultiSpatialRelationDraft] = React.useState(''); + const [resolvedTitle, setResolvedTitle] = React.useState(''); + const [textAssetPath, setTextAssetPath] = React.useState(''); + const [isReadingTA, setIsReadingTA] = React.useState(false); + const [hasTextAsset, setHasTextAsset] = React.useState(false); + const [polygonScaleDraft, setPolygonScaleDraft] = React.useState('1'); + const lastUndoObjectKeyRef = React.useRef(null); + const lastUndoMultiKeyRef = React.useRef(null); + const lastPolygonScaleObjectKeyRef = React.useRef(null); + const polygonScaleSnapshotRef = React.useRef(null); + const panelRef = React.useRef(null); + const contentRef = React.useRef(null); + const sectionRefs = React.useRef>({}); + const isPanelHoveredRef = React.useRef(false); // Derived Object Binding (Source of Truth) - // We re-render whenever objectVersion changes (subscribed via store hook) + // We re-render whenever objectVersion changes. let obj: any = null; + void objectVersion; if (game) { if (selectedObjectId === 'SETTINGS') { obj = game.settings; @@ -31,9 +182,97 @@ export const PropertiesPanel: React.FC = () => { } const uiScale = game?.settings?.editor?.uiScale || 1.0; + const supportsTextAsset = + selectedObjectType === 'SCENE' || + (selectedObjectType !== 'MULTI' && + selectedObjectType !== 'SETTINGS' && + game?.editor?.selectedObject?.type !== 'Walkbox'); const multiObjects = game?.editor?.selectionManager?.hasMultiSelection() ? game.editor.selectionManager.getSelectedObjects() : []; + const spatialRelationOptions = [ + { value: '', label: '(None)' }, + { value: 'in', label: 'In' }, + { value: 'on', label: 'On' }, + { value: 'under', label: 'Under' }, + { value: 'behind', label: 'Behind' }, + ]; + const getSpatialRelationOptions = React.useCallback( + (hasParent: boolean) => + hasParent ? spatialRelationOptions.filter((option) => option.value !== '') : spatialRelationOptions, + [spatialRelationOptions] + ); + + const getSpatialDescendantNames = React.useCallback((rootNames: string[]) => { + const scene = game?.sceneManager?.currentScene; + if (!scene || !rootNames.length) return new Set(); + + const allObjects = [...scene.entities, ...scene.walkbox, ...scene.triggerboxes]; + const childrenByParent = new Map(); + + allObjects.forEach((item: any) => { + const parentId = typeof item?.spatial?.parentNodeId === 'string' ? item.spatial.parentNodeId.trim() : ''; + const name = typeof item?.name === 'string' ? item.name.trim() : ''; + if (!parentId || !name) return; + const children = childrenByParent.get(parentId) || []; + children.push(name); + childrenByParent.set(parentId, children); + }); + + const visited = new Set(); + const stack = [...rootNames.filter(Boolean)]; + while (stack.length) { + const current = stack.pop()!; + const children = childrenByParent.get(current) || []; + children.forEach((child) => { + if (visited.has(child)) return; + visited.add(child); + stack.push(child); + }); + } + + return visited; + }, [game]); + + const getSceneSpatialParentOptions = React.useCallback(() => { + const scene = game?.sceneManager?.currentScene; + if (!scene || !obj) { + return [{ value: '', label: '(None)' }]; + } + + const excludedNames = new Set([obj?.name].filter(Boolean)); + getSpatialDescendantNames([obj?.name]).forEach((name) => excludedNames.add(name)); + + const allObjects = [...scene.entities, ...scene.walkbox, ...scene.triggerboxes]; + const options = allObjects + .filter((item: any) => item && !excludedNames.has(item.name)) + .map((item: any) => ({ + value: item.name, + label: item.customName?.trim() || item.name, + })); + + return [{ value: '', label: '(None)' }, ...options]; + }, [game, obj, getSpatialDescendantNames]); + + const getMultiSpatialParentOptions = React.useCallback(() => { + const scene = game?.sceneManager?.currentScene; + if (!scene || !multiObjects.length) { + return [{ value: '', label: '(None)' }]; + } + + const selectedNames = new Set(multiObjects.map((item: any) => item?.name).filter(Boolean)); + getSpatialDescendantNames(Array.from(selectedNames)).forEach((name) => selectedNames.add(name)); + + const allObjects = [...scene.entities, ...scene.walkbox, ...scene.triggerboxes]; + const options = allObjects + .filter((item: any) => item && !selectedNames.has(item.name)) + .map((item: any) => ({ + value: item.name, + label: item.customName?.trim() || item.name, + })); + + return [{ value: '', label: '(None)' }, ...options]; + }, [game, multiObjects, getSpatialDescendantNames]); const getSharedValue = (arr: any[], getter: (o: any) => any) => { if (!arr.length) return ''; @@ -53,13 +292,466 @@ export const PropertiesPanel: React.FC = () => { return first ? 'on' : 'off'; }; + const formatPanelNumber = React.useCallback((value: any): number | string => { + if (value === '' || value === null || value === undefined) return ''; + const n = Number(value); + if (!Number.isFinite(n)) return value; + return Number(n.toFixed(3)); + }, []); + + const setSectionRef = React.useCallback( + (section: number) => (node: HTMLDivElement | null) => { + sectionRefs.current[section] = node; + }, + [] + ); + + const scrollToSection = React.useCallback((section: number) => { + const container = contentRef.current; + const node = sectionRefs.current[section]; + if (!container || !node) return; + const containerRect = container.getBoundingClientRect(); + const nodeRect = node.getBoundingClientRect(); + const targetTop = container.scrollTop + (nodeRect.top - containerRect.top) - 8; + container.scrollTo({ + top: Math.max(0, targetTop), + behavior: 'smooth', + }); + }, []); + + const isPanelTextEntryFocused = React.useCallback(() => { + const active = document.activeElement as HTMLElement | null; + if (!active || !panelRef.current || !panelRef.current.contains(active)) return false; + if (active.matches('input, textarea, select, [contenteditable="true"]')) return true; + if (active.closest('.custom-select-container')) return true; + return false; + }, []); + + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (!isPanelHoveredRef.current) return; + if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return; + if (isPanelTextEntryFocused()) return; + + const key = e.key; + if (!/^[0-6]$/.test(key)) return; + + e.preventDefault(); + scrollToSection(parseInt(key, 10)); + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isPanelTextEntryFocused, scrollToSection]); + + React.useEffect(() => { + const panel = panelRef.current; + if (!panel) return; + + const labels = panel.querySelectorAll('label.e-label'); + labels.forEach((node) => { + const label = node as HTMLLabelElement; + if (label.dataset.tooltipFixed === 'true') { + label.classList.add('e-tooltip-label'); + return; + } + + const normalized = normalizeTooltipLabelText(label.textContent || ''); + const tooltip = PROPERTIES_LABEL_TOOLTIPS[normalized]; + if (!tooltip) { + label.removeAttribute('title'); + label.classList.remove('e-tooltip-label'); + return; + } + + label.title = tooltip; + label.classList.add('e-tooltip-label'); + }); + }); + + const getPolyCentroid = React.useCallback((poly: { x: number; y: number }[] = []) => { + if (!poly.length) return { x: 0, y: 0 }; + const sum = poly.reduce((acc, pt) => ({ x: acc.x + pt.x, y: acc.y + pt.y }), { x: 0, y: 0 }); + return { x: sum.x / poly.length, y: sum.y / poly.length }; + }, []); + + const translatePolyTo = React.useCallback( + (targetX: number, targetY: number) => { + if (!obj?.poly?.length) return; + const centroid = getPolyCentroid(obj.poly); + const dx = targetX - centroid.x; + const dy = targetY - centroid.y; + obj.poly = obj.poly.map((pt: any) => ({ + x: Math.round(pt.x + dx), + y: Math.round(pt.y + dy), + })); + incrementObjectVersion(); + }, + [getPolyCentroid, incrementObjectVersion, obj] + ); + + const getQuadCentroid = React.useCallback((quad: any) => { + const verts = quad?.vertices || []; + if (!verts.length) return { x: quad?.x || 0, y: quad?.y || 0 }; + const sum = verts.reduce((acc: any, v: any) => ({ x: acc.x + v.x, y: acc.y + v.y }), { x: 0, y: 0 }); + return { x: sum.x / verts.length, y: sum.y / verts.length }; + }, []); + + const translateQuadTo = React.useCallback( + (targetX: number, targetY: number) => { + if (!obj?.vertices?.length) return; + const centroid = getQuadCentroid(obj); + const dx = targetX - centroid.x; + const dy = targetY - centroid.y; + obj.vertices = obj.vertices.map((v: any) => ({ + ...v, + x: v.x + dx, + y: v.y + dy, + })); + obj.x = targetX; + obj.y = targetY; + incrementObjectVersion(); + }, + [getQuadCentroid, incrementObjectVersion, obj] + ); + + const scalePolyByFactor = React.useCallback( + (poly: { x: number; y: number }[], factor: number, originX: number, originY: number) => + poly.map((pt) => ({ + x: Math.round(originX + (pt.x - originX) * factor), + y: Math.round(originY + (pt.y - originY) * factor), + })), + [] + ); + + const scaleQuadVerticesByFactor = React.useCallback( + (vertices: any[], factor: number, originX: number, originY: number) => + vertices.map((v) => ({ + ...v, + x: originX + (v.x - originX) * factor, + y: originY + (v.y - originY) * factor, + })), + [] + ); + + const applyPolygonScaleDraft = React.useCallback( + (nextScaleRaw: string) => { + if (!obj || !(selectedObjectType === 'Triggerbox' || selectedObjectType === 'Quad')) return; + const nextScale = parseFloat(nextScaleRaw); + if (!Number.isFinite(nextScale) || nextScale <= 0) return; + + const objectKey = `${selectedObjectType || 'Object'}:${obj.name || ''}`; + if (lastPolygonScaleObjectKeyRef.current !== objectKey) { + game?.editor?.saveUndoState(); + if (selectedObjectType === 'Quad' && obj.vertices?.length) { + polygonScaleSnapshotRef.current = { + key: objectKey, + kind: 'quad', + vertices: obj.vertices.map((v: any) => ({ ...v })), + }; + } else if (obj.poly?.length) { + polygonScaleSnapshotRef.current = { + key: objectKey, + kind: 'poly', + poly: obj.poly.map((pt: any) => ({ x: pt.x, y: pt.y })), + }; + } else { + polygonScaleSnapshotRef.current = null; + } + } + + const snapshot = polygonScaleSnapshotRef.current; + if (!snapshot || snapshot.key !== objectKey) { + setPolygonScaleDraft(nextScaleRaw); + lastPolygonScaleObjectKeyRef.current = objectKey; + return; + } + + if (snapshot.kind === 'quad' && selectedObjectType === 'Quad' && snapshot.vertices?.length) { + const sourceVertices = snapshot.vertices.map((v: any) => ({ ...v })); + const sourceCentroid = { + x: sourceVertices.reduce((acc: number, v: any) => acc + v.x, 0) / sourceVertices.length, + y: sourceVertices.reduce((acc: number, v: any) => acc + v.y, 0) / sourceVertices.length, + }; + obj.vertices = scaleQuadVerticesByFactor( + sourceVertices, + nextScale, + sourceCentroid.x, + sourceCentroid.y + ); + obj.x = Math.round( + obj.vertices.reduce((acc: number, v: any) => acc + v.x, 0) / obj.vertices.length + ); + obj.y = Math.round( + obj.vertices.reduce((acc: number, v: any) => acc + v.y, 0) / obj.vertices.length + ); + } else if (snapshot.kind === 'poly' && snapshot.poly?.length) { + const sourcePoly = snapshot.poly.map((pt: any) => ({ x: pt.x, y: pt.y })); + const sourceCentroid = getPolyCentroid(sourcePoly); + obj.poly = scalePolyByFactor(sourcePoly, nextScale, sourceCentroid.x, sourceCentroid.y); + } + + setPolygonScaleDraft(nextScaleRaw); + lastPolygonScaleObjectKeyRef.current = objectKey; + incrementObjectVersion(); + }, + [ + game, + getPolyCentroid, + getQuadCentroid, + incrementObjectVersion, + obj, + scalePolyByFactor, + scaleQuadVerticesByFactor, + selectedObjectType, + ] + ); + const applyToMulti = (fn: (o: any) => void) => { + const multiKey = `MULTI:${multiObjects.map((item: any) => item?.name || '').filter(Boolean).join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } multiObjects.forEach(fn); incrementObjectVersion(); incrementHierarchyVersion(); }; + const applyToMultiRoots = (fn: (o: any) => void) => { + const multiKey = `MULTI:${multiObjects.map((item: any) => item?.name || '').filter(Boolean).join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } + const selectedNames = new Set(multiObjects.map((item: any) => item?.name).filter(Boolean)); + multiObjects.forEach((o: any) => { + const parentId = typeof o?.spatial?.parentNodeId === 'string' ? o.spatial.parentNodeId.trim() : ''; + if (parentId && selectedNames.has(parentId)) return; + fn(o); + }); + incrementObjectVersion(); + incrementHierarchyVersion(); + }; + + React.useEffect(() => { + if (selectedObjectType !== 'MULTI' || multiObjects.length <= 1) { + setMultiSpatialParentDraft(''); + setMultiSpatialRelationDraft(''); + return; + } + + const sharedParent = getSharedValue(multiObjects, (o: any) => o.spatial?.parentNodeId || ''); + const sharedRelation = getSharedValue( + multiObjects, + (o: any) => (o.spatial?.parentNodeId ? o.spatial?.relation || 'in' : o.spatial?.relation || '') + ); + + setMultiSpatialParentDraft(sharedParent === '' ? '' : sharedParent); + setMultiSpatialRelationDraft(sharedRelation === '' ? '' : sharedRelation); + }, [selectedObjectType, selectedObjectId, objectVersion, multiObjects.length]); + + React.useEffect(() => { + setPolygonScaleDraft('1'); + lastPolygonScaleObjectKeyRef.current = null; + polygonScaleSnapshotRef.current = null; + }, [selectedObjectType, selectedObjectId]); + + const loadResolvedTitle = React.useCallback( + async (forceReload: boolean = false) => { + if (!game || !obj || selectedObjectType === 'MULTI' || selectedObjectType === 'SETTINGS') { + setResolvedTitle(''); + setTextAssetPath(''); + setHasTextAsset(false); + return; + } + + if (!supportsTextAsset) { + setResolvedTitle(''); + setTextAssetPath(''); + setHasTextAsset(false); + return; + } + + if (selectedObjectType === 'SCENE') { + const scene = game.sceneManager.currentScene; + if (!scene) return; + const asset = forceReload + ? await game.textAssets.readSceneAsset(scene, true) + : await game.textAssets.readSceneAsset(scene, false); + setHasTextAsset(!!asset); + setResolvedTitle(game.textAssets.getResolvedSceneField(scene, 'title') || ''); + setTextAssetPath(game.textAssets.getSceneAssetProjectPath(scene.id)); + return; + } + + if (game.editor?.selectedObject) { + const selected = game.editor.selectedObject; + const asset = forceReload + ? await game.textAssets.readObjectAsset(selected, true) + : await game.textAssets.readObjectAsset(selected, false); + setHasTextAsset(!!asset); + setResolvedTitle(game.textAssets.getResolvedObjectField(selected, 'title') || ''); + setTextAssetPath(game.textAssets.getObjectAssetProjectPath(selected.name)); + } + }, + [game, obj, selectedObjectType, supportsTextAsset] + ); + + React.useEffect(() => { + loadResolvedTitle(false).catch((err) => { + console.error('Failed to load text asset title:', err); + }); + }, [loadResolvedTitle, selectedObjectId, selectedObjectType]); + + const handleOpenTA = async () => { + if (!game || !obj) return; + try { + if (selectedObjectType === 'SCENE') { + const scene = game.sceneManager.currentScene; + if (!scene) return; + await game.textAssets.openSceneAsset(scene); + } else if (game.editor?.selectedObject) { + await game.textAssets.openObjectAsset(game.editor.selectedObject); + } + await loadResolvedTitle(true); + } catch (err) { + console.error('Failed to open text asset:', err); + game.showNotification?.(`Failed to open TA: ${err}`); + } + }; + + const handleReadTA = async () => { + if (!game || !obj) return; + setIsReadingTA(true); + try { + const path = + selectedObjectType === 'SCENE' + ? game.textAssets.getSceneAssetProjectPath(game.sceneManager.currentScene?.id || '') + : game.editor?.selectedObject + ? game.textAssets.getObjectAssetProjectPath(game.editor.selectedObject.name) + : ''; + const defaultContent = + selectedObjectType === 'SCENE' + ? JSON.stringify( + game.textAssets.buildDefaultSceneAsset(game.sceneManager.currentScene as any), + null, + 2 + ) + : game.editor?.selectedObject + ? JSON.stringify( + game.textAssets.buildDefaultObjectAsset(game.editor.selectedObject), + null, + 2 + ) + : '{}'; + + await fetch('/api/read-file', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, content: defaultContent }), + }); + await loadResolvedTitle(true); + incrementObjectVersion(); + game.showNotification?.('Text asset reloaded'); + } catch (err) { + console.error('Failed to read text asset:', err); + game.showNotification?.(`Failed to read TA: ${err}`); + } finally { + setIsReadingTA(false); + } + }; + + const handleDeleteTA = async () => { + if (!game || !obj || !hasTextAsset) return; + const confirmed = window.confirm(`Delete text asset?\n${textAssetPath}`); + if (!confirmed) return; + + try { + if (selectedObjectType === 'SCENE') { + const scene = game.sceneManager.currentScene; + if (!scene) return; + await game.textAssets.deleteSceneAsset(scene); + } else if (game.editor?.selectedObject) { + await game.textAssets.deleteObjectAsset(game.editor.selectedObject); + } + await loadResolvedTitle(true); + incrementObjectVersion(); + game.showNotification?.('Text asset deleted'); + } catch (err) { + console.error('Failed to delete text asset:', err); + game.showNotification?.(`Failed to delete TA: ${err}`); + } + }; + + const renderSection = ( + section: number, + title: string | null, + color: 'blue' | 'red' | 'yellow' | 'purple' | 'neutral', + children: React.ReactNode + ) => ( +
+ {title !== null && ( +
+
+ {section} + {title} +
+
+ )} + {children} +
+ ); + + const renderOpacityBlurControls = ( + opacityValue: number | '', + blurValue: number | '', + onOpacityChange: (nextOpacity: number) => void, + onBlurChange: (nextBlur: number) => void + ) => { + const normalizedOpacity = opacityValue === '' ? 1 : Number(opacityValue); + const normalizedBlur = blurValue === '' ? 0 : Number(blurValue); + const opacityUi = Math.round((1 - normalizedOpacity) * 100); + const blurUi = Math.max(0, Math.min(50, Math.round(normalizedBlur))); + + return ( +
+
+ + onOpacityChange(1 - parseInt(e.target.value, 10) / 100)} + /> +
+
+ + onBlurChange(parseInt(e.target.value, 10))} + /> +
+
+ ); + }; + React.useEffect(() => { + lastUndoObjectKeyRef.current = null; + lastUndoMultiKeyRef.current = null; if (selectedObjectType !== 'MULTI') { setGroupIdDraft(''); } @@ -68,20 +760,27 @@ export const PropertiesPanel: React.FC = () => { if (!obj || !game) { return (
{ + isPanelHoveredRef.current = true; if (game) game.isMouseOverUI = true; }} onMouseLeave={() => { + isPanelHoveredRef.current = false; if (game) game.isMouseOverUI = false; }} + onBlurCapture={() => { + lastUndoObjectKeyRef.current = null; + lastUndoMultiKeyRef.current = null; + }} style={{ fontSize: `${12 * uiScale}px` }} >
{selectedObjectId === 'SETTINGS' ? 'SETTINGS (Loading...)' : 'PROPERTIES'}
-
+
{selectedObjectId === 'SETTINGS' ? 'Loading Settings...' : 'No Selection'}
@@ -91,9 +790,12 @@ export const PropertiesPanel: React.FC = () => { if (selectedObjectType === 'MULTI' && multiObjects.length > 1) { const group = game.editor.selectionManager.getGroupTransform(); const entitiesAndQuads = multiObjects.filter((o: any) => o instanceof Entity); + const parallaxObjects = multiObjects.filter( + (o: any) => o instanceof Entity || o instanceof Triggerbox || (o as any).type === 'Quad' + ); const quads = multiObjects.filter((o: any) => (o as any).type === 'Quad'); const sharedLayer = getSharedValue(multiObjects, (o) => o.layer ?? 0); - const sharedParallax = getSharedValue(entitiesAndQuads, (o: any) => o.parallax ?? 1); + const sharedParallax = getSharedValue(parallaxObjects, (o: any) => o.parallax ?? 1); const sharedBlendMode = getSharedValue( entitiesAndQuads, (o: any) => o.blendMode || 'source-over' @@ -113,19 +815,27 @@ export const PropertiesPanel: React.FC = () => { entitiesAndQuads, (o: any) => !!o.ignoreScaling ); + const sharedParentNodeId = getSharedValue(multiObjects, (o: any) => o.spatial?.parentNodeId || ''); const sharedLocked = getSharedBooleanState(multiObjects, (o: any) => !!o.locked); const sharedDisabled = getSharedBooleanState(multiObjects, (o: any) => !!o.disabled); return (
{ + isPanelHoveredRef.current = true; if (game) game.isMouseOverUI = true; }} onMouseLeave={() => { + isPanelHoveredRef.current = false; if (game) game.isMouseOverUI = false; }} + onBlurCapture={() => { + lastUndoObjectKeyRef.current = null; + lastUndoMultiKeyRef.current = null; + }} style={{ fontSize: `${12 * uiScale}px` }} >
@@ -134,462 +844,545 @@ export const PropertiesPanel: React.FC = () => { X
-
-
- -
- (<Enter> = append, <Ctrl+Enter> = remove) -
- setGroupIdDraft(e.target.value)} - onKeyDown={(e) => { - if (e.key !== 'Enter') return; - e.preventDefault(); - const raw = groupIdDraft.trim(); - if (!raw) return; - const prepared = raw - .split(',') - .map((x) => x.trim()) - .filter(Boolean) - .map((x) => (x.startsWith('#') ? x : `#${x}`)); - - let changedCount = 0; - multiObjects.forEach((o: any) => { - const existing = (o.groupID || '') - .split(',') - .map((x: string) => x.trim()) - .filter(Boolean); - - if (e.ctrlKey) { - const filtered = existing.filter((x: string) => !prepared.includes(x)); - if (filtered.length !== existing.length) { - changedCount++; - } - o.groupID = filtered.join(','); - return; - } - - const merged = [...new Set([...existing, ...prepared])]; - if (merged.length !== existing.length) { - changedCount++; - } - o.groupID = merged.join(','); - }); - - if (changedCount > 0) { - incrementObjectVersion(); - incrementHierarchyVersion(); - const tagsText = prepared.join(', '); - if (e.ctrlKey) { - game.showNotification( - `Removed ${tagsText} from ${changedCount} object${changedCount === 1 ? '' : 's'}` - ); - } else { - game.showNotification( - `Appended ${tagsText} to ${changedCount} object${changedCount === 1 ? '' : 's'}` - ); - } - } - setGroupIdDraft(''); - }} - /> -
- -
-
- - { - const x = parseFloat(e.target.value); - game.editor.selectionManager.applyGroupTransform( - isNaN(x) ? 0 : x, - group.offsetY, - group.scale - ); - incrementObjectVersion(); - }} - /> -
-
- - { - const y = parseFloat(e.target.value); - game.editor.selectionManager.applyGroupTransform( - group.offsetX, - isNaN(y) ? 0 : y, - group.scale - ); - incrementObjectVersion(); - }} - /> -
-
- -
- - { - const s = parseFloat(e.target.value); - game.editor.selectionManager.applyGroupTransform( - group.offsetX, - group.offsetY, - isNaN(s) ? 1 : s - ); - incrementObjectVersion(); - }} - /> -
- -
-
- - { - const v = parseInt(e.target.value); - if (isNaN(v)) return; - applyToMulti((o) => { - o.layer = v; - }); - }} - /> -
- {entitiesAndQuads.length > 0 ? ( -
- +
+ {renderSection( + 0, + null, + 'neutral', + <> +
+ +
+ (<Enter> = append, <Ctrl+Enter> = remove) +
{ - const v = parseFloat(e.target.value); - if (isNaN(v)) return; - applyToMulti((o: any) => { - if (o instanceof Entity) o.parallax = v; - }); - }} - /> -
- ) : ( -
- )} -
+ value={groupIdDraft} + placeholder="#group" + onChange={(e) => setGroupIdDraft(e.target.value)} + onKeyDown={(e) => { + if (e.key !== 'Enter') return; + e.preventDefault(); + const raw = groupIdDraft.trim(); + if (!raw) return; + const prepared = raw + .split(',') + .map((x) => x.trim()) + .filter(Boolean) + .map((x) => (x.startsWith('#') ? x : `#${x}`)); + + let changedCount = 0; + multiObjects.forEach((o: any) => { + const existing = (o.groupID || '') + .split(',') + .map((x: string) => x.trim()) + .filter(Boolean); + + if (e.ctrlKey) { + const filtered = existing.filter((x: string) => !prepared.includes(x)); + if (filtered.length !== existing.length) changedCount++; + o.groupID = filtered.join(','); + return; + } - {entitiesAndQuads.length > 0 && ( -
-
- - { - const v = parseFloat(e.target.value); - if (isNaN(v)) return; - applyToMulti((o: any) => { - if (o instanceof Entity) o.opacity = v; + const merged = [...new Set([...existing, ...prepared])]; + if (merged.length !== existing.length) changedCount++; + o.groupID = merged.join(','); }); + + if (changedCount > 0) { + const multiKey = `MULTI:${multiObjects + .map((item: any) => item?.name || '') + .filter(Boolean) + .join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } + incrementObjectVersion(); + incrementHierarchyVersion(); + const tagsText = prepared.join(', '); + if (e.ctrlKey) { + game.showNotification( + `Removed ${tagsText} from ${changedCount} object${changedCount === 1 ? '' : 's'}` + ); + } else { + game.showNotification( + `Appended ${tagsText} to ${changedCount} object${changedCount === 1 ? '' : 's'}` + ); + } + } + setGroupIdDraft(''); }} />
-
- - { - const v = parseInt(e.target.value); - if (isNaN(v)) return; - applyToMulti((o: any) => { - if (o instanceof Entity) o.blur = v; + +
+ + { - const v = e.target.value; - applyToMulti((o: any) => { - if (o.color !== undefined) o.color = v; - }); - }} - /> - { - const v = e.target.value; - applyToMulti((o: any) => { - if (o.color !== undefined) o.color = v; - }); - }} - /> -
-
-
-
- + {sharedParentNodeId && ( +
+ { - if (el) el.indeterminate = sharedIsGrid === 'mixed'; - }} - onChange={(e) => { - applyToMulti((o: any) => { - if ((o as any).type === 'Quad') (o as any).isGrid = e.target.checked; - }); - }} - /> - Retro Grid - -
+ )} + )} - {quads.length > 0 && sharedIsGrid !== 'off' && ( + {renderSection( + 1, + 'Transform', + 'blue', <>
- + { - const v = parseInt(e.target.value); - if (isNaN(v)) return; - applyToMulti((o: any) => { - if ((o as any).type === 'Quad') (o as any).gridLinesX = v; - }); + const multiKey = `MULTI:${multiObjects + .map((item: any) => item?.name || '') + .filter(Boolean) + .join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } + const x = parseFloat(e.target.value); + game.editor.selectionManager.applyGroupTransform( + isNaN(x) ? 0 : x, + group.offsetY, + group.scale + ); + incrementObjectVersion(); }} - min={1} - max={50} />
- + { - const v = parseInt(e.target.value); - if (isNaN(v)) return; - applyToMulti((o: any) => { - if ((o as any).type === 'Quad') (o as any).gridLinesY = v; - }); + const multiKey = `MULTI:${multiObjects + .map((item: any) => item?.name || '') + .filter(Boolean) + .join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } + const y = parseFloat(e.target.value); + game.editor.selectionManager.applyGroupTransform( + group.offsetX, + isNaN(y) ? 0 : y, + group.scale + ); + incrementObjectVersion(); + }} + /> +
+
+ +
+
+ + { + const multiKey = `MULTI:${multiObjects + .map((item: any) => item?.name || '') + .filter(Boolean) + .join('|')}`; + if (game?.editor && lastUndoMultiKeyRef.current !== multiKey) { + game.editor.saveUndoState(); + lastUndoMultiKeyRef.current = multiKey; + } + const s = parseFloat(e.target.value); + if (isNaN(s) || s <= 0) return; + game.editor.selectionManager.applyGroupTransform(group.offsetX, group.offsetY, s); + incrementObjectVersion(); }} - min={1} - max={50} />
- + { - const v = parseFloat(e.target.value); + const v = parseInt(e.target.value); if (isNaN(v)) return; - applyToMulti((o: any) => { - if ((o as any).type === 'Quad') (o as any).lineWidth = v; + applyToMulti((o) => { + o.layer = v; }); }} - step={0.1} - min={0.1} - max={10} />
+
+ {parallaxObjects.length > 0 ? ( + <> + + { + const v = parseFloat(e.target.value); + if (isNaN(v)) return; + applyToMulti((o: any) => { + if (o instanceof Entity || o instanceof Triggerbox || (o as any).type === 'Quad') { + o.parallax = v; + } + }); + }} + /> + + ) : ( +
+ )} +
-
- -
+ + {entitiesAndQuads.length > 0 && ( +
+ +
+ )} + + )} + + {renderSection( + 2, + 'Visual', + 'yellow', + <> + {entitiesAndQuads.length > 0 && ( + renderOpacityBlurControls( + sharedOpacity, + sharedBlur, + (nextOpacity) => { + applyToMulti((o: any) => { + if (o instanceof Entity) o.opacity = nextOpacity; + }); + }, + (nextBlur) => { + applyToMulti((o: any) => { + if (o instanceof Entity) o.blur = nextBlur; + }); + } + ) + )} + + {entitiesAndQuads.length > 0 && ( +
+
+ +
+ { + const v = e.target.value; + applyToMulti((o: any) => { + if (o.color !== undefined) o.color = v; + }); + }} + /> + { + const v = e.target.value; + applyToMulti((o: any) => { + if (o.color !== undefined) o.color = v; + }); + }} + /> +
+
+
+
+ + { + if (el) el.indeterminate = sharedIsGrid === 'mixed'; + }} + onChange={(e) => { + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).isGrid = e.target.checked; + }); + }} + /> + Retro Grid + +
+ )} + + {quads.length > 0 && sharedIsGrid !== 'off' && ( + <> +
+
+ + { + const v = parseInt(e.target.value); + if (isNaN(v)) return; + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).gridLinesX = v; + }); + }} + min={1} + max={50} + /> +
+
+ + { + const v = parseInt(e.target.value); + if (isNaN(v)) return; + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).gridLinesY = v; + }); + }} + min={1} + max={50} + /> +
+
+ + { + const v = parseFloat(e.target.value); + if (isNaN(v)) return; + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).lineWidth = v; + }); + }} + step={0.1} + min={0.1} + max={10} + /> +
+
+
+ +
+ { + const v = e.target.value; + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).gridColor = v; + }); + }} + /> + { + const v = e.target.value; + applyToMulti((o: any) => { + if ((o as any).type === 'Quad') (o as any).gridColor = v; + }); + }} + /> +
+
+ + )} + + )} + + {renderSection( + 6, + null, + 'neutral', + <> +
+ + +
+ Disabled +
)} - -
-
- -
- -
- -
- -
); @@ -598,6 +1391,16 @@ export const PropertiesPanel: React.FC = () => { const handleChange = (field: string, value: any, enforceNumber = false) => { if (!obj) return; + if (selectedObjectType !== 'SETTINGS' && game?.editor) { + const objectKey = selectedObjectType === 'SCENE' + ? `SCENE:${obj.id || ''}` + : `${selectedObjectType || 'Object'}:${obj.name || ''}`; + if (lastUndoObjectKeyRef.current !== objectKey) { + game.editor.saveUndoState(); + lastUndoObjectKeyRef.current = objectKey; + } + } + let finalVal = value; if (enforceNumber) { finalVal = parseFloat(value); @@ -666,14 +1469,25 @@ export const PropertiesPanel: React.FC = () => { } }; + const isEntityLike = + selectedObjectType === 'Entity' || selectedObjectType === 'Actor' || selectedObjectType === 'Static'; + const isTriggerbox = selectedObjectType === 'Triggerbox'; + const isWalkbox = selectedObjectType === 'Walkbox'; + const isScene = selectedObjectType === 'SCENE'; + const isSettings = selectedObjectType === 'SETTINGS'; + const isObjectWithScriptEvents = !isSettings && !isScene && !isWalkbox && selectedObjectType !== 'MULTI'; + return (
{ + isPanelHoveredRef.current = true; if (game) game.isMouseOverUI = true; }} onMouseLeave={() => { + isPanelHoveredRef.current = false; if (game) game.isMouseOverUI = false; }} style={{ fontSize: `${12 * uiScale}px` }} @@ -687,762 +1501,584 @@ export const PropertiesPanel: React.FC = () => {
-
- {selectedObjectType !== 'SETTINGS' && ( - <> - {/* Common: Name -> ID */} -
- - { - // Local update only - const val = e.target.value; - if (selectedObjectType === 'SCENE') obj.id = val; - else obj.name = val; - incrementObjectVersion(); - }} - onBlur={(e) => { - // Commit with Validation - const rawVal = e.target.value; - const finalVal = rawVal.trim(); - const field = selectedObjectType === 'SCENE' ? 'id' : 'name'; - - // Validation (Only for Name/ID) - let isValid = true; - const scene = game?.sceneManager?.currentScene; - - if (selectedObjectType !== 'SCENE' && scene) { - // Check duplicates - // Check Entities - const dupEntity = scene.entities.find( - (ent) => ent.name === finalVal && ent !== game?.editor?.selectedObject - ); - // Check Triggerboxes - const dupTrigger = scene.triggerboxes - ? scene.triggerboxes.find( - (tb) => tb.name === finalVal && tb !== game?.editor?.selectedObject - ) - : null; - - if (dupEntity || dupTrigger) { - console.warn(`[PropertiesPanel] Duplicate Name '${finalVal}' rejected.`); - // @ts-ignore - if (game.showMessage) game.showMessage(`Name '${finalVal}' already exists!`); - isValid = false; +
+ {!isSettings && + renderSection( + 0, + null, + 'neutral', + <> +
+ + { + const val = e.target.value; + if (isScene) obj.id = val; + else obj.name = val; + incrementObjectVersion(); + }} + onBlur={(e) => { + const rawVal = e.target.value; + const finalVal = rawVal.trim(); + const field = isScene ? 'id' : 'name'; + + let isValid = true; + const scene = game?.sceneManager?.currentScene; + + if (!isScene && scene) { + const dupEntity = scene.entities.find( + (ent) => ent.name === finalVal && ent !== game?.editor?.selectedObject + ); + const dupTrigger = scene.triggerboxes + ? scene.triggerboxes.find( + (tb) => tb.name === finalVal && tb !== game?.editor?.selectedObject + ) + : null; + + if (dupEntity || dupTrigger) { + console.warn(`[PropertiesPanel] Duplicate Name '${finalVal}' rejected.`); + // @ts-ignore + if (game.showMessage) game.showMessage(`Name '${finalVal}' already exists!`); + isValid = false; + } } - } - if (isValid) { - handleChange(field, finalVal); - } else { - // Revert to original from real object - let realObj: any = null; - if (game?.editor) realObj = game.editor.selectedObject; + if (isValid) { + handleChange(field, finalVal); + } else { + let realObj: any = null; + if (game?.editor) realObj = game.editor.selectedObject; - if (realObj) { - if (selectedObjectType === 'SCENE') obj.id = realObj.id; - else obj.name = realObj.name; - incrementObjectVersion(); + if (realObj) { + if (isScene) obj.id = realObj.id; + else obj.name = realObj.name; + incrementObjectVersion(); + } } - } - }} - /> -
- - )} + }} + /> +
- {selectedObjectType !== 'SETTINGS' && selectedObjectType !== 'SCENE' && ( -
- - { - const val = e.target.value; - // Auto-format: Ensure every token starts with # - // 1. Split by comma - const tokens = val.split(','); - const newTokens = tokens.map((t) => { - // Don't auto-add to the very last token if it's empty (user just typed comma) - if (t.length === 0) return ''; - - let clean = t; - // If this is a new char entry (not just backspace), check prefix - const trimmed = t.trimStart(); - if (trimmed.length > 0 && !trimmed.startsWith('#')) { - // Find where the white space ends to insert # - const firstCharIdx = t.length - trimmed.length; - clean = t.substring(0, firstCharIdx) + '#' + trimmed; - } - return clean; - }); + {supportsTextAsset && ( +
+ + e.currentTarget.blur()} + style={{ pointerEvents: 'none' }} + /> + {textAssetPath && ( + <> +
+ + + {hasTextAsset && ( + + )} +
+
{textAssetPath}
+ + )} +
+ )} - handleChange('groupID', newTokens.join(',')); - }} - /> -
- )} + {!isScene && !isSettings && ( +
+ + { + 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; + }); - {/* Entity Properties (Static, Actor, Entity) - Moved & Compacted */} - {(selectedObjectType === 'Entity' || - selectedObjectType === 'Actor' || - selectedObjectType === 'Static') && ( - <> - {/* Display Name */} -
- - handleChange('customName', e.target.value)} - /> -
- - {/* Transform: X, Y, W, H */} -
-
- - handleChange('x', e.target.value, true)} - /> -
-
- - handleChange('y', e.target.value, true)} - /> -
-
- - handleChange('width', e.target.value, true)} - /> -
-
- - handleChange('height', e.target.value, true)} - /> -
-
- - {/* Scale, Layer, Parallax */} -
-
- - handleChange('modelScale', e.target.value, true)} - /> -
-
- - handleChange('layer', e.target.value, true)} - /> -
-
- - { - const val = parseFloat(e.target.value); - const newP = isNaN(val) ? 1.0 : val; - const oldP = obj.parallax !== undefined ? obj.parallax : 1.0; - - // Auto-Correct Position to prevent visual jump - // NewPos = OldPos + Cam * (NewP - OldP) - const scene = game.sceneManager.currentScene; - if (scene && game.editor.selectedObject) { - const camX = scene.camera.x; - const camY = scene.camera.y; - - const dx = camX * (newP - oldP); - const dy = camY * (newP - oldP); - - // Apply to Local - obj.x += dx; - obj.y += dy; - - // Apply to Real (Must do this manually as handleChange only does the targeting field) - if ( - game.editor && - game.editor.selectedObject && - 'x' in game.editor.selectedObject - ) { - (game.editor.selectedObject as any).x = obj.x; - (game.editor.selectedObject as any).y = obj.y; - } - } - - handleChange('parallax', newP, true); - }} - /> -
-
- - {/* Color & Blend Mode */} -
-
- -
- handleChange('color', e.target.value)} - /> - handleChange('color', e.target.value)} />
-
-
- - handleChange('opacity', e.target.value, true)} - /> -
-
- - handleChange('blur', 50 - parseInt(e.target.value))} - /> -
-
+ )} - {/* Sprite */} -
- -
- handleChange('spriteName', e.target.value)} - /> - -
-
+
+ + { + game.editor.saveUndoState(); + obj.spatial = { + ...(obj.spatial || {}), + parentNodeId: obj.spatial?.parentNodeId || null, + relation: value || (obj.spatial?.parentNodeId ? 'in' : null), + }; + incrementObjectVersion(); + incrementHierarchyVersion(); + }} + options={getSpatialRelationOptions(true)} + style={{ width: '100%' }} + /> +
+ )} +
+ )} + + )} - {/* Colliders + Flags */} -
-
- - handleChange('colliderWidth', e.target.value, true)} - /> -
-
- - handleChange('colliderHeight', e.target.value, true)} - /> -
-
+ {isEntityLike && ( + <> + {renderSection( + 1, + 'Transform', + 'blue', + <> +
+
+ + handleChange('x', e.target.value, true)} + /> +
+
+ + handleChange('y', e.target.value, true)} + /> +
+
+ + handleChange('height', e.target.value, true)} + /> +
+
+ + handleChange('width', e.target.value, true)} + /> +
+
-
- -
+
+
+ + handleChange('modelScale', e.target.value, true)} + /> +
+
+ + handleChange('layer', e.target.value, true)} + /> +
+
+ + { + const val = parseFloat(e.target.value); + const newP = isNaN(val) ? 1.0 : val; + const oldP = obj.parallax !== undefined ? obj.parallax : 1.0; + const scene = game.sceneManager.currentScene; + if (scene && game.editor.selectedObject) { + const camX = scene.camera.x; + const camY = scene.camera.y; + obj.x += camX * (newP - oldP); + obj.y += camY * (newP - oldP); + if (game.editor && game.editor.selectedObject && 'x' in game.editor.selectedObject) { + (game.editor.selectedObject as any).x = obj.x; + (game.editor.selectedObject as any).y = obj.y; + } + } + handleChange('parallax', newP, true); + }} + /> +
+
-
- -
-
- -
+
+
+ + handleChange('colliderHeight', e.target.value, true)} + /> +
+
+ + handleChange('colliderWidth', e.target.value, true)} + /> +
+
- {/* Interactions */} -
-
- SCRIPT EVENTS - handleChange('ignoreScaling', e.target.checked)} + /> + Disable Depth-scaling + +
+ + )} - {obj.interactions && - Object.keys(obj.interactions).map((verb) => ( -
-
- {verb.toUpperCase()} + {renderSection( + 2, + 'Visual', + 'yellow', + <> +
+
+ +
+ handleChange('color', e.target.value)} + /> + handleChange('color', e.target.value)} + />
+
+
+ + { - obj.interactions[verb] = e.target.value; - // Sync to real object - if (game.editor.selectedObject) { - (game.editor.selectedObject as any).interactions[verb] = e.target.value; - } - incrementObjectVersion(); - }} + style={{ flex: 1 }} + value={obj.spriteName || ''} + onChange={(e) => handleChange('spriteName', e.target.value)} />
- ))} -
- - )} - - {/* Walkbox/Triggerbox Properties */} - {(selectedObjectType === 'Walkbox' || selectedObjectType === 'Triggerbox') && ( -
- {selectedObjectType === 'Walkbox' && ( -
- - handleChange('layer', e.target.value, true)} - /> -
+
+ )} -
- -
-
- -
-
+ )} - {/* Quad Properties */} - {selectedObjectType === 'Quad' && ( + {selectedObjectType === 'Walkbox' && (
- {/* Layer */} -
- - handleChange('layer', e.target.value, true)} - /> -
- - {/* Opacity / Blur */} -
-
- - handleChange('opacity', e.target.value, true)} - /> -
-
- - handleChange('blur', 50 - parseInt(e.target.value))} - /> -
-
- - {/* Fill Color */}
- - {obj.filled !== false && ( -
- handleChange('color', e.target.value)} - /> - handleChange('color', e.target.value)} - /> -
- )} -
- - {/* Retro Grid */} -
- + + handleChange('gridLinesX', parseInt(e.target.value))} - min={1} - max={50} + value={formatPanelNumber(getPolyCentroid(obj.poly).x)} + onChange={(e) => translatePolyTo(parseFloat(e.target.value) || 0, getPolyCentroid(obj.poly).y)} />
- + handleChange('gridLinesY', parseInt(e.target.value))} - min={1} - max={50} + value={formatPanelNumber(getPolyCentroid(obj.poly).y)} + onChange={(e) => translatePolyTo(getPolyCentroid(obj.poly).x, parseFloat(e.target.value) || 0)} />
+
+
- + handleChange('lineWidth', parseFloat(e.target.value))} - step={0.1} - min={0.1} - max={10} + value={polygonScaleDraft} + onChange={(e) => applyPolygonScaleDraft(e.target.value)} />
-
-
- -
+
+ handleChange('gridColor', e.target.value)} + value={formatPanelNumber(obj.layer || 0)} + onChange={(e) => handleChange('layer', e.target.value, true)} /> +
+
+ handleChange('gridColor', e.target.value)} + value={formatPanelNumber(obj.parallax ?? 1)} + onChange={(e) => { + const val = parseFloat(e.target.value); + const newP = isNaN(val) ? 1.0 : val; + const oldP = obj.parallax !== undefined ? obj.parallax : 1.0; + const scene = game.sceneManager.currentScene; + if (scene && obj.poly?.length) { + const dx = scene.camera.x * (newP - oldP); + const dy = scene.camera.y * (newP - oldP); + obj.poly = obj.poly.map((pt: any) => ({ + x: Math.round(pt.x + dx), + y: Math.round(pt.y + dy), + })); + } + handleChange('parallax', newP, true); + }} />
)} - {/* Blend & Sort (Extras) */} + + )} + + {/* Quad Properties */} + {selectedObjectType === 'Quad' && ( +
+
+
+
+ 1 + Transform +
+
+
+
- - translateQuadTo(parseFloat(e.target.value) || 0, getQuadCentroid(obj).y)} + /> +
+
+ + translateQuadTo(getQuadCentroid(obj).x, parseFloat(e.target.value) || 0)} />
- + + handleChange('layer', e.target.value, true)} + /> +
+
+ +
+
+ + handleChange('parallax', e.target.value, true)} + /> +
+
+ + applyPolygonScaleDraft(e.target.value)} + /> +
+
+ handleChange('filled', e.target.checked)} + /> + Fill Color + + {obj.filled !== false && ( +
+ handleChange('color', e.target.value)} + /> + handleChange('color', e.target.value)} + /> +
+ )} +
+ + {/* Retro Grid */} +
+ +
+ + {obj.isGrid && ( + <> +
+
+ + handleChange('gridLinesX', parseInt(e.target.value))} + min={1} + max={50} + /> +
+
+ + handleChange('gridLinesY', parseInt(e.target.value))} + min={1} + max={50} + /> +
+
+ + handleChange('lineWidth', parseFloat(e.target.value))} + step={0.1} + min={0.1} + max={10} + /> +
+
+
+ +
+ handleChange('gridColor', e.target.value)} + /> + handleChange('gridColor', e.target.value)} + /> +
+
+ + )} -
- -
-
-
@@ -2307,7 +3065,7 @@ export const PropertiesPanel: React.FC = () => { { comp.offsetX = parseFloat(e.target.value); incrementObjectVersion(); @@ -2321,7 +3079,7 @@ export const PropertiesPanel: React.FC = () => { { comp.offsetY = parseFloat(e.target.value); incrementObjectVersion(); @@ -2350,38 +3108,20 @@ export const PropertiesPanel: React.FC = () => {
)} - {selectedObjectType === 'Quad' && ( -
- Drag VERTEX: Hold ALT to snap to vertices/grid. -
- Hold SHIFT for angle snap. -
- )} - {selectedObjectType === 'Actor' && ( <> -
-
- ACTOR PROPERTIES +
+
+
+ 4 + ACTOR PROP. +
{/* Is Player */}
-
@@ -2435,7 +3175,7 @@ export const PropertiesPanel: React.FC = () => { type="number" step="10" className="e-input" - value={obj.animationSpeed !== undefined ? obj.animationSpeed : 150} + value={formatPanelNumber(obj.animationSpeed !== undefined ? obj.animationSpeed : 150)} onChange={(e) => handleChange('animationSpeed', e.target.value, true)} />
@@ -2443,13 +3183,8 @@ export const PropertiesPanel: React.FC = () => { {/* Animation Sets */}
ANIMATION SETS + > + x + +
+ + {/* Directions */} + {['down', 'up', 'left', 'right'].map((dir) => ( +
+
+ {dir.toUpperCase()} +
+ + +
+ ))} +
+ ); + })} + + )} + + {isObjectWithScriptEvents && ( +
+
+
+ 5 + SCRIPT EVENTS +
+
+ { + obj.interactions[verb] = e.target.value; + if (game.editor.selectedObject) { + (game.editor.selectedObject as any).interactions[verb] = e.target.value; + } + incrementObjectVersion(); + }} + /> + +
+ ))} +
+ )} + + {!isSettings && !isScene && !isWalkbox && ( +
+ {isTriggerbox && ( + <> + +
+ {mode && mode.includes('DRAW') + ? 'Click to add points. Press ENTER to finish. Hold Shift for 22.5° snap.' + : 'To edit, drag vertices on screen. Hold Shift for 22.5° snap.'} +
+ + )} + + {selectedObjectType === 'Quad' && ( +
+ Drag VERTEX: Hold ALT to snap to vertices/grid. +
+ Hold SHIFT for angle snap. +
+ )} + +
+ + + +
+
+ )} + + {/* SCENE Properties */} + {selectedObjectType === 'SCENE' && ( + <> + {(obj.camera || obj.defaultCamera) && + renderSection( + 1, + 'Camera', + 'blue', + <> +
+
+ + { + if (obj.camera) { + obj.camera.x = parseFloat(e.target.value); + incrementObjectVersion(); + } + }} + /> +
+
+ + { + if (obj.camera) { + obj.camera.y = parseFloat(e.target.value); + incrementObjectVersion(); + } + }} + />
- - {/* Directions */} - {['down', 'up', 'left', 'right'].map((dir) => ( -
+ + { + if (obj.camera) { + obj.camera.zoom = parseFloat(e.target.value); + incrementObjectVersion(); + } }} - > -
- {dir.toUpperCase()} -
- - -
- ))} + /> +
- ); - })} - - )} - - {/* SCENE Properties */} - {selectedObjectType === 'SCENE' && ( - <> -
- - handleChange('name', e.target.value)} - /> -
- {/* Camera properties */} - {(obj.camera || obj.defaultCamera) && ( -
-
- CAMERA -
-
-
- - { - if (obj.camera) { - obj.camera.x = parseFloat(e.target.value); - incrementObjectVersion(); - } - }} - /> -
-
- - { - if (obj.camera) { - obj.camera.y = parseFloat(e.target.value); - incrementObjectVersion(); - } - }} - /> -
-
- - { - if (obj.camera) { - obj.camera.zoom = parseFloat(e.target.value); - incrementObjectVersion(); - } - }} - /> +
+
-
-
- -
-
-
- - - handleChange('cameraSpeed', parseFloat(e.target.value), true) - } - /> -
-
- - - handleChange('camDeadzoneX', parseFloat(e.target.value), true) - } - /> -
-
- - - handleChange('camDeadzoneY', parseFloat(e.target.value), true) - } - /> +
+
+ + handleChange('cameraSpeed', parseFloat(e.target.value), true)} + /> +
+
+ + handleChange('camDeadzoneX', parseFloat(e.target.value), true)} + /> +
+
+ + handleChange('camDeadzoneY', parseFloat(e.target.value), true)} + /> +
-
- <> +
-
- Camera Bounds (Min/Max) -
+
Camera Bounds (Min/Max)
@@ -2735,7 +3611,7 @@ export const PropertiesPanel: React.FC = () => { type="number" className="e-input" placeholder="None" - value={obj.camMinX !== undefined ? obj.camMinX : ''} + value={obj.camMinX !== undefined ? formatPanelNumber(obj.camMinX) : ''} onChange={(e) => handleChange( 'camMinX', @@ -2751,7 +3627,7 @@ export const PropertiesPanel: React.FC = () => { type="number" className="e-input" placeholder="None" - value={obj.camMaxX !== undefined ? obj.camMaxX : ''} + value={obj.camMaxX !== undefined ? formatPanelNumber(obj.camMaxX) : ''} onChange={(e) => handleChange( 'camMaxX', @@ -2767,7 +3643,7 @@ export const PropertiesPanel: React.FC = () => { type="number" className="e-input" placeholder="None" - value={obj.camMinY !== undefined ? obj.camMinY : ''} + value={obj.camMinY !== undefined ? formatPanelNumber(obj.camMinY) : ''} onChange={(e) => handleChange( 'camMinY', @@ -2783,7 +3659,7 @@ export const PropertiesPanel: React.FC = () => { type="number" className="e-input" placeholder="None" - value={obj.camMaxY !== undefined ? obj.camMaxY : ''} + value={obj.camMaxY !== undefined ? formatPanelNumber(obj.camMaxY) : ''} onChange={(e) => handleChange( 'camMaxY', @@ -2795,89 +3671,81 @@ export const PropertiesPanel: React.FC = () => {
- -
- )} - {/* Default Camera (Start Position) */} - {obj.defaultCamera && ( -
-
- DEFAULT CAMERA -
-
-
- - { - obj.defaultCamera.x = parseFloat(e.target.value); - incrementObjectVersion(); - }} - /> -
-
- - { - obj.defaultCamera.y = parseFloat(e.target.value); - incrementObjectVersion(); - }} - /> -
-
- - { - obj.defaultCamera.zoom = parseFloat(e.target.value); - incrementObjectVersion(); - }} - /> -
-
-
- -
-
- )} + {obj.defaultCamera && ( +
+
Default Camera
+
+
+ + { + obj.defaultCamera.x = parseFloat(e.target.value); + incrementObjectVersion(); + }} + /> +
+
+ + { + obj.defaultCamera.y = parseFloat(e.target.value); + incrementObjectVersion(); + }} + /> +
+
+ + { + obj.defaultCamera.zoom = parseFloat(e.target.value); + incrementObjectVersion(); + }} + /> +
+
+
+ +
+
+ )} + + )} - {/* Scaling Settings */} - {game.sceneManager.currentScene && ( -
-
- SCALING -
- {(() => { + {game.sceneManager.currentScene && + renderSection( + 2, + 'Scaling', + 'yellow', + (() => { const s = game.sceneManager.currentScene.scaling; return ( <>
-
{s.enabled && ( -
+
{ s.min = parseFloat(e.target.value); incrementObjectVersion(); @@ -2913,7 +3779,7 @@ export const PropertiesPanel: React.FC = () => { type="number" step="0.1" className="e-input" - value={s.max} + value={formatPanelNumber(s.max)} onChange={(e) => { s.max = parseFloat(e.target.value); incrementObjectVersion(); @@ -2925,7 +3791,7 @@ export const PropertiesPanel: React.FC = () => { { s.horizon = parseFloat(e.target.value); incrementObjectVersion(); @@ -2937,7 +3803,7 @@ export const PropertiesPanel: React.FC = () => { { s.front = parseFloat(e.target.value); incrementObjectVersion(); @@ -2948,9 +3814,8 @@ export const PropertiesPanel: React.FC = () => { )} ); - })()} -
- )} + })() + )} )} @@ -2959,8 +3824,8 @@ export const PropertiesPanel: React.FC = () => { <>
@@ -2972,7 +3837,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - UI Scale {(obj.editor?.uiScale || 1.0).toFixed(1)}x + UI Scale {formatPanelNumber(obj.editor?.uiScale || 1.0)}x { min="0.5" max="2.0" step="0.1" - value={obj.editor?.uiScale || 1.0} + value={formatPanelNumber(obj.editor?.uiScale || 1.0)} onChange={(e) => { if (!obj.editor) obj.editor = { uiScale: 1.0 }; obj.editor.uiScale = parseFloat(e.target.value); @@ -2995,8 +3860,8 @@ export const PropertiesPanel: React.FC = () => {
@@ -3028,7 +3893,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - Curvature {obj.crt.curvature.toFixed(2)} + Curvature {formatPanelNumber(obj.crt.curvature)} { min="0" max="0.5" step="0.01" - value={obj.crt.curvature} + value={formatPanelNumber(obj.crt.curvature)} onChange={(e) => { obj.crt.curvature = parseFloat(e.target.value); incrementObjectVersion(); @@ -3048,7 +3913,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - Vignette {obj.crt.vignette.toFixed(2)} + Vignette {formatPanelNumber(obj.crt.vignette)} { min="0" max="1" step="0.05" - value={obj.crt.vignette} + value={formatPanelNumber(obj.crt.vignette)} onChange={(e) => { obj.crt.vignette = parseFloat(e.target.value); incrementObjectVersion(); @@ -3068,7 +3933,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - Scanline Count {Math.round(obj.crt.scanlineCount)} + Scanline Count {formatPanelNumber(obj.crt.scanlineCount)} { min="100" max="2000" step="50" - value={obj.crt.scanlineCount} + value={formatPanelNumber(obj.crt.scanlineCount)} onChange={(e) => { obj.crt.scanlineCount = parseFloat(e.target.value); incrementObjectVersion(); @@ -3088,7 +3953,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - Scanline Intensity {obj.crt.scanlineIntensity.toFixed(2)} + Scanline Intensity {formatPanelNumber(obj.crt.scanlineIntensity)} { min="0" max="1" step="0.05" - value={obj.crt.scanlineIntensity} + value={formatPanelNumber(obj.crt.scanlineIntensity)} onChange={(e) => { obj.crt.scanlineIntensity = parseFloat(e.target.value); incrementObjectVersion(); @@ -3108,7 +3973,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - RGB Split {obj.crt.aberration.toFixed(1)} + RGB Split {formatPanelNumber(obj.crt.aberration)} { min="0" max="5" step="0.1" - value={obj.crt.aberration} + value={formatPanelNumber(obj.crt.aberration)} onChange={(e) => { obj.crt.aberration = parseFloat(e.target.value); incrementObjectVersion(); @@ -3128,7 +3993,7 @@ export const PropertiesPanel: React.FC = () => { className="e-label" style={{ display: 'flex', justifyContent: 'space-between' }} > - Bloom {obj.crt.bloom.toFixed(2)} + Bloom {formatPanelNumber(obj.crt.bloom)} { min="0" max="1" step="0.05" - value={obj.crt.bloom} + value={formatPanelNumber(obj.crt.bloom)} onChange={(e) => { obj.crt.bloom = parseFloat(e.target.value); incrementObjectVersion(); @@ -3149,7 +4014,7 @@ export const PropertiesPanel: React.FC = () => { style={{ display: 'flex', justifyContent: 'space-between' }} > Phosphor / Grain{' '} - {obj.crt.phosphor ? obj.crt.phosphor.toFixed(2) : '0.00'} + {formatPanelNumber(obj.crt.phosphor || 0)} { min="0" max="1" step="0.05" - value={obj.crt.phosphor || 0} + value={formatPanelNumber(obj.crt.phosphor || 0)} onChange={(e) => { obj.crt.phosphor = parseFloat(e.target.value); incrementObjectVersion(); @@ -3181,10 +4046,7 @@ export const PropertiesPanel: React.FC = () => { )} -
+