【ゲーム】JavaScript:55 ドラクエ風RPG

 「🗡️ ドラクエ風RPG 🏰」は、12×12マスの広大なフィールドを舞台に、勇者(🧑‍🎤)を操作して城(🏰)にいるドラゴンを討伐し、世界に平和を取り戻すシミュレーションRPG風ブラウザゲームです。フィールドには森・荒地・山岳・火山など多彩な地形がランダム生成され、教会(⛪)ではHP・MPが全回復。モンスターと戦い、経験値を稼いでレベルアップしながら進みます。

遊び方・操作方法

  1. タイトル画面の「スタート」ボタンをクリックするとゲームが開始。
  2. フィールド画面では、画面右下の矢印ボタンか、マップにフォーカスした状態でカーソルキーで勇者を移動。
  3. 教会(⛪)に入ると自動でHP・MPが全回復。
  4. 歩くごとに一定率でモンスターとエンカウント。教会・城のマスは必ずイベント発生。
  5. 戦闘中は「たたかう」「まほう」「かいふく」「にげる」ボタンを押してアクションを実行。
  6. HPが0になるとゲームオーバー。北の城(🏰)でドラゴンを倒すとエンディング。

ルール

  • フィールド(12×12)を自由に移動し、最終的に北端の城🏰へ到達するとボス戦。
  • 教会(⛪)でHP・MPを全回復できる。教会はマップ内に3カ所ランダム配置。
  • 森や山岳など地形によってモンスター遭遇率が変化。
  • モンスターを倒すと経験値を獲得し、一定の経験値でレベルアップ(HP/MP全回復&ステータス強化)。
  • ボス「ドラゴン」を倒すとクリア。HP0で敗北するとゲームオーバー。

🎮ゲームプレイ

以下のリンク先から実際にプレイできます。

55 ドラクエ風RPG

素材のダウンロード

以下のリンクから使用する素材をダウンロードできます。

効果音:魔王魂

doraquest_title.pngdoraquest_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>

アルゴリズムの流れ

手順処理内容
1showTitle() でタイトル画面を描画し、スタートボタンに startGame() を紐付け
2startGame() でマップ・教会・城・プレイヤー・ボスを初期化、フィールドBGM再生
3renderGame() でフィールド&ステータス&移動UIを描画
4movePlayer(dir) で移動処理→教会回復 or 城ボス戦 or ランダムエンカウント判定
5startBattle(type) で戦闘ステートに切り替え、バトルBGM再生
6renderBattle() で戦闘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」モードを追加し、幅広い層が楽しめるように。
  • オートセーブ機能:ブラウザのローカルストレージにプレイ状況を保存し、リロード後も続きからプレイ可能に。

アドバイス
まずは基本システムを安定稼働させ、次に「アイテム」「スキル」「イベント」を段階的に追加すると、ユーザーの飽きが来にくくなります。ローカルストレージを活用したオートセーブや、シェア可能な「最速クリアタイム」機能を実装すると、リプレイ性と拡散性が向上します!