|
|
@ -1595,6 +1595,43 @@ |
|
|
.char-sheet-skill.locked .skill-name { |
|
|
.char-sheet-skill.locked .skill-name { |
|
|
color: #666; |
|
|
color: #666; |
|
|
} |
|
|
} |
|
|
|
|
|
.char-sheet-monster-count { |
|
|
|
|
|
font-size: 16px; |
|
|
|
|
|
color: #ffd93d; |
|
|
|
|
|
margin-bottom: 10px; |
|
|
|
|
|
} |
|
|
|
|
|
.char-sheet-monster-list { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
flex-direction: column; |
|
|
|
|
|
gap: 8px; |
|
|
|
|
|
} |
|
|
|
|
|
.char-sheet-monster { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
gap: 10px; |
|
|
|
|
|
padding: 8px; |
|
|
|
|
|
background: rgba(0,0,0,0.2); |
|
|
|
|
|
border-radius: 8px; |
|
|
|
|
|
} |
|
|
|
|
|
.char-sheet-monster .monster-thumb { |
|
|
|
|
|
width: 32px; |
|
|
|
|
|
height: 32px; |
|
|
|
|
|
object-fit: contain; |
|
|
|
|
|
} |
|
|
|
|
|
.char-sheet-monster .monster-info { |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
font-size: 13px; |
|
|
|
|
|
color: #fff; |
|
|
|
|
|
} |
|
|
|
|
|
.char-sheet-monster .monster-hp { |
|
|
|
|
|
font-size: 11px; |
|
|
|
|
|
color: #ff6b6b; |
|
|
|
|
|
} |
|
|
|
|
|
.char-sheet-no-monsters { |
|
|
|
|
|
color: #666; |
|
|
|
|
|
font-style: italic; |
|
|
|
|
|
font-size: 13px; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/* Skill Choice Modal */ |
|
|
/* Skill Choice Modal */ |
|
|
.skill-choice-modal { |
|
|
.skill-choice-modal { |
|
|
@ -1956,63 +1993,59 @@ |
|
|
top: 10px; |
|
|
top: 10px; |
|
|
left: 50%; |
|
|
left: 50%; |
|
|
transform: translateX(-50%); |
|
|
transform: translateX(-50%); |
|
|
background: rgba(0, 0, 0, 0.85); |
|
|
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.9); |
|
|
color: white; |
|
|
color: white; |
|
|
padding: 10px 20px; |
|
|
|
|
|
border-radius: 25px; |
|
|
|
|
|
font-size: 13px; |
|
|
|
|
|
|
|
|
padding: 8px 12px; |
|
|
|
|
|
border-radius: 12px; |
|
|
|
|
|
font-size: 11px; |
|
|
z-index: 1000; |
|
|
z-index: 1000; |
|
|
display: flex; |
|
|
display: flex; |
|
|
gap: 18px; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
|
|
|
flex-direction: column; |
|
|
|
|
|
gap: 4px; |
|
|
border: 2px solid #e94560; |
|
|
border: 2px solid #e94560; |
|
|
box-shadow: 0 4px 15px rgba(233, 69, 96, 0.3); |
|
|
box-shadow: 0 4px 15px rgba(233, 69, 96, 0.3); |
|
|
|
|
|
cursor: pointer; |
|
|
|
|
|
min-width: 140px; |
|
|
} |
|
|
} |
|
|
.rpg-hud-class { |
|
|
|
|
|
font-weight: bold; |
|
|
|
|
|
color: #e94560; |
|
|
|
|
|
} |
|
|
|
|
|
.rpg-hud-stats { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
gap: 12px; |
|
|
|
|
|
} |
|
|
|
|
|
.rpg-hud-stat { |
|
|
|
|
|
|
|
|
.rpg-hud-bar { |
|
|
display: flex; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
align-items: center; |
|
|
gap: 4px; |
|
|
|
|
|
|
|
|
gap: 6px; |
|
|
} |
|
|
} |
|
|
.rpg-hud-stat-label { |
|
|
|
|
|
|
|
|
.rpg-hud-bar-label { |
|
|
color: #888; |
|
|
color: #888; |
|
|
font-size: 11px; |
|
|
|
|
|
} |
|
|
|
|
|
.rpg-hud-hp { color: #ff6b6b; } |
|
|
|
|
|
.rpg-hud-mp { color: #4ecdc4; } |
|
|
|
|
|
.rpg-hud-xp { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
gap: 6px; |
|
|
|
|
|
|
|
|
font-size: 10px; |
|
|
|
|
|
font-weight: bold; |
|
|
|
|
|
width: 20px; |
|
|
|
|
|
text-align: right; |
|
|
} |
|
|
} |
|
|
.rpg-hud-xp-bar { |
|
|
|
|
|
width: 60px; |
|
|
|
|
|
height: 8px; |
|
|
|
|
|
background: #333; |
|
|
|
|
|
border-radius: 4px; |
|
|
|
|
|
|
|
|
.rpg-hud-bar-track { |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
height: 10px; |
|
|
|
|
|
background: #222; |
|
|
|
|
|
border-radius: 5px; |
|
|
overflow: hidden; |
|
|
overflow: hidden; |
|
|
border: 1px solid #555; |
|
|
|
|
|
|
|
|
border: 1px solid #444; |
|
|
} |
|
|
} |
|
|
.rpg-hud-xp-fill { |
|
|
|
|
|
|
|
|
.rpg-hud-bar-fill { |
|
|
height: 100%; |
|
|
height: 100%; |
|
|
background: linear-gradient(90deg, #ffd93d, #f0c419); |
|
|
|
|
|
transition: width 0.3s ease; |
|
|
transition: width 0.3s ease; |
|
|
border-radius: 3px; |
|
|
|
|
|
|
|
|
border-radius: 4px; |
|
|
} |
|
|
} |
|
|
.rpg-hud-xp-text { |
|
|
|
|
|
color: #ffd93d; |
|
|
|
|
|
font-size: 10px; |
|
|
|
|
|
min-width: 45px; |
|
|
|
|
|
|
|
|
.rpg-hud-bar-fill.hp-fill { |
|
|
|
|
|
background: linear-gradient(90deg, #ff6b6b, #ee5a5a); |
|
|
} |
|
|
} |
|
|
.rpg-hud-monsters { |
|
|
|
|
|
color: #ffd93d; |
|
|
|
|
|
|
|
|
.rpg-hud-bar-fill.mp-fill { |
|
|
|
|
|
background: linear-gradient(90deg, #4ecdc4, #3dbdb5); |
|
|
|
|
|
} |
|
|
|
|
|
.rpg-hud-bar-fill.xp-fill { |
|
|
|
|
|
background: linear-gradient(90deg, #ffd93d, #f0c419); |
|
|
|
|
|
} |
|
|
|
|
|
.rpg-hud-bar-text { |
|
|
|
|
|
font-size: 9px; |
|
|
|
|
|
min-width: 42px; |
|
|
|
|
|
text-align: right; |
|
|
|
|
|
color: #aaa; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/* Combat Overlay Styles */ |
|
|
/* Combat Overlay Styles */ |
|
|
@ -2142,6 +2175,13 @@ |
|
|
color: #ffd93d; |
|
|
color: #ffd93d; |
|
|
font-weight: bold; |
|
|
font-weight: bold; |
|
|
} |
|
|
} |
|
|
|
|
|
.combat-log-miss { |
|
|
|
|
|
color: #888; |
|
|
|
|
|
font-style: italic; |
|
|
|
|
|
} |
|
|
|
|
|
.combat-log-buff { |
|
|
|
|
|
color: #a8e6cf; |
|
|
|
|
|
} |
|
|
.combat-skills { |
|
|
.combat-skills { |
|
|
display: grid; |
|
|
display: grid; |
|
|
grid-template-columns: repeat(2, 1fr); |
|
|
grid-template-columns: repeat(2, 1fr); |
|
|
@ -2312,31 +2352,27 @@ |
|
|
<div id="compassIndicator" class="compass-indicator">N</div> |
|
|
<div id="compassIndicator" class="compass-indicator">N</div> |
|
|
|
|
|
|
|
|
<!-- RPG HUD (shown when player has class) --> |
|
|
<!-- RPG HUD (shown when player has class) --> |
|
|
<div id="rpgHud" class="rpg-hud" style="display: none;"> |
|
|
|
|
|
<div class="rpg-hud-class" style="cursor: pointer;" onclick="showCharacterSheet()" title="View Character Sheet">🏃 <span id="hudClassName">Trail Runner</span></div> |
|
|
|
|
|
<div class="rpg-hud-stats"> |
|
|
|
|
|
<div class="rpg-hud-stat"> |
|
|
|
|
|
<span class="rpg-hud-stat-label">Lv</span> |
|
|
|
|
|
<span id="hudLevel" class="rpg-hud-hp">1</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="rpg-hud-xp"> |
|
|
|
|
|
<span class="rpg-hud-stat-label">XP</span> |
|
|
|
|
|
<div class="rpg-hud-xp-bar"> |
|
|
|
|
|
<div class="rpg-hud-xp-fill" id="hudXpBar" style="width: 0%;"></div> |
|
|
|
|
|
|
|
|
<div id="rpgHud" class="rpg-hud" style="display: none;" onclick="showCharacterSheet()" title="Tap for Character Sheet"> |
|
|
|
|
|
<div class="rpg-hud-bar"> |
|
|
|
|
|
<span class="rpg-hud-bar-label">HP</span> |
|
|
|
|
|
<div class="rpg-hud-bar-track hp-track"> |
|
|
|
|
|
<div class="rpg-hud-bar-fill hp-fill" id="hudHpBar" style="width: 100%;"></div> |
|
|
</div> |
|
|
</div> |
|
|
<span class="rpg-hud-xp-text" id="hudXpText">0/100</span> |
|
|
|
|
|
|
|
|
<span class="rpg-hud-bar-text" id="hudHp">100/100</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="rpg-hud-stat"> |
|
|
|
|
|
<span class="rpg-hud-stat-label">HP</span> |
|
|
|
|
|
<span id="hudHp" class="rpg-hud-hp">100/100</span> |
|
|
|
|
|
|
|
|
<div class="rpg-hud-bar"> |
|
|
|
|
|
<span class="rpg-hud-bar-label">MP</span> |
|
|
|
|
|
<div class="rpg-hud-bar-track mp-track"> |
|
|
|
|
|
<div class="rpg-hud-bar-fill mp-fill" id="hudMpBar" style="width: 100%;"></div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="rpg-hud-stat"> |
|
|
|
|
|
<span class="rpg-hud-stat-label">MP</span> |
|
|
|
|
|
<span id="hudMp" class="rpg-hud-mp">50/50</span> |
|
|
|
|
|
|
|
|
<span class="rpg-hud-bar-text" id="hudMp">50/50</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="rpg-hud-bar"> |
|
|
|
|
|
<span class="rpg-hud-bar-label">XP</span> |
|
|
|
|
|
<div class="rpg-hud-bar-track xp-track"> |
|
|
|
|
|
<div class="rpg-hud-bar-fill xp-fill" id="hudXpBar" style="width: 0%;"></div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="rpg-hud-monsters"> |
|
|
|
|
|
👹 <span id="hudMonsterCount">0</span>/<span id="hudMonsterMax">2</span> |
|
|
|
|
|
|
|
|
<span class="rpg-hud-bar-text" id="hudXpText">0/100</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
@ -2699,6 +2735,12 @@ |
|
|
<!-- Populated by JS --> |
|
|
<!-- Populated by JS --> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="char-sheet-section"> |
|
|
|
|
|
<h3>👹 Monsters</h3> |
|
|
|
|
|
<div class="char-sheet-monsters" id="charSheetMonsters"> |
|
|
|
|
|
<!-- Populated by JS --> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
@ -3312,6 +3354,15 @@ |
|
|
calculate: (atk) => Math.floor(atk * 2), |
|
|
calculate: (atk) => Math.floor(atk * 2), |
|
|
hits: 3, |
|
|
hits: 3, |
|
|
description: 'Strike 3 times for 2x ATK each' |
|
|
description: 'Strike 3 times for 2x ATK each' |
|
|
|
|
|
}, |
|
|
|
|
|
'admin_banish': { |
|
|
|
|
|
name: 'Banish All', |
|
|
|
|
|
icon: '⚡', |
|
|
|
|
|
mpCost: 0, |
|
|
|
|
|
levelReq: 1, |
|
|
|
|
|
type: 'admin_clear', |
|
|
|
|
|
adminOnly: true, |
|
|
|
|
|
description: 'Instantly banish all enemies (Admin only)' |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
@ -3329,6 +3380,12 @@ |
|
|
let MONSTER_DIALOGUES = {}; |
|
|
let MONSTER_DIALOGUES = {}; |
|
|
let monsterTypesLoaded = false; |
|
|
let monsterTypesLoaded = false; |
|
|
|
|
|
|
|
|
|
|
|
// Skills loaded from database API |
|
|
|
|
|
let SKILLS_DB = {}; // Base skill definitions from API |
|
|
|
|
|
let CLASS_SKILL_NAMES = []; // Class-specific skill names |
|
|
|
|
|
let MONSTER_SKILLS = {}; // Skills assigned to each monster type |
|
|
|
|
|
let skillsLoaded = false; |
|
|
|
|
|
|
|
|
// Load monster types from the database |
|
|
// Load monster types from the database |
|
|
async function loadMonsterTypes() { |
|
|
async function loadMonsterTypes() { |
|
|
try { |
|
|
try { |
|
|
@ -3343,6 +3400,8 @@ |
|
|
baseAtk: t.baseAtk, |
|
|
baseAtk: t.baseAtk, |
|
|
baseDef: t.baseDef, |
|
|
baseDef: t.baseDef, |
|
|
xpReward: t.xpReward, |
|
|
xpReward: t.xpReward, |
|
|
|
|
|
accuracy: t.accuracy || 85, |
|
|
|
|
|
dodge: t.dodge || 5, |
|
|
levelScale: t.levelScale |
|
|
levelScale: t.levelScale |
|
|
}; |
|
|
}; |
|
|
MONSTER_DIALOGUES[t.id] = t.dialogues; |
|
|
MONSTER_DIALOGUES[t.id] = t.dialogues; |
|
|
@ -3355,6 +3414,134 @@ |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Load skills from database |
|
|
|
|
|
async function loadSkillsFromDatabase() { |
|
|
|
|
|
try { |
|
|
|
|
|
// Load base skills |
|
|
|
|
|
const skillsResponse = await fetch('/api/skills'); |
|
|
|
|
|
if (skillsResponse.ok) { |
|
|
|
|
|
const skills = await skillsResponse.json(); |
|
|
|
|
|
skills.forEach(s => { |
|
|
|
|
|
SKILLS_DB[s.id] = { |
|
|
|
|
|
id: s.id, |
|
|
|
|
|
name: s.name, |
|
|
|
|
|
description: s.description, |
|
|
|
|
|
type: s.type, |
|
|
|
|
|
mpCost: s.mpCost, |
|
|
|
|
|
basePower: s.basePower, |
|
|
|
|
|
accuracy: s.accuracy, |
|
|
|
|
|
hitCount: s.hitCount, |
|
|
|
|
|
target: s.target, |
|
|
|
|
|
statusEffect: s.statusEffect, |
|
|
|
|
|
playerUsable: s.playerUsable, |
|
|
|
|
|
monsterUsable: s.monsterUsable |
|
|
|
|
|
}; |
|
|
|
|
|
}); |
|
|
|
|
|
console.log('Loaded skills from database:', Object.keys(SKILLS_DB)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Load class skill names |
|
|
|
|
|
const namesResponse = await fetch('/api/class-skill-names'); |
|
|
|
|
|
if (namesResponse.ok) { |
|
|
|
|
|
CLASS_SKILL_NAMES = await namesResponse.json(); |
|
|
|
|
|
console.log('Loaded class skill names:', CLASS_SKILL_NAMES.length); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
skillsLoaded = true; |
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
console.error('Failed to load skills:', err); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Load skills for a specific monster type |
|
|
|
|
|
async function loadMonsterSkills(monsterTypeId) { |
|
|
|
|
|
if (MONSTER_SKILLS[monsterTypeId]) return MONSTER_SKILLS[monsterTypeId]; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
const response = await fetch(`/api/monster-types/${monsterTypeId}/skills`); |
|
|
|
|
|
if (response.ok) { |
|
|
|
|
|
MONSTER_SKILLS[monsterTypeId] = await response.json(); |
|
|
|
|
|
console.log(`Loaded skills for ${monsterTypeId}:`, MONSTER_SKILLS[monsterTypeId].length); |
|
|
|
|
|
return MONSTER_SKILLS[monsterTypeId]; |
|
|
|
|
|
} |
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
console.error(`Failed to load monster skills for ${monsterTypeId}:`, err); |
|
|
|
|
|
} |
|
|
|
|
|
return []; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Get skill display name for a class (or base name if no custom) |
|
|
|
|
|
function getSkillForClass(skillId, classId) { |
|
|
|
|
|
const baseSkill = SKILLS_DB[skillId] || SKILLS[skillId]; |
|
|
|
|
|
if (!baseSkill) return null; |
|
|
|
|
|
|
|
|
|
|
|
// Check for class-specific name |
|
|
|
|
|
const customName = CLASS_SKILL_NAMES.find( |
|
|
|
|
|
n => n.skillId === skillId && n.classId === classId |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
...baseSkill, |
|
|
|
|
|
displayName: customName ? customName.customName : baseSkill.name, |
|
|
|
|
|
displayDescription: customName?.customDescription || baseSkill.description |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Calculate hit chance: skill accuracy + (attacker accuracy - 90) - defender dodge |
|
|
|
|
|
function calculateHitChance(attackerAccuracy, defenderDodge, skillAccuracy) { |
|
|
|
|
|
const hitChance = skillAccuracy + (attackerAccuracy - 90) - defenderDodge; |
|
|
|
|
|
return Math.max(5, Math.min(99, hitChance)); // Clamp 5-99% |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Roll for hit |
|
|
|
|
|
function rollHit(hitChance) { |
|
|
|
|
|
return Math.random() * 100 < hitChance; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Select a monster skill using weighted random |
|
|
|
|
|
function selectMonsterSkill(monsterTypeId, monsterLevel) { |
|
|
|
|
|
const skills = MONSTER_SKILLS[monsterTypeId] || []; |
|
|
|
|
|
|
|
|
|
|
|
// Filter by level requirement |
|
|
|
|
|
const validSkills = skills.filter(s => monsterLevel >= s.minLevel); |
|
|
|
|
|
|
|
|
|
|
|
if (validSkills.length === 0) { |
|
|
|
|
|
// Fallback to basic attack |
|
|
|
|
|
return SKILLS_DB['basic_attack'] || { id: 'basic_attack', name: 'Attack', basePower: 100, accuracy: 95 }; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Weighted random selection |
|
|
|
|
|
const totalWeight = validSkills.reduce((sum, s) => sum + s.weight, 0); |
|
|
|
|
|
let random = Math.random() * totalWeight; |
|
|
|
|
|
|
|
|
|
|
|
for (const skill of validSkills) { |
|
|
|
|
|
random -= skill.weight; |
|
|
|
|
|
if (random <= 0) { |
|
|
|
|
|
return { |
|
|
|
|
|
id: skill.skillId, |
|
|
|
|
|
name: skill.name, |
|
|
|
|
|
basePower: skill.basePower, |
|
|
|
|
|
accuracy: skill.accuracy, |
|
|
|
|
|
hitCount: skill.hitCount || 1, |
|
|
|
|
|
statusEffect: skill.statusEffect, |
|
|
|
|
|
type: skill.type |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Fallback |
|
|
|
|
|
const lastSkill = validSkills[validSkills.length - 1]; |
|
|
|
|
|
return { |
|
|
|
|
|
id: lastSkill.skillId, |
|
|
|
|
|
name: lastSkill.name, |
|
|
|
|
|
basePower: lastSkill.basePower, |
|
|
|
|
|
accuracy: lastSkill.accuracy, |
|
|
|
|
|
hitCount: lastSkill.hitCount || 1, |
|
|
|
|
|
statusEffect: lastSkill.statusEffect, |
|
|
|
|
|
type: lastSkill.type |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Dialogue phase thresholds (in minutes) |
|
|
// Dialogue phase thresholds (in minutes) |
|
|
const DIALOGUE_PHASES = [ |
|
|
const DIALOGUE_PHASES = [ |
|
|
{ maxMinutes: 5, phase: 'annoyed' }, |
|
|
{ maxMinutes: 5, phase: 'annoyed' }, |
|
|
@ -5121,6 +5308,14 @@ |
|
|
console.log('Connected to multi-user tracking'); |
|
|
console.log('Connected to multi-user tracking'); |
|
|
clearTimeout(wsReconnectTimer); |
|
|
clearTimeout(wsReconnectTimer); |
|
|
|
|
|
|
|
|
|
|
|
// Register authenticated user for real-time updates |
|
|
|
|
|
if (currentUser && currentUser.id) { |
|
|
|
|
|
ws.send(JSON.stringify({ |
|
|
|
|
|
type: 'auth', |
|
|
|
|
|
authUserId: currentUser.id |
|
|
|
|
|
})); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Send our icon info if we have it |
|
|
// Send our icon info if we have it |
|
|
if (myIcon && myColor) { |
|
|
if (myIcon && myColor) { |
|
|
setTimeout(() => { |
|
|
setTimeout(() => { |
|
|
@ -5241,6 +5436,12 @@ |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
case 'statsUpdated': |
|
|
|
|
|
// Admin updated our stats - refresh from server |
|
|
|
|
|
console.log('Stats updated by admin, refreshing...'); |
|
|
|
|
|
refreshPlayerStats(); |
|
|
|
|
|
break; |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
@ -8644,6 +8845,17 @@ |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Register authenticated user with WebSocket for real-time updates |
|
|
|
|
|
function registerWebSocketAuth() { |
|
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN && currentUser && currentUser.id) { |
|
|
|
|
|
ws.send(JSON.stringify({ |
|
|
|
|
|
type: 'auth', |
|
|
|
|
|
authUserId: currentUser.id |
|
|
|
|
|
})); |
|
|
|
|
|
console.log('Registered auth user', currentUser.id, 'with WebSocket'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
async function login(username, password) { |
|
|
async function login(username, password) { |
|
|
const response = await fetch('/api/login', { |
|
|
const response = await fetch('/api/login', { |
|
|
method: 'POST', |
|
|
method: 'POST', |
|
|
@ -8659,6 +8871,7 @@ |
|
|
localStorage.setItem('accessToken', accessToken); |
|
|
localStorage.setItem('accessToken', accessToken); |
|
|
localStorage.setItem('refreshToken', refreshToken); |
|
|
localStorage.setItem('refreshToken', refreshToken); |
|
|
updateAuthUI(); |
|
|
updateAuthUI(); |
|
|
|
|
|
registerWebSocketAuth(); |
|
|
// Initialize RPG system for eligible users |
|
|
// Initialize RPG system for eligible users |
|
|
await initializePlayerStats(currentUser.username); |
|
|
await initializePlayerStats(currentUser.username); |
|
|
return { success: true }; |
|
|
return { success: true }; |
|
|
@ -8683,6 +8896,7 @@ |
|
|
localStorage.setItem('accessToken', accessToken); |
|
|
localStorage.setItem('accessToken', accessToken); |
|
|
localStorage.setItem('refreshToken', refreshToken); |
|
|
localStorage.setItem('refreshToken', refreshToken); |
|
|
updateAuthUI(); |
|
|
updateAuthUI(); |
|
|
|
|
|
registerWebSocketAuth(); |
|
|
// Initialize RPG system for eligible users |
|
|
// Initialize RPG system for eligible users |
|
|
await initializePlayerStats(currentUser.username); |
|
|
await initializePlayerStats(currentUser.username); |
|
|
return { success: true }; |
|
|
return { success: true }; |
|
|
@ -8730,6 +8944,7 @@ |
|
|
if (response.ok) { |
|
|
if (response.ok) { |
|
|
currentUser = await response.json(); |
|
|
currentUser = await response.json(); |
|
|
updateAuthUI(); |
|
|
updateAuthUI(); |
|
|
|
|
|
registerWebSocketAuth(); |
|
|
// Initialize RPG system for eligible users |
|
|
// Initialize RPG system for eligible users |
|
|
await initializePlayerStats(currentUser.username); |
|
|
await initializePlayerStats(currentUser.username); |
|
|
} else { |
|
|
} else { |
|
|
@ -9134,6 +9349,28 @@ |
|
|
`; |
|
|
`; |
|
|
}).join(''); |
|
|
}).join(''); |
|
|
|
|
|
|
|
|
|
|
|
// Update monsters section |
|
|
|
|
|
const maxMonsters = getMaxMonsters(); |
|
|
|
|
|
const monsterCount = monsterEntourage.length; |
|
|
|
|
|
let monstersHtml = `<div class="char-sheet-monster-count">${monsterCount}/${maxMonsters} nearby</div>`; |
|
|
|
|
|
if (monsterCount > 0) { |
|
|
|
|
|
monstersHtml += '<div class="char-sheet-monster-list">'; |
|
|
|
|
|
monsterEntourage.forEach(m => { |
|
|
|
|
|
const type = MONSTER_TYPES[m.type] || { name: 'Unknown', icon: '👹' }; |
|
|
|
|
|
monstersHtml += ` |
|
|
|
|
|
<div class="char-sheet-monster"> |
|
|
|
|
|
<img src="/mapgameimgs/${m.type}50.png" onerror="this.src='/mapgameimgs/default50.png'" alt="${type.name}" class="monster-thumb"> |
|
|
|
|
|
<span class="monster-info">Lv${m.level} ${type.name}</span> |
|
|
|
|
|
<span class="monster-hp">${m.hp}/${m.maxHp} HP</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
`; |
|
|
|
|
|
}); |
|
|
|
|
|
monstersHtml += '</div>'; |
|
|
|
|
|
} else { |
|
|
|
|
|
monstersHtml += '<div class="char-sheet-no-monsters">No monsters nearby</div>'; |
|
|
|
|
|
} |
|
|
|
|
|
document.getElementById('charSheetMonsters').innerHTML = monstersHtml; |
|
|
|
|
|
|
|
|
document.getElementById('charSheetModal').style.display = 'flex'; |
|
|
document.getElementById('charSheetModal').style.display = 'flex'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -9411,6 +9648,29 @@ |
|
|
showCharCreatorModal(); |
|
|
showCharCreatorModal(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Refresh player stats from server (used when admin updates stats) |
|
|
|
|
|
async function refreshPlayerStats() { |
|
|
|
|
|
const token = localStorage.getItem('accessToken'); |
|
|
|
|
|
if (!token) return; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
const response = await fetch('/api/user/rpg-stats', { |
|
|
|
|
|
headers: { 'Authorization': `Bearer ${token}` } |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (response.ok) { |
|
|
|
|
|
const serverStats = await response.json(); |
|
|
|
|
|
if (serverStats && serverStats.name) { |
|
|
|
|
|
playerStats = serverStats; |
|
|
|
|
|
console.log('Refreshed RPG stats from server:', playerStats); |
|
|
|
|
|
updateRpgHud(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.error('Failed to refresh RPG stats:', e); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Save player stats to server (and localStorage as backup) |
|
|
// Save player stats to server (and localStorage as backup) |
|
|
function savePlayerStats() { |
|
|
function savePlayerStats() { |
|
|
if (!playerStats) return; |
|
|
if (!playerStats) return; |
|
|
@ -9436,11 +9696,15 @@ |
|
|
function updateRpgHud() { |
|
|
function updateRpgHud() { |
|
|
if (!playerStats) return; |
|
|
if (!playerStats) return; |
|
|
|
|
|
|
|
|
document.getElementById('hudLevel').textContent = playerStats.level; |
|
|
|
|
|
|
|
|
// Update HP bar |
|
|
|
|
|
const hpPercent = Math.min(100, (playerStats.hp / playerStats.maxHp) * 100); |
|
|
|
|
|
document.getElementById('hudHpBar').style.width = hpPercent + '%'; |
|
|
document.getElementById('hudHp').textContent = `${playerStats.hp}/${playerStats.maxHp}`; |
|
|
document.getElementById('hudHp').textContent = `${playerStats.hp}/${playerStats.maxHp}`; |
|
|
|
|
|
|
|
|
|
|
|
// Update MP bar |
|
|
|
|
|
const mpPercent = Math.min(100, (playerStats.mp / playerStats.maxMp) * 100); |
|
|
|
|
|
document.getElementById('hudMpBar').style.width = mpPercent + '%'; |
|
|
document.getElementById('hudMp').textContent = `${playerStats.mp}/${playerStats.maxMp}`; |
|
|
document.getElementById('hudMp').textContent = `${playerStats.mp}/${playerStats.maxMp}`; |
|
|
document.getElementById('hudMonsterCount').textContent = monsterEntourage.length; |
|
|
|
|
|
document.getElementById('hudMonsterMax').textContent = getMaxMonsters(); |
|
|
|
|
|
|
|
|
|
|
|
// Update XP bar |
|
|
// Update XP bar |
|
|
const xpNeeded = playerStats.level * 100; |
|
|
const xpNeeded = playerStats.level * 100; |
|
|
@ -9764,13 +10028,19 @@ |
|
|
// ========================================== |
|
|
// ========================================== |
|
|
|
|
|
|
|
|
// Initiate combat with a monster |
|
|
// Initiate combat with a monster |
|
|
function initiateCombat(clickedMonster) { |
|
|
|
|
|
|
|
|
async function initiateCombat(clickedMonster) { |
|
|
if (combatState) return; // Already in combat |
|
|
if (combatState) return; // Already in combat |
|
|
if (!playerStats) return; |
|
|
if (!playerStats) return; |
|
|
if (monsterEntourage.length === 0) return; |
|
|
if (monsterEntourage.length === 0) return; |
|
|
|
|
|
|
|
|
|
|
|
// Load skills for each unique monster type |
|
|
|
|
|
const uniqueTypes = [...new Set(monsterEntourage.map(m => m.type))]; |
|
|
|
|
|
await Promise.all(uniqueTypes.map(type => loadMonsterSkills(type))); |
|
|
|
|
|
|
|
|
// Gather ALL monsters from entourage for multi-monster combat |
|
|
// Gather ALL monsters from entourage for multi-monster combat |
|
|
const monstersInCombat = monsterEntourage.map(m => ({ |
|
|
|
|
|
|
|
|
const monstersInCombat = monsterEntourage.map(m => { |
|
|
|
|
|
const monsterType = MONSTER_TYPES[m.type]; |
|
|
|
|
|
return { |
|
|
id: m.id, |
|
|
id: m.id, |
|
|
type: m.type, |
|
|
type: m.type, |
|
|
level: m.level, |
|
|
level: m.level, |
|
|
@ -9778,8 +10048,11 @@ |
|
|
maxHp: m.maxHp, |
|
|
maxHp: m.maxHp, |
|
|
atk: m.atk, |
|
|
atk: m.atk, |
|
|
def: m.def, |
|
|
def: m.def, |
|
|
data: MONSTER_TYPES[m.type] |
|
|
|
|
|
})); |
|
|
|
|
|
|
|
|
accuracy: monsterType?.accuracy || 85, |
|
|
|
|
|
dodge: monsterType?.dodge || 5, |
|
|
|
|
|
data: monsterType |
|
|
|
|
|
}; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
// Find the clicked monster's index to make it the initial target |
|
|
// Find the clicked monster's index to make it the initial target |
|
|
const clickedIndex = monstersInCombat.findIndex(m => m.id === clickedMonster.id); |
|
|
const clickedIndex = monstersInCombat.findIndex(m => m.id === clickedMonster.id); |
|
|
@ -9791,13 +10064,17 @@ |
|
|
mp: playerStats.mp, |
|
|
mp: playerStats.mp, |
|
|
maxMp: playerStats.maxMp, |
|
|
maxMp: playerStats.maxMp, |
|
|
atk: playerStats.atk, |
|
|
atk: playerStats.atk, |
|
|
def: playerStats.def |
|
|
|
|
|
|
|
|
def: playerStats.def, |
|
|
|
|
|
accuracy: playerStats.accuracy || 90, |
|
|
|
|
|
dodge: playerStats.dodge || 10 |
|
|
}, |
|
|
}, |
|
|
monsters: monstersInCombat, |
|
|
monsters: monstersInCombat, |
|
|
selectedTargetIndex: clickedIndex >= 0 ? clickedIndex : 0, |
|
|
selectedTargetIndex: clickedIndex >= 0 ? clickedIndex : 0, |
|
|
turn: 'player', |
|
|
turn: 'player', |
|
|
currentMonsterTurn: 0, |
|
|
currentMonsterTurn: 0, |
|
|
log: [] |
|
|
|
|
|
|
|
|
log: [], |
|
|
|
|
|
playerStatusEffects: [], // [{type, damage, turnsLeft}] |
|
|
|
|
|
defenseBuffTurns: 0 // Turns remaining for defense buff |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
showCombatUI(); |
|
|
showCombatUI(); |
|
|
@ -9837,6 +10114,21 @@ |
|
|
skillsContainer.appendChild(btn); |
|
|
skillsContainer.appendChild(btn); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Add admin-only Banish All skill for admins |
|
|
|
|
|
if (currentUser && currentUser.is_admin) { |
|
|
|
|
|
const adminSkill = SKILLS['admin_banish']; |
|
|
|
|
|
const btn = document.createElement('button'); |
|
|
|
|
|
btn.className = 'skill-btn'; |
|
|
|
|
|
btn.dataset.skillId = 'admin_banish'; |
|
|
|
|
|
btn.style.borderColor = '#ff6b35'; |
|
|
|
|
|
btn.innerHTML = ` |
|
|
|
|
|
<span class="skill-name">${adminSkill.icon} ${adminSkill.name}</span> |
|
|
|
|
|
<span class="skill-cost free">Admin</span> |
|
|
|
|
|
`; |
|
|
|
|
|
btn.onclick = () => executePlayerSkill('admin_banish'); |
|
|
|
|
|
skillsContainer.appendChild(btn); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Set up flee button |
|
|
// Set up flee button |
|
|
document.getElementById('combatFleeBtn').onclick = fleeCombat; |
|
|
document.getElementById('combatFleeBtn').onclick = fleeCombat; |
|
|
|
|
|
|
|
|
@ -9948,11 +10240,67 @@ |
|
|
log.scrollTop = log.scrollHeight; |
|
|
log.scrollTop = log.scrollHeight; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Process status effects at start of player turn |
|
|
|
|
|
function processPlayerStatusEffects() { |
|
|
|
|
|
if (!combatState || combatState.playerStatusEffects.length === 0) return; |
|
|
|
|
|
|
|
|
|
|
|
let totalDamage = 0; |
|
|
|
|
|
const effectsToRemove = []; |
|
|
|
|
|
|
|
|
|
|
|
combatState.playerStatusEffects.forEach((effect, i) => { |
|
|
|
|
|
if (effect.type === 'poison') { |
|
|
|
|
|
totalDamage += effect.damage; |
|
|
|
|
|
addCombatLog(`☠️ Poison deals ${effect.damage} damage!`, 'damage'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
effect.turnsLeft--; |
|
|
|
|
|
if (effect.turnsLeft <= 0) { |
|
|
|
|
|
effectsToRemove.push(i); |
|
|
|
|
|
addCombatLog(`💨 ${effect.type.charAt(0).toUpperCase() + effect.type.slice(1)} wore off!`); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Remove expired effects (in reverse order to preserve indices) |
|
|
|
|
|
effectsToRemove.reverse().forEach(i => { |
|
|
|
|
|
combatState.playerStatusEffects.splice(i, 1); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (totalDamage > 0) { |
|
|
|
|
|
combatState.player.hp -= totalDamage; |
|
|
|
|
|
updateCombatUI(); |
|
|
|
|
|
|
|
|
|
|
|
// Check for defeat from status damage |
|
|
|
|
|
if (combatState.player.hp <= 0) { |
|
|
|
|
|
handleCombatDefeat(); |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Decrement defense buff |
|
|
|
|
|
if (combatState.defenseBuffTurns > 0) { |
|
|
|
|
|
combatState.defenseBuffTurns--; |
|
|
|
|
|
if (combatState.defenseBuffTurns === 0) { |
|
|
|
|
|
addCombatLog(`🛡️ Defense buff expired!`); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Execute a player skill |
|
|
// Execute a player skill |
|
|
function executePlayerSkill(skillId) { |
|
|
function executePlayerSkill(skillId) { |
|
|
if (!combatState || combatState.turn !== 'player') return; |
|
|
if (!combatState || combatState.turn !== 'player') return; |
|
|
|
|
|
|
|
|
const skill = SKILLS[skillId]; |
|
|
|
|
|
|
|
|
// Get skill from DB first, then fall back to hardcoded SKILLS |
|
|
|
|
|
const dbSkill = SKILLS_DB[skillId]; |
|
|
|
|
|
const hardcodedSkill = SKILLS[skillId]; |
|
|
|
|
|
const skill = dbSkill || hardcodedSkill; |
|
|
|
|
|
|
|
|
|
|
|
if (!skill) { |
|
|
|
|
|
addCombatLog(`Unknown skill: ${skillId}`); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const levelReq = skill.levelReq || 1; |
|
|
const levelReq = skill.levelReq || 1; |
|
|
if (playerStats.level < levelReq) { |
|
|
if (playerStats.level < levelReq) { |
|
|
addCombatLog(`You need to be level ${levelReq} to use ${skill.name}!`); |
|
|
addCombatLog(`You need to be level ${levelReq} to use ${skill.name}!`); |
|
|
@ -9963,28 +10311,106 @@ |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Get class-specific display name |
|
|
|
|
|
const skillDisplay = getSkillForClass(skillId, playerStats.class) || skill; |
|
|
|
|
|
const displayName = skillDisplay.displayName || skill.name; |
|
|
|
|
|
|
|
|
// Deduct MP |
|
|
// Deduct MP |
|
|
combatState.player.mp -= skill.mpCost; |
|
|
combatState.player.mp -= skill.mpCost; |
|
|
|
|
|
|
|
|
// Get the targeted monster |
|
|
// Get the targeted monster |
|
|
const target = combatState.monsters[combatState.selectedTargetIndex]; |
|
|
const target = combatState.monsters[combatState.selectedTargetIndex]; |
|
|
|
|
|
|
|
|
if (skill.type === 'damage') { |
|
|
|
|
|
const rawDamage = skill.calculate(combatState.player.atk); |
|
|
|
|
|
|
|
|
if (skill.type === 'admin_clear') { |
|
|
|
|
|
// Admin-only: instantly defeat all monsters (no XP) |
|
|
|
|
|
if (!currentUser || !currentUser.is_admin) { |
|
|
|
|
|
addCombatLog('This skill requires admin privileges!'); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
const monsterCount = combatState.monsters.length; |
|
|
|
|
|
combatState.monsters.forEach(m => m.hp = 0); |
|
|
|
|
|
addCombatLog(`⚡ Admin Banish! All ${monsterCount} enemies vanished!`, 'victory'); |
|
|
|
|
|
updateCombatUI(); |
|
|
|
|
|
|
|
|
|
|
|
// End combat immediately (no XP awarded) |
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
const monsterIds = combatState.monsters.map(m => m.id); |
|
|
|
|
|
monsterIds.forEach(id => removeMonster(id)); |
|
|
|
|
|
playerStats.hp = combatState.player.hp; |
|
|
|
|
|
playerStats.mp = combatState.player.mp; |
|
|
|
|
|
savePlayerStats(); |
|
|
|
|
|
updateRpgHud(); |
|
|
|
|
|
closeCombatUI(); |
|
|
|
|
|
}, 1000); |
|
|
|
|
|
return; |
|
|
|
|
|
} else if (skill.type === 'damage') { |
|
|
|
|
|
// Calculate hit chance |
|
|
|
|
|
const skillAccuracy = dbSkill ? dbSkill.accuracy : 95; |
|
|
|
|
|
const hitChance = calculateHitChance( |
|
|
|
|
|
combatState.player.accuracy, |
|
|
|
|
|
target.dodge, |
|
|
|
|
|
skillAccuracy |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// Roll for hit |
|
|
|
|
|
if (!rollHit(hitChance)) { |
|
|
|
|
|
addCombatLog(`❌ ${displayName} missed ${target.data.name}! (${hitChance}% chance)`, 'miss'); |
|
|
|
|
|
endPlayerTurn(); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Calculate damage - support both old calculate() and new basePower |
|
|
|
|
|
let rawDamage; |
|
|
|
|
|
if (hardcodedSkill && hardcodedSkill.calculate) { |
|
|
|
|
|
rawDamage = hardcodedSkill.calculate(combatState.player.atk); |
|
|
|
|
|
} else if (dbSkill) { |
|
|
|
|
|
rawDamage = Math.floor(combatState.player.atk * (dbSkill.basePower / 100)); |
|
|
|
|
|
} else { |
|
|
|
|
|
rawDamage = combatState.player.atk; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Handle multi-hit skills |
|
|
|
|
|
const hitCount = skill.hitCount || skill.hits || 1; |
|
|
|
|
|
let totalDamage = 0; |
|
|
|
|
|
|
|
|
|
|
|
for (let hit = 0; hit < hitCount; hit++) { |
|
|
const damage = Math.max(1, rawDamage - target.def); |
|
|
const damage = Math.max(1, rawDamage - target.def); |
|
|
|
|
|
totalDamage += damage; |
|
|
target.hp -= damage; |
|
|
target.hp -= damage; |
|
|
addCombatLog(`You used ${skill.name} on ${target.data.name}! Dealt ${damage} damage!`, 'damage'); |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (hitCount > 1) { |
|
|
|
|
|
addCombatLog(`✨ ${displayName} hits ${target.data.name} ${hitCount} times for ${totalDamage} total damage!`, 'damage'); |
|
|
|
|
|
} else { |
|
|
|
|
|
addCombatLog(`⚔️ ${displayName} hits ${target.data.name} for ${totalDamage} damage!`, 'damage'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Check if this monster died |
|
|
// Check if this monster died |
|
|
if (target.hp <= 0) { |
|
|
if (target.hp <= 0) { |
|
|
addCombatLog(`${target.data.name} was defeated!`, 'victory'); |
|
|
|
|
|
// Auto-retarget to next living monster if available |
|
|
|
|
|
|
|
|
addCombatLog(`💀 ${target.data.name} was defeated!`, 'victory'); |
|
|
autoRetarget(); |
|
|
autoRetarget(); |
|
|
} |
|
|
} |
|
|
} else if (skill.type === 'heal') { |
|
|
} else if (skill.type === 'heal') { |
|
|
const healAmount = skill.calculate(combatState.player.maxHp); |
|
|
|
|
|
|
|
|
let healAmount; |
|
|
|
|
|
if (hardcodedSkill && hardcodedSkill.calculate) { |
|
|
|
|
|
healAmount = hardcodedSkill.calculate(combatState.player.maxHp); |
|
|
|
|
|
} else if (dbSkill) { |
|
|
|
|
|
healAmount = Math.floor(combatState.player.maxHp * (dbSkill.basePower / 100)); |
|
|
|
|
|
} else { |
|
|
|
|
|
healAmount = 30; |
|
|
|
|
|
} |
|
|
combatState.player.hp = Math.min(combatState.player.maxHp, combatState.player.hp + healAmount); |
|
|
combatState.player.hp = Math.min(combatState.player.maxHp, combatState.player.hp + healAmount); |
|
|
addCombatLog(`You used ${skill.name}! Healed ${healAmount} HP!`, 'heal'); |
|
|
|
|
|
|
|
|
addCombatLog(`💚 ${displayName}! Healed ${healAmount} HP!`, 'heal'); |
|
|
|
|
|
} else if (skill.type === 'buff') { |
|
|
|
|
|
// Handle defense buff |
|
|
|
|
|
if (dbSkill && dbSkill.statusEffect && dbSkill.statusEffect.type === 'defense_up') { |
|
|
|
|
|
combatState.defenseBuffTurns = dbSkill.statusEffect.duration || 2; |
|
|
|
|
|
const buffPercent = dbSkill.statusEffect.percent || 50; |
|
|
|
|
|
addCombatLog(`🛡️ ${displayName}! DEF +${buffPercent}% for ${combatState.defenseBuffTurns} turns!`, 'buff'); |
|
|
|
|
|
} else if (hardcodedSkill && hardcodedSkill.effect === 'dodge') { |
|
|
|
|
|
// Legacy dodge buff |
|
|
|
|
|
addCombatLog(`⚡ ${displayName}! Next attack will be dodged!`); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
updateCombatUI(); |
|
|
updateCombatUI(); |
|
|
@ -9996,7 +10422,11 @@ |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Start monster turns sequence |
|
|
|
|
|
|
|
|
endPlayerTurn(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// End player turn and start monster turns |
|
|
|
|
|
function endPlayerTurn() { |
|
|
combatState.turn = 'monsters'; |
|
|
combatState.turn = 'monsters'; |
|
|
combatState.currentMonsterTurn = 0; |
|
|
combatState.currentMonsterTurn = 0; |
|
|
updateCombatUI(); |
|
|
updateCombatUI(); |
|
|
@ -10038,6 +10468,14 @@ |
|
|
// All monsters have attacked, return to player turn |
|
|
// All monsters have attacked, return to player turn |
|
|
combatState.turn = 'player'; |
|
|
combatState.turn = 'player'; |
|
|
updateCombatUI(); |
|
|
updateCombatUI(); |
|
|
|
|
|
|
|
|
|
|
|
// Process status effects at start of player turn |
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
if (combatState && combatState.playerStatusEffects.length > 0) { |
|
|
|
|
|
const survived = processPlayerStatusEffects(); |
|
|
|
|
|
if (!survived) return; // Player died from status effects |
|
|
|
|
|
} |
|
|
|
|
|
}, 200); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Execute one monster's attack |
|
|
// Execute one monster's attack |
|
|
@ -10048,10 +10486,76 @@ |
|
|
combatState.currentMonsterTurn = monsterIndex; |
|
|
combatState.currentMonsterTurn = monsterIndex; |
|
|
updateCombatUI(); |
|
|
updateCombatUI(); |
|
|
|
|
|
|
|
|
const damage = Math.max(1, monster.atk - combatState.player.def); |
|
|
|
|
|
|
|
|
// Select a skill using weighted random (or basic attack if none) |
|
|
|
|
|
const selectedSkill = selectMonsterSkill(monster.type, monster.level); |
|
|
|
|
|
|
|
|
|
|
|
// Calculate hit chance |
|
|
|
|
|
const skillAccuracy = selectedSkill.accuracy || 85; |
|
|
|
|
|
const hitChance = calculateHitChance( |
|
|
|
|
|
monster.accuracy, |
|
|
|
|
|
combatState.player.dodge, |
|
|
|
|
|
skillAccuracy |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// Roll for hit |
|
|
|
|
|
if (!rollHit(hitChance)) { |
|
|
|
|
|
addCombatLog(`❌ ${monster.data.name}'s ${selectedSkill.name} missed! (${hitChance}% chance)`, 'miss'); |
|
|
|
|
|
combatState.currentMonsterTurn++; |
|
|
|
|
|
setTimeout(executeMonsterTurns, 800); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Calculate effective defense (with buff if active) |
|
|
|
|
|
let effectiveDef = combatState.player.def; |
|
|
|
|
|
if (combatState.defenseBuffTurns > 0) { |
|
|
|
|
|
effectiveDef = Math.floor(effectiveDef * 1.5); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Handle different skill types |
|
|
|
|
|
if (selectedSkill.type === 'status') { |
|
|
|
|
|
// Status effect skill (like poison) |
|
|
|
|
|
const baseDamage = selectedSkill.basePower || 20; |
|
|
|
|
|
const damage = Math.max(1, Math.floor(monster.atk * (baseDamage / 100)) - effectiveDef); |
|
|
|
|
|
combatState.player.hp -= damage; |
|
|
|
|
|
|
|
|
|
|
|
// Apply status effect |
|
|
|
|
|
if (selectedSkill.statusEffect) { |
|
|
|
|
|
const effect = selectedSkill.statusEffect; |
|
|
|
|
|
// Check if already poisoned |
|
|
|
|
|
const existing = combatState.playerStatusEffects.find(e => e.type === effect.type); |
|
|
|
|
|
if (!existing) { |
|
|
|
|
|
combatState.playerStatusEffects.push({ |
|
|
|
|
|
type: effect.type, |
|
|
|
|
|
damage: effect.damage || 5, |
|
|
|
|
|
turnsLeft: effect.duration || 3 |
|
|
|
|
|
}); |
|
|
|
|
|
addCombatLog(`🔥 ${monster.data.name} uses ${selectedSkill.name}! ${damage} damage + ${effect.type} applied!`, 'damage'); |
|
|
|
|
|
} else { |
|
|
|
|
|
addCombatLog(`🔥 ${monster.data.name} uses ${selectedSkill.name}! ${damage} damage! (Already ${effect.type}ed)`, 'damage'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
// Regular damage skill or basic attack |
|
|
|
|
|
const basePower = selectedSkill.basePower || 100; |
|
|
|
|
|
const rawDamage = Math.floor(monster.atk * (basePower / 100)); |
|
|
|
|
|
const hitCount = selectedSkill.hitCount || 1; |
|
|
|
|
|
let totalDamage = 0; |
|
|
|
|
|
|
|
|
|
|
|
for (let hit = 0; hit < hitCount; hit++) { |
|
|
|
|
|
const damage = Math.max(1, rawDamage - effectiveDef); |
|
|
|
|
|
totalDamage += damage; |
|
|
combatState.player.hp -= damage; |
|
|
combatState.player.hp -= damage; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (selectedSkill.id === 'basic_attack' || selectedSkill.name === 'Attack') { |
|
|
|
|
|
addCombatLog(`⚔️ ${monster.data.name} attacks! You take ${totalDamage} damage!`, 'damage'); |
|
|
|
|
|
} else if (hitCount > 1) { |
|
|
|
|
|
addCombatLog(`🔥 ${monster.data.name} uses ${selectedSkill.name}! ${hitCount} hits for ${totalDamage} total damage!`, 'damage'); |
|
|
|
|
|
} else { |
|
|
|
|
|
addCombatLog(`🔥 ${monster.data.name} uses ${selectedSkill.name}! You take ${totalDamage} damage!`, 'damage'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
addCombatLog(`${monster.data.name} attacks! You take ${damage} damage!`, 'damage'); |
|
|
|
|
|
updateCombatUI(); |
|
|
updateCombatUI(); |
|
|
|
|
|
|
|
|
// Check for defeat |
|
|
// Check for defeat |
|
|
@ -10163,8 +10667,8 @@ |
|
|
// END RPG COMBAT SYSTEM FUNCTIONS |
|
|
// END RPG COMBAT SYSTEM FUNCTIONS |
|
|
// ========================================== |
|
|
// ========================================== |
|
|
|
|
|
|
|
|
// Load monster types from database, then initialize auth |
|
|
|
|
|
loadMonsterTypes().then(() => { |
|
|
|
|
|
|
|
|
// Load monster types and skills from database, then initialize auth |
|
|
|
|
|
Promise.all([loadMonsterTypes(), loadSkillsFromDatabase()]).then(() => { |
|
|
loadCurrentUser(); |
|
|
loadCurrentUser(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|