← back to davejharmon__murderhouse

Function bodies 456 total

All specs Real LLM only Function bodies
getEventsForPhase function · javascript · L1117-L1121 (5 LOC)
server/definitions/events.js
export function getEventsForPhase(phase) {
  return Object.values(events)
    .filter((e) => e.phase.includes(phase) && !e.isInterrupt)
    .sort((a, b) => a.priority - b.priority);
}
getAllEvents function · javascript · L1123-L1125 (3 LOC)
server/definitions/events.js
export function getAllEvents() {
  return { ...events };
}
getItem function · javascript · L116-L118 (3 LOC)
server/definitions/items.js
export function getItem(itemId) {
  return items[itemId] || null;
}
getAllItems function · javascript · L123-L125 (3 LOC)
server/definitions/items.js
export function getAllItems() {
  return Object.values(items);
}
buildRolePool function · javascript · L446-L453 (8 LOC)
server/definitions/roles.js
export function buildRolePool(playerCount) {
  const composition = GAME_COMPOSITION[playerCount];
  if (!composition)
    throw new Error(`No composition for ${playerCount} players`);
  const pool = [...composition];
  while (pool.length < playerCount) pool.push(RoleId.NOBODY);
  return pool;
}
getRole function · javascript · L455-L457 (3 LOC)
server/definitions/roles.js
export function getRole(roleId) {
  return roles[roleId] || null;
}
getAllRoles function · javascript · L459-L461 (3 LOC)
server/definitions/roles.js
export function getAllRoles() {
  return { ...roles };
}
Repobility (the analyzer behind this table) · https://repobility.com
GovernorPardonFlow class · javascript · L41-L324 (284 LOC)
server/flows/GovernorPardonFlow.js
export class GovernorPardonFlow extends InterruptFlow {
  static get id() {
    return 'pardon';
  }

  static get hooks() {
    return ['onVoteResolution'];
  }

  /**
   * Check if this flow should trigger
   * @param {Object} context - { voteEventId, resolution, instance }
   * @returns {boolean}
   */
  canTrigger(context) {
    const { resolution } = context;

    // Only trigger on elimination
    if (resolution?.outcome !== 'eliminated' || !resolution.victim) {
      return false;
    }

    // Check for judges or gavel holders (excluding cowards — they lose all actions)
    const judges = this.game.getAlivePlayers().filter((p) => {
      if (p.hasItem(ItemId.COWARD)) return false;
      return (
        p.role.id === RoleId.JUDGE ||
        (p.hasItem(ItemId.GAVEL) && p.canUseItem(ItemId.GAVEL))
      );
    });

    return judges.length > 0;
  }

