[Astro #90] LAYER FIGHTER // バトル強化 - 被弾アニメ・プレイヤーKO・敵攻撃バリエーション・2本先取

[Astro #90] LAYER FIGHTER // バトル強化 - 被弾アニメ・プレイヤーKO・敵攻撃バリエーション・2本先取

はじめに

前回(Astro #89)でタイトル画面を完成させた。今回はバトルシステムの強化に集中した。

1. 被弾アニメ(SYS_HURT)

Assets.json に追加

{
  "id": "SYS_HURT",
  "type": "system",
  "name": "Hurt",
  "file": "/animations/anim/Reaction.vrma",
  "version": "1.0.0",
  "animSpeed": 0.8,
  "duration": 40
}

duration でアニメ再生フレーム数を管理する。animSpeed で再生速度も調整できる。

ANIM_MAP に追加

HITSTUN: 'SYS_HURT',

NO_LOOP_ANIMS に追加

const NO_LOOP_ANIMS = new Set([
  'SYS_PUNCH', 'SYS_CROUCH_PUNCH', 'SYS_KICK',
  'SYS_KICK_KNEE', 'SYS_KICK_FORWARD', 'SYS_KICK_LOW',
  'SYS_HURT',  // ← 追加
]);

2. HITSTUN 中の入力キャンセル防止

被弾中に移動ボタンを押すとアニメがキャンセルされる問題を修正した。

ステートマシンの入り口で HITSTUN 中は入力を受け付けず、actionDoneIDLE に戻すだけにする:

const isLocked = ['PUNCHING', 'KICKING', 'HITSTUN', ...].includes(curState);
const actionDone = actionFrameRef.current >= actionDurRef.current;

if (!isLocked || actionDone) {
  if (curState === 'HITSTUN' && actionDone) {
    transitionState('IDLE', 1, frame);
  } else {
    // 通常の入力処理
  }
}

ヒット時の transitionStateduration を渡すことで、アニメが終わるまで入力をブロックできる:

const hurtDef = (assets as any).animations?.find((a: any) => a.id === 'SYS_HURT');
transitionState('HITSTUN', hurtDef?.duration ?? 40, frame);

3. 敵の被弾アニメ(enemyHitstunAnimRef)

敵にはプレイヤーと同じステートマシンがないため、enemyHitstunAnimRef で被弾アニメの残りフレームを管理する暫定対処を実装した。

const enemyHitstunAnimRef = useRef(0);

// ヒット時
setEnemyAnimId('SYS_HURT');
const hurtDef = (assets as any).animations?.find((a: any) => a.id === 'SYS_HURT');
enemyHitstunAnimRef.current = hurtDef?.duration ?? 40;

// AIブロック内
if (enemyHitstunAnimRef.current > 0) {
  enemyHitstunAnimRef.current--;
  ep.facingRight = p.posX > ep.posX; // 向きだけ更新
} else {
  // 通常のAI処理
}

根本的には敵にもステートマシンを持たせるべきだが、現状は暫定対処で動いている。


4. プレイヤーKO判定

敵のKO判定しかなかったので、プレイヤー側のKO判定を追加した。

koWinnerRef で勝者を管理し、表示用に koWinner state と同期する:

const koWinnerRef = useRef<'PLAYER' | 'ENEMY'>('PLAYER');
const [koWinner, setKoWinner] = useState<'PLAYER' | 'ENEMY'>('PLAYER');

// 敵KO
if (roundPhaseRef.current === 'FIGHTING' && ep.hp <= 0) {
  koWinnerRef.current = 'PLAYER';
  setKoWinner('PLAYER');
  roundPhaseRef.current = 'KO';
  setRoundPhase('KO');
  // ...
}

// プレイヤーKO
if (roundPhaseRef.current === 'FIGHTING' && p.hp <= 0) {
  koWinnerRef.current = 'ENEMY';
  setKoWinner('ENEMY');
  roundPhaseRef.current = 'KO';
  setRoundPhase('KO');
  // ...
}

WAIT_INPUT / VICTORY の表示で koWinner で分岐:

{koWinner === 'PLAYER' ? 'YOU WIN' : 'YOU LOSE'}

5. 敵攻撃バリエーション(Assets.json 駆動)

これまで PUNCH_L だけをハードコードしていた敵の攻撃を、Assets.json で管理する形に変更した。

{
  "id": "ENEMY_CHEN_QIANYU",
  "attackMoves": ["PUNCH_L", "KICK_M"]
}

コード側はランダム選択するだけ:

const attackMoves = enemyVrmDef?.attackMoves ?? ['PUNCH_L'];
const selectedMoveId = attackMoves[Math.floor(Math.random() * attackMoves.length)];
ep.attackMoveId = selectedMoveId;

// アニメも Moves.json の animId から取得
const selectedMove = (moves as any).moves.find((m: any) => m.id === selectedMoveId);
setEnemyAnimId(selectedMove?.animId ?? 'SYS_PUNCH');

JSON に技を追加するだけで敵の攻撃バリエーションが増やせる。


6. 2本先取の勝利条件

playerWinsRef / enemyWinsRef を追加してゲームループ内で参照できるようにした:

const playerWinsRef = useRef(0);
const enemyWinsRef  = useRef(0);

// 勝利時に ref も更新
playerWinsRef.current++;
setPlayerWins(prev => prev + 1);

WAIT_INPUT フェーズで2本先取チェック:

if (playerWinsRef.current >= 2 || enemyWinsRef.current >= 2) {
  // ゲーム終了 → 全リセット
  playerWinsRef.current = 0;
  enemyWinsRef.current  = 0;
  setPlayerWins(0);
  setEnemyWins(0);
  // キャラクターもリセット
  // ...
}

今後の課題

  • しゃがみ中のヒットボックス判定(しゃがみで上段攻撃を避けられるようにする)
  • しゃがみ状態でのキック技バリエーション
  • 敵のステートマシン化(被弾アニメの暫定対処を根本解決)
  • コンボシステム(難易度高いので後回し)