1use crate::{
2 combat::{
3 self, Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage,
4 DamageKind, DamageSource, GroupTarget,
5 },
6 comp::{
7 Body, CharacterState, Ori, StateUpdate, beam,
8 body::{biped_large, bird_large, golem},
9 character_state::OutputEvents,
10 object::Body::{Flamethrower, Lavathrower},
11 },
12 event::LocalEvent,
13 outcome::Outcome,
14 resources::Secs,
15 states::{
16 behavior::{CharacterBehavior, JoinData},
17 utils::*,
18 },
19 terrain::Block,
20 util::Dir,
21};
22use hashbrown::HashMap;
23use serde::{Deserialize, Serialize};
24use std::time::Duration;
25use vek::*;
26
27#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
29pub struct StaticData {
30 pub buildup_duration: Duration,
32 pub recover_duration: Duration,
34 pub beam_duration: Secs,
36 pub damage: f32,
38 pub tick_rate: f32,
40 pub range: f32,
42 pub end_radius: f32,
45 pub damage_effect: Option<CombatEffect>,
47 pub energy_regen: f32,
49 pub energy_drain: f32,
51 pub ori_rate: f32,
53 pub ability_info: AbilityInfo,
55 pub specifier: beam::FrontendSpecifier,
57}
58
59#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
60pub struct Data {
61 pub static_data: StaticData,
64 pub timer: Duration,
66 pub stage_section: StageSection,
68 pub aim_dir: Dir,
70 pub beam_offset: Vec3<f32>,
72}
73
74impl CharacterBehavior for Data {
75 fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate {
76 let mut update = StateUpdate::from(data);
77
78 let ori_rate = self.static_data.ori_rate;
79
80 handle_orientation(data, &mut update, ori_rate, None);
81 handle_move(data, &mut update, 0.4);
82 handle_jump(data, output_events, &mut update, 1.0);
83
84 let rel_vel = data.vel.0 - data.physics.ground_vel;
86 let body_offsets = beam_offsets(
88 data.body,
89 data.inputs.look_dir,
90 update.ori.look_vec(),
91 rel_vel,
92 data.physics.on_ground,
93 );
94
95 match self.stage_section {
96 StageSection::Buildup => {
97 if self.timer < self.static_data.buildup_duration {
98 update.character = CharacterState::BasicBeam(Data {
100 timer: tick_attack_or_default(data, self.timer, None),
101 ..*self
102 });
103 if matches!(data.body, Body::Object(Flamethrower | Lavathrower)) {
104 output_events.emit_local(LocalEvent::CreateOutcome(
106 Outcome::FlamethrowerCharge {
107 pos: data.pos.0 + *data.ori.look_dir() * (data.body.max_radius()),
108 },
109 ));
110 }
111 } else {
112 let attack = {
113 let energy = AttackEffect::new(
114 None,
115 CombatEffect::EnergyReward(self.static_data.energy_regen),
116 )
117 .with_requirement(CombatRequirement::AnyDamage);
118 let mut damage = AttackDamage::new(
119 Damage {
120 source: DamageSource::Energy,
121 kind: DamageKind::Energy,
122 value: self.static_data.damage,
123 },
124 Some(GroupTarget::OutOfGroup),
125 rand::random(),
126 );
127 if let Some(effect) = self.static_data.damage_effect {
128 damage = damage.with_effect(effect);
129 }
130 let precision_mult =
131 combat::compute_precision_mult(data.inventory, data.msm);
132 Attack::default()
133 .with_damage(damage)
134 .with_precision(precision_mult)
135 .with_effect(energy)
136 .with_combo_increment()
137 };
138
139 data.updater.insert(data.entity, beam::Beam {
141 attack,
142 end_radius: self.static_data.end_radius,
143 range: self.static_data.range,
144 duration: self.static_data.beam_duration,
145 tick_dur: Secs(1.0 / self.static_data.tick_rate as f64),
146 hit_entities: Vec::new(),
147 hit_durations: HashMap::new(),
148 specifier: self.static_data.specifier,
149 bezier: QuadraticBezier3 {
150 start: data.pos.0 + body_offsets,
151 ctrl: data.pos.0 + body_offsets,
152 end: data.pos.0 + body_offsets,
153 },
154 });
155 update.character = CharacterState::BasicBeam(Data {
157 beam_offset: body_offsets,
158 timer: Duration::default(),
159 stage_section: StageSection::Action,
160 ..*self
161 });
162 }
163 },
164 StageSection::Action => {
165 if input_is_pressed(data, self.static_data.ability_info.input)
166 && (self.static_data.energy_drain <= f32::EPSILON
167 || update.energy.current() > 0.0)
168 {
169 let beam_ori = {
170 let look_dir = data.inputs.look_dir;
184 let xy_dir = Dir::from_unnormalized(Vec3::new(look_dir.x, look_dir.y, 0.0))
185 .unwrap_or_default();
186 let pitch = xy_dir.rotation_between(look_dir);
187
188 Ori::from(Vec3::new(
189 update.ori.look_vec().x,
190 update.ori.look_vec().y,
191 0.0,
192 ))
193 .prerotated(pitch)
194 };
195
196 update.character = CharacterState::BasicBeam(Data {
197 beam_offset: body_offsets,
198 aim_dir: beam_ori.look_dir(),
199 timer: tick_attack_or_default(data, self.timer, None),
200 ..*self
201 });
202
203 update
205 .energy
206 .change_by(-self.static_data.energy_drain * data.dt.0);
207 } else {
208 update.character = CharacterState::BasicBeam(Data {
209 timer: Duration::default(),
210 stage_section: StageSection::Recover,
211 ..*self
212 });
213 }
214 },
215 StageSection::Recover => {
216 if self.timer < self.static_data.recover_duration {
217 update.character = CharacterState::BasicBeam(Data {
218 timer: tick_attack_or_default(
219 data,
220 self.timer,
221 Some(data.stats.recovery_speed_modifier),
222 ),
223 ..*self
224 });
225 } else {
226 end_ability(data, &mut update);
228 data.updater.remove::<beam::Beam>(data.entity);
230 }
231 },
232 _ => {
233 end_ability(data, &mut update);
235 data.updater.remove::<beam::Beam>(data.entity);
237 },
238 }
239
240 handle_interrupts(data, &mut update, output_events);
242
243 update
244 }
245}
246
247fn height_offset(body: &Body, look_dir: Dir, velocity: Vec3<f32>, on_ground: Option<Block>) -> f32 {
248 match body {
249 Body::BirdLarge(b) => {
251 let height_factor = match b.species {
252 bird_large::Species::Phoenix => 0.5,
253 bird_large::Species::Cockatrice => 0.4,
254 _ => 0.3,
255 };
256 body.height() * height_factor
257 + if on_ground.is_none() {
258 (2.0 - velocity.xy().magnitude() * 0.25).max(-1.0)
259 } else {
260 0.0
261 }
262 },
263 Body::Golem(b) => {
264 let height_factor = match b.species {
265 golem::Species::Mogwai => 0.4,
266 _ => 0.9,
267 };
268 const DIR_COEFF: f32 = 2.0;
269 body.height() * height_factor + look_dir.z * DIR_COEFF
270 },
271 Body::BipedLarge(b) => match b.species {
272 biped_large::Species::Mindflayer => body.height() * 0.6,
273 biped_large::Species::SeaBishop => body.height() * 0.4,
274 biped_large::Species::Cursekeeper => body.height() * 0.8,
275 _ => body.height() * 0.5,
276 },
277 _ => body.height() * 0.5,
278 }
279}
280
281pub fn beam_offsets(
282 body: &Body,
283 look_dir: Dir,
284 ori: Vec3<f32>,
285 velocity: Vec3<f32>,
286 on_ground: Option<Block>,
287) -> Vec3<f32> {
288 let dim = body.dimensions();
289 let (width, length) = (dim.x, dim.y);
291 let body_radius = if length > width {
292 body.max_radius()
294 } else {
295 body.min_radius()
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}