feat: Implement 2D tactical view and refine LOS with corner detection

This commit is contained in:
2026-01-06 20:50:46 +01:00
parent c0a9299dc5
commit 61c7cc3313
5 changed files with 255 additions and 8 deletions

View File

@@ -823,25 +823,76 @@ export class GameEngine {
break;
}
// Helper: Distance from Cell Center to Ray (for grazing tolerance)
const getDist = () => {
const cx = currentX + 0.5;
const cy = currentY + 0.5;
const len = Math.sqrt(dx * dx + dy * dy);
if (len === 0) return 0;
return Math.abs(dy * cx - dx * cy + dx * y1 - dy * x1) / len;
};
// Tolerance: Allow shots to pass if they graze the edge (0.5 is full width)
// 0.4 means the outer 20% of the tile is "safe" to shoot through.
const ENTITY_HITBOX_RADIUS = 0.4;
// 2. Monster Check
const m = this.monsters.find(m => m.x === currentX && m.y === currentY && !m.isDead && m.id !== target.id);
if (m) {
blocked = true;
blocker = { type: 'monster', entity: m };
console.log(`[LOS] Blocked by MONSTER: ${m.name}`);
break;
if (getDist() < ENTITY_HITBOX_RADIUS) {
blocked = true;
blocker = { type: 'monster', entity: m };
console.log(`[LOS] Blocked by MONSTER: ${m.name}`);
break;
} else {
console.log(`[LOS] Grazed MONSTER ${m.name} (Dist: ${getDist().toFixed(2)})`);
}
}
// 3. Hero Check
const h = this.heroes.find(h => h.x === currentX && h.y === currentY && h.id !== hero.id);
if (h) {
blocked = true;
blocker = { type: 'hero', entity: h };
console.log(`[LOS] Blocked by HERO: ${h.name}`);
break;
if (getDist() < ENTITY_HITBOX_RADIUS) {
blocked = true;
blocker = { type: 'hero', entity: h };
console.log(`[LOS] Blocked by HERO: ${h.name}`);
break;
} else {
console.log(`[LOS] Grazed HERO ${h.name} (Dist: ${getDist().toFixed(2)})`);
}
}
}
if (currentX === endX && currentY === endY) break;
// CORNER CROSSING CHECK: Prevent diagonal wall leaking
// When tMaxX ≈ tMaxY, the ray passes through a vertex shared by 4 cells.
// Standard algorithm only visits 2 of them. We must check BOTH neighbors.
const CORNER_EPSILON = 0.001;
const cornerCrossing = Math.abs(tMaxX - tMaxY) < CORNER_EPSILON;
if (cornerCrossing) {
// Check both orthogonal neighbors
const neighborX = currentX + stepX;
const neighborY = currentY + stepY;
// Check horizontal neighbor
if (this.dungeon.grid.isWall(neighborX, currentY)) {
blocked = true;
blocker = { type: 'wall', x: neighborX, y: currentY };
console.log(`[LOS] Blocked by CORNER WALL at ${neighborX},${currentY}`);
break;
}
// Check vertical neighbor
if (this.dungeon.grid.isWall(currentX, neighborY)) {
blocked = true;
blocker = { type: 'wall', x: currentX, y: neighborY };
console.log(`[LOS] Blocked by CORNER WALL at ${currentX},${neighborY}`);
break;
}
}
if (tMaxX < tMaxY) {
tMaxX += tDeltaX;
currentX += stepX;