feedback5 O3: 自选 operator kits for 逻各斯, 真言 and 维伊 (every skill, talent, trait and module at stages 1 / 3), the engine hook palsyTrigger

- kits/ops/op-logos.js (中坚术师; CCR-Δ, CCR-Y), op-mantra.js (本源术师; PRI-X), op-veen.js (秘术师; MSC-Y), registered in
  OPERATOR_KIT_FILES one line per file; numbers from data/backups.json, mechanics from the PRTS 备注 (cited in each header),
  [ASSUMED] items marked there. Data triggers kept; 维伊 S1 (an AUTO self buff) fires at full SP (the owner's rule).
- server/sim/ai.js: enemyAttack emits `palsyTrigger { enemy, buff, keep }` when a 麻痹 stack interrupts an attack (PRTS 真言
  备注 "触发麻痹时指的是敌人的攻击被麻痹打断时"); `keep` leaves the stack (真言 噤声限域). Additive: no listener, no change.
  docs/SIM.md §3 / §5 rows; test/sim/palsy-trigger.test.js.
- Tests: test/content/op_logos.test.js, op_mantra.test.js, op_veen.test.js (every form, skill, talent, module, range,
  trigger, targeting, damage type).
- Golden: only the diy family moved — diy-003 … diy-007 now field 逻各斯 / 真言 / 维伊 in every form × module × skill (60
  configurations; diy-003 … 006 held the prototypes before), the prototype picks moved to diy-008 … 011 (same packing, new
  ids); diy-001 / 002 (推进之王) and roster, bonds, fields, matches, standins byte-identical.
This commit is contained in:
sganggs committed 2026-10-06 00:04:18 +08:00
1 parent 742f478526
commit ba66c2c32e
11 files changed
+2246 -104

No files matched your search

+279
View File
@@ -0,0 +1,279 @@
// server/sim/content/kits/ops/op-logos.js — 逻各斯 (char_4133_logos) 自选 operator kit: 6★ 中坚术师 (术师), an owned-6★ pick
// of the tier-5 and tier-6 自选 slots; every skill, both talents, the trait and both modules (CCR-Δ, CCR-Y) at every form.
// Kit contract and the 自选 rules: ../README.md ("How to add an operator (自选)").
//
// Forms (data/backups.json units.char_4133_logos, 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) and PRTS 逻各斯 (the
// 备注 quoted below). Every number is read from the record (`chess` / the skill blackboards); the few constants below exist
// only in the PRTS 备注.
// - Trait (中坚术师) "攻击造成法术伤害": the profession default (ranged arts bolt, hits air units, targetable by ground
// enemies). CCR-Δ “来自河谷的笔盒” adds "造成法术伤害时附带相当于8%伤害的凋亡损伤" (hidden module talent
// `ep_damage_ratio`): every arts damage he deals — attacks, the talent's extra hits, 殁亡's follow-up — fills 8 % of the
// HP it removed as 凋亡 损伤 (the kits' element convention, docs/SIM.md §7.2). CCR-Y “《语义范式百科》” adds
// "普通攻击命中精英或领袖敌人时获得1点技力" (trait bb `sp`): +sp SP per normal-attack hit on an elite or leader enemy
// (none while a timed skill runs — AK) and, per the 语汇演化 备注 "<Y模组>…可触发模组提供的额外特性", per talent hit too.
// - T1 语汇演化 "对一个目标发起攻击时,有40%几率额外对攻击范围内一个随机目标造成相当于攻击力60%的法术伤害并使其停顿0.8秒"
// (bb prob / atk_scale / sluggish): rolled for every target of each of his attacks and after 殁亡's follow-up hit (S1 备注
// "可触发第一天赋"); the extra hit lands at once on a random targetable enemy of his current range [ASSUMED: the attacked
// target can be drawn], then 停顿 (the engine `sluggish`) for `sluggish` s. 备注 "该天赋的触发不依赖普通攻击,不受缴械类效果
// 制约". CCR-Δ stage 2+: prob 0.5 / 0.6 and, on a target in its 凋亡 burst, `element_atk_scale` × ATK 元素伤害 after the arts
// hit (备注 "先造成法术伤害,后造成元素伤害"). CCR-Y stage 2+: `emit_count` 2 — 备注 "本质上是将该天赋重复执行2次": two
// independent executions (each its own roll and random target).
// - T2 剜魂具辞 "攻击使目标在5秒内法术抗性-10且受到的法术伤害提高150点" (bb duration / magic_resistance / atk_addition):
// each of his attack hits gives the target, for `duration` s, RES `magic_resistance` and +`atk_addition` on every arts
// damage it takes (备注 "在计算攻击倍率后、计算法术抗性前增加当次伤害", "可对当次伤害生效", "即使法术伤害的攻击力为0也可
// 生效"): added to the hit's pre-mitigation amount, the hit that applies it included. One instance per enemy whoever
// applies it (同名效果: applyStrongest).
// - S1 殁亡 (AUTO, 持续时间无限 ⇒ toggle; the data's DEFAULT trigger — it acts on enemies): range 3-3, ATK +atk, and an aura
// (备注 "光环效果,不依赖攻击行为"): every enemy of his range whose HP is below attack@kill_atk_scale × his ATK takes
// attack@kill_damage (9999999) 无来源 true damage (备注 "无来源真实斩杀伤害"; no dodge), then another random enemy of his
// range takes arts damage equal to the HP the first had left (备注: his T2 bonus applies, T1 rolls). At most one try per
// enemy per entry into the range, and none ever again on an enemy that survived one (备注; a 频次 unit takes 1 HP, a
// leader in a boss battle loses nothing to 限伤 — 备注 "卫戍协议的最终攻势Boss…只能以常规的“当前生命值”处理斩杀的触发").
// - S2 提喻 (MANUAL, 20 s, data DEFAULT): RES +magic_resistance; the attack locks one target (an instant hit, no bolt
// [ASSUMED: a lock-on beam]) every attack@cooldown s whatever the attack speed (备注 "攻击间隔固定为0.5s"), each hit
// attack@atk_scale_base + attack@atk_scale_delta × n of his ATK and the target's movement speed × (1 + attack@move_speed
// × n), n = the hits made on it so far, at most attack@max_stack_cnt (备注 "线性提升" — 3× and 40 % after 5 s). A stun /
// freeze, the target's fall or no attack for 0.1 s past the cadence (备注 "每0.1s,若自身未处于攻击状态,则退出锁定状态")
// resets the lock and its slow; the next attack locks anew.
// - S3 延异视阈 (MANUAL, 30 s, data ACTIVE_RANGE on its 3-4): ATK +atk, attack@max_target targets, and every enemy
// projectile whose position is on his range flies at projectile_move_scale × its speed (备注 "降低至0.05倍"; leaving the
// range restores it) and is removed when the skill ends — not when he is disarmed then (备注 "受缴械类效果制约") nor when
// he is knocked out [ASSUMED]. Every enemy projectile counts as 可变速 / 可消除 [ASSUMED]. The sim's projectiles live in
// `battle.projectiles.list` (ProjectileSystem, server/sim/projectiles.js): the kit adjusts `speed` / `maxAge` there.
import { num, talentBb, moduleBb, traitBb, skillRec, up, giveSp, once } from '../shared/tier1.js';
import { isElite, elementDmg } from '../shared/tier6.js';
import { hasHp } from '../../../damage.js';
import { COLS, ROWS } from '../../../constants.js';
const S1 = 'skchr_logos_1';
const S2 = 'skchr_logos_2';
const S3 = 'skchr_logos_3';
const ANY = Object.freeze({ canHitFly: true });
/** 剜魂具辞: the debuff on an enemy (one instance whoever applies it). */
const T2_KEY = 'logos:soulRend';
/** 提喻 备注 "每0.1s,若自身未处于攻击状态,则退出锁定状态": the lock ends 0.1 s after a missed attack. */
const LOCK_IDLE = 0.1;
/** Enemy projectiles slowed by a running 延异视阈 (projectile → { orig speed, by: Set of unit ids }); per projectile. */
const SLOWED = new WeakMap();
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const tileOf = (p) => {
const r = Math.round(p.y), c = Math.round(p.x);
return r >= 0 && r < ROWS && c >= 0 && c < COLS ? r * COLS + c : -1;
};
/** Enemy projectiles on the field (ProjectileSystem list: the sim's only projectile store). */
const enemyShots = (battle) => (battle.projectiles?.list ?? []).filter((p) => p && p.source && p.source.side === 'enemy');
/** 延异视阈: slow the enemy projectiles on `unit`'s range, restore those that left it. */
function slowShots(battle, unit, scale) {
const set = unit.rangeKeySet;
for (const p of enemyShots(battle)) {
let s = SLOWED.get(p);
if (set && set.has(tileOf(p))) {
if (!s) { s = { orig: p.speed, by: new Set() }; SLOWED.set(p, s); }
s.by.add(unit.id);
p.speed = s.orig * scale;
p.maxAge = Math.max(p.maxAge, p.age + 1); // a slowed shot never lands by age while it is held
} else if (s && s.by.delete(unit.id) && !s.by.size) {
p.speed = s.orig;
SLOWED.delete(p);
}
}
}
/** 延异视阈 ends: clear the enemy projectiles on the range (unless disarmed / knocked out), release the rest. */
function endShots(battle, unit, clear) {
const set = unit.rangeKeySet;
let n = 0;
if (clear && set && battle.projectiles) {
const keep = [];
for (const p of battle.projectiles.list) {
if (p && p.source && p.source.side === 'enemy' && set.has(tileOf(p))) { n++; SLOWED.delete(p); } else keep.push(p);
}
battle.projectiles.list = keep;
}
for (const p of enemyShots(battle)) {
const s = SLOWED.get(p);
if (s && s.by.delete(unit.id) && !s.by.size) { p.speed = s.orig; SLOWED.delete(p); }
}
if (n) battle.fx('bulletClear', { x: unit.x, y: unit.y, id: unit.id, n });
}
export default {
char_4133_logos: (bb, chess) => {
const t0 = talentBb(chess, 0);
const t1 = talentBb(chess, 1);
const hidden = moduleBb(chess); // CCR-Δ: ep_damage_ratio
const tb = traitBb(chess); // CCR-Y: sp
const b1 = bbOf(chess, S1), b2 = bbOf(chess, S2), b3 = bbOf(chess, S3);
const s1 = skillRec(chess, S1), s3 = skillRec(chess, S3);
const lex = {
prob: num(t0.prob), scale: num(t0.atk_scale), sluggish: num(t0.sluggish),
elemScale: num(t0.element_atk_scale), emit: Math.max(1, Math.floor(num(t0.emit_count, 1))),
};
/** 语汇演化: `emit` executions, each a `prob` roll for a random enemy of his range. */
function lexicon(battle, unit) {
if (!(lex.prob > 0) || !(lex.scale > 0)) return;
for (let k = 0; k < lex.emit; k++) {
if (!up(unit) || !battle.rng.chance(lex.prob)) continue;
const t = battle.rng.pick(battle.enemiesInKeys(unit.rangeKeys, unit, ANY));
if (!t) continue;
battle.fx('logosLexicon', { x: t.x, y: t.y, id: unit.id, target: t.id });
battle.dealDamage(unit, t, { amount: unit.s.atk * lex.scale, type: 'arts', tags: ['talent', 'logosLexicon'] });
if (t.alive && lex.sluggish > 0) battle.applyStatus(t, 'sluggish', { duration: lex.sluggish, source: unit });
if (lex.elemScale > 0 && hasHp(t) && t.findBuff('apoptosisBurst')) {
battle.dealDamage(unit, t, { amount: unit.s.atk * lex.elemScale, type: 'elemental', element: 'apoptosis', canDodge: false, tags: ['talent', 'logosLexicon'] });
}
}
}
/** 殁亡's execution of `e` (HP below the threshold), then the follow-up arts hit on another random enemy of his range. */
function execute(battle, unit, e, st) {
const left = e.hp;
battle.fx('logosExecute', { x: e.x, y: e.y, id: unit.id, target: e.id });
battle.dealDamage(unit, e, { amount: num(b1['attack@kill_damage'], 9999999), type: 'true', canDodge: false, sourceless: true, isSkill: true, tags: ['skill', 'execute'] });
if (e.alive) st.spared.add(e.id); // survived (不死, 频次, 限伤 …): never tried again
const t = battle.rng.pick(battle.enemiesInKeys(unit.rangeKeys, unit, ANY).filter((x) => x !== e));
if (!t || !(left > 0)) return;
battle.dealDamage(unit, t, { amount: left, type: 'arts', isSkill: true, tags: ['skill', 'logosPerish'] });
lexicon(battle, unit);
}
/** 提喻: end the lock (its slow leaves the target). */
function unlock(battle, unit) {
const L = unit.mem.logosLock;
if (L && L.target) battle.removeBuff(L.target, `logos:lockSlow:${unit.id}`);
unit.mem.logosLock = null;
}
const lockIv = num(b2['attack@cooldown'], 0.5);
const base2 = num(b2['attack@atk_scale_base'], 1), delta2 = num(b2['attack@atk_scale_delta']);
const maxN = Math.max(0, Math.floor(num(b2['attack@max_stack_cnt'])));
const s2On = (unit) => !!(unit.skill && unit.skill.active && unit.skill.id === S2);
return {
skills: {
[S1]: {
kind: 'toggle',
mods: { atkPct: num(b1.atk) },
targeting: { rangeGrid: s1?.rangeGrid ?? null },
onStart({ unit }) { unit.mem.logosExec = { tried: new Set(), spared: unit.mem.logosExec?.spared ?? new Set() }; },
onTick({ battle, unit }) {
const st = unit.mem.logosExec;
if (!st || !up(unit)) return;
const inRange = battle.enemiesInKeys(unit.rangeKeys, unit, ANY);
const ids = new Set(inRange.map((e) => e.id));
for (const id of st.tried) if (!ids.has(id)) st.tried.delete(id); // left the range: a new entry may try again
const thr = num(b1['attack@kill_atk_scale']) * unit.s.atk;
for (const e of inRange) {
if (st.tried.has(e.id) || st.spared.has(e.id) || !e.alive || !hasHp(e) || !(e.hp < thr)) continue;
st.tried.add(e.id);
execute(battle, unit, e, st);
if (!up(unit)) return;
}
},
},
[S2]: {
kind: 'duration',
mods: { resFlat: num(b2.magic_resistance) },
attack: {
projectile: 'beam',
atkScale: base2,
// the hit's scale: base + delta × n (n = the hits already made on the locked target)
dmgMul: (battle, unit, target) => {
const L = unit.mem.logosLock;
const n = L && L.target === target ? L.n : 0;
return base2 > 0 ? (base2 + delta2 * n) / base2 : 1;
},
onEachHit({ battle, unit, target, kind }) {
const L = unit.mem.logosLock;
if (kind !== 'main' || !L || L.target !== target || !target.alive) return;
L.n = Math.min(maxN, L.n + 1);
const mul = Math.max(0, 1 + num(b2['attack@move_speed']) * L.n);
battle.addBuff(target, { key: `logos:lockSlow:${unit.id}`, duration: lockIv + LOCK_IDLE + 0.2, source: unit, mods: { moveMul: mul } });
},
},
onStart({ battle, unit }) { unlock(battle, unit); unit.mem.logosLockAt = null; },
onAttack({ battle, unit }) { unit.mem.logosLockAt = battle.time; },
onTick({ battle, unit }) {
const L = unit.mem.logosLock;
// a stun / freeze breaks the lock; so does no attack for LOCK_IDLE past the cadence
if (L && (!unit.canAct || !L.target.alive || battle.time - unit.mem.logosLockAt > lockIv + LOCK_IDLE + 1e-9)) unlock(battle, unit);
if (!unit.canAct) return;
// 攻击间隔固定为0.5s: the next attack exactly lockIv after the last one (the engine counts atkCd down right after)
const at = unit.mem.logosLockAt;
if (at == null) { unit.atkCd = Math.min(unit.atkCd, lockIv); return; }
const left = at + lockIv - battle.time;
unit.atkCd = left > 1e-6 ? left + battle.dt : 0;
},
onEnd({ battle, unit }) { unlock(battle, unit); },
},
[S3]: {
kind: 'duration',
mods: { atkPct: num(b3.atk) },
targeting: { rangeGrid: s3?.rangeGrid ?? null, maxTargets: num(b3['attack@max_target'], 1) },
onTick({ battle, unit }) { slowShots(battle, unit, num(b3.projectile_move_scale, 1)); },
onEnd({ battle, unit, reason }) { endShots(battle, unit, reason !== 'death' && !unit.s.flags.disarm); },
},
},
talents: [
{ install(battle, unit) { // 语汇演化: every target of each of his attacks
battle.on('attack', (ctx) => {
if (ctx.attacker !== unit || !up(unit)) return;
for (let i = 0; i < ctx.targets.length; i++) lexicon(battle, unit);
}, { owner: unit });
} },
{ install(battle, unit) { // 剜魂具辞: his attack hits mark the target (before mitigation: the hit itself counts)
const dur = num(t1.duration), res = num(t1.magic_resistance), add = num(t1.atk_addition);
if (!(dur > 0) || !(add > 0 || res)) return;
battle.on('hit', (ctx) => {
if (ctx.source !== unit || !ctx.dmg?.isAttack || !ctx.target || ctx.target.side !== 'enemy') return;
battle.applyStrongest(ctx.target, T2_KEY, { duration: dur, value: add, source: unit, mods: () => (res ? { resFlat: res } : {}) });
}, { owner: unit, priority: 10 });
// "受到的法术伤害提高N点": every arts damage on a marked enemy, after the attack multiplier, before its RES
once(battle, 'logos:artsAddition', () => battle.on('hit', (ctx) => {
const d = ctx.dmg;
if (!d || d.type !== 'arts' || !ctx.target || ctx.target.side !== 'enemy') return;
const m = ctx.target.findBuff(T2_KEY);
if (m && m.data && m.data.value > 0) d.amount += m.data.value;
}, { priority: -50 }));
} },
],
install(battle, unit) {
// 提喻: keep attacking the locked target while it stays a target; a new lock otherwise
battle.on('beforeAttack', (ctx) => {
if (ctx.attacker !== unit || !s2On(unit) || !ctx.targets.length) return;
const L = unit.mem.logosLock;
const prof = ctx.profile || unit.profile;
if (L && L.target.alive) {
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, prof);
if (cands.includes(L.target) || battle.blockedTargets(unit, prof).includes(L.target)) { ctx.targets = [L.target]; return; }
}
unlock(battle, unit);
unit.mem.logosLock = { target: ctx.targets[0], n: 0 };
ctx.targets = [ctx.targets[0]];
}, { owner: unit });
// CCR-Δ: 8 % of every arts damage as 凋亡 损伤
const ep = num(hidden.ep_damage_ratio);
if (ep > 0) {
battle.on('damaged', (ctx) => {
if (ctx.source !== unit || ctx.type !== 'arts' || !(ctx.amount > 0) || !ctx.target || ctx.target.side !== 'enemy') return;
elementDmg(battle, unit, ctx.target, 'apoptosis', ctx.amount * ep, ['trait', 'logosCcrd']);
}, { owner: unit });
}
// CCR-Y: a normal-attack (or 语汇演化) hit on an elite / leader enemy gives SP
const sp = num(tb.sp);
if (sp > 0) {
battle.on('damaged', (ctx) => {
const d = ctx.dmg;
if (ctx.source !== unit || !d || ctx.type === 'element' || !ctx.target || ctx.target.side !== 'enemy') return;
if ((d.isAttack || (d.tags || []).includes('logosLexicon')) && isElite(ctx.target)) giveSp(unit, sp, 'trait');
}, { owner: unit });
}
},
};
},
};