1use crate::{
2 Explosion, RadiusEffect, combat,
3 combat::{Attack, AttackDamage, Damage, DamageKind::Crushing, DamageSource, GroupTarget},
4 comp::{
5 CharacterState, MeleeConstructor, StateUpdate, character_state::OutputEvents,
6 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 pub ori_modifier: f32,
39 #[serde(default)]
40 pub custom_combo: CustomCombo,
41}
42
43impl Strike<f32> {
44 pub fn to_duration(self) -> Strike<Duration> {
45 Strike::<Duration> {
46 melee_constructor: self.melee_constructor,
47 buildup_duration: Duration::from_secs_f32(self.buildup_duration),
48 swing_duration: Duration::from_secs_f32(self.swing_duration),
49 hit_timing: self.hit_timing,
50 recover_duration: Duration::from_secs_f32(self.recover_duration),
51 movement: self.movement,
52 ori_modifier: self.ori_modifier,
53 custom_combo: self.custom_combo,
54 }
55 }
56
57 #[must_use]
58 pub fn adjusted_by_stats(self, stats: Stats) -> Self {
59 Self {
60 melee_constructor: self.melee_constructor.adjusted_by_stats(stats),
61 buildup_duration: self.buildup_duration / stats.speed,
62 swing_duration: self.swing_duration / stats.speed,
63 hit_timing: self.hit_timing,
64 recover_duration: self.recover_duration / stats.speed,
65 movement: self.movement,
66 ori_modifier: self.ori_modifier,
67 custom_combo: self.custom_combo,
68 }
69 }
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
73#[serde(deny_unknown_fields)]
74pub struct StrikeMovement {
75 pub buildup: Option<ForcedMovement>,
76 pub swing: Option<ForcedMovement>,
77 pub recover: Option<ForcedMovement>,
78}
79
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
83pub struct StaticData {
85 pub strikes: Vec<Strike<Duration>>,
87 pub energy_cost_per_strike: f32,
89 pub specifier: Option<combo_melee2::FrontendSpecifier>,
91 pub auto_progress: bool,
94 pub ability_info: AbilityInfo,
95}
96#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
99pub struct Data {
100 pub static_data: StaticData,
103 pub exhausted: bool,
105 pub start_next_strike: bool,
107 pub timer: Duration,
109 pub stage_section: StageSection,
112 pub completed_strikes: usize,
115}
116
117impl CharacterBehavior for Data {
118 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
119 let mut update = StateUpdate::from(data);
120
121 handle_orientation(data, &mut update, 1.0, None);
122 handle_move(data, &mut update, 0.7);
123 handle_interrupts(data, &mut update, output_events);
124
125 let strike_data = self.strike_data();
126
127 match self.stage_section {
128 StageSection::Buildup => {
129 if let Some(movement) = strike_data.movement.buildup {
130 handle_forced_movement(data, &mut update, movement);
131 }
132 if self.timer < strike_data.buildup_duration {
133 if let CharacterState::ComboMelee2(c) = &mut update.character {
135 c.timer = tick_attack_or_default(data, self.timer, None);
136 }
137 } else {
138 if let CharacterState::ComboMelee2(c) = &mut update.character {
140 c.timer = Duration::default();
141 c.stage_section = StageSection::Action;
142 }
143 }
144 if let Some(FrontendSpecifier::ClayGolemDash) = self.static_data.specifier {
145 output_events.emit_local(LocalEvent::CreateOutcome(Outcome::ClayGolemDash {
147 pos: data.pos.0,
148 }));
149 }
150 },
151 StageSection::Action => {
152 if let Some(movement) = strike_data.movement.swing {
153 handle_forced_movement(data, &mut update, movement);
154 }
155 if input_is_pressed(data, self.static_data.ability_info.input)
156 || self.static_data.auto_progress
157 {
158 if let CharacterState::ComboMelee2(c) = &mut update.character {
159 c.start_next_strike =
162 (c.completed_strikes + 1) < c.static_data.strikes.len();
163 }
164 }
165 if self.timer.as_secs_f32()
166 > strike_data.hit_timing * strike_data.swing_duration.as_secs_f32()
167 && !self.exhausted
168 {
169 if let CharacterState::ComboMelee2(c) = &mut update.character {
170 c.timer = tick_attack_or_default(data, self.timer, None);
171 c.exhausted = true;
172 }
173
174 let precision_mult = combat::compute_precision_mult(data.inventory, data.msm);
175 let tool_stats = get_tool_stats(data, self.static_data.ability_info);
176
177 data.updater.insert(
178 data.entity,
179 strike_data
180 .melee_constructor
181 .custom_combo(strike_data.custom_combo)
182 .create_melee(precision_mult, tool_stats),
183 );
184 } else if self.timer < strike_data.swing_duration {
185 if let CharacterState::ComboMelee2(c) = &mut update.character {
187 c.timer = tick_attack_or_default(data, self.timer, None);
188 }
189 if self.static_data.specifier == Some(FrontendSpecifier::IronGolemFist) {
190 let damage = AttackDamage::new(
191 Damage {
192 source: DamageSource::Explosion,
193 kind: Crushing,
194 value: 10.0,
195 },
196 Some(GroupTarget::OutOfGroup),
197 rand::random(),
198 );
199 let attack = Attack::default().with_damage(damage);
200 let explosion = Explosion {
201 effects: vec![RadiusEffect::Attack(attack)],
202 radius: data.body.max_radius() * 10.0,
203 reagent: Some(Reagent::Yellow),
204 min_falloff: 0.5,
205 };
206 let pos =
207 data.pos.0 + (*data.ori.look_dir() * (data.body.max_radius() * 3.0));
208 let explosition =
209 Vec3::new(pos.x, pos.y, pos.z + (data.body.height() / 2.0));
210 output_events.emit_server(ExplosionEvent {
211 pos: explosition,
212 explosion,
213 owner: Some(*data.uid),
214 });
215 }
216 } else if self.start_next_strike {
217 if let CharacterState::ComboMelee2(c) = &mut update.character {
218 c.completed_strikes += 1;
219 }
220 next_strike(data, &mut update);
221 } else {
222 if let CharacterState::ComboMelee2(c) = &mut update.character {
224 c.timer = Duration::default();
225 c.stage_section = StageSection::Recover;
226 }
227 }
228 },
229 StageSection::Recover => {
230 if let Some(movement) = strike_data.movement.recover {
231 handle_forced_movement(data, &mut update, movement);
232 }
233 if self.timer < strike_data.recover_duration {
234 if let CharacterState::ComboMelee2(c) = &mut update.character {
236 c.timer = tick_attack_or_default(
237 data,
238 self.timer,
239 Some(data.stats.recovery_speed_modifier),
240 );
241 }
242 } else {
243 end_melee_ability(data, &mut update);
245 }
246 },
247 _ => {
248 end_melee_ability(data, &mut update);
250 },
251 }
252
253 update
254 }
255}
256
257impl Data {
258 pub fn strike_data(&self) -> &Strike<Duration> {
259 &self.static_data.strikes[self.completed_strikes % self.static_data.strikes.len()]
260 }
261}
262
263fn next_strike(data: &JoinData, update: &mut StateUpdate) {
264 let revert_to_wield = if let CharacterState::ComboMelee2(c) = &mut update.character {
265 if update
266 .energy
267 .try_change_by(-c.static_data.energy_cost_per_strike)
268 .is_ok()
269 {
270 c.exhausted = false;
271 c.start_next_strike = false;
272 c.timer = Duration::default();
273 c.stage_section = StageSection::Buildup;
274 false
275 } else {
276 true
277 }
278 } else {
279 false
280 };
281 if revert_to_wield {
282 end_melee_ability(data, update)
283 }
284}
285
286#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
287pub enum FrontendSpecifier {
288 ClayGolemDash,
289 IronGolemFist,
290}