webtalekit-alpha 0.2.11 → 0.2.14
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 +231 -87
- package/package.json +12 -2
- package/parser/checker.js +184 -0
- package/parser/checker.test.ts +491 -0
- package/parser/cli.js +34 -2
- package/parser/parser.js +25 -18
- package/src/core/defaultUIHandler.js +309 -0
- package/src/core/defaultUIHandler.js.map +1 -0
- package/src/core/drawer.js +65 -49
- package/src/core/drawer.js.map +1 -1
- package/src/core/index.js +418 -179
- package/src/core/scenarioManager.js +33 -12
- package/src/core/scenarioManager.js.map +1 -1
- package/src/resource/soundObject.js +4 -2
- package/src/resource/soundObject.js.map +1 -1
- package/src/utils/eventBus.js +88 -0
- package/src/utils/eventBus.js.map +1 -0
- package/src/utils/fallbackTemplate.js +13 -0
- package/src/utils/fallbackTemplate.js.map +1 -0
- package/src/utils/logger.js +45 -1
- package/src/utils/logger.js.map +1 -1
- package/src/utils/store.js +5 -0
- package/src/utils/store.js.map +1 -1
package/src/core/index.js
CHANGED
|
@@ -4,34 +4,41 @@ import { ImageObject } from '../resource/ImageObject'
|
|
|
4
4
|
import { ResourceManager } from './resourceManager'
|
|
5
5
|
import { SoundObject } from '../resource/soundObject'
|
|
6
6
|
import engineConfig from '../../engineConfig.json'
|
|
7
|
-
import { outputLog } from '../utils/logger'
|
|
8
7
|
import { sleep } from '../utils/waitUtil'
|
|
8
|
+
import { getDefaultDialogTemplate } from '../utils/fallbackTemplate'
|
|
9
|
+
import { generateStore } from '../utils/store'
|
|
10
|
+
import { EventBus } from '../utils/eventBus'
|
|
11
|
+
import { DefaultUIHandler } from './defaultUIHandler'
|
|
12
|
+
import { logError } from '../utils/logger'
|
|
9
13
|
|
|
10
14
|
export class Core {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
// プロパティの初期化
|
|
17
|
+
this.bgm = null
|
|
18
|
+
this.isAuto = false
|
|
19
|
+
this.isNext = false
|
|
20
|
+
this.isSkip = false
|
|
21
|
+
this.onNextHandler = null
|
|
22
|
+
this.sceneFile = {}
|
|
23
|
+
this.sceneConfig = {}
|
|
24
|
+
this.commandList = {
|
|
25
|
+
text: this.textHandler,
|
|
26
|
+
choice: this.choiceHandler,
|
|
27
|
+
show: this.showHandler,
|
|
28
|
+
newpage: this.newpageHandler,
|
|
29
|
+
hide: this.hideHandler,
|
|
30
|
+
jump: this.jumpHandler,
|
|
31
|
+
sound: this.soundHandler,
|
|
32
|
+
say: this.sayHandler,
|
|
33
|
+
if: this.ifHandler,
|
|
34
|
+
call: this.callHandler,
|
|
35
|
+
moveto: this.moveToHandler,
|
|
36
|
+
route: this.routeHandler,
|
|
37
|
+
wait: this.waitHandler,
|
|
38
|
+
dialog: this.dialogHandler,
|
|
39
|
+
save: this.saveHandler,
|
|
40
|
+
load: this.loadHandler,
|
|
41
|
+
}
|
|
35
42
|
// gameContainerの初期化(HTMLのgameContainerを取得する)
|
|
36
43
|
this.gameContainer = document.getElementById('gameContainer')
|
|
37
44
|
// Drawerの初期化(canvasタグのサイズを設定する)
|
|
@@ -42,16 +49,23 @@ export class Core {
|
|
|
42
49
|
this.resourceManager = new ResourceManager(import(/* webpackIgnore: true */ '/src/resource/config.js')) // webpackIgnoreでバンドルを無視する
|
|
43
50
|
this.displayedImages = {}
|
|
44
51
|
this.usedSounds = {}
|
|
52
|
+
// ストレージの初期化
|
|
53
|
+
this.store = generateStore()
|
|
54
|
+
// EventBusの初期化
|
|
55
|
+
this.eventBus = new EventBus()
|
|
56
|
+
// DefaultUIHandlerの登録(customUI: trueの場合はスキップ)
|
|
57
|
+
if (!options.customUI) {
|
|
58
|
+
DefaultUIHandler.register(this.eventBus, this.drawer, this.gameContainer, engineConfig.resolution)
|
|
59
|
+
}
|
|
45
60
|
}
|
|
46
61
|
|
|
47
62
|
setConfig(config) {
|
|
48
|
-
outputLog('call', 'debug', config)
|
|
49
63
|
// ゲームの設定情報をセットする
|
|
50
64
|
engineConfig = config
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
async start(initScene) {
|
|
54
|
-
|
|
68
|
+
try {
|
|
55
69
|
// TODO: ブラウザ用のビルドの場合は、最初にクリックしてもらう
|
|
56
70
|
// titleタグの内容を書き換える
|
|
57
71
|
document.title = engineConfig.title
|
|
@@ -59,38 +73,43 @@ export class Core {
|
|
|
59
73
|
await this.loadScene(initScene || 'title')
|
|
60
74
|
// 画面を表示する
|
|
61
75
|
await this.loadScreen(this.sceneConfig)
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this.
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
document.querySelector('#gameContainer').addEventListener('keyup', (e) => {
|
|
72
|
-
if (e.key === 'Control') {
|
|
73
|
-
this.drawer.isSkip = true
|
|
74
|
-
this.isNext = false
|
|
75
|
-
}
|
|
76
|
-
})
|
|
77
|
-
document.querySelector('#gameContainer').addEventListener('click', (e) => {
|
|
78
|
-
this.onNextHandler()
|
|
76
|
+
// 入力イベントを設定する(DefaultUIHandlerに委譲)
|
|
77
|
+
await this.eventBus.emit('input:bind', {
|
|
78
|
+
onNext: () => { if (this.onNextHandler) this.onNextHandler() },
|
|
79
|
+
setSkip: (drawerSkip, coreNext) => {
|
|
80
|
+
this.drawer.isSkip = drawerSkip
|
|
81
|
+
this.isNext = coreNext
|
|
82
|
+
},
|
|
83
|
+
toggleAuto: () => { this.isAuto = !this.isAuto },
|
|
84
|
+
toggleSkip: () => { this.isSkip = !this.isSkip },
|
|
79
85
|
})
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
await this.textHandler('タップでスタート')
|
|
88
|
+
// BGMを再生する
|
|
89
|
+
await this.soundHandler({
|
|
90
|
+
mode: 'bgm',
|
|
91
|
+
src: this.sceneConfig.bgm,
|
|
92
|
+
loop: true,
|
|
93
|
+
play: true,
|
|
94
|
+
})
|
|
95
|
+
// シナリオを実行する
|
|
96
|
+
while (this.scenarioManager.hasNext()) {
|
|
97
|
+
await this.runScenario()
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
// エラーをログに記録(スタックトレース付き)
|
|
101
|
+
await logError(error, 'Error in runScenario')
|
|
102
|
+
// エラーをアラートで表示
|
|
103
|
+
alert(`システムエラーが発生しました。\n詳細はコンソールで確認してください。:\n${error.message}`)
|
|
104
|
+
throw error
|
|
87
105
|
}
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
async loadScene(sceneFileName) {
|
|
91
|
-
outputLog('call', 'debug', sceneFileName)
|
|
92
109
|
// sceneファイルを読み込む
|
|
93
|
-
|
|
110
|
+
// ESモジュールの名前空間オブジェクトは外部から書き込み不可なため、プレーンオブジェクトにコピーする
|
|
111
|
+
const moduleNamespace = await import(/* webpackChunkName: "[request]" */ `/src/js/${sceneFileName}.js`)
|
|
112
|
+
this.sceneFile = { ...moduleNamespace }
|
|
94
113
|
// sceneファイルの初期化処理を実行
|
|
95
114
|
if (this.sceneFile.init) {
|
|
96
115
|
this.sceneFile.init(this.getAPIForScript())
|
|
@@ -98,102 +117,127 @@ export class Core {
|
|
|
98
117
|
// シナリオの進行状況を初期化
|
|
99
118
|
this.scenarioManager.setScenario(this.sceneFile.scenario, sceneFileName)
|
|
100
119
|
this.sceneConfig = { ...this.sceneConfig, ...this.sceneFile.sceneConfig }
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
async
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// ゲーム進行用に必要な情報をセットする
|
|
122
|
-
this.drawer.setScreen(this.gameContainer, engineConfig.resolution)
|
|
123
|
-
// シナリオの進行状況を保存
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ファイルの存在確認を行う関数
|
|
123
|
+
async checkResourceExists(url) {
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(url, { method: 'HEAD' })
|
|
126
|
+
return response.ok
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async loadScreen(sceneConfig, options = {}) {
|
|
133
|
+
const {
|
|
134
|
+
isDialog = false, // ダイアログモードかどうか
|
|
135
|
+
fallbackTemplate = null, // フォールバック用テンプレート
|
|
136
|
+
skipBackground = false, // 背景画像の読み込みをスキップ
|
|
137
|
+
} = options
|
|
138
|
+
|
|
139
|
+
// 画面名を設定する。
|
|
124
140
|
this.scenarioManager.progress.currentScene = sceneConfig.name
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
this.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
this.scenarioManager.setSceneName(sceneConfig.name)
|
|
142
|
+
// テンプレートの存在確認(ダイアログ以外のみ)
|
|
143
|
+
if (!isDialog && !(await this.checkResourceExists(sceneConfig.template))) {
|
|
144
|
+
console.error(`Template file not found: ${sceneConfig.template}`)
|
|
145
|
+
throw new Error(`Template file not found: ${sceneConfig.template}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!this.gameContainer) {
|
|
149
|
+
throw new Error('Game container not found.')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// screen:loadイベントを発行してHTMLの読み込み・パース・DOM注入をDefaultUIHandlerに委譲する
|
|
153
|
+
// テンプレートURLとフォールバック関数を渡すことで、UIフレームワークが独自のfetch/描画処理を実装できる
|
|
154
|
+
await this.eventBus.emit('screen:load', {
|
|
155
|
+
template: sceneConfig.template,
|
|
156
|
+
isDialog,
|
|
157
|
+
fallbackTemplate,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
if (!skipBackground) {
|
|
161
|
+
console.info(`background: ${await this.checkResourceExists(sceneConfig.background)}`)
|
|
162
|
+
// 背景画像の存在確認
|
|
163
|
+
if (!(await this.checkResourceExists(sceneConfig.background))) {
|
|
164
|
+
throw new Error(`Background image not found: ${sceneConfig.background}`)
|
|
165
|
+
} else {
|
|
166
|
+
// 背景画像を表示する
|
|
167
|
+
const background = await new ImageObject().setImageAsync(sceneConfig.background)
|
|
168
|
+
this.displayedImages['background'] = {
|
|
169
|
+
image: background,
|
|
170
|
+
size: {
|
|
171
|
+
width: this.gameContainer.clientWidth,
|
|
172
|
+
height: this.gameContainer.clientHeight,
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
this.drawer.show(this.displayedImages)
|
|
133
177
|
}
|
|
134
|
-
this.drawer.show(this.displayedImages)
|
|
135
|
-
this.bgm = await new SoundObject().setAudioAsync(sceneConfig.bgm)
|
|
136
178
|
}
|
|
137
179
|
|
|
138
180
|
async runScenario() {
|
|
139
|
-
|
|
181
|
+
|
|
140
182
|
let scenarioObject = this.scenarioManager.next()
|
|
141
183
|
if (!scenarioObject) {
|
|
142
184
|
return
|
|
143
185
|
}
|
|
144
|
-
outputLog('scenarioObject', 'debug', scenarioObject)
|
|
145
186
|
// シナリオオブジェクトのtypeプロパティに応じて、対応する関数を実行する
|
|
146
|
-
const
|
|
147
|
-
|
|
187
|
+
const commandType = scenarioObject.type || 'text'
|
|
188
|
+
const commandFunction = this.commandList[commandType]
|
|
189
|
+
|
|
190
|
+
// コマンドが存在しない場合のエラーハンドリング
|
|
191
|
+
if (!commandFunction) {
|
|
192
|
+
const errorMessage = `Error: Command type "${commandType}" is not defined`
|
|
193
|
+
throw new Error(errorMessage)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const boundFunction = commandFunction.bind(this)
|
|
148
197
|
scenarioObject = await this.httpHandler(scenarioObject)
|
|
198
|
+
|
|
199
|
+
// ifグローバル属性の処理
|
|
200
|
+
if (scenarioObject.if !== undefined) {
|
|
201
|
+
const condition = this.executeCode(`return ${scenarioObject.if}`)
|
|
202
|
+
|
|
203
|
+
// 条件がfalseの場合、このタグの処理をスキップ
|
|
204
|
+
if (!condition) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
149
209
|
await boundFunction(scenarioObject)
|
|
150
210
|
}
|
|
151
211
|
|
|
152
212
|
async textHandler(scenarioObject) {
|
|
153
|
-
outputLog('textHandler:line', 'debug', scenarioObject)
|
|
154
213
|
// 文章だけの場合は、contentプロパティに配列として設定する
|
|
155
214
|
if (typeof scenarioObject === 'string') scenarioObject = { content: [scenarioObject] }
|
|
156
215
|
// httpレスポンスがある場合は、list.contentに追加して、表示対象に加える
|
|
157
216
|
if (scenarioObject.then || scenarioObject.error) {
|
|
158
217
|
scenarioObject.content = scenarioObject.content.concat(scenarioObject.then || scenarioObject.error)
|
|
159
218
|
}
|
|
160
|
-
outputLog('call', 'debug', scenarioObject)
|
|
161
|
-
|
|
162
|
-
// 名前が設定されている場合は、名前を表示する
|
|
163
|
-
if (scenarioObject.name) {
|
|
164
|
-
this.drawer.drawName(scenarioObject.name)
|
|
165
|
-
} else {
|
|
166
|
-
this.drawer.drawName('')
|
|
167
|
-
}
|
|
168
219
|
|
|
169
220
|
//prettier-ignore
|
|
170
221
|
this.onNextHandler = () => { this.drawer.isSkip = true }
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
} else {
|
|
185
|
-
const container = this.drawer.createDecoratedElement(text)
|
|
186
|
-
await this.drawer.drawText(this.expandVariable(text.content[0]), text.speed || 25, container)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
222
|
+
|
|
223
|
+
// text:clearイベントを発行してテキスト表示領域をクリアする
|
|
224
|
+
await this.eventBus.emit('text:clear')
|
|
225
|
+
|
|
226
|
+
// text:showイベントを発行してテキスト表示をDefaultUIHandlerに委譲する
|
|
227
|
+
await this.eventBus.emit('text:show', {
|
|
228
|
+
name: scenarioObject.name || '',
|
|
229
|
+
content: scenarioObject.content,
|
|
230
|
+
speed: this.isSkip ? 1 : scenarioObject.speed || 25,
|
|
231
|
+
expandVariable: this.expandVariable.bind(this),
|
|
232
|
+
waitFn: this.waitHandler.bind(this),
|
|
233
|
+
})
|
|
234
|
+
|
|
190
235
|
await this.waitHandler({ wait: scenarioObject.time })
|
|
191
236
|
this.drawer.isSkip = false
|
|
192
237
|
this.scenarioManager.setHistory(scenarioObject.content)
|
|
193
238
|
}
|
|
194
239
|
|
|
195
240
|
expandVariable(text) {
|
|
196
|
-
outputLog('call', 'debug', text)
|
|
197
241
|
if (typeof text !== 'string') return text
|
|
198
242
|
return text.replace(/{{([^{}]+)}}/g, (match) => {
|
|
199
243
|
const expr = match.slice(2, -2)
|
|
@@ -205,35 +249,42 @@ export class Core {
|
|
|
205
249
|
async waitHandler(line) {
|
|
206
250
|
// line.timeがある場合、line.waitに代入する
|
|
207
251
|
if (line.time) line.wait = line.time
|
|
208
|
-
outputLog('call', 'debug', line)
|
|
209
252
|
//prettier-ignore
|
|
210
253
|
this.onNextHandler = () => { this.isNext = true }
|
|
211
|
-
outputLog('wait type', 'debug', typeof line.wait)
|
|
212
254
|
|
|
213
255
|
// line.waitが数値に変換可能な文字列の場合、数値に変換
|
|
214
256
|
if (typeof line.wait === 'string' && !isNaN(Number(line.wait))) {
|
|
215
257
|
line.wait = Number(line.wait)
|
|
216
258
|
}
|
|
259
|
+
|
|
260
|
+
// スキップモードが有効な場合は全ての待機をスキップする
|
|
261
|
+
if (this.isSkip) {
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
217
265
|
if (typeof line.wait === 'number') {
|
|
218
|
-
outputLog('wait number', 'debug', line.wait)
|
|
219
266
|
if (line.wait > 0 || this.isAuto) {
|
|
220
267
|
const waitTime = line.wait || 1500
|
|
221
268
|
// 指定された時間だけ待機
|
|
222
269
|
await sleep(waitTime)
|
|
223
270
|
}
|
|
224
271
|
} else {
|
|
225
|
-
|
|
226
|
-
|
|
272
|
+
if (this.isAuto) {
|
|
273
|
+
// オートモードが有効な場合はデフォルト時間後に自動進行する
|
|
274
|
+
await sleep(1500)
|
|
275
|
+
} else {
|
|
276
|
+
// 改行ごとに入力待ち
|
|
277
|
+
await this.clickWait()
|
|
278
|
+
}
|
|
227
279
|
}
|
|
228
280
|
}
|
|
229
281
|
|
|
230
282
|
// クリック待ち処理
|
|
231
283
|
async clickWait() {
|
|
232
|
-
outputLog('call', 'debug')
|
|
233
284
|
this.drawer.setVisibility('#waitCircle', true)
|
|
234
285
|
return new Promise((resolve) => {
|
|
235
286
|
const intervalId = setInterval(() => {
|
|
236
|
-
if (this.isNext) {
|
|
287
|
+
if (this.isNext || this.isAuto || this.isSkip) {
|
|
237
288
|
this.drawer.setVisibility('#waitCircle', false)
|
|
238
289
|
clearInterval(intervalId)
|
|
239
290
|
this.isNext = false
|
|
@@ -244,31 +295,29 @@ export class Core {
|
|
|
244
295
|
}
|
|
245
296
|
|
|
246
297
|
async sayHandler(line) {
|
|
247
|
-
outputLog('call', 'debug', line)
|
|
248
298
|
// say(name:string, pattern: string, voice: {playの引数}, ...text)
|
|
249
|
-
if (line.voice) await this.soundHandler({ path: line.voice, play:
|
|
299
|
+
if (line.voice) await this.soundHandler({ path: line.voice, play: true })
|
|
250
300
|
await this.textHandler({ content: line.content, name: line.name, speed: line.speed || 25 })
|
|
251
301
|
this.scenarioManager.setHistory(line)
|
|
252
302
|
}
|
|
253
303
|
|
|
254
304
|
async choiceHandler(line) {
|
|
255
|
-
outputLog('call', 'debug', line)
|
|
256
|
-
document.querySelector('#interactiveView').style.visibility = 'visible'
|
|
257
305
|
if (line.prompt) this.textHandler(line.prompt)
|
|
258
306
|
// ムスタッシュ構文があるときは、変数の展開
|
|
259
307
|
line.content.forEach((choice) => {
|
|
260
308
|
choice.label = this.expandVariable(choice.label)
|
|
261
309
|
})
|
|
262
|
-
|
|
310
|
+
// choice:showイベントを発行して選択肢の表示と選択結果の取得をDefaultUIHandlerに委譲する
|
|
311
|
+
const [result] = await this.eventBus.emit('choice:show', line)
|
|
312
|
+
const { selectId, onSelect: selectHandler } = result || {}
|
|
263
313
|
if (selectHandler !== undefined) {
|
|
264
314
|
this.scenarioManager.addScenario(selectHandler)
|
|
265
315
|
}
|
|
266
316
|
this.scenarioManager.setHistory({ line, ...selectId })
|
|
267
|
-
|
|
317
|
+
this.isNext = false
|
|
268
318
|
}
|
|
269
319
|
|
|
270
320
|
jumpHandler(line) {
|
|
271
|
-
outputLog('call:', 'debug', line.index)
|
|
272
321
|
// ジャンプ先が現在の行より小さいときは、今の行とジャンプ先の行の間で、sub=falseの行を抽出して、scenarioManagerに追加する
|
|
273
322
|
if (line.index < this.scenarioManager.getIndex()) {
|
|
274
323
|
// scenarioManagerからシナリオを取得
|
|
@@ -278,23 +327,19 @@ export class Core {
|
|
|
278
327
|
before: scenario.slice(0, line.index),
|
|
279
328
|
after: scenario.slice(this.scenarioManager.getIndex()),
|
|
280
329
|
}
|
|
281
|
-
outputLog('noEditScenarioList', 'debug', noEditScenarioList)
|
|
282
330
|
// ジャンプ先のインデックスまでのシナリオを取得
|
|
283
331
|
const scenarioList = scenario.slice(line.index, this.scenarioManager.getIndex())
|
|
284
|
-
outputLog('scenarioList', 'debug', scenarioList)
|
|
285
332
|
// sub=falseの行だけを取得
|
|
286
333
|
const subFalseScenario = scenarioList.filter((line) => !line.sub)
|
|
287
|
-
|
|
334
|
+
// after に残っている sub=true の要素を除去(前回の選択肢の残骸を除去する)
|
|
335
|
+
const filteredAfter = noEditScenarioList.after.filter((item) => !item.sub)
|
|
288
336
|
// scenarioManagerに追加
|
|
289
|
-
this.scenarioManager.setScenario([...noEditScenarioList.before, ...subFalseScenario, ...
|
|
290
|
-
outputLog('scenarioManager', 'debug', this.scenarioManager.getScenario())
|
|
337
|
+
this.scenarioManager.setScenario([...noEditScenarioList.before, ...subFalseScenario, ...filteredAfter])
|
|
291
338
|
}
|
|
292
|
-
this.newpageHandler()
|
|
293
339
|
this.scenarioManager.setIndex(Number(line.index))
|
|
294
340
|
}
|
|
295
341
|
|
|
296
342
|
async showHandler(line) {
|
|
297
|
-
outputLog('line', 'debug', line)
|
|
298
343
|
// ムスタッシュ構文があるときは、変数の展開
|
|
299
344
|
Object.keys(line).forEach((item) => {
|
|
300
345
|
line[item] = this.expandVariable(line[item])
|
|
@@ -352,7 +397,6 @@ export class Core {
|
|
|
352
397
|
entry: line.entry,
|
|
353
398
|
}
|
|
354
399
|
|
|
355
|
-
outputLog('displayedImages', 'debug', this.displayedImages[key])
|
|
356
400
|
if (line.sepia) this.displayedImages[key].image.setSepia(line.sepia)
|
|
357
401
|
if (line.mono) this.displayedImages[key].image.setMonochrome(line.mono)
|
|
358
402
|
if (line.blur) this.displayedImages[key].image.setBlur(line.blur)
|
|
@@ -371,11 +415,9 @@ export class Core {
|
|
|
371
415
|
// 通常の表示処理
|
|
372
416
|
this.drawer.show(this.displayedImages)
|
|
373
417
|
}
|
|
374
|
-
outputLog('this.displayedImages', 'debug', this.displayedImages)
|
|
375
418
|
}
|
|
376
419
|
|
|
377
420
|
async hideHandler(line) {
|
|
378
|
-
outputLog('call', 'debug', line)
|
|
379
421
|
const targetImage = this.displayedImages[line.name]
|
|
380
422
|
if (line.mode === 'cg') {
|
|
381
423
|
this.displayedImages = { ...this.tempImages }
|
|
@@ -395,49 +437,52 @@ export class Core {
|
|
|
395
437
|
}
|
|
396
438
|
|
|
397
439
|
async moveToHandler(line) {
|
|
398
|
-
outputLog('moveToHandler:line', 'debug', line)
|
|
399
440
|
const key = line.name
|
|
400
|
-
outputLog('moveToHandler:displayedImages', 'debug', this.displayedImages)
|
|
401
441
|
await this.drawer.moveTo(key, this.displayedImages, { x: line.x, y: line.y }, line.duration | 1)
|
|
402
442
|
}
|
|
403
443
|
|
|
404
444
|
async getImageObject(line) {
|
|
405
|
-
outputLog('call', 'debug', line)
|
|
406
445
|
const name = line.name || line.src.split('/').pop()
|
|
407
446
|
let image
|
|
447
|
+
|
|
448
|
+
// ファイルの存在確認
|
|
449
|
+
if (!(await this.checkResourceExists(line.src))) {
|
|
450
|
+
throw new Error(`Image file not found: ${line.src}`)
|
|
451
|
+
}
|
|
452
|
+
|
|
408
453
|
// 既にインスタンスがある場合は、それを使う
|
|
409
454
|
if (Object.hasOwn(this.displayedImages, name)) {
|
|
410
455
|
const targetImage = this.displayedImages[name]
|
|
411
456
|
const imageObject = targetImage ? targetImage.image : new ImageObject()
|
|
412
457
|
image = await imageObject.setImageAsync(line.src)
|
|
413
458
|
} else {
|
|
414
|
-
outputLog('new ImageObject', 'debug')
|
|
415
459
|
image = await new ImageObject().setImageAsync(line.src)
|
|
416
460
|
}
|
|
417
461
|
return image
|
|
418
462
|
}
|
|
419
463
|
|
|
420
464
|
async soundHandler(line) {
|
|
421
|
-
|
|
422
|
-
|
|
465
|
+
const soundObject = await this.getSoundObject(line)
|
|
466
|
+
|
|
423
467
|
if (line.mode === 'bgm') {
|
|
424
|
-
|
|
468
|
+
// BGMの場合、既存のBGMを停止して、新しいBGMをセットする
|
|
469
|
+
if (this.bgm && this.bgm.isPlaying) {
|
|
425
470
|
this.bgm.stop()
|
|
426
471
|
}
|
|
427
|
-
soundObject = await this.getSoundObject(line)
|
|
428
472
|
this.bgm = soundObject
|
|
473
|
+
this.bgm.play(true)
|
|
429
474
|
} else {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
475
|
+
if ('play' in line) {
|
|
476
|
+
'loop' in line ? soundObject.play(true) : soundObject.play()
|
|
477
|
+
}
|
|
433
478
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
} else if ('stop' in line) {
|
|
479
|
+
|
|
480
|
+
if ('stop' in line) {
|
|
437
481
|
soundObject.stop()
|
|
438
482
|
} else if ('pause' in line) {
|
|
439
483
|
soundObject.pause()
|
|
440
484
|
}
|
|
485
|
+
|
|
441
486
|
// soundObjectを管理オブジェクトに追加
|
|
442
487
|
const key = line.name || line.src.split('/').pop()
|
|
443
488
|
this.usedSounds[key] = {
|
|
@@ -446,9 +491,17 @@ export class Core {
|
|
|
446
491
|
}
|
|
447
492
|
|
|
448
493
|
async getSoundObject(line) {
|
|
449
|
-
outputLog('call', 'debug', line)
|
|
450
494
|
const name = line.name || line.src.split('/').pop()
|
|
451
495
|
let resource
|
|
496
|
+
|
|
497
|
+
// ファイルの存在確認
|
|
498
|
+
if (line.src) {
|
|
499
|
+
if (!(await this.checkResourceExists(line.src))) {
|
|
500
|
+
throw new Error(`Sound file not found: ${line.src}`)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 既にインスタンスがある場合は、それを使う
|
|
452
505
|
if (Object.hasOwn(this.usedSounds, name)) {
|
|
453
506
|
const targetResource = this.usedSounds[name]
|
|
454
507
|
const soundObject = targetResource ? targetResource.audio : new SoundObject()
|
|
@@ -460,7 +513,6 @@ export class Core {
|
|
|
460
513
|
}
|
|
461
514
|
|
|
462
515
|
newpageHandler() {
|
|
463
|
-
outputLog('call', 'debug')
|
|
464
516
|
this.displayedImages = {
|
|
465
517
|
background: {
|
|
466
518
|
image: this.getBackground(),
|
|
@@ -475,20 +527,12 @@ export class Core {
|
|
|
475
527
|
}
|
|
476
528
|
|
|
477
529
|
async ifHandler(line) {
|
|
478
|
-
outputLog('call', 'debug', line)
|
|
479
530
|
const isTrue = this.executeCode(`return ${line.condition}`)
|
|
480
|
-
outputLog(`${isTrue}`, 'debug')
|
|
481
531
|
const appendScenario = isTrue ? line.content[0].content : line.content[1].content
|
|
482
|
-
outputLog('', 'debug', appendScenario)
|
|
483
532
|
this.scenarioManager.addScenario(appendScenario)
|
|
484
533
|
}
|
|
485
534
|
|
|
486
535
|
async routeHandler(line) {
|
|
487
|
-
outputLog('call', 'debug', line)
|
|
488
|
-
if (this.bgm.isPlaying) {
|
|
489
|
-
this.bgm.stop()
|
|
490
|
-
this.bgm = null
|
|
491
|
-
}
|
|
492
536
|
this.newpageHandler()
|
|
493
537
|
if (this.sceneFile.cleanUp) {
|
|
494
538
|
// 終了処理を実行する
|
|
@@ -499,20 +543,26 @@ export class Core {
|
|
|
499
543
|
// 画面を表示する
|
|
500
544
|
await this.loadScreen(this.sceneConfig)
|
|
501
545
|
// BGMを再生する
|
|
502
|
-
this.
|
|
546
|
+
this.soundHandler({
|
|
547
|
+
mode: 'bgm',
|
|
548
|
+
src: this.sceneConfig.bgm,
|
|
549
|
+
loop: true,
|
|
550
|
+
play: true,
|
|
551
|
+
})
|
|
503
552
|
}
|
|
504
553
|
|
|
505
554
|
// Sceneファイルに、ビルド時に実行処理を追加して、そこに処理をお願いしたほうがいいかも?
|
|
506
|
-
callHandler(line) {
|
|
507
|
-
|
|
508
|
-
|
|
555
|
+
async callHandler(line) {
|
|
556
|
+
const result = this.executeCode(line.method)
|
|
557
|
+
if (result && typeof result.then === 'function') {
|
|
558
|
+
await result
|
|
559
|
+
}
|
|
509
560
|
}
|
|
510
561
|
|
|
511
562
|
async httpHandler(line) {
|
|
512
563
|
if (!(line.get || line.post || line.put || line.delete)) {
|
|
513
564
|
return line
|
|
514
565
|
}
|
|
515
|
-
outputLog('call', 'debug', line)
|
|
516
566
|
// progress属性を処理する
|
|
517
567
|
// prettier-ignore
|
|
518
568
|
const progressText = line.content.filter((content) => content.type === 'progress')[0]
|
|
@@ -538,8 +588,6 @@ export class Core {
|
|
|
538
588
|
}),
|
|
539
589
|
{},
|
|
540
590
|
)
|
|
541
|
-
outputLog('headers', 'debug', headers)
|
|
542
|
-
outputLog('body', 'debug', body)
|
|
543
591
|
const response = await fetch(line.get || line.post || line.put || line.delete, {
|
|
544
592
|
method: line.get ? 'GET' : line.post ? 'POST' : line.put ? 'PUT' : 'DELETE',
|
|
545
593
|
headers: headers,
|
|
@@ -548,7 +596,6 @@ export class Core {
|
|
|
548
596
|
if (response.ok) {
|
|
549
597
|
const json = await response.json()
|
|
550
598
|
this.sceneFile.res = json
|
|
551
|
-
outputLog('res', 'debug', json)
|
|
552
599
|
line.then = line.content.filter((content) => content.type === 'then')[0].content
|
|
553
600
|
} else {
|
|
554
601
|
this.sceneFile.res = json
|
|
@@ -570,6 +617,27 @@ export class Core {
|
|
|
570
617
|
return line
|
|
571
618
|
}
|
|
572
619
|
|
|
620
|
+
async dialogHandler(scenarioObject) {
|
|
621
|
+
if (!scenarioObject || !scenarioObject.content) {
|
|
622
|
+
throw new Error('Invalid scenario object for dialog handler.')
|
|
623
|
+
}
|
|
624
|
+
// ダイアログのテンプレートを読み込む
|
|
625
|
+
// screen:loadイベント(isDialog:true)ハンドラ内で既存ダイアログの閉鎖も行われる
|
|
626
|
+
await this.loadScreen(scenarioObject, {
|
|
627
|
+
isDialog: true,
|
|
628
|
+
skipBackground: true,
|
|
629
|
+
skipBgm: true,
|
|
630
|
+
fallbackTemplate: getDefaultDialogTemplate,
|
|
631
|
+
})
|
|
632
|
+
// dialog:showイベントを発行してダイアログのDOM操作をDefaultUIHandlerに委譲する
|
|
633
|
+
const [result] = await this.eventBus.emit('dialog:show', {
|
|
634
|
+
content: scenarioObject.content,
|
|
635
|
+
expandVariable: this.expandVariable.bind(this),
|
|
636
|
+
addScenario: this.scenarioManager.addScenario.bind(this.scenarioManager),
|
|
637
|
+
})
|
|
638
|
+
return result
|
|
639
|
+
}
|
|
640
|
+
|
|
573
641
|
setBackground(image) {
|
|
574
642
|
this.displayedImages['background'] = image
|
|
575
643
|
}
|
|
@@ -579,19 +647,39 @@ export class Core {
|
|
|
579
647
|
}
|
|
580
648
|
|
|
581
649
|
executeCode(code) {
|
|
582
|
-
outputLog('call', 'debug', code)
|
|
583
650
|
try {
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
|
|
651
|
+
const keys = Object.keys(this.sceneFile).filter((key) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key))
|
|
652
|
+
const declarations = keys.map((key) => `let ${key} = _ctx[${JSON.stringify(key)}];`).join('\n')
|
|
653
|
+
const writeBacks = keys.map((key) => `_ctx[${JSON.stringify(key)}] = ${key};`).join('\n')
|
|
654
|
+
const wrappedCode = `${declarations}\nconst _result = (function() { ${code} })();\n${writeBacks}\nreturn _result;`
|
|
655
|
+
const func = new Function('_ctx', wrappedCode)
|
|
656
|
+
return func(this.sceneFile)
|
|
657
|
+
} catch (error) {
|
|
658
|
+
throw new Error(`Error executing code: ${error.message}`)
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async executeScenario(scenarioObjects) {
|
|
663
|
+
const snap = this.scenarioManager.snapshot()
|
|
664
|
+
try {
|
|
665
|
+
this.scenarioManager.setScenario(scenarioObjects)
|
|
666
|
+
await this.runScenario()
|
|
667
|
+
return { success: true }
|
|
587
668
|
} catch (error) {
|
|
588
|
-
console.error('
|
|
669
|
+
console.error('scenario error:', error)
|
|
670
|
+
return {
|
|
671
|
+
success: false,
|
|
672
|
+
error: error.message || 'Unknown error',
|
|
673
|
+
}
|
|
674
|
+
} finally {
|
|
675
|
+
this.scenarioManager.restore(snap)
|
|
589
676
|
}
|
|
590
677
|
}
|
|
591
678
|
|
|
592
679
|
// Scriptから安全にアクセスできるメソッドを定義
|
|
593
680
|
getAPIForScript() {
|
|
594
681
|
return {
|
|
682
|
+
eventBus: this.eventBus,
|
|
595
683
|
drawer: {
|
|
596
684
|
drawName: this.drawer.drawName.bind(this.drawer),
|
|
597
685
|
drawText: this.drawer.drawText.bind(this.drawer),
|
|
@@ -655,7 +743,158 @@ export class Core {
|
|
|
655
743
|
moveto: this.moveToHandler.bind(this),
|
|
656
744
|
route: this.routeHandler.bind(this),
|
|
657
745
|
wait: this.waitHandler.bind(this),
|
|
746
|
+
save: this.saveHandler.bind(this),
|
|
747
|
+
load: this.loadHandler.bind(this),
|
|
748
|
+
},
|
|
749
|
+
save: {
|
|
750
|
+
save: this.saveHandler.bind(this),
|
|
751
|
+
load: this.loadHandler.bind(this),
|
|
752
|
+
getSaveData: () => this.getSaveData(),
|
|
753
|
+
setSaveData: (data) => this.setSaveData(data),
|
|
754
|
+
getSaveList: () => this.getSaveList(),
|
|
755
|
+
deleteSave: (slot) => this.deleteSave(slot),
|
|
756
|
+
},
|
|
757
|
+
store: this.store,
|
|
758
|
+
playback: {
|
|
759
|
+
toggleAuto: () => { this.isAuto = !this.isAuto },
|
|
760
|
+
setAuto: (value) => { this.isAuto = value },
|
|
761
|
+
getAuto: () => this.isAuto,
|
|
762
|
+
toggleSkip: () => { this.isSkip = !this.isSkip },
|
|
763
|
+
setSkip: (value) => { this.isSkip = value },
|
|
764
|
+
getSkip: () => this.isSkip,
|
|
765
|
+
},
|
|
766
|
+
sandbox: {
|
|
767
|
+
execute: this.executeScenario.bind(this),
|
|
768
|
+
},
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async saveHandler(line) {
|
|
773
|
+
const slot = line.slot || 'auto'
|
|
774
|
+
const name = line.name || `セーブ${slot}`
|
|
775
|
+
|
|
776
|
+
const saveData = {
|
|
777
|
+
slot: slot,
|
|
778
|
+
name: name,
|
|
779
|
+
timestamp: new Date().toISOString(),
|
|
780
|
+
scenarioManager: {
|
|
781
|
+
progress: JSON.parse(JSON.stringify(this.scenarioManager.progress)),
|
|
782
|
+
sceneName: this.scenarioManager.getSceneName() || this.sceneConfig.name,
|
|
783
|
+
currentIndex: this.scenarioManager.getIndex(),
|
|
784
|
+
history: this.scenarioManager.getHistory ? [...this.scenarioManager.getHistory()] : [],
|
|
658
785
|
},
|
|
786
|
+
sceneConfig: this.sceneConfig,
|
|
787
|
+
displayedImages: Object.keys(this.displayedImages).reduce((acc, key) => {
|
|
788
|
+
if (key !== 'background') {
|
|
789
|
+
acc[key] = {
|
|
790
|
+
src: this.displayedImages[key].image?.src || null,
|
|
791
|
+
pos: this.displayedImages[key].pos,
|
|
792
|
+
size: this.displayedImages[key].size,
|
|
793
|
+
look: this.displayedImages[key].look,
|
|
794
|
+
entry: this.displayedImages[key].entry,
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return acc
|
|
798
|
+
}, {}),
|
|
799
|
+
backgroundImage: this.displayedImages.background?.image?.getImage()?.src || null,
|
|
800
|
+
usedSounds: Object.keys(this.usedSounds).reduce((acc, key) => {
|
|
801
|
+
acc[key] = {
|
|
802
|
+
src: this.usedSounds[key].audio?.src || null,
|
|
803
|
+
}
|
|
804
|
+
return acc
|
|
805
|
+
}, {}),
|
|
806
|
+
bgmSrc: this.bgm?.src || null,
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
this.store.set(`save_${slot}`, saveData)
|
|
810
|
+
|
|
811
|
+
if (line.message !== false) {
|
|
812
|
+
await this.textHandler(`ゲームをセーブしました: ${name}`)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async loadHandler(line) {
|
|
817
|
+
const slot = line.slot || 'auto'
|
|
818
|
+
|
|
819
|
+
const saveDataRaw = this.store.get(`save_${slot}`)
|
|
820
|
+
if (!saveDataRaw) {
|
|
821
|
+
throw new Error(`セーブデータが見つかりません: スロット${slot}`)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ディープコピーで循環参照を回避
|
|
825
|
+
const saveData = JSON.parse(JSON.stringify(saveDataRaw))
|
|
826
|
+
|
|
827
|
+
const sceneName = saveData.scenarioManager.sceneName || saveData.sceneConfig.name
|
|
828
|
+
if (!sceneName) {
|
|
829
|
+
throw new Error('Scene name not found in save data')
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// シーンとプログレスを復元
|
|
833
|
+
await this.loadScene(sceneName)
|
|
834
|
+
await this.loadScreen(saveData.sceneConfig, { skipBackground: true, skipBgm: true })
|
|
835
|
+
|
|
836
|
+
// 読んだところまで復元
|
|
837
|
+
this.scenarioManager.setSceneName(saveData.scenarioManager.sceneName)
|
|
838
|
+
this.scenarioManager.setIndex(saveData.scenarioManager.currentIndex)
|
|
839
|
+
this.scenarioManager.setHistory(saveData.scenarioManager.history || [])
|
|
840
|
+
this.scenarioManager.progress = { ...this.scenarioManager.progress, ...saveData.scenarioManager.progress }
|
|
841
|
+
|
|
842
|
+
// 画面の復元
|
|
843
|
+
this.displayedImages = {}
|
|
844
|
+
if (saveData.backgroundImage) {
|
|
845
|
+
const background = await new ImageObject().setImageAsync(saveData.backgroundImage)
|
|
846
|
+
this.displayedImages['background'] = {
|
|
847
|
+
image: background,
|
|
848
|
+
size: {
|
|
849
|
+
width: this.gameContainer.clientWidth,
|
|
850
|
+
height: this.gameContainer.clientHeight,
|
|
851
|
+
},
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
for (const [key, imageData] of Object.entries(saveData.displayedImages)) {
|
|
856
|
+
if (imageData.src) {
|
|
857
|
+
const image = await new ImageObject().setImageAsync(imageData.src)
|
|
858
|
+
this.displayedImages[key] = {
|
|
859
|
+
image: image,
|
|
860
|
+
pos: imageData.pos,
|
|
861
|
+
size: imageData.size,
|
|
862
|
+
look: imageData.look,
|
|
863
|
+
entry: imageData.entry,
|
|
864
|
+
}
|
|
865
|
+
}
|
|
659
866
|
}
|
|
867
|
+
|
|
868
|
+
// BGMの復元
|
|
869
|
+
if (saveData.bgmSrc) {
|
|
870
|
+
this.soundHandler({ mode: 'bgm', src: saveData.bgmSrc, loop: true, play: true })
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
this.drawer.show(this.displayedImages)
|
|
874
|
+
|
|
875
|
+
if (line.message !== false) {
|
|
876
|
+
await this.textHandler(`ゲームをロードしました: ${saveData.name}`)
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
getSaveData() {
|
|
881
|
+
const saveKeys = Object.keys(this.store).filter((key) => key.startsWith('save_'))
|
|
882
|
+
return saveKeys
|
|
883
|
+
.map((key) => this.store[key])
|
|
884
|
+
.sort((a, b) => {
|
|
885
|
+
return new Date(b.timestamp) - new Date(a.timestamp)
|
|
886
|
+
})
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
setSaveData(data) {
|
|
890
|
+
this.store.set(`save_${data.slot}`, data)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
getSaveList() {
|
|
894
|
+
return this.getSaveData()
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
deleteSave(slot) {
|
|
898
|
+
this.store.remove(`save_${slot}`)
|
|
660
899
|
}
|
|
661
900
|
}
|