Skip to main content

rgpre/import/
combat_ini.rs

1//! Typed combat moves and taunts from COMBAT.INI.
2use super::ini::{ini_enum, ini_section, required, sections};
3use crate::{Result, error::Error};
4use std::collections::BTreeMap;
5
6/// Combat definitions indexed by the numeric suffix of their section names.
7#[derive(Debug, Clone, PartialEq, serde::Serialize)]
8pub struct CombatIni {
9    pub misc: CombatSettings,
10    pub attacks: BTreeMap<u32, Attack>,
11    pub defenses: BTreeMap<u32, Defense>,
12    pub taunts: BTreeMap<u32, Taunt>,
13}
14
15impl CombatIni {
16    /// Requires `[misc]`. Sparse IDs and omitted fields are preserved.
17    /// Section/key matching ignores case and the first duplicate key wins.
18    pub fn parse(text: &str) -> Result<Self> {
19        let sections = sections(text, "COMBAT.INI")?;
20        let mut result = Self {
21            misc: CombatSettings::parse(Some(required(&sections, "COMBAT.INI", "misc")?))?,
22            attacks: BTreeMap::new(),
23            defenses: BTreeMap::new(),
24            taunts: BTreeMap::new(),
25        };
26        for (name, section) in &sections {
27            for prefix in ["attack", "defend", "taunt"] {
28                let Some(suffix) = name.strip_prefix(prefix) else {
29                    continue;
30                };
31                let id = suffix.parse::<u32>().map_err(|_| {
32                    Error::Parse(format!("COMBAT.INI [{name}]: invalid section index"))
33                })?;
34                let duplicate = match prefix {
35                    "attack" => result
36                        .attacks
37                        .insert(id, Attack::parse(Some(section))?)
38                        .is_some(),
39                    "defend" => result
40                        .defenses
41                        .insert(id, Defense::parse(Some(section))?)
42                        .is_some(),
43                    _ => result
44                        .taunts
45                        .insert(id, Taunt::parse(Some(section))?)
46                        .is_some(),
47                };
48                if duplicate {
49                    return Err(Error::Parse(format!(
50                        "COMBAT.INI [{name}]: duplicate numeric section index {id}"
51                    )));
52                }
53            }
54        }
55        Ok(result)
56    }
57}
58
59ini_section! {
60    /// Global combat parameters in original engine units.
61    CombatSettings {
62        sword_clank_1: i32 => "sword_clank_1",
63        sword_clank_2: i32 => "sword_clank_2",
64        non_engaged_dist: i32 => "non_engaged_dist",
65        in_face_dist: i32 => "in_face_dist",
66        non_engaged_spacing: i32 => "non_engaged_spacing",
67        in_combat_threshold: i32 => "in_combat_threshold",
68        player_can_die: bool => "player_can_die",
69        cyrus_defend_interval: i32 => "cyrus_defend_interval",
70        node_timer: i32 => "node_timer",
71        max_taunts: u32 => "max_taunts",
72    }
73}
74
75ini_enum! {
76    /// Attack category; undocumented codes retain their numeric value.
77    AttackType { Melee = 0, Missile = 1, Combo = 2, Stab = 3, Finishing = 4 }
78}
79
80ini_enum! {
81    /// Vertical attack targeting zone.
82    Elevation { Regular = 0, Above = 1, Below = 2 }
83}
84
85ini_section! {
86    /// One attack, including the optional sub-attack references of a combo.
87    Attack {
88        attack_type: AttackType => "type",
89        arc: i32 => "arc",
90        arc_center: i32 => "arc_center",
91        elevation: Elevation => "elevation",
92        min_range: i32 => "min_range",
93        max_range: i32 => "max_range",
94        first_collide_frame: i32 => "first_collide_frame",
95        collide_duration: i32 => "collide_duration",
96        damage: i32 => "damage",
97        force: i32 => "force",
98        defense: i32 => "defense",
99        first_defend_frame: i32 => "first_defend_frame",
100        defend_duration: i32 => "defend_duration",
101        animation: i32 => "animation",
102        col_vertex: i32 => "col_vertex",
103        col_sphere_size: i32 => "col_sphere_size",
104        num_attacks: u32 => "num_attacks",
105        attack00: u32 => "attack00",
106        attack01: u32 => "attack01",
107        attack02: u32 => "attack02",
108    }
109}
110
111ini_section! {
112    /// One defense move. Omitted `arc`, including in defend00, remains `None`.
113    Defense {
114        arc: i32 => "arc",
115        min_range: i32 => "min_range",
116        max_range: i32 => "max_range",
117        first_collide_frame: i32 => "first_collide_frame",
118        collide_duration: i32 => "collide_duration",
119        animation: i32 => "animation",
120    }
121}
122
123ini_enum! {
124    /// Event that triggers a combat voice line.
125    TauntTrigger {
126        BeginCombat = 0,
127        Attack = 1,
128        HurtByOpponent = 2,
129        HitOpponent = 3,
130        Defend = 4,
131        Move = 5,
132        Death = 6,
133        KilledOpponent = 7,
134        WantSwitchOut = 8,
135        WantSwitchIn = 9,
136        UnarmedMaleOpponent = 10,
137        UnarmedFemaleOpponent = 11,
138    }
139}
140
141ini_section! {
142    /// Combat voice label, preserving its case and any `&NNN` reference.
143    Taunt {
144        trigger: TauntTrigger => "type",
145        rtx_label: String => "rtx_label",
146        animation: i32 => "animation",
147    }
148}