@origingame/create-game 0.1.1 → 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 +17 -9
- package/archetypes/README.md +5 -2
- 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 +80 -20
- package/looks/catalogue.json +40 -40
- package/package.json +5 -4
- 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/.origin/setup +14 -0
- package/template-2d/LICENSES.md +15 -0
- package/template-2d/README.md +42 -0
- package/template-2d/assets/audio/coin.wav +0 -0
- package/template-2d/assets/audio/jump.wav +0 -0
- package/template-2d/assets/fonts/OFL.txt +93 -0
- package/template-2d/assets/fonts/ui.ttf +0 -0
- package/template-2d/assets/fonts/ui.ttf.ogt +11 -0
- package/template-2d/assets/looks/origin-game-2d.ogt +12 -0
- package/template-2d/assets/maps/level.ogt +112 -0
- 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/assets/sprites/coin.ogt +11 -0
- package/template-2d/assets/sprites/coin.png +0 -0
- package/template-2d/assets/sprites/coin.png.ogt +7 -0
- package/template-2d/assets/sprites/courier.ogt +34 -0
- package/template-2d/assets/sprites/courier.png +0 -0
- package/template-2d/assets/sprites/courier.png.ogt +7 -0
- package/template-2d/assets/strings/en.ogt +11 -0
- package/template-2d/assets/tiles/city.ogt +17 -0
- package/template-2d/assets/tiles/city.png +0 -0
- package/template-2d/assets/tiles/city.png.ogt +7 -0
- package/template-2d/gitattributes +22 -0
- package/template-2d/gitignore +8 -0
- package/template-2d/package.json +18 -0
- package/template-2d/project.ogt +21 -0
- package/template-2d/src/main.ts +460 -0
- package/template-2d/tests/keeper.og.ts +93 -0
- package/template-2d/tests/smoke.og.ts +60 -0
- package/template-2d/tsconfig.json +17 -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
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @origingame/create-game
|
|
2
2
|
|
|
3
|
-
Scaffolds an Origin Engine game from
|
|
4
|
-
|
|
3
|
+
Scaffolds an Origin Engine game from a project template of the engine
|
|
4
|
+
repository, the 3D template `templates/game` or the 2D starter
|
|
5
|
+
`templates/game-2d`: `project.ogt`, `src/`, `assets/`, `tests/`,
|
|
5
6
|
`package.json` and `tsconfig.json`, with the game's name and id filled in.
|
|
6
7
|
|
|
7
8
|
```sh
|
|
@@ -9,13 +10,20 @@ bun packages/create/bin/create.ts my-game --name "My Game" # in the engine rep
|
|
|
9
10
|
cd my-game && bun install && og cook && og run .
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
`create [dir] [--name "Name"] [--id slug] [--archetype name] [--premise "one line"] [--look slug] [--force]`: the
|
|
13
|
+
`create [dir] [--name "Name"] [--id slug] [--profile 2d|3d] [--archetype name] [--premise "one line"] [--look slug] [--force]`: the
|
|
13
14
|
id defaults to the name in lowercase-hyphen form (or the directory's name);
|
|
14
15
|
a directory that is not empty is refused unless `--force` is given.
|
|
16
|
+
`--profile 2d` writes the 2D starter (a courier's platformer: sprites,
|
|
17
|
+
flipbooks, a tilemap with one-way ledges, 2D physics, a HUD and a pause
|
|
18
|
+
menu, one playtest), `--profile 3d` the 3D template; without the flag, a
|
|
19
|
+
premise that says 2D, platformer, side-scroller, metroidvania, pixel art,
|
|
20
|
+
sprites or tiles without saying 3D gets the 2D starter, anything else the
|
|
21
|
+
3D template. The output's `Profile:` line says which and why.
|
|
15
22
|
`--archetype` starts from a complete game of `templates/archetypes/`
|
|
16
|
-
instead of
|
|
17
|
-
`roguelike`, `idle`, `rhythm
|
|
18
|
-
`archetypes/`)
|
|
23
|
+
instead of either template: `runner`, `match3`, `topdown`, `tower-defense`,
|
|
24
|
+
`roguelike`, `idle`, `rhythm`, `third-person` or `arena` (the published package carries them under
|
|
25
|
+
`archetypes/`); an archetype has its own profile, so `--profile` beside it
|
|
26
|
+
is refused.
|
|
19
27
|
|
|
20
28
|
Every game gets its look: `assets/looks/<id>.ogt`, an `.ogt` of `type look`
|
|
21
29
|
(og-taste §12 of the knowledge package) that `GameDesc.look` in the entry
|
|
@@ -30,7 +38,7 @@ catalogue (`packages/knowledge/looks/catalogue.json`, 37 styles):
|
|
|
30
38
|
whole, then to catalogue order). On a terminal, with neither flag, create
|
|
31
39
|
asks for the premise (`--no-prompt` does not);
|
|
32
40
|
- otherwise the template's own look stays: the plain template extends
|
|
33
|
-
`faceted-low-poly`, the idle archetype carries its
|
|
41
|
+
`faceted-low-poly`, the idle archetype carries its UI style and render
|
|
34
42
|
settings, and the other archetypes get a look that extends nothing (their
|
|
35
43
|
frames stay as their `references/` recorded them).
|
|
36
44
|
|
|
@@ -42,8 +50,8 @@ is, what it extends and how to change it. For an archetype, a preset moves
|
|
|
42
50
|
the frames its `references/` hold; create says so and how to record them
|
|
43
51
|
again.
|
|
44
52
|
|
|
45
|
-
The published package carries the template under `template
|
|
46
|
-
catalogue under `looks/`, copied by the `prepack` script
|
|
53
|
+
The published package carries the 3D template under `template/`, the 2D
|
|
54
|
+
starter under `template-2d/` and the style catalogue under `looks/`, copied by the `prepack` script
|
|
47
55
|
(`scripts/pack-template.ts`); build output (`cooked/`,
|
|
48
56
|
`dist/`, `node_modules/`) is never copied. The template's `gitattributes`
|
|
49
57
|
(media through Git LFS) is written as the project's `.gitattributes`: under
|
package/archetypes/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Archetypes
|
|
2
2
|
|
|
3
3
|
Complete games a Maker starts from, each a project like `templates/game`
|
|
4
|
-
(`project.ogt`, `src/`, `assets/`,
|
|
5
|
-
`tsconfig.json`) that `og run`, `og test` and
|
|
4
|
+
and the 2D starter `templates/game-2d` (`project.ogt`, `src/`, `assets/`,
|
|
5
|
+
`tests/`, `package.json`, `tsconfig.json`) that `og run`, `og test` and
|
|
6
|
+
`og build` take as is; `npm create @origingame/game --archetype <name>`
|
|
7
|
+
writes one, with its own profile. They
|
|
6
8
|
rebuild the platform's six Godot Discover prototypes on Origin Engine,
|
|
7
9
|
keeping each prototype's premise and lessons and none of its code or art,
|
|
8
10
|
and add a top-down room built for the engine's 2D and game-feel modules
|
|
@@ -17,6 +19,7 @@ and a third-person vale, the engine's mid-size 3D slice.
|
|
|
17
19
|
| `roguelike/` | Lamp Dive | 2d | the arrows or WASD, a tap on the next cell, a pad, or the HUD's d-pad | step a turn at a time through a fresh tomb, strike what the lamp shows, take what it finds, reach the relic on floor 2 with one life |
|
|
18
20
|
| `idle/` | Spark Orb | 2d | taps or Space on the orb, 1–4 or the cards to buy, a pad | tap for sparks, buy generators that make them, double each kind every tenth, collapse into a supernova for stardust, come back to what it made while away |
|
|
19
21
|
| `rhythm/` | Three-Lane Beat | 2d | A S D or the arrows, fingers on the lanes, a pad | read the notes falling to the line, hit each on its time, keep the combo and the groove, clear the song |
|
|
22
|
+
| `arena/` | Coin Duel | 2d | WASD or the arrows, Space or Shift to dash, a pad; two players over a room | race the other player to the coin, first to five; played in lockstep through `og relay` or the platform's rooms |
|
|
20
23
|
| `third-person/` | Ember Vale | 3d | WASD or the arrows, Shift, Space, hold F, Q and E; a pad; a touch joystick, Jump and Kindle | walk, wade and jump a lake vale at dusk, kindle four beacons before night while stone sentinels on their rounds hunt you |
|
|
21
24
|
|
|
22
25
|
Each archetype's README has its rules, components, systems, tunables, the
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Prepares this Game's cloud sandbox snapshot: Origin runs it once per
|
|
3
|
+
# snapshot build, in the checkout, without a terminal, for at most 20 minutes,
|
|
4
|
+
# and saves the result for new sandboxes of the Game. Keep it idempotent; it
|
|
5
|
+
# never receives credentials. Only ignored output is kept: node_modules/ and
|
|
6
|
+
# cooked/ (og cook is incremental, so a new sandbox starts cooked).
|
|
7
|
+
set -eu
|
|
8
|
+
if [ -f bun.lock ]; then bun install --frozen-lockfile; else bun install; fi
|
|
9
|
+
og="${ORIGIN_OG_BIN:-og}"
|
|
10
|
+
if ! command -v "$og" >/dev/null 2>&1; then
|
|
11
|
+
echo "no og on this executor class; the first og run cooks instead"
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
"$og" cook .
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Asset licences
|
|
2
|
+
|
|
3
|
+
| File | Licence | Origin |
|
|
4
|
+
|---|---|---|
|
|
5
|
+
| `assets/audio/*.ogt` | CC0 1.0 | self-made: og sfx presets (`coin`, `whoosh`, `levelup`) the cook synthesises |
|
|
6
|
+
| `assets/fx/coin.ogt` | CC0 1.0 | self-made: an untextured particle emitter |
|
|
7
|
+
|
|
8
|
+
The UI draws in the engine's built-in neon style (`assets/looks/arena.ogt`), whose fonts the engine package carries with their own licences. Everything else the game draws is solid sprites the code places; there is no other art.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Coin Duel — the arena archetype (2D, two players)
|
|
2
|
+
|
|
3
|
+
Two fighters, Red and Blue, on one floor race for one coin. Each touch
|
|
4
|
+
scores a point and moves the coin; the first to five wins the round. Move
|
|
5
|
+
with WASD or the arrows (or a pad's left stick), dash with Space or Shift.
|
|
6
|
+
|
|
7
|
+
金币对决:两名玩家在同一场地抢一枚金币,碰到得一分,金币换位置,先到五分者赢。通过房间(`og relay` 或平台房间)以锁步同步对战。
|
|
8
|
+
|
|
9
|
+
The game plays alone, with the local keys driving Red. It also plays over a
|
|
10
|
+
**room in lockstep** (docs/design.md §2.8, §2.10): each process runs the
|
|
11
|
+
whole world, and only the two players' inputs and the world's hash cross
|
|
12
|
+
the room.
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
og relay # terminal 1: ws://127.0.0.1:47900/<room>
|
|
16
|
+
og run . --room ws://127.0.0.1:47900/duel --realtime # terminals 2 and 3
|
|
17
|
+
og run . --headless --setup bot:1 --ticks 600 --json # alone, scripted
|
|
18
|
+
og test . # tests/duel.og.ts (two processes through a relay), tests/solo.og.ts
|
|
19
|
+
og polish . # og-taste and og-feel measured over both playtests
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## How it is made deterministic
|
|
23
|
+
|
|
24
|
+
- `start` sets `og.Lockstep` (`enabled`, `players: 2`, `inputDelay: 2`,
|
|
25
|
+
`hashEvery: 10`). It spawns two fighters, each with an `og.PlayerInput`
|
|
26
|
+
whose `player` is its seat, and the coin.
|
|
27
|
+
- `Submit` is the **only** system that reads `og.Input` and `og.Gamepad`.
|
|
28
|
+
It hands this peer's input to `og.net.submit`.
|
|
29
|
+
- `Move` and `CoinDrop` read `og.PlayerInput`, `Tuning` and `og.Rng` only:
|
|
30
|
+
the same on every peer.
|
|
31
|
+
- `Feedback` and `Voices` play what `Move` and `CoinDrop` asked for in
|
|
32
|
+
`Duel` (`sfx`, `took`): the coin's chime, a ring of sparks where it lay,
|
|
33
|
+
"+1" in the taker's ink, a 3-tick hit stop and a light shake; a dash's
|
|
34
|
+
whoosh; a round's fanfare and a heavier shake. They read the world
|
|
35
|
+
alone, so every peer plays them the same.
|
|
36
|
+
- `Hud` draws this peer's own line ("You are Red", "Waiting for 1 more")
|
|
37
|
+
and keeps nothing in the world.
|
|
38
|
+
|
|
39
|
+
A system that read `og.Input` to move a fighter, or one that used
|
|
40
|
+
`Math.random()`, `Date.now()` or a module variable, would part the worlds:
|
|
41
|
+
`og.Desync` names the first divergent tick, and `dump_desync` names the
|
|
42
|
+
component and field (`og run --commands "...:dump_desync:d.json"`).
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
| Command | What it does |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `bot:<n>` | This peer's input comes from scripted bot n (it runs toward the coin and weaves); 0 hands the input back to the keys. It is input, so each peer's bot drives only that peer's seat. |
|
|
49
|
+
| `warp:<player>:<x>:<y>` | Put fighter 0 (Red) or 1 (Blue) at a point of the floor (0..16 × 0..9). |
|
|
50
|
+
|
|
51
|
+
## Tunables
|
|
52
|
+
|
|
53
|
+
`Tuning`: `speed`, `dashSpeed`, `dashTicks`, `dashCooldown`, `target`,
|
|
54
|
+
`restTicks`. A tunable must be the same on every peer. Change it in code or
|
|
55
|
+
on every peer at the same tick; otherwise `og.Desync` reports it.
|
|
56
|
+
|
|
57
|
+
The look (`assets/looks/arena.ogt`) puts the UI in the catalogue's neon
|
|
58
|
+
style. `og polish` on vulkan-cpu scores taste 100 and feel 66; its one
|
|
59
|
+
finding is frame time, which lavapipe's software rasteriser sets (about
|
|
60
|
+
27 ms to draw four sprites), not the game.
|
|
61
|
+
|
|
62
|
+
## What a Maker changes first
|
|
63
|
+
|
|
64
|
+
The fighters' look (they are solid sprites), the rules in `src/game.ts`,
|
|
65
|
+
and `players` for more seats (up to 16; seats nobody holds read zero
|
|
66
|
+
input). Keep every rule a function of `og.PlayerInput`, the world and
|
|
67
|
+
`og.Rng`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type particles
|
|
2
|
+
version 1
|
|
3
|
+
---
|
|
4
|
+
# A coin taken: a ring of gold sparks thrown from where it lay.
|
|
5
|
+
maxParticles 40
|
|
6
|
+
shape point
|
|
7
|
+
spread 180
|
|
8
|
+
rate 0
|
|
9
|
+
burst 0 28
|
|
10
|
+
duration 0.1
|
|
11
|
+
looping false
|
|
12
|
+
lifetime 0.35 0.7
|
|
13
|
+
speed 3 6
|
|
14
|
+
size 0.12 0.24
|
|
15
|
+
colorOverLife {
|
|
16
|
+
key 0 1 0.8 0.3 1
|
|
17
|
+
key 1 1 0.5 0.1 0
|
|
18
|
+
}
|
|
19
|
+
drag 3
|
|
20
|
+
blend additive
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type look
|
|
2
|
+
version 1
|
|
3
|
+
---
|
|
4
|
+
# Coin Duel's look (og-taste §12), named by GameDesc.look in src/main.ts:
|
|
5
|
+
# the catalogue's neon UI style for the scores and the status line (an
|
|
6
|
+
# arcade duel on a dark floor; `og polish` flagged the engine's placeholder
|
|
7
|
+
# prototype style). The floor, the fighters and the coin are the sprites
|
|
8
|
+
# start places in their own inks.
|
|
9
|
+
name "Coin Duel"
|
|
10
|
+
ui "@styles/neon/style.ogt"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Media to Git LFS, as the platform's other scaffolds route them; text stays
|
|
2
|
+
# diffable (.ogt scenes and settings are line-based text by design).
|
|
3
|
+
*.glb filter=lfs diff=lfs merge=lfs -text
|
|
4
|
+
*.gltf filter=lfs diff=lfs merge=lfs -text
|
|
5
|
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
|
6
|
+
*.png filter=lfs diff=lfs merge=lfs -text
|
|
7
|
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
|
8
|
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
|
9
|
+
*.webp filter=lfs diff=lfs merge=lfs -text
|
|
10
|
+
*.ktx2 filter=lfs diff=lfs merge=lfs -text
|
|
11
|
+
*.hdr filter=lfs diff=lfs merge=lfs -text
|
|
12
|
+
*.exr filter=lfs diff=lfs merge=lfs -text
|
|
13
|
+
*.wav filter=lfs diff=lfs merge=lfs -text
|
|
14
|
+
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
|
15
|
+
*.ogg filter=lfs diff=lfs merge=lfs -text
|
|
16
|
+
*.flac filter=lfs diff=lfs merge=lfs -text
|
|
17
|
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
|
18
|
+
*.ttf filter=lfs diff=lfs merge=lfs -text
|
|
19
|
+
*.otf filter=lfs diff=lfs merge=lfs -text
|
|
20
|
+
*.woff2 filter=lfs diff=lfs merge=lfs -text
|
|
21
|
+
*.ogt text eol=lf
|
|
22
|
+
*.ts text eol=lf
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The host this project's playtest helpers (tests/*.ts) touch under bun:
|
|
2
|
+
// `node:` modules, `process`, `Buffer`, `TextEncoder`, `import.meta.url`.
|
|
3
|
+
// Declared here because the project type-checks with `types: []` (no
|
|
4
|
+
// @types/node, no bun-types): `og check` reads them through tsconfig.json's
|
|
5
|
+
// include. Game code (src/) uses none of them.
|
|
6
|
+
|
|
7
|
+
declare module "node:fs" {
|
|
8
|
+
export function existsSync(path: string): boolean;
|
|
9
|
+
export function readFileSync(path: string): Uint8Array;
|
|
10
|
+
export function readFileSync(path: string, encoding: string): string;
|
|
11
|
+
export function writeFileSync(path: string, data: string | Uint8Array): void;
|
|
12
|
+
export function mkdirSync(path: string, options?: { recursive?: boolean }): void;
|
|
13
|
+
export function copyFileSync(from: string, to: string): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module "node:path" {
|
|
17
|
+
export function dirname(path: string): string;
|
|
18
|
+
export function join(...parts: string[]): string;
|
|
19
|
+
export function resolve(...parts: string[]): string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare module "node:url" {
|
|
23
|
+
export function fileURLToPath(url: string | URL): string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare module "node:zlib" {
|
|
27
|
+
export function inflateSync(data: Uint8Array): Uint8Array;
|
|
28
|
+
export function deflateSync(data: Uint8Array, options?: { level?: number }): Uint8Array;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare const process: {
|
|
32
|
+
readonly env: Record<string, string | undefined>;
|
|
33
|
+
cwd(): string;
|
|
34
|
+
readonly stdout: { write(text: string): boolean };
|
|
35
|
+
};
|
|
36
|
+
declare const Buffer: { concat(chunks: readonly Uint8Array[]): Uint8Array };
|
|
37
|
+
declare class TextEncoder {
|
|
38
|
+
encode(text: string): Uint8Array;
|
|
39
|
+
}
|
|
40
|
+
declare class URL {
|
|
41
|
+
constructor(url: string, base?: string | URL);
|
|
42
|
+
readonly href: string;
|
|
43
|
+
}
|
|
44
|
+
interface ImportMeta {
|
|
45
|
+
readonly url: string;
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "origin-arena",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "og dev .",
|
|
8
|
+
"start": "og run .",
|
|
9
|
+
"build": "og build .",
|
|
10
|
+
"publish-build": "og build . --publish",
|
|
11
|
+
"cook": "og cook .",
|
|
12
|
+
"check": "og check .",
|
|
13
|
+
"test": "og test ."
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@origingame/engine": "0.3.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// Coin Duel's state and systems. Everything that moves the world reads
|
|
2
|
+
// og.PlayerInput, the per-player input lockstep makes the same on every
|
|
3
|
+
// peer; og.Input (this peer's keys) is read by one system only, Submit,
|
|
4
|
+
// which hands it to og.net.submit. That split is the whole of what makes the
|
|
5
|
+
// game play over a room (docs/design.md §2.10): a system that read og.Input
|
|
6
|
+
// to move a fighter would move it on this peer alone, and the other peer's
|
|
7
|
+
// og.Desync would name the field.
|
|
8
|
+
|
|
9
|
+
import { og, defineComponent, defineResource, defineSystem, f32, u32 } from "@origingame/engine";
|
|
10
|
+
import type { Result, World } from "@origingame/engine";
|
|
11
|
+
|
|
12
|
+
/** The arena's floor, world units: (0, 0) to (WIDTH, HEIGHT). */
|
|
13
|
+
export const WIDTH = 16;
|
|
14
|
+
export const HEIGHT = 9;
|
|
15
|
+
/** A fighter's half size and the coin's: they touch when their boxes overlap. */
|
|
16
|
+
export const FIGHTER = 0.4;
|
|
17
|
+
export const COIN = 0.25;
|
|
18
|
+
/** og.PlayerInput.buttons: bit 0 dashes. */
|
|
19
|
+
export const DASH = 1;
|
|
20
|
+
/** Duel.sfx: the sounds asked for this tick. */
|
|
21
|
+
export const Sound = { Coin: 1, Dash: 2, Win: 4 } as const;
|
|
22
|
+
/** Gold, linear: the coin, its sparks. */
|
|
23
|
+
const GOLD_TEXT = { r: 1, g: 0.78, b: 0.25 };
|
|
24
|
+
/** Red and Blue for the "+1" over the taker. */
|
|
25
|
+
const TEXT_INKS = [
|
|
26
|
+
{ r: 1, g: 0.42, b: 0.36 },
|
|
27
|
+
{ r: 0.45, g: 0.68, b: 1 },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** One of the two fighters: its own state beside the og.PlayerInput the engine writes. */
|
|
31
|
+
export const Fighter = defineComponent("Fighter", {
|
|
32
|
+
/** Ticks left of a dash (moving at Tuning.dashSpeed), then of its cooldown below zero. */
|
|
33
|
+
dash: f32(0, { unit: "ticks" }),
|
|
34
|
+
/** The last move direction, for a dash without keys. */
|
|
35
|
+
faceX: f32(1),
|
|
36
|
+
faceY: f32(0),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** The coin both fighters run for. */
|
|
40
|
+
export const Coin = defineComponent("Coin", {});
|
|
41
|
+
|
|
42
|
+
/** The round: the scores, the coin, the positions the coin and the HUD read. */
|
|
43
|
+
export const Duel = defineResource("Duel", {
|
|
44
|
+
score0: u32(),
|
|
45
|
+
score1: u32(),
|
|
46
|
+
/** The fighter who reached Tuning.target first, plus one; 0 while playing. */
|
|
47
|
+
winner: u32(),
|
|
48
|
+
/** Ticks until the next round once a fighter won. */
|
|
49
|
+
rest: u32(0, { unit: "ticks" }),
|
|
50
|
+
/** The coin was taken this tick: CoinDrop moves it. */
|
|
51
|
+
taken: u32(),
|
|
52
|
+
coinX: f32(WIDTH / 2, { unit: "m" }),
|
|
53
|
+
coinY: f32(HEIGHT / 2, { unit: "m" }),
|
|
54
|
+
coins: u32(),
|
|
55
|
+
/** Where each fighter stands, written by Move. */
|
|
56
|
+
x0: f32(0, { unit: "m" }),
|
|
57
|
+
y0: f32(0, { unit: "m" }),
|
|
58
|
+
x1: f32(0, { unit: "m" }),
|
|
59
|
+
y1: f32(0, { unit: "m" }),
|
|
60
|
+
/** Sound.* bits asked for this tick; Feedback plays and clears them. */
|
|
61
|
+
sfx: u32(),
|
|
62
|
+
/** The coin taken this tick, by whom (player + 1), and where it lay: Feedback's sparks, "+1" and shake. */
|
|
63
|
+
took: u32(),
|
|
64
|
+
tookX: f32(0, { unit: "m" }),
|
|
65
|
+
tookY: f32(0, { unit: "m" }),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** A one-shot voice: Feedback starts it when its Sound bit is asked for. */
|
|
69
|
+
export const Voice = defineComponent("Voice", { bit: u32(), plays: u32() });
|
|
70
|
+
/** The coin's spark emitter: Feedback moves it where the coin was and restarts it. */
|
|
71
|
+
export const Sparks = defineComponent("Sparks", { bursts: u32() });
|
|
72
|
+
|
|
73
|
+
/** The tunables: `set:Tuning.<field>=<value>` and Tune write them; the same on every peer, or the worlds part. */
|
|
74
|
+
export const Tuning = defineResource("Tuning", {
|
|
75
|
+
speed: f32(6, { unit: "m/s", min: 1, max: 20 }),
|
|
76
|
+
dashSpeed: f32(16, { unit: "m/s", min: 1, max: 40 }),
|
|
77
|
+
dashTicks: f32(8, { unit: "ticks", min: 1, max: 30 }),
|
|
78
|
+
dashCooldown: f32(40, { unit: "ticks", min: 0, max: 240 }),
|
|
79
|
+
target: u32(5, { min: 1, max: 99 }),
|
|
80
|
+
restTicks: u32(120, { unit: "ticks", min: 1, max: 600 }),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export function must<T>(r: Result<T>): T {
|
|
84
|
+
if (!r.ok) throw new Error(r.reason);
|
|
85
|
+
return r.value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A scripted player for playtests and the two-process run (`bot:<n>`):
|
|
90
|
+
* this peer's input comes from the tick instead of the keys. It is input,
|
|
91
|
+
* so it lives outside the world: each peer's bot drives only its own seat.
|
|
92
|
+
*/
|
|
93
|
+
let bot = 0;
|
|
94
|
+
export function setBot(n: number): void {
|
|
95
|
+
if (!Number.isInteger(n) || n < 0) throw new Error("write bot:<n> with a whole number (0 hands the keys back)");
|
|
96
|
+
bot = n;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The bot's input at a tick: towards the coin, weaving by its number, a dash every 90 ticks. */
|
|
100
|
+
function botInput(n: number, tick: number, from: { x: number; y: number }, to: { x: number; y: number }): { x: number; y: number; buttons: number } {
|
|
101
|
+
const dx = to.x - from.x;
|
|
102
|
+
const dy = to.y - from.y;
|
|
103
|
+
const len = Math.max(Math.hypot(dx, dy), 1e-3);
|
|
104
|
+
const weave = 0.7 * Math.sin(tick * 0.045 * (1 + 0.3 * n) + n);
|
|
105
|
+
return { x: dx / len - (dy / len) * weave, y: dy / len + (dx / len) * weave, buttons: (tick + 23 * n) % 90 < 2 ? DASH : 0 };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** This peer's keys (or pad, or bot) to og.net.submit: the only reader of og.Input. */
|
|
109
|
+
export const Submit = defineSystem("Submit", { reads: [og.Input, og.Gamepad, og.Lockstep, Duel] }, (_q, time, world) => {
|
|
110
|
+
if (bot > 0) {
|
|
111
|
+
const d = must(world.resource(Duel));
|
|
112
|
+
const me = must(world.resource(og.Lockstep)).localPlayer === 1 ? { x: d.x1, y: d.y1 } : { x: d.x0, y: d.y0 };
|
|
113
|
+
const input = botInput(bot, time.tick, me, { x: d.coinX, y: d.coinY });
|
|
114
|
+
must(og.net.submit(world, { x: Math.max(-1, Math.min(1, input.x)), y: Math.max(-1, Math.min(1, input.y)), buttons: input.buttons }));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const input = must(world.resource(og.Input));
|
|
118
|
+
const pad = must(world.resource(og.Gamepad));
|
|
119
|
+
const sx = pad.axis("leftX", 0);
|
|
120
|
+
const sy = pad.axis("leftY", 0);
|
|
121
|
+
const x = Math.max(-1, Math.min(1, input.axis("KeyA", "KeyD") + input.axis("ArrowLeft", "ArrowRight") + (Math.abs(sx) > 0.25 ? sx : 0)));
|
|
122
|
+
const y = Math.max(-1, Math.min(1, input.axis("KeyS", "KeyW") + input.axis("ArrowDown", "ArrowUp") - (Math.abs(sy) > 0.25 ? sy : 0)));
|
|
123
|
+
const dash = input.down("Space") || input.down("ShiftLeft") || pad.down("a", 0);
|
|
124
|
+
must(og.net.submit(world, { x, y, buttons: dash ? DASH : 0 }));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/** Each fighter from its player's og.PlayerInput: walk, dash, stay on the floor, take the coin. */
|
|
128
|
+
export const Move = defineSystem(
|
|
129
|
+
"Move",
|
|
130
|
+
{ reads: [og.PlayerInput, Tuning], writes: [Fighter, og.Transform, og.Sprite, Duel], after: ["Submit"] },
|
|
131
|
+
(q, time, world) => {
|
|
132
|
+
const t = must(world.resource(Tuning));
|
|
133
|
+
const duel = must(world.resource(Duel));
|
|
134
|
+
const playing = duel.winner === 0;
|
|
135
|
+
for (const a of q.archetypes()) {
|
|
136
|
+
const input = a.column(og.PlayerInput);
|
|
137
|
+
const f = a.column(Fighter);
|
|
138
|
+
const p = a.column(og.Transform).position;
|
|
139
|
+
const sprite = a.column(og.Sprite);
|
|
140
|
+
for (let i = 0; i < a.count; i++) {
|
|
141
|
+
let x = playing ? input.x[i]! : 0;
|
|
142
|
+
let y = playing ? input.y[i]! : 0;
|
|
143
|
+
const len = Math.hypot(x, y);
|
|
144
|
+
if (len > 1) {
|
|
145
|
+
x /= len;
|
|
146
|
+
y /= len;
|
|
147
|
+
}
|
|
148
|
+
if (len > 0.1) {
|
|
149
|
+
f.faceX[i] = x / Math.max(len, 1e-6);
|
|
150
|
+
f.faceY[i] = y / Math.max(len, 1e-6);
|
|
151
|
+
}
|
|
152
|
+
if (playing && (input.buttons[i]! & DASH) !== 0 && f.dash[i]! <= -t.dashCooldown) {
|
|
153
|
+
f.dash[i] = t.dashTicks;
|
|
154
|
+
duel.sfx |= Sound.Dash;
|
|
155
|
+
}
|
|
156
|
+
let vx = x * t.speed;
|
|
157
|
+
let vy = y * t.speed;
|
|
158
|
+
if (f.dash[i]! > 0) {
|
|
159
|
+
vx = f.faceX[i]! * t.dashSpeed;
|
|
160
|
+
vy = f.faceY[i]! * t.dashSpeed;
|
|
161
|
+
}
|
|
162
|
+
if (f.dash[i]! > -t.dashCooldown) f.dash[i]! -= 1;
|
|
163
|
+
p[i * 3] = Math.max(FIGHTER, Math.min(WIDTH - FIGHTER, p[i * 3]! + vx * time.dt));
|
|
164
|
+
p[i * 3 + 1] = Math.max(FIGHTER, Math.min(HEIGHT - FIGHTER, p[i * 3 + 1]! + vy * time.dt));
|
|
165
|
+
sprite.flash[i] = f.dash[i]! > 0 ? 0.6 : 0;
|
|
166
|
+
if (input.player[i] === 0) {
|
|
167
|
+
duel.x0 = p[i * 3]!;
|
|
168
|
+
duel.y0 = p[i * 3 + 1]!;
|
|
169
|
+
} else {
|
|
170
|
+
duel.x1 = p[i * 3]!;
|
|
171
|
+
duel.y1 = p[i * 3 + 1]!;
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
playing &&
|
|
175
|
+
duel.taken === 0 &&
|
|
176
|
+
Math.abs(p[i * 3]! - duel.coinX) < FIGHTER + COIN &&
|
|
177
|
+
Math.abs(p[i * 3 + 1]! - duel.coinY) < FIGHTER + COIN
|
|
178
|
+
) {
|
|
179
|
+
duel.taken = input.player[i]! + 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
/** The coin taken: a point to its taker, the coin to a fresh place from og.Rng, and the round's end at Tuning.target. */
|
|
187
|
+
export const CoinDrop = defineSystem(
|
|
188
|
+
"CoinDrop",
|
|
189
|
+
{ reads: [Coin, Tuning], writes: [og.Transform, Duel, og.Rng], after: ["Move"] },
|
|
190
|
+
(q, _time, world) => {
|
|
191
|
+
const t = must(world.resource(Tuning));
|
|
192
|
+
const duel = must(world.resource(Duel));
|
|
193
|
+
const rng = must(world.resource(og.Rng));
|
|
194
|
+
if (duel.winner !== 0) {
|
|
195
|
+
duel.rest -= 1;
|
|
196
|
+
if (duel.rest === 0) {
|
|
197
|
+
duel.winner = 0;
|
|
198
|
+
duel.score0 = 0;
|
|
199
|
+
duel.score1 = 0;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (duel.taken !== 0) {
|
|
203
|
+
if (duel.taken === 1) duel.score0 += 1;
|
|
204
|
+
else duel.score1 += 1;
|
|
205
|
+
duel.coins += 1;
|
|
206
|
+
duel.took = duel.taken;
|
|
207
|
+
duel.tookX = duel.coinX;
|
|
208
|
+
duel.tookY = duel.coinY;
|
|
209
|
+
duel.sfx |= Sound.Coin;
|
|
210
|
+
duel.taken = 0;
|
|
211
|
+
duel.coinX = rng.range(1, WIDTH - 1);
|
|
212
|
+
duel.coinY = rng.range(1, HEIGHT - 1);
|
|
213
|
+
const best = Math.max(duel.score0, duel.score1);
|
|
214
|
+
if (best >= t.target) {
|
|
215
|
+
duel.winner = duel.score0 >= t.target ? 1 : 2;
|
|
216
|
+
duel.rest = t.restTicks;
|
|
217
|
+
duel.sfx |= Sound.Win;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const a of q.archetypes()) {
|
|
221
|
+
const p = a.column(og.Transform).position;
|
|
222
|
+
for (let i = 0; i < a.count; i++) {
|
|
223
|
+
p[i * 3] = duel.coinX;
|
|
224
|
+
p[i * 3 + 1] = duel.coinY;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* What a coin, a dash and a round won look and sound like (og-feel §3, §4, §7): the coin's
|
|
232
|
+
* chime and a ring of sparks where it lay, "+1" in the taker's ink, a short hit stop and a
|
|
233
|
+
* light shake; a dash's whoosh; the round's fanfare and a heavier shake. Everything is asked
|
|
234
|
+
* for in Duel by the systems before and played here from the world alone, so every peer of a
|
|
235
|
+
* room plays the same.
|
|
236
|
+
*/
|
|
237
|
+
export const Feedback = defineSystem(
|
|
238
|
+
"Feedback",
|
|
239
|
+
{ writes: [Duel, Sparks, og.Transform, og.ParticleSystem], after: ["CoinDrop"] },
|
|
240
|
+
(q, time, world) => {
|
|
241
|
+
const duel = must(world.resource(Duel));
|
|
242
|
+
if (duel.took === 0) return;
|
|
243
|
+
const who = duel.took - 1;
|
|
244
|
+
must(og.render.floatingText(world, { text: "+1", position: { x: duel.tookX, y: duel.tookY + 0.5, z: 1 }, color: TEXT_INKS[who] ?? GOLD_TEXT, size: 0.6, rise: 1.2, spread: 0.2, lifetime: 0.7 }));
|
|
245
|
+
must(og.time.hitStop(world, 3));
|
|
246
|
+
must(og.camera.shake(world, duel.winner !== 0 ? 0.45 : 0.15));
|
|
247
|
+
for (const a of q.archetypes()) {
|
|
248
|
+
const p = a.column(og.Transform).position;
|
|
249
|
+
const ps = a.column(og.ParticleSystem);
|
|
250
|
+
const sp = a.column(Sparks);
|
|
251
|
+
for (let i = 0; i < a.count; i++) {
|
|
252
|
+
p[i * 3] = duel.tookX;
|
|
253
|
+
p[i * 3 + 1] = duel.tookY;
|
|
254
|
+
ps.startTick[i] = time.tick;
|
|
255
|
+
sp.bursts[i] = sp.bursts[i]! + 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
duel.took = 0;
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
/** Plays each sound Duel.sfx asks for once; a voice still sounding stops this tick and starts on the next. */
|
|
263
|
+
export const Voices = defineSystem("Voices", { writes: [Duel, Voice, og.AudioSource], after: ["Feedback"] }, (q, _time, world) => {
|
|
264
|
+
const duel = must(world.resource(Duel));
|
|
265
|
+
if (duel.sfx === 0) return;
|
|
266
|
+
let left = duel.sfx;
|
|
267
|
+
for (const a of q.archetypes()) {
|
|
268
|
+
const v = a.column(Voice);
|
|
269
|
+
const src = a.column(og.AudioSource);
|
|
270
|
+
for (let i = 0; i < a.count; i++) {
|
|
271
|
+
const b = v.bit[i]!;
|
|
272
|
+
if ((duel.sfx & b) === 0) continue;
|
|
273
|
+
if (src.playing[i]) src.playing[i] = 0;
|
|
274
|
+
else {
|
|
275
|
+
src.playing[i] = 1;
|
|
276
|
+
v.plays[i] = v.plays[i]! + 1;
|
|
277
|
+
left &= ~b;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
duel.sfx = left;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
/** The HUD: both scores, and who this peer plays or whom it waits for. */
|
|
285
|
+
export const Hud = defineSystem("Hud", { reads: [Duel, og.Lockstep] }, (_q, _time, world) => {
|
|
286
|
+
const duel = must(world.resource(Duel));
|
|
287
|
+
const net = og.net.status(world);
|
|
288
|
+
const lock = must(world.resource(og.Lockstep));
|
|
289
|
+
og.ui.anchor("Score", { anchor: "top", direction: "row", gap: 32, padding: { left: 16, top: 12, right: 16, bottom: 12 } }, () => {
|
|
290
|
+
og.ui.label("P1", `Red ${duel.score0}`, { step: "xl", color: { token: "danger" }, nowrap: true });
|
|
291
|
+
og.ui.label("P2", `Blue ${duel.score1}`, { step: "xl", color: { token: "info" }, nowrap: true });
|
|
292
|
+
});
|
|
293
|
+
// This peer's own line: it differs between peers, so it is drawn and never kept in the world.
|
|
294
|
+
const line =
|
|
295
|
+
duel.winner !== 0
|
|
296
|
+
? `${duel.winner === 1 ? "Red" : "Blue"} wins`
|
|
297
|
+
: net.running
|
|
298
|
+
? lock.localPlayer < lock.players
|
|
299
|
+
? `You are ${lock.localPlayer === 0 ? "Red" : "Blue"}`
|
|
300
|
+
: "Watching"
|
|
301
|
+
: net.peer === null
|
|
302
|
+
? "Local: Red"
|
|
303
|
+
: `Waiting for ${Math.max(0, lock.players - net.peers.length)} more`;
|
|
304
|
+
og.ui.anchor("Status", { anchor: "bottom", padding: { left: 16, top: 12, right: 16, bottom: 16 } }, () => {
|
|
305
|
+
og.ui.label("Line", line, { color: { token: "inkMuted" }, nowrap: true });
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
/** The facts `og run --json` and the probe print. */
|
|
310
|
+
export function facts(world: World): Record<string, unknown> {
|
|
311
|
+
const d = world.resource(Duel);
|
|
312
|
+
if (!d.ok) return {};
|
|
313
|
+
return { score0: d.value.score0, score1: d.value.score1, coins: d.value.coins, winner: d.value.winner };
|
|
314
|
+
}
|