# 機能設計書 9-難易度選択

## 概要

K-Shoot MANIAの選曲画面において、Light / Challenge / Extended / Infinite の4段階の難易度を左右キーまたはアナログ入力で切り替える機能の設計書である。

### 本機能の処理概要

難易度選択機能は、選曲画面（start_select）のメインループ内で動作し、方向キー（左右）・アナログ入力（levslider）により現在選択中の楽曲の難易度（levnow変数: 0=Light, 1=Challenge, 2=Extended, 3=Infinite）を切り替える。選択した難易度にデータが存在しない場合は、隣接する難易度に自動フォールバックする機構を持つ。

**業務上の目的・背景**: 各楽曲は最大4段階の難易度譜面を持つが、全難易度が存在するとは限らない。プレイヤーが意図する難易度をスムーズに選択し、データ未存在時にも適切な難易度に自動遷移させることで操作性を向上させる。

**機能の利用シーン**: 選曲画面で楽曲を選択した状態で、左右キーまたはアナログ入力により難易度を変更する。曲選択変更時にも自動的にフォールバック処理が動作する。

**主要な処理内容**:
1. 右キー入力によるlevnowインクリメント（levnow++、上限3）
2. 左キー入力によるlevnowデクリメント（levnow--、下限0）
3. アナログ入力（levslider/LEVSLIDER_MAX）による難易度変更
4. 存在しない難易度のスキップ処理（隣接難易度への自動ジャンプ）
5. 表示用難易度（levm）のフォールバック処理
6. 難易度変更時のSE再生・ジャケット/スコア/楽曲情報のリフレッシュ
7. 曲選択変更時の自動フォールバック

**関連システム・外部連携**: BASS Audio Library（SE再生）

**権限による制御**: 特になし。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 3 | 曲選択画面 | 主画面 | 方向キー左右・アナログ入力によるLight/Challenge/Extended/Infiniteの4段階難易度切替 |

## 機能種別

ナビゲーション・状態管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| levnow | int | Yes | 現在の難易度 | 0-3 (0=Light, 1=Challenge, 2=Extended, 3=Infinite) |
| levm | int | Yes | 表示用難易度 | 0-3 |
| selnow | int | Yes | 現在の選択曲位置 | 0 - mnum-1 |
| levslider | int | No | アナログ入力による難易度スライダー値 | -LEVSLIDER_MAX ~ LEVSLIDER_MAX |
| analogsw | int | No | アナログ入力の軸スワップフラグ | 0 or 1 |
| analoginput | int | No | アナログ入力方式 | 0-5 |

### 入力データソース

- キーボード方向キー（左:37、右:39）
- キーバインド設定（key.(6+2*analogsw) / key.(7+2*analogsw)）
- ジョイスティック入力（getjoykeystat(6+2*analogsw) / getjoykeystat(7+2*analogsw)）
- アナログスティック入力（stickstat.(analogsw)）
- minfostr配列の難易度別データ有無

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| levnow | int | 変更後の難易度（0-3） |
| levm | int | 表示用難易度（フォールバック適用済み） |

### 出力先

- グローバル変数（levnow, levm）
- SE再生（sellid: 難易度変更SE）
- *refreshscore, *refreshjacket, *refreshsonginfo の各サブルーチン呼び出し

## 処理フロー

### 処理シーケンス

```
1. 右キー入力処理（1217-1287行目）
   ├─ キー入力判定（方向キー39 / キーバインド / ジョイスティック / アナログスティック）
   ├─ 前提条件チェック（ダイアログ非表示 & 120ms経過 & levnow<3 & 通常曲）
   ├─ 隣接難易度検索
   │   ├─ levnow+1にデータあり → levnow++
   │   ├─ levnow=0 & Extendedあり → levnow=2
   │   └─ levnow<=1 & Infiniteあり → levnow=3
   ├─ SE再生（sellid）
   ├─ フォールバック処理（levm設定）
   └─ リフレッシュ（score/jacket/songinfo）
2. 左キー入力処理（1288-1358行目）
   ├─ キー入力判定（方向キー37 / キーバインド / ジョイスティック / アナログスティック）
   ├─ 前提条件チェック（ダイアログ非表示 & 120ms経過 & levnow>0 & 通常曲）
   ├─ 隣接難易度検索
   │   ├─ levnow-1にデータあり → levnow--
   │   ├─ levnow=3 & Challengeあり → levnow=1
   │   └─ levnow>=2 & Lightあり → levnow=0
   ├─ SE再生（sellid）
   ├─ フォールバック処理（levm設定）
   └─ リフレッシュ（score/jacket/songinfo）
3. アナログ入力処理（684-825行目）
   ├─ levslider/LEVSLIDER_MAX で移動量算出
   ├─ 正方向（684-752行目）: levnow++ と同等のロジック
   ├─ 負方向（753-821行目）: levnow-- と同等のロジック
   └─ levslider剰余リセット（822-825行目）
4. 曲変更時フォールバック（89-123行目）
   └─ minfostr.selnow.levm.0="" 時に隣接難易度を探索
```

