Prism Raid — Triangleシューティング
三角形の機体で3ウェーブを迎撃する、小さな縦型シューティングです。自機は上向き、敵は下向きに回転したTriangleで描画します。弾は矩形と円、背景は細い矩形です。画像・音声・外部サービス・AIモデルは使いません。
サンプルギャラリーから起動できます。Start missionで開始し、Watch autopilotでは通常の入力APIを使う自動プレイヤーを観察できます。
操作
| 操作 | PC | タッチ |
|---|---|---|
| 移動 | WASD/矢印 | 下部の4方向ボタンを長押し |
| ダッシュ | Shiftを長押し | DASHを長押し |
| 連射 | Spaceを長押し | FIREを長押し |
| 一時停止・再開 | P/Esc、Pause/Resume | Pause/Resume |
| 再開始・メニュー | Restart/Menu | Restart/Menu |
移動と射撃は同時に使えます。タッチは方向とFIREに2本の指を使います。Start missionとRestart後は盤面へフォーカスします。盤面の外へフォーカスが移る、windowがblurする、タブが隠れるとブラウザ入力は解放されます。ネイティブボタン上のキーはゲームに奪われません。
ルール
- HPは5。敵本体・敵弾への接触で1減り、その後1シミュレーション秒は接触ダメージを受けません。敵弾と接触した敵本体は消費されます。
- 下端を突破した敵はrelayへ1ダメージを与えます。これは自機の無敵時間と独立した失点です。
- 3ウェーブの敵数は6/8/10。全出現済みの敵がいなくなると次へ進み、最後を生き残るとクリアです。HPが0、または75シミュレーション秒に達するとゲームオーバーです。
- 珊瑚色のscoutはHP1・100点、琥珀色のdrifterはHP2・160点、紫のgunnerはHP3・220点。drifterは狙い弾、gunnerは狙い弾と左右への弾を撃ちます。
- 自機は0.16秒ごとに2発を発射します。弾は貫通せず、消費後の同一フレームの接触から重複ダメージ・得点を発生させません。
- Restartは新しいSceneを作り、敵・弾・入力元・購読・タイマー・描画を破棄します。Engineの乱数列は継続します。初回seedは2026です。
公開APIでの構成
| ソース | 責務とAPI |
|---|---|
| logic.ts | Actor・Component・Transform・Motion・型付きRaidActions、Trigger、型付きイベント、Sceneタイマーで移動・発射・HP・ウェーブを実装 |
| view.ts | 公開RenderableでTriangle・Box・Circleを配置し、透明度と背景を更新 |
| main.ts | Game・Scene、ActionController、ActionSource、Button/HoldButton、Label、ポーズと破棄をブラウザへ結び付ける |
| autoplay.ts | コピーされたゲーム状態を読むルールベースの照準・回避。書き込みは通常のActionSource.setのみ |
ロジックはDOM・Pixi・Web実行環境をimportしません。描画の有無でゲーム用乱数の消費順が変わらないよう、背景の配置は別の固定パターンです。Raid.snapshot()が固有のHP・弾・敵・武器待機時間・ウェーブ等をコピーし、Engine.snapshot()が入力・seed状態・一般的なActor状態を提供します。復元APIではありません。
Triangleと接触の近似
import {
Actor,
Collider,
CollisionResponse,
CollisionShape,
Renderable,
Shape,
Transform,
} from "@simpludify/core";
const ship = new Actor();
ship.add(new Transform());
ship.add(
new Renderable({
source: { kind: "shape", shape: Shape.Triangle },
width: 28,
height: 36,
color: 0x65a8ed,
}),
);
ship.add(
new Collider({
shape: CollisionShape.Circle,
radius: 11,
response: CollisionResponse.Trigger,
}),
);
Triangleの既定は上向き、原点は外接矩形の中心です。頂点は(0,-height/2), (width/2,height/2), (-width/2,height/2)。敵はTransform.rotationを約πにして下向きへ回します。描画と接触形状は独立しており、自機は半径11、敵は14/19、自機弾は6、敵弾は5の円で近似します。Solid・CollisionBodyはこのゲームには不要です。
接触判定は離散的です。自機弾300、敵弾165世界単位/秒、WebのmaxDeltaは1/30秒に設定し、薄すぎない当たり判定を使います。大きなEngine.stepや極端な弾速ではすり抜けます。テストは高速弾の見逃しを再現し、同じ経路を小さなステップで検出します。接線付近の全経路や連続衝突を保証するものではありません。
ContactEnterで弾を最初にdisposeしてからダメージを適用します。相手の破棄も確認し、複数接触で二重撃破しません。ContactExitは通知済みで生存する相手だけが受け取れるため、消費済みの弾の後処理には使用しません。
起動・検証
リポジトリのルートで実行します。
npm ci
npm run dev -- --example prism-raid
npm test -- tests/unit/prism-raid.test.ts
npm run build -- --target web --example prism-raid --base /Simpludify/
npm run site:build -- --base /Simpludify/
npm run site:check
npm run test:site
同じseed・入力列・時間刻みでゲーム状態を再現できます。ヘッドレス検証と自動プレイヤーは実時間を待たずに進めます。Webの可変フレームやGPU出力を含む全環境でのビット一致は保証しません。大きなステップはタイマーの欠落回数や接触経路を変えるため、高速検証でも小さな固定刻みを使います。
APIの使い勝手・性能・検証記録を参照してください。自動ブラウザ検証はChromiumと合成タッチが対象であり、実機Safari/Firefoxや任意のGPUでの保証とは分けています。
Action入力とリバインド
RaidActions.MoveはAxis2D、Fire・Sprint・PauseはButtonです。new Raid(scene, engine.input, random)で構成し、移動Componentと戦闘ルールは物理キーを参照しません。キーボードはcontrolsのbindings、タッチは独立ActionSource、自動プレイヤーはround.inputへMove/Fireを設定します。controls.rebindで物理キーを変更しても、タッチ・自動プレイや戦闘ロジックは変わりません。pause/resumeは停止中も捕捉するonPressedを利用します。詳細な入力契約には所有権・合成・設定保存をまとめています。
移動は標準ActionMovementが担当し、Inputフェーズのゲーム固有SprintScaleがscaleを1/2へ設定します。Motion.speedは基準速度220で、ダッシュ倍率とは別の責務です。移動後のクランプ・射撃・寿命判定はLateUpdateで接触判定より前に行います。
記録と再生
replay.tsは同じゲームロジックを名前付きenemy乱数ストリームで動かし、論理Actionを記録・JSON往復・再生して状態を比較します。通常のブラウザプレイでは記録を有効にしません。Sprint Action追加後のサンプルゲーム版はprism-raid-replay-2です。旧版はAction定義が一致しないため再記録してください。ReplayData形式はv1のままです。
npm run replay:demo
npm run replay:demo -- --out mission.json
ファイル保存はCLI側の処理で、CoreのReplayはメモリ上のJSON互換データだけを扱います。ゲームバージョンはサンプル自身が指定します。Random・Replay・固定ステップに手順と制約をまとめています。