DNDGamePlayer: Electron редактор сцен, презентация, упаковка electron-builder

Made-with: Cursor
This commit is contained in:
Ivan Fontosh
2026-04-19 14:16:54 +08:00
commit a6cbcc273e
82 changed files with 22195 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
import type { AssetId, GraphNodeId, ProjectId, SceneId } from './ids';
export const PROJECT_SCHEMA_VERSION = 4 as const;
export type IsoDateTimeString = string;
export type MediaAssetType = 'image' | 'video' | 'audio';
export type MediaAssetBase = {
id: AssetId;
type: MediaAssetType;
mime: string;
originalName: string;
relPath: string;
sha256: string;
sizeBytes: number;
createdAt: IsoDateTimeString;
};
export type ImageAsset = MediaAssetBase & {
type: 'image';
widthPx?: number;
heightPx?: number;
};
export type VideoAsset = MediaAssetBase & {
type: 'video';
durationMs?: number;
widthPx?: number;
heightPx?: number;
};
export type AudioAsset = MediaAssetBase & {
type: 'audio';
durationMs?: number;
};
export type MediaAsset = ImageAsset | VideoAsset | AudioAsset;
/** Только видео и аудио сцены (изображения — только превью, отдельное поле). */
export type SceneAudioRef = {
assetId: AssetId;
autoplay: boolean;
loop: boolean;
};
export type SceneMediaRefs = {
videos: AssetId[];
audios: SceneAudioRef[];
};
export type SceneSettings = {
autoplayVideo: boolean;
autoplayAudio: boolean;
loopVideo: boolean;
loopAudio: boolean;
};
export type SceneLayout = {
x: number;
y: number;
};
/** Узел на визуальном графе (одна сцена может иметь несколько узлов). */
export type SceneGraphNode = {
id: GraphNodeId;
sceneId: SceneId;
x: number;
y: number;
/** Ровно один узел в проекте может быть начальной сценой для входа в граф. */
isStartScene: boolean;
};
export type SceneGraphEdge = {
id: string;
sourceGraphNodeId: GraphNodeId;
targetGraphNodeId: GraphNodeId;
};
export type Scene = {
id: SceneId;
title: string;
description: string;
/** Превью ассет (изображение или видео). */
previewAssetId: AssetId | null;
previewAssetType: 'image' | 'video' | null;
/** Для видео-превью: автозапуск (в редакторе/списках/на графе). */
previewVideoAutostart: boolean;
/** Поворот превью в градусах (0/90/180/270). */
previewRotationDeg: 0 | 90 | 180 | 270;
media: SceneMediaRefs;
settings: SceneSettings;
connections: SceneId[];
layout: SceneLayout;
};
export type ProjectMeta = {
name: string;
/** Имя файла проекта без суффикса `.dnd.zip` (то, что пользователь редактирует). */
fileBaseName: string;
createdAt: IsoDateTimeString;
updatedAt: IsoDateTimeString;
/** Версия приложения, с которой проект был создан (не меняется при сохранениях). */
createdWithAppVersion: string;
/** Версия приложения при последнем сохранении. */
appVersion: string;
schemaVersion: typeof PROJECT_SCHEMA_VERSION;
};
export type Project = {
id: ProjectId;
meta: ProjectMeta;
scenes: Record<SceneId, Scene>;
assets: Record<AssetId, MediaAsset>;
currentSceneId: SceneId | null;
/** Текущая нода графа (важно, когда одна сцена имеет несколько нод). */
currentGraphNodeId: GraphNodeId | null;
/** Позиции карточек на графе; логические связи сцен по-прежнему в `Scene.connections`. */
sceneGraphNodes: SceneGraphNode[];
sceneGraphEdges: SceneGraphEdge[];
};
+102
View File
@@ -0,0 +1,102 @@
export type EffectToolType = 'fog' | 'fire' | 'rain' | 'lightning' | 'freeze' | 'eraser';
export type EffectInstanceType = 'fog' | 'fire' | 'rain' | 'lightning' | 'freeze' | 'scorch' | 'ice';
/** Нормализованные координаты (0..1) относительно области предпросмотра/презентации. */
export type NPoint = { x: number; y: number; tMs: number; pressure?: number };
export type EffectInstanceBase = {
id: string;
type: EffectInstanceType;
/** Для детерминизма процедурных эффектов. */
seed: number;
/** Время создания по часам main-процесса. */
createdAtMs: number;
};
export type FogInstance = EffectInstanceBase & {
type: 'fog';
points: NPoint[];
radiusN: number;
opacity: number;
lifetimeMs: number | null;
};
export type FireInstance = EffectInstanceBase & {
type: 'fire';
points: NPoint[];
radiusN: number;
opacity: number;
lifetimeMs: number | null;
};
export type RainInstance = EffectInstanceBase & {
type: 'rain';
points: NPoint[];
radiusN: number;
opacity: number;
lifetimeMs: number | null;
};
export type LightningInstance = EffectInstanceBase & {
type: 'lightning';
start: { x: number; y: number };
end: { x: number; y: number };
widthN: number;
intensity: number;
lifetimeMs: number;
};
export type FreezeInstance = EffectInstanceBase & {
type: 'freeze';
at: { x: number; y: number };
intensity: number;
lifetimeMs: number;
};
export type ScorchInstance = EffectInstanceBase & {
/** Внутренний инстанс: след после молнии. */
type: 'scorch';
at: { x: number; y: number };
radiusN: number;
opacity: number;
lifetimeMs: number;
};
export type IceInstance = EffectInstanceBase & {
/** Внутренний инстанс: ледяной след после заморозки. */
type: 'ice';
at: { x: number; y: number };
radiusN: number;
opacity: number;
lifetimeMs: number;
};
export type EffectInstance =
| FogInstance
| FireInstance
| RainInstance
| LightningInstance
| FreezeInstance
| ScorchInstance
| IceInstance;
export type EffectToolState = {
tool: EffectToolType;
radiusN: number;
intensity: number;
};
export type EffectsState = {
revision: number;
/** Текущее время по часам main-процесса (для синхронной анимации между окнами). */
serverNowMs: number;
tool: EffectToolState;
instances: EffectInstance[];
};
export type EffectsEvent =
| { kind: 'tool.set'; tool: EffectToolState }
| { kind: 'instances.clear' }
| { kind: 'instance.add'; instance: EffectInstance }
| { kind: 'instance.remove'; id: string };
+22
View File
@@ -0,0 +1,22 @@
export type Brand<T, B extends string> = T & { readonly __brand: B };
export type ProjectId = Brand<string, 'ProjectId'>;
export type SceneId = Brand<string, 'SceneId'>;
export type AssetId = Brand<string, 'AssetId'>;
export type GraphNodeId = Brand<string, 'GraphNodeId'>;
export function asProjectId(value: string): ProjectId {
return value as ProjectId;
}
export function asSceneId(value: string): SceneId {
return value as SceneId;
}
export function asAssetId(value: string): AssetId {
return value as AssetId;
}
export function asGraphNodeId(value: string): GraphNodeId {
return value as GraphNodeId;
}
+4
View File
@@ -0,0 +1,4 @@
export * from './domain';
export * from './effects';
export * from './ids';
export * from './videoPlayback';
+21
View File
@@ -0,0 +1,21 @@
import type { AssetId } from './ids';
export type VideoPlaybackState = {
revision: number;
serverNowMs: number;
/** Какая именно видео-дорожка сейчас синхронизируется (previewAssetId). */
targetAssetId: AssetId | null;
playing: boolean;
playbackRate: number;
/** Опорная точка: при `anchorServerMs` видео было на `anchorVideoTimeSec`. */
anchorServerMs: number;
anchorVideoTimeSec: number;
};
export type VideoPlaybackEvent =
| { kind: 'target.set'; assetId: AssetId | null; autostart?: boolean }
| { kind: 'play' }
| { kind: 'pause' }
| { kind: 'stop' }
| { kind: 'seek'; timeSec: number }
| { kind: 'rate.set'; rate: number };