feat(faction): Implement faction combat system with attack mechanics, pathfinding, and damage handling

- Added speed and attackRate stats to faction configuration
- Implemented faction class with health management, attack animations, and combat logic
- Added sight detection to find and pursue enemies
- Implemented attack mechanics with damage calculation and enemy destruction handling
- Removed old collision detection system in favor of new combat system
- Added timer-based attack and sight checks
- Implemented melee attack animations and enemy targeting
This commit is contained in:
Brian Fertig 2025-10-20 19:46:59 -06:00
parent 425978dcb4
commit 75c2be852e
4 changed files with 118 additions and 53 deletions

View File

@ -6,8 +6,10 @@ export const FACTION_CONFIG = {
stats: { stats: {
type: 'melee', type: 'melee',
health: 5, health: 5,
speed: 50,
attackMin: 1, attackMin: 1,
attackMax: 4, attackMax: 4,
attackRate: 2000,
meleeDefense: 1, meleeDefense: 1,
rangeDefense: 2, rangeDefense: 2,
magicDefense: 1, magicDefense: 1,

View File

@ -21,15 +21,20 @@ export class Faction extends Phaser.GameObjects.Sprite {
Object.entries(FACTION_CONFIG[faction][frame].stats).forEach(([key, value]) => { Object.entries(FACTION_CONFIG[faction][frame].stats).forEach(([key, value]) => {
this.stats[key] = value; this.stats[key] = value;
}); });
console.log(this); this.stats.fullHealth = this.stats.health;
// Path following properties // Path following properties
this.path = path; this.path = path;
this.side = side; this.side = side;
this.currentWaypoint = 0; this.currentWaypoint = 0;
this.speed = 50;
this.arrived = false; this.arrived = false;
this.isPathPaused = false; this.isPathPaused = false;
this.movingToEnemy = false;
this.enemy = false;
// Timers
this.timerSight = 0;
this.timerAttack = 0;
this.animKey = `${texture}-${frame}`; this.animKey = `${texture}-${frame}`;
this.createAnim(); this.createAnim();
@ -50,6 +55,15 @@ export class Faction extends Phaser.GameObjects.Sprite {
frameRate: 2, frameRate: 2,
repeat: -1 repeat: -1
}); });
this.scene.anims.create({
key: `${this.animKey}-attack`,
frames: [
{ key: this.texture.key, frame: this.frame.name+2 },
{ key: this.texture.key, frame: this.frame.name+3 }
],
frameRate: 2,
repeat: -1
});
} }
} }
@ -94,8 +108,8 @@ export class Faction extends Phaser.GameObjects.Sprite {
// Set velocity based on angle and speed // Set velocity based on angle and speed
this.body.setVelocity( this.body.setVelocity(
Math.cos(angle) * this.speed, Math.cos(angle) * this.stats.speed,
Math.sin(angle) * this.speed Math.sin(angle) * this.stats.speed
); );
} }
} }
@ -120,10 +134,104 @@ export class Faction extends Phaser.GameObjects.Sprite {
this.setFlipX(false); // Moving left, flipped this.setFlipX(false); // Moving left, flipped
} }
} }
// Timers
this.timerSight += delta;
this.timerAttack += delta;
// Move towards next waypoint if not arrived and path is not paused // Move towards next waypoint if not arrived and path is not paused
if (!this.arrived && !this.isPathPaused) { if (!this.arrived && !this.isPathPaused) {
this.moveToWaypoint(this.currentWaypoint); this.moveToWaypoint(this.currentWaypoint);
} }
// Check distance to closest unit of the other side every second
if (this.timerSight >= 500 && this.movingToEnemy === false && this.enemy === false) {
this.timerSight = 0;
this.checkSight();
}
// Handle moving towards an enemy
if (this.movingToEnemy && this.movingToEnemy.active) {
const distance = Phaser.Math.Distance.Between(this.x, this.y, this.movingToEnemy.x, this.movingToEnemy.y);
if (distance <= this.stats.range) {
this.enemy = this.movingToEnemy;
this.movingToEnemy = false;
this.body.setVelocity(0, 0);
this.play(`${this.animKey}-attack`, true);
}
}
else if (this.movingToEnemy !== false) {
this.movingToEnemy = false;
this.resumePath();
}
// Attack
if (this.enemy && this.enemy.active && this.timerAttack >= 2000) {
this.timerAttack = 0;
console.log('attack');
this.attack();
}
}
checkSight() {
let enemyGroup;
if (this.side === 'left') {
enemyGroup = this.scene.factionRight;
} else {
enemyGroup = this.scene.factionLeft;
}
if (!enemyGroup) return;
let closestDistance = Infinity;
let closestEnemy = null;
// Find the closest enemy unit
enemyGroup.children.iterate((enemy) => {
if (enemy && enemy !== this) {
const distance = Phaser.Math.Distance.Between(this.x, this.y, enemy.x, enemy.y);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
});
if (closestEnemy && closestDistance <= this.stats.sight) {
this.pausePath();
this.movingToEnemy = closestEnemy;
const angle = Phaser.Math.Angle.Between(this.x, this.y, closestEnemy.x, closestEnemy.y);
const speed = this.stats.speed;
this.body.setVelocity(
Math.cos(angle) * speed,
Math.sin(angle) * speed
);
}
}
attack() {
if (this.stats.type === 'melee' && this.enemy && this.enemy.active) {
const status = this.enemy.takeDamage('melee', Phaser.Math.Between(this.stats.attackMin, this.stats.attackMax));
if (status === 'dead') {
this.resumePath();
}
}
}
takeDamage(type, amount) {
const defense = this.stats[`${type}Defense`];
const damage = Math.max(0, amount - defense);
this.stats.health -= damage;
// Check if unit is dead
if (this.stats.health <= 0) {
this.destroy();
return 'dead';
}
else {
return 'alive';
}
} }
} }

