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.
143 lines
8.5 KiB
JavaScript
143 lines
8.5 KiB
JavaScript
// server/sim/content/kits/ops/chess_char_4_16-texas2.js — 缄默德克萨斯 (char_1028_texas2) kit, tier 4.
|
||
// Conventions of the tier-4 kits: ../shared/tier4.js; kit contract and rules: ../README.md.
|
||
|
||
import { sortEnemyTargets } from '../../../targeting.js';
|
||
import {
|
||
AURA, num, tbb, grid, targetsInGrid, whileDeployed, toggleBuff, resCut, isSel, alt, lonely, withDefaults,
|
||
} from '../shared/tier4.js';
|
||
|
||
export default withDefaults({
|
||
// ===== 缄默德克萨斯 (executor) S3 剑雨滂沱 (passive) — deploy burst 2×115 % + 1.5 s stun, sword rain 1/s; talents
|
||
// S1 细雨无声 (passive, for the skill duration: ATK up; hits silence the target 5 s/8 s — 失去特殊能力 — with
|
||
// 260/320 arts per s meanwhile); S2 阵雨连绵 (passive: deploy burst 150 %/180 % arts + RES −15 %/−20 % around her;
|
||
// for the skill duration ATK up and attacks become arts double hits). Talent 德克萨斯传统 applies to every passive.
|
||
// The S3 rain hits air units (PRTS 备注 "效果可对空"); so does the S2 deploy burst [ASSUMED: no note].
|
||
chess_char_4_16_a: (bb, chess, def) => {
|
||
const t0 = tbb(def, 0), t1 = tbb(def, 1);
|
||
const tb = def.traitBb || {};
|
||
const g = grid(def.skill?.rangeGrid) || [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 0], [0, 1], [1, -1], [1, 0], [1, 1]];
|
||
const S1 = isSel(def, 'skchr_texas2_1'), S2 = isSel(def, 'skchr_texas2_2');
|
||
const dur = num(def.skill?.duration, S1 ? 11 : S2 ? 8 : 6);
|
||
const castS1 = (battle, unit) => {
|
||
if (!unit.alive || !unit.deployed) return;
|
||
battle.fx('swordRain', { x: unit.x, y: unit.y, id: unit.id });
|
||
};
|
||
const castS2 = (battle, unit) => {
|
||
if (!unit.alive || !unit.deployed) return;
|
||
const mr = num(bb.magic_resistance, 0);
|
||
for (const e of targetsInGrid(battle, unit, g)) {
|
||
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1.5), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
|
||
// 同名效果取最高: one RES cut per enemy (the strongest), never one per 缄默德克萨斯
|
||
if (e.alive && mr) battle.applyStrongest(e, 'texas2:resDown', { duration: num(bb.debuff_duration, 8), value: mr, mods: resCut, source: unit });
|
||
}
|
||
battle.fx('swordStorm', { x: unit.x, y: unit.y, id: unit.id });
|
||
};
|
||
const castS3 = (battle, unit) => {
|
||
if (!unit.alive || !unit.deployed) return;
|
||
for (const e of targetsInGrid(battle, unit, g)) {
|
||
for (let i = 0; i < 2 && e.alive; i++) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb['appear.atk_scale'], 1.15), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
|
||
if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb['appear.stun'], 1.5), source: unit });
|
||
}
|
||
battle.fx('swordStorm', { x: unit.x, y: unit.y, id: unit.id });
|
||
unit.mem.rainTimer?.cancel();
|
||
unit.mem.rainTimer = battle.every(Math.max(0.1, num(bb['texas2_s_3[sword].interval'], 1)), (b, sched) => {
|
||
if (!unit.alive || !unit.deployed || !unit.skill?.active) { sched.cancel(); return; }
|
||
const list = targetsInGrid(b, unit, g);
|
||
sortEnemyTargets(b, unit, list, null);
|
||
for (const e of list.slice(0, Math.max(1, Math.floor(num(bb.max_target, 2))))) {
|
||
b.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 0.85), type: 'arts', isSkill: true, tags: ['skill', 'swordRain'] });
|
||
if (e.alive) b.applyStatus(e, 'stun', { duration: num(bb.stun, 0.2), source: unit });
|
||
b.fx('swordRain', { x: e.x, y: e.y, id: e.id });
|
||
}
|
||
}, { owner: unit });
|
||
// Duration advances by dt in the ally phase; align the rain with that clock so the final wave lands before end.
|
||
unit.mem.rainTimer.due -= battle.dt;
|
||
};
|
||
const cast = S1 ? castS1 : S2 ? castS2 : castS3;
|
||
const skill = {
|
||
kind: 'duration', activateOnDeploy: true, duration: dur, spCost: 0, spType: 'none', trigger: 'NEVER',
|
||
mods: { atkPct: num(t0.atk, 0.2) + (S1 || S2 ? num(bb.atk) : 0) },
|
||
onStart({ battle, unit, reason }) {
|
||
if (reason === 'kill' && unit.mem.texasRecastPending) {
|
||
unit.mem.texasRecastPending = false; // the synchronous burst already performed this recast
|
||
return;
|
||
}
|
||
unit.mem.texasCasting = true;
|
||
cast(battle, unit);
|
||
unit.mem.texasCasting = false;
|
||
},
|
||
onEnd({ unit, reason }) {
|
||
if (reason === 'recast' && unit.mem.texasRecastPending) return;
|
||
unit.mem.rainTimer?.cancel();
|
||
},
|
||
};
|
||
return {
|
||
skill,
|
||
skills: alt(def, {
|
||
skchr_texas2_1: () => skill,
|
||
skchr_texas2_2: () => skill,
|
||
}),
|
||
// S2: 攻击变为二连击 while the passive lasts
|
||
trait: S2 ? { hitsFn: (b, u) => (u.skill?.active ? 2 : 1) } : undefined,
|
||
talents: [
|
||
{ install(battle, unit) { // 德克萨斯传统 (2nd half): first kill of each deployment ⇒ full heal + recast
|
||
// (the passive's deploy burst runs inside skill.reset, BEFORE the `deploy` hook: the per-deployment state is
|
||
// therefore reset when she leaves the field, so a kill by the deploy burst itself counts)
|
||
battle.on('death', (c) => {
|
||
if (c.unit !== unit) return;
|
||
unit.mem.texasKilled = unit.mem.texasCasting = unit.mem.texasRecastPending = false;
|
||
}, { owner: unit });
|
||
battle.on('kill', (c) => {
|
||
if (c.killer !== unit || !unit.alive || c.victim.side !== 'enemy' || unit.mem.texasKilled) return;
|
||
unit.mem.texasKilled = true;
|
||
battle.removeBuff(unit, 'texas2:swordplay');
|
||
battle.heal(unit, unit, unit.s.maxHp * num(t0.hp_ratio, 1), { self: true });
|
||
if (unit.mem.texasCasting) {
|
||
unit.mem.texasRecastPending = true;
|
||
cast(battle, unit); // preserve the burst's synchronous reentry and damage order
|
||
} else {
|
||
unit.skill.end('recast');
|
||
unit.skill.activate('kill', { free: true });
|
||
}
|
||
}, { owner: unit });
|
||
battle.on('skillStart', (c) => {
|
||
if (c.unit !== unit || !unit.mem.texasRecastPending) return;
|
||
// Complete the lifecycle after observers received the outer start; the burst already recast above.
|
||
unit.skill.end('recast');
|
||
unit.skill.activate('kill', { free: true });
|
||
}, { owner: unit, priority: -2000 });
|
||
} },
|
||
{ install(battle, unit) { // 德克萨斯剑术: until her first kill after each deployment: ASPD +8, −25 % damage taken
|
||
battle.on('deploy', (c) => {
|
||
if (c.unit !== unit || unit.mem.texasKilled) return; // the deploy burst may already have scored the kill
|
||
battle.addBuff(unit, { key: 'texas2:swordplay', mods: { aspd: num(t1.attack_speed, 8), dmgTakenMul: 1 - num(t1.damage_resistance, 0.25) } });
|
||
}, { owner: unit });
|
||
} },
|
||
],
|
||
install(battle, unit) {
|
||
if (S1) {
|
||
// 攻击使命中目标失去特殊能力N秒,期间目标每秒受到X点法术伤害 (landed normal attacks while the passive lasts)
|
||
const sil = num(bb['attack@silence'], 5), dotDur = num(bb['attack@texas2_s_1[dot].duration'], sil);
|
||
const dot = num(bb['attack@texas2_s_1[dot].dot_damage'], 260), dotIv = Math.max(0.1, num(bb['attack@texas2_s_1[dot].interval'], 1));
|
||
battle.on('damaged', (c) => {
|
||
const e = c.target;
|
||
if (c.source !== unit || !c.dmg?.isAttack || e.side !== 'enemy' || !e.alive || !unit.skill?.active) return;
|
||
battle.applyStatus(e, 'silence', { duration: sil, source: unit });
|
||
battle.addBuff(e, { key: `texas2:drizzleDot:${unit.id}`, duration: dotDur + 1e-6, interval: dotIv, source: unit, refresh: 'extend', // a re-hit refreshes it, the per-second ticks keep their rhythm
|
||
onTick: ({ unit: t }) => battle.dealDamage(unit, t, { amount: dot, type: 'arts', isSkill: true, tags: ['skill', 'dot'] }) });
|
||
}, { owner: unit });
|
||
}
|
||
if (S2) {
|
||
// 攻击…造成法术伤害 while the passive lasts (the double hit is the kit trait's hitsFn)
|
||
battle.on('hit', (c) => {
|
||
if (c.source === unit && c.dmg.isAttack && c.dmg.type === 'phys' && unit.skill?.active) c.dmg.type = 'arts';
|
||
}, { owner: unit, priority: 50 });
|
||
}
|
||
const a = num(tb.atk, 0); // module (elite): ATK +10 % with no ally on the 4 adjacent tiles
|
||
if (!a) return;
|
||
whileDeployed(battle, unit, AURA, () => toggleBuff(battle, unit, 'texas2:module', lonely(battle, unit), { atkPct: a }));
|
||
},
|
||
};
|
||
},
|
||
});
|