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