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.
This commit is contained in:
commit
a0a7660287
55 files changed
+2140
-736
No files matched your search
@@ -10,7 +10,8 @@
|
||||
// which also ignore power_def_penetrate DEF / power_magic_resist_penetrate RES (defIgnorePct / resIgnorePct)
|
||||
// 迅捷 swiftShip member skill end → p = min(1, base_prob + prob_per_stack·L): +normal_sp SP; L ≥ power_bond_stack_cnt:
|
||||
// every operator's skill end rolls p again for +power_sp (members roll both, research [ASSUMED])
|
||||
// 灵巧 skillfulShip aura: members + operators on their 4 (L ≥ 40: 8) adjacent tiles ASPD +(base + per·L), once per unit
|
||||
// 灵巧 skillfulShip aura: members + operators on their 4 (L ≥ 40: 8) adjacent tiles ASPD +(base + per·L), once per unit;
|
||||
// killed members keep providing it from their body tile until redeploy; retreat / forcedExit do not
|
||||
// 奥术 arcaneShip member arts damage → target arts taken ×(base + per·L) for weak_duration s; tier 2: ×power_weak_scale
|
||||
// when the target is below hp_ratio at application. ONE instance per target whatever applies it — the
|
||||
// two players of a pair field compete for it, the strongest wins (battle.applyStrongest, 同名效果取最高:
|
||||
@@ -31,7 +32,7 @@
|
||||
// + 脆弱 ×damage_scale for weak[limit] s
|
||||
// 助力 deputShip all operators DEF +(base + per·L), redeploy time ×(1 + respawn_time)
|
||||
// 突袭 raidShip member idle ≥ no_attack_duration s (or skill ready — a passive skill that is on counts, GitHub
|
||||
// #49) with no enemy in range → "保留技力立即再部署"
|
||||
// #49, and so does a deploy-timed skill while it runs, #109) with no enemy in range → "保留技力立即再部署"
|
||||
// next to the most advanced ground enemy it can reach: on a free tile its position may be deployed
|
||||
// on from which its range covers that enemy (GitHub issue #51 [ASSUMED]: the first of the 8 most
|
||||
// advanced that has such a tile; none → it stays and the next poll looks again, never a jump that
|
||||
@@ -220,10 +221,14 @@ function updateAura(battle, st) {
|
||||
next.clear();
|
||||
const offs = st.auraWide ? N8 : N4;
|
||||
for (const m of st.members[ID.skillful]) {
|
||||
if (!onField(m)) continue;
|
||||
next.add(m);
|
||||
const active = onField(m);
|
||||
// isDown also covers voluntary / forced exits since v0.1.2. Only a kill keeps the aura.
|
||||
if (!active && (m.removeReason !== 'killed' || !battle.isDown(m))) continue;
|
||||
if (active) next.add(m);
|
||||
// A knocked-out member still covers neighbours around the tile where it waits to redeploy.
|
||||
const [r, c] = active ? [m.tileR, m.tileC] : battle.restTile(m);
|
||||
for (const [dr, dc] of offs) {
|
||||
const a = battle.unitAt(m.tileR + dr, m.tileC + dc);
|
||||
const a = battle.unitAt(r + dr, c + dc);
|
||||
if (a && a.kind === 'op' && a.ownerId === st.pid && onField(a)) next.add(a);
|
||||
}
|
||||
}
|
||||
@@ -317,8 +322,12 @@ function raidPoll(battle, st) {
|
||||
// 缄默德克萨斯 jumping within her passive's 10 s with no enemy in range). The engine keeps a passive on for the whole
|
||||
// deployment, so such a member may jump whenever nothing is in its range [ASSUMED: "技能就绪" of a passive = the
|
||||
// skill being on]; the landing rule below keeps it from hopping. Here only — the global `ready` stays as it is.
|
||||
// A deploy-timed skill (#109: kind 'duration' with no SP — spType 'none' — run from the deployment: 宴 S2, 斯卡蒂 S2,
|
||||
// 伊内丝 S3, 缄默德克萨斯 S1–S3, 耀骑士临光 S2, the generic "部署后…N秒内" passives) counts the same way while its
|
||||
// window runs — the 10 s of the reporter's footage; once it has ended only the idle trigger is left.
|
||||
const sk = u.skill;
|
||||
const ready = !!(sk && !sk.noSkill && (sk.ready || (sk.kind === 'passive' && sk.active)) && !(sk.active && sk.isTimed));
|
||||
const deploySkillOn = !!(sk && sk.active && (sk.kind === 'passive' || sk.spType === 'none'));
|
||||
const ready = !!(sk && !sk.noSkill && ((sk.ready && !(sk.active && sk.isTimed)) || deploySkillOn));
|
||||
const idleOk = battle.time - since >= idle - 1e-9;
|
||||
if (!(ready || idleOk)) continue;
|
||||
if (battle.enemiesInKeys(u.rangeKeys || [], u, u.profile).length) continue;
|
||||
|
||||
@@ -28,9 +28,10 @@
|
||||
// "附带…凋亡/灼燃/神经损伤" + ep_damage_ratio ⇒ element damage on hit (× damage dealt when the text says "伤害N%的…损伤",
|
||||
// else × ATK); "屏障" + shield_max_hp_ratio / hp_ratio ⇒ self shield at start decaying over its duration (砾, 新约能天使);
|
||||
// "立即流失N%当前生命" + hp_ratio ⇒ self HP loss at start (宴, 风丸); hp_ratio + "恢复/回复…生命" ⇒ self heal at start.
|
||||
// Passive skills only apply stat mods (for bb.duration s when the text says "N秒内": 宴; for the skill's own duration
|
||||
// when an ON_DEPLOY passive says "部署后…" without one in its blackboard: 一击即退, duration 10 — PRTS 技能 持续 10) and
|
||||
// the self/counter effects above — their scales describe procs (bombs, sword rain, counters) that need a kit.
|
||||
// Passive skills with stat mods + bb.duration + "N秒内" use a deployment duration (宴), and so do the ON_DEPLOY passives that
|
||||
// say "部署后…" with the skill's own duration and none in their blackboard (一击即退, duration 10 — PRTS 技能 持续 10):
|
||||
// kind 'duration', activateOnDeploy (#109); other passives apply stat mods and the self/counter effects above — their
|
||||
// scales describe procs (bombs, sword rain, counters) that need a hand-authored kit.
|
||||
// "立即获得N点部署费用" + cost ⇒ +cost DP for the player at the start (冲锋号令); an AUTO skill that does nothing else
|
||||
// fires as soon as its SP is full (as 德克萨斯's kit casts the same skill: kits/ops/chess_char_1_08-texas.js).
|
||||
// genericTalents(def): the talents a generic kit can apply exactly — an unconditional stat line ("攻击力+8%",
|
||||
@@ -112,10 +113,8 @@ export function genericSkillSpec(sk, bb = sk?.bb ?? {}, def = null) {
|
||||
if (!sk) return null;
|
||||
const g = getter(bb);
|
||||
const ga = attackGetter(bb);
|
||||
const kind = genericKind(sk, bb);
|
||||
let kind = genericKind(sk, bb);
|
||||
const desc = String(sk.description || '');
|
||||
const passive = kind === 'passive';
|
||||
const timed = kind === 'duration' || kind === 'ammo' || kind === 'toggle';
|
||||
// "受到攻击时…造成…" numbers belong to a counter effect (the operator's own, or an ally's: 刺玫 "该角色受到攻击时")
|
||||
const counterCtx = /受到(敌人的)?攻击时/.test(desc);
|
||||
const counterText = counterCtx && !/该(角色|干员|单位)受到攻击时/.test(desc);
|
||||
@@ -154,9 +153,21 @@ export function genericSkillSpec(sk, bb = sk?.bb ?? {}, def = null) {
|
||||
set('resIgnoreFlat', g('magic_resist_penetrate_fixed'));
|
||||
set('defIgnoreFlat', g('def_penetrate_fixed'));
|
||||
|
||||
// Effect inference keeps the original kind; a deployment window only changes the lifecycle.
|
||||
const passive = kind === 'passive';
|
||||
const timed = kind === 'duration' || kind === 'ammo' || kind === 'toggle';
|
||||
// a passive stat buff limited in time ("部署后…在14秒内攻击力+65%", 宴) runs as a duration skill from every deployment;
|
||||
// so does an ON_DEPLOY passive whose duration is the skill's own (一击即退 "部署后攻击力+X%,防御力+Y%", duration 10, no bb key)
|
||||
const ownDuration = num(bb.duration) === undefined && sk.duration > 0 && /^部署后/.test(desc) ? sk.duration : 0;
|
||||
const passiveTimed = passive && Object.keys(mods).length
|
||||
? (num(bb.duration) > 0 && /\d+(\.\d+)?秒内/.test(desc) ? num(bb.duration) : ownDuration)
|
||||
: 0;
|
||||
if (passiveTimed) kind = 'duration';
|
||||
|
||||
// ---- targeting / attack override (never for passives: their scales describe procs)
|
||||
const targeting = {};
|
||||
const attack = {};
|
||||
if (passiveTimed && /攻击[^。;]*(造成|变为|变成)[^。;]*法术伤害|伤害类型变为法术/.test(desc)) attack.dmgType = 'arts';
|
||||
const mt = ga('max_target');
|
||||
if (!passive && mt !== undefined && mt > 0) targeting.maxTargets = Math.floor(mt);
|
||||
const ext = g('ability_range_forward_extend');
|
||||
@@ -278,17 +289,12 @@ export function genericSkillSpec(sk, bb = sk?.bb ?? {}, def = null) {
|
||||
id: sk.id,
|
||||
name: sk.name,
|
||||
kind,
|
||||
duration: kind === 'duration' ? sk.duration : (kind === 'ammo' && sk.duration > 0 ? sk.duration : undefined),
|
||||
duration: kind === 'duration' ? (passiveTimed || sk.duration) : (kind === 'ammo' && sk.duration > 0 ? sk.duration : undefined),
|
||||
ammo,
|
||||
};
|
||||
|
||||
// passive stat buffs limited in time ("部署后…在14秒内攻击力+65%", 宴) become a timed buff at each deployment; so do the
|
||||
// ON_DEPLOY passives whose duration is the skill's own (一击即退 "部署后攻击力+X%,防御力+Y%", duration 10, no bb key)
|
||||
const ownDuration = num(bb.duration) === undefined && sk.duration > 0 && /^部署后/.test(desc) ? sk.duration : 0;
|
||||
const passiveTimed = passive && Object.keys(mods).length
|
||||
? (num(bb.duration) > 0 && /\d+(\.\d+)?秒内/.test(desc) ? num(bb.duration) : ownDuration)
|
||||
: 0;
|
||||
if (Object.keys(mods).length && !passiveTimed) spec.mods = mods;
|
||||
if (passiveTimed) Object.assign(spec, { activateOnDeploy: true, spCost: 0, spType: 'none', trigger: 'NEVER' });
|
||||
if (Object.keys(mods).length) spec.mods = mods;
|
||||
if (Object.keys(targeting).length) spec.targeting = targeting;
|
||||
// instant/charges skills act on the next attack: mods/targeting without an explicit attack still need one
|
||||
if (!Object.keys(attack).length && (kind === 'instant' || kind === 'charges') && (spec.mods || spec.targeting)) spec.attack = {};
|
||||
@@ -303,10 +309,6 @@ export function genericSkillSpec(sk, bb = sk?.bb ?? {}, def = null) {
|
||||
// ---- start / end effects
|
||||
const starts = [];
|
||||
const ends = [];
|
||||
if (passiveTimed) {
|
||||
const m = { ...mods };
|
||||
starts.push(({ battle, unit }) => battle.addBuff(unit, { key: `generic:passive:${sk.id ?? 'skill'}`, duration: passiveTimed, mods: m, tags: ['skill'] }));
|
||||
}
|
||||
if (gainsDp) {
|
||||
starts.push(({ battle, unit }) => {
|
||||
battle.addDp(unit.ownerId, dpGain);
|
||||
|
||||
@@ -12,7 +12,6 @@ export default {
|
||||
// hp_recovery_per_sec_by_max_hp_ratio × max HP per second (the 生命回复速度 attribute: works under her 武者 no-heal).
|
||||
chess_char_1_18_a: (bb, chess) => {
|
||||
const t = talentBb(chess, 0);
|
||||
const buffKey = 'utage:s2';
|
||||
const s1 = skillBbOf(chess, 'skchr_utage_1');
|
||||
return {
|
||||
skills: {
|
||||
@@ -24,16 +23,16 @@ export default {
|
||||
},
|
||||
},
|
||||
skill: {
|
||||
kind: 'passive',
|
||||
kind: 'duration', activateOnDeploy: true, duration: num(bb.duration), spCost: 0, spType: 'none', trigger: 'NEVER',
|
||||
mods: { atkPct: num(bb.atk) },
|
||||
onStart({ battle, unit }) {
|
||||
const loss = unit.hp * num(bb.hp_ratio);
|
||||
if (loss > 0 && unit.hp - loss >= 1) battle.loseHp(unit, loss, { source: unit });
|
||||
battle.addBuff(unit, { key: buffKey, duration: num(bb.duration), mods: { atkPct: num(bb.atk) }, tags: ['skill'], visible: true });
|
||||
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1, id: unit.id, skill: 'breach' });
|
||||
},
|
||||
},
|
||||
talents: [{ install(battle, unit) {
|
||||
onHitBy(battle, unit, ({ dmg }) => { if (dmg.isAttack && dmg.type === 'phys' && unit.findBuff(buffKey)) dmg.type = 'arts'; });
|
||||
onHitBy(battle, unit, ({ dmg }) => { if (dmg.isAttack && dmg.type === 'phys' && unit.skill?.id === 'skchr_utage_2' && unit.skill.active) dmg.type = 'arts'; });
|
||||
const maxAs = num(t.min_attack_speed), minHp = num(t.min_hp_ratio);
|
||||
if (maxAs > 0 && minHp < 1) {
|
||||
battle.on('tick', () => {
|
||||
|
||||
@@ -25,11 +25,8 @@ export default {
|
||||
return {
|
||||
skills: {
|
||||
skchr_wildmn_1: {
|
||||
kind: 'passive',
|
||||
onStart({ battle, unit }) {
|
||||
const d = num(r1?.duration);
|
||||
if (d > 0) battle.addBuff(unit, { key: 'wildmn:s1', duration: d, mods: { aspd: num(r1?.bb?.attack_speed) }, tags: ['skill'], visible: true });
|
||||
},
|
||||
kind: 'duration', activateOnDeploy: true, duration: num(r1?.duration), spCost: 0, spType: 'none', trigger: 'NEVER',
|
||||
mods: { aspd: num(r1?.bb?.attack_speed) },
|
||||
},
|
||||
},
|
||||
skill: {
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
const t = talentBb(chess, 0);
|
||||
return {
|
||||
skill: {
|
||||
kind: 'passive',
|
||||
kind: 'duration', activateOnDeploy: true, duration: num(bb.duration, 10), spCost: 0, spType: 'none', trigger: 'NEVER',
|
||||
onStart({ battle, unit }) {
|
||||
const total = unit.s.maxHp * num(bb.hp_ratio);
|
||||
const dur = num(bb.duration, 10);
|
||||
@@ -34,10 +34,11 @@ export default {
|
||||
});
|
||||
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
|
||||
},
|
||||
onEnd({ battle, unit }) { battle.removeBuff(unit, 'gravel:rats'); },
|
||||
},
|
||||
skills: {
|
||||
skchr_gravel_1: {
|
||||
kind: 'passive',
|
||||
kind: 'duration', activateOnDeploy: true, duration: num(bb.duration), spCost: 0, spType: 'none', trigger: 'NEVER',
|
||||
onStart({ battle, unit }) {
|
||||
const v = num(bb.def), dur = num(bb.duration);
|
||||
if (!(v > 0) || !(dur > 0)) return;
|
||||
@@ -52,6 +53,7 @@ export default {
|
||||
});
|
||||
battle.fx('buff', { x: unit.x, y: unit.y, id: unit.id, kind: 'def' });
|
||||
},
|
||||
onEnd({ battle, unit }) { battle.removeBuff(unit, 'gravel:shadow'); },
|
||||
},
|
||||
},
|
||||
talents: [{ install(battle, unit) {
|
||||
|
||||
@@ -20,10 +20,8 @@ export default {
|
||||
skills: altSkills(chess, d, bb, {
|
||||
'skcom_quickattack[3]': statSkill,
|
||||
skchr_skadi_2: (s) => ({
|
||||
kind: 'passive',
|
||||
onStart({ battle, unit }) { // "部署后N秒内攻击力+X" (a passive starts at every deployment)
|
||||
battle.addBuff(unit, { key: 'skill:skadi_wave', duration: num(s.bb.duration, 20), mods: { atkPct: num(s.bb.atk) }, visible: true });
|
||||
},
|
||||
kind: 'duration', activateOnDeploy: true, duration: num(s.bb.duration, 20), spCost: 0, spType: 'none', trigger: 'NEVER',
|
||||
mods: { atkPct: num(s.bb.atk) },
|
||||
}),
|
||||
}),
|
||||
talents: [
|
||||
|
||||
@@ -39,21 +39,9 @@ export default withDefaults({
|
||||
},
|
||||
}),
|
||||
skchr_ines_3: () => ({
|
||||
kind: 'passive',
|
||||
kind: 'duration', duration: s3Dur, spCost: 0, spType: 'none', trigger: 'NEVER',
|
||||
onStart({ battle, unit }) {
|
||||
if (!unit.mem.inesS3Placed) {
|
||||
// 首次部署: place a 影哨 (her talent's sentry, left on retreat) and leave; 立刻刷新再部署时间 — the redeploy
|
||||
// (auto, paying her DP cost like every redeploy) follows as soon as it is affordable
|
||||
unit.mem.inesS3Placed = true;
|
||||
battle.after(0, () => {
|
||||
if (!unit.alive || !unit.deployed) return;
|
||||
battle.retreat(unit, { reason: 'retreat' });
|
||||
unit.respawnAt = battle.time;
|
||||
battle.fx('sentry', { x: unit.x, y: unit.y, id: unit.id });
|
||||
}, { owner: unit });
|
||||
return;
|
||||
}
|
||||
battle.addBuff(unit, { key: 'ines:s3', duration: s3Dur, mods: { atkPct: num(bb.atk) }, visible: true });
|
||||
battle.addBuff(unit, { key: 'ines:s3', mods: { atkPct: num(bb.atk) }, visible: true });
|
||||
// 立刻收回影哨: the sentry flies back to her and hits ≤ max_target enemies on its way (within
|
||||
// projectile_range of the segment, [ASSUMED] nearest to its start first)
|
||||
const from = unit.mem.inesSentryAt;
|
||||
@@ -77,6 +65,7 @@ export default withDefaults({
|
||||
}
|
||||
battle.fx('beam', { x: ax, y: ay, tx: bx, ty: by, id: unit.id });
|
||||
},
|
||||
onEnd({ battle, unit }) { battle.removeBuff(unit, 'ines:s3'); },
|
||||
}),
|
||||
}),
|
||||
skill: {
|
||||
@@ -146,11 +135,27 @@ export default withDefaults({
|
||||
],
|
||||
install(battle, unit) {
|
||||
if (S3) {
|
||||
battle.on('deploy', (c) => {
|
||||
if (c.unit !== unit || c.move) return;
|
||||
if (unit.mem.inesS3Placed) {
|
||||
unit.skill.activate('deploy');
|
||||
return;
|
||||
}
|
||||
// 首次部署 only places a 影哨 and retreats; it is not a skill activation.
|
||||
// 立刻刷新再部署时间: redeploy as soon as its DP cost is affordable.
|
||||
unit.mem.inesS3Placed = true;
|
||||
battle.after(0, () => {
|
||||
if (!unit.alive || !unit.deployed) return;
|
||||
battle.retreat(unit, { reason: 'retreat' });
|
||||
unit.respawnAt = battle.time;
|
||||
battle.fx('sentry', { x: unit.x, y: unit.y, id: unit.id });
|
||||
}, { owner: unit });
|
||||
}, { owner: unit });
|
||||
// where the talent's 影哨 stays (every leave but an expiry), for the recall of the next deployment
|
||||
battle.on('death', (c) => { if (c.unit === unit && c.reason !== 'expired') unit.mem.inesSentryAt = { x: unit.x, y: unit.y }; }, { owner: unit, priority: 5 });
|
||||
// 技能期间每对一个敌人造成伤害就获得1点部署费用
|
||||
battle.on('damaged', (c) => {
|
||||
if (c.source !== unit || c.target.side !== 'enemy' || !(c.amount > 0) || c.type === 'element' || !unit.findBuff('ines:s3')) return;
|
||||
if (c.source !== unit || c.target.side !== 'enemy' || !(c.amount > 0) || c.type === 'element' || !unit.skill?.active || !(unit.skill.timeLeft > 0)) return;
|
||||
battle.addDp(unit.ownerId, num(bb.cost, 1));
|
||||
}, { owner: unit });
|
||||
}
|
||||
|
||||
@@ -18,17 +18,12 @@ export default withDefaults({
|
||||
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 talentAtk = (battle, unit) => battle.addBuff(unit, { key: 'texas2:rainAtk', duration: dur, mods: { atkPct: num(t0.atk, 0.2) } });
|
||||
const castS1 = (battle, unit) => {
|
||||
if (!unit.alive || !unit.deployed) return;
|
||||
talentAtk(battle, unit);
|
||||
battle.addBuff(unit, { key: 'texas2:drizzle', duration: dur, mods: { atkPct: num(bb.atk) }, visible: true });
|
||||
battle.fx('swordRain', { x: unit.x, y: unit.y, id: unit.id });
|
||||
};
|
||||
const castS2 = (battle, unit) => {
|
||||
if (!unit.alive || !unit.deployed) return;
|
||||
talentAtk(battle, unit);
|
||||
battle.addBuff(unit, { key: 'texas2:shower', duration: dur, mods: { atkPct: num(bb.atk) }, visible: true });
|
||||
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'] });
|
||||
@@ -39,16 +34,14 @@ export default withDefaults({
|
||||
};
|
||||
const castS3 = (battle, unit) => {
|
||||
if (!unit.alive || !unit.deployed) return;
|
||||
talentAtk(battle, unit);
|
||||
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.rainUntil = battle.time + dur;
|
||||
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 || b.time > unit.mem.rainUntil + 1e-9) { sched.cancel(); return; }
|
||||
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))))) {
|
||||
@@ -57,28 +50,62 @@ export default withDefaults({
|
||||
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: { kind: 'passive', onStart({ battle, unit }) { cast(battle, unit); } },
|
||||
skill,
|
||||
skills: alt(def, {
|
||||
skchr_texas2_1: () => ({ kind: 'passive', onStart({ battle, unit }) { cast(battle, unit); } }),
|
||||
skchr_texas2_2: () => ({ kind: 'passive', onStart({ battle, unit }) { cast(battle, unit); } }),
|
||||
skchr_texas2_1: () => skill,
|
||||
skchr_texas2_2: () => skill,
|
||||
}),
|
||||
// S2: 攻击变为二连击 while the passive lasts
|
||||
trait: S2 ? { hitsFn: (b, u) => (u.findBuff('texas2:shower') ? 2 : 1) } : undefined,
|
||||
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) unit.mem.texasKilled = false; }, { owner: unit });
|
||||
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 });
|
||||
cast(battle, unit);
|
||||
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) => {
|
||||
@@ -94,7 +121,7 @@ export default withDefaults({
|
||||
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.findBuff('texas2:drizzle')) return;
|
||||
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'] }) });
|
||||
@@ -103,7 +130,7 @@ export default withDefaults({
|
||||
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.findBuff('texas2:shower')) c.dmg.type = 'arts';
|
||||
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
|
||||
|
||||
@@ -46,23 +46,24 @@ function nearl2(bb, chess, def) {
|
||||
// (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: 'passive',
|
||||
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);
|
||||
const combo = !!prev && prev !== unit && hasBond(prev, 'kazimierzShip');
|
||||
battle.addBuff(unit, { key: 'nearl2:night', mods: { atkPct: num(bb.atk) }, duration: dur, visible: true });
|
||||
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 });
|
||||
const seq = unit.deploySeq;
|
||||
battle.after(dur, () => {
|
||||
if (!live(unit) || unit.deploySeq !== seq) return;
|
||||
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 });
|
||||
}, { owner: unit });
|
||||
},
|
||||
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 });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user