feat: Add floating combat text and damage flash feedback
This commit is contained in:
28
src/main.js
28
src/main.js
@@ -103,6 +103,34 @@ game.turnManager.on('phase_changed', (phase) => {
|
|||||||
|
|
||||||
game.onCombatResult = (log) => {
|
game.onCombatResult = (log) => {
|
||||||
ui.showCombatLog(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) => {
|
game.onEntityMove = (entity, path) => {
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ export class GameRenderer {
|
|||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
|
||||||
this.scene.add(ambientLight);
|
this.scene.add(ambientLight);
|
||||||
|
|
||||||
|
// Group for floating texts
|
||||||
|
this.floatingTextGroup = new THREE.Group();
|
||||||
|
this.scene.add(this.floatingTextGroup);
|
||||||
|
|
||||||
// Directional Light (Sun/Moon - creates shadows)
|
// Directional Light (Sun/Moon - creates shadows)
|
||||||
const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
|
const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);
|
||||||
dirLight.position.set(50, 100, 50);
|
dirLight.position.set(50, 100, 50);
|
||||||
@@ -260,29 +264,20 @@ export class GameRenderer {
|
|||||||
const mesh = this.entities.get(entityId);
|
const mesh = this.entities.get(entityId);
|
||||||
if (!mesh) return;
|
if (!mesh) return;
|
||||||
|
|
||||||
// 1. Red Halo (Temporary) - MATCHING ATTACKER SIZE (0.3 - 0.4)
|
// 1. Flash Effect (White Flash)
|
||||||
const hitRingGeom = new THREE.RingGeometry(0.3, 0.4, 32);
|
mesh.traverse((child) => {
|
||||||
const hitRingMat = new THREE.MeshBasicMaterial({
|
if (child.material && child.material.map) { // Texture mesh
|
||||||
color: 0xff0000,
|
// Store original color if not stored
|
||||||
side: THREE.DoubleSide,
|
if (!child.userData.originalColor) {
|
||||||
transparent: true,
|
child.userData.originalColor = child.material.color.clone();
|
||||||
opacity: 0.9
|
}
|
||||||
});
|
// Set to red/white flash
|
||||||
const hitRing = new THREE.Mesh(hitRingGeom, hitRingMat);
|
child.material.color.setHex(0xff0000);
|
||||||
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(() => {
|
setTimeout(() => {
|
||||||
if (mesh && hitRing) mesh.remove(hitRing);
|
if (child.material) child.material.color.copy(child.userData.originalColor);
|
||||||
}, 1200);
|
}, 150);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 2. Shake Animation (800ms)
|
// 2. Shake Animation (800ms)
|
||||||
const originalPos = mesh.position.clone();
|
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) {
|
triggerDeathAnimation(entityId) {
|
||||||
const mesh = this.entities.get(entityId);
|
const mesh = this.entities.get(entityId);
|
||||||
if (!mesh) return;
|
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
|
// Handle Footsteps Audio Globally
|
||||||
if (window.SOUND_MANAGER) {
|
if (window.SOUND_MANAGER) {
|
||||||
if (isAnyMoving) {
|
if (isAnyMoving) {
|
||||||
|
|||||||
Reference in New Issue
Block a user