
【ゲーム】JavaScript:55 ドラクエ風RPG
「🗡️ ドラクエ風RPG 🏰」は、12×12マスの広大なフィールドを舞台に、勇者(🧑🎤)を操作して城(🏰)にいるドラゴンを討伐し、世界に平和を取り戻すシミュレーションRPG風ブラウザゲームです。フィールドには森・荒地・山岳・火山など多彩な地形がランダム生成され、教会(⛪)ではHP・MPが全回復。モンスターと戦い、経験値を稼いでレベルアップしながら進みます。
遊び方・操作方法
- タイトル画面の「スタート」ボタンをクリックするとゲームが開始。
- フィールド画面では、画面右下の矢印ボタンか、マップにフォーカスした状態でカーソルキーで勇者を移動。
- 教会(⛪)に入ると自動でHP・MPが全回復。
- 歩くごとに一定率でモンスターとエンカウント。教会・城のマスは必ずイベント発生。
- 戦闘中は「たたかう」「まほう」「かいふく」「にげる」ボタンを押してアクションを実行。
- HPが0になるとゲームオーバー。北の城(🏰)でドラゴンを倒すとエンディング。
ルール
- フィールド(12×12)を自由に移動し、最終的に北端の城🏰へ到達するとボス戦。
- 教会(⛪)でHP・MPを全回復できる。教会はマップ内に3カ所ランダム配置。
- 森や山岳など地形によってモンスター遭遇率が変化。
- モンスターを倒すと経験値を獲得し、一定の経験値でレベルアップ(HP/MP全回復&ステータス強化)。
- ボス「ドラゴン」を倒すとクリア。HP0で敗北するとゲームオーバー。
🎮ゲームプレイ
以下のリンク先から実際にプレイできます。
55 ドラクエ風RPG
素材のダウンロード
以下のリンクから使用する素材をダウンロードできます。
効果音:魔王魂
doraquest_title.png | doraquest_bg.png |
---|---|
![]() | ![]() |
ゲーム画面イメージ

