← back to davejharmon__murderhouse

Function bodies 456 total

All specs Real LLM only Function bodies
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';
        break
EventPanel 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 t
GameLog 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,
    player
Provenance: 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
  cons
PortraitSelectorModal 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.clea
Open 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}>+{point
CompositionSlide 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 })
        lastSam
Repobility · 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.heartbeatDebugBa
ItemTipSlide 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.h
RoleTipSlide 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.badge
AnimatedScore 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} classN
ScoreUpdateSlide 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>
          ))}
        </di
VoteTallySlide 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 ›