<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Crawlio Search</title>
<style>
:root {
color-scheme: light;
--text: #202124;
--muted: #5f6368;
--line: #dadce0;
--blue: #4285f4;
--red: #ea4335;
--yellow: #fbbc04;
--green: #34a853;
--shadow: 0 18px 40px rgba(60, 64, 67, 0.15);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--text);
font-family: Arial, "Hiragino Kaku Gothic ProN", "Yu Gothic", Meiryo, sans-serif;
background:
radial-gradient(circle at top left, rgba(66, 133, 244, 0.12), transparent 32rem),
linear-gradient(180deg, #fff 0%, #f7f9fc 68%, #eef3fa 100%);
}
a {
color: inherit;
text-decoration: none;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 64px;
padding: 0 28px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 7px;
color: #3c4043;
font-size: 15px;
font-weight: 700;
}
.brand-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.nav {
display: flex;
gap: 22px;
color: #3c4043;
font-size: 14px;
}
.nav a:hover {
text-decoration: underline;
}
main {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
}
.search-shell {
position: relative;
display: grid;
place-items: center;
min-height: 430px;
padding: 38px 0 46px;
overflow: hidden;
}
.crawler-visual {
position: absolute;
inset: 12px 0 auto;
height: 320px;
pointer-events: none;
opacity: 0.92;
}
.orbit {
position: absolute;
left: 50%;
top: 50%;
border: 1px solid rgba(95, 99, 104, 0.18);
border-radius: 50%;
transform: translate(-50%, -50%);
}
.orbit-a {
width: min(640px, 86vw);
height: 210px;
}
.orbit-b {
width: min(440px, 68vw);
height: 145px;
transform: translate(-50%, -50%) rotate(-12deg);
}
.node {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: 0 0 0 8px rgba(66, 133, 244, 0.08);
}
.node-a {
left: calc(50% - 302px);
top: 120px;
background: var(--blue);
}
.node-b {
left: calc(50% + 250px);
top: 88px;
background: var(--green);
}
.node-c {
left: calc(50% + 72px);
top: 216px;
background: var(--red);
}
.scan-line {
position: absolute;
left: 50%;
top: 64px;
width: 3px;
height: 220px;
background: linear-gradient(180deg, transparent, rgba(66, 133, 244, 0.72), transparent);
animation: scan 3.4s ease-in-out infinite;
}
.wordmark {
position: relative;
z-index: 1;
margin: 52px 0 25px;
font-size: clamp(68px, 12vw, 112px);
font-weight: 700;
line-height: 0.95;
}
.blue { color: var(--blue); }
.red { color: var(--red); }
.yellow { color: var(--yellow); }
.green { color: var(--green); }
.search-form {
position: relative;
z-index: 1;
width: min(640px, 100%);
}
.search-box {
display: grid;
grid-template-columns: 24px 1fr 42px;
align-items: center;
min-height: 58px;
padding: 0 8px 0 21px;
background: #fff;
border: 1px solid var(--line);
border-radius: 32px;
box-shadow: 0 2px 8px rgba(60, 64, 67, 0.08);
transition: box-shadow 160ms ease, border-color 160ms ease;
}
.search-box:focus-within,
.search-box:hover {
border-color: transparent;
box-shadow: var(--shadow);
}
.search-box svg,
.icon-button svg {
width: 22px;
height: 22px;
fill: #5f6368;
}
input {
width: 100%;
border: 0;
outline: 0;
padding: 0 14px;
color: var(--text);
font-size: 17px;
background: transparent;
}
.icon-button {
display: grid;
place-items: center;
width: 42px;
height: 42px;
border: 0;
border-radius: 50%;
background: transparent;
cursor: pointer;
}
.icon-button:hover {
background: #f1f3f4;
}
.actions {
display: flex;
justify-content: center;
gap: 12px;
margin-top: 24px;
}
.actions button {
min-width: 112px;
min-height: 38px;
border: 1px solid #f8f9fa;
border-radius: 4px;
padding: 0 18px;
color: #3c4043;
background: #f8f9fa;
font-size: 14px;
cursor: pointer;
}
.actions button:hover {
border-color: #dadce0;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.08);
}
.results-area {
display: none;
max-width: 760px;
margin: 0 auto 46px;
}
.results-area.visible {
display: block;
}
.result-meta {
margin-bottom: 18px;
color: var(--muted);
font-size: 14px;
}
.result {
padding: 18px 0;
border-top: 1px solid #edf0f2;
}
.result-url {
color: #3c4043;
font-size: 13px;
}
.result h3 {
margin: 4px 0 6px;
color: #1a0dab;
font-size: 21px;
font-weight: 400;
}
.result p {
margin: 0;
color: #4d5156;
font-size: 14px;
line-height: 1.55;
}
.crawler-panel {
margin: 12px 0 28px;
padding: 24px;
background: rgba(255, 255, 255, 0.82);
border: 1px solid rgba(218, 220, 224, 0.9);
border-radius: 8px;
box-shadow: 0 12px 32px rgba(60, 64, 67, 0.08);
backdrop-filter: blur(10px);
}
.panel-heading {
display: flex;
align-items: end;
justify-content: space-between;
gap: 18px;
margin-bottom: 18px;
}
.eyebrow {
margin: 0 0 4px;
color: var(--blue);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h2 {
margin: 0;
font-size: 24px;
}
.pulse {
display: inline-flex;
align-items: center;
gap: 8px;
color: #137333;
font-size: 12px;
font-weight: 700;
}
.pulse::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 0 8px rgba(52, 168, 83, 0.12);
}
.crawl-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.crawl-card {
min-height: 122px;
padding: 16px;
border: 1px solid #e6eaee;
border-radius: 8px;
background: #fff;
}
.crawl-card strong {
display: block;
margin-bottom: 8px;
font-size: 15px;
}
.crawl-card span {
display: block;
color: var(--muted);
font-size: 13px;
line-height: 1.45;
}
.crawl-progress {
height: 5px;
margin-top: 14px;
overflow: hidden;
border-radius: 999px;
background: #edf0f2;
}
.crawl-progress i {
display: block;
height: 100%;
width: var(--progress);
border-radius: inherit;
background: linear-gradient(90deg, var(--blue), var(--green));
}
.stats-band {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1px;
overflow: hidden;
margin-bottom: 44px;
border: 1px solid #dfe4ea;
border-radius: 8px;
background: #dfe4ea;
}
.stats-band div {
padding: 22px;
background: #fff;
}
.stats-band strong,
.stats-band span {
display: block;
}
.stats-band strong {
margin-bottom: 5px;
font-size: 27px;
}
.stats-band span {
color: var(--muted);
font-size: 13px;
}
footer {
display: flex;
flex-wrap: wrap;
gap: 22px;
padding: 18px 28px;
color: #70757a;
background: #f2f2f2;
font-size: 14px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@keyframes scan {
0%, 100% {
transform: translateX(-260px);
opacity: 0.35;
}
50% {
transform: translateX(260px);
opacity: 1;
}
}
@media (max-width: 760px) {
.topbar {
padding: 0 16px;
}
.nav {
gap: 12px;
font-size: 13px;
}
.search-shell {
min-height: 390px;
}
.crawler-visual {
height: 270px;
}
.node-a {
left: 6%;
}
.node-b {
left: 86%;
}
.node-c {
left: 58%;
}
.actions {
flex-wrap: wrap;
}
.crawl-grid,
.stats-band {
grid-template-columns: 1fr;
}
.panel-heading {
align-items: start;
flex-direction: column;
}
}
</style>
</head>
<body>
<header class="topbar">
<a class="brand" href="#" aria-label="Crawlio Search">
<span class="brand-dot blue"></span>
<span class="brand-dot red"></span>
<span class="brand-dot yellow"></span>
<span class="brand-dot green"></span>
<span>Crawlio</span>
</a>
<nav class="nav" aria-label="メイン">
<a href="#crawler">Crawler</a>
<a href="#index">Index</a>
<a href="#status">Status</a>
</nav>
</header>
<main>
<section class="search-shell" aria-labelledby="hero-title">
<div class="crawler-visual" aria-hidden="true">
<div class="orbit orbit-a"></div>
<div class="orbit orbit-b"></div>
<div class="node node-a"></div>
<div class="node node-b"></div>
<div class="node node-c"></div>
<div class="scan-line"></div>
</div>
<h1 id="hero-title" class="wordmark">
<span class="blue">C</span><span class="red">r</span><span class="yellow">a</span><span class="blue">w</span><span class="green">l</span><span class="red">i</span><span class="blue">o</span>
</h1>
<form class="search-form" id="searchForm">
<label class="sr-only" for="query">検索キーワード</label>
<div class="search-box">
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M10.8 18a7.2 7.2 0 1 1 5.1-12.3 7.2 7.2 0 0 1-5.1 12.3Zm0-2a5.2 5.2 0 1 0 0-10.4 5.2 5.2 0 0 0 0 10.4Zm6.3.1 4 4-1.4 1.4-4-4 1.4-1.4Z" />
</svg>
<input id="query" name="query" autocomplete="off" placeholder="URL、キーワード、サイト名を検索" />
<button class="icon-button" type="button" id="voiceButton" aria-label="音声検索">
<svg aria-hidden="true" viewBox="0 0 24 24">
<path d="M12 14a3 3 0 0 0 3-3V6a3 3 0 1 0-6 0v5a3 3 0 0 0 3 3Zm5-3a5 5 0 0 1-10 0H5a7 7 0 0 0 6 6.9V21h2v-3.1a7 7 0 0 0 6-6.9h-2Z" />
</svg>
</button>
</div>
<div class="actions">
<button type="submit">検索</button>
<button type="button" id="crawlButton">クローラーを走らせる</button>
</div>
</form>
</section>
<section class="results-area" aria-live="polite">
<div class="result-meta" id="resultMeta">約 8,420,000 件中 0.38 秒</div>
<div class="results" id="results"></div>
</section>
<section class="crawler-panel" id="crawler" aria-labelledby="crawler-title">
<div class="panel-heading">
<div>
<p class="eyebrow">Live Crawl</p>
<h2 id="crawler-title">巡回中のページ</h2>
</div>
<span class="pulse">ONLINE</span>
</div>
<div class="crawl-grid" id="crawlGrid"></div>
</section>
<section class="stats-band" id="index" aria-label="インデックス統計">
<div>
<strong>12.8B</strong>
<span>Indexed pages</span>
</div>
<div>
<strong>94ms</strong>
<span>Median lookup</span>
</div>
<div>
<strong>37K/s</strong>
<span>Crawl rate</span>
</div>
<div>
<strong>99.98%</strong>
<span>Freshness</span>
</div>
</section>
</main>
<footer id="status">
<span>Japan</span>
<span>Privacy</span>
<span>Terms</span>
<span>Search Console</span>
</footer>
<script>
const results = [
{
title: "Crawlio Search Console - サイトのクロール状況",
url: "https://crawlio.example/search-console",
text: "サイトマップ、robots.txt、インデックス登録、検索パフォーマンスをまとめて確認できます。"
},
{
title: "高速インデックスの仕組み",
url: "https://crawlio.example/docs/indexing",
text: "分散クローラーがページを発見し、内容を解析して、新しい検索結果へ反映します。"
},
{
title: "ニュース、画像、動画を横断検索",
url: "https://crawlio.example/discover",
text: "キーワードに関連するページ、メディア、トレンドをひとつの検索画面で素早く探せます。"
},
{
title: "Web Crawler Health Report",
url: "https://status.crawlio.example/crawler",
text: "現在のクロール速度、エラー率、再訪問キュー、インデックス鮮度のライブ統計です。"
}
];
const crawlItems = [
["news.metro.jp/today", "HTML parsed / 32 links discovered", 78],
["shop.example.com/products", "Sitemap queued / canonical found", 64],
["docs.dev.local/api", "Robots allowed / snippets updated", 91],
["media.example.net/video", "Metadata extracted / thumbnail indexed", 56],
["blog.studio.jp/launch", "Fresh content detected / rank signals ready", 84],
["archive.city.jp/events", "Recrawl scheduled / duplicate checked", 43]
];
const form = document.querySelector("#searchForm");
const queryInput = document.querySelector("#query");
const resultsArea = document.querySelector(".results-area");
const resultMeta = document.querySelector("#resultMeta");
const resultList = document.querySelector("#results");
const crawlGrid = document.querySelector("#crawlGrid");
const crawlButton = document.querySelector("#crawlButton");
const voiceButton = document.querySelector("#voiceButton");
function renderResults(query = "クローラー") {
const filtered = results.map((item) => ({
...item,
title: query ? `${item.title} | ${query}` : item.title
}));
resultMeta.textContent = `約 ${(8420000 + query.length * 17321).toLocaleString("ja-JP")} 件中 ${(0.21 + Math.random() * 0.28).toFixed(2)} 秒`;
resultList.innerHTML = filtered
.map(
(item) => `
<article class="result">
<div class="result-url">${item.url}</div>
<h3>${item.title}</h3>
<p>${item.text}</p>
</article>
`
)
.join("");
resultsArea.classList.add("visible");
}
function renderCrawlGrid(offset = 0) {
crawlGrid.innerHTML = crawlItems
.map(([url, status, progress], index) => {
const shifted = Math.min(99, Math.max(24, progress + ((offset + index * 7) % 18) - 7));
return `
<article class="crawl-card">
<strong>${url}</strong>
<span>${status}</span>
<div class="crawl-progress" aria-label="クロール進捗 ${shifted}%">
<i style="--progress: ${shifted}%"></i>
</div>
</article>
`;
})
.join("");
}
form.addEventListener("submit", (event) => {
event.preventDefault();
renderResults(queryInput.value.trim() || "クローラー");
});
crawlButton.addEventListener("click", () => {
renderCrawlGrid(Math.floor(Math.random() * 20));
renderResults(queryInput.value.trim() || "live crawl");
document.querySelector("#crawler").scrollIntoView({ behavior: "smooth", block: "start" });
});
voiceButton.addEventListener("click", () => {
queryInput.value = "最新のインデックス状況";
queryInput.focus();
});
renderCrawlGrid();
</script>
</body>
</html>
タグ: programming
ネタ神AI Pro – アイデアメーカー
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ネタ神AI Pro - アイデアメーカー</title>
<style>
:root {
--bg: #070914;
--bg2: #111831;
--card: rgba(255, 255, 255, 0.08);
--card2: rgba(255, 255, 255, 0.13);
--text: #f5f7ff;
--muted: #aeb8df;
--line: rgba(255, 255, 255, 0.16);
--primary: #7c5cff;
--cyan: #00d4ff;
--green: #38ffad;
--yellow: #ffd35c;
--red: #ff5c7c;
--shadow: 0 22px 55px rgba(0, 0, 0, 0.35);
--radius: 22px;
}
body.light {
--bg: #eef2ff;
--bg2: #ffffff;
--card: rgba(255, 255, 255, 0.8);
--card2: rgba(255, 255, 255, 0.95);
--text: #151829;
--muted: #566179;
--line: rgba(20, 30, 60, 0.14);
--shadow: 0 18px 45px rgba(40, 60, 110, 0.16);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", "Hiragino Sans", "Yu Gothic", sans-serif;
color: var(--text);
background:
radial-gradient(circle at 20% 10%, rgba(124, 92, 255, 0.32), transparent 28%),
radial-gradient(circle at 90% 20%, rgba(0, 212, 255, 0.24), transparent 28%),
radial-gradient(circle at 50% 100%, rgba(255, 92, 124, 0.15), transparent 32%),
linear-gradient(135deg, var(--bg), var(--bg2));
transition: 0.25s;
}
button,
input,
select,
textarea {
font-family: inherit;
}
button {
border: 1px solid var(--line);
border-radius: 14px;
background: var(--card2);
color: var(--text);
padding: 11px 14px;
font-weight: 800;
cursor: pointer;
transition: 0.2s;
backdrop-filter: blur(12px);
}
button:hover {
transform: translateY(-2px);
filter: brightness(1.08);
}
.btn-main {
border: none;
background: linear-gradient(135deg, var(--primary), var(--cyan));
color: white;
box-shadow: 0 16px 35px rgba(0, 212, 255, 0.2);
}
.btn-green {
border: none;
background: linear-gradient(135deg, #13bf84, var(--green));
color: #06120d;
}
.btn-red {
border-color: rgba(255, 92, 124, 0.4);
background: rgba(255, 92, 124, 0.14);
}
.app {
width: min(1380px, 94%);
margin: 0 auto;
padding: 28px 0 70px;
}
header {
display: flex;
justify-content: space-between;
gap: 18px;
align-items: center;
margin-bottom: 22px;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.logo {
width: 58px;
height: 58px;
border-radius: 20px;
background: linear-gradient(135deg, var(--primary), var(--cyan));
display: grid;
place-items: center;
font-size: 30px;
box-shadow: 0 20px 45px rgba(124, 92, 255, 0.35);
}
h1, h2, h3, h4, p {
margin-top: 0;
}
.brand h1 {
margin: 0;
font-size: clamp(27px, 4vw, 46px);
letter-spacing: 0.03em;
}
.brand p {
margin: 4px 0 0;
color: var(--muted);
font-size: 14px;
}
.header-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.hero {
border: 1px solid var(--line);
background: var(--card);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(16px);
padding: 26px;
margin-bottom: 22px;
overflow: hidden;
position: relative;
}
.hero::after {
content: "";
position: absolute;
width: 320px;
height: 320px;
right: -120px;
bottom: -160px;
border-radius: 50%;
background: rgba(0, 212, 255, 0.13);
filter: blur(8px);
}
.hero h2 {
font-size: clamp(24px, 3vw, 40px);
margin-bottom: 10px;
line-height: 1.35;
}
.hero p {
color: var(--muted);
line-height: 1.8;
max-width: 900px;
margin-bottom: 0;
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 22px;
}
.stat {
border: 1px solid var(--line);
background: var(--card);
border-radius: 18px;
padding: 16px;
box-shadow: var(--shadow);
}
.stat strong {
display: block;
font-size: 24px;
margin-bottom: 3px;
}
.stat span {
color: var(--muted);
font-size: 13px;
}
.layout {
display: grid;
grid-template-columns: 420px 1fr;
gap: 22px;
align-items: start;
}
.panel {
border: 1px solid var(--line);
background: var(--card);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(16px);
overflow: hidden;
}
.panel-header {
padding: 18px 20px;
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
}
.panel-header h3 {
margin: 0;
font-size: 19px;
}
.panel-body {
padding: 20px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
background: linear-gradient(135deg, var(--green), var(--cyan));
color: #06121c;
font-size: 12px;
font-weight: 900;
white-space: nowrap;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
color: var(--muted);
font-size: 13px;
font-weight: 800;
margin-bottom: 8px;
}
input,
select,
textarea {
width: 100%;
border: 1px solid var(--line);
background: rgba(0, 0, 0, 0.22);
color: var(--text);
border-radius: 14px;
padding: 12px 13px;
outline: none;
font-size: 15px;
transition: 0.2s;
}
body.light input,
body.light select,
body.light textarea {
background: rgba(255, 255, 255, 0.85);
}
select option {
background: #10162a;
color: white;
}
input:focus,
select:focus,
textarea:focus {
border-color: rgba(0, 212, 255, 0.85);
box-shadow: 0 0 0 4px rgba(0, 212, 255, 0.12);
}
textarea {
min-height: 105px;
resize: vertical;
line-height: 1.7;
}
.two {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.chip {
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.08);
color: var(--muted);
border-radius: 999px;
padding: 8px 10px;
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: 0.2s;
}
.chip:hover {
color: var(--text);
border-color: rgba(0, 212, 255, 0.7);
transform: translateY(-1px);
}
.button-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 16px;
}
.button-grid .wide-btn {
grid-column: 1 / -1;
}
.result-tools {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
.empty {
text-align: center;
padding: 70px 24px;
color: var(--muted);
}
.empty .icon {
font-size: 64px;
margin-bottom: 12px;
}
.idea-list {
display: grid;
gap: 16px;
}
.idea-card {
border: 1px solid var(--line);
background: rgba(0, 0, 0, 0.18);
border-radius: 20px;
overflow: hidden;
}
body.light .idea-card {
background: rgba(255, 255, 255, 0.78);
}
.idea-top {
padding: 20px;
border-bottom: 1px solid var(--line);
display: grid;
grid-template-columns: 1fr auto;
gap: 15px;
align-items: start;
}
.idea-title {
margin: 0;
font-size: clamp(24px, 3vw, 36px);
line-height: 1.25;
}
.meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.pill {
font-size: 12px;
font-weight: 900;
padding: 6px 9px;
border-radius: 999px;
border: 1px solid var(--line);
color: var(--muted);
background: rgba(255,255,255,0.07);
}
.score-box {
width: 96px;
text-align: center;
padding: 12px;
border-radius: 18px;
background: linear-gradient(135deg, rgba(124, 92, 255, 0.35), rgba(0, 212, 255, 0.24));
border: 1px solid var(--line);
}
.score-box strong {
display: block;
font-size: 26px;
}
.score-box span {
color: var(--muted);
font-size: 12px;
font-weight: 800;
}
.catch {
padding: 16px 20px;
font-size: 17px;
line-height: 1.7;
background: rgba(255, 255, 255, 0.07);
border-bottom: 1px solid var(--line);
}
.idea-body {
padding: 20px;
}
.sections {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.section {
border: 1px solid var(--line);
border-radius: 17px;
padding: 16px;
background: rgba(0, 0, 0, 0.18);
}
body.light .section {
background: rgba(255, 255, 255, 0.62);
}
.section.wide {
grid-column: 1 / -1;
}
.section h4 {
margin: 0 0 10px;
font-size: 15px;
}
.section p,
.section li {
color: var(--muted);
line-height: 1.75;
font-size: 14px;
}
.section p {
margin-bottom: 0;
}
.section ul,
.section ol {
margin: 0;
padding-left: 22px;
}
.idea-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0 20px 20px;
}
.history-controls {
display: grid;
grid-template-columns: 1fr 180px;
gap: 10px;
margin-bottom: 14px;
}
.history-list {
display: grid;
gap: 10px;
}
.history-item {
border: 1px solid var(--line);
border-radius: 16px;
padding: 13px;
cursor: pointer;
background: rgba(0, 0, 0, 0.15);
transition: 0.2s;
}
body.light .history-item {
background: rgba(255, 255, 255, 0.7);
}
.history-item:hover {
transform: translateY(-2px);
border-color: rgba(0, 212, 255, 0.6);
}
.history-item strong {
display: block;
margin-bottom: 5px;
}
.history-item small {
color: var(--muted);
}
.toast {
position: fixed;
right: 20px;
bottom: 20px;
background: rgba(10, 15, 30, 0.94);
color: white;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 16px;
padding: 14px 18px;
box-shadow: var(--shadow);
opacity: 0;
transform: translateY(20px);
pointer-events: none;
transition: 0.25s;
z-index: 100;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
.footer {
margin-top: 28px;
text-align: center;
color: var(--muted);
font-size: 13px;
}
@media (max-width: 1050px) {
header {
flex-direction: column;
align-items: flex-start;
}
.layout {
grid-template-columns: 1fr;
}
.stats {
grid-template-columns: repeat(2, 1fr);
}
.sections {
grid-template-columns: 1fr;
}
.idea-top {
grid-template-columns: 1fr;
}
.score-box {
width: 100%;
}
}
@media (max-width: 620px) {
.two,
.button-grid,
.history-controls,
.stats {
grid-template-columns: 1fr;
}
.header-actions {
width: 100%;
}
.header-actions button {
flex: 1;
}
}
</style>
</head>
<body>
<div class="app">
<header>
<div class="brand">
<div class="logo">💡</div>
<div>
<h1>ネタ神AI Pro</h1>
<p>API不要。ブラウザだけで動く創作・Webサービス企画メーカー</p>
</div>
</div>
<div class="header-actions">
<button onclick="toggleTheme()">テーマ切替</button>
<button onclick="downloadText()">TXT出力</button>
<button onclick="downloadJSON()">JSON出力</button>
<button class="btn-red" onclick="clearAll()">全削除</button>
</div>
</header>
<section class="hero">
<h2>APIなしでも、かなり使える「企画書生成ツール」にする。</h2>
<p>
外部AIに接続せず、ローカルのテンプレート・ランダム生成・条件分岐だけで、Webサービス、AIツール、ゲーム、小説、SNSなどの企画案を作ります。
API料金もキー管理も不要です。まず作品として公開しやすい形です。
</p>
</section>
<section class="stats">
<div class="stat">
<strong id="statIdeas">0</strong>
<span>今回生成した案</span>
</div>
<div class="stat">
<strong id="statSaved">0</strong>
<span>保存済みアイデア</span>
</div>
<div class="stat">
<strong>0円</strong>
<span>API利用料</span>
</div>
<div class="stat">
<strong>100%</strong>
<span>ローカル動作</span>
</div>
</section>
<main class="layout">
<section class="panel">
<div class="panel-header">
<h3>生成条件</h3>
<span class="badge">NO API</span>
</div>
<div class="panel-body">
<div class="two">
<div class="form-group">
<label for="genre">ジャンル</label>
<select id="genre">
<option>Webサービス</option>
<option>AIツール</option>
<option>ゲーム</option>
<option>小説</option>
<option>SNS</option>
<option>動画サイト</option>
<option>ポートフォリオ</option>
<option>便利ツール</option>
<option>学習サービス</option>
<option>創作支援</option>
</select>
</div>
<div class="form-group">
<label for="mood">雰囲気</label>
<select id="mood">
<option>かっこいい</option>
<option>やさしい</option>
<option>近未来</option>
<option>ファンタジー</option>
<option>シンプル</option>
<option>高級感</option>
<option>かわいい</option>
<option>ダーク</option>
<option>実用的</option>
<option>ゲーム風</option>
</select>
</div>
</div>
<div class="two">
<div class="form-group">
<label for="level">開発難易度</label>
<select id="level">
<option>簡単</option>
<option>普通</option>
<option>本格</option>
<option>超本格</option>
</select>
</div>
<div class="form-group">
<label for="target">ターゲット</label>
<select id="target">
<option>個人クリエイター</option>
<option>学生</option>
<option>社会人</option>
<option>在宅ワーカー</option>
<option>ゲーム制作者</option>
<option>小説家志望</option>
<option>配信者</option>
<option>初心者</option>
<option>副業したい人</option>
</select>
</div>
</div>
<div class="two">
<div class="form-group">
<label for="amount">生成数</label>
<select id="amount">
<option value="1">1個</option>
<option value="3" selected>3個</option>
<option value="5">5個</option>
</select>
</div>
<div class="form-group">
<label for="style">出力スタイル</label>
<select id="style">
<option>企画書風</option>
<option>サービス紹介風</option>
<option>開発メモ風</option>
<option>ピッチ資料風</option>
</select>
</div>
</div>
<div class="form-group">
<label for="keywords">キーワード</label>
<textarea id="keywords" placeholder="例:AI / RPG / SNS / メモ / 仕事 / 創作 / ポートフォリオ"></textarea>
<div class="chips">
<span class="chip" onclick="addKeyword('AI')">AI</span>
<span class="chip" onclick="addKeyword('RPG')">RPG</span>
<span class="chip" onclick="addKeyword('SNS')">SNS</span>
<span class="chip" onclick="addKeyword('小説')">小説</span>
<span class="chip" onclick="addKeyword('仕事')">仕事</span>
<span class="chip" onclick="addKeyword('メモ')">メモ</span>
<span class="chip" onclick="addKeyword('ポートフォリオ')">ポートフォリオ</span>
<span class="chip" onclick="addKeyword('動画')">動画</span>
<span class="chip" onclick="addKeyword('学習')">学習</span>
<span class="chip" onclick="addKeyword('ゲーム開発')">ゲーム開発</span>
</div>
</div>
<div class="form-group">
<label for="problem">解決したい悩み</label>
<textarea id="problem" placeholder="例:何を作ればいいかわからない。作業が続かない。アイデアを整理できない。"></textarea>
</div>
<div class="button-grid">
<button class="btn-main wide-btn" onclick="generateIdeas()">アイデア生成</button>
<button onclick="randomSet()">ランダム条件</button>
<button onclick="makePractical()">現実的にする</button>
<button onclick="makeFantasy()">派手にする</button>
<button onclick="clearForm()">入力クリア</button>
</div>
</div>
</section>
<section class="panel">
<div class="panel-header">
<h3>生成結果</h3>
<div class="result-tools">
<button onclick="copyAll()">コピー</button>
<button class="btn-green" onclick="saveAll()">全部保存</button>
</div>
</div>
<div class="panel-body">
<div id="result">
<div class="empty">
<div class="icon">🧠</div>
<h2>まだアイデアはありません</h2>
<p>左の条件を入れて「アイデア生成」を押してください。</p>
</div>
</div>
</div>
</section>
</main>
<section class="panel" style="margin-top:22px;">
<div class="panel-header">
<h3>保存したアイデア</h3>
<span class="badge" id="savedCount">0件</span>
</div>
<div class="panel-body">
<div class="history-controls">
<input id="historySearch" placeholder="保存アイデアを検索" oninput="renderHistory()" />
<select id="historyGenre" onchange="renderHistory()">
<option value="all">全ジャンル</option>
<option>Webサービス</option>
<option>AIツール</option>
<option>ゲーム</option>
<option>小説</option>
<option>SNS</option>
<option>動画サイト</option>
<option>ポートフォリオ</option>
<option>便利ツール</option>
<option>学習サービス</option>
<option>創作支援</option>
</select>
</div>
<div class="history-list" id="historyList"></div>
</div>
</section>
<div class="footer">
ネタ神AI Pro / APIなしローカル版 / HTML・CSS・JavaScriptのみ
</div>
</div>
<div class="toast" id="toast">完了しました</div>
<script>
const DATA = {
titleHeads: [
"Nova", "Idea", "Neta", "Mira", "Chrono", "Elder", "Prompt", "Vision",
"Craft", "Yume", "Neo", "Astra", "Luna", "Meta", "Spark", "Quest"
],
titleTails: {
"Webサービス": ["Hub", "Works", "Base", "Cloud", "Studio", "Panel", "Link", "Board"],
"AIツール": ["AI", "Brain", "Agent", "Prompt", "Copilot", "Mind", "Assist", "Genius"],
"ゲーム": ["Quest", "Chronicle", "Saga", "Blade", "Dungeon", "Legend", "Arc", "World"],
"小説": ["Novel", "Story", "Tale", "Script", "Lore", "Ink", "Scene", "Dream"],
"SNS": ["Verse", "Circle", "Post", "Talk", "Room", "Link", "Wave", "Nest"],
"動画サイト": ["Tube", "Stream", "Clip", "Vision", "Cast", "Channel", "View", "Media"],
"ポートフォリオ": ["Portfolio", "Gallery", "Works", "Profile", "Card", "Showcase", "Archive", "Page"],
"便利ツール": ["Tool", "Memo", "Desk", "Kit", "Task", "Quick", "Utility", "Simple"],
"学習サービス": ["Learn", "Study", "Lesson", "Skill", "Academy", "Trainer", "Coach", "Note"],
"創作支援": ["Create", "Maker", "Muse", "Seed", "Craft", "Atelier", "Generator", "Factory"]
},
moodDesc: {
"かっこいい": "鋭く洗練された印象で、使うだけで制作意欲が上がる",
"やさしい": "初心者でも迷わない、安心感のある",
"近未来": "AI時代らしい自動化と先進性を感じる",
"ファンタジー": "クエストやギルドのような世界観を活かした",
"シンプル": "余計な機能を削り、すぐ使えることに集中した",
"高級感": "プロ向けツールのように落ち着いた印象の",
"かわいい": "親しみやすく、毎日開きたくなる",
"ダーク": "深い世界観と中二感を活かした",
"実用的": "仕事や制作の効率化に直結する",
"ゲーム風": "レベル、経験値、クエストのような要素を持つ"
},
features: {
"Webサービス": ["ユーザー投稿", "検索", "タグ分類", "お気に入り", "ランキング", "管理画面", "コメント", "カテゴリ管理", "共有リンク", "レスポンシブUI"],
"AIツール": ["文章生成", "テンプレート選択", "プロンプト保存", "履歴管理", "自動分類", "要約", "言い換え", "コピー", "お気に入り", "出力形式変更"],
"ゲーム": ["キャラクター管理", "クエスト", "ステージ選択", "スキル", "装備", "敵図鑑", "ストーリー分岐", "進行度保存", "称号", "実績"],
"小説": ["キャラ設定", "世界観管理", "章立て", "プロット", "セリフ案", "伏線メモ", "用語集", "文体変換", "シーン整理", "年表"],
"SNS": ["タイムライン", "投稿", "いいね", "フォロー", "通知", "プロフィール", "ハッシュタグ", "DM風UI", "おすすめ投稿", "AI投稿提案"],
"動画サイト": ["動画カード", "検索", "カテゴリ", "ランキング", "チャンネル", "視聴履歴", "コメント", "お気に入り", "おすすめ", "タグ"],
"ポートフォリオ": ["作品カード", "リンク管理", "カテゴリ分類", "紹介文生成", "スキル表示", "実績一覧", "検索", "テーマ変更", "外部リンク", "更新履歴"],
"便利ツール": ["メモ", "ToDo", "検索", "タグ", "自動整形", "コピー", "履歴", "エクスポート", "チェックリスト", "通知風表示"],
"学習サービス": ["学習記録", "復習リスト", "クイズ", "用語集", "進捗", "AI風解説", "弱点メモ", "計画作成", "達成バッジ", "問題生成"],
"創作支援": ["アイデア生成", "タイトル案", "キャッチコピー", "キャラ案", "世界観案", "企画書化", "画像プロンプト", "構成案", "メモ保存", "ネタ帳"]
},
monetization: [
"無料版+Pro版",
"広告表示",
"買い切り版",
"テンプレート販売",
"月額プレミアム",
"法人向けプラン",
"追加保存枠の課金",
"作品公開ページの有料カスタム",
"支援・投げ銭",
"素材パック販売"
],
risks: [
"機能を増やしすぎると完成しにくくなる",
"最初からログインや課金を入れると開発が重くなる",
"ターゲットが広すぎると特徴が薄くなる",
"保存機能の設計を後回しにすると作り直しが出やすい",
"見た目だけ作って実用性が弱いと使われにくい",
"スマホ対応を忘れると使い勝手が落ちる"
],
firstSteps: {
"簡単": ["1画面UIを作る", "入力欄と生成ボタンを作る", "結果表示を作る", "コピー機能を付ける", "ローカル保存を付ける"],
"普通": ["基本UIを作る", "複数パターン生成を作る", "履歴保存を作る", "検索と絞り込みを作る", "テキスト出力を作る"],
"本格": ["MVPを作る", "保存データ構造を決める", "ログインなし版を完成させる", "ユーザー登録版を検討する", "公開ページを整える"],
"超本格": ["小さいMVPを先に作る", "フロントとバックエンドを分ける", "DB設計をする", "課金やログインを後から追加する", "運用コストを確認する"]
}
};
let currentIdeas = [];
let generatedCount = 0;
function $(id) {
return document.getElementById(id);
}
function val(id) {
return $(id).value.trim();
}
function pick(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function shuffle(arr) {
return [...arr].sort(() => Math.random() - 0.5);
}
function escapeHTML(str) {
return String(str)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function addKeyword(word) {
const box = $("keywords");
if (!box.value.includes(word)) {
box.value = box.value ? box.value + " / " + word : word;
}
}
function randomSet() {
randomSelect("genre");
randomSelect("mood");
randomSelect("level");
randomSelect("target");
randomSelect("style");
const sets = [
"AI / メモ / 作業効率",
"RPG / クエスト / 進捗管理",
"SNS / 投稿 / AI風返信",
"小説 / 世界観 / キャラクター",
"在宅ワーク / 日報 / 整理",
"ポートフォリオ / 作品 / 自動紹介",
"動画 / まとめ / ランキング",
"学習 / 復習 / クイズ",
"ゲーム開発 / アイデア / 仕様書",
"創作 / ネタ帳 / 企画書"
];
const problems = [
"何を作ればいいかわからない",
"作業が続かない",
"アイデアが散らばって整理できない",
"作品紹介文を書くのが難しい",
"学習した内容を忘れやすい",
"毎日の進捗を見える化したい",
"企画を作っても途中で止まりやすい"
];
$("keywords").value = pick(sets);
$("problem").value = pick(problems);
showToast("ランダム条件を入れました");
}
function randomSelect(id) {
const el = $(id);
el.selectedIndex = Math.floor(Math.random() * el.options.length);
}
function makePractical() {
$("mood").value = "実用的";
$("level").value = "簡単";
$("style").value = "開発メモ風";
if (!$("problem").value) {
$("problem").value = "毎日の作業やアイデアを整理して、次にやることを明確にしたい";
}
showToast("現実的な条件に寄せました");
}
function makeFantasy() {
$("mood").value = "ファンタジー";
$("style").value = "サービス紹介風";
if (!$("keywords").value.includes("ギルド")) {
addKeyword("ギルド");
addKeyword("クエスト");
}
showToast("派手な条件に寄せました");
}
function clearForm() {
$("keywords").value = "";
$("problem").value = "";
showToast("入力をクリアしました");
}
function generateIdeas() {
const amount = Number(val("amount"));
currentIdeas = [];
for (let i = 0; i < amount; i++) {
currentIdeas.push(createIdea(i));
}
generatedCount += amount;
$("statIdeas").textContent = generatedCount;
renderIdeas();
showToast(`${amount}個のアイデアを生成しました`);
}
function createIdea(index) {
const genre = val("genre");
const mood = val("mood");
const level = val("level");
const target = val("target");
const style = val("style");
const keywords = val("keywords") || "AI / 創作 / アイデア";
const problem = val("problem") || "アイデアを整理して、作り始めやすくしたい";
const title = makeTitle(genre, keywords, index);
const features = shuffle(DATA.features[genre]).slice(0, 6);
const mvp = features.slice(0, 3);
const money = shuffle(DATA.monetization).slice(0, 3);
const risks = shuffle(DATA.risks).slice(0, 3);
const steps = DATA.firstSteps[level];
const score = calcScore(genre, level);
const keywordMain = splitKeywords(keywords)[0] || "アイデア";
return {
id: Date.now() + Math.random(),
createdAt: new Date().toLocaleString("ja-JP"),
title,
genre,
mood,
level,
target,
style,
keywords,
problem,
score,
catchcopy: makeCatch(target, mood, genre, keywordMain),
overview: makeOverview(genre, mood, target, keywords, problem, style),
unique: makeUnique(genre, mood, keywordMain),
features,
mvp,
money,
risks,
steps,
devTime: makeDevTime(level),
nextAction: makeNextAction(level, genre),
design: makeDesign(mood),
pitch: makePitch(title, target, genre, problem)
};
}
function splitKeywords(text) {
return text.split(/[\/、,\s]+/).map(x => x.trim()).filter(Boolean);
}
function makeTitle(genre, keywords, index) {
const keys = splitKeywords(keywords);
const key = keys[index % Math.max(keys.length, 1)] || "Idea";
const head = pick(DATA.titleHeads);
const tail = pick(DATA.titleTails[genre] || DATA.titleTails["創作支援"]);
const patterns = [
`${head}${tail}`,
`${key}${tail}`,
`${head} ${tail}`,
`${key}メーカー`,
`${key}ギルド`,
`${key}Forge`,
`${head}ノート`,
`${key}ラボ`,
`${head}Factory`,
`${key}クエスト`
];
return pick(patterns);
}
function makeCatch(target, mood, genre, key) {
const desc = DATA.moodDesc[mood];
const patterns = [
`${target}の「作りたい」を形にする、${desc}${genre}。`,
`${key}を起点に、企画・整理・実行まで支える${genre}。`,
`思いつきを企画に変える、${target}向けの${desc}サービス。`,
`迷っている時間を減らし、制作を前に進める${genre}。`
];
return pick(patterns);
}
function makeOverview(genre, mood, target, keywords, problem, style) {
const desc = DATA.moodDesc[mood];
if (style === "ピッチ資料風") {
return `${problem}という悩みを持つ${target}に向けて、${keywords}を軸にした${genre}を提供します。${desc}体験により、ユーザーはアイデア出しから整理、実行までを短時間で進められます。`;
}
if (style === "開発メモ風") {
return `${keywords}をテーマにした${genre}。まずは小さく作る。${target}が抱える「${problem}」を解決するため、生成、保存、検索、コピーの流れを重視する。`;
}
if (style === "サービス紹介風") {
return `この${genre}は、${target}が${keywords}に関するアイデアをすばやく整理できるサービスです。${desc}デザインで、毎日開きたくなる使い心地を目指します。`;
}
return `${keywords}をテーマにした${genre}です。${target}が抱える「${problem}」を解決するため、アイデア出し、情報整理、保存、次の行動提案をまとめて行える企画にします。`;
}
function makeUnique(genre, mood, key) {
return `${key}をただ生成するだけでなく、MVP、開発手順、収益化、注意点まで同時に出せる点が特徴です。${mood}な方向性を明確にすることで、似たような${genre}との差別化もしやすくなります。`;
}
function calcScore(genre, level) {
let score = 82;
if (level === "簡単") score += 10;
if (level === "普通") score += 5;
if (level === "本格") score -= 2;
if (level === "超本格") score -= 9;
if (genre === "便利ツール") score += 4;
if (genre === "AIツール") score += 3;
if (genre === "ゲーム") score -= 4;
if (genre === "SNS") score -= 3;
score += Math.floor(Math.random() * 9) - 4;
return Math.max(55, Math.min(98, score));
}
function makeDevTime(level) {
if (level === "簡単") return "1日〜3日";
if (level === "普通") return "1週間〜2週間";
if (level === "本格") return "1か月〜3か月";
return "3か月以上";
}
function makeNextAction(level, genre) {
if (level === "簡単") {
return `まずは${genre}の1画面版を作ります。入力欄、生成ボタン、結果表示、保存だけで完成扱いにするのが安全です。`;
}
if (level === "普通") {
return `最初にUIを作り、そのあと保存・検索・出力機能を追加します。ログイン機能は後回しで大丈夫です。`;
}
if (level === "本格") {
return `MVPを公開できる状態まで作ってから、ユーザー登録やデータベースを検討します。最初から全部入れない方が完成します。`;
}
return `超本格版は重いので、まずはプロトタイプを完成させてください。完成後にサーバー、DB、課金、ログインを分割して追加する流れが安全です。`;
}
function makeDesign(mood) {
const map = {
"かっこいい": "黒背景、青紫グラデーション、カード型UI、シャープなボタン",
"やさしい": "白背景、淡い青や緑、角丸カード、大きめ文字",
"近未来": "ダーク背景、ネオン、水色アクセント、ガラス風UI",
"ファンタジー": "羊皮紙風、ギルドカード、クエストボード風UI",
"シンプル": "白背景、余白多め、入力欄と結果表示を中心にする",
"高級感": "黒と金、細い罫線、落ち着いたカードUI",
"かわいい": "パステルカラー、丸いボタン、アイコン多め",
"ダーク": "黒、赤紫、重厚な影、世界観重視",
"実用的": "管理画面風、見出し明確、コピー・保存ボタンを目立たせる",
"ゲーム風": "ステータス画面、経験値バー、クエストカード風"
};
return map[mood] || "カード型で見やすいUI";
}
function makePitch(title, target, genre, problem) {
return `${title}は、${target}が抱える「${problem}」を解決する${genre}です。複雑な作業を整理し、次にやることを明確にすることで、制作や仕事を止めずに進められるようにします。`;
}
function renderIdeas() {
const result = $("result");
if (currentIdeas.length === 0) {
result.innerHTML = `
<div class="empty">
<div class="icon">🧠</div>
<h2>まだアイデアはありません</h2>
<p>左の条件を入れて「アイデア生成」を押してください。</p>
</div>
`;
return;
}
result.innerHTML = `
<div class="idea-list">
${currentIdeas.map(renderIdeaCard).join("")}
</div>
`;
}
function renderIdeaCard(idea, index) {
return `
<article class="idea-card">
<div class="idea-top">
<div>
<h2 class="idea-title">${escapeHTML(idea.title)}</h2>
<div class="meta">
<span class="pill">${escapeHTML(idea.genre)}</span>
<span class="pill">${escapeHTML(idea.mood)}</span>
<span class="pill">${escapeHTML(idea.level)}</span>
<span class="pill">${escapeHTML(idea.target)}</span>
<span class="pill">開発目安 ${escapeHTML(idea.devTime)}</span>
</div>
</div>
<div class="score-box">
<strong>${idea.score}</strong>
<span>実現度</span>
</div>
</div>
<div class="catch">
${escapeHTML(idea.catchcopy)}
</div>
<div class="idea-body">
<div class="sections">
<div class="section wide">
<h4>📝 概要</h4>
<p>${escapeHTML(idea.overview)}</p>
</div>
<div class="section">
<h4>🎯 解決する悩み</h4>
<p>${escapeHTML(idea.problem)}</p>
</div>
<div class="section">
<h4>🎨 デザイン方針</h4>
<p>${escapeHTML(idea.design)}</p>
</div>
<div class="section">
<h4>⚙️ 主な機能</h4>
<ul>
${idea.features.map(x => `<li>${escapeHTML(x)}</li>`).join("")}
</ul>
</div>
<div class="section">
<h4>🚀 MVP機能</h4>
<ol>
${idea.mvp.map(x => `<li>${escapeHTML(x)}</li>`).join("")}
</ol>
</div>
<div class="section">
<h4>💰 収益化案</h4>
<ul>
${idea.money.map(x => `<li>${escapeHTML(x)}</li>`).join("")}
</ul>
</div>
<div class="section">
<h4>⚠️ リスク</h4>
<ul>
${idea.risks.map(x => `<li>${escapeHTML(x)}</li>`).join("")}
</ul>
</div>
<div class="section wide">
<h4>✅ 開発ステップ</h4>
<ol>
${idea.steps.map(x => `<li>${escapeHTML(x)}</li>`).join("")}
</ol>
</div>
<div class="section wide">
<h4>✨ 差別化ポイント</h4>
<p>${escapeHTML(idea.unique)}</p>
</div>
<div class="section wide">
<h4>📣 紹介文</h4>
<p>${escapeHTML(idea.pitch)}</p>
</div>
<div class="section wide">
<h4>👉 次にやること</h4>
<p>${escapeHTML(idea.nextAction)}</p>
</div>
</div>
</div>
<div class="idea-actions">
<button onclick="copyOne(${index})">この案をコピー</button>
<button onclick="saveOne(${index})">保存</button>
<button onclick="regenerateOne(${index})">この案だけ再生成</button>
</div>
</article>
`;
}
function ideaToText(idea) {
return `
【タイトル】
${idea.title}
【ジャンル】
${idea.genre}
【雰囲気】
${idea.mood}
【ターゲット】
${idea.target}
【開発難易度】
${idea.level}
【開発目安】
${idea.devTime}
【実現度】
${idea.score}点
【キャッチコピー】
${idea.catchcopy}
【解決する悩み】
${idea.problem}
【概要】
${idea.overview}
【主な機能】
${idea.features.map((x, i) => `${i + 1}. ${x}`).join("\n")}
【MVP機能】
${idea.mvp.map((x, i) => `${i + 1}. ${x}`).join("\n")}
【収益化案】
${idea.money.map((x, i) => `${i + 1}. ${x}`).join("\n")}
【リスク】
${idea.risks.map((x, i) => `${i + 1}. ${x}`).join("\n")}
【開発ステップ】
${idea.steps.map((x, i) => `${i + 1}. ${x}`).join("\n")}
【デザイン方針】
${idea.design}
【差別化ポイント】
${idea.unique}
【紹介文】
${idea.pitch}
【次にやること】
${idea.nextAction}
`.trim();
}
function copyOne(index) {
const idea = currentIdeas[index];
if (!idea) return;
copyText(ideaToText(idea));
}
function copyAll() {
if (currentIdeas.length === 0) {
showToast("先に生成してください");
return;
}
copyText(currentIdeas.map(ideaToText).join("\n\n====================\n\n"));
}
function copyText(text) {
navigator.clipboard.writeText(text)
.then(() => showToast("コピーしました"))
.catch(() => showToast("コピーに失敗しました"));
}
function regenerateOne(index) {
currentIdeas[index] = createIdea(index);
renderIdeas();
showToast("再生成しました");
}
function getSaved() {
try {
return JSON.parse(localStorage.getItem("netagami_saved")) || [];
} catch {
return [];
}
}
function setSaved(data) {
localStorage.setItem("netagami_saved", JSON.stringify(data));
updateStats();
}
function saveOne(index) {
const idea = currentIdeas[index];
if (!idea) return;
const saved = getSaved();
saved.unshift(idea);
setSaved(saved.slice(0, 100));
renderHistory();
showToast("保存しました");
}
function saveAll() {
if (currentIdeas.length === 0) {
showToast("先に生成してください");
return;
}
const saved = getSaved();
setSaved([...currentIdeas, ...saved].slice(0, 100));
renderHistory();
showToast("全部保存しました");
}
function renderHistory() {
const list = $("historyList");
const saved = getSaved();
const q = $("historySearch").value.trim().toLowerCase();
const genre = $("historyGenre").value;
let filtered = saved;
if (genre !== "all") {
filtered = filtered.filter(x => x.genre === genre);
}
if (q) {
filtered = filtered.filter(x => {
return JSON.stringify(x).toLowerCase().includes(q);
});
}
$("savedCount").textContent = `${saved.length}件`;
if (filtered.length === 0) {
list.innerHTML = `<p style="color:var(--muted);">保存アイデアはありません。</p>`;
return;
}
list.innerHTML = filtered.map(item => `
<div class="history-item" onclick="loadSaved('${item.id}')">
<strong>${escapeHTML(item.title)}</strong>
<small>${escapeHTML(item.genre)} / ${escapeHTML(item.level)} / ${escapeHTML(item.createdAt)}</small>
</div>
`).join("");
}
function loadSaved(id) {
const saved = getSaved();
const idea = saved.find(x => String(x.id) === String(id));
if (!idea) return;
currentIdeas = [idea];
renderIdeas();
window.scrollTo({ top: 0, behavior: "smooth" });
showToast("保存アイデアを表示しました");
}
function downloadText() {
if (currentIdeas.length === 0) {
showToast("先に生成してください");
return;
}
const text = currentIdeas.map(ideaToText).join("\n\n====================\n\n");
downloadFile("netagami-ideas.txt", text, "text/plain");
showToast("TXT出力しました");
}
function downloadJSON() {
const data = currentIdeas.length ? currentIdeas : getSaved();
if (data.length === 0) {
showToast("出力するデータがありません");
return;
}
downloadFile("netagami-ideas.json", JSON.stringify(data, null, 2), "application/json");
showToast("JSON出力しました");
}
function downloadFile(filename, content, type) {
const blob = new Blob([content], { type: type + ";charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function clearAll() {
if (!confirm("生成結果と保存履歴を削除しますか?")) return;
currentIdeas = [];
localStorage.removeItem("netagami_saved");
renderIdeas();
renderHistory();
updateStats();
showToast("削除しました");
}
function toggleTheme() {
document.body.classList.toggle("light");
localStorage.setItem("netagami_theme", document.body.classList.contains("light") ? "light" : "dark");
}
function updateStats() {
$("statSaved").textContent = getSaved().length;
}
function showToast(message) {
const toast = $("toast");
toast.textContent = message;
toast.classList.add("show");
setTimeout(() => {
toast.classList.remove("show");
}, 1800);
}
function init() {
if (localStorage.getItem("netagami_theme") === "light") {
document.body.classList.add("light");
}
$("keywords").value = "AI / 創作 / Webサービス / メモ";
$("problem").value = "何を作ればいいかわからない。アイデアを企画書レベルまで整理したい。";
renderHistory();
updateStats();
}
init();
</script>
</body>
</html>
STGGAME.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bullet Hell STG Game</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top, #1f2b5c, #070915 70%);
display: flex;
align-items: center;
justify-content: center;
font-family: system-ui, sans-serif;
color: white;
overflow: hidden;
}
.wrap { text-align: center; }
h1 {
margin: 0 0 10px;
font-size: 28px;
letter-spacing: 0.08em;
}
canvas {
background: #050816;
border: 3px solid #ffffff33;
border-radius: 16px;
box-shadow: 0 20px 80px #000a;
display: block;
}
.info {
margin-top: 10px;
color: #dce6ff;
font-size: 14px;
}
.panel {
position: fixed;
top: 16px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px;
background: #0008;
border: 1px solid #fff2;
padding: 8px 16px;
border-radius: 999px;
backdrop-filter: blur(8px);
font-weight: 700;
}
</style>
</head>
<body>
<div class="panel">
<div>Score: <span id="score">0</span></div>
<div>HP: <span id="hp">5</span></div>
<div>Power: <span id="power">1</span></div>
</div>
<div class="wrap">
<h1>BULLET STORM</h1>
<canvas id="game" width="480" height="640"></canvas>
<div class="info">移動: WASD / 方向キー ショット: Space パワーアップを取ると弾が強化 リスタート: Enter</div>
</div>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const scoreEl = document.getElementById("score");
const hpEl = document.getElementById("hp");
const powerEl = document.getElementById("power");
const keys = {};
const player = {
x: canvas.width / 2,
y: canvas.height - 70,
w: 34,
h: 42,
speed: 5,
hp: 5,
power: 1,
shotCooldown: 0,
invincible: 0
};
let bullets = [];
let enemyBullets = [];
let enemies = [];
let items = [];
let particles = [];
let stars = [];
let score = 0;
let enemyTimer = 0;
let itemTimer = 0;
let gameOver = false;
let boss = null;
let bossTimer = 0;
let frame = 0;
for (let i = 0; i < 100; i++) {
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: Math.random() * 2 + 0.5,
speed: Math.random() * 2 + 1
});
}
window.addEventListener("keydown", (e) => {
keys[e.key.toLowerCase()] = true;
if (gameOver && e.key === "Enter") restart();
});
window.addEventListener("keyup", (e) => {
keys[e.key.toLowerCase()] = false;
});
function restart() {
player.x = canvas.width / 2;
player.y = canvas.height - 70;
player.hp = 5;
player.power = 1;
player.shotCooldown = 0;
player.invincible = 0;
bullets = [];
enemyBullets = [];
enemies = [];
items = [];
particles = [];
score = 0;
enemyTimer = 0;
itemTimer = 0;
frame = 0;
gameOver = false;
updateUI();
}
function updateUI() {
scoreEl.textContent = score;
hpEl.textContent = player.hp;
powerEl.textContent = player.power;
}
function addPlayerBullet(x, y, vx, vy, power = 1) {
bullets.push({ x, y, w: 6, h: 16, vx, vy, power });
}
function shoot() {
if (player.power === 1) {
addPlayerBullet(player.x, player.y - 25, 0, -10);
} else if (player.power === 2) {
addPlayerBullet(player.x - 9, player.y - 25, 0, -10);
addPlayerBullet(player.x + 9, player.y - 25, 0, -10);
} else if (player.power === 3) {
addPlayerBullet(player.x, player.y - 28, 0, -11);
addPlayerBullet(player.x - 16, player.y - 20, -1.2, -10);
addPlayerBullet(player.x + 16, player.y - 20, 1.2, -10);
} else {
addPlayerBullet(player.x, player.y - 30, 0, -12, 2);
addPlayerBullet(player.x - 14, player.y - 24, -0.8, -11);
addPlayerBullet(player.x + 14, player.y - 24, 0.8, -11);
addPlayerBullet(player.x - 24, player.y - 10, -1.7, -10);
addPlayerBullet(player.x + 24, player.y - 10, 1.7, -10);
}
player.shotCooldown = Math.max(4, 10 - player.power);
}
function spawnEnemy() {
if (boss) return;
const size = Math.random() * 16 + 28;
enemies.push({
x: Math.random() * (canvas.width - size) + size / 2,
y: -size,
w: size,
h: size,
speed: Math.random() * 1.4 + 1.4,
hp: size > 38 ? 4 : 2,
shotTimer: Math.floor(Math.random() * 50),
type: Math.random() < 0.35 ? "spread" : "normal"
});
}
function spawnPowerItem(x = Math.random() * (canvas.width - 40) + 20, y = -20) {
items.push({
x,
y,
w: 24,
h: 24,
speed: 2.2,
type: "power"
});
}
function enemyShoot(enemy) {
if (enemy.type === "spread") {
for (let i = -2; i <= 2; i++) {
enemyBullets.push({
x: enemy.x,
y: enemy.y + enemy.h / 2,
w: 8,
h: 8,
vx: i * 1.1,
vy: 3.2
});
}
} else {
const dx = player.x - enemy.x;
const dy = player.y - enemy.y;
const len = Math.hypot(dx, dy) || 1;
enemyBullets.push({
x: enemy.x,
y: enemy.y + enemy.h / 2,
w: 8,
h: 8,
vx: dx / len * 3.2,
vy: dy / len * 3.2
});
}
}
function createExplosion(x, y) {
for (let i = 0; i < 16; i++) {
particles.push({
x,
y,
vx: (Math.random() - 0.5) * 7,
vy: (Math.random() - 0.5) * 7,
life: 24,
r: Math.random() * 4 + 2
});
}
}
function isHit(a, b) {
return (
a.x - a.w / 2 < b.x + b.w / 2 &&
a.x + a.w / 2 > b.x - b.w / 2 &&
a.y - a.h / 2 < b.y + b.h / 2 &&
a.y + a.h / 2 > b.y - b.h / 2
);
}
function damagePlayer() {
if (player.invincible > 0) return;
player.hp--;
player.power = Math.max(1, player.power - 1);
player.invincible = 80;
createExplosion(player.x, player.y);
updateUI();
if (player.hp <= 0) gameOver = true;
}
function update() {
if (gameOver) return;
frame++;
// boss spawn
bossTimer++;
if (!boss && bossTimer > 2000) {
boss = {
x: canvas.width / 2,
y: 120,
w: 120,
h: 120,
hp: 200,
maxHp: 200,
shotTimer: 0
};
}
if (keys["arrowleft"] || keys["a"]) player.x -= player.speed;
if (keys["arrowright"] || keys["d"]) player.x += player.speed;
if (keys["arrowup"] || keys["w"]) player.y -= player.speed;
if (keys["arrowdown"] || keys["s"]) player.y += player.speed;
player.x = Math.max(player.w / 2, Math.min(canvas.width - player.w / 2, player.x));
player.y = Math.max(player.h / 2, Math.min(canvas.height - player.h / 2, player.y));
if (player.invincible > 0) player.invincible--;
if (player.shotCooldown > 0) player.shotCooldown--;
if ((keys[" "] || keys["space"]) && player.shotCooldown <= 0) shoot();
bullets.forEach((b) => {
b.x += b.vx;
b.y += b.vy;
});
bullets = bullets.filter((b) => b.y > -30 && b.x > -30 && b.x < canvas.width + 30);
enemyBullets.forEach((b) => {
b.x += b.vx;
b.y += b.vy;
});
enemyBullets = enemyBullets.filter((b) => b.y < canvas.height + 30 && b.x > -30 && b.x < canvas.width + 30);
enemyTimer++;
if (enemyTimer > Math.max(20, 40 - Math.floor(score / 1000))) {
spawnEnemy();
enemyTimer = 0;
}
itemTimer++;
if (itemTimer > 520) {
spawnPowerItem();
itemTimer = 0;
}
enemies.forEach((e) => {
e.y += e.speed;
e.shotTimer++;
if (e.shotTimer > 70) {
enemyShoot(e);
e.shotTimer = 0;
}
});
// boss behavior
if (boss) {
boss.shotTimer++;
if (boss.shotTimer % 40 === 0) {
for (let i = 0; i < 20; i++) {
const angle = (Math.PI * 2 / 20) * i + frame * 0.02;
enemyBullets.push({
x: boss.x,
y: boss.y,
w: 10,
h: 10,
vx: Math.cos(angle) * 3,
vy: Math.sin(angle) * 3
});
}
}
}
items.forEach((item) => {
item.y += item.speed;
item.x += Math.sin((frame + item.y) * 0.04) * 0.8;
});
items = items.filter((item) => item.y < canvas.height + 40);
for (let i = items.length - 1; i >= 0; i--) {
if (isHit(player, items[i])) {
player.power = Math.min(4, player.power + 1);
score += 300;
createExplosion(items[i].x, items[i].y);
items.splice(i, 1);
updateUI();
}
}
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
if (isHit(player, e)) {
createExplosion(e.x, e.y);
enemies.splice(i, 1);
damagePlayer();
continue;
}
if (e.y > canvas.height + 50) {
enemies.splice(i, 1);
damagePlayer();
}
}
for (let i = enemyBullets.length - 1; i >= 0; i--) {
if (isHit(player, enemyBullets[i])) {
enemyBullets.splice(i, 1);
damagePlayer();
}
}
for (let i = enemies.length - 1; i >= 0; i--) {
for (let j = bullets.length - 1; j >= 0; j--) {
if (isHit(enemies[i], bullets[j])) {
enemies[i].hp -= bullets[j].power;
bullets.splice(j, 1);
if (enemies[i].hp <= 0) {
const drop = Math.random() < 0.18;
if (drop) spawnPowerItem(enemies[i].x, enemies[i].y);
createExplosion(enemies[i].x, enemies[i].y);
enemies.splice(i, 1);
score += 100;
updateUI();
}
break;
}
}
}
particles.forEach((p) => {
p.x += p.vx;
p.y += p.vy;
p.life--;
});
particles = particles.filter((p) => p.life > 0);
stars.forEach((s) => {
s.y += s.speed;
if (s.y > canvas.height) {
s.y = 0;
s.x = Math.random() * canvas.width;
}
});
}
function drawPlayer() {
if (player.invincible > 0 && Math.floor(frame / 5) % 2 === 0) return;
ctx.save();
ctx.translate(player.x, player.y);
// body
const grad = ctx.createLinearGradient(0, -20, 0, 30);
grad.addColorStop(0, "#7df9ff");
grad.addColorStop(1, "#0077ff");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(0, -26);
ctx.lineTo(20, 22);
ctx.lineTo(0, 12);
ctx.lineTo(-20, 22);
ctx.closePath();
ctx.fill();
// cockpit
ctx.fillStyle = "#ffffff";
ctx.beginPath();
ctx.arc(0, -6, 6, 0, Math.PI * 2);
ctx.fill();
// wings glow
ctx.fillStyle = "#00eaff";
ctx.globalAlpha = 0.5;
ctx.fillRect(-24, 0, 8, 10);
ctx.fillRect(16, 0, 8, 10);
ctx.globalAlpha = 1;
// engine flame
ctx.fillStyle = "#ffcf5a";
ctx.beginPath();
ctx.moveTo(-8, 24);
ctx.lineTo(0, 40 + Math.random() * 6);
ctx.lineTo(8, 24);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawEnemy(e) {
ctx.save();
ctx.translate(e.x, e.y);
// core
const grad = ctx.createRadialGradient(0, 0, 4, 0, 0, e.w / 2);
grad.addColorStop(0, "#fff");
grad.addColorStop(1, e.type === "spread" ? "#ff00cc" : "#ff0000");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, e.w / 2, 0, Math.PI * 2);
ctx.fill();
// spikes
ctx.strokeStyle = "#fff";
ctx.lineWidth = 2;
for (let i = 0; i < 6; i++) {
const angle = (Math.PI * 2 / 6) * i + frame * 0.01;
ctx.beginPath();
ctx.moveTo(Math.cos(angle) * 6, Math.sin(angle) * 6);
ctx.lineTo(Math.cos(angle) * (e.w / 2 + 8), Math.sin(angle) * (e.w / 2 + 8));
ctx.stroke();
}
ctx.restore();
}
function drawPowerItem(item) {
ctx.save();
ctx.translate(item.x, item.y);
ctx.rotate(frame * 0.05);
ctx.fillStyle = "#68ff7a";
ctx.fillRect(-12, -12, 24, 24);
ctx.fillStyle = "#052";
ctx.font = "bold 18px system-ui";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("P", 0, 1);
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#050816";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#ffffff";
stars.forEach((s) => {
ctx.globalAlpha = 0.4 + Math.random() * 0.5;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
bullets.forEach((b) => {
ctx.fillStyle = b.power >= 2 ? "#fff36a" : "#8ffcff";
ctx.fillRect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h);
});
enemyBullets.forEach((b) => {
ctx.fillStyle = "#ff9a3b";
ctx.beginPath();
ctx.arc(b.x, b.y, b.w / 2, 0, Math.PI * 2);
ctx.fill();
});
items.forEach(drawPowerItem);
enemies.forEach(drawEnemy);
// draw boss
if (boss) {
ctx.save();
ctx.translate(boss.x, boss.y);
const grad = ctx.createRadialGradient(0, 0, 10, 0, 0, boss.w / 2);
grad.addColorStop(0, "#fff");
grad.addColorStop(1, "#ff00aa");
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(0, 0, boss.w / 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// HP bar
ctx.fillStyle = "#000";
ctx.fillRect(80, 20, 320, 16);
ctx.fillStyle = "#ff0066";
ctx.fillRect(80, 20, 320 * (boss.hp / boss.maxHp), 16);
}
drawPlayer();
particles.forEach((p) => {
ctx.globalAlpha = p.life / 24;
ctx.fillStyle = "#ffd35a";
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
if (gameOver) {
ctx.fillStyle = "#000b";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.font = "bold 42px system-ui";
ctx.fillText("GAME OVER", canvas.width / 2, canvas.height / 2 - 30);
ctx.font = "20px system-ui";
ctx.fillText("Score: " + score, canvas.width / 2, canvas.height / 2 + 10);
ctx.fillText("Enterでリスタート", canvas.width / 2, canvas.height / 2 + 48);
}
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
updateUI();
loop();
</script>
</body>
</html>
AICharacter掲示板
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>AIキャラ会話掲示板 - Virtual Guild Board</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", "Meiryo", sans-serif;
background: linear-gradient(135deg, #0f172a, #1e293b, #111827);
color: #e5e7eb;
}
header {
padding: 24px;
background: rgba(0,0,0,0.35);
border-bottom: 1px solid rgba(255,255,255,0.08);
text-align: center;
}
header h1 {
margin: 0;
font-size: 32px;
color: #f8fafc;
}
header p {
margin-top: 8px;
color: #cbd5e1;
font-size: 14px;
}
.container {
max-width: 1300px;
margin: 0 auto;
padding: 20px;
display: grid;
grid-template-columns: 300px 1fr 320px;
gap: 20px;
}
.panel {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 18px;
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
overflow: hidden;
backdrop-filter: blur(10px);
}
.panel-title {
padding: 16px 18px;
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.04);
}
.character-list {
padding: 14px;
display: flex;
flex-direction: column;
gap: 12px;
max-height: 720px;
overflow-y: auto;
}
.character-card {
padding: 14px;
border-radius: 14px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.06);
}
.character-card h3 {
margin: 0 0 6px 0;
font-size: 17px;
}
.character-meta {
font-size: 13px;
color: #cbd5e1;
margin-bottom: 8px;
}
.character-desc {
font-size: 13px;
color: #e2e8f0;
line-height: 1.6;
}
.main-board {
display: flex;
flex-direction: column;
min-height: 780px;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
padding: 14px;
border-bottom: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.03);
}
button, select, input, textarea {
font: inherit;
}
button {
border: none;
border-radius: 10px;
padding: 10px 14px;
cursor: pointer;
background: linear-gradient(135deg, #3b82f6, #2563eb);
color: white;
transition: 0.2s ease;
}
button:hover {
transform: translateY(-1px);
filter: brightness(1.08);
}
.danger {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.sub {
background: linear-gradient(135deg, #64748b, #475569);
}
.chat-area {
flex: 1;
padding: 18px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 14px;
min-height: 500px;
max-height: 580px;
}
.post {
display: flex;
gap: 12px;
align-items: flex-start;
padding: 14px;
border-radius: 16px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.06);
animation: fadeIn 0.25s ease;
}
.avatar {
width: 48px;
height: 48px;
min-width: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
background: rgba(255,255,255,0.12);
border: 1px solid rgba(255,255,255,0.12);
}
.post-content {
flex: 1;
}
.post-header {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-bottom: 6px;
}
.name {
font-weight: bold;
font-size: 15px;
color: #ffffff;
}
.role {
font-size: 12px;
color: #93c5fd;
background: rgba(59,130,246,0.15);
padding: 3px 8px;
border-radius: 999px;
}
.time {
margin-left: auto;
font-size: 12px;
color: #94a3b8;
}
.message {
font-size: 15px;
line-height: 1.75;
color: #f1f5f9;
white-space: pre-wrap;
word-break: break-word;
}
.composer {
padding: 16px;
border-top: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.03);
display: flex;
flex-direction: column;
gap: 10px;
}
.composer-top {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
select, input, textarea {
width: 100%;
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(15,23,42,0.85);
color: #fff;
padding: 10px 12px;
outline: none;
}
textarea {
resize: vertical;
min-height: 100px;
}
.composer-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.right-panel-content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.status-box, .topic-box, .memory-box {
padding: 14px;
border-radius: 14px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.06);
}
.status-line {
margin: 8px 0;
font-size: 14px;
color: #e2e8f0;
}
.topic-tag {
display: inline-block;
margin: 6px 6px 0 0;
padding: 6px 10px;
border-radius: 999px;
background: rgba(16,185,129,0.18);
color: #bbf7d0;
font-size: 12px;
}
.memory-item {
font-size: 13px;
padding: 8px 10px;
margin-top: 8px;
border-radius: 10px;
background: rgba(255,255,255,0.04);
color: #dbeafe;
line-height: 1.6;
}
.footer-note {
text-align: center;
color: #94a3b8;
font-size: 12px;
padding: 16px;
}
.online-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
background: #22c55e;
box-shadow: 0 0 8px #22c55e;
}
@keyframes fadeIn {
from {
transform: translateY(8px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@media (max-width: 1100px) {
.container {
grid-template-columns: 1fr;
}
.main-board {
min-height: auto;
}
.chat-area {
max-height: 500px;
}
}
</style>
</head>
<body>
<header>
<h1>AIキャラ会話掲示板 - Virtual Guild Board</h1>
<p>APIなし / ローカル動作 / AIキャラ同士が自動で会話するファンタジー掲示板</p>
</header>
<div class="container">
<!-- 左 -->
<aside class="panel">
<div class="panel-title">キャラクター一覧</div>
<div class="character-list" id="characterList"></div>
</aside>
<!-- 中央 -->
<main class="panel main-board">
<div class="panel-title">ギルド広場</div>
<div class="toolbar">
<button id="toggleAutoBtn">自動会話ON/OFF</button>
<button id="manualTalkBtn" class="sub">AI会話を1回進める</button>
<button id="eventBtn" class="sub">イベント発生</button>
<button id="saveBtn" class="sub">保存</button>
<button id="clearBtn" class="danger">会話をリセット</button>
</div>
<div class="chat-area" id="chatArea"></div>
<div class="composer">
<div class="composer-top">
<select id="userName">
<option value="旅人">旅人</option>
<option value="冒険者">冒険者</option>
<option value="見習い魔法使い">見習い魔法使い</option>
<option value="傭兵">傭兵</option>
<option value="吟遊詩人">吟遊詩人</option>
</select>
</div>
<textarea id="userMessage" placeholder="メッセージを書いてください。例:今日は魔王城へ向かうべきかな?"></textarea>
<div class="composer-actions">
<button id="sendBtn">投稿する</button>
<button id="userTriggerBtn" class="sub">投稿後にAI反応</button>
</div>
</div>
</main>
<!-- 右 -->
<aside class="panel">
<div class="panel-title">ワールド情報</div>
<div class="right-panel-content">
<div class="status-box">
<div><span class="online-dot"></span>状態</div>
<div class="status-line">自動会話: <span id="autoStatus">停止中</span></div>
<div class="status-line">現在の話題: <span id="currentTopic">雑談</span></div>
<div class="status-line">投稿数: <span id="postCount">0</span></div>
</div>
<div class="topic-box">
<div><strong>話題タグ</strong></div>
<div id="topicTags"></div>
</div>
<div class="memory-box">
<div><strong>最近の話題メモ</strong></div>
<div id="memoryList"></div>
</div>
</div>
</aside>
</div>
<div class="footer-note">
HTML/CSS/JavaScriptのみで動作します。データはブラウザに保存されます。
</div>
<script>
const characters = [
{
id: "hero",
name: "セイン",
role: "勇者",
emoji: "⚔️",
personality: "まっすぐで熱血。前向き。",
desc: "世界を旅する若き勇者。困っている人を見ると放っておけない。",
styles: {
start: ["よし、", "さて、", "うーん、", "そうだな、"],
end: ["だ!", "だな。", "じゃないか?", "行くしかない!"],
flavor: ["魔王", "冒険", "仲間", "ダンジョン", "伝説"]
}
},
{
id: "mage",
name: "リリィ",
role: "魔法使い",
emoji: "🔮",
personality: "冷静で知的。少し毒舌。",
desc: "古代魔法を研究している少女。理屈で考えるタイプ。",
styles: {
start: ["理論的には、", "その話なら、", "少し気になるのは、", "魔法的に言えば、"],
end: ["ですね。", "だと思います。", "かもしれません。", "要検証です。"],
flavor: ["魔法", "精霊", "古代遺跡", "呪文", "研究"]
}
},
{
id: "knight",
name: "ガルド",
role: "騎士",
emoji: "🛡️",
personality: "真面目で忠誠心が強い。",
desc: "王国騎士団に所属する重騎士。秩序と責任を重んじる。",
styles: {
start: ["王国のためにも、", "騎士としては、", "規律を守るなら、", "任務として考えると、"],
end: ["異論はない。", "それが正しい。", "油断は禁物だ。", "準備が必要だ。"],
flavor: ["王国", "任務", "警戒", "防衛", "規律"]
}
},
{
id: "merchant",
name: "ミーナ",
role: "商人",
emoji: "💰",
personality: "明るく現実的。商売人。",
desc: "各地を巡る行商人。儲け話と珍品に目がない。",
styles: {
start: ["それより、", "商売の話をすると、", "利益で考えると、", "ふふっ、"],
end: ["儲かりそうね。", "悪くないわ。", "値段次第かな。", "面白い商機だわ。"],
flavor: ["市場", "金貨", "商品", "取引", "珍品"]
}
},
{
id: "assassin",
name: "クロウ",
role: "暗殺者",
emoji: "🗡️",
personality: "寡黙でクール。影のある口調。",
desc: "裏社会で名を知られる暗殺者。静かに本質を突く。",
styles: {
start: ["……", "無駄口は嫌いだが、", "影から見る限り、", "静かに言うが、"],
end: ["それだけだ。", "油断するな。", "匂うな。", "嫌な予感がする。"],
flavor: ["影", "敵", "罠", "裏路地", "追跡"]
}
}
];
const defaultTopics = [
"魔王討伐",
"古代遺跡",
"王国の依頼",
"森の異変",
"ギルドの噂",
"珍しいアイテム",
"危険なダンジョン",
"旅の準備",
"精霊の目撃情報",
"闇市場"
];
const eventTopics = [
"城下町で祭りが始まった",
"北の洞窟にドラゴン出現",
"謎の商人が秘宝を売っている",
"王国から緊急依頼が届いた",
"森で精霊の暴走が起きている",
"魔王軍の斥候が発見された",
"夜の港で密輸の噂が広がっている"
];
const generalPhrases = [
"最近の空気、少し変わった気がする",
"今日は何か起きそうな予感がある",
"仲間がいると旅は違う",
"静かな日ほど何かが起きるものだ",
"準備を怠ると危ない",
"噂話にも案外ヒントがある",
"この町には秘密が多い",
"力だけでは解決しないこともある",
"運だけでは生き残れない",
"今のうちに備えておくべきだ"
];
const replyRules = [
{
keywords: ["魔王", "討伐", "倒す"],
responses: {
hero: ["魔王を倒せば世界は少しは平和になるはずだ!", "ついに決戦の時かもしれないな!"],
mage: ["魔王クラスの相手なら準備不足は危険です。", "封印術式も調べておくべきですね。"],
knight: ["討伐任務なら戦力の整理が必要だ。", "王国への報告も忘れるな。"],
merchant: ["討伐の前に装備をそろえないと損するわよ。", "その話、特需が出そうね。"],
assassin: ["魔王より先に側近を潰すべきだ。", "正面から行くのは愚策かもしれない。"]
}
},
{
keywords: ["金", "お金", "金貨", "報酬"],
responses: {
hero: ["報酬も大事だけど、困っている人を助けたいな。", "金だけじゃなく名誉も欲しいところだ!"],
mage: ["研究費は必要ですからね。", "魔導書は高いので報酬は重要です。"],
knight: ["報酬より任務達成が優先だ。", "とはいえ補給費は無視できない。"],
merchant: ["その話なら私の出番ね。", "利益率の高い案件なら乗るわ。"],
assassin: ["金額次第で動く者も多い。", "報酬の匂いには裏がある。"]
}
},
{
keywords: ["遺跡", "古代", "秘宝"],
responses: {
hero: ["秘宝か……冒険心がくすぐられるな!", "遺跡には夢があるよな!"],
mage: ["古代遺跡は知識の宝庫です。", "その話、かなり興味があります。"],
knight: ["遺跡調査には護衛が必要だ。", "罠の警戒を優先しよう。"],
merchant: ["秘宝は高く売れる可能性があるわね。", "希少品なら市場が動くわ。"],
assassin: ["遺跡には死人の匂いがする。", "宝より罠を疑え。"]
}
},
{
keywords: ["森", "精霊", "自然"],
responses: {
hero: ["森の異変なら放っておけないな。", "精霊と仲良くできたら心強いな!"],
mage: ["精霊系の異常反応かもしれません。", "自然魔力の乱れを疑います。"],
knight: ["森は視界が悪い。隊列を乱すな。", "索敵役が必要だな。"],
merchant: ["森の特産品が取れなくなるのは困るわ。", "薬草の値段も上がりそう。"],
assassin: ["森では音と気配に気をつけろ。", "姿の見えない敵ほど厄介だ。"]
}
},
{
keywords: ["こんにちは", "初めまして", "はじめまして"],
responses: {
hero: ["ようこそ!一緒に冒険の話をしよう!", "よろしくな!"],
mage: ["ようこそ。この掲示板は案外にぎやかですよ。", "初めまして。興味深いですね。"],
knight: ["歓迎しよう。礼節を守ってくれれば問題ない。", "ここでは情報共有が重要だ。"],
merchant: ["いらっしゃい。いい情報があれば教えてね。", "歓迎するわ、旅人さん。"],
assassin: ["……新顔か。好きにするといい。", "静かにしていれば問題ない。"]
}
}
];
let posts = [];
let memoryTopics = [];
let currentTopic = "雑談";
let autoTalk = false;
let autoTimer = null;
const characterList = document.getElementById("characterList");
const chatArea = document.getElementById("chatArea");
const currentTopicEl = document.getElementById("currentTopic");
const postCountEl = document.getElementById("postCount");
const autoStatusEl = document.getElementById("autoStatus");
const topicTagsEl = document.getElementById("topicTags");
const memoryListEl = document.getElementById("memoryList");
function renderCharacters() {
characterList.innerHTML = "";
characters.forEach(char => {
const card = document.createElement("div");
card.className = "character-card";
card.innerHTML = `
<h3>${char.emoji} ${char.name}</h3>
<div class="character-meta">${char.role} / ${char.personality}</div>
<div class="character-desc">${char.desc}</div>
`;
characterList.appendChild(card);
});
}
function getTimeString() {
const now = new Date();
return now.toLocaleTimeString("ja-JP", {
hour: "2-digit",
minute: "2-digit"
});
}
function addPost(name, role, emoji, message, isUser = false) {
const post = {
id: Date.now() + Math.random(),
name,
role,
emoji,
message,
time: getTimeString(),
isUser
};
posts.push(post);
renderPosts();
saveData();
}
function renderPosts() {
chatArea.innerHTML = "";
posts.forEach(post => {
const el = document.createElement("div");
el.className = "post";
el.innerHTML = `
<div class="avatar">${post.emoji}</div>
<div class="post-content">
<div class="post-header">
<div class="name">${escapeHtml(post.name)}</div>
<div class="role">${escapeHtml(post.role)}</div>
<div class="time">${post.time}</div>
</div>
<div class="message">${escapeHtml(post.message)}</div>
</div>
`;
chatArea.appendChild(el);
});
chatArea.scrollTop = chatArea.scrollHeight;
postCountEl.textContent = posts.length;
}
function escapeHtml(text) {
return text
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function randomItem(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
function pickCharacter(excludeId = null) {
const pool = excludeId ? characters.filter(c => c.id !== excludeId) : characters;
return randomItem(pool);
}
function updateTopic(newTopic) {
currentTopic = newTopic;
currentTopicEl.textContent = currentTopic;
memoryTopics.unshift(newTopic);
memoryTopics = [...new Set(memoryTopics)].slice(0, 8);
renderTopics();
renderMemory();
saveData();
}
function renderTopics() {
topicTagsEl.innerHTML = "";
const mixed = [currentTopic, ...defaultTopics.slice(0, 6)];
[...new Set(mixed)].forEach(topic => {
const tag = document.createElement("span");
tag.className = "topic-tag";
tag.textContent = topic;
topicTagsEl.appendChild(tag);
});
}
function renderMemory() {
memoryListEl.innerHTML = "";
if (memoryTopics.length === 0) {
memoryListEl.innerHTML = `<div class="memory-item">まだ話題メモはありません。</div>`;
return;
}
memoryTopics.forEach(topic => {
const item = document.createElement("div");
item.className = "memory-item";
item.textContent = topic;
memoryListEl.appendChild(item);
});
}
function buildCharacterSentence(character, topic = currentTopic) {
const style = character.styles;
const start = randomItem(style.start);
const end = randomItem(style.end);
const flavor = randomItem(style.flavor);
const phrase = randomItem(generalPhrases);
const patterns = [
`${start}${topic}について言えば、${flavor}が鍵になりそう${end}`,
`${start}${phrase}。特に${flavor}が絡むなら注意${end}`,
`${start}${topic}の件は気になる。${flavor}の情報を集めたい${end}`,
`${start}${flavor}を見直した方がいい。${topic}にも繋がる${end}`,
`${start}${phrase}。${topic}と${flavor}は無関係じゃない${end}`
];
return randomItem(patterns);
}
function getRuleBasedReply(inputText, character) {
const text = inputText.toLowerCase();
for (const rule of replyRules) {
const matched = rule.keywords.some(keyword => text.includes(keyword.toLowerCase()));
if (matched) {
const responses = rule.responses[character.id];
if (responses && responses.length > 0) {
return randomItem(responses);
}
}
}
return null;
}
function extractTopicFromText(text) {
const found = defaultTopics.find(topic => text.includes(topic.replace("の", ""))) ||
eventTopics.find(topic => text.includes(topic.slice(0, 4)));
if (found) return found;
if (text.includes("魔王")) return "魔王討伐";
if (text.includes("遺跡")) return "古代遺跡";
if (text.includes("森")) return "森の異変";
if (text.includes("金") || text.includes("報酬")) return "報酬と金貨";
if (text.includes("王国")) return "王国の依頼";
if (text.includes("精霊")) return "精霊の目撃情報";
if (text.includes("ダンジョン")) return "危険なダンジョン";
return null;
}
function aiTalkOnce(previousSpeakerId = null) {
const speaker = pickCharacter(previousSpeakerId);
const msg = buildCharacterSentence(speaker, currentTopic);
addPost(speaker.name, speaker.role, speaker.emoji, msg);
}
function aiReplyToText(text, count = 2) {
const detectedTopic = extractTopicFromText(text);
if (detectedTopic) updateTopic(detectedTopic);
let usedIds = [];
for (let i = 0; i < count; i++) {
const pool = characters.filter(c => !usedIds.includes(c.id));
const speaker = randomItem(pool);
usedIds.push(speaker.id);
let reply = getRuleBasedReply(text, speaker);
if (!reply) {
reply = buildCharacterSentence(speaker, currentTopic);
}
addPost(speaker.name, speaker.role, speaker.emoji, reply);
}
}
function generateEvent() {
const eventText = randomItem(eventTopics);
updateTopic(eventText);
addPost("ワールド通知", "システム", "📢", eventText);
setTimeout(() => {
aiReplyToText(eventText, 3);
}, 300);
}
function toggleAutoTalk() {
autoTalk = !autoTalk;
autoStatusEl.textContent = autoTalk ? "稼働中" : "停止中";
if (autoTalk) {
autoTimer = setInterval(() => {
const count = Math.random() < 0.4 ? 2 : 1;
let prevId = null;
for (let i = 0; i < count; i++) {
const speaker = pickCharacter(prevId);
prevId = speaker.id;
const msg = buildCharacterSentence(speaker, currentTopic);
addPost(speaker.name, speaker.role, speaker.emoji, msg);
}
if (Math.random() < 0.28) {
updateTopic(randomItem(defaultTopics));
}
}, 4500);
} else {
clearInterval(autoTimer);
}
saveData();
}
function saveData() {
const data = {
posts,
memoryTopics,
currentTopic,
autoTalk
};
localStorage.setItem("virtualGuildBoardData", JSON.stringify(data));
}
function loadData() {
const raw = localStorage.getItem("virtualGuildBoardData");
if (!raw) return false;
try {
const data = JSON.parse(raw);
posts = data.posts || [];
memoryTopics = data.memoryTopics || [];
currentTopic = data.currentTopic || "雑談";
autoTalk = false;
currentTopicEl.textContent = currentTopic;
renderPosts();
renderTopics();
renderMemory();
autoStatusEl.textContent = "停止中";
return true;
} catch (e) {
console.error("読み込み失敗", e);
return false;
}
}
function clearBoard() {
if (!confirm("会話ログをリセットしますか?")) return;
posts = [];
memoryTopics = [];
currentTopic = "雑談";
currentTopicEl.textContent = currentTopic;
renderPosts();
renderTopics();
renderMemory();
saveData();
addWelcomePosts();
}
function addWelcomePosts() {
addPost("ワールド通知", "システム", "🌍", "Virtual Guild Boardへようこそ。ここではAIキャラたちが自由に会話します。");
addPost("セイン", "勇者", "⚔️", "よし、今日も冒険の情報を集めよう!");
addPost("リリィ", "魔法使い", "🔮", "掲示板の反応を見る限り、今日は賑やかになりそうですね。");
addPost("ミーナ", "商人", "💰", "儲け話でも危険な依頼でも、情報は早い者勝ちよ。");
}
document.getElementById("sendBtn").addEventListener("click", () => {
const userName = document.getElementById("userName").value.trim();
const userMessage = document.getElementById("userMessage").value.trim();
if (!userMessage) {
alert("メッセージを入力してください。");
return;
}
addPost(userName, "プレイヤー", "🧑", userMessage, true);
const maybeTopic = extractTopicFromText(userMessage);
if (maybeTopic) {
updateTopic(maybeTopic);
}
document.getElementById("userMessage").value = "";
});
document.getElementById("userTriggerBtn").addEventListener("click", () => {
const userName = document.getElementById("userName").value.trim();
const userMessage = document.getElementById("userMessage").value.trim();
if (!userMessage) {
alert("メッセージを入力してください。");
return;
}
addPost(userName, "プレイヤー", "🧑", userMessage, true);
aiReplyToText(userMessage, 3);
document.getElementById("userMessage").value = "";
});
document.getElementById("manualTalkBtn").addEventListener("click", () => {
aiTalkOnce();
});
document.getElementById("eventBtn").addEventListener("click", () => {
generateEvent();
});
document.getElementById("toggleAutoBtn").addEventListener("click", () => {
toggleAutoTalk();
});
document.getElementById("saveBtn").addEventListener("click", () => {
saveData();
alert("保存しました。");
});
document.getElementById("clearBtn").addEventListener("click", () => {
clearBoard();
});
renderCharacters();
const loaded = loadData();
if (!loaded || posts.length === 0) {
renderTopics();
renderMemory();
addWelcomePosts();
updateTopic("ギルドの噂");
} else {
renderTopics();
renderMemory();
}
</script>
</body>
</html>
横スクロールSTG公開!
C++ string
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
s = "This is a"; // 最初の文字列
s.append(" pen."); // 文字列の追加
cout << s << endl;
cout << "文字列の長さ:" << s.length() << endl;
// printfで表示
printf("char*:%s\n", s.c_str());
return 0;
}
C++ コンソールから入力
#include <iostream>
using namespace std;
int main() {
int a;
cin >> a;
cout << "a=" << a << endl;
return 0;
}
C++ HelloWorld
#include <iostream>
using namespace std;
int main() {
cout << "HelloWorld." << endl;
return 0;
}
Aran Red Fantasy.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Aran Red Fantasy - Ultimate</title>
<style>
/* ===============================
基本CSSスタイル
=============================== */
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
margin: 0;
padding: 0;
}
header, footer, nav {
background-color: #005ce6;
color: #fff;
text-align: center;
padding: 10px;
}
header h1, footer .container { margin: 0; }
nav a {
color: #fff;
text-decoration: none;
margin: 0 8px;
padding: 5px 8px;
display: inline-block;
}
nav a:hover { background-color: #004bb5; border-radius: 4px; }
nav a.active { background-color: #003a8c; border-radius: 4px; }
main { padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
.button {
background-color: #4CAF50;
border: none;
color: white;
padding: 8px 16px;
text-align: center;
text-decoration: none;
font-size: 14px;
margin: 4px 2px;
cursor: pointer;
border-radius: 5px;
}
.button:hover { background-color: #45a049; }
.disabled { opacity: 0.6; cursor: default; }
.muted { color:#667; font-size: 13px; }
/* カード風 */
.card {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
padding: 16px;
margin-bottom: 20px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
}
.card h3 { margin-top: 0; }
/* プログレスバー */
.progress-bar {
background-color: #ddd;
border-radius: 5px;
height: 20px;
width: 100%;
margin-bottom: 10px;
}
.progress {
background-color: #4CAF50;
height: 100%;
border-radius: 5px;
width: 0%;
}
/* インベントリアイテム表示 */
.inventory-item {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
display: inline-block;
margin: 5px;
padding: 10px;
min-width: 120px;
text-align: center;
cursor: pointer;
transition: background-color 0.2s;
user-select: none;
}
.inventory-item:hover { background-color: #eef; }
/* メッセージ表示 */
.message {
background-color: #fff8dd;
border: 1px solid #f5c666;
padding: 10px;
margin-bottom: 10px;
border-radius: 5px;
white-space: pre-wrap;
}
/* モーダル */
.modal-bg {
position: fixed;
top: 0; left: 0;
width:100%; height:100%;
background: rgba(0,0,0,.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.modal {
background: #fff;
padding: 20px;
border-radius: 5px;
text-align: center;
max-width: 520px;
width: 92%;
}
.modal h2 { margin-top: 0; }
.modal img { max-width: 100%; height: auto; border-radius: 6px; }
/* キャラクター表示 */
#character-image {
max-width: 420px;
width: 100%;
height: auto;
margin: 20px auto;
display: block;
border-radius: 8px;
border: 1px solid #ddd;
background: #fff;
}
/* バトル用スタイル */
.battle-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.enemy-card {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 250px;
padding: 16px;
text-align: center;
}
/* メッセージログ */
.log {
background-color: #eef;
border: 1px solid #bbe;
border-radius: 5px;
padding: 10px;
max-height: 300px;
overflow-y: auto;
margin: 10px 0;
white-space: pre-wrap;
}
/* ロケーションボタン */
#location-buttons button { margin-right: 10px; }
/* スキル一覧 */
.skill-list { list-style: none; padding: 0; }
.skill-list li { margin: 5px 0; }
/* クエストログ */
#quest-log-list { list-style: none; padding: 0; }
#quest-log-list li { margin: 4px 0; }
/* 実績一覧 */
#achievement-list { list-style: none; padding: 0; }
#achievement-list li { margin: 5px 0; }
/* ===============================
アートギャラリー
=============================== */
.gallery-grid{
display:grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
}
.gallery-item{
background:#fff;
border:1px solid #ddd;
border-radius:8px;
overflow:hidden;
box-shadow: 0px 2px 4px rgba(0,0,0,0.08);
display:flex;
flex-direction:column;
}
.gallery-item img{
width:100%;
height:auto;
display:block;
background:#fff;
}
.gallery-meta{
padding:10px;
display:flex;
align-items:center;
justify-content:space-between;
gap:8px;
flex-wrap:wrap;
}
.badge{
display:inline-block;
padding:4px 8px;
border-radius:999px;
font-size:12px;
background:#eef;
border:1px solid #bbe;
color:#223;
}
.badge.owned{
background:#e9ffe9;
border-color:#9fd49f;
color:#1c5a1c;
}
.badge.rarity-ur{
background:#fff2cc;
border-color:#f3d27a;
color:#6b4b00;
}
.badge.rarity-ssr{
background:#e8f0ff;
border-color:#9fb7ff;
color:#133a7a;
}
.gallery-actions{
display:flex;
gap:8px;
flex-wrap:wrap;
padding: 0 10px 12px;
}
/* ガチャUI */
.gacha-row{
display:flex;
align-items:center;
justify-content:space-between;
gap:10px;
flex-wrap:wrap;
}
.gacha-result{
margin-top:10px;
display:flex;
gap:10px;
flex-wrap:wrap;
align-items:flex-start;
}
.gacha-card{
width: 220px;
background:#fff;
border:1px solid #ddd;
border-radius:10px;
overflow:hidden;
box-shadow: 0px 2px 4px rgba(0,0,0,0.08);
}
.gacha-card img{ width:100%; display:block; }
.gacha-card .p{ padding:10px; }
</style>
</head>
<body>
<!-- ===============================
ヘッダー
=============================== -->
<header>
<h1>Aran Red Fantasy - Ultimate</h1>
</header>
<!-- ===============================
ナビゲーション
=============================== -->
<nav>
<div class="container">
<a href="#" id="home-link" onclick="showPage('home')">Home</a>
<a href="#" id="quests-link" onclick="showPage('quests')">Quests</a>
<a href="#" id="items-link" onclick="showPage('items')">Items</a>
<a href="#" id="friends-link" onclick="showPage('friends')">Companions</a>
<a href="#" id="character-link" onclick="showPage('character')">Character</a>
<a href="#" id="art-link" onclick="showPage('art')">Art Gallery</a>
<a href="#" id="battle-link" onclick="showPage('battle')">Battle</a>
<a href="#" id="store-link" onclick="showPage('store')">Store</a>
<a href="#" id="craft-link" onclick="showPage('craft')">Craft</a>
<a href="#" id="skills-link" onclick="showPage('skills')">Skills</a>
<a href="#" id="questlog-link" onclick="showPage('questlog')">QuestLog</a>
<a href="#" id="achievements-link" onclick="showPage('achievements')">Achievements</a>
</div>
</nav>
<!-- ===============================
メインコンテンツ
=============================== -->
<main>
<div class="container" id="content">
<!-- ===============================
Home
=============================== -->
<div id="home">
<h2>Welcome to Aran Red Fantasy!</h2>
<p>Explore the world, complete quests, craft items, recruit companions, and unlock achievements!</p>
<div id="home-message"></div>
<button class="button" onclick="showLoginModal()">Log In / Change User</button>
<button class="button" onclick="logout()">Logout (Reset All Data)</button>
<br/><br/>
<!-- BGM(継続再生対応:audioはページ外に置く) -->
<div class="card">
<h3>BGM</h3>
<p class="muted">※ 最初の1回だけ「Enable BGM」を押してください(ブラウザの自動再生ブロック対策)。以後はページ切替しても継続します。</p>
<button class="button" id="bgm-enable-btn" onclick="enableBGM()">Enable BGM (First Click)</button>
<button class="button" onclick="toggleMusic()">Toggle Music</button>
<div class="muted" id="bgm-status">Status: Off</div>
</div>
<!-- ★ガチャ(SSR/UR追加) -->
<div class="card">
<h3>Art Gacha</h3>
<div class="gacha-row">
<div class="muted">
Cost: <strong>10 Gold</strong> / pull<br/>
SSR/URが出ます。引いたアートは自動で所持になり、アートギャラリーに追加されます。
</div>
<div>
<button class="button" onclick="pullArtGacha(1)">Pull x1</button>
<button class="button" onclick="pullArtGacha(10)">Pull x10</button>
</div>
</div>
<div class="muted" id="gacha-status">—</div>
<div class="gacha-result" id="gacha-result"></div>
</div>
<!-- ランダムイベント/天候表示 -->
<div id="weather-display"></div>
<button class="button" onclick="triggerRandomEvent()">Check Random Event</button>
<!-- 昼夜サイクル -->
<div id="day-night-display"></div>
<button class="button" onclick="advanceTime()">Pass Time (+6h)</button>
<!-- 宿屋で休息 -->
<h3>Inn</h3>
<button class="button" onclick="restAtInn()">Rest at Inn (10 Gold)</button>
<!-- ロケーション移動 -->
<div id="location-section" class="card">
<h3>Locations</h3>
<div id="location-buttons">
<button class="button" onclick="moveLocation('Town')">Move to Town</button>
<button class="button" onclick="moveLocation('Forest')">Move to Forest</button>
<button class="button" onclick="moveLocation('Dungeon')">Move to Dungeon</button>
<button class="button" onclick="moveLocation('Mountain')">Move to Mountain</button>
</div>
<p>Current Location: <span id="current-location">Town</span></p>
<div class="log" id="location-log"></div>
</div>
</div>
<!-- ===============================
Quests
=============================== -->
<div id="quests" style="display: none;">
<h2>Quests</h2>
<h3>Main Quests</h3>
<div class="card" id="dragon-quest">
<h4>Defeat the Dragon</h4>
<p>A fierce dragon has appeared near the village! Defeat it to save the locals.</p>
<div class="progress-bar">
<div class="progress" id="dragon-progress"></div>
</div>
<p>Reward: 100 Gold, 100 XP, Dragon Scale</p>
<button class="button" onclick="startQuest('dragon')">Start Quest</button>
</div>
<div class="card" id="final-quest" style="display: none;">
<h4>The Ancient Evil (Final)</h4>
<p>The final threat emerges after you've proven your strength! Vanquish it!</p>
<div class="progress-bar">
<div class="progress" id="final-progress"></div>
</div>
<p>Reward: 200 Gold, 200 XP, Legendary Relic</p>
<button class="button" onclick="startQuest('final')">Start Quest</button>
</div>
<h3>Side Quests</h3>
<div class="card" id="crystal-quest">
<h4>Collect Magic Crystals</h4>
<p>Gather magical crystals scattered around the forest. Watch out for monsters!</p>
<div class="progress-bar">
<div class="progress" id="crystal-progress"></div>
</div>
<p>Reward: 50 Gold, 50 XP, Magic Crystal</p>
<button class="button" onclick="startQuest('crystal')">Start Quest</button>
</div>
<div class="card" id="orc-quest">
<h4>Eliminate the Orc Bandits</h4>
<p>A group of orc bandits is attacking travelers. Defeat them to restore peace!</p>
<div class="progress-bar">
<div class="progress" id="orc-progress"></div>
</div>
<p>Reward: 80 Gold, 70 XP, Orc Tusk</p>
<button class="button" onclick="startQuest('orc')">Start Quest</button>
</div>
</div>
<!-- ===============================
Items
=============================== -->
<div id="items" style="display: none;">
<h2>Inventory</h2>
<p>Click an item to use/equip/sell it (if applicable).</p>
<div id="inventory"></div>
</div>
<!-- ===============================
Companions
=============================== -->
<div id="friends" style="display: none;">
<h2>Companions</h2>
<p>Hire companions who fight alongside you!</p>
<input type="text" id="friendName" placeholder="Companion name" />
<button class="button" onclick="hireCompanion()">Hire Companion</button>
<h3>Your Companions</h3>
<ul id="companion-list"></ul>
<p class="muted">* Each companion has its own level, HP, and Attack. They also gain XP when you do.</p>
</div>
<!-- ===============================
Character
=============================== -->
<div id="character" style="display: none;">
<h2>Character</h2>
<img src="a.png" alt="Character" id="character-image"/>
<p>Name: <span id="character-name"></span></p>
<p>Level: <span id="character-level"></span></p>
<p>HP: <span id="character-hp"></span> / <span id="character-maxhp"></span></p>
<p>XP: <span id="character-xp"></span> / <span id="character-nextLevelXp"></span></p>
<p>Gold: <span id="character-gold"></span></p>
<p>Attack: <span id="character-attack"></span></p>
<p>Defense: <span id="character-defense"></span></p>
<p>Skill Points: <span id="character-skillpoints"></span></p>
<p>Active Buffs/Debuffs: <span id="character-buffs">None</span></p>
<p>Special Items: <span id="character-items">None</span></p>
<p class="muted">Portrait changes when you buy or pull an art (Store / Gacha).</p>
</div>
<!-- ===============================
Art Gallery
=============================== -->
<div id="art" style="display: none;">
<h2>アートギャラリー</h2>
<p class="muted">所持済みのアートは「Set as Character Art」でキャラクター画像に設定できます。ガチャでも入手できます。</p>
<div class="card">
<h3>Your Art Collection</h3>
<div id="art-collection-summary" class="muted"></div>
</div>
<div class="gallery-grid" id="art-gallery-grid"></div>
</div>
<!-- ===============================
Battle
=============================== -->
<div id="battle" style="display: none;">
<h2>Battle Arena</h2>
<p>Choose an enemy to fight or wait for random encounters in the wild!</p>
<div class="battle-container">
<div class="enemy-card">
<h3>Slime</h3>
<p>HP: 30</p>
<p>Attack: 1-3</p>
<p>Reward: 10 Gold, 10 XP</p>
<button class="button" onclick="startBattle('slime')">Fight Slime</button>
</div>
<div class="enemy-card">
<h3>Goblin</h3>
<p>HP: 50</p>
<p>Attack: 2-5</p>
<p>Reward: 20 Gold, 20 XP</p>
<button class="button" onclick="startBattle('goblin')">Fight Goblin</button>
</div>
<div class="enemy-card">
<h3>Orc Warrior</h3>
<p>HP: 80</p>
<p>Attack: 5-8</p>
<p>Reward: 40 Gold, 40 XP</p>
<button class="button" onclick="startBattle('orcEnemy')">Fight Orc</button>
</div>
</div>
<div class="log" id="battle-log"></div>
</div>
<!-- ===============================
Store
=============================== -->
<div id="store" style="display: none;">
<h2>Store</h2>
<p>Use your gold to purchase or sell items!</p>
<div class="card">
<h3>Buy Items</h3>
<div>
<h4>Minor Health Potion (20 Gold)</h4>
<button class="button" onclick="buyItem('Minor Health Potion')">Buy</button>
</div>
<div>
<h4>Major Health Potion (50 Gold)</h4>
<button class="button" onclick="buyItem('Major Health Potion')">Buy</button>
</div>
<div>
<h4>Iron Sword (80 Gold)</h4>
<button class="button" onclick="buyItem('Iron Sword')">Buy</button>
</div>
<div>
<h4>Steel Armor (100 Gold)</h4>
<button class="button" onclick="buyItem('Steel Armor')">Buy</button>
</div>
<div>
<h4>Lucky Ring (120 Gold)</h4>
<button class="button" onclick="buyItem('Lucky Ring')">Buy</button>
</div>
</div>
<!-- アート購入:購入するとキャラクター絵が変わる -->
<div class="card">
<h3>Art Shop(購入でキャラクター画像が変わる)</h3>
<p class="muted">Buy an art → it becomes “Owned” and you can set it anytime. (Gacha also adds Owned.)</p>
<div id="art-shop-list"></div>
</div>
<div class="card">
<h3>Sell Items</h3>
<p>Click an item in your inventory to sell it, if possible.</p>
<p class="muted">(You can't sell special quest items or currently equipped gear.)</p>
</div>
</div>
<!-- ===============================
Craft
=============================== -->
<div id="craft" style="display: none;">
<h2>Item Crafting</h2>
<p>Combine items to create something new!</p>
<div class="card">
<h3>Example Recipes</h3>
<ul>
<li>Dragon Scale + Orc Tusk => Dragon Tusk Lance (Weapon)</li>
<li>Magic Crystal + Magic Crystal => Greater Crystal (Special)</li>
</ul>
<p>Select any two items from your inventory to craft (if a valid recipe exists).</p>
</div>
<p>Currently Selected: <span id="craft-selection">None</span></p>
<button class="button" id="craft-button" onclick="attemptCraft()" disabled>Craft</button>
</div>
<!-- ===============================
Skills
=============================== -->
<div id="skills" style="display: none;">
<h2>Skills</h2>
<p>Use skill points to learn or upgrade skills!</p>
<p>You have <span id="skill-point-display"></span> skill points.</p>
<ul class="skill-list" id="skill-list"></ul>
</div>
<!-- ===============================
QuestLog
=============================== -->
<div id="questlog" style="display: none;">
<h2>Quest Log</h2>
<ul id="quest-log-list"></ul>
</div>
<!-- ===============================
Achievements
=============================== -->
<div id="achievements" style="display: none;">
<h2>Achievements</h2>
<ul id="achievement-list"></ul>
</div>
</div>
</main>
<!-- ===============================
フッター
=============================== -->
<footer>
<div class="container">
© 2025 Aran Red Fantasy
</div>
</footer>
<!-- ===============================
ログインモーダル
=============================== -->
<div class="modal-bg" id="login-modal-bg" style="display: none;">
<div class="modal">
<h2>Enter Your Name</h2>
<input type="text" id="loginName" placeholder="Your name" />
<br/><br/>
<button class="button" onclick="confirmLogin()">Login</button>
<button class="button" onclick="closeLoginModal()">Cancel</button>
</div>
</div>
<!-- ===============================
アイテム使用モーダル
=============================== -->
<div class="modal-bg" id="item-modal-bg" style="display: none;">
<div class="modal">
<h2 id="item-modal-title">Use/Equip Item</h2>
<p id="item-modal-description"></p>
<button class="button" onclick="confirmItemUse()">Use/Equip</button>
<button class="button" onclick="closeItemModal()">Cancel</button>
</div>
</div>
<!-- ===============================
アートプレビューモーダル
=============================== -->
<div class="modal-bg" id="art-modal-bg" style="display: none;">
<div class="modal">
<h2 id="art-modal-title">Art Preview</h2>
<img id="art-modal-img" alt="Art Preview" />
<p class="muted" id="art-modal-desc"></p>
<div style="margin-top:10px;">
<button class="button" id="art-modal-set-btn" onclick="confirmSetPortrait()">Set as Character Art</button>
<button class="button" onclick="closeArtModal()">Close</button>
</div>
</div>
</div>
<!-- ===============================
BGM本体(継続再生のためページ切替の外に置く)
=============================== -->
<audio id="bgm" loop preload="auto" playsinline>
<source src="http://tyosuke20xx.com/fjordnosundakaze.mp3" type="audio/mpeg">
</audio>
<!-- ===============================
JavaScript
=============================== -->
<script>
// -------------------------------------------
// ページ切り替え
// -------------------------------------------
function showPage(page) {
const pages = [
"home", "quests", "items", "friends", "character",
"art",
"battle", "store", "craft", "skills", "questlog", "achievements"
];
pages.forEach(p => {
const pageElement = document.getElementById(p);
const linkElement = document.getElementById(p + '-link');
if (!pageElement) return;
if (p === page) {
pageElement.style.display = "block";
if (linkElement) linkElement.classList.add("active");
} else {
pageElement.style.display = "none";
if (linkElement) linkElement.classList.remove("active");
}
});
if (page === "skills") refreshSkillList();
if (page === "questlog") updateQuestLog();
if (page === "achievements") updateAchievementList();
if (page === "art") renderArtGallery();
if (page === "store") renderArtShop();
}
// -------------------------------------------
// ローカルストレージキー
// -------------------------------------------
const LS_KEY_USER = "ARF_Username_Ultimate";
const LS_KEY_CHARACTER = "ARF_Character_Ultimate";
const LS_KEY_INVENTORY = "ARF_Inventory_Ultimate";
const LS_KEY_COMPANIONS = "ARF_Companions_Ultimate";
const LS_KEY_QUESTS = "ARF_Quests_Ultimate";
const LS_KEY_SKILLS = "ARF_Skills_Ultimate";
const LS_KEY_DAYTIME = "ARF_Daytime_Ultimate";
const LS_KEY_WEATHER = "ARF_Weather_Ultimate";
const LS_KEY_ACHIEVEMENT = "ARF_Achievement_Ultimate";
// BGM状態
const LS_KEY_BGM = "ARF_BGM_STATE_Ultimate";
// -------------------------------------------
// ★アート定義(SSR1〜SSR3 + UR1〜UR10)
// -------------------------------------------
const ART_LIST = [
// SSR(追加)
{ key:"SSR1", name:"SSR Art 1", url:"http://tyosuke20xx.com/SSR1.png", cost: 20, rarity:"SSR" },
{ key:"SSR2", name:"SSR Art 2", url:"http://tyosuke20xx.com/SSR2.png", cost: 20, rarity:"SSR" },
{ key:"SSR3", name:"SSR Art 3", url:"http://tyosuke20xx.com/SSR3.png", cost: 20, rarity:"SSR" },
// UR
{ key:"UR1", name:"UR Art 1", url:"http://tyosuke20xx.com/UR1.png", cost: 30, rarity:"UR" },
{ key:"UR2", name:"UR Art 2", url:"http://tyosuke20xx.com/UR2.png", cost: 30, rarity:"UR" },
{ key:"UR3", name:"UR Art 3", url:"http://tyosuke20xx.com/UR3.png", cost: 30, rarity:"UR" },
{ key:"UR4", name:"UR Art 4", url:"http://tyosuke20xx.com/UR4.png", cost: 30, rarity:"UR" },
{ key:"UR5", name:"UR Art 5", url:"http://tyosuke20xx.com/UR5.png", cost: 30, rarity:"UR" },
{ key:"UR6", name:"UR Art 6", url:"http://tyosuke20xx.com/UR6.png", cost: 30, rarity:"UR" },
{ key:"UR7", name:"UR Art 7", url:"http://tyosuke20xx.com/UR7.png", cost: 30, rarity:"UR" },
{ key:"UR8", name:"UR Art 8", url:"http://tyosuke20xx.com/UR8.png", cost: 30, rarity:"UR" },
{ key:"UR9", name:"UR Art 9", url:"http://tyosuke20xx.com/UR9.png", cost: 30, rarity:"UR" },
{ key:"UR10", name:"UR Art 10", url:"http://tyosuke20xx.com/UR10.png", cost: 30, rarity:"UR" }
];
// -------------------------------------------
// ★ガチャ設定(SSR/UR抽選)
// -------------------------------------------
const GACHA_COST = 10; // 1回10G
const GACHA_RATE_UR = 10; // UR 10%
const GACHA_RATE_SSR = 90; // SSR 90%(残り)
// -------------------------------------------
// キャラクター情報
// -------------------------------------------
let character = {
name: "Adventurer",
level: 1,
hp: 50,
maxHp: 50,
xp: 0,
nextLevelXp: 100,
gold: 0,
attack: 5,
defense: 2,
skillPoints: 0,
location: "Town",
specialItems: [],
buffs: [],
ownedArtKeys: [],
portraitUrl: "a.png"
};
// -------------------------------------------
// クエスト情報
// -------------------------------------------
let mainQuests = {
dragon: {
name: "Defeat the Dragon",
progress: 0,
reward: { gold: 100, xp: 100, items: ["Dragon Scale"] },
isRunning: false,
isCompleted: false,
unlockNext: "final",
locked: false
},
final: {
name: "The Ancient Evil",
progress: 0,
reward: { gold: 200, xp: 200, items: ["Legendary Relic"] },
isRunning: false,
isCompleted: false,
unlockNext: null,
locked: true
}
};
let sideQuests = {
crystal: {
name: "Collect Magic Crystals",
progress: 0,
reward: { gold: 50, xp: 50, items: ["Magic Crystal"] },
isRunning: false,
isCompleted: false,
unlockNext: null,
locked: false
},
orc: {
name: "Eliminate the Orc Bandits",
progress: 0,
reward: { gold: 80, xp: 70, items: ["Orc Tusk"] },
isRunning: false,
isCompleted: false,
unlockNext: null,
locked: false
}
};
function getAllQuests() {
return { ...mainQuests, ...sideQuests };
}
// -------------------------------------------
// インベントリ
// -------------------------------------------
let inventory = [];
// -------------------------------------------
// 仲間
// -------------------------------------------
let companions = [];
// -------------------------------------------
// スキル
// -------------------------------------------
let skills = {
Fireball: {
name: "Fireball",
level: 0,
maxLevel: 3,
cost: 1,
description: "Deal extra magic damage in battle"
},
Heal: {
name: "Heal",
level: 0,
maxLevel: 3,
cost: 1,
description: "Restores some HP at the start of battle"
}
};
// -------------------------------------------
// バトル用エネミー
// -------------------------------------------
const enemies = {
slime: {
name: "Slime",
hp: 30,
attackMin: 1,
attackMax: 3,
rewardGold: 10,
rewardXp: 10
},
goblin: {
name: "Goblin",
hp: 50,
attackMin: 2,
attackMax: 5,
rewardGold: 20,
rewardXp: 20
},
orcEnemy: {
name: "Orc Warrior",
hp: 80,
attackMin: 5,
attackMax: 8,
rewardGold: 40,
rewardXp: 40
}
};
// -------------------------------------------
// ストアアイテム
// -------------------------------------------
const storeItems = {
"Minor Health Potion": { name: "Minor Health Potion", type: "potion", heal: 20, cost: 20 },
"Major Health Potion": { name: "Major Health Potion", type: "potion", heal: 50, cost: 50 },
"Iron Sword": { name: "Iron Sword", type: "weapon", attack: 5, cost: 80, equipped: false },
"Steel Armor": { name: "Steel Armor", type: "armor", defense: 5, cost: 100, equipped: false },
"Lucky Ring": { name: "Lucky Ring", type: "accessory", attack: 1, defense: 1, cost: 120, equipped: false }
};
// -------------------------------------------
// クラフト用レシピ
// -------------------------------------------
const craftRecipes = [
{
components: ["Dragon Scale", "Orc Tusk"].sort(),
result: { name: "Dragon Tusk Lance", type: "weapon", attack: 10, equipped: false }
},
{
components: ["Magic Crystal", "Magic Crystal"].sort(),
result: { name: "Greater Crystal", type: "special" }
}
];
// -------------------------------------------
// 昼夜 & 天候
// -------------------------------------------
let currentHour = 12;
let currentWeather = "Sunny";
const possibleWeathers = ["Sunny","Rainy","Storm","Cloudy"];
// -------------------------------------------
// 実績
// -------------------------------------------
let achievements = {
firstKill: { name: "First Blood", description: "Defeat your first enemy.", isUnlocked: false },
level5: { name: "Rising Hero", description: "Reach Level 5.", isUnlocked: false },
quest3: { name: "Quest Hunter", description: "Complete 3 Quests.", isUnlocked: false }
};
// -------------------------------------------
// onload
// -------------------------------------------
window.onload = function() {
loadLocalData();
// BGM(継続再生 & 状態保存)
loadBgmState();
wireBgmAutoSave();
updateEnableBtn();
setBgmStatus(isMusicPlaying ? "On (will resume)" : "Off");
resumeBgmOnNextUserActionIfNeeded();
updateCharacterInfo();
updateQuestVisibility();
updateInventoryDisplay();
updateCompanionList();
updateDayNightDisplay();
updateWeatherDisplay();
updateAchievementList();
renderArtShop();
renderArtGallery();
showPage('home');
document.getElementById("current-location").textContent = character.location;
};
// -------------------------------------------
// ローカルストレージ: 読込/保存/リセット
// -------------------------------------------
function loadLocalData() {
let storedName = localStorage.getItem(LS_KEY_USER);
if (storedName) character.name = storedName;
let storedChar = localStorage.getItem(LS_KEY_CHARACTER);
if (storedChar) {
try {
const parsed = JSON.parse(storedChar);
character = { ...character, ...parsed };
} catch(e) {}
}
if (!Array.isArray(character.ownedArtKeys)) character.ownedArtKeys = [];
if (!character.portraitUrl) character.portraitUrl = "a.png";
let storedInv = localStorage.getItem(LS_KEY_INVENTORY);
if (storedInv) { try { inventory = JSON.parse(storedInv); } catch(e) {} }
let storedComp = localStorage.getItem(LS_KEY_COMPANIONS);
if (storedComp) { try { companions = JSON.parse(storedComp); } catch(e) {} }
let storedMQ = localStorage.getItem(LS_KEY_QUESTS+"_main");
if (storedMQ) { try { mainQuests = JSON.parse(storedMQ); } catch(e) {} }
let storedSQ = localStorage.getItem(LS_KEY_QUESTS+"_side");
if (storedSQ) { try { sideQuests = JSON.parse(storedSQ); } catch(e) {} }
let storedSkills = localStorage.getItem(LS_KEY_SKILLS);
if (storedSkills) { try { skills = JSON.parse(storedSkills); } catch(e) {} }
let storedHour = localStorage.getItem(LS_KEY_DAYTIME+"_hour");
if (storedHour) currentHour = parseInt(storedHour, 10);
let storedWeather = localStorage.getItem(LS_KEY_WEATHER);
if (storedWeather) currentWeather = storedWeather;
let storedAchv = localStorage.getItem(LS_KEY_ACHIEVEMENT);
if (storedAchv) { try { achievements = JSON.parse(storedAchv); } catch(e) {} }
}
function saveLocalData() {
localStorage.setItem(LS_KEY_USER, character.name);
localStorage.setItem(LS_KEY_CHARACTER, JSON.stringify(character));
localStorage.setItem(LS_KEY_INVENTORY, JSON.stringify(inventory));
localStorage.setItem(LS_KEY_COMPANIONS, JSON.stringify(companions));
localStorage.setItem(LS_KEY_QUESTS+"_main", JSON.stringify(mainQuests));
localStorage.setItem(LS_KEY_QUESTS+"_side", JSON.stringify(sideQuests));
localStorage.setItem(LS_KEY_SKILLS, JSON.stringify(skills));
localStorage.setItem(LS_KEY_DAYTIME+"_hour", currentHour.toString());
localStorage.setItem(LS_KEY_WEATHER, currentWeather);
localStorage.setItem(LS_KEY_ACHIEVEMENT, JSON.stringify(achievements));
}
function logout() {
if (!confirm("All data will be cleared. Are you sure?")) return;
localStorage.clear();
location.reload();
}
// -------------------------------------------
// ログインモーダル
// -------------------------------------------
function showLoginModal() {
document.getElementById("login-modal-bg").style.display = "flex";
}
function closeLoginModal() {
document.getElementById("login-modal-bg").style.display = "none";
}
function confirmLogin() {
const inputName = document.getElementById("loginName").value.trim();
if (inputName) {
character.name = inputName;
saveLocalData();
updateCharacterInfo();
}
closeLoginModal();
}
// -------------------------------------------
// キャラクター情報表示更新
// -------------------------------------------
function updateCharacterInfo() {
document.getElementById("character-name").textContent = character.name;
document.getElementById("character-level").textContent = character.level;
document.getElementById("character-hp").textContent = character.hp;
document.getElementById("character-maxhp").textContent = character.maxHp;
document.getElementById("character-xp").textContent = character.xp;
document.getElementById("character-nextLevelXp").textContent = character.nextLevelXp;
document.getElementById("character-gold").textContent = character.gold;
document.getElementById("character-attack").textContent = character.attack;
document.getElementById("character-defense").textContent = character.defense;
document.getElementById("character-skillpoints").textContent = character.skillPoints;
const img = document.getElementById("character-image");
if (img) img.src = character.portraitUrl || "a.png";
if (character.specialItems.length > 0) {
document.getElementById("character-items").textContent = character.specialItems.join(", ");
} else {
document.getElementById("character-items").textContent = "None";
}
if (character.buffs.length > 0) {
document.getElementById("character-buffs").textContent = character.buffs.map(b => b.name).join(", ");
} else {
document.getElementById("character-buffs").textContent = "None";
}
saveLocalData();
checkAchievements();
}
// -------------------------------------------
// レベルアップ
// -------------------------------------------
function addXp(amount) {
character.xp += amount;
while (character.xp >= character.nextLevelXp) {
character.level++;
character.xp -= character.nextLevelXp;
character.nextLevelXp = character.level * 100;
character.maxHp += 20;
character.hp = character.maxHp;
character.attack += 1;
character.defense += 1;
character.skillPoints += 1;
showHomeMessage(`Level up! Now Level ${character.level} (+1 Skill Point).`);
for (let c of companions) {
c.level++;
c.hp = c.maxHp;
c.attack++;
}
}
updateCharacterInfo();
}
// -------------------------------------------
// バフ/デバフ
// -------------------------------------------
function addBuff(buffObj) {
character.buffs.push(buffObj);
updateCharacterInfo();
}
function processBuffsEachTurn(logElm) {
for (let i = character.buffs.length - 1; i >= 0; i--) {
const b = character.buffs[i];
if (b.effectType === "dot") {
character.hp -= b.effectValue;
if (character.hp < 0) character.hp = 0;
logMessage(logElm, `[${b.name}] You take ${b.effectValue} damage! (HP: ${character.hp})`);
}
b.turns--;
if (b.turns <= 0) {
logMessage(logElm, `[${b.name}] effect ended.`);
character.buffs.splice(i, 1);
}
}
}
// -------------------------------------------
// ホームメッセージ
// -------------------------------------------
function showHomeMessage(msg) {
const homeMessage = document.getElementById('home-message');
homeMessage.innerHTML = `<div class="message">${msg}</div>`;
}
// -------------------------------------------
// 昼夜
// -------------------------------------------
function updateDayNightDisplay() {
let dnElm = document.getElementById("day-night-display");
let hourStr = (currentHour < 10) ? "0"+currentHour : currentHour;
let isNight = (currentHour >= 18 || currentHour < 6);
let dayNight = isNight ? "Night" : "Day";
dnElm.innerHTML = `<p>Time: ${hourStr}:00 (${dayNight})</p>`;
}
function advanceTime() {
currentHour += 6;
if (currentHour >= 24) currentHour -= 24;
saveLocalData();
updateDayNightDisplay();
showHomeMessage("Time passes by...");
}
// -------------------------------------------
// 天候
// -------------------------------------------
function updateWeatherDisplay() {
const wElm = document.getElementById("weather-display");
wElm.innerHTML = `<p>Weather: ${currentWeather}</p>`;
}
function changeWeatherRandom() {
currentWeather = possibleWeathers[Math.floor(Math.random() * possibleWeathers.length)];
updateWeatherDisplay();
saveLocalData();
}
// -------------------------------------------
// ランダムイベント
// -------------------------------------------
function triggerRandomEvent() {
const randomRoll = Math.random();
let msg = "";
if (randomRoll < 0.2) {
msg = "A traveling merchant appears, offering rare goods (not yet implemented).";
} else if (randomRoll < 0.4) {
changeWeatherRandom();
msg = `The weather suddenly changes to ${currentWeather}!`;
} else if (randomRoll < 0.6) {
addBuff({ name: 'Poison', turns: 3, effectType: 'dot', effectValue: 3 });
msg = "You stepped on a poisonous trap! You are now poisoned.";
} else {
msg = "Nothing special happens.";
}
showHomeMessage(msg);
}
// -------------------------------------------
// 宿屋
// -------------------------------------------
function restAtInn() {
if (character.gold < 10) {
showHomeMessage("Not enough gold to rest at the inn!");
return;
}
character.gold -= 10;
character.hp = character.maxHp;
for (let c of companions) c.hp = c.maxHp;
showHomeMessage("You and your companions rest at the inn and recover full HP.");
updateCharacterInfo();
}
// -------------------------------------------
// ロケーション移動 + ランダムエンカウント
// -------------------------------------------
function moveLocation(newLocation) {
character.location = newLocation;
document.getElementById("current-location").textContent = newLocation;
saveLocalData();
const logElm = document.getElementById('location-log');
logElm.textContent = `You moved to ${newLocation}.`;
let encounterChance = 0;
if (newLocation === "Town") encounterChance = 0;
else if (newLocation === "Forest") encounterChance = 40;
else if (newLocation === "Dungeon") encounterChance = 70;
else if (newLocation === "Mountain") encounterChance = 50;
const roll = Math.random() * 100;
if (roll < encounterChance) {
const enemyKeys = Object.keys(enemies);
const randEnemyKey = enemyKeys[Math.floor(Math.random() * enemyKeys.length)];
logElm.textContent += `\nA wild ${enemies[randEnemyKey].name} appears!`;
startBattle(randEnemyKey);
}
}
// -------------------------------------------
// 仲間の雇用
// -------------------------------------------
function hireCompanion() {
const input = document.getElementById('friendName');
let name = input.value.trim();
if (!name) return;
let newCompanion = { name, level: 1, hp: 30, maxHp: 30, attack: 2 };
companions.push(newCompanion);
input.value = "";
updateCompanionList();
saveLocalData();
showHomeMessage(`${name} joined your party!`);
}
function updateCompanionList() {
const listElm = document.getElementById('companion-list');
listElm.innerHTML = "";
companions.forEach(c => {
const li = document.createElement("li");
li.textContent = `${c.name} (Lv ${c.level}, HP ${c.hp}/${c.maxHp}, ATK ${c.attack})`;
listElm.appendChild(li);
});
}
// -------------------------------------------
// インベントリ表示
// -------------------------------------------
let selectedItemIndex = null;
let selectedForCraft = [];
function updateInventoryDisplay() {
const invElm = document.getElementById('inventory');
invElm.innerHTML = "";
if (inventory.length === 0) {
invElm.innerHTML = "<p>Your inventory is empty.</p>";
return;
}
inventory.forEach((item, index) => {
const div = document.createElement("div");
div.className = "inventory-item";
div.textContent = item.name;
if (item.equipped) div.style.border = "2px solid #4CAF50";
div.onclick = () => onInventoryItemClick(index);
invElm.appendChild(div);
});
}
function onInventoryItemClick(index) {
if (document.getElementById("craft").style.display === "block") {
toggleCraftSelection(index);
return;
}
selectedItemIndex = index;
const item = inventory[index];
const modalTitle = document.getElementById("item-modal-title");
const modalDesc = document.getElementById("item-modal-description");
if (item.type === "potion") {
modalTitle.textContent = `Use ${item.name}?`;
modalDesc.textContent = `This potion restores ${item.heal} HP.`;
} else if (item.type === "weapon") {
modalTitle.textContent = `Equip ${item.name}?`;
modalDesc.textContent = `Weapon (+${item.attack} Attack).`;
} else if (item.type === "armor") {
modalTitle.textContent = `Equip ${item.name}?`;
modalDesc.textContent = `Armor (+${item.defense} Defense).`;
} else if (item.type === "accessory") {
modalTitle.textContent = `Equip ${item.name}?`;
modalDesc.textContent = `Accessory (+${item.attack} ATK, +${item.defense} DEF).`;
} else {
modalTitle.textContent = item.name;
modalDesc.textContent = "A special item. No direct use/equip.";
}
if (canSellItem(item)) {
modalDesc.textContent += `\n(Sell price: ${sellPrice(item)} Gold)`;
}
document.getElementById("item-modal-bg").style.display = "flex";
}
function closeItemModal() {
document.getElementById("item-modal-bg").style.display = "none";
selectedItemIndex = null;
}
function confirmItemUse() {
if (selectedItemIndex === null) return;
const item = inventory[selectedItemIndex];
if (item.type === "potion") {
character.hp += item.heal;
if (character.hp > character.maxHp) character.hp = character.maxHp;
inventory.splice(selectedItemIndex, 1);
showHomeMessage(`${item.name} used! You recovered ${item.heal} HP.`);
}
else if (item.type === "weapon") {
unequipItem("weapon");
item.equipped = true;
character.attack += item.attack;
showHomeMessage(`${item.name} equipped. (+${item.attack} Attack)`);
}
else if (item.type === "armor") {
unequipItem("armor");
item.equipped = true;
character.defense += item.defense;
showHomeMessage(`${item.name} equipped. (+${item.defense} Defense)`);
}
else if (item.type === "accessory") {
unequipItem("accessory");
item.equipped = true;
character.attack += item.attack;
character.defense += item.defense;
showHomeMessage(`${item.name} equipped. (+${item.attack} ATK, +${item.defense} DEF)`);
}
else {
if (canSellItem(item)) {
let price = sellPrice(item);
character.gold += price;
inventory.splice(selectedItemIndex, 1);
showHomeMessage(`You sold ${item.name} for ${price} Gold.`);
} else {
showHomeMessage(`You can't use ${item.name} right now.`);
}
}
updateCharacterInfo();
updateInventoryDisplay();
closeItemModal();
}
function unequipItem(type) {
for (let i = 0; i < inventory.length; i++) {
let it = inventory[i];
if (it.type === type && it.equipped) {
it.equipped = false;
if (type === "weapon") character.attack -= it.attack;
else if (type === "armor") character.defense -= it.defense;
else if (type === "accessory") { character.attack -= it.attack; character.defense -= it.defense; }
}
}
}
function canSellItem(item) {
if (item.equipped) return false;
if (item.type === "special") return false;
return !["weapon","armor","accessory","potion"].includes(item.type) ? true : false;
}
function sellPrice(item) { return 30; }
// -------------------------------------------
// クエストUI
// -------------------------------------------
function updateQuestVisibility() {
const finalQuestCard = document.getElementById("final-quest");
finalQuestCard.style.display = (!mainQuests.final.locked) ? "block" : "none";
}
function startQuest(questKey) {
let q = mainQuests[questKey] || sideQuests[questKey];
if (!q) return;
if (q.isRunning || q.isCompleted) return;
if (q.locked) {
showHomeMessage("This quest is locked. Complete the previous quest first!");
return;
}
q.isRunning = true;
q.progress = 0;
updateProgressBar(questKey);
let progressInterval = setInterval(() => {
q.progress += 5;
if (q.progress > 100) q.progress = 100;
updateProgressBar(questKey);
if (q.progress === 100) {
clearInterval(progressInterval);
completeQuest(questKey);
}
}, 400);
}
function completeQuest(questKey) {
let q = mainQuests[questKey] || sideQuests[questKey];
q.isRunning = false;
q.isCompleted = true;
character.gold += q.reward.gold;
addXp(q.reward.xp);
q.reward.items.forEach(it => character.specialItems.push(it));
showHomeMessage(`${q.name} completed! You got ${q.reward.gold} Gold, ${q.reward.xp} XP, and ${q.reward.items.join(", ")}.`);
if (q.unlockNext) {
if (mainQuests[q.unlockNext]) mainQuests[q.unlockNext].locked = false;
else if (sideQuests[q.unlockNext]) sideQuests[q.unlockNext].locked = false;
}
updateQuestVisibility();
updateCharacterInfo();
saveLocalData();
}
function updateProgressBar(questKey) {
const bar = document.getElementById(questKey + '-progress');
let q = mainQuests[questKey] || sideQuests[questKey];
if (bar) bar.style.width = q.progress + '%';
}
// -------------------------------------------
// バトル
// -------------------------------------------
function startBattle(enemyKey) {
const enemyDef = enemies[enemyKey];
if (!enemyDef) return;
const logElm = document.getElementById('battle-log');
logElm.innerHTML = `A wild ${enemyDef.name} appears! (HP: ${enemyDef.hp})`;
if (skills.Heal.level > 0) {
const healAmount = skills.Heal.level * 10;
character.hp += healAmount;
if (character.hp > character.maxHp) character.hp = character.maxHp;
logMessage(logElm, `[Skill: Heal Lv${skills.Heal.level}] You healed ${healAmount} HP!`);
updateCharacterInfo();
}
let enemyHp = enemyDef.hp;
let battleInterval = setInterval(() => {
processBuffsEachTurn(logElm);
if (character.hp <= 0) {
clearInterval(battleInterval);
logMessage(logElm, "You have been defeated...");
saveLocalData();
return;
}
let baseDamage = getRandomInt(character.attack - 2, character.attack + 2);
if (baseDamage < 1) baseDamage = 1;
if (skills.Fireball.level > 0) {
let extra = skills.Fireball.level * 2;
baseDamage += extra;
logMessage(logElm, `[Fireball Lv${skills.Fireball.level}] Extra ${extra} magic damage!`);
}
let totalCompanionDamage = 0;
companions.forEach(c => { if (c.hp > 0) totalCompanionDamage += c.attack; });
let totalDamage = baseDamage + totalCompanionDamage;
enemyHp -= totalDamage;
logMessage(logElm, `You (and companions) deal ${totalDamage} damage! (Enemy HP: ${Math.max(enemyHp, 0)})`);
if (enemyHp <= 0) {
clearInterval(battleInterval);
logMessage(logElm, `You defeated the ${enemyDef.name}!`);
character.gold += enemyDef.rewardGold;
addXp(enemyDef.rewardXp);
updateCompanionXP(enemyDef.rewardXp);
updateCharacterInfo();
saveLocalData();
achievements.firstKill.isUnlocked = true;
updateAchievementList();
return;
}
let eAtk = getRandomInt(enemyDef.attackMin, enemyDef.attackMax);
let dmgToPlayer = eAtk - character.defense;
if (dmgToPlayer < 1) dmgToPlayer = 1;
character.hp -= dmgToPlayer;
if (character.hp < 0) character.hp = 0;
logMessage(logElm, `The ${enemyDef.name} hits you for ${dmgToPlayer}. (Your HP: ${character.hp})`);
updateCharacterInfo();
if (character.hp <= 0) {
clearInterval(battleInterval);
logMessage(logElm, "You have been defeated...");
saveLocalData();
return;
}
}, 800);
}
function logMessage(logElm, msg) {
const p = document.createElement("p");
p.textContent = msg;
logElm.appendChild(p);
logElm.scrollTop = logElm.scrollHeight;
}
function updateCompanionXP(amount) {
for (let c of companions) {
c.level += Math.floor(amount/50);
c.maxHp += 5;
c.hp = c.maxHp;
c.attack += 1;
}
updateCompanionList();
}
// -------------------------------------------
// ストア購入
// -------------------------------------------
function buyItem(itemKey) {
const itemDef = storeItems[itemKey];
if (!itemDef) return;
if (character.gold < itemDef.cost) {
showHomeMessage(`You don't have enough gold to buy ${itemDef.name}.`);
return;
}
character.gold -= itemDef.cost;
let newItem = JSON.parse(JSON.stringify(itemDef));
if (["weapon","armor","accessory"].includes(newItem.type)) newItem.equipped = false;
inventory.push(newItem);
showHomeMessage(`You bought ${newItem.name}!`);
updateCharacterInfo();
updateInventoryDisplay();
}
// -------------------------------------------
// アート所持/購入/設定
// -------------------------------------------
function isArtOwned(artKey) {
return character.ownedArtKeys.includes(artKey);
}
function grantArt(artKey, setAsPortrait=false) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return false;
if (!isArtOwned(art.key)) {
character.ownedArtKeys.push(art.key);
}
if (setAsPortrait) {
character.portraitUrl = art.url;
}
saveLocalData();
updateCharacterInfo();
renderArtShop();
renderArtGallery();
return true;
}
function buyArt(artKey) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return;
if (isArtOwned(art.key)) {
showHomeMessage(`You already own ${art.name}.`);
return;
}
if (character.gold < art.cost) {
showHomeMessage(`Not enough gold to buy ${art.name}. Need ${art.cost} Gold.`);
return;
}
character.gold -= art.cost;
// 購入したら即キャラ絵変更
grantArt(art.key, true);
showHomeMessage(`Purchased ${art.name}! Character portrait changed.`);
}
function setPortraitFromArt(artKey) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return;
if (!isArtOwned(art.key)) {
showHomeMessage("You don't own this art yet. Buy it in Store or pull it from Gacha.");
return;
}
character.portraitUrl = art.url;
saveLocalData();
updateCharacterInfo();
showHomeMessage(`Character portrait set to ${art.name}.`);
}
// Storeのアート一覧描画
function renderArtShop() {
const wrap = document.getElementById("art-shop-list");
if (!wrap) return;
wrap.innerHTML = "";
ART_LIST.forEach(a => {
const owned = isArtOwned(a.key);
const rarityBadge = a.rarity === "UR"
? `<span class="badge rarity-ur">UR</span>`
: `<span class="badge rarity-ssr">SSR</span>`;
const row = document.createElement("div");
row.style.marginBottom = "10px";
row.innerHTML = `
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;">
<div>
<strong>${a.name}</strong>
${rarityBadge}
<span class="muted">(${a.cost} Gold)</span>
${owned ? `<span class="badge owned">Owned</span>` : `<span class="badge">Not Owned</span>`}
</div>
<div>
<button class="button" onclick="buyArt('${a.key}')" ${owned ? 'disabled class="button disabled"' : ''} ${owned ? 'disabled' : ''}>Buy</button>
<button class="button" onclick="openArtModal('${a.key}')">Preview</button>
</div>
</div>
`;
wrap.appendChild(row);
});
}
// ギャラリー描画
function renderArtGallery() {
const grid = document.getElementById("art-gallery-grid");
const sum = document.getElementById("art-collection-summary");
if (!grid || !sum) return;
const ownedCount = character.ownedArtKeys.length;
sum.textContent = `Owned: ${ownedCount} / ${ART_LIST.length}`;
grid.innerHTML = "";
ART_LIST.forEach(a => {
const owned = isArtOwned(a.key);
const item = document.createElement("div");
item.className = "gallery-item";
const img = document.createElement("img");
img.src = a.url;
img.alt = a.name;
img.loading = "lazy";
img.style.cursor = "pointer";
img.onclick = () => openArtModal(a.key);
const meta = document.createElement("div");
meta.className = "gallery-meta";
meta.innerHTML = `
<div>
<strong>${a.name}</strong><br/>
<span class="muted">${a.cost} Gold</span>
</div>
<div style="display:flex; gap:6px; align-items:center;">
${a.rarity === "UR"
? `<span class="badge rarity-ur">UR</span>`
: `<span class="badge rarity-ssr">SSR</span>`
}
${owned ? `<span class="badge owned">Owned</span>` : `<span class="badge">Not Owned</span>`}
</div>
`;
const actions = document.createElement("div");
actions.className = "gallery-actions";
const btnPreview = document.createElement("button");
btnPreview.className = "button";
btnPreview.textContent = "Preview";
btnPreview.onclick = () => openArtModal(a.key);
const btnSet = document.createElement("button");
btnSet.className = "button";
btnSet.textContent = "Set as Character Art";
btnSet.disabled = !owned;
if (!owned) btnSet.classList.add("disabled");
btnSet.onclick = () => setPortraitFromArt(a.key);
actions.appendChild(btnPreview);
actions.appendChild(btnSet);
item.appendChild(img);
item.appendChild(meta);
item.appendChild(actions);
grid.appendChild(item);
});
}
// -------------------------------------------
// ★ガチャ(SSR1〜SSR3/UR1〜UR10から抽選)
// - 引いたアートはOwnedに追加
// - 1枚目だけは演出的にキャラ絵も即変更(setAsPortrait=true)
// -------------------------------------------
function pullArtGacha(times) {
const totalCost = GACHA_COST * times;
const status = document.getElementById("gacha-status");
const resultWrap = document.getElementById("gacha-result");
resultWrap.innerHTML = "";
if (character.gold < totalCost) {
status.textContent = `Not enough gold. Need ${totalCost} Gold.`;
showHomeMessage(`Not enough gold for gacha. Need ${totalCost} Gold.`);
return;
}
character.gold -= totalCost;
let pulled = [];
for (let i=0; i<times; i++) {
const art = rollOneArt();
pulled.push(art);
// 1枚目だけ即ポートレート変更(継続仕様)
grantArt(art.key, i === 0);
}
updateCharacterInfo();
status.textContent = `Pulled ${times} time(s). Cost ${totalCost} Gold.`;
// 表示
pulled.forEach((a, idx) => {
const card = document.createElement("div");
card.className = "gacha-card";
card.innerHTML = `
<img src="${a.url}" alt="${a.name}">
<div class="p">
<strong>${a.name}</strong><br/>
<span class="muted">${a.rarity}</span>
${isArtOwned(a.key) ? `<span class="badge owned" style="margin-left:6px;">Owned</span>` : ``}
</div>
`;
card.onclick = () => openArtModal(a.key);
resultWrap.appendChild(card);
});
showHomeMessage(`Gacha result: ${pulled.map(a => a.rarity + " " + a.key).join(", ")}`);
saveLocalData();
renderArtShop();
renderArtGallery();
}
function rollOneArt() {
const r = Math.random() * 100;
let rarity = (r < GACHA_RATE_UR) ? "UR" : "SSR";
const pool = ART_LIST.filter(a => a.rarity === rarity);
// 念のため
if (pool.length === 0) return ART_LIST[Math.floor(Math.random() * ART_LIST.length)];
return pool[Math.floor(Math.random() * pool.length)];
}
// -------------------------------------------
// アートプレビュー・モーダル
// -------------------------------------------
let pendingArtKey = null;
function openArtModal(artKey) {
const art = ART_LIST.find(a => a.key === artKey);
if (!art) return;
pendingArtKey = artKey;
document.getElementById("art-modal-title").textContent = `${art.name} (${art.rarity})`;
document.getElementById("art-modal-img").src = art.url;
const owned = isArtOwned(art.key);
document.getElementById("art-modal-desc").textContent =
owned ? "Owned: You can set this as your character art." : "Not owned: Buy it in Store or pull it from Gacha.";
const setBtn = document.getElementById("art-modal-set-btn");
setBtn.disabled = !owned;
if (!owned) setBtn.classList.add("disabled");
else setBtn.classList.remove("disabled");
document.getElementById("art-modal-bg").style.display = "flex";
}
function closeArtModal() {
document.getElementById("art-modal-bg").style.display = "none";
pendingArtKey = null;
}
function confirmSetPortrait() {
if (!pendingArtKey) return;
setPortraitFromArt(pendingArtKey);
closeArtModal();
}
// -------------------------------------------
// クラフト関連
// -------------------------------------------
function toggleCraftSelection(invIndex) {
const item = inventory[invIndex];
if (selectedForCraft.includes(invIndex)) {
selectedForCraft = selectedForCraft.filter(i => i !== invIndex);
} else {
if (selectedForCraft.length >= 2) {
showHomeMessage("You can only select up to 2 items for crafting.");
return;
}
selectedForCraft.push(invIndex);
}
updateCraftSelectionDisplay();
}
function updateCraftSelectionDisplay() {
let names = selectedForCraft.map(i => inventory[i].name);
if (names.length === 0) names.push("None");
document.getElementById("craft-selection").textContent = names.join(" & ");
document.getElementById("craft-button").disabled = (selectedForCraft.length < 2);
}
function attemptCraft() {
if (selectedForCraft.length < 2) return;
let itemA = inventory[selectedForCraft[0]];
let itemB = inventory[selectedForCraft[1]];
let combo = [itemA.name, itemB.name].sort();
let craftedItem = null;
for (let r of craftRecipes) {
if (r.components[0] === combo[0] && r.components[1] === combo[1]) {
craftedItem = r.result;
break;
}
}
if (!craftedItem) {
showHomeMessage("No valid recipe found for these items.");
selectedForCraft = [];
updateCraftSelectionDisplay();
return;
}
let idxA = Math.max(selectedForCraft[0], selectedForCraft[1]);
let idxB = Math.min(selectedForCraft[0], selectedForCraft[1]);
inventory.splice(idxA, 1);
inventory.splice(idxB, 1);
inventory.push(craftedItem);
showHomeMessage(`You crafted: ${craftedItem.name}!`);
selectedForCraft = [];
updateCraftSelectionDisplay();
updateInventoryDisplay();
saveLocalData();
}
// -------------------------------------------
// スキル
// -------------------------------------------
function refreshSkillList() {
document.getElementById("skill-point-display").textContent = character.skillPoints;
const listElm = document.getElementById("skill-list");
listElm.innerHTML = "";
for (let sKey in skills) {
let sk = skills[sKey];
let li = document.createElement("li");
li.innerHTML = `
<strong>${sk.name} (Lv${sk.level}/${sk.maxLevel})</strong>
- ${sk.description}
${
sk.level < sk.maxLevel
? `(<button onclick="learnSkill('${sKey}')">Upgrade (cost ${sk.cost})</button>)`
: ''
}
`;
listElm.appendChild(li);
}
}
function learnSkill(skillKey) {
let skill = skills[skillKey];
if (!skill) return;
if (skill.level >= skill.maxLevel) {
showHomeMessage(`${skill.name} is already at max level.`);
return;
}
if (character.skillPoints < skill.cost) {
showHomeMessage(`Not enough skill points to upgrade ${skill.name}.`);
return;
}
character.skillPoints -= skill.cost;
skill.level++;
showHomeMessage(`You upgraded ${skill.name} to level ${skill.level}.`);
updateCharacterInfo();
refreshSkillList();
}
// -------------------------------------------
// クエストログ
// -------------------------------------------
function updateQuestLog() {
const logElm = document.getElementById("quest-log-list");
logElm.innerHTML = "";
let allQ = getAllQuests();
for (let key in allQ) {
let q = allQ[key];
let status = q.isCompleted ? "Completed" : (q.isRunning ? "In Progress" : "Not Started");
let li = document.createElement("li");
li.textContent = `${q.name}: ${status}`;
logElm.appendChild(li);
}
}
// -------------------------------------------
// 実績
// -------------------------------------------
function checkAchievements() {
if (character.level >= 5) achievements.level5.isUnlocked = true;
let completedCount = 0;
let allQ = getAllQuests();
for (let key in allQ) if (allQ[key].isCompleted) completedCount++;
if (completedCount >= 3) achievements.quest3.isUnlocked = true;
saveLocalData();
updateAchievementList();
}
function updateAchievementList() {
const listElm = document.getElementById("achievement-list");
listElm.innerHTML = "";
for (let aKey in achievements) {
let a = achievements[aKey];
let status = a.isUnlocked ? "Unlocked" : "Locked";
let li = document.createElement("li");
li.textContent = `${a.name} - ${a.description} [${status}]`;
listElm.appendChild(li);
}
}
// -------------------------------------------
// BGM(継続再生 & 状態保存 & 復帰)
// -------------------------------------------
let isMusicPlaying = false;
let bgmUnlocked = false;
let wantAutoResume = false;
function setBgmStatus(text) {
const s = document.getElementById("bgm-status");
if (s) s.textContent = "Status: " + text;
}
function saveBgmState() {
const audio = document.getElementById("bgm");
if (!audio) return;
const state = {
unlocked: bgmUnlocked,
playing: isMusicPlaying,
volume: audio.volume,
time: audio.currentTime
};
localStorage.setItem(LS_KEY_BGM, JSON.stringify(state));
}
function loadBgmState() {
const audio = document.getElementById("bgm");
if (!audio) return;
const raw = localStorage.getItem(LS_KEY_BGM);
if (!raw) return;
try {
const st = JSON.parse(raw);
bgmUnlocked = !!st.unlocked;
isMusicPlaying = !!st.playing;
wantAutoResume = isMusicPlaying;
if (typeof st.volume === "number") audio.volume = st.volume;
if (typeof st.time === "number") {
audio.addEventListener("loadedmetadata", () => {
try { audio.currentTime = Math.max(0, st.time); } catch(e) {}
}, { once: true });
}
} catch(e) {}
}
function wireBgmAutoSave() {
const audio = document.getElementById("bgm");
if (!audio) return;
audio.addEventListener("play", () => { isMusicPlaying = true; saveBgmState(); });
audio.addEventListener("pause", () => { isMusicPlaying = false; saveBgmState(); });
audio.addEventListener("volumechange", saveBgmState);
let lastSave = 0;
audio.addEventListener("timeupdate", () => {
const now = Date.now();
if (now - lastSave > 4000) {
lastSave = now;
saveBgmState();
}
});
}
function updateEnableBtn() {
const btn = document.getElementById("bgm-enable-btn");
if (!btn) return;
if (bgmUnlocked) {
btn.textContent = "BGM Enabled";
btn.classList.add("disabled");
btn.disabled = true;
} else {
btn.textContent = "Enable BGM (First Click)";
btn.classList.remove("disabled");
btn.disabled = false;
}
}
function resumeBgmOnNextUserActionIfNeeded() {
if (!bgmUnlocked || !wantAutoResume) return;
const audio = document.getElementById("bgm");
if (!audio) return;
const resumeOnce = () => {
audio.play().then(() => {
isMusicPlaying = true;
wantAutoResume = false;
setBgmStatus("On (Resumed)");
saveBgmState();
}).catch(() => {
setBgmStatus("Blocked (Enable again)");
});
};
document.addEventListener("pointerdown", resumeOnce, { once: true });
document.addEventListener("keydown", resumeOnce, { once: true });
}
function enableBGM() {
const audio = document.getElementById("bgm");
if (!audio) return;
audio.volume = 0.6;
audio.play().then(() => {
bgmUnlocked = true;
isMusicPlaying = true;
wantAutoResume = false;
updateEnableBtn();
setBgmStatus("On");
showHomeMessage("BGM Enabled & Playing");
saveBgmState();
}).catch(() => {
setBgmStatus("Blocked (Click again)");
showHomeMessage("BGM blocked by browser. Click Enable BGM again.");
});
}
function toggleMusic() {
const audio = document.getElementById("bgm");
if (!audio) return;
if (!bgmUnlocked) {
showHomeMessage("First, click 'Enable BGM (First Click)'.");
setBgmStatus("Locked");
return;
}
if (!isMusicPlaying) {
audio.play().then(() => {
isMusicPlaying = true;
setBgmStatus("On");
showHomeMessage("Music On");
saveBgmState();
}).catch(() => {
setBgmStatus("Blocked");
showHomeMessage("Music could not be played (browser block).");
});
} else {
audio.pause();
isMusicPlaying = false;
setBgmStatus("Off");
showHomeMessage("Music Off");
saveBgmState();
}
}
// -------------------------------------------
// 汎用ランダム整数
// -------------------------------------------
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
</script>
</body>
</html>
Apple Vision Pro2発売予想
「Apple Vision Pro 2」みたいな “本当の第2世代フラッグシップ” は、
早くても 2027〜2028 年ごろ と見ておくのが現実的。
それより前に出るのは、
- すでに発表済みの M5版 Vision Pro(マイナーチェンジ)(2025年10月発売)Lifewire
- さらにその後の 廉価版「Vision Air」(仮)(2027年量産開始見込み)UploadVR+1
という「派生モデル」で、本気の Vision Pro 2 とは少し別ラインと考えた方がいい。
1. 今の公式&有力リークの流れ
① Vision Pro (初代) → M5チップ版
- 2024年:初代 Vision Pro 発売($3,499)
- 2025年10月:M5チップ搭載のアップグレード版 Vision Pro 発表&発売
- M5で性能&AI処理大幅アップ
- バッテリー持ち向上、120Hzリフレッシュレート
- 新しい「Dual Knit Band」で装着感改善Lifewire
👉 これは 「Vision Pro 1.5」的なマイナーチェンジ。
② 廉価版ライン「Vision Air」(仮)
- アナリスト Ming-Chi Kuo などのレポートで
「Vision Air」は2027年後半に量産開始予定、Vision Proより40%以上軽く、価格も半額以下を目指す と報告。UploadVR+1 - 価格も $1,500〜$1,800 くらいを狙うと言われている(あくまで噂)。
③ 本来の「Vision Pro 2」計画
- 以前のリークでは、
- もっと軽く
- 高性能ディスプレイ
- バッテリー改善
- 価格も少し下げた Vision Pro 2 を「Vision Air の後」に出す構想があった。MacRumors+1
- しかし 2024年時点で
「Vision Pro 2 の開発一時停止」「まずは廉価版に集中」 という報道も出ている。MacRumors+1
つまり Apple の中でも
先に安いモデルで市場を広げるか?
それとも高級路線を維持するか?
という路線変更が何度も揺れている状態。
2. じゃあ「Vision Pro 2」はいつ出そう?
公開情報とリークを全部まとめて、ジョブズ風に乱暴に整理すると:
- 2025年:M5版 Vision Pro(マイナーアップデート) → これはもう発表済み。Lifewire
- 2026年:新しい XR/メガネ系ハードは出ない見込み(アナリストレポート複数が「2026年は空白」と予測)。Medium+1
- 2027年:廉価版 Vision Air が量産&発売(予定)。UploadVR+1
- そのあと:Vision Pro 2(真の第2世代)が来る
- MacRumors などは、Vision Air のあとにハイエンド Pro 2 を計画していたが、時期は2028年ごろに後ろ倒しになっていると報じている。MacRumors
なので、現実的な予想は:
Vision Pro 2 (第2世代フラッグシップ)
→ 2027年末〜2028年以降が本命
「2026年発売」という噂も一部あるけど、
- 2026年は新ヘッドセット無し予想
- Vision Air が 2027 年に控えている
この2点を考えると、2026年 Vision Pro 2 はかなり望み薄。
3. どんな進化がありそう?
現時点の予想スペック(リーク+業界予測)では:
- チップ:M4 or M5 以降(AI処理強化、Apple Intelligence 連携前提)UC Today+1
- ディスプレイ:より省電力で高輝度なOLED / micro-OLED(サプライチェーン側の情報多数)oled-info.com+1
- 重量:Vision Air と同等か、それ以上の軽量化(長時間装着を前提に)Road to VR
- 価格戦略:
- Vision Air:普及価格帯
- Vision Pro 2:ハイエンド路線維持(でも初代よりはやや下げる可能性)
4. ざっくり年表イメージ
- 2024:Vision Pro (初代)
- 2025:Vision Pro M5版(性能&快適性アップのマイナーチェンジ)
- 2026:新XR本体は出ない可能性が高い
- 2027:廉価版 Vision Air
- 2028〜:本命の Vision Pro 2(第2世代フラッグシップ)
5. ユーザー視点の「買いタイミング」
あなたの立場で考えると:
- 今すぐ体験したい
→ 2025年の M5版 Vision Pro は「開発者・クリエイター用ハイエンド機」としてはかなり完成度高いはず。 - できるだけコスパ重視+フルダイブの入口体験したい
→ 2027年の Vision Air 待ちが無難。 - 最高の完成度まで待つ派
→ 2028年以降の Vision Pro 2 をターゲットにして、今はQuestとかで遊びながら様子見。
もし「自分のVRサービス/ゲームをどの世代向けに作るか?」って視点で戦略を立てたいなら、
- 2025〜2027:Vision Pro (初代+M5) + 他社ヘッドセット向けに土台を作る期間
- 2027〜2030:Vision Air & Vision Pro 2 向けに本命のフルスケール版を投下する期間
ってロードマップで考えるのが現実的だと思う。

