@luna-editor/engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/Player.d.ts +3 -0
  2. package/dist/Player.js +336 -0
  3. package/dist/atoms/screen-size.d.ts +22 -0
  4. package/dist/atoms/screen-size.js +8 -0
  5. package/dist/components/BacklogUI.d.ts +2 -0
  6. package/dist/components/BacklogUI.js +115 -0
  7. package/dist/components/ConversationLogUI.d.ts +2 -0
  8. package/dist/components/ConversationLogUI.js +115 -0
  9. package/dist/components/DebugControls.d.ts +12 -0
  10. package/dist/components/DebugControls.js +5 -0
  11. package/dist/components/DialogueBox.d.ts +2 -0
  12. package/dist/components/DialogueBox.js +28 -0
  13. package/dist/components/EndScreen.d.ts +8 -0
  14. package/dist/components/EndScreen.js +5 -0
  15. package/dist/components/GameScreen.d.ts +9 -0
  16. package/dist/components/GameScreen.js +111 -0
  17. package/dist/components/OverlayUI.d.ts +13 -0
  18. package/dist/components/OverlayUI.js +21 -0
  19. package/dist/components/PluginComponentProvider.d.ts +14 -0
  20. package/dist/components/PluginComponentProvider.js +24 -0
  21. package/dist/constants/screen-size.d.ts +3 -0
  22. package/dist/constants/screen-size.js +6 -0
  23. package/dist/contexts/DataContext.d.ts +24 -0
  24. package/dist/contexts/DataContext.js +101 -0
  25. package/dist/hooks/useBacklog.d.ts +14 -0
  26. package/dist/hooks/useBacklog.js +82 -0
  27. package/dist/hooks/useConversationLog.d.ts +14 -0
  28. package/dist/hooks/useConversationLog.js +82 -0
  29. package/dist/hooks/usePlayerLogic.d.ts +21 -0
  30. package/dist/hooks/usePlayerLogic.js +145 -0
  31. package/dist/hooks/usePluginAPI.d.ts +19 -0
  32. package/dist/hooks/usePluginAPI.js +42 -0
  33. package/dist/hooks/usePluginEvents.d.ts +14 -0
  34. package/dist/hooks/usePluginEvents.js +197 -0
  35. package/dist/hooks/usePreloadImages.d.ts +2 -0
  36. package/dist/hooks/usePreloadImages.js +56 -0
  37. package/dist/hooks/useScreenSize.d.ts +89 -0
  38. package/dist/hooks/useScreenSize.js +87 -0
  39. package/dist/hooks/useTypewriter.d.ts +11 -0
  40. package/dist/hooks/useTypewriter.js +56 -0
  41. package/dist/hooks/useUIVisibility.d.ts +9 -0
  42. package/dist/hooks/useUIVisibility.js +19 -0
  43. package/dist/hooks/useVoice.d.ts +4 -0
  44. package/dist/hooks/useVoice.js +21 -0
  45. package/dist/index.d.ts +10 -0
  46. package/dist/index.js +9 -0
  47. package/dist/plugin/PluginManager.d.ts +108 -0
  48. package/dist/plugin/PluginManager.js +851 -0
  49. package/dist/plugin/luna-react.d.ts +41 -0
  50. package/dist/plugin/luna-react.js +99 -0
  51. package/dist/sdk.d.ts +512 -0
  52. package/dist/sdk.js +64 -0
  53. package/dist/types.d.ts +186 -0
  54. package/dist/types.js +2 -0
  55. package/dist/utils/attributeNormalizer.d.ts +5 -0
  56. package/dist/utils/attributeNormalizer.js +53 -0
  57. package/dist/utils/facePositionCalculator.d.ts +29 -0
  58. package/dist/utils/facePositionCalculator.js +127 -0
  59. package/package.json +55 -0
