feat: Phase 8 - VFX 和 AI 打赏反应系统

- Unity: 添加 VFXManager 实现金币雨和爱心爆炸特效
- Unity: NetworkManager 支持 GiftEffect 事件
- Unity: AgentVisual 支持自定义时长的 SpeechBubble
- Backend: LLMService 支持生成个性化感谢语
- Backend: Engine 统一处理礼物逻辑 (handle_gift)
- Backend: TwitchBot 接入新的礼物处理流程
This commit is contained in:
empty
2026-01-01 21:38:49 +08:00
parent 1f29010de6
commit d1b02b4dfd
12 changed files with 731 additions and 57 deletions

View File

@@ -948,11 +948,17 @@ class GameEngine:
# Use the existing process_comment method to handle commands
await self.process_comment(user, text)
async def process_bits(self, user: str, amount: int) -> None:
"""Process Twitch bits and convert to game gold."""
# 1 Bit = 1 Gold conversion rate
gold_added = amount
async def handle_gift(self, user: str, amount: int, gift_type: str = "bits") -> None:
"""
Handle a gift/donation (bits, subscription, or test).
Args:
user: Name of the donor
amount: Value of the gift
gift_type: Type of gift (bits, test, etc.)
"""
# 1. Add gold to user
gold_added = amount
with get_db_session() as db:
user_obj = self._get_or_create_user(db, user)
user_obj.gold += gold_added
@@ -960,14 +966,38 @@ class GameEngine:
await self._broadcast_event(EventType.USER_UPDATE, {
"user": user,
"gold": user_obj.gold,
"message": f"{user} received {gold_added} gold from {amount} bits!"
"message": f"{user} received {gold_added} gold!"
})
# Also broadcast a special bits event for UI effects
await self._broadcast_event("bits_received", {
"user": user,
"bits": amount,
"gold": gold_added
})
logger.info(f"Processed bits: {user} -> {amount} bits -> {gold_added} gold")
# Check for alive agents for reaction
alive_agents = db.query(Agent).filter(Agent.status == "Alive").all()
agent = random.choice(alive_agents) if alive_agents else None
# Extract data immediately to avoid DetachedInstanceError after session closes
agent_name = agent.name if agent else "Survivor"
agent_personality = agent.personality if agent else "friendly"
# 2. Generate AI gratitude
gratitude = await llm_service.generate_gratitude(
user=user,
amount=amount,
agent_name=agent_name,
agent_personality=agent_personality,
gift_name=gift_type
)
# 3. Broadcast gift effect to Unity
await self._broadcast_event("gift_effect", {
"user": user,
"gift_type": gift_type,
"value": amount,
"message": f"{user} sent {amount} {gift_type}!",
"agent_name": agent_name if agent else None,
"gratitude": gratitude,
"duration": 8.0
})
logger.info(f"Processed gift: {user} -> {amount} {gift_type} (Gratitude: {gratitude})")
async def process_bits(self, user: str, amount: int) -> None:
"""Deprecated: Use handle_gift instead."""
await self.handle_gift(user, amount, "bits")

View File

