0.2.5-dev: merge claude/036-touch

# Conflicts:
#	CHANGELOG.md
#	docs/history/0.2.5.md
This commit is contained in:
sganggs committed 2026-10-12 15:00:10 +08:00
commit a8ff5e7c1d
22 files changed
+294 -59

No files matched your search

+4 -2
View File
@@ -13,7 +13,7 @@ export class BattleDeploy {
* operator that left the field comes back on its body tile, restTile). `keepSp` = { sp, charges }
* restored right after the skill reset, before the `deploy` hook fires.
*/
_deploy(u, { initial = false, carry = null, tile = null, keepSp = null } = {}) {
_deploy(u, { initial = false, carry = null, tile = null, keepSp = null, preBattleAggroSeq = null } = {}) {
if ((u.alive && u.deployed) || u._removing) return false;
const R0 = tile ? tile[0] : u.homeR, C0 = tile ? tile[1] : u.homeC;
if (!this.grid.inRect(R0, C0)) {
@@ -37,7 +37,9 @@ export class BattleDeploy {
u.x = C0; u.y = R0; u.tileR = R0; u.tileC = C0;
u.blocking = [];
u.deploySeq = ++this._deploySeq;
u.aggroSeq = u.deploySeq;
// A prepared map character is instantiated after the board deployment but keeps an earlier target order.
// Its deploySeq remains the identity of this actual deployment for scheduled effects and redeploy checks.
u.aggroSeq = preBattleAggroSeq ?? u.deploySeq;
u.deployedAt = this.time;
u.atkCd = 0;
u.lastAttackAt = -Infinity;
+1 -1
View File
@@ -60,7 +60,7 @@ export class BattleSummons {
if (opts.stats) for (const [k, v] of Object.entries(opts.stats)) if (Number.isFinite(v)) u.base[k] = v;
this._setupUnit(u, opts.kit ?? null);
if (opts.untargetable) this.addBuff(u, { key: 'trait:untargetable', flags: { untargetable: true }, persist: true, allowDead: true });
if (!this._deploy(u, { initial: false })) {
if (!this._deploy(u, { initial: false, preBattleAggroSeq: opts.preBattleAggroSeq })) {
// tile still busy (a unit stands there even with `force`): drop the half-built token and its hooks
u.removed = true;
this.offOwner(u);
+11 -5
View File
@@ -35,7 +35,8 @@
// retained team grant gives every player's field the map character of the
// variant whose common_condition holds for that player's elites on the field
// (condition_golden_chess_le cnt → 预备干员-医疗, _ge cnt → Touch), spawned at
// combat start on its stage slot (tokens.spawnMapChar)
// combat start on its stage slot (tokens.spawnMapChar). [ASSUMED] that slot's
// aggroSeq precedes ordinary initial operators; deploySeq stays the runtime spawn
// Prep-only bands (economy / shop / grants / layers at prep end or round start) live in bands/meta.js.
import {
@@ -238,12 +239,17 @@ function installMapChars(battle) {
const holders = battle.players.filter((ps) => matchBands(battle, ps.playerId).includes(AMEDIC_BAND));
if (!holders.length) return;
battle.on('battleStart', () => {
const chars = [];
for (const ps of holders) {
const elites = deployedOps(battle, ps.playerId).filter(isElite).length;
for (const id of amedicCharsFor(elites, variants)) {
const u = spawnMapChar(battle, ps.playerId, id);
if (u) fxOn(battle, 'summon', u, keyOf(AMEDIC_BAND), AMEDIC_BAND);
}
for (const id of amedicCharsFor(elites, variants)) chars.push({ playerId: ps.playerId, id });
}
for (const [i, { playerId, id }] of chars.entries()) {
// The reserve medic occupies this stage slot in preparation; Touch replaces it at battle start. [ASSUMED]
// Replacement preserves the slot's priority before ordinary deployments. Shared-field ordering between
// prepared characters follows player order, as for other initial placements (lifecycle.js).
const u = spawnMapChar(battle, playerId, id, { preBattleAggroSeq: i - chars.length });
if (u) fxOn(battle, 'summon', u, keyOf(AMEDIC_BAND), AMEDIC_BAND);
}
});
}
+5 -3
View File
@@ -1566,9 +1566,11 @@ function touchKit(bb, raw, def) {
/**
* Spawn a band map character (char_605_cmedic 预备干员-医疗 / char_613_acmedc Touch) at its stage position on the
* player's half (data/stages.json `mapChars`; `_multi_only` positions only with a partner). Returns the unit or null.
* player's half (data/stages.json `mapChars`; `_multi_only` positions only with a partner). A prepared character may
* supply `preBattleAggroSeq` so its targeting order precedes the ordinary battle-start deployment. Returns the unit
* or null.
*/
export function spawnMapChar(battle, playerId, tokenId, { alias = null } = {}) {
export function spawnMapChar(battle, playerId, tokenId, { alias = null, preBattleAggroSeq = null } = {}) {
const list = battle.stage?.raw?.mapChars ?? battle.stage?.mapChars ?? [];
const ps = battle.getPlayer(playerId);
const multi = battle.players.length > 1;
@@ -1585,7 +1587,7 @@ export function spawnMapChar(battle, playerId, tokenId, { alias = null } = {}) {
for (const m of cands) {
if (!tileFree(battle, m.pos[0], m.pos[1])) continue;
// stage positions are field tiles: their direction is taken as it is (default RIGHT)
const u = battle.spawnToken(playerId, tokenId, m.pos[0], m.pos[1], { dir: normDir(m.dir) });
const u = battle.spawnToken(playerId, tokenId, m.pos[0], m.pos[1], { dir: normDir(m.dir), preBattleAggroSeq });
if (u) return u;
}
return null;
+2 -1
View File
@@ -224,7 +224,8 @@ export function sortEnemyTargets(battle, attacker, cands, priority) {
/**
* Enemy target selection among allies (PRTS 作战机制 索敌, 敌方: "阻挡→特殊优先级→仇恨值(更容易被攻击→…→最后部署的目标→
* 不容易被攻击)→最早出现"): its blocker first → highest taunt level → latest deployed (`aggroSeq`: the deploy order —
* Battle.start deploys the operators first and ranks the start-of-battle summons after them). Special priorities
* Battle.start deploys the operators first and ranks the start-of-battle summons after them; a prepared 外勤医疗 map
* character keeps an earlier aggro order). Special priorities
* (优先攻击…) are the enemies' own content: `e.profile.canTarget` filters, key sorts broken by `aggroCmp`.
*/
export function sortAllyTargets(enemy, cands) {
+1 -1
View File
@@ -77,7 +77,7 @@ export class Unit {
this.deathAt = -Infinity;
this.respawnAt = Infinity;
this.deploySeq = 0; // deployment counter; also the identity of one deployment (content: `seq === u.deploySeq`)
this.aggroSeq = 0; // 仇恨 order (the later the more attacked): = deploySeq, except summons of the initial deployment
this.aggroSeq = 0; // 仇恨 order: usually deploySeq; initial summons and prepared map characters are exceptions
this.spawnSeq = 0;
this.elem = { burn: 0, neural: 0, necrosis: 0, apoptosis: 0, erosion: 0 };
this.burstPending = null; // { [element]: true } while that element's burst resolves (damage.js burstLocked)