1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use configs::abilities::ProjectileId;
5use configs::game_config::GameConfig;
6use essences::abilities::AbilityId;
7use essences::bots::Bot;
8use essences::character_state::CharacterState;
9use essences::entity::{
10 ActionWithDeadline, EntityAction, EntityAttributes, EntityId, EssencesCustomEventData,
11};
12use essences::fighting::EntityTeam;
13use essences::items::Item;
14use essences::{abilities::AbilityShard, entity::Coordinates};
15use event_system::event::{Event, EventPluginized, EventRhaiEnum, EventVec};
16use event_system::script::{
17 runner::{ScriptRandom, ScriptTestReport},
18 scope::ScriptScope,
19 types::{
20 AbilityIdsVec, AbilityShardsVec, ConditionalProgress, CurrenciesVec, ESCurrencyUnit,
21 EntityAttributesVec, ItemIdsVec,
22 },
23};
24use event_system::state::State;
25use rhai::{Array, CustomType, TypeBuilder};
26use schemars::JsonSchema;
27use serde::{Deserialize, Serialize};
28use uuid::Uuid;
29
30use crate::event::{OverlordEvent, OverlordEventNewQuests, OverlordEventSpawnEntity};
31use crate::game_config_helpers::GameConfigLookup;
32use crate::state::OverlordState;
33
34#[derive(thiserror::Error, Debug)]
35pub enum ScriptError {
36 #[error("no ScriptResult in the end")]
37 NoScriptResultFound,
38}
39
40#[derive(Debug, Clone, Default, PartialEq, Eq)]
41pub struct StartCastAbilityScriptResult {
42 pub delay_ticks: Option<u64>,
43 pub animation_duration_ticks: Option<u64>,
44 pub target_entity_id: Option<Uuid>,
45
46 pub coordinates: Option<Coordinates>,
47 pub run_duration_ticks: Option<u64>,
48}
49
50impl CustomType for StartCastAbilityScriptResult {
51 fn build(mut builder: TypeBuilder<Self>) {
52 builder.with_set(
53 "delay_ticks",
54 |this: &mut StartCastAbilityScriptResult, v: u64| this.delay_ticks = Some(v),
55 );
56 builder.with_set(
57 "animation_duration_ticks",
58 |this: &mut StartCastAbilityScriptResult, v: u64| {
59 this.animation_duration_ticks = Some(v)
60 },
61 );
62 builder.with_set(
63 "target_entity_id",
64 |this: &mut StartCastAbilityScriptResult, id: Uuid| this.target_entity_id = Some(id),
65 );
66 builder.with_set(
67 "coordinates",
68 |this: &mut StartCastAbilityScriptResult, v: Coordinates| this.coordinates = Some(v),
69 );
70 builder.with_set(
71 "run_duration_ticks",
72 |this: &mut StartCastAbilityScriptResult, v: u64| this.run_duration_ticks = Some(v),
73 );
74 }
75}
76
77#[derive(Clone, Default)]
78pub struct StartCastAbilityScriptResultVec(
79 Arc<std::sync::Mutex<Vec<StartCastAbilityScriptResult>>>,
80);
81
82impl rhai::CustomType for StartCastAbilityScriptResultVec {
83 fn build(mut builder: rhai::TypeBuilder<Self>) {
84 builder
85 .with_name("StartCastAbilityScriptResultVec")
86 .with_fn("push_attack", StartCastAbilityScriptResultVec::push_attack)
87 .with_fn("push_run", StartCastAbilityScriptResultVec::push_run);
88 }
89}
90
91impl StartCastAbilityScriptResultVec {
92 pub fn push_run(&mut self, coordinates: Coordinates, run_duration_ticks: u64) {
93 self.0.lock().unwrap().push(StartCastAbilityScriptResult {
94 coordinates: Some(coordinates),
95 run_duration_ticks: Some(run_duration_ticks),
96 ..Default::default()
97 })
98 }
99
100 pub fn push_attack(
101 &mut self,
102 delay_ticks: u64,
103 animation_duration_ticks: u64,
104 target_entity_id: Uuid,
105 ) {
106 self.0.lock().unwrap().push(StartCastAbilityScriptResult {
107 delay_ticks: Some(delay_ticks),
108 animation_duration_ticks: Some(animation_duration_ticks),
109 target_entity_id: Some(target_entity_id),
110 ..Default::default()
111 })
112 }
113
114 pub fn results(&self) -> Vec<StartCastAbilityScriptResult> {
115 self.0.lock().unwrap().clone()
116 }
117}
118
119#[derive(Clone, Debug, PartialEq)]
120pub enum StartCastAbilityResult {
121 Run {
122 coordinates: Coordinates,
123 run_duration_ticks: u64,
124 },
125 Attack {
126 delay_ticks: u64,
127 animation_duration_ticks: u64,
128 target_entity_id: Uuid,
129 },
130 None,
131}
132
133impl StartCastAbilityResult {
134 pub fn into_entity_action_with_deadline(
135 &self,
136 class_id: Uuid,
137 game_config: &GameConfig,
138 ability_id: AbilityId,
139 current_tick: u64,
140 ) -> anyhow::Result<ActionWithDeadline> {
141 match self {
142 StartCastAbilityResult::Run { .. } => {
143 anyhow::bail!("Got Run for StartCastAbilityResult into_entity_action")
144 }
145 StartCastAbilityResult::Attack {
146 delay_ticks,
147 animation_duration_ticks,
148 target_entity_id,
149 } => {
150 let Some(class) = game_config.class(class_id) else {
151 anyhow::bail!("Failed to get class with id: {}", class_id);
152 };
153
154 if !class.basic_abilities.contains(&ability_id) {
155 Ok(ActionWithDeadline {
156 action: EntityAction::CastAbility {
157 ability_id,
158 target_entity_id: *target_entity_id,
159 },
160 deadline_tick: current_tick + *delay_ticks + *animation_duration_ticks,
161 })
162 } else {
163 Ok(ActionWithDeadline {
164 action: EntityAction::CastBasicAbility {
165 ability_id,
166 target_entity_id: *target_entity_id,
167 },
168 deadline_tick: current_tick + *delay_ticks + *animation_duration_ticks,
169 })
170 }
171 }
172 StartCastAbilityResult::None => {
173 anyhow::bail!("Got NONE for StartCastAbilityResult into_entity_action")
174 }
175 }
176 }
177
178 pub fn into_event(
179 &self,
180 ability_id: AbilityId,
181 by_entity_id: EntityId,
182 ) -> Option<EventPluginized<OverlordEvent, OverlordState>> {
183 match self {
184 StartCastAbilityResult::Run {
185 coordinates,
186 run_duration_ticks,
187 } => Some(EventPluginized::now(OverlordEvent::StartMove {
188 entity_id: by_entity_id,
189 to: coordinates.clone(),
190 duration_ticks: *run_duration_ticks,
191 })),
192 StartCastAbilityResult::Attack {
193 delay_ticks,
194 animation_duration_ticks,
195 ..
196 } => Some(EventPluginized::delayed(
197 OverlordEvent::StartedCastAbility {
198 by_entity_id,
199 ability_id,
200 duration_ticks: *animation_duration_ticks,
201 },
202 *delay_ticks,
203 )),
204 StartCastAbilityResult::None => None,
205 }
206 }
207
208 #[allow(clippy::type_complexity)]
209 pub fn vec_into_actions_with_deadlines_and_events(
210 results: &Vec<StartCastAbilityResult>,
211 class_id: Uuid,
212 game_config: &GameConfig,
213 ability_id: AbilityId,
214 entity_id: EntityId,
215 current_tick: u64,
216 ) -> anyhow::Result<(
217 Vec<ActionWithDeadline>,
218 Vec<EventPluginized<OverlordEvent, OverlordState>>,
219 )> {
220 let mut actions = vec![];
221 let mut events = vec![];
222
223 for result in results {
224 if matches!(result, StartCastAbilityResult::Attack { .. }) {
225 actions.push(result.into_entity_action_with_deadline(
226 class_id,
227 game_config,
228 ability_id,
229 current_tick,
230 )?);
231 }
232
233 if let Some(event) = result.into_event(ability_id, entity_id) {
234 events.push(event);
235 }
236 }
237
238 Ok((actions, events))
239 }
240}
241
242#[derive(Clone, Default)]
243pub struct DescriptionValuesVec(Arc<std::sync::Mutex<Vec<f64>>>);
244
245impl rhai::CustomType for DescriptionValuesVec {
246 fn build(mut builder: rhai::TypeBuilder<Self>) {
247 builder
248 .with_name("DescriptionValuesVec")
249 .with_fn("push", DescriptionValuesVec::push)
250 .with_fn("push", DescriptionValuesVec::push_int);
251 }
252}
253
254impl DescriptionValuesVec {
255 pub fn push(&mut self, value: f64) {
256 self.0.lock().unwrap().push(value)
257 }
258
259 pub fn push_int(&mut self, value: i64) {
260 self.0.lock().unwrap().push(value as f64)
261 }
262
263 pub fn values(&self) -> Vec<f64> {
264 self.0.lock().unwrap().clone()
265 }
266}
267
268#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
269pub struct CustomEventData(pub BTreeMap<String, i64>);
270
271impl CustomEventData {
272 pub fn add(&mut self, key: &str, delta: i64) {
273 let value = self
274 .0
275 .entry(key.to_owned())
276 .and_modify(|x| *x += delta)
277 .or_insert(delta);
278 if *value == 0 {
279 self.0.remove(key);
280 }
281 }
282}
283
284impl From<EssencesCustomEventData> for CustomEventData {
285 fn from(value: EssencesCustomEventData) -> Self {
286 CustomEventData(value.0)
287 }
288}
289
290impl From<CustomEventData> for EssencesCustomEventData {
291 fn from(value: CustomEventData) -> Self {
292 EssencesCustomEventData(value.0)
293 }
294}
295
296impl CustomType for CustomEventData {
297 fn build(mut builder: TypeBuilder<Self>) {
298 builder
299 .with_name("CustomEventData")
300 .with_fn("CustomEventData", Self::default)
301 .with_indexer_get(|ea: &mut CustomEventData, idx: String| -> rhai::Dynamic {
302 match ea.0.get(&idx) {
303 Some(value) => (*value).into(),
304 None => ().into(),
305 }
306 })
307 .with_fn("add", CustomEventData::add);
308 }
309}
310
311#[derive(Debug, Clone, Default, CustomType)]
312pub struct StartCastProjectileResult {
313 pub projectile_data: CustomEventData,
314 pub animation_duration_ticks: u64,
315}
316
317impl StartCastProjectileResult {
318 pub fn into_frontend_event(
319 &self,
320 by_entity_id: EntityId,
321 to_entity_id: EntityId,
322 projectile_id: ProjectileId,
323 ) -> EventPluginized<OverlordEvent, OverlordState> {
324 EventPluginized::now(OverlordEvent::StartedCastProjectile {
325 by_entity_id,
326 to_entity_id,
327 projectile_id,
328 duration_ticks: self.animation_duration_ticks,
329 })
330 }
331}
332
333#[derive(Debug, Clone, Default, PartialEq, Eq)]
334pub struct UuidIntPair {
335 pub id: Uuid,
336 pub value: i64,
337}
338
339impl CustomType for UuidIntPair {
340 fn build(mut builder: TypeBuilder<Self>) {
341 builder.with_fn("UuidIntPair", |id: Uuid, value: i64| Self { id, value });
342 }
343}
344
345#[derive(Debug, Clone, Default, PartialEq, Eq)]
346pub struct OpponentGenerationResult {
347 pub items: Vec<UuidIntPair>,
348 pub abilities: Vec<UuidIntPair>,
349 pub power: i64,
350 pub level: i64,
351 pub class_id: Uuid,
352}
353
354impl OpponentGenerationResult {
355 pub fn push_item(&mut self, item: UuidIntPair) {
356 self.items.push(item);
357 }
358
359 pub fn push_ability(&mut self, ability: UuidIntPair) {
360 self.abilities.push(ability);
361 }
362
363 pub fn set_power(&mut self, power: i64) {
364 self.power = power;
365 }
366
367 pub fn set_level(&mut self, level: i64) {
368 self.level = level;
369 }
370
371 pub fn set_class_id(&mut self, class_id: Uuid) {
372 self.class_id = class_id;
373 }
374}
375
376impl CustomType for OpponentGenerationResult {
377 fn build(mut builder: TypeBuilder<Self>) {
378 builder
379 .with_name("OpponentGenerationResult")
380 .with_fn("push_item", OpponentGenerationResult::push_item)
381 .with_fn("push_ability", OpponentGenerationResult::push_ability)
382 .with_fn("set_power", OpponentGenerationResult::set_power)
383 .with_fn("set_level", OpponentGenerationResult::set_level)
384 .with_fn("set_class_id", OpponentGenerationResult::set_class_id);
385 }
386}
387
388#[derive(Debug, Clone, Default, PartialEq, Eq)]
389pub struct TestPlayerResult {
390 pub items: Vec<UuidIntPair>,
391 pub abilities: Vec<UuidIntPair>,
392 pub pets: Vec<UuidIntPair>,
393 pub level: i64,
394 pub class_id: Uuid,
395 pub custom_values: Vec<(String, i64)>,
396 pub chapter_level: Option<i64>,
397 pub username: Option<String>,
398}
399
400impl TestPlayerResult {
401 pub fn push_item(&mut self, item: UuidIntPair) {
402 self.items.push(item);
403 }
404
405 pub fn push_ability(&mut self, ability: UuidIntPair) {
406 self.abilities.push(ability);
407 }
408
409 pub fn push_pet(&mut self, pet: UuidIntPair) {
410 self.pets.push(pet);
411 }
412
413 pub fn set_level(&mut self, level: i64) {
414 self.level = level;
415 }
416
417 pub fn set_class_id(&mut self, class_id: Uuid) {
418 self.class_id = class_id;
419 }
420
421 pub fn set_custom_value(&mut self, key: String, value: i64) {
422 self.custom_values.push((key, value));
423 }
424
425 pub fn set_chapter_level(&mut self, chapter_level: i64) {
426 self.chapter_level = Some(chapter_level);
427 }
428
429 pub fn set_username(&mut self, username: String) {
430 self.username = Some(username);
431 }
432}
433
434impl CustomType for TestPlayerResult {
435 fn build(mut builder: TypeBuilder<Self>) {
436 builder
437 .with_name("TestPlayerResult")
438 .with_fn("push_item", TestPlayerResult::push_item)
439 .with_fn("push_ability", TestPlayerResult::push_ability)
440 .with_fn("push_pet", TestPlayerResult::push_pet)
441 .with_fn("set_level", TestPlayerResult::set_level)
442 .with_fn("set_class_id", TestPlayerResult::set_class_id)
443 .with_fn("set_custom_value", TestPlayerResult::set_custom_value)
444 .with_fn("set_chapter_level", TestPlayerResult::set_chapter_level)
445 .with_fn("set_username", TestPlayerResult::set_username);
446 }
447}
448
449#[derive(Debug)]
450pub struct ScriptRunner<E: Event + EventRhaiEnum, S: State> {
451 script_runner: event_system::script::runner::ScriptRunner<E, S>,
452}
453
454impl<E: Event + EventRhaiEnum, S: State> ScriptRunner<E, S> {
455 pub fn new(game_config: &GameConfig) -> Self {
456 let mut script_runner = event_system::script::runner::ScriptRunner::new();
457
458 for script_module in &game_config.script_modules {
459 script_runner.register_module(&script_module.name, &script_module.script);
460 }
461
462 script_runner.build_type::<StartCastAbilityScriptResult>();
463 script_runner.build_type::<StartCastAbilityScriptResultVec>();
464 script_runner.build_type::<Coordinates>();
465 script_runner.build_type::<DescriptionValuesVec>();
466 script_runner.build_type::<StartCastProjectileResult>();
467 script_runner.build_type::<CustomEventData>();
468 script_runner.build_type::<OpponentGenerationResult>();
469 script_runner.build_type::<TestPlayerResult>();
470 script_runner.build_type::<UuidIntPair>();
471
472 script_runner.get_engine().register_fn(
473 "spawn_entity",
474 |entity_template_id: Uuid,
475 position: Coordinates,
476 entity_team: EntityTeam,
477 has_big_hp_bar: bool,
478 entity_attributes: EntityAttributes,
479 random: ScriptRandom| {
480 OverlordEventSpawnEntity {
481 id: uuid::Builder::from_random_bytes(random.random_bytes()).into_uuid(),
482 entity_template_id,
483 position,
484 entity_team,
485 has_big_hp_bar,
486 entity_attributes,
487 }
488 },
489 );
490
491 script_runner
492 .get_engine()
493 .register_fn("give_quests", |quest_ids: Array| OverlordEventNewQuests {
494 quest_ids: quest_ids
495 .into_iter()
496 .map(|d| d.cast::<Uuid>())
497 .collect::<Vec<Uuid>>(),
498 });
499
500 Self { script_runner }
501 }
502
503 pub fn compile_script_public(&self, script: &str) -> anyhow::Result<rhai::AST> {
504 self.script_runner.compile_script_public(script)
505 }
506
507 pub fn build_type<T: CustomType>(&mut self) {
508 self.script_runner.build_type::<T>();
509 }
510
511 pub fn register_module(&mut self, name: &str, script: &str) {
512 self.script_runner.register_module(name, script);
513 }
514
515 pub fn run_test(
516 &mut self,
517 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
518 script: &str,
519 ) -> Vec<ScriptTestReport> {
520 self.script_runner.run_test(scope_setter, script)
521 }
522
523 pub fn run_event(
525 &self,
526 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
527 state: &S,
528 script: &str,
529 ) -> Result<Vec<E>, Box<dyn std::error::Error>> {
530 let mut scope = scope_setter(ScriptScope::new()).scope;
531 scope.push_constant("State", state.clone());
532 scope.push("Result", EventVec::<E>::default());
533
534 self.script_runner.run_with_scope(&mut scope, script)?;
535
536 let Some(result) = scope.get_value::<EventVec<E>>("Result") else {
537 return Err(Box::new(ScriptError::NoScriptResultFound));
538 };
539
540 Ok(result.events())
541 }
542
543 pub fn run_conditional_progress(
544 &self,
545 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
546 script: &str,
547 ) -> ConditionalProgress {
548 let mut scope = scope_setter(ScriptScope::new()).scope;
549
550 scope.push(
551 "Result",
552 ConditionalProgress {
553 current: 0,
554 target: 0,
555 },
556 );
557
558 if let Err(err) = self.script_runner.run_with_scope(&mut scope, script) {
559 tracing::error!("Error running conditional progress script: {}", err);
560 return ConditionalProgress {
561 current: 0,
562 target: 1,
563 };
564 }
565
566 scope
567 .get_value::<ConditionalProgress>("Result")
568 .unwrap_or_else(|| {
569 tracing::error!("Conditional progress script result not found");
570 ConditionalProgress {
571 current: 0,
572 target: 1,
573 }
574 })
575 }
576
577 pub fn run_currencies_calculate(
578 &self,
579 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
580 script: &str,
581 ) -> Vec<ESCurrencyUnit> {
582 let mut scope = scope_setter(ScriptScope::new()).scope;
583
584 scope.push("Result", CurrenciesVec::default());
585
586 if let Err(err) = self.script_runner.run_with_scope(&mut scope, script) {
587 tracing::error!("Error running currency script script: {err}");
588 return vec![];
589 }
590
591 match scope.get_value::<CurrenciesVec>("Result") {
592 Some(v) => v.currencies(),
593 None => {
594 tracing::error!("Currency Unit script result not found");
595 vec![]
596 }
597 }
598 }
599
600 pub fn run_ability_shards_calculate(
601 &self,
602 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
603 script: &str,
604 ) -> Vec<AbilityShard> {
605 let mut scope = scope_setter(ScriptScope::new()).scope;
606
607 scope.push("Result", AbilityShardsVec::default());
608
609 if let Err(err) = self.script_runner.run_with_scope(&mut scope, script) {
610 tracing::error!("Error running currency script script: {err}");
611 return vec![];
612 }
613
614 match scope.get_value::<AbilityShardsVec>("Result") {
615 Some(v) => v
616 .ability_shards()
617 .into_iter()
618 .map(|ability_shard| AbilityShard {
619 ability_id: ability_shard.ability_id,
620 shards_amount: ability_shard.shards_amount,
621 })
622 .collect(),
623 None => {
624 tracing::error!("Currency Unit script result not found");
625 vec![]
626 }
627 }
628 }
629
630 pub fn run_description_values_calculate(
631 &self,
632 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
633 script: &str,
634 ability_level: i64,
635 ) -> Vec<f64> {
636 let mut scope = scope_setter(ScriptScope::new()).scope;
637
638 scope.push_constant("AbilityLevel", ability_level);
639 scope.push("Result", DescriptionValuesVec::default());
640
641 if let Err(err) = self.script_runner.run_with_scope(&mut scope, script) {
642 tracing::error!("Error running description values script: {err}");
643 return vec![];
644 }
645
646 match scope.get_value::<DescriptionValuesVec>("Result") {
647 Some(v) => v.values(),
648 None => {
649 tracing::error!("Description values script result not found");
650 vec![]
651 }
652 }
653 }
654
655 pub fn run_talent_description_values_calculate(
656 &self,
657 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
658 script: &str,
659 talent_level: i64,
660 ) -> Vec<f64> {
661 let mut scope = scope_setter(ScriptScope::new()).scope;
662
663 scope.push_constant("TalentLevel", talent_level);
664 scope.push("Result", DescriptionValuesVec::default());
665
666 if let Err(err) = self.script_runner.run_with_scope(&mut scope, script) {
667 tracing::error!("Error running talent description values script: {err}");
668 return vec![];
669 }
670
671 match scope.get_value::<DescriptionValuesVec>("Result") {
672 Some(v) => v.values(),
673 None => {
674 tracing::error!("Talent description values script result not found");
675 vec![]
676 }
677 }
678 }
679
680 pub fn run_abilities_ids_script(
681 &self,
682 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
683 script: &str,
684 ) -> Vec<Uuid> {
685 let mut scope = scope_setter(ScriptScope::new()).scope;
686
687 scope.push("Result", AbilityIdsVec::default());
688
689 if let Err(err) = self.script_runner.run_with_scope(&mut scope, script) {
690 tracing::error!("Error running fast abilities equip script: {err}");
691 return vec![];
692 }
693
694 match scope.get_value::<AbilityIdsVec>("Result") {
695 Some(v) => v.ids(),
696 None => {
697 tracing::error!("Fast abilities equip script result not found");
698 vec![]
699 }
700 }
701 }
702
703 pub fn run_item_ids_script(
704 &self,
705 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
706 script: &str,
707 ) -> Vec<Uuid> {
708 let mut scope = scope_setter(ScriptScope::new()).scope;
709
710 scope.push("Result", ItemIdsVec::default());
711
712 if let Err(err) = self.script_runner.run_with_scope(&mut scope, script) {
713 tracing::error!("Error running item ids script: {err}");
714 return vec![];
715 }
716
717 match scope.get_value::<ItemIdsVec>("Result") {
718 Some(v) => v.ids(),
719 None => {
720 tracing::error!("Item ids script result not found");
721 vec![]
722 }
723 }
724 }
725
726 pub fn run_start_cast_ability(
727 &self,
728 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
729 state: S,
730 script: &str,
731 ) -> anyhow::Result<Vec<StartCastAbilityResult>> {
732 let mut scope = scope_setter(ScriptScope::new()).scope;
733 scope.push_constant("State", state);
734 scope.push("Result", StartCastAbilityScriptResultVec::default());
735
736 self.script_runner.run_with_scope(&mut scope, script)?;
737
738 let results = match scope.get_value::<StartCastAbilityScriptResultVec>("Result") {
739 Some(v) => v.results(),
740 None => {
741 anyhow::bail!(ScriptError::NoScriptResultFound)
742 }
743 };
744
745 let mut converted_results = Vec::new();
746
747 let mut running = false;
748
749 for result in results.clone().into_iter() {
750 if result.run_duration_ticks.is_some() && result.animation_duration_ticks.is_some() {
751 anyhow::bail!(
752 "Attack and run provided in one singular result {:?}",
753 result
754 )
755 }
756
757 let converted_result = if let (Some(run_duration_ticks), Some(coordinates)) =
758 (result.run_duration_ticks, result.coordinates)
759 {
760 running = true;
761 StartCastAbilityResult::Run {
762 coordinates,
763 run_duration_ticks,
764 }
765 } else if let (
766 Some(animation_duration_ticks),
767 Some(target_entity_id),
768 Some(delay_ticks),
769 ) = (
770 result.animation_duration_ticks,
771 result.target_entity_id,
772 result.delay_ticks,
773 ) {
774 StartCastAbilityResult::Attack {
775 delay_ticks,
776 animation_duration_ticks,
777 target_entity_id,
778 }
779 } else {
780 StartCastAbilityResult::None
781 };
782 converted_results.push(converted_result);
783 }
784
785 if running && converted_results.len() > 1 {
786 anyhow::bail!(
788 "More than 1 result in start_cast_ability with running {:?}",
789 results
790 )
791 };
792
793 Ok(converted_results)
794 }
795
796 pub fn run_start_cast_projectile(
797 &self,
798 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
799 script: &str,
800 ) -> anyhow::Result<StartCastProjectileResult> {
801 let mut scope = scope_setter(ScriptScope::new()).scope;
802
803 scope.push("Result", StartCastProjectileResult::default());
804
805 if let Err(err) = self.script_runner.run_with_scope(&mut scope, script) {
806 tracing::error!("Error running start cast projectile script: {err}");
807 return Err(err);
808 };
809
810 match scope.get_value::<StartCastProjectileResult>("Result") {
811 Some(v) => Ok(v),
812 None => {
813 anyhow::bail!(ScriptError::NoScriptResultFound)
814 }
815 }
816 }
817
818 pub fn run_expression<T: Clone + Send + Sync + 'static>(
819 &self,
820 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
821 script: &str,
822 ) -> anyhow::Result<T> {
823 let mut scope = scope_setter(ScriptScope::new()).scope;
824 match self.script_runner.eval_with_scope(&mut scope, script) {
825 Ok(result) => Ok(result),
826 Err(err) => {
827 tracing::error!("Error running expression script: {}", err);
828 Err(err)
829 }
830 }
831 }
832
833 pub fn run_party_power_adjust(
834 &self,
835 player_character_state: &CharacterState,
836 party_character_state: &CharacterState,
837 script: &str,
838 ) -> anyhow::Result<i64> {
839 self.run_expression(
840 |mut scope_setter| {
841 scope_setter.set_const("PlayerCharacterState", player_character_state.clone());
842 scope_setter.set_const("PartyCharacterState", party_character_state.clone());
843 scope_setter
844 },
845 script,
846 )
847 }
848
849 pub fn calculate_character_power(
850 &self,
851 character_state: &essences::character_state::CharacterState,
852 game_config: &GameConfig,
853 ) -> anyhow::Result<i64> {
854 let Ok(power) = self.run_expression(
855 |mut scope_setter| {
856 scope_setter.set_const("CharacterState", character_state.clone());
857 scope_setter
858 },
859 &game_config.game_settings.character_power_calculate_script,
860 ) else {
861 anyhow::bail!("Couldn't calculate player power");
862 };
863 Ok(power)
864 }
865
866 pub fn calculate_bot_power(&self, bot: &Bot, game_config: &GameConfig) -> anyhow::Result<i64> {
867 let Ok(power) = self.run_expression(
868 |mut scope_setter| {
869 scope_setter.set_const("Bot", bot.clone());
870 scope_setter
871 },
872 &game_config.game_settings.character_power_calculate_script,
873 ) else {
874 anyhow::bail!("Couldn't calculate bot power");
875 };
876 Ok(power)
877 }
878
879 pub fn calculate_item_power(
880 &self,
881 item: Item,
882 game_config: &GameConfig,
883 ) -> anyhow::Result<i64> {
884 let Ok(power) = self.run_expression(
885 |mut scope_setter| {
886 scope_setter.set_const("Item", item);
887 scope_setter
888 },
889 &game_config.game_settings.item_power_calculate_script,
890 ) else {
891 anyhow::bail!("Couldn't calculate item power");
892 };
893 Ok(power)
894 }
895
896 pub fn generate_opponent(
897 &self,
898 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
899 character_state: &essences::character_state::CharacterState,
900 expected_rating: i64,
901 script: &str,
902 ) -> anyhow::Result<OpponentGenerationResult> {
903 let mut scope = scope_setter(ScriptScope::new()).scope;
904
905 scope.push_constant("CharacterState", character_state.clone());
906 scope.push_constant("ExpectedRating", expected_rating);
907 scope.push("Result", OpponentGenerationResult::default());
908 scope.push("Random", ScriptRandom::from_entropy());
909
910 if let Err(err) = self.script_runner.run_with_scope(&mut scope, script) {
911 tracing::error!("Error running generate opponent script: {err}");
912 return Err(err);
913 }
914
915 match scope.get_value::<OpponentGenerationResult>("Result") {
916 Some(v) => Ok(v),
917 None => {
918 anyhow::bail!(ScriptError::NoScriptResultFound)
919 }
920 }
921 }
922
923 pub fn generate_test_player(
924 &self,
925 character_state: &essences::character_state::CharacterState,
926 script: &str,
927 ) -> anyhow::Result<TestPlayerResult> {
928 let mut scope = ScriptScope::<E>::new().scope;
929
930 scope.push_constant("CharacterState", character_state.clone());
931 scope.push("Result", TestPlayerResult::default());
932 scope.push("Random", ScriptRandom::from_entropy());
933
934 if let Err(err) = self.script_runner.run_with_scope(&mut scope, script) {
935 tracing::error!("Error running test_party_player_script: {err}");
936 return Err(err);
937 }
938
939 match scope.get_value::<TestPlayerResult>("Result") {
940 Some(v) => Ok(v),
941 None => anyhow::bail!(ScriptError::NoScriptResultFound),
942 }
943 }
944
945 pub fn run_entity_attributes_calculate(
946 &self,
947 scope_setter: impl FnOnce(ScriptScope<E>) -> ScriptScope<E>,
948 script: &str,
949 ) -> super::attributes::AttributeDeltas {
950 let mut attributes_deltas = super::attributes::AttributeDeltas::new();
951
952 let mut scope = scope_setter(ScriptScope::new()).scope;
953 scope.push("Result", EntityAttributesVec::default());
954
955 if let Err(err) = self.script_runner.run_with_scope(&mut scope, script) {
956 tracing::error!("Error running currency script script: {err}");
957 return attributes_deltas;
958 }
959
960 match scope.get_value::<EntityAttributesVec>("Result") {
961 Some(v) => {
962 v.entity_attributes()
963 .into_iter()
964 .for_each(|entity_attribute| {
965 *attributes_deltas.entry(entity_attribute.id).or_insert(0) +=
966 entity_attribute.value;
967 });
968 }
969 None => {
970 tracing::error!("Currency Unit script result not found");
971 }
972 }
973
974 attributes_deltas
975 }
976
977 pub fn try_get_item_template_id(
978 &self,
979 character_state: &CharacterState,
980 game_config: &GameConfig,
981 ) -> anyhow::Result<Option<Uuid>> {
982 let item_template_id = self.run_expression::<Option<Uuid>>(
983 |mut scope_setter| {
984 scope_setter.set_const("CharacterState", character_state.clone());
985 scope_setter
986 },
987 &game_config.game_settings.chest_item_choose_script,
988 )?;
989 Ok(item_template_id)
990 }
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996
997 use crate::event::OverlordEventDamage;
998 use crate::{event::OverlordEvent, state::OverlordState};
999 use configs::tests_game_config::generate_game_config_for_tests;
1000 use essences::character_state::CharacterState;
1001 use essences::characters::Character;
1002 use event_system::event::EventStruct;
1003 use event_system::script::runner::ScriptRandom;
1004
1005 use rand::SeedableRng;
1006 use test_log::test;
1007
1008 use std::str::FromStr;
1009
1010 fn create_script_runner() -> ScriptRunner<OverlordEvent, OverlordState> {
1011 let game_config = generate_game_config_for_tests();
1012 ScriptRunner::new(&game_config)
1013 }
1014
1015 #[test]
1016 fn test_event() {
1017 let cases = 42; let state = OverlordState {
1019 character_state: CharacterState {
1020 character: Character {
1021 cases,
1022 ..Default::default()
1023 },
1024 ..Default::default()
1025 },
1026 ..Default::default()
1027 };
1028 let event = OverlordEvent::Damage {
1029 entity_id: Uuid::now_v7(),
1030 damage: 10,
1031 damage_data: Default::default(),
1032 };
1033 let runner = create_script_runner();
1034
1035 let result = runner
1036 .run_event(
1037 |mut scope_setter: ScriptScope<_>| {
1038 scope_setter.set_event(OverlordEventDamage::from_enum(event.clone()));
1039 scope_setter
1040 },
1041 &state,
1042 r#"
1043 fn push_to_result(Result, Event) {
1044 Result.push(Event);
1045 }
1046
1047 push_to_result(Result, Event);
1048 Result.push(OverlordEventError("hello", "world"));
1049 Result.push(OverlordEventNewCharacterLevel(State.character_state.character.cases));
1050 "#,
1051 )
1052 .unwrap();
1053
1054 assert_eq!(
1055 result,
1056 vec![
1057 event,
1058 OverlordEvent::Error {
1059 code: "hello".to_string(),
1060 message: "world".to_string()
1061 },
1062 OverlordEvent::NewCharacterLevel { level: cases },
1063 ]
1064 );
1065 }
1066
1067 #[test]
1068 fn test_conditional_progress() {
1069 let runner = create_script_runner();
1070
1071 let result = runner.run_conditional_progress(
1072 |mut scope_setter: ScriptScope<_>| {
1073 scope_setter.set_const("current", 10i64);
1074 scope_setter
1075 },
1076 "
1077 Result.current = current;
1078 Result.target = 42;
1079 ",
1080 );
1081
1082 assert_eq!(result.current, 10);
1083 assert_eq!(result.target, 42);
1084 assert!(!result.completed());
1085 }
1086
1087 #[test]
1088 fn test_currency_unit() {
1089 let runner = create_script_runner();
1090
1091 let currency_id = Uuid::now_v7();
1092 let currency2_id = Uuid::now_v7();
1093 let result = runner.run_currencies_calculate(
1094 |mut scope_setter: ScriptScope<_>| {
1095 scope_setter.set_const("currency_id", currency_id);
1096 scope_setter.set_const("currency2_id", currency2_id);
1097 scope_setter.set_const("amount", 10i64);
1098 scope_setter
1099 },
1100 "
1101 Result.push(CurrencyUnit(currency_id, amount));
1102 Result.push(CurrencyUnit(currency2_id, amount));
1103 ",
1104 );
1105
1106 assert_eq!(result.len(), 2);
1107 assert_eq!(
1108 result[0],
1109 ESCurrencyUnit {
1110 currency_id,
1111 amount: 10
1112 }
1113 );
1114 assert_eq!(
1115 result[1],
1116 ESCurrencyUnit {
1117 currency_id: currency2_id,
1118 amount: 10
1119 }
1120 );
1121 }
1122
1123 #[test]
1124 fn test_fast_equip_abilities() {
1125 let runner = create_script_runner();
1126
1127 let ability_id = Uuid::now_v7();
1128 let result = runner.run_abilities_ids_script(
1129 |mut scope_setter: ScriptScope<_>| {
1130 scope_setter.set_const("ability_id", ability_id);
1131 scope_setter.set_const("slots", 2u64);
1132 scope_setter
1133 },
1134 "
1135 while Result.len() < slots {
1136 Result.push(ability_id)
1137 }
1138 ",
1139 );
1140
1141 assert_eq!(result.len(), 2);
1142 assert_eq!(result[0], ability_id);
1143 assert_eq!(result[1], ability_id);
1144 }
1145
1146 #[test]
1147 fn test_expression() {
1148 let runner = create_script_runner();
1149
1150 let result: i64 = runner
1151 .run_expression::<i64>(
1152 |mut scope_setter: ScriptScope<_>| {
1153 scope_setter.set_const("current", 10i64);
1154 scope_setter
1155 },
1156 "let a = 10;
1157 let b = 32;
1158 a + b",
1159 )
1160 .unwrap();
1161
1162 assert_eq!(result, 42);
1163 }
1164
1165 #[test]
1166 fn test_random() {
1167 let runner = create_script_runner();
1168
1169 assert_eq!(
1170 runner
1171 .run_expression::<f64>(
1172 |mut scope_setter: ScriptScope<_>| {
1173 scope_setter.set_const(
1174 "Random",
1175 ScriptRandom::new(rand::rngs::StdRng::seed_from_u64(42)),
1176 );
1177 scope_setter
1178 },
1179 "Random.random()",
1180 )
1181 .unwrap(),
1182 0.5265574090027738
1183 );
1184 assert_eq!(
1185 runner
1186 .run_expression::<i64>(
1187 |mut scope_setter: ScriptScope<_>| {
1188 scope_setter.set_const(
1189 "Random",
1190 ScriptRandom::new(rand::rngs::StdRng::seed_from_u64(42)),
1191 );
1192 scope_setter
1193 },
1194 "Random.randint(1337, 1488)",
1195 )
1196 .unwrap(),
1197 1416
1198 );
1199 assert_eq!(
1200 runner
1201 .run_expression::<i64>(
1202 |mut scope_setter: ScriptScope<_>| {
1203 scope_setter.set_const(
1204 "Random",
1205 ScriptRandom::new(rand::rngs::StdRng::seed_from_u64(42)),
1206 );
1207 scope_setter
1208 },
1209 "Random.rand_round(1337.42)",
1210 )
1211 .unwrap(),
1212 1338
1213 );
1214 assert_eq!(
1215 runner.run_currencies_calculate(
1216 |mut scope_setter: ScriptScope<_>| {
1217 scope_setter.set_const(
1218 "Random",
1219 ScriptRandom::new(rand::rngs::StdRng::seed_from_u64(42)),
1220 );
1221 scope_setter
1222 },
1223 r#"
1224 fn add_currency(result, random) {
1225 result.push(CurrencyUnit(uuid("b59b33a2-4d19-4e2c-9cea-e03ea15882a0"), random.randint(1337, 1488)));
1226 }
1227
1228 Result.push(CurrencyUnit(uuid("22297520-abb8-45b9-9fbb-c418b8001246"), Random.randint(1337, 1488)));
1229 add_currency(Result, Random);
1230 "#,
1231 ),
1232 vec![
1233 ESCurrencyUnit {
1234 currency_id: Uuid::from_str("22297520-abb8-45b9-9fbb-c418b8001246").unwrap(),
1235 amount: 1416
1236 },
1237 ESCurrencyUnit {
1238 currency_id: Uuid::from_str("b59b33a2-4d19-4e2c-9cea-e03ea15882a0").unwrap(),
1239 amount: 1418
1240 },
1241 ]
1242 );
1243 }
1244}