feedback5 WY: flying units hover 1.3 tiles (the client's 0.35 ÷ the standard prefab scale 0.27); the 帝国炮火先兆者 pair is drawn 1.263× taller and 木制瑞印 mirrored, as their official prefabs (from PR #211 by @xcdoge; the owner's decision of 2026-10-06)
Ported by hand from PR #211 by @xcdoge: - render/units.js: FLY_HOVER 0.32 → 1.3, flat (the official fly offset is one model-independent Vector3(0, 0.35, 0) in the client's character space, whose unit is the standard battle-prefab scale 0.27; an official screenshot measures 1.2–1.4 tiles); enemyModelScaleY multiplies the skeleton's Y scale and the head (bar) height, also in the impostor path; a mirrorX model flips on top of the direction flip. The body, its HP bar, damage numbers, projectile hits and skill rings ride `hover`, the shadow stays on the ground; one camera drives the units over the 2D and the 3D board. - tools/build-data.mjs MODEL_STRETCH_Y / MIRRORED_PREFABS → enemies.json modelScaleY / mirrorX; data/enemies.json regenerated offline here (the PR's one-line blob was not taken) — JSON compare: only enemy_1112_emppnt / enemy_1112_emppnt_2 modelScaleY 1.263 and enemy_1196_msfyin mirrorX true. - tools/local-extract/enemy_model_offsets.py (optional, host-side prefab reader) and docs/research/12-flying-visuals-official.md (without the claim that the owner said 「都要」 on 2026-10-05 — the owner's decision of 2026-10-06 is cited; where the quirk enemies appear corrected against 0.2.0's data: 帝国炮火先兆者 is a FLY special enemy of the mode, 帝国炮火中枢先兆者 comes with the bounty 战术特训·飞行III, 木制瑞印 is in no wave). No screenshot or extracted art committed. Tests (from PR #211): test/render/unitview.test.js (FLY_HOVER ≈ 0.35 / 0.27; the same lift for every flyer; a flying view lifts by FLY_HOVER — added here: its body and HP bar ride the lift, its shadow stays on the ground), test/render/playtest6-enemies.test.js (the data keys; the 1.263 aspect and bar height; the mirrored model's x sign); both fail without the renderer change; the 425 render unit tests pass. Docs: DATA §9, ASSETS, design client.md, research 00 / 12, DESIGN §25.13.5 and the §25.10 table. Golden unchanged (drawing only; the sim reads neither field).
This commit is contained in:
1 parent
4e58481706
commit
66ae86b969
12 files changed
+703
-17
No files matched your search
@@ -5,9 +5,15 @@
|
||||
|
||||
import { test, describe, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { installFakePixi, fakeViewCtx } from './fakepixi.js';
|
||||
import { presetCamera } from '../../public/js/render/projection.js';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const ASSETS = JSON.parse(readFileSync(path.join(ROOT, 'data/assets.json'), 'utf8'));
|
||||
|
||||
let fake, UnitView, T;
|
||||
before(async () => {
|
||||
fake = installFakePixi();
|
||||
@@ -272,3 +278,70 @@ describe('enemy preview pen figures (lod idle)', () => {
|
||||
assert.ok(steps() <= 12, `idle loop stepped ≈ every 3rd frame (${steps()} of 30)`);
|
||||
});
|
||||
});
|
||||
|
||||
// Player report 2026-10-05: 「无人机等飞行单位贴图位置明显偏低」, and the follow-up "绝对不止 0.35" with an official
|
||||
// screenshot of 帝国炮火先兆者 over a tile (PR #211 by @xcdoge; the owner's decision of 2026-10-06). The lift is the
|
||||
// official client's own single constant — Vector3(0, 0.35, 0) written by Torappu.Battle.CharacterAnimator's constructor
|
||||
// (docs/research/12-flying-visuals-official.md) — measured in the client's character space, whose unit is the standard
|
||||
// battle-prefab scale 0.27, so the tile-space lift is 0.35 / 0.27 ≈ 1.3 tiles (the screenshot measures 1.2–1.4). The
|
||||
// client applies it to the model's root transform and nothing per model (its battle prefabs carry no flyer-specific
|
||||
// vertical offset), so a model whose art hangs below its origin keeps that hang and flies with it — 妖怪 at ≈ 0.9 tiles
|
||||
// of rotor clearance. The previous flat 0.32 left every flyer ~1 tile too low (the two 妖怪 drones even had their art
|
||||
// under the tile).
|
||||
describe('flying units hover FLY_HOVER above the ground, whatever their model', () => {
|
||||
const boundsOf = (key) => { const sp = ASSETS.enemies[key].spine; return (sp.front || sp).bounds; };
|
||||
const MODEL_K = { enemy_1005_yokai: 0.7407, enemy_1005_yokai_2: 0.8148, enemy_1040_bombd: 0.7407, enemy_1042_frostd: 0.6667 };
|
||||
/** Independent algorithm: tiles the art bottom hangs below the unit's ground point. */
|
||||
const sinkOf = (key) => (-boundsOf(key).y / 320) * MODEL_K[key];
|
||||
|
||||
test('FLY_HOVER is the client constant 0.35 in character space, i.e. 0.35 / 0.27 tiles', async () => {
|
||||
const { FLY_HOVER } = await import('../../public/js/render/units.js');
|
||||
const STANDARD_PREFAB_SCALE = 0.27; // enemies.json modelScale is a multiple of it (units.js enemyModelScale)
|
||||
const want = 0.35 / STANDARD_PREFAB_SCALE;
|
||||
assert.ok(Math.abs(FLY_HOVER - want) < 0.02, `FLY_HOVER ${FLY_HOVER} ≈ 0.35 / ${STANDARD_PREFAB_SCALE} = ${want.toFixed(3)} 格`);
|
||||
});
|
||||
|
||||
test('every flyer of this mode gets the same lift, whatever its model hangs below its origin', async () => {
|
||||
const { FLY_HOVER } = await import('../../public/js/render/units.js');
|
||||
for (const key of Object.keys(MODEL_K)) {
|
||||
const sink = sinkOf(key);
|
||||
assert.ok(sink > 0, `${key}: the model does hang ${sink.toFixed(3)} tiles below its pivot`);
|
||||
// the lift is model-independent, so a flyer's visible clearance is FLY_HOVER − sink, and it differs per model
|
||||
assert.ok(FLY_HOVER - sink > 0.8, `${key}: 净高度 ${(FLY_HOVER - sink).toFixed(3)} 格(修复前 0.32 − sink 为负 → 贴地)`);
|
||||
}
|
||||
// with the old flat 0.32 the two 妖怪 drones had a negative clearance = art under the tile, and 寒霜 floated 0.25
|
||||
assert.ok(0.32 - sinkOf('enemy_1005_yokai') < 0, 'before: 妖怪 −0.06 tiles');
|
||||
assert.ok(0.32 - sinkOf('enemy_1042_frostd') > 0.2, 'before: 寒霜 floated 0.25 tiles (inconsistent)');
|
||||
// a model whose art starts above its pivot (帝国炮火先兆者) gets the plain FLY_HOVER
|
||||
assert.equal(boundsOf('enemy_1112_emppnt').y > 0, true, '帝国炮火先兆者 art bottom is above the origin');
|
||||
assert.ok(FLY_HOVER > 1.2, 'the sub-tile 0.35 left every flyer about one tile low');
|
||||
});
|
||||
|
||||
test('a flying UnitView lifts by FLY_HOVER — its body and HP bar ride it, its shadow stays on the ground; a ground view keeps its feet on the tile', async () => {
|
||||
const { FLY_HOVER } = await import('../../public/js/render/units.js');
|
||||
const bounds = boundsOf('enemy_1005_yokai');
|
||||
const entry = { skel: '/s/x.skel', atlas: '/s/x.atlas', textures: ['/s/x.png'], anims: { idle: 'Idle' }, animations: { Idle: 1 }, bounds };
|
||||
const assets = {
|
||||
picture: () => null, image: async () => null, spineEntry: () => entry,
|
||||
spine: { acquire: async () => ({ animations: [{ name: 'Idle' }] }), release() {} },
|
||||
};
|
||||
const ctx = fakeViewCtx(fake.P, { assets, cam: cam, lookupDef: () => ({ modelScale: MODEL_K.enemy_1005_yokai }) });
|
||||
const fly = new UnitView(ctx, { id: 1, side: 'enemy', kind: 'enemy', defId: 'enemy_1005_yokai', x: 5, y: 12, maxHp: 100, motion: 'FLY' }, {});
|
||||
await tick(); await tick();
|
||||
for (let i = 0; i < 180; i++) fly.update(1 / 60, cam(), i / 60);
|
||||
assert.ok(Math.abs(fly.hover - FLY_HOVER) < 1e-3, `hover ${fly.hover.toFixed(3)} ≈ FLY_HOVER ${FLY_HOVER}`);
|
||||
const ground = new UnitView(ctx, { id: 2, side: 'enemy', kind: 'enemy', defId: 'enemy_1005_yokai', x: 5, y: 12, maxHp: 100 }, {});
|
||||
await tick(); await tick();
|
||||
for (let i = 0; i < 60; i++) ground.update(1 / 60, cam(), i / 60);
|
||||
assert.equal(ground.hover, 0, 'a ground unit is not lifted');
|
||||
// the same tile: the flyer's body (and the bar above it) is FLY_HOVER higher on screen, the shadows coincide
|
||||
const c = cam();
|
||||
const lift = c.project(5, 12, 0).y - c.project(5, 12, fly.hover).y;
|
||||
assert.ok(lift > 0);
|
||||
assert.ok(Math.abs((ground.screen.y - fly.screen.y) - lift) < 1e-6, `body lifted by the projected FLY_HOVER (${ground.screen.y - fly.screen.y} vs ${lift})`);
|
||||
// the bar sits the head height above the body (the projected scale at the body's height, so not exactly `lift`)
|
||||
assert.ok(fly.screen.top < fly.screen.y && Math.abs((fly.screen.y - fly.screen.top) - (ground.screen.y - ground.screen.top)) < 0.1 * (ground.screen.y - ground.screen.top), 'the HP bar keeps its head height over the body');
|
||||
assert.ok(ground.screen.top - fly.screen.top > 0.9 * lift, `the HP bar rides the body (${ground.screen.top - fly.screen.top} vs ${lift})`);
|
||||
assert.ok(Math.abs(fly.shadow.position.y - ground.shadow.position.y) < 1e-9, 'the shadow stays on the ground');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user