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 pub end_radius: f32,
51 pub damage_effect: Option<CombatEffect>,
53 pub energy_regen: f32,
55 pub energy_drain: f32,
57 pub ori_rate: f32,
59 pub move_efficiency: f32,
61 pub ability_info: AbilityInfo,
63 pub specifier: beam::FrontendSpecifier,
65}
66
67#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
68pub struct Data {
69 pub static_data: StaticData,
72 pub timer: Duration,
74 pub stage_section: StageSection,
76 pub aim_dir: Dir,
78 pub beam_offset: Vec3<f32>,
80}
81
82impl CharacterBehavior for Data {
83 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
84 let mut update = StateUpdate::from(data);
85
86 let ori_rate = self.static_data.ori_rate;
87
88 handle_orientation(data, &mut update, ori_rate, None);
89 handle_move(data, &mut update, self.static_data.move_efficiency);
90 handle_jump(data, output_events, &mut update, 1.0);
91
92 let rel_vel = data.vel.0 - data.physics.ground_vel;
94 let body_offsets = beam_offsets(
96 data.body,
97 data.inputs.look_dir,
98 update.ori.look_vec(),
99 rel_vel,
100 data.physics.on_ground,
101 );
102
103 match self.stage_section {
104 StageSection::Buildup => {
105 if self.timer < self.static_data.buildup_duration {
106 update.character = CharacterState::BasicBeam(Data {
108 timer: tick_attack_or_default(data, self.timer, None),
109 ..*self
110 });
111 if matches!(data.body, Body::Object(Flamethrower | Lavathrower)) {
112 output_events.emit_local(LocalEvent::CreateOutcome(
114 Outcome::FlamethrowerCharge {
115 pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
116 },
117 ));
118 }
119 } else {
120 let attack = {
121 let energy = AttackEffect::new(
122 None,
123 CombatEffect::EnergyReward(self.static_data.energy_regen),
124 )
125 .with_requirement(CombatRequirement::AnyDamage);
126 let mut damage = AttackDamage::new(
127 Damage {
128 source: DamageSource::Energy,
129 kind: DamageKind::Energy,
130 value: self.static_data.damage,
131 },
132 Some(GroupTarget::OutOfGroup),
133 rand::random(),
134 );
135 if let Some(effect) = self.static_data.damage_effect {
136 damage = damage.with_effect(effect);
137 }
138 let precision_mult =
139 combat::compute_precision_mult(data.inventory, data.msm);
140 Attack::default()
141 .with_damage(damage)
142 .with_precision(precision_mult)
143 .with_effect(energy)
144 .with_combo_increment()
145 };
146
147 data.updater.insert(data.entity, beam::Beam {
149 attack,
150 dodgeable: self.static_data.dodgeable,
151 start_radius: 0.0,
152 end_radius: self.static_data.end_radius,
153 range: self.static_data.range,
154 duration: self.static_data.beam_duration,
155 tick_dur: Secs(1.0 / self.static_data.tick_rate as f64),
156 hit_entities: Vec::new(),
157 hit_durations: HashMap::new(),
158 specifier: self.static_data.specifier,
159 bezier: QuadraticBezier3 {
160 start: data.pos.0 + body_offsets,
161 ctrl: data.pos.0 + body_offsets,
162 end: data.pos.0 + body_offsets,
163 },
164 });
165 update.character = CharacterState::BasicBeam(Data {
167 beam_offset: body_offsets,
168 timer: Duration::default(),
169 stage_section: StageSection::Action,
170 ..*self
171 });
172 }
173 },
174 StageSection::Action => {
175 if input_is_pressed(data, self.static_data.ability_info.input)
176 && (self.static_data.energy_drain <= f32::EPSILON
177 || update.energy.current() > 0.0)
178 {
179 let beam_dir = data.inputs.look_dir.merge_z(data.ori.look_dir());
183
184 update.character = CharacterState::BasicBeam(Data {
185 beam_offset: body_offsets,
186 aim_dir: beam_dir,
187 timer: tick_attack_or_default(data, self.timer, None),
188 ..*self
189 });
190
191 update
193 .energy
194 .change_by(-self.static_data.energy_drain * data.dt.0);
195 } else {
196 update.character = CharacterState::BasicBeam(Data {
197 timer: Duration::default(),
198 stage_section: StageSection::Recover,
199 ..*self
200 });
201 }
202 },
203 StageSection::Recover => {
204 if self.timer < self.static_data.recover_duration {
205 update.character = CharacterState::BasicBeam(Data {
206 timer: tick_attack_or_default(
207 data,
208 self.timer,
209 Some(data.stats.recovery_speed_modifier),
210 ),
211 ..*self
212 });
213 } else {
214 end_ability(data, &mut update);
216 data.updater.remove::<beam::Beam>(data.entity);
218 }
219 },
220 _ => {
221 end_ability(data, &mut update);
223 data.updater.remove::<beam::Beam>(data.entity);
225 },
226 }
227
228 handle_interrupts(data, &mut update, output_events);
230
231 update
232 }
233}
234
235fn height_offset(body: &Body, look_dir: Dir, velocity: Vec3<f32>, on_ground: Option<Block>) -> f32 {
236 match body {
237 Body::BirdLarge(b) => {
239 let height_factor = match b.species {
240 bird_large::Species::Phoenix => 0.5,
241 bird_large::Species::Cockatrice => 0.4,
242 _ => 0.3,
243 };
244 body.height() * height_factor
245 + if on_ground.is_none() {
246 (2.0 - velocity.xy().magnitude() * 0.25).max(-1.0)
247 } else {
248 0.0
249 }
250 },
251 Body::Golem(b) => {
252 let height_factor = match b.species {
253 golem::Species::Mogwai => 0.4,
254 _ => 0.9,
255 };
256 const DIR_COEFF: f32 = 2.0;
257 body.height() * height_factor + look_dir.z * DIR_COEFF
258 },
259 Body::BipedLarge(b) => match b.species {
260 biped_large::Species::Mindflayer => body.height() * 0.6,
261 biped_large::Species::SeaBishop => body.height() * 0.4,
262 biped_large::Species::Cursekeeper => body.height() * 0.8,
263 biped_large::Species::Gigasfire => body.height() * 0.18,
264 _ => body.height() * 0.5,
265 },
266 Body::QuadrupedMedium(b) => match b.species {
267 quadruped_medium::Species::Elephant => body.height() * 0.4,
268 _ => body.height() * 0.5,
269 },
270 _ => body.height() * 0.5,
271 }
272}
273
274pub fn beam_offsets(
275 body: &Body,
276 look_dir: Dir,
277 ori: Vec3<f32>,
278 velocity: Vec3<f32>,
279 on_ground: Option<Block>,
280) -> Vec3<f32> {
281 let dim = body.dimensions();
282 let (width, length) = (dim.x, dim.y);
284 let body_radius = match body {
285 Body::QuadrupedMedium(b) if matches!(b.species, quadruped_medium::Species::Elephant) => {
286 body.max_radius() * 1.4
287 },
288 _ => {
289 if length > width {
290 body.max_radius()
292 } else {
293 body.min_radius()
295 }
296 },
297 };
298 let body_offsets_z = height_offset(body, look_dir, velocity, on_ground);
299 Vec3::new(
300 body_radius * ori.x * 1.1,
301 body_radius * ori.y * 1.1,
302 body_offsets_z,
303 )
304}