296 lines
8.9 KiB
TypeScript
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')
|
|
}
|
|
}
|