1use std::cell::RefCell;
38use std::collections::HashMap;
39
40use dotzuki_engine::battle::rng::BattleRng as EngineRng;
41use dotzuki_engine::battle::stack::{
42 BattleCtx, Effect, EffectProvider, EffectState, Event, MoveContext,
43};
44use dotzuki_engine::battle::{
45 BattleProvider, BattleState, BattlerRef, BattlerState, DamageResult, EffectResult, EnumMap,
46 MoveEffect,
47};
48use dotzuki_rules::{
49 CompiledRuleset, EffectKind, LoadError, RuleBindings, RulesHost, RulesProvider, Ruleset,
50};
51
52use super::{basic_attack, normalize_stat_key, stage_multiplier, Combatant, Skill, MAX_STAGE};
53
54pub const DATA_ID_BASE: u32 = 0x10_000;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct StatId(pub u16);
64
65#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct StatusId(pub u16);
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct TypeId(pub u16);
73
74#[derive(Debug, Clone)]
77pub struct VolatileKind {
78 pub name: String,
80 pub amount: u16,
82}
83
84#[derive(Debug, Clone, Default)]
87pub struct SpeciesData {
88 pub element: Option<String>,
90}
91
92#[derive(Debug, Default, Clone, Copy)]
99pub struct GenericProvider;
100
101fn eff_stat(b: &BattlerState<GenericProvider>, key: &str) -> u32 {
104 let Some(host) = GenericProvider::rules_host() else {
105 return 1;
106 };
107 let Some(idx) = host
108 .compiled
109 .stats
110 .iter()
111 .position(|s| normalize_stat_key(s) == key)
112 else {
113 return 1;
114 };
115 let id = StatId(idx as u16);
116 let raw = u32::from(b.stats.get(id).copied().unwrap_or(1));
117 let stage = b.stat_stages.get(id).copied().unwrap_or(0);
118 stage_multiplier(raw, stage)
119}
120
121impl BattleProvider for GenericProvider {
122 type Monster = ();
123 type Move = Skill;
124 type Ability = ();
125 type Status = StatusId;
126 type Stat = StatId;
127 type Species = SpeciesData;
128 type Type = TypeId;
129 type Item = ();
130
131 fn calculate_damage(
135 &self,
136 move_: &Skill,
137 attacker: &BattlerState<Self>,
138 defender: &BattlerState<Self>,
139 random: u8,
140 is_critical: bool,
141 ) -> DamageResult {
142 let base = move_.power as u64 * eff_stat(attacker, "attack") as u64
143 / eff_stat(defender, "defense").max(1) as u64;
144 let varied = base * (85 + u64::from(random % 16)) / 100;
145 let after_crit = if is_critical { varied * 3 / 2 } else { varied };
146 DamageResult {
147 damage: after_crit.max(1).min(u64::from(u16::MAX)) as u16,
148 effectiveness: 1.0,
149 is_miss: false,
150 }
151 }
152
153 fn select_move(&self, battler: &BattlerState<Self>, _state: &BattleState<Self>) -> Self::Move {
154 battler.moves.first().cloned().unwrap_or_else(basic_attack)
155 }
156
157 fn apply_move_effect(
158 &self,
159 _effect: MoveEffect,
160 _user: &mut BattlerState<Self>,
161 _target: &mut BattlerState<Self>,
162 ) -> EffectResult {
163 EffectResult::NoEffect
164 }
165
166 fn create_monster(&self, species: Self::Species, level: u8) -> BattlerState<Self> {
167 BattlerState::new(species, 1, 1, EnumMap::default(), Vec::new()).with_level(level)
168 }
169}
170
171impl EffectProvider for GenericProvider {
172 type EffectStateKind = VolatileKind;
173
174 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
177 None
178 }
179
180 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
182 None
183 }
184
185 fn turn_order_rank(
188 &self,
189 state: &BattleState<Self>,
190 who: BattlerRef,
191 _action: &Self::Move,
192 ) -> (i32, i32) {
193 let b = if who.side == 0 {
194 &state.player_battlers[who.slot as usize]
195 } else {
196 &state.opponent_battlers[who.slot as usize]
197 };
198 (0, -(eff_stat(b, "speed") as i32))
199 }
200}
201
202thread_local! {
210 static HOST: RefCell<Option<&'static RulesHost<GenericProvider>>> =
211 const { RefCell::new(None) };
212}
213
214pub fn install_compiled(compiled: CompiledRuleset) {
216 let host = RulesHost::new(compiled, GenericBindings);
217 let leaked: &'static RulesHost<GenericProvider> = Box::leak(Box::new(host));
218 HOST.with(|h| *h.borrow_mut() = Some(leaked));
219}
220
221impl RulesProvider for GenericProvider {
222 type Bindings = GenericBindings;
223
224 fn compiled(&self) -> &CompiledRuleset {
225 &Self::rules_host().expect("rules host installed").compiled
226 }
227 fn bindings(&self) -> &Self::Bindings {
228 &Self::rules_host().expect("rules host installed").bindings
229 }
230 fn rules_host() -> Option<&'static RulesHost<GenericProvider>> {
231 HOST.with(|h| *h.borrow())
232 }
233}
234
235#[derive(Debug, Default, Clone, Copy)]
241pub struct GenericBindings;
242
243impl GenericBindings {
244 fn stat_key(stat_index: usize) -> Option<String> {
246 let host = GenericProvider::rules_host()?;
247 let name = host.compiled.stats.get(stat_index)?;
248 Some(normalize_stat_key(name))
249 }
250
251 fn type_name(type_index: usize) -> Option<String> {
253 let host = GenericProvider::rules_host()?;
254 host.compiled.types.get(type_index).cloned()
255 }
256}
257
258impl RuleBindings<GenericProvider> for GenericBindings {
259 fn apply_boost(
260 &self,
261 b: &mut BattlerState<GenericProvider>,
262 stat_index: usize,
263 stages: i8,
264 ) -> bool {
265 if Self::stat_key(stat_index).is_none() {
266 return false;
267 }
268 let id = StatId(stat_index as u16);
269 let cur = b.stat_stages.get(id).copied().unwrap_or(0);
270 b.stat_stages
271 .set(id, (cur + stages).clamp(-MAX_STAGE, MAX_STAGE));
272 true
273 }
274
275 fn set_status(&self, b: &mut BattlerState<GenericProvider>, status_index: usize) -> bool {
276 b.status = Some(StatusId(status_index as u16));
277 true
278 }
279
280 fn has_type(&self, b: &BattlerState<GenericProvider>, type_index: usize) -> bool {
281 match (Self::type_name(type_index), &b.species.element) {
282 (Some(name), Some(element)) => name.eq_ignore_ascii_case(element),
283 _ => false,
284 }
285 }
286
287 fn type_chart_mult(
291 &self,
292 ctx: &BattleCtx<'_, GenericProvider>,
293 move_type_index: usize,
294 defender: BattlerRef,
295 ) -> (u32, u32) {
296 let Some(host) = GenericProvider::rules_host() else {
297 return (1, 1);
298 };
299 let Some(element) = &ctx.battler(defender).species.element else {
300 return (1, 1);
301 };
302 let Some(def_index) = host
303 .compiled
304 .types
305 .iter()
306 .position(|t| t.eq_ignore_ascii_case(element))
307 else {
308 return (1, 1);
309 };
310 host.compiled.chart_mult(move_type_index, def_index)
311 }
312
313 fn make_volatile(&self, name: &str, amount: u16) -> Option<VolatileKind> {
314 Some(VolatileKind {
315 name: name.to_string(),
316 amount,
317 })
318 }
319
320 fn has_volatile(
321 &self,
322 ctx: &BattleCtx<'_, GenericProvider>,
323 who: BattlerRef,
324 name: &str,
325 ) -> bool {
326 ctx.effects
327 .iter()
328 .any(|e| e.host == who && e.kind.name == name)
329 }
330
331 fn battler_level(&self, b: &BattlerState<GenericProvider>) -> u16 {
332 u16::from(b.level)
333 }
334
335 fn has_status(&self, b: &BattlerState<GenericProvider>, status_index: usize) -> bool {
336 b.status == Some(StatusId(status_index as u16))
337 }
338
339 fn has_any_status(&self, b: &BattlerState<GenericProvider>) -> bool {
340 b.status.is_some()
341 }
342
343 }
347
348pub fn status_index_of(ruleset: &Ruleset, name: &str) -> Option<usize> {
354 ruleset
355 .effects
356 .iter()
357 .filter(|r| r.kind == EffectKind::Status)
358 .position(|r| r.id == name)
359}
360
361pub fn status_names(ruleset: &Ruleset) -> Vec<String> {
364 ruleset
365 .effects
366 .iter()
367 .filter(|r| r.kind == EffectKind::Status)
368 .map(|r| r.id.clone())
369 .collect()
370}
371
372pub fn compile_ruleset(ruleset: &Ruleset) -> Result<CompiledRuleset, LoadError> {
376 CompiledRuleset::compile::<GenericProvider, GenericBindings>(
377 ruleset,
378 DATA_ID_BASE,
379 &GenericBindings,
380 |name| status_index_of(ruleset, name),
381 )
382}
383
384pub fn ron_moves(ruleset: &Ruleset, resource: Option<&str>) -> HashMap<String, RonMove> {
389 ruleset
390 .effects
391 .iter()
392 .filter(|r| r.kind == EffectKind::Move)
393 .map(|rec| {
394 let cost = if rec.cost.is_empty() {
395 None
396 } else {
397 resource.map(|res| {
398 rec.cost
399 .iter()
400 .filter(|c| c.resource == res)
401 .map(|c| u32::from(c.amount))
402 .sum()
403 })
404 };
405 (
406 rec.id.clone(),
407 RonMove {
408 power: rec.power,
409 accuracy: rec.accuracy,
410 mtype: rec.mtype.clone(),
411 cost,
412 },
413 )
414 })
415 .collect()
416}
417
418pub fn validate_ruleset(rules_text: &str) -> Vec<String> {
421 match Ruleset::from_ron(rules_text) {
422 Err(e) => vec![e.to_string()],
423 Ok(ruleset) => match compile_ruleset(&ruleset) {
424 Ok(_) => Vec::new(),
425 Err(e) => vec![e.to_string()],
426 },
427 }
428}
429
430#[derive(Debug, Clone, Default)]
435pub struct RonMove {
436 pub power: Option<u32>,
438 pub accuracy: Option<u32>,
440 pub mtype: Option<String>,
442 pub cost: Option<u32>,
445}
446
447pub struct HookState {
452 pub state: BattleState<GenericProvider>,
454 pub effects: Vec<EffectState<GenericProvider>>,
456 pub mv: MoveContext,
458 pub registry: Vec<&'static Effect<GenericProvider>>,
461 pub move_records: HashMap<String, RonMove>,
463 pub status_names: Vec<String>,
465 pub stat_names: Vec<String>,
467 pub has_resource: bool,
469}
470
471impl HookState {
472 pub fn battler_ref(side: super::Side) -> BattlerRef {
474 match side {
475 super::Side::Player => BattlerRef::PLAYER,
476 super::Side::Enemy => BattlerRef::OPPONENT,
477 }
478 }
479
480 pub fn battler(&self, side: super::Side) -> &BattlerState<GenericProvider> {
482 let r = Self::battler_ref(side);
483 if r.side == 0 {
484 &self.state.player_battlers[r.slot as usize]
485 } else {
486 &self.state.opponent_battlers[r.slot as usize]
487 }
488 }
489
490 pub fn subscribes(&self, skill_id: &str, event: Event) -> bool {
493 let Some(host) = GenericProvider::rules_host() else {
494 return false;
495 };
496 host.compiled
497 .hooks
498 .values()
499 .any(|h| h.source_id == skill_id && h.event == event)
500 }
501}
502
503pub struct RngAdapter<'a>(pub &'a mut dyn super::BattleRng);
507
508impl EngineRng for RngAdapter<'_> {
509 fn next_u8(&mut self) -> u8 {
510 self.0.byte()
511 }
512}
513
514fn raw_stat(c: &Combatant, name: &str) -> u32 {
527 match normalize_stat_key(name).as_str() {
528 "hp" => c.max_hp,
529 "defense" => c.defense,
530 "speed" => c.speed,
531 _ => c.attack,
532 }
533}
534
535fn status_id_of(status: &Option<String>, status_names: &[String]) -> Option<StatusId> {
539 status
540 .as_ref()
541 .and_then(|name| status_names.iter().position(|n| n == name))
542 .map(|idx| StatusId(idx as u16))
543}
544
545pub fn mirror_of(
548 c: &Combatant,
549 stat_names: &[String],
550 status_names: &[String],
551 has_resource: bool,
552) -> BattlerState<GenericProvider> {
553 let mut b = BattlerState::new(
554 SpeciesData {
555 element: c.element.clone(),
556 },
557 c.hp.min(u32::from(u16::MAX)) as u16,
558 c.max_hp.min(u32::from(u16::MAX)) as u16,
559 EnumMap::default(),
560 c.skills.clone(),
561 )
562 .with_level(c.level);
563 b.status = status_id_of(&c.status, status_names);
564 sync_to_mirror(c, &mut b, stat_names, status_names, has_resource);
565 b
566}
567
568pub fn sync_to_mirror(
571 c: &Combatant,
572 b: &mut BattlerState<GenericProvider>,
573 stat_names: &[String],
574 status_names: &[String],
575 has_resource: bool,
576) {
577 b.hp = c.hp.min(u32::from(u16::MAX)) as u16;
578 b.max_hp = c.max_hp.min(u32::from(u16::MAX)) as u16;
579 b.level = c.level;
580 b.status = status_id_of(&c.status, status_names);
581 for (i, name) in stat_names.iter().enumerate() {
582 let id = StatId(i as u16);
583 b.stats
584 .set(id, raw_stat(c, name).min(u32::from(u16::MAX)) as u16);
585 b.stat_stages.set(id, c.stages.get(name));
586 }
587 if has_resource {
588 b.resources.set(
589 0,
590 c.mp.min(u32::from(u16::MAX)) as u16,
591 c.max_mp.min(u32::from(u16::MAX)) as u16,
592 );
593 }
594}
595
596pub fn sync_from_mirror(
598 b: &BattlerState<GenericProvider>,
599 c: &mut Combatant,
600 stat_names: &[String],
601 status_names: &[String],
602 has_resource: bool,
603) {
604 c.hp = u32::from(b.hp);
605 c.max_hp = u32::from(b.max_hp);
606 c.status = b
607 .status
608 .as_ref()
609 .and_then(|id| status_names.get(id.0 as usize).cloned());
610 for (i, name) in stat_names.iter().enumerate() {
611 let id = StatId(i as u16);
612 if let Some(stage) = b.stat_stages.get(id) {
613 c.stages.set(name, *stage);
614 }
615 }
616 if has_resource {
617 c.mp = u32::from(b.resources.current(0).unwrap_or(0));
618 c.max_mp = u32::from(b.resources.max(0).unwrap_or(0));
619 }
620}