added inventory service for armor and jewelry
This commit is contained in:
parent
09a6cd4946
commit
57a2384511
13 changed files with 2630 additions and 25 deletions
|
|
@ -27,6 +27,7 @@ services:
|
|||
DB_WAL_AUTOCHECKPOINT_PAGES: "${DB_WAL_AUTOCHECKPOINT_PAGES}"
|
||||
SHARED_SECRET: "${SHARED_SECRET}"
|
||||
LOG_LEVEL: "${LOG_LEVEL:-INFO}"
|
||||
INVENTORY_SERVICE_URL: "http://inventory-service:8000"
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: "json-file"
|
||||
|
|
|
|||
84
inventory-service/add_dictionaries.py
Normal file
84
inventory-service/add_dictionaries.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Add dictionaries to the comprehensive enum database."""
|
||||
|
||||
import json
|
||||
|
||||
# AttributeSetInfo dictionary manually extracted (COMPLETE)
|
||||
attribute_set_info = {
|
||||
"2": "Test",
|
||||
"4": "Carraida's Benediction",
|
||||
"5": "Noble Relic Set",
|
||||
"6": "Ancient Relic Set",
|
||||
"7": "Relic Alduressa Set",
|
||||
"8": "Shou-jen Set",
|
||||
"9": "Empyrean Rings Set",
|
||||
"10": "Arm, Mind, Heart Set",
|
||||
"11": "Coat of the Perfect Light Set",
|
||||
"12": "Leggings of Perfect Light Set",
|
||||
"13": "Soldier's Set",
|
||||
"14": "Adept's Set",
|
||||
"15": "Archer's Set",
|
||||
"16": "Defender's Set",
|
||||
"17": "Tinker's Set",
|
||||
"18": "Crafter's Set",
|
||||
"19": "Hearty Set",
|
||||
"20": "Dexterous Set",
|
||||
"21": "Wise Set",
|
||||
"22": "Swift Set",
|
||||
"23": "Hardenend Set",
|
||||
"24": "Reinforced Set",
|
||||
"25": "Interlocking Set",
|
||||
"26": "Flame Proof Set",
|
||||
"27": "Acid Proof Set",
|
||||
"28": "Cold Proof Set",
|
||||
"29": "Lightning Proof Set",
|
||||
"30": "Dedication Set",
|
||||
"31": "Gladiatorial Clothing Set",
|
||||
"32": "Ceremonial Clothing",
|
||||
"33": "Protective Clothing",
|
||||
"34": "Noobie Armor",
|
||||
"35": "Sigil of Defense",
|
||||
"36": "Sigil of Destruction",
|
||||
"37": "Sigil of Fury",
|
||||
"38": "Sigil of Growth",
|
||||
"39": "Sigil of Vigor",
|
||||
"40": "Heroic Protector Set",
|
||||
"41": "Heroic Destroyer Set",
|
||||
"42": "Olthoi Armor D Red",
|
||||
"43": "Olthoi Armor C Rat",
|
||||
"44": "Olthoi Armor C Red",
|
||||
"45": "Olthoi Armor F Red",
|
||||
"46": "Olthoi Armor K Red",
|
||||
"47": "Olthoi Armor M Red",
|
||||
"48": "Olthoi Armor B Red",
|
||||
"49": "Olthoi Armor B Rat",
|
||||
"50": "Olthoi Armor K Rat",
|
||||
"51": "Olthoi Armor M Rat",
|
||||
"52": "Olthoi Armor F Rat",
|
||||
"53": "Olthoi Armor D Rat"
|
||||
}
|
||||
|
||||
# Load existing database
|
||||
with open('comprehensive_enum_database_v2.json', 'r') as f:
|
||||
db = json.load(f)
|
||||
|
||||
# Add dictionaries section
|
||||
if 'dictionaries' not in db:
|
||||
db['dictionaries'] = {}
|
||||
|
||||
db['dictionaries']['AttributeSetInfo'] = {
|
||||
'name': 'AttributeSetInfo',
|
||||
'description': 'Equipment set names',
|
||||
'values': attribute_set_info
|
||||
}
|
||||
|
||||
# Save updated database
|
||||
with open('comprehensive_enum_database_v2.json', 'w') as f:
|
||||
json.dump(db, f, indent=2)
|
||||
|
||||
print("Added AttributeSetInfo dictionary to comprehensive enum database")
|
||||
print(f"Total equipment sets: {len(attribute_set_info)}")
|
||||
print("Example sets:")
|
||||
for set_id in ['13', '14', '16', '21']:
|
||||
if set_id in attribute_set_info:
|
||||
print(f" {set_id} -> {attribute_set_info[set_id]}")
|
||||
|
|
@ -2,14 +2,19 @@
|
|||
"metadata": {
|
||||
"extracted_at": "2025-06-10",
|
||||
"source": "Mag-Plugins/Shared (comprehensive)",
|
||||
"version": "2.0.0"
|
||||
"version": "2.1.0",
|
||||
"last_updated": "2025-06-12",
|
||||
"notes": "COMPLETE extraction: Added missing CoverageMask values (1,2,4,16,etc) and BoolValueKey enum"
|
||||
},
|
||||
"enums": {
|
||||
"CoverageMask": {
|
||||
"name": "CoverageMask",
|
||||
"values": {
|
||||
"0": "None",
|
||||
"1": "Unknown",
|
||||
"2": "UnderwearUpperLegs",
|
||||
"4": "UnderwearLowerLegs",
|
||||
"8": "UnderwearChest",
|
||||
"16": "UnderwearAbdomen",
|
||||
"32": "UnderwearUpperArms",
|
||||
"64": "UnderwearLowerArms",
|
||||
"256": "OuterwearUpperLegs",
|
||||
|
|
@ -590,14 +595,9 @@
|
|||
"128": "HadNoVitae",
|
||||
"129": "NoOlthoiTalk",
|
||||
"130": "AutowieldLeft",
|
||||
"131": "/* custom */",
|
||||
"132": "[ServerOnly]",
|
||||
"9001": "LinkedPortalOneSummon",
|
||||
"134": "[ServerOnly]",
|
||||
"9002": "LinkedPortalTwoSummon",
|
||||
"136": "[ServerOnly]",
|
||||
"9003": "HouseEvicted",
|
||||
"138": "[ServerOnly]",
|
||||
"9004": "UntrainedSkills",
|
||||
"201326592": "Lockable_Decal",
|
||||
"201326593": "Inscribable_Decal"
|
||||
|
|
@ -1996,9 +1996,254 @@
|
|||
"12": "HeritageType"
|
||||
},
|
||||
"source_file": "WieldRequirement.cs"
|
||||
},
|
||||
"AttributeSetInfo": {
|
||||
"name": "AttributeSetInfo",
|
||||
"description": "Equipment set names mapping (Soldier's Set, Adept's Set, Defender's Set, Wise Set, etc.)",
|
||||
"values": {
|
||||
"2": "Test",
|
||||
"4": "Carraida's Benediction",
|
||||
"5": "Noble Relic Set",
|
||||
"6": "Ancient Relic Set",
|
||||
"7": "Relic Alduressa Set",
|
||||
"8": "Shou-jen Set",
|
||||
"9": "Empyrean Rings Set",
|
||||
"10": "Arm, Mind, Heart Set",
|
||||
"11": "Coat of the Perfect Light Set",
|
||||
"12": "Leggings of Perfect Light Set",
|
||||
"13": "Soldier's Set",
|
||||
"14": "Adept's Set",
|
||||
"15": "Archer's Set",
|
||||
"16": "Defender's Set",
|
||||
"17": "Tinker's Set",
|
||||
"18": "Crafter's Set",
|
||||
"19": "Hearty Set",
|
||||
"20": "Dexterous Set",
|
||||
"21": "Wise Set",
|
||||
"22": "Swift Set",
|
||||
"23": "Hardened Set",
|
||||
"24": "Reinforced Set",
|
||||
"25": "Interlocking Set",
|
||||
"26": "Flame Proof Set",
|
||||
"27": "Acid Proof Set",
|
||||
"28": "Cold Proof Set",
|
||||
"29": "Lightning Proof Set",
|
||||
"30": "Dedication Set",
|
||||
"31": "Gladiatorial Clothing Set",
|
||||
"32": "Ceremonial Clothing",
|
||||
"33": "Protective Clothing",
|
||||
"34": "Noobie Armor",
|
||||
"35": "Sigil of Defense",
|
||||
"36": "Sigil of Destruction",
|
||||
"37": "Sigil of Fury",
|
||||
"38": "Sigil of Growth",
|
||||
"39": "Sigil of Vigor",
|
||||
"40": "Heroic Protector Set",
|
||||
"41": "Heroic Destroyer Set",
|
||||
"42": "Olthoi Armor D Red",
|
||||
"43": "Olthoi Armor C Rat",
|
||||
"44": "Olthoi Armor C Red",
|
||||
"45": "Olthoi Armor D Rat",
|
||||
"46": "Upgraded Relic Alduressa Set",
|
||||
"47": "Upgraded Ancient Relic Set",
|
||||
"48": "Upgraded Noble Relic Set",
|
||||
"49": "Weave of Alchemy",
|
||||
"50": "Weave of Arcane Lore",
|
||||
"51": "Weave of Armor Tinkering",
|
||||
"52": "Weave of Assess Person",
|
||||
"53": "Weave of Light Weapons",
|
||||
"54": "Weave of Missile Weapons",
|
||||
"55": "Weave of Cooking",
|
||||
"56": "Weave of Creature Enchantment",
|
||||
"57": "Weave of Missile Weapons",
|
||||
"58": "Weave of Finesse",
|
||||
"59": "Weave of Deception",
|
||||
"60": "Weave of Fletching",
|
||||
"61": "Weave of Healing",
|
||||
"62": "Weave of Item Enchantment",
|
||||
"63": "Weave of Item Tinkering",
|
||||
"64": "Weave of Leadership",
|
||||
"65": "Weave of Life Magic",
|
||||
"66": "Weave of Loyalty",
|
||||
"67": "Weave of Light Weapons",
|
||||
"68": "Weave of Magic Defense",
|
||||
"69": "Weave of Magic Item Tinkering",
|
||||
"70": "Weave of Mana Conversion",
|
||||
"71": "Weave of Melee Defense",
|
||||
"72": "Weave of Missile Defense",
|
||||
"73": "Weave of Salvaging",
|
||||
"74": "Weave of Light Weapons",
|
||||
"75": "Weave of Light Weapons",
|
||||
"76": "Weave of Heavy Weapons",
|
||||
"77": "Weave of Missile Weapons",
|
||||
"78": "Weave of Two Handed Combat",
|
||||
"79": "Weave of Light Weapons",
|
||||
"80": "Weave of Void Magic",
|
||||
"81": "Weave of War Magic",
|
||||
"82": "Weave of Weapon Tinkering",
|
||||
"83": "Weave of Assess Creature",
|
||||
"84": "Weave of Dirty Fighting",
|
||||
"85": "Weave of Dual Wield",
|
||||
"86": "Weave of Recklessness",
|
||||
"87": "Weave of Shield",
|
||||
"88": "Weave of Sneak Attack",
|
||||
"89": "Ninja_New",
|
||||
"90": "Weave of Summoning",
|
||||
"91": "Shrouded Soul",
|
||||
"92": "Darkened Mind",
|
||||
"93": "Clouded Spirit",
|
||||
"131": "Brown Society Locket",
|
||||
"132": "Yellow Society Locket",
|
||||
"133": "Red Society Band",
|
||||
"134": "Green Society Band",
|
||||
"135": "Purple Society Band",
|
||||
"136": "Blue Society Band",
|
||||
"137": "Gauntlet Garb"
|
||||
},
|
||||
"source_file": "Dictionaries.cs"
|
||||
},
|
||||
"SkillInfo": {
|
||||
"name": "SkillInfo",
|
||||
"description": "Skill names mapping",
|
||||
"values": {
|
||||
"1": "Axe",
|
||||
"2": "Bow",
|
||||
"3": "Crossbow",
|
||||
"4": "Dagger",
|
||||
"5": "Mace",
|
||||
"6": "Melee Defense",
|
||||
"7": "Missile Defense",
|
||||
"8": "Sling",
|
||||
"9": "Spear",
|
||||
"10": "Staff",
|
||||
"11": "Sword",
|
||||
"12": "Thrown Weapons",
|
||||
"13": "Unarmed Combat",
|
||||
"14": "Arcane Lore",
|
||||
"15": "Magic Defense",
|
||||
"16": "Mana Conversion",
|
||||
"18": "Item Tinkering",
|
||||
"19": "Assess Person",
|
||||
"20": "Deception",
|
||||
"21": "Healing",
|
||||
"22": "Jump",
|
||||
"23": "Lockpick",
|
||||
"24": "Run",
|
||||
"27": "Assess Creature",
|
||||
"28": "Weapon Tinkering",
|
||||
"29": "Armor Tinkering",
|
||||
"30": "Magic Item Tinkering",
|
||||
"31": "Creature Enchantment",
|
||||
"32": "Item Enchantment",
|
||||
"33": "Life Magic",
|
||||
"34": "War Magic",
|
||||
"35": "Leadership",
|
||||
"36": "Loyalty",
|
||||
"37": "Fletching",
|
||||
"38": "Alchemy",
|
||||
"39": "Cooking",
|
||||
"40": "Salvaging",
|
||||
"41": "Two Handed Combat",
|
||||
"42": "Gearcraft",
|
||||
"43": "Void",
|
||||
"44": "Heavy Weapons",
|
||||
"45": "Light Weapons",
|
||||
"46": "Finesse Weapons",
|
||||
"47": "Missile Weapons",
|
||||
"48": "Shield",
|
||||
"49": "Dual Wield",
|
||||
"50": "Recklessness",
|
||||
"51": "Sneak Attack",
|
||||
"52": "Dirty Fighting",
|
||||
"53": "Challenge",
|
||||
"54": "Summoning"
|
||||
},
|
||||
"source_file": "Dictionaries.cs"
|
||||
},
|
||||
"MasteryInfo": {
|
||||
"name": "MasteryInfo",
|
||||
"description": "Weapon mastery names mapping",
|
||||
"values": {
|
||||
"1": "Unarmed Weapon",
|
||||
"2": "Sword",
|
||||
"3": "Axe",
|
||||
"4": "Mace",
|
||||
"5": "Spear",
|
||||
"6": "Dagger",
|
||||
"7": "Staff",
|
||||
"8": "Bow",
|
||||
"9": "Crossbow",
|
||||
"10": "Thrown",
|
||||
"11": "Two Handed Combat"
|
||||
},
|
||||
"source_file": "Dictionaries.cs"
|
||||
},
|
||||
"MaterialInfo": {
|
||||
"name": "MaterialInfo",
|
||||
"description": "Material names mapping",
|
||||
"values": {},
|
||||
"source_file": "Dictionaries.cs"
|
||||
}
|
||||
},
|
||||
"dictionaries": {
|
||||
"AttributeSetInfo": {
|
||||
"name": "AttributeSetInfo",
|
||||
"description": "Equipment set names",
|
||||
"values": {
|
||||
"2": "Test",
|
||||
"4": "Carraida's Benediction",
|
||||
"5": "Noble Relic Set",
|
||||
"6": "Ancient Relic Set",
|
||||
"7": "Relic Alduressa Set",
|
||||
"8": "Shou-jen Set",
|
||||
"9": "Empyrean Rings Set",
|
||||
"10": "Arm, Mind, Heart Set",
|
||||
"11": "Coat of the Perfect Light Set",
|
||||
"12": "Leggings of Perfect Light Set",
|
||||
"13": "Soldier's Set",
|
||||
"14": "Adept's Set",
|
||||
"15": "Archer's Set",
|
||||
"16": "Defender's Set",
|
||||
"17": "Tinker's Set",
|
||||
"18": "Crafter's Set",
|
||||
"19": "Hearty Set",
|
||||
"20": "Dexterous Set",
|
||||
"21": "Wise Set",
|
||||
"22": "Swift Set",
|
||||
"23": "Hardenend Set",
|
||||
"24": "Reinforced Set",
|
||||
"25": "Interlocking Set",
|
||||
"26": "Flame Proof Set",
|
||||
"27": "Acid Proof Set",
|
||||
"28": "Cold Proof Set",
|
||||
"29": "Lightning Proof Set",
|
||||
"30": "Dedication Set",
|
||||
"31": "Gladiatorial Clothing Set",
|
||||
"32": "Ceremonial Clothing",
|
||||
"33": "Protective Clothing",
|
||||
"34": "Noobie Armor",
|
||||
"35": "Sigil of Defense",
|
||||
"36": "Sigil of Destruction",
|
||||
"37": "Sigil of Fury",
|
||||
"38": "Sigil of Growth",
|
||||
"39": "Sigil of Vigor",
|
||||
"40": "Heroic Protector Set",
|
||||
"41": "Heroic Destroyer Set",
|
||||
"42": "Olthoi Armor D Red",
|
||||
"43": "Olthoi Armor C Rat",
|
||||
"44": "Olthoi Armor C Red",
|
||||
"45": "Olthoi Armor F Red",
|
||||
"46": "Olthoi Armor K Red",
|
||||
"47": "Olthoi Armor M Red",
|
||||
"48": "Olthoi Armor B Red",
|
||||
"49": "Olthoi Armor B Rat",
|
||||
"50": "Olthoi Armor K Rat",
|
||||
"51": "Olthoi Armor M Rat",
|
||||
"52": "Olthoi Armor F Rat",
|
||||
"53": "Olthoi Armor D Rat"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dictionaries": {},
|
||||
"spells": {
|
||||
"name": "Spells",
|
||||
"values": {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,29 @@ class Item(Base):
|
|||
value = Column(Integer, default=0)
|
||||
burden = Column(Integer, default=0)
|
||||
|
||||
# Equipment status
|
||||
current_wielded_location = Column(Integer, default=0, index=True) # 0 = not equipped
|
||||
|
||||
# Item state
|
||||
bonded = Column(Integer, default=0) # 0=Normal, 1=Bonded, 2=Sticky, 4=Destroy on drop
|
||||
attuned = Column(Integer, default=0) # 0=Normal, 1=Attuned
|
||||
unique = Column(Boolean, default=False)
|
||||
|
||||
# Stack/Container properties
|
||||
stack_size = Column(Integer, default=1)
|
||||
max_stack_size = Column(Integer, default=1)
|
||||
items_capacity = Column(Integer) # For containers
|
||||
containers_capacity = Column(Integer) # For containers
|
||||
|
||||
# Durability
|
||||
structure = Column(Integer) # Current durability
|
||||
max_structure = Column(Integer) # Maximum durability
|
||||
|
||||
# Special item flags
|
||||
rare_id = Column(Integer) # For rare items
|
||||
lifespan = Column(Integer) # Item decay timer
|
||||
remaining_lifespan = Column(Integer) # Remaining decay time
|
||||
|
||||
# Metadata flags
|
||||
has_id_data = Column(Boolean, default=False)
|
||||
last_id_time = Column(BigInteger, default=0)
|
||||
|
|
@ -60,6 +83,11 @@ class ItemCombatStats(Base):
|
|||
elemental_damage_vs_monsters = Column(Float)
|
||||
variance = Column(Float)
|
||||
|
||||
# Advanced damage properties
|
||||
cleaving = Column(Integer) # Cleaving damage
|
||||
crit_damage_rating = Column(Integer) # Critical damage multiplier
|
||||
damage_over_time = Column(Integer) # DoT damage
|
||||
|
||||
# Attack properties
|
||||
attack_bonus = Column(Float)
|
||||
weapon_time = Column(Integer)
|
||||
|
|
@ -74,9 +102,24 @@ class ItemCombatStats(Base):
|
|||
|
||||
# Resistances
|
||||
resist_magic = Column(Integer)
|
||||
crit_resist_rating = Column(Integer)
|
||||
crit_damage_resist_rating = Column(Integer)
|
||||
dot_resist_rating = Column(Integer)
|
||||
life_resist_rating = Column(Integer)
|
||||
nether_resist_rating = Column(Integer)
|
||||
|
||||
# Healing/Recovery
|
||||
heal_over_time = Column(Integer)
|
||||
healing_resist_rating = Column(Integer)
|
||||
|
||||
# Mana properties
|
||||
mana_conversion_bonus = Column(Float)
|
||||
|
||||
# PvP properties
|
||||
pk_damage_rating = Column(Integer)
|
||||
pk_damage_resist_rating = Column(Integer)
|
||||
gear_pk_damage_rating = Column(Integer)
|
||||
gear_pk_damage_resist_rating = Column(Integer)
|
||||
|
||||
class ItemRequirements(Base):
|
||||
"""Wield requirements and skill prerequisites."""
|
||||
|
|
@ -105,8 +148,30 @@ class ItemEnhancements(Base):
|
|||
workmanship = Column(Float)
|
||||
salvage_workmanship = Column(Float)
|
||||
|
||||
# Advanced tinkering
|
||||
num_times_tinkered = Column(Integer, default=0)
|
||||
free_tinkers_bitfield = Column(Integer) # Which tinkers are free
|
||||
num_items_in_material = Column(Integer) # Salvage yield
|
||||
|
||||
# Additional imbue effects
|
||||
imbue_attempts = Column(Integer, default=0)
|
||||
imbue_successes = Column(Integer, default=0)
|
||||
imbued_effect2 = Column(Integer)
|
||||
imbued_effect3 = Column(Integer)
|
||||
imbued_effect4 = Column(Integer)
|
||||
imbued_effect5 = Column(Integer)
|
||||
imbue_stacking_bits = Column(Integer) # Which imbues stack
|
||||
|
||||
# Set information
|
||||
item_set = Column(String(100), index=True)
|
||||
equipment_set_extra = Column(Integer) # Additional set bonuses
|
||||
|
||||
# Special properties
|
||||
aetheria_bitfield = Column(Integer) # Aetheria slot properties
|
||||
heritage_specific_armor = Column(Integer) # Heritage armor type
|
||||
|
||||
# Cooldowns
|
||||
shared_cooldown = Column(Integer) # Cooldown group ID
|
||||
|
||||
class ItemRatings(Base):
|
||||
"""Modern rating system properties."""
|
||||
|
|
@ -126,6 +191,22 @@ class ItemRatings(Base):
|
|||
heal_boost_rating = Column(Integer)
|
||||
vitality_rating = Column(Integer)
|
||||
mana_conversion_rating = Column(Integer)
|
||||
weakness_rating = Column(Integer) # Weakness debuff strength
|
||||
nether_over_time = Column(Integer) # Nether DoT
|
||||
|
||||
# Gear totals
|
||||
gear_damage = Column(Integer) # Total gear damage
|
||||
gear_damage_resist = Column(Integer) # Total gear damage resist
|
||||
gear_crit = Column(Integer) # Total gear crit
|
||||
gear_crit_resist = Column(Integer) # Total gear crit resist
|
||||
gear_crit_damage = Column(Integer) # Total gear crit damage
|
||||
gear_crit_damage_resist = Column(Integer) # Total gear crit damage resist
|
||||
gear_healing_boost = Column(Integer) # Total gear healing boost
|
||||
gear_max_health = Column(Integer) # Total gear max health
|
||||
gear_nether_resist = Column(Integer) # Total gear nether resist
|
||||
gear_life_resist = Column(Integer) # Total gear life resist
|
||||
gear_overpower = Column(Integer) # Total gear overpower
|
||||
gear_overpower_resist = Column(Integer) # Total gear overpower resist
|
||||
|
||||
# Calculated totals
|
||||
total_rating = Column(Integer)
|
||||
|
|
@ -164,6 +245,9 @@ def create_indexes(engine):
|
|||
# Item search indexes
|
||||
sa.Index('ix_items_search', Item.character_name, Item.name, Item.object_class).create(engine, checkfirst=True)
|
||||
|
||||
# Equipment status index for filtering equipped items
|
||||
sa.Index('ix_items_equipped', Item.character_name, Item.current_wielded_location).create(engine, checkfirst=True)
|
||||
|
||||
# Combat stats indexes for filtering
|
||||
sa.Index('ix_combat_damage', ItemCombatStats.max_damage).create(engine, checkfirst=True)
|
||||
sa.Index('ix_combat_armor', ItemCombatStats.armor_level).create(engine, checkfirst=True)
|
||||
|
|
|
|||
87
inventory-service/extract_all_missing_enums.py
Normal file
87
inventory-service/extract_all_missing_enums.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive enum extraction from ALL Mag-Plugins Constants files.
|
||||
This script extracts ALL missing enum values to complete our enum database.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def extract_coverage_mask():
|
||||
"""Extract complete CoverageMask enum with ALL values."""
|
||||
file_path = "../unused/Mag-Plugins/Shared/Constants/CoverageMask.cs"
|
||||
|
||||
coverage_values = {}
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract enum values using regex
|
||||
enum_pattern = r'(\w+)\s*=\s*0x([0-9A-Fa-f]+)'
|
||||
matches = re.findall(enum_pattern, content)
|
||||
|
||||
for name, hex_value in matches:
|
||||
decimal_value = int(hex_value, 16)
|
||||
coverage_values[str(decimal_value)] = name
|
||||
|
||||
return {
|
||||
"name": "CoverageMask",
|
||||
"values": coverage_values,
|
||||
"source_file": "CoverageMask.cs"
|
||||
}
|
||||
|
||||
def extract_bool_value_key():
|
||||
"""Extract BoolValueKey enum (currently missing)."""
|
||||
file_path = "../unused/Mag-Plugins/Shared/Constants/BoolValueKey.cs"
|
||||
|
||||
bool_values = {}
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract enum values
|
||||
enum_pattern = r'(\w+)\s*=\s*(\d+)'
|
||||
matches = re.findall(enum_pattern, content)
|
||||
|
||||
for name, value in matches:
|
||||
bool_values[value] = name
|
||||
|
||||
return {
|
||||
"name": "BoolValueKey",
|
||||
"values": bool_values,
|
||||
"source_file": "BoolValueKey.cs"
|
||||
}
|
||||
|
||||
def load_current_database():
|
||||
"""Load current enum database."""
|
||||
with open('comprehensive_enum_database_v2.json', 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def update_enum_database():
|
||||
"""Update the enum database with ALL missing values."""
|
||||
print("Loading current enum database...")
|
||||
db = load_current_database()
|
||||
|
||||
print("Extracting complete CoverageMask...")
|
||||
complete_coverage = extract_coverage_mask()
|
||||
db['enums']['CoverageMask'] = complete_coverage
|
||||
|
||||
print("Extracting BoolValueKey...")
|
||||
bool_values = extract_bool_value_key()
|
||||
db['enums']['BoolValueKey'] = bool_values
|
||||
|
||||
# Update metadata
|
||||
db['metadata']['last_updated'] = "2025-06-12"
|
||||
db['metadata']['notes'] = "COMPLETE extraction: Added missing CoverageMask values (1,2,4,16,etc) and BoolValueKey enum"
|
||||
|
||||
print("Saving updated database...")
|
||||
with open('comprehensive_enum_database_v2.json', 'w') as f:
|
||||
json.dump(db, f, indent=2)
|
||||
|
||||
print("✅ Enum database updated successfully!")
|
||||
print(f"✅ CoverageMask now has {len(complete_coverage['values'])} values")
|
||||
print(f"✅ BoolValueKey now has {len(bool_values['values'])} values")
|
||||
|
||||
if __name__ == "__main__":
|
||||
update_enum_database()
|
||||
135
inventory-service/extract_dictionaries.py
Normal file
135
inventory-service/extract_dictionaries.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
#\!/usr/bin/env python3
|
||||
"""Extract dictionaries from Mag-Plugins Dictionaries.cs file."""
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
def extract_attribute_set_info():
|
||||
"""Extract AttributeSetInfo dictionary from Dictionaries.cs."""
|
||||
with open('/home/erik/MosswartOverlord/unused/Mag-Plugins/Shared/Constants/Dictionaries.cs', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find the AttributeSetInfo dictionary
|
||||
pattern = r'AttributeSetInfo\s*=\s*new\s+Dictionary<int,\s*string>\s*\{([^}]+)\}'
|
||||
match = re.search(pattern, content, re.DOTALL)
|
||||
|
||||
if not match:
|
||||
print("AttributeSetInfo not found\!")
|
||||
return {}
|
||||
|
||||
dict_content = match.group(1)
|
||||
|
||||
# Extract entries
|
||||
entry_pattern = r'\{\s*(\d+),\s*"([^"]+)"\s*\}'
|
||||
entries = re.findall(entry_pattern, dict_content)
|
||||
|
||||
attribute_sets = {}
|
||||
for set_id, set_name in entries:
|
||||
attribute_sets[set_id] = set_name
|
||||
|
||||
return attribute_sets
|
||||
|
||||
def extract_material_info():
|
||||
"""Extract MaterialInfo dictionary from Dictionaries.cs."""
|
||||
with open('/home/erik/MosswartOverlord/unused/Mag-Plugins/Shared/Constants/Dictionaries.cs', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find the MaterialInfo dictionary
|
||||
pattern = r'MaterialInfo\s*=\s*new\s+Dictionary<int,\s*string>\s*\{([^}]+)\}'
|
||||
match = re.search(pattern, content, re.DOTALL)
|
||||
|
||||
if not match:
|
||||
print("MaterialInfo not found\!")
|
||||
return {}
|
||||
|
||||
dict_content = match.group(1)
|
||||
|
||||
# Extract entries
|
||||
entry_pattern = r'\{\s*(\d+),\s*"([^"]+)"\s*\}'
|
||||
entries = re.findall(entry_pattern, dict_content)
|
||||
|
||||
materials = {}
|
||||
for mat_id, mat_name in entries:
|
||||
materials[mat_id] = mat_name
|
||||
|
||||
return materials
|
||||
|
||||
def extract_skill_info():
|
||||
"""Extract SkillInfo dictionary from Dictionaries.cs."""
|
||||
with open('/home/erik/MosswartOverlord/unused/Mag-Plugins/Shared/Constants/Dictionaries.cs', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find the SkillInfo dictionary
|
||||
pattern = r'SkillInfo\s*=\s*new\s+Dictionary<int,\s*string>\s*\{([^}]+)\}'
|
||||
match = re.search(pattern, content, re.DOTALL)
|
||||
|
||||
if not match:
|
||||
print("SkillInfo not found\!")
|
||||
return {}
|
||||
|
||||
dict_content = match.group(1)
|
||||
|
||||
# Extract entries - handle hex values
|
||||
entry_pattern = r'\{\s*(0x[0-9A-Fa-f]+ < /dev/null | \d+),\s*"([^"]+)"\s*\}'
|
||||
entries = re.findall(entry_pattern, dict_content)
|
||||
|
||||
skills = {}
|
||||
for skill_id, skill_name in entries:
|
||||
# Convert hex to decimal if needed
|
||||
if skill_id.startswith('0x'):
|
||||
skill_id = str(int(skill_id, 16))
|
||||
skills[skill_id] = skill_name
|
||||
|
||||
return skills
|
||||
|
||||
def extract_mastery_info():
|
||||
"""Extract MasteryInfo dictionary from Dictionaries.cs."""
|
||||
with open('/home/erik/MosswartOverlord/unused/Mag-Plugins/Shared/Constants/Dictionaries.cs', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find the MasteryInfo dictionary
|
||||
pattern = r'MasteryInfo\s*=\s*new\s+Dictionary<int,\s*string>\s*\{([^}]+)\}'
|
||||
match = re.search(pattern, content, re.DOTALL)
|
||||
|
||||
if not match:
|
||||
print("MasteryInfo not found\!")
|
||||
return {}
|
||||
|
||||
dict_content = match.group(1)
|
||||
|
||||
# Extract entries
|
||||
entry_pattern = r'\{\s*(\d+),\s*"([^"]+)"\s*\}'
|
||||
entries = re.findall(entry_pattern, dict_content)
|
||||
|
||||
masteries = {}
|
||||
for mastery_id, mastery_name in entries:
|
||||
masteries[mastery_id] = mastery_name
|
||||
|
||||
return masteries
|
||||
|
||||
def main():
|
||||
"""Extract all dictionaries and save to JSON."""
|
||||
dictionaries = {
|
||||
'AttributeSetInfo': extract_attribute_set_info(),
|
||||
'MaterialInfo': extract_material_info(),
|
||||
'SkillInfo': extract_skill_info(),
|
||||
'MasteryInfo': extract_mastery_info()
|
||||
}
|
||||
|
||||
# Print summary
|
||||
for dict_name, dict_data in dictionaries.items():
|
||||
print(f"{dict_name}: {len(dict_data)} entries")
|
||||
if dict_data and dict_name == 'AttributeSetInfo':
|
||||
# Show some equipment set examples
|
||||
for set_id in ['13', '14', '16', '21']:
|
||||
if set_id in dict_data:
|
||||
print(f" {set_id} -> {dict_data[set_id]}")
|
||||
|
||||
# Save to file
|
||||
with open('extracted_dictionaries.json', 'w') as f:
|
||||
json.dump(dictionaries, f, indent=2)
|
||||
|
||||
print("\nDictionaries saved to extracted_dictionaries.json")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
60
inventory-service/extract_dictionaries_v2.py
Normal file
60
inventory-service/extract_dictionaries_v2.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#\!/usr/bin/env python3
|
||||
"""Extract dictionaries from Mag-Plugins Dictionaries.cs file."""
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
def extract_dictionary(dict_name):
|
||||
"""Extract a dictionary from Dictionaries.cs by name."""
|
||||
with open('/home/erik/MosswartOverlord/unused/Mag-Plugins/Shared/Constants/Dictionaries.cs', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find the dictionary - handle multiline definitions
|
||||
pattern = rf'{dict_name}\s*=\s*new\s+Dictionary<int,\s*string>\s*\{{(.*?)^\s*\}};'
|
||||
match = re.search(pattern, content, re.DOTALL < /dev/null | re.MULTILINE)
|
||||
|
||||
if not match:
|
||||
print(f"{dict_name} not found\!")
|
||||
return {}
|
||||
|
||||
dict_content = match.group(1)
|
||||
|
||||
# Extract entries - handle both decimal and hex values
|
||||
entry_pattern = r'\{\s*(0x[0-9A-Fa-f]+|\d+),\s*"([^"]+)"\s*\}'
|
||||
entries = re.findall(entry_pattern, dict_content)
|
||||
|
||||
result = {}
|
||||
for key, value in entries:
|
||||
# Convert hex to decimal if needed
|
||||
if key.startswith('0x'):
|
||||
key = str(int(key, 16))
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
def main():
|
||||
"""Extract all dictionaries and save to JSON."""
|
||||
dictionaries = {
|
||||
'AttributeSetInfo': extract_dictionary('AttributeSetInfo'),
|
||||
'MaterialInfo': extract_dictionary('MaterialInfo'),
|
||||
'SkillInfo': extract_dictionary('SkillInfo'),
|
||||
'MasteryInfo': extract_dictionary('MasteryInfo')
|
||||
}
|
||||
|
||||
# Print summary
|
||||
for dict_name, dict_data in dictionaries.items():
|
||||
print(f"{dict_name}: {len(dict_data)} entries")
|
||||
if dict_data and dict_name == 'AttributeSetInfo':
|
||||
# Show some equipment set examples
|
||||
for set_id in ['13', '14', '16', '21']:
|
||||
if set_id in dict_data:
|
||||
print(f" {set_id} -> {dict_data[set_id]}")
|
||||
|
||||
# Save to file
|
||||
with open('extracted_dictionaries.json', 'w') as f:
|
||||
json.dump(dictionaries, f, indent=2)
|
||||
|
||||
print("\nDictionaries saved to extracted_dictionaries.json")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
6
inventory-service/extracted_dictionaries.json
Normal file
6
inventory-service/extracted_dictionaries.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"AttributeSetInfo": {},
|
||||
"MaterialInfo": {},
|
||||
"SkillInfo": {},
|
||||
"MasteryInfo": {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
318
main.py
318
main.py
|
|
@ -13,8 +13,8 @@ import sys
|
|||
from typing import Dict, List, Any
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Header, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import FastAPI, Header, HTTPException, Query, WebSocket, WebSocketDisconnect, Request
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
from fastapi.routing import APIRoute
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
|
@ -386,6 +386,64 @@ async def get_total_rares():
|
|||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
# --- GET Spawn Heat Map Endpoint ---------------------------------
|
||||
@app.get("/spawns/heatmap")
|
||||
async def get_spawn_heatmap_data(
|
||||
hours: int = Query(24, ge=1, le=168, description="Lookback window in hours (1-168)"),
|
||||
limit: int = Query(10000, ge=100, le=50000, description="Maximum number of spawn points to return")
|
||||
):
|
||||
"""
|
||||
Aggregate spawn locations for heat-map visualization.
|
||||
|
||||
Returns spawn event coordinates grouped by location with intensity counts
|
||||
for the specified time window.
|
||||
|
||||
Response format:
|
||||
{
|
||||
"spawn_points": [{"ew": float, "ns": float, "intensity": int}, ...],
|
||||
"total_points": int,
|
||||
"timestamp": "UTC-ISO"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||||
|
||||
# Aggregate spawn events by coordinates within time window
|
||||
query = """
|
||||
SELECT ew, ns, COUNT(*) AS spawn_count
|
||||
FROM spawn_events
|
||||
WHERE timestamp >= :cutoff
|
||||
GROUP BY ew, ns
|
||||
ORDER BY spawn_count DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
|
||||
rows = await database.fetch_all(query, {"cutoff": cutoff, "limit": limit})
|
||||
|
||||
spawn_points = [
|
||||
{
|
||||
"ew": float(row["ew"]),
|
||||
"ns": float(row["ns"]),
|
||||
"intensity": int(row["spawn_count"])
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
result = {
|
||||
"spawn_points": spawn_points,
|
||||
"total_points": len(spawn_points),
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"hours_window": hours
|
||||
}
|
||||
|
||||
logger.debug(f"Heat map data: {len(spawn_points)} unique spawn locations from last {hours} hours")
|
||||
return JSONResponse(content=jsonable_encoder(result))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Heat map query failed: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Spawn heat map query failed")
|
||||
|
||||
|
||||
# --- GET Inventory Endpoints ---------------------------------
|
||||
@app.get("/inventory/{character_name}")
|
||||
async def get_character_inventory(character_name: str):
|
||||
|
|
@ -514,6 +572,230 @@ async def list_characters_with_inventories():
|
|||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
# --- Inventory Service Character List Proxy ---------------------
|
||||
@app.get("/inventory-characters")
|
||||
async def get_inventory_characters():
|
||||
"""Get character list from inventory service - proxy to avoid routing conflicts."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(f"{INVENTORY_SERVICE_URL}/characters/list")
|
||||
|
||||
if response.status_code == 200:
|
||||
return JSONResponse(content=response.json())
|
||||
else:
|
||||
logger.error(f"Inventory service returned {response.status_code}: {response.text}")
|
||||
raise HTTPException(status_code=response.status_code, detail="Failed to get characters from inventory service")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to proxy inventory characters request: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Failed to get inventory characters")
|
||||
|
||||
|
||||
# --- Inventory Search Service Proxy Endpoints -------------------
|
||||
@app.get("/search/items")
|
||||
async def search_items_proxy(
|
||||
text: str = Query(None, description="Search item names, descriptions, or properties"),
|
||||
character: str = Query(None, description="Limit search to specific character"),
|
||||
include_all_characters: bool = Query(False, description="Search across all characters"),
|
||||
equipment_status: str = Query(None, description="equipped, unequipped, or all"),
|
||||
equipment_slot: int = Query(None, description="Equipment slot mask"),
|
||||
# Item category filtering
|
||||
armor_only: bool = Query(False, description="Show only armor items"),
|
||||
jewelry_only: bool = Query(False, description="Show only jewelry items"),
|
||||
weapon_only: bool = Query(False, description="Show only weapon items"),
|
||||
# Spell filtering
|
||||
has_spell: str = Query(None, description="Must have this specific spell (by name)"),
|
||||
spell_contains: str = Query(None, description="Spell name contains this text"),
|
||||
legendary_cantrips: str = Query(None, description="Comma-separated list of legendary cantrip names"),
|
||||
# Combat properties
|
||||
min_damage: int = Query(None, description="Minimum damage"),
|
||||
max_damage: int = Query(None, description="Maximum damage"),
|
||||
min_armor: int = Query(None, description="Minimum armor level"),
|
||||
max_armor: int = Query(None, description="Maximum armor level"),
|
||||
min_attack_bonus: float = Query(None, description="Minimum attack bonus"),
|
||||
min_crit_damage_rating: int = Query(None, description="Minimum critical damage rating"),
|
||||
min_damage_rating: int = Query(None, description="Minimum damage rating"),
|
||||
min_heal_boost_rating: int = Query(None, description="Minimum heal boost rating"),
|
||||
max_level: int = Query(None, description="Maximum wield level requirement"),
|
||||
min_level: int = Query(None, description="Minimum wield level requirement"),
|
||||
material: str = Query(None, description="Material type (partial match)"),
|
||||
min_workmanship: float = Query(None, description="Minimum workmanship"),
|
||||
has_imbue: bool = Query(None, description="Has imbue effects"),
|
||||
item_set: str = Query(None, description="Item set name (partial match)"),
|
||||
min_tinks: int = Query(None, description="Minimum tinker count"),
|
||||
bonded: bool = Query(None, description="Bonded status"),
|
||||
attuned: bool = Query(None, description="Attuned status"),
|
||||
unique: bool = Query(None, description="Unique item status"),
|
||||
is_rare: bool = Query(None, description="Rare item status"),
|
||||
min_condition: int = Query(None, description="Minimum condition percentage"),
|
||||
min_value: int = Query(None, description="Minimum item value"),
|
||||
max_value: int = Query(None, description="Maximum item value"),
|
||||
max_burden: int = Query(None, description="Maximum burden"),
|
||||
sort_by: str = Query("name", description="Sort field: name, value, damage, armor, workmanship"),
|
||||
sort_dir: str = Query("asc", description="Sort direction: asc or desc"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
limit: int = Query(50, ge=1, le=200, description="Items per page")
|
||||
):
|
||||
"""Proxy to inventory service comprehensive item search."""
|
||||
try:
|
||||
# Build query parameters
|
||||
params = {}
|
||||
if text: params["text"] = text
|
||||
if character: params["character"] = character
|
||||
if include_all_characters: params["include_all_characters"] = include_all_characters
|
||||
if equipment_status: params["equipment_status"] = equipment_status
|
||||
if equipment_slot is not None: params["equipment_slot"] = equipment_slot
|
||||
# Category filtering
|
||||
if armor_only: params["armor_only"] = armor_only
|
||||
if jewelry_only: params["jewelry_only"] = jewelry_only
|
||||
if weapon_only: params["weapon_only"] = weapon_only
|
||||
# Spell filtering
|
||||
if has_spell: params["has_spell"] = has_spell
|
||||
if spell_contains: params["spell_contains"] = spell_contains
|
||||
if legendary_cantrips: params["legendary_cantrips"] = legendary_cantrips
|
||||
# Combat properties
|
||||
if min_damage is not None: params["min_damage"] = min_damage
|
||||
if max_damage is not None: params["max_damage"] = max_damage
|
||||
if min_armor is not None: params["min_armor"] = min_armor
|
||||
if max_armor is not None: params["max_armor"] = max_armor
|
||||
if min_attack_bonus is not None: params["min_attack_bonus"] = min_attack_bonus
|
||||
if min_crit_damage_rating is not None: params["min_crit_damage_rating"] = min_crit_damage_rating
|
||||
if min_damage_rating is not None: params["min_damage_rating"] = min_damage_rating
|
||||
if min_heal_boost_rating is not None: params["min_heal_boost_rating"] = min_heal_boost_rating
|
||||
if max_level is not None: params["max_level"] = max_level
|
||||
if min_level is not None: params["min_level"] = min_level
|
||||
if material: params["material"] = material
|
||||
if min_workmanship is not None: params["min_workmanship"] = min_workmanship
|
||||
if has_imbue is not None: params["has_imbue"] = has_imbue
|
||||
if item_set: params["item_set"] = item_set
|
||||
if min_tinks is not None: params["min_tinks"] = min_tinks
|
||||
if bonded is not None: params["bonded"] = bonded
|
||||
if attuned is not None: params["attuned"] = attuned
|
||||
if unique is not None: params["unique"] = unique
|
||||
if is_rare is not None: params["is_rare"] = is_rare
|
||||
if min_condition is not None: params["min_condition"] = min_condition
|
||||
if min_value is not None: params["min_value"] = min_value
|
||||
if max_value is not None: params["max_value"] = max_value
|
||||
if max_burden is not None: params["max_burden"] = max_burden
|
||||
params["sort_by"] = sort_by
|
||||
params["sort_dir"] = sort_dir
|
||||
params["page"] = page
|
||||
params["limit"] = limit
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.get(
|
||||
f"{INVENTORY_SERVICE_URL}/search/items",
|
||||
params=params
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return JSONResponse(content=response.json())
|
||||
else:
|
||||
logger.error(f"Inventory search service returned {response.status_code}")
|
||||
raise HTTPException(status_code=response.status_code, detail="Inventory search service error")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Could not reach inventory service: {e}")
|
||||
raise HTTPException(status_code=503, detail="Inventory service unavailable")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to search items: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@app.get("/search/equipped/{character_name}")
|
||||
async def search_equipped_items_proxy(
|
||||
character_name: str,
|
||||
slot: int = Query(None, description="Specific equipment slot mask")
|
||||
):
|
||||
"""Proxy to inventory service equipped items search."""
|
||||
try:
|
||||
params = {}
|
||||
if slot is not None:
|
||||
params["slot"] = slot
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
f"{INVENTORY_SERVICE_URL}/search/equipped/{character_name}",
|
||||
params=params
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return JSONResponse(content=response.json())
|
||||
elif response.status_code == 404:
|
||||
raise HTTPException(status_code=404, detail=f"No equipped items found for character '{character_name}'")
|
||||
else:
|
||||
logger.error(f"Inventory service returned {response.status_code} for equipped items search")
|
||||
raise HTTPException(status_code=response.status_code, detail="Inventory service error")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Could not reach inventory service: {e}")
|
||||
raise HTTPException(status_code=503, detail="Inventory service unavailable")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to search equipped items for {character_name}: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@app.get("/search/upgrades/{character_name}/{slot}")
|
||||
async def find_equipment_upgrades_proxy(
|
||||
character_name: str,
|
||||
slot: int,
|
||||
upgrade_type: str = Query("damage", description="What to optimize for: damage, armor, workmanship, value")
|
||||
):
|
||||
"""Proxy to inventory service equipment upgrades search."""
|
||||
try:
|
||||
params = {"upgrade_type": upgrade_type}
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
f"{INVENTORY_SERVICE_URL}/search/upgrades/{character_name}/{slot}",
|
||||
params=params
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return JSONResponse(content=response.json())
|
||||
elif response.status_code == 404:
|
||||
raise HTTPException(status_code=404, detail=f"No upgrade options found for character '{character_name}' slot {slot}")
|
||||
else:
|
||||
logger.error(f"Inventory service returned {response.status_code} for upgrades search")
|
||||
raise HTTPException(status_code=response.status_code, detail="Inventory service error")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Could not reach inventory service: {e}")
|
||||
raise HTTPException(status_code=503, detail="Inventory service unavailable")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to find equipment upgrades for {character_name} slot {slot}: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@app.get("/sets/list")
|
||||
async def list_equipment_sets_proxy():
|
||||
"""Proxy to inventory service equipment sets list."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(f"{INVENTORY_SERVICE_URL}/sets/list")
|
||||
|
||||
if response.status_code == 200:
|
||||
return JSONResponse(content=response.json())
|
||||
else:
|
||||
logger.error(f"Inventory service returned {response.status_code} for sets list")
|
||||
raise HTTPException(status_code=response.status_code, detail="Inventory service error")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Could not reach inventory service: {e}")
|
||||
raise HTTPException(status_code=503, detail="Inventory service unavailable")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list equipment sets: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
# -------------------- WebSocket endpoints -----------------------
|
||||
## WebSocket connection tracking
|
||||
# Set of browser WebSocket clients subscribed to live updates
|
||||
|
|
@ -994,6 +1276,38 @@ async def serve_icon(icon_filename: str):
|
|||
# Icon not found
|
||||
raise HTTPException(status_code=404, detail="Icon not found")
|
||||
|
||||
# -------------------- Inventory Service Proxy ---------------------------
|
||||
|
||||
@app.get("/inv/test")
|
||||
async def test_inventory_route():
|
||||
"""Test route to verify inventory proxy is working"""
|
||||
return {"message": "Inventory proxy route is working"}
|
||||
|
||||
@app.api_route("/inv/{path:path}", methods=["GET", "POST"])
|
||||
async def proxy_inventory_service(path: str, request: Request):
|
||||
"""Proxy all inventory service requests"""
|
||||
try:
|
||||
inventory_service_url = os.getenv('INVENTORY_SERVICE_URL', 'http://inventory-service:8000')
|
||||
logger.info(f"Proxying to inventory service: {inventory_service_url}/{path}")
|
||||
|
||||
# Forward the request to inventory service
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.request(
|
||||
method=request.method,
|
||||
url=f"{inventory_service_url}/{path}",
|
||||
params=request.query_params,
|
||||
headers=dict(request.headers),
|
||||
content=await request.body()
|
||||
)
|
||||
return Response(
|
||||
content=response.content,
|
||||
status_code=response.status_code,
|
||||
headers=dict(response.headers)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to proxy inventory request: {e}")
|
||||
raise HTTPException(status_code=500, detail="Inventory service unavailable")
|
||||
|
||||
# Icons are now served from static/icons directory
|
||||
# Serve SPA files (catch-all for frontend routes)
|
||||
# Mount the single-page application frontend (static assets) at root path
|
||||
|
|
|
|||
|
|
@ -31,6 +31,21 @@
|
|||
⚔️ Total Kills: <span id="totalKillsCount">Loading...</span>
|
||||
</div>
|
||||
|
||||
<!-- Heat map toggle -->
|
||||
<div class="heatmap-toggle">
|
||||
<label>
|
||||
<input type="checkbox" id="heatmapToggle">
|
||||
🔥 Show Spawn Heat Map
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Inventory search link -->
|
||||
<div class="inventory-search-link">
|
||||
<a href="#" id="inventorySearchBtn" onclick="openInventorySearch()">
|
||||
📦 Inventory Search
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Text input to filter active players by name -->
|
||||
<input type="text" id="playerFilter" class="player-filter" placeholder="Filter players..." />
|
||||
|
||||
|
|
@ -48,6 +63,7 @@
|
|||
<div id="mapContainer">
|
||||
<div id="mapGroup">
|
||||
<img id="map" src="dereth.png" alt="Dereth map">
|
||||
<canvas id="heatmapCanvas"></canvas>
|
||||
<svg id="trails"></svg>
|
||||
<div id="dots"></div>
|
||||
</div>
|
||||
|
|
|
|||
130
static/script.js
130
static/script.js
|
|
@ -181,6 +181,14 @@ const CHAT_COLOR_MAP = {
|
|||
31: '#FFFF00' // AdminTell
|
||||
};
|
||||
|
||||
/* ---------- Heat Map Globals ---------- */
|
||||
let heatmapCanvas, heatmapCtx;
|
||||
let heatmapEnabled = false;
|
||||
let heatmapData = null;
|
||||
let heatTimeout = null;
|
||||
const HEAT_PADDING = 50; // px beyond viewport to still draw
|
||||
const HEAT_THROTTLE = 16; // ~60 fps
|
||||
|
||||
/**
|
||||
* ---------- Player Color Assignment ----------------------------
|
||||
* Uses a predefined accessible color palette for player dots to ensure
|
||||
|
|
@ -344,6 +352,108 @@ function pxToWorld(x, y) {
|
|||
return { ew, ns };
|
||||
}
|
||||
|
||||
/* ---------- Heat Map Functions ---------- */
|
||||
|
||||
function initHeatMap() {
|
||||
heatmapCanvas = document.getElementById('heatmapCanvas');
|
||||
if (!heatmapCanvas) {
|
||||
console.error('Heat map canvas not found');
|
||||
return;
|
||||
}
|
||||
|
||||
heatmapCtx = heatmapCanvas.getContext('2d');
|
||||
|
||||
const toggle = document.getElementById('heatmapToggle');
|
||||
if (toggle) {
|
||||
toggle.addEventListener('change', e => {
|
||||
heatmapEnabled = e.target.checked;
|
||||
if (heatmapEnabled) {
|
||||
fetchHeatmapData();
|
||||
} else {
|
||||
clearHeatmap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', debounce(() => {
|
||||
if (heatmapEnabled && heatmapData) {
|
||||
renderHeatmap();
|
||||
}
|
||||
}, 250));
|
||||
}
|
||||
|
||||
async function fetchHeatmapData() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/spawns/heatmap?hours=24&limit=50000`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Heat map API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
heatmapData = data.spawn_points; // [{ew, ns, intensity}]
|
||||
console.log(`Loaded ${heatmapData.length} heat map points from last ${data.hours_window} hours`);
|
||||
renderHeatmap();
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch heat map data:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderHeatmap() {
|
||||
if (!heatmapEnabled || !heatmapData || !heatmapCanvas || !imgW || !imgH) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set canvas size to match map dimensions (1:1 DPI)
|
||||
heatmapCanvas.width = imgW;
|
||||
heatmapCanvas.height = imgH;
|
||||
heatmapCtx.clearRect(0, 0, imgW, imgH);
|
||||
|
||||
// Current visible map rect in px for viewport culling
|
||||
const vw = wrap.clientWidth;
|
||||
const vh = wrap.clientHeight;
|
||||
const viewL = -offX / scale;
|
||||
const viewT = -offY / scale;
|
||||
const viewR = viewL + vw / scale;
|
||||
const viewB = viewT + vh / scale;
|
||||
|
||||
// Render heat map points with viewport culling
|
||||
for (const point of heatmapData) {
|
||||
const { x, y } = worldToPx(point.ew, point.ns);
|
||||
|
||||
// Skip points outside visible area (with padding for smooth edges)
|
||||
if (x < viewL - HEAT_PADDING || x > viewR + HEAT_PADDING ||
|
||||
y < viewT - HEAT_PADDING || y > viewB + HEAT_PADDING) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Smaller, more precise spots to clearly show individual spawn locations
|
||||
const radius = Math.max(5, Math.min(12, 5 + Math.sqrt(point.intensity * 0.5)));
|
||||
|
||||
// Sharp gradient with distinct boundaries between spawn points
|
||||
const gradient = heatmapCtx.createRadialGradient(x, y, 0, x, y, radius);
|
||||
gradient.addColorStop(0, `rgba(255, 0, 0, ${Math.min(0.9, point.intensity / 40)})`); // Bright red center
|
||||
gradient.addColorStop(0.6, `rgba(255, 100, 0, ${Math.min(0.4, point.intensity / 120)})`); // Quick fade to orange
|
||||
gradient.addColorStop(1, 'rgba(255, 150, 0, 0)');
|
||||
|
||||
heatmapCtx.fillStyle = gradient;
|
||||
heatmapCtx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
|
||||
}
|
||||
}
|
||||
|
||||
function clearHeatmap() {
|
||||
if (heatmapCtx && heatmapCanvas) {
|
||||
heatmapCtx.clearRect(0, 0, heatmapCanvas.width, heatmapCanvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
function debounce(fn, ms) {
|
||||
let timeout;
|
||||
return (...args) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => fn(...args), ms);
|
||||
};
|
||||
}
|
||||
|
||||
// Show or create a stats window for a character
|
||||
function showStatsWindow(name) {
|
||||
if (statsWindows[name]) {
|
||||
|
|
@ -892,6 +1002,14 @@ function clampPan() {
|
|||
function updateView() {
|
||||
clampPan();
|
||||
applyTransform();
|
||||
|
||||
// Throttled heat map re-rendering during pan/zoom
|
||||
if (heatmapEnabled && heatmapData && !heatTimeout) {
|
||||
heatTimeout = setTimeout(() => {
|
||||
renderHeatmap();
|
||||
heatTimeout = null;
|
||||
}, HEAT_THROTTLE);
|
||||
}
|
||||
}
|
||||
|
||||
function fitToWindow() {
|
||||
|
|
@ -970,6 +1088,7 @@ img.onload = () => {
|
|||
fitToWindow();
|
||||
startPolling();
|
||||
initWebSocket();
|
||||
initHeatMap();
|
||||
};
|
||||
|
||||
/* ---------- rendering sorted list & dots ------------------------ */
|
||||
|
|
@ -1647,3 +1766,14 @@ function createMilestoneFireworks() {
|
|||
}
|
||||
}
|
||||
|
||||
/* ==================== INVENTORY SEARCH FUNCTIONALITY ==================== */
|
||||
|
||||
/**
|
||||
* Opens the dedicated inventory search page in a new browser tab.
|
||||
*/
|
||||
function openInventorySearch() {
|
||||
// Open the dedicated inventory search page in a new tab
|
||||
window.open('/inventory.html', '_blank');
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
126
static/style.css
126
static/style.css
|
|
@ -1176,3 +1176,129 @@ body.noselect, body.noselect * {
|
|||
.screen-shake {
|
||||
animation: screen-shake 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* ---------- Heat Map Canvas Layer ---------- */
|
||||
#heatmapCanvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
opacity: 0.85;
|
||||
mix-blend-mode: screen; /* Additive blending for nice heat map effect */
|
||||
}
|
||||
|
||||
/* Trails and dots use default positioning - no changes needed for layering */
|
||||
|
||||
/* Heat map toggle styling */
|
||||
.heatmap-toggle {
|
||||
margin: 0 0 12px;
|
||||
padding: 6px 12px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.heatmap-toggle input {
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.heatmap-toggle label {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Inventory search link styling */
|
||||
.inventory-search-link {
|
||||
margin: 0 0 12px;
|
||||
padding: 8px 12px;
|
||||
background: var(--card);
|
||||
border: 1px solid #4a9eff;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inventory-search-link a {
|
||||
color: #4a9eff;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.inventory-search-link a:hover {
|
||||
color: #fff;
|
||||
background: rgba(74, 158, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
padding: 2px 4px;
|
||||
margin: -2px -4px;
|
||||
}
|
||||
|
||||
/* Sortable column styles for inventory tables */
|
||||
.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
padding-right: 20px \!important;
|
||||
}
|
||||
|
||||
.sortable:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.results-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.results-table th,
|
||||
.results-table td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #333;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.results-table th {
|
||||
background-color: #222;
|
||||
font-weight: bold;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.results-table tr:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right \!important;
|
||||
}
|
||||
|
||||
.results-info {
|
||||
margin-bottom: 10px;
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Spell/Cantrip column styling */
|
||||
.spells-cell {
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
max-width: 200px;
|
||||
word-wrap: break-word;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.legendary-cantrip {
|
||||
color: #ffd700;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.regular-spell {
|
||||
color: #88ccff;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue