feat: Add floating combat text and damage flash feedback

This commit is contained in:
2026-01-07 20:16:55 +01:00
parent 5c5cc13903
commit df3f892eb2
2 changed files with 101 additions and 22 deletions

View File

@@ -103,6 +103,34 @@ game.turnManager.on('phase_changed', (phase) => {
game.onCombatResult = (log) => {
ui.showCombatLog(log);
// 1. Show Attack Roll on Attacker
// Find Attacker pos
const attacker = game.heroes.find(h => h.id === log.attackerId) || game.monsters.find(m => m.id === log.attackerId);
if (attacker) {
const rollColor = log.hitSuccess ? '#00ff00' : '#888888'; // Green vs Gray
renderer.showFloatingText(attacker.x, attacker.y, `🎲 ${log.hitRoll}`, rollColor);
}
// 2. Show Damage on Defender
const defender = game.heroes.find(h => h.id === log.defenderId) || game.monsters.find(m => m.id === log.defenderId);
if (defender) {
setTimeout(() => { // Slight delay for cause-effect
if (log.hitSuccess) {
if (log.woundsCaused > 0) {
// HIT and WOUND
renderer.triggerDamageEffect(log.defenderId);
renderer.showFloatingText(defender.x, defender.y, `💥 -${log.woundsCaused}`, '#ff0000');
} else {
// BLOCKED (Hit but Toughness saved)
renderer.showFloatingText(defender.x, defender.y, `🛡️ Block`, '#ffff00');
}
} else {
// MISS
renderer.showFloatingText(defender.x, defender.y, `💨 Miss`, '#aaaaaa');
}
}, 500);
}
};
game.onEntityMove = (entity, path) => {

View File

@@ -59,6 +59,10 @@ export class GameRenderer {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
this.scene.add(ambientLight);
// Group for floating texts
this.floatingTextGroup = new THREE.Group();
this.scene.add(this.floatingTextGroup);
// Directional Light (Sun/Moon - creates shadows)
const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
dirLight.position.set(50, 100, 50);
@@ -260,29 +264,20 @@ export class GameRenderer {
const mesh = this.entities.get(entityId);
if (!mesh) return;
// 1. Red Halo (Temporary) - MATCHING ATTACKER SIZE (0.3 - 0.4)
const hitRingGeom = new THREE.RingGeometry(0.3, 0.4, 32);
const hitRingMat = new THREE.MeshBasicMaterial({
color: 0xff0000,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.9
// 1. Flash Effect (White Flash)
mesh.traverse((child) => {
if (child.material && child.material.map) { // Texture mesh
// Store original color if not stored
if (!child.userData.originalColor) {
child.userData.originalColor = child.material.color.clone();
}
// Set to red/white flash
child.material.color.setHex(0xff0000);
setTimeout(() => {
if (child.material) child.material.color.copy(child.userData.originalColor);
}, 150);
}
});
const hitRing = new THREE.Mesh(hitRingGeom, hitRingMat);
hitRing.rotation.x = -Math.PI / 2;
// Align with floor
const h = 1.56;
hitRing.position.y = -h / 2 + 0.05;
hitRing.name = "HitRing";
mesh.add(hitRing);
// Remove Red Halo after 1200ms (matching the timing in MonsterAI)
setTimeout(() => {
if (mesh && hitRing) mesh.remove(hitRing);
}, 1200);
// 2. Shake Animation (800ms)
const originalPos = mesh.position.clone();
@@ -297,6 +292,43 @@ export class GameRenderer {
};
}
showFloatingText(x, y, text, color = "#ffffff") {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 128; // Rectangular
const ctx = canvas.getContext('2d');
ctx.font = "bold 60px Arial";
ctx.textAlign = "center";
ctx.lineWidth = 4;
ctx.strokeStyle = "black";
ctx.strokeText(text, 128, 64);
ctx.fillStyle = color;
ctx.fillText(text, 128, 64);
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.SpriteMaterial({ map: texture, transparent: true });
const sprite = new THREE.Sprite(material);
// Position slightly above head (standard height ~1.5)
sprite.position.set(x, 2.0, -y);
// Small initial random offset for stacking readability
sprite.position.x += (Math.random() - 0.5) * 0.2;
// Scale down to world units
sprite.scale.set(2, 1, 1);
sprite.userData = {
startTime: performance.now(),
duration: 2000, // 2 seconds life
startY: sprite.position.y
};
this.floatingTextGroup.add(sprite);
}
triggerDeathAnimation(entityId) {
const mesh = this.entities.get(entityId);
if (!mesh) return;
@@ -474,6 +506,25 @@ export class GameRenderer {
}
});
// Update Floating Texts
const now = time;
for (let i = this.floatingTextGroup.children.length - 1; i >= 0; i--) {
const sprite = this.floatingTextGroup.children[i];
const elapsed = now - sprite.userData.startTime;
const progress = elapsed / sprite.userData.duration;
if (progress >= 1) {
this.floatingTextGroup.remove(sprite);
} else {
// Float Up
sprite.position.y = sprite.userData.startY + (progress * 1.5);
// Fade Out in last half
if (progress > 0.5) {
sprite.material.opacity = 1 - ((progress - 0.5) * 2);
}
}
}
// Handle Footsteps Audio Globally
if (window.SOUND_MANAGER) {
if (isAnyMoving) {