plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


79646b4 — Peter John 2026-11-02T14:04:34+05:30
T5: web bridge + `plum game` + README
Files changed (2) hide show
  1. README.md +2 -0
  2. plum-game/src/web.rs +5 -188
README.md CHANGED
@@ -1027,6 +1027,8 @@ export fun draw(g: G)
1027
1027
 
1028
1028
  The host calls `init` once after the window exists, 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.
1029
1029
 
1030
+ **Headless check:** `PLUM_GAME_CAPTURE=out.png plum-game game.wasm` runs `PLUM_GAME_FRAMES` frames (default 30), saves the framebuffer as an upright PNG and exits 0.
1031
+
1030
1032
  **Assets** are never part of the repository. The asset root is `--assets DIR`, else `$PLUM_GAME_ASSETS`, else `../kam_macroquad/assets` beside the repository. `--web` copies only the preloaded files into `dist/assets`.
1031
1033
 
1032
1034
  **Host API** (`std/Game`; `Int` handles are `-1` on failure; coordinates are world units once a camera is set):
plum-game/src/web.rs CHANGED
@@ -9,12 +9,9 @@
9
9
  //! buffer, copies the Plum `Str` into it with `strLen`/`strByte`, passes
10
10
  //! `(ptr, len)` to the `pg_*` function and releases the buffer with `pg_free`.
11
11
 
12
- use crate::api::{quarter_turns_radians, AssetStore, Backend, Color};
13
- use crate::assets::{Atlas, MapData};
12
+ use crate::api::{AssetStore, Backend};
14
- use crate::mq::{key_code, mouse_button};
13
+ use crate::mq::MqBackend;
15
- use macroquad::prelude as mq;
16
14
  use std::cell::RefCell;
17
- use std::collections::HashMap;
18
15
 
