/* 贪蛇出动 - 蛇实体:移动/转向/生长/路径采样 */ (function () { 'use strict'; window.GS = window.GS || {}; const U = GS.U; class Snake { constructor(opts) { this.id = opts.id; this.name = opts.name; this.isPlayer = !!opts.isPlayer; this.color = opts.color; this.skin = opts.skin || 0; // 0 纯色 1 条纹 this.angle = opts.angle != null ? opts.angle : U.rand(0, Math.PI * 2); this.targetAngle = this.angle; this.turnRate = this.isPlayer ? GS.CFG.turnRate : GS.CFG.botTurnRate; this.score = opts.score || GS.CFG.startScore; this.dead = false; this.boosting = false; this.shield = false; // 护盾:抵挡一次致命碰撞 this.invincibleUntil = 0; // 无敌时间戳(火箭/复活保护) this.magnetUntil = 0; // 磁铁截止 this.kills = 0; this.coins = 0; // 本局吃到的金币数 this.lucky = 0; // 本局幸运星 // 路径:密集头轨迹点 this.path = [{ x: opts.x, y: opts.y }]; this.pathLen = 0; this._segs = []; this._segsDirty = true; this.wobble = U.rand(0, Math.PI * 2); // 身体摆动相位 } get head() { return this.path[this.path.length - 1]; } // 半径随分数增长 get radius() { return U.clamp(7 + Math.sqrt(Math.max(0, this.score)) * 0.32, 7, 30); } // 身体节数 get segCount() { return Math.floor(10 + this.score / 6); } get spacing() { return Math.max(5, this.radius * 0.5); } get speed() { return this.boosting ? GS.CFG.boostSpeed : GS.CFG.baseSpeed; } invincible(now) { return now < this.invincibleUntil; } update(dt, now) { if (this.dead) return; // 转向限速 const da = U.normAngle(this.targetAngle - this.angle); const maxTurn = this.turnRate * dt; this.angle += U.clamp(da, -maxTurn, maxTurn); const sp = this.speed; const h = this.head; const nx = h.x + Math.cos(this.angle) * sp * dt; const ny = h.y + Math.sin(this.angle) * sp * dt; this.path.push({ x: nx, y: ny }); this.pathLen += sp * dt; // 裁剪路径:保留足够长度 const need = this.segCount * this.spacing + this.radius * 4; while (this.path.length > 2 && this.pathLen > need) { const d = U.dist(this.path[0].x, this.path[0].y, this.path[1].x, this.path[1].y); if (this.pathLen - d < need) break; this.pathLen -= d; this.path.shift(); } this._segsDirty = true; this.wobble += dt * (this.boosting ? 14 : 9); } // 从头部沿路径每隔 spacing 采样一节(不修改原路径) segments() { if (!this._segsDirty && this._segs.length) return this._segs; const segs = [{ x: this.head.x, y: this.head.y }]; const n = this.segCount; const spacing = this.spacing; let acc = 0; // 自上一个采样点起已走过但不足 spacing 的距离 for (let i = this.path.length - 1; i > 0 && segs.length < n; i--) { let ax = this.path[i].x, ay = this.path[i].y; const b = this.path[i - 1]; let d = U.dist(ax, ay, b.x, b.y); if (d < 1e-9) continue; while (acc + d >= spacing && segs.length < n) { const t = (spacing - acc) / d; const px = ax + (b.x - ax) * t; const py = ay + (b.y - ay) * t; segs.push({ x: px, y: py }); d -= (spacing - acc); ax = px; ay = py; acc = 0; } acc += d; } this._segs = segs; this._segsDirty = false; return segs; } grow(v) { this.score += v; } // 死亡:返回尸体坐标(用于掉币) die() { this.dead = true; return this.segments().slice(); } } GS.Snake = Snake; })();