Some checks failed
Release / release (push) Has been cancelled
Full-stack dashboard for controlling, automating, and analyzing Artifacts MMO characters via the game's HTTP API. Backend (FastAPI): - Async Artifacts API client with rate limiting and retry - 6 automation strategies (combat, gathering, crafting, trading, task, leveling) - Automation engine with runner, manager, cooldown tracker, pathfinder - WebSocket relay (game server -> frontend) - Game data cache, character snapshots, price history, analytics - 9 API routers, 7 database tables, 3 Alembic migrations - 108 unit tests Frontend (Next.js 15 + shadcn/ui): - Live character dashboard with HP/XP bars and cooldowns - Character detail with stats, equipment, inventory, skills, manual actions - Automation management with live log streaming - Interactive canvas map with content-type coloring and zoom/pan - Bank management, Grand Exchange with price charts - Events, logs, analytics pages with Recharts - WebSocket auto-reconnect with query cache invalidation - Settings page, error boundaries, dark theme Infrastructure: - Docker Compose (dev + prod) - GitHub Actions CI/CD - Documentation (Architecture, Automation, Deployment, API)
157 lines
5.2 KiB
Python
157 lines
5.2 KiB
Python
"""Equipment optimizer for suggesting gear improvements."""
|
|
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
|
|
from app.schemas.game import CharacterSchema, ItemSchema
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Equipment slot names and the item types that can go in them
|
|
_SLOT_TYPE_MAP: dict[str, list[str]] = {
|
|
"weapon_slot": ["weapon"],
|
|
"shield_slot": ["shield"],
|
|
"helmet_slot": ["helmet"],
|
|
"body_armor_slot": ["body_armor"],
|
|
"leg_armor_slot": ["leg_armor"],
|
|
"boots_slot": ["boots"],
|
|
"ring1_slot": ["ring"],
|
|
"ring2_slot": ["ring"],
|
|
"amulet_slot": ["amulet"],
|
|
"artifact1_slot": ["artifact"],
|
|
"artifact2_slot": ["artifact"],
|
|
"artifact3_slot": ["artifact"],
|
|
}
|
|
|
|
# Effect names that contribute to the equipment score
|
|
_ATTACK_EFFECTS = {"attack_fire", "attack_earth", "attack_water", "attack_air"}
|
|
_DEFENSE_EFFECTS = {"res_fire", "res_earth", "res_water", "res_air"}
|
|
_HP_EFFECTS = {"hp"}
|
|
_DAMAGE_EFFECTS = {"dmg_fire", "dmg_earth", "dmg_water", "dmg_air"}
|
|
|
|
|
|
@dataclass
|
|
class EquipmentSuggestion:
|
|
"""A suggestion to equip a different item in a slot."""
|
|
|
|
slot: str
|
|
current_item_code: str
|
|
suggested_item_code: str
|
|
current_score: float
|
|
suggested_score: float
|
|
improvement: float
|
|
reason: str
|
|
|
|
|
|
@dataclass
|
|
class EquipmentAnalysis:
|
|
"""Full analysis of a character's equipment vs available items."""
|
|
|
|
suggestions: list[EquipmentSuggestion] = field(default_factory=list)
|
|
total_current_score: float = 0.0
|
|
total_best_score: float = 0.0
|
|
|
|
|
|
class EquipmentOptimizer:
|
|
"""Analyzes character equipment and suggests improvements.
|
|
|
|
Uses a simple scoring system: sum of all attack + defense + HP stats
|
|
from item effects.
|
|
"""
|
|
|
|
def suggest_equipment(
|
|
self,
|
|
character: CharacterSchema,
|
|
available_items: list[ItemSchema],
|
|
) -> EquipmentAnalysis:
|
|
"""Analyze the character's current equipment and suggest improvements.
|
|
|
|
Parameters
|
|
----------
|
|
character:
|
|
The character to analyze.
|
|
available_items:
|
|
Items available to the character (e.g. from bank).
|
|
|
|
Returns
|
|
-------
|
|
EquipmentAnalysis with suggestions for each slot where a better item exists.
|
|
"""
|
|
# Build a lookup of item code -> ItemSchema
|
|
item_lookup: dict[str, ItemSchema] = {
|
|
item.code: item for item in available_items
|
|
}
|
|
|
|
analysis = EquipmentAnalysis()
|
|
|
|
for slot, valid_types in _SLOT_TYPE_MAP.items():
|
|
current_code = getattr(character, slot, "")
|
|
current_item = item_lookup.get(current_code) if current_code else None
|
|
current_score = self._score_item(current_item) if current_item else 0.0
|
|
|
|
# Find the best available item for this slot
|
|
candidates = [
|
|
item
|
|
for item in available_items
|
|
if item.type in valid_types and item.level <= character.level
|
|
]
|
|
|
|
if not candidates:
|
|
analysis.total_current_score += current_score
|
|
analysis.total_best_score += current_score
|
|
continue
|
|
|
|
best_candidate = max(candidates, key=lambda i: self._score_item(i))
|
|
best_score = self._score_item(best_candidate)
|
|
|
|
analysis.total_current_score += current_score
|
|
analysis.total_best_score += max(current_score, best_score)
|
|
|
|
# Only suggest if there's an actual improvement
|
|
improvement = best_score - current_score
|
|
if improvement > 0 and best_candidate.code != current_code:
|
|
suggestion = EquipmentSuggestion(
|
|
slot=slot,
|
|
current_item_code=current_code or "(empty)",
|
|
suggested_item_code=best_candidate.code,
|
|
current_score=current_score,
|
|
suggested_score=best_score,
|
|
improvement=improvement,
|
|
reason=(
|
|
f"Replace {current_code or 'empty'} "
|
|
f"(score {current_score:.1f}) with {best_candidate.code} "
|
|
f"(score {best_score:.1f}, +{improvement:.1f})"
|
|
),
|
|
)
|
|
analysis.suggestions.append(suggestion)
|
|
|
|
# Sort suggestions by improvement descending
|
|
analysis.suggestions.sort(key=lambda s: s.improvement, reverse=True)
|
|
|
|
return analysis
|
|
|
|
@staticmethod
|
|
def _score_item(item: ItemSchema | None) -> float:
|
|
"""Calculate a simple composite score for an item.
|
|
|
|
Score = sum of all attack effects + defense effects + HP + damage effects.
|
|
"""
|
|
if item is None:
|
|
return 0.0
|
|
|
|
score = 0.0
|
|
for effect in item.effects:
|
|
name = effect.name.lower()
|
|
if name in _ATTACK_EFFECTS:
|
|
score += effect.value
|
|
elif name in _DEFENSE_EFFECTS:
|
|
score += effect.value
|
|
elif name in _HP_EFFECTS:
|
|
score += effect.value * 0.5 # HP is weighted less than raw stats
|
|
elif name in _DAMAGE_EFFECTS:
|
|
score += effect.value * 1.5 # Damage bonuses are weighted higher
|
|
|
|
# Small bonus for higher-level items (tie-breaker)
|
|
score += item.level * 0.1
|
|
|
|
return score
|