plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
53de7e3
— Peter John
2026-11-03T21:43:41+05:30
Pure scene graph on enums: Node record + Kind enum, Custom(actor: Actor)
- docs/plans/2026-11-03-pure-scene-enums.incidents.jsonl +55 -0
- docs/plans/2026-11-03-pure-scene-enums.md +277 -0
- docs/plans/2026-11-03-pure-scene-enums.progress.md +52 -0
- docs/plans/2026-11-03-pure-scene-enums.report.json +418 -0
- docs/plans/2026-11-03-pure-scene-enums.report.md +59 -0
- docs/plans/2026-11-03-state-view-parity.review.md +6 -0
- plum-checker/src/lib.rs +24 -1
- plum-checker/src/ownership.rs +154 -6
- plum-checker/tests/fixtures/custom_actor_probe2.plum +58 -0
- plum-checker/tests/fixtures/g3_generic_funstar.plum +34 -0
- plum-checker/tests/optional_unpinned_tests.rs +1 -1
- plum-checker/tests/ownership_moves_test.rs +67 -0
- plum-checker/tests/ownership_trait_args_test.rs +73 -0
- plum-checker/tests/variant_pattern_tests.rs +47 -0
- plum-core/src/union_lower.rs +3 -2
- plum-core/tests/forwarder_own_test.rs +51 -0
- plum-examples/game_pure_scene.plum +129 -145
- plum-game/web-test/fixtures/stress_pure_scene.plum +33 -45
- plum-std/game/Actions.plum +63 -57
- plum-std/game/PureScene.plum +212 -161
- plum-std/game/SceneInput.plum +23 -30
- plum-std/game/SceneTree.plum +94 -89
- plum-wasm-codegen/src/lib.rs +12 -0
- plum-wasm-codegen/tests/empty_record_embed_e2e_test.rs +76 -0
docs/plans/2026-11-03-pure-scene-enums.incidents.jsonl
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; cargo test -p plum-checker 21 | grep -E FAILED|failed; | grep -v 0"}
|
|
2
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; cargo build --quiet 21|tail -3; ./target/debug/plum test .superlite/re"}
|
|
3
|
+
{"file": "-", "line": 0, "context": "exit code 1: For more information, try '--help'.", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "20:43:44"}
|
|
4
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; cargo test -p plum-checker --test ownership_trait_args_test 21 | tail "}
|
|
5
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; D=/private/tmp/claude-501/-Users-pyrossh-Code-plum/7471cdc3-437e-41b8-9"}
|
|
6
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; D=/private/tmp/claude-501/-Users-pyrossh-Code-plum/7471cdc3-437e-41b8-9"}
|
|
7
|
+
{"file": "plum-std/game/SceneTree.plum", "line": 242, "context": "T2: bare variant-name pattern (MoveBy =>) on an inline-variant enum with a payload compiles but never matches; should match or be a compile error", "agent": "coordinator", "time": "20:44:46"}
|
|
8
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; D=/private/tmp/claude-501/-Users-pyrossh-Code-plum/7471cdc3-437e-41b8-9"}
|
|
9
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; D=/private/tmp/claude-501/-Users-pyrossh-Code-plum/7471cdc3-437e-41b8-9"}
|
|
10
|
+
{"kind":"rule-block","agent":"coordinator","tool":"Bash","target":"> MEMORY.md"}
|
|
11
|
+
{"file": "-", "line": 0, "context": "exit code 1: For more information, try '--help'.", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "20:51:19"}
|
|
12
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; cargo test -p plum-checker --test ownership_trait_args_test 21 | tail "}
|
|
13
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T3", "agent": "pt_review", "kind": "verified", "time": "20:56:59"}
|
|
14
|
+
{"file": "plum-core/", "line": 0, "context": "T3: change outside Owns blocked by checkpoint; nothing saved: 1 files under plum-core/, first 1: plum-core/src/union_lower.rs", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "20:57:29"}
|
|
15
|
+
{"file": "plum-core/", "line": 0, "context": "T3: change outside Owns blocked by checkpoint; nothing saved: 2 files under plum-core/, first 2: plum-core/src/union_lower.rs, plum-core/tests/forwarder_own_test.rs", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "20:58:28"}
|
|
16
|
+
{"file": "plum-core/", "line": 0, "context": "T4: change outside Owns blocked by checkpoint; nothing saved: 2 files under plum-core/, first 2: plum-core/src/union_lower.rs, plum-core/tests/forwarder_own_test.rs", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "20:58:28"}
|
|
17
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; cargo test -p plum-checker -p plum-core 21 | grep -E ^test result|FAI"}
|
|
18
|
+
{"file": "docs/", "line": 0, "context": "T1: change outside Owns blocked by checkpoint; nothing saved: 1 files under docs/, first 1: docs/plans/2026-11-03-state-view-parity.review.md", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "21:06:32"}
|
|
19
|
+
{"file": "plum-core/", "line": 0, "context": "T1: change outside Owns blocked by checkpoint; nothing saved: 2 files under plum-core/, first 2: plum-core/src/union_lower.rs, plum-core/tests/forwarder_own_test.rs", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "21:06:32"}
|
|
20
|
+
{"file": "docs/", "line": 0, "context": "T1: change outside Owns blocked by checkpoint; nothing saved: 1 files under docs/, first 1: docs/plans/2026-11-03-state-view-parity.review.md", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "21:06:35"}
|
|
21
|
+
{"file": "plum-core/", "line": 0, "context": "T1: change outside Owns blocked by checkpoint; nothing saved: 2 files under plum-core/, first 2: plum-core/src/union_lower.rs, plum-core/tests/forwarder_own_test.rs", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "21:06:35"}
|
|
22
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; sed -i 's/ cx.tys.insert(n, t);/ "}
|
|
23
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; sed -i '588s/cx.tys.insert(n, t);/let _ = (n, t);/' plum-checker/src/ow"}
|
|
24
|
+
{"file": "docs/", "line": 0, "context": "T1: change outside Owns blocked by checkpoint; nothing saved: 1 files under docs/, first 1: docs/plans/2026-11-03-state-view-parity.review.md", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "21:19:58"}
|
|
25
|
+
{"file": "docs/", "line": 0, "context": "T8: change outside Owns blocked by checkpoint; nothing saved: 1 files under docs/, first 1: docs/plans/2026-11-03-state-view-parity.review.md", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "21:19:58"}
|
|
26
|
+
{"file": "docs/", "line": 0, "context": "T3: change outside Owns blocked by checkpoint; nothing saved: 1 files under docs/, first 1: docs/plans/2026-11-03-state-view-parity.review.md", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "21:19:58"}
|
|
27
|
+
{"file": "docs/", "line": 0, "context": "T4: change outside Owns blocked by checkpoint; nothing saved: 1 files under docs/, first 1: docs/plans/2026-11-03-state-view-parity.review.md", "agent": "checkpoint", "kind": "exception", "resolution": "open", "time": "21:19:58"}
|
|
28
|
+
{"file": "", "line": 0, "context": "verified: seam T1↔T3", "agent": "pt_review", "kind": "verified", "time": "21:20:05"}
|
|
29
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 128, "context": "seam T3↔T4: T3 Produces: PureScene.childrenOf: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:20:05"}
|
|
30
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 128, "context": "seam T3↔T4: T3 Produces: PureScene.custom: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:20:05"}
|
|
31
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 128, "context": "seam T3↔T4: T3 Produces: PureScene.drawTree: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:20:05"}
|
|
32
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 128, "context": "seam T3↔T4: T3 Produces: PureScene.localMatrix: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:20:05"}
|
|
33
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 128, "context": "seam T3↔T4: T3 Produces: PureScene.group: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:20:05"}
|
|
34
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 128, "context": "seam T3↔T4: T3 Produces: PureScene.sprite: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:20:05"}
|
|
35
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 128, "context": "seam T3↔T4: T3 Produces: PureScene.label: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:20:05"}
|
|
36
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 128, "context": "seam T3↔T4: T3 Produces: PureScene.button: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:20:05"}
|
|
37
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 128, "context": "seam T3↔T4: T3 Produces: PureScene.custom: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:20:05"}
|
|
38
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 128, "context": "seam T3↔T4: T3 Produces: PureScene.hit: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:20:05"}
|
|
39
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 152, "context": "seam T4↔T6: T4 Produces: SceneTree.findHit: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:21:40"}
|
|
40
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 152, "context": "seam T4↔T6: T4 Produces: SceneTree.idAt: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:21:40"}
|
|
41
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 152, "context": "seam T4↔T6: T4 Produces: SceneTree.findPath: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:21:40"}
|
|
42
|
+
{"file": ".superlite/repros/empty_record_embed.plum", "line": 6, "context": "field-less record embedding trait: Panel() -> codegen unknown class 'Panel' (Panel__new). Workaround: dummy field", "agent": "T5", "kind": "workaround", "resolution": "dummy field in Panel", "confidence": "high", "time": "21:22:09"}
|
|
43
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 152, "context": "seam T4↔T5: T4 Produces: SceneTree.findHit: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:22:46"}
|
|
44
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 152, "context": "seam T4↔T5: T4 Produces: SceneTree.idAt: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:22:46"}
|
|
45
|
+
{"file": "docs/plans/2026-11-03-pure-scene-enums.md", "line": 152, "context": "seam T4↔T5: T4 Produces: SceneTree.findPath: no module found (file missing)", "agent": "pt_review", "kind": "exception", "confidence": "high", "resolution": "open", "time": "21:22:46"}
|
|
46
|
+
{"file": "-", "line": 0, "context": "generic_funstar_wired: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "21:29:07"}
|
|
47
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum/plum-checker/tests; sed -i '' 's# /../.superlite/repros/g3_generic_funst"}
|
|
48
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; grep -n fixtures plum-checker/tests/*.rs | head; cargo test -q -p plu"}
|
|
49
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; touch plum-checker/src/lib.rs; cargo build --quiet 21 | tail -5; cargo"}
|
|
50
|
+
{"kind":"rule-block","agent":"subagent","tool":"Bash","target":"full suite: cd /Users/pyrossh/Code/plum; cargo build -q -p plum-cli 21 | tail -5; ./target/debug/plum test plum"}
|
|
51
|
+
{"file": "plum-checker/src/ownership.rs", "line": 1, "context": "residual after F-FR-3: an alias returned from a call (c := first(n)) into an own value is not tracked as a view; using c after moving n is not rejected; needs lifetime inference (candidate for the su…", "agent": "coordinator", "time": "21:36:21"}
|
|
52
|
+
{"file": "-", "line": 0, "context": "filter_form_no_closure: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "21:41:05"}
|
|
53
|
+
{"file": "-", "line": 0, "context": "one_copy_per_distinct_type: failed", "agent": "run_tests", "kind": "test-failure", "resolution": "open", "time": "21:41:05"}
|
|
54
|
+
{"kind":"rule-block","agent":"coordinator","tool":"Bash","target":"rm target/master-check"}
|
|
55
|
+
{"kind":"rule-block","agent":"coordinator","tool":"Bash","target":"rm /Users/pyrossh/Code/plum/target/master-check"}
|
docs/plans/2026-11-03-pure-scene-enums.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Pure scene graph on enums
|
|
2
|
+
|
|
3
|
+
**Review plan:** docs/plans/2026-11-03-pure-scene-enums.review.md
|
|
4
|
+
**Goal:** move std/game PureScene, SceneTree, SceneInput and Actions, plus `game_pure_scene.plum` and its stress fixture, from a 14-field `own trait Actor` and record unions to `own record Node` + `enum Kind`, with `Custom(actor: Actor)` (behaviour-only trait, like Rust's `Box<dyn Actor>`) for game kinds; fix the ownership gap the probe found.
|
|
5
|
+
**Architecture:** T1 checker ownership fix (borrowed arg to a trait method's borrowed param). T2 Actions: `record Action(elapsed, current, kind: ActionKind)`. T3 PureScene: Node/Kind/Actor, builders, draw, hit, Button input. T4 SceneTree + SceneInput over Node. T5 game, T6 stress fixture, T7 README + host checks + headed benchmark.
|
|
6
|
+
**Tech stack:** Rust workspace (plum-checker), Plum std (plum-std/game), plum-game host tests, playwright-rs web-test.
|
|
7
|
+
**Spec:** docs/plans/2026-11-03-pure-scene-enums.review.md
|
|
8
|
+
**Detail:** contracts
|
|
9
|
+
**Target-min:** 25
|
|
10
|
+
**Budget-usd:** 40
|
|
11
|
+
|
|
12
|
+
Global constraints:
|
|
13
|
+
- Language: Plum (std/examples) and Rust (checker). Fast Plum tests: `cargo build --quiet && ./target/debug/plum test <file.plum>` from the repo root; `touch` edited .rs files before building (cargo can skip recompiles). Rust: `cargo test -p plum-checker --test <file>`.
|
|
14
|
+
- Baselines: `./target/debug/plum test plum-std plum-examples` 632 passed; host: `cargo test --manifest-path plum-game/Cargo.toml --test host_test` 17 passed.
|
|
15
|
+
- Shapes (exact):
|
|
16
|
+
- `record Transform(x: Float, y: Float, width: Float, height: Float, originX: Float, originY: Float, scaleX: Float, scaleY: Float, rotation: Float)` with `fun localMatrix(self) -> Affine` and `fun contains(self, p: Vec2) -> Bool` (box test in local coords).
|
|
17
|
+
- `own record Node(Transform(x, y, width, height, originX, originY, scaleX, scaleY, rotation), id: Int, color: Color, visible: Bool, touchable: Touchable, actions: Slice[Action], kind: Kind)` (embedding: `n.x`, `n.localMatrix()` work directly).
|
|
18
|
+
- Child lists and test trees are built with variadic init, `Slice(a, b, c)` / `List(a, b)`, never `Slice[T]()` + push chains; there is no `[a, b]` literal.
|
|
19
|
+
- `enum Kind = | Group(children: Slice[Node]) | Sprite(tex: Int, srcX: Float, srcY: Float, srcW: Float, srcH: Float) | Label(text: Str, size: Float) | Button(text: Str, upTex: Int, downTex: Int, pressed: Bool, over: Bool) | Custom(actor: Actor)`
|
|
20
|
+
- `trait Actor =` no fields; `fun act(self, own n: Node, dt: Float) -> Node = n` and `fun draw(self, n: Node, m: Affine, tint: Color) = return` (defaults).
|
|
21
|
+
- `record Action(elapsed: Float, current: Action?, kind: ActionKind)`; `enum ActionKind = | MoveBy(dx: Float, dy: Float, duration: Float) | RotateBy(radians: Float, duration: Float) | Delay(duration: Float) | Sequence(steps: Slice[Action]) | Parallel(steps: Slice[Action]) | Forever(template: Action)`.
|
|
22
|
+
- Inline-variant fields can't be assigned: `n.kind = Button(...)` rebuilds a kind; Action keeps `elapsed`/`current` on the record.
|
|
23
|
+
- Plum: `test "name"` + `assert`; an assignment in a match arm and a nested `match` each go on their own line.
|
|
24
|
+
- Plum modules can't be stubbed: a module compiling against one still being rewritten may fail transiently; retry, don't edit it.
|
|
25
|
+
- Every existing test keeps its name and asserted values; local test helpers may change.
|
|
26
|
+
- Never `plum format` a real file (rewrites in place).
|
|
27
|
+
- Out of scope: std/game/Scene, game_scene.plum, scene.plum, trait_scene.plum, traits.plum, Picture, game_state_view; generic traits.
|
|
28
|
+
- A compiler gap hit while porting: `incident add`, report BLOCKED with a minimal repro in `.superlite/repros/`; never work around silently.
|
|
29
|
+
|
|
30
|
+
## Main diagram
|
|
31
|
+
```mermaid
|
|
32
|
+
flowchart LR
|
|
33
|
+
OW["T1 ownership.rs: trait-method borrowed args"] --> PS["T3 PureScene: Node, Kind, Actor"]
|
|
34
|
+
AC["T2 Actions: Action + ActionKind"] --> PS
|
|
35
|
+
PS --> ST["T4 SceneTree + SceneInput"]
|
|
36
|
+
ST --> G["T5 game_pure_scene.plum"]
|
|
37
|
+
ST --> F["T6 stress_pure_scene fixture"]
|
|
38
|
+
G --> D["T7 README, host checks, benchmark"]
|
|
39
|
+
F --> D
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## File structure
|
|
43
|
+
Each task's `Owns` line is its file list; one responsibility per file as today.
|
|
44
|
+
|
|
45
|
+
Spec map: The new shape -> T2-T5; What you will get -> T3-T7; What will be built -> T1-T7; How it fits together -> main diagram; Decisions you can change -> defaults taken (box hit; Action record + enum); Risks -> T1 + incidents; How we will know it works -> task tests + T7; Rough time and cost -> schedule.
|
|
46
|
+
|
|
47
|
+
## Task graph
|
|
48
|
+
```mermaid
|
|
49
|
+
flowchart LR
|
|
50
|
+
T1[ownership fix]
|
|
51
|
+
T2[Actions]
|
|
52
|
+
T3[PureScene]
|
|
53
|
+
T4[SceneTree SceneInput]
|
|
54
|
+
T5[game]
|
|
55
|
+
T6[fixture]
|
|
56
|
+
T7[docs bench]
|
|
57
|
+
%% taskgraph
|
|
58
|
+
T1 --> T3
|
|
59
|
+
T3 --> T4
|
|
60
|
+
T4 --> T5
|
|
61
|
+
T4 --> T6
|
|
62
|
+
T5 --> T7
|
|
63
|
+
T6 --> T7
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Ruling: sequential - T3..T7 are Plum modules that import each other and Plum has no stubs, so each one's tests compile the previous module - cost if wrong: ~10 min.
|
|
67
|
+
|
|
68
|
+
## Predicted schedule
|
|
69
|
+
Generated by `superlite-cli plan_tools lint`.
|
|
70
|
+
|
|
71
|
+
## Checker
|
|
72
|
+
|
|
73
|
+
### T1 ownership: borrowed argument to a trait method · Complexity 4 (O-1) · Est. context 40k
|
|
74
|
+
**Owns:** plum-checker/src/ownership.rs, plum-checker/tests/ownership_trait_args_test.rs
|
|
75
|
+
**Components:** 1
|
|
76
|
+
**LOC owned:** 90
|
|
77
|
+
**Tests:** fast
|
|
78
|
+
**Model:** sonnet
|
|
79
|
+
**Est. time:** 3 min
|
|
80
|
+
**Consumes:** none
|
|
81
|
+
**Produces:** ownership check accepts a borrowed `own` value passed to a borrowed param of a method called on a trait value
|
|
82
|
+
**Review Focus:** state: the fix must not let a borrowed value reach an `own` param or a stored field.
|
|
83
|
+
**Test code (named, exact):**
|
|
84
|
+
- `trait_method_borrowed_arg_ok`: the source of `.superlite/repros/custom_actor_probe2.plum` checks with no errors (`a.score(n)` where `score(self, n: Node)` and `n` is borrowed).
|
|
85
|
+
- `trait_method_own_arg_moves`: in the same program, a fn `fun g(own n: Node, a: Actor) -> Float = { m := a.act(n, 0.1); n.x }` reports `"n" used after it was moved`.
|
|
86
|
+
- `borrowed_to_own_trait_param_rejected`: `fun h(n: Node, a: Actor) -> Node = a.act(n, 0.1)` (borrowed `n` to `own` param) reports `cannot store borrowed "n"`.
|
|
87
|
+
- `plain_fn_borrowed_arg_still_ok`: an unchanged plain-fn borrowed call still checks clean.
|
|
88
|
+
**Steps:** 1 failing tests, 2 FAIL, 3 find why a trait-method call treats a borrowed param as a store (method param modes are not looked up through the trait / its embedders) and fix, 4 PASS, 5 `cargo test -p plum-checker` green.
|
|
89
|
+
**Notes:** the error comes from the "cannot store borrowed" path near ownership.rs:799-822; param modes for a call on a trait value should come from the trait's method declaration.
|
|
90
|
+
|
|
91
|
+
### T8 trait forwarder keeps `own` (follow-up to T1, finding F-T1-1) · Complexity 3 (O-1) · Est. context 30k
|
|
92
|
+
**Owns:** plum-core/src/union_lower.rs, plum-core/tests/forwarder_own_test.rs
|
|
93
|
+
**Components:** 1
|
|
94
|
+
**LOC owned:** 40
|
|
95
|
+
**Tests:** fast
|
|
96
|
+
**Model:** sonnet
|
|
97
|
+
**Est. time:** 2 min
|
|
98
|
+
**Consumes:** none
|
|
99
|
+
**Produces:** synthesized trait forwarders copy `own` / `own self` from the trait method declaration
|
|
100
|
+
**Review Focus:** state: forwarder params carry the declared mode.
|
|
101
|
+
**Test code:** `forwarder_own_test.rs`: the lowered forwarder of `fun act(self, own n: Node, dt: Float) -> Node` has `n.isOwn == true`.
|
|
102
|
+
**Steps:** done by the F-T1-1 fixer; recorded here so its files have an owner.
|
|
103
|
+
|
|
104
|
+
### T9 field-less record embedding a trait (compiler gap found by T5) · Complexity 4 (O-1) · Est. context 40k
|
|
105
|
+
**Owns:** plum-wasm-codegen/src/lib.rs, plum-core/src/embeds.rs, plum-core/src/ctors.rs, plum-wasm-codegen/tests/empty_record_embed_e2e_test.rs, plum-examples/game_pure_scene.plum
|
|
106
|
+
**Components:** 1
|
|
107
|
+
**LOC owned:** 60
|
|
108
|
+
**Tests:** fast
|
|
109
|
+
**Model:** sonnet
|
|
110
|
+
**Est. time:** 3 min
|
|
111
|
+
**Consumes:** none
|
|
112
|
+
**Produces:** `record Panel(Actor)` (no own fields, embeds a field-less trait) constructs with `Panel()` and dispatches its trait methods
|
|
113
|
+
**Review Focus:** state: zero-field struct type in wasm-gc; trait dispatch to it.
|
|
114
|
+
**Test code:** `empty_record_embed_e2e_test.rs`: `.superlite/repros/empty_record_embed.plum` runs and passes; a `List`/`Slice` of `Actor` mixing `Panel()` and a record with fields dispatches `draw` to each.
|
|
115
|
+
**Steps:** fix "codegen: unknown class or enum variant 'Panel' (in 'Panel__new')"; then remove the `tag: Int` workaround field from `Panel` in game_pure_scene.plum (tests unchanged).
|
|
116
|
+
|
|
117
|
+
### T10 tracked test fixtures (final review F-FR-1) · Complexity 2 (O-1) · Est. context 25k
|
|
118
|
+
**Owns:** plum-checker/tests/fixtures/custom_actor_probe2.plum, plum-checker/tests/fixtures/g3_generic_funstar.plum, plum-checker/tests/optional_unpinned_tests.rs, plum-checker/tests/ownership_trait_args_test.rs
|
|
119
|
+
**Components:** 1
|
|
120
|
+
**LOC owned:** 40
|
|
121
|
+
**Tests:** fast
|
|
122
|
+
**Model:** sonnet
|
|
123
|
+
**Est. time:** 2 min
|
|
124
|
+
**Consumes:** none
|
|
125
|
+
**Produces:** checker tests no longer read git-ignored `.superlite/repros` files
|
|
126
|
+
**Review Focus:** file: fixtures tracked, paths relative to the test file.
|
|
127
|
+
**Test code:** `generic_funstar_wired` and the 7 ownership_trait_args tests pass from fixtures.
|
|
128
|
+
**Steps:** see .superlite/fixes/F-FR-1.md.
|
|
129
|
+
|
|
130
|
+
### T11 bare pattern on a payload variant is an error (final review F-FR-2) · Complexity 4 (O-1) · Est. context 45k
|
|
131
|
+
**Owns:** plum-checker/src/lib.rs, plum-core/src/union_lower.rs, plum-checker/tests/variant_pattern_tests.rs
|
|
132
|
+
**Components:** 1
|
|
133
|
+
**LOC owned:** 60
|
|
134
|
+
**Tests:** fast
|
|
135
|
+
**Model:** sonnet
|
|
136
|
+
**Est. time:** 3 min
|
|
137
|
+
**Consumes:** none
|
|
138
|
+
**Produces:** `MoveBy =>` on a variant with fields is a positioned compile error; unit variants and union-enum members unchanged
|
|
139
|
+
**Review Focus:** deep: nested patterns; union-enum narrowing still works.
|
|
140
|
+
**Test code:** `bare_payload_variant_pattern_rejected`, `bare_unit_variant_pattern_ok`.
|
|
141
|
+
**Steps:** see .superlite/fixes/F-FR-2.md.
|
|
142
|
+
|
|
143
|
+
### T12 ownership: projections after a move (final review F-FR-3) · Complexity 3 (O-1) · Est. context 35k
|
|
144
|
+
**Owns:** plum-checker/src/ownership.rs, plum-checker/tests/ownership_moves_test.rs
|
|
145
|
+
**Components:** 1
|
|
146
|
+
**LOC owned:** 40
|
|
147
|
+
**Tests:** fast
|
|
148
|
+
**Model:** sonnet
|
|
149
|
+
**Est. time:** 2 min
|
|
150
|
+
**Consumes:** none
|
|
151
|
+
**Produces:** negative tests prove no own projection survives a move; non-own reads before a move stay legal
|
|
152
|
+
**Review Focus:** state: soundness of moves.
|
|
153
|
+
**Test code:** `own_field_used_after_move_rejected`, `own_child_projection_after_move_rejected`, `non_own_value_read_before_move_ok`.
|
|
154
|
+
**Steps:** see .superlite/fixes/F-FR-3.md.
|
|
155
|
+
|
|
156
|
+
## Std modules
|
|
157
|
+
|
|
158
|
+
**Probe entry:** run=./target/debug/plum test plum-std/game
|
|
159
|
+
Package: library/evaluator; categories: state, numeric, deep.
|
|
160
|
+
|
|
161
|
+
### T2 Actions on Action + ActionKind · Complexity 5 (O-1) · Est. context 45k
|
|
162
|
+
**Owns:** plum-std/game/Actions.plum
|
|
163
|
+
**Components:** 1
|
|
164
|
+
**LOC owned:** 150
|
|
165
|
+
**Tests:** fast
|
|
166
|
+
**Model:** sonnet
|
|
167
|
+
**Est. time:** 4 min
|
|
168
|
+
**Consumes:** none
|
|
169
|
+
**Produces:** Actions.moveBy(dx: Float, dy: Float, secs: Float) -> Action
|
|
170
|
+
e.g. Actions.moveBy(10.0, 0.0, 1.0) -> Action(elapsed: 0.0, current: null, kind: MoveBy(dx: 10.0, dy: 0.0, duration: 1.0))
|
|
171
|
+
e.g. Actions.moveBy(0.0, 5.0, 0.0) -> Action(elapsed: 0.0, current: null, kind: MoveBy(dx: 0.0, dy: 5.0, duration: 0.0))
|
|
172
|
+
**Produces:** Actions.stepAll(p: Pose, acts: Slice[Action], dt: Float) -> Step
|
|
173
|
+
(rotateBy/delay/sequence/parallel/forever keep their current signatures)
|
|
174
|
+
e.g. Actions.stepAll(Pose(x: 0.0, y: 0.0, rotation: 0.0, scaleX: 1.0, scaleY: 1.0), [Actions.moveBy(10.0, 0.0, 1.0)], 0.5) -> Step(pose.x: 5.0, spare: 0.0, done: false)
|
|
175
|
+
e.g. same list stepped again with 0.6 -> pose.x 10.0 (cumulative), done: true, list now empty
|
|
176
|
+
**Review Focus:** numeric: zero-length actions finish at once (no 0/0); Forever with a zero-length template terminates; state: stepping stays in place (`step_all_no_alloc_steady_state` keeps passing).
|
|
177
|
+
**Test code:** all 13 existing tests keep names and values, including `actions_golden_values` (GoldRun rows unchanged in value; GoldRun may stay a record).
|
|
178
|
+
**Steps:** port types to the global-constraint shapes; `fresh` rebuilds an unstarted Action; Forever keeps the template in its kind and the running lap in `current`; run `plum test plum-std/game/Actions.plum`.
|
|
179
|
+
|
|
180
|
+
### T3 PureScene on Node, Kind, Actor · Complexity 6 (O-1) · Est. context 50k
|
|
181
|
+
**Owns:** plum-std/game/PureScene.plum
|
|
182
|
+
**Components:** 1
|
|
183
|
+
**LOC owned:** 180
|
|
184
|
+
**Tests:** fast
|
|
185
|
+
**Model:** sonnet
|
|
186
|
+
**Est. time:** 5 min
|
|
187
|
+
**Consumes:** Actions.moveBy(dx: Float, dy: Float, secs: Float) -> Action (T2)
|
|
188
|
+
**Produces:** PureScene.childrenOf(n: Node) -> Slice[Node]?
|
|
189
|
+
e.g. PureScene.childrenOf(PureScene.group(1, 0.0, 0.0, 10.0, 10.0, Slice(sprite))) -> Slice(sprite)
|
|
190
|
+
e.g. PureScene.childrenOf(PureScene.label(2, "a", 0.0, 0.0, 12.0)) -> null
|
|
191
|
+
**Produces:** PureScene.custom(id: Int, x: Float, y: Float, w: Float, h: Float, actor: Actor) -> Node
|
|
192
|
+
e.g. PureScene.custom(7, 1.0, 2.0, 32.0, 32.0, ship) -> Node(id: 7, x: 1.0, y: 2.0, width: 32.0, height: 32.0, scale 1, rotation 0, color White, visible true, touchable Disabled, actions Slice[Action](), kind: Custom(actor: ship))
|
|
193
|
+
**Produces:** PureScene.drawTree(n: Node, parent: Affine, tint: Color)
|
|
194
|
+
e.g. PureScene.drawTree(custom node, Affine.Identity, Color.White) -> calls actor.draw(node, local matrix, White) once
|
|
195
|
+
**Produces:** PureScene.localMatrix(n: Node) -> Affine
|
|
196
|
+
**Produces:** PureScene.group(id: Int, x: Float, y: Float, w: Float, h: Float, children: Slice[Node]) -> Node · PureScene.sprite(id: Int, tex: Int, x: Float, y: Float, w: Float, h: Float) -> Node · PureScene.label(id: Int, text: Str, x: Float, y: Float, size: Float) -> Node · PureScene.button(id: Int, text: Str, upTex: Int, downTex: Int, x: Float, y: Float, w: Float, h: Float) -> Node · PureScene.custom(id: Int, x: Float, y: Float, w: Float, h: Float, actor: Actor) -> Node
|
|
197
|
+
**Produces:** PureScene.hit(n: Node, p: Vec2) -> Bool · PureScene.onPointer(own n: Node, e: Pointer) -> Handled · `own record Handled(node: Node, events: Slice[UiEvent])` with `fun intoNode(own self) -> Node`
|
|
198
|
+
e.g. PureScene.hit(PureScene.sprite(1, 0, 0.0, 0.0, 10.0, 10.0) with touchable Enabled, Vec2(x: 5.0, y: 5.0)) -> true; Vec2(x: 11.0, y: 5.0) -> false
|
|
199
|
+
e.g. onPointer(button, Pointer(kind: Down, over: true)) -> events [UiEvent(id, Press)], node kind Button(pressed: true, over: true)
|
|
200
|
+
**Review Focus:** state: Button press/release/enter/exit/click sequence unchanged; deep: drawTree over nested groups.
|
|
201
|
+
**Test code:** existing 5 tests keep names and values; add `custom_actor_act_and_draw_dispatch`: a test record embedding `Actor` whose `act` adds 1.0 to `n.x`, a `custom` node at x 2.0, `act` via the node's kind -> x 3.0; `draw` reached once (counter field on the record).
|
|
202
|
+
**Steps:** Transform/Node/Kind/Actor per Global constraints (Node embeds Transform; localMatrix and the box hit move onto Transform); drawing matches on kind (Sprite/Label/Button paint, Group recurses, Custom calls `actor.draw`); `hit` is the old box test; onPointer handles Button by rebuilding its kind, everything else returns no events; run `plum test plum-std/game/PureScene.plum`.
|
|
203
|
+
|
|
204
|
+
### T4 SceneTree and SceneInput over Node · Complexity 6 (O-1) · Est. context 55k
|
|
205
|
+
**Owns:** plum-std/game/SceneTree.plum, plum-std/game/SceneInput.plum
|
|
206
|
+
**Components:** 2
|
|
207
|
+
**LOC owned:** 170
|
|
208
|
+
**Tests:** fast
|
|
209
|
+
**Model:** sonnet
|
|
210
|
+
**Est. time:** 5 min
|
|
211
|
+
**Consumes:** PureScene.childrenOf(n: Node) -> Slice[Node]? (T3) · PureScene.onPointer(own n: Node, e: Pointer) -> Handled (T3) · Actions.stepAll(p: Pose, acts: Slice[Action], dt: Float) -> Step (T2)
|
|
212
|
+
**Produces:** SceneTree.update(own n: Node, dt: Float) -> Node
|
|
213
|
+
e.g. SceneTree.update(custom node x 1.0 whose actor adds 1.0 to n.x, 0.016) -> same node, x 2.0
|
|
214
|
+
e.g. SceneTree.update(group with child moveBy(10, 0, 1), 0.5) -> child x +5.0
|
|
215
|
+
**Produces:** SceneInput.stage(own root: Node) -> Stage
|
|
216
|
+
e.g. SceneInput.stage(root) -> Stage(root: root, camera: Affine.Identity, captured: null)
|
|
217
|
+
**Produces:** SceneTree.findHit(n: Node, parent: Affine, p: Vec2) -> Slice[Int]? · SceneTree.idAt(n: Node, path: Slice[Int], depth: Int) -> Int · SceneTree.findPath(n: Node, id: Int) -> Slice[Int]? · SceneTree.modify(own n: Node, id: Int, f: fn(Node) -> Node) -> Node · SceneTree.route(own n: Node, path: Slice[Int], depth: Int, e: Pointer) -> Handled
|
|
218
|
+
`own record Stage(root: Node, camera: Affine, captured: Capture?)` keeps input/frame/draw names and args.
|
|
219
|
+
**Review Focus:** state: in-place update (`update_changes_tree_in_place`, `input_changes_stage_in_place`); deep: routing through nested groups; numeric: rotated/scaled hit agrees with routing.
|
|
220
|
+
**Test code:** all 14 SceneTree and 10 SceneInput tests keep names and values.
|
|
221
|
+
**Steps:** update = `actor.act` for Custom, then actions on the pose, then Group children via `mapInPlace`; findHit/route match `Group(ks)`; run `plum test plum-std/game/SceneTree.plum plum-std/game/SceneInput.plum`.
|
|
222
|
+
|
|
223
|
+
## Game and fixture
|
|
224
|
+
|
|
225
|
+
**Probe entry:** run=./target/debug/plum test plum-examples/game_pure_scene.plum
|
|
226
|
+
Package: data pipeline (game rules); categories: state, numeric.
|
|
227
|
+
|
|
228
|
+
### T5 Orb Run on Node · Complexity 5 (O-1) · Est. context 55k
|
|
229
|
+
**Owns:** plum-examples/game_pure_scene.plum
|
|
230
|
+
**Components:** 1
|
|
231
|
+
**LOC owned:** 190
|
|
232
|
+
**Tests:** fast
|
|
233
|
+
**Model:** sonnet
|
|
234
|
+
**Est. time:** 5 min
|
|
235
|
+
**Consumes:** SceneTree.update(own n: Node, dt: Float) -> Node (T4) · SceneInput.stage(own root: Node) -> Stage (T4) · PureScene.custom(id: Int, x: Float, y: Float, w: Float, h: Float, actor: Actor) -> Node (T3)
|
|
236
|
+
**Produces:** none
|
|
237
|
+
**Review Focus:** state: orbs respawn, ship flashes on hit, restart button click; numeric: drift and wrap unchanged.
|
|
238
|
+
**Test code:** every existing `test` keeps name and values.
|
|
239
|
+
**Steps:** `Panel`, `Ship`, `Orb`, `Mine`, `Star` become records embedding `Actor` with only their own fields (clock, vx, vy, phase, spin, seed, tex, colour if needed); position/size live on the Node; `create` builders return Node via `PureScene.custom`; `act` moves `n.x`/`n.y`/`n.rotation`; helpers like `cx(a)` take Node; build layers and child lists with `Slice(a, b, ...)`; run the file's tests.
|
|
240
|
+
|
|
241
|
+
### T6 stress_pure_scene on Node · Complexity 3 (O-1) · Est. context 35k
|
|
242
|
+
**Owns:** plum-game/web-test/fixtures/stress_pure_scene.plum
|
|
243
|
+
**Components:** 1
|
|
244
|
+
**LOC owned:** 120
|
|
245
|
+
**Tests:** fast
|
|
246
|
+
**Model:** sonnet
|
|
247
|
+
**Est. time:** 2.5 min
|
|
248
|
+
**Consumes:** SceneTree.update(own n: Node, dt: Float) -> Node (T4) · PureScene.drawTree(n: Node, parent: Affine, tint: Color) (T3)
|
|
249
|
+
**Produces:** none
|
|
250
|
+
**Review Focus:** numeric: same entity count and draw count per frame as before.
|
|
251
|
+
**Test code:** `./target/debug/plum compile plum-game/web-test/fixtures/stress_pure_scene.plum` (or `plum check`) succeeds; any `test` blocks keep values.
|
|
252
|
+
**Steps:** port the same way as T5; keep entity counts and per-frame behaviour.
|
|
253
|
+
|
|
254
|
+
## Docs and benchmark
|
|
255
|
+
|
|
256
|
+
**Probe entry:** run=cargo test --manifest-path plum-game/Cargo.toml --test host_test
|
|
257
|
+
Package: file/generator (docs) + host checks; categories: protocol.
|
|
258
|
+
|
|
259
|
+
### T7 README, host checks, benchmark · Complexity 3 (O-1) · Est. context 40k
|
|
260
|
+
**Status:** draft
|
|
261
|
+
**Owns:** README.md, plum-game/tests/host_test.rs, docs/plans/2026-11-03-pure-scene-enums.benchmark.md
|
|
262
|
+
**Components:** 2
|
|
263
|
+
**LOC owned:** 90
|
|
264
|
+
**Tests:** slow
|
|
265
|
+
**Model:** sonnet
|
|
266
|
+
**Est. time:** 4 min
|
|
267
|
+
**Consumes:** none
|
|
268
|
+
**Produces:** none
|
|
269
|
+
**Review Focus:** protocol: `pureSceneExampleRunsFrames`, `pureSceneMatchesSceneDrawCounts` and web check `pure_scene_renders_and_animates` pass.
|
|
270
|
+
**Test code:** `cargo test --manifest-path plum-game/Cargo.toml --test host_test` all pass; `cargo test --manifest-path plum-game/web-test/Cargo.toml --test web_check pure_scene` passes.
|
|
271
|
+
**Steps:** README "Pure scene graph" section and its example: Node + Kind, `Custom(actor: Actor)` with a short record example; headed benchmark: `cargo build --manifest-path plum-game/web-test/Cargo.toml` then `plum-game/web-test/target/debug/plum-web-test <fixture> --headed --uncapped --measure 10 --frames-wait 6 --json` for `stress_scene.plum` and `stress_pure_scene.plum` in the same session; write a table (avg, p95, update, draw) and the bar verdict (avg within +10%, p95 within +15% of mutable) to the benchmark file. If headed Chromium can't open, report DONE_WITH_CONCERNS with headless numbers labelled as such.
|
|
272
|
+
|
|
273
|
+
## Review Focus
|
|
274
|
+
Per package lines above; checker: ownership stays sound.
|
|
275
|
+
|
|
276
|
+
## Self-review
|
|
277
|
+
Every spec section maps (Spec map); Consumes match Produces; T5/T6 own distinct files.
|
docs/plans/2026-11-03-pure-scene-enums.progress.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
- start ad3ec48
|
|
2
|
+
- unblocked: dispatched T0 T1 at 20:41:23; waiting on fixes: T2 T3 T4 T5 T6 T7
|
|
3
|
+
- Ruling: T0 skipped - every stub file already exists and Plum modules can't be stubbed; real modules stand in - cost if wrong: none
|
|
4
|
+
- checkpoint T0 68b81bc54e Wave 0 skeleton
|
|
5
|
+
- Ruling: T4-T7 held as draft until their Plum dependencies checkpoint - lint drops the edges because examples exist, but Plum modules have no stubs, so a dependent compiles the real half-rewritten module - cost if wrong: ~10 min longer wall clock
|
|
6
|
+
- unblocked: dispatched T2 at 20:43:24; waiting on fixes: none (T4-T7 draft hold)
|
|
7
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=0.0
|
|
8
|
+
- Ruling: SceneTree helper ttActKind updated by T4 (owner) - T2 changed Action's shape - cost if wrong: none
|
|
9
|
+
- checkpoint T2 1a816dd148 Actions on Action + ActionKind
|
|
10
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=271.1
|
|
11
|
+
- tests fast passed=0 failed=1 skipped=0 seconds=0.0
|
|
12
|
+
- Ruling: [scope] user asked (mid-run) for Node to embed a Transform record and for variadic Slice(...)/List(...) construction instead of push chains in pure code and example; there is no [a, b] literal - applies to T3-T7, not yet dispatched - cost if wrong: none
|
|
13
|
+
- unblocked: dispatched T3 at 20:55:11 (reused T2 agent); waiting on fixes: none (T4-T7 draft hold)
|
|
14
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=261.0
|
|
15
|
+
- checkpoint T1 1c6b96eb2d ownership: borrowed argument to a trait method
|
|
16
|
+
- checkpoint T3 c473382748 PureScene on Node, Kind, Actor
|
|
17
|
+
- unblocked: dispatched T4 (reused T2/T3 agent), F-T1-1 fixer, T3 review at 20:56:59; waiting on fixes: none (T5-T7 draft hold)
|
|
18
|
+
- T3 reviewed: no defects; +3 tests (enter/exit, non-button onPointer, invisible skip); 11 passed
|
|
19
|
+
- Ruling: [scope] T7 (README, host checks, headed benchmark) dropped by the user - PureScene may be retired after a state/view parity plan; T5-T6 still run so the tree compiles - cost if wrong: README pure-scene section stale until then
|
|
20
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=263.3
|
|
21
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.7
|
|
22
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.4
|
|
23
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.3
|
|
24
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.3
|
|
25
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.3
|
|
26
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.3
|
|
27
|
+
- Ruling: T8 added for F-T1-1's plum-core files (union_lower.rs forwarder own flags + test) - T1's Owns block is frozen - cost if wrong: none
|
|
28
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=280.1
|
|
29
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=305.7
|
|
30
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.9
|
|
31
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.3
|
|
32
|
+
- checkpoint T8 ef98109949 trait forwarder keeps `own` (follow-up to T1, finding F-T1-1)
|
|
33
|
+
- checkpoint T3 52d52baeec PureScene on Node, Kind, Actor
|
|
34
|
+
- checkpoint T4 562509426a SceneTree and SceneInput over Node
|
|
35
|
+
- unblocked: dispatched T5 T6 at 21:20:44 (fresh; old agents evicted >80k); T1 T3 T4 T8 checkpointed; T7 dropped
|
|
36
|
+
- checkpoint T6 97e883c0a2 stress_pure_scene on Node
|
|
37
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.9
|
|
38
|
+
- checkpoint T5 4cb7ef4fbb Orb Run on Node
|
|
39
|
+
- Ruling: T9 added - T5 hit a codegen gap (field-less record embedding a trait: unknown variant 'Panel__new'); fix in the compiler and drop T5's visible tag-field workaround - cost if wrong: ~5 min
|
|
40
|
+
- unblocked: dispatched T9 (fixer) at 21:23:06; T5 T6 checkpointed
|
|
41
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=221.8
|
|
42
|
+
- checkpoint T9 85f9e7d84d field-less record embedding a trait (compiler gap found by T5)
|
|
43
|
+
- tests full passed=80 failed=1 skipped=0 seconds=15.2
|
|
44
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.4
|
|
45
|
+
- tests fast passed=1 failed=0 skipped=0 seconds=0.3
|
|
46
|
+
- Ruling: final review CHANGES_REQUESTED - findings 2 and 4 stale (T9 fixed Panel; union_lower checkpointed under T8); real: F-FR-1 test fixtures in git-ignored .superlite (T10), F-FR-2 bare payload-variant pattern silently never matches -> compile error (T11), F-FR-3 negative move/projection tests (T12); run_tests --fast filters cargo by path (runs ~nothing) so fixers run cargo directly - cost if wrong: ~15 min
|
|
47
|
+
- F-FR-3 fixed: views map (pattern/element projections of own roots) now error after move; residual call-result aliases logged
|
|
48
|
+
- checkpoint T10 20527fe08e tracked test fixtures (final review F-FR-1)
|
|
49
|
+
- checkpoint T11 b32e36ab14 bare pattern on a payload variant is an error (final review F-FR-2)
|
|
50
|
+
- checkpoint T12 5f840f27dc ownership: projections after a move (final review F-FR-3)
|
|
51
|
+
- tests full passed=609 failed=2 skipped=0 seconds=276.6
|
|
52
|
+
- Ruling: codegen_tests filter_form_no_closure + one_copy_per_distinct_type fail on clean master too (pre-existing since closure specialisation, merged 8d9b551); not caused by this run; follow-up offered - cost if wrong: none
|
docs/plans/2026-11-03-pure-scene-enums.report.json
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
{
|
|
2
|
+
"projection_note": "projection: fixed forecast saved at lint 2026-11-03T20:41:17 (plan edited since)",
|
|
3
|
+
"headline": {
|
|
4
|
+
"wall_min": {
|
|
5
|
+
"projected": 43.834,
|
|
6
|
+
"actual": 40.875
|
|
7
|
+
},
|
|
8
|
+
"cost_usd_agents": {
|
|
9
|
+
"projected": 1.105389,
|
|
10
|
+
"actual": 3.029868
|
|
11
|
+
},
|
|
12
|
+
"task_min": {
|
|
13
|
+
"projected": 58.138,
|
|
14
|
+
"actual": 6.813
|
|
15
|
+
},
|
|
16
|
+
"agents": {
|
|
17
|
+
"projected": 7,
|
|
18
|
+
"actual": 30
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"feature": null,
|
|
22
|
+
"cost_split": {
|
|
23
|
+
"agents_usd": 1.156554,
|
|
24
|
+
"rework_usd": 1.873314,
|
|
25
|
+
"coordinator_usd": 14.923822,
|
|
26
|
+
"total_usd": 17.95369
|
|
27
|
+
},
|
|
28
|
+
"concurrency": {
|
|
29
|
+
"peak": 2,
|
|
30
|
+
"projected_peak": 2
|
|
31
|
+
},
|
|
32
|
+
"critical_path": {
|
|
33
|
+
"actual": [
|
|
34
|
+
"T3",
|
|
35
|
+
"T4",
|
|
36
|
+
"T5"
|
|
37
|
+
],
|
|
38
|
+
"projected": [
|
|
39
|
+
"T1",
|
|
40
|
+
"T3",
|
|
41
|
+
"T4",
|
|
42
|
+
"T5",
|
|
43
|
+
"T7"
|
|
44
|
+
],
|
|
45
|
+
"projected_min": 43.834
|
|
46
|
+
},
|
|
47
|
+
"model_mix": {
|
|
48
|
+
"unknown": {
|
|
49
|
+
"tasks": 5,
|
|
50
|
+
"cost_usd": 1.873314
|
|
51
|
+
},
|
|
52
|
+
"sonnet": {
|
|
53
|
+
"tasks": 5,
|
|
54
|
+
"cost_usd": 1.156554
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"coordinator_cost_by_model": {
|
|
58
|
+
"opus": 14.923822
|
|
59
|
+
},
|
|
60
|
+
"quality": {
|
|
61
|
+
"tests": {
|
|
62
|
+
"passed": 609,
|
|
63
|
+
"failed": 2,
|
|
64
|
+
"skipped": 0,
|
|
65
|
+
"seconds": 276.6,
|
|
66
|
+
"mode": "full",
|
|
67
|
+
"failures": [
|
|
68
|
+
"-::filter_form_no_closure: failed",
|
|
69
|
+
"-::one_copy_per_distinct_type: failed"
|
|
70
|
+
],
|
|
71
|
+
"skipped_tests": []
|
|
72
|
+
},
|
|
73
|
+
"first_time_pass": 0,
|
|
74
|
+
"first_time_n": 0,
|
|
75
|
+
"rework_dispatches": 6,
|
|
76
|
+
"open_incidents": 34
|
|
77
|
+
},
|
|
78
|
+
"tasks": [
|
|
79
|
+
{
|
|
80
|
+
"task": "T5",
|
|
81
|
+
"model": "sonnet",
|
|
82
|
+
"pred_min": 11.385,
|
|
83
|
+
"actual_min": 1.963,
|
|
84
|
+
"delta": {
|
|
85
|
+
"abs": -9.4217,
|
|
86
|
+
"pct": -82.8
|
|
87
|
+
},
|
|
88
|
+
"cost_usd": 0.351123,
|
|
89
|
+
"first_time_pass": null
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"task": "T4",
|
|
93
|
+
"model": "sonnet",
|
|
94
|
+
"pred_min": 10.615,
|
|
95
|
+
"actual_min": 1.357,
|
|
96
|
+
"delta": {
|
|
97
|
+
"abs": -9.2583,
|
|
98
|
+
"pct": -87.2
|
|
99
|
+
},
|
|
100
|
+
"cost_usd": 0.195004,
|
|
101
|
+
"first_time_pass": null
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"task": "T3",
|
|
105
|
+
"model": "sonnet",
|
|
106
|
+
"pred_min": 10.615,
|
|
107
|
+
"actual_min": 1.452,
|
|
108
|
+
"delta": {
|
|
109
|
+
"abs": -9.1633,
|
|
110
|
+
"pct": -86.3
|
|
111
|
+
},
|
|
112
|
+
"cost_usd": 0.292406,
|
|
113
|
+
"first_time_pass": null
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"task": "T2",
|
|
117
|
+
"model": "sonnet",
|
|
118
|
+
"pred_min": 9.076,
|
|
119
|
+
"actual_min": 1.135,
|
|
120
|
+
"delta": {
|
|
121
|
+
"abs": -7.941,
|
|
122
|
+
"pct": -87.5
|
|
123
|
+
},
|
|
124
|
+
"cost_usd": 0.176055,
|
|
125
|
+
"first_time_pass": null
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"task": "T6",
|
|
129
|
+
"model": "sonnet",
|
|
130
|
+
"pred_min": 5.228,
|
|
131
|
+
"actual_min": 0.907,
|
|
132
|
+
"delta": {
|
|
133
|
+
"abs": -4.3213,
|
|
134
|
+
"pct": -82.7
|
|
135
|
+
},
|
|
136
|
+
"cost_usd": 0.141966,
|
|
137
|
+
"first_time_pass": null
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"task": "T1",
|
|
141
|
+
"model": null,
|
|
142
|
+
"pred_min": 4.074,
|
|
143
|
+
"actual_min": 0.0,
|
|
144
|
+
"delta": {
|
|
145
|
+
"abs": -4.074,
|
|
146
|
+
"pct": -100.0
|
|
147
|
+
},
|
|
148
|
+
"cost_usd": 1.061078,
|
|
149
|
+
"first_time_pass": null
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"task": "T10",
|
|
153
|
+
"model": null,
|
|
154
|
+
"pred_min": null,
|
|
155
|
+
"actual_min": 0.0,
|
|
156
|
+
"delta": {
|
|
157
|
+
"abs": null,
|
|
158
|
+
"pct": null
|
|
159
|
+
},
|
|
160
|
+
"cost_usd": 0.141179,
|
|
161
|
+
"first_time_pass": null
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"task": "T11",
|
|
165
|
+
"model": null,
|
|
166
|
+
"pred_min": null,
|
|
167
|
+
"actual_min": 0.0,
|
|
168
|
+
"delta": {
|
|
169
|
+
"abs": null,
|
|
170
|
+
"pct": null
|
|
171
|
+
},
|
|
172
|
+
"cost_usd": 0.224343,
|
|
173
|
+
"first_time_pass": null
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"task": "T12",
|
|
177
|
+
"model": null,
|
|
178
|
+
"pred_min": null,
|
|
179
|
+
"actual_min": 0.0,
|
|
180
|
+
"delta": {
|
|
181
|
+
"abs": null,
|
|
182
|
+
"pct": null
|
|
183
|
+
},
|
|
184
|
+
"cost_usd": 0.281546,
|
|
185
|
+
"first_time_pass": null
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"task": "T9",
|
|
189
|
+
"model": null,
|
|
190
|
+
"pred_min": null,
|
|
191
|
+
"actual_min": 0.0,
|
|
192
|
+
"delta": {
|
|
193
|
+
"abs": null,
|
|
194
|
+
"pct": null
|
|
195
|
+
},
|
|
196
|
+
"cost_usd": 0.165168,
|
|
197
|
+
"first_time_pass": null
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
"cost_table": {
|
|
201
|
+
"source": "est",
|
|
202
|
+
"usd": 3.247,
|
|
203
|
+
"tokens": 8480136,
|
|
204
|
+
"wall_min": 568011343.9345,
|
|
205
|
+
"rows": [
|
|
206
|
+
{
|
|
207
|
+
"agent": "a1a960c040d23e232",
|
|
208
|
+
"model": "sonnet",
|
|
209
|
+
"wall_min": 0.924,
|
|
210
|
+
"tokens": 207294,
|
|
211
|
+
"usd": 0.142
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"agent": "a27dd2c4b62114365",
|
|
215
|
+
"model": "sonnet",
|
|
216
|
+
"wall_min": 3.9581,
|
|
217
|
+
"tokens": 486765,
|
|
218
|
+
"usd": 0.2815
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"agent": "a39ba404785fc1b21",
|
|
222
|
+
"model": "sonnet",
|
|
223
|
+
"wall_min": 2.3132,
|
|
224
|
+
"tokens": 360848,
|
|
225
|
+
"usd": 0.1412
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"agent": "a3c01abfc2bde8c78",
|
|
229
|
+
"model": "sonnet",
|
|
230
|
+
"wall_min": 5.7162,
|
|
231
|
+
"tokens": 392705,
|
|
232
|
+
"usd": 0.1652
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"agent": "a50a134039ef9862d",
|
|
236
|
+
"model": "sonnet",
|
|
237
|
+
"wall_min": 29895337.9274,
|
|
238
|
+
"tokens": 0,
|
|
239
|
+
"usd": 0.0
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"agent": "a5e546c8e19deafd2",
|
|
243
|
+
"model": "sonnet",
|
|
244
|
+
"wall_min": 29895365.5444,
|
|
245
|
+
"tokens": 0,
|
|
246
|
+
"usd": 0.0
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"agent": "a6ac27873b5a7d513",
|
|
250
|
+
"model": "sonnet",
|
|
251
|
+
"wall_min": 4.33,
|
|
252
|
+
"tokens": 574372,
|
|
253
|
+
"usd": 0.2243
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"agent": "a78c56b4db21c27c4",
|
|
257
|
+
"model": "sonnet",
|
|
258
|
+
"wall_min": 29895325.359,
|
|
259
|
+
"tokens": 0,
|
|
260
|
+
"usd": 0.0
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"agent": "a7dc4ab6ebd2288db",
|
|
264
|
+
"model": "sonnet",
|
|
265
|
+
"wall_min": 14.2417,
|
|
266
|
+
"tokens": 1721696,
|
|
267
|
+
"usd": 0.4792
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"agent": "a7f4019524006638c",
|
|
271
|
+
"model": "sonnet",
|
|
272
|
+
"wall_min": 29895361.9656,
|
|
273
|
+
"tokens": 0,
|
|
274
|
+
"usd": 0.0
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"agent": "a816dca9d1e31f8ac",
|
|
278
|
+
"model": "sonnet",
|
|
279
|
+
"wall_min": 15.0172,
|
|
280
|
+
"tokens": 1499690,
|
|
281
|
+
"usd": 0.5796
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"agent": "a85fd2e6c24d170b0",
|
|
285
|
+
"model": "sonnet",
|
|
286
|
+
"wall_min": 1.9766,
|
|
287
|
+
"tokens": 745714,
|
|
288
|
+
"usd": 0.3511
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"agent": "a897f89c2a7bcb3ba",
|
|
292
|
+
"model": "sonnet",
|
|
293
|
+
"wall_min": 29895331.2866,
|
|
294
|
+
"tokens": 0,
|
|
295
|
+
"usd": 0.0
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"agent": "a8d31035bcea62a1b",
|
|
299
|
+
"model": "sonnet",
|
|
300
|
+
"wall_min": 29895304.8965,
|
|
301
|
+
"tokens": 0,
|
|
302
|
+
"usd": 0.0
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"agent": "a916e3bfe065e33ca",
|
|
306
|
+
"model": "sonnet",
|
|
307
|
+
"wall_min": 29895308.043,
|
|
308
|
+
"tokens": 0,
|
|
309
|
+
"usd": 0.0
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"agent": "a91f7f59b51d17204",
|
|
313
|
+
"model": "sonnet",
|
|
314
|
+
"wall_min": 29895316.4158,
|
|
315
|
+
"tokens": 0,
|
|
316
|
+
"usd": 0.0
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"agent": "aa647f9c93fa5b7b1",
|
|
320
|
+
"model": "sonnet",
|
|
321
|
+
"wall_min": 29895329.8664,
|
|
322
|
+
"tokens": 0,
|
|
323
|
+
"usd": 0.0
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"agent": "aa83af5df90f57224",
|
|
327
|
+
"model": "opus",
|
|
328
|
+
"wall_min": 0.5738,
|
|
329
|
+
"tokens": 45856,
|
|
330
|
+
"usd": 0.2171
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"agent": "abf8945d7b644a1d5",
|
|
334
|
+
"model": "sonnet",
|
|
335
|
+
"wall_min": 0.3868,
|
|
336
|
+
"tokens": 111302,
|
|
337
|
+
"usd": 0.0839
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"agent": "aca9f95e013875bcb",
|
|
341
|
+
"model": "sonnet",
|
|
342
|
+
"wall_min": 29895313.8694,
|
|
343
|
+
"tokens": 0,
|
|
344
|
+
"usd": 0.0
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
"agent": "ad6209113dcffa8c6",
|
|
348
|
+
"model": "sonnet",
|
|
349
|
+
"wall_min": 29895307.0034,
|
|
350
|
+
"tokens": 0,
|
|
351
|
+
"usd": 0.0
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"agent": "adc378a5bab157e94",
|
|
355
|
+
"model": "sonnet",
|
|
356
|
+
"wall_min": 29895335.7,
|
|
357
|
+
"tokens": 0,
|
|
358
|
+
"usd": 0.0
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"agent": "adcb4dcfc908bc785",
|
|
362
|
+
"model": "sonnet",
|
|
363
|
+
"wall_min": 29895309.2257,
|
|
364
|
+
"tokens": 0,
|
|
365
|
+
"usd": 0.0
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"agent": "adf67542c96687e8a",
|
|
369
|
+
"model": "sonnet",
|
|
370
|
+
"wall_min": 29895353.2172,
|
|
371
|
+
"tokens": 0,
|
|
372
|
+
"usd": 0.0
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"agent": "ae54f4cfa8b37d20b",
|
|
376
|
+
"model": "sonnet",
|
|
377
|
+
"wall_min": 29895351.7776,
|
|
378
|
+
"tokens": 0,
|
|
379
|
+
"usd": 0.0
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
"agent": "ae555f72c936ddec4",
|
|
383
|
+
"model": "sonnet",
|
|
384
|
+
"wall_min": 22.8393,
|
|
385
|
+
"tokens": 2333894,
|
|
386
|
+
"usd": 0.5819
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"agent": "ae670ac54bcd2838c",
|
|
390
|
+
"model": "sonnet",
|
|
391
|
+
"wall_min": 29895314.8372,
|
|
392
|
+
"tokens": 0,
|
|
393
|
+
"usd": 0.0
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
"agent": "ae76a82a935523db2",
|
|
397
|
+
"model": "sonnet",
|
|
398
|
+
"wall_min": 29895335.1586,
|
|
399
|
+
"tokens": 0,
|
|
400
|
+
"usd": 0.0
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
"agent": "af8f810c7141bc774",
|
|
404
|
+
"model": "sonnet",
|
|
405
|
+
"wall_min": 29895359.1034,
|
|
406
|
+
"tokens": 0,
|
|
407
|
+
"usd": 0.0
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"agent": "afc0910dfeb3a2d5d",
|
|
411
|
+
"model": "sonnet",
|
|
412
|
+
"wall_min": 29895310.4604,
|
|
413
|
+
"tokens": 0,
|
|
414
|
+
"usd": 0.0
|
|
415
|
+
}
|
|
416
|
+
]
|
|
417
|
+
}
|
|
418
|
+
}
|
docs/plans/2026-11-03-pure-scene-enums.report.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# superlite build report
|
|
2
|
+
|
|
3
|
+
projection: fixed forecast saved at lint 2026-11-03T20:41:17 (plan edited since)
|
|
4
|
+
|
|
5
|
+
Projected Actual Delta
|
|
6
|
+
wall-clock (min) 43.8 40.9 -3.0 (-7%)
|
|
7
|
+
agent cost (USD, +rework) 1.11 3.03 +1.92 (+174%)
|
|
8
|
+
task-minutes 58.1 6.8 -51.3 (-88%)
|
|
9
|
+
agents 7 30 +23 (+329%)
|
|
10
|
+
cost split: agents $1.16, rework $1.87, coordinator $14.92, total $17.95
|
|
11
|
+
concurrency: peak 2 at once (projected 2); critical path actual T3 T4 T5; projected T1 > T3 > T4 > T5 > T7 (43.8 min)
|
|
12
|
+
model mix: sonnet 5 tasks $1.16, unknown 5 tasks $1.87, coordinator opus $14.92
|
|
13
|
+
quality: tests 609 passed, 2 failed; first-time pass n/a; rework dispatches 6; open incidents 34
|
|
14
|
+
|
|
15
|
+
task model pred actual delta cost
|
|
16
|
+
T5 sonnet 11.4 2.0 -9.4 (-83%) $0.351
|
|
17
|
+
T4 sonnet 10.6 1.4 -9.3 (-87%) $0.195
|
|
18
|
+
T3 sonnet 10.6 1.5 -9.2 (-86%) $0.292
|
|
19
|
+
T2 sonnet 9.1 1.1 -7.9 (-88%) $0.176
|
|
20
|
+
T6 sonnet 5.2 0.9 -4.3 (-83%) $0.142
|
|
21
|
+
(+5 smaller misses; `superlite-cli report --json` lists all)
|
|
22
|
+
|
|
23
|
+
agent cost table:
|
|
24
|
+
| agent | model | wall_min | tokens | usd |
|
|
25
|
+
| --- | --- | --- | --- | --- |
|
|
26
|
+
| a1a960c040d23e232 | sonnet | 0.924 | 207294 | 0.142 |
|
|
27
|
+
| a27dd2c4b62114365 | sonnet | 3.9581 | 486765 | 0.2815 |
|
|
28
|
+
| a39ba404785fc1b21 | sonnet | 2.3132 | 360848 | 0.1412 |
|
|
29
|
+
| a3c01abfc2bde8c78 | sonnet | 5.7162 | 392705 | 0.1652 |
|
|
30
|
+
| a50a134039ef9862d | sonnet | 29895337.9274 | 0 | 0.0 |
|
|
31
|
+
| a5e546c8e19deafd2 | sonnet | 29895365.5444 | 0 | 0.0 |
|
|
32
|
+
| a6ac27873b5a7d513 | sonnet | 4.33 | 574372 | 0.2243 |
|
|
33
|
+
| a78c56b4db21c27c4 | sonnet | 29895325.359 | 0 | 0.0 |
|
|
34
|
+
| a7dc4ab6ebd2288db | sonnet | 14.2417 | 1721696 | 0.4792 |
|
|
35
|
+
| a7f4019524006638c | sonnet | 29895361.9656 | 0 | 0.0 |
|
|
36
|
+
| a816dca9d1e31f8ac | sonnet | 15.0172 | 1499690 | 0.5796 |
|
|
37
|
+
| a85fd2e6c24d170b0 | sonnet | 1.9766 | 745714 | 0.3511 |
|
|
38
|
+
| a897f89c2a7bcb3ba | sonnet | 29895331.2866 | 0 | 0.0 |
|
|
39
|
+
| a8d31035bcea62a1b | sonnet | 29895304.8965 | 0 | 0.0 |
|
|
40
|
+
| a916e3bfe065e33ca | sonnet | 29895308.043 | 0 | 0.0 |
|
|
41
|
+
| a91f7f59b51d17204 | sonnet | 29895316.4158 | 0 | 0.0 |
|
|
42
|
+
| aa647f9c93fa5b7b1 | sonnet | 29895329.8664 | 0 | 0.0 |
|
|
43
|
+
| aa83af5df90f57224 | opus | 0.5738 | 45856 | 0.2171 |
|
|
44
|
+
| abf8945d7b644a1d5 | sonnet | 0.3868 | 111302 | 0.0839 |
|
|
45
|
+
| aca9f95e013875bcb | sonnet | 29895313.8694 | 0 | 0.0 |
|
|
46
|
+
| ad6209113dcffa8c6 | sonnet | 29895307.0034 | 0 | 0.0 |
|
|
47
|
+
| adc378a5bab157e94 | sonnet | 29895335.7 | 0 | 0.0 |
|
|
48
|
+
| adcb4dcfc908bc785 | sonnet | 29895309.2257 | 0 | 0.0 |
|
|
49
|
+
| adf67542c96687e8a | sonnet | 29895353.2172 | 0 | 0.0 |
|
|
50
|
+
| ae54f4cfa8b37d20b | sonnet | 29895351.7776 | 0 | 0.0 |
|
|
51
|
+
| ae555f72c936ddec4 | sonnet | 22.8393 | 2333894 | 0.5819 |
|
|
52
|
+
| ae670ac54bcd2838c | sonnet | 29895314.8372 | 0 | 0.0 |
|
|
53
|
+
| ae76a82a935523db2 | sonnet | 29895335.1586 | 0 | 0.0 |
|
|
54
|
+
| af8f810c7141bc774 | sonnet | 29895359.1034 | 0 | 0.0 |
|
|
55
|
+
| afc0910dfeb3a2d5d | sonnet | 29895310.4604 | 0 | 0.0 |
|
|
56
|
+
| totals | | 568011343.9345 | 8480136 | 3.247 |
|
|
57
|
+
cost source: est
|
|
58
|
+
|
|
59
|
+
takeaway: ran 7% faster than projected; cost 174% over; biggest miss T5
|
docs/plans/2026-11-03-state-view-parity.review.md
CHANGED
|
@@ -47,6 +47,11 @@ The mutable examples (`game.plum`, `game_scene.plum`, `scene.plum`, `trait_scene
|
|
|
47
47
|
- capture is dropped when the captured id is no longer in the picture.
|
|
48
48
|
- Button visuals come from the UI state: `Ui.pressed(ui, id)`, `Ui.hovered(ui, id)`. `Ui.button(id, label, rect, ui)` builds the button's `SceneNode`.
|
|
49
49
|
- Events are `UiEvent(id, kind)`, as in PureScene, and the game handles them in `step`.
|
|
50
|
+
- **Static subtree reuse (user, 2026-11-03).** Parts of a picture that never change are built once and returned as the same value every frame. Examples: a starfield's layout, terrain tiles, HUD frames, panel backgrounds.
|
|
51
|
+
- `view` places them as-is (`Group(.., children: Cons(s.staticBg, ...))`), so they cost no allocation per frame. Only the moving parts are rebuilt.
|
|
52
|
+
- Anything that varies over time is applied around the shared subtree, not inside it: a twinkle via `Filter(Alpha(..), child: staticStar)`, a scroll via `Group(Translate(..), child: staticMap)`.
|
|
53
|
+
- Stored in `State` (built in `newState`), or as a module-level constant when it depends on nothing.
|
|
54
|
+
- Test: a wasmparser/allocation count shows the static part adds no `struct.new` per frame. `game_state_view`'s starfield becomes static (positions fixed, twinkle as a per-frame alpha wrapper).
|
|
50
55
|
- **The game shell stays thin.** `update` reads input into a `Msg`, then calls `step`, then `Ui.step` against `view(state)`, then `step` again for each `UiEvent`. Sound remains the only side effect.
|
|
51
56
|
|
|
52
57
|
## What you will get
|
|
@@ -68,6 +73,7 @@ The mutable examples (`game.plum`, `game_scene.plum`, `scene.plum`, `trait_scene
|
|
|
68
73
|
| Picture tags | `Tagged(id, child)`, `hitId`, draw/bounds pass-through; tests | S |
|
|
69
74
|
| Ui | `UiState`, `Ui.step`, events, capture, `pressed`/`hovered`, `button`; PureScene-equivalent tests | M |
|
|
70
75
|
| Orb Run state/view | restart via Ui, Anim for the title and hit flash; tests unchanged plus Ui/Anim cases | M |
|
|
76
|
+
| Static subtrees | starfield built once in `State`, twinkle as an alpha wrapper; allocation test | S |
|
|
71
77
|
| Stress fixture | tags, buttons, animations in `stress_state_view.plum` | S |
|
|
72
78
|
| Benchmark + README | headed table, "Functional scenes" section | S |
|
|
73
79
|
|
plum-checker/src/lib.rs
CHANGED
|
@@ -3906,6 +3906,12 @@ fn liftInlineVariant(
|
|
|
3906
3906
|
Ok(lifted)
|
|
3907
3907
|
}
|
|
3908
3908
|
|
|
3909
|
+
thread_local! {
|
|
3910
|
+
/// Address of a top-level `if X := v` pattern, where a bare inline-variant name
|
|
3911
|
+
/// narrows `v` instead of matching a value.
|
|
3912
|
+
static ALLOW_BARE_TOP: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3909
3915
|
/// Checks a single case pattern against the type of the subject it matches, binding any
|
|
3910
3916
|
/// new names it introduces into `env`. Constructor-payload sub-patterns (`Next(x)`) bind
|
|
3911
3917
|
/// against that variant's declared field types (see `EnumVariantInfo::field_types`).
|
|
@@ -3932,6 +3938,20 @@ pub(crate) fn checkPattern(
|
|
|
3932
3938
|
}
|
|
3933
3939
|
}
|
|
3934
3940
|
}
|
|
3941
|
+
// An inline variant with fields is never matched bare (Rust semantics): the bare
|
|
3942
|
+
// name would narrow to the hidden member and silently never match as written.
|
|
3943
|
+
if let ast::CasePattern::Name(n) = pat {
|
|
3944
|
+
let allowed = ALLOW_BARE_TOP.with(|c| c.get()) == pat as *const ast::CasePattern as usize;
|
|
3945
|
+
if let (false, Some(owner), Some(subject)) =
|
|
3946
|
+
(allowed, unions::inlineOwner(n), nominalName(subject_ty))
|
|
3947
|
+
{
|
|
3948
|
+
if owner == subject {
|
|
3949
|
+
return Err(format!(
|
|
3950
|
+
"{n} has fields; match it with one pattern per field, e.g. {n}(_, _)"
|
|
3951
|
+
));
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3935
3955
|
if let Some(r) = unions::checkMemberPattern(pat, subject_ty, env, ctx) {
|
|
3936
3956
|
return r;
|
|
3937
3957
|
}
|
|
@@ -4637,7 +4657,10 @@ fn bindCondEnv(cond: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<TypeEn
|
|
|
4637
4657
|
}
|
|
4638
4658
|
pat => {
|
|
4639
4659
|
let narrow = unions::narrowTarget(&b.value, pat, &ty, env, ctx);
|
|
4660
|
+
ALLOW_BARE_TOP.with(|c| c.set(pat as *const ast::CasePattern as usize));
|
|
4640
|
-
checkPattern(pat, &ty, &mut benv, ctx)
|
|
4661
|
+
let r = checkPattern(pat, &ty, &mut benv, ctx);
|
|
4662
|
+
ALLOW_BARE_TOP.with(|c| c.set(0));
|
|
4663
|
+
r?;
|
|
4641
4664
|
unions::applyNarrow(narrow, &b.case, &mut benv, ctx);
|
|
4642
4665
|
}
|
|
4643
4666
|
}
|
plum-checker/src/ownership.rs
CHANGED
|
@@ -85,6 +85,7 @@ pub fn checkOwnership(src: &Source) -> Vec<CheckError> {
|
|
|
85
85
|
selfTy: None,
|
|
86
86
|
tys: HashMap::new(),
|
|
87
87
|
elems: HashMap::new(),
|
|
88
|
+
views: HashMap::new(),
|
|
88
89
|
traits: HashMap::new(),
|
|
89
90
|
dead: false,
|
|
90
91
|
loops: Vec::new(),
|
|
@@ -159,6 +160,9 @@ struct Cx<'a> {
|
|
|
159
160
|
tys: HashMap<String, String>,
|
|
160
161
|
/// `Slice[T]` locals whose element type `T` is known.
|
|
161
162
|
elems: HashMap<String, String>,
|
|
163
|
+
/// Borrowed projections of an owned binding (pattern binders / element views
|
|
164
|
+
/// holding own data), mapped to the root binding: moving the root kills them.
|
|
165
|
+
views: HashMap<String, String>,
|
|
162
166
|
traits: HashMap<String, &'a TraitDecl>,
|
|
163
167
|
/// The current path ended in return/crash/break/continue.
|
|
164
168
|
dead: bool,
|
|
@@ -214,6 +218,7 @@ impl<'a> Cx<'a> {
|
|
|
214
218
|
self.selfTy = selfTy;
|
|
215
219
|
self.tys = HashMap::new();
|
|
216
220
|
self.elems = HashMap::new();
|
|
221
|
+
self.views = HashMap::new();
|
|
217
222
|
self.dead = false;
|
|
218
223
|
self.loops = Vec::new();
|
|
219
224
|
self.swap = None;
|
|
@@ -257,6 +262,29 @@ impl<'a> Cx<'a> {
|
|
|
257
262
|
None
|
|
258
263
|
}
|
|
259
264
|
|
|
265
|
+
/// Receiver type unknown (e.g. bound by a pattern): a method called `name`, but
|
|
266
|
+
/// only when EVERY method of that name in the program (trait methods, required
|
|
267
|
+
/// methods, record/enum methods) takes `self` and each param the same way, so
|
|
268
|
+
/// the call's modes do not depend on which type the receiver turns out to be.
|
|
269
|
+
/// Otherwise `None`, and the caller consumes every argument.
|
|
270
|
+
fn uniqueTraitMethod(&self, name: &str) -> Option<&'a Fn> {
|
|
271
|
+
let all = self
|
|
272
|
+
.traits
|
|
273
|
+
.values()
|
|
274
|
+
.flat_map(|t| t.methods.iter().chain(t.required.iter()))
|
|
275
|
+
.chain(self.methods.values().copied())
|
|
276
|
+
.filter(|f| f.name == name);
|
|
277
|
+
let mut found: Option<&'a Fn> = None;
|
|
278
|
+
for f in all {
|
|
279
|
+
match found {
|
|
280
|
+
None => found = Some(f),
|
|
281
|
+
Some(g) if sameModes(g, f) => {}
|
|
282
|
+
Some(_) => return None,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
found
|
|
286
|
+
}
|
|
287
|
+
|
|
260
288
|
/// `xs.at(i)` and friends on a `List[Own]`: a borrowed view of an element.
|
|
261
289
|
fn elemRead(&self, e: &Expr) -> Option<(String, String)> {
|
|
262
290
|
let Expr::Attribute(a) = e else { return None };
|
|
@@ -269,6 +297,61 @@ impl<'a> Cx<'a> {
|
|
|
269
297
|
Some((format!("{}.{}", recv, c.name), el.clone()))
|
|
270
298
|
}
|
|
271
299
|
|
|
300
|
+
/// The owned binding a `match` subject / element read is rooted in, looking
|
|
301
|
+
/// through views (`n.kind`, `n`, `ks.at(0)` where `ks` is a view of `n`).
|
|
302
|
+
fn viewRoot(&self, e: &Expr) -> Option<String> {
|
|
303
|
+
let r = match e {
|
|
304
|
+
Expr::Paren(i) => return self.viewRoot(i),
|
|
305
|
+
Expr::Attribute(a) if matches!(a.attr, AttrKind::Method(_)) => {
|
|
306
|
+
return self.elemViewRoot(e).or_else(|| self.viewRoot(&a.object));
|
|
307
|
+
}
|
|
308
|
+
Expr::Attribute(a) => return self.viewRoot(&a.object),
|
|
309
|
+
_ => nameOf(e)?,
|
|
310
|
+
};
|
|
311
|
+
if let Some(root) = self.views.get(&r) {
|
|
312
|
+
return Some(root.clone());
|
|
313
|
+
}
|
|
314
|
+
self.env.contains_key(&r).then_some(r)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// `ks.at(i)` where `ks` is a view into an owned binding: that binding.
|
|
318
|
+
fn elemViewRoot(&self, e: &Expr) -> Option<String> {
|
|
319
|
+
let Expr::Attribute(a) = e else { return None };
|
|
320
|
+
let AttrKind::Method(c) = &a.attr else { return None };
|
|
321
|
+
if !matches!(c.name.as_str(), "at" | "get" | "first" | "last") {
|
|
322
|
+
return None;
|
|
323
|
+
}
|
|
324
|
+
self.viewRoot(&a.object)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/// Names a pattern binds to own data: an own-typed field or a `Slice[Own]`.
|
|
328
|
+
fn viewBinds(&self, p: &CasePattern, out: &mut Vec<String>) {
|
|
329
|
+
let CasePattern::Class { name, fields } = p else { return };
|
|
330
|
+
let owner = self.enums.get(name).copied().or_else(|| {
|
|
331
|
+
self.enums
|
|
332
|
+
.values()
|
|
333
|
+
.copied()
|
|
334
|
+
.find(|e| e.variants.iter().any(|v| &v.name == name))
|
|
335
|
+
});
|
|
336
|
+
let Some(v) = owner.and_then(|e| e.variants.iter().find(|v| &v.name == name)) else {
|
|
337
|
+
return;
|
|
338
|
+
};
|
|
339
|
+
for (i, f) in fields.iter().enumerate() {
|
|
340
|
+
let holdsOwn = match v.fields.get(i) {
|
|
341
|
+
Some(t) => {
|
|
342
|
+
self.own.contains(&t.name)
|
|
343
|
+
|| (t.name == "Slice" && t.generics.iter().any(|g| self.own.contains(&g.name)))
|
|
344
|
+
}
|
|
345
|
+
None => false,
|
|
346
|
+
};
|
|
347
|
+
match f {
|
|
348
|
+
CasePattern::Name(n) if holdsOwn => out.push(n.clone()),
|
|
349
|
+
CasePattern::Class { .. } => self.viewBinds(f, out),
|
|
350
|
+
_ => {}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
272
355
|
fn isOwnTy(&self, ty: &Option<String>) -> bool {
|
|
273
356
|
ty.as_ref().is_some_and(|t| self.own.contains(t))
|
|
274
357
|
}
|
|
@@ -395,10 +478,16 @@ impl<'a> Cx<'a> {
|
|
|
395
478
|
|
|
396
479
|
/// Names a pattern binds to a value of own type, with that type. `ty` is the
|
|
397
480
|
/// type of the value the pattern matches.
|
|
481
|
+
fn patternBinds(
|
|
482
|
+
&self,
|
|
483
|
+
p: &CasePattern,
|
|
484
|
+
ty: Option<String>,
|
|
485
|
+
onlyOwn: bool,
|
|
398
|
-
|
|
486
|
+
out: &mut Vec<(String, String)>,
|
|
487
|
+
) {
|
|
399
488
|
match p {
|
|
400
489
|
CasePattern::Name(n) => {
|
|
401
|
-
if let Some(t) = ty.filter(|t| self.own.contains(t)) {
|
|
490
|
+
if let Some(t) = ty.filter(|t| !onlyOwn || self.own.contains(t)) {
|
|
402
491
|
out.push((n.clone(), t));
|
|
403
492
|
}
|
|
404
493
|
}
|
|
@@ -419,7 +508,7 @@ impl<'a> Cx<'a> {
|
|
|
419
508
|
(None, Some(t)) if i == 0 => Some(t.name.clone()),
|
|
420
509
|
_ => None,
|
|
421
510
|
};
|
|
422
|
-
self.patternBinds(f, fty, out);
|
|
511
|
+
self.patternBinds(f, fty, onlyOwn, out);
|
|
423
512
|
}
|
|
424
513
|
}
|
|
425
514
|
_ => {}
|
|
@@ -520,7 +609,7 @@ impl<'a> Cx<'a> {
|
|
|
520
609
|
for (i, p) in c.patterns.iter().enumerate() {
|
|
521
610
|
let ty = m.subjects.get(i).and_then(|e| self.typeOf(e));
|
|
522
611
|
let mut found = Vec::new();
|
|
523
|
-
self.patternBinds(p, ty, &mut found);
|
|
612
|
+
self.patternBinds(p, ty, true, &mut found);
|
|
524
613
|
if modes.get(i) == Some(&Some(Mode::Owned)) && !found.is_empty() {
|
|
525
614
|
consumes = true;
|
|
526
615
|
}
|
|
@@ -548,12 +637,40 @@ impl<'a> Cx<'a> {
|
|
|
548
637
|
for n in names {
|
|
549
638
|
cx.env.remove(&n);
|
|
550
639
|
cx.tys.remove(&n);
|
|
640
|
+
cx.views.remove(&n);
|
|
641
|
+
}
|
|
642
|
+
// Binders holding own data read out of an owned binding are
|
|
643
|
+
// views into it: they die when it is moved. (`match n` itself
|
|
644
|
+
// consumes `n`, so its binders are moved out, not views.)
|
|
645
|
+
let subjectRoot = m.subjects.iter().find_map(|e| match e {
|
|
646
|
+
Expr::Var(_) | Expr::Self_ => nameOf(e)
|
|
647
|
+
.and_then(|n| cx.views.get(&n).cloned()),
|
|
648
|
+
e => cx.viewRoot(e),
|
|
649
|
+
});
|
|
650
|
+
if let Some(root) = subjectRoot {
|
|
651
|
+
let mut vs = Vec::new();
|
|
652
|
+
for p in &c.patterns {
|
|
653
|
+
cx.viewBinds(p, &mut vs);
|
|
654
|
+
}
|
|
655
|
+
for n in vs {
|
|
656
|
+
cx.views.insert(n, root.clone());
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Every binder gets its declared type (own or not), so a method
|
|
660
|
+
// call on it resolves to its real type's method.
|
|
661
|
+
for (i, p) in c.patterns.iter().enumerate() {
|
|
662
|
+
let ty = m.subjects.get(i).and_then(|e| cx.typeOf(e));
|
|
663
|
+
let mut all = Vec::new();
|
|
664
|
+
cx.patternBinds(p, ty, false, &mut all);
|
|
665
|
+
for (n, t) in all {
|
|
666
|
+
cx.tys.insert(n, t);
|
|
667
|
+
}
|
|
551
668
|
}
|
|
552
669
|
for (i, p) in c.patterns.iter().enumerate() {
|
|
553
670
|
let Some(Some(mode)) = modes.get(i).copied() else { continue };
|
|
554
671
|
let ty = m.subjects.get(i).and_then(|e| cx.typeOf(e));
|
|
555
672
|
let mut found = Vec::new();
|
|
556
|
-
cx.patternBinds(p, ty, &mut found);
|
|
673
|
+
cx.patternBinds(p, ty, true, &mut found);
|
|
557
674
|
for (n, t) in found {
|
|
558
675
|
cx.tys.insert(n.clone(), t.clone());
|
|
559
676
|
cx.env.insert(
|
|
@@ -674,6 +791,10 @@ impl<'a> Cx<'a> {
|
|
|
674
791
|
AssignTarget::Var(x) => {
|
|
675
792
|
let val = a.values.get(i).or(a.values.first());
|
|
676
793
|
let view = val.and_then(|v| self.elemRead(v));
|
|
794
|
+
self.views.remove(x);
|
|
795
|
+
if let Some(root) = val.and_then(|v| self.elemViewRoot(v)) {
|
|
796
|
+
self.views.insert(x.clone(), root);
|
|
797
|
+
}
|
|
677
798
|
let valTy = match &view {
|
|
678
799
|
Some((_, el)) => Some(el.clone()),
|
|
679
800
|
None => val.and_then(|v| self.typeOf(v)),
|
|
@@ -733,6 +854,19 @@ impl<'a> Cx<'a> {
|
|
|
733
854
|
// ---- expressions -------------------------------------------------------
|
|
734
855
|
|
|
735
856
|
fn checkLive(&mut self, name: &str) {
|
|
857
|
+
if let Some(root) = self.views.get(name).cloned() {
|
|
858
|
+
if let Some((l, c)) = self.env.get(&root).and_then(|b| b.moved) {
|
|
859
|
+
self.views.remove(name); // report once
|
|
860
|
+
let p = self.pos;
|
|
861
|
+
self.err(
|
|
862
|
+
p,
|
|
863
|
+
format!(
|
|
864
|
+
"\"{}\" used after it was moved at {}:{} (it is a view into \"{}\")",
|
|
865
|
+
name, l, c, root
|
|
866
|
+
),
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
736
870
|
let Some(b) = self.env.get_mut(name) else { return };
|
|
737
871
|
if let Some((l, c)) = b.moved {
|
|
738
872
|
b.moved = None; // report once
|
|
@@ -932,7 +1066,9 @@ impl<'a> Cx<'a> {
|
|
|
932
1066
|
Expr::TypeName(t) => Some(t.clone()),
|
|
933
1067
|
o => self.typeOf(o),
|
|
934
1068
|
};
|
|
1069
|
+
let callee = recvTy
|
|
935
|
-
|
|
1070
|
+
.and_then(|t| self.findMethod(&t, &c.name, 0))
|
|
1071
|
+
.or_else(|| self.uniqueTraitMethod(&c.name));
|
|
936
1072
|
if c.line != 0 {
|
|
937
1073
|
self.pos = (c.line, c.column);
|
|
938
1074
|
}
|
|
@@ -959,6 +1095,7 @@ impl<'a> Cx<'a> {
|
|
|
959
1095
|
patternNames(b.pattern(), &mut names);
|
|
960
1096
|
for n in names {
|
|
961
1097
|
self.env.remove(&n);
|
|
1098
|
+
self.views.remove(&n);
|
|
962
1099
|
}
|
|
963
1100
|
if let Some(g) = &b.case.guard {
|
|
964
1101
|
self.walk(g);
|
|
@@ -987,6 +1124,17 @@ impl<'a> Cx<'a> {
|
|
|
987
1124
|
}
|
|
988
1125
|
|
|
989
1126
|
/// Param `i`, clamped to a trailing variadic param.
|
|
1127
|
+
/// Two methods take `self` and every param in the same way (as a call site sees it).
|
|
1128
|
+
fn sameModes(a: &Fn, b: &Fn) -> bool {
|
|
1129
|
+
let takes = |p: &Param| {
|
|
1130
|
+
p.isOwn
|
|
1131
|
+
|| matches!(&p.ty, ParamType::Type(t) | ParamType::Variadic(t) if isGeneric(&t.name))
|
|
1132
|
+
};
|
|
1133
|
+
a.ownSelf == b.ownSelf
|
|
1134
|
+
&& a.params.len() == b.params.len()
|
|
1135
|
+
&& a.params.iter().zip(&b.params).all(|(x, y)| takes(x) == takes(y))
|
|
1136
|
+
}
|
|
1137
|
+
|
|
990
1138
|
fn paramAt(f: &Fn, i: usize) -> Option<&Param> {
|
|
991
1139
|
f.params.get(i).or_else(|| {
|
|
992
1140
|
f.params
|
plum-checker/tests/fixtures/custom_actor_probe2.plum
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import std/Int
|
|
2
|
+
import std/Float
|
|
3
|
+
import std/Str
|
|
4
|
+
import std/Slice
|
|
5
|
+
|
|
6
|
+
own record Node(id: Int, x: Float, y: Float, kind: Kind)
|
|
7
|
+
|
|
8
|
+
enum Kind =
|
|
9
|
+
| Group(children: Slice[Node])
|
|
10
|
+
| Label(text: Str, size: Float)
|
|
11
|
+
| Custom(actor: Actor)
|
|
12
|
+
|
|
13
|
+
# Behaviour only: the game's records embed it, like Box<dyn Actor>.
|
|
14
|
+
trait Actor =
|
|
15
|
+
fun act(self, own n: Node, dt: Float) -> Node
|
|
16
|
+
fun score(self, n: Node) -> Float
|
|
17
|
+
|
|
18
|
+
record Ship(Actor, clock: Float) =
|
|
19
|
+
override fun act(self, own n: Node, dt: Float) -> Node =
|
|
20
|
+
self.clock = self.clock + dt
|
|
21
|
+
n
|
|
22
|
+
override fun score(self, n: Node) -> Float = self.clock
|
|
23
|
+
|
|
24
|
+
record Orb(Actor, vx: Float) =
|
|
25
|
+
override fun act(self, own n: Node, dt: Float) -> Node =
|
|
26
|
+
n.x = n.x + self.vx * dt
|
|
27
|
+
n
|
|
28
|
+
override fun score(self, n: Node) -> Float = n.x
|
|
29
|
+
|
|
30
|
+
fun actorOf(n: Node) -> Actor? = match n.kind
|
|
31
|
+
Custom(a) => a
|
|
32
|
+
_ => null
|
|
33
|
+
|
|
34
|
+
fun update(own n: Node, dt: Float) -> Node =
|
|
35
|
+
if a := actorOf(n)
|
|
36
|
+
return a.act(n, dt)
|
|
37
|
+
match n.kind
|
|
38
|
+
Group(ks) =>
|
|
39
|
+
_ := ks.mapInPlace(|k| update(k, dt))
|
|
40
|
+
_ => 0
|
|
41
|
+
n
|
|
42
|
+
|
|
43
|
+
fun total(n: Node) -> Float = match n.kind
|
|
44
|
+
Group(ks) =>
|
|
45
|
+
s := 0.0
|
|
46
|
+
for i := range ks.length()
|
|
47
|
+
s = s + total(ks.at(i))
|
|
48
|
+
s
|
|
49
|
+
Custom(a) => a.score(n)
|
|
50
|
+
Label(_, _) => 0.0
|
|
51
|
+
|
|
52
|
+
test "custom_actor"
|
|
53
|
+
ks := Slice[Node]()
|
|
54
|
+
ks.push(Node(id: 1, x: 1.0, y: 0.0, kind: Custom(actor: Orb(vx: 2.0))))
|
|
55
|
+
ks.push(Node(id: 2, x: 0.0, y: 0.0, kind: Custom(actor: Ship(clock: 0.0))))
|
|
56
|
+
root := Node(id: 0, x: 0.0, y: 0.0, kind: Group(children: ks))
|
|
57
|
+
r := update(root, 0.5)
|
|
58
|
+
assert total(r) == 2.0 + 0.5
|
plum-checker/tests/fixtures/g3_generic_funstar.plum
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module g3
|
|
2
|
+
|
|
3
|
+
enum Link[T] =
|
|
4
|
+
| Nil
|
|
5
|
+
| Cons(head: T, tail: Link[T])
|
|
6
|
+
|
|
7
|
+
fun length(self) -> Int = match self
|
|
8
|
+
Nil => 0
|
|
9
|
+
Cons(_, t) => 1 + t.length()
|
|
10
|
+
|
|
11
|
+
fun* items(self) *T =
|
|
12
|
+
cur := self
|
|
13
|
+
while true
|
|
14
|
+
if Cons(h, t) := cur
|
|
15
|
+
cur = t
|
|
16
|
+
yield h
|
|
17
|
+
else
|
|
18
|
+
break
|
|
19
|
+
|
|
20
|
+
fun* collect(xs: Link[T]) *T -> Link[T] =
|
|
21
|
+
out: Link[T] := Nil
|
|
22
|
+
for x := range xs.items()
|
|
23
|
+
v := yield x
|
|
24
|
+
out = Cons(head: v, tail: out)
|
|
25
|
+
out
|
|
26
|
+
|
|
27
|
+
fun both() -> Int =
|
|
28
|
+
a: Link[Int] := Cons(head: 1, tail: Cons(head: 2, tail: Nil))
|
|
29
|
+
b: Link[Str] := Cons(head: "x", tail: Nil)
|
|
30
|
+
ints := for x := range collect(a)
|
|
31
|
+
x
|
|
32
|
+
strs := for s := range collect(b)
|
|
33
|
+
s
|
|
34
|
+
ints.length() + strs.length()
|
plum-checker/tests/optional_unpinned_tests.rs
CHANGED
|
@@ -55,7 +55,7 @@ fn annotated_bare_tag_ok() {
|
|
|
55
55
|
|
|
56
56
|
#[test]
|
|
57
57
|
fn generic_funstar_wired() {
|
|
58
|
-
let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/
|
|
58
|
+
let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/g3_generic_funstar.plum")).unwrap();
|
|
59
59
|
let mut parser = tree_sitter::Parser::new();
|
|
60
60
|
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
|
|
61
61
|
let tree = parser.parse(&src, None).unwrap();
|
plum-checker/tests/ownership_moves_test.rs
CHANGED
|
@@ -163,3 +163,70 @@ fn alias_borrowed_param_rejected() {
|
|
|
163
163
|
let t = texts("fun f(s: Sprite) -> Float =\n t := s\n t.x\n");
|
|
164
164
|
assert!(t.iter().any(|m| m.starts_with("cannot store borrowed \"s\"")), "{t:?}");
|
|
165
165
|
}
|
|
166
|
+
|
|
167
|
+
const NODE_DEFS: &str = "\
|
|
168
|
+
import std/Float
|
|
169
|
+
import std/Slice
|
|
170
|
+
|
|
171
|
+
own record Node(id: Int, x: Float, kind: Kind)
|
|
172
|
+
|
|
173
|
+
enum Kind =
|
|
174
|
+
| Group(children: Slice[Node])
|
|
175
|
+
| Custom(actor: Actor)
|
|
176
|
+
|
|
177
|
+
trait Actor =
|
|
178
|
+
fun act(self, own n: Node, dt: Float) -> Node
|
|
179
|
+
|
|
180
|
+
fun consume(own n: Node) -> Node = n
|
|
181
|
+
fun look(n: Node) -> Float = n.x
|
|
182
|
+
fun first(n: Node) -> Node? = match n.kind
|
|
183
|
+
Group(ks) => ks.at(0)
|
|
184
|
+
_ => null
|
|
185
|
+
fun actorOf(n: Node) -> Actor? = match n.kind
|
|
186
|
+
Custom(a) => a
|
|
187
|
+
_ => null
|
|
188
|
+
";
|
|
189
|
+
|
|
190
|
+
fn node_texts(body: &str) -> Vec<String> {
|
|
191
|
+
checkOwnership(&parse(&format!("{NODE_DEFS}\n{body}\n")))
|
|
192
|
+
.into_iter()
|
|
193
|
+
.map(|e| e.message)
|
|
194
|
+
.collect()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#[test]
|
|
198
|
+
fn own_field_used_after_move_rejected() {
|
|
199
|
+
let t = node_texts("fun f(own n: Node) -> Float =\n m := consume(n)\n n.x\n");
|
|
200
|
+
assert!(t.iter().any(|m| m.starts_with("\"n\" used after it was moved")), "{t:?}");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[test]
|
|
204
|
+
fn own_child_projection_after_move_rejected() {
|
|
205
|
+
let t = node_texts(
|
|
206
|
+
"fun f(own n: Node) -> Float =\n match n.kind\n Group(ks) =>\n m := consume(n)\n ks.len()\n _ => 0.0\n",
|
|
207
|
+
);
|
|
208
|
+
assert!(t.iter().any(|m| m.contains("used after it was moved")), "{t:?}");
|
|
209
|
+
// An element read out of the child slice is a view too.
|
|
210
|
+
let t2 = node_texts(
|
|
211
|
+
"fun f(own n: Node) -> Float =\n match n.kind\n Group(ks) =>\n c := ks.at(0)\n m := consume(n)\n look(c)\n _ => 0.0\n",
|
|
212
|
+
);
|
|
213
|
+
assert!(t2.iter().any(|m| m.contains("used after it was moved")), "{t2:?}");
|
|
214
|
+
// An own-typed payload binder is a view into the scrutinee's root.
|
|
215
|
+
let defs = "own record Cell(id: Int, kind: Shape)\nenum Shape =\n | Pair(first: Cell)\n | Leaf(x: Float)\nfun eat(own n: Cell) -> Cell = n\nfun peek(n: Cell) -> Int = n.id\n";
|
|
216
|
+
let body = "fun f(own n: Cell) -> Int =\n match n.kind\n Pair(k) =>\n m := eat(n)\n peek(k)\n _ => 0\n";
|
|
217
|
+
let t3: Vec<String> = checkOwnership(&parse(&format!("{defs}\n{body}\n"))).into_iter().map(|e| e.message).collect();
|
|
218
|
+
assert!(t3.iter().any(|m| m.contains("used after it was moved")), "{t3:?}");
|
|
219
|
+
let ok = "fun f(own n: Cell) -> Int =\n match n.kind\n Pair(k) =>\n v := peek(k)\n m := eat(n)\n v\n _ => 0\n";
|
|
220
|
+
let t4: Vec<String> = checkOwnership(&parse(&format!("{defs}\n{ok}\n"))).into_iter().map(|e| e.message).collect();
|
|
221
|
+
assert!(t4.is_empty(), "{t4:?}");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[test]
|
|
225
|
+
fn non_own_value_read_before_move_ok() {
|
|
226
|
+
let t = node_texts(
|
|
227
|
+
"fun f(own n: Node, dt: Float) -> Node =\n if a := actorOf(n)\n return a.act(n, dt)\n n\n",
|
|
228
|
+
);
|
|
229
|
+
assert!(t.is_empty(), "{t:?}");
|
|
230
|
+
let t2 = node_texts("fun g(own n: Node) -> Float =\n v := n.x\n m := consume(n)\n v\n");
|
|
231
|
+
assert!(t2.is_empty(), "{t2:?}");
|
|
232
|
+
}
|
plum-checker/tests/ownership_trait_args_test.rs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_checker::ownership::checkOwnership;
|
|
3
|
+
use plum_core::{ast::Source, AstParser};
|
|
4
|
+
|
|
5
|
+
fn parse(src: &str) -> Source {
|
|
6
|
+
let mut parser = tree_sitter::Parser::new();
|
|
7
|
+
parser
|
|
8
|
+
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
9
|
+
.unwrap();
|
|
10
|
+
let tree = parser.parse(src, None).unwrap();
|
|
11
|
+
assert!(
|
|
12
|
+
!tree.root_node().has_error(),
|
|
13
|
+
"embedded test source has parse errors:\n{src}"
|
|
14
|
+
);
|
|
15
|
+
AstParser::new(src).parseSource(tree.root_node())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PROBE: &str = include_str!("fixtures/custom_actor_probe2.plum");
|
|
19
|
+
|
|
20
|
+
fn texts(body: &str) -> Vec<String> {
|
|
21
|
+
checkOwnership(&parse(&format!("{PROBE}\n{body}\n")))
|
|
22
|
+
.into_iter()
|
|
23
|
+
.map(|e| e.message)
|
|
24
|
+
.collect()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[test]
|
|
28
|
+
fn trait_method_borrowed_arg_ok() {
|
|
29
|
+
let t = texts("");
|
|
30
|
+
assert!(t.is_empty(), "{t:?}");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[test]
|
|
34
|
+
fn trait_method_own_arg_moves() {
|
|
35
|
+
let t = texts("fun g(own n: Node, a: Actor) -> Float =\n m := a.act(n, 0.1)\n n.x\n");
|
|
36
|
+
assert!(t.iter().any(|m| m.starts_with("\"n\" used after it was moved")), "{t:?}");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[test]
|
|
40
|
+
fn borrowed_to_own_trait_param_rejected() {
|
|
41
|
+
let t = texts("fun h(n: Node, a: Actor) -> Node = a.act(n, 0.1)\n");
|
|
42
|
+
assert!(t.iter().any(|m| m.contains("cannot store borrowed \"n\"")), "{t:?}");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn plain_fn_borrowed_arg_still_ok() {
|
|
47
|
+
let t = texts("fun look(n: Node) -> Float = n.x\nfun k(n: Node) -> Float = look(n)\n");
|
|
48
|
+
assert!(t.is_empty(), "{t:?}");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn same_name_method_modes_disagree_is_conservative() {
|
|
53
|
+
let t = texts(
|
|
54
|
+
"trait Probe =\n fun f(self, n: Node) -> Float\n\nrecord Sq(Probe, w: Float) =\n override fun f(self, n: Node) -> Float = n.x\n\nrecord Other(w: Float) =\n fun f(self, own n: Node) -> Float = n.x\n\nfun u(n: Node) -> Float = match n.kind\n Custom(a) => a.f(n)\n _ => 0.0\n",
|
|
55
|
+
);
|
|
56
|
+
assert!(t.iter().any(|m| m.contains("cannot store borrowed \"n\"")), "{t:?}");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn same_name_method_modes_agree_uses_them() {
|
|
61
|
+
let t = texts(
|
|
62
|
+
"record Other(w: Float) =\n fun score(self, n: Node) -> Float = n.x\n\nfun u(n: Node) -> Float = match n.kind\n Custom(a) => a.score(n)\n _ => 0.0\n",
|
|
63
|
+
);
|
|
64
|
+
assert!(t.is_empty(), "{t:?}");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[test]
|
|
68
|
+
fn pattern_bound_trait_receiver_resolves_despite_same_name_method() {
|
|
69
|
+
let t = texts(
|
|
70
|
+
"trait Drawer =\n fun draw(self, n: Node) -> Float\n\nrecord Pen(Drawer, w: Float) =\n override fun draw(self, n: Node) -> Float = n.x\n\nrecord Stage2(w: Float) =\n fun draw(self) -> Float = self.w\n\nenum Hold =\n | Mine(d: Drawer)\n\nown record Item(h: Hold)\n\nfun u(i: Item, n: Node) -> Float = match i.h\n Mine(d) => d.draw(n)\n",
|
|
71
|
+
);
|
|
72
|
+
assert!(t.is_empty(), "{t:?}");
|
|
73
|
+
}
|
plum-checker/tests/variant_pattern_tests.rs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
//! A bare variant name in a pattern: valid for payload-less variants and union-enum
|
|
3
|
+
//! members, a positioned error for an inline variant that has fields.
|
|
4
|
+
use plum_checker::checkSource;
|
|
5
|
+
use plum_core::unions::{beginPlan, hoistInlineVariants, resolveMembers, takePlan};
|
|
6
|
+
use plum_core::AstParser;
|
|
7
|
+
|
|
8
|
+
const DEFS: &str = "\
|
|
9
|
+
enum Act = Stop | MoveBy(dx: Int, dy: Int)
|
|
10
|
+
record Circle(r: Float)
|
|
11
|
+
record Dot(x: Int)
|
|
12
|
+
enum Shape = Circle | Dot
|
|
13
|
+
";
|
|
14
|
+
|
|
15
|
+
fn run(body: &str) -> Result<(), Vec<String>> {
|
|
16
|
+
let src = format!("{DEFS}\n{body}\n");
|
|
17
|
+
let mut parser = tree_sitter::Parser::new();
|
|
18
|
+
parser
|
|
19
|
+
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
20
|
+
.unwrap();
|
|
21
|
+
let tree = parser.parse(&src, None).unwrap();
|
|
22
|
+
assert!(!tree.root_node().has_error(), "parse errors:\n{src}");
|
|
23
|
+
let mut source = AstParser::new(&src).parseSource(tree.root_node());
|
|
24
|
+
let errs = hoistInlineVariants(&mut source);
|
|
25
|
+
assert!(errs.is_empty(), "{errs:?}");
|
|
26
|
+
let errs = resolveMembers(&mut source);
|
|
27
|
+
assert!(errs.is_empty(), "{errs:?}");
|
|
28
|
+
beginPlan();
|
|
29
|
+
let r = checkSource(&source).map_err(|e| e.into_iter().map(|x| x.message).collect());
|
|
30
|
+
takePlan();
|
|
31
|
+
r
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[test]
|
|
35
|
+
fn bare_payload_variant_pattern_rejected() {
|
|
36
|
+
let e = run("fun f(a: Act) -> Int =\n match a\n MoveBy => 1\n _ => 0")
|
|
37
|
+
.expect_err("bare payload variant must be rejected")
|
|
38
|
+
.join(" | ");
|
|
39
|
+
assert!(e.contains("MoveBy has fields"), "{e}");
|
|
40
|
+
run("fun f(a: Act) -> Int =\n match a\n MoveBy(_, _) => 1\n _ => 0").unwrap();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[test]
|
|
44
|
+
fn bare_unit_variant_pattern_ok() {
|
|
45
|
+
run("fun f(a: Act) -> Int =\n match a\n Stop => 1\n _ => 0").unwrap();
|
|
46
|
+
run("fun f(s: Shape) -> Int =\n match s\n Circle => 1\n Dot => 2").unwrap();
|
|
47
|
+
}
|
plum-core/src/union_lower.rs
CHANGED
|
@@ -677,7 +677,8 @@ fn synthesize(
|
|
|
677
677
|
.params
|
|
678
678
|
.iter()
|
|
679
679
|
.map(|p| Param {
|
|
680
|
+
// The forwarder hands the value straight on, so it takes it exactly as declared.
|
|
680
|
-
isOwn:
|
|
681
|
+
isOwn: p.isOwn,
|
|
681
682
|
name: p.name.clone(),
|
|
682
683
|
ty: substParam(&p.ty, &map),
|
|
683
684
|
default: p.default.clone(),
|
|
@@ -715,7 +716,7 @@ isOwn: false,
|
|
|
715
716
|
})
|
|
716
717
|
.collect();
|
|
717
718
|
Some(Fn {
|
|
718
|
-
ownSelf:
|
|
719
|
+
ownSelf: decl.ownSelf,
|
|
719
720
|
name: method.to_string(),
|
|
720
721
|
file: decl.file.clone(),
|
|
721
722
|
line: 0,
|
plum-core/tests/forwarder_own_test.rs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_core::ast::*;
|
|
3
|
+
use plum_core::union_lower::applyPlan;
|
|
4
|
+
use plum_core::unions::UnionPlan;
|
|
5
|
+
use plum_core::AstParser;
|
|
6
|
+
|
|
7
|
+
fn parse(src: &str) -> Source {
|
|
8
|
+
let mut parser = tree_sitter::Parser::new();
|
|
9
|
+
parser
|
|
10
|
+
.set_language(&tree_sitter_plum::LANGUAGE.into())
|
|
11
|
+
.unwrap();
|
|
12
|
+
let tree = parser.parse(src, None).unwrap();
|
|
13
|
+
assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
|
|
14
|
+
AstParser::new(src).parseSource(tree.root_node())
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const SRC: &str = "struct Circle(r: Float) =\n fun grow(self, own c: Circle, k: Float) -> Float = 1.0\nstruct Square(w: Float) =\n fun grow(self, own c: Circle, k: Float) -> Float = 2.0\nenum Shape = Circle | Square\nfun f(s: Shape, c: Circle) -> Float =\n s.grow(c, 1.0)\n";
|
|
18
|
+
|
|
19
|
+
#[test]
|
|
20
|
+
fn synthesized_forwarder_copies_own_flags() {
|
|
21
|
+
let mut s = parse(SRC);
|
|
22
|
+
let call = s
|
|
23
|
+
.items
|
|
24
|
+
.iter()
|
|
25
|
+
.find_map(|i| match i {
|
|
26
|
+
Item::Fn(f) if f.name == "f" => match &f.body {
|
|
27
|
+
FnBody::Block(b) => match &b.stmts[0] {
|
|
28
|
+
Stmt::Expr(e, ..) => Some(e as *const Expr as usize),
|
|
29
|
+
_ => None,
|
|
30
|
+
},
|
|
31
|
+
_ => None,
|
|
32
|
+
},
|
|
33
|
+
_ => None,
|
|
34
|
+
})
|
|
35
|
+
.unwrap();
|
|
36
|
+
let mut plan = UnionPlan::default();
|
|
37
|
+
plan.dispatches
|
|
38
|
+
.insert(call, vec!["Shape__Circle".into(), "Shape__Square".into()]);
|
|
39
|
+
applyPlan(&mut s, &plan);
|
|
40
|
+
let g = s
|
|
41
|
+
.items
|
|
42
|
+
.iter()
|
|
43
|
+
.find_map(|i| match i {
|
|
44
|
+
Item::Fn(f) if f.name == "grow" && f.type_param.as_deref() == Some("Shape") => Some(f),
|
|
45
|
+
_ => None,
|
|
46
|
+
})
|
|
47
|
+
.expect("no synthesized grow");
|
|
48
|
+
let own: Vec<bool> = g.params.iter().map(|p| p.isOwn).collect();
|
|
49
|
+
assert_eq!(own, vec![true, false]);
|
|
50
|
+
assert!(!g.ownSelf);
|
|
51
|
+
}
|
plum-examples/game_pure_scene.plum
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
# restarts after the game is over.
|
|
6
6
|
#
|
|
7
7
|
# What differs from game_scene.plum. There, actors are mutable objects that point at
|
|
8
|
-
# their parent and `stage.act` changes them in place. Here
|
|
8
|
+
# their parent and `stage.act` changes them in place. Here the scene is made of `Node` values
|
|
9
|
+
# (std `Group`, `Sprite`, `Label`, `Button` kinds, and `Custom` nodes whose behaviour is a
|
|
9
|
-
# `
|
|
10
|
+
# record embedding `Actor`: `Ship`, `Orb`, `Mine`, `Star`, `Panel`):
|
|
10
|
-
# `act` returns the next
|
|
11
|
+
# `act` returns the next node, `Stage.frame` returns a new `Stage`, and a change from the
|
|
11
12
|
# rules ("this orb is collected", "show the game-over panel") is a `SceneTree.modify`
|
|
12
13
|
# on an actor id that returns a new tree. The `World` record below is the mutable shell the
|
|
13
14
|
# host talks to: it keeps the current `Stage` and the run's state (score, lives, the ship's
|
|
@@ -141,232 +142,217 @@ fun shipStep(s: ShipState, ax: Float, ay: Float, dt: Float) -> ShipState =
|
|
|
141
142
|
fun newShip() -> ShipState =
|
|
142
143
|
ShipState(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, vx: 0.0, vy: 0.0, rotation: 0.0, hurt: 0.0)
|
|
143
144
|
|
|
144
|
-
# The centre of
|
|
145
|
+
# The centre of a node drawn as a sprite-sized box.
|
|
145
|
-
fun cx(
|
|
146
|
+
fun cx(n: Node) -> Float =
|
|
146
|
-
|
|
147
|
+
n.x + n.width / 2.0
|
|
147
148
|
|
|
148
|
-
fun cy(
|
|
149
|
+
fun cy(n: Node) -> Float =
|
|
149
|
-
|
|
150
|
+
n.y + n.height / 2.0
|
|
150
151
|
|
|
152
|
+
# Actor kinds of this game. Each holds only its own fields; position and size live on
|
|
151
|
-
#
|
|
153
|
+
# the `Node`. Each `create` returns a sprite-sized node; it takes the centre.
|
|
152
154
|
|
|
153
|
-
# A filled rectangle.
|
|
155
|
+
# A filled rectangle; its colour is the node's.
|
|
154
|
-
|
|
156
|
+
record Panel(Actor) =
|
|
155
|
-
Actor(id, x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, touchable, actions)) =
|
|
156
157
|
|
|
157
|
-
fun create(id: Int, x: Float, y: Float, w: Float, h: Float, c: Color) ->
|
|
158
|
+
fun create(id: Int, x: Float, y: Float, w: Float, h: Float, c: Color) -> Node =
|
|
158
|
-
Panel(
|
|
159
|
-
id: id, x: x, y: y, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
160
|
-
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: c, visible: true,
|
|
161
|
-
|
|
159
|
+
n := PureScene.custom(id, x, y, w, h, Panel())
|
|
160
|
+
n.color = c
|
|
161
|
+
n
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
override fun draw(self, m: Affine, tint: Color) =
|
|
163
|
+
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
165
164
|
p := m.apply(Vec2(x: 0.0, y: 0.0))
|
|
166
|
-
drawRect(p.x, p.y,
|
|
165
|
+
drawRect(p.x, p.y, n.width * m.scaleX(), n.height * m.scaleY(), tint)
|
|
167
166
|
|
|
168
167
|
# The player: only a picture that follows `ShipState`; `act` runs its animation clock.
|
|
169
|
-
own record Ship(
|
|
170
|
-
Actor(id, x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, touchable, actions),
|
|
171
|
-
|
|
168
|
+
record Ship(Actor, clock: Float, tex: Int) =
|
|
172
|
-
|
|
173
|
-
fun create(sheet: Int, s: ShipState) -> Ship =
|
|
174
|
-
Ship(
|
|
175
|
-
id: Id.Ship, x: s.x - Rules.Half, y: s.y - Rules.Half, width: Rules.Size, height: Rules.Size,
|
|
176
|
-
originX: Rules.Half, originY: Rules.Half, scaleX: 1.0, scaleY: 1.0, rotation: 0.0,
|
|
177
|
-
color: Color.White, visible: true, touchable: Disabled, actions: Slice[Action](),
|
|
178
|
-
clock: 0.0, tex: sheet)
|
|
179
169
|
|
|
170
|
+
fun create(sheet: Int, s: ShipState) -> Node =
|
|
171
|
+
n := PureScene.custom(Id.Ship, s.x - Rules.Half, s.y - Rules.Half, Rules.Size, Rules.Size, Ship(clock: 0.0, tex: sheet))
|
|
172
|
+
n.originX = Rules.Half
|
|
173
|
+
n.originY = Rules.Half
|
|
174
|
+
n
|
|
180
175
|
|
|
181
|
-
override fun act(own
|
|
176
|
+
override fun act(self, own n: Node, dt: Float) -> Node =
|
|
182
177
|
self.clock = self.clock + dt
|
|
183
|
-
|
|
178
|
+
n
|
|
184
179
|
|
|
185
|
-
override fun draw(self, m: Affine, tint: Color) =
|
|
180
|
+
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
186
|
-
c := m.apply(Vec2(x:
|
|
181
|
+
c := m.apply(Vec2(x: n.width / 2.0, y: n.height / 2.0))
|
|
187
182
|
drawCircle(c.x, c.y, 17.0 * m.scaleX(), Color(r: 0.4, g: 0.8, b: 1.0, a: 0.12 * tint.a))
|
|
188
183
|
frame := Float(frameOf(self.clock, 4, 14.0))
|
|
189
|
-
pureDrawSprite(m, tint,
|
|
184
|
+
pureDrawSprite(m, tint, n.width, n.height, self.tex, frame * Rules.Cell, 0.0, Rules.Cell, Rules.Cell)
|
|
190
185
|
|
|
191
186
|
# A glowing orb drifting about. `phase` offsets its animation from the other orbs.
|
|
192
|
-
own record Orb(
|
|
193
|
-
Actor(id, x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, touchable, actions),
|
|
194
|
-
|
|
187
|
+
record Orb(Actor, vx: Float, vy: Float, phase: Float, clock: Float, tex: Int) =
|
|
195
|
-
|
|
196
|
-
fun create(sheet: Int, phase: Float, id: Int, at: Motion) -> Orb =
|
|
197
|
-
Orb(
|
|
198
|
-
id: id, x: at.x - Rules.Half, y: at.y - Rules.Half, width: Rules.Size, height: Rules.Size,
|
|
199
|
-
originX: Rules.Half, originY: Rules.Half, scaleX: 1.0, scaleY: 1.0, rotation: 0.0,
|
|
200
|
-
color: Color.White, visible: true, touchable: Disabled, actions: Slice[Action](),
|
|
201
|
-
vx: at.vx, vy: at.vy, phase: phase, clock: 0.0, tex: sheet)
|
|
202
188
|
|
|
189
|
+
fun create(sheet: Int, phase: Float, id: Int, at: Motion) -> Node =
|
|
190
|
+
Orb.createAt(sheet, phase, id, at, 0.0)
|
|
203
191
|
|
|
192
|
+
# An orb that carries on an animation `clock`, so a respawn or freeze never jumps a frame.
|
|
204
|
-
fun
|
|
193
|
+
fun createAt(sheet: Int, phase: Float, id: Int, at: Motion, clock: Float) -> Node =
|
|
205
|
-
|
|
194
|
+
n := PureScene.custom(id, at.x - Rules.Half, at.y - Rules.Half, Rules.Size, Rules.Size,
|
|
195
|
+
Orb(vx: at.vx, vy: at.vy, phase: phase, clock: clock, tex: sheet))
|
|
196
|
+
n.originX = Rules.Half
|
|
197
|
+
n.originY = Rules.Half
|
|
198
|
+
n
|
|
206
199
|
|
|
207
|
-
override fun act(own
|
|
200
|
+
override fun act(self, own n: Node, dt: Float) -> Node =
|
|
208
|
-
m := driftOf(self.
|
|
201
|
+
m := driftOf(Motion(x: n.x + Rules.Half, y: n.y + Rules.Half, vx: self.vx, vy: self.vy), Rules.OrbRadius, dt)
|
|
209
202
|
self.clock = self.clock + dt
|
|
210
|
-
|
|
203
|
+
n.x = m.x - Rules.Half
|
|
211
|
-
|
|
204
|
+
n.y = m.y - Rules.Half
|
|
212
205
|
self.vx = m.vx
|
|
213
206
|
self.vy = m.vy
|
|
214
|
-
|
|
207
|
+
n
|
|
215
208
|
|
|
216
|
-
override fun draw(self, m: Affine, tint: Color) =
|
|
209
|
+
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
217
|
-
c := m.apply(Vec2(x:
|
|
210
|
+
c := m.apply(Vec2(x: n.width / 2.0, y: n.height / 2.0))
|
|
218
211
|
k := m.scaleX()
|
|
219
212
|
blendMode(Blend.Add)
|
|
220
213
|
drawCircle(c.x, c.y, 26.0 * k, Color(r: 0.35, g: 0.22, b: 0.0, a: 0.5 * tint.a))
|
|
221
214
|
drawCircle(c.x, c.y, 17.0 * k, Color(r: 0.6, g: 0.4, b: 0.05, a: 0.5 * tint.a))
|
|
222
215
|
blendMode(Blend.Normal)
|
|
223
216
|
frame := Float(frameOf(self.clock + self.phase, 4, 8.0))
|
|
224
|
-
pureDrawSprite(m, tint,
|
|
217
|
+
pureDrawSprite(m, tint, n.width, n.height, self.tex, frame * Rules.Cell, Rules.Cell, Rules.Cell, Rules.Cell)
|
|
225
218
|
|
|
226
219
|
# A mine drifting about, turning `spin` radians per second (0 once the game is over).
|
|
227
|
-
own record Mine(
|
|
228
|
-
Actor(id, x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, touchable, actions),
|
|
229
|
-
|
|
220
|
+
record Mine(Actor, vx: Float, vy: Float, spin: Float, clock: Float, tex: Int) =
|
|
230
221
|
|
|
231
|
-
fun create(sheet: Int, id: Int, at: Motion, spin: Float) ->
|
|
222
|
+
fun create(sheet: Int, id: Int, at: Motion, spin: Float) -> Node =
|
|
232
|
-
Mine(
|
|
233
|
-
id: id, x: at.x - Rules.Half, y: at.y - Rules.Half, width: Rules.Size, height: Rules.Size,
|
|
234
|
-
originX: Rules.Half, originY: Rules.Half, scaleX: 1.0, scaleY: 1.0, rotation: 0.0,
|
|
235
|
-
color: Color.White, visible: true, touchable: Disabled, actions: Slice[Action](),
|
|
236
|
-
|
|
223
|
+
Mine.createAt(sheet, id, at, spin, 0.0)
|
|
237
224
|
|
|
225
|
+
fun createAt(sheet: Int, id: Int, at: Motion, spin: Float, clock: Float) -> Node =
|
|
226
|
+
n := PureScene.custom(id, at.x - Rules.Half, at.y - Rules.Half, Rules.Size, Rules.Size,
|
|
227
|
+
Mine(vx: at.vx, vy: at.vy, spin: spin, clock: clock, tex: sheet))
|
|
228
|
+
n.originX = Rules.Half
|
|
229
|
+
n.originY = Rules.Half
|
|
230
|
+
n
|
|
238
231
|
|
|
239
|
-
fun motion(self) -> Motion =
|
|
240
|
-
Motion(x: self.x + Rules.Half, y: self.y + Rules.Half, vx: self.vx, vy: self.vy)
|
|
241
|
-
|
|
242
|
-
override fun act(own
|
|
232
|
+
override fun act(self, own n: Node, dt: Float) -> Node =
|
|
243
|
-
m := driftOf(self.
|
|
233
|
+
m := driftOf(Motion(x: n.x + Rules.Half, y: n.y + Rules.Half, vx: self.vx, vy: self.vy), Rules.MineRadius, dt)
|
|
244
234
|
self.clock = self.clock + dt
|
|
245
|
-
|
|
235
|
+
n.x = m.x - Rules.Half
|
|
246
|
-
|
|
236
|
+
n.y = m.y - Rules.Half
|
|
247
237
|
self.vx = m.vx
|
|
248
238
|
self.vy = m.vy
|
|
249
|
-
|
|
239
|
+
n.rotation = n.rotation + self.spin * dt
|
|
250
|
-
|
|
240
|
+
n
|
|
251
241
|
|
|
252
|
-
override fun draw(self, m: Affine, tint: Color) =
|
|
242
|
+
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
253
243
|
frame := Float(frameOf(self.clock, 4, 4.0))
|
|
254
|
-
pureDrawSprite(m, tint,
|
|
244
|
+
pureDrawSprite(m, tint, n.width, n.height, self.tex, frame * Rules.Cell, 2.0 * Rules.Cell, Rules.Cell, Rules.Cell)
|
|
255
245
|
|
|
256
246
|
# A background star that twinkles.
|
|
257
|
-
own record Star(
|
|
258
|
-
Actor(id, x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, touchable, actions),
|
|
259
|
-
|
|
247
|
+
record Star(Actor, seed: Float, clock: Float) =
|
|
260
248
|
|
|
261
|
-
fun create(i: Int) ->
|
|
249
|
+
fun create(i: Int) -> Node =
|
|
262
250
|
r := 1.0 + Float(i % 3) * 0.5
|
|
263
|
-
Star(
|
|
264
|
-
|
|
251
|
+
PureScene.custom(Id.Star + i, Float({i * 7919} % 800) - r, Float({i * 104729} % 600) - r, r * 2.0, r * 2.0,
|
|
265
|
-
width: r * 2.0, height: r * 2.0, originX: 0.0, originY: 0.0, scaleX: 1.0, scaleY: 1.0,
|
|
266
|
-
rotation: 0.0, color: Color.White, visible: true, touchable: Disabled, actions: Slice[Action](),
|
|
267
|
-
seed: Float(i), clock: 0.0)
|
|
252
|
+
Star(seed: Float(i), clock: 0.0))
|
|
268
|
-
|
|
269
253
|
|
|
270
|
-
override fun act(own
|
|
254
|
+
override fun act(self, own n: Node, dt: Float) -> Node =
|
|
271
255
|
self.clock = self.clock + dt
|
|
272
|
-
|
|
256
|
+
n
|
|
273
257
|
|
|
274
|
-
override fun draw(self, m: Affine, tint: Color) =
|
|
258
|
+
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
275
259
|
twinkle := 0.25 + 0.2 * Math.sin(self.clock * 2.0 + self.seed)
|
|
276
|
-
r :=
|
|
260
|
+
r := n.width / 2.0
|
|
277
261
|
c := m.apply(Vec2(x: r, y: r))
|
|
278
262
|
drawCircle(c.x, c.y, r * m.scaleX(), Color(r: 0.7, g: 0.8, b: 1.0, a: twinkle * tint.a))
|
|
279
263
|
|
|
280
264
|
# --- reading and changing the tree ---
|
|
281
265
|
|
|
282
266
|
# The child of `a` reached by `path` (not empty), borrowed from the tree.
|
|
283
|
-
fun nodeAt(a:
|
|
267
|
+
fun nodeAt(a: Node, path: Slice[Int], depth: Int) -> Node =
|
|
284
268
|
ks := kidsOf(a)
|
|
285
269
|
i := path.at(depth)
|
|
286
270
|
if depth + 1 >= path.length()
|
|
287
271
|
return ks.at(i)
|
|
288
272
|
nodeAt(ks.at(i), path, depth + 1)
|
|
289
273
|
|
|
290
|
-
# The
|
|
274
|
+
# The node with `id` below `root`, searched depth first, borrowed from the tree.
|
|
291
|
-
fun find(root:
|
|
275
|
+
fun find(root: Node, id: Int) -> Node =
|
|
292
276
|
path := SceneTree.findPath(root, id) ?: crash "no such actor"
|
|
293
277
|
nodeAt(root, path, 0)
|
|
294
278
|
|
|
295
|
-
fun kidsOf(
|
|
279
|
+
fun kidsOf(n: Node) -> Slice[Node] =
|
|
296
|
-
|
|
280
|
+
PureScene.childrenOf(n) ?: crash "not a group"
|
|
297
281
|
|
|
298
|
-
fun setVisible(own
|
|
282
|
+
fun setVisible(own n: Node, v: Bool) -> Node =
|
|
299
|
-
|
|
283
|
+
n.visible = v
|
|
300
|
-
|
|
284
|
+
n
|
|
301
285
|
|
|
302
286
|
# The group `g` with `k` added as its last child.
|
|
303
|
-
fun addKid(own g:
|
|
287
|
+
fun addKid(own g: Node, own k: Node) -> Node =
|
|
304
288
|
ks := kidsOf(g)
|
|
305
289
|
ks.push(k)
|
|
306
290
|
g
|
|
307
291
|
|
|
308
|
-
fun
|
|
292
|
+
fun actorClock(a: Actor) -> Float =
|
|
309
293
|
match a
|
|
310
|
-
Mine(_, _, _,
|
|
294
|
+
Mine(_, _, _, clock, _) => clock
|
|
311
|
-
Orb(_, _, _,
|
|
295
|
+
Orb(_, _, _, clock, _) => clock
|
|
312
|
-
Star(_,
|
|
296
|
+
Star(_, clock) => clock
|
|
313
297
|
_ => 0.0
|
|
314
298
|
|
|
299
|
+
fun clockOf(n: Node) -> Float =
|
|
300
|
+
match n.kind
|
|
301
|
+
Custom(a) => actorClock(a)
|
|
302
|
+
_ => 0.0
|
|
303
|
+
|
|
315
|
-
fun textOf(
|
|
304
|
+
fun textOf(n: Node) -> Str =
|
|
316
|
-
match
|
|
305
|
+
match n.kind
|
|
317
|
-
Label(
|
|
306
|
+
Label(text, _) => text
|
|
318
307
|
_ => ""
|
|
319
308
|
|
|
320
|
-
fun hudLabel(id: Int, text: Str, x: Float, y: Float, size: Float, c: Color) ->
|
|
309
|
+
fun hudLabel(id: Int, text: Str, x: Float, y: Float, size: Float, c: Color) -> Node =
|
|
321
310
|
l := PureScene.label(id, text, x, y, size)
|
|
322
311
|
l.color = c
|
|
323
312
|
l
|
|
324
313
|
|
|
325
|
-
fun icon(sheet: Int, i: Int) ->
|
|
314
|
+
fun icon(sheet: Int, i: Int) -> Node =
|
|
326
315
|
s := PureScene.sprite(Id.Icon + i, sheet, Rules.ArenaW - 40.0 - Float(i) * 36.0, 12.0, 32.0, 32.0)
|
|
327
|
-
s.srcX = 0.0
|
|
328
|
-
s.srcY = 0.0
|
|
329
|
-
s.
|
|
316
|
+
s.kind = Sprite(tex: sheet, srcX: 0.0, srcY: 0.0, srcW: Rules.Cell, srcH: Rules.Cell)
|
|
330
|
-
s.srcH = Rules.Cell
|
|
331
317
|
s
|
|
332
318
|
|
|
333
319
|
# The whole scene: backdrop, stars, mines, orbs, ship and HUD layers, in drawing order.
|
|
334
320
|
# The layer groups are 0 x 0 so they never swallow a click; the HUD holds the
|
|
335
321
|
# game-over overlay with its restart button.
|
|
336
|
-
fun buildRoot(rng: Random, sheet: Int, best: Int) ->
|
|
322
|
+
fun buildRoot(rng: Random, sheet: Int, best: Int) -> Node =
|
|
337
323
|
ship := newShip()
|
|
338
|
-
stars := Slice[
|
|
324
|
+
stars := Slice[Node]()
|
|
339
325
|
for i := range 40
|
|
340
326
|
stars.push(Star.create(i))
|
|
341
|
-
mines := Slice[
|
|
327
|
+
mines := Slice[Node]()
|
|
342
|
-
orbs := Slice[
|
|
328
|
+
orbs := Slice[Node]()
|
|
343
329
|
for i := range Rules.OrbCount
|
|
344
330
|
orbs.push(Orb.create(sheet, Float(i) * 0.1, Id.Orb + i, spawn(rng, ship.x, ship.y, 120.0, 40.0)))
|
|
345
331
|
for i := range mineCount(0)
|
|
346
332
|
mines.push(Mine.create(sheet, Id.Mine + i, spawn(rng, ship.x, ship.y, 220.0, 90.0), 2.0))
|
|
347
|
-
over := Slice[Actor]()
|
|
348
|
-
over.push(Panel.create(Id.OverlayPanel, 0.0, 230.0, Rules.ArenaW, 170.0, Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6)))
|
|
349
|
-
over.push(hudLabel(Id.OverTitle, "Game over", 300.0, 244.0, 48.0, Color.Red))
|
|
350
|
-
over.push(hudLabel(Id.OverHint, "Press Enter to play again", 270.0, 300.0, 24.0, Color.White))
|
|
351
333
|
btn := PureScene.button(Id.Restart, "Again", sheet, sheet, 340.0, 342.0, 120.0, 44.0)
|
|
352
334
|
btn.color = Color(r: 0.5, g: 0.8, b: 1.0, a: 0.9)
|
|
353
335
|
btn.visible = false
|
|
354
|
-
over
|
|
336
|
+
over := Slice(
|
|
337
|
+
Panel.create(Id.OverlayPanel, 0.0, 230.0, Rules.ArenaW, 170.0, Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6)),
|
|
338
|
+
hudLabel(Id.OverTitle, "Game over", 300.0, 244.0, 48.0, Color.Red),
|
|
339
|
+
hudLabel(Id.OverHint, "Press Enter to play again", 270.0, 300.0, 24.0, Color.White),
|
|
340
|
+
btn)
|
|
355
341
|
overlay := PureScene.group(Id.Overlay, 0.0, 0.0, 0.0, 0.0, over)
|
|
356
342
|
overlay.visible = false
|
|
357
|
-
hud := Slice
|
|
343
|
+
hud := Slice(
|
|
358
|
-
|
|
344
|
+
hudLabel(Id.Score, "Score 0", 16.0, 12.0, 28.0, Color.White),
|
|
359
|
-
|
|
345
|
+
hudLabel(Id.Best, "Best {best}", 16.0, 44.0, 20.0, Color.Yellow))
|
|
360
346
|
for i := range Rules.StartLives
|
|
361
347
|
hud.push(icon(sheet, i))
|
|
362
348
|
hud.push(overlay)
|
|
363
|
-
layers := Slice
|
|
349
|
+
layers := Slice(
|
|
364
|
-
|
|
350
|
+
Panel.create(Id.Backdrop, 0.0, 0.0, Rules.ArenaW, Rules.ArenaH, Color.rgb(0.07, 0.08, 0.16)),
|
|
365
|
-
|
|
351
|
+
PureScene.group(Id.StarLayer, 0.0, 0.0, 0.0, 0.0, stars),
|
|
366
|
-
|
|
352
|
+
PureScene.group(Id.MineLayer, 0.0, 0.0, 0.0, 0.0, mines),
|
|
367
|
-
|
|
353
|
+
PureScene.group(Id.OrbLayer, 0.0, 0.0, 0.0, 0.0, orbs),
|
|
368
|
-
|
|
354
|
+
Ship.create(sheet, ship),
|
|
369
|
-
|
|
355
|
+
PureScene.group(Id.Hud, 0.0, 0.0, 0.0, 0.0, hud))
|
|
370
356
|
PureScene.group(Id.Root, 0.0, 0.0, Rules.ArenaW, Rules.ArenaH, layers)
|
|
371
357
|
|
|
372
358
|
# --- the world ---
|
|
@@ -398,7 +384,7 @@ fun newWorld(seed: Int, sheet: Int) -> World =
|
|
|
398
384
|
lives: Rules.StartLives, over: false, clock: 0.0, wasDown: false, sheet: sheet,
|
|
399
385
|
pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
|
|
400
386
|
|
|
401
|
-
fun swap(w: World, id: Int, f: fn(
|
|
387
|
+
fun swap(w: World, id: Int, f: fn(Node) -> Node) =
|
|
402
388
|
w.stage.root = SceneTree.modify(w.stage.root, id, f)
|
|
403
389
|
|
|
404
390
|
# Starts a fresh game, keeping the best score, the camera and the random stream.
|
|
@@ -436,32 +422,29 @@ fun freeze(w: World) =
|
|
|
436
422
|
sheet := w.sheet
|
|
437
423
|
swap(w, id, |a| stillMine(sheet, id, a))
|
|
438
424
|
|
|
439
|
-
fun orbActor(sheet: Int, phase: Float, id: Int, at: Motion) ->
|
|
425
|
+
fun orbActor(sheet: Int, phase: Float, id: Int, at: Motion) -> Node =
|
|
440
426
|
Orb.create(sheet, phase, id, at)
|
|
441
427
|
|
|
442
428
|
# A new orb that carries on `old`'s animation clock, so a respawn or freeze never jumps a frame.
|
|
443
|
-
fun orbKeepClock(sheet: Int, phase: Float, id: Int, at: Motion, old:
|
|
429
|
+
fun orbKeepClock(sheet: Int, phase: Float, id: Int, at: Motion, old: Node) -> Node =
|
|
444
|
-
|
|
430
|
+
Orb.createAt(sheet, phase, id, at, clockOf(old))
|
|
445
|
-
o.clock = clockOf(old)
|
|
446
|
-
o
|
|
447
431
|
|
|
448
|
-
fun mineActor(sheet: Int, id: Int, at: Motion, spin: Float) ->
|
|
432
|
+
fun mineActor(sheet: Int, id: Int, at: Motion, spin: Float) -> Node =
|
|
449
433
|
Mine.create(sheet, id, at, spin)
|
|
450
434
|
|
|
451
|
-
fun stillMine(sheet: Int, id: Int, a:
|
|
435
|
+
fun stillMine(sheet: Int, id: Int, a: Node) -> Node =
|
|
452
|
-
m := Mine.
|
|
436
|
+
m := Mine.createAt(sheet, id, Motion(x: cx(a), y: cy(a), vx: 0.0, vy: 0.0), 0.0, clockOf(a))
|
|
453
437
|
m.rotation = a.rotation
|
|
454
|
-
m.clock = clockOf(a)
|
|
455
438
|
m
|
|
456
439
|
|
|
457
|
-
# Moves the ship
|
|
440
|
+
# Moves the ship node to the ship state; it blinks while it is hurt, and is hidden once the game is over.
|
|
458
441
|
fun showShip(w: World) =
|
|
459
442
|
blink := w.ship.hurt > 0.0 && frameOf(w.clock, 2, 10.0) == 0
|
|
460
443
|
visible := !w.over && !blink
|
|
461
444
|
s := w.ship
|
|
462
445
|
swap(w, Id.Ship, |a| shipActor(a, s, visible))
|
|
463
446
|
|
|
464
|
-
fun shipActor(own a:
|
|
447
|
+
fun shipActor(own a: Node, s: ShipState, visible: Bool) -> Node =
|
|
465
448
|
a.x = s.x - Rules.Half
|
|
466
449
|
a.y = s.y - Rules.Half
|
|
467
450
|
a.rotation = s.rotation
|
|
@@ -662,12 +645,13 @@ test "drifters bounce off the walls"
|
|
|
662
645
|
b := driftOf(Motion(x: 795.0, y: 300.0, vx: 100.0, vy: 0.0), Rules.OrbRadius, 0.1)
|
|
663
646
|
assert b.vx == -100.0
|
|
664
647
|
assert b.x == Rules.ArenaW - 10.0
|
|
665
|
-
orb := Orb.create(-1, 0.0, Id.Orb, Motion(x: 795.0, y: 300.0, vx: 100.0, vy: 0.0))
|
|
648
|
+
orb := Orb.create(-1, 0.0, Id.Orb, Motion(x: 795.0, y: 300.0, vx: 100.0, vy: 0.0))
|
|
649
|
+
moved := SceneTree.update(orb, 0.1)
|
|
666
|
-
assert near(cx(
|
|
650
|
+
assert near(cx(moved), Rules.ArenaW - 10.0)
|
|
667
651
|
|
|
668
652
|
test "a mine spins as it drifts"
|
|
669
653
|
mine := Mine.create(-1, Id.Mine, Motion(x: 400.0, y: 300.0, vx: 10.0, vy: 0.0), 2.0)
|
|
670
|
-
next :=
|
|
654
|
+
next := SceneTree.update(mine, 0.5)
|
|
671
655
|
assert near(next.rotation, 1.0)
|
|
672
656
|
assert near(cx(next), 405.0)
|
|
673
657
|
|
plum-game/web-test/fixtures/stress_pure_scene.plum
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# The stress scene of stress_scene.plum on the PURE scene graph (std/game/PureScene):
|
|
2
2
|
# 5000 sprites in 50 slowly rotating groups, 500 rects and 200 circles (sprites of a
|
|
3
3
|
# 1x1 white texture), 50 texts with a changing number, drawn through a camera. Every
|
|
4
|
-
#
|
|
4
|
+
# node is an own value: each frame changes the tree in place with SceneTree.update. Group and
|
|
5
5
|
# sprite spin comes from `forever(rotateBy(...))` actions; drifting and counting
|
|
6
6
|
# actors do theirs in `act`.
|
|
7
7
|
module stress_pure_scene
|
|
@@ -43,71 +43,60 @@ fun caption(m: Affine, tint: Color, s: Str, size: Float) =
|
|
|
43
43
|
drawText(s, p.x, p.y, size * m.scaleY(), tint)
|
|
44
44
|
|
|
45
45
|
# A square of the white texture that moves by (dx, dy) per frame and wraps.
|
|
46
|
-
own record Drifter(
|
|
47
|
-
Actor(id, x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, touchable, actions),
|
|
48
|
-
|
|
46
|
+
record Drifter(Actor, tex: Int, dx: Float, dy: Float) =
|
|
49
|
-
override fun act(own
|
|
47
|
+
override fun act(self, own n: Node, dt: Float) -> Node =
|
|
50
|
-
|
|
48
|
+
n.x = n.x + self.dx
|
|
51
|
-
|
|
49
|
+
n.y = n.y + self.dy
|
|
52
|
-
if
|
|
50
|
+
if n.x > Float(Counts.WorldW)
|
|
53
|
-
|
|
51
|
+
n.x = n.x - Float(Counts.WorldW)
|
|
54
|
-
if
|
|
52
|
+
if n.y > Float(Counts.WorldH)
|
|
55
|
-
|
|
53
|
+
n.y = n.y - Float(Counts.WorldH)
|
|
56
|
-
|
|
54
|
+
n
|
|
57
|
-
|
|
55
|
+
|
|
58
|
-
override fun draw(self, m: Affine, tint: Color) =
|
|
56
|
+
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
59
|
-
quad(m, tint,
|
|
57
|
+
quad(m, tint, n.width, n.height, self.tex, 0.0, 0.0, 0.0, 0.0)
|
|
60
58
|
|
|
61
59
|
# Shows "n <count>" and counts up each frame.
|
|
62
|
-
own record CountLabel(
|
|
63
|
-
Actor(id, x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, touchable, actions),
|
|
64
|
-
|
|
60
|
+
record CountLabel(Actor, n: Int, text: Str) =
|
|
65
|
-
override fun act(own
|
|
61
|
+
override fun act(self, own n: Node, dt: Float) -> Node =
|
|
66
62
|
self.n = self.n + 1
|
|
67
63
|
self.text = "n {self.n}"
|
|
68
|
-
|
|
64
|
+
n
|
|
69
65
|
|
|
70
|
-
override fun draw(self, m: Affine, tint: Color) =
|
|
66
|
+
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
71
67
|
caption(m, tint, self.text, 14.0)
|
|
72
68
|
|
|
73
|
-
fun drifter(id: Int, tex: Int, c: Color, x: Float, y: Float, w: Float, h: Float, dx: Float, dy: Float) ->
|
|
69
|
+
fun drifter(id: Int, tex: Int, c: Color, x: Float, y: Float, w: Float, h: Float, dx: Float, dy: Float) -> Node =
|
|
74
|
-
d := Drifter(
|
|
75
|
-
id: id, x: x, y: y, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
76
|
-
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: c, visible: true,
|
|
77
|
-
|
|
70
|
+
d := Drifter(tex: tex, dx: dx, dy: dy)
|
|
78
71
|
me: Actor := d
|
|
72
|
+
n := PureScene.custom(id, x, y, w, h, me)
|
|
73
|
+
n.color = c
|
|
79
|
-
|
|
74
|
+
n
|
|
80
75
|
|
|
81
|
-
fun countLabel(id: Int, n: Int, x: Float, y: Float) ->
|
|
76
|
+
fun countLabel(id: Int, n: Int, x: Float, y: Float) -> Node =
|
|
82
|
-
l := CountLabel(
|
|
83
|
-
id: id, x: x, y: y, width: 60.0, height: 14.0, originX: 0.0, originY: 0.0,
|
|
84
|
-
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
85
|
-
|
|
77
|
+
l := CountLabel(n: n, text: "n {n}")
|
|
86
78
|
me: Actor := l
|
|
87
|
-
me
|
|
79
|
+
PureScene.custom(id, x, y, 60.0, 14.0, me)
|
|
88
80
|
|
|
89
81
|
fun spinning(rate: Float) -> Slice[Action] =
|
|
90
82
|
acts := Slice[Action]()
|
|
91
83
|
acts.push(Actions.forever(Actions.rotateBy(rate, 1.0)))
|
|
92
84
|
acts
|
|
93
85
|
|
|
94
|
-
fun dancer(id: Int, sheet: Int, j: Int, i: Int) ->
|
|
86
|
+
fun dancer(id: Int, sheet: Int, j: Int, i: Int) -> Node =
|
|
95
87
|
s := PureScene.sprite(id, sheet, Float({j * j * 13 + j * 37} % 150), Float({j * j * 29 + j * 53} % 170), 16.0, 16.0)
|
|
96
88
|
s.originX = 8.0
|
|
97
89
|
s.originY = 8.0
|
|
98
90
|
s.rotation = Float(i)
|
|
99
|
-
s.
|
|
91
|
+
s.kind = Sprite(tex: sheet, srcX: Float(i % 4 * 16), srcY: 0.0, srcW: 16.0, srcH: 16.0)
|
|
100
|
-
s.srcW = 16.0
|
|
101
|
-
s.srcH = 16.0
|
|
102
92
|
s.actions = spinning(1.2)
|
|
103
|
-
me: Actor := s
|
|
104
|
-
|
|
93
|
+
s
|
|
105
94
|
|
|
106
95
|
fun buildWorld(sheet: Int, white: Int) -> World =
|
|
107
|
-
root := Slice[
|
|
96
|
+
root := Slice[Node]()
|
|
108
97
|
per := Counts.Sprites / Counts.Groups
|
|
109
98
|
for gi := range Counts.Groups
|
|
110
|
-
kids := Slice[
|
|
99
|
+
kids := Slice[Node]()
|
|
111
100
|
for j := range per
|
|
112
101
|
i := gi * per + j
|
|
113
102
|
kids.push(dancer(1000 + i, sheet, j, i))
|
|
@@ -115,15 +104,14 @@ fun buildWorld(sheet: Int, white: Int) -> World =
|
|
|
115
104
|
g.originX = 75.0
|
|
116
105
|
g.originY = 85.0
|
|
117
106
|
g.actions = spinning(0.24)
|
|
118
|
-
me: Actor := g
|
|
119
|
-
root.push(
|
|
107
|
+
root.push(g)
|
|
120
108
|
for i := range Counts.Rects
|
|
121
109
|
root.push(drifter(10000 + i, white, hue(i), Float(i * 61 % Counts.WorldW), Float(i * 29 % Counts.WorldH), 12.0, 8.0, 2.0, 0.0))
|
|
122
110
|
for i := range Counts.Circles
|
|
123
111
|
root.push(drifter(20000 + i, white, hue(i + 3), Float(i * 83 % Counts.WorldW), Float(i * 47 % Counts.WorldH), 12.0, 12.0, 3.0, 1.0))
|
|
124
112
|
for i := range Counts.Texts
|
|
125
113
|
root.push(countLabel(30000 + i, i, Float(i * 30 % Counts.WorldW), Float(i * 17 % Counts.WorldH)))
|
|
126
|
-
top
|
|
114
|
+
top := PureScene.group(1, 0.0, 0.0, Float(Counts.WorldW), Float(Counts.WorldH), root)
|
|
127
115
|
cam := Affine(a: 0.8, b: 0.0, c: 0.0, d: 0.8, e: 0.0, f: 0.0)
|
|
128
116
|
World(sheet: sheet, stage: Stage(root: top, camera: cam, captured: null))
|
|
129
117
|
|
|
@@ -151,7 +139,7 @@ test "ten frames of update run"
|
|
|
151
139
|
w := buildWorld(0, 0)
|
|
152
140
|
for i := range 10
|
|
153
141
|
update(w, 0.016)
|
|
154
|
-
if k := w.stage.root
|
|
142
|
+
if k := PureScene.childrenOf(w.stage.root)
|
|
155
143
|
assert k.length() == 50 + 500 + 200 + 50
|
|
156
144
|
assert {k.at(50).x - 20.0}.abs() < 0.000001
|
|
157
145
|
else
|
plum-std/game/Actions.plum
CHANGED
|
@@ -14,20 +14,18 @@ import std/Float
|
|
|
14
14
|
# The animated part of an actor's transform.
|
|
15
15
|
struct Pose(x: Float, y: Float, rotation: Float, scaleX: Float, scaleY: Float)
|
|
16
16
|
|
|
17
|
+
# One action. `elapsed` and `current` (the running lap of a `Forever`, null
|
|
18
|
+
# before it starts) are the mutable state; `kind` is the immutable description.
|
|
19
|
+
# A `Forever`'s `template` is the pristine action restarted each lap.
|
|
20
|
+
record Action(elapsed: Float, current: Action?, kind: ActionKind)
|
|
21
|
+
|
|
22
|
+
enum ActionKind =
|
|
17
|
-
|
|
23
|
+
| MoveBy(dx: Float, dy: Float, duration: Float)
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
| RotateBy(radians: Float, duration: Float)
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
| Delay(duration: Float)
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
| Sequence(steps: Slice[Action])
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
| Parallel(steps: Slice[Action])
|
|
26
|
-
|
|
27
|
-
# `template` is the pristine action restarted each lap, `current` the lap in progress.
|
|
28
|
-
|
|
28
|
+
| Forever(template: Action)
|
|
29
|
-
|
|
30
|
-
enum Action = MoveBy | RotateBy | Delay | Sequence | Parallel | Forever
|
|
31
29
|
|
|
32
30
|
# How much of a timed action one step covers. A zero-length action finishes at
|
|
33
31
|
# once and applies all of itself (no 0/0).
|
|
@@ -39,40 +37,41 @@ struct Step(pose: Pose, spare: Float, done: Bool)
|
|
|
39
37
|
|
|
40
38
|
primitive Actions =
|
|
41
39
|
fun moveBy(dx: Float, dy: Float, secs: Float) -> Action =
|
|
42
|
-
MoveBy(dx: dx, dy: dy, duration: secs
|
|
40
|
+
Action(elapsed: 0.0, current: null, kind: MoveBy(dx: dx, dy: dy, duration: secs))
|
|
43
41
|
|
|
44
42
|
fun rotateBy(radians: Float, secs: Float) -> Action =
|
|
45
|
-
RotateBy(radians: radians, duration: secs
|
|
43
|
+
Action(elapsed: 0.0, current: null, kind: RotateBy(radians: radians, duration: secs))
|
|
46
44
|
|
|
47
45
|
fun delay(secs: Float) -> Action =
|
|
48
|
-
|
|
46
|
+
Action(elapsed: 0.0, current: null, kind: Delay(duration: secs))
|
|
49
47
|
|
|
50
48
|
fun sequence(steps: Slice[Action]) -> Action =
|
|
51
|
-
Sequence(steps: steps)
|
|
49
|
+
Action(elapsed: 0.0, current: null, kind: Sequence(steps: steps))
|
|
52
50
|
|
|
53
51
|
fun parallel(steps: Slice[Action]) -> Action =
|
|
54
|
-
Parallel(steps: steps)
|
|
52
|
+
Action(elapsed: 0.0, current: null, kind: Parallel(steps: steps))
|
|
55
53
|
|
|
56
54
|
fun forever(a: Action) -> Action =
|
|
57
|
-
Forever(template: Actions.fresh(a)
|
|
55
|
+
Action(elapsed: 0.0, current: a, kind: Forever(template: Actions.fresh(a)))
|
|
58
56
|
|
|
59
57
|
# An unstarted copy of `a` (elapsed 0, all steps present).
|
|
60
58
|
fun fresh(a: Action) -> Action =
|
|
61
|
-
match a
|
|
59
|
+
match a.kind
|
|
62
|
-
MoveBy
|
|
60
|
+
MoveBy(dx, dy, duration) => Actions.moveBy(dx, dy, duration)
|
|
63
|
-
RotateBy
|
|
61
|
+
RotateBy(radians, duration) => Actions.rotateBy(radians, duration)
|
|
64
|
-
Delay =>
|
|
62
|
+
Delay(duration) => Actions.delay(duration)
|
|
65
|
-
Sequence =>
|
|
63
|
+
Sequence(steps) =>
|
|
66
64
|
out := Slice[Action]()
|
|
67
|
-
for i := range
|
|
65
|
+
for i := range steps.length()
|
|
68
|
-
out.push(Actions.fresh(
|
|
66
|
+
out.push(Actions.fresh(steps.at(i)))
|
|
69
|
-
|
|
67
|
+
Actions.sequence(out)
|
|
70
|
-
Parallel =>
|
|
68
|
+
Parallel(steps) =>
|
|
71
69
|
out := Slice[Action]()
|
|
72
|
-
for i := range
|
|
70
|
+
for i := range steps.length()
|
|
73
|
-
out.push(Actions.fresh(
|
|
71
|
+
out.push(Actions.fresh(steps.at(i)))
|
|
74
|
-
|
|
72
|
+
Actions.parallel(out)
|
|
73
|
+
Forever(template) =>
|
|
75
|
-
|
|
74
|
+
Action(elapsed: 0.0, current: Actions.fresh(template), kind: Forever(template: template))
|
|
76
75
|
|
|
77
76
|
fun tick(duration: Float, elapsed: Float, dt: Float) -> Tick =
|
|
78
77
|
if duration <= 0.0
|
|
@@ -81,43 +80,50 @@ primitive Actions =
|
|
|
81
80
|
Tick(used: used, frac: used / duration, done: elapsed + used >= duration)
|
|
82
81
|
|
|
83
82
|
fun stepAction(p: Pose, a: Action, dt: Float) -> Step =
|
|
84
|
-
match a
|
|
83
|
+
match a.kind
|
|
85
|
-
MoveBy =>
|
|
84
|
+
MoveBy(dx, dy, duration) =>
|
|
86
|
-
k := Actions.tick(
|
|
85
|
+
k := Actions.tick(duration, a.elapsed, dt)
|
|
87
86
|
a.elapsed = a.elapsed + k.used
|
|
88
|
-
q := Pose(x: p.x +
|
|
87
|
+
q := Pose(x: p.x + dx * k.frac, y: p.y + dy * k.frac, rotation: p.rotation, scaleX: p.scaleX, scaleY: p.scaleY)
|
|
89
88
|
Step(pose: q, spare: if k.done then dt - k.used else 0.0, done: k.done)
|
|
90
|
-
RotateBy =>
|
|
89
|
+
RotateBy(radians, duration) =>
|
|
91
|
-
k := Actions.tick(
|
|
90
|
+
k := Actions.tick(duration, a.elapsed, dt)
|
|
92
91
|
a.elapsed = a.elapsed + k.used
|
|
93
|
-
q := Pose(x: p.x, y: p.y, rotation: p.rotation +
|
|
92
|
+
q := Pose(x: p.x, y: p.y, rotation: p.rotation + radians * k.frac, scaleX: p.scaleX, scaleY: p.scaleY)
|
|
94
93
|
Step(pose: q, spare: if k.done then dt - k.used else 0.0, done: k.done)
|
|
95
|
-
Delay =>
|
|
94
|
+
Delay(duration) =>
|
|
96
|
-
k := Actions.tick(
|
|
95
|
+
k := Actions.tick(duration, a.elapsed, dt)
|
|
97
96
|
a.elapsed = a.elapsed + k.used
|
|
98
97
|
Step(pose: p, spare: if k.done then dt - k.used else 0.0, done: k.done)
|
|
99
|
-
Sequence =>
|
|
98
|
+
Sequence(steps) =>
|
|
100
|
-
if
|
|
99
|
+
if steps.length() == 0
|
|
101
100
|
return Step(pose: p, spare: dt, done: true)
|
|
102
|
-
s := Actions.stepAction(p,
|
|
101
|
+
s := Actions.stepAction(p, steps.at(0), dt)
|
|
103
102
|
if !s.done
|
|
104
103
|
return Step(pose: s.pose, spare: 0.0, done: false)
|
|
105
104
|
# The rest starts in this same frame with the time left over; the list
|
|
106
105
|
# is one shorter on every call, so this terminates.
|
|
107
|
-
|
|
106
|
+
steps.removeAt(0)
|
|
108
107
|
Actions.stepAction(s.pose, a, s.spare)
|
|
109
|
-
Parallel =>
|
|
108
|
+
Parallel(steps) =>
|
|
110
|
-
Actions.stepAll(p,
|
|
109
|
+
Actions.stepAll(p, steps, dt)
|
|
111
|
-
Forever =>
|
|
110
|
+
Forever(template) =>
|
|
112
111
|
# Restarts at most once per step, with the spare time, so a zero-length
|
|
113
112
|
# template cannot spin forever inside one step.
|
|
113
|
+
if a.current == null
|
|
114
|
+
a.current = Actions.fresh(template)
|
|
115
|
+
lap := template
|
|
116
|
+
if c := a.current
|
|
117
|
+
lap = c
|
|
114
|
-
s := Actions.stepAction(p,
|
|
118
|
+
s := Actions.stepAction(p, lap, dt)
|
|
115
119
|
if !s.done
|
|
116
120
|
return Step(pose: s.pose, spare: 0.0, done: false)
|
|
117
|
-
|
|
121
|
+
next := Actions.fresh(template)
|
|
118
|
-
r := Actions.stepAction(s.pose,
|
|
122
|
+
r := Actions.stepAction(s.pose, next, s.spare)
|
|
119
123
|
if r.done
|
|
120
|
-
a.current = Actions.fresh(
|
|
124
|
+
a.current = Actions.fresh(template)
|
|
125
|
+
else
|
|
126
|
+
a.current = next
|
|
121
127
|
Step(pose: r.pose, spare: 0.0, done: false)
|
|
122
128
|
|
|
123
129
|
# Steps every action side by side in place and drops the finished ones by
|
|
@@ -146,13 +152,13 @@ fun actNear(a: Float, b: Float) -> Bool =
|
|
|
146
152
|
{a - b}.abs() < 0.000001
|
|
147
153
|
|
|
148
154
|
fun actSeqLen(a: Action) -> Int =
|
|
149
|
-
match a
|
|
155
|
+
match a.kind
|
|
150
|
-
Sequence =>
|
|
156
|
+
Sequence(steps) => steps.length()
|
|
151
157
|
_ => 0 - 1
|
|
152
158
|
|
|
153
159
|
fun actElapsed(a: Action) -> Float =
|
|
154
|
-
match a
|
|
160
|
+
match a.kind
|
|
155
|
-
MoveBy => a.elapsed
|
|
161
|
+
MoveBy(_, _, _) => a.elapsed
|
|
156
162
|
_ => 0.0 - 1.0
|
|
157
163
|
|
|
158
164
|
fun actPose0() -> Pose =
|
plum-std/game/PureScene.plum
CHANGED
|
@@ -10,11 +10,12 @@ import std/game/Affine
|
|
|
10
10
|
import std/game/Game
|
|
11
11
|
import std/game/Actions
|
|
12
12
|
|
|
13
|
-
# A pure scene graph:
|
|
13
|
+
# A pure scene graph: a node is an own record (`Node`) holding a `Transform` and
|
|
14
|
-
#
|
|
14
|
+
# an `enum Kind`. Group, Sprite, Label and Button are built in; games add their
|
|
15
|
+
# own behaviour with `Custom(actor: Actor)`, a behaviour-only trait their records
|
|
16
|
+
# embed (like Rust's `Box<dyn Actor>`). `onPointer` takes the node, changes it in
|
|
15
|
-
#
|
|
17
|
+
# place and hands it back; readers borrow. Only `drawTree` talks to the host,
|
|
16
|
-
# Only `drawTree` talks to the host, through `std/game/Game`.
|
|
17
|
-
#
|
|
18
|
+
# through `std/game/Game`. Actions and tree-wide updates live in std/game/SceneTree.
|
|
18
19
|
|
|
19
20
|
enum Touchable =
|
|
20
21
|
| Enabled
|
|
@@ -38,104 +39,55 @@ enum UiKind =
|
|
|
38
39
|
|
|
39
40
|
record UiEvent(id: Int, kind: UiKind)
|
|
40
41
|
|
|
41
|
-
# The
|
|
42
|
+
# The node's position, size, origin, scale and rotation.
|
|
42
|
-
|
|
43
|
+
record Transform(x: Float, y: Float, width: Float, height: Float, originX: Float, originY: Float, scaleX: Float, scaleY: Float, rotation: Float) =
|
|
43
|
-
|
|
44
|
+
|
|
44
|
-
# The
|
|
45
|
+
# The transform into the parent's coordinates: translate to the position,
|
|
46
|
+
# rotate and scale about the origin.
|
|
45
|
-
fun
|
|
47
|
+
fun localMatrix(self) -> Affine =
|
|
48
|
+
cs := Math.cos(self.rotation)
|
|
49
|
+
sn := Math.sin(self.rotation)
|
|
46
|
-
|
|
50
|
+
ma := cs * self.scaleX
|
|
47
|
-
|
|
51
|
+
mb := sn * self.scaleX
|
|
48
|
-
|
|
52
|
+
mc := 0.0 - sn * self.scaleY
|
|
49
|
-
|
|
53
|
+
md := cs * self.scaleY
|
|
50
|
-
|
|
54
|
+
Affine(
|
|
51
|
-
x: Float, y: Float, width: Float, height: Float,
|
|
52
|
-
originX: Float, originY: Float,
|
|
53
|
-
|
|
55
|
+
a: ma, b: mb, c: mc, d: md,
|
|
54
|
-
rotation: Float,
|
|
55
|
-
color: Color,
|
|
56
|
-
visible: Bool,
|
|
57
|
-
touchable: Touchable,
|
|
58
|
-
actions: Slice[Action]) =
|
|
59
|
-
|
|
60
|
-
# This actor after `dt` seconds of its own behaviour; the default is itself.
|
|
61
|
-
fun act(own self, dt: Float) -> Actor = self
|
|
62
|
-
|
|
63
|
-
# The children of a group; null for a leaf.
|
|
64
|
-
fun kids(self) -> Slice[Actor]? = null
|
|
65
|
-
|
|
66
|
-
# Paints this actor only, at its stage transform `m` with the combined `tint`.
|
|
67
|
-
fun draw(self, m: Affine, tint: Color) =
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
# Whether `p`, in this actor's own coordinates, is on it.
|
|
71
|
-
fun hit(self, p: Vec2) -> Bool =
|
|
72
|
-
self.touchable == Enabled && p.x >= 0.0 && p.x <= self.width && p.y >= 0.0 && p.y <= self.height
|
|
73
|
-
|
|
74
|
-
# Handles a pointer event and returns the new actor with the events it raised.
|
|
75
|
-
fun onPointer(own self, e: Pointer) -> Handled =
|
|
76
|
-
Handled(actor: self, events: Slice[UiEvent]())
|
|
77
|
-
|
|
78
|
-
own record Group(
|
|
79
|
-
Actor(id, x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, touchable, actions),
|
|
80
|
-
children: Slice[Actor]) =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
56
|
+
e: self.x + self.originX - {ma * self.originX + mc * self.originY},
|
|
84
|
-
|
|
85
|
-
own record Sprite(
|
|
86
|
-
Actor(id, x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, touchable, actions),
|
|
87
|
-
|
|
57
|
+
f: self.y + self.originY - {mb * self.originX + md * self.originY})
|
|
88
|
-
|
|
89
58
|
|
|
59
|
+
# Whether `p`, in the node's own coordinates, is inside its box.
|
|
90
|
-
|
|
60
|
+
fun contains(self, p: Vec2) -> Bool =
|
|
91
|
-
|
|
61
|
+
p.x >= 0.0 && p.x <= self.width && p.y >= 0.0 && p.y <= self.height
|
|
92
62
|
|
|
93
|
-
own record
|
|
63
|
+
own record Node(
|
|
94
|
-
|
|
64
|
+
Transform(x, y, width, height, originX, originY, scaleX, scaleY, rotation),
|
|
95
|
-
|
|
65
|
+
id: Int, color: Color, visible: Bool, touchable: Touchable, actions: Slice[Action], kind: Kind)
|
|
96
66
|
|
|
67
|
+
enum Kind =
|
|
68
|
+
| Group(children: Slice[Node])
|
|
69
|
+
| Sprite(tex: Int, srcX: Float, srcY: Float, srcW: Float, srcH: Float)
|
|
70
|
+
| Label(text: Str, size: Float)
|
|
71
|
+
| Button(text: Str, upTex: Int, downTex: Int, pressed: Bool, over: Bool)
|
|
72
|
+
| Custom(actor: Actor)
|
|
97
73
|
|
|
98
|
-
|
|
74
|
+
# Game behaviour for a `Custom` node. The defaults do nothing.
|
|
99
|
-
|
|
75
|
+
trait Actor =
|
|
100
76
|
|
|
101
|
-
own record Button(
|
|
102
|
-
|
|
77
|
+
# The node after `dt` seconds of this actor's behaviour; the default is itself.
|
|
103
|
-
|
|
78
|
+
fun act(self, own n: Node, dt: Float) -> Node = n
|
|
104
79
|
|
|
80
|
+
# Paints the node at its stage transform `m` with the combined `tint`.
|
|
81
|
+
fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
82
|
+
return
|
|
105
83
|
|
|
84
|
+
# The node after handling an event, and the events it raised.
|
|
106
|
-
|
|
85
|
+
own record Handled(node: Node, events: Slice[UiEvent]) =
|
|
107
|
-
t := self.upTex
|
|
108
|
-
if self.pressed && self.over
|
|
109
|
-
t = self.downTex
|
|
110
|
-
pureDrawSprite(m, tint, self.width, self.height, t, 0.0, 0.0, 0.0, 0.0)
|
|
111
|
-
size := self.height * 0.5
|
|
112
|
-
ox := {self.width - size * 0.6 * Float(self.text.length())} / 2.0
|
|
113
|
-
oy := {self.height - size} / 2.0
|
|
114
|
-
pureDrawText(m.mul(Affine(a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: ox, f: oy)), tint, self.text, size)
|
|
115
86
|
|
|
87
|
+
# The node, moved out; read `events` first, they stay behind.
|
|
116
|
-
|
|
88
|
+
fun intoNode(own self) -> Node =
|
|
117
|
-
ev := Slice[UiEvent]()
|
|
118
|
-
|
|
89
|
+
match self
|
|
119
|
-
wasOver := self.over
|
|
120
|
-
wasPressed := self.pressed
|
|
121
|
-
if e.kind == Down
|
|
122
|
-
self.pressed = true
|
|
123
|
-
self.over = e.over
|
|
124
|
-
ev.push(UiEvent(id: id, kind: Press))
|
|
125
|
-
else if e.kind == Move
|
|
126
|
-
self.over = e.over
|
|
127
|
-
if e.over && !wasOver
|
|
128
|
-
ev.push(UiEvent(id: id, kind: Enter))
|
|
129
|
-
if !e.over && wasOver
|
|
130
|
-
ev.push(UiEvent(id: id, kind: Exit))
|
|
131
|
-
else
|
|
132
|
-
self.over = e.over
|
|
133
|
-
if wasPressed
|
|
134
|
-
self.pressed = false
|
|
135
|
-
ev.push(UiEvent(id: id, kind: Release))
|
|
136
|
-
if e.over
|
|
137
|
-
ev.push(UiEvent(id: id, kind: Click))
|
|
138
|
-
|
|
90
|
+
Handled(n, _) => n
|
|
139
91
|
|
|
140
92
|
fun pureDrawSprite(m: Affine, tint: Color, w: Float, h: Float, tex: Int, srcX: Float, srcY: Float, srcW: Float, srcH: Float) =
|
|
141
93
|
sx := w * m.scaleX()
|
|
@@ -150,64 +102,101 @@ fun pureDrawText(m: Affine, tint: Color, s: Str, size: Float) =
|
|
|
150
102
|
fun pureMulColor(a: Color, b: Color) -> Color =
|
|
151
103
|
Color(r: a.r * b.r, g: a.g * b.g, b: a.b * b.b, a: a.a * b.a)
|
|
152
104
|
|
|
105
|
+
fun pureDrawButton(n: Node, m: Affine, tint: Color, text: Str, upTex: Int, downTex: Int, pressed: Bool, over: Bool) =
|
|
106
|
+
t := upTex
|
|
107
|
+
if pressed && over
|
|
108
|
+
t = downTex
|
|
109
|
+
pureDrawSprite(m, tint, n.width, n.height, t, 0.0, 0.0, 0.0, 0.0)
|
|
110
|
+
size := n.height * 0.5
|
|
111
|
+
ox := {n.width - size * 0.6 * Float(text.length())} / 2.0
|
|
112
|
+
oy := {n.height - size} / 2.0
|
|
113
|
+
pureDrawText(m.mul(Affine(a: 1.0, b: 0.0, c: 0.0, d: 1.0, e: ox, f: oy)), tint, text, size)
|
|
114
|
+
|
|
115
|
+
fun pureNode(id: Int, x: Float, y: Float, w: Float, h: Float, touchable: Touchable, kind: Kind) -> Node =
|
|
116
|
+
Node(
|
|
117
|
+
x: x, y: y, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
118
|
+
scaleX: 1.0, scaleY: 1.0, rotation: 0.0,
|
|
119
|
+
id: id, color: Color.White, visible: true, touchable: touchable,
|
|
120
|
+
actions: Slice[Action](), kind: kind)
|
|
121
|
+
|
|
153
122
|
primitive PureScene =
|
|
154
123
|
|
|
155
|
-
# The
|
|
124
|
+
# The node's transform into its parent's coordinates.
|
|
156
|
-
# position, rotate and scale about the origin.
|
|
157
|
-
fun localMatrix(
|
|
125
|
+
fun localMatrix(n: Node) -> Affine =
|
|
158
|
-
|
|
126
|
+
n.localMatrix()
|
|
159
|
-
sn := Math.sin(a.rotation)
|
|
160
|
-
ma := cs * a.scaleX
|
|
161
|
-
mb := sn * a.scaleX
|
|
162
|
-
mc := 0.0 - sn * a.scaleY
|
|
163
|
-
md := cs * a.scaleY
|
|
164
|
-
Affine(
|
|
165
|
-
a: ma, b: mb, c: mc, d: md,
|
|
166
|
-
e: a.x + a.originX - {ma * a.originX + mc * a.originY},
|
|
167
|
-
f: a.y + a.originY - {mb * a.originX + md * a.originY})
|
|
168
127
|
|
|
169
|
-
# Draws the
|
|
128
|
+
# Draws the node and its children given the parent's matrix and tint.
|
|
170
|
-
fun drawTree(
|
|
129
|
+
fun drawTree(n: Node, parent: Affine, tint: Color) =
|
|
171
|
-
if !
|
|
130
|
+
if !n.visible
|
|
172
131
|
return
|
|
173
|
-
m := parent *
|
|
132
|
+
m := parent * n.localMatrix()
|
|
174
|
-
t := pureMulColor(tint,
|
|
133
|
+
t := pureMulColor(tint, n.color)
|
|
175
|
-
a.draw(m, t)
|
|
176
|
-
match
|
|
134
|
+
match n.kind
|
|
177
|
-
Group(
|
|
135
|
+
Group(children) =>
|
|
178
|
-
for i := range
|
|
136
|
+
for i := range children.length()
|
|
179
|
-
PureScene.drawTree(
|
|
137
|
+
PureScene.drawTree(children.at(i), m, t)
|
|
138
|
+
Sprite(tex, srcX, srcY, srcW, srcH) =>
|
|
139
|
+
pureDrawSprite(m, t, n.width, n.height, tex, srcX, srcY, srcW, srcH)
|
|
140
|
+
Label(text, size) =>
|
|
141
|
+
pureDrawText(m, t, text, size)
|
|
142
|
+
Button(text, upTex, downTex, pressed, over) =>
|
|
143
|
+
pureDrawButton(n, m, t, text, upTex, downTex, pressed, over)
|
|
144
|
+
Custom(actor) =>
|
|
145
|
+
actor.draw(n, m, t)
|
|
146
|
+
|
|
147
|
+
# The children of a group, or null for anything else.
|
|
148
|
+
fun childrenOf(n: Node) -> Slice[Node]? =
|
|
149
|
+
match n.kind
|
|
150
|
+
Group(children) => children
|
|
151
|
+
_ => null
|
|
152
|
+
|
|
153
|
+
# Whether `p`, in the node's own coordinates, is on it.
|
|
154
|
+
fun hit(n: Node, p: Vec2) -> Bool =
|
|
155
|
+
n.touchable == Enabled && n.contains(p)
|
|
156
|
+
|
|
157
|
+
# Handles a pointer event and returns the new node with the events it raised.
|
|
158
|
+
# Only a Button reacts.
|
|
159
|
+
fun onPointer(own n: Node, e: Pointer) -> Handled =
|
|
160
|
+
ev := Slice[UiEvent]()
|
|
161
|
+
match n.kind
|
|
162
|
+
Button(text, upTex, downTex, wasPressed, wasOver) =>
|
|
163
|
+
pressed := wasPressed
|
|
164
|
+
over := wasOver
|
|
165
|
+
if e.kind == Down
|
|
166
|
+
pressed = true
|
|
167
|
+
over = e.over
|
|
168
|
+
ev.push(UiEvent(id: n.id, kind: Press))
|
|
169
|
+
else if e.kind == Move
|
|
170
|
+
over = e.over
|
|
171
|
+
if e.over && !wasOver
|
|
172
|
+
ev.push(UiEvent(id: n.id, kind: Enter))
|
|
173
|
+
if !e.over && wasOver
|
|
174
|
+
ev.push(UiEvent(id: n.id, kind: Exit))
|
|
175
|
+
else
|
|
176
|
+
over = e.over
|
|
177
|
+
if wasPressed
|
|
178
|
+
pressed = false
|
|
179
|
+
ev.push(UiEvent(id: n.id, kind: Release))
|
|
180
|
+
if e.over
|
|
181
|
+
ev.push(UiEvent(id: n.id, kind: Click))
|
|
182
|
+
n.kind = Button(text: text, upTex: upTex, downTex: downTex, pressed: pressed, over: over)
|
|
180
183
|
_ => 0
|
|
184
|
+
Handled(node: n, events: ev)
|
|
185
|
+
|
|
186
|
+
fun group(id: Int, x: Float, y: Float, w: Float, h: Float, children: Slice[Node]) -> Node =
|
|
187
|
+
pureNode(id, x, y, w, h, Enabled, Group(children: children))
|
|
188
|
+
|
|
189
|
+
fun sprite(id: Int, tex: Int, x: Float, y: Float, w: Float, h: Float) -> Node =
|
|
190
|
+
pureNode(id, x, y, w, h, Enabled, Sprite(tex: tex, srcX: 0.0, srcY: 0.0, srcW: 0.0, srcH: 0.0))
|
|
181
191
|
|
|
182
|
-
# The children of a group, or null for a leaf.
|
|
183
|
-
fun childrenOf(a: Actor) -> Slice[Actor]? =
|
|
184
|
-
a.kids()
|
|
185
|
-
|
|
186
|
-
fun group(id: Int, x: Float, y: Float, w: Float, h: Float, children: Slice[Actor]) -> Group =
|
|
187
|
-
Group(
|
|
188
|
-
id: id, x: x, y: y, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
189
|
-
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
190
|
-
touchable: Enabled, actions: Slice[Action](), children: children)
|
|
191
|
-
|
|
192
|
-
fun sprite(id: Int, tex: Int, x: Float, y: Float, w: Float, h: Float) -> Sprite =
|
|
193
|
-
Sprite(
|
|
194
|
-
id: id, x: x, y: y, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
195
|
-
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
196
|
-
touchable: Enabled, actions: Slice[Action](),
|
|
197
|
-
tex: tex, srcX: 0.0, srcY: 0.0, srcW: 0.0, srcH: 0.0)
|
|
198
|
-
|
|
199
|
-
fun label(id: Int, text: Str, x: Float, y: Float, size: Float) ->
|
|
192
|
+
fun label(id: Int, text: Str, x: Float, y: Float, size: Float) -> Node =
|
|
200
|
-
Label(
|
|
201
|
-
|
|
193
|
+
pureNode(id, x, y, size * 0.6 * Float(text.length()), size, Disabled, Label(text: text, size: size))
|
|
202
|
-
|
|
194
|
+
|
|
203
|
-
touchable: Disabled, actions: Slice[Action](), text: text, size: size)
|
|
204
|
-
|
|
205
|
-
fun button(id: Int, text: Str, upTex: Int, downTex: Int, x: Float, y: Float, w: Float, h: Float) ->
|
|
195
|
+
fun button(id: Int, text: Str, upTex: Int, downTex: Int, x: Float, y: Float, w: Float, h: Float) -> Node =
|
|
206
|
-
Button(
|
|
207
|
-
id: id, x: x, y: y, width: w, height: h, originX: 0.0, originY: 0.0,
|
|
208
|
-
scaleX: 1.0, scaleY: 1.0, rotation: 0.0, color: Color.White, visible: true,
|
|
209
|
-
touchable: Enabled, actions: Slice[Action](),
|
|
210
|
-
|
|
196
|
+
pureNode(id, x, y, w, h, Enabled, Button(text: text, upTex: upTex, downTex: downTex, pressed: false, over: false))
|
|
197
|
+
|
|
198
|
+
fun custom(id: Int, x: Float, y: Float, w: Float, h: Float, actor: Actor) -> Node =
|
|
199
|
+
pureNode(id, x, y, w, h, Disabled, Custom(actor: actor))
|
|
211
200
|
|
|
212
201
|
fun psKinds(h: Handled) -> Slice[UiKind] =
|
|
213
202
|
out := Slice[UiKind]()
|
|
@@ -215,9 +204,9 @@ fun psKinds(h: Handled) -> Slice[UiKind] =
|
|
|
215
204
|
out.push(h.events.at(i).kind)
|
|
216
205
|
out
|
|
217
206
|
|
|
218
|
-
fun psIsPressed(
|
|
207
|
+
fun psIsPressed(n: Node) -> Bool =
|
|
219
|
-
match
|
|
208
|
+
match n.kind
|
|
220
|
-
Button(_, _, _,
|
|
209
|
+
Button(_, _, _, pressed, _) => pressed
|
|
221
210
|
_ => false
|
|
222
211
|
|
|
223
212
|
fun psClicks(h: Handled) -> Int =
|
|
@@ -230,6 +219,23 @@ fun psClicks(h: Handled) -> Int =
|
|
|
230
219
|
fun psOverPtr(kind: PointerKind, over: Bool) -> Pointer =
|
|
231
220
|
Pointer(kind: kind, x: 1.0, y: 1.0, over: over)
|
|
232
221
|
|
|
222
|
+
# A game actor: counts its draws and moves its node right by 1 on each act.
|
|
223
|
+
record Beacon(Actor, draws: Int) =
|
|
224
|
+
override fun act(self, own n: Node, dt: Float) -> Node =
|
|
225
|
+
n.x = n.x + 1.0
|
|
226
|
+
n
|
|
227
|
+
override fun draw(self, n: Node, m: Affine, tint: Color) =
|
|
228
|
+
self.draws = self.draws + 1
|
|
229
|
+
|
|
230
|
+
fun psActorOf(n: Node) -> Actor? = match n.kind
|
|
231
|
+
Custom(a) => a
|
|
232
|
+
_ => null
|
|
233
|
+
|
|
234
|
+
fun psActed(own n: Node, dt: Float) -> Node =
|
|
235
|
+
if a := psActorOf(n)
|
|
236
|
+
return a.act(n, dt)
|
|
237
|
+
n
|
|
238
|
+
|
|
233
239
|
test "local_matrix_translates"
|
|
234
240
|
s := PureScene.sprite(1, 0, 10.0, 0.0, 2.0, 2.0)
|
|
235
241
|
p := PureScene.localMatrix(s).apply(Vec2(x: 0.0, y: 0.0))
|
|
@@ -238,11 +244,11 @@ test "local_matrix_translates"
|
|
|
238
244
|
|
|
239
245
|
test "button_click_needs_press_and_over"
|
|
240
246
|
b := PureScene.button(7, "go", 1, 2, 0.0, 0.0, 10.0, 10.0)
|
|
241
|
-
h1 :=
|
|
247
|
+
h1 := PureScene.onPointer(b, psOverPtr(Down, true))
|
|
242
248
|
ks := psKinds(h1)
|
|
243
249
|
n1 := h1.events.length()
|
|
244
250
|
id1 := h1.events.at(0).id
|
|
245
|
-
h2 :=
|
|
251
|
+
h2 := PureScene.onPointer(h1.intoNode(), psOverPtr(Up, true))
|
|
246
252
|
ks.push(h2.events.at(0).kind)
|
|
247
253
|
ks.push(h2.events.at(1).kind)
|
|
248
254
|
assert n1 == 1
|
|
@@ -255,23 +261,68 @@ test "button_click_needs_press_and_over"
|
|
|
255
261
|
|
|
256
262
|
test "button_release_elsewhere_no_click"
|
|
257
263
|
b := PureScene.button(7, "go", 1, 2, 0.0, 0.0, 10.0, 10.0)
|
|
258
|
-
h1 :=
|
|
264
|
+
h1 := PureScene.onPointer(b, psOverPtr(Down, true))
|
|
259
|
-
h2 :=
|
|
265
|
+
h2 := PureScene.onPointer(h1.intoNode(), psOverPtr(Up, false))
|
|
260
266
|
assert h2.events.length() == 1
|
|
261
267
|
assert h2.events.at(0).kind == Release
|
|
262
|
-
assert !psIsPressed(h2.
|
|
268
|
+
assert !psIsPressed(h2.intoNode())
|
|
263
269
|
|
|
264
270
|
test "button_drag_off_and_back_one_click"
|
|
265
271
|
b := PureScene.button(7, "go", 1, 2, 0.0, 0.0, 10.0, 10.0)
|
|
266
|
-
a1 :=
|
|
272
|
+
a1 := PureScene.onPointer(b, psOverPtr(Down, true)).intoNode()
|
|
267
|
-
h2 :=
|
|
273
|
+
h2 := PureScene.onPointer(a1, psOverPtr(Move, false))
|
|
268
274
|
c2 := psClicks(h2)
|
|
269
|
-
h3 :=
|
|
275
|
+
h3 := PureScene.onPointer(h2.intoNode(), psOverPtr(Move, true))
|
|
270
276
|
c3 := psClicks(h3)
|
|
271
|
-
h4 :=
|
|
277
|
+
h4 := PureScene.onPointer(h3.intoNode(), psOverPtr(Up, true))
|
|
272
278
|
assert c2 + c3 + psClicks(h4) == 1
|
|
273
279
|
|
|
274
280
|
test "on_pointer_hands_back_changed_actor"
|
|
275
281
|
b := PureScene.button(7, "go", 1, 2, 0.0, 0.0, 10.0, 10.0)
|
|
276
|
-
h :=
|
|
282
|
+
h := PureScene.onPointer(b, psOverPtr(Down, true))
|
|
277
|
-
assert psIsPressed(h.
|
|
283
|
+
assert psIsPressed(h.intoNode())
|
|
284
|
+
|
|
285
|
+
test "custom_actor_act_and_draw_dispatch"
|
|
286
|
+
beacon := Beacon(draws: 0)
|
|
287
|
+
n := PureScene.custom(7, 2.0, 0.0, 8.0, 8.0, beacon)
|
|
288
|
+
PureScene.drawTree(n, Affine.Identity, Color.White)
|
|
289
|
+
assert beacon.draws == 1
|
|
290
|
+
moved := psActed(n, 0.016)
|
|
291
|
+
assert moved.x == 3.0
|
|
292
|
+
|
|
293
|
+
test "draw_tree_nested_groups_reaches_custom"
|
|
294
|
+
beacon := Beacon(draws: 0)
|
|
295
|
+
inner := PureScene.group(2, 0.0, 0.0, 10.0, 10.0, Slice(PureScene.custom(3, 1.0, 1.0, 4.0, 4.0, beacon)))
|
|
296
|
+
root := PureScene.group(1, 0.0, 0.0, 10.0, 10.0, Slice(inner))
|
|
297
|
+
PureScene.drawTree(root, Affine.Identity, Color.White)
|
|
298
|
+
assert beacon.draws == 1
|
|
299
|
+
|
|
300
|
+
test "hit_box_and_children_of"
|
|
301
|
+
s := PureScene.sprite(1, 0, 0.0, 0.0, 10.0, 10.0)
|
|
302
|
+
assert PureScene.hit(s, Vec2(x: 5.0, y: 5.0))
|
|
303
|
+
assert !PureScene.hit(s, Vec2(x: 11.0, y: 5.0))
|
|
304
|
+
assert PureScene.childrenOf(s) == null
|
|
305
|
+
assert PureScene.childrenOf(PureScene.group(2, 0.0, 0.0, 1.0, 1.0, Slice(s))) != null
|
|
306
|
+
|
|
307
|
+
test "button_move_raises_enter_then_exit"
|
|
308
|
+
b := PureScene.button(7, "go", 1, 2, 0.0, 0.0, 10.0, 10.0)
|
|
309
|
+
h1 := PureScene.onPointer(b, psOverPtr(Move, true))
|
|
310
|
+
assert h1.events.length() == 1
|
|
311
|
+
assert h1.events.at(0).kind == Enter
|
|
312
|
+
h2 := PureScene.onPointer(h1.intoNode(), psOverPtr(Move, false))
|
|
313
|
+
assert h2.events.length() == 1
|
|
314
|
+
assert h2.events.at(0).kind == Exit
|
|
315
|
+
assert !psIsPressed(h2.intoNode())
|
|
316
|
+
|
|
317
|
+
test "on_pointer_non_button_raises_nothing"
|
|
318
|
+
s := PureScene.sprite(1, 0, 0.0, 0.0, 10.0, 10.0)
|
|
319
|
+
h := PureScene.onPointer(s, psOverPtr(Down, true))
|
|
320
|
+
assert h.events.length() == 0
|
|
321
|
+
assert h.intoNode().id == 1
|
|
322
|
+
|
|
323
|
+
test "draw_tree_skips_invisible_custom"
|
|
324
|
+
beacon := Beacon(draws: 0)
|
|
325
|
+
n := PureScene.custom(7, 0.0, 0.0, 8.0, 8.0, beacon)
|
|
326
|
+
n.visible = false
|
|
327
|
+
PureScene.drawTree(n, Affine.Identity, Color.White)
|
|
328
|
+
assert beacon.draws == 0
|
plum-std/game/SceneInput.plum
CHANGED
|
@@ -35,7 +35,7 @@ own record StageHandled(stage: Stage, events: Slice[UiEvent]) =
|
|
|
35
35
|
match self
|
|
36
36
|
StageHandled(s, _) => s
|
|
37
37
|
|
|
38
|
-
own record Stage(root:
|
|
38
|
+
own record Stage(root: Node, camera: Affine, captured: Capture?) =
|
|
39
39
|
|
|
40
40
|
# Handles one input: routes it, returns the new stage and the events raised.
|
|
41
41
|
fun input(own self, e: Input) -> StageHandled =
|
|
@@ -105,7 +105,7 @@ fun inputIsPrefix(a: Slice[Int], b: Slice[Int]) -> Bool =
|
|
|
105
105
|
true
|
|
106
106
|
|
|
107
107
|
primitive SceneInput =
|
|
108
|
-
fun stage(own root:
|
|
108
|
+
fun stage(own root: Node) -> Stage =
|
|
109
109
|
Stage(root: root, camera: Affine.Identity, captured: null)
|
|
110
110
|
|
|
111
111
|
# This frame's pointer inputs from the host mouse: a press, a release, or a
|
|
@@ -122,26 +122,25 @@ primitive SceneInput =
|
|
|
122
122
|
out.push(Input(kind: PointerMove, x: m.x, y: m.y))
|
|
123
123
|
out
|
|
124
124
|
|
|
125
|
-
fun inBtnTree() ->
|
|
125
|
+
fun inBtnTree() -> Node =
|
|
126
|
-
b:
|
|
126
|
+
b: Node := PureScene.button(5, "Go", 0, 1, 10.0, 10.0, 100.0, 40.0)
|
|
127
|
-
kids := Slice
|
|
127
|
+
kids := Slice(b)
|
|
128
|
-
kids.push(b)
|
|
129
|
-
g:
|
|
128
|
+
g: Node := PureScene.group(1, 0.0, 0.0, 800.0, 600.0, kids)
|
|
130
129
|
g
|
|
131
130
|
|
|
132
131
|
fun inBtnPressed(s: Stage) -> Bool =
|
|
133
|
-
if k := s.root
|
|
132
|
+
if k := PureScene.childrenOf(s.root)
|
|
134
133
|
return psIsPressed(k.at(0))
|
|
135
134
|
false
|
|
136
135
|
|
|
137
136
|
fun inBtnOver(s: Stage) -> Bool =
|
|
138
|
-
if k := s.root
|
|
137
|
+
if k := PureScene.childrenOf(s.root)
|
|
139
138
|
return inOverOf(k.at(0))
|
|
140
139
|
false
|
|
141
140
|
|
|
142
|
-
fun inOverOf(a:
|
|
141
|
+
fun inOverOf(a: Node) -> Bool =
|
|
143
|
-
match a
|
|
142
|
+
match a.kind
|
|
144
|
-
Button(_, _, _, _,
|
|
143
|
+
Button(_, _, _, _, over) => over
|
|
145
144
|
_ => false
|
|
146
145
|
|
|
147
146
|
fun inDown(x: Float, y: Float) -> Input =
|
|
@@ -160,15 +159,14 @@ fun inClicks(events: Slice[UiEvent]) -> Int =
|
|
|
160
159
|
n = n + 1
|
|
161
160
|
n
|
|
162
161
|
|
|
163
|
-
fun inXformTree() ->
|
|
162
|
+
fun inXformTree() -> Node =
|
|
164
|
-
b:
|
|
163
|
+
b: Node := PureScene.button(5, "Go", 0, 1, 10.0, 10.0, 100.0, 40.0)
|
|
165
|
-
kids := Slice
|
|
164
|
+
kids := Slice(b)
|
|
166
|
-
kids.push(b)
|
|
167
165
|
g := PureScene.group(1, 100.0, 0.0, 800.0, 600.0, kids)
|
|
168
166
|
g.rotation = 1.5707963267948966
|
|
169
167
|
g.scaleX = 2.0
|
|
170
168
|
g.scaleY = 2.0
|
|
171
|
-
me:
|
|
169
|
+
me: Node := g
|
|
172
170
|
me
|
|
173
171
|
|
|
174
172
|
test "press_sets_capture"
|
|
@@ -202,7 +200,7 @@ test "drag_off_and_back_one_click"
|
|
|
202
200
|
|
|
203
201
|
test "stale_capture_dropped"
|
|
204
202
|
h1 := SceneInput.stage(inBtnTree()).input(inDown(20.0, 20.0))
|
|
205
|
-
empty:
|
|
203
|
+
empty: Node := PureScene.group(1, 0.0, 0.0, 800.0, 600.0, Slice[Node]())
|
|
206
204
|
swapped := Stage(root: empty, camera: h1.stage.camera, captured: h1.stage.captured)
|
|
207
205
|
h2 := swapped.input(inUp(20.0, 20.0))
|
|
208
206
|
assert h2.events.length() == 0
|
|
@@ -223,24 +221,19 @@ test "camera_maps_screen_to_world"
|
|
|
223
221
|
|
|
224
222
|
test "frame_handles_inputs_then_updates"
|
|
225
223
|
spr := PureScene.sprite(6, 0, 300.0, 300.0, 10.0, 10.0)
|
|
226
|
-
acts := Slice[Action]()
|
|
227
|
-
acts
|
|
224
|
+
acts := Slice(Actions.moveBy(10.0, 0.0, 1.0))
|
|
228
225
|
spr.actions = acts
|
|
229
|
-
b:
|
|
226
|
+
b: Node := PureScene.button(5, "Go", 0, 1, 10.0, 10.0, 100.0, 40.0)
|
|
230
|
-
sp:
|
|
227
|
+
sp: Node := spr
|
|
231
|
-
kids := Slice
|
|
228
|
+
kids := Slice(b, sp)
|
|
232
|
-
kids.push(b)
|
|
233
|
-
kids.push(sp)
|
|
234
|
-
root:
|
|
229
|
+
root: Node := PureScene.group(1, 0.0, 0.0, 800.0, 600.0, kids)
|
|
235
|
-
ins := Slice[Input]()
|
|
236
|
-
ins
|
|
230
|
+
ins := Slice(inDown(20.0, 20.0), inUp(20.0, 20.0))
|
|
237
|
-
ins.push(inUp(20.0, 20.0))
|
|
238
231
|
h := SceneInput.stage(root).frame(0.5, ins)
|
|
239
232
|
assert h.events.length() == 3
|
|
240
233
|
assert h.events.at(0).kind == Press
|
|
241
234
|
assert h.events.at(1).kind == Release
|
|
242
235
|
assert h.events.at(2).kind == Click
|
|
243
|
-
if k := h.stage.root
|
|
236
|
+
if k := PureScene.childrenOf(h.stage.root)
|
|
244
237
|
assert {k.at(1).x - 305.0}.abs() < 0.000001
|
|
245
238
|
else
|
|
246
239
|
assert false
|
plum-std/game/SceneTree.plum
CHANGED
|
@@ -8,34 +8,44 @@ import std/game/Affine
|
|
|
8
8
|
import std/game/Actions
|
|
9
9
|
import std/game/PureScene
|
|
10
10
|
|
|
11
|
+
fun treeActorOf(n: Node) -> Actor? =
|
|
12
|
+
match n.kind
|
|
13
|
+
Custom(actor) => actor
|
|
14
|
+
_ => null
|
|
15
|
+
|
|
11
16
|
fun treePrepend(i: Int, rest: Slice[Int]) -> Slice[Int] =
|
|
12
|
-
out := Slice
|
|
17
|
+
out := Slice(i)
|
|
13
|
-
out.push(i)
|
|
14
18
|
for j := range rest.length()
|
|
15
19
|
out.push(rest.at(j))
|
|
16
20
|
out
|
|
17
21
|
|
|
18
22
|
primitive SceneTree =
|
|
19
23
|
|
|
24
|
+
# A Custom node's actor acts on it; every other node is itself.
|
|
25
|
+
fun act(own n: Node, dt: Float) -> Node =
|
|
26
|
+
if actor := treeActorOf(n)
|
|
27
|
+
return actor.act(n, dt)
|
|
28
|
+
n
|
|
29
|
+
|
|
20
|
-
# The
|
|
30
|
+
# The node after `dt` seconds: a Custom actor's own `act`, then its actions on its pose,
|
|
21
31
|
# then every child. The tree is changed in place and handed back.
|
|
22
|
-
fun update(own a:
|
|
32
|
+
fun update(own a: Node, dt: Float) -> Node =
|
|
23
|
-
b :=
|
|
33
|
+
b := SceneTree.act(a, dt)
|
|
24
34
|
r := Actions.stepAll(Pose(x: b.x, y: b.y, rotation: b.rotation, scaleX: b.scaleX, scaleY: b.scaleY), b.actions, dt)
|
|
25
35
|
b.x = r.pose.x
|
|
26
36
|
b.y = r.pose.y
|
|
27
37
|
b.rotation = r.pose.rotation
|
|
28
38
|
b.scaleX = r.pose.scaleX
|
|
29
39
|
b.scaleY = r.pose.scaleY
|
|
30
|
-
match b
|
|
40
|
+
match b.kind
|
|
31
|
-
Group(
|
|
41
|
+
Group(ks) =>
|
|
32
42
|
_ := ks.mapInPlace(|k| SceneTree.update(k, dt))
|
|
33
43
|
_ => 0
|
|
34
44
|
b
|
|
35
45
|
|
|
36
46
|
# The path (child indexes from `a`) to the topmost actor under `p`, which is
|
|
37
47
|
# in the coordinates of `a`'s parent; `parent` is that parent's stage matrix.
|
|
38
|
-
fun findHit(a:
|
|
48
|
+
fun findHit(a: Node, parent: Affine, p: Vec2) -> Slice[Int]? =
|
|
39
49
|
if !a.visible || a.touchable == Disabled
|
|
40
50
|
return null
|
|
41
51
|
m := parent * PureScene.localMatrix(a)
|
|
@@ -45,8 +55,8 @@ primitive SceneTree =
|
|
|
45
55
|
lp := Vec2(x: 0.0, y: 0.0)
|
|
46
56
|
if q := inv
|
|
47
57
|
lp = q.apply(p)
|
|
48
|
-
match a
|
|
58
|
+
match a.kind
|
|
49
|
-
Group(
|
|
59
|
+
Group(ks) =>
|
|
50
60
|
i := ks.length() - 1
|
|
51
61
|
while i >= 0
|
|
52
62
|
sub := SceneTree.findHit(ks.at(i), m, p)
|
|
@@ -54,16 +64,16 @@ primitive SceneTree =
|
|
|
54
64
|
return treePrepend(i, s)
|
|
55
65
|
i = i - 1
|
|
56
66
|
_ => 0
|
|
57
|
-
if
|
|
67
|
+
if PureScene.hit(a, lp)
|
|
58
68
|
return Slice[Int]()
|
|
59
69
|
null
|
|
60
70
|
|
|
61
71
|
# The id of the actor at `path` below `a`, or -1 if the path leads nowhere.
|
|
62
|
-
fun idAt(a:
|
|
72
|
+
fun idAt(a: Node, path: Slice[Int], depth: Int) -> Int =
|
|
63
73
|
if depth >= path.length()
|
|
64
74
|
return a.id
|
|
65
|
-
match a
|
|
75
|
+
match a.kind
|
|
66
|
-
Group(
|
|
76
|
+
Group(ks) =>
|
|
67
77
|
i := path.at(depth)
|
|
68
78
|
if i >= 0 && i < ks.length()
|
|
69
79
|
return SceneTree.idAt(ks.at(i), path, depth + 1)
|
|
@@ -71,11 +81,11 @@ primitive SceneTree =
|
|
|
71
81
|
-1
|
|
72
82
|
|
|
73
83
|
# The path to the first actor with `id`, depth first; null if none.
|
|
74
|
-
fun findPath(a:
|
|
84
|
+
fun findPath(a: Node, id: Int) -> Slice[Int]? =
|
|
75
85
|
if a.id == id
|
|
76
86
|
return Slice[Int]()
|
|
77
|
-
match a
|
|
87
|
+
match a.kind
|
|
78
|
-
Group(
|
|
88
|
+
Group(ks) =>
|
|
79
89
|
for i := range ks.length()
|
|
80
90
|
sub := SceneTree.findPath(ks.at(i), id)
|
|
81
91
|
if s := sub
|
|
@@ -84,17 +94,17 @@ primitive SceneTree =
|
|
|
84
94
|
null
|
|
85
95
|
|
|
86
96
|
# The tree where the first actor with `id` is replaced by `f` of it.
|
|
87
|
-
fun modify(own a:
|
|
97
|
+
fun modify(own a: Node, id: Int, f: fn(Node) -> Node) -> Node =
|
|
88
98
|
path := SceneTree.findPath(a, id)
|
|
89
99
|
if p := path
|
|
90
100
|
return SceneTree.modifyAt(a, p, 0, f)
|
|
91
101
|
a
|
|
92
102
|
|
|
93
|
-
fun modifyAt(own a:
|
|
103
|
+
fun modifyAt(own a: Node, path: Slice[Int], depth: Int, f: fn(Node) -> Node) -> Node =
|
|
94
104
|
if depth >= path.length()
|
|
95
105
|
return f(a)
|
|
96
|
-
match a
|
|
106
|
+
match a.kind
|
|
97
|
-
Group(
|
|
107
|
+
Group(ks) =>
|
|
98
108
|
i := path.at(depth)
|
|
99
109
|
if i >= 0 && i < ks.length()
|
|
100
110
|
c := ks.takeAt(i)
|
|
@@ -105,39 +115,36 @@ primitive SceneTree =
|
|
|
105
115
|
# Delivers `e` (in the coordinates of `a`'s parent) along `path`: the child on
|
|
106
116
|
# the path first, then `a` itself. A path that runs out reaches the deepest
|
|
107
117
|
# actor it can.
|
|
108
|
-
fun route(own a:
|
|
118
|
+
fun route(own a: Node, path: Slice[Int], depth: Int, e: Pointer) -> Handled =
|
|
109
119
|
events := Slice[UiEvent]()
|
|
110
120
|
r := SceneTree.routeInto(a, path, depth, e, events)
|
|
111
|
-
Handled(
|
|
121
|
+
Handled(node: r, events: events)
|
|
112
122
|
|
|
113
123
|
# `route`, appending the raised events to `events` and returning the actor.
|
|
114
|
-
fun routeInto(own a:
|
|
124
|
+
fun routeInto(own a: Node, path: Slice[Int], depth: Int, e: Pointer, events: Slice[UiEvent]) -> Node =
|
|
115
125
|
lp := Vec2(x: e.x, y: e.y)
|
|
116
126
|
inv := PureScene.localMatrix(a).invert()
|
|
117
127
|
if q := inv
|
|
118
128
|
lp = q.apply(lp)
|
|
119
129
|
local := Pointer(kind: e.kind, x: lp.x, y: lp.y, over: e.over)
|
|
120
130
|
if depth < path.length()
|
|
121
|
-
match a
|
|
131
|
+
match a.kind
|
|
122
|
-
Group(
|
|
132
|
+
Group(ks) =>
|
|
123
133
|
i := path.at(depth)
|
|
124
134
|
if i >= 0 && i < ks.length()
|
|
125
135
|
c := ks.takeAt(i)
|
|
126
136
|
ks.putAt(i, SceneTree.routeInto(c, path, depth + 1, local, events))
|
|
127
137
|
_ => 0
|
|
128
|
-
h :=
|
|
138
|
+
h := PureScene.onPointer(a, local)
|
|
129
139
|
for j := range h.events.length()
|
|
130
140
|
events.push(h.events.at(j))
|
|
131
|
-
h.
|
|
141
|
+
h.intoNode()
|
|
132
|
-
|
|
142
|
+
|
|
133
|
-
fun ttFixture() ->
|
|
143
|
+
fun ttFixture() -> Node =
|
|
134
|
-
inner := Slice[Actor]()
|
|
135
|
-
inner
|
|
144
|
+
inner := Slice(PureScene.sprite(4, 0, 0.0, 0.0, 5.0, 5.0))
|
|
136
|
-
kids := Slice[Actor]()
|
|
137
|
-
kids.push(PureScene.sprite(2, 0, 0.0, 0.0, 10.0, 10.0))
|
|
138
|
-
kids.
|
|
145
|
+
kids := Slice(PureScene.sprite(2, 0, 0.0, 0.0, 10.0, 10.0), PureScene.group(3, 20.0, 0.0, 10.0, 10.0, inner))
|
|
139
146
|
root := PureScene.group(1, 0.0, 0.0, 100.0, 100.0, kids)
|
|
140
|
-
me:
|
|
147
|
+
me: Node := root
|
|
141
148
|
me
|
|
142
149
|
|
|
143
150
|
fun ttNoPath(r: Slice[Int]?) -> Bool =
|
|
@@ -154,98 +161,98 @@ fun ttPathEq(r: Slice[Int]?, want: Slice[Int]) -> Bool =
|
|
|
154
161
|
false
|
|
155
162
|
|
|
156
163
|
fun ttInts(a: Int, b: Int) -> Slice[Int] =
|
|
157
|
-
out := Slice
|
|
164
|
+
out := Slice(a, b)
|
|
158
|
-
out.push(a)
|
|
159
|
-
out.push(b)
|
|
160
165
|
out
|
|
161
166
|
|
|
162
167
|
fun ttAt(x: Float, y: Float) -> Vec2 =
|
|
163
168
|
Vec2(x: x, y: y)
|
|
164
169
|
|
|
165
|
-
fun ttMovingTree() ->
|
|
170
|
+
fun ttMovingTree() -> Node =
|
|
166
171
|
s := PureScene.sprite(2, 0, 0.0, 0.0, 10.0, 10.0)
|
|
167
172
|
s.actions = Slice(Actions.moveBy(10.0, 0.0, 1.0))
|
|
168
|
-
kids := Slice
|
|
173
|
+
kids := Slice(s)
|
|
169
|
-
kids.push(s)
|
|
170
174
|
root := PureScene.group(1, 0.0, 0.0, 100.0, 100.0, kids)
|
|
171
|
-
me:
|
|
175
|
+
me: Node := root
|
|
172
176
|
me
|
|
173
177
|
|
|
174
|
-
fun ttFirstKidX(a:
|
|
178
|
+
fun ttFirstKidX(a: Node) -> Float =
|
|
175
|
-
match a
|
|
179
|
+
match a.kind
|
|
176
|
-
Group(
|
|
180
|
+
Group(k) =>
|
|
177
181
|
return k.at(0).x
|
|
178
182
|
_ => 0
|
|
179
183
|
0.0 - 1.0
|
|
180
184
|
|
|
181
|
-
fun ttFirstKidActions(a:
|
|
185
|
+
fun ttFirstKidActions(a: Node) -> Int =
|
|
182
|
-
match a
|
|
186
|
+
match a.kind
|
|
183
|
-
Group(
|
|
187
|
+
Group(k) =>
|
|
184
188
|
return k.at(0).actions.length()
|
|
185
189
|
_ => 0
|
|
186
190
|
0 - 1
|
|
187
191
|
|
|
188
|
-
fun ttSetTouch(own t:
|
|
192
|
+
fun ttSetTouch(own t: Node, id: Int, tc: Touchable) -> Node =
|
|
189
193
|
SceneTree.modify(t, id, |a| ttSetTouchOf(a, tc))
|
|
190
194
|
|
|
191
|
-
fun ttSetTouchOf(own a:
|
|
195
|
+
fun ttSetTouchOf(own a: Node, tc: Touchable) -> Node =
|
|
192
196
|
a.touchable = tc
|
|
193
197
|
a
|
|
194
198
|
|
|
195
|
-
fun ttSetScaleX(own a:
|
|
199
|
+
fun ttSetScaleX(own a: Node) -> Node =
|
|
196
200
|
a.scaleX = 0.0
|
|
197
201
|
a
|
|
198
202
|
|
|
199
|
-
fun ttSetX9(own a:
|
|
203
|
+
fun ttSetX9(own a: Node) -> Node =
|
|
200
204
|
a.x = 9.0
|
|
201
205
|
a
|
|
202
206
|
|
|
203
|
-
fun ttKidX(a:
|
|
207
|
+
fun ttKidX(a: Node, i: Int) -> Float =
|
|
204
|
-
match a
|
|
208
|
+
match a.kind
|
|
205
|
-
Group(
|
|
209
|
+
Group(k) =>
|
|
206
210
|
return k.at(i).x
|
|
207
211
|
_ => 0
|
|
208
212
|
0.0 - 1.0
|
|
209
213
|
|
|
210
|
-
fun ttButtonRoot() ->
|
|
214
|
+
fun ttButtonRoot() -> Node =
|
|
211
|
-
kids := Slice[Actor]()
|
|
212
|
-
kids
|
|
215
|
+
kids := Slice(PureScene.button(5, "Go", 0, 1, 10.0, 10.0, 100.0, 40.0))
|
|
213
216
|
root := PureScene.group(1, 0.0, 0.0, 800.0, 600.0, kids)
|
|
214
|
-
me:
|
|
217
|
+
me: Node := root
|
|
215
218
|
me
|
|
216
219
|
|
|
217
220
|
fun ttZero() -> Slice[Int] =
|
|
218
|
-
out := Slice
|
|
221
|
+
out := Slice(0)
|
|
219
|
-
out.push(0)
|
|
220
222
|
out
|
|
221
223
|
|
|
222
224
|
# Five actions on one sprite: the 1st, 3rd and 5th finish in the first 0.1s step,
|
|
223
225
|
# the 2nd (MoveBy) and 4th (RotateBy) keep running.
|
|
224
|
-
fun ttCompactTree() ->
|
|
226
|
+
fun ttCompactTree() -> Node =
|
|
225
227
|
s := PureScene.sprite(2, 0, 0.0, 0.0, 10.0, 10.0)
|
|
226
228
|
s.actions = Slice(Actions.moveBy(1.0, 0.0, 0.1), Actions.moveBy(0.0, 10.0, 10.0), Actions.rotateBy(1.0, 0.1), Actions.rotateBy(2.0, 10.0), Actions.delay(0.1))
|
|
227
|
-
kids := Slice
|
|
229
|
+
kids := Slice(s)
|
|
228
|
-
kids.push(s)
|
|
229
230
|
root := PureScene.group(1, 0.0, 0.0, 100.0, 100.0, kids)
|
|
230
|
-
me:
|
|
231
|
+
me: Node := root
|
|
231
232
|
me
|
|
232
233
|
|
|
233
|
-
fun ttKid(a:
|
|
234
|
+
fun ttKid(a: Node) -> Node =
|
|
234
|
-
match a
|
|
235
|
+
match a.kind
|
|
235
|
-
Group(
|
|
236
|
+
Group(k) =>
|
|
236
237
|
return k.at(0)
|
|
237
238
|
_ => 0
|
|
238
|
-
none:
|
|
239
|
+
none: Node := PureScene.sprite(0, 0, 0.0, 0.0, 0.0, 0.0)
|
|
239
240
|
none
|
|
240
241
|
|
|
241
242
|
# 1 MoveBy, 2 RotateBy, 3 Delay, 0 anything else.
|
|
242
243
|
fun ttActKind(a: Action) -> Int =
|
|
243
|
-
match a
|
|
244
|
+
match a.kind
|
|
244
|
-
MoveBy => 1
|
|
245
|
+
MoveBy(_, _, _) => 1
|
|
245
|
-
RotateBy => 2
|
|
246
|
+
RotateBy(_, _) => 2
|
|
246
|
-
Delay => 3
|
|
247
|
+
Delay(_) => 3
|
|
247
248
|
_ => 0
|
|
248
249
|
|
|
250
|
+
# Adds 1.0 to its node's x on every act.
|
|
251
|
+
record TtStepper(Actor, ticks: Int) =
|
|
252
|
+
override fun act(self, own n: Node, dt: Float) -> Node =
|
|
253
|
+
n.x = n.x + 1.0
|
|
254
|
+
n
|
|
255
|
+
|
|
249
256
|
test "find_hit_nested"
|
|
250
257
|
r := SceneTree.findHit(ttFixture(), Affine.Identity, ttAt(21.0, 1.0))
|
|
251
258
|
assert ttPathEq(r, ttInts(1, 0))
|
|
@@ -256,8 +263,7 @@ test "find_hit_miss"
|
|
|
256
263
|
test "id_at_paths"
|
|
257
264
|
t := ttFixture()
|
|
258
265
|
assert SceneTree.idAt(t, ttInts(1, 0), 0) == 4
|
|
259
|
-
five := Slice
|
|
266
|
+
five := Slice(5)
|
|
260
|
-
five.push(5)
|
|
261
267
|
assert SceneTree.idAt(t, five, 0) == -1
|
|
262
268
|
assert SceneTree.idAt(t, Slice[Int](), 0) == 1
|
|
263
269
|
|
|
@@ -293,22 +299,17 @@ test "scale_zero_not_hit"
|
|
|
293
299
|
assert ttPathEq(r, Slice[Int]())
|
|
294
300
|
|
|
295
301
|
test "topmost_child_wins"
|
|
296
|
-
kids := Slice[Actor]()
|
|
297
|
-
kids.push(PureScene.sprite(2, 0, 0.0, 0.0, 10.0, 10.0))
|
|
298
|
-
kids.
|
|
302
|
+
kids := Slice(PureScene.sprite(2, 0, 0.0, 0.0, 10.0, 10.0), PureScene.sprite(3, 0, 0.0, 0.0, 10.0, 10.0))
|
|
299
303
|
root := PureScene.group(1, 0.0, 0.0, 100.0, 100.0, kids)
|
|
300
|
-
me:
|
|
304
|
+
me: Node := root
|
|
301
305
|
r := SceneTree.findHit(me, Affine.Identity, ttAt(1.0, 1.0))
|
|
302
|
-
want := Slice
|
|
306
|
+
want := Slice(1)
|
|
303
|
-
want.push(1)
|
|
304
307
|
assert ttPathEq(r, want)
|
|
305
308
|
|
|
306
309
|
test "modify_changes_first_match_only"
|
|
307
|
-
kids := Slice[Actor]()
|
|
308
|
-
kids.
|
|
310
|
+
kids := Slice(PureScene.sprite(7, 0, 0.0, 0.0, 10.0, 10.0), PureScene.sprite(7, 0, 0.0, 0.0, 10.0, 10.0))
|
|
309
|
-
kids.push(PureScene.sprite(7, 0, 0.0, 0.0, 10.0, 10.0))
|
|
310
311
|
root := PureScene.group(1, 0.0, 0.0, 100.0, 100.0, kids)
|
|
311
|
-
me:
|
|
312
|
+
me: Node := root
|
|
312
313
|
n := SceneTree.modify(me, 7, |a| ttSetX9(a))
|
|
313
314
|
assert ttKidX(n, 0) == 9.0
|
|
314
315
|
assert ttKidX(n, 1) == 0.0
|
|
@@ -318,7 +319,7 @@ test "route_child_first_then_group"
|
|
|
318
319
|
h1 := SceneTree.route(g, ttZero(), 0, Pointer(kind: Down, x: 20.0, y: 20.0, over: true))
|
|
319
320
|
n1 := h1.events.length()
|
|
320
321
|
k1 := h1.events.at(0).kind
|
|
321
|
-
h2 := SceneTree.route(h1.
|
|
322
|
+
h2 := SceneTree.route(h1.intoNode(), ttZero(), 0, Pointer(kind: Up, x: 20.0, y: 20.0, over: true))
|
|
322
323
|
assert n1 == 1
|
|
323
324
|
assert k1 == Press
|
|
324
325
|
assert h2.events.length() == 2
|
|
@@ -327,8 +328,7 @@ test "route_child_first_then_group"
|
|
|
327
328
|
assert h2.events.at(1).id == 5
|
|
328
329
|
|
|
329
330
|
test "stale_path_reaches_group_only"
|
|
330
|
-
nine := Slice
|
|
331
|
+
nine := Slice(9)
|
|
331
|
-
nine.push(9)
|
|
332
332
|
h := SceneTree.route(ttButtonRoot(), nine, 0, Pointer(kind: Down, x: 20.0, y: 20.0, over: true))
|
|
333
333
|
assert h.events.length() == 0
|
|
334
334
|
|
|
@@ -351,3 +351,8 @@ test "compaction_keeps_order"
|
|
|
351
351
|
assert {ttKid(m).y - 0.6}.abs() < 1e-6
|
|
352
352
|
assert {ttKid(m).rotation - 1.12}.abs() < 1e-6
|
|
353
353
|
assert {ttKid(m).x - 1.0}.abs() < 1e-6
|
|
354
|
+
|
|
355
|
+
test "update_runs_custom_actor_act"
|
|
356
|
+
c := PureScene.custom(9, 1.0, 0.0, 4.0, 4.0, TtStepper(ticks: 0))
|
|
357
|
+
n := SceneTree.update(PureScene.group(1, 0.0, 0.0, 10.0, 10.0, Slice(c)), 0.016)
|
|
358
|
+
assert ttFirstKidX(n) == 2.0
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -7798,6 +7798,18 @@ fn compileExprCore(
|
|
|
7798
7798
|
state,
|
|
7799
7799
|
)?;
|
|
7800
7800
|
}
|
|
7801
|
+
// A field-less variant or record built with call syntax (`Panel()`, e.g. a record
|
|
7802
|
+
// that only embeds a field-less trait): same value as the bare name.
|
|
7803
|
+
ast::Expr::ClassCall(call)
|
|
7804
|
+
if call.fields.is_empty()
|
|
7805
|
+
&& call.spread.is_none()
|
|
7806
|
+
&& ctx
|
|
7807
|
+
.enum_variants
|
|
7808
|
+
.get(&call.type_name)
|
|
7809
|
+
.is_some_and(|info| info.field_types.is_empty() && info.values.is_empty()) =>
|
|
7810
|
+
{
|
|
7811
|
+
compileExpr(&ast::Expr::TypeName(call.type_name.clone()), body, ctx, state)?;
|
|
7812
|
+
}
|
|
7801
7813
|
// A NAMED-payload enum variant construction (`Circle(radius: 5)`, as
|
|
7802
7814
|
// opposed to the positional/generic-payload form `Next(T)` — constructed
|
|
7803
7815
|
// via `Expr::FnCall` instead, elsewhere) — mirrors `plum-checker`'s own
|
plum-wasm-codegen/tests/empty_record_embed_e2e_test.rs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#![allow(non_snake_case)]
|
|
2
|
+
use plum_wasm_codegen::compileSource;
|
|
3
|
+
|
|
4
|
+
fn gcEngine() -> wasmtime::Engine {
|
|
5
|
+
let mut config = wasmtime::Config::new();
|
|
6
|
+
config.wasm_gc(true);
|
|
7
|
+
config.wasm_function_references(true);
|
|
8
|
+
wasmtime::Engine::new(&config).expect("engine with GC config should construct")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
fn repoRoot() -> std::path::PathBuf {
|
|
12
|
+
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fn load(name: &str, src: &str) -> Result<plum_core::ast::Source, String> {
|
|
16
|
+
let dir = std::env::temp_dir().join(format!("plum_empty_record_embed_{}_{}", name, std::process::id()));
|
|
17
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
18
|
+
let path = dir.join("main.plum");
|
|
19
|
+
std::fs::write(&path, src).unwrap();
|
|
20
|
+
let r = plum_core::loadAndMerge(&path, &repoRoot());
|
|
21
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
22
|
+
r.map_err(|e| format!("{e}"))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Compiles `src` through the real pipeline and runs its `main() -> Int`.
|
|
26
|
+
fn runInt(name: &str, src: &str) -> i64 {
|
|
27
|
+
let source = load(name, src).expect("load");
|
|
28
|
+
let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
|
|
29
|
+
wasmparser::validate(&bytes).expect("valid wasm");
|
|
30
|
+
let engine = gcEngine();
|
|
31
|
+
let module = wasmtime::Module::new(&engine, &bytes).unwrap();
|
|
32
|
+
let mut store = wasmtime::Store::new(&engine, ());
|
|
33
|
+
let mut linker = wasmtime::Linker::new(&engine);
|
|
34
|
+
linker
|
|
35
|
+
.define_unknown_imports_as_default_values(&mut store, &module)
|
|
36
|
+
.unwrap();
|
|
37
|
+
let instance = linker.instantiate(&mut store, &module).unwrap();
|
|
38
|
+
let main = instance
|
|
39
|
+
.get_typed_func::<(), i64>(&mut store, "main")
|
|
40
|
+
.unwrap();
|
|
41
|
+
main.call(&mut store, ()).unwrap()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/Slice\n\n";
|
|
46
|
+
|
|
47
|
+
fn prog(body: &str) -> String {
|
|
48
|
+
format!("{PRE}{body}")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn field_less_record_embedding_field_less_trait_dispatches_in_a_slice() {
|
|
53
|
+
let src = prog("\
|
|
54
|
+
trait Actor =
|
|
55
|
+
fun act(self, n: Int) -> Int = n
|
|
56
|
+
fun draw(self) -> Int = 1
|
|
57
|
+
|
|
58
|
+
record Panel(Actor) =
|
|
59
|
+
override fun draw(self) -> Int = 10
|
|
60
|
+
|
|
61
|
+
record Ship(Actor, speed: Int) =
|
|
62
|
+
override fun act(self, n: Int) -> Int = n + self.speed
|
|
63
|
+
override fun draw(self) -> Int = 100
|
|
64
|
+
|
|
65
|
+
fun main() -> Int =
|
|
66
|
+
xs: Slice[Actor] := Slice[Actor]()
|
|
67
|
+
xs.push(Panel())
|
|
68
|
+
xs.push(Ship(speed: 5))
|
|
69
|
+
total := 0
|
|
70
|
+
for i := range 2
|
|
71
|
+
total = total + xs[i].draw() + xs[i].act(1000)
|
|
72
|
+
total
|
|
73
|
+
");
|
|
74
|
+
// Panel: draw 10 + act 1000; Ship: draw 100 + act 1005.
|
|
75
|
+
assert_eq!(runInt("slice", &src), 2115);
|
|
76
|
+
}
|