fix(hunt): v2 九修 S1-S9 落地——10 人并发模拟迭代裁定(同阵容复测:去留 6/10→10/10)
研究闭环:v1 三轮 28 局四死因(失衡/社交真空/交互摩擦/观光羞辱)→ 四视角分析 → synthesis S1-S9 裁定 → 实施 → 同 10 人配置复测两轮 20 局。依据 research/hunt-sim/ (synthesis.md / metrics-v2.md / report.md),设计登记 23-hunt.md §16。 服务端 hunt-room.js: - S5 协作人声包:真人座位角色接线(杀熊/三连杀/旋风斩≥3 播角色句、greet 全座位); 满编真人房呼喊 2.5s 无 bot 应答 → 邻座角色代答 help_ack;喊话预算 10s 窗 2→3 - S7 难度重构:waveCounts 4 人房 ×4/3;alignPincers 第 3-5 波齐压拍(跨座位同种类 同秒进内圈,种子确定性保持);mobYielded bot 对人类防区怪让位 2s(只约束 bot, 不拦真人避免挥空真话说谎);gradeForEx 双轴评级(S=耐久≥9 且 ≥2 人各 6 杀, 单人带飞局上限 A) 客户端 game.js: - S1 WAVE 下滑恒出击(无怪=空挥真话),菜单收窄到波间休整窗(E3 修订)—— v1 误触菜单单人单局最多 14 次清零 - S2 冷却首拦才震 + 就绪 beat 轻叮(v1 全场 666 次乱滑连震) - S3 0 杀不播 kill_band0(「你还没开张」下架;复测 P08 0杀→12杀) - S4 看门狗重定义:本波出过手一级语音不触发、语音闲置每局 ≤1、一级降纯震 - S5③ help_call/help_ack 永不超时丢弃 - S6 后半平面 loop 低通 750Hz 第二线索(N/S 判读 63/69%→75/81%) - S8 教学作答步(T1-T3)下滑不再误跳 - S9 状态链按变化播(「固若金汤」复读归零) 测试同步:hunt-sim A1/A2/A12 断言更新(真人 greet/波次缩放/预算 3)、 playtest Block 3 按 S1 新语义改写(战斗中下滑=空挥 + 波间菜单) 回归:hunt-sim 16/16 + playtest 213 + guide 132 + engine-voice 19 全绿; balance-scan:挂机局 5/8 落 C/失守带(v1 恒满血)、optimal 单人带飞封顶 A 遗留:营地零损耗未破(18/20 局余10)——v3 头号,参数方案 report.md §5
This commit is contained in:
+116
-19
@@ -72,12 +72,14 @@ const BOT_TICK_MS = 500;
|
||||
const SNAP_MS = 500; // 2Hz
|
||||
const ROOM_CODE_CHARS = '23456789ABCDEFGHJKMNPQRSTUVWXYZ'; // §6.2 无歧义字符集
|
||||
const BOTLINE_WINDOW_MS = 10000; // §7.4 全局节流窗口
|
||||
const BOTLINE_BUDGET = 2; // 窗口内氛围句上限
|
||||
const BOTLINE_BUDGET = 3; // 窗口内氛围句上限(v2 S5① 真人座位角色接线后 2→3;help_* 恒优先)
|
||||
const GREET_FIRST_MS = 1000;
|
||||
const GREET_GAP_MS = 1600; // ≥1.5s(§7.4 greet 间隔)
|
||||
const COVER_MS = 10000; // §7.3 补位偏置持续 10s
|
||||
const RATE_PER_SEC = 20; // §6.1 应用消息速率帽
|
||||
const RATE_STRIKE_MS = 10000; // 再犯判定窗
|
||||
const SECTOR_YIELD_MS = 2000; // v2 S7③:bot 对人类防区怪的让位窗
|
||||
const CALLOUT_ACK_DELAY_MS = 2500; // v2 S5②:无 bot 应答时邻座角色代答延迟
|
||||
|
||||
// §7.2 bot 难度校准(§15 裁决 10 调参;初版 2.2+1.2r 无犹豫实测零泄漏过强):
|
||||
// 反应带更迟、出手决策带空转概率(500ms tick 内愣神)、旋风斩节制。
|
||||
@@ -115,11 +117,12 @@ function waveRng(seed, wave) {
|
||||
*/
|
||||
function buildWaveScript(seed, wave, seatCount) {
|
||||
const spec = WAVE_TABLE[wave];
|
||||
const counts = waveCounts(spec, seatCount);
|
||||
const rng = waveRng(seed, wave);
|
||||
const layout = LAYOUTS[seatCount];
|
||||
const mkinds = [];
|
||||
for (let i = 0; i < spec.wolf; i++) mkinds.push('wolf');
|
||||
for (let i = 0; i < spec.bear; i++) mkinds.push('bear');
|
||||
for (let i = 0; i < counts.wolf; i++) mkinds.push('wolf');
|
||||
for (let i = 0; i < counts.bear; i++) mkinds.push('bear');
|
||||
for (let i = mkinds.length - 1; i > 0; i--) { // Fisher-Yates,走波次 rng
|
||||
const j = Math.floor(rng() * (i + 1));
|
||||
[mkinds[i], mkinds[j]] = [mkinds[j], mkinds[i]];
|
||||
@@ -138,6 +141,67 @@ function buildWaveScript(seed, wave, seatCount) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 波次量按座位缩放(v2 修复 S7①:3 人房维持原值;4 人房 ×4/3——
|
||||
* 6 局真人快速局全部零损耗满血 S 的根因之一是吞吐冗余,见 research/hunt-sim/analysis/01)。
|
||||
*/
|
||||
function waveCounts(spec, seatCount) {
|
||||
const f = seatCount / 3;
|
||||
return {
|
||||
wolf: Math.round(spec.wolf * f),
|
||||
bear: spec.bear ? Math.max(1, Math.round(spec.bear * f)) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 齐压拍(v2 修复 S7②):第 3-5 波把既有剧本里 1-2 组「不同座位」的怪
|
||||
* 重定时到同一毫秒进内圈(数量不变、种子确定性)。单人 2.5s 出击冷却在
|
||||
* 齐压拍数学上必漏 → 呼喊/分工/旋风斩分配第一次成为唯一解。
|
||||
* 纯函数:不动 buildWaveScript(A2 单测口径)。
|
||||
*/
|
||||
function alignPincers(script, seed, wave) {
|
||||
if (wave < 3) return script;
|
||||
const rng = waveRng((seed ^ 0x91CE) >>> 0, wave);
|
||||
const groups = wave === 3 ? 1 : 2;
|
||||
const used = new Set();
|
||||
for (let g = 0; g < groups; g++) {
|
||||
const avail = script.map((e, i) => ({ e, i }))
|
||||
.filter(({ e, i }) => !used.has(i));
|
||||
// 找一对不同座位、同种类的条目(齐压必须跨防区才有强制分工意义;
|
||||
// 同种类保证 eta 原值不动——狼仍是狼速,只是同秒抵达)
|
||||
let pair = null;
|
||||
for (let a = 0; a < avail.length && !pair; a++) {
|
||||
for (let b = avail.length - 1; b > a && !pair; b--) {
|
||||
if (avail[a].e.seat !== avail[b].e.seat && avail[a].e.mkind === avail[b].e.mkind) {
|
||||
pair = [avail[a], avail[b]];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!pair) break;
|
||||
// 对齐到较晚时刻(同秒进内圈),加 ≤300ms 微差防叠声
|
||||
const late = Math.max(pair[0].e.at, pair[1].e.at);
|
||||
pair[0].e.at = late;
|
||||
pair[1].e.at = late + Math.floor(rng() * 300);
|
||||
used.add(pair[0].i);
|
||||
used.add(pair[1].i);
|
||||
}
|
||||
return script.sort((a, b) => a.at - b.at);
|
||||
}
|
||||
|
||||
/**
|
||||
* 评级(v2 修复 S7④ 双轴):S = 耐久≥9 且 至少两名猎人均有 ≥6 个人击杀
|
||||
* (零损耗 + 打了配合的房间才是 S;「1 高手带飞局」上限 A)。
|
||||
* gradeFor 保留原单轴口径供兼容引用。
|
||||
*/
|
||||
function gradeForEx(dur, killsBySlot) {
|
||||
if (dur >= 9) {
|
||||
const contributors = Object.values(killsBySlot || {}).filter((k) => k >= 6).length;
|
||||
if (contributors >= 2) return 'S';
|
||||
return 'A';
|
||||
}
|
||||
return gradeFor(dur);
|
||||
}
|
||||
|
||||
/** §3.6 评级(服务端直发,客户端不算分) */
|
||||
function gradeFor(dur) {
|
||||
if (dur >= 9) return 'S';
|
||||
@@ -349,8 +413,8 @@ class Room {
|
||||
this.startedAt = this.now;
|
||||
this.phase = this.teach ? 'teach' : 'wave';
|
||||
this.broadcastRoster(); // 补位完成花名册
|
||||
this.botSlots().sort((a, b) => a.slot - b.slot).forEach((b, i) => { // §7.4 greet 逐只 ≥1.5s
|
||||
this.pendingLines.push({ at: GREET_FIRST_MS + i * GREET_GAP_MS, slot: b.slot, line: 'greet' });
|
||||
this.occupiedSlots().sort((a, b) => a.slot - b.slot).forEach((s, i) => { // v2 S5①:真人座位角色也打招呼(§7.4 间隔)
|
||||
if (s.char) this.pendingLines.push({ at: GREET_FIRST_MS + i * GREET_GAP_MS, slot: s.slot, line: 'greet' });
|
||||
});
|
||||
if (this.teach) {
|
||||
// §5 教学波:3 狼、逼近 12s、突入零惩罚。狼 1 至多补放 2 次(做错重教 ≤2 次)。
|
||||
@@ -365,8 +429,10 @@ class Room {
|
||||
this.wave = n;
|
||||
this.broadcastEv({ kind: 'wave', wave: n });
|
||||
const base = this.ts();
|
||||
this.script = buildWaveScript(this.seed, n, this.seatCount)
|
||||
.map((e) => ({ ...e, atTs: base + e.at }));
|
||||
this.script = alignPincers(
|
||||
buildWaveScript(this.seed, n, this.seatCount).map((e) => ({ ...e, atTs: base + e.at })),
|
||||
this.seed, n,
|
||||
);
|
||||
this.scriptIdx = 0;
|
||||
if (n >= 2) { // §7.4 wave 喊话:第 2 波起轮转一只 bot
|
||||
const bots = this.botSlots().sort((a, b) => a.slot - b.slot);
|
||||
@@ -387,12 +453,24 @@ class Room {
|
||||
if (alt) d = alt;
|
||||
}
|
||||
const id = this.nextMobId++;
|
||||
const mob = { id, mkind, dir: d, eta };
|
||||
const mob = { id, mkind, dir: d, eta, seat, innerAt: null }; // seat/innerAt:bot 让位判定(S7③)
|
||||
this.mobs.set(id, mob);
|
||||
this.broadcastEv({ kind: 'spawn', id, mkind, dir: d, eta });
|
||||
return mob;
|
||||
}
|
||||
|
||||
/**
|
||||
* bot 让位窗(v2 修复 S7③,bot 单侧礼貌版):怪进内圈头 2 秒,bot 不与
|
||||
* 该防区的人类座位抢——弱者的保底出手窗。只约束 bot(对真人不可见、
|
||||
* 不拦截真人出手,避免「挥空真话说谎」的暗墙)。
|
||||
*/
|
||||
mobYielded(mob) {
|
||||
if (mob.innerAt === null || mob.seat === undefined) return false;
|
||||
if (this.now - mob.innerAt >= SECTOR_YIELD_MS) return false;
|
||||
const owner = this.slots[mob.seat];
|
||||
return !!(owner && !owner.bot && owner.alive);
|
||||
}
|
||||
|
||||
breachMob(mob) {
|
||||
this.mobs.delete(mob.id);
|
||||
mob.breached = true;
|
||||
@@ -440,11 +518,12 @@ class Room {
|
||||
}
|
||||
slot.skillCdUntil = this.now + SKILL_CD_MS;
|
||||
slot.lastActAt = this.now;
|
||||
const clearedMobs = [...this.mobs.values()].filter((m) => m.eta <= MELEE_ETA);
|
||||
const clearedMobs = [...this.mobs.values()]
|
||||
.filter((m) => m.eta <= MELEE_ETA && !(slot.bot && this.mobYielded(m))); // bot 旋风斩也让位(S7③)
|
||||
for (const mob of clearedMobs) this.mobs.delete(mob.id);
|
||||
this.broadcastEv({ kind: 'skill', slot: slot.slot, cleared: clearedMobs.map((m) => m.id) });
|
||||
for (const mob of clearedMobs) this.creditKill(slot, mob);
|
||||
if (slot.bot && clearedMobs.length) this.tryBotline(slot, 'skill');
|
||||
if (slot.char && clearedMobs.length >= 3) this.tryBotline(slot, 'skill'); // v2 S5①:真人大清场也报
|
||||
}
|
||||
|
||||
adjudicateCallout(slot) {
|
||||
@@ -468,6 +547,17 @@ class Room {
|
||||
killDone: false,
|
||||
settled: false,
|
||||
};
|
||||
} else {
|
||||
// v2 S5②:满编真人房无 bot 可应——2.5s 后由邻座真人「扮演的角色」代答
|
||||
// help_ack(呼喊-应答听觉闭环在真人房成立的兜底;v3 若加真人应答手势则替换)
|
||||
const neighbor = this.occupiedSlots()
|
||||
.filter((s) => s.slot !== slot.slot && s.char && s.alive)
|
||||
.sort((a, b) => a.slot - b.slot)[0];
|
||||
if (neighbor) {
|
||||
this.pendingLines.push({
|
||||
at: this.ts() + CALLOUT_ACK_DELAY_MS, slot: neighbor.slot, line: 'help_ack', priority: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +567,7 @@ class Room {
|
||||
if (slot.cover && !slot.cover.settled && slot.cover.callerSector.includes(mob.dir)) {
|
||||
slot.cover.killDone = true;
|
||||
}
|
||||
if (slot.bot && slot.char) { // §7.4 kill 喊话:杀熊或三连杀,15s/只节流
|
||||
if (slot.char) { // v2 S5①:真人也「扮演座位角色」——杀熊/三连杀照播角色 kill 句(§3.3 声音身份接线)
|
||||
slot.recentKills.push(this.now);
|
||||
slot.recentKills = slot.recentKills.filter((t) => this.now - t <= 6000);
|
||||
const triple = slot.recentKills.length >= 3;
|
||||
@@ -513,7 +603,7 @@ class Room {
|
||||
|
||||
// ---------------- bot AI(§7.2,500ms tick,确定性) ----------------
|
||||
|
||||
/** 方向压力 P(d)=Σ(1/eta)(仅计 eta≤4);主防区 ×1.5;补位期呼喊者防区 ×2.0(覆盖主防区偏置)。 */
|
||||
/** 方向压力 P(d)=Σ(1/eta)(仅计 eta≤4 且未被让位的怪);主防区 ×1.5;补位期呼喊者防区 ×2.0(覆盖主防区偏置)。 */
|
||||
botPickDir(bot) {
|
||||
const cov = bot.cover && this.now < bot.cover.until ? bot.cover : null;
|
||||
let bestDir = null;
|
||||
@@ -521,7 +611,7 @@ class Room {
|
||||
for (let d = 0; d < 8; d++) {
|
||||
let p = 0;
|
||||
for (const m of this.mobs.values()) {
|
||||
if (DIR_IDX[m.dir] === d && m.eta <= MELEE_ETA) p += 1 / Math.max(m.eta, 0.1);
|
||||
if (DIR_IDX[m.dir] === d && m.eta <= MELEE_ETA && !this.mobYielded(m)) p += 1 / Math.max(m.eta, 0.1);
|
||||
}
|
||||
if (p <= 0) continue;
|
||||
if (cov ? cov.callerSector.includes(DIRS[d]) : bot.sector.includes(DIRS[d])) {
|
||||
@@ -540,14 +630,14 @@ class Room {
|
||||
const close = [...this.mobs.values()].filter((m) => m.eta <= tune.skillEta).length;
|
||||
if (close >= tune.skillMin && this.botRng() < tune.skillP) this.adjudicateSkill(bot);
|
||||
}
|
||||
// 出击:冷却就绪 + max P 方向 + 反应带 + 犹豫空转(裁决 10)
|
||||
// 出击:冷却就绪 + max P 方向 + 反应带 + 犹豫空转(裁决 10);让位怪不抢(S7③)
|
||||
if (this.now >= bot.cdUntil) {
|
||||
const dir = this.botPickDir(bot);
|
||||
if (dir) {
|
||||
const arc = arcOf(dir);
|
||||
let best = null;
|
||||
for (const m of this.mobs.values()) {
|
||||
if (!arc.includes(DIR_IDX[m.dir]) || m.eta > MELEE_ETA) continue;
|
||||
if (!arc.includes(DIR_IDX[m.dir]) || m.eta > MELEE_ETA || this.mobYielded(m)) continue;
|
||||
if (!best || m.eta < best.eta) best = m;
|
||||
}
|
||||
if (best
|
||||
@@ -621,10 +711,10 @@ class Room {
|
||||
this.mobs.clear();
|
||||
this.script = null;
|
||||
this.pendingWave = null;
|
||||
const grade = win ? gradeFor(this.dur) : null; // 失守无评级(§3.6/§15 裁决 9)
|
||||
const kills = {};
|
||||
for (const [k, v] of Object.entries(this.killsBySlot)) kills[k] = v;
|
||||
for (const s of this.occupiedSlots()) kills[String(s.slot)] = s.kills;
|
||||
const grade = win ? gradeForEx(this.dur, kills) : null; // v2 S7④ 双轴评级(失守无评级,§3.6/§15 裁决 9)
|
||||
this.broadcastEv({ kind: 'end', win, dur: this.dur, grade, kills });
|
||||
this.endAt = this.now;
|
||||
}
|
||||
@@ -699,9 +789,10 @@ class Room {
|
||||
}
|
||||
// 教学波推进
|
||||
if (this.phase === 'teach') this.tickTeach();
|
||||
// 怪物推进 + 突入结算
|
||||
// 怪物推进 + 突入结算(进内圈打让位时间戳,S7③)
|
||||
for (const mob of [...this.mobs.values()]) {
|
||||
mob.eta -= dt / 1000;
|
||||
if (mob.innerAt === null && mob.eta <= MELEE_ETA) mob.innerAt = this.now;
|
||||
if (mob.eta <= 0) {
|
||||
this.breachMob(mob);
|
||||
if (this.phase === 'end' || this.destroyed) return;
|
||||
@@ -722,11 +813,13 @@ class Room {
|
||||
this.lastBotAt = this.now;
|
||||
this.botTick();
|
||||
}
|
||||
// 喊话调度(greet 等)
|
||||
// 喊话调度(greet 等;v2 S5② 真人代答 help_ack 也走此队列)
|
||||
while (this.pendingLines.length && this.pendingLines[0].at <= ts) {
|
||||
const l = this.pendingLines.shift();
|
||||
const s = this.slots[l.slot];
|
||||
if (s && s.bot) this.tryBotline(s, l.line);
|
||||
if (s && s.char && s.alive !== false) {
|
||||
this.emitBotline(s, l.line, { priority: !!l.priority });
|
||||
}
|
||||
}
|
||||
// 快照兜底(2Hz)
|
||||
if (this.now - this.lastSnapAt >= SNAP_MS) {
|
||||
@@ -1000,7 +1093,10 @@ module.exports = {
|
||||
createManager,
|
||||
Room,
|
||||
buildWaveScript,
|
||||
waveCounts,
|
||||
alignPincers,
|
||||
gradeFor,
|
||||
gradeForEx,
|
||||
mulberry32,
|
||||
DIRS,
|
||||
LAYOUTS,
|
||||
@@ -1018,4 +1114,5 @@ module.exports = {
|
||||
REST_MS,
|
||||
ROOM_CODE_CHARS,
|
||||
BOT_TUNING_DEFAULT,
|
||||
SECTOR_YIELD_MS,
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import hunt from '../hunt-room.js';
|
||||
|
||||
const {
|
||||
createManager, buildWaveScript, gradeFor, DIRS, LAYOUTS, WAVE_TABLE,
|
||||
createManager, buildWaveScript, waveCounts, gradeFor, gradeForEx, DIRS, LAYOUTS, WAVE_TABLE,
|
||||
TICK_MS, GRACE_MS, REST_MS, LOBBY_MS, END_KEEP_MS,
|
||||
} = hunt;
|
||||
|
||||
@@ -189,7 +189,7 @@ test('A1 全流程:匹配→开局 bot 补位→5 波→胜利结算,事件
|
||||
const end = lastEv(conn, 'end');
|
||||
assert.equal(end.win, true, '1 真人+2 bot 应能守过 5 波');
|
||||
assert.ok(end.dur >= 1 && end.dur <= 10, `余耐久 ${end.dur} 在 1-10`);
|
||||
assert.equal(end.grade, gradeFor(end.dur), '评级与耐久一致(§3.6,归因真话)');
|
||||
assert.equal(end.grade, gradeForEx(end.dur, end.kills), '评级双轴一致(v2 S7④:耐久档 × 参与度,归因真话)');
|
||||
let killSum = 0;
|
||||
for (const [k, v] of Object.entries(end.kills)) {
|
||||
assert.ok(['0', '1', '2'].includes(k));
|
||||
@@ -211,7 +211,7 @@ test('A1 全流程:匹配→开局 bot 补位→5 波→胜利结算,事件
|
||||
assert.ok(snaps[snaps.length - 1].ts <= endTs, '终局后 snap 停发');
|
||||
// ---- botline greet:逐只间隔 ≥1.5s(§7.4) ----
|
||||
const greets = evs(conn, 'botline').filter((e) => e.line === 'greet');
|
||||
assert.equal(greets.length, 2);
|
||||
assert.equal(greets.length, 3, 'greet:2 bot + 真人座位角色(v2 S5① 真人接线)');
|
||||
assert.ok(greets[1].ts - greets[0].ts >= 1500, 'greet 间隔 ≥1.5s');
|
||||
|
||||
// ---- 失守不写榜(服务器侧口径):本局为胜局,反向用 A9 覆盖 ----
|
||||
@@ -228,11 +228,15 @@ test('A2 刷怪剧本 seed 复现:同 seed 同剧本,方位 ∈ 座位防区
|
||||
assert.deepEqual(a, b, `seat=${seatCount} wave=${w} 同 seed 同剧本`);
|
||||
const c = buildWaveScript(43, w, seatCount);
|
||||
assert.notDeepEqual(a, c, '异 seed 异剧本');
|
||||
const spec = WAVE_TABLE[w];
|
||||
const counts = waveCounts(spec, seatCount); // v2 S7①:波次量按座位缩放
|
||||
assert.equal(a.length, counts.wolf + counts.bear, `seat=${seatCount} w=${w} 波次量 = 缩放后狼+熊`);
|
||||
assert.equal(a.filter((e) => e.mkind === 'bear').length, counts.bear, `seat=${seatCount} w=${w} 熊数`);
|
||||
const layout = LAYOUTS[seatCount];
|
||||
a.forEach((e, i) => {
|
||||
assert.equal(e.seat, i % seatCount, '按座位数轮转发牌');
|
||||
assert.ok(layout[e.seat].sector.includes(e.dir), '方位落在该座位防区内');
|
||||
assert.equal(e.at, (i + 1) * WAVE_TABLE[w].gap * 1000, '刷怪间隔 §3.4');
|
||||
assert.equal(e.at, (i + 1) * spec.gap * 1000, '刷怪间隔 §3.4(齐压拍在 startWave 层,纯函数不动)');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -628,17 +632,17 @@ test('A12 botline 全局节流:10s 窗氛围 ≤2 条;help_call/help_ack 优
|
||||
|
||||
const before = evs(conn, 'botline').length;
|
||||
for (let i = 0; i < 5; i++) room.emitBotline(bot, 'greet'); // 连发 5 条氛围句
|
||||
assert.equal(evs(conn, 'botline').length - before, 2, '10s 窗氛围句 ≤2,超出丢弃');
|
||||
assert.equal(evs(conn, 'botline').length - before, 3, '10s 窗氛围句 ≤3(v2 S5① 预算 2→3),超出丢弃');
|
||||
|
||||
// 预算耗尽时呼救语义仍必达
|
||||
room.emitBotline(bot, 'help_ack', { priority: true });
|
||||
assert.equal(evs(conn, 'botline').length - before, 3, 'help_ack 优先不被氛围顶掉');
|
||||
assert.equal(evs(conn, 'botline').length - before, 4, 'help_ack 优先不被氛围顶掉(3 氛围 + 1 优先)');
|
||||
assert.equal(evs(conn, 'botline').filter((e) => e.line === 'help_ack').length, 1);
|
||||
|
||||
// 窗口滑过后恢复
|
||||
fastForward(mgr, 10.5);
|
||||
room.emitBotline(bot, 'greet');
|
||||
assert.equal(evs(conn, 'botline').length - before, 4, '10s 窗滑过后氛围恢复投递');
|
||||
assert.equal(evs(conn, 'botline').length - before, 5, '10s 窗滑过后氛围恢复投递');
|
||||
room.destroy();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user