@ -2691,6 +2691,32 @@
top: 0;
top: 0;
left: 0;
left: 0;
}
}
/* Player attack animation - rubber band toward monster (right side) */
@keyframes playerAttack {
0% {
transform: translateX(0);
}
20% {
transform: translateX(-20px) scale(0.9);
}
50% {
transform: translateX(30px) scale(1.15);
}
70% {
transform: translateX(5px) scale(1.05);
}
100% {
transform: translateX(0) scale(1);
}
}
.player-entry.attacking {
border-color: #4ecdc4;
box-shadow: 0 0 15px rgba(78, 205, 196, 0.6);
background: rgba(78, 205, 196, 0.15);
}
.player-entry.attacking .player-entry-icon {
animation: playerAttack 0.5s ease-out;
}
.monster-side {
.monster-side {
flex: 1;
flex: 1;
max-width: 200px;
max-width: 200px;
@ -16190,6 +16216,192 @@
}
}
}
}
// Animate player attack - returns a Promise that resolves when animation completes
function animatePlayerAttack() {
return new Promise((resolve) => {
const playerEntry = document.querySelector('.player-entry');
if (!playerEntry) {
resolve();
return;
}
// Add attacking class to trigger animation
playerEntry.classList.add('attacking');
// Remove class after animation completes
setTimeout(() => {
playerEntry.classList.remove('attacking');
resolve();
}, 500);
});
}
// Scroll to a monster and animate receiving damage - returns Promise
function scrollToMonsterAndAnimate(monsterIndex, animationType = 'attack') {
return new Promise((resolve) => {
const container = document.getElementById('monsterList');
if (!container) {
resolve();
return;
}
const entries = container.querySelectorAll('.monster-entry');
const entry = entries[monsterIndex];
if (!entry) {
resolve();
return;
}
// Scroll the monster into view smoothly
entry.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
// Highlight the entry being attacked
entry.classList.add('selected');
// Wait for scroll, then animate
setTimeout(() => {
const icon = entry.querySelector('.monster-entry-icon');
if (icon) {
// Simple hit reaction animation
icon.style.animation = 'none';
icon.offsetHeight; // Force reflow
icon.style.animation = 'monsterAttack 0.4s ease-out reverse';
setTimeout(() => {
icon.style.animation = '';
resolve();
}, 400);
} else {
resolve();
}
}, 300);
});
}
// Execute player skill with full animation sequence
async function executeAnimatedPlayerSkill(skillId, targets, skill, displayName, rawSkill) {
const hitCount = skill.hitCount || skill.hits || 1;
const skillAccuracy = rawSkill ? (rawSkill.accuracy || 95) : 95;
const skillTarget = rawSkill ? rawSkill.target : (skill.target || 'enemy');
let rawDamage;
if (rawSkill & & rawSkill.calculate) {
rawDamage = rawSkill.calculate(combatState.player.atk);
} else if (rawSkill & & rawSkill.basePower) {
rawDamage = Math.floor(combatState.player.atk * (rawSkill.basePower / 100));
} else {
rawDamage = combatState.player.atk;
}
let grandTotalDamage = 0;
let monstersHit = 0;
let monstersKilled = 0;
// For each target (or each hit for multi-hit skills)
for (let i = 0; i < targets.length ; i + + ) {
const targetIndex = targets[i];
const currentTarget = combatState.monsters[targetIndex];
// Skip if already dead
if (currentTarget.hp < = 0) {
if (targets.length > 1) {
addCombatLog(`💨 ${currentTarget.data.name} already defeated!`, 'info');
}
continue;
}
// Animate player attacking
await animatePlayerAttack();
// Scroll to and highlight the target
await scrollToMonsterAndAnimate(targetIndex);
// Calculate hit
const hitChance = calculateHitChance(
combatState.player.accuracy,
currentTarget.dodge,
skillAccuracy
);
if (!rollHit(hitChance)) {
addCombatLog(`❌ ${displayName} missed ${currentTarget.data.name}! (${hitChance}% chance)`, 'miss');
playSfx('missed');
// Brief pause before next attack
await new Promise(r => setTimeout(r, 300));
continue;
}
monstersHit++;
// Calculate effective defense
let effectiveMonsterDef = currentTarget.def;
if (currentTarget.buffs & & currentTarget.buffs.defense & & currentTarget.buffs.defense.turnsLeft > 0) {
const buffPercent = currentTarget.buffs.defense.percent || 50;
effectiveMonsterDef = Math.floor(currentTarget.def * (1 + buffPercent / 100));
}
// For same_target hitCount, apply all hits at once
const hitsOnThisTarget = (skillTarget !== 'all_enemies' & & targets.length === 1) ? hitCount : 1;
let totalDamage = 0;
for (let hit = 0; hit < hitsOnThisTarget ; hit + + ) {
const baseDamage = calculateDamage(rawDamage, effectiveMonsterDef);
const damage = applyDamageVariance(baseDamage);
totalDamage += damage;
currentTarget.hp -= damage;
}
grandTotalDamage += totalDamage;
// Log the hit
if (hitsOnThisTarget > 1) {
addCombatLog(`✨ ${displayName} hits ${currentTarget.data.name} ${hitsOnThisTarget} times for ${totalDamage} total damage!`, 'damage');
playSfx('player_skill');
} else {
addCombatLog(`⚔️ ${displayName} hits ${currentTarget.data.name} for ${totalDamage} damage!`, 'damage');
playSfx('player_attack');
}
// Check if killed
if (currentTarget.hp < = 0) {
monstersKilled++;
recordMonsterKill(currentTarget);
animateMonsterAttack(targetIndex, 'death');
playSfx('monster_death');
const xpReward = (currentTarget.data?.xpReward || 10) * currentTarget.level;
playerStats.xp += xpReward;
combatState.player.xpGained = (combatState.player.xpGained || 0) + xpReward;
addCombatLog(`💀 ${currentTarget.data.name} was defeated! +${xpReward} XP`, 'victory');
removeMonster(currentTarget.id);
checkLevelUp();
savePlayerStats();
updateRpgHud();
}
// Update UI after each hit
updateCombatUI();
renderMonsterList();
// Brief pause between targets
if (i < targets.length - 1 ) {
await new Promise(r => setTimeout(r, 400));
}
}
// Summary for multi-target skills
if (targets.length > 1 & & skillTarget === 'all_enemies') {
if (monstersHit === 0) {
addCombatLog(`❌ ${displayName} missed all enemies!`, 'miss');
} else {
addCombatLog(`🌟 ${displayName} complete: ${monstersHit} enemies hit for ${grandTotalDamage} total damage!`, 'damage');
}
}
return { monstersHit, grandTotalDamage, monstersKilled };
}
// Render the monster list in combat UI
// Render the monster list in combat UI
function renderMonsterList() {
function renderMonsterList() {
const container = document.getElementById('monsterList');
const container = document.getElementById('monsterList');
@ -16326,8 +16538,8 @@
renderMonsterList();
renderMonsterList();
}
}
// Execute multi-hit skill with selected targets
function executeMultiHitSkill() {
// Execute multi-hit skill with selected targets (animated version)
async function executeMultiHitSkill() {
if (!combatState || !combatState.targetingMode || !combatState.pendingSkill) return;
if (!combatState || !combatState.targetingMode || !combatState.pendingSkill) return;
const skill = combatState.pendingSkill;
const skill = combatState.pendingSkill;
@ -16352,19 +16564,24 @@
let grandTotalDamage = 0;
let grandTotalDamage = 0;
let hitsLanded = 0;
let hitsLanded = 0;
let monstersKilled = 0;
let monstersKilled = 0;
const hitResults = []; // Track results for each hit
// Process each hit with its selected target
// Process each hit with animation
for (let hitNum = 0; hitNum < targetIndices.length ; hitNum + + ) {
for (let hitNum = 0; hitNum < targetIndices.length ; hitNum + + ) {
const targetIndex = targetIndices[hitNum];
const targetIndex = targetIndices[hitNum];
const currentTarget = combatState.monsters[targetIndex];
const currentTarget = combatState.monsters[targetIndex];
// Skip if target is already dead (from previous hit)
// Skip if target is already dead (from previous hit)
if (currentTarget.hp < = 0) {
if (currentTarget.hp < = 0) {
hitResults.push({ hit: hitNum + 1, target: currentTarget.data.name, result: 'already defeated' } );
addCombatLog(`💨 Hit ${hitNum + 1}: ${currentTarget.data.name} already defeated!`, 'info' );
continue;
continue;
}
}
// Animate player attacking
await animatePlayerAttack();
// Scroll to and highlight the target monster
await scrollToMonsterAndAnimate(targetIndex);
// Calculate hit chance
// Calculate hit chance
const hitChance = calculateHitChance(
const hitChance = calculateHitChance(
combatState.player.accuracy,
combatState.player.accuracy,
@ -16374,7 +16591,9 @@
// Roll for hit
// Roll for hit
if (!rollHit(hitChance)) {
if (!rollHit(hitChance)) {
hitResults.push({ hit: hitNum + 1, target: currentTarget.data.name, result: 'miss', hitChance });
addCombatLog(`❌ Hit ${hitNum + 1}: Missed ${currentTarget.data.name}! (${hitChance}% chance)`, 'miss');
playSfx('missed');
await new Promise(r => setTimeout(r, 300));
continue;
continue;
}
}
@ -16392,45 +16611,34 @@
grandTotalDamage += damage;
grandTotalDamage += damage;
hitsLanded++;
hitsLanded++;
hitResults.push({ hit: hitNum + 1, target: currentTarget.data.name, result: 'hit', damage });
addCombatLog(`⚔️ Hit ${hitNum + 1}: ${damage} damage to ${currentTarget.data.name}!`, 'damage');
playSfx('player_attack');
// Check if this killed the monster
// Check if this killed the monster
if (currentTarget.hp < = 0) {
if (currentTarget.hp < = 0) {
monstersKilled++;
monstersKilled++;
recordMonsterKill(currentTarget); // Track kill for bestiary
// Play death animation
recordMonsterKill(currentTarget);
animateMonsterAttack(targetIndex, 'death');
animateMonsterAttack(targetIndex, 'death');
playSfx('monster_death');
playSfx('monster_death');
// Award XP immediately
const xpReward = (currentTarget.data?.xpReward || 10) * currentTarget.level;
const xpReward = (currentTarget.data?.xpReward || 10) * currentTarget.level;
playerStats.xp += xpReward;
playerStats.xp += xpReward;
combatState.player.xpGained = (combatState.player.xpGained || 0) + xpReward;
combatState.player.xpGained = (combatState.player.xpGained || 0) + xpReward;
hitResults[hitResults.length - 1].killed = true;
hitResults[hitResults.length - 1].xp = xpReward;
addCombatLog(`💀 ${currentTarget.data.name} defeated! +${xpReward} XP`, 'victory');
removeMonster(currentTarget.id);
removeMonster(currentTarget.id);
checkLevelUp();
checkLevelUp();
savePlayerStats();
savePlayerStats();
updateRpgHud();
updateRpgHud();
}
}
}
// Log results for each hit
hitResults.forEach(r => {
if (r.result === 'miss') {
addCombatLog(`❌ Hit ${r.hit}: Missed ${r.target}! (${r.hitChance}% chance)`, 'miss');
playSfx('missed');
} else if (r.result === 'already defeated') {
addCombatLog(`💨 Hit ${r.hit}: ${r.target} already defeated!`, 'info');
} else if (r.result === 'hit') {
if (r.killed) {
addCombatLog(`⚔️ Hit ${r.hit}: ${r.damage} damage to ${r.target}!`, 'damage');
addCombatLog(`💀 ${r.target} defeated! +${r.xp} XP`, 'victory');
} else {
addCombatLog(`⚔️ Hit ${r.hit}: ${r.damage} damage to ${r.target}!`, 'damage');
// Update UI after each hit
updateCombatUI();
renderMonsterList();
// Brief pause between hits
if (hitNum < targetIndices.length - 1 ) {
await new Promise(r => setTimeout(r, 400));
}
}
playSfx('player_attack');
}
}
});
// Summary
// Summary
if (hitsLanded > 0) {
if (hitsLanded > 0) {
@ -16581,8 +16789,8 @@
return true;
return true;
}
}
// Execute a player skill
function executePlayerSkill(skillId) {
// Execute a player skill (async for animations)
async function executePlayerSkill(skillId) {
console.log('[DEBUG] executePlayerSkill called with:', skillId);
console.log('[DEBUG] executePlayerSkill called with:', skillId);
if (!combatState || combatState.turn !== 'player') {
if (!combatState || combatState.turn !== 'player') {
console.log('[DEBUG] Early return - combatState:', !!combatState, 'turn:', combatState?.turn);
console.log('[DEBUG] Early return - combatState:', !!combatState, 'turn:', combatState?.turn);
@ -16692,6 +16900,29 @@
// Determine targets based on skill.target (reuse variables from above)
// Determine targets based on skill.target (reuse variables from above)
const targets = skillTarget === 'all_enemies' ? livingMonsters : [target];
const targets = skillTarget === 'all_enemies' ? livingMonsters : [target];
// Animate player attack
await animatePlayerAttack();
// For all_enemies skills with multiple targets, animate each hit sequentially
if (skillTarget === 'all_enemies' & & targets.length > 1) {
const targetIndices = targets.map(t => combatState.monsters.indexOf(t));
await executeAnimatedPlayerSkill(skillId, targetIndices, skill, displayName, dbSkill || hardcodedSkill);
// Check victory and end turn
const remainingMonsters = combatState.monsters.filter(m => m.hp > 0);
if (remainingMonsters.length === 0) {
handleCombatVictory();
return;
}
endPlayerTurn();
return;
}
// Scroll to the target monster for single-target attacks
if (targets.length === 1) {
await scrollToMonsterAndAnimate(combatState.selectedTargetIndex);
}
// Calculate base damage - support both old calculate() and new basePower
// Calculate base damage - support both old calculate() and new basePower
let rawDamage;
let rawDamage;
if (hardcodedSkill & & hardcodedSkill.calculate) {
if (hardcodedSkill & & hardcodedSkill.calculate) {