plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
532b997
— Peter John
2026-11-02T18:33:22+05:30
Move the game-engine std modules (Game, Scene, Color, Vec2) to plum-std/game; LSP std index is recursive
- README.md +4 -4
- plum-cli/src/main.rs +1 -1
- plum-cli/tests/host_stub_test.rs +3 -3
- plum-core/tests/loader_test.rs +26 -0
- plum-examples/game.plum +3 -3
- plum-examples/scene.plum +5 -5
- plum-game/src/api.rs +1 -1
- plum-game/tests/fixtures/assets_fixture.plum +3 -3
- plum-game/tests/fixtures/host_fixture.plum +3 -3
- plum-game/tests/fixtures/target_fixture.plum +3 -3
- plum-game/web-test/BENCHMARK.md +3 -3
- plum-game/web-test/fixtures/interleave.plum +3 -3
- plum-game/web-test/fixtures/rendertarget.plum +3 -3
- plum-game/web-test/fixtures/stress.plum +3 -3
- plum-game/web-test/fixtures/stress_scene.plum +5 -5
- plum-game/web-test/fixtures/stress_world.plum +5 -5
- plum-game/web-test/fixtures/text_sizes.plum +3 -3
- plum-game/web/plum_bridge.js +2 -2
- plum-lsp/src/completion.rs +29 -12
- plum-lsp/src/main.rs +9 -0
- plum-std/{Color.plum → game/Color.plum} +0 -0
- plum-std/{Game.plum → game/Game.plum} +2 -2
- plum-std/{Scene.plum → game/Scene.plum} +4 -4
- plum-std/{Vec2.plum → game/Vec2.plum} +0 -0
README.md
CHANGED
|
@@ -1043,9 +1043,9 @@ Games run in the browser on Plum's own WebGPU renderer (`plum-game/web/plum_brid
|
|
|
1043
1043
|
|
|
1044
1044
|
`plum-examples/game.plum` is a small arcade game: steer a ship with the arrow keys or WASD, collect orbs, dodge mines (Enter restarts). It draws shapes, text, additive orb glow (`blendMode(Blend.Add)`) and an animated sprite sheet (`plum-examples/assets/sprites.png`, generated by `plum-examples/tools/make_sprites.py`) and plays music and sound effects (`plum-examples/assets/*.wav`, generated by `plum-examples/tools/make_sounds.py`); both generators live outside the asset dir so they are not shipped. For a bigger game built on this engine see the Knights and Merchants port in the sibling repo `../kam_plum` (`./run.sh` there). Its rules are plain functions with `test` blocks, so `plum test plum-examples` covers them without a browser.
|
|
1045
1045
|
|
|
1046
|
-
**`std/Scene`** is a scene-graph library written in Plum (see [`plum-examples/scene.plum`](plum-examples/scene.plum)): a tree of `Node`s (group, sprite, rect, circle, text) under a `Stage`, with parent-relative position, rotation, scale and colour, per-node `act` behaviours, a camera and hit testing; its demo renders a minimap into a texture with `textureCreate` and `renderTarget`. Use it for UI and other hierarchical scenes, where moving a parent should move everything in it. For a large world, keep the game objects in flat records and lists instead (for example `enum Entity = Unit | House` with an `update` and a `draw` that the enum dispatches to its variants) and draw them directly through `std/Game`; a tree of thousands of nodes costs a closure call and a matrix composition per node every frame.
|
|
1046
|
+
**`std/game/Scene`** is a scene-graph library written in Plum (see [`plum-examples/scene.plum`](plum-examples/scene.plum)): a tree of `Node`s (group, sprite, rect, circle, text) under a `Stage`, with parent-relative position, rotation, scale and colour, per-node `act` behaviours, a camera and hit testing; its demo renders a minimap into a texture with `textureCreate` and `renderTarget`. Use it for UI and other hierarchical scenes, where moving a parent should move everything in it. For a large world, keep the game objects in flat records and lists instead (for example `enum Entity = Unit | House` with an `update` and a `draw` that the enum dispatches to its variants) and draw them directly through `std/game/Game`; a tree of thousands of nodes costs a closure call and a matrix composition per node every frame.
|
|
1047
1047
|
|
|
1048
|
-
`std/Scene` follows libGDX Scene2D for draw order and culling. A node's z-index is its position in its parent's child list (draw order is list order, hit testing goes in reverse):
|
|
1048
|
+
`std/game/Scene` follows libGDX Scene2D for draw order and culling. A node's z-index is its position in its parent's child list (draw order is list order, hit testing goes in reverse):
|
|
1049
1049
|
|
|
1050
1050
|
```plum
|
|
1051
1051
|
n.zIndex() # index among its siblings, -1 without a parent
|
|
@@ -1071,7 +1071,7 @@ export fun update(g: G, dt: Float)
|
|
|
1071
1071
|
export fun draw(g: G)
|
|
1072
1072
|
```
|
|
1073
1073
|
|
|
1074
|
-
The page calls `init` once after the assets are loaded, then `update` and `draw` every frame. A game imports `std/Game`, `std/Vec2` and `std/Color`; pure logic can import `std/Vec2` and `std/Color` alone. Under `plum test` and `plum run` the host functions are stubs that trap with `host function 'X' is not available here`, so `test` blocks work as long as they don't call the host.
|
|
1074
|
+
The page calls `init` once after the assets are loaded, then `update` and `draw` every frame. A game imports `std/game/Game`, `std/game/Vec2` and `std/game/Color`; pure logic can import `std/game/Vec2` and `std/game/Color` alone. Under `plum test` and `plum run` the host functions are stubs that trap with `host function 'X' is not available here`, so `test` blocks work as long as they don't call the host.
|
|
1075
1075
|
|
|
1076
1076
|
**Headless tests:** the `plum-game` crate (a standalone crate, wasmtime only) runs a compiled game with no browser against a `Recorder` backend that logs every host call as a string and answers input from scripted values, so `cargo test --manifest-path plum-game/Cargo.toml` checks games' draw-call sequences (`plum-game/tests/host_test.rs`). `plum-game <wasm> [--assets DIR] [--frames N]` does the same from the command line and prints the host calls of the last frame.
|
|
1077
1077
|
|
|
@@ -1081,7 +1081,7 @@ The page calls `init` once after the assets are loaded, then `update` and `draw`
|
|
|
1081
1081
|
|
|
1082
1082
|
**Sounds:** the browser decodes every `.wav` and `.ogg` file of the asset root before `init`, and `soundLoad` looks the decoded sound up by path with the same path rules as `textureLoad`. Browsers only start audio after a user gesture (a key press or click); sounds played before that are silent, and the music of the example begins once you press a key.
|
|
1083
1083
|
|
|
1084
|
-
**Host API** (`std/Game`; texture and sound handles are `-1` on failure; coordinates are world units once a camera is set):
|
|
1084
|
+
**Host API** (`std/game/Game`; texture and sound handles are `-1` on failure; coordinates are world units once a camera is set):
|
|
1085
1085
|
|
|
1086
1086
|
| function | meaning |
|
|
1087
1087
|
|---|---|
|
plum-cli/src/main.rs
CHANGED
|
@@ -605,7 +605,7 @@ fn hostImports(
|
|
|
605
605
|
},
|
|
606
606
|
)))
|
|
607
607
|
}
|
|
608
|
-
// A `plum` import this host doesn't define (the game API of `std/Game`)
|
|
608
|
+
// A `plum` import this host doesn't define (the game API of `std/game/Game`)
|
|
609
609
|
// is stubbed with a function that traps when called, so a program
|
|
610
610
|
// that imports it can still run or test code that never calls it.
|
|
611
611
|
("plum", n) => {
|
plum-cli/tests/host_stub_test.rs
CHANGED
|
@@ -36,7 +36,7 @@ fn runPlumTest(name: &str, source: &str) -> (bool, String) {
|
|
|
36
36
|
fn stubbed_host_import_unused_test_passes() {
|
|
37
37
|
let (ok, all) = runPlumTest(
|
|
38
38
|
"pass",
|
|
39
|
-
"import std/Int\nimport std/Game\n\ntest \"pure\"\n assert 1 + 1 == 2\n",
|
|
39
|
+
"import std/Int\nimport std/game/Game\n\ntest \"pure\"\n assert 1 + 1 == 2\n",
|
|
40
40
|
);
|
|
41
41
|
assert!(ok, "output: {all}");
|
|
42
42
|
assert!(all.contains("1 passed, 0 failed"), "output: {all}");
|
|
@@ -46,7 +46,7 @@ fn stubbed_host_import_unused_test_passes() {
|
|
|
46
46
|
fn stubbed_host_import_traps_when_called() {
|
|
47
47
|
let (ok, all) = runPlumTest(
|
|
48
48
|
"trap",
|
|
49
|
-
"import std/Int\nimport std/Float\nimport std/Game\n\ntest \"calls host\"\n assert screenWidth() > 0.0\n",
|
|
49
|
+
"import std/Int\nimport std/Float\nimport std/game/Game\n\ntest \"calls host\"\n assert screenWidth() > 0.0\n",
|
|
50
50
|
);
|
|
51
51
|
assert!(!ok, "output: {all}");
|
|
52
52
|
assert!(
|
|
@@ -59,7 +59,7 @@ fn stubbed_host_import_traps_when_called() {
|
|
|
59
59
|
fn real_host_functions_still_work_next_to_stubs() {
|
|
60
60
|
let (ok, all) = runPlumTest(
|
|
61
61
|
"real",
|
|
62
|
-
"import std/Int\nimport std/Str\nimport std/Os\nimport std/Game\n\ntest \"real\"\n printLn(\"hello\")\n assert 1 == 1\n",
|
|
62
|
+
"import std/Int\nimport std/Str\nimport std/Os\nimport std/game/Game\n\ntest \"real\"\n printLn(\"hello\")\n assert 1 == 1\n",
|
|
63
63
|
);
|
|
64
64
|
assert!(ok, "output: {all}");
|
|
65
65
|
assert!(all.contains("hello"), "output: {all}");
|
plum-core/tests/loader_test.rs
CHANGED
|
@@ -505,3 +505,29 @@ fn legacySingleSelfVariantIsAnError() {
|
|
|
505
505
|
"enum User has a single variant named User — write record User(...)"
|
|
506
506
|
);
|
|
507
507
|
}
|
|
508
|
+
|
|
509
|
+
#[test]
|
|
510
|
+
fn nestedStdImportResolvesToASubdirectory() {
|
|
511
|
+
let dir = TempDir::new().unwrap();
|
|
512
|
+
let lib_path = dir.path().join("libs");
|
|
513
|
+
writeLibFile(
|
|
514
|
+
&lib_path,
|
|
515
|
+
"plum-std/game/Util",
|
|
516
|
+
"module std\n\nfun helperFn(b: Bool) -> Bool =\n b\n",
|
|
517
|
+
);
|
|
518
|
+
let entry = writeLibFile(
|
|
519
|
+
&lib_path,
|
|
520
|
+
"main",
|
|
521
|
+
"module fixtures\n\nimport std/game/Util\n\nfun mainFn(b: Bool) -> Bool =\n helperFn(b)\n",
|
|
522
|
+
);
|
|
523
|
+
let source = loadAndMerge(&entry, &lib_path).expect("nested std import loads");
|
|
524
|
+
assert!(source.items.iter().any(
|
|
525
|
+
|i| matches!(i, plum_core::ast::Item::Fn(f) if f.name == "helperFn" && f.file.ends_with("game/Util.plum"))
|
|
526
|
+
));
|
|
527
|
+
let missing = writeLibFile(
|
|
528
|
+
&lib_path,
|
|
529
|
+
"other",
|
|
530
|
+
"module fixtures\n\nimport std/Util\n\nfun f(b: Bool) -> Bool =\n b\n",
|
|
531
|
+
);
|
|
532
|
+
assert!(loadAndMerge(&missing, &lib_path).is_err());
|
|
533
|
+
}
|
plum-examples/game.plum
CHANGED
|
@@ -13,9 +13,9 @@ import std/Float
|
|
|
13
13
|
import std/List
|
|
14
14
|
import std/Math
|
|
15
15
|
import std/Random
|
|
16
|
-
import std/Vec2
|
|
16
|
+
import std/game/Vec2
|
|
17
|
-
import std/Color
|
|
17
|
+
import std/game/Color
|
|
18
|
-
import std/Game
|
|
18
|
+
import std/game/Game
|
|
19
19
|
|
|
20
20
|
primitive Rules =
|
|
21
21
|
ArenaW = 800.0
|
plum-examples/scene.plum
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Clockwork: a scene-graph demo (`plum game plum-examples/scene.plum`). A sun, four
|
|
2
|
-
# planets and their moons orbit by nesting rotating groups (std/Scene): each orbit
|
|
2
|
+
# planets and their moons orbit by nesting rotating groups (std/game/Scene): each orbit
|
|
3
3
|
# is a group whose rotation carries everything inside it, so a moon only ever
|
|
4
4
|
# says "I sit 40 units from my planet" and the maths composes the rest. Click a
|
|
5
5
|
# body to select it (a halo and its name), click again or the background to clear;
|
|
@@ -17,10 +17,10 @@ import std/Int
|
|
|
17
17
|
import std/Float
|
|
18
18
|
import std/List
|
|
19
19
|
import std/Math
|
|
20
|
-
import std/Vec2
|
|
20
|
+
import std/game/Vec2
|
|
21
|
-
import std/Color
|
|
21
|
+
import std/game/Color
|
|
22
|
-
import std/Game
|
|
22
|
+
import std/game/Game
|
|
23
|
-
import std/Scene
|
|
23
|
+
import std/game/Scene
|
|
24
24
|
|
|
25
25
|
primitive Layout =
|
|
26
26
|
Width = 800.0
|
plum-game/src/api.rs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//! The host API a Plum game programs against: one `Backend` method per
|
|
2
|
-
//! extern in `plum-std/Game.plum`, in Rust types (`Float` is `f32`, `Int` is
|
|
2
|
+
//! extern in `plum-std/game/Game.plum`, in Rust types (`Float` is `f32`, `Int` is
|
|
3
3
|
//! `i32`, `Str` is `&str`, a `Color` is `[f32; 4]`, a `Vec2` is `(f32, f32)`).
|
|
4
4
|
//! `Int` handles are indexes into host vectors, `-1` on failure. The browser
|
|
5
5
|
//! bridge (`web/plum_bridge.js`) implements the same API; this crate's
|
plum-game/tests/fixtures/assets_fixture.plum
CHANGED
|
@@ -3,9 +3,9 @@ import std/Int
|
|
|
3
3
|
import std/Float
|
|
4
4
|
import std/Str
|
|
5
5
|
import std/Os
|
|
6
|
-
import std/Vec2
|
|
6
|
+
import std/game/Vec2
|
|
7
|
-
import std/Color
|
|
7
|
+
import std/game/Color
|
|
8
|
-
import std/Game
|
|
8
|
+
import std/game/Game
|
|
9
9
|
|
|
10
10
|
record State(frames: Int)
|
|
11
11
|
|
plum-game/tests/fixtures/host_fixture.plum
CHANGED
|
@@ -3,9 +3,9 @@ import std/Int
|
|
|
3
3
|
import std/Float
|
|
4
4
|
import std/Str
|
|
5
5
|
import std/Os
|
|
6
|
-
import std/Vec2
|
|
6
|
+
import std/game/Vec2
|
|
7
|
-
import std/Color
|
|
7
|
+
import std/game/Color
|
|
8
|
-
import std/Game
|
|
8
|
+
import std/game/Game
|
|
9
9
|
|
|
10
10
|
record State(frames: Int)
|
|
11
11
|
|
plum-game/tests/fixtures/target_fixture.plum
CHANGED
|
@@ -2,9 +2,9 @@ module target_fixture
|
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Float
|
|
4
4
|
import std/Str
|
|
5
|
-
import std/Vec2
|
|
5
|
+
import std/game/Vec2
|
|
6
|
-
import std/Color
|
|
6
|
+
import std/game/Color
|
|
7
|
-
import std/Game
|
|
7
|
+
import std/game/Game
|
|
8
8
|
|
|
9
9
|
record State(canvas: Int, bad: Int)
|
|
10
10
|
|
plum-game/web-test/BENCHMARK.md
CHANGED
|
@@ -14,7 +14,7 @@ the Plum `Scene` tree). Plan: `docs/plans/2026-11-02-render-experiments.md`.
|
|
|
14
14
|
- Machine: Apple M3 Pro (`sysctl machdep.cpu.brand_string`), `arm64` (`uname -m`), macOS 26.6.2, 120 Hz display
|
|
15
15
|
- Browser: Playwright Chromium build 1248 (156.0.8078.4); headless runs use its `chrome-headless-shell`
|
|
16
16
|
- Scene (1280x720, camera scale 0.8): 5000 moving rotated sprites (4-frame sheet, source rect + flipX), 500 rects, 200 circles, 50 texts whose number changes every frame. Motion is a function of the frame counter, so every run draws the same frames.
|
|
17
|
-
- Fixtures (`fixtures/`): `stress.plum` (immediate, `std/Game`), `stress_cmd.plum` (immediate, `std/GameCmd` command buffer), `stress_scene.plum` (`std/Scene` tree, per-call), `stress_scene_cmd.plum` (`std/SceneCmd` tree, command buffer).
|
|
17
|
+
- Fixtures (`fixtures/`): `stress.plum` (immediate, `std/game/Game`), `stress_cmd.plum` (immediate, `std/GameCmd` command buffer), `stress_scene.plum` (`std/game/Scene` tree, per-call), `stress_scene_cmd.plum` (`std/SceneCmd` tree, command buffer).
|
|
18
18
|
- Command: `plum-web-test <fixture> --renderer R [--headed] --uncapped --measure 10 --frames-wait 6 --json`. `--uncapped` passes `--disable-gpu-vsync --disable-frame-rate-limit`, so the rAF interval is the work per frame, not the display refresh. 6 s warm-up, then 10 s measured.
|
|
19
19
|
- Every cell was run twice; the table shows the run with the better average and the range of the two averages. Cells ran back to back on an otherwise idle laptop (no thermal control), so treat differences under about 5% as noise.
|
|
20
20
|
- Phase split (`update_ms`, `draw_ms`, `render_ms` in the JSON): mean ms per frame measured in the page with `performance.now()` around `update`, `draw` and the rest. The counters exist only while `--measure` runs (`globalThis.__plumMeasure`; a null check per frame otherwise).
|
|
@@ -57,7 +57,7 @@ Headless is software rendering: the GPU work runs on the CPU and competes with t
|
|
|
57
57
|
|
|
58
58
|
| fixture | update ms | draw ms (run 1 / 2) | present ms (waits for vsync) |
|
|
59
59
|
|---|---|---|---|
|
|
60
|
-
| stress (per-call `std/Game`) | 0.009 | 0.814 / 0.809 | 7.51 / 7.52 |
|
|
60
|
+
| stress (per-call `std/game/Game`) | 0.009 | 0.814 / 0.809 | 7.51 / 7.52 |
|
|
61
61
|
| stress_cmd (`std/GameCmd`) | 0.010 / 0.009 | 0.538 / 0.532 | 7.78 / 7.80 |
|
|
62
62
|
|
|
63
63
|
## Analysis
|
|
@@ -101,7 +101,7 @@ Same machine and method as above (headed, uncapped, `--measure 10 --frames-wait
|
|
|
101
101
|
- Interleaved textures: 4000 draw calls became 1 (the bound is ceil(textures / 16)), and the frame time fell from 1.3 ms to 0.45 ms (-65%); render time (encode + submit) from 1.0 ms to 0.15 ms.
|
|
102
102
|
- Stress: the 16-slot batcher alone cost about 5-8% (the vertex grew from 20 to 24 bytes for the slot), within run-to-run spread; the 50 changing strings used to cost 50 canvas rasterisations plus 50 texture uploads per frame, and the SDF atlas removes both, so the scene is now about 40% faster than before (0.98-1.03 ms against 1.58-1.67 ms) with 1 draw call instead of 52.
|
|
103
103
|
|
|
104
|
-
## Scene culling and y-sort (std/Scene)
|
|
104
|
+
## Scene culling and y-sort (std/game/Scene)
|
|
105
105
|
|
|
106
106
|
Fixture `stress_world.plum`: one world layer with 20,000 sprites in a 4000x2300 world, a 1280x720 view at scale 1 (about 2,000 sprites in view while the camera drifts), 500 sprites walking each frame, and `layer.sortChildren(|a, b| a.y < b.y)` every frame. `World.Culling = 1` also calls `setCullingArea` with the camera view; the "no culling" row is the same file with `Culling = 0`. Same machine and method as above (headed, gpu per-call, `--uncapped --measure 10 --frames-wait 6`); one run per cell, so differences under about 5% are noise.
|
|
107
107
|
|
plum-game/web-test/fixtures/interleave.plum
CHANGED
|
@@ -6,9 +6,9 @@ import std/Str
|
|
|
6
6
|
import std/Int
|
|
7
7
|
import std/Float
|
|
8
8
|
import std/Array
|
|
9
|
-
import std/Vec2
|
|
9
|
+
import std/game/Vec2
|
|
10
|
-
import std/Color
|
|
10
|
+
import std/game/Color
|
|
11
|
-
import std/Game
|
|
11
|
+
import std/game/Game
|
|
12
12
|
|
|
13
13
|
primitive Scene =
|
|
14
14
|
Sprites = 4000
|
plum-game/web-test/fixtures/rendertarget.plum
CHANGED
|
@@ -14,9 +14,9 @@ module rendertarget
|
|
|
14
14
|
import std/Str
|
|
15
15
|
import std/Int
|
|
16
16
|
import std/Float
|
|
17
|
-
import std/Vec2
|
|
17
|
+
import std/game/Vec2
|
|
18
|
-
import std/Color
|
|
18
|
+
import std/game/Color
|
|
19
|
-
import std/Game
|
|
19
|
+
import std/game/Game
|
|
20
20
|
|
|
21
21
|
record Targets(big: Int, small: Int, glass: Int, t: Int)
|
|
22
22
|
|
plum-game/web-test/fixtures/stress.plum
CHANGED
|
@@ -6,9 +6,9 @@ module stress
|
|
|
6
6
|
import std/Str
|
|
7
7
|
import std/Int
|
|
8
8
|
import std/Float
|
|
9
|
-
import std/Vec2
|
|
9
|
+
import std/game/Vec2
|
|
10
|
-
import std/Color
|
|
10
|
+
import std/game/Color
|
|
11
|
-
import std/Game
|
|
11
|
+
import std/game/Game
|
|
12
12
|
|
|
13
13
|
primitive Scene =
|
|
14
14
|
Sprites = 5000
|
plum-game/web-test/fixtures/stress_scene.plum
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# The stress scene of stress.plum built as a Scene2D-like scene graph (std/Scene):
|
|
1
|
+
# The stress scene of stress.plum built as a Scene2D-like scene graph (std/game/Scene):
|
|
2
2
|
# 5000 sprites in 50 slowly rotating groups, 500 rects, 200 circles and 50 texts
|
|
3
3
|
# with a changing number, drawn through a camera. Every node moves in its own
|
|
4
4
|
# `act` behaviour, so each frame runs ~5800 closures and a composed-transform draw.
|
|
@@ -7,10 +7,10 @@ import std/Str
|
|
|
7
7
|
import std/Int
|
|
8
8
|
import std/Float
|
|
9
9
|
import std/List
|
|
10
|
-
import std/Vec2
|
|
10
|
+
import std/game/Vec2
|
|
11
|
-
import std/Color
|
|
11
|
+
import std/game/Color
|
|
12
|
-
import std/Game
|
|
12
|
+
import std/game/Game
|
|
13
|
-
import std/Scene
|
|
13
|
+
import std/game/Scene
|
|
14
14
|
|
|
15
15
|
primitive Counts =
|
|
16
16
|
Sprites = 5000
|
plum-game/web-test/fixtures/stress_world.plum
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# A large y-sorted world through the scene graph (std/Scene): 20000 sprites in one
|
|
1
|
+
# A large y-sorted world through the scene graph (std/game/Scene): 20000 sprites in one
|
|
2
2
|
# world layer, about 2000 of them in the 1280x720 view. Every frame a few hundred
|
|
3
3
|
# sprites walk, the layer is y-sorted with `sortChildren`, and (when `Culling` is
|
|
4
4
|
# 1) the camera view is set as the layer's culling area so only the visible
|
|
@@ -9,10 +9,10 @@ import std/Int
|
|
|
9
9
|
import std/Float
|
|
10
10
|
import std/Math
|
|
11
11
|
import std/List
|
|
12
|
-
import std/Vec2
|
|
12
|
+
import std/game/Vec2
|
|
13
|
-
import std/Color
|
|
13
|
+
import std/game/Color
|
|
14
|
-
import std/Game
|
|
14
|
+
import std/game/Game
|
|
15
|
-
import std/Scene
|
|
15
|
+
import std/game/Scene
|
|
16
16
|
|
|
17
17
|
primitive World =
|
|
18
18
|
Sprites = 20000
|
plum-game/web-test/fixtures/text_sizes.plum
CHANGED
|
@@ -5,9 +5,9 @@ module text_sizes
|
|
|
5
5
|
import std/Str
|
|
6
6
|
import std/Int
|
|
7
7
|
import std/Float
|
|
8
|
-
import std/Vec2
|
|
8
|
+
import std/game/Vec2
|
|
9
|
-
import std/Color
|
|
9
|
+
import std/game/Color
|
|
10
|
-
import std/Game
|
|
10
|
+
import std/game/Game
|
|
11
11
|
|
|
12
12
|
record Sizes(t: Int)
|
|
13
13
|
|
plum-game/web/plum_bridge.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// backend is WebGPU where the browser has it; there is no fallback (a page
|
|
9
9
|
// without navigator.gpu says so).
|
|
10
10
|
//
|
|
11
|
-
// Each std/Game draw extern appends vertices to the batcher directly.
|
|
11
|
+
// Each std/game/Game draw extern appends vertices to the batcher directly.
|
|
12
12
|
// Plum's `Str` is a GC struct JS can't read: it is read through the module's
|
|
13
13
|
// `strLen`/`strByte` exports and built with `strNew`/`strSetByte`.
|
|
14
14
|
(function () {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
// --- input ----------------------------------------------------------------------
|
|
50
50
|
|
|
51
|
-
// KeyboardEvent.code -> GLFW key number (the numbering of `Key` in Game.plum).
|
|
51
|
+
// KeyboardEvent.code -> GLFW key number (the numbering of `Key` in game/Game.plum).
|
|
52
52
|
function glfwKey(code) {
|
|
53
53
|
if (code.length === 4 && code.startsWith("Key")) return code.charCodeAt(3);
|
|
54
54
|
if (code.length === 6 && code.startsWith("Digit")) return code.charCodeAt(5);
|
plum-lsp/src/completion.rs
CHANGED
|
@@ -446,24 +446,27 @@ pub fn memberItems(
|
|
|
446
446
|
items
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
/// Maps every top-level name declared anywhere under `<lib_path>/plum-std/
|
|
449
|
+
/// Maps every top-level name declared anywhere under `<lib_path>/plum-std/`
|
|
450
|
+
/// (subdirectories included) to that file's `import` path (`Str.plum` ->
|
|
450
|
-
///
|
|
451
|
+
/// `"std/Str"`, `game/Scene.plum` -> `"std/game/Scene"`) — built by parsing
|
|
451
452
|
/// each file standalone (no import resolution needed just to list names).
|
|
452
453
|
/// Meant to be computed once and cached by the caller.
|
|
453
454
|
pub fn stdLibIndex(lib_path: &Path) -> HashMap<String, String> {
|
|
454
455
|
let mut map = HashMap::new();
|
|
455
456
|
let std_dir = lib_path.join("plum-std");
|
|
457
|
+
let mut files = Vec::new();
|
|
458
|
+
collectPlumFiles(&std_dir, &mut files);
|
|
459
|
+
files.sort();
|
|
460
|
+
for path in files {
|
|
456
|
-
|
|
461
|
+
let Ok(rel) = path.strip_prefix(&std_dir) else {
|
|
457
|
-
return map;
|
|
458
|
-
};
|
|
459
|
-
for entry in entries.flatten() {
|
|
460
|
-
let path = entry.path();
|
|
461
|
-
if path.extension().and_then(|e| e.to_str()) != Some("plum") {
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
|
|
465
462
|
continue;
|
|
466
463
|
};
|
|
464
|
+
let rel = rel.with_extension("");
|
|
465
|
+
let parts: Vec<String> = rel
|
|
466
|
+
.components()
|
|
467
|
+
.map(|c| c.as_os_str().to_string_lossy().into_owned())
|
|
468
|
+
.collect();
|
|
469
|
+
let import_path = format!("std/{}", parts.join("/"));
|
|
467
470
|
let Ok(text) = std::fs::read_to_string(&path) else {
|
|
468
471
|
continue;
|
|
469
472
|
};
|
|
@@ -478,7 +481,6 @@ pub fn stdLibIndex(lib_path: &Path) -> HashMap<String, String> {
|
|
|
478
481
|
continue;
|
|
479
482
|
};
|
|
480
483
|
let source = plum_core::AstParser::new(&text).parseSource(tree.root_node());
|
|
481
|
-
let import_path = format!("std/{stem}");
|
|
482
484
|
for item in &source.items {
|
|
483
485
|
let name = match item {
|
|
484
486
|
ast::Item::Fn(f) if f.type_param.is_none() => Some(f.name.clone()),
|
|
@@ -507,6 +509,21 @@ pub fn stdLibIndex(lib_path: &Path) -> HashMap<String, String> {
|
|
|
507
509
|
map
|
|
508
510
|
}
|
|
509
511
|
|
|
512
|
+
/// Every `.plum` file under `dir`, recursively.
|
|
513
|
+
fn collectPlumFiles(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
|
|
514
|
+
let Ok(entries) = std::fs::read_dir(dir) else {
|
|
515
|
+
return;
|
|
516
|
+
};
|
|
517
|
+
for entry in entries.flatten() {
|
|
518
|
+
let path = entry.path();
|
|
519
|
+
if path.is_dir() {
|
|
520
|
+
collectPlumFiles(&path, out);
|
|
521
|
+
} else if path.extension().and_then(|e| e.to_str()) == Some("plum") {
|
|
522
|
+
out.push(path);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
510
527
|
/// A `TextEdit` inserting `import <import_path>` right after the last
|
|
511
528
|
/// existing `import` line (or after the `module` line, or at the very top of
|
|
512
529
|
/// the file if neither exists) — plain line scanning, since `ast::Import`
|
plum-lsp/src/main.rs
CHANGED
|
@@ -531,9 +531,18 @@ mod tests {
|
|
|
531
531
|
("Testing", "std/Testing"),
|
|
532
532
|
("Http", "std/Http"),
|
|
533
533
|
("Int", "std/Int"),
|
|
534
|
+
("Stage", "std/game/Scene"),
|
|
535
|
+
("drawRect", "std/game/Game"),
|
|
536
|
+
("Vec2", "std/game/Vec2"),
|
|
534
537
|
] {
|
|
535
538
|
assert_eq!(index.get(name).map(String::as_str), Some(path), "{name}");
|
|
536
539
|
}
|
|
540
|
+
// Completing a game name offers `std/game/Game` and auto-imports that path.
|
|
541
|
+
let edit = completion::importInsertionEdit(
|
|
542
|
+
"module t\nimport std/Int\n",
|
|
543
|
+
index.get("drawRect").expect("drawRect indexed"),
|
|
544
|
+
);
|
|
545
|
+
assert_eq!(edit.new_text, "import std/game/Game\n");
|
|
537
546
|
let dir = std::env::temp_dir().join(format!("plum-lsp-ns-{}", std::process::id()));
|
|
538
547
|
std::fs::create_dir_all(&dir).unwrap();
|
|
539
548
|
let path = dir.join("t.plum");
|
plum-std/{Color.plum → game/Color.plum}
RENAMED
|
File without changes
|
plum-std/{Game.plum → game/Game.plum}
RENAMED
|
@@ -2,8 +2,8 @@ module std
|
|
|
2
2
|
import std/Int
|
|
3
3
|
import std/Float
|
|
4
4
|
import std/Str
|
|
5
|
-
import std/Vec2
|
|
5
|
+
import std/game/Vec2
|
|
6
|
-
import std/Color
|
|
6
|
+
import std/game/Color
|
|
7
7
|
|
|
8
8
|
# The host API of a game: drawing, textures, sounds, files and input, all
|
|
9
9
|
# provided by the game host (`plum game`). Texture and sound handles are -1 on
|
plum-std/{Scene.plum → game/Scene.plum}
RENAMED
|
@@ -4,9 +4,9 @@ import std/Float
|
|
|
4
4
|
import std/Str
|
|
5
5
|
import std/List
|
|
6
6
|
import std/Math
|
|
7
|
-
import std/Vec2
|
|
7
|
+
import std/game/Vec2
|
|
8
|
-
import std/Color
|
|
8
|
+
import std/game/Color
|
|
9
|
-
import std/Game
|
|
9
|
+
import std/game/Game
|
|
10
10
|
|
|
11
11
|
# A Scene2D-like scene graph: a tree of `Node`s under a `Stage`. A node has a
|
|
12
12
|
# position, size, origin (the pivot of its rotation and scale), scale, rotation
|
|
@@ -20,7 +20,7 @@ import std/Game
|
|
|
20
20
|
# stage.root.add(ring)
|
|
21
21
|
# ring.add(Node.rect(40.0, 20.0, Color.Red).at(10.0, 5.0))
|
|
22
22
|
# stage.act(dt) # runs every node's `act` behaviour, depth first
|
|
23
|
-
# stage.draw() # draws through `std/Game`
|
|
23
|
+
# stage.draw() # draws through `std/game/Game`
|
|
24
24
|
#
|
|
25
25
|
# The transform maths (`Affine`, `localMatrix`, `worldMatrix`, `flatten`,
|
|
26
26
|
# `hitNode`) never calls the host, so it runs under `plum test`.
|
plum-std/{Vec2.plum → game/Vec2.plum}
RENAMED
|
File without changes
|