19
16
  extern "C" {
20
17
  /// 1 once `plum.wasm` is instantiated and its exports are callable.
@@ -46,198 +43,18 @@ pub fn bridge_draw() {
46
43
  unsafe { plum_bridge_draw() }
47
44
  }
48
45
 
49
- const MAPELEM: &str = "data/defines/mapelem.dat";
50
-
51
- /// The files fetched before the first frame, keyed by their path relative to
52
- /// the asset root. Atlases and maps are served from here.
53
- pub struct WebBackend {
54
- files: HashMap<String, Vec<u8>>,
55
- assets: AssetStore,
56
- textures: Vec<Vec<Option<mq::Texture2D>>>,
57
- cam: (f32, f32, f32),
58
- }
59
-
60
46
  thread_local! {
61
- static BACKEND: RefCell<WebBackend> = RefCell::new(WebBackend::new());
47
+ static BACKEND: RefCell<MqBackend> = RefCell::new(MqBackend::with_assets(AssetStore::preloaded()));
62
48
  }
63
49
 
64
50
  /// Runs `f` on the backend. `f` never calls back into JS.
65
- fn with<R>(f: impl FnOnce(&mut WebBackend) -> R) -> R {
51
+ fn with<R>(f: impl FnOnce(&mut MqBackend) -> R) -> R {
66
52
  BACKEND.with(|b| f(&mut b.borrow_mut()))
67
53
  }
68
54
 
69
55
  /// Registers a fetched file under its asset-root-relative path.
70
56
  pub fn preload(path: &str, bytes: Vec<u8>) {
71
- with(|b| {
72
- b.files.insert(path.to_string(), bytes);
57
+ with(|b| b.assets_mut().insert(path, bytes));
73
- });
74
- }
75
-
76
- fn col(c: Color) -> mq::Color {
77
- mq::Color::new(c[0], c[1], c[2], c[3])
78
- }
79
-
80
- impl WebBackend {
81
- fn new() -> Self {
82
- WebBackend { files: HashMap::new(), assets: AssetStore::new(None), textures: Vec::new(), cam: (0.0, 0.0, 1.0) }
83
- }
84
-
85
- fn to_screen(&self, x: f32, y: f32) -> (f32, f32) {
86
- ((x - self.cam.0) * self.cam.2, (y - self.cam.1) * self.cam.2)
87
- }
88
-
89
- fn texture(&mut self, atlas: i32, idx: i32) -> Option<mq::Texture2D> {
90
- let (a, i) = (usize::try_from(atlas).ok()?, usize::try_from(idx).ok()?);
91
- while self.textures.len() <= a {
92
- let n = self.assets.atlases.get(self.textures.len()).map_or(0, |at| at.sprites.len());
93
- self.textures.push(vec![None; n]);
94
- }
95
- if i >= self.textures[a].len() {
96
- return None;
97
- }
98
- if self.textures[a][i].is_none() {
99
- let s = self.assets.atlases.get(a)?.sprites.get(i)?.as_ref()?;
100
- let t = mq::Texture2D::from_rgba8(s.w, s.h, &s.rgba);
101
- t.set_filter(mq::FilterMode::Nearest);
102
- self.textures[a][i] = Some(t);
103
- }
104
- self.textures[a][i].clone()
105
- }
106
- }
107
-
108
- impl Backend for WebBackend {
109
- fn screen_width(&mut self) -> f32 {
110
- mq::screen_width()
111
- }
112
- fn screen_height(&mut self) -> f32 {
113
- mq::screen_height()
114
- }
115
- fn frame_time(&mut self) -> f32 {
116
- mq::get_frame_time()
117
- }
118
- fn clear(&mut self, c: Color) {
119
- mq::clear_background(col(c));
120
- }
121
- fn draw_rect(&mut self, x: f32, y: f32, w: f32, h: f32, c: Color) {
122
- let (sx, sy) = self.to_screen(x, y);
123
- mq::draw_rectangle(sx, sy, w * self.cam.2, h * self.cam.2, col(c));
124
- }
125
- fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, t: f32, c: Color) {
126
- let (a, b) = (self.to_screen(x1, y1), self.to_screen(x2, y2));
127
- mq::draw_line(a.0, a.1, b.0, b.1, t * self.cam.2, col(c));
128
- }
129
- fn draw_text(&mut self, s: &str, x: f32, y: f32, size: f32, c: Color) {
130
- let (sx, sy) = self.to_screen(x, y);
131
- let px = size * self.cam.2;
132
- let dims = mq::measure_text(s, None, px.max(1.0) as u16, 1.0);
133
- mq::draw_text(s, sx, sy + dims.offset_y, px, col(c));
134
- }
135
- fn set_camera(&mut self, x: f32, y: f32, scale: f32) {
136
- self.cam = (x, y, scale);
137
- }
138
- fn reset_camera(&mut self) {
139
- self.cam = (0.0, 0.0, 1.0);
140
- }
141
- fn atlas_load(&mut self, name: &str) -> i32 {
142
- let Some(bytes) = self.files.get(&format!("sprites/{name}.rxx")) else { return -1 };
143
- match Atlas::parse(bytes) {
144
- Ok(a) => {
145
- self.assets.atlases.push(a);
146
- self.assets.atlases.len() as i32 - 1
147
- }
148
- Err(_) => -1,
149
- }
150
- }
151
- fn sprite_count(&mut self, a: i32) -> i32 {
152
- self.assets.sprite_count(a)
153
- }
154
- fn sprite_w(&mut self, a: i32, i: i32) -> i32 {
155
- self.assets.sprite_w(a, i)
156
- }
157
- fn sprite_h(&mut self, a: i32, i: i32) -> i32 {
158
- self.assets.sprite_h(a, i)
159
- }
160
- fn sprite_pivot_x(&mut self, a: i32, i: i32) -> i32 {
161
- self.assets.sprite_pivot_x(a, i)
162
- }
163
- fn sprite_pivot_y(&mut self, a: i32, i: i32) -> i32 {
164
- self.assets.sprite_pivot_y(a, i)
165
- }
166
- fn sprite_color(&mut self, a: i32, i: i32) -> Color {
167
- self.assets.sprite_color(a, i)
168
- }
169
- fn draw_sprite(&mut self, a: i32, i: i32, x: f32, y: f32, flip_x: bool, tint: Color) {
170
- let Some(tex) = self.texture(a, i) else { return };
171
- let (px, py) = (self.assets.sprite_pivot_x(a, i) as f32, self.assets.sprite_pivot_y(a, i) as f32);
172
- let (sx, sy) = self.to_screen(x + px, y + py);
173
- let s = self.cam.2;
174
- let params = mq::DrawTextureParams { dest_size: Some(mq::vec2(tex.width() * s, tex.height() * s)), flip_x, ..Default::default() };
175
- mq::draw_texture_ex(&tex, sx, sy, col(tint), params);
176
- }
177
- fn draw_tile(&mut self, a: i32, i: i32, x: f32, y: f32, size: f32, quarter_turns: i32, tint: Color) {
178
- let Some(tex) = self.texture(a, i) else { return };
179
- let (sx, sy) = self.to_screen(x, y);
180
- let side = size * self.cam.2;
181
- let params = mq::DrawTextureParams {
182
- dest_size: Some(mq::vec2(side, side)),
183
- rotation: quarter_turns_radians(quarter_turns),
184
- pivot: Some(mq::vec2(sx + side / 2.0, sy + side / 2.0)),
185
- ..Default::default()
186
- };
187
- mq::draw_texture_ex(&tex, sx, sy, col(tint), params);
188
- }
189
- fn is_key_down(&mut self, k: i32) -> bool {
190
- key_code(k).is_some_and(mq::is_key_down)
191
- }
192
- fn is_key_pressed(&mut self, k: i32) -> bool {
193
- key_code(k).is_some_and(mq::is_key_pressed)
194
- }
195
- fn mouse_pos(&mut self) -> (f32, f32) {
196
- mq::mouse_position()
197
- }
198
- fn is_mouse_down(&mut self, b: i32) -> bool {
199
- mouse_button(b).is_some_and(mq::is_mouse_button_down)
200
- }
201
- fn is_mouse_pressed(&mut self, b: i32) -> bool {
202
- mouse_button(b).is_some_and(mq::is_mouse_button_pressed)
203
- }
204
- fn mouse_wheel(&mut self) -> f32 {
205
- let dy = mq::mouse_wheel().1;
206
- if dy == 0.0 {
207
- 0.0
208
- } else {
209
- dy.signum()
210
- }
211
- }
212
- fn map_load(&mut self, path: &str) -> i32 {
213
- let Some(bytes) = self.files.get(path) else { return -1 };
214
- let mapelem = self.files.get(MAPELEM).map_or(&[][..], |v| v.as_slice());
215
- match MapData::parse_with(bytes, mapelem) {
216
- Ok(m) => {
217
- self.assets.maps.push(m);
218
- self.assets.maps.len() as i32 - 1
219
- }
220
- Err(_) => -1,
221
- }
222
- }
223
- fn map_width(&mut self, m: i32) -> i32 {
224
- self.assets.map_width(m)
225
- }
226
- fn map_height(&mut self, m: i32) -> i32 {
227
- self.assets.map_height(m)
228
- }
229
- fn map_terrain(&mut self, m: i32, x: i32, y: i32) -> i32 {
230
- self.assets.map_terrain(m, x, y)
231
- }
232
- fn map_rotation(&mut self, m: i32, x: i32, y: i32) -> i32 {
233
- self.assets.map_rotation(m, x, y)
234
- }
235
- fn map_elevation(&mut self, m: i32, x: i32, y: i32) -> i32 {
236
- self.assets.map_elevation(m, x, y)
237
- }
238
- fn map_object(&mut self, m: i32, x: i32, y: i32) -> i32 {
239
- self.assets.map_object(m, x, y)
240
- }
241
58
  }
242
59
 
243
60
  // --- exports called by plum_bridge.js ----------------------------------------