@origingame/create-game 0.2.0 → 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.
Files changed (157) hide show
  1. package/README.md +2 -2
  2. package/archetypes/README.md +1 -0
  3. package/archetypes/arena/.origin/setup +14 -0
  4. package/archetypes/arena/LICENSES.md +8 -0
  5. package/archetypes/arena/README.md +67 -0
  6. package/archetypes/arena/assets/audio/coin.ogt +9 -0
  7. package/archetypes/arena/assets/audio/dash.ogt +9 -0
  8. package/archetypes/arena/assets/audio/win.ogt +9 -0
  9. package/archetypes/arena/assets/fx/coin.ogt +20 -0
  10. package/archetypes/arena/assets/looks/arena.ogt +10 -0
  11. package/archetypes/arena/gitattributes +22 -0
  12. package/archetypes/arena/gitignore +8 -0
  13. package/archetypes/arena/host.d.ts +46 -0
  14. package/archetypes/arena/package.json +18 -0
  15. package/archetypes/arena/project.ogt +14 -0
  16. package/archetypes/arena/src/game.ts +314 -0
  17. package/archetypes/arena/src/main.ts +100 -0
  18. package/archetypes/arena/tests/duel.og.ts +32 -0
  19. package/archetypes/arena/tests/solo.og.ts +22 -0
  20. package/archetypes/arena/tsconfig.json +17 -0
  21. package/archetypes/idle/LICENSES.md +1 -1
  22. package/archetypes/idle/README.md +3 -3
  23. package/archetypes/idle/assets/looks/idle.ogt +1 -1
  24. package/archetypes/idle/assets/{skins → styles}/sparkorb/LICENSES.md +4 -4
  25. package/archetypes/idle/assets/{skins/sparkorb/skin.ogt → styles/sparkorb/style.ogt} +47 -47
  26. package/archetypes/idle/package.json +1 -1
  27. package/archetypes/idle/project.ogt +1 -1
  28. package/archetypes/idle/references/metal/card.png +0 -0
  29. package/archetypes/idle/references/metal/orbit.png +0 -0
  30. package/archetypes/idle/references/metal/title.png +0 -0
  31. package/archetypes/idle/src/main.ts +2 -2
  32. package/archetypes/idle/src/ui.ts +11 -11
  33. package/archetypes/idle/tools/make-assets.py +4 -4
  34. package/archetypes/match3/package.json +1 -1
  35. package/archetypes/match3/project.ogt +1 -1
  36. package/archetypes/rhythm/LICENSES.md +1 -1
  37. package/archetypes/rhythm/README.md +3 -3
  38. package/archetypes/rhythm/assets/{skins → styles}/lanebeat/LICENSES.md +4 -4
  39. package/archetypes/rhythm/assets/{skins/lanebeat/skin.ogt → styles/lanebeat/style.ogt} +45 -45
  40. package/archetypes/rhythm/package.json +1 -1
  41. package/archetypes/rhythm/project.ogt +1 -1
  42. package/archetypes/rhythm/references/metal/results.png +0 -0
  43. package/archetypes/rhythm/references/metal/title.png +0 -0
  44. package/archetypes/rhythm/src/main.ts +2 -2
  45. package/archetypes/rhythm/src/ui.ts +11 -11
  46. package/archetypes/roguelike/LICENSES.md +1 -1
  47. package/archetypes/roguelike/README.md +3 -3
  48. package/archetypes/roguelike/assets/{skins → styles}/lampdive/LICENSES.md +4 -4
  49. package/archetypes/roguelike/assets/{skins/lampdive/skin.ogt → styles/lampdive/style.ogt} +41 -41
  50. package/archetypes/roguelike/package.json +1 -1
  51. package/archetypes/roguelike/project.ogt +1 -1
  52. package/archetypes/roguelike/references/metal/over.png +0 -0
  53. package/archetypes/roguelike/references/metal/title.png +0 -0
  54. package/archetypes/roguelike/src/main.ts +2 -2
  55. package/archetypes/roguelike/src/ui.ts +7 -7
  56. package/archetypes/runner/package.json +1 -1
  57. package/archetypes/runner/project.ogt +1 -1
  58. package/archetypes/third-person/LICENSES.md +1 -1
  59. package/archetypes/third-person/README.md +3 -3
  60. package/archetypes/third-person/assets/{skins → styles}/ember/LICENSES.md +2 -2
  61. package/archetypes/third-person/assets/{skins/ember/skin.ogt → styles/ember/style.ogt} +40 -41
  62. package/archetypes/third-person/package.json +1 -1
  63. package/archetypes/third-person/project.ogt +1 -1
  64. package/archetypes/third-person/references/metal/beacon.png +0 -0
  65. package/archetypes/third-person/references/metal/dawn.png +0 -0
  66. package/archetypes/third-person/references/metal/title.png +0 -0
  67. package/archetypes/third-person/references/webgl2/beacon.png +0 -0
  68. package/archetypes/third-person/src/main.ts +2 -2
  69. package/archetypes/third-person/src/ui.ts +10 -10
  70. package/archetypes/topdown/package.json +1 -1
  71. package/archetypes/topdown/project.ogt +1 -1
  72. package/archetypes/tower-defense/LICENSES.md +1 -1
  73. package/archetypes/tower-defense/README.md +5 -5
  74. package/archetypes/tower-defense/assets/{skins → styles}/pathguard/LICENSES.md +4 -4
  75. package/archetypes/tower-defense/assets/styles/pathguard/atlas.png +0 -0
  76. package/archetypes/tower-defense/assets/styles/pathguard/style.ogt +111 -0
  77. package/archetypes/tower-defense/package.json +1 -1
  78. package/archetypes/tower-defense/project.ogt +1 -1
  79. package/archetypes/tower-defense/references/metal/end.png +0 -0
  80. package/archetypes/tower-defense/references/metal/title.png +0 -0
  81. package/archetypes/tower-defense/references/metal/wave.png +0 -0
  82. package/archetypes/tower-defense/src/main.ts +2 -2
  83. package/archetypes/tower-defense/src/ui.ts +19 -17
  84. package/bin/create.ts +11 -8
  85. package/looks/catalogue.json +40 -40
  86. package/package.json +1 -1
  87. package/template/README.md +2 -1
  88. package/template/assets/audio/sting.ogt +10 -0
  89. package/template/assets/timelines/intro.ogt +101 -0
  90. package/template/assets/timelines/narration.srt +11 -0
  91. package/template/host.d.ts +46 -0
  92. package/template/package.json +1 -1
  93. package/template/project.ogt +1 -1
  94. package/template/references/metal/intro-4.png +0 -0
  95. package/template/references/metal/intro-8.png +0 -0
  96. package/template/src/main.ts +1 -1
  97. package/template/tests/frames.ts +134 -0
  98. package/template/tests/intro.og.ts +65 -0
  99. package/template/tests/smoke.og.ts +30 -36
  100. package/template/tsconfig.json +1 -1
  101. package/template-2d/LICENSES.md +1 -0
  102. package/template-2d/README.md +5 -2
  103. package/template-2d/assets/rigs/keeper.ogt +292 -0
  104. package/template-2d/assets/rigs/keeper.png +0 -0
  105. package/template-2d/assets/rigs/keeper.png.ogt +7 -0
  106. package/template-2d/package.json +1 -1
  107. package/template-2d/project.ogt +1 -1
  108. package/template-2d/src/main.ts +159 -4
  109. package/template-2d/tests/keeper.og.ts +93 -0
  110. package/archetypes/tower-defense/assets/skins/pathguard/atlas.png +0 -0
  111. package/archetypes/tower-defense/assets/skins/pathguard/skin.ogt +0 -111
  112. /package/archetypes/idle/assets/{skins → styles}/sparkorb/ChakraPetch-BoldItalic.ttf +0 -0
  113. /package/archetypes/idle/assets/{skins → styles}/sparkorb/ChakraPetch-BoldItalic.ttf.ogt +0 -0
  114. /package/archetypes/idle/assets/{skins → styles}/sparkorb/OFL.txt +0 -0
  115. /package/archetypes/idle/assets/{skins → styles}/sparkorb/Orbitron-Medium.ttf +0 -0
  116. /package/archetypes/idle/assets/{skins → styles}/sparkorb/Orbitron-Medium.ttf.ogt +0 -0
  117. /package/archetypes/idle/assets/{skins → styles}/sparkorb/Rajdhani-Bold.ttf +0 -0
  118. /package/archetypes/idle/assets/{skins → styles}/sparkorb/Rajdhani-Bold.ttf.ogt +0 -0
  119. /package/archetypes/idle/assets/{skins → styles}/sparkorb/atlas.png +0 -0
  120. /package/archetypes/idle/assets/{skins → styles}/sparkorb/atlas.png.ogt +0 -0
  121. /package/archetypes/idle/assets/{skins → styles}/sparkorb/icons.png +0 -0
  122. /package/archetypes/idle/assets/{skins → styles}/sparkorb/icons.png.ogt +0 -0
  123. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/ChakraPetch-Medium.ttf +0 -0
  124. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/ChakraPetch-Medium.ttf.ogt +0 -0
  125. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/JetBrainsMono-Regular.ttf +0 -0
  126. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/JetBrainsMono-Regular.ttf.ogt +0 -0
  127. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/OFL.txt +0 -0
  128. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/Orbitron-Bold.ttf +0 -0
  129. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/Orbitron-Bold.ttf.ogt +0 -0
  130. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/atlas.png +0 -0
  131. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/atlas.png.ogt +0 -0
  132. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/icons.png +0 -0
  133. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/icons.png.ogt +0 -0
  134. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/DotGothic16-Regular.ttf +0 -0
  135. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/DotGothic16-Regular.ttf.ogt +0 -0
  136. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/OFL.txt +0 -0
  137. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/atlas.png +0 -0
  138. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/atlas.png.ogt +0 -0
  139. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/icons.png +0 -0
  140. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/icons.png.ogt +0 -0
  141. /package/archetypes/third-person/assets/{skins → styles}/ember/Cinzel-Bold.ttf +0 -0
  142. /package/archetypes/third-person/assets/{skins → styles}/ember/Cinzel-Bold.ttf.ogt +0 -0
  143. /package/archetypes/third-person/assets/{skins → styles}/ember/Newsreader-Medium.ttf +0 -0
  144. /package/archetypes/third-person/assets/{skins → styles}/ember/Newsreader-Medium.ttf.ogt +0 -0
  145. /package/archetypes/third-person/assets/{skins → styles}/ember/OFL.txt +0 -0
  146. /package/archetypes/third-person/assets/{skins → styles}/ember/atlas.png +0 -0
  147. /package/archetypes/third-person/assets/{skins → styles}/ember/atlas.png.ogt +0 -0
  148. /package/archetypes/third-person/assets/{skins → styles}/ember/icons.png +0 -0
  149. /package/archetypes/third-person/assets/{skins → styles}/ember/icons.png.ogt +0 -0
  150. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/Fredoka-SemiBold.ttf +0 -0
  151. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/Fredoka-SemiBold.ttf.ogt +0 -0
  152. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/LilitaOne-Regular.ttf +0 -0
  153. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/LilitaOne-Regular.ttf.ogt +0 -0
  154. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/OFL.txt +0 -0
  155. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/atlas.png.ogt +0 -0
  156. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/icons.png +0 -0
  157. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/icons.png.ogt +0 -0
