Ctrl K
ComunidadGuías…

plugins/hushle/src/index.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { createElement } from 'react';
import type { GamePlugin } from '@lobbyforge/plugin-sdk';
import { CATALOG_SUMMARY_KEY, PluginPermission, loadPluginLocale } from '@lobbyforge/plugin-sdk';
import { hushleReducer, validateHushleAction } from './actions';
import { LOCALE_TABLES, SHIPPED_LOCALES } from './locales.generated';
import { createHushleInitialState, migrateHushleState } from './state';
import type { HushleAction, HushleState } from './state';
import { HushlePanel, type HushlePanelClientProps } from './renderClient';
import { HUSHLE_PLUGIN_ID } from './plugin-id';

// Also registered by the panel, but that is a 'use client' module the
// server never evaluates — the host reads `catalog.summary` server-side.
loadPluginLocale(HUSHLE_PLUGIN_ID, LOCALE_TABLES);

export type {
  HushleAction,
  HushleState,
  HushleTeam,
  HushleCard,
  HushleSettings,
  HushlePhase,
  HushleLanguage,
  HushleDifficulty,
  HushleTimer,
} from './state';

export {
  createHushleInitialState,
  migrateHushleState,
  HUSHLE_STATE_VERSION,
  HUSHLE_DEFAULT_TURN_DURATION_SECONDS,
  HUSHLE_DEFAULT_CARDS_PER_TURN,
  HUSHLE_DEFAULT_TEAM_SIZE,
  HUSHLE_DEFAULT_DIFFICULTY_DISTRIBUTION,
  HUSHLE_TIME_UP_GRACE_MS,
} from './state';
export { hushleExplainerQueue, hushleNextExplainerForTeam } from './actions';
export { HUSHLE_PLUGIN_ID } from './plugin-id';
export {
  HUSHLE_BUILTIN_PACKS,
  getDefaultPackSlugForLanguage,
  getLanguageForPackSlug,
} from './decks';
// Server-only: the seeder imports @lobbyforge/db which transitively pulls
// in `postgres` (Node.js-only). Import via the subpath
// `@lobbyforge/hushle/builtInPacks` from server-side code only.
// `export { seedBuiltinHushlePacks } from './builtInPacks';` would crash
// the client bundle for the room page.

export const hushlePlugin: GamePlugin<HushleState, HushleAction> = {
  manifest: {
    id: HUSHLE_PLUGIN_ID,
    name: 'Hushle',
    version: '0.3.0',
    type: 'game',
    minAppVersion: '0.1.0',
    permissions: [
      PluginPermission.MANAGE_GAME_SESSION,
      PluginPermission.MANAGE_SCORES,
      PluginPermission.SEND_ROOM_MESSAGE,
      PluginPermission.MANAGE_TIMER,
    ],
    // Derived from locales/*.json, so the catalogue can never claim a
    // language the plugin does not actually ship.
    locales: SHIPPED_LOCALES,
    entryClient: './renderClient.js',
    catalog: {
      category: 'game',
      // Translated in locales/*.json; the host shows the viewer's language.
      summary: LOCALE_TABLES.en[CATALOG_SUMMARY_KEY],
      publisher: 'LobbyForge',
      trustLevel: 'official',
      playerConfig: {
        minPlayers: 4,
        maxPlayers: 12,
        defaultMaxPlayers: 8,
        supportsSpectators: true,
        supportsQueue: true,
        overflowPolicy: 'spectator',
      },
      requiresVoiceRoom: true,
      externalAccountRequired: false,
      compatibleAppVersion: '>=0.2.0',
      tags: ['word-game', 'party', 'voice'],
    },
  },
  actionPolicies: {
    'start-game': { role: 'host' },
    'set-teams': { role: 'host' },
    'start-turn': { role: 'host' },
    'set-explainer': { role: 'host' },
    'next-card': { role: 'host' },
    'correct-guess': { role: 'host' },
    pass: { role: 'host' },
    penalty: { role: 'host' },
    /**
     * Classic-Taboo buzzer: any SERVER MEMBER may dispatch it (the
     * route has already verified server membership). The host injects
     * the authenticated actor id as `bustedBy` (actorFields) and the
     * reducer verifies the caller sits on an OPPOSING team per the
     * host-configured rosters — teammates and floaters cannot bust.
     * ('player' role is unusable today: game_session_players is only
     * populated for the creator, so a 'player' check would 403 real
     * opponents.)
     */
    'bust-forbidden': { role: 'member', actorFields: ['bustedBy'] },
    'end-turn': { role: 'host' },
    'end-game': { role: 'host' },
  },
  // "Start new game" on the final screen: the host accepts `start-game` from
  // the ended phase (and nothing else — every other action is refused with
  // `session_ended`). The reducer sends everyone back to team setup.
  restartActions: ['start-game'],
  validateAction: validateHushleAction,
  createInitialState: () => createHushleInitialState(),
  handleAction: (_ctx, state, action) => hushleReducer(state, action),
  migrateState: (raw: unknown) => migrateHushleState(raw),
  // beta-review: this used to CALL HushlePanel as a plain function, so
  // the panel's hooks (useState/useEffect/useMemo) were appended to the
  // hook list of whatever component invoked renderClient. The host's
  // panel is rendered conditionally, so the hook count changed between
  // renders and React threw #310 ("rendered more hooks than during the
  // previous render") — the whole voice room crashed to its error
  // boundary the moment an activity loaded. Returning an ELEMENT gives
  // the panel its own component instance and its own hooks.
  renderClient: (props: unknown) =>
    createElement(HushlePanel, props as HushlePanelClientProps),
};