剧本杀直播优化:商店细节、弹幕投票、阵营对抗

a2515135414小时前未分类2

互动系统详细设计方案

一、道具商店系统

1. 商店架构设计

┌─────────────────────────────────────────────────────────────┐
│                      道具商店系统架构                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐   │
│  │  礼物接收   │ ──→ │  积分转换   │ ──→ │  商店余额   │   │
│  └─────────────┘     └─────────────┘     └─────────────┘   │
│         │                                       │           │
│         ↓                                       ↓           │
│  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐   │
│  │  礼物价值表 │     │  道具库存   │ ←── │  购买道具   │   │
│  └─────────────┘     └─────────────┘     └─────────────┘   │
│                            │                               │
│                            ↓                               │
│                    ┌─────────────┐                         │
│                    │  道具效果   │                         │
│                    │  触发系统   │                         │
│                    └─────────────┘                         │
└─────────────────────────────────────────────────────────────┘

2. 积分与货币体系

积分获取方式

获取途径积分数量说明
送小心心(1抖币)1积分基础互动
送玫瑰(10抖币)12积分略有加成
送啤酒(99抖币)120积分中等礼物
送嘉年华(3000抖币)4000积分大额礼物有额外加成
发弹幕参与投票0.5积分/次鼓励互动
连续观看10分钟5积分留存奖励
正确猜中凶手50积分参与奖励

积分转换代码

pythonDownloadCopy codeclass PointSystem:
    def __init__(self):        # 礼物价值映射表(抖音)
        self.gift_values = {
            "小心心": {"price": 1, "points": 1},
            "玫瑰": {"price": 10, "points": 12},
            "抖音": {"price": 10, "points": 12},
            "棒棒糖": {"price": 9, "points": 10},
            "啤酒": {"price": 99, "points": 120},
            "仙女棒": {"price": 199, "points": 250},
            "浪漫烟花": {"price": 599, "points": 750},
            "私人飞机": {"price": 3000, "points": 4000},
            "嘉年华": {"price": 3000, "points": 4000},
            "城堡": {"price": 10000, "points": 15000},
        }        
        # 用户积分存储
        self.user_points = {}        
        # 用户道具背包
        self.user_inventory = {}
    
    def add_gift_points(self, user_id: str, gift_name: str, count: int = 1):
        """添加礼物积分"""
        if gift_name in self.gift_values:
            points = self.gift_values[gift_name]["points"] * count
            
            if user_id not in self.user_points:
                self.user_points[user_id] = 0
            
            self.user_points[user_id] += points
            
            return {
                "success": True,
                "added_points": points,
                "total_points": self.user_points[user_id]
            }
        return {"success": False, "message": "未知礼物"}
    
    def add_interaction_points(self, user_id: str, action: str):
        """添加互动积分"""
        action_points = {
            "vote": 0.5,
            "watch_10min": 5,
            "correct_guess": 50,
            "first_chat": 2
        }
        
        if action in action_points:
            if user_id not in self.user_points:
                self.user_points[user_id] = 0
            
            self.user_points[user_id] += action_points[action]
    
    def get_balance(self, user_id: str) -> float:
        """查询积分余额"""
        return self.user_points.get(user_id, 0)

3. 道具商店设计

道具分类与效果

pythonDownloadCopy codefrom enum import Enum
from dataclasses import dataclass
from typing import Optional, List

class ItemCategory(Enum):
    SURVIVAL = "求生道具"      # 帮助主角
    HORROR = "恐怖道具"        # 增加难度
    INFO = "情报道具"          # 获取信息
    SPECIAL = "特殊道具"       # 特殊效果class ItemRarity(Enum):
    COMMON = "普通"           # 白色
    RARE = "稀有"             # 蓝色
    EPIC = "史诗"             # 紫色
    LEGENDARY = "传说"        # 金色@dataclass
class ShopItem:
    id: str
    name: str
    description: str
    category: ItemCategory
    rarity: ItemRarity
    price: int                      # 积分价格
    stock: int                      # 库存限制(-1为无限)
    cooldown: int                   # 使用冷却(秒)
    effect_duration: int            # 效果持续时间(秒)
    icon: str                       # 图标文件名
    effect_type: str                # 效果类型
    effect_value: dict              # 效果参数# 完整道具列表SHOP_ITEMS = {    # ============ 求生道具 ============
    "flashlight": ShopItem(
        id="flashlight",
        name="🔦 手电筒",
        description="照亮黑暗区域,可能发现隐藏线索",
        category=ItemCategory.SURVIVAL,
        rarity=ItemRarity.COMMON,
        price=30,
        stock=-1,
        cooldown=60,
        effect_duration=30,
        icon="flashlight.png",
        effect_type="reveal_area",
        effect_value={"reveal_chance": 0.7}
    ),
    
    "talisman": ShopItem(
        id="talisman",
        name="🧿 护身符",
        description="抵挡一次惊吓事件,保护主角安全",
        category=ItemCategory.SURVIVAL,
        rarity=ItemRarity.RARE,
        price=100,
        stock=5,  # 每场限5个
        cooldown=120,
        effect_duration=0,
        icon="talisman.png",
        effect_type="block_jumpscare",
        effect_value={"block_count": 1}
    ),
    
    "mirror_cover": ShopItem(
        id="mirror_cover",
        name="🪞 镜子遮布",
        description="暂时遮住镜子,阻止镜中人窥视",
        category=ItemCategory.SURVIVAL,
        rarity=ItemRarity.RARE,
        price=150,
        stock=3,
        cooldown=180,
        effect_duration=60,
        icon="mirror_cover.png",
        effect_type="disable_mirror",
        effect_value={"duration": 60}
    ),
    
    "holy_water": ShopItem(
        id="holy_water",
        name="💧 圣水",
        description="净化一个房间,降低恐怖事件发生概率",
        category=ItemCategory.SURVIVAL,
        rarity=ItemRarity.EPIC,
        price=300,
        stock=2,
        cooldown=300,
        effect_duration=180,
        icon="holy_water.png",
        effect_type="reduce_horror",
        effect_value={"reduction": 0.5, "duration": 180}
    ),
    
    "resurrection_cross": ShopItem(
        id="resurrection_cross",
        name="✝️ 复活十字架",
        description="如果主角即将死亡,自动触发保护(仅一次)",
        category=ItemCategory.SURVIVAL,
        rarity=ItemRarity.LEGENDARY,
        price=800,
        stock=1,
        cooldown=0,
        effect_duration=0,
        icon="cross.png",
        effect_type="auto_revive",
        effect_value={"revive_count": 1}
    ),    
    # ============ 恐怖道具 ============
    "cursed_doll": ShopItem(
        id="cursed_doll",
        name="🎎 诅咒人偶",
        description="召唤一个诅咒人偶,增加恐怖氛围",
        category=ItemCategory.HORROR,
        rarity=ItemRarity.COMMON,
        price=20,
        stock=-1,
        cooldown=30,
        effect_duration=0,
        icon="cursed_doll.png",
        effect_type="add_horror_element",
        effect_value={"element": "cursed_doll", "fear_add": 5}
    ),
    
    "mirror_crack": ShopItem(
        id="mirror_crack",
        name="💔 裂镜之力",
        description="让镜子出现裂痕,镜中人变得更加狂暴",
        category=ItemCategory.HORROR,
        rarity=ItemRarity.RARE,
        price=80,
        stock=5,
        cooldown=120,
        effect_duration=120,
        icon="crack.png",
        effect_type="enhance_mirror_person",
        effect_value={"aggression": 1.5, "duration": 120}
    ),
    
    "lights_out": ShopItem(
        id="lights_out",
        name="🌑 熄灯",
        description="强制关闭所有灯光30秒",
        category=ItemCategory.HORROR,
        rarity=ItemRarity.RARE,
        price=100,
        stock=3,
        cooldown=180,
        effect_duration=30,
        icon="lights_out.png",
        effect_type="lights_off",
        effect_value={"duration": 30}
    ),
    
    "summon_ghost": ShopItem(
        id="summon_ghost",
        name="👻 召唤幽灵",
        description="召唤一个随机幽灵出现在场景中",
        category=ItemCategory.HORROR,
        rarity=ItemRarity.EPIC,
        price=200,
        stock=3,
        cooldown=240,
        effect_duration=60,
        icon="ghost.png",
        effect_type="spawn_ghost",
        effect_value={"ghost_type": "random", "duration": 60}
    ),
    
    "death_timer": ShopItem(
        id="death_timer",
        name="⏰ 死亡倒计时",
        description="触发一个60秒的死亡倒计时,必须在时间内完成任务",
        category=ItemCategory.HORROR,
        rarity=ItemRarity.LEGENDARY,
        price=500,
        stock=1,
        cooldown=0,
        effect_duration=60,
        icon="death_timer.png",
        effect_type="death_countdown",
        effect_value={"countdown": 60}
    ),    
    # ============ 情报道具 ============
    "ouija_board": ShopItem(
        id="ouija_board",
        name="🔮 通灵板",
        description="向灵体询问一个问题,获得模糊的提示",
        category=ItemCategory.INFO,
        rarity=ItemRarity.RARE,
        price=120,
        stock=3,
        cooldown=180,
        effect_duration=0,
        icon="ouija.png",
        effect_type="get_hint",
        effect_value={"hint_level": "vague"}
    ),
    
    "old_diary": ShopItem(
        id="old_diary",
        name="📔 旧日记本",
        description="随机获得一条背景故事线索",
        category=ItemCategory.INFO,
        rarity=ItemRarity.COMMON,
        price=50,
        stock=-1,
        cooldown=120,
        effect_duration=0,
        icon="diary.png",
        effect_type="get_clue",
        effect_value={"clue_type": "background"}
    ),
    
    "spirit_detector": ShopItem(
        id="spirit_detector",
        name="📡 灵体探测器",
        description="显示当前房间的危险等级",
        category=ItemCategory.INFO,
        rarity=ItemRarity.RARE,
        price=80,
        stock=5,
        cooldown=90,
        effect_duration=30,
        icon="detector.png",
        effect_type="show_danger",
        effect_value={"duration": 30}
    ),
    
    "truth_eye": ShopItem(
        id="truth_eye",
        name="👁️ 真相之眼",
        description="直接揭示一条关键线索",
        category=ItemCategory.INFO,
        rarity=ItemRarity.EPIC,
        price=400,
        stock=2,
        cooldown=300,
        effect_duration=0,
        icon="eye.png",
        effect_type="reveal_key_clue",
        effect_value={"clue_type": "key"}
    ),    
    # ============ 特殊道具 ============
    "time_rewind": ShopItem(
        id="time_rewind",
        name="⏪ 时间回溯",
        description="回到上一个选择点,重新做出决定",
        category=ItemCategory.SPECIAL,
        rarity=ItemRarity.LEGENDARY,
        price=1000,
        stock=1,
        cooldown=0,
        effect_duration=0,
        icon="time_rewind.png",
        effect_type="rewind_choice",
        effect_value={"rewind_steps": 1}
    ),
    
    "fate_dice": ShopItem(
        id="fate_dice",
        name="🎲 命运骰子",
        description="掷骰子决定接下来的命运(随机好坏事件)",
        category=ItemCategory.SPECIAL,
        rarity=ItemRarity.EPIC,
        price=150,
        stock=-1,
        cooldown=180,
        effect_duration=0,
        icon="dice.png",
        effect_type="random_event",
        effect_value={"good_chance": 0.5, "bad_chance": 0.5}
    ),
    
    "gift_bomb": ShopItem(
        id="gift_bomb",
        name="🎁 礼物炸弹",
        description="使用后下一个送礼物的人获得双倍积分",
        category=ItemCategory.SPECIAL,
        rarity=ItemRarity.RARE,
        price=100,
        stock=5,
        cooldown=300,
        effect_duration=60,
        icon="gift_bomb.png",
        effect_type="double_points",
        effect_value={"duration": 60, "multiplier": 2}
    )
}

