Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
dist
coverage
docs/.vitepress/dist
docs/.vitepress/cache
*.min.js
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

178 changes: 78 additions & 100 deletions src/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import engineConfig from '../../engineConfig.json'
import { sleep } from '../utils/waitUtil'
import { getDefaultDialogTemplate } from '../utils/fallbackTemplate'
import { generateStore } from '../utils/store'
import { logError } from '../utils/logger'

export class Core {
constructor() {
Expand Down Expand Up @@ -118,16 +119,6 @@ export class Core {
}
}

// ファイルの存在確認を行う関数
async checkResourceExists(url) {
try {
const response = await fetch(url, { method: 'HEAD' })
return response.ok
} catch (error) {
return false
}
}

async loadScreen(sceneConfig, options = {}) {
const {
isDialog = false, // ダイアログモードかどうか
Expand Down Expand Up @@ -207,34 +198,44 @@ export class Core {
}

async runScenario() {
let scenarioObject = this.scenarioManager.next()
if (!scenarioObject) {
return
}
// シナリオオブジェクトのtypeプロパティに応じて、対応する関数を実行する
const commandType = scenarioObject.type || 'text'
const commandFunction = this.commandList[commandType]

// コマンドが存在しない場合のエラーハンドリング
if (!commandFunction) {
const errorMessage = `Error: Command type "${commandType}" is not defined`
throw new Error(errorMessage)
}
try {
let scenarioObject = this.scenarioManager.next()
if (!scenarioObject) {
return
}
// シナリオオブジェクトのtypeプロパティに応じて、対応する関数を実行する
const commandType = scenarioObject.type || 'text'
const commandFunction = this.commandList[commandType]

// コマンドが存在しない場合のエラーハンドリング
if (!commandFunction) {
const errorMessage = `Error: Command type "${commandType}" is not defined`
throw new Error(errorMessage)
}

const boundFunction = commandFunction.bind(this)
scenarioObject = await this.httpHandler(scenarioObject)
const boundFunction = commandFunction.bind(this)
scenarioObject = await this.httpHandler(scenarioObject)

// ifグローバル属性の処理
if (scenarioObject.if !== undefined) {
const condition = this.executeCode(`return ${scenarioObject.if}`)
// ifグローバル属性の処理
if (scenarioObject.if !== undefined) {
const condition = this.executeCode(`return ${scenarioObject.if}`)

// 条件がfalseの場合、このタグの処理をスキップ
if (!condition) {
return
// 条件がfalseの場合、このタグの処理をスキップ
if (!condition) {
return
}
}
}

await boundFunction(scenarioObject)
await boundFunction(scenarioObject)
} catch (error) {
// エラーをログに記録(スタックトレース付き)
await logError(error, 'Error in runScenario')

// エラーをアラートで表示
alert(`システムエラーが発生しました:\n${error.message}`)

// エラーを再スローせず、ゲームを継続可能にする
}
}

async textHandler(scenarioObject) {
Expand Down Expand Up @@ -473,12 +474,7 @@ export class Core {

// ファイルの存在確認
if (!(await this.checkResourceExists(line.src))) {
console.error(`Image file not found: ${line.src}`)

// エラーメッセージを表示
await this.textHandler(`エラー: 画像ファイルが見つかりません: ${line.src}`)
// 空の画像オブジェクトを返す
return new ImageObject()
throw new Error(`Image file not found: ${line.src}`)
}

// 既にインスタンスがある場合は、それを使う
Expand Down Expand Up @@ -528,12 +524,7 @@ export class Core {
// ファイルの存在確認
if(line.src){
if (!(await this.checkResourceExists(line.src))) {
console.error(`Sound file not found: ${line.src}`)

// エラーメッセージを表示
await this.textHandler(`エラー: 音声ファイルが見つかりません: ${line.src}`)
// 空のサウンドオブジェクトを返す
return new SoundObject()
throw new Error(`Sound file not found: ${line.src}`)
}
}

Expand Down Expand Up @@ -745,7 +736,7 @@ export class Core {
const func = new Function(...Object.keys(context), code)
return func.apply(null, Object.values(context))
} catch (error) {
console.error('Error executing code:', error)
throw new Error(`Error executing code: ${error.message}`)
}
}

Expand Down Expand Up @@ -878,75 +869,62 @@ export class Core {

const saveDataRaw = this.store.get ? this.store.get(`save_${slot}`) : this.store[`save_${slot}`]
if (!saveDataRaw) {
const errorMsg = `セーブデータが見つかりません: スロット${slot}`

if (line.message !== false) {
await this.textHandler(errorMsg)
}
return
throw new Error(`セーブデータが見つかりません: スロット${slot}`)
}

// ディープコピーで循環参照を回避
const saveData = JSON.parse(JSON.stringify(saveDataRaw))

try {
const sceneName = saveData.scenarioManager.sceneName || saveData.sceneConfig.name
if (!sceneName) {
throw new Error('Scene name not found in save data')
}
const sceneName = saveData.scenarioManager.sceneName || saveData.sceneConfig.name
if (!sceneName) {
throw new Error('Scene name not found in save data')
}

// シーンとプログレスを復元
await this.loadScene(sceneName)
await this.loadScreen(saveData.sceneConfig, { skipBackground: true, skipBgm: true })
// シーンとプログレスを復元
await this.loadScene(sceneName)
await this.loadScreen(saveData.sceneConfig, { skipBackground: true, skipBgm: true })

// 読んだところまで復元
this.scenarioManager.setSceneName(saveData.scenarioManager.sceneName)
this.scenarioManager.setIndex(saveData.scenarioManager.currentIndex)
this.scenarioManager.setHistory(saveData.scenarioManager.history || [])
this.scenarioManager.progress = { ...this.scenarioManager.progress, ...saveData.scenarioManager.progress }
// 読んだところまで復元
this.scenarioManager.setSceneName(saveData.scenarioManager.sceneName)
this.scenarioManager.setIndex(saveData.scenarioManager.currentIndex)
this.scenarioManager.setHistory(saveData.scenarioManager.history || [])
this.scenarioManager.progress = { ...this.scenarioManager.progress, ...saveData.scenarioManager.progress }

// 画面の復元
this.displayedImages = {}
if (saveData.backgroundImage) {
const background = await new ImageObject().setImageAsync(saveData.backgroundImage)
this.displayedImages['background'] = {
image: background,
size: {
width: this.gameContainer.clientWidth,
height: this.gameContainer.clientHeight,
},
}
// 画面の復元
this.displayedImages = {}
if (saveData.backgroundImage) {
const background = await new ImageObject().setImageAsync(saveData.backgroundImage)
this.displayedImages['background'] = {
image: background,
size: {
width: this.gameContainer.clientWidth,
height: this.gameContainer.clientHeight,
},
}
}

for (const [key, imageData] of Object.entries(saveData.displayedImages)) {
if (imageData.src) {
const image = await new ImageObject().setImageAsync(imageData.src)
this.displayedImages[key] = {
image: image,
pos: imageData.pos,
size: imageData.size,
look: imageData.look,
entry: imageData.entry,
}
for (const [key, imageData] of Object.entries(saveData.displayedImages)) {
if (imageData.src) {
const image = await new ImageObject().setImageAsync(imageData.src)
this.displayedImages[key] = {
image: image,
pos: imageData.pos,
size: imageData.size,
look: imageData.look,
entry: imageData.entry,
}
}
}

// BGMの復元
if (saveData.bgmSrc) {
this.soundHandler({ mode: 'bgm', src: saveData.bgmSrc, loop: true, play: true })
}

this.drawer.show(this.displayedImages)
// BGMの復元
if (saveData.bgmSrc) {
this.soundHandler({ mode: 'bgm', src: saveData.bgmSrc, loop: true, play: true })
}

if (line.message !== false) {
await this.textHandler(`ゲームをロードしました: ${saveData.name}`)
}
} catch (error) {
const errorMsg = `ロードに失敗しました: ${error.message}`
this.drawer.show(this.displayedImages)

if (line.message !== false) {
await this.textHandler(errorMsg)
}
if (line.message !== false) {
await this.textHandler(`ゲームをロードしました: ${saveData.name}`)
}
}

Expand Down
33 changes: 33 additions & 0 deletions src/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,36 @@ export async function outputLog(msg: string = 'None', level: LogLevel = 'log', o
console.error('Error getting stack trace:', error);
}
}

/**
* Log an error with stack trace
* @param error - The error object to log
* @param additionalInfo - Additional context information
*/
export async function logError(error: Error, additionalInfo?: string): Promise<void> {
try {
const stackframes = await StackTrace.fromError(error);
const stackString = stackframes
.map(sf => {
const functionName = sf.functionName || '<anonymous>';
const fileName = sf.fileName || '<unknown>';
const lineNumber = sf.lineNumber || 0;
const columnNumber = sf.columnNumber || 0;
return ` at ${functionName} (${fileName}:${lineNumber}:${columnNumber})`;
})
.join('\n');

const errorMessage = [
'ERROR:',
additionalInfo ? `Context: ${additionalInfo}` : '',
`Message: ${error.message}`,
'Stack trace:',
stackString
].filter(line => line).join('\n');

console.error(errorMessage);
} catch (stackError) {
// Fallback if stack trace generation fails
console.error('ERROR:', additionalInfo || '', error.message, error.stack);
}
}
76 changes: 76 additions & 0 deletions tests/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { logError, outputLog } from '../src/utils/logger';

describe('Logger', () => {
// Mock console methods
let consoleErrorSpy: jest.SpyInstance;
let consoleLogSpy: jest.SpyInstance;

beforeEach(() => {
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
});

afterEach(() => {
consoleErrorSpy.mockRestore();
consoleLogSpy.mockRestore();
});

describe('logError', () => {
it('should log error with stack trace', async () => {
const error = new Error('Test error message');

await logError(error);

expect(consoleErrorSpy).toHaveBeenCalled();
const loggedMessage = consoleErrorSpy.mock.calls[0][0];
expect(loggedMessage).toContain('ERROR:');
expect(loggedMessage).toContain('Test error message');
expect(loggedMessage).toContain('Stack trace:');
});

it('should log error with additional context', async () => {
const error = new Error('Test error');
const context = 'Error in test function';

await logError(error, context);

expect(consoleErrorSpy).toHaveBeenCalled();
const loggedMessage = consoleErrorSpy.mock.calls[0][0];
expect(loggedMessage).toContain('Context: Error in test function');
expect(loggedMessage).toContain('Test error');
});

it('should handle errors when stack trace generation fails', async () => {
const error = new Error('Test error');
error.stack = 'mock stack trace';

// This should still work even if StackTrace.fromError fails
await logError(error);

expect(consoleErrorSpy).toHaveBeenCalled();
});
});

describe('outputLog', () => {
it('should output log with stack trace', async () => {
await outputLog('Test message', 'log');

expect(consoleLogSpy).toHaveBeenCalled();
const loggedArgs = consoleLogSpy.mock.calls[0];
expect(loggedArgs[0]).toBe('LOG');
expect(loggedArgs[2]).toBe('Test message');
});

it('should handle error level', async () => {
await outputLog('Error message', 'error');

expect(consoleErrorSpy).toHaveBeenCalled();
});

it('should default to log level if invalid level provided', async () => {
await outputLog('Test message', 'invalid' as any);

expect(consoleLogSpy).toHaveBeenCalled();
});
});
});