| name | dora-engine-coding |
| description | Dora SSR coding rules for game/workspace projects; prevents browser DOM/Canvas/Node.js code in Dora engine scripts and forces Dora API lookup before using unfamiliar engine APIs. |
| always | true |
Dora Engine Coding Rules
These rules apply whenever writing, fixing or reviewing code for a Dora SSR workspace/game project.
What Runtime This Is
- Dora runtime game code is executed by the Dora engine, not by a browser or Node.js.
- The normal runtime entry is the project-root
init.ts or init.lua.
- TypeScript/TSX is source code and is transpiled to Lua. The engine executes the Lua output.
- Prefer editing
.ts/.tsx source files. Do not hand-edit generated .lua unless the task is specifically about Lua output or the project is Lua-only.
- If a project contains web files (
index.html, DOM Canvas code, etc.) and also has init.ts, treat init.ts as the Dora runtime entry unless the user explicitly asks for a browser/Web IDE frontend.
Runtime vs Browser Decision
Before writing code, decide the target:
- Dora game/runtime script: use Dora engine APIs and this skill.
- Web IDE/frontend page: browser APIs may be valid, but only if the user explicitly asks for Web IDE/frontend/browser work.
For Dora runtime scripts, never generate:
document, window, HTMLElement, HTMLCanvasElement, CanvasRenderingContext2D
requestAnimationFrame, browser alert, DOM event listeners
setInterval, setTimeout, NodeJS.Timeout
- npm-only packages, Node
fs/path/process
- HTML
index.html as the runtime game entry
If those APIs already exist in a Dora runtime project, treat them as incompatible code to rewrite.
Hard Rule: Do Not Guess Dora APIs
Dora has its own TypeScript definitions such as Dora.d.ts. Do not invent names/signatures.
Lookup protocol
Call search_dora_doc before using any Dora API that is not shown in the baseline section below or not already used correctly in the project.
For implementation tasks, keep lookup bounded so it does not replace implementation:
- When the runtime reports a fresh project, start from the disclosed short code file or create the requested entry directly (default to
init.ts for a Dora TypeScript task). Consult relevant tutorials or APIs when they help, and prefer an early build so subsequent work can use compiler feedback.
- If the requested game fits the baseline APIs below, do not search first. Inspect the entry, implement the smallest complete playable loop, then build.
- Before the first edit, use at most one batched
search_dora_doc call for all genuinely unfamiliar APIs. Do not issue separate searches for APIs already covered by the baseline.
- After that lookup, edit and build. Search again only for a concrete compiler or runtime error that cannot be resolved from the returned signature.
- Do not use runtime command tools for project discovery, file inspection, or API/type-definition lookup. Use the corresponding file/search tools.
- Prefer build diagnostics over speculative design research. Do not spend repeated Agent steps comparing optional architectures before a playable baseline exists.
- After editing source, build before another
search_dora_doc call. A later search is justified only by a concrete build/runtime diagnostic that requires an unfamiliar API.
Use:
programmingLanguage: "ts" for .ts files.
programmingLanguage: "tsx" for DoraX/JSX files.
docType: "dora-api" for signatures/types.
docType: "dora-tutorial" for usage examples when signatures alone are not enough.
docLanguage: "zh" if the user is Chinese or Chinese docs are preferred; otherwise "en".
After searching:
- Read the returned signature/JSDoc carefully.
- Use the exact module name, export name, parameter order, return type, and enum value.
- If the result is incomplete, search narrower terms or read the referenced
.d.ts/tutorial file.
- If search fails, say the API was not found and implement a simpler baseline Dora version instead of guessing.
Search especially for:
- physics/collision:
PhysicsWorld, Body, BodyDef, FixtureDef, Sensor
- ECS:
Entity, Group, Observer, component queries
- UI/components/layout:
Button, Menu, AlignNode, UI controls
- audio:
Audio, AudioSource, buses/effects
- async/coroutines:
thread, threadLoop, sleep, once, loop, scheduler jobs
- scene/camera:
Director, Camera, Director.ui, Director.entry, scheduler APIs
- resources/files:
Content, Cache, Path, asset loading/saving
- sprite/animation/actions:
Sprite, Model, Playable, Action, Move, Scale, Sequence, animation slots
- particles/effects/video:
Particle, EffekNode, VideoNode, TIC80Node
- tile maps:
TileNode
- Platformer helpers:
Platformer, PlatformWorld, platformer bodies/units
- vector graphics/custom rendering:
DrawNode, VGNode, nvg
- input beyond simple keyboard:
Mouse, touch, controller, IME, node slots
Example tool searches:
search_dora_doc pattern="PhysicsWorld|Body|FixtureDef" docType="dora-api" programmingLanguage="ts" limit=8
search_dora_doc pattern="Button|Menu|AlignNode" docType="dora-api" programmingLanguage="ts" limit=8
search_dora_doc pattern="AudioSource|Audio" docType="dora-api" programmingLanguage="ts" limit=8
search_dora_doc pattern="thread|sleep|threadLoop" docType="dora-api" programmingLanguage="ts" limit=8
Module Import Rules
Use Dora runtime modules, not browser packages:
- Core engine APIs:
import { ... } from 'Dora';
- JSX/DoraX UI syntax: use
.tsx and import from DoraX. Use toNode() for one-shot scene creation, or createRoot() with signal() for dynamic TSX diff rendering.
- DoraX dynamic roots track the signals read during render. Use stable
key values for dynamic sibling lists, and call root.unmount() when a dynamic root is no longer needed.
- Platformer framework:
import * as Platformer from 'Platformer'; or exact exports found by API search.
- ImGui tools/UI:
import * as ImGui from 'ImGui'; plus enum exports from ImGui when needed.
- Vector graphics:
import * as nvg from 'nvg'; when using VGNode/NanoVG APIs.
- Do not import React from npm for DoraX runtime code.
TypeScript Hygiene
- Do not use
any in Dora runtime TypeScript. Use concrete types, unknown with narrowing, generics, or exact unions/records.
- Do not use bare
null in runtime code. Use undefined/omitted fields for absent Lua values.
- In
edit_file content, indent with ordinary space characters. Never emit the two literal characters \\t at the beginning of a source line; they are not indentation and will make TypeScript parsing fail.
- Dora factory namespaces are values, not annotation types. Annotate instances with
Vec2.Type, Color.Type, Label.Type, DrawNode.Type, and the corresponding X.Type; do not use bare X or ReturnType<typeof X>.
- The Dora TypeScript-to-Lua subset does not support
Math.hypot, Math.random, or Math.imul. Use Math.sqrt(x*x + y*y), inject randomness, and use ordinary bounded arithmetic.
- Lua arrays cannot represent
undefined/null elements. Do not build arrays from optional factory results such as Label(...); narrow each optional result before inserting it, or handle the labels individually.
Coordinate System
- Dora uses a left-handed coordinate system, positive X is right and positive Y is up.
Director.entry, Director.ui, and similar root nodes use screen-center origin: (0, 0) is the screen center.
- Do not use browser-style top-left coordinates unless explicitly converting from that space.
- For game screen adaptation, read
ts/adapting-to-screen.md directly before designing responsive layout behavior.
Parent and child local coordinates
- For a parent node with nonzero
width and height, anchor selects which point of the parent's own rectangle is placed at the parent's position. It does not move the local origin used by the parent's children.
- A child is positioned in the parent's rectangular local space, whose
(0, 0) is the rectangle's bottom-left corner. Even with the default anchor = Vec2(0.5, 0.5), a child at Vec2.zero is at that bottom-left corner, not at the parent's center.
- To center a child in the parent rectangle, place it at
Vec2(parent.width / 2, parent.height / 2).
- To align a child with the parent's anchor point, place it at
Vec2(parent.width * parent.anchor.x, parent.height * parent.anchor.y).
Keep a full-screen drawing root unsized when its children should use screen-center coordinates:
const screen = Node();
screen.addTo(Director.entry);
const background = DrawNode();
const {width, height} = View.size;
background.drawPolygon([
Vec2(-width / 2, -height / 2),
Vec2(width / 2, -height / 2),
Vec2(width / 2, height / 2),
Vec2(-width / 2, height / 2),
], Color(20, 30, 50, 255));
background.addTo(screen);
For a sized parent, position children in its bottom-left-based local rectangle:
panel.size = Size(320, 180);
panel.position = Vec2.zero;
label.position = Vec2(panel.width / 2, panel.height / 2);
Inside that sized node's touch handler, touch.location uses the same bottom-left-based local space. Convert it before comparing against centered model coordinates:
const local = touch.location;
const centered = Vec2(
local.x - panel.width * panel.anchor.x,
local.y - panel.height * panel.anchor.y
);
Do not give a full-screen drawing root a size merely for adaptation. Add a separate sized input node when a real hit area is required, and deliberately use or convert its local coordinates as shown above.
Input coordinate and enum baseline
These names come directly from the built-in Dora.d.ts; do not spend a search step rediscovering them:
touch.location: position in the receiving node's local coordinate system.
touch.viewLocation: position in the current view's logical coordinate system.
touch.worldLocation: position in world coordinates. Prefer node-local handlers with touchEnabled = true and the node's real size over global manual hit testing.
- The return key is
KeyName.Return, not KeyName.Enter. Number keys are KeyName.Num0 through KeyName.Num9; letters and arrows use names such as KeyName.W, KeyName.Left, and KeyName.Up.
- Controller buttons use
ButtonName.A/B/X/Y, Back, Start, Up/Down/Left/Right, LeftShoulder/RightShoulder, and LeftStick/RightStick. Axes use AxisName.LeftX/LeftY/RightX/RightY/LeftTrigger/RightTrigger.
- Controller calls are
Controller.isButtonDown/Up/Pressed(controllerId, ButtonName.X) and Controller.getAxis(controllerId, AxisName.LeftX).
- For a centered playfield of width
w and height h, every model-space object and configured region must initially lie within [-w/2,w/2] × [-h/2,h/2]. With Dora's positive-Y-up convention, a bottom paddle has negative Y, bricks near the top have positive Y below h/2, launch-up velocity is positive Y, and falling velocity is negative Y. Check these signs and bounds before the first build.
Scheduling Semantics
- A Dora
Node has one schedule slot. Calling node.schedule(...) again replaces that node's previous scheduled callback; it does not add another concurrent callback.
- Put update and render work in one scheduled callback, or attach independent callbacks to distinct child nodes.
- Runtime probes must never add another schedule to the node whose game loop is being tested. Observe state from the existing callback or use a separate probe node, then remove the probe after validation.
Baseline Dora APIs for Simple 2D Prototypes
For a tiny runtime prototype, this baseline is allowed without extra API search:
import { Color, Director, DrawNode, KeyName, Keyboard, Label, Node, Vec2, View } from 'Dora';
const root = Node();
root.addTo(Director.entry);
const draw = DrawNode();
draw.addTo(root);
const title = Label('sarasa-mono-sc-regular', 20);
if (title) {
title.text = 'Dora Game';
title.y = View.size.height / 2 - 40;
title.addTo(root);
}
let x = 0;
root.schedule((dt) => {
if (Keyboard.isKeyPressed(KeyName.Left)) x -= 240 * dt;
if (Keyboard.isKeyPressed(KeyName.Right)) x += 240 * dt;
draw.();
draw.((x, ), , (, , , ));
;
});
Baseline mapping:
- Scene root:
Director.entry, not HTML body/canvas.
- Main loop:
Node.schedule((dt) => false) or node.onUpdate, not requestAnimationFrame.
- Timers: accumulate
dt in scheduled updates, not setInterval/setTimeout.
- Primitive drawing:
DrawNode.drawDot, drawSegment, drawPolygon.
DrawNode has no drawRect; draw rectangles with four Vec2 vertices passed to drawPolygon.
- Text:
Label('sarasa-mono-sc-regular', size) and guard because Label(...) may return undefined.
- Text anchoring uses the inherited
anchor = Vec2(x, y) property. There is no anchorX. Do not assign string values such as 'Left', 'Center', or 'Right' to Label.alignment.
- For safe screen-edge HUD text, use
View.size, put left labels at -View.size.width / 2 + margin with anchor = Vec2(0, 0.5), and right labels at View.size.width / 2 - margin with anchor = Vec2(1, 0.5).
- If the playfield is scaled and draws a top wall/frame, derive HUD Y from the rendered field top rather than guessing from
View.size alone. Put the label baseline below the rendered wall by at least the wall thickness plus one font line height, and set HUD labels to a foreground z above the field DrawNodes. Confirm the initial frame visually; a successful lifecycle check cannot detect clipped or covered text.
- Give title/subtitle or state/instruction label pairs distinct Y positions with at least one full line-height of separation; never leave both at
(0, 0).
- Check overlay text against the initial player/game objects as well as against its sibling labels. For a centered starting actor, place both title and instruction lines together above or below that actor with a full line-height/object-radius gap; separating the two labels is insufficient if one line then covers the actor.
- Keyboard hold state:
Keyboard.isKeyPressed(KeyName.Left).
- One-frame key down/up:
Keyboard.isKeyDown(...) / .
Runtime API Coverage Map
Use this as a decision map. It is not a full API reference; exact signatures come from search_dora_doc.
| Need | Likely start | Lookup rule |
|---|
| Basic scene/game loop | Director.entry, Node.schedule | Baseline enough for tiny prototypes |
| Primitive drawing | DrawNode, Vec2, Color | Search for advanced vertices/VGNode |
| Sprites/assets | Sprite, Content, Cache, Path | Search before loading assets |
| Text/UI/layout | Label, Menu, Button, AlignNode, DoraX | Search before UI controls/layout |
| Keyboard/mouse/touch/controller | Keyboard, KeyName, Mouse, node slots | Search beyond simple keyboard |
| Actions/animation | Action, Move, Scale, Sequence, Playable, Model | Search exact signatures |
| Physics/collision | PhysicsWorld, Body, BodyDef, FixtureDef, Sensor | Always search first |
| ECS/game architecture | Entity, Group, components | Always search first |
| Audio | Audio, AudioSource | Always search first |
| Async/coroutines | thread, threadLoop, sleep, once, loop | Search when using coroutine/timing APIs |
| Tile maps | TileNode | Always search first |
| Particles/effects/video | Particle, EffekNode, , |
Implementation Workflow
- Inspect project files and locate the real runtime entry (
init.ts, init.lua, or an existing run script).
- Identify source language: TS/TSX vs Lua. Prefer TS/TSX source edits when present.
- Search Dora API before any non-baseline API use.
- Keep entry wiring real: the entry must import/instantiate/start the game logic.
- Avoid orphan modules: if creating classes/modules, export/import them correctly and ensure
init.ts uses them.
- When adding imports, choose Dora module names or project-root module paths from the init directory/search folders, not raw filesystem paths or
.//../ paths. TypeScript project imports use slash-separated paths such as import { Game } from 'game/Game'; Lua module cache names passed to requireProjectModule use dots such as "game.Game".
- For small prototypes, prefer one self-contained
init.ts first; refactor into modules only when it is already running or the user asks.
- Replace any
any or bare null introduced during implementation before building.
- Run
build on the changed file/project when available.
- Build success is not just the top-level
success field. Inspect per-file messages; if any message reports failure or diagnostics, fix them before finishing.
- If TS build says the Web IDE/transpile service is not connected, tell the user to open the Web IDE/keep Dora running, or use the existing project build path if available.
- When a runtime validation tool is available, follow its active skill and tool description exactly; do not assume it is a general-purpose shell.
- When several related fixes are in one authored file, combine them into one coherent edit when practical, then build immediately. Do not repeatedly edit one assertion at a time without rerunning the smallest relevant test.
- Diagnose authored game logic and test fixtures before inspecting generated Lua. Check fixture preconditions, automatic state transitions, time-step clamping, and terminal-state resets first.
- Files under
.agent/main are the main Agent's persistent memory. You may edit them deliberately to record durable project knowledge, user decisions, or a precise active checkpoint. Keep the existing structure, avoid duplicating transient progress, and let later consolidation merge the update with newer evidence. Memory-only edits do not require a project build. When proactively updating one memory file, read it once and apply one coherent edit that updates every affected section; do not spend separate decisions editing its status, architecture, decisions, and known issues one section at a time.
Engine Log
When diagnosing launch failures, runtime exceptions, engine-service errors, or explicit script print output, read the read-only virtual file @dora_full_logs.txt. Use a negative line range such as startLine: -200 to inspect the latest entries. This path is not part of the workspace and must not be edited or enumerated as a project file.
Game Runtime Acceptance
For a playable game, build success and running=true are necessary but not sufficient.
- When a runtime validation tool is available, prefer a pure TypeScript test module that exports a function returning a report whose first line is
passed or failed. Follow the active runtime-tool skill for loading it. Do not create a test entry, schedule, or marker file unless the test specifically needs a running scene.
- Treat a report whose first line is
failed as a failed command even if the Lua call itself completed. Fix the smallest authored logic/fixture issue, rebuild, and call requireProjectModule again.
- Keep failure reports bounded and useful: report at most 12 unique failures, never append the same assertion once per retry-loop iteration, and stop a fixture loop immediately when an iteration makes no progress.
- When an active runtime tool provides entry lifecycle helpers, start the real
init entry, verify it remains running across at least two observations, exercise at least one meaningful input/state transition when the harness can do so, then stop it and verify it is no longer running.
- Treat runtime state checks as headless validation only. They do not prove that game objects are visible, correctly layered, legible, or pleasant to play.
- Before claiming a game is complete, explicitly report visual/manual play status. If no visual inspection tool is available, mark visual validation as
not_run and ask the caller to launch the game for visual acceptance; never silently equate running=true with a visually correct game.
- When a visual inspection path is available, launch the real project, inspect the initial frame, trigger the primary input, inspect the changed frame, and stop cleanly. Confirm that the playfield, player-controlled object, hazards/targets, HUD, and overlays are visible in the intended z-order.
Review Checklist Before Finish
- Runtime entry actually starts the game/app.
- No browser DOM/Canvas/Node-only APIs remain in runtime scripts.
- Code imports from Dora runtime modules (
Dora, DoraX, Platformer, ImGui, nvg) as appropriate.
- TypeScript project imports use valid slash-separated Dora root/search-path module names (for example
game/Game), with no .//../ prefix or source/output file extension; requireProjectModule dependency cache names use dots (for example game.Game).
- No
any or bare null was added to Dora runtime TypeScript.
- Non-baseline Dora APIs were confirmed with
search_dora_doc or existing correct project code.
- Game loop uses Dora scheduling and
dt.
- Each node has at most one intended scheduled callback; validation probes do not replace the game loop.
- Rendering nodes are attached to
Director.entry or an existing Dora node.
- Direct
Director.entry/Director.ui coordinates use screen-center origin with positive X right and positive Y up.
- Children of sized parent nodes use the parent rectangle's bottom-left local origin; the parent's
anchor is not the children's coordinate origin.
- Input uses Dora input APIs and correct
KeyName enums.
- Build/transpile was run when possible and per-file messages were checked.
- The actual game entry was launched; initial and post-input frames were visually checked, or visual validation was explicitly reported as not run.
- Opaque background nodes are behind gameplay nodes, and HUD/overlays are above them.