便便蛇蛇 - Poop Snake
by RocketWolf272728 lines99.4 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>便便蛇蛇 - Poop Snake</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box;user-select:-webkit-user-select:none;-webkit-tap-highlight-color:transparent}
:root{--tile:#d4eef9;--tile2:#c0e3f2;--grout:#a8d4e6;--accent:#ff9eb5;--gold:#ffd700;--blue:#66b3ff;--green:#77dd77;--red:#ff6666}
html,body{width:100%;height:100%;overflow:hidden;font-family:'Fredoka',sans-serif;background:linear-gradient(160deg,#e8f6fc 0%,#d0eaf5 50%,#b8ddee 100%)}
body{display:flex;flex-direction:column;align-items:center;justify-content:flex-start;padding:8px;min-height:100vh}
#hud{width:100%;max-width:500px;display:flex;justify-content:space-between;align-items:center;background:rgba(255,255,255,0.85);backdrop-filter:blur(8px);border-radius:18px;padding:8px 14px;margin-bottom:6px;box-shadow:0 4px 20px rgba(0,0,0,0.08);flex-wrap:wrap;gap:6px}
.hud-item{display:flex;flex-direction:column;align-items:center;font-size:11px;color:#5a7a8a;font-weight:500}
.hud-item .val{font-size:18px;font-weight:700;color:#2d5a72;line-height:1.1}
.hud-item .val.gold{color:#d4a017}
.hud-item .val.pink{color:#d94a6e}
.hud-item .val.green{color:#3a9a3a}
.hud-item .val.blue{color:#3a7ab5}
#comboItem .val{animation:badgePulse 0.6s infinite alternate}
#pauseBtn{background:linear-gradient(135deg,#ff9eb5,#ff7a9e);color:#fff;border:none;border-radius:14px;padding:10px 18px;font-family:inherit;font-weight:600;font-size:14px;cursor:pointer;box-shadow:0 3px 10px rgba(255,122,158,0.4);transition:transform 0.15s}
#pauseBtn:active{transform:scale(0.92)}
#soundBtn{background:linear-gradient(135deg,#77dd77,#55bb55);color:#fff;border:none;border-radius:14px;padding:10px 12px;font-family:inherit;font-weight:600;font-size:14px;cursor:pointer;box-shadow:0 3px 10px rgba(85,187,85,0.4);transition:transform 0.15s}
#soundBtn.muted{background:linear-gradient(135deg,#bbb,#888);box-shadow:0 3px 10px rgba(100,100,100,0.3)}
#soundBtn:active{transform:scale(0.92)}
#shopBtn{background:linear-gradient(135deg,#ffd700,#daa520);color:#fff;border:none;border-radius:14px;padding:10px 12px;font-family:inherit;font-weight:600;font-size:14px;cursor:pointer;box-shadow:0 3px 10px rgba(218,165,32,0.4);transition:transform 0.15s}
#shopBtn:active{transform:scale(0.92)}
.shop-item{display:flex;align-items:center;justify-content:space-between;background:rgba(255,255,255,0.12);border-radius:12px;padding:10px 14px;margin-bottom:8px;gap:10px}
.shop-item-info{display:flex;align-items:center;gap:10px}
.shop-color-preview{width:36px;height:36px;border-radius:8px;border:2px solid rgba(255,255,255,0.4);display:flex;overflow:hidden}
.shop-color-preview span{flex:1}
.shop-item-name{color:#fff;font-weight:600;font-size:14px}
.shop-item-desc{color:#c0dde8;font-size:11px;margin-top:2px}
.shop-buy-btn{background:linear-gradient(135deg,#77dd77,#4caf50);color:#fff;border:none;border-radius:10px;padding:6px 12px;font-family:inherit;font-weight:600;font-size:12px;cursor:pointer;white-space:nowrap}
.shop-buy-btn:disabled{background:#888;cursor:not-allowed;opacity:0.6}
.shop-select-btn{background:linear-gradient(135deg,#66b3ff,#3a7ab5)}
.shop-owned-tag{color:#ffd700;font-size:11px;font-weight:600}
#gameWrap{position:relative;width:100%;max-width:500px;aspect-ratio:16/22;border-radius:20px;overflow:hidden;box-shadow:0 10px 40px rgba(0,0,0,0.15),inset 0 0 0 3px rgba(255,255,255,0.8);background:var(--tile)}
#gameCanvas{display:block;width:100%;height:100%}
#statusBadges{position:absolute;top:8px;left:8px;display:flex;flex-direction:column;gap:5px;z-index:10;pointer-events:none}
.badge{display:flex;align-items:center;gap:4px;background:rgba(255,255,255,0.92);border-radius:12px;padding:4px 10px;font-size:11px;font-weight:600;box-shadow:0 2px 8px rgba(0,0,0,0.1);animation:badgePulse 1s infinite alternate}
.badge.invincible{background:linear-gradient(135deg,#ffe4a0,#ffd700);color:#7a5a00}
.badge.speed{background:linear-gradient(135deg,#a0e8a0,#77dd77);color:#2a6a2a}
.badge.combo{background:linear-gradient(135deg,#ffcce0,#ff9eb5);color:#7a3a5a}
@keyframes badgePulse{from{transform:scale(1)}to{transform:scale(1.05)}}
@keyframes floatUp{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-30px)}}
@keyframes shake{0%,100%{transform:translate(0,0)}25%{transform:translate(-3px,2px)}50%{transform:translate(2px,-3px)}75%{transform:translate(-2px,-2px)}}
@keyframes glow{0%,100%{filter:drop-shadow(0 0 4px rgba(255,215,0,0.6))}50%{filter:drop-shadow(0 0 12px rgba(255,215,0,1))}}
@keyframes tileShift{0%{background-position:0 0}100%{background-position:40px 40px}}
.badge.glow{animation:badgePulse 1s infinite alternate, glow 0.8s ease-in-out infinite}
#mobileControls{display:none}
@media(max-width:600px){
#mobileControls{display:grid;grid-template-columns:repeat(3,64px);grid-template-rows:repeat(3,64px);gap:6px;margin-top:12px}
#hud{padding:6px 10px}
.hud-item .val{font-size:15px}
.hud-item{font-size:10px}
}
.dpad-btn{display:flex;align-items:center;justify-content:center;background:linear-gradient(145deg,#fff,#e0eef5);border-radius:50%;box-shadow:0 5px 15px rgba(0,0,0,0.15),inset 0 -3px 8px rgba(0,0,0,0.08);font-size:26px;color:#3a6a82;font-weight:700;cursor:pointer;transition:all 0.1s;border:none;font-family:inherit}
.dpad-btn:active,.dpad-btn.active{transform:scale(0.9);box-shadow:0 2px 6px rgba(0,0,0,0.15),inset 0 2px 5px rgba(0,0,0,0.1)}
.dpad-up{grid-column:2;grid-row:1}
.dpad-left{grid-column:1;grid-row:2}
.dpad-right{grid-column:3;grid-row:2}
.dpad-down{grid-column:2;grid-row:3}
.dpad-center{grid-column:2;grid-row:2;background:linear-gradient(145deg,#ff9eb5,#ff7a9e);color:#fff;font-size:16px;cursor:default;box-shadow:0 4px 12px rgba(255,122,158,0.4)}
.dpad-center:active{transform:none}
.overlay{position:absolute;inset:0;background:rgba(30,60,80,0.78);backdrop-filter:blur(10px);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:20;padding:20px;border-radius:20px;overflow-y:auto}
.overlay.hidden{display:none}
.mascot{font-size:80px;margin-bottom:10px;filter:drop-shadow(0 4px 8px rgba(0,0,0,0.3));animation:bounce 2s infinite}
@keyframes bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}}
.overlay h1{color:#fff;font-size:32px;font-weight:700;margin-bottom:6px;text-align:center;text-shadow:0 2px 10px rgba(0,0,0,0.3)}
.overlay h2{color:#fff;font-size:24px;font-weight:700;margin-bottom:12px;text-align:center}
.overlay p{color:#d0e8f2;font-size:14px;margin-bottom:12px;text-align:center;max-width:320px;line-height:1.5}
.name-input{background:rgba(255,255,255,0.95);border-radius:14px;padding:12px 18px;border:none;font-family:inherit;font-size:18px;font-weight:600;color:#2d5a72;width:240px;text-align:center;margin-bottom:12px;box-shadow:0 4px 15px rgba(0,0,0,0.15);outline:none}
.name-input:focus{box-shadow:0 4px 20px rgba(255,158,181,0.5)}
.diff-label{color:#fff;font-size:14px;font-weight:600;margin-bottom:8px}
.diff-options{display:flex;gap:6px;flex-wrap:wrap;justify-content:center;margin-bottom:14px;max-width:320px}
.diff-btn{background:rgba(255,255,255,0.85);border:none;border-radius:10px;padding:6px 12px;font-family:inherit;font-weight:600;font-size:13px;color:#3a6a82;cursor:pointer;transition:all 0.15s}
.diff-btn:hover{background:#fff}
.diff-btn.selected{background:linear-gradient(135deg,#ff9eb5,#ff7a9e);color:#fff;transform:scale(1.05)}
.start-btn{background:linear-gradient(135deg,#77dd77,#4caf50);color:#fff;border:none;border-radius:18px;padding:14px 40px;font-family:inherit;font-weight:700;font-size:20px;cursor:pointer;box-shadow:0 6px 20px rgba(76,175,80,0.4);transition:transform 0.15s;margin-top:8px}
.start-btn:active{transform:scale(0.95)}
.legend{display:grid;grid-template-columns:1fr 1fr;gap:6px 16px;background:rgba(255,255,255,0.15);border-radius:14px;padding:10px 16px;margin:10px 0;font-size:12px;color:#e0f0f8}
.legend-item{display:flex;align-items:center;gap:6px}
.legend-color{width:14px;height:14px;border-radius:50%;flex-shrink:0}
.instructions{color:#c0dde8;font-size:12px;margin-top:8px;text-align:center;line-height:1.6}
.score-summary{background:rgba(255,255,255,0.15);border-radius:14px;padding:16px 24px;margin:10px 0;text-align:center}
.score-summary .big{font-size:42px;font-weight:700;color:#fff}
.score-summary .label{font-size:13px;color:#c0dde8;margin-top:4px}
.new-record{color:#ffd700;font-size:18px;font-weight:700;margin-top:6px;text-shadow:0 0 10px rgba(255,215,0,0.6)}
.shake{animation:shake 0.45s}
@keyframes shake{0%,100%{transform:translate(0,0)}10%{transform:translate(-6px,4px)}20%{transform:translate(6px,-3px)}30%{transform:translate(-5px,-5px)}40%{transform:translate(5px,5px)}50%{transform:translate(-4px,3px)}60%{transform:translate(4px,-2px)}70%{transform:translate(-3px,4px)}80%{transform:translate(3px,-3px)}90%{transform:translate(-2px,2px)}}
/* 测试面板样式 */
#testPanel{position:fixed;top:10px;right:10px;z-index:9999;background:rgba(30,60,80,0.92);border-radius:12px;padding:12px;box-shadow:0 4px 20px rgba(0,0,0,0.3);display:none;min-width:180px}
#testPanel.visible{display:block}
#testPanel h3{color:#fff;font-size:13px;font-weight:700;margin:0 0 8px;text-align:center;border-bottom:1px solid rgba(255,255,255,0.2);padding-bottom:6px}
#testPanel .test-btn{display:block;width:100%;margin:4px 0;padding:8px 12px;background:linear-gradient(135deg,#ff9eb5,#ff7a9e);color:#fff;border:none;border-radius:8px;font-family:inherit;font-weight:600;font-size:12px;cursor:pointer;transition:transform 0.15s}
#testPanel .test-btn:hover{transform:scale(1.03)}
#testPanel .test-btn:active{transform:scale(0.97)}
#testPanel .test-btn.green{background:linear-gradient(135deg,#77dd77,#4caf50)}
#testPanel .test-btn.blue{background:linear-gradient(135deg,#66b3ff,#3a7ab5)}
#testPanel .test-btn.orange{background:linear-gradient(135deg,#ffb366,#ff8c1a)}
#testPanel .test-btn.red{background:linear-gradient(135deg,#ff6666,#cc3333)}
#testPanel .test-btn.purple{background:linear-gradient(135deg,#c77dff,#9d4edd)}
#testPanel .hint{color:rgba(255,255,255,0.5);font-size:10px;margin-top:8px;text-align:center;line-height:1.4}
#testToggle{position:fixed;top:10px;right:10px;z-index:9999;background:rgba(30,60,80,0.8);color:#fff;border:none;border-radius:8px;padding:6px 10px;font-size:11px;cursor:pointer;display:none}
#testToggle.visible{display:block}
</style>
</head>
<body>
<div id="hud">
<div class="hud-item"><span>得分</span><span class="val" id="score">0</span></div>
<div class="hud-item"><span>名字</span><span class="val" id="playerName">便便</span></div>
<div class="hud-item"><span>长度</span><span class="val green" id="length">3</span></div>
<div class="hud-item"><span>难度</span><span class="val pink" id="difficulty">低级</span></div>
<div class="hud-item" id="waveItem" style="display:none"><span>波次</span><span class="val blue" id="waveLabel">0</span></div>
<div class="hud-item" id="comboItem" style="display:none"><span>连击</span><span class="val pink" id="comboLabel">0</span></div>
<div class="hud-item"><span>最高</span><span class="val gold" id="highScore">0</span></div>
<div class="hud-item"><span>金币</span><span class="val gold" id="coinsDisplay">0</span></div>
<div class="hud-item" id="shieldItem" style="display:none"><span>护盾</span><span class="val blue" id="shieldDisplay">0</span></div>
<button id="pauseBtn">⏸</button>
<button id="soundBtn" title="声音">🔊</button>
<button id="shopBtn" title="商店">🛒</button>
</div>
<div id="gameWrap">
<canvas id="gameCanvas" width="480" height="660"></canvas>
<div id="statusBadges"></div>
<div id="startOverlay" class="overlay">
<div class="mascot">💩</div>
<h1>便便蛇蛇</h1>
<p>欢迎来到马桶浴室!吃掉便便,避开厕所和敌人蛇蛇!</p>
<input type="text" class="name-input" id="nameInput" maxlength="8" value="便便" placeholder="输入你的名字">
<div style="font-size:12px; font-weight:700; color:var(--blue); margin:8px 0 2px;">控制方式:</div>
<div class="difficulty-select" id="ctrlSelect">
<button class="diff-btn active" data-ctrl="dpad">点按</button>
<button class="diff-btn" data-ctrl="swipe">滑动</button>
</div>
<div class="diff-label">选择难度</div>
<div class="diff-options" id="diffOptions"></div>
<div class="legend">
<div class="legend-item"><span class="legend-color" style="background:#8b4513"></span>普通便便 +10分 +1金币</div>
<div class="legend-item"><span class="legend-color" style="background:#9b59b6"></span>神秘道具 随机效果</div>
<div class="legend-item"><span class="legend-color" style="background:#6b4226;border-radius:0;width:12px;height:14px"></span>马桶/敌蛇 致命</div>
</div>
<div class="instructions">⬅️⬆️⬇️➡️ 方向键 / WASD / 虚拟按键控制<br>空格键暂停 · 移动端使用下方按键</div>
<button class="start-btn" id="startBtn">开始游戏</button>
<button class="start-btn" id="shopBtnStart" style="background:linear-gradient(135deg,#ffd700,#daa520);margin-top:10px">🛒 商店</button>
</div>
<div id="pauseOverlay" class="overlay hidden">
<div style="font-size:70px;margin-bottom:10px">⏸️</div>
<h1>已暂停</h1>
<p>按空格键或点击继续按钮恢复游戏</p>
<button class="start-btn" id="resumeBtn">继续游戏</button>
<button class="start-btn" id="restartBtn" style="background:linear-gradient(135deg,#ff9eb5,#ff7a9e);margin-top:10px">重新开始</button>
</div>
<div id="gameOverOverlay" class="overlay hidden">
<div style="font-size:70px;margin-bottom:10px">💀</div>
<h2>游戏结束</h2>
<div class="score-summary">
<div class="big" id="finalScore">0</div>
<div class="label">最终得分</div>
<div class="new-record hidden" id="newRecord">🎉 新纪录!</div>
</div>
<div style="color:#ffd700;font-size:16px;font-weight:600;margin-bottom:8px" id="coinsEarned">获得 0 金币</div>
<p id="gameOverReason"></p>
<div style="color:#c0dde8;font-size:12px;margin-bottom:12px" id="statsLine"></div>
<button class="start-btn" id="playAgainBtn">再玩一局</button>
<button class="start-btn" id="shopBtnGameOver" style="background:linear-gradient(135deg,#ffd700,#daa520);margin-top:10px">🛒 商店</button>
<button class="start-btn" id="backToMenuBtn" style="background:linear-gradient(135deg,#7ab5d4,#5a8fae);margin-top:10px">返回主菜单</button>
</div>
<div id="shopOverlay" class="overlay hidden">
<div style="font-size:60px;margin-bottom:6px">🛒</div>
<h2>商店</h2>
<div style="color:#ffd700;font-size:16px;font-weight:600;margin-bottom:10px" id="shopCoinsDisplay">💰 0</div>
<div style="display:flex;gap:8px;margin-bottom:12px">
<button class="diff-btn active" id="shopTabSkins">皮肤</button>
<button class="diff-btn" id="shopTabUpgrades">升级</button>
</div>
<div id="shopSkinsPanel" style="width:100%;max-width:340px"></div>
<div id="shopUpgradesPanel" style="width:100%;max-width:340px;display:none"></div>
<button class="start-btn" id="closeShopBtn" style="margin-top:14px">关闭</button>
</div>
</div>
<div id="mobileControls">
<div class="dpad-btn dpad-up" data-dir="up">▲</div>
<div class="dpad-btn dpad-left" data-dir="left">◀</div>
<div class="dpad-center">💩</div>
<div class="dpad-btn dpad-right" data-dir="right">▶</div>
<div class="dpad-btn dpad-down" data-dir="down">▼</div>
</div>
<!-- 测试面板 -->
<button id="testToggle">🧪 测试</button>
<div id="testPanel">
<h3>🧪 测试控制面板</h3>
<button class="test-btn orange" onclick="testSpawnWarning()">测试警告 (W键)</button>
<button class="test-btn red" onclick="testSpawnCountdown()">测试倒计时 (E键)</button>
<button class="test-btn" onclick="testSpawnFalling()">测试马桶落下 (R键)</button>
<button class="test-btn blue" onclick="testFullSequence()">完整流程 (T键)</button>
<button class="test-btn green" onclick="testClearAll()">清空所有 (C键)</button>
<button class="test-btn purple" onclick="testRandomWave()">随机一波 (Q键)</button>
<hr style="border:none;border-top:1px solid rgba(255,255,255,0.2);margin:8px 0">
<div style="color:#fff;font-size:11px;font-weight:600;margin-bottom:6px">难度测试</div>
<button class="test-btn red" onclick="testDifficulty10()">难度10测试 (D键)</button>
<button class="test-btn orange" onclick="testSetDifficulty(10)">设难度10</button>
<button class="test-btn" onclick="testForceWave(10)">强制波(难度10)</button>
<button class="test-btn blue" onclick="testCheckStatus()">检查状态(S键)</button>
<div class="hint">
快捷键:<br>
W=警告 E=倒计时<br>
R=落下 T=全流程<br>
Q=随机波 C=清空<br>
D=难度10 S=状态<br>
H=显示/隐藏面板
</div>
</div>
<script>
(function(){
'use strict';
var COLS = 16;
var ROWS = 22;
var canvas = document.getElementById('gameCanvas');
var ctx = canvas.getContext('2d');
var CELL = canvas.width / COLS;
var LS_KEY = 'neon_snake_best';
var STATE = { MENU:0, PLAYING:1, PAUSED:2, OVER:3 };
var game = null;
var audioCtx = null;
function initAudio(){
if(!audioCtx){
try{ audioCtx = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ audioCtx = null; }
}
}
function playBeep(freq, dur, type, vol){
if(!audioCtx || (game && game.soundMuted)) return;
try{
var osc = audioCtx.createOscillator();
var gain = audioCtx.createGain();
osc.type = type||'sine';
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(vol||0.1, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + dur);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + dur);
}catch(e){}
}
function playEatSound(){ playBeep(600, 0.08, 'square', 0.08); setTimeout(function(){playBeep(800, 0.08, 'square', 0.06);}, 60); }
function playPowerUpSound(){ playBeep(500, 0.1, 'sine', 0.1); setTimeout(function(){playBeep(700, 0.1, 'sine', 0.1);}, 80); setTimeout(function(){playBeep(900, 0.15, 'sine', 0.1);}, 160); }
function playGameOverSound(){ playBeep(300, 0.2, 'sawtooth', 0.12); setTimeout(function(){playBeep(200, 0.3, 'sawtooth', 0.12);}, 150); setTimeout(function(){playBeep(100, 0.5, 'sawtooth', 0.12);}, 350); }
function makeGame(){
return {
state: STATE.MENU,
snake: [],
dir: {x:1, y:0},
nextDir: {x:1, y:0},
score: 0,
coins: parseInt(localStorage.getItem('poop_snake_coins')||'0',10),
coinsEarned: 0,
length: 3,
speed: 220,
baseSpeed: 220,
lastStep: 0,
food: null,
specialFoods: [],
toilets: [],
warnings: [],
enemySnakes: [],
particles: [],
floatTexts: [],
shakeTime: 0,
shakeMag: 0,
difficulty: 0,
playerName: '便便',
controlType: 'dpad',
highScore: parseInt(localStorage.getItem(LS_KEY)||'0',10),
invincibleUntil: 0,
speedBoostUntil: 0,
stepProgress: 0,
lastTime: 0,
specialSpawnTimers: { mystery:0 },
toiletSpawnTimer: 0,
toiletSpawnQueue: [],
enemySpawnTimer: 0,
enemySpawnInterval: 15000,
toiletWaveInterval: 5000,
currentWave: 0,
tick: 0,
comboCount: 0,
lastEatTime: 0,
frameCount: 0,
pulseTime: 0,
elapsedTime: 0,
flashAlpha: 0,
flashColor: '',
flashDuration: 300,
achievements: [],
nextMilestone: 50,
notifications: [],
notifTimer: 0,
soundMuted: false,
upgradeLevel: parseInt(localStorage.getItem('poop_snake_upgrade')||'0',10),
shieldCharges: 0,
skinKey: localStorage.getItem('poop_snake_selected_skin') || 'default',
tips: [
'吃便便可以加分数哦!',
'连续吃便便可以获得连击奖励!',
'金便便是无敌的!',
'蓝便便会缩短蛇身',
'红便便可以加速',
'绿便便会召唤敌蛇',
'小心马桶!',
'敌蛇5秒后会消失',
'空格键可以暂停游戏',
'最高难度每15秒生成敌蛇'
],
tipTimer: 0
};
}
var SKINS = {
default: {name:'经典棕', colors:['#a0693a','#8b5a2b','#6b4226'], price:0},
fire: {name:'火焰红', colors:['#ff6b35','#e63946','#9d0208'], price:100},
ice: {name:'冰霜蓝', colors:['#a8dadc','#457b9d','#1d3557'], price:150},
gold: {name:'黄金蛇', colors:['#ffd700','#daa520','#b8860b'], price:300}
};
var UPGRADE_COSTS = [50, 150, 400, 1000, 2500];
function saveCoins(){
localStorage.setItem('poop_snake_coins', String(game.coins));
}
function getPurchasedSkins(){
try {
var arr = JSON.parse(localStorage.getItem('poop_snake_skins')||'[]');
return Array.isArray(arr) ? arr : [];
} catch(e) { return []; }
}
function savePurchasedSkins(arr){
localStorage.setItem('poop_snake_skins', JSON.stringify(arr));
}
function setSelectedSkin(key){
localStorage.setItem('poop_snake_selected_skin', key);
}
function saveUpgradeLevel(level){
localStorage.setItem('poop_snake_upgrade', String(level));
}
function initGame(){
initAudio();
game = makeGame();
game.state = STATE.PLAYING;
game.playerName = document.getElementById('nameInput').value.trim() || '便便';
game.controlType = document.getElementById('ctrlSelect').dataset.selected || 'dpad';
game.shieldCharges = game.upgradeLevel;
document.getElementById('playerName').textContent = game.playerName;
game.highScore = parseInt(localStorage.getItem(LS_KEY)||'0',10);
document.getElementById('highScore').textContent = game.highScore;
game.snake = [];
var cx = Math.floor(COLS/2), cy = Math.floor(ROWS/2);
for(var i=0;i<3;i++){
game.snake.push({x:cx-i, y:cy, prevX:cx-i, prevY:cy});
}
game.dir = {x:1,y:0};
game.nextDir = {x:1,y:0};
game.difficulty = parseInt(document.getElementById('diffOptions').dataset.selected||'0',10);
updateDifficultyUI();
game.food = spawnFood();
game.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
function updateDifficultyUI(){
var d = game.difficulty;
document.getElementById('difficulty').textContent = d;
}
function spawnFood(){
var pos, tries=0;
do{
pos = { x: Math.floor(Math.random()*COLS), y: Math.floor(Math.random()*ROWS) };
tries++;
if(tries>300) return pos;
}while(isOccupied(pos.x,pos.y,false,false));
return pos;
}
function spawnSpecialFood(type){
var pos, tries=0;
do{
pos = { x: Math.floor(Math.random()*COLS), y: Math.floor(Math.random()*ROWS) };
tries++;
if(tries>300) return null;
}while(isOccupied(pos.x,pos.y,true,false));
var durations = { mystery: 7000 };
game.specialFoods.push({ type:type, x:pos.x, y:pos.y, spawnTime:performance.now(), duration:durations[type] });
}
function isOccupied(x,y,includeSpecials,includeEnemies){
var i;
for(i=0;i<game.snake.length;i++) if(game.snake[i].x===x && game.snake[i].y===y) return true;
if(game.food && game.food.x===x && game.food.y===y) return true;
for(i=0;i<game.toilets.length;i++) if(game.toilets[i].x===x && game.toilets[i].y===y) return true;
if(includeSpecials){
for(i=0;i<game.specialFoods.length;i++) if(game.specialFoods[i].x===x && game.specialFoods[i].y===y) return true;
}
if(includeEnemies){
for(var e=0;e<game.enemySnakes.length;e++){
var es = game.enemySnakes[e];
for(var s=0;s<es.segments.length;s++){
if(es.segments[s].x===x && es.segments[s].y===y) return true;
}
}
}
return false;
}
function spawnToilet(now){
var d = game.difficulty;
if(d<1) return;
var perWave = d * 2;
// 将这一波的马桶随机分散在接下来的5秒内生成
for(var i=0;i<perWave;i++){
var delay = Math.random() * game.toiletWaveInterval;
game.toiletSpawnQueue.push({ spawnAt: now + delay });
}
}
function isOnFood(x, y){
if(game.food && game.food.x===x && game.food.y===y) return true;
for(var i=0;i<game.specialFoods.length;i++){
if(game.specialFoods[i].x===x && game.specialFoods[i].y===y) return true;
}
return false;
}
function spawnSingleToilet(){
var pos = null;
var tries = 0;
while(tries < 200){
pos = { x: Math.floor(Math.random()*COLS), y: Math.floor(Math.random()*ROWS) };
tries++;
if(!isOccupied(pos.x,pos.y,true,true) && !isTooCloseToSnake(pos) && !isWarningAt(pos.x,pos.y) && !isOnFood(pos.x,pos.y)){
break;
}
pos = null;
}
if(pos){
game.warnings.push({
x: pos.x,
y: pos.y,
state: 'warn',
stateTime: 0,
warnDuration: 1000,
countDuration: 2000,
fallDuration: 400,
fallY: -1,
});
}
}
function isWarningAt(x,y){
for(var i=0;i<game.warnings.length;i++){
if(game.warnings[i].x===x && game.warnings[i].y===y) return true;
}
return false;
}
function isTooCloseToSnake(pos){
// 检查蛇头周围4格(避免生成在蛇头前方导致来不及躲避)
var head = game.snake[0];
if(Math.abs(head.x-pos.x)<4 && Math.abs(head.y-pos.y)<4) return true;
// 检查整条蛇身(避免马桶生成在蛇身上)
for(var i=1;i<game.snake.length;i++){
if(Math.abs(game.snake[i].x-pos.x)<2 && Math.abs(game.snake[i].y-pos.y)<2) return true;
}
return false;
}
function spawnEnemySnake(now){
var len = 2 + Math.floor(Math.random()*3);
var edge = Math.floor(Math.random()*4);
var startX, startY;
if(edge===0){ startX=Math.floor(Math.random()*COLS); startY=0; }
else if(edge===1){ startX=COLS-1; startY=Math.floor(Math.random()*ROWS); }
else if(edge===2){ startX=Math.floor(Math.random()*COLS); startY=ROWS-1; }
else{ startX=0; startY=Math.floor(Math.random()*ROWS); }
var dirs = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
var dir = dirs[Math.floor(Math.random()*4)];
var segments = [];
for(var i=0;i<len;i++){
segments.push({ x:startX - dir.x*i, y:startY - dir.y*i });
}
game.enemySnakes.push({
segments: segments,
dir: dir,
moveTimer: 0,
lifeTime: 5000,
spawnTime: now,
moveInterval: 450,
prevSegments: segments.map(function(s){return {x:s.x,y:s.y};})
});
}
function spawnEnemySnakeAt(x,y,now){
var len = 2 + Math.floor(Math.random()*3);
var dirs = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
var dir = dirs[Math.floor(Math.random()*4)];
var segments = [];
for(var i=0;i<len;i++){
var sx = Math.max(0, Math.min(COLS-1, x - dir.x*i));
var sy = Math.max(0, Math.min(ROWS-1, y - dir.y*i));
segments.push({ x:sx, y:sy });
}
game.enemySnakes.push({
segments: segments,
dir: dir,
moveTimer: 0,
lifeTime: 5000,
spawnTime: now,
moveInterval: 450,
prevSegments: segments.map(function(s){return {x:s.x,y:s.y};})
});
}
function gameLoop(now){
if(!game || game.state===STATE.OVER) return;
if(game.state===STATE.PAUSED){
game.lastTime = now;
requestAnimationFrame(gameLoop);
return;
}
var dt = now - game.lastTime;
game.lastTime = now;
update(dt, now);
render(now);
requestAnimationFrame(gameLoop);
}
function update(dt, now){
if(game.state!==STATE.PLAYING) return;
game.tick++;
game.frameCount++;
game.elapsedTime += dt;
game.specialSpawnTimers.mystery += dt;
if(game.specialSpawnTimers.mystery >= 8000){
game.specialSpawnTimers.mystery = 0;
if(game.specialFoods.length < 3)
spawnSpecialFood('mystery');
}
for(var i=game.specialFoods.length-1;i>=0;i--){
if(now - game.specialFoods[i].spawnTime > game.specialFoods[i].duration){
game.specialFoods.splice(i,1);
}
}
if(game.difficulty>0){
game.toiletSpawnTimer += dt;
if(game.toiletSpawnTimer >= game.toiletWaveInterval){
game.toiletSpawnTimer = 0;
spawnToilet(now);
game.currentWave++;
flashScreen('rgba(100,150,200,0.15)', 300);
spawnFloatText(8, 1, '第' + game.currentWave + '波来袭!', '#3a7ab5');
}
// 检查生成队列:到了时间就生成单个马桶(随机分散掉落)
for(var qi=game.toiletSpawnQueue.length-1; qi>=0; qi--){
if(now >= game.toiletSpawnQueue[qi].spawnAt){
spawnSingleToilet();
game.toiletSpawnQueue.splice(qi, 1);
}
}
for(var wi = game.warnings.length-1; wi>=0; wi--){
var w = game.warnings[wi];
w.stateTime += dt;
if(w.state === 'warn'){
if(w.stateTime >= w.warnDuration){
w.state = 'countdown';
w.stateTime = 0;
}
} else if(w.state === 'countdown'){
if(w.stateTime >= w.countDuration){
w.state = 'falling';
w.stateTime = 0;
}
} else if(w.state === 'falling'){
// 落下动画:从屏幕顶部掉到目标位置
var t = Math.min(w.stateTime / w.fallDuration, 1);
var targetY = w.y * CELL + CELL/2;
var startY = -CELL * 2;
w.fallY = startY + (targetY - startY) * t;
if(t >= 1){
// 落下完成,检查蛇身是否在该位置(整条蛇,不只是头部)
var hitSnake = false;
for(var si=0; si<game.snake.length; si++){
if(game.snake[si].x === w.x && game.snake[si].y === w.y){
hitSnake = true;
break;
}
}
if(hitSnake){
// 蛇身在该位置,马桶消失(避免不公平死亡)
game.warnings.splice(wi, 1);
spawnFloatText(w.x, w.y, '安全!', '#77dd77');
spawnParticles(w.x, w.y, '#77dd77');
} else {
// 正式成为马桶障碍(10秒后自动消失)
var expireTime = now + 10000;
game.toilets.push({x: w.x, y: w.y, spawnTime: now, landTime: now, expireTime: expireTime});
game.warnings.splice(wi, 1);
spawnParticles(w.x, w.y, '#ff6b6b');
flashScreen('rgba(255,100,100,0.15)', 200);
}
}
}
}
game.enemySpawnTimer += dt;
if(game.enemySpawnTimer >= game.enemySpawnInterval){
game.enemySpawnTimer = 0;
spawnEnemySnake(now);
}
// 马桶过期检查:10秒后自动消失
for(var ti = game.toilets.length-1; ti >= 0; ti--){
if(game.toilets[ti].expireTime && now >= game.toilets[ti].expireTime){
var expired = game.toilets[ti];
spawnParticles(expired.x, expired.y, '#a8b8c4');
game.toilets.splice(ti, 1);
}
}
}
if(game.comboCount > 0 && now - game.lastEatTime > 3000){
game.comboCount = 0;
}
if(game.tipTimer === undefined) game.tipTimer = 0;
game.tipTimer += dt;
if(game.tipTimer > 15000 && game.tips && game.tips.length > 0){
game.tipTimer = 0;
var tip = game.tips[Math.floor(Math.random()*game.tips.length)];
spawnFloatText(8, 10, tip, '#5a7a8a');
}
var speedMul = now < game.speedBoostUntil ? 1.35 : 1;
var currentSpeed = game.speed / speedMul;
game.lastStep += dt;
game.stepProgress = Math.min(game.lastStep / currentSpeed, 1);
while(game.lastStep >= currentSpeed){
game.lastStep -= currentSpeed;
game.stepProgress = 0;
stepSnake(now);
if(game.state===STATE.OVER) return;
}
for(var i=game.enemySnakes.length-1;i>=0;i--){
var es = game.enemySnakes[i];
if(now - es.spawnTime > es.lifeTime){
game.enemySnakes.splice(i,1);
continue;
}
es.moveTimer += dt;
if(es.moveTimer >= es.moveInterval){
es.moveTimer = 0;
moveEnemySnake(es, now);
if(game.state===STATE.OVER) return;
}
}
for(var i=game.particles.length-1;i>=0;i--){
var p = game.particles[i];
p.life -= dt;
p.x += p.vx * dt/1000;
p.y += p.vy * dt/1000;
p.vy += 300 * dt/1000;
p.vx *= 0.98;
if(p.life <= 0) game.particles.splice(i,1);
}
for(var i=game.floatTexts.length-1;i>=0;i--){
var f = game.floatTexts[i];
f.life -= dt;
f.y += f.vy * dt/1000;
if(f.life <= 0) game.floatTexts.splice(i,1);
}
for(var i=game.notifications.length-1;i>=0;i--){
game.notifications[i].life -= dt;
if(game.notifications[i].life <= 0){
game.notifications.splice(i,1);
}
}
if(game.shakeTime > 0){
game.shakeTime -= dt;
if(game.shakeTime <= 0) game.shakeMag = 0;
}
if(game.flashAlpha > 0){
game.flashAlpha -= dt / (game.flashDuration || 300);
if(game.flashAlpha < 0) game.flashAlpha = 0;
}
if(game.pulseTime > 0){
game.pulseTime -= dt;
}
if(game.elapsedTime > 60000 && game.achievements.indexOf('survivor') < 0){
onAchievement('survivor');
}
if(now < game.invincibleUntil && game.achievements.indexOf('invincible') < 0){
onAchievement('invincible');
}
if(now < game.speedBoostUntil && game.achievements.indexOf('speed') < 0){
onAchievement('speed');
}
updateBadges(now);
}
function stepSnake(now){
game.dir = game.nextDir;
for(var i=0;i<game.snake.length;i++){
game.snake[i].prevX = game.snake[i].x;
game.snake[i].prevY = game.snake[i].y;
}
var head = game.snake[0];
var newX = head.x + game.dir.x;
var newY = head.y + game.dir.y;
// 四周穿墙:从一边出去从对面进来
if(newX < 0) newX = COLS - 1;
else if(newX >= COLS) newX = 0;
if(newY < 0) newY = ROWS - 1;
else if(newY >= ROWS) newY = 0;
var invincible = now < game.invincibleUntil;
if(invincible){
// 无敌时:碰马桶/敌蛇自动掉头
for(var t=0;t<game.toilets.length;t++){
if(game.toilets[t].x===newX && game.toilets[t].y===newY){
game.dir = {x: -game.dir.x, y: -game.dir.y};
game.nextDir = game.dir;
spawnFloatText(newX, newY, '自动掉头!', '#ffd700');
return;
}
}
for(var e=0;e<game.enemySnakes.length;e++){
var es = game.enemySnakes[e];
for(var s=0;s<es.segments.length;s++){
if(es.segments[s].x===newX && es.segments[s].y===newY){
game.dir = {x: -game.dir.x, y: -game.dir.y};
game.nextDir = game.dir;
spawnFloatText(newX, newY, '自动掉头!', '#ffd700');
return;
}
}
}
} else {
// 普通模式:碰马桶/敌蛇判负(有护盾则掉头)
for(var t=0;t<game.toilets.length;t++){
if(game.toilets[t].x===newX && game.toilets[t].y===newY){
if(game.shieldCharges > 0){
game.shieldCharges--;
game.dir = {x: -game.dir.x, y: -game.dir.y};
game.nextDir = game.dir;
spawnFloatText(newX, newY, '护盾抵挡!', '#66b3ff');
return;
}
triggerGameOver('掉进马桶了!');
return;
}
}
for(var e=0;e<game.enemySnakes.length;e++){
var es = game.enemySnakes[e];
for(var s=0;s<es.segments.length;s++){
if(es.segments[s].x===newX && es.segments[s].y===newY){
if(game.shieldCharges > 0){
game.shieldCharges--;
game.dir = {x: -game.dir.x, y: -game.dir.y};
game.nextDir = game.dir;
spawnFloatText(newX, newY, '护盾抵挡!', '#66b3ff');
return;
}
triggerGameOver('被敌人蛇蛇咬到了!');
return;
}
}
}
}
game.snake.unshift({x:newX, y:newY, prevX:head.x, prevY:head.y});
var grew = false;
if(game.food && game.food.x===newX && game.food.y===newY){
game.comboCount++;
game.lastEatTime = now;
var comboBonus = Math.min(game.comboCount, 5);
var points = 10 + (comboBonus-1)*2;
game.score += points;
game.coins += 1;
game.coinsEarned += 1;
game.length++;
game.speed = Math.max(80, game.baseSpeed - Math.floor(game.length*0.8));
game.baseSpeed = game.speed;
spawnParticles(newX, newY, '#8b4513');
spawnFloatText(newX, newY, '+' + points + (game.comboCount>1?' x'+game.comboCount:''), '#8b4513');
game.food = spawnFood();
grew = true;
// 吃大便消除马桶(难度越高消除越多)
var removeCount = 1 + Math.floor(game.difficulty / 2);
for(var ri = 0; ri < removeCount && game.toilets.length > 0; ri++){
var removed = game.toilets.shift();
spawnParticles(removed.x, removed.y, '#c0392b');
}
if(removeCount > 1 && game.toilets.length >= removeCount - 1){
spawnFloatText(newX, newY - CELL, '清除 ' + removeCount + ' 个马桶!', '#c0392b');
}
playEatSound();
game.pulseTime = 200;
if(game.score >= game.nextMilestone){
onAchievement('score');
game.nextMilestone += 50;
}
if(game.length === 10){ onAchievement('length10'); }
if(game.length === 25){ onAchievement('length25'); }
if(game.length === 50){ onAchievement('length50'); }
}
for(var i=game.specialFoods.length-1;i>=0;i--){
var sf = game.specialFoods[i];
if(sf.x===newX && sf.y===newY){
applySpecial(sf.type, now);
spawnParticles(newX, newY, specialColor(sf.type));
spawnFloatText(newX, newY, specialLabel(sf.type), specialColor(sf.type));
game.specialFoods.splice(i,1);
// 注意:特殊道具不增加蛇长,所以不设置 grew
playPowerUpSound();
game.flashAlpha = 0.3;
game.flashColor = specialColor(sf.type);
break;
}
}
if(!grew){
game.snake.pop();
}
updateHUD();
}
function triggerGameOver(reason){
if(game.state===STATE.OVER) return;
game.state = STATE.OVER;
saveCoins();
game.shakeMag = 18;
game.shakeTime = 450;
game.flashAlpha = 0.5;
game.flashColor = '#ff3333';
playGameOverSound();
var isNew = game.score > game.highScore;
if(isNew){
game.highScore = game.score;
localStorage.setItem(LS_KEY, String(game.highScore));
}
document.getElementById('gameOverReason').textContent = reason || '';
document.getElementById('finalScore').textContent = game.score;
document.getElementById('coinsEarned').textContent = '获得 ' + game.coinsEarned + ' 金币';
document.getElementById('newRecord').classList.toggle('hidden', !isNew);
if(isNew){
celebrateNewRecord();
}
document.getElementById('highScore').textContent = game.highScore;
var statsParts = [];
statsParts.push('长度: ' + game.length);
statsParts.push('难度: ' + game.difficulty);
statsParts.push('存活: ' + Math.floor(game.elapsedTime/1000) + '秒');
var achText = '';
if(game.achievements.length > 0){
var icons = game.achievements.map(function(a){ return ACHIEVEMENTS[a] ? ACHIEVEMENTS[a].icon : ''; });
achText = ' 成就: ' + icons.join(' ');
}
document.getElementById('statsLine').textContent = statsParts.join(' · ') + achText;
setTimeout(function(){
document.getElementById('gameOverOverlay').classList.remove('hidden');
}, 600);
}
function applySpecial(type, now){
if(type==='mystery'){
var effects = ['speed', 'shorten', 'enemy', 'invincible'];
var effect = effects[Math.floor(Math.random()*effects.length)];
if(effect==='speed'){
game.speedBoostUntil = now + 10000;
game.lastEffect = 'speed';
} else if(effect==='shorten'){
for(var i=0;i<3 && game.snake.length>3;i++){
game.snake.pop();
game.length--;
}
game.lastEffect = 'shorten';
} else if(effect==='enemy'){
var head = game.snake[0];
var offsetDirs = [{x:0,y:-3},{x:0,y:3},{x:3,y:0},{x:-3,y:0}];
var od = offsetDirs[Math.floor(Math.random()*4)];
spawnEnemySnakeAt(head.x + od.x, head.y + od.y, now);
game.lastEffect = 'enemy';
} else if(effect==='invincible'){
game.invincibleUntil = now + 7000;
game.lastEffect = 'invincible';
}
}
}
function flashScreen(color, duration){
game.flashColor = color;
game.flashAlpha = 1;
game.flashDuration = duration || 300;
}
function celebrateNewRecord(){
for(var i=0;i<40;i++){
setTimeout(function(idx){
var colors = ['#ffd700','#ff6666','#66b3ff','#77dd77','#ff9eb5'];
var color = colors[idx % colors.length];
spawnParticles(
Math.floor(Math.random()*COLS),
Math.floor(Math.random()*ROWS),
color
);
}, i * 50, i);
}
for(var k=0;k<8;k++){
setTimeout(function(c){
playBeep(600 + c*200, 0.15, 'sine', 0.1);
}, k * 120, k);
}
}
function shakeScreen(mag, duration){
game.shakeMag = mag || 8;
game.shakeTime = duration || 300;
}
function specialColor(type){
return { mystery:'#9b59b6' }[type] || '#9b59b6';
}
function specialLabel(type){
return { mystery:'神秘!' }[type] || '神秘!';
}
var ACHIEVEMENTS = {
score: { icon:'🏆', title:'分数里程碑!', desc:'得分达到 ' },
length10: { icon:'📏', title:'小蛇成形!', desc:'蛇身长度达到 10' },
length25: { icon:'🐍', title:'蛇蛇蛇!', desc:'蛇身长度达到 25' },
length50: { icon:'🐲', title:'巨龙降临!', desc:'蛇身长度达到 50' },
invincible: { icon:'⭐', title:'无敌状态!', desc:'黄金便便加持中' },
speed: { icon:'⚡', title:'极速冲刺!', desc:'加速效果启动' },
survivor: { icon:'🛡️', title:'生存专家!', desc:'存活超过 60 秒' }
};
function onAchievement(type){
if(game.achievements.indexOf(type) >= 0 && type!=='score') return;
game.achievements.push(type);
var ach = ACHIEVEMENTS[type];
var msg = ach.desc;
if(type==='score') msg += game.score + '!';
game.notifications.push({ icon:ach.icon, title:ach.title, desc:msg, life:3000, maxLife:3000 });
game.notifTimer = 3000;
var head = game.snake[0];
spawnFloatText(head.x, head.y, ach.icon + ' ' + ach.title, '#ffd700');
playBeep(1200, 0.2, 'sine', 0.12);
setTimeout(function(){ playBeep(1600, 0.15, 'sine', 0.1); }, 100);
}
function drawNotifications(){
var startY = canvas.height * 0.15;
for(var i=0;i<game.notifications.length;i++){
var n = game.notifications[i];
var alpha = Math.min(1, n.life / 500);
var slideY = (1 - n.life/n.maxLife) * -30;
ctx.globalAlpha = alpha;
var boxW = 220;
var boxH = 56;
var boxX = (canvas.width - boxW)/2;
var boxY = startY + i * (boxH + 8) + slideY;
ctx.fillStyle = 'rgba(30,60,80,0.92)';
roundRect(ctx, boxX, boxY, boxW, boxH, 12);
ctx.fill();
ctx.strokeStyle = '#ffd700';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = '24px sans-serif';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(n.icon, boxX + 14, boxY + boxH/2);
ctx.font = 'bold 14px Fredoka, sans-serif';
ctx.fillStyle = '#ffd700';
ctx.fillText(n.title, boxX + 46, boxY + boxH/2 - 8);
ctx.font = '11px Fredoka, sans-serif';
ctx.fillStyle = '#d0e8f2';
ctx.fillText(n.desc, boxX + 46, boxY + boxH/2 + 10);
}
ctx.globalAlpha = 1;
}
function moveEnemySnake(es, now){
if(!es.segments || es.segments.length===0) return;
for(var i=0;i<es.segments.length;i++){
es.prevSegments[i] = { x: es.segments[i].x, y: es.segments[i].y };
}
var head = es.segments[0];
var nx = head.x + es.dir.x;
var ny = head.y + es.dir.y;
var blocked = false;
if(nx<0 || nx>=COLS || ny<0 || ny>=ROWS) blocked = true;
if(!blocked){
for(var t=0;t<game.toilets.length;t++){
if(game.toilets[t].x===nx && game.toilets[t].y===ny){ blocked=true; break; }
}
}
if(!blocked){
for(var s=0;s<es.segments.length;s++){
if(es.segments[s].x===nx && es.segments[s].y===ny){ blocked=true; break; }
}
}
if(blocked){
var dirs = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
var options = dirs.filter(function(d){return !(d.x===-es.dir.x && d.y===-es.dir.y);});
options = options.filter(function(d){
var tx = head.x+d.x, ty = head.y+d.y;
if(tx<0||tx>=COLS||ty<0||ty>=ROWS) return false;
for(var t=0;t<game.toilets.length;t++) if(game.toilets[t].x===tx&&game.toilets[t].y===ty) return false;
return true;
});
if(options.length===0){
es.segments = [];
return;
}
es.dir = options[Math.floor(Math.random()*options.length)];
nx = head.x + es.dir.x;
ny = head.y + es.dir.y;
}
es.segments.unshift({x:nx, y:ny});
es.segments.pop();
if(game.snake.length>0){
var playerHead = game.snake[0];
for(var s=0;s<es.segments.length;s++){
var seg = es.segments[s];
if(seg.x===playerHead.x && seg.y===playerHead.y){
es.segments = [];
spawnParticles(playerHead.x, playerHead.y, '#8b5a2b');
spawnFloatText(playerHead.x, playerHead.y, '敌蛇消失!', '#8b5a2b');
return;
}
}
}
}
function spawnParticles(gx, gy, color){
var cx = gx*CELL + CELL/2;
var cy = gy*CELL + CELL/2;
for(var i=0;i<16;i++){
var angle = (Math.PI*2) * (i/16) + Math.random()*0.5;
var speed = 80 + Math.random()*150;
game.particles.push({
x:cx, y:cy,
vx: Math.cos(angle)*speed,
vy: Math.sin(angle)*speed - 80,
life: 500 + Math.random()*400,
maxLife: 900,
color: color,
size: 3 + Math.random()*5
});
}
for(var i=0;i<6;i++){
game.particles.push({
x: cx + (Math.random()-0.5)*10,
y: cy + (Math.random()-0.5)*10,
vx: (Math.random()-0.5)*40,
vy: -40 - Math.random()*60,
life: 800 + Math.random()*400,
maxLife: 1200,
color: '#fff8dc',
size: 1 + Math.random()*2,
sparkle: true
});
}
}
function spawnFloatText(gx, gy, text, color){
game.floatTexts.push({
x: gx*CELL + CELL/2,
y: gy*CELL + CELL/2,
vy: -60,
life: 1000,
text: text,
color: color,
size: 16
});
}
function updateHUD(){
document.getElementById('score').textContent = game.score;
document.getElementById('length').textContent = game.length;
document.getElementById('difficulty').textContent = game.difficulty;
document.getElementById('highScore').textContent = game.highScore;
document.getElementById('coinsDisplay').textContent = game.coins;
var shieldItem = document.getElementById('shieldItem');
if(game.shieldCharges > 0){
shieldItem.style.display = 'flex';
document.getElementById('shieldDisplay').textContent = game.shieldCharges;
} else {
shieldItem.style.display = 'none';
}
var waveItem = document.getElementById('waveItem');
if(game.difficulty > 0 && game.currentWave > 0){
waveItem.style.display = 'flex';
document.getElementById('waveLabel').textContent = game.currentWave;
} else {
waveItem.style.display = 'none';
}
var comboItem = document.getElementById('comboItem');
if(game.comboCount > 1){
comboItem.style.display = 'flex';
document.getElementById('comboLabel').textContent = game.comboCount;
} else {
comboItem.style.display = 'none';
}
}
function updateBadges(now){
var badges = document.getElementById('statusBadges');
badges.innerHTML = '';
if(now < game.invincibleUntil){
var remain = Math.ceil((game.invincibleUntil - now)/1000);
var b = document.createElement('div');
b.className = 'badge invincible';
b.innerHTML = '⭐ 无敌 ' + remain + 's';
badges.appendChild(b);
}
if(now < game.speedBoostUntil){
var remain2 = Math.ceil((game.speedBoostUntil - now)/1000);
var b2 = document.createElement('div');
b2.className = 'badge speed';
b2.innerHTML = '⚡ 加速 ' + remain2 + 's';
badges.appendChild(b2);
}
if(game.comboCount >= 2){
var b3 = document.createElement('div');
b3.className = 'badge combo';
b3.innerHTML = '🔥 连击 x' + game.comboCount;
badges.appendChild(b3);
}
}
function render(now){
ctx.clearRect(0,0,canvas.width,canvas.height);
var sx=0, sy=0;
if(game.shakeTime > 0){
sx = (Math.random()-0.5)*game.shakeMag;
sy = (Math.random()-0.5)*game.shakeMag;
}
ctx.save();
ctx.translate(sx, sy);
drawTileBackground();
drawWarnings(now);
drawToilets(now);
var t = game.stepProgress;
drawSnake(t, now);
drawFood(now);
drawSpecialFoods(now);
drawEnemySnakes(now);
if(game.comboCount >= 2){
ctx.save();
var comboAlpha = 0.3 + 0.3*Math.sin(now*0.01);
ctx.globalAlpha = comboAlpha;
ctx.fillStyle = '#ff9eb5';
ctx.font = 'bold 16px Fredoka, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
var comboStr = 'x' + game.comboCount + ' 连击!';
ctx.fillText(comboStr, canvas.width/2, 80);
ctx.globalAlpha = comboAlpha * 0.5;
ctx.fillStyle = '#fff';
ctx.fillText(comboStr, canvas.width/2 + 2, 82);
ctx.restore();
}
if(now < game.speedBoostUntil){
var speedPulse = 0.3 + 0.2*Math.sin(now*0.02);
ctx.save();
ctx.globalAlpha = speedPulse;
ctx.fillStyle = '#ff6666';
ctx.fillRect(0,0,canvas.width,4);
ctx.fillRect(0,canvas.height-4,canvas.width,4);
ctx.restore();
}
drawParticles();
drawFloatTexts();
drawNotifications();
if(game.flashAlpha > 0){
ctx.fillStyle = game.flashColor;
ctx.globalAlpha = game.flashAlpha;
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.globalAlpha = 1;
}
var vignetteAlpha = 0;
if(game.snake.length > 50) vignetteAlpha = Math.min(0.4, (game.snake.length-50)*0.02);
if(game.enemySnakes.length > 0) vignetteAlpha = Math.max(vignetteAlpha, 0.15);
if(vignetteAlpha > 0){
var vg = ctx.createRadialGradient(canvas.width/2, canvas.height/2, canvas.width*0.2, canvas.width/2, canvas.height/2, canvas.width*0.75);
vg.addColorStop(0, 'rgba(255,0,0,0)');
vg.addColorStop(1, 'rgba(180,0,0,' + vignetteAlpha + ')');
ctx.fillStyle = vg;
ctx.fillRect(0,0,canvas.width,canvas.height);
}
ctx.restore();
}
function drawTileBackground(){
var wallGrad = ctx.createLinearGradient(0,0,0,canvas.height);
wallGrad.addColorStop(0, '#e8f0f5');
wallGrad.addColorStop(0.3, '#dde8f0');
wallGrad.addColorStop(1, '#c5d5e0');
ctx.fillStyle = wallGrad;
ctx.fillRect(0,0,canvas.width,canvas.height);
for(var y=0;y<ROWS;y++){
for(var x=0;x<COLS;x++){
var px = x*CELL, py = y*CELL;
var isAlt = (x+y)%2===0;
var tileBase = isAlt ? '#e8f0f5' : '#dce6ed';
ctx.fillStyle = tileBase;
ctx.fillRect(px, py, CELL, CELL);
ctx.save();
ctx.globalAlpha = 0.12;
ctx.strokeStyle = '#8a9bab';
ctx.lineWidth = 1;
var midX = px + CELL/2, midY = py + CELL/2;
ctx.beginPath();
ctx.moveTo(midX - CELL*0.25, midY);
ctx.lineTo(midX + CELL*0.25, midY);
ctx.moveTo(midX, midY - CELL*0.25);
ctx.lineTo(midX, midY + CELL*0.25);
ctx.stroke();
ctx.fillStyle = '#8a9bab';
ctx.beginPath();
ctx.arc(midX, midY, 1.5, 0, Math.PI*2);
ctx.fill();
ctx.restore();
ctx.strokeStyle = 'rgba(130,150,165,0.4)';
ctx.lineWidth = 1;
ctx.strokeRect(px+0.5, py+0.5, CELL-1, CELL-1);
}
}
var wallH = canvas.height * 0.08;
ctx.fillStyle = 'rgba(200,180,160,0.25)';
ctx.fillRect(0, 0, canvas.width, wallH);
var reflectGrad = ctx.createLinearGradient(0, canvas.height*0.6, 0, canvas.height);
reflectGrad.addColorStop(0, 'rgba(255,255,255,0)');
reflectGrad.addColorStop(0.5, 'rgba(255,255,255,0.08)');
reflectGrad.addColorStop(1, 'rgba(180,200,215,0.2)');
ctx.fillStyle = reflectGrad;
ctx.fillRect(0, canvas.height*0.6, canvas.width, canvas.height*0.4);
ctx.save();
for(var i = 0; i < 8; i++){
var hx = ((i * 73 + 19) % 100) / 100;
var hy = ((i * 97 + 41) % 100) / 100;
var sx = hx * canvas.width;
var sy = hy * canvas.height;
ctx.globalAlpha = 0.06;
ctx.fillStyle = '#7a8a95';
ctx.beginPath();
ctx.arc(sx, sy, 6 + (i%3)*2, 0, Math.PI*2);
ctx.fill();
}
ctx.restore();
var topGlow = ctx.createRadialGradient(canvas.width/2, 0, 0, canvas.width/2, 0, canvas.width*0.8);
topGlow.addColorStop(0, 'rgba(255,255,255,0.4)');
topGlow.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = topGlow;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawWarnings(now){
for(var i = 0; i < game.warnings.length; i++){
var w = game.warnings[i];
var cx = w.x * CELL + CELL/2;
var s = CELL * 0.45;
if(w.state === 'warn'){
// 橙色闪烁警告
var pulse = 0.5 + 0.5 * Math.sin(now/80);
var alpha = 0.4 + 0.4 * pulse;
ctx.save();
ctx.globalAlpha = alpha;
// 外圈
ctx.strokeStyle = '#ffa500';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(cx, w.y*CELL + CELL/2, s*1.1, 0, Math.PI*2);
ctx.stroke();
// 内圈填充
ctx.globalAlpha = alpha * 0.5;
ctx.fillStyle = 'rgba(255,165,0,0.2)';
ctx.beginPath();
ctx.arc(cx, w.y*CELL + CELL/2, s*0.9, 0, Math.PI*2);
ctx.fill();
// "!" 标记
ctx.globalAlpha = 1;
ctx.fillStyle = '#ff8c00';
ctx.font = 'bold ' + Math.round(CELL*0.55) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('!', cx, w.y*CELL + CELL/2);
ctx.restore();
} else if(w.state === 'countdown'){
// 红色倒计时
var pulse = 0.5 + 0.5 * Math.sin(now/60);
var alpha = 0.5 + 0.5 * pulse;
var elapsed = w.stateTime;
var remaining = Math.max(1, Math.ceil(3 - elapsed/1000));
ctx.save();
ctx.globalAlpha = alpha;
// 外圈闪烁
ctx.strokeStyle = '#ff3b3b';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(cx, w.y*CELL + CELL/2, s*1.15, 0, Math.PI*2);
ctx.stroke();
// 内圈
ctx.globalAlpha = alpha * 0.7;
ctx.fillStyle = 'rgba(255,59,59,0.25)';
ctx.beginPath();
ctx.arc(cx, w.y*CELL + CELL/2, s*0.95, 0, Math.PI*2);
ctx.fill();
// 冲击波扩散
var waveProgress = (elapsed % 1000) / 1000;
ctx.globalAlpha = (1 - waveProgress) * 0.6;
ctx.strokeStyle = '#ff3b3b';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(cx, w.y*CELL + CELL/2, s*0.5 + waveProgress * s * 1.5, 0, Math.PI*2);
ctx.stroke();
// 倒计时数字
ctx.globalAlpha = 1;
ctx.fillStyle = '#ff1a1a';
ctx.strokeStyle = '#fff';
ctx.lineWidth = 3;
ctx.font = 'bold ' + Math.round(CELL*0.65) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
var cy = w.y*CELL + CELL/2;
ctx.strokeText(remaining, cx, cy);
ctx.fillText(remaining, cx, cy);
ctx.restore();
} else if(w.state === 'falling'){
// 马桶正在落下
ctx.save();
// 画拖影
ctx.globalAlpha = 0.3;
var targetY = w.y * CELL + CELL/2;
var progress = w.stateTime / w.fallDuration;
var trailLen = CELL * 2 * progress;
var trailGrad = ctx.createLinearGradient(cx, w.fallY - trailLen, cx, w.fallY);
trailGrad.addColorStop(0, 'rgba(100,150,200,0)');
trailGrad.addColorStop(1, 'rgba(100,150,200,0.4)');
ctx.fillStyle = trailGrad;
ctx.fillRect(cx - CELL*0.35, w.fallY - trailLen, CELL*0.7, trailLen);
// 画落下中的马桶
ctx.globalAlpha = 1;
var sizeScale = 0.7 + 0.3 * progress; // 越接近目标越大
var tempS = CELL * 0.38 * sizeScale;
// 简易马桶图形(落动态)
var mx = cx;
var my = w.fallY;
// 地面阴影
ctx.fillStyle = 'rgba(60,80,100,0.15)';
ctx.beginPath();
ctx.ellipse(mx, targetY + CELL*0.42, CELL*0.5*sizeScale, CELL*0.12, 0, 0, Math.PI*2);
ctx.fill();
// 马桶本体(简化版,落动态显示)
var bodyGrad = ctx.createLinearGradient(mx-tempS, my-tempS, mx+tempS, my+tempS);
bodyGrad.addColorStop(0, '#e8f4fa');
bodyGrad.addColorStop(1, '#a8b8c4');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.ellipse(mx, my, tempS*0.9, tempS*1.05, 0, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#5a7080';
ctx.lineWidth = 2;
ctx.stroke();
// 水箱
ctx.fillStyle = '#d0dde8';
roundRect(ctx, mx-tempS*0.6, my-tempS*0.9, tempS*1.2, tempS*0.5, tempS*0.08);
ctx.fill();
ctx.stroke();
// 马桶水
ctx.fillStyle = '#5ca8c9';
ctx.beginPath();
ctx.ellipse(mx, my+tempS*0.15, tempS*0.45, tempS*0.35, 0, 0, Math.PI*2);
ctx.fill();
// 溅水特效
if(progress > 0.5){
var splashAlpha = (progress - 0.5) * 2;
ctx.globalAlpha = splashAlpha * 0.6;
ctx.fillStyle = '#7fc8e0';
for(var si = 0; si < 8; si++){
var angle = (si / 8) * Math.PI * 2;
var dist = CELL * 0.4 * progress;
var spx = mx + Math.cos(angle) * dist;
var spy = targetY + Math.sin(angle) * dist * 0.5;
ctx.beginPath();
ctx.arc(spx, spy, 2 + Math.random()*2, 0, Math.PI*2);
ctx.fill();
}
}
ctx.restore();
}
}
}
function drawToilets(now){
for(var i=0;i<game.toilets.length;i++){
var t = game.toilets[i];
var alpha = 1;
// 最后3秒闪烁
if(t.expireTime && now > 0){
var remaining = t.expireTime - now;
if(remaining < 3000 && remaining > 0){
var blink = Math.sin(remaining * 0.02) * 0.3 + 0.7;
alpha = blink;
} else if(remaining <= 0){
continue; // 过期的不绘制
}
}
ctx.save();
ctx.globalAlpha = alpha;
drawToilet(t.x*CELL, t.y*CELL);
ctx.restore();
}
}
function drawToilet(px, py){
var cx = px + CELL/2;
var cy = py + CELL/2;
var s = CELL*0.38;
ctx.fillStyle = 'rgba(60,80,100,0.2)';
ctx.beginPath();
ctx.ellipse(cx, cy+s*1.1, s*1.05, s*0.22, 0, 0, Math.PI*2);
ctx.fill();
var baseGrad = ctx.createLinearGradient(cx-s*0.9, cy+s*0.6, cx+s*0.9, cy+s*1.1);
baseGrad.addColorStop(0, '#e8edf2');
baseGrad.addColorStop(1, '#b8c4d0');
ctx.fillStyle = baseGrad;
ctx.beginPath();
ctx.ellipse(cx, cy+s*0.85, s*0.92, s*0.35, 0, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#7a8a98';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#c0ccd6';
ctx.beginPath();
ctx.moveTo(cx-s*0.15, cy-s*0.1);
ctx.lineTo(cx-s*0.18, cy-s*0.7);
ctx.lineTo(cx+s*0.18, cy-s*0.7);
ctx.lineTo(cx+s*0.15, cy-s*0.1);
ctx.closePath();
ctx.fill();
var tankGrad = ctx.createLinearGradient(cx-s*0.65, cy-s*1.15, cx+s*0.65, cy-s*0.35);
tankGrad.addColorStop(0, '#f0f5fa');
tankGrad.addColorStop(0.4, '#d8e4ed');
tankGrad.addColorStop(1, '#a8b8c4');
ctx.fillStyle = tankGrad;
roundRect(ctx, cx-s*0.62, cy-s*1.12, s*1.24, s*0.72, s*0.1);
ctx.fill();
ctx.strokeStyle = '#5a7080';
ctx.lineWidth = 1.8;
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.beginPath();
roundRect(ctx, cx-s*0.55, cy-s*1.08, s*0.2, s*0.6, s*0.05);
ctx.fill();
var btnGrad = ctx.createRadialGradient(cx, cy-s*0.78, 0, cx, cy-s*0.78, s*0.1);
btnGrad.addColorStop(0, '#5a7080');
btnGrad.addColorStop(1, '#3a4a55');
ctx.fillStyle = btnGrad;
ctx.beginPath();
ctx.arc(cx, cy-s*0.78, s*0.09, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.beginPath();
ctx.arc(cx-s*0.03, cy-s*0.81, s*0.035, 0, Math.PI*2);
ctx.fill();
var bowlGrad = ctx.createRadialGradient(cx-s*0.1, cy+s*0.05, s*0.1, cx, cy+s*0.1, s*1);
bowlGrad.addColorStop(0, '#ffffff');
bowlGrad.addColorStop(0.5, '#e4ecf2');
bowlGrad.addColorStop(0.85, '#c0d0dc');
bowlGrad.addColorStop(1, '#90a4b4');
ctx.fillStyle = bowlGrad;
ctx.beginPath();
ctx.ellipse(cx, cy+s*0.08, s*0.9, s*0.78, 0, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#5a7080';
ctx.lineWidth = 2;
ctx.stroke();
var innerGrad = ctx.createRadialGradient(cx, cy+s*0.15, 0, cx, cy+s*0.18, s*0.6);
innerGrad.addColorStop(0, '#a0b8c8');
innerGrad.addColorStop(0.6, '#7a95a8');
innerGrad.addColorStop(1, '#5a7080');
ctx.fillStyle = innerGrad;
ctx.beginPath();
ctx.ellipse(cx, cy+s*0.15, s*0.6, s*0.5, 0, 0, Math.PI*2);
ctx.fill();
var waterGrad = ctx.createRadialGradient(cx-s*0.1, cy+s*0.12, 0, cx, cy+s*0.2, s*0.45);
waterGrad.addColorStop(0, '#8ac5e0');
waterGrad.addColorStop(0.4, '#5ca8c9');
waterGrad.addColorStop(0.8, '#3a7a95');
waterGrad.addColorStop(1, '#2a5a70');
ctx.fillStyle = waterGrad;
ctx.beginPath();
ctx.ellipse(cx, cy+s*0.2, s*0.45, s*0.38, 0, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.ellipse(cx-s*0.05, cy+s*0.18, s*0.25, s*0.08, -0.2, 0, Math.PI*2);
ctx.stroke();
ctx.beginPath();
ctx.ellipse(cx+s*0.05, cy+s*0.22, s*0.15, s*0.05, 0.3, 0, Math.PI*2);
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.55)';
ctx.beginPath();
ctx.ellipse(cx-s*0.2, cy+s*0.05, s*0.12, s*0.06, -0.4, 0, Math.PI*2);
ctx.fill();
var seatGrad = ctx.createLinearGradient(cx, cy-s*0.15, cx, cy+s*0.05);
seatGrad.addColorStop(0, '#f8fbfd');
seatGrad.addColorStop(1, '#d8e4ed');
ctx.fillStyle = seatGrad;
ctx.beginPath();
ctx.ellipse(cx, cy-s*0.05, s*0.88, s*0.38, 0, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#6a7f8f';
ctx.lineWidth = 1.8;
ctx.stroke();
ctx.strokeStyle = 'rgba(90,112,128,0.6)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.ellipse(cx, cy-s*0.03, s*0.62, s*0.48, 0, 0, Math.PI*2);
ctx.stroke();
ctx.save();
ctx.translate(cx, cy-s*0.1);
ctx.rotate(-0.35);
var lidGrad = ctx.createLinearGradient(-s*0.7, -s*0.3, s*0.7, s*0.1);
lidGrad.addColorStop(0, '#f0f5fa');
lidGrad.addColorStop(0.5, '#d0dde8');
lidGrad.addColorStop(1, '#b0c4d4');
ctx.fillStyle = lidGrad;
ctx.beginPath();
ctx.ellipse(0, -s*0.12, s*0.72, s*0.18, 0, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#5a7080';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.restore();
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.beginPath();
ctx.ellipse(cx-s*0.35, cy-s*0.15, s*0.3, s*0.07, -0.3, 0, Math.PI);
ctx.fill();
var shineGrad = ctx.createLinearGradient(cx-s*0.6, cy+s*0.2, cx-s*0.3, cy+s*0.4);
shineGrad.addColorStop(0, 'rgba(255,255,255,0.25)');
shineGrad.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = shineGrad;
ctx.fillRect(cx-s*0.7, cy-s*0.05, s*0.4, s*0.6);
}
function drawSnake(t, now){
var invincible = now < game.invincibleUntil;
var speedBoost = now < game.speedBoostUntil;
var pulse = game.pulseTime > 0 ? 1 + 0.15*(game.pulseTime/200) : 1;
if(game.snake.length > 3){
ctx.save();
var trailColor = invincible ? 'rgba(255,215,0,' : (speedBoost ? 'rgba(255,100,100,' : 'rgba(139,90,43,');
ctx.globalAlpha = (invincible?0.18:0.12) * (1 - t*0.5);
for(var ti=game.snake.length-1;ti>=0;ti--){
var ts = game.snake[ti];
var tix = ts.prevX + (ts.x - ts.prevX) * t;
var tiy = ts.prevY + (ts.y - ts.prevY) * t;
tix = tix*CELL + CELL/2;
tiy = tiy*CELL + CELL/2;
var trailSize = CELL * 0.9 * (1 - ti/game.snake.length*0.5);
var tg = ctx.createRadialGradient(tix, tiy, 0, tix, tiy, trailSize);
tg.addColorStop(0, trailColor + '0.5)');
tg.addColorStop(1, trailColor + '0)');
ctx.fillStyle = tg;
ctx.fillRect(tix-trailSize, tiy-trailSize, trailSize*2, trailSize*2);
}
ctx.restore();
}
for(var i=game.snake.length-1;i>=0;i--){
var seg = game.snake[i];
var ix = seg.prevX + (seg.x - seg.prevX) * t;
var iy = seg.prevY + (seg.y - seg.prevY) * t;
ix = (ix * CELL) + CELL/2;
iy = (iy * CELL) + CELL/2;
var isHead = i===0;
var baseSize = CELL * 0.85;
var size = isHead ? baseSize * pulse : baseSize;
var flash = invincible ? (Math.sin(now/80)>0 ? 1 : 0.55) : 1;
var color1, color2, color3;
if(invincible){
color1 = 'hsl(48, 100%, ' + (60+15*flash) + '%)';
color2 = 'hsl(38, 100%, ' + (50+10*flash) + '%)';
color3 = 'hsl(28, 100%, 40%)';
} else if(speedBoost){
var hue = (now/5)%360;
color1 = 'hsl(' + hue + ', 90%, 65%)';
color2 = 'hsl(' + ((hue+20)%360) + ', 90%, 55%)';
color3 = '#cc2233';
} else {
var skin = SKINS[game.skinKey] || SKINS.default;
var dark = Math.floor(i/2)%2===0;
color1 = dark ? skin.colors[0] : skin.colors[1];
color2 = skin.colors[2];
color3 = darkenHex(skin.colors[2], 30);
}
ctx.save();
if(invincible){ ctx.shadowColor = 'rgba(255,215,0,0.7)'; ctx.shadowBlur = 16; }
else if(speedBoost){ ctx.shadowColor = 'rgba(255,100,100,0.55)'; ctx.shadowBlur = 14; }
else{ ctx.shadowColor = 'rgba(0,0,0,0.22)'; ctx.shadowBlur = 5; }
ctx.fillStyle = color1;
roundRect(ctx, ix-size/2, iy-size/2, size, size, size*0.32);
ctx.fill();
ctx.restore();
ctx.fillStyle = color2;
roundRect(ctx, ix-size/2+3, iy-size/2+3, size-6, size*0.45, size*0.25);
ctx.fill();
ctx.fillStyle = color3;
ctx.fillRect(ix-size*0.25, iy+size*0.15, size*0.5, size*0.08);
if(isHead){
var d = game.dir;
var ex = ix + d.x*size*0.12;
var ey = iy + d.y*size*0.12;
var eo = size*0.18;
var eyeSize = size*0.16;
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(ex - d.y*eo, ey + d.x*eo, eyeSize, 0, Math.PI*2);
ctx.arc(ex + d.y*eo, ey - d.x*eo, eyeSize, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#1a1a1a';
ctx.beginPath();
ctx.arc(ex - d.y*eo + d.x*2, ey + d.x*eo + d.y*2, eyeSize*0.5, 0, Math.PI*2);
ctx.arc(ex + d.y*eo + d.x*2, ey - d.x*eo + d.y*2, eyeSize*0.5, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'rgba(255,150,170,0.55)';
ctx.beginPath();
ctx.arc(ix - size*0.22, ey + size*0.18, size*0.09, 0, Math.PI*2);
ctx.arc(ix + size*0.22, ey + size*0.18, size*0.09, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.2)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(ix, iy+size*0.05, size*0.12, 0.2, Math.PI-0.2);
ctx.stroke();
// 蛇头上方显示玩家名字
if(game.playerName && i===0){
var name = game.playerName;
ctx.save();
ctx.font = 'bold 12px Fredoka, sans-serif';
var nameWidth = ctx.measureText(name).width;
var tagW = nameWidth + 12;
var tagH = 16;
var tagX = ix - tagW/2;
var tagY = iy - size*0.75;
// 名字背景胶囊
ctx.fillStyle = 'rgba(255,158,181,0.92)';
roundRect(ctx, tagX, tagY, tagW, tagH, 8);
ctx.fill();
ctx.strokeStyle = 'rgba(255,122,158,0.6)';
ctx.lineWidth = 1.5;
ctx.stroke();
// 名字文字
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(name, ix, tagY + tagH/2);
// 小三角指向蛇头
ctx.fillStyle = 'rgba(255,158,181,0.92)';
ctx.beginPath();
ctx.moveTo(ix - 4, tagY + tagH);
ctx.lineTo(ix, tagY + tagH + 4);
ctx.lineTo(ix + 4, tagY + tagH);
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
}
}
function drawFood(now){
if(!game.food) return;
var bob = Math.sin(now/300) * 2;
var cx = game.food.x*CELL + CELL/2;
var cy = game.food.y*CELL + CELL/2 + bob;
var glow = ctx.createRadialGradient(cx, cy, 0, cx, cy, CELL*0.7);
glow.addColorStop(0, 'rgba(139,69,19,0.3)');
glow.addColorStop(1, 'transparent');
ctx.fillStyle = glow;
ctx.fillRect(cx-CELL, cy-CELL, CELL*2, CELL*2);
drawPoopEmoji(cx, cy, CELL*0.75, '#8b4513', '#5d2e0a');
}
function drawPoopEmoji(cx, cy, size, mainColor, darkColor){
var s = size;
ctx.fillStyle = mainColor;
ctx.beginPath();
ctx.ellipse(cx, cy - s*0.05, s*0.44, s*0.4, 0, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx, cy - s*0.3, s*0.32, s*0.27, 0, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(cx, cy - s*0.52, s*0.2, s*0.16, 0, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = darkColor;
ctx.beginPath();
ctx.ellipse(cx, cy + s*0.32, s*0.44, s*0.14, 0, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(cx - s*0.14, cy - s*0.4, s*0.065, 0, Math.PI*2);
ctx.arc(cx + s*0.14, cy - s*0.4, s*0.065, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#1a1a1a';
ctx.beginPath();
ctx.arc(cx - s*0.14, cy - s*0.4, s*0.032, 0, Math.PI*2);
ctx.arc(cx + s*0.14, cy - s*0.4, s*0.032, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'rgba(255,150,170,0.55)';
ctx.beginPath();
ctx.arc(cx - s*0.21, cy - s*0.3, s*0.055, 0, Math.PI*2);
ctx.arc(cx + s*0.21, cy - s*0.3, s*0.055, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,0.15)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cx - s*0.08, cy + s*0.05);
ctx.quadraticCurveTo(cx, cy + s*0.12, cx + s*0.08, cy + s*0.05);
ctx.stroke();
}
function drawSpecialFoods(now){
for(var i=0;i<game.specialFoods.length;i++){
var sf = game.specialFoods[i];
var elapsed = now - sf.spawnTime;
var life = sf.duration;
var remaining = 1 - elapsed/life;
var bob = Math.sin(now/200 + sf.x*0.7 + sf.y*0.5) * 3;
var cx = sf.x*CELL + CELL/2;
var cy = sf.y*CELL + CELL/2 + bob;
var color = specialColor(sf.type);
ctx.save();
var glow = ctx.createRadialGradient(cx, cy, 0, cx, cy, CELL*0.8);
glow.addColorStop(0, color + 'aa');
glow.addColorStop(0.4, color + '44');
glow.addColorStop(1, 'transparent');
ctx.fillStyle = glow;
ctx.fillRect(cx-CELL, cy-CELL, CELL*2, CELL*2);
var pulse = 0.85 + 0.15*Math.sin(now/120);
var alpha = remaining < 0.3 ? 0.35 + 0.65*Math.abs(Math.sin(now/60)) : 1;
ctx.globalAlpha = alpha;
ctx.shadowColor = color;
ctx.shadowBlur = 15;
// 所有特殊道具都用 emoji 大便形状,只是颜色不同
var darkColor = darkenHex(color, 40);
drawPoopEmoji(cx, cy, CELL*0.75*pulse, color, darkColor);
ctx.fillStyle = '#fff';
ctx.font = 'bold ' + Math.round(CELL*0.35) + 'px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('?', cx, cy + CELL*0.22);
ctx.shadowBlur = 0;
ctx.restore();
if(remaining < 0.5){
var barW = CELL * 0.6;
var barX = cx - barW/2;
var barY = cy - CELL*0.55;
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.fillRect(barX, barY, barW, 3);
ctx.fillStyle = color;
ctx.fillRect(barX, barY, barW * Math.max(0, remaining*2-0.5), 3);
}
}
}
function drawStar(cx, cy, r, color){
ctx.fillStyle = color;
ctx.beginPath();
for(var i=0;i<10;i++){
var angle = (Math.PI/5)*i - Math.PI/2;
var rad = i%2===0 ? r : r*0.45;
var x = cx + Math.cos(angle)*rad;
var y = cy + Math.sin(angle)*rad;
if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
}
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#b8960f';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.beginPath();
ctx.arc(cx-r*0.2, cy-r*0.2, r*0.15, 0, Math.PI*2);
ctx.fill();
}
function drawWaterDrop(cx, cy, r, color){
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(cx, cy-r);
ctx.bezierCurveTo(cx+r*1.1, cy-r*0.2, cx+r*0.6, cy+r*0.9, cx, cy+r);
ctx.bezierCurveTo(cx-r*0.6, cy+r*0.9, cx-r*1.1, cy-r*0.2, cx, cy-r);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.65)';
ctx.beginPath();
ctx.ellipse(cx-r*0.25, cy-r*0.15, r*0.18, r*0.28, -0.3, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = 'rgba(40,80,140,0.4)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(cx, cy-r);
ctx.bezierCurveTo(cx+r*1.1, cy-r*0.2, cx+r*0.6, cy+r*0.9, cx, cy+r);
ctx.stroke();
}
function drawBomb(cx, cy, r, color){
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(cx, cy+r*0.05, r*0.85, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#3a3a3a';
ctx.beginPath();
ctx.arc(cx, cy-r*0.25, r*0.18, 0, Math.PI*2);
ctx.fill();
ctx.strokeStyle = '#3a3a3a';
ctx.lineWidth = 3.5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(cx, cy-r*0.25);
ctx.quadraticCurveTo(cx+r*0.2, cy-r*0.55, cx+r*0.45, cy-r*0.55);
ctx.stroke();
ctx.lineCap = 'butt';
ctx.fillStyle = '#ff6600';
ctx.beginPath();
ctx.arc(cx+r*0.48, cy-r*0.55, r*0.12, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#ffdd00';
ctx.beginPath();
ctx.arc(cx+r*0.48, cy-r*0.55, r*0.06, 0, Math.PI*2);
ctx.fill();
}
function drawLightning(cx, cy, r, color){
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(cx-r*0.35, cy-r);
ctx.lineTo(cx+r*0.25, cy-r*0.25);
ctx.lineTo(cx-r*0.15, cy-r*0.05);
ctx.lineTo(cx+r*0.45, cy+r);
ctx.lineTo(cx-r*0.05, cy+r*0.15);
ctx.lineTo(cx+r*0.15, cy);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#aa3333';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.beginPath();
ctx.moveTo(cx-r*0.15, cy-r*0.5);
ctx.lineTo(cx+r*0.05, cy-r*0.3);
ctx.lineTo(cx-r*0.05, cy-r*0.1);
ctx.closePath();
ctx.fill();
}
function drawEnemySnakes(now){
for(var e=0;e<game.enemySnakes.length;e++){
var es = game.enemySnakes[e];
if(!es.segments || es.segments.length===0) continue;
var age = now - es.spawnTime;
var lifeFrac = 1 - age/es.lifeTime;
var alpha = lifeFrac < 0.25 ? (0.3 + 0.7*Math.abs(Math.sin(now/60))) : 1;
ctx.globalAlpha = alpha;
for(var i=0;i<es.segments.length;i++){
var seg = es.segments[i];
var ix, iy;
if(es.prevSegments && es.prevSegments[i]){
var p = es.prevSegments[i];
ix = p.x + (seg.x - p.x) * 0.5;
iy = p.y + (seg.y - p.y) * 0.5;
} else {
ix = seg.x;
iy = seg.y;
}
ix = ix*CELL + CELL/2;
iy = iy*CELL + CELL/2;
var size = CELL * 0.8;
var isHead = i===0;
var mainColor = isHead ? '#6b4226' : '#8b5a2b';
var darkColor = isHead ? '#4a2a15' : '#6b4226';
ctx.save();
ctx.shadowColor = 'rgba(100,60,30,0.5)';
ctx.shadowBlur = 8;
ctx.fillStyle = mainColor;
roundRect(ctx, ix-size/2, iy-size/2, size, size, size*0.28);
ctx.fill();
ctx.restore();
ctx.fillStyle = darkColor;
roundRect(ctx, ix-size/2+2, iy-size/2+2, size-4, size*0.4, size*0.22);
ctx.fill();
if(isHead){
ctx.fillStyle = '#ff0';
ctx.beginPath();
ctx.arc(ix-5, iy-3, 3, 0, Math.PI*2);
ctx.arc(ix+5, iy-3, 3, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(ix-5, iy-3, 1.5, 0, Math.PI*2);
ctx.arc(ix+5, iy-3, 1.5, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = 'rgba(255,100,100,0.6)';
ctx.beginPath();
ctx.arc(ix, iy+4, 4, 0, Math.PI, true);
ctx.fill();
}
}
ctx.globalAlpha = 1;
}
}
function drawParticles(){
for(var i=0;i<game.particles.length;i++){
var p = game.particles[i];
var alpha = Math.max(0, p.life / p.maxLife);
ctx.globalAlpha = alpha;
if(p.sparkle){
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.vx * 0.02);
ctx.fillStyle = p.color;
ctx.fillRect(-p.size*0.5, -p.size*0.5, p.size, p.size);
ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')';
ctx.fillRect(-p.size*0.2, -p.size*0.2, p.size*0.4, p.size*0.4);
ctx.restore();
} else {
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
ctx.fill();
}
}
ctx.globalAlpha = 1;
}
function drawFloatTexts(){
for(var i=0;i<game.floatTexts.length;i++){
var f = game.floatTexts[i];
var alpha = Math.max(0, f.life / 1000);
ctx.globalAlpha = alpha;
ctx.fillStyle = f.color;
ctx.font = 'bold ' + f.size + 'px Fredoka, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.strokeStyle = 'rgba(255,255,255,0.9)';
ctx.lineWidth = 3;
ctx.strokeText(f.text, f.x, f.y);
ctx.fillText(f.text, f.x, f.y);
}
ctx.globalAlpha = 1;
}
function roundRect(ctx, x, y, w, h, r){
ctx.beginPath();
ctx.moveTo(x+r, y);
ctx.lineTo(x+w-r, y);
ctx.quadraticCurveTo(x+w, y, x+w, y+r);
ctx.lineTo(x+w, y+h-r);
ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h);
ctx.lineTo(x+r, y+h);
ctx.quadraticCurveTo(x, y+h, x, y+h-r);
ctx.lineTo(x, y+r);
ctx.quadraticCurveTo(x, y, x+r, y);
ctx.closePath();
}
function darkenHex(hex, amount){
hex = hex.replace('#','');
var r = Math.max(0, parseInt(hex.substr(0,2),16) - amount);
var g = Math.max(0, parseInt(hex.substr(2,2),16) - amount);
var b = Math.max(0, parseInt(hex.substr(4,2),16) - amount);
return 'rgb('+r+','+g+','+b+')';
}
function setDir(nx, ny){
if(!game || game.state!==STATE.PLAYING) return;
if(game.dir.x === -nx && game.dir.y === -ny) return;
if(game.dir.x === nx && game.dir.y === ny) return;
game.nextDir = {x:nx, y:ny};
}
var diffOptions = document.getElementById('diffOptions');
for(var i=1;i<=10;i++){
(function(val){
var btn = document.createElement('button');
btn.className = 'diff-btn';
btn.textContent = val;
btn.dataset.val = val;
btn.addEventListener('click', function(){
var btns = document.querySelectorAll('.diff-btn');
for(var b=0;b<btns.length;b++) btns[b].classList.remove('selected');
btn.classList.add('selected');
diffOptions.dataset.selected = val;
});
diffOptions.appendChild(btn);
})(i);
}
diffOptions.dataset.selected = '1';
diffOptions.querySelector('[data-val="1"]').classList.add('selected');
var ctrlSelect = document.getElementById('ctrlSelect');
if(ctrlSelect){
var ctrlBtns = ctrlSelect.querySelectorAll('.diff-btn');
for(var ci=0; ci<ctrlBtns.length; ci++){
ctrlBtns[ci].addEventListener('click', (function(btn){
return function(){
for(var cj=0; cj<ctrlBtns.length; cj++) ctrlBtns[cj].classList.remove('active');
btn.classList.add('active');
ctrlSelect.dataset.selected = btn.dataset.ctrl;
};
})(ctrlBtns[ci]));
}
ctrlSelect.dataset.selected = ctrlBtns[0].dataset.ctrl;
}
document.getElementById('startBtn').addEventListener('click', function(){
document.getElementById('startOverlay').classList.add('hidden');
initGame();
});
document.getElementById('pauseBtn').addEventListener('click', function(){
if(game && game.state===STATE.PLAYING){
game.state = STATE.PAUSED;
document.getElementById('pauseOverlay').classList.remove('hidden');
}
});
document.getElementById('soundBtn').addEventListener('click', function(){
game.soundMuted = !game.soundMuted;
var btn = document.getElementById('soundBtn');
if(game.soundMuted){
btn.classList.add('muted');
btn.textContent = '🔇';
} else {
btn.classList.remove('muted');
btn.textContent = '🔊';
}
});
document.getElementById('resumeBtn').addEventListener('click', function(){
if(game && game.state===STATE.PAUSED){
game.state = STATE.PLAYING;
game.lastTime = performance.now();
document.getElementById('pauseOverlay').classList.add('hidden');
}
});
document.getElementById('restartBtn').addEventListener('click', function(){
document.getElementById('pauseOverlay').classList.add('hidden');
document.getElementById('startOverlay').classList.remove('hidden');
game = null;
});
document.getElementById('playAgainBtn').addEventListener('click', function(){
document.getElementById('gameOverOverlay').classList.add('hidden');
initGame();
});
document.getElementById('backToMenuBtn').addEventListener('click', function(){
document.getElementById('gameOverOverlay').classList.add('hidden');
document.getElementById('startOverlay').classList.remove('hidden');
game = null;
});
document.addEventListener('keydown', function(e){
var k = e.key.toLowerCase();
if(k==='arrowup'||k==='w'){ setDir(0,-1); e.preventDefault(); }
else if(k==='arrowdown'||k==='s'){ setDir(0,1); e.preventDefault(); }
else if(k==='arrowleft'||k==='a'){ setDir(-1,0); e.preventDefault(); }
else if(k==='arrowright'||k==='d'){ setDir(1,0); e.preventDefault(); }
else if(k===' '||k==='p'){
e.preventDefault();
if(game && game.state===STATE.PLAYING){
game.state = STATE.PAUSED;
document.getElementById('pauseOverlay').classList.remove('hidden');
} else if(game && game.state===STATE.PAUSED){
game.state = STATE.PLAYING;
game.lastTime = performance.now();
document.getElementById('pauseOverlay').classList.add('hidden');
}
}
});
var dpadBtns = document.querySelectorAll('.dpad-btn');
for(var di=0;di<dpadBtns.length;di++){
(function(btn){
var dir = btn.dataset.dir;
var active = false;
var holdInterval = null;
var start = function(e){
e.preventDefault();
active = true;
btn.classList.add('active');
if(dir==='up') setDir(0,-1);
else if(dir==='down') setDir(0,1);
else if(dir==='left') setDir(-1,0);
else if(dir==='right') setDir(1,0);
holdInterval = setInterval(function(){
if(!active) return;
if(dir==='up') setDir(0,-1);
else if(dir==='down') setDir(0,1);
else if(dir==='left') setDir(-1,0);
else if(dir==='right') setDir(1,0);
}, 180);
};
var end = function(){
active = false;
btn.classList.remove('active');
if(holdInterval){ clearInterval(holdInterval); holdInterval = null; }
};
btn.addEventListener('touchstart', start, {passive:false});
btn.addEventListener('touchend', end);
btn.addEventListener('touchcancel', end);
btn.addEventListener('mousedown', start);
btn.addEventListener('mouseup', end);
btn.addEventListener('mouseleave', end);
})(dpadBtns[di]);
}
document.addEventListener('visibilitychange', function(){
if(document.hidden && game && game.state===STATE.PLAYING){
game.state = STATE.PAUSED;
document.getElementById('pauseOverlay').classList.remove('hidden');
}
});
var touchStartX = 0, touchStartY = 0, touchActive = false;
document.addEventListener('touchstart', function(e){
if(game.state!==STATE.PLAYING) return;
if(game.controlType !== 'swipe') return;
var t = e.touches[0];
touchStartX = t.clientX;
touchStartY = t.clientY;
touchActive = true;
}, {passive:true});
document.addEventListener('touchmove', function(e){
if(!touchActive || game.controlType !== 'swipe') return;
var t = e.touches[0];
var dx = t.clientX - touchStartX;
var dy = t.clientY - touchStartY;
var threshold = 25;
if(Math.abs(dx) > threshold || Math.abs(dy) > threshold){
if(Math.abs(dx) > Math.abs(dy)){
setDir(dx > 0 ? 1 : -1, 0);
} else {
setDir(0, dy > 0 ? 1 : -1);
}
touchStartX = t.clientX;
touchStartY = t.clientY;
}
}, {passive:true});
document.addEventListener('touchend', function(){ touchActive = false; }, {passive:true});
function vibrate(ms){
if(navigator.vibrate){
try{ navigator.vibrate(ms); }catch(e){}
}
}
var origTriggerGameOver = triggerGameOver;
triggerGameOver = function(reason){
vibrate([80, 40, 120]);
origTriggerGameOver(reason);
};
var origStepSnake = stepSnake;
stepSnake = function(now){
var lenBefore = game.snake ? game.snake.length : 3;
origStepSnake(now);
if(game && game.snake && game.snake.length > lenBefore){
vibrate(20);
}
};
game = makeGame();
game.highScore = parseInt(localStorage.getItem(LS_KEY)||'0',10);
document.getElementById('highScore').textContent = game.highScore;
document.getElementById('coinsDisplay').textContent = game.coins;
function openShop(){
if(!game){ game = makeGame(); }
document.getElementById('shopOverlay').classList.remove('hidden');
document.getElementById('shopTabSkins').classList.add('active');
document.getElementById('shopTabUpgrades').classList.remove('active');
document.getElementById('shopSkinsPanel').style.display = 'block';
document.getElementById('shopUpgradesPanel').style.display = 'none';
renderShop();
}
function closeShop(){
document.getElementById('shopOverlay').classList.add('hidden');
}
function renderShop(){
if(!game){ game = makeGame(); }
game.coins = parseInt(localStorage.getItem('poop_snake_coins')||'0',10);
document.getElementById('shopCoinsDisplay').textContent = '💰 ' + game.coins;
renderSkinsPanel();
renderUpgradesPanel();
}
function renderSkinsPanel(){
var panel = document.getElementById('shopSkinsPanel');
var purchased = getPurchasedSkins();
var selected = localStorage.getItem('poop_snake_selected_skin') || 'default';
var html = '';
for(var key in SKINS){
if(!SKINS.hasOwnProperty(key)) continue;
var skin = SKINS[key];
var owned = key==='default' || purchased.indexOf(key) >= 0;
var isSelected = selected === key;
var colorsPreview = '';
for(var ci=0; ci<skin.colors.length; ci++){
colorsPreview += '<span style="background:' + skin.colors[ci] + '"></span>';
}
html += '<div class="shop-item">' +
'<div class="shop-item-info">' +
'<div class="shop-color-preview">' + colorsPreview + '</div>' +
'<div>' +
'<div class="shop-item-name">' + skin.name + '</div>' +
'<div class="shop-item-desc">价格: ' + (skin.price===0?'免费':skin.price+' 金币') + '</div>' +
'</div>' +
'</div>' +
'<div>';
if(isSelected){
html += '<span class="shop-owned-tag">✓ 使用中</span>';
} else if(owned){
html += '<button class="shop-buy-btn shop-select-btn" data-skin="' + key + '">选择</button>';
} else {
var canAfford = game.coins >= skin.price;
html += '<button class="shop-buy-btn" data-buy-skin="' + key + '"' + (!canAfford?' disabled':'') + '>购买</button>';
}
html += '</div></div>';
}
panel.innerHTML = html;
var buyBtns = panel.querySelectorAll('[data-buy-skin]');
for(var i=0; i<buyBtns.length; i++){
buyBtns[i].addEventListener('click', function(){
var key = this.getAttribute('data-buy-skin');
buySkin(key);
});
}
var selectBtns = panel.querySelectorAll('[data-skin]');
for(var j=0; j<selectBtns.length; j++){
selectBtns[j].addEventListener('click', function(){
var key = this.getAttribute('data-skin');
setSelectedSkin(key);
game.skinKey = key;
renderShop();
});
}
}
function buySkin(key){
var skin = SKINS[key];
if(!skin) return;
if(game.coins < skin.price) return;
game.coins -= skin.price;
saveCoins();
var purchased = getPurchasedSkins();
if(purchased.indexOf(key) < 0){
purchased.push(key);
savePurchasedSkins(purchased);
}
setSelectedSkin(key);
game.skinKey = key;
renderShop();
}
function renderUpgradesPanel(){
var panel = document.getElementById('shopUpgradesPanel');
var level = parseInt(localStorage.getItem('poop_snake_upgrade')||'0',10);
var html = '<div style="color:#fff;font-weight:600;margin-bottom:10px">当前等级: ' + level + ' / ' + UPGRADE_COSTS.length + '</div>';
html += '<div style="color:#c0dde8;font-size:12px;margin-bottom:10px">每级提供 1 次抵挡伤害 (护盾)</div>';
for(var i=0; i<UPGRADE_COSTS.length; i++){
var cost = UPGRADE_COSTS[i];
var lv = i + 1;
var isCurrent = level >= lv;
var isNext = level + 1 === lv;
var canAfford = game.coins >= cost;
html += '<div class="shop-item">' +
'<div class="shop-item-info">' +
'<div style="width:32px;height:32px;border-radius:50%;background:' + (isCurrent?'linear-gradient(135deg,#ffd700,#daa520)':'rgba(255,255,255,0.2)') + ';display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:14px">' + lv + '</div>' +
'<div>' +
'<div class="shop-item-name">等级 ' + lv + ' (' + cost + ' 金币)</div>' +
'<div class="shop-item-desc">' + (isCurrent?'✓ 已解锁':'获得 '+lv+' 次护盾') + '</div>' +
'</div>' +
'</div>' +
'<div>';
if(isCurrent){
html += '<span class="shop-owned-tag">✓</span>';
} else if(isNext){
html += '<button class="shop-buy-btn" data-upgrade="' + i + '"' + (!canAfford?' disabled':'') + '>升级</button>';
} else {
html += '<span style="color:#888;font-size:12px">锁定</span>';
}
html += '</div></div>';
}
panel.innerHTML = html;
var upBtns = panel.querySelectorAll('[data-upgrade]');
for(var j=0; j<upBtns.length; j++){
upBtns[j].addEventListener('click', function(){
var idx = parseInt(this.getAttribute('data-upgrade'), 10);
buyUpgrade(idx);
});
}
}
function buyUpgrade(idx){
var cost = UPGRADE_COSTS[idx];
if(game.coins < cost) return;
game.coins -= cost;
saveCoins();
var newLevel = idx + 1;
saveUpgradeLevel(newLevel);
game.upgradeLevel = newLevel;
renderShop();
}
document.getElementById('shopBtn').addEventListener('click', openShop);
document.getElementById('shopBtnStart').addEventListener('click', openShop);
document.getElementById('shopBtnGameOver').addEventListener('click', openShop);
document.getElementById('closeShopBtn').addEventListener('click', closeShop);
document.getElementById('shopTabSkins').addEventListener('click', function(){
this.classList.add('active');
document.getElementById('shopTabUpgrades').classList.remove('active');
document.getElementById('shopSkinsPanel').style.display = 'block';
document.getElementById('shopUpgradesPanel').style.display = 'none';
});
document.getElementById('shopTabUpgrades').addEventListener('click', function(){
this.classList.add('active');
document.getElementById('shopTabSkins').classList.remove('active');
document.getElementById('shopUpgradesPanel').style.display = 'block';
document.getElementById('shopSkinsPanel').style.display = 'none';
});
// ============ 测试函数 ============
window.testSpawnWarning = function(){
if(!game) return;
// 在地图中央生成一个警告
var x = Math.floor(COLS/2);
var y = Math.floor(ROWS/2);
// 避开蛇头
if(game.snake && game.snake[0]){
var hx = game.snake[0].x, hy = game.snake[0].y;
if(Math.abs(x-hx)<2 && Math.abs(y-hy)<2){
x = (x+3) % COLS;
}
}
game.warnings.push({x:x, y:y, state:'warn', stateTime:0, warnDuration:1500, countDuration:3000, fallDuration:400, fallY:-1});
console.log('🧪 测试:警告已生成 at', x, y);
};
window.testSpawnCountdown = function(){
if(!game) return;
var x = Math.floor(COLS/2) + 3;
var y = Math.floor(ROWS/2);
if(game.snake && game.snake[0]){
var hx = game.snake[0].x, hy = game.snake[0].y;
if(Math.abs(x-hx)<2 && Math.abs(y-hy)<2){ x = (x+4) % COLS; }
}
game.warnings.push({x:x, y:y, state:'countdown', stateTime:0, warnDuration:1500, countDuration:3000, fallDuration:400, fallY:-1});
console.log('🧪 测试:倒计时已生成 at', x, y);
};
window.testSpawnFalling = function(){
if(!game) return;
var x = Math.floor(COLS/2);
var y = Math.floor(ROWS/2) + 3;
if(game.snake && game.snake[0]){
var hx = game.snake[0].x, hy = game.snake[0].y;
if(Math.abs(x-hx)<2 && Math.abs(y-hy)<2){ y = (y+4) % ROWS; }
}
game.warnings.push({x:x, y:y, state:'falling', stateTime:0, warnDuration:1500, countDuration:3000, fallDuration:600, fallY:-CELL*2});
console.log('🧪 测试:马桶落下已生成 at', x, y);
};
window.testFullSequence = function(){
if(!game) return;
// 模拟完整流程:警告→倒计时→落下
var x = Math.floor(COLS/2) + 5;
var y = Math.floor(ROWS/2);
if(game.snake && game.snake[0]){
var hx = game.snake[0].x, hy = game.snake[0].y;
if(Math.abs(x-hx)<3 && Math.abs(y-hy)<3){ x = (x+6) % COLS; y = (y+3) % ROWS; }
}
game.warnings.push({x:x, y:y, state:'warn', stateTime:0, warnDuration:1000, countDuration:2000, fallDuration:500, fallY:-1});
console.log('🧪 测试:完整流程已生成 at', x, y);
};
window.testClearAll = function(){
if(!game) return;
game.warnings = [];
game.toilets = [];
console.log('🧪 测试:已清空所有警告和马桶');
};
window.testRandomWave = function(){
if(!game) return;
// 模拟难度5的一波
var d = 5;
for(var i=0;i<d*3;i++){
var pos = null, tries = 0;
while(tries < 100){
pos = { x: Math.floor(Math.random()*COLS), y: Math.floor(Math.random()*ROWS) };
tries++;
if(!isOccupied(pos.x,pos.y,true,true) && !isTooCloseToSnake(pos) && !isWarningAt(pos.x,pos.y)){
break;
}
pos = null;
}
if(pos){
game.warnings.push({x:pos.x, y:pos.y, state:'warn', stateTime:0, warnDuration:800+Math.random()*800, countDuration:2000+Math.random()*1000, fallDuration:400+Math.random()*200, fallY:-1});
}
}
console.log('🧪 测试:随机一波已生成');
};
// 设置难度并测试
window.testSetDifficulty = function(d){
if(!game) return;
game.difficulty = d;
game.toiletWaveInterval = 5000;
game.toiletSpawnQueue = [];
game.toiletSpawnTimer = 0;
console.log('🧪 测试:难度已设置为 ' + d + ', 每波数量: ' + (d*4) + '个, 间隔: 5秒, 随机分散掉落');
var el = document.getElementById('difficulty');
if(el) el.textContent = d;
};
// 难度10级测试 - 快速生成多波马桶验证
window.testDifficulty10 = function(){
if(!game) return;
game.difficulty = 10;
game.toiletWaveInterval = 5000;
game.currentWave = 0;
console.log('========== 难度10级测试开始 ==========');
console.log('配置: 难度=10, 每波数量=' + (10*4) + '个, 间隔=5秒');
console.log('警告时间: warn=1s + countdown=2s = 总计3秒');
console.log('');
// 立即生成第一波
var wave1Count = 10 * 4;
console.log('第1波: 准备生成 ' + wave1Count + ' 个马桶...');
var w1 = spawnTestWave(wave1Count);
console.log('第1波完成: 成功生成 ' + w1.success + ' 个警告, 失败 ' + w1.fail + ' 个');
// 延迟生成后续波
var testResults = [{wave:1, count:w1.success, time:Date.now()}];
var waveNum = 2;
var interval = setInterval(function(){
if(waveNum > 3 || game.state === STATE.OVER){
clearInterval(interval);
console.log('');
console.log('========== 测试结果汇总 ==========');
console.log('总波数: ' + testResults.length);
for(var r of testResults){
console.log('第' + r.wave + '波: ' + r.count + ' 个马桶警告');
}
console.log('总计生成: ' + testResults.reduce(function(s,r){return s+r.count;},0) + ' 个警告');
console.log('预期每波: 40个 (难度10 × 4)');
console.log('警告状态检查:');
var warnState = {warn:0, countdown:0, falling:0};
for(var w of game.warnings){
if(warnState[w.state] !== undefined) warnState[w.state]++;
}
console.log(' warn(警告): ' + warnState.warn + ' 个');
console.log(' countdown(倒计时): ' + warnState.countdown + ' 个');
console.log(' falling(落下中): ' + warnState.falling + ' 个');
console.log('当前已落地马桶: ' + game.toilets.length + ' 个');
console.log('===================================');
return;
}
var count = 10 * 4;
console.log('第' + waveNum + '波: 生成 ' + count + ' 个马桶...');
var result = spawnTestWave(count);
testResults.push({wave: waveNum, count: result.success, time: Date.now()});
waveNum++;
}, 500); // 0.5秒间隔快速测试
document.getElementById('difficulty').textContent = 10;
};
// 辅助函数:生成一波测试马桶
function spawnTestWave(count){
var success = 0;
var fail = 0;
for(var i=0;i<count;i++){
var pos = null;
var tries = 0;
while(tries < 100){
pos = { x: Math.floor(Math.random()*COLS), y: Math.floor(Math.random()*ROWS) };
tries++;
if(!isOccupied(pos.x,pos.y,true,true) && !isTooCloseToSnake(pos) && !isWarningAt(pos.x,pos.y)){
break;
}
pos = null;
}
if(pos){
game.warnings.push({
x: pos.x,
y: pos.y,
state: 'warn',
stateTime: 0,
warnDuration: 1000,
countDuration: 2000,
fallDuration: 400,
fallY: -1,
spawnTime: Date.now()
});
success++;
} else {
fail++;
}
}
return {success: success, fail: fail};
}
// 强制生成一波马桶(忽略碰撞检测)
window.testForceWave = function(d){
if(!game) return;
d = d || 10;
var count = d * 4;
console.log('强制生成难度' + d + '的一波: ' + count + '个马桶');
var success = 0;
for(var i=0;i<count;i++){
var x = Math.floor(Math.random()*COLS);
var y = Math.floor(Math.random()*ROWS);
game.warnings.push({
x: x, y: y, state: 'warn', stateTime: 0,
warnDuration: 1000, countDuration: 2000, fallDuration: 400, fallY: -1,
spawnTime: Date.now()
});
success++;
}
console.log('成功生成: ' + success + ' 个警告');
};
// 统计当前测试状态
window.testCheckStatus = function(){
if(!game) return;
var status = {
difficulty: game.difficulty,
waveInterval: game.toiletWaveInterval + 'ms',
warnings: game.warnings.length,
toilets: game.toilets.length,
warnState: {warn:0, countdown:0, falling:0}
};
for(var w of game.warnings){
if(status.warnState[w.state] !== undefined) status.warnState[w.state]++;
}
console.log('========== 游戏状态检查 ==========');
console.log('难度: ' + status.difficulty);
console.log('波次间隔: ' + status.waveInterval);
console.log('警告总数: ' + status.warnings);
console.log(' - warn(橙色): ' + status.warnState.warn);
console.log(' - countdown(红色): ' + status.warnState.countdown);
console.log(' - falling(落下中): ' + status.warnState.falling);
console.log('已落地马桶: ' + status.toilets);
console.log('==================================');
return status;
};
// 显示/隐藏测试面板
window.toggleTestPanel = function(){
var panel = document.getElementById('testPanel');
var toggle = document.getElementById('testToggle');
if(panel.classList.contains('visible')){
panel.classList.remove('visible');
toggle.classList.remove('visible');
} else {
panel.classList.add('visible');
toggle.classList.add('visible');
}
};
// 初始化测试面板
document.getElementById('testToggle').addEventListener('click', window.toggleTestPanel);
// 添加测试快捷键
document.addEventListener('keydown', function(e){
if(!game || game.state !== STATE.PLAYING) return;
var k = e.key.toLowerCase();
if(k === 'h'){
e.preventDefault();
window.toggleTestPanel();
} else if(k === 'w'){
e.preventDefault();
window.testSpawnWarning();
} else if(k === 'e'){
e.preventDefault();
window.testSpawnCountdown();
} else if(k === 'r'){
e.preventDefault();
window.testSpawnFalling();
} else if(k === 't'){
e.preventDefault();
window.testFullSequence();
} else if(k === 'q'){
e.preventDefault();
window.testRandomWave();
} else if(k === 'c'){
e.preventDefault();
window.testClearAll();
} else if(k === 'd'){
e.preventDefault();
window.testDifficulty10();
} else if(k === 's'){
e.preventDefault();
window.testCheckStatus();
}
});
console.log('🧪 测试模式已激活!按 H 显示测试面板,或直接按 W/E/R/T/Q/C/D/S 快捷键');
})();
</script>
</body>
</html>Game Source: 便便蛇蛇 - Poop Snake
Creator: RocketWolf27
Libraries: none
Complexity: complex (2728 lines, 99.4 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: poop-snake-rocketwolf27" to link back to the original. Then publish at arcadelab.ai/publish.