feedback5: merge W5V — the owner's decisions of 2026-10-06: 望's placed 棋子 take and return her stock (her skills cycle all battle), skill triggers through summon ranges (麦哲伦 S1, 令 S3, 电弧 S2 / S3; summoner.js summonTriggerArea), a melee 突袭 member prefers a tile where it can block (GitHub #148); from the sources: 泥岩 S3 闭锁 releases and stops blocking (PRTS 异常效果 不可阻挡), 忍冬 / 深靛 / 阿 pinned as they are, the 自选 kits' 庇护 copies use holdProtect; DESIGN §24.2's #148 note points to §25; golden: roster-040, boss-boss_2 / 3 / 4 / 6 / 7-pair, hidden-boss_10-pair moved (W5V's own; the sleep-port moves kept)

This commit is contained in:
sganggs committed 2026-10-06 18:33:11 +08:00
commit 73b810ec45
27 files changed
+746 -334

No files matched your search

@@ -11,7 +11,8 @@ export default {
// 柔光缚目: attacks bind the target `duration` s with `prob`; bound enemies are never chosen as targets.
// Alternate S1 灯塔守卫者: for `duration` s the skill range (4-1), attack interval ×(1 + base_attack_time) (PRTS:
// "大幅度缩短(-80%)" ⇒ ×0.2 — a flat −0.8 s on 3 s would be less than S2's "略微缩短 ×0.7"), every attack hits for
// attack@atk_scale × ATK arts. Elite module MSC-X (store 4) is the mystic profile of the module trait (TUNE.mystic);
// attack@atk_scale × ATK arts — its stored energies too (PRTS S1 备注 "该技能的"每次攻击的攻击倍率"会实时应用在特性积攒的"攻击
// 能量"抛射物上"; the client's ChargeAttackS1 reads the same atk_scale), not the branch's general 100 %. Elite module MSC-X (store 4) is the mystic profile of the module trait (TUNE.mystic);
// the store itself is the shared 秘术师 profile (professions.js installMystic: at the attack check, PRTS 分支特性信息).
chess_char_1_17_a: (bb, chess, def) => {
const t = talentBb(chess, 0);
@@ -20,7 +20,9 @@ export default {
// regen; 精锐 module SOL-X: ATK/DEF + while blocking
// S1 小施惩戒 (charges): next attack + extra_damage_ratio × ATK arts and +cost DP; S2 坠刃拷问 (charges, the data rule
// SKILL_RANGE: fires once an enemy is inside its own range 3-12, charges 3 s apart): +cost DP, ≤ max_target enemies of that range take atk_scale arts and
// 停顿 — the ones already 停顿 are also stunned. (SOL-Y "首次部署时部署费用-4": the initial deployment is free.)
// 停顿 — the ones already 停顿 are also stunned; its targets come from the 3-12 only (PRTS S2 备注 "可对空;无法选中被自身
// 阻挡而没有位于技能范围内的敌人" — an S2 note, not S3's: S3 隐狐之艺 has none, its client selector is the standard
// 「同时攻击阻挡的所有敌人」 one). (SOL-Y "首次部署时部署费用-4": the initial deployment is free.)
chess_char_3_18_a: (bb, chess, def) => {
const d = defOf(chess, def);
const t0 = talentBb(d, 0), t1 = talentBb(d, 1);
@@ -8,10 +8,12 @@ import {
export default withDefaults({
// ===== 泥岩 (unyield) S2 岩崩锤 — next attack: heal 4 %, 190 % phys to all ground enemies around, 30 % stun; talents
// S1 防御力强化·γ型 (TAKE_DAMAGE); S3 秽壤的血脉 (10 s dormant — cannot act, takes no damage, keeps blocking —
// with enemies around −60 % speed; then ground enemies around stunned 3 s/3.5 s and for the rest of the skill
// BAT −0.3 s, ATK/DEF up, attacks every blocked enemy); module UNY-Y (沃土的愿景): ATK/DEF +8 % with no ally
// on the 8 tiles around
// S1 防御力强化·γ型 (TAKE_DAMAGE); S3 秽壤的血脉 (10 s dormant — PRTS 备注 "实际将会进入闭锁状态": 闭锁 = 强制缴械 +
// 无敌 + 不可阻挡 (PRTS 异常效果: SHELTERING; 不可阻挡 BLOCK_FREE "无法阻挡/被阻挡,自动解除阻挡") — she makes no attack,
// takes no damage and blocks nobody: the enemies she held walk on (flag noBlock — "keeps blocking" until 0.2.0
// WV); the 备注's 眩晕 / 冻结 / 沉默 反制 are not modelled — with enemies around −60 % speed; then ground
// enemies around stunned 3 s/3.5 s and for the rest of the skill BAT −0.3 s, ATK/DEF up, attacks every blocked
// enemy); module UNY-Y (沃土的愿景): ATK/DEF +8 % with no ally on the 8 tiles around
chess_char_4_18_a: (bb, chess, def) => {
const t0 = tbb(def, 0), t1 = tbb(def, 1);
const tb = def.traitBb || {};
@@ -25,7 +27,8 @@ export default withDefaults({
attack: { hitAllBlocked: true },
onStart({ battle, unit }) {
unit.mem.mudS3 = { awake: false, t: 0 };
battle.addBuff(unit, { key: 'mudrok:dormant', duration: sleep + 1, flags: { invulnerable: true, disarm: true }, visible: true });
// 闭锁: 强制缴械 + 无敌 + 不可阻挡 (her blocked enemies are released: ai.js lets go of a noBlock blocker)
battle.addBuff(unit, { key: 'mudrok:dormant', duration: sleep + 1, flags: { invulnerable: true, disarm: true, noBlock: true }, visible: true });
battle.fx('stone', { x: unit.x, y: unit.y, id: unit.id });
},
onTick({ battle, unit, dt }) {
+3 -1
View File
@@ -43,7 +43,9 @@
// +attack_speed (S3) for the skill's duration (per 阿 on the target: independentCharacterSource). [ASSUMED] the volley
// lands at once (its 0.42 s of shots and flight are not modelled); with no ally in his range the cast does nothing (the
// client's sequence stops when its attack finds no target) — his skill still runs and spends its SP: the data's DEFAULT
// trigger needs an enemy, not an ally (reported as an open question).
// trigger needs an enemy, not an ally — settled by the sources in 0.2.0 WV: no skillTriggerDataList row names 阿 or 怪杰
// (the basic strategy), the skill prefabs cast with no target and keep the SP spent (skchr_haak_2 / _3 `_allowNoTarget`
// 1, `_checkHasTargetBeforeDoCast` 0, `_recoverSpIfNoTarget` 0) and PRTS 阿 has no note on it.
import { num, talentBb, moduleBb, moduleOn, skillRec, statBuff, toggleBuff, up } from '../shared/tier1.js';
import { toLocal } from '../../../dir.js';
+1 -2
View File
@@ -47,7 +47,7 @@
// invulnerable / untargetable one) and while it stays levitated takes atk_scale × her ATK arts damage per second (the
// first after 0.5 s, then every `interval` s; 持续伤害, not dodgeable; 无来源 at her last ATK once she left).
import { num, up, traitBb, talentBb, skillRec, batMod, giveSp } from '../shared/tier1.js';
import { num, up, traitBb, talentBb, skillRec, batMod, giveSp, protectMods } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { COLS } from '../../../constants.js';
@@ -58,7 +58,6 @@ const S3 = 'skchr_haruka_3';
export const BUBBLE = 'haruka:bubble';
/** The S3 levitation damage (haruka_s_3_debuff). */
export const FLOAT_DOT = 'haruka:float';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
/** 浮光泡影: the break delay when the talent carries no `interval`. */
const BREAK_DELAY = 0.5;
/** S3's levitation damage: the first tick (haruka_s_3_debuff firstTriggerInterval). */
+2 -5
View File
@@ -47,7 +47,7 @@
// - S3 披荆斩棘 (MANUAL, hurt SP; data DEFAULT, rawRule TAKE_DAMAGE): at once barrier +hp_ratio × max HP (topping T1's,
// capped), then `duration` s ATK +atk, attack interval +base_attack_time s (a flat +0.9 on 1.6 s), taunt +taunt_level.
import { num, traitBb, talentBb, skillRec, toggleBuff, batMod, up, enemiesInGrid } from '../shared/tier1.js';
import { num, traitBb, talentBb, skillRec, toggleBuff, batMod, up, enemiesInGrid, holdProtect, PROTECT } from '../shared/tier1.js';
import { isHpLoss } from '../../../damage.js';
const S1 = 'skchr_judge_1';
@@ -60,9 +60,6 @@ const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1,
/** judge_s_2[aoe]: firstTriggerInterval 0.9 s, then triggerInterval 1 s. */
const S2_FIRST = 0.9;
const S2_EVERY = 1;
/** 庇护 (ba.protect): one "同名效果取最高" effect per unit, whoever grants it (battle.applyStrongest). */
const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const tagged = (d, tag) => !!(d && Array.isArray(d.tags) && d.tags.includes(tag));
@@ -79,7 +76,7 @@ export default {
/** S2's 庇护 (damage_resistance[inf] for the skill's time). */
const protect = (battle, unit, skill) => {
const dr = num(b2.damage_resistance);
if (dr > 0) battle.applyStrongest(unit, PROTECT, { duration: Math.max(0.05, skill.timeLeft), value: dr, mods: protectMods, source: unit });
holdProtect(battle, unit, dr, Math.max(0.05, skill.timeLeft), unit); // 庇护: the shared effect (同名效果取最高)
};
/** judge_t_1's shield_scale: 1, + S2's shield_scale while S2 runs. */
const shieldScale = (unit) => 1 + (unit.skill?.active && unit.skill.id === S2 ? num(b2.shield_scale) : 0);
+11 -3
View File
@@ -42,7 +42,9 @@
// The 备注's cast with no target cannot happen here: the data's DEFAULT casts it on an attack.
// - S3 宁作吾 (MANUAL, data DEFAULT, 30 s): she and her summons ATK +atk, DEF +def; every `interval` (0.5) s from the moment
// it covers a summon, every ground enemy on that summon's x-5 takes atk_scale × 令's ATK arts (溅射: not dodgeable, her
// damage — PRTS); +cnt held when it ends (not when she leaves). Passive: the 弦惊 merge — at a 弦惊's deployment, another
// damage — PRTS); +cnt held when it ends (not when she leaves). Its cast: the data's DEFAULT, and — the owner's
// larger-range rule of 2026-10-06 (its pulses act on each summon's x-5) — also a ground enemy on a standing summon's x-5
// (shared/summoner.js summonTriggerArea, checked every tick). Passive: the 弦惊 merge — at a 弦惊's deployment, another
// basic 弦惊 of hers on its range ⇒ that one turns 高级形态 and the new one is absorbed (withdrawn); else, the new one
// standing on the range of one or more basic ones ⇒ it turns 高级形态 and the latest deployed of them is absorbed. 高级形态
// (until it leaves the field): its skill's 2.max_hp / 2.atk / 2.def (直接乘算), RES ×(1 + 2.magic_resistance), attack
@@ -53,7 +55,7 @@
// stats). Its attributes are in the stats.
import { num, talentBb, traitBb, moduleOn, skillRec, batMod, giveSp, up } from '../shared/tier1.js';
import { summonDeck, holdBuff, tokenStat } from '../shared/summoner.js';
import { summonDeck, holdBuff, tokenStat, summonTriggerArea } from '../shared/summoner.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { acquireTargets } from '../../../ai.js';
import { COLS, PROJECTILE_SPEEDS } from '../../../constants.js';
@@ -72,6 +74,8 @@ export const RECYCLE_DELAY = 1.2;
const X5 = Object.freeze([[1, 0], [0, -1], [0, 0], [0, 1], [-1, 0]]);
const TAG_S2 = 'ling:s2';
const TAG_S3 = 'ling:s3';
/** S3's pulses: ground enemies only ("不可对空"). */
const GROUND = Object.freeze({ canHitFly: false });
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const skillOn = (u, id) => !!(u && u.skill && u.skill.active && u.skill.id === id);
@@ -127,7 +131,7 @@ function soulKit(t, owner, opts) {
if (battle.time + 1e-9 < s.mem.lingAoeAt) return;
s.mem.lingAoeAt += iv;
const keys = absoluteRangeKeys(X5, s.tileR, s.tileC, s.dir, 0);
for (const e of battle.enemiesInKeys(keys, s, { canHitFly: false })) {
for (const e of battle.enemiesInKeys(keys, s, GROUND)) {
if (!e.alive) continue;
battle.dealDamage(owner, e, { amount: owner.s.atk * num(opts.b3.atk_scale), type: 'arts', isSkill: true, isSplash: true, canDodge: false, tags: ['skill', TAG_S3] });
}
@@ -241,6 +245,10 @@ export default {
} },
// 随付笺咏醉屠苏 lives in the deck's onLeave above
],
install(battle, unit) {
// S3's cast: a ground enemy on a standing summon's x-5 too (the owner's larger-range rule, 2026-10-06)
summonTriggerArea(battle, unit, S3, (t) => ({ keys: absoluteRangeKeys(X5, t.tileR, t.tileC, t.dir, 0), profile: GROUND }));
},
};
},
};
+9 -3
View File
@@ -38,7 +38,9 @@
// - S1 高效制冷模块 (MANUAL, data DEFAULT, 15 s): passive (while S1 is picked and not running) every attack@interval (3) s from
// her deployment / the skill's end, every enemy (air units too) on her attack range and on each of her standing drones'
// ranges (龙腾.F: x-4) takes 停顿 attack@sluggish s; while it runs, every 3 s from the cast, 束缚 attack@frozen_duration s
// instead. It acts while she is stunned or silenced (the buff is neither — isStunnable / isSilenceable 0).
// instead. It acts while she is stunned or silenced (the buff is neither — isStunnable / isSilenceable 0). Its cast:
// the data's DEFAULT, and — the owner's larger-range rule of 2026-10-06 (its 束缚 acts through the drones' ranges) —
// also an enemy (air units too) on a standing drone's range (shared/summoner.js summonTriggerArea, checked every tick).
// - S2 激光开采模块 (MANUAL, data DEFAULT, 15 s): she and her drones ASPD +attack_speed; a 龙腾.L strikes every enemy on its tile
// (群体) while it runs. S3 武装打击模块 (MANUAL, data DEFAULT, 15 s): she and her drones ATK +atk; 龙腾.A's blast radius
// BLAST_S3 (1.25). Passive parts ("无人机可以部署在近战位 / 远程位"): placement — the piece's position, nothing in battle.
@@ -51,10 +53,12 @@
// drones' lower costs (the module variant); stage 2+ T1 "最多同时部署4个", stage 3 "属性更强的" (the module variant's stats).
import { num, talentBb, traitBb, moduleOn, skillRec, up } from '../shared/tier1.js';
import { summonDeck, holdBuff, tokenStat } from '../shared/summoner.js';
import { summonDeck, holdBuff, tokenStat, summonTriggerArea } from '../shared/summoner.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { COLS } from '../../../constants.js';
/** S1's selection on her range and her drones' (air units too). */
const ANY = Object.freeze({ canHitFly: true });
const S1 = 'skchr_mgllan_1';
const S2 = 'skchr_mgllan_2';
const S3 = 'skchr_mgllan_3';
@@ -171,6 +175,8 @@ export default {
install(battle, unit) {
// S1 高效制冷模块: every attack@interval s, 停顿 (passive) / 束缚 (running) on her range and her drones' ranges
if (picked !== S1) return;
// its cast: an enemy on a standing drone's range too (the owner's larger-range rule, 2026-10-06)
summonTriggerArea(battle, unit, S1, (d) => ({ keys: d.rangeKeys, profile: ANY }));
const iv = num(b1['attack@interval'], 3);
if (!(iv > 0)) return;
const restart = (c) => { if (c.unit === unit) unit.mem.mgllanPulseAt = battle.time + iv; };
@@ -186,7 +192,7 @@ export default {
if (!(duration > 0)) return;
const done = new Set();
for (const src of [unit, ...(unit.mem.summonDeck?.standing() ?? [])]) {
for (const e of battle.enemiesInKeys(src.rangeKeys, src, { canHitFly: true })) {
for (const e of battle.enemiesInKeys(src.rangeKeys, src, ANY)) {
if (done.has(e)) continue;
done.add(e);
battle.applyStatus(e, key, { duration, source: src });
+3 -6
View File
@@ -71,7 +71,7 @@
// [ASSUMED] (beyond op-ironmn.js's): the 监工专员 loses its 1 HP per pulse with or without an operator in range (dec_blood is a
// passive buff of the ability); the combined range one step deep; the 小工程师's 1 s heal / SP timer runs from its deployment.
import { num, talentBb, traitBb, skillRec, up, giveSp } from '../shared/tier1.js';
import { num, talentBb, traitBb, skillRec, up, giveSp, holdProtect } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { COLS } from '../../../constants.js';
import { frontOf } from '../../../dir.js';
@@ -86,9 +86,6 @@ export const NSTBLD = 'token_10061_nasti_nstbld';
const DEVICES = new Set([NSTDEF, NSTCHR, NSTBLD]);
/** A device's 1-1 when its def carries none. */
const FRONT1 = Object.freeze([[0, 0], [0, 1]]);
/** 庇护 (ba.protect): one "同名效果取最高" effect per unit, whoever grants it (battle.applyStrongest; op-judge.js, op-zumama.js). */
const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
/** 注意安全's 庇护 refresh: a little longer than the check period, so it never lapses while it holds. */
const T2_PERIOD = 0.25;
const PROTECT_HOLD = T2_PERIOD + 0.05;
@@ -402,13 +399,13 @@ export default {
if (!up(unit)) { triggered = false; timers.clear(); return; }
if (!triggered) triggered = battle.enemies.some((e) => e.alive && !e.hidden && RANGED_WAYS.has(String(e.def?.applyWay ?? '').toUpperCase()));
if (!triggered) {
if (resBase > 0) battle.applyStrongest(unit, PROTECT, { duration: PROTECT_HOLD, value: resBase, mods: protectMods, source: unit });
holdProtect(battle, unit, resBase, PROTECT_HOLD, unit); // 庇护: the shared effect (同名效果取最高)
return;
}
const who = [unit, ...battle.allyUnits.filter((a) => a !== unit && up(a) && !a.hidden && a.kind === 'op' && RANGED_POS.has(String(a.def?.position ?? '').toUpperCase()))];
for (const a of [...timers.keys()]) if (!who.includes(a)) timers.delete(a);
for (const a of who) {
if (resPlus > 0) battle.applyStrongest(a, PROTECT, { duration: PROTECT_HOLD, value: resPlus, mods: protectMods, source: unit });
holdProtect(battle, a, resPlus, PROTECT_HOLD, unit);
if (!(spEvery > 0)) continue;
if (!timers.has(a)) { timers.set(a, battle.time + spEvery); continue; }
if (battle.time + 1e-9 < timers.get(a)) continue;
+12 -2
View File
@@ -43,10 +43,14 @@
// - S2 环形鳞地 (MANUAL, data DEFAULT, 25 s): +cnt held; she and her summons ATK +atk (base_attack_time 0: no change); 赛柯's
// attacks fire BULLET_OFFSETS (three bullets: its centre, 0.2 tile left-up and right-down — fixed, PRTS) that fly its token
// skill's attack@projectile_life_time s (0.6 at rank 4, 0.8 at rank 7: "子弹飞行距离+1"); its 3-2 range is unchanged at ranks
// 4 / 7 (PRTS). Passive "会持续向前方发射子弹": 赛柯's data.
// 4 / 7 (PRTS). Passive "会持续向前方发射子弹": 赛柯's data. Its cast: the data's DEFAULT, and — the owner's larger-range
// rule of 2026-10-06 (it acts through 赛柯's shots) — also a ground enemy on a standing summon's range (its 3-2: the
// bullets reach no air unit; shared/summoner.js summonTriggerArea, checked every tick).
// - S3 手牵手 (MANUAL, data DEFAULT, 30 s): +cnt held; she and her summons ATK +atk; before each damage of her attacks and of
// any damage of her summons on an enemy: 停顿 `sluggish` s and 法术脆弱 (artsFragile damage_scale − 1, "同名效果取最高") for
// weak[magic][limit] s (a dodged hit still carries them [ASSUMED]). Passive "召唤物可部署在远程位…协同攻击": 桑特拉 (above).
// Its cast: the data's DEFAULT, and — the owner's larger-range rule of 2026-10-06 (it acts through her summons' damage)
// — also an enemy (air units too: S3 备注 "召唤物可对空") on a standing summon's range (summonTriggerArea).
// - Modules SO-A 电弧特勤证章 / SO-B 新起点: their trait and talent parts act "在【岁的界园志异】中" only (the client gates them to
// the 集成战略 theme rogue_yan / validInGameTag roguelike): N/A here — the module talents the composed record carries
// (SO-A's hidden respawn_time / prob / sp, SO-B's attack@max_target and x-5 recall) are ignored; their attributes are in the
@@ -54,7 +58,7 @@
// its tests.
import { num, talentBb, skillRec, up } from '../shared/tier1.js';
import { summonDeck, holdBuff, tokenStat } from '../shared/summoner.js';
import { summonDeck, holdBuff, tokenStat, summonTriggerArea } from '../shared/summoner.js';
import { acquireTargets } from '../../../ai.js';
import { aggregateMods } from '../../../buffs.js';
import { dirVec } from '../../../dir.js';
@@ -79,6 +83,9 @@ const BULLET_OFFSETS = Object.freeze([[0, 0], [-0.2, 0.2], [0.2, -0.2]]);
const LINK_SPEED = 50;
const SHIELD_TYPES = Object.freeze(['phys', 'arts', 'true']);
const KEY_BARRIER = 'radian:s1:barrier';
/** The summons' areas a skill casts on (summonTriggerArea): 赛柯's bullets hit ground enemies only, S3's summons air too. */
const GROUND = Object.freeze({ canHitFly: false });
const ANY = Object.freeze({ canHitFly: true });
const TAG_BULLET = 'radian:bullet';
const TAG_LINK = 'radian:link';
@@ -268,6 +275,9 @@ export default {
// 加油~ lives in the deck's install above (its 鼓舞 at each deployment and every second)
],
install(battle, unit) {
// S2 / S3's cast: an enemy on a standing summon's range too (the owner's larger-range rule, 2026-10-06)
summonTriggerArea(battle, unit, S2, (t) => ({ keys: t.rangeKeys, profile: GROUND }));
summonTriggerArea(battle, unit, S3, (t) => ({ keys: t.rangeKeys, profile: ANY }));
// S3 手牵手: before each damage of her attacks / of her summons' damage on an enemy, 停顿 + 法术脆弱
if (picked !== S3) return;
const slug = num(b3.sluggish), dur = num(b3['weak[magic][limit]']), frag = num(b3.damage_scale, 1) - 1;
+2 -5
View File
@@ -54,7 +54,7 @@
// 单位时释放技能"), which tools/build-data.mjs (TRIGGER_ALLY_RULES) writes as the engine's SKILL_RANGE + `allies` on the
// skill range x-2 (an injured, healable allied unit there, herself included; 古米 S1's option).
import { num, traitBb, skillRec, batMod, up, giveSp } from '../shared/tier1.js';
import { num, traitBb, skillRec, batMod, up, giveSp, holdProtect } from '../shared/tier1.js';
import { COLS } from '../../../constants.js';
import { effectiveProfile, acquireTargets, performAttack } from '../../../ai.js';
@@ -80,9 +80,6 @@ const PROF_NEEDED = 3;
const SUI_NEEDED = 4;
/** 【岁】: character_table groupId 'sui' (data/backups.json diy.operators powers carry it for the 自选 picks). */
const SUI = new Set(['char_2014_nian', 'char_2015_dusk', 'char_2023_ling', 'char_2024_chyue', 'char_2025_shu', 'char_2026_yu', 'char_2027_wang']);
/** 庇护 (ba.protect): one "同名效果取最高" effect per unit, whoever grants it (battle.applyStrongest). */
const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
const FARM = 'talent:shu:farm';
const HARVEST = 'skill:shu:harvest';
@@ -122,7 +119,7 @@ export default {
if (hp > 0 && !(cur && cur.source !== unit && (cur.data?.v ?? 0) > hp && cur.timeLeft > 0.05)) {
battle.addBuff(a, { key: FARM, duration: FARM_HOLD, mods: { hpRegen: hp }, source: unit, data: { v: hp }, tags: ['talent'] });
}
if (dr > 0) battle.applyStrongest(a, PROTECT, { duration: FARM_HOLD, value: dr, mods: protectMods, source: unit });
holdProtect(battle, a, dr, FARM_HOLD, unit); // 庇护: the shared effect (同名效果取最高)
}
};
/** Sow the tiles of `grid` around (r, c) — only tiles that can be deployed on or walked. Returns the new ones. */
+3 -6
View File
@@ -59,7 +59,7 @@
// held like the hammer's (PRIO_UNDYING_HELD −99).
// Statuses: none applied to enemies. Damage: arts normal attacks only.
import { num, up, traitBb, moduleBb, talentBb, skillRec, once } from '../shared/tier1.js';
import { num, up, traitBb, moduleBb, talentBb, skillRec, once, holdProtect } from '../shared/tier1.js';
import { releaseSkillSummon } from '../../tokens.js';
import { holdsUndying, PRIO_UNDYING_HELD, PRIO_REVIVE, PRIO_RESPAWN } from '../../items/battle.js';
import { absoluteRangeKeys } from '../../../targeting.js';
@@ -69,9 +69,6 @@ const S1 = 'skchr_slent2_1';
const S2 = 'skchr_slent2_2';
const S3 = 'skchr_slent2_3';
export const LAMP = 'token_10029_slent2_protrb';
/** 庇护 (ba.protect, the client's damage_resistance[inf]): one "同名效果取最高" effect per unit (battle.applyStrongest). */
export const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
/** The client's trigger period of slent2_t_1 / slent2_t_2 (talent abilities, triggerInterval 0.1). */
export const TALENT_IV = 0.1;
/** Lifetime of a refreshed effect: a little longer than the refresh period, so it never lapses while it holds. */
@@ -136,7 +133,7 @@ function lampKit(t, owner, t1) {
const keys = new Set(absoluteRangeKeys(grid, lamp.tileR, lamp.tileC, lamp.dir, 0));
for (const a of alliesOn(battle, lamp, keys)) {
const v = shelterOf(tb, a, scale);
if (v > 0) battle.applyStrongest(a, PROTECT, { duration: HOLD, value: v, mods: protectMods, source: lamp });
holdProtect(battle, a, v, HOLD, lamp); // 庇护 (the client's damage_resistance[inf]): the shared effect
}
}, { owner: lamp, immediate: true });
},
@@ -194,7 +191,7 @@ export default {
const now = new Set(allies);
for (const a of allies) {
const v = shelterOf(t1, a, scale);
if (v > 0) battle.applyStrongest(a, PROTECT, { duration: HOLD, value: v, mods: protectMods, source: unit });
holdProtect(battle, a, v, HOLD, unit);
const mult = isRhine(a) ? 2 : 1;
if (maxHp > 0) battle.applyStrongest(a, MAXHP_KEY, { duration: HOLD, value: maxHp * mult, mods: (x) => ({ hpPct: x }), source: unit });
if (regenRatio > 0 && a.hpRatio < regenBelow - 1e-9) {
+16 -5
View File
@@ -22,12 +22,19 @@
// may). Module TRP-X "部署费用更低(-1)…可同时部署的陷阱数量提升(+1)": the 棋子 variant's cost 2 (the DP of its returns) and
// deploy limit 7 (the hand count — the match's); its attributes in the stats.
// - T1 铸子 (cnt / attack@max_spawn_cnt; "最多拥有7枚"): the 棋子 are hand pieces the player places; they deploy with the board
// for free and without using her stock (stock = cnt at the start, at most STOCK_CAP; the skills add to it). 棋子 / 跟子
// for free. Her stock (持有库存: the 棋子 in her hand — cnt at the start, at most STOCK_CAP; the skills add to it) counts
// them — the owner's decision of 2026-10-06, a deliberate deviation from PRTS 卫戍协议/帮助 (placed summons "无视所属干员的
// 持有状态,不消耗持有数量", which left her stock at cnt: one S1 / S2 cast reached the cap and 阻回 held her SP for the rest
// of the battle): every deployment of a 棋子 (the battle start's and each return) takes one from it, like one she deployed
// herself (never below 0 [ASSUMED: a 7th placed piece of TRP-X's deploy limit takes none]), and a 棋子 that leaves the
// field (set off, or gone with her) goes back into it at once by the official summon rule — its card back in her hand
// while its redeploy time runs (as shared/summoner.js's recall) —, at most the cap [ASSUMED: one over the cap is lost].
// So "立即获得两枚棋子" and the 阻回 at the cap keep cycling through the battle. 棋子 / 跟子
// that touch on a side (上下 / 左右) activate each other and stay active once activated [ASSUMED from the S2 备注]; an
// enemy (air units too) on an active one's tile sets it off: its effect, then it is used up (a 棋子 leaves the field; a
// 跟子 is gone) [ASSUMED: a trap is spent by its trigger]. A spent 棋子 comes back on its tile ("原地再部署") after its
// redeploy time (2 s), paying its cost, while 望 is on the field — its own card returns to the stock as it leaves, so the
// stock does not change [ASSUMED]. That return is the mode's deployment of her card: it counts as a 手动部署 for 铸子 —
// redeploy time (2 s), paying its cost and one of her stock, while 望 is on the field. That return is the mode's
// deployment of her card: it counts as a 手动部署 for 铸子 —
// one 跟子 on a tile next to it (the PRTS order above), at most attack@max_spawn_cnt on the field [ASSUMED: the battle-start
// deployment does not count]. When 望 leaves the field her 棋子 and 跟子 vanish (PRTS 分支特性信息); the 棋子 come back
// with her.
@@ -195,7 +202,7 @@ export default {
const at = battle.time + Math.max(0, num(stone.base.respawnTime));
battle.every(RETRY, (b, sched) => {
if (stone.alive || stone.removed || b.finished) { sched.cancel(); return; }
if (b.time + 1e-9 < at || !up(unit)) return;
if (b.time + 1e-9 < at || !up(unit) || unit.mem.wang.stock < 1) return; // (one of her stock: the deployment takes it)
const r = stone.homeR, c = stone.homeC;
const s3on = unit.skill?.id === S3 && unit.skill.active && inHerRange(unit, r, c);
if (groundFoeOn(b, r, c) && !s3on) return; // "陷阱无法放置于敌人已在的格子中"
@@ -219,12 +226,16 @@ export default {
battle.addBuff(unit, { key: 'trait:wangStone', flags: { untargetable: true }, persist: true, allowDead: true });
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
unit.mem.wangNode = { seq: ++owner.mem.wang.seq, act: false, h: false, v: false, stacks: 0 };
const w = owner.mem.wang;
unit.mem.wangNode = { seq: ++w.seq, act: false, h: false, v: false, stacks: 0 };
w.stock = Math.max(0, w.stock - 1); // it occupies her stock (the owner's decision of 2026-10-06)
}, { owner: unit });
battle.on('death', (ctx) => {
if (ctx.unit !== unit || battle.finished) return;
unit.removed = false; // the piece is kept (Battle.redeploy accepts it; its hooks stay)
unit.mem.wangNode = null;
const w = owner.mem.wang;
w.stock = Math.min(stockCap, w.stock + 1); // its card goes back into her stock (the official summon rule)
scheduleReturn(battle, owner, unit);
}, { owner: unit, priority: -10 });
},
+2 -5
View File
@@ -42,7 +42,7 @@
// she is stunned `stun` s (zumama_s_3 ON_BUFF_FINISH 'stun'; a stunned operator blocks nobody).
// - Melee physical, ground-only (data canHitFly false), block 1, range 1-1; ground enemies target her (no 起飞 / 迷彩).
import { num, traitBb, skillRec, toggleBuff, batMod, up } from '../shared/tier1.js';
import { num, traitBb, skillRec, toggleBuff, batMod, up, holdProtect } from '../shared/tier1.js';
const S1 = 'skchr_zumama_1';
const S2 = 'skchr_zumama_2';
@@ -51,9 +51,6 @@ const S3 = 'skchr_zumama_3';
const T1_PERIOD = 0.1;
/** 庇护 refresh: a little longer than the check period, so it never lapses while it holds. */
const PROTECT_HOLD = T1_PERIOD + 0.05;
/** 庇护 (ba.protect): one "同名效果取最高" effect per unit, whoever grants it (battle.applyStrongest). */
const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
/** S2's blockee stun, renewed every tick while it holds (three ticks long). */
const STUN_PULSE = 0.1;
const VALOR = 'talent:zumama:valor';
@@ -101,7 +98,7 @@ export default {
const has = unit.findBuff(VALOR);
if (high && !has && scale !== 1) battle.addBuff(unit, { key: VALOR, mods: { atkScaleMul: scale }, tags: ['talent'] });
else if (!high && has) battle.removeBuff(unit, VALOR);
if (up(unit) && !high && dr > 0) battle.applyStrongest(unit, PROTECT, { duration: PROTECT_HOLD, value: dr, mods: protectMods, source: unit });
if (up(unit) && !high) holdProtect(battle, unit, dr, PROTECT_HOLD, unit); // 庇护: the shared effect
};
battle.every(T1_PERIOD, check, { owner: unit, immediate: true });
battle.on('deploy', (ctx) => { if (ctx.unit === unit) check(); }, { owner: unit });