View File

@ -39,9 +39,6 @@ export class Level extends Phaser.Scene {
const test = new Faction(this, 4*64, 5*64, 'dark-ages', 0, 'left', 1, 'dark-ages').setOrigin(0.5); const test = new Faction(this, 4*64, 5*64, 'dark-ages', 0, 'left', 1, 'dark-ages').setOrigin(0.5);
const test2 = new Faction(this, 27*64, 4*64, 'dark-ages', 0, 'right', 1, 'dark-ages').setOrigin(0.5); const test2 = new Faction(this, 27*64, 4*64, 'dark-ages', 0, 'right', 1, 'dark-ages').setOrigin(0.5);
// Add collision detection
this.physics.add.overlap(this.factionLeft, this.factionRight, this.handleFactionCollision, null, this);
} }
update(time, delta) { update(time, delta) {
@ -58,52 +55,6 @@ export class Level extends Phaser.Scene {
}); });
} }
handleFactionCollision(faction1, faction2) {
// Check if factions are on opposite sides
if (faction1.side !== faction2.side) {
// Calculate distance between factions
const distance = Phaser.Math.Distance.Between(faction1.x, faction1.y, faction2.x, faction2.y);
// If within attack range
if (distance < 150) {
// Pause path following and move towards each other
faction1.pausePath();
faction2.pausePath();
// Move towards each other
const angle = Phaser.Math.Angle.Between(faction1.x, faction1.y, faction2.x, faction2.y);
const speed = 50;
faction1.body.setVelocity(
Math.cos(angle) * speed,
Math.sin(angle) * speed
);
faction2.body.setVelocity(
Math.cos(angle + Math.PI) * speed,
Math.sin(angle + Math.PI) * speed
);
// Flip sprites based on movement direction
if (faction1.body.velocity.x > 0) {
faction1.setFlipX(true);
} else {
faction1.setFlipX(false);
}
if (faction2.body.velocity.x > 0) {
faction2.setFlipX(true);
} else {
faction2.setFlipX(false);
}
} else {
// If they're not close enough, resume normal path following
faction1.resumePath();
faction2.resumePath();
}
}
}
onPointerDown(pointer) { onPointerDown(pointer) {
this.isDragging = true; this.isDragging = true;
this.dragStartX = pointer.x; this.dragStartX = pointer.x;

4
start_web.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
# Start a simple HTTP server on port 8000
python3 -m http.server 8000