@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.
- package/README.md +2 -2
- package/archetypes/README.md +1 -0
- package/archetypes/arena/.origin/setup +14 -0
- package/archetypes/arena/LICENSES.md +8 -0
- package/archetypes/arena/README.md +67 -0
- package/archetypes/arena/assets/audio/coin.ogt +9 -0
- package/archetypes/arena/assets/audio/dash.ogt +9 -0
- package/archetypes/arena/assets/audio/win.ogt +9 -0
- package/archetypes/arena/assets/fx/coin.ogt +20 -0
- package/archetypes/arena/assets/looks/arena.ogt +10 -0
- package/archetypes/arena/gitattributes +22 -0
- package/archetypes/arena/gitignore +8 -0
- package/archetypes/arena/host.d.ts +46 -0
- package/archetypes/arena/package.json +18 -0
- package/archetypes/arena/project.ogt +14 -0
- package/archetypes/arena/src/game.ts +314 -0
- package/archetypes/arena/src/main.ts +100 -0
- package/archetypes/arena/tests/duel.og.ts +32 -0
- package/archetypes/arena/tests/solo.og.ts +22 -0
- package/archetypes/arena/tsconfig.json +17 -0
- package/archetypes/idle/LICENSES.md +1 -1
- package/archetypes/idle/README.md +3 -3
- package/archetypes/idle/assets/looks/idle.ogt +1 -1
- package/archetypes/idle/assets/{skins → styles}/sparkorb/LICENSES.md +4 -4
- package/archetypes/idle/assets/{skins/sparkorb/skin.ogt → styles/sparkorb/style.ogt} +47 -47
- package/archetypes/idle/package.json +1 -1
- package/archetypes/idle/project.ogt +1 -1
- package/archetypes/idle/references/metal/card.png +0 -0
- package/archetypes/idle/references/metal/orbit.png +0 -0
- package/archetypes/idle/references/metal/title.png +0 -0
- package/archetypes/idle/src/main.ts +2 -2
- package/archetypes/idle/src/ui.ts +11 -11
- package/archetypes/idle/tools/make-assets.py +4 -4
- package/archetypes/match3/package.json +1 -1
- package/archetypes/match3/project.ogt +1 -1
- package/archetypes/rhythm/LICENSES.md +1 -1
- package/archetypes/rhythm/README.md +3 -3
- package/archetypes/rhythm/assets/{skins → styles}/lanebeat/LICENSES.md +4 -4
- package/archetypes/rhythm/assets/{skins/lanebeat/skin.ogt → styles/lanebeat/style.ogt} +45 -45
- package/archetypes/rhythm/package.json +1 -1
- package/archetypes/rhythm/project.ogt +1 -1
- package/archetypes/rhythm/references/metal/results.png +0 -0
- package/archetypes/rhythm/references/metal/title.png +0 -0
- package/archetypes/rhythm/src/main.ts +2 -2
- package/archetypes/rhythm/src/ui.ts +11 -11
- package/archetypes/roguelike/LICENSES.md +1 -1
- package/archetypes/roguelike/README.md +3 -3
- package/archetypes/roguelike/assets/{skins → styles}/lampdive/LICENSES.md +4 -4
- package/archetypes/roguelike/assets/{skins/lampdive/skin.ogt → styles/lampdive/style.ogt} +41 -41
- package/archetypes/roguelike/package.json +1 -1
- package/archetypes/roguelike/project.ogt +1 -1
- package/archetypes/roguelike/references/metal/over.png +0 -0
- package/archetypes/roguelike/references/metal/title.png +0 -0
- package/archetypes/roguelike/src/main.ts +2 -2
- package/archetypes/roguelike/src/ui.ts +7 -7
- package/archetypes/runner/package.json +1 -1
- package/archetypes/runner/project.ogt +1 -1
- package/archetypes/third-person/LICENSES.md +1 -1
- package/archetypes/third-person/README.md +3 -3
- package/archetypes/third-person/assets/{skins → styles}/ember/LICENSES.md +2 -2
- package/archetypes/third-person/assets/{skins/ember/skin.ogt → styles/ember/style.ogt} +40 -41
- 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/main.ts +2 -2
- package/archetypes/third-person/src/ui.ts +10 -10
- package/archetypes/topdown/package.json +1 -1
- package/archetypes/topdown/project.ogt +1 -1
- package/archetypes/tower-defense/LICENSES.md +1 -1
- package/archetypes/tower-defense/README.md +5 -5
- package/archetypes/tower-defense/assets/{skins → styles}/pathguard/LICENSES.md +4 -4
- package/archetypes/tower-defense/assets/styles/pathguard/atlas.png +0 -0
- package/archetypes/tower-defense/assets/styles/pathguard/style.ogt +111 -0
- package/archetypes/tower-defense/package.json +1 -1
- package/archetypes/tower-defense/project.ogt +1 -1
- package/archetypes/tower-defense/references/metal/end.png +0 -0
- package/archetypes/tower-defense/references/metal/title.png +0 -0
- package/archetypes/tower-defense/references/metal/wave.png +0 -0
- package/archetypes/tower-defense/src/main.ts +2 -2
- package/archetypes/tower-defense/src/ui.ts +19 -17
- package/bin/create.ts +11 -8
- package/looks/catalogue.json +40 -40
- package/package.json +1 -1
- package/template/README.md +2 -1
- package/template/assets/audio/sting.ogt +10 -0
- package/template/assets/timelines/intro.ogt +101 -0
- package/template/assets/timelines/narration.srt +11 -0
- package/template/host.d.ts +46 -0
- package/template/package.json +1 -1
- package/template/project.ogt +1 -1
- package/template/references/metal/intro-4.png +0 -0
- package/template/references/metal/intro-8.png +0 -0
- package/template/src/main.ts +1 -1
- package/template/tests/frames.ts +134 -0
- package/template/tests/intro.og.ts +65 -0
- package/template/tests/smoke.og.ts +30 -36
- package/template/tsconfig.json +1 -1
- package/template-2d/LICENSES.md +1 -0
- package/template-2d/README.md +5 -2
- package/template-2d/assets/rigs/keeper.ogt +292 -0
- package/template-2d/assets/rigs/keeper.png +0 -0
- package/template-2d/assets/rigs/keeper.png.ogt +7 -0
- package/template-2d/package.json +1 -1
- package/template-2d/project.ogt +1 -1
- package/template-2d/src/main.ts +159 -4
- package/template-2d/tests/keeper.og.ts +93 -0
- package/archetypes/tower-defense/assets/skins/pathguard/atlas.png +0 -0
- package/archetypes/tower-defense/assets/skins/pathguard/skin.ogt +0 -111
- /package/archetypes/idle/assets/{skins → styles}/sparkorb/ChakraPetch-BoldItalic.ttf +0 -0
- /package/archetypes/idle/assets/{skins → styles}/sparkorb/ChakraPetch-BoldItalic.ttf.ogt +0 -0
- /package/archetypes/idle/assets/{skins → styles}/sparkorb/OFL.txt +0 -0
- /package/archetypes/idle/assets/{skins → styles}/sparkorb/Orbitron-Medium.ttf +0 -0
- /package/archetypes/idle/assets/{skins → styles}/sparkorb/Orbitron-Medium.ttf.ogt +0 -0
- /package/archetypes/idle/assets/{skins → styles}/sparkorb/Rajdhani-Bold.ttf +0 -0
- /package/archetypes/idle/assets/{skins → styles}/sparkorb/Rajdhani-Bold.ttf.ogt +0 -0
- /package/archetypes/idle/assets/{skins → styles}/sparkorb/atlas.png +0 -0
- /package/archetypes/idle/assets/{skins → styles}/sparkorb/atlas.png.ogt +0 -0
- /package/archetypes/idle/assets/{skins → styles}/sparkorb/icons.png +0 -0
- /package/archetypes/idle/assets/{skins → styles}/sparkorb/icons.png.ogt +0 -0
- /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/ChakraPetch-Medium.ttf +0 -0
- /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/ChakraPetch-Medium.ttf.ogt +0 -0
- /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/JetBrainsMono-Regular.ttf +0 -0
- /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/JetBrainsMono-Regular.ttf.ogt +0 -0
- /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/OFL.txt +0 -0
- /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/Orbitron-Bold.ttf +0 -0
- /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/Orbitron-Bold.ttf.ogt +0 -0
- /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/atlas.png +0 -0
- /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/atlas.png.ogt +0 -0
- /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/icons.png +0 -0
- /package/archetypes/rhythm/assets/{skins → styles}/lanebeat/icons.png.ogt +0 -0
- /package/archetypes/roguelike/assets/{skins → styles}/lampdive/DotGothic16-Regular.ttf +0 -0
- /package/archetypes/roguelike/assets/{skins → styles}/lampdive/DotGothic16-Regular.ttf.ogt +0 -0
- /package/archetypes/roguelike/assets/{skins → styles}/lampdive/OFL.txt +0 -0
- /package/archetypes/roguelike/assets/{skins → styles}/lampdive/atlas.png +0 -0
- /package/archetypes/roguelike/assets/{skins → styles}/lampdive/atlas.png.ogt +0 -0
- /package/archetypes/roguelike/assets/{skins → styles}/lampdive/icons.png +0 -0
- /package/archetypes/roguelike/assets/{skins → styles}/lampdive/icons.png.ogt +0 -0
- /package/archetypes/third-person/assets/{skins → styles}/ember/Cinzel-Bold.ttf +0 -0
- /package/archetypes/third-person/assets/{skins → styles}/ember/Cinzel-Bold.ttf.ogt +0 -0
- /package/archetypes/third-person/assets/{skins → styles}/ember/Newsreader-Medium.ttf +0 -0
- /package/archetypes/third-person/assets/{skins → styles}/ember/Newsreader-Medium.ttf.ogt +0 -0
- /package/archetypes/third-person/assets/{skins → styles}/ember/OFL.txt +0 -0
- /package/archetypes/third-person/assets/{skins → styles}/ember/atlas.png +0 -0
- /package/archetypes/third-person/assets/{skins → styles}/ember/atlas.png.ogt +0 -0
- /package/archetypes/third-person/assets/{skins → styles}/ember/icons.png +0 -0
- /package/archetypes/third-person/assets/{skins → styles}/ember/icons.png.ogt +0 -0
- /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/Fredoka-SemiBold.ttf +0 -0
- /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/Fredoka-SemiBold.ttf.ogt +0 -0
- /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/LilitaOne-Regular.ttf +0 -0
- /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/LilitaOne-Regular.ttf.ogt +0 -0
- /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/OFL.txt +0 -0
- /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/atlas.png.ogt +0 -0
- /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/icons.png +0 -0
- /package/archetypes/tower-defense/assets/{skins → styles}/pathguard/icons.png.ogt +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// The reference-frame check: decode the PNGs a run wrote and compare them
|
|
2
|
+
// with references/<tier>/ under og-tests' tolerance (a channel off by more
|
|
3
|
+
// than 32/255 is an outlier; fail above 0.5 % outliers or an RGB RMSE above
|
|
4
|
+
// 0.01). A tier without its own references compares with `metal` and says
|
|
5
|
+
// so. `OG_FX_CAPTURE=1 og test .` records this tier's references instead.
|
|
6
|
+
// (Copied from templates/fx-showcase/tests/frames.ts, so the template stands alone.)
|
|
7
|
+
|
|
8
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { inflateSync } from "node:zlib";
|
|
12
|
+
import type { PlaytestChecks, PlaytestRun } from "@origingame/engine/playtest";
|
|
13
|
+
|
|
14
|
+
export interface Image {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
/** RGBA8 rows. */
|
|
18
|
+
rgba: Uint8Array;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** An 8-bit RGB or RGBA, non-interlaced PNG, unfiltered. */
|
|
22
|
+
export function decodePng(bytes: Uint8Array): Image {
|
|
23
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
24
|
+
let at = 8;
|
|
25
|
+
let width = 0;
|
|
26
|
+
let height = 0;
|
|
27
|
+
let channels = 4;
|
|
28
|
+
const idat: Uint8Array[] = [];
|
|
29
|
+
while (at < bytes.length) {
|
|
30
|
+
const len = view.getUint32(at);
|
|
31
|
+
const type = String.fromCharCode(...bytes.subarray(at + 4, at + 8));
|
|
32
|
+
const data = bytes.subarray(at + 8, at + 8 + len);
|
|
33
|
+
if (type === "IHDR") {
|
|
34
|
+
width = view.getUint32(at + 8);
|
|
35
|
+
height = view.getUint32(at + 12);
|
|
36
|
+
const depth = data[8]!;
|
|
37
|
+
const color = data[9]!;
|
|
38
|
+
if (depth !== 8 || (color !== 6 && color !== 2) || data[12] !== 0) throw new Error(`unsupported PNG (depth ${depth}, colour ${color}, interlace ${data[12]})`);
|
|
39
|
+
channels = color === 6 ? 4 : 3;
|
|
40
|
+
} else if (type === "IDAT") idat.push(data);
|
|
41
|
+
else if (type === "IEND") break;
|
|
42
|
+
at += 12 + len;
|
|
43
|
+
}
|
|
44
|
+
const raw = inflateSync(Buffer.concat(idat));
|
|
45
|
+
const stride = width * channels;
|
|
46
|
+
const out = new Uint8Array(width * height * 4);
|
|
47
|
+
const prev = new Uint8Array(stride);
|
|
48
|
+
const cur = new Uint8Array(stride);
|
|
49
|
+
for (let y = 0; y < height; y++) {
|
|
50
|
+
const filter = raw[y * (stride + 1)]!;
|
|
51
|
+
const line = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
52
|
+
for (let i = 0; i < stride; i++) {
|
|
53
|
+
const a = i >= channels ? cur[i - channels]! : 0;
|
|
54
|
+
const b = prev[i]!;
|
|
55
|
+
const c = i >= channels ? prev[i - channels]! : 0;
|
|
56
|
+
let v = line[i]!;
|
|
57
|
+
if (filter === 1) v += a;
|
|
58
|
+
else if (filter === 2) v += b;
|
|
59
|
+
else if (filter === 3) v += (a + b) >> 1;
|
|
60
|
+
else if (filter === 4) {
|
|
61
|
+
const p = a + b - c;
|
|
62
|
+
const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
|
|
63
|
+
v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
64
|
+
}
|
|
65
|
+
cur[i] = v & 255;
|
|
66
|
+
}
|
|
67
|
+
for (let x = 0; x < width; x++) {
|
|
68
|
+
for (let k = 0; k < 4; k++) out[(y * width + x) * 4 + k] = k < channels ? cur[x * channels + k]! : 255;
|
|
69
|
+
}
|
|
70
|
+
prev.set(cur);
|
|
71
|
+
}
|
|
72
|
+
return { width, height, rgba: out };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** og-tests' rule: outlier fraction and RGB RMSE between two equal-size frames. */
|
|
76
|
+
export function difference(a: Image, b: Image): { outliers: number; rmse: number } {
|
|
77
|
+
if (a.width !== b.width || a.height !== b.height) return { outliers: 1, rmse: 1 };
|
|
78
|
+
let outliers = 0;
|
|
79
|
+
let sum = 0;
|
|
80
|
+
const n = a.width * a.height;
|
|
81
|
+
for (let p = 0; p < n; p++) {
|
|
82
|
+
let out = false;
|
|
83
|
+
for (let k = 0; k < 3; k++) {
|
|
84
|
+
const d = Math.abs(a.rgba[p * 4 + k]! - b.rgba[p * 4 + k]!);
|
|
85
|
+
if (d > 32) out = true;
|
|
86
|
+
const f = d / 255;
|
|
87
|
+
sum += f * f;
|
|
88
|
+
}
|
|
89
|
+
if (out) outliers++;
|
|
90
|
+
}
|
|
91
|
+
return { outliers: outliers / n, rmse: Math.sqrt(sum / (n * 3)) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The playtest's output directory (the bundled playtest runs from it). */
|
|
95
|
+
export function outDir(): string {
|
|
96
|
+
return dirname(fileURLToPath(import.meta.url));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The project's references/, from its og-out/test/<name>/. */
|
|
100
|
+
export function referencesDir(): string {
|
|
101
|
+
return resolve(outDir(), "..", "..", "..", "references");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Compares each screenshot with its reference (`<prefix><file>`), or records it. */
|
|
105
|
+
export function checkFrames(run: PlaytestRun, t: PlaytestChecks, files: readonly string[], prefix: string): void {
|
|
106
|
+
// Read only where there is a renderer: without one (tier `none`) each frame is
|
|
107
|
+
// reported "no renderer, frame unchecked" and every other check still runs.
|
|
108
|
+
t.frame(files, () => {
|
|
109
|
+
const refs = referencesDir();
|
|
110
|
+
t.ok(existsSync(refs) || process.env.OG_FX_CAPTURE !== undefined, `the references directory exists at ${refs}`);
|
|
111
|
+
const own = join(refs, run.tier);
|
|
112
|
+
const capture = process.env.OG_FX_CAPTURE !== undefined;
|
|
113
|
+
for (const f of files) {
|
|
114
|
+
const actual = join(outDir(), f);
|
|
115
|
+
// Each file on its own: this tier's reference where it has one, metal's otherwise.
|
|
116
|
+
const mine = join(own, prefix + f);
|
|
117
|
+
const ref = capture || existsSync(mine) ? mine : join(refs, "metal", prefix + f);
|
|
118
|
+
if (ref !== mine) process.stdout.write(` note: no ${run.tier} reference for ${prefix}${f}; comparing with metal\n`);
|
|
119
|
+
if (capture) {
|
|
120
|
+
mkdirSync(own, { recursive: true });
|
|
121
|
+
copyFileSync(actual, ref);
|
|
122
|
+
t.ok(true, `recorded ${ref}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!existsSync(ref)) {
|
|
126
|
+
t.ok(false, `${ref} is missing: record it with OG_FX_CAPTURE=1 og test .`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const d = difference(decodePng(readFileSync(actual)), decodePng(readFileSync(ref)));
|
|
130
|
+
process.stdout.write(` ${f}: ${(d.outliers * 100).toFixed(3)} % outliers, RMSE ${d.rmse.toFixed(5)} against ${ref}\n`);
|
|
131
|
+
t.ok(d.outliers <= 0.005 && d.rmse <= 0.01, `${f} matches ${prefix}${f}: ${(d.outliers * 100).toFixed(3)} % outliers (≤ 0.5 %), RMSE ${d.rmse.toFixed(5)} (≤ 0.01)`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The intro cutscene (assets/timelines/intro.ogt) played from the command
|
|
2
|
+
// channel: `timeline:play:intro` makes the entity `timeline-intro`, and the
|
|
3
|
+
// timeline drives the camera along the "flyby" rail, walks the trickster
|
|
4
|
+
// with its clips, shows the narration from narration.srt, plays two sounds,
|
|
5
|
+
// runs `spawn_wave:2` at 5.5 s and fades in and out. At 4 s and 8 s the
|
|
6
|
+
// caption on screen is the .srt's cue for that time and the frames are
|
|
7
|
+
// compared with references/<tier>/intro-4.png and intro-8.png
|
|
8
|
+
// (tests/frames.ts; `OG_FX_CAPTURE=1 og test .` records them). After the end
|
|
9
|
+
// the camera is the game's again.
|
|
10
|
+
import { playtest } from "@origingame/engine/playtest";
|
|
11
|
+
import { checkFrames } from "./frames.ts";
|
|
12
|
+
|
|
13
|
+
interface EntityDump {
|
|
14
|
+
components: Record<string, Record<string, number | boolean | string | number[]>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UiDump {
|
|
18
|
+
elements: Array<{ id: string; text: string; x: number; y: number; width: number; height: number }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const FILES = ["intro-4.png", "intro-8.png"] as const;
|
|
22
|
+
|
|
23
|
+
export default playtest({
|
|
24
|
+
name: "the intro plays its rail camera, captions, clips, event and fades from timeline:play:intro",
|
|
25
|
+
size: "1280x720",
|
|
26
|
+
commands: [
|
|
27
|
+
"0:timeline:play:intro",
|
|
28
|
+
"4:screenshot:intro-4.png",
|
|
29
|
+
"4:dump_ui:ui-4.json",
|
|
30
|
+
"4:dump_entity:timeline-intro:timeline-4.json",
|
|
31
|
+
"4:dump_entity:trickster:trickster-4.json",
|
|
32
|
+
"8:screenshot:intro-8.png",
|
|
33
|
+
"8:dump_ui:ui-8.json",
|
|
34
|
+
"8:dump_world:world-8.json",
|
|
35
|
+
"10.5:dump_entity:timeline-intro:timeline-end.json",
|
|
36
|
+
"10.5:dump_ui:ui-end.json",
|
|
37
|
+
"10.6:quit",
|
|
38
|
+
].join(";"),
|
|
39
|
+
check(run, t) {
|
|
40
|
+
t.equal(run.warnings.join("; "), "", "the script ran without warnings");
|
|
41
|
+
t.ok(run.quit, "the script reached quit");
|
|
42
|
+
const caption = (file: string) =>
|
|
43
|
+
run.json<UiDump>(file).elements.find((e) => e.id.startsWith("timeline-caption-") && e.id.endsWith("-text"));
|
|
44
|
+
const at4 = caption("ui-4.json");
|
|
45
|
+
t.equal(at4?.text, "The trickster walks out to meet it.", "at 4 s the caption is the .srt's second cue");
|
|
46
|
+
t.ok(at4 !== undefined && at4.y > 600, `the caption sits at the bottom (y ${at4?.y})`);
|
|
47
|
+
t.equal(caption("ui-8.json")?.text, "Here they come.", "at 8 s the caption is the third cue");
|
|
48
|
+
const tl = run.json<EntityDump>("timeline-4.json").components["og.Timeline"]!;
|
|
49
|
+
t.equal(tl["playing"], true, "the timeline plays at 4 s");
|
|
50
|
+
t.near(Number(tl["time"]), 4, 0.02, "its time is the run's (it started at tick 0)");
|
|
51
|
+
// The transform track walks the trickster from (2 0 0) at 2 s to (0.5 0 1.5) at 5 s.
|
|
52
|
+
const p = run.json<EntityDump>("trickster-4.json").components["og.Transform"]!["position"] as number[];
|
|
53
|
+
t.ok(p[0]! < 2 && p[0]! > 0.5 && p[2]! > 0 && p[2]! < 1.5, `the trickster is on its way at 4 s (${p.join(", ")})`);
|
|
54
|
+
const world = run.json<{ entities: number }>("world-8.json");
|
|
55
|
+
t.ok(world.entities >= 10, `the event key at 5.5 s ran spawn_wave:2 (${world.entities} entities)`);
|
|
56
|
+
const end = run.json<EntityDump>("timeline-end.json").components["og.Timeline"]!;
|
|
57
|
+
t.equal(end["playing"], false, "the timeline ended");
|
|
58
|
+
t.equal(Number(end["time"]), 10, "at its duration");
|
|
59
|
+
t.equal(caption("ui-end.json"), undefined, "no caption after the end");
|
|
60
|
+
t.frame(FILES, () => {
|
|
61
|
+
for (const f of FILES) t.equal(run.png(f).width, 1280, `${f} is 1280 wide`);
|
|
62
|
+
});
|
|
63
|
+
checkFrames(run, t, FILES, "");
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -1,43 +1,37 @@
|
|
|
1
1
|
// The template's playtest, run by `og test` through `og run --headless
|
|
2
2
|
// --commands`: a tap on the menu's Start button (found by its UI id, no key
|
|
3
3
|
// fallback) starts the trickster spinning and drops the crate onto the
|
|
4
|
-
// ground
|
|
4
|
+
// ground. Every check is an `expect:` step evaluated in the run at its tick
|
|
5
|
+
// (docs/design.md §3.4): a failed one fails the run with the tick and what
|
|
6
|
+
// it observed, and a frame one without a renderer is reported unchecked.
|
|
7
|
+
// The frame, the scene described as data, the dumps and the UI tree are
|
|
8
|
+
// written beside them.
|
|
5
9
|
import { playtest } from "@origingame/engine/playtest";
|
|
6
10
|
|
|
7
|
-
interface EntityDump {
|
|
8
|
-
components: Record<string, Record<string, number | boolean | string | number[]>>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface UiDump {
|
|
12
|
-
elements: Array<{ id: string; parent: string; text: string; x: number; y: number; width: number; height: number }>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
11
|
export default playtest({
|
|
16
|
-
name: "A tap on Start spins the trickster and drops the crate; the run writes its frame, dumps and UI tree",
|
|
17
|
-
commands:
|
|
18
|
-
"0.5:touch_move_to:Start
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
t.ok(y > 0.4 && y < 0.6, `the crate fell from 4 m and rests on the ground (y ${y.toFixed(3)})`);
|
|
42
|
-
},
|
|
12
|
+
name: "A tap on Start spins the trickster and drops the crate; the run writes its frame, description, dumps and UI tree",
|
|
13
|
+
commands: [
|
|
14
|
+
"0.5:touch_move_to:Start",
|
|
15
|
+
"0.6:touch_start",
|
|
16
|
+
"0.7:touch_end",
|
|
17
|
+
// The tap was one press; the run started; the button says so.
|
|
18
|
+
"0.8:expect:event:og.PointerDown:1",
|
|
19
|
+
"0.8:expect:state:started==true",
|
|
20
|
+
"0.9:expect:ui:text:=Started",
|
|
21
|
+
// The trickster spins; the crate fell from 4 m and rests on the ground.
|
|
22
|
+
"2.0:expect:state:spin>0",
|
|
23
|
+
"2.0:expect:entity:crate:within:-3,0.4,-1,1,0.6,2",
|
|
24
|
+
"2.0:expect:event:og.CollisionStarted",
|
|
25
|
+
// The frame (1280x720): the sky at the top, the crate and the trickster where they stand.
|
|
26
|
+
"2.0:screenshot:smoke.png",
|
|
27
|
+
"2.0:expect:region:560,16,64,24:color:#9da5b1:24",
|
|
28
|
+
"2.0:expect:entity:crate:within:380,360,280,240",
|
|
29
|
+
"2.0:expect:entity:trickster:within:640,300,240,280",
|
|
30
|
+
"2.1:describe:describe.json",
|
|
31
|
+
"2.1:dump_entity:trickster:trickster.json",
|
|
32
|
+
"2.15:dump_entity:crate:crate.json",
|
|
33
|
+
"2.2:dump_world:world.json",
|
|
34
|
+
"2.3:dump_ui:ui.json",
|
|
35
|
+
"2.5:quit",
|
|
36
|
+
].join(";"),
|
|
43
37
|
});
|
package/template/tsconfig.json
CHANGED
package/template-2d/LICENSES.md
CHANGED
|
@@ -9,6 +9,7 @@ its `tools/make-assets.py`), and the rest is written by hand.
|
|
|
9
9
|
| `assets/sprites/courier.png`, `assets/sprites/coin.png` and their `.ogt` | CC0 1.0 | self-made: the runner archetype's courier and coin |
|
|
10
10
|
| `assets/tiles/city.png` | CC0 1.0 | self-made: the runner archetype's city tileset |
|
|
11
11
|
| `assets/tiles/city.ogt`, `assets/maps/level.ogt` | CC0 1.0 | self-made, written by hand |
|
|
12
|
+
| `assets/rigs/keeper.png`, `assets/rigs/keeper.ogt` | CC0 1.0 | self-made: the lamp keeper's parts drawn with ImageMagick primitives, its rig written by hand |
|
|
12
13
|
| `assets/audio/jump.wav`, `assets/audio/coin.wav` | CC0 1.0 | self-made: the runner archetype's synthesised tones |
|
|
13
14
|
| `assets/fonts/ui.ttf` | SIL Open Font License 1.1 (`assets/fonts/OFL.txt`) | Noto Sans SC Regular, subset to Latin-1 by the runner archetype from the project template's `assets/fonts/NotoSansSC-Regular-subset.ttf` |
|
|
14
15
|
| `assets/looks/*.ogt`, `assets/strings/*.ogt`, every other `.ogt` | CC0 1.0 | self-made, written by hand |
|
package/template-2d/README.md
CHANGED
|
@@ -5,11 +5,13 @@ a premise that says 2D, platformer, side-scroller, metroidvania or pixel
|
|
|
5
5
|
art): the smallest whole 2D Origin Engine project. A courier runs and jumps
|
|
6
6
|
the rooftops of one level, takes coins, jumps up through one-way ledges and
|
|
7
7
|
lands on them, loses a life in the pit and clears the level at the far
|
|
8
|
-
wall; Escape (or the HUD's Pause) opens the pause menu.
|
|
8
|
+
wall; Escape (or the HUD's Pause) opens the pause menu. Beside the spawn a
|
|
9
|
+
lamp keeper, a 2D skeletal rig, breathes on a ramp with its feet planted
|
|
10
|
+
and walks along it when sent (`keeper:<x>`), stepping on the plank.
|
|
9
11
|
|
|
10
12
|
```sh
|
|
11
13
|
og run . # headless with the probe; og dev . reloads on save
|
|
12
|
-
og test . # tests/smoke.og.ts
|
|
14
|
+
og test . # tests/smoke.og.ts, tests/keeper.og.ts
|
|
13
15
|
og build . # the play page
|
|
14
16
|
```
|
|
15
17
|
|
|
@@ -20,6 +22,7 @@ og build . # the play page
|
|
|
20
22
|
| The project in the 2D profile: an orthographic camera, sprites, 2D physics | `project.ogt`, `start` in `src/main.ts` | og-game §2, §8 |
|
|
21
23
|
| A hand-written tilemap over a tileset: a parallax skyline, roofs that collide, ledges on their own collider layer (`colliderLayers 2`) that the controller makes one-way | `assets/maps/level.ogt`, `assets/tiles/city.ogt` | og-game §8 (the grammar) |
|
|
22
24
|
| A flipbook sheet, its clips numbered in file order and checked against the code | `assets/sprites/courier.ogt`, `CLIP`, `checkClips` | og-game §8 |
|
|
25
|
+
| A 2D skeletal rig: bones, slots, region attachments, a weighted-mesh cape the idle clip deforms, two-bone IK legs planted on a slope (og.IkTarget2D) and stepping along it under a walk clip with Bézier keys, its clip events (breaths, footfalls) read as og.Skeleton2DEvent, its lantern an og.Light2D under og.Lighting2D (layers 1 and 2) | `assets/rigs/keeper.ogt`, `spawnKeeper`, `Walk`, `Plant`, `tests/keeper.og.ts` | og-game §8 |
|
|
23
26
|
| og.PlatformerController: run, jump (held for the full height), coyote time, the jump buffer, one-way ledges, Down to drop | `Control`, the hero in `start` | og-game §13; og-feel §12 |
|
|
24
27
|
| The map's objects: the spawn, the coins, the goal | `start`, `Pickups` | og-game §8 |
|
|
25
28
|
| Input on the press's tick: keys, a pad, the HUD's touch controls on a phone | `Control`, `Hud` | og-feel §1; og-ui §5 |
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
type rig2d
|
|
2
|
+
version 1
|
|
3
|
+
---
|
|
4
|
+
# The lamp keeper (templates/game-2d): a 2D rig drawn as textured triangles.
|
|
5
|
+
# Bones are pixels with +y up, unit 20 pixels to a world unit; the legs are
|
|
6
|
+
# two-bone IK chains planted on the ramp by og.IkTarget2D (src/main.ts, Plant),
|
|
7
|
+
# the cape a weighted mesh over the hip and two cape bones, fluttered by the
|
|
8
|
+
# idle clip's deform keys. The walk clip bobs the hip on Bézier keys, swings
|
|
9
|
+
# the arm and trails the cape; its "step" events mark each foot's contact,
|
|
10
|
+
# and src/main.ts's Plant moves the feet's IK targets in step with it (a
|
|
11
|
+
# 0.8 s cycle of a 0.4 m stride: the keeper walks 0.5 m/s). Written by hand
|
|
12
|
+
# (og-game §8, the rig2d grammar).
|
|
13
|
+
texture "keeper.png"
|
|
14
|
+
filter nearest
|
|
15
|
+
unit 20
|
|
16
|
+
bone root {
|
|
17
|
+
}
|
|
18
|
+
bone hip {
|
|
19
|
+
parent root
|
|
20
|
+
at 0 22
|
|
21
|
+
rotation 90
|
|
22
|
+
length 16
|
|
23
|
+
}
|
|
24
|
+
bone head {
|
|
25
|
+
parent hip
|
|
26
|
+
at 16 0
|
|
27
|
+
rotation -90
|
|
28
|
+
}
|
|
29
|
+
bone arm {
|
|
30
|
+
parent hip
|
|
31
|
+
at 14 -5
|
|
32
|
+
rotation 180
|
|
33
|
+
length 13
|
|
34
|
+
}
|
|
35
|
+
bone thigh-b {
|
|
36
|
+
parent hip
|
|
37
|
+
at 0 2
|
|
38
|
+
rotation 180
|
|
39
|
+
length 11
|
|
40
|
+
}
|
|
41
|
+
bone shin-b {
|
|
42
|
+
parent thigh-b
|
|
43
|
+
at 11 0
|
|
44
|
+
length 11
|
|
45
|
+
}
|
|
46
|
+
bone thigh-f {
|
|
47
|
+
parent hip
|
|
48
|
+
at 0 -2
|
|
49
|
+
rotation 180
|
|
50
|
+
length 11
|
|
51
|
+
}
|
|
52
|
+
bone shin-f {
|
|
53
|
+
parent thigh-f
|
|
54
|
+
at 11 0
|
|
55
|
+
length 11
|
|
56
|
+
}
|
|
57
|
+
bone cape-1 {
|
|
58
|
+
parent hip
|
|
59
|
+
at 15 4
|
|
60
|
+
rotation 180
|
|
61
|
+
length 12
|
|
62
|
+
}
|
|
63
|
+
bone cape-2 {
|
|
64
|
+
parent cape-1
|
|
65
|
+
at 12 0
|
|
66
|
+
length 12
|
|
67
|
+
}
|
|
68
|
+
bone foot-b {
|
|
69
|
+
parent root
|
|
70
|
+
at -3 0
|
|
71
|
+
}
|
|
72
|
+
bone foot-f {
|
|
73
|
+
parent root
|
|
74
|
+
at 3 0
|
|
75
|
+
}
|
|
76
|
+
slot cape {
|
|
77
|
+
bone hip
|
|
78
|
+
attachment cape
|
|
79
|
+
}
|
|
80
|
+
slot thigh-b {
|
|
81
|
+
bone thigh-b
|
|
82
|
+
attachment thigh-b
|
|
83
|
+
color 0.72 0.72 0.8 1
|
|
84
|
+
}
|
|
85
|
+
slot shin-b {
|
|
86
|
+
bone shin-b
|
|
87
|
+
attachment shin-b
|
|
88
|
+
color 0.72 0.72 0.8 1
|
|
89
|
+
}
|
|
90
|
+
slot boot-b {
|
|
91
|
+
bone shin-b
|
|
92
|
+
attachment boot-b
|
|
93
|
+
color 0.72 0.72 0.8 1
|
|
94
|
+
}
|
|
95
|
+
slot torso {
|
|
96
|
+
bone hip
|
|
97
|
+
attachment torso
|
|
98
|
+
}
|
|
99
|
+
slot thigh-f {
|
|
100
|
+
bone thigh-f
|
|
101
|
+
attachment thigh-f
|
|
102
|
+
}
|
|
103
|
+
slot shin-f {
|
|
104
|
+
bone shin-f
|
|
105
|
+
attachment shin-f
|
|
106
|
+
}
|
|
107
|
+
slot boot-f {
|
|
108
|
+
bone shin-f
|
|
109
|
+
attachment boot-f
|
|
110
|
+
}
|
|
111
|
+
slot head {
|
|
112
|
+
bone head
|
|
113
|
+
attachment head
|
|
114
|
+
}
|
|
115
|
+
slot arm {
|
|
116
|
+
bone arm
|
|
117
|
+
attachment arm
|
|
118
|
+
}
|
|
119
|
+
slot lantern {
|
|
120
|
+
bone arm
|
|
121
|
+
attachment lantern
|
|
122
|
+
}
|
|
123
|
+
region torso {
|
|
124
|
+
slot torso
|
|
125
|
+
rect 0 0 12 20
|
|
126
|
+
at 8 0
|
|
127
|
+
rotation -90
|
|
128
|
+
size 12 20
|
|
129
|
+
}
|
|
130
|
+
region head {
|
|
131
|
+
slot head
|
|
132
|
+
rect 15 0 14 12
|
|
133
|
+
at 0 5
|
|
134
|
+
rotation 0
|
|
135
|
+
size 14 12
|
|
136
|
+
}
|
|
137
|
+
region arm {
|
|
138
|
+
slot arm
|
|
139
|
+
rect 32 16 4 14
|
|
140
|
+
at 6.5 0
|
|
141
|
+
rotation 90
|
|
142
|
+
size 4 14
|
|
143
|
+
}
|
|
144
|
+
region lantern {
|
|
145
|
+
slot lantern
|
|
146
|
+
rect 52 16 8 10
|
|
147
|
+
at 15 0
|
|
148
|
+
rotation 90
|
|
149
|
+
size 8 10
|
|
150
|
+
}
|
|
151
|
+
region thigh-b {
|
|
152
|
+
slot thigh-b
|
|
153
|
+
rect 32 0 5 12
|
|
154
|
+
at 5.5 0
|
|
155
|
+
rotation 90
|
|
156
|
+
size 5 12
|
|
157
|
+
}
|
|
158
|
+
region shin-b {
|
|
159
|
+
slot shin-b
|
|
160
|
+
rect 40 0 5 12
|
|
161
|
+
at 5.5 0
|
|
162
|
+
rotation 90
|
|
163
|
+
size 5 12
|
|
164
|
+
}
|
|
165
|
+
region boot-b {
|
|
166
|
+
slot boot-b
|
|
167
|
+
rect 48 0 10 5
|
|
168
|
+
at 10.5 2
|
|
169
|
+
rotation 90
|
|
170
|
+
size 10 5
|
|
171
|
+
}
|
|
172
|
+
region thigh-f {
|
|
173
|
+
slot thigh-f
|
|
174
|
+
rect 32 0 5 12
|
|
175
|
+
at 5.5 0
|
|
176
|
+
rotation 90
|
|
177
|
+
size 5 12
|
|
178
|
+
}
|
|
179
|
+
region shin-f {
|
|
180
|
+
slot shin-f
|
|
181
|
+
rect 40 0 5 12
|
|
182
|
+
at 5.5 0
|
|
183
|
+
rotation 90
|
|
184
|
+
size 5 12
|
|
185
|
+
}
|
|
186
|
+
region boot-f {
|
|
187
|
+
slot boot-f
|
|
188
|
+
rect 48 0 10 5
|
|
189
|
+
at 10.5 2
|
|
190
|
+
rotation 90
|
|
191
|
+
size 10 5
|
|
192
|
+
}
|
|
193
|
+
mesh cape {
|
|
194
|
+
slot cape
|
|
195
|
+
vertex 0 32 hip 16.000 16.000 1
|
|
196
|
+
vertex 6 32 hip 16.000 9.000 1
|
|
197
|
+
vertex 12 32 hip 16.000 2.000 1
|
|
198
|
+
vertex 0 40 hip 7.000 16.000 0.4 cape-1 8.000 -12.000 0.6
|
|
199
|
+
vertex 6 40 hip 7.000 9.000 0.4 cape-1 8.000 -5.000 0.6
|
|
200
|
+
vertex 12 40 hip 7.000 2.000 0.4 cape-1 8.000 2.000 0.6
|
|
201
|
+
vertex 0 48 cape-1 17.000 -12.000 0.5 cape-2 5.000 -12.000 0.5
|
|
202
|
+
vertex 6 48 cape-1 17.000 -5.000 0.5 cape-2 5.000 -5.000 0.5
|
|
203
|
+
vertex 12 48 cape-1 17.000 2.000 0.5 cape-2 5.000 2.000 0.5
|
|
204
|
+
vertex 0 56 cape-2 14.000 -12.000 1
|
|
205
|
+
vertex 6 56 cape-2 14.000 -5.000 1
|
|
206
|
+
vertex 12 56 cape-2 14.000 2.000 1
|
|
207
|
+
triangles 0 3 1 1 3 4 1 4 2 2 4 5 3 6 4 4 6 7 4 7 5 5 7 8 6 9 7 7 9 10 7 10 8 8 10 11
|
|
208
|
+
}
|
|
209
|
+
ik leg-b {
|
|
210
|
+
bones thigh-b shin-b
|
|
211
|
+
target foot-b
|
|
212
|
+
bend clockwise
|
|
213
|
+
}
|
|
214
|
+
ik leg-f {
|
|
215
|
+
bones thigh-f shin-f
|
|
216
|
+
target foot-f
|
|
217
|
+
bend clockwise
|
|
218
|
+
}
|
|
219
|
+
clip idle {
|
|
220
|
+
duration 2
|
|
221
|
+
loop true
|
|
222
|
+
bone hip {
|
|
223
|
+
translate 0 0 0
|
|
224
|
+
translate 1 0 -1.5
|
|
225
|
+
translate 2 0 0
|
|
226
|
+
}
|
|
227
|
+
bone head {
|
|
228
|
+
rotate 0 0
|
|
229
|
+
rotate 1 4
|
|
230
|
+
rotate 2 0
|
|
231
|
+
}
|
|
232
|
+
bone arm {
|
|
233
|
+
rotate 0 4
|
|
234
|
+
rotate 1 -8
|
|
235
|
+
rotate 2 4
|
|
236
|
+
}
|
|
237
|
+
bone cape-1 {
|
|
238
|
+
rotate 0 -4
|
|
239
|
+
rotate 1 8
|
|
240
|
+
rotate 2 -4
|
|
241
|
+
}
|
|
242
|
+
bone cape-2 {
|
|
243
|
+
rotate 0 -6
|
|
244
|
+
rotate 1 12
|
|
245
|
+
rotate 2 -6
|
|
246
|
+
}
|
|
247
|
+
deform cape {
|
|
248
|
+
key 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
249
|
+
key 0.5 0 0 0 0 0 0 0 -1 0 -1 0 -1 0 -1 0 -1 0 -1 0 -3 0 -3 0 -3 0 -3 0 -3 0 -3 0 -5 0 -5 0 -5
|
|
250
|
+
key 1 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 2 0 2 0 2 0 2 0 2 0 2 0 3 0 3 0 3
|
|
251
|
+
key 1.5 0 0 0 0 0 0 0 -1 0 -1 0 -1 0 -1 0 -1 0 -1 0 -3.5 0 -3.5 0 -3.5 0 -3.5 0 -3.5 0 -3.5 0 -6 0 -6 0 -6
|
|
252
|
+
key 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
|
253
|
+
}
|
|
254
|
+
event 0 "inhale"
|
|
255
|
+
}
|
|
256
|
+
clip walk {
|
|
257
|
+
duration 0.8
|
|
258
|
+
loop true
|
|
259
|
+
bone hip {
|
|
260
|
+
translate 0 0 -1 curve 0.3 0 0.6 1
|
|
261
|
+
translate 0.2 0 0.6 curve 0.4 0 0.7 1
|
|
262
|
+
translate 0.4 0 -1 curve 0.3 0 0.6 1
|
|
263
|
+
translate 0.6 0 0.6 curve 0.4 0 0.7 1
|
|
264
|
+
translate 0.8 0 -1
|
|
265
|
+
}
|
|
266
|
+
bone head {
|
|
267
|
+
rotate 0 -2
|
|
268
|
+
rotate 0.4 2
|
|
269
|
+
rotate 0.8 -2
|
|
270
|
+
}
|
|
271
|
+
bone arm {
|
|
272
|
+
rotate 0 16 curve 0.42 0 0.58 1
|
|
273
|
+
rotate 0.4 -12 curve 0.42 0 0.58 1
|
|
274
|
+
rotate 0.8 16
|
|
275
|
+
}
|
|
276
|
+
bone cape-1 {
|
|
277
|
+
rotate 0 -10
|
|
278
|
+
rotate 0.2 -14
|
|
279
|
+
rotate 0.4 -10
|
|
280
|
+
rotate 0.6 -14
|
|
281
|
+
rotate 0.8 -10
|
|
282
|
+
}
|
|
283
|
+
bone cape-2 {
|
|
284
|
+
rotate 0 -12
|
|
285
|
+
rotate 0.3 -20
|
|
286
|
+
rotate 0.5 -12
|
|
287
|
+
rotate 0.7 -20
|
|
288
|
+
rotate 0.8 -12
|
|
289
|
+
}
|
|
290
|
+
event 0 "step"
|
|
291
|
+
event 0.4 "step"
|
|
292
|
+
}
|
|
Binary file
|
package/template-2d/package.json
CHANGED
package/template-2d/project.ogt
CHANGED
|
@@ -7,7 +7,7 @@ version 1
|
|
|
7
7
|
name "Origin Game 2D"
|
|
8
8
|
id "origin-game-2d"
|
|
9
9
|
# The engine this game is written against (design §9: upgrades are explicit).
|
|
10
|
-
engine "0.
|
|
10
|
+
engine "0.3.0"
|
|
11
11
|
entry "src/main.ts"
|
|
12
12
|
# 2d: an orthographic camera, sprites, flipbooks, tilemaps and 2D physics.
|
|
13
13
|
profile "2d"
|