@origingame/create-game 0.2.0 → 0.4.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 (163) hide show
  1. package/README.md +2 -2
  2. package/archetypes/README.md +2 -1
  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 +5 -5
  23. package/archetypes/idle/assets/looks/idle.ogt +2 -2
  24. package/archetypes/idle/assets/{skins → styles}/sparkorb/LICENSES.md +6 -6
  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/components.ts +1 -1
  32. package/archetypes/idle/src/main.ts +5 -5
  33. package/archetypes/idle/src/ui.ts +13 -13
  34. package/archetypes/idle/tools/make-assets.py +5 -5
  35. package/archetypes/match3/package.json +1 -1
  36. package/archetypes/match3/project.ogt +1 -1
  37. package/archetypes/rhythm/LICENSES.md +1 -1
  38. package/archetypes/rhythm/README.md +5 -5
  39. package/archetypes/rhythm/assets/{skins → styles}/lanebeat/LICENSES.md +6 -6
  40. package/archetypes/rhythm/assets/{skins/lanebeat/skin.ogt → styles/lanebeat/style.ogt} +45 -45
  41. package/archetypes/rhythm/package.json +1 -1
  42. package/archetypes/rhythm/project.ogt +1 -1
  43. package/archetypes/rhythm/references/metal/results.png +0 -0
  44. package/archetypes/rhythm/references/metal/title.png +0 -0
  45. package/archetypes/rhythm/src/components.ts +1 -1
  46. package/archetypes/rhythm/src/main.ts +3 -3
  47. package/archetypes/rhythm/src/ui.ts +12 -12
  48. package/archetypes/roguelike/LICENSES.md +1 -1
  49. package/archetypes/roguelike/README.md +5 -5
  50. package/archetypes/roguelike/assets/{skins → styles}/lampdive/LICENSES.md +6 -6
  51. package/archetypes/roguelike/assets/{skins/lampdive/skin.ogt → styles/lampdive/style.ogt} +41 -41
  52. package/archetypes/roguelike/package.json +1 -1
  53. package/archetypes/roguelike/project.ogt +1 -1
  54. package/archetypes/roguelike/references/metal/over.png +0 -0
  55. package/archetypes/roguelike/references/metal/title.png +0 -0
  56. package/archetypes/roguelike/src/components.ts +1 -1
  57. package/archetypes/roguelike/src/main.ts +2 -2
  58. package/archetypes/roguelike/src/ui.ts +8 -8
  59. package/archetypes/runner/package.json +1 -1
  60. package/archetypes/runner/project.ogt +1 -1
  61. package/archetypes/third-person/LICENSES.md +1 -1
  62. package/archetypes/third-person/README.md +4 -4
  63. package/archetypes/third-person/assets/{skins → styles}/ember/LICENSES.md +4 -4
  64. package/archetypes/third-person/assets/{skins/ember/skin.ogt → styles/ember/style.ogt} +40 -41
  65. package/archetypes/third-person/package.json +1 -1
  66. package/archetypes/third-person/project.ogt +1 -1
  67. package/archetypes/third-person/references/metal/beacon.png +0 -0
  68. package/archetypes/third-person/references/metal/dawn.png +0 -0
  69. package/archetypes/third-person/references/metal/title.png +0 -0
  70. package/archetypes/third-person/references/webgl2/beacon.png +0 -0
  71. package/archetypes/third-person/src/components.ts +1 -1
  72. package/archetypes/third-person/src/main.ts +3 -3
  73. package/archetypes/third-person/src/sound.ts +1 -1
  74. package/archetypes/third-person/src/ui.ts +11 -11
  75. package/archetypes/topdown/package.json +1 -1
  76. package/archetypes/topdown/project.ogt +1 -1
  77. package/archetypes/tower-defense/LICENSES.md +1 -1
  78. package/archetypes/tower-defense/README.md +5 -5
  79. package/archetypes/tower-defense/assets/{skins → styles}/pathguard/LICENSES.md +4 -4
  80. package/archetypes/tower-defense/assets/styles/pathguard/atlas.png +0 -0
  81. package/archetypes/tower-defense/assets/styles/pathguard/style.ogt +111 -0
  82. package/archetypes/tower-defense/package.json +1 -1
  83. package/archetypes/tower-defense/project.ogt +1 -1
  84. package/archetypes/tower-defense/references/metal/end.png +0 -0
  85. package/archetypes/tower-defense/references/metal/title.png +0 -0
  86. package/archetypes/tower-defense/references/metal/wave.png +0 -0
  87. package/archetypes/tower-defense/src/components.ts +1 -1
  88. package/archetypes/tower-defense/src/main.ts +2 -2
  89. package/archetypes/tower-defense/src/ui.ts +20 -18
  90. package/bin/create.ts +11 -8
  91. package/looks/catalogue.json +40 -40
  92. package/package.json +2 -2
  93. package/template/README.md +2 -1
  94. package/template/assets/audio/sting.ogt +10 -0
  95. package/template/assets/timelines/intro.ogt +101 -0
  96. package/template/assets/timelines/narration.srt +11 -0
  97. package/template/host.d.ts +46 -0
  98. package/template/package.json +1 -1
  99. package/template/project.ogt +1 -1
  100. package/template/references/metal/intro-4.png +0 -0
  101. package/template/references/metal/intro-8.png +0 -0
  102. package/template/src/main.ts +1 -1
  103. package/template/tests/frames.ts +134 -0
  104. package/template/tests/intro.og.ts +65 -0
  105. package/template/tests/smoke.og.ts +30 -36
  106. package/template/tsconfig.json +1 -1
  107. package/template-2d/LICENSES.md +1 -0
  108. package/template-2d/README.md +6 -2
  109. package/template-2d/assets/rigs/keeper.ogt +292 -0
  110. package/template-2d/assets/rigs/keeper.png +0 -0
  111. package/template-2d/assets/rigs/keeper.png.ogt +7 -0
  112. package/template-2d/package.json +1 -1
  113. package/template-2d/project.ogt +1 -1
  114. package/template-2d/src/main.ts +240 -4
  115. package/template-2d/tests/keeper.og.ts +178 -0
  116. package/archetypes/tower-defense/assets/skins/pathguard/atlas.png +0 -0
  117. package/archetypes/tower-defense/assets/skins/pathguard/skin.ogt +0 -111
  118. /package/archetypes/idle/assets/{skins → styles}/sparkorb/ChakraPetch-BoldItalic.ttf +0 -0
  119. /package/archetypes/idle/assets/{skins → styles}/sparkorb/ChakraPetch-BoldItalic.ttf.ogt +0 -0
  120. /package/archetypes/idle/assets/{skins → styles}/sparkorb/OFL.txt +0 -0
  121. /package/archetypes/idle/assets/{skins → styles}/sparkorb/Orbitron-Medium.ttf +0 -0
  122. /package/archetypes/idle/assets/{skins → styles}/sparkorb/Orbitron-Medium.ttf.ogt +0 -0
  123. /package/archetypes/idle/assets/{skins → styles}/sparkorb/Rajdhani-Bold.ttf +0 -0
  124. /package/archetypes/idle/assets/{skins → styles}/sparkorb/Rajdhani-Bold.ttf.ogt +0 -0
  125. /package/archetypes/idle/assets/{skins → styles}/sparkorb/atlas.png +0 -0
  126. /package/archetypes/idle/assets/{skins → styles}/sparkorb/atlas.png.ogt +0 -0
  127. /package/archetypes/idle/assets/{skins → styles}/sparkorb/icons.png +0 -0
  128. /package/archetypes/idle/assets/{skins → styles}/sparkorb/icons.png.ogt +0 -0
  129. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/ChakraPetch-Medium.ttf +0 -0
  130. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/ChakraPetch-Medium.ttf.ogt +0 -0
  131. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/JetBrainsMono-Regular.ttf +0 -0
  132. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/JetBrainsMono-Regular.ttf.ogt +0 -0
  133. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/OFL.txt +0 -0
  134. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/Orbitron-Bold.ttf +0 -0
  135. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/Orbitron-Bold.ttf.ogt +0 -0
  136. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/atlas.png +0 -0
  137. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/atlas.png.ogt +0 -0
  138. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/icons.png +0 -0
  139. /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/icons.png.ogt +0 -0
  140. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/DotGothic16-Regular.ttf +0 -0
  141. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/DotGothic16-Regular.ttf.ogt +0 -0
  142. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/OFL.txt +0 -0
  143. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/atlas.png +0 -0
  144. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/atlas.png.ogt +0 -0
  145. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/icons.png +0 -0
  146. /package/archetypes/roguelike/assets/{skins → styles}/lampdive/icons.png.ogt +0 -0
  147. /package/archetypes/third-person/assets/{skins → styles}/ember/Cinzel-Bold.ttf +0 -0
  148. /package/archetypes/third-person/assets/{skins → styles}/ember/Cinzel-Bold.ttf.ogt +0 -0
  149. /package/archetypes/third-person/assets/{skins → styles}/ember/Newsreader-Medium.ttf +0 -0
  150. /package/archetypes/third-person/assets/{skins → styles}/ember/Newsreader-Medium.ttf.ogt +0 -0
  151. /package/archetypes/third-person/assets/{skins → styles}/ember/OFL.txt +0 -0
  152. /package/archetypes/third-person/assets/{skins → styles}/ember/atlas.png +0 -0
  153. /package/archetypes/third-person/assets/{skins → styles}/ember/atlas.png.ogt +0 -0
  154. /package/archetypes/third-person/assets/{skins → styles}/ember/icons.png +0 -0
  155. /package/archetypes/third-person/assets/{skins → styles}/ember/icons.png.ogt +0 -0
  156. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/Fredoka-SemiBold.ttf +0 -0
  157. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/Fredoka-SemiBold.ttf.ogt +0 -0
  158. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/LilitaOne-Regular.ttf +0 -0
  159. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/LilitaOne-Regular.ttf.ogt +0 -0
  160. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/OFL.txt +0 -0
  161. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/atlas.png.ogt +0 -0
  162. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/icons.png +0 -0
  163. /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/icons.png.ogt +0 -0
