feat: update inventory frontend and services to current production state

This commit is contained in:
erik 2026-03-07 08:37:32 +00:00
parent 7050cfb8b7
commit fc557ab1d5
4 changed files with 1321 additions and 307 deletions

View file

@ -1363,7 +1363,7 @@ async def process_inventory(inventory: InventoryItem):
for table in ('item_raw_data', 'item_combat_stats', 'item_requirements',
'item_enhancements', 'item_ratings', 'item_spells'):
await database.execute(
sa.text(f"DELETE FROM {table} WHERE item_id = ANY(:ids)"),
f"DELETE FROM {table} WHERE item_id = ANY(:ids)",
{"ids": db_ids}
)
@ -1577,7 +1577,7 @@ async def upsert_inventory_item(character_name: str, item: Dict[str, Any]):
for table in ('item_raw_data', 'item_combat_stats', 'item_requirements',
'item_enhancements', 'item_ratings', 'item_spells'):
await database.execute(
sa.text(f"DELETE FROM {table} WHERE item_id = ANY(:ids)"),
f"DELETE FROM {table} WHERE item_id = ANY(:ids)",
{"ids": db_ids}
)
await database.execute(
@ -1730,7 +1730,35 @@ async def upsert_inventory_item(character_name: str, item: Dict[str, Any]):
raise HTTPException(status_code=500, detail=error_msg)
logger.info(f"Single item upsert for {character_name}: item_id={item_game_id}, processed={processed_count}")
return {"status": "ok", "processed": processed_count}
# Fetch the just-upserted item with all joins and enrich it
enriched_item = None
try:
enrich_query = """
SELECT
i.id, i.item_id, i.name, i.icon, i.object_class, i.value, i.burden,
i.has_id_data, i.timestamp,
i.current_wielded_location, i.container_id, i.items_capacity,
cs.max_damage, cs.armor_level, cs.damage_bonus, cs.attack_bonus,
r.wield_level, r.skill_level, r.equip_skill, r.lore_requirement,
e.material, e.imbue, e.item_set, e.tinks, e.workmanship,
rt.damage_rating, rt.crit_rating, rt.heal_boost_rating,
rd.int_values, rd.double_values, rd.string_values, rd.bool_values, rd.original_json
FROM items i
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id
LEFT JOIN item_requirements r ON i.id = r.item_id
LEFT JOIN item_enhancements e ON i.id = e.item_id
LEFT JOIN item_ratings rt ON i.id = rt.item_id
LEFT JOIN item_raw_data rd ON i.id = rd.item_id
WHERE i.id = :db_id
"""
row = await database.fetch_one(enrich_query, {"db_id": db_item_id})
if row:
enriched_item = enrich_db_item(row)
except Exception as e:
logger.warning(f"Failed to enrich item after upsert: {e}")
return {"status": "ok", "processed": processed_count, "item": enriched_item}
@app.delete("/inventory/{character_name}/item/{item_id}",
@ -1755,7 +1783,7 @@ async def delete_inventory_item(character_name: str, item_id: int):
for table in ('item_raw_data', 'item_combat_stats', 'item_requirements',
'item_enhancements', 'item_ratings', 'item_spells'):
await database.execute(
sa.text(f"DELETE FROM {table} WHERE item_id = ANY(:ids)"),
f"DELETE FROM {table} WHERE item_id = ANY(:ids)",
{"ids": db_ids}
)
@ -1771,6 +1799,224 @@ async def delete_inventory_item(character_name: str, item_id: int):
return {"status": "ok", "deleted": deleted_count}
def enrich_db_item(item) -> dict:
"""Enrich a single DB row (from the JOIN query) into the full frontend format.
Takes a mapping (e.g. asyncpg Record or dict) from the items+joins query and returns
a clean dict with translated material names, spell info, combat stats, ratings,
workmanship text, mana display, etc. Identical logic to what get_character_inventory
used inline extracted here so upsert_inventory_item can reuse it.
"""
processed_item = dict(item)
# Get comprehensive translations from original_json
if processed_item.get('original_json'):
original_json = processed_item['original_json']
# Handle case where original_json might be stored as string
if isinstance(original_json, str):
try:
original_json = json.loads(original_json)
except (json.JSONDecodeError, TypeError):
original_json = {}
if original_json:
# Extract properties and get translations
properties = extract_item_properties(original_json)
# Add translated properties to the item
processed_item['translated_properties'] = properties
# Add material name - use material directly if it's already a string
if processed_item.get('material'):
if isinstance(processed_item['material'], str):
processed_item['material_name'] = processed_item['material']
else:
processed_item['material_name'] = translate_material_type(processed_item['material'])
# Add object class translation
if processed_item.get('object_class'):
processed_item['object_class_name'] = translate_object_class(processed_item['object_class'], original_json)
# Add skill translation
if processed_item.get('equip_skill'):
processed_item['equip_skill_name'] = translate_skill(processed_item['equip_skill'])
# Flatten the structure - move translated properties to top level
if 'translated_properties' in processed_item:
translated_props = processed_item.pop('translated_properties')
# Move spells to top level
if 'spells' in translated_props:
processed_item['spells'] = translated_props['spells']
# Move translated_ints to top level for enhanced tooltips
if 'translated_ints' in translated_props:
processed_item['enhanced_properties'] = translated_props['translated_ints']
# Add weapon damage information
if 'weapon_damage' in translated_props:
weapon_damage = translated_props['weapon_damage']
if weapon_damage.get('damage_range'):
processed_item['damage_range'] = weapon_damage['damage_range']
if weapon_damage.get('damage_type'):
processed_item['damage_type'] = weapon_damage['damage_type']
if weapon_damage.get('min_damage'):
processed_item['min_damage'] = weapon_damage['min_damage']
# Add weapon speed information
if 'weapon_speed' in translated_props:
speed_info = translated_props['weapon_speed']
if speed_info.get('speed_text'):
processed_item['speed_text'] = speed_info['speed_text']
if speed_info.get('speed_value'):
processed_item['speed_value'] = speed_info['speed_value']
# Add mana and spellcraft information
if 'mana_info' in translated_props:
mana_info = translated_props['mana_info']
if mana_info.get('mana_display'):
processed_item['mana_display'] = mana_info['mana_display']
if mana_info.get('spellcraft'):
processed_item['spellcraft'] = mana_info['spellcraft']
if mana_info.get('current_mana'):
processed_item['current_mana'] = mana_info['current_mana']
if mana_info.get('max_mana'):
processed_item['max_mana'] = mana_info['max_mana']
# Add icon overlay/underlay information from translated properties
if 'translated_ints' in translated_props:
translated_ints = translated_props['translated_ints']
# Icon overlay - check for the proper property name
if 'IconOverlay_Decal_DID' in translated_ints and translated_ints['IconOverlay_Decal_DID'] > 0:
processed_item['icon_overlay_id'] = translated_ints['IconOverlay_Decal_DID']
# Icon underlay - check for the proper property name
if 'IconUnderlay_Decal_DID' in translated_ints and translated_ints['IconUnderlay_Decal_DID'] > 0:
processed_item['icon_underlay_id'] = translated_ints['IconUnderlay_Decal_DID']
# Add comprehensive combat stats
if 'combat' in translated_props:
combat_stats = translated_props['combat']
if combat_stats.get('max_damage', -1) != -1:
processed_item['max_damage'] = combat_stats['max_damage']
if combat_stats.get('armor_level', -1) != -1:
processed_item['armor_level'] = combat_stats['armor_level']
if combat_stats.get('damage_bonus', -1.0) != -1.0:
processed_item['damage_bonus'] = combat_stats['damage_bonus']
if combat_stats.get('attack_bonus', -1.0) != -1.0:
processed_item['attack_bonus'] = combat_stats['attack_bonus']
# Add missing combat bonuses
if combat_stats.get('melee_defense_bonus', -1.0) != -1.0:
processed_item['melee_defense_bonus'] = combat_stats['melee_defense_bonus']
if combat_stats.get('magic_defense_bonus', -1.0) != -1.0:
processed_item['magic_defense_bonus'] = combat_stats['magic_defense_bonus']
if combat_stats.get('missile_defense_bonus', -1.0) != -1.0:
processed_item['missile_defense_bonus'] = combat_stats['missile_defense_bonus']
if combat_stats.get('elemental_damage_vs_monsters', -1.0) != -1.0:
processed_item['elemental_damage_vs_monsters'] = combat_stats['elemental_damage_vs_monsters']
if combat_stats.get('mana_conversion_bonus', -1.0) != -1.0:
processed_item['mana_conversion_bonus'] = combat_stats['mana_conversion_bonus']
# Add comprehensive requirements
if 'requirements' in translated_props:
requirements = translated_props['requirements']
if requirements.get('wield_level', -1) != -1:
processed_item['wield_level'] = requirements['wield_level']
if requirements.get('skill_level', -1) != -1:
processed_item['skill_level'] = requirements['skill_level']
if requirements.get('lore_requirement', -1) != -1:
processed_item['lore_requirement'] = requirements['lore_requirement']
if requirements.get('equip_skill'):
processed_item['equip_skill'] = requirements['equip_skill']
# Add comprehensive enhancements
if 'enhancements' in translated_props:
enhancements = translated_props['enhancements']
if enhancements.get('material'):
processed_item['material'] = enhancements['material']
# Add material information from translations
if 'translations' in translated_props:
trans = translated_props['translations']
if trans.get('material_name'):
processed_item['material_name'] = trans['material_name']
if trans.get('material_id'):
processed_item['material_id'] = trans['material_id']
if trans.get('item_type_name'):
processed_item['item_type_name'] = trans['item_type_name']
# Continue with other enhancements
if 'enhancements' in translated_props:
enhancements = translated_props['enhancements']
if enhancements.get('imbue'):
processed_item['imbue'] = enhancements['imbue']
if enhancements.get('tinks', -1) != -1:
processed_item['tinks'] = enhancements['tinks']
if enhancements.get('workmanship', -1.0) != -1.0:
processed_item['workmanship'] = enhancements['workmanship']
processed_item['workmanship_text'] = translate_workmanship(int(enhancements['workmanship']))
if enhancements.get('item_set'):
processed_item['item_set'] = enhancements['item_set']
# Add equipment set name translation
set_id = str(enhancements['item_set']).strip()
dictionaries = ENUM_MAPPINGS.get('dictionaries', {})
attribute_set_info = dictionaries.get('AttributeSetInfo', {}).get('values', {})
if set_id in attribute_set_info:
processed_item['item_set_name'] = attribute_set_info[set_id]
else:
processed_item['item_set_name'] = f"Set {set_id}"
# Add comprehensive ratings (use gear totals as fallback for armor/clothing)
if 'ratings' in translated_props:
ratings = translated_props['ratings']
# Damage rating: use individual rating or gear total
if ratings.get('damage_rating', -1) != -1:
processed_item['damage_rating'] = ratings['damage_rating']
elif ratings.get('gear_damage', -1) > 0:
processed_item['damage_rating'] = ratings['gear_damage']
# Crit rating
if ratings.get('crit_rating', -1) != -1:
processed_item['crit_rating'] = ratings['crit_rating']
elif ratings.get('gear_crit', -1) > 0:
processed_item['crit_rating'] = ratings['gear_crit']
# Crit damage rating: use individual rating or gear total
if ratings.get('crit_damage_rating', -1) != -1:
processed_item['crit_damage_rating'] = ratings['crit_damage_rating']
elif ratings.get('gear_crit_damage', -1) > 0:
processed_item['crit_damage_rating'] = ratings['gear_crit_damage']
# Heal boost rating: use individual rating or gear total
if ratings.get('heal_boost_rating', -1) != -1:
processed_item['heal_boost_rating'] = ratings['heal_boost_rating']
elif ratings.get('gear_healing_boost', -1) > 0:
processed_item['heal_boost_rating'] = ratings['gear_healing_boost']
# Apply material prefix to item name if material exists
if processed_item.get('material_name') and processed_item.get('name'):
original_name = processed_item['name']
material_name = processed_item['material_name']
# Don't add prefix if name already starts with the material
if not original_name.lower().startswith(material_name.lower()):
processed_item['name'] = f"{material_name} {original_name}"
processed_item['original_name'] = original_name # Preserve original for reference
# Remove raw data from response (keep clean output)
processed_item.pop('int_values', None)
processed_item.pop('double_values', None)
processed_item.pop('string_values', None)
processed_item.pop('bool_values', None)
processed_item.pop('original_json', None)
# Remove null values for cleaner response
processed_item = {k: v for k, v in processed_item.items() if v is not None}
return processed_item
@app.get("/inventory/{character_name}",
summary="Get Character Inventory",
description="Retrieve processed inventory data for a specific character with normalized item properties.",
@ -1784,8 +2030,9 @@ async def get_character_inventory(
query = """
SELECT
i.id, i.name, i.icon, i.object_class, i.value, i.burden,
i.id, i.item_id, i.name, i.icon, i.object_class, i.value, i.burden,
i.has_id_data, i.timestamp,
i.current_wielded_location, i.container_id, i.items_capacity,
cs.max_damage, cs.armor_level, cs.damage_bonus, cs.attack_bonus,
r.wield_level, r.skill_level, r.equip_skill, r.lore_requirement,
e.material, e.imbue, e.item_set, e.tinks, e.workmanship,
@ -1812,216 +2059,7 @@ async def get_character_inventory(
raise HTTPException(status_code=404, detail=f"No inventory found for {character_name}")
# Convert to structured format with enhanced translations
processed_items = []
for item in items:
processed_item = dict(item)
# Get comprehensive translations from original_json
if processed_item.get('original_json'):
original_json = processed_item['original_json']
# Handle case where original_json might be stored as string
if isinstance(original_json, str):
try:
original_json = json.loads(original_json)
except (json.JSONDecodeError, TypeError):
original_json = {}
if original_json:
# Extract properties and get translations
properties = extract_item_properties(original_json)
# Add translated properties to the item
processed_item['translated_properties'] = properties
# Add material name - use material directly if it's already a string
if processed_item.get('material'):
if isinstance(processed_item['material'], str):
processed_item['material_name'] = processed_item['material']
else:
processed_item['material_name'] = translate_material_type(processed_item['material'])
# Add object class translation
if processed_item.get('object_class'):
processed_item['object_class_name'] = translate_object_class(processed_item['object_class'], original_json)
# Add skill translation
if processed_item.get('equip_skill'):
processed_item['equip_skill_name'] = translate_skill(processed_item['equip_skill'])
# Flatten the structure - move translated properties to top level
if 'translated_properties' in processed_item:
translated_props = processed_item.pop('translated_properties')
# Move spells to top level
if 'spells' in translated_props:
processed_item['spells'] = translated_props['spells']
# Move translated_ints to top level for enhanced tooltips
if 'translated_ints' in translated_props:
processed_item['enhanced_properties'] = translated_props['translated_ints']
# Add weapon damage information
if 'weapon_damage' in translated_props:
weapon_damage = translated_props['weapon_damage']
if weapon_damage.get('damage_range'):
processed_item['damage_range'] = weapon_damage['damage_range']
if weapon_damage.get('damage_type'):
processed_item['damage_type'] = weapon_damage['damage_type']
if weapon_damage.get('min_damage'):
processed_item['min_damage'] = weapon_damage['min_damage']
# Add weapon speed information
if 'weapon_speed' in translated_props:
speed_info = translated_props['weapon_speed']
if speed_info.get('speed_text'):
processed_item['speed_text'] = speed_info['speed_text']
if speed_info.get('speed_value'):
processed_item['speed_value'] = speed_info['speed_value']
# Add mana and spellcraft information
if 'mana_info' in translated_props:
mana_info = translated_props['mana_info']
if mana_info.get('mana_display'):
processed_item['mana_display'] = mana_info['mana_display']
if mana_info.get('spellcraft'):
processed_item['spellcraft'] = mana_info['spellcraft']
if mana_info.get('current_mana'):
processed_item['current_mana'] = mana_info['current_mana']
if mana_info.get('max_mana'):
processed_item['max_mana'] = mana_info['max_mana']
# Add icon overlay/underlay information from translated properties
if 'translated_ints' in translated_props:
translated_ints = translated_props['translated_ints']
# Icon overlay - check for the proper property name
if 'IconOverlay_Decal_DID' in translated_ints and translated_ints['IconOverlay_Decal_DID'] > 0:
processed_item['icon_overlay_id'] = translated_ints['IconOverlay_Decal_DID']
# Icon underlay - check for the proper property name
if 'IconUnderlay_Decal_DID' in translated_ints and translated_ints['IconUnderlay_Decal_DID'] > 0:
processed_item['icon_underlay_id'] = translated_ints['IconUnderlay_Decal_DID']
# Add comprehensive combat stats
if 'combat' in translated_props:
combat_stats = translated_props['combat']
if combat_stats.get('max_damage', -1) != -1:
processed_item['max_damage'] = combat_stats['max_damage']
if combat_stats.get('armor_level', -1) != -1:
processed_item['armor_level'] = combat_stats['armor_level']
if combat_stats.get('damage_bonus', -1.0) != -1.0:
processed_item['damage_bonus'] = combat_stats['damage_bonus']
if combat_stats.get('attack_bonus', -1.0) != -1.0:
processed_item['attack_bonus'] = combat_stats['attack_bonus']
# Add missing combat bonuses
if combat_stats.get('melee_defense_bonus', -1.0) != -1.0:
processed_item['melee_defense_bonus'] = combat_stats['melee_defense_bonus']
if combat_stats.get('magic_defense_bonus', -1.0) != -1.0:
processed_item['magic_defense_bonus'] = combat_stats['magic_defense_bonus']
if combat_stats.get('missile_defense_bonus', -1.0) != -1.0:
processed_item['missile_defense_bonus'] = combat_stats['missile_defense_bonus']
if combat_stats.get('elemental_damage_vs_monsters', -1.0) != -1.0:
processed_item['elemental_damage_vs_monsters'] = combat_stats['elemental_damage_vs_monsters']
if combat_stats.get('mana_conversion_bonus', -1.0) != -1.0:
processed_item['mana_conversion_bonus'] = combat_stats['mana_conversion_bonus']
# Add comprehensive requirements
if 'requirements' in translated_props:
requirements = translated_props['requirements']
if requirements.get('wield_level', -1) != -1:
processed_item['wield_level'] = requirements['wield_level']
if requirements.get('skill_level', -1) != -1:
processed_item['skill_level'] = requirements['skill_level']
if requirements.get('lore_requirement', -1) != -1:
processed_item['lore_requirement'] = requirements['lore_requirement']
if requirements.get('equip_skill'):
processed_item['equip_skill'] = requirements['equip_skill']
# Add comprehensive enhancements
if 'enhancements' in translated_props:
enhancements = translated_props['enhancements']
if enhancements.get('material'):
processed_item['material'] = enhancements['material']
# Add material information from translations
if 'translations' in translated_props:
trans = translated_props['translations']
if trans.get('material_name'):
processed_item['material_name'] = trans['material_name']
if trans.get('material_id'):
processed_item['material_id'] = trans['material_id']
if trans.get('item_type_name'):
processed_item['item_type_name'] = trans['item_type_name']
# Continue with other enhancements
if 'enhancements' in translated_props:
enhancements = translated_props['enhancements']
if enhancements.get('imbue'):
processed_item['imbue'] = enhancements['imbue']
if enhancements.get('tinks', -1) != -1:
processed_item['tinks'] = enhancements['tinks']
if enhancements.get('workmanship', -1.0) != -1.0:
processed_item['workmanship'] = enhancements['workmanship']
processed_item['workmanship_text'] = translate_workmanship(int(enhancements['workmanship']))
if enhancements.get('item_set'):
processed_item['item_set'] = enhancements['item_set']
# Add equipment set name translation
set_id = str(enhancements['item_set']).strip()
dictionaries = ENUM_MAPPINGS.get('dictionaries', {})
attribute_set_info = dictionaries.get('AttributeSetInfo', {}).get('values', {})
if set_id in attribute_set_info:
processed_item['item_set_name'] = attribute_set_info[set_id]
else:
processed_item['item_set_name'] = f"Set {set_id}"
# Add comprehensive ratings (use gear totals as fallback for armor/clothing)
if 'ratings' in translated_props:
ratings = translated_props['ratings']
# Damage rating: use individual rating or gear total
if ratings.get('damage_rating', -1) != -1:
processed_item['damage_rating'] = ratings['damage_rating']
elif ratings.get('gear_damage', -1) > 0:
processed_item['damage_rating'] = ratings['gear_damage']
# Crit rating
if ratings.get('crit_rating', -1) != -1:
processed_item['crit_rating'] = ratings['crit_rating']
elif ratings.get('gear_crit', -1) > 0:
processed_item['crit_rating'] = ratings['gear_crit']
# Crit damage rating: use individual rating or gear total
if ratings.get('crit_damage_rating', -1) != -1:
processed_item['crit_damage_rating'] = ratings['crit_damage_rating']
elif ratings.get('gear_crit_damage', -1) > 0:
processed_item['crit_damage_rating'] = ratings['gear_crit_damage']
# Heal boost rating: use individual rating or gear total
if ratings.get('heal_boost_rating', -1) != -1:
processed_item['heal_boost_rating'] = ratings['heal_boost_rating']
elif ratings.get('gear_healing_boost', -1) > 0:
processed_item['heal_boost_rating'] = ratings['gear_healing_boost']
# Apply material prefix to item name if material exists
if processed_item.get('material_name') and processed_item.get('name'):
original_name = processed_item['name']
material_name = processed_item['material_name']
# Don't add prefix if name already starts with the material
if not original_name.lower().startswith(material_name.lower()):
processed_item['name'] = f"{material_name} {original_name}"
processed_item['original_name'] = original_name # Preserve original for reference
# Remove raw data from response (keep clean output)
processed_item.pop('int_values', None)
processed_item.pop('double_values', None)
processed_item.pop('string_values', None)
processed_item.pop('bool_values', None)
processed_item.pop('original_json', None)
# Remove null values for cleaner response
processed_item = {k: v for k, v in processed_item.items() if v is not None}
processed_items.append(processed_item)
processed_items = [enrich_db_item(item) for item in items]
return {
"character_name": character_name,