@origingame/create-game 0.3.0 → 0.5.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/README.md +5 -2
- package/archetypes/README.md +1 -1
- package/archetypes/arena/package.json +1 -1
- package/archetypes/arena/project.ogt +1 -1
- package/archetypes/idle/README.md +4 -4
- package/archetypes/idle/assets/looks/idle.ogt +1 -1
- package/archetypes/idle/assets/styles/sparkorb/LICENSES.md +2 -2
- package/archetypes/idle/assets/styles/sparkorb/style.ogt +1 -1
- package/archetypes/idle/package.json +1 -1
- package/archetypes/idle/project.ogt +1 -1
- package/archetypes/idle/src/components.ts +1 -1
- package/archetypes/idle/src/main.ts +5 -5
- package/archetypes/idle/src/ui.ts +3 -3
- package/archetypes/idle/tools/make-assets.py +1 -1
- package/archetypes/match3/package.json +1 -1
- package/archetypes/match3/project.ogt +1 -1
- package/archetypes/rhythm/README.md +2 -2
- package/archetypes/rhythm/assets/styles/lanebeat/LICENSES.md +2 -2
- package/archetypes/rhythm/assets/styles/lanebeat/style.ogt +1 -1
- package/archetypes/rhythm/package.json +1 -1
- package/archetypes/rhythm/project.ogt +1 -1
- package/archetypes/rhythm/src/components.ts +1 -1
- package/archetypes/rhythm/src/main.ts +3 -3
- package/archetypes/rhythm/src/ui.ts +2 -2
- package/archetypes/roguelike/README.md +2 -2
- package/archetypes/roguelike/assets/styles/lampdive/LICENSES.md +2 -2
- package/archetypes/roguelike/assets/styles/lampdive/style.ogt +1 -1
- package/archetypes/roguelike/package.json +1 -1
- package/archetypes/roguelike/project.ogt +1 -1
- package/archetypes/roguelike/src/components.ts +1 -1
- package/archetypes/roguelike/src/main.ts +2 -2
- package/archetypes/roguelike/src/ui.ts +2 -2
- package/archetypes/runner/package.json +1 -1
- package/archetypes/runner/project.ogt +1 -1
- package/archetypes/third-person/README.md +2 -2
- package/archetypes/third-person/assets/styles/ember/LICENSES.md +2 -2
- package/archetypes/third-person/assets/styles/ember/style.ogt +1 -1
- package/archetypes/third-person/package.json +1 -1
- package/archetypes/third-person/project.ogt +1 -1
- package/archetypes/third-person/references/metal/beacon.png +0 -0
- package/archetypes/third-person/references/metal/dawn.png +0 -0
- package/archetypes/third-person/references/metal/title.png +0 -0
- package/archetypes/third-person/references/webgl2/beacon.png +0 -0
- package/archetypes/third-person/src/components.ts +1 -1
- package/archetypes/third-person/src/main.ts +3 -3
- package/archetypes/third-person/src/sound.ts +1 -1
- package/archetypes/third-person/src/ui.ts +2 -2
- package/archetypes/topdown/package.json +1 -1
- package/archetypes/topdown/project.ogt +1 -1
- package/archetypes/tower-defense/package.json +1 -1
- package/archetypes/tower-defense/project.ogt +1 -1
- package/archetypes/tower-defense/src/components.ts +1 -1
- package/archetypes/tower-defense/src/main.ts +2 -2
- package/archetypes/tower-defense/src/ui.ts +2 -2
- package/bin/create.ts +15 -11
- package/looks/catalogue.json +123 -2
- package/package.json +2 -2
- package/template/package.json +1 -1
- package/template/project.ogt +1 -1
- package/template-2d/README.md +3 -2
- package/template-2d/package.json +1 -1
- package/template-2d/project.ogt +1 -1
- package/template-2d/src/main.ts +111 -30
- package/template-2d/tests/keeper.og.ts +93 -8
package/template-2d/src/main.ts
CHANGED
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
// idle clip deforms, whose feet two-bone IK planted on the slope
|
|
17
17
|
// (og.IkTarget2D, the Plant system), and whose lantern lights the sprite
|
|
18
18
|
// layers through og.Lighting2D (og-game §8; og-anim's 2D skeletons). Sent
|
|
19
|
-
// along the ramp (`keeper:<x>`), it
|
|
20
|
-
//
|
|
21
|
-
//
|
|
19
|
+
// along the ramp (`keeper:<x>`), it turns to face the way (a paper turn
|
|
20
|
+
// over a few ticks), plays its walk clip there, each foot planted while it
|
|
21
|
+
// carries the keeper and lifted while it swings, lets both feet land when
|
|
22
|
+
// it arrives, turns back and breathes again (the Walk and Plant systems).
|
|
23
|
+
// No foot ever moves faster than the gait's own swing.
|
|
22
24
|
//
|
|
23
25
|
// The frame's look is data: GameDesc.look names a file under assets/looks/
|
|
24
26
|
// that extends a preset of the style catalogue, applied before start runs.
|
|
@@ -47,6 +49,16 @@ const LIFT = 0.08;
|
|
|
47
49
|
const LANTERN = { x: 0.25, y: 1.0 } as const;
|
|
48
50
|
/** 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
51
|
const WALK = { speed: 0.5, stride: 0.4, lift: 0.08 } as const;
|
|
52
|
+
/**
|
|
53
|
+
* The fastest a foot moves (m/s): the gait's swing at its start, a stride
|
|
54
|
+
* forward in half a cycle (twice the walking speed) while it rises up the
|
|
55
|
+
* slope and off it (the lift's parabola, 4 lift over the swing's length,
|
|
56
|
+
* at its steepest). A foot sent elsewhere (setting off, stopping, a turn)
|
|
57
|
+
* steps there no faster.
|
|
58
|
+
*/
|
|
59
|
+
const FOOT_SPEED = Math.hypot(2 * WALK.speed, 2 * WALK.speed * RAMP.slope + (8 * WALK.lift * WALK.speed) / WALK.stride);
|
|
60
|
+
/** A turn's ticks: the rig narrows to its edge and opens the other way (og.Transform's scale x, never 0). */
|
|
61
|
+
const TURN_TICKS = 12;
|
|
50
62
|
|
|
51
63
|
/** The courier. */
|
|
52
64
|
export const Hero = defineComponent("Hero", { spawnX: f32(0, { unit: "m" }), spawnY: f32(0, { unit: "m" }) });
|
|
@@ -54,8 +66,17 @@ export const Hero = defineComponent("Hero", { spawnX: f32(0, { unit: "m" }), spa
|
|
|
54
66
|
/** A coin of the map's `coin` objects; taken once. */
|
|
55
67
|
export const Coin = defineComponent("Coin", { taken: bool() });
|
|
56
68
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
69
|
+
/**
|
|
70
|
+
* One of the keeper's feet: its IK target stands on the ramp `dx` ahead of
|
|
71
|
+
* the keeper (behind when negative), `lag` of a gait cycle after the front
|
|
72
|
+
* foot; `x`, `y` is where the foot is.
|
|
73
|
+
*/
|
|
74
|
+
export const Plant = defineComponent("Plant", {
|
|
75
|
+
dx: f32(0, { unit: "m" }),
|
|
76
|
+
lag: f32(0, { min: 0, max: 1 }),
|
|
77
|
+
x: f32(0, { unit: "m" }),
|
|
78
|
+
y: f32(0, { unit: "m" }),
|
|
79
|
+
});
|
|
59
80
|
|
|
60
81
|
/** A voice: the sound bit (Sound) it plays when Game.sfx asks for it. */
|
|
61
82
|
export const Voice = defineComponent("Voice", { bit: u32() });
|
|
@@ -86,13 +107,20 @@ export const Game = defineResource("Game", {
|
|
|
86
107
|
steps: u32(),
|
|
87
108
|
});
|
|
88
109
|
|
|
89
|
-
/** Where the keeper walks (`keeper:<x>`), how far it has walked since it set off (its gait's phase), and its clips. */
|
|
110
|
+
/** Where the keeper walks (`keeper:<x>`), how far it has walked since it set off (its gait's phase), its turn and stop, and its clips. */
|
|
90
111
|
export const KeeperWalk = defineResource("KeeperWalk", {
|
|
91
112
|
goal: f32(6, { unit: "m" }),
|
|
92
113
|
walked: f32(0, { unit: "m" }),
|
|
93
114
|
walking: bool(),
|
|
94
115
|
/** -1 facing left (towards the spawn), 1 right. */
|
|
95
116
|
facing: f32(-1),
|
|
117
|
+
/** Ticks of a turn done (0: not turning). */
|
|
118
|
+
turning: u32(),
|
|
119
|
+
/** og.Transform's scale x: 1, or through a turn from 1 towards -1. */
|
|
120
|
+
squash: f32(1),
|
|
121
|
+
/** Arrived, the feet stepping to stand (Plant sets `landed` when both are down). */
|
|
122
|
+
settling: bool(),
|
|
123
|
+
landed: bool(true),
|
|
96
124
|
idle: u32(),
|
|
97
125
|
walk: u32(),
|
|
98
126
|
});
|
|
@@ -201,30 +229,62 @@ export const Animate = defineSystem("Animate", { reads: [Hero, og.PlatformerCont
|
|
|
201
229
|
/**
|
|
202
230
|
* Walks the keeper towards KeeperWalk.goal along the ramp at the gait's
|
|
203
231
|
* speed (by scaled time, as the clip plays: paused, both hold), facing the
|
|
204
|
-
* way it goes, with the walk clip
|
|
205
|
-
*
|
|
232
|
+
* way it goes, with the walk clip. Facing the other way first, it turns
|
|
233
|
+
* over TURN_TICKS (og.Transform's scale x eased from 1 to -1, never 0, then
|
|
234
|
+
* flipX), its feet stepping across with it. On arrival it holds the walk
|
|
235
|
+
* pose until Plant has both feet down, turns to face the spawn, and once
|
|
236
|
+
* they are down again breathes with the idle clip (which starts over,
|
|
237
|
+
* breathing in).
|
|
206
238
|
*/
|
|
207
239
|
export const Walk = defineSystem("Walk", { writes: [KeeperWalk, og.Skeleton2D, og.Transform], after: ["Control"] }, (_q, time, world) => {
|
|
208
240
|
const keeper = world.find("keeper");
|
|
209
241
|
if (keeper === null) return;
|
|
210
242
|
const w = must(world.resource(KeeperWalk));
|
|
211
243
|
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
244
|
const s = must(world.get(keeper, og.Skeleton2D));
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
const place = (nx: number) => must(world.set(keeper, og.Transform, { position: { x: nx, y: rampY(nx) + LIFT, z: 0 }, scale: { x: w.squash, y: 1, z: 1 } }));
|
|
246
|
+
if (w.turning > 0 || (time.scaledDt > 0 && w.turning === 0 && wantsTurn(w, x))) {
|
|
247
|
+
// A turn: the rig narrows to its edge and opens facing the other way.
|
|
248
|
+
if (time.scaledDt === 0) return;
|
|
249
|
+
w.turning += 1;
|
|
250
|
+
if (w.turning < TURN_TICKS) {
|
|
251
|
+
w.squash = Math.cos((Math.PI * (w.turning - 0.5)) / (TURN_TICKS - 0.5));
|
|
252
|
+
} else {
|
|
253
|
+
w.turning = 0;
|
|
254
|
+
w.squash = 1;
|
|
255
|
+
w.facing = -w.facing;
|
|
256
|
+
must(world.set(keeper, og.Skeleton2D, { flipX: w.facing < 0 }));
|
|
257
|
+
}
|
|
258
|
+
place(x);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const left = w.goal - x;
|
|
262
|
+
if (left !== 0) {
|
|
263
|
+
const step = WALK.speed * time.scaledDt;
|
|
264
|
+
const arrived = Math.abs(left) <= step;
|
|
265
|
+
const nx = arrived ? w.goal : x + Math.sign(left) * step;
|
|
266
|
+
w.walked = (w.walking ? w.walked : 0) + Math.abs(nx - x);
|
|
267
|
+
w.walking = !arrived;
|
|
268
|
+
w.settling = arrived;
|
|
269
|
+
w.landed = false;
|
|
270
|
+
place(nx);
|
|
271
|
+
// Walking plays the walk clip from its first step; arrived, it holds its pose while the feet land.
|
|
272
|
+
if (s.clip !== w.walk) must(world.set(keeper, og.Skeleton2D, { clip: w.walk, speed: 1 }));
|
|
273
|
+
else if (arrived) must(world.set(keeper, og.Skeleton2D, { speed: 0 }));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (w.settling && w.landed) w.settling = false;
|
|
277
|
+
// Standing, both feet down and facing the spawn: breathe.
|
|
278
|
+
if (!w.settling && w.landed && w.facing < 0 && s.clip !== w.idle) must(world.set(keeper, og.Skeleton2D, { clip: w.idle, speed: 1 }));
|
|
226
279
|
});
|
|
227
280
|
|
|
281
|
+
/** Whether the keeper must turn before it goes on: towards its goal, or back to the spawn once it stands with both feet down. */
|
|
282
|
+
function wantsTurn(w: { goal: number; facing: number; settling: boolean; walking: boolean }, x: number): boolean {
|
|
283
|
+
const left = w.goal - x;
|
|
284
|
+
if (left !== 0) return Math.sign(left) !== w.facing && !w.walking;
|
|
285
|
+
return !w.settling && w.facing > 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
228
288
|
/**
|
|
229
289
|
* Puts the keeper's feet on the ramp (og.IkTarget2D: the legs' two-bone IK
|
|
230
290
|
* reaches each target, the knees bend as the hip bobs). Standing, each foot
|
|
@@ -232,14 +292,19 @@ export const Walk = defineSystem("Walk", { writes: [KeeperWalk, og.Skeleton2D, o
|
|
|
232
292
|
* distance walked over the stride and the back foot's half a cycle later:
|
|
233
293
|
* for half a cycle a foot stays where it is on the plank while the keeper
|
|
234
294
|
* passes over it, for the other half it swings a stride forward, lifted on
|
|
235
|
-
* a parabola.
|
|
236
|
-
*
|
|
295
|
+
* a parabola. A foot steps to where it should be at most FOOT_SPEED fast,
|
|
296
|
+
* lifted while it is still on its way, so turning, setting off and
|
|
297
|
+
* stopping are steps, not jumps (each leg hangs from its own hip: the
|
|
298
|
+
* front foot crosses to the other side in a turn). Keeps the lantern's light at its hand, and
|
|
299
|
+
* counts the clips' breaths and footfalls.
|
|
237
300
|
*/
|
|
238
|
-
export const Plant2D = defineSystem("Plant", { reads: [
|
|
301
|
+
export const Plant2D = defineSystem("Plant", { reads: [og.Skeleton2DEvent], writes: [Game, KeeperWalk, Plant, og.IkTarget2D, og.Transform], after: ["Walk"] }, (q, time, world) => {
|
|
239
302
|
const keeper = world.find("keeper");
|
|
240
303
|
if (keeper === null) return;
|
|
241
304
|
const at = must(world.get(keeper, og.Transform)).position;
|
|
242
305
|
const w = must(world.resource(KeeperWalk));
|
|
306
|
+
const reach = FOOT_SPEED * time.scaledDt;
|
|
307
|
+
let down = true;
|
|
243
308
|
for (const a of q.archetypes()) {
|
|
244
309
|
const plant = a.column(Plant);
|
|
245
310
|
const target = a.column(og.IkTarget2D).target;
|
|
@@ -258,13 +323,22 @@ export const Plant2D = defineSystem("Plant", { reads: [Plant, KeeperWalk, og.Ske
|
|
|
258
323
|
lift = WALK.lift * 4 * s * (1 - s);
|
|
259
324
|
}
|
|
260
325
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
326
|
+
// Turning, each foot follows its hip as the rig narrows and opens the other way.
|
|
327
|
+
const wx = at.x + w.facing * w.squash * ahead;
|
|
328
|
+
const [x, y] = toward(plant.x[i]!, plant.y[i]!, wx, rampY(wx) + ANKLE + lift, reach);
|
|
329
|
+
// Still on its way: lifted with how far it has left (a step's height at half a stride), as fast as it may rise.
|
|
330
|
+
const rest = Math.abs(wx - x);
|
|
331
|
+
const [fx, fy] = rest > 1e-6 ? toward(plant.x[i]!, plant.y[i]!, x, Math.max(y, rampY(x) + ANKLE + WALK.lift * Math.min(1, rest / (WALK.stride / 2))), reach) : [x, y];
|
|
332
|
+
plant.x[i] = fx;
|
|
333
|
+
plant.y[i] = fy;
|
|
334
|
+
target[i * 2] = fx;
|
|
335
|
+
target[i * 2 + 1] = fy;
|
|
336
|
+
if (Math.hypot(wx - fx, rampY(wx) + ANKLE + lift - fy) > 1e-5 || lift > 0) down = false;
|
|
264
337
|
}
|
|
265
338
|
}
|
|
339
|
+
w.landed = down;
|
|
266
340
|
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 } }));
|
|
341
|
+
if (lantern !== null) must(world.set(lantern, og.Transform, { position: { x: at.x - w.facing * w.squash * LANTERN.x, y: at.y + LANTERN.y, z: 0 } }));
|
|
268
342
|
const g = must(world.resource(Game));
|
|
269
343
|
for (const e of world.events(og.Skeleton2DEvent)) {
|
|
270
344
|
if (e.name === "inhale") g.inhales += 1;
|
|
@@ -272,6 +346,13 @@ export const Plant2D = defineSystem("Plant", { reads: [Plant, KeeperWalk, og.Ske
|
|
|
272
346
|
}
|
|
273
347
|
});
|
|
274
348
|
|
|
349
|
+
/** (x, y) moved towards (tx, ty) by at most `reach`. */
|
|
350
|
+
function toward(x: number, y: number, tx: number, ty: number, reach: number): [number, number] {
|
|
351
|
+
const d = Math.hypot(tx - x, ty - y);
|
|
352
|
+
if (d <= reach) return [tx, ty];
|
|
353
|
+
return [x + ((tx - x) * reach) / d, y + ((ty - y) * reach) / d];
|
|
354
|
+
}
|
|
355
|
+
|
|
275
356
|
/** Plays each sound asked for once: a voice still playing is stopped this tick and played again the next. */
|
|
276
357
|
export const Voices = defineSystem("Voices", { reads: [Voice], writes: [Game, og.AudioSource], after: ["Pickups"] }, (q, _time, world) => {
|
|
277
358
|
const g = must(world.resource(Game));
|
|
@@ -340,7 +421,7 @@ function spawnKeeper(world: World, rig: Rig2DInfo): void {
|
|
|
340
421
|
const keeper = must(world.spawn({ name: "keeper", position: { x, y: rampY(x) + LIFT, z: 0 } }));
|
|
341
422
|
must(world.insert(keeper, og.Skeleton2D, { rig: rig.rig, clip: idle, flipX: true, layer: 1 }));
|
|
342
423
|
const w = must(world.resource(KeeperWalk));
|
|
343
|
-
Object.assign(w, { goal: x, walked: 0, walking: false, facing: -1, idle, walk });
|
|
424
|
+
Object.assign(w, { goal: x, walked: 0, walking: false, facing: -1, turning: 0, squash: 1, settling: false, landed: true, idle, walk });
|
|
344
425
|
// Facing left, the front foot is the left one; the back foot steps half a cycle later.
|
|
345
426
|
for (const [name, ahead, lag] of [["leg-f", 0.15, 0], ["leg-b", -0.15, 0.5]] as const) {
|
|
346
427
|
const constraint = rig.constraints.indexOf(name);
|
|
@@ -348,7 +429,7 @@ function spawnKeeper(world: World, rig: Rig2DInfo): void {
|
|
|
348
429
|
const fx = x - ahead;
|
|
349
430
|
const foot = must(world.spawn({ name: `keeper-${name}` }));
|
|
350
431
|
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 }));
|
|
432
|
+
must(world.insert(foot, Plant, { dx: ahead, lag, x: fx, y: rampY(fx) + ANKLE }));
|
|
352
433
|
}
|
|
353
434
|
const light = must(world.spawn({ name: "lantern", position: { x: x + LANTERN.x, y: rampY(x) + LIFT + LANTERN.y, z: 0 } }));
|
|
354
435
|
must(world.insert(light, og.Light2D, { color: { r: 1, g: 0.72, b: 0.35 }, intensity: 1.6, radius: 4, castShadows: false }));
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// the ramp and reached; `keeper:7` sends it walking up the ramp with its
|
|
4
4
|
// walk clip, facing the way it goes, one foot planted on the plank while
|
|
5
5
|
// the other swings above it; it arrives, faces the spawn and breathes again
|
|
6
|
-
// with both feet planted, higher.
|
|
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.
|
|
7
10
|
import { playtest } from "@origingame/engine/playtest";
|
|
8
11
|
|
|
9
12
|
interface Got<T> {
|
|
@@ -24,15 +27,38 @@ const LIFT = 0.08;
|
|
|
24
27
|
/** The rig's clips, in file order. */
|
|
25
28
|
const IDLE = 0;
|
|
26
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(";");
|
|
27
47
|
|
|
28
48
|
export default playtest({
|
|
29
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",
|
|
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.
|
|
34
|
-
"
|
|
35
|
-
|
|
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(";"),
|
|
36
62
|
check(run, t) {
|
|
37
63
|
t.ok(run.quit, "the script reached quit");
|
|
38
64
|
for (const png of ["keeper.png", "walk.png"]) {
|
|
@@ -73,7 +99,7 @@ export default playtest({
|
|
|
73
99
|
}
|
|
74
100
|
}
|
|
75
101
|
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.
|
|
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.
|
|
77
103
|
const before = foot("walk-back.json")["target"] as number[];
|
|
78
104
|
const later = foot("walk-back-later.json")["target"] as number[];
|
|
79
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(", ")})`);
|
|
@@ -82,6 +108,65 @@ export default playtest({
|
|
|
82
108
|
const up = skeleton("keeper-up.json");
|
|
83
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")})`);
|
|
84
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
|
+
|
|
85
170
|
// A loop of 2 s from tick 0 to 2.1 s passed its event at 0 and at 2; the walk
|
|
86
171
|
// (2 s at 0.5 m/s, a step every 0.4 s) stepped 5 times, and the idle clip
|
|
87
172
|
// started over on arrival with a breath.
|