feedback3 WA: 联防 carries the HP ratio and the SP only — a skill running at the end of the own combat enters switched off (no free restart), the SP counts stored charges, board summons keep their SP
This commit is contained in:
1 parent
bdb0765c55
commit
8252be248f
16 files changed
+220
-48
No files matched your search
@@ -1544,7 +1544,9 @@ export class PlayerState {
|
||||
if (carry && carry.has(piece.uid)) u.carryState = carry.get(piece.uid);
|
||||
units.push(u);
|
||||
} else if (piece.kind === 'token') {
|
||||
units.push({ uid: piece.uid, kind: 'token', tokenId: piece.id, row: r, col: c, dir: pieceDir(piece), ownerUid: piece.ownerUid });
|
||||
const u = { uid: piece.uid, kind: 'token', tokenId: piece.id, row: r, col: c, dir: pieceDir(piece), ownerUid: piece.ownerUid };
|
||||
if (carry && carry.has(piece.uid)) u.carryState = carry.get(piece.uid); // 联防: { sp } (unite.js)
|
||||
units.push(u);
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -829,10 +829,11 @@ export function validateClientResult(spec, raw, { gd = null } = {}) {
|
||||
const seen = new Set();
|
||||
for (const u of Array.isArray(p.unitsEnd) ? p.unitsEnd : []) {
|
||||
// units the sim created during the battle (no board uid) or not on this player's board carry nothing the match uses
|
||||
if (!u || !Number.isInteger(u.uid) || !own.chess.has(u.uid) || seen.has(u.uid)) continue;
|
||||
// (the board's operators and summon pieces: 联防 carries an operator's HP ratio and SP, a summon's SP — unite.js)
|
||||
if (!u || !Number.isInteger(u.uid) || !own.all.has(u.uid) || seen.has(u.uid)) continue;
|
||||
seen.add(u.uid);
|
||||
if (!finiteIn(u.hpPct, 0, 1) || !finiteIn(u.sp, 0, 1e5)) return bad('unit state');
|
||||
unitsEnd.push({ uid: u.uid, defId: own.chess.get(u.uid), hpPct: u.hpPct, sp: u.sp, skillActive: !!u.skillActive, alive: !!u.alive && u.hpPct > 0 });
|
||||
unitsEnd.push({ uid: u.uid, defId: own.all.get(u.uid), hpPct: u.hpPct, sp: u.sp, skillActive: !!u.skillActive, alive: !!u.alive && u.hpPct > 0 });
|
||||
}
|
||||
const unitStats = [];
|
||||
for (const u of Array.isArray(p.unitStats) ? p.unitStats : []) {
|
||||
|
||||
+15
-6
@@ -5,15 +5,17 @@
|
||||
// players chosen by most units on the field (downed included) > has an active bond (存疑) > most undowned units, then
|
||||
// seat; with 2 helpers the one ranked first by most units > active bond > Σ active bond layers (存疑) > most undowned
|
||||
// units "率先迎敌" on the RIGHT-hand field (colOffset +8, where the escaped_multi routes enter), the other keeps the
|
||||
// left half (colOffset 0); a lone helper plays escaped_single on its own field. Their operators keep HP%, SP and a
|
||||
// running timed skill from the end of their own combat (BattleResult.unitsEnd → PlayerBattleInput.units[].carryState,
|
||||
// "阵地以其当前状态"). An operator knocked out at the end of its own combat (alive false) is fielded with
|
||||
// left half (colOffset 0); a lone helper plays escaped_single on its own field. Their operators keep the HP ratio and
|
||||
// the SP (技力, stored charges included) from the end of their own combat, nothing else — a skill still running then
|
||||
// enters switched off (BattleResult.unitsEnd → PlayerBattleInput.units[].carryState `{ hpPct, sp }`, "阵地以其当前状态";
|
||||
// community report #34 / GitHub #82: it used to restart for free). An operator knocked out at the end of its own combat (alive false) is fielded with
|
||||
// `carryState: { down: true }`: PRTS "部署完成后,将对应单位的生命比例、技力修改至与上一阶段结束时相同(召唤物仅修改技力,
|
||||
// 上一阶段为退场状态的干员强制退场)" — the sim deploys it with everyone and forces it out at once (constants.js
|
||||
// FORCED_EXIT), so it lies on its own tile with the redeploy ring and comes back like after any knock-out (user
|
||||
// playtest #5 item 2: it used to be left out and vanished). Its timer is its full redeploy time (the official 联防
|
||||
// setup carries only hp / tech per operator, research 09 §3 HelpBattleInfo; the user confirmed it restarts). Summons are fielded as the board has
|
||||
// them (a summon's own end state is not carried: unitsEnd lists operators only). Enemies = the union of every leaker's
|
||||
// setup carries only hp / tech per operator, research 09 §3 HelpBattleInfo; the user confirmed it restarts). The board's summon
|
||||
// pieces are fielded as the board has them and keep only their SP ("召唤物仅修改技力": carryState `{ sp }`, unitsEnd lists
|
||||
// them beside the operators; one off the field at the end enters fresh [ASSUMED]). Enemies = the union of every leaker's
|
||||
// counted leaks (same stats: the SpawnSpec mods travel with the leak), routed on the escaped template (`escaped_single`
|
||||
// for 1 helper, `escaped_multi` for 2): walkers on its `lrsldr` action, flyers on `yokai`, tokens on `gopro_2` /
|
||||
// `lazerd` (waves.js buildUniteWave); kill bounties keep paying the killer (a helper). No IN_BATTLE layer gains. Time
|
||||
@@ -105,11 +107,18 @@ export function uniteBattleOpts(m, plan, timeLimit) {
|
||||
const players = plan.helpers.map((ps, i) => {
|
||||
const carry = new Map();
|
||||
const r = m.lastResults.get(ps.playerId);
|
||||
const summonUids = new Set();
|
||||
for (const p of ps.board.values()) if (p && p.kind === 'token') summonUids.add(p.uid);
|
||||
for (const u of (r && r.unitsEnd) || []) {
|
||||
if (!u || u.uid == null) continue;
|
||||
const sp = Number.isFinite(u.sp) ? Math.max(0, u.sp) : 0;
|
||||
// a summon: its SP only ("召唤物仅修改技力"); one off the field at the end enters fresh [ASSUMED]
|
||||
if (summonUids.has(u.uid)) { if (u.alive) carry.set(u.uid, { sp }); continue; }
|
||||
// knocked out at the end of its own combat: 强制退场 right after the deployment (see header)
|
||||
if (!u.alive) { carry.set(u.uid, { down: true }); continue; }
|
||||
carry.set(u.uid, { hpPct: Number.isFinite(u.hpPct) ? Math.max(0.01, Math.min(1, u.hpPct)) : 1, sp: Number.isFinite(u.sp) ? u.sp : 0, skillActive: !!u.skillActive });
|
||||
// HP ratio and SP only: a skill still running at the end is not carried (unitsEnd `skillActive` stays unused —
|
||||
// community report #34, GitHub #82)
|
||||
carry.set(u.uid, { hpPct: Number.isFinite(u.hpPct) ? Math.max(0.01, Math.min(1, u.hpPct)) : 1, sp });
|
||||
}
|
||||
// 2 helpers: the first one meets the enemies first on the right-hand field (escaped_multi enters at col 18)
|
||||
const colOffset = plan.helpers.length > 1 && i === 0 ? 8 : 0;
|
||||
|
||||
+15
-4
@@ -124,6 +124,7 @@ export class Battle {
|
||||
this.finished = false;
|
||||
this.reason = null;
|
||||
this.started = false;
|
||||
this._startDeploying = false;
|
||||
/** @type {Unit[]} every unit ever created */
|
||||
this.units = [];
|
||||
/** @type {Unit[]} alive enemies (compacted each tick) */
|
||||
@@ -360,11 +361,15 @@ export class Battle {
|
||||
// a summon piece flagged `deferDeploy` by content (one its owner's loadout does not make — 赫默 on S1 with a drone
|
||||
// piece —, or a skill's summon when shared/constants.js SKILL_SUMMON_START_DEPLOY is off: content/tokens.js
|
||||
// dockSkillSummons) stays off the field, its tile reserved (isReservedTile), until content deploys it
|
||||
// (_startDeploying: a board piece content brings in during this deployment — a tactician's 流形 / 狼群 comes with
|
||||
// its owner's deploy — takes its 联防 carry on that first deployment too)
|
||||
this._startDeploying = true;
|
||||
for (const kind of ['op', 'token']) {
|
||||
const per = lists.map((l) => l.filter((u) => u.kind === kind && !u.deferDeploy));
|
||||
const n = Math.max(0, ...per.map((l) => l.length));
|
||||
for (let i = 0; i < n; i++) for (const l of per) if (l[i]) this._safe(() => this._deploy(l[i], { initial: true }), 'initialDeploy', l[i]);
|
||||
}
|
||||
this._startDeploying = false;
|
||||
// 仇恨 (targeting.js sortAllyTargets: the later deployed is attacked first): every summon that came in during the
|
||||
// initial deployment — also one an operator's deploy brought along (a tactician's 援军, a start-of-battle summon)
|
||||
// — ranks after all the operators (of every player on the field [ASSUMED]), in the order it came
|
||||
@@ -539,10 +544,14 @@ export class Battle {
|
||||
for (const ps of this.players) {
|
||||
const pp = this._perPlayer[ps.playerId];
|
||||
pp.perfect = !pp.leaked.some((l) => l.counted !== false);
|
||||
pp.unitsEnd = ps.units.filter((u) => u.kind === 'op').map((u) => ({
|
||||
// the operators and the board's summon pieces (a board uid): 联防 carries an operator's HP ratio and SP, a summon's
|
||||
// SP only (match/unite.js). `sp` is the official 技力 — stored charges included (PRTS 技能 "可充能X次…当前技力上限等于该
|
||||
// 技能技力需求的X倍"); a running skill spent its SP at activation, so it reports what was left (0 for one charge).
|
||||
// `skillActive` is reported, never carried.
|
||||
pp.unitsEnd = ps.units.filter((u) => u.kind === 'op' || (u.kind === 'token' && u.uid != null)).map((u) => ({
|
||||
uid: u.uid, id: u.id, defId: u.defId,
|
||||
hpPct: u.alive ? Math.max(0, Math.min(1, u.hp / u.s.maxHp)) : 0,
|
||||
sp: u.skill && !u.skill.noSkill ? Math.round(u.skill.sp * 100) / 100 : 0,
|
||||
sp: u.skill && !u.skill.noSkill ? Math.round(u.skill.spTotal * 100) / 100 : 0,
|
||||
skillActive: !!(u.skill && u.skill.active && u.skill.kind !== 'passive'),
|
||||
alive: !!u.alive,
|
||||
}));
|
||||
@@ -883,8 +892,10 @@ export class Battle {
|
||||
u.ground = this.grid.isLow(R0, C0) && !this._elevated?.has(k);
|
||||
u.markDirty();
|
||||
u.hp = u.s.maxHp;
|
||||
const cs = initial ? (carry ?? u.carry) : null;
|
||||
if (cs && Number.isFinite(cs.hpPct)) u.hp = Math.max(1, u.s.maxHp * Math.max(0.01, Math.min(1, cs.hpPct)));
|
||||
// 联防 carry (PRTS 卫戍协议/帮助 §联防阶段 "将对应单位的生命比例、技力修改至与上一阶段结束时相同(召唤物仅修改技力…)"):
|
||||
// an operator's HP ratio here, the SP in skill.reset — a summon's SP only
|
||||
const cs = initial || (first && this._startDeploying) ? (carry ?? u.carry) : null;
|
||||
if (cs && u.kind === 'op' && Number.isFinite(cs.hpPct)) u.hp = Math.max(1, u.s.maxHp * Math.max(0.01, Math.min(1, cs.hpPct)));
|
||||
this._occ[k] = u;
|
||||
this._refreshRange(u); // also rebuilds baseRangeKeys (initial range incl. permanent rangeExtend)
|
||||
if (!u.skill) this._setupUnit(u);
|
||||
|
||||
+17
-5
@@ -161,7 +161,22 @@ export class SkillRuntime {
|
||||
return this.battle._safe(() => fn(this._ctx(extra)), `skill.${fnName}`, this.unit);
|
||||
}
|
||||
|
||||
/** Called on every (re)deployment. `carry` = { sp, skillActive } for unite helpers. */
|
||||
/**
|
||||
* The official 技力 (PRTS 技能: "可充能X次…当前技力上限等于该技能技力需求的X倍"): the stored charges × cost plus the SP
|
||||
* towards the next one — what 联防 carries (BattleResult unitsEnd `sp`; reset rebuilds the charges from it).
|
||||
*/
|
||||
get spTotal() {
|
||||
if (this.noSkill || this.kind === 'passive') return 0;
|
||||
const cost = this.spCost;
|
||||
return this.charges >= this.maxCharges ? this.maxCharges * cost : this.charges * cost + this.sp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on every (re)deployment. `carry` = { sp } for unite (联防) helpers: their 技力 at the end of their own combat
|
||||
* (unitsEnd `sp` = spTotal, rebuilt into charges here). Nothing else of the skill is carried — PRTS 卫戍协议/帮助 §联防阶段
|
||||
* "将对应单位的生命比例、技力修改至与上一阶段结束时相同": a skill that was running enters 联防 switched off, with the SP it
|
||||
* had left (spent at its activation: 0 for a one-charge skill — PRTS 技能 "触发技能后…消耗相应的技力").
|
||||
*/
|
||||
reset(carry = null) {
|
||||
this.active = false;
|
||||
this.pending = false;
|
||||
@@ -181,9 +196,6 @@ export class SkillRuntime {
|
||||
this.gainSp(carry && Number.isFinite(carry.sp) ? carry.sp : this.initSp, 'init', true);
|
||||
// a free (spCost 0) non-passive skill is available once per deployment
|
||||
if (this.spCost <= 0) this.charges = this.maxCharges;
|
||||
// unite helpers whose timed skill was running when their combat ended: it keeps running (a fresh duration/ammo),
|
||||
// without spending a charge — the carried SP is what they had accumulated (0 while a skill runs).
|
||||
if (carry && carry.skillActive && this.isTimed) this.activate('carry', { free: true });
|
||||
}
|
||||
|
||||
_startPassive() {
|
||||
@@ -396,7 +408,7 @@ export class SkillRuntime {
|
||||
this.activations++;
|
||||
this.lastStart = this.battle.time;
|
||||
const b = this.battle;
|
||||
if (this.manual && reason !== 'carry') this.opReadyAt = b.time + AUTO_OP_COOLDOWN;
|
||||
if (this.manual) this.opReadyAt = b.time + AUTO_OP_COOLDOWN;
|
||||
if (this.isTimed) {
|
||||
this.active = true;
|
||||
this.timeLeft = this.kind === 'duration' ? Math.max(0.01, this.duration) : (this.kind === 'ammo' && this.duration > 0 ? this.duration : Infinity);
|
||||
|
||||
Reference in new issue
Block a user