feedback5: merge O9 — 自选 kits for 帕拉斯, 止颂 and 贝洛内 (with his 牵绊); golden: only diy moved

This commit is contained in:
sganggs committed 2026-10-06 01:33:46 +08:00
commit a2d2f6a12f
8 files changed
+2056 -96

No files matched your search

+3
View File
@@ -109,6 +109,9 @@ export const OPERATOR_KIT_FILES = Object.freeze([
'op-zumama.js',
'op-judge.js',
'op-shu.js',
'op-pallas.js',
'op-lessng.js',
'op-demetr.js',
]);
/**
+315
View File
@@ -0,0 +1,315 @@
// server/sim/content/kits/ops/op-demetr.js — 贝洛内 (char_4037_demetr) 自选 operator kit: 6★ 斗士 (近卫), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait, his module at every form and his summon 牵绊.
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_4037_demetr, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
// elite = E2 Lv60, rank 7, FGT-Y (his only module) at stage 1 (tier 5) or 3 (tier 6) or none. Potential 0 [ASSUMED: no
// account]. Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json; the 牵绊 token
// record backups.json tokens.token_10065_demetr_dmtpos); PRTS 贝洛内 (家族手段 备注 "防御减少效果于贝洛内每次普通攻击时,计算伤害前
// 施加(可影响当次攻击)…防御减少方式为最终乘算…多个贝洛内之间的【手段】间仅防御减少量最高者生效…每0.1秒自然更新一次;当具有≥6层层数
// 的【手段】自然更新/响应更新时,若该【手段】的来源(贝洛内)不处于2技能状态或已离场,层数将强制变回5层…增伤效果为线性提升,可对任何来自
// 贝洛内的伤害(不止普通攻击)生效"; 街头直觉 备注 "部署时立刻获得80%物理/法术闪避,之后每秒衰减2%(至多衰减20次),最终变为40%";
// 家主的余裕 备注 "可触发两次第一天赋"; 军师的手段 备注; 清算 备注 — the 牵绊 rules, "每次移动的目标地块必须为合法、可部署、未部署有
// 牵绊/贝洛内自己以外单位的地面地块", "在受到致命伤害结算后立即结束技能,因返回而部署时获得短暂的不死", "若技能开启时,若存在目标则会立刻
// 尝试移动;否则会尝试原地移动一次", "技能结束时,若不位于初始位置且场上不存在牵绊,贝洛内将被强制撤退"); PRTS 牵绊 备注 (无敌, 孤立,
// 晕眩 / 冻结 / 寒冷 / 沉睡 免疫); PRTS 卫戍协议/帮助 特殊策略 (【清算】 "在自定义范围内存在敌人时释放技能", x-2); gamedata_const
// ba.addash 移动 "不退场,以当前血量在目标位置部署"; the client's battle data — charpack char_4037_demetr (every attack ability
// carries demetr_t1[def_dec]; S2 mode `demetr_s2[mark]`; S3 modes), skchr_demetr_3 (FlyToTarget / FlyToTargetSecond: the
// x-2 selector around his located tile, buildable melee low-ground tiles empty but for him and his token; RespawnEnd), buff
// _template_data `demetr_t1[def_dec]` / `[def_dec_core]` / `[dmg_inc]`, `demetr_t2[evade]`, `demetr_s3[bonus]` / `[target_timer]`
// / `[timer]` / `[elite_finish_listener]` / `[undeadable]` / `[restore]`; his skeleton (Skill_3_Back 0.5 s).
// - Trait (斗士) "能够阻挡一个敌人": the profession default (block 1, melee, ground only). Module FGT-Y “实用的工具” "生命值高于
// 50%时攻击速度+10" (trait bb attack_speed / hp_ratio): ASPD while HP > hp_ratio.
// - T1 家族手段 (bb attack@def / attack@limited_stack_cnt / attack@s2_limited_stack_cnt / attack@def_dec_duration,
// min/max_hp_ratio, min/max_add_on_scale): each of his attack hits puts 【手段】 on its target before the damage (it counts
// for that hit): one stack more (at most limited_stack_cnt, s2_limited_stack_cnt while S2 runs), DEF ×(1 − |def| × stacks)
// (FINAL_SCALER), def_dec_duration s from the last hit; at s2_limited_stack_cnt stacks the target is 停顿 (sluggish) for as
// long as it keeps them; every 0.1 s a 【手段】 above limited_stack_cnt whose 贝洛内 is not in S2 (or not on the field) drops
// back to limited_stack_cnt, keeping its time. One 【手段】 per enemy: several 贝洛内 (a shared field) share its stacks and
// the higher per-stack value [ASSUMED: the client keeps one per source and only the highest reduction acts]. And every
// damage of his ×(1 + add-on), the add-on linear from min_add_on_scale at min_hp_ratio of the target's HP to
// max_add_on_scale at max_hp_ratio and below (DamageScaleAccordingToHpRatio, the target's HP before the hit). FGT-Y stage 3:
// 8 % per stack, +42 % at most.
// - T2 街头直觉 (bb init_prob / dec_prob / trig_cnt): from every deployment — a 【移动】 is one (demetr_s3_t[born_sync]) —
// physical and arts dodge init_prob, −dec_prob each second trig_cnt times (80 % → 40 %), then stays.
// - S1 家主的余裕 (AUTO, attack SP 4; data DEFAULT): the next attack strikes its target twice for atk_scale × ATK physical
// (`_additionalTimes` 1 — both strikes land in the attack [ASSUMED: the skeleton's 0.2 s between them is not modelled]);
// each strike adds a 【手段】 stack.
// - S2 军师的手段 (MANUAL, 15 / 18 s; data ACTIVE_RANGE on its 3-13): range 3-13, ASPD +attack_speed, every attack strikes
// attack@max_target targets for attack@atk_scale × ATK physical; 【手段】 up to s2_limited_stack_cnt and 停顿 at the cap. Its
// attack is a projectile in the client (projectile_chr_demetr_s2): the hits land at the attack [ASSUMED: flight not
// modelled]; ground targets only, as every attack of his (selector targetMotion 1).
// - S3 清算 (MANUAL, 30 s; data CUSTOM_RANGE on x-2): ATK +atk, ASPD +attack_speed, each damage instance attack@…prob to be
// ×prob_atk_scale (AtkScaleUp: on the attack's ATK, before DEF). At the cast he 【移动】s (Battle.moveRedeploy: no exit, HP
// / buffs / the skill kept, deploy effects again — 街头直觉's 80 %) onto the tile of the first ground enemy of the x-2 around
// his located tile (his tile at the cast) whose tile he may take — a melee-buildable low-ground tile of the field, nobody
// on it but his 牵绊 or himself, no knocked-out operator, no other piece's reserved tile — in the usual target order
// (sortEnemyTargets); without one he moves in place once. He takes another target the same way when the elite / leader
// enemy he attacked last (within attack@finish_listener_duration s) is knocked out, and whenever 1 s (interval × time_stack)
// passed without him dealing damage — retried every interval while no target exists, never while he cannot act. Leaving his
// located tile he leaves 牵绊 there (when none of his is up); moving back onto it retreats 牵绊 first. A lethal hit while it
// runs does not knock him out: 不死, untargetable, blocking nothing, 禁疗 (demetr_s3[undeadable]) and the skill ends on the
// next tick. At the end he returns — a 【移动】 onto his located tile (in place when he is there) that empties his SP — and
// gets a short 不死 (RETURN_UNDYING); not on that tile and no 牵绊 of his up (or the tile taken) ⇒ forced retreat ('retreat').
// The client's wind-up / landing clips (Skill_3_ReStart_Begin 0.2 s, _End 0.3 s) are not modelled: each move is instant
// [ASSUMED].
// - 牵绊 (token_10065_demetr_dmtpos, S3 only — its variant bySkill 2): "贝洛内技能结束时返回该位置,并且其他干员不能部署" — it holds
// his located tile; 无敌, untargetable, 孤立, no attack, blocks nothing (blockCnt 0) — PRTS 牵绊 备注.
// Melee, ground-only (data canHitFly false: no anti-air); ground enemies target him normally (no 起飞 / 迷彩 / 隐匿).
import { num, talentBb, traitBb, skillRec, up, onHitBy, toggleBuff, once } from '../shared/tier1.js';
import { isElite } from '../shared/tier6.js';
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
const S1 = 'skchr_demetr_1';
const S2 = 'skchr_demetr_2';
const S3 = 'skchr_demetr_3';
export const BOND = 'token_10065_demetr_dmtpos';
/** S3's search area when the data carries none (x-2: the 5 × 5 square around him without its corners). */
const X2 = Object.freeze([[2, -1], [2, 0], [2, 1], [1, -2], [1, -1], [1, 0], [1, 1], [1, 2], [0, -2], [0, -1], [0, 0], [0, 1],
[0, 2], [-1, -2], [-1, -1], [-1, 0], [-1, 1], [-1, 2], [-2, -1], [-2, 0], [-2, 1]]);
/** "因返回而部署时获得短暂的不死" (PRTS 备注; no number): his return clip, Skill_3_Back 0.5 s [ASSUMED]. */
export const RETURN_UNDYING = 0.5;
/** 【手段】 on an enemy (one per enemy) and its 停顿 at the S2 cap; the 【手段】 refresh tick (PRTS 备注 "每0.1秒自然更新一次"). */
export const MEANS_KEY = 'talent:demetr:means';
export const MEANS_SLUG_KEY = 'talent:demetr:means:sluggish';
const MEANS_TICK = 0.1;
export const STREET_KEY = 'talent:demetr:street';
export const UNDEAD_KEY = 'skill:demetr:undeadable';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const s2On = (u) => !!(u && u.skill && u.skill.active && u.skill.id === S2);
const s3On = (u) => !!(u && u.skill && u.skill.active && u.skill.id === S3);
/** 牵绊's kit (a fresh one per summon): no attack, 无敌 and 孤立 (spawnToken adds untargetable). */
export function bondKit() {
return {
skill: null,
trait: { noAttack: true },
install(battle, tok) {
battle.addBuff(tok, { key: 'token:demetr:bond', flags: { invulnerable: true, isolated: true }, persist: true, allowDead: true });
},
};
}
/** Put / refresh 【手段】 on `e` (the stack rule of demetr_t1[def_dec]) and its 停顿 at the S2 cap. */
function applyMeans(battle, unit, e, p) {
const cur = e.findBuff(MEANS_KEY);
const old = cur?.data?.stacks ?? 0;
const cap = s2On(unit) ? p.cap8 : p.cap5;
const stacks = old < cap ? old + 1 : old;
const other = cur && cur.data?.src !== unit && up(cur.data?.src) ? num(cur.data?.pct) : 0;
const pct = Math.max(p.pct, other);
setMeans(battle, e, unit, { stacks, pct, cap5: p.cap5, cap8: p.cap8 }, p.dur);
}
function setMeans(battle, e, src, data, dur) {
battle.addBuff(e, { key: MEANS_KEY, duration: dur, refresh: 'replace', source: src, mods: { defMul: Math.max(0, 1 - data.pct * data.stacks) }, data: { ...data, src }, tags: ['talent'] });
if (data.stacks >= data.cap8) battle.addBuff(e, { key: MEANS_SLUG_KEY, duration: dur, refresh: 'replace', source: src, status: 'sluggish', mods: { moveMul: 0.2 }, visible: true, tags: ['talent'] });
else battle.removeBuff(e, MEANS_SLUG_KEY);
}
/** The 0.1 s update of every 【手段】 (battle-wide, once): above the normal cap without its 贝洛内 in S2 ⇒ back to it. */
function installMeansTick(battle) {
once(battle, 'demetr:means', () => battle.every(MEANS_TICK, () => {
for (const e of battle.enemies) {
if (!e.alive) continue;
const b = e.findBuff(MEANS_KEY);
const d = b?.data;
if (!d || !(d.stacks > d.cap5)) continue;
if (d.src && d.src.alive && d.src.deployed && s2On(d.src)) continue;
setMeans(battle, e, d.src, { stacks: d.cap5, pct: d.pct, cap5: d.cap5, cap8: d.cap8 }, b.timeLeft);
}
}));
}
export default {
char_4037_demetr: (bb, chess) => {
const tb = traitBb(chess); // FGT-Y: attack_speed / hp_ratio
const t0 = talentBb(chess, 0); // 家族手段 (FGT-Y stage 3: def −0.08, max_add_on_scale 0.42)
const t1 = talentBb(chess, 1); // 街头直觉
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2), s3 = skillRec(chess, S3);
const area = s3?.trigger?.customRangeGrid ?? X2;
const means = {
pct: Math.abs(num(t0['attack@def'])), cap5: Math.max(1, Math.floor(num(t0['attack@limited_stack_cnt'], 5))),
cap8: Math.max(1, Math.floor(num(t0['attack@s2_limited_stack_cnt'], 8))), dur: num(t0['attack@def_dec_duration'], 10),
};
const hpHi = num(t0.min_hp_ratio, 1), hpLo = num(t0.max_hp_ratio, 0.2);
const addLo = num(t0.min_add_on_scale), addHi = num(t0.max_add_on_scale);
const iv = num(b3['attack@demetr_s3[target_timer].interval'], 0.5);
const idle = iv * Math.max(1, num(b3['attack@demetr_s3[target_timer].time_stack'], 2));
const listen = num(b3['attack@finish_listener_duration'], 3);
const prob = num(b3['attack@demetr_s3[bonus].prob']), probScale = num(b3['attack@demetr_s3[bonus].prob_atk_scale'], 1);
/** A tile he may 【移动】 onto (FlyToTarget selector options, PRTS 备注). */
const tileOk = (battle, unit, st, r, c) => {
if (!battle.grid.inRect(r, c) || !battle.grid.canStand(r, c) || !battle.grid.isLow(r, c) || battle.downOn(r, c)) return false;
if (r === unit.tileR && c === unit.tileC) return true;
const occ = battle.unitAt(r, c);
if (occ) return occ === st.bond;
return !battle.isReservedTile(r, c);
};
/** The first ground enemy of the x-2 around his located tile standing on a tile he may take. */
const pickTarget = (battle, unit, st) => {
const keys = absoluteRangeKeys(area, st.home[0], st.home[1], unit.dir, 0);
const list = battle.enemiesInKeys(keys, unit, { canHitFly: false, groundOnly: true })
.filter((e) => !e.isFlying && tileOk(battle, unit, st, Math.round(e.y), Math.round(e.x)));
sortEnemyTargets(battle, unit, list, unit.profile?.priority ?? null);
return list[0] ?? null;
};
const retreatBond = (battle, st) => {
if (st.bond && st.bond.alive) battle.retreat(st.bond, { reason: 'expired', permanent: true });
st.bond = null;
};
/** One 【移动】 of S3 onto (r, c): 牵绊 retreated first when it is his located tile, left there when he leaves it. */
const moveTo = (battle, unit, st, r, c) => {
const [hr, hc] = st.home;
const home = r === hr && c === hc;
if (home && (unit.tileR !== hr || unit.tileC !== hc)) retreatBond(battle, st);
const fromX = unit.x, fromY = unit.y;
if (!battle.moveRedeploy(unit, r, c)) return false;
battle.fx('teleport', { x: unit.x, y: unit.y, id: unit.id, fromX, fromY });
if (!home && !(st.bond && st.bond.alive)) st.bond = battle.spawnToken(unit, BOND, hr, hc, { untargetable: true, kit: bondKit() });
return true;
};
/** FlyToTarget(Second): move onto the next target's tile; false without one (or when he cannot act). */
const flyTo = (battle, unit, st) => {
if (!up(unit) || !unit.canAct || st.undead) return false;
const t = pickTarget(battle, unit, st);
if (!t || !moveTo(battle, unit, st, Math.round(t.y), Math.round(t.x))) return false;
st.next = battle.time + idle;
return true;
};
return {
skills: {
[S1]: { kind: 'instant', attack: { atkScale: num(b1.atk_scale, 1), hits: 2 } },
[S2]: {
kind: 'duration',
mods: { aspd: num(b2.attack_speed) },
targeting: { ...(s2?.rangeGrid?.length ? { rangeGrid: s2.rangeGrid } : {}), maxTargets: Math.max(1, num(b2['attack@max_target'], 3)) },
attack: { atkScale: num(b2['attack@atk_scale'], 1) },
},
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3['attack@demetr_s3[bonus].atk']), aspd: num(b3['attack@demetr_s3[bonus].attack_speed']) },
onStart({ battle, unit, skill }) {
const st = { home: [unit.tileR, unit.tileC], bond: null, next: battle.time + idle, act: skill.activations, lastHit: null, lastHitAt: -Infinity, retarget: false, undead: false };
unit.mem.demetrS3 = st;
// "若技能开启时,若存在目标则会立刻尝试移动;否则会尝试原地移动一次"
if (!flyTo(battle, unit, st) && up(unit)) moveTo(battle, unit, st, unit.tileR, unit.tileC);
},
onTick({ battle, unit }) {
const st = unit.mem.demetrS3;
if (!st) return;
// the elite / leader he attacked last fell: FlyToTargetSecond at once (one try; the idle timer runs on)
if (st.retarget) {
st.retarget = false;
if (flyTo(battle, unit, st)) return;
}
if (battle.time + 1e-9 < st.next) return;
// no target (or he cannot act): the timer keeps counting — the next try at its next interval
if (!flyTo(battle, unit, st)) while (st.next <= battle.time + 1e-9) st.next += iv;
},
onEnd({ battle, unit }) {
const st = unit.mem.demetrS3;
unit.mem.demetrS3 = null;
if (!st) return;
battle.removeBuff(unit, UNDEAD_KEY);
if (!unit.alive || !unit.deployed) { retreatBond(battle, st); return; }
const [hr, hc] = st.home;
const atHome = unit.tileR === hr && unit.tileC === hc;
const bondUp = !!(st.bond && st.bond.alive);
retreatBond(battle, st);
const fromX = unit.x, fromY = unit.y;
// 【返回】: a 【移动】 onto the located tile (in place when he stands on it) that empties the SP (demetr_s3[restore])
if ((atHome || bondUp) && battle.moveRedeploy(unit, hr, hc, { clearSp: true })) {
if (!atHome) battle.fx('teleport', { x: unit.x, y: unit.y, id: unit.id, fromX, fromY });
unit.mem.demetrGraceUntil = battle.time + RETURN_UNDYING;
return;
}
battle.retreat(unit, { reason: 'retreat' }); // "贝洛内将被强制撤退"
},
},
},
talents: [
{ install(battle, unit) { // 家族手段
installMeansTick(battle);
battle.on('hit', (c) => {
if (c.source !== unit || !c.dmg?.isAttack || !c.target || c.target.side !== 'enemy' || !c.target.alive) return;
if (means.pct > 0) applyMeans(battle, unit, c.target, means);
}, { owner: unit, priority: 10 });
if (addHi || addLo) {
onHitBy(battle, unit, (c) => {
const r = c.target.hpRatio;
const t = hpHi > hpLo ? Math.min(1, Math.max(0, (hpHi - r) / (hpHi - hpLo))) : (r <= hpLo ? 1 : 0);
c.dmg.mul *= 1 + addLo + (addHi - addLo) * t;
});
}
} },
{ install(battle, unit) { // 街头直觉
const init = num(t1.init_prob), dec = num(t1.dec_prob), cnt = Math.max(0, Math.floor(num(t1.trig_cnt)));
if (!(init > 0)) return;
const set = (p) => battle.addBuff(unit, { key: STREET_KEY, mods: { dodgePhys: p, dodgeArts: p }, data: { p }, tags: ['talent'] });
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
const seq = unit.deploySeq;
let p = init, n = 0;
set(p);
if (!(dec > 0) || !cnt) return;
battle.every(1, (b, sc) => {
if (!unit.alive || unit.deploySeq !== seq) { sc.cancel(); return; }
p = Math.abs(p - dec);
set(p);
if (++n >= cnt) sc.cancel();
}, { owner: unit });
}, { owner: unit });
} },
],
install(battle, unit) {
// FGT-Y: 生命值高于50%时攻击速度+10
const aspd = num(tb.attack_speed);
if (aspd) toggleBuff(battle, unit, 'trait:demetr:aspd', () => unit.hpRatio > num(tb.hp_ratio, 0.5), { aspd });
// S3 清算: ×prob_atk_scale at prob per damage instance (AtkScaleUp: the attack's ATK, before DEF)
if (prob > 0 && probScale !== 1) {
onHitBy(battle, unit, (c) => {
if (s3On(unit) && battle.rng.chance(prob)) c.dmg.amount *= probScale;
});
}
// S3: the target he attacked last (an elite / leader one re-targets him when it falls within `listen` s)
battle.on('hit', (c) => {
const st = unit.mem.demetrS3;
if (!st || c.source !== unit || !c.dmg?.isAttack || !c.target || c.target.side !== 'enemy') return;
st.lastHit = c.target;
st.lastHitAt = battle.time;
}, { owner: unit });
// S3: 1 s without output damage re-targets him (the timer restarts with each damage instance he deals)
battle.on('damaged', (c) => {
const st = unit.mem.demetrS3;
if (st && c.source === unit && c.target && c.target.side === 'enemy') st.next = battle.time + idle;
}, { owner: unit });
battle.on('death', (c) => {
const st = unit.mem.demetrS3;
if (!st || c.reason !== 'killed' || c.unit !== st.lastHit || !isElite(c.unit) || battle.time - st.lastHitAt > listen + 1e-9) return;
st.lastHit = null;
st.retarget = true; // taken in his next skill tick (the enemy index of that tick sees every enemy)
}, { owner: unit });
// S3: 受到致命伤害时不撤退但会结束技能 (demetr_s3[undeadable]); the return's short 不死
battle.on('fatal', (c) => {
if (c.unit !== unit || c.prevented) return;
const st = unit.mem.demetrS3;
if (st && s3On(unit)) {
c.prevented = true;
if (st.undead) return;
st.undead = true;
battle.addBuff(unit, { key: UNDEAD_KEY, flags: { untargetable: true, noBlock: true, healFree: true }, tags: ['skill'], visible: true });
battle.fx('undying', { x: unit.x, y: unit.y, id: unit.id });
const act = st.act;
battle.after(0, () => { if (unit.mem.demetrS3 === st && s3On(unit) && unit.skill.activations === act) unit.skill.end('fatal'); }, { owner: unit });
return;
}
if (battle.time < (unit.mem.demetrGraceUntil ?? -Infinity) - 1e-9) c.prevented = true;
}, { owner: unit, priority: -50 });
},
};
},
};
+160
View File
@@ -0,0 +1,160 @@
// server/sim/content/kits/ops/op-lessng.js — 止颂 (char_4011_lessng) 自选 operator kit: 6★ 无畏者 (近卫), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules at every form.
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_4011_lessng, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
// elite = E2 Lv60, rank 7, the picked module at stage 1 (tier 5) or 3 (tier 6). Potential 0 [ASSUMED: no account].
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json); PRTS 止颂 (苦痛专注 备注
// "将降低无来源或任意来源于非“自身阻挡的敌人”的物理、法术伤害…伤害降低效果于“受到伤害前”生效…<Y模组>额外效果为在计算伤害前,若伤害
// 目标满足条件则临时获取…物理穿透(百分比)Buff(不可叠加,直接加算)"; 痛楚砺刃 备注 "天赋于受到伤害时触发,无需成功受到伤害也可
// 生效…重复受到伤害时,重置本天赋持续时间"; 苦修破誓 备注 "技能期间,持有状态免疫、晕眩反制、寒冷反制、冻结反制…技能开启时,若有
// 异常状态,会先对自身造成法术伤害(来源为自身,可致死),然后清除自身所有异常状态,最后再获得技能效果"; DRE-X 备注 "于每次计算伤害时检查
// 目标是否处于阻挡/被阻挡状态…提升效果为攻击力倍率提升"; DRE-Y 备注 "属于免死效果,触发效果后获得属性变化后生命上限100%的生命复原");
// the client's battle data — buff_template_data `lessng_t1` (ON_APPLYING_MODIFIER: he blocks, the source is not blocked by
// him ⇒ DamageScale 1 − damage_resistance on PHYSICAL_AND_MAGICAL), `lessng_t2` (ON_TAKE_DAMAGE ⇒ ATK MULTIPLIER for
// add_atk_duration), `lessng_s3[atk_scale]` / `lessng_e_002_trait[blocked]` (ON_CALCULATE_DAMAGE, CheckBlocked of the
// target — by anyone — ⇒ AtkScaleUp), `lessng_s3[anti]`, `lessng_e_003_trait` / `lessng_e_003_talent`; charpack
// char_4011_lessng (S2 mode: `_additionalTimes` 1, its talent with `_applyTalentScale`), skchr_lessng_2 (`_castOnLocate`,
// the mode for `duration` s), skchr_lessng_3 (`_canUseInAbnormalState`); his skeleton (Skill_2_Loop: OnAttack 0.300 / 0.567 s).
// - Trait (无畏者) "能够阻挡一个敌人": the profession default (block 1, melee, ground only). Module DRE-X “沉锋之束” "攻击被阻挡
// 的敌人时攻击力提升至115%" (trait bb atk_scale): ×atk_scale on his attack's ATK against a target blocked by anyone (a
// profile dmgMul — the scale of the attack, before DEF). Module DRE-Y “苦修者的抗压训练” "被击倒时不撤退且回复所有生命但生命上限
// -60%,攻击速度+30(单次部署只触发1次)" (trait bb max_hp 0.4 / attack_speed / hp_ratio): the first lethal hit of a deployment
// is prevented, max HP ×max_hp (FINAL_SCALER) and ASPD +attack_speed for the rest of that deployment, HP set to hp_ratio of
// the new max (生命复原, not a heal); not while a 不死 already holds him ("_dontConsumeWhenUndeadable": 坚固维式重锤's window,
// items/battle.js holdsUndying).
// - T1 苦痛专注 "阻挡时,受到来自非自身阻挡敌人的物理和法术伤害降低35%" (bb damage_resistance): while he blocks an enemy, a
// physical or arts damage instance whose source is not an enemy he blocks — 无来源, an ally, himself included — is
// ×(1 − damage_resistance) (`hit`: before the 伤判 / barrier steps). S2's window doubles it (talent_scale). DRE-Y stage 3:
// 40 %, and "攻击自身阻挡的敌人时,无视目标12%的防御力" (the hidden module talent's def_penetrate: defIgnorePct on his damage to an
// enemy he blocks).
// - T2 痛楚砺刃 "受到伤害后,攻击力+12%,持续15秒(不可叠加)" (bb atk / add_atk_duration): every damage instance aimed at him
// (`hit`, so a dodged or shielded one too; never a 流失) sets ATK +atk for add_atk_duration s, refreshed, never stacked.
// DRE-X stage 3: +20 %.
// - S1 强力击·γ型 (AUTO, attack SP 3; data DEFAULT): the next attack at atk_scale × ATK.
// - S2 虔修对决 (PASSIVE, 部署后, 18 / 21 s): from every deployment for the skill's duration: 苦痛专注 ×talent_scale, ATK +atk and
// every attack strikes twice (2连击 — both strikes land in the attack [ASSUMED: the skeleton's 0.267 s between them is not
// modelled]); then back to his single attack (lessng_s2[switch_mode]).
// - S3 苦修破誓 (MANUAL, 20 s; data DEFAULT): max HP +max_hp (MULTIPLIER), attacks on a blocked target (by anyone) at
// lessng_s3[atk_scale] × ATK, immune to every 异常状态 (晕眩, 寒冷, 冻结 … — tier6 ABNORMAL) while it runs. Cast while under a
// 异常状态: magical_value arts damage to himself first (source himself, may knock him out; 苦痛专注 and 痛楚砺刃 see it as any
// damage), then every 异常状态 is cleansed, then the effects apply (the HP buff after the damage). "可以无视异常状态开启": the
// basic strategy casts it when he is about to attack, and 沉默 does not hold it back; 晕眩 / 冻结 / 沉睡 stop his attack and so
// the cast [ASSUMED: the official 基础策略 waits for an attack too].
// Melee, ground-only (data canHitFly false: no anti-air); ground enemies target him normally (no 起飞 / 迷彩 / 隐匿).
import { num, talentBb, moduleBb, traitBb, skillRec, up } from '../shared/tier1.js';
import { ABNORMAL, hasAbnormal, cleanseAbnormal } from '../shared/tier6.js';
import { holdsUndying } from '../../items/battle.js';
const S1 = 'skchr_lessng_1';
const S2 = 'skchr_lessng_2';
const S3 = 'skchr_lessng_3';
/** S2 虔修对决's window after a deployment (2连击, ATK, 苦痛专注 ×talent_scale). */
export const DUEL_KEY = 'skill:lessng:duel';
/** S3 苦修破誓's effects while it runs (max HP; the status immunity and the blocked-target scale read it). */
export const OATH_KEY = 'skill:lessng:oath';
export const PAIN_KEY = 'talent:lessng:pain';
export const REBORN_KEY = 'trait:lessng:reborn';
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
/** He blocks at least one living enemy (苦痛专注 "阻挡时"). */
const blocking = (u) => u.blocking.some((e) => e && e.alive && e.blockedBy === u);
export default {
char_4011_lessng: (bb, chess) => {
const tb = traitBb(chess); // DRE-X: atk_scale; DRE-Y: max_hp / attack_speed / hp_ratio / value
const t0 = talentBb(chess, 0); // 苦痛专注 (DRE-Y stage 3: 0.4)
const t1 = talentBb(chess, 1); // 痛楚砺刃 (DRE-X stage 3: 0.2)
const hb = moduleBb(chess); // DRE-Y stage 3: def_penetrate
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s2 = skillRec(chess, S2);
const blockedScale = num(tb.atk_scale, 1);
const oathScale = num(b3['lessng_s3[atk_scale].atk_scale'], 1);
const duelOn = (u) => !!u.findBuff(DUEL_KEY);
const oathOn = (u) => !!u.findBuff(OATH_KEY);
return {
skills: {
[S1]: { kind: 'instant', attack: { atkScale: num(b1.atk_scale, 1) } },
[S2]: {
kind: 'passive',
onStart({ battle, unit }) { // every deployment (skills.js reset → passive start)
const dur = num(s2?.duration);
if (dur > 0) battle.addBuff(unit, { key: DUEL_KEY, duration: dur, mods: { atkPct: num(b2.atk) }, tags: ['skill'], visible: true });
},
},
[S3]: {
kind: 'duration',
onStart({ battle, unit }) {
if (hasAbnormal(unit)) {
battle.dealDamage(unit, unit, { amount: num(b3.magical_value), type: 'arts', canDodge: false, isSkill: true, tags: ['skill', 'lessngOath'] });
if (!unit.alive) return;
cleanseAbnormal(battle, unit);
}
battle.addBuff(unit, { key: OATH_KEY, mods: { hpPct: num(b3.max_hp) }, tags: ['skill'], visible: true });
},
onEnd({ battle, unit }) { battle.removeBuff(unit, OATH_KEY); },
},
},
trait: {
// S2's 2连击 (only while its window runs; 1 otherwise and under every other skill)
hitsFn: (b, u) => (duelOn(u) ? 2 : 1),
// DRE-X ×atk_scale and S3 ×lessng_s3[atk_scale] on a blocked target (CheckBlocked of the target: by anyone)
dmgMul: (b, u, t) => (t && t.blockedBy ? blockedScale * (oathOn(u) ? oathScale : 1) : 1),
},
talents: [
{ install(battle, unit) { // 苦痛专注
const cut = num(t0.damage_resistance), scale2 = num(b2.talent_scale, 1);
if (!(cut > 0)) return;
battle.on('hit', (c) => {
if (c.target !== unit || !up(unit) || !blocking(unit)) return;
const d = c.dmg;
if (!d || (d.type !== 'phys' && d.type !== 'arts')) return;
const src = c.source; // null = 无来源
if (src && src.side === 'enemy' && src.blockedBy === unit) return;
d.mul *= Math.max(0, 1 - cut * (duelOn(unit) ? scale2 : 1));
}, { owner: unit });
} },
{ install(battle, unit) { // 痛楚砺刃
const atk = num(t1.atk), dur = num(t1.add_atk_duration, 15);
if (!atk) return;
battle.on('hit', (c) => {
if (c.target !== unit || !up(unit)) return;
battle.addBuff(unit, { key: PAIN_KEY, duration: dur, refresh: 'replace', mods: { atkPct: atk }, tags: ['talent'], visible: true });
}, { owner: unit });
} },
],
install(battle, unit) {
// DRE-Y stage 3: 攻击自身阻挡的敌人时,无视目标12%的防御力 (every damage of his to an enemy he blocks)
const pen = num(hb.def_penetrate);
if (pen > 0) {
battle.on('hit', (c) => {
if (c.source === unit && c.target && c.target.side === 'enemy' && c.target.blockedBy === unit) c.dmg.defIgnorePct += pen;
}, { owner: unit });
}
// DRE-Y trait: 被击倒时不撤退且回复所有生命但生命上限-60%,攻击速度+30(单次部署只触发1次)
if (tb.hp_ratio != null && (tb.max_hp != null || tb.value != null)) {
const hpMul = tb.max_hp != null ? num(tb.max_hp, 0.4) : 1 - num(tb.value, 0.6);
let usedSeq = -1;
battle.on('fatal', (c) => {
if (c.unit !== unit || c.prevented || usedSeq === unit.deploySeq || holdsUndying(battle, unit)) return;
c.prevented = true;
usedSeq = unit.deploySeq;
battle.addBuff(unit, { key: REBORN_KEY, mods: { hpMul: Math.max(0.05, hpMul), aspd: num(tb.attack_speed) }, tags: ['trait'], visible: true });
unit.hp = Math.max(1, unit.s.maxHp * num(tb.hp_ratio, 1));
battle.fx('revive', { x: unit.x, y: unit.y, id: unit.id, module: true });
}, { owner: unit, priority: -60 });
}
// S3 苦修破誓: status immunity while it runs; "可以无视异常状态开启" — 沉默 does not hold the basic-strategy cast back
battle.on('beforeStatus', (c) => {
if (c.target === unit && oathOn(unit) && ABNORMAL.has(c.status)) c.cancel = true;
}, { owner: unit });
battle.on('beforeAttack', (c) => {
const sk = unit.skill;
if (c.attacker !== unit || !sk || sk.id !== S3 || !sk.ready || sk.active || sk.opCooling || !unit.s.flags.silence) return;
sk.activate('abnormal');
}, { owner: unit });
},
};
},
};
+168
View File
@@ -0,0 +1,168 @@
// server/sim/content/kits/ops/op-pallas.js — 帕拉斯 (char_485_pallas) 自选 operator kit: 6★ 教官 (近卫), an owned-6★ pick of
// the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules at every form.
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_485_pallas, the DIY slot statuses): normal = E2 Lv1, skills at rank 4, no module;
// elite = E2 Lv60, rank 7, the picked module at stage 1 (tier 5) or 3 (tier 6). Potential 0 [ASSUMED: no account].
// Sources: character_table / skill_table / battle_equip_table (zh_CN, as built into backups.json); PRTS 帕拉斯 (英勇的祝福 备注
// "持有增益的干员撤退时,增益立刻转交至帕拉斯;自身持有增益,满足条件的干员部署时,帕拉斯的增益立刻转交给对方"; 女神的振奋 备注
// "本天赋无视自身获得的禁疗,且必定进行治疗"); gamedata_const ba.strong 精力充沛 "生命值高于一定比例时获得一定属性加成(同类属性取
// 最高)"; the client's battle data — charpack char_485_pallas (英雄的诞生: an aura on every ally tagged `minos`; S3: an aura on
// her 1-1 for operators only, professionMask without TOKEN / TRAP), skchr_pallas_1 (`_additionalTimes` 1), buff_template_data
// `pallas_t_2` / `pallas_t_2[heal]`, `pallas_s_3[peak]`, `pallas_e_t_2[trigger]` and [uc]equips pallas_equip_2_x_p2.
// - Trait (教官) "可以攻击到较远敌人,攻击自身未阻挡的敌人时攻击力提升至120%": the profession profile (professions.js
// instructor: ×atk_scale on a target she does not block, her attacks only; range 2-2). Module INS-X “故乡的山岩” raises
// it to 130% (its traitOverride bb atk_scale, read by the profile through the composed record).
// - T1 英雄的诞生 "在场时,所有【米诺斯】干员生命值高于80%时获得+25%攻击力的精力充沛" (bb peak_performance.hp_ratio / .atk):
// while she is deployed, every allied operator of nation minos (her included — the aura has no self exclusion) gets ATK
// +atk while its HP ratio is above hp_ratio. INS-X stage 3: above 50 %, +30 % (the module's talent change). 精力充沛 of
// several sources is "同类属性取最高": one buff per ally carrying the highest ATK bonus whose HP condition holds (her T1
// and her S3 alike, several 帕拉斯 of a shared field too), re-evaluated every tick. 帕拉斯 is the only minos operator of
// the data (chess.json and backups.json), so in practice the talent is hers.
// - T2 女神的振奋 "每攻击一名敌人时为自身与身前一格的我方干员恢复40点生命值" (bb value): pallas_t_2 is ON_OUTPUT_DAMAGE — every
// damage instance she deals (S1's two strikes, each S3 target) heals every ally on her 1-1 (her tile and the tile in
// front, rotated with her) `value` HP, a heal that ignores 禁疗 (FixedValueHeal _ignoreHealFree; PRTS 备注) but not
// "不成为其他角色的治疗目标" (the heal's selector is HEAL-purpose). INS-Y “赫里亚之辉” stage 2+: value 60 and "并使所有【米诺斯】干员
// 额外恢复40点生命值" (talent bb pallas_e_t_2.value): pallas_e_t_2[trigger] (ON_OUTPUT_DAMAGE) uses pallas_e_heal on every
// ally its aura marks — tags minos, _selfOption INCLUDE, so herself too — for that much more (also ignoring 禁疗).
// - Module INS-Y "可以额外部署在远程位" (hidden module talent buildable_type 2) is a placement rule of the prep, not of the
// battle: by the owner's decision of 2026-10-04 (shared/highGround.js) only elite 歌蕾蒂娅 + HOK-Y may take a 高台, so a
// 帕拉斯 piece stays on the melee tiles — an open question for the owner (DESIGN draft 0.2.0.o9), nothing in this kit.
// - S1 胜利的连击 (AUTO, attack SP 3; data DEFAULT): the next attack strikes its target twice for atk_scale × ATK physical
// (skchr_pallas_1 `_additionalTimes` 1, `_triggerDelta` 0.1 s: the second strike 0.1 s after the first — both land in the
// attack here [ASSUMED: the 0.1 s is not modelled]); the instructor ×1.2 applies to each strike.
// - S2 信念的长鞭 (MANUAL, 20 / 22 s): attack range +ability_range_forward_extend tile forward (2-2 ⇒ 4 tiles: targeting
// rangeExtend, the card's live range follows), ATK +atk, every attack attack@buff_prob to stun its target attack@stun s.
// Its trigger is the data's DEFAULT: tools/build-data.mjs ATTACK_RANGE_CHANGE does not read "攻击范围向前延伸一格", so the
// owner's ACTIVE_RANGE rule (a running range that strictly contains the own one) does not reach it — reported, not
// worked around (the brief: report a trigger that looks wrong).
// - S3 英勇的祝福 (MANUAL, 30 s; data DEFAULT): ATK +atk, attacks hit attack@max_target (3) targets; the operator deployed
// on the tile in front of her, when that tile is low ground (pallas_s_3[peak] CheckHeightTypeOfRootTile LOWLAND: on any
// other tile the buff is finished on it), holds "生命值高于80%获得+35%攻击力的精力充沛、防御力+20%、阻挡数+1"
// (attack@peak_performance.hp_ratio / .atk, attack@def, attack@block_cnt) — else she holds it herself; the holder is
// re-read every tick, so it passes to her the moment the front operator leaves and back when one deploys there (PRTS
// 备注). Summons do not hold it (the aura's professionMask). The DEF / block part is per source (independentCharacterSource).
// Melee, ground-only (data canHitFly false: no anti-air); ground enemies target her normally (no 起飞 / 迷彩 / 隐匿).
import { num, talentBb, skillRec, up, alliesInGridOf } from '../shared/tier1.js';
import { frontOf } from '../../../dir.js';
const S1 = 'skchr_pallas_1';
const S2 = 'skchr_pallas_2';
const S3 = 'skchr_pallas_3';
/** The nation of 英雄的诞生 / pallas_e_heal (the client's `minos` tag = character_table nationId minos). */
const MINOS = 'minos';
/** T2's heal area: her 1-1 (pallas_t_2 `_rangeId` 1-1) — her tile and the tile in front. */
const RANGE_1_1 = Object.freeze([[0, 0], [0, 1]]);
/** The one 精力充沛 buff of an ally (ba.strong "同类属性取最高"). */
export const PEAK_KEY = 'talent:pallas:peak';
/** The S3 holder's DEF / block buff, per 帕拉斯 (independentCharacterSource). */
export const holderKey = (unit) => `skill:pallas:blessing#${unit.id}`;
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const isMinos = (a) => !!a && a.kind === 'op' && (a.def?.raw?.nationId ?? a.def?.nationId) === MINOS;
/**
* The 精力充沛 sources of a battle (a Set of `(ally) => ATK bonus`, 0 when one does not apply); the first registration
* installs the battle-wide tick that keeps, on every deployed ally, one buff with the highest bonus whose HP condition
* holds (never a sum: "同类属性取最高").
*/
const PEAKS = new WeakMap();
function peakSources(battle) {
let set = PEAKS.get(battle);
if (set) return set;
PEAKS.set(battle, (set = new Set()));
battle.on('tick', () => {
for (const a of battle.allyUnits) {
if (!a.alive || !a.deployed || a.kind === 'device') continue;
let v = 0;
for (const f of set) v = Math.max(v, f(a));
const cur = a.findBuff(PEAK_KEY);
if ((cur?.data?.v ?? 0) === v) continue;
if (v > 0) battle.addBuff(a, { key: PEAK_KEY, mods: { atkPct: v }, data: { v }, tags: ['talent'] });
else battle.removeBuff(a, PEAK_KEY);
}
});
return set;
}
/**
* The holder of 英勇的祝福's buff: the operator deployed on the tile in front of her when it stands on low ground (and her
* aura may select it), else herself.
*/
export function blessingHolder(battle, unit) {
const [fr, fc] = frontOf(unit.tileR, unit.tileC, unit.dir, 1);
const a = battle.grid.inRect(fr, fc) ? battle.unitAt(fr, fc) : null;
if (a && a !== unit && a.side === 'ally' && a.kind === 'op' && a.deployed && a.ground !== false && battle.grid.isLow(fr, fc)
&& battle.allySelectable(a, unit)) return a;
return unit;
}
export default {
char_485_pallas: (bb, chess) => {
const t0 = talentBb(chess, 0); // 英雄的诞生 (INS-X stage 3: hp_ratio 0.5, atk 0.3)
const t1 = talentBb(chess, 1); // 女神的振奋 (INS-Y stage 3: value 60, pallas_e_t_2.value 40)
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const blessDef = num(b3['attack@def']), blessBlock = num(b3['attack@block_cnt']);
const blessHp = num(b3['attack@peak_performance.hp_ratio'], 0.8), blessAtk = num(b3['attack@peak_performance.atk']);
/** Move 英勇的祝福 to the current holder (DEF / block here; its 精力充沛 goes through the shared tick). */
const bless = ({ battle, unit }) => {
const st = unit.mem.pallasBless ?? (unit.mem.pallasBless = { holder: null });
const h = unit.skill?.active && up(unit) ? blessingHolder(battle, unit) : null;
if (st.holder === h) return;
if (st.holder) battle.removeBuff(st.holder, holderKey(unit));
st.holder = h;
if (h) battle.addBuff(h, { key: holderKey(unit), source: unit, mods: { defPct: blessDef, blockCnt: blessBlock }, tags: ['skill'], visible: true });
};
return {
skills: {
[S1]: { kind: 'instant', attack: { atkScale: num(b1.atk_scale, 1), hits: 2 } },
[S2]: {
kind: 'duration',
mods: { atkPct: num(b2.atk) },
targeting: { rangeExtend: Math.max(0, Math.round(num(b2.ability_range_forward_extend))) },
attack: {
onHit({ battle, unit, target }) {
if (target && target.alive && target.side === 'enemy' && battle.rng.chance(num(b2['attack@buff_prob']))) {
battle.applyStatus(target, 'stun', { duration: num(b2['attack@stun']), source: unit });
}
},
},
},
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3.atk) },
targeting: { maxTargets: Math.max(1, num(b3['attack@max_target'], 3)) },
onStart: bless,
onTick: bless,
onEnd: bless,
},
},
talents: [
{ install(battle, unit) { // 英雄的诞生 (and S3's 精力充沛, same 同类取最高 buff)
const hp0 = num(t0['peak_performance.hp_ratio'], 0.8), atk0 = num(t0['peak_performance.atk']);
peakSources(battle).add((a) => {
if (!up(unit) || !battle.allySelectable(a, unit)) return 0;
let v = 0;
if (atk0 > 0 && isMinos(a) && a.hpRatio > hp0) v = atk0;
if (blessAtk > 0 && unit.mem.pallasBless?.holder === a && unit.skill?.active && a.hpRatio > blessHp) v = Math.max(v, blessAtk);
return v;
});
} },
{ install(battle, unit) { // 女神的振奋 (+ INS-Y stage 2+: the minos extra heal)
const value = num(t1.value), extra = num(t1['pallas_e_t_2.value']);
if (!(value > 0) && !(extra > 0)) return;
battle.on('damaged', (c) => {
if (c.source !== unit || !c.target || c.target.side !== 'enemy' || c.type === 'element' || !up(unit)) return;
if (value > 0) {
for (const a of alliesInGridOf(battle, unit, RANGE_1_1)) battle.heal(unit, a, value, { self: a === unit, ignoreHealFree: true, tags: ['talent'] });
}
if (extra > 0) {
for (const a of battle.alliesFor(unit)) if (isMinos(a)) battle.heal(unit, a, extra, { self: a === unit, ignoreHealFree: true, tags: ['talent', 'module'] });
}
}, { owner: unit });
} },
],
};
},
};