1use crate::chars::{Character, CharacterId};
12use crate::geom::{px, Aabb, LocalBox, Vec2i};
13use crate::input::{Btn, Buttons, Chord, InputBuffer, InputFrame, Motion, CHORD_WINDOW};
14use crate::moves::{CancelRule, HitLevel, MoveDef, MoveId, ThrowKind};
15
16pub const METER_MAX: i32 = 1000;
17pub const PREJUMP: u8 = 4;
19pub const LANDING_RECOVERY: u8 = 2;
20pub const HOP_LANDING_RECOVERY: u8 = 0;
22pub const GETUP_FRAMES: u16 = 24;
24pub const KNOCKDOWN_FRAMES: u16 = 32;
25pub const THROW_TECH_WINDOW: u8 = 7;
27pub const COMMAND_GRAB_HOLD: u8 = 4;
29pub const THROW_TECH_FRAMES: u16 = 16;
30pub const BACKDASH_FRAMES: u16 = 14;
31pub const RC_COST: i32 = 250;
32pub const RC_FREEZE_FRAMES: u8 = 6;
33pub const FEINT_RECOVERY: u16 = 8;
35pub const CANCEL_LATE_FRAMES: u16 = 2;
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
40pub enum Stance {
41 Stand,
42 Crouch,
43 Air,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq, Hash)]
47pub enum Action {
48 Stand,
49 Crouch,
50 Walk {
51 forward: bool,
52 },
53 Run,
54 BackDash {
55 frame: u16,
56 },
57 Prejump {
58 frame: u8,
59 dir_x: i32,
60 hop: bool,
61 },
62 Jump {
63 air_ok: bool,
64 hop: bool,
65 },
66 Attack {
67 move_id: MoveId,
68 frame: u16,
69 connected: Connect,
70 },
71 Feint {
73 frame: u16,
74 },
75 Block {
76 crouching: bool,
77 stun: u8,
78 },
79 Hit {
80 stun: u8,
81 knockdown: bool,
82 },
83 Knockdown {
84 frame: u16,
85 },
86 Getup {
87 frame: u16,
88 },
89 Thrown {
91 frame: u8,
92 techable: bool,
93 damage: i32,
94 meter: i32,
95 },
96 ThrowTech {
97 frame: u16,
98 },
99 Landing {
100 frame: u16,
101 total: u16,
102 },
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
106pub enum Connect {
107 None,
108 Hit,
109 Block,
110}
111
112impl Action {
113 pub fn attacking(&self) -> Option<(MoveId, u16, Connect)> {
114 match *self {
115 Action::Attack {
116 move_id,
117 frame,
118 connected,
119 } => Some((move_id, frame, connected)),
120 _ => None,
121 }
122 }
123
124 pub fn in_hitstun(&self) -> bool {
125 matches!(self, Action::Hit { .. })
126 }
127
128 pub fn in_blockstun(&self) -> bool {
129 matches!(self, Action::Block { .. })
130 }
131
132 pub fn actionable(&self) -> bool {
133 matches!(
134 self,
135 Action::Stand
136 | Action::Crouch
137 | Action::Walk { .. }
138 | Action::Run
139 | Action::Jump { air_ok: true, .. }
140 )
141 }
142
143 pub fn is_hop(&self) -> bool {
144 matches!(
145 self,
146 Action::Jump { hop: true, .. } | Action::Prejump { hop: true, .. }
147 )
148 }
149
150 pub fn throw_protected(&self) -> bool {
153 matches!(
154 self,
155 Action::Hit { .. }
156 | Action::Block { .. }
157 | Action::Knockdown { .. }
158 | Action::Getup { .. }
159 | Action::Thrown { .. }
160 | Action::ThrowTech { .. }
161 )
162 }
163
164 pub fn name(&self) -> &'static str {
165 match self {
166 Action::Stand => "stand",
167 Action::Crouch => "crouch",
168 Action::Walk { forward: true } => "walk",
169 Action::Walk { forward: false } => "backwalk",
170 Action::Run => "run",
171 Action::BackDash { .. } => "backdash",
172 Action::Prejump { hop: true, .. } => "prehop",
173 Action::Prejump { .. } => "prejump",
174 Action::Jump { hop: true, .. } => "hop",
175 Action::Jump { .. } => "jump",
176 Action::Attack { move_id, .. } => move_id.slot_name(),
177 Action::Feint { .. } => "feint",
178 Action::Block { .. } => "block",
179 Action::Hit { .. } => "hitstun",
180 Action::Knockdown { .. } => "down",
181 Action::Getup { .. } => "getup",
182 Action::Thrown { .. } => "thrown",
183 Action::ThrowTech { .. } => "tech",
184 Action::Landing { .. } => "land",
185 }
186 }
187}
188
189#[derive(Clone, Debug, PartialEq, Eq, Hash)]
190pub struct Fighter {
191 pub id: CharacterId,
192 pub pos: Vec2i,
193 pub vel: Vec2i,
194 pub facing_right: bool,
195 pub health: i32,
196 pub meter: i32,
197 pub gauge: i32,
199 pub gauge_idle: u16,
201 pub action: Action,
202 pub buffer: InputBuffer,
203 pub airborne: bool,
204 pub hop: bool,
206 pub combo: u8,
207 pub juggle: u8,
208 pub last_move: Option<MoveId>,
209 pub gravity_override: Option<i32>,
210 pub land_recovery: u8,
212 pub channel_frames: u16,
214 pub frozen_presses: Buttons,
217 pub frozen_chord: Option<Chord>,
218 pub last_distance: i32,
220 pub has_planted: bool,
221}
222
223impl Fighter {
224 pub fn spawn(id: CharacterId, x: i32, facing_right: bool) -> Self {
225 let data = id.data();
226 Self {
227 id,
228 pos: Vec2i::new(x, 0),
229 vel: Vec2i::ZERO,
230 facing_right,
231 health: data.max_health,
232 meter: 0,
233 gauge: data.gauge.start,
234 gauge_idle: 0,
235 action: Action::Stand,
236 buffer: InputBuffer::default(),
237 airborne: false,
238 hop: false,
239 combo: 0,
240 juggle: 0,
241 last_move: None,
242 gravity_override: None,
243 land_recovery: 0,
244 channel_frames: 0,
245 frozen_presses: Buttons::default(),
246 frozen_chord: None,
247 last_distance: px(200),
248 has_planted: false,
249 }
250 }
251
252 pub fn data(&self) -> &'static Character {
253 self.id.data()
254 }
255
256 pub fn input(&self) -> InputFrame {
257 self.buffer.latest()
258 }
259
260 pub fn pressed_now(&self) -> Buttons {
262 self.buffer.pressed().or(self.frozen_presses)
263 }
264
265 pub fn chord_now(&self) -> Option<Chord> {
266 self.buffer.chord().or(self.frozen_chord)
267 }
268
269 pub fn buff_tier(&self) -> i32 {
271 let step = self.data().gauge.buff_step;
272 if step <= 0 {
273 0
274 } else {
275 self.gauge / step
276 }
277 }
278
279 pub fn pushbox(&self) -> Aabb {
280 let d = self.data();
281 let h = match self.hurt_stance() {
282 Stance::Crouch => d.crouch_h,
283 Stance::Air => d.stand_h * 4 / 5,
284 Stance::Stand => d.stand_h,
285 };
286 Aabb::from_center_size(self.pos.x, self.pos.y, d.push_w, h)
287 }
288
289 pub fn hurtboxes(&self) -> Vec<Aabb> {
290 let d = self.data();
291 let local = match self.hurt_stance() {
292 Stance::Crouch => d.hurt_crouch(),
293 Stance::Air => {
294 let h = if self.hop {
295 d.stand_h * 7 / 10
296 } else {
297 d.stand_h * 4 / 5
298 };
299 LocalBox::new(-d.push_w / 2, px(8), d.push_w + px(6), h)
300 }
301 Stance::Stand => d.hurt_stand(),
302 };
303 vec![local.to_world(self.pos, self.facing_right)]
304 }
305
306 pub fn visual_aura_box(&self) -> Option<Aabb> {
309 self.data()
310 .aura
311 .map(|aura| aura.to_world(self.pos, self.facing_right))
312 }
313
314 pub fn hurt_stance(&self) -> Stance {
315 if self.airborne {
316 return Stance::Air;
317 }
318 match &self.action {
319 Action::Crouch
320 | Action::Block {
321 crouching: true, ..
322 } => Stance::Crouch,
323 Action::Attack { move_id, .. } if move_id.is_crouching() => Stance::Crouch,
324 Action::Knockdown { .. } | Action::Getup { .. } => Stance::Crouch,
325 _ => Stance::Stand,
326 }
327 }
328
329 pub fn current_move(&self) -> Option<&'static MoveDef> {
330 self.action
331 .attacking()
332 .and_then(|(id, _, _)| self.data().move_def(id))
333 }
334
335 pub fn strike_invuln(&self) -> bool {
336 if let Action::Attack { move_id, frame, .. } = self.action {
337 if let Some(mv) = self.data().move_def(move_id) {
338 return mv.invuln.strike_on(frame);
339 }
340 }
341 matches!(
343 self.action,
344 Action::Knockdown { .. } | Action::Getup { .. } | Action::ThrowTech { .. }
345 )
346 }
347
348 pub fn throw_invuln(&self) -> bool {
349 if let Action::Attack { move_id, frame, .. } = self.action {
350 if let Some(mv) = self.data().move_def(move_id) {
351 return mv.invuln.throw_on(frame);
352 }
353 }
354 false
355 }
356
357 pub fn passing_through(&self) -> bool {
359 self.current_move().map(|m| m.pass_through).unwrap_or(false)
360 }
361
362 pub fn hitboxes(&self) -> Vec<Aabb> {
363 let Some((id, frame, connected)) = self.action.attacking() else {
364 return Vec::new();
365 };
366 if connected != Connect::None {
367 return Vec::new();
369 }
370 let Some(mv) = self.data().move_def(id) else {
371 return Vec::new();
372 };
373 mv.hitboxes_on(frame)
374 .map(|b| b.to_world(self.pos, self.facing_right))
375 .collect()
376 }
377
378 pub fn would_block(&self, level: HitLevel) -> bool {
379 if self.airborne {
380 return false;
381 }
382 if !matches!(
383 self.action,
384 Action::Stand
385 | Action::Crouch
386 | Action::Walk { .. }
387 | Action::Run
388 | Action::Block { .. }
389 ) {
390 return false;
391 }
392 let inp = self.input();
393 if !inp.back() {
394 return false;
395 }
396 match level {
397 HitLevel::Low => inp.down(),
398 HitLevel::High => !inp.down(),
399 HitLevel::Mid => true,
400 }
401 }
402
403 pub fn add_meter(&mut self, amount: i32) {
404 self.meter = (self.meter + amount).clamp(0, METER_MAX);
405 }
406
407 pub fn spend_meter(&mut self, amount: i32) -> bool {
408 if self.meter >= amount {
409 self.meter -= amount;
410 true
411 } else {
412 false
413 }
414 }
415
416 pub fn add_gauge(&mut self, amount: i32) {
417 let max = self.data().gauge.max;
418 self.gauge = (self.gauge + amount).clamp(0, max);
419 }
420
421 pub fn spend_gauge(&mut self, amount: i32) -> bool {
422 if amount <= 0 {
423 return true;
424 }
425 if self.gauge >= amount {
426 self.gauge -= amount;
427 self.gauge_idle = 0;
428 true
429 } else {
430 false
431 }
432 }
433
434 pub fn face_toward(&mut self, other_x: i32) {
435 if matches!(
436 self.action,
437 Action::Attack { .. }
438 | Action::Feint { .. }
439 | Action::Hit { .. }
440 | Action::Block { .. }
441 | Action::Knockdown { .. }
442 | Action::Getup { .. }
443 | Action::Thrown { .. }
444 | Action::ThrowTech { .. }
445 | Action::Prejump { .. }
446 | Action::BackDash { .. }
447 ) {
448 return;
449 }
450 if other_x != self.pos.x {
451 self.facing_right = other_x > self.pos.x;
452 }
453 }
454
455 pub fn buffer_input(&mut self, raw: InputFrame) {
456 self.buffer.push(raw);
457 }
458
459 pub fn accumulate_frozen(&mut self) {
461 self.frozen_presses = self.frozen_presses.or(self.buffer.pressed());
462 if let Some(c) = self.buffer.chord() {
463 self.frozen_chord = Some(c);
464 }
465 }
466
467 fn clear_frozen(&mut self) {
468 self.frozen_presses = Buttons::default();
469 self.frozen_chord = None;
470 }
471
472 pub fn try_roman_cancel(&mut self) -> bool {
475 if self.chord_now() != Some(Chord::RomanCancel) {
476 return false;
477 }
478 let Action::Attack { move_id, .. } = self.action else {
479 return false;
480 };
481 if move_id == MoveId::Throw || move_id == MoveId::CommandGrab {
482 return false;
483 }
484 if !self.spend_meter(RC_COST) {
485 return false;
486 }
487 self.clear_frozen();
488 self.vel = Vec2i::ZERO;
489 self.gravity_override = None;
490 self.land_recovery = 0;
491 self.action = if self.airborne {
492 Action::Jump {
493 air_ok: true,
494 hop: self.hop,
495 }
496 } else {
497 Action::Stand
498 };
499 true
500 }
501
502 pub fn tick_pre_collision(&mut self, stage_w: i32) {
505 self.tick_gauge();
506 self.step_action();
507 self.integrate(stage_w);
508 self.clear_frozen();
509 }
510
511 fn tick_gauge(&mut self) {
512 let g = self.data().gauge;
513 self.gauge_idle = self.gauge_idle.saturating_add(1);
514 if g.regen_amount > 0
515 && self.gauge < g.max
516 && self.gauge_idle >= g.regen_delay
517 && g.regen_every > 0
518 && (self.gauge_idle - g.regen_delay).is_multiple_of(g.regen_every)
519 {
520 self.add_gauge(g.regen_amount);
521 }
522 }
523
524 fn step_action(&mut self) {
525 match self.action.clone() {
526 Action::Hit { stun, knockdown } => self.step_hit(stun, knockdown),
527 Action::Block { crouching, stun } => {
528 if stun == 0 {
529 self.resume_control();
530 } else {
531 self.action = Action::Block {
532 crouching,
533 stun: stun.saturating_sub(1),
534 };
535 }
536 }
537 Action::Knockdown { frame } => {
538 self.airborne = false;
539 self.pos.y = 0;
540 self.vel = Vec2i::ZERO;
541 if frame + 1 >= KNOCKDOWN_FRAMES {
542 self.action = Action::Getup { frame: 0 };
543 } else {
544 self.action = Action::Knockdown { frame: frame + 1 };
545 }
546 }
547 Action::Getup { frame } => {
548 if frame + 1 >= GETUP_FRAMES {
549 self.resume_control();
550 } else {
551 self.action = Action::Getup { frame: frame + 1 };
552 }
553 }
554 Action::Thrown { .. } => {
556 self.vel = Vec2i::ZERO;
557 }
558 Action::ThrowTech { frame } => {
559 self.vel.x = if self.facing_right { -px(4) } else { px(4) };
560 if frame + 1 >= THROW_TECH_FRAMES {
561 self.vel.x = 0;
562 self.resume_control();
563 } else {
564 self.action = Action::ThrowTech { frame: frame + 1 };
565 }
566 }
567 Action::Landing { frame, total } => {
568 self.vel = Vec2i::ZERO;
569 if frame + 1 >= total {
570 self.resume_control();
571 } else {
572 self.action = Action::Landing {
573 frame: frame + 1,
574 total,
575 };
576 }
577 }
578 Action::Feint { frame } => {
579 self.vel.x = 0;
580 if frame + 1 >= FEINT_RECOVERY {
581 self.resume_control();
582 } else {
583 self.action = Action::Feint { frame: frame + 1 };
584 }
585 }
586 Action::Prejump { frame, dir_x, hop } => {
587 let hop = hop || !self.input().up();
590 if frame + 1 >= PREJUMP {
591 self.begin_jump(dir_x, hop);
592 } else {
593 self.action = Action::Prejump {
594 frame: frame + 1,
595 dir_x,
596 hop,
597 };
598 }
599 }
600 Action::BackDash { frame } => {
601 if frame == 0 {
602 self.vel.x = if self.facing_right { -px(7) } else { px(7) };
603 }
604 if frame + 1 >= BACKDASH_FRAMES {
605 self.vel.x = 0;
606 self.resume_control();
607 } else {
608 if frame > 8 {
609 self.vel.x = self.vel.x * 3 / 4;
610 }
611 self.action = Action::BackDash { frame: frame + 1 };
612 }
613 }
614 Action::Attack {
615 move_id,
616 frame,
617 connected,
618 } => self.step_attack(move_id, frame, connected),
619 Action::Jump { air_ok, hop } => {
620 let gravity = self.gravity_override.unwrap_or(self.data().gravity);
621 if air_ok && self.pos.y + self.vel.y - gravity > 0 {
622 self.try_air_offense(hop);
623 }
624 }
625 Action::Run => {
626 if !self.try_ground_offense() {
627 let inp = self.input();
628 if inp.up() {
629 self.action = Action::Prejump {
630 frame: 0,
631 dir_x: 1,
632 hop: false,
633 };
634 } else if !inp.forward() || inp.down() {
635 self.vel.x = 0;
636 self.idle_from_input();
637 } else {
638 let spd = self.data().run_speed;
639 self.vel.x = if self.facing_right { spd } else { -spd };
640 self.action = Action::Run;
641 }
642 }
643 }
644 Action::Stand | Action::Crouch | Action::Walk { .. } => {
645 if !self.try_ground_offense() {
646 self.ground_locomotion();
647 }
648 }
649 }
650 }
651
652 fn step_hit(&mut self, stun: u8, knockdown: bool) {
653 if stun == 0 {
654 if self.airborne {
655 self.action = Action::Hit { stun: 0, knockdown };
659 } else if knockdown {
660 self.action = Action::Knockdown { frame: 0 };
661 } else {
662 self.resume_control();
663 }
664 } else {
665 self.action = Action::Hit {
666 stun: stun - 1,
667 knockdown,
668 };
669 }
670 }
671
672 fn step_attack(&mut self, move_id: MoveId, frame: u16, connected: Connect) {
673 let Some(mv) = self.data().move_def(move_id).cloned() else {
674 self.idle_from_input();
675 return;
676 };
677
678 if let Some(chord) = self.chord_now() {
679 match chord {
680 Chord::Feint if mv.feintable && mv.in_startup(frame) => {
681 self.vel = Vec2i::ZERO;
682 self.gravity_override = None;
683 self.land_recovery = 0;
684 self.channel_frames = 0;
685 self.action = Action::Feint { frame: 0 };
686 return;
687 }
688 Chord::Throw | Chord::Ex | Chord::Overhead
691 if !self.airborne
692 && connected == Connect::None
693 && frame < CHORD_WINDOW as u16
694 && !matches!(
695 mv.id,
696 MoveId::Throw
697 | MoveId::Overhead
698 | MoveId::ExA
699 | MoveId::ExB
700 | MoveId::Super
701 ) =>
702 {
703 let refund_gauge = mv.gauge_cost;
704 let refund_meter = mv.meter_cost;
705 self.add_gauge(refund_gauge);
706 self.add_meter(refund_meter);
707 if self.try_chord(chord) {
708 return;
709 }
710 let _ = self.spend_gauge(refund_gauge);
711 let _ = self.spend_meter(refund_meter);
712 }
713 _ => {}
714 }
715 }
716
717 let pressed = self.pressed_now();
718
719 if pressed.any() && !mv.followups.is_empty() {
721 for b in pressed.iter() {
722 if let Some(next) = mv.followup_for(b, frame) {
723 self.start_move(next);
724 return;
725 }
726 }
727 }
728
729 if self.try_cancel(&mv, frame, connected, pressed) {
730 return;
731 }
732
733 if let Some(ch) = mv.channel {
735 if frame == mv.first_active()
736 && self.input().buttons.get(ch.button)
737 && self.channel_frames < ch.max_frames
738 {
739 self.channel_frames += 1;
740 self.add_gauge(ch.gauge_per_frame);
741 return;
742 }
743 }
744
745 let next = frame + 1;
746 if mv.vel_frames > 0 && next >= mv.vel_frames as u16 && !self.airborne {
747 self.vel.x = 0;
748 }
749 if mv.finished(next) {
750 self.gravity_override = None;
751 self.channel_frames = 0;
752 if self.airborne {
753 self.action = Action::Jump {
754 air_ok: false,
755 hop: self.hop,
756 };
757 } else {
758 self.vel.x = 0;
759 self.resume_control();
760 }
761 } else {
762 self.action = Action::Attack {
763 move_id,
764 frame: next,
765 connected,
766 };
767 }
768 }
769
770 fn try_cancel(
771 &mut self,
772 mv: &MoveDef,
773 frame: u16,
774 connected: Connect,
775 pressed: Buttons,
776 ) -> bool {
777 let can = match (mv.cancel, connected) {
778 (CancelRule::Never, _) => false,
779 (CancelRule::OnHit, Connect::Hit) => true,
780 (CancelRule::OnHitOrBlock, Connect::Hit | Connect::Block) => true,
781 _ => false,
782 };
783 if !can || frame < mv.first_active() || frame > mv.last_active() + CANCEL_LATE_FRAMES {
784 return false;
785 }
786 let ex = self.chord_now() == Some(Chord::Ex);
787 if !pressed.any() && !ex {
788 return false;
789 }
790 if let Some(id) = self.data().match_special(
791 self.airborne,
792 pressed,
793 ex,
794 self.meter,
795 self.gauge,
796 &self.buffer,
797 ) {
798 if id.is_rekka() && id != MoveId::Rekka1 {
799 return false;
800 }
801 self.start_move(id);
802 return true;
803 }
804 false
805 }
806
807 fn try_chord(&mut self, chord: Chord) -> bool {
809 match chord {
810 Chord::Throw => {
811 if self.data().has_move(MoveId::Throw) {
812 self.start_move(MoveId::Throw);
813 return true;
814 }
815 }
816 Chord::Overhead => {
817 if self.data().has_move(MoveId::Overhead) {
818 self.start_move(MoveId::Overhead);
819 return true;
820 }
821 }
822 Chord::Ex => {
823 if let Some(id) = self.data().match_special(
824 false,
825 Buttons::default(),
826 true,
827 self.meter,
828 self.gauge,
829 &self.buffer,
830 ) {
831 self.start_move(id);
832 return true;
833 }
834 }
835 _ => {}
836 }
837 false
838 }
839
840 fn try_ground_offense(&mut self) -> bool {
841 let inp = self.input();
842 let pressed = self.pressed_now();
843
844 if let Some(chord) = self.chord_now() {
845 if self.try_chord(chord) {
846 return true;
847 }
848 }
849
850 if pressed.any() {
851 let mut id = self.data().match_special(
852 false,
853 pressed,
854 false,
855 self.meter,
856 self.gauge,
857 &self.buffer,
858 );
859 if id == Some(MoveId::ShotA)
861 && self.has_planted
862 && self.data().has_move(MoveId::Detonate)
863 {
864 id = Some(MoveId::Detonate);
865 }
866 if let Some(id) = id {
867 if id.is_rekka() && id != MoveId::Rekka1 {
868 } else {
870 self.start_move(id);
871 return true;
872 }
873 }
874 if let Some(id) = self.pick_normal(pressed) {
875 self.start_move(id);
876 return true;
877 }
878 }
879
880 if inp.up() {
881 let dir_x = if inp.forward() {
882 1
883 } else if inp.back() {
884 -1
885 } else {
886 0
887 };
888 self.action = Action::Prejump {
889 frame: 0,
890 dir_x,
891 hop: false,
892 };
893 self.vel.x = 0;
894 return true;
895 }
896
897 if self.buffer.motion(Motion::BackDash) && !self.airborne {
898 self.action = Action::BackDash { frame: 0 };
899 return true;
900 }
901 if self.buffer.motion(Motion::ForwardDash) && inp.forward() && !inp.down() {
902 self.action = Action::Run;
903 let spd = self.data().run_speed;
904 self.vel.x = if self.facing_right { spd } else { -spd };
905 return true;
906 }
907 false
908 }
909
910 fn try_air_offense(&mut self, _hop: bool) {
911 let pressed = self.pressed_now();
912 if !pressed.any() {
913 return;
914 }
915 if let Some(id) =
916 self.data()
917 .match_special(true, pressed, false, self.meter, self.gauge, &self.buffer)
918 {
919 self.start_move(id);
920 return;
921 }
922 if let Some(id) = self.pick_normal(pressed) {
923 self.start_move(id);
924 }
925 }
926
927 fn pick_normal(&self, pressed: Buttons) -> Option<MoveId> {
928 for b in [Btn::HS, Btn::ST, Btn::S, Btn::FL, Btn::K, Btn::P] {
931 if !pressed.get(b) {
932 continue;
933 }
934 if let Some(id) = self.data().select_normal(
935 self.input().down() && !self.airborne,
936 self.airborne,
937 b,
938 self.last_distance,
939 ) {
940 return Some(id);
941 }
942 }
943 None
944 }
945
946 fn ground_locomotion(&mut self) {
947 let inp = self.input();
948 let d = self.data();
949 if inp.down() {
950 self.vel.x = 0;
951 self.action = Action::Crouch;
952 return;
953 }
954 if inp.forward() {
955 self.vel.x = if self.facing_right {
956 d.walk_fwd
957 } else {
958 -d.walk_fwd
959 };
960 self.action = Action::Walk { forward: true };
961 } else if inp.back() {
962 self.vel.x = if self.facing_right {
963 -d.walk_back
964 } else {
965 d.walk_back
966 };
967 self.action = Action::Walk { forward: false };
968 } else {
969 self.vel.x = 0;
970 self.action = Action::Stand;
971 }
972 }
973
974 fn idle_from_input(&mut self) {
975 self.gravity_override = None;
976 if self.airborne {
977 self.action = Action::Jump {
978 air_ok: false,
979 hop: self.hop,
980 };
981 } else {
982 self.ground_locomotion();
983 }
984 }
985
986 fn resume_control(&mut self) {
989 self.idle_from_input();
990 if !self.airborne {
991 self.try_ground_offense();
992 }
993 }
994
995 fn begin_jump(&mut self, dir_x: i32, hop: bool) {
996 let d = self.data();
997 let signed = if self.facing_right { dir_x } else { -dir_x };
998 if hop {
999 self.vel.x = signed * d.hop_x;
1000 self.vel.y = d.hop_y;
1001 } else {
1002 self.vel.x = signed * d.jump_x;
1003 self.vel.y = d.jump_y;
1004 }
1005 self.airborne = true;
1006 self.hop = hop;
1007 self.action = Action::Jump { air_ok: true, hop };
1008 }
1009
1010 pub fn start_move(&mut self, id: MoveId) {
1011 self.last_move = Some(id);
1012 self.channel_frames = 0;
1013 if let Some(mv) = self.data().move_def(id) {
1014 if !self.airborne || mv.vel_x != 0 {
1017 self.vel.x = if self.facing_right { mv.vel_x } else { -mv.vel_x };
1018 }
1019 if mv.vel_y != 0 {
1020 self.vel.y = mv.vel_y;
1021 self.airborne = true;
1022 self.hop = false;
1023 self.land_recovery = mv.land_recovery;
1024 }
1025 self.gravity_override = mv.gravity_override;
1026 if mv.meter_cost > 0 {
1027 let _ = self.spend_meter(mv.meter_cost);
1028 }
1029 if mv.gauge_cost > 0 {
1030 let _ = self.spend_gauge(mv.gauge_cost);
1031 }
1032 }
1033 self.action = Action::Attack {
1034 move_id: id,
1035 frame: 0,
1036 connected: Connect::None,
1037 };
1038 }
1039
1040 fn integrate(&mut self, stage_w: i32) {
1041 if self.airborne {
1042 let g = self.gravity_override.unwrap_or(self.data().gravity);
1043 self.vel.y -= g;
1044 }
1045 self.pos.x += self.vel.x;
1046 self.pos.y += self.vel.y;
1047
1048 let half = self.data().push_w / 2;
1049 self.pos.x = self.pos.x.clamp(half, stage_w - half);
1050
1051 if self.pos.y <= 0 {
1052 self.pos.y = 0;
1053 if self.airborne {
1054 self.airborne = false;
1055 self.vel = Vec2i::ZERO;
1056 self.gravity_override = None;
1057 match self.action {
1058 Action::Hit { knockdown: true, .. } => {
1059 self.action = Action::Knockdown { frame: 0 };
1060 }
1061 Action::Hit { stun: 0, .. } => {
1064 self.action = Action::Landing {
1065 frame: 0,
1066 total: LANDING_RECOVERY as u16,
1067 };
1068 }
1069 Action::Hit { .. } => {}
1071 _ => {
1072 let base = if self.hop { HOP_LANDING_RECOVERY } else { LANDING_RECOVERY };
1073 let total = base.max(self.land_recovery) as u16;
1074 if total == 0 {
1075 self.resume_control();
1076 } else {
1077 self.action = Action::Landing { frame: 0, total };
1078 }
1079 }
1080 }
1081 self.land_recovery = 0;
1082 self.hop = false;
1083 }
1084 }
1085 }
1086
1087 pub fn mark_connected(&mut self, hit: bool) {
1088 if let Action::Attack { connected, .. } = &mut self.action {
1089 *connected = if hit { Connect::Hit } else { Connect::Block };
1090 }
1091 }
1092
1093 pub fn apply_hit(
1094 &mut self,
1095 stun: u8,
1096 knockdown: bool,
1097 launch: i32,
1098 push: i32,
1099 from_right: bool,
1100 ) {
1101 self.combo = self.combo.saturating_add(1);
1102 let dir = if from_right { -1 } else { 1 };
1103 self.pos.x += dir * push;
1106 if launch > 0 {
1107 self.vel.x = dir * (push / 4);
1108 self.vel.y = launch;
1109 self.airborne = true;
1110 } else if !self.airborne {
1111 self.vel = Vec2i::ZERO;
1112 }
1113 self.action = Action::Hit { stun, knockdown };
1114 self.hop = false;
1115 self.gravity_override = None;
1116 self.land_recovery = 0;
1117 self.channel_frames = 0;
1118 }
1119
1120 pub fn apply_block(&mut self, stun: u8, push: i32, from_right: bool, crouching: bool) {
1121 self.combo = 0;
1122 let dir = if from_right { -1 } else { 1 };
1123 self.pos.x += dir * push;
1124 if !self.airborne {
1125 self.vel = Vec2i::ZERO;
1126 }
1127 self.action = Action::Block { crouching, stun };
1128 }
1129
1130 pub fn apply_grab(&mut self, kind: ThrowKind, damage: i32, meter: i32) {
1131 self.vel = Vec2i::ZERO;
1132 self.gravity_override = None;
1133 self.channel_frames = 0;
1134 self.action = Action::Thrown {
1135 frame: 0,
1136 techable: kind == ThrowKind::Normal,
1137 damage,
1138 meter,
1139 };
1140 }
1141
1142 pub fn reset_combo(&mut self) {
1143 self.combo = 0;
1144 self.juggle = 0;
1145 }
1146}