feedback5: merge CB3 — community reports (operator kits): 砾's normal attack hits twice at 50 % 伤害倍率 (a 频次 enemy loses 2), 乌尔比安's S3 moves only onto his own board (never behind the one-helper 联防 map's red gate nor onto a boss field's other half), the 「受到攻击时」 counters answer every enemy damage instance (菲莱 S2 on 深溟巢涌者's pulse, 星熊, 泡泡, 刺玫, 余, 玛恩纳, 年, 斩业星熊), an in-place 复活 restarts a deploy-timed skill so a 突袭 member jumps at once, 银灰's normal attack plays its own impact (and 雷蛇 / 初雪 / 圣约送葬人 drop a skill mode's sound); five reports pinned; §25.17 between CB2's §25.16 and CB4's §25.18; data/assets.json lists 银灰's impact (p_imp_spear_n.mp3 fetched); golden: vs feedback5 CB3's own rules — roster-002/004/005/008/010/012/016/024/033/044, bond-indomShip-low/high, bond-kazimierzShip-low/high, bond-lateranoShip-low, boss-boss_2-solo, boss-boss_4-pair/-solo, hidden-boss_8-pair/-solo, hidden-boss_9-solo, diy-051, matches solo-FUNNY-2, coop2-NORMAL-3, coop4-ABYSS-7, coop2-ABYSS-10-boosted; vs CB3 exactly what feedback5 changed since its base (CB2, CB4, CB5)
The merge:
- server/sim/battle/players.js: CB3's onOwnBoard hunk is byte-identical to CB4's (git kept one); its comment and
docs/SIM.md's onOwnBoard row now name 乌尔比安's S3 【移动】 next to the 突袭 landing (DESIGN §25.17.2 / §25.18).
- test/sim/facing-invariance.test.js: CB3's scenario setup (onOwnBoard off on the open field) and the CB4 merge's
突袭 test (its enemy inside the board) merged cleanly, both kept.
- data/assets.json: CB3 had regenerated it offline without 银灰's entry (his official impact file was not on disk);
`node tools/fetch-assets.mjs --add-only` (shared public/assets, add-only) downloaded p_imp_spear_n.mp3 — 8.8 KB,
the only new file (ok 1, skipped 7968; the 117 misses are the known upstream gaps) — and rewrote the manifest.
Field-wise JSON compare with CB3's manifest: only audio.sfx.units.char_172_svrash = { hit: p_imp_spear_n } plus hash
and stats (files / bytes / sfxUnits +1); against feedback5: CB3's four operators (银灰 now the spear impact and no
S3 swing, 雷蛇 / 初雪 / 圣约送葬人 without the skill-mode impact) plus hash / stats. §25.17.6 says so.
- docs/history/0.2.0.md: §25.17 placed in order, the intro's per-stream list and workstream list name CB3.
Golden moves outside CB3's commit list: coop2-NORMAL-3 fields 砾 from round 3 (her double hit) and 刺玫 in rounds 4-7
(instrumented); solo-FUNNY-1 (in CB3's list through the counters) fields no counter operator on feedback5's path, only
砾, whose two half hits change no kill, leak, LP or end stat there, so its digest stays.
Gate: non-browser suites 5003 / 5003, npm run golden ok, test/golden.test.js 9 / 9, lint 0 errors, check:imports ok,
typecheck ok.
This commit is contained in:
commit
d841d6ac22
33 files changed
+956
-245
No files matched your search
+6
-1
@@ -286,9 +286,14 @@ export function resolveHit(b, u, prof, target, info, x, y) {
|
||||
let mulT = skillMul;
|
||||
if (prof.dmgMul) { const m = typeof prof.dmgMul === 'function' ? prof.dmgMul(b, u, target) : prof.dmgMul; if (Number.isFinite(m)) mulT *= m; }
|
||||
const hits = prof.hitsFn ? prof.hitsFn(b, u, info) : Math.max(1, prof.hits || 1);
|
||||
// `hitDmgMul` (professions.js header): each instance's 伤害倍率 — the DamageInfo `mul`, after DEF / RES — and only the
|
||||
// first instance may give 受击回复 (砾's two 50 % hits, PRTS 砾 特性备注)
|
||||
const split = Number.isFinite(prof.hitDmgMul) && prof.hitDmgMul > 0 ? prof.hitDmgMul : null;
|
||||
let dealtMain = 0;
|
||||
for (let h = 0; h < hits && target.alive; h++) {
|
||||
dealtMain += b.dealDamage(u, target, { amount: atk * scale * mulT, type: baseType, isAttack: true, isSkill: info.isSkill, tags: prof.tags || [], attackId });
|
||||
const d = { amount: atk * scale * mulT, type: baseType, isAttack: true, isSkill: info.isSkill, tags: prof.tags || [], attackId };
|
||||
if (split != null) { d.mul = split; if (h > 0) d.noSp = true; }
|
||||
dealtMain += b.dealDamage(u, target, d);
|
||||
}
|
||||
dealtTotal += dealtMain;
|
||||
if (prof.onHitStatus && target.alive) b.applyStatus(target, prof.onHitStatus.key, { duration: prof.onHitStatus.duration, source: u, value: prof.onHitStatus.value });
|
||||
|
||||
@@ -79,7 +79,8 @@ export class BattlePlayers {
|
||||
* tiles its pieces are deployed on) mapped onto this field — the 联防 right-hand helper's +8 columns, the boss field's
|
||||
* rows 2–5, the mirrored right half of a pair (whatever coordinates the player's input uses). Never the hand / 临时
|
||||
* 整备区 rows of a boss field (rows 0–1, inside BOSS_RECT) nor the other half of a 联防 or boss field. Automatic
|
||||
* redeployments that must stay where the player could deploy read it (the 突袭 landing, bonds/addon/battle.js raidTile).
|
||||
* redeployments and moves that must stay where the player could deploy read it (the 突袭 landing, bonds/addon/battle.js
|
||||
* raidTile; 乌尔比安's S3 【移动】, kits/ops/chess_char_5_05-ulpia.js).
|
||||
*/
|
||||
onOwnBoard(ps, r, c) {
|
||||
if (!ps) return false;
|
||||
|
||||
@@ -246,8 +246,26 @@ function hammerState(battle, rt, u) {
|
||||
*/
|
||||
export function deploymentOf(u) { return `${u.deploySeq}:${u.mem.revives | 0}`; }
|
||||
|
||||
/** An in-place 复活 (M3茧甲, 埃芒加德) happened: a new deployment for the once-per-deployment lock (deploymentOf). */
|
||||
export function revivedInPlace(u) { if (u && u.mem) u.mem.revives = (u.mem.revives | 0) + 1; }
|
||||
/**
|
||||
* An in-place 复活 (M3茧甲, 埃芒加德) happened. PRTS's 复活 is 退场 + a 0-time / 0-cost redeploy (PRTS 盟约记录 备注) — a new
|
||||
* deployment, of which the remake carries: the once-per-deployment lock (deploymentOf) and a deploy-timed skill ("部署后…秒内",
|
||||
* SkillSpec `activateOnDeploy`: 缄默德克萨斯 S1–S3, 宴 S2, 斯卡蒂 S2, 砾's barrier …) running again — started anew when its window
|
||||
* had ended, its window back to the full duration when it still ran (the next tick, out of the damage pipeline that set the
|
||||
* revive off). Community report of 2026-10-06 (item 44): a 突袭 member counts such a running window as 技能就绪
|
||||
* (bonds/addon/battle.js raidPoll), and after an in-place revive it waited out the 10 s idle time instead of jumping at once
|
||||
* as after a redeploy. The SP reset and the other 部署时 effects of PRTS's form stay unmodelled (DESIGN §21.21).
|
||||
*/
|
||||
export function revivedInPlace(u) {
|
||||
if (!u || !u.mem) return;
|
||||
u.mem.revives = (u.mem.revives | 0) + 1;
|
||||
const sk = u.skill;
|
||||
if (!sk || sk.noSkill || !sk.spec || !sk.spec.activateOnDeploy || !sk.battle) return;
|
||||
const seq = u.deploySeq, n = u.mem.revives;
|
||||
sk.battle.after(0, () => {
|
||||
if (!u.alive || !u.deployed || u.deploySeq !== seq || u.mem.revives !== n || u.skill !== sk) return;
|
||||
if (sk.active) { if (sk.duration > 0) sk.timeLeft = sk.duration; } else sk.activate('deploy', { free: true });
|
||||
}, { owner: u });
|
||||
}
|
||||
|
||||
/**
|
||||
* Does `u` hold 坚固维式重锤's 不死 right now — a window started in this deployment that has not run out? The window lives
|
||||
|
||||
@@ -6,8 +6,9 @@ import { onDefaultSkill } from '../shared/tier2.js';
|
||||
|
||||
export default {
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// 2_08 泡泡 “挨打”: stops attacking; DEF +def, taunt +taunt_level, each attack received returns atk_scale × DEF phys
|
||||
// damage to the attacker. 尖刺盾: attackers get ATK −atk for `duration` s. Elite module (PRO-X): DEF +def while blocking.
|
||||
// 2_08 泡泡 “挨打”: stops attacking; DEF +def, taunt +taunt_level, each enemy damage instance received (its attack or not:
|
||||
// the official bubble_s_2 / bubble_t_1 fire on ON_TAKE_DAMAGE — tier1 byEnemyAttack) returns atk_scale × DEF phys damage
|
||||
// to its source. 尖刺盾: that source gets ATK −atk for `duration` s. Elite module (PRO-X): DEF +def while blocking.
|
||||
// S1 防御力强化·β型 (alt, TAKE_DAMAGE from data): DEF +def for its duration; the counter belongs to “挨打” only.
|
||||
chess_char_2_08_a: (bb, chess) => {
|
||||
const t = talentBb(chess, 0);
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
|
||||
import { num, talentBb, installAura } from '../shared/tier1.js';
|
||||
|
||||
/**
|
||||
* 砾's normal attack (PRTS 砾 特性备注): "在砾的攻击动作下,每次普通攻击造成两段伤害,但是每段最终只会造成50%的伤害(在计算防御/减伤后,在
|
||||
* 重设伤害前);第二段伤害不会触发目标的受击回复" — two damage instances of 50 % 伤害倍率 each (DamageInfo `mul`, after DEF: not a 50 %
|
||||
* 攻击倍率), so a 频次 enemy loses 2 per attack; the second carries no 受击回复 (ai.js resolveHit `hitDmgMul`). Every attack she
|
||||
* makes is a normal attack (both her skills are passive).
|
||||
*/
|
||||
const GRAVEL_HITS = 2;
|
||||
const GRAVEL_HIT_DMG_MUL = 0.5;
|
||||
|
||||
export default {
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// 2_12 砾 鼠群 (passive): at each deployment a barrier of hp_ratio × max HP that decays to 0 over `duration` s
|
||||
@@ -10,10 +19,11 @@ export default {
|
||||
// 快速部署: own deployment cost +cost (−1). Elite 小个子支援: every unit whose initial deployment cost ≤ cond.cost
|
||||
// DEF +def while 砾 is on the field. (Elite module withdraw refund: no manual retreat in battle ⇒ no effect.)
|
||||
// S1 影袭 (alt, passive): at each deployment DEF +def, decaying linearly to 0 over `duration` s, updated once per
|
||||
// second (PRTS: "防御力加成每秒更新一次").
|
||||
// second (PRTS: "防御力加成每秒更新一次"). Trait note: every normal attack hits twice at 50 % (GRAVEL_HIT_DMG_MUL above).
|
||||
chess_char_2_12_a: (bb, chess) => {
|
||||
const t = talentBb(chess, 0);
|
||||
return {
|
||||
trait: { hits: GRAVEL_HITS, hitDmgMul: GRAVEL_HIT_DMG_MUL },
|
||||
skill: {
|
||||
kind: 'duration', activateOnDeploy: true, duration: num(bb.duration, 10), spCost: 0, spType: 'none', trigger: 'NEVER',
|
||||
onStart({ battle, unit }) {
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
// Conventions of the tier-3 kits: ../shared/tier3.js; kit contract and rules: ../README.md.
|
||||
|
||||
import { num, defOf, talentBb, traitBb, selectedId, altSkills, fx, NINE, giveSp } from '../shared/tier3.js';
|
||||
import { isHpLoss } from '../../../damage.js';
|
||||
|
||||
export default {
|
||||
// ---- 3_06 菲莱 · 本源铁卫 — S2 冥河诅咒: stops attacking, HP +; when attacked blasts the ground enemies of the 3×3
|
||||
// around her (PRTS 备注: range x-4; arts + ep_damage_ratio × ATK apoptosis, aoe_cd); ATK + once hit by element
|
||||
// ---- 3_06 菲莱 · 本源铁卫 — S2 冥河诅咒: stops attacking, HP +; "受到攻击时" — any damage instance she takes (official
|
||||
// philae_s_2: ON_TAKE_DAMAGE with no source or attack filter) — blasts the ground enemies of the 3×3 around her
|
||||
// (PRTS 备注: range x-4; arts + ep_damage_ratio × ATK apoptosis, aoe_cd); ATK + once hit by element
|
||||
// damage; 神河谕使: 元素损伤 taken −damage_resistance, +SP on apoptosis
|
||||
// 精锐 module PRP-X "阻挡敌人时,自身造成的元素损伤提升15%": EVERY element fill she deals (the S2 blast, a 灼燃维式重锤
|
||||
// she carries …) ×ep_damage_scale while she blocks — an `elementHit` multiplier, like 余's
|
||||
@@ -85,7 +87,11 @@ export default {
|
||||
if (isS2 && sk && sk.active && !unit.findBuff('skill:philae_rage')) battle.addBuff(unit, { key: 'skill:philae_rage', mods: { atkPct: num(bb.atk) }, visible: true });
|
||||
return;
|
||||
}
|
||||
if (!isS2 || !sk || !sk.active || !ctx.dmg?.isAttack || !ctx.source || ctx.source.side !== 'enemy') return;
|
||||
// the official philae_s_2 (buff_template_data) fires on ON_TAKE_DAMAGE with no filter: any damage instance — an
|
||||
// enemy's attack, a skill hit, 深溟巢涌者's pulse, a 无来源 hit — never a 流失 or an element 损伤 (above), nor a
|
||||
// counter / reflection. Until 0.2.0 enemy attacks only (community report of 2026-10-06, item 30)
|
||||
const d = ctx.dmg;
|
||||
if (!isS2 || !sk || !sk.active || !d || isHpLoss(d) || (d.tags || []).some((t) => t === 'counter' || t === 'reflect')) return;
|
||||
if (battle.time < (unit.mem.philaeCd ?? -Infinity)) return;
|
||||
unit.mem.philaeCd = battle.time + cd;
|
||||
// "周围的地面敌人" = range x-4, the 3×3 tiles around her (PRTS 备注)
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
|
||||
|
||||
import { AURA, num, tbb, whileDeployed, pulse, toggleBuff, isSel, alt, withDefaults } from '../shared/tier4.js';
|
||||
import { byEnemyAttack } from '../shared/tier1.js';
|
||||
|
||||
export default withDefaults({
|
||||
// ===== 星熊 (protector) S2 荆棘 (passive) — DEF +13 %, counters every attacker for 65 % ATK phys; talents
|
||||
// ===== 星熊 (protector) S2 荆棘 (passive) — DEF +13 %, answers every enemy damage instance with 65 % ATK phys on its source; talents
|
||||
// S1 战意 (TAKE_DAMAGE: DEF/ATK up); S3 力之锯 (ATK/DEF up, cuts every enemy on her front tile — all enemies of her
|
||||
// range); module PRO-X (护身符): DEF +20 % while blocking
|
||||
chess_char_4_17_a: (bb, chess, def) => {
|
||||
@@ -40,7 +41,8 @@ export default withDefaults({
|
||||
if (!S2) return;
|
||||
battle.on('damaged', (c) => {
|
||||
const src = c.source;
|
||||
if (c.target !== unit || !unit.alive || !src || src.side !== 'enemy' || !src.alive || !c.dmg?.isAttack) return;
|
||||
// 荆棘 = the official inverse_damage: every enemy damage instance, not its attacks only (tier1 byEnemyAttack)
|
||||
if (c.target !== unit || !unit.alive || !byEnemyAttack(c) || !src.alive) return;
|
||||
battle.dealDamage(unit, src, { amount: unit.s.atk * num(bb.atk_scale, 0.65), type: 'phys', canDodge: false, isSkill: true, tags: ['counter'] });
|
||||
battle.fx('thorns', { x: unit.x, y: unit.y, id: unit.id });
|
||||
}, { owner: unit });
|
||||
|
||||
@@ -13,7 +13,8 @@ export default {
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// 乌尔比安 — S3 必须开辟的通路 (25 s, CUSTOM_RANGE row ahead): max HP/ATK +, throws an anchor forward that stops on the
|
||||
// first enemy or at max distance — on his own tile while he blocks: 135 % ATK phys + 6 s stun around it
|
||||
// (projectile_range); moves onto the anchor tile when deployable and not his own (a 从不混淆的方向 marker keeps his tile)
|
||||
// (projectile_range); moves onto the anchor tile when deployable (a tile of his own board) and not his own (a 从不混淆的方向
|
||||
// marker keeps his tile)
|
||||
// and returns at skill end — both 【移动】, i.e. free redeploys (Battle.moveRedeploy; the return empties his SP).
|
||||
// T1 本性的坚守: heal 100 (160 below 50 %) on every hit taken. T2 血脉的哺养: per kill +120 max HP / +30 ATK (×9),
|
||||
// other Abyssal Hunters +50 %. Module (elite): healing received ×1.2.
|
||||
@@ -90,8 +91,12 @@ export default {
|
||||
// anchor on his own tile leaves him where he stands, no marker, nothing to return from [ASSUMED: the "tile one
|
||||
// beyond the landing" is not tried when the landing is his own tile]
|
||||
if (stop === 0 || !unit.alive) return;
|
||||
// PRTS 备注: landing tile > the tile one beyond it > his own tile (a deployable, free, unreserved melee tile)
|
||||
const ok = ([r, c]) => (r !== unit.tileR || c !== unit.tileC) && battle.grid.inRect(r, c) && battle.grid.canStand(r, c) && !battle.grid.isObstacle(r, c) && !battle.isReservedTile(r, c);
|
||||
// PRTS 备注: landing tile > the tile one beyond it > his own tile (a deployable, free, unreserved melee tile).
|
||||
// "可部署" is a tile his player may deploy on: his own board (Battle.onOwnBoard) — never the other half of a 联防 or
|
||||
// boss field nor a boss field's hand / 临时整备区 rows. Community report of 2026-10-06 (item 27) 「乌尔比安使用3技能会在
|
||||
// 联防阶段跳到红门后」: on the one-helper 联防 map (escaped_single, enemies from the middle gate at col 10) an anchor that
|
||||
// met no enemy flew on to the right half, where no enemy ever walks, and he moved there for the whole skill
|
||||
const ok = ([r, c]) => (r !== unit.tileR || c !== unit.tileC) && battle.grid.inRect(r, c) && battle.onOwnBoard(unit.player, r, c) && battle.grid.canStand(r, c) && !battle.grid.isObstacle(r, c) && !battle.isReservedTile(r, c);
|
||||
const dest = [[sr, sc], frontOf(unit.tileR, unit.tileC, unit.dir, stop + 1)].find(ok);
|
||||
if (dest == null) return;
|
||||
const home = [unit.tileR, unit.tileC];
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
num, on, inRange, talent, skillGrid, batPct, mods, inFaction, isOp, selectedId, lazySkills, skillRange, leaderOf,
|
||||
whileOn, permBuff,
|
||||
} from '../shared/tier5.js';
|
||||
import { byEnemyAttack } from '../shared/tier1.js';
|
||||
|
||||
/** 玛恩纳 游侠 "周围存在3名及以上敌人" radius (8-neighbourhood). */
|
||||
const AROUND_RADIUS = 1.5;
|
||||
@@ -18,7 +19,7 @@ export default {
|
||||
// PRTS 备注 the trait bonus drops by 10 % (per_kill_reduce, absolute: +400 % → +390 %) for each enemy knocked out by
|
||||
// his own attack (or the damage it carries — not 无动于衷's reflection, not a mark another operator's attack set off),
|
||||
// settled after that attack, never below +0 %. T1 游侠: ×1.1 ATK on attacks (×1.15 and −15 % damage taken with ≥3
|
||||
// enemies around). T2 无动于衷: taunt +1, Kazimierz ops reflect 15 % of his ATK as true damage when attacked.
|
||||
// enemies around). T2 无动于衷: taunt +1, Kazimierz ops reflect 15 % of his ATK as true damage when an enemy damages them.
|
||||
// S1 未声张的怒火 (duration, SEARCH): attacks attack@atk_scale × ATK, DEF +. S2 未宽解的悲哀 (duration): skill range,
|
||||
// BAT +0.3 s, attacks attack@atk_scale × ATK twice; a kill of his own attacks during the skill keeps the trait ramp
|
||||
// when it ends. S1 / S2 have no 对空 note: ground only, like his trait. S2's data rule is ACTIVE_RANGE on its 2-3 (the
|
||||
@@ -85,7 +86,9 @@ export default {
|
||||
if (!(scale > 0)) return;
|
||||
battle.on('damaged', (c) => {
|
||||
const a = c.target, src = c.source;
|
||||
if (a.side !== 'ally' || !isOp(a) || !isKazimierz(a) || !src || src.side !== 'enemy' || !src.alive || !c.dmg?.isAttack) return;
|
||||
// the official mlynar_t_2[inverse] (ON_TAKE_DAMAGE, InverseDamage from an enemy source): every enemy damage instance
|
||||
// a Kazimierz operator takes, not its attacks only (tier1 byEnemyAttack)
|
||||
if (a.side !== 'ally' || !isOp(a) || !isKazimierz(a) || !byEnemyAttack(c) || !src.alive) return;
|
||||
if (!on(unit) || leaderOf(battle, unit) !== unit) return;
|
||||
battle.dealDamage(unit, src, { amount: unit.s.atk * scale, type: 'true', canDodge: false, tags: ['talent', 'reflect'] });
|
||||
}, { owner: unit });
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
num, bv, tbb, moduleBb, live, opsOf, ANY, onDefaultSkill, selectedSkill, skillGridOf, AROUND8, onElementHit,
|
||||
elementDmg, aura,
|
||||
} from '../shared/tier6.js';
|
||||
import { holdProtect } from '../shared/tier1.js';
|
||||
import { holdProtect, byEnemyAttack } from '../shared/tier1.js';
|
||||
|
||||
/**
|
||||
* "传送至自身位置": a ground enemy that can reach `unit`'s tile on the ground grid is moved onto it (unblocked, its
|
||||
@@ -40,8 +40,8 @@ function yu(bb, chess, def) {
|
||||
// "将第二天赋效果赋予全场所有干员" belongs to S3 (the default skill) only
|
||||
const s3On = (unit) => isDef && !!unit.skill?.active;
|
||||
const skills = {
|
||||
// S1 今日做东 (TAKE_DAMAGE, hurt SP): passive taunt +taunt_level; active: HP / DEF +, every attack taken ⇒
|
||||
// ep_damage_ratio × ATK 灼燃损伤 on the attacker (install below)
|
||||
// S1 今日做东 (TAKE_DAMAGE, hurt SP): passive taunt +taunt_level; active: HP / DEF +, every enemy damage instance taken ⇒
|
||||
// ep_damage_ratio × ATK 灼燃损伤 on its source (install below)
|
||||
skchr_yu_1: { kind: 'duration', mods: { hpPct: num(bb.max_hp), defPct: num(bb.def) } },
|
||||
// S2 厚礼上宾 (cast with an enemy on its x-1: the data's SKILL_RANGE, a deliberate deviation from the 重装 TAKE_DAMAGE
|
||||
// row — tools/build-data.mjs TRIGGER_DEVIATIONS, DESIGN §22.10): atk_scale × ATK arts on every enemy of the skill range
|
||||
@@ -71,7 +71,8 @@ function yu(bb, chess, def) {
|
||||
battle.addBuff(unit, { key: 'yu:host', mods: { taunt: num(bb.taunt_level, 1) }, persist: true, allowDead: true });
|
||||
battle.on('damaged', (ctx) => {
|
||||
const s = ctx.source;
|
||||
if (ctx.target !== unit || !unit.skill?.active || !s || s.side !== 'enemy' || !s.alive || !ctx.dmg?.isAttack) return;
|
||||
// "每次受到攻击" = the official yu_s_1[inverse_damage]: every enemy damage instance (tier1 byEnemyAttack)
|
||||
if (ctx.target !== unit || !unit.skill?.active || !byEnemyAttack(ctx) || !s.alive) return;
|
||||
elementDmg(battle, unit, s, 'burn', unit.s.atk * num(bb.ep_damage_ratio), ['skill']);
|
||||
}, { owner: unit });
|
||||
},
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
// data's 'phys' too since 0.2.0 WE2 — build-data classifyAttack) and arts while one runs (every skill's attack override); block 3, ground-only
|
||||
// melee on her 1-1. Module AST-X 无迹 adds "且攻击和受到攻击时对目标额外造成10%攻击力的法术伤害" (trait bb atk_scale): every
|
||||
// damage instance of her normal attacks (skill attacks included; not the shield, a counter or the module's own damage)
|
||||
// is followed by atk_scale × ATK arts on that enemy (附加伤害: no dodge), and every enemy attack she takes returns
|
||||
// atk_scale × ATK arts (普通伤害) to the attacker — whether a skill runs or not [ASSUMED: PRTS's list of the checked
|
||||
// is followed by atk_scale × ATK arts on that enemy (附加伤害: no dodge), and every enemy damage instance she takes
|
||||
// returns atk_scale × ATK arts (普通伤害) to its source (tier1 byEnemyAttack) — whether a skill runs or not [ASSUMED: PRTS's list of the checked
|
||||
// conditions names no skill period].
|
||||
// - T1 业火 "生命降至0时进入“我执”…": a lethal damage instance leaves her on the field in 我执 instead (the damage past her
|
||||
// HP opens the negative pool — the engine keeps her at its 1-HP floor for the official 0); in 我执 every later damage
|
||||
@@ -34,8 +34,8 @@
|
||||
// - T2 鬼之架势 "在场时,自身获得最高+50法术抗性和35%攻击力的坚忍(损失70%生命值时达到最大加成)": RES +min_magic_resistance and ATK
|
||||
// +min_atk scaled linearly by the HP lost from max_hp_ratio down to min_hp_ratio (坚忍), re-read every tick; in 我执 at
|
||||
// its maximum. AST-X stage 3: the maximum at 50 % lost (min_hp_ratio 0.5) and "达到最大加成时,攻击力额外+15%" (atk).
|
||||
// - S1 恶业苦果 (AUTO, 受击回复 SP, 持续时间无限 — a toggle): ATK +atk, DEF +def, arts attacks; every enemy attack she takes
|
||||
// deals atk_scale × ATK arts to the attacker (a direct pick of the damage source, enemies only). A self buff: the owner's
|
||||
// - S1 恶业苦果 (AUTO, 受击回复 SP, 持续时间无限 — a toggle): ATK +atk, DEF +def, arts attacks; every enemy damage instance
|
||||
// she takes deals atk_scale × ATK arts to its source (a direct pick of the damage source, enemies only). A self buff: the owner's
|
||||
// AUTO rule fires it as soon as its SP is full (`trigger: 'SP_FULL'`, as 引星棘刺 S1 — kits/README.md checklist 5).
|
||||
// - S2 无始无明 (AUTO, attack SP; data DEFAULT): the cast replaces the attack about to be made — the shield throw, three hits
|
||||
// of attack@atk_scale × ATK arts on every enemy she blocks (none blocked: her target [ASSUMED]); then the shield circles
|
||||
@@ -52,7 +52,7 @@
|
||||
// refund of a withdrawal is not modelled — no retreat in the sim refunds DP). No strategy of this mode closes it: PRTS
|
||||
// 帮助 lists the 上半 row that did as removed in 下半, so in a fight the skill simply runs its `duration`.
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, up } from '../shared/tier1.js';
|
||||
import { num, talentBb, traitBb, skillRec, up, byEnemyAttack } from '../shared/tier1.js';
|
||||
import { canTargetEnemy } from '../../../targeting.js';
|
||||
import { bodyInRadius } from '../../../body.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
@@ -252,15 +252,17 @@ export default {
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// S1 恶业苦果: every enemy attack she takes ⇒ atk_scale × ATK arts back to the attacker
|
||||
// S1 恶业苦果: every enemy damage instance she takes (the official hsgma2_s_1 on ON_TAKE_DAMAGE, not its attacks only:
|
||||
// tier1 byEnemyAttack) ⇒ atk_scale × ATK arts back to its source
|
||||
battle.on('damaged', (c) => {
|
||||
const s = c.source;
|
||||
if (c.target !== unit || !unit.alive || unit.skill?.id !== S1 || !unit.skill.active) return;
|
||||
if (!s || s.side !== 'enemy' || !s.alive || !c.dmg?.isAttack) return;
|
||||
if (!byEnemyAttack(c) || !s.alive) return;
|
||||
battle.dealDamage(unit, s, { amount: unit.s.atk * num(b1.atk_scale), type: 'arts', canDodge: false, isSkill: true, ignoreSelect: true, tags: ['skill', 'counter'] });
|
||||
battle.fx('counter', { x: s.x, y: s.y, id: unit.id });
|
||||
}, { owner: unit });
|
||||
// AST-X 无迹: her attack damage ⇒ +atk_scale × ATK arts (附加伤害); an enemy attack she takes ⇒ atk_scale × ATK arts back
|
||||
// AST-X 无迹: her attack damage ⇒ +atk_scale × ATK arts (附加伤害); an enemy damage instance she takes (the official
|
||||
// hsgma2_e_003_tr_take: ON_TAKE_DAMAGE from the other side) ⇒ atk_scale × ATK arts back
|
||||
const astx = num(tb.atk_scale);
|
||||
if (astx > 0) {
|
||||
battle.on('damaged', (c) => {
|
||||
@@ -270,7 +272,7 @@ export default {
|
||||
}, { owner: unit, priority: -20 });
|
||||
battle.on('damaged', (c) => {
|
||||
const s = c.source;
|
||||
if (c.target !== unit || !unit.alive || !s || s.side !== 'enemy' || !s.alive || !c.dmg?.isAttack) return;
|
||||
if (c.target !== unit || !unit.alive || !byEnemyAttack(c) || !s.alive) return;
|
||||
battle.dealDamage(unit, s, { amount: unit.s.atk * astx, type: 'arts', ignoreSelect: true, tags: ['module', ASTX, 'counter'] });
|
||||
}, { owner: unit });
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
// no-SP-during-a-skill rule, tier1 giveSp).
|
||||
// - S1 锡灼 (MANUAL, time SP; data DEFAULT — the owner's 重装 cast-in-range exception, rawRule TAKE_DAMAGE): `duration` s of
|
||||
// DEF +def and ATK +atk; normal attacks deal arts damage.
|
||||
// - S2 铜印 (MANUAL; data DEFAULT): `duration` s — stops attacking; DEF +def, block +block_cnt; every enemy attack she takes
|
||||
// (each damage instance of one, as 星熊 荆棘 / 泡泡) deals atk_scale × ATK arts to the attacker and silences it `silence`
|
||||
// - S2 铜印 (MANUAL; data DEFAULT): `duration` s — stops attacking; DEF +def, block +block_cnt; every enemy damage instance she
|
||||
// takes (its attack or not, as 星熊 荆棘 / 泡泡) deals atk_scale × ATK arts to its source and silences it `silence`
|
||||
// s ("失去特殊能力"): a direct pick of the damage source (ignoreSelect), enemies only (PRTS 备注), even when a 护盾 layer
|
||||
// negated the hit.
|
||||
// - S3 铁御 (MANUAL; data DEFAULT — its x-2 is a 技能范围 for the allies, no attack-range change): `duration` s of ATK
|
||||
@@ -37,7 +37,7 @@
|
||||
// −one_minus_status_resistance), refreshed every AURA s while the skill runs; two 年 of a shared field keep the stronger
|
||||
// (installAura's rule). The DEF / block buff ends with the skill, the 抵抗 within AURA_DUR.
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, statBuff, toggleBuff, up, giveSp } from '../shared/tier1.js';
|
||||
import { num, talentBb, traitBb, skillRec, statBuff, toggleBuff, up, giveSp, byEnemyAttack } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
|
||||
@@ -154,12 +154,13 @@ export default {
|
||||
battle.addBuff(unit, { key: FORT, mods: nz({ hpPct: perHp * n, healingTakenMul: Math.min(healMax, healBase + healAdd * n) }), data: { n }, tags: ['trait'] });
|
||||
}, { owner: unit });
|
||||
}
|
||||
// S2 铜印: every enemy attack she takes ⇒ atk_scale × ATK arts back to the attacker + silence `silence` s
|
||||
// S2 铜印: every enemy damage instance she takes (the official inverse_damage[magic] + nian_s_2 on ON_TAKE_DAMAGE, not
|
||||
// its attacks only: tier1 byEnemyAttack) ⇒ atk_scale × ATK arts back to its source + silence `silence` s
|
||||
const scale = num(b2.atk_scale), sil = num(b2.silence);
|
||||
battle.on('damaged', (c) => {
|
||||
const src = c.source;
|
||||
if (c.target !== unit || !unit.alive || unit.skill?.id !== S2 || !unit.skill.active) return;
|
||||
if (!src || src.side !== 'enemy' || !src.alive || !c.dmg?.isAttack) return;
|
||||
if (!byEnemyAttack(c) || !src.alive) return;
|
||||
if (scale > 0) battle.dealDamage(unit, src, { amount: unit.s.atk * scale, type: 'arts', canDodge: false, isSkill: true, ignoreSelect: true, tags: ['skill', 'counter'] });
|
||||
if (sil > 0 && src.alive) battle.applyStatus(src, 'silence', { duration: sil, source: unit });
|
||||
battle.fx('counter', { x: src.x, y: src.y, id: unit.id });
|
||||
|
||||
@@ -31,6 +31,7 @@ import { COLS, TICK } from '../../../constants.js';
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { offsetTile } from '../../../dir.js';
|
||||
import { bodyInKeys, bodyTileReach } from '../../../body.js';
|
||||
import { isHpLoss } from '../../../damage.js';
|
||||
|
||||
// =================================================================================================================
|
||||
// shared helpers (named exports; content/index.js only merges the default export)
|
||||
@@ -66,8 +67,21 @@ export const cheb = (a, b) => (b.hitArea ? bodyTileReach(b, Math.round(a.y), Mat
|
||||
: Math.max(Math.abs(Math.round(a.y) - Math.round(b.y)), Math.abs(Math.round(a.x) - Math.round(b.x))));
|
||||
/** Normal attack hit on its primary target (no splash, no chain jump). */
|
||||
export const isMainHit = (dmg) => !!dmg && dmg.isAttack && !dmg.isSplash && !(dmg.tags && dmg.tags.includes('chain'));
|
||||
/** Damage ctx caused by an enemy's attack. */
|
||||
export const byEnemyAttack = (ctx) => !!ctx.source && ctx.source.side === 'enemy' && !!ctx.dmg && ctx.dmg.isAttack;
|
||||
/**
|
||||
* `damaged` ctx that sets off a "受到攻击时" counter: a damage instance from an enemy, of any kind — its normal attack, a
|
||||
* skill hit, an area pulse (深溟巢涌者's 无途径 法术伤害). The official counters fire on ON_TAKE_DAMAGE from a source of the
|
||||
* other side, not on attacks only (ArknightsGameData buff_template_data: inverse_damage / inverse_damage[magic] — 星熊 S2,
|
||||
* 年 S2 —, bubble_s_2 / bubble_t_1 — 泡泡 —, vendla_s_2 — 刺玫 —, yu_s_1[inverse_damage] — 余 S1 —, hsgma2_s_1 /
|
||||
* hsgma2_e_003_tr_take — 斩业星熊 —, mlynar_t_2[inverse] — 玛恩纳 无动于衷; 菲莱 S2's philae_s_2 checks no source at all).
|
||||
* Never a 流失 (PRTS 作战机制 "生命流失…不会触发反伤、受击回复等受到攻击触发的时点"), an element 损伤 (its own event), 无来源 damage
|
||||
* (no source to strike back) or another counter / reflection (tags 'counter' / 'reflect': no ping-pong). Community report
|
||||
* of 2026-10-06 (item 30): it took enemy attacks only, so 深溟巢涌者's pulse never set a counter off.
|
||||
*/
|
||||
export const byEnemyAttack = (ctx) => {
|
||||
const s = ctx.source, d = ctx.dmg;
|
||||
if (!s || s.side !== 'enemy' || !d || d.sourceless || ctx.type === 'element' || isHpLoss(d)) return false;
|
||||
return !(Array.isArray(d.tags) && (d.tags.includes('counter') || d.tags.includes('reflect')));
|
||||
};
|
||||
/**
|
||||
* `damaged` ctx of a damage that removed HP and can give 受击回复 SP — the engine's rule (damage.js applyHpLoss: not a 流失
|
||||
* (`noSp`, Battle.loseHp), not an element 损伤), whatever its source: an attack, a zone, the 无来源 源石溶剂 tick.
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
// healThrough(healer, ally) → bool (a healer that selects and heals that ally through its 禁疗 — Battle
|
||||
// injuredAlliesInKeys, damage.js heal; 凯尔希 on her Mon3tr)
|
||||
// hitsFn(battle, unit, info) → n (info: the hit's { isSkill, index, attackId, energy })
|
||||
// hitDmgMul n (伤害倍率 of each of the `hits` instances on the main target: DamageInfo `mul`, applied after DEF / RES
|
||||
// and the damage multipliers, not 攻击倍率 — a 频次 enemy still loses 1 per instance; the instances after
|
||||
// the first carry `noSp`, no 受击回复: 砾's two 50 % hits, PRTS 砾 特性备注 — ai.js resolveHit)
|
||||
// install(battle, unit) — per-unit hooks, called once at setup
|
||||
// storeEnergy(battle, unit) → bool / releaseEnergy(battle, unit) → n (秘术师: the attack check found no valid target ⇒
|
||||
// store one energy, false when full; the energies leaving with an attack — installMystic)
|
||||
|
||||
Reference in new issue
Block a user