feat: fix inventory service SQL injection, add slot population, and live frontend updates
- Replace f-string SQL interpolation with parameterized ANY(:ids) queries - Populate slot column from IntValues[231735296] (Decal Slot key) - Add startup migration to add container_id/slot columns to existing DB - Extract createInventorySlot() for reuse by initial load and live deltas - Add updateInventoryLive() handler for WebSocket inventory_delta messages - Add inventory_delta case to browser WebSocket message dispatcher Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
749652d534
commit
f145e6e131
2 changed files with 201 additions and 148 deletions
|
|
@ -358,7 +358,19 @@ async def startup():
|
||||||
|
|
||||||
# Create tables if they don't exist
|
# Create tables if they don't exist
|
||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
# Migrate: add container_id and slot columns if missing (added for live inventory)
|
||||||
|
from sqlalchemy import inspect as sa_inspect
|
||||||
|
inspector = sa_inspect(engine)
|
||||||
|
existing_columns = {c['name'] for c in inspector.get_columns('items')}
|
||||||
|
with engine.begin() as conn:
|
||||||
|
if 'container_id' not in existing_columns:
|
||||||
|
conn.execute(sa.text("ALTER TABLE items ADD COLUMN container_id BIGINT DEFAULT 0"))
|
||||||
|
logger.info("Migration: added container_id column to items table")
|
||||||
|
if 'slot' not in existing_columns:
|
||||||
|
conn.execute(sa.text("ALTER TABLE items ADD COLUMN slot INTEGER DEFAULT -1"))
|
||||||
|
logger.info("Migration: added slot column to items table")
|
||||||
|
|
||||||
# Create performance indexes
|
# Create performance indexes
|
||||||
create_indexes(engine)
|
create_indexes(engine)
|
||||||
|
|
||||||
|
|
@ -1407,6 +1419,7 @@ async def process_inventory(inventory: InventoryItem):
|
||||||
|
|
||||||
# Container/position tracking
|
# Container/position tracking
|
||||||
container_id=item_data.get('ContainerId', 0),
|
container_id=item_data.get('ContainerId', 0),
|
||||||
|
slot=int(item_data.get('IntValues', {}).get('231735296', item_data.get('IntValues', {}).get(231735296, -1))),
|
||||||
|
|
||||||
# Item state
|
# Item state
|
||||||
bonded=basic['bonded'],
|
bonded=basic['bonded'],
|
||||||
|
|
@ -1561,13 +1574,13 @@ async def upsert_inventory_item(character_name: str, item: Dict[str, Any]):
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
id_list = ','.join(str(row['id']) for row in existing)
|
db_ids = [row['id'] for row in existing]
|
||||||
await database.execute(f"DELETE FROM item_raw_data WHERE item_id IN ({id_list})")
|
for table in ('item_raw_data', 'item_combat_stats', 'item_requirements',
|
||||||
await database.execute(f"DELETE FROM item_combat_stats WHERE item_id IN ({id_list})")
|
'item_enhancements', 'item_ratings', 'item_spells'):
|
||||||
await database.execute(f"DELETE FROM item_requirements WHERE item_id IN ({id_list})")
|
await database.execute(
|
||||||
await database.execute(f"DELETE FROM item_enhancements WHERE item_id IN ({id_list})")
|
sa.text(f"DELETE FROM {table} WHERE item_id = ANY(:ids)"),
|
||||||
await database.execute(f"DELETE FROM item_ratings WHERE item_id IN ({id_list})")
|
{"ids": db_ids}
|
||||||
await database.execute(f"DELETE FROM item_spells WHERE item_id IN ({id_list})")
|
)
|
||||||
await database.execute(
|
await database.execute(
|
||||||
"DELETE FROM items WHERE character_name = :character_name AND item_id = :item_id",
|
"DELETE FROM items WHERE character_name = :character_name AND item_id = :item_id",
|
||||||
{"character_name": character_name, "item_id": item_game_id}
|
{"character_name": character_name, "item_id": item_game_id}
|
||||||
|
|
@ -1597,6 +1610,7 @@ async def upsert_inventory_item(character_name: str, item: Dict[str, Any]):
|
||||||
|
|
||||||
# Container/position tracking
|
# Container/position tracking
|
||||||
container_id=item.get('ContainerId', 0),
|
container_id=item.get('ContainerId', 0),
|
||||||
|
slot=int(item.get('IntValues', {}).get('231735296', item.get('IntValues', {}).get(231735296, -1))),
|
||||||
|
|
||||||
# Item state
|
# Item state
|
||||||
bonded=basic['bonded'],
|
bonded=basic['bonded'],
|
||||||
|
|
@ -1736,15 +1750,15 @@ async def delete_inventory_item(character_name: str, item_id: int):
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
id_list = ','.join(str(row['id']) for row in existing)
|
db_ids = [row['id'] for row in existing]
|
||||||
|
|
||||||
# Delete from all related tables first
|
# Delete from all related tables first
|
||||||
await database.execute(f"DELETE FROM item_raw_data WHERE item_id IN ({id_list})")
|
for table in ('item_raw_data', 'item_combat_stats', 'item_requirements',
|
||||||
await database.execute(f"DELETE FROM item_combat_stats WHERE item_id IN ({id_list})")
|
'item_enhancements', 'item_ratings', 'item_spells'):
|
||||||
await database.execute(f"DELETE FROM item_requirements WHERE item_id IN ({id_list})")
|
await database.execute(
|
||||||
await database.execute(f"DELETE FROM item_enhancements WHERE item_id IN ({id_list})")
|
sa.text(f"DELETE FROM {table} WHERE item_id = ANY(:ids)"),
|
||||||
await database.execute(f"DELETE FROM item_ratings WHERE item_id IN ({id_list})")
|
{"ids": db_ids}
|
||||||
await database.execute(f"DELETE FROM item_spells WHERE item_id IN ({id_list})")
|
)
|
||||||
|
|
||||||
# Delete from main items table
|
# Delete from main items table
|
||||||
await database.execute(
|
await database.execute(
|
||||||
|
|
|
||||||
305
static/script.js
305
static/script.js
|
|
@ -893,6 +893,175 @@ function updateStatsTimeRange(content, name, timeRange) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show or create an inventory window for a character
|
// Show or create an inventory window for a character
|
||||||
|
/**
|
||||||
|
* Create a single inventory slot DOM element from item data.
|
||||||
|
* Used by both initial inventory load and live delta updates.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Create layered icon container
|
||||||
|
const iconContainer = document.createElement('div');
|
||||||
|
iconContainer.className = 'item-icon-composite';
|
||||||
|
|
||||||
|
// Get base icon ID with portal.dat offset
|
||||||
|
const iconRaw = item.icon || item.Icon || 0;
|
||||||
|
const baseIconId = (iconRaw + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||||
|
|
||||||
|
// Check for overlay and underlay from enhanced format or legacy format
|
||||||
|
let overlayIconId = null;
|
||||||
|
let underlayIconId = null;
|
||||||
|
|
||||||
|
// Enhanced format (inventory service) - check for proper icon overlay/underlay properties
|
||||||
|
if (item.icon_overlay_id && item.icon_overlay_id > 0) {
|
||||||
|
overlayIconId = (item.icon_overlay_id + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.icon_underlay_id && item.icon_underlay_id > 0) {
|
||||||
|
underlayIconId = (item.icon_underlay_id + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Enhanced format (inventory service) - check spells object for decal info
|
||||||
|
if (!overlayIconId && !underlayIconId && item.spells && typeof item.spells === 'object') {
|
||||||
|
if (item.spells.spell_decal_218103838 && item.spells.spell_decal_218103838 > 100) {
|
||||||
|
overlayIconId = (item.spells.spell_decal_218103838 + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||||
|
}
|
||||||
|
if (item.spells.spell_decal_218103848 && item.spells.spell_decal_218103848 > 100) {
|
||||||
|
underlayIconId = (item.spells.spell_decal_218103848 + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||||
|
}
|
||||||
|
} else if (item.IntValues) {
|
||||||
|
// Raw delta format from plugin - IntValues directly on item
|
||||||
|
if (item.IntValues['218103849'] && item.IntValues['218103849'] > 100) {
|
||||||
|
overlayIconId = (item.IntValues['218103849'] + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||||
|
}
|
||||||
|
if (item.IntValues['218103850'] && item.IntValues['218103850'] > 100) {
|
||||||
|
underlayIconId = (item.IntValues['218103850'] + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||||
|
}
|
||||||
|
} else if (item.item_data) {
|
||||||
|
// Legacy format - parse item_data
|
||||||
|
try {
|
||||||
|
const itemData = typeof item.item_data === 'string' ? JSON.parse(item.item_data) : item.item_data;
|
||||||
|
if (itemData.IntValues) {
|
||||||
|
if (itemData.IntValues['218103849'] && itemData.IntValues['218103849'] > 100) {
|
||||||
|
overlayIconId = (itemData.IntValues['218103849'] + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||||
|
}
|
||||||
|
if (itemData.IntValues['218103850'] && itemData.IntValues['218103850'] > 100) {
|
||||||
|
underlayIconId = (itemData.IntValues['218103850'] + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse item data for', item.name || item.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create underlay (bottom layer)
|
||||||
|
if (underlayIconId) {
|
||||||
|
const underlayImg = document.createElement('img');
|
||||||
|
underlayImg.className = 'icon-underlay';
|
||||||
|
underlayImg.src = `/icons/${underlayIconId}.png`;
|
||||||
|
underlayImg.alt = 'underlay';
|
||||||
|
underlayImg.onerror = function() { this.style.display = 'none'; };
|
||||||
|
iconContainer.appendChild(underlayImg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create base icon (middle layer)
|
||||||
|
const baseImg = document.createElement('img');
|
||||||
|
baseImg.className = 'icon-base';
|
||||||
|
baseImg.src = `/icons/${baseIconId}.png`;
|
||||||
|
baseImg.alt = item.name || item.Name || 'Unknown Item';
|
||||||
|
baseImg.onerror = function() { this.src = '/icons/06000133.png'; };
|
||||||
|
iconContainer.appendChild(baseImg);
|
||||||
|
|
||||||
|
// Create overlay (top layer)
|
||||||
|
if (overlayIconId) {
|
||||||
|
const overlayImg = document.createElement('img');
|
||||||
|
overlayImg.className = 'icon-overlay';
|
||||||
|
overlayImg.src = `/icons/${overlayIconId}.png`;
|
||||||
|
overlayImg.alt = 'overlay';
|
||||||
|
overlayImg.onerror = function() { this.style.display = 'none'; };
|
||||||
|
iconContainer.appendChild(overlayImg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create tooltip data (handle both inventory-service format and raw plugin format)
|
||||||
|
const itemName = item.name || item.Name || 'Unknown Item';
|
||||||
|
slot.dataset.name = itemName;
|
||||||
|
slot.dataset.value = item.value || item.Value || 0;
|
||||||
|
slot.dataset.burden = item.burden || item.Burden || 0;
|
||||||
|
|
||||||
|
// Store enhanced data for tooltips
|
||||||
|
if (item.max_damage !== undefined || item.object_class_name !== undefined || item.spells !== undefined) {
|
||||||
|
const enhancedData = {};
|
||||||
|
const possibleProps = [
|
||||||
|
'max_damage', 'armor_level', 'damage_bonus', 'attack_bonus',
|
||||||
|
'wield_level', 'skill_level', 'lore_requirement', 'equip_skill', 'equip_skill_name',
|
||||||
|
'material', 'material_name', 'material_id', 'imbue', 'item_set', 'tinks',
|
||||||
|
'workmanship', 'workmanship_text', 'damage_rating', 'crit_rating',
|
||||||
|
'heal_boost_rating', 'has_id_data', 'object_class_name', 'spells',
|
||||||
|
'enhanced_properties', 'damage_range', 'damage_type', 'min_damage',
|
||||||
|
'speed_text', 'speed_value', 'mana_display', 'spellcraft', 'current_mana', 'max_mana',
|
||||||
|
'melee_defense_bonus', 'magic_defense_bonus', 'missile_defense_bonus',
|
||||||
|
'elemental_damage_vs_monsters', 'mana_conversion_bonus', 'icon_overlay_id', 'icon_underlay_id'
|
||||||
|
];
|
||||||
|
possibleProps.forEach(prop => {
|
||||||
|
if (item.hasOwnProperty(prop) && item[prop] !== undefined && item[prop] !== null) {
|
||||||
|
enhancedData[prop] = item[prop];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
slot.dataset.enhancedData = JSON.stringify(enhancedData);
|
||||||
|
} else {
|
||||||
|
slot.dataset.enhancedData = JSON.stringify({});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tooltip on hover
|
||||||
|
slot.addEventListener('mouseenter', e => showInventoryTooltip(e, slot));
|
||||||
|
slot.addEventListener('mousemove', e => showInventoryTooltip(e, slot));
|
||||||
|
slot.addEventListener('mouseleave', hideInventoryTooltip);
|
||||||
|
|
||||||
|
slot.appendChild(iconContainer);
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle live inventory delta updates from WebSocket.
|
||||||
|
* Updates the inventory grid for a character if their inventory window is open.
|
||||||
|
*/
|
||||||
|
function updateInventoryLive(delta) {
|
||||||
|
const name = delta.character_name;
|
||||||
|
const win = inventoryWindows[name];
|
||||||
|
if (!win) return; // No inventory window open for this character
|
||||||
|
|
||||||
|
const grid = win.querySelector('.inventory-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
if (delta.action === 'remove') {
|
||||||
|
const itemId = 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;
|
||||||
|
const existing = grid.querySelector(`[data-item-id="${itemId}"]`);
|
||||||
|
if (existing) {
|
||||||
|
const newSlot = createInventorySlot(delta.item);
|
||||||
|
existing.replaceWith(newSlot);
|
||||||
|
} else {
|
||||||
|
const newSlot = createInventorySlot(delta.item);
|
||||||
|
grid.appendChild(newSlot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update item count
|
||||||
|
const countEl = win.querySelector('.inventory-count');
|
||||||
|
if (countEl) {
|
||||||
|
const slotCount = grid.querySelectorAll('.inventory-slot').length;
|
||||||
|
countEl.textContent = `${slotCount} items`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showInventoryWindow(name) {
|
function showInventoryWindow(name) {
|
||||||
debugLog('showInventoryWindow called for:', name);
|
debugLog('showInventoryWindow called for:', name);
|
||||||
const windowId = `inventoryWindow-${name}`;
|
const windowId = `inventoryWindow-${name}`;
|
||||||
|
|
@ -937,139 +1106,7 @@ function showInventoryWindow(name) {
|
||||||
|
|
||||||
// Render each item
|
// Render each item
|
||||||
data.items.forEach(item => {
|
data.items.forEach(item => {
|
||||||
|
grid.appendChild(createInventorySlot(item));
|
||||||
const slot = document.createElement('div');
|
|
||||||
slot.className = 'inventory-slot';
|
|
||||||
|
|
||||||
// Create layered icon container
|
|
||||||
const iconContainer = document.createElement('div');
|
|
||||||
iconContainer.className = 'item-icon-composite';
|
|
||||||
|
|
||||||
// Get base icon ID with portal.dat offset
|
|
||||||
const baseIconId = (item.icon + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
|
||||||
|
|
||||||
// Check for overlay and underlay from enhanced format or legacy format
|
|
||||||
let overlayIconId = null;
|
|
||||||
let underlayIconId = null;
|
|
||||||
|
|
||||||
// Enhanced format (inventory service) - check for proper icon overlay/underlay properties
|
|
||||||
if (item.icon_overlay_id && item.icon_overlay_id > 0) {
|
|
||||||
overlayIconId = (item.icon_overlay_id + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.icon_underlay_id && item.icon_underlay_id > 0) {
|
|
||||||
underlayIconId = (item.icon_underlay_id + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Enhanced format (inventory service) - check spells object for decal info
|
|
||||||
if (!overlayIconId && !underlayIconId && item.spells && typeof item.spells === 'object') {
|
|
||||||
// Icon overlay (using the actual property names from the data)
|
|
||||||
// Only use valid icon IDs (must be > 100 to avoid invalid small IDs)
|
|
||||||
if (item.spells.spell_decal_218103838 && item.spells.spell_decal_218103838 > 100) {
|
|
||||||
overlayIconId = (item.spells.spell_decal_218103838 + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Icon underlay
|
|
||||||
if (item.spells.spell_decal_218103848 && item.spells.spell_decal_218103848 > 100) {
|
|
||||||
underlayIconId = (item.spells.spell_decal_218103848 + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
|
||||||
}
|
|
||||||
} else if (item.item_data) {
|
|
||||||
// Legacy format - parse item_data
|
|
||||||
try {
|
|
||||||
const itemData = typeof item.item_data === 'string' ? JSON.parse(item.item_data) : item.item_data;
|
|
||||||
|
|
||||||
if (itemData.IntValues) {
|
|
||||||
// Icon overlay (ID 218103849) - only use valid icon IDs
|
|
||||||
if (itemData.IntValues['218103849'] && itemData.IntValues['218103849'] > 100) {
|
|
||||||
overlayIconId = (itemData.IntValues['218103849'] + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Icon underlay (ID 218103850) - only use valid icon IDs
|
|
||||||
if (itemData.IntValues['218103850'] && itemData.IntValues['218103850'] > 100) {
|
|
||||||
underlayIconId = (itemData.IntValues['218103850'] + 0x06000000).toString(16).toUpperCase().padStart(8, '0');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to parse item data for', item.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create underlay (bottom layer)
|
|
||||||
if (underlayIconId) {
|
|
||||||
const underlayImg = document.createElement('img');
|
|
||||||
underlayImg.className = 'icon-underlay';
|
|
||||||
underlayImg.src = `/icons/${underlayIconId}.png`;
|
|
||||||
underlayImg.alt = 'underlay';
|
|
||||||
underlayImg.onerror = function() { this.style.display = 'none'; };
|
|
||||||
iconContainer.appendChild(underlayImg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create base icon (middle layer)
|
|
||||||
const baseImg = document.createElement('img');
|
|
||||||
baseImg.className = 'icon-base';
|
|
||||||
baseImg.src = `/icons/${baseIconId}.png`;
|
|
||||||
baseImg.alt = item.name || 'Unknown Item';
|
|
||||||
baseImg.onerror = function() {
|
|
||||||
// Final fallback
|
|
||||||
this.src = '/icons/06000133.png';
|
|
||||||
};
|
|
||||||
iconContainer.appendChild(baseImg);
|
|
||||||
|
|
||||||
// Create overlay (top layer)
|
|
||||||
if (overlayIconId) {
|
|
||||||
const overlayImg = document.createElement('img');
|
|
||||||
overlayImg.className = 'icon-overlay';
|
|
||||||
overlayImg.src = `/icons/${overlayIconId}.png`;
|
|
||||||
overlayImg.alt = 'overlay';
|
|
||||||
overlayImg.onerror = function() { this.style.display = 'none'; };
|
|
||||||
iconContainer.appendChild(overlayImg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create tooltip data
|
|
||||||
slot.dataset.name = item.name || 'Unknown Item';
|
|
||||||
slot.dataset.value = item.value || 0;
|
|
||||||
slot.dataset.burden = item.burden || 0;
|
|
||||||
|
|
||||||
// Store enhanced data for tooltips
|
|
||||||
// All data now comes from inventory service (no more local fallback)
|
|
||||||
if (item.max_damage !== undefined || item.object_class_name !== undefined || item.spells !== undefined) {
|
|
||||||
// Inventory service provides clean, structured data with translations
|
|
||||||
// Only include properties that actually exist on the item
|
|
||||||
const enhancedData = {};
|
|
||||||
|
|
||||||
// Check all possible enhanced properties from inventory service
|
|
||||||
const possibleProps = [
|
|
||||||
'max_damage', 'armor_level', 'damage_bonus', 'attack_bonus',
|
|
||||||
'wield_level', 'skill_level', 'lore_requirement', 'equip_skill', 'equip_skill_name',
|
|
||||||
'material', 'material_name', 'material_id', 'imbue', 'item_set', 'tinks',
|
|
||||||
'workmanship', 'workmanship_text', 'damage_rating', 'crit_rating',
|
|
||||||
'heal_boost_rating', 'has_id_data', 'object_class_name', 'spells',
|
|
||||||
'enhanced_properties', 'damage_range', 'damage_type', 'min_damage',
|
|
||||||
'speed_text', 'speed_value', 'mana_display', 'spellcraft', 'current_mana', 'max_mana',
|
|
||||||
'melee_defense_bonus', 'magic_defense_bonus', 'missile_defense_bonus',
|
|
||||||
'elemental_damage_vs_monsters', 'mana_conversion_bonus', 'icon_overlay_id', 'icon_underlay_id'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Only add properties that exist and have meaningful values
|
|
||||||
possibleProps.forEach(prop => {
|
|
||||||
if (item.hasOwnProperty(prop) && item[prop] !== undefined && item[prop] !== null) {
|
|
||||||
enhancedData[prop] = item[prop];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
slot.dataset.enhancedData = JSON.stringify(enhancedData);
|
|
||||||
} else {
|
|
||||||
// No enhanced data available
|
|
||||||
slot.dataset.enhancedData = JSON.stringify({});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tooltip on hover
|
|
||||||
slot.addEventListener('mouseenter', e => showInventoryTooltip(e, slot));
|
|
||||||
slot.addEventListener('mousemove', e => showInventoryTooltip(e, slot));
|
|
||||||
slot.addEventListener('mouseleave', hideInventoryTooltip);
|
|
||||||
|
|
||||||
slot.appendChild(iconContainer);
|
|
||||||
grid.appendChild(slot);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
invContent.appendChild(grid);
|
invContent.appendChild(grid);
|
||||||
|
|
@ -2301,6 +2338,8 @@ function initWebSocket() {
|
||||||
} else if (msg.type === 'character_stats') {
|
} else if (msg.type === 'character_stats') {
|
||||||
characterStats[msg.character_name] = msg;
|
characterStats[msg.character_name] = msg;
|
||||||
updateCharacterWindow(msg.character_name, msg);
|
updateCharacterWindow(msg.character_name, msg);
|
||||||
|
} else if (msg.type === 'inventory_delta') {
|
||||||
|
updateInventoryLive(msg);
|
||||||
} else if (msg.type === 'server_status') {
|
} else if (msg.type === 'server_status') {
|
||||||
handleServerStatusUpdate(msg);
|
handleServerStatusUpdate(msg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue