diff --git a/packages/webgal/package.json b/packages/webgal/package.json index 892349f77..059b6ba76 100644 --- a/packages/webgal/package.json +++ b/packages/webgal/package.json @@ -12,7 +12,7 @@ "@emotion/css": "^11.11.2", "@icon-park/react": "^1.4.2", "@reduxjs/toolkit": "^1.8.1", - "angular-expressions": "^1.4.3", + "angular-expressions": "^1.5.1", "axios": "^0.30.2", "cloudlogjs": "^1.0.9", "gifuct-js": "^2.1.2", diff --git a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts index 04f5c997b..9a541dfe7 100644 --- a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts +++ b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts @@ -4,34 +4,18 @@ import { logger } from '../../util/logger'; import { IStageState } from '@/store/stageInterface'; import { restoreScene } from '../scene/restoreScene'; import { webgalStore } from '@/store/store'; -import { getValueFromStateElseKey } from '@/Core/gameScripts/setVar'; -import { strIf } from '@/Core/controller/gamePlay/strIf'; import { nextSentence } from '@/Core/controller/gamePlay/nextSentence'; import cloneDeep from 'lodash/cloneDeep'; import { ISceneEntry } from '@/Core/Modules/scene'; -import { IBacklogItem } from '@/Core/Modules/backlog'; -import { SYSTEM_CONFIG } from '@/config'; import { WebGAL } from '@/Core/WebGAL'; import { getBooleanArgByKey, getStringArgByKey } from '@/Core/util/getSentenceArg'; +import { EvaluateExpression } from '@/Core/util/evalSentenceFn'; export const whenChecker = (whenValue: string | undefined): boolean => { if (whenValue === undefined) { return true; } - // 先把变量解析出来 - const valExpArr = whenValue.split(/([+\-*\/()>=|<=|==|&&|\|\||!=)/g); - const valExp = valExpArr - .map((_e) => { - const e = _e.trim(); - if (e.match(/[a-zA-Z]/)) { - if (e.match(/^(true|false)$/)) { - return e; - } - return getValueFromStateElseKey(e, true, true); - } else return e; - }) - .reduce((pre, curr) => pre + curr, ''); - return !!strIf(valExp); + return !!EvaluateExpression(whenValue, { ErrorReturnsBoolean: true }); }; /** @@ -62,7 +46,8 @@ export const scriptExecutor = () => { if (contentExp !== null) { contentExp.forEach((e) => { - const contentVarValue = getValueFromStateElseKey(e.replace(/(? { +export const callScene = (sceneUrl: string, sceneName: string, args: Array) => { if (WebGAL.sceneManager.lockSceneWrite) { return; } @@ -35,6 +39,13 @@ export const callScene = (sceneUrl: string, sceneName: string) => { scenePrefetcher(subSceneListUniq); logger.debug('现在调用场景,调用结果:', WebGAL.sceneManager.sceneData); WebGAL.sceneManager.lockSceneWrite = false; + // 写入场景调用参数 + webgalStore.dispatch( + stageActions.addSceneArgument({ + url: sceneUrl, + value: args, + }), + ); nextSentence(); }) .catch((e) => { diff --git a/packages/webgal/src/Core/controller/scene/changeScene.ts b/packages/webgal/src/Core/controller/scene/changeScene.ts index 67df0879a..74e414d7c 100644 --- a/packages/webgal/src/Core/controller/scene/changeScene.ts +++ b/packages/webgal/src/Core/controller/scene/changeScene.ts @@ -6,13 +6,17 @@ import uniqWith from 'lodash/uniqWith'; import { scenePrefetcher } from '@/Core/util/prefetcher/scenePrefetcher'; import { WebGAL } from '@/Core/WebGAL'; +import { arg } from './sceneInterface'; +import { stageActions } from '@/store/stageReducer'; +import { webgalStore } from '@/store/store'; /** * 切换场景 * @param sceneUrl 场景路径 * @param sceneName 场景名称 + * @param args 场景参数 */ -export const changeScene = (sceneUrl: string, sceneName: string) => { +export const changeScene = (sceneUrl: string, sceneName: string, args: Array) => { if (WebGAL.sceneManager.lockSceneWrite) { return; } @@ -29,6 +33,13 @@ export const changeScene = (sceneUrl: string, sceneName: string) => { scenePrefetcher(subSceneListUniq); logger.debug('现在切换场景,切换后的结果:', WebGAL.sceneManager.sceneData); WebGAL.sceneManager.lockSceneWrite = false; + // 写入场景调用参数 + webgalStore.dispatch( + stageActions.addSceneArgument({ + url: sceneUrl, + value: args, + }), + ); nextSentence(); }) .catch((e) => { diff --git a/packages/webgal/src/Core/gameScripts/callSceneScript.ts b/packages/webgal/src/Core/gameScripts/callSceneScript.ts index 48aec5494..37fdee983 100644 --- a/packages/webgal/src/Core/gameScripts/callSceneScript.ts +++ b/packages/webgal/src/Core/gameScripts/callSceneScript.ts @@ -9,7 +9,7 @@ import { callScene } from '../controller/scene/callScene'; export const callSceneScript = (sentence: ISentence): IPerform => { const sceneNameArray: Array = sentence.content.split('/'); const sceneName = sceneNameArray[sceneNameArray.length - 1]; - callScene(sentence.content, sceneName); + callScene(sentence.content, sceneName, sentence.args); return { performName: 'none', duration: 0, diff --git a/packages/webgal/src/Core/gameScripts/changeSceneScript.ts b/packages/webgal/src/Core/gameScripts/changeSceneScript.ts index 91a2d17a7..fd9cd8d18 100644 --- a/packages/webgal/src/Core/gameScripts/changeSceneScript.ts +++ b/packages/webgal/src/Core/gameScripts/changeSceneScript.ts @@ -9,7 +9,7 @@ import { changeScene } from '../controller/scene/changeScene'; export const changeSceneScript = (sentence: ISentence): IPerform => { const sceneNameArray: Array = sentence.content.split('/'); const sceneName = sceneNameArray[sceneNameArray.length - 1]; - changeScene(sentence.content, sceneName); + changeScene(sentence.content, sceneName, sentence.args); return { performName: 'none', duration: 0, diff --git a/packages/webgal/src/Core/gameScripts/choose/index.tsx b/packages/webgal/src/Core/gameScripts/choose/index.tsx index b7813c4cc..4ac2b03f3 100644 --- a/packages/webgal/src/Core/gameScripts/choose/index.tsx +++ b/packages/webgal/src/Core/gameScripts/choose/index.tsx @@ -1,4 +1,4 @@ -import { ISentence } from '@/Core/controller/scene/sceneInterface'; +import { arg, ISentence } from '@/Core/controller/scene/sceneInterface'; import { IPerform } from '@/Core/Modules/perform/performInterface'; import { changeScene } from '@/Core/controller/scene/changeScene'; import { jmp } from '@/Core/gameScripts/label/jmp'; @@ -62,7 +62,7 @@ export const choose = (sentence: ISentence): IPerform => { // eslint-disable-next-line react/no-deprecated ReactDOM.render( - + , document.getElementById('chooseContainer'), ); @@ -80,7 +80,7 @@ export const choose = (sentence: ISentence): IPerform => { }; }; -function Choose(props: { chooseOptions: ChooseOption[] }) { +function Choose(props: { chooseOptions: ChooseOption[]; args: Array }) { const font = useFontFamily(); const { playSeEnter, playSeClick } = useSEByWebgalStore(); const applyStyle = useApplyStyle('choose'); @@ -97,7 +97,7 @@ function Choose(props: { chooseOptions: ChooseOption[] }) { ? () => { playSeClick(); if (e.jumpToScene) { - changeScene(e.jump, e.text); + changeScene(e.jump, e.text, props.args); } else { jmp(e.jump); } diff --git a/packages/webgal/src/Core/gameScripts/setVar.ts b/packages/webgal/src/Core/gameScripts/setVar.ts index 75fdfa801..5c8d57de9 100644 --- a/packages/webgal/src/Core/gameScripts/setVar.ts +++ b/packages/webgal/src/Core/gameScripts/setVar.ts @@ -3,15 +3,12 @@ import { IPerform } from '@/Core/Modules/perform/performInterface'; import { webgalStore } from '@/store/store'; import { setStageVar } from '@/store/stageReducer'; import { logger } from '@/Core/util/logger'; -import { compile } from 'angular-expressions'; import { setScriptManagedGlobalVar } from '@/store/userDataReducer'; import { ActionCreatorWithPayload } from '@reduxjs/toolkit'; import { ISetGameVar } from '@/store/stageInterface'; import { dumpToStorageFast } from '@/Core/controller/storage/storageController'; -import expression from 'angular-expressions'; -import get from 'lodash/get'; -import random from 'lodash/random'; import { getBooleanArgByKey } from '../util/getSentenceArg'; +import { EvaluateExpression } from '../util/evalSentenceFn'; /** * 设置变量 @@ -29,47 +26,14 @@ export const setVar = (sentence: ISentence): IPerform => { if (sentence.content.match(/\s*=\s*/)) { const key = sentence.content.split(/\s*=\s*/)[0]; const valExp = sentence.content.split(/\s*=\s*/)[1]; - if (/^\s*[a-zA-Z_$][\w$]*\s*\(.*\)\s*$/.test(valExp)) { - webgalStore.dispatch(targetReducerFunction({ key, value: EvaluateExpression(valExp) })); - } else if (valExp.match(/[+\-*\/()]/)) { - // 如果包含加减乘除号,则运算 - // 先取出运算表达式中的变量 - const valExpArr = valExp.split(/([+\-*\/()])/g); - // 将变量替换为变量的值,然后合成表达式字符串 - const valExp2 = valExpArr - .map((e) => { - if (!e.trim().match(/^[a-zA-Z_$][a-zA-Z0-9_.]*$/)) { - // 检查是否是变量名,不是就返回本身 - return e; - } - const _r = getValueFromStateElseKey(e.trim(), true); - return typeof _r === 'string' ? `'${_r}'` : _r; - }) - .reduce((pre, curr) => pre + curr, ''); - let result = ''; - try { - const exp = compile(valExp2); - result = exp(); - } catch (e) { - logger.error('expression compile error', e); - } - webgalStore.dispatch(targetReducerFunction({ key, value: result })); - } else if (valExp.match(/true|false/)) { - if (valExp.match(/true/)) { - webgalStore.dispatch(targetReducerFunction({ key, value: true })); - } - if (valExp.match(/false/)) { - webgalStore.dispatch(targetReducerFunction({ key, value: false })); - } - } else if (valExp.length === 0) { + if (valExp.length === 0) { webgalStore.dispatch(targetReducerFunction({ key, value: '' })); + } else if (!isNaN(Number(valExp))) { + webgalStore.dispatch(targetReducerFunction({ key, value: Number(valExp) })); } else { - if (!isNaN(Number(valExp))) { - webgalStore.dispatch(targetReducerFunction({ key, value: Number(valExp) })); - } else { - // 字符串 - webgalStore.dispatch(targetReducerFunction({ key, value: getValueFromStateElseKey(valExp, true) })); - } + webgalStore.dispatch( + targetReducerFunction({ key, value: EvaluateExpression(valExp, { InvalidValueReturns: 'origin' }) }), + ); } if (setGlobal) { logger.debug('设置全局变量:', { key, value: webgalStore.getState().userData.globalGameVar[key] }); @@ -88,55 +52,3 @@ export const setVar = (sentence: ISentence): IPerform => { stopTimeout: undefined, // 暂时不用,后面会交给自动清除 }; }; - -type BaseVal = string | number | boolean | undefined; - -/** - * 执行函数 - */ -function EvaluateExpression(val: string) { - const instance = expression.compile(val); - return instance({ - random: (...args: any[]) => { - return args.length ? random(...args) : Math.random(); - }, - }); -} - -/** - * 取不到时返回 undefined - */ -export function getValueFromState(key: string) { - let ret: any; - const stage = webgalStore.getState().stage; - const userData = webgalStore.getState().userData; - const _Merge = { stage, userData }; // 不要直接合并到一起,防止可能的键冲突 - if (stage.GameVar.hasOwnProperty(key)) { - ret = stage.GameVar[key]; - } else if (userData.globalGameVar.hasOwnProperty(key)) { - ret = userData.globalGameVar[key]; - } else if (key.startsWith('$')) { - const propertyKey = key.replace('$', ''); - ret = get(_Merge, propertyKey, undefined) as BaseVal; - } - return ret; -} - -/** - * 取不到时返回 {key} - */ -export function getValueFromStateElseKey(key: string, useKeyNameAsReturn = false, quoteString = false) { - const valueFromState = getValueFromState(key); - if (valueFromState === null || valueFromState === undefined) { - logger.warn('valueFromState result null, key = ' + key); - if (useKeyNameAsReturn) { - return key; - } - return `{${key}}`; - } - // 用 "" 包裹字符串,用于使用 compile 条件判断,处理字符串类型的变量 - if (quoteString && typeof valueFromState === 'string') { - return `"${valueFromState.replaceAll('"', '\\"')}"`; - } - return valueFromState; -} diff --git a/packages/webgal/src/Core/util/evalSentenceFn.ts b/packages/webgal/src/Core/util/evalSentenceFn.ts new file mode 100644 index 000000000..d0017ccab --- /dev/null +++ b/packages/webgal/src/Core/util/evalSentenceFn.ts @@ -0,0 +1,82 @@ +import { webgalStore } from '@/store/store'; +import { random } from 'lodash'; +import { WebGAL } from '../WebGAL'; +import expression from 'angular-expressions'; +import { logger } from '@/Core/util/logger'; + +// 是否是函数调用 +export const isFunctionCall = (valExp: string) => { + return /^\s*[a-zA-Z_$][\w$]*\s*\(.*\)\s*$/.test(valExp); +}; + +export interface EvaluateExpressionOptions { + /** + * 当是无效值 `null | undefined | 报错` 时返回原值还是返回{...}包裹 + * @default block {...}包裹 + */ + InvalidValueReturns?: 'origin' | 'block'; + /** + * 当是表达式报错时,是否返回布尔值 + */ + ErrorReturnsBoolean?: boolean; +} + +/** + * 执行运行时表达式 + */ +export const EvaluateExpression = (val: string, options: EvaluateExpressionOptions = {}) => { + const sceneUrl = WebGAL.sceneManager.sceneData.currentScene.sceneUrl; + const sceneArguments = webgalStore.getState().stage.sceneArguments; + const stage = webgalStore.getState().stage; + const userData = webgalStore.getState().userData; + const globalVars = userData.globalGameVar; + const localVars = stage.GameVar; + const _Merge = { $stage: stage, $userData: userData }; // 不要直接合并到一起,防止可能的键冲突 + try { + const instance = expression.compile(val); + const evalResult = instance({ + // 注入变量 + ...globalVars, + ...localVars, + ..._Merge, + // 随机函数 + random(...args: any[]) { + return args.length ? random(...args) : Math.random(); + }, + // 获取场景调用参数 + getArg(key: string) { + const target = sceneArguments[sceneUrl]; + if (target) { + return target.find((item) => item.key === key)?.value ?? null; + } + return null; + }, + }); + + if ((evalResult === null || evalResult === undefined) && options) { + switch (options.InvalidValueReturns) { + case 'block': + return `{${val}}`; + case 'origin': + return val; + default: + return evalResult; + } + } + + return evalResult; + } catch { + logger.warn('EvaluateExpression throw error, expr = ' + val); + if (options.ErrorReturnsBoolean) { + return false; + } + switch (options.InvalidValueReturns) { + case 'block': + return `{${val}}`; + case 'origin': + return val; + default: + return `{${val}}`; + } + } +}; diff --git a/packages/webgal/src/Stage/TextBox/TextBox.tsx b/packages/webgal/src/Stage/TextBox/TextBox.tsx index dea485b2d..0dd3e4c9c 100644 --- a/packages/webgal/src/Stage/TextBox/TextBox.tsx +++ b/packages/webgal/src/Stage/TextBox/TextBox.tsx @@ -265,7 +265,7 @@ interface Segment { } function parseString(input: string): Segment[] { - const regex = /(\[(.*?)\]\((.*?)\))|([^\[\]]+)/g; + const regex = /(\[([^\]]+)\]\(([^)]+)\))|([\s\S]+?(?=\[|$))/g; const result: Segment[] = []; let match: RegExpExecArray | null; diff --git a/packages/webgal/src/store/stageInterface.ts b/packages/webgal/src/store/stageInterface.ts index 907df19be..d0982b800 100644 --- a/packages/webgal/src/store/stageInterface.ts +++ b/packages/webgal/src/store/stageInterface.ts @@ -1,4 +1,4 @@ -import { ISentence } from '@/Core/controller/scene/sceneInterface'; +import { arg, ISentence } from '@/Core/controller/scene/sceneInterface'; import { BlinkParam, FocusParam } from '@/Core/live2DCore'; /** @@ -237,6 +237,16 @@ export interface IStageState { isDisableTextbox: boolean; replacedUIlable: Record; figureMetaData: figureMetaData; + sceneArguments: ISceneArgument; +} + +export interface ISceneArgument { + [key: string]: Array; +} + +export interface ISceneArgumentPayload { + url: string; + value: Array; } /** diff --git a/packages/webgal/src/store/stageReducer.ts b/packages/webgal/src/store/stageReducer.ts index 57e1ffe42..23871bcb6 100644 --- a/packages/webgal/src/store/stageReducer.ts +++ b/packages/webgal/src/store/stageReducer.ts @@ -13,9 +13,9 @@ import { ILive2DFocus, ILive2DMotion, IRunPerform, + ISceneArgumentPayload, ISetGameVar, ISetStagePayload, - IStageAnimationSetting, IStageState, IUpdateAnimationSettingPayload, } from '@/store/stageInterface'; @@ -74,6 +74,7 @@ export const initState: IStageState = { isDisableTextbox: false, replacedUIlable: {}, figureMetaData: {}, + sceneArguments: {}, }; /** @@ -318,6 +319,9 @@ const stageSlice = createSlice({ state.figureMetaData[action.payload[0]][action.payload[1]] = action.payload[2]; } }, + addSceneArgument: (state, action: PayloadAction) => { + state.sceneArguments[action.payload.url] = action.payload.value; + }, }, }); diff --git a/yarn.lock b/yarn.lock index 062e2199d..a9d3b1a5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1426,10 +1426,10 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -angular-expressions@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/angular-expressions/-/angular-expressions-1.4.3.tgz#24fb9e8e9e0278885dd0f7e28512ac88a74037f6" - integrity sha512-r7j+dqOuHy0OYiR5AazDixU/Us3TDN2FfuxGX4Dq6d61Y2MhBQHMdUNBfkkLPjDqVm2Is394h31gC3bcBwy9zw== +angular-expressions@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/angular-expressions/-/angular-expressions-1.5.1.tgz#32aa451c4bd4bc589efc031ed2aae9bf53d4e455" + integrity sha512-Ukcyuye0eb15zuvMFR7Kziuf7gV0gYAZXMevNI40rXF8f9k+/yk8+r5edFWRFOwnqSvAEFoP2bKNXaXgYa+Ayw== ansi-escapes@^4.3.0: version "4.3.2"