2025-10-15 12:27:24 -05:00
|
|
|
<div class="p-4 bg-[#071323] rounded">
|
|
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
|
|
<h2 class="text-white text-lg font-semibold">Items</h2>
|
|
|
|
|
<div>
|
|
|
|
|
<button id="createItemBtn" class="inline-flex items-center gap-2 px-3 py-1 bg-indigo-600 rounded text-white">Crear item</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="itemsList" class="space-y-3">
|
|
|
|
|
<% if (typeof guildRoles !== 'undefined') { %>
|
|
|
|
|
<!-- placeholder: server will not inject items; client will fetch -->
|
|
|
|
|
<% } %>
|
|
|
|
|
<div class="text-white/60">Cargando items...</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Create / Edit modal -->
|
|
|
|
|
<div id="itemModal" class="fixed inset-0 hidden items-center justify-center z-50">
|
|
|
|
|
<div class="absolute inset-0 bg-black/70"></div>
|
|
|
|
|
<div class="relative w-full max-w-3xl bg-[#0b1320] rounded shadow p-6 z-10">
|
|
|
|
|
<h3 id="modalTitle" class="text-white text-lg mb-2">Crear item</h3>
|
2025-10-15 13:16:37 -05:00
|
|
|
<div id="modalError" class="hidden text-red-400 text-sm mb-2"></div>
|
2025-10-15 12:27:24 -05:00
|
|
|
<form id="itemForm">
|
|
|
|
|
<input type="hidden" name="id" id="itemId">
|
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/80">Key</label>
|
|
|
|
|
<input id="fieldKey" name="key" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/80">Name</label>
|
|
|
|
|
<input id="fieldName" name="name" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/80">Category</label>
|
|
|
|
|
<input id="fieldCategory" name="category" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/80">Icon</label>
|
|
|
|
|
<input id="fieldIcon" name="icon" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-span-2">
|
|
|
|
|
<label class="text-white/80">Description</label>
|
|
|
|
|
<textarea id="fieldDescription" name="description" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" rows="3"></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/80">Tags (comma separated)</label>
|
|
|
|
|
<input id="fieldTags" name="tags" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="text-white/80">Max Per Inventory</label>
|
|
|
|
|
<input id="fieldMaxPer" name="maxPerInventory" type="number" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-span-2">
|
2025-10-15 12:57:35 -05:00
|
|
|
<label class="text-white/80">Props (UI)</label>
|
|
|
|
|
<div id="propsUI" class="w-full mt-1 p-3 rounded bg-[#0f1720] text-white space-y-2">
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<input type="checkbox" id="propCraftable" />
|
|
|
|
|
<label for="propCraftable" class="text-white/80">Craftable</label>
|
|
|
|
|
<input id="propRecipeKey" placeholder="recipe.key (opcional)" class="ml-3 p-1 rounded bg-[#0f1620] text-white text-sm" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<input type="checkbox" id="propEquipable" />
|
|
|
|
|
<label for="propEquipable" class="text-white/80">Equipable</label>
|
|
|
|
|
<label class="text-white/80 ml-3">Slot</label>
|
|
|
|
|
<select id="propSlot" class="p-1 rounded bg-[#0f1620] text-white text-sm">
|
|
|
|
|
<option value="">--</option>
|
|
|
|
|
<option value="head">head</option>
|
|
|
|
|
<option value="body">body</option>
|
|
|
|
|
<option value="legs">legs</option>
|
|
|
|
|
<option value="hand">hand</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<label class="text-white/80">Attack</label>
|
|
|
|
|
<input id="propAttack" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
<label class="text-white/80 ml-3">Defense</label>
|
|
|
|
|
<input id="propDefense" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<label class="text-white/80">Durability</label>
|
|
|
|
|
<input id="propDurability" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
<label class="text-white/80 ml-3">Max Durability</label>
|
|
|
|
|
<input id="propMaxDurability" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<label class="text-white/80">Custom JSON (for advanced)</label>
|
|
|
|
|
<textarea id="propCustomJson" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white font-mono" rows="3">{}</textarea>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="border-t border-white/6 pt-2">
|
|
|
|
|
<h4 class="text-white/80 text-sm mb-2">Tool</h4>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<label class="text-white/80">Type</label>
|
|
|
|
|
<input id="propToolType" class="p-1 rounded bg-[#0f1620] text-white text-sm" placeholder="pickaxe/sword/etc" />
|
|
|
|
|
<label class="text-white/80 ml-3">Tier</label>
|
|
|
|
|
<input id="propToolTier" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="border-t border-white/6 pt-2">
|
|
|
|
|
<h4 class="text-white/80 text-sm mb-2">Breakable / Durability</h4>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<input type="checkbox" id="propBreakable" />
|
|
|
|
|
<label for="propBreakable" class="text-white/80">Breakable</label>
|
|
|
|
|
<label class="text-white/80 ml-3">Durability per use</label>
|
|
|
|
|
<input id="propDurabilityPerUse" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="border-t border-white/6 pt-2">
|
|
|
|
|
<h4 class="text-white/80 text-sm mb-2">Consumable / Usable</h4>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<input type="checkbox" id="propUsable" />
|
|
|
|
|
<label for="propUsable" class="text-white/80">Usable</label>
|
|
|
|
|
<input type="checkbox" id="propPurgeAllEffects" />
|
|
|
|
|
<label for="propPurgeAllEffects" class="text-white/80">Purge all effects</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-3 mt-2">
|
|
|
|
|
<label class="text-white/80">Heal amount</label>
|
|
|
|
|
<input id="propHealAmount" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="border-t border-white/6 pt-2">
|
|
|
|
|
<h4 class="text-white/80 text-sm mb-2">Damage / Stats / Bonuses</h4>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<label class="text-white/80">Damage</label>
|
|
|
|
|
<input id="propDamage" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
<label class="text-white/80 ml-3">Damage Bonus</label>
|
|
|
|
|
<input id="propDamageBonus" type="number" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-3 mt-2">
|
|
|
|
|
<label class="text-white/80">Attack</label>
|
|
|
|
|
<input id="statAttack" type="number" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
<label class="text-white/80 ml-3">HP</label>
|
|
|
|
|
<input id="statHp" type="number" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
<label class="text-white/80 ml-3">Defense</label>
|
|
|
|
|
<input id="statDefense" type="number" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-3 mt-2">
|
|
|
|
|
<label class="text-white/80">XP Reward</label>
|
|
|
|
|
<input id="statXpReward" type="number" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="border-t border-white/6 pt-2">
|
|
|
|
|
<h4 class="text-white/80 text-sm mb-2">Requirements</h4>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<input type="checkbox" id="reqToolRequired" />
|
|
|
|
|
<label for="reqToolRequired" class="text-white/80">Require Tool</label>
|
|
|
|
|
<label class="text-white/80 ml-3">Tool Type</label>
|
|
|
|
|
<input id="reqToolType" placeholder="pickaxe/sword" class="p-1 rounded bg-[#0f1620] text-white text-sm" />
|
|
|
|
|
<label class="text-white/80 ml-3">Min Tier</label>
|
|
|
|
|
<input id="reqMinTier" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="border-t border-white/6 pt-2">
|
|
|
|
|
<h4 class="text-white/80 text-sm mb-2">Economy</h4>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<label class="text-white/80">Sell Price</label>
|
|
|
|
|
<input id="propSellPrice" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-28" />
|
|
|
|
|
<label class="text-white/80 ml-3">Buy Price</label>
|
|
|
|
|
<input id="propBuyPrice" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-28" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="border-t border-white/6 pt-2">
|
|
|
|
|
<h4 class="text-white/80 text-sm mb-2">Chest / Drops</h4>
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<input type="checkbox" id="propChestEnabled" />
|
|
|
|
|
<label for="propChestEnabled" class="text-white/80">Chest enabled</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mt-2">
|
|
|
|
|
<label class="text-white/80">Rewards</label>
|
|
|
|
|
<div id="rewardsList" class="space-y-2 mt-2"></div>
|
|
|
|
|
<div class="flex gap-2 mt-2">
|
|
|
|
|
<select id="newRewardType" class="p-1 rounded bg-[#0f1620] text-white text-sm">
|
|
|
|
|
<option value="coins">Coins</option>
|
|
|
|
|
<option value="item">Item</option>
|
|
|
|
|
</select>
|
|
|
|
|
<input id="newRewardAmount" placeholder="amount" class="p-1 rounded bg-[#0f1620] text-white text-sm w-32" />
|
|
|
|
|
<input id="newRewardItemKey" placeholder="item.key (if item)" class="p-1 rounded bg-[#0f1620] text-white text-sm" />
|
|
|
|
|
<button type="button" id="addRewardBtn" class="px-2 py-1 rounded bg-indigo-600 text-white text-sm">Añadir</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<button type="button" id="loadRawPropsBtn" class="px-2 py-1 rounded bg-sky-600 text-white text-sm">Cargar props (admin)</button>
|
|
|
|
|
<span id="loadRawMsg" class="text-sm text-white/60 ml-3"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-15 12:27:24 -05:00
|
|
|
</div>
|
|
|
|
|
<div class="col-span-2">
|
2025-10-15 12:57:35 -05:00
|
|
|
<label class="text-white/80">Metadata (UI)</label>
|
|
|
|
|
<div class="w-full mt-1 p-3 rounded bg-[#0f1720] text-white">
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<label class="text-white/80">Rarity</label>
|
|
|
|
|
<select id="metaRarity" class="p-1 rounded bg-[#0f1620] text-white text-sm">
|
|
|
|
|
<option value="common">common</option>
|
|
|
|
|
<option value="uncommon">uncommon</option>
|
|
|
|
|
<option value="rare">rare</option>
|
|
|
|
|
<option value="epic">epic</option>
|
|
|
|
|
</select>
|
|
|
|
|
<label class="text-white/80 ml-3">Weight</label>
|
|
|
|
|
<input id="metaWeight" type="number" min="0" class="p-1 rounded bg-[#0f1620] text-white text-sm w-24" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mt-2">
|
|
|
|
|
<label class="text-white/80">Custom Metadata (advanced)</label>
|
|
|
|
|
<textarea id="metaCustomJson" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white font-mono" rows="3">{}</textarea>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-15 12:27:24 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mt-4 flex items-center justify-end gap-2">
|
|
|
|
|
<button type="button" id="cancelItemBtn" class="px-3 py-1 rounded bg-white/6 text-white">Cancelar</button>
|
|
|
|
|
<button type="submit" id="saveItemBtn" class="px-3 py-1 rounded bg-green-600 text-white">Guardar</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
(function(){
|
|
|
|
|
const guildId = '<%= selectedGuildId ? selectedGuildId : '' %>';
|
2025-10-15 13:16:37 -05:00
|
|
|
let cachedItems = [];
|
2025-10-15 13:52:53 -05:00
|
|
|
let currentOpenEditId = 0;
|
2025-10-15 13:16:37 -05:00
|
|
|
function setModalError(msg) {
|
|
|
|
|
const el = document.getElementById('modalError');
|
|
|
|
|
if (!el) return;
|
|
|
|
|
el.textContent = msg || '';
|
|
|
|
|
if (msg) el.classList.remove('hidden'); else el.classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
function clearModalError() { setModalError(''); }
|
2025-10-15 12:27:24 -05:00
|
|
|
const list = document.getElementById('itemsList');
|
|
|
|
|
const modal = document.getElementById('itemModal');
|
|
|
|
|
const form = document.getElementById('itemForm');
|
|
|
|
|
const createBtn = document.getElementById('createItemBtn');
|
|
|
|
|
const cancelBtn = document.getElementById('cancelItemBtn');
|
|
|
|
|
const modalTitle = document.getElementById('modalTitle');
|
|
|
|
|
|
|
|
|
|
function showModal(edit=false) {
|
|
|
|
|
modal.classList.remove('hidden');
|
|
|
|
|
modal.classList.add('flex');
|
|
|
|
|
}
|
|
|
|
|
function hideModal() {
|
|
|
|
|
modal.classList.add('hidden');
|
|
|
|
|
modal.classList.remove('flex');
|
|
|
|
|
form.reset();
|
2025-10-15 12:57:35 -05:00
|
|
|
// reset structured props/meta
|
|
|
|
|
document.getElementById('propCraftable').checked = false;
|
|
|
|
|
document.getElementById('propRecipeKey').value = '';
|
|
|
|
|
document.getElementById('propEquipable').checked = false;
|
|
|
|
|
document.getElementById('propSlot').value = '';
|
|
|
|
|
document.getElementById('propAttack').value = '';
|
|
|
|
|
document.getElementById('propDefense').value = '';
|
|
|
|
|
document.getElementById('propDurability').value = '';
|
|
|
|
|
document.getElementById('propMaxDurability').value = '';
|
|
|
|
|
document.getElementById('propCustomJson').value = '{}';
|
|
|
|
|
// new fields
|
|
|
|
|
document.getElementById('propToolType').value = '';
|
|
|
|
|
document.getElementById('propToolTier').value = '';
|
|
|
|
|
document.getElementById('propBreakable').checked = false;
|
|
|
|
|
document.getElementById('propDurabilityPerUse').value = '';
|
|
|
|
|
document.getElementById('propUsable').checked = false;
|
|
|
|
|
document.getElementById('propPurgeAllEffects').checked = false;
|
|
|
|
|
document.getElementById('propHealAmount').value = '';
|
|
|
|
|
document.getElementById('propDamage').value = '';
|
|
|
|
|
document.getElementById('propDamageBonus').value = '';
|
|
|
|
|
document.getElementById('statAttack').value = '';
|
|
|
|
|
document.getElementById('statHp').value = '';
|
|
|
|
|
document.getElementById('statDefense').value = '';
|
|
|
|
|
document.getElementById('statXpReward').value = '';
|
|
|
|
|
document.getElementById('reqToolRequired').checked = false;
|
|
|
|
|
document.getElementById('reqToolType').value = '';
|
|
|
|
|
document.getElementById('reqMinTier').value = '';
|
|
|
|
|
document.getElementById('propSellPrice').value = '';
|
|
|
|
|
document.getElementById('propBuyPrice').value = '';
|
|
|
|
|
document.getElementById('propChestEnabled').checked = false;
|
|
|
|
|
// rewards UI reset
|
2025-10-15 13:52:53 -05:00
|
|
|
const rewardsEl = document.getElementById('rewardsList');
|
|
|
|
|
if (rewardsEl) { rewardsEl.innerHTML = ''; rewardsEl.dataset.rewards = JSON.stringify([]); }
|
2025-10-15 12:57:35 -05:00
|
|
|
document.getElementById('newRewardType').value = 'coins';
|
|
|
|
|
document.getElementById('newRewardAmount').value = '';
|
|
|
|
|
document.getElementById('newRewardItemKey').value = '';
|
|
|
|
|
document.getElementById('metaRarity').value = 'common';
|
|
|
|
|
document.getElementById('metaWeight').value = '';
|
|
|
|
|
document.getElementById('metaCustomJson').value = '{}';
|
2025-10-15 12:27:24 -05:00
|
|
|
document.getElementById('itemId').value = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchItems() {
|
|
|
|
|
if (!guildId) return;
|
|
|
|
|
list.innerHTML = '<div class="text-white/60">Cargando items...</div>';
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items`, { headers: { 'Accept':'application/json' } });
|
|
|
|
|
if (!res.ok) throw new Error('fetch-failed');
|
|
|
|
|
const j = await res.json();
|
|
|
|
|
if (!j.ok) throw new Error('bad');
|
2025-10-15 13:16:37 -05:00
|
|
|
cachedItems = j.items || [];
|
|
|
|
|
renderList(cachedItems);
|
2025-10-15 12:27:24 -05:00
|
|
|
} catch (err) {
|
|
|
|
|
list.innerHTML = '<div class="text-red-400">Error cargando items</div>';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 12:57:35 -05:00
|
|
|
// Rewards UI helpers
|
|
|
|
|
function renderRewardsList(rewards) {
|
|
|
|
|
const container = document.getElementById('rewardsList');
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
if (!Array.isArray(rewards) || rewards.length === 0) return;
|
|
|
|
|
rewards.forEach((r, idx) => {
|
|
|
|
|
const row = document.createElement('div');
|
|
|
|
|
row.className = 'flex items-center gap-2';
|
|
|
|
|
if (r.type === 'coins' || typeof r.coins !== 'undefined') {
|
|
|
|
|
const amt = r.coins || r.amount || 0;
|
|
|
|
|
row.innerHTML = `<div class="text-white/80">Coins: <strong class="text-white">${amt}</strong></div>`;
|
|
|
|
|
} else if (r.type === 'item' || r.itemKey) {
|
|
|
|
|
const key = r.itemKey || (r.items && r.items[0] && r.items[0].key) || '';
|
|
|
|
|
const qty = r.qty || (r.items && r.items[0] && r.items[0].quantity) || r.quantity || 1;
|
|
|
|
|
row.innerHTML = `<div class="text-white/80">Item: <strong class="text-white">${escapeHtml(key)}</strong> x${qty}</div>`;
|
|
|
|
|
} else {
|
|
|
|
|
row.innerHTML = `<div class="text-white/80">${escapeHtml(JSON.stringify(r))}</div>`;
|
|
|
|
|
}
|
|
|
|
|
const del = document.createElement('button');
|
|
|
|
|
del.className = 'px-2 py-1 bg-red-600 text-white rounded text-sm ml-2';
|
|
|
|
|
del.textContent = 'Eliminar';
|
|
|
|
|
del.addEventListener('click', ()=>{
|
|
|
|
|
const list = getCurrentRewards();
|
|
|
|
|
list.splice(idx,1);
|
|
|
|
|
renderRewardsList(list);
|
|
|
|
|
setCurrentRewards(list);
|
|
|
|
|
renderTagChips();
|
|
|
|
|
});
|
|
|
|
|
row.appendChild(del);
|
|
|
|
|
container.appendChild(row);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCurrentRewards() {
|
|
|
|
|
try {
|
|
|
|
|
const el = document.getElementById('rewardsList');
|
|
|
|
|
const nodes = Array.from(el.children || []);
|
|
|
|
|
// store rewards in dataset for simple persistence
|
|
|
|
|
const raw = el.dataset.rewards;
|
|
|
|
|
return raw ? JSON.parse(raw) : [];
|
|
|
|
|
} catch (e) { return []; }
|
|
|
|
|
}
|
|
|
|
|
function setCurrentRewards(arr) { document.getElementById('rewardsList').dataset.rewards = JSON.stringify(arr || []); }
|
|
|
|
|
|
|
|
|
|
document.getElementById('addRewardBtn').addEventListener('click', ()=>{
|
|
|
|
|
const type = document.getElementById('newRewardType').value;
|
|
|
|
|
const amt = Number(document.getElementById('newRewardAmount').value) || 0;
|
|
|
|
|
const key = document.getElementById('newRewardItemKey').value.trim();
|
|
|
|
|
const list = getCurrentRewards();
|
|
|
|
|
if (type === 'coins') {
|
|
|
|
|
list.push({ coins: amt });
|
|
|
|
|
} else {
|
2025-10-15 13:16:37 -05:00
|
|
|
if (!key) { setModalError('item.key requerido'); return; }
|
2025-10-15 12:57:35 -05:00
|
|
|
list.push({ items: [{ key, quantity: amt || 1 }] });
|
|
|
|
|
}
|
|
|
|
|
setCurrentRewards(list);
|
|
|
|
|
renderRewardsList(list);
|
|
|
|
|
// clear input
|
|
|
|
|
document.getElementById('newRewardAmount').value = '';
|
|
|
|
|
document.getElementById('newRewardItemKey').value = '';
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-15 13:16:37 -05:00
|
|
|
// Hide item key input when reward type is coins
|
|
|
|
|
const newRewardTypeEl = document.getElementById('newRewardType');
|
|
|
|
|
const newRewardItemKeyEl = document.getElementById('newRewardItemKey');
|
|
|
|
|
function updateRewardKeyVisibility() {
|
|
|
|
|
if (!newRewardTypeEl || !newRewardItemKeyEl) return;
|
|
|
|
|
if (newRewardTypeEl.value === 'coins') {
|
|
|
|
|
newRewardItemKeyEl.disabled = true;
|
|
|
|
|
newRewardItemKeyEl.classList.add('opacity-50');
|
|
|
|
|
} else {
|
|
|
|
|
newRewardItemKeyEl.disabled = false;
|
|
|
|
|
newRewardItemKeyEl.classList.remove('opacity-50');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
newRewardTypeEl.addEventListener('change', ()=>{ updateRewardKeyVisibility(); clearModalError(); });
|
|
|
|
|
updateRewardKeyVisibility();
|
|
|
|
|
|
2025-10-15 12:57:35 -05:00
|
|
|
// Tag chips UI
|
|
|
|
|
const tagsChipsContainer = document.createElement('div');
|
|
|
|
|
tagsChipsContainer.id = 'tagsChips';
|
|
|
|
|
tagsChipsContainer.className = 'mt-2 flex flex-wrap gap-2';
|
|
|
|
|
const tagsField = document.getElementById('fieldTags');
|
|
|
|
|
tagsField.parentNode.appendChild(tagsChipsContainer);
|
|
|
|
|
|
|
|
|
|
function renderTagChips() {
|
|
|
|
|
const base = tagsField.value.split(',').map(s=>s.trim()).filter(Boolean);
|
|
|
|
|
const auto = [];
|
|
|
|
|
// build preview from current UI selections
|
|
|
|
|
if (document.getElementById('propCraftable').checked) auto.push('craftable');
|
|
|
|
|
if (document.getElementById('propToolType').value.trim()) auto.push('tool');
|
|
|
|
|
if (document.getElementById('propDamage').value.trim() || document.getElementById('propAttack').value.trim()) auto.push('weapon');
|
|
|
|
|
if (document.getElementById('propUsable').checked) auto.push('consumable');
|
|
|
|
|
if (document.getElementById('propEquipable').checked) auto.push('equipable');
|
|
|
|
|
if (document.getElementById('propChestEnabled').checked) auto.push('chest');
|
|
|
|
|
if (document.getElementById('propBreakable').checked) auto.push('breakable');
|
|
|
|
|
if (document.getElementById('propSellPrice').value.trim()) auto.push('sellable');
|
|
|
|
|
if (document.getElementById('propBuyPrice').value.trim()) auto.push('buyable');
|
|
|
|
|
const merged = Array.from(new Set([...base, ...auto]));
|
|
|
|
|
tagsChipsContainer.innerHTML = '';
|
|
|
|
|
merged.forEach(t=>{
|
|
|
|
|
const chip = document.createElement('span');
|
|
|
|
|
chip.className = 'px-2 py-1 rounded bg-white/6 text-sm text-white';
|
|
|
|
|
chip.textContent = t;
|
|
|
|
|
tagsChipsContainer.appendChild(chip);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// update chips when some inputs change
|
|
|
|
|
['propCraftable','propToolType','propDamage','propAttack','propUsable','propEquipable','propChestEnabled','propBreakable','propSellPrice','propBuyPrice','fieldTags'].forEach(id=>{
|
|
|
|
|
const el = document.getElementById(id);
|
|
|
|
|
if (!el) return;
|
|
|
|
|
el.addEventListener('input', renderTagChips);
|
|
|
|
|
el.addEventListener('change', renderTagChips);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
2025-10-15 12:27:24 -05:00
|
|
|
function renderList(items) {
|
|
|
|
|
if (!Array.isArray(items) || items.length === 0) {
|
|
|
|
|
list.innerHTML = '<div class="text-white/60">No hay items definidos.</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
list.innerHTML = '';
|
|
|
|
|
for (const it of items) {
|
|
|
|
|
const el = document.createElement('div');
|
|
|
|
|
el.className = 'p-3 bg-[#071a2a] rounded flex items-start justify-between';
|
|
|
|
|
el.innerHTML = `<div>
|
|
|
|
|
<div class="text-white font-medium">${escapeHtml(it.name)} <span class="text-white/50">(${escapeHtml(it.key)})</span></div>
|
|
|
|
|
<div class="text-white/60 text-sm mt-1">${escapeHtml(it.description || '')}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<button data-id="${it.id}" class="editBtn px-2 py-1 bg-indigo-600 rounded text-white text-sm">Editar</button>
|
|
|
|
|
<button data-id="${it.id}" class="delBtn px-2 py-1 bg-red-600 rounded text-white text-sm">Eliminar</button>
|
|
|
|
|
</div>`;
|
|
|
|
|
list.appendChild(el);
|
|
|
|
|
}
|
|
|
|
|
// attach handlers
|
|
|
|
|
Array.from(list.querySelectorAll('.editBtn')).forEach(b=>b.addEventListener('click', onEdit));
|
|
|
|
|
Array.from(list.querySelectorAll('.delBtn')).forEach(b=>b.addEventListener('click', onDelete));
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 12:35:16 -05:00
|
|
|
function escapeHtml(s){ if (!s) return ''; return String(s).replace(/[&<>"']/g, (m)=>({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m] || m); }
|
2025-10-15 12:27:24 -05:00
|
|
|
|
|
|
|
|
function onEdit(e) {
|
|
|
|
|
const id = e.currentTarget.getAttribute('data-id');
|
|
|
|
|
openEdit(id);
|
|
|
|
|
}
|
|
|
|
|
async function openEdit(id) {
|
2025-10-15 13:16:37 -05:00
|
|
|
clearModalError();
|
2025-10-15 12:27:24 -05:00
|
|
|
try {
|
2025-10-15 13:16:37 -05:00
|
|
|
// Prefer using cached items to avoid an extra network call and potential race conditions
|
2025-10-15 13:52:53 -05:00
|
|
|
let it = (cachedItems || []).find(x=>String(x.id) === String(id));
|
2025-10-15 13:16:37 -05:00
|
|
|
if (!it) {
|
|
|
|
|
// fallback to fetch single list if not in cache
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items`);
|
|
|
|
|
if (res && res.ok) {
|
|
|
|
|
const j = await res.json();
|
2025-10-15 13:52:53 -05:00
|
|
|
it = (j.items || []).find(x=>String(x.id) === String(id));
|
2025-10-15 13:16:37 -05:00
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// ignore and continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!it) { setModalError('Item no encontrado'); return; }
|
2025-10-15 12:27:24 -05:00
|
|
|
document.getElementById('itemId').value = it.id;
|
|
|
|
|
document.getElementById('fieldKey').value = it.key || '';
|
|
|
|
|
document.getElementById('fieldName').value = it.name || '';
|
|
|
|
|
document.getElementById('fieldCategory').value = it.category || '';
|
|
|
|
|
document.getElementById('fieldIcon').value = it.icon || '';
|
|
|
|
|
document.getElementById('fieldDescription').value = it.description || '';
|
|
|
|
|
document.getElementById('fieldTags').value = (Array.isArray(it.tags) ? it.tags.join(',') : '');
|
|
|
|
|
document.getElementById('fieldMaxPer').value = it.maxPerInventory || '';
|
2025-10-15 12:57:35 -05:00
|
|
|
// For security we do NOT expose stored props/metadata via the API. UI fields will be empty/default and admin can attempt raw load.
|
|
|
|
|
document.getElementById('propCraftable').checked = false;
|
|
|
|
|
document.getElementById('propRecipeKey').value = '';
|
|
|
|
|
document.getElementById('propEquipable').checked = false;
|
|
|
|
|
document.getElementById('propSlot').value = '';
|
|
|
|
|
document.getElementById('propAttack').value = '';
|
|
|
|
|
document.getElementById('propDefense').value = '';
|
|
|
|
|
document.getElementById('propDurability').value = '';
|
|
|
|
|
document.getElementById('propMaxDurability').value = '';
|
|
|
|
|
document.getElementById('propCustomJson').value = '{}';
|
|
|
|
|
document.getElementById('metaRarity').value = 'common';
|
|
|
|
|
document.getElementById('metaWeight').value = '';
|
|
|
|
|
document.getElementById('metaCustomJson').value = '{}';
|
|
|
|
|
// new fields defaults on edit
|
|
|
|
|
document.getElementById('propToolType').value = '';
|
|
|
|
|
document.getElementById('propToolTier').value = '';
|
|
|
|
|
document.getElementById('propBreakable').checked = false;
|
|
|
|
|
document.getElementById('propDurabilityPerUse').value = '';
|
|
|
|
|
document.getElementById('propUsable').checked = false;
|
|
|
|
|
document.getElementById('propPurgeAllEffects').checked = false;
|
|
|
|
|
document.getElementById('propHealAmount').value = '';
|
|
|
|
|
document.getElementById('propDamage').value = '';
|
|
|
|
|
document.getElementById('propDamageBonus').value = '';
|
|
|
|
|
document.getElementById('statAttack').value = '';
|
|
|
|
|
document.getElementById('statHp').value = '';
|
|
|
|
|
document.getElementById('statDefense').value = '';
|
|
|
|
|
document.getElementById('statXpReward').value = '';
|
|
|
|
|
document.getElementById('reqToolRequired').checked = false;
|
|
|
|
|
document.getElementById('reqToolType').value = '';
|
|
|
|
|
document.getElementById('reqMinTier').value = '';
|
|
|
|
|
document.getElementById('propSellPrice').value = '';
|
|
|
|
|
document.getElementById('propBuyPrice').value = '';
|
|
|
|
|
document.getElementById('propChestEnabled').checked = false;
|
|
|
|
|
document.getElementById('propDropsJson').value = '{}';
|
|
|
|
|
// populate rewards if we have a cached representation in the item (not exposed by default)
|
|
|
|
|
document.getElementById('rewardsList').dataset.rewards = JSON.stringify([]);
|
|
|
|
|
renderRewardsList([]);
|
2025-10-15 12:27:24 -05:00
|
|
|
modalTitle.textContent = 'Editar item';
|
|
|
|
|
showModal(true);
|
|
|
|
|
} catch (err) {
|
2025-10-15 13:16:37 -05:00
|
|
|
setModalError('Error al abrir item');
|
2025-10-15 12:27:24 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 12:57:35 -05:00
|
|
|
// Try to load raw props for the currently opened item (admin-only; server may reject)
|
|
|
|
|
async function tryLoadRawProps(itemId) {
|
|
|
|
|
const msgEl = document.getElementById('loadRawMsg');
|
|
|
|
|
msgEl.textContent = '';
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items/${encodeURIComponent(itemId)}?raw=1`, { headers: { 'Accept':'application/json' } });
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
// server might reject raw access; silent friendly message
|
|
|
|
|
msgEl.textContent = 'raw no permitido';
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const j = await res.json();
|
|
|
|
|
if (!j || !j.ok) { msgEl.textContent = 'no data'; return null; }
|
|
|
|
|
msgEl.textContent = 'raw cargado';
|
|
|
|
|
return j.item || null;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
msgEl.textContent = 'error';
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 12:27:24 -05:00
|
|
|
async function onDelete(e) {
|
|
|
|
|
const id = e.currentTarget.getAttribute('data-id');
|
|
|
|
|
if (!confirm('Eliminar item?')) return;
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
|
|
if (!res.ok) throw new Error('delete-failed');
|
|
|
|
|
await fetchItems();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert('Error al eliminar');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 13:52:53 -05:00
|
|
|
createBtn.addEventListener('click', ()=>{ clearModalError(); currentOpenEditId++; hideModal(); modalTitle.textContent = 'Crear item'; showModal(false); });
|
|
|
|
|
cancelBtn.addEventListener('click', ()=>{ hideModal(); clearModalError(); });
|
2025-10-15 12:27:24 -05:00
|
|
|
|
|
|
|
|
form.addEventListener('submit', async (ev)=>{
|
2025-10-15 13:52:53 -05:00
|
|
|
async function openEdit(id) {
|
|
|
|
|
clearModalError();
|
|
|
|
|
const myOpenId = ++currentOpenEditId;
|
|
|
|
|
try {
|
|
|
|
|
// Prefer using cached items to avoid an extra network call and potential race conditions
|
|
|
|
|
let it = (cachedItems || []).find(x=>String(x.id) === String(id));
|
|
|
|
|
if (!it) {
|
|
|
|
|
// fallback to fetch single list if not in cache
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items`);
|
|
|
|
|
if (res && res.ok) {
|
|
|
|
|
const j = await res.json();
|
|
|
|
|
it = (j.items || []).find(x=>String(x.id) === String(id));
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// ignore and continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// if another open request happened meanwhile, abort applying
|
|
|
|
|
if (myOpenId !== currentOpenEditId) return;
|
|
|
|
|
if (!it) { setModalError('Item no encontrado'); return; }
|
|
|
|
|
document.getElementById('itemId').value = it.id;
|
|
|
|
|
document.getElementById('fieldKey').value = it.key || '';
|
|
|
|
|
document.getElementById('fieldName').value = it.name || '';
|
|
|
|
|
document.getElementById('fieldCategory').value = it.category || '';
|
|
|
|
|
document.getElementById('fieldIcon').value = it.icon || '';
|
|
|
|
|
document.getElementById('fieldDescription').value = it.description || '';
|
|
|
|
|
document.getElementById('fieldTags').value = (Array.isArray(it.tags) ? it.tags.join(',') : '');
|
|
|
|
|
document.getElementById('fieldMaxPer').value = it.maxPerInventory || '';
|
|
|
|
|
// For security we do NOT expose stored props/metadata via the API. UI fields will be empty/default and admin can attempt raw load.
|
|
|
|
|
document.getElementById('propCraftable').checked = false;
|
|
|
|
|
document.getElementById('propRecipeKey').value = '';
|
|
|
|
|
document.getElementById('propEquipable').checked = false;
|
|
|
|
|
document.getElementById('propSlot').value = '';
|
|
|
|
|
document.getElementById('propAttack').value = '';
|
|
|
|
|
document.getElementById('propDefense').value = '';
|
|
|
|
|
document.getElementById('propDurability').value = '';
|
|
|
|
|
document.getElementById('propMaxDurability').value = '';
|
|
|
|
|
document.getElementById('propCustomJson').value = '{}';
|
|
|
|
|
document.getElementById('metaRarity').value = 'common';
|
|
|
|
|
document.getElementById('metaWeight').value = '';
|
|
|
|
|
document.getElementById('metaCustomJson').value = '{}';
|
|
|
|
|
// new fields defaults on edit
|
|
|
|
|
document.getElementById('propToolType').value = '';
|
|
|
|
|
document.getElementById('propToolTier').value = '';
|
|
|
|
|
document.getElementById('propBreakable').checked = false;
|
|
|
|
|
document.getElementById('propDurabilityPerUse').value = '';
|
|
|
|
|
document.getElementById('propUsable').checked = false;
|
|
|
|
|
document.getElementById('propPurgeAllEffects').checked = false;
|
|
|
|
|
document.getElementById('propHealAmount').value = '';
|
|
|
|
|
document.getElementById('propDamage').value = '';
|
|
|
|
|
document.getElementById('propDamageBonus').value = '';
|
|
|
|
|
document.getElementById('statAttack').value = '';
|
|
|
|
|
document.getElementById('statHp').value = '';
|
|
|
|
|
document.getElementById('statDefense').value = '';
|
|
|
|
|
document.getElementById('statXpReward').value = '';
|
|
|
|
|
document.getElementById('reqToolRequired').checked = false;
|
|
|
|
|
document.getElementById('reqToolType').value = '';
|
|
|
|
|
document.getElementById('reqMinTier').value = '';
|
|
|
|
|
document.getElementById('propSellPrice').value = '';
|
|
|
|
|
document.getElementById('propBuyPrice').value = '';
|
|
|
|
|
document.getElementById('propChestEnabled').checked = false;
|
|
|
|
|
document.getElementById('propDropsJson').value = '{}';
|
|
|
|
|
// populate rewards if we have a cached representation in the item (not exposed by default)
|
|
|
|
|
const rewardsEl2 = document.getElementById('rewardsList');
|
|
|
|
|
if (rewardsEl2) { rewardsEl2.dataset.rewards = JSON.stringify([]); }
|
|
|
|
|
renderRewardsList([]);
|
|
|
|
|
modalTitle.textContent = 'Editar item';
|
|
|
|
|
showModal(true);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setModalError('Error al abrir item');
|
|
|
|
|
}
|
2025-10-15 12:57:35 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-15 13:52:53 -05:00
|
|
|
} catch (e) { setModalError('JSON inválido en Drops/Chest: ' + (e && e.message ? e.message : String(e))); return; }
|
2025-10-15 12:57:35 -05:00
|
|
|
// merge custom JSON
|
|
|
|
|
try {
|
|
|
|
|
const custom = JSON.parse(document.getElementById('propCustomJson').value || '{}');
|
|
|
|
|
if (custom && typeof custom === 'object') Object.assign(parsedProps, custom);
|
2025-10-15 13:52:53 -05:00
|
|
|
} catch (e) { setModalError('JSON inválido en Props personalizado: ' + (e && e.message ? e.message : String(e))); return; }
|
2025-10-15 12:57:35 -05:00
|
|
|
|
|
|
|
|
// Build metadata
|
|
|
|
|
let parsedMeta = { rarity: document.getElementById('metaRarity').value };
|
|
|
|
|
const w = document.getElementById('metaWeight').value.trim(); if (w) parsedMeta.weight = Number(w);
|
|
|
|
|
try {
|
|
|
|
|
const customM = JSON.parse(document.getElementById('metaCustomJson').value || '{}');
|
|
|
|
|
if (customM && typeof customM === 'object') Object.assign(parsedMeta, customM);
|
2025-10-15 13:52:53 -05:00
|
|
|
} catch (e) { setModalError('JSON inválido en Metadata personalizado: ' + (e && e.message ? e.message : String(e))); return; }
|
2025-10-15 12:27:24 -05:00
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
|
key: document.getElementById('fieldKey').value.trim(),
|
|
|
|
|
name: document.getElementById('fieldName').value.trim(),
|
|
|
|
|
category: document.getElementById('fieldCategory').value.trim(),
|
|
|
|
|
icon: document.getElementById('fieldIcon').value.trim(),
|
|
|
|
|
description: document.getElementById('fieldDescription').value.trim(),
|
|
|
|
|
tags: document.getElementById('fieldTags').value.split(',').map(s=>s.trim()).filter(Boolean),
|
|
|
|
|
maxPerInventory: Number(document.getElementById('fieldMaxPer').value) || null,
|
2025-10-15 12:57:35 -05:00
|
|
|
props: Object.keys(parsedProps).length ? parsedProps : null,
|
|
|
|
|
metadata: Object.keys(parsedMeta).length ? parsedMeta : null,
|
2025-10-15 12:27:24 -05:00
|
|
|
};
|
2025-10-15 12:57:35 -05:00
|
|
|
// Auto-generate tags from props UI
|
|
|
|
|
try {
|
|
|
|
|
const autoTags = new Set(payload.tags);
|
|
|
|
|
if (parsedProps.craftable) autoTags.add('craftable');
|
|
|
|
|
if (parsedProps.tool) autoTags.add('tool');
|
|
|
|
|
if (parsedProps.damage || parsedProps.attack || parsedProps.stats) autoTags.add('weapon');
|
|
|
|
|
if (parsedProps.usable) autoTags.add('consumable');
|
|
|
|
|
if (parsedProps.equipable) autoTags.add('equipable');
|
|
|
|
|
if (parsedProps.chest) autoTags.add('chest');
|
|
|
|
|
if (parsedProps.breakable) autoTags.add('breakable');
|
|
|
|
|
if (parsedProps.sellPrice) autoTags.add('sellable');
|
|
|
|
|
if (parsedProps.buyPrice) autoTags.add('buyable');
|
|
|
|
|
// merge back to payload.tags as array
|
|
|
|
|
payload.tags = Array.from(autoTags).filter(Boolean);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// ignore tagging failures
|
|
|
|
|
}
|
2025-10-15 12:27:24 -05:00
|
|
|
try {
|
|
|
|
|
if (id) {
|
|
|
|
|
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'Content-Type':'application/json' }, body: JSON.stringify(payload) });
|
|
|
|
|
if (!res.ok) throw new Error('save-failed');
|
|
|
|
|
} else {
|
|
|
|
|
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items`, { method: 'POST', headers: { 'Content-Type':'application/json' }, body: JSON.stringify(payload) });
|
|
|
|
|
if (!res.ok) throw new Error('create-failed');
|
|
|
|
|
}
|
|
|
|
|
hideModal();
|
2025-10-15 13:52:53 -05:00
|
|
|
clearModalError();
|
2025-10-15 12:27:24 -05:00
|
|
|
await fetchItems();
|
|
|
|
|
} catch (err) {
|
2025-10-15 13:52:53 -05:00
|
|
|
setModalError('Error al guardar item. ¿JSON válido en Props/Metadata?');
|
2025-10-15 12:27:24 -05:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-15 12:57:35 -05:00
|
|
|
// load raw props button handler
|
|
|
|
|
document.getElementById('loadRawPropsBtn').addEventListener('click', async ()=>{
|
|
|
|
|
const id = document.getElementById('itemId').value;
|
|
|
|
|
if (!id) return alert('Abra un item primero para cargar props raw');
|
|
|
|
|
const raw = await tryLoadRawProps(id);
|
|
|
|
|
if (!raw) return;
|
|
|
|
|
try {
|
|
|
|
|
if (raw.props && typeof raw.props === 'object') {
|
|
|
|
|
// map known props to UI
|
|
|
|
|
document.getElementById('propCraftable').checked = !!raw.props.craftable;
|
|
|
|
|
document.getElementById('propRecipeKey').value = raw.props.recipe && raw.props.recipe.key ? raw.props.recipe.key : '';
|
|
|
|
|
document.getElementById('propEquipable').checked = !!raw.props.equipable;
|
|
|
|
|
document.getElementById('propSlot').value = raw.props.slot || '';
|
|
|
|
|
document.getElementById('propAttack').value = raw.props.attack || '';
|
|
|
|
|
document.getElementById('propDefense').value = raw.props.defense || '';
|
|
|
|
|
document.getElementById('propDurability').value = raw.props.durability || '';
|
|
|
|
|
document.getElementById('propMaxDurability').value = raw.props.maxDurability || '';
|
|
|
|
|
// tool
|
|
|
|
|
document.getElementById('propToolType').value = (raw.props.tool && raw.props.tool.type) ? raw.props.tool.type : '';
|
|
|
|
|
document.getElementById('propToolTier').value = (raw.props.tool && typeof raw.props.tool.tier !== 'undefined') ? String(raw.props.tool.tier) : '';
|
|
|
|
|
// breakable
|
|
|
|
|
document.getElementById('propBreakable').checked = !!(raw.props.breakable && raw.props.breakable.enabled);
|
|
|
|
|
document.getElementById('propDurabilityPerUse').value = (raw.props.breakable && raw.props.breakable.durabilityPerUse) ? String(raw.props.breakable.durabilityPerUse) : '';
|
|
|
|
|
// usable
|
|
|
|
|
document.getElementById('propUsable').checked = !!raw.props.usable;
|
|
|
|
|
document.getElementById('propPurgeAllEffects').checked = !!raw.props.purgeAllEffects;
|
|
|
|
|
document.getElementById('propHealAmount').value = raw.props.heal || '';
|
|
|
|
|
// damage/bonuses
|
|
|
|
|
document.getElementById('propDamage').value = raw.props.damage || '';
|
|
|
|
|
document.getElementById('propDamageBonus').value = raw.props.damageBonus || '';
|
|
|
|
|
// stats
|
|
|
|
|
document.getElementById('statAttack').value = (raw.props.stats && raw.props.stats.attack) ? String(raw.props.stats.attack) : '';
|
|
|
|
|
document.getElementById('statHp').value = (raw.props.stats && raw.props.stats.hp) ? String(raw.props.stats.hp) : '';
|
|
|
|
|
document.getElementById('statDefense').value = (raw.props.stats && raw.props.stats.defense) ? String(raw.props.stats.defense) : '';
|
|
|
|
|
document.getElementById('statXpReward').value = (raw.props.stats && raw.props.stats.xpReward) ? String(raw.props.stats.xpReward) : '';
|
|
|
|
|
// requirements
|
|
|
|
|
document.getElementById('reqToolRequired').checked = !!(raw.props.requirements && raw.props.requirements.tool && raw.props.requirements.tool.required);
|
|
|
|
|
document.getElementById('reqToolType').value = (raw.props.requirements && raw.props.requirements.tool && raw.props.requirements.tool.toolType) ? raw.props.requirements.tool.toolType : '';
|
|
|
|
|
document.getElementById('reqMinTier').value = (raw.props.requirements && raw.props.requirements.tool && typeof raw.props.requirements.tool.minTier !== 'undefined') ? String(raw.props.requirements.tool.minTier) : '';
|
|
|
|
|
// economy
|
|
|
|
|
document.getElementById('propSellPrice').value = raw.props.sellPrice || '';
|
|
|
|
|
document.getElementById('propBuyPrice').value = raw.props.buyPrice || '';
|
|
|
|
|
// chest/drops
|
|
|
|
|
document.getElementById('propChestEnabled').checked = !!(raw.props.chest && raw.props.chest.enabled);
|
|
|
|
|
// map rewards into UI
|
|
|
|
|
const rewards = raw.props.chest && raw.props.chest.rewards ? raw.props.chest.rewards : [];
|
|
|
|
|
document.getElementById('rewardsList').dataset.rewards = JSON.stringify(rewards || []);
|
|
|
|
|
renderRewardsList(rewards || []);
|
|
|
|
|
// leave extra fields in custom JSON
|
|
|
|
|
const copy = Object.assign({}, raw.props);
|
|
|
|
|
delete copy.craftable; delete copy.recipe; delete copy.equipable; delete copy.slot; delete copy.attack; delete copy.defense; delete copy.durability; delete copy.maxDurability;
|
|
|
|
|
document.getElementById('propCustomJson').value = JSON.stringify(copy, null, 2);
|
|
|
|
|
}
|
|
|
|
|
if (raw.metadata && typeof raw.metadata === 'object') {
|
|
|
|
|
document.getElementById('metaRarity').value = raw.metadata.rarity || 'common';
|
|
|
|
|
document.getElementById('metaWeight').value = raw.metadata.weight || '';
|
|
|
|
|
const copyM = Object.assign({}, raw.metadata);
|
|
|
|
|
delete copyM.rarity; delete copyM.weight;
|
|
|
|
|
document.getElementById('metaCustomJson').value = JSON.stringify(copyM, null, 2);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2025-10-15 13:52:53 -05:00
|
|
|
setModalError('Error al mapear raw props: ' + (e && e.message ? e.message : String(e)));
|
2025-10-15 12:57:35 -05:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-15 12:27:24 -05:00
|
|
|
// Initial load
|
|
|
|
|
fetchItems();
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
</div>
|