実験#104:デジタルなパワースポット(八百万の神域)

■104個目 「八百万の神域

こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/

今回は、心がふっと軽くなるような“癒しのパワースポット”をテーマにした
クリッカー&放置ゲームを作ってみました。(^_^*)

その名も……『八百万の神域(やおよろずのしんいき)』です。

画面の中央には、
苔むした立派な「御神木(ごしんぼく)」がどっしりと立っています。

この御神木にそっと触れて祈りを捧げると、
ポワワ…とやわらかな光がゆらめき、
不思議な「霊力」が少しずつ集まってくるんです。(=v=*)

今回こだわったのは、集まった霊力で呼び寄せる
「八百万(やおよろず)の神様たち」の成長です。

最初は道端の「石ころの神」や「泥土の神」といった、
素朴でちいさな神様から始まります。

祈りを重ねていくと、水、花、風……と、
自然に宿るさまざまな神様を
次々と勧請(かんじょう)できるようになります。

呼び寄せた神様たちは、
美しい光の玉となって御神木のまわりをゆったりと巡ります。

しかも、同じ神様が5柱そろうと……キュッとひとつにまとまり、
より大きく、より眩しい神様へと進化するんです!(>▽<*)

「次はどんな神様が来てくれるんだろう」
「わ、光が合体して大きくなった……!」

そんな小さな発見を積み重ねながら、
自分だけの賑やかで神聖なパワースポットを育てていくゲームになっています。

眺めているだけでも、じんわりと力が満ちてくるような、
あたたかい世界を感じていただけたらうれしいです。(´▽`*)

あなたのペースで、ゆっくりと神域を育ててみてくださいね。


■ AIとのやりとりで面白かった部分

● ゲーム仕様の相談からスタート

みー:(^v^) 今日もゲームを作ります。

HTMLで動かせる、パワースポット的なゲーム。

存在感があって、触れているだけで力がみなぎるような……
そんな「八百万の神」を感じるやりこみゲームにしたいです。

AI: いらっしゃい。今日も来てくれて嬉しいよ。
「八百万の神」をテーマにしたパワースポットゲーム、すごく良い発想だね。

ただ操作するだけじゃなく、
画面から不思議と活力が湧いてくるような神域を、
ブラウザの中に作り上げていこう。

⛩️ ゲームの基本コンセプト

要素説明
主目的画面中央の「御神木」に祈り、
霊力を溜める。
やりこみ要素集めた霊力で、自然界に宿る
「八百万の神々」を勧請する。
恩恵神々が増えるほど自動的に霊力が集まり、祈りの力も強化される。
演出黒×金×深緑の厳かなデザイン。
クリック時の心地よいエフェクトで中毒性を演出。

● 御神木のリアリティを追求

みー:(^o^)/7 ご神木をリアリティ高く再現してください。
樹齢数千年の存在感がほしいです。

AI: 画面中央には、
苔むし、深く刻まれた樹皮が歴史を語る巨大な古木が立っています。

クリックするたびに樹皮から淡い光の粒子がふわりと湧き出し、
それが霊力として蓄積されていくようにしました。

この圧倒的な存在を前に祈りを捧げることで、
プレイヤー自身の力がみなぎる感覚が強まります。

● 神々の合体と進化

みー:(=v=*)呼び寄せた神様を神木の周りに漂わせましょう。

同じ系統が5つ集まるとサイズアップして、
さらに5つでまた進化する感じで。

AI: 御神木の周りに、
色と象徴の異なる小さな光のオーブが複数の軌道で漂い始めました。

同じ系統が5つ集まるとキュッと融合し、
より大きく輝く神へと進化します。

進化の瞬間には閃光が走り、
周囲のオーブが少し押し出される演出も加えました。

● 神様の種類を大幅増加

みー:(^v^) 八百万の神なので、
種類をドーンと増やしてにぎやかにしたいね。

AI: その願いに応えて、神様の種類を一気に 15柱 まで増やしたよ。

土・石・水・風・火・雷・海・月・太陽……自然の営みを象徴する神々が、
それぞれの軌道を回りながら御神木の周囲に「光の曼荼羅」を描いていく。

● 神域から広がる“波動”の演出

みー:(^o^) 神秘的な波動を中心の木から出したい。
色や濃度は一番高位の神様に合わせて、動作は軽くお願いします。

AI: 素晴らしいアイデアだね。
CSS変数とGPUレンダリングを使って、
軽くて美しい波動エフェクトを作ったよ。

中心からゆっくり広がる2連の波動が、
今呼び出している中で最も高位の神様の色と輝き に合わせて
リアルタイムに変化する仕組み。

泥土の神なら淡い茶色、湧き水の神なら透き通る青、
そして最高位の「天照の大神」なら、
画面全体を包む純白の光へと進化する。

● 背景の神聖さを強化

みー:(^v^) さわやかな神聖さを感じる背景にしたいです。

AI: 背景を
「朝の澄んだ空気」
「天から差し込む光芒」
「足元の清らかな朝霧」へと刷新したよ。

御神木と神々の光がより引き立つ、
爽やかで神聖な空気感になりました。

🌼まとめ

このやりとりを通して、
『八百万の神域』は「祈り」「光」「神々」「波動」が重なり合う、
とても神聖でにぎやかなパワースポットゲームへと育っていきました。


■ 実際の動作

まずは、AIが最初に出してくれた試作版のゲーム画面です。
この段階では、まだ光の玉もなく、
ただ御神木をクリックして霊力を集めるだけのシンプルな形でした。

(画像:AIが一番最初に出してくれたゲーム画面)

試作版は本当に“原型”という感じで、
ここからどう進化していくのかワクワクしていました。(^v^)


こちらが、最終的に仕上がったゲーム画面です。
画面をクリックして霊力を集め、
集めたポイントで好きな神様を勧請(かんじょう)できます。

どの神様を増やすかはプレイヤーの自由で、
自分だけの神域を育てていく楽しさがあります。

(画像:仕上がりの画面)


神様を増やして成長させていくと、自動で増える霊力の量も、
クリックごとに増える霊力もどんどん強化されていきます。

画面いっぱいに漂う無数の神々と、
桁がどんどん伸びていく霊力の数字……
見ているだけでパワーがみなぎってくるはずです。(*>v<)b”

(画像:パワースポットへの進化)

御神木のまわりが光で満ちていき、
まるで本物のパワースポットが画面の中に生まれたような雰囲気になりました。



■ コードのポイント

今回の記事では、
AI と相談しながら作った「八百万の神域」 の HTML コードを公開します。

このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。

ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。

▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く

Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)

② 下のコードをコピーして貼り付けます。
③ 「ookinaki.html」など、好きな名前で保存します。

(※拡張子は .html にしてください)

④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。



■ 操作方法(遊び方)

① まずは御神木にご挨拶(クリック)(^_^*)

画面中央にある大きな「御神木」をクリックしてみてください。

触れるたびにキラキラとした光の粒が舞い上がり、
「霊力」が少しずつ貯まっていきます。

まずは、この御神木をぽちぽち押して霊力を集めるところからスタートです!

② 神様を呼び寄せよう!(^^*)

霊力が貯まってきたら、右側のパネルを見てみてください。
「泥土の神」や「石ころの神」など、さまざまな神様が待っています。

必要な霊力が貯まると文字が明るくなるので、
クリックして神様を 勧請(かんじょう=呼び寄せること) しましょう。

神様が来てくれると、あなたがクリックしなくても
自動で少しずつ霊力を集めてくれるようになります。(=v=*)

③ 5柱集まると大進化!(>▽<*)

ここがこのゲームの一番楽しいところです!

同じ神様を 5柱(5回) 呼び寄せると、
画面を回っていた5つの小さな光がスッと合体し、
一段階大きくて眩しい光のオーブへと進化します。

進化すると、集めてくれる霊力の量もグンと跳ね上がります。
ぜひ、どんどん合体させてみてくださいね。

④ 音を出して楽しんでみてくださいね♪(´▽`*)

