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 scale_factor: f32,
62 pub clear_color: [f32; 4],
64 pub save_dir: PathBuf,
66}
67
68impl RenderBridgeState {
69 pub fn new(base_dir: PathBuf) -> Self {
70 let save_dir = base_dir.join(".arcane").join("saves");
71 Self {
72 sprite_commands: Vec::new(),
73 camera_x: 0.0,
74 camera_y: 0.0,
75 camera_zoom: 1.0,
76 delta_time: 0.0,
77 keys_down: std::collections::HashSet::new(),
78 keys_pressed: std::collections::HashSet::new(),
79 mouse_x: 0.0,
80 mouse_y: 0.0,
81 texture_load_queue: Vec::new(),
82 base_dir,
83 next_texture_id: 1,
84 texture_path_to_id: std::collections::HashMap::new(),
85 tilemaps: TilemapStore::new(),
86 ambient_light: [1.0, 1.0, 1.0],
87 point_lights: Vec::new(),
88 audio_commands: Vec::new(),
89 next_sound_id: 1,
90 sound_path_to_id: std::collections::HashMap::new(),
91 font_texture_queue: Vec::new(),
92 viewport_width: 800.0,
93 viewport_height: 600.0,
94 scale_factor: 1.0,
95 clear_color: [0.1, 0.1, 0.15, 1.0],
96 save_dir,
97 }
98 }
99}
100
101#[deno_core::op2(fast)]
104pub fn op_draw_sprite(
105 state: &mut OpState,
106 texture_id: u32,
107 x: f64,
108 y: f64,
109 w: f64,
110 h: f64,
111 layer: i32,
112 uv_x: f64,
113 uv_y: f64,
114 uv_w: f64,
115 uv_h: f64,
116 tint_r: f64,
117 tint_g: f64,
118 tint_b: f64,
119 tint_a: f64,
120) {
121 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
122 bridge.borrow_mut().sprite_commands.push(SpriteCommand {
123 texture_id,
124 x: x as f32,
125 y: y as f32,
126 w: w as f32,
127 h: h as f32,
128 layer,
129 uv_x: uv_x as f32,
130 uv_y: uv_y as f32,
131 uv_w: uv_w as f32,
132 uv_h: uv_h as f32,
133 tint_r: tint_r as f32,
134 tint_g: tint_g as f32,
135 tint_b: tint_b as f32,
136 tint_a: tint_a as f32,
137 });
138}
139
140#[deno_core::op2(fast)]
142pub fn op_clear_sprites(state: &mut OpState) {
143 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
144 bridge.borrow_mut().sprite_commands.clear();
145}
146
147#[deno_core::op2(fast)]
150pub fn op_set_camera(state: &mut OpState, x: f64, y: f64, zoom: f64) {
151 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
152 let mut b = bridge.borrow_mut();
153 b.camera_x = x as f32;
154 b.camera_y = y as f32;
155 b.camera_zoom = zoom as f32;
156}
157
158#[deno_core::op2]
160#[serde]
161pub fn op_get_camera(state: &mut OpState) -> Vec<f64> {
162 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
163 let b = bridge.borrow();
164 vec![b.camera_x as f64, b.camera_y as f64, b.camera_zoom as f64]
165}
166
167#[deno_core::op2(fast)]
170pub fn op_load_texture(state: &mut OpState, #[string] path: &str) -> u32 {
171 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
172 let mut b = bridge.borrow_mut();
173
174 let resolved = if std::path::Path::new(path).is_absolute() {
176 path.to_string()
177 } else {
178 b.base_dir.join(path).to_string_lossy().to_string()
179 };
180
181 if let Some(&id) = b.texture_path_to_id.get(&resolved) {
183 return id;
184 }
185
186 let id = b.next_texture_id;
187 b.next_texture_id += 1;
188 b.texture_path_to_id.insert(resolved.clone(), id);
189 b.texture_load_queue.push((resolved, id));
190 id
191}
192
193#[deno_core::op2(fast)]
195pub fn op_is_key_down(state: &mut OpState, #[string] key: &str) -> bool {
196 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
197 bridge.borrow().keys_down.contains(key)
198}
199
200#[deno_core::op2(fast)]
202pub fn op_is_key_pressed(state: &mut OpState, #[string] key: &str) -> bool {
203 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
204 bridge.borrow().keys_pressed.contains(key)
205}
206
207#[deno_core::op2]
209#[serde]
210pub fn op_get_mouse_position(state: &mut OpState) -> Vec<f64> {
211 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
212 let b = bridge.borrow();
213 vec![b.mouse_x as f64, b.mouse_y as f64]
214}
215
216#[deno_core::op2(fast)]
218pub fn op_get_delta_time(state: &mut OpState) -> f64 {
219 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
220 bridge.borrow().delta_time
221}
222
223#[deno_core::op2(fast)]
226pub fn op_create_solid_texture(
227 state: &mut OpState,
228 #[string] name: &str,
229 r: u32,
230 g: u32,
231 b: u32,
232 a: u32,
233) -> u32 {
234 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
235 let mut br = bridge.borrow_mut();
236
237 let key = format!("__solid__{name}");
238 if let Some(&id) = br.texture_path_to_id.get(&key) {
239 return id;
240 }
241
242 let id = br.next_texture_id;
243 br.next_texture_id += 1;
244 br.texture_path_to_id.insert(key.clone(), id);
245 br.texture_load_queue
247 .push((format!("__solid__:{name}:{r}:{g}:{b}:{a}"), id));
248 id
249}
250
251#[deno_core::op2(fast)]
253pub fn op_create_tilemap(
254 state: &mut OpState,
255 texture_id: u32,
256 width: u32,
257 height: u32,
258 tile_size: f64,
259 atlas_columns: u32,
260 atlas_rows: u32,
261) -> u32 {
262 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
263 bridge
264 .borrow_mut()
265 .tilemaps
266 .create(texture_id, width, height, tile_size as f32, atlas_columns, atlas_rows)
267}
268
269#[deno_core::op2(fast)]
271pub fn op_set_tile(state: &mut OpState, tilemap_id: u32, gx: u32, gy: u32, tile_id: u32) {
272 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
273 if let Some(tm) = bridge.borrow_mut().tilemaps.get_mut(tilemap_id) {
274 tm.set_tile(gx, gy, tile_id as u16);
275 }
276}
277
278#[deno_core::op2(fast)]
280pub fn op_get_tile(state: &mut OpState, tilemap_id: u32, gx: u32, gy: u32) -> u32 {
281 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
282 bridge
283 .borrow()
284 .tilemaps
285 .get(tilemap_id)
286 .map(|tm| tm.get_tile(gx, gy) as u32)
287 .unwrap_or(0)
288}
289
290#[deno_core::op2(fast)]
293pub fn op_draw_tilemap(state: &mut OpState, tilemap_id: u32, world_x: f64, world_y: f64, layer: i32) {
294 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
295 let mut b = bridge.borrow_mut();
296 let cam_x = b.camera_x;
297 let cam_y = b.camera_y;
298 let cam_zoom = b.camera_zoom;
299 let vp_w = 800.0;
301 let vp_h = 600.0;
302
303 if let Some(tm) = b.tilemaps.get(tilemap_id) {
304 let cmds = tm.bake_visible(world_x as f32, world_y as f32, layer, cam_x, cam_y, cam_zoom, vp_w, vp_h);
305 b.sprite_commands.extend(cmds);
306 }
307}
308
309#[deno_core::op2(fast)]
314pub fn op_set_ambient_light(state: &mut OpState, r: f64, g: f64, b: f64) {
315 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
316 bridge.borrow_mut().ambient_light = [r as f32, g as f32, b as f32];
317}
318
319#[deno_core::op2(fast)]
322pub fn op_add_point_light(
323 state: &mut OpState,
324 x: f64,
325 y: f64,
326 radius: f64,
327 r: f64,
328 g: f64,
329 b: f64,
330 intensity: f64,
331) {
332 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
333 bridge.borrow_mut().point_lights.push(PointLight {
334 x: x as f32,
335 y: y as f32,
336 radius: radius as f32,
337 r: r as f32,
338 g: g as f32,
339 b: b as f32,
340 intensity: intensity as f32,
341 });
342}
343
344#[deno_core::op2(fast)]
346pub fn op_clear_lights(state: &mut OpState) {
347 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
348 bridge.borrow_mut().point_lights.clear();
349}
350
351#[deno_core::op2(fast)]
355pub fn op_load_sound(state: &mut OpState, #[string] path: &str) -> u32 {
356 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
357 let mut b = bridge.borrow_mut();
358
359 let resolved = if std::path::Path::new(path).is_absolute() {
360 path.to_string()
361 } else {
362 b.base_dir.join(path).to_string_lossy().to_string()
363 };
364
365 if let Some(&id) = b.sound_path_to_id.get(&resolved) {
366 return id;
367 }
368
369 let id = b.next_sound_id;
370 b.next_sound_id += 1;
371 b.sound_path_to_id.insert(resolved.clone(), id);
372 b.audio_commands.push(BridgeAudioCommand::LoadSound { id, path: resolved });
373 id
374}
375
376#[deno_core::op2(fast)]
379pub fn op_play_sound(state: &mut OpState, id: u32, volume: f64, looping: bool) {
380 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
381 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::PlaySound { id, volume: volume as f32, looping });
382}
383
384#[deno_core::op2(fast)]
386pub fn op_stop_sound(state: &mut OpState, id: u32) {
387 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
388 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::StopSound { id });
389}
390
391#[deno_core::op2(fast)]
393pub fn op_stop_all_sounds(state: &mut OpState) {
394 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
395 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::StopAll);
396}
397
398#[deno_core::op2(fast)]
401pub fn op_set_master_volume(state: &mut OpState, volume: f64) {
402 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
403 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::SetMasterVolume { volume: volume as f32 });
404}
405
406#[deno_core::op2(fast)]
410pub fn op_create_font_texture(state: &mut OpState) -> u32 {
411 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
412 let mut b = bridge.borrow_mut();
413
414 let key = "__builtin_font__".to_string();
415 if let Some(&id) = b.texture_path_to_id.get(&key) {
416 return id;
417 }
418
419 let id = b.next_texture_id;
420 b.next_texture_id += 1;
421 b.texture_path_to_id.insert(key, id);
422 b.font_texture_queue.push(id);
423 id
424}
425
426#[deno_core::op2]
430#[serde]
431pub fn op_get_viewport_size(state: &mut OpState) -> Vec<f64> {
432 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
433 let b = bridge.borrow();
434 vec![b.viewport_width as f64, b.viewport_height as f64]
435}
436
437#[deno_core::op2(fast)]
439pub fn op_get_scale_factor(state: &mut OpState) -> f64 {
440 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
441 bridge.borrow().scale_factor as f64
442}
443
444#[deno_core::op2(fast)]
446pub fn op_set_background_color(state: &mut OpState, r: f64, g: f64, b: f64) {
447 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
448 let mut br = bridge.borrow_mut();
449 br.clear_color = [r as f32, g as f32, b as f32, 1.0];
450}
451
452#[deno_core::op2(fast)]
456pub fn op_save_file(state: &mut OpState, #[string] key: &str, #[string] value: &str) -> bool {
457 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
458 let save_dir = bridge.borrow().save_dir.clone();
459
460 if !key.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
462 return false;
463 }
464
465 if std::fs::create_dir_all(&save_dir).is_err() {
467 return false;
468 }
469
470 let path = save_dir.join(format!("{key}.json"));
471 std::fs::write(path, value).is_ok()
472}
473
474#[deno_core::op2]
476#[string]
477pub fn op_load_file(state: &mut OpState, #[string] key: &str) -> String {
478 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
479 let save_dir = bridge.borrow().save_dir.clone();
480
481 let path = save_dir.join(format!("{key}.json"));
482 std::fs::read_to_string(path).unwrap_or_default()
483}
484
485#[deno_core::op2(fast)]
487pub fn op_delete_file(state: &mut OpState, #[string] key: &str) -> bool {
488 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
489 let save_dir = bridge.borrow().save_dir.clone();
490
491 let path = save_dir.join(format!("{key}.json"));
492 std::fs::remove_file(path).is_ok()
493}
494
495#[deno_core::op2]
497#[serde]
498pub fn op_list_save_files(state: &mut OpState) -> Vec<String> {
499 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
500 let save_dir = bridge.borrow().save_dir.clone();
501
502 let mut keys = Vec::new();
503 if let Ok(entries) = std::fs::read_dir(&save_dir) {
504 for entry in entries.flatten() {
505 let path = entry.path();
506 if path.extension().map_or(false, |ext| ext == "json") {
507 if let Some(stem) = path.file_stem() {
508 keys.push(stem.to_string_lossy().to_string());
509 }
510 }
511 }
512 }
513 keys.sort();
514 keys
515}
516
517deno_core::extension!(
518 render_ext,
519 ops = [
520 op_draw_sprite,
521 op_clear_sprites,
522 op_set_camera,
523 op_get_camera,
524 op_load_texture,
525 op_is_key_down,
526 op_is_key_pressed,
527 op_get_mouse_position,
528 op_get_delta_time,
529 op_create_solid_texture,
530 op_create_tilemap,
531 op_set_tile,
532 op_get_tile,
533 op_draw_tilemap,
534 op_set_ambient_light,
535 op_add_point_light,
536 op_clear_lights,
537 op_load_sound,
538 op_play_sound,
539 op_stop_sound,
540 op_stop_all_sounds,
541 op_set_master_volume,
542 op_create_font_texture,
543 op_get_viewport_size,
544 op_get_scale_factor,
545 op_set_background_color,
546 op_save_file,
547 op_load_file,
548 op_delete_file,
549 op_list_save_files,
550 ],
551);