というわけでして、前回本格的落ち物パズルゲームが遊べるブログ記事を作りましたが、今回は第2段!
魔法使いの農家さん!?が森の中でジャガイモや果物を収穫して家まで運ぶ!というゲームを作りました。
見た目はこんな感じ
収穫
森に落ちてるジャガイモ、ぶどう、メロンを回収し、🏚️まで持っていくとスコアが増えます。
初期は5個しか拾えませんが、Lvアップする事により持てる数が増えていきます。
Lvアップするには?
森の中にいるスライム🔵を倒すか、夜に現れる💀を倒すと経験値がもらえ、一定の数が貯まるとLvアップします。
攻撃方法は?
敵がいる方に方向キーを入れ、Zキーで攻撃。
序盤の攻撃範囲は向いてる方の1マス分ですが、Lvアップをする事により前方3方向、さらにLvアップすると前方3方向+距離が伸びるようにしてあります。
敵の種類
現在…というかテストで作ったバージョンだと下記しか作っていません。
🔵=スライム
💀=骸骨
条件を満たすと出現するボス
🔴=赤いスライム
💀=骸骨王 普通の骸骨より大きめ
前回作った本格はパズルゲームと同じくこのゲームのコードを公開しますので、もし改造したい!自分のブログで遊んでみたい!という人は下記コードを使ってください。
ちなみに前回のゲームは動画でも簡単に解説していますので、よろしかったらどうぞ。
なお、自分のブログにてこのゲームを公開する場合、この記事のURLでよろしいので、
ゲームコード配布場所 Gスカの適当に遊べ
URL https://skgura.blogspot.com/2026/02/madou-noufu.html
という感じで表記をお願いします。
という感じで表記をお願いします。
↓以下コード
<div id="game-container" style="text-align: center; padding: 10px; font-family: sans-serif; width: 420px; margin: 0 auto; background: #1a1a1a; border: 5px solid #444; border-radius: 20px; color: white; box-shadow: 0 10px 30px rgba(0,0,0,0.5); position: relative;">
<div id="game-over-screen" style="display:none; position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); width:300px; background:rgba(0,0,0,0.85); border:3px solid #ff4444; border-radius:15px; z-index:100; padding:20px;">
<h2 style="color:#ff4444; margin:0 0 15px 0;">GAME OVER</h2>
<p style="font-size:1.2em; margin:5px 0;">FINAL SCORE: <span id="final-score" style="color:#ffeb3b; font-weight:bold;">0</span></p>
<button onclick="location.reload()" style="margin-top:20px; padding:10px 20px; font-size:1em; cursor:pointer; background:#ff4444; color:white; border:none; border-radius:8px; font-weight:bold;">RETRY</button>
</div>
<div id="start-screen" style="position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); width:300px; background:rgba(0,0,0,0.85); border:3px solid #4caf50; border-radius:15px; z-index:90; padding:20px;">
<h2 style="color:#4caf50; margin:0 0 10px 0;">ゲームタイトル</h2>
<p style="font-size:0.9em; line-height:1.4;">Z:攻撃 / 矢印:移動<br>一部音がでます!</p>
<button id="start-button" style="margin-top:20px; padding:12px 24px; font-size:1.1em; cursor:pointer; background:#4caf50; color:white; border:none; border-radius:8px; font-weight:bold;">スタート</button>
</div>
<div id="status-area" style="background: #f0f0f0; padding: 10px; border-radius: 12px; margin-bottom: 8px; color: #333; text-align: left; border: 2px solid #888;">
<div id="timer-box" style="text-align: center; background: #333; color: #fff; border-radius: 6px; padding: 5px; margin-bottom: 8px; border: 2px solid #555;">
<span id="time-label" style="font-weight: bold; font-size: 1.1em; margin-right: 10px;">朝の収穫タイム</span>
<span id="countdown" style="font-size: 1.4em; font-family: 'Courier New', monospace; color: #ffeb3b; font-weight: bold;">20</span><span style="font-size: 0.8em; color: #ffeb3b;">s</span>
</div>
<div style="font-size: 0.85em; font-weight: bold; margin-bottom: 2px; color: #d32f2f;">LIFE (HP)</div>
<div style="width: 100%; background: #ccc; height: 14px; border-radius: 7px; overflow: hidden; margin-bottom: 8px; border: 1px solid #999;">
<div id="hp-bar" style="width: 100%; background: #ff4444; height: 100%; transition: width 0.3s;"></div>
</div>
<div style="font-size: 0.85em; font-weight: bold; margin-bottom: 2px; color: #2e7d32;">EXP (Lv.<span id="lv-num">1</span>)</div>
<div style="width: 100%; background: #ccc; height: 10px; border-radius: 5px; overflow: hidden; border: 1px solid #999;">
<div id="exp-bar" style="width: 0%; background: #4caf50; height: 100%; transition: width 0.3s;"></div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 10px; font-size: 0.75em; font-weight: bold; background: #fff; padding: 6px; border-radius: 6px; border: 1px solid #ddd;">
<span>🔵:<span id="slime-count">0</span>/15 💀:<span id="skull-count">0</span>/10</span>
<span>🧺:<span id="bag-count">0</span>/<span id="bag-max">5</span> 💰:<span id="score-val">0</span></span>
</div>
</div>
<canvas id="gameCanvas" width="400" height="400" style="background: #c8e6c9; border: 4px solid #000; border-radius: 5px;"></canvas>
<div id="controls" style="margin-top: 10px; display: grid; grid-template-columns: repeat(3, 75px); gap: 10px; justify-content: center;">
<span></span><button onmousedown="move(0, -1)">↑</button><span></span>
<button onmousedown="move(-1, 0)">←</button>
<button onmousedown="attack()" style="background: #ffeb3b; font-weight: bold; border-radius: 50%;">Z</button>
<button onmousedown="move(1, 0)">→</button>
<span></span><button onmousedown="move(0, 1)">↓</button><span></span>
</div>
</div>
<script>
(function() {
// --- 1. 変数の準備:HTML要素を取得してプログラムで使えるようにする ---
const canvas = document.getElementById('gameCanvas'), ctx = canvas.getContext('2d');
const hpBar = document.getElementById('hp-bar'), expBar = document.getElementById('exp-bar');
const lvNum = document.getElementById('lv-num'), scoreVal = document.getElementById('score-val');
const slimeDisp = document.getElementById('slime-count'), skullDisp = document.getElementById('skull-count');
const bagDisp = document.getElementById('bag-count'), bagMaxDisp = document.getElementById('bag-max');
const timeLabel = document.getElementById('time-label'), countdownDisp = document.getElementById('countdown');
const startScreen = document.getElementById('start-screen'), startBtn = document.getElementById('start-button');
const gameOverScreen = document.getElementById('game-over-screen');
// --- 2. オーディオ設定(Web Audio API) ---
let audioCtx = null;
function initAudio() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
// 状況に合わせて音を生成する関数
function playSound(type) {
if (!audioCtx) return;
const now = audioCtx.currentTime;
if (type === 'hit') { // 攻撃音
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.type = 'square'; o.frequency.setValueAtTime(150, now); o.frequency.exponentialRampToValueAtTime(40, now + 0.1);
g.gain.setValueAtTime(0.2, now); g.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(now + 0.1);
} else if (type === 'pickup') { // アイテム取得音
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.type = 'sine'; o.frequency.setValueAtTime(880, now); o.frequency.exponentialRampToValueAtTime(1760, now + 0.1);
g.gain.setValueAtTime(0.1, now); g.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(now + 0.1);
} else if (type === 'lvup') { // レベルアップファンファーレ
[523.25, 659.25, 783.99, 1046.50].forEach((f, i) => {
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.type = 'square'; o.frequency.setValueAtTime(f, now + i * 0.1);
g.gain.setValueAtTime(0.05, now + i * 0.1); g.gain.exponentialRampToValueAtTime(0.01, now + i * 0.1 + 0.2);
o.connect(g); g.connect(audioCtx.destination); o.start(now + i * 0.1); o.stop(now + i * 0.1 + 0.2);
});
} else if (type === 'slime-boss') { // ボススライム登場音
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.type = 'triangle'; o.frequency.setValueAtTime(100, now); o.frequency.exponentialRampToValueAtTime(500, now + 0.15); o.frequency.exponentialRampToValueAtTime(150, now + 0.5);
g.gain.setValueAtTime(0.3, now); g.gain.exponentialRampToValueAtTime(0.01, now + 0.5);
o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(now + 0.5);
} else if (type === 'skull-boss') { // ボスガイコツ登場音
[100, 110, 120].forEach(f => {
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.type = 'sawtooth'; o.frequency.setValueAtTime(f, now); o.frequency.exponentialRampToValueAtTime(40, now + 0.8);
g.gain.setValueAtTime(0.15, now); g.gain.linearRampToValueAtTime(0, now + 0.8);
o.connect(g); g.connect(audioCtx.destination); o.start(); o.stop(now + 0.8);
});
}
}
// --- 3. ゲームの初期データ設定 ---
const GS = 20, TIMES = [20, 20, 40]; // 1マスのサイズ、朝・夕・夜の秒数
let player = { x: 5, y: 5, dx: 0, dy: 1 }, map = [], enemies = [], attackEffects = [], gameMessages = [];
let score = 0, bag = 0, lv = 1, exp = 0, hp = 10, maxHp = 10, gameTime = 0, killedSlimes = 0, killedSkulls = 0, isRunning = false, lastMode = 0;
// マップ生成:10%の確率で「木(障害物)」を置く。2,2の地点には「家」
for (let y = 0; y < 20; y++) { map[y] = []; for (let x = 0; x < 20; x++) map[y][x] = Math.random() < 0.1 ? '🌳' : ''; }
map[2][2] = '🏠';
// 次のレベルまでの必要経験値を計算
function getNextExp() { return lv * 5; }
// 画面中央にメッセージを出す関数
function addMessage(txt, color = "yellow") { gameMessages.push({text: txt, timer: 60, color: color}); }
// --- 4. 敵のAI(A*風経路探索):プレイヤーを追い詰める頭脳 ---
function findPath(start, target) {
let openList = [{x: start.x, y: start.y, g: 0, h: Math.abs(start.x-target.x)+Math.abs(start.y-target.y), parent: null}];
let closedList = new Set();
while(openList.length > 0) {
openList.sort((a, b) => (a.g + a.h) - (b.g + b.h)); // 近い順に並び替え
let curr = openList.shift();
if(curr.x === target.x && curr.y === target.y) return curr;
closedList.add(curr.x + "," + curr.y);
[[0,-1],[0,1],[-1,0],[1,0]].forEach(([dx, dy]) => {
let nx = curr.x + dx, ny = curr.y + dy;
if(nx>=0 && nx<20 && ny>=0 && ny<20 && map[ny][nx] !== '🌳' && !closedList.has(nx+","+ny)) {
let g = curr.g + 1;
let existing = openList.find(o => o.x === nx && o.y === ny);
if(!existing || g < existing.g) { if(!existing) openList.push({x: nx, y: ny, g: g, h: Math.abs(nx-target.x)+Math.abs(ny-target.y), parent: curr}); else { existing.g = g; existing.parent = curr; } }
}
});
if(closedList.size > 150) break; // 探索しすぎ防止(重くならないように)
}
return null;
}
// --- 5. 描画処理:1秒間に何度も画面を書き換える ---
function draw() {
let total = TIMES[0]+TIMES[1]+TIMES[2], cur = gameTime % total, mode = cur < TIMES[0] ? 0 : (cur < TIMES[0]+TIMES[1] ? 1 : 2);
// 時間帯によって背景色を変える
canvas.style.background = ["#c8e6c9","#ffe0b2","#1a237e"][mode];
ctx.clearRect(0,0,400,400); ctx.textAlign = "center"; ctx.textBaseline = "middle";
// マップ、アイテム(🌳、🏠など)を描画
for (let y=0; y<20; y++) for (let x=0; x<20; x++) if (map[y][x]) ctx.fillText(map[y][x], x*GS+10, y*GS+10);
// 敵を描画
enemies.forEach(e => {
ctx.font = (e.type === 'KING' ? "35px" : "18px") + " sans-serif";
ctx.fillText(e.emoji, e.x*GS+10, e.y*GS+10);
if(e.hp > 1) { // 敵のHPが1より大きい時だけ数字を表示
ctx.fillStyle = (e.type === 'S' || e.type === 'KING') ? "#00ffff" : "white";
ctx.font = "bold 11px sans-serif"; ctx.strokeStyle = "black"; ctx.lineWidth = 2;
ctx.strokeText(e.hp, e.x*GS+18, e.y*GS+4); ctx.fillText(e.hp, e.x*GS+18, e.y*GS+4);
}
});
// プレイヤーと攻撃エフェクト
ctx.font = "18px sans-serif"; ctx.fillText("🧙", player.x*GS+10, player.y*GS+10);
attackEffects.forEach(eff => ctx.fillText("💥", eff.x*GS+10, eff.y*GS+10));
// メッセージ表示(LEVEL UP!など)
gameMessages.forEach((m, i) => {
ctx.fillStyle = m.color; ctx.font = "bold 24px sans-serif"; ctx.strokeStyle = "black"; ctx.lineWidth = 3;
ctx.strokeText(m.text, 200, 150 - (i * 30)); ctx.fillText(m.text, 200, 150 - (i * 30)); m.timer--;
});
gameMessages = gameMessages.filter(m => m.timer > 0);
// UI(数字とバー)の更新
slimeDisp.innerText = killedSlimes; skullDisp.innerText = killedSkulls; bagDisp.innerText = bag; bagMaxDisp.innerText = lv * 5; lvNum.innerText = lv; scoreVal.innerText = score;
hpBar.style.width = (hp / maxHp * 100) + "%"; expBar.style.width = (exp / getNextExp() * 100) + "%";
let remain = mode===0 ? TIMES[0]-cur : (mode===1 ? (TIMES[0]+TIMES[1])-cur : total-cur);
timeLabel.innerText = ["朝の収穫タイム", "夕暮れの気配", "恐怖のナイトメア"][mode]; countdownDisp.innerText = remain;
if(isRunning) requestAnimationFrame(draw); // ゲーム中なら次のフレームを予約
}
// --- 6. プレイヤーの移動処理 ---
window.move = function(mx, my) {
if(!isRunning) return;
let nx = player.x + mx, ny = player.y + my; player.dx = mx; player.dy = my;
if (nx>=0 && nx<20 && ny>=0 && ny<20 && map[ny][nx] !== '🌳') {
player.x = nx; player.y = ny;
// 作物の収穫判定
if (['🥔','🍈','🍇'].includes(map[ny][nx]) && bag < lv*5) {
playSound('pickup'); score += map[ny][nx]==='🥔' ? 10 : (map[ny][nx]==='🍇' ? 30 : 50); bag++; map[ny][nx] = '';
}
// 家に帰って出荷(ボーナススコア)
if (map[ny][nx] === '🏠' && bag > 0) { score += bag*5; bag = 0; }
}
};
// --- 7. 攻撃(魔法の鎌)処理 ---
window.attack = function() {
if(!isRunning) return;
let targets = [], reach = (lv >= 10) ? 2 : 1; // Lv10で射程2に
for(let r = 1; r <= reach; r++) {
targets.push({x: player.x + player.dx * r, y: player.y + player.dy * r});
if (lv >= 5) { // Lv5で広範囲攻撃(左右にも判定)
let sX = player.dy, sY = player.dx;
targets.push({x: player.x + (player.dx * r) + sX, y: player.y + (player.dy * r) + sY});
targets.push({x: player.x + (player.dx * r) - sX, y: player.y + (player.dy * r) - sY});
}
}
attackEffects = targets; // 💥を表示
targets.forEach(t => {
let hitIdx = enemies.findIndex(e => e.x === t.x && e.y === t.y);
if (hitIdx !== -1) {
playSound('hit'); enemies[hitIdx].hp--;
if (enemies[hitIdx].hp <= 0) { // 敵を倒した時
if(enemies[hitIdx].type === 'B') { // スライムを倒した時
killedSlimes++;
if(killedSlimes >= 15) { // 15体ごとに赤スライム出現チャンス
killedSlimes = 0;
if(Math.random() < 0.5) {
playSound('slime-boss'); addMessage("RED SLIME!!", "red");
enemies.push({ x: 0, y: 0, type: 'RED', emoji: '🔴', hp: 8, exp: 15 });
}
}
}
if(enemies[hitIdx].type === 'S') { // ガイコツを倒した時
killedSkulls++;
if(killedSkulls >= 10) { // 10体ごとにガイコツ王出現チャンス
killedSkulls = 0;
if(Math.random() < 0.33) {
playSound('skull-boss'); addMessage("SKULL KING!!", "purple");
enemies.push({ x: 19, y: 19, type: 'KING', emoji: '💀', hp: 20, exp: 40 });
}
}
}
exp += enemies[hitIdx].exp; enemies.splice(hitIdx, 1);
// レベルアップ判定
if (exp >= getNextExp()) { playSound('lvup'); exp = 0; lv++; hp = maxHp; addMessage("LEVEL UP!", "#00ff00"); }
}
}
});
setTimeout(() => { attackEffects = []; }, 200); // 0.2秒後にエフェクト消去
};
// --- 8. メインループ:時間経過と敵の動き ---
function gameLoop() {
if(!isRunning) return;
gameTime++;
let total = TIMES[0]+TIMES[1]+TIMES[2], cur = gameTime % total, mode = cur < TIMES[0] ? 0 : (cur < TIMES[0]+TIMES[1] ? 1 : 2);
// 昼夜交代時の掃除システム:ボス以外をリセット
if (mode === 2 && lastMode !== 2) { enemies = enemies.filter(e => e.type !== 'B'); addMessage("NIGHT FALLS...", "white"); }
if (mode === 0 && lastMode === 2) { enemies = enemies.filter(e => e.type !== 'S'); addMessage("MORNING!", "yellow"); }
lastMode = mode;
// 作物の自然発生(5秒に1回)
if (gameTime % 5 === 0) {
let rx = Math.floor(Math.random()*20), ry = Math.floor(Math.random()*20);
if (map[ry][rx] === '') map[ry][rx] = Math.random() < 0.1 ? '🍈' : (Math.random() < 0.4 ? '🍇' : '🥔');
}
// 敵の移動ロジック
enemies.forEach(e => {
let speed = (e.type === 'RED' || e.type === 'KING' || e.type === 'S') ? 0.9 : 0.6;
if(Math.random() > speed) return; // スピード調整
let path = findPath({x: e.x, y: e.y}, {x: player.x, y: player.y});
if(path && path.parent && (['S','RED','KING'].includes(e.type))) { // 執念深い敵の追尾移動
let next = path; while(next.parent && next.parent.parent) next = next.parent; e.x = next.x; e.y = next.y;
} else { // 通常のランダム移動
let dx = Math.floor(Math.random()*3)-1, dy = Math.floor(Math.random()*3)-1;
let nx = e.x+dx, ny = e.y+dy; if (nx>=0 && nx<20 && ny>=0 && ny<20 && map[ny][nx] !== '🌳') { e.x = nx; e.y = ny; }
}
});
// 敵の自動補充
if (enemies.length < 6) {
let rx, ry; do { rx = Math.floor(Math.random()*20); ry = Math.floor(Math.random()*20); } while (map[ry][rx] !== '' || Math.abs(rx-player.x) < 3);
enemies.push({ x: rx, y: ry, type: mode === 2 ? 'S' : 'B', emoji: mode === 2 ? '💀' : '🔵', hp: mode === 2 ? 3 : 1, exp: mode === 2 ? 5 : 1 });
}
// ダメージ判定:プレイヤーと同じマスに敵がいたらHPマイナス
enemies.forEach(e => { if (e.x === player.x && e.y === player.y) { hp--; if (hp <= 0) { isRunning = false; document.getElementById('final-score').innerText = score; gameOverScreen.style.display = 'block'; } } });
setTimeout(gameLoop, 800); // 0.8秒ごとにループ
}
// --- 9. 入力・開始イベント ---
startBtn.onclick = () => { initAudio(); startScreen.style.display = 'none'; isRunning = true; draw(); gameLoop(); };
window.onkeydown = function(e) {
if(!isRunning) return;
if(e.key==="ArrowUp")move(0,-1); if(e.key==="ArrowDown")move(0,1);
if(e.key==="ArrowLeft")move(-1,0); if(e.key==="ArrowRight")move(1,0);
if(e.key.toLowerCase()==="z")attack();
if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.key))e.preventDefault(); // 画面スクロール防止
};
})();
</script>
0 件のコメント:
コメントを投稿
I'm sorry, Japanese text only.
荒らし目的と思われるコメントは気づき次第対処します。