2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* @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.
|
2025-12-19 05:19:22 +00:00
|
|
|
* Updated to support Camera Focus Targeting, Emissive Textures, and Multi-Material Voxels.
|
2025-12-22 20:55:41 +00:00
|
|
|
* @class
|
2025-12-16 23:52:58 +00:00
|
|
|
*/
|
|
|
|
|
export class VoxelManager {
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* @param {VoxelGrid} grid - Voxel grid to render
|
|
|
|
|
* @param {THREE.Scene} scene - Three.js scene
|
|
|
|
|
*/
|
2025-12-19 05:19:22 +00:00
|
|
|
constructor(grid, scene) {
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {VoxelGrid} */
|
2025-12-16 23:52:58 +00:00
|
|
|
this.grid = grid;
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {THREE.Scene} */
|
2025-12-16 23:52:58 +00:00
|
|
|
this.scene = scene;
|
2025-12-19 05:19:22 +00:00
|
|
|
|
|
|
|
|
// Map of Voxel ID -> InstancedMesh
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {Map<number, THREE.InstancedMesh>} */
|
2025-12-19 05:19:22 +00:00
|
|
|
this.meshes = new Map();
|
|
|
|
|
|
|
|
|
|
// Default Material Definitions (Fallback)
|
|
|
|
|
// Store actual Material instances, not just configs
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {Record<number, THREE.MeshStandardMaterial | THREE.MeshStandardMaterial[]>} */
|
2025-12-19 05:19:22 +00:00
|
|
|
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
|
2026-01-14 19:11:22 +00:00
|
|
|
20: new THREE.MeshStandardMaterial({
|
|
|
|
|
color: 0x0088ff,
|
|
|
|
|
emissive: 0x002244,
|
|
|
|
|
transparent: true,
|
|
|
|
|
opacity: 0.8,
|
|
|
|
|
}), // Hard Light Bridge
|
|
|
|
|
22: new THREE.MeshStandardMaterial({
|
|
|
|
|
color: 0xff00ff,
|
|
|
|
|
emissive: 0x440044,
|
|
|
|
|
}), // Teleporter Node
|
2025-12-17 19:26:42 +00:00
|
|
|
};
|
2025-12-19 05:19:22 +00:00
|
|
|
|
|
|
|
|
// Shared Geometry
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {THREE.BoxGeometry} */
|
2025-12-19 05:19:22 +00:00
|
|
|
this.geometry = new THREE.BoxGeometry(1, 1, 1);
|
|
|
|
|
|
|
|
|
|
// Camera Anchor: Invisible object to serve as OrbitControls target
|
2025-12-22 20:55:41 +00:00
|
|
|
/** @type {THREE.Object3D} */
|
2025-12-19 05:19:22 +00:00
|
|
|
this.focusTarget = new THREE.Object3D();
|
|
|
|
|
this.focusTarget.name = "CameraFocusTarget";
|
|
|
|
|
this.scene.add(this.focusTarget);
|
2025-12-24 05:01:54 +00:00
|
|
|
|
|
|
|
|
// 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
|
|
|
/**
|
2025-12-19 05:19:22 +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.
|
2025-12-22 20:55:41 +00:00
|
|
|
* @param {GeneratedAssets} assets - Generated assets from world generator
|
2025-12-17 19:26:42 +00:00
|
|
|
*/
|
2025-12-19 05:19:22 +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
|
|
|
|
2025-12-19 05:19:22 +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
|
|
|
|
2025-12-19 05:19:22 +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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-12-19 05:19:22 +00:00
|
|
|
* Re-calculates meshes based on grid data.
|
2025-12-16 23:52:58 +00:00
|
|
|
*/
|
|
|
|
|
update() {
|
2025-12-19 05:19:22 +00:00
|
|
|
// 1. Cleanup existing meshes
|
|
|
|
|
this.meshes.forEach((mesh) => {
|
|
|
|
|
this.scene.remove(mesh);
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-19 05:19:22 +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
|
|
|
|
2025-12-28 01:21:31 +00:00
|
|
|
// Note: Mesh objects don't have a dispose() method, only geometry and material do
|
2025-12-19 05:19:22 +00:00
|
|
|
});
|
|
|
|
|
this.meshes.clear();
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-19 05:19:22 +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
|
|
|
|
2025-12-19 05:19:22 +00:00
|
|
|
if (id !== 0) {
|
|
|
|
|
if (!buckets[id]) buckets[id] = [];
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-19 05:19:22 +00:00
|
|
|
dummy.position.set(x, y, z);
|
2025-12-17 19:26:42 +00:00
|
|
|
dummy.updateMatrix();
|
2025-12-19 05:19:22 +00:00
|
|
|
buckets[id].push(dummy.matrix.clone());
|
2025-12-16 23:52:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 05:19:22 +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
|
|
|
/**
|
2025-12-19 05:19:22 +00:00
|
|
|
* Helper to center the camera view on the grid.
|
2025-12-22 20:55:41 +00:00
|
|
|
* @param {import("three/examples/jsm/controls/OrbitControls.js").OrbitControls} controls - The OrbitControls instance to update
|
2025-12-17 19:26:42 +00:00
|
|
|
*/
|
2025-12-19 05:19:22 +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-19 05:19:22 +00:00
|
|
|
}
|
2025-12-16 23:52:58 +00:00
|
|
|
|
2025-12-22 20:55:41 +00:00
|
|
|
/**
|
|
|
|
|
* Updates a single voxel (triggers full update).
|
|
|
|
|
* @param {number} x - X coordinate
|
|
|
|
|
* @param {number} y - Y coordinate
|
|
|
|
|
* @param {number} z - Z coordinate
|
|
|
|
|
*/
|
2025-12-19 05:19:22 +00:00
|
|
|
updateVoxel(x, y, z) {
|
|
|
|
|
this.update();
|
2025-12-16 23:52:58 +00:00
|
|
|
}
|
2025-12-24 05:01:54 +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();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-28 01:21:31 +00:00
|
|
|
// Note: Mesh objects don't have a dispose() method, only geometry and material do
|
2025-12-24 05:01:54 +00:00
|
|
|
});
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-28 01:21:31 +00:00
|
|
|
// Note: Mesh objects don't have a dispose() method, only geometry and material do
|
2025-12-24 05:01:54 +00:00
|
|
|
});
|
|
|
|
|
this.aoeReticle.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 04:50:11 +00:00
|
|
|
/**
|
|
|
|
|
* 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
|
2026-01-03 04:43:28 +00:00
|
|
|
|
2025-12-31 04:50:11 +00:00
|
|
|
// 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);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 05:01:54 +00:00
|
|
|
/**
|
|
|
|
|
* Clears all highlights (range and AoE).
|
|
|
|
|
*/
|
|
|
|
|
clearHighlights() {
|
|
|
|
|
this.clearRangeHighlights();
|
|
|
|
|
this.clearReticle();
|
|
|
|
|
}
|
2026-01-03 04:43:28 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clears all meshes and disposes of geometry/materials.
|
|
|
|
|
*/
|
|
|
|
|
clear() {
|
|
|
|
|
// Clear instanced meshes
|
|
|
|
|
this.meshes.forEach((mesh) => {
|
|
|
|
|
this.scene.remove(mesh);
|
|
|
|
|
if (mesh.geometry) mesh.geometry.dispose();
|
|
|
|
|
// Materials are shared, handled below
|
|
|
|
|
});
|
|
|
|
|
this.meshes.clear();
|
|
|
|
|
|
|
|
|
|
// Clear highlights
|
|
|
|
|
this.clearHighlights();
|
|
|
|
|
|
|
|
|
|
// Dispose of materials
|
|
|
|
|
Object.values(this.materials).forEach((material) => {
|
|
|
|
|
if (Array.isArray(material)) {
|
|
|
|
|
material.forEach((m) => {
|
|
|
|
|
if (m.map) m.map.dispose();
|
|
|
|
|
if (m.emissiveMap) m.emissiveMap.dispose();
|
|
|
|
|
if (m.normalMap) m.normalMap.dispose();
|
|
|
|
|
if (m.roughnessMap) m.roughnessMap.dispose();
|
|
|
|
|
if (m.bumpMap) m.bumpMap.dispose();
|
|
|
|
|
m.dispose();
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
if (material.map) material.map.dispose();
|
|
|
|
|
if (material.emissiveMap) material.emissiveMap.dispose();
|
|
|
|
|
if (material.normalMap) material.normalMap.dispose();
|
|
|
|
|
if (material.roughnessMap) material.roughnessMap.dispose();
|
|
|
|
|
if (material.bumpMap) material.bumpMap.dispose();
|
|
|
|
|
material.dispose();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Clear references
|
|
|
|
|
this.materials = {};
|
|
|
|
|
}
|
2025-12-16 23:52:58 +00:00
|
|
|
}
|