@@ -0,0 +1,178 @@
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. Every tick from the command to the end
7
+ // is dumped: the turns take several ticks, and no foot ever moves further
8
+ // in a tick than the gait's own swing does, setting off and stopping
9
+ // included; the idle clip comes back only once both feet are down.
10
+ import { playtest } from "@origingame/engine/playtest";
11
+
12
+ interface Got<T> {
13
+ value: T;
14
+ }
15
+
16
+ type Fields = Record<string, number | boolean | string | number[]>;
17
+
18
+ interface EntityDump {
19
+ components: Record<string, Fields>;
20
+ }
21
+
22
+ /** The ramp's top at x (src/main.ts's RAMP), plus the ankle's height. */
23
+ const ankleY = (x: number): number => 2 + 0.21256 * (x - 4.5) + 0.12;
24
+ /** The plank's ends and the highest a swinging foot lifts (src/main.ts's RAMP and WALK). */
25
+ const PLANK = { x0: 4.5, x1: 7.6 } as const;
26
+ const LIFT = 0.08;
27
+ /** The rig's clips, in file order. */
28
+ const IDLE = 0;
29
+ const WALK = 1;
30
+ /**
31
+ * The most a foot moves in a 60 Hz tick: the gait's swing at its start,
32
+ * worked out by hand from src/main.ts's WALK and RAMP. Forward twice the
33
+ * walking speed, 1 m/s; up the 0.21256 slope at that, plus the lift's
34
+ * parabola at its steepest, 4 x 0.08 m over the swing's half cycle of
35
+ * 0.4 s (2.5 per second of its parameter), 0.8 m/s: 1.01256 m/s. Together
36
+ * 1.42316 m/s, 0.023719 m a tick.
37
+ */
38
+ const STRIDE_STEP = 0.023719;
39
+ /** The ticks dumped every tick: from just before `keeper:7` (2.2 s) to the end (5 s). */
40
+ const FIRST = 130;
41
+ const LAST = 300;
42
+ const perTick = Array.from({ length: LAST - FIRST + 1 }, (_, i) => {
43
+ const k = FIRST + i;
44
+ const at = (j: number) => (k / 60 + 0.002 * j).toFixed(4);
45
+ return `${at(1)}:dump_entity:keeper:k${k}.json;${at(2)}:dump_entity:keeper-leg-f:f${k}.json;${at(3)}:dump_entity:keeper-leg-b:b${k}.json`;
46
+ }).join(";");
47
+
48
+ export default playtest({
49
+ name: "The lamp keeper breathes on its ramp with both feet planted, walks up the slope stepping on the plank, and stands planted again",
50
+ commands: [
51
+ "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",
52
+ "2.1:get:Game:game.json;2.2:keeper:7",
53
+ "2.85:screenshot:walk.png;2.851:dump_entity:keeper:walk.json;2.852:dump_entity:keeper-leg-f:walk-front.json;2.853:dump_entity:keeper-leg-b:walk-back.json",
54
+ "3.15:dump_entity:keeper:walk-later.json;3.151:dump_entity:keeper-leg-f:walk-front-later.json;3.152:dump_entity:keeper-leg-b:walk-back-later.json",
55
+ perTick,
56
+ "5.1:dump_entity:keeper:keeper-up.json;5.11:dump_entity:keeper-leg-f:front-up.json;5.12:dump_entity:keeper-leg-b:back-up.json;5.2:get:Game:game-end.json;5.3:quit",
57
+ ]
58
+ .join(";")
59
+ .split(";")
60
+ .sort((a, b) => Number(a.split(":")[0]) - Number(b.split(":")[0]))
61
+ .join(";"),
62
+ check(run, t) {
63
+ t.ok(run.quit, "the script reached quit");
64
+ for (const png of ["keeper.png", "walk.png"]) {
65
+ t.frame(png, () => {
66
+ const frame = run.png(png);
67
+ t.ok(frame.width > 0 && frame.height > 0, `a ${frame.width}x${frame.height} frame was captured`);
68
+ });
69
+ }
70
+ const skeleton = (file: string) => run.json<EntityDump>(file).components["og.Skeleton2D"]!;
71
+ const at = (file: string) => (run.json<EntityDump>(file).components["og.Transform"]!["position"] as number[])[0]!;
72
+ const foot = (file: string) => run.json<EntityDump>(file).components["og.IkTarget2D"]!;
73
+
74
+ // Standing: the idle clip, both feet on the ramp under the keeper.
75
+ const idle = skeleton("keeper.json");
76
+ 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"]})`);
77
+ 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) {
78
+ const ik = foot(file);
79
+ const target = ik["target"] as number[];
80
+ const x = x0 + dx;
81
+ 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(", ")})`);
82
+ t.ok(ik["reached"] === true, `the ${label} foot reached it (error ${ik["error"]})`);
83
+ }
84
+
85
+ // Walking: the walk clip, facing up the ramp, the keeper between where it was and where it goes.
86
+ let planted = 0;
87
+ 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) {
88
+ const s = skeleton(keeper);
89
+ const x = at(keeper);
90
+ t.ok(s["clip"] === WALK && s["flipX"] === false, `walking, the keeper plays its walk clip facing right (clip ${s["clip"]}, flipX ${s["flipX"]})`);
91
+ t.ok(x > 6 && x < 7, `walking, it is part of the way along the ramp (x ${x})`);
92
+ for (const [file, label] of [[front, "front"], [back, "back"]] as const) {
93
+ const ik = foot(file);
94
+ const [fx, fy] = ik["target"] as [number, number];
95
+ const lift = fy - ankleY(fx);
96
+ 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)`);
97
+ t.ok(ik["reached"] === true, `walking, the ${label} foot reached its target (error ${ik["error"]})`);
98
+ if (Math.abs(lift) < 1e-4) planted += 1;
99
+ }
100
+ }
101
+ t.ok(planted >= 2, `while it walks a foot stays planted on the plank (${planted} of 4 targets on it)`);
102
+ // The back foot carried the keeper from 2.85 s to 3.15 s (the walk starts after the turn): it did not move.
103
+ const before = foot("walk-back.json")["target"] as number[];
104
+ const later = foot("walk-back-later.json")["target"] as number[];
105
+ 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(", ")})`);
106
+
107
+ // Arrived: idle again, facing the spawn, at x 7.
108
+ const up = skeleton("keeper-up.json");
109
+ 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")})`);
110
+
111
+ // Tick by tick: the keeper's scale x, facing, clip and place, and each foot's target.
112
+ const ticks = Array.from({ length: LAST - FIRST + 1 }, (_, i) => {
113
+ const k = FIRST + i;
114
+ const keeper = run.json<EntityDump>(`k${k}.json`).components;
115
+ const s = keeper["og.Skeleton2D"]!;
116
+ return {
117
+ k,
118
+ x: (keeper["og.Transform"]!["position"] as number[])[0]!,
119
+ squash: (keeper["og.Transform"]!["scale"] as number[])[0]!,
120
+ flipX: s["flipX"] === true,
121
+ clip: s["clip"] as number,
122
+ feet: [`f${k}.json`, `b${k}.json`].map((f) => foot(f)["target"] as number[]),
123
+ };
124
+ });
125
+ // No foot moves further in a tick than the gait's swing: setting off, walking, stopping and both turns.
126
+ let worst = 0;
127
+ let worstAt = 0;
128
+ for (let i = 1; i < ticks.length; i++) {
129
+ for (const f of [0, 1]) {
130
+ const [a, b] = [ticks[i - 1]!.feet[f]!, ticks[i]!.feet[f]!];
131
+ const d = Math.hypot(b[0]! - a[0]!, b[1]! - a[1]!);
132
+ if (d > worst) [worst, worstAt] = [d, ticks[i]!.k];
133
+ }
134
+ }
135
+ t.ok(worst <= STRIDE_STEP + 1e-6, `no foot moves more than a gait tick's ${STRIDE_STEP} m in one tick (at most ${worst.toFixed(6)} m, tick ${worstAt})`);
136
+ // The walk itself moves the swinging foot nearly that far, so the bound is the gait's, not a loose one.
137
+ t.ok(worst > STRIDE_STEP * 0.9, `the walk's swing reaches the bound (${worst.toFixed(6)} m)`);
138
+ // Each turn: several ticks of a narrowed rig (never edge-on), standing still, between one facing and the other.
139
+ const turns: { from: number; to: number; ticks: number; still: boolean; steady: boolean }[] = [];
140
+ for (let i = 1; i < ticks.length; i++) {
141
+ if (ticks[i]!.flipX === ticks[i - 1]!.flipX) continue;
142
+ let j = i - 1;
143
+ while (j > 0 && ticks[j]!.squash !== 1) j--;
144
+ const span = ticks.slice(j, i + 1);
145
+ turns.push({
146
+ from: ticks[j]!.k,
147
+ to: ticks[i]!.k,
148
+ ticks: i - j - 1,
149
+ still: span.every((u) => Math.abs(u.x - span[0]!.x) < 1e-6),
150
+ steady: span.every((u, n) => u.squash !== 0 && (n === 0 || Math.abs(u.squash * (u.flipX ? -1 : 1) - span[n - 1]!.squash * (span[n - 1]!.flipX ? -1 : 1)) <= 0.5)),
151
+ });
152
+ }
153
+ t.ok(turns.length === 2, `the keeper turned twice: setting off and on arrival (${turns.length})`);
154
+ for (const u of turns) {
155
+ t.ok(u.ticks >= 4 && u.still && u.steady, `a turn (ticks ${u.from} to ${u.to}) narrows the rig over ${u.ticks} ticks in place, never edge-on or by half its width at once`);
156
+ }
157
+ // The stop: the idle clip returns only with both feet down on the ramp, where they stand.
158
+ const back = ticks.findIndex((u, i) => i > 0 && u.clip === IDLE && ticks[i - 1]!.clip !== IDLE);
159
+ t.ok(back > 0, `the idle clip came back (tick ${ticks[back]?.k})`);
160
+ if (back > 0) {
161
+ const u = ticks[back]!;
162
+ for (const [f, dx] of [[0, -0.15], [1, 0.15]] as const) {
163
+ const [fx, fy] = u.feet[f]!;
164
+ t.ok(Math.abs(fx! - (u.x + dx)) < 1e-4 && Math.abs(fy! - ankleY(fx!)) < 1e-4, `when idle returns, foot ${f} is down where it stands (${fx}, ${fy})`);
165
+ }
166
+ const arrived = ticks.findIndex((v) => Math.abs(v.x - 7) < 1e-6);
167
+ t.ok(arrived > 0 && back - arrived > 1, `between arriving (tick ${ticks[arrived]?.k}) and breathing (tick ${u.k}) the feet land and the keeper turns`);
168
+ }
169
+
170
+ // A loop of 2 s from tick 0 to 2.1 s passed its event at 0 and at 2; the walk
171
+ // (2 s at 0.5 m/s, a step every 0.4 s) stepped 5 times, and the idle clip
172
+ // started over on arrival with a breath.
173
+ const game = run.json<Got<{ inhales: number }>>("game.json").value;
174
+ t.ok(game.inhales === 2, `og.Skeleton2DEvent counted two breaths before the walk (${game.inhales})`);
175
+ const end = run.json<Got<{ inhales: number; steps: number }>>("game-end.json").value;
176
+ t.ok(end.steps === 5 && end.inhales === 3, `the walk clip's events counted 5 steps, then a third breath (${end.steps}, ${end.inhales})`);
177
+ },
178
+ });
@@ -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
- }