  /**
   * Initialize the Judge pardon flow
   * @param {Object} context - { voteEventId, resolution, instance }
   * @returns {Obje
canTrigger method · javascript · L55-L73 (19 LOC)
server/flows/GovernorPardonFlow.js
  canTrigger(context) {
    const { resolution } = context;

    // Only trigger on elimination
    if (resolution?.outcome !== 'eliminated' || !resolution.victim) {
      return false;
    }

    // Check for judges or gavel holders (excluding cowards — they lose all actions)
    const judges = this.game.getAlivePlayers().filter((p) => {
      if (p.hasItem(ItemId.COWARD)) return false;
      return (
        p.role.id === RoleId.JUDGE ||
        (p.hasItem(ItemId.GAVEL) && p.canUseItem(ItemId.GAVEL))
      );
    });

    return judges.length > 0;
  }
trigger method · javascript · L80-L134 (55 LOC)
server/flows/GovernorPardonFlow.js
  trigger(context) {
    const { voteEventId, resolution, instance } = context;
    const condemned = resolution.victim;

    const judges = this.game.getAlivePlayers().filter((p) => {
      if (p.hasItem(ItemId.COWARD)) return false;
      return (
        p.role.id === RoleId.JUDGE ||
        (p.hasItem(ItemId.GAVEL) && p.canUseItem(ItemId.GAVEL))
      );
    });

    this.phase = 'active';
    this.state = {
      condemnedId: condemned.id,
      condemnedName: condemned.name,
      voteEventId,
      voteResolution: resolution,
      voteInstance: instance,
      judgeIds: judges.map((g) => g.id),
    };

    // Remove vote from judges' pending events
    for (const judge of judges) {
      judge.pendingEvents.delete(voteEventId);
    }

    // Create the pardon event
    this.game._startFlowEvent(this.id, {
      name: str('events', 'judgePardon.name'),
      description: str('events', 'judgePardon.description'),
      verb: 'pardon',
      participants: this.state.judgeIds,
    
getParticipants method · javascript · L140-L142 (3 LOC)
server/flows/GovernorPardonFlow.js
  getParticipants() {
    return this.state?.judgeIds || [];
  }
getValidTargets method · javascript · L149-L158 (10 LOC)
server/flows/GovernorPardonFlow.js
  getValidTargets(playerId) {
    if (!this.state) return [];
    if (!this.state.judgeIds.includes(playerId)) return [];

    // Cannot pardon yourself
    if (this.state.condemnedId === playerId) return [];

    const condemned = this.game.getPlayer(this.state.condemnedId);
    return condemned ? [condemned] : [];
  }
onSelection method · javascript · L167-L194 (28 LOC)
server/flows/GovernorPardonFlow.js
  onSelection(judgeId, targetId) {
    if (!this.state || !this.state.judgeIds.includes(judgeId)) {
      return null;
    }

    const judge = this.game.getPlayer(judgeId);
    const condemned = this.game.getPlayer(this.state.condemnedId);

    if (!judge || !condemned) {
      return { error: 'Invalid state' };
    }

    // Check gavel usage before resolve (canUseItem may change after state changes)
    const usesGavel = judge.hasItem(ItemId.GAVEL) && judge.canUseItem(ItemId.GAVEL);

    // Pardon = selected the condemned player
    const result = targetId === this.state.condemnedId
      ? this.resolvePardon(judge, condemned)
      : this.resolveExecution(judge, condemned);

    // Add gavel consumption to the result
    if (usesGavel) {
      if (!result.consumeItems) result.consumeItems = [];
      result.consumeItems.push({ playerId: judgeId, itemId: ItemId.GAVEL });
    }

    return result;
  }
resolvePardon method · javascript · L203-L228 (26 LOC)
server/flows/GovernorPardonFlow.js
  resolvePardon(judge, condemned) {
    this.phase = 'resolving';

    const message = str('log', 'pardoned', { judge: judge.getNameWithEmoji(), victim: condemned.getNameWithEmoji() });

    this.cleanup();

    return {
      success: true,
      pardoned: true,
      message,
      slides: [{
        slide: {
          type: 'death',
          playerId: condemned.id,
          title: str('slides', 'flow.pardonedTitle'),
          subtitle: str('slides', 'flow.pardonedSubtitle', { name: condemned.name }),
          revealRole: false,
          style: SlideStyle.POSITIVE,
        },
        jumpTo: true,
        isDeath: false,
      }],
      log: message,
    };
  }
resolveExecution method · javascript · L237-L281 (45 LOC)
server/flows/GovernorPardonFlow.js
  resolveExecution(judge, condemned) {
    this.phase = 'resolving';

    const condemnedId = this.state.condemnedId;
    const voteResolution = this.state.voteResolution;
    const voteInstance = this.state.voteInstance;
    const message = str('log', 'notPardoned', { judge: judge.getNameWithEmoji(), victim: condemned.getNameWithEmoji() });

    // Build slides array: "NO PARDON" title, then execution death slide
    const slides = [
      {
        slide: {
          type: 'title',
          title: str('slides', 'flow.noPardonTitle'),
          subtitle: str('slides', 'flow.noPardonSubtitle', { name: condemned.name }),
          style: SlideStyle.HOSTILE,
        },
        jumpTo: false,
        isDeath: false,
      },
    ];

    if (voteResolution?.slide) {
      const voterIds = Object.entries(voteInstance?.results || {})
        .filter(([, targetId]) => targetId === condemnedId)
        .map(([voterId]) => voterId);
      slides.push({
        slide: { ...voteResolution.slide,
Source: Repobility analyzer · https://repobility.com
onPlayerDisconnect method · javascript · L289-L303 (15 LOC)
server/flows/GovernorPardonFlow.js
  onPlayerDisconnect(player) {
    if (!this.state || !this.state.judgeIds.includes(player.id)) return null;
    // Only auto-execute if every judge has fully disconnected
    const anyStillConnected = this.state.judgeIds.some(gid => {
      if (gid === player.id) return false; // this one just disconnected
      const g = this.game.getPlayer(gid);
      return g && g.connections.length > 0;
    });
    if (anyStillConnected) return null;
    const condemned = this.game.getPlayer(this.state.condemnedId);
    const judge = player; // use disconnecting player as the "decider" for logging
    if (!condemned) { this.cleanup(); return null; }
    this.game.addLog(str('log', 'judgeDisconnected', { name: player.getNameWithEmoji() }));
    return this.resolveExecution(judge, condemned);
  }
cleanup method · javascript · L308-L323 (16 LOC)
server/flows/GovernorPardonFlow.js
  cleanup() {
    // Clear pending events for judges
    if (this.state?.judgeIds) {
      for (const gid of this.state.judgeIds) {
        const judge = this.game.getPlayer(gid);
        if (judge) {
          judge.clearFromEvent(this.id);
        }
      }
    }

    // Clean up legacy interruptData (for backwards compatibility)
    this.game.interruptData = null;

    super.cleanup();
  }
HunterRevengeFlow class · javascript · L38-L239 (202 LOC)
server/flows/HunterRevengeFlow.js
export class HunterRevengeFlow extends InterruptFlow {
  static get id() {
    return 'hunterRevenge';
  }

  static get hooks() {
    return ['onDeath'];
  }

  /**
   * Check if this flow should trigger
   * @param {Object} context - { player, cause, deathResult }
   * @returns {boolean}
   */
  canTrigger(context) {
    const { player, deathResult } = context;
    return (
      deathResult?.interrupt === true && player.role?.id === RoleId.HUNTER
    );
  }

