1use crate::{
2 Explosion, RadiusEffect,
3 combat::{self, Attack, AttackDamage, Damage, DamageKind::Crushing, 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, 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 .clone()
192 .custom_combo(strike_data.custom_combo)
193 .create_melee(
194 precision_mult,
195 tool_stats,
196 self.static_data.ability_info,
197 ),
198 );
199 } else if self.timer < strike_data.swing_duration {
200 if let CharacterState::ComboMelee2(c) = &mut update.character {
202 c.timer = tick_attack_or_default(data, self.timer, None);
203 }
204 if self.static_data.specifier == Some(FrontendSpecifier::IronGolemFist) {
206 let damage = AttackDamage::new(
207 Damage {
208 kind: Crushing,
209 value: 10.0,
210 },
211 Some(GroupTarget::OutOfGroup),
212 rand::random(),
213 );
214 let attack =
215 Attack::new(Some(self.static_data.ability_info)).with_damage(damage);
216 let explosion = Explosion {
217 effects: vec![RadiusEffect::Attack {
218 attack,
219 dodgeable: Dodgeable::Roll,
220 }],
221 radius: data.body.max_radius() * 10.0,
222 reagent: Some(Reagent::Yellow),
223 min_falloff: 0.5,
224 };
225 let pos =
226 data.pos.0 + (*data.ori.look_dir() * (data.body.max_radius() * 3.0));
227 let explosition =
228 Vec3::new(pos.x, pos.y, pos.z + (data.body.height() / 2.0));
229 output_events.emit_server(ExplosionEvent {
230 pos: explosition,
231 explosion,
232 owner: Some(*data.uid),
233 });
234 }
235 } else if self.start_next_strike {
236 if let CharacterState::ComboMelee2(c) = &mut update.character {
237 c.completed_strikes += 1;
238 }
239 next_strike(data, &mut update, strike_data);
240 } else {
241 if let CharacterState::ComboMelee2(c) = &mut update.character {
243 c.timer = Duration::default();
244 c.stage_section = StageSection::Recover;
245 c.movement_modifier = strike_data.movement_modifier.recover;
246 c.ori_modifier = strike_data.ori_modifier.recover;
247 }
248 }
249 },
250 StageSection::Recover => {
251 if let Some(movement) = strike_data.movement.recover {
252 handle_forced_movement(data, &mut update, movement);
253 }
254 if self.timer < strike_data.recover_duration {
255 if let CharacterState::ComboMelee2(c) = &mut update.character {
257 c.timer = tick_attack_or_default(
258 data,
259 self.timer,
260 Some(data.stats.recovery_speed_modifier),
261 );
262 }
263 } else {
264 end_melee_ability(data, &mut update);
266 }
267 },
268 _ => {
269 end_melee_ability(data, &mut update);
271 },
272 }
273
274 update
275 }
276}
277
278impl Data {
279 pub fn strike_data(&self) -> &Strike<Duration> {
280 &self.static_data.strikes[self.completed_strikes % self.static_data.strikes.len()]
281 }
282}
283
284fn next_strike(data: &JoinData, update: &mut StateUpdate, strike_data: &Strike<Duration>) {
285 let revert_to_wield = if let CharacterState::ComboMelee2(c) = &mut update.character {
286 if update
287 .energy
288 .try_change_by(-c.static_data.energy_cost_per_strike)
289 .is_ok()
290 {
291 c.exhausted = false;
292 c.start_next_strike = false;
293 c.timer = Duration::default();
294 c.stage_section = StageSection::Buildup;
295 c.movement_modifier = strike_data.movement_modifier.buildup;
296 c.ori_modifier = strike_data.ori_modifier.buildup;
297 false
298 } else {
299 true
300 }
301 } else {
302 false
303 };
304 if revert_to_wield {
305 end_melee_ability(data, update)
306 }
307}
308
309#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
310pub enum FrontendSpecifier {
311 ClayGolemDash,
312 IronGolemFist,
313}