1use crate::{
2 Explosion, RadiusEffect,
3 combat::{self, Attack, AttackDamage, Damage, DamageKind::Crushing, DamageSource, GroupTarget},
4 comp::{
5 CharacterState, MeleeConstructor, StateUpdate, ability::Dodgeable,
6 character_state::OutputEvents, item::Reagent, melee::CustomCombo, tool::Stats,
7 },
8 event::{ExplosionEvent, LocalEvent},
9 outcome::Outcome,
10 states::{
11 behavior::{CharacterBehavior, JoinData},
12 combo_melee2,
13 utils::*,
14 },
15};
16use serde::{Deserialize, Serialize};
17use std::time::Duration;
18use vek::*;
19
20#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct Strike<T> {
23 pub melee_constructor: MeleeConstructor,
25 pub buildup_duration: T,
27 pub swing_duration: T,
30 pub hit_timing: f32,
32 pub recover_duration: T,
34 #[serde(default)]
36 pub movement: StrikeMovement,
37 #[serde(default)]
39 pub movement_modifier: MovementModifier,
40 #[serde(default)]
42 pub ori_modifier: OrientationModifier,
43 #[serde(default)]
44 pub custom_combo: CustomCombo,
45}
46
47impl Strike<f32> {
48 pub fn to_duration(self) -> Strike<Duration> {
49 Strike::<Duration> {
50 melee_constructor: self.melee_constructor,
51 buildup_duration: Duration::from_secs_f32(self.buildup_duration),
52 swing_duration: Duration::from_secs_f32(self.swing_duration),
53 hit_timing: self.hit_timing,
54 recover_duration: Duration::from_secs_f32(self.recover_duration),
55 movement: self.movement,
56 movement_modifier: self.movement_modifier,
57 ori_modifier: self.ori_modifier,
58 custom_combo: self.custom_combo,
59 }
60 }
61
62 #[must_use]
63 pub fn adjusted_by_stats(self, stats: Stats) -> Self {
64 Self {
65 melee_constructor: self.melee_constructor.adjusted_by_stats(stats),
66 buildup_duration: self.buildup_duration / stats.speed,
67 swing_duration: self.swing_duration / stats.speed,
68 hit_timing: self.hit_timing,
69 recover_duration: self.recover_duration / stats.speed,
70 movement: self.movement,
71 movement_modifier: self.movement_modifier,
72 ori_modifier: self.ori_modifier,
73 custom_combo: self.custom_combo,
74 }
75 }
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
79#[serde(deny_unknown_fields)]
80pub struct StrikeMovement {
81 pub buildup: Option<ForcedMovement>,
82 pub swing: Option<ForcedMovement>,
83 pub recover: Option<ForcedMovement>,
84}
85
86#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
89pub struct StaticData {
91 pub strikes: Vec<Strike<Duration>>,
93 pub energy_cost_per_strike: f32,
95 pub specifier: Option<combo_melee2::FrontendSpecifier>,
97 pub auto_progress: bool,
100 pub ability_info: AbilityInfo,
101}
102#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
105pub struct Data {
106 pub static_data: StaticData,
109 pub exhausted: bool,
111 pub start_next_strike: bool,
113 pub timer: Duration,
115 pub stage_section: StageSection,
118 pub completed_strikes: usize,
121 pub movement_modifier: Option<f32>,
123 pub ori_modifier: Option<f32>,
125}
126
127impl CharacterBehavior for Data {
128 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
129 let mut update = StateUpdate::from(data);
130
131 handle_orientation(data, &mut update, self.ori_modifier.unwrap_or(1.0), None);
132 handle_move(data, &mut update, self.movement_modifier.unwrap_or(0.7));
133 handle_interrupts(data, &mut update, output_events);
134
135 let strike_data = self.strike_data();
136
137 match self.stage_section {
138 StageSection::Buildup => {
139 if let Some(movement) = strike_data.movement.buildup {
140 handle_forced_movement(data, &mut update, movement);
141 }
142 if self.timer < strike_data.buildup_duration {
143 if let CharacterState::ComboMelee2(c) = &mut update.character {
145 c.timer = tick_attack_or_default(data, self.timer, None);
146 }
147 } else {
148 if let CharacterState::ComboMelee2(c) = &mut update.character {
150 c.timer = Duration::default();
151 c.stage_section = StageSection::Action;
152 c.movement_modifier = strike_data.movement_modifier.swing;
153 c.ori_modifier = strike_data.ori_modifier.swing;
154 }
155 }
156 if let Some(FrontendSpecifier::ClayGolemDash) = self.static_data.specifier {
157 output_events.emit_local(LocalEvent::CreateOutcome(Outcome::ClayGolemDash {
159 pos: data.pos.0,
160 }));
161 }
162 },
163 StageSection::Action => {
164 if let Some(movement) = strike_data.movement.swing {
165 handle_forced_movement(data, &mut update, movement);
166 }
167 if (input_is_pressed(data, self.static_data.ability_info.input)
168 || self.static_data.auto_progress)
169 && let CharacterState::ComboMelee2(c) = &mut update.character
170 {
171 c.start_next_strike = (c.completed_strikes + 1) < c.static_data.strikes.len();
174 }
175 if self.timer.as_secs_f32()
176 > strike_data.hit_timing * strike_data.swing_duration.as_secs_f32()
177 && !self.exhausted
178 {
179 if let CharacterState::ComboMelee2(c) = &mut update.character {
180 c.timer = tick_attack_or_default(data, self.timer, None);
181 c.exhausted = true;
182 }
183
184 let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
185 let tool_stats = get_tool_stats(data, self.static_data.ability_info);
186
187 data.updater.insert(
188 data.entity,
189 strike_data
190 .melee_constructor
191 .custom_combo(strike_data.custom_combo)
192 .create_melee(precision_mult, tool_stats),
193 );
194 } else if self.timer < strike_data.swing_duration {
195 if let CharacterState::ComboMelee2(c) = &mut update.character {
197 c.timer = tick_attack_or_default(data, self.timer, None);
198 }
199 if self.static_data.specifier == Some(FrontendSpecifier::IronGolemFist) {
200 let damage = AttackDamage::new(
201 Damage {
202 source: DamageSource::Explosion,
203 kind: Crushing,
204 value: 10.0,
205 },
206 Some(GroupTarget::OutOfGroup),
207 rand::random(),
208 );
209 let attack = Attack::default().with_damage(damage);
210 let explosion = Explosion {
211 effects: vec![RadiusEffect::Attack {
212 attack,
213 dodgeable: Dodgeable::Roll,
214 }],
215 radius: data.body.max_radius() * 10.0,
216 reagent: Some(Reagent::Yellow),
217 min_falloff: 0.5,
218 };
219 let pos =
220 data.pos.0 + (*data.ori.look_dir() * (data.body.max_radius() * 3.0));
221 let explosition =
222 Vec3::new(pos.x, pos.y, pos.z + (data.body.height() / 2.0));
223 output_events.emit_server(ExplosionEvent {
224 pos: explosition,
225 explosion,
226 owner: Some(*data.uid),
227 });
228 }
229 } else if self.start_next_strike {
230 if let CharacterState::ComboMelee2(c) = &mut update.character {
231 c.completed_strikes += 1;
232 }
233 next_strike(data, &mut update, strike_data);
234 } else {
235 if let CharacterState::ComboMelee2(c) = &mut update.character {
237 c.timer = Duration::default();
238 c.stage_section = StageSection::Recover;
239 c.movement_modifier = strike_data.movement_modifier.recover;
240 c.ori_modifier = strike_data.ori_modifier.recover;
241 }
242 }
243 },
244 StageSection::Recover => {
245 if let Some(movement) = strike_data.movement.recover {
246 handle_forced_movement(data, &mut update, movement);
247 }
248 if self.timer < strike_data.recover_duration {
249 if let CharacterState::ComboMelee2(c) = &mut update.character {
251 c.timer = tick_attack_or_default(
252 data,
253 self.timer,
254 Some(data.stats.recovery_speed_modifier),
255 );
256 }
257 } else {
258 end_melee_ability(data, &mut update);
260 }
261 },
262 _ => {
263 end_melee_ability(data, &mut update);
265 },
266 }
267
268 update
269 }
270}
271
272impl Data {
273 pub fn strike_data(&self) -> &Strike<Duration> {
274 &self.static_data.strikes[self.completed_strikes % self.static_data.strikes.len()]
275 }
276}
277
278fn next_strike(data: &JoinData, update: &mut StateUpdate, strike_data: &Strike<Duration>) {
279 let revert_to_wield = if let CharacterState::ComboMelee2(c) = &mut update.character {
280 if update
281 .energy
282 .try_change_by(-c.static_data.energy_cost_per_strike)
283 .is_ok()
284 {
285 c.exhausted = false;
286 c.start_next_strike = false;
287 c.timer = Duration::default();
288 c.stage_section = StageSection::Buildup;
289 c.movement_modifier = strike_data.movement_modifier.buildup;
290 c.ori_modifier = strike_data.ori_modifier.buildup;
291 false
292 } else {
293 true
294 }
295 } else {
296 false
297 };
298 if revert_to_wield {
299 end_melee_ability(data, update)
300 }
301}
302
303#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
304pub enum FrontendSpecifier {
305 ClayGolemDash,
306 IronGolemFist,
307}