Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save StringManolo/7a12afdf19d74b318284a720ed1e3dd5 to your computer and use it in GitHub Desktop.
Kimi K2
export class BattleScene extends Phaser.Scene {
constructor() {
super({ key: 'BattleScene' });
}
init(data) {
this.playerData = data.playerData;
this.onBattleEnd = data.onBattleEnd;
this.terrain = data.terrain;
// Initialize battle system
this.battleSystem = null;
this.battleMenu = null;
this.isProcessingTurn = false;
}
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 attack animation 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");
// 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];
}
// Background
const background = this.add.image(400, 300, backgroundKey);
background.setDisplaySize(800, 600);
// Setup battle units
this.setupBattleUnits();
// Create UI elements
this.createBattleUI();
// Initialize battle system
this.battleSystem = new BattleSystem(this);
// Remove old click handler
this.input.off('pointerdown');
// Play battle music
this.sound.play('battle_music', { loop: true });
}
setupBattleUnits() {
// Get enemy configuration
this.enemyConfig = this.getEnemyForTerrain(this.terrain);
// Create enemy unit with stats
this.enemyUnit = new BattleUnit({
name: this.enemyConfig.name,
key: this.enemyConfig.key,
maxHp: this.getEnemyStats(this.enemyConfig.key).hp,
attack: this.getEnemyStats(this.enemyConfig.key).attack,
defense: this.getEnemyStats(this.enemyConfig.key).defense,
speed: this.getEnemyStats(this.enemyConfig.key).speed,
isPlayer: false
});
// Create player unit with stats from data or defaults
const playerStats = this.getPlayerStats();
this.playerUnit = new BattleUnit({
name: playerStats.name || 'Warrior',
key: 'warrior_basic_attack',
maxHp: playerStats.maxHp,
maxMp: playerStats.maxMp || 50,
attack: playerStats.attack,
defense: playerStats.defense,
magic: playerStats.magic || 15,
speed: playerStats.speed,
isPlayer: true
});
// Add enemy sprite
this.enemy = this.add.image(400, 330, this.enemyConfig.key);
this.enemy.setDisplaySize(175, 175);
// Add player sprite
this.warrior = this.add.sprite(400, 650, 'warrior_basic_attack');
this.warrior.setOrigin(0.5, 1);
this.warrior.setDisplaySize(175, 175);
this.warrior.setDepth(2);
this.isAttacking = false;
}
getPlayerStats() {
const defaults = {
name: 'Warrior',
maxHp: 500,
maxMp: 50,
attack: 75,
defense: 45,
magic: 15,
speed: 25
};
if (!this.playerData) return defaults;
return {
name: this.playerData.name || defaults.name,
maxHp: this.playerData.maxHp || defaults.maxHp,
maxMp: this.playerData.maxMp || defaults.maxMp,
attack: this.playerData.attack || defaults.attack,
defense: this.playerData.defense || defaults.defense,
magic: this.playerData.magic || defaults.magic,
speed: this.playerData.speed || defaults.speed,
currentHp: this.playerData.currentHp || this.playerData.maxHp || defaults.maxHp,
currentMp: this.playerData.currentMp || this.playerData.maxMp || defaults.maxMp
};
}
getEnemyStats(enemyKey) {
const enemyStats = {
'goat': { hp: 200, attack: 40, defense: 20, speed: 35 },
'war_dwarf': { hp: 350, attack: 60, defense: 50, speed: 15 },
'snake': { hp: 150, attack: 35, defense: 15, speed: 40 },
'wolf': { hp: 250, attack: 50, defense: 25, speed: 30 },
'fly': { hp: 100, attack: 25, defense: 10, speed: 45 },
'war_blob': { hp: 400, attack: 45, defense: 40, speed: 10 },
'war_orc': { hp: 450, attack: 70, defense: 45, speed: 20 },
'magic_demon': { hp: 600, attack: 80, defense: 60, speed: 25 }
};
return enemyStats[enemyKey] || enemyStats['goat'];
}
createBattleUI() {
// Create menu background with Graphics
const menuGraphics = this.add.graphics();
menuGraphics.fillStyle(0x000000, 0.85);
menuGraphics.fillRoundedRect(10, 440, 780, 150, 8);
menuGraphics.lineStyle(2, 0xffffff, 1);
menuGraphics.strokeRoundedRect(10, 440, 780, 150, 8);
this.menuBg = menuGraphics;
this.menuBg.setDepth(10);
this.menuBg.setVisible(false);
// Create HP Bars
this.createHPBars();
// Create ATB Bars
this.createATBBars();
// Create Battle Menu
this.createBattleMenu();
// Battle log
this.battleLog = this.add.text(10, 10, '', {
fontSize: '16px',
fill: '#ffffff',
backgroundColor: '#000000',
padding: { x: 10, y: 5 }
});
this.battleLog.setDepth(10);
}
createHPBars() {
const playerHpX = 100;
const playerHpY = 450;
const hpBarWidth = 150;
const hpBarHeight = 20;
// Player HP Bar Background
this.playerHpBarBg = this.add.rectangle(playerHpX, playerHpY, hpBarWidth, hpBarHeight, 0x333333);
this.playerHpBarBg.setOrigin(0.5);
this.playerHpBarBg.setDepth(10);
// Player HP Bar
this.playerHpBar = this.add.rectangle(playerHpX - hpBarWidth / 2, playerHpY, hpBarWidth, hpBarHeight, 0x00ff00);
this.playerHpBar.setOrigin(0, 0.5);
this.playerHpBar.setDepth(11);
this.playerHpText = this.add.text(playerHpX, playerHpY - 20, '', {
fontSize: '14px',
fill: '#ffffff'
}).setOrigin(0.5).setDepth(12);
// Enemy HP Bar
const enemyHpX = 400;
const enemyHpY = 250;
const enemyHpBarWidth = 150;
const enemyHpBarHeight = 15;
this.enemyHpBarBg = this.add.rectangle(enemyHpX, enemyHpY, enemyHpBarWidth, enemyHpBarHeight, 0x333333);
this.enemyHpBarBg.setOrigin(0.5);
this.enemyHpBarBg.setDepth(10);
this.enemyHpBar = this.add.rectangle(enemyHpX - enemyHpBarWidth / 2, enemyHpY, enemyHpBarWidth, enemyHpBarHeight, 0xff0000);
this.enemyHpBar.setOrigin(0, 0.5);
this.enemyHpBar.setDepth(11);
this.enemyHpText = this.add.text(enemyHpX, enemyHpY - 15, '', {
fontSize: '12px',
fill: '#ffffff'
}).setOrigin(0.5).setDepth(12);
this.updateHPBars();
}
createATBBars() {
const playerAtbX = 100;
const playerAtbY = 480;
const atbBarWidth = 150;
const atbBarHeight = 10;
// Player ATB Bar Background
this.playerAtbBarBg = this.add.rectangle(playerAtbX, playerAtbY, atbBarWidth, atbBarHeight, 0x222222);
this.playerAtbBarBg.setOrigin(0.5);
this.playerAtbBarBg.setDepth(10);
// Player ATB Bar
this.playerAtbBar = this.add.rectangle(playerAtbX - atbBarWidth / 2, playerAtbY, atbBarWidth, atbBarHeight, 0x00ff00);
this.playerAtbBar.setOrigin(0, 0.5);
this.playerAtbBar.setDepth(11);
// Enemy ATB Bar
const enemyAtbX = 400;
const enemyAtbY = 270;
const enemyAtbBarWidth = 150;
const enemyAtbBarHeight = 10;
this.enemyAtbBarBg = this.add.rectangle(enemyAtbX, enemyAtbY, enemyAtbBarWidth, enemyAtbBarHeight, 0x222222);
this.enemyAtbBarBg.setOrigin(0.5);
this.enemyAtbBarBg.setDepth(10);
this.enemyAtbBar = this.add.rectangle(enemyAtbX - enemyAtbBarWidth / 2, enemyAtbY, enemyAtbBarWidth, enemyAtbBarHeight, 0xff0000);
this.enemyAtbBar.setOrigin(0, 0.5);
this.enemyAtbBar.setDepth(11);
}
createBattleMenu() {
this.menuOptions = ['Attack', 'Magic', 'Ability', 'Defend', 'Items', 'Flee'];
this.menuIndex = 0;
this.menuTexts = [];
this.menuVisible = false;
for (let i = 0; i < this.menuOptions.length; i++) {
const text = this.add.text(200 + (i % 3) * 150, 470 + Math.floor(i / 3) * 30,
this.menuOptions[i], {
fontSize: '18px',
fill: '#ffffff',
backgroundColor: '#333333',
padding: { x: 10, y: 5 }
});
text.setInteractive();
text.setDepth(12);
text.setVisible(false);
text.on('pointerover', () => {
if (this.menuVisible) text.setBackgroundColor('#666666');
});
text.on('pointerout', () => {
if (this.menuVisible) text.setBackgroundColor('#333333');
});
text.on('pointerdown', () => {
if (this.menuVisible) this.selectMenuOption(i);
});
this.menuTexts.push(text);
}
}
showBattleMenu() {
this.menuVisible = true;
this.menuBg.setVisible(true);
this.menuTexts.forEach(text => text.setVisible(true));
this.updateMenuSelection();
}
hideBattleMenu() {
this.menuVisible = false;
this.menuBg.setVisible(false);
this.menuTexts.forEach(text => text.setVisible(false));
}
updateMenuSelection() {
this.menuTexts.forEach((text, index) => {
if (index === this.menuIndex) {
text.setBackgroundColor('#888888');
} else {
text.setBackgroundColor('#333333');
}
});
}
selectMenuOption(index) {
if (this.isProcessingTurn) return;
const option = this.menuOptions[index];
this.hideBattleMenu();
this.isProcessingTurn = true;
switch (option) {
case 'Attack':
this.executeAttack();
break;
case 'Magic':
this.executeMagic();
break;
case 'Ability':
this.executeAbility();
break;
case 'Defend':
this.executeDefend();
break;
case 'Items':
this.executeItems();
break;
case 'Flee':
this.executeFlee();
break;
}
}
executeAttack() {
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 });
// Calculate damage
const damage = this.calculateDamage(this.playerUnit, this.enemyUnit, 'physical');
this.enemyUnit.currentHp = Math.max(0, this.enemyUnit.currentHp - damage);
this.updateHPBars();
this.showDamageNumber(damage, 400, 330, false);
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.isAttacking = false;
this.endTurn();
}
});
});
});
});
}
});
}
executeMagic() {
this.logMessage(`${this.playerUnit.name} casts a spell!`);
this.time.delayedCall(1000, () => this.endTurn());
}
executeAbility() {
this.logMessage(`${this.playerUnit.name} uses a special ability!`);
this.time.delayedCall(1000, () => this.endTurn());
}
executeDefend() {
this.logMessage(`${this.playerUnit.name} is defending!`);
this.playerUnit.isDefending = true;
this.time.delayedCall(500, () => this.endTurn());
}
executeItems() {
this.logMessage(`${this.playerUnit.name} uses an item!`);
this.time.delayedCall(1000, () => this.endTurn());
}
executeFlee() {
this.logMessage(`${this.playerUnit.name} attempts to flee!`);
const fleeChance = 0.6;
const success = Math.random() < fleeChance;
if (success) {
this.logMessage('Fled successfully!');
this.time.delayedCall(1000, () => this.endBattle(false, true));
} else {
this.logMessage("Couldn't escape!");
this.time.delayedCall(1000, () => this.endTurn());
}
}
enemyTurn() {
this.logMessage(`${this.enemyUnit.name} attacks!`);
this.time.delayedCall(500, () => {
const damage = this.calculateDamage(this.enemyUnit, this.playerUnit, 'physical');
this.playerUnit.currentHp = Math.max(0, this.playerUnit.currentHp - damage);
this.updateHPBars();
this.showDamageNumber(damage, 400, 650, true);
this.time.delayedCall(1000, () => this.endTurn());
});
}
calculateDamage(attacker, defender, type) {
const baseDamage = attacker.attack;
const defense = defender.defense * (defender.isDefending ? 1.5 : 1);
const variance = 0.85 + Math.random() * 0.3;
let damage = Math.floor((baseDamage * variance) - defense);
damage = Math.max(1, damage);
return damage;
}
showDamageNumber(damage, x, y, isPlayerDamage) {
const damageText = this.add.text(x, y - 50, `-${damage}`, {
fontSize: '24px',
fill: isPlayerDamage ? '#ff0000' : '#ffff00',
stroke: '#000000',
strokeThickness: 4
}).setOrigin(0.5);
this.tweens.add({
targets: damageText,
y: y - 100,
alpha: 0,
duration: 1000,
onComplete: () => damageText.destroy()
});
}
updateHPBars() {
// Player HP
const playerHpPercent = this.playerUnit.currentHp / this.playerUnit.maxHp;
this.playerHpBar.width = 150 * playerHpPercent;
this.playerHpText.setText(`${this.playerUnit.currentHp}/${this.playerUnit.maxHp}`);
// Enemy HP
const enemyHpPercent = this.enemyUnit.currentHp / this.enemyUnit.maxHp;
this.enemyHpBar.width = 150 * enemyHpPercent;
this.enemyHpText.setText(`${this.enemyUnit.currentHp}/${this.enemyUnit.maxHp}`);
}
updateATBBars() {
if (!this.battleSystem || this.isProcessingTurn) return;
// Player ATB
const playerAtbPercent = this.battleSystem.getATBPercent(this.playerUnit);
this.playerAtbBar.width = 150 * playerAtbPercent;
// Enemy ATB
const enemyAtbPercent = this.battleSystem.getATBPercent(this.enemyUnit);
this.enemyAtbBar.width = 150 * enemyAtbPercent;
}
logMessage(message) {
this.battleLog.setText(message);
this.time.delayedCall(2000, () => {
if (this.battleLog.text === message) {
this.battleLog.setText('');
}
});
}
endTurn() {
// Reset defending status
this.playerUnit.isDefending = false;
this.enemyUnit.isDefending = false;
// Reset ATB
if (this.battleSystem) {
this.battleSystem.resetATB(this.playerUnit);
this.battleSystem.resetATB(this.enemyUnit);
}
this.isProcessingTurn = false;
// Check for battle end
if (this.playerUnit.currentHp <= 0) {
this.endBattle(false, false);
} else if (this.enemyUnit.currentHp <= 0) {
this.endBattle(true, false);
}
}
endBattle(victory, fled) {
this.sound.stopAll();
this.scene.stop();
if (this.onBattleEnd) {
const result = {
victory: victory,
fled: fled,
experience: victory ? this.enemyUnit.maxHp : 0,
gold: victory ? Math.floor(this.enemyUnit.maxHp / 5) : 0,
enemy: this.enemyUnit.name,
playerStats: {
currentHp: this.playerUnit.currentHp,
currentMp: this.playerUnit.currentMp
}
};
this.onBattleEnd(result);
}
}
update() {
if (this.battleSystem && !this.isProcessingTurn) {
this.battleSystem.update();
this.updateATBBars();
// Check if player ATB is full and menu is not shown
if (this.battleSystem.isATBFull(this.playerUnit) && !this.menuVisible) {
this.showBattleMenu();
}
// Check if enemy ATB is full
if (this.battleSystem.isATBFull(this.enemyUnit) && !this.isProcessingTurn) {
this.isProcessingTurn = true;
this.hideBattleMenu();
this.enemyTurn();
}
}
}
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];
}
}
// Battle Unit Class
class BattleUnit {
constructor(config) {
this.name = config.name;
this.key = config.key;
this.maxHp = config.maxHp;
this.currentHp = config.currentHp || config.maxHp;
this.maxMp = config.maxMp || 0;
this.currentMp = config.currentMp || config.maxMp || 0;
this.attack = config.attack;
this.defense = config.defense;
this.magic = config.magic || 0;
this.speed = config.speed;
this.isPlayer = config.isPlayer;
this.isDefending = false;
this.atbValue = 0;
}
}
// Battle System Class
class BattleSystem {
constructor(scene) {
this.scene = scene;
this.maxATB = 1000;
this.isPaused = false;
}
update() {
if (this.isPaused) return;
// Update player ATB
if (this.scene.playerUnit.currentHp > 0 && !this.isATBFull(this.scene.playerUnit)) {
this.scene.playerUnit.atbValue += this.scene.playerUnit.speed;
}
// Update enemy ATB
if (this.scene.enemyUnit.currentHp > 0 && !this.isATBFull(this.scene.enemyUnit)) {
this.scene.enemyUnit.atbValue += this.scene.enemyUnit.speed;
}
}
isATBFull(unit) {
return unit.atbValue >= this.maxATB;
}
getATBPercent(unit) {
return Math.min(1, unit.atbValue / this.maxATB);
}
resetATB(unit) {
unit.atbValue = 0;
}
pause() {
this.isPaused = true;
}
resume() {
this.isPaused = false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment