Browse Source

Add utility skill management to admin panel

- Add Utility Skills section with dedicated table showing effect type, value, duration, cooldown
- Add "Add Utility Skill" button with pre-configured defaults
- Filter utility skills out of main combat skills table
- Fix updateSkill() to handle both snake_case and camelCase field names
- Add getUtilitySkillConfig() for database lookup of utility skill settings
- Replace hardcoded BUFF_CONFIGS in server.js with dynamic database lookup
- Support effect types: HP/MP regen multipliers, ATK/DEF boosts, XP multiplier

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
master
HikeMap User 1 month ago
parent
commit
db2ea4f351
  1. 263
      admin.html
  2. 86
      database.js
  3. 24
      server.js

263
admin.html

@ -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 = allSkills.map(s => {
tbody.innerHTML = combatSkills.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 = {

86
database.js

@ -245,6 +245,14 @@ class HikeMapDB {
this.db.exec(`ALTER TABLE monster_types ADD COLUMN dodge INTEGER DEFAULT 5`); this.db.exec(`ALTER TABLE monster_types ADD COLUMN dodge INTEGER DEFAULT 5`);
} catch (e) { /* Column already exists */ } } catch (e) { /* Column already exists */ }
// Migration: Add MP to monster_types
try {
this.db.exec(`ALTER TABLE monster_types ADD COLUMN base_mp INTEGER DEFAULT 20`);
} catch (e) { /* Column already exists */ }
try {
this.db.exec(`ALTER TABLE monster_types ADD COLUMN level_scale_mp INTEGER DEFAULT 5`);
} catch (e) { /* Column already exists */ }
// Migration: Add custom_name to monster_skills for per-monster skill renaming // Migration: Add custom_name to monster_skills for per-monster skill renaming
try { try {
this.db.exec(`ALTER TABLE monster_skills ADD COLUMN custom_name TEXT`); this.db.exec(`ALTER TABLE monster_skills ADD COLUMN custom_name TEXT`);
@ -831,15 +839,24 @@ class HikeMapDB {
createMonsterType(monsterData) { createMonsterType(monsterData) {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO monster_types (id, name, icon, base_hp, base_atk, base_def, xp_reward, INSERT INTO monster_types (id, name, icon, base_hp, base_atk, base_def, xp_reward,
level_scale_hp, level_scale_atk, level_scale_def, min_level, max_level, spawn_weight, dialogues, enabled)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
level_scale_hp, level_scale_atk, level_scale_def, min_level, max_level, spawn_weight, dialogues, enabled,
base_mp, level_scale_mp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`); `);
// Support both camelCase (legacy) and snake_case (new admin UI) field names // Support both camelCase (legacy) and snake_case (new admin UI) field names
const baseHp = monsterData.baseHp || monsterData.base_hp; const baseHp = monsterData.baseHp || monsterData.base_hp;
const baseAtk = monsterData.baseAtk || monsterData.base_atk; const baseAtk = monsterData.baseAtk || monsterData.base_atk;
const baseDef = monsterData.baseDef || monsterData.base_def; const baseDef = monsterData.baseDef || monsterData.base_def;
const baseMp = monsterData.baseMp || monsterData.base_mp || 20;
const xpReward = monsterData.xpReward || monsterData.base_xp; const xpReward = monsterData.xpReward || monsterData.base_xp;
const levelScale = monsterData.levelScale || { hp: 10, atk: 2, def: 1 };
// Merge levelScale with defaults for any missing properties
const levelScaleInput = monsterData.levelScale || {};
const levelScale = {
hp: levelScaleInput.hp ?? monsterData.level_scale_hp ?? 10,
atk: levelScaleInput.atk ?? monsterData.level_scale_atk ?? 2,
def: levelScaleInput.def ?? monsterData.level_scale_def ?? 1,
mp: levelScaleInput.mp ?? monsterData.level_scale_mp ?? 5
};
const minLevel = monsterData.minLevel || monsterData.min_level || 1; const minLevel = monsterData.minLevel || monsterData.min_level || 1;
const maxLevel = monsterData.maxLevel || monsterData.max_level || 5; const maxLevel = monsterData.maxLevel || monsterData.max_level || 5;
const spawnWeight = monsterData.spawnWeight || monsterData.spawn_weight || 100; const spawnWeight = monsterData.spawnWeight || monsterData.spawn_weight || 100;
@ -862,7 +879,9 @@ class HikeMapDB {
maxLevel, maxLevel,
spawnWeight, spawnWeight,
dialogues, dialogues,
monsterData.enabled !== false ? 1 : 0
monsterData.enabled !== false ? 1 : 0,
baseMp,
levelScale.mp || 5
); );
} }
@ -871,15 +890,24 @@ class HikeMapDB {
UPDATE monster_types SET UPDATE monster_types SET
name = ?, icon = ?, base_hp = ?, base_atk = ?, base_def = ?, name = ?, icon = ?, base_hp = ?, base_atk = ?, base_def = ?,
xp_reward = ?, level_scale_hp = ?, level_scale_atk = ?, level_scale_def = ?, xp_reward = ?, level_scale_hp = ?, level_scale_atk = ?, level_scale_def = ?,
min_level = ?, max_level = ?, spawn_weight = ?, dialogues = ?, enabled = ?
min_level = ?, max_level = ?, spawn_weight = ?, dialogues = ?, enabled = ?,
base_mp = ?, level_scale_mp = ?
WHERE id = ? WHERE id = ?
`); `);
// Support both camelCase (legacy) and snake_case (new admin UI) field names // Support both camelCase (legacy) and snake_case (new admin UI) field names
const baseHp = monsterData.baseHp || monsterData.base_hp; const baseHp = monsterData.baseHp || monsterData.base_hp;
const baseAtk = monsterData.baseAtk || monsterData.base_atk; const baseAtk = monsterData.baseAtk || monsterData.base_atk;
const baseDef = monsterData.baseDef || monsterData.base_def; const baseDef = monsterData.baseDef || monsterData.base_def;
const baseMp = monsterData.baseMp || monsterData.base_mp || 20;
const xpReward = monsterData.xpReward || monsterData.base_xp; const xpReward = monsterData.xpReward || monsterData.base_xp;
const levelScale = monsterData.levelScale || { hp: 10, atk: 2, def: 1 };
// Merge levelScale with defaults for any missing properties
const levelScaleInput = monsterData.levelScale || {};
const levelScale = {
hp: levelScaleInput.hp ?? monsterData.level_scale_hp ?? 10,
atk: levelScaleInput.atk ?? monsterData.level_scale_atk ?? 2,
def: levelScaleInput.def ?? monsterData.level_scale_def ?? 1,
mp: levelScaleInput.mp ?? monsterData.level_scale_mp ?? 5
};
const minLevel = monsterData.minLevel || monsterData.min_level || 1; const minLevel = monsterData.minLevel || monsterData.min_level || 1;
const maxLevel = monsterData.maxLevel || monsterData.max_level || 5; const maxLevel = monsterData.maxLevel || monsterData.max_level || 5;
const spawnWeight = monsterData.spawnWeight || monsterData.spawn_weight || 100; const spawnWeight = monsterData.spawnWeight || monsterData.spawn_weight || 100;
@ -902,6 +930,8 @@ class HikeMapDB {
spawnWeight, spawnWeight,
dialogues, dialogues,
monsterData.enabled !== false ? 1 : 0, monsterData.enabled !== false ? 1 : 0,
baseMp,
levelScale.mp || 5,
id id
); );
} }
@ -989,6 +1019,19 @@ class HikeMapDB {
return stmt.get(id); return stmt.get(id);
} }
// Get utility skill configuration from status_effect JSON
// Returns { effectType, effectValue, durationHours, cooldownHours } or null
getUtilitySkillConfig(skillId) {
const skill = this.getSkill(skillId);
if (!skill || skill.type !== 'utility') return null;
if (!skill.status_effect) return null;
try {
return JSON.parse(skill.status_effect);
} catch {
return null;
}
}
createSkill(skillData) { createSkill(skillData) {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO skills (id, name, description, type, mp_cost, base_power, accuracy, hit_count, target, status_effect, player_usable, monster_usable, enabled) INSERT INTO skills (id, name, description, type, mp_cost, base_power, accuracy, hit_count, target, status_effect, player_usable, monster_usable, enabled)
@ -1022,9 +1065,23 @@ class HikeMapDB {
player_usable = ?, monster_usable = ?, enabled = ? player_usable = ?, monster_usable = ?, enabled = ?
WHERE id = ? WHERE id = ?
`); `);
const statusEffect = skillData.statusEffect
? (typeof skillData.statusEffect === 'string' ? skillData.statusEffect : JSON.stringify(skillData.statusEffect))
// Handle both camelCase and snake_case field names
const rawStatusEffect = skillData.statusEffect || skillData.status_effect;
const statusEffect = rawStatusEffect
? (typeof rawStatusEffect === 'string' ? rawStatusEffect : JSON.stringify(rawStatusEffect))
: null; : null;
// Handle player_usable/playerUsable (check for explicit false)
const playerUsable = skillData.player_usable !== undefined
? (skillData.player_usable ? 1 : 0)
: (skillData.playerUsable !== false ? 1 : 0);
const monsterUsable = skillData.monster_usable !== undefined
? (skillData.monster_usable ? 1 : 0)
: (skillData.monsterUsable !== false ? 1 : 0);
const enabled = skillData.enabled !== undefined
? (skillData.enabled ? 1 : 0)
: 1;
return stmt.run( return stmt.run(
skillData.name, skillData.name,
skillData.description, skillData.description,
@ -1035,9 +1092,9 @@ class HikeMapDB {
skillData.hitCount || skillData.hit_count || 1, skillData.hitCount || skillData.hit_count || 1,
skillData.target || 'enemy', skillData.target || 'enemy',
statusEffect, statusEffect,
skillData.playerUsable !== false ? 1 : 0,
skillData.monsterUsable !== false ? 1 : 0,
skillData.enabled !== false ? 1 : 0,
playerUsable,
monsterUsable,
enabled,
id id
); );
} }
@ -1310,7 +1367,12 @@ class HikeMapDB {
accuracy: 100, accuracy: 100,
hitCount: 1, hitCount: 1,
target: 'self', target: 'self',
statusEffect: { type: 'mp_regen_multiplier', value: 2.0, duration: 3600 },
statusEffect: {
effectType: 'mp_regen_multiplier',
effectValue: 2.0,
durationHours: 1,
cooldownHours: 24
},
playerUsable: false, // Not usable in combat - it's a utility skill playerUsable: false, // Not usable in combat - it's a utility skill
monsterUsable: false monsterUsable: false
}, },

