Function bodies 456 total
App function · javascript · L14-L31 (18 LOC)client/src/App.jsx
export default function App() {
return (
<GameProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/player/:id" element={<Player />} />
<Route path="/host" element={<Host />} />
<Route path="/screen" element={<Screen />} />
<Route path="/debug" element={<DebugGrid />} />
<Route path="/operator" element={<Operator />} />
<Route path="/slides" element={<SlideEditor />} />
<Route path="/strings" element={<StringSheets />} />
</Routes>
</BrowserRouter>
</GameProvider>
);
}CalibrationModal function · javascript · L8-L374 (367 LOC)client/src/components/CalibrationModal.jsx
export default function CalibrationModal({
isOpen,
onClose,
players,
calibrationState,
hostSettings,
send,
}) {
const [displayResting, setDisplayResting] = useState(65);
const [displayElevated, setDisplayElevated] = useState(110);
const [threshold, setThreshold] = useState(110);
const [countdown, setCountdown] = useState(0);
// Sync display range + threshold from host settings
useEffect(() => {
if (hostSettings) {
setDisplayResting(hostSettings.heartbeatDisplayResting ?? 65);
setDisplayElevated(hostSettings.heartbeatDisplayElevated ?? 110);
setThreshold(hostSettings.heartbeatThreshold ?? 110);
}
}, [hostSettings]);
// Countdown timer
useEffect(() => {
if (!calibrationState || calibrationState.phase === 'review') {
setCountdown(0);
return;
}
const tick = () => {
const remaining = Math.max(0, Math.ceil(
(calibrationState.startTime + calibrationState.duration - Date.now()) / 1000
));
median function · javascript · L376-L381 (6 LOC)client/src/components/CalibrationModal.jsx
function median(arr) {
if (!arr.length) return 0;
const sorted = [...arr].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
}CustomEventModal function · javascript · L8-L181 (174 LOC)client/src/components/CustomEventModal.jsx
export default function CustomEventModal({
isOpen,
onClose,
onSubmit,
availableItems,
availableRoles
}) {
const [mechanism, setMechanism] = useState('vote');
const [rewardType, setRewardType] = useState('item');
const [rewardParam, setRewardParam] = useState('');
if (!isOpen) return null;
const mechanismLabel = MECHANISMS.find(m => m.id === mechanism)?.name || 'Event';
const handleSubmit = (e) => {
e.preventDefault();
// Build description based on reward type
let description = '';
switch (rewardType) {
case 'item':
const item = availableItems.find(i => i.id === rewardParam);
description = `Vote for who receives: ${item?.name || rewardParam}`;
break;
case 'role':
const role = availableRoles.find(r => r.id === rewardParam);
description = `Vote for who becomes: ${role?.name || rewardParam}`;
break;
case 'resurrection':
description = 'Vote for who to resurrect';
breakEventPanel function · javascript · L7-L215 (209 LOC)client/src/components/EventPanel.jsx
export default function EventPanel({
pendingEvents,
activeEvents,
eventProgress,
eventMetadata,
currentPhase,
onStartEvent,
onStartAllEvents,
onResolveEvent,
onResolveAllEvents,
onSkipEvent,
onDebugAutoSelectAll,
onCreateCustomEvent,
onResetEvent,
onStartEventTimer,
timerDuration,
}) {
const [showCustomEventModal, setShowCustomEventModal] = useState(false);
const hasPending = pendingEvents.length > 0;
const hasActive = activeEvents.length > 0;
const isDayPhase = currentPhase === 'day';
const anyUncommitted = activeEvents.some((eventId) => {
const progress = eventProgress[eventId] || {};
return (progress.total || 0) > (progress.responded || 0);
});
const handleAutoSelectAllEvents = () => {
activeEvents.forEach((eventId) => {
const progress = eventProgress[eventId] || {};
if ((progress.total || 0) > (progress.responded || 0)) {
onDebugAutoSelectAll(eventId);
}
});
};
// Convert AVAILABLE_ITEMS tGameLog function · javascript · L5-L57 (53 LOC)client/src/components/GameLog.jsx
export default function GameLog({ entries = [], autoScroll = true }) {
const bottomRef = useRef(null);
const [trimmedAt, setTrimmedAt] = useState(0);
// Auto-scroll to bottom on new entries (skip when panel is off-screen)
useEffect(() => {
if (autoScroll) {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [entries.length, autoScroll]);
const formatTime = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
});
};
const visible = entries.slice(Math.max(trimmedAt, entries.length - 200));
return (
<div className={styles.log}>
<header className={styles.header}>
<h2>Log</h2>
{entries.length > 3 && (
<button
className={styles.clearBtn}
onClick={() => setTrimmedAt(Math.max(trimmedAt, entries.length - 3))}
title='Keep last 3 entries'
>
HeartbeatModal function · javascript · L5-L24 (20 LOC)client/src/components/HeartbeatModal.jsx
export default function HeartbeatModal({ isOpen, onClose, players, onPushHeartbeatSlide }) {
const activePlayers = (players || []).filter(p => p.heartbeat?.active);
return (
<Modal isOpen={isOpen} onClose={onClose} title="HEARTBEAT">
<div className={styles.buttonGroup}>
{activePlayers.map(p => (
<button
key={p.id}
onClick={() => { onPushHeartbeatSlide(p.id); onClose(); }}
>
❤️ {p.name}
<span className={styles.bpm}>{p.heartbeat.bpm} BPM</span>
{p.heartbeat.fake && <span className={styles.debugBadge}>DEBUG</span>}
</button>
))}
</div>
</Modal>
);
}If a scraper extracted this row, it came from Repobility (https://repobility.com)
ItemManagerModal function · javascript · L6-L85 (80 LOC)client/src/components/ItemManagerModal.jsx
export default function ItemManagerModal({
isOpen,
onClose,
player,
onGiveItem,
onRemoveItem,
}) {
if (!player) return null;
const inventory = player.inventory || [];
const hiddenInventory = player.hiddenInventory || [];
return (
<Modal isOpen={isOpen} onClose={onClose} title={`Items — ${player.name}`}>
{/* Current inventory */}
{(inventory.length > 0 || hiddenInventory.length > 0) && (
<div className={styles.section}>
<h3 className={styles.sectionTitle}>Holding</h3>
<div className={styles.itemList}>
{inventory.map((item, idx) => {
const display = ITEM_DISPLAY[item.id];
return (
<div key={idx} className={styles.itemRow} title={display?.description}>
<span className={styles.itemEmoji}>{display?.emoji || '?'}</span>
<span className={styles.itemName}>{display?.name || item.id}</span>
<span className={styles.itemUses}>Modal function · javascript · L6-L27 (22 LOC)client/src/components/Modal.jsx
export default function Modal({
isOpen,
onClose,
title,
children,
}) {
if (!isOpen) return null;
return (
<div className={styles.overlay} onClick={onClose}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<header className={styles.header}>
<h2>{title}</h2>
<button className={styles.closeBtn} onClick={onClose}>✕</button>
</header>
<div className={styles.content}>
{children}
</div>
</div>
</div>
);
}parseXbm function · javascript · L17-L30 (14 LOC)client/src/components/PixelGlyph.jsx
function parseXbm(data) {
const grid = []
for (let row = 0; row < ICON_SIZE; row++) {
const rowPixels = []
const offset = row * BYTES_PER_ROW
for (let col = 0; col < ICON_SIZE; col++) {
const byteIdx = offset + Math.floor(col / 8)
const bitIdx = col % 8
rowPixels.push((data[byteIdx] >> bitIdx) & 1)
}
grid.push(rowPixels)
}
return grid
}generatePixelMaps function · javascript · L33-L49 (17 LOC)client/src/components/PixelGlyph.jsx
function generatePixelMaps() {
const base = []
const phase = []
const speed = []
for (let row = 0; row < ICON_SIZE; row++) {
const bRow = [], pRow = [], sRow = []
for (let col = 0; col < ICON_SIZE; col++) {
bRow.push(0.7 + Math.random() * 0.3) // range 0.7–1.0
pRow.push(Math.random() * Math.PI * 2)
sRow.push(1.5 + Math.random() * 1.5) // 1.5–3.0 radians/sec
}
base.push(bRow)
phase.push(pRow)
speed.push(sRow)
}
return { base, phase, speed }
}drawChannel function · javascript · L52-L84 (33 LOC)client/src/components/PixelGlyph.jsx
function drawChannel(ctx, grid, maps, time, channelR, channelG, channelB) {
ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE)
const imageData = ctx.createImageData(CANVAS_SIZE, CANVAS_SIZE)
const pixels = imageData.data
for (let row = 0; row < ICON_SIZE; row++) {
const gradientFactor = 1.0 - 0.3 * (row / (ICON_SIZE - 1))
for (let col = 0; col < ICON_SIZE; col++) {
if (!grid[row][col]) continue
const flicker = Math.sin(time * maps.speed[row][col] + maps.phase[row][col]) * FLICKER_AMPLITUDE
const intensity = Math.max(0.6, Math.min(1.0, maps.base[row][col] + flicker)) * gradientFactor
const r = Math.round(channelR * intensity)
const g = Math.round(channelG * intensity)
const b = Math.round(channelB * intensity)
const cellX = col * CELL
const cellY = row * CELL
for (let py = 0; py < PIXEL_SIZE; py++) {
for (let px = 0; px < PIXEL_SIZE; px++) {
const idx = ((cellY + py) * CANVAS_SIZE + (cellX + px)) * 4
PixelGlyph function · javascript · L86-L141 (56 LOC)client/src/components/PixelGlyph.jsx
export default function PixelGlyph({ iconId, size, children }) {
const iconData = Icons[iconId]
const mapsRef = useRef(null)
const glowRef = useRef(null)
const redRef = useRef(null)
const greenRef = useRef(null)
const blueRef = useRef(null)
const rafRef = useRef(null)
if (!mapsRef.current) {
mapsRef.current = generatePixelMaps()
}
useEffect(() => {
if (!iconData) return
const grid = parseXbm(iconData)
const { r, g, b } = AMBER
const maps = mapsRef.current
const glowCtx = glowRef.current?.getContext('2d')
const redCtx = redRef.current?.getContext('2d')
const greenCtx = greenRef.current?.getContext('2d')
const blueCtx = blueRef.current?.getContext('2d')
let startTime = null
const animate = (timestamp) => {
if (!startTime) startTime = timestamp
const time = (timestamp - startTime) / 1000
if (glowCtx) drawChannel(glowCtx, grid, maps, time, r, g, b)
if (redCtx) drawChannel(redCtx, grid, maps, time,PlayerConsole function · javascript · L9-L140 (132 LOC)client/src/components/PlayerConsole.jsx
export default function PlayerConsole({
player,
gameState,
eventPrompt,
selectedTarget,
confirmedTarget,
abstained,
hasActiveEvent,
onSwipeUp,
onSwipeDown,
onConfirm,
onAbstain,
onUseItem,
onIdleScrollUp,
onIdleScrollDown,
connected,
compact = false,
}) {
const isAlive = player?.status === PlayerStatus.ALIVE;
const isDead = player?.status === PlayerStatus.DEAD;
const phase = gameState?.phase;
// Derive idle state from server data (no active event, alive, in-game)
const isIdle = !hasActiveEvent && isAlive && phase !== GamePhase.LOBBY && phase !== GamePhase.GAME_OVER;
// Get current icon data from display state
const icons = player?.display?.icons;
const idleScrollIndex = player?.display?.idleScrollIndex ?? 0;
const currentIconSlot = icons?.[idleScrollIndex];
// Handle UP button (rotary switch increment)
const handleUp = () => {
if (isIdle) {
onIdleScrollUp?.();
} else if (hasActiveEvent) {
onSwipeUp();
}
getCardStateKey function · javascript · L16-L61 (46 LOC)client/src/components/PlayerGrid.jsx
function getCardStateKey(props) {
const {
player,
isAlive,
isActive,
hasUncommittedSelection,
isLobby,
isEditing,
editedName,
events,
targeters,
} = props;
// Build a string key from all data that affects rendering
const invKey = [
...(player.inventory || []).map((i) => `${i.id}:${i.uses}`),
...(player.hiddenInventory || []).map((i) => `~${i.id}`),
].join(',');
const eventsKey = events.join(',');
const targetersKey = targeters
.map((t) => `${t.odId}:${t.confirmed}`)
.join(',');
return [
player.id,
player.name,
player.portrait,
player.seatNumber,
player.connected,
player.terminalConnected,
player.role,
player.roleName,
player.roleColor,
player.preAssignedRole,
isAlive,
isActive,
hasUncommittedSelection,
isLobby,
isEditing,
isEditing ? editedName : '', // Only include editedName when editing this card
invKey,
eventsKey,
targetersKey,
playerProvenance: Repobility (https://repobility.com) — every score reproducible from /scan/
playerCardPropsAreEqual function · javascript · L63-L65 (3 LOC)client/src/components/PlayerGrid.jsx
function playerCardPropsAreEqual(prevProps, nextProps) {
return getCardStateKey(prevProps) === getCardStateKey(nextProps);
}PlayerGrid function · javascript · L287-L462 (176 LOC)client/src/components/PlayerGrid.jsx
export default function PlayerGrid({
players,
eventParticipants,
eventProgress,
isLobby,
onKill,
onRevive,
onKick,
onGiveItem,
onRemoveItem,
onChangeRole,
onPreAssignRole,
onDebugAutoSelect,
onSetName,
onSetPortrait,
}) {
const [editingPlayerId, setEditingPlayerId] = useState(null);
const [editedName, setEditedName] = useState('');
const [portraitModalPlayer, setPortraitModalPlayer] = useState(null);
const [itemModalPlayer, setItemModalPlayer] = useState(null);
// Memoize event participation lookup
const playerEvents = useMemo(() => {
const eventsMap = {};
for (const player of players) {
const events = [];
for (const [eventId, participants] of Object.entries(eventParticipants)) {
if (participants.includes(player.id)) {
events.push(eventId);
}
}
eventsMap[player.id] = events;
}
return eventsMap;
}, [players, eventParticipants]);
// Memoize targeting data for all players
consPortraitSelectorModal function · javascript · L13-L67 (55 LOC)client/src/components/PortraitSelectorModal.jsx
export default function PortraitSelectorModal({
isOpen,
onClose,
onSelect,
currentPortrait,
playerName,
}) {
const [selectedPortrait, setSelectedPortrait] = useState(currentPortrait);
const handleSubmit = () => {
if (selectedPortrait && selectedPortrait !== currentPortrait) {
onSelect(selectedPortrait);
}
onClose();
};
return (
<Modal isOpen={isOpen} onClose={onClose} title={`Select Portrait for ${playerName}`}>
<div className={styles.grid}>
{AVAILABLE_PORTRAITS.map((portrait) => (
<div
key={portrait}
className={`${styles.portraitOption} ${
selectedPortrait === portrait ? styles.selected : ''
}`}
onClick={() => setSelectedPortrait(portrait)}
>
<img
src={`/images/players/${portrait}`}
alt={portrait}
className={styles.portraitImage}
/>
{selectedPortrait === portrait && (
ScoresModal function · javascript · L6-L73 (68 LOC)client/src/components/ScoresModal.jsx
export default function ScoresModal({
isOpen,
onClose,
players = [],
scores = {},
onSetScore,
scoringConfig = { survived: 1, winningTeam: 1, bestInvestigator: 2 },
onScoringConfigChange,
}) {
return (
<Modal isOpen={isOpen} onClose={onClose} title="SCORES">
<div className={styles.sections}>
<section className={styles.section}>
<h3>Scoring Rules</h3>
{[
{ key: 'survived', label: getStr('host', 'scoring.survived') },
{ key: 'winningTeam', label: getStr('host', 'scoring.winningTeam') },
{ key: 'bestInvestigator', label: getStr('host', 'scoring.bestInvestigator') },
].map(({ key, label }) => (
<div key={key} className={styles.scoringRow}>
<span className={styles.scoringLabel}>{label}</span>
<input
type="number"
min="0"
max="99"
value={scoringConfig[key] ?? 0}
onChange={(e)ScreenPreview function · javascript · L8-L36 (29 LOC)client/src/components/ScreenPreview.jsx
export default function ScreenPreview() {
const containerRef = useRef(null)
const [scale, setScale] = useState(1)
useEffect(() => {
const el = containerRef.current
if (!el) return
const ro = new ResizeObserver(([entry]) => {
setScale(entry.contentRect.width / NATIVE_W)
})
ro.observe(el)
return () => ro.disconnect()
}, [])
return (
<div
ref={containerRef}
className={styles.container}
style={{ height: Math.round(NATIVE_H * scale) }}
>
<iframe
src='/screen'
className={styles.frame}
style={{ transform: `scale(${scale})`, transformOrigin: 'top left' }}
title='Screen preview'
/>
</div>
)
}presetRoleSummary function · javascript · L8-L21 (14 LOC)client/src/components/SettingsModal.jsx
function presetRoleSummary(preset) {
const playerCount = Object.keys(preset.players ?? {}).length;
const mode = preset.roleMode || 'random';
if (mode === 'assigned' && preset.roleAssignments) {
const roles = Object.values(preset.roleAssignments)
.map(id => ROLE_DISPLAY[id]?.emoji ?? '?')
.join('');
return `${playerCount}p · ${roles}`;
}
if (preset.rolePool) {
return `${playerCount}p · random (${preset.rolePool.length})`;
}
return `${playerCount}p`;
}SettingsModal function · javascript · L23-L144 (122 LOC)client/src/components/SettingsModal.jsx
export default function SettingsModal({
isOpen,
onClose,
presets = [],
onSavePreset,
onLoadPreset,
onDeletePreset,
defaultPresetId = null,
onSetDefault,
timerDuration,
onTimerDurationChange,
onOpenCalibration,
onOpenScores,
}) {
const [newPresetName, setNewPresetName] = useState('');
const handleSave = () => {
const name = newPresetName.trim();
if (!name) return;
const existing = presets.find(p => p.name.toLowerCase() === name.toLowerCase());
if (existing) {
if (!window.confirm(`A preset named "${existing.name}" already exists. Overwrite it?`)) return;
onSavePreset(name, existing.id);
} else {
onSavePreset(name);
}
setNewPresetName('');
};
const handleKeyDown = (e) => {
if (e.key === 'Enter') handleSave();
};
return (
<Modal isOpen={isOpen} onClose={onClose} title="SETTINGS">
<div className={styles.sections}>
<section className={styles.section}>
<h3>Game Presets</h3>
SlideControls function · javascript · L5-L65 (61 LOC)client/src/components/SlideControls.jsx
export default function SlideControls({
slideQueue,
onNext,
onPrev,
onClear,
autoAdvanceEnabled,
onToggleAutoAdvance,
}) {
const { queue = [], currentIndex = -1 } = slideQueue;
const total = queue.length;
const current = currentIndex + 1;
const hasSlides = total > 0;
const canPrev = currentIndex > 0;
const canNext = currentIndex < total - 1;
return (
<section className={styles.controls}>
<h2>Slides</h2>
<div className={styles.counter}>
{hasSlides ? `${current} / ${total}` : 'No slides'}
</div>
<div className={styles.buttons}>
<button
onClick={onPrev}
disabled={!canPrev}
title="Previous slide"
>
◀ Prev
</button>
<button
onClick={onNext}
disabled={!canNext}
className={canNext ? 'primary' : ''}
title="Next slide"
>
Next ▶
</button>
</div>
<button
className={styles.cleaOpen data scored by Repobility · https://repobility.com
BestSuspectSlide function · javascript · L7-L55 (49 LOC)client/src/components/slides/BestSuspectSlide.jsx
export default function BestSuspectSlide({ slide }) {
const [showBadge, setShowBadge] = useState(false)
useEffect(() => {
setShowBadge(false)
const timer = setTimeout(() => setShowBadge(true), 1500)
return () => clearTimeout(timer)
}, [slide.id])
const winner = slide.winner // { id, name, portrait }
const suspects = slide.suspects || [] // [{ name, portrait, wasCorrect }]
const points = slide.points || 0
return (
<div key={slide.id} className={styles.slide}>
<h1 className={styles.scoreUpdateTitle} style={{ fontSize: fitFontSize(slide.title, 6) }}>
{slide.title}
</h1>
{winner && (
<div className={styles.bestSuspectHero}>
<div className={styles.portraitWrap}>
<img
src={`/images/players/${winner.portrait}`}
alt={winner.name}
className={styles.largePortrait}
/>
{showBadge && (
<div className={styles.scoreBadgeLarge}>+{pointCompositionSlide function · javascript · L8-L71 (64 LOC)client/src/components/slides/CompositionSlide.jsx
export default function CompositionSlide({ slide, strings = SLIDE_STRINGS.composition }) {
const { roles = [], teamCounts = {} } = slide
const cellRoles = roles.filter((r) => r.team === 'cell')
const circleRoles = roles.filter((r) => r.team === 'circle')
const unassignedCount = teamCounts.unassigned || 0
const pluralize = (name, count) => {
if (count <= 1) return name
if (name.endsWith('y') && !/[aeiou]y$/i.test(name)) return `${name.slice(0, -1)}ies`
return `${name}s`
}
const renderRoleCluster = (role, index) => (
<div key={role.roleId} className={`${styles.compCluster} ${index > 0 ? styles.compClusterSep : ''}`}>
<div className={styles.compClusterEmojis}>
{Array(role.count)
.fill(null)
.map((_, i) => (
<span key={i} className={styles.compEmoji}>
{USE_PIXEL_GLYPHS ? (
<PixelGlyph iconId={role.roleId} size="6vw">
{role.roleEmoji}
</PixelGlyph>
CountdownSlide function · javascript · L5-L13 (9 LOC)client/src/components/slides/CountdownSlide.jsx
export default function CountdownSlide({ slide }) {
return (
<div key={slide.id} className={styles.slide}>
{slide.title && <h1 className={styles.title} style={{ fontSize: fitFontSize(slide.title) }}>{slide.title}</h1>}
<div className={styles.countdown}>{slide.seconds || 0}</div>
{slide.subtitle && <p className={styles.subtitle}>{slide.subtitle}</p>}
</div>
)
}DeathSlide function · javascript · L7-L95 (89 LOC)client/src/components/slides/DeathSlide.jsx
export default function DeathSlide({ slide, players, strings = SLIDE_STRINGS.death }) {
const player = players?.find(p => p.id === slide.playerId)
if (!player) return null
if (slide.coward) {
return (
<div key={slide.id} className={`${styles.slide} ${styles.deathSlide}`}>
<h1
className={styles.title}
style={{ fontSize: fitFontSize(slide.title), color: getSlideColor(slide, SlideStyle.WARNING) }}
>
{slide.title}
</h1>
<div className={styles.deathReveal}>
<div className={styles.cowardPortraitWrap}>
<img
src={`/images/players/${player.portrait}`}
alt={player.name}
className={`${styles.largePortrait} ${styles.cowardPortrait}`}
/>
<div className={styles.cowardBadgeLarge}>{strings.coward}</div>
{player.hasNovote && <div className={styles.tooMadBadgeLarge}>{strings.mad}</div>}
</div>
<h2 className=FallbackSlide function · javascript · L6-L44 (39 LOC)client/src/components/slides/FallbackSlide.jsx
export default function FallbackSlide({ gameState, strings = SLIDE_STRINGS.fallback }) {
const phase = gameState?.phase
if (!phase || phase === GamePhase.LOBBY) {
return (
<div className={styles.slide}>
<h1 className={styles.title}>{strings.title}</h1>
<p className={styles.subtitle}>
{strings.players.replace('{n}', gameState?.players?.length || 0)}
</p>
</div>
)
}
return (
<div className={styles.slide}>
<h1 className={styles.title}>
{phase === GamePhase.DAY
? `${strings.day} ${gameState.dayCount}`
: `${strings.night} ${gameState.dayCount}`}
</h1>
<div className={styles.gallery}>
{gameState?.players?.map((p) => {
const isDead = p.status !== PlayerStatus.ALIVE
return (
<div
key={p.id}
className={`${styles.playerThumb} ${isDead ? styles.dead : ''} ${p.isCowering && !isDead ? styles.cowering : ''}`}
AnimatedBpm function · javascript · L10-L35 (26 LOC)client/src/components/slides/GallerySlide.jsx
function AnimatedBpm({ value, threshold }) {
const [displayed, setDisplayed] = useState(value)
const displayedRef = useRef(value)
const targetRef = useRef(value)
targetRef.current = value
useEffect(() => {
const tick = setInterval(() => {
const target = targetRef.current
const current = displayedRef.current
if (current === target) return
const next = current + Math.sign(target - current)
displayedRef.current = next
setDisplayed(next)
}, 30)
return () => clearInterval(tick)
}, [])
return (
<span
className={`${styles.thumbBpm} ${displayed >= threshold ? styles.thumbBpmDanger : ''}`}
style={{ color: bpmColor(displayed, threshold) }}
>{displayed}</span>
)
}GallerySlide function · javascript · L40-L226 (187 LOC)client/src/components/slides/GallerySlide.jsx
export default function GallerySlide({ slide, players, gameState, eventTimers, strings = SLIDE_STRINGS.gallery }) {
const getPlayer = (id) => players?.find(p => p.id === id)
const allPlayers = gameState?.players || []
const deadCellMembers = useMemo(
() =>
allPlayers
.filter(p => p.status !== PlayerStatus.ALIVE && p.roleTeam === 'cell')
.sort((a, b) => (a.deathTimestamp || 0) - (b.deathTimestamp || 0)),
[allPlayers],
)
const heartbeatMode = gameState?.heartbeatMode
const heartbeatThreshold = gameState?.heartbeatThreshold ?? 110
// ── Timer gallery ────────────────────────────────────────────────────────────
const [timerDisplay, setTimerDisplay] = useState(null)
useEffect(() => {
if (!slide.timerEventId) {
setTimerDisplay(null)
return
}
// Production: use eventTimers from game state
if (eventTimers) {
const entries = Object.entries(eventTimers)
if (entries.length === 0) { setTimerDisplay(null); HeartbeatGraph function · javascript · L10-L159 (150 LOC)client/src/components/slides/HeartbeatSlide.jsx
function HeartbeatGraph({ bpm, active }) {
const canvasRef = useRef(null)
const animRef = useRef(null)
const bpmRef = useRef(bpm || 72)
const activeRef = useRef(active)
const historyRef = useRef([])
const lastSampleRef = useRef(0)
bpmRef.current = bpm
activeRef.current = active
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
const W = canvas.width
const H = canvas.height
const PAD_TOP = 30
const PAD_BOT = 20
const graphH = H - PAD_TOP - PAD_BOT
const startTime = performance.now()
historyRef.current = [{ time: startTime, bpm: bpmRef.current }]
lastSampleRef.current = startTime
const draw = (now) => {
const currentBpm = bpmRef.current
const isActive = activeRef.current
if (now - lastSampleRef.current >= BPM_SAMPLE_INTERVAL) {
historyRef.current.push({ time: now, bpm: isActive ? currentBpm : -1 })
lastSamRepobility · code-quality intelligence · https://repobility.com
AnimatedBpm function · javascript · L161-L186 (26 LOC)client/src/components/slides/HeartbeatSlide.jsx
function AnimatedBpm({ value, threshold }) {
const [displayed, setDisplayed] = useState(value)
const displayedRef = useRef(value)
const targetRef = useRef(value)
targetRef.current = value
useEffect(() => {
const tick = setInterval(() => {
const target = targetRef.current
const current = displayedRef.current
if (current === target) return
const next = current + Math.sign(target - current)
displayedRef.current = next
setDisplayed(next)
}, 30)
return () => clearInterval(tick)
}, [])
return (
<span
className={`${styles.thumbBpm} ${displayed >= threshold ? styles.thumbBpmDanger : ''}`}
style={{ color: bpmColor(displayed, threshold) }}
>{displayed}</span>
)
}HeartbeatSlide function · javascript · L188-L223 (36 LOC)client/src/components/slides/HeartbeatSlide.jsx
export default function HeartbeatSlide({ slide, gameState, strings = SLIDE_STRINGS.heartbeat }) {
const livePlayer = gameState?.players?.find(p => p.id === slide.playerId)
const liveActive = livePlayer?.heartbeat?.active ?? true
const liveBpm = liveActive ? (livePlayer?.heartbeat?.bpm || slide.bpm || 72) : 0
const isDebug = livePlayer?.heartbeat?.fake ?? slide.fake ?? false
return (
<div className={`${styles.slide} ${styles.heartbeatSlide}`}>
<div className={styles.heartbeatHeader}>
<div className={styles.portraitWrap}>
<img
src={`/images/players/${slide.portrait}`}
alt={slide.playerName}
className={styles.heartbeatPortrait}
/>
{livePlayer?.hasNovote && <div className={styles.tooMadBadgeLarge}>{strings.mad ?? SLIDE_STRINGS.death.mad}</div>}
</div>
<span className={styles.heartbeatName}>{slide.playerName}</span>
{isDebug && <span className={styles.heartbeatDebugBaItemTipSlide function · javascript · L8-L37 (30 LOC)client/src/components/slides/ItemTipSlide.jsx
export default function ItemTipSlide({ slide, strings = SLIDE_STRINGS.roleTip }) {
const itemColor = '#d4af37'
const usesLabel = slide.maxUses === -1
? strings.passive
: slide.maxUses === 1
? strings.singleUse
: `${slide.maxUses} ${strings.uses}`
return (
<div key={slide.id} className={styles.slide}>
{slide.title && <h1 className={styles.title} style={{ fontSize: fitFontSize(slide.title) }}>{slide.title}</h1>}
<div className={styles.roleEmoji}>
{USE_PIXEL_GLYPHS ? (
<PixelGlyph iconId={slide.itemId} size="15vw">
{slide.itemEmoji}
</PixelGlyph>
) : slide.itemEmoji}
</div>
<h1 className={styles.title} style={{ fontSize: fitFontSize(slide.itemName), color: itemColor }}>
{slide.itemName}
</h1>
<div className={styles.badgeRow}>
<div className={styles.abilityBadge} style={{ borderColor: itemColor, color: itemColor }}>
{usesLabel}
</div>
</OperatorReveal function · javascript · L7-L80 (74 LOC)client/src/components/slides/OperatorSlide.jsx
function OperatorReveal({ words, slideId }) {
const WORD_INTERVAL = 1100
const GLITCH_DURATION = 480
const FLICKER_MS = 65
const START_DELAY = 1600
const [phases, setPhases] = useState(() => words.map(() => 'hidden'))
const [glitchTexts, setGlitchTexts] = useState(() => words.map(() => ''))
const [glitchStyles, setGlitchStyles] = useState(() => words.map(() => ({})))
useEffect(() => {
setPhases(words.map(() => 'hidden'))
setGlitchTexts(words.map(() => ''))
setGlitchStyles(words.map(() => ({})))
const clearFns = []
words.forEach((word, i) => {
const jitter = Math.floor(Math.random() * 180)
const glitchAt = START_DELAY + i * WORD_INTERVAL + jitter - GLITCH_DURATION
const revealAt = START_DELAY + i * WORD_INTERVAL + jitter
const glitchTimer = setTimeout(() => {
setPhases(prev => { const n = [...prev]; n[i] = 'glitch'; return n })
const iid = setInterval(() => {
const len = OperatorSlide function · javascript · L82-L90 (9 LOC)client/src/components/slides/OperatorSlide.jsx
export default function OperatorSlide({ slide }) {
const words = slide.words || []
return (
<div key={slide.id} className={`${styles.slide} ${styles.operatorSlide}`}>
<p className={styles.operatorEyebrow}>{slide.title}</p>
<OperatorReveal words={words} slideId={slide.id} />
</div>
)
}PlayerRevealSlide function · javascript · L6-L60 (55 LOC)client/src/components/slides/PlayerRevealSlide.jsx
export default function PlayerRevealSlide({ slide, players }) {
const player = players?.find(p => p.id === slide.playerId)
if (!player) return null
const voters = (slide.voterIds || []).map(id => players?.find(p => p.id === id)).filter(Boolean)
return (
<div key={slide.id} className={styles.slide}>
{slide.title && (
<h1 className={styles.title} style={{ fontSize: fitFontSize(slide.title), color: getSlideColor(slide) }}>
{slide.title}
</h1>
)}
<div className={styles.playerReveal}>
<div className={styles.portraitWrap}>
<img
src={`/images/players/${player.portrait}`}
alt={player.name}
className={styles.largePortrait}
/>
{slide.jesterWon && <div className={styles.winnerBadgeLarge}>WINNER</div>}
{!slide.jesterWon && player.isCowering && <div className={styles.cowardBadgeLarge}>{SLIDE_STRINGS.death.coward}</div>}
{!slide.jesterWon && player.hRoleTipSlide function · javascript · L8-L55 (48 LOC)client/src/components/slides/RoleTipSlide.jsx
export default function RoleTipSlide({ slide, strings = SLIDE_STRINGS.roleTip }) {
const isCell = slide.team === 'cell'
const isNeutral = slide.team === 'neutral'
const teamColor = isCell ? '#c94c4c' : isNeutral ? '#e8a020' : '#7eb8da'
const teamLabel = isCell ? (strings.cell ?? 'CELL') : isNeutral ? strings.independent : (strings.circle ?? 'CIRCLE')
return (
<div
key={slide.id}
className={`${styles.slide} ${isCell ? styles.cellTip : ''}`}
>
{slide.title && <h1 className={styles.title} style={{ fontSize: fitFontSize(slide.title) }}>{slide.title}</h1>}
<div className={styles.roleEmoji}>
{USE_PIXEL_GLYPHS ? (
<PixelGlyph iconId={slide.roleId} size="15vw">
{slide.roleEmoji}
</PixelGlyph>
) : slide.roleEmoji}
</div>
<h1 className={styles.title} style={{ fontSize: fitFontSize(slide.roleName), color: slide.roleColor }}>
{slide.roleName}
</h1>
<div className={styles.badgeAnimatedScore function · javascript · L7-L28 (22 LOC)client/src/components/slides/ScoresSlide.jsx
function AnimatedScore({ from, to, animate }) {
const [value, setValue] = useState(from)
const ref = useRef(null)
useEffect(() => {
if (!animate) { setValue(from); return }
if (from === to) { setValue(to); return }
const duration = 800
const start = performance.now()
const tick = (now) => {
const t = Math.min((now - start) / duration, 1)
// ease-out quad
const eased = 1 - (1 - t) * (1 - t)
setValue(Math.round(from + (to - from) * eased))
if (t < 1) ref.current = requestAnimationFrame(tick)
}
ref.current = requestAnimationFrame(tick)
return () => cancelAnimationFrame(ref.current)
}, [from, to, animate])
return <>{value}</>
}If a scraper extracted this row, it came from Repobility (https://repobility.com)
ScoresSlide function · javascript · L35-L136 (102 LOC)client/src/components/slides/ScoresSlide.jsx
export default function ScoresSlide({ slide, strings = SLIDE_STRINGS.scores }) {
const entries = slide.entries || []
const previousEntries = slide.previousEntries || null
const hasAnimation = previousEntries && previousEntries.length > 0
const [phase, setPhase] = useState(hasAnimation ? PHASE_INIT : PHASE_SHUFFLE)
useEffect(() => {
if (!hasAnimation) { setPhase(PHASE_SHUFFLE); return }
setPhase(PHASE_INIT)
const t1 = setTimeout(() => setPhase(PHASE_COUNT), 600)
const t2 = setTimeout(() => setPhase(PHASE_SHUFFLE), 1800)
return () => { clearTimeout(t1); clearTimeout(t2) }
}, [slide.id, hasAnimation])
if (!hasAnimation) {
// Static scoreboard (no previous data)
return (
<div key={slide.id} className={`${styles.slide} ${styles.scoreSlide}`}>
<h1 className={styles.scoreTitle}>{slide.title || strings.title}</h1>
<div className={styles.scoreTable}>
{entries.map((entry, i) => (
<div key={entry.name} classNScoreUpdateSlide function · javascript · L7-L43 (37 LOC)client/src/components/slides/ScoreUpdateSlide.jsx
export default function ScoreUpdateSlide({ slide }) {
const [showBadges, setShowBadges] = useState(false)
useEffect(() => {
setShowBadges(false)
const timer = setTimeout(() => setShowBadges(true), 1500)
return () => clearTimeout(timer)
}, [slide.id])
const groups = slide.groups || []
return (
<div key={slide.id} className={styles.slide}>
<h1 className={styles.scoreUpdateTitle} style={{ fontSize: fitFontSize(slide.title, 6) }}>
{slide.title}
</h1>
{groups.map((group, gi) => (
<div key={gi} className={styles.scoreUpdateGroup}>
<h2 className={styles.scoreUpdateGroupLabel}>{group.label}</h2>
<div className={styles.gallery}>
{group.players.map((p) => (
<div key={p.id} className={styles.playerThumb}>
<div className={styles.portraitWrap}>
<img src={`/images/players/${p.portrait}`} alt={p.name} />
{showBadges && (
<bpmColor function · javascript · L19-L28 (10 LOC)client/src/components/slides/slideUtils.js
export function bpmColor(bpm, threshold) {
if (bpm >= threshold) return BPM_COLOR.DANGER
const start = threshold * BPM_COLOR.RAMP_START
if (bpm <= start) return BPM_COLOR.NEUTRAL
const t = (bpm - start) / (threshold - start) // 0 → 1
const hue = Math.round(BPM_COLOR.HUE_YELLOW * (1 - t)) // yellow → red
const sat = Math.round(BPM_COLOR.SAT_START + t * BPM_COLOR.SAT_RANGE)
const lit = Math.round(BPM_COLOR.LIT_START - t * BPM_COLOR.LIT_RANGE)
return `hsl(${hue}, ${sat}%, ${lit}%)`
}fitFontSize function · javascript · L32-L36 (5 LOC)client/src/components/slides/slideUtils.js
export function fitFontSize(text, maxVw = 8) {
if (!text) return `${maxVw}vw`
const sized = 90 / (String(text).length * 0.65)
return `${Math.min(maxVw, sized).toFixed(2)}vw`
}getSlideColor function · javascript · L39-L42 (4 LOC)client/src/components/slides/slideUtils.js
export function getSlideColor(slide, defaultStyle = SlideStyle.NEUTRAL) {
const slideStyle = slide.style || defaultStyle
return SlideStyleColors[slideStyle]
}TitleSlide function · javascript · L6-L26 (21 LOC)client/src/components/slides/TitleSlide.jsx
export default function TitleSlide({ slide, players }) {
const player = slide.playerId ? players?.find(p => p.id === slide.playerId) : null
return (
<div className={styles.slide}>
{player && (
<div className={styles.portraitWrap}>
<img
src={`/images/players/${player.portrait}`}
alt={player.name}
className={styles.largePortrait}
/>
{player.isCowering && <div className={styles.cowardBadgeLarge}>{SLIDE_STRINGS.death.coward}</div>}
{player.hasNovote && <div className={styles.tooMadBadgeLarge}>{SLIDE_STRINGS.death.mad}</div>}
</div>
)}
<h1 className={styles.title} style={{ fontSize: fitFontSize(slide.title) }}>{slide.title}</h1>
{slide.subtitle && <p className={styles.subtitle}>{slide.subtitle}</p>}
</div>
)
}VictorySlide function · javascript · L5-L36 (32 LOC)client/src/components/slides/VictorySlide.jsx
export default function VictorySlide({ slide }) {
return (
<div key={slide.id} className={`${styles.slide} ${styles.victorySlide}`}>
<h1
className={styles.victoryTitle}
style={{ color: getSlideColor(slide) }}
>
{slide.title}
</h1>
{slide.subtitle && <p className={styles.subtitle}>{slide.subtitle}</p>}
{slide.winners && slide.winners.length > 0 && (
<div className={styles.victoryGallery}>
{slide.winners.map((w) => (
<div
key={w.id}
className={`${styles.playerThumb} ${!w.isAlive ? styles.dead : ''}`}
>
<img src={`/images/players/${w.portrait}`} alt={w.name} />
<span className={styles.thumbName}>{w.name}</span>
<span
className={styles.victoryRole}
style={{ color: w.roleColor }}
>
{w.roleName}
</span>
</div>
))}
</diVoteTallySlide function · javascript · L5-L59 (55 LOC)client/src/components/slides/VoteTallySlide.jsx
export default function VoteTallySlide({ slide, players }) {
const { tally, voters, frontrunners, anonymousVoting, title, subtitle } = slide
const getPlayer = (id) => players?.find(p => p.id === id)
const sorted = Object.entries(tally || {})
.map(([id, count]) => ({
player: getPlayer(id),
count,
voterIds: voters?.[id] || [],
isFrontrunner: frontrunners?.includes(id) || frontrunners?.includes(Number(id)) || false,
}))
.filter((entry) => entry.player)
.sort((a, b) => b.count - a.count)
return (
<div key={slide.id} className={styles.slide}>
<h1 className={styles.title} style={{ fontSize: fitFontSize(title || 'VOTES') }}>{title || 'VOTES'}</h1>
<div className={styles.tallyList}>
{sorted.map(({ player, count, voterIds, isFrontrunner }) => (
<div
key={player.id}
className={`${styles.tallyRow} ${isFrontrunner ? styles.tallyRowFrontrunner : ''}`}
>
<img
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
StatusLed function · javascript · L20-L38 (19 LOC)client/src/components/StatusLed.jsx
export default function StatusLed({ status, connected }) {
const displayStatus = connected === false ? 'reconnecting' : status;
const color = STATUS_COLORS[displayStatus];
if (!color) return null;
const { r, g, b } = color;
const rgb = `${r}, ${g}, ${b}`;
const isPulsing = PULSE_STATES.has(displayStatus);
return (
<div
className={`${styles.led} ${isPulsing ? styles.pulse : ''}`}
style={{
backgroundColor: `rgb(${rgb})`,
boxShadow: `0 0 8px rgba(${rgb}, 0.6), 0 0 16px rgba(${rgb}, 0.3)`,
}}
/>
);
}measureText function · javascript · L43-L45 (3 LOC)client/src/components/TinyScreen.jsx
function measureText(str, font) {
return (str || '').length * font.width
}drawChar function · javascript · L50-L79 (30 LOC)client/src/components/TinyScreen.jsx
function drawChar(ctx, charCode, x, baselineY, font) {
const glyph = font.glyphs[charCode]
if (!glyph) return
const topY = baselineY - font.baseline
if (font.width <= 8) {
// 6x10: each row is a byte, top bits are pixels
for (let row = 0; row < font.height; row++) {
const byte = glyph[row]
if (byte === 0) continue
for (let bit = 7; bit >= (8 - font.width); bit--) {
if (byte & (1 << bit)) {
ctx.fillRect(x + (7 - bit), topY + row, 1, 1)
}
}
}
} else {
// 10x20: each row is a uint16, top bits are pixels
for (let row = 0; row < font.height; row++) {
const word = glyph[row]
if (word === 0) continue
for (let bit = 15; bit >= (16 - font.width); bit--) {
if (word & (1 << bit)) {
ctx.fillRect(x + (15 - bit), topY + row, 1, 1)
}
}
}
}
}page 1 / 10next ›