Add character (warrior) and rat/bat sprites
This commit is contained in:
BIN
public/bat.png
Normal file
BIN
public/bat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/rat.png
Normal file
BIN
public/rat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
BIN
public/warrior.png
Normal file
BIN
public/warrior.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -114,6 +114,7 @@ export function generateWorld(level: number, runState: RunState): { world: World
|
|||||||
actors.set(playerId, {
|
actors.set(playerId, {
|
||||||
id: playerId,
|
id: playerId,
|
||||||
isPlayer: true,
|
isPlayer: true,
|
||||||
|
type: "player",
|
||||||
pos: { x: playerX, y: playerY },
|
pos: { x: playerX, y: playerY },
|
||||||
speed: GAME_CONFIG.player.speed,
|
speed: GAME_CONFIG.player.speed,
|
||||||
energy: 0,
|
energy: 0,
|
||||||
@@ -139,6 +140,7 @@ export function generateWorld(level: number, runState: RunState): { world: World
|
|||||||
actors.set(enemyId, {
|
actors.set(enemyId, {
|
||||||
id: enemyId,
|
id: enemyId,
|
||||||
isPlayer: false,
|
isPlayer: false,
|
||||||
|
type: random() < 0.5 ? "rat" : "bat",
|
||||||
pos: { x: enemyX, y: enemyY },
|
pos: { x: enemyX, y: enemyY },
|
||||||
speed: GAME_CONFIG.enemy.minSpeed + Math.floor(random() * (GAME_CONFIG.enemy.maxSpeed - GAME_CONFIG.enemy.minSpeed)),
|
speed: GAME_CONFIG.enemy.minSpeed + Math.floor(random() * (GAME_CONFIG.enemy.maxSpeed - GAME_CONFIG.enemy.minSpeed)),
|
||||||
energy: 0,
|
energy: 0,
|
||||||
@@ -155,7 +157,5 @@ export function generateWorld(level: number, runState: RunState): { world: World
|
|||||||
return { world: { width, height, tiles, actors, exit }, playerId };
|
return { world: { width, height, tiles, actors, exit }, playerId };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility - will be removed in Phase 2
|
|
||||||
/** @deprecated Use generateWorld instead */
|
|
||||||
export const makeTestWorld = generateWorld;
|
export const makeTestWorld = generateWorld;
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,15 @@ export function applyAction(w: World, actorId: EntityId, action: Action): SimEve
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (target.stats.hp <= 0) {
|
if (target.stats.hp <= 0) {
|
||||||
|
events.push({
|
||||||
|
type: "killed",
|
||||||
|
targetId: target.id,
|
||||||
|
killerId: actorId,
|
||||||
|
x: target.pos.x,
|
||||||
|
y: target.pos.y,
|
||||||
|
victimType: target.type
|
||||||
|
});
|
||||||
w.actors.delete(target.id);
|
w.actors.delete(target.id);
|
||||||
events.push({ type: "killed", targetId: target.id, killerId: actorId });
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
events.push({ type: "waited", actorId }); // Missed or invalid target
|
events.push({ type: "waited", actorId }); // Missed or invalid target
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ export type Action =
|
|||||||
| { type: "wait" };
|
| { type: "wait" };
|
||||||
|
|
||||||
export type SimEvent =
|
export type SimEvent =
|
||||||
| { type: "moved"; actorId: EntityId; from: Vec2; to: Vec2 }
|
|
||||||
| { type: "moved"; actorId: EntityId; from: Vec2; to: Vec2 }
|
| { type: "moved"; actorId: EntityId; from: Vec2; to: Vec2 }
|
||||||
| { type: "attacked"; attackerId: EntityId; targetId: EntityId }
|
| { type: "attacked"; attackerId: EntityId; targetId: EntityId }
|
||||||
| { type: "damaged"; targetId: EntityId; amount: number; hp: number; x: number; y: number }
|
| { type: "damaged"; targetId: EntityId; amount: number; hp: number; x: number; y: number }
|
||||||
| { type: "killed"; targetId: EntityId; killerId: EntityId }
|
| { type: "killed"; targetId: EntityId; killerId: EntityId; x: number; y: number; victimType?: "player" | "rat" | "bat" }
|
||||||
| { type: "waited"; actorId: EntityId };
|
| { type: "waited"; actorId: EntityId };
|
||||||
|
|
||||||
export type Stats = {
|
export type Stats = {
|
||||||
@@ -37,6 +36,7 @@ export type RunState = {
|
|||||||
export type Actor = {
|
export type Actor = {
|
||||||
id: EntityId;
|
id: EntityId;
|
||||||
isPlayer: boolean;
|
isPlayer: boolean;
|
||||||
|
type?: "player" | "rat" | "bat";
|
||||||
pos: Vec2;
|
pos: Vec2;
|
||||||
speed: number;
|
speed: number;
|
||||||
energy: number;
|
energy: number;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { GAME_CONFIG } from "../game/config/GameConfig";
|
|||||||
export class DungeonRenderer {
|
export class DungeonRenderer {
|
||||||
private scene: Phaser.Scene;
|
private scene: Phaser.Scene;
|
||||||
private gfx: Phaser.GameObjects.Graphics;
|
private gfx: Phaser.GameObjects.Graphics;
|
||||||
|
private playerSprite?: Phaser.GameObjects.Sprite;
|
||||||
|
private enemySprites: Map<EntityId, Phaser.GameObjects.Sprite> = new Map();
|
||||||
|
|
||||||
// FOV
|
// FOV
|
||||||
private fov!: any;
|
private fov!: any;
|
||||||
@@ -60,6 +62,84 @@ 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 player sprite
|
||||||
|
if (!this.playerSprite) {
|
||||||
|
this.playerSprite = this.scene.add.sprite(0, 0, "warrior", 0);
|
||||||
|
this.playerSprite.setDepth(100);
|
||||||
|
|
||||||
|
// Calculate display size to fit within tile while maintaining 12:15 aspect ratio
|
||||||
|
const scale = TILE_SIZE / 15; // Fit height to tile size
|
||||||
|
this.playerSprite.setScale(scale);
|
||||||
|
|
||||||
|
// Simple animations from PD source
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'warrior-idle',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('warrior', { frames: [0, 0, 0, 1, 0, 0, 1, 1] }),
|
||||||
|
frameRate: 2,
|
||||||
|
repeat: -1
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'warrior-run',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('warrior', { frames: [2, 3, 4, 5, 6, 7] }),
|
||||||
|
frameRate: 15,
|
||||||
|
repeat: -1
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'warrior-die',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('warrior', { frames: [8, 9, 10, 11, 12] }),
|
||||||
|
frameRate: 10,
|
||||||
|
repeat: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
this.playerSprite.play('warrior-idle');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rat animations
|
||||||
|
if (!this.scene.anims.exists('rat-idle')) {
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'rat-idle',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('rat', { frames: [0, 0, 0, 1] }),
|
||||||
|
frameRate: 4,
|
||||||
|
repeat: -1
|
||||||
|
});
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'rat-run',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('rat', { frames: [6, 7, 8, 9, 10] }),
|
||||||
|
frameRate: 10,
|
||||||
|
repeat: -1
|
||||||
|
});
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'rat-die',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('rat', { frames: [11, 12, 13, 14] }),
|
||||||
|
frameRate: 10,
|
||||||
|
repeat: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bat animations
|
||||||
|
if (!this.scene.anims.exists('bat-idle')) {
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'bat-idle',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('bat', { frames: [0, 1] }),
|
||||||
|
frameRate: 8,
|
||||||
|
repeat: -1
|
||||||
|
});
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'bat-run',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('bat', { frames: [0, 1] }),
|
||||||
|
frameRate: 12,
|
||||||
|
repeat: -1
|
||||||
|
});
|
||||||
|
this.scene.anims.create({
|
||||||
|
key: 'bat-die',
|
||||||
|
frames: this.scene.anims.generateFrameNumbers('bat', { frames: [4, 5, 6] }),
|
||||||
|
frameRate: 10,
|
||||||
|
repeat: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.fov = new FOV.PreciseShadowcasting((x: number, y: number) => {
|
this.fov = new FOV.PreciseShadowcasting((x: number, y: number) => {
|
||||||
if (!inBounds(this.world, x, y)) return false;
|
if (!inBounds(this.world, x, y)) return false;
|
||||||
return !isWall(this.world, x, y);
|
return !isWall(this.world, x, y);
|
||||||
@@ -176,14 +256,50 @@ export class DungeonRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Actors (enemies only if visible)
|
// Actors (enemies only if visible)
|
||||||
|
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;
|
||||||
if (!a.isPlayer && !isVis) continue;
|
|
||||||
|
|
||||||
const color = a.isPlayer ? GAME_CONFIG.rendering.playerColor : GAME_CONFIG.rendering.enemyColor;
|
if (a.isPlayer) {
|
||||||
this.gfx.fillStyle(color, 1);
|
if (this.playerSprite) {
|
||||||
this.gfx.fillRect(a.pos.x * TILE_SIZE + 4, a.pos.y * TILE_SIZE + 4, TILE_SIZE - 8, TILE_SIZE - 8);
|
this.playerSprite.setPosition(a.pos.x * TILE_SIZE + TILE_SIZE / 2, a.pos.y * TILE_SIZE + TILE_SIZE / 2);
|
||||||
|
this.playerSprite.setVisible(true);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVis) continue;
|
||||||
|
|
||||||
|
activeEnemyIds.add(a.id);
|
||||||
|
let sprite = this.enemySprites.get(a.id);
|
||||||
|
|
||||||
|
const textureKey = a.type === "bat" ? "bat" : "rat";
|
||||||
|
|
||||||
|
if (!sprite) {
|
||||||
|
sprite = this.scene.add.sprite(0, 0, textureKey, 0);
|
||||||
|
sprite.setDepth(99);
|
||||||
|
const scale = TILE_SIZE / 15;
|
||||||
|
sprite.setScale(scale);
|
||||||
|
sprite.play(`${textureKey}-idle`);
|
||||||
|
this.enemySprites.set(a.id, sprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite.setPosition(a.pos.x * TILE_SIZE + TILE_SIZE / 2, a.pos.y * TILE_SIZE + TILE_SIZE / 2);
|
||||||
|
sprite.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide/Cleanup inactive/non-visible enemy sprites
|
||||||
|
for (const [id, sprite] of this.enemySprites.entries()) {
|
||||||
|
if (!activeEnemyIds.has(id)) {
|
||||||
|
sprite.setVisible(false);
|
||||||
|
// We could also destroy if they are dead, but hide is safer for now
|
||||||
|
if (!this.world.actors.has(id)) {
|
||||||
|
sprite.destroy();
|
||||||
|
this.enemySprites.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render minimap
|
// Render minimap
|
||||||
@@ -295,4 +411,17 @@ export class DungeonRenderer {
|
|||||||
onComplete: () => text.destroy()
|
onComplete: () => text.destroy()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spawnCorpse(x: number, y: number, type: "player" | "rat" | "bat") {
|
||||||
|
const textureKey = type === "player" ? "warrior" : type;
|
||||||
|
const corpse = this.scene.add.sprite(
|
||||||
|
x * TILE_SIZE + TILE_SIZE / 2,
|
||||||
|
y * TILE_SIZE + TILE_SIZE / 2,
|
||||||
|
textureKey,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
corpse.setDepth(50);
|
||||||
|
corpse.setScale(TILE_SIZE / 15);
|
||||||
|
corpse.play(`${textureKey}-die`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ export class GameScene extends Phaser.Scene {
|
|||||||
super("GameScene");
|
super("GameScene");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preload() {
|
||||||
|
this.load.spritesheet("warrior", "warrior.png", { frameWidth: 12, frameHeight: 15 });
|
||||||
|
this.load.spritesheet("rat", "rat.png", { frameWidth: 16, frameHeight: 15 });
|
||||||
|
this.load.spritesheet("bat", "bat.png", { frameWidth: 15, frameHeight: 15 });
|
||||||
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
this.cursors = this.input.keyboard!.createCursorKeys();
|
this.cursors = this.input.keyboard!.createCursorKeys();
|
||||||
|
|
||||||
@@ -217,6 +223,9 @@ export class GameScene extends Phaser.Scene {
|
|||||||
if (ev.type === "damaged") {
|
if (ev.type === "damaged") {
|
||||||
console.log("Showing damage:", ev.amount, "at", ev.x, ev.y);
|
console.log("Showing damage:", ev.amount, "at", ev.x, ev.y);
|
||||||
this.dungeonRenderer.showDamage(ev.x, ev.y, ev.amount);
|
this.dungeonRenderer.showDamage(ev.x, ev.y, ev.amount);
|
||||||
|
} else if (ev.type === "killed") {
|
||||||
|
console.log("Showing corpse for:", ev.victimType, "at", ev.x, ev.y);
|
||||||
|
this.dungeonRenderer.spawnCorpse(ev.x, ev.y, ev.victimType || "rat");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user