@origingame/create-game 0.3.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.
- 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/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/archetypes/README.md
CHANGED
|
@@ -64,7 +64,7 @@ commands its playtests use and what a Maker changes first.
|
|
|
64
64
|
og.CharacterController with blend trees and IK, navmesh sentinels on
|
|
65
65
|
og.ai, particles, decals and the post stack, on the fantasy style pack
|
|
66
66
|
(its README).
|
|
67
|
-
- **The look as data.** `idle/` names its frame treatment and
|
|
67
|
+
- **The look as data.** `idle/` names its frame treatment and style in
|
|
68
68
|
`assets/looks/idle.ogt` through `GameDesc.look` (og-taste §12); the
|
|
69
69
|
others set theirs in `start` until they move to a look. A look that
|
|
70
70
|
extends a catalogue preset changes the frames, so `references/` are
|
|
@@ -168,10 +168,10 @@ pure functions, the stars come from an og.Rng stream forked by name, and
|
|
|
168
168
|
an `owned<n>` field.
|
|
169
169
|
3. **Prestige**: `novaGain`, `dustMultiplier`.
|
|
170
170
|
4. **The look**: `assets/looks/idle.ogt` (named by `GameDesc.look`) holds
|
|
171
|
-
the frame's treatment and names the
|
|
171
|
+
the frame's treatment and names the style; `extends "<slug>"` there
|
|
172
172
|
starts from a style of the catalogue (og-taste §12), and the reference
|
|
173
173
|
frames are then recorded again (`OG_FX_CAPTURE=1 og test .`). The
|
|
174
|
-
|
|
174
|
+
style's palette is `assets/styles/sparkorb/style.ogt`, the art
|
|
175
175
|
`tools/make-assets.py`: `python3 tools/make-assets.py && og cook .`.
|
|
176
176
|
5. **Words and numbers**: `assets/strings/*.ogt` (a locale's `num.step`
|
|
177
177
|
and units make its big numbers); a new Chinese string needs the script
|
|
@@ -221,8 +221,8 @@ in the engine repository).
|
|
|
221
221
|
`tools/make-assets.py` regenerates every asset byte for byte (two runs
|
|
222
222
|
give the same bytes): the orb, the glow, the spark and the motes, the
|
|
223
223
|
emitters, the sounds and the music, the `og sfx` preset files of the UI's
|
|
224
|
-
sounds, the font subset and the
|
|
225
|
-
lists them; the
|
|
224
|
+
sounds, the font subset and the style's recoloured atlas. `LICENSES.md`
|
|
225
|
+
lists them; the style's are in `assets/styles/sparkorb/LICENSES.md`.
|
|
226
226
|
|
|
227
227
|
## Known limits (engine gaps, not design)
|
|
228
228
|
|
|
@@ -7,7 +7,7 @@ version 1
|
|
|
7
7
|
# suit a glowing orb) adds that style's treatment under the lines below,
|
|
8
8
|
# and the reference frames are then recorded again (OG_FX_CAPTURE=1 og test .).
|
|
9
9
|
name "Spark Orb"
|
|
10
|
-
# The UI wears the sparkorb
|
|
10
|
+
# The UI wears the sparkorb style (start keeps it and adds the sounds).
|
|
11
11
|
ui "styles/sparkorb/style.ogt"
|
|
12
12
|
# The night and the orb are authored colour: not tone mapped, and no light
|
|
13
13
|
# from the sky.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Spark Orb's
|
|
1
|
+
# Spark Orb's style (the engine's neon style pack, recoloured): licences
|
|
2
2
|
|
|
3
3
|
The palette, the body size and the fallback font in `style.ogt` are this
|
|
4
4
|
game's edits by hand; `atlas.png` is the pack's atlas with its magenta
|
|
@@ -15,5 +15,5 @@ frames turned violet by `tools/make-assets.py`; the rest is the pack as
|
|
|
15
15
|
| `Rajdhani-Bold.ttf` | SIL Open Font License 1.1 (`OFL.txt`) | Rajdhani Bold (Indian Type Foundry, Google Fonts); subset to Latin-1 with fontTools. Copyright (c) 2014 Indian Type Foundry (info@indiantypefoundry.com) |
|
|
16
16
|
|
|
17
17
|
Characters these fonts lack (Chinese, Japanese, Korean) are drawn with the
|
|
18
|
-
|
|
18
|
+
style's `fallback` font, `fonts/NotoSansSC-subset.ttf` (Noto Sans SC subset to
|
|
19
19
|
this game's Chinese strings, OFL 1.1, `assets/fonts/OFL.txt`).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
type style
|
|
2
2
|
version 1
|
|
3
3
|
---
|
|
4
|
-
# Spark Orb's
|
|
4
|
+
# Spark Orb's style: the engine's neon style pack (templates/ui-kit/tools/make-styles.py, CC0 art,
|
|
5
5
|
# OFL fonts; LICENSES.md) with this game's palette (the orb's gold as the one accent on night
|
|
6
6
|
# violet) and its Chinese fallback font, edited by hand.
|
|
7
7
|
name "sparkorb"
|
|
@@ -24,7 +24,7 @@ export const Sound = { Tap: 1, Buy: 2, Milestone: 4, Nova: 8, Welcome: 16, Move:
|
|
|
24
24
|
/** The run, the save and the effects' bookkeeping. */
|
|
25
25
|
export const Game = defineResource("Game", {
|
|
26
26
|
phase: u32(Phase.Title, { doc: "Phase.* of src/components.ts." }),
|
|
27
|
-
leftTitle: bool(false, { doc: "The player has left the title the game opened on: from then on the title enters with the
|
|
27
|
+
leftTitle: bool(false, { doc: "The player has left the title the game opened on: from then on the title enters with the style's entrance (the opening one is built settled, og.ui.mainMenu's instant: the web draws it behind the play gate)." }),
|
|
28
28
|
paused: bool(false),
|
|
29
29
|
settingsOpen: bool(false),
|
|
30
30
|
held: bool(false, { doc: "The time scale is held at 0 (paused, in the settings or a dialog)." }),
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// the rate.
|
|
10
10
|
//
|
|
11
11
|
// The look (GameDesc.look, a file under assets/looks/) sets the frame's
|
|
12
|
-
// treatment and the UI
|
|
12
|
+
// treatment and the UI style before start runs. `start` loads the strings,
|
|
13
13
|
// the sounds and the orb's art and spawns the rows: the orb and its glow,
|
|
14
14
|
// 48 motes (12 per generator), the burst pools, a voice per sound and the
|
|
15
15
|
// music. The systems are in
|
|
@@ -35,11 +35,11 @@ function must<T>(r: { ok: true; value?: T } | { ok: false; reason: string }): T
|
|
|
35
35
|
|
|
36
36
|
function start(world: World): void {
|
|
37
37
|
for (const l of LANGUAGES) must(og.i18n.load(world, `strings/${l.code}.ogt`));
|
|
38
|
-
// GameDesc.look set the frame and loaded the
|
|
39
|
-
// keeps that
|
|
40
|
-
const
|
|
38
|
+
// GameDesc.look set the frame and loaded the style before start; the UI
|
|
39
|
+
// keeps that style and adds the sounds.
|
|
40
|
+
const style = og.render.look(world)?.style ?? null;
|
|
41
41
|
const sound = (path: string) => must(og.load.sound(world, path));
|
|
42
|
-
must(og.ui.configure(world, { style
|
|
42
|
+
must(og.ui.configure(world, { style, sounds: { press: sound("audio/ui-press.ogt"), open: sound("audio/ui-open.ogt"), close: sound("audio/ui-close.ogt"), error: sound("audio/ui-error.ogt"), success: sound("audio/ui-success.ogt") } }));
|
|
43
43
|
|
|
44
44
|
const camera = must(world.resource(og.Camera));
|
|
45
45
|
camera.orthographic = true;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Spark Orb's screens on og.ui and the
|
|
1
|
+
// Spark Orb's screens on og.ui and the style in assets/styles/sparkorb (the
|
|
2
2
|
// engine's neon pack in the orb's gold on night violet): the title, the
|
|
3
3
|
// HUD (the sparks and their rate top left, stardust and pause top right,
|
|
4
4
|
// the four generators along the bottom, the supernova's progress), the
|
|
@@ -25,7 +25,7 @@ export const LANGUAGES = [
|
|
|
25
25
|
{ code: "zh-CN", key: "lang.zh" },
|
|
26
26
|
] as const;
|
|
27
27
|
|
|
28
|
-
/** The generators' icons in the
|
|
28
|
+
/** The generators' icons in the style's atlas. */
|
|
29
29
|
const ICONS = ["fire", "bolt", "gem", "star"] as const;
|
|
30
30
|
|
|
31
31
|
/** The tokens' spacing scale in CSS px (og.ui.frameTheme answers it too, but builds the whole theme each call). */
|
|
@@ -373,7 +373,7 @@ export const Ui = defineSystem("Ui", { reads: [Tuning, og.Input, og.Gamepad], wr
|
|
|
373
373
|
],
|
|
374
374
|
footer: say(world, "title.footer"),
|
|
375
375
|
// The title the game opens on is built settled: the web draws it behind the play gate, where
|
|
376
|
-
// the
|
|
376
|
+
// the style's entrance would stand part-way until the tap (og-platform-sdk §3). Later titles enter.
|
|
377
377
|
instant: !g.leftTitle,
|
|
378
378
|
});
|
|
379
379
|
if (!g.settingsOpen && r.selected === "play") {
|
|
@@ -272,7 +272,7 @@ render sprite 0
|
|
|
272
272
|
""")
|
|
273
273
|
|
|
274
274
|
|
|
275
|
-
# ── the
|
|
275
|
+
# ── the style's atlas ───────────────────────────────────────────────────────
|
|
276
276
|
|
|
277
277
|
def read_png(path):
|
|
278
278
|
"""An 8-bit RGBA, non-interlaced PNG's rows of pixels (what make-styles.py writes)."""
|
|
@@ -70,7 +70,7 @@ og build . --publish # dist/: the play page as it ships
|
|
|
70
70
|
of ten or more broken, louder); the rings breathe with the beat (not
|
|
71
71
|
with reduced motion). There is no hit stop: it would hold the picture
|
|
72
72
|
while the music runs on.
|
|
73
|
-
- **Screens on og.ui with a
|
|
73
|
+
- **Screens on og.ui with a style** (og-ui §2–§6, og-taste §1–§7): the
|
|
74
74
|
engine's sci-fi pack recoloured as `assets/styles/lanebeat`; the title
|
|
75
75
|
(Play, Calibrate, Settings), the HUD (the score as an `og.ui.counter`
|
|
76
76
|
and the groove top left, Pause top right, the combo beside the
|
|
@@ -221,7 +221,7 @@ in the engine repository).
|
|
|
221
221
|
give the same bytes): the chart and the stems from one set of note lists,
|
|
222
222
|
the click, the taps, the miss, the clear and the fail, the note, the ring,
|
|
223
223
|
the lane's glow, the spark, the hit's burst, the `og sfx` preset files of
|
|
224
|
-
the UI's sounds and the font subset. `LICENSES.md` lists them; the
|
|
224
|
+
the UI's sounds and the font subset. `LICENSES.md` lists them; the style's
|
|
225
225
|
are in `assets/styles/lanebeat/LICENSES.md`.
|
|
226
226
|
|
|
227
227
|
## Known limits (engine gaps, not design)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Three-Lane Beat's
|
|
1
|
+
# Three-Lane Beat's style (the engine's sci-fi style pack, recoloured): licences
|
|
2
2
|
|
|
3
3
|
The palette, the body sizes and the fallback font in `style.ogt` are this
|
|
4
4
|
game's edits by hand; the rest is the pack as `templates/styles/scifi` ships it.
|
|
@@ -13,5 +13,5 @@ game's edits by hand; the rest is the pack as `templates/styles/scifi` ships it.
|
|
|
13
13
|
| `Orbitron-Bold.ttf` | SIL Open Font License 1.1 (`OFL.txt`) | Orbitron Bold (github.com/theleagueof/orbitron); unmodified. Copyright 2018 The Orbitron Project Authors (https://github.com/theleagueof/orbitron), with Reserved Font Name: "Orbitron". |
|
|
14
14
|
|
|
15
15
|
Characters these fonts lack (Chinese, Japanese, Korean) are drawn with the
|
|
16
|
-
|
|
16
|
+
style's `fallback` font, `fonts/NotoSansSC-subset.ttf` (Noto Sans SC subset to
|
|
17
17
|
this game's Chinese strings, OFL 1.1, `assets/fonts/OFL.txt`).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
type style
|
|
2
2
|
version 1
|
|
3
3
|
---
|
|
4
|
-
# Three-Lane Beat's
|
|
4
|
+
# Three-Lane Beat's style: the engine's sci-fi style pack (templates/ui-kit/tools/make-styles.py,
|
|
5
5
|
# CC0 art, OFL fonts; LICENSES.md) with this game's palette and its Chinese fallback font, by hand.
|
|
6
6
|
name "lanebeat"
|
|
7
7
|
scheme dark
|
|
@@ -25,7 +25,7 @@ export const Sound = { Tap0: 1, Tap1: 2, Tap2: 4, Miss: 8, Clear: 16, Fail: 32,
|
|
|
25
25
|
/** The play: the song's clock, the judgements, the score, the groove, the calibration, the best. */
|
|
26
26
|
export const Game = defineResource("Game", {
|
|
27
27
|
phase: u32(Phase.Title, { doc: "Phase.* of src/components.ts." }),
|
|
28
|
-
leftTitle: bool(false, { doc: "The player has left the title the game opened on: from then on the title enters with the
|
|
28
|
+
leftTitle: bool(false, { doc: "The player has left the title the game opened on: from then on the title enters with the style's entrance (the opening one is built settled, og.ui.mainMenu's instant: the web draws it behind the play gate)." }),
|
|
29
29
|
paused: bool(false),
|
|
30
30
|
settingsOpen: bool(false),
|
|
31
31
|
held: bool(false, { doc: "The time scale is held at 0 (paused or in the settings)." }),
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// The song is its stems, started on bar lines through og.Music (src/
|
|
11
11
|
// systems.ts's Band); the chart is a scene (assets/charts/neon-pulse.ogt)
|
|
12
|
-
// of a Song row and a Note row per note. `start` loads them, the
|
|
12
|
+
// of a Song row and a Note row per note. `start` loads them, the style, the
|
|
13
13
|
// strings and the sounds, and spawns the clock, the stems, the receptors
|
|
14
14
|
// and the voices. The systems are in src/systems.ts, the screens in
|
|
15
15
|
// src/ui.ts and the state in src/components.ts.
|
|
@@ -32,9 +32,9 @@ function must<T>(r: { ok: true; value?: T } | { ok: false; reason: string }): T
|
|
|
32
32
|
|
|
33
33
|
function start(world: World): void {
|
|
34
34
|
for (const l of LANGUAGES) must(og.i18n.load(world, `strings/${l.code}.ogt`));
|
|
35
|
-
const
|
|
35
|
+
const style = must(og.load.style(world, "styles/lanebeat/style.ogt"));
|
|
36
36
|
const sound = (path: string) => must(og.load.sound(world, path));
|
|
37
|
-
must(og.ui.configure(world, { style
|
|
37
|
+
must(og.ui.configure(world, { style, sounds: { press: sound("audio/ui-press.ogt"), open: sound("audio/ui-open.ogt"), close: sound("audio/ui-close.ogt"), error: sound("audio/ui-error.ogt"), success: sound("audio/ui-success.ogt") } }));
|
|
38
38
|
|
|
39
39
|
const settings = must(world.resource(og.RenderSettings));
|
|
40
40
|
settings.environmentIntensity = 0;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Three-Lane Beat's screens on og.ui and the
|
|
1
|
+
// Three-Lane Beat's screens on og.ui and the style in assets/styles/lanebeat
|
|
2
2
|
// (the engine's sci-fi pack): the title, the HUD (the score and the groove
|
|
3
3
|
// top left, Pause top right, the combo and its multiplier beside the
|
|
4
4
|
// highway), the pause menu, the settings with the offset, the calibration
|
|
@@ -380,7 +380,7 @@ export const Ui = defineSystem("Ui", { reads: [Tuning, og.Input, og.Gamepad], wr
|
|
|
380
380
|
],
|
|
381
381
|
footer: say(world, "title.footer"),
|
|
382
382
|
// The title the game opens on is built settled: the web draws it behind the play gate, where
|
|
383
|
-
// the
|
|
383
|
+
// the style's entrance would stand part-way until the tap (og-platform-sdk §3). Later titles enter.
|
|
384
384
|
instant: !g.leftTitle,
|
|
385
385
|
});
|
|
386
386
|
if (!g.settingsOpen && r.selected === "play") begin(world, g);
|
|
@@ -61,7 +61,7 @@ og build . --publish # dist/: the play page as it ships
|
|
|
61
61
|
flash, a squash, the red number, a hit stop and a kick; the guardian
|
|
62
62
|
glows red and shows "!" the turn before a charged strike (twice the hit
|
|
63
63
|
stop and trauma); its fall and the lamp going out are heavier still.
|
|
64
|
-
- **Screens on og.ui with a
|
|
64
|
+
- **Screens on og.ui with a style** (og-ui §2–§6, og-taste §1–§7): the
|
|
65
65
|
engine's pixel pack recoloured as `assets/styles/lampdive` (the tomb's
|
|
66
66
|
violet panels, the lamp's amber as the one accent); the title over the
|
|
67
67
|
lamp-lit first room, the HUD as capsules in the corners (health meter,
|
|
@@ -237,7 +237,7 @@ in the engine repository).
|
|
|
237
237
|
`tools/make-assets.py` regenerates every asset byte for byte (two runs
|
|
238
238
|
give the same bytes): the tileset, the map, the sprite sheet and the glow,
|
|
239
239
|
the sounds and the music, the `og sfx` preset files of the UI's sounds and
|
|
240
|
-
the footstep, and the font subset. `LICENSES.md` lists them; the
|
|
240
|
+
the footstep, and the font subset. `LICENSES.md` lists them; the style's
|
|
241
241
|
are in `assets/styles/lampdive/LICENSES.md`.
|
|
242
242
|
|
|
243
243
|
## Known limits (engine gaps, not design)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Lamp Dive's
|
|
1
|
+
# Lamp Dive's style (the engine's pixel style pack, recoloured): licences
|
|
2
2
|
|
|
3
3
|
The palette, the body text's 1 px dark shadow and the fallback font in
|
|
4
4
|
`style.ogt` are this game's edits by hand; the rest is the pack as
|
|
@@ -12,5 +12,5 @@ The palette, the body text's 1 px dark shadow and the fallback font in
|
|
|
12
12
|
| `DotGothic16-Regular.ttf` | SIL Open Font License 1.1 (`OFL.txt`) | DotGothic16 (github.com/fontworks-fonts/DotGothic16); subset to Latin-1 with fontTools. Copyright 2020 The DotGothic16 Project Authors (https://github.com/fontworks-fonts/DotGothic16/) |
|
|
13
13
|
|
|
14
14
|
Characters these fonts lack (Chinese, Japanese, Korean) are drawn with the
|
|
15
|
-
|
|
15
|
+
style's `fallback` font, `fonts/NotoSansSC-subset.ttf` (Noto Sans SC subset to
|
|
16
16
|
this game's Chinese strings, OFL 1.1, `assets/fonts/OFL.txt`).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
type style
|
|
2
2
|
version 1
|
|
3
3
|
---
|
|
4
|
-
# Lamp Dive's
|
|
4
|
+
# Lamp Dive's style: the engine's pixel style pack (templates/ui-kit/tools/make-styles.py; CC0 art, OFL
|
|
5
5
|
# fonts; LICENSES.md) in tomb colours: plum stone panels, the lamp's amber as the one accent.
|
|
6
6
|
name "lampdive"
|
|
7
7
|
scheme dark
|
|
@@ -30,7 +30,7 @@ export const LOOT_SLOTS = 12;
|
|
|
30
30
|
/** The dive: the lamp-bearer, the floor, the turn, the counts and the effects' bookkeeping. */
|
|
31
31
|
export const Game = defineResource("Game", {
|
|
32
32
|
phase: u32(Phase.Title, { doc: "Phase.* of src/components.ts." }),
|
|
33
|
-
leftTitle: bool(false, { doc: "The player has left the title the game opened on: from then on the title enters with the
|
|
33
|
+
leftTitle: bool(false, { doc: "The player has left the title the game opened on: from then on the title enters with the style's entrance (the opening one is built settled, og.ui.mainMenu's instant: the web draws it behind the play gate)." }),
|
|
34
34
|
paused: bool(false),
|
|
35
35
|
settingsOpen: bool(false),
|
|
36
36
|
held: bool(false, { doc: "The time scale is held at 0 (paused or in the settings)." }),
|
|
@@ -37,9 +37,9 @@ function must<T>(r: { ok: true; value?: T } | { ok: false; reason: string }): T
|
|
|
37
37
|
|
|
38
38
|
function start(world: World): void {
|
|
39
39
|
for (const l of LANGUAGES) must(og.i18n.load(world, `strings/${l.code}.ogt`));
|
|
40
|
-
const
|
|
40
|
+
const style = must(og.load.style(world, "styles/lampdive/style.ogt"));
|
|
41
41
|
const sound = (path: string) => must(og.load.sound(world, path));
|
|
42
|
-
must(og.ui.configure(world, { style
|
|
42
|
+
must(og.ui.configure(world, { style, sounds: { press: sound("audio/ui-press.ogt"), open: sound("audio/ui-open.ogt"), close: sound("audio/ui-close.ogt"), error: sound("audio/ui-error.ogt"), success: sound("audio/ui-success.ogt") } }));
|
|
43
43
|
|
|
44
44
|
const settings = must(world.resource(og.RenderSettings));
|
|
45
45
|
settings.environmentIntensity = 0;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Lamp Dive's screens on og.ui and the
|
|
1
|
+
// Lamp Dive's screens on og.ui and the style in assets/styles/lampdive (the
|
|
2
2
|
// engine's pixel pack in tomb colours): the title, the HUD (health, strike,
|
|
3
3
|
// armour and gold in the top-left corner, the floor and pause in the
|
|
4
4
|
// top-right, the draught and, on a touch screen, Wait and a d-pad along the
|
|
@@ -317,7 +317,7 @@ export const Ui = defineSystem("Ui", { reads: [Tuning], writes: [Game, Prefs, og
|
|
|
317
317
|
],
|
|
318
318
|
footer: say(world, "title.footer"),
|
|
319
319
|
// The title the game opens on is built settled: the web draws it behind the play gate, where
|
|
320
|
-
// the
|
|
320
|
+
// the style's entrance would stand part-way until the tap (og-platform-sdk §3). Later titles enter.
|
|
321
321
|
instant: !g.leftTitle,
|
|
322
322
|
});
|
|
323
323
|
if (!g.settingsOpen && r.selected === "play") startDive(world, g);
|
|
@@ -180,7 +180,7 @@ stages.
|
|
|
180
180
|
2. **The pressure**: `duskSeconds`, the sentinels' speeds and sight, their
|
|
181
181
|
minds in `assets/ai/`, `HEARTS` in `src/components.ts`.
|
|
182
182
|
3. **The look**: the post stack, fog and sky in `buildVale`, the water's
|
|
183
|
-
colours, the
|
|
183
|
+
colours, the style's palette in `assets/styles/ember/style.ogt`.
|
|
184
184
|
4. **The characters**: another CC0 humanoid in `assets/models/` with the
|
|
185
185
|
clip library retargeted onto it (`src/anim.ts`), the graphs' thresholds.
|
|
186
186
|
5. **Words**: `assets/strings/*.ogt`; a new Chinese string needs the script
|
|
@@ -230,7 +230,7 @@ Nothing here is downloaded. `tools/make-assets.py` regenerates the vale's
|
|
|
230
230
|
heightmap and control map, the sounds and music, the `og sfx` presets, the
|
|
231
231
|
sentinels' minds and the font subset byte for byte; the characters, the
|
|
232
232
|
clip library, the textures and the beacon's effects are the engine's own
|
|
233
|
-
CC0 assets. `LICENSES.md` lists every file; the
|
|
233
|
+
CC0 assets. `LICENSES.md` lists every file; the style's are in
|
|
234
234
|
`assets/styles/ember/LICENSES.md`.
|
|
235
235
|
|
|
236
236
|
## Known limits (engine gaps, not design)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# The ember
|
|
1
|
+
# The ember style (a copy of the fantasy style pack): licences
|
|
2
2
|
|
|
3
3
|
| File | Licence | Origin |
|
|
4
4
|
|---|---|---|
|
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
| `Newsreader-Medium.ttf` | SIL Open Font License 1.1 (`OFL.txt`) | Newsreader (github.com/productiontype/Newsreader); a static instance (wght 500, opsz 16) of the variable font, subset to Latin-1 with fontTools. Copyright 2020 The Newsreader Project Authors (http://github.com/productiontype/Newsreader) |
|
|
10
10
|
|
|
11
11
|
Characters these fonts lack (Chinese, Japanese, Korean) are drawn with the
|
|
12
|
-
|
|
12
|
+
style's `fallback` font, `fonts/NotoSansSC-subset.ttf` (the platform's
|
|
13
13
|
subset, OFL 1.1, `assets/fonts/OFL.txt`).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
type style
|
|
2
2
|
version 1
|
|
3
3
|
---
|
|
4
|
-
# Ember Vale's
|
|
4
|
+
# Ember Vale's style: a copy of the fantasy style pack (templates/ui-kit/tools/make-styles.py; CC0 art, OFL fonts; LICENSES.md).
|
|
5
5
|
name "ember"
|
|
6
6
|
scheme dark
|
|
7
7
|
atlas "styles/ember/atlas.png" 384 256
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -31,7 +31,7 @@ export const HEARTS = 3;
|
|
|
31
31
|
/** The run: the hero's progress, the clock and the effects' bookkeeping. */
|
|
32
32
|
export const Game = defineResource("Game", {
|
|
33
33
|
phase: u32(Phase.Title, { doc: "Phase.* of src/components.ts." }),
|
|
34
|
-
leftTitle: bool(false, { doc: "The player has left the title the game opened on: from then on the title enters with the
|
|
34
|
+
leftTitle: bool(false, { doc: "The player has left the title the game opened on: from then on the title enters with the style's entrance (the opening one is built settled, og.ui.mainMenu's instant: the web draws it behind the play gate)." }),
|
|
35
35
|
ending: u32(Ending.None, { doc: "Ending.* once a run is over." }),
|
|
36
36
|
paused: bool(false),
|
|
37
37
|
settingsOpen: bool(false),
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// grab costs a heart. Three hearts gone, or night, ends the run; every
|
|
9
9
|
// beacon lit wins it. Then the summary, and again.
|
|
10
10
|
//
|
|
11
|
-
// `start` loads the strings, the
|
|
11
|
+
// `start` loads the strings, the style and the sounds, builds the vale
|
|
12
12
|
// (src/world.ts) and spawns the hero (src/hero.ts), the sentinels
|
|
13
13
|
// (src/sentinels.ts) and the voices (src/sound.ts). The beacons are in
|
|
14
14
|
// src/beacons.ts, the screens and the run's clock in src/ui.ts and the
|
|
@@ -26,9 +26,9 @@ import { BEACON_AT, buildVale, ground, HOURS, must, setHour } from "./world.ts";
|
|
|
26
26
|
|
|
27
27
|
function start(world: World): void {
|
|
28
28
|
for (const l of LANGUAGES) must(og.i18n.load(world, `strings/${l.code}.ogt`));
|
|
29
|
-
const
|
|
29
|
+
const style = must(og.load.style(world, "styles/ember/style.ogt"));
|
|
30
30
|
const sound = (path: string) => must(og.load.sound(world, path));
|
|
31
|
-
must(og.ui.configure(world, { style
|
|
31
|
+
must(og.ui.configure(world, { style, sounds: { press: sound("audio/ui-press.ogt"), open: sound("audio/ui-open.ogt"), close: sound("audio/ui-close.ogt"), error: sound("audio/ui-error.ogt"), success: sound("audio/ui-success.ogt") } }));
|
|
32
32
|
buildVale(world);
|
|
33
33
|
spawnHero(world);
|
|
34
34
|
spawnSentinels(world);
|
|
@@ -31,7 +31,7 @@ export function spawnVoices(world: World): void {
|
|
|
31
31
|
must(world.insert(fx, og.AudioBusFx, { bus: "music", duck: true, duckBus: "sfx", duckDepth: 5, duckRelease: 0.6 }));
|
|
32
32
|
const echo = must(world.spawn({ name: "sfx-bus" }));
|
|
33
33
|
must(world.insert(echo, og.AudioBusFx, { bus: "sfx", reverb: 0.18 }));
|
|
34
|
-
// The touch HUD's Jump and Kindle: the
|
|
34
|
+
// The touch HUD's Jump and Kindle: the style's press sound on the ui bus,
|
|
35
35
|
// like every og.ui button, beside the jump's or the fire's own effect.
|
|
36
36
|
const tap = must(world.spawn({ name: `sfx-${Sound.Tap}` }));
|
|
37
37
|
must(world.insert(tap, og.AudioSource, { sound: sound("audio/ui-press.ogt"), playing: false, bus: "ui", volume: 0.5, priority: 150 }));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Ember Vale's screens on og.ui and the
|
|
1
|
+
// Ember Vale's screens on og.ui and the style in assets/styles/ember (the
|
|
2
2
|
// engine's fantasy pack): the title over an orbit of the vale, the HUD
|
|
3
3
|
// (hearts and beacons top-left, the dusk clock and pause top-right, the
|
|
4
4
|
// kindling prompt and its progress at the bottom; on a touch screen a
|
|
@@ -421,7 +421,7 @@ export const Ui = defineSystem("Ui", { reads: [Tuning, og.Gamepad, og.Pointers],
|
|
|
421
421
|
],
|
|
422
422
|
footer: say(world, og.ui.frameViewport().coarsePointer ? "title.footer.touch" : "title.footer"),
|
|
423
423
|
// The title the game opens on is built settled: the web draws it behind the play gate, where
|
|
424
|
-
// the
|
|
424
|
+
// the style's entrance would stand part-way until the tap (og-platform-sdk §3). Later titles enter.
|
|
425
425
|
instant: !g.leftTitle,
|
|
426
426
|
});
|
|
427
427
|
if (!g.settingsOpen && r.selected === "play" && g.phase === Phase.Title) startRun(world, g);
|
|
@@ -28,7 +28,7 @@ export const Refusal = { None: 0, Poor: 1, Blocked: 2, Taken: 3 } as const;
|
|
|
28
28
|
/** The round: the lantern's light, the purse, the waves and the effects' bookkeeping. */
|
|
29
29
|
export const Game = defineResource("Game", {
|
|
30
30
|
phase: u32(Phase.Title, { doc: "Phase.* of src/components.ts." }),
|
|
31
|
-
leftTitle: bool(false, { doc: "The player has left the title the game opened on: from then on the title enters with the
|
|
31
|
+
leftTitle: bool(false, { doc: "The player has left the title the game opened on: from then on the title enters with the style's entrance (the opening one is built settled, og.ui.mainMenu's instant: the web draws it behind the play gate)." }),
|
|
32
32
|
paused: bool(false, { doc: "The pause menu is open (the garden holds)." }),
|
|
33
33
|
settingsOpen: bool(false, { doc: "The settings dialog is open." }),
|
|
34
34
|
won: bool(false, { doc: "The last round held every wave." }),
|
|
@@ -32,11 +32,11 @@ function must<T>(r: { ok: true; value?: T } | { ok: false; reason: string }): T
|
|
|
32
32
|
|
|
33
33
|
function start(world: World): void {
|
|
34
34
|
for (const l of LANGUAGES) must(og.i18n.load(world, `strings/${l.code}.ogt`));
|
|
35
|
-
const
|
|
35
|
+
const style = must(og.load.style(world, "styles/pathguard/style.ogt"));
|
|
36
36
|
const sound = (path: string) => must(og.load.sound(world, path));
|
|
37
37
|
must(
|
|
38
38
|
og.ui.configure(world, {
|
|
39
|
-
style
|
|
39
|
+
style,
|
|
40
40
|
sounds: { press: sound("audio/ui-press.ogt"), open: sound("audio/ui-open.ogt"), close: sound("audio/ui-close.ogt"), error: sound("audio/ui-error.ogt"), success: sound("audio/ui-success.ogt") },
|
|
41
41
|
}),
|
|
42
42
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Path Guard's screens on og.ui and the
|
|
1
|
+
// Path Guard's screens on og.ui and the style in assets/styles/pathguard:
|
|
2
2
|
// the title, the HUD (the light and the coins in the top-left corner, the
|
|
3
3
|
// wave and pause in the top-right, the plant tray on the right on a wide
|
|
4
4
|
// screen and along the bottom on a phone), the pause menu, the settings
|
|
@@ -351,7 +351,7 @@ export const Ui = defineSystem("Ui", { reads: [Tuning], writes: [Game, Prefs, og
|
|
|
351
351
|
],
|
|
352
352
|
footer: say(world, "title.footer"),
|
|
353
353
|
// The title the game opens on is built settled: the web draws it behind the play gate, where
|
|
354
|
-
// the
|
|
354
|
+
// the style's entrance would stand part-way until the tap (og-platform-sdk §3). Later titles enter.
|
|
355
355
|
instant: !g.leftTitle,
|
|
356
356
|
});
|
|
357
357
|
if (!g.settingsOpen && r.selected === "play") startRound(world, g, t);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@origingame/create-game",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Scaffolds an Origin Engine game from the project template: npm create @origingame/game.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,6 +29,6 @@
|
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
31
|
"prepack": "bun scripts/pack-template.ts",
|
|
32
|
-
"test": "bun test ${OG_BUN_TEST_TIMEOUT:+--timeout=$OG_BUN_TEST_TIMEOUT} tests/"
|
|
32
|
+
"test": "bun scripts/pack-template.ts && bun test ${OG_BUN_TEST_TIMEOUT:+--timeout=$OG_BUN_TEST_TIMEOUT} tests/"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/template/package.json
CHANGED
package/template/project.ogt
CHANGED
package/template-2d/README.md
CHANGED
|
@@ -7,7 +7,8 @@ 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
8
|
wall; Escape (or the HUD's Pause) opens the pause menu. Beside the spawn a
|
|
9
9
|
lamp keeper, a 2D skeletal rig, breathes on a ramp with its feet planted
|
|
10
|
-
and walks along it when sent (`keeper:<x>`)
|
|
10
|
+
and walks along it when sent (`keeper:<x>`): it turns over a few ticks,
|
|
11
|
+
steps along the plank, lets both feet land and turns back.
|
|
11
12
|
|
|
12
13
|
```sh
|
|
13
14
|
og run . # headless with the probe; og dev . reloads on save
|
|
@@ -22,7 +23,7 @@ og build . # the play page
|
|
|
22
23
|
| The project in the 2D profile: an orthographic camera, sprites, 2D physics | `project.ogt`, `start` in `src/main.ts` | og-game §2, §8 |
|
|
23
24
|
| 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) |
|
|
24
25
|
| 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 |
|
|
26
|
+
| 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 (turning over 12 ticks, no foot faster than the gait's swing, settling before idle), 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 |
|
|
26
27
|
| 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 |
|
|
27
28
|
| The map's objects: the spawn, the coins, the goal | `start`, `Pickups` | og-game §8 |
|
|
28
29
|
| 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 |
|
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.4.0"
|
|
11
11
|
entry "src/main.ts"
|
|
12
12
|
# 2d: an orthographic camera, sprites, flipbooks, tilemaps and 2D physics.
|
|
13
13
|
profile "2d"
|
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.
|