CSSの基礎

CSSって?

  • HTMLの見た目(色・余白・レイアウト・アニメ)を指定するスタイル言語。
  • 重要キーワード:Cascade(優先順位の連なり)、Specificity(詳細度)、Inheritance(継承)。

1) CSSの書き方・読み込み方

<!-- 外部ファイル(推奨) -->
<link rel="stylesheet" href="styles.css">

<!-- ページ内(学習用) -->
<style>
  p { color: #333; }
</style>

<!-- インライン(基本非推奨) -->
<p style="color:#333;">テキスト</p>

2) セレクタの基本

/* タイプ(要素) */      h1 { ... }
/* クラス */             .btn { ... }
/* ID */                 #header { ... }   /* 乱用しない */
 /* 子孫・子・隣接 */    nav a { ... }  nav > a { ... }  h2 + p { ... }
/* 属性 */               input[type="email"] { ... }
/* 擬似クラス */         a:hover, li:nth-child(2) { ... }
/* 擬似要素 */           p::first-line, a::after { content:"→"; }

3) カスケード&優先度(Specificity)

  • 計算イメージ:ID(100) > class/属性/擬似クラス(10) > 要素/擬似要素(1)
  • 競合したら:後勝ち(後から書いた方が有効)
  • !importantは最終手段(設計悪化のもと)

4) ボックスモデル(超重要)

margin ─ 外側の余白
border ─ 枠線
padding ─ 内側の余白
content ─ 中身
* { box-sizing: border-box; } /* 幅計算が直感的になる定番 */

5) 単位&色

  • 単位:px(固定)/ %(相対)/ em(親のfont-size基準)/ rem(ルート基準)/ vw,vh(ビューポート)
    → レスポンシブは rem% を多用。
  • 色:#222 / rgb(34 34 34) / hsl(210 10% 20%)(HSLは調整しやすい)
  • 変数:--brand: #5865f2;color: var(--brand);

6) 文字・余白の基本

html { font-size: 16px; }       /* remの基準 */
body { line-height: 1.7; }
h1 { font-size: clamp(1.5rem, 3vw, 2.5rem); } /* 可変サイズ */
p  { margin: 0 0 1rem; }

7) レイアウト:display / Flex / Grid

display

.block { display: block; }        /* 幅いっぱい */
.inline { display: inline; }      /* 行内 */
.inline-block { display:inline-block; }

Flex(1次元レイアウト:横並び・縦中央寄せが得意)

.container {
  display: flex;
  gap: 1rem;
  align-items: center;        /* 交差軸整列(縦) */
  justify-content: space-between; /* 主軸整列(横) */
}
.center {
  display:flex; align-items:center; justify-content:center;
}

Grid(2次元レイアウト:段組が得意)

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* 3列 */
  gap: 1rem;
}

/* レスポンシブな自動詰め */
.auto-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1rem;
}

8) 位置指定

.box { position: relative; }
.badge {
  position: absolute; top: .5rem; right: .5rem;
}
header { position: sticky; top: 0; } /* スクロール追従 */

9) レスポンシブ(メディアクエリ・コンテナクエリ)

/* 画面幅が768px以上で適用(モバイル優先) */
@media (min-width: 768px) {
  .nav { display: flex; }
}

/* コンテナクエリ(対応ブラウザ増) */
.card { container-type: inline-size; }
@container (min-width: 500px) {
  .card__side-by-side { display:flex; }
}

10) トランジション&トランスフォーム

.btn {
  transition: transform .2s ease, background-color .2s;
}
.btn:hover {
  transform: translateY(-2px) scale(1.02);
  background: #111;
  color: #fff;
}

11) リセットとベース

/* まずはこれでOKな最小ベース */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
img, video { max-width: 100%; height: auto; display:block; }
button, input, select, textarea { font: inherit; }
:root {
  --bg: #fff; --fg:#222; --muted:#6b7280; --brand:#3b82f6;
}
@media (prefers-color-scheme: dark) {
  :root { --bg:#0b0b0f; --fg:#e5e7eb; --muted:#9ca3af; }
}
body { background: var(--bg); color: var(--fg); line-height:1.7; }
a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }

12) すぐ試せるミニページ(HTML+CSS)

