plugins/hushle/src/index.ts
Rama main · réplica de lectura del repositorio original.
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),
};