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.
151 lines
8.0 KiB
JavaScript
151 lines
8.0 KiB
JavaScript
// 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,
|
||
};
|