feedback5 WJ: 伺夜's 狼群 takes its official 战术点形态 and S1 领袖的呼唤 acts by the pack's state (PRTS 狼群领袖 / S1 备注)

The fatal hit on the last 狼影, or a 撤退 while 伺夜 stands, no longer kills the pack. It enters its 战术点形态, the
remake's knocked-out piece: off the fight (no block, no attack, not targetable), kept, its tile reserved, 0 狼影. After
the 狼影 recovery time it comes back on its tile at full HP with 1 狼影 and a fresh 狼影 cycle. That time is the token
talent's interval, read from the data: 25 s on both chess. Until now it came back after the token's 10 s redeploy time
with 2 狼影.

Both packs share this (tokens.js installWolfTacticalPoint): the prep-placed 狼群 piece and the 伺夜 kit's own pack.
- A return timer carries the form's seq and a return cancels it, so a stale timer never revives the pack early.
- When 伺夜 leaves, a standing pack still leaves with him ('expired', no form). A pack in its form ends it with no
  return; his redeploy brings a fresh pack.

S1 casts when ready while the pack is on the field in either form, and always pays +cost DP:
① in its 战术点形态 the pack comes back at once with 1 狼影 and a fresh cycle;
② below the cap, +1 狼影, the pack's own cycle untouched;
③ at the cap, the pack's HP is reset to max.
S2 still needs a standing pack: its 备注 says nothing of the form [ASSUMED]. A held S2 gift stays on the pack through
the form [ASSUMED].

fx: the kit's pack keeps 'summon' / 'revive', the piece keeps 'wolfShadow' / 'wolfShadowLost'. Entering the form shows
'wolfShadowLost' with count 0. No new kinds, no client change. Data unchanged.

Docs: DESIGN §5.6 (SP_FULL note), PLAYING (作战中, 自动触发), SIM (token pieces), the tokens.js / kit comments.

Tests:
- kits_alt_t3, new: 3_19 战术点形态 (both chess × both packs), S1 ①②③ with the stale-timer guard, 撤退 / 伺夜 leaving.
- kits_alt_t3, rewritten: the S1 no-pack test, and the S2 auto-cast test (no cast in the form, a cast once the pack is
  back).
- kits_t3 3_19 wolf pack and tokens_devices 狼群 board piece: the form, then a return with 1 狼影.

Golden (each fields 伺夜 with his pack knocked out at its last 狼影 while he stands: 25 s out and 1 狼影 instead of
10 s and 2):
- roster-007 and roster-009: the fights change.
- roster-028 (elite S1): S1 brings the pack back at once.
- roster-026 and roster-040: 伺夜 falls before the old return; only the new 'wolfShadowLost' fx moves.
- bond-siracusaShip-high.
- matches solo-NORMAL-1, coop2-NORMAL-3, coop3-ABYSS-5, coop2-NORMAL-8-serverrun: the bots field 伺夜.
This commit is contained in:
sganggs committed 2026-10-05 19:47:12 +08:00
1 parent a98b02477d
commit 4304eb057c
11 files changed
+577 -222

No files matched your search