### フローチャート

```mermaid
flowchart TD
    A[入力検出] --> B{入力種別}
    B -->|右キー| C{levnow<3?}
    B -->|左キー| D{levnow>0?}
    B -->|アナログ| E[levslider値計算]
    C -->|Yes| F{levnow+1にデータあり?}
    C -->|No| Z[処理なし]
    D -->|Yes| G{levnow-1にデータあり?}
    D -->|No| Z
    F -->|Yes| H[levnow++]
    F -->|No| I{スキップ先にデータあり?}
    I -->|Yes| J[levnowをスキップ先に設定]
    I -->|No| Z
    G -->|Yes| K[levnow--]
    G -->|No| L{スキップ先にデータあり?}
    L -->|Yes| M[levnowをスキップ先に設定]
    L -->|No| Z
    E --> N{levslider/LEVSLIDER_MAX>=1?}
    N -->|Yes| C
    N -->|No| O{levslider<0?}
    O -->|Yes| D
    O -->|No| Z
    H --> P[SE再生]
    J --> P
    K --> P
    M --> P
    P --> Q[levm=levnow]
    Q --> R{minfostr.selnow.levm.0が空?}
    R -->|Yes| S[フォールバック処理]
    R -->|No| T[リフレッシュ処理]
    S --> T
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | 難易度インデックス | 0=Light, 1=Challenge, 2=Extended, 3=Infinite | 常時 |
| BR-02 | 難易度範囲制限 | levnowは0以上3以下に制限 | 常時（46行目: `levnow=max(min(levnow,3),0)`） |
| BR-03 | 通常曲限定 | 難易度変更はminfo.j.0.0>=0かつminfo.j.0.0<4の場合のみ可能 | キー入力時 |
| BR-04 | 入力リピート制御 | 120msのインターバルで連続入力を制御（levplustp/levminustp） | キー入力時 |
| BR-05 | 隣接スキップ | levnow+1/-1にデータがない場合、2段階先の難易度へスキップ | データ未存在時 |
| BR-06 | フォールバック | levmの値に対応するデータがない場合、3→2→1→0の優先度で降格、または上位へ探索 | 曲変更時・難易度変更時 |
| BR-07 | LEVSLIDER_MAX | levsliderの閾値は8 | アナログ入力時 |

### 計算ロジック

- アナログ入力: `levslider += int(double(stickstat.0) * cos(minf(double(stickstat.0)/6, M_PI/2.1f)))` （select.hsp 556-559行目）
- 移動判定: `repeat min(max(levslider/LEVSLIDER_MAX,0),1)` （select.hsp 684行目）
- 剰余リセット: `levslider = levslider \ LEVSLIDER_MAX` （select.hsp 824行目）
- フォールバック順序（levm=3の場合）: 2→1→0 （select.hsp 712-718行目）
- フォールバック順序（levm=2の場合）: 1→3→0 （select.hsp 720-726行目）
- フォールバック順序（levm=1の場合）: 0→2→3 （select.hsp 728-734行目）
- フォールバック順序（levm=0の場合）: 1→2→3 （select.hsp 736-742行目）

## データベース操作仕様

本機能ではデータベース操作は行わない。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | 難易度データ不在 | minfostr.selnow.levnow.0="" | フォールバック処理で隣接難易度に自動切替 |

### リトライ仕様

リトライ処理は実装されていない。

## トランザクション仕様

トランザクション処理は適用されない。

## パフォーマンス要件

- 入力リピート制御: 120msインターバル（levplustp/levminustp）
- アナログ入力処理: LEVSLIDER_MAX=8の閾値で移動量判定

## セキュリティ考慮事項

特になし。

## 備考

- levnowは選択中の難易度、levmは表示用難易度を表す。両者は通常同一だが、データ未存在時にlevmのみフォールバックされる場合がある。
- ソート方式切替時にlevnow=levmとして同期される（select.hsp 249行目）。
- SE再生時、既にフォールバック済みの難易度と同一の場合はSEを再生しない（例: `if(levm!=2):BASS_ChannelPlay stat1`）。

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | kshootmania.hsp | `kshootmania.hsp` | LEVSLIDER_MAX=8定数 |

**読解のコツ**: levnowは0-3の整数値で難易度を表す。minfostr配列の第2次元がこの値に対応し、`minfostr.selnow.levnow.0`が空文字列の場合は該当難易度のデータが存在しないことを意味する。

- **27行目**: `#define LEVSLIDER_MAX 8`

