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
@@ -0,0 +1,86 @@
/* 贪蛇出动 - 核心逻辑单元测试(无 DOM,Node 直接运行)
* 运行: node dev_test_scripts/unit/test_greedy_snake_core.js
*/
'use strict';
const path = require('path');
const fs = require('fs');
global.window = global; // 让模块的 window.GS 挂到全局
const JS_DIR = path.join(__dirname, '..', '..', 'public', 'tools', 'greedy_snake', 'js');
for (const f of ['config.js', 'utils.js', 'snake.js', 'ai.js', 'world.js']) {
const src = fs.readFileSync(path.join(JS_DIR, f), 'utf-8');
new Function(src)();
}
const { U, Snake, AI, World, CFG } = global.GS;
let failed = 0;
const assert = (cond, msg) => {
if (cond) console.log(' [PASS] ' + msg);
else { failed++; console.error(' [FAIL] ' + msg); }
};
console.log('== 工具函数 ==');
assert(U.fmt(276496) === '276,496', 'fmt 千分位: ' + U.fmt(276496));
assert(U.fmt(1194000) === '1,194k', 'fmt k 格式: ' + U.fmt(1194000));
assert(Math.abs(U.normAngle(Math.PI * 2)) < 1e-9, 'normAngle 归一化');
assert(U.clamp(5, 0, 3) === 3, 'clamp');
console.log('== 蛇移动与采样 ==');
const s = new Snake({ id: 'p', name: 't', isPlayer: true, color: { body: '#f00', dark: '#900' }, x: 2000, y: 2000, angle: 0 });
s.targetAngle = 0;
for (let i = 0; i < 120; i++) s.update(1 / 60, 0);
assert(Math.abs(s.head.x - (2000 + 130 * 2)) < 3, '直线移动距离正确: ' + (s.head.x - 2000).toFixed(1));
const segs = s.segments();
assert(segs.length === s.segCount, '身体节数=' + segs.length);
const d01 = U.dist(segs[0].x, segs[0].y, segs[1].x, segs[1].y);
assert(Math.abs(d01 - s.spacing) < 0.5, '节间距=spacing: ' + d01.toFixed(2));
console.log('== 世界/金币/碰撞 ==');
const w = new World();
assert(w.coins.length >= CFG.coinCount, '初始金币数: ' + w.coins.length);
const before = w.coins.length;
w.dropFromSegments(segs);
assert(w.coins.length > before, '尸体掉币: +' + (w.coins.length - before));
const s2 = new Snake({ id: 'p2', name: 't2', isPlayer: true, color: { body: '#f00', dark: '#900' }, x: 2000, y: 3000, angle: 0 });
s2.targetAngle = 0;
for (let i = 0; i < 120; i++) s2.update(1 / 60, 0); // 先走出一条身体
const bot = new Snake({ id: 'b', name: 'bot', color: { body: '#fff', dark: '#ccc' }, x: s2.head.x + 120, y: s2.head.y, score: 20, angle: Math.PI });
bot.targetAngle = Math.PI; // 朝玩家身体冲
let hit = null;
for (let i = 0; i < 60 && !hit; i++) {
s2.update(1 / 60, 0); bot.update(1 / 60, 0);
hit = w.collide(bot, [s2, bot]);
}
assert(hit !== null, '撞身体判定: ' + (hit && hit.type));
console.log('== AI + 综合模拟 600 步 ==');
const w2 = new World();
const player = new Snake({ id: 'p', name: 'me', isPlayer: true, color: { body: '#f00', dark: '#900' }, x: 2000, y: 2000 });
const bots = [];
const ais = new Map();
for (let i = 0; i < CFG.botCount; i++) {
const b = new Snake({ id: 'b' + i, name: 'bot' + i, color: { body: '#fff', dark: '#ccc' }, x: U.rand(500, 3500), y: U.rand(500, 3500), score: U.randInt(10, 200) });
bots.push(b); ais.set(b.id, new AI(b));
}
let deaths = 0, eaten0 = player.score;
for (let t = 0; t < 600; t++) {
player.targetAngle += 0.02;
for (const b of bots) ais.get(b.id).update(1 / 60, w2, [player, ...bots]);
for (const x of [player, ...bots]) x.update(1 / 60, t);
w2.update(1 / 60, [player, ...bots], t);
for (const x of [player, ...bots]) {
if (x.dead) continue;
const h = w2.collide(x, [player, ...bots]);
if (h) {
if (x.shield) { x.shield = false; continue; }
x.die(); deaths++;
}
}
}
assert(true, '模拟完成,死亡数=' + deaths + ',玩家分数 ' + eaten0 + ' -> ' + Math.floor(player.score));
assert(w2.coins.length > 0, '模拟后金币存在: ' + w2.coins.length);
assert(isFinite(player.head.x) && isFinite(player.head.y), '玩家坐标有限');
console.log(failed === 0 ? '\n全部通过' : `\n${failed} 项失败`);
process.exit(failed === 0 ? 0 : 1);