Implement Elf Ranged Combat and Pinned Mechanic
- Added 'Shoot Bow' action for Elf with Ballistic Skill mechanics (1995 rules). - Implemented strict Line of Sight (LOS) raycasting (Amanatides & Woo) with UI feedback for blockers. - Added 'Pinned' status: Heroes adjacent to monsters (without intervening walls) cannot move. - Enhanced UI with visual indicators for blocked shots (red circles) and temporary modals. - Polished 'End Phase' button layout and hidden it during Monster phase.
This commit is contained in:
@@ -44,6 +44,9 @@ export class GameRenderer {
|
||||
this.highlightGroup = new THREE.Group();
|
||||
this.scene.add(this.highlightGroup);
|
||||
|
||||
this.rangedGroup = new THREE.Group();
|
||||
this.scene.add(this.rangedGroup);
|
||||
|
||||
this.entities = new Map();
|
||||
}
|
||||
|
||||
@@ -1055,4 +1058,76 @@ export class GameRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
clearRangedTargeting() {
|
||||
if (this.rangedGroup) {
|
||||
while (this.rangedGroup.children.length > 0) {
|
||||
const child = this.rangedGroup.children[0];
|
||||
this.rangedGroup.remove(child);
|
||||
if (child.geometry) child.geometry.dispose();
|
||||
if (child.material) {
|
||||
if (Array.isArray(child.material)) child.material.forEach(m => m.dispose());
|
||||
else child.material.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showRangedTargeting(hero, monster, losResult) {
|
||||
this.clearRangedTargeting();
|
||||
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 (Center to Center at approx waist height)
|
||||
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 (Red Ring)
|
||||
if (!losResult.clear && losResult.blocker) {
|
||||
const b = losResult.blocker;
|
||||
// If blocker is Entity (Hero/Monster), show bright red ring
|
||||
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 // Always visible on top
|
||||
});
|
||||
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);
|
||||
}
|
||||
// Walls are implicit (Line just turns red and stops/passes through)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user