Use wall + floor assets from Pixel dungeon
This commit is contained in:
BIN
public/assets/tiles0.png
Normal file
BIN
public/assets/tiles0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -30,7 +30,7 @@ export const GAME_CONFIG = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
rendering: {
|
rendering: {
|
||||||
tileSize: 24,
|
tileSize: 16,
|
||||||
cameraZoom: 2,
|
cameraZoom: 2,
|
||||||
wallColor: 0x2b2b2b,
|
wallColor: 0x2b2b2b,
|
||||||
floorColor: 0x161616,
|
floorColor: 0x161616,
|
||||||
@@ -45,6 +45,15 @@ export const GAME_CONFIG = {
|
|||||||
visibleStrengthFactor: 0.65
|
visibleStrengthFactor: 0.65
|
||||||
},
|
},
|
||||||
|
|
||||||
|
terrain: {
|
||||||
|
empty: 1,
|
||||||
|
wall: 4,
|
||||||
|
water: 63,
|
||||||
|
emptyDeco: 24,
|
||||||
|
wallDeco: 12,
|
||||||
|
exit: 8
|
||||||
|
},
|
||||||
|
|
||||||
ui: {
|
ui: {
|
||||||
minimapPanelWidth: 340,
|
minimapPanelWidth: 340,
|
||||||
minimapPanelHeight: 220,
|
minimapPanelHeight: 220,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export type EntityId = number;
|
|||||||
|
|
||||||
export type Vec2 = { x: number; y: number };
|
export type Vec2 = { x: number; y: number };
|
||||||
|
|
||||||
export type Tile = 0 | 1; // 0 = floor, 1 = wall
|
export type Tile = number;
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
| { type: "move"; dx: number; dy: number }
|
| { type: "move"; dx: number; dy: number }
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface Room {
|
|||||||
export function generateWorld(level: number, runState: RunState): { world: World; playerId: EntityId } {
|
export function generateWorld(level: number, runState: RunState): { world: World; playerId: EntityId } {
|
||||||
const width = GAME_CONFIG.map.width;
|
const width = GAME_CONFIG.map.width;
|
||||||
const height = GAME_CONFIG.map.height;
|
const height = GAME_CONFIG.map.height;
|
||||||
const tiles: Tile[] = new Array(width * height).fill(1); // Start with all walls
|
const tiles: Tile[] = new Array(width * height).fill(GAME_CONFIG.terrain.wall); // Start with all walls
|
||||||
|
|
||||||
const random = seededRandom(level * 12345);
|
const random = seededRandom(level * 12345);
|
||||||
|
|
||||||
@@ -52,6 +52,8 @@ export function generateWorld(level: number, runState: RunState): { world: World
|
|||||||
|
|
||||||
placeEnemies(level, rooms, actors, random);
|
placeEnemies(level, rooms, actors, random);
|
||||||
|
|
||||||
|
decorate(width, height, tiles, random, exit);
|
||||||
|
|
||||||
return { world: { width, height, tiles, actors, exit }, playerId };
|
return { world: { width, height, tiles, actors, exit }, playerId };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +101,7 @@ function doesOverlap(newRoom: Room, rooms: Room[]): boolean {
|
|||||||
function carveRoom(room: Room, tiles: Tile[], world: any): void {
|
function carveRoom(room: Room, tiles: Tile[], world: any): void {
|
||||||
for (let x = room.x; x < room.x + room.width; x++) {
|
for (let x = room.x; x < room.x + room.width; x++) {
|
||||||
for (let y = room.y; y < room.y + room.height; y++) {
|
for (let y = room.y; y < room.y + room.height; y++) {
|
||||||
tiles[idx(world, x, y)] = 0;
|
tiles[idx(world, x, y)] = GAME_CONFIG.terrain.empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,22 +115,95 @@ function carveCorridor(room1: Room, room2: Room, tiles: Tile[], world: any, rand
|
|||||||
if (random() < 0.5) {
|
if (random() < 0.5) {
|
||||||
// Horizontal then vertical
|
// Horizontal then vertical
|
||||||
for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) {
|
for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) {
|
||||||
tiles[idx(world, x, y1)] = 0;
|
tiles[idx(world, x, y1)] = GAME_CONFIG.terrain.empty;
|
||||||
}
|
}
|
||||||
for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) {
|
for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) {
|
||||||
tiles[idx(world, x2, y)] = 0;
|
tiles[idx(world, x2, y)] = GAME_CONFIG.terrain.empty;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Vertical then horizontal
|
// Vertical then horizontal
|
||||||
for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) {
|
for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) {
|
||||||
tiles[idx(world, x1, y)] = 0;
|
tiles[idx(world, x1, y)] = GAME_CONFIG.terrain.empty;
|
||||||
}
|
}
|
||||||
for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) {
|
for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) {
|
||||||
tiles[idx(world, x, y2)] = 0;
|
tiles[idx(world, x, y2)] = GAME_CONFIG.terrain.empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function decorate(width: number, height: number, tiles: Tile[], random: () => number, exit: Vec2): void {
|
||||||
|
const world = { width, height };
|
||||||
|
|
||||||
|
// Set exit tile
|
||||||
|
tiles[idx(world as any, exit.x, exit.y)] = GAME_CONFIG.terrain.exit;
|
||||||
|
|
||||||
|
// Add water patches (similar to PD Sewers)
|
||||||
|
const waterMask = generatePatch(width, height, 0.45, 5, random);
|
||||||
|
for (let i = 0; i < tiles.length; i++) {
|
||||||
|
if (tiles[i] === GAME_CONFIG.terrain.empty && waterMask[i]) {
|
||||||
|
tiles[i] = GAME_CONFIG.terrain.water;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wall decorations
|
||||||
|
for (let y = 0; y < height - 1; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const i = idx(world as any, x, y);
|
||||||
|
const nextY = idx(world as any, x, y + 1);
|
||||||
|
|
||||||
|
if (tiles[i] === GAME_CONFIG.terrain.wall &&
|
||||||
|
tiles[nextY] === GAME_CONFIG.terrain.water &&
|
||||||
|
random() < 0.25) {
|
||||||
|
tiles[i] = GAME_CONFIG.terrain.wallDeco;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floor decorations (moss)
|
||||||
|
for (let y = 1; y < height - 1; y++) {
|
||||||
|
for (let x = 1; x < width - 1; x++) {
|
||||||
|
const i = idx(world as any, x, y);
|
||||||
|
if (tiles[i] === GAME_CONFIG.terrain.empty) {
|
||||||
|
let wallCount = 0;
|
||||||
|
if (tiles[idx(world as any, x + 1, y)] === GAME_CONFIG.terrain.wall) wallCount++;
|
||||||
|
if (tiles[idx(world as any, x - 1, y)] === GAME_CONFIG.terrain.wall) wallCount++;
|
||||||
|
if (tiles[idx(world as any, x, y + 1)] === GAME_CONFIG.terrain.wall) wallCount++;
|
||||||
|
if (tiles[idx(world as any, x, y - 1)] === GAME_CONFIG.terrain.wall) wallCount++;
|
||||||
|
|
||||||
|
if (random() * 16 < wallCount * wallCount) {
|
||||||
|
tiles[i] = GAME_CONFIG.terrain.emptyDeco;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple cellular automata for generating patches of terrain
|
||||||
|
*/
|
||||||
|
function generatePatch(width: number, height: number, fillChance: number, iterations: number, random: () => number): boolean[] {
|
||||||
|
let map = new Array(width * height).fill(false).map(() => random() < fillChance);
|
||||||
|
|
||||||
|
for (let step = 0; step < iterations; step++) {
|
||||||
|
const nextMap = new Array(width * height).fill(false);
|
||||||
|
for (let y = 1; y < height - 1; y++) {
|
||||||
|
for (let x = 1; x < width - 1; x++) {
|
||||||
|
let neighbors = 0;
|
||||||
|
for (let dy = -1; dy <= 1; dy++) {
|
||||||
|
for (let dx = -1; dx <= 1; dx++) {
|
||||||
|
if (map[(y + dy) * width + (x + dx)]) neighbors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (neighbors > 4) nextMap[y * width + x] = true;
|
||||||
|
else if (neighbors < 4) nextMap[y * width + x] = false;
|
||||||
|
else nextMap[y * width + x] = map[y * width + x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map = nextMap;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
function placeEnemies(level: number, rooms: Room[], actors: Map<EntityId, Actor>, random: () => number): void {
|
function placeEnemies(level: number, rooms: Room[], actors: Map<EntityId, Actor>, random: () => number): void {
|
||||||
let enemyId = 2;
|
let enemyId = 2;
|
||||||
const numEnemies = GAME_CONFIG.enemy.baseCount + level * GAME_CONFIG.enemy.baseCountPerLevel + Math.floor(random() * GAME_CONFIG.enemy.randomBonus);
|
const numEnemies = GAME_CONFIG.enemy.baseCount + level * GAME_CONFIG.enemy.baseCountPerLevel + Math.floor(random() * GAME_CONFIG.enemy.randomBonus);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { World, EntityId } from "../../core/types";
|
import type { World, EntityId } from "../../core/types";
|
||||||
|
import { GAME_CONFIG } from "../../core/config/GameConfig";
|
||||||
|
|
||||||
export function inBounds(w: World, x: number, y: number): boolean {
|
export function inBounds(w: World, x: number, y: number): boolean {
|
||||||
return x >= 0 && y >= 0 && x < w.width && y < w.height;
|
return x >= 0 && y >= 0 && x < w.width && y < w.height;
|
||||||
@@ -9,7 +10,8 @@ export function idx(w: World, x: number, y: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isWall(w: World, x: number, y: number): boolean {
|
export function isWall(w: World, x: number, y: number): boolean {
|
||||||
return w.tiles[idx(w, x, y)] === 1;
|
const tile = w.tiles[idx(w, x, y)];
|
||||||
|
return tile === GAME_CONFIG.terrain.wall || tile === GAME_CONFIG.terrain.wallDeco;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBlocked(w: World, x: number, y: number): boolean {
|
export function isBlocked(w: World, x: number, y: number): boolean {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { GAME_CONFIG } from "../core/config/GameConfig";
|
|||||||
|
|
||||||
export class DungeonRenderer {
|
export class DungeonRenderer {
|
||||||
private scene: Phaser.Scene;
|
private scene: Phaser.Scene;
|
||||||
private gfx: Phaser.GameObjects.Graphics;
|
private map?: Phaser.Tilemaps.Tilemap;
|
||||||
|
private layer?: Phaser.Tilemaps.TilemapLayer;
|
||||||
|
|
||||||
private playerSprite?: Phaser.GameObjects.Sprite;
|
private playerSprite?: Phaser.GameObjects.Sprite;
|
||||||
private enemySprites: Map<EntityId, Phaser.GameObjects.Sprite> = new Map();
|
private enemySprites: Map<EntityId, Phaser.GameObjects.Sprite> = new Map();
|
||||||
private corpseSprites: Phaser.GameObjects.Sprite[] = [];
|
private corpseSprites: Phaser.GameObjects.Sprite[] = [];
|
||||||
@@ -25,36 +27,28 @@ export class DungeonRenderer {
|
|||||||
private minimapGfx!: Phaser.GameObjects.Graphics;
|
private minimapGfx!: Phaser.GameObjects.Graphics;
|
||||||
private minimapContainer!: Phaser.GameObjects.Container;
|
private minimapContainer!: Phaser.GameObjects.Container;
|
||||||
private minimapBg!: Phaser.GameObjects.Rectangle;
|
private minimapBg!: Phaser.GameObjects.Rectangle;
|
||||||
private minimapVisible = false; // Off by default
|
private minimapVisible = false;
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene) {
|
constructor(scene: Phaser.Scene) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.gfx = this.scene.add.graphics();
|
|
||||||
|
|
||||||
// Initialize minimap
|
|
||||||
this.initMinimap();
|
this.initMinimap();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initMinimap() {
|
private initMinimap() {
|
||||||
this.minimapContainer = this.scene.add.container(0, 0);
|
this.minimapContainer = this.scene.add.container(0, 0);
|
||||||
this.minimapContainer.setScrollFactor(0); // Fixed to camera
|
this.minimapContainer.setScrollFactor(0);
|
||||||
this.minimapContainer.setDepth(1001); // Same as menu
|
this.minimapContainer.setDepth(1001);
|
||||||
|
|
||||||
// Background panel (like menu)
|
|
||||||
this.minimapBg = this.scene.add
|
this.minimapBg = this.scene.add
|
||||||
.rectangle(0, 0, GAME_CONFIG.ui.minimapPanelWidth, GAME_CONFIG.ui.minimapPanelHeight, 0x000000, 0.8)
|
.rectangle(0, 0, GAME_CONFIG.ui.minimapPanelWidth, GAME_CONFIG.ui.minimapPanelHeight, 0x000000, 0.8)
|
||||||
.setStrokeStyle(1, 0xffffff, 0.9)
|
.setStrokeStyle(1, 0xffffff, 0.9)
|
||||||
.setInteractive(); // Capture clicks
|
.setInteractive();
|
||||||
|
|
||||||
this.minimapGfx = this.scene.add.graphics();
|
this.minimapGfx = this.scene.add.graphics();
|
||||||
|
|
||||||
this.minimapContainer.add(this.minimapBg);
|
this.minimapContainer.add(this.minimapBg);
|
||||||
this.minimapContainer.add(this.minimapGfx);
|
this.minimapContainer.add(this.minimapGfx);
|
||||||
|
|
||||||
// Position in center
|
|
||||||
this.positionMinimap();
|
this.positionMinimap();
|
||||||
|
|
||||||
// Start hidden
|
|
||||||
this.minimapContainer.setVisible(false);
|
this.minimapContainer.setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +58,25 @@ export class DungeonRenderer {
|
|||||||
this.visible = new Uint8Array(this.world.width * this.world.height);
|
this.visible = new Uint8Array(this.world.width * this.world.height);
|
||||||
this.visibleStrength = new Float32Array(this.world.width * this.world.height);
|
this.visibleStrength = new Float32Array(this.world.width * this.world.height);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
|
||||||
|
const tileset = this.map.addTilesetImage("tiles0", "tiles0", 16, 16, 0, 0)!;
|
||||||
|
this.layer = this.map.createLayer(0, tileset, 0, 0)!;
|
||||||
|
this.layer.setDepth(0);
|
||||||
|
|
||||||
|
// Initial tile states (hidden)
|
||||||
|
this.layer.forEachTile(tile => {
|
||||||
|
tile.setVisible(false);
|
||||||
|
});
|
||||||
|
|
||||||
// Clear old corpses
|
// Clear old corpses
|
||||||
for (const sprite of this.corpseSprites) {
|
for (const sprite of this.corpseSprites) {
|
||||||
sprite.destroy();
|
sprite.destroy();
|
||||||
@@ -75,11 +88,10 @@ export class DungeonRenderer {
|
|||||||
this.playerSprite = this.scene.add.sprite(0, 0, "warrior", 0);
|
this.playerSprite = this.scene.add.sprite(0, 0, "warrior", 0);
|
||||||
this.playerSprite.setDepth(100);
|
this.playerSprite.setDepth(100);
|
||||||
|
|
||||||
// Calculate display size to fit within tile while maintaining 12:15 aspect ratio
|
// Calculate scale to fit 15px high sprite into 16px tile
|
||||||
const scale = TILE_SIZE / 15; // Fit height to tile size
|
const scale = 1.0;
|
||||||
this.playerSprite.setScale(scale);
|
this.playerSprite.setScale(scale);
|
||||||
|
|
||||||
// Simple animations from PD source
|
|
||||||
this.scene.anims.create({
|
this.scene.anims.create({
|
||||||
key: 'warrior-idle',
|
key: 'warrior-idle',
|
||||||
frames: this.scene.anims.generateFrameNumbers('warrior', { frames: [0, 0, 0, 1, 0, 0, 1, 1] }),
|
frames: this.scene.anims.generateFrameNumbers('warrior', { frames: [0, 0, 0, 1, 0, 0, 1, 1] }),
|
||||||
@@ -104,7 +116,7 @@ export class DungeonRenderer {
|
|||||||
this.playerSprite.play('warrior-idle');
|
this.playerSprite.play('warrior-idle');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rat animations
|
// Enemy animations
|
||||||
if (!this.scene.anims.exists('rat-idle')) {
|
if (!this.scene.anims.exists('rat-idle')) {
|
||||||
this.scene.anims.create({
|
this.scene.anims.create({
|
||||||
key: 'rat-idle',
|
key: 'rat-idle',
|
||||||
@@ -126,7 +138,6 @@ export class DungeonRenderer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bat animations
|
|
||||||
if (!this.scene.anims.exists('bat-idle')) {
|
if (!this.scene.anims.exists('bat-idle')) {
|
||||||
this.scene.anims.create({
|
this.scene.anims.create({
|
||||||
key: 'bat-idle',
|
key: 'bat-idle',
|
||||||
@@ -153,13 +164,11 @@ export class DungeonRenderer {
|
|||||||
return !isWall(this.world, x, y);
|
return !isWall(this.world, x, y);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Position minimap
|
|
||||||
this.positionMinimap();
|
this.positionMinimap();
|
||||||
}
|
}
|
||||||
|
|
||||||
private positionMinimap() {
|
private positionMinimap() {
|
||||||
const cam = this.scene.cameras.main;
|
const cam = this.scene.cameras.main;
|
||||||
// Center on screen like menu
|
|
||||||
this.minimapContainer.setPosition(cam.width / 2, cam.height / 2);
|
this.minimapContainer.setPosition(cam.width / 2, cam.height / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +196,6 @@ export class DungeonRenderer {
|
|||||||
this.visible[i] = 1;
|
this.visible[i] = 1;
|
||||||
this.seen[i] = 1;
|
this.seen[i] = 1;
|
||||||
|
|
||||||
// falloff: 1 at center, ~0.4 at radius edge
|
|
||||||
const radiusT = Phaser.Math.Clamp(r / GAME_CONFIG.player.viewRadius, 0, 1);
|
const radiusT = Phaser.Math.Clamp(r / GAME_CONFIG.player.viewRadius, 0, 1);
|
||||||
const falloff = 1 - radiusT * 0.6;
|
const falloff = 1 - radiusT * 0.6;
|
||||||
const strength = Phaser.Math.Clamp(v * falloff, 0, 1);
|
const strength = Phaser.Math.Clamp(v * falloff, 0, 1);
|
||||||
@@ -205,67 +213,31 @@ export class DungeonRenderer {
|
|||||||
return this.seen;
|
return this.seen;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(playerPath: Vec2[]) {
|
render(_playerPath: Vec2[]) {
|
||||||
this.gfx.clear();
|
if (!this.world || !this.layer) return;
|
||||||
|
|
||||||
if (!this.world) return;
|
|
||||||
|
|
||||||
// Tiles w/ fog + falloff + silhouettes
|
|
||||||
for (let y = 0; y < this.world.height; y++) {
|
|
||||||
for (let x = 0; x < this.world.width; x++) {
|
|
||||||
const i = idx(this.world, x, y);
|
|
||||||
|
|
||||||
|
// Update Tiles
|
||||||
|
this.layer.forEachTile(tile => {
|
||||||
|
const i = idx(this.world, tile.x, tile.y);
|
||||||
const isSeen = this.seen[i] === 1;
|
const isSeen = this.seen[i] === 1;
|
||||||
const isVis = this.visible[i] === 1;
|
const isVis = this.visible[i] === 1;
|
||||||
|
|
||||||
if (!isSeen) {
|
if (!isSeen) {
|
||||||
this.gfx.fillStyle(0x000000, 1);
|
tile.setVisible(false);
|
||||||
this.gfx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wall = isWall(this.world, x, y);
|
|
||||||
const base = wall ? GAME_CONFIG.rendering.wallColor : GAME_CONFIG.rendering.floorColor;
|
|
||||||
|
|
||||||
let alpha: number;
|
|
||||||
if (isVis) {
|
|
||||||
const s = this.visibleStrength[i];
|
|
||||||
alpha = Phaser.Math.Clamp(GAME_CONFIG.rendering.visibleMinAlpha + s * GAME_CONFIG.rendering.visibleStrengthFactor, GAME_CONFIG.rendering.visibleMinAlpha, GAME_CONFIG.rendering.visibleMaxAlpha);
|
|
||||||
} else {
|
} else {
|
||||||
alpha = wall ? GAME_CONFIG.rendering.fogAlphaWall : GAME_CONFIG.rendering.fogAlphaFloor;
|
tile.setVisible(true);
|
||||||
|
if (isVis) {
|
||||||
|
tile.alpha = 1.0;
|
||||||
|
tile.tint = 0xffffff;
|
||||||
|
} else {
|
||||||
|
tile.alpha = isWall(this.world, tile.x, tile.y) ? 0.4 : 0.2;
|
||||||
|
tile.tint = 0x888888;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.gfx.fillStyle(base, alpha);
|
// Actors
|
||||||
this.gfx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit (stairs) if seen
|
|
||||||
{
|
|
||||||
const ex = this.world.exit.x;
|
|
||||||
const ey = this.world.exit.y;
|
|
||||||
const i = idx(this.world, ex, ey);
|
|
||||||
if (this.seen[i] === 1) {
|
|
||||||
const alpha = this.visible[i] === 1 ? 1.0 : GAME_CONFIG.rendering.visibleMinAlpha;
|
|
||||||
this.gfx.fillStyle(GAME_CONFIG.rendering.exitColor, alpha);
|
|
||||||
this.gfx.fillRect(ex * TILE_SIZE + 7, ey * TILE_SIZE + 7, TILE_SIZE - 14, TILE_SIZE - 14);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path preview (seen only)
|
|
||||||
if (playerPath.length >= 2) {
|
|
||||||
this.gfx.fillStyle(GAME_CONFIG.rendering.pathPreviewColor, GAME_CONFIG.rendering.visibleMinAlpha);
|
|
||||||
for (const p of playerPath) {
|
|
||||||
// We can check isSeen via internal helper or just local array since we're inside
|
|
||||||
const i = idx(this.world, p.x, p.y);
|
|
||||||
if (this.seen[i] !== 1) continue;
|
|
||||||
this.gfx.fillRect(p.x * TILE_SIZE + 6, p.y * TILE_SIZE + 6, TILE_SIZE - 12, TILE_SIZE - 12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actors (enemies only if visible)
|
|
||||||
const activeEnemyIds = new Set<EntityId>();
|
const activeEnemyIds = new Set<EntityId>();
|
||||||
|
|
||||||
for (const a of this.world.actors.values()) {
|
for (const a of this.world.actors.values()) {
|
||||||
const i = idx(this.world, a.pos.x, a.pos.y);
|
const i = idx(this.world, a.pos.x, a.pos.y);
|
||||||
const isVis = this.visible[i] === 1;
|
const isVis = this.visible[i] === 1;
|
||||||
@@ -282,14 +254,11 @@ export class DungeonRenderer {
|
|||||||
|
|
||||||
activeEnemyIds.add(a.id);
|
activeEnemyIds.add(a.id);
|
||||||
let sprite = this.enemySprites.get(a.id);
|
let sprite = this.enemySprites.get(a.id);
|
||||||
|
|
||||||
const textureKey = a.type === "bat" ? "bat" : "rat";
|
const textureKey = a.type === "bat" ? "bat" : "rat";
|
||||||
|
|
||||||
if (!sprite) {
|
if (!sprite) {
|
||||||
sprite = this.scene.add.sprite(0, 0, textureKey, 0);
|
sprite = this.scene.add.sprite(0, 0, textureKey, 0);
|
||||||
sprite.setDepth(99);
|
sprite.setDepth(99);
|
||||||
const scale = TILE_SIZE / 15;
|
|
||||||
sprite.setScale(scale);
|
|
||||||
sprite.play(`${textureKey}-idle`);
|
sprite.play(`${textureKey}-idle`);
|
||||||
this.enemySprites.set(a.id, sprite);
|
this.enemySprites.set(a.id, sprite);
|
||||||
}
|
}
|
||||||
@@ -298,11 +267,9 @@ export class DungeonRenderer {
|
|||||||
sprite.setVisible(true);
|
sprite.setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide/Cleanup inactive/non-visible enemy sprites
|
|
||||||
for (const [id, sprite] of this.enemySprites.entries()) {
|
for (const [id, sprite] of this.enemySprites.entries()) {
|
||||||
if (!activeEnemyIds.has(id)) {
|
if (!activeEnemyIds.has(id)) {
|
||||||
sprite.setVisible(false);
|
sprite.setVisible(false);
|
||||||
// We could also destroy if they are dead, but hide is safer for now
|
|
||||||
if (!this.world.actors.has(id)) {
|
if (!this.world.actors.has(id)) {
|
||||||
sprite.destroy();
|
sprite.destroy();
|
||||||
this.enemySprites.delete(id);
|
this.enemySprites.delete(id);
|
||||||
@@ -310,16 +277,13 @@ export class DungeonRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render minimap
|
|
||||||
this.renderMinimap();
|
this.renderMinimap();
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderMinimap() {
|
private renderMinimap() {
|
||||||
this.minimapGfx.clear();
|
this.minimapGfx.clear();
|
||||||
|
|
||||||
if (!this.world) return;
|
if (!this.world) return;
|
||||||
|
|
||||||
// Calculate scale to fit map within panel
|
|
||||||
const padding = GAME_CONFIG.ui.minimapPadding;
|
const padding = GAME_CONFIG.ui.minimapPadding;
|
||||||
const availableWidth = GAME_CONFIG.ui.minimapPanelWidth - padding * 2;
|
const availableWidth = GAME_CONFIG.ui.minimapPanelWidth - padding * 2;
|
||||||
const availableHeight = GAME_CONFIG.ui.minimapPanelHeight - padding * 2;
|
const availableHeight = GAME_CONFIG.ui.minimapPanelHeight - padding * 2;
|
||||||
@@ -328,73 +292,44 @@ export class DungeonRenderer {
|
|||||||
const scaleY = availableHeight / this.world.height;
|
const scaleY = availableHeight / this.world.height;
|
||||||
const tileSize = Math.floor(Math.min(scaleX, scaleY));
|
const tileSize = Math.floor(Math.min(scaleX, scaleY));
|
||||||
|
|
||||||
// Center the map within the panel
|
|
||||||
const mapPixelWidth = this.world.width * tileSize;
|
const mapPixelWidth = this.world.width * tileSize;
|
||||||
const mapPixelHeight = this.world.height * tileSize;
|
const mapPixelHeight = this.world.height * tileSize;
|
||||||
const offsetX = -mapPixelWidth / 2;
|
const offsetX = -mapPixelWidth / 2;
|
||||||
const offsetY = -mapPixelHeight / 2;
|
const offsetY = -mapPixelHeight / 2;
|
||||||
|
|
||||||
// Draw only seen tiles
|
|
||||||
for (let y = 0; y < this.world.height; y++) {
|
for (let y = 0; y < this.world.height; y++) {
|
||||||
for (let x = 0; x < this.world.width; x++) {
|
for (let x = 0; x < this.world.width; x++) {
|
||||||
const i = idx(this.world, x, y);
|
const i = idx(this.world, x, y);
|
||||||
const isSeen = this.seen[i] === 1;
|
if (this.seen[i] !== 1) continue;
|
||||||
|
|
||||||
if (!isSeen) continue;
|
|
||||||
|
|
||||||
const wall = isWall(this.world, x, y);
|
const wall = isWall(this.world, x, y);
|
||||||
const color = wall ? 0x666666 : 0x333333;
|
const color = wall ? 0x666666 : 0x333333;
|
||||||
|
|
||||||
this.minimapGfx.fillStyle(color, 1);
|
this.minimapGfx.fillStyle(color, 1);
|
||||||
this.minimapGfx.fillRect(
|
this.minimapGfx.fillRect(offsetX + x * tileSize, offsetY + y * tileSize, tileSize, tileSize);
|
||||||
offsetX + x * tileSize,
|
|
||||||
offsetY + y * tileSize,
|
|
||||||
tileSize,
|
|
||||||
tileSize
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw exit if seen
|
|
||||||
const ex = this.world.exit.x;
|
const ex = this.world.exit.x;
|
||||||
const ey = this.world.exit.y;
|
const ey = this.world.exit.y;
|
||||||
const exitIdx = idx(this.world, ex, ey);
|
if (this.seen[idx(this.world, ex, ey)] === 1) {
|
||||||
if (this.seen[exitIdx] === 1) {
|
|
||||||
this.minimapGfx.fillStyle(0xffd166, 1);
|
this.minimapGfx.fillStyle(0xffd166, 1);
|
||||||
this.minimapGfx.fillRect(
|
this.minimapGfx.fillRect(offsetX + ex * tileSize, offsetY + ey * tileSize, tileSize, tileSize);
|
||||||
offsetX + ex * tileSize,
|
|
||||||
offsetY + ey * tileSize,
|
|
||||||
tileSize,
|
|
||||||
tileSize
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw player
|
|
||||||
const player = [...this.world.actors.values()].find(a => a.isPlayer);
|
const player = [...this.world.actors.values()].find(a => a.isPlayer);
|
||||||
if (player) {
|
if (player) {
|
||||||
this.minimapGfx.fillStyle(0x66ff66, 1);
|
this.minimapGfx.fillStyle(0x66ff66, 1);
|
||||||
this.minimapGfx.fillRect(
|
this.minimapGfx.fillRect(offsetX + player.pos.x * tileSize, offsetY + player.pos.y * tileSize, tileSize, tileSize);
|
||||||
offsetX + player.pos.x * tileSize,
|
|
||||||
offsetY + player.pos.y * tileSize,
|
|
||||||
tileSize,
|
|
||||||
tileSize
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw visible enemies
|
|
||||||
for (const a of this.world.actors.values()) {
|
for (const a of this.world.actors.values()) {
|
||||||
if (a.isPlayer) continue;
|
if (a.isPlayer) continue;
|
||||||
const i = idx(this.world, a.pos.x, a.pos.y);
|
const i = idx(this.world, a.pos.x, a.pos.y);
|
||||||
const isVis = this.visible[i] === 1;
|
if (this.visible[i] === 1) {
|
||||||
if (!isVis) continue;
|
|
||||||
|
|
||||||
this.minimapGfx.fillStyle(0xff6666, 1);
|
this.minimapGfx.fillStyle(0xff6666, 1);
|
||||||
this.minimapGfx.fillRect(
|
this.minimapGfx.fillRect(offsetX + a.pos.x * tileSize, offsetY + a.pos.y * tileSize, tileSize, tileSize);
|
||||||
offsetX + a.pos.x * tileSize,
|
}
|
||||||
offsetY + a.pos.y * tileSize,
|
|
||||||
tileSize,
|
|
||||||
tileSize
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +364,6 @@ export class DungeonRenderer {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
corpse.setDepth(50);
|
corpse.setDepth(50);
|
||||||
corpse.setScale(TILE_SIZE / 15);
|
|
||||||
corpse.play(`${textureKey}-die`);
|
corpse.play(`${textureKey}-die`);
|
||||||
this.corpseSprites.push(corpse);
|
this.corpseSprites.push(corpse);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.load.spritesheet("warrior", "warrior.png", { frameWidth: 12, frameHeight: 15 });
|
this.load.spritesheet("warrior", "warrior.png", { frameWidth: 12, frameHeight: 15 });
|
||||||
this.load.spritesheet("rat", "rat.png", { frameWidth: 16, frameHeight: 15 });
|
this.load.spritesheet("rat", "rat.png", { frameWidth: 16, frameHeight: 15 });
|
||||||
this.load.spritesheet("bat", "bat.png", { frameWidth: 15, frameHeight: 15 });
|
this.load.spritesheet("bat", "bat.png", { frameWidth: 15, frameHeight: 15 });
|
||||||
|
this.load.spritesheet("tiles0", "assets/tiles0.png", { frameWidth: 16, frameHeight: 16 });
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|||||||
Reference in New Issue
Block a user