diff --git a/API.md b/API.md index 291bab8..23a682e 100644 --- a/API.md +++ b/API.md @@ -7,7 +7,7 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT | GET | `projects/{id}` | `{id,name,revision,updatedAt,project}` | | PUT | `projects/{id}` | `{revision, project}` → `{revision,updatedAt}` or **409** `{error,revision}` when stale | | DELETE | `projects/{id}` | 204 | -| GET | `textures` | `[{file,width,height,bytes,updatedAt}]` – what the image picker offers | +| GET | `textures` | `[{file,width,height,bytes,updatedAt}]` — what the image picker offers | | POST | `textures` | **raw image bytes** → `{file,width,height,bytes}` (201) | | DELETE | `textures/{file}` | 204 | diff --git a/Animations.md b/Animations.md index 040e823..2d1778a 100644 --- a/Animations.md +++ b/Animations.md @@ -1,14 +1,14 @@ An **animation asset** is a list of pictures played one after another. Each entry is what a -sprite's `texture` can hold – a whole texture or one of its frames – so an animation may mix +sprite's `texture` can hold — a whole texture or one of its frames — so an animation may mix frames of several sheets, and a picture listed twice is held twice as long; there is no per-frame duration beyond that. Its two properties are **Frames per second** (default 10) and **Playback**: *Loop*, *Once* (stops on the last frame) or *Ping-pong* (back and forth, without showing the turning frames twice). -**A sprite wears it through `texture`** – the same property, the same picker (animations are +**A sprite wears it through `texture`** — the same property, the same picker (animations are listed first, marked "(animation)") and the same instance overrides as a texture or a frame. The sprite stays what is drawn, sized, tinted and collided with; the animation is only a picture -that changes over time. *Set to W × H px* takes the size of the first frame. The fastest way in +that changes over time. *Set to W x H px* takes the size of the first frame. The fastest way in is right-clicking a texture → **Create animation from texture**, which plays its frames in order (the whole picture when it has none), named after the texture, in one undo step. @@ -27,11 +27,11 @@ strip the wheel scrolls the tiles instead. no clock and redraws on changes only. The `AnimationSystem` gives every sprite its own clock, kept on the entity and not in `props` (the world writes changed props back as overrides). The one rule that makes it usable from a graph: **a different animation starts from its first frame, the same one set again -runs on** – so `On update → Properties (Sprite, texture on Set)` may choose Run or Idle every +runs on** — so `On update → Properties (Sprite, texture on Set)` may choose Run or Idle every frame without freezing the animation on frame one. The system ticks before the behaviors, so **`On animation finished`** fires in the frame a *Once* animation reaches its end, exactly once, with the sprite as `object`. Like collisions it is heard by the behaviors of the sprite's owner -subtree – the sprite itself, or a sprite placed next to the behavior below the same object – and +subtree — the sprite itself, or a sprite placed next to the behavior below the same object — and by a behavior without an owner for every sprite; its *Animation* setting narrows it to one. There is no node to restart an animation that is already running; that is left for when a game needs it. diff --git a/Architecture.md b/Architecture.md index 21a0822..43e911f 100644 --- a/Architecture.md +++ b/Architecture.md @@ -1,11 +1,11 @@ ## Stack -- **PixiJS v8** – rendering and pointer events. No game engine; runtime systems live in `src/runtime/`. +- **PixiJS v8** — rendering and pointer events. No game engine; runtime systems live in `src/runtime/`. `main.ts` creates the WebGL context itself with `desynchronized`, which Pixi has no option for: frames skip the compositor queue, so the UI keeps up with the pointer (click to picture measured - 72–77 ms before, 16–18 ms after). The price is possible tearing while things move fast; + 72-77 ms before, 16-18 ms after). The price is possible tearing while things move fast; `?vsync` falls back to Pixi's own context. A browser without the mode simply ignores it. -- **Own canvas widget toolkit** (`src/ui/`) – panels, buttons, lists, text/number/color fields, +- **Own canvas widget toolkit** (`src/ui/`) — panels, buttons, lists, text/number/color fields, popups. The only lasting DOM element is a hidden `` used as a keyboard bridge (IME, mobile keyboard, clipboard) while a canvas text field is focused; *Upload…* adds a short-lived file input. @@ -14,23 +14,23 @@ that trusted `e.detail` never saw a second click. Presses are counted per key rather than per widget, because a list rebuilds its rows when the first click changes the selection. Menus read as lists: entries are left-aligned and scroll once they pass 320 px instead of - running off the screen. A menu that grows with the project – the parent picker lists every - object of the scene – is opened with `{ searchable: true }`, which puts a filter box above + running off the screen. A menu that grows with the project — the parent picker lists every + object of the scene — is opened with `{ searchable: true }`, which puts a filter box above the entries (Enter takes the first match). A field that opens a menu is a `Dropdown`: value flush left, chevron drawn flush right rather than written into the label. Every label that can be wider than its control is cut with an ellipsis (`truncate` in `Label.ts`, shared by `Label`, `ListRow` and `Button`). A `Button` keeps the full text and - measures from it – only what is drawn is shortened – and a subclass that puts something else + measures from it — only what is drawn is shortened — and a subclass that puts something else inside the control takes its room off `labelSlot()`, so a long value is cut rather than run underneath the chevron. Given a `width`, a button is exactly that wide and truncates instead: the Behavior, Texture, Animation and Container pickers keep their size whatever they show; the tab - bar uses 90 px as a floor. A button that can draw its label bold – on an accent fill: primary, - danger, a toggled tab – is measured bold, or a dialog's `Delete` comes out as `Del…`. + bar uses 90 px as a floor. A button that can draw its label bold — on an accent fill: primary, + danger, a toggled tab — is measured bold, or a dialog's `Delete` comes out as `Del…`. The padding is an inset rather than a claim, so a 24 px icon button spends it on the glyph instead of truncating it away; a button that switches between label forms (`↺` and `↺ ▾`) asks for `minWidth` and grows. `__gm.textOverflows()` guards the whole class of bug: it reports any visible text drawn wider than the widget owning it, `__gm.labelsDropped()` guards the other - direction – a button too narrow to draw any of its label – and the `pickerWidth()` of + direction — a button too narrow to draw any of its label — and the `pickerWidth()` of `__gm.graph` / `__gm.texture` / `__gm.container` catch a picker that resizes itself around its content (e2e step 14). - **Vite + TypeScript**, **Vitest** for core, toolkit and editor logic. @@ -51,13 +51,13 @@ src/core/store/ serialize (untrusted JSON → Project), http (one API cal src/runtime/ World, Entity, System interface, Input tracker, CollisionSystem, AnimationSystem, BehaviorSystem src/ui/ widget toolkit (generic, knows nothing about assets) src/viewport/ view (transforms), items (what to draw), symbols (how to draw it), - textureFill, TextureLibrary, activeCamera – no editor concepts, so tests reach it freely + textureFill, TextureLibrary, activeCamera — no editor concepts, so tests reach it freely src/editor/ App → Editor (layout + ticker), PlaySession (the run), ProjectSession (load/create), EditorRun (run in editor), state, actions, shortcuts, debugHook, autosave, textureImport, framing (fit/marquee/snap maths), AssetTabEditor (the shared shell of the asset tabs) -src/editor/viewport/ WorldViewport base + SceneViewport/GameViewport – editing tools, hence not in src/viewport/ +src/editor/viewport/ WorldViewport base + SceneViewport/GameViewport — editing tools, hence not in src/viewport/ src/editor/inspector/ Inspector panel, host (what a section builds into), field factory, previewHandlers (scrub → preview → command), valueSpecs - (where a value lives – shared with the List tab), + (where a value lives — shared with the List tab), one section per selection kind (asset/object/node/frame), variableRows (one row shape for every variable), portSection, aspectAction, objectQueries/vectorRows/parentRow src/editor/panels/ AssetPanel, HierarchyPanel, Toolbar @@ -79,7 +79,7 @@ twice a second) in edit and play mode alike; `__gm.fps()` exposes the same numbe Rules that keep it organised: `core/` imports nothing from other layers; `ui/` never sees assets; position exists only on scene objects; overrides are merged in exactly one place -(`core/properties/resolve.ts`); the inspector is generated from `PropertySchema` – a new +(`core/properties/resolve.ts`); the inspector is generated from `PropertySchema` — a new property is one schema line; a new node is one file **plus a row in the table in `tests/core/node-catalog.test.ts`**, which fails if a node has no case. Comments state contracts and reasons, not what the code does: a `/** */` on every export whose name does not say it all, diff --git a/Behaviors.md b/Behaviors.md index 010b3c5..10272d9 100644 --- a/Behaviors.md +++ b/Behaviors.md @@ -8,7 +8,7 @@ shown on the node and editable in the inspector. - **Variables** are the behavior's own property schema (`BehaviorAsset.variables`): they appear in the inspector, can be overridden per placed instance, and each running instance gets its own mutable copy. One node reads and writes them, `Variables`: its settings are a *list* - of variables, and each one is an output that can be read at any time – no flow has to reach it – + of variables, and each one is an output that can be read at any time — no flow has to reach it — and is read at the moment it is asked, so a read after a write sees the new value. A variable switched to **Set** in the inspector is an input too, written when the flow arrives; the node has its flow pins only while at least one is. Unconnected and with nothing typed, it writes the zero @@ -21,59 +21,59 @@ shown on the node and editable in the inspector. `+ Property` offers: *Sprite*, *Text*, *Collider*, *Camera* and *Container* offer their fields, *Behavior* the variables of the behavior picked below it (renaming such a variable carries the port along, in any graph), and *Any* offers the kinds (Number, Text, …) and leaves the name to be - typed. A picked property keeps its field and default – an unconnected `color` is a colour field + typed. A picked property keeps its field and default — an unconnected `color` is a colour field starting white, shown on the node as `#ffffff`, and a camera's `width` is its whole-pixel view width. Switching the type keeps the ports; one the new type lacks is marked. The node shows its type as a setting row (*Type: Sprite*, plus *Behavior: Game* under that type). Textures and scenes are no type here: they are not objects in a running game. A variable is a - **number, a text, a yes/no, a colour, a texture, a state machine, a list or a struct**; a list declares **nothing** about its items – + **number, a text, a yes/no, a colour, a texture, a state machine, a list or a struct**; a list declares **nothing** about its items — each one is a number, a text, a yes/no, a colour, a texture, a state machine or a struct and says so itself, so one list may hold several kinds at once. A list is - compared by its items, never by identity – otherwise every reload would look like an override. + compared by its items, never by identity — otherwise every reload would look like an override. There is no list *of objects*: a list is stored in the project file and an object is a live handle that no reload survives. A set of objects is a list of their names, walked with `Repeat` and `Object by name`. A list property is shown as a button saying how many items it holds; clicking it opens the **List** tab, which gives each item a row with the same widget the - inspector would use for that kind – a struct item is its fields as rows under each other. The inspector stays visible beside the tab, so what the list + inspector would use for that kind — a struct item is its fields as rows under each other. The inspector stays visible beside the tab, so what the list belongs to is still on screen. Which list the tab shows is a `ListSlot` (asset property, object override or node setting, plus the `field` of a struct that holds one) on the editor state, because no tab may reach into the `Editor` to switch itself on. An **unconnected `list` input** of a node is the same button and the same tab: its inline value lives in the very same params bag a setting does, and the slot stops resolving the moment an edge feeds the port. What an item holds is **its own** and is chosen there: the **kind mark** in front of a row (`#`, `Aa`, `✓`, `◧`, `▦`, `⊞`) - is the picker – a click opens the menu, with no chevron, since the mark has to stay as narrow as - its column – and `+ Item ▾` asks for the kind of the one it appends. Because the kind lives in the + is the picker — a click opens the menu, with no chevron, since the mark has to stay as narrow as + its column — and `+ Item ▾` asks for the kind of the one it appends. Because the kind lives in the value, every such change is an ordinary **value edit**: one command, one undo step, and no neighbouring item touched. A **struct item** carries its own shape the same way: its fields are renamed, retyped, added (`+ Field ▾`) and removed on its own rows, which grows that one item and not the list. Three kinds need saying, because JSON cannot tell them from the value beside them: a **colour** is a number, and a **texture** or a **state machine** is an id, which is a text. Such a *tagged* kind wears its - name as the one key of a small object – `{"$color": 16711680}`, `{"$texture": "walk-up"}` (the - picker a sprite's `texture` property has – a whole picture, one frame of a sheet, or an animation), + name as the one key of a small object — `{"$color": 16711680}`, `{"$texture": "walk-up"}` (the + picker a sprite's `texture` property has — a whole picture, one frame of a sheet, or an animation), `{"$statemachine": "doors"}`. A field name cannot start with `$` (`isValidFieldName`), so a tag can never collide with a struct item of one field. Either reads as what it wraps on a data edge (`Item at`), the graph having neither colours nor pictures of its own, and `Set item` leaves - the slot the kind it was. A **texture is a kind wherever a kind is picked** – a variable, a struct - field, a list item, an Any port of `Properties`, a field of `Pack` – and the `Texture` node hands + the slot the kind it was. A **texture is a kind wherever a kind is picked** — a variable, a struct + field, a list item, an Any port of `Properties`, a field of `Pack` — and the `Texture` node hands one into the graph, as `Number` and `Text` do a number and a text; a **state machine** is the same kind of kind, with the `State machine` node. **Every kind is one row of one table** (`core/properties/kinds.ts`): its word and mark, what it travels as on an edge, its empty value, how a value is read as it, whether it is tagged and where it may be picked. The kind lists, the `+ …` menus, the tags, the port fields and the literal nodes all follow from the row, so a new kind is that row plus, if it names something in the project, its picker. Declared (a variable, a struct - field of one, a port) it needs no tag: the declaration says what it is, and the value is the id – + field of one, a port) it needs no tag: the declaration says what it is, and the value is the id — the way a colour field is a number. A declared port of kind colour or texture carries the field that edits it and travels as a number or a text, since the graph has neither; a port declared as the number a colour travels as still names the property (`isPortOf`). A **struct** variable is the other way round: few fields, so they fit under each other right in the inspector. Its head row renames it and adds fields (`+ Field`), each - field row below carries its own name, its value and a remove – the port editor `Pack` already - has, one level down and with a value column – and a field may itself be a **list**, which is + field row below carries its own name, its value and a remove — the port editor `Pack` already + has, one level down and with a value column — and a field may itself be a **list**, which is the one count button that opens the tab one level deeper. Adding, renaming or removing a field reaches the declaration, the behavior's own value **and every instance override** in one command, so no stored value keeps a shape its declaration no longer promises. A placed instance draws every variable, whatever its kind, with the very same rows (`buildVariable`), minus the renaming, the - kind chooser and the `+ Field` / `×` buttons: it may override the values, not declare them. A + kind chooser and the `+ Field` / `x` buttons: it may override the values, not declare them. A struct's ↺ / ↑ sit on its head row, because an override covers the whole bundle rather than a single field. All of it sits on **one column grid** with the rest of the inspector: mark (28 px), gap and name (90 px) are exactly as wide as a plain row's label (124 px), a nested field's name gives up what @@ -83,24 +83,24 @@ shown on the node and editable in the inspector. viewport). The `Owner` node returns it; `Self` returns the behavior itself; `Parent` walks up from any object. - **Auto-repeat on `On key down`**: two settings on the node, *Repeat after (ms)* and *Repeat - interval (ms)* – milliseconds, so both stay whole numbers. A delay of 0 leaves the node as it was – one fire per press; with a delay set, a + interval (ms)* — milliseconds, so both stay whole numbers. A delay of 0 leaves the node as it was — one fire per press; with a delay set, a key that stays held fires again after it and then every interval (0 = every frame). That is the sideways slide of a Tetris piece, or a held arrow walking a menu. The browser's own key repeat is deliberately not used: its delay and rate are an operating-system setting, not a game's. Both rows stay off the node body while the delay is 0, so a plain key node keeps its single line. - **Node set** (`src/core/graph/nodes/`, one file per node): events `On start`, `On update`, `On key down/up/held`, - `On timer`, `On collision enter/stay/exit`, `On click` (the mouse pressed on the owner – anywhere when it has none), `On animation finished`, `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End` · data `Self`, `Owner`, `Parent`, `Object by name`, `Name of`, - `Is asset` (is this object an instance of asset X – how you tell what you collided with), + `On timer`, `On collision enter/stay/exit`, `On click` (the mouse pressed on the owner — anywhere when it has none), `On animation finished`, `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End` · data `Self`, `Owner`, `Parent`, `Object by name`, `Name of`, + `Is asset` (is this object an instance of asset X — how you tell what you collided with), `Variables`, `Properties`, `Get position` (local and world), `Is enabled`, literals `Number`/`Text`/`Boolean`/`Texture`, `Add/Subtract/Multiply/Divide`, `Min/Max/Abs/Sqrt/Clamp`, `Floor/Ceil/Round`, `Modulo`, `Random`, `Time`, `To text`, `To number`, `Join text`, `Substring`, `Find text`, `Equals`, - lists `List` (a constant), `Fill` (one value repeated – how an empty board is made), `Count`, + lists `List` (a constant), `Fill` (one value repeated — how an empty board is made), `Count`, `Item at`, `Set item` (a *new* list, the one that came in is left alone), `Sub list`, `Join lists`, `Index of`, structs `Pack`/`Expand`, `Length`, `Distance`, `Normalize`, `Axis input` (four keys → a normalized direction, so diagonals are not faster), `Compare`, `And/Or/Not`, `Key is down`, `Mouse position`, `Mouse button`, `Is hovered`, `Is touching` · - actions `Move` (dx, dy, scale, dt – connect an event's dt for per-second movement), `Set position`, `Spawn` + actions `Move` (dx, dy, scale, dt — connect an event's dt for per-second movement), `Set position`, `Spawn` (asset setting + x/y/z, optional parent → outputs the new object), `Destroy`, `Log`, `Stop game`, `Restart game`. A node type is one file exporting a `NodeDef` (ports, settings, `run`), listed in `nodes/index.ts`; palette, ports, validation and inspector fields are generated from it. A port may declare a `default` for its inline value @@ -110,7 +110,7 @@ shown on the node and editable in the inspector. settings and from the graph around it). `resolveDef(node, graph, lookup)` is what applies both, so the canvas and the inspector see a definition that fits *this* node; a node whose ports are derived reads them back off that definition, which is why `run` is handed it. `Sequence` keeps - one free output below the last connected one – connecting the spare grows it, unplugging the + one free output below the last connected one — connecting the spare grows it, unplugging the bottom edge shrinks it again (keys `a`, `b`, `c`, … up to 26). `Run behavior` takes its ports from the behavior it names, and a behavior's `Start`/`End` node declares them. Pointing a call at a different behavior **takes the edges of the ports that go away** and gives them back on @@ -118,71 +118,71 @@ shown on the node and editable in the inspector. would come back to life once the port returns. - **Structs**: several named values travelling as one, so a bundle needs one edge instead of one per part. `Pack` declares its fields as inputs, `Expand` reads them back out as outputs, a - behavior port may be declared `struct` – which is how a bundle crosses a call – and a + behavior port may be declared `struct` — which is how a bundle crosses a call — and a **variable** may be one, which is how a bundle is stored. A `Pack`/`Expand` can take its fields from a variable that already has the shape (**From variable**: a struct variable, or the first - struct item a list holds – a list declares nothing, so its value is the only source) instead of + struct item a list holds — a list declares nothing, so its value is the only source) instead of having them typed again; that adds the fields it does not have yet as one undo step and leaves the ones it has alone. Writing a struct variable keeps the fields it declares: what arrives fills the ones it names, the rest stay put (`mergeStruct`), so - one node can set part of a bundle without erasing it – "the same piece, one row lower" is a + one node can set part of a bundle without erasing it — "the same piece, one row lower" is a `Pack` of that one field. Depth is bounded by the *value*, not by convention: a list item is a primitive, a tagged colour, texture or state machine, or a struct of flat ones (`listItems.ts`), a struct field says what it holds (`StructFieldKind`, primitives plus `list`), and the list a *field* holds takes flat items only. Forbidding just that one combination is what keeps the shapes finite: `list → struct → primitive` and `struct → list → primitive` are the two deepest a stored value - goes – exactly what `asStoredValue` lets through an untyped slot – so the deep compare and the + goes — exactly what `asStoredValue` lets through an untyped slot — so the deep compare and the editor stay finite. The List tab **offers only the kinds its slot can hold** (`ListAccess.kinds`): on a list of its own all six, on a list a struct field holds the five flat ones. Offering the - struct there would add an item the next write drops – which reads as the menu entry doing nothing. A live entity is + struct there would add an item the next write drops — which reads as the menu entry doing nothing. A live entity is an object on a data edge too, so it carries a `Symbol` brand (`ENTITY_HANDLE`) that no struct field name can impersonate; nothing may hand a property value to `String()` (`textOf` instead), or a struct would read as `[object Object]`. A struct port has no inline value (its parts come - from a `Pack`), and compatibility is per *kind*, not per shape – any struct fits any struct port. + from a `Pack`), and compatibility is per *kind*, not per shape — any struct fits any struct port. - **Loops**: `Repeat` runs its `body` output a number of times, counting `index` up from 0, and continues at `done`. Data nodes are cached for the length of one event pulse, but a loop round drops that cache, so an expression built on `index` really changes with it. That is what makes a graph able to work through a list: the Tetris example draws its 200 board cells that way, `Object by name` taking the name the round put together (`"c" + index`). Two guards, not one: a pulse may take 100 000 steps (a loop legitimately runs long), but exec edges may only nest - 1000 deep – that is what catches a flow feeding back into itself before the call stack does. + 1000 deep — that is what catches a flow feeding back into itself before the call stack does. - **A behavior as a node** (nesting): a behavior that has a **Start** and an **End** node can be run as a step of another behavior's graph. Every input declared on the start node becomes an input port of the calling node, every output declared on the end node one of its outputs; the node is drawn under the behavior's own name, and the palette lists it there too, so a call is - one pick. Declaring a port keeps both sides in step – renaming carries the typed-in value along, + one pick. Declaring a port keeps both sides in step — renaming carries the typed-in value along, removing takes the edges hanging on it in *every* graph that calls the behavior. A call is a **call, not an inlining**: the nested graph runs in its own pulse cache with a fresh copy of its variable defaults, so values travel in and out through the ports and nothing else. That makes a - call repeatable and keeps a nested behavior free of hidden state – for state, use a behavior + call repeatable and keeps a nested behavior free of hidden state — for state, use a behavior object in the scene (see *Cross-object state* below). A behavior that would run itself raises an error instead of looping; there is one start and one end node per behavior. Three ways in and out of a call: **drag** a behavior out of the asset panel onto the graph and it lands as a call where you let go; **double-click** a call node to open the graph it runs. The - way down stays above the graph as a **breadcrumb** (`Game › Move › Other`): click a crumb to go + way down stays above the graph as a **breadcrumb** (`Game > Move > Other`): click a crumb to go back there; the first one returns to where you started and the bar goes away. Opening a behavior - any other way – the asset panel, the scene, the picker – ends the trail. And a call that cannot - do anything **says so on the node** – no behavior picked, the behavior deleted, + any other way — the asset panel, the scene, the picker — ends the trail. And a call that cannot + do anything **says so on the node** — no behavior picked, the behavior deleted, or no start node in it. The last one is the one worth naming: without a start node there is no way in, so the call is skipped and the flow simply carries on at its `exec` output. A missing - *end* node is not a defect – a call is always synchronous, "done" is the nested flow running out, + *end* node is not a defect — a call is always synchronous, "done" is the nested flow running out, and the end node only decides what comes back. - **Cross-object state**: a behavior's variables *are* the properties of its object, so another graph reads and writes them with `Object by name` → `Properties` of type *Behavior*. That is how the example game keeps one coin counter for the whole run: picking a coin up is one node, `coins` fed through `Add` back into itself. - **Mouse**: the Game tab feeds the pointer to the world in world units. `On click` fires for - the behavior whose owner was hit – colliders decide when there are any, otherwise the sprites + the behavior whose owner was hit — colliders decide when there are any, otherwise the sprites and texts of the owner and everything below it, so a button made of a box plus a label works as one. A switched-off screen cannot swallow clicks. A behavior **without an owner** has no shape that could be hit, so it takes every click inside the camera frame instead (the - `world x`/`world y` outputs say where) – that is the "click anywhere" listener. + `world x`/`world y` outputs say where) — that is the "click anywhere" listener. - **Idiomatic movement** (the demo's "Mover", five nodes): `On update → Move(Owner, dx = Axis input.x, dy = Axis input.y, scale = speed, dt = On update.dt)`, `speed` being a `Variables` node. - **Idiomatic platformer** (the example game's "Player"): `On update` adds `gravity · dt` to a `vy` variable and moves the owner by `(axis.x · speed, vy)` with *Collide with solids* on; `blocked y` while falling means standing, so `vy` goes back to 0 and `onGround` becomes true; - `On key down (Space)` sets `vy = −jumpSpeed` when `onGround`. + `On key down (Space)` sets `vy = -jumpSpeed` when `onGround`. - **Editing**: right-click the canvas for the searchable palette, drag port → port to connect (types are checked; a data input holds one link and is replaced), drag nodes, right-click a node or port to delete/disconnect, `Delete` removes the selection, wheel zooms. Everything is @@ -190,12 +190,12 @@ shown on the node and editable in the inspector. - **Mouse and selection work as in the scene view.** The **middle button pans** from anywhere, a node under the pointer included (shift-drag on empty canvas does too); the left button never pans. Dragging on empty canvas pulls a **rubber band** instead, which selects every node it - touches – nodes are large rectangles, so touching counts, unlike the scene view's test of an + touches — nodes are large rectangles, so touching counts, unlike the scene view's test of an object's position. Ctrl/Shift-click adds or removes single nodes, dragging any node of a - selection moves all of them, and `Delete` removes all of them – one undo step either way. + selection moves all of them, and `Delete` removes all of them — one undo step either way. - **Dragging an edge tells you where it would land** before you let go: the port it would connect to wears a white ring and the line snaps to it. Dropping on a node's *body* rather than a port - takes that node's first free compatible port – or, when none is free, the last compatible one, + takes that node's first free compatible port — or, when none is free, the last compatible one, whose link is then replaced. A drop into empty space opens the palette filtered to the node types this edge could reach at all, and the picked node arrives already wired; node and link go in as **one** undo step (`core/graph/dropTarget.ts` decides all of this, the canvas only draws @@ -204,12 +204,12 @@ shown on the node and editable in the inspector. - **A link can be hidden**, so a connection across the whole graph does not have to be drawn as a line across the whole graph. Right-click a port → *Hide link*; the curve is replaced by a short stub at each end, ending in a dot captioned with the node at the *other* end. The connection is - untouched – hiding is drawing only, and `folded` on the edge is the entire change. The curve + untouched — hiding is drawing only, and `folded` on the edge is the entire change. The curve comes back, dimmed, **while either of its two nodes is selected**, and always while a pulse is travelling it, so the live trace never loses its thread. Clicking a dot selects the node at the far end, which both answers "where does this go?" and lends the curve back; right-clicking it offers *Show link* and *Disconnect* (`editor/graph/folding.ts` holds the - visibility rule, the canvas only draws it). **A port's hidden links share one mark** – drawn per + visibility rule, the canvas only draws it). **A port's hidden links share one mark** — drawn per link, several of them would stack stub, dot and caption on the very same pixels. The mark then grows a little, is captioned `Add +2`, and one click selects *every* far end at once, which is also what lends all of its curves back; the menu counts what it acts on (*Show 3 links*), and @@ -219,7 +219,7 @@ shown on the node and editable in the inspector. acts on the whole selection as one undo step. Every link of one side then meets at a single point on the title bar, and its hidden links share one mark per side. It narrows to its title (never wider than an open node, where a long title is cut). A collapsed node has no - ports to grab, so a link dropped on it goes nowhere – expand it to wire it. Like hiding a link, + ports to grab, so a link dropped on it goes nowhere — expand it to wire it. Like hiding a link, this is drawing only: `collapsed` on the node is saved with the project. A collapsed `Variables` or `Properties` node, titled just that while open, is titled by what it does, since its ports are out of sight: @@ -227,9 +227,9 @@ shown on the node and editable in the inspector. only read, else *Set speed* / *Get speed*, and *No variables* when it has none. - **Header bar** (the same one the Texture and Container tabs have): the behavior picker, **Fit** (which zooms out far enough to hold every node, measuring each by its own drawn - height – `editor/framing.ts`), and the size of the graph as "N nodes · M links". + height — `editor/framing.ts`), and the size of the graph as "N nodes · M links". - **Snap** (header checkbox, off by default): a drag drops each node it carries on the nearest - 24 px grid line – the same grid the background draws. It takes effect on the next drag only, so + 24 px grid line — the same grid the background draws. It takes effect on the next drag only, so switching it on never shuffles a graph that is already laid out, and a node nobody drags never moves. Every node of a selection snaps **for itself**, so dragging a group is how you line one up; the offset is a decision about rectangles and lives with the others in `editor/framing.ts`. @@ -237,9 +237,9 @@ shown on the node and editable in the inspector. or one of its nodes shows that graph; a selection about anything else (a sprite, nothing at all) leaves the tab on the behavior you were editing, because checking a sprite's position must not empty your work. Only a behavior that no longer exists is dropped, and then the tab - falls back to another one – the picker is how you switch on purpose. + falls back to another one — the picker is how you switch on purpose. - **Live view**: Play always switches to the Game tab; switch to Behavior while playing and - the exec edges and nodes that just ran light up – the same canvas draws editor and game, so + the exec edges and nodes that just ran light up — the same canvas draws editor and game, so this costs nothing extra. Each fire of an exec edge also sends a bead along it (0.3 s, at most one per 0.15 s and edge): a key press is a single bead, `On update` an even chain. Beads run on game time, so a paused game holds them where they are. Every data output shows, in the same colour, the value it last diff --git a/Colliders.md b/Colliders.md index 46b3951..a2ba41f 100644 --- a/Colliders.md +++ b/Colliders.md @@ -1,5 +1,5 @@ A **collider** is an asset (`shape` box/circle, `width`/`height` or `radius`) placed like any -other object – typically as a child of the sprite it belongs to, so its position is the offset +other object — typically as a child of the sprite it belongs to, so its position is the offset from that *body* and it moves with it. Right-click an object in **Scene objects** → *Add collider* to get one with one click: a box collider sized like the sprite, parented at its centre. Colliders can be reused across objects, overridden per instance @@ -11,19 +11,19 @@ every frame it tests all collider pairs (world positions applied; touching edges and sorts them into *enter*, *stay* and *exit*. Detection never pushes anything apart. A behavior receives `On collision enter/stay/exit` for the colliders of its **owner** (its parent; itself when top-level): the outputs are `other` (the other body), `other collider` -and `collider` (the own one). `Is touching(a, b)` answers the same question on demand – an +and `collider` (the own one). `Is touching(a, b)` answers the same question on demand — an object stands for its collider children, a collider for itself. The geometry lives in `src/core/properties/collider.ts` so editor and runtime agree on it. **Blocking** is opt-in per movement: the `Move` node has a **Collide with solids** setting (default off). When enabled it moves an object one axis at a time and, after each axis, pushes -its *solid* colliders back to just touch any solid collider they ran into – so the other axis +its *solid* colliders back to just touch any solid collider they ran into — so the other axis still moves and the object slides along walls. Its `blocked x` / `blocked y` outputs tell which axes hit (always `false` with the setting off). A collider with **Trigger (no blocking)** enabled (`isTrigger`, drawn amber instead of green) still produces collision events but never blocks. An overlap that already exists is never resolved (no jolt when objects start stacked or inside a wall) but also never deepened, so such an object can -always leave. A move longer than the thinnest collider involved is split into sub-steps – a +always leave. A move longer than the thinnest collider involved is split into sub-steps — a fast object cannot jump across a thin wall and end up on the other side. Colliders of the moving object and of anything parented under it travel with it and cannot block it. `Move` without the setting and `Set position` ignore colliders entirely. diff --git a/Concepts.md b/Concepts.md index de8d847..5ddec8c 100644 --- a/Concepts.md +++ b/Concepts.md @@ -3,25 +3,25 @@ | **Asset** | Named thing with a type (`scene`, `camera`, `sprite`, `text`, `behavior`, `collider`, `container`, `texture`, `animation`, `statemachine`) and typed properties. Assets have **no position**. | | **Texture** | An uploaded image. Scenes, textures, animations and state machines are the four types that cannot be placed: a texture has no position, it is *worn* by a sprite through the ordinary property `texture`. See [Textures](Textures). | | **State machine** | A named set of **states**, edited as nodes in its own tab. A behavior puts it into a state with `Set state` and branches on it with `State branch`; each behavior instance keeps its own current state. See [State machines](State-machines). | -| **Text** | An asset that draws a line of text (`text`, `fontSize`, `color`, `align`) – titles, HUD readouts, button labels. Its content is an ordinary property, so a `Properties` node of type Text with `text` switched to Set changes it while playing. | -| **Enabled** | Every placeable asset has the switch `enabled` (default on). A disabled object is not drawn, does not collide and its behavior does not run – **and neither does anything parented below it**. Switching one instance off is a normal override, which makes a container a *screen*: switch it on to show it, off to hide it. From a graph, `Properties` with `enabled` switched to Set does it, and `Is enabled` asks (including the parents). | +| **Text** | An asset that draws a line of text (`text`, `fontSize`, `color`, `align`) — titles, HUD readouts, button labels. Its content is an ordinary property, so a `Properties` node of type Text with `text` switched to Set changes it while playing. | +| **Enabled** | Every placeable asset has the switch `enabled` (default on). A disabled object is not drawn, does not collide and its behavior does not run — **and neither does anything parented below it**. Switching one instance off is a normal override, which makes a container a *screen*: switch it on to show it, off to hide it. From a graph, `Properties` with `enabled` switched to Set does it, and `Is enabled` asks (including the parents). | | **Scene object** | An asset placed into a scene: a link to the asset + a **position** (`x`, `y`, `z`) + sparse **overrides** of the asset's properties + an optional **parent** (another object of the scene). Scenes cannot be placed. | -| **Container** | A reusable group ("prefab"): the asset holds a **template** of objects (asset link, local position, overrides, parent links) centred on its origin. Placing a container stores the container object **and nothing else** – its content is resolved from the template on every read, so a template edit reaches every instance at once. **Create container** turns the current selection into such an asset: the selected objects move into the template and the scene keeps one placed container at their centre (world positions unchanged). The template is edited in the **Container** tab (see [Containers](Containers)) or from an instance through **Apply to container**. | +| **Container** | A reusable group ("prefab"): the asset holds a **template** of objects (asset link, local position, overrides, parent links) centred on its origin. Placing a container stores the container object **and nothing else** — its content is resolved from the template on every read, so a template edit reaches every instance at once. **Create container** turns the current selection into such an asset: the selected objects move into the template and the scene keeps one placed container at their centre (world positions unchanged). The template is edited in the **Container** tab (see [Containers](Containers)) or from an instance through **Apply to container**. | | **Selection** | One or more scene objects. Click selects, `Ctrl`/`Shift`-click adds/removes, dragging on empty scene space draws a rubber band (objects whose position lies inside are selected). Dragging a selected object moves the whole selection; `Delete` removes it; the inspector offers **Create container** and **Remove** for several objects. | | **Behavior** | Logic as an asset: a **node graph** plus the **variables** it declares. Placed like anything else (a diamond symbol in the scene); its parent is the **owner** the graph acts on. | -| **Run in editor** | A switch only behaviors have (default off). With it on the behavior keeps running while the project is **not** playing – and unlike Play it then edits the *saved* model: what it moves or writes is a real change, autosaved and not on the undo stack. See *Running without Play*. | +| **Run in editor** | A switch only behaviors have (default off). With it on the behavior keeps running while the project is **not** playing — and unlike Play it then edits the *saved* model: what it moves or writes is a real change, autosaved and not on the undo stack. See *Running without Play*. | | **Collider** | A shape (box or circle) for collision detection, placed as a **child** of the object it belongs to (its *body*). Drawn as a green outline in edit mode only. | -| **Override** | A per-object value that replaces the asset's value for one property. Only changed keys are stored; the inspector paints their label gold and offers a reset (↺) and an **apply** (↑) that writes the value into the asset instead. Both appear only where the value really differs from its base – an override carrying the asset's own value is none. | -| **Default** | Every property has a schema default. Asset values that differ from it are marked gold in the inspector and can be reset (↺) the same way an override can. The asset's name in the **Assets** panel carries the same gold while any of them does (`assetDeviates`, `src/core/properties/deviation.ts` – the inspector marks the rows of that same schema, so the list cannot claim a deviation the inspector then fails to show). A behavior's variables are not in it: their default is declared on the variable itself, so the asset value cannot differ from it. | -| **Instance** | An object resolved from a placed container's template. Its **values** can be edited – position, name, property overrides – and only what actually deviates is stored, as a sparse `instanceEdits` entry on the container object keyed by the template path. Everything else keeps following the template. Its **structure** cannot: it can not be removed, re-parented or given a collider, and nothing of the scene can be parented into it. ↺ ▾ resets to the nearest base, which inside a container is the **template as the containers in between leave it**; ↑ ▾ offers **Apply to container** (write the value into the template, where every other instance still at the old value picks it up) and/or **Apply to asset** (which drops the template's override as well). With one base left the menu collapses to a plain ↺ / ↑, with none the buttons disappear. Gold marks the same thing the buttons offer. | +| **Override** | A per-object value that replaces the asset's value for one property. Only changed keys are stored; the inspector paints their label gold and offers a reset (↺) and an **apply** (↑) that writes the value into the asset instead. Both appear only where the value really differs from its base — an override carrying the asset's own value is none. | +| **Default** | Every property has a schema default. Asset values that differ from it are marked gold in the inspector and can be reset (↺) the same way an override can. The asset's name in the **Assets** panel carries the same gold while any of them does (`assetDeviates`, `src/core/properties/deviation.ts` — the inspector marks the rows of that same schema, so the list cannot claim a deviation the inspector then fails to show). A behavior's variables are not in it: their default is declared on the variable itself, so the asset value cannot differ from it. | +| **Instance** | An object resolved from a placed container's template. Its **values** can be edited — position, name, property overrides — and only what actually deviates is stored, as a sparse `instanceEdits` entry on the container object keyed by the template path. Everything else keeps following the template. Its **structure** cannot: it can not be removed, re-parented or given a collider, and nothing of the scene can be parented into it. ↺ ▾ resets to the nearest base, which inside a container is the **template as the containers in between leave it**; ↑ ▾ offers **Apply to container** (write the value into the template, where every other instance still at the old value picks it up) and/or **Apply to asset** (which drops the template's override as well). With one base left the menu collapses to a plain ↺ / ↑, with none the buttons disappear. Gold marks the same thing the buttons offer. | | **Unpack** | Writes a placed container's content into the scene as ordinary objects (hierarchy context menu, or the inspector's *Content* section). The ids survive it, so the selection does; from then on the structure is editable and the objects no longer follow the template. | | **Edit mode** | The Scene tab draws the project model; panels, grid, gizmos and drag-to-move are active. | -| **Play mode** | The editor switches to a **runtime copy** of the project (same ids). A `World` is instantiated from its active scene (one entity per scene object, resolved properties as mutable state) and shown in the **Game** tab. Systems tick it and mirror their changes into the copy; every panel stays open and edits the copy, which the world picks up immediately. **Stop discards world and copy** – the saved model is never touched. | +| **Play mode** | The editor switches to a **runtime copy** of the project (same ids). A `World` is instantiated from its active scene (one entity per scene object, resolved properties as mutable state) and shown in the **Game** tab. Systems tick it and mirror their changes into the copy; every panel stays open and edits the copy, which the world picks up immediately. **Stop discards world and copy** — the saved model is never touched. | -**The camera defines what is rendered.** A camera asset has a view size (`width` × `height`, -default 640×360) and a `zoom`; the frame it covers in world units is `size / zoom`. The Scene -tab draws that frame as an outline. The Game tab renders only that frame – fitted into the -view with black letterboxing – and objects outside it are culled. Of several cameras the first +**The camera defines what is rendered.** A camera asset has a view size (`width` x `height`, +default 640x360) and a `zoom`; the frame it covers in world units is `size / zoom`. The Scene +tab draws that frame as an outline. The Game tab renders only that frame — fitted into the +view with black letterboxing — and objects outside it are culled. Of several cameras the first one in hierarchy order that is **switched on** (`enabled`, its parents included) is the active one, so switching it off hands the view to the next; with none on the tab stays empty and says *No active camera*. Without playing the tab previews the scene through that same camera, so it @@ -38,7 +38,7 @@ from texture* and *Delete…*. In container*, *Unparent*, *Expand all* / *Collapse all* and *Remove*. **The console.** Under whichever centre tab is showing sits the **Console** panel, always -visible and resized by the divider above it – dragged all the way shut it keeps its own header +visible and resized by the divider above it — dragged all the way shut it keeps its own header to be dragged open by. It mirrors everything that reaches the browser console: a `Log` node's message, Pixi's warnings, an uncaught error, and the editor's own notices (a failure in red, a hint in plain text; the *Playing · …* status line is a mode rather than an event and stays out). @@ -51,13 +51,13 @@ Scene tab shows the live positions (symbols, gizmos and dragging included), the inspector show and edit the running objects, the graph can be changed, undo/redo work on the play session's own history. All of it happens in the runtime copy: values a behavior changes appear as overrides (gold, ↺ resets to the asset value), objects a graph destroys vanish from -the lists, objects a graph spawns (`Spawn` node) appear in them – selectable, editable, still -only in the runtime copy –, and Esc/Stop brings back exactly what was saved. Autosave pauses +the lists, objects a graph spawns (`Spawn` node) appear in them — selectable, editable, still +only in the runtime copy —, and Esc/Stop brings back exactly what was saved. Autosave pauses while playing. **Pausing.** The toolbar's *Pause* button (or `Ctrl+Enter`) holds the running world: it is no longer ticked, so behaviors, timers and collisions stand still and the world clock stops with -them. It is a sub-state of play mode, not a third mode – the runtime copy, the panels and the +them. It is a sub-state of play mode, not a third mode — the runtime copy, the panels and the keyboard behave exactly as while playing, and everything the editor can do during play it can do during a hold. Keys and clicks that arrive while it is held are **dropped** rather than queued, so resuming does not fire a frame's worth of input at once. Stop clears the hold, so a @@ -67,24 +67,24 @@ paused game can never outlive its run. project is not playing (`src/editor/EditorRun.ts`, ticked by the Editor next to the play session). The switch is an ordinary property, so it can be set on the behavior asset and overridden per object. What it runs is a `World` over the **saved** model, and that is the whole difference to -Play: a position or variable it changes is a real edit – it is autosaved, the panels show it, and +Play: a position or variable it changes is a real edit — it is autosaved, the panels show it, and **undo does not take it back**. Which is why the switch is off by default and why it belongs on behaviors that arrange or build content rather than on ones that simulate. **Input goes to the tab you are looking at.** While the **Scene** tab is on screen the keyboard and -the mouse are the editor's – typing a name or walking the hierarchy with the arrow keys must not +the mouse are the editor's — typing a name or walking the hierarchy with the arrow keys must not steer a game at the same time. In the **Game** tab, where no key means anything to the editor, they reach the run instead: `On key …`, `Key is down` and `On click` work there, and the tab says so in its top-left corner. Leaving the tab (or focusing a field in it) hands the keyboard back and lets every held key go, so nothing keeps walking with nobody pressing anything. What the run does *not* get: -- **structural changes to the content of a placed container** – such content is resolved from the +- **structural changes to the content of a placed container** — such content is resolved from the template, not stored (see [Containers](Containers)), and the editor run must not bake it out, since that is itself an edit. Its behaviors do run, and what they move or set is kept as an `instanceEdits` entry on the instance; but a `Destroy` of resolved content is dropped from the running world without being saved, and comes back the next time the world is rebuilt. -- **a second life** – only the behaviors of the **loaded** scene run, and only in edit mode; Play +- **a second life** — only the behaviors of the **loaded** scene run, and only in edit mode; Play stops the editor run, since that world runs every behavior anyway. An exception in a graph, or a `Stop game` node, ends the run and puts the reason in the notice line instead of repeating it every frame. diff --git a/Containers.md b/Containers.md index e090874..1134401 100644 --- a/Containers.md +++ b/Containers.md @@ -1,8 +1,8 @@ -The **Container tab** shows one container's template with the *same* viewport the Scene tab uses – +The **Container tab** shows one container's template with the *same* viewport the Scene tab uses — `ContainerCanvas` subclasses `SceneViewport` and swaps nothing but which asset holds the objects, so picking, rubber band, dragging, gizmos, labels and parent lines are not written twice. It adds a cross at the **origin**, because that is the point the container is held by when placed: where the -content sits relative to it is the thing being authored. A template has no background of its own – +content sits relative to it is the thing being authored. A template has no background of its own — painting one would invent the scene it is not in. The **Scene objects** panel lists whatever the visible tab edits, so in the Container tab it lists @@ -15,7 +15,7 @@ a scene asset and a container asset are both `ObjectHolder`s, and every object c included) and the size of the template as "N objects". Placing needs no button of its own: an asset dragged from the panel onto this view goes into the template, and a container dragged onto its own template is refused while it hovers rather than at the drop. -Like the Behavior tab it follows the selection without being bound to it – the container asset, one of +Like the Behavior tab it follows the selection without being bound to it — the container asset, one of its placed copies or one of its template objects shows it, anything else leaves the tab on what you were editing, and only a deleted container makes it fall back to another one. Selecting a template *object* also switches to the tab, the rule a graph node and a texture frame already follow: it @@ -23,10 +23,10 @@ exists in no other view. Selecting the asset itself does not. Placing a container anywhere creates **one** object. Its content is never copied: `resolveObjects` (`src/core/model/instance.ts`) weaves the template in whenever the holder is read, one level per -nesting step, and the resolved objects get derived ids – `~