Created
November 11, 2025 23:17
-
-
Save StringManolo/d328980c5cba4d234b41f6d7a22fe5ac to your computer and use it in GitHub Desktop.
Chat GPT5 generated (with thinking)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| export class BattleScene extends Phaser.Scene { | |
| constructor() { | |
| super({ key: 'BattleScene' }); | |
| } | |
| init(data) { | |
| this.playerData = data.playerData || null; | |
| this.onBattleEnd = data.onBattleEnd || null; | |
| this.terrain = data.terrain || 'grass'; | |
| } | |
| preload() { | |
| // Load battle backgrounds | |
| this.load.image('battle_grass', 'assets/images/battles/battle_grass.png'); | |
| this.load.image('battle_forest', 'assets/images/battles/battle_forest.png'); | |
| this.load.image('battle_mountain', 'assets/images/battles/battle_mountain.png'); | |
| this.load.image('battle_dirt', 'assets/images/battles/battle_dirt.png'); | |
| ``` | |
| // Load basic enemies | |
| this.load.image('goat', 'assets/images/enemies/goat.png'); | |
| this.load.image('war_dwarf', 'assets/images/enemies/war_dwarf.png'); | |
| this.load.image('snake', 'assets/images/enemies/snake.png'); | |
| this.load.image('wolf', 'assets/images/enemies/wolf.png'); | |
| this.load.image('fly', 'assets/images/enemies/fly.png'); | |
| this.load.image('war_blob', 'assets/images/enemies/war_blob.png'); | |
| this.load.image('war_orc', 'assets/images/enemies/war_orc.png'); | |
| this.load.image('magic_demon', 'assets/images/enemies/magic_demon.png'); | |
| // Warrior images (attack frames) | |
| this.load.image("warrior_basic_attack", "assets/images/battleAnimations/warrior_basic_1.png"); | |
| this.load.image("warrior_basic_attack_2", "assets/images/battleAnimations/warrior_basic_2.png"); | |
| this.load.image("warrior_basic_attack_3", "assets/images/battleAnimations/warrior_basic_3.png"); | |
| this.load.image("warrior_basic_attack_4", "assets/images/battleAnimations/warrior_basic_4.png"); | |
| // Sounds | |
| this.load.audio("warrior_slash", "assets/audio/warrior_slash.wav"); | |
| this.load.audio("battle_music", "assets/audio/battle.mp3"); | |
| ``` | |
| } | |
| create() { | |
| // ------------------------- | |
| // Background + enemy + warrior (kept from original) | |
| // ------------------------- | |
| let backgroundKey = "battle_grass"; | |
| const terrainMap = { | |
| 'grass': 'battle_grass', | |
| 'forest': 'battle_forest', | |
| 'mountain': 'battle_mountain', | |
| 'path': 'battle_dirt', | |
| 'water': 'battle_grass' | |
| }; | |
| if (this.terrain && terrainMap[this.terrain]) backgroundKey = terrainMap[this.terrain]; | |
| const background = this.add.image(400, 300, backgroundKey).setDisplaySize(800, 600); | |
| ``` | |
| // Enemy selection (kept logic but now we also attach stats) | |
| this.enemyConfig = this.getEnemyForTerrain(this.terrain); | |
| this.enemySprite = this.add.image(600, 320, this.enemyConfig.key).setDisplaySize(175, 175); | |
| this.enemyNameTag = this.add.text(600, 260, this.enemyConfig.name, { | |
| fontSize: '20px', | |
| fill: '#fff', | |
| backgroundColor: '#000000' | |
| }).setOrigin(0.5); | |
| // Player warrior sprite (kept) | |
| this.warrior = this.add.sprite(200, 460, 'warrior_basic_attack').setOrigin(0.5, 1).setDisplaySize(175, 175).setDepth(2); | |
| // Keep music | |
| this.sound.play('battle_music', { loop: true }); | |
| // ------------------------- | |
| // Combat system initialization (new) | |
| // ------------------------- | |
| this.isAttacking = false; | |
| this.atbRunning = true; // whether ATB bars fill | |
| this.atbPaused = false; // paused when someone chooses action | |
| this.actors = []; // will contain player and enemy actor objects | |
| // Default player stats; allow overriding with this.playerData | |
| const defaultPlayer = { | |
| id: 'player1', | |
| name: (this.playerData && this.playerData.name) || 'Warrior', | |
| sprite: this.warrior, | |
| hp: (this.playerData && this.playerData.hp) || 500, | |
| maxHp: (this.playerData && this.playerData.maxHp) || 500, | |
| mp: (this.playerData && this.playerData.mp) || 50, | |
| maxMp: (this.playerData && this.playerData.maxMp) || 50, | |
| atk: (this.playerData && this.playerData.atk) || 60, | |
| mag: (this.playerData && this.playerData.mag) || 30, | |
| def: (this.playerData && this.playerData.def) || 20, | |
| speed: (this.playerData && this.playerData.speed) || 40, | |
| level: (this.playerData && this.playerData.level) || 1, | |
| atb: 0, | |
| isPlayer: true, | |
| defending: false | |
| }; | |
| // Enemy default stats derived from pool entry weights (deterministic-ish) | |
| const enemyDefaults = this.enemyStatsFromKey(this.enemyConfig.key); | |
| this.enemy = { | |
| id: 'enemy1', | |
| name: this.enemyConfig.name, | |
| sprite: this.enemySprite, | |
| hp: enemyDefaults.maxHp, | |
| maxHp: enemyDefaults.maxHp, | |
| mp: 0, | |
| maxMp: 0, | |
| atk: enemyDefaults.atk, | |
| mag: enemyDefaults.mag, | |
| def: enemyDefaults.def, | |
| speed: enemyDefaults.speed, | |
| atb: 0, | |
| isPlayer: false, | |
| defending: false | |
| }; | |
| this.actors.push(defaultPlayer, this.enemy); | |
| // UI: HP bars and ATB bars | |
| this.createBattleUI(); | |
| // Input: open menu on click when it's player's turn AND ready | |
| this.input.off('pointerdown'); | |
| this.input.on('pointerdown', (pointer) => { | |
| // if menu is visible do nothing, else if player ATB full, show menu | |
| if (this.menuVisible) return; | |
| const player = this.actors.find(a => a.isPlayer); | |
| if (player && player.atb >= 100 && !this.atbPaused) { | |
| // ensure we pause other bars and show menu | |
| this.pauseATB(); | |
| this.showActionMenu(player); | |
| } | |
| }); | |
| // Begin ATB loop handled in update() | |
| this.menuVisible = false; | |
| this.currentActorTurn = null; | |
| // Small battle log | |
| this.log = []; | |
| ``` | |
| } | |
| update(time, delta) { | |
| if (!this.atbRunning || this.atbPaused) return; | |
| // delta in ms -> convert to seconds | |
| const dt = delta / 1000; | |
| for (const actor of this.actors) { | |
| actor.atb += actor.speed * dt; // linear fill: speed points per second | |
| if (actor.atb >= 100) { | |
| actor.atb = 100; | |
| this.onActorReady(actor); | |
| break; // stop processing others in this frame (we will pause) | |
| } | |
| } | |
| this.updateATBBars(); | |
| } | |
| // ------------------------- | |
| // ATB / Turn handling | |
| // ------------------------- | |
| onActorReady(actor) { | |
| // Pause ATB filling and present action selection if player, otherwise AI chooses | |
| this.pauseATB(); | |
| this.currentActorTurn = actor; | |
| if (actor.isPlayer) { | |
| this.showActionMenu(actor); | |
| } else { | |
| this.enemyChooseAction(actor); | |
| } | |
| } | |
| pauseATB() { | |
| this.atbPaused = true; | |
| this.menuVisible = false; | |
| } | |
| resumeATB() { | |
| this.atbPaused = false; | |
| this.menuVisible = false; | |
| // Reset any actors with atb >= 100 but who didn't get to act? leave as is | |
| } | |
| // ------------------------- | |
| // Action Menu UI | |
| // ------------------------- | |
| createBattleUI() { | |
| // Player UI group (left) | |
| const player = this.actors[0]; | |
| const enemy = this.actors[1]; | |
| ``` | |
| // HP bar backgrounds | |
| this.playerHpBg = this.add.rectangle(140, 80, 260, 28, 0x222222).setOrigin(0, 0.5); | |
| this.playerHpBar = this.add.rectangle(140, 80, 260 * (player.hp / player.maxHp), 28, 0xff4444).setOrigin(0, 0.5); | |
| this.playerNameText = this.add.text(140, 60, `${player.name} Lv${player.level}`, { fontSize: '14px', fill: '#fff' }).setOrigin(0, 0.5); | |
| this.playerHpText = this.add.text(140 + 130, 80, `${player.hp}/${player.maxHp}`, { fontSize: '12px', fill: '#fff' }).setOrigin(0.5); | |
| // Enemy HP bar (top-right) | |
| this.enemyHpBg = this.add.rectangle(460, 80, 260, 28, 0x222222).setOrigin(0, 0.5); | |
| this.enemyHpBar = this.add.rectangle(460, 80, 260 * (enemy.hp / enemy.maxHp), 28, 0xff4444).setOrigin(0, 0.5); | |
| this.enemyHpText = this.add.text(460 + 130, 80, `${enemy.hp}/${enemy.maxHp}`, { fontSize: '12px', fill: '#fff' }).setOrigin(0.5); | |
| this.enemyNameTextUI = this.add.text(460, 60, enemy.name, { fontSize: '14px', fill: '#fff' }).setOrigin(0, 0.5); | |
| // ATB bars (small) | |
| this.playerAtbBarBg = this.add.rectangle(140, 110, 260, 10, 0x111111).setOrigin(0, 0.5); | |
| this.playerAtbBar = this.add.rectangle(140, 110, 260 * (this.actors[0].atb / 100), 10, 0x00cc00).setOrigin(0, 0.5); | |
| this.enemyAtbBarBg = this.add.rectangle(460, 110, 260, 10, 0x111111).setOrigin(0, 0.5); | |
| this.enemyAtbBar = this.add.rectangle(460, 110, 260 * (this.actors[1].atb / 100), 10, 0x00cc00).setOrigin(0, 0.5); | |
| // Action menu container (hidden initially) | |
| this.menuContainer = this.add.container(120, 370).setDepth(50).setVisible(false); | |
| const menuBg = this.add.rectangle(0, 0, 560, 160, 0x000000, 0.7).setOrigin(0); | |
| this.menuContainer.add(menuBg); | |
| // Menu options (buttons) | |
| const opts = ['Attack', 'Magic', 'Skill', 'Defend', 'Items', 'Run']; | |
| this.menuButtons = []; | |
| for (let i = 0; i < opts.length; i++) { | |
| const y = 20 + i * 22; | |
| const txt = this.add.text(20, y, opts[i], { fontSize: '18px', fill: '#fff' }).setInteractive({ useHandCursor: true }); | |
| txt.on('pointerdown', () => { | |
| this.menuOptionSelected(opts[i]); | |
| }); | |
| txt.on('pointerover', () => txt.setStyle({ fill: '#ff0' })); | |
| txt.on('pointerout', () => txt.setStyle({ fill: '#fff' })); | |
| this.menuContainer.add(txt); | |
| this.menuButtons.push(txt); | |
| } | |
| this.logText = this.add.text(20, 550, '', { fontSize: '14px', fill: '#fff' }).setDepth(60); | |
| this.updateLog(); | |
| ``` | |
| } | |
| updateATBBars() { | |
| this.playerAtbBar.width = 260 * (this.actors[0].atb / 100); | |
| this.enemyAtbBar.width = 260 * (this.actors[1].atb / 100); | |
| } | |
| updateHPBars() { | |
| const player = this.actors[0]; | |
| const enemy = this.actors[1]; | |
| this.playerHpBar.width = 260 * (Math.max(0, player.hp) / player.maxHp); | |
| this.playerHpText.setText(`${Math.max(0, Math.round(player.hp))}/${player.maxHp}`); | |
| this.enemyHpBar.width = 260 * (Math.max(0, enemy.hp) / enemy.maxHp); | |
| this.enemyHpText.setText(`${Math.max(0, Math.round(enemy.hp))}/${enemy.maxHp}`); | |
| } | |
| // ------------------------- | |
| // Menu and action handling | |
| // ------------------------- | |
| showActionMenu(actor) { | |
| if (!actor.isPlayer) return; | |
| this.menuVisible = true; | |
| this.menuContainer.setVisible(true); | |
| // highlight Attack by default | |
| this.menuButtons.forEach(b => b.setStyle({ backgroundColor: null })); | |
| } | |
| hideActionMenu() { | |
| this.menuVisible = false; | |
| this.menuContainer.setVisible(false); | |
| } | |
| menuOptionSelected(option) { | |
| const actor = this.currentActorTurn; | |
| if (!actor) return; | |
| this.hideActionMenu(); | |
| switch (option) { | |
| case 'Attack': | |
| this.doAttack(actor, this.actors[1]); // player attacks enemy | |
| break; | |
| case 'Magic': | |
| this.doMagic(actor, this.actors[1]); | |
| break; | |
| case 'Skill': | |
| this.doSkill(actor, this.actors[1]); | |
| break; | |
| case 'Defend': | |
| this.doDefend(actor); | |
| break; | |
| case 'Items': | |
| this.doItem(actor); | |
| break; | |
| case 'Run': | |
| this.tryRun(actor); | |
| break; | |
| } | |
| } | |
| // ------------------------- | |
| // Actions implementations | |
| // ------------------------- | |
| doAttack(actor, target) { | |
| // Move the original startAttack logic into this method for the player. | |
| if (actor.isPlayer) { | |
| // Play attack animation sequence on warrior sprite (original code preserved/adapted) | |
| this.isAttacking = true; | |
| const originalX = this.warrior.x; | |
| const originalY = this.warrior.y; | |
| this.tweens.add({ | |
| targets: this.warrior, | |
| x: 400, | |
| y: 400, | |
| displayWidth: 175 * 0.7, | |
| displayHeight: 175 * 0.7, | |
| duration: 200, | |
| onComplete: () => { | |
| this.warrior.setTexture('warrior_basic_attack_2'); | |
| this.time.delayedCall(25, () => { | |
| this.warrior.setTexture('warrior_basic_attack_3'); | |
| this.sound.play('warrior_slash', { volume: 0.8 }); | |
| this.time.delayedCall(20, () => { | |
| this.warrior.setTexture('warrior_basic_attack_4'); | |
| this.time.delayedCall(60, () => { | |
| this.warrior.setTexture('warrior_basic_attack'); | |
| this.tweens.add({ | |
| targets: this.warrior, | |
| x: originalX, | |
| y: originalY, | |
| displayWidth: 175, | |
| displayHeight: 175, | |
| duration: 200, | |
| onComplete: () => { | |
| this.time.delayedCall(400, () => { | |
| // calculate damage and apply | |
| const dmg = this.calculateDamage(actor, target, 'physical'); | |
| target.hp -= dmg; | |
| this.addToLog(`${actor.name} hits ${target.name} for ${dmg} damage.`); | |
| this.updateHPBars(); | |
| this.isAttacking = false; | |
| this.endTurn(actor); | |
| }); | |
| } | |
| }); | |
| }); | |
| }); | |
| }); | |
| } | |
| }); | |
| } else { | |
| // enemy attack: quick animation / flash | |
| const dmg = this.calculateDamage(actor, this.actors[0], 'physical'); | |
| this.actors[0].hp -= dmg; | |
| this.addToLog(`${actor.name} attacks ${this.actors[0].name} for ${dmg}.`); | |
| this.updateHPBars(); | |
| this.time.delayedCall(600, () => this.endTurn(actor)); | |
| } | |
| } | |
| doMagic(actor, target) { | |
| if (actor.mp <= 0) { | |
| this.addToLog(`${actor.name} has no MP.`); | |
| this.time.delayedCall(300, () => this.endTurn(actor)); | |
| return; | |
| } | |
| const mpCost = 8; | |
| if (actor.mp < mpCost) { | |
| this.addToLog(`${actor.name} doesn't have enough MP.`); | |
| this.time.delayedCall(300, () => this.endTurn(actor)); | |
| return; | |
| } | |
| actor.mp -= mpCost; | |
| const dmg = Math.round((actor.mag * 1.4) - (target.def * 0.4)); | |
| target.hp -= Math.max(1, dmg); | |
| this.addToLog(`${actor.name} casts a spell on ${target.name} for ${Math.max(1, dmg)} damage.`); | |
| this.updateHPBars(); | |
| this.time.delayedCall(500, () => this.endTurn(actor)); | |
| } | |
| doSkill(actor, target) { | |
| // generic skill: stronger physical hit but costs nothing here | |
| const dmg = this.calculateDamage(actor, target, 'skill'); | |
| target.hp -= dmg; | |
| this.addToLog(`${actor.name} uses a skill on ${target.name} for ${dmg}.`); | |
| this.updateHPBars(); | |
| this.time.delayedCall(400, () => this.endTurn(actor)); | |
| } | |
| doDefend(actor) { | |
| actor.defending = true; | |
| this.addToLog(`${actor.name} braces for incoming attacks (Defend).`); | |
| this.time.delayedCall(300, () => this.endTurn(actor)); | |
| } | |
| doItem(actor) { | |
| // Very small item system: 1 potion heals 100 | |
| const heal = 100; | |
| actor.hp = Math.min(actor.maxHp, actor.hp + heal); | |
| this.addToLog(`${actor.name} uses a Potion and recovers ${heal} HP.`); | |
| this.updateHPBars(); | |
| this.time.delayedCall(300, () => this.endTurn(actor)); | |
| } | |
| tryRun(actor) { | |
| // simple flee calculation based on speed | |
| const chance = Math.random() * 100; | |
| if (actor.speed > this.actors[1].speed) { | |
| this.addToLog(`${actor.name} successfully fled the battle.`); | |
| this.sound.stopAll(); | |
| this.scene.stop(); | |
| if (this.onBattleEnd) { | |
| this.onBattleEnd({ victory: false, fled: true, experience: 0, gold: 0, enemy: this.enemy.name }); | |
| } | |
| return; | |
| } else { | |
| this.addToLog(`${actor.name} failed to flee.`); | |
| this.time.delayedCall(300, () => this.endTurn(actor)); | |
| } | |
| } | |
| enemyChooseAction(actor) { | |
| // very simple AI: prioritize attack, if hp low try defend or use skill | |
| if (actor.hp < actor.maxHp * 0.25 && Math.random() < 0.4) { | |
| this.doDefend(actor); | |
| } else { | |
| this.time.delayedCall(400, () => this.doAttack(actor, this.actors[0])); | |
| } | |
| } | |
| endTurn(actor) { | |
| // reset actor's atb and defending buff if any consumed on next incoming damage | |
| actor.atb = 0; | |
| actor.defending = false; // defend lasted for one turn | |
| this.currentActorTurn = null; | |
| this.updateHPBars(); | |
| // Check for deaths and end battle if needed | |
| const player = this.actors[0], enemy = this.actors[1]; | |
| if (player.hp <= 0 || enemy.hp <= 0) { | |
| const victory = enemy.hp <= 0 && player.hp > 0; | |
| this.sound.stopAll(); | |
| // small delay then end | |
| this.time.delayedCall(500, () => { | |
| this.scene.stop(); | |
| if (this.onBattleEnd) { | |
| this.onBattleEnd({ | |
| victory: victory, | |
| experience: victory ? 100 : 0, | |
| gold: victory ? 50 : 0, | |
| enemy: enemy.name | |
| }); | |
| } | |
| }); | |
| return; | |
| } | |
| // Resume ATB filling | |
| this.resumeATB(); | |
| } | |
| // ------------------------- | |
| // Utility: Damage calc, logs, enemy stat generation | |
| // ------------------------- | |
| calculateDamage(actor, target, type = 'physical') { | |
| let base; | |
| if (type === 'physical') base = actor.atk; | |
| else if (type === 'skill') base = actor.atk * 1.25; | |
| else base = actor.mag; | |
| let defense = target.def; | |
| if (target.defending) defense *= 1.6; | |
| const raw = Math.max(1, Math.round(base - (defense * 0.5))); | |
| // add small random variance | |
| const variance = Math.round(raw * (0.85 + Math.random() * 0.3)); | |
| return Math.max(1, variance); | |
| } | |
| addToLog(text) { | |
| this.log.unshift(text); | |
| if (this.log.length > 6) this.log.length = 6; | |
| this.updateLog(); | |
| } | |
| updateLog() { | |
| this.logText.setText(this.log.slice(0, 6).reverse().join('\n')); | |
| } | |
| enemyStatsFromKey(key) { | |
| // reasonable defaults per enemy type | |
| const mapping = { | |
| 'goat': { maxHp: 120, atk: 18, def: 8, mag: 0, speed: 35 }, | |
| 'war_dwarf': { maxHp: 230, atk: 30, def: 18, mag: 0, speed: 28 }, | |
| 'war_blob': { maxHp: 200, atk: 22, def: 12, mag: 8, speed: 20 }, | |
| 'magic_demon': { maxHp: 320, atk: 40, def: 18, mag: 30, speed: 30 }, | |
| 'wolf': { maxHp: 170, atk: 25, def: 10, mag: 0, speed: 45 }, | |
| 'snake': { maxHp: 110, atk: 16, def: 6, mag: 6, speed: 42 }, | |
| 'fly': { maxHp: 90, atk: 12, def: 4, mag: 0, speed: 55 }, | |
| 'war_orc': { maxHp: 250, atk: 34, def: 20, mag: 0, speed: 30 } | |
| }; | |
| return mapping[key] || { maxHp: 150, atk: 20, def: 10, mag: 0, speed: 30 }; | |
| } | |
| // ------------------------- | |
| // Enemy selection methods (preserved) | |
| // ------------------------- | |
| getEnemyForTerrain(terrain) { | |
| const enemyPools = { | |
| 'mountain': [ | |
| { key: 'goat', name: 'Mountain Goat', weight: 35 }, | |
| { key: 'war_dwarf', name: 'War Dwarf', weight: 30 }, | |
| { key: 'war_blob', name: 'War Blob', weight: 20 }, | |
| { key: 'magic_demon', name: 'Magic Demon', weight: 5 }, | |
| { key: 'wolf', name: 'Wolf', weight: 10 } | |
| ], | |
| 'forest': [ | |
| { key: 'snake', name: 'Forest Snake', weight: 40 }, | |
| { key: 'wolf', name: 'Wolf', weight: 35 }, | |
| { key: 'fly', name: 'Giant Fly', weight: 15 }, | |
| { key: 'magic_demon', name: 'Magic Demon', weight: 5 }, | |
| { key: 'war_orc', name: 'Forest Orc', weight: 5 } | |
| ], | |
| 'grass': [ | |
| { key: 'fly', name: 'Giant Fly', weight: 40 }, | |
| { key: 'wolf', name: 'Wolf', weight: 30 }, | |
| { key: 'snake', name: 'Snake', weight: 15 }, | |
| { key: 'magic_demon', name: 'Magic Demon', weight: 5 }, | |
| { key: 'goat', name: 'Goat', weight: 10 } | |
| ], | |
| 'path': [ | |
| { key: 'war_orc', name: 'Orc Warrior', weight: 40 }, | |
| { key: 'fly', name: 'Giant Fly', weight: 25 }, | |
| { key: 'wolf', name: 'Wolf', weight: 20 }, | |
| { key: 'magic_demon', name: 'Magic Demon', weight: 5 }, | |
| { key: 'snake', name: 'Snake', weight: 10 } | |
| ], | |
| 'water': [ | |
| { key: 'fly', name: 'Giant Fly', weight: 40 }, | |
| { key: 'snake', name: 'Water Snake', weight: 30 }, | |
| { key: 'magic_demon', name: 'Magic Demon', weight: 10 }, | |
| { key: 'wolf', name: 'Wolf', weight: 20 } | |
| ] | |
| }; | |
| const pool = enemyPools[terrain] || enemyPools['grass']; | |
| return this.selectRandomEnemy(pool); | |
| } | |
| selectRandomEnemy(enemyPool) { | |
| const totalWeight = enemyPool.reduce((sum, enemy) => sum + enemy.weight, 0); | |
| let random = Math.random() * totalWeight; | |
| let weightSum = 0; | |
| for (const enemy of enemyPool) { | |
| weightSum += enemy.weight; | |
| if (random <= weightSum) return enemy; | |
| } | |
| return enemyPool[0]; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment