diff --git a/packages/parser/package.json b/packages/parser/package.json index 2fa43f96a..d607e329a 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -8,7 +8,8 @@ "build": "rimraf -rf ./build && rollup --config", "build-ci": "rollup --config", "debug": "tsx test/debug.ts", - "debug-scss-parser": "tsx test/debugCssParser.ts" + "debug-scss-parser": "tsx test/debugCssParser.ts", + "debug-linebreak-parser": "tsx test/debug-linebreak.ts" }, "types": "./build/types/index.d.ts", "module": "./build/es/index.js", diff --git a/packages/parser/src/index.ts b/packages/parser/src/index.ts index 9bcdd4197..030e1bc7b 100644 --- a/packages/parser/src/index.ts +++ b/packages/parser/src/index.ts @@ -8,7 +8,8 @@ import { configParser, WebgalConfig } from './configParser/configParser'; import { fileType } from './interface/assets'; import { IAsset } from './interface/sceneInterface'; import { sceneParser } from './sceneParser'; -import {IWebGALStyleObj, scss2cssinjsParser} from "./styleParser"; +import { IWebGALStyleObj, scss2cssinjsParser } from "./styleParser"; +import { sceneTextPreProcess } from "./sceneTextPreProcessor"; export default class SceneParser { private readonly SCRIPT_CONFIG_MAP: ConfigMap; @@ -76,3 +77,4 @@ export default class SceneParser { } export { ADD_NEXT_ARG_LIST, SCRIPT_CONFIG }; +export { sceneTextPreProcess }; diff --git a/packages/parser/src/sceneTextPreProcessor.ts b/packages/parser/src/sceneTextPreProcessor.ts new file mode 100644 index 000000000..244c2c91b --- /dev/null +++ b/packages/parser/src/sceneTextPreProcessor.ts @@ -0,0 +1,223 @@ +/** + * Preprocessor for scene text. + * + * Use two-pass to generate a new scene text that concats multiline sequences + * into a single line and add placeholder lines to preserve the original number + * of lines. + * + * @param sceneText The original scene text + * @returns The processed scene text + */ +export function sceneTextPreProcess(sceneText: string): string { + let lines = sceneText.replaceAll('\r', '').split('\n'); + + lines = sceneTextPreProcessPassOne(lines); + lines = sceneTextPreProcessPassTwo(lines); + + return lines.join('\n'); +} + +/** + * Pass one. + * + * Add escape character to all lines that should be multiline. + * + * @param lines The original lines + * @returns The processed lines + */ +function sceneTextPreProcessPassOne(lines: string[]): string[] { + const processedLines: string[] = []; + let lastLineIsMultiline = false; + let thisLineIsMultiline = false; + + for (const line of lines) { + thisLineIsMultiline = false; + + if (canBeMultiline(line)) { + thisLineIsMultiline = true; + } + + if (shouldNotBeMultiline(line, lastLineIsMultiline)) { + thisLineIsMultiline = false; + } + + if (thisLineIsMultiline) { + processedLines[processedLines.length - 1] += '\\'; + } + + processedLines.push(line); + + lastLineIsMultiline = thisLineIsMultiline; + } + + return processedLines; +} + +function canBeMultiline(line: string): boolean { + if (!line.startsWith(' ')) { + return false; + } + + const trimmedLine = line.trimStart(); + return trimmedLine.startsWith('|') || trimmedLine.startsWith('-'); +} + +/** + * Logic to check if a line should not be multiline. + * + * @param line The line to check + * @returns If the line should not be multiline + */ +function shouldNotBeMultiline(line: string, lastLineIsMultiline: boolean): boolean { + if (!lastLineIsMultiline && isEmptyLine(line)) { + return true; + } + + // Custom logic: if the line contains -concat, it should not be multiline + if (line.indexOf('-concat') !== -1) { + return true; + } + + return false; +} + +function isEmptyLine(line: string): boolean { + return line.trim() === ''; +} + + +/** + * Pass two. + * + * Traverse the lines to + * - remove escape characters + * - add placeholder lines to preserve the original number of lines. + * + * @param lines The lines in pass one + * @returns The processed lines + */ +function sceneTextPreProcessPassTwo(lines: string[]): string[] { + const processedLines: string[] = []; + let currentMultilineContent = ""; + let placeHolderLines: string[] = []; + + function concat(line: string) { + let trimmed = line.trim(); + if (trimmed.startsWith('-')) { + trimmed = " " + trimmed; + } + currentMultilineContent = currentMultilineContent + trimmed; + placeHolderLines.push(placeholderLine(line)); + } + + for (const line of lines) { + console.log(line); + if (line.endsWith('\\')) { + const trueLine = line.slice(0, -1); + + if (currentMultilineContent === "") { + // first line + currentMultilineContent = trueLine; + } else { + // middle line + concat(trueLine); + } + continue; + } + + if (currentMultilineContent !== "") { + // end line + concat(line); + processedLines.push(currentMultilineContent); + processedLines.push(...placeHolderLines); + + placeHolderLines = []; + currentMultilineContent = ""; + continue; + } + + processedLines.push(line); + } + + return processedLines; +} + +/** + * Placeholder Line. Adding this line preserves the original number of lines + * in the scene text, so that it can be compatible with the graphical editor. + * + * @param content The original content on this line + * @returns The placeholder line + */ +function placeholderLine(content = "") { + return ";_WEBGAL_LINE_BREAK_" + content; +} + +// export function sceneTextPreProcess(sceneText: string): string { +// const lines = sceneText.replaceAll('\r', '').split('\n'); +// const processedLines: string[] = []; +// let lastNonMultilineIndex = -1; +// let isInMultilineSequence = false; + +// function isMultiline(line: string): boolean { +// if (!line.startsWith(' ')) return false; +// const trimmedLine = line.trimStart(); +// return trimmedLine.startsWith('|') || trimmedLine.startsWith('-'); +// } + +// for (let i = 0; i < lines.length; i++) { +// const line = lines[i]; + +// if (line.trim() === '') { +// // Empty line handling +// if (isInMultilineSequence) { +// // Check if the next line is a multiline line + +// let isStillInMulti = false; +// for (let j = i + 1; j < lines.length; j++) { +// const lookForwardLine = lines[j] || ''; +// // 遇到正常语句了,直接中断 +// if (lookForwardLine.trim() !== '' && !isMultiline(lookForwardLine)) { +// isStillInMulti = false; +// break; +// } +// // 必须找到后面接的是参数,并且中间没有遇到任何正常语句才行 +// if (lookForwardLine.trim() !== '' && isMultiline(lookForwardLine)) { +// isStillInMulti = true; +// break; +// } +// } +// if (isStillInMulti) { +// // Still within a multiline sequence +// processedLines.push(';_WEBGAL_LINE_BREAK_'); +// } else { +// // End of multiline sequence +// isInMultilineSequence = false; +// processedLines.push(line); +// } +// } else { +// // Preserve empty lines outside of multiline sequences +// processedLines.push(line); +// } +// } else if (isMultiline(line)) { +// // Multiline statement handling +// if (lastNonMultilineIndex >= 0) { +// // Concatenate to the previous non-multiline statement +// const trimedLine = line.trimStart(); +// const addBlank = trimedLine.startsWith('-') ? ' ' : ''; +// processedLines[lastNonMultilineIndex] += addBlank + trimedLine; +// } + +// // Add the special comment line +// processedLines.push(';_WEBGAL_LINE_BREAK_' + line); +// isInMultilineSequence = true; +// } else { +// // Non-multiline statement handling +// processedLines.push(line); +// lastNonMultilineIndex = processedLines.length - 1; +// isInMultilineSequence = false; +// } +// } + +// return processedLines.join('\n'); +// } \ No newline at end of file diff --git a/packages/parser/test/debug-linebreak.ts b/packages/parser/test/debug-linebreak.ts new file mode 100644 index 000000000..03e32dff7 --- /dev/null +++ b/packages/parser/test/debug-linebreak.ts @@ -0,0 +1,13 @@ +import {sceneTextPreProcess} from "../src/sceneTextPreProcessor"; +import * as fsp from "fs/promises"; + + +async function debug() { + const sceneRaw = await fsp.readFile('test/test-resources/line-break.txt'); + const sceneText = sceneRaw.toString(); + const result = sceneTextPreProcess(sceneText) + console.log(result) + console.log(result.split('\n').length) +} + +debug(); diff --git a/packages/parser/test/parser.test.ts b/packages/parser/test/parser.test.ts index 5df62f23f..05161f6b4 100644 --- a/packages/parser/test/parser.test.ts +++ b/packages/parser/test/parser.test.ts @@ -1,9 +1,9 @@ import SceneParser from "../src/index"; -import {ADD_NEXT_ARG_LIST, SCRIPT_CONFIG} from "../src/config/scriptConfig"; -import {expect, test} from "vitest"; -import {commandType, ISentence} from "../src/interface/sceneInterface"; -import * as fsp from 'fs/promises' -import {fileType} from "../src/interface/assets"; +import { ADD_NEXT_ARG_LIST, SCRIPT_CONFIG } from "../src/config/scriptConfig"; +import { expect, test } from "vitest"; +import { commandType, ISentence } from "../src/interface/sceneInterface"; +import * as fsp from 'fs/promises'; +import { fileType } from "../src/interface/assets"; test("label", async () => { @@ -21,7 +21,7 @@ test("label", async () => { commandRaw: "label", content: "end", args: [ - {key: "next", value: true} + { key: "next", value: true } ], sentenceAssets: [], subScene: [] @@ -45,10 +45,10 @@ test("args", async () => { commandRaw: "changeFigure", content: "m2.png", args: [ - {key: "left", value: true}, - {key: "next", value: true} + { key: "left", value: true }, + { key: "next", value: true } ], - sentenceAssets: [{name: "m2.png", url: 'm2.png', type: fileType.figure, lineNumber: 0}], + sentenceAssets: [{ name: "m2.png", url: 'm2.png', type: fileType.figure, lineNumber: 0 }], subScene: [] }; expect(result.sentenceList).toContainEqual(expectSentenceItem); @@ -86,16 +86,16 @@ test("long-script", async () => { return fileName; }, ADD_NEXT_ARG_LIST, SCRIPT_CONFIG); - console.log('line count:', sceneText.split('\n').length) - console.time('parse-time-consumed') + console.log('line count:', sceneText.split('\n').length); + console.time('parse-time-consumed'); const result = parser.parse(sceneText, "start", "/start.txt"); - console.timeEnd('parse-time-consumed') + console.timeEnd('parse-time-consumed'); const expectSentenceItem: ISentence = { command: commandType.label, commandRaw: "label", content: "end", args: [ - {key: "next", value: true} + { key: "next", value: true } ], sentenceAssets: [], subScene: [] @@ -118,7 +118,7 @@ test("var", async () => { command: commandType.say, commandRaw: "WebGAL", content: "a=1?", - args: [{key: 'speaker', value: 'WebGAL'}, {key: 'when', value: "a==1"}], + args: [{ key: 'speaker', value: 'WebGAL' }, { key: 'when', value: "a==1" }], sentenceAssets: [], subScene: [] }; @@ -137,18 +137,18 @@ Game_key:0f86dstRf; Title_img:WebGAL_New_Enter_Image.png; Title_bgm:s_Title.mp3; Title_logos: 1.png | 2.png | Image Logo.png| -show -active=false -add=op! -count=3;This is a fake config, do not reference anything. - `) + `); expect(configFesult).toContainEqual({ command: 'Title_logos', args: ['1.png', '2.png', 'Image Logo.png'], options: [ - {key: 'show', value: true}, - {key: 'active', value: false}, - {key: 'add', value: 'op!'}, - {key: 'count', value: 3}, + { key: 'show', value: true }, + { key: 'active', value: false }, + { key: 'add', value: 'op!' }, + { key: 'count', value: 3 }, ] - }) -}) + }); +}); test("config-stringify", async () => { const parser = new SceneParser((assetList) => { @@ -162,20 +162,20 @@ Game_key:0f86dstRf; Title_img:WebGAL_New_Enter_Image.png; Title_bgm:s_Title.mp3; Title_logos: 1.png | 2.png | Image Logo.png| -show -active=false -add=op! -count=3;This is a fake config, do not reference anything. - `) + `); const stringifyResult = parser.stringifyConfig(configFesult); - const configResult2 = parser.parseConfig(stringifyResult) + const configResult2 = parser.parseConfig(stringifyResult); expect(configResult2).toContainEqual({ command: 'Title_logos', args: ['1.png', '2.png', 'Image Logo.png'], options: [ - {key: 'show', value: true}, - {key: 'active', value: false}, - {key: 'add', value: 'op!'}, - {key: 'count', value: 3}, + { key: 'show', value: true }, + { key: 'active', value: false }, + { key: 'add', value: 'op!' }, + { key: 'count', value: 3 }, ] - }) -}) + }); +}); test("say statement", async () => { @@ -184,13 +184,13 @@ test("say statement", async () => { return fileName; }, ADD_NEXT_ARG_LIST, SCRIPT_CONFIG); - const result = parser.parse(`say:123 -speaker=xx;`,'test','test') + const result = parser.parse(`say:123 -speaker=xx;`, 'test', 'test'); expect(result.sentenceList).toContainEqual({ command: commandType.say, commandRaw: "say", content: "123", - args: [{key: 'speaker', value: 'xx'}], + args: [{ key: 'speaker', value: 'xx' }], sentenceAssets: [], subScene: [] - }) -}) + }); +}); diff --git a/packages/parser/test/parserMultiline.test.ts b/packages/parser/test/parserMultiline.test.ts new file mode 100644 index 000000000..42815bcd2 --- /dev/null +++ b/packages/parser/test/parserMultiline.test.ts @@ -0,0 +1,148 @@ +import { sceneTextPreProcess } from "../src/sceneTextPreProcessor"; +import { expect, test } from "vitest"; + +test("parser-multiline-basic", async () => { + const testScene = `changeFigure:a.png -left + -next + -id=id1 + +saySomething`; + const expected = `changeFigure:a.png -left -next -id=id1 +;_WEBGAL_LINE_BREAK_ -next +;_WEBGAL_LINE_BREAK_ -id=id1 + +saySomething`; + + const preprocessedScene = sceneTextPreProcess(testScene); + expect(preprocessedScene).toEqual(expected); +}); + + +test("parser-multiline-disable-when-encounter-concat-1", async () => { + const testScene = `intro:aaa + |bbb -concat +`; + const expected = `intro:aaa + |bbb -concat +`; + + const preprocessedScene = sceneTextPreProcess(testScene); + expect(preprocessedScene).toEqual(expected); +}); + + +test("parser-multiline-disable-when-encounter-concat-2", async () => { + const testScene = `intro:aaa + |bbb + |ccc -concat +`; + const expected = `intro:aaa|bbb +;_WEBGAL_LINE_BREAK_ |bbb + |ccc -concat +`; + + const preprocessedScene = sceneTextPreProcess(testScene); + expect(preprocessedScene).toEqual(expected); +}); + +test("parser-multiline-user-force-allow-multiline-in-concat", async () => { + const testScene = String.raw`intro:aaa\ +|bbb\ +|ccc -concat +`; + const expected = `intro:aaa|bbb|ccc -concat +;_WEBGAL_LINE_BREAK_|bbb +;_WEBGAL_LINE_BREAK_|ccc -concat +`; + + const preprocessedScene = sceneTextPreProcess(testScene); + expect(preprocessedScene).toEqual(expected); +}); + +test("parser-multiline-others-same-as-before", async () => { + const testScene = `听起来是不是非常吸引人? -v4.wav; +changeFigure:none -right -next; +setAnimation:l2c -target=fig-left -next; +WebGAL 引擎也具有动画系统和特效系统,使用 WebGAL 开发的游戏可以拥有很好的表现效果。 -v5.wav; +`; + + const preprocessedScene = sceneTextPreProcess(testScene); + expect(preprocessedScene).toEqual(testScene); +}); + +test("parser-multiline-full", async () => { + const testScene = `changeFigure:a.png -left + -next + -id=id1 + +intro:aaa + |bbb|ccc + |ddd + -next; + +; WebGAL 引擎会默认读取 start.txt 作为初始场景,因此请不要删除,并在初始场景内跳转到其他场景 +bgm:s_Title.mp3; +unlockBgm:s_Title.mp3 -name=雲を追いかけて; +intro:你好 +|欢迎来到 WebGAL 的世界; +changeBg:bg.png -next; +unlockCg:bg.png -name=良夜; // 解锁CG并赋予名称 +changeFigure:stand.png -left -next; +setAnimation:enter-from-left + -target=fig-left -next; +WebGAL:欢迎使用 WebGAL!这是一款全新的网页端视觉小说引擎。 + -v1.wav; +changeFigure:stand2.png + -right -next; +WebGAL 是使用 Web 技术开发的引擎,因此在网页端有良好的表现。 -v2.wav; +由于这个特性,如果你将 WebGAL 部署到服务器或网页托管平台上,玩家只需要一串链接就可以开始游玩! -v3.wav; +setAnimation:move-front-and-back + -target=fig-left + -next; + +听起来是不是非常吸引人? -v4.wav; +changeFigure:none -right -next; +setAnimation:l2c -target=fig-left -next; +WebGAL 引擎也具有动画系统和特效系统,使用 WebGAL 开发的游戏可以拥有很好的表现效果。 + -v5.wav; +`; + + const expected = `changeFigure:a.png -left -next -id=id1 +;_WEBGAL_LINE_BREAK_ -next +;_WEBGAL_LINE_BREAK_ -id=id1 + +intro:aaa|bbb|ccc|ddd -next; +;_WEBGAL_LINE_BREAK_ |bbb|ccc +;_WEBGAL_LINE_BREAK_ |ddd +;_WEBGAL_LINE_BREAK_ -next; + +; WebGAL 引擎会默认读取 start.txt 作为初始场景,因此请不要删除,并在初始场景内跳转到其他场景 +bgm:s_Title.mp3; +unlockBgm:s_Title.mp3 -name=雲を追いかけて; +intro:你好 +|欢迎来到 WebGAL 的世界; +changeBg:bg.png -next; +unlockCg:bg.png -name=良夜; // 解锁CG并赋予名称 +changeFigure:stand.png -left -next; +setAnimation:enter-from-left -target=fig-left -next; +;_WEBGAL_LINE_BREAK_ -target=fig-left -next; +WebGAL:欢迎使用 WebGAL!这是一款全新的网页端视觉小说引擎。 -v1.wav; +;_WEBGAL_LINE_BREAK_ -v1.wav; +changeFigure:stand2.png -right -next; +;_WEBGAL_LINE_BREAK_ -right -next; +WebGAL 是使用 Web 技术开发的引擎,因此在网页端有良好的表现。 -v2.wav; +由于这个特性,如果你将 WebGAL 部署到服务器或网页托管平台上,玩家只需要一串链接就可以开始游玩! -v3.wav; +setAnimation:move-front-and-back -target=fig-left -next; +;_WEBGAL_LINE_BREAK_ -target=fig-left +;_WEBGAL_LINE_BREAK_ -next; + +听起来是不是非常吸引人? -v4.wav; +changeFigure:none -right -next; +setAnimation:l2c -target=fig-left -next; +WebGAL 引擎也具有动画系统和特效系统,使用 WebGAL 开发的游戏可以拥有很好的表现效果。 -v5.wav; +;_WEBGAL_LINE_BREAK_ -v5.wav; +`; + + const preprocessedScene = sceneTextPreProcess(testScene); + expect(preprocessedScene).toEqual(expected); +}); diff --git a/packages/parser/test/test-resources/line-break.txt b/packages/parser/test/test-resources/line-break.txt new file mode 100644 index 000000000..750e21cae --- /dev/null +++ b/packages/parser/test/test-resources/line-break.txt @@ -0,0 +1,38 @@ +changeFigure:a.png -left + -next + + + -id=id1 + +intro:aaa + |bbb|ccc + |ddd + -next; + +; WebGAL 引擎会默认读取 start.txt 作为初始场景,因此请不要删除,并在初始场景内跳转到其他场景 +bgm:s_Title.mp3; +unlockBgm:s_Title.mp3 -name=雲を追いかけて; +intro:你好 +|欢迎来到 WebGAL 的世界; +changeBg:bg.png -next; +unlockCg:bg.png -name=良夜; // 解锁CG并赋予名称 +changeFigure:stand.png -left -next; +setAnimation:enter-from-left + -target=fig-left -next; +WebGAL:欢迎使用 WebGAL!这是一款全新的网页端视觉小说引擎。 + -v1.wav; +changeFigure:stand2.png + -right -next; +WebGAL 是使用 Web 技术开发的引擎,因此在网页端有良好的表现。 -v2.wav; +由于这个特性,如果你将 WebGAL 部署到服务器或网页托管平台上,玩家只需要一串链接就可以开始游玩! -v3.wav; +setAnimation:move-front-and-back + -target=fig-left + + + -next; + +听起来是不是非常吸引人? -v4.wav; +changeFigure:none -right -next; +setAnimation:l2c -target=fig-left -next; +WebGAL 引擎也具有动画系统和特效系统,使用 WebGAL 开发的游戏可以拥有很好的表现效果。 + -v5.wav; diff --git a/packages/webgal/package.json b/packages/webgal/package.json index 52f5b5a1d..2b2f4bbc0 100644 --- a/packages/webgal/package.json +++ b/packages/webgal/package.json @@ -1,7 +1,7 @@ { "name": "webgal", "private": true, - "version": "4.5.9", + "version": "4.5.10", "scripts": { "dev": "vite --host --port 3000", "build": "cross-env NODE_ENV=production tsc && vite build --base=./", diff --git a/packages/webgal/public/game/config.txt b/packages/webgal/public/game/config.txt index 6b1274182..3bc42af83 100644 --- a/packages/webgal/public/game/config.txt +++ b/packages/webgal/public/game/config.txt @@ -3,6 +3,3 @@ Game_key:0f87dstRg; Title_img:WebGAL_New_Enter_Image.png; Title_bgm:s_Title.mp3; Game_Logo:WebGalEnter.png; -Debug:true; -GameNum:10; -GameNum_Double:20; diff --git a/packages/webgal/public/game/scene/demo_en.txt b/packages/webgal/public/game/scene/demo_en.txt index 55c8918a8..103e0d2b2 100644 --- a/packages/webgal/public/game/scene/demo_en.txt +++ b/packages/webgal/public/game/scene/demo_en.txt @@ -11,7 +11,7 @@ Welcome to WebGAL! -e002_Welcome_to_WebGAL.mp3; WebGAL is a completely new web visual engine never seen before. -e003_WebGAL_is_a_completely_new_web.mp3; changeFigure:stand2.png -right -next; It is an engine developed using web technology, so it performs well on web pages. -e004_It_is_an_engine_developedusing_web.mp3; -Thanks to this feature, once published on your website's platform,|players can simply click a link to play your game on your website anytime, anywhere! -e005_Thanks_to_this_ feature_once.mp3; +Thanks to this feature, once published on your website's platform, players can simply click a link to play your game on your website anytime, anywhere! -e005_Thanks_to_this_ feature_once.mp3; setAnimation:move-front-and-back -target=fig-left -next; Very attractive, don't you think? -e006_Very attractive.mp3; changeFigure:none -right -next; diff --git a/packages/webgal/public/game/template/template.json b/packages/webgal/public/game/template/template.json index 3194a3688..378821d2e 100644 --- a/packages/webgal/public/game/template/template.json +++ b/packages/webgal/public/game/template/template.json @@ -1,4 +1,4 @@ { "name":"Default Template", - "webgal-version":"4.5.9" + "webgal-version":"4.5.10" } diff --git a/packages/webgal/src/Core/gameScripts/intro.tsx b/packages/webgal/src/Core/gameScripts/intro.tsx index fd984ae4c..0132ba0ec 100644 --- a/packages/webgal/src/Core/gameScripts/intro.tsx +++ b/packages/webgal/src/Core/gameScripts/intro.tsx @@ -41,6 +41,7 @@ export const intro = (sentence: ISentence): IPerform => { let chosenAnimationClass = styles.fadeIn; let delayTime = 1500; let isHold = false; + let isUserForward = false; for (const e of sentence.args) { if (e.key === 'backgroundColor') { @@ -74,6 +75,14 @@ export const intro = (sentence: ISentence): IPerform => { isHold = true; } } + if (e.key === 'userForward') { + // 用户手动控制向前步进 + if (e.value === true) { + isUserForward = true; + isHold = true; // 用户手动控制向前步进,所以必须是 hold + delayTime = 99999999; // 设置一个很大的延迟,这样自然就看起来不自动继续了 + } + } } const introContainerStyle = { @@ -105,6 +114,30 @@ export const intro = (sentence: ISentence): IPerform => { if (introContainer) { const children = introContainer.childNodes[0].childNodes[0].childNodes as any; const len = children.length; + if (isUserForward) { + let isEnd = true; + for (const node of children) { + // 当前语句的延迟显示时间 + const currentDelay = Number(node.style.animationDelay.split('ms')[0]); + // 当前语句还没有显示,降低显示延迟,因为现在时间因为用户操作,相当于向前推进了 + if (currentDelay > 0) { + isEnd = false; + // 用 Animation API 操作,浏览器版本太低就无办法了 + const nodeAnimations = node.getAnimations(); + node.style.animationDelay = '0ms '; + for (const ani of nodeAnimations) { + ani.currentTime = 0; + ani.play(); + } + } + } + if (isEnd) { + clearTimeout(timeout); + clearTimeout(setBlockingStateTimeout); + WebGAL.gameplay.performController.unmountPerform(performName); + } + return; + } children.forEach((node: HTMLDivElement, index: number) => { // 当前语句的延迟显示时间 const currentDelay = Number(node.style.animationDelay.split('ms')[0]); diff --git a/packages/webgal/src/Core/gameScripts/say.ts b/packages/webgal/src/Core/gameScripts/say.ts index 95fb97cfa..27c016ff5 100644 --- a/packages/webgal/src/Core/gameScripts/say.ts +++ b/packages/webgal/src/Core/gameScripts/say.ts @@ -24,7 +24,7 @@ export const say = (sentence: ISentence): IPerform => { let dialogKey = Math.random().toString(); // 生成一个随机的key let dialogToShow = sentence.content; // 获取对话内容 if (dialogToShow) { - dialogToShow = String(dialogToShow).replace(/ /g, '\u00a0'); // 替换空格 + dialogToShow = String(dialogToShow).replace(/ {2,}/g, (match) => '\u00a0'.repeat(match.length)); // 替换连续两个或更多空格 } const isConcat = getSentenceArgByKey(sentence, 'concat'); // 是否是继承语句 const isNotend = getSentenceArgByKey(sentence, 'notend') as boolean; // 是否有 notend 参数 diff --git a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts index 333d9f7b9..3d6dad14c 100644 --- a/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts +++ b/packages/webgal/src/Core/util/coreInitialFunction/infoFetcher.ts @@ -44,7 +44,7 @@ export const infoFetcher = (url: string) => { } else { let res: any = args[0].trim(); if (/^(true|false)$/g.test(args[0])) { - res = !!res; + res = res === 'true'; } else if (/^[0-9]+\.?[0-9]+$/g.test(args[0])) { res = Number(res); } diff --git a/packages/webgal/src/Stage/TextBox/TextBox.tsx b/packages/webgal/src/Stage/TextBox/TextBox.tsx index 9e33b072d..9e28cac09 100644 --- a/packages/webgal/src/Stage/TextBox/TextBox.tsx +++ b/packages/webgal/src/Stage/TextBox/TextBox.tsx @@ -101,7 +101,13 @@ function isCJK(character: string) { return !!character.match(/[\u4e00-\u9fa5]|[\u0800-\u4e00]|[\uac00-\ud7ff]/); } -export function compileSentence(sentence: string, lineLimit: number, ignoreLineLimit?: boolean): EnhancedNode[][] { +// eslint-disable-next-line max-params +export function compileSentence( + sentence: string, + lineLimit: number, + ignoreLineLimit?: boolean, + replace_space_with_nbsp = true, +): EnhancedNode[][] { // 先拆行 const lines = sentence.split(/(? useEscape(val)); // 对每一行进行注音处理 @@ -111,7 +117,7 @@ export function compileSentence(sentence: string, lineLimit: number, ignoreLineL line.forEach((node, index) => { match(node.type) .with(SegmentType.String, () => { - const chars = splitChars(node.value as string); + const chars = splitChars(node.value as string, replace_space_with_nbsp); // eslint-disable-next-line max-nested-callbacks ln.push(...chars.map((c) => ({ reactNode: c }))); }) @@ -135,8 +141,9 @@ export function compileSentence(sentence: string, lineLimit: number, ignoreLineL /** * @param sentence + * @param replace_space_with_nbsp */ -export function splitChars(sentence: string) { +export function splitChars(sentence: string, replace_space_with_nbsp = true) { if (!sentence) return ['']; const words: string[] = []; let word = ''; @@ -157,13 +164,15 @@ export function splitChars(sentence: string) { // cjkFlag = false; // continue; // } - if (character === ' ') { + if (character === ' ' || character === '\u00a0') { // Space if (word) { words.push(word); word = ''; } - words.push('\u00a0'); + if (replace_space_with_nbsp) { + words.push('\u00a0'); + } else words.push(character); cjkFlag = false; } else if (isCJK(character) && !isPunctuation(character)) { if (!cjkFlag && word) { diff --git a/packages/webgal/src/UI/Backlog/Backlog.tsx b/packages/webgal/src/UI/Backlog/Backlog.tsx index c79093eaa..86d00e657 100644 --- a/packages/webgal/src/UI/Backlog/Backlog.tsx +++ b/packages/webgal/src/UI/Backlog/Backlog.tsx @@ -7,7 +7,7 @@ import { setVisibility } from '@/store/GUIReducer'; import { logger } from '@/Core/util/logger'; import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import useTrans from '@/hooks/useTrans'; -import { compileSentence, EnhancedNode, splitChars } from '@/Stage/TextBox/TextBox'; +import { compileSentence, EnhancedNode } from '@/Stage/TextBox/TextBox'; import useSoundEffect from '@/hooks/useSoundEffect'; import { WebGAL } from '@/Core/WebGAL'; @@ -27,7 +27,7 @@ export const Backlog = () => { // logger.info('backlogList render'); for (let i = 0; i < WebGAL.backlogManager.getBacklog().length; i++) { const backlogItem = WebGAL.backlogManager.getBacklog()[i]; - const showTextArray = compileSentence(backlogItem.currentStageState.showText, 3, true); + const showTextArray = compileSentence(backlogItem.currentStageState.showText, 3, true, false); const showTextArray2 = showTextArray.map((line) => { return line.map((c) => { return c.reactNode; @@ -48,13 +48,13 @@ export const Backlog = () => { ); }); const showNameArray = compileSentence(backlogItem.currentStageState.showName, 3, true); - const showNameArray2 = showNameArray.map((line)=>{ + const showNameArray2 = showNameArray.map((line) => { return line.map((c) => { - return c.reactNode; + return c.reactNode; }); }); const showNameArrayReduced = mergeStringsAndKeepObjects(showNameArray2); - const nameElementList = showNameArrayReduced.map((line,index)=>{ + const nameElementList = showNameArrayReduced.map((line, index) => { return (
{line.map((e, index) => { diff --git a/packages/webgal/src/UI/PanicOverlay/PanicOverlay.tsx b/packages/webgal/src/UI/PanicOverlay/PanicOverlay.tsx index 6b21774e3..59ea5f08f 100644 --- a/packages/webgal/src/UI/PanicOverlay/PanicOverlay.tsx +++ b/packages/webgal/src/UI/PanicOverlay/PanicOverlay.tsx @@ -8,9 +8,13 @@ import { PanicYoozle } from '@/UI/PanicOverlay/PanicYoozle/PanicYoozle'; export const PanicOverlay = () => { const GUIStore = useSelector((state: RootState) => state.GUI); const [showOverlay, setShowOverlay] = useState(false); + const globalVars = useSelector((state: RootState) => state.userData.globalGameVar); + const panic = globalVars['Show_panic']; + const hidePanic = panic === false; useEffect(() => { - setShowOverlay(GUIStore.showPanicOverlay); - }, [GUIStore.showPanicOverlay]); + const isShowOverlay = GUIStore.showPanicOverlay && !hidePanic; + setShowOverlay(isShowOverlay); + }, [GUIStore.showPanicOverlay, hidePanic]); return ReactDOM.createPortal(
{showOverlay && }
, document.querySelector('div#panic-overlay')!, diff --git a/packages/webgal/src/config/info.ts b/packages/webgal/src/config/info.ts index 8fd76da0f..2eab51c41 100644 --- a/packages/webgal/src/config/info.ts +++ b/packages/webgal/src/config/info.ts @@ -1,5 +1,5 @@ export const __INFO = { - version: 'WebGAL 4.5.9', + version: 'WebGAL 4.5.10', contributors: [ // 现在改为跳转到 GitHub 了 ], diff --git a/releasenote.md b/releasenote.md index 28f366fa2..16fe31c3e 100644 --- a/releasenote.md +++ b/releasenote.md @@ -8,15 +8,15 @@ #### 新功能 -对话内容支持不间断的连续空格 +支持 intro, say 以及参数的多行语法书写方式 -#### 修复 +支持通过配置文件控制是否要启用紧急回避界面 -读取存档时意外在状态表中存储了多份演出记录的问题 +intro 支持关闭自动展示下一句,只有用户手动点击鼠标或按下键盘时才展示下一句 -带有 id 的效果音播放在停止后演出未完全清除的问题 +#### 修复 -对状态表和演出控制器中的演出列表在插入时去重 +英语对话渐显和布局问题 ## Release Notes @@ -29,15 +29,16 @@ #### New Features -Dialogue content now supports continuous spaces. +Supports multi-line syntax for intro, say, and parameters. -#### Fixes +Supports enabling/disabling the emergency skip interface via configuration file. -Fixed an issue where multiple performance records were unexpectedly stored in the state table when loading a save. +Intro supports disabling automatic display of the next sentence; only displays the next sentence when the user manually clicks the mouse or presses a key. -Fixed an issue where performances with IDs were not completely cleared after stopping sound effects playback. -Deduplicated performance lists in the state table and performance controller upon insertion. +#### Fixes + +English dialogue fade-in and layout issues. ## リリースノート @@ -50,13 +51,13 @@ Deduplicated performance lists in the state table and performance controller upo #### 新機能 -会話内容で連続するスペースが正しく表示されるようになりました。 +intro、say、およびパラメータの複数行構文をサポート -#### 修正 +設定ファイルで緊急回避インターフェースを有効にするかどうかを制御可能に -セーブデータ読み込み時に、ステータステーブルに複数の演出記録が重複して保存される問題を修正しました。 +intro で自動的に次の文を表示するのを無効化し、ユーザーがマウスをクリックまたはキーボードを押したときにのみ次の文を表示するように変更 -IDを持つ効果音が停止した後、演出が完全にクリアされない問題を修正しました。 +#### 修正 -ステータステーブルと演出コントローラーの演出リストにおいて、重複した項目が挿入されるのを防ぐように修正しました。 +英語の会話のフェードインとレイアウトの問題