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}"
|
DB_WAL_AUTOCHECKPOINT_PAGES: "${DB_WAL_AUTOCHECKPOINT_PAGES}"
|
||||||
SHARED_SECRET: "${SHARED_SECRET}"
|
SHARED_SECRET: "${SHARED_SECRET}"
|
||||||
LOG_LEVEL: "${LOG_LEVEL:-INFO}"
|
LOG_LEVEL: "${LOG_LEVEL:-INFO}"
|
||||||
|
INVENTORY_SERVICE_URL: "http://inventory-service:8000"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
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": {
|
"metadata": {
|
||||||
"extracted_at": "2025-06-10",
|
"extracted_at": "2025-06-10",
|
||||||
"source": "Mag-Plugins/Shared (comprehensive)",
|
"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": {
|
"enums": {
|
||||||
"CoverageMask": {
|
"CoverageMask": {
|
||||||
"name": "CoverageMask",
|
"name": "CoverageMask",
|
||||||
"values": {
|
"values": {
|
||||||
"0": "None",
|
"1": "Unknown",
|
||||||
|
"2": "UnderwearUpperLegs",
|
||||||
|
"4": "UnderwearLowerLegs",
|
||||||
"8": "UnderwearChest",
|
"8": "UnderwearChest",
|
||||||
|
"16": "UnderwearAbdomen",
|
||||||
"32": "UnderwearUpperArms",
|
"32": "UnderwearUpperArms",
|
||||||
"64": "UnderwearLowerArms",
|
"64": "UnderwearLowerArms",
|
||||||
"256": "OuterwearUpperLegs",
|
"256": "OuterwearUpperLegs",
|
||||||
|
|
@ -590,14 +595,9 @@
|
||||||
"128": "HadNoVitae",
|
"128": "HadNoVitae",
|
||||||
"129": "NoOlthoiTalk",
|
"129": "NoOlthoiTalk",
|
||||||
"130": "AutowieldLeft",
|
"130": "AutowieldLeft",
|
||||||
"131": "/* custom */",
|
|
||||||
"132": "[ServerOnly]",
|
|
||||||
"9001": "LinkedPortalOneSummon",
|
"9001": "LinkedPortalOneSummon",
|
||||||
"134": "[ServerOnly]",
|
|
||||||
"9002": "LinkedPortalTwoSummon",
|
"9002": "LinkedPortalTwoSummon",
|
||||||
"136": "[ServerOnly]",
|
|
||||||
"9003": "HouseEvicted",
|
"9003": "HouseEvicted",
|
||||||
"138": "[ServerOnly]",
|
|
||||||
"9004": "UntrainedSkills",
|
"9004": "UntrainedSkills",
|
||||||
"201326592": "Lockable_Decal",
|
"201326592": "Lockable_Decal",
|
||||||
"201326593": "Inscribable_Decal"
|
"201326593": "Inscribable_Decal"
|
||||||
|
|
@ -1996,9 +1996,254 @@
|
||||||
"12": "HeritageType"
|
"12": "HeritageType"
|
||||||
},
|
},
|
||||||
"source_file": "WieldRequirement.cs"
|
"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": {
|
"spells": {
|
||||||
"name": "Spells",
|
"name": "Spells",
|
||||||
"values": {
|
"values": {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,29 @@ class Item(Base):
|
||||||
value = Column(Integer, default=0)
|
value = Column(Integer, default=0)
|
||||||
burden = 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
|
# Metadata flags
|
||||||
has_id_data = Column(Boolean, default=False)
|
has_id_data = Column(Boolean, default=False)
|
||||||
last_id_time = Column(BigInteger, default=0)
|
last_id_time = Column(BigInteger, default=0)
|
||||||
|
|
@ -60,6 +83,11 @@ class ItemCombatStats(Base):
|
||||||
elemental_damage_vs_monsters = Column(Float)
|
elemental_damage_vs_monsters = Column(Float)
|
||||||
variance = 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 properties
|
||||||
attack_bonus = Column(Float)
|
attack_bonus = Column(Float)
|
||||||
weapon_time = Column(Integer)
|
weapon_time = Column(Integer)
|
||||||
|
|
@ -74,10 +102,25 @@ class ItemCombatStats(Base):
|
||||||
|
|
||||||
# Resistances
|
# Resistances
|
||||||
resist_magic = Column(Integer)
|
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 properties
|
||||||
mana_conversion_bonus = Column(Float)
|
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):
|
class ItemRequirements(Base):
|
||||||
"""Wield requirements and skill prerequisites."""
|
"""Wield requirements and skill prerequisites."""
|
||||||
__tablename__ = 'item_requirements'
|
__tablename__ = 'item_requirements'
|
||||||
|
|
@ -105,8 +148,30 @@ class ItemEnhancements(Base):
|
||||||
workmanship = Column(Float)
|
workmanship = Column(Float)
|
||||||
salvage_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
|
# Set information
|
||||||
item_set = Column(String(100), index=True)
|
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):
|
class ItemRatings(Base):
|
||||||
"""Modern rating system properties."""
|
"""Modern rating system properties."""
|
||||||
|
|
@ -126,6 +191,22 @@ class ItemRatings(Base):
|
||||||
heal_boost_rating = Column(Integer)
|
heal_boost_rating = Column(Integer)
|
||||||
vitality_rating = Column(Integer)
|
vitality_rating = Column(Integer)
|
||||||
mana_conversion_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
|
# Calculated totals
|
||||||
total_rating = Column(Integer)
|
total_rating = Column(Integer)
|
||||||
|
|
@ -164,6 +245,9 @@ def create_indexes(engine):
|
||||||
# Item search indexes
|
# Item search indexes
|
||||||
sa.Index('ix_items_search', Item.character_name, Item.name, Item.object_class).create(engine, checkfirst=True)
|
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
|
# Combat stats indexes for filtering
|
||||||
sa.Index('ix_combat_damage', ItemCombatStats.max_damage).create(engine, checkfirst=True)
|
sa.Index('ix_combat_damage', ItemCombatStats.max_damage).create(engine, checkfirst=True)
|
||||||
sa.Index('ix_combat_armor', ItemCombatStats.armor_level).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 typing import Dict, List, Any
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, Header, HTTPException, Query, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, Header, HTTPException, Query, WebSocket, WebSocketDisconnect, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse, Response
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
|
@ -386,6 +386,64 @@ async def get_total_rares():
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
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 ---------------------------------
|
# --- GET Inventory Endpoints ---------------------------------
|
||||||
@app.get("/inventory/{character_name}")
|
@app.get("/inventory/{character_name}")
|
||||||
async def get_character_inventory(character_name: str):
|
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")
|
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 endpoints -----------------------
|
||||||
## WebSocket connection tracking
|
## WebSocket connection tracking
|
||||||
# Set of browser WebSocket clients subscribed to live updates
|
# Set of browser WebSocket clients subscribed to live updates
|
||||||
|
|
@ -994,6 +1276,38 @@ async def serve_icon(icon_filename: str):
|
||||||
# Icon not found
|
# Icon not found
|
||||||
raise HTTPException(status_code=404, detail="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
|
# Icons are now served from static/icons directory
|
||||||
# Serve SPA files (catch-all for frontend routes)
|
# Serve SPA files (catch-all for frontend routes)
|
||||||
# Mount the single-page application frontend (static assets) at root path
|
# Mount the single-page application frontend (static assets) at root path
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,21 @@
|
||||||
⚔️ Total Kills: <span id="totalKillsCount">Loading...</span>
|
⚔️ Total Kills: <span id="totalKillsCount">Loading...</span>
|
||||||
</div>
|
</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 -->
|
<!-- Text input to filter active players by name -->
|
||||||
<input type="text" id="playerFilter" class="player-filter" placeholder="Filter players..." />
|
<input type="text" id="playerFilter" class="player-filter" placeholder="Filter players..." />
|
||||||
|
|
||||||
|
|
@ -48,6 +63,7 @@
|
||||||
<div id="mapContainer">
|
<div id="mapContainer">
|
||||||
<div id="mapGroup">
|
<div id="mapGroup">
|
||||||
<img id="map" src="dereth.png" alt="Dereth map">
|
<img id="map" src="dereth.png" alt="Dereth map">
|
||||||
|
<canvas id="heatmapCanvas"></canvas>
|
||||||
<svg id="trails"></svg>
|
<svg id="trails"></svg>
|
||||||
<div id="dots"></div>
|
<div id="dots"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
130
static/script.js
130
static/script.js
|
|
@ -181,6 +181,14 @@ const CHAT_COLOR_MAP = {
|
||||||
31: '#FFFF00' // AdminTell
|
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 ----------------------------
|
* ---------- Player Color Assignment ----------------------------
|
||||||
* Uses a predefined accessible color palette for player dots to ensure
|
* Uses a predefined accessible color palette for player dots to ensure
|
||||||
|
|
@ -344,6 +352,108 @@ function pxToWorld(x, y) {
|
||||||
return { ew, ns };
|
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
|
// Show or create a stats window for a character
|
||||||
function showStatsWindow(name) {
|
function showStatsWindow(name) {
|
||||||
if (statsWindows[name]) {
|
if (statsWindows[name]) {
|
||||||
|
|
@ -892,6 +1002,14 @@ function clampPan() {
|
||||||
function updateView() {
|
function updateView() {
|
||||||
clampPan();
|
clampPan();
|
||||||
applyTransform();
|
applyTransform();
|
||||||
|
|
||||||
|
// Throttled heat map re-rendering during pan/zoom
|
||||||
|
if (heatmapEnabled && heatmapData && !heatTimeout) {
|
||||||
|
heatTimeout = setTimeout(() => {
|
||||||
|
renderHeatmap();
|
||||||
|
heatTimeout = null;
|
||||||
|
}, HEAT_THROTTLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fitToWindow() {
|
function fitToWindow() {
|
||||||
|
|
@ -970,6 +1088,7 @@ img.onload = () => {
|
||||||
fitToWindow();
|
fitToWindow();
|
||||||
startPolling();
|
startPolling();
|
||||||
initWebSocket();
|
initWebSocket();
|
||||||
|
initHeatMap();
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---------- rendering sorted list & dots ------------------------ */
|
/* ---------- 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 {
|
.screen-shake {
|
||||||
animation: screen-shake 0.5s ease-in-out;
|
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