4. 商店系统完整代码

pythonDownloadCopy codeimport time
from typing import Dict, List, Optional
from dataclasses import dataclass
import json

@dataclass
class UserInventoryItem:
    item_id: str
    quantity: int
    last_used: float  # 上次使用时间戳class ItemShop:
    def __init__(self, story_engine, obs_controller, voice_player):
        self.point_system = PointSystem()
        self.items = SHOP_ITEMS.copy()
        self.user_inventory: Dict[str, Dict[str, UserInventoryItem]] = {}
        self.active_effects: List[dict] = []        
        # 关联系统
        self.story = story_engine
        self.obs = obs_controller
        self.voice = voice_player        
        # 商店状态
        self.shop_open = True
        self.scene_stock_reset = {}  # 每场重置的库存记录
    
    def get_shop_display(self) -> str:
        """获取商店显示文本"""
        display = "══════ 🏪 道具商店 ══════\n\n"
        
        for category in ItemCategory:
            display += f"【{category.value}】\n"
            for item_id, item in self.items.items():
                if item.category == category:
                    stock_text = f"库存:{item.stock}" if item.stock > 0 else "∞"
                    if item.stock == 0:
                        stock_text = "售罄"
                    display += f"  {item.name} - {item.price}积分 ({stock_text})\n"
            display += "\n"
        
        display += "发送「购买+道具名」购买道具\n"
        display += "发送「使用+道具名」使用道具\n"
        display += "发送「背包」查看已购道具"
        
        return display
    
    def purchase_item(self, user_id: str, item_name: str) -> dict:
        """购买道具"""        # 查找道具
        item = None
        item_id = None
        for iid, i in self.items.items():
            if i.name.replace(" ", "").find(item_name) >= 0 or item_name in i.name:
                item = i
                item_id = iid
                break
        
        if not item:
            return {
                "success": False,
                "message": f"未找到道具「{item_name}」"
            }        
        # 检查库存
        if item.stock == 0:
            return {
                "success": False,
                "message": f"「{item.name}」已售罄"
            }        
        # 检查余额
        balance = self.point_system.get_balance(user_id)
        if balance < item.price:
            return {
                "success": False,
                "message": f"积分不足!需要{item.price}积分,当前{balance}积分"
            }        
        # 扣除积分
        self.point_system.user_points[user_id] -= item.price        
        # 减少库存
        if item.stock > 0:
            self.items[item_id].stock -= 1        
        # 添加到背包
        if user_id not in self.user_inventory:
            self.user_inventory[user_id] = {}
        
        if item_id in self.user_inventory[user_id]:
            self.user_inventory[user_id][item_id].quantity += 1
        else:
            self.user_inventory[user_id][item_id] = UserInventoryItem(
                item_id=item_id,
                quantity=1,
                last_used=0
            )
        
        return {
            "success": True,
            "message": f"成功购买「{item.name}」!",
            "item": item,
            "remaining_points": self.point_system.get_balance(user_id)
        }
    
    def use_item(self, user_id: str, item_name: str) -> dict:
        """使用道具"""        # 检查背包
        if user_id not in self.user_inventory:
            return {"success": False, "message": "背包是空的"}        
        # 查找道具
        target_item = None
        target_id = None
        for item_id, inv_item in self.user_inventory[user_id].items():
            item = self.items[item_id]
            if item_name in item.name or item.name.replace(" ", "").find(item_name) >= 0:
                target_item = inv_item
                target_id = item_id
                break
        
        if not target_item or target_item.quantity <= 0:
            return {"success": False, "message": f"背包中没有「{item_name}」"}
        
        item = self.items[target_id]        
        # 检查冷却
        current_time = time.time()
        if current_time - target_item.last_used < item.cooldown:
            remaining = int(item.cooldown - (current_time - target_item.last_used))
            return {
                "success": False,
                "message": f"「{item.name}」冷却中,还需{remaining}秒"
            }        
        # 消耗道具
        target_item.quantity -= 1
        target_item.last_used = current_time        
        # 触发效果
        effect_result = self._trigger_item_effect(user_id, item)
        
        return {
            "success": True,
            "message": f"使用了「{item.name}」!{effect_result}",
            "effect": item.effect_type,
            "remaining": target_item.quantity
        }
    
    def _trigger_item_effect(self, user_id: str, item: ShopItem) -> str:
        """触发道具效果"""
        effect_handlers = {
            "reveal_area": self._effect_reveal_area,
            "block_jumpscare": self._effect_block_jumpscare,
            "disable_mirror": self._effect_disable_mirror,
            "reduce_horror": self._effect_reduce_horror,
            "auto_revive": self._effect_auto_revive,
            "add_horror_element": self._effect_add_horror,
            "enhance_mirror_person": self._effect_enhance_mirror,
            "lights_off": self._effect_lights_off,
            "spawn_ghost": self._effect_spawn_ghost,
            "death_countdown": self._effect_death_countdown,
            "get_hint": self._effect_get_hint,
            "get_clue": self._effect_get_clue,
            "show_danger": self._effect_show_danger,
            "reveal_key_clue": self._effect_reveal_key_clue,
            "rewind_choice": self._effect_rewind,
            "random_event": self._effect_random_event,
            "double_points": self._effect_double_points,
        }
        
        handler = effect_handlers.get(item.effect_type)
        if handler:
            return handler(user_id, item)
        
        return "效果已触发"
    
    def _effect_reveal_area(self, user_id: str, item: ShopItem) -> str:
        """手电筒效果"""
        import random
        if random.random() < item.effect_value["reveal_chance"]:            # 触发发现隐藏线索
            self.obs.trigger_effect("flashlight_on")
            self.story.reveal_hidden_clue()
            return "手电筒照亮了黑暗,你发现了一条隐藏线索!"
        else:
            self.obs.trigger_effect("flashlight_on")
            return "手电筒照亮了周围,但没有发现什么特别的东西。"
    
    def _effect_block_jumpscare(self, user_id: str, item: ShopItem) -> str:
        """护身符效果"""
        self.active_effects.append({
            "type": "block_jumpscare",
            "count": item.effect_value["block_count"],
            "user": user_id
        })
        self.obs.trigger_effect("talisman_activate")
        return "护身符散发出淡淡的光芒,下一次惊吓将被抵挡!"
    
    def _effect_disable_mirror(self, user_id: str, item: ShopItem) -> str:
        """遮镜效果"""
        duration = item.effect_value["duration"]
        self.obs.set_source_visibility("浴室场景", "镜子遮布", True)
        self.active_effects.append({
            "type": "mirror_disabled",
            "until": time.time() + duration,
            "user": user_id
        })
        return f"镜子被遮住了,镜中人暂时无法窥视({duration}秒)"
    
    def _effect_lights_off(self, user_id: str, item: ShopItem) -> str:
        """熄灯效果"""
        duration = item.effect_value["duration"]
        self.obs.trigger_effect("lights_out")
        self.voice.play_sound_effect("lights_off")
        return f"所有灯光熄灭了!({duration}秒后恢复)"
    
    def _effect_spawn_ghost(self, user_id: str, item: ShopItem) -> str:
        """召唤幽灵"""
        import random
        ghost_types = ["女鬼", "小孩幽灵", "黑影", "白衣女子"]
        ghost = random.choice(ghost_types)
        self.obs.set_source_visibility("主场景", f"幽灵_{ghost}", True)
        self.voice.play_sound_effect("ghost_appear")
        return f"一个{ghost}出现在了房间里..."
    
    def _effect_get_hint(self, user_id: str, item: ShopItem) -> str:
        """通灵板效果"""
        hints = [
            "它...害怕...光...",
            "七天...倒计时...已经开始...",
            "镜子...不只是...镜子...",
            "她的名字...是关键...",
            "化妆镜...找到它..."
        ]
        import random
        hint = random.choice(hints)
        self.voice.speak_realtime(hint, emotion="whisper")
        return f"通灵板传来模糊的信息:「{hint}」"
    
    def _effect_rewind(self, user_id: str, item: ShopItem) -> str:
        """时间回溯"""
        if self.story.can_rewind():
            self.story.rewind_to_last_choice()
            self.obs.trigger_effect("time_rewind")
            return "时间开始倒流...你回到了上一个选择点!"
        else:
            return "无法回溯,没有可返回的选择点。"
    
    def get_user_inventory(self, user_id: str) -> str:
        """获取用户背包"""
        if user_id not in self.user_inventory:
            return "你的背包是空的"
        
        display = f"══════ 🎒 你的背包 ══════\n"
        display += f"积分余额:{self.point_system.get_balance(user_id)}\n\n"
        
        for item_id, inv_item in self.user_inventory[user_id].items():
            if inv_item.quantity > 0:
                item = self.items[item_id]
                display += f"{item.name} x{inv_item.quantity}\n"
        
        return display
    
    def check_and_apply_effects(self, event_type: str) -> bool:
        """检查并应用效果(供其他系统调用)"""
        current_time = time.time()        
        # 清理过期效果
        self.active_effects = [
            e for e in self.active_effects
            if e.get("until", float('inf')) > current_time
            and e.get("count", 1) > 0
        ]        
        # 检查是否有效果能阻止该事件
        for effect in self.active_effects:
            if event_type == "jumpscare" and effect["type"] == "block_jumpscare":
                effect["count"] -= 1
                return True  # 事件被阻止
            
            if event_type == "mirror_event" and effect["type"] == "mirror_disabled":
                return True  # 事件被阻止
        
        return False  # 事件正常触发

