@kawaijs/renderer-dom 0.1.11 → 0.1.13
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 +43 -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 +48 -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 +258 -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 +257 -0
- package/dist/components/stage-layer.js.map +1 -0
- package/dist/components/vfx-layer.d.ts +32 -0
- package/dist/components/vfx-layer.d.ts.map +1 -0
- package/dist/components/vfx-layer.js +357 -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 +146 -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 +65 -56
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +655 -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 +66 -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 +85 -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 +2081 -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,103 @@ 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
|
+
this.setGameplayUiInert(true);
|
|
360
|
+
}
|
|
361
|
+
hideMainMenu() {
|
|
362
|
+
this.mainMenu.hide();
|
|
363
|
+
this.setGameplayUiInert(false);
|
|
364
|
+
}
|
|
365
|
+
/** Keep title-screen a11y tree clean: gameplay controls must not stay focusable under the menu. */
|
|
366
|
+
setGameplayUiInert(inert) {
|
|
367
|
+
const ui = this.stageLayer.stageEl.querySelector('.kawa-ui-layer');
|
|
368
|
+
if (!ui)
|
|
369
|
+
return;
|
|
370
|
+
if (inert)
|
|
371
|
+
ui.setAttribute('inert', '');
|
|
372
|
+
else
|
|
373
|
+
ui.removeAttribute('inert');
|
|
374
|
+
}
|
|
375
|
+
isMainMenuActive() {
|
|
376
|
+
return this.mainMenu.isActive();
|
|
377
|
+
}
|
|
378
|
+
startNewGame() {
|
|
379
|
+
// Ignore residual click-through from the Start button onto the stage (Opera / touchpads).
|
|
380
|
+
this.advanceLockUntil = Date.now() + 350;
|
|
381
|
+
this.vm.start();
|
|
382
|
+
this.hideMainMenu();
|
|
383
|
+
this.mainMenuOptions?.onStart?.();
|
|
384
|
+
}
|
|
385
|
+
async continueLatestGame() {
|
|
386
|
+
const saveManager = this.vm.getSaveManager();
|
|
387
|
+
const slots = await saveManager.listSlots(10);
|
|
388
|
+
const validSlots = slots.filter((s) => s !== null);
|
|
389
|
+
if (validSlots.length === 0)
|
|
390
|
+
return false;
|
|
391
|
+
validSlots.sort((a, b) => b.timestamp - a.timestamp);
|
|
392
|
+
const latest = validSlots[0];
|
|
393
|
+
const ok = await this.vm.load(latest.id);
|
|
394
|
+
if (ok) {
|
|
395
|
+
this.hideMainMenu();
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
async showSaveLoadModal(mode) {
|
|
401
|
+
await showSaveLoadModal(this.rootEl, this.vm, mode, () => {
|
|
402
|
+
this.hideMainMenu();
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
showSettingsModal() {
|
|
406
|
+
showSettingsModal(this.rootEl, {
|
|
407
|
+
typewriterSpeed: this.typewriterSpeed,
|
|
408
|
+
autoDelayMs: this.autoDelayMs,
|
|
409
|
+
musicVolume: this.musicVolume,
|
|
410
|
+
soundVolume: this.soundVolume,
|
|
411
|
+
onSettingsChange: (s) => {
|
|
412
|
+
this.typewriterSpeed = s.typewriterSpeed;
|
|
413
|
+
this.autoDelayMs = s.autoDelayMs;
|
|
414
|
+
if (s.musicVolume !== undefined) {
|
|
415
|
+
this.musicVolume = s.musicVolume;
|
|
416
|
+
this.audioManager?.setMusicVolume(this.musicVolume);
|
|
417
|
+
}
|
|
418
|
+
if (s.soundVolume !== undefined) {
|
|
419
|
+
this.soundVolume = s.soundVolume;
|
|
420
|
+
this.audioManager?.setSoundVolume(this.soundVolume);
|
|
421
|
+
}
|
|
422
|
+
this.dialogueBox.setTypewriterSpeed(this.typewriterSpeed);
|
|
423
|
+
this.saveSettings();
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
showHistoryModal() {
|
|
428
|
+
showHistoryModal(this.rootEl, this.vm);
|
|
429
|
+
}
|
|
430
|
+
showAboutModal() {
|
|
431
|
+
showAboutModal(this.rootEl, this.mainMenuOptions);
|
|
432
|
+
}
|
|
433
|
+
showGalleryModal() {
|
|
434
|
+
const galleryItems = this.mainMenuOptions?.galleryItems ?? [];
|
|
435
|
+
showGalleryModal(this.rootEl, this.vm, galleryItems, (path) => this.assetResolver(path, 'background'));
|
|
436
|
+
}
|
|
229
437
|
updateModeUI() {
|
|
230
|
-
|
|
231
|
-
|
|
438
|
+
this.quickMenu.setAutoActive(this.isAutoMode);
|
|
439
|
+
this.quickMenu.setSkipActive(this.isSkipMode);
|
|
440
|
+
if (this.isSkipMode) {
|
|
441
|
+
this.stageLayer.modeBadgeEl.innerHTML = `${SVG_ICONS.skip} SKIP`;
|
|
442
|
+
this.stageLayer.modeBadgeEl.style.display = 'inline-flex';
|
|
232
443
|
}
|
|
233
|
-
if (this.
|
|
234
|
-
this.
|
|
444
|
+
else if (this.isAutoMode) {
|
|
445
|
+
this.stageLayer.modeBadgeEl.innerHTML = `${SVG_ICONS.auto} AUTO`;
|
|
446
|
+
this.stageLayer.modeBadgeEl.style.display = 'inline-flex';
|
|
235
447
|
}
|
|
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
|
-
}
|
|
448
|
+
else {
|
|
449
|
+
this.stageLayer.modeBadgeEl.style.display = 'none';
|
|
248
450
|
}
|
|
249
451
|
}
|
|
250
452
|
scheduleAutoAdvance() {
|
|
@@ -255,14 +457,20 @@ export class DOMRenderer {
|
|
|
255
457
|
if (!this.isAutoMode)
|
|
256
458
|
return;
|
|
257
459
|
const state = this.vm.getState();
|
|
258
|
-
if (state.isFinished ||
|
|
460
|
+
if (state.isFinished ||
|
|
461
|
+
(state.choices && state.choices.length > 0) ||
|
|
462
|
+
(state.hotspots && state.hotspots.length > 0) ||
|
|
463
|
+
state.pendingInput) {
|
|
259
464
|
return;
|
|
260
465
|
}
|
|
261
466
|
const duration = Math.max(800, this.autoDelayMs + (state.dialogue?.text.length ?? 0) * 15);
|
|
262
467
|
this.autoTimer = window.setTimeout(() => {
|
|
263
468
|
if (this.isAutoMode) {
|
|
264
469
|
const cur = this.vm.getState();
|
|
265
|
-
if (!cur.isFinished &&
|
|
470
|
+
if (!cur.isFinished &&
|
|
471
|
+
(!cur.choices || cur.choices.length === 0) &&
|
|
472
|
+
(!cur.hotspots || cur.hotspots.length === 0) &&
|
|
473
|
+
!cur.pendingInput) {
|
|
266
474
|
this.vm.next();
|
|
267
475
|
}
|
|
268
476
|
}
|
|
@@ -272,155 +480,137 @@ export class DOMRenderer {
|
|
|
272
480
|
this.container.innerHTML = '';
|
|
273
481
|
this.rootEl = document.createElement('div');
|
|
274
482
|
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';
|
|
483
|
+
this.rootEl.style.touchAction = 'manipulation';
|
|
484
|
+
if (this.options.embed) {
|
|
485
|
+
this.rootEl.classList.add('kawa-embed');
|
|
486
|
+
this.rootEl.dataset.kawaEmbed = '1';
|
|
487
|
+
}
|
|
488
|
+
this.stageLayer = new StageLayerComponent(this.assetResolver);
|
|
295
489
|
const uiLayerEl = document.createElement('div');
|
|
296
490
|
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');
|
|
491
|
+
this.choiceMenu = this.options.components?.createChoiceMenu
|
|
492
|
+
? this.options.components.createChoiceMenu({
|
|
493
|
+
onSelect: (index) => {
|
|
494
|
+
this.vm.choose(index);
|
|
495
|
+
}
|
|
496
|
+
})
|
|
497
|
+
: new ChoiceMenuComponent({
|
|
498
|
+
onSelect: (index) => {
|
|
499
|
+
this.vm.choose(index);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
this.hotspotLayer = new HotspotLayerComponent((id) => {
|
|
503
|
+
this.vm.selectHotspot(id);
|
|
361
504
|
});
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
505
|
+
this.dialogueBox = this.options.components?.createDialogueBox
|
|
506
|
+
? this.options.components.createDialogueBox(this.typewriterSpeed)
|
|
507
|
+
: new DialogueBoxComponent(this.typewriterSpeed);
|
|
508
|
+
this.quickMenu = new QuickMenuComponent({
|
|
509
|
+
onTitle: () => this.showMainMenu(),
|
|
510
|
+
onBack: () => {
|
|
511
|
+
if (this.isAutoMode)
|
|
512
|
+
this.toggleAutoMode(false);
|
|
513
|
+
if (this.isSkipMode)
|
|
514
|
+
this.toggleSkipMode(false);
|
|
515
|
+
this.vm.rollback();
|
|
516
|
+
},
|
|
517
|
+
onHistory: () => this.showHistoryModal(),
|
|
518
|
+
onAuto: () => this.toggleAutoMode(),
|
|
519
|
+
onSkip: () => this.toggleSkipMode(),
|
|
520
|
+
onSave: () => void this.showSaveLoadModal('save'),
|
|
521
|
+
onLoad: () => void this.showSaveLoadModal('load'),
|
|
522
|
+
onSettings: () => this.showSettingsModal()
|
|
369
523
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
524
|
+
if (this.options.features?.quickMenu === false || this.options.embed) {
|
|
525
|
+
this.quickMenu.el.style.display = 'none';
|
|
526
|
+
}
|
|
527
|
+
uiLayerEl.appendChild(this.choiceMenu.el);
|
|
528
|
+
uiLayerEl.appendChild(this.dialogueBox.el);
|
|
529
|
+
uiLayerEl.appendChild(this.quickMenu.el);
|
|
530
|
+
this.stageLayer.stageEl.appendChild(uiLayerEl);
|
|
531
|
+
this.stageLayer.stageEl.appendChild(this.hotspotLayer.el);
|
|
532
|
+
this.mainMenu = new MainMenuComponent(this.rootEl, this.vm, this.mainMenuOptions, {
|
|
533
|
+
onStartNewGame: () => this.startNewGame(),
|
|
534
|
+
onContinueGame: () => this.continueLatestGame(),
|
|
535
|
+
assetResolver: this.assetResolver,
|
|
536
|
+
getSettingsConfig: () => ({
|
|
537
|
+
typewriterSpeed: this.typewriterSpeed,
|
|
538
|
+
autoDelayMs: this.autoDelayMs,
|
|
539
|
+
musicVolume: this.musicVolume,
|
|
540
|
+
soundVolume: this.soundVolume,
|
|
541
|
+
onSettingsChange: (s) => {
|
|
542
|
+
this.typewriterSpeed = s.typewriterSpeed;
|
|
543
|
+
this.autoDelayMs = s.autoDelayMs;
|
|
544
|
+
if (s.musicVolume !== undefined) {
|
|
545
|
+
this.musicVolume = s.musicVolume;
|
|
546
|
+
this.audioManager?.setMusicVolume(this.musicVolume);
|
|
547
|
+
}
|
|
548
|
+
if (s.soundVolume !== undefined) {
|
|
549
|
+
this.soundVolume = s.soundVolume;
|
|
550
|
+
this.audioManager?.setSoundVolume(this.soundVolume);
|
|
551
|
+
}
|
|
552
|
+
this.dialogueBox.setTypewriterSpeed(this.typewriterSpeed);
|
|
553
|
+
this.saveSettings();
|
|
554
|
+
}
|
|
555
|
+
})
|
|
377
556
|
});
|
|
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);
|
|
557
|
+
this.mainMenu.mount(this.stageLayer.stageEl);
|
|
558
|
+
this.rootEl.appendChild(this.stageLayer.stageEl);
|
|
393
559
|
this.container.appendChild(this.rootEl);
|
|
560
|
+
this.viewportAdapter = new ViewportAdapter(this.container, this.stageLayer.stageEl, this.options.virtualCanvas);
|
|
394
561
|
}
|
|
395
562
|
bindEvents() {
|
|
396
|
-
// Clicking on dialogue advances or finishes typewriter
|
|
397
|
-
this.
|
|
398
|
-
if (this.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (
|
|
402
|
-
|
|
563
|
+
// Clicking on stage or dialogue box advances or finishes typewriter
|
|
564
|
+
this.stageLayer.stageEl.addEventListener('click', (e) => {
|
|
565
|
+
if (this.isMainMenuActive())
|
|
566
|
+
return;
|
|
567
|
+
const target = e.target;
|
|
568
|
+
if (target.closest('button') ||
|
|
569
|
+
target.closest('.kawa-quick-menu') ||
|
|
570
|
+
target.closest('.kawa-choice-container') ||
|
|
571
|
+
target.closest('.kawa-hotspot-layer') ||
|
|
572
|
+
target.closest('.kawa-modal-overlay') ||
|
|
573
|
+
target.closest('.kawa-ending-card')) {
|
|
574
|
+
return;
|
|
403
575
|
}
|
|
404
|
-
this.handleUserAdvance();
|
|
405
|
-
});
|
|
406
|
-
this.backBtnEl.addEventListener('click', (e) => {
|
|
407
|
-
e.stopPropagation();
|
|
408
576
|
if (this.isAutoMode)
|
|
409
577
|
this.toggleAutoMode(false);
|
|
410
578
|
if (this.isSkipMode)
|
|
411
579
|
this.toggleSkipMode(false);
|
|
412
|
-
this.
|
|
580
|
+
this.handleUserAdvance();
|
|
413
581
|
});
|
|
414
582
|
// Keyboard controls
|
|
415
583
|
this.boundKeyHandler = (e) => {
|
|
584
|
+
const confirm = this.rootEl.querySelector('.kawa-confirm-overlay');
|
|
585
|
+
if (confirm) {
|
|
586
|
+
if (e.key === 'Escape') {
|
|
587
|
+
e.preventDefault();
|
|
588
|
+
confirm.remove();
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const lightbox = this.rootEl.querySelector('.kawa-gallery-lightbox');
|
|
593
|
+
if (lightbox) {
|
|
594
|
+
if (e.key === 'Escape') {
|
|
595
|
+
e.preventDefault();
|
|
596
|
+
lightbox.remove();
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
416
600
|
const modal = this.rootEl.querySelector('.kawa-modal-overlay');
|
|
417
601
|
if (modal) {
|
|
418
602
|
if (e.key === 'Escape')
|
|
419
603
|
modal.remove();
|
|
420
604
|
return;
|
|
421
605
|
}
|
|
606
|
+
if (this.isMainMenuActive()) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
422
609
|
if (e.code === 'Space' || e.code === 'Enter') {
|
|
423
|
-
if (document.activeElement &&
|
|
610
|
+
if (document.activeElement &&
|
|
611
|
+
(document.activeElement.tagName === 'BUTTON' ||
|
|
612
|
+
document.activeElement.tagName === 'INPUT' ||
|
|
613
|
+
document.activeElement.tagName === 'TEXTAREA')) {
|
|
424
614
|
return;
|
|
425
615
|
}
|
|
426
616
|
e.preventDefault();
|
|
@@ -441,15 +631,16 @@ export class DOMRenderer {
|
|
|
441
631
|
else if (e.key === 'a' || e.key === 'A') {
|
|
442
632
|
this.toggleAutoMode();
|
|
443
633
|
}
|
|
444
|
-
else if (e.key === '
|
|
634
|
+
else if (e.key === 'Control') {
|
|
635
|
+
// Ctrl toggles skip (Ren'Py-style). Tab is left for accessibility focus navigation.
|
|
445
636
|
e.preventDefault();
|
|
446
637
|
this.toggleSkipMode();
|
|
447
638
|
}
|
|
448
639
|
else if (e.key === 's' || e.key === 'S') {
|
|
449
|
-
this.showSaveLoadModal('save');
|
|
640
|
+
void this.showSaveLoadModal('save');
|
|
450
641
|
}
|
|
451
642
|
else if (e.key === 'l' || e.key === 'L') {
|
|
452
|
-
this.showSaveLoadModal('load');
|
|
643
|
+
void this.showSaveLoadModal('load');
|
|
453
644
|
}
|
|
454
645
|
else if (e.key === 'h' || e.key === 'H') {
|
|
455
646
|
this.showHistoryModal();
|
|
@@ -457,514 +648,244 @@ export class DOMRenderer {
|
|
|
457
648
|
else if (e.key === 'p' || e.key === 'P' || e.key === 'o' || e.key === 'O') {
|
|
458
649
|
this.showSettingsModal();
|
|
459
650
|
}
|
|
651
|
+
else if (e.key === 'Escape') {
|
|
652
|
+
this.showMainMenu();
|
|
653
|
+
}
|
|
460
654
|
};
|
|
461
655
|
if (typeof window !== 'undefined') {
|
|
462
656
|
window.addEventListener('keydown', this.boundKeyHandler);
|
|
463
657
|
}
|
|
658
|
+
// Mobile: swipe left = advance, swipe right = rollback
|
|
659
|
+
if (this.options.features?.touchGestures !== false) {
|
|
660
|
+
this.touchControls = bindSwipeControls(this.stageLayer.stageEl, {
|
|
661
|
+
onSwipeLeft: () => {
|
|
662
|
+
if (this.isMainMenuActive())
|
|
663
|
+
return;
|
|
664
|
+
if (this.isAutoMode)
|
|
665
|
+
this.toggleAutoMode(false);
|
|
666
|
+
if (this.isSkipMode)
|
|
667
|
+
this.toggleSkipMode(false);
|
|
668
|
+
this.handleUserAdvance();
|
|
669
|
+
},
|
|
670
|
+
onSwipeRight: () => {
|
|
671
|
+
if (this.isMainMenuActive())
|
|
672
|
+
return;
|
|
673
|
+
if (this.isAutoMode)
|
|
674
|
+
this.toggleAutoMode(false);
|
|
675
|
+
if (this.isSkipMode)
|
|
676
|
+
this.toggleSkipMode(false);
|
|
677
|
+
if (this.vm.canRollback())
|
|
678
|
+
this.vm.rollback();
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}
|
|
464
682
|
}
|
|
465
683
|
handleUserAdvance() {
|
|
466
|
-
if (this.
|
|
467
|
-
|
|
684
|
+
if (this.isMainMenuActive())
|
|
685
|
+
return;
|
|
686
|
+
if (this.vm.getState().pendingInput)
|
|
687
|
+
return;
|
|
688
|
+
if (this.dialogueBox.getIsTypewriting()) {
|
|
689
|
+
this.dialogueBox.finishTypewriter();
|
|
690
|
+
const debounce = this.options.features?.advanceDebounceMs ?? 220;
|
|
691
|
+
this.advanceLockUntil = Date.now() + Math.max(0, debounce);
|
|
692
|
+
return;
|
|
468
693
|
}
|
|
469
|
-
|
|
470
|
-
|
|
694
|
+
if (Date.now() < this.advanceLockUntil) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const state = this.vm.getState();
|
|
698
|
+
if (state.choices && state.choices.length > 0) {
|
|
699
|
+
return;
|
|
471
700
|
}
|
|
701
|
+
if (state.hotspots && state.hotspots.length > 0) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
this.vm.next();
|
|
472
705
|
}
|
|
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
|
-
}
|
|
706
|
+
syncUrlLabel(state) {
|
|
707
|
+
if (!this.options.syncUrlLabel || typeof window === 'undefined' || typeof history === 'undefined') {
|
|
708
|
+
return;
|
|
506
709
|
}
|
|
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
|
-
}
|
|
710
|
+
const label = state.currentLabel;
|
|
711
|
+
if (!label || label.startsWith('__'))
|
|
712
|
+
return;
|
|
713
|
+
try {
|
|
714
|
+
const url = new URL(window.location.href);
|
|
715
|
+
if (url.searchParams.get('at') === label)
|
|
716
|
+
return;
|
|
717
|
+
url.searchParams.delete('label');
|
|
718
|
+
url.searchParams.set('at', label);
|
|
719
|
+
history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
|
|
521
720
|
}
|
|
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'})`;
|
|
721
|
+
catch {
|
|
722
|
+
// ignore malformed URL environments
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
syncShareMeta(state) {
|
|
726
|
+
const share = this.options.share;
|
|
727
|
+
if (!share && !this.options.syncUrlLabel)
|
|
728
|
+
return;
|
|
729
|
+
const pageUrl = typeof window !== 'undefined' ? window.location.href : undefined;
|
|
730
|
+
const meta = buildSceneShareMeta(this.vm.getStory(), state, share, pageUrl);
|
|
731
|
+
applyDocumentShareMeta({
|
|
732
|
+
...meta,
|
|
733
|
+
image: meta.image ? resolveShareAssetUrl(meta.image) : undefined
|
|
734
|
+
}, share);
|
|
735
|
+
}
|
|
736
|
+
applyThemeAndStyles(state) {
|
|
737
|
+
if (state.theme) {
|
|
738
|
+
this.rootEl.dataset.kawaTheme = state.theme;
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
delete this.rootEl.dataset.kawaTheme;
|
|
742
|
+
}
|
|
743
|
+
const nextTargets = new Set(Object.keys(state.styleClasses ?? {}));
|
|
744
|
+
for (const target of this.appliedStyleTargets) {
|
|
745
|
+
if (!nextTargets.has(target)) {
|
|
746
|
+
this.rootEl.classList.forEach((cls) => {
|
|
747
|
+
if (cls.startsWith(`kawa-style-${target}-`)) {
|
|
748
|
+
this.rootEl.classList.remove(cls);
|
|
564
749
|
}
|
|
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);
|
|
750
|
+
});
|
|
575
751
|
}
|
|
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;
|
|
752
|
+
}
|
|
753
|
+
for (const [target, name] of Object.entries(state.styleClasses ?? {})) {
|
|
754
|
+
const prefix = `kawa-style-${target}-`;
|
|
755
|
+
this.rootEl.classList.forEach((cls) => {
|
|
756
|
+
if (cls.startsWith(prefix) && cls !== `${prefix}${name}`) {
|
|
757
|
+
this.rootEl.classList.remove(cls);
|
|
588
758
|
}
|
|
589
|
-
}
|
|
759
|
+
});
|
|
760
|
+
this.rootEl.classList.add(`${prefix}${name}`);
|
|
590
761
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
762
|
+
this.appliedStyleTargets = nextTargets;
|
|
763
|
+
}
|
|
764
|
+
render(state) {
|
|
765
|
+
this.quickMenu.setBackDisabled(!this.vm.canRollback());
|
|
766
|
+
this.applyThemeAndStyles(state);
|
|
767
|
+
// Timed pause: auto-advance after pendingPauseMs (click still skips via handleUserAdvance)
|
|
768
|
+
if (this.pauseTimer) {
|
|
769
|
+
clearTimeout(this.pauseTimer);
|
|
770
|
+
this.pauseTimer = null;
|
|
771
|
+
}
|
|
772
|
+
if (state.pendingPauseMs != null && state.pendingPauseMs > 0 && !state.isFinished) {
|
|
773
|
+
const delay = state.pendingPauseMs;
|
|
774
|
+
this.pauseTimer = window.setTimeout(() => {
|
|
775
|
+
this.pauseTimer = null;
|
|
776
|
+
if (this.destroyed)
|
|
777
|
+
return;
|
|
778
|
+
const cur = this.vm.getState();
|
|
779
|
+
if (cur.pendingPauseMs != null && !cur.isFinished) {
|
|
780
|
+
this.vm.next();
|
|
599
781
|
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
782
|
+
}, delay);
|
|
783
|
+
}
|
|
784
|
+
// 1. Background scene
|
|
785
|
+
this.stageLayer.updateBackground(state.visual.background, state.visual.transition);
|
|
786
|
+
// 2. Character sprites
|
|
787
|
+
this.stageLayer.updateCharacters(state.visual.characters);
|
|
788
|
+
// 3. VFX & Atmospheric Layer
|
|
789
|
+
this.stageLayer.updateVfx(state.visual.vfx);
|
|
790
|
+
// 4. Fullscreen CG Layer
|
|
791
|
+
this.stageLayer.updateCG(state.visual.activeCG);
|
|
792
|
+
// 5. Branching Choice Menu
|
|
793
|
+
this.choiceMenu.render(state.choices);
|
|
794
|
+
// 5b. CG / stage hotspots
|
|
795
|
+
this.hotspotLayer.render(state.hotspots);
|
|
796
|
+
// 6. Dialogue Box (respect `window hide` / `window show`)
|
|
797
|
+
if (state.windowVisible === false) {
|
|
798
|
+
this.dialogueBox.render(null);
|
|
605
799
|
}
|
|
606
800
|
else {
|
|
607
|
-
this.
|
|
801
|
+
this.dialogueBox.render(state.dialogue, () => {
|
|
802
|
+
if (this.isAutoMode &&
|
|
803
|
+
!state.isFinished &&
|
|
804
|
+
(!state.choices || state.choices.length === 0) &&
|
|
805
|
+
(!state.hotspots || state.hotspots.length === 0) &&
|
|
806
|
+
!state.pendingInput) {
|
|
807
|
+
this.scheduleAutoAdvance();
|
|
808
|
+
}
|
|
809
|
+
});
|
|
608
810
|
}
|
|
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);
|
|
811
|
+
// 6b. Text input prompt (`input var "prompt"`)
|
|
812
|
+
if (state.pendingInput) {
|
|
813
|
+
if (!this.rootEl.querySelector('.kawa-input-overlay')) {
|
|
814
|
+
const prompt = state.pendingInput.prompt;
|
|
815
|
+
const current = typeof state.variables[state.pendingInput.variable] === 'string'
|
|
816
|
+
? String(state.variables[state.pendingInput.variable])
|
|
817
|
+
: '';
|
|
818
|
+
showInputModal(this.rootEl, {
|
|
819
|
+
prompt,
|
|
820
|
+
defaultValue: current,
|
|
821
|
+
onSubmit: (value) => this.vm.submitInput(value)
|
|
631
822
|
});
|
|
632
|
-
|
|
633
|
-
});
|
|
823
|
+
}
|
|
634
824
|
}
|
|
635
825
|
else {
|
|
636
|
-
this.
|
|
826
|
+
this.rootEl.querySelector('.kawa-input-overlay')?.remove();
|
|
637
827
|
}
|
|
638
|
-
//
|
|
828
|
+
// 7. Ending Screen if finished
|
|
639
829
|
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
|
-
}
|
|
830
|
+
this.renderEndingCard();
|
|
665
831
|
}
|
|
666
832
|
else {
|
|
667
|
-
const
|
|
668
|
-
if (
|
|
669
|
-
|
|
833
|
+
const endingEl = this.stageLayer.stageEl.querySelector('.kawa-ending-card');
|
|
834
|
+
if (endingEl)
|
|
835
|
+
endingEl.remove();
|
|
670
836
|
}
|
|
671
837
|
}
|
|
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();
|
|
838
|
+
renderEndingCard() {
|
|
839
|
+
// Don't remount — recreating replayed the pop animation and looked like a jump.
|
|
840
|
+
if (this.stageLayer.stageEl.querySelector('.kawa-ending-card')) {
|
|
686
841
|
return;
|
|
687
842
|
}
|
|
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}">
|
|
843
|
+
const endingCard = document.createElement('div');
|
|
844
|
+
endingCard.className = 'kawa-ending-card';
|
|
845
|
+
endingCard.setAttribute('role', 'alert');
|
|
846
|
+
endingCard.setAttribute('aria-label', 'Story Finished');
|
|
847
|
+
endingCard.innerHTML = `
|
|
848
|
+
<div class="kawa-ending-title">The End</div>
|
|
849
|
+
<div class="kawa-ending-subtitle">Thank you for playing!</div>
|
|
745
850
|
`;
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
851
|
+
const restartBtn = document.createElement('button');
|
|
852
|
+
restartBtn.type = 'button';
|
|
853
|
+
restartBtn.className = 'kawa-choice-btn kawa-ending-btn';
|
|
854
|
+
restartBtn.style.marginTop = '16px';
|
|
855
|
+
restartBtn.innerHTML = `${SVG_ICONS.replay} Play Again`;
|
|
856
|
+
restartBtn.addEventListener('click', (e) => {
|
|
857
|
+
e.preventDefault();
|
|
858
|
+
e.stopPropagation();
|
|
859
|
+
endingCard.remove();
|
|
860
|
+
this.startNewGame();
|
|
752
861
|
});
|
|
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();
|
|
862
|
+
const menuBtn = document.createElement('button');
|
|
863
|
+
menuBtn.type = 'button';
|
|
864
|
+
menuBtn.className = 'kawa-choice-btn kawa-ending-btn';
|
|
865
|
+
menuBtn.style.marginTop = '10px';
|
|
866
|
+
menuBtn.innerHTML = `${SVG_ICONS.home} Main Menu`;
|
|
867
|
+
menuBtn.addEventListener('click', (e) => {
|
|
868
|
+
e.preventDefault();
|
|
869
|
+
e.stopPropagation();
|
|
870
|
+
endingCard.remove();
|
|
871
|
+
this.showMainMenu();
|
|
770
872
|
});
|
|
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);
|
|
873
|
+
endingCard.appendChild(restartBtn);
|
|
874
|
+
endingCard.appendChild(menuBtn);
|
|
875
|
+
this.stageLayer.stageEl.appendChild(endingCard);
|
|
798
876
|
}
|
|
799
877
|
saveSettings() {
|
|
800
878
|
if (typeof localStorage !== 'undefined') {
|
|
801
879
|
try {
|
|
802
880
|
localStorage.setItem('kawaijs_settings', JSON.stringify({
|
|
803
881
|
typewriterSpeed: this.typewriterSpeed,
|
|
804
|
-
autoDelayMs: this.autoDelayMs
|
|
882
|
+
autoDelayMs: this.autoDelayMs,
|
|
883
|
+
musicVolume: this.musicVolume,
|
|
884
|
+
soundVolume: this.soundVolume
|
|
805
885
|
}));
|
|
806
886
|
}
|
|
807
887
|
catch { }
|
|
808
888
|
}
|
|
809
889
|
}
|
|
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
890
|
}
|
|
970
891
|
//# sourceMappingURL=renderer.js.map
|