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(
154 &self,
155 battler: &BattlerState<Self>,
156 _state: &BattleState<Self>,
157 ) -> Self::Move {
158 battler.moves.first().cloned().unwrap_or_else(basic_attack)
159 }
160
161 fn apply_move_effect(
162 &self,
163 _effect: MoveEffect,
164 _user: &mut BattlerState<Self>,
165 _target: &mut BattlerState<Self>,
166 ) -> EffectResult {
167 EffectResult::NoEffect
168 }
169
170 fn create_monster(&self, species: Self::Species, level: u8) -> BattlerState<Self> {
171 BattlerState::new(species, 1, 1, EnumMap::default(), Vec::new()).with_level(level)
172 }
173}
174
175impl EffectProvider for GenericProvider {
176 type EffectStateKind = VolatileKind;
177
178 fn effect_for_move(&self, _m: &Self::Move) -> Option<&'static Effect<Self>> {
181 None
182 }
183
184 fn effect_for_status(&self, _s: &Self::Status) -> Option<&'static Effect<Self>> {
186 None
187 }
188
189 fn turn_order_rank(
192 &self,
193 state: &BattleState<Self>,
194 who: BattlerRef,
195 _action: &Self::Move,
196 ) -> (i32, i32) {
197 let b = if who.side == 0 {
198 &state.player_battlers[who.slot as usize]
199 } else {
200 &state.opponent_battlers[who.slot as usize]
201 };
202 (0, -(eff_stat(b, "speed") as i32))
203 }
204}
205
206thread_local! {
214 static HOST: RefCell<Option<&'static RulesHost<GenericProvider>>> =
215 const { RefCell::new(None) };
216}
217
218pub fn install_compiled(compiled: CompiledRuleset) {
220 let host = RulesHost::new(compiled, GenericBindings);
221 let leaked: &'static RulesHost<GenericProvider> = Box::leak(Box::new(host));
222 HOST.with(|h| *h.borrow_mut() = Some(leaked));
223}
224
225impl RulesProvider for GenericProvider {
226 type Bindings = GenericBindings;
227
228 fn compiled(&self) -> &CompiledRuleset {
229 &Self::rules_host().expect("rules host installed").compiled
230 }
231 fn bindings(&self) -> &Self::Bindings {
232 &Self::rules_host().expect("rules host installed").bindings
233 }
234 fn rules_host() -> Option<&'static RulesHost<GenericProvider>> {
235 HOST.with(|h| *h.borrow())
236 }
237}
238
239#[derive(Debug, Default, Clone, Copy)]
245pub struct GenericBindings;
246
247impl GenericBindings {
248 fn stat_key(stat_index: usize) -> Option<String> {
250 let host = GenericProvider::rules_host()?;
251 let name = host.compiled.stats.get(stat_index)?;
252 Some(normalize_stat_key(name))
253 }
254
255 fn type_name(type_index: usize) -> Option<String> {
257 let host = GenericProvider::rules_host()?;
258 host.compiled.types.get(type_index).cloned()
259 }
260}
261
262impl RuleBindings<GenericProvider> for GenericBindings {
263 fn apply_boost(&self, b: &mut BattlerState<GenericProvider>, stat_index: usize, stages: i8) -> bool {
264 if Self::stat_key(stat_index).is_none() {
265 return false;
266 }
267 let id = StatId(stat_index as u16);
268 let cur = b.stat_stages.get(id).copied().unwrap_or(0);
269 b.stat_stages.set(id, (cur + stages).clamp(-MAX_STAGE, MAX_STAGE));
270 true
271 }
272
273 fn set_status(&self, b: &mut BattlerState<GenericProvider>, status_index: usize) -> bool {
274 b.status = Some(StatusId(status_index as u16));
275 true
276 }
277
278 fn has_type(&self, b: &BattlerState<GenericProvider>, type_index: usize) -> bool {
279 match (Self::type_name(type_index), &b.species.element) {
280 (Some(name), Some(element)) => name.eq_ignore_ascii_case(element),
281 _ => false,
282 }
283 }
284
285 fn type_chart_mult(
289 &self,
290 ctx: &BattleCtx<'_, GenericProvider>,
291 move_type_index: usize,
292 defender: BattlerRef,
293 ) -> (u32, u32) {
294 let Some(host) = GenericProvider::rules_host() else {
295 return (1, 1);
296 };
297 let Some(element) = &ctx.battler(defender).species.element else {
298 return (1, 1);
299 };
300 let Some(def_index) = host
301 .compiled
302 .types
303 .iter()
304 .position(|t| t.eq_ignore_ascii_case(element))
305 else {
306 return (1, 1);
307 };
308 host.compiled.chart_mult(move_type_index, def_index)
309 }
310
311 fn make_volatile(&self, name: &str, amount: u16) -> Option<VolatileKind> {
312 Some(VolatileKind {
313 name: name.to_string(),
314 amount,
315 })
316 }
317
318 fn has_volatile(&self, ctx: &BattleCtx<'_, GenericProvider>, who: BattlerRef, name: &str) -> bool {
319 ctx.effects
320 .iter()
321 .any(|e| e.host == who && e.kind.name == name)
322 }
323
324 fn battler_level(&self, b: &BattlerState<GenericProvider>) -> u16 {
325 u16::from(b.level)
326 }
327
328 fn has_status(&self, b: &BattlerState<GenericProvider>, status_index: usize) -> bool {
329 b.status == Some(StatusId(status_index as u16))
330 }
331
332 fn has_any_status(&self, b: &BattlerState<GenericProvider>) -> bool {
333 b.status.is_some()
334 }
335
336 }
340
341pub fn status_index_of(ruleset: &Ruleset, name: &str) -> Option<usize> {
347 ruleset
348 .effects
349 .iter()
350 .filter(|r| r.kind == EffectKind::Status)
351 .position(|r| r.id == name)
352}
353
354pub fn status_names(ruleset: &Ruleset) -> Vec<String> {
357 ruleset
358 .effects
359 .iter()
360 .filter(|r| r.kind == EffectKind::Status)
361 .map(|r| r.id.clone())
362 .collect()
363}
364
365pub fn compile_ruleset(ruleset: &Ruleset) -> Result<CompiledRuleset, LoadError> {
369 CompiledRuleset::compile::<GenericProvider, GenericBindings>(
370 ruleset,
371 DATA_ID_BASE,
372 &GenericBindings,
373 |name| status_index_of(ruleset, name),
374 )
375}
376
377pub fn ron_moves(ruleset: &Ruleset, resource: Option<&str>) -> HashMap<String, RonMove> {
382 ruleset
383 .effects
384 .iter()
385 .filter(|r| r.kind == EffectKind::Move)
386 .map(|rec| {
387 let cost = if rec.cost.is_empty() {
388 None
389 } else {
390 resource.map(|res| {
391 rec.cost
392 .iter()
393 .filter(|c| c.resource == res)
394 .map(|c| u32::from(c.amount))
395 .sum()
396 })
397 };
398 (
399 rec.id.clone(),
400 RonMove {
401 power: rec.power,
402 accuracy: rec.accuracy,
403 mtype: rec.mtype.clone(),
404 cost,
405 },
406 )
407 })
408 .collect()
409}
410
411pub fn validate_ruleset(rules_text: &str) -> Vec<String> {
414 match Ruleset::from_ron(rules_text) {
415 Err(e) => vec![e.to_string()],
416 Ok(ruleset) => match compile_ruleset(&ruleset) {
417 Ok(_) => Vec::new(),
418 Err(e) => vec![e.to_string()],
419 },
420 }
421}
422
423#[derive(Debug, Clone, Default)]
428pub struct RonMove {
429 pub power: Option<u32>,
431 pub accuracy: Option<u32>,
433 pub mtype: Option<String>,
435 pub cost: Option<u32>,
438}
439
440pub struct HookState {
445 pub state: BattleState<GenericProvider>,
447 pub effects: Vec<EffectState<GenericProvider>>,
449 pub mv: MoveContext,
451 pub registry: Vec<&'static Effect<GenericProvider>>,
454 pub move_records: HashMap<String, RonMove>,
456 pub status_names: Vec<String>,
458 pub stat_names: Vec<String>,
460 pub has_resource: bool,
462}
463
464impl HookState {
465 pub fn battler_ref(side: super::Side) -> BattlerRef {
467 match side {
468 super::Side::Player => BattlerRef::PLAYER,
469 super::Side::Enemy => BattlerRef::OPPONENT,
470 }
471 }
472
473 pub fn battler(&self, side: super::Side) -> &BattlerState<GenericProvider> {
475 let r = Self::battler_ref(side);
476 if r.side == 0 {
477 &self.state.player_battlers[r.slot as usize]
478 } else {
479 &self.state.opponent_battlers[r.slot as usize]
480 }
481 }
482
483 pub fn subscribes(&self, skill_id: &str, event: Event) -> bool {
486 let Some(host) = GenericProvider::rules_host() else {
487 return false;
488 };
489 host.compiled
490 .hooks
491 .values()
492 .any(|h| h.source_id == skill_id && h.event == event)
493 }
494}
495
496pub struct RngAdapter<'a>(pub &'a mut dyn super::BattleRng);
500
501impl EngineRng for RngAdapter<'_> {
502 fn next_u8(&mut self) -> u8 {
503 self.0.byte()
504 }
505}
506
507fn raw_stat(c: &Combatant, name: &str) -> u32 {
520 match normalize_stat_key(name).as_str() {
521 "hp" => c.max_hp,
522 "defense" => c.defense,
523 "speed" => c.speed,
524 _ => c.attack,
525 }
526}
527
528fn status_id_of(status: &Option<String>, status_names: &[String]) -> Option<StatusId> {
532 status
533 .as_ref()
534 .and_then(|name| status_names.iter().position(|n| n == name))
535 .map(|idx| StatusId(idx as u16))
536}
537
538pub fn mirror_of(
541 c: &Combatant,
542 stat_names: &[String],
543 status_names: &[String],
544 has_resource: bool,
545) -> BattlerState<GenericProvider> {
546 let mut b = BattlerState::new(
547 SpeciesData {
548 element: c.element.clone(),
549 },
550 c.hp.min(u32::from(u16::MAX)) as u16,
551 c.max_hp.min(u32::from(u16::MAX)) as u16,
552 EnumMap::default(),
553 c.skills.clone(),
554 )
555 .with_level(c.level);
556 b.status = status_id_of(&c.status, status_names);
557 sync_to_mirror(c, &mut b, stat_names, status_names, has_resource);
558 b
559}
560
561pub fn sync_to_mirror(
564 c: &Combatant,
565 b: &mut BattlerState<GenericProvider>,
566 stat_names: &[String],
567 status_names: &[String],
568 has_resource: bool,
569) {
570 b.hp = c.hp.min(u32::from(u16::MAX)) as u16;
571 b.max_hp = c.max_hp.min(u32::from(u16::MAX)) as u16;
572 b.level = c.level;
573 b.status = status_id_of(&c.status, status_names);
574 for (i, name) in stat_names.iter().enumerate() {
575 let id = StatId(i as u16);
576 b.stats.set(id, raw_stat(c, name).min(u32::from(u16::MAX)) as u16);
577 b.stat_stages.set(id, c.stages.get(name));
578 }
579 if has_resource {
580 b.resources.set(
581 0,
582 c.mp.min(u32::from(u16::MAX)) as u16,
583 c.max_mp.min(u32::from(u16::MAX)) as u16,
584 );
585 }
586}
587
588pub fn sync_from_mirror(
590 b: &BattlerState<GenericProvider>,
591 c: &mut Combatant,
592 stat_names: &[String],
593 status_names: &[String],
594 has_resource: bool,
595) {
596 c.hp = u32::from(b.hp);
597 c.max_hp = u32::from(b.max_hp);
598 c.status = b
599 .status
600 .as_ref()
601 .and_then(|id| status_names.get(id.0 as usize).cloned());
602 for (i, name) in stat_names.iter().enumerate() {
603 let id = StatId(i as u16);
604 if let Some(stage) = b.stat_stages.get(id) {
605 c.stages.set(name, *stage);
606 }
607 }
608 if has_resource {
609 c.mp = u32::from(b.resources.current(0).unwrap_or(0));
610 c.max_mp = u32::from(b.resources.max(0).unwrap_or(0));
611 }
612}