+125 -11
View File
@@ -21,7 +21,8 @@
// full HP; its and 伺夜's attacks ignore def_penetrate_fixed DEF of enemies it blocks; while 伺夜's
// timed skill runs every attack damage instance (each bite) of the pack / 伺夜 on an enemy it blocks
// adds 伺夜 ATK × bb scale arts; module: ×damage_scale damage from enemies it blocks; is its owner's
// tactician 援军 (1.5× trait), leaves with it; respawns respawnTime s after being killed
// tactician 援军 (1.5× trait), leaves with it; the fatal hit on its last shadow (or a 撤退) ⇒ 战术点形态
// for the talent interval, then back on its tile with 1 shadow (installWolfTacticalPoint, both packs)
// 流形 copy skill (SP from data, golden +sp; starts only when an operator can be copied, else waits ready):
// copies scale × HP/ATK/DEF/RES, block, BAT/ASPD, range and the damage type of the nearest allied
// operator; no attack before its copy; ranged copy splits every N attacks (clone lasts the talent
@@ -74,10 +75,12 @@
// makes the token (Battle.spawnToken also refuses summons the owner's loadout does not produce).
//
// Exports for other content: spawnYanyou, spawnMapChar, findSummonTile, summonToken, tacticalPoint, wolfShadows,
// releaseSkillSummon, SKILL_SUMMON_START_DEPLOY, CAT_SHIELD_KEY, TOKEN_IDS; mapCharTalents and touchGospel (Touch's 攫升 /
// 超脱 and 恳切福音, shared with the Touch 补位 stand-in kit, kits/ops/standin-acmedc.js).
// wolfShadowInterval, installWolfTacticalPoint, wolfTacticalPoint, wolfReturnNow (the 狼群's 战术点形态, shared with the
// 伺夜 kit's own pack, kits/ops/chess_char_3_19-vigil.js), releaseSkillSummon, SKILL_SUMMON_START_DEPLOY, CAT_SHIELD_KEY,
// TOKEN_IDS; mapCharTalents and touchGospel (Touch's 攫升 / 超脱 and 恳切福音, shared with the Touch 补位 stand-in kit,
// kits/ops/standin-acmedc.js).
import { COLS, ROWS, MOVE_SCALE } from '../constants.js';
import { COLS, ROWS, MOVE_SCALE, FORCED_EXIT } from '../constants.js';
import { absoluteRangeKeys, sortEnemyTargets, canTargetEnemy } from '../targeting.js';
import { bodyInKeys, bodyOnTile } from '../body.js';
import { hasHp } from '../damage.js';
@@ -607,14 +610,109 @@ function paperDoll(bb, raw, def) {
};
}
/** Current “狼影” count of a 狼群 token (0 when not a wolf pack). */
/** Current “狼影” count of a 狼群 token (0 when not a wolf pack, and in its 战术点形态). */
export function wolfShadows(unit) { return unit && unit.defId === TOKEN_IDS.wolfPack ? (unit.mem.shadows ?? 0) : 0; }
// ---- 狼群 战术点形态 — both packs: the board piece (wolfPack below) and the 伺夜 kit's own pack
// (kits/ops/chess_char_3_19-vigil.js). PRTS 伺夜 天赋 狼群领袖 备注 and 狼群 召唤物信息 备注: "受到致命伤时,如果狼影层数>1则
// 消耗一层狼影并重设生命值至上限,为1则消耗一层狼影变为战术点形态;手动撤退、强制撤退时狼影层数归零并变为战术点形态;战术点形态的
// 持续时间等于“狼影”恢复时间,持续时间结束后狼影层数变回1层", "战术点形态期间:不进行普通攻击,持有无敌、强制缴械、不死…",
// "持有者离场后强制撤退场上的狼群(不触发上述效果)". The remake's 战术点形态 is its knocked-out piece: off the fight
// (`alive` false: no block, no attack, not targetable) but kept (`removed` false: its hooks live on and its tile stays
// reserved — Battle.isReservedTile — as the official device holds it), with 0 狼影, for the 狼影 recovery time
// (wolfShadowInterval: the talent's interval, data); then it is redeployed on its tile, free, at full HP with 1 狼影 and a
// fresh growth cycle (the pack's own `deploy` handler reads `mem.wolfReturn`). 伺夜 S1 领袖的呼唤 ① brings it back at
// once (wolfReturnNow); every return timer carries the form's `seq`, so a stale one never revives a pack that came back
// and fell again.
/**
* Removal reasons that put a 狼群 in its 战术点形态: the fatal hit on its last 狼影 (the engine's knock-out) and a 撤退 —
* manual (`battle.retreat`'s default reason) or forced. No sim path retreats the pack that way today; 'expired' (its
* owner leaving, the deploy limit) and 'raid' (an instant redeploy) never do.
*/
const WOLF_TAC_EXITS = new Set(['killed', 'retreat', FORCED_EXIT]);
/** Seconds between two return tries of a 狼群 whose tile is taken when its 战术点形态 ends (Battle.isReservedTile keeps it). */
const WOLF_RETURN_RETRY = 0.25;
/** The 狼群领袖 talent of a 狼群 def (its 狼影 stack talent), else its first talent. */
const wolfLeader = (def) => talentWith(def, 'vigil_wolf_t_1_enhance[trigger].max_stack_cnt') ?? def?.talents?.[0] ?? null;
/**
* “狼影” recovery time of a 狼群 def: its 狼群领袖 talent interval ("每25秒增加一只"; data — 25 s for both 伺夜 chess). The
* growth cycle and the length of the 战术点形态. 0 when the data has none.
*/
export function wolfShadowInterval(def) {
const lb = wolfLeader(def)?.bb ?? {};
return Math.max(0, num(lb['vigil_wolf_t_1_enhance[trigger].interval'] ?? lb.interval, 0));
}
/** The 战术点形态 of a 狼群 (`{ seq, since, until }`) while it is in it, else null. */
export function wolfTacticalPoint(unit) {
const tp = unit?.mem?.wolfTac;
return tp && !unit.alive && !unit.removed ? tp : null;
}
/** The pack's owner stands on the field (no owner unit: a test spawn). */
const wolfOwnerStands = (u) => !u.ownerUnit || (u.ownerUnit.alive && u.ownerUnit.deployed);
/**
* Bring a 狼群 back from its 战术点形态 at once (the end of the form; 伺夜 S1 ①: "立刻切换至拥有1只狼影的召唤物形态(会更新
* 狼群的狼影刷新周期)"): redeployed on its tile, free, at full HP with 1 狼影 and a fresh growth cycle (its `deploy`
* handler reads `mem.wolfReturn` = { src }); the pending return is cancelled. False when it is not in the form, its owner
* is off the field or its tile is taken.
*/
export function wolfReturnNow(battle, unit, src = null) {
const tp = wolfTacticalPoint(unit);
if (!tp || battle.finished || !wolfOwnerStands(unit)) return false;
unit.mem.wolfReturn = { src };
let ok;
try { ok = battle.redeploy(unit, { free: true }); } finally { unit.mem.wolfReturn = null; }
if (!ok) return false;
if (unit.mem.wolfTac === tp) unit.mem.wolfTac = null;
if (tp.timer) tp.timer.cancel();
return true;
}
/** The end of the 战术点形态 `seq` (its timer): the pack comes back — another try shortly when its tile is taken. */
function wolfTimerReturn(battle, unit, seq) {
const tp = wolfTacticalPoint(unit);
if (!tp || tp.seq !== seq || battle.finished) return;
if (wolfReturnNow(battle, unit)) return;
tp.timer = battle.after(WOLF_RETURN_RETRY, () => wolfTimerReturn(battle, unit, seq), { owner: unit });
}
/**
* The 战术点形态 of a 狼群 unit (see above). `onEnter(battle, unit)` sets its 狼影 to 0 — each pack keeps its own count,
* buff and fx.
*/
export function installWolfTacticalPoint(battle, unit, { onEnter = null } = {}) {
battle.on('death', (ctx) => {
if (ctx.unit !== unit || battle.finished || !WOLF_TAC_EXITS.has(ctx.reason) || !wolfOwnerStands(unit)) return;
unit.removed = false;
// the 狼影 recovery time from the data; a record without one falls back to the token's redeploy time (data)
const t = wolfShadowInterval(unit.def) || Math.max(0, num(unit.base.respawnTime, 0));
const seq = (unit.mem.wolfTacSeq ?? 0) + 1;
unit.mem.wolfTacSeq = seq;
const tp = { seq, since: battle.time, until: battle.time + t, timer: null };
unit.mem.wolfTac = tp;
if (onEnter) onEnter(battle, unit);
tp.timer = battle.after(t, () => wolfTimerReturn(battle, unit, seq), { owner: unit });
}, { owner: unit, priority: -10 });
// its owner leaving ends the form without a return ("不触发上述效果"): the piece waits for the owner's redeploy, which
// brings a fresh pack (its initial 狼影) — as for a standing pack, which leaves with its owner
battle.on('death', (ctx) => {
const tp = wolfTacticalPoint(unit);
if (!tp || !unit.ownerUnit || ctx.unit !== unit.ownerUnit) return;
unit.mem.wolfTac = null;
if (tp.timer) tp.timer.cancel();
}, { owner: unit });
}
/** 狼群 (伺夜 talent 狼群领袖/狼群天性; S3 bb on the token). */
function wolfPack(bb, raw, def) {
const leader = talentWith(def, 'vigil_wolf_t_1_enhance[trigger].max_stack_cnt') ?? def?.talents?.[0] ?? null;
const leader = wolfLeader(def);
const lb = leader?.bb ?? {};
const interval = num(lb['vigil_wolf_t_1_enhance[trigger].interval'] ?? lb.interval, 0);
const interval = wolfShadowInterval(def);
const perBlock = num(lb['vigil_wolf_t_1_enhance[trigger].block_cnt'] ?? lb.block_cnt, 1);
const mMax = String(leader?.description ?? '').match(/至多(\d+)只/);
const maxShadows = mMax ? +mMax[1] : 1 + num(lb['vigil_wolf_t_1_enhance[trigger].max_stack_cnt'], 0);
@@ -635,8 +733,13 @@ function wolfPack(bb, raw, def) {
const mi = ot.match(/初始(两|二|\d+)只/);
const initShadows = Math.max(1, Math.min(maxShadows, mi ? (/\d/.test(mi[1]) ? +mi[1] : 2) : maxShadows - 1));
onDeploy(battle, unit, () => {
setShadows(battle, unit, initShadows);
// back from its 战术点形态 (wolfReturnNow): 1 狼影 (PRTS 狼群 "从战术点形态转变为召唤物形态后拥有1只“狼影”") and a
// fresh growth cycle; any other deployment: the initial count
const back = unit.mem.wolfReturn;
setShadows(battle, unit, back ? 1 : initShadows);
unit.hp = unit.s.maxHp;
// (伺夜 S1 ① shows its own summon fx)
if (back && back.src == null) battle.fx('wolfShadow', { x: unit.x, y: unit.y, id: unit.id, n: 1 });
const seq = unit.deploySeq;
if (interval > 0) {
battle.every(interval, (b, sched) => {
@@ -650,6 +753,7 @@ function wolfPack(bb, raw, def) {
const o = ownerOf(unit);
if (o && o.profile?.sub === 'tactician') o.trait.reinforcement = unit;
}, 20);
// fatal with more than one 狼影: one is lost, full HP; on the last one the knock-out goes through — 战术点形态
battle.on('fatal', (ctx) => {
if (ctx.unit !== unit || ctx.prevented || !((unit.mem.shadows ?? 0) > 1)) return;
ctx.prevented = true;
@@ -657,6 +761,12 @@ function wolfPack(bb, raw, def) {
unit.hp = unit.s.maxHp;
battle.fx('wolfShadowLost', { x: unit.x, y: unit.y, id: unit.id, n: unit.mem.shadows });
}, { owner: unit, priority: -50 });
installWolfTacticalPoint(battle, unit, {
onEnter: (b, u) => {
setShadows(b, u, 0);
b.fx('wolfShadowLost', { x: u.x, y: u.y, id: u.id, n: 0 });
},
});
// 狼群天性 ("伺夜和狼群对其的攻击无视其175防御力") and the owner's S3 bonus ("狼群与伺夜攻击被狼群阻挡的单位造成伤害
// 时,额外造成相当于伺夜攻击力N%的法术伤害", one per damage instance = per bite) cover the pack's and 伺夜's own
// attacks; with a hand-authored 伺夜 kit (managed) that kit applies both. The module guard is intrinsic.
@@ -676,14 +786,14 @@ function wolfPack(bb, raw, def) {
battle.dealDamage(ctx.source, e, { amount: o.s.atk * s3Scale, type: 'arts', isSkill: true, canDodge: false, tags: ['vigil'] });
}, { owner: unit });
}
// the 援军 leaves with its tactician (a hand-authored 伺夜 kit does the same for its pack) and a killed pack
// does not come back while its tactician is down (the tactician's redeploy brings it back)
// the 援军 leaves with its tactician (a hand-authored 伺夜 kit does the same for its pack); a pack in its 战术点形态
// ends it then (installWolfTacticalPoint) and the tactician's redeploy brings it back fresh. No respawn timer of
// its own: the token's redeploy time (data 10 s) is not the 战术点形态's length (the 狼影 interval)
const tacticianOwner = () => ownerOf(unit)?.profile?.sub === 'tactician';
battle.on('death', (ctx) => {
if (ctx.unit !== unit.ownerUnit || !unit.alive || managed(unit) || !tacticianOwner()) return;
battle.retreat(unit, { reason: 'expired', permanent: true });
}, { owner: unit });
enableRespawn(battle, unit, { delay: (u) => u.base.respawnTime, requireOwner: tacticianOwner });
},
};
}
@@ -1464,6 +1574,10 @@ function ensureReinforcement(battle, owner, tokenId) {
const mine = battle.allyUnits.filter((t) => t.kind === 'token' && t.defId === tokenId && t.ownerUnit === owner && !t.mem.isClone && !t.mem.mlyssClone);
const live = mine.find((t) => t.alive);
if (live) { owner.trait.reinforcement = live; return live; }
// a 狼群 in its 战术点形态 is still the 援军 on the field: it comes back by its own timer (a 【移动】 of the owner fires
// `deploy` without ending the form)
const tac = mine.find((t) => wolfTacticalPoint(t));
if (tac) { owner.trait.reinforcement = tac; return tac; }
const waiting = mine.find((t) => !t.alive && !t.removed);
if (waiting && battle.redeploy(waiting, { free: true })) { owner.trait.reinforcement = waiting; return waiting; }
// the tactical point the player chose (the board piece's tile, else the last one's) when still usable, else the