feedback7: merge fb7-hud — HP-bar readouts (#303 / #378 / #383: ammo cells, 伺夜's wolf-shadow pips, 斩业星熊's 我执 bar), #302 renderer abort, #292 held skill cue, #334 atlas slots, #354 梓兰's per-card frost

- Grok review: MERGE (no defect). Golden unchanged (union OK); broad node --test 5327 (5326 pass, 0 fail, 1 skipped); lint 0 errors.
This commit is contained in:
sganggs committed 2026-10-08 20:29:27 +08:00
commit 7d6fefe8f5
33 files changed
+1751 -77

No files matched your search

+16 -3
View File
@@ -4,7 +4,7 @@
import { EVENT_BUFFER_CAP } from '../constants.js';
import { elementView } from '../damage.js';
import { unitInfo, snapshotUnits } from '../snapshot.js';
import { unitInfo, snapshotUnits, ammoView, wolfView, negView } from '../snapshot.js';
export class BattleEvents {
fx(kind, params = {}) {
@@ -37,7 +37,11 @@ export class BattleEvents {
* down: [[id, respawnAt, respawnTime, state, row, col]] — operators that left the field waiting to redeploy (isDown): the
* game time their respawn timer ends, its length (s), constants.js DOWN_STATE and the tile they lie on (and
* come back on: _layBody — where they fell, or their home);
* elem: [[id, element, fill, cooldownEnd, cooldown]] — the element gauge each unit shows (damage.js elementView).
* elem: [[id, element, fill, cooldownEnd, cooldown]] — the element gauge each unit shows (damage.js elementView);
* ammo: [[id, rounds left, rounds in the magazine]] — a running ammo skill, whole rounds (snapshot.js ammoView): the segmented bar;
* wolves: [[id, 狼影 left, the talent's maximum]] — 伺夜's 狼群 (snapshot.js wolfView): the pips under the HP bar;
* neg: [[id, fill]] — the share of its cap a negative-HP pool holds (snapshot.js negView; 斩业星熊's 我执): the red bar.
* The last three are display only: no sim state reads them, and the nine-field unit tuples are unchanged.
*/
snapshot() {
const snap = {
@@ -60,13 +64,22 @@ export class BattleEvents {
(down || (down = [])).push([u.id, r2(u.respawnAt), r2(Math.max(0, u.respawnAt - u.deathAt)), this._downState(u), ...this.restTile(u)]);
}
if (down) snap.down = down;
let elem = null;
let elem = null, ammo = null, wolves = null, neg = null;
for (const u of this.units) {
if (!u.alive || !u.deployed || u.hidden) continue;
const v = elementView(u, this.time);
if (v) (elem || (elem = [])).push([u.id, v[0], v[1], v[2], v[3]]);
const am = ammoView(u);
if (am) (ammo || (ammo = [])).push([u.id, am[0], am[1]]);
const wv = wolfView(u);
if (wv) (wolves || (wolves = [])).push([u.id, wv[0], wv[1]]);
const ng = negView(u);
if (ng) (neg || (neg = [])).push([u.id, ng]);
}
if (elem) snap.elem = elem;
if (ammo) snap.ammo = ammo;
if (wolves) snap.wolves = wolves;
if (neg) snap.neg = neg;
return snap;
}
@@ -43,6 +43,7 @@ export default {
skill: null,
trait: { hitsFn: (b, w) => Math.max(1, w.mem.wolves || 1) },
install(battle, w) {
w.mem.wolfCapacity = maxWolves; // (b.snap `wolves`, snapshot.js wolfView: the pips under the HP bar)
const tal = w.def.talents || [];
const wb = tal[0]?.bb ?? {};
const per = num(wb.block_cnt, num(wb['vigil_wolf_t_1_enhance[trigger].block_cnt'], 1));
+3
View File
@@ -180,6 +180,9 @@ export default {
const capOf = () => num(t0.max_minus_hp_ratio, 2) * unit.s.maxHp;
const quiet = num(t0['hsgma2_t_1[heal].interval'], 5);
const regen = num(t0['hsgma2_t_1[heal].hp_recovery_per_sec_by_max_hp_ratio']);
// the pool as a share of its cap, read when a snapshot is taken (b.snap `neg`, snapshot.js negView): the green HP bar
// sits on the 1-HP floor in 我执, so the client draws the pool as the red bar — display only
unit.negFill = () => (unit.mem.hsEgo && unit.mem.hsEgo.pool > 0 ? unit.mem.hsEgo.pool / Math.max(1e-9, capOf()) : 0);
const enter = (pool) => {
unit.mem.hsEgo = { pool: Math.max(0, pool), lastHurt: battle.time };
battle.addBuff(unit, { key: EGO, status: 'healFree', flags: { noHeal: true, healFree: true }, tags: ['talent'] });
+1
View File
@@ -773,6 +773,7 @@ function wolfPack(bb, raw, def) {
skill: null,
trait: { hitsFn: (b, u) => Math.max(1, u.mem.shadows ?? 1) },
install(battle, unit) {
unit.mem.wolfCapacity = maxShadows; // (b.snap `wolves`, snapshot.js wolfView: the pips under the HP bar)
// initial shadows: 伺夜's talent text ("初始两只"), else one below the maximum
const ot = (ownerOf(unit)?.def?.talents || []).map((t) => t.description || '').join(' ');
const mi = ot.match(/初始(两|二|\d+)只/);
+44 -1
View File
@@ -3,7 +3,8 @@
// b.snap = { fieldId, t, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]], dp, killed, total }
// (hp of a countdown summon — unit.countdown, content/tokens.js startCountdown — is maxHp × the share of its life left)
// UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, motion?, boss?, uid?,
// form?, skillIndex?, moduleId?, items?, standInFor?, diy? } (standInFor = the replaced operator's charId of a 补位
// form?, skillIndex?, ammoSkill?, moduleId?, items?, standInFor?, diy? } (ammoSkill = an ally whose skill is an ammo
// magazine: the HP bar shows its rounds as cells, b.snap `ammo`; standInFor = the replaced operator's charId of a 补位
// stand-in; diy = a 自选 piece's pick { charId, skillIndex, uniEquipId } — defId is its slot, spine / avatar the operator's;
// form = the unit's current model form — an enemy's, content/enemies/helpers.js setForm:
// 掠海漂移体 'crawl', 暴鸰 'bombed', 转译基底·α's forms …; a 傀儡师 fighting as its 替身 'doll', professions.js — a view built
@@ -48,6 +49,9 @@ export function unitInfo(u) {
uid: u.uid ?? undefined,
// DESIGN §16: the equipped skill's index (the renderer / audio pick that skill's Spine clip and sound)
skillIndex: u.side === 'ally' && Number.isInteger(d.skill?.index) ? d.skill.index : undefined,
// an ally whose skill is an ammo magazine: the renderer shows it as the segmented bar under the HP bar (b.snap `ammo`) and
// draws no sustained skill aura for it — known from the unit's first appearance, before any snapshot or skill event
ammoSkill: u.side === 'ally' && u.skill?.kind === 'ammo' ? true : undefined,
// DESIGN §16: an elite ally's equipped module (uniEquipId | 'none'; display only — a teammate's unit in a shared
// field shows its owner's module in the detail card)
moduleId: u.side === 'ally' && d.golden && typeof d.loadout?.moduleId === 'string' ? d.loadout.moduleId : undefined,
@@ -118,6 +122,45 @@ export function unitTuple(u, t) {
return [u.id, r2(u.x), r2(u.y), hp, maxHp, r1(sp), spMax, flagsOf(u), animOf(u, t)];
}
/**
* The three optional HUD readouts of b.snap (events.js snapshot(): `ammo`, `wolves`, `neg`; each only for a living,
* deployed, visible unit and only when it has one). They ride beside the 9-field tuples — the tuple, and the SP
* fraction the ammo skill still puts in it, stay as they are.
*
* `ammoView(u)` = `[rounds left, rounds in the magazine]` of a running ammo skill (whole numbers; the magazine is the
* activation's real total — extra bullets and refills included — that unitTuple's SP fraction runs on): the segmented
* yellow bar under the HP bar (隐现's S2 holds 14 rounds, skill_table skchr_inside_2 `attack@trigger_time`).
*/
export function ammoView(u) {
const sk = u.skill;
if (u.side !== 'ally' || !sk || !sk.active || sk.kind !== 'ammo') return null;
const left = Math.max(0, Math.ceil(sk.ammoLeft - 1e-9));
return [left, Math.max(1, Math.ceil((sk.ammoMax || sk.ammo || 0) - 1e-9), left)];
}
/**
* `wolfView(u)` = `[狼影 left, the talent's maximum]` of 伺夜's 狼群 pack (content/tokens.js wolfPack and the managed kit
* chess_char_3_19-vigil.js keep the count in `mem.shadows` and the maximum in `mem.wolfCapacity`): the pips under the HP
* bar. A pack in its 战术点形态 is not alive and shows nothing.
*/
export function wolfView(u) {
const cap = u.mem?.wolfCapacity, n = u.mem?.shadows;
if (u.side !== 'ally' || !(cap >= 1) || !Number.isFinite(n)) return null;
const max = Math.round(cap);
return [Math.max(0, Math.min(max, Math.round(n))), max];
}
/**
* `negView(u)` = the share (0.01–1) of its cap that a unit's negative-HP pool holds, null without a pool: 斩业星熊's 我执
* (kits/ops/op-hsgma2.js sets `unit.negFill`, the pool read when the snapshot is taken) — the red bar over the drained HP
* bar. The pool itself, and the 1-HP floor she stands on, are the sim's and are not touched.
*/
export function negView(u) {
if (typeof u.negFill !== 'function') return null;
const v = u.negFill();
return v > 0 ? Math.min(1, Math.max(0.01, r2(v))) : null;
}
/** Units included in a snapshot: deployed & visible, plus recently dead ones (DIE animation). */
export function snapshotUnits(units, t) {
const out = [];
+3
View File
@@ -84,6 +84,9 @@ export class Unit {
// a countdown summon's life on the field ({ from, until } battle times; content/tokens.js startCountdown, cleared by
// every deployment): its bar shows the time left (snapshot.js unitTuple), its HP never moves (无敌 + 禁疗)
this.countdown = null;
// a unit with a negative-HP pool (斩业星熊's 我执, kits/ops/op-hsgma2.js) sets this to a function returning the share of the
// pool's cap it holds (0–1): b.snap's `neg` list (snapshot.js negView) draws it as the red bar; display only
this.negFill = null;
// knocked out, the unit lies — and redeploys — on its home tile instead of where it fell (Battle._layBody) while content
// holds this: 乌尔比安 moved by his S3 (the owner's decision of 2026-10-07, a deviation from PRTS's "where it fell");
// every deployment clears it (battle/deploy.js _deploy)