@@ -11,11 +11,20 @@
11
11
  // Everything the starter shows is in og-game §8 (sprites, flipbooks,
12
12
  // tilemaps), §13 (the controller) and og-feel §12 (its tunables).
13
13
  //
14
+ // Beside the spawn a lamp keeper stands on a ramp: a 2D skeletal rig
15
+ // (assets/rigs/keeper.ogt, og.Skeleton2D) whose cape is a weighted mesh the
16
+ // idle clip deforms, whose feet two-bone IK planted on the slope
17
+ // (og.IkTarget2D, the Plant system), and whose lantern lights the sprite
18
+ // layers through og.Lighting2D (og-game §8; og-anim's 2D skeletons). Sent
19
+ // along the ramp (`keeper:<x>`), it plays its walk clip there, each foot
20
+ // planted while it carries the keeper and lifted while it swings, and
21
+ // breathes again when it arrives (the Walk and Plant systems).
22
+ //
14
23
  // The frame's look is data: GameDesc.look names a file under assets/looks/
15
24
  // that extends a preset of the style catalogue, applied before start runs.
16
25
 
17
26
  import { og, defineComponent, defineResource, defineSystem, bool, f32, u32, SHAPE_BOX } from "@origingame/engine";
18
- import type { FlipbookInfo, Result, TileObject } from "@origingame/engine";
27
+ import type { FlipbookInfo, Result, Rig2DInfo, TileObject, World } from "@origingame/engine";
19
28
 
