junkerstock
 ccc0.0.24 

<!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 (Shop Modal & Tap)</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: 4px;
background: rgba(0, 0, 0, 0.3);
padding: 6px 0;
}
.score-row {
display: flex;
justify-content: center;
align-items: center; /* ★重要: 垂直方向の中央揃え */
gap: 15px;
width: 100%;
height: 24px; /* 行の高さを固定してズレを防ぐ */
}
.stat-item {
white-space: nowrap;
font-size: 14px;
display: flex;
align-items: center; /* 項目内も中央揃え */
line-height: 1;
}

.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: 4px; font-size: 16px; padding-bottom: 2px; }

/* コンパスの文字調整 */
#compassVal {
color: #ff9900;
font-weight: 900;
font-size: 18px;
letter-spacing: -2px;
display: inline-block;
margin-top: -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;
}

/* ★変更: Tap Home メッセージ */
#tapHomeMsg {
display: none;
position: absolute;
top: 55%; /* 家(50%)の少し下 */
left: 50%;
transform: translate(-50%, 0);
color: #fff;
background-color: rgba(0,0,0,0.6);
padding: 5px 10px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
pointer-events: none; /* クリックを透過させる */
z-index: 25;
border: 1px solid #fff;
animation: blink 1.5s infinite;
}
@keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }

/* ★変更: ショップメニュー(画面中央モーダル) */
#shopMenu {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* 完全中央配置 */
z-index: 50;
background: rgba(10, 10, 10, 0.95);
padding: 20px;
border: 2px solid #ffd700;
border-radius: 12px;
text-align: center;
flex-direction: column;
gap: 12px;
box-shadow: 0 0 50px rgba(0,0,0,0.8);
min-width: 280px;
}
.shop-btn {
background: #333; color: white;
border: 1px solid #555; padding: 12px 15px;
font-family: monospace; font-weight: bold; font-size: 14px;
cursor: pointer; border-radius: 4px; width: 100%;
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-close { border-color: #666; color: #fff; background: #444; margin-top: 10px; }
.btn-active-mode { background: #ffcc00 !important; color: black !important; }

#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: 60; 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;
}
</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="tapHomeMsg">Tap Home</div>

<div id="shopMenu">
<div style="color:#aaa; font-size:14px; margin-bottom:5px; border-bottom:1px solid #444; padding-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>
<button id="closeShopBtn" class="shop-btn btn-close" onclick="closeShop()">閉じる</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 isShopOpen = 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");

// キャンバスをクリックした時の処理(家タップ判定)
canvas.addEventListener("click", handleCanvasClick);

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;

closeShop(); // リセット時は閉じる

applyConfig('huge');
updateUI();
updateCompass();
updateStatus(`STAGE ${currentStageId}<br>家で待機中...`);

document.getElementById("gameOverScreen").style.display = "none";
document.getElementById("statusMessage").style.visibility = "visible";
document.getElementById("tapHomeMsg").style.display = "block"; // 最初は表示

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 handleCanvasClick(evt) {
if (!isResting) return;
// 家にいる時のみ
if (px !== currentHome.x || py !== currentHome.y) return;

// キャンバスの中心付近をクリックしたか簡易判定
// (isRestingで家にいるなら、家は常に画面中央)
const rect = canvas.getBoundingClientRect();
const clickX = evt.clientX - rect.left;
const clickY = evt.clientY - rect.top;

const centerX = rect.width / 2;
const centerY = rect.height / 2;
// 半径50px以内なら家とみなす
const dist = Math.sqrt(Math.pow(clickX - centerX, 2) + Math.pow(clickY - centerY, 2));

if (dist < 100) { // 少し広めに判定
openShop();
}
}

// ★新規: ショップ開閉
function openShop() {
isShopOpen = true;
document.getElementById("shopMenu").style.display = "flex";
document.getElementById("tapHomeMsg").style.display = "none";
updateStatus("ショップを開いています");
}
function closeShop() {
isShopOpen = false;
document.getElementById("shopMenu").style.display = "none";
// 家にいるならTap Homeを表示
if (px === currentHome.x && py === currentHome.y && isResting) {
document.getElementById("tapHomeMsg").style.display = "block";
updateStatus(`STAGE ${currentStageId}<br>買い物 または 出発`);
}
}

function game() {
if (hitStopActive) return;

if (!isResting) {
// ショップが開いていたら動かない(念の為)
if (isShopOpen) return;

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>買い物 または 出発`);
}

// ★修正: 自動でショップを開かず、Tap Homeを表示
if (!isShopOpen) {
document.getElementById("tapHomeMsg").style.display = "block";
}
}
}
} else {
// 家の外
document.getElementById("shopMenu").style.display = "none";
document.getElementById("tapHomeMsg").style.display = "none";
deliverySequence = 1;

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";
document.getElementById("tapHomeMsg").style.display = "none";
}

function keyPush(evt) {
if (isGameOver) return;

// ★修正: ショップが開いている間は移動させない
if (isShopOpen) 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 (isShopOpen) 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;

// タップ(移動なし)の場合はhandleCanvasClickで処理されるので何もしない
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
centerX
centerY
cfg
charset
class
clickX
clickY
closeShop -------( Function )
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
dist
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
handleCanvasClick -------( Function )
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 )
isShopOpen
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
openShop -------( Function )
ox
oy
parseStageData -------( Function )
parts
pulseOffsetXY
pulseScale
pulseSize
px
py
rect
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

Content-type: text/html error-smemo8

ERROR !

ファイルの差し替えに失敗しました: ./smemo8.log