oriverse-engine 0.1.1

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.
@@ -0,0 +1,512 @@
1
+ # Oriverse game authoring guide (artifact / CDN build)
2
+
3
+ How to write a complete small game in Weave for the Oriverse artifact engine.
4
+ Everything here works offline and fully procedurally — no market assets, no network.
5
+ Three complete, engine-tested example games are at the end; pattern-match from them.
6
+ The full language/API reference is `weave_documentation.txt` (same folder/URL as this file).
7
+
8
+ ## 1. The shape of a game
9
+
10
+ A game is ONE block of Weave text (the "world description") placed inside the
11
+ `<script type="text/weave" id="oriverse-weave">` tag of your `game.html`. It has two layers:
12
+
13
+ ```text
14
+ #class SomeClass <- class code: events + functions (one block per class)
15
+ ...weave statements...
16
+ #end
17
+ #texture SomeTexture <- optional procedural texture DSL
18
+ #end
19
+ #mesh SomeMesh <- optional procedural mesh DSL
20
+ #end
21
+
22
+ saved thing { ... } <- spawnable-later object templates
23
+ object Floor { ... } <- scene objects placed at boot
24
+ display hint { ... } <- visual-only object (no collision/interaction)
25
+ physics crate { ... } <- dynamic (gravity) object
26
+ region goal { ... } <- invisible overlap-test volume
27
+ screenCanvas HUD { ... } <- 2D screen-space UI
28
+ ```
29
+
30
+ The engine boots straight into play mode. A player spawns at `Origin()` (0,0,0) with a
31
+ third-person camera. If you do NOT define `#class player`, a default controller is
32
+ injected: WASD camera-relative movement + Space jump. Define `#class player ... #end`
33
+ only to customize movement/camera/abilities.
34
+
35
+ ## 2. World description basics
36
+
37
+ - Units: 1 unit = 1 cm. Z is up. A tile is 100x100 cm. The player is 180 cm tall, 40 wide.
38
+ - Primitive models: `"cube"`, `"sphere"`, `"cylinder"`, `"capsule"`, `"cone"`, `"wedge"`.
39
+ Base cube is 100x100x100; sphere is 100 diameter.
40
+ - `position = (x, y, z)` for mesh-backed objects is the BOTTOM-CENTER pivot (an object at
41
+ z=0 stands on the ground plane). `size = (x, y, z)` stretches; scalar `size = 80` is uniform.
42
+ - For axis-aligned walls/floors prefer exact corners:
43
+ `object NorthWall { model="cube" bounds_min=(-1500,1100,0) bounds_max=(1500,1180,280) }`
44
+ - `color = (r, g, b)` channels are 0-1 floats (or `#RRGGBB`). FOOTGUN: 0-255 style values
45
+ clamp every channel to 1 and render untinted white.
46
+ - Attach a class with `:` — `object Tower : TowerClass { ... }`. Override class variables
47
+ from the scene: `object boss : Enemy { override MaxHP = 500 }` (Number/Bool/Vector only).
48
+ - Tags need no declaration: `tag = Tag.Coin` in a clause, `AddTag(Tag.Coin)` in code.
49
+ - Object kinds: `object` (static, collides), `physics` (dynamic, falls), `display`
50
+ (visual-only: no collision, no Touched/Click events), `crowd` (NPC with avoidance),
51
+ `region` (invisible overlap volume), `saved` (template for runtime spawning).
52
+ - A hidden "manager" object is the standard place for game state and loops:
53
+
54
+ ```text
55
+ display manager : GameManager { visible = false }
56
+ ```
57
+
58
+ ## 3. Class code: events, loops, timers
59
+
60
+ ```text
61
+ #class GameManager
62
+ var Score = 0 // class variables persist; readable/writable cross-object
63
+ event OnPreSpawn { // runs before OnSpawned; put AddTag(...) here
64
+ AddTag(Tag.Manager)
65
+ }
66
+ event OnSpawned { // main entry; runs once when the object spawns
67
+ var tmr = Timer() // Timer starts in the Done state
68
+ forever { // once per frame (implicit WaitNextFrame between iterations)
69
+ if tmr.IsDone() {
70
+ tmr.StartCountDown(0.5)
71
+ // ... do something every 0.5 s ...
72
+ }
73
+ }
74
+ }
75
+ event KeyboardDown[R] { // any player pressed R (use ...ByThisPlayer only in #class player)
76
+ RestartGame()
77
+ }
78
+ #end
79
+ ```
80
+
81
+ - `forever { ... }` = per-frame loop; `Wait(seconds)` sleeps inside event code;
82
+ `continue` skips to the next frame inside `forever`.
83
+ - At most ONE continuous move and ONE continuous rotate stick per object per frame
84
+ (`Move*Continuously`, `RotateToward*` — the last call wins). Teleports win over moves.
85
+ - `GetPosition()` returns the transform from the START of the current frame.
86
+ - Cross-object access works on variables and functions: `other.Score += 1`, `other.Pop()`.
87
+ - Text concat: `"Score: " + Score`. Random: `RandomNumber(from, to)` (deterministic per tick).
88
+ - Debug: `Print("...")` to console, `Say("...")` as a speech bubble,
89
+ `SpawnFloatingText("+1", Vector(0,1,0))` for damage/pickup popups.
90
+
91
+ ## 4. The player class
92
+
93
+ Default (injected when you don't write one):
94
+
95
+ ```text
96
+ #class player
97
+ event OnSpawned {
98
+ forever {
99
+ var Direction = GetPlayerInputDirectionCamera(Key.W, Key.S, Key.A, Key.D)
100
+ MoveInDirectionContinuouslyFacing(Direction, 900)
101
+ }
102
+ }
103
+ event KeyboardDownByThisPlayer[Space] {
104
+ if IsOnGround() {
105
+ Jump(1000)
106
+ }
107
+ }
108
+ #end
109
+ ```
110
+
111
+ Customization patterns (all inside `#class player`):
112
+
113
+ - Speed/feel: change `900` (cm/s); prefer `GetMoveVectorCamera()` for gameplay movement
114
+ (it also handles mobile thumbsticks). `Jump(1400)` for a higher jump.
115
+ - Actions: `event ActionDownByThisPlayer[Primary]` (left click / tap), `[Secondary]`,
116
+ `[Interact]` (E), `[Jump]`. `IsActionPressedByThisPlayer(Action.Primary)` for held state.
117
+ - Aim at the crosshair: `GetMouseTargetObjectWithTag(Tag.Target)` returns the object under
118
+ the crosshair (or `None` — always check `.Exists()`).
119
+ - Dash (compose motion yourself; last continuous call in a frame wins):
120
+
121
+ ```text
122
+ var DashTmr : Timer
123
+ event KeyboardDownByThisPlayer[LeftShift] {
124
+ DashTmr.StartCountDown(0.15)
125
+ }
126
+ event OnSpawned {
127
+ forever {
128
+ var dir = GetMoveVectorCamera()
129
+ if not DashTmr.IsDone() {
130
+ MoveInDirectionContinuouslyFacing(dir, 2800)
131
+ } else {
132
+ MoveInDirectionContinuouslyFacing(dir, 900)
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ - IMPORTANT: `#class player` is auto-attached to every player. Never declare a player
139
+ `object` in the scene, and never spawn one.
140
+
141
+ ## 5. Spawning and despawning at runtime
142
+
143
+ Declare a template with `saved`, then spawn instances from code:
144
+
145
+ ```text
146
+ saved coin {
147
+ display coin : Coin { model = "cylinder" size = (60, 60, 12) color = (1, 0.85, 0.2) }
148
+ }
149
+ ```
150
+
151
+ - `SpawnObject(SavedObject.coin, Position(x, y, z))` -> returns the new Object.
152
+ - `Destroy()` removes the calling object (or `other.Destroy()`).
153
+ - Census by tag: `GetAllObjectsWithTag(Tag.Coin).Count()`, `NearestObjectWithTag(Tag.X)`,
154
+ `DestroyObjectsWithTag(Tag.X)`.
155
+ - A `saved` clause must contain exactly one root object (or one root group).
156
+
157
+ ## 6. Collision and proximity interactions
158
+
159
+ Two robust patterns (see the frame model in the docs for exact timing):
160
+
161
+ 1. Proximity poll (best for pickups/damage auras; works with `display` objects too):
162
+
163
+ ```text
164
+ forever {
165
+ var p = NearestPlayer()
166
+ if p.Exists() and SurfaceGapTo(p) < 70 {
167
+ p.SpawnFloatingText("+1", Vector(1, 0.9, 0.2))
168
+ Destroy()
169
+ }
170
+ }
171
+ ```
172
+
173
+ 2. Contact events on colliding objects (`object`/`physics`, NOT `display`):
174
+
175
+ ```text
176
+ event Touched {
177
+ if Event.TouchedObject.IsPlayer() { ... }
178
+ }
179
+ ```
180
+
181
+ Regions fire `event EnterRegion` / `ExitRegion` with `Event.TouchedObject` — use a
182
+ `region` for goal areas, kill floors below the map, checkpoints.
183
+
184
+ ## 7. Score and UI text
185
+
186
+ Declare a screen canvas in the world description, then update labels from code:
187
+
188
+ ```text
189
+ screenCanvas HUD {
190
+ textLabel ScoreText { offset = (20, 20) text = "Score: 0" color = (1, 1, 0.85) }
191
+ textLabel MidText { align = (center, center) text = "" color = (1, 1, 0.3) }
192
+ }
193
+ ```
194
+
195
+ - UI base units assume a 1600x900 window and scale with resize.
196
+ - From code: `ResolveObjectByNameOnce("ScoreText").SetText("Score: " + Score)` — resolve
197
+ once into a var outside the `forever` loop, then call `SetText` on it.
198
+ - A centered label with empty text doubles as the win/lose banner.
199
+ - For panels/menus prefer auto-layout containers (`vStack`/`hStack`/`grid`); every direct
200
+ child of a stack needs an explicit `size`. See the `<ui>` docs section.
201
+
202
+ ## 8. Win / lose / restart
203
+
204
+ The manager pattern (used by all three examples):
205
+
206
+ - Keep `var GameOver = false` (or `RoundOver`) on the tagged manager.
207
+ - Losing conditions write it cross-object: `NearestObjectWithTag(Tag.Manager).GameOver = true`.
208
+ - The manager's `forever` loop shows the banner and freezes scoring when set.
209
+ - `event KeyboardDown[R] { RestartGame() }` reloads the whole world (fresh state).
210
+
211
+ ## 9. Procedural textures and meshes (quickstarts)
212
+
213
+ `#texture` defines a material usable as `material = "Name"` on any object (verbatim
214
+ docs example; see `<texture_dsl_examples>` in the docs for concrete/wood/metal/brick):
215
+
216
+ ```text
217
+ #texture StylizedGrass
218
+ tile_m = 3.0;
219
+ wrap = repeat;
220
+ let n = noise.fbm(uv * 4.0, octaves: 4, seed: 5, period: 4);
221
+ out.color = ramp(n, [0.0: vec3(0.337, 0.376, 0.165), 1.0: vec3(0.408, 0.459, 0.212)]);
222
+ out.orm = orm(occlusion: 0.8, roughness: 0.55, metallic: 0.0);
223
+ #end
224
+ object Ground { model = "cube" bounds_min = (-2000, -2000, -100) bounds_max = (2000, 2000, 0) material = "StylizedGrass" }
225
+ ```
226
+
227
+ `#mesh` defines a static model usable as `model = "mesh.Name"`:
228
+
229
+ ```text
230
+ #mesh Rock
231
+ let base = scale(sphere(r: 90, segments: 10, material: "Stone"), (1.4, 0.85, 0.55));
232
+ out.geo = displace(base, amount: 18, seed: 4);
233
+ #end
234
+ object RockA { model = "mesh.Rock" position = (300, 400, 0) }
235
+ ```
236
+
237
+ Mesh DSL statements end with `;` and use `let`; pair material slots with a flat
238
+ `#texture Stone color=#8B8B84` for quick color-blocking. Full generator list is in the
239
+ `<mesh_dsl_usage>` docs section.
240
+
241
+ ## 10. Footguns (each one has burned an AI before)
242
+
243
+ - Colors are 0-1 floats. `color = (255, 200, 50)` renders WHITE, silently.
244
+ - Everything must be procedural. `Ori.model.*` / market URLs hard-error in this build.
245
+ - `display` objects never get `Touched`/`ClickThisObject` events and don't collide.
246
+ Use the proximity-poll pattern, or use `object`/`physics`/`region` kinds.
247
+ - One continuous move per object per frame; the last call wins. Compose speeds yourself
248
+ (see the dash pattern) instead of calling `Move*` twice.
249
+ - `None` is only valid for Object variables; always guard with `.Exists()` before use.
250
+ - `KeyboardDownByThisPlayer` / `ActionDownByThisPlayer` / `IsKeyPressedByThisPlayer` /
251
+ `GetMoveVectorCamera` belong in `#class player` only; plain `KeyboardDown[..]` fires
252
+ for ANY player and belongs in manager-style classes.
253
+ - `for i in 0..8` excludes 8. Statements can share a line with `;`.
254
+ - Don't declare or spawn player objects; `#class player` auto-attaches to each player.
255
+ - Class-level `var T : Timer` declares a timer field; local `var t = Timer()` also works.
256
+ New timers start Done — call `StartCountDown(s)` to arm.
257
+ - Don't rely on cross-object `OnSpawned` ordering; it is unspecified.
258
+
259
+ ## 11. Complete example games (engine-tested)
260
+
261
+ Each example below ships as a `.weave` file in `examples/` (same folder/URL as this
262
+ file), is compiled and run by the engine's native test suite on every build, and is
263
+ kept under ~100 lines. To play one: replace the contents of the `#oriverse-weave`
264
+ script tag in `game.html` with the example source.
265
+
266
+ ### 11a. Coin Collector (`examples/coin_collector.weave`)
267
+
268
+ Collect 10 spinning coins; HUD counter; win banner + R to restart. Demonstrates: saved
269
+ objects + runtime spawning, tags, proximity pickup, default player controller.
270
+
271
+ ```text
272
+ // Coin Collector — walk into the 10 spinning coins to collect them all. Press R to replay.
273
+ // Demonstrates: saved objects + runtime spawning, tags, proximity pickup, HUD score text,
274
+ // win + restart flow. Movement uses the engine's default player controller (no #class player).
275
+
276
+ #class Coin
277
+ event OnPreSpawn {
278
+ AddTag(Tag.Coin)
279
+ }
280
+ event OnSpawned {
281
+ forever {
282
+ RotateLeftInstantly(4)
283
+ var p = NearestPlayer()
284
+ if p.Exists() and SurfaceGapTo(p) < 70 {
285
+ p.SpawnFloatingText("+1", Vector(1, 0.9, 0.2))
286
+ Destroy()
287
+ }
288
+ }
289
+ }
290
+ #end
291
+
292
+ #class GameManager
293
+ var TotalCoins = 10
294
+ var SeenCoins = false
295
+ event OnSpawned {
296
+ for i in 0..TotalCoins {
297
+ var pos = Position(RandomNumber(-1400, 1400), RandomNumber(-1400, 1400), 80)
298
+ SpawnObject(SavedObject.coin, pos)
299
+ }
300
+ var scoreLabel = ResolveObjectByNameOnce("ScoreText")
301
+ var midLabel = ResolveObjectByNameOnce("MidText")
302
+ forever {
303
+ // Spawned coins register their tag on the next tick, so wait until the census
304
+ // has seen at least one coin before treating "0 left" as a win.
305
+ var left = GetAllObjectsWithTag(Tag.Coin).Count()
306
+ var collected = TotalCoins - left
307
+ scoreLabel.SetText("Coins: " + collected + " / " + TotalCoins)
308
+ if left == 0 and collected == TotalCoins and SeenCoins {
309
+ midLabel.SetText("YOU WIN! Press R to play again")
310
+ } else {
311
+ midLabel.SetText("")
312
+ }
313
+ if left > 0 {
314
+ SeenCoins = true
315
+ }
316
+ }
317
+ }
318
+ event KeyboardDown[R] {
319
+ RestartGame()
320
+ }
321
+ #end
322
+
323
+ saved coin {
324
+ display coin : Coin { model = "cylinder" size = (60, 60, 12) rotation = (90, 0, 0) color = (1, 0.85, 0.2) }
325
+ }
326
+
327
+ object Ground {
328
+ model = "cube"
329
+ position = (0, 0, -50)
330
+ size = (4000, 4000, 100)
331
+ color = (0.32, 0.5, 0.34)
332
+ }
333
+ object PillarA { position = (400, 400, 150) size = (100, 100, 300) color = (0.62, 0.58, 0.52) }
334
+ object PillarB { position = (-600, 300, 150) size = (100, 100, 300) color = (0.62, 0.58, 0.52) }
335
+ object PillarC { position = (200, -700, 150) size = (100, 100, 300) color = (0.62, 0.58, 0.52) }
336
+ display manager : GameManager { visible = false }
337
+
338
+ screenCanvas HUD {
339
+ textLabel ScoreText { offset = (20, 20) text = "Coins: 0 / 10" color = (1, 1, 0.85) }
340
+ textLabel MidText { align = (center, center) text = "" color = (1, 1, 0.3) }
341
+ }
342
+ ```
343
+
344
+ ### 11b. Dodge the Falling Blocks (`examples/dodge_blocks.weave`)
345
+
346
+ Red physics cubes rain down; touching one ends the run; survival-seconds score.
347
+ Demonstrates: timed spawner, dynamic physics objects, cross-object state writes,
348
+ lose + restart flow.
349
+
350
+ ```text
351
+ // Dodge the Falling Blocks — red cubes rain down; touching one ends the run. Press R to retry.
352
+ // Demonstrates: a hidden manager object, timed spawning with Timer, dynamic physics objects,
353
+ // cross-object variable writes (block -> manager), survival-seconds score, lose + restart flow.
354
+ // Movement uses the engine's default player controller (no #class player).
355
+
356
+ #class FallingBlock
357
+ var LifeTmr : Timer
358
+ event OnSpawned {
359
+ LifeTmr.StartCountDown(8) // despawn landed blocks so the arena stays playable
360
+ forever {
361
+ if LifeTmr.IsDone() {
362
+ Destroy()
363
+ }
364
+ var p = NearestPlayer()
365
+ if p.Exists() and SurfaceGapTo(p) < 5 {
366
+ NearestObjectWithTag(Tag.Manager).GameOver = true
367
+ }
368
+ }
369
+ }
370
+ #end
371
+
372
+ #class GameManager
373
+ var GameOver = false
374
+ var Score = 0
375
+ event OnPreSpawn {
376
+ AddTag(Tag.Manager)
377
+ }
378
+ event OnSpawned {
379
+ var secondTmr = Timer()
380
+ var dropTmr = Timer()
381
+ var scoreLabel = ResolveObjectByNameOnce("ScoreText")
382
+ var midLabel = ResolveObjectByNameOnce("MidText")
383
+ forever {
384
+ if GameOver {
385
+ midLabel.SetText("GAME OVER — survived " + Score + " s. Press R to retry")
386
+ continue
387
+ }
388
+ if secondTmr.IsDone() {
389
+ secondTmr.StartCountDown(1)
390
+ Score += 1
391
+ scoreLabel.SetText("Survived: " + Score + " s")
392
+ }
393
+ if dropTmr.IsDone() {
394
+ dropTmr.StartCountDown(0.35)
395
+ var pos = Position(RandomNumber(-900, 900), RandomNumber(-900, 900), 1300)
396
+ SpawnObject(SavedObject.block, pos)
397
+ }
398
+ }
399
+ }
400
+ event KeyboardDown[R] {
401
+ RestartGame()
402
+ }
403
+ #end
404
+
405
+ saved block {
406
+ physics block : FallingBlock { model = "cube" size = (120, 120, 120) color = (0.85, 0.25, 0.2) }
407
+ }
408
+
409
+ object Ground {
410
+ model = "cube"
411
+ position = (0, 0, -50)
412
+ size = (2200, 2200, 100)
413
+ color = (0.4, 0.42, 0.5)
414
+ }
415
+ display manager : GameManager { visible = false }
416
+
417
+ screenCanvas HUD {
418
+ textLabel ScoreText { offset = (20, 20) text = "Survived: 0 s" color = (1, 1, 1) }
419
+ textLabel MidText { align = (center, center) text = "" color = (1, 0.45, 0.35) }
420
+ }
421
+ ```
422
+
423
+ ### 11c. Target Range (`examples/target_range.weave`)
424
+
425
+ Click the floating spheres through the crosshair; 30-second round. Demonstrates: custom
426
+ `#class player` (movement + jump + shooting), crosshair ray queries, user-defined funcs
427
+ called cross-object, TeleportTo respawn, countdown timer.
428
+
429
+ ```text
430
+ // Target Range — aim at the floating spheres and left-click to score. 30-second round, R restarts.
431
+ // Demonstrates: a custom #class player (movement + jump + click-to-shoot with the crosshair ray),
432
+ // user-defined funcs called cross-object, TeleportTo respawn, countdown round timer, HUD score.
433
+
434
+ #class player
435
+ event OnSpawned {
436
+ forever {
437
+ var dir = GetMoveVectorCamera()
438
+ MoveInDirectionContinuouslyFacing(dir, 900)
439
+ }
440
+ }
441
+ event KeyboardDownByThisPlayer[Space] {
442
+ if IsOnGround() {
443
+ Jump(1000)
444
+ }
445
+ }
446
+ event ActionDownByThisPlayer[Primary] {
447
+ var t = GetMouseTargetObjectWithTag(Tag.Target)
448
+ if t.Exists() and not NearestObjectWithTag(Tag.Manager).RoundOver {
449
+ t.Pop()
450
+ }
451
+ }
452
+ #end
453
+
454
+ #class Target
455
+ func Pop() {
456
+ SpawnFloatingText("+1", Vector(0.3, 1, 0.4))
457
+ NearestObjectWithTag(Tag.Manager).Hits += 1
458
+ TeleportTo(Position(RandomNumber(-1200, 1200), RandomNumber(500, 1600), RandomNumber(100, 500)))
459
+ }
460
+ event OnPreSpawn {
461
+ AddTag(Tag.Target)
462
+ }
463
+ #end
464
+
465
+ #class GameManager
466
+ var Hits = 0
467
+ var TimeLeft = 30
468
+ var RoundOver = false
469
+ event OnPreSpawn {
470
+ AddTag(Tag.Manager)
471
+ }
472
+ event OnSpawned {
473
+ var secondTmr = Timer()
474
+ secondTmr.StartCountDown(1)
475
+ var scoreLabel = ResolveObjectByNameOnce("ScoreText")
476
+ var midLabel = ResolveObjectByNameOnce("MidText")
477
+ forever {
478
+ scoreLabel.SetText("Hits: " + Hits + " Time: " + TimeLeft)
479
+ if RoundOver {
480
+ continue
481
+ }
482
+ if secondTmr.IsDone() {
483
+ secondTmr.StartCountDown(1)
484
+ TimeLeft -= 1
485
+ if TimeLeft <= 0 {
486
+ RoundOver = true
487
+ midLabel.SetText("TIME! Final score: " + Hits + " — press R to retry")
488
+ }
489
+ }
490
+ }
491
+ }
492
+ event KeyboardDown[R] {
493
+ RestartGame()
494
+ }
495
+ #end
496
+
497
+ object Ground {
498
+ model = "cube"
499
+ position = (0, 0, -50)
500
+ size = (4000, 4000, 100)
501
+ color = (0.45, 0.4, 0.35)
502
+ }
503
+ object TargetA : Target { model = "sphere" position = (-600, 900, 250) size = 90 color = (1, 0.3, 0.3) }
504
+ object TargetB : Target { model = "sphere" position = (0, 1200, 350) size = 90 color = (1, 0.3, 0.3) }
505
+ object TargetC : Target { model = "sphere" position = (700, 800, 200) size = 90 color = (1, 0.3, 0.3) }
506
+ display manager : GameManager { visible = false }
507
+
508
+ screenCanvas HUD {
509
+ textLabel ScoreText { offset = (20, 20) text = "Hits: 0 Time: 30" color = (1, 1, 0.85) }
510
+ textLabel MidText { align = (center, center) text = "" color = (0.4, 1, 0.5) }
511
+ }
512
+ ```
@@ -0,0 +1,70 @@
1
+ // Coin Collector — walk into the 10 spinning coins to collect them all. Press R to replay.
2
+ // Demonstrates: saved objects + runtime spawning, tags, proximity pickup, HUD score text,
3
+ // win + restart flow. Movement uses the engine's default player controller (no #class player).
4
+
5
+ #class Coin
6
+ event OnPreSpawn {
7
+ AddTag(Tag.Coin)
8
+ }
9
+ event OnSpawned {
10
+ forever {
11
+ RotateLeftInstantly(4)
12
+ var p = NearestPlayer()
13
+ if p.Exists() and SurfaceGapTo(p) < 70 {
14
+ p.SpawnFloatingText("+1", Vector(1, 0.9, 0.2))
15
+ Destroy()
16
+ }
17
+ }
18
+ }
19
+ #end
20
+
21
+ #class GameManager
22
+ var TotalCoins = 10
23
+ var SeenCoins = false
24
+ event OnSpawned {
25
+ for i in 0..TotalCoins {
26
+ var pos = Position(RandomNumber(-1400, 1400), RandomNumber(-1400, 1400), 80)
27
+ SpawnObject(SavedObject.coin, pos)
28
+ }
29
+ var scoreLabel = ResolveObjectByNameOnce("ScoreText")
30
+ var midLabel = ResolveObjectByNameOnce("MidText")
31
+ forever {
32
+ // Spawned coins register their tag on the next tick, so wait until the census
33
+ // has seen at least one coin before treating "0 left" as a win.
34
+ var left = GetAllObjectsWithTag(Tag.Coin).Count()
35
+ var collected = TotalCoins - left
36
+ scoreLabel.SetText("Coins: " + collected + " / " + TotalCoins)
37
+ if left == 0 and collected == TotalCoins and SeenCoins {
38
+ midLabel.SetText("YOU WIN! Press R to play again")
39
+ } else {
40
+ midLabel.SetText("")
41
+ }
42
+ if left > 0 {
43
+ SeenCoins = true
44
+ }
45
+ }
46
+ }
47
+ event KeyboardDown[R] {
48
+ RestartGame()
49
+ }
50
+ #end
51
+
52
+ saved coin {
53
+ display coin : Coin { model = "cylinder" size = (60, 60, 12) rotation = (90, 0, 0) color = (1, 0.85, 0.2) }
54
+ }
55
+
56
+ object Ground {
57
+ model = "cube"
58
+ position = (0, 0, -50)
59
+ size = (4000, 4000, 100)
60
+ color = (0.32, 0.5, 0.34)
61
+ }
62
+ object PillarA { position = (400, 400, 150) size = (100, 100, 300) color = (0.62, 0.58, 0.52) }
63
+ object PillarB { position = (-600, 300, 150) size = (100, 100, 300) color = (0.62, 0.58, 0.52) }
64
+ object PillarC { position = (200, -700, 150) size = (100, 100, 300) color = (0.62, 0.58, 0.52) }
65
+ display manager : GameManager { visible = false }
66
+
67
+ screenCanvas HUD {
68
+ textLabel ScoreText { offset = (20, 20) text = "Coins: 0 / 10" color = (1, 1, 0.85) }
69
+ textLabel MidText { align = (center, center) text = "" color = (1, 1, 0.3) }
70
+ }
@@ -0,0 +1,70 @@
1
+ // Dodge the Falling Blocks — red cubes rain down; touching one ends the run. Press R to retry.
2
+ // Demonstrates: a hidden manager object, timed spawning with Timer, dynamic physics objects,
3
+ // cross-object variable writes (block -> manager), survival-seconds score, lose + restart flow.
4
+ // Movement uses the engine's default player controller (no #class player).
5
+
6
+ #class FallingBlock
7
+ var LifeTmr : Timer
8
+ event OnSpawned {
9
+ LifeTmr.StartCountDown(8) // despawn landed blocks so the arena stays playable
10
+ forever {
11
+ if LifeTmr.IsDone() {
12
+ Destroy()
13
+ }
14
+ var p = NearestPlayer()
15
+ if p.Exists() and SurfaceGapTo(p) < 5 {
16
+ NearestObjectWithTag(Tag.Manager).GameOver = true
17
+ }
18
+ }
19
+ }
20
+ #end
21
+
22
+ #class GameManager
23
+ var GameOver = false
24
+ var Score = 0
25
+ event OnPreSpawn {
26
+ AddTag(Tag.Manager)
27
+ }
28
+ event OnSpawned {
29
+ var secondTmr = Timer()
30
+ var dropTmr = Timer()
31
+ var scoreLabel = ResolveObjectByNameOnce("ScoreText")
32
+ var midLabel = ResolveObjectByNameOnce("MidText")
33
+ forever {
34
+ if GameOver {
35
+ midLabel.SetText("GAME OVER — survived " + Score + " s. Press R to retry")
36
+ continue
37
+ }
38
+ if secondTmr.IsDone() {
39
+ secondTmr.StartCountDown(1)
40
+ Score += 1
41
+ scoreLabel.SetText("Survived: " + Score + " s")
42
+ }
43
+ if dropTmr.IsDone() {
44
+ dropTmr.StartCountDown(0.35)
45
+ var pos = Position(RandomNumber(-900, 900), RandomNumber(-900, 900), 1300)
46
+ SpawnObject(SavedObject.block, pos)
47
+ }
48
+ }
49
+ }
50
+ event KeyboardDown[R] {
51
+ RestartGame()
52
+ }
53
+ #end
54
+
55
+ saved block {
56
+ physics block : FallingBlock { model = "cube" size = (120, 120, 120) color = (0.85, 0.25, 0.2) }
57
+ }
58
+
59
+ object Ground {
60
+ model = "cube"
61
+ position = (0, 0, -50)
62
+ size = (2200, 2200, 100)
63
+ color = (0.4, 0.42, 0.5)
64
+ }
65
+ display manager : GameManager { visible = false }
66
+
67
+ screenCanvas HUD {
68
+ textLabel ScoreText { offset = (20, 20) text = "Survived: 0 s" color = (1, 1, 1) }
69
+ textLabel MidText { align = (center, center) text = "" color = (1, 0.45, 0.35) }
70
+ }