20
29
  /** The courier's clips, in the sheet's file order (og-game §8): `start` checks them. */
21
30
  const CLIP = { idle: 0, run: 1, jump: 2, fall: 3 } as const;
@@ -28,6 +37,16 @@ const BOX = { w: 0.9, h: 1.9 } as const;
28
37
  /** Below this the courier fell into the pit. */
29
38
  const FALL_Y = -3;
30
39
  const LIVES = 3;
40
+ /** The keeper's ramp: a plank from (x0, y0) on the roof rising at `slope` (tan 12°) to x1. */
41
+ const RAMP = { x0: 4.5, y0: 2, x1: 7.6, slope: 0.21256, sinHalf: 0.104528, cosHalf: 0.994522 } as const;
42
+ /** The ramp's top at x. */
43
+ const rampY = (x: number): number => RAMP.y0 + RAMP.slope * (x - RAMP.x0);
44
+ /** The keeper's ankles and its root above the ramp (its legs reach 1.1 from a hip 1.1 up: a slight bend), the lantern from its feet, in world units. */
45
+ const ANKLE = 0.12;
46
+ const LIFT = 0.08;
47
+ const LANTERN = { x: 0.25, y: 1.0 } as const;
48
+ /** The keeper's gait: the walk clip's 0.8 s cycle carries it a stride of 0.4 m (a step per foot), each foot lifted at most `lift` mid-swing. */
49
+ const WALK = { speed: 0.5, stride: 0.4, lift: 0.08 } as const;
31
50
 
32
51
  /** The courier. */
33
52
  export const Hero = defineComponent("Hero", { spawnX: f32(0, { unit: "m" }), spawnY: f32(0, { unit: "m" }) });
