1use crate::{
2 combat::{
3 self, Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage,
4 DamageKind, DamageSource, GroupTarget,
5 },
6 comp::{
7 Body, CharacterState, StateUpdate,
8 ability::Dodgeable,
9 beam,
10 body::{biped_large, bird_large, golem},
11 character_state::OutputEvents,
12 object::Body::{Flamethrower, Lavathrower},
13 quadruped_medium,
14 },
15 event::LocalEvent,
16 outcome::Outcome,
17 resources::Secs,
18 states::{
19 behavior::{CharacterBehavior, JoinData},
20 utils::*,
21 },
22 terrain::Block,
23 util::Dir,
24};
25use hashbrown::HashMap;
26use serde::{Deserialize, Serialize};
27use std::time::Duration;
28use vek::*;
29
30#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
32pub struct StaticData {
33 pub buildup_duration: Duration,
35 pub recover_duration: Duration,
37 pub beam_duration: Secs,
39 pub damage: f32,
41 pub tick_rate: f32,
43 pub range: f32,
45 #[serde(default)]
47 pub dodgeable: Dodgeable,
48 #[serde(default)]
50 pub blockable: bool,
51 pub end_radius: f32,
54 pub damage_effect: Option<CombatEffect>,
56 pub energy_regen: f32,
58 pub energy_drain: f32,
60 pub ori_rate: f32,
62 pub move_efficiency: f32,
64 pub ability_info: AbilityInfo,
66 pub specifier: beam::FrontendSpecifier,
68}
69
70#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
71pub struct Data {
72 pub static_data: StaticData,
75 pub timer: Duration,
77 pub stage_section: StageSection,
79 pub aim_dir: Dir,
81 pub beam_offset: Vec3<f32>,
83}
84
85impl CharacterBehavior for Data {
86 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
87 let mut update = StateUpdate::from(data);
88
89 let ori_rate = self.static_data.ori_rate;
90
91 handle_orientation(data, &mut update, ori_rate, None);
92 handle_move(data, &mut update, self.static_data.move_efficiency);
93 handle_jump(data, output_events, &mut update, 1.0);
94
95 let rel_vel = data.vel.0 - data.physics.ground_vel;
97 let body_offsets = beam_offsets(
99 data.body,
100 data.inputs.look_dir,
101 update.ori.look_vec(),
102 rel_vel,
103 data.physics.on_ground,
104 );
105
106 match self.stage_section {
107 StageSection::Buildup => {
108 if self.timer < self.static_data.buildup_duration {
109 update.character = CharacterState::BasicBeam(Data {
111 timer: tick_attack_or_default(data, self.timer, None),
112 ..*self
113 });
114 if matches!(data.body, Body::Object(Flamethrower | Lavathrower)) {
115 output_events.emit_local(LocalEvent::CreateOutcome(
117 Outcome::FlamethrowerCharge {
118 pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
119 },
120 ));
121 }
122 } else {
123 let attack = {
124 let energy = AttackEffect::new(
125 None,
126 CombatEffect::EnergyReward(self.static_data.energy_regen),
127 )
128 .with_requirement(CombatRequirement::AnyDamage);
129 let mut damage = AttackDamage::new(
130 Damage {
131 source: DamageSource::Energy,
132 kind: DamageKind::Energy,
133 value: self.static_data.damage,
134 },
135 Some(GroupTarget::OutOfGroup),
136 rand::random(),
137 );
138 if let Some(effect) = self.static_data.damage_effect {
139 damage = damage.with_effect(effect);
140 }
141 let precision_mult =
142 combat::compute_precision_mult(data.inventory, data.msm);
143 Attack::default()
144 .with_damage(damage)
145 .with_precision(precision_mult)
146 .with_blockable(self.static_data.blockable)
147 .with_effect(energy)
148 .with_combo_increment()
149 };
150
151 data.updater.insert(data.entity, beam::Beam {
153 attack,
154 dodgeable: self.static_data.dodgeable,
155 start_radius: 0.0,
156 end_radius: self.static_data.end_radius,
157 range: self.static_data.range,
158 duration: self.static_data.beam_duration,
159 tick_dur: Secs(1.0 / self.static_data.tick_rate as f64),
160 hit_entities: Vec::new(),
161 hit_durations: HashMap::new(),
162 specifier: self.static_data.specifier,
163 bezier: QuadraticBezier3 {
164 start: data.pos.0 + body_offsets,
165 ctrl: data.pos.0 + body_offsets,
166 end: data.pos.0 + body_offsets,
167 },
168 });
169 update.character = CharacterState::BasicBeam(Data {
171 beam_offset: body_offsets,
172 timer: Duration::default(),
173 stage_section: StageSection::Action,
174 ..*self
175 });
176 }
177 },
178 StageSection::Action => {
179 if input_is_pressed(data, self.static_data.ability_info.input)
180 && (self.static_data.energy_drain <= f32::EPSILON
181 || update.energy.current() > 0.0)
182 {
183 let beam_dir = data.inputs.look_dir.merge_z(data.ori.look_dir());
187
188 update.character = CharacterState::BasicBeam(Data {
189 beam_offset: body_offsets,
190 aim_dir: beam_dir,
191 timer: tick_attack_or_default(data, self.timer, None),
192 ..*self
193 });
194
195 update
197 .energy
198 .change_by(-self.static_data.energy_drain * data.dt.0);
199 } else {
200 update.character = CharacterState::BasicBeam(Data {
201 timer: Duration::default(),
202 stage_section: StageSection::Recover,
203 ..*self
204 });
205 }
206 },
207 StageSection::Recover => {
208 if self.timer < self.static_data.recover_duration {
209 update.character = CharacterState::BasicBeam(Data {
210 timer: tick_attack_or_default(
211 data,
212 self.timer,
213 Some(data.stats.recovery_speed_modifier),
214 ),
215 ..*self
216 });
217 } else {
218 end_ability(data, &mut update);
220 data.updater.remove::<beam::Beam>(data.entity);
222 }
223 },
224 _ => {
225 end_ability(data, &mut update);
227 data.updater.remove::<beam::Beam>(data.entity);
229 },
230 }
231
232 handle_interrupts(data, &mut update, output_events);
234
235 update
236 }
237}
238
239fn height_offset(body: &Body, look_dir: Dir, velocity: Vec3<f32>, on_ground: Option<Block>) -> f32 {
240 match body {
241 Body::BirdLarge(b) => {
243 let height_factor = match b.species {
244 bird_large::Species::Phoenix => 0.5,
245 bird_large::Species::Cockatrice => 0.4,
246 _ => 0.3,
247 };
248 body.height() * height_factor
249 + if on_ground.is_none() {
250 (2.0 - velocity.xy().magnitude() * 0.25).max(-1.0)
251 } else {
252 0.0
253 }
254 },
255 Body::Golem(b) => {
256 let height_factor = match b.species {
257 golem::Species::Mogwai => 0.4,
258 _ => 0.9,
259 };
260 const DIR_COEFF: f32 = 2.0;
261 body.height() * height_factor + look_dir.z * DIR_COEFF
262 },
263 Body::BipedLarge(b) => match b.species {
264 biped_large::Species::Mindflayer => body.height() * 0.6,
265 biped_large::Species::SeaBishop => body.height() * 0.4,
266 biped_large::Species::Cursekeeper => body.height() * 0.8,
267 biped_large::Species::Gigasfire => body.height() * 0.18,
268 _ => body.height() * 0.5,
269 },
270 Body::QuadrupedMedium(b) => match b.species {
271 quadruped_medium::Species::Elephant => body.height() * 0.4,
272 _ => body.height() * 0.5,
273 },
274 _ => body.height() * 0.5,
275 }
276}
277
278pub fn beam_offsets(
279 body: &Body,
280 look_dir: Dir,
281 ori: Vec3<f32>,
282 velocity: Vec3<f32>,
283 on_ground: Option<Block>,
284) -> Vec3<f32> {
285 let dim = body.dimensions();
286 let (width, length) = (dim.x, dim.y);
288 let body_radius = match body {
289 Body::QuadrupedMedium(b) if matches!(b.species, quadruped_medium::Species::Elephant) => {
290 body.max_radius() * 1.4
291 },
292 _ => {
293 if length > width {
294 body.max_radius()
296 } else {
297 body.min_radius()
299 }
300 },
301 };
302 let body_offsets_z = height_offset(body, look_dir, velocity, on_ground);
303 Vec3::new(
304 body_radius * ori.x * 1.1,
305 body_radius * ori.y * 1.1,
306 body_offsets_z,
307 )
308}