@ -580,15 +580,102 @@
}
}
.monster-skill-item .skill-animation {
.monster-skill-item .skill-animation {
width: 10 0px;
width: 9 0px;
padding: 4px;
padding: 4px;
font-size: 12 px;
font-size: 11 px;
background: rgba(255,255,255,0.1);
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
border-radius: 4px;
color: inherit;
color: inherit;
}
}
.skill-animation-section {
display: flex;
flex-direction: column;
gap: 2px;
}
.skill-animation-row {
display: flex;
gap: 4px;
align-items: center;
}
.skill-animation-row select {
flex: 1;
}
.anim-test-btn {
width: 24px;
height: 24px;
padding: 0;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 4px;
background: rgba(100,200,100,0.2);
color: #8f8;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.anim-test-btn:hover {
background: rgba(100,200,100,0.4);
}
/* Animation tester panel */
.animation-tester {
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.animation-tester h4 {
margin: 0 0 10px 0;
color: #8cf;
}
.animation-tester-content {
display: flex;
gap: 20px;
align-items: center;
}
.animation-preview {
width: 64px;
height: 64px;
background: rgba(255,255,255,0.1);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.animation-preview img {
width: 48px;
height: 48px;
object-fit: contain;
}
.animation-tester-controls {
display: flex;
flex-direction: column;
gap: 8px;
}
.animation-tester-controls select {
padding: 6px 10px;
min-width: 150px;
}
.animation-tester-controls button {
padding: 6px 15px;
}
.monster-skill-item label {
.monster-skill-item label {
font-size: 11px;
font-size: 11px;
color: #888;
color: #888;
@ -1322,6 +1409,12 @@
<!-- Populated dynamically -->
<!-- Populated dynamically -->
< / select >
< / select >
< / div >
< / div >
< div class = "form-group" >
< label > Miss Animation< / label >
< select id = "monsterMissAnim" >
<!-- Populated dynamically -->
< / select >
< / div >
< / div >
< / div >
< h4 > Animation Preview< / h4 >
< h4 > Animation Preview< / h4 >
@ -1506,6 +1599,33 @@
< / div >
< / div >
< / div >
< / div >
<!-- Combat Buff Configuration (hidden by default, shown for buff type) -->
< div class = "status-effect-section" id = "buffConfigSection" style = "display: none;" >
< h4 > Combat Buff Configuration< / h4 >
< p style = "font-size: 11px; color: #666; margin-bottom: 10px;" >
Configure the in-combat buff effect (e.g., Defense Up, Accuracy Up)
< / p >
< div class = "form-row-3" >
< div class = "form-group" >
< label > Buff Type< / label >
< select id = "buffEffectType" >
< option value = "defense_up" > Defense Up< / option >
< option value = "accuracy_up" > Accuracy Up< / option >
< option value = "attack_up" > Attack Up< / option >
< option value = "dodge_up" > Dodge Up< / option >
< / select >
< / div >
< div class = "form-group" >
< label > Boost (%)< / label >
< input type = "number" id = "buffEffectPercent" value = "20" min = "1" max = "200" >
< / div >
< div class = "form-group" >
< label > Duration (turns)< / label >
< input type = "number" id = "buffEffectDuration" value = "3" min = "1" max = "10" >
< / div >
< / div >
< / div >
<!-- Utility Skill Configuration (hidden by default) -->
<!-- Utility Skill Configuration (hidden by default) -->
< div class = "status-effect-section" id = "utilityConfigSection" style = "display: none;" >
< div class = "status-effect-section" id = "utilityConfigSection" style = "display: none;" >
< h4 > Utility Skill Configuration< / h4 >
< h4 > Utility Skill Configuration< / h4 >
@ -1525,6 +1645,7 @@
< option value = "xp_multiplier" > XP Multiplier< / option >
< option value = "xp_multiplier" > XP Multiplier< / option >
< option value = "explore_radius_multiplier" > Explore Radius Multiplier< / option >
< option value = "explore_radius_multiplier" > Explore Radius Multiplier< / option >
< option value = "homebase_radius_multiplier" > Homebase Radius Multiplier< / option >
< option value = "homebase_radius_multiplier" > Homebase Radius Multiplier< / option >
< option value = "wander_range_multiplier" > Wander Range Multiplier< / option >
< / select >
< / select >
< / div >
< / div >
< div class = "form-group" >
< div class = "form-group" >
@ -1672,6 +1793,23 @@
Assign skills to this class. Set unlock level and choice group for skill selection at level-up.
Assign skills to this class. Set unlock level and choice group for skill selection at level-up.
< br > Skills with the same choice group at the same level = player picks one.
< br > Skills with the same choice group at the same level = player picks one.
< / p >
< / p >
<!-- Animation Tester -->
< div class = "animation-tester" >
< h4 > 🎬 Animation Tester< / h4 >
< div class = "animation-tester-content" >
< div class = "animation-preview" id = "animationPreview" >
< img src = "/mapgameimgs/player/runner.png" id = "animationPreviewImg" >
< / div >
< div class = "animation-tester-controls" >
< select id = "animationTesterSelect" >
< option value = "" > -- Select Animation --< / option >
< / select >
< button type = "button" class = "btn btn-primary" onclick = "testAnimation()" > ▶ Play< / button >
< / div >
< / div >
< / div >
< div id = "classSkillsList" > < / div >
< div id = "classSkillsList" > < / div >
< div class = "add-skill-row" >
< div class = "add-skill-row" >
< select id = "addClassSkillSelect" >
< select id = "addClassSkillSelect" >
@ -2111,6 +2249,7 @@
document.getElementById('monsterAttackAnim').value = monster.attack_animation || 'attack';
document.getElementById('monsterAttackAnim').value = monster.attack_animation || 'attack';
document.getElementById('monsterDeathAnim').value = monster.death_animation || 'death';
document.getElementById('monsterDeathAnim').value = monster.death_animation || 'death';
document.getElementById('monsterIdleAnim').value = monster.idle_animation || 'idle';
document.getElementById('monsterIdleAnim').value = monster.idle_animation || 'idle';
document.getElementById('monsterMissAnim').value = monster.miss_animation || 'miss';
// Update preview icon
// Update preview icon
document.getElementById('animPreviewIcon').src = `/mapgameimgs/monsters/${monster.key}100.png`;
document.getElementById('animPreviewIcon').src = `/mapgameimgs/monsters/${monster.key}100.png`;
@ -2164,6 +2303,7 @@
document.getElementById('monsterAttackAnim').value = monster.attack_animation || 'attack';
document.getElementById('monsterAttackAnim').value = monster.attack_animation || 'attack';
document.getElementById('monsterDeathAnim').value = monster.death_animation || 'death';
document.getElementById('monsterDeathAnim').value = monster.death_animation || 'death';
document.getElementById('monsterIdleAnim').value = monster.idle_animation || 'idle';
document.getElementById('monsterIdleAnim').value = monster.idle_animation || 'idle';
document.getElementById('monsterMissAnim').value = monster.miss_animation || 'miss';
document.getElementById('animPreviewIcon').src = '/mapgameimgs/monsters/default100.png';
document.getElementById('animPreviewIcon').src = '/mapgameimgs/monsters/default100.png';
// Clear skills (cloned monster needs to be saved first)
// Clear skills (cloned monster needs to be saved first)
@ -2183,6 +2323,7 @@
document.getElementById('monsterAttackAnim').value = 'attack';
document.getElementById('monsterAttackAnim').value = 'attack';
document.getElementById('monsterDeathAnim').value = 'death';
document.getElementById('monsterDeathAnim').value = 'death';
document.getElementById('monsterIdleAnim').value = 'idle';
document.getElementById('monsterIdleAnim').value = 'idle';
document.getElementById('monsterMissAnim').value = 'miss';
document.getElementById('animPreviewIcon').src = '/mapgameimgs/monsters/default100.png';
document.getElementById('animPreviewIcon').src = '/mapgameimgs/monsters/default100.png';
// Clear skills (new monster needs to be saved first)
// Clear skills (new monster needs to be saved first)
currentMonsterSkills = [];
currentMonsterSkills = [];
@ -2199,7 +2340,7 @@
const animations = typeof MONSTER_ANIMATIONS !== 'undefined' ? MONSTER_ANIMATIONS : {};
const animations = typeof MONSTER_ANIMATIONS !== 'undefined' ? MONSTER_ANIMATIONS : {};
const animIds = Object.keys(animations);
const animIds = Object.keys(animations);
const dropdowns = ['monsterAttackAnim', 'monsterDeathAnim', 'monsterIdleAnim', 'testAnimationSelect'];
const dropdowns = ['monsterAttackAnim', 'monsterDeathAnim', 'monsterIdleAnim', 'monsterMissAnim', ' testAnimationSelect'];
dropdowns.forEach(dropdownId => {
dropdowns.forEach(dropdownId => {
const dropdown = document.getElementById(dropdownId);
const dropdown = document.getElementById(dropdownId);
@ -2227,6 +2368,119 @@
}).join('');
}).join('');
}
}
// Get player animation options HTML (for class skill dropdowns)
function getPlayerAnimationOptionsHtml(selectedValue) {
let options = '< option value = "" > Default< / option > ';
// Add player animations first (if available)
if (typeof PLAYER_ANIMATIONS !== 'undefined') {
options += '< optgroup label = "Player Animations" > ';
for (const [id, anim] of Object.entries(PLAYER_ANIMATIONS)) {
const selected = selectedValue === id ? 'selected' : '';
options += `< option value = "${id}" $ { selected } > ${anim.name}< / option > `;
}
options += '< / optgroup > ';
}
// Add monster animations (player can use these too)
if (typeof MONSTER_ANIMATIONS !== 'undefined') {
options += '< optgroup label = "Monster Animations" > ';
for (const [id, anim] of Object.entries(MONSTER_ANIMATIONS)) {
const selected = selectedValue === id ? 'selected' : '';
options += `< option value = "${id}" $ { selected } > ${anim.name}< / option > `;
}
options += '< / optgroup > ';
}
return options;
}
// Initialize animation tester dropdown
function initAnimationTester() {
const select = document.getElementById('animationTesterSelect');
if (!select) return;
select.innerHTML = getPlayerAnimationOptionsHtml('');
}
// Test animation from the main tester panel
function testAnimation() {
const select = document.getElementById('animationTesterSelect');
const animationId = select.value;
if (!animationId) {
showToast('Select an animation first', 'error');
return;
}
playAnimationPreview(animationId, 'animationPreviewImg');
}
// Test animation for a specific skill
function testSkillAnimation(classSkillId) {
const select = document.getElementById(`anim-select-${classSkillId}`);
let animationId = select ? select.value : '';
// If no animation selected, use default attack
if (!animationId) {
animationId = 'attack';
}
playAnimationPreview(animationId, 'animationPreviewImg');
}
// Play animation on the preview element
function playAnimationPreview(animationId, imgElementId) {
const img = document.getElementById(imgElementId);
if (!img) return;
// Get animation definition
let anim = null;
let prefix = 'player';
if (typeof PLAYER_ANIMATIONS !== 'undefined' & & PLAYER_ANIMATIONS[animationId]) {
anim = PLAYER_ANIMATIONS[animationId];
prefix = 'player';
} else if (typeof MONSTER_ANIMATIONS !== 'undefined' & & MONSTER_ANIMATIONS[animationId]) {
anim = MONSTER_ANIMATIONS[animationId];
prefix = 'monster';
}
if (!anim) {
showToast('Animation not found', 'error');
return;
}
// Inject keyframes if not already present
injectAnimationKeyframes(animationId, anim, prefix);
// Reset and play animation
const loopStr = anim.loop ? ' infinite' : '';
const fillStr = anim.fillMode ? ` ${anim.fillMode}` : '';
const easing = anim.easing || 'ease-out';
img.style.animation = 'none';
img.offsetHeight; // Force reflow
img.style.animation = `${prefix}_${animationId} ${anim.duration}ms ${easing}${loopStr}${fillStr}`;
// Remove animation after it completes (unless looping)
if (!anim.loop) {
setTimeout(() => {
img.style.animation = '';
}, anim.duration);
}
}
// Inject CSS keyframes for an animation if not already present
function injectAnimationKeyframes(animationId, anim, prefix) {
const animName = `${prefix}_${animationId}`;
// Check if already injected
if (document.getElementById(`keyframe-${animName}`)) return;
const style = document.createElement('style');
style.id = `keyframe-${animName}`;
style.textContent = `@keyframes ${animName} { ${anim.keyframes} }`;
document.head.appendChild(style);
}
// Update artwork preview in OSM tag modal
// Update artwork preview in OSM tag modal
function updateArtworkPreview() {
function updateArtworkPreview() {
const artworkNum = parseInt(document.getElementById('osmTagArtwork').value) || 1;
const artworkNum = parseInt(document.getElementById('osmTagArtwork').value) || 1;
@ -2322,6 +2576,7 @@
attack_animation: document.getElementById('monsterAttackAnim').value,
attack_animation: document.getElementById('monsterAttackAnim').value,
death_animation: document.getElementById('monsterDeathAnim').value,
death_animation: document.getElementById('monsterDeathAnim').value,
idle_animation: document.getElementById('monsterIdleAnim').value,
idle_animation: document.getElementById('monsterIdleAnim').value,
miss_animation: document.getElementById('monsterMissAnim').value,
dialogues: JSON.stringify(dialogues)
dialogues: JSON.stringify(dialogues)
};
};
@ -2424,7 +2679,8 @@
'def_boost_percent': 'DEF %',
'def_boost_percent': 'DEF %',
'xp_multiplier': 'XP',
'xp_multiplier': 'XP',
'explore_radius_multiplier': 'Explore Radius',
'explore_radius_multiplier': 'Explore Radius',
'homebase_radius_multiplier': 'Homebase Radius'
'homebase_radius_multiplier': 'Homebase Radius',
'wander_range_multiplier': 'Wander Range'
};
};
tbody.innerHTML = utilitySkills.map(s => {
tbody.innerHTML = utilitySkills.map(s => {
@ -2465,19 +2721,29 @@
function handleSkillTypeChange() {
function handleSkillTypeChange() {
const skillType = document.getElementById('skillType').value;
const skillType = document.getElementById('skillType').value;
const utilitySection = document.getElementById('utilityConfigSection');
const utilitySection = document.getElementById('utilityConfigSection');
const statusEffectSection = document.querySelector('.status-effect-section:not(#utilityConfigSection)');
const buffSection = document.getElementById('buffConfigSection');
const statusEffectSection = document.querySelector('.status-effect-section:not(#utilityConfigSection):not(#buffConfigSection)');
// Hide all special sections first
utilitySection.style.display = 'none';
buffSection.style.display = 'none';
if (statusEffectSection) statusEffectSection.style.display = 'none';
if (skillType === 'utility') {
if (skillType === 'utility') {
utilitySection.style.display = 'block';
utilitySection.style.display = 'block';
if (statusEffectSection) statusEffectSection.style.display = 'none';
// Auto-set defaults for utility skills
// Auto-set defaults for utility skills
document.getElementById('skillPlayerUsable').checked = false;
document.getElementById('skillPlayerUsable').checked = false;
document.getElementById('skillMonsterUsable').checked = false;
document.getElementById('skillMonsterUsable').checked = false;
document.getElementById('skillTarget').value = 'self';
document.getElementById('skillTarget').value = 'self';
document.getElementById('skillMpCost').value = '0';
document.getElementById('skillMpCost').value = '0';
document.getElementById('skillBasePower').value = '0';
document.getElementById('skillBasePower').value = '0';
} else if (skillType === 'buff') {
buffSection.style.display = 'block';
// Auto-set defaults for buff skills
document.getElementById('skillTarget').value = 'self';
document.getElementById('skillBasePower').value = '0';
} else {
} else {
utilitySection.style.display = 'none';
// Show status effect section for damage/heal/status/debuff types
if (statusEffectSection) statusEffectSection.style.display = 'block';
if (statusEffectSection) statusEffectSection.style.display = 'block';
}
}
}
}
@ -2543,31 +2809,51 @@
document.getElementById('utilityEffectValue').value = effect.effectValue || 2.0;
document.getElementById('utilityEffectValue').value = effect.effectValue || 2.0;
document.getElementById('utilityDurationHours').value = effect.durationHours || 1;
document.getElementById('utilityDurationHours').value = effect.durationHours || 1;
document.getElementById('utilityCooldownHours').value = effect.cooldownHours || 24;
document.getElementById('utilityCooldownHours').value = effect.cooldownHours || 24;
// Reset combat status effect fields
// Reset other fields
document.getElementById('skillStatusType').value = '';
document.getElementById('buffEffectType').value = 'defense_up';
document.getElementById('buffEffectPercent').value = 20;
document.getElementById('buffEffectDuration').value = 3;
} else if (skill.type === 'buff') {
// Combat buff config (defense_up, accuracy_up, etc.)
document.getElementById('buffEffectType').value = effect.type || 'defense_up';
document.getElementById('buffEffectPercent').value = effect.percent || 20;
document.getElementById('buffEffectDuration').value = effect.duration || 3;
// Reset other fields
document.getElementById('skillStatusType').value = '';
document.getElementById('skillStatusType').value = '';
document.getElementById('utilityEffectType').value = 'mp_regen_multiplier';
document.getElementById('utilityEffectValue').value = 2.0;
document.getElementById('utilityDurationHours').value = 1;
document.getElementById('utilityCooldownHours').value = 24;
} else {
} else {
// Combat status effect
// Combat status effect (poison, burn, etc.)
document.getElementById('skillStatusType').value = effect.type || '';
document.getElementById('skillStatusType').value = effect.type || '';
document.getElementById('skillStatusDamage').value = effect.damage || 5;
document.getElementById('skillStatusDamage').value = effect.damage || 5;
document.getElementById('skillStatusDuration').value = effect.duration || 3;
document.getElementById('skillStatusDuration').value = effect.duration || 3;
// Reset utility fields
// Reset other fields
document.getElementById('utilityEffectType').value = 'mp_regen_multiplier';
document.getElementById('utilityEffectType').value = 'mp_regen_multiplier';
document.getElementById('utilityEffectValue').value = 2.0;
document.getElementById('utilityEffectValue').value = 2.0;
document.getElementById('utilityDurationHours').value = 1;
document.getElementById('utilityDurationHours').value = 1;
document.getElementById('utilityCooldownHours').value = 24;
document.getElementById('utilityCooldownHours').value = 24;
document.getElementById('buffEffectType').value = 'defense_up';
document.getElementById('buffEffectPercent').value = 20;
document.getElementById('buffEffectDuration').value = 3;
}
}
} catch {
} catch {
document.getElementById('skillStatusType').value = '';
document.getElementById('skillStatusType').value = '';
}
}
} else {
} else {
// Reset all effect fields to defaults
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('utilityEffectType').value = 'mp_regen_multiplier';
document.getElementById('utilityEffectValue').value = 2.0;
document.getElementById('utilityEffectValue').value = 2.0;
document.getElementById('utilityDurationHours').value = 1;
document.getElementById('utilityDurationHours').value = 1;
document.getElementById('utilityCooldownHours').value = 24;
document.getElementById('utilityCooldownHours').value = 24;
document.getElementById('buffEffectType').value = 'defense_up';
document.getElementById('buffEffectPercent').value = 20;
document.getElementById('buffEffectDuration').value = 3;
}
}
// Toggle visibility of form sections based on skill type
// Toggle visibility of form sections based on skill type
@ -2782,8 +3068,15 @@
durationHours: parseFloat(document.getElementById('utilityDurationHours').value) || 1,
durationHours: parseFloat(document.getElementById('utilityDurationHours').value) || 1,
cooldownHours: parseFloat(document.getElementById('utilityCooldownHours').value) || 24
cooldownHours: parseFloat(document.getElementById('utilityCooldownHours').value) || 24
});
});
} else if (skillType === 'buff') {
// Combat buff skill - use buff config fields
statusEffect = JSON.stringify({
type: document.getElementById('buffEffectType').value,
percent: parseInt(document.getElementById('buffEffectPercent').value) || 20,
duration: parseInt(document.getElementById('buffEffectDuration').value) || 3
});
} else {
} else {
// Combat skill - use status effect fields
// Combat skill - use status effect fields (poison, burn, etc.)
const statusType = document.getElementById('skillStatusType').value;
const statusType = document.getElementById('skillStatusType').value;
if (statusType) {
if (statusType) {
statusEffect = JSON.stringify({
statusEffect = JSON.stringify({
@ -2954,6 +3247,9 @@
// Populate skill dropdown for adding
// Populate skill dropdown for adding
populateClassSkillSelect();
populateClassSkillSelect();
// Initialize animation tester
initAnimationTester();
// Load class skills
// Load class skills
await loadClassSkills(classData.id);
await loadClassSkills(classData.id);
@ -2969,6 +3265,7 @@
currentClassSkills = [];
currentClassSkills = [];
document.getElementById('classSkillsList').innerHTML = '< p style = "color: #666; font-size: 12px;" > Save class first, then edit to add skills.< / p > ';
document.getElementById('classSkillsList').innerHTML = '< p style = "color: #666; font-size: 12px;" > Save class first, then edit to add skills.< / p > ';
populateClassSkillSelect();
populateClassSkillSelect();
initAnimationTester();
document.getElementById('classModal').classList.add('active');
document.getElementById('classModal').classList.add('active');
});
});
@ -3110,6 +3407,15 @@
onchange="updateClassSkill(${cs.id}, 'choice_group', this.value || null)"
onchange="updateClassSkill(${cs.id}, 'choice_group', this.value || null)"
placeholder="-" title="Choice group (empty = auto-learn)">
placeholder="-" title="Choice group (empty = auto-learn)">
< / div >
< / div >
< div class = "skill-animation-section" >
< label > Anim< / label >
< div class = "skill-animation-row" >
< select class = "skill-animation" id = "anim-select-${cs.id}" onchange = "updateClassSkill(${cs.id}, 'player_animation', this.value || null)" title = "Player animation override" >
${getPlayerAnimationOptionsHtml(cs.player_animation)}
< / select >
< button type = "button" class = "anim-test-btn" onclick = "testSkillAnimation(${cs.id})" title = "Test animation" > ▶< / button >
< / div >
< / div >
< button type = "button" class = "btn btn-danger btn-small" onclick = "removeClassSkill(${cs.id})" > ✕< / button >
< button type = "button" class = "btn btn-danger btn-small" onclick = "removeClassSkill(${cs.id})" > ✕< / button >
< / div >
< / div >
`;
`;