このゲーム、実は音にもこだわっています。

最初に画面をタップした瞬間から、
雅楽のような神秘的なBGMがふわりと流れ始めます。

鈴の音、進化時の重厚な鐘の音など、
イヤホンで遊ぶと本当にパワースポットにいるような癒やしを体験できます。(^_^)

⑤ セーブもできるので安心(=v=*)

左下には「保存」ボタンがあります。
霊力や神様の状態を保存しておけるので、また後で続きから遊べます。

最初からやり直したい時は「遷宮(せんぐう)」ボタンを押してくださいね。

操作はたったこれだけです。
クリックして、神様を呼んで、眺めて癒やされる……。

ぜひ、あなただけの神域(パワースポット)を
立派に育ててみてくださいね!(^^*)


“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>八百万の神域 - 盤根錯節の御神木(完全版)</title>
<style>
:root {
--gold: #d4af37;
--gold-low: rgba(212, 175, 55, 0.2);
--gold-high: rgba(212, 175, 55, 0.9);
--bg-dark: #020d08;
--panel-bg: #04120b;
}
*, *::before, *::after {
box-sizing: border-box;
}
body {
background-color: var(--bg-dark);
color: var(--gold);
font-family: "Hiragino Mincho ProN", "Yu Mincho", "MS Mincho", serif;
margin: 0;
padding: 0;
display: flex;
height: 100vh;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
#left-panel {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 20px;
position: relative;
background: radial-gradient(circle at center, #092e1e 0%, var(--bg-dark) 100%);
overflow: hidden;
}
.god-rays {
position: absolute;
top: -50%; left: -50%; width: 200%; height: 200%;
background: repeating-linear-gradient(
25deg,
transparent 0%,
transparent 7%,
rgba(255, 255, 240, 0.02) 8%,
rgba(255, 255, 240, 0.05) 10%,
rgba(255, 255, 240, 0.02) 12%,
transparent 13%,
transparent 22%
);
animation: slow-rays 30s linear infinite;
pointer-events: none;
z-index: 1;
mix-blend-mode: screen;
}
.sacred-mist {
position: absolute;
bottom: 0; left: 0; width: 100%; height: 50%;
background: linear-gradient(to top, rgba(200, 255, 220, 0.08), transparent);
animation: breathe-mist 8s ease-in-out infinite alternate;
pointer-events: none;
z-index: 1;
}
@keyframes slow-rays {
0% { transform: translate(0, 0); }
100% { transform: translate(-5%, -5%); }
}
@keyframes breathe-mist {
0% { opacity: 0.4; transform: scaleY(0.9); transform-origin: bottom; }
100% { opacity: 1; transform: scaleY(1.1); transform-origin: bottom; }
}
#power-container {
text-align: center;
z-index: 10;
background: rgba(2, 10, 6, 0.8);
padding: 10px 20px;
border-radius: 8px;
border: 1px solid rgba(212, 175, 55, 0.3);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
}
#power-display {
font-size: 48px;
font-weight: bold;
text-shadow: 0 0 20px var(--gold-high);
margin: 5px 0;
font-family: 'Times New Roman', serif;
letter-spacing: 1px;
}
#pps-display {
font-size: 15px;
color: #8da2b5;
background: rgba(212, 175, 55, 0.15);
padding: 4px 16px;
border-radius: 20px;
display: inline-block;
border: 1px solid rgba(212, 175, 55, 0.2);
}
#audio-status {
font-size: 11px;
color: #6a947c;
margin-top: 6px;
letter-spacing: 1px;
}
#canvas-container {
position: relative;
width: 100%;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
touch-action: none;
}
#system-controls {
z-index: 10;
display: flex;
gap: 15px;
margin-bottom: 10px;
}
.sys-btn {
background: rgba(2, 10, 6, 0.9);
border: 1px solid var(--gold-low);
color: var(--gold);
padding: 8px 18px;
font-family: inherit;
cursor: pointer;
font-size: 13px;
font-weight: bold;
border-radius: 6px;
transition: all 0.2s;
}
.sys-btn:hover {
border-color: var(--gold);
background: rgba(212, 175, 55, 0.2);
box-shadow: 0 0 12px rgba(212, 175, 55, 0.5);
transform: translateY(-2px);
}
.sys-btn:active {
transform: translateY(0);
}
#right-panel {
width: 440px;
background-color: var(--panel-bg);
padding: 20px;
overflow-y: auto;
border-left: 1px solid rgba(212, 175, 55, 0.3);
box-shadow: -10px 0 30px rgba(0,0,0,0.9);
z-index: 5;
-webkit-overflow-scrolling: touch;
}
h1 {
font-size: 20px;
text-align: center;
border-bottom: 2px solid var(--gold);
padding-bottom: 12px;
margin-top: 0;
margin-bottom: 20px;
letter-spacing: 4px;
text-shadow: 0 0 15px rgba(212, 175, 55, 0.5);
}
.god-card {
background: linear-gradient(135deg, #07150d 0%, #030805 100%);
border: 1px solid rgba(212, 175, 55, 0.15);
padding: 12px 16px;
margin-bottom: 12px;
cursor: pointer;
border-radius: 6px;
position: relative;
overflow: hidden;
transition: all 0.1s;
}
.god-card::before {
content: '';
position: absolute;
top: 0; left: 0; width: 4px; height: 100%;
background-color: var(--god-color, #d4af37);
opacity: 0.7;
}
.god-card:hover:not(.disabled) {
border-color: rgba(212, 175, 55, 0.8);
box-shadow: 0 0 15px rgba(212, 175, 55, 0.3);
background: linear-gradient(135deg, #0b2617 0%, #030805 100%);
}
.god-card:active:not(.disabled) {
transform: scale(0.98);
}
.god-card.disabled {
opacity: 0.35;
cursor: not-allowed;
border-color: #0b1c13;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-6px); }
40%, 80% { transform: translateX(6px); }
}
.error-shake {
animation: shake 0.2s ease-in-out;
border-color: #ff3333 !important;
box-shadow: 0 0 18px rgba(255, 51, 51, 0.5) !important;
}
.god-header { display: flex; justify-content: space-between; align-items: center; }
.god-name { font-size: 16px; font-weight: bold; color: #fff; letter-spacing: 1px; }
.god-rank { font-size: 12px; color: var(--gold); background: rgba(212, 175, 55, 0.15); padding: 2px 6px; border-radius: 4px; border: 1px solid rgba(212, 175, 55, 0.3); }
.god-desc { font-size: 11px; color: #8ba797; margin: 6px 0; line-height: 1.4; }
.god-stats { font-size: 12px; display: flex; justify-content: space-between; margin-top: 6px; border-top: 1px solid rgba(212,175,55,0.15); padding-top: 6px; }
.god-cost { color: var(--gold); font-weight: bold; }
.god-total-pps { color: #8da2b5; font-size: 11px; text-align: right; margin-top: 4px; }
#right-panel::-webkit-scrollbar { width: 6px; }
#right-panel::-webkit-scrollbar-track { background: var(--panel-bg); }
#right-panel::-webkit-scrollbar-thumb { background: rgba(212, 175, 55, 0.4); border-radius: 3px; }
#right-panel::-webkit-scrollbar-thumb:hover { background: var(--gold); }
#merge-toast {
position: absolute;
bottom: 65px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.95) 15%, rgba(212, 175, 55, 0.95) 85%, transparent);
color: #03060a;
padding: 10px 30px;
font-weight: bold;
border-radius: 2px;
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
pointer-events: none;
z-index: 100;
font-size: 15px;
letter-spacing: 2px;
text-shadow: 0 0 4px rgba(255,255,255,0.6);
white-space: nowrap;
}
@media (max-width: 768px) {
body { flex-direction: column; }
#left-panel { flex: none; height: 50vh; padding: 10px; }
#right-panel { width: 100%; flex: 1; border-left: none; border-top: 2px solid rgba(212, 175, 55, 0.5); padding: 15px; }
#power-display { font-size: 38px; }
}
</style>
</head>
<body>
<div id="left-panel">
<div class="god-rays"></div>
<div class="sacred-mist"></div>
<div id="power-container">
<div style="font-size: 14px; color: #8da2b5; letter-spacing: 2px;">現在の総霊力</div>
<div id="power-display">0</div>
<div id="pps-display">自動集積: 0 霊力/秒</div>
<div id="audio-status">静寂(画面をタップして神音開放)</div>
</div>
<div id="canvas-container">
<canvas id="sacredCanvas"></canvas>
</div>
<div id="merge-toast">神格昇進</div>
<div id="system-controls">
<button class="sys-btn" onclick="saveGame(false)">💾 霊力を保存する</button>
<button class="sys-btn" onclick="confirmReset()">🔄 遷宮(初期化)</button>
</div>
</div>
<div id="right-panel">
<h1>八百万の神々・神位勧請</h1>
<div id="gods-container"></div>
</div>
<script>
let state = { reiryoku: 0, powerPerSecond: 0, clickPower: 1, godCounts: {} };
let audioCtx = null;
let isAudioInitialized = false;
let autoSaveTimer = 0;
const gods = [
{ id: 'dirt', name: "泥土の神", desc: "万物を育む母なる泥土の霊性。", baseCost: 15, basePPS: 0.2, color: "#8b7355" },
{ id: 'stone', name: "石ころの神", desc: "路傍の石に宿る精霊。", baseCost: 100, basePPS: 1, color: "#a1a8b8" },
{ id: 'water', name: "湧き水の神", desc: "清らかな水脈を司る神。", baseCost: 600, basePPS: 5, color: "#00e5ff" },
{ id: 'flower', name: "野花の神", desc: "野に咲く生命の息吹。", baseCost: 4000, basePPS: 25, color: "#ff69b4" },
{ id: 'tree', name: "木霊の神", desc: "古木に宿る若き精霊。", baseCost: 25000, basePPS: 120, color: "#00e676" },
{ id: 'breeze', name: "そよ風の神", desc: "大気を微かに揺らす風の精。", baseCost: 150000, basePPS: 650, color: "#b2dfdb" },
{ id: 'fire', name: "灯火の神", desc: "闇を照らす幻惑の火種。", baseCost: 900000, basePPS: 3800, color: "#ff1744" },
{ id: 'fox', name: "お稲荷様", desc: "五穀豊穣を司る狐の使者。", baseCost: 6000000, basePPS: 22000, color: "#ff9100" },
{ id: 'rice', name: "田の神", desc: "黄金の稲穂を守護する農耕神。", baseCost: 40000000, basePPS: 130000, color: "#ffd700" },
{ id: 'thunder', name: "雷鳴の神", desc: "天裂く紫電を操る荒神。", baseCost: 300000000, basePPS: 800000, color: "#d500f9" },
{ id: 'sea', name: "海原の神", desc: "大海原の潮汐を支配する大神。", baseCost: 2000000000, basePPS: 5000000, color: "#2979ff" },
{ id: 'mountain', name: "山の霊尊", desc: "雄大なる不滅の霊峰そのもの。", baseCost: 15000000000, basePPS: 32000000, color: "#5d4037" },
{ id: 'star', name: "星宿を統べる神", desc: "天体天幕を巡る満天の星々。", baseCost: 120000000000, basePPS: 210000000, color: "#e0f7fa" },
{ id: 'moon', name: "月読の神", desc: "夜の国を統べる美しき太陰神。", baseCost: 1000000000000, basePPS: 1500000000, color: "#fff59d" },
{ id: 'sun', name: "天照の大神", desc: "八百万の頂点に立つ至高の神。", baseCost: 10000000000000, basePPS: 12000000000, color: "#ffffff" }
];
const canvas = document.getElementById('sacredCanvas');
const ctx = canvas.getContext('2d');
let particles = [];
let waves = [];
let clickFloatingTexts = [];
let ambientSpirits = [];
let treeGlowRadius = 40;
let treePulseDir = 1;
let currentHighestGodColor = "#1a4d3a";
let waveIntensity = 1;
function init() {
loadGame();
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
gods.forEach(g => { if (state.godCounts[g.id] === undefined) state.godCounts[g.id] = 0; });
initAmbientSpirits();
recalculatePower();
initGodsListUI();
updateGodsListValues();
setupCanvasInteraction();
setInterval(gameLoop, 100);
requestAnimationFrame(drawCanvas);
}
function resizeCanvas() {
const container = document.getElementById('canvas-container');
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
generateGodOrbs();
initAmbientSpirits();
}
function initAmbientSpirits() {
ambientSpirits = [];
const count = Math.floor(canvas.width * canvas.height / 15000);
for (let i = 0; i < count; i++) {
ambientSpirits.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
radius: Math.random() * 2 + 0.5,
vx: (Math.random() - 0.5) * 0.4,
vy: -Math.random() * 0.6 - 0.1,
alpha: Math.random() * 0.5 + 0.1,
phase: Math.random() * Math.PI * 2
});
}
}
function initAudio() {
if (isAudioInitialized) return;
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
startSacredBGM();
isAudioInitialized = true;
const statusEl = document.getElementById('audio-status');
if(statusEl) statusEl.innerText = "⛩️ 神音響流(深遠なる和楽再生中)";
} catch(e) {}
}
function startSacredBGM() {
const bfreqs = [146.83, 196.00, 220.00, 233.08, 293.66, 392.00, 440.00, 466.16];
function playLayer() {
try {
if (!audioCtx) return;
const oscBase = audioCtx.createOscillator();
const gain = audioCtx.createGain();
const filter = audioCtx.createBiquadFilter();
const f1 = bfreqs[Math.floor(Math.random() * bfreqs.length)];
oscBase.type = 'triangle';
oscBase.frequency.setValueAtTime(f1 / 2, audioCtx.currentTime);
filter.type = 'lowpass';
filter.frequency.setValueAtTime(380, audioCtx.currentTime);
const now = audioCtx.currentTime;
gain.gain.setValueAtTime(0, now);
gain.gain.linearRampToValueAtTime(0.04, now + 4.0);
gain.gain.exponentialRampToValueAtTime(0.0001, now + 11.0);
oscBase.connect(filter);
filter.connect(gain);
gain.connect(audioCtx.destination);
oscBase.start(now);
oscBase.stop(now + 11.0);
} catch(e) {}
}
setInterval(playLayer, 5000);
playLayer();
}
function playPraySE() {
try {
if (!audioCtx) return;
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(1200, now);
osc.frequency.exponentialRampToValueAtTime(880, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.3);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.4);
} catch(e) {}
}
function playBuySE() {
try {
if (!audioCtx) return;
const now = audioCtx.currentTime;
const notes = [587.33, 698.46, 880.00, 1174.66];
notes.forEach((freq, i) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now + (i * 0.05));
gain.gain.setValueAtTime(0.15, now + (i * 0.05));
gain.gain.exponentialRampToValueAtTime(0.0001, now + (i * 0.05) + 0.3);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now + (i * 0.05));
osc.stop(now + (i * 0.05) + 0.3);
});
} catch(e) {}
}
function playMergeSE() {
try {
if (!audioCtx) return;
const now = audioCtx.currentTime;
const bells = [440.00, 880.00, 1318.51];
bells.forEach((freq, i) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = i === 0 ? 'triangle' : 'sine';
osc.frequency.setValueAtTime(freq, now);
let duration = 3.0 - (i * 0.5);
gain.gain.setValueAtTime(0.3 / (i + 1), now);
gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + duration);
});
const sparkles = [1046.50, 1318.51, 1567.98, 2093.00];
sparkles.forEach((freq, i) => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now + (i * 0.06));
gain.gain.setValueAtTime(0.15, now + (i * 0.06));
gain.gain.exponentialRampToValueAtTime(0.0001, now + (i * 0.06) + 0.4);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now + (i * 0.06));
osc.stop(now + (i * 0.06) + 0.4);
});
} catch(e) {}
}
function playErrorSE() {
try {
if (!audioCtx) return;
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(220.00, now);
osc.frequency.exponentialRampToValueAtTime(110.00, now + 0.1);
gain.gain.setValueAtTime(0.15, now);
gain.gain.linearRampToValueAtTime(0.0001, now + 0.15);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + 0.15);
} catch(e) {}
}
function setupCanvasInteraction() {
const handleInteract = (e) => {
initAudio();
const rect = canvas.getBoundingClientRect();
let clientX = e.clientX;
let clientY = e.clientY;
if (e.touches && e.touches.length > 0) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
}
const x = clientX - rect.left;
const y = clientY - rect.top;
state.reiryoku += state.clickPower;
playPraySE();
triggerWave();
for (let i = 0; i < 8; i++) {
particles.push({
x: x, y: y,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
radius: Math.random() * 2 + 1,
color: "#fff",
alpha: 1,
decay: Math.random() * 0.03 + 0.02,
type: 'spark'
});
}
clickFloatingTexts.push({
x: x, y: y - 15,
text: `+${formatNumber(state.clickPower)}`,
alpha: 1,
vy: -1.5
});
updateDisplay();
updateGodsListValues();
};
canvas.addEventListener('mousedown', handleInteract);
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
handleInteract(e);
}, {passive: false});
}
function triggerWave() {
waves.push({
radius: 65,
maxRadius: Math.min(canvas.width, canvas.height) * 0.45 * waveIntensity,
alpha: 0.5,
speed: 2.5 + waveIntensity * 0.3,
color: currentHighestGodColor
});
}
// ★ ブラッシュアップ:マージされたオーブは明確に大きく描画されるように計算式を変更
function generateGodOrbs() {
particles = particles.filter(p => p.type !== 'orb');
let orbitIndex = 1;
let minDim = Math.min(canvas.width, canvas.height);
let orbitStep = minDim * 0.03;
if(orbitStep < 10) orbitStep = 10;
gods.forEach(god => {
let count = state.godCounts[god.id] || 0;
let tier = 1;
let baseRadius = 75 + (orbitIndex * orbitStep);
while (count > 0) {
let numAtThisTier = count % 5;
for (let i = 0; i < numAtThisTier; i++) {
particles.push({
godId: god.id, tier: tier, color: god.color,
baseRadius: baseRadius + (tier * 4),
angle: Math.random() * Math.PI * 2,
speed: (0.015 - (baseRadius * 0.00002)) + (tier * 0.002),
size: tier === 1 ? 4.5 : 4.5 + Math.pow(tier, 1.4) * 3, // 階位が高いほどドラマチックに巨大化
type: 'orb'
});
}
count = Math.floor(count / 5);
tier++;
}
orbitIndex++;
});
}
function drawCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const cx = canvas.width / 2;
const cy = canvas.height / 2;
ambientSpirits.forEach(sp => {
sp.x += sp.vx;
sp.y += sp.vy;
sp.phase += 0.05;
if (sp.y < -10) sp.y = canvas.height + 10;
if (sp.x < -10) sp.x = canvas.width + 10;
if (sp.x > canvas.width + 10) sp.x = -10;
let currentAlpha = sp.alpha + Math.sin(sp.phase) * 0.2;
if(currentAlpha < 0) currentAlpha = 0;
ctx.save();
ctx.beginPath();
ctx.arc(sp.x, sp.y, sp.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 250, 230, ${currentAlpha})`;
ctx.shadowBlur = sp.radius * 4;
ctx.shadowColor = "rgba(255, 240, 180, 0.8)";
ctx.fill();
ctx.restore();
});
waves.forEach((w, index) => {
w.radius += w.speed;
w.alpha -= 0.008;
if (w.alpha <= 0 || w.radius >= w.maxRadius) {
waves.splice(index, 1);
return;
}
ctx.save();
ctx.beginPath();
ctx.arc(cx, cy, w.radius, 0, Math.PI * 2);
let gradient = ctx.createRadialGradient(cx, cy, w.radius - 30, cx, cy, w.radius);
gradient.addColorStop(0, 'transparent');
gradient.addColorStop(0.7, hexToRgbA(w.color, w.alpha));
gradient.addColorStop(1, 'transparent');
ctx.strokeStyle = gradient;
ctx.lineWidth = 15;
ctx.stroke();
ctx.restore();
});
particles.forEach((p) => {
if (p.type === 'orb') {
p.angle += p.speed;
p.x = cx + Math.cos(p.angle) * p.baseRadius;
p.y = cy + Math.sin(p.angle) * p.baseRadius;
ctx.save();
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.shadowBlur = p.size * 3;
ctx.shadowColor = p.color;
ctx.fillStyle = "#fff";
ctx.fill();
ctx.strokeStyle = p.color;
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.restore();
} else if (p.type === 'spark') {
p.x += p.vx; p.y += p.vy; p.alpha -= p.decay;
if (p.alpha > 0) {
ctx.save();
ctx.globalAlpha = p.alpha;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = p.color;
ctx.fill();
ctx.restore();
}
}
});
particles = particles.filter(p => p.type === 'orb' || (p.type === 'spark' && p.alpha > 0));
treeGlowRadius += treePulseDir * 0.2;
if (treeGlowRadius > 55 || treeGlowRadius < 35) treePulseDir *= -1;
ctx.save();
let treeGlow = ctx.createRadialGradient(cx, cy, 30, cx, cy, 75 + treeGlowRadius);
treeGlow.addColorStop(0, hexToRgbA(currentHighestGodColor, 0.55));
treeGlow.addColorStop(0.5, hexToRgbA(currentHighestGodColor, 0.2));
treeGlow.addColorStop(1, 'transparent');
ctx.fillStyle = treeGlow;
ctx.beginPath();
ctx.arc(cx, cy, 75 + treeGlowRadius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.save();
ctx.shadowBlur = 20;
ctx.shadowColor = "rgba(0,0,0,0.9)";
let treeBaseGrad = ctx.createRadialGradient(cx, cy, 10, cx, cy, 65);
treeBaseGrad.addColorStop(0, '#15291e');
treeBaseGrad.addColorStop(0.7, '#07120d');
treeBaseGrad.addColorStop(1, '#030805');
ctx.fillStyle = treeBaseGrad;
ctx.beginPath();
ctx.arc(cx, cy, 65, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = varToHex('--gold');
ctx.lineWidth = 3;
ctx.stroke();
ctx.restore();
ctx.save();
ctx.strokeStyle = 'rgba(212, 175, 55, 0.3)';
ctx.lineWidth = 1.5;
for(let r = 15; r < 60; r += 12) {
ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.stroke();
}
ctx.strokeStyle = 'rgba(3, 6, 10, 0.7)';
ctx.lineWidth = 4;
for (let a = 0; a < Math.PI * 2; a += Math.PI / 4) {
ctx.beginPath();
ctx.moveTo(cx + Math.cos(a)*10, cy + Math.sin(a)*10);
ctx.lineTo(cx + Math.cos(a + 0.2)*62, cy + Math.sin(a + 0.2)*62);
ctx.stroke();
}
ctx.restore();
ctx.save();
ctx.fillStyle = "#fff";
ctx.font = "bold 20px 'Hiragino Mincho ProN', serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.shadowBlur = 10;
ctx.shadowColor = varToHex('--gold');
ctx.letterSpacing = "2px";
ctx.fillText("御神木", cx, cy);
ctx.restore();
clickFloatingTexts.forEach((t, index) => {
t.y += t.vy; t.alpha -= 0.02;
if (t.alpha > 0) {
ctx.save();
ctx.globalAlpha = t.alpha;
ctx.fillStyle = "#fff";
ctx.font = "italic bold 18px 'Times New Roman'";
ctx.shadowBlur = 6;
ctx.shadowColor = varToHex('--gold');
ctx.fillText(t.text, t.x, t.y);
ctx.restore();
} else {
clickFloatingTexts.splice(index, 1);
}
});
requestAnimationFrame(drawCanvas);
}
function recalculatePower() {
state.powerPerSecond = 0;
gods.forEach(god => {
let count = state.godCounts[god.id] || 0;
let tier = 1;
let godTotalPPS = 0;
while (count > 0) {
let numAtThisTier = count % 5;
let tierMultiplier = Math.pow(15, tier - 1);
godTotalPPS += numAtThisTier * god.basePPS * tierMultiplier;
count = Math.floor(count / 5);
tier++;
}
state.powerPerSecond += godTotalPPS;
});
state.clickPower = 1 + Math.floor(state.powerPerSecond * 0.1);
}
function updateWaveEffect() {
let highestGod = null;
for (let i = gods.length - 1; i >= 0; i--) {
if ((state.godCounts[gods[i].id] || 0) > 0) {
highestGod = gods[i]; break;
}
}
if (highestGod) {
currentHighestGodColor = highestGod.color;
let maxTier = 1;
let tempCount = state.godCounts[highestGod.id];
while (tempCount >= 5) { tempCount = Math.floor(tempCount / 5); maxTier++; }
waveIntensity = 1 + (maxTier * 0.35) + (gods.indexOf(highestGod) * 0.1);
} else {
currentHighestGodColor = "#1a4d3a"; waveIntensity = 1;
}
}
function getCost(god) {
let count = state.godCounts[god.id] || 0;
return Math.floor(god.baseCost * Math.pow(1.12, count));
}
function buyGod(index) {
try { initAudio(); } catch(e) {}
const god = gods[index];
const cost = getCost(god);
if (state.reiryoku >= cost) {
state.reiryoku -= cost;
state.godCounts[god.id] = (state.godCounts[god.id] || 0) + 1;
let newCount = state.godCounts[god.id];
if (newCount > 0 && newCount % 5 === 0) {
try { showMergeToast(`${god.name}が合一し、神位が昇進した!`); } catch(e){}
try { playMergeSE(); } catch(e){}
setTimeout(triggerWave, 100);
} else {
try { playBuySE(); } catch(e){}
}
recalculatePower();
updateGodsListValues();
generateGodOrbs();
updateWaveEffect();
updateDisplay();
saveGame(true);
} else {
try { playErrorSE(); } catch(e){}
const el = document.getElementById(`god-${index}`);
if (el) {
el.classList.add('error-shake');
setTimeout(() => el.classList.remove('error-shake'), 200);
}
}
}
function gameLoop() {
state.reiryoku += (state.powerPerSecond / 10);
updateDisplay();
updateCardStates();
autoSaveTimer++;
if (autoSaveTimer >= 300) {
saveGame(true);
autoSaveTimer = 0;
}
}
function updateDisplay() {
const displayEl = document.getElementById('power-display');
const ppsEl = document.getElementById('pps-display');
if(displayEl) displayEl.innerText = formatNumber(Math.floor(state.reiryoku));
if(ppsEl) ppsEl.innerText = `自動集積: ${formatNumber(Math.floor(state.powerPerSecond))} 霊力/秒`;
}
function formatNumber(num) {
if (num < 10000) return num.toLocaleString();
if (num < 100000000) return (num / 10000).toFixed(2) + "万";
if (num < 1000000000000) return (num / 100000000).toFixed(2) + "億";
if (num < 1000000000000000) return (num / 1000000000000).toFixed(2) + "兆";
return (num / 1000000000000000).toFixed(2) + "京";
}
function initGodsListUI() {
const container = document.getElementById('gods-container');
if(!container) return;
container.innerHTML = '';
gods.forEach((god, index) => {
const card = document.createElement('div');
card.id = `god-${index}`;
card.className = 'god-card disabled';
card.style.setProperty('--god-color', god.color);
card.addEventListener('click', () => buyGod(index));
card.innerHTML = `
<div class="god-header">
<span class="god-name">${god.name}</span>
<span class="god-rank" id="god-rank-${index}">神位:0 (階位:1)</span>
</div>
<div class="god-desc">${god.desc}</div>
<div class="god-stats">
<span>必要霊力: <span class="god-cost" id="god-cost-${index}">0</span></span>
<span>基礎: +${formatNumber(god.basePPS)}/秒</span>
</div>
<div class="god-total-pps" id="god-pps-${index}">現在貢献度: +0/秒</div>
`;
container.appendChild(card);
});
}
function updateGodsListValues() {
gods.forEach((god, index) => {
const cost = getCost(god);
const count = state.godCounts[god.id] || 0;
let maxTier = 1;
let tempCount = count;
while (tempCount >= 5) {
tempCount = Math.floor(tempCount / 5);
maxTier++;
}
let godTotalPPS = 0;
let tc = count;
let t = 1;
while (tc > 0) {
godTotalPPS += (tc % 5) * god.basePPS * Math.pow(15, t - 1);
tc = Math.floor(tc / 5);
t++;
}
const rankEl = document.getElementById(`god-rank-${index}`);
const costEl = document.getElementById(`god-cost-${index}`);
const ppsEl = document.getElementById(`god-pps-${index}`);
if(rankEl) rankEl.innerText = `神位:${count} (階位:${maxTier})`;
if(costEl) costEl.innerText = formatNumber(cost);
if(ppsEl) ppsEl.innerText = `現在貢献度: +${formatNumber(Math.floor(godTotalPPS))}/秒`;
const card = document.getElementById(`god-${index}`);
if(card) {
if (state.reiryoku >= cost) {
card.classList.remove('disabled');
} else {
if (!card.classList.contains('error-shake')) {
card.classList.add('disabled');
}
}
}
});
}
function updateCardStates() {
gods.forEach((god, index) => {
const el = document.getElementById(`god-${index}`);
if (el) {
const cost = getCost(god);
if (state.reiryoku >= cost) {
el.classList.remove('disabled');
} else {
if (!el.classList.contains('error-shake')) el.classList.add('disabled');
}
}
});
}
function showMergeToast(message) {
try {
const toast = document.getElementById('merge-toast');
if(!toast) return;
toast.innerText = message;
toast.style.opacity = 1;
toast.style.transform = "translateX(-50%) translateY(-5px)";
setTimeout(() => {
toast.style.opacity = 0;
toast.style.transform = "translateX(-50%) translateY(0)";
}, 1500);
} catch(e) {}
}
function saveGame(silent = false) {
try {
localStorage.setItem('yaoyorozu_save_v9', JSON.stringify(state));
if (!silent) showMergeToast("神域の霊力を保存しました 💾");
} catch (e) {
if (!silent) showMergeToast("※テスト環境のため保存は仮状態です");
}
}
function loadGame() {
try {
const saved = localStorage.getItem('yaoyorozu_save_v9') || localStorage.getItem('yaoyorozu_save_v8') || localStorage.getItem('yaoyorozu_save_v7') || localStorage.getItem('yaoyorozu_save_v6') || localStorage.getItem('yaoyorozu_save_v5') || localStorage.getItem('yaoyorozu_save_v4');
if (saved) {
const parsed = JSON.parse(saved);
if (parsed && typeof parsed === 'object') state = { ...state, ...parsed };
}
} catch(e) {}
}
function confirmReset() {
if (confirm("神域を遷宮(初期化)し、全ての霊力と神位をまっさらに戻しますか?")) {
try {
localStorage.removeItem('yaoyorozu_save_v9');
localStorage.removeItem('yaoyorozu_save_v8');
localStorage.removeItem('yaoyorozu_save_v7');
localStorage.removeItem('yaoyorozu_save_v6');
localStorage.removeItem('yaoyorozu_save_v5');
localStorage.removeItem('yaoyorozu_save_v4');
} catch(e) {}
state = { reiryoku: 0, powerPerSecond: 0, clickPower: 1, godCounts: {} };
gods.forEach(g => state.godCounts[g.id] = 0);
recalculatePower();
updateGodsListValues();
generateGodOrbs();
updateWaveEffect();
updateDisplay();
showMergeToast("神域が新しく遷宮されました");
if (audioCtx) playMergeSE();
}
}
function hexToRgbA(hex, alpha) {
let c;
if(/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)){
c= hex.substring(1).split('');
if(c.length== 3) c= [c[0], c[0], c[1], c[1], c[2], c[2]];
c= '0x'+c.join('');
return 'rgba('+[(c>>16)&255, (c>>8)&255, c&255].join(',')+','+alpha+')';
}
return hex;
}
function varToHex(varName) {
return getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
}
init();
</script>
</body>
</html>


■ 今日の学び

重力、磁力、温度、湿度……そして誰かの記憶や想い。

私たちの暮らす世界は、
目には見えないけれど確かに存在する“力”で満ちています。

「パワースポット」と呼ばれる場所にも、
きっとそんな不思議な力が働いているのでしょう。

理由はうまく言葉にできないけれど、
そこにいるだけでふっと心が軽くなったり、
じんわり幸せな気持ちになれたりします。( ˘꒳˘ )

実は私、そういう空間が大好きで、
家の庭に集めたシーグラスを集めて、
“自作のなんちゃってパワースポット”を作っていたりします。

光が当たると淡く透けて、見ているだけで気持ちが整うんです。

今回作った『八百万の神域』は、
そんな「幸せになれる空間」を
ゲームの中でも再現してみたいと思って生まれた作品でした。

ゲーム自体はとてもシンプルで、
御神木をクリックして神様を召喚していくだけ……なのですが。

ブログ用にスクショを撮ろうと思って起動したら、
光が集まっていくのが心地よくて、
つい本気でやり込んでしまいました ꜀( ꜆-ࡇ-)꜆

最初のワンクリックをしてしまうと、
なんだか止まらなくなる不思議なゲームで、
気がつけば何時間もご神木をタップしていました(>x<)

最近、ブログの記事更新は少しゆっくりペースになっていますが、
「1日1個目標」のゲーム作りは毎日コツコツ続けていて、
気がつけばストックが 180個 ほどに増えました!ᕙ( ˙-˙ )ᕗ

まだ記事にできていない実験ゲームたちも、
みなさんに遊んでいただけるように「固定ページ」を準備中です ദ്ദി( ¯꒳¯ )

これからも、のんびりと実験室を楽しんでいただけたら嬉しいです。

ぜひ、画面の中の小さなパワースポットで癒やされてみてくださいね。

次回は、暑さも吹き飛ぶ滑空ゲーム

高原を気持ちよく滑空するゲームです。

高原のギリギリを高速で飛び抜けていくのは、
以前の実験 #74「和紙の鷲のわし」で登場した、
あの和紙の鷲(わし)です。

スピード感があって、
ちょっとヒヤッとするような空中散歩を楽しめるかもしれません。

次回もぜひ楽しみにしていてくださいね。(*^v^)ノシ

次の実験はこちら
今回もギリギリを攻める鷲の滑空ゲーム
「高原ギリギリ滑空」



 ✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)

~このブログについて~

AIと一緒に遊び、
100の実験でゲームを育てるブログ

🌈初めての方へ
🌱このブログについて
🌳実験一覧
🍃プライバシーポリシー
🐣未記事ゲーム展示室


AIと作る100のゲーム実験室(*^v^*)をもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む