aether-shards/src/grid/VoxelManager.js

757 lines
25 KiB
JavaScript
Raw Normal View History

/**
* @typedef {import("./types.js").GeneratedAssets} GeneratedAssets
* @typedef {import("./VoxelGrid.js").VoxelGrid} VoxelGrid
*/
2025-12-16 23:52:58 +00:00
import * as THREE from "three";
/**
* VoxelManager.js
2025-12-17 19:26:42 +00:00
* Handles the Three.js rendering of the VoxelGrid data.
* Updated to support Camera Focus Targeting, Emissive Textures, and Multi-Material Voxels.
* @class
2025-12-16 23:52:58 +00:00
*/
export class VoxelManager {
/**
* @param {VoxelGrid} grid - Voxel grid to render
* @param {THREE.Scene} scene - Three.js scene
*/
constructor(grid, scene) {
/** @type {VoxelGrid} */
2025-12-16 23:52:58 +00:00
this.grid = grid;
/** @type {THREE.Scene} */
2025-12-16 23:52:58 +00:00
this.scene = scene;
// Map of Voxel ID -> InstancedMesh
/** @type {Map<number, THREE.InstancedMesh>} */
this.meshes = new Map();
// Default Material Definitions (Fallback)
// Store actual Material instances, not just configs
/** @type {Record<number, THREE.MeshStandardMaterial | THREE.MeshStandardMaterial[]>} */
this.materials = {
1: new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.8 }), // Stone
2: new THREE.MeshStandardMaterial({ color: 0x3d2817, roughness: 1.0 }), // Dirt/Floor Base
10: new THREE.MeshStandardMaterial({ color: 0x8b4513 }), // Wood (Destructible)
15: new THREE.MeshStandardMaterial({
color: 0x00ffff,
emissive: 0x004444,
}), // Crystal
2025-12-17 19:26:42 +00:00
};
// Shared Geometry
/** @type {THREE.BoxGeometry} */
this.geometry = new THREE.BoxGeometry(1, 1, 1);
// Camera Anchor: Invisible object to serve as OrbitControls target
/** @type {THREE.Object3D} */
this.focusTarget = new THREE.Object3D();
this.focusTarget.name = "CameraFocusTarget";
this.scene.add(this.focusTarget);
// Highlight tracking (managed externally, but VoxelManager provides helper methods)
// These will be Set<THREE.Mesh> passed from GameLoop
/** @type {Set<THREE.Mesh> | null} */
this.rangeHighlights = null;
/** @type {Set<THREE.Mesh> | null} */
this.aoeReticle = null;
2025-12-16 23:52:58 +00:00
}
2025-12-17 19:26:42 +00:00
/**
* Updates the material definitions with generated assets.
* Supports both simple Canvas textures and complex {diffuse, emissive, normal, roughness, bump} objects.
* NOW SUPPORTS: 'palette' for batch loading procedural variations.
* @param {GeneratedAssets} assets - Generated assets from world generator
2025-12-17 19:26:42 +00:00
*/
updateMaterials(assets) {
if (!assets) return;
// Helper to create a material INSTANCE from an asset
const createMaterial = (asset) => {
if (!asset) return null;
const matDef = {
color: 0xffffff, // White base to show texture colors
roughness: 0.8,
};
// Check if it's a simple Canvas or a Composite Object
if (
asset instanceof OffscreenCanvas ||
asset instanceof HTMLCanvasElement
) {
// Simple Diffuse Map
const tex = new THREE.CanvasTexture(asset);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
tex.colorSpace = THREE.SRGBColorSpace;
matDef.map = tex;
} else if (asset.diffuse) {
// Complex Map (Diffuse + optional maps)
const diffTex = new THREE.CanvasTexture(asset.diffuse);
diffTex.magFilter = THREE.NearestFilter;
diffTex.minFilter = THREE.NearestFilter;
diffTex.colorSpace = THREE.SRGBColorSpace;
matDef.map = diffTex;
// Emissive Map
if (asset.emissive) {
const emTex = new THREE.CanvasTexture(asset.emissive);
emTex.magFilter = THREE.NearestFilter;
emTex.minFilter = THREE.NearestFilter;
emTex.colorSpace = THREE.SRGBColorSpace;
matDef.emissiveMap = emTex;
matDef.emissive = 0xffffff; // Max brightness for the map values
matDef.emissiveIntensity = 1.0; // Adjustable glow strength
}
// Normal Map
if (asset.normal) {
const normTex = new THREE.CanvasTexture(asset.normal);
normTex.magFilter = THREE.NearestFilter;
normTex.minFilter = THREE.NearestFilter;
normTex.colorSpace = THREE.NoColorSpace; // Normal maps are data, linear color space
matDef.normalMap = normTex;
}
// Roughness Map
if (asset.roughness) {
const roughTex = new THREE.CanvasTexture(asset.roughness);
roughTex.magFilter = THREE.NearestFilter;
roughTex.minFilter = THREE.NearestFilter;
roughTex.colorSpace = THREE.NoColorSpace;
matDef.roughnessMap = roughTex;
matDef.roughness = 1.0; // Let the map drive the value
}
// Bump Map
if (asset.bump) {
const bumpTex = new THREE.CanvasTexture(asset.bump);
bumpTex.magFilter = THREE.NearestFilter;
bumpTex.minFilter = THREE.NearestFilter;
bumpTex.colorSpace = THREE.NoColorSpace;
matDef.bumpMap = bumpTex;
matDef.bumpScale = 0.05; // Standard bump scale for voxels
}
}
return new THREE.MeshStandardMaterial(matDef);
};
// 1. Process Standard Single Textures (Legacy/Base)
const floorMat = assets.textures
? createMaterial(assets.textures["floor"])
: null;
const wallMat = assets.textures
? createMaterial(assets.textures["wall"])
: null;
// ID 1: Wall (All faces same)
if (wallMat) this.materials[1] = wallMat;
// ID 2: Floor (Top = Floor Tex, Sides/Bottom = Wall Tex)
if (floorMat) {
// Use wall texture for sides if available.
// If not, fallback to the existing definition for ID 1 (Stone) to ensure sides look like walls.
let sideMat = wallMat;
if (!sideMat && this.materials[1] && !Array.isArray(this.materials[1])) {
sideMat = this.materials[1];
}
// Fallback to floor material if absolutely nothing else exists
if (!sideMat) sideMat = floorMat;
// BoxGeometry Material Index Order: Right, Left, Top, Bottom, Front, Back
this.materials[2] = [
sideMat, // Right (+x)
sideMat, // Left (-x)
floorMat, // Top (+y) - The Damp Cave Floor
sideMat, // Bottom (-y)
sideMat, // Front (+z)
sideMat, // Back (-z)
];
2025-12-17 19:26:42 +00:00
}
2025-12-16 23:52:58 +00:00
// 2. Process Palette (Procedural Variations)
if (assets.palette) {
// First pass: Create all materials from palette assets
for (const [idStr, asset] of Object.entries(assets.palette)) {
const id = parseInt(idStr);
const mat = createMaterial(asset);
if (mat) {
this.materials[id] = mat;
}
}
// Second pass: Organize multi-material arrays for floors
for (const [idStr, asset] of Object.entries(assets.palette)) {
const id = parseInt(idStr);
// Logic for Floor Variations (IDs 200-299)
if (id >= 200 && id <= 299) {
const floorMat = this.materials[id];
2025-12-16 23:52:58 +00:00
// Attempt to find matching wall variation (e.g. Floor 205 -> Wall 105)
// If missing, fallback to Base Wall (ID 1)
let sideMat = this.materials[id - 100];
if (
!sideMat &&
this.materials[1] &&
!Array.isArray(this.materials[1])
) {
sideMat = this.materials[1];
}
// Fallback to floor material itself if absolutely nothing else exists
if (!sideMat) sideMat = floorMat;
if (sideMat && floorMat) {
this.materials[id] = [
sideMat,
sideMat,
floorMat,
sideMat,
sideMat,
sideMat,
];
}
}
}
}
2025-12-16 23:52:58 +00:00
2025-12-17 19:26:42 +00:00
this.update();
2025-12-16 23:52:58 +00:00
}
/**
* Re-calculates meshes based on grid data.
2025-12-16 23:52:58 +00:00
*/
update() {
// 1. Cleanup existing meshes
this.meshes.forEach((mesh) => {
this.scene.remove(mesh);
2025-12-16 23:52:58 +00:00
// Handle cleanup for single material or array of materials
if (Array.isArray(mesh.material)) {
mesh.material.forEach((m) => {
// Dispose textures if they are unique to this mesh instance
// (But here we share them via this.materials, so be careful not to over-dispose if reused)
// For now, assume simple cleanup
// if (m.map) m.map.dispose();
// m.dispose();
});
} else {
// if (mesh.material.map) mesh.material.map.dispose();
// mesh.material.dispose();
}
2025-12-16 23:52:58 +00:00
// Note: Mesh objects don't have a dispose() method, only geometry and material do
});
this.meshes.clear();
2025-12-16 23:52:58 +00:00
// 2. Bucket coordinates by Voxel ID
const buckets = {};
const dummy = new THREE.Object3D();
for (let x = 0; x < this.grid.size.x; x++) {
for (let y = 0; y < this.grid.size.y; y++) {
for (let z = 0; z < this.grid.size.z; z++) {
const id = this.grid.getCell(x, y, z);
2025-12-16 23:52:58 +00:00
if (id !== 0) {
if (!buckets[id]) buckets[id] = [];
2025-12-16 23:52:58 +00:00
dummy.position.set(x, y, z);
2025-12-17 19:26:42 +00:00
dummy.updateMatrix();
buckets[id].push(dummy.matrix.clone());
2025-12-16 23:52:58 +00:00
}
}
}
}
// 3. Generate InstancedMesh for each ID
for (const [idStr, matrices] of Object.entries(buckets)) {
const id = parseInt(idStr);
const count = matrices.length;
// Get the actual Material Instance (or Array of Instances)
const material =
this.materials[id] ||
new THREE.MeshStandardMaterial({ color: 0xff00ff });
const mesh = new THREE.InstancedMesh(this.geometry, material, count);
matrices.forEach((mat, index) => {
mesh.setMatrixAt(index, mat);
});
mesh.instanceMatrix.needsUpdate = true;
this.meshes.set(id, mesh);
this.scene.add(mesh);
}
// 4. Update Focus Target to Center of Grid
// We position it at the horizontal center, and slightly up (y=0 or 1)
this.focusTarget.position.set(
this.grid.size.x / 2,
0,
this.grid.size.z / 2
);
2025-12-17 19:26:42 +00:00
}
2025-12-16 23:52:58 +00:00
2025-12-17 19:26:42 +00:00
/**
* Helper to center the camera view on the grid.
* @param {import("three/examples/jsm/controls/OrbitControls.js").OrbitControls} controls - The OrbitControls instance to update
2025-12-17 19:26:42 +00:00
*/
focusCamera(controls) {
if (controls && this.focusTarget) {
controls.target.copy(this.focusTarget.position);
controls.update();
2025-12-16 23:52:58 +00:00
}
}
2025-12-16 23:52:58 +00:00
/**
* Updates a single voxel (triggers full update).
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @param {number} z - Z coordinate
*/
updateVoxel(x, y, z) {
this.update();
2025-12-16 23:52:58 +00:00
}
/**
* Sets the highlight tracking sets (called from GameLoop).
* @param {Set<THREE.Mesh>} rangeHighlights - Set to track range highlight meshes
* @param {Set<THREE.Mesh>} aoeReticle - Set to track AoE reticle meshes
*/
setHighlightSets(rangeHighlights, aoeReticle) {
this.rangeHighlights = rangeHighlights;
this.aoeReticle = aoeReticle;
}
/**
* Highlights tiles within skill range (red outline).
* @param {import("./types.js").Position} sourcePos - Source position
* @param {number} range - Skill range in tiles
* @param {string} style - Highlight style (e.g., 'RED_OUTLINE')
*/
highlightRange(sourcePos, range, style = "RED_OUTLINE") {
if (!this.rangeHighlights) return;
// Clear existing range highlights
this.clearRangeHighlights();
// Generate all tiles within Manhattan distance range
const tiles = [];
for (let x = sourcePos.x - range; x <= sourcePos.x + range; x++) {
for (let y = sourcePos.y - range; y <= sourcePos.y + range; y++) {
for (let z = sourcePos.z - range; z <= sourcePos.z + range; z++) {
const dist =
Math.abs(x - sourcePos.x) +
Math.abs(y - sourcePos.y) +
Math.abs(z - sourcePos.z);
if (dist <= range) {
// Check if position is valid and walkable
if (this.grid.isValidBounds({ x, y, z })) {
tiles.push({ x, y, z });
}
}
}
}
}
// Create red outline materials (similar to movement highlights but red)
const outerGlowMaterial = new THREE.LineBasicMaterial({
color: 0x660000,
transparent: true,
opacity: 0.3,
});
const midGlowMaterial = new THREE.LineBasicMaterial({
color: 0x880000,
transparent: true,
opacity: 0.5,
});
const highlightMaterial = new THREE.LineBasicMaterial({
color: 0xff0000, // Bright red
transparent: true,
opacity: 1.0,
});
const thickMaterial = new THREE.LineBasicMaterial({
color: 0xcc0000,
transparent: true,
opacity: 0.8,
});
// Create base plane geometry
const baseGeometry = new THREE.PlaneGeometry(1, 1);
baseGeometry.rotateX(-Math.PI / 2);
// Create highlights for each tile
tiles.forEach((pos) => {
// Find walkable Y level (similar to movement highlights)
let walkableY = pos.y;
// Check if there's a floor at this position
if (this.grid.getCell(pos.x, pos.y - 1, pos.z) === 0) {
// No floor, try to find walkable level
for (let checkY = pos.y; checkY >= 0; checkY--) {
if (this.grid.getCell(pos.x, checkY - 1, pos.z) !== 0) {
walkableY = checkY;
break;
}
}
}
const floorSurfaceY = walkableY - 0.5;
// Outer glow
const outerGlowGeometry = new THREE.PlaneGeometry(1.15, 1.15);
outerGlowGeometry.rotateX(-Math.PI / 2);
const outerGlowEdges = new THREE.EdgesGeometry(outerGlowGeometry);
const outerGlowLines = new THREE.LineSegments(
outerGlowEdges,
outerGlowMaterial
);
outerGlowLines.position.set(pos.x, floorSurfaceY + 0.003, pos.z);
this.scene.add(outerGlowLines);
this.rangeHighlights.add(outerGlowLines);
// Mid glow
const midGlowGeometry = new THREE.PlaneGeometry(1.08, 1.08);
midGlowGeometry.rotateX(-Math.PI / 2);
const midGlowEdges = new THREE.EdgesGeometry(midGlowGeometry);
const midGlowLines = new THREE.LineSegments(
midGlowEdges,
midGlowMaterial
);
midGlowLines.position.set(pos.x, floorSurfaceY + 0.002, pos.z);
this.scene.add(midGlowLines);
this.rangeHighlights.add(midGlowLines);
// Thick outline
const thickGeometry = new THREE.PlaneGeometry(1.02, 1.02);
thickGeometry.rotateX(-Math.PI / 2);
const thickEdges = new THREE.EdgesGeometry(thickGeometry);
const thickLines = new THREE.LineSegments(thickEdges, thickMaterial);
thickLines.position.set(pos.x, floorSurfaceY + 0.001, pos.z);
this.scene.add(thickLines);
this.rangeHighlights.add(thickLines);
// Main bright outline
const edgesGeometry = new THREE.EdgesGeometry(baseGeometry);
const lineSegments = new THREE.LineSegments(
edgesGeometry,
highlightMaterial
);
lineSegments.position.set(pos.x, floorSurfaceY, pos.z);
this.scene.add(lineSegments);
this.rangeHighlights.add(lineSegments);
});
}
/**
* Shows AoE reticle (solid red highlight) for affected tiles.
* @param {import("./types.js").Position[]} tiles - Array of tile positions in AoE
*/
showReticle(tiles) {
if (!this.aoeReticle) return;
// Clear existing reticle
this.clearReticle();
// Create solid red material (more opaque than outline)
const reticleMaterial = new THREE.MeshStandardMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.4,
emissive: 0x330000,
emissiveIntensity: 0.5,
});
// Create plane geometry for solid highlight
const planeGeometry = new THREE.PlaneGeometry(1, 1);
planeGeometry.rotateX(-Math.PI / 2);
// Create solid highlights for each tile
tiles.forEach((pos) => {
// Find walkable Y level
let walkableY = pos.y;
if (this.grid.getCell(pos.x, pos.y - 1, pos.z) === 0) {
for (let checkY = pos.y; checkY >= 0; checkY--) {
if (this.grid.getCell(pos.x, checkY - 1, pos.z) !== 0) {
walkableY = checkY;
break;
}
}
}
const floorSurfaceY = walkableY - 0.5;
// Create solid plane mesh
const plane = new THREE.Mesh(planeGeometry, reticleMaterial);
plane.position.set(pos.x, floorSurfaceY + 0.01, pos.z);
this.scene.add(plane);
this.aoeReticle.add(plane);
});
}
/**
* Clears all range highlights.
*/
clearRangeHighlights() {
if (!this.rangeHighlights) return;
this.rangeHighlights.forEach((mesh) => {
this.scene.remove(mesh);
if (mesh.geometry) mesh.geometry.dispose();
if (mesh.material) {
if (Array.isArray(mesh.material)) {
mesh.material.forEach((m) => m.dispose());
} else {
mesh.material.dispose();
}
}
// Note: Mesh objects don't have a dispose() method, only geometry and material do
});
this.rangeHighlights.clear();
}
/**
* Clears AoE reticle.
*/
clearReticle() {
if (!this.aoeReticle) return;
this.aoeReticle.forEach((mesh) => {
this.scene.remove(mesh);
if (mesh.geometry) mesh.geometry.dispose();
if (mesh.material) {
if (Array.isArray(mesh.material)) {
mesh.material.forEach((m) => m.dispose());
} else {
mesh.material.dispose();
}
}
// Note: Mesh objects don't have a dispose() method, only geometry and material do
});
this.aoeReticle.clear();
}
/**
* Highlights specific tiles with obstruction-based dimming.
* @param {Array<{pos: import("./types.js").Position, obstruction: number}>} tilesWithObstruction - Array of tile positions with obstruction levels (0-1)
* @param {string} style - Highlight style (e.g., 'RED_OUTLINE')
*/
highlightTilesWithObstruction(tilesWithObstruction, style = "RED_OUTLINE") {
if (!this.rangeHighlights) return;
// Clear existing range highlights
this.clearRangeHighlights();
// Create highlights for each tile with obstruction-based opacity
tilesWithObstruction.forEach(({ pos, obstruction }) => {
// Calculate opacity based on obstruction (0 obstruction = full brightness, 1 obstruction = dim)
// Success chance = 1 - obstruction, so opacity should reflect that
const baseOpacity = 1.0;
const dimmedOpacity = baseOpacity * (1 - obstruction * 0.7); // Dim up to 70% based on obstruction
// Create materials with obstruction-based opacity
const outerGlowMaterial = new THREE.LineBasicMaterial({
color: 0x660000,
transparent: true,
opacity: 0.3 * dimmedOpacity,
});
const midGlowMaterial = new THREE.LineBasicMaterial({
color: 0x880000,
transparent: true,
opacity: 0.5 * dimmedOpacity,
});
const highlightMaterial = new THREE.LineBasicMaterial({
color: 0xff0000, // Bright red
transparent: true,
opacity: dimmedOpacity,
});
const thickMaterial = new THREE.LineBasicMaterial({
color: 0xcc0000,
transparent: true,
opacity: 0.8 * dimmedOpacity,
});
// Find walkable Y level (similar to movement highlights)
let walkableY = pos.y;
// Check if there's a floor at this position
if (this.grid.getCell(pos.x, pos.y - 1, pos.z) === 0) {
// No floor, try to find walkable level
for (let checkY = pos.y; checkY >= 0; checkY--) {
if (this.grid.getCell(pos.x, checkY - 1, pos.z) !== 0) {
walkableY = checkY;
break;
}
}
}
const floorSurfaceY = walkableY - 0.5;
// Outer glow
const outerGlowGeometry = new THREE.PlaneGeometry(1.15, 1.15);
outerGlowGeometry.rotateX(-Math.PI / 2);
const outerGlowEdges = new THREE.EdgesGeometry(outerGlowGeometry);
const outerGlowLines = new THREE.LineSegments(
outerGlowEdges,
outerGlowMaterial
);
outerGlowLines.position.set(pos.x, floorSurfaceY + 0.003, pos.z);
this.scene.add(outerGlowLines);
this.rangeHighlights.add(outerGlowLines);
// Mid glow
const midGlowGeometry = new THREE.PlaneGeometry(1.08, 1.08);
midGlowGeometry.rotateX(-Math.PI / 2);
const midGlowEdges = new THREE.EdgesGeometry(midGlowGeometry);
const midGlowLines = new THREE.LineSegments(
midGlowEdges,
midGlowMaterial
);
midGlowLines.position.set(pos.x, floorSurfaceY + 0.004, pos.z);
this.scene.add(midGlowLines);
this.rangeHighlights.add(midGlowLines);
// Main highlight
const baseGeometry = new THREE.PlaneGeometry(1, 1);
baseGeometry.rotateX(-Math.PI / 2);
const highlightEdges = new THREE.EdgesGeometry(baseGeometry);
const highlightLines = new THREE.LineSegments(
highlightEdges,
highlightMaterial
);
highlightLines.position.set(pos.x, floorSurfaceY + 0.005, pos.z);
this.scene.add(highlightLines);
this.rangeHighlights.add(highlightLines);
// Thick border
const thickGeometry = new THREE.PlaneGeometry(1.02, 1.02);
thickGeometry.rotateX(-Math.PI / 2);
const thickEdges = new THREE.EdgesGeometry(thickGeometry);
const thickLines = new THREE.LineSegments(thickEdges, thickMaterial);
thickLines.position.set(pos.x, floorSurfaceY + 0.006, pos.z);
this.scene.add(thickLines);
this.rangeHighlights.add(thickLines);
});
}
/**
* Highlights specific tiles (used for skill targeting with validation).
* @param {import("./types.js").Position[]} tiles - Array of tile positions to highlight
* @param {string} style - Highlight style (e.g., 'RED_OUTLINE')
*/
highlightTiles(tiles, style = "RED_OUTLINE") {
if (!this.rangeHighlights) return;
// Clear existing range highlights
this.clearRangeHighlights();
// Create red outline materials (similar to movement highlights but red)
const outerGlowMaterial = new THREE.LineBasicMaterial({
color: 0x660000,
transparent: true,
opacity: 0.3,
});
const midGlowMaterial = new THREE.LineBasicMaterial({
color: 0x880000,
transparent: true,
opacity: 0.5,
});
const highlightMaterial = new THREE.LineBasicMaterial({
color: 0xff0000, // Bright red
transparent: true,
opacity: 1.0,
});
const thickMaterial = new THREE.LineBasicMaterial({
color: 0xcc0000,
transparent: true,
opacity: 0.8,
});
// Create base plane geometry
const baseGeometry = new THREE.PlaneGeometry(1, 1);
baseGeometry.rotateX(-Math.PI / 2);
// Create highlights for each tile
tiles.forEach((pos) => {
// Find walkable Y level (similar to movement highlights)
let walkableY = pos.y;
// Check if there's a floor at this position
if (this.grid.getCell(pos.x, pos.y - 1, pos.z) === 0) {
// No floor, try to find walkable level
for (let checkY = pos.y; checkY >= 0; checkY--) {
if (this.grid.getCell(pos.x, checkY - 1, pos.z) !== 0) {
walkableY = checkY;
break;
}
}
}
const floorSurfaceY = walkableY - 0.5;
// Outer glow
const outerGlowGeometry = new THREE.PlaneGeometry(1.15, 1.15);
outerGlowGeometry.rotateX(-Math.PI / 2);
const outerGlowEdges = new THREE.EdgesGeometry(outerGlowGeometry);
const outerGlowLines = new THREE.LineSegments(
outerGlowEdges,
outerGlowMaterial
);
outerGlowLines.position.set(pos.x, floorSurfaceY + 0.003, pos.z);
this.scene.add(outerGlowLines);
this.rangeHighlights.add(outerGlowLines);
// Mid glow
const midGlowGeometry = new THREE.PlaneGeometry(1.08, 1.08);
midGlowGeometry.rotateX(-Math.PI / 2);
const midGlowEdges = new THREE.EdgesGeometry(midGlowGeometry);
const midGlowLines = new THREE.LineSegments(
midGlowEdges,
midGlowMaterial
);
midGlowLines.position.set(pos.x, floorSurfaceY + 0.004, pos.z);
this.scene.add(midGlowLines);
this.rangeHighlights.add(midGlowLines);
// Main highlight
const highlightEdges = new THREE.EdgesGeometry(baseGeometry);
const highlightLines = new THREE.LineSegments(
highlightEdges,
highlightMaterial
);
highlightLines.position.set(pos.x, floorSurfaceY + 0.005, pos.z);
this.scene.add(highlightLines);
this.rangeHighlights.add(highlightLines);
// Thick border
const thickGeometry = new THREE.PlaneGeometry(1.02, 1.02);
thickGeometry.rotateX(-Math.PI / 2);
const thickEdges = new THREE.EdgesGeometry(thickGeometry);
const thickLines = new THREE.LineSegments(thickEdges, thickMaterial);
thickLines.position.set(pos.x, floorSurfaceY + 0.006, pos.z);
this.scene.add(thickLines);
this.rangeHighlights.add(thickLines);
});
}
/**
* Clears all highlights (range and AoE).
*/
clearHighlights() {
this.clearRangeHighlights();
this.clearReticle();
}
2025-12-16 23:52:58 +00:00
}