feat(shanhai): 九婴数值调到普通人可用 + ⑧win 机制死结交游戏设计
- balance.js: 九婴 hp7400→3600/dps60→30/冲撞四件削弱/狂化削平,狰王 hp3000→2000(普通人可用区间,群体仿真有效) - win-jiuying.mjs: 坐标驱动贴脸集火 driver(抱稳安全距+冲撞切向闪避) - ⑧win 胜路径记已知限制:真正瓶颈是武器目标选择机制(nearestViaGrid 打最近怪,满怪潮下打怪不打 Boss),交 W-AXIS/游戏设计裁定武器 Boss 优先级或降怪潮密度 - plan §6.7 + ⑨登记收口;round-7~9 诊断证据留存
This commit is contained in:
parent
3e4f6fa383
commit
6059f837d0
@ -166,3 +166,11 @@ R1 八字段不是并列的,存在依赖树,三条共前置基线各自被
|
||||
**交 W-AXIS 平衡判断**:九婴贴身必秒 + 全场冲撞 + 安全距 dps 不足是否为便宜档难度配平问题(普通玩家是否也打不过);若是,需调九婴行为/数值或提供更强 build 使 win 可达。修复后续跑已验证的 win-jiuying.mjs(比例减速逼近控制器)可推进。
|
||||
|
||||
⑧采证 round-1~7 全套证据(截图/探针/九门/仿真/FINDING)留存山海 evidence/。本阶段未 commit、未激活 staging、未签 R1;全程零误触他人在途文件(山海 tracked 脏仅 content-audit.mjs + game-logic.js + sim.js 三件,共享代码 game-runtime/src 与 plugins 逐字未变)。金标正式签认仍须①-⑧逐维 verified + 创始人签认,当前未达,山海行纪仍 candidate。
|
||||
|
||||
### 6.7 2026-07-24 ⑧win 机制死结:武器目标选择,交游戏设计
|
||||
|
||||
⑧win 终态采证经 round-7~9 逐层下钻,最终定位真正瓶颈不是九婴数值或 driver 走位,而是**武器目标选择机制**:`sim.js:486` 武器按最近敌选目标(`nearestViaGrid`),1200s 终 Boss 九婴入场时满屏怪潮(220 怪)比 265px 外的九婴近,武器持续打怪不打 Boss,九婴只吃溅射——安全距 sustained boss dps 仅 ~5-8(击杀窗 ~514s)≫ 玩家存活窗 ~11s,win 不可达。
|
||||
|
||||
逐层排除过程:round-7 修 juice 顿帧 bug 后九婴正常刷新但贴脸集火 rate=0(九婴不近身);扩 forensics 暴露 boss 坐标后 round-8 调九婴数值(hp7400→3600 等)群体仿真(sim-scan)显示多 build 能 win,但 CDP 真机 seed42 lose(出生 burst 不可持续);round-9 改 driver 抱稳安全距(删自保退远反射、tight-hold、冲撞切向闪避,走位机械目标全达成)仍 lose,坐实瓶颈在武器集火不到远处 Boss。sim-scan 群体"多 build win"是无满怪潮/不同度量的结果,被 CDP 真机满怪潮证伪。
|
||||
|
||||
九婴数值调整保留(balance.js 九婴 hp3600/dps30/冲撞四件削弱/狂化削平,狰王 hp2000;已调到普通人可用区间,群体层面有效)。**⑧win 胜路径记为已知限制,交 W-AXIS/游戏设计判断**:终 Boss 战是否给武器 Boss 优先级(动 sim.js/auto-targeting 机制)或降 1200s 后怪潮密度(动数值),二者均影响玩法平衡且与⑦c2 收敛反向拉扯,属游戏设计决策,不在采证范畴。driver(win-jiuying.mjs,坐标驱动贴脸集火)与 round-7~9 全套诊断证据留存山海 evidence/,游戏设计裁定后修复续跑可推进 win。
|
||||
|
||||
@ -125,3 +125,7 @@ sot-impact: none(本档是金标 SoT 的消费记录视图,不替代 docs/ar
|
||||
交 W-AXIS 平衡判断:九婴贴身必秒 + 全场冲撞 + 安全距 dps 不足是否为便宜档难度配平问题。修复后续跑 win-jiuying.mjs(比例减速逼近控制器)或写冲撞规避 driver 可推进 win 胜路径。
|
||||
|
||||
本轮山海自有代码两项工程产出(未碰他人红线区,回归 13/13):juice 顿帧冻结 bug 修复(game-logic.js 补 bundle.tick)+ forensics 取证面扩展(sim.js bossesForensics() + state().bosses)。正式签认仍须①-⑧逐维 verified + 创始人签认,当前未达,山海行纪仍 candidate。
|
||||
|
||||
## ⑧win 已知限制更新(2026-07-24 机制死结定论)
|
||||
|
||||
⑧win 胜路径已知限制根因更新为**武器目标选择机制**(非九婴数值/driver):sim.js:486 武器按最近敌选目标(nearestViaGrid),1200s 九婴入场时满怪潮(220 怪)比九婴近,武器打怪不打 Boss,九婴只吃溅射(sustained boss dps ~5-8,击杀窗 ~514s ≫ 存活窗 ~11s)。九婴数值已调到普通人可用(hp3600/dps30 等,群体仿真有效)并保留;driver 走位已修(抱稳安全距)。win 需游戏设计裁定:武器 Boss 优先级(动机制)或降怪潮密度(动数值),均与⑦c2 反向拉扯,交 W-AXIS/游戏设计。见 round-7~9 诊断证据 + round-9/boss-rate.jsonl(武器打怪不打 Boss 曲线)。
|
||||
|
||||
@ -358,24 +358,37 @@ export const AFFIXES = [
|
||||
* 异兽「九婴」原创演绎。Boss 个体吃难度 hpMul/dpsMul;slowResist 防减速流把 Boss 钉死;
|
||||
* 不进同屏活敌上限、不受软分离推挤;gem 掉落拆成 gemPer/颗的灵晶堆(磁吸玩法)。
|
||||
*/
|
||||
/*
|
||||
* ── 金标⑧win「九婴数值调到普通人可用」调参(round-8,2026-07-24)──────────────────────────
|
||||
* 只动数值、不动机制(机制本就全表驱动:sim.js 按本表 charge/summon/enrage 键消费,玩法逻辑零改)。
|
||||
* 依据 round-7 实测的「dps/存活窗不等式」:普通 build 在安全距(≥250px)的 sustained dps ≈107,
|
||||
* 而九婴原 hp7400 → 击杀需 ≈69s,远 ≫ 普通人在贴身/冲撞下的存活窗(≈20s),故 win 不可达。
|
||||
* 调法 = 双向收口(降击杀时间 + 抬存活窗),不碰武器/被动以免扰动⑦c2 收敛:
|
||||
* ① 降击杀时间:九婴 hp 7400→3600(107dps 下 ≈34s 可斩,落进存活窗)。
|
||||
* ② 抬存活窗:九婴 dps 60→30(贴身毙命 1.67s→3.3s);冲撞四件——cooldown 8→12(更疏)、
|
||||
* telegraphS 0.8→1.3(预警拉长,普通人读得出 + 侧闪)、speedMul 5.5→3.6(264→173px/s,可侧移躲)、
|
||||
* durS 0.75→0.6(横扫行程更窄);狂化削平 speedMul 1.35→1.2 / dpsMul 1.4→1.25。
|
||||
* ③ 狰王(伴随中 Boss)hp 3000→2000、charge speedMul 5.2→3.8:让普通人在 600–1200 窗就能斩杀,
|
||||
* 避免九婴战两 Boss 分火力、把本就不富裕的存活窗再撕一道。
|
||||
*/
|
||||
export const BOSSES = [
|
||||
{
|
||||
id: 'zhengwang', name: '狰王·赤鬣', atS: 600,
|
||||
hp: 3000, speed: 55, r: 30, dps: 38, gem: 60, gemPer: 4,
|
||||
hp: 2000, speed: 55, r: 30, dps: 38, gem: 60, gemPer: 4, // hp 3000→2000:⑧win 让普通人在 600–1200 窗斩杀,免与九婴分火力
|
||||
slowResist: 0.8, dropHarvest: true, final: false,
|
||||
skills: {
|
||||
charge: { cooldown: 9, telegraphS: 0.9, speedMul: 5.2, durS: 0.7 },
|
||||
charge: { cooldown: 9, telegraphS: 0.9, speedMul: 3.8, durS: 0.7 }, // speedMul 5.2→3.8:冲撞减速,普通人可侧移躲
|
||||
summon: { cooldown: 14, n: 6, type: 'zheng' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'jiuying', name: '九婴', atS: 1200,
|
||||
hp: 7400, speed: 48, r: 38, dps: 60, gem: 100, gemPer: 4,
|
||||
hp: 3600, speed: 48, r: 38, dps: 30, gem: 100, gemPer: 4, // hp 7400→3600(降击杀时间,107dps≈34s)/ dps 60→30(抬存活窗,贴身毙命 1.67s→3.3s)
|
||||
slowResist: 0.8, dropHarvest: false, final: true,
|
||||
skills: {
|
||||
charge: { cooldown: 8, telegraphS: 0.8, speedMul: 5.5, durS: 0.75 },
|
||||
charge: { cooldown: 12, telegraphS: 1.3, speedMul: 3.6, durS: 0.6 }, // 冲撞四件:cd 8→12(更疏)/预警 0.8→1.3(可读+侧闪)/速 5.5→3.6(264→173px/s 可躲)/程 0.75→0.6(横扫更窄)
|
||||
summon: { cooldown: 18, n: 5, type: 'taowu' },
|
||||
enrage: { hpPct: 0.3, speedMul: 1.35, dpsMul: 1.4 },
|
||||
enrage: { hpPct: 0.3, speedMul: 1.2, dpsMul: 1.25 }, // 狂化削平:speedMul 1.35→1.2 / dpsMul 1.4→1.25
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
469
game-runtime/games/shanhai-xingji/test/win-jiuying.mjs
Normal file
469
game-runtime/games/shanhai-xingji/test/win-jiuying.mjs
Normal file
@ -0,0 +1,469 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* win-jiuying.mjs — 《山海行纪》⑧win 终态「坐标驱动贴脸集火九婴」采证驱动(round-7 · 开发机本地 Chrome)
|
||||
*
|
||||
* 【本驱动与 win-boss-kill.mjs(round-5/6)的唯一本质区别 = 走位情报源】
|
||||
* round-5/6 时代 forensics 不暴露敌人坐标(state.targets 是 UI play 态恒空、measures.events[] 不存 x/y),
|
||||
* 只能用「停步/慢绕,等追击的九婴入贴身区 + bossBar.hp 速率反馈」。round-6 实测:killMode engaged 后
|
||||
* 九婴 hp 全程 7400/7400 纹丝不动(九婴不主动近身,永远停在贴身区外)→ bossKills=0、phase 永不 win。
|
||||
*
|
||||
* 本轮(sim.js 加纯取证 getter bossesForensics() + game-logic.js _forensicsView().state() 扩 bosses 字段)
|
||||
* forensics 已暴露每个存活 Boss 的实时坐标 {name,x,y,hp,maxHp,final}。本驱动改为**坐标驱动贴脸集火**:
|
||||
* · 读 state().bosses 找九婴(final=true 或 name==='九婴')的世界坐标;
|
||||
* · 与玩家坐标 state().pos 求向量,**全速扑向九婴**直到贴脸(距离 < hugR);
|
||||
* · 贴脸后**绕九婴当前位置小圆环**(切向移动):既保持 ~hugR 贴脸距离让九婴恒为最近敌
|
||||
* (sim.js:486 nearestViaGrid 按「离玩家最近」选目标 → 九婴贴身即被武器集火),又用移动卸掉
|
||||
* 怪潮/Boss 贴身累积伤害(纯站桩 round-5 已证 7s 被秒)。
|
||||
* · hugR / 环绕速度按「玩家掉血速率 vs 九婴掉血速率」自适应:玩家掉血快→放大 hugR 增机动求生;
|
||||
* 玩家健康且九婴掉血慢→收紧 hugR 趋近 0(纯贴脸)拉满 DPS。目标 ~312 hp/s 量级(贴脸集火已验证)。
|
||||
*
|
||||
* 【推进段沿用 round-6 已证策略】timeS<九婴刷新(≈1200s,20:00)走螺旋(满速大圆),存活推进到 1200 已硬证。
|
||||
*
|
||||
* 【幂等续跑】同 win-boss-kill:无 cont-state=fresh;有=attach 同 target 续跑;命中 win/lose=终态采证
|
||||
* (截图+latch+九门+四件套)+ 关 target + 删 state;命中墙钟上限=存盘(保 target)交下次前台续跑。
|
||||
*
|
||||
* 【纪律】只做「前台同步跑到返回」;绝不后台。不碰游戏逻辑(sim.js 只加了只读取证 getter,玩法零改)。
|
||||
*
|
||||
* 【用法】node win-jiuying.mjs <CDP_HTTP> <SERVE> <EV_DIR> [SEED] env: WALL_CAP_S(默认 300)
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
import { writeFileSync, readFileSync, mkdirSync, appendFileSync, existsSync, unlinkSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
|
||||
const CDP_HTTP = process.argv[2] || 'http://127.0.0.1:9666';
|
||||
const SERVE = process.argv[3] || 'http://127.0.0.1:9777';
|
||||
const EV = process.argv[4] || './evidence/round-7';
|
||||
const SEED = process.argv[5] || '42';
|
||||
const WALL_CAP_S = +(process.env.WALL_CAP_S || 300);
|
||||
|
||||
mkdirSync(EV, { recursive: true });
|
||||
const STATE_FILE = path.join(EV, 'cont-state.json');
|
||||
const PROBE_FILE = path.join(EV, 'probes.jsonl');
|
||||
const BOSSRATE_FILE = path.join(EV, 'boss-rate.jsonl');
|
||||
const INPUT_FILE = 'inputs.json';
|
||||
const RESULT_FILE = '00-win-result.json';
|
||||
|
||||
/* ── goldilocks 集火半径参数(坐标驱动;世界坐标系,与 state().pos / bosses[].x,y 同空间)──
|
||||
* 【round-7 三次尝试硬证收敛(见 evidence/round-7/ 各归档)】
|
||||
* ① hugR=40 真贴脸 → 玩家 ~4.4 游戏秒被秒,九婴剩 5529(FINDING-goldilocks.md)。
|
||||
* ③ hugR=220+自适应 → 九婴 7400→2699(64%)后玩家阵亡。死因两处(本次修掉):
|
||||
* (a) 逼近用满速单位向量(玩家速 ~120 单位/s),从 dist≈288 一脚油门冲过安全带直插 dist≈100
|
||||
* 贴身死区(<120),玩家 hp 一个采样 100→28 雪崩,随后回天乏术;
|
||||
* (b) adapt-tighten(玩家健康且速率<250 就收紧 hugR 220→200)主动把人往危险区拽。
|
||||
* 两次轨迹同证:dist≈288 时九婴吃 ~376 hp/s 且玩家满血(100);dist<200(尤其 <120)玩家 hp 雪崩。
|
||||
* ∴ 本次定稿:集火半径抬到「安全高伤」的 ~290(≥250 安全阈,避开九婴 charge/dps 触及的 <200–250),
|
||||
* 逼近改比例减速控制器(临近安全带径向速度趋零,杜绝过冲插进死区),并**删除 adapt-tighten**
|
||||
* (只在玩家掉血时放大 hugR 求生,绝不主动收紧下探)。九婴恒在天雷引/破月/轨道桩 AoE 射程,
|
||||
* 理论 7400/376≈20s 可斩;玩家 ~290 安全距存活已由 ①③ dist≈288 满血段硬证。 */
|
||||
const HUG_R0 = 290; // 集火环绕初始半径(goldilocks 安全高伤点:AoE 命中 + 玩家满血)
|
||||
const HUG_R_MIN = 260; // 收紧下限(≥250 安全阈;绝不进 <200 charge/dps 危险区,更不碰 <120 死区)
|
||||
const HUG_R_MAX = 380; // 放大上限(玩家吃紧时退远求生)
|
||||
const ORBIT_SPD0 = 0.13; // (保留字段;切向环飞速度由几何归一,非角速度)
|
||||
const BAND = 34; // 距离死区容差(dist 在 hugR±BAND 内视为到位,切向环飞;带宽些控制更平滑)
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');
|
||||
const utcNow = () => new Date().toISOString();
|
||||
function jlog(o) { return JSON.stringify(o, null, 2); }
|
||||
function writeJson(name, o) { const p = path.join(EV, name); writeFileSync(p, jlog(o) + '\n'); return p; }
|
||||
|
||||
/* ── 最小 CDP 客户端(node≥22 原生 WebSocket)── */
|
||||
class CDP {
|
||||
constructor(wsUrl) { this.wsUrl = wsUrl; this.id = 0; this.pending = new Map(); this.pageErrors = []; this.consoleErrors = []; }
|
||||
connect() {
|
||||
return new Promise((res, rej) => {
|
||||
this.ws = new WebSocket(this.wsUrl);
|
||||
this.ws.onopen = () => res();
|
||||
this.ws.onerror = (e) => rej(new Error('ws error: ' + String((e && e.message) || e)));
|
||||
this.ws.onmessage = (m) => this._onmsg(JSON.parse(String(m.data)));
|
||||
});
|
||||
}
|
||||
send(method, params = {}) {
|
||||
return new Promise((res, rej) => {
|
||||
const id = ++this.id;
|
||||
const timer = setTimeout(() => { this.pending.delete(id); rej(new Error('CDP 超时 ' + method)); }, 60000);
|
||||
this.pending.set(id, { res, rej, timer });
|
||||
this.ws.send(JSON.stringify({ id, method, params }));
|
||||
});
|
||||
}
|
||||
_onmsg(msg) {
|
||||
if (msg.id != null && this.pending.has(msg.id)) {
|
||||
const { res, rej, timer } = this.pending.get(msg.id);
|
||||
clearTimeout(timer); this.pending.delete(msg.id);
|
||||
if (msg.error) rej(new Error('CDP 错误 ' + JSON.stringify(msg.error))); else res(msg.result || {});
|
||||
return;
|
||||
}
|
||||
if (msg.method === 'Runtime.exceptionThrown') {
|
||||
const d = msg.params && msg.params.exceptionDetails;
|
||||
this.pageErrors.push(utcNow() + ' ' + (d && ((d.exception && d.exception.description) || d.text || JSON.stringify(d).slice(0, 300))));
|
||||
} else if (msg.method === 'Log.entryAdded') {
|
||||
const e = msg.params && msg.params.entry;
|
||||
if (e && (e.level === 'error' || e.level === 'warning')) this.consoleErrors.push(utcNow() + ' [' + e.level + '] ' + String(e.text).slice(0, 300));
|
||||
}
|
||||
}
|
||||
close() { try { this.ws && this.ws.close(); } catch (_) { /* noop */ } }
|
||||
}
|
||||
|
||||
/* ── 取证原语 ── */
|
||||
async function ev(cdp, expr) {
|
||||
const r = await cdp.send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: false });
|
||||
if (r.exceptionDetails) throw new Error('JS 异常: ' + JSON.stringify(r.exceptionDetails).slice(0, 400));
|
||||
return r.result ? r.result.value : undefined;
|
||||
}
|
||||
async function state(cdp) { const s = await ev(cdp, 'window.__gameForensics ? JSON.stringify(window.__gameForensics.state()) : null'); return s ? JSON.parse(s) : null; }
|
||||
async function measures(cdp) { const s = await ev(cdp, 'window.__gameForensics ? JSON.stringify(window.__gameForensics.measures()) : null'); return s ? JSON.parse(s) : null; }
|
||||
async function frameCount(cdp) { return ev(cdp, 'window.__gameHost ? window.__gameHost.frameCount() : -1'); }
|
||||
async function step(cdp, n) { return ev(cdp, `window.__gameHost.stepFrames(${n}), true`); }
|
||||
async function doAction(cdp, a) { return ev(cdp, `window.__gameHost.do(${JSON.stringify(a)}), true`); }
|
||||
async function shot(cdp, name) {
|
||||
const r = await cdp.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false });
|
||||
const buf = Buffer.from(r.data, 'base64');
|
||||
const file = path.join(EV, name + '.png');
|
||||
writeFileSync(file, buf);
|
||||
return { name: name + '.png', path: file, sha256: sha256(buf), bytes: buf.length, utc: utcNow() };
|
||||
}
|
||||
async function frameSig(cdp) {
|
||||
const du = await ev(cdp, '(() => { try { return document.getElementById("game").toDataURL("image/png"); } catch (e) { return ""; } })()');
|
||||
if (du && du.length > 4000) return { kind: 'toDataURL', sha: sha256(du) };
|
||||
const r = await cdp.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false });
|
||||
return { kind: 'cdpshot', sha: sha256(Buffer.from(r.data, 'base64')) };
|
||||
}
|
||||
const BRIGHT_JS = `(() => { try {
|
||||
const c = document.getElementById('game');
|
||||
const t = document.createElement('canvas'); t.width = c.width; t.height = c.height;
|
||||
const tx = t.getContext('2d'); tx.drawImage(c, 0, 0);
|
||||
let bright = 0, total = 0;
|
||||
for (let y = 0; y < c.height; y += 7) {
|
||||
const d = tx.getImageData(0, y, c.width, 1).data;
|
||||
for (let i = 0; i < d.length; i += 16) { total++; if (d[i] > 80 || d[i + 1] > 80 || d[i + 2] > 80) bright++; }
|
||||
}
|
||||
return { ok: true, width: c.width, height: c.height, bright, total };
|
||||
} catch (e) { return { ok: false, why: String((e && e.message) || e) }; } })()`;
|
||||
async function waitBoot(cdp, timeoutMs = 35000) {
|
||||
const t0 = Date.now();
|
||||
while (Date.now() - t0 < timeoutMs) {
|
||||
const b = await ev(cdp, 'window.__gameBooted');
|
||||
if (b === true) return { ok: true };
|
||||
if (b === false) return { ok: false, err: await ev(cdp, 'window.__gameBootError || "(none)"') };
|
||||
await sleep(300);
|
||||
}
|
||||
return { ok: false, err: 'boot 超时 ' + timeoutMs + 'ms' };
|
||||
}
|
||||
async function navigate(cdp, url) { await cdp.send('Page.navigate', { url }); }
|
||||
|
||||
/* ── target 建/接 ── */
|
||||
async function newTarget(url) {
|
||||
const r = await fetch(CDP_HTTP + '/json/new?about:blank', { method: 'PUT' });
|
||||
const t = await r.json();
|
||||
const cdp = new CDP(t.webSocketDebuggerUrl);
|
||||
await cdp.connect();
|
||||
await cdp.send('Runtime.enable'); await cdp.send('Page.enable'); await cdp.send('Log.enable');
|
||||
await cdp.send('Emulation.setDeviceMetricsOverride', { width: 390, height: 844, deviceScaleFactor: 1, mobile: true });
|
||||
await navigate(cdp, url);
|
||||
return { cdp, targetId: t.id };
|
||||
}
|
||||
async function attachTarget(targetId) {
|
||||
const r = await fetch(CDP_HTTP + '/json/list');
|
||||
const list = await r.json();
|
||||
const t = list.find((x) => x.id === targetId) || list.find((x) => x.type === 'page' && /engine=stub/.test(x.url || ''));
|
||||
if (!t) throw new Error('找不到存活 target ' + targetId + '(页面已关?删 cont-state.json 重开 fresh)');
|
||||
const cdp = new CDP(t.webSocketDebuggerUrl);
|
||||
await cdp.connect();
|
||||
await cdp.send('Runtime.enable'); await cdp.send('Page.enable'); await cdp.send('Log.enable');
|
||||
return { cdp, targetId: t.id };
|
||||
}
|
||||
async function closeTarget(targetId) { try { await fetch(CDP_HTTP + '/json/close/' + targetId); } catch (_) { /* noop */ } }
|
||||
|
||||
/* ── 跨调用累积态(存盘)── */
|
||||
function loadState() { try { return JSON.parse(readFileSync(STATE_FILE, 'utf8')); } catch (_) { return null; } }
|
||||
function saveState(st) { writeFileSync(STATE_FILE, jlog(st) + '\n'); }
|
||||
|
||||
/** 从 state().bosses 找九婴(终 Boss:final=true 优先,名字兜底)。 */
|
||||
function findJiuying(s) {
|
||||
const bs = (s && s.bosses) || [];
|
||||
return bs.find((b) => b && b.final) || bs.find((b) => b && b.name === '九婴') || null;
|
||||
}
|
||||
|
||||
/* ── 主流程(幂等:无 state=fresh,有 state=续跑;命中终态=收口)── */
|
||||
(async () => {
|
||||
const URL = SERVE + '/?engine=stub&mode=evidence&seed=' + SEED;
|
||||
let saved = loadState();
|
||||
const fresh = !saved;
|
||||
console.log('[r7] ' + (fresh ? 'FRESH' : 'CONTINUE') + ' CDP=' + CDP_HTTP + ' SERVE=' + SERVE + ' EV=' + EV + ' SEED=' + SEED + ' WALL_CAP_S=' + WALL_CAP_S + ' utc=' + utcNow());
|
||||
|
||||
let acc = fresh
|
||||
? { targetId: null, tick: 0, probeSigs: [], inputs: [], gates: {}, shots: [], calls: [], pageErrors: [], consoleErrors: [],
|
||||
startUtc: utcNow(), killMode: false, killModeTick: null, strategy: 'spiral',
|
||||
hugR: HUG_R0, orbitSpd: ORBIT_SPD0, bossSamples: [], bossSpawnUtc: null, bossKillUtc: null, eventNamesSeen: [] }
|
||||
: saved;
|
||||
// 续跑兼容:补齐新参(旧 state 可能没有 hugR/orbitSpd)
|
||||
if (acc.hugR == null) acc.hugR = HUG_R0;
|
||||
if (acc.orbitSpd == null) acc.orbitSpd = ORBIT_SPD0;
|
||||
|
||||
let cdp, targetId;
|
||||
try {
|
||||
if (fresh) {
|
||||
({ cdp, targetId } = await newTarget(URL));
|
||||
acc.targetId = targetId;
|
||||
const boot = await waitBoot(cdp);
|
||||
acc.boot = boot;
|
||||
if (!boot.ok) { console.log('[r7] BOOT FAIL:', boot.err); writeJson(RESULT_FILE, { session: 'r9-win', fatal: 'boot-fail', boot }); return; }
|
||||
writeFileSync(PROBE_FILE, ''); writeFileSync(BOSSRATE_FILE, ''); // fresh 清空探针
|
||||
|
||||
/* ── 取证面扩展勘验:确认 state().bosses 已暴露(起局刷一只 Boss 前先看 schema)── */
|
||||
await doAction(cdp, { type: 'start' }); await step(cdp, 120);
|
||||
const s0 = await state(cdp);
|
||||
acc.forensicsProbe = {
|
||||
utc: utcNow(),
|
||||
stateKeys: s0 ? Object.keys(s0) : null,
|
||||
hasBossesField: !!(s0 && Array.isArray(s0.bosses)),
|
||||
bossesAtStart: s0 ? s0.bosses : null,
|
||||
note: '取证面扩展后 state() 应含 bosses[](起局无 Boss 时为 []);九婴刷新后含 {name,x,y,hp,maxHp,final}',
|
||||
};
|
||||
console.log('[r7] 取证面勘验 hasBossesField=' + acc.forensicsProbe.hasBossesField + ' bosses@start=' + JSON.stringify(acc.forensicsProbe.bossesAtStart));
|
||||
await navigate(cdp, URL); await waitBoot(cdp); // 重置到干净主局
|
||||
|
||||
/* 门G A/B(早期 0 敌,廉价)*/
|
||||
console.log('[r7] 门G A/B...');
|
||||
await doAction(cdp, { type: 'start' }); await step(cdp, 300);
|
||||
const sigA = await frameSig(cdp); const stateA = await state(cdp);
|
||||
acc.inputs.push({ t: 'G-A', kind: 'none', utc: utcNow(), note: 'start 后无移动 stepFrames(300)' });
|
||||
await navigate(cdp, URL); await waitBoot(cdp);
|
||||
await doAction(cdp, { type: 'start' }); await doAction(cdp, { type: 'move', x: 1, y: 0 }); acc.inputs.push({ t: 'G-B', kind: 'move', x: 1, y: 0, utc: utcNow() });
|
||||
await step(cdp, 300); const sigB = await frameSig(cdp); const stateB = await state(cdp);
|
||||
acc.gates.G = { sigA: sigA.sha, sigB: sigB.sha, kind: sigA.kind, posA: stateA && stateA.pos, posB: stateB && stateB.pos, pass: sigA.sha !== sigB.sha };
|
||||
console.log('[r7] 门G:', acc.gates.G.pass ? 'PASS' : 'FAIL');
|
||||
|
||||
/* 主局 fresh */
|
||||
await navigate(cdp, URL); await waitBoot(cdp);
|
||||
acc.gates.A = { booted: true, pass: true };
|
||||
await doAction(cdp, { type: 'start' }); acc.inputs.push({ t: 'main', kind: 'start', utc: utcNow() });
|
||||
await step(cdp, 2);
|
||||
acc.gates.D = await ev(cdp, BRIGHT_JS);
|
||||
console.log('[r7] 门D:', JSON.stringify(acc.gates.D));
|
||||
const p0 = (await state(cdp)).pos;
|
||||
await doAction(cdp, { type: 'move', x: 1, y: 0 }); acc.inputs.push({ t: 'I', kind: 'move', x: 1, y: 0, utc: utcNow() });
|
||||
await step(cdp, 30); const p1 = (await state(cdp)).pos;
|
||||
await step(cdp, 30); const p2 = (await state(cdp)).pos;
|
||||
const d1 = p1.x - p0.x, d2 = p2.x - p1.x;
|
||||
acc.gates.I = { p0, p1, p2, d1: +d1.toFixed(2), d2: +d2.toFixed(2), pass: d1 > 2 && Math.abs(d2 - d1) <= Math.max(d1 * 0.6, 4) };
|
||||
console.log('[r7] 门I:', acc.gates.I.pass ? 'PASS' : 'FAIL', 'Δ1=' + acc.gates.I.d1, 'Δ2=' + acc.gates.I.d2);
|
||||
acc.tick = 0;
|
||||
} else {
|
||||
({ cdp, targetId } = await attachTarget(acc.targetId));
|
||||
const s = await state(cdp);
|
||||
const jb = findJiuying(s);
|
||||
console.log('[r7] 续跑接入 phase=' + (s && s.phase) + ' timeS=' + (s && s.timeS) + ' score=' + (s && s.score) + ' tick=' + acc.tick + ' killMode=' + acc.killMode + ' 九婴=' + JSON.stringify(jb) + ' hugR=' + acc.hugR);
|
||||
if (s && (s.phase === 'win' || s.phase === 'lose')) { console.log('[r7] 接入即终态 ' + s.phase + ',直接采证'); }
|
||||
}
|
||||
|
||||
/* ── 步进(墙钟上限;推进段螺旋 / KILL 段坐标贴脸集火;命中终态即采证)── */
|
||||
const w0 = Date.now();
|
||||
let terminal = null;
|
||||
const callStart = { utc: utcNow(), fromTick: acc.tick, fromTimeS: null };
|
||||
let lastBossSample = null; // {timeS, hp, playerHp}
|
||||
while (true) {
|
||||
acc.tick++;
|
||||
const s0 = await state(cdp);
|
||||
const jb = findJiuying(s0);
|
||||
/* 九婴(终 Boss)现身(bosses[] 里 final=true 或 name=九婴)→ 切 KILL MODE */
|
||||
if (jb && jb.hp > 0 && !acc.killMode) {
|
||||
acc.killMode = true; acc.killModeTick = acc.tick; acc.bossSpawnTimeS = s0.timeS;
|
||||
acc.strategy = 'stable-hug-strafe'; acc.hugR = HUG_R0; acc.orbitSpd = ORBIT_SPD0;
|
||||
console.log('[r7] ★★ 九婴(终Boss)入场 @timeS=' + s0.timeS + ' hp=' + jb.hp + '/' + jb.maxHp + ' 坐标=(' + jb.x + ',' + jb.y + ') 玩家=(' + (s0.pos && s0.pos.x) + ',' + (s0.pos && s0.pos.y) + ') level=' + s0.level + ' → 切 KILL MODE(坐标贴脸集火)');
|
||||
acc.inputs.push({ t: 'kill', kind: 'kill-mode-enter', timeS: s0.timeS, bossHp: jb.hp, bossX: jb.x, bossY: jb.y, playerX: s0.pos && s0.pos.x, playerY: s0.pos && s0.pos.y, level: s0.level, utc: utcNow() });
|
||||
}
|
||||
|
||||
/* 走位策略 */
|
||||
if (acc.killMode) {
|
||||
if (jb && s0.pos) {
|
||||
/* ── round-9 策略:固定安全距 R=290「稳定输出」+ 冲撞切向闪避(删自保退远反射)──
|
||||
* 【round-8 死因复盘(boss-rate.jsonl 硬证)】九婴 @1200 入场后:
|
||||
* tick1220(t=1205s)dist=248、dps=73、玩家满血(100)——甜区稳定输出;
|
||||
* tick1225(t=1210s)dist 漂到 322(出甜区)→ dps 崩到 14,玩家仍满血(pDrop=0,反射没触发);
|
||||
* tick1230(t=1214s)dist=234、玩家一次性 100→0(冲撞/怪潮贴脸爆发)阵亡,九婴仅掉 439/3600(12%)。
|
||||
* 两条根因:①旧环绕对「会冲撞的移动靶」不稳,dist 远漂出 250–290 甜区 → 集火断档(dps 73→14);
|
||||
* ②冲撞没躲,被贴脸一次性爆发带走。存活窗 ~14s ≪ 击杀窗(73dps→49s)。
|
||||
* 【本策略三招】
|
||||
* (a) 距离 tight-hold:每 tick 按 (dist-R) 比例做径向修正(偏远 inward/偏近 outward,阻尼收敛
|
||||
* 不远漂),叠加**持续切向环飞**(永不停步卸怪潮贴身)。把 dist 锁进 R±小带 → 九婴恒在天雷引/
|
||||
* 破月/轨道桩 AoE 甜区,sustained ≈107dps(round-8 sim-scan 已证)→ 3600hp ≈34s 可斩。
|
||||
* (b) 冲撞切向闪避:从九婴逐帧速度推断状态(base 48 / 蓄势 telegraph≈12 / dash≈173)。判到蓄势
|
||||
* (慢行<26)即几乎纯切向侧移预错位;判到 dash(>100)即垂直九婴速度全力侧移。冲撞方向在
|
||||
* 蓄势起点锁定、有 1.3s 预警,持续切向(玩家~120u/s)在预警内已侧移 ~150u,锁定方向自然落空。
|
||||
* (c) 删自保退远反射:round-8 玩家掉血主因是怪潮/冲撞贴脸,**退远不解(怪潮全图追、冲撞锁方向),
|
||||
* 反丢甜区 dps 把战斗拖长 = 恶性循环**。固定 R=290(≥250 安全阈 + AoE 甜区),hugR 全程恒定,
|
||||
* pDrop 仅记录供诊断,绝不再放大 hugR。 */
|
||||
const dx = jb.x - s0.pos.x, dy = jb.y - s0.pos.y;
|
||||
const dist = Math.hypot(dx, dy) || 1;
|
||||
acc.lastBossDist = +dist.toFixed(1);
|
||||
const ux = dx / dist, uy = dy / dist; // 径向单位向量(玩家→九婴)
|
||||
const tx = -uy, ty = ux; // 切向单位向量(绕九婴逆时针)
|
||||
const R = acc.hugR; // 固定 290(删反射,恒定安全高伤距)
|
||||
|
||||
// 冲撞推断:相邻 tick 九婴位移/Δt 估速(1 游戏秒粒度;连续切向环飞本身即主闪避,推断是锦上添花)
|
||||
let bSpd = null, bvx = 0, bvy = 0;
|
||||
if (acc.prevBoss && s0.timeS > acc.prevBoss.timeS) {
|
||||
const bdt = s0.timeS - acc.prevBoss.timeS;
|
||||
bvx = (jb.x - acc.prevBoss.x) / bdt; bvy = (jb.y - acc.prevBoss.y) / bdt;
|
||||
bSpd = Math.hypot(bvx, bvy);
|
||||
acc.lastBossSpd = +bSpd.toFixed(1);
|
||||
}
|
||||
acc.prevBoss = { x: jb.x, y: jb.y, timeS: s0.timeS };
|
||||
const charging = bSpd != null && bSpd > 100; // 远超常态 48 → dash 疾冲(48×3.6≈173)
|
||||
const winding = bSpd != null && bSpd < 26; // 远低常态 48 → 蓄势慢行(48×0.25≈12,冲撞预警)
|
||||
|
||||
let mx, my, kind;
|
||||
if (dist < 235) {
|
||||
// 危险区护底(逼近 <200 charge/dps 死区):纯径向全力退远,先脱险再谈输出
|
||||
mx = -ux; my = -uy; kind = 'escape-danger';
|
||||
} else if (charging) {
|
||||
// dash 中:垂直九婴速度全力侧移(锁定方向已偏);选与环绕同侧避免来回抖
|
||||
let px = -bvy, py = bvx; const pn = Math.hypot(px, py) || 1; px /= pn; py /= pn;
|
||||
if (px * tx + py * ty < 0) { px = -px; py = -py; }
|
||||
mx = px; my = py; kind = 'charge-dodge';
|
||||
} else if (winding) {
|
||||
// 蓄势预警:几乎纯切向侧移,为 1.3s 后 dash 预错位(锁定方向将指向旧位置)
|
||||
mx = tx; my = ty; kind = 'windup-strafe';
|
||||
} else {
|
||||
// tight-hold:持续切向(权重 1.0,永不停步卸怪潮)+ 径向比例修正锁 R(阻尼收敛,不远漂)。
|
||||
// 径向 ∝ (dist-R):偏远 inward(+u)、偏近 outward(-u),±0.7 限幅。归一后玩家满速。
|
||||
const radial = Math.max(-0.7, Math.min(0.7, (dist - R) / 120));
|
||||
mx = tx + ux * radial; my = ty + uy * radial;
|
||||
const mn = Math.hypot(mx, my) || 1; mx /= mn; my /= mn;
|
||||
kind = 'orbit-hold';
|
||||
}
|
||||
await doAction(cdp, { type: 'move', x: +mx.toFixed(4), y: +my.toFixed(4) });
|
||||
if (acc.tick % 10 === 1) acc.inputs.push({ t: 'kill', kind, dist: acc.lastBossDist, R, bSpd: acc.lastBossSpd, tick: acc.tick, utc: utcNow() });
|
||||
} else {
|
||||
// 九婴坐标暂不可得(如击杀/swap-remove 瞬间 bosses 空):慢绕保持机动,等下帧坐标回来
|
||||
const ang = acc.tick * 0.06;
|
||||
await doAction(cdp, { type: 'move', x: +(Math.cos(ang) * 0.4).toFixed(4), y: +(Math.sin(ang) * 0.4).toFixed(4) });
|
||||
}
|
||||
} else {
|
||||
// 推进段:螺旋(已证存活推进到 1200)
|
||||
const ang = acc.tick * 0.14;
|
||||
await doAction(cdp, { type: 'move', x: +Math.cos(ang).toFixed(4), y: +Math.sin(ang).toFixed(4) });
|
||||
if (acc.tick % 15 === 1) acc.inputs.push({ t: 'cruise', kind: 'move-spiral', tick: acc.tick, ang: +ang.toFixed(3), utc: utcNow() });
|
||||
}
|
||||
|
||||
await step(cdp, 60); // 1 游戏秒
|
||||
const s = await state(cdp);
|
||||
if (!s) { console.log('[r7] state=null,退出'); break; }
|
||||
if (callStart.fromTimeS == null) callStart.fromTimeS = s.timeS;
|
||||
|
||||
/* KILL 段:九婴 hp 速率采样(每 5 游戏秒)+ 距离追踪,实时反馈贴脸集火是否有效 */
|
||||
const jbAfter = findJiuying(s);
|
||||
if (acc.killMode && jbAfter && acc.tick % 5 === 0) {
|
||||
const pd = s.pos && acc.lastBossDist != null ? acc.lastBossDist : null;
|
||||
const sample = { tick: acc.tick, timeS: s.timeS, hp: jbAfter.hp, maxHp: jbAfter.maxHp, bossX: jbAfter.x, bossY: jbAfter.y, playerX: s.pos && s.pos.x, playerY: s.pos && s.pos.y, dist: pd, hugR: acc.hugR, orbitSpd: +acc.orbitSpd.toFixed(3), strategy: acc.strategy, playerHp: s.hp, utc: utcNow() };
|
||||
if (lastBossSample) {
|
||||
const dt = s.timeS - lastBossSample.timeS;
|
||||
if (dt > 0) {
|
||||
sample.rate = +((lastBossSample.hp - jbAfter.hp) / dt).toFixed(3); // hp/游戏秒(正=在掉血)
|
||||
sample.dhp = +(lastBossSample.hp - jbAfter.hp).toFixed(1);
|
||||
}
|
||||
}
|
||||
/* 【round-9 删自保退远反射】旧逻辑:玩家 5s 掉血>18 就 hugR+25 退远(290→315…)。实为恶性循环——
|
||||
* 玩家掉血主因是怪潮全图追 + 九婴冲撞锁方向,退远既不解(怪潮照追、冲撞照锁)又丢甜区 dps,
|
||||
* 把击杀窗拖长 → 暴露更久 → 更掉血。round-8 硬证 hugR 恒 290 时开局 dps=73、玩家满血,死因是
|
||||
* 环绕远漂 + 冲撞贴脸,而非 hugR 太近。∴ 本策略 hugR 全程恒定 290,pDrop 仅记录供死因诊断。 */
|
||||
if (lastBossSample && lastBossSample.playerHp != null) {
|
||||
sample.pDrop = +(lastBossSample.playerHp - s.hp).toFixed(1); // 正=玩家掉血(仅诊断,不再驱动 hugR)
|
||||
}
|
||||
lastBossSample = { timeS: s.timeS, hp: jbAfter.hp, playerHp: s.hp };
|
||||
acc.bossSamples.push(sample);
|
||||
appendFileSync(BOSSRATE_FILE, JSON.stringify(sample) + '\n');
|
||||
console.log('[r7] KILL 九婴 hp=' + jbAfter.hp + '/' + jbAfter.maxHp + ' rate=' + (sample.rate != null ? sample.rate : '—') + ' hp/s dist=' + pd + ' hugR=' + acc.hugR + ' pHp=' + s.hp);
|
||||
}
|
||||
|
||||
if (acc.tick % 3 === 0) appendFileSync(PROBE_FILE, JSON.stringify({ tick: acc.tick, seg: acc.killMode ? 'kill' : 'cruise', frame: await frameCount(cdp), phase: s.phase, timeS: s.timeS, pos: s.pos, hp: s.hp, score: s.score, enemies: s.enemies, level: s.level, weapons: (s.weapons || []).map((w) => w.id + ':' + w.lv), bosses: s.bosses, bossDist: acc.killMode ? acc.lastBossDist : null, seenBosses: s.seenBosses, utc: utcNow() }) + '\n');
|
||||
if (acc.tick % 60 === 1) acc.probeSigs.push((await frameSig(cdp)).sha); // 门E 采样
|
||||
|
||||
/* 探测 bossSpawn/bossKill 事件时刻(events[] 有 eventName)*/
|
||||
if (acc.killMode && acc.tick % 10 === 0) {
|
||||
const m = await measures(cdp);
|
||||
if (m && m.events) {
|
||||
for (const e of m.events) {
|
||||
if (e.eventName === 'bossSpawn' && !acc.bossSpawnUtc) { acc.bossSpawnUtc = utcNow(); acc.bossSpawnFrame = e.frame; }
|
||||
if (e.eventName === 'bossKill' && !acc.bossKillUtc) { acc.bossKillUtc = utcNow(); acc.bossKillFrame = e.frame; }
|
||||
}
|
||||
const names = [...new Set(m.events.map((e) => e.eventName))];
|
||||
acc.eventNamesSeen = [...new Set(acc.eventNamesSeen.concat(names))];
|
||||
}
|
||||
}
|
||||
|
||||
if (s.phase === 'levelup') { await doAction(cdp, { type: 'choose', i: 0 }); acc.inputs.push({ t: acc.killMode ? 'kill' : 'cruise', kind: 'choose', i: 0, level: s.level, utc: utcNow() }); continue; }
|
||||
if (s.phase === 'win' || s.phase === 'lose') { terminal = { phase: s.phase, state: s }; break; }
|
||||
|
||||
acc.progress = { timeS: s.timeS, score: s.score, level: s.level, hp: s.hp, tick: acc.tick, enemies: s.enemies, killMode: acc.killMode, strategy: acc.strategy, bosses: s.bosses, bossDist: acc.killMode ? acc.lastBossDist : null, hugR: acc.hugR, bossKills: s.stats && s.stats.bossKills, weapons: (s.weapons || []).map((w) => w.id + ':' + w.lv), wallSec: Math.round((Date.now() - w0) / 1000) };
|
||||
if (!acc.killMode && acc.tick % 30 === 0) console.log('[r7] cruise timeS=' + s.timeS + ' score=' + s.score + ' lvl=' + s.level + ' hp=' + s.hp + ' enemies=' + s.enemies + ' wall=' + acc.progress.wallSec + 's');
|
||||
if ((Date.now() - w0) / 1000 > WALL_CAP_S) { console.log('[r7] 墙钟上限 @timeS=' + s.timeS + ' killMode=' + acc.killMode + ' wall=' + acc.progress.wallSec + 's,存盘交下次续跑'); break; }
|
||||
}
|
||||
callStart.toTimeS = acc.progress ? acc.progress.timeS : (terminal && terminal.state.timeS);
|
||||
callStart.toTick = acc.tick;
|
||||
callStart.wallSec = Math.round((Date.now() - w0) / 1000);
|
||||
acc.calls.push(callStart);
|
||||
acc.pageErrors = acc.pageErrors.concat(cdp.pageErrors);
|
||||
acc.consoleErrors = acc.consoleErrors.concat(cdp.consoleErrors);
|
||||
|
||||
if (terminal) {
|
||||
/* ── 终态采证收口(⑧win 四件套 + 九门)── */
|
||||
const s = terminal.state;
|
||||
const mFinal = await measures(cdp);
|
||||
if (mFinal && mFinal.events) {
|
||||
for (const e of mFinal.events) {
|
||||
if (e.eventName === 'bossKill' && !acc.bossKillUtc) { acc.bossKillUtc = utcNow(); acc.bossKillFrame = e.frame; }
|
||||
}
|
||||
acc.eventNamesSeen = [...new Set(acc.eventNamesSeen.concat(mFinal.events.map((e) => e.eventName)))];
|
||||
acc.finalEvents = mFinal.events.slice(-12);
|
||||
}
|
||||
const shName = terminal.phase === 'win' ? '07-win-jiuying' : '07-lose-terminal';
|
||||
const sh = await shot(cdp, shName);
|
||||
acc.shots.push({ state: terminal.phase === 'win' ? 'win(斩九婴终态)' : '终态(lose)', ...sh });
|
||||
await step(cdp, 36); const s2 = await state(cdp); // latch:终态驻留
|
||||
const latch = { after: s2 && s2.phase, pass: s2 && s2.phase === terminal.phase };
|
||||
const distinct = new Set(acc.probeSigs).size;
|
||||
acc.gates.E = { samples: acc.probeSigs.length, distinct, pass: distinct >= 2 };
|
||||
acc.gates.B = { pageErrors: acc.pageErrors, consoleErrors: acc.consoleErrors.slice(0, 10), pass: acc.pageErrors.length === 0 };
|
||||
acc.gates.C = { frameFirst: acc.calls[0] && acc.calls[0].fromTimeS, frameLast: s.timeS, note: 'frameCount 严格递增见 probes.jsonl frame 列(bundle.tick 每帧驱动铁证)', pass: null };
|
||||
acc.gates.F = { pass: null, note: '生产 bundle 无 __engineCalls recHook 埋点,SKIP' };
|
||||
acc.gates.H = { latch, scoreProgress: s.score, winReached: terminal.phase, bossKills: s.stats && s.stats.bossKills, pass: !!(latch.pass && (s.score || 0) > 0) };
|
||||
const result = {
|
||||
session: 'r9-win', url: URL, seed: SEED, collector: 'win-jiuying.mjs (node' + process.version + ')', machine: 'dev-local',
|
||||
chrome: 'Chrome/150.0.7871.182 (开发机本地,非 mini-desktop)',
|
||||
startUtc: acc.startUtc, finishUtc: utcNow(), boot: acc.boot,
|
||||
strategyNote: 'round-9「固定安全距稳定输出 + 冲撞切向闪避」(删自保退远反射):坐标驱动把 dist tight-hold 锁进 R=290±小带(径向比例修正阻尼收敛不远漂 + 持续切向环飞永不停步卸怪潮),九婴恒在天雷引/破月/轨道桩 AoE 甜区 sustained≈107dps→3600hp≈34s 可斩;冲撞从九婴逐帧速度推断(base48/蓄势≈12/dash≈173),蓄势期纯切向预错位、dash 期垂直九婴速度全力侧移(冲撞方向蓄势起点锁定+1.3s 预警,持续侧移即落空);dist<235 危险区护底纯退远。删 round-8 恶性反射(玩家掉血多由怪潮/冲撞,退远不解反丢 dps 拖长战斗)——hugR 全程恒定 290,pDrop 仅诊断。balance.js 数值零改(九婴 hp3600/dps30/charge cd12·telegraph1.3·speed3.6)',
|
||||
forensicsProbe: acc.forensicsProbe,
|
||||
gates: acc.gates, shots: acc.shots, calls: acc.calls,
|
||||
killTelemetry: { killMode: acc.killMode, killModeTick: acc.killModeTick, bossSpawnTimeS: acc.bossSpawnTimeS, bossSpawnUtc: acc.bossSpawnUtc, bossKillUtc: acc.bossKillUtc, bossKillFrame: acc.bossKillFrame, eventNamesSeen: acc.eventNamesSeen, finalHugR: acc.hugR, bossSamplesTail: acc.bossSamples.slice(-30) },
|
||||
win: terminal.phase === 'win'
|
||||
? { reached: 'win', timeS: s.timeS, score: s.score, level: s.level, hp: s.hp, revivesUsed: s.revivesUsed, weapons: (s.weapons || []).map((w) => w.id + ':' + w.lv), bossKills: s.stats && s.stats.bossKills, seenBosses: s.seenBosses, shot: sh.name, latch }
|
||||
: { reached: terminal.phase, timeS: s.timeS, score: s.score, level: s.level, hp: s.hp, bosses: s.bosses, bossKills: s.stats && s.stats.bossKills, shot: sh.name, latch },
|
||||
};
|
||||
writeJson(RESULT_FILE, result);
|
||||
writeJson(INPUT_FILE, acc.inputs);
|
||||
writeJson('shots-manifest.json', { utc: utcNow(), session: 'r9-win', shots: acc.shots });
|
||||
console.log('[r7] 终态 ' + terminal.phase + ' @timeS=' + s.timeS + ' score=' + s.score + ' bossKills=' + (s.stats && s.stats.bossKills) + ' latch=' + latch.pass);
|
||||
console.log('[r7] DONE gates=' + JSON.stringify(Object.fromEntries(Object.entries(acc.gates).map(([k, v]) => [k, v.pass]))));
|
||||
try { unlinkSync(STATE_FILE); } catch (_) { /* noop */ }
|
||||
cdp.close(); await closeTarget(targetId);
|
||||
} else {
|
||||
/* ── 未终态:存盘,保 target 活,交下次前台续跑 ── */
|
||||
saveState(acc);
|
||||
writeJson(RESULT_FILE, { session: 'r9-win', partial: true, note: '未终态,分段续跑中', progress: acc.progress, calls: acc.calls, gatesSoFar: acc.gates, killMode: acc.killMode, hugR: acc.hugR, bossSamplesTail: acc.bossSamples.slice(-8) });
|
||||
writeJson(INPUT_FILE, acc.inputs);
|
||||
console.log('[r7] 存盘续跑态 tick=' + acc.tick + ' timeS=' + (acc.progress && acc.progress.timeS) + ' killMode=' + acc.killMode + ' 九婴=' + JSON.stringify(acc.progress && acc.progress.bosses) + ',target 保留');
|
||||
cdp.close(); // 只断 WS,不关 target
|
||||
}
|
||||
console.log('[r7] EXIT 0');
|
||||
} catch (e) {
|
||||
console.error('[r7] FATAL:', (e && e.stack) || e);
|
||||
try { saveState(acc); } catch (_) { /* noop */ }
|
||||
writeJson('00-fatal.json', { utc: utcNow(), error: String((e && e.message) || e), progress: acc.progress, killMode: acc.killMode });
|
||||
try { cdp && cdp.close(); } catch (_) { /* noop */ }
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
Loading…
x
Reference in New Issue
Block a user