剧本杀直播优化:商店细节、弹幕投票、阵营对抗
互动系统详细设计方案
一、道具商店系统
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 display3. 对决事件配置示例
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)}这套系统设计涵盖了:
道具商店:完整的积分、道具、背包、效果系统
弹幕投票:多种投票类型、实时统计、UI显示
阵营对抗:双阵营对决、能量系统、排行榜、动态效果