added inventory service for armor and jewelry

This commit is contained in:
erik 2025-06-12 23:05:33 +00:00
parent 09a6cd4946
commit 57a2384511
13 changed files with 2630 additions and 25 deletions

View file

@ -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"

View 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]}")

View file

@ -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": {

View file

@ -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)

View 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()

View 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()

View 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()

View file

@ -0,0 +1,6 @@
{
"AttributeSetInfo": {},
"MaterialInfo": {},
"SkillInfo": {},
"MasteryInfo": {}
}

File diff suppressed because it is too large Load diff

318
main.py
View file

@ -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

View file

@ -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>

View file

@ -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');
}

View file

@ -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;
}