plum
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
- README.md +2 -0
- 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::
|
|
12
|
+
use crate::api::{AssetStore, Backend};
|
|
14
|
-
use crate::mq::
|
|
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<
|
|
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
|
|
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
|
-
|
|
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 ----------------------------------------
|