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:
2026-01-06 20:05:56 +01:00
parent 7b28fcf1b0
commit c0a9299dc5
7 changed files with 591 additions and 6 deletions

View File

@@ -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)
}
}
}