Skip to main content

rakata_generics/git/
area_effect.rs

1//! Area-of-effect objects, which savegame GITs carry and module GITs do not.
2
3use rakata_core::ResRef;
4use rakata_formats::gff::{get_f32, get_u8, upsert_field};
5use rakata_formats::gff_label;
6use rakata_formats::{GffModel, GffStruct, GffValue};
7
8use crate::shared::ObjectId;
9/// The dimensions an area-of-effect object covers.
10///
11/// `Shape` decides which dimension fields the loader reads at all: `0` reads
12/// `Radius` and nothing else, `1` reads `Length` and `Width` and nothing
13/// else, and any other value reads neither, leaving the effect with no
14/// geometry. Modelling that as one value keeps a circle from carrying a width
15/// and a rectangle from carrying a radius.
16/// The four labels an area effect's footprint spans.
17///
18/// Declared apart from [`GitAreaEffectShape`] for the same reason the
19/// portrait's are: one value, several labels, and which of them get written
20/// depends on the variant.
21#[derive(Debug, Clone, PartialEq, GffModel)]
22#[gff_entry(Shape, wire = u8, stamped)]
23#[gff_entry(Radius, wire = f32, not_a_constant)]
24#[gff_entry(Length, wire = f32, not_a_constant)]
25#[gff_entry(Width, wire = f32, not_a_constant)]
26pub struct GitAreaEffectShapeLabels {}
27
28/// The footprint an area effect covers, as one value across four labels.
29#[derive(Debug, Clone, PartialEq)]
30pub enum GitAreaEffectShape {
31    /// `Shape = 0`: a circle of `Radius`.
32    Circle {
33        /// Effect radius (`Radius`).
34        radius: f32,
35    },
36    /// `Shape = 1`: a rectangle of `Length` by `Width`.
37    Rectangle {
38        /// Effect length (`Length`).
39        length: f32,
40        /// Effect width (`Width`).
41        width: f32,
42    },
43    /// Any other `Shape` byte, which the loader treats as no geometry.
44    None(u8),
45}
46
47impl Default for GitAreaEffectShape {
48    fn default() -> Self {
49        Self::Circle { radius: 0.0 }
50    }
51}
52
53/// An area-of-effect object placed in the area (struct type 13).
54///
55/// These are runtime spell and ability effects. Unlike every other object
56/// list, they have no blueprint: no loader in the binary ever opens a
57/// template for one, so `UseTemplates` has nothing to select between and
58/// [`Git::area_effects`](super::Git::area_effects) is a plain [`Vec`] rather than a [`GitObjects`](super::GitObjects).
59///
60/// # Not exercised by the fixture corpus
61///
62/// Every save in `fixtures/saves/` has an empty `AreaEffectList`, so this
63/// field set comes from the engine's own load and save paths rather than from
64/// real data. A test asserts the lists are still empty, so a corpus that
65/// grows one fails loudly instead of quietly going unchecked.
66#[derive(Debug, Clone, PartialEq, GffModel)]
67pub struct GitAreaEffect {
68    /// The effect's footprint, as whichever shape the file describes.
69    #[gff(flatten = GitAreaEffectShapeLabels, manual_read, manual_write)]
70    pub shape: GitAreaEffectShape,
71    /// Object tag (`Tag`).
72    #[gff(Tag, stamped)]
73    pub tag: String,
74    /// Effect row (`AreaEffectId`), into `vfx_persistent.2da`.
75    #[gff(AreaEffectId, stamped)]
76    pub area_effect_id: i32,
77    /// Originating spell (`SpellId`).
78    #[gff(SpellId, stamped)]
79    pub spell_id: u32,
80    /// Save DC of the originating spell (`SpellSaveDC`).
81    #[gff(SpellSaveDC, stamped)]
82    pub spell_save_dc: i32,
83    /// Level of the originating spell (`SpellLevel`).
84    #[gff(SpellLevel, stamped)]
85    pub spell_level: i32,
86    /// Metamagic applied to the originating spell (`MetaMagicType`).
87    #[gff(MetaMagicType, stamped)]
88    pub meta_magic_type: u8,
89    /// Object that created the effect (`CreatorId`).
90    #[gff(CreatorId, stamped)]
91    pub creator_id: u32,
92    /// Object the effect is attached to (`LinkedToObject`).
93    #[gff(LinkedToObject, stamped)]
94    pub linked_to_object: u32,
95    /// Last object to enter the effect (`LastEntered`).
96    #[gff(LastEntered, stamped)]
97    pub last_entered: u32,
98    /// Last object to leave the effect (`LastLeft`).
99    #[gff(LastLeft, stamped)]
100    pub last_left: u32,
101    /// Remaining duration (`Duration`).
102    #[gff(Duration, stamped)]
103    pub duration: u32,
104    /// How the duration is counted (`DurationType`).
105    #[gff(DurationType, stamped)]
106    pub duration_type: u8,
107    /// Day of the last heartbeat (`LastHrtbtDay`).
108    #[gff(LastHrtbtDay, stamped)]
109    pub last_heartbeat_day: u32,
110    /// Time of the last heartbeat (`LastHrtbtTime`).
111    #[gff(LastHrtbtTime, stamped)]
112    pub last_heartbeat_time: u32,
113    /// `OnHeartbeat`. Written by the engine, never read back.
114    #[gff(
115        OnHeartbeat,
116        write_only_dead = "the save writer emits it and no loader reads it back, so a restored effect never fires this hook again; the 2DA-driven re-derivation exists only on the fresh spell-cast path",
117        not_a_constant
118    )]
119    pub on_heartbeat: ResRef,
120    /// `OnUserDefined`. Written by the engine, never read back.
121    #[gff(
122        OnUserDefined,
123        write_only_dead = "no path populates it, fresh creation included, so it is dead in this build regardless of load versus save",
124        not_a_constant
125    )]
126    pub on_user_defined: ResRef,
127    /// `OnObjEnter`. Written by the engine, never read back.
128    #[gff(
129        OnObjEnter,
130        write_only_dead = "the save writer emits it and no loader reads it back, so a restored effect never fires this hook again; the 2DA-driven re-derivation exists only on the fresh spell-cast path",
131        not_a_constant
132    )]
133    pub on_obj_enter: ResRef,
134    /// `OnObjExit`. Written by the engine, never read back.
135    #[gff(
136        OnObjExit,
137        write_only_dead = "the save writer emits it and no loader reads it back, so a restored effect never fires this hook again; the 2DA-driven re-derivation exists only on the fresh spell-cast path",
138        not_a_constant
139    )]
140    pub on_obj_exit: ResRef,
141    /// Runtime object id (`ObjectId`).
142    ///
143    /// Defaults to the engine-wide `OBJECT_INVALID` placeholder rather than
144    /// zero, which is what a freshly constructed object carries.
145    #[gff(ObjectId, stamped = ObjectId::INVALID)]
146    pub object_id: ObjectId,
147    /// X orientation (`OrientationX`).
148    #[gff(OrientationX, not_a_constant)]
149    pub orientation_x: f32,
150    /// Y orientation (`OrientationY`).
151    #[gff(OrientationY, not_a_constant)]
152    pub orientation_y: f32,
153    /// Z orientation (`OrientationZ`).
154    #[gff(OrientationZ, not_a_constant)]
155    pub orientation_z: f32,
156    /// X position (`PositionX`).
157    #[gff(PositionX, stamped)]
158    pub position_x: f32,
159    /// Y position (`PositionY`).
160    #[gff(PositionY, stamped)]
161    pub position_y: f32,
162    /// Z position (`PositionZ`).
163    #[gff(PositionZ, stamped)]
164    pub position_z: f32,
165}
166
167impl GitAreaEffect {
168    /// Reads one `AreaEffectList` element, resolving the shape's four labels.
169    pub fn read_element(structure: &GffStruct) -> Self {
170        let shape = match get_u8(structure, "Shape").unwrap_or(0) {
171            0 => GitAreaEffectShape::Circle {
172                radius: get_f32(structure, "Radius").unwrap_or(0.0),
173            },
174            1 => GitAreaEffectShape::Rectangle {
175                length: get_f32(structure, "Length").unwrap_or(0.0),
176                width: get_f32(structure, "Width").unwrap_or(0.0),
177            },
178            other => GitAreaEffectShape::None(other),
179        };
180        Self {
181            shape,
182            ..Self::read_declared(structure)
183        }
184    }
185
186    /// Writes one element, emitting only the dimensions its shape uses.
187    ///
188    /// A circle carrying a `Width` would be a field the engine never wrote.
189    pub fn write_element(&self, structure: &mut GffStruct) {
190        self.write_declared(structure);
191        match &self.shape {
192            GitAreaEffectShape::Circle { radius } => {
193                upsert_field(structure, gff_label!("Shape"), GffValue::UInt8(0));
194                upsert_field(structure, gff_label!("Radius"), GffValue::Single(*radius));
195            }
196            GitAreaEffectShape::Rectangle { length, width } => {
197                upsert_field(structure, gff_label!("Shape"), GffValue::UInt8(1));
198                upsert_field(structure, gff_label!("Length"), GffValue::Single(*length));
199                upsert_field(structure, gff_label!("Width"), GffValue::Single(*width));
200            }
201            GitAreaEffectShape::None(other) => {
202                upsert_field(structure, gff_label!("Shape"), GffValue::UInt8(*other));
203            }
204        }
205    }
206}