Ensure projectiles dont get embedded in walls or blocking tiles
This commit is contained in:
50
src/engine/__tests__/throwing.test.ts
Normal file
50
src/engine/__tests__/throwing.test.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { traceProjectile } from '../gameplay/CombatLogic';
|
||||||
|
import { EntityManager } from '../EntityManager';
|
||||||
|
import { type World, type Actor, type EntityId } from '../../core/types';
|
||||||
|
|
||||||
|
const createTestWorld = (actors: Map<EntityId, Actor>): World => {
|
||||||
|
return {
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
tiles: new Array(100).fill(0), // 0 = Floor
|
||||||
|
actors,
|
||||||
|
exit: { x: 9, y: 9 }
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Throwing Mechanics', () => {
|
||||||
|
it('should land ON the wall currently (demonstrating the bug)', () => {
|
||||||
|
const actors = new Map<EntityId, Actor>();
|
||||||
|
const world = createTestWorld(actors);
|
||||||
|
const entityManager = new EntityManager(world);
|
||||||
|
|
||||||
|
// Wall at (5, 0)
|
||||||
|
world.tiles[5] = 4; // Wall
|
||||||
|
|
||||||
|
const start = { x: 0, y: 0 };
|
||||||
|
const target = { x: 5, y: 0 }; // Target the wall directly
|
||||||
|
|
||||||
|
const result = traceProjectile(world, start, target, entityManager);
|
||||||
|
|
||||||
|
// NEW BEHAVIOR: blockedPos is the tile BEFORE the wall (4, 0)
|
||||||
|
expect(result.blockedPos).toEqual({ x: 4, y: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should land ON the wall when throwing PAST a wall (demonstrating the bug)', () => {
|
||||||
|
const actors = new Map<EntityId, Actor>();
|
||||||
|
const world = createTestWorld(actors);
|
||||||
|
const entityManager = new EntityManager(world);
|
||||||
|
|
||||||
|
// Wall at (3, 0)
|
||||||
|
world.tiles[3] = 4; // Wall
|
||||||
|
|
||||||
|
const start = { x: 0, y: 0 };
|
||||||
|
const target = { x: 5, y: 0 }; // Target past the wall
|
||||||
|
|
||||||
|
const result = traceProjectile(world, start, target, entityManager);
|
||||||
|
|
||||||
|
// NEW BEHAVIOR: Hits the wall at 3,0, stops at 2,0
|
||||||
|
expect(result.blockedPos).toEqual({ x: 2, y: 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,9 +13,9 @@ export interface ProjectileResult {
|
|||||||
* Calculates the path and impact of a projectile.
|
* Calculates the path and impact of a projectile.
|
||||||
*/
|
*/
|
||||||
export function traceProjectile(
|
export function traceProjectile(
|
||||||
world: World,
|
world: World,
|
||||||
start: Vec2,
|
start: Vec2,
|
||||||
target: Vec2,
|
target: Vec2,
|
||||||
entityManager: EntityManager,
|
entityManager: EntityManager,
|
||||||
shooterId?: EntityId
|
shooterId?: EntityId
|
||||||
): ProjectileResult {
|
): ProjectileResult {
|
||||||
@@ -32,13 +32,13 @@ export function traceProjectile(
|
|||||||
// Check if we hit a combatant
|
// Check if we hit a combatant
|
||||||
const actors = entityManager.getActorsAt(p.x, p.y);
|
const actors = entityManager.getActorsAt(p.x, p.y);
|
||||||
const enemy = actors.find(a => a.category === "combatant" && a.id !== shooterId);
|
const enemy = actors.find(a => a.category === "combatant" && a.id !== shooterId);
|
||||||
|
|
||||||
if (enemy) {
|
if (enemy) {
|
||||||
hitActorId = enemy.id;
|
hitActorId = enemy.id;
|
||||||
blockedPos = p;
|
blockedPos = p;
|
||||||
} else {
|
} else {
|
||||||
// Hit wall or other obstacle
|
// Hit wall or other obstacle
|
||||||
blockedPos = p;
|
blockedPos = points[i - 1];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -63,13 +63,13 @@ export function getClosestVisibleEnemy(
|
|||||||
): Vec2 | null {
|
): Vec2 | null {
|
||||||
let closestDistSq = Infinity;
|
let closestDistSq = Infinity;
|
||||||
let closestPos: Vec2 | null = null;
|
let closestPos: Vec2 | null = null;
|
||||||
|
|
||||||
// Helper to check visibility
|
// Helper to check visibility
|
||||||
const isVisible = (x: number, y: number) => {
|
const isVisible = (x: number, y: number) => {
|
||||||
if (Array.isArray(seenTiles) || seenTiles instanceof Uint8Array || seenTiles instanceof Int8Array) {
|
if (Array.isArray(seenTiles) || seenTiles instanceof Uint8Array || seenTiles instanceof Int8Array) {
|
||||||
// Flat array
|
// Flat array
|
||||||
if (!width) return false;
|
if (!width) return false;
|
||||||
return (seenTiles as any)[y * width + x];
|
return (seenTiles as any)[y * width + x];
|
||||||
} else {
|
} else {
|
||||||
// Set<string>
|
// Set<string>
|
||||||
return (seenTiles as Set<string>).has(`${x},${y}`);
|
return (seenTiles as Set<string>).has(`${x},${y}`);
|
||||||
@@ -78,13 +78,13 @@ export function getClosestVisibleEnemy(
|
|||||||
|
|
||||||
for (const actor of world.actors.values()) {
|
for (const actor of world.actors.values()) {
|
||||||
if (actor.category !== "combatant" || actor.isPlayer) continue;
|
if (actor.category !== "combatant" || actor.isPlayer) continue;
|
||||||
|
|
||||||
// Check visibility
|
// Check visibility
|
||||||
if (!isVisible(actor.pos.x, actor.pos.y)) continue;
|
if (!isVisible(actor.pos.x, actor.pos.y)) continue;
|
||||||
|
|
||||||
const dx = actor.pos.x - origin.x;
|
const dx = actor.pos.x - origin.x;
|
||||||
const dy = actor.pos.y - origin.y;
|
const dy = actor.pos.y - origin.y;
|
||||||
const distSq = dx*dx + dy*dy;
|
const distSq = dx * dx + dy * dy;
|
||||||
|
|
||||||
if (distSq < closestDistSq) {
|
if (distSq < closestDistSq) {
|
||||||
closestDistSq = distSq;
|
closestDistSq = distSq;
|
||||||
|
|||||||
Reference in New Issue
Block a user