Created
November 12, 2025 00:02
-
-
Save StringManolo/f420dc7a67edd55e6003d801af3173ed to your computer and use it in GitHub Desktop.
Claude Sonnet 4.5
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 || this.getDefaultPlayerStats(); | |
| this.onBattleEnd = data.onBattleEnd; | |
| this.terrain = data.terrain; | |
| } | |
| 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'); | |
| // High quality assets for warrior attack animation | |
| 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"); | |
| // Sound effect for attack | |
| this.load.audio("warrior_slash", "assets/audio/warrior_slash.wav"); | |
| // Battle music | |
| this.load.audio("battle_music", "assets/audio/battle.mp3"); | |
| } | |
| create() { | |
| 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); | |
| background.setDisplaySize(800, 600); | |
| // Initialize combat system | |
| this.initializeCombatants(); | |
| this.createUI(); | |
| this.createATBSystem(); | |
| // Play the battle music | |
| this.sound.play('battle_music', { loop: true }); | |
| // Start ATB system | |
| this.startATBSystem(); | |
| } | |
| getDefaultPlayerStats() { | |
| return { | |
| name: 'Warrior', | |
| level: 1, | |
| maxHP: 100, | |
| currentHP: 100, | |
| maxMP: 50, | |
| currentMP: 50, | |
| attack: 15, | |
| defense: 10, | |
| magic: 8, | |
| speed: 10, | |
| items: [ | |
| { name: 'Potion', effect: 'heal', value: 30, count: 3 }, | |
| { name: 'Ether', effect: 'restore_mp', value: 20, count: 2 } | |
| ] | |
| }; | |
| } | |
| initializeCombatants() { | |
| // Load enemy configuration | |
| this.enemyConfig = this.getEnemyForTerrain(this.terrain); | |
| // Create enemy stats | |
| this.enemyStats = { | |
| name: this.enemyConfig.name, | |
| level: this.enemyConfig.level, | |
| maxHP: this.enemyConfig.maxHP, | |
| currentHP: this.enemyConfig.maxHP, | |
| attack: this.enemyConfig.attack, | |
| defense: this.enemyConfig.defense, | |
| magic: this.enemyConfig.magic, | |
| speed: this.enemyConfig.speed, | |
| exp: this.enemyConfig.exp, | |
| gold: this.enemyConfig.gold | |
| }; | |
| // Create enemy sprite | |
| this.enemy = this.add.image(550, 250, this.enemyConfig.key); | |
| this.enemy.setDisplaySize(175, 175); | |
| // Create warrior sprite | |
| this.warrior = this.add.sprite(250, 400, 'warrior_basic_attack'); | |
| this.warrior.setOrigin(0.5, 1); | |
| this.warrior.setDisplaySize(175, 175); | |
| this.warrior.setDepth(2); | |
| // ATB gauges | |
| this.playerATB = 0; | |
| this.enemyATB = 0; | |
| this.atbMax = 100; | |
| this.atbSpeed = 1; | |
| // Combat state | |
| this.isPlayerTurn = false; | |
| this.isEnemyTurn = false; | |
| this.battleActive = true; | |
| this.isAnimating = false; | |
| this.menuOpen = false; | |
| } | |
| createUI() { | |
| // Player HP/MP display | |
| this.playerUIBg = this.add.rectangle(150, 520, 280, 100, 0x000000, 0.7); | |
| this.playerUIBg.setOrigin(0, 0); | |
| this.playerNameText = this.add.text(160, 530, this.playerData.name, { | |
| fontSize: '18px', | |
| fill: '#fff', | |
| fontStyle: 'bold' | |
| }); | |
| this.playerHPText = this.add.text(160, 555, | |
| `HP: ${this.playerData.currentHP}/${this.playerData.maxHP}`, { | |
| fontSize: '16px', | |
| fill: '#fff' | |
| }); | |
| this.playerMPText = this.add.text(160, 580, | |
| `MP: ${this.playerData.currentMP}/${this.playerData.maxMP}`, { | |
| fontSize: '16px', | |
| fill: '#fff' | |
| }); | |
| // Player HP bar | |
| this.playerHPBarBg = this.add.rectangle(280, 558, 130, 12, 0x333333); | |
| this.playerHPBar = this.add.rectangle(280, 558, 130, 12, 0x00ff00); | |
| this.playerHPBar.setOrigin(0, 0.5); | |
| this.playerHPBarBg.setOrigin(0, 0.5); | |
| // Player MP bar | |
| this.playerMPBarBg = this.add.rectangle(280, 583, 130, 12, 0x333333); | |
| this.playerMPBar = this.add.rectangle(280, 583, 130, 12, 0x0088ff); | |
| this.playerMPBar.setOrigin(0, 0.5); | |
| this.playerMPBarBg.setOrigin(0, 0.5); | |
| // Enemy HP display | |
| this.enemyUIBg = this.add.rectangle(470, 140, 280, 80, 0x000000, 0.7); | |
| this.enemyUIBg.setOrigin(0, 0); | |
| this.enemyNameText = this.add.text(480, 150, this.enemyStats.name, { | |
| fontSize: '18px', | |
| fill: '#fff', | |
| fontStyle: 'bold' | |
| }); | |
| this.enemyHPText = this.add.text(480, 175, | |
| `HP: ${this.enemyStats.currentHP}/${this.enemyStats.maxHP}`, { | |
| fontSize: '16px', | |
| fill: '#fff' | |
| }); | |
| // Enemy HP bar | |
| this.enemyHPBarBg = this.add.rectangle(600, 178, 130, 12, 0x333333); | |
| this.enemyHPBar = this.add.rectangle(600, 178, 130, 12, 0xff0000); | |
| this.enemyHPBar.setOrigin(0, 0.5); | |
| this.enemyHPBarBg.setOrigin(0, 0.5); | |
| // Player ATB gauge | |
| this.playerATBText = this.add.text(160, 600, 'ATB', { | |
| fontSize: '14px', | |
| fill: '#ffff00' | |
| }); | |
| this.playerATBBarBg = this.add.rectangle(200, 605, 210, 10, 0x333333); | |
| this.playerATBBar = this.add.rectangle(200, 605, 0, 10, 0xffff00); | |
| this.playerATBBar.setOrigin(0, 0.5); | |
| this.playerATBBarBg.setOrigin(0, 0.5); | |
| // Enemy ATB gauge | |
| this.enemyATBText = this.add.text(480, 200, 'ATB', { | |
| fontSize: '14px', | |
| fill: '#ff8800' | |
| }); | |
| this.enemyATBBarBg = this.add.rectangle(520, 205, 210, 10, 0x333333); | |
| this.enemyATBBar = this.add.rectangle(520, 205, 0, 10, 0xff8800); | |
| this.enemyATBBar.setOrigin(0, 0.5); | |
| this.enemyATBBarBg.setOrigin(0, 0.5); | |
| // Battle menu (initially hidden) | |
| this.createBattleMenu(); | |
| } | |
| createBattleMenu() { | |
| // Main menu background | |
| this.menuBg = this.add.rectangle(400, 480, 760, 200, 0x000033, 0.9); | |
| this.menuBg.setOrigin(0.5, 0); | |
| this.menuBg.setVisible(false); | |
| // Main menu options | |
| this.mainMenuOptions = ['Attack', 'Magic', 'Ability', 'Defend', 'Item', 'Flee']; | |
| this.mainMenuButtons = []; | |
| const startX = 60; | |
| const startY = 500; | |
| const spacing = 120; | |
| this.mainMenuOptions.forEach((option, index) => { | |
| const x = startX + (index % 3) * spacing + (Math.floor(index / 3) * 360); | |
| const y = startY + Math.floor(index / 3) * 60; | |
| const bg = this.add.rectangle(x, y, 110, 45, 0x0066cc, 0.8); | |
| bg.setOrigin(0, 0); | |
| bg.setInteractive(); | |
| const text = this.add.text(x + 55, y + 22, option, { | |
| fontSize: '18px', | |
| fill: '#fff', | |
| fontStyle: 'bold' | |
| }).setOrigin(0.5); | |
| bg.on('pointerover', () => { | |
| bg.setFillStyle(0x0088ff); | |
| }); | |
| bg.on('pointerout', () => { | |
| bg.setFillStyle(0x0066cc); | |
| }); | |
| bg.on('pointerdown', () => { | |
| this.handleMenuOption(option); | |
| }); | |
| bg.setVisible(false); | |
| text.setVisible(false); | |
| this.mainMenuButtons.push({ bg, text, option }); | |
| }); | |
| // Submenu (for magic, abilities, items) | |
| this.submenuBg = this.add.rectangle(400, 480, 760, 200, 0x001133, 0.95); | |
| this.submenuBg.setOrigin(0.5, 0); | |
| this.submenuBg.setVisible(false); | |
| this.submenuTitle = this.add.text(400, 495, '', { | |
| fontSize: '20px', | |
| fill: '#ffff00', | |
| fontStyle: 'bold' | |
| }).setOrigin(0.5); | |
| this.submenuTitle.setVisible(false); | |
| this.submenuButtons = []; | |
| this.createSubmenuButtons(); | |
| // Back button for submenu | |
| this.backButton = this.add.rectangle(700, 640, 80, 35, 0xcc0000, 0.8); | |
| this.backButton.setOrigin(0.5); | |
| this.backButton.setInteractive(); | |
| this.backButtonText = this.add.text(700, 640, 'Back', { | |
| fontSize: '16px', | |
| fill: '#fff' | |
| }).setOrigin(0.5); | |
| this.backButton.on('pointerover', () => { | |
| this.backButton.setFillStyle(0xff0000); | |
| }); | |
| this.backButton.on('pointerout', () => { | |
| this.backButton.setFillStyle(0xcc0000); | |
| }); | |
| this.backButton.on('pointerdown', () => { | |
| this.closeSubmenu(); | |
| }); | |
| this.backButton.setVisible(false); | |
| this.backButtonText.setVisible(false); | |
| } | |
| createSubmenuButtons() { | |
| const startX = 60; | |
| const startY = 535; | |
| const spacing = 180; | |
| for (let i = 0; i < 12; i++) { | |
| const x = startX + (i % 4) * spacing; | |
| const y = startY + Math.floor(i / 4) * 45; | |
| const bg = this.add.rectangle(x, y, 170, 40, 0x0066cc, 0.8); | |
| bg.setOrigin(0, 0); | |
| bg.setInteractive(); | |
| const text = this.add.text(x + 85, y + 20, '', { | |
| fontSize: '16px', | |
| fill: '#fff' | |
| }).setOrigin(0.5); | |
| bg.on('pointerover', () => { | |
| bg.setFillStyle(0x0088ff); | |
| }); | |
| bg.on('pointerout', () => { | |
| bg.setFillStyle(0x0066cc); | |
| }); | |
| bg.setVisible(false); | |
| text.setVisible(false); | |
| this.submenuButtons.push({ bg, text, data: null }); | |
| } | |
| } | |
| createATBSystem() { | |
| this.atbTimer = this.time.addEvent({ | |
| delay: 50, | |
| callback: this.updateATB, | |
| callbackScope: this, | |
| loop: true | |
| }); | |
| } | |
| startATBSystem() { | |
| this.battleActive = true; | |
| } | |
| updateATB() { | |
| if (!this.battleActive || this.isAnimating) return; | |
| // Update player ATB | |
| if (!this.isPlayerTurn && this.playerATB < this.atbMax) { | |
| this.playerATB += this.atbSpeed * (this.playerData.speed / 10); | |
| if (this.playerATB >= this.atbMax) { | |
| this.playerATB = this.atbMax; | |
| this.onPlayerTurnReady(); | |
| } | |
| } | |
| // Update enemy ATB | |
| if (!this.isEnemyTurn && this.enemyATB < this.atbMax) { | |
| this.enemyATB += this.atbSpeed * (this.enemyStats.speed / 10); | |
| if (this.enemyATB >= this.atbMax) { | |
| this.enemyATB = this.atbMax; | |
| this.onEnemyTurnReady(); | |
| } | |
| } | |
| // Update ATB bars | |
| this.updateATBBars(); | |
| } | |
| updateATBBars() { | |
| const playerWidth = (this.playerATB / this.atbMax) * 210; | |
| const enemyWidth = (this.enemyATB / this.atbMax) * 210; | |
| this.playerATBBar.setSize(playerWidth, 10); | |
| this.enemyATBBar.setSize(enemyWidth, 10); | |
| } | |
| onPlayerTurnReady() { | |
| this.isPlayerTurn = true; | |
| this.battleActive = false; // Pause ATB | |
| this.showBattleMenu(); | |
| } | |
| onEnemyTurnReady() { | |
| this.isEnemyTurn = true; | |
| this.battleActive = false; // Pause ATB | |
| this.time.delayedCall(500, () => { | |
| this.executeEnemyAction(); | |
| }); | |
| } | |
| showBattleMenu() { | |
| this.menuOpen = true; | |
| this.menuBg.setVisible(true); | |
| this.mainMenuButtons.forEach(btn => { | |
| btn.bg.setVisible(true); | |
| btn.text.setVisible(true); | |
| }); | |
| } | |
| hideBattleMenu() { | |
| this.menuOpen = false; | |
| this.menuBg.setVisible(false); | |
| this.mainMenuButtons.forEach(btn => { | |
| btn.bg.setVisible(false); | |
| btn.text.setVisible(false); | |
| }); | |
| } | |
| handleMenuOption(option) { | |
| switch(option) { | |
| case 'Attack': | |
| this.hideBattleMenu(); | |
| this.executePlayerAttack(); | |
| break; | |
| case 'Magic': | |
| this.showMagicMenu(); | |
| break; | |
| case 'Ability': | |
| this.showAbilityMenu(); | |
| break; | |
| case 'Defend': | |
| this.hideBattleMenu(); | |
| this.executeDefend(); | |
| break; | |
| case 'Item': | |
| this.showItemMenu(); | |
| break; | |
| case 'Flee': | |
| this.hideBattleMenu(); | |
| this.attemptFlee(); | |
| break; | |
| } | |
| } | |
| showMagicMenu() { | |
| this.hideBattleMenu(); | |
| this.submenuBg.setVisible(true); | |
| this.submenuTitle.setText('Magic'); | |
| this.submenuTitle.setVisible(true); | |
| this.backButton.setVisible(true); | |
| this.backButtonText.setVisible(true); | |
| const magicSpells = [ | |
| { name: 'Fire', cost: 8, damage: 25, type: 'damage' }, | |
| { name: 'Ice', cost: 8, damage: 25, type: 'damage' }, | |
| { name: 'Thunder', cost: 10, damage: 30, type: 'damage' }, | |
| { name: 'Cure', cost: 12, heal: 40, type: 'heal' } | |
| ]; | |
| magicSpells.forEach((spell, index) => { | |
| if (index < this.submenuButtons.length) { | |
| const btn = this.submenuButtons[index]; | |
| const canUse = this.playerData.currentMP >= spell.cost; | |
| btn.text.setText(`${spell.name} (${spell.cost} MP)`); | |
| btn.text.setColor(canUse ? '#fff' : '#666'); | |
| btn.bg.setVisible(true); | |
| btn.text.setVisible(true); | |
| btn.data = spell; | |
| btn.bg.removeAllListeners('pointerdown'); | |
| btn.bg.on('pointerdown', () => { | |
| if (canUse) { | |
| this.closeSubmenu(); | |
| this.executeMagic(spell); | |
| } | |
| }); | |
| } | |
| }); | |
| // Hide unused buttons | |
| for (let i = magicSpells.length; i < this.submenuButtons.length; i++) { | |
| this.submenuButtons[i].bg.setVisible(false); | |
| this.submenuButtons[i].text.setVisible(false); | |
| } | |
| } | |
| showAbilityMenu() { | |
| this.hideBattleMenu(); | |
| this.submenuBg.setVisible(true); | |
| this.submenuTitle.setText('Abilities'); | |
| this.submenuTitle.setVisible(true); | |
| this.backButton.setVisible(true); | |
| this.backButtonText.setVisible(true); | |
| const abilities = [ | |
| { name: 'Power Strike', cost: 0, damageMultiplier: 1.5 }, | |
| { name: 'Double Attack', cost: 0, hits: 2 } | |
| ]; | |
| abilities.forEach((ability, index) => { | |
| if (index < this.submenuButtons.length) { | |
| const btn = this.submenuButtons[index]; | |
| btn.text.setText(ability.name); | |
| btn.text.setColor('#fff'); | |
| btn.bg.setVisible(true); | |
| btn.text.setVisible(true); | |
| btn.data = ability; | |
| btn.bg.removeAllListeners('pointerdown'); | |
| btn.bg.on('pointerdown', () => { | |
| this.closeSubmenu(); | |
| this.executeAbility(ability); | |
| }); | |
| } | |
| }); | |
| for (let i = abilities.length; i < this.submenuButtons.length; i++) { | |
| this.submenuButtons[i].bg.setVisible(false); | |
| this.submenuButtons[i].text.setVisible(false); | |
| } | |
| } | |
| showItemMenu() { | |
| this.hideBattleMenu(); | |
| this.submenuBg.setVisible(true); | |
| this.submenuTitle.setText('Items'); | |
| this.submenuTitle.setVisible(true); | |
| this.backButton.setVisible(true); | |
| this.backButtonText.setVisible(true); | |
| const items = this.playerData.items || []; | |
| items.forEach((item, index) => { | |
| if (index < this.submenuButtons.length && item.count > 0) { | |
| const btn = this.submenuButtons[index]; | |
| btn.text.setText(`${item.name} (${item.count})`); | |
| btn.text.setColor('#fff'); | |
| btn.bg.setVisible(true); | |
| btn.text.setVisible(true); | |
| btn.data = item; | |
| btn.bg.removeAllListeners('pointerdown'); | |
| btn.bg.on('pointerdown', () => { | |
| this.closeSubmenu(); | |
| this.useItem(item); | |
| }); | |
| } | |
| }); | |
| for (let i = items.length; i < this.submenuButtons.length; i++) { | |
| this.submenuButtons[i].bg.setVisible(false); | |
| this.submenuButtons[i].text.setVisible(false); | |
| } | |
| } | |
| closeSubmenu() { | |
| this.submenuBg.setVisible(false); | |
| this.submenuTitle.setVisible(false); | |
| this.backButton.setVisible(false); | |
| this.backButtonText.setVisible(false); | |
| this.submenuButtons.forEach(btn => { | |
| btn.bg.setVisible(false); | |
| btn.text.setVisible(false); | |
| btn.bg.removeAllListeners('pointerdown'); | |
| }); | |
| this.showBattleMenu(); | |
| } | |
| executePlayerAttack() { | |
| this.isAnimating = true; | |
| this.startAttack(() => { | |
| const damage = this.calculateDamage(this.playerData.attack, this.enemyStats.defense); | |
| this.damageEnemy(damage); | |
| this.endPlayerTurn(); | |
| }); | |
| } | |
| executeMagic(spell) { | |
| this.isAnimating = true; | |
| this.playerData.currentMP -= spell.cost; | |
| this.updatePlayerUI(); | |
| if (spell.type === 'damage') { | |
| const damage = spell.damage + Math.floor(this.playerData.magic * 1.5); | |
| this.showFloatingText(this.enemy.x, this.enemy.y - 100, spell.name, '#00ffff'); | |
| this.time.delayedCall(500, () => { | |
| this.damageEnemy(damage); | |
| this.endPlayerTurn(); | |
| }); | |
| } else if (spell.type === 'heal') { | |
| this.playerData.currentHP = Math.min( | |
| this.playerData.currentHP + spell.heal, | |
| this.playerData.maxHP | |
| ); | |
| this.updatePlayerUI(); | |
| this.showFloatingText(this.warrior.x, this.warrior.y - 100, `+${spell.heal}`, '#00ff00'); | |
| this.time.delayedCall(1000, () => { | |
| this.endPlayerTurn(); | |
| }); | |
| } | |
| } | |
| executeAbility(ability) { | |
| this.isAnimating = true; | |
| if (ability.damageMultiplier) { | |
| this.startAttack(() => { | |
| const damage = Math.floor( | |
| this.calculateDamage(this.playerData.attack, this.enemyStats.defense) * ability.damageMultiplier | |
| ); | |
| this.damageEnemy(damage); | |
| this.endPlayerTurn(); | |
| }); | |
| } else if (ability.hits) { | |
| let hitCount = 0; | |
| const executeHit = () => { | |
| this.startAttack(() => { | |
| const damage = this.calculateDamage(this.playerData.attack, this.enemyStats.defense); | |
| this.damageEnemy(damage); | |
| hitCount++; | |
| if (hitCount < ability.hits && this.enemyStats.currentHP > 0) { | |
| this.time.delayedCall(300, executeHit); | |
| } else { | |
| this.endPlayerTurn(); | |
| } | |
| }); | |
| }; | |
| executeHit(); | |
| } | |
| } | |
| executeDefend() { | |
| this.isAnimating = true; | |
| this.playerDefending = true; | |
| this.showFloatingText(this.warrior.x, this.warrior.y - 100, 'Defend', '#ffff00'); | |
| this.time.delayedCall(1000, () => { | |
| this.endPlayerTurn(); | |
| }); | |
| } | |
| useItem(item) { | |
| this.isAnimating = true; | |
| item.count--; | |
| if (item.effect === 'heal') { | |
| this.playerData.currentHP = Math.min( | |
| this.playerData.currentHP + item.value, | |
| this.playerData.maxHP | |
| ); | |
| this.updatePlayerUI(); | |
| this.showFloatingText(this.warrior.x, this.warrior.y - 100, `+${item.value} HP`, '#00ff00'); | |
| } else if (item.effect === 'restore_mp') { | |
| this.playerData.currentMP = Math.min( | |
| this.playerData.currentMP + item.value, | |
| this.playerData.maxMP | |
| ); | |
| this.updatePlayerUI(); | |
| this.showFloatingText(this.warrior.x, this.warrior.y - 100, `+${item.value} MP`, '#0088ff'); | |
| } | |
| this.time.delayedCall(1000, () => { | |
| this.endPlayerTurn(); | |
| }); | |
| } | |
| attemptFlee() { | |
| this.isAnimating = true; | |
| const fleeChance = 0.5; | |
| if (Math.random() < fleeChance) { | |
| this.showFloatingText(400, 300, 'Escaped!', '#ffff00'); | |
| this.time.delayedCall(1500, () => { | |
| this.endBattle(false, true); | |
| }); | |
| } else { | |
| this.showFloatingText(400, 300, 'Could not escape!', '#ff0000'); | |
| this.time.delayedCall(1500, () => { | |
| this.endPlayerTurn(); | |
| }); | |
| } | |
| } | |
| startAttack(onComplete) { | |
| const originalX = this.warrior.x; | |
| const originalY = this.warrior.y; | |
| this.tweens.add({ | |
| targets: this.warrior, | |
| x: this.enemy.x - 80, | |
| y: this.enemy.y + 50, | |
| 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: () => { | |
| if (onComplete) onComplete(); | |
| } | |
| }); | |
| }); | |
| }); | |
| }); | |
| } | |
| }); | |
| } | |
| executeEnemyAction() { | |
| this.isAnimating = true; | |
| const action = Math.random(); | |
| if (action < 0.7) { | |
| // Normal attack | |
| const damage = this.calculateDamage(this.enemyStats.attack, this.playerData.defense); | |
| const finalDamage = this.playerDefending ? Math.floor(damage * 0.5) : damage; | |
| this.playerDefending = false; | |
| this.damagePlayer(finalDamage); | |
| this.time.delayedCall(1500, () => { | |
| this.endEnemyTurn(); | |
| }); | |
| } else { | |
| // Special attack | |
| const damage = Math.floor(this.calculateDamage(this.enemyStats.attack, this.playerData.defense) * 1.5); | |
| this.showFloatingText(this.warrior.x, this.warrior.y - 100, 'Special!', '#ff00ff'); | |
| this.time.delayedCall(500, () => { | |
| this.damagePlayer(damage); | |
| this.time.delayedCall(1500, () => { | |
| this.endEnemyTurn(); | |
| }); | |
| }); | |
| } | |
| } | |
| calculateDamage(attack, defense) { | |
| const baseDamage = attack - Math.floor(defense / 2); | |
| const variance = Math.floor(baseDamage * 0.2); | |
| return Math.max(1, baseDamage + Math.floor(Math.random() * variance * 2) - variance); | |
| } | |
| damageEnemy(damage) { | |
| this.enemyStats.currentHP = Math.max(0, this.enemyStats.currentHP - damage); | |
| this.updateEnemyUI(); | |
| this.showFloatingText(this.enemy.x, this.enemy.y - 50, `-${damage}`, '#ff0000'); | |
| // Flash enemy | |
| this.tweens.add({ | |
| targets: this.enemy, | |
| alpha: 0.3, | |
| duration: 100, | |
| yoyo: true, | |
| repeat: 2 | |
| }); | |
| if (this.enemyStats.currentHP <= 0) { | |
| this.time.delayedCall(1000, () => { | |
| this.endBattle(true); | |
| }); | |
| } | |
| } | |
| damagePlayer(damage) { | |
| this.playerData.currentHP = Math.max(0, this.playerData.currentHP - damage); | |
| this.updatePlayerUI(); | |
| this.showFloatingText(this.warrior.x, this.warrior.y - 50, `-${damage}`, '#ff0000'); | |
| // Flash warrior | |
| this.tweens.add({ | |
| targets: this.warrior, | |
| alpha: 0.3, | |
| duration: 100, | |
| yoyo: true, | |
| repeat: 2 | |
| }); | |
| if (this.playerData.currentHP <= 0) { | |
| this.time.delayedCall(1000, () => { | |
| this.endBattle(false); | |
| }); | |
| } | |
| } | |
| showFloatingText(x, y, text, color) { | |
| const floatingText = this.add.text(x, y, text, { | |
| fontSize: '32px', | |
| fill: color, | |
| fontStyle: 'bold', | |
| stroke: '#000', | |
| strokeThickness: 4 | |
| }).setOrigin(0.5); | |
| this.tweens.add({ | |
| targets: floatingText, | |
| y: y - 80, | |
| alpha: 0, | |
| duration: 1500, | |
| ease: 'Power2', | |
| onComplete: () => { | |
| floatingText.destroy(); | |
| } | |
| }); | |
| } | |
| updatePlayerUI() { | |
| this.playerHPText.setText(`HP: ${this.playerData.currentHP}/${this.playerData.maxHP}`); | |
| this.playerMPText.setText(`MP: ${this.playerData.currentMP}/${this.playerData.maxMP}`); | |
| const hpPercent = this.playerData.currentHP / this.playerData.maxHP; | |
| this.playerHPBar.setSize(130 * hpPercent, 12); | |
| if (hpPercent > 0.5) { | |
| this.playerHPBar.setFillStyle(0x00ff00); | |
| } else if (hpPercent > 0.25) { | |
| this.playerHPBar.setFillStyle(0xffff00); | |
| } else { | |
| this.playerHPBar.setFillStyle(0xff0000); | |
| } | |
| const mpPercent = this.playerData.currentMP / this.playerData.maxMP; | |
| this.playerMPBar.setSize(130 * mpPercent, 12); | |
| } | |
| updateEnemyUI() { | |
| this.enemyHPText.setText(`HP: ${this.enemyStats.currentHP}/${this.enemyStats.maxHP}`); | |
| const hpPercent = this.enemyStats.currentHP / this.enemyStats.maxHP; | |
| this.enemyHPBar.setSize(130 * hpPercent, 12); | |
| if (hpPercent > 0.5) { | |
| this.enemyHPBar.setFillStyle(0xff0000); | |
| } else if (hpPercent > 0.25) { | |
| this.enemyHPBar.setFillStyle(0xff8800); | |
| } else { | |
| this.enemyHPBar.setFillStyle(0xffff00); | |
| } | |
| } | |
| endPlayerTurn() { | |
| this.isPlayerTurn = false; | |
| this.playerATB = 0; | |
| this.isAnimating = false; | |
| this.battleActive = true; | |
| } | |
| endEnemyTurn() { | |
| this.isEnemyTurn = false; | |
| this.enemyATB = 0; | |
| this.isAnimating = false; | |
| this.battleActive = true; | |
| } | |
| endBattle(victory, fled = false) { | |
| this.battleActive = false; | |
| this.sound.stopAll(); | |
| if (this.atbTimer) { | |
| this.atbTimer.destroy(); | |
| } | |
| let resultText = ''; | |
| let resultColor = ''; | |
| if (fled) { | |
| resultText = 'Fled from battle!'; | |
| resultColor = '#ffff00'; | |
| } else if (victory) { | |
| resultText = 'Victory!'; | |
| resultColor = '#00ff00'; | |
| } else { | |
| resultText = 'Defeat...'; | |
| resultColor = '#ff0000'; | |
| } | |
| const resultDisplay = this.add.text(400, 300, resultText, { | |
| fontSize: '48px', | |
| fill: resultColor, | |
| fontStyle: 'bold', | |
| stroke: '#000', | |
| strokeThickness: 6 | |
| }).setOrigin(0.5); | |
| resultDisplay.setAlpha(0); | |
| this.tweens.add({ | |
| targets: resultDisplay, | |
| alpha: 1, | |
| duration: 500 | |
| }); | |
| this.time.delayedCall(2000, () => { | |
| this.scene.stop(); | |
| if (this.onBattleEnd) { | |
| this.onBattleEnd({ | |
| victory: victory, | |
| fled: fled, | |
| experience: victory ? this.enemyStats.exp : 0, | |
| gold: victory ? this.enemyStats.gold : 0, | |
| enemy: this.enemyStats.name, | |
| playerData: this.playerData | |
| }); | |
| } | |
| }); | |
| } | |
| getEnemyForTerrain(terrain) { | |
| const enemyPools = { | |
| 'mountain': [ | |
| { | |
| key: 'goat', | |
| name: 'Mountain Goat', | |
| weight: 35, | |
| level: 3, | |
| maxHP: 60, | |
| attack: 12, | |
| defense: 8, | |
| magic: 3, | |
| speed: 12, | |
| exp: 45, | |
| gold: 30 | |
| }, | |
| { | |
| key: 'war_dwarf', | |
| name: 'War Dwarf', | |
| weight: 30, | |
| level: 5, | |
| maxHP: 90, | |
| attack: 18, | |
| defense: 15, | |
| magic: 5, | |
| speed: 8, | |
| exp: 80, | |
| gold: 60 | |
| }, | |
| { | |
| key: 'war_blob', | |
| name: 'War Blob', | |
| weight: 20, | |
| level: 6, | |
| maxHP: 100, | |
| attack: 15, | |
| defense: 12, | |
| magic: 8, | |
| speed: 6, | |
| exp: 90, | |
| gold: 70 | |
| }, | |
| { | |
| key: 'magic_demon', | |
| name: 'Magic Demon', | |
| weight: 5, | |
| level: 10, | |
| maxHP: 150, | |
| attack: 25, | |
| defense: 18, | |
| magic: 20, | |
| speed: 15, | |
| exp: 200, | |
| gold: 150 | |
| }, | |
| { | |
| key: 'wolf', | |
| name: 'Wolf', | |
| weight: 10, | |
| level: 4, | |
| maxHP: 70, | |
| attack: 16, | |
| defense: 9, | |
| magic: 2, | |
| speed: 14, | |
| exp: 55, | |
| gold: 40 | |
| } | |
| ], | |
| 'forest': [ | |
| { | |
| key: 'snake', | |
| name: 'Forest Snake', | |
| weight: 40, | |
| level: 2, | |
| maxHP: 50, | |
| attack: 10, | |
| defense: 6, | |
| magic: 4, | |
| speed: 13, | |
| exp: 35, | |
| gold: 25 | |
| }, | |
| { | |
| key: 'wolf', | |
| name: 'Wolf', | |
| weight: 35, | |
| level: 4, | |
| maxHP: 70, | |
| attack: 16, | |
| defense: 9, | |
| magic: 2, | |
| speed: 14, | |
| exp: 55, | |
| gold: 40 | |
| }, | |
| { | |
| key: 'fly', | |
| name: 'Giant Fly', | |
| weight: 15, | |
| level: 3, | |
| maxHP: 55, | |
| attack: 11, | |
| defense: 7, | |
| magic: 3, | |
| speed: 16, | |
| exp: 40, | |
| gold: 30 | |
| }, | |
| { | |
| key: 'magic_demon', | |
| name: 'Magic Demon', | |
| weight: 5, | |
| level: 10, | |
| maxHP: 150, | |
| attack: 25, | |
| defense: 18, | |
| magic: 20, | |
| speed: 15, | |
| exp: 200, | |
| gold: 150 | |
| }, | |
| { | |
| key: 'war_orc', | |
| name: 'Forest Orc', | |
| weight: 5, | |
| level: 7, | |
| maxHP: 110, | |
| attack: 20, | |
| defense: 14, | |
| magic: 6, | |
| speed: 9, | |
| exp: 100, | |
| gold: 80 | |
| } | |
| ], | |
| 'grass': [ | |
| { | |
| key: 'fly', | |
| name: 'Giant Fly', | |
| weight: 40, | |
| level: 3, | |
| maxHP: 55, | |
| attack: 11, | |
| defense: 7, | |
| magic: 3, | |
| speed: 16, | |
| exp: 40, | |
| gold: 30 | |
| }, | |
| { | |
| key: 'wolf', | |
| name: 'Wolf', | |
| weight: 30, | |
| level: 4, | |
| maxHP: 70, | |
| attack: 16, | |
| defense: 9, | |
| magic: 2, | |
| speed: 14, | |
| exp: 55, | |
| gold: 40 | |
| }, | |
| { | |
| key: 'snake', | |
| name: 'Snake', | |
| weight: 15, | |
| level: 2, | |
| maxHP: 50, | |
| attack: 10, | |
| defense: 6, | |
| magic: 4, | |
| speed: 13, | |
| exp: 35, | |
| gold: 25 | |
| }, | |
| { | |
| key: 'magic_demon', | |
| name: 'Magic Demon', | |
| weight: 5, | |
| level: 10, | |
| maxHP: 150, | |
| attack: 25, | |
| defense: 18, | |
| magic: 20, | |
| speed: 15, | |
| exp: 200, | |
| gold: 150 | |
| }, | |
| { | |
| key: 'goat', | |
| name: 'Goat', | |
| weight: 10, | |
| level: 3, | |
| maxHP: 60, | |
| attack: 12, | |
| defense: 8, | |
| magic: 3, | |
| speed: 12, | |
| exp: 45, | |
| gold: 30 | |
| } | |
| ], | |
| 'path': [ | |
| { | |
| key: 'war_orc', | |
| name: 'Orc Warrior', | |
| weight: 40, | |
| level: 7, | |
| maxHP: 110, | |
| attack: 20, | |
| defense: 14, | |
| magic: 6, | |
| speed: 9, | |
| exp: 100, | |
| gold: 80 | |
| }, | |
| { | |
| key: 'fly', | |
| name: 'Giant Fly', | |
| weight: 25, | |
| level: 3, | |
| maxHP: 55, | |
| attack: 11, | |
| defense: 7, | |
| magic: 3, | |
| speed: 16, | |
| exp: 40, | |
| gold: 30 | |
| }, | |
| { | |
| key: 'wolf', | |
| name: 'Wolf', | |
| weight: 20, | |
| level: 4, | |
| maxHP: 70, | |
| attack: 16, | |
| defense: 9, | |
| magic: 2, | |
| speed: 14, | |
| exp: 55, | |
| gold: 40 | |
| }, | |
| { | |
| key: 'magic_demon', | |
| name: 'Magic Demon', | |
| weight: 5, | |
| level: 10, | |
| maxHP: 150, | |
| attack: 25, | |
| defense: 18, | |
| magic: 20, | |
| speed: 15, | |
| exp: 200, | |
| gold: 150 | |
| }, | |
| { | |
| key: 'snake', | |
| name: 'Snake', | |
| weight: 10, | |
| level: 2, | |
| maxHP: 50, | |
| attack: 10, | |
| defense: 6, | |
| magic: 4, | |
| speed: 13, | |
| exp: 35, | |
| gold: 25 | |
| } | |
| ], | |
| 'water': [ | |
| { | |
| key: 'fly', | |
| name: 'Giant Fly', | |
| weight: 40, | |
| level: 3, | |
| maxHP: 55, | |
| attack: 11, | |
| defense: 7, | |
| magic: 3, | |
| speed: 16, | |
| exp: 40, | |
| gold: 30 | |
| }, | |
| { | |
| key: 'snake', | |
| name: 'Water Snake', | |
| weight: 30, | |
| level: 2, | |
| maxHP: 50, | |
| attack: 10, | |
| defense: 6, | |
| magic: 4, | |
| speed: 13, | |
| exp: 35, | |
| gold: 25 | |
| }, | |
| { | |
| key: 'magic_demon', | |
| name: 'Magic Demon', | |
| weight: 10, | |
| level: 10, | |
| maxHP: 150, | |
| attack: 25, | |
| defense: 18, | |
| magic: 20, | |
| speed: 15, | |
| exp: 200, | |
| gold: 150 | |
| }, | |
| { | |
| key: 'wolf', | |
| name: 'Wolf', | |
| weight: 20, | |
| level: 4, | |
| maxHP: 70, | |
| attack: 16, | |
| defense: 9, | |
| magic: 2, | |
| speed: 14, | |
| exp: 55, | |
| gold: 40 | |
| } | |
| ] | |
| }; | |
| 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