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:
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user