feat: кубики/таймер/кисть/спонсоры и задел игровых систем
Показ анимации кубиков игрокам, закрытие оверлеев с окнами, ручной ввод количества, меньший радиус кисти, блок спонсоров в «О программе»; плюс черновик game systems / character sheet. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,6 +31,16 @@ export type AppPlayer = {
|
||||
imageOffset: PlayerImageOffset;
|
||||
/** Масштаб аватара внутри круга (1 = по умолчанию). */
|
||||
imageScale: number;
|
||||
/** Optional TTRPG system id from Game Systems catalog. */
|
||||
systemId?: string | null;
|
||||
/** Frozen sheet binding: snapshotHash + values (schema freeze on bind). */
|
||||
characterSheet?: {
|
||||
systemId: string;
|
||||
snapshotHash: string;
|
||||
schemaVersion: number;
|
||||
revisionAtBind: number;
|
||||
values: Record<string, unknown>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
/** Расстановка кампанийного НПС на карте как игрового токена (в проекте). */
|
||||
@@ -276,7 +286,7 @@ export function normalizeAppPlayer(raw: unknown, teamIds?: Set<string>): AppPlay
|
||||
if (typeof obj.teamId === 'string' && obj.teamId) {
|
||||
if (!teamIds || teamIds.has(obj.teamId)) teamId = asPlayerTeamId(obj.teamId);
|
||||
}
|
||||
return {
|
||||
const out: AppPlayer = {
|
||||
id: asPlayerId(obj.id),
|
||||
name,
|
||||
imageRelPath: obj.imageRelPath,
|
||||
@@ -286,6 +296,35 @@ export function normalizeAppPlayer(raw: unknown, teamIds?: Set<string>): AppPlay
|
||||
imageOffset: clampPlayerImageOffset(obj.imageOffset),
|
||||
imageScale: clampPlayerImageScale((obj as { imageScale?: unknown }).imageScale),
|
||||
};
|
||||
if (obj.systemId === null) out.systemId = null;
|
||||
else if (typeof obj.systemId === 'string') out.systemId = obj.systemId;
|
||||
if (obj.characterSheet === null) out.characterSheet = null;
|
||||
else if (obj.characterSheet && typeof obj.characterSheet === 'object') {
|
||||
const cs = obj.characterSheet as {
|
||||
systemId?: unknown;
|
||||
snapshotHash?: unknown;
|
||||
schemaVersion?: unknown;
|
||||
revisionAtBind?: unknown;
|
||||
values?: unknown;
|
||||
};
|
||||
if (
|
||||
typeof cs.systemId === 'string' &&
|
||||
typeof cs.snapshotHash === 'string' &&
|
||||
typeof cs.schemaVersion === 'number' &&
|
||||
typeof cs.revisionAtBind === 'number' &&
|
||||
cs.values &&
|
||||
typeof cs.values === 'object'
|
||||
) {
|
||||
out.characterSheet = {
|
||||
systemId: cs.systemId,
|
||||
snapshotHash: cs.snapshotHash,
|
||||
schemaVersion: cs.schemaVersion,
|
||||
revisionAtBind: cs.revisionAtBind,
|
||||
values: cs.values as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function normalizeSceneNpcToken(raw: unknown): SceneNpcToken | null {
|
||||
|
||||
@@ -14,8 +14,10 @@ export type DiceRollRequest = {
|
||||
|
||||
export type DiceRollState = {
|
||||
revision: number;
|
||||
/** Идёт показ броска (оверлей виден). */
|
||||
/** Идёт показ броска (оверлей виден на презентации). */
|
||||
active: boolean;
|
||||
/** Показывать 3D-анимацию игрокам на экране презентации. */
|
||||
visibleToPlayers: boolean;
|
||||
/** Например "2d20". */
|
||||
notation: string;
|
||||
/** Заранее выбранные исходы — для синхрона пульт/презентация. */
|
||||
@@ -27,6 +29,7 @@ export type DiceRollState = {
|
||||
|
||||
export type DiceRollEvent =
|
||||
| { kind: 'roll'; count: number; sides: DiceSides }
|
||||
| { kind: 'setVisibleToPlayers'; visible: boolean }
|
||||
| { kind: 'hideOverlay' }
|
||||
| { kind: 'clear' };
|
||||
|
||||
@@ -34,6 +37,7 @@ export function createInitialDiceRollState(): DiceRollState {
|
||||
return {
|
||||
revision: 1,
|
||||
active: false,
|
||||
visibleToPlayers: true,
|
||||
notation: '',
|
||||
values: [],
|
||||
total: 0,
|
||||
@@ -41,9 +45,13 @@ export function createInitialDiceRollState(): DiceRollState {
|
||||
};
|
||||
}
|
||||
|
||||
/** Минимум / максимум количества кубиков в UI и store. */
|
||||
export const DICE_COUNT_MIN = 0;
|
||||
export const DICE_COUNT_MAX = 20;
|
||||
|
||||
export function clampDiceCount(n: number): number {
|
||||
if (!Number.isFinite(n)) return 1;
|
||||
return Math.max(1, Math.min(20, Math.round(n)));
|
||||
if (!Number.isFinite(n)) return DICE_COUNT_MIN;
|
||||
return Math.max(DICE_COUNT_MIN, Math.min(DICE_COUNT_MAX, Math.round(n)));
|
||||
}
|
||||
|
||||
/** Грани d100 в dice-box-threejs: 10,20,…,90,100 (лицо «00»). */
|
||||
|
||||
Reference in New Issue
Block a user