feedback6 R21F: an enemy flyer hovers FLY_HOVER above the road whatever tile it crosses (GitHub #277: one high-ground / forbidden block on its way lifted it 0.42 / 0.3 tiles and dropped it again like a step) — UnitView.sync keeps every enemy at z 0, operators and summons keep their tile top, the flyer's shadow lies on the tile under it (shadowZ) on the 2D and the 3D board; golden: unchanged

This commit is contained in:
sganggs committed 2026-10-07 17:49:51 +08:00
1 parent 783bd30afd
commit 14df10a5a3
5 files changed
+81 -11

No files matched your search

+27 -9
View File
@@ -137,8 +137,9 @@ export const SPINE_STUCK_MS = 5000;
/** Heights above this count as standing on a raised top (bench pads are the lowest raised tiles, 0.16). */
const RAISED_Z = 0.12;
/**
* Flying units hover this many tiles above the ground they cross (PR #211 by @xcdoge; the owner's decision of
* 2026-10-06; docs/research/12-flying-visuals-official.md).
* Flying units hover this many tiles up (PR #211 by @xcdoge; the owner's decision of 2026-10-06;
* docs/research/12-flying-visuals-official.md): an enemy flyer above the road (z 0) whatever tile it crosses — a raised
* block under it is no step (GitHub #277) —, an operator or summon above its tile.
*
* The official client's fly offset is a **single constant, `Vector3(0, 0.35f, 0)`**: `Torappu.Battle.CharacterAnimator`'s
* constructor stores it in the instance field at +0x114 (`GameAssembly.dll` 0x180600555 reads the constant at 0x186a78a50 =
@@ -373,9 +374,13 @@ export class UnitView {
this.golden = !!info.golden;
this.tier = clamp(Number(info.tier) || 1, 1, 6);
this.x = Number(info.x) || 0; this.y = Number(info.y) || 0; this.z = 0;
this.zTarget = null; // battle: standing height the feet ease towards (tile top under the unit)
this.zTarget = null; // battle: standing height the feet ease towards (tile top under the unit; 0 for enemies)
/** @type {number|null} */
this.shadowZ = null; // an enemy flyer's shadow height (the tile top under it), else null: the shadow is at z
/** @type {number|null} */
this.shadowZTarget = null;
this.flying = info.motion === 'FLY';
this.hover = 0; // flying: body height above the ground under it
this.hover = 0; // flying: body height above the road / the ground under it
this.dir = this.isEnemy ? null : unitDir(info);
// whether the direction is known (UnitInfo / piece `dir`), not just the legacy ±1: battle and scouting views show
// the ground wedge only then (a derived RIGHT would mislabel an UP / DOWN operator)
@@ -704,10 +709,17 @@ export class UnitView {
this.elFill = this.el ? s.elFill || 0 : 0; this.elUntil = this.el ? s.elUntil || 0 : 0; this.elDur = this.el ? s.elDur || 0 : 0;
this.x = s.x; this.y = s.y;
this.flying = !!(s.flags & UF.FLYING) || this.info.motion === 'FLY';
// ground enemies only ever walk low tiles (a rounding step onto a block edge must not pop them up)
const gz = this.isEnemy && !this.flying ? 0 : groundZ(this.ctx, s.x, s.y);
// enemies keep to the road plane: ground enemies only ever walk low tiles (a rounding step onto a block edge must not
// pop them up), and a flyer hovers FLY_HOVER above the road whatever tile it crosses — the official lift is one
// constant over the route (docs/research/12), so a block under it is no step (GitHub #277: a flyer passing over one
// high-ground / forbidden block rose and dropped like stairs). Its shadow still lies on the tile top under it.
const floor = groundZ(this.ctx, s.x, s.y);
const gz = this.isEnemy ? 0 : floor;
if (this.zTarget == null) this.z = gz;
this.zTarget = gz;
this.shadowZTarget = this.isEnemy && this.flying ? floor : null;
if (this.shadowZTarget == null) this.shadowZ = null;
else if (this.shadowZ == null) this.shadowZ = this.z;
if (s.maxHp > 0) this.maxHp = s.maxHp;
const hp = clamp(s.hp, 0, this.maxHp);
if (hp < this.hp - 0.5 && this.isBoss) this.shake = 0.25;
@@ -941,6 +953,10 @@ export class UnitView {
const d = this.zTarget - this.z;
this.z = Math.abs(d) < 1e-3 ? this.zTarget : this.z + d * Math.min(1, dt * 12);
}
if (this.shadowZTarget != null && this.shadowZ !== this.shadowZTarget) {
const d = this.shadowZTarget - this.shadowZ;
this.shadowZ = Math.abs(d) < 1e-3 ? this.shadowZTarget : this.shadowZ + d * Math.min(1, dt * 12);
}
const hoverTo = this.flying && this.alive ? FLY_HOVER : 0;
if (this.hover !== hoverTo) this.hover = Math.abs(hoverTo - this.hover) < 1e-3 ? hoverTo : this.hover + (hoverTo - this.hover) * Math.min(1, dt * 6);
const p = cam.project(this.x, this.y, this.z + this.hover + this.lift, this.screen);
@@ -977,9 +993,11 @@ export class UnitView {
if (this._cull(bx, by, s, dt)) return;
const flip = (this.isEnemy ? (ENEMY_MODEL_FACES_LEFT ? -this.visFacing : this.visFacing) : this.visFacing) * (this.mirrorX ? -1 : 1);
// shadow (on a raised top it is drawn with that block row, else in the shadow layer under everything)
placeOnGround(this.ctx, this.shadow, this.ctx.layers.shadow, this.y, this.z);
const sh = cam.project(this.x, this.y, this.z, SH_P);
// shadow (on a raised top it is drawn with that block row, else in the shadow layer under everything); an enemy
// flyer's lies on the tile under it while its body hovers from the road (shadowZ, GitHub #277)
const shz = this.shadowZ ?? this.z;
placeOnGround(this.ctx, this.shadow, this.ctx.layers.shadow, this.y, shz);
const sh = cam.project(this.x, this.y, shz, SH_P);
this.shadow.position.set(sh.x, sh.y);
const shw = s * (this.isBoss ? 1.6 : 0.95) / this.shadow.texture.width;
this.shadow.scale.set(shw, shw * (this.shadow.texture === shadowTexture() ? 1 : 1.05));