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

296 lines
8.9 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
startedAtMs: number
untilMs: number
readonly castOverlayToken?: string | undefined
readonly durationMs?: number | undefined
elapsedMs?: number | undefined
readonly stepSpeed256?: number | undefined
readonly frames?: number | undefined
readonly baseSpeed?: number | undefined
readonly effRate?: number | 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
nameSeed?: number | undefined
compVariants?: 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
modeStartedAtMs?: number | undefined
modeUntilMs?: number | undefined
modeDurationMs?: number | undefined
modeElapsedMs?: number | undefined
animStepSpeed256?: number | undefined
animEffRate?: 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.lifePct = snapshot.lifePct ?? 128
const defaultMode =
this.unitType === 'monster'
? this.lifePct <= 0
? 12
: 1
: this.unitType === 'player'
? 1
: 0
this.mode = snapshot.mode ?? defaultMode
this.direction = snapshot.direction ?? 0
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.flags & 0x04) !== 0 ? true : undefined)
this.isUnique = snapshot.isUnique ?? ((this.flags & 0x08) !== 0 ? true : undefined)
this.isSuperUnique = snapshot.isSuperUnique ?? ((this.flags & 0x02) !== 0 ? true : undefined)
this.superUniqueId = snapshot.superUniqueId
this.bossMods = snapshot.bossMods
this.nameSeed = snapshot.nameSeed
this.compVariants = snapshot.compVariants
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.mode === undefined && this.unitType === 'monster') {
if (this.lifePct <= 0) {
this.mode = 12
} else if (this.mode === 0 || this.mode === 12) {
this.mode = 1
}
}
}
if (snapshot.flags !== undefined) {
this.flags = snapshot.flags
if ((snapshot.flags & 0x04) !== 0) this.isChampion = true
if ((snapshot.flags & 0x08) !== 0) this.isUnique = true
if ((snapshot.flags & 0x02) !== 0) this.isSuperUnique = true
}
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.nameSeed !== undefined) {
this.nameSeed = snapshot.nameSeed
}
if (snapshot.compVariants !== undefined) {
this.compVariants = snapshot.compVariants
}
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
if (!this.dead && (this.unitType === 'monster' || this.unitType === 'player')) {
if (mode === 'stop') {
this.mode = 1
} else if (mode === 'run') {
this.mode = this.unitType === 'monster' ? 15 : 3
} else if (mode === 'knockback') {
this.mode = this.unitType === 'monster' ? 9 : 19
} else {
this.mode = 2
}
}
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')
}
}