feedback5: merge W5M — the nine 0.1.4 PRs (#150 #66 #146 #74 #72 #103 #149 #109 #160) and W4A (高台 per the trait, 阿戈尔 revive slots, 联防 devour) ported into the split layout
Conflicts: server/match/bot.js (W4A's comment + W5K's player data view pgd), test/golden/diy.json (regenerated). data/ equals an offline build of the merged tools/build-data.mjs. Golden regenerated on the merged code: against feedback5 the moved roster / bonds / fields / matches / standins scenarios are exactly W5M's per-PR attribution (#160 skill events, #109 deploy-timed lifecycle, #66 灵巧 sources and hidden layers, 83fc408 歌蕾蒂娅 placement), plus coop2-NORMAL-70-diy (fields operators those PRs touch) and diy-051 / diy-052 (events.skill only: Misery's deploy passives, #160); against fb5-merge4 only feedback5's own additions (the diy kits, coop2-NORMAL-70-diy).
This commit is contained in:
commit
9ce5435373
74 files changed
+2966
-1338
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;
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
// 拉特兰 lateranoShip member ammo skills start with floor(ammo × (1 + base_ammo_percent + ammo_percent_per_stack·L));
|
||||
// 6: every ammo used by a member → all members ATK +atk_per_consume (≤ max_atk_for_consume)
|
||||
// 阿戈尔 egirShip members max HP +(base_max_hp + max_hp_per_stack·L) (直接乘算); battle start devour (see devour());
|
||||
// 5: the first max_free_respawn_cnt members by position (the devour's order, fixed at battle
|
||||
// start) each redeploy at once (free) on their first knock-out
|
||||
// 5: the first max_free_respawn_cnt members knocked out (in knock-out order, the devour's food
|
||||
// included) each redeploy at once (free) on that first knock-out
|
||||
// 叙拉古 siracusaShip every member deployment: ASPD +(base + per·L) for (base_duration + per·L) s; 6: 隐匿 for the same
|
||||
// time, and while hidden / end_duration s after, every 普通伤害 hit (siracusaRolls) procs (PRD,
|
||||
// nominal `prob`) base_damage + damage_per_stack·L true damage + fear `fear` s
|
||||
@@ -34,7 +34,7 @@
|
||||
// Two players in one field: everything is per player (own members, own deployments); the 谢拉格 wind and 炎佑 act on
|
||||
// every enemy of the field (debuffs on shared enemies help both, research 06 §8.6).
|
||||
// Hook priorities: 拉特兰 skillStart −10 (after kits' own ammo changes), 阿戈尔 death 11 (5-tier revive, before 不屈's
|
||||
// death 10), layerGain −100 (after 魔王-style modifiers).
|
||||
// death 10; every `fatal` saver runs before any death hook), layerGain −100 (after 魔王-style modifiers).
|
||||
|
||||
import * as S from '../support/index.js';
|
||||
import { mitigate } from '../../damage.js';
|
||||
@@ -334,15 +334,14 @@ function installLaterano(battle, pid, bb, members) {
|
||||
|
||||
// "更靠左和靠上": left first (on the player's own board: mirrored players count from the field's right), then top first —
|
||||
// row 0 is the BOTTOM row (DESIGN §3), so the top of the board is the highest row index. The order is a board position,
|
||||
// independent of the members' directions (only "身前" follows each member's `dir`). The devour marks in this order and the
|
||||
// 5-tier revive's beneficiaries are the first members in it (PRTS "从最先部署(更靠左和靠上的)的【阿戈尔】干员开始"; the
|
||||
// battle's initial deployment order, Battle.start).
|
||||
// independent of the members' directions (only "身前" follows each member's `dir`). The devour marks in this order (PRTS
|
||||
// "从最先部署(更靠左和靠上的)的【阿戈尔】干员开始"; the battle's initial deployment order, Battle.start).
|
||||
const boardCol = (u) => (u.player && u.player.mirror ? -u.tileC : u.tileC);
|
||||
const egirOrder = (a, b) => boardCol(a) - boardCol(b) || b.tileR - a.tileR || a.id - b.id;
|
||||
|
||||
/**
|
||||
* 联防: an operator forced out at the deployment (carry.down, Battle.start, before battleStart) still stands on its
|
||||
* deploy position. The devour and the 5-tier revive slots both count it there (PRTS 盟约记录 前3名 / 最先部署).
|
||||
* deploy position for the devour (PRTS 盟约记录 最先部署). The forced exit is not a knock-out, so it takes no 5-tier slot.
|
||||
*/
|
||||
function egirDownAtStart(battle, u) {
|
||||
return S.isOp(u) && !u.alive && u.removeReason === FORCED_EXIT && !!u.carry && u.carry.down === true
|
||||
@@ -365,11 +364,14 @@ function egirDownAtStart(battle, u) {
|
||||
* out and back in the same pass still gives its marks).
|
||||
* 联防: the operators down since the end of their own combat (forced out by Battle.start) mark, are marked and resolve
|
||||
* their marks like standing ones, but nothing resolves on them (below; per players' reports, owner's decision 2026-10-04).
|
||||
* Each devoured operator adds its tier to 阿戈尔 once (IN_BATTLE gain, disabled in 联防 / boss fields).
|
||||
* Whose operator stands in front does not matter (PRTS "依次吞噬身前一格干员", no own-side limit; the owner's decision of
|
||||
* 2026-10-05 after GitHub #140 comment 4): on a shared field (联防, boss) a teammate's operator — standing, or entering
|
||||
* 联防 down — is marked like an own one, gives the same base ATK / block count, and the chain goes on through it when it
|
||||
* is an 阿戈尔 (S.isMember: its own bonds). Its knock-out is its owner's (their bonds' revives, 不屈 …), credited to the
|
||||
* marker as usual. Each devoured operator adds its tier to 阿戈尔 once (IN_BATTLE gain, disabled in 联防 / boss fields).
|
||||
* Tokens / devices / empty tiles are never devoured.
|
||||
*/
|
||||
function devour(battle, pid, bb, members) {
|
||||
const memberSet = new Set(members);
|
||||
// 联防: an operator down at the end of its own combat (carryState.down — Battle.start forced it out right before
|
||||
// battleStart, FORCED_EXIT) takes part in the devour as if it stood on its tile, then stays out: it marks in its turn,
|
||||
// it is "the unit in front" of another (the chain goes on through it when it is a member), its base ATK / block count
|
||||
@@ -378,12 +380,14 @@ function devour(battle, pid, bb, members) {
|
||||
// report #3, GitHub #33 item 3), owner's decision 2026-10-04; until 0.1.2 the forced exit came first and the chain broke.
|
||||
const downAtStart = (u) => egirDownAtStart(battle, u);
|
||||
const order = members.filter((u) => S.onField(u) || downAtStart(u)).sort(egirOrder);
|
||||
// the operator in front, whoever owns it (until 0.1.3 only the player's own): a living one, else one lying there since
|
||||
// the 联防 start (carry.down, forced out) — a teammate's included
|
||||
const opAt = (u) => {
|
||||
const [r, c] = S.frontTile(u);
|
||||
const a = S.allyAt(battle, r, c, pid);
|
||||
const a = S.allyAt(battle, r, c);
|
||||
if (a) return S.isOp(a) && a.alive ? a : null;
|
||||
const d = battle.downOn(r, c);
|
||||
return d && d.ownerId === pid && downAtStart(d) ? d : null;
|
||||
return d && downAtStart(d) ? d : null;
|
||||
};
|
||||
const markedBy = new Map(); // marker → [targets]
|
||||
const marks = [];
|
||||
@@ -398,7 +402,8 @@ function devour(battle, pid, bb, members) {
|
||||
if ((markedBy.get(t) ?? []).includes(m)) continue;
|
||||
seen.add(t);
|
||||
mine.push(t);
|
||||
if (memberSet.has(t)) queue.push(t);
|
||||
// through a marked 阿戈尔 (for the player's own operators: exactly its members; a teammate's by its own bonds)
|
||||
if (S.isMember(battle, t, 'egirShip')) queue.push(t);
|
||||
}
|
||||
markedBy.set(m, mine);
|
||||
if (!mine.length) continue;
|
||||
@@ -437,37 +442,37 @@ function installEgir(battle, pid, bb, members) {
|
||||
};
|
||||
apply();
|
||||
onLayers(battle, pid, 'egirShip', apply);
|
||||
// 5: "前3名【阿戈尔】干员首次被击倒时立刻复活" — the 3 (bonds.json max_free_respawn_cnt) are the first 3 members BY POSITION,
|
||||
// in the devour's order above = the battle's deployment order (PRTS 盟约记录 阿戈尔 备注 "从最先部署(更靠左和靠上的)的
|
||||
// 【阿戈尔】干员开始"; players' videos: the revivers go by position, not by who is eaten or knocked out first). They are
|
||||
// fixed at battle start, before the devour; each revives once, on its own first knock-out ('killed'), whatever the order
|
||||
// of knock-outs, so a member the devour knocks out comes back only when it is one of them. Until 0.1.3 the first 3
|
||||
// members knocked out took the revives and the devour's food spent them at t = 0. A member entering 联防 down (forced
|
||||
// out before battleStart) keeps the slot its position earned: the forced exit is not a 击倒 (reason !== 'killed'), so
|
||||
// the charge waits until they are knocked out after standing back up. A 调和 member counts like any other.
|
||||
// [ASSUMED] a beneficiary 埃芒加德 / M3茧甲 save in place (items revivedInPlace, the stand-in for PRTS's 0-time redeploy)
|
||||
// has used its revive (PRTS 不屈 备注: a held 复活 is consumed even when another effect redeploys the unit at once).
|
||||
// PRTS: the knocked-out unit's next deployment has 0 redeploy time and 0 cost, i.e. it IS knocked out (被击倒 triggers,
|
||||
// 克莱门莎, 幽灵鲨 … fire) and redeploys at once where it lies (the engine's rest tile, Battle._layBody: the tile it was
|
||||
// knocked out on — a raid-relocated member comes back where it fell —, or its own home when it fell on another board
|
||||
// piece's home; PRTS 卫戍协议/帮助 §作战阶段 单位部署) with full HP, SP reset and `deploy` effects (卡西米尔 / 叙拉古).
|
||||
// Death priority 11: before 不屈 (10), whose redeploy "also consumes a 复活 charge" — with this order the charge is
|
||||
// always the one used, same outcome. The marks still pending on a revived member are cancelled (devour).
|
||||
// 5: "前3名【阿戈尔】干员首次被击倒时立刻复活" — the 3 (bonds.json max_free_respawn_cnt) slots go to the first 3 members
|
||||
// knocked out, in knock-out order: the owner's decision of 2026-10-05, following players' reports (GitHub #105, #140:
|
||||
// "没被吃的阿戈尔干员也会占用复活名额" — with 0.1.3's fixed holders by position the uneaten front members held the
|
||||
// slots, so whatever the food chain ate only 3 阿戈尔 stood). A knock-out by the battle-start devour counts like any
|
||||
// other, so the eaten food usually takes them at t = 0 and survivors = uneaten members + 3. A member takes at most one
|
||||
// slot, on its first knock-out ('killed') in the battle: its later knock-outs never take one, and a first knock-out
|
||||
// after the 3 are gone stays down. However many members mark it, its pending marks are cancelled once it is knocked
|
||||
// out (devour), so a multi-devoured member spends one slot at most. Its own saver acts first: every kit, item or band
|
||||
// revive that prevents or replaces the knock-out is a `fatal` hook (斯卡蒂's DRE-Y −50, kit savers 10 … −60, 坚固维式
|
||||
// 重锤 PRIO_REVIVE −100, M3茧甲 PRIO_RESPAWN −101, 埃芒加德 −110), so the unit is never knocked out and no slot is used —
|
||||
// the slot waits for its first real knock-out [ASSUMED for 埃芒加德, a band: as M3茧甲, the item revive the owner named].
|
||||
// An operator entering 联防 down (FORCED_EXIT) is not knocked out either; a 调和 member counts like any other. Each
|
||||
// battle (normal, 联防, boss) counts its own. PRTS: the knocked-out unit's next deployment has 0 redeploy time and 0
|
||||
// cost, i.e. it IS knocked out (被击倒 triggers, 克莱门莎, 幽灵鲨 … fire) and redeploys at once where it lies (the
|
||||
// engine's rest tile, Battle._layBody: the tile it was knocked out on — a raid-relocated member comes back where it
|
||||
// fell —, or its own home when it fell on another board piece's home; PRTS 卫戍协议/帮助 §作战阶段 单位部署) with full
|
||||
// HP, SP reset and `deploy` effects (卡西米尔 / 叙拉古). Death priority 11: before 不屈 (10), whose redeploy "also
|
||||
// consumes a 复活 charge" — with this order the slot is always the one used, same outcome. Until 0.1.3 the slots were
|
||||
// fixed at battle start for the first 3 members by position (§23.20); until 0.1.2 the first 3 knocked out took them
|
||||
// but a revived member's pending marks could knock it out again (GitHub #33, fixed in devour since).
|
||||
const max = reached(battle, pid, 'egirShip', bb.power_bond_char_cnt) ? Math.max(0, Math.floor(num(bb.max_free_respawn_cnt, 0))) : 0;
|
||||
const holders = new Map(); // beneficiary → its in-place revives (mem.revives) at battle start
|
||||
battle.on('battleStart', () => {
|
||||
for (const u of members.filter((x) => S.isOp(x) && (S.onField(x) || egirDownAtStart(battle, x))).sort(egirOrder).slice(0, max)) holders.set(u, u.mem.revives | 0);
|
||||
devour(battle, pid, bb, members);
|
||||
}, { once: true });
|
||||
battle.on('battleStart', () => devour(battle, pid, bb, members), { once: true });
|
||||
if (!(max > 0)) return;
|
||||
const used = new Set();
|
||||
const memberOps = new Set(members.filter(S.isOp));
|
||||
const knockedOut = new Set(); // members whose first knock-out of this battle has happened
|
||||
let revives = 0;
|
||||
battle.on('death', (c) => {
|
||||
const u = c.unit;
|
||||
if (c.reason !== 'killed' || !holders.has(u) || used.has(u)) return;
|
||||
used.add(u);
|
||||
if ((u.mem.revives | 0) !== holders.get(u)) return; // saved in place since the battle start: the revive is gone
|
||||
if (u.alive || u.removed) return;
|
||||
if (c.reason !== 'killed' || !memberOps.has(u) || knockedOut.has(u)) return;
|
||||
knockedOut.add(u);
|
||||
if (revives >= max || u.alive || u.removed) return;
|
||||
if (!battle.redeploy(u, { free: true })) return;
|
||||
revives++;
|
||||
S.fxOn(battle, 'revive', u, 'bond:egirShip', 'respawn', { n: revives });
|
||||
|
||||
@@ -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