@origingame/create-game 0.1.1 → 0.2.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 +15 -7
- package/archetypes/README.md +4 -2
- package/archetypes/idle/package.json +1 -1
- package/archetypes/idle/project.ogt +1 -1
- package/archetypes/match3/package.json +1 -1
- package/archetypes/match3/project.ogt +1 -1
- package/archetypes/rhythm/package.json +1 -1
- package/archetypes/rhythm/project.ogt +1 -1
- package/archetypes/roguelike/package.json +1 -1
- package/archetypes/roguelike/project.ogt +1 -1
- package/archetypes/runner/package.json +1 -1
- package/archetypes/runner/project.ogt +1 -1
- package/archetypes/third-person/package.json +1 -1
- package/archetypes/third-person/project.ogt +1 -1
- 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/bin/create.ts +71 -14
- package/package.json +5 -4
- package/template/package.json +1 -1
- package/template/project.ogt +1 -1
- package/template-2d/.origin/setup +14 -0
- package/template-2d/LICENSES.md +14 -0
- package/template-2d/README.md +39 -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/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 +305 -0
- package/template-2d/tests/smoke.og.ts +60 -0
- package/template-2d/tsconfig.json +17 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type look
|
|
2
|
+
version 1
|
|
3
|
+
---
|
|
4
|
+
# The game's look (og-taste §12): the frame's art direction as data, named
|
|
5
|
+
# by GameDesc.look in src/main.ts and applied before start runs. It starts
|
|
6
|
+
# from the catalogue's 16bit-pixel preset (a 224-pixel-tall frame, eight-tone
|
|
7
|
+
# ramps, a Bayer dither: the camera's 14 units are 16 pixels each, one per
|
|
8
|
+
# texel of the tiles); a block written below wins over the preset field by
|
|
9
|
+
# field, and `extends` takes any catalogue slug. `npm create @origingame/game`
|
|
10
|
+
# writes the game's own look from its premise, or from --look <slug>.
|
|
11
|
+
name "Origin Game 2D"
|
|
12
|
+
extends "16bit-pixel"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
type tilemap
|
|
2
|
+
version 1
|
|
3
|
+
---
|
|
4
|
+
# The level (og-game §8's grammar): 48 x 14 cells of 16 px, one world unit
|
|
5
|
+
# each, over the city tileset. The roofs collide on og.Tilemap.colliderLayers
|
|
6
|
+
# (1); the ledges name their own collider layer (2), which the courier's
|
|
7
|
+
# og.PlatformerController.oneWayLayers makes one-way: it jumps up through
|
|
8
|
+
# them and lands on top, and Down drops it through.
|
|
9
|
+
size 48 14
|
|
10
|
+
tile 16 16
|
|
11
|
+
unit 1
|
|
12
|
+
tileset "tiles/city.ogt"
|
|
13
|
+
layer skyline {
|
|
14
|
+
z -2
|
|
15
|
+
parallax 0.5 1
|
|
16
|
+
repeat x
|
|
17
|
+
legend "^" 6
|
|
18
|
+
legend "f" 4
|
|
19
|
+
legend "w" 5
|
|
20
|
+
row "................................................"
|
|
21
|
+
row "................................................"
|
|
22
|
+
row "................................................"
|
|
23
|
+
row "...........................^^^^^^^.............."
|
|
24
|
+
row ".....^^^^^^................fffffff...........^^^"
|
|
25
|
+
row ".....ffffff.....^^^^^......fwfwfwf.....^^^^^.fff"
|
|
26
|
+
row "^^^^.fwfwff.....fffff.^^^^.fffffff.....fffff.fwf"
|
|
27
|
+
row "ffff.ffffff.^^^.fwfwf.ffff.fwfwfwf.^^^.fwfwf.fff"
|
|
28
|
+
row "fwff.fwfwff.fff.fffff.fwff.fffffff.fff.fffff.fwf"
|
|
29
|
+
row "ffff.ffffff.fwf.fwfwf.ffff.fwfwfwf.fwf.fwfwf.fff"
|
|
30
|
+
row "fwff.fwfwff.fff.fffff.fwff.fffffff.fff.fffff.fwf"
|
|
31
|
+
row "ffff.ffffff.fwf.fwfwf.ffff.fwfwfwf.fwf.fwfwf.fff"
|
|
32
|
+
row "fwff.fwfwff.fff.fffff.fwff.fffffff.fff.fffff.fwf"
|
|
33
|
+
row "ffff.ffffff.fwf.fwfwf.ffff.fwfwfwf.fwf.fwfwf.fff"
|
|
34
|
+
}
|
|
35
|
+
layer roofs {
|
|
36
|
+
z 0
|
|
37
|
+
collision true
|
|
38
|
+
legend "=" 0
|
|
39
|
+
legend "b" 1
|
|
40
|
+
legend "L" 2
|
|
41
|
+
legend "D" 3
|
|
42
|
+
row "b..............................................b"
|
|
43
|
+
row "b..............................................b"
|
|
44
|
+
row "D..............................................D"
|
|
45
|
+
row "b..............................................b"
|
|
46
|
+
row "b..............................................b"
|
|
47
|
+
row "b..............................................b"
|
|
48
|
+
row "b..............................................b"
|
|
49
|
+
row "D..............................................D"
|
|
50
|
+
row "b..............................................b"
|
|
51
|
+
row "b..............................................b"
|
|
52
|
+
row "b.............................======...........b"
|
|
53
|
+
row "b.............................bbLbbb...........b"
|
|
54
|
+
row "====================...========================="
|
|
55
|
+
row "bbbLbbDbbbLbbbbbbLbb...bLbbbDbbLbbbbbbLDbbbbbLbb"
|
|
56
|
+
}
|
|
57
|
+
layer ledges {
|
|
58
|
+
z 0
|
|
59
|
+
collision true
|
|
60
|
+
colliderLayers 2
|
|
61
|
+
legend "=" 0
|
|
62
|
+
row "................................................"
|
|
63
|
+
row "................................................"
|
|
64
|
+
row "................................................"
|
|
65
|
+
row "................................................"
|
|
66
|
+
row "................................................"
|
|
67
|
+
row "................................................"
|
|
68
|
+
row ".......................=====...................."
|
|
69
|
+
row "................................................"
|
|
70
|
+
row "................................................"
|
|
71
|
+
row "...........=====................................"
|
|
72
|
+
row "................................................"
|
|
73
|
+
row "................................................"
|
|
74
|
+
row "................................................"
|
|
75
|
+
row "................................................"
|
|
76
|
+
}
|
|
77
|
+
object spawn {
|
|
78
|
+
kind "spawn"
|
|
79
|
+
at 2.5 12
|
|
80
|
+
}
|
|
81
|
+
object coin-0 {
|
|
82
|
+
kind "coin"
|
|
83
|
+
at 6.5 11.2
|
|
84
|
+
}
|
|
85
|
+
object coin-1 {
|
|
86
|
+
kind "coin"
|
|
87
|
+
at 9.5 11.2
|
|
88
|
+
}
|
|
89
|
+
object coin-2 {
|
|
90
|
+
kind "coin"
|
|
91
|
+
at 13.5 8.2
|
|
92
|
+
}
|
|
93
|
+
object coin-3 {
|
|
94
|
+
kind "coin"
|
|
95
|
+
at 21.5 9.5
|
|
96
|
+
}
|
|
97
|
+
object coin-4 {
|
|
98
|
+
kind "coin"
|
|
99
|
+
at 25.5 5.2
|
|
100
|
+
}
|
|
101
|
+
object coin-5 {
|
|
102
|
+
kind "coin"
|
|
103
|
+
at 32.5 9.2
|
|
104
|
+
}
|
|
105
|
+
object coin-6 {
|
|
106
|
+
kind "coin"
|
|
107
|
+
at 40.5 11.2
|
|
108
|
+
}
|
|
109
|
+
object goal {
|
|
110
|
+
kind "goal"
|
|
111
|
+
rect 45 10 1 2
|
|
112
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
type flipbook
|
|
2
|
+
version 1
|
|
3
|
+
---
|
|
4
|
+
# The courier (the runner archetype's sheet, tools/make-assets.py there):
|
|
5
|
+
# 32 x 40 frames standing on their feet (the pivot: the entity's position is
|
|
6
|
+
# the feet, so a squash keeps them down). Clips are numbered in file order:
|
|
7
|
+
# idle 0, run 1, jump 2, fall 3, crash 4 (src/main.ts's CLIP checks them).
|
|
8
|
+
texture "courier.png"
|
|
9
|
+
pivot 0.5 0
|
|
10
|
+
grid 32 40 8 2
|
|
11
|
+
spacing 2
|
|
12
|
+
clip idle {
|
|
13
|
+
frames 14 15
|
|
14
|
+
fps 2
|
|
15
|
+
}
|
|
16
|
+
clip run {
|
|
17
|
+
frames 0 1 2 3 4 5 6 7
|
|
18
|
+
fps 14
|
|
19
|
+
event 2 "step"
|
|
20
|
+
event 6 "step"
|
|
21
|
+
}
|
|
22
|
+
clip jump {
|
|
23
|
+
frames 8
|
|
24
|
+
fps 1
|
|
25
|
+
}
|
|
26
|
+
clip fall {
|
|
27
|
+
frames 9
|
|
28
|
+
fps 1
|
|
29
|
+
}
|
|
30
|
+
clip crash {
|
|
31
|
+
frames 10 11 12 13
|
|
32
|
+
fps 12
|
|
33
|
+
mode once
|
|
34
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type strings
|
|
2
|
+
version 1
|
|
3
|
+
---
|
|
4
|
+
# The starter's strings (og.i18n, og-i18n §1): every word the HUD shows is
|
|
5
|
+
# a key here, never a literal in a system. A locale is its own file beside
|
|
6
|
+
# it (`zh-CN.ogt`, the same keys and its `fonts` line), loaded in `start`.
|
|
7
|
+
locale "en"
|
|
8
|
+
base true
|
|
9
|
+
string "hud.level" "The Rooftops"
|
|
10
|
+
string "hud.clear" "Rooftops clear!"
|
|
11
|
+
string "pause.resume" "Resume"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type tileset
|
|
2
|
+
version 1
|
|
3
|
+
---
|
|
4
|
+
# The city (the runner archetype's tileset, tools/make-assets.py there):
|
|
5
|
+
# 0 roof, 1-3 bricks, 4-7 far skyline, 8-14 near buildings, 15 the
|
|
6
|
+
# antenna's light on. The roof and the bricks collide as whole cells on a
|
|
7
|
+
# layer marked `collision true` (og-game §8).
|
|
8
|
+
texture "tiles/city.png"
|
|
9
|
+
grid 16 16 4 4
|
|
10
|
+
spacing 2
|
|
11
|
+
filter nearest
|
|
12
|
+
solid 0 1 2 3
|
|
13
|
+
# A far window that goes on and off.
|
|
14
|
+
tile 5 {
|
|
15
|
+
frames 5 4 5
|
|
16
|
+
durations 2.5 0.4 1.8
|
|
17
|
+
}
|
|
Binary file
|
|
@@ -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,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "origin-game-2d",
|
|
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.2.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type project
|
|
2
|
+
version 1
|
|
3
|
+
---
|
|
4
|
+
# The project manifest (docs/design.md §2.5): `og run`, `og build`, `og cook`,
|
|
5
|
+
# `og dev` and `og test` read it. `npm create @origingame/game` writes this
|
|
6
|
+
# 2D starter for a 2D premise (or `--profile 2d`) and fills in the name and id.
|
|
7
|
+
name "Origin Game 2D"
|
|
8
|
+
id "origin-game-2d"
|
|
9
|
+
# The engine this game is written against (design §9: upgrades are explicit).
|
|
10
|
+
engine "0.2.0"
|
|
11
|
+
entry "src/main.ts"
|
|
12
|
+
# 2d: an orthographic camera, sprites, flipbooks, tilemaps and 2D physics.
|
|
13
|
+
profile "2d"
|
|
14
|
+
tick 60
|
|
15
|
+
seed 1
|
|
16
|
+
publish {
|
|
17
|
+
# `og build --publish` keeps only the player-facing probe members
|
|
18
|
+
# (hello, tick, state, pick, entity, input, capture).
|
|
19
|
+
probe player
|
|
20
|
+
out "dist"
|
|
21
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// The 2D starter's entry: its default export is og.game(...). `og run .`
|
|
2
|
+
// runs it headless with the probe; `og dev .` reloads it on every save;
|
|
3
|
+
// `og build .` bundles it for the play page.
|
|
4
|
+
//
|
|
5
|
+
// A courier runs and jumps the rooftops of one level: a hand-written
|
|
6
|
+
// tilemap (assets/maps/level.ogt) whose roofs collide and whose ledges are
|
|
7
|
+
// one-way on their own collider layer, an og.PlatformerController moved by
|
|
8
|
+
// the arrows, WASD, Space or a pad, the courier's flipbook clips chosen by
|
|
9
|
+
// what the controller did, coins to take, a pit that costs a life, a goal
|
|
10
|
+
// at the far end, a camera that follows, and the UI kit's platformer HUD.
|
|
11
|
+
// Everything the starter shows is in og-game §8 (sprites, flipbooks,
|
|
12
|
+
// tilemaps), §13 (the controller) and og-feel §12 (its tunables).
|
|
13
|
+
//
|
|
14
|
+
// The frame's look is data: GameDesc.look names a file under assets/looks/
|
|
15
|
+
// that extends a preset of the style catalogue, applied before start runs.
|
|
16
|
+
|
|
17
|
+
import { og, defineComponent, defineResource, defineSystem, bool, f32, u32, SHAPE_BOX } from "@origingame/engine";
|
|
18
|
+
import type { FlipbookInfo, Result, TileObject } from "@origingame/engine";
|
|
19
|
+
|
|
20
|
+
/** The courier's clips, in the sheet's file order (og-game §8): `start` checks them. */
|
|
21
|
+
const CLIP = { idle: 0, run: 1, jump: 2, fall: 3 } as const;
|
|
22
|
+
/** The camera's height in world units: 14 cells of 16 px, the 16bit-pixel look's 224-pixel frame. */
|
|
23
|
+
const VIEW_H = 14;
|
|
24
|
+
/** The ledges' collider layer (the map's `colliderLayers 2`), one-way for the courier. */
|
|
25
|
+
const LEDGES = 2;
|
|
26
|
+
/** The courier's box (og.Collider): its feet are its position. */
|
|
27
|
+
const BOX = { w: 0.9, h: 1.9 } as const;
|
|
28
|
+
/** Below this the courier fell into the pit. */
|
|
29
|
+
const FALL_Y = -3;
|
|
30
|
+
const LIVES = 3;
|
|
31
|
+
|
|
32
|
+
/** The courier. */
|
|
33
|
+
export const Hero = defineComponent("Hero", { spawnX: f32(0, { unit: "m" }), spawnY: f32(0, { unit: "m" }) });
|
|
34
|
+
|
|
35
|
+
/** A coin of the map's `coin` objects; taken once. */
|
|
36
|
+
export const Coin = defineComponent("Coin", { taken: bool() });
|
|
37
|
+
|
|
38
|
+
/** A voice: the sound bit (Sound) it plays when Game.sfx asks for it. */
|
|
39
|
+
export const Voice = defineComponent("Voice", { bit: u32() });
|
|
40
|
+
|
|
41
|
+
const Sound = { Jump: 1, Coin: 2 } as const;
|
|
42
|
+
|
|
43
|
+
/** The run: what the HUD shows and the systems share. */
|
|
44
|
+
export const Game = defineResource("Game", {
|
|
45
|
+
coins: u32(),
|
|
46
|
+
total: u32(),
|
|
47
|
+
lives: u32(LIVES),
|
|
48
|
+
won: bool(),
|
|
49
|
+
/** The last life was lost: Pickups starts the level again. */
|
|
50
|
+
restart: bool(),
|
|
51
|
+
goalX: f32(0, { unit: "m" }),
|
|
52
|
+
heroX: f32(0, { unit: "m" }),
|
|
53
|
+
heroY: f32(0, { unit: "m" }),
|
|
54
|
+
/** Sounds asked for this tick (Sound bits), played by Voices. */
|
|
55
|
+
sfx: u32(),
|
|
56
|
+
/** The pause menu is open: the courier and time hold. */
|
|
57
|
+
paused: bool(),
|
|
58
|
+
/** The HUD's touch controls this tick (a phone): the run and the held jump. */
|
|
59
|
+
touchX: f32(0, { min: -1, max: 1 }),
|
|
60
|
+
touchJump: bool(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function must<T>(r: Result<T>): T {
|
|
64
|
+
if (!r.ok) throw new Error(r.reason);
|
|
65
|
+
return r.value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Input to the controller, on the press's tick (og-feel §1): the run, the
|
|
70
|
+
* held jump (released early, the jump is lower), Down to drop through a
|
|
71
|
+
* ledge. The jump's sound starts on the press while the controller can
|
|
72
|
+
* jump (og-feel §12). A fall into the pit costs a life and puts the
|
|
73
|
+
* courier back at the spawn; the last life starts the level again.
|
|
74
|
+
*/
|
|
75
|
+
export const Control = defineSystem(
|
|
76
|
+
"Control",
|
|
77
|
+
{ reads: [Hero, og.Input, og.Gamepad], writes: [Game, og.PlatformerController, og.Transform], after: ["Hud"] },
|
|
78
|
+
(q, _time, world) => {
|
|
79
|
+
const g = must(world.resource(Game));
|
|
80
|
+
const input = must(world.resource(og.Input));
|
|
81
|
+
const pad = must(world.resource(og.Gamepad));
|
|
82
|
+
const stick = pad.axis("leftX", 0);
|
|
83
|
+
const run = Math.max(-1, Math.min(1, input.axis("ArrowLeft", "ArrowRight") + input.axis("KeyA", "KeyD") + (Math.abs(stick) > 0.25 ? stick : 0) + g.touchX));
|
|
84
|
+
const jump = input.down("Space") || input.down("ArrowUp") || input.down("KeyW") || pad.down("a", 0) || g.touchJump;
|
|
85
|
+
const pressed = input.pressed("Space") || input.pressed("ArrowUp") || input.pressed("KeyW") || pad.pressed("a", 0);
|
|
86
|
+
const down = input.down("ArrowDown") || input.down("KeyS") || pad.down("down", 0);
|
|
87
|
+
for (const a of q.archetypes()) {
|
|
88
|
+
const hero = a.column(Hero);
|
|
89
|
+
const pc = a.column(og.PlatformerController);
|
|
90
|
+
const p = a.column(og.Transform).position;
|
|
91
|
+
for (let i = 0; i < a.count; i++) {
|
|
92
|
+
const playing = !g.won && !g.paused;
|
|
93
|
+
pc.moveX[i] = playing ? run : 0;
|
|
94
|
+
pc.jump[i] = playing && jump ? 1 : 0;
|
|
95
|
+
pc.down[i] = playing && down ? 1 : 0;
|
|
96
|
+
if (playing && pressed && (pc.grounded[i] === 1 || pc.coyote[i]! > 0)) g.sfx |= Sound.Jump;
|
|
97
|
+
if (p[i * 3 + 1]! < FALL_Y) {
|
|
98
|
+
g.lives -= 1;
|
|
99
|
+
// The last life: Pickups puts every coin back.
|
|
100
|
+
if (g.lives === 0) g.restart = true;
|
|
101
|
+
p[i * 3] = hero.spawnX[i]!;
|
|
102
|
+
p[i * 3 + 1] = hero.spawnY[i]!;
|
|
103
|
+
pc.velocity[i * 3] = pc.velocity[i * 3 + 1] = 0;
|
|
104
|
+
}
|
|
105
|
+
g.heroX = p[i * 3]!;
|
|
106
|
+
g.heroY = p[i * 3 + 1]!;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
/** A coin the courier's box touches is taken; reaching the goal clears the level; the last life lost puts every coin back. */
|
|
113
|
+
export const Pickups = defineSystem("Pickups", { writes: [Game, Coin, og.Sprite], reads: [og.Transform], after: ["Control"] }, (q, _time, world) => {
|
|
114
|
+
const g = must(world.resource(Game));
|
|
115
|
+
const restart = g.restart;
|
|
116
|
+
if (restart) {
|
|
117
|
+
// The last life was lost: every coin back, the lives full.
|
|
118
|
+
g.restart = false;
|
|
119
|
+
g.coins = 0;
|
|
120
|
+
g.lives = LIVES;
|
|
121
|
+
}
|
|
122
|
+
for (const a of q.archetypes()) {
|
|
123
|
+
const coin = a.column(Coin);
|
|
124
|
+
const sprite = a.column(og.Sprite);
|
|
125
|
+
const p = a.column(og.Transform).position;
|
|
126
|
+
for (let i = 0; i < a.count; i++) {
|
|
127
|
+
if (restart) {
|
|
128
|
+
coin.taken[i] = 0;
|
|
129
|
+
sprite.visible[i] = 1;
|
|
130
|
+
}
|
|
131
|
+
if (coin.taken[i] === 1) continue;
|
|
132
|
+
const dx = Math.abs(p[i * 3]! - g.heroX);
|
|
133
|
+
const dy = p[i * 3 + 1]! - g.heroY;
|
|
134
|
+
if (dx < BOX.w / 2 + 0.35 && dy > -0.35 && dy < BOX.h + 0.35) {
|
|
135
|
+
coin.taken[i] = 1;
|
|
136
|
+
sprite.visible[i] = 0;
|
|
137
|
+
g.coins += 1;
|
|
138
|
+
g.sfx |= Sound.Coin;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!g.won && g.heroX >= g.goalX) {
|
|
143
|
+
g.won = true;
|
|
144
|
+
g.sfx |= Sound.Coin;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/** The courier's clip from what its controller did, and its facing. */
|
|
149
|
+
export const Animate = defineSystem("Animate", { reads: [Hero, og.PlatformerController], writes: [og.Flipbook, og.Sprite], after: ["Control"] }, (q) => {
|
|
150
|
+
for (const a of q.archetypes()) {
|
|
151
|
+
const pc = a.column(og.PlatformerController);
|
|
152
|
+
const fb = a.column(og.Flipbook);
|
|
153
|
+
const sprite = a.column(og.Sprite);
|
|
154
|
+
for (let i = 0; i < a.count; i++) {
|
|
155
|
+
const vx = pc.velocity[i * 3]!;
|
|
156
|
+
const vy = pc.velocity[i * 3 + 1]!;
|
|
157
|
+
const clip = pc.grounded[i] !== 1 ? (vy > 0 ? CLIP.jump : CLIP.fall) : Math.abs(vx) > 0.2 ? CLIP.run : CLIP.idle;
|
|
158
|
+
if (fb.clip[i] !== clip) fb.clip[i] = clip;
|
|
159
|
+
sprite.flipX[i] = pc.facing[i]! < 0 ? 1 : 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/** Plays each sound asked for once: a voice still playing is stopped this tick and played again the next. */
|
|
165
|
+
export const Voices = defineSystem("Voices", { reads: [Voice], writes: [Game, og.AudioSource], after: ["Pickups"] }, (q, _time, world) => {
|
|
166
|
+
const g = must(world.resource(Game));
|
|
167
|
+
if (g.sfx === 0) return;
|
|
168
|
+
let left = g.sfx;
|
|
169
|
+
for (const a of q.archetypes()) {
|
|
170
|
+
const voice = a.column(Voice);
|
|
171
|
+
const source = a.column(og.AudioSource);
|
|
172
|
+
for (let i = 0; i < a.count; i++) {
|
|
173
|
+
const bit = voice.bit[i]!;
|
|
174
|
+
if ((g.sfx & bit) === 0) continue;
|
|
175
|
+
if (source.playing[i]) source.playing[i] = 0;
|
|
176
|
+
else {
|
|
177
|
+
source.playing[i] = 1;
|
|
178
|
+
left &= ~bit;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
g.sfx = left;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The UI kit's platformer HUD (og-ui §6): lives, coins, the level's name,
|
|
187
|
+
* the pause button and, on a phone, the touch controls (taken by Control
|
|
188
|
+
* this tick: Hud runs first). Pause (the button, Escape or the pad's Start)
|
|
189
|
+
* opens the kit's pause menu and holds time until Resume.
|
|
190
|
+
*/
|
|
191
|
+
export const Hud = defineSystem("Hud", { writes: [Game] }, (_q, _time, world) => {
|
|
192
|
+
const g = must(world.resource(Game));
|
|
193
|
+
const hud = og.ui.platformerHud("Hud", {
|
|
194
|
+
lives: g.lives,
|
|
195
|
+
maxLives: LIVES,
|
|
196
|
+
coins: g.coins,
|
|
197
|
+
score: g.coins * 100,
|
|
198
|
+
level: og.i18n.t(world, g.won ? "hud.clear" : "hud.level"),
|
|
199
|
+
timeLeft: null,
|
|
200
|
+
// A d-pad and Jump where the pointer is a finger (og-ui §5).
|
|
201
|
+
touch: og.ui.frameViewport().coarsePointer,
|
|
202
|
+
});
|
|
203
|
+
g.touchX = g.paused ? 0 : hud.moveX;
|
|
204
|
+
g.touchJump = !g.paused && hud.held.includes("jump");
|
|
205
|
+
const menu = og.ui.pauseMenu("Pause", { open: g.paused, items: [{ id: "resume", label: og.i18n.t(world, "pause.resume") }] });
|
|
206
|
+
if (g.paused && menu.selected === "resume") {
|
|
207
|
+
g.paused = false;
|
|
208
|
+
must(og.time.setScale(world, 1));
|
|
209
|
+
} else if (!g.paused && hud.paused) {
|
|
210
|
+
g.paused = true;
|
|
211
|
+
must(og.time.setScale(world, 0));
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
function checkClips(info: FlipbookInfo): void {
|
|
216
|
+
for (const [name, index] of Object.entries(CLIP)) {
|
|
217
|
+
const at = og.anim.clip(info, name);
|
|
218
|
+
if (at !== index) throw new Error(`sprites/courier.ogt: clip ${name} is ${at}, the code expects ${index}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A map object's point in world units: the map's top-left corner is at (0, height). */
|
|
223
|
+
function at(o: TileObject, height: number): { x: number; y: number } {
|
|
224
|
+
return { x: o.position.x, y: height + o.position.y };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default og.game({
|
|
228
|
+
// The art direction as data (assets/looks/origin-game-2d.ogt; og-taste §12).
|
|
229
|
+
look: "looks/origin-game-2d.ogt",
|
|
230
|
+
tick: 60,
|
|
231
|
+
seed: 1,
|
|
232
|
+
resources: [Game],
|
|
233
|
+
systems: [Hud, Control, Pickups, Animate, Voices],
|
|
234
|
+
start(world) {
|
|
235
|
+
const map = must(og.load.tilemap(world, "maps/level.ogt"));
|
|
236
|
+
const courier = must(og.load.flipbook(world, "sprites/courier.ogt"));
|
|
237
|
+
checkClips(courier);
|
|
238
|
+
const coin = must(og.load.flipbook(world, "sprites/coin.ogt"));
|
|
239
|
+
const height = map.height * map.tileHeight;
|
|
240
|
+
|
|
241
|
+
const camera = must(world.resource(og.Camera));
|
|
242
|
+
camera.orthographic = true;
|
|
243
|
+
camera.height = VIEW_H;
|
|
244
|
+
camera.position = { x: 8, y: height / 2, z: 50 };
|
|
245
|
+
camera.background = { r: 0.0042, g: 0.0048, b: 0.018 };
|
|
246
|
+
|
|
247
|
+
// The map draws from its top-left corner and gives its collision layers static colliders.
|
|
248
|
+
const level = must(world.spawn({ name: "level", position: { x: 0, y: height, z: 0 } }));
|
|
249
|
+
must(world.insert(level, og.Tilemap, { map: map.map, layer: -10 }));
|
|
250
|
+
|
|
251
|
+
const g = must(world.resource(Game));
|
|
252
|
+
for (const o of map.objects) {
|
|
253
|
+
const p = at(o, height);
|
|
254
|
+
if (o.kind === "spawn") {
|
|
255
|
+
const hero = must(world.spawn({ name: "hero", position: { x: p.x, y: p.y, z: 0 } }));
|
|
256
|
+
must(world.insert(hero, Hero, { spawnX: p.x, spawnY: p.y }));
|
|
257
|
+
must(world.insert(hero, og.Sprite, { texture: courier.texture, width: 2, height: 2.5, anchor: { x: 0.5, y: 0 }, layer: 2 }));
|
|
258
|
+
must(world.insert(hero, og.Flipbook, { sheet: courier.sheet, clip: CLIP.idle }));
|
|
259
|
+
must(world.insert(hero, og.Collider, { shape: SHAPE_BOX, size: { x: BOX.w, y: BOX.h, z: 1 }, offset: { x: 0, y: BOX.h / 2, z: 0 } }));
|
|
260
|
+
// og-feel §12's platformer tunables; the ledges' layer is one-way.
|
|
261
|
+
must(world.insert(hero, og.PlatformerController, { maxSpeed: 8, jumpHeight: 3.6, timeToApex: 0.4, oneWayLayers: LEDGES, wallJumps: false }));
|
|
262
|
+
must(world.insert(hero, og.Follow2D, { offset: { x: 0, y: 1.5 }, deadZone: { x: 1, y: 0.8 }, smoothTime: 0.12, lookAhead: 0.2, lookAheadMax: { x: 2, y: 1 } }));
|
|
263
|
+
} else if (o.kind === "coin") {
|
|
264
|
+
const e = must(world.spawn({ name: o.name, position: { x: p.x, y: p.y, z: 0 } }));
|
|
265
|
+
must(world.insert(e, Coin));
|
|
266
|
+
must(world.insert(e, og.Sprite, { texture: coin.texture, width: 0.7, height: 0.7, layer: 1 }));
|
|
267
|
+
must(world.insert(e, og.Flipbook, { sheet: coin.sheet, clip: 0 }));
|
|
268
|
+
g.total += 1;
|
|
269
|
+
} else if (o.kind === "goal") {
|
|
270
|
+
g.goalX = p.x;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const [path, bit] of [["audio/jump.wav", Sound.Jump], ["audio/coin.wav", Sound.Coin]] as const) {
|
|
275
|
+
const sound = og.load.sound(world, path);
|
|
276
|
+
if (!sound.ok) continue;
|
|
277
|
+
const v = must(world.spawn({ name: `voice-${bit}` }));
|
|
278
|
+
must(world.insert(v, Voice, { bit }));
|
|
279
|
+
must(world.insert(v, og.AudioSource, { sound: sound.value, playing: false, volume: 0.5 }));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Every string the HUD shows (og-i18n §1), and its font.
|
|
283
|
+
must(og.i18n.load(world, "strings/en.ogt"));
|
|
284
|
+
const font = og.load.font(world, "fonts/ui.ttf");
|
|
285
|
+
if (font.ok) og.ui.configure(world, { font: font.value });
|
|
286
|
+
},
|
|
287
|
+
commands: {
|
|
288
|
+
/** `warp:<x>:<y>`: put the courier's feet there. */
|
|
289
|
+
warp: (world, args) => {
|
|
290
|
+
const hero = world.find("hero");
|
|
291
|
+
const x = Number(args[0]);
|
|
292
|
+
const y = Number(args[1]);
|
|
293
|
+
if (hero === null || !Number.isFinite(x) || !Number.isFinite(y)) throw new Error("write warp:<x>:<y>");
|
|
294
|
+
must(world.set(hero, og.Transform, { position: { x, y, z: 0 } }));
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
commandHelp: {
|
|
298
|
+
warp: { usage: "<x>:<y>", doc: "Put the courier's feet at a world point." },
|
|
299
|
+
},
|
|
300
|
+
facts: (world) => {
|
|
301
|
+
const g = world.resource(Game);
|
|
302
|
+
if (!g.ok) return {};
|
|
303
|
+
return { coins: g.value.coins, total: g.value.total, lives: g.value.lives, won: g.value.won, x: g.value.heroX, y: g.value.heroY };
|
|
304
|
+
},
|
|
305
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The starter's playtest, run by `og test` through `og run --headless
|
|
2
|
+
// --commands`: the courier runs right over two coins, jumps up through the
|
|
3
|
+
// one-way ledge above it and lands on top, Escape opens the pause menu and
|
|
4
|
+
// holds time, Escape again resumes, and a fall into the pit costs a life
|
|
5
|
+
// and puts the courier back at the spawn.
|
|
6
|
+
import { playtest } from "@origingame/engine/playtest";
|
|
7
|
+
|
|
8
|
+
interface Got<T> {
|
|
9
|
+
value: T;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Game {
|
|
13
|
+
coins: number;
|
|
14
|
+
lives: number;
|
|
15
|
+
paused: boolean;
|
|
16
|
+
heroX: number;
|
|
17
|
+
heroY: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface EntityDump {
|
|
21
|
+
components: Record<string, Record<string, number | boolean | string | number[]>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface UiDump {
|
|
25
|
+
elements: Array<{ id: string; text: string; width: number; height: number }>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default playtest({
|
|
29
|
+
name: "The courier runs over two coins, jumps up through a one-way ledge onto it, pauses and resumes, and the pit costs a life",
|
|
30
|
+
commands:
|
|
31
|
+
"0.5:key_down:ArrowRight;2.0:key_up:ArrowRight;" +
|
|
32
|
+
"2.3:key_down:Space;2.9:key_up:Space;" +
|
|
33
|
+
"3.5:screenshot:ledge.png;3.55:dump_entity:hero:hero.json;3.6:get:Game:ledge.json;3.65:dump_ui:ui.json;" +
|
|
34
|
+
"3.7:key_down:Escape;3.75:key_up:Escape;4.2:get:Game:paused.json;" +
|
|
35
|
+
"4.3:key_down:Escape;4.35:key_up:Escape;4.5:get:Game:resumed.json;" +
|
|
36
|
+
"4.6:warp:21:-1;5.0:get:Game:fell.json;5.1:quit",
|
|
37
|
+
check(run, t) {
|
|
38
|
+
t.ok(run.quit, "the script reached quit");
|
|
39
|
+
t.frame("ledge.png", () => {
|
|
40
|
+
const frame = run.png("ledge.png");
|
|
41
|
+
t.ok(frame.width > 0 && frame.height > 0, `a ${frame.width}x${frame.height} frame was captured`);
|
|
42
|
+
});
|
|
43
|
+
const ledge = run.json<Got<Game>>("ledge.json").value;
|
|
44
|
+
t.ok(ledge.coins === 2, `the run took the two coins on the roof (${ledge.coins})`);
|
|
45
|
+
// The ledge (cells 11-15 of row 9) has its top at y 5: the courier passed up through it and stands on it.
|
|
46
|
+
t.ok(ledge.heroX > 11 && ledge.heroX < 16, `the courier is under the ledge's span (x ${ledge.heroX.toFixed(2)})`);
|
|
47
|
+
t.ok(Math.abs(ledge.heroY - 5) < 0.05, `and stands on the one-way ledge (y ${ledge.heroY.toFixed(3)})`);
|
|
48
|
+
const hero = run.json<EntityDump>("hero.json");
|
|
49
|
+
t.ok(hero.components["og.PlatformerController"]!["grounded"] === true, "the controller is grounded on the ledge");
|
|
50
|
+
const ui = run.json<UiDump>("ui.json");
|
|
51
|
+
const count = ui.elements.find((e) => e.id === "Count");
|
|
52
|
+
t.ok(count !== undefined && count.text === "2", `the platformer HUD counts the coins ("${count?.text}")`);
|
|
53
|
+
const paused = run.json<Got<Game>>("paused.json").value;
|
|
54
|
+
t.ok(paused.paused, "Escape opened the pause menu");
|
|
55
|
+
t.ok(!run.json<Got<Game>>("resumed.json").value.paused, "Escape again resumed");
|
|
56
|
+
const fell = run.json<Got<Game>>("fell.json").value;
|
|
57
|
+
t.ok(fell.lives === 2, `the pit cost a life (${fell.lives} left)`);
|
|
58
|
+
t.ok(Math.abs(fell.heroX - 2.5) < 0.5 && fell.heroY < 2.5, `the courier is back at the spawn (${fell.heroX.toFixed(2)}, ${fell.heroY.toFixed(2)})`);
|
|
59
|
+
},
|
|
60
|
+
});
|