24
server.js

@ -1201,20 +1201,10 @@ app.post('/api/user/buffs/activate', authenticateToken, (req, res) => {
return res.status(400).json({ error: 'Buff type is required' }); return res.status(400).json({ error: 'Buff type is required' });
} }
// Define buff configurations
const BUFF_CONFIGS = {
'second_wind': {
effectType: 'mp_regen_multiplier',
effectValue: 2.0, // Double MP regen
durationHours: 1, // 1 hour
cooldownHours: 24 // 24 hour cooldown
}
// Add more buff types here as needed
};
const config = BUFF_CONFIGS[buffType];
// Get buff configuration from database (utility skill's status_effect JSON)
const config = db.getUtilitySkillConfig(buffType);
if (!config) { if (!config) {
return res.status(400).json({ error: 'Unknown buff type' });
return res.status(400).json({ error: 'Unknown buff type or not a utility skill' });
} }
// Check if buff can be activated (not on cooldown) // Check if buff can be activated (not on cooldown)
@ -1226,7 +1216,7 @@ app.post('/api/user/buffs/activate', authenticateToken, (req, res) => {
}); });
} }
// Activate the buff
// Activate the buff using config from database
db.activateBuff( db.activateBuff(
req.user.userId, req.user.userId,
buffType, buffType,
@ -1277,6 +1267,7 @@ app.get('/api/monster-types', (req, res) => {
baseHp: t.base_hp, baseHp: t.base_hp,
baseAtk: t.base_atk, baseAtk: t.base_atk,
baseDef: t.base_def, baseDef: t.base_def,
baseMp: t.base_mp || 20,
xpReward: t.xp_reward, xpReward: t.xp_reward,
accuracy: t.accuracy || 85, accuracy: t.accuracy || 85,
dodge: t.dodge || 5, dodge: t.dodge || 5,
@ -1286,7 +1277,8 @@ app.get('/api/monster-types', (req, res) => {
levelScale: { levelScale: {
hp: t.level_scale_hp, hp: t.level_scale_hp,
atk: t.level_scale_atk, atk: t.level_scale_atk,
def: t.level_scale_def
def: t.level_scale_def,
mp: t.level_scale_mp || 5
}, },
dialogues: JSON.parse(t.dialogues) dialogues: JSON.parse(t.dialogues)
})); }));
@ -1456,8 +1448,10 @@ app.get('/api/admin/monster-types', adminOnly, (req, res) => {
base_hp: t.base_hp, base_hp: t.base_hp,
base_atk: t.base_atk, base_atk: t.base_atk,
base_def: t.base_def, base_def: t.base_def,
base_mp: t.base_mp || 20,
base_xp: t.xp_reward, base_xp: t.xp_reward,
spawn_weight: t.spawn_weight || 100, spawn_weight: t.spawn_weight || 100,
level_scale_mp: t.level_scale_mp || 5,
dialogues: t.dialogues, dialogues: t.dialogues,
enabled: !!t.enabled, enabled: !!t.enabled,
created_at: t.created_at created_at: t.created_at

Loading…
Cancel
Save