feedback7: merge fb7-render — #382 赤刃明霄陈 S3 sword-qi drawn (sim chen3Wave fx), #381 enemy attack recovery held in interpolation, #380 push / pull slide (corpse stays where pushed; same-step kills finish the slide), #384 eye masks, 香槟炸弹 HP bar hidden, #246 an unwound attack plays its whole clip
- Conflicts with the merged fb7-hud (both added display-only snapshot lists): server/sim/battle/events.js, types/core.js,
public/js/render/interp.js, docs/SIM.md, docs/design/network.md — union of ammo / wolves / neg and stand / standCut.
- Grok review: MERGE AFTER FIXES — the HIGH finding (same-step push kill) fixed on the branch (7a33bc7e).
- Golden: diy-073 / diy-074 (fx counts only; union OK); broad node --test 5442 (5441 pass, 0 fail, 1 skipped); lint 0
errors; typecheck.
This commit is contained in:
commit
cf98f4c3f8
27 files changed
+1812
-53
No files matched your search
@@ -535,6 +535,7 @@ export function updateEnemy(b, e, dt) {
|
||||
if (!e.alive) return;
|
||||
// hidden (teleporting) enemies only advance wait legs
|
||||
const stunned = e.s.flags.stun;
|
||||
if (stunned || e.hidden || e.s.flags.fear) b._cutAttackStand(e);
|
||||
const prevCd = e.atkCd;
|
||||
if (e.atkCd > 0 && !stunned && !e.hidden) e.atkCd = Math.max(0, e.atkCd - dt);
|
||||
// a stun / freeze / sleep / 浮空 — or leaving the field — takes the enemy out of its attack: a swing short of its damage
|
||||
@@ -644,6 +645,7 @@ function advanceRoute(b, e, dt, R, standing = false) {
|
||||
continue;
|
||||
}
|
||||
if (leg.t === 'disappear') {
|
||||
b._cutAttackStand(e);
|
||||
b._setHidden(e, true);
|
||||
e.atkStandUntil = -Infinity; standing = false; // off the field: its attack clip is over
|
||||
R.legIdx++; R.pts = null;
|
||||
|
||||
@@ -97,6 +97,7 @@ export class BattleDeploy {
|
||||
}
|
||||
|
||||
_remove(unit, reason, killer = null, permanent = false, dying = false) {
|
||||
this._cutAttackStand(unit);
|
||||
unit.alive = false;
|
||||
unit.removeReason = reason;
|
||||
unit.deployed = false;
|
||||
|
||||
@@ -141,6 +141,7 @@ export class BattleDisplacement {
|
||||
if (moved > 0) {
|
||||
this._unblock(e);
|
||||
// 失衡 ends the attack clip it stood for (PRTS 状态机: the states are exclusive — UNBALANCE, then DEFAULT → MOVE)
|
||||
this._cutAttackStand(e);
|
||||
e.atkStandUntil = -Infinity;
|
||||
if (e.route) e.route.pts = null;
|
||||
this.fx('displace', { x: e.x, y: e.y, id: e.id });
|
||||
|
||||
@@ -32,6 +32,16 @@ export class BattleEvents {
|
||||
return ev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that an enemy's attack recovery (atkStandUntil) is cut or ignored now — snapshot `standCut`, display metadata
|
||||
* (render/interp.js); never changes the attack's timing or state.
|
||||
*/
|
||||
_cutAttackStand(unit) {
|
||||
if (unit.side === 'enemy' && Number.isFinite(unit.atkStandUntil) && unit.atkStandUntil > this.time) {
|
||||
unit.atkStandCutAt = this.time;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact full snapshot of this field (DESIGN §8.2 b.snap), plus (only when non-empty):
|
||||
* down: [[id, respawnAt, respawnTime, state, row, col]] — operators that left the field waiting to redeploy (isDown): the
|
||||
@@ -40,8 +50,13 @@ export class BattleEvents {
|
||||
* 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.
|
||||
* neg: [[id, fill]] — the share of its cap a negative-HP pool holds (snapshot.js negView; 斩业星熊's 我执): the red bar;
|
||||
* stand: [[id, until]] — when each enemy's attack recovery ends (atkStandUntil; alive, deployed, visible, not
|
||||
* feared or stunned) — display metadata (render/interp.js holds the position until then);
|
||||
* standCut: [[id, at]] — the latest time each listed enemy's recovery was cut or ignored (_cutAttackStand), the
|
||||
* dying ones in their death window included.
|
||||
* ammo / wolves / neg / stand / standCut are display only: no sim state reads them, and the nine-field unit tuples are
|
||||
* unchanged.
|
||||
*/
|
||||
snapshot() {
|
||||
const snap = {
|
||||
@@ -64,9 +79,18 @@ 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, ammo = null, wolves = null, neg = null;
|
||||
let elem = null, ammo = null, wolves = null, neg = null, stand = null, standCut = null;
|
||||
let listed = null; // the ids in snap.units, built for the first enemy with a cut to report
|
||||
for (const u of this.units) {
|
||||
if (u.side === 'enemy' && u.atkStandCutAt >= 0) {
|
||||
const cutAt = Math.round(u.atkStandCutAt * 1000) / 1000;
|
||||
if (cutAt <= snap.t && (listed || (listed = new Set(snap.units.map((x) => x[0])))).has(u.id)) (standCut || (standCut = [])).push([u.id, cutAt]);
|
||||
}
|
||||
if (!u.alive || !u.deployed || u.hidden) continue;
|
||||
const until = Math.round(u.atkStandUntil * 1000) / 1000;
|
||||
if (u.side === 'enemy' && !u.s.flags.fear && !u.s.flags.stun && Number.isFinite(until) && until > snap.t) {
|
||||
(stand || (stand = [])).push([u.id, until]);
|
||||
}
|
||||
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);
|
||||
@@ -80,6 +104,8 @@ export class BattleEvents {
|
||||
if (ammo) snap.ammo = ammo;
|
||||
if (wolves) snap.wolves = wolves;
|
||||
if (neg) snap.neg = neg;
|
||||
if (stand) snap.stand = stand;
|
||||
if (standCut) snap.standCut = standCut;
|
||||
return snap;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,7 @@ export class BattleSpawns {
|
||||
e.atkCd = 0;
|
||||
e.pauseUntil = -Infinity; // content holds (暴鸰's drop)
|
||||
e.atkStandUntil = -Infinity; // standing for its attack clip (ai.js attackStand)
|
||||
e.atkStandCutAt = -Infinity; // display metadata: when its stand was last cut or ignored (snapshot standCut)
|
||||
e.swing = false; // a normal attack swung, its damage frame not reached yet (ai.js enemyAttack)
|
||||
// every enemy profile starts with the same fields (stable object shapes keep the hot loop's property reads fast);
|
||||
// `dmgType` null = the data's (content may arm a data-unarmed enemy: ai.js enemyAttack); `blockFree` = its 索敌不受阻挡
|
||||
|
||||
@@ -46,7 +46,10 @@
|
||||
// field, the stage tiles beyond it are another player's half] —, an 侵入点 or a 保护目标 it turns 90° clockwise (four
|
||||
// turns at most per tick; boxed in on every side it waits) and forgets whom it hit; each enemy it can select within 1.3
|
||||
// (air too) is hit once per straight run for the larger of hp_ratio × its current HP (the unit's own HP, PRTS) and
|
||||
// projectile_min_atk_scale × her ATK at the hit, arts (RES applies — PRTS "非伤害保底或无视法术抗性"; 普通伤害). Her
|
||||
// projectile_min_atk_scale × her ATK at the hit, arts (RES applies — PRTS "非伤害保底或无视法术抗性"; 普通伤害).
|
||||
// The wave is a kit-managed probe, not a snapshot projectile, so it is shown with `fx('chen3Wave')` events every
|
||||
// WAVE_FX_EVERY s (its position, the previous point and its direction — render/fx 'qi': a procedural blade trail with
|
||||
// a crescent head, not the official particle prefab); before that the 龙剑气 had no visual at all (PR #382). Her
|
||||
// begin clips (S1 0.2 s, S3 0.433 s) are not modelled [ASSUMED: the engine starts skills at once].
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, statBuff, toggleBuff, up } from '../shared/tier1.js';
|
||||
@@ -69,6 +72,14 @@ const RETARGET_RADIUS = 1.7;
|
||||
const WAVE_SPEED = 1.5;
|
||||
const WAVE_PROBE = 0.25;
|
||||
const WAVE_HIT_RADIUS = 1.3;
|
||||
/**
|
||||
* How often the wave's position is sent to the client as an `fx` (`chen3Wave`, carrying the previous point and its
|
||||
* direction). The wave is a kit-managed probe (it turns corners and lives as long as the skill), so it is not in the
|
||||
* snapshot's projectile list: without these events the client has nothing to draw and the 龙剑气 is invisible (the cast
|
||||
* emitted one `dash` puff and each hit a `slash` spark). The rate is the renderer's: each event draws a short blade
|
||||
* trail whose life outlasts the gap, so the events join into one continuous wave.
|
||||
*/
|
||||
const WAVE_FX_EVERY = 0.12;
|
||||
/** S1's early end: a silence of hers that ends within this of the skill's planned end ended with it. */
|
||||
const SILENCE_SLACK = 0.1;
|
||||
const AIR = Object.freeze({ canHitFly: true });
|
||||
@@ -215,7 +226,8 @@ export default {
|
||||
attack: { atkScale: num(b3['attack@atk_scale'], 1), hits: 3 }, // `_additionalTimes` 2
|
||||
onStart({ battle, unit, skill }) {
|
||||
const [dr, dc] = unit.fwd;
|
||||
unit.mem.chen3Wave = { x: unit.x, y: unit.y, dr, dc, hit: new Set(), act: skill.activations, seq: unit.deploySeq };
|
||||
unit.mem.chen3Wave = { x: unit.x, y: unit.y, dr, dc, hit: new Set(), act: skill.activations, seq: unit.deploySeq,
|
||||
fx: 0, fxX: unit.x, fxY: unit.y };
|
||||
battle.fx('dash', { x: unit.x, y: unit.y, id: unit.id, skill: 'chen3:wave' });
|
||||
},
|
||||
onEnd({ unit }) { unit.mem.chen3Wave = null; },
|
||||
@@ -285,8 +297,17 @@ export default {
|
||||
w.dr = 0 - w.dc; // (`0 - x`: never a negative zero)
|
||||
w.dc = dr;
|
||||
w.hit.clear();
|
||||
w.fxX = w.x; w.fxY = w.y; // the drawn trail starts again at the corner (no streak across the turn)
|
||||
}
|
||||
if (!waveBlocked(battle, w)) { w.x += w.dc * WAVE_SPEED * dt; w.y += w.dr * WAVE_SPEED * dt; }
|
||||
// the 龙剑气 itself: tell the client where it is now (and where it came from) so it can be drawn — while it
|
||||
// runs, while it is boxed in waiting, and after a turn (the direction comes along for the renderer)
|
||||
w.fx += dt;
|
||||
if (w.fx >= WAVE_FX_EVERY) {
|
||||
w.fx = 0;
|
||||
battle.fx('chen3Wave', { x: w.x, y: w.y, fromX: w.fxX, fromY: w.fxY, dr: w.dr, dc: w.dc, id: unit.id, skill: 'chen3:wave' });
|
||||
w.fxX = w.x; w.fxY = w.y;
|
||||
}
|
||||
for (const e of battle.foesInRadius(w.x, w.y, WAVE_HIT_RADIUS)) {
|
||||
if (w.hit.has(e) || !canTargetEnemy(unit, e, AIR)) continue;
|
||||
w.hit.add(e);
|
||||
|
||||
Reference in new issue
Block a user