Skip to content

Instantly share code, notes, and snippets.

@StringManolo
Created November 11, 2025 23:29
Show Gist options
  • Select an option

  • Save StringManolo/ba54841c54e8ffa047ad8f1f3a3a42dd to your computer and use it in GitHub Desktop.

Select an option

Save StringManolo/ba54841c54e8ffa047ad8f1f3a3a42dd to your computer and use it in GitHub Desktop.
Gemini 2.5 flash
import { getGame } from "./GameConfig"; // Placeholder for actual Game config import
// --- Stat and Character Models ---
class BattleCharacter {
constructor(name, maxHp, attack, magic, defense, atbRate) {
this.name = name;
this.maxHp = maxHp;
this.currentHp = maxHp;
this.attack = attack;
this.magic = magic;
this.defense = defense;
this.atbRate = atbRate; // Rate at which ATB bar fills (e.g., 5)
this.atbValue = 0; // Current ATB value (0 to 100)
this.isDefending = false;
}
}
class BattleEnemy extends BattleCharacter {
constructor(config) {
super(config.name, config.hp, config.attack, config.magic, config.defense, config.atbRate);
this.key = config.key;
}
}
// --- Phaser Battle Scene ---
export class BattleScene extends Phaser.Scene {
constructor() {
super({ key: 'BattleScene' });
this.battleState = 'INIT';
this.uiElements = [];
}
init(data) {
this.playerData = data.playerData || {
name: 'Warrior',
maxHp: 200,
attack: 30,
magic: 15,
defense: 10,
atbRate: 5 // Default ATB rate
};
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
// TODO: Create sprites for enemies animations
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 (assuming the original image assets are used)
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() {
this.battleState = 'INIT';
this.sound.play('battle_music', { loop: true });
// 1. Setup Background and Enemy
const backgroundKey = this.getBackgroundKey(this.terrain);
this.add.image(400, 300, backgroundKey).setDisplaySize(800, 600);
this.enemyConfig = this.getEnemyForTerrain(this.terrain);
// 2. Initialize Player and Enemy Objects with Stats
this.player = new BattleCharacter(
this.playerData.name,
this.playerData.maxHp,
this.playerData.attack,
this.playerData.magic,
this.playerData.defense,
this.playerData.atbRate
);
this.enemy = new BattleEnemy(this.enemyConfig);
// 3. Setup Sprites
this.enemySprite = this.add.image(600, 330, this.enemy.key).setDisplaySize(175, 175);
this.warriorSprite = this.add.sprite(200, 480, 'warrior_basic_attack').setDisplaySize(175, 175);
this.warriorSprite.setOrigin(0.5, 1);
this.warriorSprite.setDepth(2);
// 4. Setup UI (HP Bars and Name Tags)
this.setupUI();
// 5. Start Battle
this.input.off('pointerdown'); // Remove original pointerdown listener
this.battleState = 'WAITING_FOR_ATB';
console.log("Battle started! State: WAITING_FOR_ATB");
}
update(time, delta) {
if (this.battleState === 'WAITING_FOR_ATB') {
const deltaFactor = delta / 1000; // Convert ms to seconds
// Fill Player ATB
this.player.atbValue = Math.min(100, this.player.atbValue + this.player.atbRate * deltaFactor * 10);
this.updateHpBar('player', this.player.currentHp, this.player.maxHp);
this.updateAtbBar('player', this.player.atbValue);
if (this.player.atbValue >= 100) {
this.player.atbValue = 100;
this.battleState = 'PLAYER_TURN';
this.handlePlayerTurn();
return;
}
// Fill Enemy ATB
this.enemy.atbValue = Math.min(100, this.enemy.atbValue + this.enemy.atbRate * deltaFactor * 10);
this.updateHpBar('enemy', this.enemy.currentHp, this.enemy.maxHp);
this.updateAtbBar('enemy', this.enemy.atbValue);
if (this.enemy.atbValue >= 100) {
this.enemy.atbValue = 100;
this.battleState = 'ENEMY_TURN';
this.handleEnemyTurn();
return;
}
}
}
setupUI() {
// Clear previous UI elements
this.uiElements.forEach(el => el.destroy());
this.uiElements = [];
const barHeight = 10;
// --- Player Status Panel (Bottom Left) ---
const pX = 10;
const pY = 520;
this.add.rectangle(pX, pY - 20, 200, 70, 0x000000, 0.7).setOrigin(0, 0).setStrokeStyle(2, 0xffffff);
// Player Name
this.uiElements.push(this.add.text(pX + 10, pY - 10, this.player.name, { fontSize: '14px', fill: '#fff' }));
// Player HP Bar
this.uiElements.push(this.add.text(pX + 10, pY + 10, 'HP:', { fontSize: '12px', fill: '#fff' }));
this.playerHpBar = this.createBar(pX + 40, pY + 15, 150, barHeight, 0x880000, 0x00ff00, 100);
this.uiElements.push(this.playerHpBar.barOutline, this.playerHpBar.barFill);
// Player ATB Bar
this.uiElements.push(this.add.text(pX + 10, pY + 30, 'ATB:', { fontSize: '12px', fill: '#fff' }));
this.playerAtbBar = this.createBar(pX + 40, pY + 35, 150, barHeight, 0x888888, 0x3366ff, 0);
this.uiElements.push(this.playerAtbBar.barOutline, this.playerAtbBar.barFill);
// --- Enemy Status Panel (Top Right) ---
const eX = 590;
const eY = 10;
this.add.rectangle(eX, eY, 200, 70, 0x000000, 0.7).setOrigin(0, 0).setStrokeStyle(2, 0xffffff);
// Enemy Name
this.uiElements.push(this.add.text(eX + 10, eY + 5, this.enemy.name, { fontSize: '14px', fill: '#fff' }));
// Enemy HP Bar
this.uiElements.push(this.add.text(eX + 10, eY + 25, 'HP:', { fontSize: '12px', fill: '#fff' }));
this.enemyHpBar = this.createBar(eX + 40, eY + 30, 150, barHeight, 0x880000, 0x00ff00, 100);
this.uiElements.push(this.enemyHpBar.barOutline, this.enemyHpBar.barFill);
// Enemy ATB Bar
this.uiElements.push(this.add.text(eX + 10, eY + 45, 'ATB:', { fontSize: '12px', fill: '#fff' }));
this.enemyAtbBar = this.createBar(eX + 40, eY + 50, 150, barHeight, 0x888888, 0x3366ff, 0);
this.uiElements.push(this.enemyAtbBar.barOutline, this.enemyAtbBar.barFill);
this.updateHpBar('player', this.player.currentHp, this.player.maxHp);
this.updateHpBar('enemy', this.enemy.currentHp, this.enemy.maxHp);
}
createBar(x, y, width, height, bgColor, fillColor, percentage) {
const barOutline = this.add.rectangle(x, y, width + 2, height + 2, 0x000000).setOrigin(0, 0);
const barFill = this.add.rectangle(x + 1, y + 1, width * (percentage / 100), height, fillColor).setOrigin(0, 0);
barFill.fillColor = fillColor;
barFill.maxWidth = width;
return { barOutline, barFill };
}
updateHpBar(target, currentHp, maxHp) {
const percentage = Math.max(0, currentHp / maxHp);
const bar = target === 'player' ? this.playerHpBar.barFill : this.enemyHpBar.barFill;
bar.width = bar.maxWidth * percentage;
bar.fillColor = percentage > 0.5 ? 0x00ff00 : percentage > 0.2 ? 0xffff00 : 0xff0000;
bar.fillColor = Phaser.Display.Color.GetColor(
(bar.fillColor >> 16) & 0xFF,
(bar.fillColor >> 8) & 0xFF,
bar.fillColor & 0xFF
);
}
updateAtbBar(target, atbValue) {
const percentage = atbValue / 100;
const bar = target === 'player' ? this.playerAtbBar.barFill : this.enemyAtbBar.barFill;
bar.width = bar.maxWidth * percentage;
}
// --- Turn Management and Menu ---
handlePlayerTurn() {
this.player.isDefending = false;
this.showActionMenu();
console.log("Player's Turn! Choose an action.");
}
handleEnemyTurn() {
this.enemy.isDefending = false;
this.player.atbValue = 0; // Enemy consumed its turn
this.hideActionMenu();
this.battleState = 'PROCESSING_ACTION';
// Simple Enemy AI: always attack
this.time.delayedCall(1500, () => {
this.performEnemyAttack();
});
}
showActionMenu() {
const menuX = 600;
const menuY = 400;
// Menu background
this.actionMenuBg = this.add.rectangle(menuX - 5, menuY - 5, 180, 200, 0x000000, 0.9).setOrigin(0, 0).setStrokeStyle(2, 0xffffff);
this.uiElements.push(this.actionMenuBg);
const actions = [
{ text: 'Attack', handler: this.performAttack },
{ text: 'Magic', handler: this.performMagic },
{ text: 'Ability', handler: this.performAbility },
{ text: 'Defend', handler: this.performDefend },
{ text: 'Item', handler: this.useItem },
{ text: 'Flee', handler: this.performFlee },
];
actions.forEach((action, index) => {
const btn = this.add.text(menuX, menuY + index * 30, action.text, { fontSize: '18px', fill: '#fff' })
.setOrigin(0, 0)
.setInteractive({ useHandCursor: true })
.on('pointerdown', () => {
action.handler.call(this); // Call the handler within the scene context
})
.on('pointerover', () => {
btn.setFill('#ffcc00');
})
.on('pointerout', () => {
btn.setFill('#fff');
});
this.uiElements.push(btn);
});
}
hideActionMenu() {
this.uiElements = this.uiElements.filter(el => {
if (el === this.actionMenuBg || this.actionMenuBg?.parentContainer?.list.includes(el)) {
el.destroy();
return false;
}
return true;
});
this.actionMenuBg?.destroy();
this.actionMenuBg = null;
}
// --- Action Handlers ---
performAttack() {
this.hideActionMenu();
this.battleState = 'PROCESSING_ACTION';
// Save warrior position before moving to attack
const originalX = this.warriorSprite.x;
const originalY = this.warriorSprite.y;
// Target position (Enemy)
const targetX = this.enemySprite.x - 70;
const targetY = this.enemySprite.y + 50;
// Move to the enemy and reduce size of the warrior
this.tweens.add({
targets: this.warriorSprite,
x: targetX,
y: targetY,
displayWidth: 175 * 0.7,
displayHeight: 175 * 0.7,
duration: 200,
onComplete: () => {
// Attack animation sequence
this.warriorSprite.setTexture('warrior_basic_attack_2');
this.time.delayedCall(25, () => {
this.warriorSprite.setTexture('warrior_basic_attack_3');
this.sound.play('warrior_slash', { volume: 0.8 });
this.time.delayedCall(20, () => {
this.warriorSprite.setTexture('warrior_basic_attack_4');
// Apply Damage
const damage = Math.max(1, this.player.attack - (this.enemy.defense / 2));
this.takeDamage('enemy', damage);
this.showDamageText(this.enemySprite.x, this.enemySprite.y - 100, damage, '#ff0000');
this.cameraShake(50, 0.005); // Minor shake on hit
this.time.delayedCall(60, () => {
// Go back to default animation
this.warriorSprite.setTexture('warrior_basic_attack');
// Restaurate warrior to default position
this.tweens.add({
targets: this.warriorSprite,
x: originalX,
y: originalY,
displayWidth: 175,
displayHeight: 175,
duration: 200,
onComplete: () => {
this.player.atbValue = 0; // Reset ATB
this.checkBattleEnd();
}
});
});
});
});
}
});
}
performEnemyAttack() {
// Enemy attack animation and damage
this.tweens.add({
targets: this.enemySprite,
x: this.warriorSprite.x + 70,
duration: 300,
yoyo: true,
ease: 'Sine.easeInOut',
onYoyo: () => {
// Apply Damage
const damage = Math.max(1, this.enemy.attack - (this.player.defense / 2));
this.takeDamage('player', damage);
this.showDamageText(this.warriorSprite.x, this.warriorSprite.y - 200, damage, '#ff0000');
this.sound.play('warrior_slash', { volume: 0.5 });
},
onComplete: () => {
this.enemy.atbValue = 0; // Reset ATB
this.checkBattleEnd();
}
});
}
performMagic() {
this.hideActionMenu();
this.battleState = 'PROCESSING_ACTION';
this.showStatusText(`${this.player.name} casts Fire! (Placeholder)`, 1500, () => {
const damage = Math.max(1, this.player.magic + 10 - (this.enemy.defense / 4));
this.takeDamage('enemy', damage);
this.showDamageText(this.enemySprite.x, this.enemySprite.y - 100, damage, '#00ffff');
this.player.atbValue = 0;
this.checkBattleEnd();
});
}
performAbility() {
this.hideActionMenu();
this.battleState = 'PROCESSING_ACTION';
this.showStatusText(`${this.player.name} uses Cleave! (Placeholder)`, 1500, () => {
const damage = Math.max(1, this.player.attack * 1.5 - this.enemy.defense);
this.takeDamage('enemy', damage);
this.showDamageText(this.enemySprite.x, this.enemySprite.y - 100, damage, '#ff9900');
this.player.atbValue = 0;
this.checkBattleEnd();
});
}
performDefend() {
this.hideActionMenu();
this.battleState = 'PROCESSING_ACTION';
this.player.isDefending = true;
this.showStatusText(`${this.player.name} is defending!`, 1500, () => {
this.player.atbValue = 0;
this.checkBattleEnd();
});
}
useItem() {
this.hideActionMenu();
this.battleState = 'PROCESSING_ACTION';
this.showStatusText(`${this.player.name} uses Potion! (Placeholder)`, 1500, () => {
const heal = 50;
this.player.currentHp = Math.min(this.player.maxHp, this.player.currentHp + heal);
this.updateHpBar('player', this.player.currentHp, this.player.maxHp);
this.showDamageText(this.warriorSprite.x, this.warriorSprite.y - 200, heal, '#00ff00', 'Heal');
this.player.atbValue = 0;
this.checkBattleEnd();
});
}
performFlee() {
this.hideActionMenu();
this.battleState = 'PROCESSING_ACTION';
const success = Math.random() > 0.4; // 60% chance to flee
const fleeMessage = success ? 'Flee successful!' : 'Flee failed!';
this.showStatusText(fleeMessage, 2000, () => {
if (success) {
this.endBattle(false); // End battle, no victory/rewards
} else {
this.player.atbValue = 0;
this.checkBattleEnd(); // Go back to waiting for ATB
}
});
}
// --- Utility Functions ---
takeDamage(target, damage) {
const combatant = target === 'player' ? this.player : this.enemy;
combatant.currentHp = Math.max(0, combatant.currentHp - damage);
if (target === 'player') {
this.updateHpBar('player', this.player.currentHp, this.player.maxHp);
} else {
this.updateHpBar('enemy', this.enemy.currentHp, this.enemy.maxHp);
if (this.enemy.currentHp <= 0) {
this.enemySprite.setTint(0x555555);
}
}
}
showDamageText(x, y, amount, color, prefix = '') {
const text = this.add.text(x, y, `${prefix}${amount}`, {
fontSize: '30px',
fill: color,
stroke: '#000000',
strokeThickness: 4,
fontFamily: 'Arial Black'
}).setOrigin(0.5);
this.tweens.add({
targets: text,
y: y - 50,
alpha: 0,
duration: 1000,
onComplete: () => {
text.destroy();
}
});
}
showStatusText(message, duration, onComplete) {
const statusText = this.add.text(400, 550, message, {
fontSize: '24px',
fill: '#fff',
backgroundColor: '#333',
padding: { x: 10, y: 5 }
}).setOrigin(0.5);
this.uiElements.push(statusText);
this.time.delayedCall(duration, () => {
statusText.destroy();
this.uiElements = this.uiElements.filter(el => el !== statusText);
if (onComplete) onComplete();
});
}
cameraShake(duration, intensity) {
this.cameras.main.shake(duration, intensity);
}
checkBattleEnd() {
if (this.player.currentHp <= 0) {
this.endBattle(false);
} else if (this.enemy.currentHp <= 0) {
this.endBattle(true);
} else {
// Continue the battle loop
this.battleState = 'WAITING_FOR_ATB';
}
}
endBattle(victory) {
if (this.battleState === 'BATTLE_END') return;
this.battleState = 'BATTLE_END';
this.sound.stopAll();
const resultMessage = victory ? 'VICTORY!' : 'DEFEAT...';
this.showStatusText(resultMessage, 3000, () => {
this.scene.stop();
if (this.onBattleEnd) {
this.onBattleEnd({
victory: victory,
experience: victory ? 100 : 0,
gold: victory ? 50 : 0,
enemy: this.enemy.name
});
}
});
}
// --- Configuration from original code ---
getBackgroundKey(terrain) {
const terrainMap = {
'grass': 'battle_grass',
'forest': 'battle_forest',
'mountain': 'battle_mountain',
'path': 'battle_dirt',
'water': 'battle_grass'
};
return terrainMap[terrain] || 'battle_grass';
}
// select enemies based on terrain + chance of finding the enemy in that terrain
getEnemyForTerrain(terrain) {
const baseEnemyStats = {
// HP, ATK, MAG, DEF, ATB Rate
'goat': { hp: 80, attack: 10, magic: 5, defense: 5, atbRate: 4 },
'war_dwarf': { hp: 150, attack: 25, magic: 5, defense: 15, atbRate: 3 },
'snake': { hp: 100, attack: 15, magic: 10, defense: 8, atbRate: 6 },
'wolf': { hp: 120, attack: 20, magic: 5, defense: 10, atbRate: 5 },
'fly': { hp: 50, attack: 8, magic: 2, defense: 2, atbRate: 7 },
'war_blob': { hp: 250, attack: 15, magic: 5, defense: 20, atbRate: 2 },
'war_orc': { hp: 180, attack: 30, magic: 10, defense: 12, atbRate: 4 },
'magic_demon': { hp: 130, attack: 20, magic: 35, defense: 8, atbRate: 5 },
};
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': [ // Not implemented yet, walking on water not unlocked
{ 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'];
const selectedEnemy = this.selectRandomEnemy(pool);
// Merge base stats with selection
return {
...selectedEnemy,
...baseEnemyStats[selectedEnemy.key]
};
}
// Select the enemy based on %
selectRandomEnemy(enemyPool) {
// Calculate total weight
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;
}
}
// Fallback: first enemy available
return enemyPool[0];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment