Files
Idle-Fantasy-Save-Viewer/advisor.py
T

121 lines
3.9 KiB
Python

"""Skill training recommendations from game recipe data + save state."""
from __future__ import annotations
from typing import Any
from game_data import format_display_name, manifest, recipes_for_skill, supported_advisor_skills, xp_per_minute
def _inventory_map(snapshot: dict[str, Any]) -> dict[str, int]:
inv: dict[str, int] = {}
for item in snapshot.get("inventory") or []:
key = item.get("key")
if key:
inv[str(key)] = int(item.get("qty") or 0)
return inv
def _skill_state(snapshot: dict[str, Any], skill_key: str) -> dict[str, Any] | None:
for skill in snapshot.get("skills") or []:
if skill.get("key") == skill_key:
return skill
return None
def missing_materials(
materials: dict[str, int],
inventory: dict[str, int],
) -> dict[str, int]:
missing: dict[str, int] = {}
for item_key, needed in materials.items():
need = int(needed)
have = inventory.get(item_key, 0)
if have < need:
missing[item_key] = need - have
return missing
def crafts_to_next_level(xp_remaining: int, xp_per_item: float) -> int:
if xp_remaining <= 0 or xp_per_item <= 0:
return 0
return int((xp_remaining + xp_per_item - 1) // xp_per_item)
def advise_skill(
skill_key: str,
snapshot: dict[str, Any],
*,
limit: int = 8,
) -> dict[str, Any]:
skill_key = skill_key.strip().lower()
recipes = recipes_for_skill(skill_key)
if not recipes:
return {
"skill_key": skill_key,
"supported": False,
"error": "unsupported_skill",
"supported_skills": supported_advisor_skills(),
}
skill = _skill_state(snapshot, skill_key)
if not skill:
return {
"skill_key": skill_key,
"supported": True,
"error": "skill_not_in_save",
}
level = int(skill.get("level") or 1)
xp_in_level = int(skill.get("xp_in_level") or 0)
xp_needed = int(skill.get("xp_needed") or 1)
xp_remaining = max(0, xp_needed - xp_in_level)
inventory = _inventory_map(snapshot)
candidates: list[dict[str, Any]] = []
for activity_key, recipe in recipes.items():
level_required = int(recipe.get("level_required") or 1)
if level_required > level:
continue
materials = {str(k): int(v) for k, v in (recipe.get("materials") or {}).items()}
missing = missing_materials(materials, inventory)
rate = xp_per_minute(recipe)
if rate <= 0:
continue
eta_minutes = int((xp_remaining + rate - 1) // rate) if xp_remaining > 0 else 0
craft_count = crafts_to_next_level(xp_remaining, float(recipe.get("xp_per_item") or 0))
candidates.append({
"activity_key": activity_key,
"display_name": format_display_name(recipe, activity_key),
"type": recipe.get("type"),
"level_required": level_required,
"xp_per_item": float(recipe.get("xp_per_item") or 0),
"time_per_item": int(recipe.get("time_per_item") or 60),
"xp_per_minute": round(rate, 2),
"materials": materials,
"can_craft": not missing,
"missing_materials": missing,
"eta_minutes_to_level": eta_minutes,
"crafts_to_next_level": craft_count,
})
candidates.sort(
key=lambda row: (
0 if row["can_craft"] else 1,
-row["xp_per_minute"],
row["level_required"],
),
)
meta = manifest()
return {
"skill_key": skill_key,
"skill_name": skill.get("name") or skill_key.replace("_", " ").title(),
"skill_level": level,
"xp_remaining_in_level": xp_remaining,
"supported": True,
"game_data_sha": meta.get("source_sha"),
"game_data_synced_at": meta.get("synced_at"),
"recommendations": candidates[:limit],
}