Playtest #6 follow-up (v2.5.1): leader HP review — 直接乘算, one debuff instance, 999 layer cap, 限伤; elite to the board; 假想敌:胄 kit
- co-op leaders died within seconds: leader HP is the official data (40 values pinned); bond / strategy / item / 机变 / 特质 "+X%" bonuses now add up (PRTS 直接乘算) instead of compounding; a same-named enemy debuff keeps one (strongest) instance per target (奥术 across a pair field); leader parts pass damage 1:1; fixed co-op pool (the user's call) - official limits read from the client: each bond stops at 999 layers (MAX_GARRISON_STACK); a leader hit of 300000+ in boss / hidden battles is cancelled (MAX_BATTLE_DAMAGE, 限伤) — shared/constants.js switches, research 11 - a merge that consumes a deployed copy puts the elite on that tile and facing (user-confirmed); gold target highlight, promotion effect - 假想敌:胄 per PRTS: one shell, shells within range, any drone death costs 2 % max HP, 剑/锤 sorties and 瘫痪 - DESIGN §20.10–§20.14; 2842 unit tests, all 28 browser suites green; Chrome = Node on boss fields
This commit is contained in:
1 parent
6464e4b46a
commit
620f88c0d8
81 files changed
+4194
-447
No files matched your search
+48
-13
@@ -22,12 +22,12 @@
|
||||
// battle continues. After MAX_INTERNAL_ERRORS the battle force-ends as a timeout.
|
||||
|
||||
import { TICK, ROWS, COLS, BLOCK_RADIUS_SQ, DP_DEFAULTS, DOWN_STATE, FORCED_EXIT, MAX_BATTLE_TIME, MAX_INTERNAL_ERRORS, COLD_FREEZE_DURATION, OBSTACLE_DEVICES, EVENT_BUFFER_CAP, BOSS_ROW_OFFSET, MAX_HOOK_DEPTH, MAX_ALIVE_ENEMIES, LEVITATE_HALF_WEIGHT, RESIST_DEFAULT, RESIST_PALSY_DECAY, PUSH_TILES, PUSH_TILES_EFFECT, PULL_WEAK_SHARE, PULL_CRAWL, PULL_ORIGIN, PULL_STOP_RADIUS, PUSH_DIRECTIONAL_MIN_DIST, AUTO_OP_COOLDOWN } from './constants.js';
|
||||
import { GEO } from '../../shared/constants.js';
|
||||
import { GEO, layerGainRoom } from '../../shared/constants.js';
|
||||
import { createRng } from './rng.js';
|
||||
import { Grid } from './grid.js';
|
||||
import { Unit } from './units.js';
|
||||
import { makeBuff, STATUS, RESIST_STATUSES } from './buffs.js';
|
||||
import { dealDamage as pipeDamage, heal as pipeHeal, applyHpLoss, makeDamageInfo, reduceElement, palsyBuff, elementView } from './damage.js';
|
||||
import { dealDamage as pipeDamage, heal as pipeHeal, applyHpLoss, makeDamageInfo, reduceElement, palsyBuff, elementView, leaderHitCancelled } from './damage.js';
|
||||
import { absoluteRangeKeys, canTargetEnemy } from './targeting.js';
|
||||
import { bodyKeys, bodyInKeys, bodyInRadius } from './body.js';
|
||||
import { normDir, mirrorDir, localOrder, localBefore } from './dir.js';
|
||||
@@ -1351,19 +1351,39 @@ export class Battle {
|
||||
buff.data = { ...buff.data, attract: { r, c, pts: null, i: 0, ver: -1 } };
|
||||
}
|
||||
|
||||
/** "同名效果取最高": keep the strongest value; a weaker one that outlasts it resumes afterwards (buff.data.tail). */
|
||||
/**
|
||||
* "同名效果取最高" for a content effect that is not a catalogue status (no immunities, 抵抗, `beforeStatus` /
|
||||
* `statusApplied` hooks or status icon): ONE instance of `key` per target whatever the number of sources — the
|
||||
* strongest `value` applies, a weaker application only extends past the stronger one's end (it then resumes), an
|
||||
* equal one refreshes to the longer duration. `mods(value)` builds the buff's mods. The engine's default for two
|
||||
* same-named buffs (PRTS 作战机制 "同名buff的默认叠加策略buff只能表现出一个"). Returns true when applied.
|
||||
* @param {object} target
|
||||
* @param {string} key
|
||||
* @param {{ duration: number, value: number, mods: (v: number) => object, source?: object|null }} opts
|
||||
*/
|
||||
applyStrongest(target, key, { duration, value, mods, source = null } = {}) {
|
||||
if (!target || !target.alive || !(Number(duration) > 0) || !Number.isFinite(value) || typeof mods !== 'function') return false;
|
||||
this._applyValuedStatus(target, key, { mods, valued: value, plain: true }, Number(duration), value, source);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* "同名效果取最高": keep the strongest value; a weaker one that outlasts it resumes afterwards (buff.data.tail). A
|
||||
* `plain` template (applyStrongest) is an ordinary invisible buff, not a status.
|
||||
*/
|
||||
_applyValuedStatus(target, key, tpl, duration, value, source) {
|
||||
const strength = (v) => Math.abs(Number.isFinite(v) ? v : tpl.valued);
|
||||
const make = (v, dur, tail) => ({
|
||||
...(tpl.buff || null), // extra buff fields of the status (抵抗: the 麻痹 decay tick)
|
||||
key, duration: dur, refresh: 'replace', mods: tpl.mods(v), flags: tpl.flags || null, status: key, visible: true, source,
|
||||
key, duration: dur, refresh: 'replace', mods: tpl.mods(v), flags: tpl.flags || null, status: tpl.plain ? null : key,
|
||||
visible: !tpl.plain, source,
|
||||
data: { value: v, tail },
|
||||
onExpire: ({ battle, unit, buff }) => {
|
||||
const t = buff.data.tail;
|
||||
if (t && t.until - battle.time > 1e-6 && unit.alive) battle.addBuff(unit, make(t.value, t.until - battle.time, null));
|
||||
},
|
||||
});
|
||||
const old = target.buffs.find((b) => b.key === key && b.status === key);
|
||||
const old = target.buffs.find((b) => b.key === key && (tpl.plain || b.status === key));
|
||||
if (!old) { this.addBuff(target, make(value, duration, null)); return; }
|
||||
const oldV = old.data && Number.isFinite(old.data.value) ? old.data.value : tpl.valued;
|
||||
const oldEnd = this.time + old.timeLeft, newEnd = this.time + duration;
|
||||
@@ -1400,12 +1420,17 @@ export class Battle {
|
||||
* DamageInfo this loss derives from (damage passed on to a leader, split, shared…): its tags are inherited and it is
|
||||
* kept as `dmg.origin`, so `damaged` handlers that skip their own tagged damage also skip what it turned into; a loss
|
||||
* derived from 无来源 damage (`from.sourceless`, element bursts) is 无来源 too (hooks see no source, `source` is credited).
|
||||
* `sourceless: true` makes the loss itself 无来源 ("受到等量的无来源生命流失": hooks see no source; `source` keeps the
|
||||
* credit — the stats and the per-player shared-pool tally).
|
||||
* On a leader in a boss / hidden battle a loss of ≥ BOSS_HIT_LIMIT is cancelled like a hit (damage.js leaderHitCancelled).
|
||||
*/
|
||||
loseHp(target, amount, { source = null, silent = false, tags = null, from = null } = {}) {
|
||||
loseHp(target, amount, { source = null, silent = false, tags = null, from = null, sourceless = false } = {}) {
|
||||
if (!target || !target.alive || !(amount > 0)) return 0;
|
||||
// 限伤 (shared/constants.js BOSS_HIT_LIMIT): a loss passed on to a leader (parts' 传递, 无人机) is one hit too
|
||||
if (leaderHitCancelled(this, target, amount)) return 0;
|
||||
const t = ['hpLoss'];
|
||||
for (const list of [from && from.tags, tags]) if (Array.isArray(list)) for (const x of list) if (!t.includes(x)) t.push(x);
|
||||
return applyHpLoss(this, source, target, amount, { type: 'true', tags: t, noSp: true, silent, origin: from ?? null, sourceless: !!(from && from.sourceless) });
|
||||
return applyHpLoss(this, source, target, amount, { type: 'true', tags: t, noSp: true, silent, origin: from ?? null, sourceless: !!sourceless || !!(from && from.sourceless) });
|
||||
}
|
||||
|
||||
reduceElement(target, amount, element = null) { return reduceElement(target, amount, element); }
|
||||
@@ -1966,19 +1991,29 @@ export class Battle {
|
||||
return ps.dp;
|
||||
}
|
||||
|
||||
/** Record an IN_BATTLE layer gain (no-op when gains are disabled). Returns the layers added. */
|
||||
/**
|
||||
* Record an IN_BATTLE layer gain (no-op when gains are disabled). Returns the layers added: at most the room left
|
||||
* under BOND_LAYER_CAP (999, shared/constants.js) on the live copy — the client's AddBondCount `min(L + n, 999)`; the
|
||||
* `layerGain` hook (魔王's +1 …) runs first, then the clamp; a bond already at the cap gains 0 (no hook, no event).
|
||||
* Without a live copy of the bond (a partial PlayerBattleInput) the battle's own gains count; the match's settle
|
||||
* clamps the persistent count the same way.
|
||||
*/
|
||||
addLayers(playerId, bondId, n, reason = '', opts = {}) {
|
||||
if (!this.flags.layerGainsEnabled || !(n > 0) || !Number.isFinite(n) || playerId == null) return 0;
|
||||
const pp = this._pp(playerId);
|
||||
if (!pp) return 0;
|
||||
const ps = this.getPlayer(playerId);
|
||||
const live = ps && ps.bonds[bondId] ? (ps.bonds[bondId].layers ?? 0) : (pp.layerGains[bondId] ?? 0);
|
||||
if (!(layerGainRoom(live, Infinity) > 0)) return 0;
|
||||
const source = opts.source ?? null;
|
||||
const ctx = { playerId, bondId, n, reason, source, tile: Array.isArray(opts.tile) ? opts.tile : this._sourceTile(source) };
|
||||
if (this._hooks.layerGain) { this.emit('layerGain', ctx); if (!(ctx.n > 0) || !Number.isFinite(ctx.n)) return 0; }
|
||||
pp.layerGains[bondId] = (pp.layerGains[bondId] ?? 0) + ctx.n;
|
||||
const ps = this.getPlayer(playerId);
|
||||
if (ps && ps.bonds[bondId]) ps.bonds[bondId].layers = (ps.bonds[bondId].layers ?? 0) + ctx.n;
|
||||
this._ev(['layer', playerId, bondId, ctx.n]);
|
||||
return ctx.n;
|
||||
const add = layerGainRoom(live, ctx.n);
|
||||
if (!(add > 0)) return 0;
|
||||
pp.layerGains[bondId] = (pp.layerGains[bondId] ?? 0) + add;
|
||||
if (ps && ps.bonds[bondId]) ps.bonds[bondId].layers = (ps.bonds[bondId].layers ?? 0) + add;
|
||||
this._ev(['layer', playerId, bondId, add]);
|
||||
return add;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -200,3 +200,17 @@ export const BOSS_ROW_OFFSET = -7;
|
||||
* #6 item 5). With the rule a pool always reads 0 or at least 1 HP: 0 on the HUD = the leader is down.
|
||||
*/
|
||||
export const BOSS_POOL_MIN_HP = 1;
|
||||
|
||||
/**
|
||||
* How the percentage attribute bonuses (ATK / DEF / max HP "+X%") of the 卫戍 systems combine: 盟约, 策略, 装备, the
|
||||
* 机变 cards and the per-layer 特质 (garrisons' 每叠加N层 …+X%). Official: PRTS 卫戍协议:盟约 下半/PRTS盟约记录 "盟约效果,
|
||||
* 策略效果,装备效果提供的属性加成均为直接乘算"; 直接乘算 is the modifier class whose values are SUMMED with every other
|
||||
* 直接乘算 — a skill's "攻击力+X%" included — before multiplying (PRTS 游戏数据基础 属性基本公式 "直接乘算结果 D_t = t₁ + t₂ +
|
||||
* … + tₙ", A = (A₀ + D_p)(1 + D_t); PRTS 作战机制 X₂ = X₁(1 + b₁% + b₂%)). 'add' = that rule: the engine's additive
|
||||
* atkPct / defPct / hpPct (content/support directMods). 'multiply' = the v2.5 reading (each source its own ×(1 + x)
|
||||
* atkMul / defMul / hpMul), which compounded: at 600 精准 layers a ranged operator with four more such bonuses dealt
|
||||
* ×74 instead of ×12 and Final Assault leaders fell in seconds (user report after playtest #6). "提升至X%/X倍" effects
|
||||
* (炎佑 ×1.5, 攻击海怪敌人时攻击力提升至150%) and the char_attribute_mul 特质 (+20 % / +25 % on the chess itself, a rune on
|
||||
* the base attributes) stay multipliers.
|
||||
*/
|
||||
export const DIRECT_BONUS_STACKING = 'add';
|
||||
@@ -4,8 +4,8 @@
|
||||
// (numbers from the blackboard, never hard-coded). Band buffs apply in every battle kind (normal / 联防 / boss /
|
||||
// hidden); IN_BATTLE layer gains are no-ops where the match disables them (support.gainLayers).
|
||||
//
|
||||
// act1autochess_band2_buff 阿米娅 众志合一 ≥ value_i active bonds (highest i) ⇒ every operator ATK × (1 + atk_i),
|
||||
// HP × (1 + max_hp_i) (直接乘算, bonds snapshot at combat start)
|
||||
// act1autochess_band2_buff 阿米娅 众志合一 ≥ value_i active bonds (highest i) ⇒ every operator ATK +atk_i,
|
||||
// HP +max_hp_i (直接乘算 — support directMods —, bonds snapshot at combat start)
|
||||
// act1autochess_band28_buff 埃芒加德 命结之秘 the first max_respawn_cnt operator knock-downs of the battle revive at
|
||||
// once at full HP (runs after every other death saver, items included)
|
||||
// act1autochess_band13_buff 克莱门莎 崇高牺牲 a <bond_id> operator knocked down ⇒ +its tier (等阶) <bond_id> layers
|
||||
@@ -18,13 +18,13 @@
|
||||
// act1autochess_band18_buff 休谟斯 回收利用 a ground (地面) operator's skill ends ⇒ a random operator on its 4
|
||||
// neighbouring tiles gains `sp` SP
|
||||
// act1autochess_band15_buff 卡莱莎 食腐之蝶 an operator knocked down ⇒ every other unit of the player on the field
|
||||
// ATK × (1 + atk × n), n ≤ max_stack_cnt (+20 % … +200 %, additive stacks),
|
||||
// ATK +atk × n, n ≤ max_stack_cnt (+20 % … +200 %, additive stacks),
|
||||
// until that unit is knocked down itself or the battle ends
|
||||
// act1autochess_band19_buff 陈 以己之长 the player's operators' physical / arts damage becomes 弱点伤害
|
||||
// act2autochess_band12_buff 夕 墨色真颜 at combat start operators of which ≥ 2 of the same name (same base chess,
|
||||
// elite or not) stand on the field get ATK × (1 + atk)
|
||||
// elite or not) stand on the field get ATK +atk
|
||||
// act2autochess_band3_buff 伊奥莱塔 统御号令 at combat start n = elite operators on the field ⇒ every elite operator
|
||||
// ATK × (1 + atk_per_cnt × n), HP × (1 + max_hp_per_cnt × n)
|
||||
// ATK +atk_per_cnt × n, HP +max_hp_per_cnt × n (all 直接乘算)
|
||||
// auto_chess_change_map Touch 外勤医疗 "所有玩家场地上出现一名<预备干员-医疗>": while any alive player of the match
|
||||
// holds band_amedic, every player's field gets the map character of the
|
||||
// variant whose common_condition holds for that player's elites on the field
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
import {
|
||||
num, buffsOf, bandRecord, isOp, onField, isElite, tierOf, unitBonds, activeBondIds, playerOps, passiveBuff, fxOn,
|
||||
matchBands, gainLayers, alliesAround, N4, baseChessId, isGroundOp,
|
||||
matchBands, gainLayers, alliesAround, N4, baseChessId, isGroundOp, directMods,
|
||||
} from '../support/index.js';
|
||||
import { weaknessRetype, addShieldLayer, PRIO_REVIVE } from '../items/battle.js';
|
||||
import { spawnMapChar } from '../tokens.js';
|
||||
@@ -58,9 +58,7 @@ const BY_KEY = {
|
||||
if (n >= num(p[`value_${i}`], Infinity)) { atk = num(p[`atk_${i}`]); hp = num(p[`max_hp_${i}`]); }
|
||||
}
|
||||
if (!(atk > 0 || hp > 0)) return;
|
||||
const mods = {};
|
||||
if (atk) mods.atkMul = 1 + atk;
|
||||
if (hp) mods.hpMul = 1 + hp;
|
||||
const mods = directMods({ atk, hp });
|
||||
for (const u of playerOps(battle, ps.playerId)) passiveBuff(battle, u, keyOf(bandId), mods);
|
||||
},
|
||||
|
||||
@@ -157,7 +155,7 @@ const BY_KEY = {
|
||||
if (a === d) continue;
|
||||
const cur = a.findBuff(key);
|
||||
const n = Math.min(cap, (cur && cur.data ? cur.data.n : 0) + 1);
|
||||
battle.addBuff(a, { key, mods: { atkMul: 1 + step * n }, refresh: 'replace', data: { n }, visible: true });
|
||||
battle.addBuff(a, { key, mods: directMods({ atk: step * n }), refresh: 'replace', data: { n }, visible: true });
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -184,7 +182,7 @@ const BY_KEY = {
|
||||
}
|
||||
for (const list of groups.values()) {
|
||||
if (list.length < 2) continue;
|
||||
for (const u of list) passiveBuff(battle, u, keyOf(bandId), { atkMul: 1 + atk });
|
||||
for (const u of list) passiveBuff(battle, u, keyOf(bandId), directMods({ atk }));
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -197,9 +195,7 @@ const BY_KEY = {
|
||||
const elites = deployedOps(battle, ps.playerId).filter(isElite);
|
||||
const n = elites.length;
|
||||
if (!n) return;
|
||||
const mods = {};
|
||||
if (a) mods.atkMul = 1 + a * n;
|
||||
if (h) mods.hpMul = 1 + h * n;
|
||||
const mods = directMods({ atk: a * n, hp: h * n });
|
||||
for (const u of elites) passiveBuff(battle, u, keyOf(bandId), mods);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
// server/sim/content/bonds/addon/battle.js — battle side of the 15 add-on bonds (research 02 §3.9–§3.23).
|
||||
//
|
||||
// Every number comes from data/bonds.json (`env_gbuff_new` buff blackboards). Effects only apply to the owning
|
||||
// Every number comes from data/bonds.json (`env_gbuff_new` buff blackboards). ATK / DEF / max HP "+X%" are 直接乘算
|
||||
// (support directMods: additive with every other percentage, PRTS 盟约记录). Effects only apply to the owning
|
||||
// player's units (`unit.ownerId === playerId`): two players sharing a boss / 联防 field never buff each other.
|
||||
// Layer-dependent values read the LIVE layers (`battle.getPlayer(pid).bonds[id].layers`): an IN_BATTLE layer gain
|
||||
// (engine `layerGain` hook, which fires before the layers are written) schedules one deferred recompute per player.
|
||||
//
|
||||
// 精准 preciShip members ATK ×(1 + base_atk + atk_per_stack·L); tier 2 (3 distinct): members + RANGED operators,
|
||||
// 精准 preciShip members ATK +(base_atk + atk_per_stack·L); tier 2 (3 distinct): members + RANGED operators,
|
||||
// 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
|
||||
// 奥术 arcaneShip member arts damage → target arts taken ×(base + per·L) for weak_duration s (one refreshing instance
|
||||
// per player); tier 2: ×power_weak_scale when the target is below hp_ratio at application
|
||||
// 坚守 steadShip all operators max HP ×(1 + base + per·L); tier 2: 40 % of a non-member operator's damage is borne by
|
||||
// 奥术 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, 同名效果取最高:
|
||||
// PRTS 作战机制 "同名buff的默认叠加策略buff只能表现出一个"; 巴哈姆特 12316 first-hand: "共享型buff會跟對面搶
|
||||
// 如果對面層數比你高就不需要再特別激活直接吃他的奧術buff"). v2.5 kept one per player, so two players'
|
||||
// instances multiplied (×5.4 × ×5.6 on a leader at ~250 layers, DESIGN §20.10)
|
||||
// 坚守 steadShip all operators max HP +(base + per·L); tier 2: 40 % of a non-member operator's damage is borne by
|
||||
// the members on the field (split evenly, sourceless true damage — already mitigated), members’ thorns
|
||||
// (base + per·L arts, sourceless, ≤ 1 per cd_duration per member) + 脆弱 ×damage_scale for weak[limit] s
|
||||
// 助力 deputShip all operators DEF ×(1 + base + per·L), redeploy time ×(1 + respawn_time)
|
||||
// (base + per·L arts, sourceless but credited to the member hit, ≤ 1 per cd_duration per member)
|
||||
// + 脆弱 ×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) with no enemy in range → "保留技力立即再部署"
|
||||
// next to the most advanced ground enemy: a real redeployment (retreat + free redeploy on the
|
||||
// landing tile, full HP, `deploy` fires — 部署时 traits such as 史尔特尔, 突袭手雷, 卡西米尔, 叙拉古)
|
||||
// with its SP / charges kept (engine redeploy tile + keepSp); ATK/HP ×(1 + base + per·L)
|
||||
// with its SP / charges kept (engine redeploy tile + keepSp); ATK/HP +(base + per·L)
|
||||
// until it leaves the field; later redeploys use its board tile again;
|
||||
// L ≥ power_bond_stack_cnt: every operator ASPD +power_attack_speed
|
||||
// 不屈 indomShip ground operator knocked out → p = min(1, base + per·L) immediate free redeploy; tier 2: every
|
||||
// operator on the field +sp SP
|
||||
// 协防 emptyShip all operators phys/arts taken ×(1 − damage_resistance); members dealt ×damage_scale_normal
|
||||
// (elite ×damage_scale_extra)
|
||||
// 独行 soloShip the member(s) ATK ×(1 + atk), HP ×(1 + max_hp), +sp SP on every deploy
|
||||
// 绝技 suntShip tier 1: elite operators ATK ×(1 + power_atk); tier 2: elites (and their summons) SP cost ×sp_ratio
|
||||
// 独行 soloShip the member(s) ATK +atk, HP +max_hp, +sp SP on every deploy
|
||||
// 绝技 suntShip tier 1: elite operators ATK +power_atk; tier 2: elites (and their summons) SP cost ×sp_ratio
|
||||
// 远见 / 奇迹 / 投资人 / 调和 have no battle effect (prep side, membership in support/index.js).
|
||||
//
|
||||
// Hooks registered only when a bond needs them. Priorities: `hit` −20 (坚守 redirect, after other modifiers had their
|
||||
@@ -37,7 +43,7 @@ import { absoluteRangeKeys, canTargetEnemy } from '../../../targeting.js';
|
||||
import { localOrder, localBefore } from '../../../dir.js';
|
||||
import {
|
||||
num, bondRecord, buffParams, bondTier, bondLayers, isMember, isElite, isGroundOp, onField, playerOps, passiveBuff,
|
||||
fxOn, N4, N8, bodyInKeys,
|
||||
fxOn, N4, N8, bodyInKeys, directMods,
|
||||
} from '../../support/index.js';
|
||||
|
||||
export const ID = Object.freeze({
|
||||
@@ -56,6 +62,9 @@ const AURA_POLL = 0.25;
|
||||
const RAID_POLL = 0.25;
|
||||
const RAID_SEARCH = 2; // landing tiles within this Chebyshev distance of the target enemy
|
||||
|
||||
/** 奥术 vulnerability mods for a multiplier (battle.applyStrongest). */
|
||||
const arcaneMods = (v) => ({ artsTakenMul: v });
|
||||
|
||||
/** `env_gbuff_new` blackboard of a bond (numbers only). */
|
||||
export function bondBb(bondId) {
|
||||
return buffParams(bondRecord(bondId), 'env_gbuff_new') ?? {};
|
||||
@@ -90,7 +99,7 @@ function applyPassives(battle, st) {
|
||||
if (t[ID.preci]) {
|
||||
const bb = st.bb[ID.preci];
|
||||
const wide = t[ID.preci] >= 2;
|
||||
const mods = { atkMul: 1 + num(bb.base_atk) + num(bb.atk_per_stack) * L(battle, st, ID.preci) };
|
||||
const mods = directMods({ atk: num(bb.base_atk) + num(bb.atk_per_stack) * L(battle, st, ID.preci) });
|
||||
if (wide) { mods.defIgnorePct = num(bb.power_def_penetrate); mods.resIgnorePct = num(bb.power_magic_resist_penetrate); }
|
||||
for (const u of st.ops) {
|
||||
if (st.members[ID.preci].has(u) || (wide && u.def?.position === 'RANGED')) passiveBuff(battle, u, KEY.preci, mods);
|
||||
@@ -98,12 +107,12 @@ function applyPassives(battle, st) {
|
||||
}
|
||||
if (t[ID.stead]) {
|
||||
const bb = st.bb[ID.stead];
|
||||
const mods = { hpMul: 1 + num(bb.base_max_hp) + num(bb.max_hp_per_stack) * L(battle, st, ID.stead) };
|
||||
const mods = directMods({ hp: num(bb.base_max_hp) + num(bb.max_hp_per_stack) * L(battle, st, ID.stead) });
|
||||
for (const u of st.ops) passiveBuff(battle, u, KEY.stead, mods);
|
||||
}
|
||||
if (t[ID.deput]) {
|
||||
const bb = st.bb[ID.deput];
|
||||
const mods = { defMul: 1 + num(bb.base_def) + num(bb.def_per_stack) * L(battle, st, ID.deput), redeployMul: Math.max(0, 1 + num(bb.respawn_time)) };
|
||||
const mods = directMods({ def: num(bb.base_def) + num(bb.def_per_stack) * L(battle, st, ID.deput) }, { redeployMul: Math.max(0, 1 + num(bb.respawn_time)) });
|
||||
for (const u of st.ops) passiveBuff(battle, u, KEY.deput, mods);
|
||||
}
|
||||
if (t[ID.raid]) {
|
||||
@@ -124,8 +133,8 @@ function applyPassives(battle, st) {
|
||||
if (t[ID.arcane]) {
|
||||
const bb = st.bb[ID.arcane];
|
||||
const m = num(bb.base_damage_scale, 1) + num(bb.damage_scale_per_stack) * L(battle, st, ID.arcane);
|
||||
st.arcaneMods = { artsTakenMul: Math.max(0, m) };
|
||||
st.arcaneLowMods = { artsTakenMul: Math.max(0, m * num(bb.power_weak_scale, 1)) };
|
||||
st.arcaneMul = Math.max(0, m);
|
||||
st.arcaneLowMul = Math.max(0, m * num(bb.power_weak_scale, 1));
|
||||
}
|
||||
if (t[ID.skillful]) {
|
||||
const bb = st.bb[ID.skillful];
|
||||
@@ -137,10 +146,10 @@ function applyPassives(battle, st) {
|
||||
}
|
||||
|
||||
function raidMods(bb, lv) {
|
||||
const mods = {
|
||||
atkMul: 1 + num(bb.base_atk) + num(bb.atk_per_stack) * lv,
|
||||
hpMul: 1 + num(bb.base_max_hp) + num(bb.max_hp_per_stack) * lv,
|
||||
};
|
||||
const mods = directMods({
|
||||
atk: num(bb.base_atk) + num(bb.atk_per_stack) * lv,
|
||||
hp: num(bb.base_max_hp) + num(bb.max_hp_per_stack) * lv,
|
||||
});
|
||||
const aspd = num(bb.base_attack_speed) + num(bb.attack_speed_per_stack) * lv;
|
||||
if (aspd) mods.aspd = aspd;
|
||||
return mods;
|
||||
@@ -161,12 +170,12 @@ function applyStatic(battle, st) {
|
||||
}
|
||||
if (t[ID.solo]) {
|
||||
const bb = st.bb[ID.solo];
|
||||
const mods = { atkMul: 1 + num(bb.atk), hpMul: 1 + num(bb.max_hp) };
|
||||
const mods = directMods({ atk: num(bb.atk), hp: num(bb.max_hp) });
|
||||
for (const u of st.members[ID.solo]) passiveBuff(battle, u, KEY.solo, mods);
|
||||
}
|
||||
if (t[ID.sunt]) {
|
||||
const bb = st.bb[ID.sunt];
|
||||
const mods = { atkMul: 1 + num(bb.power_atk) };
|
||||
const mods = directMods({ atk: num(bb.power_atk) });
|
||||
for (const u of st.ops) {
|
||||
if (!isElite(u)) continue;
|
||||
passiveBuff(battle, u, KEY.sunt, mods);
|
||||
@@ -438,9 +447,12 @@ export function install(battle) {
|
||||
t.mem[STEAD_CD] = battle.time;
|
||||
const src = c.source;
|
||||
if (!src || src.side !== 'enemy' || !src.alive) return;
|
||||
const d = battle.makeDamage({ amount: num(bb.base_damage_value) + num(bb.damage_value_per_stack) * L(battle, st, ID.stead), type: 'arts', canDodge: false });
|
||||
// "伤害来源受到(850+10×L)点法术伤害": 无来源 (no attacker multipliers, hooks see no source) but credited to the
|
||||
// member hit — its player's stats and shared-pool tally count it, like an element burst (v2.5: no credit at all,
|
||||
// up to 19 % of a co-op leader's pool and 88 % of a solo one went unattributed, DESIGN §20.10)
|
||||
const d = battle.makeDamage({ amount: num(bb.base_damage_value) + num(bb.damage_value_per_stack) * L(battle, st, ID.stead), type: 'arts', canDodge: false, sourceless: true });
|
||||
d.steadThorn = true;
|
||||
battle.dealDamage(null, src, d);
|
||||
battle.dealDamage(t, src, d);
|
||||
if (src.alive) battle.applyStatus(src, 'fragile', { duration: num(bb['weak[limit]'], 5), value: num(bb.damage_scale, 1) - 1, source: t });
|
||||
return;
|
||||
}
|
||||
@@ -451,17 +463,8 @@ export function install(battle) {
|
||||
if (!st || !st.tiers[ID.arcane] || !st.members[ID.arcane].has(src)) return;
|
||||
const bb = st.bb[ID.arcane];
|
||||
const low = st.tiers[ID.arcane] >= 2 && t.hpRatio < num(bb.hp_ratio);
|
||||
const mods = low ? st.arcaneLowMods : st.arcaneMods;
|
||||
const key = st.arcaneKey ?? (st.arcaneKey = `${KEY.arcane}:${st.pid}`);
|
||||
const dur = num(bb.weak_duration, 3);
|
||||
const b = t.findBuff(key);
|
||||
if (b) {
|
||||
b.timeLeft = dur;
|
||||
b.duration = dur;
|
||||
if (b.mods !== mods) { b.mods = mods; t.markDirty(); }
|
||||
} else {
|
||||
battle.addBuff(t, { key, duration: dur, mods });
|
||||
}
|
||||
// one 奥术 instance per target, the strongest (both players of a pair field included): see the header
|
||||
battle.applyStrongest(t, KEY.arcane, { duration: num(bb.weak_duration, 3), value: low ? st.arcaneLowMul : st.arcaneMul, mods: arcaneMods, source: src });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
// server/sim/content/bonds/core.js — the 8 core bonds (核心盟约, research 02 §3.1–3.8), battle side + prep side.
|
||||
//
|
||||
// 炎 yanShip members ATK ×(1 + base_atk + atk_per_stack·L); 6: one “炎佑” (its template ATK / max HP + 30 %
|
||||
// 炎 yanShip members ATK +(base_atk + atk_per_stack·L); 6: one “炎佑” (its template ATK / max HP + 30 %
|
||||
// of the 炎 ATK / max HP sums at battle start — PRTS "增加…(最终加算)"); 9: two 炎佑, ATK ×atk,
|
||||
// damage taken ×(1 − damage_resistance)
|
||||
// (every bond "+X%" ATK / max HP here is 直接乘算 — S.directMods, additive with the other percentages, PRTS 盟约记录)
|
||||
// 萨尔贡 sargonShip member skill start → every member on the field gets an independent stack (ASPD +base_attack_speed
|
||||
// for base_time + time_per_stack·L s, ≤ max_buff_stack_cnt); 6: each stack also ATK +base_atk
|
||||
// (additive, one atkMul); 6 + band_narant: instead lends the caster's equipment (tier ≤
|
||||
// (additive, one 直接乘算 buff); 6 + band_narant: instead lends the caster's equipment (tier ≤
|
||||
// filter_item_level) to the 8 surrounding operators (items.js lendItemEffects)
|
||||
// 维多利亚 victoriaShip members carrying equipment deal ×(base_damage_scale + damage_scale_per_stack·L);
|
||||
// 6: ATK +atk_normal_equip per item (+atk_golden_equip more per golden item);
|
||||
// 6: ATK +atk_normal_equip per item (+atk_golden_equip more per golden item; 直接乘算);
|
||||
// prep: every `layer` layers → `count` item(s) from `pool` (bond_layer_added_reward_equip)
|
||||
// 谢拉格 kjeragShip members deal ×base_damage_scale, vs cold / frozen enemies ×(base_ex_damage_scale +
|
||||
// ex_damage_scale_per_stack·L) instead; 6: cold wind (devices.js kjeragColdWind)
|
||||
// 拉特兰 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 ×(1 + base_max_hp + max_hp_per_stack·L); battle start devour (see devour());
|
||||
// 阿戈尔 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 knocked out for the first time redeploy at once (free)
|
||||
// 叙拉古 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, attacks proc (PRD, nominal `prob`) base_damage +
|
||||
@@ -176,8 +177,8 @@ const ownTag = (dmg, tag) => !!dmg && Array.isArray(dmg.tags) && dmg.tags.includ
|
||||
function installYan(battle, pid, bb, members) {
|
||||
const L = () => S.bondLayers(battle, pid, 'yanShip');
|
||||
const apply = () => {
|
||||
const mul = 1 + num(bb.base_atk) + num(bb.atk_per_stack) * L();
|
||||
for (const u of members) S.passiveBuff(battle, u, 'bond:yan', { atkMul: mul });
|
||||
const mods = S.directMods({ atk: num(bb.base_atk) + num(bb.atk_per_stack) * L() });
|
||||
for (const u of members) S.passiveBuff(battle, u, 'bond:yan', mods);
|
||||
};
|
||||
apply();
|
||||
onLayers(battle, pid, 'yanShip', apply);
|
||||
@@ -220,7 +221,7 @@ function installSargon(battle, pid, bb, members) {
|
||||
if (!atkStacks) return;
|
||||
let n = 0;
|
||||
for (const b of u.buffs) if (b.key === 'bond:sargon') n++;
|
||||
if (n > 0 && u.alive) battle.addBuff(u, { key: 'bond:sargon:atk', mods: { atkMul: 1 + perAtk * n } });
|
||||
if (n > 0 && u.alive) battle.addBuff(u, { key: 'bond:sargon:atk', mods: S.directMods({ atk: perAtk * n }) });
|
||||
else battle.removeBuff(u, 'bond:sargon:atk');
|
||||
};
|
||||
battle.on('skillStart', ({ unit }) => {
|
||||
@@ -278,7 +279,7 @@ function installVictoria(battle, pid, bb, members) {
|
||||
if (!ids.length) continue;
|
||||
let add = 0;
|
||||
for (const id of ids) add += S.isGoldenId(id) ? golden : normal;
|
||||
if (add > 0) S.passiveBuff(battle, u, 'bond:victoria:atk', { atkMul: 1 + add });
|
||||
if (add > 0) S.passiveBuff(battle, u, 'bond:victoria:atk', S.directMods({ atk: add }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +321,7 @@ function installLaterano(battle, pid, bb, members) {
|
||||
if (!memberSet.has(unit) || st.bonus >= cap) return;
|
||||
st.used++;
|
||||
st.bonus = Math.min(cap, st.used * per);
|
||||
for (const m of members) S.passiveBuff(battle, m, 'bond:laterano:ammo', { atkMul: 1 + st.bonus });
|
||||
for (const m of members) S.passiveBuff(battle, m, 'bond:laterano:ammo', S.directMods({ atk: st.bonus }));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -391,8 +392,8 @@ function devour(battle, pid, bb, members) {
|
||||
|
||||
function installEgir(battle, pid, bb, members) {
|
||||
const apply = () => {
|
||||
const mul = 1 + num(bb.base_max_hp, 0) + num(bb.max_hp_per_stack, 0) * S.bondLayers(battle, pid, 'egirShip');
|
||||
for (const u of members) S.passiveBuff(battle, u, 'bond:egir', { hpMul: mul });
|
||||
const mods = S.directMods({ hp: num(bb.base_max_hp, 0) + num(bb.max_hp_per_stack, 0) * S.bondLayers(battle, pid, 'egirShip') });
|
||||
for (const u of members) S.passiveBuff(battle, u, 'bond:egir', mods);
|
||||
};
|
||||
apply();
|
||||
onLayers(battle, pid, 'egirShip', apply);
|
||||
@@ -464,7 +465,7 @@ function installKazimierz(battle, pid, bb, members) {
|
||||
const cap = num(bb.base_max_atk_when_born, 0) + num(bb.max_atk_when_born_per_stack, 0) * S.bondLayers(battle, pid, 'kazimierzShip');
|
||||
const bonus = Math.max(0, Math.min(cap, st.deploys * num(bb.atk_when_born, 0)));
|
||||
if (!(bonus > 0)) return;
|
||||
for (const u of members) S.passiveBuff(battle, u, 'bond:kazimierz', { atkMul: 1 + bonus });
|
||||
for (const u of members) S.passiveBuff(battle, u, 'bond:kazimierz', S.directMods({ atk: bonus }));
|
||||
};
|
||||
battle.on('deploy', ({ unit }) => {
|
||||
if (!S.isOp(unit) || unit.ownerId !== pid) return;
|
||||
|
||||
+150
-50
@@ -2,8 +2,8 @@
|
||||
//
|
||||
// install(battle) ensures the enemy dispatch hooks (enemies.js) and attaches a boss kit to every spawned leader / part
|
||||
// listed in BOSS_KITS. Damage dealt TO a leader routes to the shared boss pool through the engine (tag 'boss' +
|
||||
// sharedBoss); parts that "传递伤害" push a share of the damage they take into the pool with battle.loseHp(leader, …)
|
||||
// credited to the attacker. Numbers come from the (template-overridden) talent / skill blackboards; the rest are named
|
||||
// sharedBoss); parts that "传递伤害" push the damage they take into the pool with a 无来源 battle.loseHp(leader, …)
|
||||
// credited to the attacker (PART_TRANSFER = 1, "等量"). Numbers come from the (template-overridden) talent / skill blackboards; the rest are named
|
||||
// constants ([ASSUMED] or PRTS-sourced). Boss HP rules ("<20 %", "≤50 %") read the leader's HP ratio, which the engine
|
||||
// syncs from the shared pool, so both mirrored copies switch phase together.
|
||||
//
|
||||
@@ -17,18 +17,23 @@
|
||||
// (skipped if the match already spawned a second copy, or opts.bossMirror === false).
|
||||
//
|
||||
// Leaders (see each kit for details):
|
||||
// boss_1/8 假想敌:胄 random arts beam (range 8); 灭顶之灾 刺胄之弹 at the highest-ATK operator (8-hit flying shell,
|
||||
// 3×3 stun + phys DoT on arrival); <20 %: damage ×0.5 + 2 shells; 死亡集群 drones — a drone
|
||||
// killed by an operator costs the leader 2 % max HP. boss_8 adds 斩胄之剑 / 破胄之锤.
|
||||
// 斩胄之剑 / 破胄之锤 hover invulnerable next to 胄, AoE attacks; dive at the lowest-ATK operator (3×3 stun + DoT);
|
||||
// 15 hits during the dive shoot it down: grounded 20 s, damage ×1.3, PART_TRANSFER to 胄.
|
||||
// boss_1/8 假想敌:胄 random arts beam (range 8); 灭顶之灾 one 刺胄之弹 at the highest-ATK operator in range (8-hit
|
||||
// flying shell, 3×3 stun + phys DoT on arrival); <20 %: damage ×0.5 (no extra shell: PRTS
|
||||
// 能力修正); 死亡集群 drones — a drone that dies costs the leader 2 % of its max HP = the shared
|
||||
// pool's (DRONE_LINK_BASE, [ASSUMED] reading). boss_8 adds 斩胄之剑 / 破胄之锤.
|
||||
// 斩胄之剑 / 破胄之锤 初始模式: hover invulnerable next to 胄, AoE attacks (锤 150 % ATK). 出击模式: blink to the
|
||||
// level's blink route, fly at the lowest-ATK operator (3×3 stun + DoT); 15 hits shoot it
|
||||
// down (瘫痪: stun 10 s, ground unit, damage ×1.3). Either way a new 初始模式 copy with its HP
|
||||
// replaces it at home. Every damage it takes costs 胄 as much (PART_TRANSFER, 无来源, 等量) —
|
||||
// grounded per both texts, during the dive per the PRTS talent [ASSUMED].
|
||||
// boss_2/9 假想敌:铳 unblockable; highest-DEF target in range; erosion; ASPD ramp (+80 × 5) on the same target
|
||||
// (floor 20); 最终之罚 charge at the highest-DEF ground unit (disabled by the h07_02 override).
|
||||
// boss_9: damage ×0.2 while springs live, 盲信之誓 links (100 phys/s on the lines), 末日布道 dash.
|
||||
// 碎铳之簧 a/b/c arts barrier (absorbs arts after RES; phys ×0.1, counter phys + erosion) / element shield
|
||||
// (phys+arts ×0.1, broken by its own element burst; every (spCost+1)-th attack a bouncing erosion
|
||||
// shot) / 5-hit shield (every (spCost+1)-th attack a ten-hit combo); unblockable while shielded;
|
||||
// shield back 25 s after breaking; PART_TRANSFER of damage to 铳 and, split, to the other springs.
|
||||
// shield back 25 s after breaking; every damage taken costs 铳 as much (PART_TRANSFER, 无来源)
|
||||
// and, split, the other springs.
|
||||
// boss_3/10 假想敌:管 summons 余音 (dark) every 40 s (20 s < 50 %); normal attacks strike dark 余音 (their pulse hurts
|
||||
// operators); 裂管之奏 3 strikes on every dark 余音 (AoE arts + apoptosis). boss_10 + 假想敌:弦
|
||||
// (invulnerable, gold 余音, 断弦之奏 AoE + flips them dark; leaves with 管).
|
||||
@@ -63,8 +68,42 @@ import {
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// constants
|
||||
|
||||
/** Share of the damage a grounded 剑/锤 or a 碎铳之簧 passes to its leader ("以一定比例") [ASSUMED]. */
|
||||
export const PART_TRANSFER = 0.5;
|
||||
/**
|
||||
* Share of the damage a 剑/锤 or a 碎铳之簧 takes that its leader loses: 1, 等量 — PRTS talents (“斩胄之剑” / “破胄之锤”
|
||||
* "受到伤害时令假想敌:胄受到等量的无来源生命流失"; 碎铳之簧 (all three) "…令全场范围内仇恨值最高的1名假想敌:铳受到等量的无来源
|
||||
* 生命流失"); the enemy data carries no ratio blackboard (the handbook's "以一定比例" is that 1). The loss is 无来源 (hooks see
|
||||
* no source; the attacker keeps the credit — stats, the per-player pool tally) and follows EVERY damage taken: a
|
||||
* 剑/锤 only takes damage outside its invulnerable 初始模式 (its 出击模式 dive and 【瘫痪】). The two official texts
|
||||
* disagree on the dive: the PRTS talent says "受到伤害时" (any damage), the in-game handbook (data/enemies.json abilities,
|
||||
* PRTS 能力) "被击落时受到伤害以一定比例传递" (only once shot down) — the dive hits passing on is [ASSUMED] (the talent
|
||||
* text; ≤ 15 hits per dive). A loss passed on to a leader is one hit for 限伤 (sim/damage.js leaderHitCancelled, via
|
||||
* Battle.loseHp). v2.5: 0.5 [ASSUMED], and a 剑/锤 passed damage on only while grounded (DESIGN §20.10, §20.13).
|
||||
*/
|
||||
export const PART_TRANSFER = 1;
|
||||
/** The 斩胄之剑 / 破胄之锤 share — the same PRTS 等量 as PART_TRANSFER (kept as a name for the blade kit and its tests). */
|
||||
export const BLADE_TRANSFER = PART_TRANSFER;
|
||||
/** 破胄之锤 normal attack: 150 % ATK (PRTS 天赋 "普通攻击对攻击范围内的所有我方单位造成攻击力150%的物理普通伤害"; 斩胄之剑 100 %). */
|
||||
const BLADE_ATK_SCALE = Object.freeze({ enemy_9015_acstmb: 1.5 });
|
||||
/**
|
||||
* Level branches of each part (act1autochess_h08_01 / _s): 斩胄之剑 spawns on the start of `left_hand_origin` (3,12),
|
||||
* 破胄之锤 on `right_hand_origin` (3,8). `<hand>_blink` = the preset route 掷剑 blinks to (its start; PRTS "将自身路径改为
|
||||
* 关卡预设路径并闪现至路径起点"), `<hand>_origin` = the route the new 初始模式 copy is summoned on ("以关卡预设路径召唤").
|
||||
*/
|
||||
const BLADE_HAND = Object.freeze({ enemy_9014_acstma: 'left_hand', enemy_9015_acstmb: 'right_hand' });
|
||||
/**
|
||||
* 死亡集群: a drone that dies (PRTS 能力修正 "该无人机单位死亡时", whoever kills it) costs 胄 `hp_ratio` (2 %) of "最大生命值"
|
||||
* (PRTS 假想敌:胄 "该妖怪死亡时令假想敌:胄受到最大生命值2%的真实伤害"). WHICH max HP is not documented [ASSUMED reading]:
|
||||
* 'pool' (default, = v2.5): the leader's max HP as the battle shows it — the shared pool (the engine syncs the unit's
|
||||
* HP to it). It is the same HP the "自身生命值低于20%" talent reads, and that one must read the pool (the
|
||||
* unit's data 600 000 is above the co-op 标准 pool 247 500 and above every solo pool, where a fixed 600 000
|
||||
* would never drop below 20 %). A drone = 2 % of the leader bar in every mode and difficulty (终极 72 000).
|
||||
* 'unit': the in-battle unit's data max HP (600 000 / hidden 1 200 000, 不死) — 12 000 / 24 000 per drone at every
|
||||
* difficulty: 0.33 % of the 终极 bar but 19 % of the solo 标准 bar (61 875), so drones decide solo fights.
|
||||
* Round 2 of the boss-HP review tried 'unit'; the review measured the solo regression, so the default is back to 'pool'.
|
||||
* 限伤 (shared/constants.js BOSS_HIT_LIMIT): the loss is one hit through Battle.loseHp — it lands up to the largest pool
|
||||
* today (boss_8 终极 7 200 000 → 144 000) and would be cancelled above a 14 999 950 pool (ceil(0.02 × pool) ≥ 300000).
|
||||
*/
|
||||
export const DRONE_LINK_BASE = 'pool';
|
||||
/** 卢西恩 / 不祥幻影 AoE radius (PRTS "半径2"). */
|
||||
const LUCIEN_AOE_RADIUS = 2;
|
||||
/** 余音 on-hit pulse radius (PRTS 1.6) and 合奏 radii per form (PRTS gold 0.8 / dark 1.6). */
|
||||
@@ -159,6 +198,13 @@ function toGoal(b, x, y, motion = 'WALK') {
|
||||
return { motion, start: [y, x], end: best, checkpoints: [] };
|
||||
}
|
||||
|
||||
/** The extra route of a template branch's first spawn (or null). */
|
||||
function branchRoute(tpl, name) {
|
||||
const br = tpl && tpl.branches && tpl.branches[name];
|
||||
const s = Array.isArray(br) && Array.isArray(br[0]) ? br[0][0] : null;
|
||||
return (s && tpl.extraRoutes && tpl.extraRoutes[s.routeIndex]) || null;
|
||||
}
|
||||
|
||||
/** Spawn one phase of a template branch. Returns the spawned enemies. */
|
||||
function branchSpawn(b, tpl, name, phaseIdx, { fallback = null, mods = null, at = null } = {}) {
|
||||
const br = tpl && tpl.branches && tpl.branches[name];
|
||||
@@ -295,10 +341,10 @@ function segDist(px, py, ax, ay, bx, by) {
|
||||
function kitHelm(ab, e, b, tpl) {
|
||||
const s1 = ab.sk['1'], s2 = ab.sk['2'];
|
||||
const lowRatio = T(ab, '1.hp_ratio') ?? 0, lowScale = T(ab, '1.damage_scale') ?? 1;
|
||||
const hidden = /_2$/.test(e.defId);
|
||||
const P = { low: false };
|
||||
const range = () => e.base.rangeRadius || 8;
|
||||
const shellTargets = (b2) => opsOnly(hidden ? targetsNear(b2, e, range()) : allTargets(b2, e));
|
||||
// 灭顶之灾 "触发索敌和普通攻击相同:选择攻击范围内攻击力最高的1名我方干员" (PRTS 技能, both copies)
|
||||
const shellTargets = (b2) => opsOnly(targetsNear(b2, e, range()));
|
||||
return [
|
||||
{
|
||||
spawn(b2, e2, a, ab2) { ab2.atkType = 'arts'; },
|
||||
@@ -307,22 +353,27 @@ function kitHelm(ab, e, b, tpl) {
|
||||
iv: 0.25,
|
||||
tick(b2, e2) {
|
||||
if (P.low || !(e2.hpRatio < lowRatio)) return;
|
||||
P.low = true; // <20 %: damage taken ×0.5, extra shell
|
||||
P.low = true; // <20 %: damage taken ×0.5 (no extra shell: see 灭顶之灾)
|
||||
b2.addBuff(e2, { key: 'boss:helmGuard', persist: true, visible: true, mods: { physTakenMul: lowScale, artsTakenMul: lowScale } }); // 物理与法术伤害降低
|
||||
b2.fx('phase', { x: e2.x, y: e2.y, id: e2.id, kind: 'helmLow' });
|
||||
},
|
||||
},
|
||||
s1 && {
|
||||
cd: s1.cd, icd: s1.icd, cond: (b2) => shellTargets(b2).length > 0,
|
||||
fire(b2, e2) { // 【灭顶之灾】
|
||||
const n = P.low ? 2 : 1;
|
||||
const ts = shellTargets(b2).sort((p, q) => q.s.atk - p.s.atk || aggroCmp(p, q)).slice(0, n);
|
||||
for (const t of ts) fireShell(b2, e2, t);
|
||||
// 【灭顶之灾】 one shell, also below 20 %: the handbook's "额外发射<刺胄之弹>" is wrong in game (PRTS 能力修正 原因 6
|
||||
// "描述与游戏实际表现不符合": "【灭顶之灾】不会额外发射") and no blackboard holds a second shell (skill 1 has none)
|
||||
fire(b2, e2) {
|
||||
const t = shellTargets(b2).sort((p, q) => q.s.atk - p.s.atk || aggroCmp(p, q))[0];
|
||||
if (t) fireShell(b2, e2, t);
|
||||
},
|
||||
},
|
||||
s2 && {
|
||||
cd: s2.cd, icd: s2.icd,
|
||||
fire(b2, e2) { // 【死亡集群】: drones; one killed by an operator costs the leader hp_ratio × max HP
|
||||
// 【死亡集群】: a drone on the branch route; when it dies (PRTS 能力修正 "该无人机单位死亡时", whoever kills it) the leader
|
||||
// loses hp_ratio × max HP. Drone HP: `summon.hp_ratio` (隐秘核心 only) scales it [ASSUMED: the summon's max HP]; the
|
||||
// `max_hp` 0.5 both copies carry is not read [ASSUMED] — no source says what it does (PRTS 技能 lists the 2 % only,
|
||||
// the 妖怪 page has no summon rule, the level gives the 妖怪 no override; as an attribute key it would be +50 %).
|
||||
fire(b2, e2) {
|
||||
const hpMul = s2.bb['summon.hp_ratio'] > 0 ? s2.bb['summon.hp_ratio'] : null;
|
||||
const key = s2.bs.enemy_key ?? 'enemy_1005_yokai';
|
||||
const drones = branchSpawn(b2, tpl, s2.bs.branch_id ?? 'boss_summon_enemy', 0, {
|
||||
@@ -333,9 +384,10 @@ function kitHelm(ab, e, b, tpl) {
|
||||
const ratio = s2.bb.hp_ratio ?? 0;
|
||||
for (const d of drones) attach(b2, d, [{
|
||||
death(c, b3) {
|
||||
if (c.reason !== 'killed' || !c.killer || c.killer.side !== 'ally' || !(ratio > 0)) return;
|
||||
if (c.reason !== 'killed' || !(ratio > 0)) return; // a leak is no death
|
||||
const boss = e2.alive ? e2 : b3.aliveEnemies().find((o) => o.isBoss && o.defId === e2.defId);
|
||||
if (boss) { b3.loseHp(boss, boss.s.maxHp * ratio, { source: c.killer }); b3.fx('beam', { x: d.x, y: d.y, from: d.id, to: boss.id, kind: 'droneLink' }); }
|
||||
const by = c.killer && c.killer.side === 'ally' ? c.killer : null; // credited to the killing operator
|
||||
if (boss) { b3.loseHp(boss, droneLinkBase(boss) * ratio, { source: by }); b3.fx('beam', { x: d.x, y: d.y, from: d.id, to: boss.id, kind: 'droneLink' }); }
|
||||
},
|
||||
}]);
|
||||
},
|
||||
@@ -343,6 +395,16 @@ function kitHelm(ab, e, b, tpl) {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Max HP the 死亡集群 drone link reads (DRONE_LINK_BASE): 'pool' = the leader's shown max HP (the shared pool), 'unit' =
|
||||
* the unit's data max HP (template × spawn hpMul).
|
||||
*/
|
||||
export function droneLinkBase(boss, base = DRONE_LINK_BASE) {
|
||||
if (base !== 'unit' || !boss.def || !(boss.def.maxHp > 0)) return boss.s.maxHp;
|
||||
const m = boss.mods && Number(boss.mods.hpMul);
|
||||
return boss.def.maxHp * (Number.isFinite(m) && m > 0 ? m : 1);
|
||||
}
|
||||
|
||||
/** Launch a 刺胄之弹 from `boss` at `target`'s tile. */
|
||||
function fireShell(b, boss, target) {
|
||||
const tr = target.tileR, tc = target.tileC;
|
||||
@@ -368,54 +430,90 @@ function kitShell(ab, e) {
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* “斩胄之剑” / “破胄之锤” (PRTS 天赋 + 技能 掷剑 / 掷锤; numbers from skill 1's blackboard).
|
||||
* 初始模式 ('hover'): 无敌 + 自缚 [ASSUMED: also untargetable, so operators spend no attacks on it]; AoE physical attack
|
||||
* on every ally in its range (锤 150 % ATK, BLADE_ATK_SCALE).
|
||||
* 掷剑 (skill 1 cd / icd, an operator on the field) → 出击模式 ('dive'): no normal attack, vulnerable; blinks to the start
|
||||
* of its `<hand>_blink` route and flies at the tile of the lowest-ATK operator (无视无法选择).
|
||||
* Arriving (no 【瘫痪】): `stun` s stun + `dot_damage`/s for `dot_duration` s on the 3×3 around it, then it is replaced.
|
||||
* Once, after `max_hit_cnt` damage instances: 【瘫痪】 ('down') — `special_stun_duration` s, but it ends with its own
|
||||
* `stun` s stun ("该晕眩结束时也会结束【瘫痪】"): ground unit, damage taken ×`damage_scale`; when it ends, replaced.
|
||||
* Replaced: a new 初始模式 copy with its current HP is summoned on its `<hand>_origin` route (at home without a template),
|
||||
* then it is killed (强制击杀自身; a part: no kill count, no bounty). The copy is a new unit [ASSUMED: only its HP is
|
||||
* inherited] — its 掷剑 starts from the initial cooldown again, so the data's cooldown (80 s) never comes into play.
|
||||
* Every damage it takes is lost by 假想敌:胄 too (BLADE_TRANSFER = PART_TRANSFER 1, 无来源, credited to the attacker).
|
||||
*/
|
||||
function kitBlade(ab, e, b, tpl) {
|
||||
const s = ab.sk['1'];
|
||||
const bb = s ? s.bb : {};
|
||||
const hand = BLADE_HAND[e.defId];
|
||||
const P = { state: 'hover', home: { x: e.x, y: e.y }, hits: 0, target: null };
|
||||
const leader = (b2) => nearestOf(b2, e, (o) => o.isBoss && /enemy_9013_acstmk/.test(o.defId));
|
||||
const hover = (b2, on) => {
|
||||
if (on) b2.addBuff(e, { key: 'boss:hover', persist: true, visible: true, flags: { invulnerable: true, untargetable: true } });
|
||||
else b2.removeBuff(e, 'boss:hover');
|
||||
};
|
||||
const replace = (b2, e2) => { // 以关卡预设路径召唤一个继承自身生命值的初始模式的 copy,随后强制击杀自身
|
||||
P.state = 'gone';
|
||||
e2.motion = 'FLY';
|
||||
const origin = hand ? branchRoute(tpl, `${hand}_origin`) : null;
|
||||
const home = [P.home.y, P.home.x];
|
||||
const n = b2.spawnEnemy(e2.defId, {
|
||||
route: origin || { motion: 'FLY', start: home, end: home, steps: [{ t: 'wait', s: 99999 }] }, pos: origin ? undefined : home,
|
||||
tag: e2.tag, countInTotal: false, mods: e2.mods, ownerPlayerId: e2.ownerId,
|
||||
});
|
||||
if (n) { n.hp = Math.min(n.s.maxHp, e2.hp); b2.fx('blink', { x: n.x, y: n.y, id: n.id, fx: e2.x, fy: e2.y }); }
|
||||
b2.kill(e2, null);
|
||||
};
|
||||
const shotDown = (b2, e2) => { // 【瘫痪】
|
||||
P.state = 'down';
|
||||
e2.motion = 'WALK'; // 变为地面单位(不改变寻路方式)
|
||||
const stun = bb.stun ?? 0;
|
||||
b2.applyStatus(e2, 'stun', { duration: stun, source: null, force: true });
|
||||
b2.addBuff(e2, { key: 'boss:downed', duration: Math.min(bb.special_stun_duration ?? stun, stun), visible: true,
|
||||
mods: { dmgTakenMul: bb.damage_scale ?? 1 }, onExpire: () => { if (e2.alive && P.state === 'down') replace(b2, e2); } });
|
||||
b2.fx('phase', { x: e2.x, y: e2.y, id: e2.id, kind: 'shotDown' });
|
||||
};
|
||||
return [
|
||||
{
|
||||
spawn(b2, e2) { hover(b2, true); b2.addBuff(e2, { key: 'boss:anchor', persist: true, flags: { noMove: true } }); },
|
||||
spawn(b2, e2) {
|
||||
hover(b2, true);
|
||||
b2.addBuff(e2, { key: 'boss:anchor', persist: true, flags: { noMove: true, unblockable: true } }); // 自缚 (moved by hand) · 不可阻挡
|
||||
if (BLADE_ATK_SCALE[e2.defId]) e2.profile.atkScale = BLADE_ATK_SCALE[e2.defId];
|
||||
},
|
||||
before(c, b2, e2) { const l = targetsNear(b2, e2, e2.base.rangeRadius || 1.6, { ranged: false }); if (l.length) c.targets = l; }, // 范围物理伤害
|
||||
taken(c, b2, e2) {
|
||||
if (P.state === 'dive' && ++P.hits >= (bb.max_hit_cnt ?? Infinity)) {
|
||||
// shot down: ground unit, damage ×damage_scale, stunned special_stun_duration, then flies home
|
||||
P.state = 'down';
|
||||
e2.motion = 'WALK';
|
||||
b2.addBuff(e2, { key: 'boss:downed', duration: bb.special_stun_duration ?? 0, visible: true, mods: { dmgTakenMul: bb.damage_scale ?? 1 },
|
||||
onExpire: () => { if (e2.alive) { e2.motion = 'FLY'; P.state = 'return'; } } });
|
||||
b2.applyStatus(e2, 'stun', { duration: bb.special_stun_duration ?? 0, source: null, force: true });
|
||||
b2.fx('phase', { x: e2.x, y: e2.y, id: e2.id, kind: 'shotDown' });
|
||||
return;
|
||||
}
|
||||
if (P.state === 'down' && c.source && c.source.side === 'ally' && c.amount > 0) {
|
||||
if (c.amount > 0) { // 受到伤害时令假想敌:胄受到等量的无来源生命流失 (【瘫痪】; 出击模式 [ASSUMED], see PART_TRANSFER)
|
||||
const L = leader(b2);
|
||||
if (L) b2.loseHp(L, c.amount * PART_TRANSFER, { source: c.source, from: c.dmg }); // 被击落时受到伤害以一定比例传递给假想敌:胄
|
||||
const src = c.source || c.credit; // a 无来源 burst still passes on (credited)
|
||||
if (L) b2.loseHp(L, c.amount * BLADE_TRANSFER, { source: src && src.side === 'ally' ? src : null, from: c.dmg, sourceless: true });
|
||||
}
|
||||
if (P.state === 'dive' && e2.alive && ++P.hits >= (bb.max_hit_cnt ?? Infinity)) shotDown(b2, e2); // 仅1次
|
||||
},
|
||||
tick(b2, e2, a, dt) {
|
||||
const sp = e2.s.moveSpeed * MOVE_SCALE * dt;
|
||||
if (P.state === 'dive') {
|
||||
const t = P.target;
|
||||
if (!stepToward(e2, t.c, t.r, sp)) return;
|
||||
stunBlast(b2, e2, t.r, t.c, bb.stun ?? 0, bb.dot_damage ?? 0, bb.dot_duration ?? 0, 'bladeDive');
|
||||
P.state = 'return';
|
||||
} else if (P.state === 'return') {
|
||||
if (stepToward(e2, P.home.x, P.home.y, sp)) { P.state = 'hover'; hover(b2, true); }
|
||||
}
|
||||
if (P.state !== 'dive' || !canCast(e2)) return; // a stunned blade does not fly on
|
||||
const t = P.target;
|
||||
if (!stepToward(e2, t.c, t.r, e2.s.moveSpeed * MOVE_SCALE * dt)) return;
|
||||
// arrival: 3×3 stun + DoT (无来源: credited to 胄, as the shell's), then the 初始模式 copy takes over
|
||||
stunBlast(b2, leader(b2) || e2, t.r, t.c, bb.stun ?? 0, bb.dot_damage ?? 0, bb.dot_duration ?? 0, 'bladeDive');
|
||||
replace(b2, e2);
|
||||
},
|
||||
},
|
||||
s && {
|
||||
cd: s.cd, icd: s.icd, cond: (b2) => P.state === 'hover' && opsOnly(allTargets(b2, e)).length > 0,
|
||||
fire(b2, e2) { // fly at the lowest-ATK operator
|
||||
const t = opsOnly(allTargets(b2, e2)).sort((p, q) => p.s.atk - q.s.atk || aggroCmp(p, q))[0];
|
||||
cd: s.cd, icd: s.icd, cond: (b2) => P.state === 'hover' && opsOnly(allTargets(b2, e)).length > 0, // 存在至少1名可选我方干员
|
||||
fire(b2, e2) { // 【掷剑】/【掷锤】 → 出击模式
|
||||
const t = opsOnly(b2.allies()).sort((p, q) => p.s.atk - q.s.atk || aggroCmp(p, q))[0];
|
||||
if (!t) return;
|
||||
P.state = 'dive'; P.hits = 0; P.target = { r: t.tileR, c: t.tileC };
|
||||
hover(b2, false);
|
||||
b2.addBuff(e2, { key: 'boss:sortie', persist: true, flags: { disarm: true } }); // 出击模式:不进行普通攻击
|
||||
const blink = hand ? branchRoute(tpl, `${hand}_blink`) : null;
|
||||
if (blink && Array.isArray(blink.start)) {
|
||||
const from = { x: e2.x, y: e2.y };
|
||||
e2.x = blink.start[1]; e2.y = blink.start[0];
|
||||
b2.fx('blink', { x: e2.x, y: e2.y, id: e2.id, fx: from.x, fy: from.y });
|
||||
}
|
||||
b2.fx('charge', { x: e2.x, y: e2.y, id: e2.id, tx: t.tileC, ty: t.tileR, kind: 'bladeDive' });
|
||||
},
|
||||
},
|
||||
@@ -545,14 +643,16 @@ function kitSpring(ab, e) {
|
||||
},
|
||||
burst(c, b) { if (kind === 'element' && P.up) drop(b); }, // 自身元素损伤爆发时,护盾消失
|
||||
taken(c, b, e2) {
|
||||
const src = c.source || c.credit; // a 无来源 burst still passes on (credited)
|
||||
if (!src || src.side !== 'ally' || !(c.amount > 0)) return;
|
||||
if (!(c.amount > 0)) return;
|
||||
if (c.dmg.tags && c.dmg.tags.includes(SPRING_SHARE_TAG)) return; // a share never passes on again
|
||||
// 【盲信之誓】 受到伤害时以一定比例传递给假想敌:铳 (and, split, to the other springs — 隐秘核心 text)
|
||||
const s0 = c.source || c.credit; // a 无来源 burst still passes on (credited)
|
||||
const src = s0 && s0.side === 'ally' ? s0 : null;
|
||||
// 【盲信之誓】 受到伤害时令假想敌:铳受到等量的无来源生命流失 (PRTS); the even split to the other springs follows the
|
||||
// 隐秘核心 handbook ("传递给<假想敌:铳>和场上其他<“碎铳之簧”>") — not in the PRTS talent, [ASSUMED] amount
|
||||
const g = gun(b);
|
||||
if (g) b.loseHp(g, c.amount * PART_TRANSFER, { source: src, from: c.dmg });
|
||||
if (g) b.loseHp(g, c.amount * PART_TRANSFER, { source: src, from: c.dmg, sourceless: true });
|
||||
const others = b.enemies.filter((o) => o.alive && o !== e2 && isSpring(o));
|
||||
for (const o of others) b.loseHp(o, (c.amount * PART_TRANSFER) / others.length, { source: src, from: c.dmg, tags: [SPRING_SHARE_TAG] });
|
||||
for (const o of others) b.loseHp(o, (c.amount * PART_TRANSFER) / others.length, { source: src, from: c.dmg, tags: [SPRING_SHARE_TAG], sourceless: true });
|
||||
},
|
||||
tick(b, e2, a, dt) {
|
||||
if (!P.up && P.downAt != null && b.time - P.downAt >= regen) raise(b);
|
||||
|
||||
@@ -48,16 +48,17 @@
|
||||
// env_gbuff_new_with_verify act1autochess_debuff_3 {value} 自愈: every damage instance an own unit (operator or
|
||||
// summon) takes heals it `value` (not HP loss 流失, not gauge
|
||||
// fills, not while a 禁疗 status is on)
|
||||
// env_gbuff_new_with_verify act1autochess_debuff_9 {atk, def} 无瑕: operators at full HP get ATK/DEF ×(1 + x)
|
||||
// env_gbuff_new_with_verify act1autochess_debuff_9 {atk, def} 无瑕: operators at full HP get ATK/DEF +x
|
||||
// env_gbuff_new halfIdle_magic_resist_penetration {magic_resist_penetrate_fixed} 火力: operators ignore N RES
|
||||
// env_gbuff_new halfIdle_def_penetration {def_penetrate} 锐利: operators ignore x DEF
|
||||
// char_respawntime_mul {scale} 征召: operators' redeploy time × scale
|
||||
// enemy_attribute_mul / enemy_attribute_add {atk|def|magic_resistance|max_hp|attack_speed|move_speed}
|
||||
// (+ bbStr enemy_level_type ELITE / BOSS) 排斥/责罚/裁决: enemies of the player's half (ownerId)
|
||||
// of that rank get the debuff at spawn (all ranks without a type)
|
||||
// Attribute bonuses are 直接乘算 (support/index.js convention): ×(1 + x) multipliers; flat values stay flat.
|
||||
// Operator attribute bonuses are 直接乘算 (support directMods: additive with every other percentage, PRTS 盟约记录 /
|
||||
// 游戏数据基础); flat values stay flat; the enemies' enemy_attribute_mul stays a multiplier.
|
||||
|
||||
import { gameData, num, buffsOf, passiveBuff, effectRecord } from './support/index.js';
|
||||
import { gameData, num, buffsOf, passiveBuff, effectRecord, directMods } from './support/index.js';
|
||||
import { isShopItem } from '../simdata.js';
|
||||
|
||||
// =====================================================================================================================
|
||||
@@ -305,9 +306,10 @@ export function registerMeta(registry) {
|
||||
|
||||
const RUNE = Object.freeze({ heal: 'act1autochess_debuff_3', flawless: 'act1autochess_debuff_9' });
|
||||
|
||||
/** Operator mods of an env_gbuff blackboard (numbers only; 直接乘算 for attributes). */
|
||||
/** Operator mods of an env_gbuff blackboard (numbers only; 直接乘算 for attributes — support directMods). */
|
||||
function opModsOf(p) {
|
||||
const mods = {};
|
||||
const direct = { atk: 0, def: 0, hp: 0 };
|
||||
const add = (k, v) => { if (Number.isFinite(v) && v !== 0) mods[k] = (mods[k] ?? 0) + v; };
|
||||
const mul = (k, v) => { if (Number.isFinite(v) && v !== 0) mods[k] = (mods[k] ?? 1) * (1 + v); };
|
||||
for (const [k, raw] of Object.entries(p || {})) {
|
||||
@@ -318,15 +320,15 @@ function opModsOf(p) {
|
||||
case 'magic_resist_penetrate': add('resIgnorePct', v); break;
|
||||
case 'def_penetrate': add('defIgnorePct', v); break;
|
||||
case 'def_penetrate_fixed': add('defIgnoreFlat', v); break;
|
||||
case 'atk': mul('atkMul', v); break;
|
||||
case 'def': mul('defMul', v); break;
|
||||
case 'max_hp': mul('hpMul', v); break;
|
||||
case 'atk': direct.atk += v; break;
|
||||
case 'def': direct.def += v; break;
|
||||
case 'max_hp': direct.hp += v; break;
|
||||
case 'attack_speed': add('aspd', Math.abs(v) < 1 ? v * 100 : v); break;
|
||||
case 'damage_scale': mul('dmgDealtMul', v); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
return mods;
|
||||
return directMods(direct, mods);
|
||||
}
|
||||
|
||||
/** Enemy mods of an enemy_attribute_mul / _add blackboard. */
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
// act1autochess_gar_event_addition_cnt (魔王) layerGain: a 'garrison' gain whose source stands on the tile in front
|
||||
// of 魔王 (range_id 1-1) — or was knocked out there this instant (幽灵鲨 "被击倒时"; engine ctx.tile)
|
||||
// — gets +extra_cnt per bond (the extra does not count toward the source's per-battle cap).
|
||||
// stat traits GAIN_BUFF (atkMul / hpMul), attr_common_global_buff (spRecoveryFlat), attrByBond,
|
||||
// stat traits GAIN_BUFF (battleRuneKey char_attribute_mul "攻击力和生命值+20%": a rune on the chess's base
|
||||
// attributes, so atkMul / hpMul), attr_common_global_buff (spRecoveryFlat; "+X%" ATK / HP would be
|
||||
// 直接乘算), attrByBond ("每叠加N层…+X%": 直接乘算, support directMods),
|
||||
// respawnTimeByBond, attrByBond_add_onstart, ab_damageScaleByBond — layer readers use
|
||||
// floor(Σ layers of the listed ACTIVE bonds / divide_num) and are recomputed after every layer
|
||||
// gain (research 02 §5 [ASSUMED] only active bonds count).
|
||||
@@ -340,8 +342,7 @@ const INSTALLERS = {
|
||||
for (const it of list) {
|
||||
const mods = {};
|
||||
if (S.num(it.bb.sp_recovery_per_sec, 0)) mods.spRecoveryFlat = S.num(it.bb.sp_recovery_per_sec);
|
||||
if (S.num(it.bb.atk, 0)) mods.atkMul = 1 + S.num(it.bb.atk);
|
||||
if (S.num(it.bb.max_hp, 0)) mods.hpMul = 1 + S.num(it.bb.max_hp);
|
||||
Object.assign(mods, S.directMods({ atk: S.num(it.bb.atk, 0), hp: S.num(it.bb.max_hp, 0) }));
|
||||
if (S.num(it.bb.attack_speed, 0)) mods.aspd = S.num(it.bb.attack_speed);
|
||||
S.passiveBuff(battle, it.unit, `gar:${it.gid}`, mods);
|
||||
}
|
||||
@@ -369,10 +370,8 @@ export function stepsOf(battle, it) {
|
||||
}
|
||||
|
||||
function attrMods(bb, k) {
|
||||
const m = {};
|
||||
if (S.num(bb.atk, 0)) m.atkMul = 1 + S.num(bb.atk) * k;
|
||||
if (S.num(bb.max_hp, 0)) m.hpMul = 1 + S.num(bb.max_hp) * k;
|
||||
if (S.num(bb.def, 0)) m.defMul = 1 + S.num(bb.def) * k;
|
||||
// "每叠加N层,本干员攻击力+X%": a 直接乘算 bonus like every other "+X%" of the 卫戍 systems (support directMods)
|
||||
const m = S.directMods({ atk: S.num(bb.atk, 0) * k, hp: S.num(bb.max_hp, 0) * k, def: S.num(bb.def, 0) * k });
|
||||
if (S.num(bb.attack_speed, 0)) m.aspd = S.num(bb.attack_speed) * k;
|
||||
if (S.num(bb.hp_recovery_per_sec, 0)) m.hpRegen = S.num(bb.hp_recovery_per_sec) * k;
|
||||
if (S.num(bb.sp_recovery_per_sec, 0)) m.spRecoveryFlat = S.num(bb.sp_recovery_per_sec) * k;
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
// Numbers always come from the concrete record's buffs (support.buffsOf: `{ ...bb, ...bbStr }` per buff, looked up by
|
||||
// the buff's bbStr.key — the flattened `params` of data/items.json lose duplicate keys such as 蒸汽之心 attack_speed).
|
||||
//
|
||||
// Stat rule (research 02 §2.1, PRTS "直接乘算"; research 04 §2 assumed additive %): every equipment percentage is its own
|
||||
// multiplier on the final stat — atk/def/max_hp → atkMul/defMul/hpMul = 1 + x; attack_speed → aspd (additive);
|
||||
// Stat rule (PRTS 盟约记录 "装备效果提供的属性加成均为直接乘算"; 直接乘算 = summed with every other percentage, PRTS 游戏数据
|
||||
// 基础 — research 04 §2's additive reading; v2.5 had each item its own multiplier): atk/def/max_hp → support directMods
|
||||
// (atkPct/defPct/hpPct += x; constants.js DIRECT_BONUS_STACKING); attack_speed → aspd (additive);
|
||||
// magic_resistance → resFlat; respawn_time → redeployMul 1 + x; sp_recovery_per_sec → spRecoveryFlat;
|
||||
// magic_resist_penetrate → resIgnorePct; taunt_level → taunt. Two different items on one unit both apply, and a
|
||||
// normal + golden copy of the same item may coexist (different ids → different buff keys) and both apply.
|
||||
@@ -33,7 +34,7 @@
|
||||
|
||||
import {
|
||||
num, itemRecord, itemKeyOf, buffsOf, isOp, onField, unitBonds, isMember, bondActive, isGroundOp, frontTile,
|
||||
alliesAround, passiveBuff, fxOn, battleStore, contentInfo, itemsOf,
|
||||
alliesAround, passiveBuff, fxOn, battleStore, contentInfo, itemsOf, directMods,
|
||||
} from '../support/index.js';
|
||||
import { mitigate, hasHp } from '../../damage.js';
|
||||
|
||||
@@ -68,9 +69,8 @@ function statMods(p) {
|
||||
const m = {};
|
||||
let any = false;
|
||||
const set = (k, v) => { m[k] = v; any = true; };
|
||||
if (num(p.atk)) set('atkMul', 1 + num(p.atk));
|
||||
if (num(p.def)) set('defMul', 1 + num(p.def));
|
||||
if (num(p.max_hp)) set('hpMul', Math.max(0.01, 1 + num(p.max_hp)));
|
||||
// ATK / DEF / max HP "+X%" are 直接乘算 (support directMods: additive with every other percentage)
|
||||
for (const [k, v] of Object.entries(directMods({ atk: num(p.atk), def: num(p.def), hp: num(p.max_hp) ? Math.max(-0.99, num(p.max_hp)) : 0 }))) set(k, v);
|
||||
if (num(p.attack_speed)) set('aspd', num(p.attack_speed));
|
||||
if (num(p.magic_resistance)) set('resFlat', num(p.magic_resistance));
|
||||
if (num(p.respawn_time)) set('redeployMul', Math.max(0, 1 + num(p.respawn_time)));
|
||||
@@ -366,7 +366,7 @@ const BY_ITEM = {
|
||||
if (Math.hypot(c.target.x - u.x, c.target.y - u.y) >= r - 1e-6) c.dmg.mul *= sc;
|
||||
});
|
||||
},
|
||||
// 炎国短刀: each skill activation +atk (≤ atk_buff_cnt stacks) — one multiplier 1 + atk × stacks
|
||||
// 炎国短刀: each skill activation +atk (≤ atk_buff_cnt stacks) — one 直接乘算 bonus of atk × stacks
|
||||
chess_item_3_04_e(battle, u, rec, S) {
|
||||
const p = bp(rec, 'act1vautochess_equip_acarm024_global_buff');
|
||||
if (!p) return;
|
||||
@@ -375,7 +375,7 @@ const BY_ITEM = {
|
||||
S.on('skillStart', (c) => {
|
||||
if (c.unit !== u || n >= cap || !chance(battle, num(p.prob, 1))) return;
|
||||
n++;
|
||||
S.stat('stacks', { atkMul: 1 + num(p.atk) * n });
|
||||
S.stat('stacks', directMods({ atk: num(p.atk) * n }));
|
||||
});
|
||||
},
|
||||
// 迅捷作战粮: on deploy SP += sp_each_person × (1 + other operators sharing a bond)
|
||||
@@ -401,7 +401,7 @@ const BY_ITEM = {
|
||||
chess_item_3_06_e(battle, u, rec, S) {
|
||||
const p = bp(rec, 'act1autochess_equip_acarm049_global_buff');
|
||||
if (!p) return;
|
||||
if (num(p.init_max_hp)) S.stat('hp', { hpMul: 1 + num(p.init_max_hp) });
|
||||
if (num(p.init_max_hp)) S.stat('hp', directMods({ hp: num(p.init_max_hp) }));
|
||||
const ex = num(p.ex_max_hp);
|
||||
if (!ex) return;
|
||||
const key = S.key('front');
|
||||
@@ -413,7 +413,7 @@ const BY_ITEM = {
|
||||
const here = initial ? a.homeR === fr && a.homeC === fc : onField(a) && a.tileR === fr && a.tileC === fc;
|
||||
if (here) { occupied = true; break; }
|
||||
}
|
||||
if (!occupied) S.buff(u, { key, mods: { hpMul: 1 + ex }, refresh: 'replace' });
|
||||
if (!occupied) S.buff(u, { key, mods: directMods({ hp: ex }), refresh: 'replace' });
|
||||
else battle.removeBuff(u, key);
|
||||
};
|
||||
S.on('deploy', (c) => { if (c.unit === u) check(!!c.initial); });
|
||||
@@ -675,7 +675,7 @@ const BY_ITEM = {
|
||||
if (partner && carries(battle, u, partner) && c.skill && c.skill.isTimed) {
|
||||
combo = true;
|
||||
doomed = false;
|
||||
S.buff(u, { key: comboKey, mods: { atkMul: 1 + num(p.atk) } });
|
||||
S.buff(u, { key: comboKey, mods: directMods({ atk: num(p.atk) }) });
|
||||
}
|
||||
});
|
||||
S.on('skillEnd', (c) => {
|
||||
@@ -715,7 +715,7 @@ const BY_ITEM = {
|
||||
const stealth = !!u.s.flags.stealth;
|
||||
if (stealth && st.bonus < max) {
|
||||
st.bonus = Math.min(max, st.bonus + per * TICKS);
|
||||
S.buff(u, { key, mods: { atkMul: 1 + st.bonus }, refresh: 'replace' });
|
||||
S.buff(u, { key, mods: directMods({ atk: st.bonus }), refresh: 'replace' });
|
||||
}
|
||||
if (st.wasStealth && !stealth && st.bonus > 0) st.armed = true;
|
||||
st.wasStealth = stealth;
|
||||
|
||||
@@ -109,6 +109,8 @@ function inspire(battle, target, val, src, stat = 'atk') {
|
||||
battle.addBuff(target, { key, mods: stat === 'hp' ? { hpFlat: flat } : { atkFlat: flat }, duration: AURA_DUR, source: src, visible: true, data: { src: src.id, val } });
|
||||
}
|
||||
/** Reveal stealthed enemies on the given tiles. */
|
||||
/** RES cut mods for a blackboard magic_resistance value (battle.applyStrongest): |v| < 1 = ×(1 + v), else flat v. */
|
||||
const resCut = (v) => (Math.abs(v) < 1 ? { resMul: Math.max(0, 1 + v) } : { resFlat: v });
|
||||
const reveal = (battle, enemies) => { for (const e of enemies) if (e.s.flags.stealth) pulse(battle, e, 'aura:reveal', null, { flags: { reveal: true } }); };
|
||||
/**
|
||||
* Knock-back of 力度 `force` away from `from` (a radial push, Battle.push: the official 力度 − 重量 distance — PRTS 推与拉;
|
||||
@@ -1077,7 +1079,8 @@ const kits = {
|
||||
for (const e of enemiesOnRange(battle, unit)) {
|
||||
const f = e.s.flags;
|
||||
const m = f.freeze ? num(t0.damage_scale_freeze, 1.5) : f.cold ? num(t0.damage_scale_cold, 1.25) : 0;
|
||||
if (m) battle.addBuff(e, { key: `gnosis:fragile:${unit.id}`, mods: { dmgTakenMul: m }, duration: 0.15 });
|
||||
// 同名效果取最高: one 坚冰 per enemy, the strongest — two 灵知 (a pair / 联防 partner's copy) never compound
|
||||
if (m) battle.applyStrongest(e, 'gnosis:fragile', { duration: 0.15, value: m, mods: (v) => ({ dmgTakenMul: v }), source: unit });
|
||||
}
|
||||
});
|
||||
} },
|
||||
@@ -1105,10 +1108,10 @@ const kits = {
|
||||
kind: (def.skill?.maxCharges ?? 1) > 1 ? 'charges' : 'instant',
|
||||
targeting: g ? { rangeGrid: g } : undefined,
|
||||
onStart({ battle, unit }) {
|
||||
const debuff = Math.abs(mr) < 1 ? { resMul: Math.max(0, 1 + mr) } : { resFlat: mr };
|
||||
for (const e of targetsInRange(battle, unit)) {
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale, 1.7), type: 'arts', isSkill: true, tags: ['skill', 'burst'] });
|
||||
if (e.alive && mr) battle.addBuff(e, { key: `lionhd:res:${unit.id}`, mods: debuff, duration: num(bb.duration, 6), source: unit });
|
||||
// 同名效果取最高: one RES cut per enemy (the strongest), never one per 莱恩哈特
|
||||
if (e.alive && mr) battle.applyStrongest(e, 'lionhd:res', { duration: num(bb.duration, 6), value: mr, mods: resCut, source: unit });
|
||||
}
|
||||
battle.fx('explosion', { x: unit.x, y: unit.y, id: unit.id });
|
||||
},
|
||||
@@ -1179,10 +1182,10 @@ const kits = {
|
||||
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);
|
||||
const debuff = Math.abs(mr) < 1 ? { resMul: Math.max(0, 1 + mr) } : { resFlat: mr };
|
||||
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'] });
|
||||
if (e.alive && mr) battle.addBuff(e, { key: `texas2:resDown:${unit.id}`, mods: debuff, duration: num(bb.debuff_duration, 8), source: unit });
|
||||
// 同名效果取最高: 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 });
|
||||
};
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
// never edit it; module-specific helpers live in the submodules.
|
||||
//
|
||||
// Conventions shared by every wave-B module (details in docs/CONTENT.md):
|
||||
// * Attribute bonuses from bonds, bands, equipment and 机变 cards are "直接乘算" (research 02 §2.1): each source is its
|
||||
// own multiplier on the final stat → `atkMul` / `defMul` / `hpMul` (1 + x), never the skill-style `atkPct`.
|
||||
// ASPD stays additive (`aspd`), damage bonuses are `dmgDealtMul`, redeploy changes `redeployMul`.
|
||||
// * Attribute bonuses ("+X%" ATK / DEF / max HP) from bonds, bands, equipment, 机变 cards and the per-layer 特质 are
|
||||
// "直接乘算" (PRTS 盟约记录; PRTS 游戏数据基础: 直接乘算 values are summed with every other 直接乘算, a skill's "攻击力+X%"
|
||||
// included) → `directMods({ atk, def, hp })` = the additive `atkPct` / `defPct` / `hpPct` (constants.js
|
||||
// DIRECT_BONUS_STACKING; 'multiply' restores the v2.5 per-source ×(1 + x) `atkMul`). "提升至X%" effects stay
|
||||
// multipliers. ASPD stays additive (`aspd`), damage bonuses are `dmgDealtMul`, redeploy changes `redeployMul`.
|
||||
// * Match-long passives use `passiveBuff()` (persist + allowDead + replace): idempotent by key, survive death.
|
||||
// * Buff keys / fx `src` are namespaced: `bond:<bondId>…`, `item:<itemKey>…`, `gar:<effectKey>…`, `band:<bandId>…`,
|
||||
// `choice:<effectId>…`.
|
||||
@@ -16,7 +18,7 @@
|
||||
// `PlayerBattleInput.contentInfo`, written by the `global:contentb_info` meta handler (support/meta.js).
|
||||
|
||||
import { getData } from '../../../data.js';
|
||||
import { COLS } from '../../constants.js';
|
||||
import { COLS, DIRECT_BONUS_STACKING } from '../../constants.js';
|
||||
import { frontOf, offsetTile } from '../../dir.js';
|
||||
import { bodyInKeys, bodyDist, bodyInRadius, bodyOnTile, bodyTileReach } from '../../body.js';
|
||||
|
||||
@@ -267,6 +269,25 @@ export const inRange = (u, target) => !!(u && target && u.rangeKeySet && onKeys(
|
||||
// =====================================================================================================================
|
||||
// battle: buffs, fx, per-battle state
|
||||
|
||||
/**
|
||||
* Buff mods of a 直接乘算 attribute bonus (bonds, bands, equipment, 机变 cards, per-layer 特质): ratios `atk` / `def` /
|
||||
* `hp` (+0.3 = "+30%"; negative lowers) → `atkPct` / `defPct` / `hpPct`, summed by the engine with every other additive
|
||||
* percentage (constants.js DIRECT_BONUS_STACKING 'add', the official rule); 'multiply' → `atkMul` / `defMul` / `hpMul`
|
||||
* = 1 + x (the v2.5 reading). Zero / non-finite parts are left out; `extra` mods are merged in.
|
||||
*/
|
||||
export function directMods({ atk = 0, def = 0, hp = 0 } = {}, extra = null) {
|
||||
const m = {};
|
||||
const put = (pct, mul, v) => {
|
||||
if (!(typeof v === 'number' && Number.isFinite(v)) || v === 0) return;
|
||||
if (DIRECT_BONUS_STACKING === 'multiply') m[mul] = Math.max(0, 1 + v);
|
||||
else m[pct] = v;
|
||||
};
|
||||
put('atkPct', 'atkMul', atk);
|
||||
put('defPct', 'defMul', def);
|
||||
put('hpPct', 'hpMul', hp);
|
||||
return extra ? Object.assign(m, extra) : m;
|
||||
}
|
||||
|
||||
/** Match-long passive buff (survives death/redeploy; re-adding the same key replaces it — idempotent). */
|
||||
export function passiveBuff(battle, u, key, mods, extra = {}) {
|
||||
return battle.addBuff(u, { key, mods, persist: true, allowDead: true, refresh: 'replace', ...extra });
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// dealDamage order: (element → gauge path) | invulnerable? → 'hit' hook (mutable DamageInfo, may set cancel)
|
||||
// → dodge (phys/arts, canDodge) → mitigation (phys: DEF, arts: RES, true: none)
|
||||
// → × source dmgDealtMul (× phys/artsDealtMul) × target dmgTakenMul (not for 元素伤害) × type-taken mul × dmg.mul
|
||||
// → 限伤 (leaders in boss / hidden battles: a hit of ceil(final) ≥ BOSS_HIT_LIMIT is cancelled, see leaderHitCancelled)
|
||||
// → shields (hit-negating barriers first, then HP shields) → HP loss (boss pool routing) → 'damaged' hook
|
||||
// → SP-on-hurt / TAKE_DAMAGE trigger → fatal/kill.
|
||||
// Phys: max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 5 %·A); Arts: max(A×(1 − R′/100), 5 %·A) with
|
||||
@@ -45,6 +46,7 @@
|
||||
// has `hitSleep` or the damage carries `ignoreSleep`.
|
||||
|
||||
import { MIN_DAMAGE_RATIO, ELEMENT, ELEMENT_ORDER, PALSY_MAX } from './constants.js';
|
||||
import { BOSS_HIT_LIMIT } from '../../shared/constants.js';
|
||||
|
||||
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
|
||||
|
||||
@@ -112,6 +114,26 @@ export function mitigate(amount, type, target, ign = {}) {
|
||||
return amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 限伤 — the official boss-hit limit (shared/constants.js BOSS_HIT_LIMIT, docs/research/11-limits-official.md §2;
|
||||
* client `AutoChessStepModeManager._OnBossEnemyTakeDamage`: `d = ceil(value); if (d >= 300000) modifier.Cancel()`).
|
||||
* True when the hit of `amount` HP about to land on `target` is cancelled: the target is a leader (`isBoss`: the
|
||||
* tag-'boss' units — data/bosses.json enemyKey, the official IsBossEnemy list — and their mirrored copies; never a part,
|
||||
* escort, drone or other minion), the battle is a boss / hidden one (the client's step mode: every boss battle outside
|
||||
* training) and ceil(amount) ≥ BOSS_HIT_LIMIT. The cancel is whole (no clamp): no HP / pool loss, no credit, no damage
|
||||
* number. A 'hitCap' fx event `{ id, n: ceil(amount) }` marks it for the client, which draws nothing — the official
|
||||
* shows no number [ASSUMED]. Every HP-damage kind is checked as the official `modifier.isDamage` (phys, arts, true,
|
||||
* 元素伤害 incl. element bursts, DoT ticks — they all come through dealDamage — and losses passed on to a leader through
|
||||
* Battle.loseHp); element 损伤 (gauge fill, 'element') removes no HP and is never checked. Deterministic (Math.ceil of
|
||||
* the same double on every engine).
|
||||
*/
|
||||
export function leaderHitCancelled(battle, target, amount) {
|
||||
if (!(BOSS_HIT_LIMIT > 0) || !target || !target.isBoss || (battle.kind !== 'boss' && battle.kind !== 'hidden')) return false;
|
||||
if (!(Math.ceil(amount) >= BOSS_HIT_LIMIT)) return false;
|
||||
battle.fx('hitCap', { x: target.x, y: target.y, id: target.id, n: Math.ceil(amount) });
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Absorb damage with shields on `target`. Returns the remaining amount. */
|
||||
export function absorbShields(battle, target, amount) {
|
||||
if (amount <= 0) return 0;
|
||||
@@ -196,6 +218,9 @@ export function dealDamage(battle, source, target, dmgIn) {
|
||||
mul *= type === 'phys' ? ts.physTakenMul : type === 'arts' ? ts.artsTakenMul : type === 'elemental' ? ts.elementalTakenMul : ts.trueTakenMul;
|
||||
final *= mul;
|
||||
if (!(final > 0) || !Number.isFinite(final)) final = 0;
|
||||
// 限伤: a leader's hit of ≥ BOSS_HIT_LIMIT in a boss / hidden battle is cancelled before it reaches shields / HP — what
|
||||
// ran before it (the attack, its SP, `hit` hook effects, separate element 损伤) stays; nothing after it happens
|
||||
if (final > 0 && leaderHitCancelled(battle, target, final)) return 0;
|
||||
final = absorbShields(battle, target, final);
|
||||
return applyHpLoss(battle, source, target, final, dmg);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user