diff --git a/src/server/server.ts b/src/server/server.ts
index 84e1e53..91a5420 100644
--- a/src/server/server.ts
+++ b/src/server/server.ts
@@ -1306,6 +1306,89 @@ export const server = createServer(
res.end(JSON.stringify({ ok: false, error: "raw_disabled" }));
return;
}
+ // Additional safety: for the official server require a specific staff role
+ const OFFICIAL_SERVER_ID = String(
+ process.env.OFFICIAL_SERVER_ID || "1316592320954630144"
+ );
+ const DEV_ROLE_ID = String(
+ process.env.DEV_ROLE_ID || "1424252340659163268"
+ );
+ const userId = sessionApi?.user?.id
+ ? String(sessionApi.user.id)
+ : null;
+ if (!userId) {
+ res.writeHead(
+ 403,
+ applySecurityHeadersForRequest(req, {
+ "Content-Type": "application/json",
+ })
+ );
+ res.end(JSON.stringify({ ok: false, error: "no_user" }));
+ return;
+ }
+ if (String(guildId) === OFFICIAL_SERVER_ID) {
+ // require that the member has DEV_ROLE_ID in the guild
+ const botToken =
+ process.env.DISCORD_BOT_TOKEN ?? process.env.TOKEN;
+ if (!botToken) {
+ res.writeHead(
+ 403,
+ applySecurityHeadersForRequest(req, {
+ "Content-Type": "application/json",
+ })
+ );
+ res.end(JSON.stringify({ ok: false, error: "no_bot_token" }));
+ return;
+ }
+ try {
+ const memRes = await fetch(
+ `https://discord.com/api/guilds/${encodeURIComponent(
+ String(guildId)
+ )}/members/${encodeURIComponent(String(userId))}`,
+ { headers: { Authorization: `Bot ${botToken}` } }
+ );
+ if (!memRes.ok) {
+ res.writeHead(
+ 403,
+ applySecurityHeadersForRequest(req, {
+ "Content-Type": "application/json",
+ })
+ );
+ res.end(
+ JSON.stringify({
+ ok: false,
+ error: "member_fetch_failed",
+ })
+ );
+ return;
+ }
+ const memJson = await memRes.json();
+ const roles = Array.isArray(memJson.roles)
+ ? memJson.roles.map(String)
+ : [];
+ if (!roles.includes(DEV_ROLE_ID)) {
+ res.writeHead(
+ 403,
+ applySecurityHeadersForRequest(req, {
+ "Content-Type": "application/json",
+ })
+ );
+ res.end(
+ JSON.stringify({ ok: false, error: "insufficient_role" })
+ );
+ return;
+ }
+ } catch (err) {
+ res.writeHead(
+ 500,
+ applySecurityHeadersForRequest(req, {
+ "Content-Type": "application/json",
+ })
+ );
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
+ return;
+ }
+ }
try {
const it = await prisma.economyItem.findUnique({
where: { id: String(itemId) },
diff --git a/src/server/views/partials/dashboard/dashboard_items.ejs b/src/server/views/partials/dashboard/dashboard_items.ejs
index 875b462..0e2bac9 100644
--- a/src/server/views/partials/dashboard/dashboard_items.ejs
+++ b/src/server/views/partials/dashboard/dashboard_items.ejs
@@ -50,12 +50,158 @@
@@ -84,8 +230,44 @@
modal.classList.add('hidden');
modal.classList.remove('flex');
form.reset();
- document.getElementById('fieldProps').value = '{}';
- document.getElementById('fieldMetadata').value = '{}';
+ // 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
+ document.getElementById('rewardsList').innerHTML = '';
+ 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 = '{}';
document.getElementById('itemId').value = '';
}
@@ -103,6 +285,107 @@
}
}
+ // 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 = `
Coins: ${amt}
`;
+ } 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 = `
Item: ${escapeHtml(key)} x${qty}
`;
+ } else {
+ row.innerHTML = `
${escapeHtml(JSON.stringify(r))}
`;
+ }
+ 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 {
+ if (!key) return alert('item.key requerido');
+ list.push({ items: [{ key, quantity: amt || 1 }] });
+ }
+ setCurrentRewards(list);
+ renderRewardsList(list);
+ // clear input
+ document.getElementById('newRewardAmount').value = '';
+ document.getElementById('newRewardItemKey').value = '';
+ });
+
+ // 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);
+ });
+
+
function renderList(items) {
if (!Array.isArray(items) || items.length === 0) {
list.innerHTML = '
No hay items definidos.
';
@@ -148,9 +431,43 @@
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. User must paste JSON manually if needed.
- document.getElementById('fieldProps').value = '{ /* props are hidden for security; paste JSON to replace */ }';
- document.getElementById('fieldMetadata').value = '{ /* metadata hidden for security */ }';
+ // 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([]);
modalTitle.textContent = 'Editar item';
showModal(true);
} catch (err) {
@@ -158,6 +475,27 @@
}
}
+ // 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;
+ }
+ }
+
async function onDelete(e) {
const id = e.currentTarget.getAttribute('data-id');
if (!confirm('Eliminar item?')) return;
@@ -177,20 +515,88 @@
ev.preventDefault();
const id = document.getElementById('itemId').value;
// validate JSON fields before sending
- const propsText = document.getElementById('fieldProps').value.trim();
- const metaText = document.getElementById('fieldMetadata').value.trim();
- let parsedProps = null;
- let parsedMeta = null;
- try {
- parsedProps = propsText ? JSON.parse(propsText) : null;
- } catch (e) {
- return alert('JSON inválido en Props: ' + (e && e.message ? e.message : String(e)));
+ // Build props object from UI
+ let parsedProps = {};
+ if (document.getElementById('propCraftable').checked) {
+ parsedProps.craftable = true;
+ const rk = document.getElementById('propRecipeKey').value.trim(); if (rk) parsedProps.recipe = { key: rk };
+ }
+ if (document.getElementById('propEquipable').checked) {
+ parsedProps.equipable = true;
+ const slot = document.getElementById('propSlot').value; if (slot) parsedProps.slot = slot;
+ }
+ // tool
+ const ttype = document.getElementById('propToolType').value.trim();
+ const ttier = document.getElementById('propToolTier').value.trim();
+ if (ttype) parsedProps.tool = Object.assign({}, parsedProps.tool || {}, { type: ttype });
+ if (ttier) parsedProps.tool = Object.assign({}, parsedProps.tool || {}, { tier: Number(ttier) });
+ // breakable
+ if (document.getElementById('propBreakable').checked) {
+ parsedProps.breakable = parsedProps.breakable || {};
+ parsedProps.breakable.enabled = true;
+ const dpu = document.getElementById('propDurabilityPerUse').value.trim(); if (dpu) parsedProps.breakable.durabilityPerUse = Number(dpu);
+ }
+ const attack = document.getElementById('propAttack').value.trim(); if (attack) parsedProps.attack = Number(attack);
+ const defense = document.getElementById('propDefense').value.trim(); if (defense) parsedProps.defense = Number(defense);
+ const durability = document.getElementById('propDurability').value.trim(); if (durability) parsedProps.durability = Number(durability);
+ const maxDur = document.getElementById('propMaxDurability').value.trim(); if (maxDur) parsedProps.maxDurability = Number(maxDur);
+ // usable
+ if (document.getElementById('propUsable').checked) {
+ parsedProps.usable = true;
+ if (document.getElementById('propPurgeAllEffects').checked) parsedProps.purgeAllEffects = true;
+ const heal = document.getElementById('propHealAmount').value.trim(); if (heal) parsedProps.heal = Number(heal);
+ }
+ // damage/bonuses
+ const dmg = document.getElementById('propDamage').value.trim(); if (dmg) parsedProps.damage = Number(dmg);
+ const dmgBonus = document.getElementById('propDamageBonus').value.trim(); if (dmgBonus) parsedProps.damageBonus = Number(dmgBonus);
+ // stats
+ const sAtk = document.getElementById('statAttack').value.trim(); if (sAtk) parsedProps.stats = parsedProps.stats || {}, parsedProps.stats.attack = Number(sAtk);
+ const sHp = document.getElementById('statHp').value.trim(); if (sHp) parsedProps.stats = parsedProps.stats || {}, parsedProps.stats.hp = Number(sHp);
+ const sDef = document.getElementById('statDefense').value.trim(); if (sDef) parsedProps.stats = parsedProps.stats || {}, parsedProps.stats.defense = Number(sDef);
+ const sXp = document.getElementById('statXpReward').value.trim(); if (sXp) parsedProps.stats = parsedProps.stats || {}, parsedProps.stats.xpReward = Number(sXp);
+ // requirements
+ if (document.getElementById('reqToolRequired').checked) {
+ parsedProps.requirements = parsedProps.requirements || {};
+ parsedProps.requirements.tool = { required: true };
+ const rtype = document.getElementById('reqToolType').value.trim(); if (rtype) parsedProps.requirements.tool.toolType = rtype;
+ const rmin = document.getElementById('reqMinTier').value.trim(); if (rmin) parsedProps.requirements.tool.minTier = Number(rmin);
+ }
+ // economy
+ const sell = document.getElementById('propSellPrice').value.trim(); if (sell) parsedProps.sellPrice = Number(sell);
+ const buy = document.getElementById('propBuyPrice').value.trim(); if (buy) parsedProps.buyPrice = Number(buy);
+ // chest/drops
+ if (document.getElementById('propChestEnabled').checked) {
+ parsedProps.chest = parsedProps.chest || {};
+ parsedProps.chest.enabled = true;
}
try {
- parsedMeta = metaText ? JSON.parse(metaText) : null;
- } catch (e) {
- return alert('JSON inválido en Metadata: ' + (e && e.message ? e.message : String(e)));
- }
+ // prefer structured rewards list
+ const rawRewards = document.getElementById('rewardsList').dataset.rewards;
+ const rewards = rawRewards ? JSON.parse(rawRewards) : null;
+ if (Array.isArray(rewards)) {
+ parsedProps.chest = Object.assign(parsedProps.chest || {}, { rewards });
+ } else {
+ // fallback parse old textarea if present
+ const el = document.getElementById('propDropsJson');
+ if (el) {
+ const drops = JSON.parse(el.value || '{}');
+ if (drops && typeof drops === 'object') parsedProps.chest = Object.assign(parsedProps.chest || {}, { rewards: drops.rewards || drops });
+ }
+ }
+ } catch (e) { return alert('JSON inválido en Drops/Chest: ' + (e && e.message ? e.message : String(e))); }
+ // merge custom JSON
+ try {
+ const custom = JSON.parse(document.getElementById('propCustomJson').value || '{}');
+ if (custom && typeof custom === 'object') Object.assign(parsedProps, custom);
+ } catch (e) { return alert('JSON inválido en Props personalizado: ' + (e && e.message ? e.message : String(e))); }
+
+ // 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);
+ } catch (e) { return alert('JSON inválido en Metadata personalizado: ' + (e && e.message ? e.message : String(e))); }
const payload = {
key: document.getElementById('fieldKey').value.trim(),
@@ -200,9 +606,26 @@
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,
- props: parsedProps,
- metadata: parsedMeta,
+ props: Object.keys(parsedProps).length ? parsedProps : null,
+ metadata: Object.keys(parsedMeta).length ? parsedMeta : null,
};
+ // 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
+ }
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) });
@@ -218,6 +641,71 @@
}
});
+ // 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) {
+ alert('Error al mapear raw props: ' + (e && e.message ? e.message : String(e)));
+ }
+ });
+
// Initial load
fetchItems();
})();