package/dist/sdk.js ADDED
@@ -0,0 +1,64 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ // SDK exports for plugin development
3
+ // This file contains all types and utilities needed for plugin development
4
+ import React from "react";
5
+ // Component registration types
6
+ export var ComponentType;
7
+ (function (ComponentType) {
8
+ ComponentType["Backlog"] = "Backlog";
9
+ ComponentType["DialogueBox"] = "DialogueBox";
10
+ ComponentType["CharacterDisplay"] = "CharacterDisplay";
11
+ ComponentType["ActionMenu"] = "ActionMenu";
12
+ ComponentType["SaveLoadMenu"] = "SaveLoadMenu";
13
+ ComponentType["SaveMenu"] = "SaveMenu";
14
+ ComponentType["LoadMenu"] = "LoadMenu";
15
+ ComponentType["SettingsMenu"] = "SettingsMenu";
16
+ ComponentType["GameMenu"] = "GameMenu";
17
+ ComponentType["Config"] = "Config";
18
+ })(ComponentType || (ComponentType = {}));
19
+ // Component types are already exported as part of the interfaces above
20
+ // Plugin utility function
21
+ export function definePlugin(plugin) {
22
+ return plugin;
23
+ }
24
+ // Helper function for component registration
25
+ export function defineComponent(type, component) {
26
+ return { type, component };
27
+ }
28
+ /**
29
+ * Calculate effect position relative to face
30
+ * Utility function for plugins to position effects relative to character face
31
+ */
32
+ export function calculateFaceRelativePosition(options) {
33
+ const { facePosition, relativePosition, offsetX = 0, offsetY = 0 } = options;
34
+ let baseX = facePosition.x;
35
+ let baseY = facePosition.y;
36
+ // Adjust base position based on relative position
37
+ switch (relativePosition) {
38
+ case "forehead":
39
+ baseY = facePosition.y - facePosition.height * 0.3;
40
+ break;
41
+ case "cheek_left":
42
+ baseX = facePosition.x - facePosition.width * 0.3;
43
+ break;
44
+ case "cheek_right":
45
+ baseX = facePosition.x + facePosition.width * 0.3;
46
+ break;
47
+ case "chin":
48
+ baseY = facePosition.y + facePosition.height * 0.3;
49
+ break;
50
+ case "center":
51
+ default:
52
+ // Use face center as is
53
+ break;
54
+ }
55
+ return {
56
+ x: baseX + offsetX,
57
+ y: baseY + offsetY,
58
+ };
59
+ }
60
+ // Export React for plugin use
61
+ export { React };
62
+ export { OverlayUI } from "./components/OverlayUI";
63
+ // Screen size constants
64
+ export { aspectRatio, BasisHeight, BasisWidth } from "./constants/screen-size";
@@ -0,0 +1,186 @@
1
+ export type ScenarioBlockType = "dialogue" | "narration" | "action_node" | "character_entrance" | "character_exit";
2
+ export interface ScenarioBlock {
3
+ id: string;
4
+ scenarioId: string;
5
+ order: number;
6
+ blockType: ScenarioBlockType;
7
+ content: string | null;
8
+ speakerId: string | null;
9
+ speakerStateId: string | null;
10
+ partVoiceId: string | null;
11
+ actionNodeId: string | null;
12
+ linkedExitBlockId: string | null;
13
+ createdAt: Date;
14
+ updatedAt: Date;
15
+ speaker?: {
16
+ id: string;
17
+ name: string;
18
+ typeId: string;
19
+ workId: string;
20
+ description: string | null;
21
+ order: number;
22
+ } | null;
23
+ speakerState?: {
24
+ id: string;
25
+ name: string;
26
+ imageUrl: string | null;
27
+ cropArea: string | null;
28
+ scale?: number;
29
+ translateY?: number;
30
+ translateX?: number;
31
+ } | null;
32
+ actionNode?: ActionNode | null;
33
+ attributeValues: ScenarioBlockAttributeValue[];
34
+ entityAttributeValues: ScenarioBlockEntityAttributeValue[];
35
+ characters?: ScenarioBlockCharacter[];
36
+ linkedExitBlock?: {
37
+ id: string;
38
+ order: number;
39
+ } | null;
40
+ }
41
+ export interface ScenarioBlockCharacter {
42
+ id: string;
43
+ scenarioBlockId: string;
44
+ objectId: string;
45
+ entityStateId: string;
46
+ positionX?: number | null;
47
+ positionY?: number | null;
48
+ zIndex?: number | null;
49
+ scale?: number | null;
50
+ object: {
51
+ id: string;
52
+ name: string;
53
+ };
54
+ entityState: {
55
+ id: string;
56
+ name: string;
57
+ imageUrl: string | null;
58
+ cropArea: string | null;
59
+ scale?: number;
60
+ translateY?: number;
61
+ translateX?: number;
62
+ };
63
+ }
64
+ export interface ActionNode {
65
+ id: string;
66
+ workId: string;
67
+ name: string;
68
+ description: string | null;
69
+ createdAt: Date;
70
+ updatedAt: Date;
71
+ order: number;
72
+ pluginPackageName?: string | null;
73
+ nodeType?: string | null;
74
+ type?: string;
75
+ }
76
+ export interface ScenarioBlockAttributeValue {
77
+ scenarioBlockId: string;
78
+ attributeId: number;
79
+ value: string | null;
80
+ attribute: {
81
+ id: number;
82
+ name: string;
83
+ attributeType: string;
84
+ isNullable: boolean;
85
+ };
86
+ }
87
+ export interface ScenarioBlockEntityAttributeValue {
88
+ scenarioBlockId: string;
89
+ attributeId: number;
90
+ objectId: string;
91
+ entityStateId: string;
92
+ attribute: {
93
+ id: number;
94
+ name: string;
95
+ attributeType: string;
96
+ isNullable: boolean;
97
+ };
98
+ object: {
99
+ id: string;
100
+ name: string;
101
+ };
102
+ entityState: {
103
+ id: string;
104
+ name: string;
105
+ imageUrl: string | null;
106
+ cropArea: string | null;
107
+ scale?: number;
108
+ translateY?: number;
109
+ translateX?: number;
110
+ };
111
+ }
112
+ export interface Character {
113
+ id: string;
114
+ name: string;
115
+ state?: EntityState;
116
+ element?: HTMLElement;
117
+ }
118
+ export interface EntityState {
119
+ id: string;
120
+ name: string;
121
+ imageUrl?: string | null;
122
+ }
123
+ export interface DisplayedCharacter {
124
+ objectId: string;
125
+ entityStateId: string;
126
+ positionX?: number | null;
127
+ positionY?: number | null;
128
+ zIndex?: number | null;
129
+ scale?: number | null;
130
+ object: {
131
+ id: string;
132
+ name: string;
133
+ };
134
+ entityState: {
135
+ id: string;
136
+ name: string;
137
+ imageUrl: string | null;
138
+ cropArea: string | null;
139
+ scale?: number;
140
+ translateY?: number;
141
+ translateX?: number;
142
+ };
143
+ }
144
+ export interface PublishedScenario {
145
+ id: string;
146
+ scenarioId: string;
147
+ workId: string;
148
+ name: string;
149
+ description: string | null;
150
+ blocks: ScenarioBlock[];
151
+ publishedAt: Date;
152
+ }
153
+ export interface PlayerSettings {
154
+ aspectRatio: string;
155
+ bgObjectFit: "contain" | "cover";
156
+ }
157
+ export interface PluginConfig {
158
+ packageName: string;
159
+ bundleUrl: string;
160
+ config?: unknown;
161
+ }
162
+ export interface PlayerProps {
163
+ scenario: PublishedScenario;
164
+ settings?: PlayerSettings;
165
+ plugins?: PluginConfig[];
166
+ onEnd?: () => void;
167
+ onScenarioEnd?: () => void;
168
+ onScenarioStart?: () => void;
169
+ onScenarioCancelled?: () => void;
170
+ className?: string;
171
+ autoplay?: boolean;
172
+ }
173
+ export interface PlayerState {
174
+ currentBlockIndex: number;
175
+ isPlaying: boolean;
176
+ isEnded: boolean;
177
+ }
178
+ export interface BacklogEntry {
179
+ id: string;
180
+ timestamp: number;
181
+ blockIndex: number;
182
+ blockType: string;
183
+ content: string | null;
184
+ speakerName?: string;
185
+ speakerState?: string;
186
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // Common types shared between engine and plugin-sdk
2
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { ScenarioBlock } from "../types";
2
+ /**
3
+ * Normalize attribute values from database format to plugin-friendly format
4
+ */
5
+ export declare function normalizeAttributes(block: ScenarioBlock): Record<string, unknown>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Normalize attribute values from database format to plugin-friendly format
3
+ */
4
+ export function normalizeAttributes(block) {
5
+ var _a, _b;
6
+ const attributes = {};
7
+ // Process regular attributes (text, number, select, etc.)
8
+ (_a = block.attributeValues) === null || _a === void 0 ? void 0 : _a.forEach((attrValue) => {
9
+ var _a;
10
+ if (((_a = attrValue === null || attrValue === void 0 ? void 0 : attrValue.attribute) === null || _a === void 0 ? void 0 : _a.name) && attrValue.value !== undefined) {
11
+ const value = normalizeAttributeValue(attrValue.value, attrValue.attribute.attributeType);
12
+ attributes[attrValue.attribute.name] = value;
13
+ }
14
+ });
15
+ // Process entity attributes (entity type)
16
+ (_b = block.entityAttributeValues) === null || _b === void 0 ? void 0 : _b.forEach((entityAttr) => {
17
+ var _a;
18
+ if ((_a = entityAttr === null || entityAttr === void 0 ? void 0 : entityAttr.attribute) === null || _a === void 0 ? void 0 : _a.name) {
19
+ // For entity attributes, provide rich object instead of just ID
20
+ attributes[entityAttr.attribute.name] = entityAttr.objectId;
21
+ // Also provide detailed entity info with _detail suffix
22
+ attributes[`${entityAttr.attribute.name}_detail`] = {
23
+ objectId: entityAttr.objectId,
24
+ object: entityAttr.object,
25
+ entityState: entityAttr.entityState,
26
+ };
27
+ }
28
+ });
29
+ return attributes;
30
+ }
31
+ /**
32
+ * Normalize individual attribute value based on its type
33
+ */
34
+ function normalizeAttributeValue(value, type) {
35
+ if (value === null || value === undefined) {
36
+ return null;
37
+ }
38
+ switch (type) {
39
+ case "number":
40
+ return Number(value);
41
+ case "select":
42
+ return String(value);
43
+ case "text":
44
+ case "textarea":
45
+ return String(value || "");
46
+ case "json":
47
+ return typeof value === "string" ? JSON.parse(value) : value;
48
+ case "array":
49
+ return Array.isArray(value) ? value : [value];
50
+ default:
51
+ return value;
52
+ }
53
+ }
@@ -0,0 +1,29 @@
1
+ export interface CropArea {
2
+ x: number;
3
+ y: number;
4
+ width: number;
5
+ height: number;
6
+ }
7
+ export interface FacePosition {
8
+ x: number;
9
+ y: number;
10
+ width: number;
11
+ height: number;
12
+ }
13
+ export interface EntityState {
14
+ cropArea: string | null;
15
+ scale?: number;
16
+ translateX?: number;
17
+ translateY?: number;
18
+ }
19
+ /**
20
+ * Calculate face position from character element and entity state
21
+ */
22
+ export declare function calculateFacePosition(characterElement: HTMLElement, entityState: EntityState): FacePosition | null;
23
+ /**
24
+ * Calculate effect position relative to face
25
+ */
26
+ export declare function calculateEffectPosition(facePosition: FacePosition, relativePosition: "forehead" | "cheek_left" | "cheek_right" | "chin" | "center", offsetX?: number, offsetY?: number): {
27
+ x: number;
28
+ y: number;
29
+ };
@@ -0,0 +1,127 @@
1
+ // Face position calculation utility for emotion effects
2
+ // Calculates face position from EntityState cropArea data
3
+ /**
4
+ * Parse cropArea JSON string to CropArea object
5
+ */
6
+ function parseCropArea(cropAreaString) {
7
+ if (!cropAreaString)
8
+ return null;
9
+ try {
10
+ const parsed = JSON.parse(cropAreaString);
11
+ if (typeof parsed === "object" &&
12
+ typeof parsed.x === "number" &&
13
+ typeof parsed.y === "number" &&
14
+ typeof parsed.width === "number" &&
15
+ typeof parsed.height === "number") {
16
+ return parsed;
17
+ }
18
+ return null;
19
+ }
20
+ catch (_a) {
21
+ return null;
22
+ }
23
+ }
24
+ /**
25
+ * Calculate face position from character element and entity state
26
+ */
27
+ export function calculateFacePosition(characterElement, entityState) {
28
+ const cropArea = parseCropArea(entityState.cropArea);
29
+ if (!cropArea)
30
+ return null;
31
+ // Get character element's actual display size
32
+ const elementRect = characterElement.getBoundingClientRect();
33
+ const elementWidth = elementRect.width;
34
+ const elementHeight = elementRect.height;
35
+ // Get the image element inside character element
36
+ const imgElement = characterElement.querySelector("img");
37
+ if (!imgElement)
38
+ return null;
39
+ // Get natural image dimensions
40
+ const naturalWidth = imgElement.naturalWidth;
41
+ const naturalHeight = imgElement.naturalHeight;
42
+ if (naturalWidth === 0 || naturalHeight === 0)
43
+ return null;
44
+ // Calculate how the image is displayed (considering object-fit: contain/cover)
45
+ const computedStyle = window.getComputedStyle(imgElement);
46
+ const objectFit = computedStyle.objectFit || "contain";
47
+ // Calculate display dimensions based on object-fit
48
+ let displayWidth;
49
+ let displayHeight;
50
+ let offsetX = 0;
51
+ let offsetY = 0;
52
+ if (objectFit === "contain") {
53
+ // Image is scaled to fit within the element while maintaining aspect ratio
54
+ const elementAspect = elementWidth / elementHeight;
55
+ const imageAspect = naturalWidth / naturalHeight;
56
+ if (imageAspect > elementAspect) {
57
+ // Image is wider - fit to width
58
+ displayWidth = elementWidth;
59
+ displayHeight = elementWidth / imageAspect;
60
+ offsetY = (elementHeight - displayHeight) / 2;
61
+ }
62
+ else {
63
+ // Image is taller - fit to height
64
+ displayHeight = elementHeight;
65
+ displayWidth = elementHeight * imageAspect;
66
+ offsetX = (elementWidth - displayWidth) / 2;
67
+ }
68
+ }
69
+ else {
70
+ // For other object-fit values, assume full element size
71
+ displayWidth = elementWidth;
72
+ displayHeight = elementHeight;
73
+ }
74
+ // Calculate scale factor from natural to displayed image
75
+ const scaleX = displayWidth / naturalWidth;
76
+ const scaleY = displayHeight / naturalHeight;
77
+ // Apply entity state transformations
78
+ const entityScale = entityState.scale || 1.0;
79
+ const entityTranslateX = entityState.translateX || 0;
80
+ const entityTranslateY = entityState.translateY || 0;
81
+ // Calculate face position in displayed coordinates
82
+ const faceDisplayWidth = cropArea.width * scaleX * entityScale;
83
+ const faceDisplayHeight = cropArea.height * scaleY * entityScale;
84
+ // Face center position
85
+ const faceCenterX = offsetX +
86
+ (cropArea.x + cropArea.width / 2) * scaleX * entityScale +
87
+ entityTranslateX;
88
+ const faceCenterY = offsetY +
89
+ (cropArea.y + cropArea.height / 2) * scaleY * entityScale +
90
+ entityTranslateY;
91
+ return {
92
+ x: faceCenterX,
93
+ y: faceCenterY,
94
+ width: faceDisplayWidth,
95
+ height: faceDisplayHeight,
96
+ };
97
+ }
98
+ /**
99
+ * Calculate effect position relative to face
100
+ */
101
+ export function calculateEffectPosition(facePosition, relativePosition, offsetX = 0, offsetY = 0) {
102
+ let baseX = facePosition.x;
103
+ let baseY = facePosition.y;
104
+ // Adjust base position based on relative position
105
+ switch (relativePosition) {
106
+ case "forehead":
107
+ baseY = facePosition.y - facePosition.height * 0.3;
108
+ break;
109
+ case "cheek_left":
110
+ baseX = facePosition.x - facePosition.width * 0.3;
111
+ break;
112
+ case "cheek_right":
113
+ baseX = facePosition.x + facePosition.width * 0.3;
114
+ break;
115
+ case "chin":
116
+ baseY = facePosition.y + facePosition.height * 0.3;
117
+ break;
118
+ case "center":
119
+ default:
120
+ // Use face center as is
121
+ break;
122
+ }
123
+ return {
124
+ x: baseX + offsetX,
125
+ y: baseY + offsetY,
126
+ };
127
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@luna-editor/engine",
3
+ "version": "0.1.0",
4
+ "description": "Luna Editor scenario playback engine",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./sdk": {
16
+ "types": "./dist/sdk.d.ts",
17
+ "default": "./dist/sdk.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "dev": "tsc --watch",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "peerDependencies": {
26
+ "react": "^18.0.0 || ^19.0.0",
27
+ "react-dom": "^18.0.0 || ^19.0.0"
28
+ },
29
+ "dependencies": {
30
+ "clsx": "^2.1.1",
31
+ "lucide-react": "^0.503.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^19",
35
+ "@types/react-dom": "^19",
36
+ "typescript": "^5",
37
+ "react": "^19.0.0",
38
+ "react-dom": "^19.0.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/instansys/luna-editor.git",
46
+ "directory": "packages/engine"
47
+ },
48
+ "keywords": [
49
+ "scenario",
50
+ "playback",
51
+ "visual-novel",
52
+ "game-engine"
53
+ ],
54
+ "license": "MIT"
55
+ }