feat(phase6): Add sounds, team chat, reactions, monitor, settings, and CSV import/export
Sound System: - Add soundStore with volume/mute persistence - Add useSound hook with Web Audio API fallback - Add SoundControl component for in-game volume adjustment - Play sounds for correct/incorrect, steal, timer, victory/defeat Team Chat: - Add TeamChat component with collapsible panel - Add team_message WebSocket event (team-only visibility) - Store up to 50 messages per session Emoji Reactions: - Add EmojiReactions bar with 8 emojis - Add ReactionOverlay with floating animations (Framer Motion) - Add rate limiting (1 reaction per 3 seconds) - Broadcast reactions to all players in room Admin Monitor: - Add Monitor page showing active rooms from Redis - Display player counts, team composition, status - Add ability to close problematic rooms Admin Settings: - Add Settings page for game configuration - Configure points/times by difficulty, steal penalty, max players - Store config in JSON file with service helpers CSV Import/Export: - Add export endpoint with optional filters - Add import endpoint with validation and error reporting - Add UI buttons and import result modal in Questions page Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import socketio
|
||||
import time
|
||||
from datetime import datetime
|
||||
from app.services.room_manager import room_manager
|
||||
from app.services.game_manager import game_manager
|
||||
@@ -8,6 +9,11 @@ from app.schemas.achievement import PlayerStats
|
||||
from app.models.base import get_async_session
|
||||
|
||||
|
||||
# Rate limiting para reacciones: {room_code: {player_name: last_reaction_timestamp}}
|
||||
reaction_rate_limits: dict[str, dict[str, float]] = {}
|
||||
REACTION_COOLDOWN_SECONDS = 3
|
||||
|
||||
|
||||
async def get_db_session():
|
||||
"""Helper para obtener sesion de BD en contexto de socket."""
|
||||
AsyncSessionLocal = get_async_session()
|
||||
@@ -353,13 +359,56 @@ def register_socket_events(sio: socketio.AsyncServer):
|
||||
)
|
||||
|
||||
@sio.event
|
||||
async def emoji_reaction(sid, data):
|
||||
"""Send an emoji reaction visible to all."""
|
||||
async def team_message(sid, data):
|
||||
"""Send a team chat message - only visible to teammates."""
|
||||
room_code = data.get("room_code", "")
|
||||
team = data.get("team", "")
|
||||
player_name = data.get("player_name", "")
|
||||
message = data.get("message", "")[:500] # Limit message length
|
||||
|
||||
if not all([room_code, team, player_name, message]):
|
||||
return
|
||||
|
||||
# Validate player exists in room
|
||||
player = await room_manager.get_player(sid)
|
||||
if not player or player["room"] != room_code or player["team"] != team:
|
||||
return
|
||||
|
||||
# Get room data to find team members
|
||||
room = await room_manager.get_room(room_code)
|
||||
if not room:
|
||||
return
|
||||
|
||||
# Get socket IDs of team members
|
||||
team_sockets = [
|
||||
p["socket_id"] for p in room["teams"][team]
|
||||
if p.get("socket_id")
|
||||
]
|
||||
|
||||
# Emit only to team members
|
||||
message_data = {
|
||||
"player_name": player_name,
|
||||
"team": team,
|
||||
"message": message,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
for socket_id in team_sockets:
|
||||
await sio.emit(
|
||||
"receive_team_message",
|
||||
message_data,
|
||||
to=socket_id
|
||||
)
|
||||
|
||||
@sio.event
|
||||
async def send_reaction(sid, data):
|
||||
"""Send an emoji reaction visible to all players in the room."""
|
||||
player = await room_manager.get_player(sid)
|
||||
if not player:
|
||||
return
|
||||
|
||||
room_code = player["room"]
|
||||
room_code = data.get("room_code", player["room"])
|
||||
player_name = data.get("player_name", player["name"])
|
||||
emoji = data.get("emoji", "")
|
||||
|
||||
# Validate emoji
|
||||
@@ -367,16 +416,37 @@ def register_socket_events(sio: socketio.AsyncServer):
|
||||
if emoji not in allowed_emojis:
|
||||
return
|
||||
|
||||
# Rate limiting: max 1 reaction every 3 seconds per player
|
||||
current_time = time.time()
|
||||
if room_code not in reaction_rate_limits:
|
||||
reaction_rate_limits[room_code] = {}
|
||||
|
||||
last_reaction = reaction_rate_limits[room_code].get(player_name, 0)
|
||||
if current_time - last_reaction < REACTION_COOLDOWN_SECONDS:
|
||||
# Player is rate limited, ignore the reaction
|
||||
return
|
||||
|
||||
# Update last reaction time
|
||||
reaction_rate_limits[room_code][player_name] = current_time
|
||||
|
||||
# Emit to ALL players in the room (both teams)
|
||||
await sio.emit(
|
||||
"emoji_reaction",
|
||||
"receive_reaction",
|
||||
{
|
||||
"player_name": player["name"],
|
||||
"player_name": player_name,
|
||||
"team": player["team"],
|
||||
"emoji": emoji
|
||||
"emoji": emoji,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
},
|
||||
room=room_code
|
||||
)
|
||||
|
||||
# Keep old event name for backwards compatibility
|
||||
@sio.event
|
||||
async def emoji_reaction(sid, data):
|
||||
"""Alias for send_reaction (backwards compatibility)."""
|
||||
await send_reaction(sid, data)
|
||||
|
||||
@sio.event
|
||||
async def timer_expired(sid, data):
|
||||
"""Handle timer expiration."""
|
||||
|
||||
Reference in New Issue
Block a user