more lulz and features
This commit is contained in:
@@ -15,7 +15,7 @@ if os.path.exists('.env'):
|
||||
|
||||
|
||||
# Version
|
||||
VERSION = '1.0.5'
|
||||
VERSION = '1.0.7'
|
||||
|
||||
# Server settings
|
||||
SERVER_HOST = '0.0.0.0'
|
||||
|
||||
@@ -38,9 +38,7 @@ ALLOWED_CHARS = string.ascii_letters + string.digits + '_-'
|
||||
DIAL_CODES = {
|
||||
'*420#': 'trippy_toggle', # toggles UI trippy mode globally for everyone
|
||||
'*1337#': 'rainbow_nick_toggle', # toggles the dialer's own rainbow nick
|
||||
'*101#': 'knock', # plays a knock sound for everyone in the room
|
||||
'*300#': 'laugh', # plays a laugh sound for everyone in the room
|
||||
'*212#': 'seinfeld', # plays a random 10s slice of the Seinfeld bass riff
|
||||
'*111#': 'sound_menu', # opens the soundboard popup (pick a sound for everyone)
|
||||
'*88#': 'voice_changer', # opens the dialer's voice changer popup (local FX)
|
||||
'*666#': 'schizo_toggle', # toggles schizo mode (subtle UI shake/wiggle/morph)
|
||||
'*9059#': 'pong_toggle', # toggles pong mode (webcam tiles bounce around)
|
||||
@@ -56,9 +54,7 @@ DIAL_CODES = {
|
||||
DIAL_CODE_DESCRIPTIONS = [
|
||||
('*420#', 'Toggle trippy color mode (everyone)'),
|
||||
('*1337#', 'Toggle rainbow nickname (just you)'),
|
||||
('*101#', 'Play a knock sound (everyone)'),
|
||||
('*300#', 'Play a laugh sound (everyone)'),
|
||||
('*212#', 'Play a random 3-5s of the Seinfeld bass (everyone)'),
|
||||
('*111#', 'Open the soundboard - pick a sound for everyone'),
|
||||
('*88#', 'Open the voice changer (just you)'),
|
||||
('*666#', 'Toggle schizo mode (everyone)'),
|
||||
('*9059#', 'Toggle pong mode (everyone)'),
|
||||
@@ -73,13 +69,18 @@ DIAL_CODE_DESCRIPTIONS = [
|
||||
MAX_RECORDING_BYTES = 512 * 1024 # ~500KB cap, plenty for 10s of opus at low bitrate
|
||||
DIAL_MAX_LEN = 32
|
||||
|
||||
# Sounds the soundboard (*111#) may broadcast. 'seinfeld' plays a random slice; the rest
|
||||
# play the whole (already-trimmed) file.
|
||||
SOUNDBOARD = {'knock', 'laugh', 'seinfeld', 'nfl', 'crickets', 'suspend', 'grinder', 'iphone'}
|
||||
|
||||
# Seinfeld bass riff (static/sounds/seinfeld.mp3). A random slice is picked server-side
|
||||
# and broadcast so everyone hears the same clip.
|
||||
SEINFELD_TRACK_LEN = 54 # seconds (actual file is ~54.19s)
|
||||
SEINFELD_CLIP_MIN = 3 # *212# plays a random 3-5s slice
|
||||
SEINFELD_CLIP_MIN = 3 # soundboard plays a random 3-5s slice
|
||||
SEINFELD_CLIP_MAX = 5
|
||||
SEINFELD_JOIN_LEN = 3 # join easter egg plays a 3s slice
|
||||
SEINFELD_JOIN_LEN = 3 # join easter egg plays a 3s Seinfeld slice
|
||||
JOIN_SEINFELD_CHANCE = 0.10 # 1-in-10 joins get Seinfeld instead of the normal join sound
|
||||
JOIN_NFL_CHANCE = 0.10 # a separate 1-in-10 get the NFL sound instead
|
||||
|
||||
|
||||
def random_seinfeld(clip_len: float) -> dict:
|
||||
@@ -89,6 +90,18 @@ def random_seinfeld(clip_len: float) -> dict:
|
||||
return {'start': start, 'duration': round(clip_len, 2)}
|
||||
|
||||
|
||||
def roll_join_sound():
|
||||
'''Roll the join-sound easter egg. Returns a dict describing what to play, or None
|
||||
for the normal join sound. Seinfeld and NFL each get an independent 1-in-10 shot.'''
|
||||
|
||||
r = random.random()
|
||||
if r < JOIN_SEINFELD_CHANCE:
|
||||
return {'kind': 'clip', 'sound': 'seinfeld', **random_seinfeld(SEINFELD_JOIN_LEN)}
|
||||
if r < JOIN_SEINFELD_CHANCE + JOIN_NFL_CHANCE:
|
||||
return {'kind': 'sound', 'sound': 'nfl'}
|
||||
return None
|
||||
|
||||
|
||||
def generate_captcha():
|
||||
'''Generate a random captcha question and answer'''
|
||||
|
||||
@@ -344,19 +357,18 @@ async def handle_message(client_id: str, data: dict):
|
||||
'pong_mode' : pong_mode
|
||||
})
|
||||
|
||||
# 1-in-10 joins swap the normal join sound for a random 3s Seinfeld slice. Rolled
|
||||
# once server-side so the whole room hears the same thing. Only on genuine joins,
|
||||
# not reconnects (which are frequent on mobile).
|
||||
join_seinfeld = random_seinfeld(SEINFELD_JOIN_LEN) if random.random() < JOIN_SEINFELD_CHANCE else None
|
||||
# Join-sound easter egg, rolled once server-side so the whole room hears the same
|
||||
# thing. Only on genuine joins, not reconnects (which are frequent on mobile).
|
||||
join_sound = roll_join_sound()
|
||||
|
||||
await broadcast(client_id, {
|
||||
'type' : 'user_joined',
|
||||
'id' : client_id,
|
||||
'username' : username,
|
||||
'mic_on' : clients[client_id].get('mic_on', True),
|
||||
'cam_on' : clients[client_id].get('cam_on', False),
|
||||
'screen_on' : clients[client_id].get('screen_on', False),
|
||||
'join_seinfeld': join_seinfeld
|
||||
'type' : 'user_joined',
|
||||
'id' : client_id,
|
||||
'username' : username,
|
||||
'mic_on' : clients[client_id].get('mic_on', True),
|
||||
'cam_on' : clients[client_id].get('cam_on', False),
|
||||
'screen_on' : clients[client_id].get('screen_on', False),
|
||||
'join_sound': join_sound
|
||||
})
|
||||
|
||||
elif msg_type == 'reconnect':
|
||||
@@ -479,6 +491,19 @@ async def handle_message(client_id: str, data: dict):
|
||||
'enabled' : enabled
|
||||
})
|
||||
|
||||
elif msg_type == 'play_soundboard':
|
||||
# A user picked a sound from the *111# soundboard popup. Validate against the
|
||||
# allow-list, then fan out. Seinfeld gets a random 3-5s slice; the rest play whole.
|
||||
sound = data.get('sound')
|
||||
if sound not in SOUNDBOARD:
|
||||
return
|
||||
logging.info(f'[{client_id}] Soundboard: {sound}')
|
||||
if sound == 'seinfeld':
|
||||
clip = random_seinfeld(random.uniform(SEINFELD_CLIP_MIN, SEINFELD_CLIP_MAX))
|
||||
await broadcast_all({'type': 'play_clip', 'sound': 'seinfeld', **clip})
|
||||
else:
|
||||
await broadcast_all({'type': 'play_sound', 'sound': sound})
|
||||
|
||||
elif msg_type == 'car_mode':
|
||||
# Car mode = audio-only. We track the flag and fan it out so every other client
|
||||
# pauses the video/screen streams they send to this user (client-side, via
|
||||
@@ -534,17 +559,11 @@ async def handle_message(client_id: str, data: dict):
|
||||
trippy_mode = not trippy_mode
|
||||
logging.info(f'[{client_id}] Trippy mode -> {trippy_mode}')
|
||||
await broadcast_all({'type': 'trippy_status', 'enabled': trippy_mode})
|
||||
elif action == 'knock':
|
||||
logging.info(f'[{client_id}] Knock')
|
||||
await broadcast_all({'type': 'play_sound', 'sound': 'knock'})
|
||||
elif action == 'laugh':
|
||||
logging.info(f'[{client_id}] Laugh')
|
||||
await broadcast_all({'type': 'play_sound', 'sound': 'laugh'})
|
||||
elif action == 'seinfeld':
|
||||
# Pick one random 3-5s slice so the whole room hears the same clip.
|
||||
clip = random_seinfeld(random.uniform(SEINFELD_CLIP_MIN, SEINFELD_CLIP_MAX))
|
||||
logging.info(f'[{client_id}] Seinfeld clip @ {clip["start"]}s for {clip["duration"]}s')
|
||||
await broadcast_all({'type': 'play_clip', 'sound': 'seinfeld', **clip})
|
||||
elif action == 'sound_menu':
|
||||
# Private trigger - only the dialer's soundboard popup opens. Picking a sound
|
||||
# there sends a 'play_soundboard' message that fans out to everyone.
|
||||
logging.info(f'[{client_id}] Soundboard open')
|
||||
await clients[client_id]['ws'].send_json({'type': 'sound_menu_open'})
|
||||
elif action == 'voice_changer':
|
||||
# Private trigger - only the dialer's UI opens the voice changer popup. The FX
|
||||
# are applied client-side to the dialer's own outgoing audio.
|
||||
|
||||
@@ -469,6 +469,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Soundboard popup (opened via *111#). Picking a sound plays it for everyone. -->
|
||||
<div id="sound-menu-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Soundboard</h3>
|
||||
<button id="sound-menu-close" class="modal-close-btn" title="Close">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sound-menu-grid">
|
||||
<button class="sound-btn btn-secondary" data-sound="knock">Knock</button>
|
||||
<button class="sound-btn btn-secondary" data-sound="laugh">Laugh</button>
|
||||
<button class="sound-btn btn-secondary" data-sound="seinfeld">Seinfeld</button>
|
||||
<button class="sound-btn btn-secondary" data-sound="nfl">NFL</button>
|
||||
<button class="sound-btn btn-secondary" data-sound="crickets">Crickets</button>
|
||||
<button class="sound-btn btn-secondary" data-sound="suspend">Suspense</button>
|
||||
<button class="sound-btn btn-secondary" data-sound="grinder">Grinder</button>
|
||||
<button class="sound-btn btn-secondary" data-sound="iphone">iPhone</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dial codes list modal (shown only to the dialer who hits *#06#) -->
|
||||
<div id="dial-codes-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
|
||||
+10
-4
@@ -422,11 +422,13 @@ function handleSignal(data) {
|
||||
console.log('[Signal] user_joined:', data.id, 'micOn:', data.mic_on, 'state:', state.users[data.id]);
|
||||
updateUI();
|
||||
|
||||
// Notification and sound. 1-in-10 joins (rolled server-side) play a random
|
||||
// Seinfeld slice instead of the normal join sound.
|
||||
// Notification and sound. The server may roll a random join-sound easter egg
|
||||
// (Seinfeld slice or NFL); otherwise the normal join sound plays.
|
||||
showNotification('HardChats', `${data.username} joined the room`, 'user-join');
|
||||
if (data.join_seinfeld) {
|
||||
playSoundClip('seinfeld', data.join_seinfeld.start, data.join_seinfeld.duration);
|
||||
if (data.join_sound && data.join_sound.kind === 'clip') {
|
||||
playSoundClip(data.join_sound.sound, data.join_sound.start, data.join_sound.duration);
|
||||
} else if (data.join_sound && data.join_sound.kind === 'sound') {
|
||||
playSound(data.join_sound.sound);
|
||||
} else {
|
||||
playSound('join');
|
||||
}
|
||||
@@ -567,6 +569,10 @@ function handleSignal(data) {
|
||||
openVoiceChanger();
|
||||
break;
|
||||
|
||||
case 'sound_menu_open':
|
||||
openSoundMenu();
|
||||
break;
|
||||
|
||||
case 'request_broadcast_recording':
|
||||
uploadRecording();
|
||||
break;
|
||||
|
||||
@@ -20,6 +20,15 @@ function initDialListeners() {
|
||||
if (e.target.id === 'dial-codes-modal') closeDialCodes();
|
||||
});
|
||||
|
||||
// Soundboard popup (*111#). Buttons stay live so you can fire several sounds.
|
||||
$('sound-menu-close')?.addEventListener('click', closeSoundMenu);
|
||||
$('sound-menu-modal')?.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'sound-menu-modal') closeSoundMenu();
|
||||
});
|
||||
document.querySelectorAll('.sound-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => send({ type: 'play_soundboard', sound: btn.dataset.sound }));
|
||||
});
|
||||
|
||||
document.querySelectorAll('.dial-key').forEach(btn => {
|
||||
btn.addEventListener('click', () => pressDialKey(btn.dataset.key));
|
||||
});
|
||||
@@ -88,6 +97,15 @@ function closeDialCodes() {
|
||||
$('dial-codes-modal')?.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Soundboard popup, opened server-side when the dialer hits *111#.
|
||||
function openSoundMenu() {
|
||||
$('sound-menu-modal')?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeSoundMenu() {
|
||||
$('sound-menu-modal')?.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Apply or remove the trippy-mode class on <body>. Server tells us when to flip via
|
||||
// 'trippy_status' broadcasts; new joiners get the current value in the 'users' message.
|
||||
// We always track the server truth in state.* but only render the effect when NOT in
|
||||
|
||||
@@ -85,7 +85,12 @@ const SOUND_FILES = {
|
||||
leave: '/static/sounds/htp.wav',
|
||||
knock: '/static/sounds/knock.mp3',
|
||||
laugh: '/static/sounds/laugh.mp3',
|
||||
seinfeld: '/static/sounds/seinfeld.mp3'
|
||||
seinfeld: '/static/sounds/seinfeld.mp3',
|
||||
nfl: '/static/sounds/nfl.mp3',
|
||||
crickets: '/static/sounds/crickets.mp3',
|
||||
suspend: '/static/sounds/suspend.mp3',
|
||||
grinder: '/static/sounds/grinder.mp3',
|
||||
iphone: '/static/sounds/iphone.mp3'
|
||||
};
|
||||
const soundElements = {};
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1501,6 +1501,18 @@ body {
|
||||
/* Flip-camera button reads as an enabled action (brighter than the default grey icon). */
|
||||
#flip-cam-btn { color: var(--text-primary); }
|
||||
|
||||
/* Soundboard popup (*111#) */
|
||||
.sound-menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.6rem;
|
||||
padding: 1rem 0 0.25rem;
|
||||
}
|
||||
.sound-menu-grid .sound-btn {
|
||||
padding: 0.85rem 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.video-tile { border-radius: 8px; }
|
||||
.video-tile .username { font-size: 0.75rem; padding: 0.5rem 0.75rem; }
|
||||
|
||||
Reference in New Issue
Block a user