Added rail tracks, cart and camera movement with arrow keys, removed enemies... 4 now
This commit is contained in:
@@ -73,10 +73,16 @@ export const GAME_CONFIG = {
|
||||
},
|
||||
|
||||
enemyScaling: {
|
||||
baseCount: 3,
|
||||
baseCountPerFloor: 3,
|
||||
baseCount: 0,
|
||||
baseCountPerFloor: 0,
|
||||
hpPerFloor: 5,
|
||||
attackPerTwoFloors: 1,
|
||||
expMultiplier: 1.2
|
||||
},
|
||||
|
||||
trapScaling: {
|
||||
baseCount: 0,
|
||||
baseCountPerFloor: 0.5
|
||||
},
|
||||
|
||||
leveling: {
|
||||
@@ -180,9 +186,14 @@ export const GAME_CONFIG = {
|
||||
{ key: "PriestessNorth", path: "assets/sprites/priestess/PriestessNorth.png" },
|
||||
{ key: "PriestessSouth", path: "assets/sprites/priestess/PriestessSouth.png" },
|
||||
{ key: "PriestessEast", path: "assets/sprites/priestess/PriestessEast.png" },
|
||||
{ key: "PriestessWest", path: "assets/sprites/priestess/PriestessWest.png" }
|
||||
{ key: "PriestessWest", path: "assets/sprites/priestess/PriestessWest.png" },
|
||||
{ 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" }
|
||||
]
|
||||
|
||||
|
||||
},
|
||||
|
||||
animations: [
|
||||
|
||||
@@ -8,9 +8,13 @@ export const TileType = {
|
||||
EXIT: 8,
|
||||
WATER: 63, // Unused but kept for safety/legacy
|
||||
DOOR_CLOSED: 5,
|
||||
DOOR_OPEN: 6
|
||||
DOOR_OPEN: 6,
|
||||
TRACK: 30, // Restored to 30 to fix duplicate key error
|
||||
SWITCH_OFF: 31,
|
||||
SWITCH_ON: 32
|
||||
} as const;
|
||||
|
||||
|
||||
export type TileType = typeof TileType[keyof typeof TileType];
|
||||
|
||||
export interface TileBehavior {
|
||||
@@ -32,9 +36,13 @@ export const TILE_DEFINITIONS: Record<number, TileBehavior> = {
|
||||
[TileType.EXIT]: { id: TileType.EXIT, isBlocking: false, isDestructible: false },
|
||||
[TileType.WATER]: { id: TileType.WATER, isBlocking: true, isDestructible: false },
|
||||
[TileType.DOOR_CLOSED]: { id: TileType.DOOR_CLOSED, isBlocking: false, isDestructible: true, isDestructibleByWalk: true, blocksVision: true, destructsTo: TileType.DOOR_OPEN },
|
||||
[TileType.DOOR_OPEN]: { id: TileType.DOOR_OPEN, isBlocking: false, isDestructible: true, isDestructibleByWalk: true, blocksVision: false, destructsTo: TileType.DOOR_CLOSED }
|
||||
[TileType.DOOR_OPEN]: { id: TileType.DOOR_OPEN, isBlocking: false, isDestructible: true, isDestructibleByWalk: true, blocksVision: false, destructsTo: TileType.DOOR_CLOSED },
|
||||
[TileType.TRACK]: { id: TileType.TRACK, isBlocking: false, isDestructible: false },
|
||||
[TileType.SWITCH_OFF]: { id: TileType.SWITCH_OFF, isBlocking: true, isDestructible: false },
|
||||
[TileType.SWITCH_ON]: { id: TileType.SWITCH_ON, isBlocking: true, isDestructible: false }
|
||||
};
|
||||
|
||||
|
||||
export function isBlocking(tile: number): boolean {
|
||||
const def = TILE_DEFINITIONS[tile];
|
||||
return def ? def.isBlocking : false;
|
||||
|
||||
@@ -27,7 +27,8 @@ export type SimEvent =
|
||||
| { type: "orb-spawned"; orbId: EntityId; x: number; y: number }
|
||||
| { type: "leveled-up"; actorId: EntityId; level: number; x: number; y: number }
|
||||
| { type: "enemy-alerted"; enemyId: EntityId; x: number; y: number }
|
||||
| { type: "move-blocked"; actorId: EntityId; x: number; y: number };
|
||||
| { type: "move-blocked"; actorId: EntityId; x: number; y: number }
|
||||
| { type: "mission-complete" };
|
||||
|
||||
|
||||
export type Stats = {
|
||||
@@ -222,6 +223,7 @@ export type World = {
|
||||
height: number;
|
||||
tiles: Tile[];
|
||||
exit: Vec2;
|
||||
trackPath: Vec2[];
|
||||
};
|
||||
|
||||
export interface UIUpdatePayload {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type ECSWorld } from "./World";
|
||||
import { type ComponentMap } from "./components";
|
||||
import { type EntityId, type Stats, type EnemyAIState, type ActorType, type Inventory, type Equipment, type Item } from "../../core/types";
|
||||
import { type EntityId, type Stats, type EnemyAIState, type ActorType, type Inventory, type Equipment, type Item, type Vec2 } from "../../core/types";
|
||||
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||
|
||||
/**
|
||||
@@ -129,10 +129,10 @@ export class EntityBuilder {
|
||||
if (type === "player") {
|
||||
throw new Error("Use asPlayer() for player entities");
|
||||
}
|
||||
|
||||
|
||||
this.components.actorType = { type };
|
||||
this.withAI("wandering");
|
||||
|
||||
|
||||
// Apply enemy stats from config
|
||||
const config = GAME_CONFIG.enemies[type as keyof typeof GAME_CONFIG.enemies];
|
||||
if (config) {
|
||||
@@ -145,7 +145,7 @@ export class EntityBuilder {
|
||||
});
|
||||
this.withEnergy(speed);
|
||||
}
|
||||
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -167,17 +167,22 @@ export class EntityBuilder {
|
||||
asTrigger(options: {
|
||||
onEnter?: boolean;
|
||||
onExit?: boolean;
|
||||
onInteract?: boolean;
|
||||
oneShot?: boolean;
|
||||
targetId?: EntityId;
|
||||
effect?: string;
|
||||
effectDuration?: number;
|
||||
}): this {
|
||||
this.components.trigger = {
|
||||
onEnter: options.onEnter ?? true,
|
||||
onExit: options.onExit,
|
||||
onInteract: options.onInteract,
|
||||
oneShot: options.oneShot,
|
||||
targetId: options.targetId,
|
||||
effect: options.effect,
|
||||
effectDuration: options.effectDuration
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -237,11 +242,26 @@ export class EntityBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure as a mine cart.
|
||||
*/
|
||||
asMineCart(path: Vec2[]): this {
|
||||
this.components.mineCart = {
|
||||
isMoving: false,
|
||||
path,
|
||||
pathIndex: 0
|
||||
};
|
||||
this.withSprite("mine_cart", 0);
|
||||
this.withName("Mine Cart");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize and register all components with the ECS world.
|
||||
* @returns The created entity ID
|
||||
*/
|
||||
build(): EntityId {
|
||||
|
||||
for (const [type, data] of Object.entries(this.components)) {
|
||||
if (data !== undefined) {
|
||||
this.world.addComponent(this.entityId, type as keyof ComponentMap, data as any);
|
||||
|
||||
@@ -19,15 +19,18 @@ export type GameEvent =
|
||||
|
||||
// Movement & trigger events
|
||||
| { type: "stepped_on"; entityId: EntityId; x: number; y: number }
|
||||
| { type: "entity_moved"; entityId: EntityId; from: { x: number; y: number }; to: { x: number; y: number } }
|
||||
| { type: "trigger_activated"; triggerId: EntityId; activatorId: EntityId }
|
||||
|
||||
|
||||
// Status effect events
|
||||
| { type: "status_applied"; entityId: EntityId; status: string; duration: number }
|
||||
| { type: "status_expired"; entityId: EntityId; status: string }
|
||||
| { type: "status_tick"; entityId: EntityId; status: string; remaining: number }
|
||||
|
||||
// World events
|
||||
| { type: "tile_changed"; x: number; y: number };
|
||||
| { type: "tile_changed"; x: number; y: number }
|
||||
| { type: "mission_complete" };
|
||||
|
||||
export type GameEventType = GameEvent["type"];
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type ECSWorld } from "./World";
|
||||
import { EntityBuilder } from "./EntityBuilder";
|
||||
import { type EntityId, type Item } from "../../core/types";
|
||||
import { type EntityId, type Item, type Vec2 } from "../../core/types";
|
||||
|
||||
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||
|
||||
/**
|
||||
@@ -222,9 +223,36 @@ export const Prefabs = {
|
||||
.withEnergy(config.speed)
|
||||
.withCombat()
|
||||
.build();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a mine cart at the start of a path.
|
||||
*/
|
||||
mineCart(world: ECSWorld, path: Vec2[]): EntityId {
|
||||
const start = path[0];
|
||||
return EntityBuilder.create(world)
|
||||
.withPosition(start.x, start.y)
|
||||
.asMineCart(path)
|
||||
.build();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a switch that triggers the mine cart.
|
||||
*/
|
||||
trackSwitch(world: ECSWorld, x: number, y: number, cartId: EntityId): EntityId {
|
||||
return EntityBuilder.create(world)
|
||||
.withPosition(x, y)
|
||||
.withName("Track Switch")
|
||||
.withSprite("dungeon", 31) // TileType.SWITCH_OFF
|
||||
.asTrigger({
|
||||
onInteract: true,
|
||||
targetId: cartId
|
||||
})
|
||||
.build();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Type for prefab factory functions.
|
||||
* Useful for creating maps of spawnable entities.
|
||||
|
||||
@@ -46,13 +46,25 @@ export interface ActorTypeComponent {
|
||||
export interface TriggerComponent {
|
||||
onEnter?: boolean; // Trigger when entity steps on this tile
|
||||
onExit?: boolean; // Trigger when entity leaves this tile
|
||||
onInteract?: boolean; // Trigger when entity interacts with this
|
||||
oneShot?: boolean; // Destroy/disable after triggering once
|
||||
triggered?: boolean; // Has already triggered (for oneShot triggers)
|
||||
targetId?: EntityId; // Target entity for this trigger (e.g., mine cart for a switch)
|
||||
damage?: number; // Damage to deal on trigger (for traps)
|
||||
|
||||
effect?: string; // Status effect to apply (e.g., "poison", "slow")
|
||||
effectDuration?: number; // Duration of applied effect
|
||||
}
|
||||
|
||||
/**
|
||||
* For the Mine Cart.
|
||||
*/
|
||||
export interface MineCartComponent {
|
||||
isMoving: boolean;
|
||||
path: Vec2[];
|
||||
pathIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status effect instance applied to an entity.
|
||||
*/
|
||||
@@ -133,6 +145,7 @@ export type ComponentMap = {
|
||||
inventory: InventoryComponent;
|
||||
equipment: EquipmentComponent;
|
||||
lifeSpan: LifeSpanComponent;
|
||||
mineCart: MineCartComponent;
|
||||
};
|
||||
|
||||
export type ComponentType = keyof ComponentMap;
|
||||
|
||||
47
src/engine/ecs/systems/MineCartSystem.ts
Normal file
47
src/engine/ecs/systems/MineCartSystem.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { System } from "../System";
|
||||
import { type ECSWorld } from "../World";
|
||||
import { type EntityId } from "../../../core/types";
|
||||
|
||||
|
||||
/**
|
||||
* System that moves the mine cart along its fixed path.
|
||||
* Moves 1 tile per update (tick).
|
||||
*/
|
||||
export class MineCartSystem extends System {
|
||||
readonly name = "MineCart";
|
||||
readonly requiredComponents = ["mineCart", "position", "sprite"] as const;
|
||||
|
||||
update(entities: EntityId[], world: ECSWorld) {
|
||||
for (const id of entities) {
|
||||
const mineCart = world.getComponent(id, "mineCart");
|
||||
const pos = world.getComponent(id, "position");
|
||||
|
||||
if (!mineCart || !pos || !mineCart.isMoving) continue;
|
||||
|
||||
// Move to next path node if available
|
||||
if (mineCart.pathIndex < mineCart.path.length - 1) {
|
||||
mineCart.pathIndex++;
|
||||
const nextPos = mineCart.path[mineCart.pathIndex];
|
||||
|
||||
// Update position component
|
||||
pos.x = nextPos.x;
|
||||
pos.y = nextPos.y;
|
||||
|
||||
// Emit event for visual feedback
|
||||
this.eventBus?.emit({
|
||||
type: "entity_moved",
|
||||
entityId: id,
|
||||
from: { x: pos.x, y: pos.y },
|
||||
to: nextPos
|
||||
});
|
||||
|
||||
} else {
|
||||
// Reached the end
|
||||
if (mineCart.isMoving) {
|
||||
mineCart.isMoving = false;
|
||||
this.eventBus?.emit({ type: "mission_complete" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export class TriggerSystem extends System {
|
||||
update(entities: EntityId[], world: ECSWorld, _dt?: number): void {
|
||||
// Get all entities with positions (potential activators)
|
||||
const allWithPosition = world.getEntitiesWith("position");
|
||||
|
||||
|
||||
for (const triggerId of entities) {
|
||||
const trigger = world.getComponent(triggerId, "trigger");
|
||||
const triggerPos = world.getComponent(triggerId, "position");
|
||||
@@ -49,9 +49,14 @@ export class TriggerSystem extends System {
|
||||
const isOnTrigger = entityPos.x === triggerPos.x && entityPos.y === triggerPos.y;
|
||||
const wasOnTrigger = this.wasEntityOnTrigger(entityId, triggerPos);
|
||||
|
||||
// Handle enter
|
||||
if (trigger.onEnter && isOnTrigger && !wasOnTrigger) {
|
||||
// Handle enter or manual trigger
|
||||
if ((trigger.onEnter && isOnTrigger && !wasOnTrigger) || (trigger.triggered && !trigger.oneShot)) {
|
||||
this.activateTrigger(triggerId, entityId, trigger, world);
|
||||
|
||||
// If it was manually triggered, we should probably reset the flag if its not oneShot
|
||||
if (trigger.triggered && !trigger.oneShot) {
|
||||
trigger.triggered = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle exit
|
||||
@@ -75,12 +80,15 @@ export class TriggerSystem extends System {
|
||||
private activateTrigger(
|
||||
triggerId: EntityId,
|
||||
activatorId: EntityId,
|
||||
trigger: {
|
||||
damage?: number;
|
||||
effect?: string;
|
||||
effectDuration?: number;
|
||||
oneShot?: boolean;
|
||||
trigger: {
|
||||
damage?: number;
|
||||
effect?: string;
|
||||
effectDuration?: number;
|
||||
oneShot?: boolean;
|
||||
triggered?: boolean;
|
||||
targetId?: EntityId;
|
||||
onInteract?: boolean;
|
||||
|
||||
},
|
||||
world: ECSWorld
|
||||
): void {
|
||||
@@ -91,12 +99,27 @@ export class TriggerSystem extends System {
|
||||
activatorId
|
||||
});
|
||||
|
||||
// Handle Mine Cart activation
|
||||
if (trigger.targetId) {
|
||||
const mineCart = world.getComponent(trigger.targetId, "mineCart");
|
||||
if (mineCart) {
|
||||
mineCart.isMoving = true;
|
||||
|
||||
// Change switch sprite to "on" (using dungeon sprite 32)
|
||||
const sprite = world.getComponent(triggerId, "sprite");
|
||||
if (sprite) {
|
||||
sprite.index = 32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply damage if trap
|
||||
|
||||
if (trigger.damage && trigger.damage > 0) {
|
||||
const stats = world.getComponent(activatorId, "stats");
|
||||
if (stats) {
|
||||
stats.hp = Math.max(0, stats.hp - trigger.damage);
|
||||
|
||||
|
||||
this.eventBus?.emit({
|
||||
type: "damage",
|
||||
entityId: activatorId,
|
||||
@@ -125,7 +148,7 @@ export class TriggerSystem extends System {
|
||||
// Mark as triggered for one-shot triggers and update sprite
|
||||
if (trigger.oneShot) {
|
||||
trigger.triggered = true;
|
||||
|
||||
|
||||
// Change sprite to triggered appearance (dungeon sprite 23)
|
||||
const sprite = world.getComponent(triggerId, "sprite");
|
||||
if (sprite) {
|
||||
|
||||
@@ -99,25 +99,21 @@ export class GameInput extends Phaser.Events.EventEmitter {
|
||||
}
|
||||
|
||||
public getCursorState() {
|
||||
// Return simplified cursor state for movement
|
||||
// Return simplified WASD state for movement
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
|
||||
const left = this.cursors.left?.isDown || this.wasd.A.isDown;
|
||||
const right = this.cursors.right?.isDown || this.wasd.D.isDown;
|
||||
const up = this.cursors.up?.isDown || this.wasd.W.isDown;
|
||||
const down = this.cursors.down?.isDown || this.wasd.S.isDown;
|
||||
const left = this.wasd.A.isDown;
|
||||
const right = this.wasd.D.isDown;
|
||||
const up = this.wasd.W.isDown;
|
||||
const down = this.wasd.S.isDown;
|
||||
|
||||
if (left) dx -= 1;
|
||||
if (right) dx += 1;
|
||||
if (up) dy -= 1;
|
||||
if (down) dy += 1;
|
||||
|
||||
const anyJustDown = Phaser.Input.Keyboard.JustDown(this.cursors.left!) ||
|
||||
Phaser.Input.Keyboard.JustDown(this.cursors.right!) ||
|
||||
Phaser.Input.Keyboard.JustDown(this.cursors.up!) ||
|
||||
Phaser.Input.Keyboard.JustDown(this.cursors.down!) ||
|
||||
Phaser.Input.Keyboard.JustDown(this.wasd.W) ||
|
||||
const anyJustDown = Phaser.Input.Keyboard.JustDown(this.wasd.W) ||
|
||||
Phaser.Input.Keyboard.JustDown(this.wasd.A) ||
|
||||
Phaser.Input.Keyboard.JustDown(this.wasd.S) ||
|
||||
Phaser.Input.Keyboard.JustDown(this.wasd.D);
|
||||
@@ -131,6 +127,19 @@ export class GameInput extends Phaser.Events.EventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
public getCameraPanState() {
|
||||
// Return Arrow key state for camera panning
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
|
||||
if (this.cursors.left?.isDown) dx -= 1;
|
||||
if (this.cursors.right?.isDown) dx += 1;
|
||||
if (this.cursors.up?.isDown) dy -= 1;
|
||||
if (this.cursors.down?.isDown) dy += 1;
|
||||
|
||||
return { dx, dy };
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
this.removeAllListeners();
|
||||
// Determine is scene specific cleanup is needed for inputs
|
||||
|
||||
@@ -108,6 +108,23 @@ function handleMove(w: World, actor: Actor, action: { dx: number; dy: number },
|
||||
handleExpCollection(actor, events, accessor);
|
||||
}
|
||||
return events;
|
||||
} else {
|
||||
// If blocked, check if we can interact with an entity at the target position
|
||||
if (actor.category === "combatant" && actor.isPlayer && accessor?.context) {
|
||||
const ecsWorld = accessor.context;
|
||||
const interactables = ecsWorld.getEntitiesWith("position", "trigger").filter(id => {
|
||||
const p = ecsWorld.getComponent(id, "position");
|
||||
const t = ecsWorld.getComponent(id, "trigger");
|
||||
return p?.x === nx && p?.y === ny && t?.onInteract;
|
||||
});
|
||||
|
||||
if (interactables.length > 0) {
|
||||
// Trigger interaction by marking it as triggered
|
||||
// The TriggerSystem will pick this up on the next update
|
||||
ecsWorld.getComponent(interactables[0], "trigger")!.triggered = true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return [{ type: "move-blocked", actorId: actor.id, x: nx, y: ny }];
|
||||
@@ -115,6 +132,7 @@ function handleMove(w: World, actor: Actor, action: { dx: number; dy: number },
|
||||
|
||||
|
||||
|
||||
|
||||
function handleAttack(_w: World, actor: Actor, action: { targetId: EntityId }, accessor: EntityAccessor): SimEvent[] {
|
||||
const target = accessor.getActor(action.targetId);
|
||||
if (target && target.category === "combatant" && actor.category === "combatant") {
|
||||
|
||||
@@ -18,6 +18,8 @@ import { Prefabs } from "../ecs/Prefabs";
|
||||
import { EntityBuilder } from "../ecs/EntityBuilder";
|
||||
|
||||
|
||||
|
||||
|
||||
interface Room {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -44,12 +46,25 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
// Set ROT's RNG seed for consistent dungeon generation
|
||||
ROT.RNG.setSeed(floor * 12345);
|
||||
|
||||
const rooms = generateRooms(width, height, tiles, floor, random);
|
||||
// Replace generateRooms call with track-first logic for mine cart mechanic
|
||||
const { rooms, trackPath } = generateTrackLevel(width, height, tiles, floor, random);
|
||||
|
||||
console.log(`[generator] Track generated with ${trackPath.length} nodes.`);
|
||||
console.log(`[generator] Rooms generated: ${rooms.length}`);
|
||||
|
||||
if (!trackPath || trackPath.length === 0) {
|
||||
throw new Error("Failed to generate track path");
|
||||
}
|
||||
|
||||
// Place player at start of track
|
||||
const playerX = trackPath[0].x;
|
||||
const playerY = trackPath[0].y;
|
||||
|
||||
// Clear track path
|
||||
for (const pos of trackPath) {
|
||||
tiles[pos.y * width + pos.x] = TileType.TRACK;
|
||||
}
|
||||
|
||||
// Place player in first room
|
||||
const firstRoom = rooms[0];
|
||||
const playerX = firstRoom.x + Math.floor(firstRoom.width / 2);
|
||||
const playerY = firstRoom.y + Math.floor(firstRoom.height / 2);
|
||||
|
||||
// Create Player Entity in ECS
|
||||
const runInventory = {
|
||||
@@ -79,248 +94,218 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
.withEnergy(GAME_CONFIG.player.speed)
|
||||
.build();
|
||||
|
||||
// No more legacy Actors Map
|
||||
// Create Mine Cart at start of track
|
||||
const cartId = Prefabs.mineCart(ecsWorld, trackPath);
|
||||
|
||||
// Place exit in last room
|
||||
const lastRoom = rooms[rooms.length - 1];
|
||||
const exit: Vec2 = {
|
||||
x: lastRoom.x + Math.floor(lastRoom.width / 2),
|
||||
y: lastRoom.y + Math.floor(lastRoom.height / 2)
|
||||
};
|
||||
const exit = { ...trackPath[trackPath.length - 1] };
|
||||
|
||||
placeEnemies(floor, rooms, ecsWorld, random);
|
||||
|
||||
// Place traps (using same ecsWorld)
|
||||
// Place Switch at the end of the track
|
||||
Prefabs.trackSwitch(ecsWorld, exit.x, exit.y, cartId);
|
||||
|
||||
// Mark all track and room tiles as occupied for objects
|
||||
const occupiedPositions = new Set<string>();
|
||||
occupiedPositions.add(`${playerX},${playerY}`); // Don't place traps on player start
|
||||
occupiedPositions.add(`${exit.x},${exit.y}`); // Don't place traps on exit
|
||||
placeTraps(floor, rooms, ecsWorld, tiles, width, random, occupiedPositions);
|
||||
|
||||
// Place doors for dungeon levels (Uniform/Digger)
|
||||
// Caves (Floors 10+) shouldn't have manufactured doors
|
||||
if (floor <= 9) {
|
||||
placeDoors(width, height, tiles, rooms, random);
|
||||
occupiedPositions.add(`${playerX},${playerY}`);
|
||||
occupiedPositions.add(`${exit.x},${exit.y}`);
|
||||
for (const pos of trackPath) {
|
||||
occupiedPositions.add(`${pos.x},${pos.y}`);
|
||||
}
|
||||
|
||||
// Place enemies
|
||||
placeEnemies(floor, rooms, ecsWorld, tiles, width, random, occupiedPositions);
|
||||
|
||||
// Place traps
|
||||
placeTraps(floor, rooms, ecsWorld, tiles, width, random, occupiedPositions);
|
||||
|
||||
// Decorate and finalize tiles
|
||||
decorate(width, height, tiles, random, exit);
|
||||
|
||||
// CRITICAL FIX: Ensure player start position is always clear!
|
||||
// Otherwise spawning in Grass (which blocks vision) makes the player blind.
|
||||
// Ensure start and end are walkable and marked
|
||||
tiles[playerY * width + playerX] = TileType.EMPTY;
|
||||
tiles[exit.y * width + exit.x] = TileType.EXIT;
|
||||
|
||||
return {
|
||||
world: { width, height, tiles, exit },
|
||||
world: { width, height, tiles, exit, trackPath },
|
||||
playerId,
|
||||
ecsWorld
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Update generateRooms signature to accept random
|
||||
function generateRooms(width: number, height: number, tiles: Tile[], floor: number, random: () => number): Room[] {
|
||||
/**
|
||||
* Generates a level with a central rail track from start to end.
|
||||
*/
|
||||
function generateTrackLevel(width: number, height: number, tiles: Tile[], _floor: number, random: () => number): { rooms: Room[], trackPath: Vec2[] } {
|
||||
const rooms: Room[] = [];
|
||||
|
||||
// Choose dungeon algorithm based on floor depth
|
||||
let dungeon: any;
|
||||
// 1. Generate Start and End points (further apart)
|
||||
const start: Vec2 = { x: 3, y: 5 + Math.floor(random() * (height - 10)) };
|
||||
const end: Vec2 = { x: width - 4, y: 5 + Math.floor(random() * (height - 10)) };
|
||||
|
||||
if (floor <= 4) {
|
||||
// Floors 1-4: Uniform (organic, irregular rooms)
|
||||
dungeon = new ROT.Map.Uniform(width, height, {
|
||||
roomWidth: [GAME_CONFIG.map.roomMinWidth, GAME_CONFIG.map.roomMaxWidth],
|
||||
roomHeight: [GAME_CONFIG.map.roomMinHeight, GAME_CONFIG.map.roomMaxHeight],
|
||||
roomDugPercentage: 0.3,
|
||||
});
|
||||
} else if (floor <= 9) {
|
||||
// Floors 5-9: Digger (traditional rectangular rooms + corridors)
|
||||
dungeon = new ROT.Map.Digger(width, height, {
|
||||
roomWidth: [GAME_CONFIG.map.roomMinWidth, GAME_CONFIG.map.roomMaxWidth],
|
||||
roomHeight: [GAME_CONFIG.map.roomMinHeight, GAME_CONFIG.map.roomMaxHeight],
|
||||
corridorLength: [2, 6],
|
||||
});
|
||||
} else {
|
||||
// Floors 10+: Cellular (natural cave systems)
|
||||
dungeon = new ROT.Map.Cellular(width, height, {
|
||||
born: [4, 5, 6, 7, 8],
|
||||
survive: [2, 3, 4, 5],
|
||||
});
|
||||
// 2. Generate Track Path (Winding random walk)
|
||||
const trackPath: Vec2[] = [];
|
||||
let curr = { ...start };
|
||||
trackPath.push(curr);
|
||||
|
||||
// Cellular needs randomization and smoothing
|
||||
dungeon.randomize(0.5);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
dungeon.create();
|
||||
}
|
||||
}
|
||||
// Bias weights
|
||||
const targetBias = 0.6;
|
||||
const straightBias = 0.2;
|
||||
|
||||
// Generate the dungeon
|
||||
dungeon.create((x: number, y: number, value: number) => {
|
||||
if (value === 0) {
|
||||
// 0 = floor, 1 = wall
|
||||
tiles[y * width + x] = TileType.EMPTY;
|
||||
}
|
||||
});
|
||||
let iter = 0;
|
||||
const maxIter = width * height;
|
||||
|
||||
// Extract room information from the generated dungeon
|
||||
const roomData = (dungeon as any).getRooms?.();
|
||||
let lastDir = { dx: 1, dy: 0 };
|
||||
|
||||
if (roomData && roomData.length > 0) {
|
||||
// Traditional dungeons (Uniform/Digger) have explicit rooms
|
||||
for (const room of roomData) {
|
||||
rooms.push({
|
||||
x: room.getLeft(),
|
||||
y: room.getTop(),
|
||||
width: room.getRight() - room.getLeft() + 1,
|
||||
height: room.getBottom() - room.getTop() + 1
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Cellular caves don't have explicit rooms, so find connected floor areas
|
||||
rooms.push(...extractRoomsFromCave(width, height, tiles));
|
||||
|
||||
// Connect the isolated cave rooms
|
||||
connectRooms(width, tiles, rooms, random);
|
||||
}
|
||||
|
||||
// Ensure we have at least 2 rooms for player/exit placement
|
||||
if (rooms.length < 2) {
|
||||
// Fallback: create two basic rooms
|
||||
rooms.push(
|
||||
{ x: 5, y: 5, width: 5, height: 5 },
|
||||
{ x: width - 10, y: height - 10, width: 5, height: 5 }
|
||||
);
|
||||
// Connect the fallback rooms
|
||||
connectRooms(width, tiles, rooms, random);
|
||||
}
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
function connectRooms(width: number, tiles: Tile[], rooms: Room[], random: () => number) {
|
||||
for (let i = 0; i < rooms.length - 1; i++) {
|
||||
const r1 = rooms[i];
|
||||
const r2 = rooms[i + 1];
|
||||
|
||||
const c1x = r1.x + Math.floor(r1.width / 2);
|
||||
const c1y = r1.y + Math.floor(r1.height / 2);
|
||||
const c2x = r2.x + Math.floor(r2.width / 2);
|
||||
const c2y = r2.y + Math.floor(r2.height / 2);
|
||||
|
||||
if (random() < 0.5) {
|
||||
digH(width, tiles, c1x, c2x, c1y);
|
||||
digV(width, tiles, c1y, c2y, c2x);
|
||||
} else {
|
||||
digV(width, tiles, c1y, c2y, c1x);
|
||||
digH(width, tiles, c1x, c2x, c2y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function digH(width: number, tiles: Tile[], x1: number, x2: number, y: number) {
|
||||
const start = Math.min(x1, x2);
|
||||
const end = Math.max(x1, x2);
|
||||
for (let x = start; x <= end; x++) {
|
||||
const idx = y * width + x;
|
||||
if (tiles[idx] === TileType.WALL) {
|
||||
tiles[idx] = TileType.EMPTY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function digV(width: number, tiles: Tile[], y1: number, y2: number, x: number) {
|
||||
const start = Math.min(y1, y2);
|
||||
const end = Math.max(y1, y2);
|
||||
for (let y = start; y <= end; y++) {
|
||||
const idx = y * width + x;
|
||||
if (tiles[idx] === TileType.WALL) {
|
||||
tiles[idx] = TileType.EMPTY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For cellular/cave maps, find clusters of floor tiles to use as "rooms"
|
||||
*/
|
||||
function extractRoomsFromCave(width: number, height: number, tiles: Tile[]): Room[] {
|
||||
const rooms: Room[] = [];
|
||||
const visited = new Set<number>();
|
||||
|
||||
// Find large connected floor areas
|
||||
for (let y = 1; y < height - 1; y++) {
|
||||
for (let x = 1; x < width - 1; x++) {
|
||||
const idx = y * width + x;
|
||||
if (tiles[idx] === TileType.EMPTY && !visited.has(idx)) {
|
||||
const cluster = floodFill(width, height, tiles, x, y, visited);
|
||||
|
||||
// Only consider clusters larger than 20 tiles
|
||||
if (cluster.length > 20) {
|
||||
// Create bounding box for this cluster
|
||||
let minX = width, maxX = 0, minY = height, maxY = 0;
|
||||
for (const pos of cluster) {
|
||||
const cx = pos % width;
|
||||
const cy = Math.floor(pos / width);
|
||||
minX = Math.min(minX, cx);
|
||||
maxX = Math.max(maxX, cx);
|
||||
minY = Math.min(minY, cy);
|
||||
maxY = Math.max(maxY, cy);
|
||||
}
|
||||
|
||||
rooms.push({
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX + 1,
|
||||
height: maxY - minY + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flood fill to find connected floor tiles
|
||||
*/
|
||||
function floodFill(width: number, height: number, tiles: Tile[], startX: number, startY: number, visited: Set<number>): number[] {
|
||||
const cluster: number[] = [];
|
||||
const queue: number[] = [startY * width + startX];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const idx = queue.shift()!;
|
||||
if (visited.has(idx)) continue;
|
||||
|
||||
visited.add(idx);
|
||||
cluster.push(idx);
|
||||
|
||||
const x = idx % width;
|
||||
const y = Math.floor(idx / width);
|
||||
|
||||
// Check 4 directions
|
||||
const neighbors = [
|
||||
{ nx: x + 1, ny: y },
|
||||
{ nx: x - 1, ny: y },
|
||||
{ nx: x, ny: y + 1 },
|
||||
{ nx: x, ny: y - 1 },
|
||||
while ((curr.x !== end.x || curr.y !== end.y) && iter < maxIter) {
|
||||
iter++;
|
||||
// Determine possible directions
|
||||
const dirs = [
|
||||
{ dx: 1, dy: 0 },
|
||||
{ dx: 0, dy: 1 },
|
||||
{ dx: 0, dy: -1 },
|
||||
{ dx: -1, dy: 0 }
|
||||
];
|
||||
|
||||
for (const { nx, ny } of neighbors) {
|
||||
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
||||
const nIdx = ny * width + nx;
|
||||
if (tiles[nIdx] === TileType.EMPTY && !visited.has(nIdx)) {
|
||||
queue.push(nIdx);
|
||||
// Score directions
|
||||
const scores = dirs.map(d => {
|
||||
let score = 0;
|
||||
|
||||
// Target bias (distance reduction)
|
||||
const distCurr = Math.abs(curr.x - end.x) + Math.abs(curr.y - end.y);
|
||||
const distNext = Math.abs((curr.x + d.dx) - end.x) + Math.abs((curr.y + d.dy) - end.y);
|
||||
if (distNext < distCurr) score += targetBias;
|
||||
|
||||
// Straight bias
|
||||
if (d.dx === lastDir.dx && d.dy === lastDir.dy) score += straightBias;
|
||||
|
||||
// Randomness
|
||||
score += random() * 0.3;
|
||||
|
||||
// Boundary check
|
||||
const nx = curr.x + d.dx;
|
||||
const ny = curr.y + d.dy;
|
||||
if (nx < 2 || nx >= width - 2 || ny < 2 || ny >= height - 2) score = -100;
|
||||
|
||||
return { d, score };
|
||||
});
|
||||
|
||||
// scores already sorted
|
||||
scores.sort((a, b) => b.score - a.score);
|
||||
const best = scores[0];
|
||||
|
||||
const nextX = curr.x + best.d.dx;
|
||||
const nextY = curr.y + best.d.dy;
|
||||
|
||||
// Create NEW position object to avoid mutation bugs
|
||||
curr = { x: nextX, y: nextY };
|
||||
lastDir = best.d;
|
||||
|
||||
// Avoid double-back if possible
|
||||
const existing = trackPath.find(p => p.x === curr.x && p.y === curr.y);
|
||||
if (!existing) {
|
||||
trackPath.push({ ...curr });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[generator] Track walker finished at (${curr.x}, ${curr.y}) after ${iter} iterations. Path length: ${trackPath.length}`);
|
||||
|
||||
// 3. Dig out the track path (Narrower 2x2 tunnel)
|
||||
for (const pos of trackPath) {
|
||||
for (let dy = 0; dy <= 1; dy++) {
|
||||
for (let dx = 0; dx <= 1; dx++) {
|
||||
const nx = pos.x + dx;
|
||||
const ny = pos.y + dy;
|
||||
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
||||
tiles[ny * width + nx] = TileType.EMPTY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cluster;
|
||||
// 4. Generate rooms branching off the track
|
||||
const numRooms = 12 + Math.floor(random() * 6);
|
||||
for (let i = 0; i < numRooms; i++) {
|
||||
const pathIdx = Math.floor(random() * trackPath.length);
|
||||
const pathNode = trackPath[pathIdx];
|
||||
|
||||
const rw = 6 + Math.floor(random() * 6);
|
||||
const rh = 5 + Math.floor(random() * 6);
|
||||
|
||||
// Random side offset
|
||||
const side = random() < 0.5 ? -1 : 1;
|
||||
let rx, ry;
|
||||
|
||||
if (random() < 0.5) { // Horizontal branch
|
||||
rx = pathNode.x + (side * Math.floor(rw / 2 + 2));
|
||||
ry = pathNode.y - Math.floor(rh / 2);
|
||||
} else { // Vertical branch
|
||||
rx = pathNode.x - Math.floor(rw / 2);
|
||||
ry = pathNode.y + (side * Math.floor(rh / 2 + 2));
|
||||
}
|
||||
|
||||
rx = Math.max(1, Math.min(width - rw - 1, rx));
|
||||
ry = Math.max(1, Math.min(height - rh - 1, ry));
|
||||
|
||||
const room = { x: rx, y: ry, width: rw, height: rh };
|
||||
|
||||
// Overlap check
|
||||
const overlap = rooms.some(r => {
|
||||
return !(room.x + room.width < r.x - 1 ||
|
||||
room.x > r.x + r.width + 1 ||
|
||||
room.y + room.height < r.y - 1 ||
|
||||
room.y > r.y + r.height + 1);
|
||||
});
|
||||
|
||||
if (overlap) continue;
|
||||
|
||||
// Dig room interior
|
||||
for (let y = ry + 1; y < ry + rh - 1; y++) {
|
||||
for (let x = rx + 1; x < rx + rw - 1; x++) {
|
||||
tiles[y * width + x] = TileType.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
// Connect room to path node
|
||||
digCorridor(width, tiles, pathNode.x, pathNode.y, rx + Math.floor(rw / 2), ry + Math.floor(rh / 2));
|
||||
|
||||
// Door at entrance
|
||||
let ex = rx + Math.floor(rw / 2);
|
||||
let ey = ry + (pathNode.y <= ry ? 0 : rh - 1);
|
||||
if (Math.abs(pathNode.x - (rx + rw / 2)) > Math.abs(pathNode.y - (ry + rh / 2))) {
|
||||
ex = (pathNode.x <= rx ? 0 : rw - 1) + rx;
|
||||
ey = ry + Math.floor(rh / 2);
|
||||
}
|
||||
tiles[ey * width + ex] = TileType.DOOR_CLOSED;
|
||||
|
||||
rooms.push(room);
|
||||
}
|
||||
|
||||
// Place visual exit at track end
|
||||
const lastNode = trackPath[trackPath.length - 1];
|
||||
tiles[lastNode.y * width + lastNode.x] = TileType.EXIT;
|
||||
|
||||
return { rooms, trackPath };
|
||||
}
|
||||
|
||||
function decorate(width: number, height: number, tiles: Tile[], random: () => number, exit: Vec2): void {
|
||||
const world = { width, height };
|
||||
function digCorridor(width: number, tiles: Tile[], x1: number, y1: number, x2: number, y2: number) {
|
||||
let currX = x1;
|
||||
let currY = y1;
|
||||
|
||||
// Set exit tile
|
||||
tiles[idx(world as any, exit.x, exit.y)] = TileType.EXIT;
|
||||
while (currX !== x2 || currY !== y2) {
|
||||
if (currX !== x2) {
|
||||
currX += x2 > currX ? 1 : -1;
|
||||
} else if (currY !== y2) {
|
||||
currY += y2 > currY ? 1 : -1;
|
||||
}
|
||||
tiles[currY * width + currX] = TileType.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function decorate(width: number, height: number, tiles: Tile[], random: () => number, _exit: Vec2): void {
|
||||
const world = { width, height };
|
||||
// Stairs removed as per user request
|
||||
|
||||
// Use Simplex noise for natural-looking grass distribution
|
||||
const grassNoise = new ROT.Noise.Simplex();
|
||||
@@ -352,7 +337,6 @@ function decorate(width: number, height: number, tiles: Tile[], random: () => nu
|
||||
if (decoValue > 0.5) {
|
||||
tiles[i] = TileType.EMPTY_DECO;
|
||||
} else if (decoValue > 0.3 && random() < 0.3) {
|
||||
// Sparse decorations at medium noise levels
|
||||
tiles[i] = TileType.EMPTY_DECO;
|
||||
}
|
||||
}
|
||||
@@ -375,11 +359,20 @@ function decorate(width: number, height: number, tiles: Tile[], random: () => nu
|
||||
}
|
||||
}
|
||||
|
||||
function placeEnemies(floor: number, rooms: Room[], ecsWorld: ECSWorld, random: () => number): void {
|
||||
function placeEnemies(
|
||||
floor: number,
|
||||
rooms: Room[],
|
||||
ecsWorld: ECSWorld,
|
||||
tiles: Tile[],
|
||||
width: number,
|
||||
random: () => number,
|
||||
occupiedPositions: Set<string>
|
||||
): void {
|
||||
const numEnemies = GAME_CONFIG.enemyScaling.baseCount + floor * GAME_CONFIG.enemyScaling.baseCountPerFloor;
|
||||
|
||||
const enemyTypes = Object.keys(GAME_CONFIG.enemies);
|
||||
const occupiedPositions = new Set<string>();
|
||||
|
||||
if (rooms.length < 2) return;
|
||||
|
||||
for (let i = 0; i < numEnemies; i++) {
|
||||
// Pick a random room (not the starting room 0)
|
||||
@@ -392,8 +385,12 @@ function placeEnemies(floor: number, rooms: Room[], ecsWorld: ECSWorld, random:
|
||||
const ex = room.x + 1 + Math.floor(random() * (room.width - 2));
|
||||
const ey = room.y + 1 + Math.floor(random() * (room.height - 2));
|
||||
const k = `${ex},${ey}`;
|
||||
const tileIdx = ey * width + ex;
|
||||
const isFloor = tiles[tileIdx] === TileType.EMPTY ||
|
||||
tiles[tileIdx] === TileType.EMPTY_DECO ||
|
||||
tiles[tileIdx] === TileType.GRASS_SAPLINGS;
|
||||
|
||||
if (!occupiedPositions.has(k)) {
|
||||
if (isFloor && !occupiedPositions.has(k)) {
|
||||
const type = enemyTypes[Math.floor(random() * enemyTypes.length)] as keyof typeof GAME_CONFIG.enemies;
|
||||
const enemyDef = GAME_CONFIG.enemies[type];
|
||||
|
||||
@@ -444,6 +441,8 @@ function placeTraps(
|
||||
const maxTraps = minTraps + 2;
|
||||
const numTraps = minTraps + Math.floor(random() * (maxTraps - minTraps + 1));
|
||||
|
||||
if (rooms.length < 2) return;
|
||||
|
||||
for (let i = 0; i < numTraps; i++) {
|
||||
// Pick a random room (not the starting room)
|
||||
const roomIdx = 1 + Math.floor(random() * (rooms.length - 1));
|
||||
@@ -491,39 +490,3 @@ function placeTraps(
|
||||
|
||||
export const makeTestWorld = generateWorld;
|
||||
|
||||
function placeDoors(width: number, height: number, tiles: Tile[], rooms: Room[], random: () => number): void {
|
||||
const checkAndPlaceDoor = (x: number, y: number) => {
|
||||
const i = idx({ width, height } as any, x, y);
|
||||
if (tiles[i] === TileType.EMPTY) {
|
||||
// Found a connection (floor tile on perimeter)
|
||||
|
||||
// 50% chance to place a door
|
||||
if (random() < 0.5) {
|
||||
// 90% chance for closed door, 10% for open
|
||||
tiles[i] = random() < 0.9 ? TileType.DOOR_CLOSED : TileType.DOOR_OPEN;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const room of rooms) {
|
||||
// Scan top and bottom walls
|
||||
const topY = room.y - 1;
|
||||
const bottomY = room.y + room.height;
|
||||
|
||||
// Scan horizontal perimeters (iterate x from left-1 to right+1 to cover corners too if needed,
|
||||
// but usually doors are in the middle segments. Let's cover the full range adjacent to room.)
|
||||
for (let x = room.x; x < room.x + room.width; x++) {
|
||||
if (topY >= 0) checkAndPlaceDoor(x, topY);
|
||||
if (bottomY < height) checkAndPlaceDoor(x, bottomY);
|
||||
}
|
||||
|
||||
// Scan left and right walls
|
||||
const leftX = room.x - 1;
|
||||
const rightX = room.x + room.width;
|
||||
|
||||
for (let y = room.y; y < room.y + room.height; y++) {
|
||||
if (leftX >= 0) checkAndPlaceDoor(leftX, y);
|
||||
if (rightX < width) checkAndPlaceDoor(rightX, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function isBlockingTile(w: World, x: number, y: number): boolean {
|
||||
|
||||
export function tryDestructTile(w: World, x: number, y: number): boolean {
|
||||
if (!inBounds(w, x, y)) return false;
|
||||
|
||||
|
||||
const i = idx(w, x, y);
|
||||
const tile = w.tiles[i];
|
||||
|
||||
@@ -48,8 +48,3 @@ export function isBlocked(w: World, x: number, y: number, accessor: EntityAccess
|
||||
|
||||
|
||||
|
||||
export function isPlayerOnExit(w: World, accessor: EntityAccessor): boolean {
|
||||
const p = accessor.getPlayer();
|
||||
if (!p) return false;
|
||||
return p.pos.x === w.exit.x && p.pos.y === w.exit.y;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export class DungeonRenderer {
|
||||
private entityAccessor!: EntityAccessor;
|
||||
private ecsWorld!: ECSWorld;
|
||||
private trapSprites: Map<number, Phaser.GameObjects.Sprite> = new Map();
|
||||
private trackSprites: Phaser.GameObjects.Sprite[] = [];
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
this.scene = scene;
|
||||
@@ -50,6 +51,12 @@ export class DungeonRenderer {
|
||||
}
|
||||
this.trapSprites.clear();
|
||||
|
||||
for (const sprite of this.trackSprites) {
|
||||
sprite.destroy();
|
||||
}
|
||||
this.trackSprites = [];
|
||||
this.trapSprites.clear();
|
||||
|
||||
for (const [, sprite] of this.enemySprites) {
|
||||
sprite.destroy();
|
||||
}
|
||||
@@ -68,22 +75,39 @@ export class DungeonRenderer {
|
||||
// Setup Tilemap
|
||||
if (this.map) this.map.destroy();
|
||||
this.map = this.scene.make.tilemap({
|
||||
data: Array.from({ length: world.height }, (_, y) =>
|
||||
Array.from({ length: world.width }, (_, x) => this.world.tiles[idx(this.world, x, y)])
|
||||
),
|
||||
tileWidth: 16,
|
||||
tileHeight: 16
|
||||
tileWidth: TILE_SIZE,
|
||||
tileHeight: TILE_SIZE,
|
||||
width: world.width,
|
||||
height: world.height
|
||||
});
|
||||
|
||||
const tileset = this.map.addTilesetImage("dungeon", "dungeon", 16, 16, 0, 0)!;
|
||||
this.layer = this.map.createLayer(0, tileset, 0, 0)!;
|
||||
this.layer.setDepth(0);
|
||||
const tileset = this.map.addTilesetImage("dungeon", "dungeon");
|
||||
if (!tileset) {
|
||||
console.error("[DungeonRenderer] FAILED to load tileset 'dungeon'!");
|
||||
// Fallback or throw?
|
||||
}
|
||||
|
||||
// Initial tile states (hidden)
|
||||
this.layer.forEachTile(tile => {
|
||||
tile.setVisible(false);
|
||||
});
|
||||
this.layer = this.map.createBlankLayer("floor", tileset || "dungeon")!;
|
||||
if (this.layer) {
|
||||
this.layer.setDepth(0);
|
||||
this.layer.setVisible(true);
|
||||
console.log(`[DungeonRenderer] Layer created. Size: ${world.width}x${world.height}`);
|
||||
} else {
|
||||
console.error("[DungeonRenderer] FAILED to create tilemap layer!");
|
||||
}
|
||||
|
||||
let tilesPlaced = 0;
|
||||
for (let y = 0; y < world.height; y++) {
|
||||
for (let x = 0; x < world.width; x++) {
|
||||
const i = y * world.width + x;
|
||||
const tile = world.tiles[i];
|
||||
if (tile !== undefined && this.layer) {
|
||||
this.layer.putTileAt(tile, x, y);
|
||||
tilesPlaced++;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`[DungeonRenderer] Placed ${tilesPlaced} tiles.`);
|
||||
this.fxRenderer.clearCorpses();
|
||||
|
||||
// Ensure player sprite exists
|
||||
@@ -111,25 +135,41 @@ export class DungeonRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// Create sprites for ECS trap entities
|
||||
// Create sprites for ECS entities with sprites (traps, mine carts, etc.)
|
||||
if (this.ecsWorld) {
|
||||
const traps = this.ecsWorld.getEntitiesWith("trigger", "position", "sprite");
|
||||
for (const trapId of traps) {
|
||||
const pos = this.ecsWorld.getComponent(trapId, "position");
|
||||
const spriteData = this.ecsWorld.getComponent(trapId, "sprite");
|
||||
console.log(`[DungeonRenderer] Creating ECS sprites...`);
|
||||
const spriteEntities = this.ecsWorld.getEntitiesWith("position", "sprite");
|
||||
for (const entId of spriteEntities) {
|
||||
// Skip player as it's handled separately
|
||||
const player = this.ecsWorld.getComponent(entId, "player");
|
||||
if (player) continue;
|
||||
|
||||
const pos = this.ecsWorld.getComponent(entId, "position");
|
||||
const spriteData = this.ecsWorld.getComponent(entId, "sprite");
|
||||
if (pos && spriteData) {
|
||||
const sprite = this.scene.add.sprite(
|
||||
pos.x * TILE_SIZE + TILE_SIZE / 2,
|
||||
pos.y * TILE_SIZE + TILE_SIZE / 2,
|
||||
spriteData.texture,
|
||||
spriteData.index
|
||||
);
|
||||
sprite.setDepth(5); // Below actors, above floor
|
||||
sprite.setVisible(false); // Hidden until FOV reveals
|
||||
this.trapSprites.set(trapId, sprite);
|
||||
try {
|
||||
const isStandalone = spriteData.texture === "mine_cart" || spriteData.texture === "ceramic_dragon_head";
|
||||
const sprite = this.scene.add.sprite(
|
||||
pos.x * TILE_SIZE + TILE_SIZE / 2,
|
||||
pos.y * TILE_SIZE + TILE_SIZE / 2,
|
||||
spriteData.texture,
|
||||
isStandalone ? undefined : (spriteData.index ?? 0)
|
||||
);
|
||||
sprite.setDepth(5);
|
||||
sprite.setVisible(true); // Force visible for diagnostics
|
||||
sprite.setAlpha(1.0); // Force opaque for diagnostics
|
||||
sprite.setDisplaySize(TILE_SIZE, TILE_SIZE);
|
||||
console.log(`[DungeonRenderer] Created sprite for ${spriteData.texture} at ${pos.x},${pos.y}`);
|
||||
this.trapSprites.set(entId, sprite);
|
||||
} catch (e) {
|
||||
console.error(`[DungeonRenderer] Failed to create sprite for entity ${entId}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render static tracks
|
||||
this.renderTracks();
|
||||
}
|
||||
|
||||
|
||||
@@ -163,9 +203,16 @@ export class DungeonRenderer {
|
||||
return this.fovManager.seenArray;
|
||||
}
|
||||
|
||||
private firstRender = true;
|
||||
|
||||
render(_playerPath: Vec2[]) {
|
||||
if (!this.world || !this.layer) return;
|
||||
|
||||
if (this.firstRender) {
|
||||
console.log(`[DungeonRenderer] First render call... World: ${this.world.width}x${this.world.height}`);
|
||||
this.firstRender = false;
|
||||
}
|
||||
|
||||
const seen = this.fovManager.seenArray;
|
||||
const visible = this.fovManager.visibleArray;
|
||||
|
||||
@@ -217,6 +264,18 @@ export class DungeonRenderer {
|
||||
}
|
||||
});
|
||||
|
||||
// Update track sprites visibility
|
||||
for (const sprite of this.trackSprites) {
|
||||
const tx = Math.floor(sprite.x / TILE_SIZE);
|
||||
const ty = Math.floor(sprite.y / TILE_SIZE);
|
||||
const i = idx(this.world, tx, ty);
|
||||
const isSeen = seen[i] === 1;
|
||||
const isVis = visible[i] === 1;
|
||||
|
||||
sprite.setVisible(isSeen);
|
||||
sprite.alpha = isVis ? 1.0 : 0.3;
|
||||
}
|
||||
|
||||
// Update trap sprites visibility and appearance
|
||||
if (this.ecsWorld) {
|
||||
for (const [trapId, sprite] of this.trapSprites) {
|
||||
@@ -224,14 +283,43 @@ export class DungeonRenderer {
|
||||
const spriteData = this.ecsWorld.getComponent(trapId, "sprite");
|
||||
|
||||
if (pos && spriteData) {
|
||||
// Bounds check
|
||||
if (pos.x < 0 || pos.x >= this.world.width || pos.y < 0 || pos.y >= this.world.height) {
|
||||
sprite.setVisible(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
const i = idx(this.world, pos.x, pos.y);
|
||||
const isSeen = seen[i] === 1;
|
||||
const isVis = visible[i] === 1;
|
||||
|
||||
sprite.setVisible(isSeen);
|
||||
|
||||
// Update position (with simple smoothing)
|
||||
const targetX = pos.x * TILE_SIZE + TILE_SIZE / 2;
|
||||
const targetY = pos.y * TILE_SIZE + TILE_SIZE / 2;
|
||||
|
||||
if (sprite.x !== targetX || sprite.y !== targetY) {
|
||||
// Check if it's far away (teleport) or nearby (tween)
|
||||
const dist = Phaser.Math.Distance.Between(sprite.x, sprite.y, targetX, targetY);
|
||||
if (dist > TILE_SIZE * 2) {
|
||||
this.scene.tweens.killTweensOf(sprite);
|
||||
sprite.setPosition(targetX, targetY);
|
||||
} else if (!this.scene.tweens.isTweening(sprite)) {
|
||||
this.scene.tweens.add({
|
||||
targets: sprite,
|
||||
x: targetX,
|
||||
y: targetY,
|
||||
duration: GAME_CONFIG.rendering.moveDuration,
|
||||
ease: 'Power1'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update sprite frame in case trap was triggered
|
||||
if (sprite.frame.name !== String(spriteData.index)) {
|
||||
const isStandalone = spriteData.texture === "mine_cart" || spriteData.texture === "ceramic_dragon_head";
|
||||
if (!isStandalone && sprite.frame.name !== String(spriteData.index)) {
|
||||
sprite.setFrame(spriteData.index);
|
||||
}
|
||||
|
||||
@@ -491,13 +579,8 @@ export class DungeonRenderer {
|
||||
? this.scene.add.sprite(startX, startY, texture)
|
||||
: this.scene.add.sprite(startX, startY, texture, frame);
|
||||
|
||||
// Scale for standalone 24x24 image should be 1.0 (or matching world scale)
|
||||
// Other sprites are 16x16.
|
||||
if (isStandalone) {
|
||||
sprite.setDisplaySize(16, 16);
|
||||
} else {
|
||||
sprite.setScale(1.0);
|
||||
}
|
||||
// Ensure all sprites fit in a single 16x16 tile.
|
||||
sprite.setDisplaySize(TILE_SIZE, TILE_SIZE);
|
||||
|
||||
sprite.setDepth(2000);
|
||||
|
||||
@@ -526,4 +609,59 @@ export class DungeonRenderer {
|
||||
shakeCamera() {
|
||||
this.scene.cameras.main.shake(100, 0.01);
|
||||
}
|
||||
|
||||
private renderTracks() {
|
||||
if (!this.world.trackPath || this.world.trackPath.length === 0) return;
|
||||
|
||||
const path = this.world.trackPath;
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const curr = path[i];
|
||||
const prev = i > 0 ? path[i - 1] : null;
|
||||
const next = i < path.length - 1 ? path[i + 1] : null;
|
||||
|
||||
let spriteKey = "track_straight";
|
||||
let angle = 0;
|
||||
|
||||
if (prev && next) {
|
||||
const dx1 = curr.x - prev.x;
|
||||
const dy1 = curr.y - prev.y;
|
||||
const dx2 = next.x - curr.x;
|
||||
const dy2 = next.y - curr.y;
|
||||
|
||||
if (dx1 === dx2 && dy1 === dy2) {
|
||||
// Straight
|
||||
spriteKey = "track_straight";
|
||||
angle = dx1 === 0 ? 0 : 90; // Asset is vertical (0 deg), rotate to 90 for horizontal
|
||||
} else {
|
||||
// Corner
|
||||
spriteKey = "track_corner";
|
||||
const p = { dx: prev.x - curr.x, dy: prev.y - curr.y };
|
||||
const n = { dx: next.x - curr.x, dy: next.y - curr.y };
|
||||
|
||||
// Top-Right: 180, Right-Bottom: 270, Bottom-Left: 0, Left-Top: 90
|
||||
if ((p.dy === -1 && n.dx === 1) || (n.dy === -1 && p.dx === 1)) angle = 180;
|
||||
else if ((p.dx === 1 && n.dy === 1) || (n.dx === 1 && p.dy === 1)) angle = 270;
|
||||
else if ((p.dy === 1 && n.dx === -1) || (n.dy === 1 && p.dx === -1)) angle = 0;
|
||||
else if ((p.dx === -1 && n.dy === -1) || (n.dx === -1 && p.dy === -1)) angle = 90;
|
||||
}
|
||||
} else if (next) {
|
||||
spriteKey = "track_straight";
|
||||
angle = (next.x === curr.x) ? 0 : 90;
|
||||
} else if (prev) {
|
||||
spriteKey = "track_straight";
|
||||
angle = (prev.x === curr.x) ? 0 : 90;
|
||||
}
|
||||
|
||||
const sprite = this.scene.add.sprite(
|
||||
curr.x * TILE_SIZE + TILE_SIZE / 2,
|
||||
curr.y * TILE_SIZE + TILE_SIZE / 2,
|
||||
spriteKey
|
||||
);
|
||||
sprite.setAngle(angle);
|
||||
sprite.setDisplaySize(TILE_SIZE, TILE_SIZE);
|
||||
sprite.setDepth(2);
|
||||
sprite.setVisible(false);
|
||||
this.trackSprites.push(sprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,12 +141,7 @@ export class FxRenderer {
|
||||
0
|
||||
);
|
||||
corpse.setDepth(50);
|
||||
// Use display size for Priestess sprites to match 1 tile
|
||||
if (textureKey.startsWith("Priestess")) {
|
||||
corpse.setDisplaySize(TILE_SIZE, TILE_SIZE);
|
||||
} else {
|
||||
corpse.setScale(1.0); // Reset to standard scale for spritesheet assets
|
||||
}
|
||||
corpse.setDisplaySize(TILE_SIZE, TILE_SIZE); // All corpses should be tile-sized
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type RangedWeaponItem,
|
||||
} from "../core/types";
|
||||
import { TILE_SIZE } from "../core/constants";
|
||||
import { isBlocked, isPlayerOnExit, tryDestructTile } from "../engine/world/world-logic";
|
||||
import { isBlocked, tryDestructTile } from "../engine/world/world-logic";
|
||||
import { applyAction, stepUntilPlayerTurn } from "../engine/simulation/simulation";
|
||||
import { generateWorld } from "../engine/world/generator";
|
||||
import { DungeonRenderer } from "../rendering/DungeonRenderer";
|
||||
@@ -28,7 +28,9 @@ import { ECSWorld } from "../engine/ecs/World";
|
||||
import { SystemRegistry } from "../engine/ecs/System";
|
||||
import { TriggerSystem } from "../engine/ecs/systems/TriggerSystem";
|
||||
import { StatusEffectSystem, applyStatusEffect } from "../engine/ecs/systems/StatusEffectSystem";
|
||||
import { MineCartSystem } from "../engine/ecs/systems/MineCartSystem";
|
||||
import { TileType } from "../core/terrain";
|
||||
|
||||
import { FireSystem } from "../engine/ecs/systems/FireSystem";
|
||||
import { EventBus } from "../engine/ecs/EventBus";
|
||||
import { generateLoot } from "../engine/systems/LootSystem";
|
||||
@@ -87,18 +89,14 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
create() {
|
||||
// this.cursors initialized in GameInput
|
||||
|
||||
|
||||
// Camera
|
||||
this.cameras.main.setBackgroundColor(0x1a1a1a);
|
||||
this.cameras.main.setZoom(GAME_CONFIG.rendering.cameraZoom);
|
||||
this.cameras.main.fadeIn(1000, 0, 0, 0);
|
||||
this.cameras.main.fadeIn(500, 0, 0, 0);
|
||||
|
||||
// Initialize Sub-systems
|
||||
this.dungeonRenderer = new DungeonRenderer(this);
|
||||
this.gameRenderer = new GameRenderer(this.dungeonRenderer);
|
||||
this.cameraController = new CameraController(this.cameras.main);
|
||||
// Note: itemManager is temporary instantiated with undefined entityAccessor until loadFloor
|
||||
this.itemManager = new ItemManager(this.world, this.entityAccessor);
|
||||
this.targetingSystem = new TargetingSystem(this);
|
||||
|
||||
@@ -126,12 +124,21 @@ export class GameScene extends Phaser.Scene {
|
||||
// Load initial floor
|
||||
this.loadFloor(1);
|
||||
|
||||
// Register Handlers
|
||||
this.playerInputHandler.registerListeners();
|
||||
this.gameEventHandler.registerListeners();
|
||||
}
|
||||
|
||||
update() {
|
||||
update(_time: number, _delta: number) {
|
||||
// Handle camera panning via arrow keys
|
||||
const cameraPan = this.gameInput.getCameraPanState();
|
||||
if (cameraPan.dx !== 0 || cameraPan.dy !== 0) {
|
||||
// Sensitivity factor for smooth panning
|
||||
// Note: we invert cameraPan dx/dy because handlePan subtracts from scroll (standard for drag)
|
||||
// but for keys we want to move "in the direction" of the key.
|
||||
const panSpeed = 12;
|
||||
this.cameraController.handlePan(-cameraPan.dx * panSpeed, -cameraPan.dy * panSpeed);
|
||||
}
|
||||
|
||||
if (!this.awaitingPlayer) return;
|
||||
if (this.isMenuOpen || this.isInventoryOpen || this.isCharacterOpen || this.dungeonRenderer.isMinimapVisible()) return;
|
||||
|
||||
@@ -189,6 +196,10 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
public emitUIUpdate() {
|
||||
if (!this.entityAccessor) {
|
||||
console.warn("[GameScene] emitUIUpdate called before entityAccessor was initialized.");
|
||||
return;
|
||||
}
|
||||
const payload: UIUpdatePayload = {
|
||||
world: this.world,
|
||||
playerId: this.playerId,
|
||||
@@ -209,7 +220,6 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
this.awaitingPlayer = false;
|
||||
this.cameraController.enableFollowMode();
|
||||
|
||||
// Process reloading progress
|
||||
const player = this.entityAccessor.getPlayer();
|
||||
@@ -358,13 +368,6 @@ export class GameScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPlayerOnExit(this.world, this.entityAccessor)) {
|
||||
this.syncRunStateFromPlayer();
|
||||
this.floorIndex++;
|
||||
this.loadFloor(this.floorIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dungeonRenderer.computeFov();
|
||||
if (this.cameraController.isFollowing) {
|
||||
const player = this.entityAccessor.getPlayer();
|
||||
@@ -377,54 +380,97 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private loadFloor(floor: number) {
|
||||
this.floorIndex = floor;
|
||||
this.cameraController.enableFollowMode();
|
||||
try {
|
||||
console.log(`[GameScene] loadFloor started for floor ${floor}`);
|
||||
this.floorIndex = floor;
|
||||
|
||||
const { world, playerId, ecsWorld } = generateWorld(floor, this.runState);
|
||||
this.world = world;
|
||||
this.playerId = playerId;
|
||||
console.log(`[GameScene] Calling generateWorld...`);
|
||||
const { world, playerId, ecsWorld } = generateWorld(floor, this.runState);
|
||||
console.log(`[GameScene] World generated. Width: ${world.width}, Height: ${world.height}`);
|
||||
|
||||
// Initialize or update entity accessor
|
||||
if (!this.entityAccessor) {
|
||||
this.entityAccessor = new EntityAccessor(this.world, this.playerId, ecsWorld);
|
||||
} else {
|
||||
this.entityAccessor.updateWorld(this.world, this.playerId, ecsWorld);
|
||||
// DIAGNOSTIC: Count tiles
|
||||
const counts: Record<number, number> = {};
|
||||
world.tiles.forEach(t => counts[t] = (counts[t] || 0) + 1);
|
||||
console.log(`[GameScene] Tile counts:`, counts);
|
||||
console.log(`[GameScene] Exit position:`, world.exit);
|
||||
|
||||
this.world = world;
|
||||
this.playerId = playerId;
|
||||
|
||||
// Initialize or update entity accessor
|
||||
console.log(`[GameScene] Setting up EntityAccessor...`);
|
||||
if (!this.entityAccessor) {
|
||||
this.entityAccessor = new EntityAccessor(this.world, this.playerId, ecsWorld);
|
||||
} else {
|
||||
this.entityAccessor.updateWorld(this.world, this.playerId, ecsWorld);
|
||||
}
|
||||
|
||||
console.log(`[GameScene] Updating ItemManager...`);
|
||||
this.itemManager.updateWorld(this.world, this.entityAccessor, ecsWorld);
|
||||
|
||||
// Initialize ECS for traps and status effects
|
||||
this.ecsWorld = ecsWorld;
|
||||
this.ecsEventBus = new EventBus();
|
||||
|
||||
// Handle level completion
|
||||
this.ecsEventBus.on("mission_complete", () => {
|
||||
console.log("[GameScene] Mission Complete! Loading next floor...");
|
||||
this.syncRunStateFromPlayer();
|
||||
this.loadFloor(this.floorIndex + 1);
|
||||
});
|
||||
// Register systems
|
||||
console.log(`[GameScene] Registering ECS systems...`);
|
||||
this.ecsRegistry = new SystemRegistry(this.ecsWorld, this.ecsEventBus);
|
||||
this.ecsRegistry.register(new TriggerSystem());
|
||||
this.ecsRegistry.register(new StatusEffectSystem());
|
||||
this.ecsRegistry.register(new MineCartSystem());
|
||||
this.ecsRegistry.register(new FireSystem(this.world));
|
||||
|
||||
console.log(`[GameScene] ECS systems registered.`);
|
||||
|
||||
this.playerPath = [];
|
||||
this.awaitingPlayer = false;
|
||||
|
||||
const paddingX = this.world.width * TILE_SIZE;
|
||||
const paddingY = this.world.height * TILE_SIZE;
|
||||
this.cameraController.setBounds(
|
||||
-paddingX,
|
||||
-paddingY,
|
||||
this.world.width * TILE_SIZE + paddingX * 2,
|
||||
this.world.height * TILE_SIZE + paddingY * 2
|
||||
);
|
||||
|
||||
console.log(`[GameScene] Initializing floor renderer...`);
|
||||
this.dungeonRenderer.initializeFloor(this.world, this.ecsWorld, this.entityAccessor);
|
||||
|
||||
this.cameras.main.fadeIn(500, 0, 0, 0); // Force fade in
|
||||
|
||||
console.log(`[GameScene] Stepping simulation until player turn...`);
|
||||
const enemyStep = stepUntilPlayerTurn(this.world, this.playerId, this.entityAccessor);
|
||||
this.awaitingPlayer = enemyStep.awaitingPlayerId === this.playerId;
|
||||
|
||||
console.log(`[GameScene] Computing FOV...`);
|
||||
this.dungeonRenderer.computeFov();
|
||||
const p = this.entityAccessor.getPlayer();
|
||||
if (p) {
|
||||
console.log(`[GameScene] Centering camera on player at ${p.pos.x},${p.pos.y}`);
|
||||
this.cameraController.centerOnTile(p.pos.x, p.pos.y);
|
||||
} else {
|
||||
console.error(`[GameScene] Player not found for camera centering!`);
|
||||
}
|
||||
|
||||
console.log(`[GameScene] Triggering first render...`);
|
||||
this.dungeonRenderer.render(this.playerPath);
|
||||
this.emitUIUpdate();
|
||||
console.log(`[GameScene] loadFloor complete.`);
|
||||
} catch (e) {
|
||||
console.error(`[GameScene] CRITICAL ERROR in loadFloor:`, e);
|
||||
// Fallback: reveal everything if possible?
|
||||
this.cameras.main.setBackgroundColor(0xff0000); // Red screen of death
|
||||
}
|
||||
|
||||
this.itemManager.updateWorld(this.world, this.entityAccessor, ecsWorld);
|
||||
|
||||
// Initialize ECS for traps and status effects
|
||||
this.ecsWorld = ecsWorld;
|
||||
this.ecsEventBus = new EventBus();
|
||||
// Register systems
|
||||
this.ecsRegistry = new SystemRegistry(this.ecsWorld, this.ecsEventBus);
|
||||
this.ecsRegistry.register(new TriggerSystem());
|
||||
this.ecsRegistry.register(new StatusEffectSystem());
|
||||
this.ecsRegistry.register(new FireSystem(this.world));
|
||||
|
||||
// NOTE: Entities are synced to ECS via EntityAccessor which bridges the World state.
|
||||
// No need to manually add player here anymore.
|
||||
|
||||
this.playerPath = [];
|
||||
this.awaitingPlayer = false;
|
||||
|
||||
this.cameraController.setBounds(0, 0, this.world.width * TILE_SIZE, this.world.height * TILE_SIZE);
|
||||
|
||||
this.dungeonRenderer.initializeFloor(this.world, this.ecsWorld, this.entityAccessor);
|
||||
|
||||
const enemyStep = stepUntilPlayerTurn(this.world, this.playerId, this.entityAccessor);
|
||||
this.awaitingPlayer = enemyStep.awaitingPlayerId === this.playerId;
|
||||
|
||||
|
||||
this.dungeonRenderer.computeFov();
|
||||
const p = this.entityAccessor.getPlayer();
|
||||
if (p) {
|
||||
this.cameraController.centerOnTile(p.pos.x, p.pos.y);
|
||||
}
|
||||
this.dungeonRenderer.render(this.playerPath);
|
||||
this.emitUIUpdate();
|
||||
}
|
||||
|
||||
|
||||
public syncRunStateFromPlayer() {
|
||||
const p = this.entityAccessor.getPlayer();
|
||||
if (!p || !p.stats || !p.inventory) return;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||
*/
|
||||
export class CameraController {
|
||||
private camera: Phaser.Cameras.Scene2D.Camera;
|
||||
private followMode: boolean = true;
|
||||
private followMode: boolean = false;
|
||||
|
||||
constructor(camera: Phaser.Cameras.Scene2D.Camera) {
|
||||
this.camera = camera;
|
||||
|
||||
@@ -137,8 +137,6 @@ export class PlayerInputHandler {
|
||||
// Movement Click
|
||||
if (button !== 0) return;
|
||||
|
||||
this.scene.cameraController.enableFollowMode();
|
||||
|
||||
if (!this.scene.awaitingPlayer) return;
|
||||
if (this.scene.isMenuOpen || this.scene.isInventoryOpen || this.scene.dungeonRenderer.isMinimapVisible()) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user