@waica/cli 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/cli.js +184 -0
- package/dist/editor/assets/abap-08VXUWAP.js +1 -0
- package/dist/editor/assets/apex-BWPQTe0t.js +1 -0
- package/dist/editor/assets/azcli-Bc_sGQ0U.js +1 -0
- package/dist/editor/assets/bat-i0X4ZdIN.js +1 -0
- package/dist/editor/assets/bicep-B5-_aFwp.js +2 -0
- package/dist/editor/assets/cameligo-DMUM7wLl.js +1 -0
- package/dist/editor/assets/clojure-Cm7r79vr.js +1 -0
- package/dist/editor/assets/codicon-DCmgc-ay.ttf +0 -0
- package/dist/editor/assets/coffee-Ba7i2nA0.js +1 -0
- package/dist/editor/assets/cpp-C7h46wYY.js +1 -0
- package/dist/editor/assets/csharp-BKxtCVv1.js +1 -0
- package/dist/editor/assets/csp-bTuwJoIa.js +1 -0
- package/dist/editor/assets/css-DIMkf-bt.js +3 -0
- package/dist/editor/assets/css.worker-Fwwr_HdT.js +88 -0
- package/dist/editor/assets/cssMode-CQE-WdwL.js +4 -0
- package/dist/editor/assets/cypher-CVaqCwHa.js +1 -0
- package/dist/editor/assets/dart-onAF5SnQ.js +1 -0
- package/dist/editor/assets/dockerfile-DZFCIeNp.js +1 -0
- package/dist/editor/assets/ecl-D05T4iGw.js +1 -0
- package/dist/editor/assets/editor.worker-DsBDrWS5.js +12 -0
- package/dist/editor/assets/elixir-6RTg0lbw.js +1 -0
- package/dist/editor/assets/flow9-C5_-GSwl.js +1 -0
- package/dist/editor/assets/freemarker2-CYgdVabn.js +3 -0
- package/dist/editor/assets/fsharp-C8Ef5oNN.js +1 -0
- package/dist/editor/assets/go-C-y9NEjX.js +1 -0
- package/dist/editor/assets/graphql-fmXr3nnJ.js +1 -0
- package/dist/editor/assets/handlebars-DdeORCJD.js +1 -0
- package/dist/editor/assets/hcl-CpzslTdj.js +1 -0
- package/dist/editor/assets/html-DTIAGV84.js +1 -0
- package/dist/editor/assets/html.worker-B3Ztmmz2.js +495 -0
- package/dist/editor/assets/htmlMode-CRV3iBih.js +4 -0
- package/dist/editor/assets/index-Btzc6zxr.css +1 -0
- package/dist/editor/assets/index-Dhkqk31v.js +11016 -0
- package/dist/editor/assets/ini-sBoK_t0W.js +1 -0
- package/dist/editor/assets/java-BEtHBSE6.js +1 -0
- package/dist/editor/assets/javascript-dxBW86cl.js +1 -0
- package/dist/editor/assets/json.worker-PWQxM7ZO.js +51 -0
- package/dist/editor/assets/jsonMode-DFA5-FFK.js +10 -0
- package/dist/editor/assets/julia-Bri6UV-V.js +1 -0
- package/dist/editor/assets/kotlin-B0qVs-E9.js +1 -0
- package/dist/editor/assets/less-B9JPFI3C.js +2 -0
- package/dist/editor/assets/lexon-CfSJPG6W.js +1 -0
- package/dist/editor/assets/liquid-CScWaXYV.js +1 -0
- package/dist/editor/assets/lua-CsQS60Ue.js +1 -0
- package/dist/editor/assets/m3-D-oSqn_W.js +1 -0
- package/dist/editor/assets/markdown-Cimd5fb3.js +1 -0
- package/dist/editor/assets/mdx-Bvp84SqA.js +1 -0
- package/dist/editor/assets/mips-CIPQ_RoX.js +1 -0
- package/dist/editor/assets/msdax-DauUninz.js +1 -0
- package/dist/editor/assets/mysql-SOo6toE5.js +1 -0
- package/dist/editor/assets/objective-c-FvmIjYaQ.js +1 -0
- package/dist/editor/assets/pascal-DrH0SRf2.js +1 -0
- package/dist/editor/assets/pascaligo-D-ptJ9y-.js +1 -0
- package/dist/editor/assets/perl-oz_6vUea.js +1 -0
- package/dist/editor/assets/pgsql-DTj74zXo.js +1 -0
- package/dist/editor/assets/php-nr791fC2.js +1 -0
- package/dist/editor/assets/pla-CopQ2nXW.js +1 -0
- package/dist/editor/assets/postiats-43DmfD33.js +1 -0
- package/dist/editor/assets/powerquery-D3hlyOfw.js +1 -0
- package/dist/editor/assets/powershell-DmHpPYUd.js +1 -0
- package/dist/editor/assets/protobuf-C531GsRP.js +2 -0
- package/dist/editor/assets/pug-Z5eAx3Zn.js +1 -0
- package/dist/editor/assets/python-CG-OtvFa.js +1 -0
- package/dist/editor/assets/qsharp-DkqhCAOL.js +1 -0
- package/dist/editor/assets/r-BwWrilGY.js +1 -0
- package/dist/editor/assets/razor-r-iUEZPj.js +1 -0
- package/dist/editor/assets/redis-ClamHrr6.js +1 -0
- package/dist/editor/assets/redshift-DT7zqm-g.js +1 -0
- package/dist/editor/assets/restructuredtext-BYgofb2h.js +1 -0
- package/dist/editor/assets/ruby-DezsRK8O.js +1 -0
- package/dist/editor/assets/rust-DdL9SqIa.js +1 -0
- package/dist/editor/assets/sb-CcwsVR0C.js +1 -0
- package/dist/editor/assets/scala-DHpiXF5c.js +1 -0
- package/dist/editor/assets/scheme-BeGwcela.js +1 -0
- package/dist/editor/assets/scss-gp-XZpBa.js +3 -0
- package/dist/editor/assets/shell-CC2rA5mh.js +1 -0
- package/dist/editor/assets/solidity-BEEn4gHE.js +1 -0
- package/dist/editor/assets/sophia-CRfGWb83.js +1 -0
- package/dist/editor/assets/sparql-D_Lu-MrJ.js +1 -0
- package/dist/editor/assets/sql-NEE52Syq.js +1 -0
- package/dist/editor/assets/st-DbInun42.js +1 -0
- package/dist/editor/assets/swift-Bxkupp3x.js +1 -0
- package/dist/editor/assets/systemverilog-Bz4Y3fRF.js +1 -0
- package/dist/editor/assets/tcl-DISqw1ZD.js +1 -0
- package/dist/editor/assets/ts.worker-0mEyHVru.js +51339 -0
- package/dist/editor/assets/tsMode-Bth1rJwM.js +11 -0
- package/dist/editor/assets/twig-De2hgUGE.js +1 -0
- package/dist/editor/assets/typescript-tNRuHsoe.js +1 -0
- package/dist/editor/assets/typespec-B8J7ngcE.js +1 -0
- package/dist/editor/assets/vb-DV3o63ZY.js +1 -0
- package/dist/editor/assets/wgsl-DpFanUEy.js +298 -0
- package/dist/editor/assets/xml-BCx9e6CK.js +1 -0
- package/dist/editor/assets/yaml-Bb-peGP_.js +1 -0
- package/dist/editor/index.html +14 -0
- package/dist/mcp/archetypes.js +103 -0
- package/dist/mcp/cli.js +6 -0
- package/dist/mcp/create-project.js +151 -0
- package/dist/mcp/introspection.js +201 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/assets/waica-coin.png +0 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/assets/waica-dog.png +0 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/assets/waica-slime.png +0 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/art.d.ts +10 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/art.js +13 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/bundle.d.ts +3 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/bundle.js +10 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/controls.d.ts +5 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/controls.js +12 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/index.d.ts +38 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/index.js +34 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/manifest.d.ts +15 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/manifest.js +26 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/prefabs.d.ts +7 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/prefabs.js +97 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry-data.d.ts +5 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry-data.js +45 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry.d.ts +6 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry.js +15 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/scene-default.d.ts +40 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/scene-default.js +93 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/ui.d.ts +10 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/dist/ui.js +24 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +30 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.d.ts +59 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.js +127 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.d.ts +25 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.js +27 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.d.ts +29 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.js +42 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/index.d.ts +9 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/index.js +9 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.d.ts +16 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.js +19 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/npc.d.ts +4 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/npc.js +14 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.d.ts +43 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.js +68 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.d.ts +124 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.js +146 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/player-identity.d.ts +3 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/player-identity.js +5 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/player-states.d.ts +18 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/player-states.js +95 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.d.ts +23 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.js +27 -0
- package/dist/mcp/node_modules/@waica/behaviors/package.json +24 -0
- package/dist/mcp/node_modules/@waica/engine/dist/aabb.d.ts +2 -0
- package/dist/mcp/node_modules/@waica/engine/dist/aabb.js +4 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/clip-player.d.ts +20 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/clip-player.js +24 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/contract.d.ts +21 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/contract.js +23 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/sheet.d.ts +65 -0
- package/dist/mcp/node_modules/@waica/engine/dist/animation/sheet.js +44 -0
- package/dist/mcp/node_modules/@waica/engine/dist/archetype.d.ts +37 -0
- package/dist/mcp/node_modules/@waica/engine/dist/archetype.js +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/camera.d.ts +71 -0
- package/dist/mcp/node_modules/@waica/engine/dist/camera.js +59 -0
- package/dist/mcp/node_modules/@waica/engine/dist/collision-shape.d.ts +26 -0
- package/dist/mcp/node_modules/@waica/engine/dist/collision-shape.js +136 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component-registry.d.ts +16 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component-registry.js +44 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +58 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component.js +11 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +80 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +210 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/dynamic-body.d.ts +68 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/dynamic-body.js +189 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/hitbox.d.ts +25 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/hitbox.js +21 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/solid.d.ts +32 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/solid.js +45 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.d.ts +51 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.js +112 -0
- package/dist/mcp/node_modules/@waica/engine/dist/entity.d.ts +22 -0
- package/dist/mcp/node_modules/@waica/engine/dist/entity.js +52 -0
- package/dist/mcp/node_modules/@waica/engine/dist/events.d.ts +8 -0
- package/dist/mcp/node_modules/@waica/engine/dist/events.js +20 -0
- package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +95 -0
- package/dist/mcp/node_modules/@waica/engine/dist/game.js +263 -0
- package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +39 -0
- package/dist/mcp/node_modules/@waica/engine/dist/index.js +26 -0
- package/dist/mcp/node_modules/@waica/engine/dist/input.d.ts +40 -0
- package/dist/mcp/node_modules/@waica/engine/dist/input.js +84 -0
- package/dist/mcp/node_modules/@waica/engine/dist/scene.d.ts +80 -0
- package/dist/mcp/node_modules/@waica/engine/dist/scene.js +93 -0
- package/dist/mcp/node_modules/@waica/engine/dist/solid-axis.d.ts +21 -0
- package/dist/mcp/node_modules/@waica/engine/dist/solid-axis.js +77 -0
- package/dist/mcp/node_modules/@waica/engine/dist/state/hooks.d.ts +90 -0
- package/dist/mcp/node_modules/@waica/engine/dist/state/hooks.js +81 -0
- package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.d.ts +83 -0
- package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.js +173 -0
- package/dist/mcp/node_modules/@waica/engine/dist/stats.d.ts +27 -0
- package/dist/mcp/node_modules/@waica/engine/dist/stats.js +54 -0
- package/dist/mcp/node_modules/@waica/engine/dist/ui.d.ts +47 -0
- package/dist/mcp/node_modules/@waica/engine/dist/ui.js +175 -0
- package/dist/mcp/node_modules/@waica/engine/package.json +25 -0
- package/dist/mcp/package-resolver.js +240 -0
- package/dist/mcp/project-path.js +86 -0
- package/dist/mcp/scaffolds.js +153 -0
- package/dist/mcp/server.js +217 -0
- package/dist/mcp/stdio.js +13 -0
- package/dist/mcp/template/README.md +22 -0
- package/dist/mcp/template/_gitignore +5 -0
- package/dist/mcp/template/index.html +25 -0
- package/dist/mcp/template/package.json.tpl +20 -0
- package/dist/mcp/template/public/waica.params.json +1 -0
- package/dist/mcp/template/src/art/waica-coin.png +0 -0
- package/dist/mcp/template/src/art/waica-dog.png +0 -0
- package/dist/mcp/template/src/art/waica-slime.png +0 -0
- package/dist/mcp/template/src/characters/player.character.json +133 -0
- package/dist/mcp/template/src/characters/slime.character.json +61 -0
- package/dist/mcp/template/src/controls.json +8 -0
- package/dist/mcp/template/src/game.json +10 -0
- package/dist/mcp/template/src/main.ts +105 -0
- package/dist/mcp/template/src/objects/coin.object.json +41 -0
- package/dist/mcp/template/src/scenes/main.scene.json +217 -0
- package/dist/mcp/template/src/stats.json +7 -0
- package/dist/mcp/template/src/tiles/block.tile.json +21 -0
- package/dist/mcp/template/src/tiles/decor.tile.json +14 -0
- package/dist/mcp/template/src/tiles/platform.tile.json +21 -0
- package/dist/mcp/template/src/ui/coin-counter.html +12 -0
- package/dist/mcp/template/tsconfig.json +15 -0
- package/dist/mcp/template/vite.config.ts +3 -0
- package/dist/mcp/validation.js +323 -0
- package/dist/mcp/workspace-loader.js +14 -0
- package/dist/mcp/workspace-runtime.js +109 -0
- package/dist/server.js +210 -0
- package/package.json +38 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Component } from '../component.js';
|
|
2
|
+
import { AnimatedSprite } from '../components/animated-sprite.js';
|
|
3
|
+
import { closestLogicSet, logicSet, registeredLogicSets, } from './hooks.js';
|
|
4
|
+
/** Whether one trigger fires. Unknown or malformed triggers never fire. */
|
|
5
|
+
export function evaluateTrigger(on, env) {
|
|
6
|
+
const sep = on.indexOf(':');
|
|
7
|
+
if (sep <= 0)
|
|
8
|
+
return false;
|
|
9
|
+
const kind = on.slice(0, sep);
|
|
10
|
+
const arg = on.slice(sep + 1);
|
|
11
|
+
if (kind === 'input')
|
|
12
|
+
return env.justPressed(arg);
|
|
13
|
+
if (kind === 'timer')
|
|
14
|
+
return env.elapsed >= Number(arg);
|
|
15
|
+
if (kind === 'signal')
|
|
16
|
+
return env.signals.has(arg);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
/** The first firing transition: the state's own edges, then '*''s. */
|
|
20
|
+
export function nextTransition(states, current, env) {
|
|
21
|
+
const edges = [...(states[current]?.transitions ?? []), ...(states['*']?.transitions ?? [])];
|
|
22
|
+
return edges.find((t) => evaluateTrigger(t.on, env));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The hooks that actually run for one phase of a state: the state's own
|
|
26
|
+
* (logic set + instance), or — when none of them define the phase — the
|
|
27
|
+
* set's 'default' entry. A custom state keeps the role's stock body
|
|
28
|
+
* update unless it brings its own.
|
|
29
|
+
*/
|
|
30
|
+
export function phaseHooks(hooks, fallback, phase) {
|
|
31
|
+
if (hooks.some((h) => h[phase]))
|
|
32
|
+
return hooks;
|
|
33
|
+
return fallback?.[phase] ? [fallback] : hooks;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Finite state machine: the single owner of a character's per-frame
|
|
37
|
+
* logic — only the active state's code moves the body, so behaviors
|
|
38
|
+
* never fight over velocity. States and transitions are prefab data;
|
|
39
|
+
* state code lives in the role's logic set (defineRole / defineStates)
|
|
40
|
+
* picked by `role`. Entering a state plays the sibling AnimatedSprite
|
|
41
|
+
* clip of the same name (or the state's `clip` override); a missing
|
|
42
|
+
* clip warns and keeps the previous one.
|
|
43
|
+
*/
|
|
44
|
+
export class StateMachine extends Component {
|
|
45
|
+
static componentName = 'StateMachine';
|
|
46
|
+
static displayName = 'State Machine';
|
|
47
|
+
static params = {
|
|
48
|
+
role: { label: 'Role' },
|
|
49
|
+
};
|
|
50
|
+
/** The character's role — names the logic set providing its state code. */
|
|
51
|
+
role = '';
|
|
52
|
+
/** Starting state; defaults to the first declared state. */
|
|
53
|
+
initial = '';
|
|
54
|
+
states = {};
|
|
55
|
+
/** Active state name. */
|
|
56
|
+
current = '';
|
|
57
|
+
/** Seconds spent in the active state. */
|
|
58
|
+
elapsed = 0;
|
|
59
|
+
instanceHooks = new Map();
|
|
60
|
+
signals = new Set();
|
|
61
|
+
warnedClips = new Set();
|
|
62
|
+
onReady() {
|
|
63
|
+
if (this.role && !logicSet(this.role)) {
|
|
64
|
+
const sets = registeredLogicSets();
|
|
65
|
+
const hint = closestLogicSet(this.role);
|
|
66
|
+
console.error(`[waica] "${this.entity.name}": role "${this.role}" has no registered state code. ` +
|
|
67
|
+
`Known: ${sets.length ? sets.join(', ') : '(none)'}.` +
|
|
68
|
+
(hint ? ` Did you mean "${hint}"?` : ''));
|
|
69
|
+
}
|
|
70
|
+
const start = this.initial || Object.keys(this.states).find((name) => name !== '*');
|
|
71
|
+
if (start)
|
|
72
|
+
this.enter(start);
|
|
73
|
+
}
|
|
74
|
+
/** Adds instance-level hooks on top of the logic set — the escape hatch. */
|
|
75
|
+
on(state, hooks) {
|
|
76
|
+
const list = this.instanceHooks.get(state) ?? [];
|
|
77
|
+
list.push(hooks);
|
|
78
|
+
this.instanceHooks.set(state, list);
|
|
79
|
+
}
|
|
80
|
+
/** Queues a signal for 'signal:<name>' transitions, consumed this frame or the next. */
|
|
81
|
+
signal(name) {
|
|
82
|
+
this.signals.add(name);
|
|
83
|
+
}
|
|
84
|
+
/** Jumps straight to a state, ignoring transitions. */
|
|
85
|
+
goto(state) {
|
|
86
|
+
if (state !== this.current)
|
|
87
|
+
this.enter(state);
|
|
88
|
+
}
|
|
89
|
+
onUpdate(dt) {
|
|
90
|
+
if (!this.current)
|
|
91
|
+
return;
|
|
92
|
+
this.run('*', 'onUpdate', dt);
|
|
93
|
+
this.run(this.current, 'onUpdate', dt);
|
|
94
|
+
this.elapsed += dt;
|
|
95
|
+
// Chained transitions settle within the frame (e.g. land → idle → run),
|
|
96
|
+
// capped so a degenerate cyclic graph can't hang the loop.
|
|
97
|
+
for (let hops = 0; hops < 8; hops++) {
|
|
98
|
+
const edge = nextTransition(this.states, this.current, this.env());
|
|
99
|
+
if (!edge)
|
|
100
|
+
break;
|
|
101
|
+
// A transition fired by a key press spends it: one press, one
|
|
102
|
+
// transition — it can't fire again from the state just entered.
|
|
103
|
+
if (edge.on.startsWith('input:'))
|
|
104
|
+
this.game.input.consume(edge.on.slice('input:'.length));
|
|
105
|
+
this.enter(edge.to);
|
|
106
|
+
}
|
|
107
|
+
this.signals.clear();
|
|
108
|
+
}
|
|
109
|
+
onCollide(other) {
|
|
110
|
+
// The state that was active when the contact happened owns it: a
|
|
111
|
+
// transition fired inside the always-hook must not hand the collision
|
|
112
|
+
// to the state it just entered.
|
|
113
|
+
const state = this.current;
|
|
114
|
+
if (!state)
|
|
115
|
+
return;
|
|
116
|
+
this.runCollision('*', other);
|
|
117
|
+
// Either side can be destroyed by a hook. Game.dispatchCollisions guards
|
|
118
|
+
// the same way between the two sides of one contact.
|
|
119
|
+
if (!this.entity.alive || !other.alive)
|
|
120
|
+
return;
|
|
121
|
+
this.runCollision(state, other);
|
|
122
|
+
}
|
|
123
|
+
env() {
|
|
124
|
+
return {
|
|
125
|
+
justPressed: (action) => this.game.input.justPressed(action) && !this.game.input.consumed(action),
|
|
126
|
+
elapsed: this.elapsed,
|
|
127
|
+
signals: this.signals,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
hooksFor(state) {
|
|
131
|
+
const fromSet = this.role ? logicSet(this.role)?.[state] : undefined;
|
|
132
|
+
return [...(fromSet ? [fromSet] : []), ...(this.instanceHooks.get(state) ?? [])];
|
|
133
|
+
}
|
|
134
|
+
run(state, phase, dt = 0) {
|
|
135
|
+
const ctx = { entity: this.entity, game: this.game, fsm: this };
|
|
136
|
+
const fallback = state !== '*' && this.role ? logicSet(this.role)?.['default'] : undefined;
|
|
137
|
+
for (const hooks of phaseHooks(this.hooksFor(state), fallback, phase)) {
|
|
138
|
+
if (phase === 'onUpdate')
|
|
139
|
+
hooks.onUpdate?.(ctx, dt);
|
|
140
|
+
else
|
|
141
|
+
hooks[phase]?.(ctx);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
runCollision(state, other) {
|
|
145
|
+
const ctx = { entity: this.entity, game: this.game, fsm: this };
|
|
146
|
+
const fallback = state !== '*' && this.role ? logicSet(this.role)?.['default'] : undefined;
|
|
147
|
+
for (const hooks of phaseHooks(this.hooksFor(state), fallback, 'onCollide')) {
|
|
148
|
+
hooks.onCollide?.(ctx, other);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
enter(state) {
|
|
152
|
+
if (this.current)
|
|
153
|
+
this.run(this.current, 'onExit');
|
|
154
|
+
this.current = state;
|
|
155
|
+
this.elapsed = 0;
|
|
156
|
+
this.playClip(state);
|
|
157
|
+
this.run(state, 'onEnter');
|
|
158
|
+
}
|
|
159
|
+
playClip(state) {
|
|
160
|
+
const sprite = this.entity.get(AnimatedSprite);
|
|
161
|
+
if (!sprite)
|
|
162
|
+
return;
|
|
163
|
+
const clip = this.states[state]?.clip ?? state;
|
|
164
|
+
if (sprite.clips[clip]) {
|
|
165
|
+
sprite.play(clip);
|
|
166
|
+
}
|
|
167
|
+
else if (!this.warnedClips.has(state)) {
|
|
168
|
+
this.warnedClips.add(state);
|
|
169
|
+
console.warn(`[waica] "${this.entity.name}": no clip "${clip}" for state "${state}" — ` +
|
|
170
|
+
`keeping "${sprite.current ?? 'none'}"`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** A stat's value: points/lives are numbers, flags are booleans, labels strings. */
|
|
2
|
+
export type StatValue = number | boolean | string;
|
|
3
|
+
/**
|
|
4
|
+
* Named game state (points, lives, door-open flags…) declared in the
|
|
5
|
+
* project's src/stats.json. Every new Game starts from the declared initial
|
|
6
|
+
* values; behaviours read and change them and can subscribe to changes.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Stats {
|
|
9
|
+
private readonly initial;
|
|
10
|
+
private values;
|
|
11
|
+
private readonly emitter;
|
|
12
|
+
constructor(initial?: Record<string, StatValue>);
|
|
13
|
+
/** Current value; undeclared stats read as undefined. */
|
|
14
|
+
get(name: string): StatValue | undefined;
|
|
15
|
+
/** Numeric read: non-number and undeclared stats count as 0. */
|
|
16
|
+
count(name: string): number;
|
|
17
|
+
/** Sets a stat (declared or not) and notifies subscribers on real changes. */
|
|
18
|
+
set(name: string, value: StatValue): void;
|
|
19
|
+
/** Adds delta (default 1) to a numeric stat; undeclared stats start at 0. */
|
|
20
|
+
add(name: string, delta?: number): number;
|
|
21
|
+
/** Back to the declared initial values, notifying every stat that changed. */
|
|
22
|
+
reset(): void;
|
|
23
|
+
/** Fires when the named stat changes. Returns the unsubscribe. */
|
|
24
|
+
onChange(name: string, handler: (value: StatValue | undefined) => void): () => void;
|
|
25
|
+
/** Every stat with its current value (declared and runtime-created). */
|
|
26
|
+
entries(): Array<[string, StatValue]>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Emitter } from './events.js';
|
|
2
|
+
/**
|
|
3
|
+
* Named game state (points, lives, door-open flags…) declared in the
|
|
4
|
+
* project's src/stats.json. Every new Game starts from the declared initial
|
|
5
|
+
* values; behaviours read and change them and can subscribe to changes.
|
|
6
|
+
*/
|
|
7
|
+
export class Stats {
|
|
8
|
+
initial;
|
|
9
|
+
values;
|
|
10
|
+
emitter = new Emitter();
|
|
11
|
+
constructor(initial = {}) {
|
|
12
|
+
this.initial = { ...initial };
|
|
13
|
+
this.values = { ...initial };
|
|
14
|
+
}
|
|
15
|
+
/** Current value; undeclared stats read as undefined. */
|
|
16
|
+
get(name) {
|
|
17
|
+
return this.values[name];
|
|
18
|
+
}
|
|
19
|
+
/** Numeric read: non-number and undeclared stats count as 0. */
|
|
20
|
+
count(name) {
|
|
21
|
+
const value = this.values[name];
|
|
22
|
+
return typeof value === 'number' ? value : 0;
|
|
23
|
+
}
|
|
24
|
+
/** Sets a stat (declared or not) and notifies subscribers on real changes. */
|
|
25
|
+
set(name, value) {
|
|
26
|
+
if (this.values[name] === value)
|
|
27
|
+
return;
|
|
28
|
+
this.values[name] = value;
|
|
29
|
+
this.emitter.emit(name, value);
|
|
30
|
+
}
|
|
31
|
+
/** Adds delta (default 1) to a numeric stat; undeclared stats start at 0. */
|
|
32
|
+
add(name, delta = 1) {
|
|
33
|
+
const next = this.count(name) + delta;
|
|
34
|
+
this.set(name, next);
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
/** Back to the declared initial values, notifying every stat that changed. */
|
|
38
|
+
reset() {
|
|
39
|
+
const previous = this.values;
|
|
40
|
+
this.values = { ...this.initial };
|
|
41
|
+
for (const name of new Set([...Object.keys(previous), ...Object.keys(this.values)])) {
|
|
42
|
+
if (previous[name] !== this.values[name])
|
|
43
|
+
this.emitter.emit(name, this.values[name]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Fires when the named stat changes. Returns the unsubscribe. */
|
|
47
|
+
onChange(name, handler) {
|
|
48
|
+
return this.emitter.on(name, handler);
|
|
49
|
+
}
|
|
50
|
+
/** Every stat with its current value (declared and runtime-created). */
|
|
51
|
+
entries() {
|
|
52
|
+
return Object.entries(this.values);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Stats } from './stats.js';
|
|
2
|
+
/**
|
|
3
|
+
* The HTML UI layer. Each piece is a self-contained HTML fragment
|
|
4
|
+
* (markup + <style>) that only DRAWS: it declares which stats it shows
|
|
5
|
+
* with {{stat}} placeholders and positions itself with its own CSS.
|
|
6
|
+
* Behaviour always comes from outside — code toggles pieces with
|
|
7
|
+
* show/hide and wires interactivity through element().
|
|
8
|
+
*
|
|
9
|
+
* Pieces mount inside a transparent overlay that covers the game canvas
|
|
10
|
+
* (each in its own shadow root, so styles never leak between pieces or
|
|
11
|
+
* into the hosting page). The whole overlay hides while the game is not
|
|
12
|
+
* simulating (pause / editor edit mode).
|
|
13
|
+
*/
|
|
14
|
+
export declare class GameUi {
|
|
15
|
+
private readonly stats;
|
|
16
|
+
/** Resolved lazily: the canvas may not be in the DOM at construction. */
|
|
17
|
+
private readonly host;
|
|
18
|
+
private readonly sources;
|
|
19
|
+
private readonly pieces;
|
|
20
|
+
private overlay?;
|
|
21
|
+
private active;
|
|
22
|
+
constructor(stats: Stats,
|
|
23
|
+
/** Resolved lazily: the canvas may not be in the DOM at construction. */
|
|
24
|
+
host: () => HTMLElement);
|
|
25
|
+
/** Registers a piece's HTML source. Re-defining an unmounted name wins. */
|
|
26
|
+
define(name: string, html: string): void;
|
|
27
|
+
defineAll(pieces: Record<string, string>): void;
|
|
28
|
+
/** Piece names available to show (defined via the registry or define()). */
|
|
29
|
+
names(): string[];
|
|
30
|
+
show(name: string): void;
|
|
31
|
+
hide(name: string): void;
|
|
32
|
+
toggle(name: string): void;
|
|
33
|
+
isVisible(name: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The piece's DOM root — the escape hatch that keeps pieces logic-free:
|
|
36
|
+
* behaviour is wired from code (element(...).querySelector + listeners).
|
|
37
|
+
* Mounts the piece hidden if it wasn't mounted yet.
|
|
38
|
+
*/
|
|
39
|
+
element(name: string): HTMLElement | null;
|
|
40
|
+
/** Called by the game loop: the overlay only draws while simulating. */
|
|
41
|
+
setActive(active: boolean): void;
|
|
42
|
+
/** Unmounts every piece and removes the overlay (Game.dispose). */
|
|
43
|
+
dispose(): void;
|
|
44
|
+
private mount;
|
|
45
|
+
private mountOverlay;
|
|
46
|
+
private sync;
|
|
47
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTML UI layer. Each piece is a self-contained HTML fragment
|
|
3
|
+
* (markup + <style>) that only DRAWS: it declares which stats it shows
|
|
4
|
+
* with {{stat}} placeholders and positions itself with its own CSS.
|
|
5
|
+
* Behaviour always comes from outside — code toggles pieces with
|
|
6
|
+
* show/hide and wires interactivity through element().
|
|
7
|
+
*
|
|
8
|
+
* Pieces mount inside a transparent overlay that covers the game canvas
|
|
9
|
+
* (each in its own shadow root, so styles never leak between pieces or
|
|
10
|
+
* into the hosting page). The whole overlay hides while the game is not
|
|
11
|
+
* simulating (pause / editor edit mode).
|
|
12
|
+
*/
|
|
13
|
+
export class GameUi {
|
|
14
|
+
stats;
|
|
15
|
+
host;
|
|
16
|
+
sources = new Map();
|
|
17
|
+
pieces = new Map();
|
|
18
|
+
overlay;
|
|
19
|
+
active = true;
|
|
20
|
+
constructor(stats,
|
|
21
|
+
/** Resolved lazily: the canvas may not be in the DOM at construction. */
|
|
22
|
+
host) {
|
|
23
|
+
this.stats = stats;
|
|
24
|
+
this.host = host;
|
|
25
|
+
}
|
|
26
|
+
/** Registers a piece's HTML source. Re-defining an unmounted name wins. */
|
|
27
|
+
define(name, html) {
|
|
28
|
+
this.sources.set(name, html);
|
|
29
|
+
}
|
|
30
|
+
defineAll(pieces) {
|
|
31
|
+
for (const [name, html] of Object.entries(pieces))
|
|
32
|
+
this.define(name, html);
|
|
33
|
+
}
|
|
34
|
+
/** Piece names available to show (defined via the registry or define()). */
|
|
35
|
+
names() {
|
|
36
|
+
return [...this.sources.keys()];
|
|
37
|
+
}
|
|
38
|
+
show(name) {
|
|
39
|
+
const piece = this.mount(name);
|
|
40
|
+
if (piece)
|
|
41
|
+
piece.visible = true;
|
|
42
|
+
this.sync();
|
|
43
|
+
}
|
|
44
|
+
hide(name) {
|
|
45
|
+
const piece = this.pieces.get(name);
|
|
46
|
+
if (piece)
|
|
47
|
+
piece.visible = false;
|
|
48
|
+
this.sync();
|
|
49
|
+
}
|
|
50
|
+
toggle(name) {
|
|
51
|
+
if (this.isVisible(name))
|
|
52
|
+
this.hide(name);
|
|
53
|
+
else
|
|
54
|
+
this.show(name);
|
|
55
|
+
}
|
|
56
|
+
isVisible(name) {
|
|
57
|
+
return this.pieces.get(name)?.visible ?? false;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The piece's DOM root — the escape hatch that keeps pieces logic-free:
|
|
61
|
+
* behaviour is wired from code (element(...).querySelector + listeners).
|
|
62
|
+
* Mounts the piece hidden if it wasn't mounted yet.
|
|
63
|
+
*/
|
|
64
|
+
element(name) {
|
|
65
|
+
return this.mount(name)?.root ?? null;
|
|
66
|
+
}
|
|
67
|
+
/** Called by the game loop: the overlay only draws while simulating. */
|
|
68
|
+
setActive(active) {
|
|
69
|
+
if (this.active === active)
|
|
70
|
+
return;
|
|
71
|
+
this.active = active;
|
|
72
|
+
this.sync();
|
|
73
|
+
}
|
|
74
|
+
/** Unmounts every piece and removes the overlay (Game.dispose). */
|
|
75
|
+
dispose() {
|
|
76
|
+
for (const piece of this.pieces.values()) {
|
|
77
|
+
for (const off of piece.unsubs)
|
|
78
|
+
off();
|
|
79
|
+
}
|
|
80
|
+
this.pieces.clear();
|
|
81
|
+
this.overlay?.remove();
|
|
82
|
+
this.overlay = undefined;
|
|
83
|
+
}
|
|
84
|
+
mount(name) {
|
|
85
|
+
const existing = this.pieces.get(name);
|
|
86
|
+
if (existing)
|
|
87
|
+
return existing;
|
|
88
|
+
const html = this.sources.get(name);
|
|
89
|
+
if (html == null) {
|
|
90
|
+
console.warn(`[waica] unknown ui piece: "${name}"`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
// Same geometry as the overlay, so the piece's own CSS positions
|
|
94
|
+
// against the full canvas. pointer-events stays off unless the piece's
|
|
95
|
+
// CSS opts in (e.g. a button with pointer-events:auto).
|
|
96
|
+
const shell = document.createElement('div');
|
|
97
|
+
shell.style.cssText = 'position:absolute;inset:0;pointer-events:none';
|
|
98
|
+
const shadow = shell.attachShadow({ mode: 'open' });
|
|
99
|
+
const root = document.createElement('div');
|
|
100
|
+
root.style.display = 'contents';
|
|
101
|
+
root.innerHTML = html;
|
|
102
|
+
shadow.append(root);
|
|
103
|
+
const piece = { shell, root, visible: false, unsubs: bindStats(root, this.stats) };
|
|
104
|
+
this.mountOverlay().append(shell);
|
|
105
|
+
this.pieces.set(name, piece);
|
|
106
|
+
return piece;
|
|
107
|
+
}
|
|
108
|
+
mountOverlay() {
|
|
109
|
+
if (this.overlay)
|
|
110
|
+
return this.overlay;
|
|
111
|
+
const overlay = document.createElement('div');
|
|
112
|
+
overlay.style.cssText = 'position:absolute;inset:0;z-index:9000;pointer-events:none';
|
|
113
|
+
const host = this.host();
|
|
114
|
+
if (getComputedStyle(host).position === 'static')
|
|
115
|
+
host.style.position = 'relative';
|
|
116
|
+
host.append(overlay);
|
|
117
|
+
this.overlay = overlay;
|
|
118
|
+
this.sync();
|
|
119
|
+
return overlay;
|
|
120
|
+
}
|
|
121
|
+
sync() {
|
|
122
|
+
if (this.overlay)
|
|
123
|
+
this.overlay.style.display = this.active ? '' : 'none';
|
|
124
|
+
for (const piece of this.pieces.values()) {
|
|
125
|
+
piece.shell.style.display = piece.visible ? '' : 'none';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const BINDING = /\{\{\s*([\w-]+)\s*\}\}/g;
|
|
130
|
+
function renderStat(value) {
|
|
131
|
+
if (value === undefined)
|
|
132
|
+
return '';
|
|
133
|
+
if (typeof value === 'boolean')
|
|
134
|
+
return value ? '✓' : '✕';
|
|
135
|
+
return String(value);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Replaces {{stat}} placeholders in the fragment's text with reactive text
|
|
139
|
+
* nodes kept in sync with the stats. Text-only by design: the binding
|
|
140
|
+
* language has no expressions — presentation, never logic.
|
|
141
|
+
*/
|
|
142
|
+
function bindStats(root, stats) {
|
|
143
|
+
const unsubs = [];
|
|
144
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
145
|
+
const targets = [];
|
|
146
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
147
|
+
// Braces inside <style>/<script> are CSS/code, not bindings.
|
|
148
|
+
if (node.parentElement?.closest('style, script'))
|
|
149
|
+
continue;
|
|
150
|
+
if ((node.nodeValue ?? '').includes('{{'))
|
|
151
|
+
targets.push(node);
|
|
152
|
+
}
|
|
153
|
+
for (const text of targets) {
|
|
154
|
+
const source = text.nodeValue ?? '';
|
|
155
|
+
const parts = [];
|
|
156
|
+
let last = 0;
|
|
157
|
+
for (const match of source.matchAll(BINDING)) {
|
|
158
|
+
const stat = match[1];
|
|
159
|
+
if (stat === undefined)
|
|
160
|
+
continue;
|
|
161
|
+
if (match.index > last)
|
|
162
|
+
parts.push(document.createTextNode(source.slice(last, match.index)));
|
|
163
|
+
const bound = document.createTextNode(renderStat(stats.get(stat)));
|
|
164
|
+
unsubs.push(stats.onChange(stat, (value) => (bound.nodeValue = renderStat(value))));
|
|
165
|
+
parts.push(bound);
|
|
166
|
+
last = match.index + match[0].length;
|
|
167
|
+
}
|
|
168
|
+
if (parts.length === 0)
|
|
169
|
+
continue;
|
|
170
|
+
if (last < source.length)
|
|
171
|
+
parts.push(document.createTextNode(source.slice(last)));
|
|
172
|
+
text.replaceWith(...parts);
|
|
173
|
+
}
|
|
174
|
+
return unsubs;
|
|
175
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@waica/engine",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Waica game engine core — archetype-driven, web-first, 2D & 3D",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/chichex/waica",
|
|
10
|
+
"directory": "packages/engine"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@types/three": "^0.185.1",
|
|
23
|
+
"three": "^0.185.1"
|
|
24
|
+
}
|
|
25
|
+
}
|