feat: 新增多项功能并添加新工具

- 私人剪贴板新增限时直通URL生成功能
- doc_cloud_keeper新增标题复制按钮
- 新增贪蛇出动游戏工具
- 新增OSS中转直链工具
- 添加批量抽帧测试脚本
This commit is contained in:
yangxiangyuan
2026-08-11 10:22:31 +08:00
parent 91148fe064
commit 6dc79a2c12
28 changed files with 3169 additions and 6 deletions
+112
View File
@@ -0,0 +1,112 @@
/* 贪蛇出动 - 蛇实体:移动/转向/生长/路径采样 */
(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;
})();