117 lines
3.6 KiB
TypeScript
117 lines
3.6 KiB
TypeScript
import {createEffect, createResource } from "solid-js";
|
|
import type {OptionsResult, RuntimeResult} from "../../yarn-spinner/runtime/results";
|
|
import {compile, parseYarn, YarnRunner} from "../../yarn-spinner";
|
|
import {loadElementSrc, resolvePath} from "../utils/path";
|
|
import {createStore} from "solid-js/store";
|
|
import {RunnerOptions} from "../../yarn-spinner/runtime/runner";
|
|
import {getTtrpgFunctions} from "./ttrpgRunner";
|
|
import { getIndexedData } from "../../data-loader/file-index";
|
|
|
|
type YarnSpinnerStore = {
|
|
dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[],
|
|
currentOptions: OptionsResult | null,
|
|
isEnded: boolean,
|
|
runnerInstance: YarnRunner | null,
|
|
}
|
|
|
|
export function createYarnStore(element: HTMLElement, props: {start: string}){
|
|
const [store, setStore] = createStore<YarnSpinnerStore>({
|
|
dialogueHistory: [],
|
|
currentOptions: null,
|
|
isEnded: false,
|
|
runnerInstance: null
|
|
});
|
|
|
|
// 获取文件路径
|
|
const {articlePath, rawSrc} = loadElementSrc(element);
|
|
const yarnPaths = (rawSrc || '').split(',')
|
|
.map((s: string) => resolvePath(articlePath, s.trim()))
|
|
.filter(Boolean);
|
|
|
|
// 加载 yarn 内容
|
|
const [yarnContent] = createResource(() => yarnPaths, loadYarnFiles);
|
|
|
|
// 创建 runner
|
|
const createRunner = () => {
|
|
const content = yarnContent();
|
|
if (!content) return null;
|
|
|
|
try {
|
|
const ast = parseYarn(content);
|
|
const program = compile(ast);
|
|
|
|
const runner = new YarnRunner(program, {
|
|
startAt: props.start,
|
|
});
|
|
const {commands, functions} = getTtrpgFunctions(runner);
|
|
runner.registerCommands(commands);
|
|
runner.registerFunctions(functions);
|
|
return runner;
|
|
} catch (error) {
|
|
console.error('Failed to initialize YarnRunner:', error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
function advance(index?: number){
|
|
const runner = store.runnerInstance;
|
|
if(!runner) return;
|
|
if(index === undefined && runner.currentResult?.isDialogueEnd) return;
|
|
if(runner.currentResult?.type === 'options'){
|
|
if(index === undefined)return;
|
|
const option = runner.currentResult.options[index];
|
|
setStore('dialogueHistory', [...store.dialogueHistory, option]);
|
|
}
|
|
runner.advance(index);
|
|
processRunnerOutput();
|
|
}
|
|
|
|
createEffect(() => {
|
|
if(!yarnContent()) return;
|
|
setStore('runnerInstance', createRunner());
|
|
requestAnimationFrame(function(){
|
|
advance();
|
|
});
|
|
});
|
|
|
|
// 处理 runner 输出
|
|
const processRunnerOutput = () => {
|
|
const runner = store.runnerInstance;
|
|
if(!runner)return;
|
|
|
|
const result = runner.currentResult;
|
|
if (!result) return;
|
|
|
|
if(result.type === 'options'){
|
|
setStore('currentOptions', result);
|
|
}else{
|
|
setStore('currentOptions', null);
|
|
}
|
|
|
|
setStore('dialogueHistory', [...store.dialogueHistory, result]);
|
|
};
|
|
|
|
// 重新开始
|
|
const restart = () => {
|
|
setStore({
|
|
dialogueHistory: [],
|
|
currentOptions: null,
|
|
isEnded: false,
|
|
runnerInstance: createRunner(),
|
|
});
|
|
processRunnerOutput();
|
|
};
|
|
|
|
return {
|
|
store,
|
|
advance,
|
|
restart,
|
|
}
|
|
}
|
|
|
|
// 加载多个 yarn 文件并拼接
|
|
async function loadYarnFiles(paths: string[]): Promise<string> {
|
|
const contents = await Promise.all(paths.map(path => getIndexedData(path)));
|
|
return contents.join('\n');
|
|
}
|