  /**
   * Initialize the Hunter revenge flow
   * @param {Object} context - { player, cause, deathResult }
   * @returns {Object} - { interrupt: true, flowId: 'hunterRevenge' }
   */
  trigger(context) {
    const { player } = context;

    this.phase = 'active';
    this.state = {
      hunterId: player.id,
      hunterName: player.name,
      triggeredInPhase: this.game.phase,
      // Store pending slide to be pushed AFTER the death slide
      // (killPlayer triggers this flow before the death slide is pushed)
      pending
canTrigger method · javascript · L52-L57 (6 LOC)
server/flows/HunterRevengeFlow.js
  canTrigger(context) {
    const { player, deathResult } = context;
    return (
      deathResult?.interrupt === true && player.role?.id === RoleId.HUNTER
    );
  }
trigger method · javascript · L64-L97 (34 LOC)
server/flows/HunterRevengeFlow.js
  trigger(context) {
    const { player } = context;

    this.phase = 'active';
    this.state = {
      hunterId: player.id,
      hunterName: player.name,
      triggeredInPhase: this.game.phase,
      // Store pending slide to be pushed AFTER the death slide
      // (killPlayer triggers this flow before the death slide is pushed)
      pendingSlide: {
        type: 'title',
        title: str('slides', 'flow.hunterTitle'),
        subtitle: str('slides', 'flow.hunterSubtitle', { name: player.name }),
        playerId: player.id,
        style: SlideStyle.HOSTILE,
      },
    };

    // Create the event using Game's flow event helper
    this.game._startFlowEvent(this.id, {
      name: str('events', 'hunterRevenge.name'),
      description: str('events', 'hunterRevenge.description'),
      verb: 'shoot',
      participants: [player.id],
      getValidTargets: (actorId) => this.getValidTargets(actorId),
      allowAbstain: false,
      playerResolved: false, // Host resolves, or au
getPendingSlide method · javascript · L104-L111 (8 LOC)
server/flows/HunterRevengeFlow.js
  getPendingSlide() {
    if (this.state?.pendingSlide) {
      const slide = this.state.pendingSlide;
      this.state.pendingSlide = null;
      return slide;
    }
    return null;
  }
getParticipants method · javascript · L117-L119 (3 LOC)
server/flows/HunterRevengeFlow.js
  getParticipants() {
    return this.state ? [this.state.hunterId] : [];
  }
getValidTargets method · javascript · L126-L131 (6 LOC)
server/flows/HunterRevengeFlow.js
  getValidTargets(playerId) {
    if (!this.state || playerId !== this.state.hunterId) return [];
    return this.game
      .getAlivePlayers()
      .filter((p) => p.id !== this.state.hunterId && !p.hasItem('coward'));
  }
Repobility · code-quality intelligence · https://repobility.com
onSelection method · javascript · L139-L155 (17 LOC)
server/flows/HunterRevengeFlow.js
  onSelection(playerId, targetId) {
    if (!this.state || playerId !== this.state.hunterId) return null;

    // Hunter cannot abstain
    if (targetId === null) {
      return { error: 'Hunter must choose a target' };
    }

    // Validate target
    const validTargets = this.getValidTargets(playerId);
    if (!validTargets.find((t) => t.id === targetId)) {
      return { error: 'Invalid target' };
    }

    // Auto-resolve when hunter makes selection
    return this.resolve(targetId);
  }
resolve method · javascript · L163-L205 (43 LOC)
server/flows/HunterRevengeFlow.js
  resolve(targetId) {
    const hunter = this.game.getPlayer(this.state.hunterId);
    const victim = this.game.getPlayer(targetId);

    if (!victim) {
      return { success: false, error: 'Invalid target' };
    }

    this.phase = 'resolving';

    const teamNames = {
      circle: str('slides', 'death.teamCircle'),
      cell: str('slides', 'death.teamCell'),
      neutral: str('slides', 'death.teamNeutral'),
    };
    const teamName = victim.role?.id === 'jester'
      ? str('slides', 'death.teamJester')
      : (teamNames[victim.role?.team] || str('slides', 'death.teamUnknown'));
    const message = str('log', 'hunterRevengeKill', { hunter: hunter.getNameWithEmoji(), victim: victim.getNameWithEmoji() });

    // Cleanup before returning (frees flow for potential nested hunter revenge)
    this.cleanup();

    return {
      success: true,
      victim,
      message,
      kills: [{ playerId: victim.id, cause: 'hunter' }],
      slides: [{
        slide: {
          type: 'deat
onPlayerDisconnect method · javascript · L213-L224 (12 LOC)
server/flows/HunterRevengeFlow.js
  onPlayerDisconnect(player) {
    if (!this.state || player.id !== this.state.hunterId) return null;
    const targets = this.getValidTargets(player.id);
    if (targets.length === 0) {
      // No valid targets — skip the revenge quietly.
      this.cleanup();
      return { success: true, kills: [], slides: [], log: str('log', 'hunterDisconnected', { name: player.getNameWithEmoji() }) };
    }
    const target = targets[Math.floor(Math.random() * targets.length)];
    this.game.addLog(str('log', 'hunterAutoResolved', { name: player.getNameWithEmoji() }));
    return this.resolve(target.id);
  }
cleanup method · javascript · L229-L238 (10 LOC)
server/flows/HunterRevengeFlow.js
  cleanup() {
    // Clear pending events BEFORE super.cleanup() which nulls state
    if (this.state?.hunterId) {
      const hunter = this.game.getPlayer(this.state.hunterId);
      if (hunter) {
        hunter.clearFromEvent(this.id);
      }
    }
    super.cleanup();
  }
InterruptFlow class · javascript · L51-L174 (124 LOC)
server/flows/InterruptFlow.js
export class InterruptFlow {
  constructor(game) {
    this.game = game;
    this.state = null;
    this.phase = 'idle';
  }

  /**
   * Unique identifier for this flow (used as event ID)
   * @returns {string}
   */
  static get id() {
    throw new Error('Subclass must implement static id getter');
  }

  /**
   * Game hooks this flow responds to (e.g., 'onDeath', 'onVoteResolution')
   * @returns {string[]}
   */
  static get hooks() {
    return [];
  }

  /**
   * Instance accessor for the flow ID
   * @returns {string}
   */
  get id() {
    return this.constructor.id;
  }

  /**
   * Check if this flow should trigger given the context
   * @param {Object} context - Trigger context (varies by flow type)
   * @returns {boolean}
   */
  canTrigger(context) {
    return false;
  }

  /**
   * Initialize and start the flow
   * @param {Object} context - Trigger context
   * @returns {Object} Result with { interrupt: boolean, flowId: string }
   */
  trigger(context) {
    throw new E
constructor method · javascript · L52-L56 (5 LOC)
server/flows/InterruptFlow.js
  constructor(game) {
    this.game = game;
    this.state = null;
    this.phase = 'idle';
  }
canTrigger method · javascript · L87-L89 (3 LOC)
server/flows/InterruptFlow.js
  canTrigger(context) {
    return false;
  }
trigger method · javascript · L96-L98 (3 LOC)
server/flows/InterruptFlow.js
  trigger(context) {
    throw new Error('Subclass must implement trigger()');
  }
Repobility · open methodology · https://repobility.com/research/
onSelection method · javascript · L106-L108 (3 LOC)
server/flows/InterruptFlow.js
  onSelection(playerId, targetId) {
    return null;
  }
resolve method · javascript · L115-L117 (3 LOC)
server/flows/InterruptFlow.js
  resolve(...args) {
    throw new Error('Subclass must implement resolve()');
  }
cleanup method · javascript · L122-L129 (8 LOC)
server/flows/InterruptFlow.js
  cleanup() {
    this.state = null;
    this.phase = 'idle';
    // Remove from activeEvents if still present
    if (this.game.activeEvents.has(this.id)) {
      this.game.activeEvents.delete(this.id);
    }
  }
getParticipants method · javascript · L135-L137 (3 LOC)
server/flows/InterruptFlow.js
  getParticipants() {
    return [];
  }
getValidTargets method · javascript · L144-L146 (3 LOC)
server/flows/InterruptFlow.js
  getValidTargets(playerId) {
    return [];
  }
getPendingSlide method · javascript · L153-L155 (3 LOC)
server/flows/InterruptFlow.js
  getPendingSlide() {
    return null;
  }
isActive method · javascript · L161-L163 (3 LOC)
server/flows/InterruptFlow.js
  isActive() {
    return this.phase === 'active' || this.phase === 'resolving';
  }
onPlayerDisconnect method · javascript · L171-L173 (3 LOC)
server/flows/InterruptFlow.js
  onPlayerDisconnect(player) {
    return null;
  }
Repobility (the analyzer behind this table) · https://repobility.com
constructor method · javascript · L41-L79 (39 LOC)
server/Game.js
  constructor(broadcast, sendToHostFn, sendToScreenFn) {
    this.broadcast = broadcast; // Function to send to all clients
    this._sendToHost = sendToHostFn; // Function to find & send to host from clients set
    this._sendToScreen = sendToScreenFn; // Function to find & send to screen from clients set
    this.host = null; // Legacy reference (kept for handler compat)
    this.slideIdCounter = 0; // Unique ID counter for slides
    this.screen = null; // Legacy reference (kept for handler compat)
    this.playerCustomizations = new Map(); // Persist player names/portraits across resets

    // Initialize interrupt flows (these persist across resets)
    this.flows = new Map([
      [HunterRevengeFlow.id, new HunterRevengeFlow(this)],
      [GovernorPardonFlow.id, new GovernorPardonFlow(this)],
    ]);

    // Death processing queue (prevents recursive killPlayer cascades)
    this._deathQueue = [];
    this._processingDeaths = false;

    // Debounced broadcast for rapid dial inpu
reset method · javascript · L81-L177 (97 LOC)
server/Game.js
  reset() {
    // Save player customizations before clearing (if players exist)
    if (this.players) {
      for (const [playerId, player] of this.players) {
        this.playerCustomizations.set(playerId, {
          name: player.name,
          portrait: player.portrait,
          preAssignedRole: player.preAssignedRole || null,
        });
      }
    }

    // Clear any running event timers
    if (this.eventTimers) {
      for (const { timeout } of this.eventTimers.values()) {
        clearTimeout(timeout);
      }
    }
    this.eventTimers = new Map();

    resetSeatCounter();
    this.players = new Map(); // id -> Player
    // NOTE: We keep host and screen connections alive during reset
    // Only clear them if explicitly needed (not during game reset)

    this.phase = GamePhase.LOBBY;
    this.dayCount = 0;

    // Event management
    this.pendingEvents = []; // Events that can be started this phase
    this.activeEvents = new Map(); // eventId -> { event, results, parti
addPlayer method · javascript · L181-L216 (36 LOC)
server/Game.js
  addPlayer(id, ws) {
    if (this.players.size >= MAX_PLAYERS) {
      return { success: false, error: 'Game is full' };
    }
    if (this.phase !== GamePhase.LOBBY) {
      return { success: false, error: 'Game already in progress' };
    }

    const player = new Player(id, ws);

    // Restore customizations if they exist from previous games
    const customization = this.playerCustomizations.get(id);
    if (customization) {
      player.name = customization.name;
      player.portrait = customization.portrait;
      if (this.phase === GamePhase.LOBBY && customization.preAssignedRole) {
        player.preAssignedRole = customization.preAssignedRole;
      }
    }

    this.players.set(id, player);

    // Ensure player has a default calibration entry
    const calConfig = this._hostSettings.heartbeatCalibration || {};
    if (!calConfig[id]) {
      calConfig[id] = { restingBpm: 60, elevatedBpm: 100, enabled: false };
      this._hostSettings.heartbeatCalibration = calConfig;
   
persistPlayerCustomization method · javascript · L218-L226 (9 LOC)
server/Game.js
  persistPlayerCustomization(player) {
    const customization = this.playerCustomizations.get(player.id) || {};
    this.playerCustomizations.set(player.id, {
      ...customization,
      name: player.name,
      portrait: player.portrait,
      preAssignedRole: player.preAssignedRole || customization.preAssignedRole || null,
    });
  }
_loadHostSettingsFromDisk method · javascript · L230-L252 (23 LOC)
server/Game.js
  _loadHostSettingsFromDisk() {
    const defaults = {
      timerDuration: 30,
      autoAdvanceEnabled: false,
      heartbeatThreshold: 110,
      scoringConfig: { survived: 1, winningTeam: 1, bestInvestigator: 2 },
      heartbeatCalibration: {},
      heartbeatDisplayResting: 65,
      heartbeatDisplayElevated: 110,
      simsCanLose: false,
      heartbeatAddNoise: false,
    };
    if (!fs.existsSync(HOST_SETTINGS_PATH)) {
      this._hostSettings = defaults;
      return;
    }
    try {
      this._hostSettings = { ...defaults, ...JSON.parse(fs.readFileSync(HOST_SETTINGS_PATH, 'utf-8')) };
    } catch (e) {
      console.error('[Server] Failed to load host settings:', e.message);
      this._hostSettings = defaults;
    }
  }
getHostSettings method · javascript · L254-L256 (3 LOC)
server/Game.js
  getHostSettings() {
    return { ...this._hostSettings };
  }
saveHostSettings method · javascript · L258-L262 (5 LOC)
server/Game.js
  saveHostSettings(settings) {
    this._hostSettings = { ...this._hostSettings, ...settings };
    fs.writeFileSync(HOST_SETTINGS_PATH, JSON.stringify(this._hostSettings, null, 2));
    this.sendToHost(ServerMsg.HOST_SETTINGS, this.getHostSettings());
  }
setDefaultPreset method · javascript · L264-L268 (5 LOC)
server/Game.js
  setDefaultPreset(id) {
    // Toggle off if already the default
    const defaultPresetId = this._hostSettings.defaultPresetId === id ? null : id;
    this.saveHostSettings({ defaultPresetId });
  }
Source: Repobility analyzer · https://repobility.com
_loadScoresFromDisk method · javascript · L272-L284 (13 LOC)
server/Game.js
  _loadScoresFromDisk() {
    if (!fs.existsSync(SCORES_PATH)) {
      this._scores = new Map();
      return;
    }
    try {
      const data = JSON.parse(fs.readFileSync(SCORES_PATH, 'utf-8'));
      this._scores = new Map(Object.entries(data).map(([k, v]) => [k, Number(v) || 0]));
    } catch (e) {
      console.error('[Server] Failed to load scores:', e.message);
      this._scores = new Map();
    }
  }
_saveScoresToDisk method · javascript · L286-L289 (4 LOC)
server/Game.js
  _saveScoresToDisk() {
    const obj = Object.fromEntries(this._scores);
    fs.writeFileSync(SCORES_PATH, JSON.stringify(obj, null, 2));
  }
setScore method · javascript · L291-L295 (5 LOC)
server/Game.js
  setScore(name, score) {
    this._scores.set(name, score);
    this._saveScoresToDisk();
    this.sendScoresToHost();
  }
‹ prevpage 5 / 10next ›