プログラム全文(doraquest.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>🗡️ ドラクエ風RPG 🏰</title>
<style>
body {
margin: 0;
padding: 0;
background: url('doraquest_bg.png') no-repeat center center fixed;
background-size: cover;
font-family: 'Yu Gothic', 'Meiryo', sans-serif;
}
.container {
width: 1000px;
margin: 40px auto 0 auto;
background: rgba(24, 32, 48, 0.92);
border-radius: 18px;
box-shadow: 0 8px 32px #2228;
min-height: 680px;
position: relative;
overflow: hidden;
color: #fff;
padding-bottom: 32px;
}
.title-img {
display: block;
margin: 32px auto 30px auto;
max-width: 420px;
}
h1 {
text-align: center;
font-size: 2.3em;
margin-top: 8px;
margin-bottom: 10px;
letter-spacing: 2px;
text-shadow: 2px 2px 5px #123, 0 0 10px #3339;
}
h2 {
text-align: center;
font-size: 1.33em;
margin-top: 10px;
margin-bottom: 16px;
letter-spacing: 2px;
text-shadow: 1px 1px 4px #0009;
}
.story-title {
text-align: center;
font-size: 1.11em;
margin-bottom: 8px;
color: #ffd700;
}
.story-content, .rules-content {
text-align: left;
margin: 0 auto 24px auto;
width: 93%;
background: rgba(255,255,255,0.13);
border-radius: 8px;
padding: 12px 18px;
font-size: 1.04em;
color: #fffbea;
letter-spacing: 0.3px;
}
.rules-title {
text-align: center;
font-size: 1.01em;
margin-bottom: 8px;
color: #84ffd7;
}
.btn, .battle-btn, .enemy-turn-btn {
display: inline-block;
margin: 0;
padding: 17px 0;
border-radius: 12px;
font-size: 1.22em;
border: none;
background: linear-gradient(90deg,#317efb,#45eab1);
color: #fff;
font-weight: bold;
letter-spacing: 2px;
cursor: pointer;
box-shadow: 0 4px 18px #2347;
transition: transform 0.07s, background 0.13s;
min-width: 192px;
width: 196px;
max-width: 220px;
height: 60px;
margin-bottom: 0;
margin-top: 0;
margin-right: 7px;
margin-left: 7px;
text-align: center;
}
.btn:hover, .battle-btn:hover, .enemy-turn-btn:hover {
transform: scale(1.04);
background: linear-gradient(90deg,#45eab1,#317efb);
}
.enemy-turn-btn[disabled] {
cursor: default;
background: linear-gradient(90deg, #cccccc 70%, #aaaaaa 100%);
color: #234;
box-shadow: 0 2px 6px #1117;
}
.center-btn-area {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
margin-top: 20px;
margin-bottom: 44px;
}
.game-area {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
margin: 10px 0 0 0;
width: 100%;
min-height: 480px;
}
.map-area {
display: flex;
flex-direction: column;
align-items: center;
width: 480px;
}
.map {
width: 480px; height: 480px;
background: #2238cc99;
border: 3px solid #90caf9;
border-radius: 10px;
display: grid;
grid-template: repeat(12, 1fr) / repeat(12, 1fr);
position: relative;
margin: 0 auto;
}
.cell {
width: 40px; height: 40px;
box-sizing: border-box;
border: 1px solid #98bbf6;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.18em;
background: rgba(255,255,255,0.09);
position: relative;
user-select: none;
}
.player {
font-size: 1.7em;
color: #fffb8b;
text-shadow: 1px 1px 8px #fbc, 0 0 7px #ffd;
font-weight: bold;
}
.boss {
font-size: 1.7em;
color: #fa2;
text-shadow: 1px 1px 8px #b20;
}
.enemy {
font-size: 1.42em;
color: #f56;
text-shadow: 1px 1px 6px #b20;
font-weight: bold;
}
.castle {
font-size: 1.4em;
color: #cdf;
text-shadow: 1px 1px 8px #fff, 0 0 10px #225;
}
.church {
font-size: 1.17em;
color: #fdf;
text-shadow: 0 0 10px #ff9;
}
.map-status {
width: 215px;
min-width: 215px;
margin: 0 0 0 16px;
background: #1d2345f4;
border-radius: 12px;
padding: 18px 24px 14px 24px;
font-size: 1.19em;
color: #ffe;
box-shadow: 0 2px 14px #0134;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
line-height: 1.73;
letter-spacing: 1.2px;
font-weight: 600;
}
.map-status b {
color: #ffd700;
font-size: 1.34em;
font-weight: bold;
letter-spacing: 2px;
display: inline-block;
margin-bottom: 7px;
}
.next-exp {
color: #6fffd7;
margin-top: 10px;
font-size: 1.04em;
letter-spacing: 1.2px;
white-space: nowrap;
}
.message-box {
display: flex;
align-items: center;
justify-content: center;
min-height: 72px;
margin: 36px auto 0 auto;
width: 93%;
height: 80px;
padding: 8px 22px;
background: rgba(30,30,50,0.97);
border-radius: 12px;
font-size: 1.42em;
box-shadow: 0 2px 13px #023b;
color: #fffd;
line-height: 1.8;
letter-spacing: 0.6px;
text-align: center;
font-weight: 500;
position: relative;
left: 50%;
top: 0;
transform: translateX(-50%);
justify-content: center;
}
.move-btns-bar {
width: 480px;
margin: 20px auto 0 auto;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
gap: 0;
}
/* ▼▼▼ここから移動ボタン用サイズ調整&アニメーション▼▼▼ */
.move-btn-side {
width: 120px;
min-width: 120px;
max-width: 120px;
height: 96px;
font-size: 1.12em;
border-radius: 8px;
border: none;
background: #37dbb6;
color: #023;
font-weight: bold;
cursor: pointer;
box-shadow: 0 2px 8px #36ab;
transition: transform 0.14s cubic-bezier(.52,2,.7,1), box-shadow 0.14s cubic-bezier(.52,2,.7,1);
text-align: center;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
.move-btns-center-col {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 120px;
min-width: 120px;
max-width: 120px;
height: 96px;
margin: 0;
padding: 0;
gap: 8px;
}
.move-btn-vertical {
width: 120px;
min-width: 120px;
max-width: 120px;
height: 40px;
font-size: 1.12em;
border-radius: 8px;
border: none;
background: #37dbb6;
color: #023;
font-weight: bold;
cursor: pointer;
box-shadow: 0 2px 8px #36ab;
transition: transform 0.14s cubic-bezier(.52,2,.7,1), box-shadow 0.14s cubic-bezier(.52,2,.7,1);
text-align: center;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
.move-btn-side:hover, .move-btn-vertical:hover {
transform: scale(1.12);
box-shadow: 0 6px 28px #2ac7cc;
z-index: 1;
}
/* ▲▲▲ここまで移動ボタン▼▲▲ */
.move-btn:active, .move-btn:focus { outline: none; }
.battle-panel {
margin: 14px auto 0 auto;
width: 97%;
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: flex-end;
}
.battle-vs-row {
width: 440px;
margin: 0 auto 3px auto;
font-size: 2.0em;
letter-spacing: 1.3px;
color: #fff8a1;
text-align: center;
font-weight: bold;
text-shadow: 2px 2px 9px #000a, 0 0 9px #ffd;
display: block;
margin-bottom: 10px;
margin-top: 3px;
}
.battle-title {
text-align: center;
font-size: 2.4em;
font-weight: bold;
color: #ffe95c;
letter-spacing: 2.5px;
margin-bottom: 5px;
margin-top: 3px;
text-shadow: 1px 1px 12px #013a, 0 0 4px #fff7;
}
.battle-center-area {
width: 440px;
margin: 0 auto;
background: #1d2345fa;
border-radius: 12px;
box-shadow: 0 2px 16px #0215;
padding: 8px 10px 17px 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.battle-status-area {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 8px;
font-size: 1.07em;
gap: 15px;
}
.battle-status-panel {
flex: 1 1 0;
background: #243063cc;
border-radius: 9px;
padding: 11px 12px 7px 13px;
margin: 2px;
min-width: 154px;
text-align: left;
color: #fff8f5;
}
.battle-status-panel h3 {
margin: 0 0 6px 0;
font-size: 1.08em;
color: #ffe677;
}
.game-over-msg {
margin: 60px 0 24px 0;
font-size: 2.0em;
font-weight: bold;
color: #ff7171;
text-align: center;
text-shadow: 2px 2px 10px #0009;
}
.center-btn {
display: block;
margin: 32px auto;
text-align: center;
}
@media (max-width: 1050px) {
.container {width: 98vw;}
.battle-center-area {width: 98vw;}
.map-area {width: 99vw;}
.map {width: 97vw;}
.move-btns-bar {width: 97vw;}
}
</style>
</head>
<body>
<div class="container" id="app"></div>
<audio id="field_bgm" src="maoudamashii-rpg_field.mp3" loop></audio>
<audio id="battle_bgm" src="maoudamashii-rpg_battle.mp3" loop></audio>
<script>
const MAP_SIZE = 12;
const PLAYER_NAME = "勇者";
const PLAYER_EMOJI = "🧑🎤";
const BOSS_ICON = "🐉";
const CASTLE_ICON = "🏰";
const CHURCH_ICON = "⛪";
const FIELD_TYPE = {
"🌳": "low", "🌲": "low", "🪨": "low",
"🏜️": "mid", "⛰️": "mid", "🌋": "mid"
};
const ENEMY_TABLE = [
{emoji:"🪱", name:"ミミズン", hp:7, atk:3, def:1, mp:0, exp:6, rank:1, lv:1},
{emoji:"🦖", name:"コドラゴン", hp:15,atk:7, def:3, mp:0, exp:20, rank:3, lv:3},
{emoji:"🧟", name:"ゾンビー", hp:23,atk:10,def:5, mp:0, exp:40, rank:4, lv:5},
{emoji:"🧌", name:"トロール", hp:38,atk:14,def:8, mp:0, exp:70, rank:5, lv:7},
];
const BOSS = {emoji:"🐉", name:"ドラゴン", hp:50,atk:17,def:11, mp:30,exp:0,rank:10,lv:9};
const LV_TABLE = [
{lv:1, need:0, maxHp:20, atk:5, def:2, mp:4},
{lv:2, need:12, maxHp:25, atk:7, def:3, mp:6},
{lv:3, need:34, maxHp:31, atk:9, def:5, mp:10},
{lv:4, need:65, maxHp:37, atk:13, def:8, mp:16},
{lv:5, need:110, maxHp:45, atk:18, def:12, mp:20},
{lv:6, need:180, maxHp:52, atk:24, def:15, mp:28},
{lv:7, need:270, maxHp:62, atk:32, def:21, mp:36},
{lv:8, need:410, maxHp:80, atk:39, def:28, mp:40},
{lv:9, need:580, maxHp:90, atk:43, def:32, mp:47},
];
let state = {
screen: 'title',
map: [],
player: null,
enemy: null,
boss: null,
msg: '',
turn: 'player',
lastAction: '',
cleared: false,
bossDefeated: false,
castle: null,
churchs: [],
battleType: '',
};
function playFieldBgm() {
let bgm = document.getElementById('field_bgm');
let bbgm = document.getElementById('battle_bgm');
if(bbgm) {bbgm.pause(); bbgm.currentTime=0;}
if(bgm) {
bgm.currentTime = 0;
bgm.volume = 0.45;
bgm.loop = true;
bgm.play();
}
}
function stopFieldBgm() {
let bgm = document.getElementById('field_bgm');
if(bgm) {bgm.pause(); bgm.currentTime=0;}
}
function playBattleBgm() {
let bgm = document.getElementById('battle_bgm');
let fbgm = document.getElementById('field_bgm');
if(fbgm) {fbgm.pause(); fbgm.currentTime=0;}
if(bgm) {
bgm.currentTime = 0;
bgm.volume = 0.48;
bgm.loop = true;
bgm.play();
}
}
function stopBattleBgm() {
let bgm = document.getElementById('battle_bgm');
if(bgm) {bgm.pause(); bgm.currentTime=0;}
}
function showTitle() {
stopFieldBgm(); stopBattleBgm();
state.screen = 'title';
state.bossDefeated = false;
const app = document.getElementById('app');
app.innerHTML = `
<img class="title-img" src="doraquest_title.png" alt="タイトル画像">
<h1>🗡️ ドラクエ風RPG 🏰</h1>
<h2>〜 新たな冒険の始まり 〜</h2>
<div class="story-title">物語のあらすじ</div>
<div class="story-content">
世界は恐ろしいドラゴンの出現により混乱の渦に包まれた。<br>
人々は平和を取り戻すため、若き勇者に最後の希望を託した。<br>
勇者は広大なフィールドを旅し、教会で力を蓄えながら数々のモンスターたちに挑む。<br>
目指すは北にそびえる城に棲む伝説のボス「ドラゴン」。<br>
果たして勇者は世界に再び平和をもたらすことができるだろうか…?
</div>
<div class="rules-title">📖 ゲームのルール</div>
<div class="rules-content">
・フィールド(12x12)を冒険し、勇者(🧑🎤)を操作します。<br>
・森や荒地、山岳、火山など多彩な地形が広がります。<br>
・「⛪」教会ではHP/MPが全回復!<br>
・マスによって敵との遭遇率が変化します。<br>
・モンスターを倒すと経験値獲得、レベルアップで強くなります。<br>
・北端の「🏰」で伝説のボスと対決し、世界に平和を!
</div>
<div class="center-btn-area">
<button class="btn" onclick="startGame()">スタート</button>
</div>
`;
}
function startGame() {
let map = [];
for(let y=0; y<MAP_SIZE; y++) {
let row = [];
for(let x=0; x<MAP_SIZE; x++) {
let r = Math.random();
let emoji = "";
if (r < 0.20) emoji = ["🌳","🌲","🪨"][Math.floor(Math.random()*3)];
else if (r < 0.30) emoji = ["🏜️","⛰️","🌋"][Math.floor(Math.random()*3)];
row.push(emoji);
}
map.push(row);
}
let castle = {x: Math.floor(MAP_SIZE/2), y: 0};
map[castle.y][castle.x] = CASTLE_ICON;
let churchs = [];
while(churchs.length < 3) {
let cx = Math.floor(Math.random()*MAP_SIZE);
let cy = Math.floor(Math.random()*MAP_SIZE*0.8+MAP_SIZE*0.2);
if(map[cy][cx] !== CASTLE_ICON && map[cy][cx] !== CHURCH_ICON) {
map[cy][cx] = CHURCH_ICON;
churchs.push({x:cx, y:cy});
}
}
let lv = 1;
let lvinfo = LV_TABLE[lv-1];
let player = {
x: Math.floor(MAP_SIZE/2), y: MAP_SIZE-1,
hp: lvinfo.maxHp,
maxHp: lvinfo.maxHp,
mp: lvinfo.mp,
maxMp: lvinfo.mp,
atk: lvinfo.atk,
def: lvinfo.def,
lv: lv,
exp: 0,
};
let boss = Object.assign({}, BOSS);
boss.hp = boss.hp;
boss.maxHp = boss.hp;
boss.mp = boss.mp;
boss.maxMp = boss.mp;
state.screen = 'game';
state.map = map;
state.player = player;
state.enemy = null;
state.boss = boss;
state.msg = 'さあ、冒険の始まりだ!北の城🏰を目指そう。';
state.cleared = false;
state.castle = castle;
state.churchs = churchs;
state.turn = 'player';
state.lastAction = '';
state.bossDefeated = false;
playFieldBgm();
renderGame();
}
function renderGame() {
const app = document.getElementById('app');
let mapHtml = '';
for(let y=0; y<MAP_SIZE; y++) {
for(let x=0; x<MAP_SIZE; x++) {
let cellClass = '';
if(state.player.x===x && state.player.y===y) cellClass='player';
else if(state.castle.x===x && state.castle.y===y) cellClass='castle';
else if(state.map[y][x]===CHURCH_ICON) cellClass='church';
mapHtml += `<div class="cell ${cellClass}">
${state.player.x===x && state.player.y===y ? PLAYER_EMOJI :
(state.castle.x===x && state.castle.y===y ? CASTLE_ICON :
(state.map[y][x]===CHURCH_ICON ? CHURCH_ICON : state.map[y][x]))}
</div>`;
}
}
let lvinfo = LV_TABLE[state.player.lv-1];
let nextExp = (state.player.lv < LV_TABLE.length)
? (LV_TABLE[state.player.lv].need - state.player.exp)
: 'MAX!';
let status = `
<div class="map-status">
<b>${PLAYER_NAME}:Lv${state.player.lv}</b><br>
HP:${state.player.hp}/${state.player.maxHp}<br>
MP:${state.player.mp}/${state.player.maxMp}<br>
EXP:${state.player.exp}<br>
ATK:${state.player.atk}<br>
DEF:${state.player.def}
<div class="next-exp">次のレベルまで:${nextExp}</div>
</div>
`;
app.innerHTML = `
<h1>🗡️ ドラクエ風RPG 🏰</h1>
<div class="game-area">
<div class="map-area">
<div class="map" tabindex="0" id="map">${mapHtml}</div>
<div class="move-btns-bar" style="display:flex;flex-direction:row;justify-content:space-between;width:480px;margin:20px auto 0 auto;">
<button class="move-btn-side" onclick="movePlayer('left')">←左</button>
<div class="move-btns-center-col">
<button class="move-btn-vertical" onclick="movePlayer('up')">↑上</button>
<button class="move-btn-vertical" onclick="movePlayer('down')">↓下</button>
</div>
<button class="move-btn-side" onclick="movePlayer('right')">右→</button>
</div>
</div>
${status}
</div>
<div class="message-box">${state.msg}</div>
`;
document.getElementById('map').focus();
document.getElementById('map').onkeydown = (e) => {
switch(e.key) {
case "ArrowUp": movePlayer('up'); break;
case "ArrowDown": movePlayer('down'); break;
case "ArrowLeft": movePlayer('left'); break;
case "ArrowRight": movePlayer('right'); break;
}
}
}
function movePlayer(dir) {
if(state.screen!=='game') return;
let {x,y} = state.player;
switch(dir) {
case 'up': if(y>0) y--; break;
case 'down': if(y<MAP_SIZE-1) y++; break;
case 'left': if(x>0) x--; break;
case 'right': if(x<MAP_SIZE-1) x++; break;
}
let cell = state.map[y][x];
let msg = '';
if(state.castle.x===x && state.castle.y===y) {
stopFieldBgm();
startBattle('boss');
return;
}
if(cell===CHURCH_ICON) {
let beforeHp=state.player.hp, beforeMp=state.player.mp;
state.player.hp = state.player.maxHp;
state.player.mp = state.player.maxMp;
msg = `⛪ 教会で祈りを捧げた…HP/MPが全回復した!(HP+${state.player.hp-beforeHp}、MP+${state.player.mp-beforeMp})`;
}
let encRate = 0.08;
if(FIELD_TYPE[cell]==='low') encRate=0.17;
else if(FIELD_TYPE[cell]==='mid') encRate=0.29;
if(Math.random() < encRate) {
stopFieldBgm();
startBattle('enemy');
return;
}
state.player.x = x;
state.player.y = y;
state.msg = msg ? msg : 'どこへ進もうか?(北の城を目指そう!)';
renderGame();
}
function startBattle(type) {
playBattleBgm();
state.screen = 'battle';
state.battleType = type;
state.turn = 'player';
let enemy;
if(type==='enemy') {
let candidate = ENEMY_TABLE.filter(e => e.lv <= state.player.lv+1);
let idx = Math.floor(Math.random()*candidate.length);
enemy = Object.assign({}, candidate[idx]);
enemy.hp = enemy.hp; enemy.maxHp = enemy.hp;
enemy.mp = enemy.mp; enemy.maxMp = enemy.mp;
} else {
enemy = Object.assign({}, state.boss);
enemy.hp = enemy.maxHp;
enemy.mp = enemy.maxMp;
}
state.enemy = enemy;
state.msg = `${enemy.name}が現れた!`;
renderBattle();
}
function renderBattle() {
const app = document.getElementById('app');
let enemy = state.enemy;
let player = state.player;
let vsRow = `<div class="battle-vs-row">${PLAYER_EMOJI} VS ${enemy.emoji}</div>`;
let statusPanels = `
<div class="battle-status-area">
<div class="battle-status-panel">
<h3>${PLAYER_NAME}(あなた)</h3>
<div>HP: ${player.hp} / ${player.maxHp}</div>
<div>MP: ${player.mp} / ${player.maxMp}</div>
<div>ATK: ${player.atk}</div>
<div>DEF: ${player.def}</div>
<div>Lv: ${player.lv}</div>
<div>EXP: ${player.exp}</div>
</div>
<div class="battle-status-panel">
<h3>${enemy.name}</h3>
<div>HP: ${enemy.hp>0?enemy.hp:0} / ${enemy.maxHp}</div>
<div>MP: ${enemy.mp!==undefined?enemy.mp:0} / ${enemy.maxMp!==undefined?enemy.maxMp:0}</div>
<div>ATK: ${enemy.atk}</div>
<div>DEF: ${enemy.def}</div>
</div>
</div>
`;
let battleBtns = '';
if(state.turn==='player') {
battleBtns = `
<div style="display:flex;justify-content:center;gap:20px;margin-bottom:12px;">
<button class="battle-btn" onclick="attack()">たたかう🗡</button>
<button class="battle-btn" onclick="castMagic()">まほう🔥</button>
</div>
<div style="display:flex;justify-content:center;gap:20px;">
<button class="battle-btn" onclick="heal()">かいふく❤️</button>
<button class="battle-btn" onclick="runAway()">にげる🏃</button>
</div>
`;
} else {
battleBtns = `
<div style="display:flex;justify-content:center;gap:20px;">
<button class="enemy-turn-btn" disabled>敵のターン...</button>
</div>
`;
}
app.innerHTML = `
<h1>🗡️ ドラクエ風RPG 🏰</h1>
<div class="battle-center-area">
<div class="battle-title">⚔️ 戦闘 🪄</div>
${vsRow}
${statusPanels}
<div class="action-btns">${battleBtns}</div>
</div>
<div class="message-box">${state.msg}</div>
`;
if(state.turn==='enemy' && enemy.hp>0 && player.hp>0) {
setTimeout(enemyAttack, 1150);
}
if(enemy.hp<=0) {
setTimeout(battleWin, 1050);
} else if(player.hp<=0) {
setTimeout(showGameOver, 1050, false);
}
}
function attack() {
if(state.screen!=='battle'||state.turn!=='player')return;
let p=state.player, e=state.enemy;
let dmg = Math.max(1, p.atk-e.def + Math.floor(Math.random()*3));
e.hp -= dmg;
state.msg = `勇者のこうげき!${e.name}に${dmg}ダメージを与えた!`;
state.turn='enemy';
renderBattle();
}
function castMagic() {
if(state.screen!=='battle'||state.turn!=='player')return;
let p=state.player, e=state.enemy;
if(p.mp<3) {
state.msg="MPが足りない!";
renderBattle();
return;
}
let dmg = Math.max(3, p.atk+5-e.def + Math.floor(Math.random()*6));
e.hp -= dmg;
p.mp -= 3;
state.msg = `勇者は まほう🔥 を唱えた!${e.name}に${dmg}のダメージ!(MP-3)`;
state.turn='enemy';
renderBattle();
}
function heal() {
if(state.screen!=='battle'||state.turn!=='player')return;
let p=state.player;
if(p.mp<2) {
state.msg="MPが足りない!";
renderBattle();
return;
}
let healVal = Math.floor(p.maxHp*0.4)+Math.floor(Math.random()*4);
p.hp = Math.min(p.maxHp, p.hp+healVal);
p.mp -= 2;
state.msg = `勇者は回復魔法❤️を使った!HPが${healVal}回復!(MP-2)`;
state.turn='enemy';
renderBattle();
}
function runAway() {
if(state.screen!=='battle'||state.turn!=='player')return;
if(Math.random()<0.7) {
state.msg="勇者は うまく逃げだした!";
setTimeout(()=>{
stopBattleBgm();
playFieldBgm();
state.screen='game'; state.msg="無事に逃げられた。"; renderGame();
},900);
} else {
state.msg="逃げようとしたが失敗した!";
state.turn='enemy';
renderBattle();
}
}
function enemyAttack() {
let p=state.player, e=state.enemy;
if(p.hp<=0||e.hp<=0)return;
let doMagic = (e.mp && e.mp>2 && Math.random()<0.32);
let dmg;
if(doMagic) {
dmg = Math.max(3, e.atk+5-p.def + Math.floor(Math.random()*6));
p.hp -= dmg;
e.mp -= 3;
state.msg = `${e.name}のまほう攻撃!勇者は${dmg}ダメージを受けた!(敵MP-3)`;
} else {
dmg = Math.max(1, e.atk-p.def + Math.floor(Math.random()*3));
p.hp -= dmg;
state.msg = `${e.name}のこうげき!勇者は${dmg}ダメージを受けた!`;
}
state.turn='player';
renderBattle();
}
function battleWin() {
let enemy=state.enemy;
let player=state.player;
if(state.battleType==='boss') {
stopBattleBgm();
showEnding();
return;
}
let getExp=enemy.exp;
player.exp += getExp;
state.msg = `${enemy.name}を倒した!経験値${getExp}を獲得。`;
let lvup=false;
while(player.lv<LV_TABLE.length && player.exp>=LV_TABLE[player.lv].need) {
player.lv++;
let info=LV_TABLE[player.lv-1];
player.maxHp=info.maxHp; player.atk=info.atk; player.def=info.def; player.maxMp=info.mp;
player.hp=player.maxHp; player.mp=player.maxMp;
lvup=true;
}
setTimeout(()=>{
stopBattleBgm();
playFieldBgm();
state.screen='game';
state.msg = lvup
? `レベルアップ!${PLAYER_NAME}はLv${player.lv}になった!HP/MP全回復!`
: 'さあ、冒険の続きだ!';
renderGame();
}, 900);
}
function showGameOver(win) {
stopBattleBgm(); stopFieldBgm();
state.screen='gameover';
const app = document.getElementById('app');
let msg = `💀 勇者は倒れてしまった…<br>世界は闇に包まれた…`;
app.innerHTML = `
<h1>🗡️ ドラクエ風RPG 🏰</h1>
<div class="game-over-msg">${msg}</div>
<button class="btn center-btn" onclick="showTitle()">タイトル画面に戻る</button>
`;
}
function showEnding() {
stopBattleBgm(); stopFieldBgm();
state.screen='ending';
const app = document.getElementById('app');
let msg = `
🎉 伝説のドラゴンを打ち倒し、世界に平和が戻った!<br>
人々は勇者の偉業を讃え、再び笑顔を取り戻した。<br>
ありがとう、勇者よ!<br>
その冒険と勇気は、永遠に語り継がれるだろう…。
`;
app.innerHTML = `
<h1>🗡️ ドラクエ風RPG 🏰</h1>
<div class="game-over-msg">${msg}</div>
<button class="btn center-btn" onclick="showTitle()">タイトル画面に戻る</button>
`;
}
showTitle();
</script>
</body>
</html>
アルゴリズムの流れ
手順 | 処理内容 |
---|---|
1 | showTitle() でタイトル画面を描画し、スタートボタンに startGame() を紐付け |
2 | startGame() でマップ・教会・城・プレイヤー・ボスを初期化、フィールドBGM再生 |
3 | renderGame() でフィールド&ステータス&移動UIを描画 |
4 | movePlayer(dir) で移動処理→教会回復 or 城ボス戦 or ランダムエンカウント判定 |
5 | startBattle(type) で戦闘ステートに切り替え、バトルBGM再生 |
6 | renderBattle() で戦闘UIを描画。プレイヤー/敵ターン判定でアクション実行 |
7 | ダメージ計算や魔法・回復・逃走を各関数で処理後、再度 renderBattle() で反映 |
8 | 敵HP≤0 → battleWin() → 経験値付与・レベルアップ・フィールド復帰 |
9 | プレイヤーHP≤0 → showGameOver() で敗北画面 |
10 | ボス討伐時 → showEnding() でエンディング画面 |
関数の詳細
関数名 | 機能概要 | 詳細説明 |
---|---|---|
showTitle() | タイトル画面の描画・BGM停止 | 物語紹介・ルール表示・スタートボタンを設置。BGMを止めてタイトルモードへ移行。 |
startGame() | ゲーム初期化処理 | マップおよびランドマーク(城・教会)の配置、プレイヤー/ボス初期ステータス設定、フィールドBGM開始。 |
renderGame() | フィールド画面の描画 | マップセルをグリッド配置し、ステータスパネル・移動ボタン・メッセージを表示。 |
movePlayer(dir) | プレイヤー移動・イベント判定 | 移動範囲チェック、教会回復・城ボス戦・モンスターエンカウント判定、メッセージ更新。 |
startBattle(type) | 戦闘開始処理 | 戦闘ステータス初期化(敵選択)、BGM切り替え、戦闘モードへ移行。 |
renderBattle() | 戦闘画面の描画 | プレイヤー・敵ステータス表示、行動ボタン配置、敵ターン自動実行判定。 |
attack() | 通常攻撃処理 | プレイヤー攻撃ダメージ計算・敵HP減少・メッセージ更新・ターン交替。 |
castMagic() | 魔法攻撃処理 | MP消費チェック・ダメージ計算・MP減少・メッセージ更新・ターン交替。 |
heal() | 回復魔法処理 | MP消費チェック・HP回復量計算・HP/MP更新・メッセージ更新・ターン交替。 |
runAway() | 逃走処理 | 逃走成功率判定→成功時フィールド復帰/失敗時敵ターン。 |
enemyAttack() | 敵ターン攻撃処理 | 敵の通常攻撃 or 魔法攻撃判定・ダメージ計算・HP/MP更新・メッセージ更新・ターン交替。 |
battleWin() | 戦闘勝利処理 | 経験値付与・レベルアップ判定・ステータス更新・フィールドBGM再開・フィールド復帰。 |
showGameOver() | ゲームオーバー画面描画 | BGM停止後、敗北メッセージとタイトル戻りボタンを表示。 |
showEnding() | エンディング画面描画 | BGM停止後、クリアメッセージとタイトル戻りボタンを表示。 |
改造のポイント
- アイテム実装:マップ上にポーションや武器のアイコンを配置し、拾うとHP回復や攻撃力アップなどの効果を追加。
- スキルツリー:レベルアップ時にスキルポイントを消費して「二段斬り」「範囲魔法」などを習得できる仕組みを導入。
- 多様なマップイベント:宝箱やトラップ、NPCとの会話イベントをランダムで発生させ、冒険をより立体的に。
- 難易度選択:敵ステータス倍率や遭遇率を変更する「Easy/Normal/Hard」モードを追加し、幅広い層が楽しめるように。
- オートセーブ機能:ブラウザのローカルストレージにプレイ状況を保存し、リロード後も続きからプレイ可能に。
アドバイス
まずは基本システムを安定稼働させ、次に「アイテム」「スキル」「イベント」を段階的に追加すると、ユーザーの飽きが来にくくなります。ローカルストレージを活用したオートセーブや、シェア可能な「最速クリアタイム」機能を実装すると、リプレイ性と拡散性が向上します!