Files
Home-Assistant/custom_components/taskmate/coord_avatars.py
T
2026-07-08 10:43:39 -04:00

116 lines
5.1 KiB
Python

"""Avatar unlockables mixin for TaskMateCoordinator.
Parents define a catalog of avatars, each gated behind a requirement (a level,
a lifetime-points total, or a best-streak length — or ``free`` for always
available). Children unlock avatars by hitting those milestones and can switch
to any avatar they've unlocked; parents can set any catalogue avatar.
"""
from __future__ import annotations
import logging
_LOGGER = logging.getLogger(__name__)
# Shipped defaults so the feature is useful out of the box. Parents can replace
# the whole list from the panel.
DEFAULT_AVATAR_CATALOG: list[dict] = [
{"id": "starter", "label": "Starter", "icon": "mdi:account-circle", "unlock_type": "free", "unlock_value": 0},
{"id": "rocket", "label": "Rocket", "icon": "mdi:rocket-launch", "unlock_type": "level", "unlock_value": 3},
{"id": "robot", "label": "Robot", "icon": "mdi:robot-happy", "unlock_type": "level", "unlock_value": 5},
{"id": "ninja", "label": "Ninja", "icon": "mdi:ninja", "unlock_type": "level", "unlock_value": 10},
{"id": "crown", "label": "Royalty", "icon": "mdi:crown", "unlock_type": "points", "unlock_value": 500},
{"id": "trophy", "label": "Champion", "icon": "mdi:trophy", "unlock_type": "points", "unlock_value": 1000},
{"id": "fire", "label": "On Fire", "icon": "mdi:fire", "unlock_type": "streak", "unlock_value": 7},
{"id": "diamond", "label": "Diamond", "icon": "mdi:diamond-stone", "unlock_type": "streak", "unlock_value": 30},
]
class AvatarsMixin:
"""Mixin providing the avatar catalogue and unlock/selection logic."""
def avatar_catalog(self) -> list[dict]:
"""The configured avatar catalogue (falls back to the shipped default)."""
raw = self.storage.get_setting("avatar_catalog", None)
if isinstance(raw, list) and raw:
return [a for a in raw if isinstance(a, dict) and a.get("icon")]
return list(DEFAULT_AVATAR_CATALOG)
def _avatar_unlocked(self, entry: dict, child) -> bool:
kind = entry.get("unlock_type", "free")
try:
value = int(entry.get("unlock_value", 0) or 0)
except (ValueError, TypeError):
value = 0
if kind == "free":
return True
if kind == "level":
return int(self.level_info(child)["level"]) >= value
if kind == "points":
return int(getattr(child, "total_points_earned", 0) or 0) >= value
if kind == "streak":
return int(getattr(child, "best_streak", 0) or 0) >= value
return False
def avatar_options_for_child(self, child) -> list[dict]:
"""Catalogue annotated with per-child unlock state (for sensors/cards)."""
out: list[dict] = []
for entry in self.avatar_catalog():
kind = entry.get("unlock_type", "free")
value = entry.get("unlock_value", 0)
if kind == "free":
req = ""
elif kind == "level":
req = f"Level {value}"
elif kind == "points":
req = f"{value} earned"
elif kind == "streak":
req = f"{value}-day streak"
else:
req = ""
out.append({
"id": entry.get("id", entry.get("icon")),
"label": entry.get("label", ""),
"icon": entry.get("icon"),
"unlocked": self._avatar_unlocked(entry, child),
"requirement": req,
})
return out
async def async_update_avatar_catalog(self, catalog: list[dict]) -> None:
"""Replace the avatar catalogue (admin)."""
cleaned: list[dict] = []
for a in catalog:
icon = (a.get("icon") or "").strip()
if not icon:
continue
cleaned.append({
"id": (a.get("id") or icon).strip(),
"label": (a.get("label") or "").strip(),
"icon": icon,
"unlock_type": a.get("unlock_type", "free"),
"unlock_value": int(a.get("unlock_value", 0) or 0),
})
self.storage.set_setting("avatar_catalog", cleaned)
await self.storage.async_save()
await self.async_refresh()
async def async_set_avatar(self, child_id: str, icon: str, enforce_unlock: bool = True) -> None:
"""Set a child's avatar.
When ``enforce_unlock`` (child self-service) the icon must be an
unlocked catalogue avatar; parents may set any catalogue avatar.
"""
child = self.get_child(child_id)
if not child:
raise ValueError(f"Child {child_id} not found")
catalog = self.avatar_catalog()
entry = next((a for a in catalog if a.get("icon") == icon or a.get("id") == icon), None)
if not entry:
raise ValueError("That avatar is not in the catalogue")
if enforce_unlock and not self._avatar_unlocked(entry, child):
raise ValueError("That avatar is not unlocked yet")
child.avatar = entry.get("icon")
self.storage.update_child(child)
await self.storage.async_save()
await self.async_refresh()