/* 贪蛇出动 - 主控:状态机 / 游戏循环 / 击杀与死亡 / 复活 / 服务端数据同步 */ (function () { 'use strict'; window.GS = window.GS || {}; const U = GS.U; const API = '/api/greedy_snake'; class Game { constructor() { this.ui = new GS.UI(); this.render = new GS.Render(document.getElementById('gs-canvas')); this.input = new GS.Input(); this.state = 'idle'; // idle / roulette / playing / dead / result this.stats = { gems: GS.CFG.initialGems, totalCoins: 0, best: 0, todayBest: null }; this.player = null; this.bots = []; this.ais = new Map(); this.world = null; this.selectedItem = -1; this.reelKeys = []; this.freeReviveUsed = false; this.treasureDone = false; this._last = 0; this._boardTimer = 0; this._respawnQueue = []; this._killerName = ''; this._reviveTimer = null; this._bindUI(); } async init() { // 拉取服务端配置与存档 const cfg = await this.ui.trackFetch('加载游戏配置', API + '/config'); if (cfg && cfg.ok && cfg.data) { Object.assign(GS.CFG, { worldSize: cfg.data.world_size || GS.CFG.worldSize, botCount: cfg.data.bot_count || GS.CFG.botCount, coinCount: cfg.data.coin_count || GS.CFG.coinCount, baseSpeed: cfg.data.base_speed || GS.CFG.baseSpeed, boostSpeed: cfg.data.boost_speed || GS.CFG.boostSpeed, treasureTarget: cfg.data.treasure_target || GS.CFG.treasureTarget, rerollCost: cfg.data.reroll_cost || GS.CFG.rerollCost, initialGems: cfg.data.initial_gems || GS.CFG.initialGems, reviveCountdown: cfg.data.revive_countdown || GS.CFG.reviveCountdown, playerName: cfg.data.player_name || GS.CFG.playerName }); this.ui.setUiMode(cfg.data.ui_mode || 'debug'); } const st = await this.ui.trackFetch('加载存档', API + '/stats'); if (st && st.ok && st.data) this.stats = Object.assign(this.stats, st.data); document.getElementById('gs-progress-target').textContent = GS.CFG.treasureTarget; this._openRoulette(); requestAnimationFrame(t => this._loop(t)); } /* ---------------- 转盘阶段 ---------------- */ _openRoulette() { this.state = 'roulette'; this.input.setVisible(false); this.ui.showHud(false); this.ui.hideResult(); this.ui.hideRevive(); this.selectedItem = -1; this._rollReels(); this.ui.showRoulette(this.stats.gems); } _rollReels() { const pool = GS.ITEM_KEYS.slice(); this.reelKeys = [U.pick(pool), U.pick(pool), U.pick(pool)]; this.ui.spinReels(this.reelKeys); setTimeout(() => this.ui.setReels(this.reelKeys, this.selectedItem), 900); } _bindUI() { // 转盘:选择道具 / 换一换 / 直接开始 document.querySelectorAll('.gs-reel').forEach(reel => { reel.addEventListener('click', () => { const idx = Number(reel.dataset.idx); this.selectedItem = this.selectedItem === idx ? -1 : idx; this.ui.setReels(this.reelKeys, this.selectedItem); }); }); document.getElementById('gs-reroll').addEventListener('click', async () => { if (this.stats.gems < GS.CFG.rerollCost) { this.ui.toast('钻石不足', 'error'); return; } const btn = document.getElementById('gs-reroll'); btn.disabled = true; this.stats.gems -= GS.CFG.rerollCost; this.ui.updateGems(this.stats.gems); this._saveStats({ gems: this.stats.gems }); this.selectedItem = -1; this._rollReels(); }); document.getElementById('gs-roulette-skip').addEventListener('click', () => this._startGame(null)); document.getElementById('gs-reels').addEventListener('dblclick', () => { if (this.selectedItem >= 0) this._startGame(this.reelKeys[this.selectedItem]); }); // 复活 document.getElementById('gs-revive-btn').addEventListener('click', () => this._revive()); document.getElementById('gs-revive-close').addEventListener('click', () => this._toResult()); // 结算 document.getElementById('gs-result-again').addEventListener('click', () => this._openRoulette()); document.getElementById('gs-result-home').addEventListener('click', () => this._openRoulette()); // 弹窗 document.getElementById('gs-dialog-close').addEventListener('click', () => this.ui.hideDialog()); document.getElementById('gs-dialog-copy').addEventListener('click', () => { const txt = document.getElementById('gs-dialog-body').textContent; if (navigator.clipboard) navigator.clipboard.writeText(txt).then(() => this.ui.toast('已复制', 'success')); }); } /* ---------------- 开局 ---------------- */ _startGame(itemKey) { this.ui.hideRoulette(); this.world = new GS.World(); this.bots = []; this.ais.clear(); this._respawnQueue = []; this.freeReviveUsed = false; this.treasureDone = false; // 玩家 const px = U.rand(800, GS.CFG.worldSize - 800); const py = U.rand(800, GS.CFG.worldSize - 800); this.player = new GS.Snake({ id: 'p', name: GS.CFG.playerName, isPlayer: true, color: GS.PLAYER_COLOR, x: px, y: py }); this._applyItem(this.player, itemKey); // AI for (let i = 0; i < GS.CFG.botCount; i++) this._spawnBot(); this.state = 'playing'; this.input.setVisible(true); this.ui.showHud(true); if (itemKey) this.ui.toast('已装备:' + GS.ITEMS[itemKey].name, 'success'); } _applyItem(s, key) { if (!key) return; const now = performance.now(); if (key === 'helmet') s.shield = true; if (key === 'rocket') s.invincibleUntil = now + 4000; if (key === 'magnet') s.magnetUntil = now + 20000; if (key === 'expcard') s.grow(200); } _spawnBot() { const b = new GS.Snake({ id: 'b' + Math.random().toString(36).slice(2, 7), name: U.pick(GS.BOT_NAMES), color: U.pick(GS.SNAKE_COLORS), skin: Math.random() < 0.5 ? 1 : 0, x: U.rand(300, GS.CFG.worldSize - 300), y: U.rand(300, GS.CFG.worldSize - 300), score: U.randInt(10, 400) }); this.bots.push(b); this.ais.set(b.id, new GS.AI(b)); } /* ---------------- 主循环 ---------------- */ _loop(t) { const dt = Math.min(0.05, (t - this._last) / 1000 || 0.016); this._last = t; if (this.state === 'playing' || this.state === 'dead') this._step(dt, t); if (this.world) this.render.draw(this.world, this._allSnakes(), this.player, t); requestAnimationFrame(tt => this._loop(tt)); } _allSnakes() { return this.player ? [this.player, ...this.bots] : this.bots; } _step(dt, now) { const p = this.player; // 玩家输入 if (this.state === 'playing' && !p.dead) { p.targetAngle = this.input.angle; const wantBoost = this.input.boost; p.boosting = wantBoost && p.score > 15; if (p.boosting) { p.score = Math.max(10, p.score - 6 * dt); // 加速消耗长度 if (Math.random() < dt * 2) this.world.addCoin( p.head.x - Math.cos(p.angle) * p.radius * 2, p.head.y - Math.sin(p.angle) * p.radius * 2, 0); } } // AI 决策 for (const b of this.bots) this.ais.get(b.id).update(dt, this.world, this._allSnakes()); // 移动 for (const s of this._allSnakes()) s.update(dt, now); // 金币/磁铁 this.world.update(dt, this._allSnakes(), now); // 相机 if (!p.dead) this.world.updateCam(p.head, this.render.w, this.render.h, dt); // 碰撞与死亡 const snakes = this._allSnakes(); for (const s of snakes) { if (s.dead) continue; const hit = this.world.collide(s, snakes); if (!hit) continue; if (s.shield) { s.shield = false; s.invincibleUntil = now + 1500; continue; } if (s.invincible(now)) continue; this._onDeath(s, hit, now); } // 复活队列 for (let i = this._respawnQueue.length - 1; i >= 0; i--) { const r = this._respawnQueue[i]; if (now >= r.at) { this._respawnQueue.splice(i, 1); this._spawnBot(); } } // 宝藏屋进度 if (!this.treasureDone && p && !p.dead && p.score >= GS.CFG.treasureTarget) { this.treasureDone = true; this.world.treasureBurst(p.head.x, p.head.y); p.lucky += 5; this.ui.toast('长度达标!宝藏屋开启,幸运星 +5', 'success'); } // HUD 刷新 this._boardTimer -= dt; if (this._boardTimer <= 0) { this._boardTimer = 0.5; this._refreshHud(); } } _onDeath(s, hit, now) { const segs = s.die(); this.world.dropFromSegments(segs); if (hit.type === 'body' && hit.snake) hit.snake.kills += 1; if (s.isPlayer) { this._killerName = hit.type === 'wall' ? '' : (hit.snake ? hit.snake.name : '对手'); this.state = 'dead'; this.input.setVisible(false); this.input.boost = false; this._startReviveCountdown(); } else { // 玩家击杀判定:撞到玩家身体 if (hit.type === 'body' && hit.snake === this.player) this.ui.toast('击杀 ' + s.name + '!', 'success'); this.bots = this.bots.filter(b => b !== s); this.ais.delete(s.id); this._respawnQueue.push({ at: now + 3000 }); } } /* ---------------- 复活 / 结算 ---------------- */ _startReviveCountdown() { const total = GS.CFG.reviveCountdown; let left = total; this.ui.showRevive(this._killerName, this.freeReviveUsed); this.ui.updateReviveCount(left, 1); clearInterval(this._reviveTimer); this._reviveTimer = setInterval(() => { left -= 0.1; if (left <= 0) { clearInterval(this._reviveTimer); this._toResult(); return; } this.ui.updateReviveCount(Math.ceil(left), left / total); }, 100); } _revive() { if (this.freeReviveUsed) return; this.freeReviveUsed = true; clearInterval(this._reviveTimer); const p = this.player; p.dead = false; // 安全点重生:世界中心附近 const x = U.clamp(p.head.x + U.rand(-600, 600), 400, GS.CFG.worldSize - 400); const y = U.clamp(p.head.y + U.rand(-600, 600), 400, GS.CFG.worldSize - 400); p.path = [{ x, y }]; p.pathLen = 0; p.shield = true; p.invincibleUntil = performance.now() + 3000; this.ui.hideRevive(); this.input.setVisible(true); this.state = 'playing'; this.ui.toast('复活成功,护盾保护中', 'success'); } _toResult() { clearInterval(this._reviveTimer); this.ui.hideRevive(); this.state = 'result'; const p = this.player; const score = Math.floor(p.score); if (score > this.stats.best) this.stats.best = score; this.stats.totalCoins += p.coins; this.stats.gems += p.kills * 5 + p.lucky; this.ui.showResult({ score, coins: p.coins, kills: p.kills, lucky: p.lucky, best: this.stats.best }); this._saveStats({ score, coins: p.coins, kills: p.kills, lucky: p.lucky, gems: this.stats.gems, totalCoins: this.stats.totalCoins }); } _refreshHud() { const p = this.player; if (!p) return; this.ui.updateStats(this.stats.totalCoins + p.coins, p.lucky, p.kills); this.ui.updateProgress(p.score, GS.CFG.treasureTarget, p.score >= 1000 && !this.treasureDone); // 排行榜 const rows = this._allSnakes().filter(s => !s.dead) .map(s => ({ name: s.name, score: Math.floor(s.score), me: s.isPlayer })) .sort((a, b) => b.score - a.score); const top = rows.slice(0, 9); const meRow = rows.find(r => r.me); if (meRow && !top.includes(meRow)) top.push(meRow); this.ui.updateBoard(top, meRow, this.stats.todayBest && this.stats.todayBest.score >= 0 ? this.stats.todayBest : { name: '-', score: this.stats.best }); } /* ---------------- 服务端同步 ---------------- */ async _saveStats(extra) { const body = Object.assign({}, extra); const res = await this.ui.trackFetch('保存战绩', API + '/stats', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (res && res.ok && res.data) { this.stats.best = res.data.best != null ? res.data.best : this.stats.best; this.stats.gems = res.data.gems != null ? res.data.gems : this.stats.gems; this.stats.totalCoins = res.data.totalCoins != null ? res.data.totalCoins : this.stats.totalCoins; this.stats.todayBest = res.data.todayBest || this.stats.todayBest; } } } window.addEventListener('DOMContentLoaded', () => { const g = new Game(); GS.game = g; g.init(); }); })();