feedback5 WV: 麦哲伦 S1, 令 S3 and 电弧 S2 / S3 also cast on an enemy inside the area they act through around a standing summon (drones' ranges, each summon's x-5 ground only, 赛柯's range ground only, 桑特拉's range) — the owner's larger-range rule (the owner's decision of 2026-10-06), via a shared summonTriggerArea and an additive { keys, profile } entry for skill.addTriggerRange; golden unchanged (no corpus battle places a 自选 summon)

This commit is contained in:
sganggs committed 2026-10-06 18:04:48 +08:00
1 parent ed5c1f2ab8
commit 7aede84be9
7 files changed
+203 -17

No files matched your search

+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 });
+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;
+31 -1
View File
@@ -1,6 +1,7 @@
// server/sim/content/kits/shared/summoner.js — the 召唤师 (summoner) branch in 卫戍: the summon deck the 自选 kits of 麦哲伦
// (ops/op-mgllan.js), 令 (ops/op-ling.js) and 电弧 (ops/op-radian.js) share — the holding (持有), the return of a placed piece
// that left the field, recalls (回收) and the summons vanishing with their owner. Kit contract and rules: ../README.md.
// that left the field, recalls (回收), the summons vanishing with their owner, and the skills that cast on an enemy in a
// summon's area (summonTriggerArea: the owner's larger-range rule, 2026-10-06). Kit contract and rules: ../README.md.
//
// Sources: the client's battle data (charpack char_248_mgllan / char_2023_ling / char_4195_radian: Talents/1
// charge_token[born] — RechargeToken `cnt` at every deployment of the owner; CommonAbilities die_to_kill_token — KillTokens
@@ -42,6 +43,35 @@ export function holdBuff(battle, u, key, want, mods, extra = null) {
battle.addBuff(u, { key, mods, tags: ['skill'], ...extra });
}
/**
* The owner's larger-range rule through summons (the owner's decision of 2026-10-06; ACTIVE_RANGE, server/sim/skills.js:
* "a skill whose effect reaches farther than the operator's own range casts when an enemy is inside the larger area"):
* when `owner`'s picked skill is `skillId`, it also casts while an enemy is inside the area it acts through around one of
* the owner's summons standing on the field — on top of its data rule (DEFAULT: an enemy in the owner's own range, about
* to attack). A content trigger range (SkillRuntime.addTriggerRange), checked every tick: `area(piece)` gives that
* summon's area as `{ keys, profile }` (absolute tile keys; the enemy profile the effect selects by — `canHitFly` false
* when it cannot reach air units) or null. Used by 麦哲伦 S1 (her drones' ranges), 令 S3 (each summon's x-5) and 电弧 S2 /
* S3 (赛柯's / 桑特拉's ranges). Returns the unregister fn, or null when the skill is another one.
* @param {object} battle
* @param {object} owner the summoner unit (its deck: owner.mem.summonDeck)
* @param {string} skillId
* @param {(piece: object) => ({ keys: number[], profile?: object } | null)} area
*/
export function summonTriggerArea(battle, owner, skillId, area) {
const sk = owner?.skill;
if (!sk || sk.id !== skillId) return null;
return sk.addTriggerRange(() => {
const deck = owner.mem.summonDeck;
if (!deck || !up(owner) || battle.finished) return [];
const out = [];
for (const t of deck.standing()) {
const a = area(t);
if (a && Array.isArray(a.keys) && a.keys.length) out.push(a);
}
return out;
});
}
/**
* A field of a summon's token record at its owner's form and module (data/backups.json `tokens[id].variants[<charId>@
* <status>]`, `byModule[uniEquipId]`): the stats the normalised def drops (`deployLimit`, `deckStack`).
+11 -6
View File
@@ -8,7 +8,8 @@
// Trigger rules (the official 技能策略, PRTS 卫戍协议/帮助 §作战阶段 技能操作; data: tools/build-data.mjs resolveTrigger):
// DEFAULT — the basic strategy: ready + about to attack/heal + enemy / injured ally in the INITIAL range (or blocked by
// a melee unit) — or, checked every tick, an enemy inside one of the content trigger ranges added with
// addTriggerRange: 海嗣, 流形;
// addTriggerRange: 海嗣, 流形, 谬因 S2's beam, the summons' areas a skill acts through (麦哲伦 S1, 令 S3, 电弧 S2 / S3 —
// the owner's larger-range rule, 2026-10-06: kits/shared/summoner.js summonTriggerArea);
// SKILL_RANGE — a MANUAL skill with a 技能范围 of its own: "不通过普通攻击/治疗触发技能,仅在技能范围内存在敌人(无视其
// 不可选中)时释放技能": any living enemy on the trigger grid (the skill range; stealthed / untargetable / flying ones
// too), checked every tick, no attack needed. Kit option `trigger.allies` (+ `hpAtMost`, default 1): a healable,
@@ -135,9 +136,12 @@ export class SkillRuntime {
/**
* Extra DEFAULT-trigger range (海嗣 "攻击范围视为自身攻击范围的延伸", 流形): `fn(battle, unit)` returns a list whose
* entries are ally units (their current `rangeKeys` count while they are on the field) or arrays of absolute tile
* keys. A targetable enemy (flyers included) on those tiles satisfies the DEFAULT rule (and unknown DEFAULT-like
* rules); it is checked every tick, since the unit itself may have nothing to attack. Returns an unregister fn.
* entries are ally units (their current `rangeKeys` count while they are on the field), arrays of absolute tile
* keys, or `{ keys, profile }` — tile keys with the enemy profile the effect selects by (`canHitFly` false: ground
* enemies only — the owner's larger-range rule through a summon's area, kits/shared/summoner.js summonTriggerArea;
* 0.2.0 WV, additive). A targetable enemy (flyers included unless the entry's profile says otherwise) on those tiles
* satisfies the DEFAULT rule (and unknown DEFAULT-like rules); it is checked every tick, since the unit itself may
* have nothing to attack. Returns an unregister fn.
*/
addTriggerRange(fn) {
if (typeof fn !== 'function') return () => {};
@@ -153,10 +157,11 @@ export class SkillRuntime {
const list = b._safe(() => fn(b, u), 'skill.triggerRange', u);
if (!list || typeof list[Symbol.iterator] !== 'function') continue;
for (const x of list) {
let keys = null;
let keys = null, prof = TRIGGER_PROFILE;
if (Array.isArray(x)) keys = x;
else if (x && typeof x === 'object' && x.side === 'ally' && x.alive && x.deployed && !x.hidden) keys = x.rangeKeys;
if (keys && keys.length && b.enemiesInKeys(keys, u, TRIGGER_PROFILE).length) return true;
else if (x && typeof x === 'object' && Array.isArray(x.keys)) { keys = x.keys; if (x.profile) prof = x.profile; }
if (keys && keys.length && b.enemiesInKeys(keys, u, prof).length) return true;
}
}
return false;