@@ -56,6 +56,24 @@ MOCK_REACTIONS = {
"My stomach is eating itself...",
"Is this how it ends? Starving on a beach?",
],
"gratitude_arrogant": [
"Finally! A worthy tribute! {user}, you understand greatness!",
"About time someone recognized my value! Thanks, {user}!",
"Hmph, {user} knows quality when they see it. Much appreciated!",
"A gift for ME? Well, obviously. Thank you, {user}!",
],
"gratitude_humble": [
"Oh my gosh, {user}! You're too kind! Thank you so much!",
"Wow, {user}, I don't deserve this! You're amazing!",
"*tears up* {user}... this means the world to me!",
"Thank you, thank you {user}! You're the best!",
],
"gratitude_neutral": [
"Hey, thanks {user}! That's really generous of you!",
"Wow, {user}! Thank you so much for the support!",
"Appreciate it, {user}! You're awesome!",
"{user}, you're a legend! Thank you!",
],
}
# Default model configuration
@@ -461,6 +479,92 @@ class LLMService:
logger.error(f"LLM API error for social interaction: {e}")
return f"{initiator_name}: ...\n{target_name}: ..."
async def generate_gratitude(
self,
user: str,
amount: int,
agent_name: str = "Survivor",
agent_personality: str = "friendly",
gift_name: str = "bits"
) -> str:
"""
Generate a special gratitude response for donations/gifts.
Args:
user: Name of the user who gave the gift
amount: Amount of the gift
agent_name: Name of the agent (optional)
agent_personality: Personality of the agent (optional)
gift_name: Type of gift (bits, subscription, etc.)
Returns:
An excited, grateful response from the agent
"""
personality = agent_personality.lower() if agent_personality else "friendly"
if self._mock_mode:
if "arrogant" in personality or "proud" in personality:
responses = MOCK_REACTIONS.get("gratitude_arrogant", [])
elif "humble" in personality or "shy" in personality or "kind" in personality:
responses = MOCK_REACTIONS.get("gratitude_humble", [])
else:
responses = MOCK_REACTIONS.get("gratitude_neutral", [])
if responses:
return random.choice(responses).format(user=user, amount=amount)
return f"Thank you so much, {user}! You're amazing!"
try:
# Customize tone based on personality
if "arrogant" in personality or "proud" in personality:
tone_instruction = (
"You are somewhat arrogant but still grateful. "
"React with confident excitement, like 'Finally, a worthy tribute!' "
"but still thank them."
)
elif "humble" in personality or "shy" in personality:
tone_instruction = (
"You are humble and easily moved. "
"React with overwhelming gratitude, maybe even get teary-eyed."
)
else:
tone_instruction = (
"React with genuine excitement and gratitude."
)
system_prompt = (
f"You are {agent_name}, a survivor on a deserted island. "
f"Personality: {personality if personality else 'friendly'}. "
f"A wealthy patron named {user} just gave you {amount} {gift_name}! "
f"{tone_instruction} "
f"Respond with extreme excitement and gratitude (max 15 words). "
f"Keep it fun and energetic!"
)
kwargs = {
"model": self._model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"{user} just gave you {amount} {gift_name}! React!"}
],
"max_tokens": 40,
"temperature": 0.95,
}
if self._api_base:
kwargs["api_base"] = self._api_base
if self._api_key and not self._api_key_header:
kwargs["api_key"] = self._api_key
if self._extra_headers:
kwargs["extra_headers"] = self._extra_headers
response = await self._acompletion(**kwargs)
return response.choices[0].message.content.strip()
except Exception as e:
logger.error(f"LLM API error for gratitude: {e}")
return f"Wow, thank you so much {user}! You're amazing!"
# Global instance for easy import
llm_service = LLMService()

View File

@@ -140,6 +140,13 @@ async def websocket_endpoint(websocket: WebSocket):
text = message.payload.get("message", "")
await engine.process_comment(user, text)
# Handle test gift action
elif message.action == "test_gift":
user = message.payload.get("user", "TestUser")
bits = int(message.payload.get("bits", 100))
# Trigger handle_gift in engine
await engine.handle_gift(user, bits, "bits")
except WebSocketDisconnect:
manager.disconnect(websocket)
except Exception as e:

View File

@@ -7,6 +7,7 @@ Compatible with twitchio 2.x (IRC-based)
import os
import logging
import random
from typing import TYPE_CHECKING
from twitchio.ext import commands
@@ -83,20 +84,19 @@ class TwitchBot(commands.Bot):
if hasattr(message, 'tags') and message.tags:
bits = message.tags.get('bits')
if bits:
try:
bits_amount = int(bits)
logger.info(f"Received {bits_amount} bits from {username}")
await self._game_engine.process_bits(username, bits_amount)
await self._handle_bits(username, int(bits))
# Send special gift effect to Unity
await self._game_engine._broadcast_event("gift_effect", {
"type": "gift_effect",
"user": username,
"value": bits_amount,
"message": f"{username} cheered {bits_amount} bits!"
})
except (ValueError, TypeError) as e:
logger.error(f"Error parsing bits amount: {e}")
async def _handle_bits(self, username: str, bits_amount: int):
"""
Handle bits donation.
Delegates to game engine's unified gift handling.
"""
try:
logger.info(f"Received {bits_amount} bits from {username}")
await self._game_engine.handle_gift(username, bits_amount, "bits")
except Exception as e:
logger.error(f"Error handling bits: {e}")
async def event_command_error(self, context, error):
"""Called when a command error occurs."""
@@ -110,3 +110,4 @@ class TwitchBot(commands.Bot):
logger.error(f"Twitch bot error: {error}")
if data:
logger.debug(f"Error data: {data}")