feedback3: merge W3A — 联防 carries HP ratio and SP only (GitHub #82); down operators take part in the 联防 devour (players' reports, owner's decision); 不屈 on 撤退 / 替身 switches; SP recovers during stun (洛洛 S2)

This commit is contained in:
sganggs committed 2026-10-04 17:57:44 +08:00
commit dcc44712ad
23 files changed
+629 -67

No files matched your search

+25 -7
View File
@@ -431,24 +431,42 @@ export function install(battle) {
});
}
// 不屈
// 不屈 (PRTS 卫戍协议:盟约 下半/PRTS盟约记录, its 修正: "地面干员被击倒、撤退、切换<替身>与<本体>时,有(18+0.4×层数)%概率立刻
// 重新部署"; "※“立刻重新部署”的实现方式为:令受益者下次部署的再部署时间和费用归零"; owner's decision 2026-10-04 to follow it).
// Rolls on a knock-out ('killed') and on a 撤退 ('retreat' — 史尔特尔's 余烬, 耀骑士临光 S2, 伊内丝 S3 … — and 行商's
// 'merchant' withdrawal): a hit redeploys it at once, free, where it lies (engine rest tile, §22.15). Not on the 突袭
// retreat ('raid': it redeploys at once and free anyway — the zeroed next deployment would change nothing [ASSUMED: no
// roll]) nor on the 联防 forced exit (FORCED_EXIT: the battle's setup, not a 撤退 — §19.3). A 傀儡师 switch (hook
// `dollSwap`, to the 替身 and back) rolls too; she stays on the field, so a hit only zeroes her NEXT deployment: the
// next time she leaves the field (knocked out, withdrawn) she is back at once and free (`u.mem.indomFreeDeploy`)
// [ASSUMED: one such deployment at a time, spent by her next deployment whatever brings it]. Tier 2 (+sp SP to every
// operator on the field) stays a knock-out effect: its own line ("地面干员被击倒时使场上所有干员技力+5") was not corrected.
if (has(ID.indom)) {
const INDOM_EXITS = new Set(['killed', 'retreat', 'merchant']);
const stOf = (u) => { const st = isGroundOp(u) ? byPid[u.ownerId] : null; return st && st.tiers[ID.indom] ? st : null; };
battle.on('death', (c) => {
const u = c.unit;
if (c.reason !== 'killed' || !isGroundOp(u)) return;
const st = byPid[u.ownerId];
if (!st || !st.tiers[ID.indom]) return;
const bb = st.bb[ID.indom];
if (st.tiers[ID.indom] >= 2) {
const banked = !!(u && u.mem && u.mem.indomFreeDeploy);
if (!INDOM_EXITS.has(c.reason) || !(stOf(u) || (banked && u.kind === 'op'))) return;
const st = stOf(u);
const bb = st ? st.bb[ID.indom] : null;
if (st && c.reason === 'killed' && st.tiers[ID.indom] >= 2) {
const sp = num(bb.sp);
if (sp > 0) for (const o of st.ops) if (onField(o) && o.skill) o.skill.gainSp(sp, 'bond');
}
if (u.alive || u.removed || u.mem[ID.indom] === battle.time) return;
if (battle.rng() < prob(bb, L(battle, st, ID.indom))) {
if (banked || (st && battle.rng() < prob(bb, L(battle, st, ID.indom)))) {
u.mem[ID.indom] = battle.time;
if (battle.redeploy(u, { free: true })) fxOn(battle, 'revive', u, 'bond:indomShip', 'redeploy');
}
}, { priority: 10 });
battle.on('dollSwap', ({ unit: u }) => {
const st = stOf(u);
if (!st || !u.alive || u.mem.indomFreeDeploy) return;
if (battle.rng() < prob(st.bb[ID.indom], L(battle, st, ID.indom))) u.mem.indomFreeDeploy = true;
});
// the zeroed deployment is the next one, whatever brings it (this redeploy, the 阿戈尔 revive, the 突袭 jump …)
battle.on('deploy', ({ unit: u }) => { if (u && u.mem && u.mem.indomFreeDeploy) u.mem.indomFreeDeploy = false; });
}
// 坚守 (tier 2) redirect + thorns, 奥术 vulnerability
+18 -3
View File
@@ -40,6 +40,7 @@ import { mitigate } from '../../damage.js';
import { spawnYanyou } from '../tokens.js';
import { kjeragColdWind } from '../devices.js';
import * as items from '../items.js';
import { FORCED_EXIT } from '../../constants.js';
const num = S.num;
@@ -343,6 +344,8 @@ function installLaterano(battle, pid, bb, members) {
* cancelled, also when it is back at once (the 5-tier 立刻复活, 不屈's 立刻重新部署, 埃芒加德 / M3茧甲): PRTS 盟约记录 "目标首次被
* 击倒后解除自身被付与但还未触发的【吞噬】效果". A marker off the field gives no further mark (the rule since 0.1.0; one knocked
* out and back in the same pass still gives its marks).
* 联防: the operators down since the end of their own combat (forced out by Battle.start) mark, are marked and resolve
* their marks like standing ones, but nothing resolves on them (below; per players' reports, owner's decision 2026-10-04).
* Each devoured operator adds its tier to 阿戈尔 once (IN_BATTLE gain, disabled in 联防 / boss fields).
* Tokens / devices / empty tiles are never devoured.
*/
@@ -352,12 +355,22 @@ function devour(battle, pid, bb, members) {
// first — row 0 is the BOTTOM row (DESIGN §3), so the top of the board is the highest row index. The order is a
// board position, independent of the members' directions (only "身前" follows each member's `dir`).
const boardCol = (u) => (u.player && u.player.mirror ? -u.tileC : u.tileC);
const order = members.filter(S.onField).sort((a, b) =>
// 联防: an operator down at the end of its own combat (carryState.down — Battle.start forced it out right before
// battleStart, FORCED_EXIT) takes part in the devour as if it stood on its tile, then stays out: it marks in its turn,
// it is "the unit in front" of another (the chain goes on through it when it is a member), its base ATK / block count
// for its marker and its own marks resolve (their 物理流失 lands, credited to it as usual); only the marks ON it resolve
// nothing — it is forced out, so it is never knocked out again, revived or devoured. Per players' reports (community
// report #3, GitHub #33 item 3), owner's decision 2026-10-04; until 0.1.2 the forced exit came first and the chain broke.
const downAtStart = (u) => S.isOp(u) && !u.alive && u.removeReason === FORCED_EXIT && !!u.carry && u.carry.down === true
&& battle.isDown(u);
const order = members.filter((u) => S.onField(u) || downAtStart(u)).sort((a, b) =>
boardCol(a) - boardCol(b) || b.tileR - a.tileR || a.id - b.id);
const opAt = (u) => {
const [r, c] = S.frontTile(u);
const a = S.allyAt(battle, r, c, pid);
return a && S.isOp(a) && a.alive ? a : null;
if (a) return S.isOp(a) && a.alive ? a : null;
const d = battle.downOn(r, c);
return d && d.ownerId === pid && downAtStart(d) ? d : null;
};
const markedBy = new Map(); // marker → [targets]
const marks = [];
@@ -392,7 +405,9 @@ function devour(battle, pid, bb, members) {
for (const [, t] of marks) if (!dep.has(t)) dep.set(t, items.deploymentOf(t));
const knocked = (t) => !t.alive || items.deploymentOf(t) !== dep.get(t);
for (const [m, t] of marks) {
if (knocked(t) || !m.alive) continue;
// (a member down since its own combat — downAtStart — resolves its marks as if it stood; a mark on it resolves nothing:
// `knocked` — it is off the field)
if (knocked(t) || !(m.alive || downAtStart(m))) continue;
S.fxOn(battle, 'devour', t, 'bond:egirShip', 'devour', { from: m.id });
if (amount > 0) battle.loseHp(t, mitigate(amount, 'phys', t.s), { source: m, tags: ['bond:egir:devour'] });
if (!layered.has(t)) {