@kawaijs/renderer-dom 0.1.11 → 0.1.12
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.
- package/README.md +34 -12
- package/dist/asset-fallbacks.d.ts +13 -0
- package/dist/asset-fallbacks.d.ts.map +1 -0
- package/dist/asset-fallbacks.js +41 -0
- package/dist/asset-fallbacks.js.map +1 -0
- package/dist/components/choice-menu.d.ts +12 -0
- package/dist/components/choice-menu.d.ts.map +1 -0
- package/dist/components/choice-menu.js +44 -0
- package/dist/components/choice-menu.js.map +1 -0
- package/dist/components/dialogue-box.d.ts +29 -0
- package/dist/components/dialogue-box.d.ts.map +1 -0
- package/dist/components/dialogue-box.js +155 -0
- package/dist/components/dialogue-box.js.map +1 -0
- package/dist/components/hotspot-layer.d.ts +13 -0
- package/dist/components/hotspot-layer.d.ts.map +1 -0
- package/dist/components/hotspot-layer.js +47 -0
- package/dist/components/hotspot-layer.js.map +1 -0
- package/dist/components/main-menu.d.ts +36 -0
- package/dist/components/main-menu.d.ts.map +1 -0
- package/dist/components/main-menu.js +257 -0
- package/dist/components/main-menu.js.map +1 -0
- package/dist/components/quick-menu.d.ts +22 -0
- package/dist/components/quick-menu.d.ts.map +1 -0
- package/dist/components/quick-menu.js +59 -0
- package/dist/components/quick-menu.js.map +1 -0
- package/dist/components/stage-layer.d.ts +39 -0
- package/dist/components/stage-layer.d.ts.map +1 -0
- package/dist/components/stage-layer.js +253 -0
- package/dist/components/stage-layer.js.map +1 -0
- package/dist/components/vfx-layer.d.ts +28 -0
- package/dist/components/vfx-layer.d.ts.map +1 -0
- package/dist/components/vfx-layer.js +310 -0
- package/dist/components/vfx-layer.js.map +1 -0
- package/dist/embed.d.ts +5 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +17 -0
- package/dist/embed.js.map +1 -0
- package/dist/icons.d.ts +31 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +31 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +31 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +69 -3
- package/dist/index.js.map +1 -1
- package/dist/layout/viewport-adapter.d.ts +52 -0
- package/dist/layout/viewport-adapter.d.ts.map +1 -0
- package/dist/layout/viewport-adapter.js +123 -0
- package/dist/layout/viewport-adapter.js.map +1 -0
- package/dist/modals/about-modal.d.ts +3 -0
- package/dist/modals/about-modal.d.ts.map +1 -0
- package/dist/modals/about-modal.js +59 -0
- package/dist/modals/about-modal.js.map +1 -0
- package/dist/modals/achievements-modal.d.ts +4 -0
- package/dist/modals/achievements-modal.d.ts.map +1 -0
- package/dist/modals/achievements-modal.js +103 -0
- package/dist/modals/achievements-modal.js.map +1 -0
- package/dist/modals/confirm-modal.d.ts +9 -0
- package/dist/modals/confirm-modal.d.ts.map +1 -0
- package/dist/modals/confirm-modal.js +80 -0
- package/dist/modals/confirm-modal.js.map +1 -0
- package/dist/modals/gallery-modal.d.ts +4 -0
- package/dist/modals/gallery-modal.d.ts.map +1 -0
- package/dist/modals/gallery-modal.js +128 -0
- package/dist/modals/gallery-modal.js.map +1 -0
- package/dist/modals/history-modal.d.ts +3 -0
- package/dist/modals/history-modal.d.ts.map +1 -0
- package/dist/modals/history-modal.js +66 -0
- package/dist/modals/history-modal.js.map +1 -0
- package/dist/modals/input-modal.d.ts +10 -0
- package/dist/modals/input-modal.d.ts.map +1 -0
- package/dist/modals/input-modal.js +62 -0
- package/dist/modals/input-modal.js.map +1 -0
- package/dist/modals/save-load-modal.d.ts +3 -0
- package/dist/modals/save-load-modal.d.ts.map +1 -0
- package/dist/modals/save-load-modal.js +262 -0
- package/dist/modals/save-load-modal.js.map +1 -0
- package/dist/modals/settings-modal.d.ts +14 -0
- package/dist/modals/settings-modal.d.ts.map +1 -0
- package/dist/modals/settings-modal.js +128 -0
- package/dist/modals/settings-modal.js.map +1 -0
- package/dist/renderer.d.ts +63 -56
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +643 -734
- package/dist/renderer.js.map +1 -1
- package/dist/share-meta.d.ts +27 -0
- package/dist/share-meta.d.ts.map +1 -0
- package/dist/share-meta.js +113 -0
- package/dist/share-meta.js.map +1 -0
- package/dist/touch.d.ts +16 -0
- package/dist/touch.d.ts.map +1 -0
- package/dist/touch.js +60 -0
- package/dist/touch.js.map +1 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +60 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/focus-trap.d.ts +6 -0
- package/dist/utils/focus-trap.d.ts.map +1 -0
- package/dist/utils/focus-trap.js +52 -0
- package/dist/utils/focus-trap.js.map +1 -0
- package/dist/utils/rich-text.d.ts +7 -0
- package/dist/utils/rich-text.d.ts.map +1 -0
- package/dist/utils/rich-text.js +31 -0
- package/dist/utils/rich-text.js.map +1 -0
- package/package.json +17 -3
- package/src/theme.css +1995 -685
package/dist/renderer.js
CHANGED
|
@@ -1,125 +1,126 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
import { AudioManager } from '@kawaijs/audio';
|
|
2
|
+
import { SVG_ICONS } from './icons.js';
|
|
3
|
+
import { defaultAssetResolver } from './types.js';
|
|
4
|
+
import { StageLayerComponent } from './components/stage-layer.js';
|
|
5
|
+
import { DialogueBoxComponent } from './components/dialogue-box.js';
|
|
6
|
+
import { ChoiceMenuComponent } from './components/choice-menu.js';
|
|
7
|
+
import { QuickMenuComponent } from './components/quick-menu.js';
|
|
8
|
+
import { MainMenuComponent } from './components/main-menu.js';
|
|
9
|
+
import { HotspotLayerComponent } from './components/hotspot-layer.js';
|
|
10
|
+
import { showSaveLoadModal } from './modals/save-load-modal.js';
|
|
11
|
+
import { showSettingsModal } from './modals/settings-modal.js';
|
|
12
|
+
import { showHistoryModal } from './modals/history-modal.js';
|
|
13
|
+
import { showAboutModal } from './modals/about-modal.js';
|
|
14
|
+
import { showGalleryModal } from './modals/gallery-modal.js';
|
|
15
|
+
import { showInputModal } from './modals/input-modal.js';
|
|
16
|
+
import { escapeHtml } from './utils/rich-text.js';
|
|
17
|
+
import { ViewportAdapter } from './layout/viewport-adapter.js';
|
|
18
|
+
import { applyDocumentShareMeta, buildSceneShareMeta, resolveShareAssetUrl } from './share-meta.js';
|
|
19
|
+
import { bindSwipeControls } from './touch.js';
|
|
20
|
+
export * from './icons.js';
|
|
21
|
+
export * from './types.js';
|
|
22
|
+
export * from './asset-fallbacks.js';
|
|
23
|
+
export * from './utils/rich-text.js';
|
|
24
|
+
export * from './utils/focus-trap.js';
|
|
25
|
+
export * from './layout/viewport-adapter.js';
|
|
26
|
+
export * from './modals/save-load-modal.js';
|
|
27
|
+
export * from './modals/settings-modal.js';
|
|
28
|
+
export * from './modals/history-modal.js';
|
|
29
|
+
export * from './modals/about-modal.js';
|
|
30
|
+
export * from './modals/gallery-modal.js';
|
|
31
|
+
export * from './modals/achievements-modal.js';
|
|
32
|
+
export * from './modals/confirm-modal.js';
|
|
33
|
+
export * from './modals/input-modal.js';
|
|
34
|
+
export * from './components/main-menu.js';
|
|
35
|
+
export * from './components/dialogue-box.js';
|
|
36
|
+
export * from './components/choice-menu.js';
|
|
37
|
+
export * from './components/quick-menu.js';
|
|
38
|
+
export * from './components/stage-layer.js';
|
|
39
|
+
export * from './components/vfx-layer.js';
|
|
40
|
+
export * from './share-meta.js';
|
|
41
|
+
export * from './embed.js';
|
|
42
|
+
export * from './touch.js';
|
|
43
|
+
export function extractGalleryItems(story) {
|
|
44
|
+
if (!story || !story.labels)
|
|
45
|
+
return [];
|
|
46
|
+
const itemsMap = new Map();
|
|
30
47
|
for (const label of Object.values(story.labels)) {
|
|
31
48
|
for (const inst of label) {
|
|
32
|
-
if (inst.type === '
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
if (inst.type === 'cg' && inst.image) {
|
|
50
|
+
const id = inst.unlockId ?? inst.image;
|
|
51
|
+
if (!itemsMap.has(id)) {
|
|
52
|
+
const rawBase = (inst.unlockId || inst.image).split(/[/\\]/).pop() || '';
|
|
53
|
+
const cleanName = rawBase.replace(/^cg[\s_]+/i, '').replace(/\.[^/.]+$/, '').replace(/[_-]+/g, ' ');
|
|
54
|
+
const title = cleanName ? cleanName.charAt(0).toUpperCase() + cleanName.slice(1) : 'Event CG';
|
|
55
|
+
itemsMap.set(id, {
|
|
56
|
+
id,
|
|
57
|
+
title,
|
|
58
|
+
image: inst.image
|
|
59
|
+
});
|
|
60
|
+
}
|
|
39
61
|
}
|
|
40
62
|
}
|
|
41
63
|
}
|
|
42
|
-
|
|
43
|
-
const img = new Image();
|
|
44
|
-
img.src = assetResolver(bg, 'background');
|
|
45
|
-
}
|
|
46
|
-
for (const char of charSet) {
|
|
47
|
-
const img = new Image();
|
|
48
|
-
img.src = assetResolver(char, 'character');
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
export const SVG_ICONS = {
|
|
52
|
-
back: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>',
|
|
53
|
-
history: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15 15"/></svg>',
|
|
54
|
-
save: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>',
|
|
55
|
-
load: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>',
|
|
56
|
-
auto: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>',
|
|
57
|
-
skip: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19"/></svg>',
|
|
58
|
-
settings: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
|
|
59
|
-
volumeOn: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>',
|
|
60
|
-
volumeMute: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/></svg>',
|
|
61
|
-
trash: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
|
|
62
|
-
close: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
|
|
63
|
-
arrowDown: '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>',
|
|
64
|
-
replay: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>'
|
|
65
|
-
};
|
|
66
|
-
/**
|
|
67
|
-
* Parses inline rich text markup: {b}...{/b}, {i}...{/i}, {color=#hex}...{/color}, {size=1.2em}...{/size}
|
|
68
|
-
*/
|
|
69
|
-
export function formatRichText(raw) {
|
|
70
|
-
let formatted = raw
|
|
71
|
-
.replace(/&/g, '&')
|
|
72
|
-
.replace(/</g, '<')
|
|
73
|
-
.replace(/>/g, '>');
|
|
74
|
-
// {b}...{/b}
|
|
75
|
-
formatted = formatted.replace(/\{b\}(.*?)\{\/b\}/gi, '<strong class="kawa-bold">$1</strong>');
|
|
76
|
-
// {i}...{/i}
|
|
77
|
-
formatted = formatted.replace(/\{i\}(.*?)\{\/i\}/gi, '<em class="kawa-italic">$1</em>');
|
|
78
|
-
// {color=#hex}...{/color}
|
|
79
|
-
formatted = formatted.replace(/\{color=([^}]+)\}(.*?)\{\/color\}/gi, '<span style="color:$1">$2</span>');
|
|
80
|
-
// {size=1.2em}...{/size}
|
|
81
|
-
formatted = formatted.replace(/\{size=([^}]+)\}(.*?)\{\/size\}/gi, '<span style="font-size:$1">$2</span>');
|
|
82
|
-
return formatted;
|
|
64
|
+
return Array.from(itemsMap.values());
|
|
83
65
|
}
|
|
84
66
|
export class DOMRenderer {
|
|
85
67
|
vm;
|
|
86
68
|
container;
|
|
69
|
+
options;
|
|
87
70
|
typewriterSpeed;
|
|
88
71
|
autoDelayMs;
|
|
72
|
+
musicVolume;
|
|
73
|
+
soundVolume;
|
|
89
74
|
assetResolver;
|
|
75
|
+
mainMenuOptions;
|
|
76
|
+
audioManager;
|
|
90
77
|
rootEl;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
modeBadgeEl;
|
|
100
|
-
dialogueBoxEl;
|
|
101
|
-
speakerTagEl;
|
|
102
|
-
dialogueTextEl;
|
|
103
|
-
choiceContainerEl;
|
|
104
|
-
backBtnEl;
|
|
105
|
-
autoBtnEl;
|
|
106
|
-
skipBtnEl;
|
|
107
|
-
currentTypewriterInterval = null;
|
|
108
|
-
isTypewriting = false;
|
|
109
|
-
fullCurrentText = '';
|
|
110
|
-
isChoicePending = false;
|
|
78
|
+
stageLayer;
|
|
79
|
+
dialogueBox;
|
|
80
|
+
choiceMenu;
|
|
81
|
+
hotspotLayer;
|
|
82
|
+
quickMenu;
|
|
83
|
+
mainMenu;
|
|
84
|
+
viewportAdapter;
|
|
85
|
+
appliedStyleTargets = new Set();
|
|
111
86
|
isAutoMode = false;
|
|
112
87
|
isSkipMode = false;
|
|
113
88
|
autoTimer = null;
|
|
114
89
|
skipInterval = null;
|
|
90
|
+
pauseTimer = null;
|
|
91
|
+
errorToastTimer = null;
|
|
92
|
+
advanceLockUntil = 0;
|
|
93
|
+
destroyed = false;
|
|
115
94
|
unsubscribeVMState;
|
|
95
|
+
unsubscribeCamera;
|
|
96
|
+
unsubscribeAudio;
|
|
97
|
+
unsubscribeError;
|
|
116
98
|
boundKeyHandler;
|
|
99
|
+
touchControls;
|
|
117
100
|
constructor(vm, options) {
|
|
118
101
|
this.vm = vm;
|
|
119
102
|
this.container = options.container;
|
|
103
|
+
this.options = options;
|
|
120
104
|
this.typewriterSpeed = options.typewriterSpeed ?? 20;
|
|
121
105
|
this.autoDelayMs = options.autoDelayMs ?? 1800;
|
|
122
106
|
this.assetResolver = options.assetResolver ?? defaultAssetResolver;
|
|
107
|
+
let finalMainMenu = options.mainMenu;
|
|
108
|
+
if (options.embed) {
|
|
109
|
+
finalMainMenu = { ...finalMainMenu, enabled: false };
|
|
110
|
+
}
|
|
111
|
+
if (finalMainMenu?.enabled !== false && (!finalMainMenu?.galleryItems || finalMainMenu.galleryItems.length === 0)) {
|
|
112
|
+
const autoItems = extractGalleryItems(this.vm.getStory());
|
|
113
|
+
if (autoItems.length > 0) {
|
|
114
|
+
finalMainMenu = {
|
|
115
|
+
...finalMainMenu,
|
|
116
|
+
galleryItems: autoItems
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.mainMenuOptions = finalMainMenu;
|
|
121
|
+
this.audioManager = options.audioManager ?? (typeof window !== 'undefined' ? new AudioManager() : undefined);
|
|
122
|
+
this.musicVolume = this.audioManager?.getMusicVolume?.() ?? 0.8;
|
|
123
|
+
this.soundVolume = this.audioManager?.getSoundVolume?.() ?? 1.0;
|
|
123
124
|
// Load saved settings if present
|
|
124
125
|
if (typeof localStorage !== 'undefined') {
|
|
125
126
|
try {
|
|
@@ -130,6 +131,14 @@ export class DOMRenderer {
|
|
|
130
131
|
this.typewriterSpeed = parsed.typewriterSpeed;
|
|
131
132
|
if (typeof parsed.autoDelayMs === 'number')
|
|
132
133
|
this.autoDelayMs = parsed.autoDelayMs;
|
|
134
|
+
if (typeof parsed.musicVolume === 'number') {
|
|
135
|
+
this.musicVolume = parsed.musicVolume;
|
|
136
|
+
this.audioManager?.setMusicVolume(this.musicVolume);
|
|
137
|
+
}
|
|
138
|
+
if (typeof parsed.soundVolume === 'number') {
|
|
139
|
+
this.soundVolume = parsed.soundVolume;
|
|
140
|
+
this.audioManager?.setSoundVolume(this.soundVolume);
|
|
141
|
+
}
|
|
133
142
|
}
|
|
134
143
|
}
|
|
135
144
|
catch { }
|
|
@@ -138,47 +147,152 @@ export class DOMRenderer {
|
|
|
138
147
|
this.bindEvents();
|
|
139
148
|
this.unsubscribeVMState = this.vm.onStateChange((state) => {
|
|
140
149
|
this.render(state);
|
|
150
|
+
this.syncUrlLabel(state);
|
|
151
|
+
this.syncShareMeta(state);
|
|
152
|
+
});
|
|
153
|
+
this.unsubscribeCamera = this.vm.onCameraEvent((e) => {
|
|
154
|
+
if (e.action === 'flash') {
|
|
155
|
+
this.flashScreen();
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.shakeScreen(e.action);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
this.unsubscribeAudio = this.vm.onAudioEvent((event) => {
|
|
162
|
+
if (!this.audioManager)
|
|
163
|
+
return;
|
|
164
|
+
if (event.action === 'play' && event.track) {
|
|
165
|
+
const url = this.assetResolver(event.track, 'audio');
|
|
166
|
+
if (event.channel === 'music') {
|
|
167
|
+
this.audioManager.playMusic?.(url, { fadein: event.fade, loop: event.loop });
|
|
168
|
+
}
|
|
169
|
+
else if (event.channel === 'sound') {
|
|
170
|
+
this.audioManager.playSound?.(url);
|
|
171
|
+
}
|
|
172
|
+
else if (event.channel === 'voice') {
|
|
173
|
+
this.audioManager.playVoice?.(url);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (event.action === 'stop') {
|
|
177
|
+
if (event.channel === 'music') {
|
|
178
|
+
this.audioManager.stopMusic?.({ fadeout: event.fade });
|
|
179
|
+
}
|
|
180
|
+
else if (event.channel === 'sound') {
|
|
181
|
+
this.audioManager.stopSound?.();
|
|
182
|
+
}
|
|
183
|
+
else if (event.channel === 'voice') {
|
|
184
|
+
this.audioManager.stopVoice?.();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
this.unsubscribeError = this.vm.onError((err) => {
|
|
189
|
+
this.showErrorToast(err.message);
|
|
190
|
+
this.options.onError?.(err);
|
|
141
191
|
});
|
|
142
|
-
//
|
|
143
|
-
|
|
192
|
+
// Default: Show Ren'Py-style Main Menu before entering story, unless explicitly disabled
|
|
193
|
+
if (this.mainMenuOptions?.enabled !== false) {
|
|
194
|
+
this.showMainMenu();
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
this.render(this.vm.getState());
|
|
198
|
+
}
|
|
144
199
|
}
|
|
145
200
|
destroy() {
|
|
201
|
+
if (this.destroyed)
|
|
202
|
+
return;
|
|
203
|
+
this.destroyed = true;
|
|
146
204
|
if (this.unsubscribeVMState) {
|
|
147
205
|
this.unsubscribeVMState();
|
|
206
|
+
this.unsubscribeVMState = undefined;
|
|
207
|
+
}
|
|
208
|
+
if (this.unsubscribeCamera) {
|
|
209
|
+
this.unsubscribeCamera();
|
|
210
|
+
this.unsubscribeCamera = undefined;
|
|
211
|
+
}
|
|
212
|
+
if (this.unsubscribeAudio) {
|
|
213
|
+
this.unsubscribeAudio();
|
|
214
|
+
this.unsubscribeAudio = undefined;
|
|
215
|
+
}
|
|
216
|
+
if (this.audioManager && typeof this.audioManager.destroy === 'function') {
|
|
217
|
+
this.audioManager.destroy();
|
|
218
|
+
}
|
|
219
|
+
if (this.unsubscribeError) {
|
|
220
|
+
this.unsubscribeError();
|
|
221
|
+
this.unsubscribeError = undefined;
|
|
148
222
|
}
|
|
149
|
-
if (this.
|
|
150
|
-
|
|
223
|
+
if (this.dialogueBox) {
|
|
224
|
+
this.dialogueBox.destroy();
|
|
225
|
+
}
|
|
226
|
+
if (this.hotspotLayer) {
|
|
227
|
+
this.hotspotLayer.destroy();
|
|
228
|
+
}
|
|
229
|
+
if (this.stageLayer) {
|
|
230
|
+
this.stageLayer.destroy();
|
|
231
|
+
}
|
|
232
|
+
if (this.viewportAdapter) {
|
|
233
|
+
this.viewportAdapter.destroy();
|
|
234
|
+
this.viewportAdapter = undefined;
|
|
151
235
|
}
|
|
152
236
|
if (this.autoTimer) {
|
|
153
237
|
clearTimeout(this.autoTimer);
|
|
238
|
+
this.autoTimer = null;
|
|
154
239
|
}
|
|
155
240
|
if (this.skipInterval) {
|
|
156
241
|
clearInterval(this.skipInterval);
|
|
242
|
+
this.skipInterval = null;
|
|
243
|
+
}
|
|
244
|
+
if (this.pauseTimer) {
|
|
245
|
+
clearTimeout(this.pauseTimer);
|
|
246
|
+
this.pauseTimer = null;
|
|
247
|
+
}
|
|
248
|
+
if (this.errorToastTimer) {
|
|
249
|
+
clearTimeout(this.errorToastTimer);
|
|
250
|
+
this.errorToastTimer = null;
|
|
157
251
|
}
|
|
158
252
|
if (this.boundKeyHandler && typeof window !== 'undefined') {
|
|
159
253
|
window.removeEventListener('keydown', this.boundKeyHandler);
|
|
254
|
+
this.boundKeyHandler = undefined;
|
|
255
|
+
}
|
|
256
|
+
if (this.touchControls) {
|
|
257
|
+
this.touchControls.destroy();
|
|
258
|
+
this.touchControls = undefined;
|
|
160
259
|
}
|
|
161
260
|
this.container.innerHTML = '';
|
|
162
261
|
}
|
|
262
|
+
showErrorToast(message) {
|
|
263
|
+
const existing = this.rootEl.querySelector('.kawa-error-toast');
|
|
264
|
+
if (existing)
|
|
265
|
+
existing.remove();
|
|
266
|
+
if (this.errorToastTimer) {
|
|
267
|
+
clearTimeout(this.errorToastTimer);
|
|
268
|
+
this.errorToastTimer = null;
|
|
269
|
+
}
|
|
270
|
+
const toast = document.createElement('div');
|
|
271
|
+
toast.className = 'kawa-error-toast';
|
|
272
|
+
toast.setAttribute('role', 'alert');
|
|
273
|
+
toast.innerHTML = `
|
|
274
|
+
<span>⚠️</span>
|
|
275
|
+
<span class="kawa-error-toast-msg">${escapeHtml(message)}</span>
|
|
276
|
+
<button type="button" class="kawa-error-toast-close" aria-label="Dismiss error">${SVG_ICONS.close}</button>
|
|
277
|
+
`;
|
|
278
|
+
toast.querySelector('.kawa-error-toast-close')?.addEventListener('click', () => {
|
|
279
|
+
toast.remove();
|
|
280
|
+
});
|
|
281
|
+
this.rootEl.appendChild(toast);
|
|
282
|
+
this.errorToastTimer = window.setTimeout(() => {
|
|
283
|
+
this.errorToastTimer = null;
|
|
284
|
+
if (toast.parentElement)
|
|
285
|
+
toast.remove();
|
|
286
|
+
}, 8000);
|
|
287
|
+
}
|
|
288
|
+
getViewportAdapter() {
|
|
289
|
+
return this.viewportAdapter;
|
|
290
|
+
}
|
|
163
291
|
shakeScreen(type = 'shake') {
|
|
164
|
-
|
|
165
|
-
return;
|
|
166
|
-
this.stageEl.classList.remove('kawa-shake', 'kawa-vpunch', 'kawa-hpunch');
|
|
167
|
-
// Trigger reflow
|
|
168
|
-
void this.stageEl.offsetWidth;
|
|
169
|
-
const cls = type === 'vpunch' ? 'kawa-vpunch' : type === 'hpunch' ? 'kawa-hpunch' : 'kawa-shake';
|
|
170
|
-
this.stageEl.classList.add(cls);
|
|
292
|
+
this.stageLayer.shakeScreen(type);
|
|
171
293
|
}
|
|
172
294
|
flashScreen() {
|
|
173
|
-
|
|
174
|
-
return;
|
|
175
|
-
const existing = this.stageEl.querySelector('.kawa-flash-overlay');
|
|
176
|
-
if (existing)
|
|
177
|
-
existing.remove();
|
|
178
|
-
const flash = document.createElement('div');
|
|
179
|
-
flash.className = 'kawa-flash-overlay';
|
|
180
|
-
this.stageEl.appendChild(flash);
|
|
181
|
-
setTimeout(() => flash.remove(), 600);
|
|
295
|
+
this.stageLayer.flashScreen();
|
|
182
296
|
}
|
|
183
297
|
toggleAutoMode(force) {
|
|
184
298
|
this.isAutoMode = force !== undefined ? force : !this.isAutoMode;
|
|
@@ -186,7 +300,7 @@ export class DOMRenderer {
|
|
|
186
300
|
this.toggleSkipMode(false);
|
|
187
301
|
}
|
|
188
302
|
this.updateModeUI();
|
|
189
|
-
if (this.isAutoMode && !this.
|
|
303
|
+
if (this.isAutoMode && !this.dialogueBox.getIsTypewriting()) {
|
|
190
304
|
this.scheduleAutoAdvance();
|
|
191
305
|
}
|
|
192
306
|
else if (!this.isAutoMode && this.autoTimer) {
|
|
@@ -200,23 +314,33 @@ export class DOMRenderer {
|
|
|
200
314
|
this.toggleAutoMode(false);
|
|
201
315
|
}
|
|
202
316
|
this.updateModeUI();
|
|
317
|
+
// Suspend decorative particle loops while skipping to cut CPU during fast-forward.
|
|
318
|
+
this.stageLayer.vfxLayer.setSuspended(this.isSkipMode);
|
|
203
319
|
if (this.isSkipMode) {
|
|
204
320
|
if (this.autoTimer) {
|
|
205
321
|
clearTimeout(this.autoTimer);
|
|
206
322
|
this.autoTimer = null;
|
|
207
323
|
}
|
|
208
324
|
if (!this.skipInterval) {
|
|
325
|
+
// Coalesce advances (~10/s) instead of hammering every 55ms.
|
|
209
326
|
this.skipInterval = window.setInterval(() => {
|
|
327
|
+
if (this.destroyed) {
|
|
328
|
+
this.toggleSkipMode(false);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
210
331
|
const state = this.vm.getState();
|
|
211
|
-
if (state.isFinished ||
|
|
332
|
+
if (state.isFinished ||
|
|
333
|
+
(state.choices && state.choices.length > 0) ||
|
|
334
|
+
(state.hotspots && state.hotspots.length > 0) ||
|
|
335
|
+
state.pendingInput) {
|
|
212
336
|
this.toggleSkipMode(false);
|
|
213
337
|
return;
|
|
214
338
|
}
|
|
215
|
-
if (this.
|
|
216
|
-
this.finishTypewriter();
|
|
339
|
+
if (this.dialogueBox.getIsTypewriting()) {
|
|
340
|
+
this.dialogueBox.finishTypewriter();
|
|
217
341
|
}
|
|
218
342
|
this.vm.next();
|
|
219
|
-
},
|
|
343
|
+
}, 100);
|
|
220
344
|
}
|
|
221
345
|
}
|
|
222
346
|
else {
|
|
@@ -226,25 +350,91 @@ export class DOMRenderer {
|
|
|
226
350
|
}
|
|
227
351
|
}
|
|
228
352
|
}
|
|
353
|
+
showMainMenu() {
|
|
354
|
+
if (this.isAutoMode)
|
|
355
|
+
this.toggleAutoMode(false);
|
|
356
|
+
if (this.isSkipMode)
|
|
357
|
+
this.toggleSkipMode(false);
|
|
358
|
+
this.mainMenu.show();
|
|
359
|
+
}
|
|
360
|
+
hideMainMenu() {
|
|
361
|
+
this.mainMenu.hide();
|
|
362
|
+
}
|
|
363
|
+
isMainMenuActive() {
|
|
364
|
+
return this.mainMenu.isActive();
|
|
365
|
+
}
|
|
366
|
+
startNewGame() {
|
|
367
|
+
// Ignore residual click-through from the Start button onto the stage (Opera / touchpads).
|
|
368
|
+
this.advanceLockUntil = Date.now() + 350;
|
|
369
|
+
this.vm.start();
|
|
370
|
+
this.hideMainMenu();
|
|
371
|
+
this.mainMenuOptions?.onStart?.();
|
|
372
|
+
}
|
|
373
|
+
async continueLatestGame() {
|
|
374
|
+
const saveManager = this.vm.getSaveManager();
|
|
375
|
+
const slots = await saveManager.listSlots(10);
|
|
376
|
+
const validSlots = slots.filter((s) => s !== null);
|
|
377
|
+
if (validSlots.length === 0)
|
|
378
|
+
return false;
|
|
379
|
+
validSlots.sort((a, b) => b.timestamp - a.timestamp);
|
|
380
|
+
const latest = validSlots[0];
|
|
381
|
+
const ok = await this.vm.load(latest.id);
|
|
382
|
+
if (ok) {
|
|
383
|
+
this.hideMainMenu();
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
async showSaveLoadModal(mode) {
|
|
389
|
+
await showSaveLoadModal(this.rootEl, this.vm, mode, () => {
|
|
390
|
+
this.hideMainMenu();
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
showSettingsModal() {
|
|
394
|
+
showSettingsModal(this.rootEl, {
|
|
395
|
+
typewriterSpeed: this.typewriterSpeed,
|
|
396
|
+
autoDelayMs: this.autoDelayMs,
|
|
397
|
+
musicVolume: this.musicVolume,
|
|
398
|
+
soundVolume: this.soundVolume,
|
|
399
|
+
onSettingsChange: (s) => {
|
|
400
|
+
this.typewriterSpeed = s.typewriterSpeed;
|
|
401
|
+
this.autoDelayMs = s.autoDelayMs;
|
|
402
|
+
if (s.musicVolume !== undefined) {
|
|
403
|
+
this.musicVolume = s.musicVolume;
|
|
404
|
+
this.audioManager?.setMusicVolume(this.musicVolume);
|
|
405
|
+
}
|
|
406
|
+
if (s.soundVolume !== undefined) {
|
|
407
|
+
this.soundVolume = s.soundVolume;
|
|
408
|
+
this.audioManager?.setSoundVolume(this.soundVolume);
|
|
409
|
+
}
|
|
410
|
+
this.dialogueBox.setTypewriterSpeed(this.typewriterSpeed);
|
|
411
|
+
this.saveSettings();
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
showHistoryModal() {
|
|
416
|
+
showHistoryModal(this.rootEl, this.vm);
|
|
417
|
+
}
|
|
418
|
+
showAboutModal() {
|
|
419
|
+
showAboutModal(this.rootEl, this.mainMenuOptions);
|
|
420
|
+
}
|
|
421
|
+
showGalleryModal() {
|
|
422
|
+
const galleryItems = this.mainMenuOptions?.galleryItems ?? [];
|
|
423
|
+
showGalleryModal(this.rootEl, this.vm, galleryItems, (path) => this.assetResolver(path, 'background'));
|
|
424
|
+
}
|
|
229
425
|
updateModeUI() {
|
|
230
|
-
|
|
231
|
-
|
|
426
|
+
this.quickMenu.setAutoActive(this.isAutoMode);
|
|
427
|
+
this.quickMenu.setSkipActive(this.isSkipMode);
|
|
428
|
+
if (this.isSkipMode) {
|
|
429
|
+
this.stageLayer.modeBadgeEl.innerHTML = `${SVG_ICONS.skip} SKIP`;
|
|
430
|
+
this.stageLayer.modeBadgeEl.style.display = 'inline-flex';
|
|
232
431
|
}
|
|
233
|
-
if (this.
|
|
234
|
-
this.
|
|
432
|
+
else if (this.isAutoMode) {
|
|
433
|
+
this.stageLayer.modeBadgeEl.innerHTML = `${SVG_ICONS.auto} AUTO`;
|
|
434
|
+
this.stageLayer.modeBadgeEl.style.display = 'inline-flex';
|
|
235
435
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
this.modeBadgeEl.innerHTML = `${SVG_ICONS.skip} SKIP`;
|
|
239
|
-
this.modeBadgeEl.style.display = 'inline-flex';
|
|
240
|
-
}
|
|
241
|
-
else if (this.isAutoMode) {
|
|
242
|
-
this.modeBadgeEl.innerHTML = `${SVG_ICONS.auto} AUTO`;
|
|
243
|
-
this.modeBadgeEl.style.display = 'inline-flex';
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
this.modeBadgeEl.style.display = 'none';
|
|
247
|
-
}
|
|
436
|
+
else {
|
|
437
|
+
this.stageLayer.modeBadgeEl.style.display = 'none';
|
|
248
438
|
}
|
|
249
439
|
}
|
|
250
440
|
scheduleAutoAdvance() {
|
|
@@ -255,14 +445,20 @@ export class DOMRenderer {
|
|
|
255
445
|
if (!this.isAutoMode)
|
|
256
446
|
return;
|
|
257
447
|
const state = this.vm.getState();
|
|
258
|
-
if (state.isFinished ||
|
|
448
|
+
if (state.isFinished ||
|
|
449
|
+
(state.choices && state.choices.length > 0) ||
|
|
450
|
+
(state.hotspots && state.hotspots.length > 0) ||
|
|
451
|
+
state.pendingInput) {
|
|
259
452
|
return;
|
|
260
453
|
}
|
|
261
454
|
const duration = Math.max(800, this.autoDelayMs + (state.dialogue?.text.length ?? 0) * 15);
|
|
262
455
|
this.autoTimer = window.setTimeout(() => {
|
|
263
456
|
if (this.isAutoMode) {
|
|
264
457
|
const cur = this.vm.getState();
|
|
265
|
-
if (!cur.isFinished &&
|
|
458
|
+
if (!cur.isFinished &&
|
|
459
|
+
(!cur.choices || cur.choices.length === 0) &&
|
|
460
|
+
(!cur.hotspots || cur.hotspots.length === 0) &&
|
|
461
|
+
!cur.pendingInput) {
|
|
266
462
|
this.vm.next();
|
|
267
463
|
}
|
|
268
464
|
}
|
|
@@ -272,155 +468,137 @@ export class DOMRenderer {
|
|
|
272
468
|
this.container.innerHTML = '';
|
|
273
469
|
this.rootEl = document.createElement('div');
|
|
274
470
|
this.rootEl.className = 'kawa-root';
|
|
275
|
-
this.
|
|
276
|
-
this.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
this.
|
|
281
|
-
this.backgroundEl.setAttribute('aria-hidden', 'true');
|
|
282
|
-
this.bgLayerA = document.createElement('div');
|
|
283
|
-
this.bgLayerA.className = 'kawa-bg-layer active';
|
|
284
|
-
this.bgLayerB = document.createElement('div');
|
|
285
|
-
this.bgLayerB.className = 'kawa-bg-layer';
|
|
286
|
-
this.backgroundEl.appendChild(this.bgLayerA);
|
|
287
|
-
this.backgroundEl.appendChild(this.bgLayerB);
|
|
288
|
-
this.charactersEl = document.createElement('div');
|
|
289
|
-
this.charactersEl.className = 'kawa-characters kawa-sprites';
|
|
290
|
-
this.charactersEl.setAttribute('aria-hidden', 'true');
|
|
291
|
-
// Mode Badge (Auto / Skip indicator)
|
|
292
|
-
this.modeBadgeEl = document.createElement('div');
|
|
293
|
-
this.modeBadgeEl.className = 'kawa-mode-badge';
|
|
294
|
-
this.modeBadgeEl.style.display = 'none';
|
|
471
|
+
this.rootEl.style.touchAction = 'manipulation';
|
|
472
|
+
if (this.options.embed) {
|
|
473
|
+
this.rootEl.classList.add('kawa-embed');
|
|
474
|
+
this.rootEl.dataset.kawaEmbed = '1';
|
|
475
|
+
}
|
|
476
|
+
this.stageLayer = new StageLayerComponent(this.assetResolver);
|
|
295
477
|
const uiLayerEl = document.createElement('div');
|
|
296
478
|
uiLayerEl.className = 'kawa-ui-layer';
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
this.
|
|
309
|
-
|
|
310
|
-
this.speakerTagEl.className = 'kawa-speaker-tag kawa-speaker';
|
|
311
|
-
this.dialogueTextEl = document.createElement('div');
|
|
312
|
-
this.dialogueTextEl.className = 'kawa-dialogue-text kawa-text';
|
|
313
|
-
this.dialogueTextEl.setAttribute('aria-live', 'polite');
|
|
314
|
-
const indicatorEl = document.createElement('div');
|
|
315
|
-
indicatorEl.className = 'kawa-continue-indicator';
|
|
316
|
-
indicatorEl.setAttribute('aria-hidden', 'true');
|
|
317
|
-
indicatorEl.innerHTML = SVG_ICONS.arrowDown;
|
|
318
|
-
this.dialogueBoxEl.appendChild(this.speakerTagEl);
|
|
319
|
-
this.dialogueBoxEl.appendChild(this.dialogueTextEl);
|
|
320
|
-
this.dialogueBoxEl.appendChild(indicatorEl);
|
|
321
|
-
// Quick Menu
|
|
322
|
-
const quickMenuEl = document.createElement('nav');
|
|
323
|
-
quickMenuEl.className = 'kawa-quick-menu';
|
|
324
|
-
quickMenuEl.setAttribute('role', 'toolbar');
|
|
325
|
-
quickMenuEl.setAttribute('aria-label', 'Game Menu');
|
|
326
|
-
this.backBtnEl = document.createElement('button');
|
|
327
|
-
this.backBtnEl.className = 'kawa-btn';
|
|
328
|
-
this.backBtnEl.innerHTML = `${SVG_ICONS.back} <span>Back</span>`;
|
|
329
|
-
this.backBtnEl.setAttribute('aria-label', 'Rollback to previous dialogue');
|
|
330
|
-
const histBtn = document.createElement('button');
|
|
331
|
-
histBtn.className = 'kawa-btn';
|
|
332
|
-
histBtn.innerHTML = `${SVG_ICONS.history} <span>History</span>`;
|
|
333
|
-
histBtn.setAttribute('aria-label', 'Open dialogue history log');
|
|
334
|
-
histBtn.addEventListener('click', (e) => {
|
|
335
|
-
e.stopPropagation();
|
|
336
|
-
this.showHistoryModal();
|
|
337
|
-
});
|
|
338
|
-
this.autoBtnEl = document.createElement('button');
|
|
339
|
-
this.autoBtnEl.className = 'kawa-btn';
|
|
340
|
-
this.autoBtnEl.innerHTML = `${SVG_ICONS.auto} <span>Auto</span>`;
|
|
341
|
-
this.autoBtnEl.setAttribute('aria-label', 'Toggle Auto Forward Mode');
|
|
342
|
-
this.autoBtnEl.addEventListener('click', (e) => {
|
|
343
|
-
e.stopPropagation();
|
|
344
|
-
this.toggleAutoMode();
|
|
345
|
-
});
|
|
346
|
-
this.skipBtnEl = document.createElement('button');
|
|
347
|
-
this.skipBtnEl.className = 'kawa-btn';
|
|
348
|
-
this.skipBtnEl.innerHTML = `${SVG_ICONS.skip} <span>Skip</span>`;
|
|
349
|
-
this.skipBtnEl.setAttribute('aria-label', 'Toggle Fast Forward Skip Mode');
|
|
350
|
-
this.skipBtnEl.addEventListener('click', (e) => {
|
|
351
|
-
e.stopPropagation();
|
|
352
|
-
this.toggleSkipMode();
|
|
353
|
-
});
|
|
354
|
-
const saveBtn = document.createElement('button');
|
|
355
|
-
saveBtn.className = 'kawa-btn';
|
|
356
|
-
saveBtn.innerHTML = `${SVG_ICONS.save} <span>Save</span>`;
|
|
357
|
-
saveBtn.setAttribute('aria-label', 'Open save game menu');
|
|
358
|
-
saveBtn.addEventListener('click', (e) => {
|
|
359
|
-
e.stopPropagation();
|
|
360
|
-
this.showSaveLoadModal('save');
|
|
479
|
+
this.choiceMenu = this.options.components?.createChoiceMenu
|
|
480
|
+
? this.options.components.createChoiceMenu({
|
|
481
|
+
onSelect: (index) => {
|
|
482
|
+
this.vm.choose(index);
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
: new ChoiceMenuComponent({
|
|
486
|
+
onSelect: (index) => {
|
|
487
|
+
this.vm.choose(index);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
this.hotspotLayer = new HotspotLayerComponent((id) => {
|
|
491
|
+
this.vm.selectHotspot(id);
|
|
361
492
|
});
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
493
|
+
this.dialogueBox = this.options.components?.createDialogueBox
|
|
494
|
+
? this.options.components.createDialogueBox(this.typewriterSpeed)
|
|
495
|
+
: new DialogueBoxComponent(this.typewriterSpeed);
|
|
496
|
+
this.quickMenu = new QuickMenuComponent({
|
|
497
|
+
onTitle: () => this.showMainMenu(),
|
|
498
|
+
onBack: () => {
|
|
499
|
+
if (this.isAutoMode)
|
|
500
|
+
this.toggleAutoMode(false);
|
|
501
|
+
if (this.isSkipMode)
|
|
502
|
+
this.toggleSkipMode(false);
|
|
503
|
+
this.vm.rollback();
|
|
504
|
+
},
|
|
505
|
+
onHistory: () => this.showHistoryModal(),
|
|
506
|
+
onAuto: () => this.toggleAutoMode(),
|
|
507
|
+
onSkip: () => this.toggleSkipMode(),
|
|
508
|
+
onSave: () => void this.showSaveLoadModal('save'),
|
|
509
|
+
onLoad: () => void this.showSaveLoadModal('load'),
|
|
510
|
+
onSettings: () => this.showSettingsModal()
|
|
369
511
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
512
|
+
if (this.options.features?.quickMenu === false || this.options.embed) {
|
|
513
|
+
this.quickMenu.el.style.display = 'none';
|
|
514
|
+
}
|
|
515
|
+
uiLayerEl.appendChild(this.choiceMenu.el);
|
|
516
|
+
uiLayerEl.appendChild(this.dialogueBox.el);
|
|
517
|
+
uiLayerEl.appendChild(this.quickMenu.el);
|
|
518
|
+
this.stageLayer.stageEl.appendChild(uiLayerEl);
|
|
519
|
+
this.stageLayer.stageEl.appendChild(this.hotspotLayer.el);
|
|
520
|
+
this.mainMenu = new MainMenuComponent(this.rootEl, this.vm, this.mainMenuOptions, {
|
|
521
|
+
onStartNewGame: () => this.startNewGame(),
|
|
522
|
+
onContinueGame: () => this.continueLatestGame(),
|
|
523
|
+
assetResolver: this.assetResolver,
|
|
524
|
+
getSettingsConfig: () => ({
|
|
525
|
+
typewriterSpeed: this.typewriterSpeed,
|
|
526
|
+
autoDelayMs: this.autoDelayMs,
|
|
527
|
+
musicVolume: this.musicVolume,
|
|
528
|
+
soundVolume: this.soundVolume,
|
|
529
|
+
onSettingsChange: (s) => {
|
|
530
|
+
this.typewriterSpeed = s.typewriterSpeed;
|
|
531
|
+
this.autoDelayMs = s.autoDelayMs;
|
|
532
|
+
if (s.musicVolume !== undefined) {
|
|
533
|
+
this.musicVolume = s.musicVolume;
|
|
534
|
+
this.audioManager?.setMusicVolume(this.musicVolume);
|
|
535
|
+
}
|
|
536
|
+
if (s.soundVolume !== undefined) {
|
|
537
|
+
this.soundVolume = s.soundVolume;
|
|
538
|
+
this.audioManager?.setSoundVolume(this.soundVolume);
|
|
539
|
+
}
|
|
540
|
+
this.dialogueBox.setTypewriterSpeed(this.typewriterSpeed);
|
|
541
|
+
this.saveSettings();
|
|
542
|
+
}
|
|
543
|
+
})
|
|
377
544
|
});
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
quickMenuEl.appendChild(this.autoBtnEl);
|
|
381
|
-
quickMenuEl.appendChild(this.skipBtnEl);
|
|
382
|
-
quickMenuEl.appendChild(saveBtn);
|
|
383
|
-
quickMenuEl.appendChild(loadBtn);
|
|
384
|
-
quickMenuEl.appendChild(settingsBtn);
|
|
385
|
-
uiLayerEl.appendChild(this.choiceContainerEl);
|
|
386
|
-
uiLayerEl.appendChild(this.dialogueBoxEl);
|
|
387
|
-
uiLayerEl.appendChild(quickMenuEl);
|
|
388
|
-
this.stageEl.appendChild(this.backgroundEl);
|
|
389
|
-
this.stageEl.appendChild(this.charactersEl);
|
|
390
|
-
this.stageEl.appendChild(this.modeBadgeEl);
|
|
391
|
-
this.stageEl.appendChild(uiLayerEl);
|
|
392
|
-
this.rootEl.appendChild(this.stageEl);
|
|
545
|
+
this.mainMenu.mount(this.stageLayer.stageEl);
|
|
546
|
+
this.rootEl.appendChild(this.stageLayer.stageEl);
|
|
393
547
|
this.container.appendChild(this.rootEl);
|
|
548
|
+
this.viewportAdapter = new ViewportAdapter(this.container, this.stageLayer.stageEl, this.options.virtualCanvas);
|
|
394
549
|
}
|
|
395
550
|
bindEvents() {
|
|
396
|
-
// Clicking on dialogue advances or finishes typewriter
|
|
397
|
-
this.
|
|
398
|
-
if (this.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (
|
|
402
|
-
|
|
551
|
+
// Clicking on stage or dialogue box advances or finishes typewriter
|
|
552
|
+
this.stageLayer.stageEl.addEventListener('click', (e) => {
|
|
553
|
+
if (this.isMainMenuActive())
|
|
554
|
+
return;
|
|
555
|
+
const target = e.target;
|
|
556
|
+
if (target.closest('button') ||
|
|
557
|
+
target.closest('.kawa-quick-menu') ||
|
|
558
|
+
target.closest('.kawa-choice-container') ||
|
|
559
|
+
target.closest('.kawa-hotspot-layer') ||
|
|
560
|
+
target.closest('.kawa-modal-overlay') ||
|
|
561
|
+
target.closest('.kawa-ending-card')) {
|
|
562
|
+
return;
|
|
403
563
|
}
|
|
404
|
-
this.handleUserAdvance();
|
|
405
|
-
});
|
|
406
|
-
this.backBtnEl.addEventListener('click', (e) => {
|
|
407
|
-
e.stopPropagation();
|
|
408
564
|
if (this.isAutoMode)
|
|
409
565
|
this.toggleAutoMode(false);
|
|
410
566
|
if (this.isSkipMode)
|
|
411
567
|
this.toggleSkipMode(false);
|
|
412
|
-
this.
|
|
568
|
+
this.handleUserAdvance();
|
|
413
569
|
});
|
|
414
570
|
// Keyboard controls
|
|
415
571
|
this.boundKeyHandler = (e) => {
|
|
572
|
+
const confirm = this.rootEl.querySelector('.kawa-confirm-overlay');
|
|
573
|
+
if (confirm) {
|
|
574
|
+
if (e.key === 'Escape') {
|
|
575
|
+
e.preventDefault();
|
|
576
|
+
confirm.remove();
|
|
577
|
+
}
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const lightbox = this.rootEl.querySelector('.kawa-gallery-lightbox');
|
|
581
|
+
if (lightbox) {
|
|
582
|
+
if (e.key === 'Escape') {
|
|
583
|
+
e.preventDefault();
|
|
584
|
+
lightbox.remove();
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
416
588
|
const modal = this.rootEl.querySelector('.kawa-modal-overlay');
|
|
417
589
|
if (modal) {
|
|
418
590
|
if (e.key === 'Escape')
|
|
419
591
|
modal.remove();
|
|
420
592
|
return;
|
|
421
593
|
}
|
|
594
|
+
if (this.isMainMenuActive()) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
422
597
|
if (e.code === 'Space' || e.code === 'Enter') {
|
|
423
|
-
if (document.activeElement &&
|
|
598
|
+
if (document.activeElement &&
|
|
599
|
+
(document.activeElement.tagName === 'BUTTON' ||
|
|
600
|
+
document.activeElement.tagName === 'INPUT' ||
|
|
601
|
+
document.activeElement.tagName === 'TEXTAREA')) {
|
|
424
602
|
return;
|
|
425
603
|
}
|
|
426
604
|
e.preventDefault();
|
|
@@ -441,15 +619,16 @@ export class DOMRenderer {
|
|
|
441
619
|
else if (e.key === 'a' || e.key === 'A') {
|
|
442
620
|
this.toggleAutoMode();
|
|
443
621
|
}
|
|
444
|
-
else if (e.key === '
|
|
622
|
+
else if (e.key === 'Control') {
|
|
623
|
+
// Ctrl toggles skip (Ren'Py-style). Tab is left for accessibility focus navigation.
|
|
445
624
|
e.preventDefault();
|
|
446
625
|
this.toggleSkipMode();
|
|
447
626
|
}
|
|
448
627
|
else if (e.key === 's' || e.key === 'S') {
|
|
449
|
-
this.showSaveLoadModal('save');
|
|
628
|
+
void this.showSaveLoadModal('save');
|
|
450
629
|
}
|
|
451
630
|
else if (e.key === 'l' || e.key === 'L') {
|
|
452
|
-
this.showSaveLoadModal('load');
|
|
631
|
+
void this.showSaveLoadModal('load');
|
|
453
632
|
}
|
|
454
633
|
else if (e.key === 'h' || e.key === 'H') {
|
|
455
634
|
this.showHistoryModal();
|
|
@@ -457,514 +636,244 @@ export class DOMRenderer {
|
|
|
457
636
|
else if (e.key === 'p' || e.key === 'P' || e.key === 'o' || e.key === 'O') {
|
|
458
637
|
this.showSettingsModal();
|
|
459
638
|
}
|
|
639
|
+
else if (e.key === 'Escape') {
|
|
640
|
+
this.showMainMenu();
|
|
641
|
+
}
|
|
460
642
|
};
|
|
461
643
|
if (typeof window !== 'undefined') {
|
|
462
644
|
window.addEventListener('keydown', this.boundKeyHandler);
|
|
463
645
|
}
|
|
646
|
+
// Mobile: swipe left = advance, swipe right = rollback
|
|
647
|
+
if (this.options.features?.touchGestures !== false) {
|
|
648
|
+
this.touchControls = bindSwipeControls(this.stageLayer.stageEl, {
|
|
649
|
+
onSwipeLeft: () => {
|
|
650
|
+
if (this.isMainMenuActive())
|
|
651
|
+
return;
|
|
652
|
+
if (this.isAutoMode)
|
|
653
|
+
this.toggleAutoMode(false);
|
|
654
|
+
if (this.isSkipMode)
|
|
655
|
+
this.toggleSkipMode(false);
|
|
656
|
+
this.handleUserAdvance();
|
|
657
|
+
},
|
|
658
|
+
onSwipeRight: () => {
|
|
659
|
+
if (this.isMainMenuActive())
|
|
660
|
+
return;
|
|
661
|
+
if (this.isAutoMode)
|
|
662
|
+
this.toggleAutoMode(false);
|
|
663
|
+
if (this.isSkipMode)
|
|
664
|
+
this.toggleSkipMode(false);
|
|
665
|
+
if (this.vm.canRollback())
|
|
666
|
+
this.vm.rollback();
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
464
670
|
}
|
|
465
671
|
handleUserAdvance() {
|
|
466
|
-
if (this.
|
|
467
|
-
|
|
672
|
+
if (this.isMainMenuActive())
|
|
673
|
+
return;
|
|
674
|
+
if (this.vm.getState().pendingInput)
|
|
675
|
+
return;
|
|
676
|
+
if (this.dialogueBox.getIsTypewriting()) {
|
|
677
|
+
this.dialogueBox.finishTypewriter();
|
|
678
|
+
const debounce = this.options.features?.advanceDebounceMs ?? 220;
|
|
679
|
+
this.advanceLockUntil = Date.now() + Math.max(0, debounce);
|
|
680
|
+
return;
|
|
468
681
|
}
|
|
469
|
-
|
|
470
|
-
|
|
682
|
+
if (Date.now() < this.advanceLockUntil) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const state = this.vm.getState();
|
|
686
|
+
if (state.choices && state.choices.length > 0) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (state.hotspots && state.hotspots.length > 0) {
|
|
690
|
+
return;
|
|
471
691
|
}
|
|
692
|
+
this.vm.next();
|
|
472
693
|
}
|
|
473
|
-
|
|
474
|
-
this.
|
|
475
|
-
|
|
476
|
-
// 1. Dual-Layer Background Crossfading
|
|
477
|
-
if (state.visual.background) {
|
|
478
|
-
this.backgroundEl.dataset.transition = state.visual.transition ?? 'none';
|
|
479
|
-
const rawBg = state.visual.background;
|
|
480
|
-
const cleanBg = rawBg.replace(/^bg[\s_]+/i, '').trim();
|
|
481
|
-
const baseName = cleanBg.replace(/\.(svg|png|jpg|jpeg|webp)$/i, '');
|
|
482
|
-
const primaryUrl = this.assetResolver(cleanBg, 'background');
|
|
483
|
-
if (primaryUrl !== this.currentBgUrl) {
|
|
484
|
-
this.currentBgUrl = primaryUrl;
|
|
485
|
-
const incomingLayer = this.activeBgLayer === 'A' ? this.bgLayerB : this.bgLayerA;
|
|
486
|
-
const outgoingLayer = this.activeBgLayer === 'A' ? this.bgLayerA : this.bgLayerB;
|
|
487
|
-
incomingLayer.style.backgroundImage = `url("${primaryUrl}")`;
|
|
488
|
-
incomingLayer.classList.add('active');
|
|
489
|
-
outgoingLayer.classList.remove('active');
|
|
490
|
-
this.activeBgLayer = this.activeBgLayer === 'A' ? 'B' : 'A';
|
|
491
|
-
// Verify and fallback if image fails to load
|
|
492
|
-
const testImg = new Image();
|
|
493
|
-
testImg.onerror = () => {
|
|
494
|
-
const fallbackPng = this.assetResolver(`${baseName}.png`, 'background');
|
|
495
|
-
const img2 = new Image();
|
|
496
|
-
img2.onload = () => {
|
|
497
|
-
incomingLayer.style.backgroundImage = `url("${fallbackPng}")`;
|
|
498
|
-
};
|
|
499
|
-
img2.onerror = () => {
|
|
500
|
-
incomingLayer.style.backgroundImage = 'radial-gradient(ellipse at center, #334155 0%, #0f172a 100%)';
|
|
501
|
-
};
|
|
502
|
-
img2.src = fallbackPng;
|
|
503
|
-
};
|
|
504
|
-
testImg.src = primaryUrl;
|
|
505
|
-
}
|
|
694
|
+
syncUrlLabel(state) {
|
|
695
|
+
if (!this.options.syncUrlLabel || typeof window === 'undefined' || typeof history === 'undefined') {
|
|
696
|
+
return;
|
|
506
697
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (!currentIds.has(id)) {
|
|
518
|
-
record.div.remove();
|
|
519
|
-
this.activeCharacters.delete(id);
|
|
520
|
-
}
|
|
698
|
+
const label = state.currentLabel;
|
|
699
|
+
if (!label || label.startsWith('__'))
|
|
700
|
+
return;
|
|
701
|
+
try {
|
|
702
|
+
const url = new URL(window.location.href);
|
|
703
|
+
if (url.searchParams.get('at') === label)
|
|
704
|
+
return;
|
|
705
|
+
url.searchParams.delete('label');
|
|
706
|
+
url.searchParams.set('at', label);
|
|
707
|
+
history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
|
|
521
708
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
else {
|
|
551
|
-
img.style.display = 'none';
|
|
552
|
-
spriteDiv.style.width = '220px';
|
|
553
|
-
spriteDiv.style.height = '420px';
|
|
554
|
-
spriteDiv.style.background = 'rgba(244, 63, 94, 0.25)';
|
|
555
|
-
spriteDiv.style.border = '2px dashed #f43f5e';
|
|
556
|
-
spriteDiv.style.borderRadius = '16px';
|
|
557
|
-
spriteDiv.style.display = 'flex';
|
|
558
|
-
spriteDiv.style.alignItems = 'center';
|
|
559
|
-
spriteDiv.style.justifyContent = 'center';
|
|
560
|
-
spriteDiv.style.color = '#ffffff';
|
|
561
|
-
spriteDiv.style.fontWeight = '600';
|
|
562
|
-
spriteDiv.style.fontSize = '1.1rem';
|
|
563
|
-
spriteDiv.textContent = `${charId}\n(${charState.expression ?? 'normal'})`;
|
|
709
|
+
catch {
|
|
710
|
+
// ignore malformed URL environments
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
syncShareMeta(state) {
|
|
714
|
+
const share = this.options.share;
|
|
715
|
+
if (!share && !this.options.syncUrlLabel)
|
|
716
|
+
return;
|
|
717
|
+
const pageUrl = typeof window !== 'undefined' ? window.location.href : undefined;
|
|
718
|
+
const meta = buildSceneShareMeta(this.vm.getStory(), state, share, pageUrl);
|
|
719
|
+
applyDocumentShareMeta({
|
|
720
|
+
...meta,
|
|
721
|
+
image: meta.image ? resolveShareAssetUrl(meta.image) : undefined
|
|
722
|
+
}, share);
|
|
723
|
+
}
|
|
724
|
+
applyThemeAndStyles(state) {
|
|
725
|
+
if (state.theme) {
|
|
726
|
+
this.rootEl.dataset.kawaTheme = state.theme;
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
delete this.rootEl.dataset.kawaTheme;
|
|
730
|
+
}
|
|
731
|
+
const nextTargets = new Set(Object.keys(state.styleClasses ?? {}));
|
|
732
|
+
for (const target of this.appliedStyleTargets) {
|
|
733
|
+
if (!nextTargets.has(target)) {
|
|
734
|
+
this.rootEl.classList.forEach((cls) => {
|
|
735
|
+
if (cls.startsWith(`kawa-style-${target}-`)) {
|
|
736
|
+
this.rootEl.classList.remove(cls);
|
|
564
737
|
}
|
|
565
|
-
};
|
|
566
|
-
spriteDiv.appendChild(img);
|
|
567
|
-
this.charactersEl.appendChild(spriteDiv);
|
|
568
|
-
existing = {
|
|
569
|
-
div: spriteDiv,
|
|
570
|
-
img,
|
|
571
|
-
expression: charState.expression,
|
|
572
|
-
position: charState.position
|
|
573
|
-
};
|
|
574
|
-
this.activeCharacters.set(charId, existing);
|
|
738
|
+
});
|
|
575
739
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
delete existing.div.dataset.expression;
|
|
583
|
-
}
|
|
584
|
-
if (existing.expression !== charState.expression) {
|
|
585
|
-
existing.expression = charState.expression;
|
|
586
|
-
existing.img.style.display = 'block';
|
|
587
|
-
existing.img.src = primarySpriteUrl;
|
|
740
|
+
}
|
|
741
|
+
for (const [target, name] of Object.entries(state.styleClasses ?? {})) {
|
|
742
|
+
const prefix = `kawa-style-${target}-`;
|
|
743
|
+
this.rootEl.classList.forEach((cls) => {
|
|
744
|
+
if (cls.startsWith(prefix) && cls !== `${prefix}${name}`) {
|
|
745
|
+
this.rootEl.classList.remove(cls);
|
|
588
746
|
}
|
|
589
|
-
}
|
|
747
|
+
});
|
|
748
|
+
this.rootEl.classList.add(`${prefix}${name}`);
|
|
590
749
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
750
|
+
this.appliedStyleTargets = nextTargets;
|
|
751
|
+
}
|
|
752
|
+
render(state) {
|
|
753
|
+
this.quickMenu.setBackDisabled(!this.vm.canRollback());
|
|
754
|
+
this.applyThemeAndStyles(state);
|
|
755
|
+
// Timed pause: auto-advance after pendingPauseMs (click still skips via handleUserAdvance)
|
|
756
|
+
if (this.pauseTimer) {
|
|
757
|
+
clearTimeout(this.pauseTimer);
|
|
758
|
+
this.pauseTimer = null;
|
|
759
|
+
}
|
|
760
|
+
if (state.pendingPauseMs != null && state.pendingPauseMs > 0 && !state.isFinished) {
|
|
761
|
+
const delay = state.pendingPauseMs;
|
|
762
|
+
this.pauseTimer = window.setTimeout(() => {
|
|
763
|
+
this.pauseTimer = null;
|
|
764
|
+
if (this.destroyed)
|
|
765
|
+
return;
|
|
766
|
+
const cur = this.vm.getState();
|
|
767
|
+
if (cur.pendingPauseMs != null && !cur.isFinished) {
|
|
768
|
+
this.vm.next();
|
|
599
769
|
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
770
|
+
}, delay);
|
|
771
|
+
}
|
|
772
|
+
// 1. Background scene
|
|
773
|
+
this.stageLayer.updateBackground(state.visual.background, state.visual.transition);
|
|
774
|
+
// 2. Character sprites
|
|
775
|
+
this.stageLayer.updateCharacters(state.visual.characters);
|
|
776
|
+
// 3. VFX & Atmospheric Layer
|
|
777
|
+
this.stageLayer.updateVfx(state.visual.vfx);
|
|
778
|
+
// 4. Fullscreen CG Layer
|
|
779
|
+
this.stageLayer.updateCG(state.visual.activeCG);
|
|
780
|
+
// 5. Branching Choice Menu
|
|
781
|
+
this.choiceMenu.render(state.choices);
|
|
782
|
+
// 5b. CG / stage hotspots
|
|
783
|
+
this.hotspotLayer.render(state.hotspots);
|
|
784
|
+
// 6. Dialogue Box (respect `window hide` / `window show`)
|
|
785
|
+
if (state.windowVisible === false) {
|
|
786
|
+
this.dialogueBox.render(null);
|
|
605
787
|
}
|
|
606
788
|
else {
|
|
607
|
-
this.
|
|
789
|
+
this.dialogueBox.render(state.dialogue, () => {
|
|
790
|
+
if (this.isAutoMode &&
|
|
791
|
+
!state.isFinished &&
|
|
792
|
+
(!state.choices || state.choices.length === 0) &&
|
|
793
|
+
(!state.hotspots || state.hotspots.length === 0) &&
|
|
794
|
+
!state.pendingInput) {
|
|
795
|
+
this.scheduleAutoAdvance();
|
|
796
|
+
}
|
|
797
|
+
});
|
|
608
798
|
}
|
|
609
|
-
//
|
|
610
|
-
if (state.
|
|
611
|
-
if (this.
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const btn = document.createElement('button');
|
|
621
|
-
btn.className = 'kawa-choice-btn kawa-choice';
|
|
622
|
-
btn.textContent = choice.text;
|
|
623
|
-
btn.addEventListener('click', (e) => {
|
|
624
|
-
e.stopPropagation();
|
|
625
|
-
if (this.isChoicePending)
|
|
626
|
-
return;
|
|
627
|
-
this.isChoicePending = true;
|
|
628
|
-
this.choiceContainerEl.style.pointerEvents = 'none';
|
|
629
|
-
this.choiceContainerEl.style.opacity = '0.5';
|
|
630
|
-
this.vm.choose(index);
|
|
799
|
+
// 6b. Text input prompt (`input var "prompt"`)
|
|
800
|
+
if (state.pendingInput) {
|
|
801
|
+
if (!this.rootEl.querySelector('.kawa-input-overlay')) {
|
|
802
|
+
const prompt = state.pendingInput.prompt;
|
|
803
|
+
const current = typeof state.variables[state.pendingInput.variable] === 'string'
|
|
804
|
+
? String(state.variables[state.pendingInput.variable])
|
|
805
|
+
: '';
|
|
806
|
+
showInputModal(this.rootEl, {
|
|
807
|
+
prompt,
|
|
808
|
+
defaultValue: current,
|
|
809
|
+
onSubmit: (value) => this.vm.submitInput(value)
|
|
631
810
|
});
|
|
632
|
-
|
|
633
|
-
});
|
|
811
|
+
}
|
|
634
812
|
}
|
|
635
813
|
else {
|
|
636
|
-
this.
|
|
814
|
+
this.rootEl.querySelector('.kawa-input-overlay')?.remove();
|
|
637
815
|
}
|
|
638
|
-
//
|
|
816
|
+
// 7. Ending Screen if finished
|
|
639
817
|
if (state.isFinished) {
|
|
640
|
-
|
|
641
|
-
this.toggleAutoMode(false);
|
|
642
|
-
if (this.isSkipMode)
|
|
643
|
-
this.toggleSkipMode(false);
|
|
644
|
-
this.choiceContainerEl.style.display = 'none';
|
|
645
|
-
const existingEnd = this.rootEl.querySelector('.kawa-ending-card');
|
|
646
|
-
if (!existingEnd) {
|
|
647
|
-
const endCard = document.createElement('div');
|
|
648
|
-
endCard.className = 'kawa-ending-card';
|
|
649
|
-
endCard.innerHTML = `
|
|
650
|
-
<div class="kawa-ending-title">The End</div>
|
|
651
|
-
<div class="kawa-ending-subtitle">Story complete! Thank you for playing.</div>
|
|
652
|
-
<div style="display:flex;gap:12px;margin-top:16px;">
|
|
653
|
-
<button class="kawa-btn kawa-btn-replay">${SVG_ICONS.replay} Play Again</button>
|
|
654
|
-
<button class="kawa-btn kawa-btn-load-end">${SVG_ICONS.load} Load Slot</button>
|
|
655
|
-
</div>
|
|
656
|
-
`;
|
|
657
|
-
endCard.querySelector('.kawa-btn-replay')?.addEventListener('click', () => {
|
|
658
|
-
this.vm.jump('start');
|
|
659
|
-
});
|
|
660
|
-
endCard.querySelector('.kawa-btn-load-end')?.addEventListener('click', () => {
|
|
661
|
-
this.showSaveLoadModal('load');
|
|
662
|
-
});
|
|
663
|
-
this.rootEl.appendChild(endCard);
|
|
664
|
-
}
|
|
818
|
+
this.renderEndingCard();
|
|
665
819
|
}
|
|
666
820
|
else {
|
|
667
|
-
const
|
|
668
|
-
if (
|
|
669
|
-
|
|
821
|
+
const endingEl = this.stageLayer.stageEl.querySelector('.kawa-ending-card');
|
|
822
|
+
if (endingEl)
|
|
823
|
+
endingEl.remove();
|
|
670
824
|
}
|
|
671
825
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
if (this.autoTimer) {
|
|
677
|
-
clearTimeout(this.autoTimer);
|
|
678
|
-
this.autoTimer = null;
|
|
679
|
-
}
|
|
680
|
-
this.fullCurrentText = text;
|
|
681
|
-
if (this.typewriterSpeed <= 0 || this.isSkipMode) {
|
|
682
|
-
this.dialogueTextEl.innerHTML = formatRichText(text);
|
|
683
|
-
this.isTypewriting = false;
|
|
684
|
-
if (this.isAutoMode)
|
|
685
|
-
this.scheduleAutoAdvance();
|
|
826
|
+
renderEndingCard() {
|
|
827
|
+
// Don't remount — recreating replayed the pop animation and looked like a jump.
|
|
828
|
+
if (this.stageLayer.stageEl.querySelector('.kawa-ending-card')) {
|
|
686
829
|
return;
|
|
687
830
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
this.finishTypewriter();
|
|
696
|
-
}
|
|
697
|
-
}, this.typewriterSpeed);
|
|
698
|
-
}
|
|
699
|
-
finishTypewriter() {
|
|
700
|
-
if (this.currentTypewriterInterval) {
|
|
701
|
-
clearInterval(this.currentTypewriterInterval);
|
|
702
|
-
this.currentTypewriterInterval = null;
|
|
703
|
-
}
|
|
704
|
-
this.dialogueTextEl.innerHTML = formatRichText(this.fullCurrentText);
|
|
705
|
-
this.isTypewriting = false;
|
|
706
|
-
if (this.isAutoMode) {
|
|
707
|
-
this.scheduleAutoAdvance();
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
showSettingsModal() {
|
|
711
|
-
const existing = this.rootEl.querySelector('.kawa-modal-overlay');
|
|
712
|
-
if (existing)
|
|
713
|
-
existing.remove();
|
|
714
|
-
const overlay = document.createElement('div');
|
|
715
|
-
overlay.className = 'kawa-modal-overlay';
|
|
716
|
-
const card = document.createElement('div');
|
|
717
|
-
card.className = 'kawa-modal-card';
|
|
718
|
-
card.setAttribute('role', 'dialog');
|
|
719
|
-
card.setAttribute('aria-modal', 'true');
|
|
720
|
-
card.setAttribute('aria-label', 'Settings');
|
|
721
|
-
const header = document.createElement('div');
|
|
722
|
-
header.className = 'kawa-modal-header';
|
|
723
|
-
const title = document.createElement('div');
|
|
724
|
-
title.className = 'kawa-modal-title';
|
|
725
|
-
title.innerHTML = `${SVG_ICONS.settings} <span>Settings</span>`;
|
|
726
|
-
const closeBtn = document.createElement('button');
|
|
727
|
-
closeBtn.className = 'kawa-btn';
|
|
728
|
-
closeBtn.innerHTML = `${SVG_ICONS.close} <span>Close</span>`;
|
|
729
|
-
closeBtn.setAttribute('aria-label', 'Close settings modal');
|
|
730
|
-
closeBtn.addEventListener('click', () => overlay.remove());
|
|
731
|
-
header.appendChild(title);
|
|
732
|
-
header.appendChild(closeBtn);
|
|
733
|
-
const body = document.createElement('div');
|
|
734
|
-
body.className = 'kawa-modal-body kawa-settings-group';
|
|
735
|
-
// 1. Text Speed Slider
|
|
736
|
-
const speedRow = document.createElement('div');
|
|
737
|
-
speedRow.className = 'kawa-setting-row';
|
|
738
|
-
const speedVal = this.typewriterSpeed;
|
|
739
|
-
speedRow.innerHTML = `
|
|
740
|
-
<div class="kawa-setting-header">
|
|
741
|
-
<span>Text Speed</span>
|
|
742
|
-
<span class="kawa-setting-value" id="kawa-speed-val">${speedVal === 0 ? 'Instant' : `${speedVal}ms`}</span>
|
|
743
|
-
</div>
|
|
744
|
-
<input type="range" class="kawa-slider" id="kawa-speed-slider" min="0" max="60" step="5" value="${speedVal}">
|
|
831
|
+
const endingCard = document.createElement('div');
|
|
832
|
+
endingCard.className = 'kawa-ending-card';
|
|
833
|
+
endingCard.setAttribute('role', 'alert');
|
|
834
|
+
endingCard.setAttribute('aria-label', 'Story Finished');
|
|
835
|
+
endingCard.innerHTML = `
|
|
836
|
+
<div class="kawa-ending-title">The End</div>
|
|
837
|
+
<div class="kawa-ending-subtitle">Thank you for playing!</div>
|
|
745
838
|
`;
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
839
|
+
const restartBtn = document.createElement('button');
|
|
840
|
+
restartBtn.type = 'button';
|
|
841
|
+
restartBtn.className = 'kawa-choice-btn kawa-ending-btn';
|
|
842
|
+
restartBtn.style.marginTop = '16px';
|
|
843
|
+
restartBtn.innerHTML = `${SVG_ICONS.replay} Play Again`;
|
|
844
|
+
restartBtn.addEventListener('click', (e) => {
|
|
845
|
+
e.preventDefault();
|
|
846
|
+
e.stopPropagation();
|
|
847
|
+
endingCard.remove();
|
|
848
|
+
this.startNewGame();
|
|
752
849
|
});
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
`;
|
|
764
|
-
const autoInput = autoRow.querySelector('#kawa-auto-slider');
|
|
765
|
-
const autoValEl = autoRow.querySelector('#kawa-auto-val');
|
|
766
|
-
autoInput.addEventListener('input', () => {
|
|
767
|
-
this.autoDelayMs = Number(autoInput.value);
|
|
768
|
-
autoValEl.textContent = `${(this.autoDelayMs / 1000).toFixed(1)}s`;
|
|
769
|
-
this.saveSettings();
|
|
850
|
+
const menuBtn = document.createElement('button');
|
|
851
|
+
menuBtn.type = 'button';
|
|
852
|
+
menuBtn.className = 'kawa-choice-btn kawa-ending-btn';
|
|
853
|
+
menuBtn.style.marginTop = '10px';
|
|
854
|
+
menuBtn.innerHTML = `${SVG_ICONS.home} Main Menu`;
|
|
855
|
+
menuBtn.addEventListener('click', (e) => {
|
|
856
|
+
e.preventDefault();
|
|
857
|
+
e.stopPropagation();
|
|
858
|
+
endingCard.remove();
|
|
859
|
+
this.showMainMenu();
|
|
770
860
|
});
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
musicRow.innerHTML = `
|
|
775
|
-
<div class="kawa-setting-header">
|
|
776
|
-
<span>${SVG_ICONS.volumeOn} Music Volume</span>
|
|
777
|
-
<span class="kawa-setting-value" id="kawa-music-val">80%</span>
|
|
778
|
-
</div>
|
|
779
|
-
<input type="range" class="kawa-slider" id="kawa-music-slider" min="0" max="100" step="5" value="80">
|
|
780
|
-
`;
|
|
781
|
-
const sfxRow = document.createElement('div');
|
|
782
|
-
sfxRow.className = 'kawa-setting-row';
|
|
783
|
-
sfxRow.innerHTML = `
|
|
784
|
-
<div class="kawa-setting-header">
|
|
785
|
-
<span>${SVG_ICONS.volumeOn} SFX Volume</span>
|
|
786
|
-
<span class="kawa-setting-value" id="kawa-sfx-val">100%</span>
|
|
787
|
-
</div>
|
|
788
|
-
<input type="range" class="kawa-slider" id="kawa-sfx-slider" min="0" max="100" step="5" value="100">
|
|
789
|
-
`;
|
|
790
|
-
body.appendChild(speedRow);
|
|
791
|
-
body.appendChild(autoRow);
|
|
792
|
-
body.appendChild(musicRow);
|
|
793
|
-
body.appendChild(sfxRow);
|
|
794
|
-
card.appendChild(header);
|
|
795
|
-
card.appendChild(body);
|
|
796
|
-
overlay.appendChild(card);
|
|
797
|
-
this.rootEl.appendChild(overlay);
|
|
861
|
+
endingCard.appendChild(restartBtn);
|
|
862
|
+
endingCard.appendChild(menuBtn);
|
|
863
|
+
this.stageLayer.stageEl.appendChild(endingCard);
|
|
798
864
|
}
|
|
799
865
|
saveSettings() {
|
|
800
866
|
if (typeof localStorage !== 'undefined') {
|
|
801
867
|
try {
|
|
802
868
|
localStorage.setItem('kawaijs_settings', JSON.stringify({
|
|
803
869
|
typewriterSpeed: this.typewriterSpeed,
|
|
804
|
-
autoDelayMs: this.autoDelayMs
|
|
870
|
+
autoDelayMs: this.autoDelayMs,
|
|
871
|
+
musicVolume: this.musicVolume,
|
|
872
|
+
soundVolume: this.soundVolume
|
|
805
873
|
}));
|
|
806
874
|
}
|
|
807
875
|
catch { }
|
|
808
876
|
}
|
|
809
877
|
}
|
|
810
|
-
async showSaveLoadModal(mode) {
|
|
811
|
-
const existing = this.rootEl.querySelector('.kawa-modal-overlay');
|
|
812
|
-
if (existing)
|
|
813
|
-
existing.remove();
|
|
814
|
-
const overlay = document.createElement('div');
|
|
815
|
-
overlay.className = 'kawa-modal-overlay';
|
|
816
|
-
const card = document.createElement('div');
|
|
817
|
-
card.className = 'kawa-modal-card';
|
|
818
|
-
card.setAttribute('role', 'dialog');
|
|
819
|
-
card.setAttribute('aria-modal', 'true');
|
|
820
|
-
card.setAttribute('aria-label', mode === 'save' ? 'Save Game' : 'Load Game');
|
|
821
|
-
const header = document.createElement('div');
|
|
822
|
-
header.className = 'kawa-modal-header';
|
|
823
|
-
const title = document.createElement('div');
|
|
824
|
-
title.className = 'kawa-modal-title';
|
|
825
|
-
title.innerHTML = `${mode === 'save' ? SVG_ICONS.save : SVG_ICONS.load} <span>${mode === 'save' ? 'Save Game' : 'Load Game'}</span>`;
|
|
826
|
-
const closeBtn = document.createElement('button');
|
|
827
|
-
closeBtn.className = 'kawa-btn';
|
|
828
|
-
closeBtn.innerHTML = `${SVG_ICONS.close} <span>Close</span>`;
|
|
829
|
-
closeBtn.setAttribute('aria-label', 'Close modal');
|
|
830
|
-
closeBtn.addEventListener('click', () => overlay.remove());
|
|
831
|
-
header.appendChild(title);
|
|
832
|
-
header.appendChild(closeBtn);
|
|
833
|
-
const body = document.createElement('div');
|
|
834
|
-
body.className = 'kawa-modal-body';
|
|
835
|
-
const grid = document.createElement('div');
|
|
836
|
-
grid.className = 'kawa-slots-grid';
|
|
837
|
-
const saveManager = this.vm.getSaveManager();
|
|
838
|
-
const slots = await saveManager.listSlots(6);
|
|
839
|
-
slots.forEach((slot, idx) => {
|
|
840
|
-
const slotNum = String(idx + 1);
|
|
841
|
-
const slotCard = document.createElement('div');
|
|
842
|
-
slotCard.className = 'kawa-slot-card';
|
|
843
|
-
const slotHeader = document.createElement('div');
|
|
844
|
-
slotHeader.className = 'kawa-slot-header';
|
|
845
|
-
const badge = document.createElement('span');
|
|
846
|
-
badge.className = 'kawa-slot-badge';
|
|
847
|
-
badge.textContent = `Slot ${slotNum}`;
|
|
848
|
-
const timeSpan = document.createElement('span');
|
|
849
|
-
timeSpan.className = 'kawa-slot-time';
|
|
850
|
-
timeSpan.textContent = slot ? new Date(slot.timestamp).toLocaleString() : 'Empty';
|
|
851
|
-
slotHeader.appendChild(badge);
|
|
852
|
-
slotHeader.appendChild(timeSpan);
|
|
853
|
-
slotCard.appendChild(slotHeader);
|
|
854
|
-
if (slot) {
|
|
855
|
-
const preview = document.createElement('div');
|
|
856
|
-
preview.className = 'kawa-slot-preview';
|
|
857
|
-
preview.textContent = slot.previewText || `Label: ${slot.snapshot.state.currentLabel}`;
|
|
858
|
-
slotCard.appendChild(preview);
|
|
859
|
-
}
|
|
860
|
-
else {
|
|
861
|
-
const emptyText = document.createElement('div');
|
|
862
|
-
emptyText.className = 'kawa-slot-empty-text';
|
|
863
|
-
emptyText.textContent = 'No save data';
|
|
864
|
-
slotCard.appendChild(emptyText);
|
|
865
|
-
}
|
|
866
|
-
const actions = document.createElement('div');
|
|
867
|
-
actions.className = 'kawa-slot-actions';
|
|
868
|
-
if (mode === 'save') {
|
|
869
|
-
const saveActionBtn = document.createElement('button');
|
|
870
|
-
saveActionBtn.className = 'kawa-slot-btn kawa-slot-btn-save';
|
|
871
|
-
saveActionBtn.innerHTML = `${SVG_ICONS.save} Save Here`;
|
|
872
|
-
saveActionBtn.setAttribute('aria-label', `Save to slot ${slotNum}`);
|
|
873
|
-
saveActionBtn.addEventListener('click', async () => {
|
|
874
|
-
await this.vm.save(slotNum);
|
|
875
|
-
overlay.remove();
|
|
876
|
-
this.showSaveLoadModal('save');
|
|
877
|
-
});
|
|
878
|
-
actions.appendChild(saveActionBtn);
|
|
879
|
-
}
|
|
880
|
-
else {
|
|
881
|
-
if (slot) {
|
|
882
|
-
const loadActionBtn = document.createElement('button');
|
|
883
|
-
loadActionBtn.className = 'kawa-slot-btn kawa-slot-btn-load';
|
|
884
|
-
loadActionBtn.innerHTML = `${SVG_ICONS.load} Load`;
|
|
885
|
-
loadActionBtn.setAttribute('aria-label', `Load slot ${slotNum}`);
|
|
886
|
-
loadActionBtn.addEventListener('click', async () => {
|
|
887
|
-
const ok = await this.vm.load(slotNum);
|
|
888
|
-
if (ok)
|
|
889
|
-
overlay.remove();
|
|
890
|
-
});
|
|
891
|
-
actions.appendChild(loadActionBtn);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
if (slot) {
|
|
895
|
-
const delBtn = document.createElement('button');
|
|
896
|
-
delBtn.className = 'kawa-slot-btn kawa-slot-btn-del';
|
|
897
|
-
delBtn.innerHTML = SVG_ICONS.trash;
|
|
898
|
-
delBtn.title = 'Delete Save';
|
|
899
|
-
delBtn.setAttribute('aria-label', `Delete slot ${slotNum}`);
|
|
900
|
-
delBtn.addEventListener('click', async () => {
|
|
901
|
-
await saveManager.deleteSlot(slotNum);
|
|
902
|
-
overlay.remove();
|
|
903
|
-
this.showSaveLoadModal(mode);
|
|
904
|
-
});
|
|
905
|
-
actions.appendChild(delBtn);
|
|
906
|
-
}
|
|
907
|
-
slotCard.appendChild(actions);
|
|
908
|
-
grid.appendChild(slotCard);
|
|
909
|
-
});
|
|
910
|
-
body.appendChild(grid);
|
|
911
|
-
card.appendChild(header);
|
|
912
|
-
card.appendChild(body);
|
|
913
|
-
overlay.appendChild(card);
|
|
914
|
-
this.rootEl.appendChild(overlay);
|
|
915
|
-
}
|
|
916
|
-
showHistoryModal() {
|
|
917
|
-
const existing = this.rootEl.querySelector('.kawa-modal-overlay');
|
|
918
|
-
if (existing)
|
|
919
|
-
existing.remove();
|
|
920
|
-
const overlay = document.createElement('div');
|
|
921
|
-
overlay.className = 'kawa-modal-overlay';
|
|
922
|
-
const card = document.createElement('div');
|
|
923
|
-
card.className = 'kawa-modal-card';
|
|
924
|
-
card.setAttribute('role', 'dialog');
|
|
925
|
-
card.setAttribute('aria-modal', 'true');
|
|
926
|
-
card.setAttribute('aria-label', 'Dialogue History');
|
|
927
|
-
const header = document.createElement('div');
|
|
928
|
-
header.className = 'kawa-modal-header';
|
|
929
|
-
const title = document.createElement('div');
|
|
930
|
-
title.className = 'kawa-modal-title';
|
|
931
|
-
title.innerHTML = `${SVG_ICONS.history} <span>Dialogue History</span>`;
|
|
932
|
-
const closeBtn = document.createElement('button');
|
|
933
|
-
closeBtn.className = 'kawa-btn';
|
|
934
|
-
closeBtn.innerHTML = `${SVG_ICONS.close} <span>Close</span>`;
|
|
935
|
-
closeBtn.setAttribute('aria-label', 'Close history modal');
|
|
936
|
-
closeBtn.addEventListener('click', () => overlay.remove());
|
|
937
|
-
header.appendChild(title);
|
|
938
|
-
header.appendChild(closeBtn);
|
|
939
|
-
const body = document.createElement('div');
|
|
940
|
-
body.className = 'kawa-modal-body';
|
|
941
|
-
const entries = this.vm.getHistoryManager().getEntries();
|
|
942
|
-
if (entries.length === 0) {
|
|
943
|
-
const emptyMsg = document.createElement('div');
|
|
944
|
-
emptyMsg.textContent = 'No dialogue history yet.';
|
|
945
|
-
emptyMsg.style.opacity = '0.6';
|
|
946
|
-
body.appendChild(emptyMsg);
|
|
947
|
-
}
|
|
948
|
-
else {
|
|
949
|
-
for (const entry of entries) {
|
|
950
|
-
const item = document.createElement('div');
|
|
951
|
-
item.className = 'kawa-history-item';
|
|
952
|
-
if (entry.speakerDisplayName) {
|
|
953
|
-
const spk = document.createElement('div');
|
|
954
|
-
spk.className = 'kawa-history-speaker';
|
|
955
|
-
spk.textContent = entry.speakerDisplayName;
|
|
956
|
-
item.appendChild(spk);
|
|
957
|
-
}
|
|
958
|
-
const txt = document.createElement('div');
|
|
959
|
-
txt.innerHTML = formatRichText(entry.text);
|
|
960
|
-
item.appendChild(txt);
|
|
961
|
-
body.appendChild(item);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
card.appendChild(header);
|
|
965
|
-
card.appendChild(body);
|
|
966
|
-
overlay.appendChild(card);
|
|
967
|
-
this.rootEl.appendChild(overlay);
|
|
968
|
-
}
|
|
969
878
|
}
|
|
970
879
|
//# sourceMappingURL=renderer.js.map
|