#### Step 2: 初期化処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | select.hsp | `src/scene/select.hsp` | levnow初期化・フォールバック |

**主要処理フロー**:
- **46行目**: `levnow=max(min(levnow,3),0)` - 初期化時のlevnow範囲クランプ
- **70行目**: `levmp=levnow` - 前フレームのlevnow保存
- **89-123行目**: 選曲画面開始時のフォールバック処理

#### Step 3: キー入力による難易度変更を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | select.hsp | `src/scene/select.hsp` | 右キー入力（1217-1287行目） |
| 3-2 | select.hsp | `src/scene/select.hsp` | 左キー入力（1288-1358行目） |

**主要処理フロー**:
- **1217-1220行目**: 右キー入力の検出（方向キー39 / キーバインド / ジョイスティック / アナログスティック）
- **1221行目**: 前提条件判定（`dialognow=0 & timenow-levplustp>120 & stat1=1 & stat2=0 & levnow<3 & ginfo2()=0 & minfo.j.0.0>=0 & minfo.j.0.0<4`）
- **1223-1244行目**: 隣接難易度検索とスキップ処理
- **1246-1281行目**: levmのフォールバック処理
- **1282-1286行目**: SE再生成功時のリフレッシュ呼び出し
- **1288-1291行目**: 左キー入力の検出
- **1292行目**: 前提条件判定（levnow>0）
- **1294-1315行目**: 隣接難易度検索とスキップ処理（逆方向）

#### Step 4: アナログ入力による難易度変更を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | select.hsp | `src/scene/select.hsp` | アナログ入力処理（555-825行目） |

**主要処理フロー**:
- **555-560行目**: levsliderへのアナログ入力値加算（cos関数による非線形マッピング）
- **684行目**: `repeat min(max(levslider/LEVSLIDER_MAX,0),1)` - 正方向閾値判定
- **753行目**: `repeat min(-1*min((levslider-LEVSLIDER_MAX)/LEVSLIDER_MAX,0),1)*(levslider<0)` - 負方向閾値判定
- **822-825行目**: levsliderの剰余リセット処理

### プログラム呼び出し階層図

```
start_select (src/scene/select.hsp)
    │
    ├─ 初期化 (46行目)
    │      └─ levnow=max(min(levnow,3),0)
    │
    ├─ 初期フォールバック (89-123行目)
    │      └─ minfostr.selnow.levm.0="" 時に隣接難易度探索
    │
    ├─ アナログ入力 (555-560行目)
    │      ├─ levslider値加算
    │      ├─ 正方向判定 (684-752行目) → levnow++
    │      ├─ 負方向判定 (753-821行目) → levnow--
    │      └─ 剰余リセット (822-825行目)
    │
    ├─ 右キー入力 (1217-1287行目)
    │      ├─ levnow++ or スキップ先へジャンプ
    │      ├─ SE再生 (sellid)
    │      ├─ フォールバック処理
    │      └─ *refreshscore, *refreshjacket, *refreshsonginfo
    │
    └─ 左キー入力 (1288-1358行目)
           ├─ levnow-- or スキップ先へジャンプ
           ├─ SE再生 (sellid)
           ├─ フォールバック処理
           └─ *refreshscore, *refreshjacket, *refreshsonginfo
```

### データフロー図

```
[入力]                          [処理]                              [出力]

右キー/左キー ──────────────▶ キー入力判定 ──────────────────▶ levnow変更
ジョイスティック ───────────▶     │                               levm更新
アナログ入力 ──────────────▶ levslider蓄積 ─────────────────▶ levnow変更
                                    │
minfostr[selnow][lev][0] ──▶ データ有無判定 ────────────────▶ フォールバック先levm
                                    │
levplustp/levminustp ──────▶ リピート制御 ──────────────────▶ 連続入力間隔制限
                                    │
                             SE再生(sellid) ───────────────▶ 音声出力
                             *refreshscore ─────────────────▶ スコア表示更新
                             *refreshjacket ────────────────▶ ジャケット表示更新
                             *refreshsonginfo ──────────────▶ 楽曲情報表示更新
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| select.hsp | `src/scene/select.hsp` | ソース | 難易度選択の入力処理・フォールバック・リフレッシュ |
| kshootmania.hsp | `kshootmania.hsp` | ソース | LEVSLIDER_MAX定数定義 |
