@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
346 lines (345 loc) • 12.8 kB
JavaScript
"use strict";
/**
* ARCHITECTURE: EntityManager
*
* Tracks all entities (mobs, players, items) in the currently loaded world view.
* Receives entity updates from the active world data source and maintains entity state.
*
* ENTITY LIFECYCLE:
* add_actor/add_player → create entity state
* move_actor_absolute/move_actor_delta → update position
* set_actor_data → update metadata
* remove_actor → delete entity state
*
* INTERPOLATION:
* Server sends at ~20 TPS, client renders at 60 FPS.
* We store previous + current positions and interpolate between them.
*/
Object.defineProperty(exports, "__esModule", { value: true });
class EntityManager {
_entities = new Map();
_playerEntities = new Map(); // username → runtimeId
_localPlayerRuntimeId;
get entities() {
return this._entities;
}
get entityCount() {
return this._entities.size;
}
getEntityByRuntimeId(runtimeId) {
return this._entities.get(runtimeId);
}
get localPlayerRuntimeId() {
return this._localPlayerRuntimeId;
}
setLocalPlayerRuntimeId(id) {
this._localPlayerRuntimeId = id;
}
/**
* Find the closest entity within the player's look direction (for attacking).
* Uses a simple sphere check + dot product test rather than full AABB raycast.
* Returns the entity's runtime ID or undefined if none found.
*/
findEntityInLookDirection(eyePos, lookDir, maxDist = 6) {
let bestId;
let bestDist = maxDist;
for (const [rid, entity] of this._entities) {
if (rid === this._localPlayerRuntimeId)
continue;
// Vector from eye to entity center (entity position + half height)
const dx = entity.position.x - eyePos.x;
const dy = entity.position.y + 0.9 - eyePos.y; // approximate center
const dz = entity.position.z - eyePos.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist > maxDist || dist < 0.5)
continue;
// Check if entity is roughly in look direction (dot product > cos(15°))
const dot = (dx * lookDir.x + dy * lookDir.y + dz * lookDir.z) / dist;
// Wider cone for closer entities (easier to hit up close)
const minDot = dist < 2 ? 0.7 : 0.9;
if (dot < minDot)
continue;
if (dist < bestDist) {
bestDist = dist;
bestId = rid;
}
}
return bestId;
}
/**
* Handle add_actor packet — a non-player entity spawns.
*/
handleAddActor(packet) {
const runtimeId = packet.runtime_id ?? packet.runtime_entity_id;
if (runtimeId === undefined)
return;
const entity = {
runtimeId,
uniqueId: packet.unique_id ?? packet.unique_entity_id,
typeId: packet.entity_type ?? packet.identifier ?? "unknown",
position: this._extractPosition(packet.position),
prevPosition: this._extractPosition(packet.position),
rotation: this._extractRotation(packet.rotation),
prevRotation: this._extractRotation(packet.rotation),
velocity: this._extractPosition(packet.velocity ?? { x: 0, y: 0, z: 0 }),
isPlayer: false,
metadata: packet.metadata,
lastUpdateTime: performance.now(),
interpolationAlpha: 1,
};
this._entities.set(runtimeId, entity);
}
/**
* Handle add_player packet — another player spawns.
*/
handleAddPlayer(packet) {
const runtimeId = packet.runtime_id ?? packet.runtime_entity_id;
if (runtimeId === undefined)
return;
const username = packet.username ?? packet.user_name ?? "Player";
const entity = {
runtimeId,
uniqueId: packet.unique_id ?? packet.unique_entity_id,
typeId: "minecraft:player",
displayName: username,
position: this._extractPosition(packet.position),
prevPosition: this._extractPosition(packet.position),
rotation: this._extractRotation(packet.rotation),
prevRotation: this._extractRotation(packet.rotation),
velocity: { x: 0, y: 0, z: 0 },
isPlayer: true,
username: username,
metadata: packet.metadata,
lastUpdateTime: performance.now(),
interpolationAlpha: 1,
};
this._entities.set(runtimeId, entity);
this._playerEntities.set(username, runtimeId);
}
/**
* Handle remove_entity packet.
* Note: remove_entity uses entity_id_self which is the UNIQUE entity ID (zigzag64),
* not the runtime ID. We need to search by uniqueId since our Map is keyed by runtimeId.
*/
handleRemoveActor(packet) {
// Try runtime_entity_id first (some packets use this)
let runtimeId = packet.runtime_entity_id;
// remove_entity packet uses entity_id_self which is the unique ID, not runtime ID
if (runtimeId === undefined && packet.entity_id_self !== undefined) {
// Search for entity by uniqueId
for (const [rid, entity] of this._entities) {
if (entity.uniqueId === packet.entity_id_self) {
runtimeId = rid;
break;
}
}
}
if (runtimeId === undefined)
return;
const entity = this._entities.get(runtimeId);
if (entity?.username) {
this._playerEntities.delete(entity.username);
}
this._entities.delete(runtimeId);
}
/**
* Handle move_actor_absolute packet.
*/
handleMoveActorAbsolute(packet) {
const runtimeId = packet.runtime_entity_id ?? packet.runtime_id;
if (runtimeId === undefined)
return;
const entity = this._entities.get(runtimeId);
if (!entity)
return;
// Store previous for interpolation
entity.prevPosition = { ...entity.position };
entity.prevRotation = { ...entity.rotation };
const pos = packet.position;
if (pos) {
entity.position = this._extractPosition(pos);
}
if (packet.rotation) {
entity.rotation = this._extractRotation(packet.rotation);
}
else {
// Some packets encode rotation as x/y/yaw values directly
if (packet.rotation_x !== undefined)
entity.rotation.x = packet.rotation_x;
if (packet.rotation_y !== undefined)
entity.rotation.y = packet.rotation_y;
if (packet.rotation_y_head !== undefined)
entity.rotation.z = packet.rotation_y_head;
}
entity.lastUpdateTime = performance.now();
entity.interpolationAlpha = 0;
}
/**
* Handle move_player packet.
*/
handleMovePlayer(packet) {
const runtimeId = packet.runtime_id ?? packet.runtime_entity_id;
if (runtimeId === undefined)
return;
if (runtimeId === this._localPlayerRuntimeId)
return; // Don't apply to local player
const entity = this._entities.get(runtimeId);
if (!entity)
return;
entity.prevPosition = { ...entity.position };
entity.prevRotation = { ...entity.rotation };
const pos = packet.position;
if (pos) {
entity.position = this._extractPosition(pos);
}
if (packet.rotation) {
entity.rotation = this._extractRotation(packet.rotation);
}
else {
if (packet.pitch !== undefined)
entity.rotation.x = packet.pitch;
if (packet.yaw !== undefined)
entity.rotation.y = packet.yaw;
if (packet.head_yaw !== undefined)
entity.rotation.z = packet.head_yaw;
}
entity.lastUpdateTime = performance.now();
entity.interpolationAlpha = 0;
}
/**
* Handle set_actor_motion packet.
*/
handleSetActorMotion(packet) {
const runtimeId = packet.runtime_entity_id ?? packet.runtime_id;
if (runtimeId === undefined)
return;
const entity = this._entities.get(runtimeId);
if (!entity)
return;
const vel = packet.velocity;
if (vel) {
entity.velocity = this._extractPosition(vel);
}
}
/**
* Handle set_actor_data packet (entity metadata flags).
*/
handleSetActorData(packet) {
const runtimeId = packet.runtime_entity_id ?? packet.runtime_id;
if (runtimeId === undefined)
return;
const entity = this._entities.get(runtimeId);
if (!entity)
return;
if (packet.metadata) {
entity.metadata = { ...entity.metadata, ...packet.metadata };
}
}
/**
* Handle move_entity_delta packet (incremental entity movement).
* This is the most frequent entity movement packet — applies deltas to current position/rotation.
*/
handleMoveEntityDelta(packet) {
const runtimeId = packet.runtime_entity_id ?? packet.runtime_id;
if (runtimeId === undefined)
return;
const entity = this._entities.get(runtimeId);
if (!entity)
return;
if (runtimeId === this._localPlayerRuntimeId)
return;
entity.prevPosition = { ...entity.position };
entity.prevRotation = { ...entity.rotation };
// move_entity_delta can contain absolute or delta values depending on flags
if (packet.x !== undefined)
entity.position.x = packet.x;
if (packet.y !== undefined)
entity.position.y = packet.y;
if (packet.z !== undefined)
entity.position.z = packet.z;
if (packet.rot_x !== undefined)
entity.rotation.x = packet.rot_x;
if (packet.rot_y !== undefined)
entity.rotation.y = packet.rot_y;
if (packet.rot_y_head !== undefined)
entity.rotation.z = packet.rot_y_head;
entity.lastUpdateTime = performance.now();
entity.interpolationAlpha = 0;
}
/**
* Update interpolation for all entities. Call every render frame.
* @param deltaMs Milliseconds since last frame
*/
updateInterpolation(deltaMs) {
const interpSpeed = deltaMs / 50; // 50ms = 1 server tick
for (const entity of this._entities.values()) {
if (entity.interpolationAlpha < 1) {
entity.interpolationAlpha = Math.min(1, entity.interpolationAlpha + interpSpeed);
}
}
}
/**
* Get interpolated position for an entity.
*/
getInterpolatedPosition(runtimeId) {
const entity = this._entities.get(runtimeId);
if (!entity)
return undefined;
const t = entity.interpolationAlpha;
return {
x: entity.prevPosition.x + (entity.position.x - entity.prevPosition.x) * t,
y: entity.prevPosition.y + (entity.position.y - entity.prevPosition.y) * t,
z: entity.prevPosition.z + (entity.position.z - entity.prevPosition.z) * t,
};
}
/**
* Get interpolated rotation for an entity.
*/
getInterpolatedRotation(runtimeId) {
const entity = this._entities.get(runtimeId);
if (!entity)
return undefined;
const t = entity.interpolationAlpha;
return {
x: this._lerpAngle(entity.prevRotation.x, entity.rotation.x, t),
y: this._lerpAngle(entity.prevRotation.y, entity.rotation.y, t),
z: this._lerpAngle(entity.prevRotation.z, entity.rotation.z, t),
};
}
/**
* Get all entities as an array.
*/
getAllEntities() {
return Array.from(this._entities.values());
}
/**
* Clear all entities (dimension change).
*/
clear() {
this._entities.clear();
this._playerEntities.clear();
}
_extractPosition(obj) {
return {
x: obj?.x ?? obj?.X ?? 0,
y: obj?.y ?? obj?.Y ?? 0,
z: obj?.z ?? obj?.Z ?? 0,
};
}
_extractRotation(obj) {
return {
x: obj?.x ?? obj?.pitch ?? 0,
y: obj?.y ?? obj?.yaw ?? 0,
z: obj?.z ?? obj?.head_yaw ?? 0,
};
}
_lerpAngle(a, b, t) {
let diff = b - a;
while (diff > 180)
diff -= 360;
while (diff < -180)
diff += 360;
return a + diff * t;
}
}
exports.default = EntityManager;