Release v0.2.10: Update package-lock.json and CI config

This commit is contained in:
Joan
2025-12-30 18:51:21 +01:00
parent 8b31011334
commit 592f38827e
108 changed files with 2755 additions and 1112 deletions

View File

@@ -922,13 +922,17 @@
</div>
<div class="property-group">
<label for="locationName">Name</label>
<input type="text" id="locationName" placeholder="Location Name">
<label>Name</label>
<div class="property-row">
<input type="text" id="locationName_en" placeholder="Name (EN)">
<input type="text" id="locationName_es" placeholder="Nombre (ES)">
</div>
</div>
<div class="property-group">
<label for="locationDescription">Description</label>
<textarea id="locationDescription" placeholder="Enter location description..."></textarea>
<label>Description</label>
<textarea id="locationDescription_en" placeholder="Description (EN)" style="margin-bottom: 5px; min-height: 60px;"></textarea>
<textarea id="locationDescription_es" placeholder="Descripción (ES)" style="min-height: 60px;"></textarea>
</div>
<div class="property-group">

View File

@@ -12,6 +12,14 @@ let liveStats = { players: {}, enemies: {} };
let canvas, ctx;
let currentTab = 'locations';
// Helper for i18n display
function getI18nDisplay(val) {
if (typeof val === 'object' && val !== null) {
return val.en || val.es || '';
}
return val || '';
}
// View state
let scale = 50;
let minScale = 10;
@@ -192,7 +200,8 @@ function renderLocationList() {
currentLocations.forEach(location => {
const item = document.createElement('div');
item.className = 'location-item';
item.dataset.name = location.name;
const displayName = getI18nDisplay(location.name);
item.dataset.name = displayName;
item.dataset.id = location.id;
if (location.id === selectedLocationId) {
item.classList.add('active');
@@ -202,7 +211,7 @@ function renderLocationList() {
const enemyCount = liveStats.enemies[location.id] || 0;
item.innerHTML = `
<div class="location-item-name">${location.name}</div>
<div class="location-item-name">${displayName}</div>
<div class="location-item-coords">📍 (${location.x}, ${location.y}) | Danger: ${location.danger_level}</div>
${playerCount > 0 || enemyCount > 0 ? `<div class="location-item-stats">👥 ${playerCount} | 👹 ${enemyCount}</div>` : ''}
`;
@@ -233,8 +242,26 @@ function populateForm(location) {
document.getElementById('propertiesForm').classList.remove('hidden');
document.getElementById('locationId').value = location.id;
document.getElementById('locationName').value = location.name;
document.getElementById('locationDescription').value = location.description;
// Handle i18n name
const name = location.name || '';
if (typeof name === 'object') {
document.getElementById('locationName_en').value = name.en || '';
document.getElementById('locationName_es').value = name.es || '';
} else {
document.getElementById('locationName_en').value = name;
document.getElementById('locationName_es').value = '';
}
// Handle i18n description
const desc = location.description || '';
if (typeof desc === 'object') {
document.getElementById('locationDescription_en').value = desc.en || '';
document.getElementById('locationDescription_es').value = desc.es || '';
} else {
document.getElementById('locationDescription_en').value = desc;
document.getElementById('locationDescription_es').value = '';
}
document.getElementById('locationX').value = location.x;
document.getElementById('locationY').value = location.y;
document.getElementById('dangerLevel').value = location.danger_level;
@@ -521,7 +548,7 @@ function drawLocations(centerX, centerY) {
ctx.fillStyle = '#e0e0e0';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(location.name, x, y + 30);
ctx.fillText(getI18nDisplay(location.name), x, y + 30);
// Draw live stats badges
const playerCount = liveStats.players[location.id] || 0;
@@ -713,8 +740,14 @@ function createNewLocation() {
async function saveLocation() {
const locationData = {
id: document.getElementById('locationId').value,
name: document.getElementById('locationName').value,
description: document.getElementById('locationDescription').value,
name: {
en: document.getElementById('locationName_en').value,
es: document.getElementById('locationName_es').value
},
description: {
en: document.getElementById('locationDescription_en').value,
es: document.getElementById('locationDescription_es').value
},
x: parseFloat(document.getElementById('locationX').value),
y: parseFloat(document.getElementById('locationY').value),
danger_level: parseInt(document.getElementById('dangerLevel').value),
@@ -1364,8 +1397,8 @@ function editInteractableInstance(instanceId, templateId) {
const editor = document.getElementById('interactableInstanceEditor');
editor.innerHTML = `
<div style="background: rgba(255,255,255,0.05); padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<h4 style="margin: 0 0 5px 0;">${template.name}</h4>
<p style="margin: 0; opacity: 0.7; font-size: 0.9em;">${template.description}</p>
<h4 style="margin: 0 0 5px 0;">${getI18nDisplay(template.name)}</h4>
<p style="margin: 0; opacity: 0.7; font-size: 0.9em;">${getI18nDisplay(template.description)}</p>
</div>
<div style="display: flex; flex-direction: column; gap: 20px;">
@@ -1448,7 +1481,7 @@ function editInteractableInstance(instanceId, templateId) {
<div id="item_rewards_${actionId}" style="margin-bottom: 10px;">
${(outcome.rewards?.items || []).map((reward, idx) => {
const selectedItem = availableItems.find(i => i.id === reward.item_id);
const displayValue = selectedItem ? `${selectedItem.emoji || '📦'} ${selectedItem.name}` : '';
const displayValue = selectedItem ? `${selectedItem.emoji || '📦'} ${getI18nDisplay(selectedItem.name)}` : '';
return `
<div class="reward-item" style="display: flex; gap: 10px; margin-bottom: 5px; align-items: center; position: relative;">
<div style="flex: 2; position: relative;">
@@ -1484,7 +1517,7 @@ function editInteractableInstance(instanceId, templateId) {
<div id="crit_item_rewards_${actionId}" style="margin-bottom: 10px;">
${(outcome.rewards?.crit_items || []).map((reward, idx) => {
const selectedItem = availableItems.find(i => i.id === reward.item_id);
const displayValue = selectedItem ? `${selectedItem.emoji || '📦'} ${selectedItem.name}` : '';
const displayValue = selectedItem ? `${selectedItem.emoji || '📦'} ${getI18nDisplay(selectedItem.name)}` : '';
return `
<div class="reward-item" style="display: flex; gap: 10px; margin-bottom: 5px; align-items: center; position: relative;">
<div style="flex: 2; position: relative;">
@@ -1590,7 +1623,7 @@ function filterItemDropdown(input) {
// Filter items
const filteredItems = availableItems.filter(item => {
const itemText = `${item.emoji || ''} ${item.name || ''} ${item.id}`.toLowerCase();
const itemText = `${item.emoji || ''} ${getI18nDisplay(item.name)} ${item.id}`.toLowerCase();
return itemText.includes(searchText);
});
@@ -1612,7 +1645,7 @@ function filterItemDropdown(input) {
optionDiv.style.borderBottom = '1px solid #2a2a4a';
optionDiv.innerHTML = `
${item.emoji || '📦'} ${item.name || item.id}
${item.emoji || '📦'} ${getI18nDisplay(item.name) || item.id}
<span style="opacity: 0.5; font-size: 0.85em; margin-left: 5px;">${item.id}</span>
`;
@@ -1628,7 +1661,7 @@ function filterItemDropdown(input) {
optionDiv.addEventListener('click', function (e) {
e.stopPropagation();
// Set the visible input to show emoji + name
input.value = `${item.emoji || '📦'} ${item.name || item.id}`;
input.value = `${item.emoji || '📦'} ${getI18nDisplay(item.name) || item.id}`;
// Set the hidden input to store the item_id
hiddenInput.value = item.id;
dropdown.style.display = 'none';
@@ -1674,7 +1707,7 @@ function filterMaterialDropdown(input, prefix) {
// Filter items
const filteredItems = availableItems.filter(item => {
const itemText = `${item.emoji || ''} ${item.name || ''} ${item.id}`.toLowerCase();
const itemText = `${item.emoji || ''} ${getI18nDisplay(item.name)} ${item.id}`.toLowerCase();
return itemText.includes(searchText);
});
@@ -1696,7 +1729,7 @@ function filterMaterialDropdown(input, prefix) {
optionDiv.style.borderBottom = '1px solid #2a2a4a';
optionDiv.innerHTML = `
${item.emoji || '📦'} ${item.name || item.id}
${item.emoji || '📦'} ${getI18nDisplay(item.name) || item.id}
<span style="opacity: 0.5; font-size: 0.85em; margin-left: 5px;">${item.id}</span>
`;
@@ -1711,7 +1744,7 @@ function filterMaterialDropdown(input, prefix) {
// Add click handler
optionDiv.addEventListener('click', function (e) {
e.stopPropagation();
input.value = `${item.emoji || '📦'} ${item.name || item.id}`;
input.value = `${item.emoji || '📦'} ${getI18nDisplay(item.name) || item.id}`;
hiddenInput.value = item.id;
dropdown.style.display = 'none';
});
@@ -1734,7 +1767,7 @@ function filterToolDropdown(input, prefix) {
// Filter items
const filteredItems = availableItems.filter(item => {
const itemText = `${item.emoji || ''} ${item.name || ''} ${item.id}`.toLowerCase();
const itemText = `${item.emoji || ''} ${getI18nDisplay(item.name)} ${item.id}`.toLowerCase();
return itemText.includes(searchText);
});
@@ -1756,7 +1789,7 @@ function filterToolDropdown(input, prefix) {
optionDiv.style.borderBottom = '1px solid #2a2a4a';
optionDiv.innerHTML = `
${item.emoji || '🔧'} ${item.name || item.id}
${item.emoji || '🔧'} ${getI18nDisplay(item.name) || item.id}
<span style="opacity: 0.5; font-size: 0.85em; margin-left: 5px;">${item.id}</span>
`;
@@ -1771,7 +1804,7 @@ function filterToolDropdown(input, prefix) {
// Add click handler
optionDiv.addEventListener('click', function (e) {
e.stopPropagation();
input.value = `${item.emoji || '🔧'} ${item.name || item.id}`;
input.value = `${item.emoji || '🔧'} ${getI18nDisplay(item.name) || item.id}`;
hiddenInput.value = item.id;
dropdown.style.display = 'none';
});
@@ -1932,14 +1965,14 @@ function renderInteractablesList(interactables) {
// Try to find template in available interactables
const template = availableInteractables.find(i => i.id === templateId);
if (template) {
displayName = template.name;
displayName = getI18nDisplay(template.name);
actionCount = Object.keys(template.actions || {}).length;
console.log(`Found template: ${displayName} with ${actionCount} actions`);
} else {
console.log(`Template ${templateId} not found in availableInteractables`);
// For new format instances that have full data
if (instance.name) {
displayName = instance.name;
displayName = getI18nDisplay(instance.name);
}
if (instance.actions) {
actionCount = Object.keys(instance.actions).length;
@@ -2327,14 +2360,15 @@ function renderNPCManagementList() {
availableNPCs.forEach(npc => {
const item = document.createElement('div');
item.className = 'management-item';
item.dataset.name = npc.name;
const displayName = getI18nDisplay(npc.name);
item.dataset.name = displayName;
item.dataset.id = npc.id;
if (npc.id === selectedNPCId) {
item.classList.add('active');
}
item.innerHTML = `
<div><strong>${npc.emoji} ${npc.name}</strong></div>
<div><strong>${npc.emoji} ${displayName}</strong></div>
<div style="font-size: 0.85em; opacity: 0.7; margin-top: 5px;">
HP: ${npc.hp_min}-${npc.hp_max} | DMG: ${npc.damage_min}-${npc.damage_max}
</div>
@@ -2374,7 +2408,10 @@ function selectNPC(npcId) {
</div>
<div class="property-group">
<label>Name</label>
<input type="text" id="npcName" value="${npc.name}">
<div class="property-row">
<input type="text" id="npcName_en" value="${typeof npc.name === 'object' ? npc.name.en || '' : npc.name || ''}" placeholder="Name (EN)">
<input type="text" id="npcName_es" value="${typeof npc.name === 'object' ? npc.name.es || '' : ''}" placeholder="Nombre (ES)">
</div>
</div>
<div class="property-group">
<label>Emoji</label>
@@ -2417,7 +2454,8 @@ function selectNPC(npcId) {
<div class="property-group">
<label>Description</label>
<textarea id="npcDescription" rows="3" style="width: 100%;">${npc.description || ''}</textarea>
<textarea id="npcDescription_en" rows="3" placeholder="Description (EN)" style="width: 100%; margin-bottom: 5px;">${typeof npc.description === 'object' ? npc.description.en || '' : npc.description || ''}</textarea>
<textarea id="npcDescription_es" rows="3" placeholder="Descripción (ES)" style="width: 100%;">${typeof npc.description === 'object' ? npc.description.es || '' : ''}</textarea>
</div>
<div class="property-group">
@@ -2587,7 +2625,10 @@ async function saveCurrentNPC() {
const npcData = {
id: document.getElementById('npcId').value,
name: document.getElementById('npcName').value,
name: {
en: document.getElementById('npcName_en').value,
es: document.getElementById('npcName_es').value
},
emoji: document.getElementById('npcEmoji').value,
hp_min: parseInt(document.getElementById('npcHpMin').value),
hp_max: parseInt(document.getElementById('npcHpMax').value),
@@ -2595,7 +2636,10 @@ async function saveCurrentNPC() {
damage_max: parseInt(document.getElementById('npcDamageMax').value),
xp_reward: parseInt(document.getElementById('npcXp').value),
defense: parseInt(document.getElementById('npcDefense').value),
description: document.getElementById('npcDescription').value,
description: {
en: document.getElementById('npcDescription_en').value,
es: document.getElementById('npcDescription_es').value
},
image_path: document.getElementById('npcImagePath').value,
loot_table: getNPCLootTable(),
corpse_loot: getNPCCorpseLoot()
@@ -2685,7 +2729,7 @@ function filterNPCLootItemDropdown(index) {
const filtered = availableItems.filter(item =>
item.id.toLowerCase().includes(searchTerm) ||
(item.name && item.name.toLowerCase().includes(searchTerm)) ||
(getI18nDisplay(item.name).toLowerCase().includes(searchTerm)) ||
(item.emoji && item.emoji.includes(searchTerm))
);
@@ -2700,7 +2744,7 @@ function filterNPCLootItemDropdown(index) {
option.style.padding = '8px';
option.style.cursor = 'pointer';
option.style.borderBottom = '1px solid #2a2a4a';
option.innerHTML = `${item.emoji || '📦'} ${item.name || item.id} <span style="opacity: 0.6; font-size: 0.9em;">(${item.id})</span>`;
option.innerHTML = `${item.emoji || '📦'} ${getI18nDisplay(item.name) || item.id} <span style="opacity: 0.6; font-size: 0.9em;">(${item.id})</span>`;
// Add hover effects
option.addEventListener('mouseover', function () {
@@ -2732,7 +2776,7 @@ function filterNPCCorpseItemDropdown(index) {
const filtered = availableItems.filter(item =>
item.id.toLowerCase().includes(searchTerm) ||
(item.name && item.name.toLowerCase().includes(searchTerm)) ||
(getI18nDisplay(item.name).toLowerCase().includes(searchTerm)) ||
(item.emoji && item.emoji.includes(searchTerm))
);
@@ -2747,7 +2791,7 @@ function filterNPCCorpseItemDropdown(index) {
option.style.padding = '8px';
option.style.cursor = 'pointer';
option.style.borderBottom = '1px solid #2a2a4a';
option.innerHTML = `${item.emoji || '📦'} ${item.name || item.id} <span style="opacity: 0.6; font-size: 0.9em;">(${item.id})</span>`;
option.innerHTML = `${item.emoji || '📦'} ${getI18nDisplay(item.name) || item.id} <span style="opacity: 0.6; font-size: 0.9em;">(${item.id})</span>`;
// Add hover effects
option.addEventListener('mouseover', function () {
@@ -2858,7 +2902,8 @@ function renderItemManagementList() {
availableItems.forEach(item => {
const elem = document.createElement('div');
elem.className = 'management-item';
elem.dataset.name = item.name;
const displayName = getI18nDisplay(item.name);
elem.dataset.name = displayName;
elem.dataset.id = item.id;
elem.dataset.type = item.type || '';
if (item.id === selectedItemId) {
@@ -2866,7 +2911,7 @@ function renderItemManagementList() {
}
elem.innerHTML = `
<div><strong>${item.emoji ? item.emoji + ' ' : ''}${item.name}</strong></div>
<div><strong>${item.emoji ? item.emoji + ' ' : ''}${displayName}</strong></div>
<div style="font-size: 0.85em; opacity: 0.7; margin-top: 5px;">
Type: ${item.type} | Weight: ${item.weight}kg
</div>
@@ -2913,7 +2958,10 @@ function selectItem(itemId) {
</div>
<div class="property-group">
<label>Name</label>
<input type="text" id="itemName" value="${item.name}">
<div class="property-row">
<input type="text" id="itemName_en" value="${typeof item.name === 'object' ? item.name.en || '' : item.name || ''}" placeholder="Name (EN)">
<input type="text" id="itemName_es" value="${typeof item.name === 'object' ? item.name.es || '' : ''}" placeholder="Nombre (ES)">
</div>
</div>
<div class="property-group">
<label>Emoji</label>
@@ -2947,7 +2995,8 @@ function selectItem(itemId) {
<div class="property-group">
<label>Description</label>
<textarea id="itemDescription" rows="3" style="width: 100%;">${item.description || ''}</textarea>
<textarea id="itemDescription_en" rows="3" placeholder="Description (EN)" style="width: 100%; margin-bottom: 5px;">${typeof item.description === 'object' ? item.description.en || '' : item.description || ''}</textarea>
<textarea id="itemDescription_es" rows="3" placeholder="Descripción (ES)" style="width: 100%;">${typeof item.description === 'object' ? item.description.es || '' : ''}</textarea>
</div>
<div class="property-group">
@@ -3204,7 +3253,8 @@ function renderMaterialsList(materials, prefix) {
return materials.map((mat, index) => {
const selectedItem = availableItems.find(i => i.id === mat.item_id);
const displayValue = selectedItem ? `${selectedItem.emoji || '📦'} ${selectedItem.name}` : '';
const displayValue = selectedItem ? `${selectedItem.emoji || '📦'} ${getI18nDisplay(selectedItem.name)}` : '';
return `
<div class="material-item" style="display: flex; gap: 10px; margin-bottom: 10px; align-items: center; background: #1a1a3e; padding: 10px; border-radius: 4px;">
@@ -3233,7 +3283,7 @@ function renderToolsList(tools, prefix) {
return tools.map((tool, index) => {
const selectedItem = availableItems.find(i => i.id === tool.item_id);
const displayValue = selectedItem ? `${selectedItem.emoji || '🔧'} ${selectedItem.name}` : '';
const displayValue = selectedItem ? `${selectedItem.emoji || '🔧'} ${getI18nDisplay(selectedItem.name)}` : '';
return `
<div class="tool-item" style="display: flex; gap: 10px; margin-bottom: 10px; align-items: center; background: #1a1a3e; padding: 10px; border-radius: 4px;">
@@ -3327,12 +3377,18 @@ async function saveCurrentItem_() {
// Build complete item data object
const itemData = {
id: document.getElementById('itemId').value,
name: document.getElementById('itemName').value,
name: {
en: document.getElementById('itemName_en').value,
es: document.getElementById('itemName_es').value
},
emoji: document.getElementById('itemEmoji').value,
type: document.getElementById('itemType').value,
weight: parseFloat(document.getElementById('itemWeight').value),
volume: parseFloat(document.getElementById('itemVolume').value),
description: document.getElementById('itemDescription').value,
description: {
en: document.getElementById('itemDescription_en').value,
es: document.getElementById('itemDescription_es').value
},
image_path: document.getElementById('itemImagePath')?.value || '',
stackable: document.getElementById('itemStackable').checked,