<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CSS基礎デモ</title>
<style>
  * { box-sizing: border-box; }
  body { margin:0; font-family: system-ui, sans-serif; line-height:1.7; }
  header, footer { padding:16px; background:#f5f5f5; }
  .wrap { max-width:960px; margin:24px auto; padding:0 16px; }
  .hero {
    padding: 48px 16px; text-align:center;
    background: linear-gradient(120deg, #e0f2fe, #fde68a);
    border-radius:16px;
  }
  .btn {
    display:inline-block; padding:.75rem 1rem; border-radius:9999px;
    background:#111; color:#fff; transition: transform .2s ease;
  }
  .btn:hover { transform: translateY(-2px); }
  .grid {
    display:grid; gap:1rem;
    grid-template-columns: repeat(auto-fit, minmax(220px,1fr));
    margin-top:24px;
  }
  .card { border:1px solid #e5e7eb; border-radius:12px; padding:16px; }
  .card h3 { margin:.25rem 0 .5rem; }
</style>
</head>
<body>
  <header><div class="wrap"><strong>CSS基礎デモ</strong></div></header>
  <main class="wrap">
    <section class="hero">
      <h1>CSSの基本を掴もう</h1>
      <p>セレクタ / ボックスモデル / Flex / Grid / レスポンシブ</p>
      <a class="btn" href="#cards">カードを見る</a>
    </section>

    <section id="cards" class="grid">
      <article class="card">
        <h3>セレクタ</h3>
        <p>`.class` / `#id` / `a:hover` / `input[type="text"]` …</p>
      </article>
      <article class="card">
        <h3>Flex</h3>
        <p>横並び・中央寄せが簡単。`display:flex; gap:1rem;`</p>
      </article>
      <article class="card">
        <h3>Grid</h3>
        <p>2次元レイアウト。`auto-fit`×`minmax()`が実用的。</p>
      </article>
    </section>
  </main>
  <footer><div class="wrap"><small>&copy; 2025 CSS Demo</small></div></footer>
</body>
</html>

13) つまずきポイント

  • 高さが合わない:親にalign-items: stretchheight:auto、画像にはdisplay:block
  • 中央寄せできない:インラインはtext-align:center、ブロックはmargin: 0 auto、Flexならcenter
  • 崩れるbox-sizing:border-boxにして、余白はgap優先、width指定は最小限。
  • 優先順位に勝てない:セレクタを少しだけ強くする(親クラスを1段増やすなど)。!importantは避ける。

次に進むなら

  • レイアウト設計:BEM・Utility First(Tailwind的考え方)
  • モダン機能:subgridcontainer querieslogical propertiesmargin-inlineなど)
  • パフォーマンス:未使用CSSの削減、content-visibilitywill-changeの慎重な活用

HTMLの基礎

HTMLってなに?

  • Webページの骨組みを作る言語(見出し・段落・画像・リンクなどの構造)。
  • 見た目はCSS、動きはJSが担当。HTMLは“意味と構造”。

まずは雛形(コピペOK)

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>はじめてのHTML</title>
  <meta name="description" content="このページの説明文" />
</head>
<body>
  <h1>こんにちは!</h1>
  <p>これは最小構成のHTML5ページです。</p>
</body>
</html>

よく使う要素(超基本)

  • 見出し:<h1>~<h6>(ページに基本はh1は1つ
  • 段落:<p>
  • リンク:<a href="https://example.com">リンク</a>
  • 画像:<img src="img.png" alt="画像の説明">alt必須
  • リスト:<ul><li>…</li></ul> / <ol>…</ol>
  • 強調:<strong>(重要) / <em>(強調)
  • 区切り:<br>(改行は最小限)、<hr>(区切り線)
  • まとまり:<div>(汎用ブロック)、<span>(汎用インライン)

セマンティック要素(構造をわかりやすく)

  • header(ヘッダー)
  • nav(ナビ)
  • main(主内容は1ページ1つ
  • section(章)
  • article(単体で完結する記事)
  • aside(補足)
  • footer(フッター)

属性のキホン

  • id(一意な識別子)/class(グループ化)
  • href(リンク先)/src(画像・スクリプト元)
  • alt(画像代替文)/title(補足ヒント)
  • target="_blank"rel="noopener noreferrer"とセットで

フォーム最小例

<form action="/search" method="get">
  <label for="q">検索:</label>
  <input id="q" name="q" type="search" required>
  <button type="submit">送信</button>
</form>

テーブル最小例(表)

<table>
  <thead><tr><th>商品</th><th>価格</th></tr></thead>
  <tbody>
    <tr><td>りんご</td><td>120</td></tr>
    <tr><td>みかん</td><td>100</td></tr>
  </tbody>
</table>

CSS / JS の読み込み

<link rel="stylesheet" href="styles.css">
<script src="app.js" defer></script>
  • deferはHTML解析後に実行(推奨)。

ちょっとだけ“実践的”なサンプル

<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>ミニサイト</title>
  <style>
    body { font-family: system-ui, sans-serif; line-height: 1.7; margin: 0; }
    header, footer { padding: 16px; background: #f5f5f5; }
    nav a { margin-right: 12px; }
    main { max-width: 920px; margin: 24px auto; padding: 0 16px; }
    img { max-width: 100%; height: auto; }
    .card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; }
  </style>
</head>
<body>
  <header>
    <h1>ミニサイト</h1>
    <nav>
      <a href="#about">概要</a>
      <a href="#gallery">ギャラリー</a>
      <a href="#contact">お問い合わせ</a>
    </nav>
  </header>

  <main id="content">
    <section id="about">
      <h2>概要</h2>
      <p>これはHTMLの基本で作ったミニページです。</p>
    </section>

    <section id="gallery">
      <h2>ギャラリー</h2>
      <div class="card">
        <img src="sample.jpg" alt="サンプル画像">
        <p>レスポンシブに画像が縮みます。</p>
      </div>
      <ul>
        <li>箇条書き1</li>
        <li>箇条書き2</li>
      </ul>
    </section>

    <section id="contact">
      <h2>お問い合わせ</h2>
      <form>
        <label for="name">お名前</label><br>
        <input id="name" name="name" required><br><br>
        <label for="msg">メッセージ</label><br>
        <textarea id="msg" name="msg" rows="4"></textarea><br><br>
        <button type="submit">送信</button>
      </form>
    </section>
  </main>

  <footer>
    <small>&copy; 2025 MiniSite</small>
  </footer>

  <script>
    // ごく簡単なJS:ナビをクリックしたらスムーズスクロール
    document.querySelectorAll('nav a').forEach(a => {
      a.addEventListener('click', e => {
        const id = a.getAttribute('href');
        if (id.startsWith('#')) {
          e.preventDefault();
          document.querySelector(id)?.scrollIntoView({ behavior: 'smooth' });
        }
      });
    });
  </script>
</body>
</html>

初心者がつまずきやすいポイント

  • 文字化け→<meta charset="utf-8">を必ず入れる。
  • スマホで拡大縮小が変→<meta name="viewport" …>を入れる。
  • 画像が大きすぎる→CSSでimg { max-width: 100%; height: auto; }
  • 見出し乱用→h1はページの主題に1回、階層は順序を守る。
  • altなし→スクリーンリーダー/SEO的にマイナス。必ず書く。

もっと深掘り(フォームのバリデーション、SEO、アクセシビリティ、Flex/Gridレイアウト、コンポーネント化など)もまとめられます。どこから強化したい?(例:フォームをしっかり、レイアウトを学ぶ、CSS設計、JS連携 など)

ひとこと履歴書


<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>ひとこと履歴書 Ultra</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <style>
    body {
      font-family: "Helvetica Neue", sans-serif;
      background: #f0f0f0;
      padding: 30px;
      max-width: 900px;
      margin: auto;
    }
    h1 {
      text-align: center;
      margin-bottom: 20px;
    }
    textarea, input[type="date"], input[type="text"] {
      width: 100%;
      padding: 10px;
      font-size: 1rem;
      margin-bottom: 10px;
    }
    .emotion-btn {
      font-size: 20px;
      padding: 6px 12px;
      border: 2px solid #ccc;
      background: white;
      cursor: pointer;
      border-radius: 6px;
    }
    .emotion-btn.selected {
      border-color: #4caf50;
      background: #e8f5e9;
    }
    button.add {
      background: #4caf50;
      color: white;
      border: none;
      border-radius: 6px;
      padding: 10px 20px;
      cursor: pointer;
      font-size: 1rem;
      margin-top: 10px;
    }
    button.export {
      background: #2196f3;
      margin-left: 10px;
    }
    .entry {
      background: white;
      padding: 10px;
      border-left: 5px solid #ccc;
      margin: 10px 0;
      border-radius: 6px;
    }
    .section-date {
      font-weight: bold;
      margin-top: 30px;
    }
    .entry .meta {
      font-size: 0.8em;
      color: #666;
    }
    #stats {
      margin: 20px 0;
      background: #fff;
      padding: 10px;
      border-radius: 8px;
      box-shadow: 0 0 3px rgba(0,0,0,0.1);
    }
  </style>
</head>
<body>

<h1>ひとこと履歴書 Ultra</h1>

<textarea id="entryInput" placeholder="今日の一言を記録しよう!"></textarea>
<input type="date" id="entryDate">
<div>
  <button class="emotion-btn" data-emotion="😊">😊 喜</button>
  <button class="emotion-btn" data-emotion="😢">😢 哀</button>
  <button class="emotion-btn" data-emotion="😠">😠 怒</button>
  <button class="emotion-btn" data-emotion="😐">😐 中立</button>
</div>
<button class="add" onclick="addEntry()">記録</button>
<button class="add export" onclick="exportCSV()">CSVダウンロード</button>

<input type="text" id="searchBox" placeholder="キーワード検索(例:嬉しい、美術館)">
<div id="stats"></div>
<canvas id="emotionChart" height="200"></canvas>
<div id="entryList"></div>

<script>
const input = document.getElementById("entryInput");
const dateInput = document.getElementById("entryDate");
const searchBox = document.getElementById("searchBox");
const entryList = document.getElementById("entryList");
const stats = document.getElementById("stats");
const emotionButtons = document.querySelectorAll(".emotion-btn");
let selectedEmotion = "😊";
dateInput.valueAsDate = new Date();

emotionButtons.forEach(btn => {
  btn.addEventListener("click", () => {
    emotionButtons.forEach(b => b.classList.remove("selected"));
    btn.classList.add("selected");
    selectedEmotion = btn.dataset.emotion;
  });
});

function addEntry() {
  const text = input.value.trim();
  const date = dateInput.value;
  if (!text || !date || !selectedEmotion) return;
  const entries = JSON.parse(localStorage.getItem("entries") || "[]");
  entries.push({ text, date, emotion: selectedEmotion, timestamp: new Date().toISOString() });
  localStorage.setItem("entries", JSON.stringify(entries));
  input.value = "";
  renderEntries();
}

function exportCSV() {
  const entries = JSON.parse(localStorage.getItem("entries") || "[]");
  let csv = "日付,感情,テキスト,記録日時\n";
  entries.forEach(e => {
    csv += `${e.date},${e.emotion},"${e.text.replace(/"/g, '""')}",${e.timestamp}\n`;
  });
  const blob = new Blob([csv], { type: "text/csv" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "hitokoto_entries.csv";
  a.click();
  URL.revokeObjectURL(url);
}

function groupByDate(entries) {
  const grouped = {};
  entries.forEach(entry => {
    if (!grouped[entry.date]) grouped[entry.date] = [];
    grouped[entry.date].push(entry);
  });
  return grouped;
}

function renderEntries() {
  const entries = JSON.parse(localStorage.getItem("entries") || "[]").reverse();
  const keyword = searchBox.value.trim();
  const filtered = keyword
    ? entries.filter(e => e.text.includes(keyword))
    : entries;

  const grouped = groupByDate(filtered);
  const emotionCounts = { "😊": 0, "😢": 0, "😠": 0, "😐": 0 };

  entryList.innerHTML = "";
  let totalTextLength = 0;

  for (const date in grouped) {
    const section = document.createElement("div");
    section.innerHTML = `<div class="section-date">📅 ${date}</div>`;
    grouped[date].forEach(entry => {
      emotionCounts[entry.emotion]++;
      totalTextLength += entry.text.length;
      const div = document.createElement("div");
      div.className = "entry";
      div.innerHTML = `
        <div>${entry.emotion} ${entry.text}</div>
        <div class="meta">${new Date(entry.timestamp).toLocaleString()}</div>
      `;
      section.appendChild(div);
    });
    entryList.appendChild(section);
  }

  const total = filtered.length;
  const avgLen = total ? Math.round(totalTextLength / total) : 0;
  stats.innerHTML = `📌 総投稿数: ${total} 件|平均文字数: ${avgLen} 字`;

  renderChart(emotionCounts);
}

function renderChart(counts) {
  const ctx = document.getElementById("emotionChart").getContext("2d");
  if (window.myChart) window.myChart.destroy();
  window.myChart = new Chart(ctx, {
    type: "pie",
    data: {
      labels: ["😊 喜", "😢 哀", "😠 怒", "😐 中立"],
      datasets: [{
        data: [
          counts["😊"],
          counts["😢"],
          counts["😠"],
          counts["😐"]
        ],
        backgroundColor: ["gold", "skyblue", "tomato", "gray"]
      }]
    },
    options: {
      plugins: { legend: { position: "bottom" } }
    }
  });
}

searchBox.addEventListener("input", renderEntries);
renderEntries();
</script>

</body>
</html>

ゲームBGM自動生成サービス.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>🎮 ゲームBGM自動生成サービス</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    :root {
      --bg: #121212;
      --card: #1e1e2e;
      --text: #ffffff;
      --accent: #ffd700;
      --shadow: rgba(0, 0, 0, 0.3);
      --button: #ffd700;
    }
    [data-theme='light'] {
      --bg: #f5f5f5;
      --card: #ffffff;
      --text: #111;
      --accent: #ff9800;
      --shadow: rgba(0, 0, 0, 0.1);
      --button: #ff9800;
    }

    body {
      margin: 0;
      font-family: 'Segoe UI', sans-serif;
      background: var(--bg);
      color: var(--text);
      transition: 0.3s;
      padding: 1rem;
    }

    header {
      text-align: center;
      margin-bottom: 1rem;
    }

    h1 {
      color: var(--accent);
      font-size: 2rem;
    }

    .container {
      max-width: 600px;
      margin: auto;
      background: var(--card);
      border-radius: 12px;
      box-shadow: 0 0 10px var(--shadow);
      padding: 1.5rem;
    }

    label {
      font-weight: bold;
      display: block;
      margin-top: 1rem;
    }

    select, button {
      width: 100%;
      padding: 0.6rem;
      margin-top: 0.5rem;
      border-radius: 8px;
      border: none;
      font-size: 1rem;
    }

    button {
      background: var(--button);
      color: #000;
      font-weight: bold;
      cursor: pointer;
      transition: 0.3s;
    }

    button:disabled {
      background: #999;
      cursor: not-allowed;
    }

    button:hover:enabled {
      opacity: 0.85;
    }

    .desc, .msg {
      margin-top: 1rem;
      font-size: 0.9rem;
      color: #ccc;
    }

    .result, .history {
      margin-top: 2rem;
    }

    audio {
      width: 100%;
      margin-top: 0.5rem;
    }

    .visualizer {
      width: 100%;
      height: 4px;
      background: linear-gradient(90deg, #ffd700, #ff9800);
      animation: pulse 2s infinite linear;
      opacity: 0;
    }

    .playing .visualizer {
      opacity: 1;
    }

    @keyframes pulse {
      0% { transform: scaleX(1); }
      50% { transform: scaleX(1.05); }
      100% { transform: scaleX(1); }
    }

    .toggle-theme {
      text-align: right;
      margin-bottom: 0.5rem;
    }

    .accordion {
      background: transparent;
      color: var(--accent);
      border: none;
      font-weight: bold;
      cursor: pointer;
      margin-top: 1rem;
      width: 100%;
      text-align: left;
      font-size: 1rem;
    }

    .accordion-content {
      max-height: 0;
      overflow: hidden;
      transition: max-height 0.3s ease;
    }

    .accordion.open + .accordion-content {
      max-height: 600px;
    }
  </style>
</head>
<body>

  <header>
    <h1>🎮 ゲームBGM自動生成</h1>
  </header>

  <div class="toggle-theme">
    <button onclick="toggleTheme()">🌓 テーマ切替</button>
  </div>

  <div class="container">
    <label for="genre">🎼 ジャンル</label>
    <select id="genre" onchange="updateDescription()">
      <option value="fantasy">ファンタジー</option>
      <option value="cyberpunk">サイバーパンク</option>
      <option value="horror">ホラー</option>
      <option value="symphonic">シンフォニック</option>
    </select>

    <label for="mood">🎭 雰囲気</label>
    <select id="mood" onchange="updateDescription()">
      <option value="mysterious">神秘的</option>
      <option value="sad">悲しい</option>
      <option value="heroic">勇ましい</option>
      <option value="fun">楽しい</option>
    </select>

    <div class="desc" id="descText">選択内容に応じてBGMを生成します。</div>

    <button id="generateBtn" onclick="generateBGM()">🎶 BGMを生成する</button>

    <div class="msg" id="msg"></div>

    <div class="result" id="result" style="display:none;">
      <h3>🎧 再生中のBGM</h3>
      <audio controls id="player" onplay="startVisualizer()" onpause="stopVisualizer()"></audio>
      <div class="visualizer" id="visualizer"></div>
    </div>

    <button class="accordion" onclick="toggleAccordion(this)">📜 再生履歴</button>
    <div class="accordion-content" id="historyList"></div>
  </div>

  <script>
    const descMap = {
      fantasy: "魔法の世界、冒険と伝説の音楽",
      cyberpunk: "近未来都市とテクノ感の融合",
      horror: "不安と恐怖を煽るBGM",
      symphonic: "壮大で感動的なオーケストラ風",
      mysterious: "謎解き、探検にぴったり",
      sad: "涙や喪失感を表現する旋律",
      heroic: "勇ましさ、戦い、勝利の象徴",
      fun: "軽快で明るいリズム"
    };

    const history = [];
    const maxHistory = 5;

    function updateDescription() {
      const g = document.getElementById('genre').value;
      const m = document.getElementById('mood').value;
      document.getElementById('descText').textContent = `🧠 ${descMap[g]} × ${descMap[m]}`;
    }

    function generateBGM() {
      const genre = document.getElementById('genre').value;
      const mood = document.getElementById('mood').value;
      const btn = document.getElementById('generateBtn');
      const player = document.getElementById('player');
      const msg = document.getElementById('msg');
      const result = document.getElementById('result');

      btn.disabled = true;
      msg.textContent = "⏳ BGMを生成中...";

      const url = `https://example.com/bgm/${genre}_${mood}_${Math.floor(Math.random()*3)+1}.mp3`;

      setTimeout(() => {
        msg.textContent = "✅ BGM生成完了!再生できます。";
        player.src = url;
        result.style.display = 'block';

        // 保存履歴
        history.unshift({ genre, mood, url });
        if (history.length > maxHistory) history.pop();
        renderHistory();
        btn.disabled = false;
      }, 1500);
    }

    function renderHistory() {
      const list = document.getElementById('historyList');
      list.innerHTML = "";
      history.forEach(item => {
        const div = document.createElement('div');
        div.innerHTML = `<strong>${item.genre} × ${item.mood}</strong><br><audio controls src="${item.url}"></audio><hr>`;
        list.appendChild(div);
      });
    }

    function toggleTheme() {
      const theme = document.documentElement.getAttribute("data-theme");
      document.documentElement.setAttribute("data-theme", theme === "light" ? "dark" : "light");
    }

    function toggleAccordion(el) {
      el.classList.toggle('open');
    }

    function startVisualizer() {
      document.getElementById('visualizer').style.opacity = '1';
    }

    function stopVisualizer() {
      document.getElementById('visualizer').style.opacity = '0';
    }

    // 初期設定
    document.documentElement.setAttribute("data-theme", "dark");
    updateDescription();
  </script>

</body>
</html>

QuestifyInfinity.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Questify Advanced Battle</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- レトロゲーム風フォント -->
  <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">

  <style>
    /* ========== 全体 ========== */
    body {
      background-color: #1a1a1a;
      color: #fff;
      font-family: 'Press Start 2P', cursive;
      margin: 0; padding: 0;
    }
    header, footer {
      background: #333; text-align: center; padding: 15px; border-bottom: 2px solid #555;
    }
    header h1 { font-size: 1.3rem; margin: 0; }
    main { max-width: 1200px; margin: 10px auto; padding-bottom: 80px; }

    .box {
      background: #2b2b2b; margin-bottom: 20px;
      border: 2px solid #555; padding: 20px; position: relative;
    }
    .section-title { font-size: 1.2rem; margin-bottom: 10px; }
    button {
      font-family: 'Press Start 2P', cursive;
      border: none; cursor: pointer; padding: 6px 10px; margin-right: 5px;
    }
    button:hover { opacity: 0.8; }
    .btn-primary { background: #006699; color: #fff; }
    .btn-delete { background: #bb3333; color: #fff; }

    /* ステータス表示 */
    .hero-info p { margin: 5px 0; }
    .xp-bar {
      background: #555; width: 100%; height: 20px; margin: 5px 0;
      position: relative; overflow: hidden;
    }
    .xp-fill {
      background: #00aa00; width: 0%; height: 100%;
      transition: width 0.3s ease;
    }

    /* パーティ */
    .party-member {
      background: #333; border: 2px solid #555;
      padding: 10px; margin-bottom: 10px;
    }

    /* スキルツリー */
    .skill-tree .skill {
      background: #333; border: 2px solid #555;
      padding: 10px; margin-bottom: 10px;
    }

    /* 装備・素材 */
    .equipment-info p { margin: 3px 0; }
    .craft-recipe {
      border: 1px dashed #999;
      margin-bottom: 10px; padding: 5px;
    }

    /* クエスト */
    .quest-list .quest {
      background: #333; border: 2px solid #555;
      padding: 10px; margin-bottom: 10px;
    }

    /* 実績 */
    .achievements .achievement {
      background: #333; border: 2px solid #555;
      padding: 10px; margin-bottom: 10px; position: relative;
    }
    .achievement.locked { opacity: 0.5; }
    .locked-label {
      position: absolute; top: 5px; right: 5px;
      background: #cc0000; padding: 3px 5px; font-size: 0.7rem;
    }

    /* バトルモーダル */
    .modal-bg {
      position: fixed; top: 0; left: 0; width: 100%; height: 100%;
      background: rgba(0,0,0,0.8);
      display: none; justify-content: center; align-items: center;
    }
    .modal {
      background: #2b2b2b; border: 2px solid #555;
      padding: 20px; max-width: 600px; width: 90%;
      position: relative;
    }
    .close-btn {
      position: absolute; top: 10px; right: 10px;
      background: #dd3333; color: #fff; padding: 5px 8px; border: none;
    }
    .battle-enemy-info, .battle-hero-info {
      margin-bottom: 10px;
    }
    #battleLog {
      border: 1px solid #555;
      min-height: 60px; padding: 5px;
      margin: 10px 0; max-height: 200px; overflow-y: auto;
    }
    .battle-action-buttons { margin-top: 10px; }
    .battle-action-buttons button { margin: 5px 5px 0 0; }
  </style>
</head>

<body>
<header>
  <h1>Questify Advanced Battle</h1>
</header>

<main>

  <!-- ===================== ヒーロー & パーティステータス ===================== -->
  <section class="box">
    <h2 class="section-title">冒険者ステータス</h2>
    <div class="hero-info">
      <p>勇者: <span id="heroNameDisplay">No Name</span></p>
      <p>職業: <span id="heroJobDisplay">未設定</span></p>
      <p>
        Lv.<span id="heroLevel">1</span>
        HP:<span id="heroHp">?</span>/<span id="heroMaxHp">?</span>
      </p>
      <div class="xp-bar">
        <div class="xp-fill" id="xpFill"></div>
      </div>
      <p>EXP: <span id="heroXp">0</span> / <span id="xpToNextLevel">100</span></p>
      <p>Gold: <span id="heroGold">0</span></p>
      <p>Skill Pts: <span id="heroSkillPts">0</span></p>
      <label>勇者名: <input type="text" id="heroNameInput" placeholder="アルス" /></label>
      <button onclick="changeHeroName()">変更</button>
    </div>
  </section>

  <section class="box">
    <h2 class="section-title">パーティメンバー</h2>
    <div id="partyList"></div>
    <button class="btn-primary" onclick="addPartyMember()">仲間を雇う (最大2人)</button>
  </section>

  <!-- ===================== スキルツリー ===================== -->
  <section class="box">
    <h2 class="section-title">職業 & スキルツリー</h2>
    <p>
      <button onclick="setJob('戦士')">戦士</button>
      <button onclick="setJob('魔法使い')">魔法使い</button>
      <button onclick="setJob('盗賊')">盗賊</button>
    </p>
    <div class="skill-tree" id="skillTree"></div>
  </section>

  <!-- ===================== 装備 & クラフト ===================== -->
  <section class="box">
    <h2 class="section-title">装備 & クラフト</h2>
    <div class="equipment-info" id="heroEquipment"></div>

    <h3>クラフトレシピ</h3>
    <div id="craftContainer"></div>
  </section>

  <!-- ===================== クエスト (デイリー/通常/ウィークリー) ===================== -->
  <section class="box">
    <h2 class="section-title">クエスト</h2>
    <div style="margin-bottom:10px;">
      <button class="btn-primary" onclick="switchQuestTab('daily')">デイリー</button>
      <button class="btn-primary" onclick="switchQuestTab('normal')">通常</button>
      <button class="btn-primary" onclick="switchQuestTab('weekly')">ウィークリー</button>
    </div>
    <div id="dailyQuests" class="quest-list"></div>
    <div id="normalQuests" class="quest-list" style="display:none;"></div>
    <div id="weeklyQuests" class="quest-list" style="display:none;"></div>
  </section>

  <!-- ===================== マップ & ボス戦 ===================== -->
  <section class="box">
    <h2 class="section-title">ワールドマップ</h2>
    <div id="mapAreaContainer"></div>
  </section>

  <!-- ===================== ランダムエンカウント ===================== -->
  <section class="box">
    <h2 class="section-title">ダンジョン潜入 (複数敵ランダムエンカウント)</h2>
    <p>ボタンを押すと **複数の敵** が出る場合も!</p>
    <button class="btn-primary" onclick="startRandomEncounter()">ダンジョンに潜る</button>
  </section>

  <!-- ===================== 実績 & ストーリー ===================== -->
  <section class="box achievements">
    <h2 class="section-title">実績(Achievements)</h2>
    <div id="achievementList"></div>
  </section>

  <section class="box">
    <h2 class="section-title">ストーリー進行</h2>
    <div id="storyProgress"></div>
  </section>

</main>

<footer>
  <p>© 2025 Questify Advanced Battle</p>
</footer>

<!-- ========== 個別ターン制バトルモーダル ========== -->
<div class="modal-bg" id="battleModalBg">
  <div class="modal" id="battleModal">
    <button class="close-btn" onclick="closeBattleModal()">×</button>
    <h2 id="battleTitle">バトル</h2>
    <p id="battleDesc"></p>

    <!-- 敵一覧表示 -->
    <div class="battle-enemy-info" id="battleEnemyInfo"></div>
    <!-- 味方一覧表示 -->
    <div class="battle-hero-info" id="battleHeroInfo"></div>

    <!-- 行動ログ -->
    <div id="battleLog"></div>

    <!-- プレイヤー操作ボタン (アクション選択) -->
    <div class="battle-action-buttons" id="battleActionButtons">
      <button onclick="chooseAttack()">攻撃</button>
      <button onclick="chooseSkill()">スキル</button>
      <button onclick="chooseItem()">アイテム</button>
      <button onclick="chooseDefend()">防御</button>
      <button onclick="chooseFlee()">逃げる</button>
    </div>
  </div>
</div>

<script>
/* =========================================
   1) データ構造 & ローカルストレージ
========================================= */
const STORAGE_KEY = "questify_battle_data";

let gameData = {
  hero: {
    name: "No Name",
    job: "未設定",
    level: 1,
    xp: 0,
    gold: 0,
    skillPts: 0,
    hp: 50,
    maxHp: 50,
    speed: 8,  // 追加: 素早さ
    equipment: { weapon: null, armor: null, accessory: null },
    materials: { wood: 0, ore: 0, magicCrystal: 0 },
    consumables: { potion: 2 },
    // アクティブスキル(例): 攻撃スキル
    activeSkills: [
      { id: "slash", name: "パワースラッシュ", baseDamage: 15 },
      // ここに追加スキルを増やす
    ]
  },
  party: [
    // 仲間も speed を持つ、activeSkills を持つなど拡張可能
    // { id, name, level, xp, hp, maxHp, speed, attack, equipment, activeSkills, ... }
  ],
  // 職業ごとのパッシブスキル
  skills: {
    warrior: [
      { id: 1, name: "剣術熟練", level: 0, maxLevel: 5, desc: "攻撃力+2/Lv" },
      { id: 2, name: "体力増強", level: 0, maxLevel: 5, desc: "HP+10/Lv" }
    ],
    mage: [
      { id: 1, name: "魔力増強", level: 0, maxLevel: 5, desc: "魔法攻撃力+3/Lv" },
      { id: 2, name: "精神集中", level: 0, maxLevel: 5, desc: "ボス戦追加ダメージ+5/Lv" }
    ],
    thief: [
      { id: 1, name: "素早さ強化", level: 0, maxLevel: 5, desc: "攻撃力+2/Lv" },
      { id: 2, name: "ゴールド盗み", level: 0, maxLevel: 5, desc: "討伐時Gold+5/Lv" }
    ]
  },
  // クラフトレシピ & クエスト & マップ & 実績 等は前回コードと同じ
  // …(省略なし、全て記載)…
  craftingRecipes: [
    {
      id: 1,
      result: { name: "回復薬", type: "consumable", itemKey: "potion", amount: 1, hpRestore: 30 },
      materialsRequired: { wood: 1, magicCrystal: 1 },
      desc: "木材1 & 魔力結晶1 で回復薬を1つ生成"
    }
  ],
  dailyQuests: [
    { id: 1, title: "【デイリー】部屋掃除", exp: 10, gold: 5, completed: false },
    { id: 2, title: "【デイリー】筋トレ15分", exp: 15, gold: 5, completed: false }
  ],
  normalQuests: [
    { id: 1, title: "HTML/CSSの学習", exp: 20, gold: 10, completed: false },
    { id: 2, title: "ランニング30分", exp: 25, gold: 10, completed: false }
  ],
  weeklyQuests: [
    { id: 1, title: "【ウィークリー】5日連続早起き", exp: 50, gold: 30, completed: false },
    { id: 2, title: "【ウィークリー】合計5時間の学習", exp: 60, gold: 40, completed: false }
  ],
  mapAreas: [
    { id: 1, name: "街", levelReq: 1, desc: "安全な街", boss: null },
    { id: 2, name: "森", levelReq: 3, desc: "木材が手に入るかも", boss: { name: "森の主", hp: 60, maxHp: 60, speed: 6, atk: 12, rewardExp: 50, rewardGold: 30 } },
    { id: 3, name: "洞窟", levelReq: 5, desc: "鉱石が眠る", boss: { name: "ゴーレム", hp: 100, maxHp: 100, speed: 4, atk: 20, rewardExp: 100, rewardGold: 80 } },
    { id: 4, name: "魔王城", levelReq: 10, desc: "魔王が支配する城", boss: { name: "魔王", hp: 200, maxHp: 200, speed: 10, atk: 35, rewardExp: 300, rewardGold: 200 } }
  ],
  achievements: [
    { id: 1, title: "初クエスト達成", desc: "クエストを1回完了", type: "questCount", target: 1, unlocked: false },
    { id: 2, title: "レベル10到達", desc: "Lv.10になる", type: "level", target: 10, unlocked: false },
    { id: 3, title: "森の主撃破", desc: "森の主を倒す", type: "bossKill", bossName: "森の主", unlocked: false },
    { id: 4, title: "魔王撃破", desc: "魔王を倒す", type: "bossKill", bossName: "魔王", unlocked: false },
    { id: 5, title: "金持ち", desc: "ゴールドが100を超える", type: "gold", target: 100, unlocked: false }
  ],
  story: [
    { bossName: "森の主", text: "森の主を倒し、森に平穏が戻った…!" },
    { bossName: "ゴーレム", text: "洞窟のゴーレムを粉砕し、鉱山への道が開けた。" },
    { bossName: "魔王", text: "魔王を倒し、世界に平和が訪れた。あなたは真の勇者!" }
  ],
  enemies: [
    { name: "森の狼", hp: 30, maxHp: 30, speed: 7, atk: 8, exp: 15, gold: 5, dropMat: { wood: 1 } },
    { name: "洞窟コウモリ", hp: 35, maxHp: 35, speed: 9, atk: 10, exp: 20, gold: 8, dropMat: { ore: 1 } },
    { name: "ゴブリン", hp: 40, maxHp: 40, speed: 5, atk: 12, exp: 25, gold: 10, dropMat: { wood: 1, ore: 1 } }
  ],
  lastDailyReset: null,
  lastWeeklyReset: null
};

function loadData() {
  const saved = localStorage.getItem(STORAGE_KEY);
  if (saved) {
    gameData = JSON.parse(saved);
  }
}
function saveData() {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(gameData));
}

/* =========================================
   2) 初期化処理
========================================= */
window.addEventListener("load", () => {
  loadData();
  dailyQuestResetCheck();
  weeklyQuestResetCheck();
  applySkillPassive();
  updateAllUI();
});

/* =========================================
   3) ユーティリティ & ステータス系
========================================= */
function xpNeededForLevel(level) {
  return level * 100;
}
function checkLevelUp() {
  while (gameData.hero.xp >= xpNeededForLevel(gameData.hero.level)) {
    gameData.hero.xp -= xpNeededForLevel(gameData.hero.level);
    gameData.hero.level++;
    gameData.hero.skillPts++;
    applySkillPassive();
    alert(`レベルアップ!Lv.${gameData.hero.level} になった。スキルポイント+1`);
  }
}
function gainExp(amount) {
  gameData.hero.xp += amount;
  checkLevelUp();
  saveData();
  updateHeroUI();
  checkAchievements();
}
function gainGold(amount) {
  gameData.hero.gold += amount;
  saveData();
  updateHeroUI();
  checkAchievements();
}
function updateHeroUI() {
  const h = gameData.hero;
  document.getElementById("heroNameDisplay").textContent = h.name;
  document.getElementById("heroJobDisplay").textContent = h.job;
  document.getElementById("heroLevel").textContent = h.level;
  document.getElementById("heroHp").textContent = h.hp;
  document.getElementById("heroMaxHp").textContent = h.maxHp;
  document.getElementById("heroXp").textContent = h.xp;
  document.getElementById("xpToNextLevel").textContent = xpNeededForLevel(h.level);
  document.getElementById("heroGold").textContent = h.gold;
  document.getElementById("heroSkillPts").textContent = h.skillPts;

  const ratio = (h.xp / xpNeededForLevel(h.level)) * 100;
  document.getElementById("xpFill").style.width = ratio + "%";

  updateHeroEquipmentUI();
}
function changeHeroName() {
  const input = document.getElementById("heroNameInput");
  const newName = input.value.trim();
  if (!newName) return;
  gameData.hero.name = newName;
  input.value = "";
  saveData();
  updateHeroUI();
}
function applySkillPassive() {
  const hero = gameData.hero;
  hero.maxHp = 50 + (hero.level - 1) * 5;
  let skillList = [];
  if (hero.job === "戦士") skillList = gameData.skills.warrior;
  if (hero.job === "魔法使い") skillList = gameData.skills.mage;
  if (hero.job === "盗賊") skillList = gameData.skills.thief;
  skillList.forEach(s => {
    if (s.name === "体力増強") {
      hero.maxHp += (s.level * 10);
    }
  });
  if (hero.hp > hero.maxHp) hero.hp = hero.maxHp;
}

/* =========================================
   4) パーティ管理
========================================= */
function addPartyMember() {
  if (gameData.party.length >= 2) {
    alert("仲間は最大2人までです。");
    return;
  }
  const newMem = {
    id: Date.now(),
    name: `仲間${gameData.party.length + 1}`,
    level: 1,
    xp: 0,
    hp: 40,
    maxHp: 40,
    speed: 5,
    attack: 5,
    equipment: { weapon: null, armor: null, accessory: null },
    activeSkills: [
      // 例: 仲間専用スキルを入れたければここに
    ]
  };
  gameData.party.push(newMem);
  alert(`${newMem.name} を雇いました!`);
  saveData();
  updateAllUI();
}

function updatePartyUI() {
  const container = document.getElementById("partyList");
  container.innerHTML = "";
  if (gameData.party.length === 0) {
    container.innerHTML = "<p>仲間はいません</p>";
    return;
  }
  gameData.party.forEach(m => {
    const div = document.createElement("div");
    div.className = "party-member";
    div.innerHTML = `<p>${m.name} (Lv.${m.level}) HP:${m.hp}/${m.maxHp} 攻:${m.attack}</p>`;
    const leaveBtn = document.createElement("button");
    leaveBtn.className = "btn-delete";
    leaveBtn.textContent = "離脱";
    leaveBtn.onclick = () => {
      if (confirm(`${m.name}を外しますか?`)) {
        gameData.party = gameData.party.filter(x => x.id !== m.id);
        saveData();
        updateAllUI();
      }
    };
    div.appendChild(leaveBtn);
    container.appendChild(div);
  });
}

function distributePartyExp(amount) {
  gameData.party.forEach(m => {
    m.xp += amount;
    while (m.xp >= m.level * 50) {
      m.xp -= m.level * 50;
      m.level++;
      m.attack += 2;
      m.maxHp += 5;
      m.hp += 5;
    }
  });
}

/* =========================================
   5) 職業 & スキルツリー
========================================= */
function setJob(job) {
  gameData.hero.job = job;
  alert(`職業を「${job}」に変更しました。`);
  saveData();
  applySkillPassive();
  updateAllUI();
}
function updateSkillTreeUI() {
  const container = document.getElementById("skillTree");
  container.innerHTML = "";
  const job = gameData.hero.job;
  if (job === "未設定") {
    container.innerHTML = "<p>職業を選択してください</p>";
    return;
  }
  let skillList = [];
  if (job === "戦士") skillList = gameData.skills.warrior;
  if (job === "魔法使い") skillList = gameData.skills.mage;
  if (job === "盗賊") skillList = gameData.skills.thief;

  skillList.forEach(s => {
    const skillDiv = document.createElement("div");
    skillDiv.className = "skill";
    skillDiv.innerHTML = `
      <h4>${s.name} (Lv.${s.level}/${s.maxLevel})</h4>
      <p>${s.desc}</p>
    `;
    const btn = document.createElement("button");
    btn.className = "btn-primary";
    if (s.level >= s.maxLevel) {
      btn.textContent = "MAX";
      btn.disabled = true;
    } else {
      btn.textContent = "強化";
      btn.onclick = () => {
        if (gameData.hero.skillPts <= 0) {
          alert("スキルポイントが足りません");
          return;
        }
        s.level++;
        gameData.hero.skillPts--;
        saveData();
        applySkillPassive();
        updateAllUI();
      };
    }
    skillDiv.appendChild(btn);
    container.appendChild(skillDiv);
  });
}

/* =========================================
   6) 装備 & クラフト(簡易化)
========================================= */
function updateHeroEquipmentUI() {
  const eqDiv = document.getElementById("heroEquipment");
  const h = gameData.hero;
  eqDiv.innerHTML = `
    <p>武器: ${h.equipment.weapon ? h.equipment.weapon.name : "なし"}</p>
    <p>防具: ${h.equipment.armor ? h.equipment.armor.name : "なし"}</p>
    <p>アクセ: ${h.equipment.accessory ? h.equipment.accessory.name : "なし"}</p>
    <p>素材: 木材(${h.materials.wood}), 鉱石(${h.materials.ore}), 結晶(${h.materials.magicCrystal})</p>
    <p>回復薬: ${h.consumables.potion || 0}個</p>
  `;
}
function updateCraftUI() {
  const cDiv = document.getElementById("craftContainer");
  cDiv.innerHTML = "";
  gameData.craftingRecipes.forEach(r => {
    const div = document.createElement("div");
    div.className = "craft-recipe";
    let matText = Object.keys(r.materialsRequired).map(m => {
      return `${m}:${r.materialsRequired[m]}`;
    }).join(", ");
    div.innerHTML = `
      <strong>${r.result.name}</strong> → 必要素材 ${matText}<br>
      ${r.desc}
    `;
    const btn = document.createElement("button");
    btn.textContent = "クラフト";
    btn.onclick = () => craftItem(r);
    div.appendChild(btn);
    cDiv.appendChild(div);
  });
}
function craftItem(recipe) {
  for (let mat in recipe.materialsRequired) {
    if ((gameData.hero.materials[mat] || 0) < recipe.materialsRequired[mat]) {
      alert("素材が足りません!");
      return;
    }
  }
  for (let mat in recipe.materialsRequired) {
    gameData.hero.materials[mat] -= recipe.materialsRequired[mat];
  }
  if (recipe.result.type === "consumable") {
    const key = recipe.result.itemKey;
    gameData.hero.consumables[key] = (gameData.hero.consumables[key] || 0) + recipe.result.amount;
    alert(`${recipe.result.name} を${recipe.result.amount}個、作成しました!`);
  }
  saveData();
  updateAllUI();
}

/* =========================================
   7) クエスト関連 (デイリー/通常/ウィークリー)
========================================= */
function switchQuestTab(tab) {
  document.getElementById("dailyQuests").style.display = (tab === "daily") ? "" : "none";
  document.getElementById("normalQuests").style.display = (tab === "normal") ? "" : "none";
  document.getElementById("weeklyQuests").style.display = (tab === "weekly") ? "" : "none";
}
function updateQuestsUI() {
  renderQuestList("dailyQuests", gameData.dailyQuests, completeDailyQuest);
  renderQuestList("normalQuests", gameData.normalQuests, completeNormalQuest);
  renderQuestList("weeklyQuests", gameData.weeklyQuests, completeWeeklyQuest);
}
function renderQuestList(containerId, questArr, completeFn) {
  const container = document.getElementById(containerId);
  container.innerHTML = "";
  questArr.forEach(q => {
    const div = document.createElement("div");
    div.className = "quest";
    const h4 = document.createElement("h4");
    h4.textContent = q.completed ? `【達成済】${q.title}` : q.title;
    const p = document.createElement("p");
    p.textContent = `EXP:${q.exp} Gold:${q.gold}`;
    const btn = document.createElement("button");
    btn.className = "btn-primary";
    if (q.completed) {
      btn.textContent = "完了済";
      btn.disabled = true;
    } else {
      btn.textContent = "達成";
      btn.onclick = () => completeFn(q.id);
    }
    div.appendChild(h4);
    div.appendChild(p);
    div.appendChild(btn);
    container.appendChild(div);
  });
}
function completeDailyQuest(id) {
  const q = gameData.dailyQuests.find(x => x.id === id);
  if (!q || q.completed) return;
  q.completed = true;
  alert(`${q.title} を達成!\nEXP+${q.exp}, Gold+${q.gold}`);
  gainExp(q.exp);
  gainGold(q.gold);
  saveData();
  updateAllUI();
}
function completeNormalQuest(id) {
  const q = gameData.normalQuests.find(x => x.id === id);
  if (!q || q.completed) return;
  q.completed = true;
  alert(`${q.title} を達成!\nEXP+${q.exp}, Gold+${q.gold}`);
  gainExp(q.exp);
  gainGold(q.gold);
  saveData();
  updateAllUI();
}
function completeWeeklyQuest(id) {
  const q = gameData.weeklyQuests.find(x => x.id === id);
  if (!q || q.completed) return;
  q.completed = true;
  alert(`${q.title} を達成!\nEXP+${q.exp}, Gold+${q.gold}`);
  gainExp(q.exp);
  gainGold(q.gold);
  saveData();
  updateAllUI();
}
function dailyQuestResetCheck() {
  const now = new Date();
  const todayStr = now.toDateString();
  if (gameData.lastDailyReset !== todayStr) {
    gameData.dailyQuests.forEach(q => q.completed = false);
    gameData.lastDailyReset = todayStr;
    alert("デイリークエストをリセットしました!");
    saveData();
  }
}
function weeklyQuestResetCheck() {
  const now = new Date();
  const year = now.getFullYear();
  const weekNum = Math.floor((now.getDate() - now.getDay() + 10) / 7);
  const currentWeekKey = `${year}-W${weekNum}`;
  if (gameData.lastWeeklyReset !== currentWeekKey) {
    gameData.weeklyQuests.forEach(q => q.completed = false);
    gameData.lastWeeklyReset = currentWeekKey;
    alert("ウィークリークエストをリセットしました!");
    saveData();
  }
}

/* =========================================
   8) ワールドマップ & ボス(複数ターン制)
========================================= */
function updateMapUI() {
  const container = document.getElementById("mapAreaContainer");
  container.innerHTML = "";
  const heroLevel = gameData.hero.level;
  gameData.mapAreas.forEach(area => {
    const areaDiv = document.createElement("div");
    areaDiv.className = "map-area";
    if (heroLevel < area.levelReq) {
      areaDiv.classList.add("locked-area");
    }
    areaDiv.innerHTML = `<h3>${area.name} (Lv.${area.levelReq}~)</h3><p>${area.desc}</p>`;
    if (area.boss && heroLevel >= area.levelReq) {
      const btn = document.createElement("button");
      btn.className = "btn-primary";
      btn.textContent = `${area.boss.name} と戦う`;
      btn.onclick = () => {
        // ボスも enemies配列扱い
        const boss = JSON.parse(JSON.stringify(area.boss));
        startBattle([boss], `ボス戦: ${boss.name}`, true);
      };
      areaDiv.appendChild(btn);
    }
    container.appendChild(areaDiv);
  });
}

/* =========================================
   9) ランダムエンカウント (複数敵対応)
========================================= */
function startRandomEncounter() {
  // ランダムに1~2体の敵を出す
  const n = Math.random() < 0.5 ? 1 : 2;
  let chosenEnemies = [];
  for (let i = 0; i < n; i++) {
    const e = gameData.enemies[Math.floor(Math.random() * gameData.enemies.length)];
    chosenEnemies.push(JSON.parse(JSON.stringify(e)));
  }
  startBattle(chosenEnemies, "ダンジョン潜入 - 複数敵が出現!");
}

/* =========================================
   10) 個別ターン制バトルロジック
========================================= */
let battleState = {
  combatants: [], // 全ユニット(勇者/仲間/敵)
  turnIndex: 0,
  log: [],
  battleOver: false,
  isBossFight: false
};

function startBattle(enemyList, title, isBoss=false) {
  battleState.combatants = [];
  battleState.turnIndex = 0;
  battleState.log = [];
  battleState.battleOver = false;
  battleState.isBossFight = isBoss;

  // 味方(勇者)
  const hero = gameData.hero;
  battleState.combatants.push({
    unitType: "hero",
    name: hero.name,
    hp: hero.hp,
    maxHp: hero.maxHp,
    speed: hero.speed,
    isDown: false,
    ref: hero,
    activeSkills: hero.activeSkills || []
  });
  // 仲間
  gameData.party.forEach(m => {
    battleState.combatants.push({
      unitType: "party",
      name: m.name,
      hp: m.hp,
      maxHp: m.maxHp,
      speed: m.speed || 5,
      isDown: false,
      ref: m,
      activeSkills: m.activeSkills || []
    });
  });
  // 敵
  enemyList.forEach(e => {
    battleState.combatants.push({
      unitType: "enemy",
      name: e.name,
      hp: e.hp,
      maxHp: e.maxHp,
      speed: e.speed || 5,
      atk: e.atk || 5,
      exp: e.exp || 0,
      gold: e.gold || 0,
      dropMat: e.dropMat || null,
      rewardExp: e.rewardExp,
      rewardGold: e.rewardGold,
      isDown: false
    });
  });

  // ソート(速度降順)
  battleState.combatants.sort((a,b) => b.speed - a.speed);

  document.getElementById("battleTitle").textContent = title;
  document.getElementById("battleDesc").textContent = isBoss ? "ボス戦だ!" : "敵が現れた!";
  openBattleModal();
  updateBattleUI();
  battleState.log.push("バトル開始!");
  // 行動者チェック
  checkCurrentTurn();
}

function openBattleModal() {
  document.getElementById("battleModalBg").style.display = "flex";
}
function closeBattleModal() {
  document.getElementById("battleModalBg").style.display = "none";
  updateAllUI();
}
function updateBattleUI() {
  // 敵一覧
  let enemyText = "<h3>敵ユニット</h3>";
  battleState.combatants
    .filter(c => c.unitType === "enemy")
    .forEach(c => {
      if (!c.isDown) {
        enemyText += `<p>${c.name} HP:${c.hp}/${c.maxHp}</p>`;
      } else {
        enemyText += `<p>${c.name} (撃破)</p>`;
      }
    });
  document.getElementById("battleEnemyInfo").innerHTML = enemyText;

  // 味方一覧
  let allyText = "<h3>味方ユニット</h3>";
  battleState.combatants
    .filter(c => c.unitType==="hero" || c.unitType==="party")
    .forEach(c => {
      if (!c.isDown) {
        allyText += `<p>${c.name} HP:${c.hp}/${c.maxHp}</p>`;
      } else {
        allyText += `<p>${c.name} (戦闘不能)</p>`;
      }
    });
  document.getElementById("battleHeroInfo").innerHTML = allyText;

  // ログ
  document.getElementById("battleLog").innerHTML = battleState.log.join("<br>");
}

/* 行動者確認 */
function checkCurrentTurn() {
  if (battleState.battleOver) return;
  if (battleState.turnIndex >= battleState.combatants.length) {
    battleState.turnIndex = 0;
  }
  let currentUnit = battleState.combatants[battleState.turnIndex];
  if (currentUnit.isDown) {
    // 次へ
    battleState.turnIndex++;
    checkBattleEnd();
    checkCurrentTurn();
    return;
  }

  if (currentUnit.unitType === "hero" || currentUnit.unitType === "party") {
    // プレイヤー(味方)行動
    battleState.log.push(`[${currentUnit.name} のターン]`);
    updateBattleUI();
    // ボタン操作で行動を待つ
  } else {
    // 敵行動
    battleState.log.push(`[${currentUnit.name} のターン(敵)]`);
    updateBattleUI();
    setTimeout(() => {
      enemyAction(currentUnit);
    }, 600);
  }
}

/* プレイヤー行動系 */
// 今どのキャラが行動中か
function getCurrentUnit() {
  if (battleState.turnIndex < battleState.combatants.length) {
    return battleState.combatants[battleState.turnIndex];
  }
  return null;
}

// 攻撃
function chooseAttack() {
  const currentUnit = getCurrentUnit();
  if (!currentUnit) return;
  if (currentUnit.unitType==="enemy") return; // 敵は自動行動

  // ターゲット: 生存している敵
  const aliveEnemies = battleState.combatants.filter(c => c.unitType==="enemy" && !c.isDown);
  if (aliveEnemies.length === 0) return; // 敵がいない
  const target = aliveEnemies[0]; // 仮に先頭を攻撃(本当は選択UIを用意してもOK)

  doAttack(currentUnit, target);
}
function doAttack(attacker, defender) {
  battleState.log.push(`${attacker.name}の攻撃!`);
  const dmg = calcDamage(attacker, defender, false);
  defender.hp -= dmg;
  battleState.log.push(`→ ${defender.name} に ${dmg} ダメージ!`);
  if (defender.hp<=0) {
    defender.hp=0;
    defender.isDown=true;
    battleState.log.push(`${defender.name}は倒れた!`);
  }
  endPlayerAction();
}

// スキル
function chooseSkill() {
  const currentUnit = getCurrentUnit();
  if (!currentUnit) return;
  if (currentUnit.activeSkills.length === 0) {
    alert("スキルがありません!");
    return;
  }
  // 例: 1つだけスキルがあるなら即使用 or promptで選択
  const skill = currentUnit.activeSkills[0];
  // ターゲット選択(敵)
  const aliveEnemies = battleState.combatants.filter(c => c.unitType==="enemy" && !c.isDown);
  if (aliveEnemies.length === 0) {
    alert("敵がいません");
    return;
  }
  const target = aliveEnemies[0]; // 簡易: 先頭
  battleState.log.push(`${currentUnit.name}は${skill.name}を発動!`);
  const dmg = skill.baseDamage + Math.floor(Math.random()*3);
  target.hp -= dmg;
  battleState.log.push(`→ ${target.name} に ${dmg} ダメージ!`);
  if (target.hp<=0) {
    target.hp=0;
    target.isDown=true;
    battleState.log.push(`${target.name}は倒れた!`);
  }
  endPlayerAction();
}

// アイテム
function chooseItem() {
  const currentUnit = getCurrentUnit();
  if (!currentUnit) return;
  if (currentUnit.unitType!=="hero") {
    alert("このキャラはアイテムを使えません。");
    return;
  }
  const hero = currentUnit.ref;
  if ((hero.consumables.potion || 0) <1) {
    alert("回復薬がありません。");
    return;
  }
  // 対象を自分に限定(簡易)
  hero.consumables.potion--;
  const heal=30;
  currentUnit.hp += heal;
  if(currentUnit.hp>currentUnit.maxHp) currentUnit.hp=currentUnit.maxHp;
  battleState.log.push(`${currentUnit.name}は回復薬を使用 → HP+${heal}`);
  endPlayerAction();
}

// 防御
function chooseDefend() {
  const currentUnit = getCurrentUnit();
  if (!currentUnit) return;
  currentUnit.isDefending=true; // 被ダメ半減など
  battleState.log.push(`${currentUnit.name}は身を守っている!(被ダメ軽減)`);
  endPlayerAction();
}

// 逃げる
function chooseFlee() {
  battleState.log.push("逃げ出した!");
  endBattle(false);
}

// プレイヤー行動終了
function endPlayerAction() {
  battleState.turnIndex++;
  checkBattleEnd();
  setTimeout(() => {
    checkCurrentTurn();
    updateBattleUI();
  }, 400);
}

// 敵行動
function enemyAction(enemyUnit) {
  // ターゲット: 生存している味方( hero/party )
  const aliveAllies = battleState.combatants.filter(c => (c.unitType==="hero"||c.unitType==="party") && !c.isDown);
  if (aliveAllies.length===0) {
    checkBattleEnd();
    return;
  }
  const target = aliveAllies[Math.floor(Math.random()*aliveAllies.length)];
  battleState.log.push(`${enemyUnit.name}の攻撃 → ${target.name}`);
  const dmg = calcDamage(enemyUnit, target, target.isDefending);
  target.hp -= dmg;
  if (target.isDefending) {
    battleState.log.push("(防御中で被ダメ軽減)");
  }
  battleState.log.push(`→ ${target.name}に${dmg}ダメージ!`);
  target.isDefending=false; // 防御は1ターンのみ

  if (target.hp<=0) {
    target.hp=0;
    target.isDown=true;
    battleState.log.push(`${target.name}は倒れた…`);
  }

  battleState.turnIndex++;
  checkBattleEnd();
  setTimeout(() => {
    checkCurrentTurn();
    updateBattleUI();
  }, 400);
}

/* ダメージ計算 */
function calcDamage(attacker, defender, defenderIsDefending) {
  let baseAtk=0;
  if (attacker.unitType==="hero"||attacker.unitType==="party") {
    // 味方の攻撃力
    // (装備や職業スキルなどは未細分化。下のcalcPlayerTotalAttackを簡易流用でもOK)
    baseAtk=10;
    if(attacker.ref && attacker.ref.job==="戦士") {
      const wSkills=gameData.skills.warrior;
      wSkills.forEach(s => {
        if(s.name==="剣術熟練") baseAtk+=(s.level*2);
      });
    }
    if(attacker.ref && attacker.ref.job==="魔法使い") {
      const mSkills=gameData.skills.mage;
      mSkills.forEach(s => {
        if(s.name==="魔力増強") baseAtk+=(s.level*3);
        if(s.name==="精神集中" && battleState.isBossFight) baseAtk+=(s.level*5);
      });
    }
    if(attacker.ref && attacker.ref.job==="盗賊") {
      const tSkills=gameData.skills.thief;
      tSkills.forEach(s=>{
        if(s.name==="素早さ強化") baseAtk+=(s.level*2);
      });
    }
    // 装備
    if(attacker.ref && attacker.ref.equipment.weapon) {
      baseAtk += (attacker.ref.equipment.weapon.attack||0);
    }
    if(attacker.ref && attacker.ref.equipment.armor) {
      baseAtk += (attacker.ref.equipment.armor.attack||0);
    }
    if(attacker.ref && attacker.ref.equipment.accessory) {
      baseAtk += (attacker.ref.equipment.accessory.attack||0);
    }
    // さらに仲間の場合はattackプロパティ?
    if(attacker.unitType==="party") {
      baseAtk += attacker.ref.attack; // m.attack
    }
  } else {
    // 敵
    baseAtk=attacker.atk||5;
  }
  // 防御中なら半減
  let finalDmg = baseAtk + Math.floor(Math.random()*3);
  if(defenderIsDefending) {
    finalDmg = Math.floor(finalDmg/2);
  }
  return finalDmg;
}

/* 勝利/敗北判定 */
function checkBattleEnd() {
  // 味方生存
  const aliveAllies = battleState.combatants.filter(c => (c.unitType==="hero"||c.unitType==="party") && !c.isDown);
  if(aliveAllies.length===0) {
    // 敗北
    battleState.log.push("味方は全滅した…");
    doLoseBattle();
    return true;
  }
  // 敵生存
  const aliveEnemies= battleState.combatants.filter(c => c.unitType==="enemy" && !c.isDown);
  if(aliveEnemies.length===0) {
    // 勝利
    battleState.log.push("敵を全て倒した! 勝利!");
    doWinBattle();
    return true;
  }
  return false;
}

function doWinBattle() {
  battleState.battleOver=true;
  // ボス or 雑魚敵全体のEXP/Gold
  let totalExp=0, totalGold=0;
  battleState.combatants.forEach(c=>{
    if(c.unitType==="enemy") {
      if(c.rewardExp) totalExp += c.rewardExp; else totalExp += (c.exp||0);
      if(c.rewardGold) totalGold += c.rewardGold; else totalGold += (c.gold||0);
    }
  });
  // 盗賊スキルでGoldボーナス
  if(gameData.hero.job==="盗賊"){
    const tSkills=gameData.skills.thief;
    tSkills.forEach(s=>{
      if(s.name==="ゴールド盗み") {
        totalGold+=(s.level*5);
      }
    });
  }
  gainExp(totalExp);
  gainGold(totalGold);
  battleState.log.push(`報酬 → EXP:${totalExp}, Gold:${totalGold}`);

  // 倒れた味方をHP1で復帰
  battleState.combatants.forEach(c=>{
    if((c.unitType==="hero"||c.unitType==="party") && c.isDown){
      c.isDown=false; c.hp=1;
      if(c.ref) c.ref.hp=1;
    } else {
      if(c.ref) c.ref.hp=c.hp; // HPを反映
    }
  });
  setTimeout(()=>{
    endBattle(true);
  },800);
}

function doLoseBattle() {
  battleState.battleOver=true;
  // ゴールド半減
  gameData.hero.gold = Math.floor(gameData.hero.gold/2);
  battleState.log.push("所持Goldが半分になった…");
  // 全員HP1で復帰
  battleState.combatants.forEach(c=>{
    if(c.unitType==="hero"||c.unitType==="party"){
      c.isDown=false;
      c.hp=1;
      if(c.ref) c.ref.hp=1;
    }
  });
  setTimeout(()=>{
    endBattle(false);
  },800);
}

function endBattle(win) {
  battleState.log.push(win ? "(勝利) バトル終了" : "(終了) バトル終了");
  updateBattleUI();
  setTimeout(()=>{
    closeBattleModal();
  },1000);
}

function calcPlayerTotalAttack(){
  // ※ 旧の一斉攻撃計算用は使わなくなったが、参考に残しておく
  return 10;
}

/* ボス撃破時の実績 & ストーリー */
function onBossDefeated(bossName) {
  gameData.achievements.forEach(a => {
    if(a.type==="bossKill" && a.bossName===bossName && !a.unlocked){
      unlockAchievement(a);
    }
  });
  const st= gameData.story.find(x=>x.bossName===bossName);
  if(st){
    battleState.log.push(st.text);
  }
  saveData();
}

/* =========================================
   11) 実績 & ストーリー
========================================= */
function updateAchievementsUI() {
  const listEl = document.getElementById("achievementList");
  listEl.innerHTML = "";
  gameData.achievements.forEach(a => {
    const div = document.createElement("div");
    div.className = "achievement";
    if(!a.unlocked) div.classList.add("locked");
    const h4 = document.createElement("h4");
    h4.textContent = a.title;
    const p = document.createElement("p");
    p.textContent = a.desc;
    if(!a.unlocked){
      const lockedLabel=document.createElement("div");
      lockedLabel.className="locked-label";
      lockedLabel.textContent="Locked";
      div.appendChild(lockedLabel);
    }
    div.appendChild(h4);
    div.appendChild(p);
    listEl.appendChild(div);
  });
}
function checkAchievements() {
  const hero=gameData.hero;
  const totalQuestCompleted= gameData.dailyQuests.filter(q=>q.completed).length
    + gameData.normalQuests.filter(q=>q.completed).length
    + gameData.weeklyQuests.filter(q=>q.completed).length;
  gameData.achievements.forEach(a=>{
    if(a.unlocked) return;
    switch(a.type){
      case "questCount":
        if(totalQuestCompleted>=a.target){
          unlockAchievement(a);
        }
        break;
      case "level":
        if(hero.level>=a.target){
          unlockAchievement(a);
        }
        break;
      case "bossKill":
        // boss討伐時に individually check
        break;
      case "gold":
        if(hero.gold>=a.target){
          unlockAchievement(a);
        }
        break;
    }
  });
}
function unlockAchievement(a) {
  a.unlocked=true;
  alert(`実績解除!「${a.title}」`);
  saveData();
  updateAchievementsUI();
}
function updateStoryProgress(){
  let text="";
  const bossKills= gameData.achievements.filter(a=>a.type==="bossKill"&&a.unlocked).map(a=>a.bossName);
  bossKills.forEach(bn=>{
    const st=gameData.story.find(x=>x.bossName===bn);
    if(st){
      text+=`<p>【${bn}】<br>${st.text}</p>`;
    }
  });
  if(!text) text="<p>まだ大きなストーリーは進んでいません。</p>";
  document.getElementById("storyProgress").innerHTML=text;
}

/* =========================================
   12) UIの一括更新
========================================= */
function updateAllUI(){
  updateHeroUI();
  updatePartyUI();
  updateSkillTreeUI();
  updateCraftUI();
  updateQuestsUI();
  updateMapUI();
  updateAchievementsUI();
  updateStoryProgress();
}
</script>
</body>
</html>

WEBサービスのアイディア

  • ここにいくつかのWEBサービスアイデアを提案します:
  • タスク自動化サービス:
  • 各種のタスク(例:ファイル管理、データバックアップ、レポート作成など)を自動化するクラウドサービス。ユーザーが日常的な作業を簡単にスクリプトやAIを使って自動化できる。
  • AI学習サポートプラットフォーム:
  • 受験生や学習者向けに、個別の学習プランを作成し、進捗を自動的に管理するサービス。AIがユーザーの理解度に応じた問題を提案したり、弱点克服のためのアドバイスを提供する。
  • ローカルコミュニティSNS:
  • 地域ごとのローカルなコミュニティSNS。近所でのイベントやニュース、フリーマーケット情報など、地域に特化した情報を交換できる。
  • オンラインスキルシェアプラットフォーム:
  • ユーザーが自身の専門知識やスキルを他のユーザーに教えたり、学んだりできるプラットフォーム。講師として登録でき、動画コンテンツやライブ講義を提供できる。
  • デジタルライフオーガナイザー:
  • ユーザーのオンラインアカウント、サブスクリプション、パスワードなどを一括で管理し、期限が近づいたら通知を送るサービス。セキュリティに配慮したデータ管理機能も持つ。
  • 趣味特化型Q&Aサイト:
  • 趣味(例:写真撮影、DIY、園芸、料理など)に特化したQ&Aサイト。ユーザーが同じ趣味を持つ仲間と意見交換や質問ができる。
  • AIパーソナルトレーナー:
  • フィットネスや食事管理をAIがサポートするサービス。日々の運動プランや食事の提案、進捗管理を行い、目標に合わせて調整。
  • ライティング支援ツール:
  • 小説、ブログ、エッセイなど、文章作成に特化したAI支援ツール。文法チェックやアイデア生成、構成アドバイスなどを提供。
  • クラウドベースのプロジェクト管理ツール:
  • チームでのプロジェクト管理を効率化するため、タスクの進捗管理、ファイル共有、コミュニケーションを一元化するツール。特にリモートワーク向けの機能が充実。
  • キャリアアドバイス&マッチングプラットフォーム:
  • AIが個人のスキルや経験に基づいてキャリアアドバイスを提供し、適切な企業やプロジェクトとマッチングしてくれるプラットフォーム。