5.

商店UI设计(OBS配置)

商店显示区域配置:
├── 位置:屏幕左侧
├── 大小:300×600像素
├── 背景:半透明黑色(#000000,透明度70%)
└── 内容层:
    ├── 标题栏(商店名称)
    ├── 分类标签页
    ├── 道具列表(滚动显示)
    └── 用户信息(积分余额)

实现方式:使用浏览器源加载本地HTML页面

shop_display.html 关键样式:
htmlDownloadCopy code<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            background: transparent;
            font-family: 'Microsoft YaHei', sans-serif;
            color: white;
            margin: 0;
            padding: 10px;
        }
        
        .shop-container {
            background: rgba(0, 0, 0, 0.8);
            border-radius: 10px;
            padding: 15px;
            border: 2px solid #8b0000;
        }
        
        .shop-title {
            font-size: 24px;
            text-align: center;
            color: #ff6b6b;
            margin-bottom: 15px;
            text-shadow: 0 0 10px #ff0000;
        }
        
        .category {
            margin-bottom: 15px;
        }
        
        .category-title {
            font-size: 16px;
            color: #ffd700;
            border-bottom: 1px solid #ffd700;
            padding-bottom: 5px;
            margin-bottom: 10px;
        }
        
        .item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 8px;
            margin: 5px 0;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 5px;
            transition: all 0.3s;
        }
        
        .item:hover {
            background: rgba(255, 0, 0, 0.3);
        }
        
        .item-name {
            font-size: 14px;
        }
        
        .item-price {
            color: #00ff00;
            font-size: 12px;
        }
        
        .item-stock {
            color: #888;
            font-size: 11px;
        }
        
        .sold-out {
            opacity: 0.5;
            text-decoration: line-through;
        }
        
        .rarity-common { border-left: 3px solid #ffffff; }
        .rarity-rare { border-left: 3px solid #0088ff; }
        .rarity-epic { border-left: 3px solid #aa00ff; }
        .rarity-legendary { border-left: 3px solid #ffaa00; }
    </style>
</head>
<body>
    <div class="shop-container">
        <div class="shop-title">🏪 道具商店</div>
        
        <div class="category">
            <div class="category-title">【求生道具】</div>
            <div class="item rarity-common">
                <span class="item-name">🔦 手电筒</span>
                <span class="item-price">30积分</span>
            </div>
            <div class="item rarity-rare">
                <span class="item-name">🧿 护身符</span>
                <span class="item-price">100积分</span>
                <span class="item-stock">库存:5</span>
            </div>            <!-- 更多道具 -->
        </div>
        
        <div class="hint">
            发送「购买+道具名」购买<br>
            发送「使用+道具名」使用
        </div>
    </div>
</body>
</html>

二、弹幕投票系统

1. 投票系统架构

┌──────────────────────────────────────────────────────────────┐
│                      弹幕投票系统流程                          │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐  │
│   │ 投票开启 │ →  │ 弹幕收集 │ →  │ 实时统计 │ →  │ 结果执行 │  │
│   └─────────┘    └─────────┘    └─────────┘    └─────────┘  │
│        │              │              │              │        │
│        ↓              ↓              ↓              ↓        │
│   ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐  │
│   │显示选项 │    │关键词匹配│    │票数可视化│    │触发剧情 │  │
│   │倒计时   │    │防刷票    │    │实时更新  │    │播放结果 │  │
│   └─────────┘    └─────────┘    └─────────┘    └─────────┘  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

2. 投票类型设计

pythonDownloadCopy codefrom enum import Enum
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Callable
import time
import asyncio

class VoteType(Enum):
    SIMPLE = "simple"           # 简单二选一
    MULTIPLE = "multiple"       # 多选一
    WEIGHTED = "weighted"       # 加权投票(礼物加权)
    ELIMINATION = "elimination" # 淘汰式投票
    TIMED_PRESSURE = "pressure" # 限时压力投票@dataclass
class VoteOption:
    keyword: str                # 触发关键词
    display_name: str           # 显示名称
    description: str            # 选项描述
    icon: str                   # 图标
    votes: int = 0              # 当前票数
    weight_bonus: float = 1.0   # 权重加成
    required_level: int = 0     # 需要的粉丝等级
    voters: List[str] = field(default_factory=list)  # 投票者列表@dataclass
class VoteSession:
    id: str
    vote_type: VoteType
    title: str
    options: List[VoteOption]
    duration: int               # 投票时长(秒)
    start_time: float           # 开始时间
    is_active: bool = True
    allow_change: bool = False  # 是否允许改票
    min_votes: int = 1          # 最少票数要求
    tie_breaker: str = "random" # 平票处理方式
    gift_weight: Dict[str, float] = field(default_factory=dict)  # 礼物加权class VoteManager:
    def __init__(self, obs_controller, voice_player):
        self.obs = obs_controller
        self.voice = voice_player
        self.current_vote: Optional[VoteSession] = None
        self.vote_history: List[VoteSession] = []
        self.user_vote_record: Dict[str, str] = {}  # 记录用户投票
                # 回调函数
        self.on_vote_start: Callable = None
        self.on_vote_update: Callable = None
        self.on_vote_end: Callable = None        
        # 防刷票配置
        self.vote_cooldown: Dict[str, float] = {}
        self.cooldown_duration = 3  # 同一用户3秒内只能投一次
    
    async def start_vote(self, vote_config: dict) -> VoteSession:
        """开启投票"""
        options = [VoteOption(**opt) for opt in vote_config["options"]]
        
        session = VoteSession(
            id=f"vote_{int(time.time())}",
            vote_type=VoteType(vote_config.get("type", "simple")),
            title=vote_config["title"],
            options=options,
            duration=vote_config.get("duration", 30),
            start_time=time.time(),
            allow_change=vote_config.get("allow_change", False),
            min_votes=vote_config.get("min_votes", 1),
            tie_breaker=vote_config.get("tie_breaker", "random"),
            gift_weight=vote_config.get("gift_weight", {})
        )
        
        self.current_vote = session
        self.user_vote_record.clear()        
        # 显示投票界面
        self._display_vote_ui(session)        
        # 播放开始提示
        await self.voice.speak_realtime(
            f"投票开始!{session.title},请发送对应关键词进行投票,时间{session.duration}秒!",
            emotion="urgent"
        )        
        # 启动倒计时
        asyncio.create_task(self._vote_countdown(session))
        
        if self.on_vote_start:
            await self.on_vote_start(session)
        
        return session
    
    async def _vote_countdown(self, session: VoteSession):
        """投票倒计时"""
        remaining = session.duration        
        # 关键时间点提醒
        remind_points = [30, 15, 10, 5, 3, 2, 1]
        
        while remaining > 0 and session.is_active:            # 更新倒计时显示
            self.obs.set_text("投票倒计时", f"剩余 {remaining} 秒")            
            # 关键时间点语音提醒
            if remaining in remind_points:
                if remaining > 5:
                    await self.voice.speak_realtime(f"还剩{remaining}秒")
                else:
                    await self.voice.speak_realtime(f"{remaining}")
            
            await asyncio.sleep(1)
            remaining -= 1        
        # 投票结束
        await self._end_vote(session)
    
    def process_vote(self, user_id: str, content: str, gift_value: int = 0) -> dict:
        """处理投票弹幕"""
        if not self.current_vote or not self.current_vote.is_active:
            return {"success": False, "message": "当前没有进行中的投票"}
        
        session = self.current_vote
        content = content.strip().lower()        
        # 防刷票检查
        current_time = time.time()
        if user_id in self.vote_cooldown:
            if current_time - self.vote_cooldown[user_id] < self.cooldown_duration:
                return {"success": False, "message": "投票太频繁,请稍后再试"}        
        # 查找匹配的选项
        matched_option = None
        for option in session.options:
            keywords = [option.keyword.lower()]            # 支持多个关键词别名
            if hasattr(option, 'aliases'):
                keywords.extend([a.lower() for a in option.aliases])
            
            if content in keywords:
                matched_option = option
                break
        
        if not matched_option:
            return {"success": False, "message": "无效的投票选项"}        
        # 检查是否已投票
        if user_id in self.user_vote_record:
            if not session.allow_change:
                return {"success": False, "message": "你已经投过票了"}
            else:                # 撤销之前的投票
                old_keyword = self.user_vote_record[user_id]
                for opt in session.options:
                    if opt.keyword == old_keyword and user_id in opt.voters:
                        opt.voters.remove(user_id)
                        opt.votes -= 1
                        break        
        # 计算投票权重
        vote_weight = 1.0        
        # 礼物加权
        if gift_value > 0 and session.gift_weight:
            for gift_threshold, weight in sorted(session.gift_weight.items(), reverse=True):
                if gift_value >= int(gift_threshold):
                    vote_weight = weight
                    break        
        # 记录投票
        matched_option.votes += int(vote_weight)
        matched_option.voters.append(user_id)
        self.user_vote_record[user_id] = matched_option.keyword
        self.vote_cooldown[user_id] = current_time        
        # 更新显示
        self._update_vote_display(session)
        
        if self.on_vote_update:
            asyncio.create_task(self.on_vote_update(session, matched_option, user_id))
        
        return {
            "success": True,
            "message": f"投票成功!你选择了【{matched_option.display_name}】",
            "weight": vote_weight
        }
    
    async def _end_vote(self, session: VoteSession):
        """结束投票"""
        session.is_active = False        
        # 计算结果
        total_votes = sum(opt.votes for opt in session.options)
        
        if total_votes < session.min_votes:            # 票数不足,随机选择或使用默认
            import random
            winner = random.choice(session.options)
            result_message = f"投票人数不足,系统随机选择了【{winner.display_name}】"
        else:            # 正常统计
            sorted_options = sorted(session.options, key=lambda x: x.votes, reverse=True)            
            # 检查平票
            if len(sorted_options) > 1 and sorted_options[0].votes == sorted_options[1].votes:
                if session.tie_breaker == "random":
                    import random
                    tied = [o for o in sorted_options if o.votes == sorted_options[0].votes]
                    winner = random.choice(tied)
                    result_message = f"票数相同!系统随机选择了【{winner.display_name}】"
                elif session.tie_breaker == "first":
                    winner = sorted_options[0]
                    result_message = f"票数相同,采用先到先得,选择【{winner.display_name}】"
            else:
                winner = sorted_options[0]
                percentage = (winner.votes / total_votes * 100) if total_votes > 0 else 0
                result_message = f"投票结束!【{winner.display_name}】获得{winner.votes}票({percentage:.1f}%)获胜!"        
        # 显示结果
        await self._display_vote_result(session, winner, result_message)        
        # 记录历史
        self.vote_history.append(session)
        self.current_vote = None
        
        if self.on_vote_end:
            await self.on_vote_end(session, winner)
        
        return winner
    
    def _display_vote_ui(self, session: VoteSession):
        """显示投票界面"""        # 显示投票框
        self.obs.set_source_visibility("主场景", "投票框", True)        
        # 设置标题
        self.obs.set_text("投票标题", session.title)        
        # 设置选项
        for i, option in enumerate(session.options):
            self.obs.set_text(f"选项{i+1}", f"【{option.keyword}】{option.display_name}")
            self.obs.set_text(f"票数{i+1}", "0")            # 设置进度条初始状态
            self.obs.trigger_effect(f"progress_bar_{i+1}_reset")
    
    def _update_vote_display(self, session: VoteSession):
        """更新投票显示"""
        total_votes = sum(opt.votes for opt in session.options)
        
        for i, option in enumerate(session.options):            # 更新票数
            self.obs.set_text(f"票数{i+1}", str(option.votes))            
            # 更新进度条
            percentage = (option.votes / total_votes * 100) if total_votes > 0 else 0
            self.obs.trigger_effect(f"progress_bar_{i+1}_set_{int(percentage)}")
    
    async def _display_vote_result(self, session: VoteSession, winner: VoteOption, message: str):
        """显示投票结果"""        # 播放结果音效
        self.voice.play_sound_effect("vote_end")        
        # 高亮获胜选项
        self.obs.trigger_effect("winner_highlight")        
        # 播放结果语音
        await self.voice.speak_realtime(message, emotion="normal")        
        # 等待2秒后隐藏投票框
        await asyncio.sleep(2)
        self.obs.set_source_visibility("主场景", "投票框", False)

3. 投票场景设计示例

pythonDownloadCopy code# 投票配置模板VOTE_TEMPLATES = {    # 简单二选一
    "simple_choice": {
        "type": "simple",
        "title": "你要怎么做?",
        "duration": 30,
        "allow_change": False,
        "options": [
            {
                "keyword": "A",
                "display_name": "打开门",
                "description": "鼓起勇气打开那扇门",
                "icon": "🚪"
            },
            {
                "keyword": "B",
                "display_name": "转身离开",
                "description": "直觉告诉你不要打开",
                "icon": "🏃"
            }
        ]
    },    
    # 多选一(探索)
    "exploration": {
        "type": "multiple",
        "title": "接下来探索哪里?",
        "duration": 45,
        "allow_change": True,
        "min_votes": 5,
        "options": [
            {
                "keyword": "卧室",
                "display_name": "卧室",
                "description": "看看床底下有什么",
                "icon": "🛏️"
            },
            {
                "keyword": "浴室",
                "display_name": "浴室",
                "description": "那面镜子很奇怪",
                "icon": "🚿"
            },
            {
                "keyword": "厨房",
                "display_name": "厨房",
                "description": "有奇怪的声音传来",
                "icon": "🍳"
            },
            {
                "keyword": "阳台",
                "display_name": "阳台",
                "description": "也许能找到逃生路线",
                "icon": "🌙"
            }
        ]
    },    
    # 加权投票(重要抉择)
    "weighted_critical": {
        "type": "weighted",
        "title": "【关键抉择】如何对付镜中人?",
        "duration": 60,
        "allow_change": False,
        "min_votes": 10,
        "tie_breaker": "random",
        "gift_weight": {
            "1": 1,      # 1-9抖币 = 1票
            "10": 2,     # 10-99抖币 = 2票
            "100": 5,    # 100-999抖币 = 5票
            "1000": 15   # 1000+抖币 = 15票
        },
        "options": [
            {
                "keyword": "对抗",
                "display_name": "正面对抗",
                "description": "用找到的圣物直接面对它",
                "icon": "⚔️"
            },
            {
                "keyword": "封印",
                "display_name": "封印镜子",
                "description": "尝试用仪式封印镜子",
                "icon": "🔮"
            },
            {
                "keyword": "沟通",
                "display_name": "尝试沟通",
                "description": "或许它只是想被理解",
                "icon": "💬"
            }
        ]
    },    
    # 限时压力投票
    "pressure_vote": {
        "type": "pressure",
        "title": "【紧急!】门外的脚步声越来越近!",
        "duration": 15,  # 只有15秒
        "allow_change": False,
        "min_votes": 3,
        "tie_breaker": "first",  # 平票时先到先得
        "options": [
            {
                "keyword": "躲",
                "display_name": "躲进衣柜",
                "description": "",
                "icon": "🙈"
            },
            {
                "keyword": "跑",
                "display_name": "从窗户逃跑",
                "description": "",
                "icon": "🏃"
            }
        ]
    },    
    # 淘汰式投票(用于多回合)
    "elimination": {
        "type": "elimination",
        "title": "谁是凶手?(票数最少的嫌疑人将被排除)",
        "duration": 60,
        "rounds": 3,  # 3轮淘汰
        "options": [
            {"keyword": "房东", "display_name": "房东老王", "icon": "👴"},
            {"keyword": "邻居", "display_name": "邻居小李", "icon": "👨"},
            {"keyword": "前任", "display_name": "前租客", "icon": "👻"},
            {"keyword": "自己", "display_name": "镜中的自己", "icon": "🪞"}
        ]
    }
}

4. 投票UI配置(OBS)

htmlDownloadCopy code<!-- vote_display.html --><!DOCTYPE html>
<html>
<head>
    <style>
        body {
            background: transparent;
            font-family: 'Microsoft YaHei', sans-serif;
            color: white;
            overflow: hidden;
        }
        
        .vote-container {
            background: linear-gradient(135deg, rgba(20, 0, 0, 0.95), rgba(40, 0, 0, 0.9));
            border: 3px solid #8b0000;
            border-radius: 15px;
            padding: 20px;
            width: 500px;
            box-shadow: 0 0 30px rgba(255, 0, 0, 0.3);
        }
        
        .vote-title {
            font-size: 28px;
            text-align: center;
            color: #ff4444;
            margin-bottom: 20px;
            text-shadow: 0 0 15px #ff0000;
            animation: pulse 2s infinite;
        }
        
        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.7; }
        }
        
        .countdown {
            text-align: center;
            font-size: 36px;
            color: #ffcc00;
            margin-bottom: 15px;
        }
        
        .countdown.urgent {
            color: #ff0000;
            animation: blink 0.5s infinite;
        }
        
        @keyframes blink {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.3; }
        }
        
        .option {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
            padding: 15px;
            margin: 10px 0;
            display: flex;
            align-items: center;
            transition: all 0.3s;
            position: relative;
            overflow: hidden;
        }
        
        .option:hover {
            background: rgba(255, 100, 100, 0.2);
        }
        
        .option.winner {
            background: rgba(255, 215, 0, 0.3);
            border: 2px solid gold;
        }
        
        .option-icon {
            font-size: 32px;
            margin-right: 15px;
        }
        
        .option-content {
            flex: 1;
        }
        
        .option-name {
            font-size: 20px;
            font-weight: bold;
        }
        
        .option-keyword {
            color: #00ff00;
            font-size: 14px;
        }
        
        .option-votes {
            font-size: 24px;
            color: #ffcc00;
            min-width: 60px;
            text-align: right;
        }
        
        .progress-bar {
            position: absolute;
            bottom: 0;
            left: 0;
            height: 4px;
            background: linear-gradient(90deg, #ff0000, #ff6600);
            transition: width 0.3s;
        }
        
        .vote-hint {
            text-align: center;
            color: #888;
            font-size: 14px;
            margin-top: 15px;
        }
        
        .gift-bonus {
            background: rgba(255, 215, 0, 0.2);
            border-radius: 5px;
            padding: 5px 10px;
            font-size: 12px;
            color: #ffd700;
            margin-top: 10px;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="vote-container">
        <div class="vote-title" id="voteTitle">你要怎么做?</div>
        
        <div class="countdown" id="countdown">30</div>
        
        <div class="option" id="option1">
            <div class="option-icon">🚪</div>
            <div class="option-content">
                <div class="option-name">打开门</div>
                <div class="option-keyword">发送【A】投票</div>
            </div>
            <div class="option-votes" id="votes1">0</div>
            <div class="progress-bar" id="progress1" style="width: 0%"></div>
        </div>
        
        <div class="option" id="option2">
            <div class="option-icon">🏃</div>
            <div class="option-content">
                <div class="option-name">转身离开</div>
                <div class="option-keyword">发送【B】投票</div>
            </div>
            <div class="option-votes" id="votes2">0</div>
            <div class="progress-bar" id="progress2" style="width: 0%"></div>
        </div>
        
        <div class="gift-bonus">
            💝 送礼物可获得额外投票权重!
        </div>
        
        <div class="vote-hint">
            投票后无法更改,请谨慎选择
        </div>
    </div>
    
    <script>        // WebSocket连接,接收实时数据更新
        const ws = new WebSocket('ws://localhost:8765');
        
        ws.onmessage = function(event) {
            const data = JSON.parse(event.data);
            
            if (data.type === 'vote_update') {
                updateVotes(data.votes);
            } else if (data.type === 'countdown') {
                updateCountdown(data.seconds);
            } else if (data.type === 'winner') {
                showWinner(data.option_id);
            }
        };
        
        function updateVotes(votes) {
            const total = Object.values(votes).reduce((a, b) => a + b, 0) || 1;
            
            for (let i = 1; i <= Object.keys(votes).length; i++) {
                const voteCount = votes[i] || 0;
                document.getElementById(`votes${i}`).textContent = voteCount;
                document.getElementById(`progress${i}`).style.width = 
                    `${(voteCount / total * 100)}%`;
            }
        }
        
        function updateCountdown(seconds) {
            const countdown = document.getElementById('countdown');
            countdown.textContent = seconds;
            
            if (seconds <= 5) {
                countdown.classList.add('urgent');
            }
        }
        
        function showWinner(optionId) {
            document.getElementById(`option${optionId}`).classList.add('winner');
        }
    </script>
</body>
</html>

三、阵营对抗系统

1. 阵营系统架构

┌──────────────────────────────────────────────────────────────┐
│                       阵营对抗系统                            │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│   ┌───────────────────┐         ┌───────────────────┐       │
│   │    🛡️ 求生阵营     │   VS    │    💀 作死阵营     │       │
│   │                   │         │                   │       │
│   │  目标:帮助主角   │         │  目标:制造恐怖   │       │
│   │  存活并逃出生天   │         │  让主角陷入绝境   │       │
│   │                   │         │                   │       │
│   │  送玫瑰/爱心     │         │  送墓碑/幽灵     │       │
│   │  = 求生能量+1    │         │  = 作死能量+1    │       │
│   │                   │         │                   │       │
│   └───────────────────┘         └───────────────────┘       │
│              │                           │                   │
│              ↓                           ↓                   │
│        ┌──────────┐               ┌──────────┐              │
│        │ 能量条   │               │ 能量条   │              │
│        │ ████░░░░ │               │ ░░░░████ │              │
│        └──────────┘               └──────────┘              │
│              │                           │                   │
│              └───────────┬───────────────┘                   │
│                          ↓                                   │
│                    ┌──────────┐                             │
│                    │ 对决事件 │                             │
│                    │ 触发判定 │                             │
│                    └──────────┘                             │
│                          │                                   │
│              ┌───────────┼───────────┐                      │
│              ↓           ↓           ↓                      │
│        ┌─────────┐ ┌─────────┐ ┌─────────┐                 │
│        │求生胜利 │ │ 平 局  │ │作死胜利 │                 │
│        │主角获救 │ │随机结果 │ │恐怖升级 │                 │
│        └─────────┘ └─────────┘ └─────────┘                 │
│                                                              │
└──────────────────────────────────────────────────────────────┘

2. 阵营系统完整代码

pythonDownloadCopy codefrom enum import Enum
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Callable
import time
import asyncio
import random

class Faction(Enum):
    SURVIVE = "survive"  # 求生阵营
    DEATH = "death"      # 作死阵营
    NEUTRAL = "neutral"  # 中立@dataclass
class FactionMember:
    user_id: str
    username: str
    faction: Faction
    contribution: int = 0      # 总贡献
    join_time: float = 0       # 加入时间
    title: str = ""            # 称号
    level: int = 1             # 阵营等级@dataclass
class FactionBattle:
    id: str
    title: str
    description: str
    start_time: float
    duration: int
    survive_power: int = 0
    death_power: int = 0
    is_active: bool = True
    threshold: int = 100       # 触发阈值
    effect_on_survive_win: str = ""
    effect_on_death_win: str = ""
    effect_on_tie: str = ""

class FactionSystem:
    def __init__(self, obs_controller, voice_player, story_engine):
        self.obs = obs_controller
        self.voice = voice_player
        self.story = story_engine        
        # 阵营成员
        self.members: Dict[str, FactionMember] = {}        
        # 阵营礼物映射
        self.survive_gifts = {
            "小心心": 1,
            "玫瑰": 10,
            "爱心": 5,
            "加油": 3,
            "仙女棒": 20,
            "守护之心": 50,
            "城堡": 100
        }
        
        self.death_gifts = {
            "墓碑": 10,
            "幽灵": 15,
            "骷髅": 20,
            "南瓜灯": 8,
            "恶魔": 30,
            "地狱火": 50,
            "死神": 100
        }        
        # 中立礼物(双方都加)
        self.neutral_gifts = {
            "啤酒": 5,
            "抖音": 5,
            "棒棒糖": 3
        }        
        # 当前对决
        self.current_battle: Optional[FactionBattle] = None        
        # 阵营能量(持续累积)
        self.global_survive_power = 0
        self.global_death_power = 0        
        # 称号系统
        self.survive_titles = {
            10: "🌱 新手守护者",
            50: "🛡️ 正义使者",
            200: "⚔️ 驱魔师",
            500: "🌟 圣骑士",
            1000: "👼 天使长",
            5000: "☀️ 光明圣者"
        }
        
        self.death_titles = {
            10: "💀 恐惧学徒",
            50: "👻 幽灵猎手",
            200: "🦇 暗夜使者",
            500: "😈 恶魔信徒",
            1000: "🔥 地狱使者",
            5000: "💀 死神代言人"
        }        
        # 回调
        self.on_faction_update: Callable = None
        self.on_battle_trigger: Callable = None
        self.on_battle_end: Callable = None
    
    def process_gift(self, user_id: str, username: str, gift_name: str, 
                     gift_count: int = 1) -> dict:
        """处理礼物,判断阵营"""
        faction = Faction.NEUTRAL
        power = 0
        
        if gift_name in self.survive_gifts:
            faction = Faction.SURVIVE
            power = self.survive_gifts[gift_name] * gift_count
            self.global_survive_power += power
            if self.current_battle:
                self.current_battle.survive_power += power
                
        elif gift_name in self.death_gifts:
            faction = Faction.DEATH
            power = self.death_gifts[gift_name] * gift_count
            self.global_death_power += power
            if self.current_battle:
                self.current_battle.death_power += power
                
        elif gift_name in self.neutral_gifts:
            faction = Faction.NEUTRAL
            power = self.neutral_gifts[gift_name] * gift_count
            self.global_survive_power += power // 2
            self.global_death_power += power // 2
            if self.current_battle:
                self.current_battle.survive_power += power // 2
                self.current_battle.death_power += power // 2
        else:
            return {"success": False, "message": "该礼物不参与阵营对抗"}        
        # 更新或创建成员
        self._update_member(user_id, username, faction, power)        
        # 更新显示
        self._update_faction_display()        
        # 检查是否触发事件
        asyncio.create_task(self._check_faction_events())
        
        return {
            "success": True,
            "faction": faction.value,
            "power_added": power,
            "message": self._get_gift_response(faction, power, username)
        }
    
    def _update_member(self, user_id: str, username: str, 
                       faction: Faction, power: int):
        """更新成员信息"""
        if user_id not in self.members:
            self.members[user_id] = FactionMember(
                user_id=user_id,
                username=username,
                faction=faction,
                contribution=power,
                join_time=time.time()
            )
        else:
            member = self.members[user_id]            
            # 如果之前是中立或不同阵营,允许转换
            if faction != Faction.NEUTRAL:
                if member.faction == Faction.NEUTRAL or member.faction == faction:
                    member.faction = faction                # 如果换阵营,清零贡献(可选规则)
                elif member.faction != faction:                    # 叛变惩罚:贡献减半
                    member.contribution = member.contribution // 2
                    member.faction = faction
            
            member.contribution += power        
        # 更新称号
        self._update_title(user_id)
    
    def _update_title(self, user_id: str):
        """更新用户称号"""
        member = self.members[user_id]
        
        titles = (self.survive_titles if member.faction == Faction.SURVIVE 
                  else self.death_titles)
        
        for threshold, title in sorted(titles.items(), reverse=True):
            if member.contribution >= threshold:
                member.title = title
                member.level = list(titles.keys()).index(threshold) + 1
                break
    
    def _get_gift_response(self, faction: Faction, power: int, 
                           username: str) -> str:
        """生成礼物响应消息"""
        if faction == Faction.SURVIVE:
            responses = [
                f"💚 {username} 为求生阵营注入了 {power} 点能量!",
                f"🛡️ {username} 的祝福让主角更加安全!+{power}",
                f"✨ 来自 {username} 的守护之力!求生+{power}"
            ]
        elif faction == Faction.DEATH:
            responses = [
                f"💀 {username} 为作死阵营献上了 {power} 点黑暗能量!",
                f"👻 {username} 召唤了更多恐惧!+{power}",
                f"🔥 {username} 的诅咒降临!作死+{power}"
            ]
        else:
            responses = [
                f"⚖️ {username} 保持中立,双方各+{power//2}",
            ]
        
        return random.choice(responses)
    
    async def start_battle(self, battle_config: dict) -> FactionBattle:
        """开启阵营对决"""
        battle = FactionBattle(
            id=f"battle_{int(time.time())}",
            title=battle_config["title"],
            description=battle_config["description"],
            start_time=time.time(),
            duration=battle_config.get("duration", 120),
            threshold=battle_config.get("threshold", 100),
            effect_on_survive_win=battle_config.get("survive_win_effect", ""),
            effect_on_death_win=battle_config.get("death_win_effect", ""),
            effect_on_tie=battle_config.get("tie_effect", "")
        )
        
        self.current_battle = battle        
        # 显示对决界面
        self._show_battle_ui(battle)        
        # 播放开始语音
        await self.voice.speak_realtime(
            f"阵营对决开始!{battle.title}!求生派和作死派的较量!持续{battle.duration}秒!",
            emotion="urgent"
        )        
        # 启动倒计时
        asyncio.create_task(self._battle_countdown(battle))
        
        if self.on_battle_trigger:
            await self.on_battle_trigger(battle)
        
        return battle
    
    async def _battle_countdown(self, battle: FactionBattle):
        """对决倒计时"""
        remaining = battle.duration
        
        while remaining > 0 and battle.is_active:            # 更新倒计时
            self.obs.set_text("对决倒计时", f"{remaining}秒")            
            # 更新能量条
            self._update_battle_display(battle)            
            # 关键时间提醒
            if remaining in [60, 30, 10, 5]:
                await self.voice.speak_realtime(
                    f"对决还剩{remaining}秒!求生{battle.survive_power}对作死{battle.death_power}!"
                )
            
            await asyncio.sleep(1)
            remaining -= 1        
        # 对决结束
        await self._end_battle(battle)
    
    async def _end_battle(self, battle: FactionBattle):
        """结束对决"""
        battle.is_active = False        
        # 判定胜负
        if battle.survive_power > battle.death_power:
            winner = Faction.SURVIVE
            result_text = f"求生阵营获胜!{battle.survive_power} vs {battle.death_power}"
            effect = battle.effect_on_survive_win
        elif battle.death_power > battle.survive_power:
            winner = Faction.DEATH
            result_text = f"作死阵营获胜!{battle.death_power} vs {battle.survive_power}"
            effect = battle.effect_on_death_win
        else:
            winner = Faction.NEUTRAL
            result_text = f"势均力敌!{battle.survive_power} vs {battle.death_power}"
            effect = battle.effect_on_tie        
        # 显示结果
        await self._display_battle_result(battle, winner, result_text)        
        # 触发对应效果
        if effect:
            await self._trigger_battle_effect(effect, winner)
        
        self.current_battle = None
        
        if self.on_battle_end:
            await self.on_battle_end(battle, winner)
    
    async def _trigger_battle_effect(self, effect: str, winner: Faction):
        """触发对决效果"""
        effects = {            # 求生胜利效果
            "safe_passage": lambda: self._effect_safe_passage(),
            "extra_clue": lambda: self._effect_extra_clue(),
            "skip_horror": lambda: self._effect_skip_horror(),
            "healing_light": lambda: self._effect_healing_light(),            
            # 作死胜利效果
            "summon_ghost": lambda: self._effect_summon_ghost(),
            "lights_out": lambda: self._effect_lights_out(),
            "jumpscare": lambda: self._effect_jumpscare(),
            "mirror_crack": lambda: self._effect_mirror_crack(),
            "death_countdown": lambda: self._effect_death_countdown(),            
            # 平局效果
            "random_event": lambda: self._effect_random_event(),
        }
        
        if effect in effects:
            await effects[effect]()
    
    async def _effect_safe_passage(self):
        """安全通道 - 跳过一个危险区域"""
        await self.voice.speak_realtime(
            "求生阵营的守护让主角安全通过了危险区域!",
            emotion="normal"
        )
        self.obs.trigger_effect("holy_shield")
        self.story.skip_next_danger()
    
    async def _effect_extra_clue(self):
        """额外线索"""
        await self.voice.speak_realtime(
            "求生阵营的祝福让主角发现了一条隐藏线索!"
        )
        self.story.reveal_hidden_clue()
    
    async def _effect_summon_ghost(self):
        """召唤幽灵"""
        await self.voice.speak_realtime(
            "作死阵营召唤出了不安的灵魂...",
            emotion="scary"
        )
        self.obs.trigger_effect("spawn_ghost")
        self.voice.play_sound_effect("ghost_laugh")
    
    async def _effect_lights_out(self):
        """强制熄灯"""
        await self.voice.speak_realtime(
            "作死阵营的黑暗力量吞噬了所有光源!",
            emotion="scary"
        )
        self.obs.trigger_effect("lights_out_30s")
        self.story.increase_fear(20)
    
    async def _effect_jumpscare(self):
        """惊吓"""
        await asyncio.sleep(random.uniform(1, 3))
        self.obs.trigger_effect("jumpscare")
        self.voice.play_sound_effect("jumpscare")
    
    async def _effect_random_event(self):
        """随机事件"""
        events = [
            ("好运降临!主角找到了一个有用的道具。", "good"),
            ("不祥的预感...似乎有什么在靠近。", "bad"),
            ("一切保持原样。", "neutral")
        ]
        event, type_ = random.choice(events)
        await self.voice.speak_realtime(event)
        
        if type_ == "good":
            self.story.add_random_item()
        elif type_ == "bad":
            self.story.increase_fear(10)
    
    def _show_battle_ui(self, battle: FactionBattle):
        """显示对决UI"""
        self.obs.set_source_visibility("主场景", "阵营对决框", True)
        self.obs.set_text("对决标题", battle.title)
        self.obs.set_text("对决描述", battle.description)
    
    def _update_battle_display(self, battle: FactionBattle):
        """更新对决显示"""
        total = battle.survive_power + battle.death_power
        if total == 0:
            total = 1
        
        survive_percent = battle.survive_power / total * 100
        death_percent = battle.death_power / total * 100
        
        self.obs.set_text("求生能量", str(battle.survive_power))
        self.obs.set_text("作死能量", str(battle.death_power))        
        # 更新能量条
        self.obs.trigger_effect(f"survive_bar_{int(survive_percent)}")
        self.obs.trigger_effect(f"death_bar_{int(death_percent)}")
    
    def _update_faction_display(self):
        """更新全局阵营显示"""
        self.obs.set_text("全局求生", str(self.global_survive_power))
        self.obs.set_text("全局作死", str(self.global_death_power))
    
    async def _display_battle_result(self, battle: FactionBattle, 
                                     winner: Faction, result_text: str):
        """显示对决结果"""        # 播放结果音效
        if winner == Faction.SURVIVE:
            self.voice.play_sound_effect("victory_holy")
            self.obs.trigger_effect("survive_victory")
        elif winner == Faction.DEATH:
            self.voice.play_sound_effect("victory_dark")
            self.obs.trigger_effect("death_victory")
        else:
            self.voice.play_sound_effect("draw")
        
        await self.voice.speak_realtime(result_text, emotion="normal")
        
        await asyncio.sleep(3)
        self.obs.set_source_visibility("主场景", "阵营对决框", False)
    
    def get_leaderboard(self, faction: Faction = None, top_n: int = 10) -> str:
        """获取排行榜"""
        if faction:
            members = [m for m in self.members.values() if m.faction == faction]
        else:
            members = list(self.members.values())
        
        sorted_members = sorted(members, key=lambda x: x.contribution, reverse=True)[:top_n]
        
        if faction == Faction.SURVIVE:
            title = "🛡️ 求生阵营排行榜"
        elif faction == Faction.DEATH:
            title = "💀 作死阵营排行榜"
        else:
            title = "🏆 总贡献排行榜"
        
        display = f"══════ {title} ══════\n\n"
        
        for i, member in enumerate(sorted_members, 1):
            medal = ["🥇", "🥈", "🥉"][i-1] if i <= 3 else f"{i}."
            display += f"{medal} {member.title} {member.username}: {member.contribution}\n"
        
        return display
    
    def get_faction_status(self) -> str:
        """获取阵营状态"""
        survive_members = len([m for m in self.members.values() 
                               if m.faction == Faction.SURVIVE])
        death_members = len([m for m in self.members.values() 
                             if m.faction == Faction.DEATH])
        
        display = "══════ ⚔️ 阵营对抗 ══════\n\n"
        display += f"🛡️ 求生阵营\n"
        display += f"   成员: {survive_members}人\n"
        display += f"   能量: {self.global_survive_power}\n\n"
        display += f"💀 作死阵营\n"
        display += f"   成员: {death_members}人\n"
        display += f"   能量: {self.global_death_power}\n\n"
        
        if self.current_battle:
            display += f"🔥 当前对决: {self.current_battle.title}\n"
            display += f"   {self.current_battle.survive_power} vs {self.current_battle.death_power}\n"
        
        return display

3. 对决事件配置示例

pythonDownloadCopy codeBATTLE_TEMPLATES = {    # 关键选择对决
    "critical_choice": {
        "title": "💀 生死抉择 🛡️",
        "description": "主角面临生死选择,你的阵营将决定他的命运!",
        "duration": 60,
        "threshold": 50,
        "survive_win_effect": "safe_passage",
        "death_win_effect": "summon_ghost",
        "tie_effect": "random_event"
    },    
    # 镜中人对决
    "mirror_confrontation": {
        "title": "🪞 镜中对决 🪞",
        "description": "镜中人正在觉醒!求生派快来压制,作死派快来助力!",
        "duration": 90,
        "threshold": 100,
        "survive_win_effect": "skip_horror",
        "death_win_effect": "mirror_crack",
        "tie_effect": "random_event"
    },    
    # 午夜惊魂
    "midnight_horror": {
        "title": "🌙 午夜惊魂 🌙",
        "description": "凌晨3点33分,最危险的时刻来临!",
        "duration": 120,
        "threshold": 200,
        "survive_win_effect": "healing_light",
        "death_win_effect": "death_countdown",
        "tie_effect": "random_event"
    },    
    # 最终对决
    "final_battle": {
        "title": "⚔️ 最终决战 ⚔️",
        "description": "决定主角命运的最后时刻!倾尽所有!",
        "duration": 180,
        "threshold": 500,
        "survive_win_effect": "true_ending",
        "death_win_effect": "bad_ending",
        "tie_effect": "neutral_ending"
    }
}

4. 阵营对抗UI配置(OBS)

htmlDownloadCopy code<!-- faction_battle.html --><!DOCTYPE html>
<html>
<head>
    <style>
        body {
            background: transparent;
            font-family: 'Microsoft YaHei', sans-serif;
            color: white;
            overflow: hidden;
        }
        
        .battle-container {
            width: 600px;
            padding: 20px;
            background: rgba(0, 0, 0, 0.85);
            border-radius: 15px;
            border: 3px solid #444;
        }
        
        .battle-title {
            text-align: center;
            font-size: 32px;
            margin-bottom: 10px;
            background: linear-gradient(90deg, #00ff00, #ff0000);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            animation: glow 2s infinite;
        }
        
        @keyframes glow {
            0%, 100% { filter: drop-shadow(0 0 5px #fff); }
            50% { filter: drop-shadow(0 0 20px #fff); }
        }
        
        .battle-description {
            text-align: center;
            color: #aaa;
            margin-bottom: 20px;
        }
        
        .versus-container {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
        }
        
        .faction {
            width: 200px;
            text-align: center;
        }
        
        .faction-survive {
            color: #00ff00;
        }
        
        .faction-death {
            color: #ff0000;
        }
        
        .faction-icon {
            font-size: 48px;
            margin-bottom: 10px;
        }
        
        .faction-name {
            font-size: 24px;
            font-weight: bold;
        }
        
        .faction-power {
            font-size: 36px;
            margin: 10px 0;
        }
        
        .vs-icon {
            font-size: 48px;
            color: #ffcc00;
            animation: pulse 1s infinite;
        }
        
        @keyframes pulse {
            0%, 100% { transform: scale(1); }
            50% { transform: scale(1.2); }
        }
        
        .power-bar-container {
            height: 30px;
            background: #333;
            border-radius: 15px;
            overflow: hidden;
            display: flex;
            margin-bottom: 20px;
        }
        
        .power-bar-survive {
            background: linear-gradient(90deg, #00aa00, #00ff00);
            height: 100%;
            transition: width 0.5s;
            display: flex;
            align-items: center;
            justify-content: flex-end;
            padding-right: 10px;
        }
        
        .power-bar-death {
            background: linear-gradient(90deg, #ff0000, #aa0000);
            height: 100%;
            transition: width 0.5s;
            display: flex;
            align-items: center;
            padding-left: 10px;
        }
        
        .countdown {
            text-align: center;
            font-size: 48px;
            color: #ffcc00;
        }
        
        .countdown.urgent {
            color: #ff0000;
            animation: blink 0.5s infinite;
        }
        
        @keyframes blink {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.3; }
        }
        
        .gift-guide {
            display: flex;
            justify-content: space-around;
            margin-top: 15px;
            padding-top: 15px;
            border-top: 1px solid #444;
        }
        
        .guide-item {
            text-align: center;
            font-size: 12px;
            color: #888;
        }
        
        .guide-gifts {
            font-size: 20px;
            margin-bottom: 5px;
        }        
        /* 胜利动画 */
        .victory-survive {
            animation: victoryGreen 0.5s ease-out;
        }
        
        .victory-death {
            animation: victoryRed 0.5s ease-out;
        }
        
        @keyframes victoryGreen {
            0% { box-shadow: 0 0 0 rgba(0, 255, 0, 0); }
            100% { box-shadow: 0 0 50px rgba(0, 255, 0, 0.8); }
        }
        
        @keyframes victoryRed {
            0% { box-shadow: 0 0 0 rgba(255, 0, 0, 0); }
            100% { box-shadow: 0 0 50px rgba(255, 0, 0, 0.8); }
        }
    </style>
</head>
<body>
    <div class="battle-container" id="battleContainer">
        <div class="battle-title" id="battleTitle">⚔️ 阵营对决 ⚔️</div>
        <div class="battle-description" id="battleDesc">主角的命运掌握在你们手中!</div>
        
        <div class="versus-container">
            <div class="faction faction-survive">
                <div class="faction-icon">🛡️</div>
                <div class="faction-name">求生阵营</div>
                <div class="faction-power" id="survivePower">0</div>
            </div>
            
            <div class="vs-icon">⚔️</div>
            
            <div class="faction faction-death">
                <div class="faction-icon">💀</div>
                <div class="faction-name">作死阵营</div>
                <div class="faction-power" id="deathPower">0</div>
            </div>
        </div>
        
        <div class="power-bar-container">
            <div class="power-bar-survive" id="surviveBar" style="width: 50%"></div>
            <div class="power-bar-death" id="deathBar" style="width: 50%"></div>
        </div>
        
        <div class="countdown" id="countdown">60</div>
        
        <div class="gift-guide">
            <div class="guide-item">
                <div class="guide-gifts">🌹 ❤️ ✨</div>
                <div>求生阵营礼物</div>
            </div>
            <div class="guide-item">
                <div class="guide-gifts">💀 👻 🔥</div>
                <div>作死阵营礼物</div>
            </div>
        </div>
    </div>
    
    <script>
        const ws = new WebSocket('ws://localhost:8765');
        
        ws.onmessage = function(event) {
            const data = JSON.parse(event.data);
            
            if (data.type === 'battle_update') {
                updateBattle(data);
            } else if (data.type === 'battle_end') {
                showVictory(data.winner);
            }
        };
        
        function updateBattle(data) {
            document.getElementById('survivePower').textContent = data.survive_power;
            document.getElementById('deathPower').textContent = data.death_power;
            document.getElementById('countdown').textContent = data.remaining;
            
            const total = data.survive_power + data.death_power || 1;
            const survivePercent = (data.survive_power / total * 100);
            const deathPercent = (data.death_power / total * 100);
            
            document.getElementById('surviveBar').style.width = survivePercent + '%';
            document.getElementById('deathBar').style.width = deathPercent + '%';            
            // 紧急状态
            const countdown = document.getElementById('countdown');
            if (data.remaining <= 10) {
                countdown.classList.add('urgent');
            }
        }
        
        function showVictory(winner) {
            const container = document.getElementById('battleContainer');
            if (winner === 'survive') {
                container.classList.add('victory-survive');
            } else if (winner === 'death') {
                container.classList.add('victory-death');
            }
        }
    </script>
</body>
</html>

四、系统整合示例

完整的弹幕处理流程

pythonDownloadCopy codeclass DanmuProcessor:
    """弹幕处理中枢"""
    
    def __init__(self, shop: ItemShop, vote_manager: VoteManager, 
                 faction_system: FactionSystem):
        self.shop = shop
        self.vote = vote_manager
        self.faction = faction_system        
        # 命令前缀
        self.commands = {
            "购买": self._handle_purchase,
            "使用": self._handle_use,
            "背包": self._handle_inventory,
            "商店": self._handle_shop,
            "阵营": self._handle_faction,
            "排行": self._handle_leaderboard,
        }
    
    async def process(self, user_id: str, username: str, content: str, 
                      gift_name: str = None, gift_value: int = 0):
        """处理弹幕/礼物"""        
        # 1. 处理礼物
        if gift_name:            # 积分系统
            self.shop.point_system.add_gift_points(user_id, gift_name)            
            # 阵营系统
            self.faction.process_gift(user_id, username, gift_name)            
            # 投票加权
            if self.vote.current_vote:
                self.vote.process_vote(user_id, content, gift_value)
            
            return        
        # 2. 处理命令
        for prefix, handler in self.commands.items():
            if content.startswith(prefix):
                param = content[len(prefix):].strip()
                return await handler(user_id, username, param)        
        # 3. 处理投票
        if self.vote.current_vote:
            result = self.vote.process_vote(user_id, content)
            if result["success"]:
                return result        
        # 4. 普通弹幕        # 添加互动积分
        self.shop.point_system.add_interaction_points(user_id, "chat")
    
    async def _handle_purchase(self, user_id: str, username: str, item_name: str):
        """处理购买命令"""
        result = self.shop.purchase_item(user_id, item_name)        # 广播购买结果
        return result
    
    async def _handle_use(self, user_id: str, username: str, item_name: str):
        """处理使用命令"""
        result = self.shop.use_item(user_id, item_name)
        return result
    
    async def _handle_inventory(self, user_id: str, username: str, _):
        """处理背包查询"""
        return {"message": self.shop.get_user_inventory(user_id)}
    
    async def _handle_shop(self, user_id: str, username: str, _):
        """处理商店查询"""
        return {"message": self.shop.get_shop_display()}
    
    async def _handle_faction(self, user_id: str, username: str, _):
        """处理阵营查询"""
        return {"message": self.faction.get_faction_status()}
    
    async def _handle_leaderboard(self, user_id: str, username: str, param: str):
        """处理排行榜查询"""
        if "求生" in param:
            faction = Faction.SURVIVE
        elif "作死" in param:
            faction = Faction.DEATH
        else:
            faction = None
        return {"message": self.faction.get_leaderboard(faction)}

这套系统设计涵盖了:

  1. 道具商店:完整的积分、道具、背包、效果系统

  2. 弹幕投票:多种投票类型、实时统计、UI显示

  3. 阵营对抗:双阵营对决、能量系统、排行榜、动态效果


返回列表

上一篇:剧本杀直播

没有最新的文章了...

相关文章

剧本杀直播

剧本杀直播完整运营方案第一部分:发展模式与流程规划一、发展阶段路线图第一阶段(1-2个月)         &...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。