Add skill training advisor with recipe data and one-click goals.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+120
@@ -0,0 +1,120 @@
|
||||
"""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 1
|
||||
return max(1, 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],
|
||||
}
|
||||
Reference in New Issue
Block a user