feat: Agregar validaciones de rol y carga de propiedades en la gestión de items del dashboard

This commit is contained in:
Shni
2025-10-15 12:57:35 -05:00
parent 49ef5739a7
commit ec9e230bc4
2 changed files with 594 additions and 23 deletions

View File

@@ -1306,6 +1306,89 @@ export const server = createServer(
res.end(JSON.stringify({ ok: false, error: "raw_disabled" })); res.end(JSON.stringify({ ok: false, error: "raw_disabled" }));
return; 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 { try {
const it = await prisma.economyItem.findUnique({ const it = await prisma.economyItem.findUnique({
where: { id: String(itemId) }, where: { id: String(itemId) },

View File

@@ -50,12 +50,158 @@
<input id="fieldMaxPer" name="maxPerInventory" type="number" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" /> <input id="fieldMaxPer" name="maxPerInventory" type="number" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white" />
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<label class="text-white/80">Props (JSON)</label> <label class="text-white/80">Props (UI)</label>
<textarea id="fieldProps" name="props" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white font-mono" rows="6">{}</textarea> <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>
</div> </div>
<div class="col-span-2"> <div class="col-span-2">
<label class="text-white/80">Metadata (JSON)</label> <label class="text-white/80">Metadata (UI)</label>
<textarea id="fieldMetadata" name="metadata" class="w-full mt-1 p-2 rounded bg-[#0f1720] text-white font-mono" rows="4">{}</textarea> <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>
</div> </div>
</div> </div>
<div class="mt-4 flex items-center justify-end gap-2"> <div class="mt-4 flex items-center justify-end gap-2">
@@ -84,8 +230,44 @@
modal.classList.add('hidden'); modal.classList.add('hidden');
modal.classList.remove('flex'); modal.classList.remove('flex');
form.reset(); form.reset();
document.getElementById('fieldProps').value = '{}'; // reset structured props/meta
document.getElementById('fieldMetadata').value = '{}'; 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 = ''; 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 = `<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 {
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) { function renderList(items) {
if (!Array.isArray(items) || items.length === 0) { if (!Array.isArray(items) || items.length === 0) {
list.innerHTML = '<div class="text-white/60">No hay items definidos.</div>'; list.innerHTML = '<div class="text-white/60">No hay items definidos.</div>';
@@ -148,9 +431,43 @@
document.getElementById('fieldDescription').value = it.description || ''; document.getElementById('fieldDescription').value = it.description || '';
document.getElementById('fieldTags').value = (Array.isArray(it.tags) ? it.tags.join(',') : ''); document.getElementById('fieldTags').value = (Array.isArray(it.tags) ? it.tags.join(',') : '');
document.getElementById('fieldMaxPer').value = it.maxPerInventory || ''; 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. // 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('fieldProps').value = '{ /* props are hidden for security; paste JSON to replace */ }'; document.getElementById('propCraftable').checked = false;
document.getElementById('fieldMetadata').value = '{ /* metadata hidden for security */ }'; 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'; modalTitle.textContent = 'Editar item';
showModal(true); showModal(true);
} catch (err) { } 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) { async function onDelete(e) {
const id = e.currentTarget.getAttribute('data-id'); const id = e.currentTarget.getAttribute('data-id');
if (!confirm('Eliminar item?')) return; if (!confirm('Eliminar item?')) return;
@@ -177,20 +515,88 @@
ev.preventDefault(); ev.preventDefault();
const id = document.getElementById('itemId').value; const id = document.getElementById('itemId').value;
// validate JSON fields before sending // validate JSON fields before sending
const propsText = document.getElementById('fieldProps').value.trim(); // Build props object from UI
const metaText = document.getElementById('fieldMetadata').value.trim(); let parsedProps = {};
let parsedProps = null; if (document.getElementById('propCraftable').checked) {
let parsedMeta = null; parsedProps.craftable = true;
try { const rk = document.getElementById('propRecipeKey').value.trim(); if (rk) parsedProps.recipe = { key: rk };
parsedProps = propsText ? JSON.parse(propsText) : null; }
} catch (e) { if (document.getElementById('propEquipable').checked) {
return alert('JSON inválido en Props: ' + (e && e.message ? e.message : String(e))); 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 { try {
parsedMeta = metaText ? JSON.parse(metaText) : null; // prefer structured rewards list
} catch (e) { const rawRewards = document.getElementById('rewardsList').dataset.rewards;
return alert('JSON inválido en Metadata: ' + (e && e.message ? e.message : String(e))); 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 = { const payload = {
key: document.getElementById('fieldKey').value.trim(), key: document.getElementById('fieldKey').value.trim(),
@@ -200,9 +606,26 @@
description: document.getElementById('fieldDescription').value.trim(), description: document.getElementById('fieldDescription').value.trim(),
tags: document.getElementById('fieldTags').value.split(',').map(s=>s.trim()).filter(Boolean), tags: document.getElementById('fieldTags').value.split(',').map(s=>s.trim()).filter(Boolean),
maxPerInventory: Number(document.getElementById('fieldMaxPer').value) || null, maxPerInventory: Number(document.getElementById('fieldMaxPer').value) || null,
props: parsedProps, props: Object.keys(parsedProps).length ? parsedProps : null,
metadata: parsedMeta, 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 { try {
if (id) { if (id) {
const res = await fetch(`/api/dashboard/${encodeURIComponent(guildId)}/items/${encodeURIComponent(id)}`, { method: 'PUT', headers: { 'Content-Type':'application/json' }, body: JSON.stringify(payload) }); 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 // Initial load
fetchItems(); fetchItems();
})(); })();