0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

戦闘・交渉・調査を別々に作らない――ResolutionTypeで共通化する【第17回】

0
Last updated at Posted at 2026-08-27

4Hえんぴつ|React+TypeScriptでWeb RPGを作る

第4章「戦闘・交渉・調査と状態管理」
第17回:戦闘・交渉・調査を別々に作らない――ResolutionTypeで共通化する

連載トップ・第0回はこちら

本連載では、設計・実装・テスト・改善にAIを活用しながら、React+TypeScriptによるWeb RPG開発を進めています。

1. 今回のテーマ

第3章では、街道で複数のイベントを発生させ、

街道へ入る
↓
イベントが起きる
↓
解決する
↓
次のイベントへ進む

という流れを作りました。

では、その「解決する」はどう作るのでしょうか。

今回のRPGでは、イベントに対して主に、

戦闘
交渉
調査

の3種類から行動を選びます。

最初に考えやすいのは、

戦闘システム
交渉システム
調査システム

をそれぞれ別々に作る方法です。

しかし現在の設計では、そうしていません。

3つを、

「イベントを解決する方法」

として共通化しています。


2. 3種類をそのまま別実装するとどうなるか

例えば、イベント画面で次のように分岐させることもできます。

以下は考え方を説明するため、実装を簡略化しています。

if (choice === "combat") {
  resolveCombat();
}

if (choice === "negotiation") {
  resolveNegotiation();
}

if (choice === "investigation") {
  resolveInvestigation();
}

最初は分かりやすいです。

しかし、それぞれの処理の中に、

能力値を取得する
難易度を取得する
成否を判定する
結果を表示する
HPや評判を更新する
イベントを解決済みにする

といった処理を書き始めると、似たコードが増えていきます。


3. 違うのは「何を使って解決するか」

戦闘・交渉・調査は、ゲーム上の意味は違います。

例えば、

戦闘
→ 力で危険を突破する

交渉
→ 相手と話して解決する

調査
→ 情報や痕跡から答えを探す

という違いがあります。

ただし、プログラム上の流れを見ると、

方法を選ぶ
↓
対応する能力値を見る
↓
難易度と比較する
↓
結果を決める
↓
ゲーム状態を更新する

という部分は共通しています。

そこで、

違う部分だけをデータにして、処理の流れは共通化する

ことにしました。


4. ResolutionTypeを作る

現在は、解決方法を次の型で表しています。

export type ResolutionType =
  | "combat"
  | "negotiation"
  | "investigation";

これによって、

const resolutionType: ResolutionType =
  "combat";

のように扱えます。

文字列ではありますが、任意の文字列ではありません。

例えば、

const resolutionType: ResolutionType =
  "magic";

と書けば、現在のResolutionTypeには存在しないためTypeScriptが検出できます。


5. 文字列を直接使うより意味を限定できる

もし、

type ResolutionType = string;

としてしまうと、

combat
Combat
battle
fight
negotiation
talk

のような表記揺れが入り込めます。

一方、

type ResolutionType =
  | "combat"
  | "negotiation"
  | "investigation";

なら、使える値を型として限定できます。

これは小さな違いですが、ゲーム内の参照箇所が増えるほど効いてきます。


6. AbilityKeyにも同じ考え方を使う

現在のMVPでは、解決方法と主能力を1対1で対応させています。

export type AbilityKey = ResolutionType;

つまり、

combat
↓
戦闘能力

negotiation
↓
交渉能力

investigation
↓
調査能力

という対応です。

能力値も、

export interface AbilitySet {
  combat: number;
  negotiation: number;
  investigation: number;
}

として持ちます。

これによって、選んだ解決方法から対応する能力値を扱いやすくなります。


7. ResolutionTypeから能力値を選べる

例えば、プレイヤーの能力値が、

const abilities = {
  combat: 4,
  negotiation: 2,
  investigation: 3,
};

だったとします。

以下は考え方を説明するため、実装を簡略化しています。

function getAbilityValue(
  abilities: AbilitySet,
  type: ResolutionType
) {
  return abilities[type];
}

すると、

getAbilityValue(abilities, "combat");
// 4

getAbilityValue(abilities, "investigation");
// 3

のように、同じ処理で取得できます。

戦闘用、交渉用、調査用に別々の関数を作る必要がありません。


8. GameEventにも処理を直接書かない

第12回では、GameEventが、

resolutionIds: string[];

を持つことを紹介しました。

イベントそのものには、

戦闘ならこの処理
交渉ならこの処理
調査ならこの処理

とは書きません。

代わりに、

