1use crate::geom::{px, LocalBox};
14use crate::input::Btn;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
17pub enum MoveId {
18 StP,
20 StK,
21 StS,
22 StHS,
23 StHSClose,
24 StFL,
25 StST,
26 CrP,
28 CrK,
29 CrS,
30 CrHS,
31 CrFL,
32 CrST,
33 JP,
35 JK,
36 JS,
37 JHS,
38 JFL,
39 JST,
40 Overhead,
42 Throw,
44 Rekka1,
46 Rekka2,
47 Rekka3,
48 Uppercut,
50 CommandGrab,
52 CommandDash,
54 ShotA,
56 ShotB,
58 Guard,
60 Detonate,
62 SpecialOverhead,
64 AirShot,
66 Charge,
68 ExA,
70 ExB,
72 Super,
74}
75
76impl MoveId {
77 pub const ALL: [MoveId; 37] = [
78 MoveId::StP,
79 MoveId::StK,
80 MoveId::StS,
81 MoveId::StHS,
82 MoveId::StHSClose,
83 MoveId::StFL,
84 MoveId::StST,
85 MoveId::CrP,
86 MoveId::CrK,
87 MoveId::CrS,
88 MoveId::CrHS,
89 MoveId::CrFL,
90 MoveId::CrST,
91 MoveId::JP,
92 MoveId::JK,
93 MoveId::JS,
94 MoveId::JHS,
95 MoveId::JFL,
96 MoveId::JST,
97 MoveId::Overhead,
98 MoveId::Throw,
99 MoveId::Rekka1,
100 MoveId::Rekka2,
101 MoveId::Rekka3,
102 MoveId::Uppercut,
103 MoveId::CommandGrab,
104 MoveId::CommandDash,
105 MoveId::ShotA,
106 MoveId::ShotB,
107 MoveId::Guard,
108 MoveId::Detonate,
109 MoveId::SpecialOverhead,
110 MoveId::AirShot,
111 MoveId::Charge,
112 MoveId::ExA,
113 MoveId::ExB,
114 MoveId::Super,
115 ];
116
117 pub fn slot_name(self) -> &'static str {
119 match self {
120 Self::StP => "5P",
121 Self::StK => "5K",
122 Self::StS => "5S",
123 Self::StHS => "5HS",
124 Self::StHSClose => "c.HS",
125 Self::StFL => "5FL",
126 Self::StST => "5ST",
127 Self::CrP => "2P",
128 Self::CrK => "2K",
129 Self::CrS => "2S",
130 Self::CrHS => "2HS",
131 Self::CrFL => "2FL",
132 Self::CrST => "2ST",
133 Self::JP => "j.P",
134 Self::JK => "j.K",
135 Self::JS => "j.S",
136 Self::JHS => "j.HS",
137 Self::JFL => "j.FL",
138 Self::JST => "j.ST",
139 Self::Overhead => "overhead",
140 Self::Throw => "throw",
141 Self::Rekka1 => "rekka 1",
142 Self::Rekka2 => "rekka 2",
143 Self::Rekka3 => "rekka 3",
144 Self::Uppercut => "uppercut",
145 Self::CommandGrab => "command grab",
146 Self::CommandDash => "command dash",
147 Self::ShotA => "shot A",
148 Self::ShotB => "shot B",
149 Self::Guard => "guard",
150 Self::Detonate => "detonate",
151 Self::SpecialOverhead => "special overhead",
152 Self::AirShot => "air shot",
153 Self::Charge => "charge",
154 Self::ExA => "EX A",
155 Self::ExB => "EX B",
156 Self::Super => "super",
157 }
158 }
159
160 pub fn is_normal(self) -> bool {
161 matches!(
162 self,
163 Self::StP
164 | Self::StK
165 | Self::StS
166 | Self::StHS
167 | Self::StHSClose
168 | Self::StFL
169 | Self::StST
170 | Self::CrP
171 | Self::CrK
172 | Self::CrS
173 | Self::CrHS
174 | Self::CrFL
175 | Self::CrST
176 | Self::JP
177 | Self::JK
178 | Self::JS
179 | Self::JHS
180 | Self::JFL
181 | Self::JST
182 | Self::Overhead
183 )
184 }
185
186 pub fn is_crouching(self) -> bool {
187 matches!(
188 self,
189 Self::CrP | Self::CrK | Self::CrS | Self::CrHS | Self::CrFL | Self::CrST
190 )
191 }
192
193 pub fn is_jumping(self) -> bool {
194 matches!(
195 self,
196 Self::JP | Self::JK | Self::JS | Self::JHS | Self::JFL | Self::JST | Self::AirShot
197 )
198 }
199
200 pub fn is_rekka(self) -> bool {
201 matches!(self, Self::Rekka1 | Self::Rekka2 | Self::Rekka3)
202 }
203
204 pub fn is_ex(self) -> bool {
205 matches!(self, Self::ExA | Self::ExB)
206 }
207
208 pub fn is_super(self) -> bool {
209 self == Self::Super
210 }
211
212 pub fn is_special(self) -> bool {
213 !self.is_normal() && self != Self::Throw
214 }
215}
216
217#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
218pub enum HitLevel {
219 High,
221 Mid,
223 Low,
225}
226
227#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
228pub enum CancelRule {
229 Never,
230 OnHit,
232 OnHitOrBlock,
234}
235
236#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
237pub enum ThrowKind {
238 None,
239 Normal,
241 Command,
243}
244
245#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
246pub struct Invuln {
247 pub start: u8,
248 pub end: u8,
249 pub strike: bool,
250 pub throw: bool,
251}
252
253impl Invuln {
254 pub const NONE: Self = Self {
255 start: 0,
256 end: 0,
257 strike: false,
258 throw: false,
259 };
260
261 pub const fn full(start: u8, end: u8) -> Self {
263 Self {
264 start,
265 end,
266 strike: true,
267 throw: true,
268 }
269 }
270
271 pub fn strike_on(self, frame: u16) -> bool {
272 self.strike && frame >= self.start as u16 && frame < self.end as u16
273 }
274
275 pub fn throw_on(self, frame: u16) -> bool {
276 self.throw && frame >= self.start as u16 && frame < self.end as u16
277 }
278}
279
280#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
281pub struct TimedBox {
282 pub start: u8,
283 pub end: u8,
284 pub hit: LocalBox,
285}
286
287impl TimedBox {
288 pub const fn span(start: u8, end: u8, hit: LocalBox) -> Self {
289 Self { start, end, hit }
290 }
291}
292
293#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
297pub struct Followup {
298 pub button: Btn,
299 pub next: MoveId,
300 pub from: u8,
301 pub to: u8,
302}
303
304#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
307pub struct ChannelDef {
308 pub button: Btn,
309 pub max_frames: u16,
310 pub gauge_per_frame: i32,
311}
312
313#[derive(Clone, Debug)]
314pub struct MoveDef {
315 pub id: MoveId,
316 pub startup: u8,
317 pub active: u8,
318 pub recovery: u8,
319 pub damage: i32,
320 pub chip: i32,
321 pub hitstun: u8,
322 pub blockstun: u8,
323 pub hitstop: u8,
324 pub blockstop: u8,
325 pub pushback_hit: i32,
326 pub pushback_block: i32,
327 pub level: HitLevel,
328 pub knockdown: bool,
329 pub launch: i32,
330 pub cancel: CancelRule,
331 pub invuln: Invuln,
332 pub hitboxes: &'static [TimedBox],
333 pub vel_x: i32,
335 pub vel_frames: u8,
337 pub vel_y: i32,
338 pub gravity_override: Option<i32>,
339 pub land_recovery: u8,
341 pub projectile: Option<ProjectileDef>,
342 pub projectile_guard: bool,
344 pub meter_on_hit: i32,
345 pub meter_cost: i32,
346 pub gauge_cost: i32,
348 pub throw: ThrowKind,
349 pub followups: &'static [Followup],
350 pub feintable: bool,
352 pub pass_through: bool,
354 pub channel: Option<ChannelDef>,
355}
356
357impl MoveDef {
358 pub fn total_frames(&self) -> u16 {
359 self.startup as u16 + self.active as u16 + self.recovery as u16
360 }
361
362 pub fn first_active(&self) -> u16 {
363 self.startup as u16
364 }
365
366 pub fn last_active(&self) -> u16 {
367 self.startup as u16 + self.active as u16
368 }
369
370 pub fn is_active(&self, frame: u16) -> bool {
371 frame >= self.first_active() && frame < self.last_active()
372 }
373
374 pub fn in_startup(&self, frame: u16) -> bool {
375 frame < self.first_active()
376 }
377
378 pub fn in_recovery(&self, frame: u16) -> bool {
379 frame >= self.last_active() && frame < self.total_frames()
380 }
381
382 pub fn finished(&self, frame: u16) -> bool {
383 frame >= self.total_frames()
384 }
385
386 pub fn advantage_on_hit(&self) -> i32 {
388 self.hitstun as i32 - (self.active as i32 - 1) - self.recovery as i32
389 }
390
391 pub fn advantage_on_block(&self) -> i32 {
392 self.blockstun as i32 - (self.active as i32 - 1) - self.recovery as i32
393 }
394
395 pub fn hitboxes_on(&self, frame: u16) -> impl Iterator<Item = LocalBox> + '_ {
396 self.hitboxes
397 .iter()
398 .filter(move |tb| frame >= tb.start as u16 && frame < tb.end as u16)
399 .map(|tb| tb.hit)
400 }
401
402 pub fn followup_for(&self, button: Btn, frame: u16) -> Option<MoveId> {
403 self.followups
404 .iter()
405 .find(|f| f.button == button && frame >= f.from as u16 && frame < f.to as u16)
406 .map(|f| f.next)
407 }
408
409 pub fn is_throw(&self) -> bool {
410 self.throw != ThrowKind::None
411 }
412
413 pub fn special(id: MoveId) -> Self {
415 Self {
416 id,
417 startup: 0,
418 active: 0,
419 recovery: 0,
420 damage: 0,
421 chip: 0,
422 hitstun: 0,
423 blockstun: 0,
424 hitstop: 8,
425 blockstop: 6,
426 pushback_hit: px(10),
427 pushback_block: px(8),
428 level: HitLevel::Mid,
429 knockdown: false,
430 launch: 0,
431 cancel: CancelRule::Never,
432 invuln: Invuln::NONE,
433 hitboxes: &[],
434 vel_x: 0,
435 vel_frames: 0,
436 vel_y: 0,
437 gravity_override: None,
438 land_recovery: 0,
439 projectile: None,
440 projectile_guard: false,
441 meter_on_hit: 0,
442 meter_cost: 0,
443 gauge_cost: 0,
444 throw: ThrowKind::None,
445 followups: &[],
446 feintable: !id.is_super() && id != MoveId::Throw,
447 pass_through: false,
448 channel: None,
449 }
450 }
451}
452
453#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
455pub enum ShotBehavior {
456 Travel,
458 Hang,
460 Plant { arm_after: u16, armed_life: u16 },
463}
464
465#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
466pub struct ProjectileDef {
467 pub kind: ProjectileKind,
468 pub behavior: ShotBehavior,
469 pub spawn: LocalBox,
471 pub vel_x: i32,
472 pub vel_y: i32,
473 pub gravity: i32,
474 pub lifetime: u16,
475 pub damage: i32,
476 pub chip: i32,
477 pub hitstun: u8,
478 pub blockstun: u8,
479 pub hitstop: u8,
480 pub pushback: i32,
481 pub hitbox: LocalBox,
482 pub blast: Option<LocalBox>,
484 pub level: HitLevel,
485 pub knockdown: bool,
486}
487
488#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
489pub enum ProjectileKind {
490 Revolver,
491 Wave,
492 AirShot,
493 Glyph,
494 Crystal,
495}
496
497impl ProjectileKind {
498 pub fn clash_class(self) -> ProjectileClass {
499 match self {
500 Self::Revolver | Self::AirShot | Self::Glyph => ProjectileClass::Light,
501 Self::Wave | Self::Crystal => ProjectileClass::Heavy,
502 }
503 }
504
505 pub fn name(self) -> &'static str {
506 match self {
507 Self::Revolver => "revolver",
508 Self::Wave => "wave",
509 Self::AirShot => "air gun",
510 Self::Glyph => "glyph",
511 Self::Crystal => "crystal",
512 }
513 }
514}
515
516#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
517pub enum ProjectileClass {
518 Light,
519 Heavy,
520}
521
522fn button_class(id: MoveId) -> (u8, i32, i32) {
524 match id {
525 MoveId::StP | MoveId::StK | MoveId::CrP | MoveId::CrK | MoveId::JP | MoveId::JK => {
526 (7, px(6), px(8))
527 }
528 MoveId::StS | MoveId::StFL | MoveId::CrS | MoveId::CrFL | MoveId::JS | MoveId::JFL => {
529 (8, px(12), px(10))
530 }
531 _ => (10, px(14), px(10)),
532 }
533}
534
535#[allow(clippy::too_many_arguments)]
537pub fn normal(
538 id: MoveId,
539 startup: u8,
540 active: u8,
541 recovery: u8,
542 damage: i32,
543 hitstun: u8,
544 blockstun: u8,
545 level: HitLevel,
546 cancel: CancelRule,
547 hit: LocalBox,
548) -> MoveDef {
549 let (hitstop, push_hit, push_block) = button_class(id);
550 let mut m = MoveDef::special(id);
551 m.startup = startup;
552 m.active = active;
553 m.recovery = recovery;
554 m.damage = damage;
555 m.hitstun = hitstun;
556 m.blockstun = blockstun;
557 m.hitstop = hitstop;
558 m.blockstop = hitstop.saturating_sub(2);
559 m.pushback_hit = push_hit;
560 m.pushback_block = push_block;
561 m.level = level;
562 m.knockdown = id == MoveId::CrST;
563 m.cancel = cancel;
564 m.hitboxes = leak_box(startup, startup + active, hit);
565 m.meter_on_hit = damage / 8;
566 m.feintable = false;
567 m
568}
569
570pub fn leak_box(start: u8, end: u8, hit: LocalBox) -> &'static [TimedBox] {
571 Box::leak(Box::new([TimedBox::span(start, end, hit)]))
573}
574
575pub fn leak_boxes(boxes: Vec<TimedBox>) -> &'static [TimedBox] {
576 Box::leak(boxes.into_boxed_slice())
577}
578
579pub fn leak_followups(f: Vec<Followup>) -> &'static [Followup] {
580 Box::leak(f.into_boxed_slice())
581}