2 ? '...' : '');
+ const artworkNum = String(tag.artwork || 1).padStart(2, '0');
return `
| ${escapeHtml(tag.id)} |
- ${escapeHtml(tag.icon)} |
+ ${artworkNum}${tag.animation ? ` (${tag.animation})` : ''} |
${prefixCount > 0 ? `${prefixCount} prefix${prefixCount > 1 ? 'es' : ''}` : 'None'}
${prefixPreview ? ` ${escapeHtml(prefixPreview)}` : ''}
@@ -3243,12 +3524,18 @@
form.reset();
+ // Populate animation dropdowns from MONSTER_ANIMATIONS if available
+ populateAnimationDropdown('osmTagAnimation');
+ populateAnimationDropdown('osmTagAnimationShadow');
+
if (tag) {
title.textContent = 'Edit OSM Tag';
document.getElementById('osmTagIdField').value = tag.id;
idInput.value = tag.id;
idInput.readOnly = true;
- document.getElementById('osmTagIcon').value = tag.icon || 'map-marker';
+ document.getElementById('osmTagArtwork').value = tag.artwork || 1;
+ document.getElementById('osmTagAnimation').value = tag.animation || '';
+ document.getElementById('osmTagAnimationShadow').value = tag.animation_shadow || '';
document.getElementById('osmTagVisibility').value = tag.visibility_distance || 400;
document.getElementById('osmTagSpawnRadius').value = tag.spawn_radius || 400;
const prefixes = typeof tag.prefixes === 'string' ? JSON.parse(tag.prefixes || '[]') : (tag.prefixes || []);
@@ -3257,12 +3544,17 @@
title.textContent = 'Add OSM Tag';
document.getElementById('osmTagIdField').value = '';
idInput.readOnly = false;
- document.getElementById('osmTagIcon').value = 'map-marker';
+ document.getElementById('osmTagArtwork').value = 1;
+ document.getElementById('osmTagAnimation').value = '';
+ document.getElementById('osmTagAnimationShadow').value = '';
document.getElementById('osmTagVisibility').value = 400;
document.getElementById('osmTagSpawnRadius').value = 400;
document.getElementById('osmTagPrefixes').value = '';
}
+ // Update artwork preview
+ updateArtworkPreview();
+
modal.classList.add('active');
}
@@ -3304,7 +3596,9 @@
const data = {
id: newId,
- icon: document.getElementById('osmTagIcon').value.trim() || 'map-marker',
+ artwork: parseInt(document.getElementById('osmTagArtwork').value) || 1,
+ animation: document.getElementById('osmTagAnimation').value || null,
+ animation_shadow: document.getElementById('osmTagAnimationShadow').value || null,
visibility_distance: parseInt(document.getElementById('osmTagVisibility').value) || 400,
spawn_radius: parseInt(document.getElementById('osmTagSpawnRadius').value) || 400,
prefixes: prefixes
diff --git a/database.js b/database.js
index 8fdf3ea..bdf260c 100644
--- a/database.js
+++ b/database.js
@@ -353,6 +353,17 @@ class HikeMapDB {
this.db.exec(`ALTER TABLE osm_tags ADD COLUMN animation_shadow TEXT DEFAULT NULL`);
} catch (e) { /* Column already exists */ }
+ // Skill icon migrations
+ try {
+ this.db.exec(`ALTER TABLE skills ADD COLUMN icon TEXT`);
+ } catch (e) { /* Column already exists */ }
+ try {
+ this.db.exec(`ALTER TABLE class_skills ADD COLUMN custom_icon TEXT`);
+ } catch (e) { /* Column already exists */ }
+ try {
+ this.db.exec(`ALTER TABLE monster_skills ADD COLUMN custom_icon TEXT`);
+ } catch (e) { /* Column already exists */ }
+
// OSM Tag settings - global prefix configuration
this.db.exec(`
CREATE TABLE IF NOT EXISTS osm_tag_settings (
@@ -1214,16 +1225,41 @@ class HikeMapDB {
return stmt.run(id);
}
+ // =====================
+ // SKILL ICON METHODS
+ // =====================
+
+ updateSkillIcon(skillId, iconFilename) {
+ const stmt = this.db.prepare(`UPDATE skills SET icon = ? WHERE id = ?`);
+ return stmt.run(iconFilename, skillId);
+ }
+
+ updateClassSkillIcon(classId, skillId, iconFilename) {
+ const stmt = this.db.prepare(`
+ UPDATE class_skills SET custom_icon = ?
+ WHERE class_id = ? AND skill_id = ?
+ `);
+ return stmt.run(iconFilename, classId, skillId);
+ }
+
+ updateMonsterSkillIcon(monsterTypeId, skillId, iconFilename) {
+ const stmt = this.db.prepare(`
+ UPDATE monster_skills SET custom_icon = ?
+ WHERE monster_type_id = ? AND skill_id = ?
+ `);
+ return stmt.run(iconFilename, monsterTypeId, skillId);
+ }
+
// =====================
// CLASS SKILL NAMES METHODS
// =====================
- // Get custom names from class_skills table (primary source - what admin panel edits)
+ // Get custom names and icons from class_skills table (primary source - what admin panel edits)
getAllClassSkillNamesFromClassSkills() {
const stmt = this.db.prepare(`
- SELECT cs.id, cs.skill_id, cs.class_id, cs.custom_name, cs.custom_description
+ SELECT cs.id, cs.skill_id, cs.class_id, cs.custom_name, cs.custom_description, cs.custom_icon
FROM class_skills cs
- WHERE cs.custom_name IS NOT NULL AND cs.custom_name != ''
+ WHERE (cs.custom_name IS NOT NULL AND cs.custom_name != '') OR cs.custom_icon IS NOT NULL
`);
return stmt.all();
}
diff --git a/index.html b/index.html
index 7104e0a..8012213 100644
--- a/index.html
+++ b/index.html
@@ -4950,6 +4950,49 @@
};
}
+ // Get skill icon with fallback chain: class/monster override → base skill → emoji
+ function getSkillIcon(skillId, contextType = null, contextId = null) {
+ const baseSkill = SKILLS_DB[skillId];
+ const hardcodedSkill = SKILLS[skillId];
+
+ // Check class override
+ if (contextType === 'class' && contextId) {
+ const classSkill = CLASS_SKILL_NAMES.find(
+ n => n.skillId === skillId && n.classId === contextId
+ );
+ if (classSkill?.customIcon) {
+ return { type: 'image', src: `/mapgameimgs/skills/${classSkill.customIcon}` };
+ }
+ }
+
+ // Check monster override
+ if (contextType === 'monster' && contextId) {
+ const monsterSkills = MONSTER_SKILLS[contextId] || [];
+ const monsterSkill = monsterSkills.find(s => s.skillId === skillId);
+ if (monsterSkill?.customIcon) {
+ return { type: 'image', src: `/mapgameimgs/skills/${monsterSkill.customIcon}` };
+ }
+ }
+
+ // Base skill icon from database
+ if (baseSkill?.icon) {
+ return { type: 'image', src: `/mapgameimgs/skills/${baseSkill.icon}` };
+ }
+
+ // Emoji fallback
+ return { type: 'emoji', value: hardcodedSkill?.icon || '⚔️' };
+ }
+
+ // Render skill icon as HTML (with error fallback to emoji)
+ function renderSkillIcon(skillId, contextType = null, contextId = null, size = 20) {
+ const icon = getSkillIcon(skillId, contextType, contextId);
+ if (icon.type === 'image') {
+ const fallbackEmoji = SKILLS[skillId]?.icon || '⚔️';
+ return ` `;
+ }
+ return icon.value;
+ }
+
// Calculate hit chance: skill accuracy + (attacker accuracy - 90) - defender dodge
function calculateHitChance(attackerAccuracy, defenderDodge, skillAccuracy) {
const hitChance = skillAccuracy + (attackerAccuracy - 90) - defenderDodge;
@@ -12216,9 +12259,10 @@
statsText = `${mpCost} MP`;
}
+ const iconHtml = renderSkillIcon(skillId, 'class', playerStats.class, 24);
return `
- ${skill.icon}
+ ${iconHtml}
${displayName}
${displayDesc}
@@ -12308,9 +12352,10 @@
}
}
+ const secondWindIconHtml = renderSkillIcon('second_wind', 'class', playerStats?.class, 24);
container.innerHTML = `
- 💨
+ ${secondWindIconHtml}
Second Wind
Double MP regen while walking for 1 hour
@@ -12433,12 +12478,12 @@
const displayName = skillInfo?.displayName || skill.name;
const displayDesc = skillInfo?.displayDescription || skill.description;
- const icon = hardcodedSkill?.icon || '⚔️';
+ const iconHtml = renderSkillIcon(skillId, 'class', playerStats.class, 32);
const mpCost = skill.mpCost || skill.mp_cost || 0;
return `
- ${icon}
+ ${iconHtml}
${displayName}
${displayDesc}
@@ -13201,7 +13246,7 @@
if (!baseSkill) return;
const displayName = skillInfo?.displayName || baseSkill.name;
- const icon = SKILLS[skillId]?.icon || baseSkill.icon || '⚔️';
+ const iconHtml = renderSkillIcon(skillId, 'class', playerStats.class, 24);
const mpCost = baseSkill.mpCost || 0;
const isActive = activeSkills.includes(skillId);
@@ -13210,7 +13255,7 @@
Level ${tier}
- ${icon}
+ ${iconHtml}
${displayName}
${mpCost > 0 ? mpCost + ' MP' : 'Free'}
@@ -13233,7 +13278,7 @@
if (!baseSkill) return;
const displayName = skillInfo?.displayName || baseSkill.name;
- const icon = SKILLS[skillId]?.icon || baseSkill.icon || '⚔️';
+ const iconHtml = renderSkillIcon(skillId, 'class', playerStats.class, 24);
const mpCost = baseSkill.mpCost || 0;
const isActive = activeSkills.includes(skillId);
const canSwap = isAtHome && !isActive;
@@ -13242,7 +13287,7 @@
|