overlord_event_system/
script.rs

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    /// Run a script and get resulting events
524    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            // TODO questionable
787            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; // TODO remove cases
1018        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}