1use std::cell::RefCell;
2use std::path::Path;
3use std::rc::Rc;
4
5use boa_engine::builtins::promise::PromiseState;
6#[cfg(target_arch = "wasm32")]
7use boa_engine::module::IdleModuleLoader;
8#[cfg(not(target_arch = "wasm32"))]
9use boa_engine::module::SimpleModuleLoader;
10use boa_engine::object::builtins::{JsFunction, JsPromise};
11use boa_engine::property::Attribute;
12use boa_engine::{
13 js_string, Context, JsArgs, JsNativeError, JsResult, JsValue, Module, NativeFunction, Source,
14};
15
16use crate::api_registrar::ScriptApiRegistrar;
17use crate::command::{CommandResult, ScriptCommand};
18
19#[derive(Debug, thiserror::Error)]
20pub enum ScriptEngineError {
21 #[error("JS error: {0}")]
22 JsError(String),
23 #[error("Script not found for map: {0}")]
24 ScriptNotFound(String),
25 #[error("Function not found: {0}")]
26 FunctionNotFound(String),
27 #[error("Engine not initialized")]
28 NotInitialized,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum EngineState {
33 Idle,
34 Running,
35 WaitingForCommand,
36 Finished,
37}
38
39struct PendingResolve {
40 resolve_fn: JsFunction,
41}
42
43pub struct SharedBridge {
47 pending_command: Option<ScriptCommand>,
48 pending_resolve: Option<PendingResolve>,
49 flags: std::collections::HashMap<String, bool>,
50 numbers: std::collections::HashMap<String, f64>,
55 texts: std::collections::HashMap<String, String>,
56 sets: std::collections::HashMap<String, std::collections::HashSet<String>>,
57 player_x: u8,
58 player_y: u8,
59 pub lang: String,
60 rng_state: u64,
66}
67
68const DEFAULT_RNG_SEED: u64 = 0x9E37_79B9_7F4A_7C15;
71
72impl SharedBridge {
73 fn new() -> Self {
74 Self {
75 pending_command: None,
76 pending_resolve: None,
77 flags: std::collections::HashMap::new(),
78 numbers: std::collections::HashMap::new(),
79 texts: std::collections::HashMap::new(),
80 sets: std::collections::HashMap::new(),
81 player_x: 0,
82 player_y: 0,
83 lang: "en".to_string(),
84 rng_state: DEFAULT_RNG_SEED,
85 }
86 }
87
88 fn next_rand(&mut self) -> u64 {
90 let mut x = self.rng_state;
91 if x == 0 {
92 x = DEFAULT_RNG_SEED;
93 }
94 x ^= x << 13;
95 x ^= x >> 7;
96 x ^= x << 17;
97 self.rng_state = x;
98 x
99 }
100}
101
102pub struct BridgeView<'a> {
108 inner: &'a SharedBridge,
109}
110
111impl<'a> BridgeView<'a> {
112 pub fn number(&self, k: &str) -> f64 {
114 self.inner.numbers.get(k).copied().unwrap_or(0.0)
115 }
116 pub fn text(&self, k: &str) -> String {
118 self.inner.texts.get(k).cloned().unwrap_or_default()
119 }
120 pub fn set_contains(&self, k: &str, v: &str) -> bool {
122 self.inner.sets.get(k).is_some_and(|s| s.contains(v))
123 }
124 pub fn flag(&self, k: &str) -> bool {
126 self.inner.flags.get(k).copied().unwrap_or(false)
127 }
128}
129
130pub struct ScriptEngine {
131 context: Context,
132 bridge: Rc<RefCell<SharedBridge>>,
133 state: EngineState,
134 current_module: Option<Module>,
136}
137
138impl ScriptEngine {
139 pub fn new() -> Self {
140 #[cfg(target_arch = "wasm32")]
141 let mut context = Context::builder()
142 .module_loader(Rc::new(IdleModuleLoader))
143 .build()
144 .expect("failed to build JS context");
145
146 #[cfg(not(target_arch = "wasm32"))]
147 let mut context = Context::builder()
148 .module_loader(Rc::new(SimpleModuleLoader::new(".").expect(
149 "failed to create module loader (current directory must exist)",
150 )))
151 .build()
152 .expect("failed to build JS context");
153 let bridge = Rc::new(RefCell::new(SharedBridge::new()));
154
155 register_core_game_api(&mut context, bridge.clone());
156
157 Self {
158 context,
159 bridge,
160 state: EngineState::Idle,
161 current_module: None,
162 }
163 }
164
165 pub fn state(&self) -> &EngineState {
166 &self.state
167 }
168
169 pub fn is_idle(&self) -> bool {
170 self.state == EngineState::Idle
171 }
172
173 pub fn is_waiting(&self) -> bool {
174 self.state == EngineState::WaitingForCommand
175 }
176
177 pub fn set_flag(&mut self, flag: &str, value: bool) {
178 self.bridge
179 .borrow_mut()
180 .flags
181 .insert(flag.to_string(), value);
182 }
183
184 pub fn get_flag(&self, flag: &str) -> bool {
185 self.bridge
186 .borrow()
187 .flags
188 .get(flag)
189 .copied()
190 .unwrap_or(false)
191 }
192
193 pub fn get_all_flags(&self) -> std::collections::HashMap<String, bool> {
196 self.bridge.borrow().flags.clone()
197 }
198
199 pub fn seed_flags(&mut self, flags: &std::collections::HashMap<String, bool>) {
202 let mut b = self.bridge.borrow_mut();
203 for (k, v) in flags {
204 b.flags.insert(k.clone(), *v);
205 }
206 }
207
208 pub fn seed_rng(&mut self, seed: u64) {
211 self.bridge.borrow_mut().rng_state = if seed == 0 { DEFAULT_RNG_SEED } else { seed };
212 }
213
214 pub fn mix_rng(&mut self, entropy: u64) {
219 let mut b = self.bridge.borrow_mut();
220 b.rng_state ^= entropy.wrapping_mul(0x2545_F491_4F6C_DD1D);
221 if b.rng_state == 0 {
222 b.rng_state = DEFAULT_RNG_SEED;
223 }
224 }
225
226 pub fn seed_number(&mut self, k: &str, v: f64) {
228 self.bridge.borrow_mut().numbers.insert(k.into(), v);
229 }
230
231 pub fn seed_text(&mut self, k: &str, v: &str) {
233 self.bridge.borrow_mut().texts.insert(k.into(), v.into());
234 }
235
236 pub fn seed_set(&mut self, k: &str, vals: &[String]) {
239 self.bridge
240 .borrow_mut()
241 .sets
242 .insert(k.into(), vals.iter().cloned().collect());
243 }
244
245 pub fn set_player_position(&mut self, x: u8, y: u8) {
246 self.bridge.borrow_mut().player_x = x;
247 self.bridge.borrow_mut().player_y = y;
248 }
249
250 pub fn set_lang(&mut self, lang: &str) {
251 self.bridge.borrow_mut().lang = lang.to_string();
252 }
253
254 pub fn load_script(&mut self, source: &str) -> Result<(), ScriptEngineError> {
255 log::info!(target: "dotzuki::overworld", "[ScriptEngine] load_script: {} bytes", source.len());
256 let src = Source::from_reader(source.as_bytes(), Some(Path::new("script.mjs")));
257 let module = Module::parse(src, None, &mut self.context).map_err(|e| {
258 log::warn!(target: "dotzuki::overworld", "[ScriptEngine] Module parse failed: {}", e);
259 ScriptEngineError::JsError(e.to_string())
260 })?;
261
262 self.context
263 .module_loader()
264 .register_module(js_string!("script.mjs"), module.clone());
265
266 let promise = module.load_link_evaluate(&mut self.context);
267 self.context.run_jobs();
268
269 match promise.state() {
270 PromiseState::Fulfilled(_) => {
271 log::info!(target: "dotzuki::overworld", "[ScriptEngine] Module evaluated OK");
272 }
273 PromiseState::Rejected(err) => {
274 log::warn!(target: "dotzuki::overworld", "[ScriptEngine] Module evaluation rejected: {:?}", err);
275 return Err(ScriptEngineError::JsError(format!(
276 "Module evaluation failed: {:?}",
277 err
278 )));
279 }
280 PromiseState::Pending => {
281 log::warn!(target: "dotzuki::overworld", "[ScriptEngine] Module evaluation stuck pending");
282 return Err(ScriptEngineError::JsError(
283 "Module evaluation stuck in pending state".to_string(),
284 ));
285 }
286 }
287
288 self.current_module = Some(module);
289
290 if let Some(ref m) = self.current_module {
291 for name in &[
292 "enterMap",
293 "talkNurse",
294 "talkLinkReceptionist",
295 "talkGentleman",
296 ] {
297 let has = m
298 .get_value(js_string!(*name), &mut self.context)
299 .map(|v| v.is_callable())
300 .unwrap_or(false);
301 log::info!(target: "dotzuki::overworld", "[ScriptEngine] Export check: {} = {}", name, has);
302 }
303 }
304
305 Ok(())
306 }
307
308 pub fn load_shared_module(
309 &mut self,
310 name: &str,
311 source: &str,
312 ) -> Result<(), ScriptEngineError> {
313 log::info!(target: "dotzuki::overworld", "[ScriptEngine] load_shared_module '{}': {} bytes", name, source.len());
314 let src = Source::from_reader(source.as_bytes(), Some(Path::new(name)));
315 let module = Module::parse(src, None, &mut self.context)
316 .map_err(|e| {
317 log::warn!(target: "dotzuki::overworld", "[ScriptEngine] Shared module parse failed: {}", e);
318 ScriptEngineError::JsError(e.to_string())
319 })?;
320
321 self.context
322 .module_loader()
323 .register_module(js_string!(name), module.clone());
324
325 let promise = module.load_link_evaluate(&mut self.context);
326 self.context.run_jobs();
327
328 match promise.state() {
329 PromiseState::Fulfilled(_) => {
330 log::info!(target: "dotzuki::overworld", "[ScriptEngine] Shared module '{}' evaluated OK", name);
331 if let Ok(val) = module.get_value(js_string!("talkNurse"), &mut self.context) {
332 log::info!(target: "dotzuki::overworld", "[ScriptEngine] Shared module talkNurse callable: {}", val.is_callable());
333 }
334 }
335 PromiseState::Rejected(err) => {
336 log::warn!(target: "dotzuki::overworld", "[ScriptEngine] Shared module '{}' rejected: {:?}", name, err);
337 return Err(ScriptEngineError::JsError(format!(
338 "Shared module evaluation failed: {:?}",
339 err
340 )));
341 }
342 PromiseState::Pending => {
343 log::warn!(target: "dotzuki::overworld", "[ScriptEngine] Shared module '{}' stuck pending", name);
344 return Err(ScriptEngineError::JsError(
345 "Shared module evaluation stuck in pending state".to_string(),
346 ));
347 }
348 }
349 Ok(())
350 }
351
352 pub fn call_function(
356 &mut self,
357 fn_name: &str,
358 args: &[JsValue],
359 ) -> Result<Option<ScriptCommand>, ScriptEngineError> {
360 let resolved = self
362 .resolved_fn_name(fn_name)
363 .unwrap_or_else(|| fn_name.to_string());
364 let fn_name = resolved.as_str();
365 log::info!(target: "dotzuki::overworld", "[ScriptEngine] call_function: {}", fn_name);
366 let module = self
367 .current_module
368 .as_ref()
369 .ok_or(ScriptEngineError::NotInitialized)?;
370
371 let func = module
372 .get_value(js_string!(fn_name), &mut self.context)
373 .map_err(|e| {
374 log::warn!(target: "dotzuki::overworld", "[ScriptEngine] get_value error for {}: {}", fn_name, e);
375 ScriptEngineError::JsError(e.to_string())
376 })?;
377
378 if func.is_undefined() || func.is_null() {
379 log::warn!(target: "dotzuki::overworld", "[ScriptEngine] Function '{}' is undefined or null", fn_name);
380 return Err(ScriptEngineError::FunctionNotFound(fn_name.to_string()));
381 }
382
383 let func_obj = func
384 .as_callable()
385 .ok_or_else(|| {
386 log::warn!(target: "dotzuki::overworld", "[ScriptEngine] Function '{}' is not callable", fn_name);
387 ScriptEngineError::FunctionNotFound(fn_name.to_string())
388 })?;
389
390 log::info!(target: "dotzuki::overworld", "[ScriptEngine] Calling function '{}'...", fn_name);
391 let result = func_obj.call(&JsValue::undefined(), args, &mut self.context);
392
393 match result {
394 Ok(_) => {
395 log::info!(target: "dotzuki::overworld", "[ScriptEngine] Function '{}' call succeeded", fn_name);
396 }
397 Err(e) => {
398 log::warn!(target: "dotzuki::overworld", "[ScriptEngine] Function '{}' call failed: {}", fn_name, e);
399 return Err(ScriptEngineError::JsError(e.to_string()));
400 }
401 }
402
403 self.context.run_jobs();
404
405 self.state = EngineState::Running;
406 let cmd = self.check_pending_command()?;
407 log::info!(target: "dotzuki::overworld", "[ScriptEngine] After call_function '{}': pending_command = {:?}", fn_name, cmd.is_some());
408 Ok(cmd)
409 }
410
411 pub fn tick(&mut self) -> Option<ScriptCommand> {
414 match self.state {
415 EngineState::WaitingForCommand => self.bridge.borrow().pending_command.clone(),
416 EngineState::Idle | EngineState::Finished => None,
417 EngineState::Running => match self.check_pending_command() {
418 Ok(cmd) => cmd,
419 Err(_) => {
420 self.state = EngineState::Finished;
421 None
422 }
423 },
424 }
425 }
426
427 pub fn signal_done(
430 &mut self,
431 result: CommandResult,
432 ) -> Result<Option<ScriptCommand>, ScriptEngineError> {
433 if self.state != EngineState::WaitingForCommand {
434 return Ok(None);
435 }
436
437 let resolve = self.bridge.borrow_mut().pending_resolve.take();
438 self.bridge.borrow_mut().pending_command = None;
439
440 if let Some(pending) = resolve {
441 let js_result = command_result_to_js(&result, &mut self.context);
442 pending
443 .resolve_fn
444 .call(&JsValue::undefined(), &[js_result], &mut self.context)
445 .map_err(|e| ScriptEngineError::JsError(e.to_string()))?;
446
447 self.context.run_jobs();
448 }
449
450 self.state = EngineState::Running;
451 self.check_pending_command()
452 }
453
454 fn check_pending_command(&mut self) -> Result<Option<ScriptCommand>, ScriptEngineError> {
455 let cmd = self.bridge.borrow().pending_command.clone();
456 if cmd.is_some() {
457 self.state = EngineState::WaitingForCommand;
458 } else if self.state == EngineState::Running {
459 self.state = EngineState::Idle;
460 }
461 Ok(cmd)
462 }
463
464 pub fn register_async_fn(
472 &mut self,
473 name: &str,
474 builder: impl Fn(&[JsValue], &mut Context) -> JsResult<ScriptCommand> + 'static,
475 ) {
476 let bridge = self.bridge.clone();
477 let func = unsafe {
478 NativeFunction::from_closure(move |_this, args, ctx| {
479 let (promise, resolvers) = JsPromise::new_pending(ctx);
480 let cmd = builder(args, ctx)?;
481 let mut b = bridge.borrow_mut();
482 b.pending_command = Some(cmd);
483 b.pending_resolve = Some(PendingResolve {
484 resolve_fn: resolvers.resolve,
485 });
486 Ok(promise.into())
487 })
488 };
489 let game_obj = self
490 .context
491 .global_object()
492 .get(js_string!("game"), &mut self.context)
493 .expect("game global not found")
494 .to_object(&mut self.context)
495 .expect("game global is not an object");
496 game_obj
497 .set(
498 js_string!(name),
499 func.to_js_function(self.context.realm()),
500 true,
501 &mut self.context,
502 )
503 .unwrap_or_else(|_| panic!("failed to register game.{}", name));
504 }
505
506 pub fn register_sync_fn<F>(&mut self, name: &str, f: F)
513 where
514 F: Fn(&[JsValue], &mut Context, &BridgeView) -> JsResult<JsValue> + 'static,
515 {
516 let bridge = self.bridge.clone();
517 let func = unsafe {
520 NativeFunction::from_closure(move |_this, args, ctx| {
521 let b = bridge.borrow();
522 let view = BridgeView { inner: &b };
523 f(args, ctx, &view)
524 })
525 };
526 let game_obj = self
527 .context
528 .global_object()
529 .get(js_string!("game"), &mut self.context)
530 .expect("game global not found")
531 .to_object(&mut self.context)
532 .expect("game global is not an object");
533 game_obj
534 .set(
535 js_string!(name),
536 func.to_js_function(self.context.realm()),
537 true,
538 &mut self.context,
539 )
540 .unwrap_or_else(|_| panic!("failed to register game.{}", name));
541 }
542
543 pub fn with_api(registrar: &dyn ScriptApiRegistrar) -> Self {
549 let mut engine = Self::new();
550 registrar.register_api(&mut engine);
551 engine
552 }
553}
554
555impl Default for ScriptEngine {
556 fn default() -> Self {
557 Self::new()
558 }
559}
560
561impl ScriptEngine {
565 pub fn call_function_no_args(
567 &mut self,
568 fn_name: &str,
569 ) -> Result<Option<ScriptCommand>, ScriptEngineError> {
570 self.call_function(fn_name, &[])
571 }
572
573 pub fn call_function_with_u8(
575 &mut self,
576 fn_name: &str,
577 arg: u8,
578 ) -> Result<Option<ScriptCommand>, ScriptEngineError> {
579 self.call_function(fn_name, &[JsValue::from(arg as i32)])
580 }
581
582 pub fn call_function_with_xy(
584 &mut self,
585 fn_name: &str,
586 x: u16,
587 y: u16,
588 ) -> Result<Option<ScriptCommand>, ScriptEngineError> {
589 self.call_function(fn_name, &[JsValue::from(x as i32), JsValue::from(y as i32)])
590 }
591
592 pub fn call_function_with_str(
594 &mut self,
595 fn_name: &str,
596 arg: &str,
597 ) -> Result<Option<ScriptCommand>, ScriptEngineError> {
598 self.call_function(fn_name, &[JsValue::from(js_string!(arg))])
599 }
600
601 fn resolved_fn_name(&mut self, fn_name: &str) -> Option<String> {
608 let module = self.current_module.clone()?;
609 if matches!(module.get_value(js_string!(fn_name), &mut self.context), Ok(v) if v.is_callable())
610 {
611 return Some(fn_name.to_string());
612 }
613 let prefixed = format!("storyline_{fn_name}");
614 if matches!(module.get_value(js_string!(prefixed.as_str()), &mut self.context), Ok(v) if v.is_callable())
615 {
616 return Some(prefixed);
617 }
618 None
619 }
620
621 pub fn has_function(&mut self, fn_name: &str) -> bool {
624 self.resolved_fn_name(fn_name).is_some()
625 }
626}
627
628fn command_result_to_js(result: &CommandResult, _context: &mut Context) -> JsValue {
629 match result {
630 CommandResult::Void => JsValue::undefined(),
631 CommandResult::Bool(b) => JsValue::from(*b),
632 CommandResult::Number(n) => JsValue::from(*n),
633 CommandResult::Text(s) => JsValue::from(js_string!(s.as_str())),
634 }
635}
636
637fn register_core_game_api(context: &mut Context, bridge: Rc<RefCell<SharedBridge>>) {
638 let mut game_obj = boa_engine::object::ObjectInitializer::new(context);
639 let game_obj = game_obj.build();
640
641 let lang_bridge = bridge.clone();
642 let lang_fn = unsafe {
643 NativeFunction::from_closure(
644 move |_this: &JsValue, _args: &[JsValue], _ctx: &mut Context| -> JsResult<JsValue> {
645 Ok(JsValue::from(js_string!(lang_bridge
646 .borrow()
647 .lang
648 .as_str())))
649 },
650 )
651 };
652 game_obj
653 .set(
654 js_string!("lang"),
655 lang_fn.to_js_function(context.realm()),
656 true,
657 context,
658 )
659 .expect("failed to register game.lang");
660
661 let t_bridge = bridge.clone();
662 let t_fn = unsafe {
663 NativeFunction::from_closure(
664 move |_this: &JsValue, args: &[JsValue], ctx: &mut Context| -> JsResult<JsValue> {
665 let en = args
666 .get_or_undefined(0)
667 .to_string(ctx)
668 .map_or(String::new(), |s| s.to_std_string_lossy());
669 let zh = args
670 .get_or_undefined(1)
671 .to_string(ctx)
672 .map_or(String::new(), |s| s.to_std_string_lossy());
673 let result = if t_bridge.borrow().lang == "zh" {
674 zh
675 } else {
676 en
677 };
678 Ok(JsValue::from(js_string!(result)))
679 },
680 )
681 };
682 game_obj
683 .set(
684 js_string!("t"),
685 t_fn.to_js_function(context.realm()),
686 true,
687 context,
688 )
689 .expect("failed to register game.t");
690
691 let rand_text_bridge = bridge.clone();
698 let rand_text_fn = unsafe {
701 NativeFunction::from_closure(
702 move |_this: &JsValue, args: &[JsValue], ctx: &mut Context| -> JsResult<JsValue> {
703 let mut options: Vec<String> = Vec::new();
705 if args.len() == 1 && args[0].is_object() {
706 let obj = args[0].to_object(ctx)?;
707 let len = obj.get(js_string!("length"), ctx)?.to_u32(ctx)?;
708 for i in 0..len {
709 options.push(obj.get(i, ctx)?.to_string(ctx)?.to_std_string_lossy());
710 }
711 } else {
712 for a in args {
713 options.push(a.to_string(ctx)?.to_std_string_lossy());
714 }
715 }
716
717 let (promise, resolvers) = JsPromise::new_pending(ctx);
718
719 let mut b = rand_text_bridge.borrow_mut();
720 let text = if options.is_empty() {
721 String::new()
722 } else {
723 let idx = (b.next_rand() % options.len() as u64) as usize;
724 options.swap_remove(idx)
725 };
726 b.pending_command = Some(ScriptCommand::ShowText { text });
727 b.pending_resolve = Some(PendingResolve {
728 resolve_fn: resolvers.resolve,
729 });
730
731 Ok(promise.into())
732 },
733 )
734 };
735 game_obj
736 .set(
737 js_string!("showRandomText"),
738 rand_text_fn.to_js_function(context.realm()),
739 true,
740 context,
741 )
742 .expect("failed to register game.showRandomText");
743
744 macro_rules! register_async_command {
745 ($name:expr, $bridge:expr, $context:expr, $game_obj:expr, $cmd_builder:expr) => {{
746 let bridge = $bridge.clone();
747 let func = unsafe {
750 NativeFunction::from_closure(move |_this, args, ctx| {
751 let (promise, resolvers) = JsPromise::new_pending(ctx);
752
753 let cmd = ($cmd_builder)(args, ctx)?;
754
755 let mut b = bridge.borrow_mut();
756 b.pending_command = Some(cmd);
757 b.pending_resolve = Some(PendingResolve {
758 resolve_fn: resolvers.resolve,
759 });
760
761 Ok(promise.into())
762 })
763 };
764 $game_obj
765 .set(
766 js_string!($name),
767 func.to_js_function($context.realm()),
768 true,
769 $context,
770 )
771 .expect(concat!("failed to register game.", $name));
772 }};
773 }
774
775 register_async_command!(
777 "showText",
778 bridge,
779 context,
780 game_obj,
781 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
782 let text = args
783 .get_or_undefined(0)
784 .to_string(ctx)?
785 .to_std_string_lossy();
786 Ok(ScriptCommand::ShowText { text })
787 }
788 );
789
790 register_async_command!(
792 "showChoice",
793 bridge,
794 context,
795 game_obj,
796 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
797 let arr = args.get_or_undefined(0).to_object(ctx)?;
798 let len = arr.get(js_string!("length"), ctx)?.to_u32(ctx)?;
799 let mut options = Vec::new();
800 for i in 0..len {
801 let val = arr.get(i, ctx)?;
802 options.push(val.to_string(ctx)?.to_std_string_lossy());
803 }
804 Ok(ScriptCommand::ShowChoice { options })
805 }
806 );
807
808 register_async_command!(
810 "moveNpc",
811 bridge,
812 context,
813 game_obj,
814 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
815 let npc_id = args
816 .get_or_undefined(0)
817 .to_string(ctx)?
818 .to_std_string_lossy();
819 let arr = args.get_or_undefined(1).to_object(ctx)?;
820 let len = arr.get(js_string!("length"), ctx)?.to_u32(ctx)?;
821 let mut path = Vec::new();
822 for i in 0..len {
823 let point = arr.get(i, ctx)?.to_object(ctx)?;
824 let x = point.get(0, ctx)?.to_u32(ctx)? as u8;
825 let y = point.get(1, ctx)?.to_u32(ctx)? as u8;
826 path.push((x, y));
827 }
828 Ok(ScriptCommand::MoveNpc { npc_id, path })
829 }
830 );
831
832 register_async_command!(
835 "startNpcMove",
836 bridge,
837 context,
838 game_obj,
839 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
840 let npc_id = args
841 .get_or_undefined(0)
842 .to_string(ctx)?
843 .to_std_string_lossy();
844 let arr = args.get_or_undefined(1).to_object(ctx)?;
845 let len = arr.get(js_string!("length"), ctx)?.to_u32(ctx)?;
846 let mut path = Vec::new();
847 for i in 0..len {
848 let point = arr.get(i, ctx)?.to_object(ctx)?;
849 let x = point.get(0, ctx)?.to_u32(ctx)? as u8;
850 let y = point.get(1, ctx)?.to_u32(ctx)? as u8;
851 path.push((x, y));
852 }
853 Ok(ScriptCommand::StartNpcMove { npc_id, path })
854 }
855 );
856
857 register_async_command!(
860 "awaitNpcMove",
861 bridge,
862 context,
863 game_obj,
864 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
865 let npc_id = args
866 .get_or_undefined(0)
867 .to_string(ctx)?
868 .to_std_string_lossy();
869 Ok(ScriptCommand::AwaitNpcMove { npc_id })
870 }
871 );
872
873 register_async_command!(
876 "movePlayer",
877 bridge,
878 context,
879 game_obj,
880 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
881 let arr = args.get_or_undefined(0).to_object(ctx)?;
882 let len = arr.get(js_string!("length"), ctx)?.to_u32(ctx)?;
883 let mut path = Vec::new();
884 for i in 0..len {
885 let point = arr.get(i, ctx)?.to_object(ctx)?;
886 let x = point.get(0, ctx)?.to_u32(ctx)? as u8;
887 let y = point.get(1, ctx)?.to_u32(ctx)? as u8;
888 path.push((x, y));
889 }
890 Ok(ScriptCommand::MovePlayer { path })
891 }
892 );
893
894 register_async_command!(
900 "movePlayerRelative",
901 bridge,
902 context,
903 game_obj,
904 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
905 let arr = args.get_or_undefined(0).to_object(ctx)?;
906 let len = arr.get(js_string!("length"), ctx)?.to_u32(ctx)?;
907 let mut steps = Vec::new();
908 for i in 0..len {
909 let entry = arr.get(i, ctx)?;
910 if entry.is_string() {
911 let dir = entry.to_string(ctx)?.to_std_string_lossy();
912 let delta = match dir.to_ascii_lowercase().as_str() {
913 "up" | "north" => (0i16, -1i16),
914 "down" | "south" => (0, 1),
915 "left" | "west" => (-1, 0),
916 "right" | "east" => (1, 0),
917 other => {
918 return Err(JsNativeError::typ()
919 .with_message(format!(
920 "movePlayerRelative: unknown direction '{other}'"
921 ))
922 .into())
923 }
924 };
925 steps.push(delta);
926 } else {
927 let point = entry.to_object(ctx)?;
928 let dx = point.get(0, ctx)?.to_i32(ctx)? as i16;
929 let dy = point.get(1, ctx)?.to_i32(ctx)? as i16;
930 steps.push((dx, dy));
931 }
932 }
933 Ok(ScriptCommand::MovePlayerRelative { steps })
934 }
935 );
936
937 register_async_command!(
940 "moveNpcTo",
941 bridge,
942 context,
943 game_obj,
944 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
945 let npc_id = args
946 .get_or_undefined(0)
947 .to_string(ctx)?
948 .to_std_string_lossy();
949 let x = args.get_or_undefined(1).to_u32(ctx)? as u8;
950 let y = args.get_or_undefined(2).to_u32(ctx)? as u8;
951 Ok(ScriptCommand::MoveNpcTo { npc_id, x, y })
952 }
953 );
954
955 register_async_command!(
958 "startNpcMoveTo",
959 bridge,
960 context,
961 game_obj,
962 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
963 let npc_id = args
964 .get_or_undefined(0)
965 .to_string(ctx)?
966 .to_std_string_lossy();
967 let x = args.get_or_undefined(1).to_u32(ctx)? as u8;
968 let y = args.get_or_undefined(2).to_u32(ctx)? as u8;
969 Ok(ScriptCommand::StartNpcMoveTo { npc_id, x, y })
970 }
971 );
972
973 register_async_command!(
976 "movePlayerTo",
977 bridge,
978 context,
979 game_obj,
980 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
981 let x = args.get_or_undefined(0).to_u32(ctx)? as u8;
982 let y = args.get_or_undefined(1).to_u32(ctx)? as u8;
983 Ok(ScriptCommand::MovePlayerTo { x, y })
984 }
985 );
986
987 register_async_command!(
989 "faceNpc",
990 bridge,
991 context,
992 game_obj,
993 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
994 let npc_id = args
995 .get_or_undefined(0)
996 .to_string(ctx)?
997 .to_std_string_lossy();
998 let direction = args
999 .get_or_undefined(1)
1000 .to_string(ctx)?
1001 .to_std_string_lossy();
1002 Ok(ScriptCommand::FaceNpc { npc_id, direction })
1003 }
1004 );
1005
1006 register_async_command!(
1008 "facePlayer",
1009 bridge,
1010 context,
1011 game_obj,
1012 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1013 let direction = args
1014 .get_or_undefined(0)
1015 .to_string(ctx)?
1016 .to_std_string_lossy();
1017 Ok(ScriptCommand::FacePlayer { direction })
1018 }
1019 );
1020
1021 register_async_command!(
1023 "setNpcFrame",
1024 bridge,
1025 context,
1026 game_obj,
1027 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1028 let npc_id = args
1029 .get_or_undefined(0)
1030 .to_string(ctx)?
1031 .to_std_string_lossy();
1032 let frame = args.get_or_undefined(1).to_number(ctx)? as u8;
1033 Ok(ScriptCommand::SetNpcFrame { npc_id, frame })
1034 }
1035 );
1036
1037 register_async_command!(
1039 "playMusic",
1040 bridge,
1041 context,
1042 game_obj,
1043 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1044 let music_id = args
1045 .get_or_undefined(0)
1046 .to_string(ctx)?
1047 .to_std_string_lossy();
1048 Ok(ScriptCommand::PlayMusic { music_id })
1049 }
1050 );
1051
1052 register_async_command!(
1054 "playSound",
1055 bridge,
1056 context,
1057 game_obj,
1058 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1059 let sound_id = args
1060 .get_or_undefined(0)
1061 .to_string(ctx)?
1062 .to_std_string_lossy();
1063 Ok(ScriptCommand::PlaySound { sound_id })
1064 }
1065 );
1066
1067 register_async_command!(
1069 "stopMusic",
1070 bridge,
1071 context,
1072 game_obj,
1073 |_args: &[JsValue], _ctx: &mut Context| -> JsResult<ScriptCommand> {
1074 Ok(ScriptCommand::StopMusic)
1075 }
1076 );
1077
1078 register_async_command!(
1080 "fadeOutMusic",
1081 bridge,
1082 context,
1083 game_obj,
1084 |_args: &[JsValue], _ctx: &mut Context| -> JsResult<ScriptCommand> {
1085 Ok(ScriptCommand::FadeOutMusic)
1086 }
1087 );
1088
1089 register_async_command!("delay", bridge, context, game_obj, |args: &[JsValue],
1091 ctx: &mut Context|
1092 -> JsResult<
1093 ScriptCommand,
1094 > {
1095 let frames = args.get_or_undefined(0).to_u32(ctx)? as u16;
1096 Ok(ScriptCommand::Delay { frames })
1097 });
1098
1099 register_async_command!("warpTo", bridge, context, game_obj, |args: &[JsValue],
1101 ctx: &mut Context|
1102 -> JsResult<
1103 ScriptCommand,
1104 > {
1105 let map = args
1106 .get_or_undefined(0)
1107 .to_string(ctx)?
1108 .to_std_string_lossy();
1109 let x = args.get_or_undefined(1).to_u32(ctx)? as u8;
1110 let y = args.get_or_undefined(2).to_u32(ctx)? as u8;
1111 Ok(ScriptCommand::WarpTo { map, x, y })
1112 });
1113
1114 register_async_command!("heal", bridge, context, game_obj, |_args: &[JsValue],
1116 _ctx: &mut Context|
1117 -> JsResult<
1118 ScriptCommand,
1119 > {
1120 Ok(ScriptCommand::Heal)
1121 });
1122
1123 register_async_command!(
1125 "fadeScreen",
1126 bridge,
1127 context,
1128 game_obj,
1129 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1130 let fade_type = args
1131 .get_or_undefined(0)
1132 .to_string(ctx)?
1133 .to_std_string_lossy();
1134 Ok(ScriptCommand::FadeScreen { fade_type })
1135 }
1136 );
1137
1138 register_async_command!(
1140 "showObject",
1141 bridge,
1142 context,
1143 game_obj,
1144 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1145 let arg = args.get_or_undefined(0);
1146 if arg.is_string() {
1147 let toggle_id = arg.to_string(ctx)?.to_std_string_lossy();
1148 Ok(ScriptCommand::ShowObjectByName { toggle_id })
1149 } else {
1150 let object_index = arg.to_u32(ctx)? as u8;
1151 Ok(ScriptCommand::ShowObject { object_index })
1152 }
1153 }
1154 );
1155
1156 register_async_command!(
1158 "hideObject",
1159 bridge,
1160 context,
1161 game_obj,
1162 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1163 let arg = args.get_or_undefined(0);
1164 if arg.is_string() {
1165 let toggle_id = arg.to_string(ctx)?.to_std_string_lossy();
1166 Ok(ScriptCommand::HideObjectByName { toggle_id })
1167 } else {
1168 let object_index = arg.to_u32(ctx)? as u8;
1169 Ok(ScriptCommand::HideObject { object_index })
1170 }
1171 }
1172 );
1173
1174 register_async_command!(
1179 "showObjectByName",
1180 bridge,
1181 context,
1182 game_obj,
1183 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1184 let toggle_id = args
1185 .get_or_undefined(0)
1186 .to_string(ctx)?
1187 .to_std_string_lossy();
1188 Ok(ScriptCommand::ShowObjectByName { toggle_id })
1189 }
1190 );
1191
1192 register_async_command!(
1194 "hideObjectByName",
1195 bridge,
1196 context,
1197 game_obj,
1198 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1199 let toggle_id = args
1200 .get_or_undefined(0)
1201 .to_string(ctx)?
1202 .to_std_string_lossy();
1203 Ok(ScriptCommand::HideObjectByName { toggle_id })
1204 }
1205 );
1206
1207 register_async_command!(
1209 "setJoyIgnore",
1210 bridge,
1211 context,
1212 game_obj,
1213 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1214 let mask = args.get_or_undefined(0).to_u32(ctx)? as u8;
1215 Ok(ScriptCommand::SetJoyIgnore { mask })
1216 }
1217 );
1218
1219 register_async_command!(
1221 "clearJoyIgnore",
1222 bridge,
1223 context,
1224 game_obj,
1225 |_args: &[JsValue], _ctx: &mut Context| -> JsResult<ScriptCommand> {
1226 Ok(ScriptCommand::ClearJoyIgnore)
1227 }
1228 );
1229
1230 register_async_command!(
1232 "followNpc",
1233 bridge,
1234 context,
1235 game_obj,
1236 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1237 let npc_id = args
1238 .get_or_undefined(0)
1239 .to_string(ctx)?
1240 .to_std_string_lossy();
1241 let target_x = args.get_or_undefined(1).to_u32(ctx)? as u8;
1242 let target_y = args.get_or_undefined(2).to_u32(ctx)? as u8;
1243 Ok(ScriptCommand::FollowNpc {
1244 npc_id,
1245 target_x,
1246 target_y,
1247 })
1248 }
1249 );
1250
1251 register_async_command!(
1253 "openShop",
1254 bridge,
1255 context,
1256 game_obj,
1257 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1258 let arr = args.get_or_undefined(0).to_object(ctx)?;
1259 let len = arr.get(js_string!("length"), ctx)?.to_u32(ctx)?;
1260 let mut items = Vec::new();
1261 for i in 0..len {
1262 let val = arr.get(i, ctx)?;
1263 items.push(val.to_string(ctx)?.to_std_string_lossy());
1264 }
1265 Ok(ScriptCommand::OpenShop { items })
1266 }
1267 );
1268
1269 register_async_command!(
1271 "showEmotionBubble",
1272 bridge,
1273 context,
1274 game_obj,
1275 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1276 let npc_id = args
1277 .get_or_undefined(0)
1278 .to_string(ctx)?
1279 .to_std_string_lossy();
1280 let emotion = args
1281 .get_or_undefined(1)
1282 .to_string(ctx)?
1283 .to_std_string_lossy();
1284 Ok(ScriptCommand::ShowEmotionBubble { npc_id, emotion })
1285 }
1286 );
1287
1288 register_async_command!(
1290 "setNpcPosition",
1291 bridge,
1292 context,
1293 game_obj,
1294 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1295 let npc_id = args
1296 .get_or_undefined(0)
1297 .to_string(ctx)?
1298 .to_std_string_lossy();
1299 let x = args.get_or_undefined(1).to_u32(ctx)? as u8;
1300 let y = args.get_or_undefined(2).to_u32(ctx)? as u8;
1301 Ok(ScriptCommand::SetNpcPosition { npc_id, x, y })
1302 }
1303 );
1304
1305 register_async_command!(
1307 "showScene",
1308 bridge,
1309 context,
1310 game_obj,
1311 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1312 let scene_name = args
1313 .get_or_undefined(0)
1314 .to_string(ctx)?
1315 .to_std_string_lossy();
1316 Ok(ScriptCommand::ShowScene {
1317 scene_name,
1318 layout_json: None,
1319 })
1320 }
1321 );
1322
1323 register_async_command!(
1325 "hideScene",
1326 bridge,
1327 context,
1328 game_obj,
1329 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1330 let scene_name = args
1331 .get_or_undefined(0)
1332 .to_string(ctx)?
1333 .to_std_string_lossy();
1334 Ok(ScriptCommand::HideScene { scene_name })
1335 }
1336 );
1337
1338 register_async_command!(
1340 "updateUI",
1341 bridge,
1342 context,
1343 game_obj,
1344 |args: &[JsValue], ctx: &mut Context| -> JsResult<ScriptCommand> {
1345 let scene_name = args
1346 .get_or_undefined(0)
1347 .to_string(ctx)?
1348 .to_std_string_lossy();
1349 let data_val = args.get_or_undefined(1);
1350 let json_val = data_val.to_json(ctx)?;
1351 let data_json = json_val.to_string();
1352 Ok(ScriptCommand::UpdateUI {
1353 scene_name,
1354 data_json,
1355 })
1356 }
1357 );
1358
1359 {
1361 let bridge = bridge.clone();
1362 let func = unsafe {
1364 NativeFunction::from_closure(move |_this, args, ctx| {
1365 let flag = args
1366 .get_or_undefined(0)
1367 .to_string(ctx)?
1368 .to_std_string_lossy();
1369 let val = bridge.borrow().flags.get(&flag).copied().unwrap_or(false);
1370 Ok(JsValue::from(val))
1371 })
1372 };
1373 game_obj
1374 .set(
1375 js_string!("getFlag"),
1376 func.to_js_function(context.realm()),
1377 true,
1378 context,
1379 )
1380 .expect("failed to register game.getFlag");
1381 }
1382
1383 {
1385 let bridge = bridge.clone();
1386 let func = unsafe {
1388 NativeFunction::from_closure(move |_this, args, ctx| {
1389 let flag = args
1390 .get_or_undefined(0)
1391 .to_string(ctx)?
1392 .to_std_string_lossy();
1393 bridge.borrow_mut().flags.insert(flag, true);
1394 Ok(JsValue::undefined())
1395 })
1396 };
1397 game_obj
1398 .set(
1399 js_string!("setFlag"),
1400 func.to_js_function(context.realm()),
1401 true,
1402 context,
1403 )
1404 .expect("failed to register game.setFlag");
1405 }
1406
1407 {
1409 let bridge = bridge.clone();
1410 let func = unsafe {
1412 NativeFunction::from_closure(move |_this, args, ctx| {
1413 let flag = args
1414 .get_or_undefined(0)
1415 .to_string(ctx)?
1416 .to_std_string_lossy();
1417 bridge.borrow_mut().flags.insert(flag, false);
1418 Ok(JsValue::undefined())
1419 })
1420 };
1421 game_obj
1422 .set(
1423 js_string!("resetFlag"),
1424 func.to_js_function(context.realm()),
1425 true,
1426 context,
1427 )
1428 .expect("failed to register game.resetFlag");
1429 }
1430
1431 {
1433 let bridge = bridge.clone();
1434 let func = unsafe {
1435 NativeFunction::from_closure(move |_this, _args, ctx| {
1436 let b = bridge.borrow();
1437 let pos = boa_engine::object::ObjectInitializer::new(ctx)
1438 .property(
1439 js_string!("x"),
1440 JsValue::from(b.player_x as i32),
1441 Attribute::all(),
1442 )
1443 .property(
1444 js_string!("y"),
1445 JsValue::from(b.player_y as i32),
1446 Attribute::all(),
1447 )
1448 .build();
1449 Ok(pos.into())
1450 })
1451 };
1452 game_obj
1453 .set(
1454 js_string!("getPlayerPosition"),
1455 func.to_js_function(context.realm()),
1456 true,
1457 context,
1458 )
1459 .expect("failed to register game.getPlayerPosition");
1460 }
1461
1462 {
1464 let bridge = bridge.clone();
1465 let func = unsafe {
1466 NativeFunction::from_closure(move |_this, _args, _ctx| {
1467 Ok(JsValue::from(bridge.borrow().player_x as i32))
1468 })
1469 };
1470 game_obj
1471 .set(
1472 js_string!("getPlayerX"),
1473 func.to_js_function(context.realm()),
1474 true,
1475 context,
1476 )
1477 .expect("failed to register game.getPlayerX");
1478 }
1479
1480 {
1482 let bridge = bridge.clone();
1483 let func = unsafe {
1484 NativeFunction::from_closure(move |_this, _args, _ctx| {
1485 Ok(JsValue::from(bridge.borrow().player_y as i32))
1486 })
1487 };
1488 game_obj
1489 .set(
1490 js_string!("getPlayerY"),
1491 func.to_js_function(context.realm()),
1492 true,
1493 context,
1494 )
1495 .expect("failed to register game.getPlayerY");
1496 }
1497
1498 context
1499 .register_global_property(js_string!("game"), game_obj, Attribute::all())
1500 .expect("failed to register global game object");
1501}