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