Imported from yui-taro/yuiyuiRPG (
AGENTS.md). Install upstream withnpx skills add yui-taro/yuiyuiRPG. Copyright stays with the author.
yuiyuiRPG 開発メモ
プロジェクトの目的
JavaScript、HTML、CSSの学習を兼ねた、ブラウザで動くターン制RPGです。
- エントリーポイント:
index.html→src/main.js - キャラクターデータ:
character.json - 実行時の読み込みは
fetch()を使うため、ブラウザで確認するときはローカルHTTPサーバー経由で開く。
フォルダごとの役割
| 場所 | 役割 |
|---|---|
index.html |
画面で使い続けるDOMの土台と、CSS・JavaScriptの読み込み |
src/main.js |
データと各クラスを生成し、Gameへ渡して起動するエントリーポイント |
src/core/Game.js |
ゲームの進行、フェーズ、プレイヤー・敵・連勝数の状態管理 |
src/ui/Screen.js |
DOMの取得と画面描画 |
src/ui/InputController.js |
ボタンのクリックを受け取り、data-actionなどをGameへ渡す |
src/systems/ |
戦闘、報酬、敵生成などのゲームルール |
src/models/ |
Character、Skillのデータと振る舞い |
src/data/CharacterLorder.js |
JSONデータをCharacterとSkillのインスタンスに変換。将来CharacterLoader.jsへ綴りを修正する候補 |
src/constants/GamePhase.js |
画面フェーズの定数 |
character.json |
キャラクターとスキルの初期データ |
images/ |
背景画像とキャラクター画像 |
style.css |
全画面共通・各画面の見た目 |
全体の関連と起動時の流れ
Gameが司令塔になり、入力、ゲームルール、画面表示をつなぐ。
index.html
→ style.cssを読み込む
→ src/main.jsを実行する
→ CharacterLorder.loadCharacters()
→ character.jsonをfetch()で読み込む
→ JSONのデータからSkillとCharacterを作る
→ Screen、InputController、BattleSystem、RewardSystemを作る
→ それらをGameへ渡す
→ game.start()
→ クリック処理を登録する
→ Screen.renderTitle()
依存関係は次のように考える。
InputController
→ ユーザーが何を押したかをGameへ伝える
Game
→ 現在の状態を持ち、次に何を起こすか決める
BattleSystem・RewardSystem・EnemyFactory
→ ゲームルールに従って値を変更する
Character・Skill
→ キャラクターとスキルのデータ・振る舞いを持つ
Screen
→ Gameから受け取った最新の値をDOMへ表示する
style.css
→ bodyの画面クラスに合わせて見た目を変える
HTML、Screen、CSSの分担
index.htmlに書くもの
- ページ全体の基本構造
- どの画面でも入れ物として使うDOM
- JavaScriptから取得する
id - CSSを読み込む
link - JavaScriptを読み込む
script
#game-info、#player-status、#enemy-status、#message-area、#button-areaなどは、画面が変わっても使い続ける入れ物。
Screen.jsに書くもの
- 現在の画面によって変わる文字
- プレイヤーや敵の現在HP・MP・能力値
- キャラクターやスキルの数によって増減するボタン
- ボタンの
data-action、data-character-index、data-skill-index - bodyに付ける画面用CSSクラス
style.cssに書くもの
- 色、余白、配置、背景、アニメーション
- 画面ごとの表示・非表示
基本の分担は次のとおり。
index.html = どこに表示するかという入れ物
Screen.js = 今、何を表示するか
style.css = どう見せるか
Game.js = 次に何が起きるか
ボタン入力がGameへ届く流れ
Screenが作るボタンにはdata-*属性を付ける。
<button
data-action="use-skill"
data-skill-index="2"
>
スキル
</button>
Game.start()が、入力を受け取ったときに実行するコールバック関数をInputControllerへ渡す。
this.inputController.setButtonClickHandler((input) => {
this.handleAction(input);
});
InputControllerは#button-areaのクリックをイベント委譲で受け取り、押されたボタンのdatasetから値を取り出す。
onButtonClick({
action: clickedButton.dataset.action,
characterIndex: clickedButton.dataset.characterIndex,
skillIndex: clickedButton.dataset.skillIndex,
});
このオブジェクトがGame.handleAction(input)へ渡され、actionに対応する処理が呼ばれる。
ボタンをクリック
→ InputControllerがdata-*を取得
→ onButtonClick(input)
→ Game.start()で渡したコールバック
→ Game.handleAction(input)
→ handleSkill()などの対応する処理
datasetから取得した値は基本的に文字列なので、配列番号として使う前にNumber()で数値へ変換する。
const index = Number(skillIndex);
JSONからモデルへの変換
character.jsonは保存用データ、CharacterとSkillはゲーム内部で扱うオブジェクト。
JSONではスネークケース、JavaScript内部ではキャメルケースを使用している。
cost_mp → costMp
hp_to_enemy → hpToEnemy
atk_to_self → atkToSelf
この変換はJavaScriptがスネークケースを使えないからではなく、JavaScript側の命名規則を統一し、外部データとゲーム内部を分けるために行う。
データ読込はクラスのstatic async loadCharacters()として実装しているため、newせずクラス名から呼ぶ。
const characters =
await CharacterLorder.loadCharacters();
画面クラスとsetScreenClass()
title-screenやreward-screenはJavaScriptの変数ではなく、bodyに付けるCSSクラス名の文字列。
this.setScreenClass("reward-screen");
実行後のブラウザ上のDOMは次のようになる。
<body class="reward-screen">
CSSでは、対応するクラスを持つbodyの見た目を定義する。
body.reward-screen {
/* 報酬画面の見た目 */
}
現在の対応関係は次のとおり。
| ゲーム状態 | bodyの画面クラス |
|---|---|
GamePhase.TITLE |
title-screen |
GamePhase.PLAYER_SELECT |
player-select-screen |
GamePhase.BATTLE |
battle-screen |
GamePhase.REWARD |
reward-screen |
GamePhase.GAME_OVER |
battle-screenを再利用 |
Screen.setScreenClass(screenClass)は、古い画面クラスを全て外してから、引数で受け取った新しい画面クラスを1つ付ける共通処理。
setScreenClass(screenClass) {
document.body.classList.remove(
"title-screen",
"player-select-screen",
"battle-screen",
"reward-screen",
);
document.body.classList.add(screenClass);
}
reward-screenなどが事前にJavaScriptで定義されている必要はない。単なる文字列であり、対応するCSSがあれば見た目が適用される。クラス内のメソッドは記述位置に関係なく呼び出せるが、共通処理なのでconstructor()の後、各render...()の前に置く。
Game.phaseとbodyの画面クラスは別の役割を持つ。
Game.phase = 今どの操作を許可するか
bodyのCSSクラス = 今どの見た目を適用するか
createStatusHtml()による共通化
プレイヤーと敵のステータスHTMLは同じ形なので、Screen.createStatusHtml(character)へまとめている。
createStatusHtml(character) {
return `
<h2>${character.name}</h2>
<p>Lv.${character.level}</p>
<p>HP:${character.hp} / ${character.maxHp}</p>
<p>MP:${character.mp} / ${character.maxMp}</p>
<p>ATK:${character.atk}</p>
<p>DEF:${character.def}</p>
`;
}
renderBattle()からプレイヤーと敵の両方に使う。
this.playerStatus.innerHTML =
this.createStatusHtml(player);
this.enemyStatus.innerHTML =
this.createStatusHtml(enemy);
createStatusHtml()はHTML文字列を作って返すだけで、実際にDOMへ入れて表示するのはrenderBattle()。
CSSカードの共通化方針
キャラクター選択の.character-select-cardと、報酬選択の.reward-cardには共通する見た目が多い。共通部分は.menu-cardへまとめ、各ボタンにクラスを2つ付ける方針。
<button class="menu-card character-select-card">
<button class="menu-card reward-card">
役割は次のように分ける。
.menu-card
= 枠線、角丸、文字色、flex、cursor、transitionなどの共通部分
.character-select-card
= キャラクターカード固有の高さ、余白、背景、影
.reward-card
= 報酬カード固有の高さ、余白、背景、影
共通CSSを先に、個別CSSを後に書く。個別クラスは必要な違いだけを上書きする。
.menu-card {
display: flex;
flex-direction: column;
justify-content: center;
border: 2px solid #c99a4b;
border-radius: 10px;
color: #fff4cf;
cursor: pointer;
}
.character-select-card {
min-height: 145px;
padding: 20px;
}
.reward-card {
min-height: 150px;
padding: 24px;
}
同じhover・activeを使う場合は、.menu-card:hover、.menu-card:activeへまとめる。
現在のゲーム進行
タイトル
→ キャラクター選択
→ 戦闘
→ 勝利:報酬選択 → 次の戦闘
→ 敗北:タイトルへ戻る
Game.phase は GamePhase の値で管理する。戦闘中以外で攻撃・スキルが実行されないよう、各操作の最初にフェーズを確認する。
実装済みの重要な仕様
敗北後
Game.handleGameOver()はGamePhase.GAME_OVERにし、Screen.renderGameOver()を呼ぶ。Screen.renderGameOver()は通常の戦闘表示を使った後、操作ボタンを「タイトルへ戻る」ボタンだけに置き換える。data-action="restart-game"はGame.handleAction()で受け取り、restartGame()がプレイヤー、敵、連勝数をリセットしてタイトル画面を描画する。
通常攻撃とスキル
- 通常攻撃と、成功したスキル使用の後の共通処理は
Game.handleBattleAfterPlayerAction(playerMessage)にまとめている。 - このメソッドは、プレイヤー行動後の勝敗判定、敵ターン、敵ターン後の勝敗判定、戦闘画面の更新を担当する。
- MP不足などスキル使用に失敗した場合は敵ターンに進めず、
playerResult.messageだけを表示する。
戦闘メッセージ
- プレイヤーと敵の行動メッセージは、
Game.handleBattleAfterPlayerAction()の最後で次の形にする。
`${playerMessage}\n${enemyResult.message}`
Screen.renderBattle()はtextContentでメッセージを表示する。HTMLとして解釈させる必要はない。style.cssのbody.battle-screen .message-areaにはwhite-space: pre-line;がある。これにより上記の改行文字が画面上でも改行として表示される。
連勝数
- 連勝数の状態は
Game.winStreak。 - 勝利時に
handleVictory()で1増やす。 Screen.renderBattle(player, enemy, message, winStreak)は4番目の引数を受け取り、#game-infoに連勝数:<数値>と表示する。renderBattle()を呼ぶ全ての場所でthis.winStreakを渡す。初回戦闘、通常攻撃・スキル後、MP不足時、報酬後の次戦闘、ゲームオーバーを確認する。- 戦闘画面では
#game-infoを隠さない。style.cssのbody.battle-screen #game-infoでロゴの下に配置している。
キャラクター画像
- キャラクターPNGは透過情報を持つ。画像周囲を紺色にしないため、
.character-areaに戦闘用の不透明背景を追加しない。 - 敵画像は
style.cssのbody.battle-screen #enemy-image { transform: scaleX(-1); }により左右反転して表示する。画像ファイルをキャラクターごとに複製する必要はない。 images/assassin.pngは左右反転済み。元画像はimages/assassin-original.pngに保存されている。
画面を修正するときのルール
Game.jsは「何が起きるか」を決める。DOM操作は基本的に書かない。Screen.jsは「どう表示するか」を担当する。- JavaScriptから見た目を大量に指定せず、画面フェーズ用のbodyクラス(例:
battle-screen)とCSSに任せる。 - ボタンには
data-actionを付け、InputControllerのイベント委譲で扱う。
コードを読みやすく保つルール
- 変数名・関数名から役割が分かる名前を使う。配列は複数形、真偽値は
is.../can...を目安にする。 - クラス名は大文字始まり(PascalCase)、関数・変数は小文字始まり(camelCase)に統一する。現在の
CharacterLorderはCharacterLoaderへ綴りを修正する候補。 - 字下げは半角スペース2つ、
if (、,の後の空白などの書式を統一する。 - 同じ処理が複数の場所にあり、一緒に修正する必要がありそうなら、役割が分かる関数へ切り出す。
- UI用の文字列とゲームルールを混ぜず、数値バランスは必要に応じて定数へまとめる。
- コードを読めば分かる「何をしているか」のコメントは減らし、コードだけでは分からない「なぜそうするか」をコメントに残す。
- 行数を減らすことだけを目的にせず、処理の流れが追いやすいことを優先する。
詳しい学習用ガイドは clean-code-guide.md を参照する。
整理済みの内容
- 未参照だった
Screen.leaveBattle()を削除した。 renderPlayerSelect()と重複していたScreen.leaveTitle()を削除した。- 未使用だった
handleVictory(battleMessage)の引数を削除し、handleVictory()にした。 Screen.renderReward()の未使用だったplayer引数を削除した。Screen.setScreenClass()を追加し、画面クラスの切り替えを共通化した。Screen.createStatusHtml()を追加し、プレイヤーと敵のステータスHTMLを共通化した。- 画面の表示・非表示は、JavaScriptの
style.displayよりbodyの画面クラスとCSSへ任せる形に整理した。 Game.handleBattleAfterPlayerAction()に、プレイヤーや敵が自傷スキルで倒れた場合の判定を追加した。src/data/fileChange.jsから、静的メソッドを持つデータ読込クラスへ名前を変更した。- 魔法使いに重複していた
Mana Drainを1件に整理した。 - スキル報酬では習得済みスキルを候補から除外し、候補がない場合も報酬選択へ戻れるようにした。
読込エラー対応を追加した経緯
character.jsonを読み込めない場合、以前はsrc/main.jsのトップレベルで処理が停止し、画面上には初期の仮文字だけが残っていた。
現在は、ゲーム開始前にScreenとInputControllerを作り、その後の読込・ゲーム生成・開始をtry...catchで囲んでいる。
src/main.js
→ ScreenとInputControllerを作る
→ try内でCharacterLorder.loadCharacters()
→ 読込成功:Gameを作ってgame.start()
→ 読込失敗:catchへ移動
→ Consoleへ詳しいエラーを出す
→ Screen.renderLoadError()で案内と再読み込みボタンを表示
CharacterLorderで残す確認
初心者が作る小規模RPGであるため、データの全項目を細かく検証する処理は追加しない。現在は、よく起こる次の4点だけを確認する。
fetch("./character.json")の結果が成功しているかresponse.json()でJSONへ変換できるか- 読み込んだデータが2件以上の配列か
- キャラクター名に対応する画像パスが
getImagePath()に設定されているか
エラーが起きる代表例は次のとおり。
character.jsonがない・ファイル名が違う
→ response.okがfalse
→ throw new Error(...)
JSONのカンマや引用符が間違っている
→ response.json()が自動的にエラーを発生
データが配列ではない、またはキャラクターが1人以下
→ 配列と件数の確認でthrow new Error(...)
character.jsonへ新しいキャラクターを追加したが、
getImagePath()のimageMapへ追加していない
→ 画像パスが空になりthrow new Error(...)
これらのエラーはawait CharacterLorder.loadCharacters()を通してsrc/main.jsへ戻り、catchが受け取る。
try {
const characters =
await CharacterLorder.loadCharacters();
// Gameを作って開始
} catch (error) {
console.error(
"ゲームデータの読み込みに失敗しました。",
error,
);
screen.renderLoadError();
}
Screen.renderLoadError()はタイトル画面の見た目を使い、利用者向けに次の内容を表示する。
ゲームデータを読み込めませんでした。
通信状態やデータの内容を確認して、再読み込みしてください。
[再読み込み]
再読み込みボタンにはdata-action="reload-game"を付ける。エラー時はGameを作成できていないため、src/main.jsがInputControllerへ専用のコールバックを渡す。
再読み込みボタン
→ InputControllerがreload-gameを取得
→ src/main.jsのコールバック
→ window.location.reload()
→ ページを最初から読み込み直す
画像マップにパスがあっても、実際のPNGファイルが存在しない場合まではこの読込確認では検出しない。
スキル報酬失敗時の流れ
勝利報酬で「敵のスキルを奪う」を選んだとき、Screen.renderSkillReward(enemy, player, errorMessage)が敵のスキルから習得済みのものを除外する。
報酬画面
→ 「敵のスキルを奪う」
→ Game.showSkillReward()
→ Screen.renderSkillReward()
→ player.hasSkill(skill.name)で習得済みか確認
→ 未習得スキルだけをボタンとして表示
未習得スキルが1件もない場合は「習得できる新しいスキルがありません。」と表示し、data-action="back-to-reward"を持つ「報酬選択へ戻る」ボタンを表示する。
報酬選択へ戻る
→ InputController
→ Game.handleAction()
→ Game.showReward()
→ 通常の報酬選択画面を再表示
スキルボタンを押すと、RewardSystem.applyStealSkill()が対象スキルの存在と同名スキルの習得状況を確認する。失敗時はsuccess: falseとmessageを返し、Game.handleSkillReward()がそのメッセージをScreen.renderSkillReward()へ渡して表示する。成功時はスキルを追加し、次の戦闘へ進む。
現在の要確認・整理候補
CharacterLorderはLoaderの綴りではないため、ファイル名・クラス名・importをCharacterLoaderへそろえる。Game.handleBattleAfterPlayerAction()のプレイヤー行動後に、enemyDefeatedの同じ条件が2回ある。古いthis.handleVictory(playerMessage)側を整理する。- 相打ちの場合を勝利・敗北・引き分けのどれにするか決める。現在の
getBattleResult()はプレイヤー敗北を先に判定する。 Skill.descriptionは読み込んでいるが画面で未使用。スキル説明として表示するか、不要ならデータ・モデル・読込処理から削除する。- 現在の
BattleSystemはCharacter.changeAtk()とCharacter.changeDef()を呼んでいるが、Characterに該当メソッドがない。ATK・DEFを変化させるスキルでTypeErrorになるため、直接Math.max(0, ...)で更新するか、Characterへメソッドを追加して役割をそろえる。 .character-select-cardと.reward-cardの共通部分を.menu-cardへまとめる。style.css内の重複したbody.battle-screen .battle-areaを1つにまとめる。BattleSystem.normalAttack()が返すdamageは現在呼び出し側で未使用。攻撃エフェクトなどで使わないなら返却値から削除できる。character.jsonへ画像パスを持たせると、キャラクター追加時に画像対応表を別ファイルで直す必要がなくなる。index.htmlのJavaScriptですぐ上書きされる仮文字は、初期表示のちらつきを避けるため空にできる。
確認手順
変更後は最低限、次を確認する。
- JavaScript構文:
node --check src/core/Game.jsとnode --check src/ui/Screen.js - タイトル → キャラクター選択 → 戦闘へ進める
- 通常攻撃、スキル成功、MP不足のスキルを試す
- プレイヤー勝利、報酬選択、次の戦闘を試す
- プレイヤー敗北後、「タイトルへ戻る」を試す
- プレイヤーまたは敵が自傷スキルで倒れた場合の勝敗を確認する
- 戦闘画面で連勝数、プレイヤー・敵の向き、メッセージ改行を確認する
- bodyに複数の画面クラスが同時に残っていないことを確認する
- ブラウザの開発者ツールConsoleにエラーがないことを確認する
作業時の注意
- 現在の作業ツリーには未コミットのユーザー変更がある。依頼と無関係な変更を戻したり、削除したりしない。
- 画像を置き換えるときは、依頼が明確な場合を除き元ファイルをバックアップする。
images/suraimu.pngの削除など、既存の差分は他の作業による可能性があるため、勝手に復元しない。