@@ -35,6 +54,9 @@ export const Hero = defineComponent("Hero", { spawnX: f32(0, { unit: "m" }), spa
35
54
  /** A coin of the map's `coin` objects; taken once. */
36
55
  export const Coin = defineComponent("Coin", { taken: bool() });
37
56
 
57
+ /** One of the keeper's feet: its IK target stays on the ramp `dx` ahead of the keeper (behind when negative), `lag` of a gait cycle after the front foot. */
58
+ export const Plant = defineComponent("Plant", { dx: f32(0, { unit: "m" }), lag: f32(0, { min: 0, max: 1 }) });
59
+
38
60
  /** A voice: the sound bit (Sound) it plays when Game.sfx asks for it. */
39
61
  export const Voice = defineComponent("Voice", { bit: u32() });
40
62
 
@@ -58,6 +80,21 @@ export const Game = defineResource("Game", {
58
80
  /** The HUD's touch controls this tick (a phone): the run and the held jump. */
59
81
  touchX: f32(0, { min: -1, max: 1 }),
60
82
  touchJump: bool(),
83
+ /** The keeper's breaths: og.Skeleton2DEvent "inhale" (the idle clip's event at 0). */
84
+ inhales: u32(),
85
+ /** The keeper's footfalls: og.Skeleton2DEvent "step" (the walk clip's, one per foot). */
86
+ steps: u32(),
87
+ });
88
+
89
+ /** Where the keeper walks (`keeper:<x>`), how far it has walked since it set off (its gait's phase), and its clips. */
90
+ export const KeeperWalk = defineResource("KeeperWalk", {
91
+ goal: f32(6, { unit: "m" }),
92
+ walked: f32(0, { unit: "m" }),
93
+ walking: bool(),
94
+ /** -1 facing left (towards the spawn), 1 right. */
95
+ facing: f32(-1),
96
+ idle: u32(),
97
+ walk: u32(),
61
98
  });
62
99
 
63
100
  function must<T>(r: Result<T>): T {
@@ -161,6 +198,80 @@ export const Animate = defineSystem("Animate", { reads: [Hero, og.PlatformerCont
161
198
  }
162
199
  });
163
200
 
201
+ /**
202
+ * Walks the keeper towards KeeperWalk.goal along the ramp at the gait's
203
+ * speed (by scaled time, as the clip plays: paused, both hold), facing the
204
+ * way it goes, with the walk clip; on arrival it faces the spawn again with
205
+ * the idle clip (which starts over, breathing in).
206
+ */
207
+ export const Walk = defineSystem("Walk", { writes: [KeeperWalk, og.Skeleton2D, og.Transform], after: ["Control"] }, (_q, time, world) => {
208
+ const keeper = world.find("keeper");
209
+ if (keeper === null) return;
210
+ const w = must(world.resource(KeeperWalk));
211
+ const x = must(world.get(keeper, og.Transform)).position.x;
212
+ const left = w.goal - x;
213
+ if (left === 0 && !w.walking) return;
214
+ const step = WALK.speed * time.scaledDt;
215
+ const arrived = Math.abs(left) <= step;
216
+ const nx = arrived ? w.goal : x + Math.sign(left) * step;
217
+ w.walked = arrived ? 0 : (w.walking ? w.walked : 0) + Math.abs(nx - x);
218
+ w.walking = !arrived;
219
+ w.facing = arrived ? -1 : Math.sign(left);
220
+ must(world.set(keeper, og.Transform, { position: { x: nx, y: rampY(nx) + LIFT, z: 0 } }));
221
+ const s = must(world.get(keeper, og.Skeleton2D));
222
+ const clip = arrived ? w.idle : w.walk;
223
+ const flipX = w.facing < 0;
224
+ // Only what changed: a new clip starts at 0 (the idle clip's breath, the walk's first step).
225
+ if (s.clip !== clip || s.flipX !== flipX) must(world.set(keeper, og.Skeleton2D, { clip, flipX }));
226
+ });
227
+
228
+ /**
229
+ * Puts the keeper's feet on the ramp (og.IkTarget2D: the legs' two-bone IK
230
+ * reaches each target, the knees bend as the hip bobs). Standing, each foot
231
+ * is `dx` from the keeper; walking, the front foot's gait phase is the
232
+ * distance walked over the stride and the back foot's half a cycle later:
233
+ * for half a cycle a foot stays where it is on the plank while the keeper
234
+ * passes over it, for the other half it swings a stride forward, lifted on
235
+ * a parabola. Keeps the lantern's light at its hand, and counts the
236
+ * clips' breaths and footfalls.
237
+ */
238
+ export const Plant2D = defineSystem("Plant", { reads: [Plant, KeeperWalk, og.Skeleton2DEvent], writes: [Game, og.IkTarget2D, og.Transform], after: ["Walk"] }, (q, _time, world) => {
239
+ const keeper = world.find("keeper");
240
+ if (keeper === null) return;
241
+ const at = must(world.get(keeper, og.Transform)).position;
242
+ const w = must(world.resource(KeeperWalk));
243
+ for (const a of q.archetypes()) {
244
+ const plant = a.column(Plant);
245
+ const target = a.column(og.IkTarget2D).target;
246
+ for (let i = 0; i < a.count; i++) {
247
+ let ahead = plant.dx[i]!;
248
+ let lift = 0;
249
+ if (w.walking) {
250
+ const phase = w.walked / WALK.stride + plant.lag[i]!;
251
+ const p = phase - Math.floor(phase);
252
+ if (p < 0.5) {
253
+ // Planted: it slides back under the keeper as fast as the keeper walks.
254
+ ahead += WALK.stride * (0.25 - p);
255
+ } else {
256
+ const s = (p - 0.5) * 2;
257
+ ahead += WALK.stride * (s - 0.5) * 0.5;
258
+ lift = WALK.lift * 4 * s * (1 - s);
259
+ }
260
+ }
261
+ const x = at.x + w.facing * ahead;
262
+ target[i * 2] = x;
263
+ target[i * 2 + 1] = rampY(x) + ANKLE + lift;
264
+ }
265
+ }
266
+ const lantern = world.find("lantern");
267
+ if (lantern !== null) must(world.set(lantern, og.Transform, { position: { x: at.x - w.facing * LANTERN.x, y: at.y + LANTERN.y, z: 0 } }));
268
+ const g = must(world.resource(Game));
269
+ for (const e of world.events(og.Skeleton2DEvent)) {
270
+ if (e.name === "inhale") g.inhales += 1;
271
+ else if (e.name === "step") g.steps += 1;
272
+ }
273
+ });
274
+
164
275
  /** Plays each sound asked for once: a voice still playing is stopped this tick and played again the next. */
165
276
  export const Voices = defineSystem("Voices", { reads: [Voice], writes: [Game, og.AudioSource], after: ["Pickups"] }, (q, _time, world) => {
166
277
  const g = must(world.resource(Game));
@@ -212,6 +323,42 @@ export const Hud = defineSystem("Hud", { writes: [Game] }, (_q, _time, world) =>
212
323
  }
213
324
  });
214
325
 
326
+ /**
327
+ * The lamp keeper: a plank ramp, the rig on it facing the spawn, a foot
328
+ * target per leg, and its lantern's light; og.Lighting2D lights layers 1
329
+ * and 2 (the keeper, the courier, the coins), the city stays as drawn.
330
+ */
331
+ function spawnKeeper(world: World, rig: Rig2DInfo): void {
332
+ const idle = og.anim.clip(rig, "idle");
333
+ const walk = og.anim.clip(rig, "walk");
334
+ if (idle < 0 || walk < 0) throw new Error("rigs/keeper.ogt needs clips idle and walk");
335
+ const mid = (RAMP.x0 + RAMP.x1) / 2;
336
+ const plank = must(world.spawn({ name: "ramp", position: { x: mid, y: rampY(mid) - 0.2, z: 0 }, quaternion: { x: 0, y: 0, z: RAMP.sinHalf, w: RAMP.cosHalf } }));
337
+ // The plank is a region of the keeper's texture (pixels 24..64 x 32..40 of 64 x 64).
338
+ must(world.insert(plank, og.Sprite, { texture: rig.texture, width: RAMP.x1 - RAMP.x0, height: 0.4, uvMin: { x: 0.375, y: 0.5 }, uvMax: { x: 1, y: 0.625 }, layer: 1 }));
339
+ const x = 6;
340
+ const keeper = must(world.spawn({ name: "keeper", position: { x, y: rampY(x) + LIFT, z: 0 } }));
341
+ must(world.insert(keeper, og.Skeleton2D, { rig: rig.rig, clip: idle, flipX: true, layer: 1 }));
342
+ const w = must(world.resource(KeeperWalk));
343
+ Object.assign(w, { goal: x, walked: 0, walking: false, facing: -1, idle, walk });
344
+ // Facing left, the front foot is the left one; the back foot steps half a cycle later.
345
+ for (const [name, ahead, lag] of [["leg-f", 0.15, 0], ["leg-b", -0.15, 0.5]] as const) {
346
+ const constraint = rig.constraints.indexOf(name);
347
+ if (constraint < 0) throw new Error(`rigs/keeper.ogt has no ik ${name}`);
348
+ const fx = x - ahead;
349
+ const foot = must(world.spawn({ name: `keeper-${name}` }));
350
+ must(world.insert(foot, og.IkTarget2D, { skeleton: keeper, constraint, target: { x: fx, y: rampY(fx) + ANKLE }, tolerance: 0.02 }));
351
+ must(world.insert(foot, Plant, { dx: ahead, lag }));
352
+ }
353
+ const light = must(world.spawn({ name: "lantern", position: { x: x + LANTERN.x, y: rampY(x) + LIFT + LANTERN.y, z: 0 } }));
354
+ must(world.insert(light, og.Light2D, { color: { r: 1, g: 0.72, b: 0.35 }, intensity: 1.6, radius: 4, castShadows: false }));
355
+ const lighting = must(world.resource(og.Lighting2D));
356
+ lighting.enabled = true;
357
+ lighting.ambient = { r: 0.62, g: 0.64, b: 0.78 };
358
+ lighting.minLayer = 1;
359
+ lighting.maxLayer = 2;
360
+ }
361
+
215
362
  function checkClips(info: FlipbookInfo): void {
216
363
  for (const [name, index] of Object.entries(CLIP)) {
217
364
  const at = og.anim.clip(info, name);
@@ -229,13 +376,14 @@ export default og.game({
229
376
  look: "looks/origin-game-2d.ogt",
230
377
  tick: 60,
231
378
  seed: 1,
232
- resources: [Game],
233
- systems: [Hud, Control, Pickups, Animate, Voices],
379
+ resources: [Game, KeeperWalk],
380
+ systems: [Hud, Control, Pickups, Animate, Walk, Plant2D, Voices],
234
381
  start(world) {
235
382
  const map = must(og.load.tilemap(world, "maps/level.ogt"));
236
383
  const courier = must(og.load.flipbook(world, "sprites/courier.ogt"));
237
384
  checkClips(courier);
238
385
  const coin = must(og.load.flipbook(world, "sprites/coin.ogt"));
386
+ spawnKeeper(world, must(og.load.rig2d(world, "rigs/keeper.ogt")));
239
387
  const height = map.height * map.tileHeight;
240
388
 
241
389
  const camera = must(world.resource(og.Camera));
@@ -293,13 +441,20 @@ export default og.game({
293
441
  if (hero === null || !Number.isFinite(x) || !Number.isFinite(y)) throw new Error("write warp:<x>:<y>");
294
442
  must(world.set(hero, og.Transform, { position: { x, y, z: 0 } }));
295
443
  },
444
+ /** `keeper:<x>`: send the keeper walking along the ramp to x (the Walk system moves it; its feet step on the plank). */
445
+ keeper: (world, args) => {
446
+ const x = Math.max(RAMP.x0 + 0.4, Math.min(RAMP.x1 - 0.4, Number(args[0])));
447
+ if (world.find("keeper") === null || !Number.isFinite(x)) throw new Error("write keeper:<x>");
448
+ must(world.resource(KeeperWalk)).goal = x;
449
+ },
296
450
  },
297
451
  commandHelp: {
298
452
  warp: { usage: "<x>:<y>", doc: "Put the courier's feet at a world point." },
453
+ keeper: { usage: "<x>", doc: "Send the lamp keeper walking along its ramp to x." },
299
454
  },
300
455
  facts: (world) => {
301
456
  const g = world.resource(Game);
302
457
  if (!g.ok) return {};
303
- return { coins: g.value.coins, total: g.value.total, lives: g.value.lives, won: g.value.won, x: g.value.heroX, y: g.value.heroY };
458
+ return { coins: g.value.coins, total: g.value.total, lives: g.value.lives, won: g.value.won, inhales: g.value.inhales, steps: g.value.steps, x: g.value.heroX, y: g.value.heroY };
304
459
  },
305
460
  });
@@ -0,0 +1,93 @@
1
+ // The lamp keeper's playtest (og-game §8, 2D skeletons): the rig plays its
2
+ // idle clip (its event counted as breaths) with both legs' IK targets on
3
+ // the ramp and reached; `keeper:7` sends it walking up the ramp with its
4
+ // walk clip, facing the way it goes, one foot planted on the plank while
5
+ // the other swings above it; it arrives, faces the spawn and breathes again
6
+ // with both feet planted, higher.
7
+ import { playtest } from "@origingame/engine/playtest";
8
+
9
+ interface Got<T> {
10
+ value: T;
11
+ }
12
+
13
+ type Fields = Record<string, number | boolean | string | number[]>;
14
+
15
+ interface EntityDump {
16
+ components: Record<string, Fields>;
17
+ }
18
+
19
+ /** The ramp's top at x (src/main.ts's RAMP), plus the ankle's height. */
20
+ const ankleY = (x: number): number => 2 + 0.21256 * (x - 4.5) + 0.12;
21
+ /** The plank's ends and the highest a swinging foot lifts (src/main.ts's RAMP and WALK). */
22
+ const PLANK = { x0: 4.5, x1: 7.6 } as const;
23
+ const LIFT = 0.08;
24
+ /** The rig's clips, in file order. */
25
+ const IDLE = 0;
26
+ const WALK = 1;
27
+
28
+ export default playtest({
29
+ name: "The lamp keeper breathes on its ramp with both feet planted, walks up the slope stepping on the plank, and stands planted again",
30
+ commands:
31
+ "1.0:screenshot:keeper.png;1.01:dump_entity:keeper:keeper.json;1.02:dump_entity:keeper-leg-f:front.json;1.03:dump_entity:keeper-leg-b:back.json;" +
32
+ "2.1:get:Game:game.json;2.2:keeper:7;" +
33
+ "2.6:screenshot:walk.png;2.61:dump_entity:keeper:walk.json;2.62:dump_entity:keeper-leg-f:walk-front.json;2.63:dump_entity:keeper-leg-b:walk-back.json;" +
34
+ "2.9:dump_entity:keeper:walk-later.json;2.91:dump_entity:keeper-leg-f:walk-front-later.json;2.92:dump_entity:keeper-leg-b:walk-back-later.json;" +
35
+ "4.6:dump_entity:keeper:keeper-up.json;4.61:dump_entity:keeper-leg-f:front-up.json;4.62:dump_entity:keeper-leg-b:back-up.json;4.7:get:Game:game-end.json;4.8:quit",
36
+ check(run, t) {
37
+ t.ok(run.quit, "the script reached quit");
38
+ for (const png of ["keeper.png", "walk.png"]) {
39
+ t.frame(png, () => {
40
+ const frame = run.png(png);
41
+ t.ok(frame.width > 0 && frame.height > 0, `a ${frame.width}x${frame.height} frame was captured`);
42
+ });
43
+ }
44
+ const skeleton = (file: string) => run.json<EntityDump>(file).components["og.Skeleton2D"]!;
45
+ const at = (file: string) => (run.json<EntityDump>(file).components["og.Transform"]!["position"] as number[])[0]!;
46
+ const foot = (file: string) => run.json<EntityDump>(file).components["og.IkTarget2D"]!;
47
+
48
+ // Standing: the idle clip, both feet on the ramp under the keeper.
49
+ const idle = skeleton("keeper.json");
50
+ t.ok(idle["clip"] === IDLE && (idle["time"] as number) > 0.9 && (idle["time"] as number) < 1.1, `the idle clip plays (clip ${idle["clip"]}, time ${idle["time"]})`);
51
+ for (const [file, label, dx, x0] of [["front.json", "front", -0.15, 6], ["back.json", "back", 0.15, 6], ["front-up.json", "front after the walk", -0.15, 7], ["back-up.json", "back after the walk", 0.15, 7]] as const) {
52
+ const ik = foot(file);
53
+ const target = ik["target"] as number[];
54
+ const x = x0 + dx;
55
+ t.ok(Math.abs(target[0]! - x) < 1e-4 && Math.abs(target[1]! - ankleY(x)) < 1e-4, `the ${label} foot's target is on the ramp (${target.join(", ")})`);
56
+ t.ok(ik["reached"] === true, `the ${label} foot reached it (error ${ik["error"]})`);
57
+ }
58
+
59
+ // Walking: the walk clip, facing up the ramp, the keeper between where it was and where it goes.
60
+ let planted = 0;
61
+ for (const [keeper, front, back] of [["walk.json", "walk-front.json", "walk-back.json"], ["walk-later.json", "walk-front-later.json", "walk-back-later.json"]] as const) {
62
+ const s = skeleton(keeper);
63
+ const x = at(keeper);
64
+ t.ok(s["clip"] === WALK && s["flipX"] === false, `walking, the keeper plays its walk clip facing right (clip ${s["clip"]}, flipX ${s["flipX"]})`);
65
+ t.ok(x > 6 && x < 7, `walking, it is part of the way along the ramp (x ${x})`);
66
+ for (const [file, label] of [[front, "front"], [back, "back"]] as const) {
67
+ const ik = foot(file);
68
+ const [fx, fy] = ik["target"] as [number, number];
69
+ const lift = fy - ankleY(fx);
70
+ t.ok(fx > PLANK.x0 && fx < PLANK.x1 && lift > -1e-4 && lift <= LIFT + 1e-4, `walking, the ${label} foot is on the plank or above it (x ${fx}, ${lift.toFixed(4)} up)`);
71
+ t.ok(ik["reached"] === true, `walking, the ${label} foot reached its target (error ${ik["error"]})`);
72
+ if (Math.abs(lift) < 1e-4) planted += 1;
73
+ }
74
+ }
75
+ t.ok(planted >= 2, `while it walks a foot stays planted on the plank (${planted} of 4 targets on it)`);
76
+ // The back foot carried the keeper from 2.6 s to 2.9 s: it did not move.
77
+ const before = foot("walk-back.json")["target"] as number[];
78
+ const later = foot("walk-back-later.json")["target"] as number[];
79
+ t.ok(Math.abs(before[0]! - later[0]!) < 1e-4 && Math.abs(before[1]! - later[1]!) < 1e-4, `the planted foot stays put while the keeper passes over it (${before.join(", ")} then ${later.join(", ")})`);
80
+
81
+ // Arrived: idle again, facing the spawn, at x 7.
82
+ const up = skeleton("keeper-up.json");
83
+ t.ok(up["clip"] === IDLE && up["flipX"] === true && Math.abs(at("keeper-up.json") - 7) < 1e-4, `it arrived at 7 and breathes facing the spawn (clip ${up["clip"]}, x ${at("keeper-up.json")})`);
84
+
85
+ // A loop of 2 s from tick 0 to 2.1 s passed its event at 0 and at 2; the walk
86
+ // (2 s at 0.5 m/s, a step every 0.4 s) stepped 5 times, and the idle clip
87
+ // started over on arrival with a breath.
88
+ const game = run.json<Got<{ inhales: number }>>("game.json").value;
89
+ t.ok(game.inhales === 2, `og.Skeleton2DEvent counted two breaths before the walk (${game.inhales})`);
90
+ const end = run.json<Got<{ inhales: number; steps: number }>>("game-end.json").value;
91
+ t.ok(end.steps === 5 && end.inhales === 3, `the walk clip's events counted 5 steps, then a third breath (${end.steps}, ${end.inhales})`);
92
+ },
93
+ });
@@ -1,111 +0,0 @@
1
- type skin
2
- version 1
3
- ---
4
- # Path Guard's skin: the engine's cartoon style pack (templates/ui-kit/tools/make-skins.py; CC0 art, OFL fonts;
5
- # LICENSES.md) in a night-garden palette: cream panels on the dark lawn, the lantern's orange as the one accent.
6
- name "pathguard"
7
- base light
8
- atlas "skins/pathguard/atlas.png" 384 256
9
- scale 1
10
- palette {
11
- bg "#10231a"
12
- bgSubtle "#15301f"
13
- surface "#fff4dc"
14
- surfaceRaised "#fff4dc"
15
- surfaceHover "#ffe9bd"
16
- materialThick "#fff4dcf5"
17
- materialRegular "#fff4dcee"
18
- materialThin "#fff4dce0"
19
- fg "#0f1a14"
20
- fgMuted "#3a483f"
21
- fgSubtle "#45534a"
22
- fgInverse "#fff4dc"
23
- fill "#ffe08a"
24
- fillStrong "#ffd466"
25
- border "#20302a"
26
- borderStrong "#20302a"
27
- accent "#b44c0d"
28
- accentHover "#ab480c"
29
- accentActive "#a0430b"
30
- accentFg "#fff4dc"
31
- accentInk "#a8400a"
32
- accentSoft "#ff8c3a40"
33
- focus "#5cb6e8"
34
- thumb "#ffffff"
35
- scrim "#07120ccc"
36
- success "#06681b"
37
- warn "#7b5102"
38
- danger "#700b1c"
39
- info "#2256af"
40
- shadow "#0b1a1299"
41
- }
42
- radius {
43
- control 12
44
- field 14
45
- block 14
46
- card 18
47
- panel 28
48
- sheet 28
49
- button 9999
50
- }
51
- ramp {
52
- body 15 20
53
- meta 13 18
54
- caption 13 18
55
- lg 19 26
56
- xl 22 28
57
- xl2 26 30
58
- xl3 30 34
59
- displaySm 32 36
60
- displayMd 40 44
61
- displayLg 52 56
62
- }
63
- durations {
64
- instant 110
65
- fast 160
66
- base 280
67
- slow 420
68
- }
69
- fonts {
70
- body "skins/pathguard/Fredoka-SemiBold.ttf"
71
- display "skins/pathguard/LilitaOne-Regular.ttf"
72
- numeric "skins/pathguard/LilitaOne-Regular.ttf"
73
- fallback "fonts/NotoSansSC-subset.ttf"
74
- }
75
- icons "skins/pathguard/icons.png" 8 4 {
76
- names "heart" "star" "coin" "lock" "pause" "gear" "sword" "shield" "potion" "gem" "key" "map" "bolt" "clock" "trophy" "home" "plus" "close" "check" "play" "back" "bag" "bomb" "crown" "flag" "apple" "leaf" "fire" "drop" "skull" "music" "person"
77
- }
78
- slice panel 4 4 96 96 30 tint
79
- slice card 104 4 64 64 20 tint
80
- slice button 172 4 48 48 20 tint
81
- slice buttonHover 224 4 48 48 20 tint
82
- slice buttonPressed 276 4 48 48 20 tint
83
- slice buttonDisabled 328 4 48 48 20 tint
84
- slice field 4 104 48 48 16 tint
85
- slice slot 56 104 48 48 16 tint
86
- slice tooltip 108 104 32 32 13 tint
87
- slice capsule 144 104 32 32 15 tint
88
- slice track 180 104 32 16 7 tint
89
- slice fill 216 104 32 16 7 tint
90
- soft 252 104 64 64 24
91
- divider 4 172 128 24 48 16 accent
92
- flourish 136 172 64 24 22 10 accent 2
93
- shadow panel 0 8 0 0 "#0b1a12cc"
94
- shadow card 0 5 0 0 "#0b1a1299"
95
- shadow button 0 5 0 0 "#0b1a12cc"
96
- shadow capsule 0 4 0 0 "#0b1a1299"
97
- shadow slot 0 3 0 0 "#0b1a1266"
98
- textShadow display 0 4 0 "#0b1a12"
99
- # Body text gets a hairline of its own ink to the right: the Chinese fallback
100
- # (Noto Sans SC Regular) reads at the weight of the Latin face.
101
- textShadow body 1 0 0 "#20302a"
102
- press 1.08 0.88 3
103
- hover -3 1.04
104
- motion {
105
- enter pop
106
- stagger 50
107
- toast pop
108
- progress 360
109
- shake 8
110
- pop 0.22
111
- }