<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Snake Game (Status Fix)</title>
<style>
body {
background-color: #111;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
color: white;
font-family: 'Courier New', Courier, monospace;
overflow: hidden;
touch-action: none; user-select: none; -webkit-user-select: none;
}
#scoreBoard {
width: 100%;
max-width: 600px;
margin-bottom: 5px;
z-index: 20;
text-shadow: 1px 1px 0 #000;
pointer-events: none;
color: #fff;
font-weight: bold;
display: flex;
flex-direction: column;
gap: 2px;
background: rgba(0, 0, 0, 0.3);
padding: 4px 0;
}
.score-row {
display: flex;
justify-content: center;
gap: 15px;
width: 100%;
}
.stat-item { white-space: nowrap; font-size: 14px; }
.label-hi { color: #ff4444; }
.label-score { color: #d87093; }
.label-gold { color: #ffd700; }
.label-garde { color: #00ffcc; }
.label-stage { color: #aaa; }
.stat-val { color: white; margin-left: 2px; font-size: 15px; }
#compassVal {
color: #ff9900; font-weight: 900; font-size: 18px; letter-spacing: -2px;
}
canvas {
border: 4px solid #555;
background-color: #0a0a0a;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
border-radius: 8px;
max-width: 98vw;
max-height: 75vh;
object-fit: contain;
z-index: 10;
}
#gameOverScreen {
display: none;
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background-color: rgba(0, 0, 0, 0.95);
padding: 30px; border: 2px solid #d87093;
z-index: 30; min-width: 240px; border-radius: 10px;
}
#replayBtn {
padding: 15px 20px; font-size: 20px; cursor: pointer;
background-color: #ffd700; border: none; width: 100%;
border-radius: 5px; font-weight: bold; margin-top: 20px; color: #000;
}
#statusMessage {
position: absolute; bottom: 5%;
color: #fff; font-size: 18px; font-weight: bold;
text-align: center; width: 100%;
text-shadow: 2px 2px 4px #000;
z-index: 20; pointer-events: none;
}
#shopMenu {
display: none;
position: absolute; bottom: 12%; z-index: 40;
background: rgba(0, 0, 0, 0.9); padding: 15px;
border: 2px solid #ffd700; border-radius: 8px;
text-align: center;
display: flex; flex-direction: column; gap: 8px;
}
.shop-btn {
background: #333; color: white;
border: 1px solid #555; padding: 10px 15px;
font-family: monospace; font-weight: bold; font-size: 14px;
cursor: pointer; border-radius: 4px; width: 260px;
transition: all 0.1s;
}
.shop-btn:active { transform: scale(0.98); }
.shop-btn:disabled { background: #222; color: #555; border-color: #333; cursor: not-allowed; }
.btn-garde { border-color: #00ffcc; color: #00ffcc; }
.btn-mode { border-color: #ffcc00; color: #ffcc00; }
.btn-active-mode { background: #ffcc00 !important; color: black !important; }
</style>
</head>
<body>
<div id="scoreBoard">
<div class="score-row">
<div class="stat-item"><span class="label-hi">HI-SCORE:</span><span id="hiScoreVal" class="stat-val">0</span></div>
<div class="stat-item"><span class="label-score">SCORE:</span><span id="totalScoreVal" class="stat-val">0</span></div>
<div class="stat-item"><span class="label-stage">STAGE:</span><span id="stageVal" class="stat-val">1</span></div>
</div>
<div class="score-row">
<div class="stat-item"><span class="label-gold">GOLD:</span><span id="goldVal" class="stat-val">0</span></div>
<div class="stat-item"><span class="label-garde">GARDE:</span><span id="gardeVal" class="stat-val">0</span></div>
<div class="stat-item"><span class="label-home">HOME:</span><span id="compassVal" class="stat-val">🏠</span></div>
</div>
</div>
<canvas id="gameCanvas"></canvas>
<div id="shopMenu">
<div style="color:#aaa; font-size:12px; margin-bottom:5px;">SHOP & STATUS</div>
<button id="buyGardeBtn" class="shop-btn btn-garde" onclick="buyGarde()">GARDE + : 100 G</button>
<button id="buyWide1Btn" class="shop-btn btn-mode" onclick="buyMode('medium')">WIDE+1 : 50 G</button>
<button id="buyWide2Btn" class="shop-btn btn-mode" onclick="buyMode('small')">WIDE+2 : 70 G</button>
</div>
<div id="statusMessage">ロード中...</div>
<div id="gameOverScreen">
<h2 style="color:red; margin:0 0 10px 0;">GAME OVER</h2>
<p style="font-size:14px; color:#ccc; margin:0;">FINAL SCORE</p>
<p style="font-size:32px; font-weight:bold; color:#d87093; margin:10px 0;"><span id="finalTotalScore">0</span></p>
<button id="replayBtn">REPLAY</button>
</div>
<script>
const STAGE_DATA_TEXT = `
stage1,60,60,
home,30,30,
gate,56,56,2,
kabe,5,50,59,50,
toge,5,45,59,49,
nasi,1,40,58,58
stage2,60,60,
home,30,30,
gate,56,56,3,
gate,56,3,1,
kabe,5,50,58,50,
toge,5,45,58,48,
nasi,1,40,58,58
stage3,60,60,
home,10,10,
gate,50,50,4,
gate,10,50,2,
kabe,20,0,20,40,
toge,40,20,40,60,
nasi,0,0,20,20
stage4,60,60,
home,30,30,
gate,58,30,5,
gate,2,30,3,
toge,10,10,50,10,
toge,10,50,50,50,
stage5,60,60,
home,30,30,
gate,30,2,6,
gate,30,58,4,
kabe,15,15,45,15,
kabe,15,45,45,45,
stage6,60,60,
home,5,5,
gate,55,55,7,
gate,55,5,5,
toge,0,30,60,30,
nasi,0,0,10,10
stage7,60,60,
home,30,30,
gate,5,30,8,
gate,55,30,6,
kabe,28,28,32,28,
kabe,28,32,32,32,
kabe,28,28,28,32,
kabe,32,28,32,32,
stage8,60,60,
home,30,30,
gate,30,5,9,
gate,30,55,7,
toge,10,10,50,50,
nasi,25,25,35,35
stage9,60,60,
home,2,2,
gate,58,58,10,
gate,2,58,8,
kabe,10,0,10,50,
kabe,20,10,20,60,
kabe,30,0,30,50,
kabe,40,10,40,60,
kabe,50,0,50,50,
stage10,60,60,
home,30,30,
gate,30,5,1,
toge,0,0,60,2,
toge,0,58,60,60,
toge,0,0,2,60,
toge,58,0,60,60,
nasi,20,20,40,40
`;
let canvas, ctx;
let gameInterval;
let stagesConfig = {};
let stagesState = {};
let currentStageId = 1;
const levels = {
small: { gs: 20, viewTiles: 35 },
medium: { gs: 40, viewTiles: 19 },
huge: { gs: 80, viewTiles: 9 }
};
const COST_GARDE = 100;
const COST_WIDE1 = 50;
const COST_WIDE2 = 70;
let currentConfigKey = 'huge';
let gs, viewTiles;
let targetFoodCount = 30;
const assets = {
head: { src: 'head.png', img: new Image(), loaded: false, fallback: null },
body: { src: 'body.png', img: new Image(), loaded: false, fallback: null },
food: { src: 'food.png', img: new Image(), loaded: false, fallback: null },
obstacle: { src: 'obstacle.png', img: new Image(), loaded: false, fallback: null },
house: { src: 'house.png', img: new Image(), loaded: false, fallback: null },
wall: { src: 'wall.png', img: new Image(), loaded: false, fallback: null },
gate: { src: 'gate.png', img: new Image(), loaded: false, fallback: null }
};
const baseSize = 80;
function parseStageData(text) {
const lines = text.split('\n');
const data = {};
let currentId = 0;
let currentObj = null;
lines.forEach(line => {
const parts = line.split(',').map(s => s.trim()).filter(s => s !== "");
if (parts.length === 0) return;
const type = parts[0].toLowerCase();
if (type.startsWith('stage')) {
let numStr = type.replace('stage', '');
let num = parseInt(numStr);
if (!isNaN(num)) { currentId = num; } else { currentId++; }
currentObj = {
width: parseInt(parts[1]),
height: parseInt(parts[2]),
home: {x:0, y:0},
gates: [],
staticWalls: [],
staticToges: [],
nasiZones: []
};
data[currentId] = currentObj;
} else if (currentObj) {
if (type === 'home') {
currentObj.home = { x: parseInt(parts[1]), y: parseInt(parts[2]) };
} else if (type === 'gate') {
currentObj.gates.push({ x: parseInt(parts[1]), y: parseInt(parts[2]), target: parseInt(parts[3]) });
} else if (type === 'kabe') {
let x1 = parseInt(parts[1]), y1 = parseInt(parts[2]);
let x2 = parseInt(parts[3]), y2 = parseInt(parts[4]);
for(let x = Math.min(x1,x2); x <= Math.max(x1,x2); x++) {
for(let y = Math.min(y1,y2); y <= Math.max(y1,y2); y++) {
currentObj.staticWalls.push({x, y});
}
}
} else if (type === 'toge') {
let x1 = parseInt(parts[1]), y1 = parseInt(parts[2]);
let x2 = parseInt(parts[3]), y2 = parseInt(parts[4]);
for(let x = Math.min(x1,x2); x <= Math.max(x1,x2); x++) {
for(let y = Math.min(y1,y2); y <= Math.max(y1,y2); y++) {
currentObj.staticToges.push({x, y});
}
}
} else if (type === 'nasi') {
currentObj.nasiZones.push({
x1: Math.min(parseInt(parts[1]), parseInt(parts[3])),
y1: Math.min(parseInt(parts[2]), parseInt(parts[4])),
x2: Math.max(parseInt(parts[1]), parseInt(parts[3])),
y2: Math.max(parseInt(parts[2]), parseInt(parts[4]))
});
}
}
});
return data;
}
function createFallbackGraphic(type) {
const c = document.createElement('canvas');
c.width = baseSize; c.height = baseSize;
const x = c.getContext('2d');
const s = baseSize;
if (type === 'head') {
x.fillStyle = "#228B22"; x.fillRect(0, 0, s, s);
x.fillStyle = "white"; x.fillRect(s*0.2, s*0.2, s*0.25, s*0.25); x.fillRect(s*0.55, s*0.2, s*0.25, s*0.25);
x.fillStyle = "black"; x.fillRect(s*0.25, s*0.25, s*0.1, s*0.1); x.fillRect(s*0.6, s*0.25, s*0.1, s*0.1);
}
else if (type === 'body') {
x.fillStyle = "#32CD32"; x.fillRect(0, 0, s, s);
x.strokeStyle = "#228B22"; x.lineWidth = s * 0.05; x.strokeRect(s*0.02, s*0.02, s*0.96, s*0.96);
}
else if (type === 'food') {
x.beginPath(); x.arc(s/2, s/2, s/2 - (s*0.1), 0, Math.PI*2);
x.fillStyle = "#ffd700"; x.fill(); x.lineWidth = s * 0.05; x.strokeStyle = "orange"; x.stroke();
}
else if (type === 'obstacle') {
x.fillStyle = "#cc0000"; x.fillRect(0, 0, s, s);
x.strokeStyle = "#550000"; x.lineWidth = s * 0.08; x.strokeRect(s*0.05, s*0.05, s*0.9, s*0.9);
x.beginPath(); x.strokeStyle = "white"; x.lineWidth = s * 0.08;
x.moveTo(s*0.2, s*0.2); x.lineTo(s*0.8, s*0.8); x.moveTo(s*0.8, s*0.2); x.lineTo(s*0.2, s*0.8); x.stroke();
}
else if (type === 'house') {
x.fillStyle = "#8B4513"; x.fillRect(s*0.1, s*0.3, s*0.8, s*0.6);
x.fillStyle = "#FFD700"; x.beginPath(); x.moveTo(s*0.05, s*0.35); x.lineTo(s*0.5, s*0.05); x.lineTo(s*0.95, s*0.35); x.fill();
x.fillStyle = "#444"; x.fillRect(s*0.4, s*0.6, s*0.2, s*0.3);
}
else if (type === 'wall') {
x.fillStyle = "#555"; x.fillRect(0, 0, s, s);
x.fillStyle = "#777"; x.fillRect(0, 0, s, s*0.1); x.fillRect(0, 0, s*0.1, s);
x.fillStyle = "#333"; x.fillRect(0, s*0.9, s, s*0.1); x.fillRect(s*0.9, 0, s*0.1, s);
x.strokeStyle = "#222"; x.strokeRect(s*0.2, s*0.2, s*0.6, s*0.6);
}
else if (type === 'gate') {
x.fillStyle = "#000033"; x.fillRect(0,0,s,s);
x.strokeStyle = "#00ccff"; x.lineWidth = s*0.1;
x.beginPath(); x.arc(s/2, s, s*0.4, Math.PI, 0); x.stroke();
x.fillStyle = "#00ccff"; x.font="bold "+(s*0.5)+"px monospace"; x.textAlign="center"; x.fillText("G", s/2, s*0.8);
}
return c;
}
function initAssets() {
Object.keys(assets).forEach(type => {
assets[type].fallback = createFallbackGraphic(type);
assets[type].img.onload = () => { assets[type].loaded = true; };
assets[type].img.src = assets[type].src;
});
}
function getAsset(type) { return assets[type].loaded ? assets[type].img : assets[type].fallback; }
let px, py;
let xv, yv;
let trail;
let tail;
let gold;
let totalScore;
let hiScore = 0;
let garde;
let deliverySequence = 1;
let isGameOver = false;
let isResting = true;
let animFrame = 0;
let touchStartX = 0; touchStartY = 0;
let hitStopActive = false;
let currentWalls = [];
let currentStaticToges = [];
let currentDynamicToges = [];
let currentFoods = [];
let currentGates = [];
let currentHome = {x:0, y:0};
let currentStageWidth, currentStageHeight;
window.onload = function() {
canvas = document.getElementById("gameCanvas");
ctx = canvas.getContext("2d");
let savedHi = localStorage.getItem('snakeHiScore');
if(savedHi) hiScore = parseInt(savedHi);
document.getElementById("hiScoreVal").innerText = hiScore;
initAssets();
stagesConfig = parseStageData(STAGE_DATA_TEXT);
document.addEventListener("keydown", keyPush);
document.addEventListener("touchstart", handleTouchStart, {passive: false});
document.addEventListener("touchend", handleTouchEnd, {passive: false});
document.getElementById("replayBtn").addEventListener("click", () => resetGame(true));
resetGame(true);
}
function applyConfig(key) {
currentConfigKey = key;
const cfg = levels[key];
gs = cfg.gs;
viewTiles = cfg.viewTiles;
canvas.width = viewTiles * gs;
canvas.height = viewTiles * gs;
updateUI();
}
function resetGame(fullReset = false) {
if (fullReset) {
stagesState = {};
currentStageId = 1;
gold = 0;
totalScore = 0;
garde = 0;
}
loadStage(currentStageId);
xv = 0; yv = 0;
trail = [];
tail = 0;
deliverySequence = 1;
isGameOver = false;
isResting = true;
animFrame = 0;
hitStopActive = false;
applyConfig('huge');
updateUI();
updateCompass();
updateStatus(`STAGE ${currentStageId}<br>家で待機中...`);
document.getElementById("gameOverScreen").style.display = "none";
document.getElementById("statusMessage").style.visibility = "visible";
document.getElementById("shopMenu").style.display = "flex";
clearInterval(gameInterval);
gameInterval = setInterval(game, 1000 / 6);
}
function loadStage(stageId) {
if (stagesConfig[currentStageId] && stagesState[currentStageId]) {
stagesState[currentStageId].dynamicToges = [...currentDynamicToges];
stagesState[currentStageId].foods = [...currentFoods];
}
currentStageId = stageId;
const cfg = stagesConfig[stageId];
if (!cfg) { console.error("Stage Data Not Found!"); return; }
currentStageWidth = cfg.width;
currentStageHeight = cfg.height;
currentHome = cfg.home;
currentGates = cfg.gates;
px = currentHome.x;
py = currentHome.y;
trail = [];
currentWalls = [];
for (let x = 0; x < currentStageWidth; x++) {
currentWalls.push({x: x, y: 0});
currentWalls.push({x: x, y: currentStageHeight - 1});
}
for (let y = 1; y < currentStageHeight - 1; y++) {
currentWalls.push({x: 0, y: y});
currentWalls.push({x: currentStageWidth - 1, y: y});
}
cfg.staticWalls.forEach(w => currentWalls.push(w));
currentStaticToges = cfg.staticToges;
if (!stagesState[stageId]) {
stagesState[stageId] = {
dynamicToges: [],
foods: []
};
currentDynamicToges = [];
currentFoods = [];
for(let i=0; i<targetFoodCount; i++) spawnFood();
} else {
currentDynamicToges = [...stagesState[stageId].dynamicToges];
currentFoods = [...stagesState[stageId].foods];
}
}
function isPositionInView(x, y) {
let halfView = Math.ceil(viewTiles / 2);
let minX = px - halfView;
let maxX = px + halfView;
let minY = py - halfView;
let maxY = py + halfView;
return (x >= minX && x <= maxX && y >= minY && y <= maxY);
}
function isSafeZone(x, y, forObstacle) {
const cfg = stagesConfig[currentStageId];
for(let z of cfg.nasiZones) {
if (x >= z.x1 && x <= z.x2 && y >= z.y1 && y <= z.y2) return true;
}
if (Math.abs(x - currentHome.x) <= 3 && Math.abs(y - currentHome.y) <= 3) return true;
for(let g of currentGates) {
if (Math.abs(x - g.x) <= 3 && Math.abs(y - g.y) <= 3) return true;
}
if (forObstacle && isPositionInView(x, y)) {
return true;
}
return false;
}
function isOccupied(x, y) {
if (x === currentHome.x && y === currentHome.y) return true;
if (isPositionOnSnake(x, y)) return true;
if (x === px && y === py) return true;
for (let f of currentFoods) if (f.x === x && f.y === y) return true;
for (let t of currentDynamicToges) if (t.x === x && t.y === y) return true;
for (let t of currentStaticToges) if (t.x === x && t.y === y) return true;
for (let w of currentWalls) if (w.x === x && w.y === y) return true;
for (let g of currentGates) if (g.x === x && g.y === y) return true;
return false;
}
function spawnFood() {
let fx, fy; let attempt = 0;
do {
fx = Math.floor(Math.random() * currentStageWidth);
fy = Math.floor(Math.random() * currentStageHeight);
attempt++; if (attempt > 1000) break;
} while(isOccupied(fx, fy) || isSafeZone(fx, fy, false));
currentFoods.push({x: fx, y: fy});
}
function spawnObstacle() {
let ox, oy; let attempt = 0;
do {
ox = Math.floor(Math.random() * currentStageWidth);
oy = Math.floor(Math.random() * currentStageHeight);
attempt++; if (attempt > 1000) return;
} while(isOccupied(ox, oy) || isSafeZone(ox, oy, true));
currentDynamicToges.push({x: ox, y: oy});
}
function addScore(amount) {
gold += amount;
totalScore += amount;
if(totalScore > hiScore) {
hiScore = totalScore;
localStorage.setItem('snakeHiScore', hiScore);
}
}
function updateCompass() {
const el = document.getElementById("compassVal");
if (px === currentHome.x && py === currentHome.y) {
el.innerText = "🏠"; return;
}
let hStr = "", vStr = "";
if (px < currentHome.x) hStr = "→"; else if (px > currentHome.x) hStr = "←";
if (py < currentHome.y) vStr = "↓"; else if (py > currentHome.y) vStr = "↑";
if (px === currentHome.x) el.innerText = vStr + vStr;
else if (py === currentHome.y) el.innerText = hStr + hStr;
else el.innerText = hStr + vStr;
}
function getGateAt(x, y) {
for (let g of currentGates) {
if (g.x === x && g.y === y) return g;
}
return null;
}
function game() {
if (hitStopActive) return;
if (!isResting) {
let startNode = getGateAt(px, py) || (px === currentHome.x && py === currentHome.y);
if (!startNode) {
trail.push({ x: px, y: py });
}
px += xv; py += yv;
while (trail.length > tail) { trail.shift(); }
let gateObj = getGateAt(px, py);
if ((px === currentHome.x && py === currentHome.y) || gateObj) {
xv = 0; yv = 0;
}
} else {
xv = 0; yv = 0;
}
updateCompass();
if (px < 0 || px >= currentStageWidth || py < 0 || py >= currentStageHeight) { gameOver(); return; }
for (let w of currentWalls) {
if (px === w.x && py === w.y) { gameOver(); return; }
}
let gateAtPos = getGateAt(px, py);
let isAtHome = (px === currentHome.x && py === currentHome.y);
if (isAtHome || gateAtPos) {
if (trail.length > 0) {
addScore(deliverySequence);
updateUI();
if (gateAtPos) {
updateStatus(`GATE OPENING... (+${deliverySequence} G!)`);
} else {
updateStatus(`納品中... (+${deliverySequence} G!)`);
}
deliverySequence++;
trail.shift();
} else {
if (gateAtPos) {
updateStatus(`GATE TRAVEL -> STAGE ${gateAtPos.target}`);
loadStage(gateAtPos.target);
isResting = true;
deliverySequence = 1;
tail = 0;
applyConfig('huge');
updateUI();
return;
} else {
if (currentConfigKey !== 'huge' && !isResting) {
applyConfig('huge');
}
deliverySequence = 1;
if (!isResting) {
isResting = true;
tail = 0;
updateStatus(`STAGE ${currentStageId}<br>買い物 または 出発`);
}
document.getElementById("shopMenu").style.display = "flex";
}
}
} else {
document.getElementById("shopMenu").style.display = "none";
deliverySequence = 1;
// ★修正: "STAGE"を含んでいても、"待機中"を含んでいなければ探索中とみなして更新する
// もしくはシンプルに「GARDE」と「OPENING」以外なら常時更新
const currentStatus = document.getElementById("statusMessage").innerHTML;
if(!currentStatus.includes("GARDE") && !currentStatus.includes("OPENING")) {
const newMsg = `STAGE ${currentStageId} 探索中...`;
if (currentStatus !== newMsg) updateStatus(newMsg);
}
}
if (!isResting && !(isAtHome || gateAtPos)) {
let hitObstacle = false;
for (let i = 0; i < currentDynamicToges.length; i++) {
if (px === currentDynamicToges[i].x && py === currentDynamicToges[i].y) {
handleDamage();
if(!isGameOver) {
currentDynamicToges.splice(i, 1);
hitStopActive = true; setTimeout(() => { hitStopActive = false; }, 150);
}
hitObstacle = true; break;
}
}
if(!hitObstacle) {
for (let t of currentStaticToges) {
if (px === t.x && py === t.y) {
handleDamage();
if(!isGameOver) {
hitStopActive = true; setTimeout(() => { hitStopActive = false; }, 150);
}
hitObstacle = true; break;
}
}
}
for (let i = 0; i < currentFoods.length; i++) {
if (px === currentFoods[i].x && py === currentFoods[i].y) {
tail++;
addScore(1);
updateUI();
currentFoods.splice(i, 1);
spawnFood();
spawnObstacle();
spawnObstacle();
break;
}
}
}
if (isGameOver) return;
// --- 描画 ---
ctx.fillStyle = "#222"; ctx.fillRect(0, 0, canvas.width, canvas.height);
const centerTileIndex = Math.floor(viewTiles / 2);
const offsetX = centerTileIndex - px;
const offsetY = centerTileIndex - py;
ctx.fillStyle = "#0a0a0a";
ctx.fillRect(offsetX * gs, offsetY * gs, currentStageWidth * gs, currentStageHeight * gs);
ctx.strokeStyle = "#333"; ctx.lineWidth = 1; ctx.beginPath();
let startX = Math.floor(-offsetX) - 1; let endX = startX + viewTiles + 2;
let startY = Math.floor(-offsetY) - 1; let endY = startY + viewTiles + 2;
for (let x = startX; x <= endX; x++) {
if(x >= 0 && x <= currentStageWidth) {
let drawX = (x + offsetX) * gs;
ctx.moveTo(drawX, (0 + offsetY) * gs); ctx.lineTo(drawX, (currentStageHeight + offsetY) * gs);
}
}
for (let y = startY; y <= endY; y++) {
if(y >= 0 && y <= currentStageHeight) {
let drawY = (y + offsetY) * gs;
ctx.moveTo((0 + offsetX) * gs, drawY); ctx.lineTo((currentStageWidth + offsetX) * gs, drawY);
}
}
ctx.stroke();
function drawObj(list, img) {
for (let o of list) {
let dx = (o.x + offsetX) * gs;
let dy = (o.y + offsetY) * gs;
if (dx >= -gs && dx <= canvas.width && dy >= -gs && dy <= canvas.height) {
ctx.drawImage(img, dx, dy, gs, gs);
}
}
}
drawObj(currentWalls, getAsset('wall'));
const gateImg = getAsset('gate');
for(let g of currentGates) {
let dx = (g.x + offsetX) * gs;
let dy = (g.y + offsetY) * gs;
if (dx >= -gs && dx <= canvas.width && dy >= -gs && dy <= canvas.height) {
ctx.drawImage(gateImg, dx, dy, gs, gs);
ctx.fillStyle = "#fff"; ctx.font="bold "+(gs*0.4)+"px monospace"; ctx.textAlign="center";
ctx.fillText(g.target, dx + gs/2, dy + gs*0.6);
}
}
animFrame++;
const pulseScale = 1 + 0.05 * Math.sin(animFrame * 0.2);
const pulseSize = gs * pulseScale;
const pulseOffsetXY = (gs - pulseSize) / 2;
const obsImg = getAsset('obstacle');
for (let o of currentDynamicToges) {
let dx = (o.x + offsetX) * gs;
let dy = (o.y + offsetY) * gs;
if (dx >= -gs && dx <= canvas.width && dy >= -gs && dy <= canvas.height) {
ctx.drawImage(obsImg, dx + pulseOffsetXY, dy + pulseOffsetXY, pulseSize, pulseSize);
}
}
for (let o of currentStaticToges) {
let dx = (o.x + offsetX) * gs;
let dy = (o.y + offsetY) * gs;
if (dx >= -gs && dx <= canvas.width && dy >= -gs && dy <= canvas.height) {
ctx.drawImage(obsImg, dx + pulseOffsetXY, dy + pulseOffsetXY, pulseSize, pulseSize);
}
}
drawObj(currentFoods, getAsset('food'));
const bodyImg = getAsset('body');
for (var i = 0; i < trail.length; i++) {
let drawTx = (trail[i].x + offsetX) * gs;
let drawTy = (trail[i].y + offsetY) * gs;
if (drawTx >= -gs && drawTx <= canvas.width && drawTy >= -gs && drawTy <= canvas.height) {
ctx.drawImage(bodyImg, drawTx, drawTy, gs, gs);
}
if (i < trail.length - 1) {
if (!isResting && !(isAtHome || gateAtPos) && trail[i].x == px && trail[i].y == py) {
gameOver(); return;
}
}
}
ctx.drawImage(getAsset('head'), (px + offsetX) * gs, (py + offsetY) * gs, gs, gs);
let drawHomeX = (currentHome.x + offsetX) * gs;
let drawHomeY = (currentHome.y + offsetY) * gs;
if (drawHomeX >= -gs && drawHomeX <= canvas.width && drawHomeY >= -gs && drawHomeY <= canvas.height) {
ctx.drawImage(getAsset('house'), drawHomeX, drawHomeY, gs, gs);
}
}
function handleDamage() {
if (garde > 0) {
garde--;
updateUI();
updateStatus("<b>GARDE発動!</b>");
} else {
gameOver();
}
}
function isPositionOnSnake(x, y) {
if (!trail) return false;
for (let i = 0; i < trail.length; i++) { if (trail[i].x === x && trail[i].y === y) return true; }
return false;
}
function updateUI() {
document.getElementById("hiScoreVal").innerText = hiScore;
document.getElementById("totalScoreVal").innerText = totalScore;
document.getElementById("goldVal").innerText = gold;
document.getElementById("gardeVal").innerText = garde;
document.getElementById("stageVal").innerText = currentStageId;
const btnGarde = document.getElementById("buyGardeBtn");
if (gold >= COST_GARDE) {
btnGarde.disabled = false; btnGarde.innerText = `GARDE + (${COST_GARDE} G)`;
} else {
btnGarde.disabled = true; btnGarde.innerText = `GARDE + (不足 ${COST_GARDE} G)`;
}
const btnWide1 = document.getElementById("buyWide1Btn");
if(currentConfigKey === 'medium') {
btnWide1.innerText = "WIDE+1 (装着中)"; btnWide1.classList.add('btn-active-mode'); btnWide1.disabled = false;
} else if (gold >= COST_WIDE1) {
btnWide1.innerText = `WIDE+1 : ${COST_WIDE1} G`; btnWide1.classList.remove('btn-active-mode'); btnWide1.disabled = false;
} else {
btnWide1.innerText = `WIDE+1 (不足 ${COST_WIDE1} G)`; btnWide1.classList.remove('btn-active-mode'); btnWide1.disabled = true;
}
const btnWide2 = document.getElementById("buyWide2Btn");
if(currentConfigKey === 'small') {
btnWide2.innerText = "WIDE+2 (装着中)"; btnWide2.classList.add('btn-active-mode'); btnWide2.disabled = false;
} else if (gold >= COST_WIDE2) {
btnWide2.innerText = `WIDE+2 : ${COST_WIDE2} G`; btnWide2.classList.remove('btn-active-mode'); btnWide2.disabled = false;
} else {
btnWide2.innerText = `WIDE+2 (不足 ${COST_WIDE2} G)`; btnWide2.classList.remove('btn-active-mode'); btnWide2.disabled = true;
}
}
function updateStatus(msg) {
const el = document.getElementById("statusMessage");
if(el) el.innerHTML = msg;
}
function buyGarde() { if (gold >= COST_GARDE) { gold -= COST_GARDE; garde++; updateUI(); updateStatus(`強化完了! GARDE: ${garde}`); } }
function buyMode(modeKey) {
let cost = (modeKey === 'medium') ? COST_WIDE1 : COST_WIDE2;
if (currentConfigKey === modeKey) { updateStatus("既にこのモードです"); return; }
if (gold >= cost) { gold -= cost; applyConfig(modeKey); updateUI(); updateStatus(`視界拡張完了! (${modeKey.toUpperCase()})`); }
}
function gameOver() {
isGameOver = true; clearInterval(gameInterval);
document.getElementById("finalTotalScore").innerText = totalScore;
document.getElementById("gameOverScreen").style.display = "block";
document.getElementById("statusMessage").style.visibility = "hidden";
document.getElementById("shopMenu").style.display = "none";
}
function keyPush(evt) {
if (isGameOver) return;
if (isResting) {
const k = evt.keyCode;
if (k >= 37 && k <= 40) isResting = false;
else return;
}
switch (evt.keyCode) {
case 37: if (xv !== 1) { xv = -1; yv = 0; } break;
case 38: if (yv !== 1) { xv = 0; yv = -1; } break;
case 39: if (xv !== -1) { xv = 1; yv = 0; } break;
case 40: if (yv !== -1) { xv = 0; yv = 1; } break;
}
}
function handleTouchStart(evt) {
if(evt.target.id === "gameCanvas" || evt.target.tagName === "BODY") evt.preventDefault();
touchStartX = evt.touches[0].clientX; touchStartY = evt.touches[0].clientY;
}
function handleTouchEnd(evt) {
if (isGameOver) return;
if(evt.target.closest('#shopMenu')) return;
if(evt.target.id === "gameCanvas" || evt.target.tagName === "BODY") evt.preventDefault();
const xDiff = touchStartX - evt.changedTouches[0].clientX;
const yDiff = touchStartY - evt.changedTouches[0].clientY;
if (Math.abs(xDiff) < 10 && Math.abs(yDiff) < 10) return;
if (isResting) isResting = false;
if (Math.abs(xDiff) > Math.abs(yDiff)) {
if (xDiff > 0) { if (xv !== 1) { xv = -1; yv = 0; } } else { if (xv !== -1) { xv = 1; yv = 0; } }
} else {
if (yDiff > 0) { if (yv !== 1) { xv = 0; yv = -1; } } else { if (yv !== -1) { xv = 0; yv = 1; } }
}
}
</script>
</body>
</html>
使用変数
| addScore -------( Function ) | |
| animFrame | |
| applyConfig -------( Function ) | |
| assets | |
| attempt | |
| baseSize | |
| bodyImg | |
| btnGarde | |
| btnWide1 | |
| btnWide2 | |
| buyGarde -------( Function ) | |
| buyMode -------( Function ) | |
| c | |
| canvas | |
| centerTileIndex | |
| cfg | |
| charset | |
| class | |
| content | |
| cost | |
| COST_GARDE | |
| COST_WIDE1 | |
| COST_WIDE2 | |
| createFallbackGraphic -------( Function ) | |
| ctx | |
| currentConfigKey | |
| currentFoods | |
| currentGates | |
| currentHome | |
| currentId | |
| currentObj | |
| currentStageHeight | |
| currentStageId | |
| currentStageWidth | |
| currentStaticToges | |
| currentStatus | |
| currentWalls | |
| data | |
| deliverySequence | |
| disabled | |
| display | |
| drawHomeX | |
| drawHomeY | |
| drawObj -------( Function ) | |
| drawTx | |
| drawTy | |
| drawX | |
| drawY | |
| dx | |
| dy | |
| dynamicToges | |
| el | |
| endX | |
| endY | |
| fallback | |
| fillStyle | |
| font | |
| foods | |
| fullReset | |
| fx | |
| fy | |
| game -------( Function ) | |
| gameInterval | |
| gameOver -------( Function ) | |
| garde | |
| gateAtPos | |
| gateImg | |
| gateObj | |
| getAsset -------( Function ) | |
| getGateAt -------( Function ) | |
| gold | |
| gs | |
| halfView | |
| handleDamage -------( Function ) | |
| handleTouchEnd -------( Function ) | |
| handleTouchStart -------( Function ) | |
| height | |
| hiScore | |
| hitObstacle | |
| hitStopActive | |
| home | |
| hStr | |
| i | |
| id | |
| initAssets -------( Function ) | |
| innerHTML | |
| innerText | |
| isAtHome | |
| isGameOver | |
| isOccupied -------( Function ) | |
| isPositionInView -------( Function ) | |
| isPositionOnSnake -------( Function ) | |
| isResting | |
| isSafeZone -------( Function ) | |
| k | |
| keyPush -------( Function ) | |
| lang | |
| length | |
| levels | |
| line | |
| lines | |
| lineWidth | |
| loaded | |
| loadStage -------( Function ) | |
| maxX | |
| maxY | |
| minX | |
| minY | |
| modeKey | |
| name | |
| newMsg | |
| num | |
| numStr | |
| obsImg | |
| offsetX | |
| offsetY | |
| onclick | |
| onload | |
| ox | |
| oy | |
| parseStageData -------( Function ) | |
| parts | |
| pulseOffsetXY | |
| pulseScale | |
| pulseSize | |
| px | |
| py | |
| resetGame -------( Function ) | |
| s | |
| savedHi | |
| scalable | |
| scale | |
| spawnFood -------( Function ) | |
| spawnObstacle -------( Function ) | |
| src | |
| stagesConfig | |
| stagesState | |
| STAGE_DATA_TEXT | |
| startNode | |
| startX | |
| startY | |
| strokeStyle | |
| style | |
| tagName | |
| tail | |
| targetFoodCount | |
| textAlign | |
| totalScore | |
| touchStartX | |
| touchStartY | |
| trail | |
| type | |
| updateCompass -------( Function ) | |
| updateStatus -------( Function ) | |
| updateUI -------( Function ) | |
| urrentDynamicToges | |
| viewTiles | |
| visibility | |
| vStr | |
| w | |
| width | |
| x1 | |
| x2 | |
| x | |
| xDiff | |
| xv | |
| y1 | |
| y2 | |
| y | |
| yDiff | |
| yv |