diff --git a/docs/PLAYING.md b/docs/PLAYING.md index 19b12245..47f5c656 100644 --- a/docs/PLAYING.md +++ b/docs/PLAYING.md @@ -73,7 +73,7 @@ - 拖动时模型一直在指针 / 手指下(脚在指针下方约半格,鼠标和触屏一样),**指针所在的格子**就是落点并高亮;装备拖到哪个格子,就配发给那个格子上的干员;在商店栏上松手则放回原位。 - 点击干员显示攻击范围和底部按钮(「撤退」回到整备区、「出售 +1」);右键 / 长按查看详细信息:头像旁是盟约,下面第一项就是干员的**特质**(如「休整期结束时 …」),之后是特性、属性、攻击范围、技能、模组、装备、天赋。属性是**实时数值**:作战中每秒刷新 4 次(当前生命、攻击、防御、法抗、攻击间隔、阻挡),休整期显示棋盘上的干员**开战时**的数值(已算入装备、盟约层数、特质、策略与机变效果);高于基础值为绿色、低于为红色并标出差值(攻击间隔变短算增益),鼠标悬停可看基础值。 - **召唤物手动摆放**:把带可部署召唤物的干员放上棋盘后,它的召唤物会进入整备区(赫默的医疗探机、巫恋的诅咒娃娃、凯瑟琳的 2 个支援装置、浊心斯卡蒂的海嗣、伺夜的狼群、缪尔赛思的流形;同一干员的召唤物叠在一格,不占部署位),像干员一样拖到棋盘上并选方向。凯瑟琳的支援装置给**它面朝的那一格**上的干员加屏障,所以要放在干员旁边、朝向干员。**战术家的援军**(伺夜的狼群、缪尔赛思的流形,也就是选战术点)和官方一样**只能放在召唤者的攻击范围内**:拖动时只有这些格子会亮起;之后在原地给战术家换朝向时,不在新攻击范围里的援军会退回整备区(还在范围里的保持不动;整备区和临时整备区都满、援军无处可退时,这次换朝向不生效并提示「整备区已满」)。撤退或出售干员时它的召唤物一起移除;**移动干员到别的格子时(包括把召唤物拖到干员身上交换位置),它已摆放的召唤物会退回整备区**,需要重新摆放。整备区满时退回的召唤物进入临时整备区,休整期结束还没处理的会在下一回合重新回到整备区。 -- 作战中:海嗣、狼群、流形、支援装置开战时就在摆放的位置部署;医疗探机、诅咒娃娃开战时也在摆放的位置免费部署一次(持续 10 / 15 秒,和官方一样不需要先开技能),之后所属干员每次发动技能时再次在原位出现;所属干员被击倒、再部署回场时,技能给的那一个如果还没出现,会立刻出现。巫恋退场时诅咒娃娃随之撤退,赫默退场时已出现的医疗探机不受影响。医疗探机、诅咒娃娃、支援装置、海嗣没有摆放就不会出现;狼群、流形(战术家的援军)没有摆放时会在战术点出现。狼群最后一只狼影被击倒后进入战术点形态:暂时离开战斗(不阻挡、不攻击、不会被攻击,格子仍然占着),经过狼影的恢复时间(25 秒)后在原地满血、带着 1 只狼影回来,之后重新开始每 25 秒加 1 只狼影;伺夜退场时狼群随之撤退,伺夜再部署回场时狼群重新带着 2 只狼影出现。 +- 作战中:海嗣、狼群、流形、支援装置开战时就在摆放的位置部署;医疗探机、诅咒娃娃开战时也在摆放的位置免费部署一次(持续 10 / 15 秒,和官方一样不需要先开技能),之后所属干员每次发动技能时再次在原位出现;所属干员被击倒、再部署回场时,技能给的那一个如果还没出现,会立刻出现。巫恋退场时诅咒娃娃随之撤退,赫默退场时已出现的医疗探机不受影响。医疗探机、诅咒娃娃、海嗣(以及自选干员的工程蓄水炮、沙地兽、打字机、本能的召唤)这类不会受到攻击、到时间就离场的召唤物不受任何伤害(活性源石也一样)、也不会被治疗,它们的血条就是倒计时:随时间减少,空了就离场。医疗探机、诅咒娃娃、支援装置、海嗣没有摆放就不会出现;狼群、流形(战术家的援军)没有摆放时会在战术点出现。狼群最后一只狼影被击倒后进入战术点形态:暂时离开战斗(不阻挡、不攻击、不会被攻击,格子仍然占着),经过狼影的恢复时间(25 秒)后在原地满血、带着 1 只狼影回来,之后重新开始每 25 秒加 1 只狼影;伺夜退场时狼群随之撤退,伺夜再部署回场时狼群重新带着 2 只狼影出现。 - **阻挡**看接触:干员阻挡中心距离约 0.7 格以内、与它接触的敌人(地面干员挡不住飞行和近地悬浮的敌人)。敌人卡在两名干员之间时,阻挡它的干员倒下、撤退或被晕眩后,另一名仍与它接触、还有空余阻挡数的干员会**立刻接替阻挡**;都没有空位时敌人才会走掉。迎面走来的敌人会停在干员格子前面。**阻挡了就一定能打到**:任何干员(包括站在近战位的远程干员和召唤物)总能攻击自己阻挡的敌人,不管它在不在攻击范围内、在干员哪一侧,并且优先攻击它(医疗干员除外:咒愈师以外的医疗挡住敌人时照常治疗队友,不会攻击它;「停止攻击」类技能期间也除外;技能原文另有限制的也按原文:普罗旺斯二技能开启时不以生命值高于 80% 的敌人为目标,寒檀二技能的冰凌随机落在攻击范围内的地块上)。 - 开战时干员按**逐列从左到右、每列从上到下**的顺序部署(最左一列最先,与阿戈尔吞噬「更靠左和靠上」的顺序相同),召唤物在所有干员之后。被阻挡的敌人先打阻挡它的干员;没被阻挡的远程敌人先打嘲讽等级高的,同级时打最后部署的(召唤物、最右一列靠下的干员和刚再部署的干员更容易挨打)。隐匿的干员只会被它阻挡的敌人攻击,没被它挡住的敌人的溅射、爆炸、范围技能、留下的伤害区域(包括假想敌:蚀裂被击倒后喷出的毒雾)以及深池伙友卫队、扎罗的减攻速光环也落不到它身上(和官方一样;写明「无视无法选择」的效果如污染秽蚀照常能打到,寒霜的减攻速光环和“墓碑”这类地图效果照常生效);**迷彩**(如忍冬三技能期间击倒敌人后、寒芒克洛丝一技能)同样只会被它阻挡的敌人攻击,也显示为半透明,但躲不开溅射、爆炸和范围伤害(同时打范围内所有单位的普通攻击——如“斩胄之剑”“破胄之锤”“灵幛”——打不到它),也不算隐匿(不触发叙拉古等「隐匿时」效果)。叙拉古的隐匿按时(32 秒 + 每层 0.4 秒)结束;忍冬的迷彩按官方持续到她下一次开技能。**蒂比起飞**期间只挡飞行敌人,地面敌人不会选她为目标(普通攻击、溅射、技能范围、燃烧区域和要选中目标的减益都落不到她身上);飞行和悬浮敌人、写明「无视无法选择」的效果(如污染秽蚀)照常能打到她;地面敌人的光环和全场效果(如“墓碑”让全场攻击力、防御力减半)、她攻击敌人时招来的反制(如重装侦察兵的暴露)照常生效,起飞前已挂上的持续伤害也照常跳。 - **技能自动释放**按官方「技能策略」:一般的手动技能在技能就绪、即将攻击 / 治疗且初始攻击范围内有敌人(治疗技能:有受伤的队友)时释放;**带独立技能范围**的技能(如德克萨斯「剑雨」、凛御银灰「御敌的锋锐」、锏「归于宁静」,不含「攻击范围扩大」类)在**技能范围内出现敌人**时就释放,不必先攻击;**开启后攻击范围比平时大**的手动技能(如莫斯提马「序时之匙」、银灰「真银斩」、史尔特尔「黄昏」、白面鸮「脑啡肽」)在**开启后的攻击范围内出现敌人**(治疗技能:有受伤的队友)时就释放,不必等敌人走进平时的攻击范围,这是本作按玩家反馈有意与官方不同的地方;**重装干员**的手动技能在受到伤害时释放(例外:深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能按玩家反馈改为攻击范围内有敌人时就释放(深巡二技能按开启后更远的攻击范围判断),余的二技能「厚礼上宾」改为周围两格(技能范围)内有敌人时就释放,这是本作有意与官方不同的地方;深巡一技能仍要受到伤害);执旗手 / 战术家 / 吟游者技能就绪即释放,解放者 / 阵法术师在初始攻击范围内有敌人时释放(开启后攻击范围扩大的技能——薄绿「风语」、蜜蜡「沙暴扩散」、卡涅利安「食噬之印」、玛恩纳「未宽解的悲哀」,以及安洁莉娜「秘杖·反重力模式」——同样按开启后的攻击范围判断)。**自动触发**的技能按其本身规则:只作用于队友的(如引星棘刺「度算浪波」、浊心斯卡蒂「同葬无光之愿」、纯烬艾雅法拉「无声润物」)技力满就释放,不用等敌人或伤员;伺夜「领袖的呼唤」「领袖的馈赠」只在场上有狼群时释放:狼群处于战术点形态时,「领袖的呼唤」也会释放并让它立刻带着 1 只狼影回到场上(狼影未满时改为加 1 只狼影,已满时把狼群的生命值回满),「领袖的馈赠」也会释放(立即获得费用,强化留给回来的狼群的下一次攻击);又如古米「备用军粮」在身边(技能范围内)有受伤的队友时就会发动,下一次攻击改为治疗该队友(不用先接敌;她的二技能「食粮烹制」是手动技能,按重装规则在受到伤害时释放);塞雷娅「急救」在她即将攻击时、身边有生命不高于一半的队友才发动,这次攻击直接改为治疗;新约能天使二技能的护盾按官方给攻击范围内**仇恨最高**的队友(嘲讽等级高的优先,同级时给最后部署的),不一定是站在最前面挨打的那个。手动技能的自动释放有 3 秒冷却:开战部署后先等 3 秒,之后同一干员两次自动释放至少间隔 3 秒(可充能的技能不会同时放完)。 diff --git a/docs/SIM.md b/docs/SIM.md index 41662235..586f7e7f 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -126,7 +126,10 @@ owner's decision of 2026-10-06, kits/ops/op-wang.js); a skill's summon (赫默 S and regardless of the holding (PRTS §作战阶段 "所有手动部署的召唤物,无视所属干员的持有状态…作战开始时立即部署一次"), then waits on its tile and takes the field there each time the skill gives one (stock ≤ 1, after the token's redeploy time once it left, free; never while its owner is off the field [ASSUMED] — a stocked one deploys as soon as the owner is -back; the doll also leaves when 巫恋 leaves, the drone stays when 赫默 leaves: PRTS token 备注). A skill's summon, a +back; the doll also leaves when 巫恋 leaves, the drone stays when 赫默 leaves: PRTS token 备注). The countdown summons — +those that "不会受到攻击" and leave after a fixed time (医疗探机, 诅咒娃娃, 海嗣; 自选: 工程蓄水炮, 沙地兽, “打字机”, 本能的召唤; +content/tokens.js `COUNTDOWN_SUMMONS`) — hold 无敌 + 禁疗 from each deployment (`startCountdown`) and their snapshot hp is +maxHp × the share of their life left (`unit.countdown`; community report of 2026-10-06, 0.2.0). A skill's summon, a device or 海嗣 that was not placed never appears (the hidden 待部署区 deploys nothing by itself); the tacticians' 狼群 / 流形 still come as their 援军 on a tactical point without a piece (content/tokens.js `tacticalPoint`). Their piece stands inside the tactician's attack range — the prep enforces "只能部署在召唤者攻击范围内" (tokens.json `ownerRange`, player diff --git a/server/sim/battle/deploy.js b/server/sim/battle/deploy.js index d5c23965..06fc44cf 100644 --- a/server/sim/battle/deploy.js +++ b/server/sim/battle/deploy.js @@ -32,6 +32,7 @@ export class BattleDeploy { u.removed = false; u.hidden = false; u.body = null; + u.countdown = null; // a countdown summon's content starts its new life in the deploy hook (content/tokens.js startCountdown) u.x = C0; u.y = R0; u.tileR = R0; u.tileC = C0; u.blocking = []; u.deploySeq = ++this._deploySeq; diff --git a/server/sim/content/kits/ops/chess_char_6_04-skadi2.js b/server/sim/content/kits/ops/chess_char_6_04-skadi2.js index 2f537b03..4c249cf5 100644 --- a/server/sim/content/kits/ops/chess_char_6_04-skadi2.js +++ b/server/sim/content/kits/ops/chess_char_6_04-skadi2.js @@ -4,6 +4,7 @@ import { aggregateMods } from '../../../buffs.js'; import { COLS } from '../../../constants.js'; import { bardRegen } from '../../../professions.js'; +import { startCountdown } from '../../tokens.js'; import { num, bv, tbb, tdesc, moduleBb, parseN, live, enemiesIn, isTok, onDefaultSkill, selectedSkill, aura, } from '../shared/tier6.js'; @@ -148,6 +149,7 @@ function skadi2(bb, chess, def) { if (t.profile) t.profile.noAttack = true; const dur = num(t.def?.talents?.[0]?.bb?.duration, parseN(tdesc(def, 0), /持续(\d+(?:\.\d+)?)秒/, 25)); const seq = t.deploySeq, r = t.tileR, c = t.tileC; + startCountdown(battle, t, dur); // a countdown summon: 无敌, 禁疗, its bar = the life left (content/tokens.js) battle.after(dur, () => { if (!t.alive || t.deploySeq !== seq) return; battle.retreat(t, { reason: 'expired', permanent: true }); diff --git a/server/sim/content/kits/ops/op-bgsnow.js b/server/sim/content/kits/ops/op-bgsnow.js index b0336b0b..c20c1e91 100644 --- a/server/sim/content/kits/ops/op-bgsnow.js +++ b/server/sim/content/kits/ops/op-bgsnow.js @@ -44,6 +44,7 @@ import { bodyKeys, bodyInKeys } from '../../../body.js'; import { toLocal } from '../../../dir.js'; import { hasHp } from '../../../damage.js'; import { COLS } from '../../../constants.js'; +import { startCountdown } from '../../tokens.js'; const S1 = 'skchr_bgsnow_1'; const S2 = 'skchr_bgsnow_2'; @@ -141,9 +142,11 @@ function typewriterKit(t, owner, chess) { } }], install(battle, unit) { if (sk?.id === TS1) installIamb(battle, unit, num(tb.prob), num(tb.atk_scale, 1), skillOn); - // 持续25秒: leaves `life` s after each deployment + // 持续25秒: leaves `life` s after each deployment — a countdown summon (content/tokens.js startCountdown): 无敌, + // 禁疗 [ASSUMED], its bar = the life left battle.on('deploy', (ctx) => { if (ctx.unit !== unit || !(life > 0)) return; + startCountdown(battle, unit, life); const seq = unit.deploySeq; battle.after(life, () => { if (unit.alive && unit.deploySeq === seq) battle.retreat(unit, { reason: 'expired', permanent: true }); }, { owner: unit }); }, { owner: unit }); diff --git a/server/sim/content/kits/ops/op-phatm2.js b/server/sim/content/kits/ops/op-phatm2.js index 9635920d..7c489ab8 100644 --- a/server/sim/content/kits/ops/op-phatm2.js +++ b/server/sim/content/kits/ops/op-phatm2.js @@ -63,7 +63,7 @@ import { num, up, traitBb, talentBb, skillRec, once } from '../shared/tier1.js'; import { isElite, onElementHit, elementDmg } from '../shared/tier6.js'; -import { releaseSkillSummon } from '../../tokens.js'; +import { releaseSkillSummon, startCountdown } from '../../tokens.js'; import { absoluteRangeKeys } from '../../../targeting.js'; import { bodyInKeys } from '../../../body.js'; import { hasHp, burstLocked } from '../../../damage.js'; @@ -124,6 +124,7 @@ function callKit(t, owner, b2) { lured = []; endAt = battle.time + life; atk = owner.s.atk; // 缓存攻击力: her ATK at the deployment (PRTS 备注) + startCountdown(battle, tok, life); // a countdown summon (content/tokens.js): 无敌, 禁疗 [ASSUMED], its bar = the life left const seq = tok.deploySeq; battle.after(life, () => { if (tok.alive && tok.deploySeq === seq) battle.retreat(tok, { reason: 'expired', permanent: true }); }, { owner: tok }); }, { owner: tok }); diff --git a/server/sim/content/kits/ops/op-ray.js b/server/sim/content/kits/ops/op-ray.js index 7c709665..2cf11db2 100644 --- a/server/sim/content/kits/ops/op-ray.js +++ b/server/sim/content/kits/ops/op-ray.js @@ -62,6 +62,7 @@ import { acquireTargets, effectiveProfile } from '../../../ai.js'; import { sortEnemyTargets } from '../../../targeting.js'; import { bodyInKeys } from '../../../body.js'; import { PUSH_DIRECTIONAL_MIN_DIST } from '../../../constants.js'; +import { startCountdown } from '../../tokens.js'; const S1 = 'skchr_ray_1'; const S2 = 'skchr_ray_2'; @@ -225,6 +226,7 @@ export default { m.rayBullets = 0; m.rayCounted = 0; battle.setExtraRange(unit, m.rayAreaKeys); + startCountdown(battle, t, life); // a countdown summon (content/tokens.js): its bar = the life left const seq = t.deploySeq; battle.after(life, () => { if (t.alive && t.deploySeq === seq) battle.retreat(t, { reason: 'expired', permanent: true }); }, { owner: unit }); }, { owner: unit }); diff --git a/server/sim/content/kits/ops/op-weedy.js b/server/sim/content/kits/ops/op-weedy.js index 8d61cb70..961a14be 100644 --- a/server/sim/content/kits/ops/op-weedy.js +++ b/server/sim/content/kits/ops/op-weedy.js @@ -48,6 +48,7 @@ import { num, traitBb, skillRec, up, giveSp } from '../shared/tier1.js'; import { absoluteRangeKeys } from '../../../targeting.js'; import { COLS } from '../../../constants.js'; +import { startCountdown } from '../../tokens.js'; const S1 = 'skchr_weedy_1'; const S2 = 'skchr_weedy_2'; @@ -174,9 +175,10 @@ function cannonKit(t, owner) { }, install(battle, unit) { battle.addBuff(unit, { key: 'trait:weedyCannon', flags: { noHeal: true }, persist: true, allowDead: true }); // 持有禁疗 - // 部署后20秒自动撤退 + // 部署后20秒自动撤退 — a countdown summon (content/tokens.js startCountdown): 无敌 [ASSUMED], 禁疗, its bar = the life left battle.on('deploy', (ctx) => { if (ctx.unit !== unit || !(life > 0)) return; + startCountdown(battle, unit, life); const seq = unit.deploySeq; battle.after(life, () => { if (unit.alive && unit.deploySeq === seq) battle.retreat(unit, { reason: 'expired', permanent: true }); }, { owner: unit }); }, { owner: unit }); diff --git a/server/sim/content/tokens.js b/server/sim/content/tokens.js index d9234261..ddb23029 100644 --- a/server/sim/content/tokens.js +++ b/server/sim/content/tokens.js @@ -5,8 +5,10 @@ // stats / talents / trait / skill at the owner's phase — a golden owner gets the `_b` variant). // Every tokenId of data/tokens.json has a kit, so `battle.spawnToken(owner, tokenId, r, c)` works with data defaults // even when the summoner's kit passes no options: -// 医疗探机 heal profile (data), untargetable, self-destructs after its withdraw skill time (10 s) -// 诅咒娃娃 no attack, aura: enemies in its range ATK/DEF + bb.atk/bb.def (−25 %/−30 %), 15 s (skill duration); +// 医疗探机 heal profile (data), untargetable, self-destructs after its withdraw skill time (10 s); a countdown +// summon (COUNTDOWN_SUMMONS: 无敌, 禁疗, its bar = the life left) +// 诅咒娃娃 no attack, aura: enemies in its range ATK/DEF + bb.atk/bb.def (−25 %/−30 %), 15 s (skill duration; a +// countdown summon); // leaves when 巫恋 leaves (PRTS 备注) — the drone stays when 赫默 leaves (PRTS 医疗探机 备注) // 沙之碑 on appear: owner ATK × atk_scale arts + stun (skill range 3×3), blocks 3 (no attack), talent duration 20 s // 战术装备 on appear: stun around (bb.stun), blocked enemies DEF + talent def (−160), talent duration 25 s @@ -14,7 +16,7 @@ // 斯卡蒂的海嗣 untargetable range extension of 浊心斯卡蒂: her trait's 生命回复速度 on the allies in its range (owner // ATK × trait ratio /s); // while the owner's skill runs: owner ATK × atk_scale true dmg/s to enemies + 鼓舞 owner ATK × bb.atk; -// talent duration 25/30 s, then redeploys after respawnTime (30/25 s, DP cost from data) +// talent duration 25/30 s (a countdown summon), then redeploys after respawnTime (30/25 s, DP cost from data) // “耀阳” on appear: owner ATK × atk_scale true + stun in the skill grid (+1 hit if the previously deployed // operator is 卡西米尔); golden trait ×atk_scale vs blocked enemies; lasts while the owner's skill runs // 纸偶 on appear: token ATK × damage_scale arts to the 8 surrounding tiles; does not block (data) @@ -76,6 +78,7 @@ // makes the token (Battle.spawnToken also refuses summons the owner's loadout does not produce). // // Exports for other content: spawnYanyou, spawnMapChar, findSummonTile, summonToken, tacticalPoint, wolfShadows, +// COUNTDOWN_SUMMONS / startCountdown (the countdown summons' 无敌 + 禁疗 and timer bar, shared with the kits that time them), // wolfShadowInterval, installWolfTacticalPoint, wolfTacticalPoint, wolfReturnNow (the 狼群's 战术点形态, shared with the // 伺夜 kit's own pack, kits/ops/chess_char_3_19-vigil.js), releaseSkillSummon, SKILL_SUMMON_START_DEPLOY, CAT_SHIELD_KEY, // TOKEN_IDS; mapCharTalents and touchGospel (Touch's 攫升 / 超脱 and 恳切福音, shared with the Touch 补位 stand-in kit, @@ -192,10 +195,42 @@ function untargetable(battle, unit) { battle.addBuff(unit, { key: 'trait:untargetable', flags: { untargetable: true }, persist: true, allowDead: true }); } -/** Withdraw the token `seconds` after this deployment (the same deployment only). */ +/** + * Countdown summons — the summons that "不会受到攻击" and leave after a fixed time: 赫默's 医疗探机 (10 s, skcom_withdraw), + * 巫恋's 诅咒娃娃 (15 s, its skill), 浊心斯卡蒂's 斯卡蒂的海嗣 (talent 远古血亲 25 / 30 s) and, among the 自选 operators, 温蒂's + * 工程蓄水炮 (20 s), 莱伊's 沙地兽 (25 s), 鸿雪's “打字机” (25 s) and 酒神's 本能的召唤 (10 s). Community report of 2026-10-06 + * (the official mode): they take no outside damage — 活性源石 included —, no operator heals them and their bar runs down + * like a timer, the summon leaving when it is empty. PRTS / client data: 海嗣 and 沙地兽 hold 无敌 + 禁疗 (PRTS 海嗣 备注 + * 「持有禁疗、无敌」; token prefabs: abnormal flags 5 + 7), 工程蓄水炮 禁疗 (PRTS 备注); the others have no abnormal flag in the + * base-game prefabs — [ASSUMED] the report's 无敌 + 禁疗 for every one of them. startCountdown gives them both (their HP never + * moves) and the bar (Unit.countdown, shown by snapshot.js as the share of the life left). The other timed summons (沙之碑, + * 战术装备, “小自在”, 结构性原理 …) block and are attacked: their HP stays HP. Until 0.2.0 a 海嗣 on 活性源石 lost 70 HP/s (of + * 100) and a hurt drone drew every medic's heals. + */ +export const COUNTDOWN_SUMMONS = Object.freeze(new Set([ + TOKEN_IDS.healDrone, TOKEN_IDS.curseDoll, TOKEN_IDS.seaborn, + 'token_10009_weedy_cannon', 'token_10034_ray_sndbst', 'token_10026_bgsnow_subbow', 'token_10054_phatm2_encdool', +])); +const COUNTDOWN_KEY = 'token:countdown'; + +/** + * A countdown summon's life on the field starts (COUNTDOWN_SUMMONS; call it from the `deploy` hook of each deployment, + * where its kit schedules its withdrawal): `seconds` from now its bar is empty. It holds 无敌 and 禁疗 from then on. A + * summon that is not a countdown one is left alone (false). + */ +export function startCountdown(battle, unit, seconds) { + const s = num(seconds, 0); + if (!unit || !COUNTDOWN_SUMMONS.has(unit.defId) || !(s > 0)) return false; + unit.countdown = { from: battle.time, until: battle.time + s }; + if (!unit.findBuff(COUNTDOWN_KEY)) battle.addBuff(unit, { key: COUNTDOWN_KEY, flags: { invulnerable: true, noHeal: true, healFree: true }, persist: true, allowDead: true }); + return true; +} + +/** Withdraw the token `seconds` after this deployment (the same deployment only); a countdown summon's bar runs with it. */ function scheduleLifetime(battle, unit, seconds) { const s = num(seconds, 0); if (!(s > 0)) return; + startCountdown(battle, unit, s); const seq = unit.deploySeq; unit.mem.expiresAt = battle.time + s; battle.after(s, () => { if (unit.alive && unit.deploySeq === seq) battle.retreat(unit, { reason: 'expired', permanent: true }); }, { owner: unit }); diff --git a/server/sim/snapshot.js b/server/sim/snapshot.js index 9236d662..8c1794bb 100644 --- a/server/sim/snapshot.js +++ b/server/sim/snapshot.js @@ -1,6 +1,7 @@ // server/sim/snapshot.js — compact serialization for clients (DESIGN §8.2). // // b.snap = { fieldId, t, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]], dp, killed, total } +// (hp of a countdown summon — unit.countdown, content/tokens.js startCountdown — is maxHp × the share of its life left) // UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, motion?, boss?, uid?, // form?, skillIndex?, moduleId?, items?, standInFor?, diy? } (standInFor = the replaced operator's charId of a 补位 // stand-in; diy = a 自选 piece's pick { charId, skillIndex, uniEquipId } — defId is its slot, spine / avatar the operator's; @@ -107,9 +108,13 @@ export function unitTuple(u, t) { sp = spMax * (sk.timeLeft / sk.duration); } } - // hp is rounded up (a living unit never shows 0) but never above the rounded max HP + // hp is rounded up (a living unit never shows 0) but never above the rounded max HP. A countdown summon (医疗探机, 海嗣 …: + // content/tokens.js startCountdown) shows the share of its life left instead — its bar runs down like a timer and it + // leaves when it is empty (community report of 2026-10-06; its HP itself never moves: 无敌, 禁疗) const maxHp = Math.max(1, Math.round(u.s.maxHp)); - const hp = u.alive ? Math.min(Math.max(1, Math.ceil(u.hp)), maxHp) : 0; + const cd = u.alive ? u.countdown : null; + const left = cd ? Math.max(0, Math.min(1, (cd.until - t) / Math.max(1e-9, cd.until - cd.from))) : 1; + const hp = u.alive ? Math.min(Math.max(1, Math.ceil(cd ? maxHp * left : u.hp)), maxHp) : 0; return [u.id, r2(u.x), r2(u.y), hp, maxHp, r1(sp), spMax, flagsOf(u), animOf(u, t)]; } diff --git a/server/sim/units.js b/server/sim/units.js index 1a1258ab..d2431fae 100644 --- a/server/sim/units.js +++ b/server/sim/units.js @@ -81,6 +81,9 @@ export class Unit { this.burstPending = null; // { [element]: true } while that element's burst resolves (damage.js burstLocked) this.tags = new Set(init.tags || []); this.mem = {}; // free scratch space for content (per unit) + // a countdown summon's life on the field ({ from, until } battle times; content/tokens.js startCountdown, cleared by + // every deployment): its bar shows the time left (snapshot.js unitTuple), its HP never moves (无敌 + 禁疗) + this.countdown = null; this.trait = {}; // profession runtime state this.stats = { dmg: 0, kills: 0, heal: 0, taken: 0, attacks: 0 }; this.hidden = false; // enemies inside a DISAPPEAR segment diff --git a/test/golden/bonds.json b/test/golden/bonds.json index 66062058..ae57ca87 100644 --- a/test/golden/bonds.json +++ b/test/golden/bonds.json @@ -1712,7 +1712,7 @@ "hooks": [1,28,1790,191,191,208,208,32,18,21,16,16,0,8141,3,27,17,1,1,18,0,0,0], "events": {"atk":226,"deploy":11,"die":20,"dmg":208,"engage":6,"fx":54,"heal":32,"leak":1,"skill":32,"spawn":26,"status":26}, "fx": {"aoe":1,"displace":19,"liberate":3,"phase":4,"pull":19,"strike":3,"summon":2,"volley":3}, - "snaps": "4f49e250 3e34819c f8f00406 187f8d75 6cef4054 31a272c7" + "snaps": "351c8009 987b22dd 7d790a0c fadc11f1 fb446f70 8805598c" }, "bond-visiShip-high": { "about": "远见 high: tier 1 layers 999; chess_char_4_17_b chess_char_4_18_b chess_char_3_03_b chess_char_4_19_b chess_char_4_16_b chess_char_2_02_b chess_char_4_20_b chess_char_4_21_b token_10000_silent_healrb", @@ -1750,7 +1750,7 @@ "hooks": [1,34,2169,158,158,244,298,40,22,25,15,14,0,3657,17,18,18,0,1,22,0,0,9], "events": {"atk":162,"deploy":14,"die":25,"dmg":298,"engage":4,"fx":92,"heal":23,"skill":31,"spawn":29,"status":31}, "fx": {"dodge":9,"featherArrow":1,"flame":3,"garrisonGrant":1,"healField":1,"pulse":43,"riposte":9,"summon":4,"swordRain":17,"swordStorm":4}, - "snaps": "8108fcf3 523bf24b 4a55bea2 c6042705 c8db73ef 3aeb96c2 7a035fd1 8b9f2773" + "snaps": "f2695385 772aaaa3 406ef0c7 3308eca7 65753010 432ff52b 07e6e68b 2aca82b1" }, "bond-yanShip-low": { "about": "炎 low: tier 1 layers 1; chess_char_3_16_a chess_char_3_03_a chess_char_3_18_a chess_char_1_03_a chess_char_2_04_a chess_char_3_14_a chess_char_3_17_a chess_char_3_19_a token_10028_vigil_wolf", diff --git a/test/golden/fields.json b/test/golden/fields.json index 35fc2c3b..b096f4ea 100644 --- a/test/golden/fields.json +++ b/test/golden/fields.json @@ -81,7 +81,7 @@ "hooks": [1,70,6000,1208,1208,2984,3373,4986,32,41,94,88,9,39706,2798,15,29,0,1,32,0,21,0], "events": {"atk":1512,"deploy":35,"die":41,"dmg":3089,"engage":17,"fx":630,"heal":489,"skill":182,"spawn":68,"status":684}, "fx": {"beam":54,"blink":1,"bombard":9,"bombardShell":9,"burst":21,"copy":3,"dp":4,"drone":335,"droneLock":17,"drones":3,"firewall":1,"garrisonGrant":1,"lightning":108,"link":1,"lock":9,"radiantSword":4,"shell":8,"snow":5,"split":2,"storm":10,"summon":8,"talent":4,"tide":5,"wanted":8}, - "snaps": "d9836110 12e5b436 35c19c21 8b602c4b 63a29594 e92667ad 11995740 f731d336 2bcec2e1 d8cbc3bd 9017d64b 51d18505 e6ef48bf 94c31471 ab8f2380 1b98c997 13c68f78 a6b5eaa0 e318ae23 fd4a1367" + "snaps": "72dc81cb e1b819af f6f48b33 12da5c05 71ab4302 b8094e94 48f11686 168adccb 8baeac29 a827fcb5 f2c51503 f6ac9a0e 26f4f8c7 1e115b43 3689b856 f9c504d5 6a28e830 a1160c66 1e744db9 78024c8a" }, "boss-boss_1-solo": { "about": "假想敌:胄 solo pool 1800000: chess_char_5_08_b chess_char_5_11_b chess_char_5_05_b chess_char_5_06_b chess_char_5_13_b chess_char_5_07_b chess_char_5_02_b chess_char_5_03_b chess_char_5_10_b chess_char_5_12_b", @@ -325,7 +325,7 @@ "tide": 5, "wanted": 3 }, - "snaps": "b5e578c7 08019f4b 2414497e e8ccf404 0a757964 27f19ffa dc979221 9a9cddad 37887448 9597c3ad 3331f98b 22dca952 01026ee5 afe32a6d 65ca8fec ed617f42 3ed37aad 2975ad94 80313e62 c870870c" + "snaps": "87a11c35 cc0e1f70 2de88ef7 a5a4efc1 36878065 d2f20d65 a5481200 93371ab3 fa830c40 8d59e885 222cca53 040b3f01 dc7f3982 cca5f067 8a116542 f1f56dac c68f3562 6833a03c 12743320 0f665121" }, "boss-boss_3-solo": { "about": "假想敌:管 solo pool 2000000: chess_char_5_08_b chess_char_5_11_b chess_char_5_05_b chess_char_5_06_b chess_char_5_13_b chess_char_5_07_b chess_char_5_02_b chess_char_5_03_b chess_char_5_10_b chess_char_5_12_b", @@ -606,7 +606,7 @@ "tide": 3, "wanted": 8 }, - "snaps": "726b45e6 655ee79a 912e8d78 fe351355 d02de871 2de23192 90fc2124 37da201e 332c960d a6a1a21f 4df5e422 d780f15c" + "snaps": "eca813c6 f271e40a c26e5584 d73b51c9 a7c760e5 818a39be 850539df a8a1d78c 1c2b94fb 61f626e7 15b5d45a 89f18974" }, "boss-boss_5-solo": { "about": "卢西恩,“猩红血钻” solo pool 780000: chess_char_5_08_b chess_char_5_11_b chess_char_5_05_b chess_char_5_06_b chess_char_5_13_b chess_char_5_07_b chess_char_5_02_b chess_char_5_03_b chess_char_5_10_b chess_char_5_12_b", @@ -865,7 +865,7 @@ "hooks": [1,52,6000,1475,1475,3513,3867,3107,22,29,95,90,263,39065,1638,4,17,0,1,22,0,13,0], "events": {"atk":1666,"deploy":35,"die":29,"dmg":3557,"engage":19,"fx":565,"heal":514,"skill":185,"spawn":50,"status":588}, "fx": {"airstrike":92,"beam":4,"blink":2,"burst":13,"column":47,"copy":4,"dp":5,"drone":334,"droneLock":16,"drones":3,"explode":6,"garrisonGrant":1,"link":1,"phase":1,"shieldBreak":4,"snow":5,"split":5,"steal":7,"sunBurst":4,"talent":4,"tide":5,"wanted":2}, - "snaps": "aac21795 826f3a92 c07605ac 63ba2617 10d034dd f637231c 679eaf63 4484256d 932a1025 95a8f717 9f641051 49ed0ece cc247786 d907a3ce 4899d3c0 a603253f 2e8677a7 fc4cd6c6 ce276a0b 7cb6d808" + "snaps": "7ec7bc8a 80b84ac8 9ef4ab5e 2af0def9 18e8a357 b2a774b5 3da4fec2 83128f9e baca593e da6a4488 535145ce d7c8184c 6695103d 37c5ae18 9014d962 41b8cdf9 a33c7228 5e09f2b0 bd896428 bffa666e" }, "boss-boss_7-solo": { "about": "“萨米的意志” solo pool 2000000: chess_char_5_08_b chess_char_5_11_b chess_char_5_05_b chess_char_5_06_b chess_char_5_13_b chess_char_5_07_b chess_char_5_02_b chess_char_5_03_b chess_char_5_10_b chess_char_5_12_b", @@ -1077,36 +1077,36 @@ "killed": 8, "total": 8, "unspawned": 0, - "bossHpLeft": 4049210, + "bossHpLeft": 4046346, "rngDraws": 1680, "errors": {"count":0,"kinds":[]}, "players": { - "p1": {"killed":8,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1426827,"bossDmg":597682,"heal":13214,"deaths":5,"layerGains":{}}, - "p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2554978,"bossDmg":913911,"heal":130784,"deaths":8,"layerGains":{}} + "p1": {"killed":8,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":1435418,"bossDmg":600545,"heal":13215,"deaths":5,"layerGains":{}}, + "p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2553890,"bossDmg":913911,"heal":130783,"deaths":8,"layerGains":{}} }, "units": [ - ["p1",1,"op","chess_char_6_03_b",2,"uniequip_002_yu",0,0,58,0,0,10883,4690,0,2], + ["p1",1,"op","chess_char_6_03_b",2,"uniequip_002_yu",0,0,60,0,0,10883,4690,0,2], ["p1",2,"op","chess_char_6_06_b",2,"uniequip_002_pepe",2939,1,11,0,23455,2258,0,35,1], ["p1",3,"op","chess_char_6_07_b",2,"uniequip_002_siege2",2990,1,17,0,52147,2547,0,50,2], ["p1",4,"op","chess_char_6_01_b",2,"uniequip_002_lemuen",99.06,1,7,1,1043569,3226,0,57,8], ["p1",5,"op","chess_char_6_02_b",2,"uniequip_002_sbell2",931.61,1,9,0,31824,10406,0,46,2], - ["p1",6,"op","chess_char_6_04_b",2,"uniequip_002_skadi2",745.66,1,52,0,8749,9134,562,0,4], + ["p1",6,"op","chess_char_6_04_b",2,"uniequip_002_skadi2",745.66,1,52,0,17203,9134,563,0,4], ["p1",7,"op","chess_char_6_05_b",2,"uniequip_002_pasngr",2451.4,1,13,1,81581,0,0,0,11], - ["p1",8,"op","chess_char_6_08_b",2,"uniequip_002_reed2",1668,1,19,1,32005,0,7161,70,2], - ["p1",9,"op","chess_char_6_09_b",0,"uniequip_002_cello",0,0,21,1,131535,12922,0,27,27], - ["p1",10,"op","chess_char_6_11_b",2,"uniequip_002_mlyss",1893,1,56,0,14353,9097,349,78,2], - ["p1",11,"token","token_10017_skadi2_dedant",null,null,0,0,28,0,0,130,0,0,null], + ["p1",8,"op","chess_char_6_08_b",2,"uniequip_002_reed2",1668,1,19,0,31409,0,7161,70,2], + ["p1",9,"op","chess_char_6_09_b",0,"uniequip_002_cello",0,0,21,2,130635,12922,0,27,27], + ["p1",10,"op","chess_char_6_11_b",2,"uniequip_002_mlyss",1893,1,57,0,15985,9097,349,78,2], + ["p1",11,"token","token_10017_skadi2_dedant",null,null,130.4,0,28,0,0,0,0,0,null], ["p1",12,"token","token_10030_mlyss_wtrman",null,null,0,0,24,0,0,1614,292,4,1], - ["p2",13,"op","chess_char_6_15_b",2,"uniequip_002_qiubai",0,0,60,3,471039,50514,0,332,3], - ["p2",14,"op","chess_char_6_19_b",2,"uniequip_002_blkkgt",0,0,61,0,247360,13406,0,68,4], - ["p2",15,"op","chess_char_5_01_b",1,"uniequip_002_excu2",0,0,59,0,13275,9642,2640,45,2], - ["p2",16,"op","chess_char_6_17_b",2,"uniequip_002_nearl2",2458.52,1,62,1,509240,22200,0,58,2], + ["p2",13,"op","chess_char_6_15_b",2,"uniequip_002_qiubai",0,0,62,3,469951,50514,0,332,3], + ["p2",14,"op","chess_char_6_19_b",2,"uniequip_002_blkkgt",0,0,63,0,247360,13406,0,68,4], + ["p2",15,"op","chess_char_5_01_b",1,"uniequip_002_excu2",0,0,61,0,13275,9642,2640,45,2], + ["p2",16,"op","chess_char_6_17_b",2,"uniequip_002_nearl2",2458.52,1,64,1,509240,22200,0,58,2], ["p2",17,"op","chess_char_6_12_b",1,"uniequip_002_rosmon",2886.8,1,8,0,264522,6236,0,75,3], ["p2",18,"op","chess_char_6_13_b",1,"uniequip_002_angel2",2215,1,14,0,662969,6421,4283,285,6], ["p2",19,"op","chess_char_6_14_b",2,"uniequip_002_lumen",2222.83,1,20,0,0,2781,19999,79,1], ["p2",20,"op","chess_char_6_16_b",2,"uniequip_002_halo2",0,0,10,0,80392,3176,881,101,3], ["p2",21,"op","chess_char_6_18_b",2,"uniequip_002_whitw2",1331,1,27,0,255595,5370,0,32,3], - ["p2",22,"op","chess_char_6_20_b",2,"uniequip_002_agoat2",0,0,25,0,0,17059,102981,70,2], + ["p2",22,"op","chess_char_6_20_b",2,"uniequip_002_agoat2",0,0,25,0,0,17059,102980,70,2], ["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null], ["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null], ["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null], @@ -1118,20 +1118,23 @@ ["p1","~2","token","token_10030_mlyss_wtrman",null,null,131.08,0,48,0,1401,2025,0,19,null], ["p1","~3","token","token_10030_mlyss_wtrman",null,null,0,0,49,0,120,1322,0,2,null], ["p1","~1","token","token_10040_siege2_vlion",null,null,3722.5,0,50,0,4750,277,0,10,null], - ["p2","~2","token","token_10019_nearl2_sword",null,null,3093.17,0,53,0,44047,4069,0,25,null], - ["p1","~2","token","token_10040_siege2_vlion",null,null,3933.26,0,55,0,0,81,0,0,null], - ["p1","~4","token","token_10030_mlyss_wtrman",null,null,1322,1,57,0,0,180,160,0,1] + ["p1","~1","token","token_10017_skadi2_dedant",null,null,130.4,0,53,0,0,0,0,0,null], + ["p2","~2","token","token_10019_nearl2_sword",null,null,3093.17,0,54,0,44047,4069,0,25,null], + ["p1","~2","token","token_10040_siege2_vlion",null,null,3933.26,0,56,0,0,81,0,0,null], + ["p1","~4","token","token_10030_mlyss_wtrman",null,null,1322,1,58,0,0,180,160,0,1], + ["p1","~2","token","token_10017_skadi2_dedant",null,null,100,0,59,0,0,0,0,0,null], + ["p1","~3","token","token_10017_skadi2_dedant",null,null,100,1,65,0,0,0,0,0,null] ], "enemies": { "enemy_1267_nhpbr_2": [6,6,0,2895,141894], "enemy_9007_acelem": [2,2,0,23398,309587], - "enemy_9017_achunt_2": [2,0,0,59262,1550790], - "enemy_9018_actrpa": [1,0,0,43264,577004], - "enemy_9019_actrpb": [1,0,0,21943,535849], - "enemy_9020_actrpc": [1,0,0,24229,938579] + "enemy_9017_achunt_2": [2,0,0,59131,1553654], + "enemy_9018_actrpa": [1,0,0,43264,579324], + "enemy_9019_actrpb": [1,0,0,21943,537009], + "enemy_9020_actrpc": [1,0,0,24229,939739] }, - "hooks": [1,60,6000,1767,1767,7110,13865,2612,25,35,93,88,317,45858,2401,14,13,0,1,29,0,17,0], - "events": {"atk":2141,"deploy":41,"die":35,"dmg":13390,"engage":22,"fx":2180,"heal":905,"skill":181,"spawn":49,"status":750}, + "hooks": [1,63,6000,1767,1767,7114,13887,2610,24,37,93,88,317,45858,2398,14,13,0,1,28,0,17,0], + "events": {"atk":2140,"deploy":44,"die":37,"dmg":13412,"engage":22,"fx":2180,"heal":906,"skill":181,"spawn":52,"status":751}, "fx": { "airstrike": 99, "beam": 24, @@ -1173,7 +1176,7 @@ "wanted": 6, "zone": 6 }, - "snaps": "9797dd57 d3ba8d58 32d06887 bf42877c 1994874a e5491557 66d383f3 1312fd65 b74b9f4a 2f28845d ead771e6 c391a1a4 f923753b 23bee15f caa10968 93e7fa5f 8c6cfd06 4d79d333 2710458e 1a607645" + "snaps": "72f6af28 6f72c7d4 04c65e53 d68ea41c 34b0f4bf 38a30bbc b2d8c8dd 8d93cc3f 748b9cb6 4a9137e0 a01dbf1e ebdbc3fd a7ffe178 2c92480a 1e31807b 97b40094 e5feed1a 763383f6 f063bb7e d211d016" }, "hidden-boss_9-solo": { "about": "假想敌:铳 solo pool 2800000: chess_char_5_08_b chess_char_5_11_b chess_char_5_05_b chess_char_5_06_b chess_char_5_13_b chess_char_5_07_b chess_char_5_02_b chess_char_5_03_b chess_char_5_10_b chess_char_5_12_b", @@ -1397,7 +1400,7 @@ "hooks": [1,25,5100,20,20,57,129,2358,12,15,6,5,7,9981,13,1,11,0,1,12,0,2,0], "events": {"atk":28,"bounty":1,"deploy":14,"die":15,"dmg":78,"engage":3,"fx":57,"skill":11,"spawn":22,"status":43}, "fx": {"bombard":7,"bombardShell":7,"burst":2,"camouflage":1,"lightning":14,"lock":7,"lure":1,"snow":12,"storm":1,"talent":1,"tide":3,"wanted":1}, - "snaps": "9845e2f6 5498943b a2c8865c 4947e8f1 7b75af29 70cf729f e3aec65c 1ab76abd 919d0c1a 8f2a907c 676cd0dc 4c514cee d56b2743 decf1034 44bec2d5 8b318871 593802f3" + "snaps": "c558a4df 5e12c668 02dad932 00fbc7af bc41cf43 ad41bea1 6e07fbe6 27dde52f 53b9a18c a640dd14 452ad2b3 46658458 8c81ee61 4c70f7fe c0fba143 3b8d81c6 aa0fc004" }, "unite-2": { "about": "联防 2 helper(s), 11 leaked: chess_char_6_15_b chess_char_6_17_b chess_char_6_09_b chess_char_6_11_b chess_char_6_12_b chess_char_6_13_b chess_char_6_14_b chess_char_6_16_b token_10030_mlyss_wtrman | chess_char_6_19_b chess_char_5_01_b chess_char_5_05_b chess_char_5_06_b chess_char_6_18_b chess_char_6_20_b chess_char_5_02_b chess_char_5_03_b", diff --git a/test/golden/roster.json b/test/golden/roster.json index 10e7e3c8..8d0a089a 100644 --- a/test/golden/roster.json +++ b/test/golden/roster.json @@ -184,7 +184,7 @@ "hooks": [1,105,3300,580,580,689,804,233,86,90,24,23,0,16480,226,29,85,0,1,90,1,0,0], "events": {"atk":681,"bounty":6,"deploy":17,"die":90,"dmg":774,"engage":9,"fx":109,"heal":203,"layer":1,"skill":47,"spawn":101,"status":221}, "fx": {"aoe":1,"blink":1,"displace":8,"ember":1,"explode":10,"garrison":1,"heal":5,"phase":1,"pulse":42,"reveal":1,"revive":1,"sleep":8,"summon":25,"undying":1,"zone":3}, - "snaps": "c29e3835 28b841b8 6caebbf9 58f57d98 6a790fcb 928cecdc 15ff1081 552544af 00dc4f61 848f1a18 310f6b34" + "snaps": "e30955f8 499cf905 25671f69 6dfb7b8d de2f1106 a052f01c c47c18ad 9c42fbaa 3cf7b1f7 86643fea 37f1f719" }, "roster-004": { "about": "pass 0 band 2: chess_char_2_08_a chess_char_2_17_a chess_char_2_18_a chess_char_2_07_a chess_char_2_09_a chess_char_2_11_a chess_char_2_13_a chess_char_2_16_a chess_char_2_12_a chess_char_2_06_a chess_char_2_10_a chess_char_2_14_a", @@ -263,24 +263,24 @@ "bossHpLeft": null, "rngDraws": 334, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":83,"total":84,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":10,"dmg":306697,"bossDmg":0,"heal":37320,"deaths":0,"layerGains":{}}}, + "players": {"p1":{"killed":83,"total":84,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":10,"dmg":306697,"bossDmg":0,"heal":37410,"deaths":0,"layerGains":{}}}, "units": [ ["p1",1,"op","chess_char_2_07_b",1,"uniequip_002_ghost",2664.2,1,2,0,0,1707,1318,0,0], ["p1",2,"op","chess_char_2_08_b",1,"uniequip_002_bubble",3373,1,4,0,49,1818,0,0,1], ["p1",3,"op","chess_char_2_09_b",1,"uniequip_002_humus",2133,1,6,28,84020,0,0,59,2], ["p1",4,"op","chess_char_2_11_b",1,"uniequip_002_kazema",2214,1,8,12,91244,15658,0,92,3], - ["p1",5,"op","chess_char_2_12_b",1,"uniequip_002_gravel",1452,1,10,2,7180,9327,0,129,1], + ["p1",5,"op","chess_char_2_12_b",1,"uniequip_002_gravel",1452,1,10,2,7180,9417,0,129,1], ["p1",6,"op","chess_char_2_19_a",1,null,1021.2,1,11,1,14957,1433,314,26,3], ["p1",7,"op","chess_char_2_01_b",1,"uniequip_002_excu",2146,1,12,20,36079,4363,0,22,1], ["p1",8,"op","chess_char_2_02_b",1,"uniequip_002_silent",1614,1,1,0,0,0,9366,28,6], ["p1",9,"op","chess_char_2_04_b",1,"uniequip_002_grabds",1270,1,3,10,38233,2115,0,53,3], ["p1",10,"op","chess_char_2_05_b",1,"uniequip_002_harold",1971.2,1,5,0,0,0,11081,40,2], - ["p1",11,"op","chess_char_2_06_b",0,"uniequip_002_papyrs",1639,1,7,0,0,0,12106,27,13], + ["p1",11,"op","chess_char_2_06_b",0,"uniequip_002_papyrs",1639,1,7,0,0,0,12196,27,13], ["p1",12,"op","chess_char_2_10_b",1,"uniequip_002_rockr",1406,1,9,5,34936,900,0,113,2], ["p1",13,"token","token_10000_silent_healrb",null,null,1000,1,104,0,0,0,3137,30,null] ], "enemies": { - "enemy_10027_vtsk": [1,0,0,10462,23138], + "enemy_10027_vtsk": [1,0,0,10552,23138], "enemy_1006_shield_2": [1,1,0,0,8000], "enemy_10122_uacann_2": [1,1,0,3476,8000], "enemy_1042_frostd": [1,1,0,0,5200], @@ -295,10 +295,10 @@ "enemy_1509_mousek": [1,1,0,7192,17600], "enemy_2010_csdcr": [1,1,0,1986,9600] }, - "hooks": [1,103,4200,714,714,1650,1643,1085,83,89,37,36,0,29028,143,53,84,0,1,83,0,0,0], - "events": {"atk":928,"bounty":5,"deploy":19,"die":89,"dmg":1603,"engage":7,"fx":66,"heal":134,"skill":73,"spawn":97,"status":439}, + "hooks": [1,103,4200,714,714,1650,1643,1083,83,89,37,36,0,29028,143,53,84,0,1,83,0,0,0], + "events": {"atk":926,"bounty":5,"deploy":19,"die":89,"dmg":1603,"engage":7,"fx":66,"heal":134,"skill":73,"spawn":97,"status":433}, "fx": {"counter":2,"explode":3,"expose":9,"overload":2,"phase":1,"shield":8,"shieldBreak":16,"sleep":7,"summon":6,"telegraph":6,"zone":6}, - "snaps": "de0d6fce aa9dbd18 71199952 51ab68f4 a5c1cf26 39c299cd 389a7224 beb950e8 500c1fa6 c0a3a103 d99c8758 679ff285 e0d2bbaf 675fa07e" + "snaps": "84edc036 a46838b0 1d9bc714 1b570575 aab0920b 4bc6df97 dd7f1064 918ab11b 31267425 a4452600 f9a6edeb 9a33bdd8 c9a35f1c 7d3f9999" }, "roster-006": { "about": "pass 0 band 3: chess_char_2_17_b chess_char_2_18_b chess_char_3_06_a chess_char_2_13_b chess_char_2_16_b chess_char_3_02_a chess_char_3_03_a chess_char_3_04_a chess_char_3_05_a chess_char_2_14_b chess_char_2_19_b chess_char_3_01_a", @@ -1072,7 +1072,7 @@ "hooks": [1,154,6900,951,1017,7281,7430,78804,91,140,40,36,17,35845,1032,57,126,45,1,102,1,7,6], "events": {"atk":2283,"bounty":3,"deploy":28,"die":95,"dmg":7385,"engage":10,"fx":552,"heal":121,"layer":1,"leak":45,"skill":76,"spawn":142,"status":2410}, "fx": {"aoe":8,"bombard":17,"bombardShell":17,"burst":7,"crit":50,"dodge":6,"explode":40,"firewall":2,"garrison":1,"iceSpike":90,"lightning":148,"lock":17,"lure":3,"revive":1,"snow":82,"soul":1,"sp":1,"storm":5,"talent":2,"telegraph":8,"tide":3,"wanted":38,"weightless":5}, - "snaps": "ee17d257 d832b8ac 2ca210aa 3cd3fc22 022376cf a362140e 0ca09d66 1f446040 29245222 70bfb33a 2b4ce12f 51a65ad5 1216d2fa f2d3b899 86e068de 5a0bbda6 1bb53622 0ef45b3c a98167d6 5245239e eac46280 b7e2f8ef 905be587" + "snaps": "76864815 62849bad 979bf882 25b6b06a 5180c227 3ecd4ada 973ab3dc 34c4af0f c0357d8d 21dd059d b2e1cc4c 8a773c5e 69aa5545 f41c0b6e 34d38c11 fd5b4e85 3a8cd96d 541d94f7 b7150f71 e0eeba75 f3b17a4b 864eec43 64dedb33" }, "roster-017": { "about": "pass 0 band 6: chess_char_6_15_a chess_char_6_19_a chess_char_6_07_a chess_char_6_17_a chess_char_6_08_a chess_char_6_09_a chess_char_6_11_a chess_char_6_12_a chess_char_6_13_a chess_char_6_14_a chess_char_6_16_a chess_char_6_18_a", @@ -1227,7 +1227,7 @@ "hooks": [1,146,5863,543,543,1347,1516,3220,125,132,42,38,32,26310,394,7,122,0,1,125,11,5,0], "events": {"atk":634,"bounty":6,"deploy":22,"die":132,"dmg":1435,"engage":13,"fx":264,"heal":169,"layer":11,"skill":80,"spawn":146,"status":444}, "fx": {"beam":5,"bombard":32,"bombardShell":32,"burst":5,"camouflage":2,"copy":1,"dp":5,"garrison":8,"lightning":99,"link":1,"lock":32,"lure":3,"scorchBurst":9,"snow":7,"split":5,"storm":7,"summon":2,"talent":1,"tide":5,"wanted":3}, - "snaps": "d98f113c d0331366 be0cebae 241fe6f7 134c6492 39edc9da 96df2aaa b5e300fe 865fba99 4cc163f0 3748edd7 acc90429 29160015 97e57a26 cabd46be 14cde004 c11ac045 97b93efb 317c26e6 835a5563" + "snaps": "9cdb845f 1a3da81a b8f8fa18 18adb6a1 045ac790 92daeb67 e9b25891 6153ba1f 318ab774 dbcd5d11 bd86ccae 2804e1a9 0b939c36 d69ed2f9 d52ba02d c74b0ce7 bf1bc7e3 b8b19432 7a71afd1 478ab631" }, "roster-019": { "about": "pass 0 band 7: chess_char_6_15_b chess_char_6_19_b chess_char_5_01_b chess_char_5_05_b chess_char_6_17_b chess_char_6_13_b chess_char_6_14_b chess_char_6_16_b chess_char_6_18_b chess_char_6_20_b chess_char_5_02_b chess_char_5_03_b", @@ -2195,7 +2195,7 @@ "hooks": [1,143,5589,311,311,508,779,843,126,129,71,69,0,13806,388,3,125,0,1,126,2,11,0], "events": {"atk":502,"bounty":4,"deploy":18,"die":129,"dmg":779,"engage":6,"fx":218,"heal":9,"layer":2,"skill":140,"spawn":142,"status":553}, "fx": {"bombard":3,"bombardShell":3,"burst":11,"copy":1,"expose":117,"frostNova":1,"garrison":2,"heal":3,"phase":1,"shieldBreak":1,"snow":14,"sp":58,"talent":1,"wanted":2}, - "snaps": "378db087 c01e4bfb 656d09bb 8ad40d0d 980adb80 0fbc6dfa 0affdeb4 5f9cb80b 96e5686c ad4f2604 0b08bcd0 e88e1b52 18910b94 829e4b19 4a00d92e 630d2588 f81f3544 df9493eb ce728565" + "snaps": "7d4b3aca f6affb06 724b3085 0ddf76eb d68a8dda a623e364 9c9d2e86 9998c540 c8bc041f 5e9130db 6dfbcc7b c6ec5c11 f21a16a2 e1fcd736 9bf63db1 696fbd1b 2c7e2d3c e2ef6c77 9b42d582" }, "roster-036": { "about": "pass 1 band 7: chess_char_6_03_b chess_char_6_15_a chess_char_6_19_a chess_char_6_17_a chess_char_6_12_a chess_char_6_13_a chess_char_6_14_a chess_char_6_16_a chess_char_6_18_a chess_char_6_20_a chess_char_6_01_b chess_char_6_02_b", @@ -2264,68 +2264,70 @@ "time": 217.2, "ticks": 6516, "reason": "cleared", - "killed": 38, + "killed": 45, "total": 131, "unspawned": 0, "bossHpLeft": null, - "rngDraws": 100, + "rngDraws": 90, "errors": {"count":0,"kinds":[]}, "players": { "p1": { - "killed": 38, + "killed": 45, "total": 131, "perfect": false, - "leaks": 93, + "leaks": 86, "leaksUncounted": 0, - "coins": 4, - "dmg": 994699, + "coins": 6, + "dmg": 1011569, "bossDmg": 0, - "heal": 30633, - "deaths": 25, - "layerGains": {"sargonShip":14} + "heal": 42323, + "deaths": 24, + "layerGains": {"sargonShip":16} } }, "units": [ - ["p1",1,"op","chess_char_6_06_b",0,"uniequip_003_pepe",0,0,89,1,39371,6491,513,13,3], - ["p1",2,"op","chess_char_6_15_b",0,"uniequip_003_qiubai",0,0,6,2,124903,6154,3570,27,6], - ["p1",3,"op","chess_char_6_07_b",0,"uniequip_003_siege2",0,0,9,2,110073,2661,0,15,5], - ["p1",4,"op","chess_char_6_04_b",0,"uniequip_003_skadi2",3523.2,1,169,0,0,10506,3936,0,3], - ["p1",5,"op","chess_char_6_05_b",0,"uniequip_003_pasngr",0,0,5,4,302318,1502,0,33,16], - ["p1",6,"op","chess_char_6_08_b",0,"uniequip_003_reed2",0,0,96,0,72197,8332,5006,52,1], - ["p1",7,"op","chess_char_6_09_b",1,"uniequip_003_cello",1352,1,170,0,57079,4352,296,20,2], - ["p1",8,"op","chess_char_6_11_b",0,"uniequip_003_mlyss",1873,1,171,3,23389,8393,901,58,2], - ["p1",9,"op","chess_char_6_12_b",0,"uniequip_003_rosmon",0,0,60,1,123922,5390,87,22,6], - ["p1",10,"op","chess_char_6_13_b",0,"none",1684.2,1,161,0,76484,11597,1724,50,5], - ["p1",11,"op","chess_char_6_14_b",0,"uniequip_003_lumen",0,0,15,0,0,12802,14600,13,5], - ["p1",12,"op","chess_char_6_16_b",0,"none",0,0,76,2,61875,2648,0,15,1], - ["p1",13,"token","token_10017_skadi2_dedant",null,null,0,0,17,0,0,100,0,0,null], - ["p1",14,"token","token_10030_mlyss_wtrman",null,null,0,0,11,0,201,1608,0,6,1], + ["p1",1,"op","chess_char_6_06_b",0,"uniequip_003_pepe",0,0,90,0,29221,6236,258,7,3], + ["p1",2,"op","chess_char_6_15_b",0,"uniequip_003_qiubai",0,0,6,1,115832,4600,2016,23,5], + ["p1",3,"op","chess_char_6_07_b",0,"uniequip_003_siege2",0,0,9,0,76591,2661,0,11,4], + ["p1",4,"op","chess_char_6_04_b",0,"uniequip_003_skadi2",3523.2,1,172,0,0,20406,11891,0,4], + ["p1",5,"op","chess_char_6_05_b",0,"uniequip_003_pasngr",0,0,5,10,297865,1502,0,34,17], + ["p1",6,"op","chess_char_6_08_b",0,"uniequip_003_reed2",0,0,98,1,68469,7787,4461,49,1], + ["p1",7,"op","chess_char_6_09_b",1,"uniequip_003_cello",1352,1,171,1,72879,4204,148,19,2], + ["p1",8,"op","chess_char_6_11_b",0,"uniequip_003_mlyss",1873,1,160,1,48208,7012,1393,94,2], + ["p1",9,"op","chess_char_6_12_b",0,"uniequip_003_rosmon",0,0,67,2,165357,7444,2140,26,8], + ["p1",10,"op","chess_char_6_13_b",0,"none",2005,1,169,0,71982,10725,2630,45,4], + ["p1",11,"op","chess_char_6_14_b",0,"uniequip_003_lumen",0,0,15,0,0,12802,13143,13,5], + ["p1",12,"op","chess_char_6_16_b",0,"none",0,0,81,0,62580,3399,751,17,1], + ["p1",13,"token","token_10017_skadi2_dedant",null,null,100,0,17,0,0,0,0,0,null], + ["p1",14,"token","token_10030_mlyss_wtrman",null,null,0,0,11,0,550,4736,3127,14,1], ["-","~1","device","trap_1105_accrate",null,null,100,0,1,0,0,0,0,0,null], ["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null], ["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null], - ["p1","~1","token","token_10030_mlyss_wtrman",null,null,0,0,43,0,522,1497,0,1,1], - ["p1","~2","token","token_10030_mlyss_wtrman",null,null,0,0,78,0,497,1192,0,1,1], - ["p1","~3","token","token_10030_mlyss_wtrman",null,null,0,0,125,0,1778,2145,0,4,1], - ["p1","~4","token","token_10030_mlyss_wtrman",null,null,0,0,153,0,90,1805,0,3,1], - ["p1","~5","token","token_10030_mlyss_wtrman",null,null,1804.5,1,172,0,0,0,0,0,1] + ["p1","~1","token","token_10030_mlyss_wtrman",null,null,0,0,32,0,104,1608,0,3,null], + ["p1","~1","token","token_10017_skadi2_dedant",null,null,100,0,54,0,0,0,0,0,null], + ["p1","~2","token","token_10030_mlyss_wtrman",null,null,0,0,80,0,0,1865,0,0,0], + ["p1","~2","token","token_10017_skadi2_dedant",null,null,100,0,109,0,0,0,0,0,null], + ["p1","~3","token","token_10030_mlyss_wtrman",null,null,0,0,125,0,1696,1684,363,5,1], + ["p1","~4","token","token_10030_mlyss_wtrman",null,null,0,0,161,0,233,1805,0,6,1], + ["p1","~3","token","token_10017_skadi2_dedant",null,null,100,1,173,0,0,0,0,0,null] ], "enemies": { - "enemy_10042_prtrop": [1,1,0,775,17199], - "enemy_10118_ymgprc": [1,0,1,12755,112019], - "enemy_1045_hammer_2": [18,3,15,3234,313913], - "enemy_1061_zomshd": [60,10,50,34663,543507], + "enemy_10042_prtrop": [1,1,0,2278,17199], + "enemy_10118_ymgprc": [1,0,1,4232,122751], + "enemy_1045_hammer_2": [18,3,15,15972,315355], + "enemy_1061_zomshd": [60,11,49,36815,558519], "enemy_1071_dftman": [1,1,0,0,8026], - "enemy_1172_dugago": [1,0,1,1428,38609], - "enemy_1210_msfden_2": [1,0,1,0,42], - "enemy_1269_nhfly": [45,23,22,19110,3747], + "enemy_1172_dugago": [1,0,1,2855,46471], + "enemy_1210_msfden_2": [1,1,0,0,45], + "enemy_1269_nhfly": [45,28,17,22220,0], "enemy_1321_wdarft": [1,0,1,0,0], - "enemy_1501_demonk": [1,0,1,13122,15956], - "enemy_2052_smgia": [1,0,1,0,43424] + "enemy_1501_demonk": [1,0,1,11941,20299], + "enemy_2052_smgia": [1,0,1,0,22008] }, - "hooks": [1,170,6516,408,408,2395,2585,24079,69,163,61,57,27,9225,1280,27,131,93,1,48,2,5,0], - "events": {"atk":518,"bounty":2,"deploy":36,"die":70,"dmg":2504,"engage":15,"fx":80,"heal":48,"layer":2,"leak":93,"skill":118,"spawn":153,"status":679}, - "fx": {"airstrike":6,"aoe":11,"burst":5,"copy":6,"explode":23,"garrison":2,"heal":3,"healField":5,"link":2,"revive":1,"stone":1,"summon":15}, - "snaps": "79552059 e5891b97 05581a54 4cf8b8b4 4c68e31e 6d0f79f9 326b9fac c54d2228 53ac3aee 4caa7e6f 63d4e63f 466be41c 29f5fd64 c6cb8659 fe3e5972 f93d955b 3456f391 0ca7b782 058d8c95 51f8fe78 675ec88f 7ffc0c56" + "hooks": [1,171,6516,450,450,2452,2627,24832,74,164,59,56,25,9642,1397,32,131,86,1,49,2,6,0], + "events": {"atk":560,"bounty":3,"deploy":37,"die":78,"dmg":2552,"engage":14,"fx":83,"heal":40,"layer":2,"leak":86,"skill":115,"spawn":155,"status":670}, + "fx": {"airstrike":5,"aoe":9,"burst":6,"cleanse":1,"copy":3,"explode":28,"garrison":2,"heal":4,"healField":5,"link":2,"revive":1,"split":1,"stone":1,"summon":15}, + "snaps": "aad8bee8 801856dd 3399a410 b92f8209 23973744 57bbfa1c 9ffd3055 be8deeb1 0bdabeab bda375ae 4afc7560 c160fb12 28f439d9 6584b7fd d1fd5246 16ea2222 4f142c9c dd788125 251f8a15 260c47ec 822d6d6e 1f8efd14" }, "roster-038": { "about": "pass 1 band 7: chess_char_5_08_b chess_char_6_19_b chess_char_5_01_b chess_char_5_05_b chess_char_5_06_b chess_char_6_17_b chess_char_5_07_b chess_char_6_18_b chess_char_6_20_b chess_char_5_02_b chess_char_5_03_b chess_char_5_10_b", @@ -2807,7 +2809,7 @@ "hooks": [1,91,5571,428,415,892,1381,11450,69,74,12,9,10,22992,758,17,68,0,1,69,2,51,0], "events": {"atk":524,"bounty":4,"deploy":20,"die":74,"dmg":1196,"engage":8,"fx":125,"layer":2,"skill":21,"spawn":90,"status":1451}, "fx": {"airstrike":5,"anchor":1,"burst":51,"dp":3,"expose":6,"garrison":2,"link":2,"snow":5,"summon":2,"summonStun":2,"talent":1,"wanted":45}, - "snaps": "cf9f9e00 4757b940 5f298e20 410a2d5d d84d51b4 7fc5ffdd 6924e4f0 afd930c6 a7d4ff63 35d5e664 a27fdae2 8dc50ed9 19f8fcbd 461d841e 4ffb9bc5 2cc4d1a8 729b0562 d4ce4f77 c940e5b3" + "snaps": "10fd15da 83cddf7f c846b231 d53dbd5c 231b2365 73a4cfd5 dbf3e959 b191323a 564195d7 3b4974b0 9b85bd56 82fc4a77 d312a559 0c646576 98456a8d a4735a50 8734e3fa 41a0eaf2 8280a082" }, "roster-046": { "about": "pass 2 band 7: chess_char_6_03_b chess_char_6_15_a chess_char_6_19_a chess_char_6_17_a chess_char_6_14_a chess_char_6_16_a chess_char_6_18_a chess_char_6_20_a chess_char_6_01_b chess_char_6_02_b chess_char_6_04_b chess_char_6_05_b token_10017_skadi2_dedant", @@ -2862,7 +2864,7 @@ "hooks": [1,165,6669,226,226,503,516,740,25,162,9,9,5,4004,335,15,134,133,1,26,1,1,0], "events": {"atk":332,"bounty":1,"deploy":31,"die":29,"dmg":507,"engage":8,"fx":167,"heal":18,"layer":1,"leak":133,"skill":18,"spawn":149,"status":425}, "fx": {"blessing":1,"blink":2,"burst":1,"camouflage":2,"displace":1,"explode":71,"garrison":1,"garrisonGrant":2,"link":1,"snow":7,"sunBurst":3,"talent":2,"telegraph":2,"zone":71}, - "snaps": "fd8ace00 3735d1a0 ac4c99f8 c5d4ca9a 8fded089 880882c7 e721adb9 0ae8c4ec e7907ddb 600d4aaf 8ba41e7f 29e57375 d2a53406 bc57a24b 0f0f5742 e95912b1 d765e411 ac479235 f1961017 bebbcde2 d00f5670 5d45684c ab83875e" + "snaps": "7d1bb456 b462eb12 665997c4 d3b0731e 733c887d 81c9ff7b d61bcd86 d87fe69c 579299cc 8f123aac dd0783cc 48859e06 0d12afe5 4eadb4fc b655b87d aa32fec6 c40fe17e e5480f12 c8fc10e0 5c4a7375 b8961ef3 38a3e52e 2342edcb" }, "roster-047": { "about": "pass 2 band 7: chess_char_6_06_b chess_char_6_15_b chess_char_6_07_b chess_char_6_17_b chess_char_6_08_b chess_char_6_09_b chess_char_6_11_b chess_char_6_12_b chess_char_6_13_b chess_char_6_14_b chess_char_6_16_b chess_char_6_18_b token_10030_mlyss_wtrman", diff --git a/test/sim/feedback5-countdown-summons.test.js b/test/sim/feedback5-countdown-summons.test.js new file mode 100644 index 00000000..68e9587b --- /dev/null +++ b/test/sim/feedback5-countdown-summons.test.js @@ -0,0 +1,72 @@ +// test/sim/feedback5-countdown-summons.test.js — the countdown summons (community report of 2026-10-06 「赫默的无人机、浊心 +// 斯卡蒂的海嗣等这种召唤物不会受到外来伤害(包括源石地板),也不会被干员治疗,血量随时间减少(血量就类似倒计时条,减少完召唤物就 +// 消失)」): the summons that "不会受到攻击" and leave after a fixed time — 医疗探机, 诅咒娃娃, 斯卡蒂的海嗣 and the 自选 ones +// (工程蓄水炮, 沙地兽, “打字机”, 本能的召唤) — hold 无敌 + 禁疗 (PRTS 海嗣 / 沙地兽 「持有禁疗、无敌」, 工程蓄水炮 禁疗; the others +// [ASSUMED] from the report) and their bar (the snapshot's hp) shows the share of their life left (content/tokens.js +// COUNTDOWN_SUMMONS / startCountdown). Until 0.2.0 a 海嗣 on 活性源石 lost 70 of its 100 HP every second and a hurt drone +// drew the medics' heals. The 吟游者 trait (item 32) cannot refill such a bar either: their HP never moves. +// Run: node --test test/sim/feedback5-countdown-summons.test.js + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { makeBattle, checkInvariants } from '../helpers/battleHarness.js'; +import { unitTuple } from '../../server/sim/snapshot.js'; +import { COUNTDOWN_SUMMONS } from '../../server/sim/content/tokens.js'; + +/** [label, owner entry (uid 1), token id, life in s] — every placed countdown summon of the pool and of the 自选 pool. */ +const CASES = [ + ['赫默 医疗探机', { chessId: 'chess_char_2_02_a' }, 'token_10000_silent_healrb', 10], + ['巫恋 诅咒娃娃', { chessId: 'chess_char_3_15_a' }, 'token_10006_vodfox_doll', null], + ['浊心斯卡蒂 海嗣', { chessId: 'chess_char_6_04_a' }, 'token_10017_skadi2_dedant', 25], + ['温蒂 工程蓄水炮', { diy: { slot: 5, charId: 'char_400_weedy', skillIndex: 0, uniEquipId: null } }, 'token_10009_weedy_cannon', 20], + ['莱伊 沙地兽', { diy: { slot: 5, charId: 'char_4117_ray', skillIndex: 0, uniEquipId: null } }, 'token_10034_ray_sndbst', 25], + ['鸿雪 打字机', { diy: { slot: 5, charId: 'char_4055_bgsnow', skillIndex: 0, uniEquipId: null } }, 'token_10026_bgsnow_subbow', 25], +]; + +function field(owner, tokenId) { + const h = makeBattle({ + units: [{ uid: 1, row: 10, col: 3, ...owner }, { uid: 2, kind: 'token', tokenId, ownerUid: 1, row: 10, col: 4 }], + autoFinish: false, timeLimit: 120, seed: 3, flags: { dpInit: 60, dpMax: 99 }, + }); + h.step(); + return { h, t: h.b.allyUnits.find((u) => u.kind === 'token' && u.defId === tokenId) }; +} +const bar = (h, t) => unitTuple(t, h.b.time)[3] / unitTuple(t, h.b.time)[4]; + +for (const [label, owner, tokenId, lifeWant] of CASES) { + test(`${label}: 无敌 + 禁疗, no outside damage (活性源石 tick), no heal, a bar that runs down with its life`, () => { + assert.ok(COUNTDOWN_SUMMONS.has(tokenId)); + const { h, t } = field(owner, tokenId); + assert.ok(t && t.alive, 'deployed from its piece'); + assert.ok(t.countdown, 'its life is counting down'); + const life = t.countdown.until - t.countdown.from; + if (lifeWant != null) assert.ok(Math.abs(life - lifeWant) < 1e-6, `life ${life} s`); + const f = t.s.flags; + assert.ok(f.invulnerable && f.noHeal && f.healFree, '无敌 + 禁疗'); + // a 活性源石 tick (true, 无来源, 环境) and an enemy-style hit change nothing + const hp0 = t.hp; + h.b.dealDamage(null, t, { amount: 70, type: 'true', canDodge: false, tags: ['terrain'] }); + h.b.dealDamage(null, t, { amount: t.s.maxHp * 3, type: 'phys' }); + assert.equal(t.hp, hp0, 'no damage reaches it'); + assert.equal(h.b.heal(h.unit(1), t, 500), 0, 'no heal reaches it'); + // the bar: full at the start, ~3/4 then ~1/4 of the way through, gone at the end + const from = t.countdown.from; + h.run(from + life * 0.25 - h.b.time); + assert.ok(Math.abs(bar(h, t) - 0.75) < 0.05, `a quarter in: ${bar(h, t).toFixed(3)}`); + h.run(life * 0.5); + assert.ok(Math.abs(bar(h, t) - 0.25) < 0.05, `three quarters in: ${bar(h, t).toFixed(3)}`); + assert.equal(t.hp, t.s.maxHp, 'its HP itself never moves'); + h.run(life * 0.25 + 0.2); + assert.ok(!t.alive, 'it leaves when the bar is empty'); + checkInvariants(h.b); + assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); + }); +} + +test('a summon that is attacked keeps its HP as HP: 伺夜\'s 狼群 takes damage, is no countdown summon', () => { + const { h, t } = field({ chessId: 'chess_char_3_19_a' }, 'token_10028_vigil_wolf'); + assert.ok(!COUNTDOWN_SUMMONS.has(t.defId) && t.countdown == null && !t.s.flags.invulnerable); + h.b.dealDamage(null, t, { amount: 100, type: 'true', canDodge: false, tags: ['terrain'] }); + assert.ok(t.hp < t.s.maxHp); + assert.equal(unitTuple(t, h.b.time)[3], Math.ceil(t.hp), 'its bar is its HP'); +});