feedback6 R21E: the 外勤医疗 Touch casts 恳切福音 on an injured ally inside its 5-2 (ACTIVE_RANGE in touchKit — the larger-range rule the Touch 补位 already runs by its data; the map character's record keeps DEFAULT), and the Touch / 预备干员-医疗 card is an operator card (干员 · 医疗 · 远程位, 特性, skill row with SP / duration, talents from the tokens.json record — it took the summon path: 召唤物, base stats only); 恳切福音 gets a line under its text that the boost is strictly below half (PRTS 修正 of 不高于, what touchGospel does; the operator card of the 补位 Touch too: SKILL_TEXT_NOTES); GitHub #260, PR #278; golden: roster-041 (its strategy Touch heals 8438 → 17072, 19 → 27 heal actions, casts unchanged at 2, the same numbers as PR #278; p1 deaths 7 → 4) and matches coop2-NORMAL-14-standins (h_0 plays 外勤医疗: defeat in round 13 → victory, teamLp 16) — both battles with that strategy's Touch, no other scenario moved

This commit is contained in:
sganggs committed 2026-10-07 17:54:17 +08:00
1 parent 783bd30afd
commit c623550f29
11 files changed
+203 -61

No files matched your search

+3 -1
View File
@@ -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 || {};
+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 };
} }
+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: {} },
+27 -27
View File
@@ -1492,8 +1492,8 @@
"combat": "client", "combat": "client",
"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":false,"roundsPassed":13,"hiddenReached":false,"hiddenCleared":false,"reason":"defeat","teamLp":0,"bossPoolLeft":59326,"virtualMs":119550}, "end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":16,"bossPoolLeft":0,"virtualMs":119900},
"rng": {"setup":25,"shop":484,"waves":0,"draft":34,"bots":17816,"meta":19}, "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": [
{ {
@@ -1553,43 +1553,43 @@
}, },
{ {
"round": 12, "round": 12,
"h_0": [1,10,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,15,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,"97827397",254,"arcaneShip 0/2, deputShip 1/32, kazimierzShip 0/8, lateranoShip 1/49, 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,"b3d60fb0",254,"arcaneShip 0/2, deputShip 1/32, kazimierzShip 0/8, lateranoShip 1/49, 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": [0,0,12,0,0,6,0,"811c9dc5",0,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 0/30, preciShip 0/33, sargonShip 0/9, siracusaShip 0/46, swiftShip 0/51, yanShip 0/13",8,57,49,130], "h_0": [1,5,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,"c4308d5d",194,"arcaneShip 0/2, deputShip 0/32, kazimierzShip 0/8, lateranoShip 1/63, miraShip 1/6, preciShip 1/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/54, skillfulShip 0/68, suntShip 1/14, swiftShip 1/55",10,57,47,127] "ai_0": [1,13,13,0,2,6,8,"5a6df95e",182,"arcaneShip 0/2, deputShip 0/38, kazimierzShip 0/8, lateranoShip 1/57, miraShip 1/6, preciShip 1/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/54, skillfulShip 0/68, suntShip 1/14, swiftShip 1/49",15,57,42,127]
} }
], ],
"players": { "players": {
"h_0": { "h_0": {
"alive": false, "alive": true,
"victory": false, "victory": true,
"roundsPassed": 12, "roundsPassed": 14,
"eliminatedRound": 13, "eliminatedRound": null,
"lp": 0, "lp": 4,
"bandId": "band_amedic", "bandId": "band_amedic",
"title": {"id":"comment_6","name":"挥金如土","picId":"comment_icon_6","text":"消耗资金最多"}, "title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"},
"trophies": 3, "trophies": 4,
"reward": 280, "reward": 400,
"stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1950591,"gold":130,"itemsEquipped":14,"kills":218,"leaks":76,"lpLost":28,"merges":4,"perfectRounds":9,"refreshes":20}, "stats": {"activatedLayers":193,"bossDamage":275036,"dmgDealt":2465720,"gold":144,"itemsEquipped":20,"kills":244,"leaks":68,"lpLost":23,"merges":6,"perfectRounds":9,"refreshes":24},
"lineup": [], "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/7","kazimierzShip 0/8","miraShip 0/30","preciShip 0/33","sargonShip 0/9","siracusaShip 0/46","swiftShip 0/51","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,
"victory": false, "victory": true,
"roundsPassed": 13, "roundsPassed": 14,
"eliminatedRound": null, "eliminatedRound": null,
"lp": 0, "lp": 12,
"bandId": "band_chiave", "bandId": "band_chiave",
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"}, "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
"trophies": 3, "trophies": 4,
"reward": 320, "reward": 400,
"stats": {"activatedLayers":280,"bossDamage":330674,"dmgDealt":3048187,"gold":142,"itemsEquipped":22,"kills":259,"leaks":47,"lpLost":10,"merges":6,"perfectRounds":12,"refreshes":21}, "stats": {"activatedLayers":280,"bossDamage":504964,"dmgDealt":3358197,"gold":140,"itemsEquipped":22,"kills":260,"leaks":42,"lpLost":10,"merges":6,"perfectRounds":12,"refreshes":21},
"lineup": ["1_01_b@12,3[2_01_e_a+1_02_e_a]","4_21_a@12,7","3_21_b@11,3[3_01_e_a+1_05_e_b]","5_20_a@11,8[3_07_e_b]","3_01_b@10,3[4_08_e_b+6_05_e_a]","3_18_a@10,7[4_11_e_b+4_02_e_b]","4_16_a@10,8","6_01_a@9,8[5_09_e_a+3_08_e_a]"], "lineup": ["1_01_b@12,3[2_01_e_a+3_08_e_b]","3_21_b@11,3[3_01_e_a+1_05_e_b]","5_20_a@11,8[3_07_e_b]","3_01_b@10,3[4_08_e_b+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[5_09_e_a+3_07_e_a]","4_21_a@9,9"],
"bonds": ["arcaneShip 0/2","deputShip 0/32","kazimierzShip 0/8","lateranoShip 1/71","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/71","miraShip 1/12","preciShip 0/2","raidShip 0/2","sargonShip 0/2","siracusaShip 1/66","skillfulShip 1/68","suntShip 1/14","swiftShip 1/49"]
} }
}, },
"standIns": { "standIns": {
@@ -1607,7 +1607,7 @@
[10,"3_18_a→char_609_acguad 4_16_a→char_615_acspec 5_10_a→char_606_csuppo"], [10,"3_18_a→char_609_acguad 4_16_a→char_615_acspec 5_10_a→char_606_csuppo"],
[11,"4_16_a→char_615_acspec 5_10_a→char_606_csuppo"], [11,"4_16_a→char_615_acspec 5_10_a→char_606_csuppo"],
[12,"4_16_a→char_615_acspec 5_10_a→char_606_csuppo 6_05_a→char_611_acnipe"], [12,"4_16_a→char_615_acspec 5_10_a→char_606_csuppo 6_05_a→char_611_acnipe"],
[13,""] [13,"4_16_a→char_615_acspec 5_10_a→char_606_csuppo 6_05_a→char_611_acnipe"]
] ]
} }
}, },
+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",
+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' })), /召唤物/);
});