Add in mana and an asset viewer
This commit is contained in:
BIN
public/assets/sprites/actors/player/soldier/Idle.png
Normal file
BIN
public/assets/sprites/actors/player/soldier/Idle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
@@ -1,8 +1,18 @@
|
||||
export interface AnimationConfig {
|
||||
key: string;
|
||||
textureKey: string;
|
||||
frames: number[];
|
||||
frameRate: number;
|
||||
repeat: number;
|
||||
}
|
||||
|
||||
export const GAME_CONFIG = {
|
||||
player: {
|
||||
initialStats: {
|
||||
maxHp: 20,
|
||||
hp: 20,
|
||||
maxMana: 20,
|
||||
mana: 20,
|
||||
attack: 5,
|
||||
defense: 2,
|
||||
level: 1,
|
||||
@@ -29,8 +39,6 @@ export const GAME_CONFIG = {
|
||||
viewRadius: 8
|
||||
},
|
||||
|
||||
|
||||
|
||||
map: {
|
||||
width: 60,
|
||||
height: 40,
|
||||
@@ -72,11 +80,16 @@ export const GAME_CONFIG = {
|
||||
baseExpToNextLevel: 10,
|
||||
expMultiplier: 1.5,
|
||||
hpGainPerLevel: 5,
|
||||
manaGainPerLevel: 3,
|
||||
attackGainPerLevel: 1,
|
||||
statPointsPerLevel: 5,
|
||||
skillPointsPerLevel: 1
|
||||
},
|
||||
|
||||
mana: {
|
||||
regenPerTurn: 2,
|
||||
regenInterval: 3 // Regenerate every 3 turns
|
||||
},
|
||||
|
||||
rendering: {
|
||||
tileSize: 16,
|
||||
@@ -97,7 +110,6 @@ export const GAME_CONFIG = {
|
||||
visibleStrengthFactor: 0.65
|
||||
},
|
||||
|
||||
|
||||
terrain: {
|
||||
empty: 1,
|
||||
wall: 4,
|
||||
@@ -126,11 +138,32 @@ export const GAME_CONFIG = {
|
||||
{ key: "rat", path: "assets/sprites/actors/enemies/rat.png", frameConfig: { frameWidth: 16, frameHeight: 15 } },
|
||||
{ key: "bat", path: "assets/sprites/actors/enemies/bat.png", frameConfig: { frameWidth: 15, frameHeight: 15 } },
|
||||
{ key: "dungeon", path: "assets/tilesets/dungeon.png", frameConfig: { frameWidth: 16, frameHeight: 16 } },
|
||||
{ key: "soldier.idle", path: "assets/sprites/actors/player/soldier/Idle.png", frameConfig: { frameWidth: 60, frameHeight: 75 } },
|
||||
],
|
||||
images: [
|
||||
{ key: "splash_bg", path: "assets/ui/splash_bg.png" }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
animations: [
|
||||
// Warrior
|
||||
{ key: "warrior-idle", textureKey: "warrior", frames: [0, 0, 0, 1, 0, 0, 1, 1], frameRate: 2, repeat: -1 },
|
||||
{ key: "warrior-run", textureKey: "warrior", frames: [2, 3, 4, 5, 6, 7], frameRate: 15, repeat: -1 },
|
||||
{ key: "warrior-die", textureKey: "warrior", frames: [8, 9, 10, 11, 12], frameRate: 10, repeat: 0 },
|
||||
|
||||
// Rat
|
||||
{ key: "rat-idle", textureKey: "rat", frames: [0, 0, 0, 1], frameRate: 4, repeat: -1 },
|
||||
{ key: "rat-run", textureKey: "rat", frames: [6, 7, 8, 9, 10], frameRate: 10, repeat: -1 },
|
||||
{ key: "rat-die", textureKey: "rat", frames: [11, 12, 13, 14], frameRate: 10, repeat: 0 },
|
||||
|
||||
// Bat
|
||||
{ key: "bat-idle", textureKey: "bat", frames: [0, 1], frameRate: 8, repeat: -1 },
|
||||
{ key: "bat-run", textureKey: "bat", frames: [0, 1], frameRate: 12, repeat: -1 },
|
||||
{ key: "bat-die", textureKey: "bat", frames: [4, 5, 6], frameRate: 10, repeat: 0 },
|
||||
|
||||
// Soldier
|
||||
{ key: "soldier-idle", textureKey: "soldier.idle", frames: [0, 1, 2, 3, 4, 5, 6, 7], frameRate: 8, repeat: -1 },
|
||||
]
|
||||
} as const;
|
||||
|
||||
export type GameConfig = typeof GAME_CONFIG;
|
||||
|
||||
@@ -31,6 +31,8 @@ export type SimEvent =
|
||||
export type Stats = {
|
||||
maxHp: number;
|
||||
hp: number;
|
||||
maxMana: number;
|
||||
mana: number;
|
||||
attack: number;
|
||||
defense: number;
|
||||
level: number;
|
||||
|
||||
@@ -17,6 +17,8 @@ describe('ProgressionManager', () => {
|
||||
stats: {
|
||||
maxHp: 20,
|
||||
hp: 20,
|
||||
maxMana: 0,
|
||||
mana: 0,
|
||||
level: 1,
|
||||
exp: 0,
|
||||
expToNextLevel: 100,
|
||||
|
||||
@@ -8,7 +8,7 @@ describe('World Generator', () => {
|
||||
it('should generate a world with correct dimensions', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10, maxMana: 20, mana: 20,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
@@ -26,7 +26,7 @@ describe('World Generator', () => {
|
||||
it('should place player actor', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10, maxMana: 20, mana: 20,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
@@ -47,7 +47,7 @@ describe('World Generator', () => {
|
||||
it('should create walkable rooms', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10, maxMana: 20, mana: 20,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
@@ -65,7 +65,7 @@ describe('World Generator', () => {
|
||||
it('should place exit in valid location', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10, maxMana: 20, mana: 20,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
@@ -83,7 +83,7 @@ describe('World Generator', () => {
|
||||
it('should create enemies', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10, maxMana: 20, mana: 20,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
@@ -111,7 +111,7 @@ describe('World Generator', () => {
|
||||
it('should generate deterministic maps for same level', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10, maxMana: 20, mana: 20,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
@@ -134,7 +134,7 @@ describe('World Generator', () => {
|
||||
it('should generate different maps for different levels', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10, maxMana: 20, mana: 20,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
@@ -152,7 +152,7 @@ describe('World Generator', () => {
|
||||
it('should scale enemy difficulty with level', () => {
|
||||
const runState = {
|
||||
stats: {
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
|
||||
maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10, maxMana: 20, mana: 20,
|
||||
statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10,
|
||||
critChance: 5, critMultiplier: 150, accuracy: 90, lifesteal: 0, evasion: 5, blockChance: 0, luck: 0,
|
||||
passiveNodes: []
|
||||
|
||||
@@ -69,6 +69,8 @@ function checkLevelUp(player: CombatantActor, events: SimEvent[]) {
|
||||
// Growth
|
||||
s.maxHp += GAME_CONFIG.leveling.hpGainPerLevel;
|
||||
s.hp = s.maxHp; // Heal on level up
|
||||
s.maxMana += GAME_CONFIG.leveling.manaGainPerLevel;
|
||||
s.mana = s.maxMana; // Restore mana on level up
|
||||
s.attack += GAME_CONFIG.leveling.attackGainPerLevel;
|
||||
|
||||
s.statPoints += GAME_CONFIG.leveling.statPointsPerLevel;
|
||||
|
||||
@@ -308,6 +308,8 @@ function placeEnemies(floor: number, rooms: Room[], actors: Map<EntityId, Actor>
|
||||
stats: {
|
||||
maxHp: scaledHp + Math.floor(random() * 4),
|
||||
hp: scaledHp + Math.floor(random() * 4),
|
||||
maxMana: 0,
|
||||
mana: 0,
|
||||
attack: scaledAttack + Math.floor(random() * 2),
|
||||
defense: enemyDef.baseDefense,
|
||||
level: 0,
|
||||
|
||||
@@ -3,6 +3,7 @@ import GameUI from "./ui/GameUI";
|
||||
import { GameScene } from "./scenes/GameScene";
|
||||
import { MenuScene } from "./scenes/MenuScene";
|
||||
import { PreloadScene } from "./scenes/PreloadScene";
|
||||
import { AssetViewerScene } from "./scenes/AssetViewerScene";
|
||||
|
||||
new Phaser.Game({
|
||||
type: Phaser.AUTO,
|
||||
@@ -15,5 +16,8 @@ new Phaser.Game({
|
||||
backgroundColor: "#111",
|
||||
pixelArt: true,
|
||||
roundPixels: true,
|
||||
scene: [PreloadScene, MenuScene, GameScene, GameUI]
|
||||
dom: {
|
||||
createContainer: true
|
||||
},
|
||||
scene: [PreloadScene, MenuScene, AssetViewerScene, GameScene, GameUI]
|
||||
});
|
||||
|
||||
@@ -53,7 +53,14 @@ export class DungeonRenderer {
|
||||
});
|
||||
|
||||
this.fxRenderer.clearCorpses();
|
||||
this.setupAnimations();
|
||||
|
||||
// Ensure player sprite exists
|
||||
if (!this.playerSprite) {
|
||||
this.playerSprite = this.scene.add.sprite(0, 0, "warrior", 0);
|
||||
this.playerSprite.setDepth(100);
|
||||
this.playerSprite.play('warrior-idle');
|
||||
}
|
||||
|
||||
this.minimapRenderer.positionMinimap();
|
||||
|
||||
// Reset player sprite position to prevent tween animation from old floor
|
||||
@@ -72,79 +79,7 @@ export class DungeonRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
private setupAnimations() {
|
||||
// Player
|
||||
if (!this.playerSprite) {
|
||||
this.playerSprite = this.scene.add.sprite(0, 0, "warrior", 0);
|
||||
this.playerSprite.setDepth(100);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Enemy 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
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggleMinimap() {
|
||||
this.minimapRenderer.toggle();
|
||||
|
||||
744
src/scenes/AssetViewerScene.ts
Normal file
744
src/scenes/AssetViewerScene.ts
Normal file
@@ -0,0 +1,744 @@
|
||||
import Phaser from "phaser";
|
||||
|
||||
export class AssetViewerScene extends Phaser.Scene {
|
||||
private bg!: Phaser.GameObjects.Graphics;
|
||||
private controlPanel!: Phaser.GameObjects.Container;
|
||||
private displayArea!: Phaser.GameObjects.Container;
|
||||
|
||||
private currentSprite?: Phaser.GameObjects.Sprite;
|
||||
private currentKey: string = "";
|
||||
private currentFrame: number = 0;
|
||||
private totalFrames: number = 0;
|
||||
private isPlaying: boolean = false;
|
||||
private animSpeed: number = 10; // fps
|
||||
|
||||
private statusText?: Phaser.GameObjects.Text;
|
||||
private frameText?: Phaser.GameObjects.Text;
|
||||
private speedText?: Phaser.GameObjects.Text;
|
||||
|
||||
constructor() {
|
||||
super("AssetViewerScene");
|
||||
}
|
||||
|
||||
create() {
|
||||
// Reset state
|
||||
this.currentSprite = undefined;
|
||||
this.currentKey = "";
|
||||
this.currentFrame = 0;
|
||||
this.totalFrames = 0;
|
||||
this.isPlaying = false;
|
||||
this.animSpeed = 10;
|
||||
|
||||
const { width, height } = this.scale;
|
||||
|
||||
// Background
|
||||
this.bg = this.add.graphics();
|
||||
this.bg.fillGradientStyle(0x0a0510, 0x0a0510, 0x1a0a2a, 0x1a0a2a, 1);
|
||||
this.bg.fillRect(0, 0, width, height);
|
||||
|
||||
this.cameras.main.fadeIn(500, 0, 0, 0);
|
||||
|
||||
// Title
|
||||
this.add.text(width / 2, 40, "ASSET VIEWER", {
|
||||
fontSize: "48px",
|
||||
color: "#ff9922",
|
||||
fontStyle: "bold",
|
||||
fontFamily: "Georgia, serif",
|
||||
stroke: "#111",
|
||||
strokeThickness: 6,
|
||||
}).setOrigin(0.5);
|
||||
|
||||
// Back button
|
||||
this.createButton(80, 40, "← BACK", 0x666666, () => {
|
||||
this.cameras.main.fadeOut(500, 0, 0, 0);
|
||||
this.cameras.main.once(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => {
|
||||
this.scene.start("MenuScene");
|
||||
});
|
||||
});
|
||||
|
||||
// Cleanup on shutdown
|
||||
this.events.once("shutdown", this.cleanup, this);
|
||||
|
||||
// Create UI sections
|
||||
this.createControlPanel(width, height);
|
||||
this.createDisplayArea(width, height);
|
||||
}
|
||||
|
||||
// State
|
||||
private selectedFrames: number[] = [];
|
||||
private createdAnimations: Map<string, Phaser.Animations.Animation> = new Map();
|
||||
|
||||
// DOM Elements ref
|
||||
private inputDom?: Phaser.GameObjects.DOMElement;
|
||||
private frameConfigDom?: Phaser.GameObjects.DOMElement;
|
||||
private animConfigDom?: Phaser.GameObjects.DOMElement;
|
||||
private animListDom?: Phaser.GameObjects.DOMElement;
|
||||
|
||||
private createControlPanel(_width: number, height: number) {
|
||||
const panelX = 20;
|
||||
const panelY = 100;
|
||||
const panelWidth = 400;
|
||||
const panelHeight = height - 120;
|
||||
|
||||
// Panel background
|
||||
const panelBg = this.add.graphics();
|
||||
panelBg.fillStyle(0x000000, 0.85);
|
||||
panelBg.fillRoundedRect(0, 0, panelWidth, panelHeight, 8);
|
||||
panelBg.lineStyle(2, 0xff9922, 0.6);
|
||||
panelBg.strokeRoundedRect(0, 0, panelWidth, panelHeight, 8);
|
||||
|
||||
this.controlPanel = this.add.container(panelX, panelY, [panelBg]);
|
||||
|
||||
let yOffset = 20;
|
||||
|
||||
// --- Section: Load Asset ---
|
||||
this.addSectionTitle(this.controlPanel, "1. LOAD ASSET", 20, yOffset);
|
||||
yOffset += 35;
|
||||
|
||||
// Path & Size Inputs
|
||||
const loadHtml = `
|
||||
<div style="font-family: monospace; font-size: 12px; color: #aaa;">
|
||||
<div style="margin-bottom: 5px;">Path: <input type="text" id="asset-path-input" value="assets/sprites/actors/player/warrior.png" style="width: 250px; padding: 4px; background: #222; color: #fff; border: 1px solid #555;"></div>
|
||||
<div style="margin-bottom: 5px;">
|
||||
Size: <input type="number" id="frame-width-input" value="12" style="width: 30px; background: #222; color: #fff; border: 1px solid #555;"> x
|
||||
<input type="number" id="frame-height-input" value="15" style="width: 30px; background: #222; color: #fff; border: 1px solid #555;">
|
||||
</div>
|
||||
<div>
|
||||
Margin: <input type="number" id="frame-margin-input" value="0" style="width: 30px; background: #222; color: #fff; border: 1px solid #555;">
|
||||
Spacing: <input type="number" id="frame-spacing-input" value="0" style="width: 30px; background: #222; color: #fff; border: 1px solid #555;">
|
||||
<button id="load-btn" style="cursor: pointer; background: #2a4; color: #fff; border: none; padding: 4px 10px; margin-left: 10px;">LOAD</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.inputDom = this.add.dom(panelX + 20, panelY + yOffset).createFromHTML(loadHtml).setOrigin(0, 0);
|
||||
this.inputDom.addListener('click');
|
||||
this.inputDom.on('click', (event: any) => {
|
||||
if (event.target.id === 'load-btn') this.loadAsset();
|
||||
});
|
||||
yOffset += 85;
|
||||
|
||||
this.statusText = this.add.text(20, yOffset, "No asset loaded", { fontSize: "11px", color: "#888" });
|
||||
this.controlPanel.add(this.statusText);
|
||||
yOffset += 25;
|
||||
|
||||
// --- Section: Frame Picker ---
|
||||
this.addSectionTitle(this.controlPanel, "2. PICK FRAMES", 20, yOffset);
|
||||
yOffset += 30;
|
||||
|
||||
// Container for frame grid
|
||||
// Use remaining height for grid but reserve space for playback controls
|
||||
const gridHeight = panelHeight - yOffset - 110;
|
||||
const gridBg = this.add.rectangle(20, yOffset, panelWidth - 40, gridHeight, 0x111111).setOrigin(0);
|
||||
this.controlPanel.add(gridBg);
|
||||
|
||||
// Helper text
|
||||
this.addLabel(this.controlPanel, "Click frames to add to sequence:", 20, yOffset - 15);
|
||||
|
||||
// Frame Grid Area (populated dynamically)
|
||||
this.frameGridContainer = this.add.container(20, yOffset);
|
||||
this.controlPanel.add(this.frameGridContainer);
|
||||
yOffset += gridHeight + 15;
|
||||
|
||||
// Global Playback & Speed
|
||||
const playBtn = this.createSmallButton(40, yOffset, "▶ Play", 0x2288ff, () => this.playAnimation());
|
||||
const stopBtn = this.createSmallButton(130, yOffset, "■ Stop", 0xff4444, () => this.stopAnimation());
|
||||
|
||||
const prevBtn = this.createSmallButton(220, yOffset, "◀ Step", 0x666666, () => this.prevFrame());
|
||||
const nextBtn = this.createSmallButton(310, yOffset, "Step ▶", 0x666666, () => this.nextFrame());
|
||||
|
||||
this.controlPanel.add([playBtn, stopBtn, prevBtn, nextBtn]);
|
||||
yOffset += 40;
|
||||
|
||||
// Speed
|
||||
this.speedText = this.add.text(20, yOffset + 8, `Speed: ${this.animSpeed} FPS`, { fontSize: "12px", color: "#ccc" });
|
||||
this.controlPanel.add(this.speedText);
|
||||
|
||||
const slowerBtn = this.createSmallButton(150, yOffset, "Slower", 0x444444, () => this.adjustSpeed(-5));
|
||||
slowerBtn.setScale(0.8);
|
||||
const fasterBtn = this.createSmallButton(220, yOffset, "Faster", 0x444444, () => this.adjustSpeed(5));
|
||||
fasterBtn.setScale(0.8);
|
||||
this.controlPanel.add([slowerBtn, fasterBtn]);
|
||||
}
|
||||
|
||||
private frameGridContainer!: Phaser.GameObjects.Container;
|
||||
|
||||
private createDisplayArea(width: number, height: number) {
|
||||
const areaX = 450;
|
||||
const areaY = 100;
|
||||
const areaWidth = width - 500;
|
||||
const areaHeight = height - 420; // Reduced height further to ensure bottom panel fits
|
||||
|
||||
const areaBg = this.add.graphics();
|
||||
areaBg.fillStyle(0x1a1a1a, 0.9);
|
||||
areaBg.fillRoundedRect(0, 0, areaWidth, areaHeight, 8);
|
||||
areaBg.lineStyle(2, 0xff9922, 0.4);
|
||||
areaBg.strokeRoundedRect(0, 0, areaWidth, areaHeight, 8);
|
||||
|
||||
// Grid pattern
|
||||
const grid = this.add.graphics();
|
||||
grid.lineStyle(1, 0x333333, 0.3);
|
||||
const gridSize = 32;
|
||||
for (let x = 0; x <= areaWidth; x += gridSize) {
|
||||
grid.lineBetween(x, 0, x, areaHeight);
|
||||
}
|
||||
for (let y = 0; y <= areaHeight; y += gridSize) {
|
||||
grid.lineBetween(0, y, areaWidth, y);
|
||||
}
|
||||
|
||||
this.displayArea = this.add.container(areaX, areaY, [areaBg, grid]);
|
||||
|
||||
// Instructions
|
||||
const instructions = this.add.text(areaWidth / 2, areaHeight / 2, "Load an asset to begin\n\nClick in this area to place sprites", {
|
||||
fontSize: "18px",
|
||||
color: "#666",
|
||||
fontFamily: "monospace",
|
||||
align: "center"
|
||||
}).setOrigin(0.5);
|
||||
this.displayArea.add(instructions);
|
||||
|
||||
// Make display area interactive for sprite placement
|
||||
const interactiveZone = this.add.zone(areaX, areaY, areaWidth, areaHeight).setOrigin(0, 0).setInteractive();
|
||||
interactiveZone.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
|
||||
// Check if clicking existing sprites? No, simpler for now
|
||||
if (this.currentKey && this.totalFrames > 0) {
|
||||
// Adjust for container position?
|
||||
// Note: pointer.x/y are global.
|
||||
// displayArea is at areaX, areaY.
|
||||
// Sprites inside displayArea should be at relative coords.
|
||||
const localX = pointer.x - areaX;
|
||||
const localY = pointer.y - areaY;
|
||||
this.placeSprite(localX, localY);
|
||||
}
|
||||
});
|
||||
|
||||
// Create Bottom Animation Panel
|
||||
this.createAnimationPanel(areaX, areaY + areaHeight + 20, areaWidth, 200);
|
||||
}
|
||||
|
||||
private createAnimationPanel(x: number, y: number, width: number, height: number) {
|
||||
const panelBg = this.add.graphics();
|
||||
panelBg.fillStyle(0x000000, 0.85);
|
||||
panelBg.fillRoundedRect(0, 0, width, height, 8);
|
||||
panelBg.lineStyle(2, 0xff9922, 0.6);
|
||||
panelBg.strokeRoundedRect(0, 0, width, height, 8);
|
||||
|
||||
const container = this.add.container(x, y, [panelBg]);
|
||||
|
||||
let yOffset = 20;
|
||||
|
||||
// --- Section: Create Animation ---
|
||||
this.addSectionTitle(container, "3. CREATE ANIMATION", 20, yOffset);
|
||||
|
||||
// Selected Frames Display (Moved here)
|
||||
this.addLabel(container, "Sequence:", 220, yOffset);
|
||||
this.frameText = this.add.text(290, yOffset, "[]", { fontSize: "12px", color: "#4f4", wordWrap: { width: width - 350 } });
|
||||
container.add(this.frameText);
|
||||
|
||||
const clearBtn = this.createSmallButton(width - 60, yOffset + 8, "Clear", 0x666666, () => {
|
||||
this.selectedFrames = [];
|
||||
this.updateFrameText();
|
||||
});
|
||||
clearBtn.setScale(0.8);
|
||||
container.add(clearBtn);
|
||||
|
||||
yOffset += 40;
|
||||
|
||||
// Config Inputs
|
||||
const animHtml = `
|
||||
<div style="font-family: monospace; font-size: 12px; color: #aaa; display: flex; align-items: center; gap: 15px;">
|
||||
<div>Key: <input type="text" id="anim-key" placeholder="run" style="width: 100px; padding: 6px; background: #222; color: #fff; border: 1px solid #555;"></div>
|
||||
<div>FPS: <input type="number" id="anim-fps" value="10" style="width: 50px; padding: 6px; background: #222; color: #fff; border: 1px solid #555;"></div>
|
||||
<div>Loop: <input type="checkbox" id="anim-loop" checked style="transform: scale(1.2);"></div>
|
||||
<button id="create-anim-btn" style="cursor: pointer; background: #f92; color: #000; border: none; padding: 6px 15px; font-weight: bold; border-radius: 4px;">CREATE ANIMATION</button>
|
||||
</div>
|
||||
`;
|
||||
this.animConfigDom = this.add.dom(x + 20, y + yOffset).createFromHTML(animHtml).setOrigin(0, 0);
|
||||
this.animConfigDom.addListener('click');
|
||||
this.animConfigDom.on('click', (event: any) => {
|
||||
if (event.target.id === 'create-anim-btn') this.createCustomAnimation();
|
||||
});
|
||||
yOffset += 50;
|
||||
|
||||
// --- Section: Animation List ---
|
||||
this.addSectionTitle(container, "4. ANIMATIONS", 20, yOffset);
|
||||
yOffset += 30;
|
||||
|
||||
const listHtml = `
|
||||
<div id="anim-list" style="width: ${width - 40}px; height: 60px; overflow-y: auto; background: #111; border: 1px solid #444; padding: 5px; font-family: monospace; font-size: 11px; color: #ccc; display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<div style="color: #666; font-style: italic; width: 100%;">No animations created</div>
|
||||
</div>
|
||||
`;
|
||||
this.animListDom = this.add.dom(x + 20, y + yOffset).createFromHTML(listHtml).setOrigin(0, 0);
|
||||
this.animListDom.addListener('click');
|
||||
}
|
||||
|
||||
private loadAsset() {
|
||||
const pathInput = document.getElementById("asset-path-input") as HTMLInputElement;
|
||||
const widthInput = document.getElementById("frame-width-input") as HTMLInputElement;
|
||||
const heightInput = document.getElementById("frame-height-input") as HTMLInputElement;
|
||||
const marginInput = document.getElementById("frame-margin-input") as HTMLInputElement;
|
||||
const spacingInput = document.getElementById("frame-spacing-input") as HTMLInputElement;
|
||||
|
||||
if (!pathInput || !widthInput || !heightInput) return;
|
||||
|
||||
const path = pathInput.value.trim();
|
||||
const frameWidth = parseInt(widthInput.value) || 16;
|
||||
const frameHeight = parseInt(heightInput.value) || 16;
|
||||
const margin = marginInput ? (parseInt(marginInput.value) || 0) : 0;
|
||||
const spacing = spacingInput ? (parseInt(spacingInput.value) || 0) : 0;
|
||||
|
||||
if (!path) {
|
||||
this.updateStatus("Please enter an asset path", "#ff4444");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate specific key based on path to update properly
|
||||
const key = `loaded_${path.split("/").pop()?.replace(".", "_")}_${Date.now()}`;
|
||||
this.currentKey = key;
|
||||
|
||||
this.updateStatus("Loading asset...", "#ffaa00");
|
||||
|
||||
// Load the spritesheet
|
||||
this.load.spritesheet(key, path, {
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
margin,
|
||||
spacing
|
||||
});
|
||||
|
||||
this.load.once("complete", () => {
|
||||
this.onAssetLoaded(key, frameWidth, frameHeight);
|
||||
});
|
||||
|
||||
this.load.once("loaderror", () => {
|
||||
this.updateStatus(`Failed to load: ${path}`, "#ff4444");
|
||||
this.currentKey = "";
|
||||
});
|
||||
|
||||
this.load.start();
|
||||
}
|
||||
|
||||
private onAssetLoaded(key: string, frameWidth: number, frameHeight: number) {
|
||||
// Clear previous sprite
|
||||
if (this.currentSprite) {
|
||||
this.currentSprite.destroy();
|
||||
}
|
||||
|
||||
// Get frame count
|
||||
const texture = this.textures.get(key);
|
||||
this.totalFrames = texture.frameTotal - 1; // Subtract __BASE frame
|
||||
this.currentFrame = 0;
|
||||
|
||||
// Create sprite in center of display area
|
||||
const areaBounds = this.displayArea.getBounds();
|
||||
const centerX = areaBounds.width / 2;
|
||||
const centerY = areaBounds.height / 2;
|
||||
|
||||
this.currentSprite = this.add.sprite(centerX, centerY, key, 0);
|
||||
this.currentSprite.setScale(4); // Scale up for visibility
|
||||
this.displayArea.add(this.currentSprite);
|
||||
|
||||
this.updateStatus(`Loaded: ${this.totalFrames} frames (${frameWidth}x${frameHeight})`, "#22aa44");
|
||||
|
||||
// Populate Grid
|
||||
this.populateFrameGrid();
|
||||
|
||||
// Clear selection
|
||||
this.selectedFrames = [];
|
||||
this.updateFrameText();
|
||||
}
|
||||
|
||||
private populateFrameGrid() {
|
||||
this.frameGridContainer.removeAll(true);
|
||||
|
||||
// Safety check
|
||||
if (!this.currentKey || !this.textures.exists(this.currentKey)) return;
|
||||
|
||||
const thumbSize = 32;
|
||||
const gap = 4;
|
||||
const cols = 8;
|
||||
|
||||
for (let i = 0; i <= this.totalFrames; i++) {
|
||||
const row = Math.floor(i / cols);
|
||||
const col = i % cols;
|
||||
const x = col * (thumbSize + gap);
|
||||
const y = row * (thumbSize + gap);
|
||||
|
||||
const bg = this.add.rectangle(x + thumbSize/2, y + thumbSize/2, thumbSize, thumbSize, 0x333333);
|
||||
bg.setInteractive();
|
||||
bg.on('pointerdown', () => {
|
||||
this.selectedFrames.push(i);
|
||||
this.updateFrameText();
|
||||
|
||||
// Visual feedback
|
||||
this.tweens.add({
|
||||
targets: bg,
|
||||
alpha: 0.5,
|
||||
duration: 50,
|
||||
yoyo: true
|
||||
});
|
||||
});
|
||||
|
||||
const sprite = this.add.sprite(x + thumbSize/2, y + thumbSize/2, this.currentKey, i);
|
||||
// Fit sprite within thumbSize
|
||||
const scale = Math.min((thumbSize - 4) / sprite.width, (thumbSize - 4) / sprite.height);
|
||||
sprite.setScale(scale);
|
||||
|
||||
const num = this.add.text(x + 2, y + 2, i.toString(), { fontSize: "8px", color: "#fff", backgroundColor: "#00000080" });
|
||||
|
||||
this.frameGridContainer.add([bg, sprite, num]);
|
||||
}
|
||||
}
|
||||
|
||||
private updateFrameText() {
|
||||
if (this.frameText) {
|
||||
this.frameText.setText(`[${this.selectedFrames.join(", ")}]`);
|
||||
}
|
||||
}
|
||||
|
||||
private createCustomAnimation() {
|
||||
if (!this.currentKey) {
|
||||
this.updateStatus("Load an asset first!", "#f44");
|
||||
return;
|
||||
}
|
||||
if (this.selectedFrames.length === 0) {
|
||||
this.updateStatus("Select frames first!", "#f44");
|
||||
return;
|
||||
}
|
||||
|
||||
const keyInput = document.getElementById('anim-key') as HTMLInputElement;
|
||||
const fpsInput = document.getElementById('anim-fps') as HTMLInputElement;
|
||||
const loopInput = document.getElementById('anim-loop') as HTMLInputElement;
|
||||
|
||||
if (!keyInput || !fpsInput) return;
|
||||
|
||||
const name = keyInput.value.trim() || `anim_${Date.now()}`;
|
||||
const fps = parseInt(fpsInput.value) || 10;
|
||||
const loop = loopInput.checked;
|
||||
|
||||
// Unique key for Phaser anims registry
|
||||
const animKey = `${name}`;
|
||||
|
||||
// Create Animation
|
||||
if (this.anims.exists(animKey)) {
|
||||
this.anims.remove(animKey);
|
||||
}
|
||||
|
||||
const anim = this.anims.create({
|
||||
key: animKey,
|
||||
frames: this.anims.generateFrameNumbers(this.currentKey, { frames: this.selectedFrames }),
|
||||
frameRate: fps,
|
||||
repeat: loop ? -1 : 0
|
||||
});
|
||||
|
||||
if (anim) {
|
||||
this.createdAnimations.set(animKey, anim as Phaser.Animations.Animation);
|
||||
this.updateStatus(`Created '${animKey}'`, "#4f4");
|
||||
this.refreshAnimList();
|
||||
|
||||
// Auto-play
|
||||
this.playAnimation(animKey);
|
||||
}
|
||||
}
|
||||
|
||||
private refreshAnimList() {
|
||||
const list = document.getElementById('anim-list');
|
||||
if (!list) return;
|
||||
|
||||
if (this.createdAnimations.size === 0) {
|
||||
list.innerHTML = `<div style="color: #666; font-style: italic;">No animations created</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
this.createdAnimations.forEach((_, key) => {
|
||||
// Generate code snippet for this animation
|
||||
const anim = this.anims.get(key);
|
||||
const frames = anim.frames.map(f => f.index).join(", ");
|
||||
const fps = anim.frameRate;
|
||||
const repeat = anim.repeat;
|
||||
|
||||
const code = `
|
||||
this.scene.anims.create({
|
||||
key: '${key}',
|
||||
frames: this.scene.anims.generateFrameNumbers('${this.currentKey.replace("loaded_", "warrior")}', { frames: [${frames}] }),
|
||||
frameRate: ${fps},
|
||||
repeat: ${repeat}
|
||||
});`;
|
||||
|
||||
html += `
|
||||
<div style="margin-bottom: 6px; padding: 4px; background: #222; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<span class="anim-item" data-key="${key}" style="cursor: pointer; color: #fa4; font-weight: bold;">${key}</span>
|
||||
<button class="copy-btn" data-code="${encodeURIComponent(code)}" style="font-size: 9px; cursor: pointer; background: #444; color: #fff; border: 1px solid #666; padding: 2px 6px;">COPY</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
list.innerHTML = html;
|
||||
|
||||
// Re-attach listeners since innerHTML nuked them
|
||||
list.querySelectorAll('.anim-item').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const k = (e.target as HTMLElement).dataset.key;
|
||||
if (k) this.playAnimation(k);
|
||||
});
|
||||
});
|
||||
|
||||
list.querySelectorAll('.copy-btn').forEach(el => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const code = decodeURIComponent((e.target as HTMLElement).dataset.code || "");
|
||||
console.log(code);
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
this.updateStatus("Code copied to clipboard!", "#4f4");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private cleanup() {
|
||||
// Clean up any loaded assets
|
||||
if (this.currentKey && this.textures.exists(this.currentKey)) {
|
||||
this.textures.remove(this.currentKey);
|
||||
}
|
||||
|
||||
// Remove animations
|
||||
if (this.anims.exists(`${this.currentKey}_anim`)) {
|
||||
this.anims.remove(`${this.currentKey}_anim`);
|
||||
}
|
||||
|
||||
// Explicitly remove DOM elements
|
||||
if (this.inputDom) { this.inputDom.destroy(); this.inputDom = undefined; }
|
||||
if (this.frameConfigDom) { this.frameConfigDom.destroy(); this.frameConfigDom = undefined; }
|
||||
if (this.animConfigDom) { this.animConfigDom.destroy(); this.animConfigDom = undefined; }
|
||||
if (this.animListDom) { this.animListDom.destroy(); this.animListDom = undefined; }
|
||||
}
|
||||
|
||||
update() {
|
||||
// Update current frame display if animation is playing
|
||||
if (this.isPlaying && this.currentSprite && this.currentSprite.anims.isPlaying) {
|
||||
this.currentFrame = this.currentSprite.anims.currentFrame?.index || 0;
|
||||
this.updateFrameText();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helper Methods ---
|
||||
|
||||
private playAnimation(key?: string) {
|
||||
if (!this.currentSprite) return;
|
||||
|
||||
// If key provided, play that. If not, play last created or fallback
|
||||
if (key) {
|
||||
this.currentSprite.play(key);
|
||||
this.updateStatus(`Playing '${key}'`, "#28f");
|
||||
this.isPlaying = true;
|
||||
} else if (this.createdAnimations.size > 0) {
|
||||
// Play first available
|
||||
const iterator = this.createdAnimations.keys();
|
||||
const firstKey = iterator.next().value;
|
||||
if (firstKey) {
|
||||
this.currentSprite.play(firstKey);
|
||||
this.updateStatus(`Playing '${firstKey}'`, "#28f");
|
||||
this.isPlaying = true;
|
||||
}
|
||||
} else if (this.totalFrames > 0) {
|
||||
// Fallback to default full sequence
|
||||
if (!this.anims.exists(`${this.currentKey}_anim`)) {
|
||||
this.anims.create({
|
||||
key: `${this.currentKey}_anim`,
|
||||
frames: this.anims.generateFrameNumbers(this.currentKey, { start: 0, end: this.totalFrames - 1 }),
|
||||
frameRate: this.animSpeed,
|
||||
repeat: -1
|
||||
});
|
||||
}
|
||||
this.currentSprite.play(`${this.currentKey}_anim`);
|
||||
this.updateStatus("Playing all frames", "#28f");
|
||||
this.isPlaying = true;
|
||||
}
|
||||
}
|
||||
|
||||
private pauseAnimation() {
|
||||
if (!this.currentSprite) return;
|
||||
|
||||
this.isPlaying = false;
|
||||
this.currentSprite.anims.pause();
|
||||
this.updateStatus("Paused", "#ffaa00");
|
||||
}
|
||||
|
||||
private stopAnimation() {
|
||||
if (!this.currentSprite) return;
|
||||
|
||||
this.isPlaying = false;
|
||||
this.currentSprite.anims.stop();
|
||||
this.currentFrame = 0;
|
||||
this.currentSprite.setFrame(0);
|
||||
this.updateFrameText();
|
||||
this.updateStatus("Stopped", "#ff4444");
|
||||
}
|
||||
|
||||
private prevFrame() {
|
||||
if (!this.currentSprite || this.totalFrames === 0) return;
|
||||
|
||||
this.pauseAnimation();
|
||||
this.currentFrame = (this.currentFrame - 1 + this.totalFrames) % this.totalFrames;
|
||||
this.currentSprite.setFrame(this.currentFrame);
|
||||
this.updateFrameText();
|
||||
}
|
||||
|
||||
private nextFrame() {
|
||||
if (!this.currentSprite || this.totalFrames === 0) return;
|
||||
|
||||
this.pauseAnimation();
|
||||
this.currentFrame = (this.currentFrame + 1) % this.totalFrames;
|
||||
this.currentSprite.setFrame(this.currentFrame);
|
||||
this.updateFrameText();
|
||||
}
|
||||
|
||||
private adjustSpeed(delta: number) {
|
||||
this.animSpeed = Math.max(1, Math.min(60, this.animSpeed + delta));
|
||||
|
||||
if (this.speedText) {
|
||||
this.speedText.setText(`Speed: ${this.animSpeed} FPS`);
|
||||
}
|
||||
|
||||
// Update animation if it exists
|
||||
if (this.anims.exists(`${this.currentKey}_anim`)) {
|
||||
const anim = this.anims.get(`${this.currentKey}_anim`);
|
||||
if (anim) {
|
||||
anim.frameRate = this.animSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
// Restart if playing default
|
||||
if (this.isPlaying && this.currentSprite && this.currentSprite.anims.getName() === `${this.currentKey}_anim`) {
|
||||
this.currentSprite.play(`${this.currentKey}_anim`);
|
||||
}
|
||||
}
|
||||
|
||||
private placeSprite(x: number, y: number) {
|
||||
if (!this.currentKey || this.totalFrames === 0) return;
|
||||
|
||||
const sprite = this.add.sprite(x, y, this.currentKey, this.currentFrame);
|
||||
sprite.setScale(4);
|
||||
sprite.setInteractive({ draggable: true });
|
||||
|
||||
// Make it draggable
|
||||
sprite.on("drag", (_pointer: any, dragX: number, dragY: number) => {
|
||||
sprite.x = dragX - this.displayArea.x;
|
||||
sprite.y = dragY - this.displayArea.y;
|
||||
});
|
||||
|
||||
// Right-click to remove
|
||||
sprite.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
|
||||
if (pointer.rightButtonDown()) {
|
||||
sprite.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
this.displayArea.add(sprite);
|
||||
}
|
||||
|
||||
private updateStatus(message: string, color: string) {
|
||||
if (this.statusText) {
|
||||
this.statusText.setText(message);
|
||||
this.statusText.setColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
private addSectionTitle(container: Phaser.GameObjects.Container, text: string, x: number, y: number) {
|
||||
const title = this.add.text(x, y, text, {
|
||||
fontSize: "16px",
|
||||
color: "#ff9922",
|
||||
fontStyle: "bold",
|
||||
fontFamily: "Verdana, sans-serif",
|
||||
letterSpacing: 2
|
||||
});
|
||||
container.add(title);
|
||||
}
|
||||
|
||||
private addLabel(container: Phaser.GameObjects.Container, text: string, x: number, y: number) {
|
||||
const label = this.add.text(x, y, text, {
|
||||
fontSize: "12px",
|
||||
color: "#aaa",
|
||||
fontFamily: "Verdana, sans-serif"
|
||||
});
|
||||
container.add(label);
|
||||
}
|
||||
|
||||
// Note: createControlPanel uses createButton/createSmallButton too, but I defined them there as local?
|
||||
// No, I called this.createButton. So I need them here.
|
||||
|
||||
private createButton(x: number, y: number, text: string, color: number, callback: () => void) {
|
||||
const width = 120;
|
||||
const height = 40;
|
||||
|
||||
const bg = this.add.graphics();
|
||||
bg.fillStyle(color, 0.8);
|
||||
bg.fillRoundedRect(-width / 2, -height / 2, width, height, 4);
|
||||
|
||||
const border = this.add.graphics();
|
||||
border.lineStyle(2, 0xffffff, 0.4);
|
||||
border.strokeRoundedRect(-width / 2, -height / 2, width, height, 4);
|
||||
|
||||
const txt = this.add.text(0, 0, text, {
|
||||
fontSize: "14px",
|
||||
color: "#ffffff",
|
||||
fontFamily: "Verdana, sans-serif",
|
||||
fontStyle: "bold"
|
||||
}).setOrigin(0.5);
|
||||
|
||||
const container = this.add.container(x, y, [bg, border, txt]);
|
||||
container.setSize(width, height);
|
||||
container.setInteractive({ useHandCursor: true });
|
||||
|
||||
container.on("pointerover", () => {
|
||||
bg.clear();
|
||||
bg.fillStyle(color, 1);
|
||||
bg.fillRoundedRect(-width / 2, -height / 2, width, height, 4);
|
||||
});
|
||||
|
||||
container.on("pointerout", () => {
|
||||
bg.clear();
|
||||
bg.fillStyle(color, 0.8);
|
||||
bg.fillRoundedRect(-width / 2, -height / 2, width, height, 4);
|
||||
});
|
||||
|
||||
container.on("pointerdown", callback);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private createSmallButton(x: number, y: number, text: string, color: number, callback: () => void) {
|
||||
const width = 80;
|
||||
const height = 32;
|
||||
|
||||
const bg = this.add.graphics();
|
||||
bg.fillStyle(color, 0.7);
|
||||
bg.fillRoundedRect(0, 0, width, height, 4);
|
||||
|
||||
const txt = this.add.text(width / 2, height / 2, text, {
|
||||
fontSize: "11px",
|
||||
color: "#ffffff",
|
||||
fontFamily: "monospace",
|
||||
fontStyle: "bold"
|
||||
}).setOrigin(0.5);
|
||||
|
||||
const container = this.add.container(x, y, [bg, txt]);
|
||||
container.setSize(width, height);
|
||||
container.setInteractive({ useHandCursor: true });
|
||||
|
||||
container.on("pointerover", () => {
|
||||
bg.clear();
|
||||
bg.fillStyle(color, 1);
|
||||
bg.fillRoundedRect(0, 0, width, height, 4);
|
||||
});
|
||||
|
||||
container.on("pointerout", () => {
|
||||
bg.clear();
|
||||
bg.fillStyle(color, 0.7);
|
||||
bg.fillRoundedRect(0, 0, width, height, 4);
|
||||
});
|
||||
|
||||
container.on("pointerdown", callback);
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,8 @@ export class GameScene extends Phaser.Scene {
|
||||
private entityManager!: EntityManager;
|
||||
private progressionManager: ProgressionManager = new ProgressionManager();
|
||||
|
||||
private turnCount = 0; // Track turns for mana regen
|
||||
|
||||
constructor() {
|
||||
super("GameScene");
|
||||
}
|
||||
@@ -261,6 +263,19 @@ export class GameScene extends Phaser.Scene {
|
||||
const enemyStep = stepUntilPlayerTurn(this.world, this.playerId, this.entityManager);
|
||||
this.awaitingPlayer = enemyStep.awaitingPlayerId === this.playerId;
|
||||
|
||||
// Increment turn counter and handle mana regeneration
|
||||
this.turnCount++;
|
||||
if (this.turnCount % GAME_CONFIG.mana.regenInterval === 0) {
|
||||
const player = this.world.actors.get(this.playerId) as CombatantActor;
|
||||
if (player && player.stats.mana < player.stats.maxMana) {
|
||||
const regenAmount = Math.min(
|
||||
GAME_CONFIG.mana.regenPerTurn,
|
||||
player.stats.maxMana - player.stats.mana
|
||||
);
|
||||
player.stats.mana += regenAmount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Process events for visual fx
|
||||
const allEvents = [...playerEvents, ...enemyStep.events];
|
||||
|
||||
@@ -54,9 +54,10 @@ export class MenuScene extends Phaser.Scene {
|
||||
});
|
||||
|
||||
// Buttons
|
||||
const buttonYStart = height * 0.65;
|
||||
const buttonYStart = height * 0.60;
|
||||
const startBtn = this.createButton(width / 2, buttonYStart, "ENTER DUNGEON", 0x2288ff);
|
||||
const optBtn = this.createButton(width / 2, buttonYStart + 80, "OPTIONS", 0x444444);
|
||||
const assetViewerBtn = this.createButton(width / 2, buttonYStart + 160, "ASSET VIEWER", 0xff9922);
|
||||
|
||||
startBtn.on("pointerdown", () => {
|
||||
this.cameras.main.fadeOut(1000, 0, 0, 0);
|
||||
@@ -68,6 +69,13 @@ export class MenuScene extends Phaser.Scene {
|
||||
optBtn.on("pointerdown", () => {
|
||||
console.log("Options clicked");
|
||||
});
|
||||
|
||||
assetViewerBtn.on("pointerdown", () => {
|
||||
this.cameras.main.fadeOut(500, 0, 0, 0);
|
||||
this.cameras.main.once(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => {
|
||||
this.scene.start("AssetViewerScene");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private createWindEffect() {
|
||||
|
||||
@@ -34,6 +34,16 @@ export class PreloadScene extends Phaser.Scene {
|
||||
});
|
||||
|
||||
this.load.on("complete", () => {
|
||||
// Create Global Animations
|
||||
GAME_CONFIG.animations.forEach(anim => {
|
||||
this.anims.create({
|
||||
key: anim.key,
|
||||
frames: this.anims.generateFrameNumbers(anim.textureKey, { frames: [...anim.frames] }), // Spread to make mutable
|
||||
frameRate: anim.frameRate,
|
||||
repeat: anim.repeat
|
||||
});
|
||||
});
|
||||
|
||||
progressBar.destroy();
|
||||
progressBox.destroy();
|
||||
loadingText.destroy();
|
||||
|
||||
@@ -6,6 +6,7 @@ export class HudComponent {
|
||||
private scene: Phaser.Scene;
|
||||
private floorText!: Phaser.GameObjects.Text;
|
||||
private healthBar!: Phaser.GameObjects.Graphics;
|
||||
private manaBar!: Phaser.GameObjects.Graphics;
|
||||
private expBar!: Phaser.GameObjects.Graphics;
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
@@ -25,8 +26,12 @@ export class HudComponent {
|
||||
this.scene.add.text(20, 55, "HP", { fontSize: "14px", color: "#ff8888", fontStyle: "bold" }).setScrollFactor(0).setDepth(1000);
|
||||
this.healthBar = this.scene.add.graphics().setScrollFactor(0).setDepth(1000);
|
||||
|
||||
// Mana Bar
|
||||
this.scene.add.text(20, 75, "MP", { fontSize: "14px", color: "#88ccff", fontStyle: "bold" }).setScrollFactor(0).setDepth(1000);
|
||||
this.manaBar = this.scene.add.graphics().setScrollFactor(0).setDepth(1000);
|
||||
|
||||
// EXP Bar
|
||||
this.scene.add.text(20, 85, "EXP", { fontSize: "14px", color: "#8888ff", fontStyle: "bold" }).setScrollFactor(0).setDepth(1000);
|
||||
this.scene.add.text(20, 95, "EXP", { fontSize: "14px", color: "#8888ff", fontStyle: "bold" }).setScrollFactor(0).setDepth(1000);
|
||||
this.expBar = this.scene.add.graphics().setScrollFactor(0).setDepth(1000);
|
||||
}
|
||||
|
||||
@@ -46,15 +51,26 @@ export class HudComponent {
|
||||
this.healthBar.lineStyle(2, 0xffffff, 0.5);
|
||||
this.healthBar.strokeRect(60, 58, 200, 12);
|
||||
|
||||
// Update Mana Bar
|
||||
this.manaBar.clear();
|
||||
this.manaBar.fillStyle(0x333333, 0.8);
|
||||
this.manaBar.fillRect(60, 78, 200, 12);
|
||||
|
||||
const manaPercent = Phaser.Math.Clamp(stats.mana / stats.maxMana, 0, 1);
|
||||
this.manaBar.fillStyle(0x3399ff, 1);
|
||||
this.manaBar.fillRect(60, 78, 200 * manaPercent, 12);
|
||||
this.manaBar.lineStyle(2, 0xffffff, 0.5);
|
||||
this.manaBar.strokeRect(60, 78, 200, 12);
|
||||
|
||||
// Update EXP Bar
|
||||
this.expBar.clear();
|
||||
this.expBar.fillStyle(0x333333, 0.8);
|
||||
this.expBar.fillRect(60, 88, 200, 8);
|
||||
this.expBar.fillRect(60, 98, 200, 8);
|
||||
|
||||
const expPercent = Phaser.Math.Clamp(stats.exp / stats.expToNextLevel, 0, 1);
|
||||
this.expBar.fillStyle(GAME_CONFIG.rendering.expOrbColor, 1);
|
||||
this.expBar.fillRect(60, 88, 200 * expPercent, 8);
|
||||
this.expBar.fillRect(60, 98, 200 * expPercent, 8);
|
||||
this.expBar.lineStyle(1, 0xffffff, 0.3);
|
||||
this.expBar.strokeRect(60, 88, 200, 8);
|
||||
this.expBar.strokeRect(60, 98, 200, 8);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user