Files
WarhammerQuest/src/view/render/InteractionRenderer.js

398 lines
14 KiB
JavaScript

import * as THREE from 'three';
export class InteractionRenderer {
constructor(scene, renderer, camera, interactionPlane, getTextureCallback) {
this.scene = scene;
this.renderer = renderer;
this.camera = camera;
this.interactionPlane = interactionPlane;
this.getTexture = getTextureCallback;
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.highlightGroup = new THREE.Group();
this.scene.add(this.highlightGroup);
this.previewGroup = new THREE.Group();
this.scene.add(this.previewGroup);
this.projectionGroup = new THREE.Group();
this.scene.add(this.projectionGroup);
this.spellPreviewGroup = new THREE.Group();
this.scene.add(this.spellPreviewGroup);
this.rangedGroup = new THREE.Group();
this.scene.add(this.rangedGroup);
this.exitHighlightGroup = new THREE.Group();
this.scene.add(this.exitHighlightGroup);
this.pathGroup = new THREE.Group();
this.scene.add(this.pathGroup);
}
setupInteraction(cameraGetter, onClick, onRightClick, onHover = null, getExitGroupCallback = null) {
const getMousePos = (event) => {
const rect = this.renderer.domElement.getBoundingClientRect();
return {
x: ((event.clientX - rect.left) / rect.width) * 2 - 1,
y: -((event.clientY - rect.top) / rect.height) * 2 + 1
};
};
const handleHover = (event) => {
if (!onHover) return;
this.mouse.set(getMousePos(event).x, getMousePos(event).y);
this.raycaster.setFromCamera(this.mouse, cameraGetter());
const intersects = this.raycaster.intersectObject(this.interactionPlane);
if (intersects.length > 0) {
const p = intersects[0].point;
const x = Math.round(p.x);
const y = Math.round(-p.z);
onHover(x, y);
}
};
this.renderer.domElement.addEventListener('mousemove', handleHover);
this.renderer.domElement.addEventListener('click', (event) => {
this.mouse.set(getMousePos(event).x, getMousePos(event).y);
this.raycaster.setFromCamera(this.mouse, cameraGetter());
// First, check if we clicked on a door mesh
if (getExitGroupCallback) {
const exitGroup = getExitGroupCallback();
if (exitGroup) {
const doorIntersects = this.raycaster.intersectObjects(exitGroup.children, false);
if (doorIntersects.length > 0) {
const doorMesh = doorIntersects[0].object;
// Only capture click if it is a door AND it is NOT open
if (doorMesh.userData.isDoor && !doorMesh.userData.isOpen) {
// Clicked on a CLOSED door! Call onClick with a special door object
onClick(null, null, doorMesh);
return;
}
}
}
}
// If no door clicked, proceed with normal cell click
const intersects = this.raycaster.intersectObject(this.interactionPlane);
if (intersects.length > 0) {
const p = intersects[0].point;
const x = Math.round(p.x);
const y = Math.round(-p.z);
onClick(x, y, null);
}
});
this.renderer.domElement.addEventListener('contextmenu', (event) => {
event.preventDefault();
this.mouse.set(getMousePos(event).x, getMousePos(event).y);
this.raycaster.setFromCamera(this.mouse, cameraGetter());
const intersects = this.raycaster.intersectObject(this.interactionPlane);
if (intersects.length > 0) {
const p = intersects[0].point;
const x = Math.round(p.x);
const y = Math.round(-p.z);
onRightClick(x, y);
}
});
}
highlightCells(cells) {
this.highlightGroup.clear();
if (!cells || cells.length === 0) return;
cells.forEach((cell, index) => {
// 1. Create Canvas with Number
const canvas = document.createElement('canvas');
canvas.width = 128;
canvas.height = 128;
const ctx = canvas.getContext('2d');
// Background
ctx.fillStyle = "rgba(255, 255, 0, 0.5)";
ctx.fillRect(0, 0, 128, 128);
// Border
ctx.strokeStyle = "rgba(255, 255, 0, 1)";
ctx.lineWidth = 4;
ctx.strokeRect(0, 0, 128, 128);
// Text (Step Number)
ctx.font = "bold 60px Arial";
ctx.fillStyle = "black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText((index + 1).toString(), 64, 64);
const texture = new THREE.CanvasTexture(canvas);
const geometry = new THREE.PlaneGeometry(0.9, 0.9);
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = -Math.PI / 2;
mesh.position.set(cell.x, 0.05, -cell.y);
this.highlightGroup.add(mesh);
});
}
showAreaPreview(cells, color = 0xffffff) {
this.spellPreviewGroup.clear();
if (!cells) return;
const geometry = new THREE.PlaneGeometry(0.9, 0.9);
const material = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.5,
side: THREE.DoubleSide
});
cells.forEach(cell => {
const mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = -Math.PI / 2;
mesh.position.set(cell.x, 0.06, -cell.y);
this.spellPreviewGroup.add(mesh);
});
}
hideAreaPreview() {
this.spellPreviewGroup.clear();
}
// ========== PATH VISUALIZATION (PRESERVED) ==========
updatePathVisualization(path) {
this.pathGroup.clear();
if (!path || path.length === 0) return;
path.forEach((step, index) => {
const geometry = new THREE.PlaneGeometry(0.8, 0.8);
const texture = this.createNumberTexture(index + 1);
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide
});
const plane = new THREE.Mesh(geometry, material);
plane.position.set(step.x, 0.02, -step.y);
plane.rotation.x = -Math.PI / 2;
plane.userData.stepIndex = index;
this.pathGroup.add(plane);
});
}
createNumberTexture(number) {
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
const ctx = canvas.getContext('2d');
// Yellow background with 50% opacity
ctx.fillStyle = 'rgba(255, 255, 0, 0.5)';
ctx.fillRect(0, 0, 64, 64);
// Border
ctx.strokeStyle = '#EDA900';
ctx.lineWidth = 4;
ctx.strokeRect(0, 0, 64, 64);
// Text
ctx.font = 'bold 36px Arial';
ctx.fillStyle = 'black';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(number.toString(), 32, 32);
const tex = new THREE.CanvasTexture(canvas);
return tex;
}
// Manual Placement
showPlacementPreview(preview) {
if (!preview) {
this.previewGroup.clear();
this.projectionGroup.clear();
return;
}
this.previewGroup.clear();
this.projectionGroup.clear();
const { card, cells, isValid, x, y, rotation } = preview;
// 1. FLOATING TILE (Y = 3)
if (card.textures && card.textures.length > 0) {
this.getTexture(card.textures[0], (texture) => {
const currentVariant = card.variants[rotation];
const rotWidth = currentVariant.width;
const rotHeight = currentVariant.height;
const cx = x + (rotWidth - 1) / 2;
const cy = y + (rotHeight - 1) / 2;
const baseWidth = card.variants.N.width;
const baseHeight = card.variants.N.height;
const geometry = new THREE.PlaneGeometry(baseWidth, baseHeight);
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide
});
const floatingTile = new THREE.Mesh(geometry, material);
floatingTile.rotation.x = -Math.PI / 2;
const rotMap = { 'N': 0, 'E': 1, 'S': 2, 'W': 3 };
const r = rotMap[rotation] !== undefined ? rotMap[rotation] : 0;
floatingTile.rotation.z = -r * (Math.PI / 2);
floatingTile.position.set(cx, 3, -cy);
this.previewGroup.add(floatingTile);
});
}
// 2. GROUND PROJECTION
const baseColor = isValid ? 0x00ff00 : 0xff0000;
const exitKeys = new Set();
if (preview.variant && preview.variant.exits) {
preview.variant.exits.forEach(ex => {
const gx = x + ex.x;
const gy = y + ex.y;
exitKeys.add(`${gx},${gy} `);
});
}
cells.forEach(cell => {
const key = `${cell.x},${cell.y} `;
let color = baseColor;
if (exitKeys.has(key)) {
color = 0x0000ff;
}
const geometry = new THREE.PlaneGeometry(0.95, 0.95);
const material = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.5,
side: THREE.DoubleSide
});
const projection = new THREE.Mesh(geometry, material);
projection.rotation.x = -Math.PI / 2;
projection.position.set(cell.x, 0.02, -cell.y);
this.projectionGroup.add(projection);
});
}
hidePlacementPreview() {
this.previewGroup.clear();
this.projectionGroup.clear();
}
showRangedTargeting(hero, monster, losResult) {
this.rangedGroup.clear();
if (!hero || !monster || !losResult) return;
// 1. Orange Fluorescence Ring on Monster
const ringGeo = new THREE.RingGeometry(0.35, 0.45, 32);
const ringMat = new THREE.MeshBasicMaterial({
color: 0xFFA500,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.8
});
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.rotation.x = -Math.PI / 2;
ring.position.set(monster.x, 0.05, -monster.y);
this.rangedGroup.add(ring);
// 2. Dashed Line logic
const points = [];
points.push(new THREE.Vector3(hero.x, 0.8, -hero.y));
points.push(new THREE.Vector3(monster.x, 0.8, -monster.y));
const lineGeo = new THREE.BufferGeometry().setFromPoints(points);
const lineMat = new THREE.LineDashedMaterial({
color: losResult.clear ? 0x00FF00 : 0xFF0000,
dashSize: 0.2,
gapSize: 0.1,
});
const line = new THREE.Line(lineGeo, lineMat);
line.computeLineDistances();
this.rangedGroup.add(line);
// 3. Blocker Visualization
if (!losResult.clear && losResult.blocker) {
const b = losResult.blocker;
if (b.type === 'hero' || b.type === 'monster') {
const blockRingGeo = new THREE.RingGeometry(0.4, 0.5, 32);
const blockRingMat = new THREE.MeshBasicMaterial({
color: 0xFF0000,
side: THREE.DoubleSide,
transparent: true,
opacity: 1.0,
depthTest: false
});
const blockRing = new THREE.Mesh(blockRingGeo, blockRingMat);
blockRing.rotation.x = -Math.PI / 2;
const bx = b.entity ? b.entity.x : b.x;
const by = b.entity ? b.entity.y : b.y;
blockRing.position.set(bx, 0.1, -by);
this.rangedGroup.add(blockRing);
}
}
}
enableDoorSelection(enabled, exitGroup) {
if (enabled) {
this.exitHighlightGroup.clear();
if (exitGroup) {
exitGroup.children.forEach(doorMesh => {
if (doorMesh.userData.isDoor && !doorMesh.userData.isOpen) {
const ringGeom = new THREE.RingGeometry(1.2, 1.4, 32);
const ringMat = new THREE.MeshBasicMaterial({
color: 0x00ff00,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.6
});
const ring = new THREE.Mesh(ringGeom, ringMat);
ring.rotation.x = -Math.PI / 2;
ring.position.copy(doorMesh.position);
ring.position.y = 0.05;
// Store reference to door for click handling
doorMesh.userData.isExit = true;
const firstCell = doorMesh.userData.cells[0];
const dirMap = { 0: 'N', 1: 'E', 2: 'S', 3: 'W' };
doorMesh.userData.exitData = {
x: firstCell.x,
y: firstCell.y,
direction: dirMap[doorMesh.userData.direction] || 'N'
};
this.exitHighlightGroup.add(ring);
}
});
}
} else {
this.exitHighlightGroup.clear();
}
}
}