diablo2-web/src/client/world/client-unit.ts

245 lines
7.0 KiB
TypeScript

/**
* Client Unit Mirror (`src/client/world/client-unit.ts`).
*
* Represents a single authoritative entity (player, monster, object, missile, item, tile)
* tracked by `ClientWorld`.
*/
import {
unitTypeFromId,
type ItemData,
type Pt,
type UnitRef,
type UnitSnapshot,
type UnitType,
type UnitTypeName,
} from '../../netproto/index.ts'
import { StatList } from './stat-list.ts'
import { UnitStateSet } from './states.ts'
export interface ClientUnitOverlay {
readonly overlayId: number
readonly token: string
frame: number
readonly totalFrames: number
readonly animRate: number
readonly loop: boolean
readonly blendMode: 0 | 1 | 2 | 3
readonly yOffset: number
readonly startedAtMs: number
ageTicks: number
readonly maxTicks: number
readonly lightRadius: number
readonly lightColor: readonly [number, number, number]
}
export interface UnitCastState {
readonly skillId: number
readonly targetX: number
readonly targetY: number
readonly mode: number
readonly startedAtMs: number
readonly untilMs: number
readonly castOverlayToken?: string | undefined
}
export function makeUnitKey(unitType: UnitType, id: number): string {
return `${unitTypeFromId(unitType)}:${id}`
}
export class ClientUnit {
readonly key: string
readonly unitType: UnitTypeName
readonly id: number
classId: number
name?: string | undefined
itemCode?: string | undefined
itemQuality?: number | undefined
goldAmount?: number | undefined
x: number
y: number
targetX?: number | undefined
targetY?: number | undefined
targetUnit?: UnitRef | undefined
moveMode?: 'walk' | 'run' | 'knockback' | 'stop' | undefined
mode: number
direction: number
lifePct: number
flags: number
warpId?: number | undefined
objectState?: number | undefined
interactType?: number | undefined
isChampion?: boolean | undefined
isUnique?: boolean | undefined
isSuperUnique?: boolean | undefined
superUniqueId?: number | undefined
bossMods?: readonly number[] | undefined
ownerType?: UnitTypeName | undefined
ownerId?: number | undefined
overheadText?: string | undefined
itemData?: ItemData | undefined
castState?: UnitCastState | undefined
lastSkillId?: number | undefined
overlays: ClientUnitOverlay[] = []
// Missile simulation fields (populated when unitType === 'missile')
missileKey?: string | undefined
vx?: number | undefined
vy?: number | undefined
ageTicks?: number | undefined
maxTicks?: number | undefined
frame?: number | undefined
totalFrames?: number | undefined
animSpeed?: number | undefined
loopAnim?: boolean | undefined
numDirections?: number | undefined
explosionMissile?: string | undefined
alwaysExplode?: boolean | undefined
pierce?: boolean | undefined
hitUnitIds?: Set<number> | undefined
altitude?: number | undefined
originX?: number | undefined
originY?: number | undefined
spiralAngle?: number | undefined
spiralRadius?: number | undefined
jitterIndex?: number | undefined
emitterKind?: 'frozenorb' | 'meteor_controller' | 'blizzard_controller' | undefined
subMissileIdx?: number | undefined
startedAtMs?: number | undefined
startedAtTick?: number | undefined
sourceSkillId?: number | undefined
skillLevel?: number | undefined
readonly stats = new StatList()
readonly states = new UnitStateSet()
constructor(snapshot: UnitSnapshot) {
this.unitType = unitTypeFromId(snapshot.unitType)
this.id = snapshot.id
this.key = `${this.unitType}:${this.id}`
this.classId = snapshot.classId >= 0 ? snapshot.classId : 0
this.name = snapshot.name
this.x = snapshot.x
this.y = snapshot.y
this.mode = snapshot.mode ?? 0
this.direction = snapshot.direction ?? 0
this.lifePct = snapshot.lifePct ?? 128
this.flags = snapshot.flags ?? 0
this.warpId = snapshot.warpId ?? (this.unitType === 'tile' ? this.classId : undefined)
this.objectState = snapshot.objectState
this.interactType = snapshot.interactType
this.isChampion = snapshot.isChampion
this.isUnique = snapshot.isUnique
this.isSuperUnique = snapshot.isSuperUnique
this.superUniqueId = snapshot.superUniqueId
this.bossMods = snapshot.bossMods
this.ownerType = snapshot.ownerType !== undefined ? unitTypeFromId(snapshot.ownerType) : undefined
this.ownerId = snapshot.ownerId
if (snapshot.states) {
this.states.replaceAll(snapshot.states)
}
}
get kind(): UnitTypeName {
return this.unitType
}
get dir(): number {
return this.direction
}
set dir(v: number) {
this.direction = v
}
get quality(): number | undefined {
return this.itemQuality
}
set quality(v: number | undefined) {
this.itemQuality = v
}
updateFromSnapshot(snapshot: UnitSnapshot): void {
if (snapshot.classId >= 0) {
this.classId = snapshot.classId
}
if (snapshot.name !== undefined && snapshot.name.length > 0) {
this.name = snapshot.name
}
if (snapshot.x !== 0 || snapshot.y !== 0) {
this.x = snapshot.x
this.y = snapshot.y
}
if (snapshot.mode !== undefined) {
this.mode = snapshot.mode
}
if (snapshot.direction !== undefined) {
this.direction = snapshot.direction
}
if (snapshot.lifePct !== undefined) {
this.lifePct = snapshot.lifePct
}
if (snapshot.flags !== undefined) {
this.flags = snapshot.flags
}
if (snapshot.warpId !== undefined) {
this.warpId = snapshot.warpId
} else if (this.unitType === 'tile' && this.warpId === undefined && this.classId >= 0) {
this.warpId = this.classId
}
if (snapshot.objectState !== undefined) {
this.objectState = snapshot.objectState
}
if (snapshot.interactType !== undefined) {
this.interactType = snapshot.interactType
}
if (snapshot.isChampion !== undefined) {
this.isChampion = snapshot.isChampion
}
if (snapshot.isUnique !== undefined) {
this.isUnique = snapshot.isUnique
}
if (snapshot.isSuperUnique !== undefined) {
this.isSuperUnique = snapshot.isSuperUnique
}
if (snapshot.superUniqueId !== undefined) {
this.superUniqueId = snapshot.superUniqueId
}
if (snapshot.bossMods !== undefined) {
this.bossMods = snapshot.bossMods
}
if (snapshot.ownerType !== undefined) {
this.ownerType = unitTypeFromId(snapshot.ownerType)
}
if (snapshot.ownerId !== undefined) {
this.ownerId = snapshot.ownerId
}
if (snapshot.states !== undefined) {
this.states.replaceAll(snapshot.states)
}
}
applyMove(
mode: 'walk' | 'run' | 'knockback' | 'stop',
to: Pt,
from?: Pt,
target?: UnitRef,
): void {
if (from && (from.x !== 0 || from.y !== 0)) {
this.x = from.x
this.y = from.y
}
this.moveMode = mode
this.targetX = to.x
this.targetY = to.y
this.targetUnit = target
if (to.x !== 0 || to.y !== 0) {
this.x = to.x
this.y = to.y
}
}
get dead(): boolean {
return this.lifePct <= 0 && (this.unitType === 'monster' || this.unitType === 'player')
}
}