JavaScript 都道府県対抗 戦国シミュレーションゲームをつくる(1)の続きです。
Contents
グローバル変数と定数
グローバル変数と定数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// [開発], [徴兵], [調査], [侵攻/移動]ボタンが表示される要素 const $actions = document.getElementById('actions'); const $development = document.getElementById('development'); // [開発]ボタン const $conscription = document.getElementById('conscription'); // [徴兵]ボタン const $research = document.getElementById('research'); // [調査]ボタン const $invasion = document.getElementById('invasion'); // [侵攻/移動]ボタン // 対象の県を選択するための各地方のボタンが表示される要素 const $selectKen = document.getElementById('select-ken'); // ターンがきている県の状態が表示される要素 const $currentKenInformation = document.getElementById('current-ken-information'); // 自分が侵攻されているときの選択肢([静観], [敵の状態を表示], [決死の全軍突撃])が表示される要素 const $battleActions1 = document.getElementById('battle-actions-1'); // 自分が侵攻しているときの選択肢([静観], [敵の状態を表示], [全軍で県庁に突入]が表示される要素 const $battleActions2 = document.getElementById('battle-actions-2'); const unitOfHeis = 1000; // 兵の単位(1回の徴兵で増える数と戦闘による喪失の数) let turnsCount = 0; // ターン数(単位:ヵ月) let playerOwnerName = ''; // 自分が担当する大名の名前 // 効果音 const fightSound = new Audio('./sounds/fight.mp3'); const noticeSound = new Audio('./sounds/notice.mp3'); const perishedSound = new Audio('./sounds/perished.mp3'); const gameclearSound = new Audio('./sounds/gameclear.mp3'); const gameoverSound = new Audio('./sounds/gameover.mp3'); const bgm = new Audio('./sounds/bgm.mp3'); // ボリューム(効果音とBGM用に2つつくる) let volume1 = 0.3; let volume2 = 0.1; |
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。
まずBGMをエンドレスで再生できるようにします。使用する音源は開始から118秒まで再生して最初に戻るようにするといい感じに聞こえるのでそのようにしています。各自で他の音源を使用するときは適切な設定にしてください。
[START]ボタンがクリックされたらゲーム開始です。ボタンがクリックされたらゲーム開始の処理をするようにイベントリスナを追加します。
1 2 3 4 5 6 7 8 9 10 |
window.onload = () => { // BGMをエンドレスで再生(118秒で最初に戻る) setInterval(() => { if(bgm.currentTime > 1 * 60 + 58){ bgm.currentTime = 0; } }, 500); initVolume(); document.getElementById('start').onclick = async() => onclickGameStart(); } |
ゲーム開始の処理
[START]ボタンがクリックされたらゲームを開始する処理を示します。
まず[START]ボタンを非表示にします。そして前回既出のinitKens関数でKenオブジェクトを初期化します。そのあとプレイヤーに担当する県を選択させてゲームループに入ります。ゲームが終わったらゲームループから抜けるので現在ターンがきている県の情報を非表示にして、[START]ボタンを再表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 |
async function onclickGameStart(){ const $startButtons = document.getElementById('start-buttons'); $startButtons.style.display = 'none'; initKens(); await selectPlayerKen(); // 後述 await gameLoop(); // 後述 $currentKenInformation.style.display = 'none'; $startButtons.style.display = 'block'; } |
ボリュームの初期設定
initVolume関数はボリューム調整に関する初期設定をおこないます。レンジスライダーを操作したときにボリューム調整できるようにするのと[テスト]ボタンをクリックしたときに設定したボリュームで効果音とBGMを再生できるようにしているだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
function initVolume(){ const $volumeRange1 = document.getElementById('volume-range1'); const $volumeValue1 = document.getElementById('volume-value1'); const testSound1 = new Audio('./sounds/fight.mp3'); const $volumeRange2 = document.getElementById('volume-range2'); const $volumeValue2 = document.getElementById('volume-value2'); const testSound2 = new Audio('./sounds/bgm.mp3'); function setVolume1(){ $volumeValue1.innerHTML = volume1; fightSound.volume = volume1; noticeSound.volume = volume1; perishedSound.volume = volume1; gameclearSound.volume = volume1; gameoverSound.volume = volume1; testSound1.volume = volume1; } function setVolume2(){ $volumeValue2.innerHTML = volume2; bgm.volume = volume2; testSound2.volume = volume2; } $volumeRange1.oninput = () => { volume1 = Number($volumeRange1.value); setVolume1(); } $volumeRange2.oninput = () => { volume2 = Number($volumeRange2.value); setVolume2(); } document.getElementById('volume-test-1').onclick = () => { testSound1.play(); setTimeout(() => { testSound1.pause(); }, 3000); } document.getElementById('volume-test-2').onclick = () => { testSound2.play(); setTimeout(() => { testSound2.pause(); }, 3000); } setVolume1(); $volumeRange1.value = volume1; setVolume2(); $volumeRange2.value = volume2; } |
効果音の再生と停止
効果音再生/停止に関する処理を示します。
playNoticeSound関数はメッセージが表示されるときの通知音を鳴らします。
1 2 3 4 |
function playNoticeSound(){ noticeSound.currentTime = 0; noticeSound.play(); } |
playPerishedSound関数は大名が滅亡(自他同様)したときの効果音を鳴らします。
1 2 3 4 |
function playPerishedSound(){ perishedSound.currentTime = 0; perishedSound.play(); } |
playFightSound関数は戦闘中の音(刀がぶつかり合うような音)を鳴らします。時間を指定して鳴らす場合とstopFightSound関数が実行されるまで繰り返し再生される場合の2つを用意しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
let playFightSoundInterval = null; function playFightSound(ms){ fightSound.currentTime = 0; fightSound.play(); if(ms != undefined){ // ms が undefined でないならその時間だけ再生する setTimeout(() => { fightSound.pause(); }, ms); } else { // そうでないならエンドレス(音源が14秒のものを使っているので第二引数に14 * 1000を指定している) playFightSoundInterval = setInterval(() => { fightSound.currentTime = 0; }, 14 * 1000); } } function stopFightSound(){ clearInterval(playFightSoundInterval); fightSound.pause(); } |
sleep関数は引数の時間だけ待機します。
1 2 3 |
async function sleep(ms){ await new Promise(resolve => setTimeout(resolve, ms)); } |
ゲーム開始の処理
ゲームを開始したらプレイヤーに自分が担当する大名を選択させます。その処理を示します。
県名が書かれているボタンをもつ要素を選択してユーザーがボタンをクリックしたら選択できるようにしているのですが、北海道と沖縄県以外は○○地方の下の階層にあります。また偵察コマンドでも同じ要素を使い回すのでその処理のぶんだけ複雑になっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
async function selectPlayerKen(){ await new Promise(resolve => { playNoticeSound(); $selectKen.style.display = 'block'; // 表示するナビゲーションの文字を担当する大名を選択するときと偵察コマンドでは変える // いまは $navi1 を表示させて $navi2 は非表示 const $navi1 = document.getElementById('select-ken-navi-1'); const $navi2 = document.getElementById('select-ken-navi-2'); $navi1.style.display = 'block'; $navi2.style.display = 'none'; document.getElementById('return-from-select-ken').style.display = 'none'; // いまは必要ないので非表示 // ○○地方のボタンがクリックされたときのイベントリスナを追加 for(let i = 1; i <= 7; i++){ document.getElementById(`btn-region-${i}`).onclick = () => { playNoticeSound(); $selectKen.style.display = 'none'; const $selectKenX = document.getElementById(`select-ken-${i}`); $selectKenX.style.display = 'block'; // [戻る]ボタンがクリックされたときのイベントリスナを追加 document.getElementById(`return-from-select-ken-${i}`).onclick = () => { playNoticeSound(); $selectKenX.style.display = 'none'; $selectKen.style.display = 'block'; } } } // 各県のボタンがクリックされたときのイベントリスナを追加 // 選択された県の情報を表示して、それでよいか確認のボタンを表示する for(let i = 0; i < 47; i++){ document.getElementById(`btn-ken-${i+1}`).onclick = () => { playNoticeSound(); const kenName = document.getElementById(`btn-ken-${i+1}`).innerHTML const ken = kens.find(_ => _.KenName == kenName); const html = ` <div class = "inner"> <p>県名:${ken.KenName}</p> <p>大名:${ken.OwnerName} 氏</p> <p>兵数:${ken.HeisCount.toLocaleString()}</p> <p>兵糧:${ken.FoodsCount.toLocaleString()}</p> <p>土地:${ken.Value.toLocaleString()}</p> <button id = "ok-ken-select" class = "buttons">OK</button> <button id = "cancel-ken-select" class = "buttons">キャンセル</button> </div> `; const $selectKenInformation = document.getElementById('select-ken-information'); $selectKenInformation.innerHTML = html; $selectKenInformation.style.display = 'block'; // [キャンセル]が選択された場合は$selectKenInformationを非表示にすることで // 各県選択ボタンが再表示される document.getElementById('cancel-ken-select').onclick = () => { playNoticeSound(); $selectKenInformation.style.display = 'none'; } // [OK]が選択された場合は選択した大名名をplayerOwnerNameにセット // そのあと県選択にかんする要素をすべて非表示にする document.getElementById('ok-ken-select').onclick = async() => { playNoticeSound(); playerOwnerName = ken.OwnerName; for(let i = 1; i <= 7; i++) document.getElementById(`select-ken-${i}`).style.display = 'none'; $selectKen.style.display = 'none'; $navi1.style.display = 'none'; // 次回から必要になるのでここで表示設定をしておく document.getElementById('return-from-select-ken').style.display = 'block'; $navi2.style.display = 'block'; const html = ` <div class = "inner"> <p style = "margin-top: 80px;">${ken.OwnerName} さん、${ken.KenName}を拠点に全国制覇にむけて頑張りましょう。</p> </div> `; $selectKenInformation.innerHTML = html; await sleep(1000); $selectKenInformation.style.display = 'none'; await sleep(1000); resolve(''); } } } }); } |
ゲームループ
担当する大名の名前が決まったらゲームループに入ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
async function gameLoop(){ bgm.currentTime = 0; bgm.play(); turnsCount = 0; while(true){ const year = Math.floor(turnsCount / 12) + 2024; const month = turnsCount % 12 + 1; document.getElementById('year').innerHTML = `${year} 年 ${month} 月`; // 10月になったら各県の兵糧を増やす if(month == 10){ playNoticeSound(); await showMessage(['10月です。各地で米の収穫がおこなわれています。'], 2000); // 後述 for(let i = 0; i < kens.length; i++){ kens[i].FoodsCount += kens[i].Value * 10000; } } // kensをシャッフルした配列をつくる const arr = []; for(let i = 0; i < kens.length; i++) arr.push(kens[i]); for(let i = 0; i < arr.length; i++) arr[i].Random = Math.random(); arr.sort((a, b) => a.Random - b.Random); // 各県にターンを回す for(let i = 0; i < arr.length; i++){ const ken = arr[i]; showCurKenInformation(ken); // 各県の情報を表示する(後述) await sleep(200); // プレイヤーの県とそうでない県でそれぞれ処理をする const ownerName = ken.OwnerName; if(ownerName == playerOwnerName) await myKenTurn(ken); // 後述 else await otherKenTurn(ken); // 後述 // ゲームオーバーとクリア判定 if(checkGameOver()){ // 後述 await onGameOver(ken); // 後述 return; } if(checkGameClear()){ // 後述 await onGameClear(ken); // 後述 return; } // プレイヤー以外の大名が滅亡した場合 if(checkPerished(ownerName)){ // 後述 playPerishedSound(); await showMessage([`${ownerName}氏は滅亡しました!!`], 2000); } } turnsCount++; } } |
メッセージの表示
showMessage関数はゲームの進行にかんするメッセージを表示するためのものです。これは普段は非表示になっているmessage要素のinnerHTMLプロパティに文字列をセットして一時的に表示させます。
showButtonがtrueのときはボタンをクリックするまで表示されたままになります。そうでない場合はmsで指定された時間がすぎると自動的に非表示になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
async function showMessage(texts, ms, showButton){ let html = '<div class = "inner">'; for(let i = 0; i < texts.length; i++) html += `<p>${texts[i]}</p>`; if(showButton === true) html += `<button id = "btn-hide-message" class = "buttons">次へ</button>`; html += '</div>'; const $message = document.getElementById('message'); $message.innerHTML = html; $message.style.display = 'block'; if(showButton !== true){ await sleep(ms); $message.style.display = 'none'; } if(showButton === true){ await new Promise(resolve => { document.getElementById('btn-hide-message').onclick = () => { $message.style.display = 'none'; resolve(''); } }); } } |
県情報の表示
showCurKenInformation関数は各県の情報を表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function showCurKenInformation(ken){ const html = ` <div class = "inner"> <p>県名:${ken.KenName}</p> <p>大名:${ken.OwnerName} 氏</p> <p>兵数:${ken.HeisCount.toLocaleString()}</p> <p>兵糧:${ken.FoodsCount.toLocaleString()}</p> <p>土地:${ken.Value.toLocaleString()}</p> </div> `; $currentKenInformation.innerHTML = html; $currentKenInformation.style.display = 'block'; } |
ゲームオーバー・クリア時の処理
checkGameClear関数はゲームクリアのときtrueを返します。ゲームクリアとは各県のOwnerNameがすべてplayerOwnerNameになったときです。
1 2 3 4 5 6 |
function checkGameClear(){ if(kens.filter(ken => ken.OwnerName != playerOwnerName).length == 0) return true; else return false; } |
checkPerished関数は引数で渡された名前の大名が滅亡したときにtrueを返します。大名が滅亡したとは各県のOwnerNameのすべてが引数の文字列ではなくなった場合で、他県に侵攻している軍のOwnerNameのなかにも引数の文字列が存在しなくなった場合です。
1 2 3 4 5 6 7 8 9 10 |
function checkPerished(name){ if(kens.filter(ken => ken.OwnerName == name).length > 0) return false; for(let i = 0; i < kens.length; i++){ if(kens[i].Invaders.filter(invader => invader.OwnerName == name).length > 0) return false; } return true; } |
checkGameOver関数はゲームオーバーのときtrueを返します。ゲームオーバーになっているかどうかはcheckPerished関数にplayerOwnerNameを渡せばわかります。
1 2 3 |
function checkGameOver(){ return checkPerished(playerOwnerName); } |
ゲームオーバーになったときにおこなわれる処理を示します。この場合は県の情報を更新したものを表示し、BGMを停止し、滅亡の効果音を鳴らします。そのあと自分自身の滅亡とゲームオーバーを示すメッセージを表示するとともにゲームオーバーの効果音を鳴らします。
1 2 3 4 5 6 7 8 9 |
async function onGameOver(ken){ showCurKenInformation(ken); bgm.pause(); await sleep(500); playPerishedSound(); await showMessage([`${playerOwnerName}氏は滅亡しました!!`], 2000); gameoverSound.play(); await showMessage([`ゲームオーバー!!`], 0, true); } |
ゲームクリアのときにおこなわれる処理を示します。この場合は県の情報を更新したものを表示し、BGMを停止し、ゲームクリアの効果音とメッセージを表示します。
1 2 3 4 5 6 7 8 |
async function onGameClear(ken){ showCurKenInformation(ken); bgm.pause(); await sleep(500); gameclearSound.play(); await showMessage([`${playerOwnerName}氏は全国統一に成功しました!!`], 2000); await showMessage([`ゲームクリア!!`], 0, true); } |