1use std::cell::RefCell;
2use std::path::PathBuf;
3use std::rc::Rc;
4
5use deno_core::OpState;
6
7use crate::renderer::SpriteCommand;
8use crate::renderer::TilemapStore;
9use crate::renderer::PointLight;
10
11#[derive(Clone, Debug)]
13pub enum BridgeAudioCommand {
14 LoadSound { id: u32, path: String },
15 PlaySound { id: u32, volume: f32, looping: bool },
16 StopSound { id: u32 },
17 StopAll,
18 SetMasterVolume { volume: f32 },
19}
20
21#[derive(Clone)]
24pub struct RenderBridgeState {
25 pub sprite_commands: Vec<SpriteCommand>,
26 pub camera_x: f32,
27 pub camera_y: f32,
28 pub camera_zoom: f32,
29 pub delta_time: f64,
30 pub keys_down: std::collections::HashSet<String>,
32 pub keys_pressed: std::collections::HashSet<String>,
33 pub mouse_x: f32,
34 pub mouse_y: f32,
35 pub texture_load_queue: Vec<(String, u32)>,
37 pub base_dir: PathBuf,
39 pub next_texture_id: u32,
41 pub texture_path_to_id: std::collections::HashMap<String, u32>,
43 pub tilemaps: TilemapStore,
45 pub ambient_light: [f32; 3],
47 pub point_lights: Vec<PointLight>,
49 pub audio_commands: Vec<BridgeAudioCommand>,
51 pub next_sound_id: u32,
53 pub sound_path_to_id: std::collections::HashMap<String, u32>,
55 pub font_texture_queue: Vec<u32>,
57 pub viewport_width: f32,
59 pub viewport_height: f32,
60 pub save_dir: PathBuf,
62}
63
64impl RenderBridgeState {
65 pub fn new(base_dir: PathBuf) -> Self {
66 let save_dir = base_dir.join(".arcane").join("saves");
67 Self {
68 sprite_commands: Vec::new(),
69 camera_x: 0.0,
70 camera_y: 0.0,
71 camera_zoom: 1.0,
72 delta_time: 0.0,
73 keys_down: std::collections::HashSet::new(),
74 keys_pressed: std::collections::HashSet::new(),
75 mouse_x: 0.0,
76 mouse_y: 0.0,
77 texture_load_queue: Vec::new(),
78 base_dir,
79 next_texture_id: 1,
80 texture_path_to_id: std::collections::HashMap::new(),
81 tilemaps: TilemapStore::new(),
82 ambient_light: [1.0, 1.0, 1.0],
83 point_lights: Vec::new(),
84 audio_commands: Vec::new(),
85 next_sound_id: 1,
86 sound_path_to_id: std::collections::HashMap::new(),
87 font_texture_queue: Vec::new(),
88 viewport_width: 800.0,
89 viewport_height: 600.0,
90 save_dir,
91 }
92 }
93}
94
95#[deno_core::op2(fast)]
98pub fn op_draw_sprite(
99 state: &mut OpState,
100 texture_id: u32,
101 x: f64,
102 y: f64,
103 w: f64,
104 h: f64,
105 layer: i32,
106 uv_x: f64,
107 uv_y: f64,
108 uv_w: f64,
109 uv_h: f64,
110 tint_r: f64,
111 tint_g: f64,
112 tint_b: f64,
113 tint_a: f64,
114) {
115 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
116 bridge.borrow_mut().sprite_commands.push(SpriteCommand {
117 texture_id,
118 x: x as f32,
119 y: y as f32,
120 w: w as f32,
121 h: h as f32,
122 layer,
123 uv_x: uv_x as f32,
124 uv_y: uv_y as f32,
125 uv_w: uv_w as f32,
126 uv_h: uv_h as f32,
127 tint_r: tint_r as f32,
128 tint_g: tint_g as f32,
129 tint_b: tint_b as f32,
130 tint_a: tint_a as f32,
131 });
132}
133
134#[deno_core::op2(fast)]
136pub fn op_clear_sprites(state: &mut OpState) {
137 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
138 bridge.borrow_mut().sprite_commands.clear();
139}
140
141#[deno_core::op2(fast)]
144pub fn op_set_camera(state: &mut OpState, x: f64, y: f64, zoom: f64) {
145 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
146 let mut b = bridge.borrow_mut();
147 b.camera_x = x as f32;
148 b.camera_y = y as f32;
149 b.camera_zoom = zoom as f32;
150}
151
152#[deno_core::op2]
154#[serde]
155pub fn op_get_camera(state: &mut OpState) -> Vec<f64> {
156 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
157 let b = bridge.borrow();
158 vec![b.camera_x as f64, b.camera_y as f64, b.camera_zoom as f64]
159}
160
161#[deno_core::op2(fast)]
164pub fn op_load_texture(state: &mut OpState, #[string] path: &str) -> u32 {
165 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
166 let mut b = bridge.borrow_mut();
167
168 let resolved = if std::path::Path::new(path).is_absolute() {
170 path.to_string()
171 } else {
172 b.base_dir.join(path).to_string_lossy().to_string()
173 };
174
175 if let Some(&id) = b.texture_path_to_id.get(&resolved) {
177 return id;
178 }
179
180 let id = b.next_texture_id;
181 b.next_texture_id += 1;
182 b.texture_path_to_id.insert(resolved.clone(), id);
183 b.texture_load_queue.push((resolved, id));
184 id
185}
186
187#[deno_core::op2(fast)]
189pub fn op_is_key_down(state: &mut OpState, #[string] key: &str) -> bool {
190 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
191 bridge.borrow().keys_down.contains(key)
192}
193
194#[deno_core::op2(fast)]
196pub fn op_is_key_pressed(state: &mut OpState, #[string] key: &str) -> bool {
197 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
198 bridge.borrow().keys_pressed.contains(key)
199}
200
201#[deno_core::op2]
203#[serde]
204pub fn op_get_mouse_position(state: &mut OpState) -> Vec<f64> {
205 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
206 let b = bridge.borrow();
207 vec![b.mouse_x as f64, b.mouse_y as f64]
208}
209
210#[deno_core::op2(fast)]
212pub fn op_get_delta_time(state: &mut OpState) -> f64 {
213 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
214 bridge.borrow().delta_time
215}
216
217#[deno_core::op2(fast)]
220pub fn op_create_solid_texture(
221 state: &mut OpState,
222 #[string] name: &str,
223 r: u32,
224 g: u32,
225 b: u32,
226 a: u32,
227) -> u32 {
228 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
229 let mut br = bridge.borrow_mut();
230
231 let key = format!("__solid__{name}");
232 if let Some(&id) = br.texture_path_to_id.get(&key) {
233 return id;
234 }
235
236 let id = br.next_texture_id;
237 br.next_texture_id += 1;
238 br.texture_path_to_id.insert(key.clone(), id);
239 br.texture_load_queue
241 .push((format!("__solid__:{name}:{r}:{g}:{b}:{a}"), id));
242 id
243}
244
245#[deno_core::op2(fast)]
247pub fn op_create_tilemap(
248 state: &mut OpState,
249 texture_id: u32,
250 width: u32,
251 height: u32,
252 tile_size: f64,
253 atlas_columns: u32,
254 atlas_rows: u32,
255) -> u32 {
256 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
257 bridge
258 .borrow_mut()
259 .tilemaps
260 .create(texture_id, width, height, tile_size as f32, atlas_columns, atlas_rows)
261}
262
263#[deno_core::op2(fast)]
265pub fn op_set_tile(state: &mut OpState, tilemap_id: u32, gx: u32, gy: u32, tile_id: u32) {
266 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
267 if let Some(tm) = bridge.borrow_mut().tilemaps.get_mut(tilemap_id) {
268 tm.set_tile(gx, gy, tile_id as u16);
269 }
270}
271
272#[deno_core::op2(fast)]
274pub fn op_get_tile(state: &mut OpState, tilemap_id: u32, gx: u32, gy: u32) -> u32 {
275 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
276 bridge
277 .borrow()
278 .tilemaps
279 .get(tilemap_id)
280 .map(|tm| tm.get_tile(gx, gy) as u32)
281 .unwrap_or(0)
282}
283
284#[deno_core::op2(fast)]
287pub fn op_draw_tilemap(state: &mut OpState, tilemap_id: u32, world_x: f64, world_y: f64, layer: i32) {
288 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
289 let mut b = bridge.borrow_mut();
290 let cam_x = b.camera_x;
291 let cam_y = b.camera_y;
292 let cam_zoom = b.camera_zoom;
293 let vp_w = 800.0;
295 let vp_h = 600.0;
296
297 if let Some(tm) = b.tilemaps.get(tilemap_id) {
298 let cmds = tm.bake_visible(world_x as f32, world_y as f32, layer, cam_x, cam_y, cam_zoom, vp_w, vp_h);
299 b.sprite_commands.extend(cmds);
300 }
301}
302
303#[deno_core::op2(fast)]
308pub fn op_set_ambient_light(state: &mut OpState, r: f64, g: f64, b: f64) {
309 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
310 bridge.borrow_mut().ambient_light = [r as f32, g as f32, b as f32];
311}
312
313#[deno_core::op2(fast)]
316pub fn op_add_point_light(
317 state: &mut OpState,
318 x: f64,
319 y: f64,
320 radius: f64,
321 r: f64,
322 g: f64,
323 b: f64,
324 intensity: f64,
325) {
326 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
327 bridge.borrow_mut().point_lights.push(PointLight {
328 x: x as f32,
329 y: y as f32,
330 radius: radius as f32,
331 r: r as f32,
332 g: g as f32,
333 b: b as f32,
334 intensity: intensity as f32,
335 });
336}
337
338#[deno_core::op2(fast)]
340pub fn op_clear_lights(state: &mut OpState) {
341 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
342 bridge.borrow_mut().point_lights.clear();
343}
344
345#[deno_core::op2(fast)]
349pub fn op_load_sound(state: &mut OpState, #[string] path: &str) -> u32 {
350 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
351 let mut b = bridge.borrow_mut();
352
353 let resolved = if std::path::Path::new(path).is_absolute() {
354 path.to_string()
355 } else {
356 b.base_dir.join(path).to_string_lossy().to_string()
357 };
358
359 if let Some(&id) = b.sound_path_to_id.get(&resolved) {
360 return id;
361 }
362
363 let id = b.next_sound_id;
364 b.next_sound_id += 1;
365 b.sound_path_to_id.insert(resolved.clone(), id);
366 b.audio_commands.push(BridgeAudioCommand::LoadSound { id, path: resolved });
367 id
368}
369
370#[deno_core::op2(fast)]
373pub fn op_play_sound(state: &mut OpState, id: u32, volume: f64, looping: bool) {
374 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
375 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::PlaySound { id, volume: volume as f32, looping });
376}
377
378#[deno_core::op2(fast)]
380pub fn op_stop_sound(state: &mut OpState, id: u32) {
381 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
382 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::StopSound { id });
383}
384
385#[deno_core::op2(fast)]
387pub fn op_stop_all_sounds(state: &mut OpState) {
388 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
389 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::StopAll);
390}
391
392#[deno_core::op2(fast)]
395pub fn op_set_master_volume(state: &mut OpState, volume: f64) {
396 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
397 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::SetMasterVolume { volume: volume as f32 });
398}
399
400#[deno_core::op2(fast)]
404pub fn op_create_font_texture(state: &mut OpState) -> u32 {
405 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
406 let mut b = bridge.borrow_mut();
407
408 let key = "__builtin_font__".to_string();
409 if let Some(&id) = b.texture_path_to_id.get(&key) {
410 return id;
411 }
412
413 let id = b.next_texture_id;
414 b.next_texture_id += 1;
415 b.texture_path_to_id.insert(key, id);
416 b.font_texture_queue.push(id);
417 id
418}
419
420#[deno_core::op2]
424#[serde]
425pub fn op_get_viewport_size(state: &mut OpState) -> Vec<f64> {
426 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
427 let b = bridge.borrow();
428 vec![b.viewport_width as f64, b.viewport_height as f64]
429}
430
431#[deno_core::op2(fast)]
435pub fn op_save_file(state: &mut OpState, #[string] key: &str, #[string] value: &str) -> bool {
436 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
437 let save_dir = bridge.borrow().save_dir.clone();
438
439 if !key.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
441 return false;
442 }
443
444 if std::fs::create_dir_all(&save_dir).is_err() {
446 return false;
447 }
448
449 let path = save_dir.join(format!("{key}.json"));
450 std::fs::write(path, value).is_ok()
451}
452
453#[deno_core::op2]
455#[string]
456pub fn op_load_file(state: &mut OpState, #[string] key: &str) -> String {
457 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
458 let save_dir = bridge.borrow().save_dir.clone();
459
460 let path = save_dir.join(format!("{key}.json"));
461 std::fs::read_to_string(path).unwrap_or_default()
462}
463
464#[deno_core::op2(fast)]
466pub fn op_delete_file(state: &mut OpState, #[string] key: &str) -> bool {
467 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
468 let save_dir = bridge.borrow().save_dir.clone();
469
470 let path = save_dir.join(format!("{key}.json"));
471 std::fs::remove_file(path).is_ok()
472}
473
474#[deno_core::op2]
476#[serde]
477pub fn op_list_save_files(state: &mut OpState) -> Vec<String> {
478 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
479 let save_dir = bridge.borrow().save_dir.clone();
480
481 let mut keys = Vec::new();
482 if let Ok(entries) = std::fs::read_dir(&save_dir) {
483 for entry in entries.flatten() {
484 let path = entry.path();
485 if path.extension().map_or(false, |ext| ext == "json") {
486 if let Some(stem) = path.file_stem() {
487 keys.push(stem.to_string_lossy().to_string());
488 }
489 }
490 }
491 }
492 keys.sort();
493 keys
494}
495
496deno_core::extension!(
497 render_ext,
498 ops = [
499 op_draw_sprite,
500 op_clear_sprites,
501 op_set_camera,
502 op_get_camera,
503 op_load_texture,
504 op_is_key_down,
505 op_is_key_pressed,
506 op_get_mouse_position,
507 op_get_delta_time,
508 op_create_solid_texture,
509 op_create_tilemap,
510 op_set_tile,
511 op_get_tile,
512 op_draw_tilemap,
513 op_set_ambient_light,
514 op_add_point_light,
515 op_clear_lights,
516 op_load_sound,
517 op_play_sound,
518 op_stop_sound,
519 op_stop_all_sounds,
520 op_set_master_volume,
521 op_create_font_texture,
522 op_get_viewport_size,
523 op_save_file,
524 op_load_file,
525 op_delete_file,
526 op_list_save_files,
527 ],
528);