4Hえんぴつ|React+TypeScriptでWeb RPGを作る
第4章「戦闘・交渉・調査と状態管理」
第20回:成功だけでは足りなかった――成功・部分成功・失敗の3段階にした理由
本連載では、設計・実装・テスト・改善にAIを活用しながら、React+TypeScriptによるWeb RPG開発を進めています。
1. 今回のテーマ
前回までで、
プレイヤー側
AbilitySet
↓
どれくらい得意か
イベント側
ResolutionDefinition
↓
どれくらい難しいか
という判定の材料を用意しました。
次に必要なのは、
判定した結果をどう表現するか
です。
最初に思いつくのは、
成功
失敗
の2種類です。
しかしRPGを作っていくと、この2つだけでは表現しにくい場面が出てきました。
例えば、
依頼の目的は達成した。
ただし、HPを失った。
あるいは、
完全には解決できなかった。
それでも次へ進むことはできる。
こうした結果です。
そこで今回のRPGでは、
export type ResolutionOutcome =
| "success"
| "partial_success"
| "failure";
という3段階にしています。
2. 成功と失敗だけなら分かりやすい
2段階の判定には大きな利点があります。
単純です。
例えば、
能力値 >= 難易度
↓
成功
能力値 < 難易度
↓
失敗
とすれば、処理も説明もしやすくなります。
プロトタイプとして動作確認するだけなら、この形でも十分です。
しかし、ゲームとしてイベントを増やしていくと、
成功ではない
でも完全な失敗でもない
という結果が欲しくなりました。
3. 「失敗=何も得られない」だと進行が止まりやすい
例えば、
壊れた荷車を助ける
イベントがあるとします。
戦闘、交渉、調査のどれかを選び、失敗したとします。
そこで、
失敗
↓
荷車を助けられなかった
↓
依頼終了
としてしまうと、プレイヤーの一度の選択だけで進行が大きく途切れることがあります。
もちろん、
失敗によって依頼そのものが終わる
ゲーム設計もあります。
ただ、今回作っている小規模RPGでは、
失敗した
↓
何かを失う
↓
それでも物語は続く
という展開も作りたいと考えました。
4. そこで部分成功を入れる
partial_successは、
成功した部分と、うまくいかなかった部分が両方ある結果
として使います。
例えば、
荷車は救出できた
↓
しかし荷物の一部を失った
あるいは、
必要な情報は手に入った
↓
しかし評判が下がった
という結果です。
単純に、
成功の弱い版
というより、
目的はある程度達成したが、代償や不利益が残る状態
として考えています。
5. 3段階にすると結果の幅が広がる
例えば、一つの調査について、
success
↓
手掛かりを発見
追加情報も得る
partial_success
↓
手掛かりは発見
ただし時間やHPを失う
failure
↓
手掛かりを得られない
別の不利益が発生
という違いを作れます。
同じイベント、同じ「調査」という選択でも、結果によってその後の状態が変わります。
6. ResolutionOutcomeで型として限定する
現在の設計では、結果種別を次のように定義しています。
export type ResolutionOutcome =
| "success"
| "partial_success"
| "failure";
文字列を自由に書けるようにはしていません。
例えば、
const outcome: ResolutionOutcome =
"almost_success";
のような値は使えません。
これによって、
success
partial_success
failure
という3種類が、このRPGにおける正式な判定結果であることを型でも表現できます。
7. ResolutionDefinitionから3つの結果へつなぐ
前回紹介したResolutionDefinitionには、
successResultId: string;
partialSuccessResultId: string;
failureResultId: string;
があります。
つまり、
ResolutionDefinition
├─ success
├─ partial_success
└─ failure
それぞれについて、別の結果データへつなげます。
これにより、
戦闘で成功した結果
戦闘で部分成功した結果
戦闘で失敗した結果
を個別に定義できます。
8. 結果そのものはResultDefinitionへ分離する
結果は、ResolutionDefinitionへ直接書き込まず、
ResultDefinitionとして分離しています。
現在の設計は次の形です。
export interface ResultDefinition {
id: string;
resolutionId: string;
outcome: ResolutionOutcome;
title: string;
narrative: string;
effects: EventEffect;
questContinuation: QuestContinuation;
nextEventId?: string;
}
ここでは、
どういう結果だったか
何を表示するか
何が変化するか
依頼をどう続けるか
を定義します。
9. 「判定」と「結果」を分ける
ここまでの構造をまとめると、
AbilitySet
↓
プレイヤー能力
ResolutionDefinition
↓
方法・能力・難易度
判定
↓
success / partial_success / failure
ResultDefinition
↓
具体的に何が起きるか
となります。
つまり、
判定結果がpartial_successだった
ことと、
HPが2減って依頼は継続する
ことは別の情報です。
これらを一つにまとめていません。
10. 同じ部分成功でも内容はイベントごとに違う
例えば、二つのイベントで同じpartial_successになったとします。
イベントAでは、
目的達成
+
HP -2
かもしれません。
イベントBでは、
目的達成
+
評判 -1
かもしれません。
つまり、
partial_success
=
必ずHPを失う
というルールではありません。
partial_successは結果の分類であり、
具体的な影響はResultDefinition側で決めます。
11. 依頼の進み方も3段階とは別にする
結果には、
export type QuestContinuation =
| "continue"
| "complete"
| "complete_with_penalty"
| "fail";
という別の分類があります。
ここも重要です。
例えば、
ResolutionOutcome
success
だからといって、
必ず、
Quest
complete
になるとは限りません。
途中イベントなら、
success
↓
continue
もあります。
12. 「部分成功=依頼継続」とも限らない
同じように、
partial_success
だから、
continue
と固定するわけでもありません。
例えば最終イベントなら、
partial_success
↓
complete_with_penalty
という結果にできます。
つまり、
判定結果
ResolutionOutcome
と
依頼進行
QuestContinuation
も分けています。
13. 失敗してもゲーム全体を止めなくてよい
failureについても同じです。
失敗したからといって、
ゲームオーバー
しか選べないわけではありません。
例えば、
failure
↓
HPを失う
↓
評判も下がる
↓
依頼は失敗
↓
町へ戻る
という展開にできます。
別のイベントなら、
failure
↓
情報を得られない
↓
それでも依頼は継続
という設計も可能です。
判定結果とゲーム進行を一対一に固定しないことで、結果の組み合わせを増やせます。
14. 部分成功は「救済」だけではない
部分成功を入れると、
プレイヤーを失敗させないための救済措置
のようにも見えます。
しかし、それだけではありません。
例えば、
成功
↓
安全だが情報は少ない
部分成功
↓
ダメージを受けるが重要情報を得る
のようにすれば、部分成功にも別の意味を持たせられます。
つまり、
成功・部分成功・失敗を単純な点数順にしなくてもよい
わけです。
15. 結果を文章だけで表現しない
例えば結果文章に、
傷を負ったが、なんとか荷車を助け出した。
と書けば、人間には部分成功だと分かります。
しかしプログラム側で、
narrative.includes("なんとか")
のように判定するわけにはいきません。
そのため、
outcome: "partial_success"
として、結果分類を明示します。
ここでも、
プレイヤーへ見せる文章
と
プログラムが使う状態
を分離しています。
16. UIでも結果を扱いやすくなる
結果種別が型として決まっていれば、画面側でも、
以下は考え方を説明するため、実装を簡略化しています。
function getOutcomeLabel(
outcome: ResolutionOutcome
): string {
switch (outcome) {
case "success":
return "成功";
case "partial_success":
return "部分成功";
case "failure":
return "失敗";
}
}
のように扱えます。
また、
成功
部分成功
失敗
で表示方法を変える場合にも、文章解析ではなくoutcomeを基準にできます。
17. 3段階に増やした分、定義漏れには注意する
3段階にすると表現力は増えます。
一方で、必要な結果データも増えます。
現在の設計では、一つのResolutionDefinitionについて、
成功結果
部分成功結果
失敗結果
の3つが必要です。
どれか一つだけ定義し忘れると、
判定はできた
↓
しかし表示する結果がない
という状態になります。
そのため、正式設計でも1つのResolutionDefinitionにつき3結果を持つことを制約としています。
18. 2段階から始めて、必要になってから増やす
今回も、
最初から3段階にすべきだった
というより、
ゲームを広げる過程で3段階が必要になった
と考えています。
最初のプロトタイプでは、
成功
失敗
でも基本ループを確認できます。
その後、
完全成功ではない
↓
でも失敗として切り捨てたくない
ケースが見えてきたため、
partial_success
を明確な状態として扱うようにしました。
19. 3段階にすると「結果の先」も設計できる
結果が3種類になると、
単に結果画面を変えるだけではありません。
例えば、
success
↓
評判 +2
↓
依頼完了
partial_success
↓
評判 +1
↓
ゴールド減少
↓
ペナルティ付き完了
failure
↓
評判 -1
↓
依頼失敗
のように、その後のゲーム状態まで変えられます。
ここから、
HP
ゴールド
評判
フラグ
所持品
などの更新へつながります。
20. 今回のポイント
今回のポイントは3つです。
- 成功/失敗だけでは表現しにくい「目的は達成したが代償がある」状態を、
partial_successとして明示する -
ResolutionOutcomeは判定結果の分類であり、HP減少などの具体的な影響とは分離する - 判定結果と依頼の継続・完了・失敗も別々に管理する
第18回では、
プレイヤーの能力
第19回では、
解決方法ごとの難易度
を用意しました。
そして今回は、
能力
+
難易度
↓
成功
部分成功
失敗
という結果の形を決めました。
次は、この結果によって、
HP・評判・ゴールドなどを実際にどう変化させるか
へ進みます。
21. 次回
次回は、
判定結果でHP・評判・ゴールドを変える――ResultDefinitionとEventEffect【第21回】
です。
success、partial_success、failureが決まっただけでは、まだゲーム状態は変わっていません。
そこで、
export interface EventEffect {
hpDelta?: number;
goldDelta?: number;
reputationDelta?: number;
addFlags?: string[];
removeFlags?: string[];
addItemIds?: string[];
removeItemIds?: string[];
}
を使って、
HPが減る
ゴールドを得る
評判が上がる
フラグが立つ
アイテムを得る
といった変化をデータとして表現します。
次回は、
「結果の文章」と「ゲーム状態への影響」をどう分離するか
を中心に見ていきます。
この記事を最後まで読んでいただき、ありがとうございます。
「♡ いいね」を押していただけるとうれしいです。これからの記事づくりの励みになります。
もし気に入っていただけましたら、フォローもよろしくお願いします。
前の記事
第19回 判定条件をイベントごとに変える――ResolutionDefinitionで難易度をデータ化する
次の記事
第21回 判定結果でHP・評判・ゴールドを変える――ResultDefinitionとEventEffect