reduced duplicate insert errors of portals, still present because of two players disovering the same portal at the same time, other changes to inventory
This commit is contained in:
parent
e7ca39318f
commit
6c646719dd
6 changed files with 1093 additions and 232 deletions
|
|
@ -11,7 +11,7 @@ from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Any
|
from typing import Dict, List, Optional, Any
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Depends, Query
|
from fastapi import FastAPI, HTTPException, Depends, Query, Request
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse, StreamingResponse
|
from fastapi.responses import JSONResponse, StreamingResponse
|
||||||
from sse_starlette.sse import EventSourceResponse
|
from sse_starlette.sse import EventSourceResponse
|
||||||
|
|
@ -24,15 +24,65 @@ from database import (
|
||||||
ItemRatings, ItemSpells, ItemRawData, DATABASE_URL, create_indexes
|
ItemRatings, ItemSpells, ItemRawData, DATABASE_URL, create_indexes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Import helpers to share enum mappings
|
||||||
|
import helpers
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# FastAPI app
|
# Import suitbuilder router - after logger is defined
|
||||||
|
try:
|
||||||
|
from suitbuilder import router as suitbuilder_router, set_database_connection
|
||||||
|
SUITBUILDER_AVAILABLE = True
|
||||||
|
logger.info("Suitbuilder module imported successfully")
|
||||||
|
except ImportError as e:
|
||||||
|
logger.warning(f"Suitbuilder module not available: {e}")
|
||||||
|
SUITBUILDER_AVAILABLE = False
|
||||||
|
set_database_connection = None
|
||||||
|
|
||||||
|
# FastAPI app with comprehensive OpenAPI documentation
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Inventory Service",
|
title="Inventory Service API",
|
||||||
description="Microservice for Asheron's Call item data processing and queries",
|
description="""
|
||||||
version="1.0.0"
|
## Comprehensive Inventory Management API for Asheron's Call
|
||||||
|
|
||||||
|
This service provides powerful search and data processing capabilities for game item inventories.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
- **Item Search**: Advanced filtering across 40+ parameters including spells, stats, sets, and more
|
||||||
|
- **Character Management**: Multi-character inventory tracking and processing
|
||||||
|
- **Equipment Optimization**: Integration with suitbuilder for optimal equipment combinations
|
||||||
|
- **Real-time Processing**: Live inventory data ingestion and normalization
|
||||||
|
- **Comprehensive Filtering**: Support for armor, jewelry, weapons, clothing, and spell-based searches
|
||||||
|
|
||||||
|
### Main Endpoints
|
||||||
|
- `/search/items` - Advanced item search with extensive filtering options
|
||||||
|
- `/process-inventory` - Process and normalize raw inventory data
|
||||||
|
- `/inventory/{character}` - Get character-specific inventory data
|
||||||
|
- `/suitbuilder/search` - Equipment optimization and suit building
|
||||||
|
|
||||||
|
### Search Parameters
|
||||||
|
The search endpoint supports comprehensive filtering including:
|
||||||
|
- **Text Search**: Item names, descriptions, properties
|
||||||
|
- **Character Filtering**: Specific characters or cross-character search
|
||||||
|
- **Equipment Status**: Equipped, unequipped, or all items
|
||||||
|
- **Item Categories**: Armor, jewelry, weapons, clothing filters
|
||||||
|
- **Combat Properties**: Damage, armor, attack bonuses, ratings
|
||||||
|
- **Spell Filtering**: Specific spells, cantrips, legendary effects
|
||||||
|
- **Requirements**: Level, skill requirements
|
||||||
|
- **Enhancements**: Materials, workmanship, item sets, tinkering
|
||||||
|
- **Item State**: Bonded, attuned, condition, rarity
|
||||||
|
- **Sorting & Pagination**: Flexible result ordering and pagination
|
||||||
|
""",
|
||||||
|
version="1.0.0",
|
||||||
|
contact={
|
||||||
|
"name": "Dereth Tracker",
|
||||||
|
"url": "https://github.com/your-repo/dereth-tracker",
|
||||||
|
},
|
||||||
|
license_info={
|
||||||
|
"name": "MIT",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configure CORS
|
# Configure CORS
|
||||||
|
|
@ -44,6 +94,11 @@ app.add_middleware(
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Include suitbuilder router
|
||||||
|
if SUITBUILDER_AVAILABLE:
|
||||||
|
app.include_router(suitbuilder_router, prefix="/suitbuilder", tags=["suitbuilder"])
|
||||||
|
logger.info("Suitbuilder endpoints included at /suitbuilder")
|
||||||
|
|
||||||
# Database connection
|
# Database connection
|
||||||
database = databases.Database(DATABASE_URL)
|
database = databases.Database(DATABASE_URL)
|
||||||
engine = sa.create_engine(DATABASE_URL)
|
engine = sa.create_engine(DATABASE_URL)
|
||||||
|
|
@ -188,6 +243,9 @@ def load_comprehensive_enums():
|
||||||
|
|
||||||
ENUM_MAPPINGS = load_comprehensive_enums()
|
ENUM_MAPPINGS = load_comprehensive_enums()
|
||||||
|
|
||||||
|
# Share enum mappings with helpers module for suitbuilder
|
||||||
|
helpers.set_enum_mappings(ENUM_MAPPINGS)
|
||||||
|
|
||||||
# Pydantic models
|
# Pydantic models
|
||||||
class InventoryItem(BaseModel):
|
class InventoryItem(BaseModel):
|
||||||
"""Raw inventory item from plugin."""
|
"""Raw inventory item from plugin."""
|
||||||
|
|
@ -204,12 +262,100 @@ class ProcessedItem(BaseModel):
|
||||||
burden: int
|
burden: int
|
||||||
# Add other fields as needed
|
# Add other fields as needed
|
||||||
|
|
||||||
|
# Response Models for API Documentation
|
||||||
|
class ItemSearchResponse(BaseModel):
|
||||||
|
"""Response model for item search endpoint."""
|
||||||
|
items: List[Dict[str, Any]]
|
||||||
|
total_count: int
|
||||||
|
page: int
|
||||||
|
limit: int
|
||||||
|
has_next: bool
|
||||||
|
has_previous: bool
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "Gold Celdon Girth",
|
||||||
|
"character_name": "Megamula XXXIII",
|
||||||
|
"armor_level": 320,
|
||||||
|
"crit_damage_rating": 2,
|
||||||
|
"set_name": "Soldier's",
|
||||||
|
"material_name": "Gold",
|
||||||
|
"equipped": False,
|
||||||
|
"spells": ["Legendary Strength", "Legendary Endurance"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total_count": 1247,
|
||||||
|
"page": 1,
|
||||||
|
"limit": 200,
|
||||||
|
"has_next": True,
|
||||||
|
"has_previous": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProcessingStats(BaseModel):
|
||||||
|
"""Response model for inventory processing results."""
|
||||||
|
processed_count: int
|
||||||
|
error_count: int
|
||||||
|
character_name: str
|
||||||
|
timestamp: datetime
|
||||||
|
errors: Optional[List[str]] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"processed_count": 186,
|
||||||
|
"error_count": 22,
|
||||||
|
"character_name": "Megamula XXXIII",
|
||||||
|
"timestamp": "2024-01-15T10:30:00Z",
|
||||||
|
"errors": ["Item ID 12345: SQL type error", "Item ID 67890: Missing required field"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SetListResponse(BaseModel):
|
||||||
|
"""Response model for equipment sets list."""
|
||||||
|
sets: List[Dict[str, Any]]
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"sets": [
|
||||||
|
{"set_name": "Soldier's", "item_count": 847, "set_id": 13},
|
||||||
|
{"set_name": "Adept's", "item_count": 623, "set_id": 14},
|
||||||
|
{"set_name": "Hearty", "item_count": 1205, "set_id": 42}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HealthResponse(BaseModel):
|
||||||
|
"""Response model for health check."""
|
||||||
|
status: str
|
||||||
|
timestamp: datetime
|
||||||
|
database_connected: bool
|
||||||
|
version: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"status": "healthy",
|
||||||
|
"timestamp": "2024-01-15T10:30:00Z",
|
||||||
|
"database_connected": True,
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Startup/shutdown events
|
# Startup/shutdown events
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def startup():
|
async def startup():
|
||||||
"""Initialize database connection and create tables."""
|
"""Initialize database connection and create tables."""
|
||||||
await database.connect()
|
await database.connect()
|
||||||
|
|
||||||
|
# Share database connection with suitbuilder
|
||||||
|
if SUITBUILDER_AVAILABLE and set_database_connection:
|
||||||
|
set_database_connection(database)
|
||||||
|
|
||||||
# Create tables if they don't exist
|
# Create tables if they don't exist
|
||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
@ -1155,12 +1301,42 @@ def extract_item_properties(item_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
@app.post("/process-inventory")
|
@app.post("/process-inventory",
|
||||||
|
response_model=ProcessingStats,
|
||||||
|
summary="Process Raw Inventory Data",
|
||||||
|
description="""
|
||||||
|
**Process and normalize raw inventory data from game plugins.**
|
||||||
|
|
||||||
|
This endpoint receives raw inventory JSON data from game plugins and processes it
|
||||||
|
into normalized database tables with comprehensive enum translation and validation.
|
||||||
|
|
||||||
|
### Processing Steps:
|
||||||
|
1. **Data Validation**: Validate incoming JSON structure and required fields
|
||||||
|
2. **Enum Translation**: Convert game IDs to human-readable names using comprehensive enum database
|
||||||
|
3. **Normalization**: Split item data into related tables (combat stats, spells, enhancements, etc.)
|
||||||
|
4. **Error Handling**: Track and report any processing errors with detailed error messages
|
||||||
|
5. **Statistics**: Return comprehensive processing statistics
|
||||||
|
|
||||||
|
### Database Schema:
|
||||||
|
- **items**: Core item properties (name, icon, value, etc.)
|
||||||
|
- **item_combat_stats**: Armor level, damage bonuses, attack ratings
|
||||||
|
- **item_enhancements**: Material, workmanship, item sets, tinkering
|
||||||
|
- **item_spells**: Spell names and categories
|
||||||
|
- **item_requirements**: Level and skill requirements
|
||||||
|
- **item_ratings**: All rating values (crit damage, damage resist, etc.)
|
||||||
|
- **item_raw_data**: Original JSON for complex queries
|
||||||
|
|
||||||
|
### Error Handling:
|
||||||
|
Returns detailed error information for items that fail to process,
|
||||||
|
including SQL type errors, missing fields, and validation failures.
|
||||||
|
""",
|
||||||
|
tags=["Data Processing"])
|
||||||
async def process_inventory(inventory: InventoryItem):
|
async def process_inventory(inventory: InventoryItem):
|
||||||
"""Process raw inventory data and store in normalized format."""
|
"""Process raw inventory data and store in normalized database format."""
|
||||||
|
|
||||||
processed_count = 0
|
processed_count = 0
|
||||||
error_count = 0
|
error_count = 0
|
||||||
|
processing_errors = []
|
||||||
|
|
||||||
async with database.transaction():
|
async with database.transaction():
|
||||||
# First, delete all existing items for this character from all related tables
|
# First, delete all existing items for this character from all related tables
|
||||||
|
|
@ -1345,20 +1521,25 @@ async def process_inventory(inventory: InventoryItem):
|
||||||
processed_count += 1
|
processed_count += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing item {item_data.get('Id', 'unknown')}: {e}")
|
error_msg = f"Error processing item {item_data.get('Id', 'unknown')}: {e}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
processing_errors.append(error_msg)
|
||||||
error_count += 1
|
error_count += 1
|
||||||
|
|
||||||
logger.info(f"Inventory processing complete for {inventory.character_name}: {processed_count} processed, {error_count} errors, {len(inventory.items)} total items received")
|
logger.info(f"Inventory processing complete for {inventory.character_name}: {processed_count} processed, {error_count} errors, {len(inventory.items)} total items received")
|
||||||
|
|
||||||
return {
|
return ProcessingStats(
|
||||||
"status": "completed",
|
processed_count=processed_count,
|
||||||
"processed": processed_count,
|
error_count=error_count,
|
||||||
"errors": error_count,
|
character_name=inventory.character_name,
|
||||||
"total_received": len(inventory.items),
|
timestamp=inventory.timestamp,
|
||||||
"character": inventory.character_name
|
errors=processing_errors if processing_errors else None
|
||||||
}
|
)
|
||||||
|
|
||||||
@app.get("/inventory/{character_name}")
|
@app.get("/inventory/{character_name}",
|
||||||
|
summary="Get Character Inventory",
|
||||||
|
description="Retrieve processed inventory data for a specific character with normalized item properties.",
|
||||||
|
tags=["Character Data"])
|
||||||
async def get_character_inventory(
|
async def get_character_inventory(
|
||||||
character_name: str,
|
character_name: str,
|
||||||
limit: int = Query(1000, le=5000),
|
limit: int = Query(1000, le=5000),
|
||||||
|
|
@ -1613,14 +1794,48 @@ async def get_character_inventory(
|
||||||
"items": processed_items
|
"items": processed_items
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health",
|
||||||
|
response_model=HealthResponse,
|
||||||
|
summary="Service Health Check",
|
||||||
|
description="Returns service health status, database connectivity, and version information.",
|
||||||
|
tags=["System"])
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""Health check endpoint."""
|
"""Health check endpoint with comprehensive status information."""
|
||||||
return {"status": "healthy", "service": "inventory-service"}
|
try:
|
||||||
|
# Test database connectivity
|
||||||
|
await database.fetch_one("SELECT 1")
|
||||||
|
db_connected = True
|
||||||
|
except:
|
||||||
|
db_connected = False
|
||||||
|
|
||||||
|
return HealthResponse(
|
||||||
|
status="healthy" if db_connected else "degraded",
|
||||||
|
timestamp=datetime.now(),
|
||||||
|
database_connected=db_connected,
|
||||||
|
version="1.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
@app.get("/sets/list")
|
@app.get("/sets/list",
|
||||||
|
response_model=SetListResponse,
|
||||||
|
summary="List Equipment Sets",
|
||||||
|
description="""
|
||||||
|
**Get all unique equipment set names with item counts.**
|
||||||
|
|
||||||
|
Returns a list of all equipment sets found in the database along with
|
||||||
|
the number of items available for each set. Useful for understanding
|
||||||
|
available equipment options and set coverage.
|
||||||
|
|
||||||
|
### Common Set IDs:
|
||||||
|
- **13**: Soldier's (combat-focused armor)
|
||||||
|
- **14**: Adept's (magical armor)
|
||||||
|
- **42**: Hearty (health/vitality focused)
|
||||||
|
- **21**: Wise (mana-focused)
|
||||||
|
- **40**: Heroic Protector
|
||||||
|
- **41**: Heroic Destroyer
|
||||||
|
""",
|
||||||
|
tags=["Equipment Sets"])
|
||||||
async def list_equipment_sets():
|
async def list_equipment_sets():
|
||||||
"""Get all unique equipment set names from the database."""
|
"""Get all unique equipment set names with item counts from the database."""
|
||||||
try:
|
try:
|
||||||
# Get equipment set IDs (the numeric collection sets)
|
# Get equipment set IDs (the numeric collection sets)
|
||||||
query = """
|
query = """
|
||||||
|
|
@ -1650,17 +1865,19 @@ async def list_equipment_sets():
|
||||||
"item_count": item_count
|
"item_count": item_count
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return SetListResponse(
|
||||||
"equipment_sets": equipment_sets,
|
sets=equipment_sets
|
||||||
"total_sets": len(equipment_sets)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to list equipment sets: {e}")
|
logger.error(f"Failed to list equipment sets: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Failed to list equipment sets")
|
raise HTTPException(status_code=500, detail="Failed to list equipment sets")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/enum-info")
|
@app.get("/enum-info",
|
||||||
|
summary="Get Enum Information",
|
||||||
|
description="Get comprehensive information about available enum translations and database statistics.",
|
||||||
|
tags=["System"])
|
||||||
async def get_enum_info():
|
async def get_enum_info():
|
||||||
"""Get information about available enum translations."""
|
"""Get information about available enum translations."""
|
||||||
if ENUM_MAPPINGS is None:
|
if ENUM_MAPPINGS is None:
|
||||||
|
|
@ -1679,7 +1896,10 @@ async def get_enum_info():
|
||||||
"database_version": ENUM_MAPPINGS.get('full_database', {}).get('metadata', {}).get('version', 'unknown')
|
"database_version": ENUM_MAPPINGS.get('full_database', {}).get('metadata', {}).get('version', 'unknown')
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/translate/{enum_type}/{value}")
|
@app.get("/translate/{enum_type}/{value}",
|
||||||
|
summary="Translate Enum Value",
|
||||||
|
description="Translate a specific enum value to human-readable name using the comprehensive enum database.",
|
||||||
|
tags=["System"])
|
||||||
async def translate_enum_value(enum_type: str, value: int):
|
async def translate_enum_value(enum_type: str, value: int):
|
||||||
"""Translate a specific enum value to human-readable name."""
|
"""Translate a specific enum value to human-readable name."""
|
||||||
|
|
||||||
|
|
@ -1753,28 +1973,63 @@ async def get_character_inventory_raw(character_name: str):
|
||||||
# INVENTORY SEARCH API ENDPOINTS
|
# INVENTORY SEARCH API ENDPOINTS
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
@app.get("/search/items")
|
@app.get("/search/items",
|
||||||
|
response_model=ItemSearchResponse,
|
||||||
|
summary="Advanced Item Search",
|
||||||
|
description="""
|
||||||
|
**Comprehensive item search with extensive filtering capabilities.**
|
||||||
|
|
||||||
|
This endpoint provides powerful search functionality across all character inventories
|
||||||
|
with support for 40+ filter parameters including text search, equipment categories,
|
||||||
|
combat properties, spells, requirements, and more.
|
||||||
|
|
||||||
|
### Example Searches:
|
||||||
|
- **Basic Text Search**: `?text=Celdon` - Find all items with "Celdon" in name
|
||||||
|
- **Character Specific**: `?character=Megamula%20XXXIII` - Items from one character
|
||||||
|
- **Multi-Character**: `?characters=Char1,Char2,Char3` - Items from specific characters
|
||||||
|
- **Set-Based**: `?item_set=13&min_crit_damage_rating=2` - Soldier's set with CD2+
|
||||||
|
- **Spell Search**: `?legendary_cantrips=Legendary%20Strength,Legendary%20Endurance`
|
||||||
|
- **Category Filter**: `?armor_only=true&min_armor=300` - High-level armor only
|
||||||
|
- **Equipment Status**: `?equipment_status=unequipped` - Only inventory items
|
||||||
|
|
||||||
|
### Advanced Filtering:
|
||||||
|
- **Combat Properties**: Filter by damage, armor, attack bonuses, ratings
|
||||||
|
- **Spell Effects**: Search for specific cantrips, wards, or legendary effects
|
||||||
|
- **Item Categories**: Armor, jewelry, weapons, clothing with sub-categories
|
||||||
|
- **Enhancement Data**: Material types, workmanship, item sets, tinkering
|
||||||
|
- **Requirements**: Level, skill requirements for wielding
|
||||||
|
- **Item State**: Bonded, attuned, condition, rarity filters
|
||||||
|
|
||||||
|
### Response Format:
|
||||||
|
Returns paginated results with item details including translated properties,
|
||||||
|
combat stats, spell information, and character ownership data.
|
||||||
|
""",
|
||||||
|
tags=["Search"])
|
||||||
async def search_items(
|
async def search_items(
|
||||||
# Text search
|
# Text search
|
||||||
text: str = Query(None, description="Search item names, descriptions, or properties"),
|
text: str = Query(None, description="Search item names, descriptions, or properties", example="Celdon"),
|
||||||
character: str = Query(None, description="Limit search to specific character"),
|
character: str = Query(None, description="Limit search to specific character", example="Megamula XXXIII"),
|
||||||
characters: str = Query(None, description="Comma-separated list of character names"),
|
characters: str = Query(None, description="Comma-separated list of character names", example="Char1,Char2,Char3"),
|
||||||
include_all_characters: bool = Query(False, description="Search across all characters"),
|
include_all_characters: bool = Query(False, description="Search across all characters"),
|
||||||
|
|
||||||
# Equipment filtering
|
# Equipment filtering
|
||||||
equipment_status: str = Query(None, description="equipped, unequipped, or all"),
|
equipment_status: str = Query(None, description="Filter by equipment status: 'equipped', 'unequipped', or omit for all", example="unequipped"),
|
||||||
equipment_slot: int = Query(None, description="Equipment slot mask (e.g., 1=head, 512=chest)"),
|
equipment_slot: int = Query(None, description="Equipment slot mask (1=head, 2=neck, 4=chest, 8=abdomen, 16=upper_arms, 32=lower_arms, 64=hands, 128=upper_legs, 256=lower_legs, 512=feet, 1024=chest2, 2048=bracelet, 4096=ring)", example=4),
|
||||||
slot_names: str = Query(None, description="Comma-separated list of slot names (e.g., Head,Chest,Ring)"),
|
slot_names: str = Query(None, description="Comma-separated list of slot names", example="Head,Chest,Ring"),
|
||||||
|
|
||||||
# Item category filtering
|
# Item category filtering
|
||||||
armor_only: bool = Query(False, description="Show only armor items"),
|
armor_only: bool = Query(False, description="Show only armor items"),
|
||||||
jewelry_only: bool = Query(False, description="Show only jewelry items"),
|
jewelry_only: bool = Query(False, description="Show only jewelry items"),
|
||||||
weapon_only: bool = Query(False, description="Show only weapon items"),
|
weapon_only: bool = Query(False, description="Show only weapon items"),
|
||||||
|
clothing_only: bool = Query(False, description="Show only clothing items (shirts/pants)"),
|
||||||
|
shirt_only: bool = Query(False, description="Show only shirt underclothes"),
|
||||||
|
pants_only: bool = Query(False, description="Show only pants underclothes"),
|
||||||
|
underwear_only: bool = Query(False, description="Show all underclothes (shirts and pants)"),
|
||||||
|
|
||||||
# Spell filtering
|
# Spell filtering
|
||||||
has_spell: str = Query(None, description="Must have this specific spell (by name)"),
|
has_spell: str = Query(None, description="Must have this specific spell (by name)", example="Legendary Strength"),
|
||||||
spell_contains: str = Query(None, description="Spell name contains this text"),
|
spell_contains: str = Query(None, description="Spell name contains this text", example="Legendary"),
|
||||||
legendary_cantrips: str = Query(None, description="Comma-separated list of legendary cantrip names"),
|
legendary_cantrips: str = Query(None, description="Comma-separated list of legendary cantrip names", example="Legendary Strength,Legendary Endurance"),
|
||||||
|
|
||||||
# Combat properties
|
# Combat properties
|
||||||
min_damage: int = Query(None, description="Minimum damage"),
|
min_damage: int = Query(None, description="Minimum damage"),
|
||||||
|
|
@ -1782,8 +2037,8 @@ async def search_items(
|
||||||
min_armor: int = Query(None, description="Minimum armor level"),
|
min_armor: int = Query(None, description="Minimum armor level"),
|
||||||
max_armor: int = Query(None, description="Maximum armor level"),
|
max_armor: int = Query(None, description="Maximum armor level"),
|
||||||
min_attack_bonus: float = Query(None, description="Minimum attack bonus"),
|
min_attack_bonus: float = Query(None, description="Minimum attack bonus"),
|
||||||
min_crit_damage_rating: int = Query(None, description="Minimum critical damage rating"),
|
min_crit_damage_rating: int = Query(None, description="Minimum critical damage rating (0-2)", example=2),
|
||||||
min_damage_rating: int = Query(None, description="Minimum damage rating"),
|
min_damage_rating: int = Query(None, description="Minimum damage rating (0-3)", example=3),
|
||||||
min_heal_boost_rating: int = Query(None, description="Minimum heal boost rating"),
|
min_heal_boost_rating: int = Query(None, description="Minimum heal boost rating"),
|
||||||
min_vitality_rating: int = Query(None, description="Minimum vitality rating"),
|
min_vitality_rating: int = Query(None, description="Minimum vitality rating"),
|
||||||
min_damage_resist_rating: int = Query(None, description="Minimum damage resist rating"),
|
min_damage_resist_rating: int = Query(None, description="Minimum damage resist rating"),
|
||||||
|
|
@ -1807,12 +2062,12 @@ async def search_items(
|
||||||
min_level: int = Query(None, description="Minimum wield level requirement"),
|
min_level: int = Query(None, description="Minimum wield level requirement"),
|
||||||
|
|
||||||
# Enhancements
|
# Enhancements
|
||||||
material: str = Query(None, description="Material type (partial match)"),
|
material: str = Query(None, description="Material type (partial match)", example="Gold"),
|
||||||
min_workmanship: float = Query(None, description="Minimum workmanship"),
|
min_workmanship: float = Query(None, description="Minimum workmanship", example=9.5),
|
||||||
has_imbue: bool = Query(None, description="Has imbue effects"),
|
has_imbue: bool = Query(None, description="Has imbue effects"),
|
||||||
item_set: str = Query(None, description="Item set ID (single set)"),
|
item_set: str = Query(None, description="Item set ID (single set)", example="13"),
|
||||||
item_sets: str = Query(None, description="Comma-separated list of item set IDs"),
|
item_sets: str = Query(None, description="Comma-separated list of item set IDs", example="13,14,42"),
|
||||||
min_tinks: int = Query(None, description="Minimum tinker count"),
|
min_tinks: int = Query(None, description="Minimum tinker count", example=3),
|
||||||
|
|
||||||
# Item state
|
# Item state
|
||||||
bonded: bool = Query(None, description="Bonded status"),
|
bonded: bool = Query(None, description="Bonded status"),
|
||||||
|
|
@ -1836,6 +2091,15 @@ async def search_items(
|
||||||
Search items across characters with comprehensive filtering options.
|
Search items across characters with comprehensive filtering options.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Initialize underwear filter type
|
||||||
|
underwear_filter_type = None
|
||||||
|
if shirt_only:
|
||||||
|
underwear_filter_type = "shirts"
|
||||||
|
elif pants_only:
|
||||||
|
underwear_filter_type = "pants"
|
||||||
|
elif underwear_only:
|
||||||
|
underwear_filter_type = "all_underwear"
|
||||||
|
|
||||||
# Build base query with CTE for computed slot names
|
# Build base query with CTE for computed slot names
|
||||||
query_parts = ["""
|
query_parts = ["""
|
||||||
WITH items_with_slots AS (
|
WITH items_with_slots AS (
|
||||||
|
|
@ -1873,9 +2137,15 @@ async def search_items(
|
||||||
COALESCE((rd.int_values->>'376')::int, -1)
|
COALESCE((rd.int_values->>'376')::int, -1)
|
||||||
) as heal_boost_rating,
|
) as heal_boost_rating,
|
||||||
COALESCE((rd.int_values->>'379')::int, -1) as vitality_rating,
|
COALESCE((rd.int_values->>'379')::int, -1) as vitality_rating,
|
||||||
COALESCE((rd.int_values->>'308')::int, -1) as damage_resist_rating,
|
GREATEST(
|
||||||
|
COALESCE((rd.int_values->>'308')::int, -1),
|
||||||
|
COALESCE((rd.int_values->>'371')::int, -1)
|
||||||
|
) as damage_resist_rating,
|
||||||
COALESCE((rd.int_values->>'315')::int, -1) as crit_resist_rating,
|
COALESCE((rd.int_values->>'315')::int, -1) as crit_resist_rating,
|
||||||
COALESCE((rd.int_values->>'316')::int, -1) as crit_damage_resist_rating,
|
GREATEST(
|
||||||
|
COALESCE((rd.int_values->>'316')::int, -1),
|
||||||
|
COALESCE((rd.int_values->>'375')::int, -1)
|
||||||
|
) as crit_damage_resist_rating,
|
||||||
COALESCE((rd.int_values->>'317')::int, -1) as healing_resist_rating,
|
COALESCE((rd.int_values->>'317')::int, -1) as healing_resist_rating,
|
||||||
COALESCE((rd.int_values->>'331')::int, -1) as nether_resist_rating,
|
COALESCE((rd.int_values->>'331')::int, -1) as nether_resist_rating,
|
||||||
COALESCE((rd.int_values->>'342')::int, -1) as healing_rating,
|
COALESCE((rd.int_values->>'342')::int, -1) as healing_rating,
|
||||||
|
|
@ -1895,6 +2165,7 @@ async def search_items(
|
||||||
COALESCE(enh.tinks, -1) as tinks,
|
COALESCE(enh.tinks, -1) as tinks,
|
||||||
COALESCE(enh.item_set, '') as item_set,
|
COALESCE(enh.item_set, '') as item_set,
|
||||||
rd.original_json,
|
rd.original_json,
|
||||||
|
COALESCE((rd.int_values->>'218103821')::int, 0) as coverage_mask,
|
||||||
|
|
||||||
-- Compute slot_name in SQL
|
-- Compute slot_name in SQL
|
||||||
CASE
|
CASE
|
||||||
|
|
@ -1979,6 +2250,47 @@ async def search_items(
|
||||||
SELECT * FROM items_with_slots
|
SELECT * FROM items_with_slots
|
||||||
"""]
|
"""]
|
||||||
|
|
||||||
|
# Apply underwear filtering by modifying the CTE query
|
||||||
|
if underwear_filter_type:
|
||||||
|
# Insert WHERE clause into the CTE before the closing parenthesis
|
||||||
|
cte_where_clause = None
|
||||||
|
if underwear_filter_type == "shirts":
|
||||||
|
# Shirts: ObjectClass 3 with UnderwearChest (8) but NOT pants patterns
|
||||||
|
# Exclude items that have both UnderwearUpperLegs (2) and UnderwearLowerLegs (4) which indicate pants
|
||||||
|
cte_where_clause = """WHERE i.object_class = 3
|
||||||
|
AND ((rd.int_values->>'218103821')::int & 8) > 0
|
||||||
|
AND NOT ((rd.int_values->>'218103821')::int & 6) = 6
|
||||||
|
AND i.name NOT ILIKE '%robe%'
|
||||||
|
AND i.name NOT ILIKE '%cloak%'
|
||||||
|
AND i.name NOT ILIKE '%pallium%'
|
||||||
|
AND i.name NOT ILIKE '%armet%'
|
||||||
|
AND i.name NOT ILIKE '%pants%'
|
||||||
|
AND i.name NOT ILIKE '%breeches%'"""
|
||||||
|
elif underwear_filter_type == "pants":
|
||||||
|
# Pants: ObjectClass 3 with UnderwearUpperLegs (2) - covers both pants and breeches
|
||||||
|
# Include both full pants (2&4) and breeches (2 only)
|
||||||
|
cte_where_clause = """WHERE i.object_class = 3
|
||||||
|
AND ((rd.int_values->>'218103821')::int & 2) = 2
|
||||||
|
AND i.name NOT ILIKE '%robe%'
|
||||||
|
AND i.name NOT ILIKE '%cloak%'
|
||||||
|
AND i.name NOT ILIKE '%pallium%'
|
||||||
|
AND i.name NOT ILIKE '%armet%'"""
|
||||||
|
elif underwear_filter_type == "all_underwear":
|
||||||
|
# All underwear: ObjectClass 3 with any underwear bits (2,4,8,16)
|
||||||
|
cte_where_clause = """WHERE i.object_class = 3
|
||||||
|
AND ((rd.int_values->>'218103821')::int & 30) > 0
|
||||||
|
AND i.name NOT ILIKE '%robe%'
|
||||||
|
AND i.name NOT ILIKE '%cloak%'
|
||||||
|
AND i.name NOT ILIKE '%pallium%'
|
||||||
|
AND i.name NOT ILIKE '%armet%'"""
|
||||||
|
|
||||||
|
if cte_where_clause:
|
||||||
|
# Insert the WHERE clause before the closing parenthesis of the CTE
|
||||||
|
query_parts[0] = query_parts[0].replace(
|
||||||
|
"LEFT JOIN item_raw_data rd ON i.id = rd.item_id\n )",
|
||||||
|
f"LEFT JOIN item_raw_data rd ON i.id = rd.item_id\n {cte_where_clause}\n )"
|
||||||
|
)
|
||||||
|
|
||||||
conditions = []
|
conditions = []
|
||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
|
|
@ -2024,14 +2336,24 @@ async def search_items(
|
||||||
|
|
||||||
# Item category filtering
|
# Item category filtering
|
||||||
if armor_only:
|
if armor_only:
|
||||||
# Armor: ObjectClass 2 (Clothing) or 3 (Armor) with armor_level > 0
|
# Armor: ObjectClass 2 (Armor) with armor_level > 0
|
||||||
conditions.append("(object_class IN (2, 3) AND COALESCE(armor_level, 0) > 0)")
|
conditions.append("(object_class = 2 AND COALESCE(armor_level, 0) > 0)")
|
||||||
elif jewelry_only:
|
elif jewelry_only:
|
||||||
# Jewelry: ObjectClass 4 (Jewelry) - rings, bracelets, necklaces, amulets
|
# Jewelry: ObjectClass 4 (Jewelry) - rings, bracelets, necklaces, amulets
|
||||||
conditions.append("object_class = 4")
|
conditions.append("object_class = 4")
|
||||||
elif weapon_only:
|
elif weapon_only:
|
||||||
# Weapons: ObjectClass 6 (MeleeWeapon), 7 (MissileWeapon), 8 (Caster) with max_damage > 0
|
# Weapons: ObjectClass 6 (MeleeWeapon), 7 (MissileWeapon), 8 (Caster) with max_damage > 0
|
||||||
conditions.append("(object_class IN (6, 7, 8) AND COALESCE(max_damage, 0) > 0)")
|
conditions.append("(object_class IN (6, 7, 8) AND COALESCE(max_damage, 0) > 0)")
|
||||||
|
elif clothing_only:
|
||||||
|
# Clothing: ObjectClass 3 (Clothing) - shirts and pants only, exclude cloaks and robes
|
||||||
|
# Focus on underclothes: shirts, pants, breeches, etc.
|
||||||
|
conditions.append("""(object_class = 3 AND
|
||||||
|
name NOT ILIKE '%cloak%' AND
|
||||||
|
name NOT ILIKE '%robe%' AND
|
||||||
|
name NOT ILIKE '%pallium%' AND
|
||||||
|
name NOT ILIKE '%armet%' AND
|
||||||
|
(name ILIKE '%shirt%' OR name ILIKE '%pants%' OR name ILIKE '%breeches%' OR name ILIKE '%baggy%' OR name ILIKE '%tunic%'))""")
|
||||||
|
# Underwear filtering is handled in the CTE modification above
|
||||||
|
|
||||||
# Spell filtering - need to join with item_spells and use spell database
|
# Spell filtering - need to join with item_spells and use spell database
|
||||||
spell_join_added = False
|
spell_join_added = False
|
||||||
|
|
@ -2350,7 +2672,8 @@ async def search_items(
|
||||||
"crit_damage_rating": "crit_damage_rating",
|
"crit_damage_rating": "crit_damage_rating",
|
||||||
"heal_boost_rating": "heal_boost_rating",
|
"heal_boost_rating": "heal_boost_rating",
|
||||||
"vitality_rating": "vitality_rating",
|
"vitality_rating": "vitality_rating",
|
||||||
"damage_resist_rating": "damage_resist_rating"
|
"damage_resist_rating": "damage_resist_rating",
|
||||||
|
"crit_damage_resist_rating": "crit_damage_resist_rating"
|
||||||
}
|
}
|
||||||
sort_field = sort_mapping.get(sort_by, "name")
|
sort_field = sort_mapping.get(sort_by, "name")
|
||||||
sort_direction = "DESC" if sort_dir.lower() == "desc" else "ASC"
|
sort_direction = "DESC" if sort_dir.lower() == "desc" else "ASC"
|
||||||
|
|
@ -2396,9 +2719,15 @@ async def search_items(
|
||||||
COALESCE((rd.int_values->>'376')::int, -1)
|
COALESCE((rd.int_values->>'376')::int, -1)
|
||||||
) as heal_boost_rating,
|
) as heal_boost_rating,
|
||||||
COALESCE((rd.int_values->>'379')::int, -1) as vitality_rating,
|
COALESCE((rd.int_values->>'379')::int, -1) as vitality_rating,
|
||||||
COALESCE((rd.int_values->>'308')::int, -1) as damage_resist_rating,
|
GREATEST(
|
||||||
|
COALESCE((rd.int_values->>'308')::int, -1),
|
||||||
|
COALESCE((rd.int_values->>'371')::int, -1)
|
||||||
|
) as damage_resist_rating,
|
||||||
COALESCE((rd.int_values->>'315')::int, -1) as crit_resist_rating,
|
COALESCE((rd.int_values->>'315')::int, -1) as crit_resist_rating,
|
||||||
COALESCE((rd.int_values->>'316')::int, -1) as crit_damage_resist_rating,
|
GREATEST(
|
||||||
|
COALESCE((rd.int_values->>'316')::int, -1),
|
||||||
|
COALESCE((rd.int_values->>'375')::int, -1)
|
||||||
|
) as crit_damage_resist_rating,
|
||||||
COALESCE((rd.int_values->>'317')::int, -1) as healing_resist_rating,
|
COALESCE((rd.int_values->>'317')::int, -1) as healing_resist_rating,
|
||||||
COALESCE((rd.int_values->>'331')::int, -1) as nether_resist_rating,
|
COALESCE((rd.int_values->>'331')::int, -1) as nether_resist_rating,
|
||||||
COALESCE((rd.int_values->>'350')::int, -1) as dot_resist_rating,
|
COALESCE((rd.int_values->>'350')::int, -1) as dot_resist_rating,
|
||||||
|
|
@ -2561,8 +2890,11 @@ async def search_items(
|
||||||
item['coverage'] = ', '.join(coverage_parts)
|
item['coverage'] = ', '.join(coverage_parts)
|
||||||
else:
|
else:
|
||||||
item['coverage'] = f"Coverage_{coverage_value}"
|
item['coverage'] = f"Coverage_{coverage_value}"
|
||||||
|
# Add raw coverage mask for armor reduction system
|
||||||
|
item['coverage_mask'] = coverage_value
|
||||||
else:
|
else:
|
||||||
item['coverage'] = None
|
item['coverage'] = None
|
||||||
|
item['coverage_mask'] = 0
|
||||||
|
|
||||||
# Add sophisticated equipment slot translation using Mag-SuitBuilder logic
|
# Add sophisticated equipment slot translation using Mag-SuitBuilder logic
|
||||||
# Use both EquipableSlots_Decal and Coverage for armor reduction
|
# Use both EquipableSlots_Decal and Coverage for armor reduction
|
||||||
|
|
@ -2657,20 +2989,14 @@ async def search_items(
|
||||||
|
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
|
||||||
return {
|
return ItemSearchResponse(
|
||||||
"items": items,
|
items=items,
|
||||||
"total_count": total_count,
|
total_count=total_count,
|
||||||
"page": page,
|
page=page,
|
||||||
"limit": limit,
|
limit=limit,
|
||||||
"total_pages": (total_count + limit - 1) // limit,
|
has_next=page * limit < total_count,
|
||||||
"search_criteria": {
|
has_previous=page > 1
|
||||||
"text": text,
|
)
|
||||||
"character": character,
|
|
||||||
"include_all_characters": include_all_characters,
|
|
||||||
"equipment_status": equipment_status,
|
|
||||||
"filters_applied": len(conditions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Search error: {e}", exc_info=True)
|
logger.error(f"Search error: {e}", exc_info=True)
|
||||||
|
|
@ -2832,7 +3158,10 @@ async def find_equipment_upgrades(
|
||||||
raise HTTPException(status_code=500, detail=f"Equipment upgrades search failed: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Equipment upgrades search failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/characters/list")
|
@app.get("/characters/list",
|
||||||
|
summary="List Characters",
|
||||||
|
description="Get a list of all characters that have inventory data in the database.",
|
||||||
|
tags=["Character Data"])
|
||||||
async def list_inventory_characters():
|
async def list_inventory_characters():
|
||||||
"""List all characters that have inventory data."""
|
"""List all characters that have inventory data."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -3495,6 +3824,7 @@ async def test_simple_search(characters: str = Query(..., description="Comma-sep
|
||||||
|
|
||||||
@app.get("/optimize/suits/stream")
|
@app.get("/optimize/suits/stream")
|
||||||
async def stream_optimize_suits(
|
async def stream_optimize_suits(
|
||||||
|
request: Request, # Add request to detect client disconnection
|
||||||
characters: str = Query(..., description="Comma-separated character names"),
|
characters: str = Query(..., description="Comma-separated character names"),
|
||||||
primary_set: Optional[int] = Query(None, description="Primary set ID requirement"),
|
primary_set: Optional[int] = Query(None, description="Primary set ID requirement"),
|
||||||
secondary_set: Optional[int] = Query(None, description="Secondary set ID requirement"),
|
secondary_set: Optional[int] = Query(None, description="Secondary set ID requirement"),
|
||||||
|
|
@ -3590,6 +3920,15 @@ async def stream_optimize_suits(
|
||||||
logger.info(f"Generated {len(armor_combinations)} armor combinations")
|
logger.info(f"Generated {len(armor_combinations)} armor combinations")
|
||||||
|
|
||||||
for i, armor_combo in enumerate(armor_combinations):
|
for i, armor_combo in enumerate(armor_combinations):
|
||||||
|
# Check for client disconnection
|
||||||
|
if await request.is_disconnected():
|
||||||
|
logger.info("Client disconnected, stopping search")
|
||||||
|
yield {
|
||||||
|
"event": "cancelled",
|
||||||
|
"data": json.dumps({"message": "Search cancelled by client"})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
# Check time limit
|
# Check time limit
|
||||||
if time.time() - start_time > limit_config["time_limit"]:
|
if time.time() - start_time > limit_config["time_limit"]:
|
||||||
yield {
|
yield {
|
||||||
|
|
@ -3630,6 +3969,15 @@ async def stream_optimize_suits(
|
||||||
logger.info(f"Combination {i}: no scored suits returned")
|
logger.info(f"Combination {i}: no scored suits returned")
|
||||||
else:
|
else:
|
||||||
logger.info(f"Combination {i}: no complete suit generated")
|
logger.info(f"Combination {i}: no complete suit generated")
|
||||||
|
|
||||||
|
# Also check for disconnection every 10 iterations
|
||||||
|
if i % 10 == 0 and await request.is_disconnected():
|
||||||
|
logger.info("Client disconnected during search")
|
||||||
|
yield {
|
||||||
|
"event": "cancelled",
|
||||||
|
"data": json.dumps({"message": "Search cancelled by client"})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
# Final status
|
# Final status
|
||||||
yield {
|
yield {
|
||||||
|
|
@ -3804,6 +4152,15 @@ def determine_item_slots(item):
|
||||||
coverage = item.get("coverage_mask", item.get("coverage", 0))
|
coverage = item.get("coverage_mask", item.get("coverage", 0))
|
||||||
item_name = item["name"].lower()
|
item_name = item["name"].lower()
|
||||||
|
|
||||||
|
# Name-based detection for clothing items first (for ObjectClass 3)
|
||||||
|
if item["object_class"] == 3:
|
||||||
|
if any(word in item_name for word in ["pants", "breeches", "baggy"]):
|
||||||
|
slots.append("Pants")
|
||||||
|
return slots
|
||||||
|
elif any(word in item_name for word in ["shirt", "tunic"]):
|
||||||
|
slots.append("Shirt")
|
||||||
|
return slots
|
||||||
|
|
||||||
# Use coverage mask if available
|
# Use coverage mask if available
|
||||||
if coverage and coverage > 0:
|
if coverage and coverage > 0:
|
||||||
slots.extend(decode_coverage_to_slots(coverage, item["object_class"], item["name"]))
|
slots.extend(decode_coverage_to_slots(coverage, item["object_class"], item["name"]))
|
||||||
|
|
@ -3844,6 +4201,16 @@ def decode_coverage_to_slots(coverage_mask, object_class=None, item_name=''):
|
||||||
|
|
||||||
# Only check for clothing patterns if this is actually a clothing item (ObjectClass 3)
|
# Only check for clothing patterns if this is actually a clothing item (ObjectClass 3)
|
||||||
if object_class == 3:
|
if object_class == 3:
|
||||||
|
# Check coverage mask patterns for underclothes first
|
||||||
|
# Shirts: UnderwearChest (8) OR UnderwearAbdomen (16) = mask & 24 > 0
|
||||||
|
if coverage_mask & 24: # 8 + 16 = 24
|
||||||
|
slots.append("Shirt")
|
||||||
|
return slots
|
||||||
|
|
||||||
|
# Pants: UnderwearUpperLegs (2) AND UnderwearLowerLegs (4) = mask & 6 = 6
|
||||||
|
if (coverage_mask & 6) == 6: # Both bits 2 and 4 must be set
|
||||||
|
slots.append("Pants")
|
||||||
|
return slots
|
||||||
# Check for clothing patterns based on actual inventory data
|
# Check for clothing patterns based on actual inventory data
|
||||||
# Specific coverage patterns for ObjectClass 3 clothing items:
|
# Specific coverage patterns for ObjectClass 3 clothing items:
|
||||||
|
|
||||||
|
|
@ -3865,8 +4232,12 @@ def decode_coverage_to_slots(coverage_mask, object_class=None, item_name=''):
|
||||||
slots.append("Shirt")
|
slots.append("Shirt")
|
||||||
return slots
|
return slots
|
||||||
|
|
||||||
# Pants = UnderwearUpperLegs (2) + UnderwearLowerLegs (4) = 6
|
# Pants = UnderwearUpperLegs (2) + UnderwearLowerLegs (4) + UnderwearAbdomen (16) = 22
|
||||||
if coverage_mask & 2 and coverage_mask & 4: # UnderwearUpperLegs + UnderwearLowerLegs
|
if coverage_mask & 2 and coverage_mask & 4 and coverage_mask & 16: # Full pants pattern
|
||||||
|
slots.append("Pants")
|
||||||
|
return slots
|
||||||
|
# Also check for simple pants pattern without abdomen
|
||||||
|
elif coverage_mask & 2 and coverage_mask & 4: # UnderwearUpperLegs + UnderwearLowerLegs
|
||||||
slots.append("Pants")
|
slots.append("Pants")
|
||||||
return slots
|
return slots
|
||||||
|
|
||||||
|
|
|
||||||
60
main.py
60
main.py
|
|
@ -1985,16 +1985,16 @@ async def ws_receive_snapshots(
|
||||||
ew = float(ew)
|
ew = float(ew)
|
||||||
z = float(z)
|
z = float(z)
|
||||||
|
|
||||||
# Round coordinates for comparison (0.01 tolerance)
|
# Round coordinates for comparison (0.1 tolerance to match DB constraint)
|
||||||
ns_rounded = round(ns, 2)
|
ns_rounded = round(ns, 1)
|
||||||
ew_rounded = round(ew, 2)
|
ew_rounded = round(ew, 1)
|
||||||
|
|
||||||
# Check if portal exists at these coordinates
|
# Check if portal exists at these coordinates
|
||||||
existing_portal = await database.fetch_one(
|
existing_portal = await database.fetch_one(
|
||||||
"""
|
"""
|
||||||
SELECT id FROM portals
|
SELECT id FROM portals
|
||||||
WHERE ROUND(ns::numeric, 2) = :ns_rounded
|
WHERE ROUND(ns::numeric, 1) = :ns_rounded
|
||||||
AND ROUND(ew::numeric, 2) = :ew_rounded
|
AND ROUND(ew::numeric, 1) = :ew_rounded
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
|
|
@ -2004,26 +2004,48 @@ async def ws_receive_snapshots(
|
||||||
)
|
)
|
||||||
|
|
||||||
if not existing_portal:
|
if not existing_portal:
|
||||||
# Store new portal in database
|
# Store new portal in database with ON CONFLICT handling
|
||||||
await database.execute(
|
# This prevents race conditions and duplicate key errors
|
||||||
portals.insert().values(
|
try:
|
||||||
portal_name=portal_name,
|
await database.execute(
|
||||||
ns=ns,
|
portals.insert().values(
|
||||||
ew=ew,
|
portal_name=portal_name,
|
||||||
z=z,
|
ns=ns,
|
||||||
discovered_at=timestamp,
|
ew=ew,
|
||||||
discovered_by=character_name
|
z=z,
|
||||||
|
discovered_at=timestamp,
|
||||||
|
discovered_by=character_name
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
logger.info(f"New portal discovered: {portal_name} at {ns_rounded}, {ew_rounded} by {character_name}")
|
||||||
logger.info(f"New portal discovered: {portal_name} at {ns_rounded}, {ew_rounded} by {character_name}")
|
except Exception as insert_error:
|
||||||
|
# If insert fails due to duplicate, update the existing portal
|
||||||
|
if "duplicate key" in str(insert_error).lower():
|
||||||
|
await database.execute(
|
||||||
|
"""
|
||||||
|
UPDATE portals
|
||||||
|
SET discovered_at = :timestamp, discovered_by = :character_name
|
||||||
|
WHERE ROUND(ns::numeric, 1) = :ns_rounded
|
||||||
|
AND ROUND(ew::numeric, 1) = :ew_rounded
|
||||||
|
""",
|
||||||
|
{
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"character_name": character_name,
|
||||||
|
"ns_rounded": ns_rounded,
|
||||||
|
"ew_rounded": ew_rounded
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.debug(f"Portal already exists (race condition), updated: {portal_name} at {ns_rounded}, {ew_rounded}")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
else:
|
else:
|
||||||
# Update timestamp for existing portal to keep it alive
|
# Update timestamp for existing portal to keep it alive
|
||||||
await database.execute(
|
await database.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE portals
|
UPDATE portals
|
||||||
SET discovered_at = :timestamp, discovered_by = :character_name
|
SET discovered_at = :timestamp, discovered_by = :character_name
|
||||||
WHERE ROUND(ns::numeric, 2) = :ns_rounded
|
WHERE ROUND(ns::numeric, 1) = :ns_rounded
|
||||||
AND ROUND(ew::numeric, 2) = :ew_rounded
|
AND ROUND(ew::numeric, 1) = :ew_rounded
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
|
|
|
||||||
|
|
@ -90,53 +90,89 @@
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Filter Section Styling */
|
||||||
|
.filter-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card-header {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.filter-row {
|
.filter-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group {
|
.filter-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 3px;
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-group label {
|
.filter-group label {
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
color: #000;
|
color: #343a40;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group-wide label {
|
||||||
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="number"],
|
input[type="number"],
|
||||||
select {
|
select {
|
||||||
border: 1px solid #999;
|
border: 1px solid #ced4da;
|
||||||
padding: 1px 3px;
|
border-radius: 3px;
|
||||||
|
padding: 4px 6px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
height: 18px;
|
height: 24px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #80bdff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
width: 120px;
|
width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"] {
|
input[type="number"] {
|
||||||
width: 40px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
width: 100px;
|
width: 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-section {
|
.range-separator {
|
||||||
display: flex;
|
color: #6c757d;
|
||||||
align-items: flex-start;
|
font-weight: bold;
|
||||||
gap: 5px;
|
margin: 0 4px;
|
||||||
margin-bottom: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.section-label {
|
.section-label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|
@ -145,11 +181,18 @@
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox-sections-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox-container {
|
.checkbox-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 4px;
|
||||||
flex: 1;
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-item {
|
.checkbox-item {
|
||||||
|
|
@ -157,6 +200,8 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
width: calc(50% - 2px);
|
||||||
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-item input[type="checkbox"] {
|
.checkbox-item input[type="checkbox"] {
|
||||||
|
|
@ -180,8 +225,9 @@
|
||||||
|
|
||||||
.search-actions {
|
.search-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
gap: 10px;
|
||||||
margin-top: 3px;
|
margin-top: 15px;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
|
|
@ -429,32 +475,52 @@
|
||||||
|
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<form class="search-form" id="inventorySearchForm">
|
<form class="search-form" id="inventorySearchForm">
|
||||||
<!-- Row 0: Equipment Type Selection -->
|
|
||||||
<div class="filter-row">
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>Type:</label>
|
|
||||||
<div style="display: flex; gap: 10px;">
|
|
||||||
<label style="display: flex; align-items: center; font-weight: normal;">
|
|
||||||
<input type="radio" name="equipmentType" id="armorOnly" value="armor" checked style="margin-right: 3px;">
|
|
||||||
Armor Only
|
|
||||||
</label>
|
|
||||||
<label style="display: flex; align-items: center; font-weight: normal;">
|
|
||||||
<input type="radio" name="equipmentType" id="jewelryOnly" value="jewelry" style="margin-right: 3px;">
|
|
||||||
Jewelry Only
|
|
||||||
</label>
|
|
||||||
<label style="display: flex; align-items: center; font-weight: normal;">
|
|
||||||
<input type="radio" name="equipmentType" id="allItems" value="all" style="margin-right: 3px;">
|
|
||||||
All Items
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Row 0.5: Slot Selection -->
|
<!-- Basic Filters -->
|
||||||
<div class="filter-row">
|
<div class="filter-card">
|
||||||
<div class="filter-group">
|
<div class="filter-card-header">Basic Search</div>
|
||||||
<label>Slot:</label>
|
|
||||||
<select id="slotFilter">
|
<div class="filter-row">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Name:</label>
|
||||||
|
<input type="text" id="searchText" placeholder="Item name">
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Status:</label>
|
||||||
|
<select id="searchEquipStatus">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="equipped">Equipped</option>
|
||||||
|
<option value="unequipped">Inventory</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Type:</label>
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<label style="display: flex; align-items: center; font-weight: normal;">
|
||||||
|
<input type="radio" name="equipmentType" id="armorOnly" value="armor" checked style="margin-right: 3px;">
|
||||||
|
Armor Only
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; font-weight: normal;">
|
||||||
|
<input type="radio" name="equipmentType" id="jewelryOnly" value="jewelry" style="margin-right: 3px;">
|
||||||
|
Jewelry Only
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; font-weight: normal;">
|
||||||
|
<input type="radio" name="equipmentType" id="shirtOnly" value="shirt" style="margin-right: 3px;">
|
||||||
|
Shirts Only
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; font-weight: normal;">
|
||||||
|
<input type="radio" name="equipmentType" id="pantsOnly" value="pants" style="margin-right: 3px;">
|
||||||
|
Pants Only
|
||||||
|
</label>
|
||||||
|
<label style="display: flex; align-items: center; font-weight: normal;">
|
||||||
|
<input type="radio" name="equipmentType" id="allItems" value="all" style="margin-right: 3px;">
|
||||||
|
All Items
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Slot:</label>
|
||||||
|
<select id="slotFilter">
|
||||||
<option value="">All Slots</option>
|
<option value="">All Slots</option>
|
||||||
<optgroup label="Armor Slots">
|
<optgroup label="Armor Slots">
|
||||||
<option value="Head">Head</option>
|
<option value="Head">Head</option>
|
||||||
|
|
@ -480,66 +546,61 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 1: Basic filters -->
|
<!-- Stats Filters -->
|
||||||
<div class="filter-row">
|
<div class="filter-card">
|
||||||
<div class="filter-group">
|
<div class="filter-card-header">Item Stats</div>
|
||||||
<label>Name:</label>
|
<div class="filter-row">
|
||||||
<input type="text" id="searchText" placeholder="Item name">
|
<div class="filter-group">
|
||||||
|
<label>Armor:</label>
|
||||||
|
<input type="number" id="searchMinArmor" placeholder="Min">
|
||||||
|
<span class="range-separator">-</span>
|
||||||
|
<input type="number" id="searchMaxArmor" placeholder="Max">
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Crit Dmg:</label>
|
||||||
|
<input type="number" id="searchMinCritDamage" placeholder="Min">
|
||||||
|
<span class="range-separator">-</span>
|
||||||
|
<input type="number" id="searchMaxCritDamage" placeholder="Max">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
|
||||||
<label>Status:</label>
|
<div class="filter-row">
|
||||||
<select id="searchEquipStatus">
|
<div class="filter-group">
|
||||||
<option value="all">All</option>
|
<label>Dmg Rating:</label>
|
||||||
<option value="equipped">Equipped</option>
|
<input type="number" id="searchMinDamageRating" placeholder="Min">
|
||||||
<option value="unequipped">Inventory</option>
|
<span class="range-separator">-</span>
|
||||||
</select>
|
<input type="number" id="searchMaxDamageRating" placeholder="Max">
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Heal Boost:</label>
|
||||||
|
<input type="number" id="searchMinHealBoost" placeholder="Min">
|
||||||
|
<span class="range-separator">-</span>
|
||||||
|
<input type="number" id="searchMaxHealBoost" placeholder="Max">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="filter-row">
|
||||||
<!-- Row 2: Stats -->
|
<div class="filter-group">
|
||||||
<div class="filter-row">
|
<label>Vitality:</label>
|
||||||
<div class="filter-group">
|
<input type="number" id="searchMinVitalityRating" placeholder="Min">
|
||||||
<label>Armor:</label>
|
</div>
|
||||||
<input type="number" id="searchMinArmor" placeholder="Min">
|
<div class="filter-group">
|
||||||
<span>-</span>
|
<label>Dmg Resist:</label>
|
||||||
<input type="number" id="searchMaxArmor" placeholder="Max">
|
<input type="number" id="searchMinDamageResistRating" placeholder="Min">
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>Crit:</label>
|
<label>Crit Dmg Resist:</label>
|
||||||
<input type="number" id="searchMinCritDamage" placeholder="Min">
|
<input type="number" id="searchMinCritDamageResistRating" placeholder="Min">
|
||||||
<span>-</span>
|
</div>
|
||||||
<input type="number" id="searchMaxCritDamage" placeholder="Max">
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>Dmg:</label>
|
|
||||||
<input type="number" id="searchMinDamageRating" placeholder="Min">
|
|
||||||
<span>-</span>
|
|
||||||
<input type="number" id="searchMaxDamageRating" placeholder="Max">
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>Heal:</label>
|
|
||||||
<input type="number" id="searchMinHealBoost" placeholder="Min">
|
|
||||||
<span>-</span>
|
|
||||||
<input type="number" id="searchMaxHealBoost" placeholder="Max">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- New Rating Filters -->
|
|
||||||
<div class="filter-row">
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>Vitality:</label>
|
|
||||||
<input type="number" id="searchMinVitalityRating" placeholder="Min">
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>Dmg Resist:</label>
|
|
||||||
<input type="number" id="searchMinDamageResistRating" placeholder="Min">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Equipment Sets -->
|
<!-- Checkbox Sections in Grid Layout -->
|
||||||
<div class="filter-section">
|
<div class="checkbox-sections-container">
|
||||||
<label class="section-label">Set:</label>
|
<!-- Equipment Sets -->
|
||||||
<div class="checkbox-container" id="equipmentSets">
|
<div class="filter-card">
|
||||||
|
<div class="filter-card-header">Equipment Sets</div>
|
||||||
|
<div class="checkbox-container" id="equipmentSets">
|
||||||
<div class="checkbox-item">
|
<div class="checkbox-item">
|
||||||
<input type="checkbox" id="set_14" value="14">
|
<input type="checkbox" id="set_14" value="14">
|
||||||
<label for="set_14">Adept's</label>
|
<label for="set_14">Adept's</label>
|
||||||
|
|
@ -605,11 +666,11 @@
|
||||||
<label for="set_29">Lightning Proof</label>
|
<label for="set_29">Lightning Proof</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Legendary Cantrips -->
|
<!-- Legendary Cantrips -->
|
||||||
<div class="filter-section">
|
<div class="filter-card">
|
||||||
<label class="section-label">Cantrips:</label>
|
<div class="filter-card-header">Legendary Cantrips</div>
|
||||||
<div class="checkbox-container" id="cantrips">
|
<div class="checkbox-container" id="cantrips">
|
||||||
<!-- Legendary Attributes -->
|
<!-- Legendary Attributes -->
|
||||||
<div class="checkbox-item">
|
<div class="checkbox-item">
|
||||||
|
|
@ -782,11 +843,11 @@
|
||||||
<label for="cantrip_legendary_storm_bane">Storm Bane</label>
|
<label for="cantrip_legendary_storm_bane">Storm Bane</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Legendary Wards -->
|
<!-- Legendary Wards -->
|
||||||
<div class="filter-section">
|
<div class="filter-card">
|
||||||
<label class="section-label">Wards:</label>
|
<div class="filter-card-header">Legendary Wards</div>
|
||||||
<div class="checkbox-container" id="protections">
|
<div class="checkbox-container" id="protections">
|
||||||
<div class="checkbox-item">
|
<div class="checkbox-item">
|
||||||
<input type="checkbox" id="protection_flame" value="Legendary Flame Ward">
|
<input type="checkbox" id="protection_flame" value="Legendary Flame Ward">
|
||||||
|
|
@ -821,15 +882,12 @@
|
||||||
<label for="protection_armor">Armor</label>
|
<label for="protection_armor">Armor</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Equipment Slots -->
|
<!-- Equipment Slots -->
|
||||||
<div class="filter-section">
|
<div class="filter-card">
|
||||||
<label class="section-label">Equipment Slots:</label>
|
<div class="filter-card-header">Equipment Slots</div>
|
||||||
|
<div class="checkbox-container" id="all-slots">
|
||||||
<!-- Armor Slots -->
|
|
||||||
<div class="checkbox-container" id="armor-slots">
|
|
||||||
<label class="subsection-label">Armor:</label>
|
|
||||||
<div class="checkbox-item">
|
<div class="checkbox-item">
|
||||||
<input type="checkbox" id="slot_head" value="Head">
|
<input type="checkbox" id="slot_head" value="Head">
|
||||||
<label for="slot_head">Head</label>
|
<label for="slot_head">Head</label>
|
||||||
|
|
@ -870,11 +928,6 @@
|
||||||
<input type="checkbox" id="slot_shield" value="Shield">
|
<input type="checkbox" id="slot_shield" value="Shield">
|
||||||
<label for="slot_shield">Shield</label>
|
<label for="slot_shield">Shield</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Jewelry Slots -->
|
|
||||||
<div class="checkbox-container" id="jewelry-slots">
|
|
||||||
<label class="subsection-label">Jewelry:</label>
|
|
||||||
<div class="checkbox-item">
|
<div class="checkbox-item">
|
||||||
<input type="checkbox" id="slot_neck" value="Neck">
|
<input type="checkbox" id="slot_neck" value="Neck">
|
||||||
<label for="slot_neck">Neck</label>
|
<label for="slot_neck">Neck</label>
|
||||||
|
|
@ -892,6 +945,7 @@
|
||||||
<label for="slot_trinket">Trinket</label>
|
<label for="slot_trinket">Trinket</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-actions">
|
<div class="search-actions">
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,10 @@ function buildSearchParameters() {
|
||||||
params.append('armor_only', 'true');
|
params.append('armor_only', 'true');
|
||||||
} else if (equipmentType === 'jewelry') {
|
} else if (equipmentType === 'jewelry') {
|
||||||
params.append('jewelry_only', 'true');
|
params.append('jewelry_only', 'true');
|
||||||
|
} else if (equipmentType === 'shirt') {
|
||||||
|
params.append('shirt_only', 'true');
|
||||||
|
} else if (equipmentType === 'pants') {
|
||||||
|
params.append('pants_only', 'true');
|
||||||
}
|
}
|
||||||
// If 'all' is selected, don't add any type filter
|
// If 'all' is selected, don't add any type filter
|
||||||
|
|
||||||
|
|
@ -298,6 +302,7 @@ function buildSearchParameters() {
|
||||||
addParam(params, 'max_heal_boost_rating', 'searchMaxHealBoost');
|
addParam(params, 'max_heal_boost_rating', 'searchMaxHealBoost');
|
||||||
addParam(params, 'min_vitality_rating', 'searchMinVitalityRating');
|
addParam(params, 'min_vitality_rating', 'searchMinVitalityRating');
|
||||||
addParam(params, 'min_damage_resist_rating', 'searchMinDamageResistRating');
|
addParam(params, 'min_damage_resist_rating', 'searchMinDamageResistRating');
|
||||||
|
addParam(params, 'min_crit_damage_resist_rating', 'searchMinCritDamageResistRating');
|
||||||
|
|
||||||
// Requirements parameters
|
// Requirements parameters
|
||||||
addParam(params, 'min_level', 'searchMinLevel');
|
addParam(params, 'min_level', 'searchMinLevel');
|
||||||
|
|
@ -438,6 +443,7 @@ function displayResults(data) {
|
||||||
<th class="text-right sortable" data-sort="heal_boost_rating">Heal Boost${getSortIcon('heal_boost_rating')}</th>
|
<th class="text-right sortable" data-sort="heal_boost_rating">Heal Boost${getSortIcon('heal_boost_rating')}</th>
|
||||||
<th class="text-right sortable" data-sort="vitality_rating">Vitality${getSortIcon('vitality_rating')}</th>
|
<th class="text-right sortable" data-sort="vitality_rating">Vitality${getSortIcon('vitality_rating')}</th>
|
||||||
<th class="text-right sortable" data-sort="damage_resist_rating">Dmg Resist${getSortIcon('damage_resist_rating')}</th>
|
<th class="text-right sortable" data-sort="damage_resist_rating">Dmg Resist${getSortIcon('damage_resist_rating')}</th>
|
||||||
|
<th class="text-right sortable" data-sort="crit_damage_resist_rating">Crit Dmg Resist${getSortIcon('crit_damage_resist_rating')}</th>
|
||||||
<th class="text-right sortable" data-sort="last_updated">Last Updated${getSortIcon('last_updated')}</th>
|
<th class="text-right sortable" data-sort="last_updated">Last Updated${getSortIcon('last_updated')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -451,6 +457,7 @@ function displayResults(data) {
|
||||||
const healBoostRating = item.heal_boost_rating > 0 ? item.heal_boost_rating : '-';
|
const healBoostRating = item.heal_boost_rating > 0 ? item.heal_boost_rating : '-';
|
||||||
const vitalityRating = item.vitality_rating > 0 ? item.vitality_rating : '-';
|
const vitalityRating = item.vitality_rating > 0 ? item.vitality_rating : '-';
|
||||||
const damageResistRating = item.damage_resist_rating > 0 ? item.damage_resist_rating : '-';
|
const damageResistRating = item.damage_resist_rating > 0 ? item.damage_resist_rating : '-';
|
||||||
|
const critDamageResistRating = item.crit_damage_resist_rating > 0 ? item.crit_damage_resist_rating : '-';
|
||||||
const status = item.is_equipped ? '⚔️ Equipped' : '📦 Inventory';
|
const status = item.is_equipped ? '⚔️ Equipped' : '📦 Inventory';
|
||||||
const statusClass = item.is_equipped ? 'status-equipped' : 'status-inventory';
|
const statusClass = item.is_equipped ? 'status-equipped' : 'status-inventory';
|
||||||
|
|
||||||
|
|
@ -522,6 +529,7 @@ function displayResults(data) {
|
||||||
<td class="text-right">${healBoostRating}</td>
|
<td class="text-right">${healBoostRating}</td>
|
||||||
<td class="text-right">${vitalityRating}</td>
|
<td class="text-right">${vitalityRating}</td>
|
||||||
<td class="text-right">${damageResistRating}</td>
|
<td class="text-right">${damageResistRating}</td>
|
||||||
|
<td class="text-right">${critDamageResistRating}</td>
|
||||||
<td class="text-right">${lastUpdated}</td>
|
<td class="text-right">${lastUpdated}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -657,6 +657,176 @@ body {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ratings display */
|
||||||
|
.item-ratings {
|
||||||
|
color: #0066cc;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #e6f3ff;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty slot styling */
|
||||||
|
.suit-item-entry.empty-slot {
|
||||||
|
opacity: 0.6;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-left: 3px solid #dee2e6;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-slot-text {
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New Column-Based Table Layout */
|
||||||
|
.suit-items-table {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suit-items-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px 120px 250px 140px 250px 60px 120px;
|
||||||
|
gap: 8px;
|
||||||
|
background: #2c3e50;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 11px;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suit-items-header > div {
|
||||||
|
color: white !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suit-items-body {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suit-item-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px 120px 250px 140px 250px 60px 120px;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 4px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suit-item-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suit-item-row:nth-child(even) {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suit-item-row.empty-slot {
|
||||||
|
opacity: 0.5;
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Column styling */
|
||||||
|
.col-slot {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-character {
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-item {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-set {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suit-items-body .col-set {
|
||||||
|
color: #1f2937;
|
||||||
|
background: #f3f4f6;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-spells {
|
||||||
|
color: #7c3aed;
|
||||||
|
font-size: 11px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-armor {
|
||||||
|
color: #059669;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-ratings {
|
||||||
|
color: #0066cc;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.need-reducing {
|
||||||
|
color: #dc2626;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments for table */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.suit-items-header,
|
||||||
|
.suit-item-row {
|
||||||
|
grid-template-columns: 70px 100px 200px 120px 200px 50px 100px;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.suit-items-header,
|
||||||
|
.suit-item-row {
|
||||||
|
grid-template-columns: 60px 80px 150px 100px 150px 40px 80px;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-spells,
|
||||||
|
.col-ratings {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Progressive Search Styles */
|
/* Progressive Search Styles */
|
||||||
.search-progress {
|
.search-progress {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
// Suitbuilder JavaScript - Constraint Solver Frontend Logic
|
// Suitbuilder JavaScript - Constraint Solver Frontend Logic
|
||||||
|
console.log('Suitbuilder.js loaded - VERSION: SCORE_ORDERING_AND_CANCELLATION_FIX_v3');
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
const API_BASE = '/inv/suitbuilder';
|
const API_BASE = '/inv/suitbuilder';
|
||||||
let currentSuits = [];
|
let currentSuits = [];
|
||||||
let lockedSlots = new Set();
|
let lockedSlots = new Set();
|
||||||
let selectedSuit = null;
|
let selectedSuit = null;
|
||||||
|
let currentSearchController = null; // AbortController for current search
|
||||||
|
|
||||||
// Initialize when page loads
|
// Initialize when page loads
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
@ -163,9 +165,14 @@ async function performSuitSearch() {
|
||||||
try {
|
try {
|
||||||
await streamOptimalSuits(constraints);
|
await streamOptimalSuits(constraints);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Suit search error:', error);
|
// Don't show error for user-cancelled searches
|
||||||
resultsDiv.innerHTML = `<div class="error">❌ Suit search failed: ${error.message}</div>`;
|
if (error.name === 'AbortError') {
|
||||||
countSpan.textContent = '';
|
console.log('Search cancelled by user');
|
||||||
|
} else {
|
||||||
|
console.error('Suit search error:', error);
|
||||||
|
resultsDiv.innerHTML = `<div class="error">❌ Suit search failed: ${error.message}</div>`;
|
||||||
|
countSpan.textContent = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,13 +267,22 @@ async function streamOptimalSuits(constraints) {
|
||||||
|
|
||||||
console.log('Starting suit search with constraints:', requestBody);
|
console.log('Starting suit search with constraints:', requestBody);
|
||||||
|
|
||||||
|
// Cancel any existing search
|
||||||
|
if (currentSearchController) {
|
||||||
|
currentSearchController.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new AbortController for this search
|
||||||
|
currentSearchController = new AbortController();
|
||||||
|
|
||||||
// Use fetch with streaming response instead of EventSource for POST support
|
// Use fetch with streaming response instead of EventSource for POST support
|
||||||
const response = await fetch(`${API_BASE}/search`, {
|
const response = await fetch(`${API_BASE}/search`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody)
|
body: JSON.stringify(requestBody),
|
||||||
|
signal: currentSearchController.signal // Add abort signal
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -340,7 +356,13 @@ async function streamOptimalSuits(constraints) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
// Don't treat abort as an error
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
console.log('Search was aborted by user');
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,14 +371,19 @@ async function streamOptimalSuits(constraints) {
|
||||||
// Event handlers
|
// Event handlers
|
||||||
function handleSuitEvent(data) {
|
function handleSuitEvent(data) {
|
||||||
try {
|
try {
|
||||||
|
console.log('NEW handleSuitEvent called with data:', data);
|
||||||
|
|
||||||
// Transform backend suit format to frontend format
|
// Transform backend suit format to frontend format
|
||||||
const transformedSuit = transformSuitData(data);
|
const transformedSuit = transformSuitData(data);
|
||||||
|
console.log('Transformed suit:', transformedSuit);
|
||||||
|
|
||||||
// Insert suit in score-ordered position (highest score first)
|
// Insert suit in score-ordered position (highest score first)
|
||||||
insertSuitInScoreOrder(transformedSuit);
|
const insertIndex = insertSuitInScoreOrder(transformedSuit);
|
||||||
|
console.log('Insert index returned:', insertIndex);
|
||||||
|
|
||||||
// Regenerate entire results display to maintain proper ordering
|
// Insert DOM element at the correct position instead of regenerating everything
|
||||||
regenerateResultsDisplay();
|
insertSuitDOMAtPosition(transformedSuit, insertIndex);
|
||||||
|
console.log('DOM insertion complete');
|
||||||
|
|
||||||
// Update count
|
// Update count
|
||||||
document.getElementById('foundCount').textContent = currentSuits.length;
|
document.getElementById('foundCount').textContent = currentSuits.length;
|
||||||
|
|
@ -364,6 +391,7 @@ async function streamOptimalSuits(constraints) {
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing suit data:', error);
|
console.error('Error processing suit data:', error);
|
||||||
|
console.error('Stack trace:', error.stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -412,6 +440,12 @@ async function streamOptimalSuits(constraints) {
|
||||||
stopButton.addEventListener('click', () => {
|
stopButton.addEventListener('click', () => {
|
||||||
searchStopped = true;
|
searchStopped = true;
|
||||||
|
|
||||||
|
// Actually abort the HTTP request
|
||||||
|
if (currentSearchController) {
|
||||||
|
currentSearchController.abort();
|
||||||
|
currentSearchController = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Update UI to show search was stopped
|
// Update UI to show search was stopped
|
||||||
const loadingDiv = document.querySelector('.loading');
|
const loadingDiv = document.querySelector('.loading');
|
||||||
if (loadingDiv) {
|
if (loadingDiv) {
|
||||||
|
|
@ -467,6 +501,8 @@ function transformSuitData(suit) {
|
||||||
* Insert a suit into the currentSuits array in score-ordered position (highest first)
|
* Insert a suit into the currentSuits array in score-ordered position (highest first)
|
||||||
*/
|
*/
|
||||||
function insertSuitInScoreOrder(suit) {
|
function insertSuitInScoreOrder(suit) {
|
||||||
|
console.log(`Inserting suit with score ${suit.score}. Current suits:`, currentSuits.map(s => s.score));
|
||||||
|
|
||||||
// Find the correct position to insert the suit (highest score first)
|
// Find the correct position to insert the suit (highest score first)
|
||||||
let insertIndex = 0;
|
let insertIndex = 0;
|
||||||
for (let i = 0; i < currentSuits.length; i++) {
|
for (let i = 0; i < currentSuits.length; i++) {
|
||||||
|
|
@ -479,12 +515,17 @@ function insertSuitInScoreOrder(suit) {
|
||||||
|
|
||||||
// Insert the suit at the correct position
|
// Insert the suit at the correct position
|
||||||
currentSuits.splice(insertIndex, 0, suit);
|
currentSuits.splice(insertIndex, 0, suit);
|
||||||
|
|
||||||
|
console.log(`Inserted at index ${insertIndex}. New order:`, currentSuits.map(s => s.score));
|
||||||
|
return insertIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regenerate the entire results display to maintain proper score ordering
|
* Regenerate the entire results display to maintain proper score ordering
|
||||||
*/
|
*/
|
||||||
function regenerateResultsDisplay() {
|
function regenerateResultsDisplay() {
|
||||||
|
console.log('Regenerating display with suits:', currentSuits.map(s => `Score: ${s.score}, ID: ${s.id}`));
|
||||||
|
|
||||||
const streamingResults = document.getElementById('streamingResults');
|
const streamingResults = document.getElementById('streamingResults');
|
||||||
if (!streamingResults) return;
|
if (!streamingResults) return;
|
||||||
|
|
||||||
|
|
@ -526,6 +567,79 @@ function regenerateResultsDisplay() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a suit DOM element at the correct position and update all medal rankings
|
||||||
|
*/
|
||||||
|
function insertSuitDOMAtPosition(suit, insertIndex) {
|
||||||
|
console.log('insertSuitDOMAtPosition called with suit score:', suit.score, 'at index:', insertIndex);
|
||||||
|
|
||||||
|
const streamingResults = document.getElementById('streamingResults');
|
||||||
|
if (!streamingResults) {
|
||||||
|
console.error('streamingResults element not found!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Current DOM children count:', streamingResults.children.length);
|
||||||
|
|
||||||
|
// Create the new suit HTML
|
||||||
|
const scoreClass = getScoreClass(suit.score);
|
||||||
|
const suitHtml = `
|
||||||
|
<div class="suit-item" data-suit-id="${suit.id}">
|
||||||
|
<div class="suit-header">
|
||||||
|
<div class="suit-score ${scoreClass}">
|
||||||
|
🔸 Suit #${suit.id} (Score: ${suit.score})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="suit-stats">
|
||||||
|
${formatSuitStats(suit)}
|
||||||
|
</div>
|
||||||
|
<div class="suit-items">
|
||||||
|
${formatSuitItems(suit.items)}
|
||||||
|
${suit.missing && suit.missing.length > 0 ? `<div class="missing-items">Missing: ${suit.missing.join(', ')}</div>` : ''}
|
||||||
|
${suit.notes && suit.notes.length > 0 ? `<div class="suit-notes">${suit.notes.join(' • ')}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Insert at the correct position
|
||||||
|
const existingSuits = streamingResults.children;
|
||||||
|
if (insertIndex >= existingSuits.length) {
|
||||||
|
// Insert at the end
|
||||||
|
streamingResults.insertAdjacentHTML('beforeend', suitHtml);
|
||||||
|
} else {
|
||||||
|
// Insert before the suit at insertIndex
|
||||||
|
existingSuits[insertIndex].insertAdjacentHTML('beforebegin', suitHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all medal rankings after insertion
|
||||||
|
updateAllMedals();
|
||||||
|
|
||||||
|
// Add click handler for the new suit
|
||||||
|
const newSuitElement = streamingResults.children[insertIndex];
|
||||||
|
newSuitElement.addEventListener('click', function() {
|
||||||
|
const suitId = parseInt(this.dataset.suitId);
|
||||||
|
selectSuit(suitId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update medal rankings for all displayed suits
|
||||||
|
*/
|
||||||
|
function updateAllMedals() {
|
||||||
|
const streamingResults = document.getElementById('streamingResults');
|
||||||
|
if (!streamingResults) return;
|
||||||
|
|
||||||
|
Array.from(streamingResults.children).forEach((suitElement, index) => {
|
||||||
|
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🔸';
|
||||||
|
const scoreElement = suitElement.querySelector('.suit-score');
|
||||||
|
if (scoreElement) {
|
||||||
|
const scoreText = scoreElement.textContent;
|
||||||
|
// Replace the existing medal with the new one
|
||||||
|
scoreElement.textContent = scoreText.replace(/^[🥇🥈🥉🔸]\s*/, medal + ' ');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate suit combinations from available items
|
* Generate suit combinations from available items
|
||||||
* This is a simplified algorithm - the full constraint solver will be more sophisticated
|
* This is a simplified algorithm - the full constraint solver will be more sophisticated
|
||||||
|
|
@ -697,7 +811,7 @@ function getScoreClass(score) {
|
||||||
* Format suit items for display - shows ALL armor slots even if empty
|
* Format suit items for display - shows ALL armor slots even if empty
|
||||||
*/
|
*/
|
||||||
function formatSuitItems(items) {
|
function formatSuitItems(items) {
|
||||||
let html = '';
|
console.log(`[DEBUG] formatSuitItems called with items:`, items);
|
||||||
|
|
||||||
// Define all expected armor/equipment slots in logical order
|
// Define all expected armor/equipment slots in logical order
|
||||||
const allSlots = [
|
const allSlots = [
|
||||||
|
|
@ -710,39 +824,161 @@ function formatSuitItems(items) {
|
||||||
'Shirt', 'Pants'
|
'Shirt', 'Pants'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
console.log(`[DEBUG] allSlots:`, allSlots);
|
||||||
|
|
||||||
|
// Create table structure with header
|
||||||
|
let html = `
|
||||||
|
<div class="suit-items-table">
|
||||||
|
<div class="suit-items-header">
|
||||||
|
<div class="col-slot">Slot</div>
|
||||||
|
<div class="col-character">Character</div>
|
||||||
|
<div class="col-item">Item</div>
|
||||||
|
<div class="col-set">Set</div>
|
||||||
|
<div class="col-spells">Spells</div>
|
||||||
|
<div class="col-armor">Armor</div>
|
||||||
|
<div class="col-ratings">Ratings</div>
|
||||||
|
</div>
|
||||||
|
<div class="suit-items-body">
|
||||||
|
`;
|
||||||
|
|
||||||
allSlots.forEach(slot => {
|
allSlots.forEach(slot => {
|
||||||
const item = items ? items[slot] : null;
|
const item = items ? items[slot] : null;
|
||||||
|
|
||||||
|
// DEBUG: Log all slots and items
|
||||||
|
console.log(`[DEBUG] Processing slot '${slot}', item:`, item);
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
// Item exists in this slot
|
// Item exists in this slot
|
||||||
const needsReducing = isMultiSlotItem(item) ? '<span class="need-reducing">Need Reducing</span>' : '';
|
const character = item.source_character || item.character_name || 'Unknown';
|
||||||
const properties = formatItemProperties(item);
|
const itemName = item.name || 'Unknown Item';
|
||||||
const ratings = formatItemRatings(item);
|
|
||||||
|
// Only show set names for armor items (not jewelry or clothing)
|
||||||
|
const armorSlots = ['Head', 'Chest', 'Upper Arms', 'Lower Arms', 'Hands',
|
||||||
|
'Abdomen', 'Upper Legs', 'Lower Legs', 'Feet'];
|
||||||
|
const setName = (armorSlots.includes(slot) && item.set_name) ? item.set_name : '-';
|
||||||
|
|
||||||
|
const spells = formatItemSpells(item);
|
||||||
|
const armor = formatItemArmor(item);
|
||||||
|
const ratings = formatItemRatingsColumns(item);
|
||||||
|
const needsReducing = isMultiSlotItem(item) ? '<span class="need-reducing">*</span>' : '';
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="suit-item-entry">
|
<div class="suit-item-row">
|
||||||
<strong>${slot}:</strong>
|
<div class="col-slot">${slot}</div>
|
||||||
<span class="item-character">${item.source_character || item.character_name || 'Unknown'}</span> -
|
<div class="col-character">${character}</div>
|
||||||
<span class="item-name">${item.name}</span>
|
<div class="col-item">${itemName}${needsReducing}</div>
|
||||||
${properties ? `<span class="item-properties">(${properties})</span>` : ''}
|
<div class="col-set">${setName}</div>
|
||||||
${ratings ? `<span class="item-ratings">[${ratings}]</span>` : ''}
|
<div class="col-spells">${spells}</div>
|
||||||
${needsReducing}
|
<div class="col-armor">${armor}</div>
|
||||||
|
<div class="col-ratings">${ratings}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
// Empty slot
|
// Empty slot
|
||||||
html += `
|
html += `
|
||||||
<div class="suit-item-entry empty-slot">
|
<div class="suit-item-row empty-slot">
|
||||||
<strong>${slot}:</strong>
|
<div class="col-slot">${slot}</div>
|
||||||
<span class="empty-slot-text">- Empty -</span>
|
<div class="col-character">-</div>
|
||||||
|
<div class="col-item">-</div>
|
||||||
|
<div class="col-set">-</div>
|
||||||
|
<div class="col-spells">-</div>
|
||||||
|
<div class="col-armor">-</div>
|
||||||
|
<div class="col-ratings">-</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format item spells for column display (focus on Legendary spells)
|
||||||
|
*/
|
||||||
|
function formatItemSpells(item) {
|
||||||
|
const spellArray = item.spells || item.spell_names || [];
|
||||||
|
if (!Array.isArray(spellArray) || spellArray.length === 0) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter for important spells (Legendary, Epic)
|
||||||
|
const importantSpells = spellArray.filter(spell =>
|
||||||
|
spell.includes('Legendary') || spell.includes('Epic')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (importantSpells.length === 0) {
|
||||||
|
return `${spellArray.length} spells`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show up to 2 important spells, abbreviate the rest
|
||||||
|
const displaySpells = importantSpells.slice(0, 2);
|
||||||
|
let result = displaySpells.join(', ');
|
||||||
|
|
||||||
|
if (importantSpells.length > 2) {
|
||||||
|
result += ` +${importantSpells.length - 2} more`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format item armor for column display
|
||||||
|
*/
|
||||||
|
function formatItemArmor(item) {
|
||||||
|
if (item.armor_level && item.armor_level > 0) {
|
||||||
|
return item.armor_level.toString();
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format item ratings for column display
|
||||||
|
*/
|
||||||
|
function formatItemRatingsColumns(item) {
|
||||||
|
const ratings = [];
|
||||||
|
|
||||||
|
// Access ratings from the ratings object if available, fallback to direct properties
|
||||||
|
const itemRatings = item.ratings || item;
|
||||||
|
|
||||||
|
// Helper function to get rating value, treating null/undefined/negative as 0
|
||||||
|
function getRatingValue(value) {
|
||||||
|
if (value === null || value === undefined || value < 0) return 0;
|
||||||
|
return Math.round(value); // Round to nearest integer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if this is clothing (shirts/pants) or armor
|
||||||
|
// Check item name patterns since ObjectClass 3 items (clothing) may appear in various slots
|
||||||
|
const itemName = item.name || '';
|
||||||
|
const isClothing = itemName.toLowerCase().includes('shirt') ||
|
||||||
|
itemName.toLowerCase().includes('pants') ||
|
||||||
|
itemName.toLowerCase().includes('breeches') ||
|
||||||
|
itemName.toLowerCase().includes('baggy') ||
|
||||||
|
(item.slot === 'Shirt' || item.slot === 'Pants');
|
||||||
|
|
||||||
|
if (isClothing) {
|
||||||
|
// Clothing: Show DR and DRR
|
||||||
|
const damageRating = getRatingValue(itemRatings.damage_rating);
|
||||||
|
const damageResist = getRatingValue(itemRatings.damage_resist_rating);
|
||||||
|
|
||||||
|
ratings.push(`DR${damageRating}`);
|
||||||
|
ratings.push(`DRR${damageResist}`);
|
||||||
|
} else {
|
||||||
|
// Armor: Show CD and CDR
|
||||||
|
const critDamage = getRatingValue(itemRatings.crit_damage_rating);
|
||||||
|
const critDamageResist = getRatingValue(itemRatings.crit_damage_resist_rating);
|
||||||
|
|
||||||
|
ratings.push(`CD${critDamage}`);
|
||||||
|
ratings.push(`CDR${critDamageResist}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ratings.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if item is multi-slot and needs reducing
|
* Check if item is multi-slot and needs reducing
|
||||||
* Only armor items need reduction - jewelry can naturally go in multiple slots
|
* Only armor items need reduction - jewelry can naturally go in multiple slots
|
||||||
|
|
@ -918,7 +1154,7 @@ function populateVisualSlots(items) {
|
||||||
|
|
||||||
slotElement.innerHTML = `
|
slotElement.innerHTML = `
|
||||||
<div class="slot-item-name">${item.name}</div>
|
<div class="slot-item-name">${item.name}</div>
|
||||||
<div class="slot-item-character">${item.character_name}</div>
|
<div class="slot-item-character">${item.source_character || item.character_name || 'Unknown'}</div>
|
||||||
<div class="slot-item-properties">${formatItemProperties(item)}</div>
|
<div class="slot-item-properties">${formatItemProperties(item)}</div>
|
||||||
${needsReducing}
|
${needsReducing}
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue