feat: update inventory frontend and services to current production state
This commit is contained in:
parent
7050cfb8b7
commit
fc557ab1d5
4 changed files with 1321 additions and 307 deletions
601
static/script.js
601
static/script.js
|
|
@ -893,6 +893,131 @@ function updateStatsTimeRange(content, name, timeRange) {
|
|||
}
|
||||
|
||||
// Show or create an inventory window for a character
|
||||
/**
|
||||
* Normalize raw plugin MyWorldObject format to flat fields expected by createInventorySlot.
|
||||
* Plugin sends PascalCase computed properties: { Id, Icon, Name, Value, Burden, ArmorLevel, Material, ... }
|
||||
* Also has raw dictionaries: { IntValues: {19: value, 5: burden, ...}, StringValues: {1: name, ...} }
|
||||
* Inventory service sends flat lowercase: { item_id, icon, name, value, burden, armor_level, ... }
|
||||
*
|
||||
* MyWorldObject uses -1 as sentinel for "not set" on int/double properties.
|
||||
*/
|
||||
function normalizeInventoryItem(item) {
|
||||
if (!item) return item;
|
||||
if (item.name && item.item_id) return item;
|
||||
|
||||
// MyWorldObject uses -1 as "not set" sentinel — filter those out
|
||||
const v = (val) => (val !== undefined && val !== null && val !== -1) ? val : undefined;
|
||||
|
||||
if (!item.item_id) item.item_id = item.Id;
|
||||
if (!item.icon) item.icon = item.Icon;
|
||||
if (!item.object_class) item.object_class = item.ObjectClass;
|
||||
if (item.HasIdData !== undefined) item.has_id_data = item.HasIdData;
|
||||
|
||||
const baseName = item.Name || (item.StringValues && item.StringValues['1']) || null;
|
||||
const material = item.Material || null;
|
||||
if (material) {
|
||||
item.material = material;
|
||||
item.material_name = material;
|
||||
}
|
||||
|
||||
// Prepend material to name (e.g. "Pants" → "Satin Pants") matching inventory-service
|
||||
if (baseName) {
|
||||
if (material && !baseName.toLowerCase().startsWith(material.toLowerCase())) {
|
||||
item.name = material + ' ' + baseName;
|
||||
} else {
|
||||
item.name = baseName;
|
||||
}
|
||||
}
|
||||
|
||||
const iv = item.IntValues || {};
|
||||
if (item.value === undefined) item.value = v(item.Value) ?? v(iv['19']);
|
||||
if (item.burden === undefined) item.burden = v(item.Burden) ?? v(iv['5']);
|
||||
|
||||
// Container/equipment tracking
|
||||
if (item.container_id === undefined) item.container_id = item.ContainerId || 0;
|
||||
if (item.current_wielded_location === undefined) {
|
||||
item.current_wielded_location = v(item.CurrentWieldedLocation) ?? v(iv['10']) ?? 0;
|
||||
}
|
||||
if (item.items_capacity === undefined) item.items_capacity = v(item.ItemsCapacity) ?? v(iv['6']);
|
||||
|
||||
const armor = v(item.ArmorLevel);
|
||||
if (armor !== undefined) item.armor_level = armor;
|
||||
|
||||
const maxDmg = v(item.MaxDamage);
|
||||
if (maxDmg !== undefined) item.max_damage = maxDmg;
|
||||
|
||||
const dmgBonus = v(item.DamageBonus);
|
||||
if (dmgBonus !== undefined) item.damage_bonus = dmgBonus;
|
||||
|
||||
const atkBonus = v(item.AttackBonus);
|
||||
if (atkBonus !== undefined) item.attack_bonus = atkBonus;
|
||||
|
||||
const elemDmg = v(item.ElementalDmgBonus);
|
||||
if (elemDmg !== undefined) item.elemental_damage_vs_monsters = elemDmg;
|
||||
|
||||
const meleeD = v(item.MeleeDefenseBonus);
|
||||
if (meleeD !== undefined) item.melee_defense_bonus = meleeD;
|
||||
|
||||
const magicD = v(item.MagicDBonus);
|
||||
if (magicD !== undefined) item.magic_defense_bonus = magicD;
|
||||
|
||||
const missileD = v(item.MissileDBonus);
|
||||
if (missileD !== undefined) item.missile_defense_bonus = missileD;
|
||||
|
||||
const manaC = v(item.ManaCBonus);
|
||||
if (manaC !== undefined) item.mana_conversion_bonus = manaC;
|
||||
|
||||
const wieldLvl = v(item.WieldLevel);
|
||||
if (wieldLvl !== undefined) item.wield_level = wieldLvl;
|
||||
|
||||
const skillLvl = v(item.SkillLevel);
|
||||
if (skillLvl !== undefined) item.skill_level = skillLvl;
|
||||
|
||||
const loreLvl = v(item.LoreRequirement);
|
||||
if (loreLvl !== undefined) item.lore_requirement = loreLvl;
|
||||
|
||||
if (item.EquipSkill) item.equip_skill = item.EquipSkill;
|
||||
if (item.Mastery) item.mastery = item.Mastery;
|
||||
if (item.ItemSet) item.item_set = item.ItemSet;
|
||||
if (item.Imbue) item.imbue = item.Imbue;
|
||||
|
||||
const tinks = v(item.Tinks);
|
||||
if (tinks !== undefined) item.tinks = tinks;
|
||||
|
||||
const work = v(item.Workmanship);
|
||||
if (work !== undefined) item.workmanship = work;
|
||||
|
||||
const damR = v(item.DamRating);
|
||||
if (damR !== undefined) item.damage_rating = damR;
|
||||
|
||||
const critR = v(item.CritRating);
|
||||
if (critR !== undefined) item.crit_rating = critR;
|
||||
|
||||
const healR = v(item.HealBoostRating);
|
||||
if (healR !== undefined) item.heal_boost_rating = healR;
|
||||
|
||||
const vitalR = v(item.VitalityRating);
|
||||
if (vitalR !== undefined) item.vitality_rating = vitalR;
|
||||
|
||||
const critDmgR = v(item.CritDamRating);
|
||||
if (critDmgR !== undefined) item.crit_damage_rating = critDmgR;
|
||||
|
||||
const damResR = v(item.DamResistRating);
|
||||
if (damResR !== undefined) item.damage_resist_rating = damResR;
|
||||
|
||||
const critResR = v(item.CritResistRating);
|
||||
if (critResR !== undefined) item.crit_resist_rating = critResR;
|
||||
|
||||
const critDmgResR = v(item.CritDamResistRating);
|
||||
if (critDmgResR !== undefined) item.crit_damage_resist_rating = critDmgResR;
|
||||
|
||||
if (item.Spells && Array.isArray(item.Spells) && item.Spells.length > 0 && !item.spells) {
|
||||
item.spells = item.Spells;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single inventory slot DOM element from item data.
|
||||
* Used by both initial inventory load and live delta updates.
|
||||
|
|
@ -900,7 +1025,7 @@ function updateStatsTimeRange(content, name, timeRange) {
|
|||
function createInventorySlot(item) {
|
||||
const slot = document.createElement('div');
|
||||
slot.className = 'inventory-slot';
|
||||
slot.setAttribute('data-item-id', item.Id || item.id || item.item_id || 0);
|
||||
slot.setAttribute('data-item-id', item.item_id || item.Id || item.id || 0);
|
||||
|
||||
// Create layered icon container
|
||||
const iconContainer = document.createElement('div');
|
||||
|
|
@ -1020,9 +1145,83 @@ function createInventorySlot(item) {
|
|||
slot.addEventListener('mouseleave', hideInventoryTooltip);
|
||||
|
||||
slot.appendChild(iconContainer);
|
||||
|
||||
// Add stack count if > 1
|
||||
const stackCount = item.count || item.Count || item.stack_count || item.StackCount || 1;
|
||||
if (stackCount > 1) {
|
||||
const countEl = document.createElement('div');
|
||||
countEl.className = 'inventory-count';
|
||||
countEl.textContent = stackCount;
|
||||
slot.appendChild(countEl);
|
||||
}
|
||||
|
||||
return slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equipment slots mapping for the AC inventory layout.
|
||||
* Grid matches the real AC "Equipment Slots Enabled" paperdoll view.
|
||||
*
|
||||
* Layout (6 cols × 6 rows):
|
||||
* Col: 1 2 3 4 5 6
|
||||
* Row 1: Neck — Head Sigil(Blue) Sigil(Yellow) Sigil(Red)
|
||||
* Row 2: Trinket — ChestArmor — — Cloak
|
||||
* Row 3: Bracelet(L) UpperArmArmor AbdomenArmor — Bracelet(R) ChestWear(Shirt)
|
||||
* Row 4: Ring(L) LowerArmArmor UpperLegArmor — Ring(R) AbdomenWear(Pants)
|
||||
* Row 5: — Hands — LowerLegArmor — —
|
||||
* Row 6: Shield — — Feet Weapon Ammo
|
||||
*/
|
||||
const EQUIP_SLOTS = {
|
||||
// Row 1: Necklace, Head, 3× Aetheria/Sigil
|
||||
32768: { name: 'Neck', row: 1, col: 1 }, // EquipMask.NeckWear
|
||||
1: { name: 'Head', row: 1, col: 3 }, // EquipMask.HeadWear
|
||||
268435456: { name: 'Sigil (Blue)', row: 1, col: 5 }, // EquipMask.SigilOne
|
||||
536870912: { name: 'Sigil (Yellow)', row: 1, col: 6 }, // EquipMask.SigilTwo
|
||||
1073741824: { name: 'Sigil (Red)', row: 1, col: 7 }, // EquipMask.SigilThree
|
||||
|
||||
// Row 2: Trinket, Chest Armor, Cloak
|
||||
67108864: { name: 'Trinket', row: 2, col: 1 }, // EquipMask.TrinketOne
|
||||
2048: { name: 'Upper Arm Armor',row: 2, col: 2 }, // EquipMask.UpperArmArmor
|
||||
512: { name: 'Chest Armor', row: 2, col: 3 }, // EquipMask.ChestArmor
|
||||
134217728: { name: 'Cloak', row: 2, col: 7 }, // EquipMask.Cloak
|
||||
|
||||
// Row 3: Bracelet(L), Lower Arm Armor, Abdomen Armor, Upper Leg Armor, Bracelet(R), Shirt
|
||||
65536: { name: 'Bracelet (L)', row: 3, col: 1 }, // EquipMask.WristWearLeft
|
||||
4096: { name: 'Lower Arm Armor',row: 3, col: 2 }, // EquipMask.LowerArmArmor
|
||||
1024: { name: 'Abdomen Armor', row: 3, col: 3 }, // EquipMask.AbdomenArmor
|
||||
8192: { name: 'Upper Leg Armor',row: 3, col: 4 }, // EquipMask.UpperLegArmor
|
||||
131072: { name: 'Bracelet (R)', row: 3, col: 5 }, // EquipMask.WristWearRight
|
||||
2: { name: 'Shirt', row: 3, col: 7 }, // EquipMask.ChestWear
|
||||
|
||||
// Row 4: Ring(L), Hands, Lower Leg Armor, Ring(R), Pants
|
||||
262144: { name: 'Ring (L)', row: 4, col: 1 }, // EquipMask.FingerWearLeft
|
||||
32: { name: 'Hands', row: 4, col: 2 }, // EquipMask.HandWear
|
||||
16384: { name: 'Lower Leg Armor',row: 4, col: 4 }, // EquipMask.LowerLegArmor
|
||||
524288: { name: 'Ring (R)', row: 4, col: 5 }, // EquipMask.FingerWearRight
|
||||
4: { name: 'Pants', row: 4, col: 7 }, // EquipMask.AbdomenWear
|
||||
|
||||
// Row 5: Feet
|
||||
256: { name: 'Feet', row: 5, col: 4 }, // EquipMask.FootWear
|
||||
|
||||
// Row 6: Shield, Weapon, Ammo
|
||||
2097152: { name: 'Shield', row: 6, col: 1 }, // EquipMask.Shield
|
||||
1048576: { name: 'Melee Weapon', row: 6, col: 3 }, // EquipMask.MeleeWeapon
|
||||
4194304: { name: 'Missile Weapon', row: 6, col: 3 }, // EquipMask.MissileWeapon
|
||||
16777216: { name: 'Held', row: 6, col: 3 }, // EquipMask.Held
|
||||
33554432: { name: 'Two Handed', row: 6, col: 3 }, // EquipMask.TwoHanded
|
||||
8388608: { name: 'Ammo', row: 6, col: 7 }, // EquipMask.Ammunition
|
||||
};
|
||||
|
||||
const SLOT_COLORS = {};
|
||||
// Purple: jewelry
|
||||
[32768, 67108864, 65536, 131072, 262144, 524288].forEach(m => SLOT_COLORS[m] = 'slot-purple');
|
||||
// Blue: armor
|
||||
[1, 512, 2048, 1024, 4096, 8192, 16384, 32, 256].forEach(m => SLOT_COLORS[m] = 'slot-blue');
|
||||
// Teal: clothing/misc
|
||||
[2, 4, 134217728, 268435456, 536870912, 1073741824].forEach(m => SLOT_COLORS[m] = 'slot-teal');
|
||||
// Dark blue: weapons/combat
|
||||
[2097152, 1048576, 4194304, 16777216, 33554432, 8388608].forEach(m => SLOT_COLORS[m] = 'slot-darkblue');
|
||||
|
||||
/**
|
||||
* Handle live inventory delta updates from WebSocket.
|
||||
* Updates the inventory grid for a character if their inventory window is open.
|
||||
|
|
@ -1030,35 +1229,260 @@ function createInventorySlot(item) {
|
|||
function updateInventoryLive(delta) {
|
||||
const name = delta.character_name;
|
||||
const win = inventoryWindows[name];
|
||||
if (!win) return; // No inventory window open for this character
|
||||
if (!win || !win._inventoryState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const grid = win.querySelector('.inventory-grid');
|
||||
if (!grid) return;
|
||||
const state = win._inventoryState;
|
||||
const getItemId = (d) => {
|
||||
if (d.item) return d.item.item_id || d.item.Id || d.item.id;
|
||||
return d.item_id;
|
||||
};
|
||||
|
||||
const itemId = getItemId(delta);
|
||||
|
||||
if (delta.action === 'remove') {
|
||||
const itemId = delta.item_id || (delta.item && (delta.item.Id || delta.item.id));
|
||||
const existing = grid.querySelector(`[data-item-id="${itemId}"]`);
|
||||
if (existing) existing.remove();
|
||||
} else if (delta.action === 'add') {
|
||||
const newSlot = createInventorySlot(delta.item);
|
||||
grid.appendChild(newSlot);
|
||||
} else if (delta.action === 'update') {
|
||||
const itemId = delta.item.Id || delta.item.id || delta.item.item_id;
|
||||
const existing = grid.querySelector(`[data-item-id="${itemId}"]`);
|
||||
if (existing) {
|
||||
const newSlot = createInventorySlot(delta.item);
|
||||
existing.replaceWith(newSlot);
|
||||
state.items = state.items.filter(i => (i.item_id || i.Id || i.id) !== itemId);
|
||||
} else if (delta.action === 'add' || delta.action === 'update') {
|
||||
normalizeInventoryItem(delta.item);
|
||||
const existingIdx = state.items.findIndex(i => (i.item_id || i.Id || i.id) === itemId);
|
||||
if (existingIdx >= 0) {
|
||||
state.items[existingIdx] = delta.item;
|
||||
} else {
|
||||
const newSlot = createInventorySlot(delta.item);
|
||||
grid.appendChild(newSlot);
|
||||
state.items.push(delta.item);
|
||||
}
|
||||
}
|
||||
|
||||
// Update item count
|
||||
const countEl = win.querySelector('.inventory-count');
|
||||
if (countEl) {
|
||||
const slotCount = grid.querySelectorAll('.inventory-slot').length;
|
||||
countEl.textContent = `${slotCount} items`;
|
||||
renderInventoryState(state);
|
||||
}
|
||||
|
||||
function renderInventoryState(state) {
|
||||
// 1. Clear equipment slots
|
||||
state.slotMap.forEach((slotEl) => {
|
||||
slotEl.innerHTML = '';
|
||||
const colorClass = slotEl.className.match(/slot-\w+/)?.[0] || '';
|
||||
slotEl.className = `inv-equip-slot empty ${colorClass}`;
|
||||
delete slotEl.dataset.itemId;
|
||||
});
|
||||
|
||||
// 2. Identify containers (object_class === 10) by item_id for sidebar
|
||||
// These are packs/sacks/pouches/foci that appear in inventory as items
|
||||
// but should ONLY show in the pack sidebar, not in the item grid.
|
||||
const containers = []; // container objects (object_class=10)
|
||||
const containerItemIds = new Set(); // item_ids of containers (to exclude from grid)
|
||||
|
||||
state.items.forEach(item => {
|
||||
if (item.object_class === 10) {
|
||||
containers.push(item);
|
||||
containerItemIds.add(item.item_id);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Separate equipped items from pack items, excluding containers from grid
|
||||
let totalBurden = 0;
|
||||
const packItems = new Map(); // container_id → [items] (non-container items only)
|
||||
|
||||
// Determine the character body container_id: items with wielded_location > 0
|
||||
// share a container_id that is NOT 0 and NOT a pack's item_id.
|
||||
// We treat non-wielded items from the body container as "main backpack" items.
|
||||
let bodyContainerId = null;
|
||||
state.items.forEach(item => {
|
||||
if (item.current_wielded_location && item.current_wielded_location > 0) {
|
||||
const cid = item.container_id;
|
||||
if (cid && cid !== 0 && !containerItemIds.has(cid)) {
|
||||
bodyContainerId = cid;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.items.forEach(item => {
|
||||
totalBurden += (item.burden || 0);
|
||||
|
||||
// Skip container objects — they go in sidebar only
|
||||
if (containerItemIds.has(item.item_id)) return;
|
||||
|
||||
if (item.current_wielded_location && item.current_wielded_location > 0) {
|
||||
const mask = item.current_wielded_location;
|
||||
const isArmor = item.object_class === 2;
|
||||
|
||||
// For armor (object_class=2): render in ALL matching slots (multi-slot display)
|
||||
// For everything else (clothing, jewelry, weapons): place in first matching slot only
|
||||
if (isArmor) {
|
||||
Object.keys(EQUIP_SLOTS).forEach(m => {
|
||||
const slotMask = parseInt(m);
|
||||
if ((mask & slotMask) === slotMask) {
|
||||
const slotDef = EQUIP_SLOTS[slotMask];
|
||||
const key = `${slotDef.row}-${slotDef.col}`;
|
||||
if (state.slotMap.has(key)) {
|
||||
const slotEl = state.slotMap.get(key);
|
||||
if (!slotEl.dataset.itemId) {
|
||||
slotEl.innerHTML = '';
|
||||
const colorClass = slotEl.className.match(/slot-\w+/)?.[0] || '';
|
||||
slotEl.className = `inv-equip-slot equipped ${colorClass}`;
|
||||
slotEl.dataset.itemId = item.item_id;
|
||||
slotEl.appendChild(createInventorySlot(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Non-armor: find the first matching slot by exact mask key, then by bit overlap
|
||||
let placed = false;
|
||||
// Try exact mask match first (e.g. necklace mask=32768 matches key 32768 directly)
|
||||
if (EQUIP_SLOTS[mask]) {
|
||||
const slotDef = EQUIP_SLOTS[mask];
|
||||
const key = `${slotDef.row}-${slotDef.col}`;
|
||||
if (state.slotMap.has(key)) {
|
||||
const slotEl = state.slotMap.get(key);
|
||||
if (!slotEl.dataset.itemId) {
|
||||
slotEl.innerHTML = '';
|
||||
const colorClass = slotEl.className.match(/slot-\w+/)?.[0] || '';
|
||||
slotEl.className = `inv-equip-slot equipped ${colorClass}`;
|
||||
slotEl.dataset.itemId = item.item_id;
|
||||
slotEl.appendChild(createInventorySlot(item));
|
||||
placed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no exact match, find first matching bit in EQUIP_SLOTS
|
||||
if (!placed) {
|
||||
for (const m of Object.keys(EQUIP_SLOTS)) {
|
||||
const slotMask = parseInt(m);
|
||||
if ((mask & slotMask) === slotMask) {
|
||||
const slotDef = EQUIP_SLOTS[slotMask];
|
||||
const key = `${slotDef.row}-${slotDef.col}`;
|
||||
if (state.slotMap.has(key)) {
|
||||
const slotEl = state.slotMap.get(key);
|
||||
if (!slotEl.dataset.itemId) {
|
||||
slotEl.innerHTML = '';
|
||||
const colorClass = slotEl.className.match(/slot-\w+/)?.[0] || '';
|
||||
slotEl.className = `inv-equip-slot equipped ${colorClass}`;
|
||||
slotEl.dataset.itemId = item.item_id;
|
||||
slotEl.appendChild(createInventorySlot(item));
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Non-equipped, non-container → pack item. Group by container_id.
|
||||
let cid = item.container_id || 0;
|
||||
// Items on the character body (not wielded) → treat as main backpack (cid=0)
|
||||
if (bodyContainerId !== null && cid === bodyContainerId) cid = 0;
|
||||
if (!packItems.has(cid)) packItems.set(cid, []);
|
||||
packItems.get(cid).push(item);
|
||||
}
|
||||
});
|
||||
|
||||
state.burdenLabel.textContent = 'Burden';
|
||||
state.burdenFill.style.height = '0%';
|
||||
|
||||
// 4. Sort containers for stable sidebar order (by unsigned item_id)
|
||||
containers.sort((a, b) => {
|
||||
const ua = a.item_id >>> 0;
|
||||
const ub = b.item_id >>> 0;
|
||||
return ua - ub;
|
||||
});
|
||||
|
||||
// 5. Render packs in sidebar
|
||||
state.packList.innerHTML = '';
|
||||
|
||||
// Helper: compute icon URL from raw icon id
|
||||
const iconUrl = (iconRaw) => {
|
||||
if (!iconRaw) return '/icons/06001080.png';
|
||||
const hex = (iconRaw + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||
return `/icons/${hex}.png`;
|
||||
};
|
||||
|
||||
// --- Main backpack (container_id === 0, non-containers) ---
|
||||
const mainPackEl = document.createElement('div');
|
||||
mainPackEl.className = `inv-pack-icon ${state.activePack === null ? 'active' : ''}`;
|
||||
const mainPackImg = document.createElement('img');
|
||||
mainPackImg.src = '/icons/06001BB1.png';
|
||||
mainPackImg.onerror = function() { this.src = '/icons/06000133.png'; };
|
||||
|
||||
const mainFillCont = document.createElement('div');
|
||||
mainFillCont.className = 'inv-pack-fill-container';
|
||||
const mainFill = document.createElement('div');
|
||||
mainFill.className = 'inv-pack-fill';
|
||||
|
||||
// Main backpack items = container_id 0, excluding container objects
|
||||
const mainPackItems = packItems.get(0) || [];
|
||||
const mainPct = Math.min(100, (mainPackItems.length / 102) * 100);
|
||||
mainFill.style.height = `${mainPct}%`;
|
||||
|
||||
mainFillCont.appendChild(mainFill);
|
||||
mainPackEl.appendChild(mainPackImg);
|
||||
mainPackEl.appendChild(mainFillCont);
|
||||
|
||||
mainPackEl.onclick = () => {
|
||||
state.activePack = null;
|
||||
renderInventoryState(state);
|
||||
};
|
||||
state.packList.appendChild(mainPackEl);
|
||||
|
||||
// --- Sub-packs: each container object (object_class=10) ---
|
||||
containers.forEach(container => {
|
||||
const cid = container.item_id; // items inside this pack have container_id = this item_id
|
||||
const packEl = document.createElement('div');
|
||||
packEl.className = `inv-pack-icon ${state.activePack === cid ? 'active' : ''}`;
|
||||
const packImg = document.createElement('img');
|
||||
// Use the container's actual icon from the API
|
||||
packImg.src = iconUrl(container.icon);
|
||||
packImg.onerror = function() { this.src = '/icons/06001080.png'; };
|
||||
|
||||
const fillCont = document.createElement('div');
|
||||
fillCont.className = 'inv-pack-fill-container';
|
||||
const fill = document.createElement('div');
|
||||
fill.className = 'inv-pack-fill';
|
||||
|
||||
const pItems = packItems.get(cid) || [];
|
||||
const capacity = container.items_capacity || 24; // default pack capacity in AC
|
||||
const pPct = Math.min(100, (pItems.length / capacity) * 100);
|
||||
fill.style.height = `${pPct}%`;
|
||||
|
||||
fillCont.appendChild(fill);
|
||||
packEl.appendChild(packImg);
|
||||
packEl.appendChild(fillCont);
|
||||
|
||||
packEl.onclick = () => {
|
||||
state.activePack = cid;
|
||||
renderInventoryState(state);
|
||||
};
|
||||
state.packList.appendChild(packEl);
|
||||
});
|
||||
|
||||
// 6. Render item grid
|
||||
state.itemGrid.innerHTML = '';
|
||||
let itemsToShow = [];
|
||||
if (state.activePack === null) {
|
||||
// Main backpack: non-container items with container_id === 0
|
||||
itemsToShow = mainPackItems;
|
||||
state.contentsHeader.textContent = 'Contents of Backpack';
|
||||
} else {
|
||||
// Sub-pack: items with matching container_id
|
||||
itemsToShow = packItems.get(state.activePack) || [];
|
||||
// Use the container's name for the header
|
||||
const activeContainer = containers.find(c => c.item_id === state.activePack);
|
||||
state.contentsHeader.textContent = activeContainer
|
||||
? `Contents of ${activeContainer.name}`
|
||||
: 'Contents of Pack';
|
||||
}
|
||||
|
||||
const numCells = Math.max(24, Math.ceil(itemsToShow.length / 6) * 6);
|
||||
for (let i = 0; i < numCells; i++) {
|
||||
const cell = document.createElement('div');
|
||||
if (i < itemsToShow.length) {
|
||||
cell.className = 'inv-item-slot occupied';
|
||||
const itemNode = createInventorySlot(itemsToShow[i]);
|
||||
cell.appendChild(itemNode);
|
||||
} else {
|
||||
cell.className = 'inv-item-slot';
|
||||
}
|
||||
state.itemGrid.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1078,19 +1502,120 @@ function showInventoryWindow(name) {
|
|||
win.dataset.character = name;
|
||||
inventoryWindows[name] = win;
|
||||
|
||||
// Loading message
|
||||
const loading = document.createElement('div');
|
||||
loading.className = 'inventory-loading';
|
||||
loading.textContent = 'Loading inventory...';
|
||||
content.appendChild(loading);
|
||||
|
||||
// Inventory content container
|
||||
const invContent = document.createElement('div');
|
||||
invContent.className = 'inventory-content';
|
||||
invContent.style.display = 'none';
|
||||
content.appendChild(invContent);
|
||||
|
||||
// Fetch inventory data from main app (which will proxy to inventory service)
|
||||
const topSection = document.createElement('div');
|
||||
topSection.className = 'inv-top-section';
|
||||
|
||||
const equipGrid = document.createElement('div');
|
||||
equipGrid.className = 'inv-equipment-grid';
|
||||
|
||||
const slotMap = new Map();
|
||||
const createdSlots = new Set();
|
||||
|
||||
Object.entries(EQUIP_SLOTS).forEach(([mask, slotDef]) => {
|
||||
const key = `${slotDef.row}-${slotDef.col}`;
|
||||
if (!createdSlots.has(key)) {
|
||||
createdSlots.add(key);
|
||||
const slotEl = document.createElement('div');
|
||||
const colorClass = SLOT_COLORS[parseInt(mask)] || 'slot-darkblue';
|
||||
slotEl.className = `inv-equip-slot empty ${colorClass}`;
|
||||
slotEl.style.left = `${(slotDef.col - 1) * 44}px`;
|
||||
slotEl.style.top = `${(slotDef.row - 1) * 44}px`;
|
||||
slotEl.dataset.pos = key;
|
||||
equipGrid.appendChild(slotEl);
|
||||
slotMap.set(key, slotEl);
|
||||
}
|
||||
});
|
||||
|
||||
const sidebar = document.createElement('div');
|
||||
sidebar.className = 'inv-sidebar';
|
||||
|
||||
const burdenContainer = document.createElement('div');
|
||||
burdenContainer.className = 'inv-burden-bar';
|
||||
const burdenFill = document.createElement('div');
|
||||
burdenFill.className = 'inv-burden-fill';
|
||||
const burdenLabel = document.createElement('div');
|
||||
burdenLabel.className = 'inv-burden-label';
|
||||
burdenLabel.textContent = 'Burden';
|
||||
burdenContainer.appendChild(burdenLabel);
|
||||
burdenContainer.appendChild(burdenFill);
|
||||
sidebar.appendChild(burdenContainer);
|
||||
|
||||
const packList = document.createElement('div');
|
||||
packList.className = 'inv-pack-list';
|
||||
sidebar.appendChild(packList);
|
||||
|
||||
topSection.appendChild(equipGrid);
|
||||
topSection.appendChild(sidebar);
|
||||
|
||||
const bottomSection = document.createElement('div');
|
||||
bottomSection.className = 'inv-bottom-section';
|
||||
|
||||
const contentsHeader = document.createElement('div');
|
||||
contentsHeader.className = 'inv-contents-header';
|
||||
contentsHeader.textContent = 'Contents of Backpack';
|
||||
|
||||
const itemGrid = document.createElement('div');
|
||||
itemGrid.className = 'inv-item-grid';
|
||||
|
||||
bottomSection.appendChild(contentsHeader);
|
||||
bottomSection.appendChild(itemGrid);
|
||||
|
||||
invContent.appendChild(topSection);
|
||||
invContent.appendChild(bottomSection);
|
||||
|
||||
const resizeGrip = document.createElement('div');
|
||||
resizeGrip.className = 'inv-resize-grip';
|
||||
win.appendChild(resizeGrip);
|
||||
|
||||
let resizing = false;
|
||||
let startY, startH;
|
||||
|
||||
resizeGrip.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resizing = true;
|
||||
startY = e.clientY;
|
||||
startH = win.offsetHeight;
|
||||
document.body.style.cursor = 'ns-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!resizing) return;
|
||||
const newH = Math.max(400, startH + (e.clientY - startY));
|
||||
win.style.height = newH + 'px';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (!resizing) return;
|
||||
resizing = false;
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
});
|
||||
|
||||
win._inventoryState = {
|
||||
items: [],
|
||||
activePack: null,
|
||||
slotMap: slotMap,
|
||||
equipGrid: equipGrid,
|
||||
itemGrid: itemGrid,
|
||||
packList: packList,
|
||||
burdenFill: burdenFill,
|
||||
burdenLabel: burdenLabel,
|
||||
contentsHeader: contentsHeader,
|
||||
characterName: name
|
||||
};
|
||||
|
||||
fetch(`${API_BASE}/inventory/${encodeURIComponent(name)}?limit=1000`)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
|
@ -1098,24 +1623,12 @@ function showInventoryWindow(name) {
|
|||
})
|
||||
.then(data => {
|
||||
loading.style.display = 'none';
|
||||
invContent.style.display = 'block';
|
||||
|
||||
// Create inventory grid
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'inventory-grid';
|
||||
|
||||
// Render each item
|
||||
data.items.forEach(item => {
|
||||
grid.appendChild(createInventorySlot(item));
|
||||
});
|
||||
|
||||
invContent.appendChild(grid);
|
||||
|
||||
// Add item count
|
||||
const count = document.createElement('div');
|
||||
count.className = 'inventory-count';
|
||||
count.textContent = `${data.item_count} items`;
|
||||
invContent.appendChild(count);
|
||||
invContent.style.display = 'flex';
|
||||
|
||||
data.items.forEach(i => normalizeInventoryItem(i));
|
||||
win._inventoryState.items = data.items;
|
||||
|
||||
renderInventoryState(win._inventoryState);
|
||||
})
|
||||
.catch(err => {
|
||||
handleError('Inventory', err, true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue