Files
ark-stronghold-protocol/server/sim/content/kits/ops/chess_char_4_16-texas2.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

143 lines
8.5 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_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 }));
},
};
},
});