Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_6_17-nearl2.js
T
sganggs a0a7660287 feedback5 WM: merge feedback4 (896dd5d, the coming 0.1.4) — PRs #150 #66 #146 #74 #72 #103 #149 #109 #160 ported into the split layout
Ported by hand:
- #109 kit hunks → kits/ops (the tier files stay re-export shims): 宴 chess_char_1_18-utage.js, 野鬃
  chess_char_1_19-wildmn.js, 砾 chess_char_2_12-gravel.js, 斯卡蒂 chess_char_3_05-skadi.js, 伊内丝
  chess_char_4_04-ines.js, 缄默德克萨斯 chess_char_4_16-texas2.js, 耀骑士临光 chess_char_6_17-nearl2.js. The +/- lines
  are identical to #109's tier1/2/3/4/6 diffs.
- #109 generic.js against W5D's own-duration passives (一击即退): the deployment window is computed where #109 computes
  it, with W5D's case included. Both forms run #109's duration lifecycle (activateOnDeploy, spType 'none', the
  skill's own mods), and W5D's 冲锋号令 DP start effect is kept.
- #103 room.js through i18n phase 1: t('{name}邀请你加入卫戍协议:盟约【{difficulty}】') with the translated difficulty
  name (the Chinese text is the PR's), plus its English line in public/i18n/en.json.
- data/assets.json: feedback5's manifest plus #72's audio.bgm.combatAlts (after boss, the plan order). files +3 and
  bytes +5 037 595 as measured on feedback4; the hash is recomputed with the generator's contentHash. No other data/
  file changed.
- docs: DESIGN §5.6 (#109's timed deployment paragraph before feedback5's trigger rules; "Permanent passive skills are
  always on"), SIM §7.4 (both generic passive cases).
- Auto-merged and checked: #149 bond-bar fold (still in screens/game.js), #160 skills.js / spine.js, #109 skills.js /
  snapshot.js, #66 addon/battle.js (restTile / isDown live in sim/battle/tiles.js), #150 units.js, #72 audio.js /
  plan.mjs, #74 plan.mjs / docs.

Where the PRs met feedback5 work:
- 突袭 (W5L, GitHub #49): raidPoll counts a skill that needs no SP while it is on. That is a passive, as before, or a
  #109 deploy-timed skill while its window runs. Once the window has ended only the idle trigger is left. Without this
  change 缄默德克萨斯 / 宴 lost #49. research 02 §3.18 says so.
- Tests changed to #109's representation: bonds_addon #49 test (kind 'duration', activateOnDeploy) plus a new test
  (window over → idle trigger only), and standin.test.js's 预备干员 PASSIVE branch (the skill's mods instead of the
  generic:passive buff).

Golden regenerated on the merged code. Attribution: each PR's gameplay change was reverted alone in a scratch copy, and
all three together. With all three reverted the digests equal feedback5's exactly. Every moved scenario was replayed
and fields the unit that explains it.
- #160 only (0.5 s skill events of deploy passives; events.skill only): roster-001/006/025/049, bond-arcaneShip-low,
  bond-emptyShip-high, bond-investShip-high, bond-kjeragShip-low, bond-siracusaShip-low/high, bond-victoriaShip-low,
  bond-yanShip-high, boss-boss_2/4/6-solo, hidden-boss_8/10-solo, standin-03/05/08/10, diy-005/006 (普罗旺斯 S1,
  琳琅诗怀雅 S1/S2, 星熊 S2, 凯瑟琳 S1, Misery S3 as stand-in / 自选 prototype).
- #109 only (deploy-timed skill state and lifecycle; 突袭 技能就绪 limited to the window; generic 一击即退):
  roster-003/004/005/021/023/024/039/041/042/046/047, bond-egirShip-high, bond-indomShip-high,
  bond-kazimierzShip-low/high, hidden-boss_8-pair, standin-01, coop4-FUNNY-6, coop2-NORMAL-8-serverrun.
- #66 only (灵巧's aura stays only for a killed member, from its body tile): roster-028, coop2-NORMAL-3,
  coop3-ABYSS-5, coop4-ABYSS-7, solo-HARD-9-boosted.
- #109 + #160: roster-002, roster-029, bond-investShip-low, bond-raidShip-low/high, bond-suntShip-low,
  bond-visiShip-high.
- #66 + #160: roster-008, roster-027.
- #66 + #109: roster-022, roster-040, bond-indomShip-low, boss-boss_2/4/6-pair, hidden-boss_10-pair, standin-06,
  coop2-HARD-4, coop2-NORMAL-14-standins.
- all three: roster-010, roster-012, roster-031.
69 scenarios. Every other digest is unchanged.
2026-10-05 23:59:05 +08:00

151 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/sim/content/kits/ops/chess_char_6_17-nearl2.js — 耀骑士临光 (char_1014_nearl2) kit, tier 6.
// Conventions of the tier-6 kits: ../shared/tier6.js; kit contract and rules: ../README.md.
import { absoluteRangeKeys } from '../../../targeting.js';
import { bodyInKeys } from '../../../body.js';
import {
num, tbb, live, hasBond, onDefaultSkill, selectedSkill, N4, bstate, freeTiles, bestTile,
} from '../shared/tier6.js';
/** Deploy-order tracker: last op deployed per owner (read before the update by priority-0 deploy handlers). */
function ensureDeployTracker(battle) {
const S = bstate(battle);
if (S.lastOp) return S.lastOp;
S.lastOp = new Map();
battle.on('deploy', ({ unit }) => { if (unit.kind === 'op') S.lastOp.set(unit.ownerId, unit); }, { priority: -100 });
return S.lastOp;
}
// ------------------------------------------------------------------------------------------------------------------
// 耀骑士临光 chess_char_6_17 (无畏者) — S3 耀阳颔首; 不畏苦暗; 破晓; module 耀阳锋刃
function nearl2(bb, chess, def) {
const t0 = tbb(def, 0), t1 = tbb(def, 1), tb = def?.traitBb || {};
const t0Grid = def?.talents?.[0]?.rangeGrid?.length ? def.talents[0].rangeGrid : N4;
const skillGrid = def?.skill?.rangeGrid?.length ? def.skill.rangeGrid : null;
const tokId = (chess?.tokens || []).find((t) => /nearl2_sword/.test(String(t))) || 'token_10019_nearl2_sword';
const sunBurst = (battle, unit, r, c, scale, stun) => {
const keys = new Set(absoluteRangeKeys(N4, r, c, 1, 0));
battle.fx('sunBurst', { x: c, y: r, id: unit.id });
for (const e of battle.enemies) {
if (!e.alive || e.hidden || !bodyInKeys(e, keys)) continue;
battle.dealDamage(unit, e, { amount: unit.s.atk * scale, type: 'true', isSkill: true, tags: ['skill'] });
if (stun > 0 && e.alive) battle.applyStatus(e, 'stun', { duration: stun, source: unit });
}
};
const isDef = onDefaultSkill(chess), sid = selectedSkill(chess, def);
const lastOps = { map: null };
const skills = {
// S1 灿焰长刃 (toggle, 持续时间无限): skill range, ATK / ASPD +
skchr_nearl2_1: {
kind: 'toggle',
mods: { atkPct: num(bb.atk), aspd: num(bb.attack_speed) },
...(skillGrid ? { targeting: { rangeGrid: skillGrid } } : {}),
},
// S2 逐夜烁光 (passive, ON_DEPLOY): for the skill duration after each deployment ATK +atk and `times` 护盾 layers
// (whole hits negated); then she withdraws and this redeploy time is ×respawn_time (×1 when the operator deployed
// right before her is 【卡西米尔】)
skchr_nearl2_2: {
kind: 'duration', activateOnDeploy: true, duration: num(def?.skill?.duration, 22), spCost: 0, spType: 'none', trigger: 'NEVER',
mods: { atkPct: num(bb.atk) },
onStart({ battle, unit, skill }) {
const dur = num(skill.duration, num(def?.skill?.duration, 22));
if (!(dur > 0)) return;
const prev = lastOps.map?.get(unit.ownerId);
unit.mem.nearl2Combo = !!prev && prev !== unit && hasBond(prev, 'kazimierzShip');
const hits = Math.floor(num(bb.times));
if (hits > 0) battle.addBuff(unit, { key: 'nearl2:shield', shieldHits: hits, duration: dur, visible: true });
},
onEnd({ battle, unit, reason }) {
battle.removeBuff(unit, 'nearl2:shield');
if (reason !== 'duration' || !live(unit)) return;
const combo = unit.mem.nearl2Combo;
battle.retreat(unit, { reason: 'retreat' });
const mul = combo ? num(bb['nearl2_s_2[withdraw][combo].respawn_time'], 1) : num(bb.respawn_time, 1);
if (Number.isFinite(unit.respawnAt) && mul !== 1) unit.respawnAt = battle.time + (unit.respawnAt - battle.time) * mul;
battle.fx('disappear', { x: unit.x, y: unit.y, id: unit.id, combo });
},
},
};
// module “骑士家族”: "被击倒时不撤退且回复所有生命但生命上限-60%,攻击速度+30(单次部署只触发1次)" and 不畏苦暗
// "部署时及首次被击倒时…" (the burst again)
const standVal = num(tb.value), standAs = num(tb.attack_speed), standHp = num(tb.hp_ratio, 1);
return {
skills,
install(battle, unit) {
if (sid === 'skchr_nearl2_2') lastOps.map = ensureDeployTracker(battle);
if (standVal > 0) {
battle.on('deploy', ({ unit: u }) => { if (u === unit) unit.mem.nearlStood = false; }, { owner: unit });
battle.on('fatal', (ctx) => {
if (ctx.unit !== unit || ctx.prevented || unit.mem.nearlStood) return;
ctx.prevented = true;
unit.mem.nearlStood = true;
battle.addBuff(unit, { key: 'nearl2:stand', mods: { hpMul: Math.max(0.05, 1 - standVal), aspd: standAs }, visible: true });
unit.hp = Math.max(1, unit.s.maxHp * standHp);
battle.fx('undying', { x: unit.x, y: unit.y, id: unit.id });
battle.emit('nearl2:knockdown', { unit });
}, { owner: unit, priority: -60 });
}
},
trait: num(tb.atk_scale, 1) > 1 ? { dmgMul: (b, u, t) => (t.blockedBy ? num(tb.atk_scale, 1) : 1) } : null,
skill: {
kind: 'duration',
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
...(skillGrid ? { targeting: { rangeGrid: skillGrid } } : {}),
onStart({ battle, unit, skill }) {
const tile = bestTile(battle, freeTiles(battle, unit, N4));
const sword = tile ? battle.spawnToken(unit, tokId, tile[0], tile[1], { duration: skill.timeLeft }) : null;
unit.mem.sun = sword;
if (sword) battle.fx('summon', { x: sword.x, y: sword.y, id: sword.id, src: unit.id });
// the token kit (content/tokens.js) performs the appear burst; without it (or without a free tile) do it here
if (!sword || !sword.kit?.fromTokens) {
sunBurst(battle, unit, sword ? sword.tileR : unit.tileR, sword ? sword.tileC : unit.tileC, num(bb.value), num(bb.value2));
}
},
onEnd({ battle, unit }) {
const s = unit.mem.sun;
unit.mem.sun = null;
if (s && s.alive) battle.retreat(s, { reason: 'expired', permanent: true });
},
},
talents: [
{ install(battle, unit) { // S3: attacks on units blocked by her or 耀阳 deal true damage
if (!isDef) return;
battle.on('hit', (ctx) => {
if (ctx.source !== unit || !unit.skill?.active || !ctx.dmg.isAttack) return;
const b = ctx.target.blockedBy;
if (b && (b === unit || b === unit.mem.sun)) ctx.dmg.type = 'true';
}, { owner: unit });
} },
{ install(battle, unit) { // 不畏苦暗 — the 4 tiles around her, air units too [ASSUMED: no 对空 note on PRTS, like “耀阳”]
const last = ensureDeployTracker(battle);
const sc = num(t0.atk_scale), stun = num(t0.stun);
const dawn = (times) => {
const foes = battle.unitsInGrid(unit, t0Grid, { side: 'enemy' });
if (foes.length) battle.fx('sunBurst', { x: unit.x, y: unit.y, id: unit.id });
for (const e of foes) {
for (let i = 0; i < times && e.alive; i++) battle.dealDamage(unit, e, { amount: unit.s.atk * sc, type: 'true', tags: ['talent'] });
if (stun > 0 && e.alive) battle.applyStatus(e, 'stun', { duration: stun, source: unit });
}
};
const kazTimes = () => { const prev = last.get(unit.ownerId); return prev && prev !== unit && hasBond(prev, 'kazimierzShip') ? 2 : 1; };
battle.on('deploy', ({ unit: u }) => {
if (u !== unit || !(sc > 0)) return;
unit.mem.nearlDawnTimes = kazTimes();
dawn(unit.mem.nearlDawnTimes);
}, { owner: unit });
// module “骑士家族”: "…及首次被击倒时" — the burst again when the trait keeps her standing
battle.on('nearl2:knockdown', ({ unit: u }) => { if (u === unit && sc > 0) dawn(unit.mem.nearlDawnTimes ?? 1); }, { owner: unit });
} },
{ install(battle, unit) { // 破晓
const p = num(t1.def_penetrate);
if (p) battle.addBuff(unit, { key: 'nearl2:dawn', mods: { defIgnorePct: p }, persist: true, allowDead: true });
} },
],
};
}
export default {
chess_char_6_17_a: nearl2,
};