Half changes to switch to exit level, Ran out of credits, re added enemies
This commit is contained in:
BIN
public/assets/sprites/items/track_switch.png
Normal file
BIN
public/assets/sprites/items/track_switch.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 896 B |
@@ -73,8 +73,8 @@ export const GAME_CONFIG = {
|
||||
},
|
||||
|
||||
enemyScaling: {
|
||||
baseCount: 0,
|
||||
baseCountPerFloor: 0,
|
||||
baseCount: 15,
|
||||
baseCountPerFloor: 5,
|
||||
hpPerFloor: 5,
|
||||
attackPerTwoFloors: 1,
|
||||
expMultiplier: 1.2
|
||||
@@ -190,7 +190,8 @@ export const GAME_CONFIG = {
|
||||
{ key: "mine_cart", path: "assets/sprites/items/mine_cart.png" },
|
||||
{ key: "track_straight", path: "assets/sprites/items/track_straight.png" },
|
||||
{ key: "track_corner", path: "assets/sprites/items/track_corner.png" },
|
||||
{ key: "track_vertical", path: "assets/sprites/items/track_vertical.png" }
|
||||
{ key: "track_vertical", path: "assets/sprites/items/track_vertical.png" },
|
||||
{ key: "track_switch", path: "assets/sprites/items/track_switch.png" }
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ function createMockWorld(): World {
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,19 +96,19 @@ describe("EntityAccessor", () => {
|
||||
ecsWorld.addComponent(actor.id, "name", { name: actor.id.toString() });
|
||||
|
||||
if (actor.category === "combatant") {
|
||||
const c = actor as CombatantActor;
|
||||
ecsWorld.addComponent(actor.id, "stats", c.stats);
|
||||
ecsWorld.addComponent(actor.id, "energy", { current: c.energy, speed: c.speed });
|
||||
ecsWorld.addComponent(actor.id, "actorType", { type: c.type });
|
||||
if (c.isPlayer) {
|
||||
ecsWorld.addComponent(actor.id, "player", {});
|
||||
} else {
|
||||
ecsWorld.addComponent(actor.id, "ai", { state: "wandering" });
|
||||
}
|
||||
const c = actor as CombatantActor;
|
||||
ecsWorld.addComponent(actor.id, "stats", c.stats);
|
||||
ecsWorld.addComponent(actor.id, "energy", { current: c.energy, speed: c.speed });
|
||||
ecsWorld.addComponent(actor.id, "actorType", { type: c.type });
|
||||
if (c.isPlayer) {
|
||||
ecsWorld.addComponent(actor.id, "player", {});
|
||||
} else {
|
||||
ecsWorld.addComponent(actor.id, "ai", { state: "wandering" });
|
||||
}
|
||||
} else if (actor.category === "collectible") {
|
||||
ecsWorld.addComponent(actor.id, "collectible", { type: "exp_orb", amount: (actor as CollectibleActor).expAmount });
|
||||
ecsWorld.addComponent(actor.id, "collectible", { type: "exp_orb", amount: (actor as CollectibleActor).expAmount });
|
||||
} else if (actor.category === "item_drop") {
|
||||
ecsWorld.addComponent(actor.id, "groundItem", { item: (actor as ItemDropActor).item });
|
||||
ecsWorld.addComponent(actor.id, "groundItem", { item: (actor as ItemDropActor).item });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ const createTestWorld = (): World => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(TileType.EMPTY),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const createTestStats = (overrides: Partial<any> = {}) => ({
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
@@ -107,11 +108,11 @@ describe('AI Behavior & Scheduling', () => {
|
||||
terrainTypes.forEach(({ type, name }) => {
|
||||
it(`should see player when standing on ${name}`, () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
actors.set(1 as EntityId, { id: 1 as EntityId, category: "combatant", isPlayer: true, pos: { x: 5, y: 0 }, stats: createTestStats(), energy: 0 } as any);
|
||||
actors.set(2 as EntityId, {
|
||||
id: 2 as EntityId, category: "combatant", isPlayer: false, pos: { x: 0, y: 0 },
|
||||
stats: createTestStats(), aiState: "wandering", energy: 0
|
||||
} as any);
|
||||
actors.set(1 as EntityId, { id: 1 as EntityId, category: "combatant", isPlayer: true, pos: { x: 5, y: 0 }, stats: createTestStats(), energy: 0 } as any);
|
||||
actors.set(2 as EntityId, {
|
||||
id: 2 as EntityId, category: "combatant", isPlayer: false, pos: { x: 0, y: 0 },
|
||||
stats: createTestStats(), aiState: "wandering", energy: 0
|
||||
} as any);
|
||||
|
||||
const world = createTestWorld();
|
||||
world.tiles[0] = type;
|
||||
@@ -132,25 +133,25 @@ describe('AI Behavior & Scheduling', () => {
|
||||
// -------------------------------------------------------------------------
|
||||
describe('AI Aggression State Machine', () => {
|
||||
it('should become pursuing when damaged by player, even if not sighting player', () => {
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
// Player far away/invisible (simulated logic)
|
||||
const player = { id: 1 as EntityId, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 }, stats: createTestStats({ attack: 1, accuracy: 100 }), energy: 0 } as any;
|
||||
const enemy = {
|
||||
id: 2 as EntityId, category: "combatant", isPlayer: false, pos: { x: 0, y: 5 },
|
||||
stats: createTestStats({ hp: 10, defense: 0, evasion: 0 }), aiState: "wandering", energy: 0
|
||||
} as any;
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
// Player far away/invisible (simulated logic)
|
||||
const player = { id: 1 as EntityId, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 }, stats: createTestStats({ attack: 1, accuracy: 100 }), energy: 0 } as any;
|
||||
const enemy = {
|
||||
id: 2 as EntityId, category: "combatant", isPlayer: false, pos: { x: 0, y: 5 },
|
||||
stats: createTestStats({ hp: 10, defense: 0, evasion: 0 }), aiState: "wandering", energy: 0
|
||||
} as any;
|
||||
|
||||
actors.set(1 as EntityId, player);
|
||||
actors.set(2 as EntityId, enemy);
|
||||
const world = createTestWorld();
|
||||
syncToECS(actors);
|
||||
actors.set(1 as EntityId, player);
|
||||
actors.set(2 as EntityId, enemy);
|
||||
const world = createTestWorld();
|
||||
syncToECS(actors);
|
||||
|
||||
const testAccessor = new EntityAccessor(world, 1 as EntityId, ecsWorld);
|
||||
applyAction(world, 1 as EntityId, { type: "attack", targetId: 2 as EntityId }, testAccessor);
|
||||
const testAccessor = new EntityAccessor(world, 1 as EntityId, ecsWorld);
|
||||
applyAction(world, 1 as EntityId, { type: "attack", targetId: 2 as EntityId }, testAccessor);
|
||||
|
||||
const updatedEnemy = testAccessor.getCombatant(2 as EntityId);
|
||||
expect(updatedEnemy?.aiState).toBe("pursuing");
|
||||
expect(updatedEnemy?.lastKnownPlayerPos).toEqual(player.pos);
|
||||
const updatedEnemy = testAccessor.getCombatant(2 as EntityId);
|
||||
expect(updatedEnemy?.aiState).toBe("pursuing");
|
||||
expect(updatedEnemy?.lastKnownPlayerPos).toEqual(player.pos);
|
||||
});
|
||||
|
||||
it("should transition from alerted to pursuing after delay even if sight is blocked", () => {
|
||||
|
||||
@@ -28,7 +28,8 @@ describe("CombatLogic - getClosestVisibleEnemy", () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
@@ -70,7 +71,8 @@ describe("CombatLogic - getClosestVisibleEnemy", () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
@@ -123,7 +125,8 @@ describe("CombatLogic - getClosestVisibleEnemy", () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
|
||||
const actors = new Map<EntityId, Actor>();
|
||||
|
||||
@@ -11,7 +11,8 @@ describe('Pathfinding', () => {
|
||||
width,
|
||||
height,
|
||||
tiles: new Array(width * height).fill(tileType),
|
||||
exit: { x: 0, y: 0 }
|
||||
exit: { x: 0, y: 0 },
|
||||
trackPath: []
|
||||
});
|
||||
|
||||
it('should find a path between two reachable points', () => {
|
||||
@@ -38,7 +39,7 @@ describe('Pathfinding', () => {
|
||||
it('should return empty array if no path exists', () => {
|
||||
const world = createTestWorld(10, 10);
|
||||
// Create a wall blockage
|
||||
for(let x=0; x<10; x++) world.tiles[10 + x] = TileType.WALL;
|
||||
for (let x = 0; x < 10; x++) world.tiles[10 + x] = TileType.WALL;
|
||||
|
||||
const seen = new Uint8Array(100).fill(1);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,8 @@ const createTestWorld = (): World => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0), // 0 = Floor
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { idx, inBounds, isWall, isBlocked, tryDestructTile, isPlayerOnExit } from '../world/world-logic';
|
||||
import { idx, inBounds, isWall, isBlocked, tryDestructTile } from '../world/world-logic';
|
||||
import { type World, type Tile } from '../../core/types';
|
||||
import { TileType } from '../../core/terrain';
|
||||
|
||||
@@ -9,7 +9,8 @@ describe('World Utilities', () => {
|
||||
width,
|
||||
height,
|
||||
tiles,
|
||||
exit: { x: 0, y: 0 }
|
||||
exit: { x: 0, y: 0 },
|
||||
trackPath: []
|
||||
});
|
||||
|
||||
describe('idx', () => {
|
||||
@@ -88,10 +89,10 @@ describe('World Utilities', () => {
|
||||
it('should return true for actor positions', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(0));
|
||||
const mockAccessor = {
|
||||
getActorsAt: (x: number, y: number) => {
|
||||
if (x === 3 && y === 3) return [{ category: "combatant" }];
|
||||
return [];
|
||||
}
|
||||
getActorsAt: (x: number, y: number) => {
|
||||
if (x === 3 && y === 3) return [{ category: "combatant" }];
|
||||
return [];
|
||||
}
|
||||
} as any;
|
||||
|
||||
expect(isBlocked(world, 3, 3, mockAccessor)).toBe(true);
|
||||
@@ -137,43 +138,8 @@ describe('World Utilities', () => {
|
||||
});
|
||||
|
||||
it('should return false for out of bounds', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(TileType.EMPTY));
|
||||
expect(tryDestructTile(world, -1, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPlayerOnExit', () => {
|
||||
it('should return true when player is on exit', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(TileType.EMPTY));
|
||||
world.exit = { x: 5, y: 5 };
|
||||
|
||||
const mockAccessor = {
|
||||
getPlayer: () => ({ pos: { x: 5, y: 5 } })
|
||||
} as any;
|
||||
|
||||
expect(isPlayerOnExit(world, mockAccessor)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when player is not on exit', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(TileType.EMPTY));
|
||||
world.exit = { x: 5, y: 5 };
|
||||
|
||||
const mockAccessor = {
|
||||
getPlayer: () => ({ pos: { x: 4, y: 4 } })
|
||||
} as any;
|
||||
|
||||
expect(isPlayerOnExit(world, mockAccessor)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when player does not exist', () => {
|
||||
const world = createTestWorld(10, 10, new Array(100).fill(TileType.EMPTY));
|
||||
world.exit = { x: 5, y: 5 };
|
||||
|
||||
const mockAccessor = {
|
||||
getPlayer: () => null
|
||||
} as any;
|
||||
|
||||
expect(isPlayerOnExit(world, mockAccessor)).toBe(false);
|
||||
expect(tryDestructTile(world, -1, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,7 +174,7 @@ export class EntityBuilder {
|
||||
effectDuration?: number;
|
||||
}): this {
|
||||
this.components.trigger = {
|
||||
onEnter: options.onEnter ?? true,
|
||||
onEnter: options.onEnter ?? false,
|
||||
onExit: options.onExit,
|
||||
onInteract: options.onInteract,
|
||||
oneShot: options.oneShot,
|
||||
|
||||
@@ -243,9 +243,11 @@ export const Prefabs = {
|
||||
return EntityBuilder.create(world)
|
||||
.withPosition(x, y)
|
||||
.withName("Track Switch")
|
||||
.withSprite("dungeon", 31) // TileType.SWITCH_OFF
|
||||
.withSprite("track_switch", 0)
|
||||
.asTrigger({
|
||||
onEnter: false,
|
||||
onInteract: true,
|
||||
oneShot: true,
|
||||
targetId: cartId
|
||||
})
|
||||
.build();
|
||||
|
||||
@@ -12,7 +12,8 @@ describe('ECS Removal and Accessor', () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 0, y: 0 }
|
||||
exit: { x: 0, y: 0 },
|
||||
trackPath: []
|
||||
};
|
||||
const accessor = new EntityAccessor(gameWorld, 999 as EntityId, ecsWorld);
|
||||
|
||||
|
||||
@@ -105,9 +105,9 @@ export class TriggerSystem extends System {
|
||||
if (mineCart) {
|
||||
mineCart.isMoving = true;
|
||||
|
||||
// Change switch sprite to "on" (using dungeon sprite 32)
|
||||
// Change switch sprite if applicable (optional for now as we only have one frame)
|
||||
const sprite = world.getComponent(triggerId, "sprite");
|
||||
if (sprite) {
|
||||
if (sprite && sprite.texture === "dungeon") {
|
||||
sprite.index = 32;
|
||||
}
|
||||
}
|
||||
@@ -149,9 +149,9 @@ export class TriggerSystem extends System {
|
||||
if (trigger.oneShot) {
|
||||
trigger.triggered = true;
|
||||
|
||||
// Change sprite to triggered appearance (dungeon sprite 23)
|
||||
// Change sprite to triggered appearance if it's a dungeon sprite
|
||||
const sprite = world.getComponent(triggerId, "sprite");
|
||||
if (sprite) {
|
||||
if (sprite && sprite.texture === "dungeon") {
|
||||
sprite.index = 23; // Triggered/spent trap appearance
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ describe('CombatLogic', () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(TileType.EMPTY),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
ecsWorld = new ECSWorld();
|
||||
// Shooter ID 1
|
||||
@@ -85,24 +86,24 @@ describe('CombatLogic', () => {
|
||||
});
|
||||
|
||||
it('should ignore shooter position', () => {
|
||||
const start = { x: 0, y: 0 };
|
||||
const end = { x: 5, y: 0 };
|
||||
const start = { x: 0, y: 0 };
|
||||
const end = { x: 5, y: 0 };
|
||||
|
||||
// Shooter at start
|
||||
const shooter = {
|
||||
id: 1 as EntityId,
|
||||
type: 'player',
|
||||
category: 'combatant',
|
||||
pos: { x: 0, y: 0 },
|
||||
isPlayer: true
|
||||
};
|
||||
syncActor(shooter);
|
||||
// Shooter at start
|
||||
const shooter = {
|
||||
id: 1 as EntityId,
|
||||
type: 'player',
|
||||
category: 'combatant',
|
||||
pos: { x: 0, y: 0 },
|
||||
isPlayer: true
|
||||
};
|
||||
syncActor(shooter);
|
||||
|
||||
const result = traceProjectile(mockWorld, start, end, accessor, 1 as EntityId);
|
||||
const result = traceProjectile(mockWorld, start, end, accessor, 1 as EntityId);
|
||||
|
||||
// Should not hit self
|
||||
expect(result.hitActorId).toBeUndefined();
|
||||
expect(result.blockedPos).toEqual(end);
|
||||
// Should not hit self
|
||||
expect(result.hitActorId).toBeUndefined();
|
||||
expect(result.blockedPos).toEqual(end);
|
||||
});
|
||||
|
||||
it('should ignore non-combatant actors (e.g. items)', () => {
|
||||
@@ -123,6 +124,6 @@ describe('CombatLogic', () => {
|
||||
// Should pass through item
|
||||
expect(result.blockedPos).toEqual(end);
|
||||
expect(result.hitActorId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,8 @@ describe("Fireable Weapons & Ammo System", () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
ecsWorld = new ECSWorld();
|
||||
accessor = new EntityAccessor(world, 1 as EntityId, ecsWorld);
|
||||
@@ -139,23 +140,23 @@ describe("Fireable Weapons & Ammo System", () => {
|
||||
});
|
||||
|
||||
it("should deep clone on spawn so pistols remain independent", () => {
|
||||
const playerActor = accessor.getPlayer()!;
|
||||
const pistol1 = createRangedWeapon("pistol");
|
||||
const playerActor = accessor.getPlayer()!;
|
||||
const pistol1 = createRangedWeapon("pistol");
|
||||
|
||||
// Spawn 1
|
||||
itemManager.spawnItem(pistol1, {x:0, y:0});
|
||||
const picked1 = itemManager.tryPickup(playerActor)! as RangedWeaponItem;
|
||||
// Spawn 1
|
||||
itemManager.spawnItem(pistol1, { x: 0, y: 0 });
|
||||
const picked1 = itemManager.tryPickup(playerActor)! as RangedWeaponItem;
|
||||
|
||||
// Spawn 2
|
||||
const pistol2 = createRangedWeapon("pistol");
|
||||
itemManager.spawnItem(pistol2, {x:0, y:0});
|
||||
const picked2 = itemManager.tryPickup(playerActor)! as RangedWeaponItem;
|
||||
// Spawn 2
|
||||
const pistol2 = createRangedWeapon("pistol");
|
||||
itemManager.spawnItem(pistol2, { x: 0, y: 0 });
|
||||
const picked2 = itemManager.tryPickup(playerActor)! as RangedWeaponItem;
|
||||
|
||||
expect(picked1).not.toBe(picked2);
|
||||
expect(picked1.stats).not.toBe(picked2.stats); // Critical!
|
||||
expect(picked1).not.toBe(picked2);
|
||||
expect(picked1.stats).not.toBe(picked2.stats); // Critical!
|
||||
|
||||
// Modifying one should not affect other
|
||||
picked1.currentAmmo = 0;
|
||||
expect(picked2.currentAmmo).toBe(6);
|
||||
// Modifying one should not affect other
|
||||
picked1.currentAmmo = 0;
|
||||
expect(picked2.currentAmmo).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,8 @@ describe('Movement Blocking Behavior', () => {
|
||||
width: 3,
|
||||
height: 3,
|
||||
tiles: new Array(9).fill(TileType.GRASS),
|
||||
exit: { x: 2, y: 2 }
|
||||
exit: { x: 2, y: 2 },
|
||||
trackPath: []
|
||||
};
|
||||
|
||||
// Blocking wall at (1, 0)
|
||||
|
||||
@@ -99,8 +99,25 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
|
||||
const exit = { ...trackPath[trackPath.length - 1] };
|
||||
|
||||
// Place Switch at the end of the track
|
||||
Prefabs.trackSwitch(ecsWorld, exit.x, exit.y, cartId);
|
||||
// Place Switch adjacent to the end of the track
|
||||
let switchPos = { x: exit.x, y: exit.y };
|
||||
const neighbors = [
|
||||
{ x: exit.x + 1, y: exit.y },
|
||||
{ x: exit.x - 1, y: exit.y },
|
||||
{ x: exit.x, y: exit.y + 1 },
|
||||
{ x: exit.x, y: exit.y - 1 },
|
||||
];
|
||||
for (const n of neighbors) {
|
||||
if (n.x >= 1 && n.x < width - 1 && n.y >= 1 && n.y < height - 1) {
|
||||
const t = tiles[n.y * width + n.x];
|
||||
if (t === TileType.EMPTY || t === TileType.EMPTY_DECO || t === TileType.GRASS || t === TileType.TRACK) {
|
||||
switchPos = n;
|
||||
// Don't break if it's track, try to find a real empty spot first
|
||||
if (t !== TileType.TRACK) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Prefabs.trackSwitch(ecsWorld, switchPos.x, switchPos.y, cartId);
|
||||
|
||||
// Mark all track and room tiles as occupied for objects
|
||||
const occupiedPositions = new Set<string>();
|
||||
@@ -366,7 +383,7 @@ function placeEnemies(
|
||||
const room = rooms[roomIdx];
|
||||
|
||||
// Try to find an empty spot in the room
|
||||
for (let attempts = 0; attempts < 5; attempts++) {
|
||||
for (let attempts = 0; attempts < 20; attempts++) {
|
||||
|
||||
const ex = room.x + 1 + Math.floor(random() * (room.width - 2));
|
||||
const ey = room.y + 1 + Math.floor(random() * (room.height - 2));
|
||||
@@ -389,6 +406,9 @@ function placeEnemies(
|
||||
EntityBuilder.create(ecsWorld)
|
||||
.asEnemy(type)
|
||||
.withPosition(ex, ey)
|
||||
.withSprite(type, 0)
|
||||
.withName(type.charAt(0).toUpperCase() + type.slice(1))
|
||||
.withCombat()
|
||||
.withStats({
|
||||
maxHp: scaledHp + Math.floor(random() * 4),
|
||||
hp: scaledHp + Math.floor(random() * 4),
|
||||
@@ -396,7 +416,6 @@ function placeEnemies(
|
||||
defense: enemyDef.baseDefense,
|
||||
})
|
||||
.withEnergy(speed) // Configured speed
|
||||
// Note: Other stats like crit/evasion are defaults from EntityBuilder or BaseStats
|
||||
.build();
|
||||
|
||||
occupiedPositions.add(k);
|
||||
|
||||
@@ -43,7 +43,20 @@ export function isBlocked(w: World, x: number, y: number, accessor: EntityAccess
|
||||
|
||||
if (!accessor) return false;
|
||||
const actors = accessor.getActorsAt(x, y);
|
||||
return actors.some(a => a.category === "combatant");
|
||||
if (actors.some(a => a.category === "combatant")) return true;
|
||||
|
||||
// Check for interactable entities (switches, etc.) that should block movement
|
||||
if (accessor.context) {
|
||||
const ecs = accessor.context;
|
||||
const isInteractable = ecs.getEntitiesWith("position", "trigger").some(id => {
|
||||
const p = ecs.getComponent(id, "position");
|
||||
const t = ecs.getComponent(id, "trigger");
|
||||
return p?.x === x && p?.y === y && t?.onInteract;
|
||||
});
|
||||
if (isInteractable) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ export class DungeonRenderer {
|
||||
const spriteData = this.ecsWorld.getComponent(entId, "sprite");
|
||||
if (pos && spriteData) {
|
||||
try {
|
||||
const isStandalone = spriteData.texture === "mine_cart" || spriteData.texture === "ceramic_dragon_head";
|
||||
const isStandalone = spriteData.texture === "mine_cart" || spriteData.texture === "ceramic_dragon_head" || spriteData.texture === "track_switch";
|
||||
const sprite = this.scene.add.sprite(
|
||||
pos.x * TILE_SIZE + TILE_SIZE / 2,
|
||||
pos.y * TILE_SIZE + TILE_SIZE / 2,
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('DungeonRenderer', () => {
|
||||
killTweensOf: vi.fn(),
|
||||
},
|
||||
time: {
|
||||
now: 0
|
||||
now: 0
|
||||
}
|
||||
};
|
||||
|
||||
@@ -152,6 +152,7 @@ describe('DungeonRenderer', () => {
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(0),
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
ecsWorld = new ECSWorld();
|
||||
accessor = new EntityAccessor(mockWorld, 1 as EntityId, ecsWorld);
|
||||
|
||||
@@ -13,7 +13,8 @@ describe('ItemManager', () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
tiles: new Array(100).fill(1), // Floor
|
||||
exit: { x: 9, y: 9 }
|
||||
exit: { x: 9, y: 9 },
|
||||
trackPath: []
|
||||
};
|
||||
|
||||
entityAccessor = {
|
||||
|
||||
Reference in New Issue
Block a user