GameEvent
↓
resolutionIds
↓
ResolutionDefinition

と参照します。

ResolutionDefinitionには、解決方法の種類や難易度、結果への参照などを持たせています。

正式設計では、1イベントにつき戦闘・交渉・調査を各1件持たせる構成です。


9. 「戦闘イベント」「交渉イベント」に分けない

この設計では、

戦闘イベント
交渉イベント
調査イベント

という分類にはしません。

一つの出来事に対して、

戦って解決する
話して解決する
調べて解決する

という複数の方法を用意します。

例えば、

街道を塞いでいる相手がいる

という一つの出来事でも、

戦闘
→ 力ずくで突破する

交渉
→ 話し合って通してもらう

調査
→ 別の通路や原因を探す

という違いを作れます。

イベントと解決方法を分けることで、同じイベントを別々の体験として扱えます。


10. 共通化すると結果処理にもつながる

解決方法をResolutionTypeとして共通化すると、その後の流れも共通化できます。

概念的には、

ResolutionTypeを選ぶ
↓
能力値を取得
↓
ResolutionDefinitionを取得
↓
難易度を見る
↓
判定
↓
成功・部分成功・失敗
↓
ResultDefinition
↓
GameStateを更新

となります。

戦闘だけ特別なルートを通るのではありません。

交渉も調査も、同じ「解決」の流れに乗せられます。


11. それでも戦闘らしさは失われない

共通化すると、

戦闘も交渉も全部同じになってしまわないか

という疑問もあります。

ここで共通化しているのは、

内部の判定フロー

です。

表示や演出まで同じにする必要はありません。

例えば、

戦闘
→ 剣や敵を意識した文章・演出

交渉
→ 会話や相手の反応を中心に表示

調査
→ 痕跡や発見内容を中心に表示

とできます。

つまり、

ゲームルール
↓
共通化

見せ方
↓
必要に応じて変える

という分け方です。


12. 共通化しすぎないことも重要

一方で、

共通化できるものは全部一つにする

わけでもありません。

将来、戦闘だけに、

ターン
装備
敵HP
状態異常

などが必要になれば、戦闘専用の仕組みを追加する可能性はあります。

現在のMVPで必要なのは、

戦闘
交渉
調査
↓
能力値と難易度によって結果を決める

という範囲です。

その範囲で共通化しています。

必要になる前から複雑な戦闘システムを作らない

という、プロトタイプから続けている方針でもあります。


13. 型を作ることが、設計を決めることになる

ResolutionTypeは短い型です。

export type ResolutionType =
  | "combat"
  | "negotiation"
  | "investigation";

コードだけを見ると数行しかありません。

しかし、この型によって、

このRPGでは
「戦闘」「交渉」「調査」を
同じ階層の解決方法として扱う

という設計が明確になります。

TypeScriptの型は、単なるエラー防止だけでなく、

ゲームのルールをコード上で表現するもの

としても使えます。


14. 今回のポイント

今回のポイントは3つです。

  • 戦闘・交渉・調査を別々のシステムとして作らず、ResolutionTypeで共通の「解決方法」として表現する
  • AbilityKeyResolutionTypeに対応させ、選択した方法から能力値を扱えるようにする
  • 共通化するのは判定の仕組みであり、文章や演出まで同じにする必要はない

最初は、

戦闘
交渉
調査

という三つの別機能に見えました。

しかし処理を整理すると、

方法を選ぶ
↓
能力を見る
↓
難易度と判定する
↓
結果を反映する

という共通構造が見えてきました。

次回は、この中の、

「能力を見る」

部分を詳しく扱います。


15. 次回

次回は、

戦闘・交渉・調査に能力値を対応させる――AbilitySetの設計【第18回】

です。

現在のプレイヤー能力は、

export interface AbilitySet {
  combat: number;
  negotiation: number;
  investigation: number;
}

という形で持っています。

次回は、

なぜ能力値をこの3種類にしたのか
ResolutionTypeとどう対応するのか
職業やスキルとどう分けるのか

を整理しながら、

「プレイヤーが何を得意としているか」をデータとしてどう持つか

を見ていきます。

 
 
この記事を最後まで読んでいただき、ありがとうございます。

♡ いいね」を押していただけるとうれしいです。これからの記事づくりの励みになります。

もし気に入っていただけましたら、フォローもよろしくお願いします。


前の記事

第16回 街道途中から再開するために、どこまで状態を持つか

次の記事

第18回 戦闘・交渉・調査に能力値を対応させる――AbilitySetの設計

連載トップ

第0回 連載の目的と開発ロードマップ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?