Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save StringManolo/d328980c5cba4d234b41f6d7a22fe5ac to your computer and use it in GitHub Desktop.
Chat GPT5 generated (with thinking)
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