From a7091c70c6bfe161535f5e7140aaf2d9d80f023c Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Mon, 5 Jan 2026 18:57:17 +1100 Subject: [PATCH] Add in mana and an asset viewer --- .../sprites/actors/player/soldier/Idle.png | Bin 0 -> 2475 bytes src/core/config/GameConfig.ts | 45 +- src/core/types.ts | 2 + .../__tests__/ProgressionManager.test.ts | 2 + src/engine/__tests__/generator.test.ts | 16 +- src/engine/simulation/simulation.ts | 2 + src/engine/world/generator.ts | 2 + src/main.ts | 6 +- src/rendering/DungeonRenderer.ts | 81 +- src/scenes/AssetViewerScene.ts | 744 ++++++++++++++++++ src/scenes/GameScene.ts | 15 + src/scenes/MenuScene.ts | 10 +- src/scenes/PreloadScene.ts | 10 + src/ui/components/HudComponent.ts | 24 +- 14 files changed, 866 insertions(+), 93 deletions(-) create mode 100644 public/assets/sprites/actors/player/soldier/Idle.png create mode 100644 src/scenes/AssetViewerScene.ts diff --git a/public/assets/sprites/actors/player/soldier/Idle.png b/public/assets/sprites/actors/player/soldier/Idle.png new file mode 100644 index 0000000000000000000000000000000000000000..035fca0ff545641908e8afbc9b04068c135ed61f GIT binary patch literal 2475 zcmaJ@dpOkV7XFPfR}3YFL3Aayh-t={WaKjBxMU~xkS)e;WV_Wcnwrc=O0H3IZ_tG! z2I=B{OK!!4nl{Fy2;)+2?FgOU{_C9QIs2SHzHdEit@nG@THo`$@3Z1vov8|F92x)s z1qXW@H_%ptCj$uwZ&pd6A^@O#9c-+QM?q$@jxn4=H9Mle|4GE6Q3npje~9x=ys6<` zHsboMIS=3J7P95B!96^d)_*tH4=ad0VY2&1WSeKUlPTfREt)lMQSv8oGB_tVx`)(v zuJ`k5_h-`Np}zBdOBb6YeHTWT8oyJ&rC`*~aw!_2Y63XcOD$q(h;6Fc;?>%7T(crhF7bB^9Wma2gs8t{Hq+_ zpyyzGw|%!-B$uN2z4`4msO*q^>dgbQTp}NfRoO^96Sc1-*#8&LyDGz;`!g34D5sC_ zzb%C^kSA*cOMf46zfTK)NVe0MD)J$YnImvxZ^gBS?Q)Zjliz*ZQ5CNRh!?o2!ch~R z;{L)2G~Pzb;(}GgocOU*!HWg)kbZ-gzc(TYFNAVZts6r4k#Kygxau>q>Xqn&tz3k* zc1Nv$ae>bk?NxKAx!=laf@)yk=fNYAi4lds(3ht~^kahIi$5?^mJ}tH{x;`1?QgM3 zI&|(jf4XG5*BQPF%xQ4uu*8O!9MeGaxO{QNQRhfV2*;4K;!ZO+9(*sibGt>MNIoK9 zKDs(mx^g?zkCGzK>gW~Acv|T-x9@uKNFbrV%KhW)TvQfb64bfwk5~z@t!ED`W`R&^ zt@-2JnyL@6GI9mQ()14cd-q4VHIi!s)jNwNOcs_wg=JJKwp%8B57?E<7Dcidp@5)wv{a&Eu$? z9IT1#zdrU+x9XCZR$^97{a%z3oET8~(~Oo;3SR28jgbkwzD2BCGA>eFdGhtxOe(pVs!P`itH@&%r8Og!!RO+T0Cj-59q+cUoaz@B?=CofdZoG-Gx02vnfTkH+a`$EG<}cuV{$1SjJ-w^5{S&4 zCA3_}4xIhpfXv@vnmL6~S@!j~g=HrA4Fh?K9zSkOh&2RRvdt*hDDU$H_dU&aP21Ii_zCPD*~8DSz1=5Nidde7y?M+M z5w74mSV5)BAxYs{_M6!AscXbkeO~4usaxQAlBp1Z7Z9jCg^gYd> z=Cr{jtYLruXvvkvo*Iq*(ag5;u0o%ut9vo{`Qv)RAAT-W=qy#t@(2jzs9y#_#H>In& z^f%JErY#QwZC#tD=l2VfK3bjw-y}Qn_(cu1V+J$@T7oE%T ztcj-v#G7cJB3*h#%TR|jiN5;*F}r1hP{vk<;#94fvP&I`d2R#Cf}5(U=u4~@-`YA{ z=o__<-NG3uU#Z%|YCV2QgI?z!c^&*qnU!HU*PZlOjh4j2gTq5Vn=~}ivXf_Tx3#zT z8=GC5f9Y`-8@=%piQ^n$n0`s!#i+OP%U-)iVH&(49;&s){KS|4R480;7rR-1+~cwf z|JOQ`{87fsZh`_k50WIbLx*V0DsvNCGw-#Y#HG0FT@3Z~FtPqrzP5AV#1G-n*|4JS zcC+3$m*t5;<^ea(JKN5q+I?D2#p zGWrWeD4}qUX8dXSq%iT$J~^C`)RdX?a_hE6;x`|!txFHA1g88`zBpNbt;1*?>!y?% z&dX^>!OwguZpQIT=fP+15YJh)mRR3})rvCdRHt@PpPSVkmxf;6SHpK(4-GEXL3??d z9Mo(9bb$o&bScea@nduGFQ{cba#U&rNS?RK<7-qfMg61Y@?lor6_J1ZU~G34z|uV { stats: { maxHp: 20, hp: 20, + maxMana: 0, + mana: 0, level: 1, exp: 0, expToNextLevel: 100, diff --git a/src/engine/__tests__/generator.test.ts b/src/engine/__tests__/generator.test.ts index 05a4a51..e190f46 100644 --- a/src/engine/__tests__/generator.test.ts +++ b/src/engine/__tests__/generator.test.ts @@ -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: [] diff --git a/src/engine/simulation/simulation.ts b/src/engine/simulation/simulation.ts index ab4be48..7e95347 100644 --- a/src/engine/simulation/simulation.ts +++ b/src/engine/simulation/simulation.ts @@ -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; diff --git a/src/engine/world/generator.ts b/src/engine/world/generator.ts index e68c0a6..de8183c 100644 --- a/src/engine/world/generator.ts +++ b/src/engine/world/generator.ts @@ -308,6 +308,8 @@ function placeEnemies(floor: number, rooms: Room[], actors: Map 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, diff --git a/src/main.ts b/src/main.ts index 6f02be6..2b72dc3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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] }); diff --git a/src/rendering/DungeonRenderer.ts b/src/rendering/DungeonRenderer.ts index 6ab265a..d423f7d 100644 --- a/src/rendering/DungeonRenderer.ts +++ b/src/rendering/DungeonRenderer.ts @@ -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(); diff --git a/src/scenes/AssetViewerScene.ts b/src/scenes/AssetViewerScene.ts new file mode 100644 index 0000000..954850a --- /dev/null +++ b/src/scenes/AssetViewerScene.ts @@ -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 = 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 = ` +
+
Path:
+
+ Size: x + +
+
+ Margin: + Spacing: + +
+
+ `; + 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 = ` +
+
Key:
+
FPS:
+
Loop:
+ +
+ `; + 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 = ` +
+
No animations created
+
+ `; + 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 = `
No animations created
`; + 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 += ` +
+ ${key} + +
+ `; + }); + 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; + } +} diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index 19ae26b..c2d1546 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -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]; diff --git a/src/scenes/MenuScene.ts b/src/scenes/MenuScene.ts index 7a94afa..46fdb68 100644 --- a/src/scenes/MenuScene.ts +++ b/src/scenes/MenuScene.ts @@ -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() { diff --git a/src/scenes/PreloadScene.ts b/src/scenes/PreloadScene.ts index 44da41a..a7ecefc 100644 --- a/src/scenes/PreloadScene.ts +++ b/src/scenes/PreloadScene.ts @@ -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(); diff --git a/src/ui/components/HudComponent.ts b/src/ui/components/HudComponent.ts index 6972969..04f55a4 100644 --- a/src/ui/components/HudComponent.ts +++ b/src/ui/components/HudComponent.ts @@ -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); } }