Files
Live-streaming/app/pet_state.py
T

143 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)