"""Persistent Kirara state that decays only during an active live session.""" from __future__ import annotations import json import logging import re import time import uuid from pathlib import Path EMOTION_DELTAS = { "开心": 1, "很开心": 3, "平静": 0, "生气": -1, "很生气": -3, } EMOTION_PREFIX = re.compile(r"^\s*\[(很开心|很生气|开心|平静|生气)\]\s*") GIFT_RECOVERY = { "饭团": (5, 0), "薯条": (5, 0), "人气票": (0, 5), "粉丝团灯牌": (0, 5), "灯牌": (0, 5), # 直播事件中常用的简写 } def clamp(value: int | float) -> int: return max(0, min(100, int(value))) def gift_recovery(gift_name: str, quantity: int = 1) -> tuple[int, int]: """Return nominal stamina/cleanliness points for a gift batch.""" stamina, cleanliness = GIFT_RECOVERY.get(str(gift_name).strip(), (0, 0)) count = max(0, int(quantity)) return stamina * count, cleanliness * count def split_emotion(reply: str) -> tuple[str, str | None]: """Return the spoken reply and its recognized leading emotion tag.""" match = EMOTION_PREFIX.match(reply) if not match: return reply, None return reply[match.end():].strip(), match.group(1) class PetState: def __init__(self, path: str | Path): self.path = Path(path) self.values = {"stamina": 100, "cleanliness": 100, "mood": 100} # Short-lived events let open frontends play each AI mood reaction once. self.recent_emotions: list[dict] = [] self.live_seconds = 0.0 # A restart must never count the offline gap as live time. self.live_active = False self.updated_at = time.time() try: data = json.loads(self.path.read_text(encoding="utf-8")) self.values = {key: clamp(data.get(key, 100)) for key in self.values} self.live_seconds = max(0.0, min(3599.999, float(data.get("live_seconds", 0)))) self.updated_at = min(self.updated_at, float(data.get("updated_at", self.updated_at))) except (OSError, ValueError, TypeError): pass self._saved_at = self.updated_at def _save(self) -> None: """持久化。保存失败只降级为日志,绝不影响礼物/直播主流程。""" try: self.path.parent.mkdir(parents=True, exist_ok=True) payload = json.dumps({**self.values, "live_seconds": self.live_seconds, "updated_at": self.updated_at}, ensure_ascii=False) try: pending = self.path.with_suffix(self.path.suffix + ".tmp") pending.write_text(payload, encoding="utf-8") pending.replace(self.path) except OSError: # 原子替换被拒(杀软锁定/权限)时退回直接覆写 self.path.write_text(payload, encoding="utf-8") Path(str(self.path) + ".tmp").unlink(missing_ok=True) self._saved_at = self.updated_at except Exception: logging.getLogger("pet_state").warning( "[桌宠] 状态保存失败(不影响礼物处理): %s", self.path, exc_info=True) def _record_emotion(self, delta: int, now: float) -> None: if not delta: return self.recent_emotions.append({ "id": uuid.uuid4().hex, "ts": now, "delta": delta, }) del self.recent_emotions[:-32] def sync(self, now: float | None = None) -> dict[str, int]: now = time.time() if now is None else now if now > self.updated_at: added = now - self.updated_at if self.live_active else 0.0 self.live_seconds += added hours = int(self.live_seconds // 3600) self.live_seconds %= 3600 for _ in range(hours): self.values["stamina"] = clamp(self.values["stamina"] - 10) self.values["cleanliness"] = clamp(self.values["cleanliness"] - 10) if self.values["stamina"] < 40 and self.values["cleanliness"] < 40: self.values["mood"] = clamp(self.values["mood"] - 5) elif self.values["stamina"] > 80 and self.values["cleanliness"] > 80: self.values["mood"] = clamp(self.values["mood"] + 3) self._record_emotion(3, now) self.updated_at = now if hours or (added and now - self._saved_at >= 60): self._save() return dict(self.values) def set_live_active(self, active: bool, now: float | None = None) -> dict[str, int]: """Settle the previous session state before switching at this instant.""" now = time.time() if now is None else now self.sync(now) if self.live_active != active: self.live_active = active self._save() return dict(self.values) def apply_emotion(self, emotion: str) -> dict[str, int]: self.sync() if emotion in EMOTION_DELTAS: delta = EMOTION_DELTAS[emotion] self.values["mood"] = clamp(self.values["mood"] + delta) self._record_emotion(delta, time.time()) self._save() return dict(self.values) def restore_gift(self, stamina: int = 0, cleanliness: int = 0) -> dict[str, int]: """Apply gift recovery while keeping stored values within 0–100.""" self.sync() if stamina or cleanliness: self.values["stamina"] = clamp(self.values["stamina"] + stamina) self.values["cleanliness"] = clamp(self.values["cleanliness"] + cleanliness) self._save() return dict(self.values)