1use crate::{
2 combat::{self, CombatEffect},
3 comp::{
4 CharacterState, LightEmitter, Pos, ProjectileConstructor, StateUpdate,
5 character_state::OutputEvents, item::ToolKind, slot::EquipSlot,
6 },
7 event::ThrowEvent,
8 states::{
9 behavior::{CharacterBehavior, JoinData},
10 utils::*,
11 },
12 util::Dir,
13};
14use serde::{Deserialize, Serialize};
15use std::time::Duration;
16
17#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
18pub enum ProjectileDir {
19 LookDir,
21 Upwards(f32),
24}
25
26#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
28pub struct StaticData {
29 pub buildup_duration: Duration,
31 pub charge_duration: Duration,
33 pub throw_duration: Duration,
35 pub recover_duration: Duration,
37 pub energy_drain: f32,
39 pub projectile: ProjectileConstructor,
41 pub projectile_light: Option<LightEmitter>,
42 pub projectile_dir: ProjectileDir,
43 pub initial_projectile_speed: f32,
44 pub scaled_projectile_speed: f32,
45 pub move_speed: f32,
47 pub ability_info: AbilityInfo,
49 pub damage_effect: Option<CombatEffect>,
51 pub equip_slot: EquipSlot,
53 pub item_hash: u64,
55 pub tool_kind: ToolKind,
58 pub hand_info: HandInfo,
61}
62
63#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
64pub struct Data {
65 pub static_data: StaticData,
68 pub timer: Duration,
70 pub stage_section: StageSection,
72 pub exhausted: bool,
74}
75
76impl Data {
77 pub fn charge_frac(&self) -> f32 {
79 if let StageSection::Charge = self.stage_section {
80 (self.timer.as_secs_f32() / self.static_data.charge_duration.as_secs_f32()).min(1.0)
81 } else {
82 0.0
83 }
84 }
85}
86
87impl CharacterBehavior for Data {
88 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
89 let mut update = StateUpdate::from(data);
90
91 handle_orientation(data, &mut update, 1.0, None);
92 handle_move(data, &mut update, self.static_data.move_speed);
93 handle_jump(data, output_events, &mut update, 1.0);
94
95 match self.stage_section {
96 StageSection::Buildup => {
97 if self.timer < self.static_data.buildup_duration {
98 update.character = CharacterState::Throw(Data {
100 timer: tick_attack_or_default(data, self.timer, None),
101 ..*self
102 });
103 } else {
104 update.character = CharacterState::Throw(Data {
106 timer: Duration::default(),
107 stage_section: StageSection::Charge,
108 ..*self
109 });
110 }
111 },
112 StageSection::Charge => {
113 if !input_is_pressed(data, self.static_data.ability_info.input)
114 && !self.exhausted
115 && data
116 .inventory
117 .and_then(|inv| inv.equipped(self.static_data.equip_slot))
118 .is_some_and(|item| item.item_hash() == self.static_data.item_hash)
119 {
120 let charge_frac = self.charge_frac();
121
122 let body_offsets = data
123 .body
124 .projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0));
125 let pos = Pos(data.pos.0 + body_offsets);
126
127 let dir = match self.static_data.projectile_dir {
128 ProjectileDir::LookDir => data.inputs.look_dir,
129 ProjectileDir::Upwards(interpolation_factor) => {
130 Dir::up().slerped_to(data.inputs.look_dir, interpolation_factor)
131 },
132 };
133
134 let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
135 let projectile = if self.static_data.projectile.scaled.is_some() {
136 self.static_data.projectile.handle_scaling(charge_frac)
137 } else {
138 self.static_data.projectile
139 };
140 let projectile = projectile.create_projectile(
141 Some(*data.uid),
142 precision_mult,
143 self.static_data.damage_effect,
144 );
145
146 output_events.emit_server(ThrowEvent {
149 entity: data.entity,
150 pos,
151 dir,
152 projectile,
153 light: self.static_data.projectile_light,
154 speed: self.static_data.initial_projectile_speed
155 + charge_frac * self.static_data.scaled_projectile_speed,
156 object: None,
157 equip_slot: self.static_data.equip_slot,
158 });
159
160 update.character = CharacterState::Throw(Data {
162 timer: Duration::default(),
163 stage_section: StageSection::Action,
164 exhausted: true,
165 ..*self
166 });
167 } else if self.timer < self.static_data.charge_duration
168 && input_is_pressed(data, self.static_data.ability_info.input)
169 {
170 update.character = CharacterState::Throw(Data {
172 timer: tick_attack_or_default(data, self.timer, None),
173 ..*self
174 });
175
176 update
178 .energy
179 .change_by(-self.static_data.energy_drain * data.dt.0);
180 } else if input_is_pressed(data, self.static_data.ability_info.input) {
181 update.character = CharacterState::Throw(Data {
183 timer: tick_attack_or_default(data, self.timer, None),
184 ..*self
185 });
186
187 update
189 .energy
190 .change_by(-self.static_data.energy_drain * data.dt.0 / 5.0);
191 }
192 },
193 StageSection::Action => {
194 if self.timer < self.static_data.throw_duration {
195 update.character = CharacterState::Throw(Data {
197 timer: tick_attack_or_default(data, self.timer, None),
198 ..*self
199 });
200 } else {
201 update.character = CharacterState::Throw(Data {
203 timer: Duration::default(),
204 stage_section: StageSection::Recover,
205 ..*self
206 });
207 }
208 },
209 StageSection::Recover => {
210 if self.timer < self.static_data.recover_duration {
211 update.character = CharacterState::Throw(Data {
213 timer: tick_attack_or_default(
214 data,
215 self.timer,
216 Some(data.stats.recovery_speed_modifier),
217 ),
218 ..*self
219 });
220 } else {
221 end_ability(data, &mut update);
223 }
224 },
225 _ => {
226 end_ability(data, &mut update);
228 },
229 }
230
231 handle_interrupts(data, &mut update, output_events);
233
234 update
235 }
236}