UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

346 lines (345 loc) 12.8 kB
"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;