@ -412,6 +412,11 @@
color: #FF9800;
color: #FF9800;
}
}
.skill-type-utility {
background: rgba(0, 188, 212, 0.2);
color: #00BCD4;
}
.form-actions {
.form-actions {
display: flex;
display: flex;
gap: 10px;
gap: 10px;
@ -701,6 +706,7 @@
< th > Key< / th >
< th > Key< / th >
< th > Level< / th >
< th > Level< / th >
< th > HP< / th >
< th > HP< / th >
< th > MP< / th >
< th > ATK< / th >
< th > ATK< / th >
< th > DEF< / th >
< th > DEF< / th >
< th > XP< / th >
< th > XP< / th >
@ -740,6 +746,36 @@
< tr > < td colspan = "11" class = "loading" > Loading...< / td > < / tr >
< tr > < td colspan = "11" class = "loading" > Loading...< / td > < / tr >
< / tbody >
< / tbody >
< / table >
< / table >
<!-- Utility Skills Subsection -->
< div style = "margin-top: 40px;" >
< div class = "section-header" style = "display: flex; justify-content: space-between; align-items: flex-start;" >
< div >
< h3 > Utility Skills< / h3 >
< p style = "font-size: 12px; color: #888; margin-top: 5px; margin-bottom: 15px;" >
Skills that provide passive buffs outside of combat (MP/HP regen, stat boosts, XP multipliers)
< / p >
< / div >
< button class = "btn btn-primary" id = "addUtilitySkillBtn" > + Add Utility Skill< / button >
< / div >
< table class = "data-table" id = "utilitySkillTable" >
< thead >
< tr >
< th > Name< / th >
< th > ID< / th >
< th > Effect Type< / th >
< th > Value< / th >
< th > Duration< / th >
< th > Cooldown< / th >
< th > Enabled< / th >
< th > Actions< / th >
< / tr >
< / thead >
< tbody id = "utilitySkillTableBody" >
< tr > < td colspan = "8" class = "loading" > Loading...< / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
< / section >
<!-- Classes Section -->
<!-- Classes Section -->
@ -936,10 +972,23 @@
< label > Base DEF< / label >
< label > Base DEF< / label >
< input type = "number" id = "monsterDef" required value = "3" min = "0" >
< input type = "number" id = "monsterDef" required value = "3" min = "0" >
< / div >
< / div >
< div class = "form-group" >
< label > Base MP< / label >
< input type = "number" id = "monsterMp" required value = "20" min = "0" >
< / div >
< / div >
< div class = "form-row-3" >
< div class = "form-group" >
< div class = "form-group" >
< label > Base XP< / label >
< label > Base XP< / label >
< input type = "number" id = "monsterXp" required value = "10" min = "1" >
< input type = "number" id = "monsterXp" required value = "10" min = "1" >
< / div >
< / div >
< div class = "form-group" >
< label > MP/Level< / label >
< input type = "number" id = "monsterMpScale" required value = "5" min = "0" >
< / div >
< div class = "form-group" >
<!-- empty for alignment -->
< / div >
< / div >
< / div >
< div class = "form-group" >
< div class = "form-group" >
< label > Spawn Weight (higher = more common)< / label >
< label > Spawn Weight (higher = more common)< / label >
@ -1086,12 +1135,13 @@
< div class = "form-row" >
< div class = "form-row" >
< div class = "form-group" >
< div class = "form-group" >
< label > Type< / label >
< label > Type< / label >
< select id = "skillType" required >
< select id = "skillType" required onchange = "handleSkillTypeChange()" >
< option value = "damage" > Damage< / option >
< option value = "damage" > Damage< / option >
< option value = "heal" > Heal< / option >
< option value = "heal" > Heal< / option >
< option value = "buff" > Buff< / option >
< option value = "buff" > Buff< / option >
< option value = "debuff" > Debuff< / option >
< option value = "debuff" > Debuff< / option >
< option value = "status" > Status Effect< / option >
< option value = "status" > Status Effect< / option >
< option value = "utility" > Utility (Out-of-Combat)< / option >
< / select >
< / select >
< / div >
< / div >
< div class = "form-group" >
< div class = "form-group" >
@ -1148,6 +1198,43 @@
< / div >
< / div >
< / div >
< / div >
<!-- Utility Skill Configuration (hidden by default) -->
< div class = "status-effect-section" id = "utilityConfigSection" style = "display: none;" >
< h4 > Utility Skill Configuration< / h4 >
< p style = "font-size: 11px; color: #666; margin-bottom: 10px;" >
Configure the buff effect for this utility skill (e.g., Second Wind, XP Boost)
< / p >
< div class = "form-row" >
< div class = "form-group" >
< label > Effect Type< / label >
< select id = "utilityEffectType" >
< option value = "hp_regen_multiplier" > HP Regen Multiplier< / option >
< option value = "mp_regen_multiplier" > MP Regen Multiplier< / option >
< option value = "atk_boost_flat" > ATK Boost (Flat)< / option >
< option value = "atk_boost_percent" > ATK Boost (%)< / option >
< option value = "def_boost_flat" > DEF Boost (Flat)< / option >
< option value = "def_boost_percent" > DEF Boost (%)< / option >
< option value = "xp_multiplier" > XP Multiplier< / option >
< / select >
< / div >
< div class = "form-group" >
< label > Effect Value< / label >
< input type = "number" id = "utilityEffectValue" value = "2.0" min = "0" step = "0.1"
placeholder="e.g., 2.0 for 2x multiplier">
< / div >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label > Duration (hours)< / label >
< input type = "number" id = "utilityDurationHours" value = "1" min = "0.1" step = "0.1" >
< / div >
< div class = "form-group" >
< label > Cooldown (hours)< / label >
< input type = "number" id = "utilityCooldownHours" value = "24" min = "0" step = "0.5" >
< / div >
< / div >
< / div >
< div class = "form-row-3" style = "margin-top: 15px;" >
< div class = "form-row-3" style = "margin-top: 15px;" >
< div class = "form-group" >
< div class = "form-group" >
< label >
< label >
@ -1570,7 +1657,7 @@
function renderMonsterTable() {
function renderMonsterTable() {
const tbody = document.getElementById('monsterTableBody');
const tbody = document.getElementById('monsterTableBody');
if (monsters.length === 0) {
if (monsters.length === 0) {
tbody.innerHTML = '< tr > < td colspan = "9 " > No monsters found< / td > < / tr > ';
tbody.innerHTML = '< tr > < td colspan = "10 " > No monsters found< / td > < / tr > ';
return;
return;
}
}
@ -1580,6 +1667,7 @@
< td > < code > ${escapeHtml(m.key)}< / code > < / td >
< td > < code > ${escapeHtml(m.key)}< / code > < / td >
< td > ${m.min_level}-${m.max_level}< / td >
< td > ${m.min_level}-${m.max_level}< / td >
< td > ${m.base_hp}< / td >
< td > ${m.base_hp}< / td >
< td > ${m.base_mp || 20}< / td >
< td > ${m.base_atk}< / td >
< td > ${m.base_atk}< / td >
< td > ${m.base_def}< / td >
< td > ${m.base_def}< / td >
< td > ${m.base_xp}< / td >
< td > ${m.base_xp}< / td >
@ -1641,6 +1729,8 @@
document.getElementById('monsterHp').value = monster.base_hp;
document.getElementById('monsterHp').value = monster.base_hp;
document.getElementById('monsterAtk').value = monster.base_atk;
document.getElementById('monsterAtk').value = monster.base_atk;
document.getElementById('monsterDef').value = monster.base_def;
document.getElementById('monsterDef').value = monster.base_def;
document.getElementById('monsterMp').value = monster.base_mp || 20;
document.getElementById('monsterMpScale').value = monster.level_scale_mp || 5;
document.getElementById('monsterXp').value = monster.base_xp;
document.getElementById('monsterXp').value = monster.base_xp;
document.getElementById('monsterWeight').value = monster.spawn_weight || 100;
document.getElementById('monsterWeight').value = monster.spawn_weight || 100;
document.getElementById('monsterEnabled').checked = monster.enabled;
document.getElementById('monsterEnabled').checked = monster.enabled;
@ -1681,6 +1771,8 @@
document.getElementById('monsterHp').value = monster.base_hp;
document.getElementById('monsterHp').value = monster.base_hp;
document.getElementById('monsterAtk').value = monster.base_atk;
document.getElementById('monsterAtk').value = monster.base_atk;
document.getElementById('monsterDef').value = monster.base_def;
document.getElementById('monsterDef').value = monster.base_def;
document.getElementById('monsterMp').value = monster.base_mp || 20;
document.getElementById('monsterMpScale').value = monster.level_scale_mp || 5;
document.getElementById('monsterXp').value = monster.base_xp;
document.getElementById('monsterXp').value = monster.base_xp;
document.getElementById('monsterWeight').value = monster.spawn_weight || 100;
document.getElementById('monsterWeight').value = monster.spawn_weight || 100;
document.getElementById('monsterEnabled').checked = false; // Disabled by default
document.getElementById('monsterEnabled').checked = false; // Disabled by default
@ -1735,8 +1827,10 @@
base_hp: parseInt(document.getElementById('monsterHp').value),
base_hp: parseInt(document.getElementById('monsterHp').value),
base_atk: parseInt(document.getElementById('monsterAtk').value),
base_atk: parseInt(document.getElementById('monsterAtk').value),
base_def: parseInt(document.getElementById('monsterDef').value),
base_def: parseInt(document.getElementById('monsterDef').value),
base_mp: parseInt(document.getElementById('monsterMp').value),
base_xp: parseInt(document.getElementById('monsterXp').value),
base_xp: parseInt(document.getElementById('monsterXp').value),
spawn_weight: parseInt(document.getElementById('monsterWeight').value),
spawn_weight: parseInt(document.getElementById('monsterWeight').value),
levelScale: { mp: parseInt(document.getElementById('monsterMpScale').value) || 5 },
enabled: document.getElementById('monsterEnabled').checked,
enabled: document.getElementById('monsterEnabled').checked,
dialogues: JSON.stringify(dialogues)
dialogues: JSON.stringify(dialogues)
};
};
@ -1778,6 +1872,7 @@
const data = await api('/api/admin/skills');
const data = await api('/api/admin/skills');
allSkills = data.skills || [];
allSkills = data.skills || [];
renderSkillTable();
renderSkillTable();
renderUtilitySkillTable(); // Also render utility skills table
populateSkillSelect(); // Also update the monster skill dropdown
populateSkillSelect(); // Also update the monster skill dropdown
} catch (e) {
} catch (e) {
showToast('Failed to load skills: ' + e.message, 'error');
showToast('Failed to load skills: ' + e.message, 'error');
@ -1786,12 +1881,14 @@
function renderSkillTable() {
function renderSkillTable() {
const tbody = document.getElementById('skillTableBody');
const tbody = document.getElementById('skillTableBody');
if (allSkills.length === 0) {
tbody.innerHTML = '< tr > < td colspan = "11" > No skills found< / td > < / tr > ';
// Filter out utility skills - they're shown in the Utility Skills section
const combatSkills = allSkills.filter(s => s.type !== 'utility');
if (combatSkills.length === 0) {
tbody.innerHTML = '< tr > < td colspan = "11" > No combat skills found< / td > < / tr > ';
return;
return;
}
}
tbody.innerHTML = all Skills.map(s => {
tbody.innerHTML = combat Skills.map(s => {
const statusEffect = s.status_effect ? JSON.parse(s.status_effect) : null;
const statusEffect = s.status_effect ? JSON.parse(s.status_effect) : null;
return `
return `
< tr >
< tr >
@ -1819,6 +1916,80 @@
`}).join('');
`}).join('');
}
}
function renderUtilitySkillTable() {
const utilitySkills = allSkills.filter(s => s.type === 'utility');
const tbody = document.getElementById('utilitySkillTableBody');
if (utilitySkills.length === 0) {
tbody.innerHTML = '< tr > < td colspan = "8" > No utility skills found. Add a skill with type "Utility" to see it here.< / td > < / tr > ';
return;
}
const effectLabels = {
'hp_regen_multiplier': 'HP Regen',
'mp_regen_multiplier': 'MP Regen',
'atk_boost_flat': 'ATK +',
'atk_boost_percent': 'ATK %',
'def_boost_flat': 'DEF +',
'def_boost_percent': 'DEF %',
'xp_multiplier': 'XP'
};
tbody.innerHTML = utilitySkills.map(s => {
let config = { effectType: '-', effectValue: '-', durationHours: '-', cooldownHours: '-' };
if (s.status_effect) {
try {
config = JSON.parse(s.status_effect);
} catch {}
}
const valueDisplay = config.effectType?.includes('percent') || config.effectType?.includes('multiplier')
? config.effectValue + 'x'
: '+' + config.effectValue;
return `
< tr >
< td > < strong > ${escapeHtml(s.name)}< / strong > < / td >
< td > < code > ${escapeHtml(s.id)}< / code > < / td >
< td > < span class = "skill-type-badge skill-type-utility" > ${effectLabels[config.effectType] || config.effectType || '-'}< / span > < / td >
< td > ${valueDisplay}< / td >
< td > ${config.durationHours || '-'}h< / td >
< td > ${config.cooldownHours || '-'}h< / td >
< td >
< label class = "toggle" >
< input type = "checkbox" $ { s . enabled ? ' checked ' : ' ' }
onchange="toggleSkill('${s.id}', this.checked)">
< span class = "toggle-slider" > < / span >
< / label >
< / td >
< td class = "actions" >
< button class = "btn btn-secondary btn-small" onclick = "editSkill('${s.id}')" > Edit< / button >
< button class = "btn btn-danger btn-small" onclick = "deleteSkill('${s.id}')" > Delete< / button >
< / td >
< / tr >
`}).join('');
}
function handleSkillTypeChange() {
const skillType = document.getElementById('skillType').value;
const utilitySection = document.getElementById('utilityConfigSection');
const statusEffectSection = document.querySelector('.status-effect-section:not(#utilityConfigSection)');
if (skillType === 'utility') {
utilitySection.style.display = 'block';
if (statusEffectSection) statusEffectSection.style.display = 'none';
// Auto-set defaults for utility skills
document.getElementById('skillPlayerUsable').checked = false;
document.getElementById('skillMonsterUsable').checked = false;
document.getElementById('skillTarget').value = 'self';
document.getElementById('skillMpCost').value = '0';
document.getElementById('skillBasePower').value = '0';
} else {
utilitySection.style.display = 'none';
if (statusEffectSection) statusEffectSection.style.display = 'block';
}
}
async function toggleSkill(id, enabled) {
async function toggleSkill(id, enabled) {
try {
try {
await api(`/api/admin/skills/${id}`, {
await api(`/api/admin/skills/${id}`, {
@ -1868,13 +2039,30 @@
document.getElementById('skillMonsterUsable').checked = skill.monster_usable;
document.getElementById('skillMonsterUsable').checked = skill.monster_usable;
document.getElementById('skillEnabled').checked = skill.enabled;
document.getElementById('skillEnabled').checked = skill.enabled;
// Parse status effect
// Parse status effect based on skill type
if (skill.status_effect) {
if (skill.status_effect) {
try {
try {
const effect = JSON.parse(skill.status_effect);
const effect = JSON.parse(skill.status_effect);
document.getElementById('skillStatusType').value = effect.type || '';
document.getElementById('skillStatusDamage').value = effect.damage || 5;
document.getElementById('skillStatusDuration').value = effect.duration || 3;
if (skill.type === 'utility') {
// Utility skill config
document.getElementById('utilityEffectType').value = effect.effectType || 'mp_regen_multiplier';
document.getElementById('utilityEffectValue').value = effect.effectValue || 2.0;
document.getElementById('utilityDurationHours').value = effect.durationHours || 1;
document.getElementById('utilityCooldownHours').value = effect.cooldownHours || 24;
// Reset combat status effect fields
document.getElementById('skillStatusType').value = '';
} else {
// Combat status effect
document.getElementById('skillStatusType').value = effect.type || '';
document.getElementById('skillStatusDamage').value = effect.damage || 5;
document.getElementById('skillStatusDuration').value = effect.duration || 3;
// Reset utility fields
document.getElementById('utilityEffectType').value = 'mp_regen_multiplier';
document.getElementById('utilityEffectValue').value = 2.0;
document.getElementById('utilityDurationHours').value = 1;
document.getElementById('utilityCooldownHours').value = 24;
}
} catch {
} catch {
document.getElementById('skillStatusType').value = '';
document.getElementById('skillStatusType').value = '';
}
}
@ -1882,8 +2070,16 @@
document.getElementById('skillStatusType').value = '';
document.getElementById('skillStatusType').value = '';
document.getElementById('skillStatusDamage').value = 5;
document.getElementById('skillStatusDamage').value = 5;
document.getElementById('skillStatusDuration').value = 3;
document.getElementById('skillStatusDuration').value = 3;
// Reset utility fields to defaults
document.getElementById('utilityEffectType').value = 'mp_regen_multiplier';
document.getElementById('utilityEffectValue').value = 2.0;
document.getElementById('utilityDurationHours').value = 1;
document.getElementById('utilityCooldownHours').value = 24;
}
}
// Toggle visibility of form sections based on skill type
handleSkillTypeChange();
document.getElementById('skillModal').classList.add('active');
document.getElementById('skillModal').classList.add('active');
}
}
@ -1895,6 +2091,30 @@
document.getElementById('skillPlayerUsable').checked = true;
document.getElementById('skillPlayerUsable').checked = true;
document.getElementById('skillMonsterUsable').checked = true;
document.getElementById('skillMonsterUsable').checked = true;
document.getElementById('skillEnabled').checked = true;
document.getElementById('skillEnabled').checked = true;
// Reset to default (damage) type and toggle visibility
document.getElementById('skillType').value = 'damage';
handleSkillTypeChange();
document.getElementById('skillModal').classList.add('active');
});
document.getElementById('addUtilitySkillBtn').addEventListener('click', () => {
document.getElementById('skillModalTitle').textContent = 'Add Utility Skill';
document.getElementById('skillForm').reset();
document.getElementById('skillEditId').value = '';
document.getElementById('skillId').disabled = false;
// Utility skills default settings
document.getElementById('skillPlayerUsable').checked = false;
document.getElementById('skillMonsterUsable').checked = false;
document.getElementById('skillEnabled').checked = true;
document.getElementById('skillTarget').value = 'self';
// Set type to utility and toggle visibility
document.getElementById('skillType').value = 'utility';
handleSkillTypeChange();
// Set default utility values
document.getElementById('utilityEffectType').value = 'mp_regen_multiplier';
document.getElementById('utilityEffectValue').value = '2.0';
document.getElementById('utilityDurationHours').value = '1';
document.getElementById('utilityCooldownHours').value = '24';
document.getElementById('skillModal').classList.add('active');
document.getElementById('skillModal').classList.add('active');
});
});
@ -1909,15 +2129,28 @@
const editId = document.getElementById('skillEditId').value;
const editId = document.getElementById('skillEditId').value;
const skillId = document.getElementById('skillId').value;
const skillId = document.getElementById('skillId').value;
// Build status effect JSON if type is selected
// Build status effect JSON based on skill type
let statusEffect = null;
let statusEffect = null;
const statusType = document.getElementById('skillStatusType').value;
if (statusType) {
const skillType = document.getElementById('skillType').value;
if (skillType === 'utility') {
// Utility skill - use utility config fields
statusEffect = JSON.stringify({
statusEffect = JSON.stringify({
type: statusType,
damage: parseInt(document.getElementById('skillStatusDamage').value) || 5,
duration: parseInt(document.getElementById('skillStatusDuration').value) || 3
effectType: document.getElementById('utilityEffectType').value,
effectValue: parseFloat(document.getElementById('utilityEffectValue').value) || 2.0,
durationHours: parseFloat(document.getElementById('utilityDurationHours').value) || 1,
cooldownHours: parseFloat(document.getElementById('utilityCooldownHours').value) || 24
});
});
} else {
// Combat skill - use status effect fields
const statusType = document.getElementById('skillStatusType').value;
if (statusType) {
statusEffect = JSON.stringify({
type: statusType,
damage: parseInt(document.getElementById('skillStatusDamage').value) || 5,
duration: parseInt(document.getElementById('skillStatusDuration').value) || 3
});
}
}
}
const data = {
const data = {