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;
10use crate::renderer::camera::CameraBounds;
11
12#[derive(Clone, Debug)]
14pub enum BridgeAudioCommand {
15 LoadSound { id: u32, path: String },
16 PlaySound { id: u32, volume: f32, looping: bool },
17 StopSound { id: u32 },
18 StopAll,
19 SetMasterVolume { volume: f32 },
20}
21
22#[derive(Clone)]
25pub struct RenderBridgeState {
26 pub sprite_commands: Vec<SpriteCommand>,
27 pub camera_x: f32,
28 pub camera_y: f32,
29 pub camera_zoom: f32,
30 pub delta_time: f64,
31 pub keys_down: std::collections::HashSet<String>,
33 pub keys_pressed: std::collections::HashSet<String>,
34 pub mouse_x: f32,
35 pub mouse_y: f32,
36 pub texture_load_queue: Vec<(String, u32)>,
38 pub base_dir: PathBuf,
40 pub next_texture_id: u32,
42 pub texture_path_to_id: std::collections::HashMap<String, u32>,
44 pub tilemaps: TilemapStore,
46 pub ambient_light: [f32; 3],
48 pub point_lights: Vec<PointLight>,
50 pub audio_commands: Vec<BridgeAudioCommand>,
52 pub next_sound_id: u32,
54 pub sound_path_to_id: std::collections::HashMap<String, u32>,
56 pub font_texture_queue: Vec<u32>,
58 pub viewport_width: f32,
60 pub viewport_height: f32,
61 pub scale_factor: f32,
63 pub clear_color: [f32; 4],
65 pub save_dir: PathBuf,
67 pub shader_create_queue: Vec<(u32, String, String)>,
69 pub shader_param_queue: Vec<(u32, u32, [f32; 4])>,
71 pub next_shader_id: u32,
73 pub effect_create_queue: Vec<(u32, String)>,
75 pub effect_param_queue: Vec<(u32, u32, [f32; 4])>,
77 pub effect_remove_queue: Vec<u32>,
79 pub effect_clear: bool,
81 pub next_effect_id: u32,
83 pub camera_bounds: Option<CameraBounds>,
85}
86
87impl RenderBridgeState {
88 pub fn new(base_dir: PathBuf) -> Self {
89 let save_dir = base_dir.join(".arcane").join("saves");
90 Self {
91 sprite_commands: Vec::new(),
92 camera_x: 0.0,
93 camera_y: 0.0,
94 camera_zoom: 1.0,
95 delta_time: 0.0,
96 keys_down: std::collections::HashSet::new(),
97 keys_pressed: std::collections::HashSet::new(),
98 mouse_x: 0.0,
99 mouse_y: 0.0,
100 texture_load_queue: Vec::new(),
101 base_dir,
102 next_texture_id: 1,
103 texture_path_to_id: std::collections::HashMap::new(),
104 tilemaps: TilemapStore::new(),
105 ambient_light: [1.0, 1.0, 1.0],
106 point_lights: Vec::new(),
107 audio_commands: Vec::new(),
108 next_sound_id: 1,
109 sound_path_to_id: std::collections::HashMap::new(),
110 font_texture_queue: Vec::new(),
111 viewport_width: 800.0,
112 viewport_height: 600.0,
113 scale_factor: 1.0,
114 clear_color: [0.1, 0.1, 0.15, 1.0],
115 save_dir,
116 shader_create_queue: Vec::new(),
117 shader_param_queue: Vec::new(),
118 next_shader_id: 1,
119 effect_create_queue: Vec::new(),
120 effect_param_queue: Vec::new(),
121 effect_remove_queue: Vec::new(),
122 effect_clear: false,
123 next_effect_id: 1,
124 camera_bounds: None,
125 }
126 }
127}
128
129#[deno_core::op2(fast)]
132pub fn op_draw_sprite(
133 state: &mut OpState,
134 texture_id: u32,
135 x: f64,
136 y: f64,
137 w: f64,
138 h: f64,
139 layer: i32,
140 uv_x: f64,
141 uv_y: f64,
142 uv_w: f64,
143 uv_h: f64,
144 tint_r: f64,
145 tint_g: f64,
146 tint_b: f64,
147 tint_a: f64,
148 rotation: f64,
149 origin_x: f64,
150 origin_y: f64,
151 flip_x: f64,
152 flip_y: f64,
153 opacity: f64,
154 blend_mode: f64,
155 shader_id: f64,
156) {
157 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
158 bridge.borrow_mut().sprite_commands.push(SpriteCommand {
159 texture_id,
160 x: x as f32,
161 y: y as f32,
162 w: w as f32,
163 h: h as f32,
164 layer,
165 uv_x: uv_x as f32,
166 uv_y: uv_y as f32,
167 uv_w: uv_w as f32,
168 uv_h: uv_h as f32,
169 tint_r: tint_r as f32,
170 tint_g: tint_g as f32,
171 tint_b: tint_b as f32,
172 tint_a: tint_a as f32,
173 rotation: rotation as f32,
174 origin_x: origin_x as f32,
175 origin_y: origin_y as f32,
176 flip_x: flip_x != 0.0,
177 flip_y: flip_y != 0.0,
178 opacity: opacity as f32,
179 blend_mode: (blend_mode as u8).min(3),
180 shader_id: shader_id as u32,
181 });
182}
183
184#[deno_core::op2(fast)]
186pub fn op_clear_sprites(state: &mut OpState) {
187 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
188 bridge.borrow_mut().sprite_commands.clear();
189}
190
191#[deno_core::op2(fast)]
194pub fn op_set_camera(state: &mut OpState, x: f64, y: f64, zoom: f64) {
195 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
196 let mut b = bridge.borrow_mut();
197 b.camera_x = x as f32;
198 b.camera_y = y as f32;
199 b.camera_zoom = zoom as f32;
200}
201
202#[deno_core::op2]
204#[serde]
205pub fn op_get_camera(state: &mut OpState) -> Vec<f64> {
206 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
207 let b = bridge.borrow();
208 vec![b.camera_x as f64, b.camera_y as f64, b.camera_zoom as f64]
209}
210
211#[deno_core::op2(fast)]
214pub fn op_load_texture(state: &mut OpState, #[string] path: &str) -> u32 {
215 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
216 let mut b = bridge.borrow_mut();
217
218 let resolved = if std::path::Path::new(path).is_absolute() {
220 path.to_string()
221 } else {
222 b.base_dir.join(path).to_string_lossy().to_string()
223 };
224
225 if let Some(&id) = b.texture_path_to_id.get(&resolved) {
227 return id;
228 }
229
230 let id = b.next_texture_id;
231 b.next_texture_id += 1;
232 b.texture_path_to_id.insert(resolved.clone(), id);
233 b.texture_load_queue.push((resolved, id));
234 id
235}
236
237#[deno_core::op2(fast)]
239pub fn op_is_key_down(state: &mut OpState, #[string] key: &str) -> bool {
240 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
241 bridge.borrow().keys_down.contains(key)
242}
243
244#[deno_core::op2(fast)]
246pub fn op_is_key_pressed(state: &mut OpState, #[string] key: &str) -> bool {
247 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
248 bridge.borrow().keys_pressed.contains(key)
249}
250
251#[deno_core::op2]
253#[serde]
254pub fn op_get_mouse_position(state: &mut OpState) -> Vec<f64> {
255 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
256 let b = bridge.borrow();
257 vec![b.mouse_x as f64, b.mouse_y as f64]
258}
259
260#[deno_core::op2(fast)]
262pub fn op_get_delta_time(state: &mut OpState) -> f64 {
263 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
264 bridge.borrow().delta_time
265}
266
267#[deno_core::op2(fast)]
270pub fn op_create_solid_texture(
271 state: &mut OpState,
272 #[string] name: &str,
273 r: u32,
274 g: u32,
275 b: u32,
276 a: u32,
277) -> u32 {
278 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
279 let mut br = bridge.borrow_mut();
280
281 let key = format!("__solid__{name}");
282 if let Some(&id) = br.texture_path_to_id.get(&key) {
283 return id;
284 }
285
286 let id = br.next_texture_id;
287 br.next_texture_id += 1;
288 br.texture_path_to_id.insert(key.clone(), id);
289 br.texture_load_queue
291 .push((format!("__solid__:{name}:{r}:{g}:{b}:{a}"), id));
292 id
293}
294
295#[deno_core::op2(fast)]
297pub fn op_create_tilemap(
298 state: &mut OpState,
299 texture_id: u32,
300 width: u32,
301 height: u32,
302 tile_size: f64,
303 atlas_columns: u32,
304 atlas_rows: u32,
305) -> u32 {
306 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
307 bridge
308 .borrow_mut()
309 .tilemaps
310 .create(texture_id, width, height, tile_size as f32, atlas_columns, atlas_rows)
311}
312
313#[deno_core::op2(fast)]
315pub fn op_set_tile(state: &mut OpState, tilemap_id: u32, gx: u32, gy: u32, tile_id: u32) {
316 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
317 if let Some(tm) = bridge.borrow_mut().tilemaps.get_mut(tilemap_id) {
318 tm.set_tile(gx, gy, tile_id as u16);
319 }
320}
321
322#[deno_core::op2(fast)]
324pub fn op_get_tile(state: &mut OpState, tilemap_id: u32, gx: u32, gy: u32) -> u32 {
325 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
326 bridge
327 .borrow()
328 .tilemaps
329 .get(tilemap_id)
330 .map(|tm| tm.get_tile(gx, gy) as u32)
331 .unwrap_or(0)
332}
333
334#[deno_core::op2(fast)]
337pub fn op_draw_tilemap(state: &mut OpState, tilemap_id: u32, world_x: f64, world_y: f64, layer: i32) {
338 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
339 let mut b = bridge.borrow_mut();
340 let cam_x = b.camera_x;
341 let cam_y = b.camera_y;
342 let cam_zoom = b.camera_zoom;
343 let vp_w = 800.0;
345 let vp_h = 600.0;
346
347 if let Some(tm) = b.tilemaps.get(tilemap_id) {
348 let cmds = tm.bake_visible(world_x as f32, world_y as f32, layer, cam_x, cam_y, cam_zoom, vp_w, vp_h);
349 b.sprite_commands.extend(cmds);
350 }
351}
352
353#[deno_core::op2(fast)]
358pub fn op_set_ambient_light(state: &mut OpState, r: f64, g: f64, b: f64) {
359 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
360 bridge.borrow_mut().ambient_light = [r as f32, g as f32, b as f32];
361}
362
363#[deno_core::op2(fast)]
366pub fn op_add_point_light(
367 state: &mut OpState,
368 x: f64,
369 y: f64,
370 radius: f64,
371 r: f64,
372 g: f64,
373 b: f64,
374 intensity: f64,
375) {
376 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
377 bridge.borrow_mut().point_lights.push(PointLight {
378 x: x as f32,
379 y: y as f32,
380 radius: radius as f32,
381 r: r as f32,
382 g: g as f32,
383 b: b as f32,
384 intensity: intensity as f32,
385 });
386}
387
388#[deno_core::op2(fast)]
390pub fn op_clear_lights(state: &mut OpState) {
391 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
392 bridge.borrow_mut().point_lights.clear();
393}
394
395#[deno_core::op2(fast)]
399pub fn op_load_sound(state: &mut OpState, #[string] path: &str) -> u32 {
400 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
401 let mut b = bridge.borrow_mut();
402
403 let resolved = if std::path::Path::new(path).is_absolute() {
404 path.to_string()
405 } else {
406 b.base_dir.join(path).to_string_lossy().to_string()
407 };
408
409 if let Some(&id) = b.sound_path_to_id.get(&resolved) {
410 return id;
411 }
412
413 let id = b.next_sound_id;
414 b.next_sound_id += 1;
415 b.sound_path_to_id.insert(resolved.clone(), id);
416 b.audio_commands.push(BridgeAudioCommand::LoadSound { id, path: resolved });
417 id
418}
419
420#[deno_core::op2(fast)]
423pub fn op_play_sound(state: &mut OpState, id: u32, volume: f64, looping: bool) {
424 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
425 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::PlaySound { id, volume: volume as f32, looping });
426}
427
428#[deno_core::op2(fast)]
430pub fn op_stop_sound(state: &mut OpState, id: u32) {
431 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
432 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::StopSound { id });
433}
434
435#[deno_core::op2(fast)]
437pub fn op_stop_all_sounds(state: &mut OpState) {
438 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
439 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::StopAll);
440}
441
442#[deno_core::op2(fast)]
445pub fn op_set_master_volume(state: &mut OpState, volume: f64) {
446 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
447 bridge.borrow_mut().audio_commands.push(BridgeAudioCommand::SetMasterVolume { volume: volume as f32 });
448}
449
450#[deno_core::op2(fast)]
454pub fn op_create_font_texture(state: &mut OpState) -> u32 {
455 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
456 let mut b = bridge.borrow_mut();
457
458 let key = "__builtin_font__".to_string();
459 if let Some(&id) = b.texture_path_to_id.get(&key) {
460 return id;
461 }
462
463 let id = b.next_texture_id;
464 b.next_texture_id += 1;
465 b.texture_path_to_id.insert(key, id);
466 b.font_texture_queue.push(id);
467 id
468}
469
470#[deno_core::op2]
474#[serde]
475pub fn op_get_viewport_size(state: &mut OpState) -> Vec<f64> {
476 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
477 let b = bridge.borrow();
478 vec![b.viewport_width as f64, b.viewport_height as f64]
479}
480
481#[deno_core::op2(fast)]
483pub fn op_get_scale_factor(state: &mut OpState) -> f64 {
484 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
485 bridge.borrow().scale_factor as f64
486}
487
488#[deno_core::op2(fast)]
490pub fn op_set_background_color(state: &mut OpState, r: f64, g: f64, b: f64) {
491 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
492 let mut br = bridge.borrow_mut();
493 br.clear_color = [r as f32, g as f32, b as f32, 1.0];
494}
495
496#[deno_core::op2(fast)]
500pub fn op_save_file(state: &mut OpState, #[string] key: &str, #[string] value: &str) -> bool {
501 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
502 let save_dir = bridge.borrow().save_dir.clone();
503
504 if !key.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
506 return false;
507 }
508
509 if std::fs::create_dir_all(&save_dir).is_err() {
511 return false;
512 }
513
514 let path = save_dir.join(format!("{key}.json"));
515 std::fs::write(path, value).is_ok()
516}
517
518#[deno_core::op2]
520#[string]
521pub fn op_load_file(state: &mut OpState, #[string] key: &str) -> String {
522 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
523 let save_dir = bridge.borrow().save_dir.clone();
524
525 let path = save_dir.join(format!("{key}.json"));
526 std::fs::read_to_string(path).unwrap_or_default()
527}
528
529#[deno_core::op2(fast)]
531pub fn op_delete_file(state: &mut OpState, #[string] key: &str) -> bool {
532 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
533 let save_dir = bridge.borrow().save_dir.clone();
534
535 let path = save_dir.join(format!("{key}.json"));
536 std::fs::remove_file(path).is_ok()
537}
538
539#[deno_core::op2]
541#[serde]
542pub fn op_list_save_files(state: &mut OpState) -> Vec<String> {
543 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
544 let save_dir = bridge.borrow().save_dir.clone();
545
546 let mut keys = Vec::new();
547 if let Ok(entries) = std::fs::read_dir(&save_dir) {
548 for entry in entries.flatten() {
549 let path = entry.path();
550 if path.extension().map_or(false, |ext| ext == "json") {
551 if let Some(stem) = path.file_stem() {
552 keys.push(stem.to_string_lossy().to_string());
553 }
554 }
555 }
556 }
557 keys.sort();
558 keys
559}
560
561#[deno_core::op2(fast)]
565pub fn op_create_shader(state: &mut OpState, #[string] name: &str, #[string] source: &str) -> u32 {
566 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
567 let mut b = bridge.borrow_mut();
568 let id = b.next_shader_id;
569 b.next_shader_id += 1;
570 b.shader_create_queue
571 .push((id, name.to_string(), source.to_string()));
572 id
573}
574
575#[deno_core::op2(fast)]
577pub fn op_set_shader_param(
578 state: &mut OpState,
579 shader_id: u32,
580 index: u32,
581 x: f64,
582 y: f64,
583 z: f64,
584 w: f64,
585) {
586 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
587 bridge.borrow_mut().shader_param_queue.push((
588 shader_id,
589 index,
590 [x as f32, y as f32, z as f32, w as f32],
591 ));
592}
593
594#[deno_core::op2(fast)]
598pub fn op_add_effect(state: &mut OpState, #[string] effect_type: &str) -> u32 {
599 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
600 let mut b = bridge.borrow_mut();
601 let id = b.next_effect_id;
602 b.next_effect_id += 1;
603 b.effect_create_queue
604 .push((id, effect_type.to_string()));
605 id
606}
607
608#[deno_core::op2(fast)]
610pub fn op_set_effect_param(
611 state: &mut OpState,
612 effect_id: u32,
613 index: u32,
614 x: f64,
615 y: f64,
616 z: f64,
617 w: f64,
618) {
619 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
620 bridge.borrow_mut().effect_param_queue.push((
621 effect_id,
622 index,
623 [x as f32, y as f32, z as f32, w as f32],
624 ));
625}
626
627#[deno_core::op2(fast)]
629pub fn op_remove_effect(state: &mut OpState, effect_id: u32) {
630 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
631 bridge.borrow_mut().effect_remove_queue.push(effect_id);
632}
633
634#[deno_core::op2(fast)]
636pub fn op_clear_effects(state: &mut OpState) {
637 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
638 bridge.borrow_mut().effect_clear = true;
639}
640
641#[deno_core::op2(fast)]
645pub fn op_set_camera_bounds(state: &mut OpState, min_x: f64, min_y: f64, max_x: f64, max_y: f64) {
646 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
647 bridge.borrow_mut().camera_bounds = Some(CameraBounds {
648 min_x: min_x as f32,
649 min_y: min_y as f32,
650 max_x: max_x as f32,
651 max_y: max_y as f32,
652 });
653}
654
655#[deno_core::op2(fast)]
657pub fn op_clear_camera_bounds(state: &mut OpState) {
658 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
659 bridge.borrow_mut().camera_bounds = None;
660}
661
662#[deno_core::op2]
664#[serde]
665pub fn op_get_camera_bounds(state: &mut OpState) -> Vec<f64> {
666 let bridge = state.borrow_mut::<Rc<RefCell<RenderBridgeState>>>();
667 let b = bridge.borrow();
668 match b.camera_bounds {
669 Some(bounds) => vec![
670 bounds.min_x as f64,
671 bounds.min_y as f64,
672 bounds.max_x as f64,
673 bounds.max_y as f64,
674 ],
675 None => vec![],
676 }
677}
678
679deno_core::extension!(
680 render_ext,
681 ops = [
682 op_draw_sprite,
683 op_clear_sprites,
684 op_set_camera,
685 op_get_camera,
686 op_load_texture,
687 op_is_key_down,
688 op_is_key_pressed,
689 op_get_mouse_position,
690 op_get_delta_time,
691 op_create_solid_texture,
692 op_create_tilemap,
693 op_set_tile,
694 op_get_tile,
695 op_draw_tilemap,
696 op_set_ambient_light,
697 op_add_point_light,
698 op_clear_lights,
699 op_load_sound,
700 op_play_sound,
701 op_stop_sound,
702 op_stop_all_sounds,
703 op_set_master_volume,
704 op_create_font_texture,
705 op_get_viewport_size,
706 op_get_scale_factor,
707 op_set_background_color,
708 op_save_file,
709 op_load_file,
710 op_delete_file,
711 op_list_save_files,
712 op_create_shader,
713 op_set_shader_param,
714 op_add_effect,
715 op_set_effect_param,
716 op_remove_effect,
717 op_clear_effects,
718 op_set_camera_bounds,
719 op_clear_camera_bounds,
720 op_get_camera_bounds,
721 ],
722);