New apps Added
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user