feedback6: merge fb6-small-fixes (see its commits)

This commit is contained in:
sganggs committed 2026-10-07 18:16:10 +08:00
commit 3f918c9f03
21 files changed
+398 -91

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# --- dependencies & generated client libs ------------------------------------------------------- # --- dependencies & generated client libs -------------------------------------------------------
node_modules/ node_modules/
# a local npm cache (npm_config_cache=./.npm-cache, npm ci --cache .npm-cache — GitHub #258)
.npm-cache/
# copied from node_modules by tools/vendor.mjs (postinstall); never commit third-party builds # copied from node_modules by tools/vendor.mjs (postinstall); never commit third-party builds
public/vendor/ public/vendor/
+1 -1
View File
@@ -371,7 +371,7 @@ that per garrison with `garrisonHooks(garrison) → hook[]`, e.g. "<进入休整
| `SERVER_GAIN` 获得时 | `onGain` | the gained piece — run **×2 while 投资人 is active, ×3 at ≥ 100 投资人 layers** | | `SERVER_GAIN` 获得时 | `onGain` | the gained piece — run **×2 while 投资人 is active, ×3 at ≥ 100 投资人 layers** |
| `SERVER_PREP_START` 进入休整期时 | `onRoundStart` | owned chess: board, and hand unless `bbStr.conditionkey === 'character_target_inboard'` | | `SERVER_PREP_START` 进入休整期时 | `onRoundStart` | owned chess: board, and hand unless `bbStr.conditionkey === 'character_target_inboard'` |
| `SERVER_PREP_FIN` 休整期结束时 | `onPrepEnd` | same | | `SERVER_PREP_FIN` 休整期结束时 | `onPrepEnd` | same |
| `SERVER_REFRESH_SHOP` 刷新时 | `onRefresh` | same — manual refreshes only (`ev.trigger` marks a re-run); 拉普兰德's "本回合首次主动刷新" counts per copy (`incPieceCounter`) | | `SERVER_REFRESH_SHOP` 刷新时 | `onRefresh` | same, as they stood when the refresh happened (taken before its first handler: a chess gained during the dispatch — 贾维's gift, the elite it completes — waits for the next refresh; one that left is skipped) — manual refreshes only (`ev.trigger` marks a re-run); 拉普兰德's "本回合首次主动刷新" counts per copy (`incPieceCounter`) |
| `SERVER_CHESS_SOLD` 售出时 | `onSold` | the sold piece | | `SERVER_CHESS_SOLD` 售出时 | `onSold` | the sold piece |
| `SERVER_PRICE` 购买价格 | `onPrice` (first) | the chess in the priced slot (`ctx.source.where === 'shop'`); SERVER_CHESS_PRICE `bb.price` is the discount off the tier price (至简 3 − 2 = 1, 红豆 2 − 1 = 1: both texts say 购买价格为1) | | `SERVER_PRICE` 购买价格 | `onPrice` (first) | the chess in the priced slot (`ctx.source.where === 'shop'`); SERVER_CHESS_PRICE `bb.price` is the discount off the tier price (至简 3 − 2 = 1, 红豆 2 − 1 = 1: both texts say 购买价格为1) |
| `IN_BATTLE` | — | battle side (server/sim/content/garrisons.js `install`) | | `IN_BATTLE` | — | battle side (server/sim/content/garrisons.js `install`) |
+4 -2
View File
@@ -663,7 +663,7 @@ the source's side):
|---|---|---| |---|---|---|
| `burn` 灼燃 | 1200 arts + RES −20, 10 s lock | 7000 元素伤害 + RES −20, 10 s lock | | `burn` 灼燃 | 1200 arts + RES −20, 10 s lock | 7000 元素伤害 + RES −20, 10 s lock |
| `neural` 神经 | stun 10 s, then 1000 true (10 s lock) | 3 `palsy` (none with 麻痹免疫), then 6000 元素伤害, 10 s lock | | `neural` 神经 | stun 10 s, then 1000 true (10 s lock) | 3 `palsy` (none with 麻痹免疫), then 6000 元素伤害, 10 s lock |
| `apoptosis` 凋亡 | 15 s: 阻回 (`noSp`: no SP gain of any kind, skills.js) + 静默 (no skill activation), −1 SP/s, 100 arts/s | 15 s: 50 % weaken recovering over the burst, 800 元素伤害/s | | `apoptosis` 凋亡 | 15 s: 阻回 (`noSp`: no SP gain of any kind, skills.js) + 静默 (no skill activation), −1 技力/s — of `spTotal`, stored charges included (PRTS 技能 可充能 "当持有者的技力流失时,充能次数也会实时降低"; `setSpTotal`, a running timed skill untouched; PR #262), 100 arts/s | 15 s: 50 % weaken recovering over the burst, 800 元素伤害/s |
| `erosion` 侵蚀 | permanent DEF −100 (stacking `erosionDown`) then 800 phys, 10 s lock | permanent DEF −120 then 5000 元素伤害, 8 s lock | | `erosion` 侵蚀 | permanent DEF −100 (stacking `erosionDown`) then 800 phys, 10 s lock | permanent DEF −120 then 5000 元素伤害, 8 s lock |
| `necrosis` (legacy spare gauge) | 12 s: 100 true/s, ATK −20 % | same | | `necrosis` (legacy spare gauge) | 12 s: 100 true/s, ATK −20 % | same |
@@ -895,7 +895,9 @@ instance) and skip `'counter'` / `'reflect'` damage. When the guard trips, the l
row (薄绿 S1, 蜜蜡 S1, 卡涅利安 S3, 玛恩纳 S2, 安洁莉娜 S3) whose attack range while it runs strictly contains the row (薄绿 S1, 蜜蜡 S1, 卡涅利安 S3, 玛恩纳 S2, 安洁莉娜 S3) whose attack range while it runs strictly contains the
unit's own range checks the DEFAULT condition — a targetable enemy (or one it blocks), a heal skill an injured ally — unit's own range checks the DEFAULT condition — a targetable enemy (or one it blocks), a heal skill an injured ally —
on `trigger.customRangeGrid` (= that running range, grown by the unit's permanent rangeExtend unless the skill's on `trigger.customRangeGrid` (= that running range, grown by the unit's permanent rangeExtend unless the skill's
`targeting.noRangeExtend`), every tick, no attack needed; `DEFAULT` with `trigger.allies` (+ `hpAtMost`, `targeting.noRangeExtend`), every tick, no attack needed (the 外勤医疗 map character Touch's 恳切福音 too, on its 5-2:
set by its kit, content/tokens.js `touchKit` — the map character's record keeps DEFAULT, build-data widens operators'
skills only; GitHub #260); `DEFAULT` with `trigger.allies` (+ `hpAtMost`,
`grid`) = the basic rule **and** such an ally on the grid: the cast replaces the attack about to be made (塞雷娅 S1 "触发 `grid`) = the basic rule **and** such an ally on the grid: the cast replaces the attack about to be made (塞雷娅 S1 "触发
时会替换当次攻击", ≤ half HP); a cast whose ally condition fails before that attack is withdrawn, its charge returned; 时会替换当次攻击", ≤ half HP); a cast whose ally condition fails before that attack is withdrawn, its charge returned;
`TAKE_DAMAGE` — ready and just hit (重装: "不受技能范围影响,受到伤害时释放技能"; in the data every MANUAL 重装 skill but `TAKE_DAMAGE` — ready and just hit (重装: "不受技能范围影响,受到伤害时释放技能"; in the data every MANUAL 重装 skill but
+1
View File
@@ -438,6 +438,7 @@
"自动触发": "Auto Trigger", "自动触发": "Auto Trigger",
"精英": "Elite", "精英": "Elite",
"领袖": "Leader", "领袖": "Leader",
"实际为生命值低于一半时提高治疗量,正好一半不提高(PRTS 修正,原文为“不高于”)": "In practice the heal boost applies below half HP, not at exactly half (PRTS correction; the original text says “at or below”)",
"物理": "Physical", "物理": "Physical",
"法术": "Arts", "法术": "Arts",
"治疗": "Healing", "治疗": "Healing",
+1
View File
@@ -439,6 +439,7 @@
"自动触发": "自動発動", "自动触发": "自動発動",
"精英": "エリート", "精英": "エリート",
"领袖": "ボス", "领袖": "ボス",
"实际为生命值低于一半时提高治疗量,正好一半不提高(PRTS 修正,原文为“不高于”)": "実際は HP が半分未満のときに治療量が上がり、ちょうど半分では上がらない(PRTS の修正。原文は「以下」)",
"物理": "物理", "物理": "物理",
"法术": "術", "法术": "術",
"治疗": "治療", "治疗": "治療",
+1
View File
@@ -435,6 +435,7 @@
"自动触发": "자동 발동", "自动触发": "자동 발동",
"精英": "정예", "精英": "정예",
"领袖": "리더", "领袖": "리더",
"实际为生命值低于一半时提高治疗量,正好一半不提高(PRTS 修正,原文为“不高于”)": "실제로는 HP가 절반 미만일 때 치료량이 증가하며, 정확히 절반일 때는 증가하지 않습니다 (PRTS 정정, 원문은 ‘이하’)",
"物理": "물리", "物理": "물리",
"法术": "마법", "法术": "마법",
"治疗": "치료", "治疗": "치료",
+1
View File
@@ -437,6 +437,7 @@
"自动触发": "自動觸發", "自动触发": "自動觸發",
"精英": "精英", "精英": "精英",
"领袖": "領袖", "领袖": "領袖",
"实际为生命值低于一半时提高治疗量,正好一半不提高(PRTS 修正,原文为“不高于”)": "實際為生命值低於一半時提高治療量,正好一半不提高(PRTS 修正,原文為「不高於」)",
"物理": "物理", "物理": "物理",
"法术": "法術", "法术": "法術",
"治疗": "治療", "治疗": "治療",
+83 -8
View File
@@ -14,7 +14,10 @@
// make the wearer its member, 本局禁用 marked; on a wearer's card the pairing it wears highlighted, 生效中 — GitHub issue // make the wearer its member, 本局禁用 marked; on a wearer's card the pairing it wears highlighted, 生效中 — GitHub issue
// #1, DESIGN §21.26) and a bond item (`giveBondId`) the line "与变形同构体一同装备时,携带者视为【X】成员" (MorphGrantLine); // #1, DESIGN §21.26) and a bond item (`giveBondId`) the line "与变形同构体一同装备时,携带者视为【X】成员" (MorphGrantLine);
// tokens — the owner's variant (a golden owner's summon: its `_b` stats / skill), how a placed summon takes // tokens — the owner's variant (a golden owner's summon: its `_b` stats / skill), how a placed summon takes
// the field (shared/constants.js SKILL_SUMMON_START_DEPLOY), its token skill and talents; enemies — stats, rank, // the field (shared/constants.js SKILL_SUMMON_START_DEPLOY), its token skill and talents; a band map character (外勤医疗's
// Touch / 预备干员-医疗, tokens.json kind 'mapChar') — an operator of the mode: 干员, its class, 特性, skill and talents
// from its own record (MapCharDetail, GitHub #260); a skill text the game contradicts gets the line under it that says
// what the sim does (SKILL_TEXT_NOTES: Touch 恳切福音's 低于一半, PRTS 修正); enemies — stats, rank,
// faction tags, abilities. Selling / destroying is the underframe's job in the // faction tags, abilities. Selling / destroying is the underframe's job in the
// match (research 09 §5, ui/underframe.js): the panel's own 出售 / 销毁 buttons only render for callers that pass // match (research 09 §5, ui/underframe.js): the panel's own 出售 / 销毁 buttons only render for callers that pass
// `editable` + handlers. `side` 'right' docks the panel at the right edge (the game screen picks the side away from a // `editable` + handlers. `side` 'right' docks the panel at the right edge (the game screen picks the side away from a
@@ -51,6 +54,13 @@ const SP_TYPE = { INCREASE_WITH_TIME: N_('自动回复'), INCREASE_WHEN_ATTACK:
const SKILL_TYPE = { MANUAL: N_('自动触发'), AUTO: N_('自动触发'), PASSIVE: N_('被动') }; const SKILL_TYPE = { MANUAL: N_('自动触发'), AUTO: N_('自动触发'), PASSIVE: N_('被动') };
/** Fallback type icon by trigger, for a garrison record without its official `eventTypeIcon` (garrisonTypeIconKey). */ /** Fallback type icon by trigger, for a garrison record without its official `eventTypeIcon` (garrisonTypeIconKey). */
const EVENT_ICON = { IN_BATTLE: 's_icon_battle', SERVER_GAIN: 's_icon_bond', SERVER_PREP_START: 's_icon_bond', SERVER_PREP_FIN: 's_icon_bond', SERVER_CHESS_SOLD: 's_icon_gold', SERVER_PRICE: 's_icon_gold', SERVER_REFRESH_SHOP: 's_icon_gold' }; const EVENT_ICON = { IN_BATTLE: 's_icon_battle', SERVER_GAIN: 's_icon_bond', SERVER_PREP_START: 's_icon_bond', SERVER_PREP_FIN: 's_icon_bond', SERVER_CHESS_SOLD: 's_icon_gold', SERVER_PRICE: 's_icon_gold', SERVER_REFRESH_SHOP: 's_icon_gold' };
/**
* A skill text the game contradicts, by skill id: the record keeps the official sentence, the card adds what the sim
* does under it (a PRTS 修正 the kit follows). Touch 恳切福音 (the 外勤医疗 map character's and the Touch 补位's):
* "对生命值不高于一半的友方单位" — PRTS corrects it to 低于 (原因 6), and content/tokens.js touchGospel boosts strictly
* below half (GitHub #260).
*/
const SKILL_TEXT_NOTES = { skchr_acmedc_3: N_('实际为生命值低于一半时提高治疗量,正好一半不提高(PRTS 修正,原文为“不高于”)') };
const RANK = { NORMAL: N_('普通'), ELITE: N_('精英'), BOSS: N_('领袖') }; const RANK = { NORMAL: N_('普通'), ELITE: N_('精英'), BOSS: N_('领袖') };
const DMG = { phys: N_('物理'), arts: N_('法术'), heal: N_('治疗'), true: N_('真实'), none: N_('无') }; const DMG = { phys: N_('物理'), arts: N_('法术'), heal: N_('治疗'), true: N_('真实'), none: N_('无') };
@@ -357,6 +367,22 @@ export function chessStatsBlock({ rec, chess, live = null }) {
</div>`; </div>`;
} }
/** A skill's tags (SP type, trigger, initial SP · cost, duration, charges): the operator card's and a map character's. */
function skillTags(sk) {
return html`
<span class="dsp dsp--${sk.spType === 'INCREASE_WHEN_ATTACK' ? 'atk' : sk.spType === 'INCREASE_WHEN_TAKEN_DAMAGE' ? 'def' : 'time'}">${t(SP_TYPE[sk.spType]) || t('技力')}</span>
<span class="dsp dsp--trig">${t(SKILL_TYPE[sk.skillType]) || t('自动触发')}</span>
${sk.spType !== 'ON_DEPLOY' && sk.skillType !== 'PASSIVE' ? html`<span class="dsp__num"><${GIcon} name="bolt" />${t('初始')} <b class="num">${sk.initSp ?? 0}</b> ${t('· 消耗')} <b class="num">${sk.spCost ?? 0}</b></span>` : null}
${sk.duration > 0 ? html`<span class="dsp__num">${t('持续')} <b class="num">${sk.duration}</b>s</span>` : null}
${sk.maxChargeTime > 1 ? html`<span class="dsp__num">${t('充能')} <b class="num">${sk.maxChargeTime}</b></span>` : null}`;
}
/** The line under a skill text the game contradicts (SKILL_TEXT_NOTES), or null. */
function skillTextNote(sk) {
const note = sk && SKILL_TEXT_NOTES[sk.skillId];
return note ? html`<p class="dhint dhint--rule" data-skill-note=${sk.skillId}><${Icon} name="info" />${t(note)}</p>` : null;
}
export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bonds, offBonds = null, loadout, onBond, live = null, hint = null, unitItems = null, standIn = null, diy = null }) { export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bonds, offBonds = null, loadout, onBond, live = null, hint = null, unitItems = null, standIn = null, diy = null }) {
const m = data.get('assets'); const m = data.get('assets');
const hp = hpOf(live, snapHp); const hp = hpOf(live, snapHp);
@@ -424,16 +450,11 @@ export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bond
<${Img} src=${skIcon} class="dskill__icon" fallback=${html`<span class="dskill__icon dskill__icon--empty">${skSlot ? html`<b class="num">${skSlot}</b>` : null}</span>`} /> <${Img} src=${skIcon} class="dskill__icon" fallback=${html`<span class="dskill__icon dskill__icon--empty">${skSlot ? html`<b class="num">${skSlot}</b>` : null}</span>`} />
<div class="dskill__meta"> <div class="dskill__meta">
<b class="dskill__name">${skSlot && (lo?.choices || 0) > 1 ? html`<span class="dskill__slot num" title=${t('技能 {skSlot}', { skSlot })}>${skSlot}</span>` : null}${sk.name}${lo && !lo.defaultSkill && !si ? html`<span class="dtag-loadout" title=${t('干员调配中选择的技能')}>${t('已调配')}</span>` : null}</b> <b class="dskill__name">${skSlot && (lo?.choices || 0) > 1 ? html`<span class="dskill__slot num" title=${t('技能 {skSlot}', { skSlot })}>${skSlot}</span>` : null}${sk.name}${lo && !lo.defaultSkill && !si ? html`<span class="dtag-loadout" title=${t('干员调配中选择的技能')}>${t('已调配')}</span>` : null}</b>
<div class="dskill__tags"> <div class="dskill__tags">${skillTags(sk)}</div>
<span class="dsp dsp--${sk.spType === 'INCREASE_WHEN_ATTACK' ? 'atk' : sk.spType === 'INCREASE_WHEN_TAKEN_DAMAGE' ? 'def' : 'time'}">${t(SP_TYPE[sk.spType]) || t('技力')}</span>
<span class="dsp dsp--trig">${t(SKILL_TYPE[sk.skillType]) || t('自动触发')}</span>
${sk.spType !== 'ON_DEPLOY' && sk.skillType !== 'PASSIVE' ? html`<span class="dsp__num"><${GIcon} name="bolt" />${t('初始')} <b class="num">${sk.initSp ?? 0}</b> ${t('· 消耗')} <b class="num">${sk.spCost ?? 0}</b></span>` : null}
${sk.duration > 0 ? html`<span class="dsp__num">${t('持续')} <b class="num">${sk.duration}</b>s</span>` : null}
${sk.maxChargeTime > 1 ? html`<span class="dsp__num">${t('充能')} <b class="num">${sk.maxChargeTime}</b></span>` : null}
</div>
</div> </div>
</div> </div>
<${RichText} as="p" text=${sk.descRaw || sk.desc} class="dtext" /> <${RichText} as="p" text=${sk.descRaw || sk.desc} class="dtext" />
${skillTextNote(sk)}
<//>` : null; <//>` : null;
blocks.module = golden && lo?.module ? html`<${Section} key="module" title=${t('模组')} micro="MODULE" class="dsec--module"> blocks.module = golden && lo?.module ? html`<${Section} key="module" title=${t('模组')} micro="MODULE" class="dsec--module">
<div class=${cx('dmodule', lo.module.none && 'is-none')} data-module=${lo.module.id}> <div class=${cx('dmodule', lo.module.none && 'is-none')} data-module=${lo.module.id}>
@@ -581,8 +602,62 @@ function tokenOwnerId(piece, pieces) {
return owner && owner.kind === 'chess' ? owner.id : null; return owner && owner.kind === 'chess' ? owner.id : null;
} }
/**
* A band map character's card (TokenDetail): labelled 干员 with its class (医疗 · 远程位), its 特性, the four stats, its
* skill — the operator card's row (icon, SP, duration), its text and SKILL_TEXT_NOTES — and its talents, all from the
* tokens.json record. Until 0.2.1 it took the summon path: 召唤物, base stats only (GitHub #260).
*/
function MapCharDetail({ token, snapHp, live, m }) {
const s = token.stats || {};
const hp = hpOf(live, snapHp);
const st = {
maxHp: liveStat(live, 'maxHp', s.maxHp), atk: liveStat(live, 'atk', s.atk), def: liveStat(live, 'def', s.def),
blockCnt: liveStat(live, 'blockCnt', s.blockCnt, (v) => String(v)),
};
const sk = token.skill && token.skill.desc ? token.skill : null;
const talents = chessTalents(token);
const trait = token.trait?.descRaw || token.trait?.desc || token.descRaw || token.desc || '';
return html`
<div class="dhead dhead--item" data-map-char=${token.tokenId}>
<div class="dhead__icon"><${Img} src=${tokenAvatarUrl(m, token.tokenId)} fallback=${html`<${GIcon} name="target" />`} /></div>
<div class="dhead__info">
<div class="dhead__chips"><span class="dtag-kind">${t('干员')}</span></div>
<h3 class="dhead__name">${token.name}</h3>
<div class="dhead__class">
<${Img} src=${profIconUrl(m, token.profession)} class="dhead__prof" />
<span>${t(PROF_NAME[token.profession]) || token.profession || ''}</span>
<span class="dhead__pos">${token.position === 'MELEE' ? t('近战位') : t('远程位')}</span>
</div>
${hp ? html`<div class="dhp"><i style=${`width:${Math.max(0, Math.min(100, (hp.hp / Math.max(1, hp.max)) * 100))}%`}></i><span class="num">${fmtNum(hp.hp)} / ${fmtNum(hp.max)}</span></div>` : null}
</div>
</div>
${trait ? html`<p class="dtrait"><${Icon} name="info" /><${RichText} text=${trait} /></p>` : null}
<div class=${cx('dstats', live && 'is-live')} data-live=${live ? live.src || 'prep' : undefined}>
<${LiveTag} live=${live} />
<${Stat} k=${t('生命上限')} ...${st.maxHp} /><${Stat} k=${t('攻击')} ...${st.atk} />
<${Stat} k=${t('防御')} ...${st.def} /><${Stat} k=${t('阻挡数')} ...${st.blockCnt} />
</div>
${sk ? html`<${Section} title=${t('技能')} micro="SKILL" class="dsec--skill">
<div class="dskill" data-skill=${sk.skillId || ''}>
<${Img} src=${skillRecordIconUrl(m, sk, { empty: false })} class="dskill__icon" fallback=${html`<span class="dskill__icon dskill__icon--empty"></span>`} />
<div class="dskill__meta">
<b class="dskill__name">${sk.name}</b>
<div class="dskill__tags">${skillTags(sk)}</div>
</div>
</div>
<${RichText} as="p" text=${sk.descRaw || sk.desc} class="dtext" />
${skillTextNote(sk)}
<//>` : null}
${talents.length ? html`<${Section} title=${t('天赋')} micro="TALENT" class="dsec--talent">
${talents.map((x, i) => html`<div key=${i} class="dtalent"><b>${x.name}</b><${RichText} text=${x.descRaw || x.desc} class="dtext" /></div>`)}
<//>` : null}`;
}
export function TokenDetail({ token, piece, ownerId = null, snapHp = null, live = null }) { export function TokenDetail({ token, piece, ownerId = null, snapHp = null, live = null }) {
const m = data.get('assets'); const m = data.get('assets');
// a band map character (外勤医疗's Touch / 预备干员-医疗, tokens.json kind 'mapChar') is an operator of the mode, not a
// summon: no owner variants — its stats, skill, talents and 特性 are on the record (GitHub #260, PR #278)
if (token?.kind === 'mapChar') return MapCharDetail({ token, snapHp, live, m });
// the owner's variant: its stats, talents and token skill (a golden owner's summon is stronger) // the owner's variant: its stats, talents and token skill (a golden owner's summon is stronger)
const v0 = tokenVariantFor(token, ownerId); const v0 = tokenVariantFor(token, ownerId);
const s = v0?.stats || token.stats || {}; const s = v0?.stats || token.stats || {};
+29 -3
View File
@@ -18,7 +18,9 @@
// onRoundStart, SERVER_PREP_FIN→onPrepEnd, SERVER_CHESS_SOLD→onSold of the sold piece, SERVER_PRICE→onPrice of the // onRoundStart, SERVER_PREP_FIN→onPrepEnd, SERVER_CHESS_SOLD→onSold of the sold piece, SERVER_PRICE→onPrice of the
// slot's chess, SERVER_REFRESH_SHOP→onRefresh); the dispatcher calls `handler[hook] ?? handler.run`. A handler may // slot's chess, SERVER_REFRESH_SHOP→onRefresh); the dispatcher calls `handler[hook] ?? handler.run`. A handler may
// widen that per garrison with `garrisonHooks(garrison) → hook[]` (e.g. "<进入休整期时><休整期结束时>"). Owned-piece // widen that per garrison with `garrisonHooks(garrison) → hook[]` (e.g. "<进入休整期时><休整期结束时>"). Owned-piece
// garrisons fire for board pieces, and for hand pieces unless bbStr.conditionkey is 'character_target_inboard'. // garrisons fire for board pieces, and for hand pieces unless bbStr.conditionkey is 'character_target_inboard'. A
// refresh (onRefresh) runs those that stood on the board / in the hand when it happened (taken before its first
// handler): a chess gained during its dispatch — 贾维's gift, the elite that gift completes — waits for the next refresh.
// 投资人 (investShip) active ⇒ SERVER_GAIN garrisons run ×2 (×3 at ≥ 100 layers) — owned by the dispatcher. // 投资人 (investShip) active ⇒ SERVER_GAIN garrisons run ×2 (×3 at ≥ 100 layers) — owned by the dispatcher.
// ctx.triggerGarrisons(uid, eventType) re-runs another piece's garrisons (铃兰, "触发…的获得时效果", 特质相同). // ctx.triggerGarrisons(uid, eventType) re-runs another piece's garrisons (铃兰, "触发…的获得时效果", 特质相同).
// Items: onEquip / onArt / onDestroy go to the item's own handler only; every other hook runs for items equipped on // Items: onEquip / onArt / onDestroy go to the item's own handler only; every other hook runs for items equipped on
@@ -204,6 +206,10 @@ export class EffectDispatcher {
if (deferItems) ps._deferItemMerge = (ps._deferItemMerge || 0) + 1; if (deferItems) ps._deferItemMerge = (ps._deferItemMerge || 0) + 1;
try { try {
const reg = this.registry; const reg = this.registry;
// onRefresh: the board / hand chess as they stood when the refresh happened, taken before any handler runs — a
// chess gained during this dispatch (贾维's gift on the 6th refresh, the elite its copy completes) was not there
// when it happened: its 刷新时 特质 waits for the next manual refresh (拉普兰德's "本回合首次主动刷新", PR #196)
const refreshed = hook === 'onRefresh' ? refreshPieces(ps) : null;
// 0. onPrice: the priced chess's own 特质 first — 购买价格为N defines the price every other modifier acts on // 0. onPrice: the priced chess's own 特质 first — 购买价格为N defines the price every other modifier acts on
if (hook === 'onPrice') this._garrisons(ps, hook, ev); if (hook === 'onPrice') this._garrisons(ps, hook, ev);
// 1. globals // 1. globals
@@ -221,7 +227,7 @@ export class EffectDispatcher {
if (h) this._call(ps, key, h, hook, { kind: 'bond', key, bondId, bond: ps.bonds[bondId] ?? null }, ev); if (h) this._call(ps, key, h, hook, { kind: 'bond', key, bondId, bond: ps.bonds[bondId] ?? null }, ev);
} }
// 4. garrisons (onPrice: already run as step 0) // 4. garrisons (onPrice: already run as step 0)
if (hook !== 'onPrice') this._garrisons(ps, hook, ev); if (hook !== 'onPrice') this._garrisons(ps, hook, ev, refreshed);
// 5. equipped items (not for the item-specific hooks): every [holder, item] pair of the owned chess, taken before // 5. equipped items (not for the item-specific hooks): every [holder, item] pair of the owned chess, taken before
// the first item runs; each runs only while still equipped on its still-owned holder — handlers move / destroy // the first item runs; each runs only while still equipped on its still-owned holder — handlers move / destroy
// pieces (header). Taking the pairs per holder as the walk reached it ran an item equipped meanwhile onto a later // pieces (header). Taking the pairs per holder as the walk reached it ran an item equipped meanwhile onto a later
@@ -279,7 +285,12 @@ export class EffectDispatcher {
return [GARRISON_HOOK[g.eventType]]; return [GARRISON_HOOK[g.eventType]];
} }
_garrisons(ps, hook, ev) { /**
* Run the owned chess's garrisons of `hook`. `refreshed` (onRefresh): the board / hand chess taken when the dispatch
* began (refreshPieces) — each runs if it is still on the board or in the hand (where it stands now); a chess gained
* meanwhile does not run, nor one that left (sold, consumed by a merge, moved to the 临时整备区).
*/
_garrisons(ps, hook, ev, refreshed = null) {
const gd = ps.gd || this.m.gd; const gd = ps.gd || this.m.gd;
const run = (piece, where) => { const run = (piece, where) => {
const rec = gd.chess(piece.id); const rec = gd.chess(piece.id);
@@ -304,6 +315,13 @@ export class EffectDispatcher {
return; return;
} }
if (hook !== 'onRoundStart' && hook !== 'onPrepEnd' && hook !== 'onRefresh') return; if (hook !== 'onRoundStart' && hook !== 'onPrepEnd' && hook !== 'onRefresh') return;
if (refreshed) {
for (const piece of refreshed) {
const loc = ps.find(piece.uid);
if (loc && loc.piece === piece && (loc.area === 'board' || loc.area === 'hand')) run(piece, loc.area);
}
return;
}
for (const { piece } of boardOrder(ps.board)) if (piece.kind === 'chess') run(piece, 'board'); for (const { piece } of boardOrder(ps.board)) if (piece.kind === 'chess') run(piece, 'board');
for (const p of ps.hand) if (p && p.kind === 'chess') run(p, 'hand'); for (const p of ps.hand) if (p && p.kind === 'chess') run(p, 'hand');
} }
@@ -385,6 +403,14 @@ function ownedChess(ps) {
return out; return out;
} }
/** The board chess (reading order) then the hand chess — the pieces whose 刷新时 特质 a refresh runs (onRefresh). */
function refreshPieces(ps) {
const out = [];
for (const { piece } of boardOrder(ps.board)) if (piece.kind === 'chess') out.push(piece);
for (const p of ps.hand) if (p && p.kind === 'chess') out.push(p);
return out;
}
/** Dispatch step 5: is `it` still equipped on `holder`, and `holder` still an owned chess of `ps`? */ /** Dispatch step 5: is `it` still equipped on `holder`, and `holder` still an owned chess of `ps`? */
function stillEquipped(ps, holder, it) { function stillEquipped(ps, holder, it) {
if (!Array.isArray(holder.items) || !holder.items.includes(it)) return false; if (!Array.isArray(holder.items) || !holder.items.includes(it)) return false;
+3 -1
View File
@@ -202,7 +202,9 @@ H.SERVER_ADD_REFRESH_CNT_MULTIPLIER_BOND_LAYER = {
// (ev.trigger) is no manual refresh: it neither fires nor counts. A new copy (bought, granted) starts at 0, and so does // (ev.trigger) is no manual refresh: it neither fires nor counts. A new copy (bought, granted) starts at 0, and so does
// an elite merged this round: a new 拉普兰德, its own first manual refresh this round adds +8 even when its copies // an elite merged this round: a new 拉普兰德, its own first manual refresh this round adds +8 even when its copies
// already fired (GitHub #169; the owner's decision of 2026-10-06 — PlayerState._mergeChess carries no counter over). // already fired (GitHub #169; the owner's decision of 2026-10-06 — PlayerState._mergeChess carries no counter over).
// [ASSUMED]: a copy bought after selling one this round is a new copy — "获得该干员后" — and fires on its own first // A copy gained by a refresh's own effects (贾维's gift on every 6th refresh, or the elite that gift completes) did not
// witness that refresh: the dispatcher runs the chess that stood there when it happened (effectsMeta.js onRefresh), so
// her first is the next manual refresh (PR #196). [ASSUMED]: a copy bought after selling one this round is a new copy — "获得该干员后" — and fires on its own first
// refresh (the server cannot tell it from any other copy; each such +4 costs her price + a refresh − the 1-fund refund, // refresh (the server cannot tell it from any other copy; each such +4 costs her price + a refresh − the 1-fund refund,
// and needs her in the shop again). // and needs her in the shop again).
const REFRESH_CNT_KEY = 'garrison:SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT:refreshes'; // per-piece counter (module-prefixed) const REFRESH_CNT_KEY = 'garrison:SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT:refreshes'; // per-piece counter (module-prefixed)
+8 -1
View File
@@ -1515,11 +1515,18 @@ export function touchGospel(bb, skill) {
}; };
} }
/** Touch (外勤医疗 map character): 恳切福音 (touchGospel) + 攫升 / 超脱 (mapCharTalents). */ /**
* Touch (外勤医疗 map character): 恳切福音 (touchGospel) + 攫升 / 超脱 (mapCharTalents). The trigger is ACTIVE_RANGE on
* the skill's 5-2 range (her running range, which strictly contains her own 3-3): an injured ally inside it casts — the
* owner's larger-range rule of 2026-10-05, read for a heal skill as for the Touch 补位 stand-in, whose data rule it is
* (data/backups.json). The map character's record keeps DEFAULT (tools/build-data.mjs resolveTrigger widens operators'
* skills only), so she waited for an injured ally in her 3-3 (GitHub #260, PR #278).
*/
function touchKit(bb, raw, def) { function touchKit(bb, raw, def) {
const sk = def?.skill; const sk = def?.skill;
if (!sk) return { skill: null, talents: mapCharTalents(def) }; if (!sk) return { skill: null, talents: mapCharTalents(def) };
const g = touchGospel(bb, sk); const g = touchGospel(bb, sk);
if (sk.rangeGrid && sk.rangeGrid.length) g.skill.trigger = { rule: 'ACTIVE_RANGE', grid: sk.rangeGrid };
return { talents: mapCharTalents(def), skill: g.skill, install: g.install }; return { talents: mapCharTalents(def), skill: g.skill, install: g.install };
} }
+6 -2
View File
@@ -478,12 +478,16 @@ function resolveBurst(battle, source, target, el) {
}, },
}); });
} else { } else {
// 15 s of 阻回 ("停止并阻止任意形式的技力回复": noSp) + 静默 (no skill activation), −1 SP and 100 arts damage per second // 15 s of 阻回 ("停止并阻止任意形式的技力回复": noSp) + 静默 (no skill activation), −1 SP and 100 arts damage per second.
// The loss is of the official 技力 (Skill.spTotal: the stored charges × cost + the SP towards the next) — PRTS 技能
// 可充能 "当持有者的技力流失时,充能次数也会实时降低": taking it from the partial bar alone left a full skill every
// charge it had (a one-charge skill stayed ready through the burst; PR #262). setSpTotal rebuilds the charges
// silently (no spGain) and leaves a running timed skill alone, whose SP was spent at its cast.
lock(c.duration, { lock(c.duration, {
flags: { silence: true, noSp: true }, interval: 1, flags: { silence: true, noSp: true }, interval: 1,
onTick: () => { onTick: () => {
const sk = target.skill; const sk = target.skill;
if (sk && !sk.noSkill && sk.kind !== 'passive' && !(sk.active && sk.isTimed) && sk.sp > 0) sk.sp = Math.max(0, sk.sp - c.spLossPerSec); if (sk && !sk.noSkill && sk.kind !== 'passive' && !(sk.active && sk.isTimed) && sk.spTotal > 0) sk.setSpTotal(Math.max(0, sk.spTotal - c.spLossPerSec));
hit(c.dps, c.dpsType); hit(c.dps, c.dpsType);
}, },
}); });
@@ -0,0 +1,65 @@
// PR #262 (#261 its closed duplicate): an operator's 凋亡 burst — PRTS 元素 "立即获得等时长的阻回、静默,该阻回、静默持续期间
// 每秒损失1点技力并受到100点无来源法术持续伤害" — drains the official 技力, stored charges included: PRTS 技能 可充能 "当持有者
// 的技力流失时,充能次数也会实时降低". The drain took only the SP towards the next charge, so a full skill (sp = cost with
// every charge stored) kept all its charges — a one-charge skill stayed ready through the whole burst. An active timed
// skill is left alone (its SP was spent at the cast; Skill.setSpTotal leaves it too).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.js';
import { hasGeneratedData } from '../../server/sim/simdata.js';
const REAL = { skip: !hasGeneratedData() };
const SNOW = 'chess_char_3_11_b'; // 精锐 雪猎: S2 风雪连弩, MANUAL, 可充能2次, 14 SP per charge
function arena(units, defs = {}) {
const h = makeBattle({ defs, units, autoFinish: false, timeLimit: 120, hooks: ['spGain', 'damaged'] });
h.step();
return h;
}
const burst = (h, u) => {
h.b.dealDamage(null, u, { type: 'element', element: 'apoptosis', amount: u.gaugeMax });
assert.ok(u.findBuff('apoptosisBurst'), '凋亡 bursts');
};
test('PR #262: 凋亡 drains a charge skill\'s stored charges — 精锐 雪猎 with both charges loses one at the first second and is empty after 15', REAL, () => {
const h = arena([{ chessId: SNOW, row: 10, col: 4 }]);
const u = h.unit(SNOW), sk = u.skill;
assert.equal(sk.maxCharges, 2);
const cost = sk.spCost;
sk.setSpTotal(2 * cost);
assert.deepEqual([sk.charges, sk.sp, sk.spTotal], [2, cost, 2 * cost], 'both charges stored');
burst(h, u);
h.run(1.01);
assert.deepEqual([sk.charges, sk.sp, sk.spTotal], [1, cost - 1, 2 * cost - 1], 'one second: −1 技力, the second charge goes');
assert.equal(sk.ready, true, 'one charge left');
h.run(14);
// (the burst ends with its 15th second: the time SP of the ticks since then is back)
assert.equal(sk.charges, 0, '15 seconds: −15 技力 — the last charge gone too');
assert.ok(Math.abs(sk.spTotal - (2 * cost - 15)) < 0.1, `${sk.spTotal} ≈ ${2 * cost - 15}`);
assert.equal(sk.ready, false);
assert.equal(sk.activations, 0, '静默: no cast through the burst');
assert.ok(!u.findBuff('apoptosisBurst'), 'the burst is over');
checkInvariants(h.b);
});
test('PR #262: a full one-charge skill is no longer ready after the first second of 凋亡; an active timed skill keeps its state', () => {
const op = chessRec({ id: 't_apo', stats: { maxHp: 1e5, spRecovery: 0 }, skill: { spCost: 20, initSp: 0, duration: 30, durationType: 'NONE' } });
const h = arena([{ chessId: 't_apo', row: 10, col: 4, uid: 1 }, { chessId: 't_apo', row: 10, col: 6, uid: 2 }], { chess: { t_apo: op } });
const a = h.unit(1), b = h.unit(2);
a.skill.setSpTotal(20);
assert.equal(a.skill.ready, true);
burst(h, a);
h.run(1.01);
assert.deepEqual([a.skill.charges, a.skill.sp, a.skill.ready], [0, 19, false], 'the ready charge is part of the 技力 it loses');
h.run(14);
assert.equal(a.skill.spTotal, 5, '−15 技力 in all');
assert.equal(h.hooksOf('spGain').length, 0, 'the drain is no SP gain (no spGain hook)');
// a running timed skill: its SP was spent at the cast — nothing to drain, and it runs on (the burst silences new casts only)
b.skill.setSpTotal(20);
assert.ok(b.skill.activate('test'));
const before = [b.skill.active, b.skill.charges, b.skill.sp];
burst(h, b);
h.run(3);
assert.deepEqual([b.skill.active, b.skill.charges, b.skill.sp], before);
checkInvariants(h.b);
});
+22
View File
@@ -1124,6 +1124,28 @@ test('Touch talents: 攫升 +3 SP to the healed unit, 超脱 +5 SP when an opera
checkInvariants(h.b); checkInvariants(h.b);
}); });
test('GitHub #260: the 外勤医疗 Touch casts 恳切福音 on an injured ally only inside the skill\'s 5-2 (ACTIVE_RANGE, as the 补位 Touch); a full-HP or unhealable one does not', REAL, () => {
// Touch stands on (10,2) facing right: her 3-3 reaches column 5, the skill's 5-2 column 7
const h = makeBattle({ stageId: 'act2autochess_m01', defs: { chess: { test_guard: guard({ stats: { maxHp: 10000, atk: 0 } }) } },
units: [{ chessId: 'test_guard', row: 10, col: 7 }], autoFinish: false, timeLimit: 60 });
h.step();
const touch = spawnMapChar(h.b, 'p1', TOKEN_IDS.touch);
assert.deepEqual([touch.tileR, touch.tileC], [10, 2]);
assert.equal(touch.skill.rule, 'ACTIVE_RANGE');
const ally = h.unit('test_guard');
touch.skill.gainSp(1000);
h.run(3);
assert.equal(touch.skill.activations, 0, 'every ally at full HP: no cast');
ally.hp = 4000;
const buff = h.b.addBuff(ally, { key: 'test:noHeal', flags: { noHeal: true } });
h.run(2);
assert.equal(touch.skill.activations, 0, 'an ally no heal can pick: no cast');
h.b.removeBuff(ally, buff);
assert.ok(h.runUntil(() => touch.skill.activations === 1, 2), 'the injured ally five tiles ahead (outside her 3-3) casts the skill');
assert.ok(h.runUntil(() => ally.hp > 4000, 5), 'and she heals it from the larger range');
checkInvariants(h.b);
});
test('spawnMapChar: per-player slots — unite: p1 #1 (10,2), p2 its multi-only slot (10,10); boss: L (3,2), R (3,18); solo never multi-only', REAL, () => { test('spawnMapChar: per-player slots — unite: p1 #1 (10,2), p2 its multi-only slot (10,10); boss: L (3,2), R (3,18); solo never multi-only', REAL, () => {
const players = [ const players = [
{ playerId: 'p1', seat: 0, side: 'L', colOffset: 0, units: [], bonds: {} }, { playerId: 'p1', seat: 0, side: 'L', colOffset: 0, units: [], bonds: {} },
+31
View File
@@ -0,0 +1,31 @@
// .gitignore keeps per-machine state out of the repository (and so out of the release packages, which ship tracked
// files only: tools/package.mjs). GitHub #258: a local npm cache (`npm_config_cache=./.npm-cache`, `npm ci --cache
// .npm-cache`) must never be committed by any contributor.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const lines = readFileSync(path.join(ROOT, '.gitignore'), 'utf8').split(/\r?\n/).map((l) => l.trim());
/** git's own verdict on a path (null when git or the repository is not available). */
function ignored(rel) {
try {
execFileSync('git', ['check-ignore', '-q', '--no-index', rel], { cwd: ROOT, stdio: 'ignore' });
return true;
} catch (e) {
return e && e.status === 1 ? false : null;
}
}
test('GitHub #258: the local npm cache (.npm-cache/) is ignored, like node_modules/ and .cache/', () => {
for (const entry of ['.npm-cache/', 'node_modules/', '.cache/']) assert.ok(lines.includes(entry), `.gitignore lists ${entry}`);
for (const rel of ['.npm-cache/_cacache/index-v5/00/aa', '.npm-cache/_logs/debug-0.log']) {
const v = ignored(rel);
if (v !== null) assert.equal(v, true, `${rel} is ignored`);
}
assert.notEqual(ignored('package.json'), true, 'a tracked file is not');
});
+20 -20
View File
@@ -2382,7 +2382,7 @@
"hooks": [1,129,6242,303,303,643,724,1632,21,124,15,15,0,5917,2,46,105,103,1,21,0,13,0], "hooks": [1,129,6242,303,303,643,724,1632,21,124,15,15,0,5917,2,46,105,103,1,21,0,13,0],
"events": {"atk":309,"deploy":21,"die":21,"dmg":681,"engage":10,"fx":124,"heal":41,"leak":103,"skill":30,"spawn":120,"status":46}, "events": {"atk":309,"deploy":21,"die":21,"dmg":681,"engage":10,"fx":124,"heal":41,"leak":103,"skill":30,"spawn":120,"status":46},
"fx": {"aoe":24,"beam":20,"burst":13,"crit":2,"explode":1,"healField":10,"shield":9,"thorns":45}, "fx": {"aoe":24,"beam":20,"burst":13,"crit":2,"explode":1,"healField":10,"shield":9,"thorns":45},
"snaps": "8af17d6a 024bce8c e0452b2c 5af87abd 42a1f8a9 0b71523e 3fd3e515 fa3770c9 2ca7b522 c32eaf65 dffbf1b7 224a00a2 994934f9 5bc28beb 05894548 582c0f90 dd02f272 cbdb633c 2fb767c6 ad6a50e0 205812fb" "snaps": "8af17d6a c4ee14aa b0c6352a 4663534b ad4f6c57 79103640 6a18ed37 6ce034df 01e7e370 1a22aca7 2ab6744d 07e795ec 55eb2147 66c813ad 16426e3e 9e7597da 454aaf0c ba882526 bb829c2c 99b4e6c6 a4e6b5a1"
}, },
"diy-059": { "diy-059": {
"about": "5_diy1_b=shu/S1/002_shu 5_diy2_b=shu/S2/002_shu 5_diy1_b=shu/S3/002_shu 6_diy1_a=shu/S1 6_diy2_a=shu/S2 6_diy1_a=shu/S3 6_diy2_b=shu/S1 6_diy1_b=shu/S2 6_diy2_b=shu/S3 6_diy1_b=shu/S1/002_shu 6_diy2_b=shu/S2/002_shu 6_diy1_b=shu/S3/002_shu", "about": "5_diy1_b=shu/S1/002_shu 5_diy2_b=shu/S2/002_shu 5_diy1_b=shu/S3/002_shu 6_diy1_a=shu/S1 6_diy2_a=shu/S2 6_diy1_a=shu/S3 6_diy2_b=shu/S1 6_diy1_b=shu/S2 6_diy2_b=shu/S3 6_diy1_b=shu/S1/002_shu 6_diy2_b=shu/S2/002_shu 6_diy1_b=shu/S3/002_shu",
@@ -2396,35 +2396,35 @@
"time": 230, "time": 230,
"ticks": 6900, "ticks": 6900,
"reason": "timeout", "reason": "timeout",
"killed": 39, "killed": 47,
"total": 96, "total": 96,
"unspawned": 0, "unspawned": 0,
"bossHpLeft": null, "bossHpLeft": null,
"rngDraws": 0, "rngDraws": 0,
"errors": {"count":0,"kinds":[]}, "errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":39,"total":96,"perfect":false,"leaks":57,"leaksUncounted":0,"coins":0,"dmg":234243,"bossDmg":0,"heal":203629,"deaths":18,"layerGains":{}}}, "players": {"p1":{"killed":47,"total":96,"perfect":false,"leaks":49,"leaksUncounted":0,"coins":0,"dmg":231786,"bossDmg":0,"heal":217108,"deaths":15,"layerGains":{}}},
"units": [ "units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_shu",0,0,103,2,14695,21669,15436,64,11], ["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_shu",0,0,106,1,16402,16000,9569,59,5],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_shu",0,0,119,0,7297,21517,15906,36,1], ["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_shu",2945.74,1,119,0,5494,19342,17088,32,2],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_shu",3070,1,120,6,14925,16583,5503,77,1], ["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_shu",3070,1,120,5,16030,16314,5378,80,1],
["p1",4,"op","chess_char_6_diy1_a",0,null,0,0,99,1,7880,21342,8555,56,6], ["p1",4,"op","chess_char_6_diy1_a",0,null,2313,1,100,2,12751,26007,6577,66,2],
["p1",5,"op","chess_char_6_diy2_a",1,null,0,0,101,0,5224,32777,48418,50,2], ["p1",5,"op","chess_char_6_diy2_a",1,null,2057.85,1,103,1,13344,39122,47357,75,2],
["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,77,2,11366,9710,12836,42,1], ["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,77,2,11366,9681,12807,42,1],
["p1",7,"op","chess_char_6_diy2_b",0,"none",2791.49,1,107,5,36418,40843,15790,135,9], ["p1",7,"op","chess_char_6_diy2_b",0,"none",2183.12,1,102,6,34558,41766,13074,133,5],
["p1",8,"op","chess_char_6_diy1_b",1,"none",0,0,102,2,16600,20626,10010,73,1], ["p1",8,"op","chess_char_6_diy1_b",1,"none",0,0,99,2,16300,33752,26299,85,2],
["p1",9,"op","chess_char_6_diy2_b",2,"none",0,0,5,5,15654,5050,26206,69,2], ["p1",9,"op","chess_char_6_diy2_b",2,"none",0,0,5,5,16102,3554,24484,71,2],
["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_shu",0,0,8,0,14640,10510,10285,43,6], ["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_shu",0,0,8,1,6712,7983,4179,20,1],
["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_shu",0,0,10,6,39992,33225,14891,106,1], ["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_shu",0,0,10,5,35380,29094,14012,94,1],
["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_shu",3180,1,116,7,49553,20305,19793,109,2], ["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_shu",3180,1,115,9,47347,18313,36285,106,2],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null], ["-","~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], ["-","~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] ["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
], ],
"enemies": {"enemy_1229_darmy":[27,13,14,45139,72535],"enemy_1229_darmy_2":[57,26,31,120688,198119],"enemy_9007_acelem":[12,0,11,69460,101381]}, "enemies": {"enemy_1229_darmy":[27,15,12,48739,79249],"enemy_1229_darmy_2":[57,32,23,133695,210887],"enemy_9007_acelem":[12,0,10,58192,72262]},
"hooks": [1,120,6900,1449,1449,4412,4696,22580,57,113,43,42,0,15690,0,93,96,56,1,57,0,39,0], "hooks": [1,120,6900,1513,1513,4343,4631,22639,62,107,26,25,0,15564,0,93,96,45,1,62,0,37,0],
"events": {"atk":1500,"deploy":21,"die":57,"dmg":4696,"engage":12,"fx":181,"heal":328,"leak":56,"skill":85,"spawn":111,"status":105}, "events": {"atk":1572,"deploy":21,"die":62,"dmg":4631,"engage":12,"fx":189,"heal":335,"leak":45,"skill":51,"spawn":111,"status":101},
"fx": {"beam":18,"burst":39,"healField":47,"teleport":77}, "fx": {"beam":18,"burst":37,"healField":49,"teleport":85},
"snaps": "8dc1b37a b5460f3d 3555fcc6 db56014a a0553a57 f4dca0ac f05a71e9 77e33494 98645a14 e2eefd2d bd5020fc d9664555 4c789e18 d251cfc2 3e54ab89 a00b133e 02012e0d 11dc3504 389257ff 0e7213e4 96b7c324 1b69e292 0f8b3ea2" "snaps": "8dc1b37a b5460f3d 3ef6a531 1b763d75 2ca58274 c2fef8fd afccd50e 9e6a1580 6921be5e a89eb8c3 950cf27b 18a4160c e6764ecc cf15dcc9 dbfe2e1c fd2de4b2 aaaf0692 702a1a56 289559cc 2f1168ca 69dba497 ce110259 f0b47a01"
}, },
"diy-060": { "diy-060": {
"about": "5_diy1_a=pallas/S1 5_diy2_a=pallas/S2 5_diy1_a=pallas/S3 5_diy2_b=pallas/S1 5_diy1_b=pallas/S2 5_diy2_b=pallas/S3 5_diy1_b=pallas/S1/002_pallas 5_diy2_b=pallas/S2/002_pallas 5_diy1_b=pallas/S3/002_pallas 5_diy2_b=pallas/S1/003_pallas 5_diy1_b=pallas/S2/003_pallas 5_diy2_b=pallas/S3/003_pallas", "about": "5_diy1_a=pallas/S1 5_diy2_a=pallas/S2 5_diy1_a=pallas/S3 5_diy2_b=pallas/S1 5_diy1_b=pallas/S2 5_diy2_b=pallas/S3 5_diy1_b=pallas/S1/002_pallas 5_diy2_b=pallas/S2/002_pallas 5_diy1_b=pallas/S3/002_pallas 5_diy2_b=pallas/S1/003_pallas 5_diy1_b=pallas/S2/003_pallas 5_diy2_b=pallas/S3/003_pallas",
+17 -17
View File
@@ -375,7 +375,7 @@
"hooks": [1,42,6000,387,387,643,1580,796,20,24,80,78,20,17921,1173,2,19,0,1,139,0,1,0], "hooks": [1,42,6000,387,387,643,1580,796,20,24,80,78,20,17921,1173,2,19,0,1,139,0,1,0],
"events": {"atk":405,"deploy":17,"die":24,"dmg":733,"engage":8,"fx":203,"heal":59,"skill":158,"spawn":36,"status":179}, "events": {"atk":405,"deploy":17,"die":24,"dmg":733,"engage":8,"fx":203,"heal":59,"skill":158,"spawn":36,"status":179},
"fx": {"anchor":1,"aoe":3,"beam":6,"burst":1,"candle":3,"ember":3,"explode":104,"healAoe":34,"overload":4,"phase":12,"revive":1,"splash":20,"substitute":1,"summon":6,"swap":1,"telegraph":1,"teleport":2}, "fx": {"anchor":1,"aoe":3,"beam":6,"burst":1,"candle":3,"ember":3,"explode":104,"healAoe":34,"overload":4,"phase":12,"revive":1,"splash":20,"substitute":1,"summon":6,"swap":1,"telegraph":1,"teleport":2},
"snaps": "87076623 3c36dd04 03a89072 745a7c6b 24a96213 0ed332a6 26e97a3b 2b49b7d6 376df693 78dab18f 1b931756 f174873b d4541524 71b41d30 2da111bb 5c2d56c1 fa32d0c4 f6866cb6 7c0b0632 9a7cccfc" "snaps": "87076623 3c36dd04 03a89072 745a7c6b 24a96213 0ed332a6 26e97a3b 2b49b7d6 4deb0556 087cdcb6 79fbf5f7 dfe3d106 fdaa7bd5 af470bb9 c87c0fc2 ed37f880 df193341 1e30dddf 21a08fc7 5270e9dd"
}, },
"boss-boss_4-pair": { "boss-boss_4-pair": {
"about": "盐风主教昆图斯 pair pool 8400000: chess_char_5_19_b chess_char_4_01_b chess_char_5_14_b chess_char_5_17_b chess_char_5_15_b chess_char_5_20_b chess_char_5_21_b chess_char_5_22_b chess_char_5_23_b chess_char_4_02_b | chess_char_4_11_b chess_char_4_04_b chess_char_4_07_b chess_char_4_16_b chess_char_4_09_b chess_char_4_06_b chess_char_4_10_b chess_char_4_12_b chess_char_4_13_b chess_char_4_14_b token_10041_cathy_catsld token_10041_cathy_catsld", "about": "盐风主教昆图斯 pair pool 8400000: chess_char_5_19_b chess_char_4_01_b chess_char_5_14_b chess_char_5_17_b chess_char_5_15_b chess_char_5_20_b chess_char_5_21_b chess_char_5_22_b chess_char_5_23_b chess_char_4_02_b | chess_char_4_11_b chess_char_4_04_b chess_char_4_07_b chess_char_4_16_b chess_char_4_09_b chess_char_4_06_b chess_char_4_10_b chess_char_4_12_b chess_char_4_13_b chess_char_4_14_b token_10041_cathy_catsld token_10041_cathy_catsld",
@@ -1283,8 +1283,8 @@
"enemy_9022_acdumm": [1,0,0,478,0], "enemy_9022_acdumm": [1,0,0,478,0],
"enemy_9023_acdums": [12,12,0,25367,120] "enemy_9023_acdums": [12,12,0,25367,120]
}, },
"hooks": [1,53,6000,1112,1250,2538,2725,6368,26,28,66,63,0,29865,1406,6,18,0,1,28,0,10,0], "hooks": [1,53,6000,1112,1250,2538,2725,6368,26,28,66,63,0,29862,1406,6,18,0,1,28,0,10,0],
"events": {"atk":1217,"deploy":33,"die":28,"dmg":2714,"engage":14,"fx":1020,"heal":66,"skill":129,"spawn":42,"status":675}, "events": {"atk":1217,"deploy":33,"die":28,"dmg":2714,"engage":14,"fx":1020,"heal":66,"skill":129,"spawn":42,"status":683},
"fx": { "fx": {
"beam": 13, "beam": 13,
"blink": 2, "blink": 2,
@@ -1321,7 +1321,7 @@
"weightless": 5, "weightless": 5,
"zone": 76 "zone": 76
}, },
"snaps": "a3593c93 05df9199 329883cc 83065c46 6d189e2d d27e20b7 d0119d66 53c297b8 408cec99 65434e27 1d704b57 5517a58b dc7344d6 58d4ad2b b8585956 3bb8237c 89be2434 4474d986 12d9ba25 a4ab8999" "snaps": "a3593c93 05df9199 329883cc 83065c46 6d189e2d d27e20b7 d0119d66 53c297b8 408cec99 65434e27 1d704b57 5517a58b dc7344d6 58d4ad2b b8585956 eedd57cd 2c69cc89 3e330a00 d5d56836 2efc2732"
}, },
"hidden-boss_10-solo": { "hidden-boss_10-solo": {
"about": "假想敌:管 solo pool 7600000: chess_char_4_17_b chess_char_4_18_b chess_char_4_23_b chess_char_4_19_b chess_char_4_22_b chess_char_4_20_b chess_char_4_21_b chess_char_4_24_b chess_char_4_25_b chess_char_4_26_b", "about": "假想敌:管 solo pool 7600000: chess_char_4_17_b chess_char_4_18_b chess_char_4_23_b chess_char_4_19_b chess_char_4_22_b chess_char_4_20_b chess_char_4_21_b chess_char_4_24_b chess_char_4_25_b chess_char_4_26_b",
@@ -1339,32 +1339,32 @@
"total": 13, "total": 13,
"unspawned": 0, "unspawned": 0,
"bossHpLeft": 7156236, "bossHpLeft": 7156236,
"rngDraws": 28, "rngDraws": 27,
"errors": {"count":0,"kinds":[]}, "errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":12,"total":13,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":0,"dmg":595215,"bossDmg":443764,"heal":73194,"deaths":0,"layerGains":{}}}, "players": {"p1":{"killed":12,"total":13,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":0,"dmg":595215,"bossDmg":443764,"heal":72505,"deaths":0,"layerGains":{}}},
"units": [ "units": [
["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",3513,1,4,1,1,0,0,1,0], ["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",3513,1,4,1,1,0,0,1,0],
["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",7440.8,1,6,0,5,3861,4102,5,1], ["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",7440.8,1,6,0,5,3861,4169,5,0],
["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",4147.8,1,7,4,193323,7944,2803,230,5], ["p1",3,"op","chess_char_4_23_b",2,"uniequip_002_gvial2",4147.8,1,7,4,193323,7934,2790,230,5],
["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",3600.6,1,5,0,0,2336,0,0,0], ["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",3600.6,1,5,0,0,2336,0,0,0],
["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",3905.8,1,8,7,206512,35880,6134,154,3], ["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",3905.8,1,8,7,206512,35241,6004,154,3],
["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",1419,1,1,0,170086,0,0,70,7], ["p1",6,"op","chess_char_4_20_b",2,"uniequip_002_fartth",1419,1,1,0,170086,0,0,70,7],
["p1",7,"op","chess_char_4_21_b",1,"uniequip_002_plosis",2920.8,1,9,0,0,4595,39389,67,2], ["p1",7,"op","chess_char_4_21_b",1,"uniequip_002_plosis",2920.8,1,9,0,0,4595,39464,67,2],
["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",2178,1,2,0,25288,0,0,20,1], ["p1",8,"op","chess_char_4_24_b",1,"uniequip_002_billro",2178,1,2,0,25288,0,0,20,1],
["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",1671,1,10,0,0,16053,3215,0,3], ["p1",9,"op","chess_char_4_25_b",2,"uniequip_002_cetsyr",1671,1,10,0,0,15953,3078,0,3],
["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",1522,1,3,0,0,2241,17551,29,6] ["p1",10,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",1522,1,3,0,0,2241,17000,28,6]
], ],
"enemies": { "enemies": {
"enemy_1005_yokai_3": [1,0,1,13187,65325], "enemy_1005_yokai_3": [1,0,1,13187,65325],
"enemy_9010_acpupp": [2,2,0,25403,86026], "enemy_9010_acpupp": [2,2,0,25403,86026],
"enemy_9021_acduml_2": [1,0,0,0,443764], "enemy_9021_acduml_2": [1,0,0,0,443764],
"enemy_9022_acdumm": [1,0,0,1006,0], "enemy_9022_acdumm": [1,0,0,1006,0],
"enemy_9023_acdums": [10,10,0,29635,100] "enemy_9023_acdums": [10,10,0,28891,100]
}, },
"hooks": [1,25,6000,614,614,1220,1745,4474,12,13,28,26,0,15177,45,8,15,1,1,18,0,7,0], "hooks": [1,25,6000,613,613,1210,1727,4456,12,13,27,25,0,15178,45,7,15,1,1,18,0,7,0],
"events": {"atk":746,"deploy":10,"die":12,"dmg":1740,"engage":6,"fx":464,"heal":136,"leak":1,"skill":56,"spawn":25,"status":248}, "events": {"atk":746,"deploy":10,"die":12,"dmg":1722,"engage":6,"fx":460,"heal":135,"leak":1,"skill":54,"spawn":25,"status":248},
"fx": {"bandage":6,"beam":15,"burst":7,"ember":2,"explode":188,"featherArrow":7,"garrisonGrant":1,"healField":2,"jungleSoul":5,"mote":68,"phase":29,"redistribute":40,"reweave":3,"rockslide":1,"sandChainsCharged":1,"shieldBreak":7,"slash":60,"spGift":9,"summon":8,"telegraph":2,"truesilver":3}, "fx": {"bandage":6,"beam":15,"burst":7,"ember":2,"explode":185,"featherArrow":7,"garrisonGrant":1,"healField":2,"jungleSoul":5,"mote":68,"phase":29,"redistribute":40,"reweave":3,"sandChainsCharged":1,"shieldBreak":7,"slash":60,"spGift":9,"summon":8,"telegraph":2,"truesilver":3},
"snaps": "e9089456 d56595f6 94862b5d c7e389c2 97042251 346900c6 d3f1fdf3 ee72f858 3559a133 032bf766 7d5d0f98 ab1e397a ca88665f 19e0cc43 4f90670f aa7bdb4e f43775ca a84c46d1 705ff5fd 5b7831a7" "snaps": "e9089456 d56595f6 94862b5d c7e389c2 97042251 346900c6 d3f1fdf3 ee72f858 76fb6eac 826bd52d 00497c63 de189efc 37b5cbcd ab1762e2 8644257f 4db1c850 efe502ae b7764884 f14f9be1 89f56bcc"
}, },
"unite-1": { "unite-1": {
"about": "联防 1 helper(s), 11 leaked: chess_char_6_03_b chess_char_6_06_b chess_char_6_07_b chess_char_6_01_b chess_char_6_02_b chess_char_6_04_b chess_char_6_05_b chess_char_6_08_b token_10017_skadi2_dedant", "about": "联防 1 helper(s), 11 leaked: chess_char_6_03_b chess_char_6_06_b chess_char_6_07_b chess_char_6_01_b chess_char_6_02_b chess_char_6_04_b chess_char_6_05_b chess_char_6_08_b token_10017_skadi2_dedant",
+12 -12
View File
@@ -1493,7 +1493,7 @@
"boost": null, "boost": null,
"setup": {"stageId":"act1autochess_m01","bossId":"boss_5","hiddenBossId":"boss_10","factions":["DOT","INVISIBLE","REFLECTION"]}, "setup": {"stageId":"act1autochess_m01","bossId":"boss_5","hiddenBossId":"boss_10","factions":["DOT","INVISIBLE","REFLECTION"]},
"end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":28,"bossPoolLeft":0,"virtualMs":119900}, "end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":28,"bossPoolLeft":0,"virtualMs":119900},
"rng": {"setup":25,"shop":525,"waves":0,"draft":34,"bots":18074,"meta":23}, "rng": {"setup":25,"shop":525,"waves":0,"draft":34,"bots":18080,"meta":23},
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0}, "errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
"rounds": [ "rounds": [
{ {
@@ -1548,18 +1548,18 @@
}, },
{ {
"round": 11, "round": 11,
"h_0": [1,26,12,0,2,5,8,"a1840a82",60,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 1/4, preciShip 0/19, siracusaShip 1/18, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, swiftShip 1/38, yanShip 0/13",20,25,5,102], "h_0": [1,25,12,0,2,5,8,"a1840a82",60,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 1/4, preciShip 0/19, siracusaShip 1/18, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, swiftShip 1/38, yanShip 0/13",20,25,5,102],
"ai_0": [1,23,13,0,0,5,8,"e76ee4ed",224,"arcaneShip 0/2, deputShip 1/30, kazimierzShip 0/8, lateranoShip 1/39, miraShip 1/4, preciShip 0/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/40, skillfulShip 1/52, suntShip 1/12, swiftShip 1/47",25,25,0,98] "ai_0": [1,23,13,0,0,5,8,"e76ee4ed",226,"arcaneShip 0/2, deputShip 1/30, kazimierzShip 0/8, lateranoShip 1/41, miraShip 1/4, preciShip 0/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/40, skillfulShip 1/52, suntShip 1/12, swiftShip 1/47",25,25,0,98]
}, },
{ {
"round": 12, "round": 12,
"h_0": [1,26,14,0,0,6,8,"efcfaab5",123,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 1/18, preciShip 1/24, siracusaShip 1/34, soloShip 1/0, suntShip 1/0, swiftShip 1/47, yanShip 0/13",15,31,16,116], "h_0": [1,25,14,0,0,6,8,"2d442d6d",115,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 1/14, preciShip 1/24, siracusaShip 1/30, soloShip 1/0, suntShip 1/0, swiftShip 1/47, yanShip 0/13",21,31,10,116],
"ai_0": [1,23,12,0,1,6,8,"3f2e303b",264,"arcaneShip 0/2, deputShip 1/32, kazimierzShip 0/8, lateranoShip 1/59, miraShip 1/6, preciShip 0/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/42, skillfulShip 1/62, suntShip 1/14, swiftShip 1/49",31,31,0,112] "ai_0": [1,23,12,0,1,6,8,"fcca8b16",266,"arcaneShip 0/2, deputShip 1/32, kazimierzShip 0/8, lateranoShip 1/61, miraShip 1/6, preciShip 0/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/42, skillfulShip 1/62, suntShip 1/14, swiftShip 1/49",31,31,0,112]
}, },
{ {
"round": 13, "round": 13,
"h_0": [1,16,12,0,2,6,8,"31530f99",169,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 1/30, preciShip 1/33, raidShip 1/0, sargonShip 1/9, siracusaShip 1/46, suntShip 1/0, swiftShip 1/51, yanShip 0/13",8,57,49,130], "h_0": [1,15,12,0,2,6,8,"bd084a10",173,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 1/20, preciShip 1/46, raidShip 1/0, sargonShip 1/22, siracusaShip 1/34, suntShip 1/0, swiftShip 1/51, yanShip 0/13",10,57,47,130],
"ai_0": [1,13,13,0,2,6,8,"38248b3d",204,"arcaneShip 0/2, deputShip 0/32, kazimierzShip 0/8, lateranoShip 1/73, miraShip 1/6, preciShip 1/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/54, skillfulShip 0/68, suntShip 1/14, swiftShip 1/55",7,57,50,127] "ai_0": [1,13,13,0,2,6,8,"e6104ee6",194,"arcaneShip 0/2, deputShip 0/38, kazimierzShip 0/8, lateranoShip 1/69, miraShip 1/6, preciShip 1/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/54, skillfulShip 0/68, suntShip 1/14, swiftShip 1/49",12,57,45,127]
} }
], ],
"players": { "players": {
@@ -1573,9 +1573,9 @@
"title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"}, "title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"},
"trophies": 4, "trophies": 4,
"reward": 400, "reward": 400,
"stats": {"activatedLayers":189,"bossDamage":277141,"dmgDealt":2376575,"gold":144,"itemsEquipped":20,"kills":238,"leaks":76,"lpLost":12,"merges":6,"perfectRounds":9,"refreshes":24}, "stats": {"activatedLayers":193,"bossDamage":239043,"dmgDealt":2465246,"gold":144,"itemsEquipped":20,"kills":246,"leaks":68,"lpLost":13,"merges":6,"perfectRounds":9,"refreshes":24},
"lineup": ["5_10_a@12,3[1_02_e_b]","5_15_a@11,3[3_11_e_a]","3_13_b@11,8[1_01_e_a]","3_19_b@10,3[4_07_e_a+3_01_e_a]","4_16_a@10,7[4_03_e_b+3_07_e_a]","1_08_b@10,8[4_11_e_b+4_02_e_a]","6_05_a@9,8[6_09_e_a+6_01_e_b]","5_02_a@9,9"], "lineup": ["5_10_a@12,3[1_02_e_b]","5_15_a@11,3[3_11_e_a]","3_13_b@11,8[1_01_e_a]","3_19_b@10,3[4_07_e_a+3_01_e_a]","4_16_a@10,7[4_03_e_b+3_07_e_a]","1_08_b@10,8[4_11_e_b+4_02_e_a]","6_05_a@9,8[6_09_e_a+6_01_e_b]","5_02_a@9,9"],
"bonds": ["arcaneShip 0/11","kazimierzShip 0/16","kjeragShip 0/8","miraShip 1/34","preciShip 1/37","sargonShip 1/9","siracusaShip 1/46","soloShip 1/0","suntShip 1/0","swiftShip 1/63","yanShip 0/13"] "bonds": ["arcaneShip 0/11","kazimierzShip 0/16","kjeragShip 0/8","miraShip 1/24","preciShip 1/50","sargonShip 1/22","siracusaShip 1/34","soloShip 1/0","suntShip 1/0","swiftShip 1/63","yanShip 0/13"]
}, },
"ai_0": { "ai_0": {
"alive": true, "alive": true,
@@ -1587,9 +1587,9 @@
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"}, "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
"trophies": 4, "trophies": 4,
"reward": 400, "reward": 400,
"stats": {"activatedLayers":290,"bossDamage":502859,"dmgDealt":3633322,"gold":140,"itemsEquipped":22,"kills":269,"leaks":50,"lpLost":10,"merges":6,"perfectRounds":12,"refreshes":21}, "stats": {"activatedLayers":292,"bossDamage":540957,"dmgDealt":3504212,"gold":140,"itemsEquipped":22,"kills":267,"leaks":45,"lpLost":10,"merges":6,"perfectRounds":12,"refreshes":21},
"lineup": ["1_01_b@12,3[2_01_e_a+3_07_e_b]","4_21_a@12,7","3_21_b@11,3[1_05_e_b+4_08_e_b]","5_20_a@11,8","3_01_b@10,3[5_09_e_a+6_05_e_a]","3_18_a@10,7","4_16_a@10,8[4_11_e_a+4_02_e_a]","6_01_a@9,8[3_07_e_a+3_08_e_b]"], "lineup": ["1_01_b@12,3[2_01_e_a+3_07_e_b]","3_21_b@11,3[1_05_e_b+4_08_e_b]","5_20_a@11,8","3_01_b@10,3[5_09_e_a+6_05_e_a]","3_18_a@10,7","4_16_a@10,8[4_11_e_a+4_02_e_a]","6_01_a@9,8[3_07_e_a+3_08_e_b]","4_21_a@9,9"],
"bonds": ["arcaneShip 0/2","deputShip 0/32","kazimierzShip 0/8","lateranoShip 1/81","miraShip 1/6","preciShip 0/2","raidShip 0/2","sargonShip 0/2","siracusaShip 1/66","skillfulShip 1/68","suntShip 1/14","swiftShip 1/55"] "bonds": ["arcaneShip 0/2","deputShip 0/38","kazimierzShip 0/8","lateranoShip 1/83","miraShip 1/6","preciShip 0/2","raidShip 0/2","sargonShip 0/2","siracusaShip 1/66","skillfulShip 1/68","suntShip 1/14","swiftShip 1/55"]
} }
}, },
"standIns": { "standIns": {
+22 -24
View File
@@ -2512,40 +2512,38 @@
"bossHpLeft": null, "bossHpLeft": null,
"rngDraws": 43, "rngDraws": 43,
"errors": {"count":0,"kinds":[]}, "errors": {"count":0,"kinds":[]},
"players": { "players": {"p1":{"killed":46,"total":47,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":8,"dmg":535533,"bossDmg":0,"heal":17173,"deaths":4,"layerGains":{}}},
"p1": {"killed":46,"total":47,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":8,"dmg":564702,"bossDmg":0,"heal":8941,"deaths":7,"layerGains":{"visiShip":5}}
},
"units": [ "units": [
["p1",1,"op","chess_char_4_23_a",1,null,0,0,63,7,28573,4358,0,47,1], ["p1",1,"op","chess_char_4_23_a",1,null,2179,1,64,2,26142,2179,0,47,2],
["p1",2,"op","chess_char_4_01_b",1,"none",2872.7,1,9,1,7066,2118,0,105,2], ["p1",2,"op","chess_char_4_01_b",1,"none",3199.72,1,9,0,7648,1791,0,82,1],
["p1",3,"op","chess_char_4_19_a",1,null,0,0,53,0,1771,3120,0,38,2], ["p1",3,"op","chess_char_4_19_a",1,null,0,0,59,0,2327,5998,0,51,3],
["p1",4,"op","chess_char_4_22_a",1,null,2022,1,7,0,1416,3450,502,43,1], ["p1",4,"op","chess_char_4_22_a",1,null,2022,1,7,0,623,2216,101,18,1],
["p1",5,"op","chess_char_4_04_b",2,"uniequip_002_ines",2149,1,66,6,63599,10027,0,61,4], ["p1",5,"op","chess_char_4_04_b",2,"uniequip_002_ines",465,1,55,4,71128,14681,0,103,2],
["p1",6,"op","chess_char_4_07_b",2,"none",2233,1,8,1,2150,400,1,1,1], ["p1",6,"op","chess_char_4_07_b",2,"none",2233,1,8,0,0,0,0,0,0],
["p1",7,"op","chess_char_4_09_b",2,"uniequip_004_mizuki",1756,1,12,18,321808,1511,0,52,2], ["p1",7,"op","chess_char_4_09_b",2,"uniequip_004_mizuki",1492.6,1,12,26,296442,1494,0,52,2],
["p1",8,"op","chess_char_4_20_a",1,null,1217,1,2,2,22436,0,0,82,3], ["p1",8,"op","chess_char_4_20_a",1,null,1217,1,2,1,20712,0,0,82,3],
["p1",9,"op","chess_char_4_24_a",2,null,1726,1,3,10,77993,0,0,31,4], ["p1",9,"op","chess_char_4_24_a",2,null,1726,1,3,12,71831,0,0,21,4],
["p1",10,"op","chess_char_4_25_a",1,null,1221,1,4,1,13938,0,0,0,3], ["p1",10,"op","chess_char_4_25_a",1,null,1221,1,4,1,11911,0,0,0,3],
["p1",11,"op","chess_char_4_02_b",1,"none",2966.25,1,1,0,0,0,0,0,0], ["p1",11,"op","chess_char_4_02_b",1,"none",2966.25,1,1,0,0,0,0,0,0],
["p1",12,"op","chess_char_4_12_b",1,"none",4562.3,1,5,0,23952,0,0,69,4], ["p1",12,"op","chess_char_4_12_b",1,"none",4562.3,1,5,0,26769,0,0,70,4],
["p1","~1","token","char_613_acmedc",null,null,1314,1,13,0,0,0,8438,19,2] ["p1","~1","token","char_613_acmedc",null,null,1314,1,13,0,0,0,17072,27,2]
], ],
"enemies": { "enemies": {
"enemy_1001_bigbo": [1,1,0,2027,17600], "enemy_1001_bigbo": [1,1,0,2124,17600],
"enemy_10097_crshd": [1,1,0,0,8000], "enemy_10097_crshd": [1,1,0,0,8000],
"enemy_1045_hammer": [1,1,0,40,8000], "enemy_1045_hammer": [1,1,0,487,8000],
"enemy_1045_hammer_2": [6,5,0,16433,92110], "enemy_1045_hammer_2": [6,5,0,19532,89637],
"enemy_1061_zomshd": [33,33,0,4573,410182], "enemy_1061_zomshd": [33,33,0,3921,383485],
"enemy_1166_dusbr": [1,1,0,0,2400], "enemy_1166_dusbr": [1,1,0,0,2400],
"enemy_1204_msfhu_2": [1,1,0,0,6], "enemy_1204_msfhu_2": [1,1,0,0,6],
"enemy_1288_duskls_2": [1,1,0,0,4805], "enemy_1288_duskls_2": [1,1,0,0,4805],
"enemy_1427_lrnazg": [1,1,0,688,12000], "enemy_1427_lrnazg": [1,1,0,688,12000],
"enemy_2010_csdcr": [1,1,0,960,9600] "enemy_2010_csdcr": [1,1,0,1080,9600]
}, },
"hooks": [1,66,5700,698,698,1192,1191,22137,53,54,29,23,63,27315,96,51,47,0,1,54,1,0,10], "hooks": [1,64,5700,662,662,1105,1100,18020,50,51,27,23,45,27645,98,54,47,0,1,51,0,0,13],
"events": {"atk":1071,"bounty":4,"deploy":19,"die":54,"dmg":1134,"engage":10,"fx":194,"heal":24,"layer":1,"skill":52,"spawn":60,"status":330}, "events": {"atk":997,"bounty":4,"deploy":17,"die":51,"dmg":1060,"engage":9,"fx":193,"heal":47,"skill":50,"spawn":60,"status":351},
"fx": {"aoe":2,"beam":4,"buff":1,"devour":4,"displace":45,"dodge":10,"ember":1,"garrison":1,"garrisonGrant":1,"link":3,"mote":72,"overload":3,"reweave":3,"riposte":7,"ripple":2,"sentry":5,"shadowWeave":20,"shield":4,"snow":1,"summon":1,"wake":4}, "fx": {"aoe":3,"beam":2,"devour":4,"displace":51,"dodge":13,"ember":1,"garrisonGrant":1,"link":3,"mote":69,"overload":3,"reweave":3,"riposte":7,"ripple":2,"sentry":3,"shadowWeave":19,"shield":3,"snow":1,"summon":1,"wake":4},
"snaps": "10b0fc57 0b25ce77 4b6dd831 dfcc878d ea02c153 8f9524b2 a967f6bd 5b979090 1c1bc0ac a3a6e47e 27b92546 3018c6fd ff67392c 97352b97 dd49f777 68cea056 3b6f2f3a c0c82ec8 f88614a7" "snaps": "10b0fc57 0b25ce77 2eb004e3 34bc73ed 2ae3d4fc 952fb4c4 fbed5c98 4dacadad a902ca6b d97ae173 1b59ad14 ba4ffa6d 9b7e70f2 318c9aa8 c804aa7e 79f72845 7a1a0e9d 96548306 9230b759"
}, },
"roster-042": { "roster-042": {
"about": "pass 2 band 5: chess_char_4_17_b chess_char_4_18_b chess_char_4_23_b chess_char_4_19_b chess_char_4_22_b chess_char_5_01_a chess_char_4_16_b chess_char_4_13_b chess_char_4_20_b chess_char_4_24_b chess_char_4_25_b chess_char_5_02_a", "about": "pass 2 band 5: chess_char_4_17_b chess_char_4_18_b chess_char_4_23_b chess_char_4_19_b chess_char_4_22_b chess_char_5_01_a chess_char_4_16_b chess_char_4_13_b chess_char_4_20_b chess_char_4_24_b chess_char_4_25_b chess_char_5_02_a",
+35
View File
@@ -184,6 +184,41 @@ test('#1 only manual refreshes count: a re-triggered "刷新时" trait (ctx.trig
m.dispose(); m.dispose();
}); });
// PR #196 (the part 0.2.0 lacks): 贾维 【团伙行动】 grants a 叙拉古 operator on every 6th manual refresh from that refresh's
// own dispatch (band step, before the garrisons). The 拉普兰德 it grants — or the elite its copy completes — is gained
// after the refresh happened: her "本回合首次主动刷新" is the next manual one. The dispatcher walks the board and hand as
// they stood when the refresh happened (EffectDispatcher.dispatch onRefresh snapshot); it walked them live, so she fired
// (and spent her first refresh) in the refresh that granted her. Round counters are not copied on a merge (§25.13.3).
for (const variant of ['a new copy', 'a copy that completes the elite']) {
test(`#1 贾维 refresh gift (${variant}, PR #196): the 拉普兰德 waits for the next manual refresh`, () => {
const s = setup();
const { m, ps } = s;
ps.bandId = 'band_chiave';
ps.shop.level = 2;
// 贾维's draw (ctx.rollChess on the meta RNG) gives 拉普兰德; the shop rolls stay as they are
const roll = m.pool.roll.bind(m.pool);
m.pool.roll = (rng, opts = {}) => (rng === m.rngMeta && typeof opts.filter === 'function' && opts.filter(LAP) ? LAP : roll(rng, opts));
s.activate();
const elite = variant !== 'a new copy';
if (elite) { give(m, ps, LAP, 'hand'); give(m, ps, LAP, 'hand'); }
for (let i = 0; i < 5; i++) s.refresh();
const before = s.L();
assert.equal(before, elite ? 8 : 0, 'the copies already owned fire on the round\'s first refresh');
assert.ok(!ps.allChess().some((p) => p.id === LAP_B) && ps.allChess().filter((p) => p.id === LAP).length === (elite ? 2 : 0));
s.refresh(); // the 6th: 贾维's gift
const lap = ps.allChess().find((p) => p.id === (elite ? LAP_B : LAP));
assert.ok(lap, elite ? 'the gift completed the elite' : 'the gift is owned');
assert.equal(s.L(), before, 'not in the refresh that granted her');
s.refresh();
assert.equal(s.L(), before + (elite ? 8 : 4), 'her first manual refresh is the next one');
s.refresh();
assert.equal(s.L(), before + (elite ? 8 : 4), 'once a round');
assert.equal(m.dispatcher.errors, 0);
checkInvariants(m);
m.dispose();
});
}
// --------------------------------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------------------------------
// #4 突变细胞 // #4 突变细胞
+34
View File
@@ -115,3 +115,37 @@ test('a golden owner\'s summon card shows the golden variant: 精锐 巫恋\'s d
assert.equal(unit.type, 'token'); assert.equal(unit.type, 'token');
assert.equal(unit.ownerId, 'chess_char_3_15_b'); assert.equal(unit.ownerId, 'chess_char_3_15_b');
}); });
// GitHub #260 (PR #278): the 外勤医疗 strategy's map characters are operators of the mode — the card read them as summons
// (召唤物, an owner variant they do not have: base stats only)
const allText = (v) => {
if (v == null || typeof v === 'boolean') return '';
if (typeof v === 'string' || typeof v === 'number') return String(v);
if (Array.isArray(v)) return v.map(allText).join('');
if (typeof v === 'object' && typeof v.type === 'function' && ['Section', 'Stat', 'LiveTag'].includes(v.type.name)) return allText(v.type(v.props));
if (typeof v === 'object' && typeof v.props?.text === 'string') return v.props.text;
return typeof v === 'object' ? allText(v.props?.children) : '';
};
test('GitHub #260: Touch / 预备干员-医疗 cards say 干员 · 医疗 and show the skill, talents and 特性 of their record; 恳切福音 says what the sim does (低于一半)', async () => {
await data.loadAll('tokens', 'assets');
for (const [id, want] of [
['char_613_acmedc', ['恳切福音', '攻击距离<@ba.vup>+2</>', '攫升', '治疗目标时使其获得3点技力', '超脱', '获得5点技力', '恢复友方单位生命', '初始', '持续']],
['char_605_cmedic', ['治疗强化·β型', '攻击力<@ba.vup>+50%</>', '攻击提升', '攻击力+4%', '恢复友方单位生命']],
]) {
// a battle click resolves the unit as its token record
const detail = resolveDetail({ kind: 'unit', unit: { id: 91, side: 'ally', defId: id } }, new Map());
assert.equal(detail.type, 'token');
const text = allText(TokenDetail(detail));
assert.match(text, /干员/);
assert.match(text, /医疗/);
assert.doesNotMatch(text, /召唤物/);
for (const w of want) assert.ok(text.includes(w), `${id}: ${w}`);
assert.equal(/PRTS 修正/.test(text), id === 'char_613_acmedc', `${id}: the 低于一半 line only under 恳切福音`);
}
// the record keeps the official sentence; the line under it is what touchGospel does (strictly below hp_ratio)
assert.match(TOKENS.char_613_acmedc.skill.desc, /不高于一半/);
assert.match(allText(TokenDetail({ token: TOKENS.char_613_acmedc })), /实际为生命值低于一半/);
// a summon keeps its summon card
assert.match(allText(TokenDetail({ token: TOKENS.token_10006_vodfox_doll, ownerId: 'chess_char_3_15_a' })), /召唤物/);
});