From 8fcd2238cc70a054a2051845d247a3a3bc0024cd Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Fri, 2 Oct 2026 06:37:16 +0000 Subject: [PATCH] fix(client): face skill-cast targets with the 1.13c dir64 ClientUnit.direction is a dir64 since the facing fix, but the SkillCast handler from the online gameplay loops still stored an 8-direction atan2 index (0 = +X). 0..7 passes assertDir64, so a casting unit silently drew a near-south row. Face the cast target through dir64FromPoints, the same 1.13c direction table as movement; a target underfoot keeps the facing. --- src/client/world/client-world.ts | 11 ++++------- tests/client/unit-facing-motion.test.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/client/world/client-world.ts b/src/client/world/client-world.ts index 7a941d3..e1e2a12 100644 --- a/src/client/world/client-world.ts +++ b/src/client/world/client-world.ts @@ -18,6 +18,7 @@ import { type UnitType, } from '../../netproto/index.ts' import { onlineItemDataToUiInventoryItem } from '../../common/items/item-bridge.ts' +import { dir64FromPoints } from '../../common/units/direction.ts' import { ClientUnit, makeUnitKey, type UnitCastState } from './client-unit.ts' import { ClientInventoryManager, type ClientItemContainers } from './inventory.ts' import { MutableSelfPlayerState, type SelfPlayerState } from './self.ts' @@ -218,13 +219,9 @@ export class ClientWorld { private applySkillCastToUnit(unit: ClientUnit, skillId: number, targetX: number, targetY: number): void { unit.targetX = targetX unit.targetY = targetY - const dx = targetX - unit.x - const dy = targetY - unit.y - if (dx !== 0 || dy !== 0) { - const angle = Math.atan2(dy, dx) - const normalized = ((angle + Math.PI * 2) % (Math.PI * 2)) / (Math.PI * 2) - unit.direction = Math.floor(normalized * 8) % 8 - } + // `ClientUnit.direction` is a dir64 (`common/units/direction.ts`): face the target through the + // same 1.13c direction table as movement; a target on the unit's own position keeps the facing. + unit.direction = dir64FromPoints(unit.x, unit.y, targetX, targetY, unit.direction) const isAttack = PHYSICAL_ATTACK_SKILL_IDS.has(skillId) const mode = unit.unitType === 'monster' ? (isAttack ? 4 : 7) : isAttack ? 7 : 10 unit.mode = mode diff --git a/tests/client/unit-facing-motion.test.ts b/tests/client/unit-facing-motion.test.ts index 0c9f482..3480762 100644 --- a/tests/client/unit-facing-motion.test.ts +++ b/tests/client/unit-facing-motion.test.ts @@ -261,3 +261,17 @@ describe('remote unit facing (RemoteEntityInterpolator)', () => { expect(snapped).toMatchObject({ x: 5003.1, moving: false, dir64: glide.dir64 }) }) }) + +describe('skill-cast facing (ClientWorld)', () => { + it('turns the caster toward the target as a dir64 and keeps it for a target underfoot', () => { + const world = new ClientWorld() + world.apply({ type: 'UnitAssign', unit: { unitType: 1, id: 40, classId: 5, x: 5000, y: 5000 } }) + const castAt = (x: number, y: number): number => { + world.apply({ type: 'SkillCast', caster: { unitType: 1, id: 40 }, skill: 0, level: 1, target: { x, y } }) + return world.getUnit('monster', 40)!.direction + } + expect(castAt(5010, 5000)).toBe(56) // world +X: screen south-east + expect(castAt(5000, 5000)).toBe(56) + expect(castAt(4990, 4990)).toBe(32) // world −X−Y: screen north + }) +})