diff --git a/packages/parser/package.json b/packages/parser/package.json index 018828993..e14524223 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "webgal-parser", - "version": "4.4.8", + "version": "4.4.9-fix1", "description": "WebGAL script parser", "scripts": { "test": "vitest", @@ -11,7 +11,7 @@ }, "types": "./build/types/index.d.ts", "module": "./build/es/index.js", - "main": "./build/cjs/index.js", + "main": "./build/cjs/index.cjs", "author": "Mahiru ", "license": "MPL-2.0", "dependencies": { diff --git a/packages/parser/rollup.config.js b/packages/parser/rollup.config.js index 381d91bd4..787b18bc7 100644 --- a/packages/parser/rollup.config.js +++ b/packages/parser/rollup.config.js @@ -28,7 +28,7 @@ export default [ input: `./src/index.ts`, output: [ { - file: "./build/cjs/index.js", + file: "./build/cjs/index.cjs", exports: "named", format: "cjs", sourcemap: !isProd diff --git a/packages/webgal/package.json b/packages/webgal/package.json index efab50860..e7115d37a 100644 --- a/packages/webgal/package.json +++ b/packages/webgal/package.json @@ -1,7 +1,7 @@ { "name": "webgal", "private": true, - "version": "4.4.9", + "version": "4.4.10", "scripts": { "dev": "vite --host --port 3000", "build": "cross-env NODE_ENV=production tsc && vite build --base=./", @@ -14,14 +14,15 @@ "angular-expressions": "^1.1.5", "axios": "^0.26.1", "cloudlogjs": "^1.0.9", - "gsap": "^3.11.3", "i18next": "^22.4.15", "localforage": "^1.10.0", "lodash": "^4.17.21", "mitt": "^3.0.0", "modern-css-reset": "^1.4.0", "pixi-filters": "^4.2.0", + "pixi-live2d-display": "^0.4.0", "pixi.js": "^6.3.0", + "popmotion": "^11.0.5", "react": "^17.0.2", "react-dom": "^17.0.2", "react-i18next": "^12.2.2", @@ -29,7 +30,6 @@ "sass": "^1.49.9", "uuid": "^9.0.0", "vite-plugin-package-version": "^1.0.2", - "pixi-live2d-display": "^0.4.0", "webgal-parser": "latest" }, "devDependencies": { diff --git a/packages/webgal/public/game/animation/animationTable.json b/packages/webgal/public/game/animation/animationTable.json index 147ef6a53..f4ed953b6 100644 --- a/packages/webgal/public/game/animation/animationTable.json +++ b/packages/webgal/public/game/animation/animationTable.json @@ -1 +1,19 @@ -["enter-from-left","enter-from-bottom","enter-from-right","shake","move-front-and-back","enter","exit","blur","oldFilm","dotFilm","reflectionFilm","glitchFilm","rgbFilm","godrayFilm","removeFilm"] +[ + "enter-from-left", + "enter-from-bottom", + "enter-from-right", + "shake", + "move-front-and-back", + "enter", + "exit", + "blur", + "oldFilm", + "dotFilm", + "reflectionFilm", + "glitchFilm", + "rgbFilm", + "godrayFilm", + "removeFilm", + "shockwaveIn", + "shockwaveOut" +] diff --git a/packages/webgal/public/game/animation/shockwaveIn.json b/packages/webgal/public/game/animation/shockwaveIn.json new file mode 100644 index 000000000..ba99326df --- /dev/null +++ b/packages/webgal/public/game/animation/shockwaveIn.json @@ -0,0 +1,12 @@ +[ + { + "shockwaveFilter": 0, + "alpha": 0, + "duration": 0 + }, + { + "shockwaveFilter": 3, + "alpha": 1, + "duration": 2000 + } +] diff --git a/packages/webgal/public/game/animation/shockwaveOut.json b/packages/webgal/public/game/animation/shockwaveOut.json new file mode 100644 index 000000000..5b0509ea9 --- /dev/null +++ b/packages/webgal/public/game/animation/shockwaveOut.json @@ -0,0 +1,12 @@ +[ + { + "shockwaveFilter": 0, + "alpha": 1, + "duration": 0 + }, + { + "shockwaveFilter": 3, + "alpha": 0, + "duration": 2000 + } +] diff --git a/packages/webgal/public/game/bgm/ontama_piano1_youkoso.mp3 b/packages/webgal/public/game/bgm/ontama_piano1_youkoso.mp3 deleted file mode 100644 index c4716e304..000000000 Binary files a/packages/webgal/public/game/bgm/ontama_piano1_youkoso.mp3 and /dev/null differ diff --git a/packages/webgal/public/game/scene/demo_animation.txt b/packages/webgal/public/game/scene/demo_animation.txt index 9a87f252c..9cd74655a 100644 --- a/packages/webgal/public/game/scene/demo_animation.txt +++ b/packages/webgal/public/game/scene/demo_animation.txt @@ -1,5 +1,5 @@ -bgm:ontama_piano1_youkoso.mp3 -volume=80 -enter=3000; -unlockBgm:ontama_piano1_youkoso.mp3 -name=ようこそ; +bgm:s_Title.mp3 -volume=80 -enter=3000; +unlockBgm:s_Title.mp3 -name=ようこそ; intro:■初めに| このデモゲームで使用している画像素材は、AI生成画像です; intro:■クレジット| 「VOICEVOX:小夜/SAYO」; intro:まばたき、口パクのアニメーションテストです; diff --git a/packages/webgal/public/game/scene/demo_en.txt b/packages/webgal/public/game/scene/demo_en.txt index 897a50aa8..55c8918a8 100644 --- a/packages/webgal/public/game/scene/demo_en.txt +++ b/packages/webgal/public/game/scene/demo_en.txt @@ -1,5 +1,5 @@ -bgm:ontama_piano1_youkoso.mp3 -volume=80 -enter=3000; -unlockBgm:ontama_piano1_youkoso.mp3 -name=welcome; +bgm:s_Title.mp3 -volume=80 -enter=3000; +unlockBgm:s_Title.mp3 -name=welcome; intro:*Getting started| The image materials used in this demo game are AI-generated images; intro:*Credit| Created By ondoku3.com; changeBg:bg.png -next; @@ -49,4 +49,4 @@ For developers who are developing games for the first time, we provide beautiful So you can start making games quickly. -e023_So_you_can_ start_making_ games_quickly.mp3; We hope that your work will be exhibited at WebGAL. -e024_We_hope_ that_your_work.mp3; Thank you for your interest in the WebGAL project! -e025_Thank_you_ for_your_interest_in_the_WebGAL_project!.mp3; -end; \ No newline at end of file +end; diff --git a/packages/webgal/public/game/scene/demo_ja.txt b/packages/webgal/public/game/scene/demo_ja.txt index 0bf5cdc7e..6cf5aad05 100644 --- a/packages/webgal/public/game/scene/demo_ja.txt +++ b/packages/webgal/public/game/scene/demo_ja.txt @@ -1,5 +1,5 @@ -bgm:ontama_piano1_youkoso.mp3 -volume=80 -enter=3000; -unlockBgm:ontama_piano1_youkoso.mp3 -name=ようこそ; +bgm:s_Title.mp3 -volume=80 -enter=3000; +unlockBgm:s_Title.mp3 -name=ようこそ; intro:■初めに| このデモゲームで使用している画像素材は、AI生成画像です; intro:■クレジット| 「VOICEVOX:小夜/SAYO」; changeBg:bg.png -next; @@ -61,4 +61,4 @@ setTempAnimation:[{"position": {"x": 500,"y": 0},"duration": 0},{"position": {"x WebGALプロジェクトチームは、あなたの作品がWebGALで展示されることを期待しています! -030_小夜SAYO(ノーマル)_ウェブギャルプロジ….wav リンクを1つ用意するだけで、無数のユーザーがあなたの作品をすぐに楽しめるようになります。 -031_小夜SAYO(ノーマル)_リンクを1つ用意す….wav; WebGALプロジェクトへのご注目、ありがとうございます! -032_小夜SAYO(ノーマル)_ウェブギャルプロジ….wav; -end; \ No newline at end of file +end; diff --git a/packages/webgal/public/game/scene/demo_zh_cn.txt b/packages/webgal/public/game/scene/demo_zh_cn.txt index 352224bd5..cac93a536 100644 --- a/packages/webgal/public/game/scene/demo_zh_cn.txt +++ b/packages/webgal/public/game/scene/demo_zh_cn.txt @@ -1,10 +1,13 @@ bgm:s_Title.mp3 -volume=80 -enter=3000; unlockBgm:s_Title.mp3 -name=雲を追いかけて; intro:你好|欢迎来到 {egine} 的世界; +changeBg:WebGalEnter.png -next; +setTransition: -target=bg-main -exit=shockwaveOut; +:你好|欢迎来到 {egine} 的世界; changeBg:bg.png -next; +setTransition: -target=bg-main -enter=shockwaveIn -next; unlockCg:bg.png -name=良い夜; // 解锁CG并赋予名称 changeFigure:stand.png -left -enter=enter-from-left -next; -:你好|欢迎来到 {egine} 的世界; miniAvatar:miniavatar.png; {heroine}:欢迎使用 {egine}!这是一款全新的网页端视觉小说引擎。 -v1.wav; changeFigure:stand2.png -right -next; diff --git a/packages/webgal/public/game/tex/rain_2.png b/packages/webgal/public/game/tex/rain_2.png deleted file mode 100644 index caaa156a3..000000000 Binary files a/packages/webgal/public/game/tex/rain_2.png and /dev/null differ diff --git a/packages/webgal/public/game/tex/rain_min.png b/packages/webgal/public/game/tex/rain_min.png deleted file mode 100644 index 4cb0d4242..000000000 Binary files a/packages/webgal/public/game/tex/rain_min.png and /dev/null differ diff --git a/packages/webgal/src/Core/Modules/animationFunctions.ts b/packages/webgal/src/Core/Modules/animationFunctions.ts index ab25fe7bd..4c77e6e2b 100644 --- a/packages/webgal/src/Core/Modules/animationFunctions.ts +++ b/packages/webgal/src/Core/Modules/animationFunctions.ts @@ -14,7 +14,7 @@ export function getAnimationObject(animationName: string, target: string, durati const targetSetEffect = webgalStore.getState().stage.effects.find((e) => e.target === target); const newEffect = cloneDeep({ ...(targetSetEffect?.transform ?? baseTransform), duration: 0 }); Object.assign(newEffect, effect); - newEffect.duration = effect.duration / 1000; + newEffect.duration = effect.duration; return newEffect; }); logger.debug('装载自定义动画', mappedEffects); diff --git a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts index 961b31aed..3dc811366 100644 --- a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts +++ b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts @@ -13,6 +13,25 @@ import { IBacklogItem } from '@/Core/Modules/backlog'; import { SYSTEM_CONFIG } from '@/config'; import { WebGAL } from '@/Core/WebGAL'; +export const whenChecker = (whenValue: string | undefined): boolean => { + if (whenValue === undefined) { + return true; + } + // 先把变量解析出来 + const valExpArr = whenValue.split(/([+\-*\/()>=|<=|==)/g); + const valExp = valExpArr + .map((e) => { + if (e.match(/[a-zA-Z]/)) { + if (e.match(/true/) || e.match(/false/)) { + return e; + } + return getValueFromState(e).toString(); + } else return e; + }) + .reduce((pre, curr) => pre + curr, ''); + return !!strIf(valExp); +}; + /** * 语句执行器 * 执行语句,同步场景状态,并根据情况立即执行下一句或者加入backlog @@ -64,7 +83,7 @@ export const scriptExecutor = () => { variableInterpolation(); // 判断这个脚本要不要执行 - let runThis: number | boolean = true; + let runThis = true; let isHasWhenArg = false; let whenValue = ''; currentScript.args.forEach((e) => { @@ -75,19 +94,7 @@ export const scriptExecutor = () => { }); // 如果语句有 when if (isHasWhenArg) { - // 先把变量解析出来 - const valExpArr = whenValue.split(/([+\-*\/()>=|<=|==)/g); - const valExp = valExpArr - .map((e) => { - if (e.match(/[a-zA-Z]/)) { - if (e.match(/true/) || e.match(/false/)) { - return e; - } - return getValueFromState(e).toString(); - } else return e; - }) - .reduce((pre, curr) => pre + curr, ''); - runThis = strIf(valExp); + runThis = whenChecker(whenValue); } // 执行语句 diff --git a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts index 05d9f3128..30c5bf8e2 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts @@ -426,9 +426,8 @@ export default class PixiStage { /** * 加载器部分 */ - const resourses = Object.keys(loader.resources); this.cacheGC(); - if (!resourses.includes(url)) { + if (!loader.resources?.[url]?.texture) { this.loadAsset(url, setup); } else { // 复用 @@ -504,9 +503,8 @@ export default class PixiStage { /** * 加载器部分 */ - const resourses = Object.keys(loader.resources); this.cacheGC(); - if (!resourses.includes(url)) { + if (!loader.resources?.[url]?.texture) { this.loadAsset(url, setup); } else { // 复用 diff --git a/packages/webgal/src/Core/controller/stage/pixi/WebGALPixiContainer.ts b/packages/webgal/src/Core/controller/stage/pixi/WebGALPixiContainer.ts index 889804fd6..bd1b11bf7 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/WebGALPixiContainer.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/WebGALPixiContainer.ts @@ -5,17 +5,45 @@ import { GlitchFilter } from '@pixi/filter-glitch'; import { RGBSplitFilter } from '@pixi/filter-rgb-split'; import { GodrayFilter } from '@pixi/filter-godray'; import * as PIXI from 'pixi.js'; +import { + getOrCreateShockwaveFilterImpl, + getShockwaveFilter, + setShockwaveFilter, +} from '@/Core/controller/stage/pixi/filters/ShockwaveFilter'; +import { + getOrCreateRadiusAlphaFilterImpl, + getRadiusAlphaFilter, + setRadiusAlphaFilter, +} from '@/Core/controller/stage/pixi/shaders/RadiusAlphaFilter'; export class WebGALPixiContainer extends PIXI.Container { + public containerFilters = new Map(); private baseX = 0; private baseY = 0; - private containerFilters = new Map(); - public constructor() { super(); } + public addFilter(filter: PIXI.Filter) { + if (this.filters) { + this.filters.push(filter); + } else { + this.filters = [filter]; + } + } + + public removeFilter(name: string) { + const filter = this.containerFilters.get(name); + if (filter) { + const index = (this?.filters ?? []).findIndex((e) => e === filter); + if (this.filters) { + this.filters.splice(index, 1); + this.containerFilters.delete(name); + } + } + } + public get blur(): number { // @ts-ignore return this.getOrCreateBlurFilter().blur as number; @@ -62,7 +90,7 @@ export class WebGALPixiContainer extends PIXI.Container { this.y = originalY; } - private getOrCreateBlurFilter() { + public getOrCreateBlurFilter() { const blurFilterFromMap = this.containerFilters.get('blur'); if (blurFilterFromMap) { return blurFilterFromMap; @@ -78,9 +106,9 @@ export class WebGALPixiContainer extends PIXI.Container { /** * old film filter - * @private + * @public */ - private getOrCreateOldFilmFilter(createMode = true) { + public getOrCreateOldFilmFilter(createMode = true) { const blurFilterFromMap = this.containerFilters.get('oldFilm'); if (blurFilterFromMap) { return blurFilterFromMap; @@ -109,9 +137,9 @@ export class WebGALPixiContainer extends PIXI.Container { /** * dot film filter - * @private + * @public */ - private getOrCreateDotFilter(createMode = true) { + public getOrCreateDotFilter(createMode = true) { const blurFilterFromMap = this.containerFilters.get('dotFilm'); if (blurFilterFromMap) { return blurFilterFromMap; @@ -140,9 +168,9 @@ export class WebGALPixiContainer extends PIXI.Container { /** * reflection film filter - * @private + * @public */ - private getOrCreateReflectionFilter(createMode = true) { + public getOrCreateReflectionFilter(createMode = true) { const blurFilterFromMap = this.containerFilters.get('reflectionFilm'); if (blurFilterFromMap) { return blurFilterFromMap; @@ -171,9 +199,9 @@ export class WebGALPixiContainer extends PIXI.Container { /** * glitchFilter film filter - * @private + * @public */ - private getOrCreateGlitchFilter(createMode = true) { + public getOrCreateGlitchFilter(createMode = true) { const blurFilterFromMap = this.containerFilters.get('glitchFilm'); if (blurFilterFromMap) { return blurFilterFromMap; @@ -202,9 +230,9 @@ export class WebGALPixiContainer extends PIXI.Container { /** * rgbSplitFilter film filter - * @private + * @public */ - private getOrCreateRGBSplitFilter(createMode = true) { + public getOrCreateRGBSplitFilter(createMode = true) { const blurFilterFromMap = this.containerFilters.get('rgbFilm'); if (blurFilterFromMap) { return blurFilterFromMap; @@ -233,9 +261,9 @@ export class WebGALPixiContainer extends PIXI.Container { /** * godrayFilter film filter - * @private + * @public */ - private getOrCreateGodrayFilter(createMode = true) { + public getOrCreateGodrayFilter(createMode = true) { const blurFilterFromMap = this.containerFilters.get('godrayFilm'); if (blurFilterFromMap) { return blurFilterFromMap; @@ -262,22 +290,31 @@ export class WebGALPixiContainer extends PIXI.Container { } else this.getOrCreateGodrayFilter(); } - private addFilter(filter: PIXI.Filter) { - if (this.filters) { - this.filters.push(filter); - } else { - this.filters = [filter]; - } + /** + * ShockwaveFilter + */ + + public getOrCreateShockwaveFilter(createMode = true) { + return getOrCreateShockwaveFilterImpl(this, createMode); + } + public get shockwaveFilter(): number { + return getShockwaveFilter(this); + } + public set shockwaveFilter(value: number) { + setShockwaveFilter(this, value); } - private removeFilter(name: string) { - const filter = this.containerFilters.get(name); - if (filter) { - const index = (this?.filters ?? []).findIndex((e) => e === filter); - if (this.filters) { - this.filters.splice(index, 1); - this.containerFilters.delete(name); - } - } + /** + * RadiusAlphaFilter + */ + + public getOrCreateRadiusAlphaFilter(createMode = true) { + return getOrCreateRadiusAlphaFilterImpl(this, createMode); + } + public get radiusAlphaFilter(): number { + return getRadiusAlphaFilter(this); + } + public set radiusAlphaFilter(value: number) { + setRadiusAlphaFilter(this, value); } } diff --git a/packages/webgal/src/Core/controller/stage/pixi/animations/timeline.ts b/packages/webgal/src/Core/controller/stage/pixi/animations/timeline.ts index ae7ea8388..217417079 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/timeline.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/timeline.ts @@ -1,5 +1,5 @@ import { ITransform } from '@/store/stageInterface'; -import { gsap } from 'gsap'; +import { animate } from 'popmotion'; import { WebGAL } from '@/Core/WebGAL'; import { webgalStore } from '@/store/store'; import { stageActions } from '@/store/stageReducer'; @@ -16,38 +16,37 @@ export function generateTimelineObj( duration: number, ) { const target = WebGAL.gameplay.pixiStage!.getStageObjByKey(targetKey); - let gsapTimeline1: gsap.core.Timeline | null = gsap.timeline(); - let gsapTimeline2: gsap.core.Timeline | null = gsap.timeline(); - let gsapTimeline3: gsap.core.Timeline | null = gsap.timeline(); - let gsapTimeline4: gsap.core.Timeline | null = gsap.timeline(); - let gsapTimeline5: gsap.core.Timeline | null = gsap.timeline(); - const gsapTimelines = [gsapTimeline1, gsapTimeline2, gsapTimeline3, gsapTimeline4, gsapTimeline5]; - for (const gsapEffect of timeline) { - const gsapEffectDuration = gsapEffect.duration; - if (target?.pixiContainer) { - gsapTimeline1.to(target.pixiContainer, { - alpha: gsapEffect.alpha, - rotation: gsapEffect.rotation, - blur: gsapEffect.blur, - duration: gsapEffectDuration, - }); - gsapTimeline2.to(target.pixiContainer.scale, { - ...gsapEffect.scale, - duration: gsapEffectDuration, - }); - gsapTimeline3.to(target.pixiContainer, { - ...gsapEffect.position, - duration: gsapEffectDuration, - }); - /** - * filters - */ - const { alpha, rotation, blur, duration, scale, position, ...rest } = gsapEffect; - gsapTimeline4.to(target.pixiContainer, { - ...rest, - duration: gsapEffectDuration, - }); - } + let currentDelay = 0; + const values = []; + const times: number[] = []; + for (const segment of timeline) { + const segmentDuration = segment.duration; + currentDelay += segmentDuration; + const { position, scale, ...segmentValues } = segment; + // 不能用 scale,因为 popmotion 不能用嵌套 + values.push({ x: position.x, y: position.y, scaleX: scale.x, scaleY: scale.y, ...segmentValues }); + if (duration !== 0) { + times.push(currentDelay / duration); + } else times.push(0); + } + const container = target?.pixiContainer; + let animateInstance: ReturnType | null = null; + // 只有有 duration 的时候才有动画 + if (duration > 0) { + animateInstance = animate({ + to: values, + offset: times, + duration, + onUpdate: (updateValue) => { + if (container) { + const { scaleX, scaleY, ...val } = updateValue; + Object.assign(container, val); + // 因为 popmotion 不能用嵌套,scale 要手动设置 + container.scale.x = scaleX; + container.scale.y = scaleY; + } + }, + }); } const { duration: sliceDuration, ...endState } = getEndStateEffect(); @@ -56,23 +55,25 @@ export function generateTimelineObj( /** * 在此书写为动画设置初态的操作 */ - function setStartState() {} + function setStartState() { + if (target?.pixiContainer) { + // 不能赋值到 position,因为 x 和 y 被 WebGALPixiContainer 代理,而 position 属性没有代理 + const { position, ...state } = getStartStateEffect(); + Object.assign(target?.pixiContainer, { x: position.x, y: position.y, ...state }); + } + } /** * 在此书写为动画设置终态的操作 */ function setEndState() { - for (const gsaptimeline of gsapTimelines) { - if (gsaptimeline) { - gsaptimeline.seek(duration); - gsaptimeline.kill(); - } + if (animateInstance) animateInstance.stop(); + animateInstance = null; + if (target?.pixiContainer) { + // 不能赋值到 position,因为 x 和 y 被 WebGALPixiContainer 代理,而 position 属性没有代理 + const { position, ...state } = getEndStateEffect(); + Object.assign(target?.pixiContainer, { x: position.x, y: position.y, ...state }); } - gsapTimeline1 = null; - gsapTimeline2 = null; - gsapTimeline3 = null; - gsapTimeline4 = null; - gsapTimeline5 = null; } /** @@ -81,16 +82,20 @@ export function generateTimelineObj( */ function tickerFunc(delta: number) {} - function getEndFilterEffect() { - const gsapEffect = timeline[timeline.length - 1]; - const { alpha, rotation, blur, duration, scale, position, ...rest } = gsapEffect; - return rest; + function getStartStateEffect() { + return timeline[0]; } function getEndStateEffect() { return timeline[timeline.length - 1]; } + function getEndFilterEffect() { + const endSegment = timeline[timeline.length - 1]; + const { alpha, rotation, blur, duration, scale, position, ...rest } = endSegment; + return rest; + } + return { setStartState, setEndState, diff --git a/packages/webgal/src/Core/controller/stage/pixi/filters/ShockwaveFilter.ts b/packages/webgal/src/Core/controller/stage/pixi/filters/ShockwaveFilter.ts new file mode 100644 index 000000000..1598901cc --- /dev/null +++ b/packages/webgal/src/Core/controller/stage/pixi/filters/ShockwaveFilter.ts @@ -0,0 +1,39 @@ +import { WebGALPixiContainer } from '@/Core/controller/stage/pixi/WebGALPixiContainer'; +import { ShockwaveFilter } from 'pixi-filters'; + +const FILTER_NAME = 'shockwaveFilter'; + +export function getOrCreateShockwaveFilterImpl(container: WebGALPixiContainer, createMode: boolean) { + const shockwaveFilterFromMap = container.containerFilters.get(FILTER_NAME); + if (shockwaveFilterFromMap) { + return shockwaveFilterFromMap; + } else { + if (createMode) { + const shockwaveFilter = new ShockwaveFilter([1280, 720]); + shockwaveFilter.time = 0; + container.addFilter(shockwaveFilter); + container.containerFilters.set(FILTER_NAME, shockwaveFilter); + return shockwaveFilter; + } + } +} + +export function getShockwaveFilter(container: WebGALPixiContainer) { + if (container.getOrCreateShockwaveFilter(false)) { + const shockwaveFilter = container.getOrCreateShockwaveFilter() as ShockwaveFilter; + return shockwaveFilter.time; + } + return 0; +} + +export function setShockwaveFilter(container: WebGALPixiContainer, value: number) { + /** + * 如果是0,就移除这个滤镜 + */ + if (value === 0) { + container.removeFilter(FILTER_NAME); + } else { + const shockwaveFilter = container.getOrCreateShockwaveFilter() as ShockwaveFilter; + if (shockwaveFilter) shockwaveFilter.time = value; + } +} diff --git a/packages/webgal/src/Core/controller/stage/pixi/shaders/RadiusAlphaFilter.ts b/packages/webgal/src/Core/controller/stage/pixi/shaders/RadiusAlphaFilter.ts new file mode 100644 index 000000000..e08d00c2c --- /dev/null +++ b/packages/webgal/src/Core/controller/stage/pixi/shaders/RadiusAlphaFilter.ts @@ -0,0 +1,88 @@ +import * as PIXI from 'pixi.js'; +import { WebGALPixiContainer } from '@/Core/controller/stage/pixi/WebGALPixiContainer'; + +const INIT_RAD = 0; +const FILTER_NAME = 'radiusAlphaFilter'; + +class RadiusAlphaFilter extends PIXI.Filter { + public constructor(center: PIXI.Point, radius: number) { + const fragmentShader = ` +// 半径透明度的fragment shader +precision mediump float; + +uniform sampler2D uSampler; // 输入纹理 +varying vec2 vTextureCoord; // 当前片元的纹理坐标 +uniform vec2 center; // 圆心坐标 +uniform float radius; // 圆的半径 + +void main(void) { + vec4 color = texture2D(uSampler, vTextureCoord); + float dist = 0.0; + dist = distance(vTextureCoord, center); // 使用不同的变量名 + + vec4 color2 = color; + + if (dist < radius) { + color2 = color * 0.3; + } + + gl_FragColor = color2; +} + `; // 填入上面的fragment shader代码 + super(null as any, fragmentShader); + this.uniforms.center = [center.x, center.y]; + this.uniforms.radius = radius; + } + + public set center(value: PIXI.Point) { + this.uniforms.center = [value.x, value.y]; + } + + public get center(): PIXI.Point { + return new PIXI.Point(this.uniforms.center[0], this.uniforms.center[1]); + } + + public set radius(value: number) { + console.log(value); + this.uniforms.radius = value; + } + + public get radius(): number { + return this.uniforms.radius; + } +} + +export function getOrCreateRadiusAlphaFilterImpl(container: WebGALPixiContainer, createMode: boolean) { + const shockwaveFilterFromMap = container.containerFilters.get(FILTER_NAME); + if (shockwaveFilterFromMap) { + return shockwaveFilterFromMap; + } else { + if (createMode) { + const shockwaveFilter = new RadiusAlphaFilter(new PIXI.Point(0.5, 0.5), INIT_RAD); + shockwaveFilter.radius = INIT_RAD; + container.addFilter(shockwaveFilter); + container.containerFilters.set(FILTER_NAME, shockwaveFilter); + return shockwaveFilter; + } + } +} + +export function getRadiusAlphaFilter(container: WebGALPixiContainer) { + if (container.getOrCreateShockwaveFilter(false)) { + const shockwaveFilter = container.getOrCreateRadiusAlphaFilter() as RadiusAlphaFilter; + return shockwaveFilter.radius; + } + return INIT_RAD; +} + +export function setRadiusAlphaFilter(container: WebGALPixiContainer, value: number) { + /** + * 如果是0,就移除这个滤镜 + */ + if (value === 0) { + container.removeFilter(FILTER_NAME); + } else { + const shockwaveFilter = container.getOrCreateRadiusAlphaFilter() as RadiusAlphaFilter; + if (shockwaveFilter) shockwaveFilter.radius = value; + } +} diff --git a/packages/webgal/src/Core/gameScripts/choose/choose.module.scss b/packages/webgal/src/Core/gameScripts/choose/choose.module.scss index 50efdcb77..a5691c717 100644 --- a/packages/webgal/src/Core/gameScripts/choose/choose.module.scss +++ b/packages/webgal/src/Core/gameScripts/choose/choose.module.scss @@ -26,6 +26,22 @@ transition: background-color 0.5s, border 0.5s, font-weight 0.5s, box-shadow 0.5s; } +.Choose_item_disabled { + font-family: "WebgalUI", serif; + cursor: not-allowed; + min-width: 50%; + padding: 0.25em 1em 0.25em 1em; + font-size: 265%; + color: rgba(142, 53, 74, 0.5); + text-align: center; + border-radius: 4px; + border: 3px solid rgba(0, 0, 0, 0); + box-shadow: 0 0 25px rgba(0, 0, 0, 0.25); + background: rgba(255, 255, 255, 0.5); + margin: 0.25em 0 0.25em 0; + transition: background-color 0.5s, border 0.5s, font-weight 0.5s, box-shadow 0.5s; +} + .Choose_item:hover { //font-weight: bold; background: rgba(255, 255, 255, 0.9); diff --git a/packages/webgal/src/Core/gameScripts/choose/index.tsx b/packages/webgal/src/Core/gameScripts/choose/index.tsx index 5c1fa6ea4..ac1de2098 100644 --- a/packages/webgal/src/Core/gameScripts/choose/index.tsx +++ b/packages/webgal/src/Core/gameScripts/choose/index.tsx @@ -10,40 +10,89 @@ import { textFont } from '@/store/userDataInterface'; import { PerformController } from '@/Core/Modules/perform/performController'; import { useSEByWebgalStore } from '@/hooks/useSoundEffect'; import { WebGAL } from '@/Core/WebGAL'; +import { whenChecker } from '@/Core/controller/gamePlay/scriptExecutor'; + +class ChooseOption { + /** + * 格式: + * (showConditionVar>1)[enableConditionVar>2]->text:jump + */ + public static parse(script: string): ChooseOption { + const parts = script.split('->'); + const conditonPart = parts.length > 1 ? parts[0] : null; + const mainPart = parts.length > 1 ? parts[1] : parts[0]; + const mainPartNodes = mainPart.split(':'); + + const option = new ChooseOption(mainPartNodes[0], mainPartNodes[1]); + if (conditonPart !== null) { + const showConditionPart = conditonPart.match(/\((.*)\)/); + if (showConditionPart) { + option.showCondition = showConditionPart[1]; + } + const enableConditionPart = conditonPart.match(/\[(.*)\]/); + if (enableConditionPart) { + option.enableCondition = enableConditionPart[1]; + } + } + return option; + } + public text: string; + public jump: string; + public jumpToScene: boolean; + public showCondition?: string; + public enableCondition?: string; + + public constructor(text: string, jump: string) { + this.text = text; + this.jump = jump; + this.jumpToScene = jump.match(/\./) !== null; + } +} /** * 显示选择枝 * @param sentence */ export const choose = (sentence: ISentence): IPerform => { - let chooseList = sentence.content.split('|'); - const chooseListFull = chooseList.map((e) => e.split(':')); + const chooseOptionScripts = sentence.content.split('|'); + const chooseOptions = chooseOptionScripts.map((e) => ChooseOption.parse(e)); const fontFamily = webgalStore.getState().userData.optionData.textboxFont; const font = fontFamily === textFont.song ? '"思源宋体", serif' : '"WebgalUI", serif'; - const { playSeEnterChoose, playSeClickChoose } = useSEByWebgalStore(); - const chooseElements = chooseListFull.map((e, i) => { - return ( -
{ - playSeClickChoose(); - if (e[1].match(/\./)) { - changeScene(e[1], e[0]); - } else { - jmp(e[1]); - } - WebGAL.gameplay.performController.unmountPerform('choose'); - }} - onMouseEnter={playSeEnterChoose} - > - {e[0]} -
- ); - }); + const { playSeEnter, playSeClick } = useSEByWebgalStore(); + // 运行时计算JSX.Element[] + const runtimeBuildList = (chooseListFull: ChooseOption[]) => { + return chooseListFull + .filter((e, i) => whenChecker(e.showCondition)) + .map((e, i) => { + const enable = whenChecker(e.enableCondition); + const className = enable ? styles.Choose_item : styles.Choose_item_disabled; + const onClick = enable + ? () => { + playSeClick(); + if (e.jumpToScene) { + changeScene(e.jump, e.text); + } else { + jmp(e.jump); + } + WebGAL.gameplay.performController.unmountPerform('choose'); + } + : () => {}; + return ( +
+ {e.text} +
+ ); + }); + }; + // eslint-disable-next-line react/no-deprecated ReactDOM.render( -
{chooseElements}
, +
{runtimeBuildList(chooseOptions)}
, document.getElementById('chooseContainer'), ); return { @@ -51,6 +100,7 @@ export const choose = (sentence: ISentence): IPerform => { duration: 1000 * 60 * 60 * 24, isHoldOn: false, stopFunction: () => { + // eslint-disable-next-line react/no-deprecated ReactDOM.render(
, document.getElementById('chooseContainer')); }, blockingNext: () => true, diff --git a/packages/webgal/src/Core/gameScripts/getUserInput/index.tsx b/packages/webgal/src/Core/gameScripts/getUserInput/index.tsx index 3f94e688e..685af3fe8 100644 --- a/packages/webgal/src/Core/gameScripts/getUserInput/index.tsx +++ b/packages/webgal/src/Core/gameScripts/getUserInput/index.tsx @@ -26,20 +26,20 @@ export const getUserInput = (sentence: ISentence): IPerform => { const buttonText = (buttonTextFromArgs === 0 ? 'OK' : buttonTextFromArgs) ?? 'OK'; const fontFamily = webgalStore.getState().userData.optionData.textboxFont; const font = fontFamily === textFont.song ? '"思源宋体", serif' : '"WebgalUI", serif'; - const { playSeEnterChoose, playSeClickChoose } = useSEByWebgalStore(); + const { playSeEnter, playSeClick } = useSEByWebgalStore(); const chooseElements = (
{title}
{ const userInput: HTMLInputElement = document.getElementById('user-input') as HTMLInputElement; if (userInput) { webgalStore.dispatch(setStageVar({ key: varKey, value: userInput?.value ?? '' })); } - playSeClickChoose(); + playSeClick(); WebGAL.gameplay.performController.unmountPerform('userInput'); nextSentence(); }} @@ -50,6 +50,7 @@ export const getUserInput = (sentence: ISentence): IPerform => {
); + // eslint-disable-next-line react/no-deprecated ReactDOM.render(
{chooseElements}
, document.getElementById('chooseContainer'), @@ -59,6 +60,7 @@ export const getUserInput = (sentence: ISentence): IPerform => { duration: 1000 * 60 * 60 * 24, isHoldOn: false, stopFunction: () => { + // eslint-disable-next-line react/no-deprecated ReactDOM.render(
, document.getElementById('chooseContainer')); }, blockingNext: () => true, diff --git a/packages/webgal/src/Core/gameScripts/intro.tsx b/packages/webgal/src/Core/gameScripts/intro.tsx index 346084195..f7f559cee 100644 --- a/packages/webgal/src/Core/gameScripts/intro.tsx +++ b/packages/webgal/src/Core/gameScripts/intro.tsx @@ -38,6 +38,7 @@ export const intro = (sentence: ISentence): IPerform => { }; let chosenAnimationClass = styles.fadeIn; let delayTime = 1500; + let isHold = false; for (const e of sentence.args) { if (e.key === 'backgroundColor') { @@ -66,6 +67,11 @@ export const intro = (sentence: ISentence): IPerform => { const parsedValue = parseInt(e.value.toString(), 10); delayTime = isNaN(parsedValue) ? delayTime : parsedValue; } + if (e.key === 'hold') { + if (e.value === true) { + isHold = true; + } + } } const introContainerStyle = { @@ -75,30 +81,53 @@ export const intro = (sentence: ISentence): IPerform => { width: '100%', height: '100%', }; + const introArray: Array = sentence.content.split(/\|/); + + let endWait = 1000; + let baseDuration = endWait + delayTime * introArray.length; + const duration = isHold ? 1000 * 60 * 60 * 24 : 1000 + delayTime * introArray.length; + let isBlocking = true; + let setBlockingStateTimeout = setTimeout(() => { + isBlocking = false; + }, baseDuration); let timeout = setTimeout(() => {}); const toNextIntroElement = () => { const introContainer = document.getElementById('introContainer'); + // 由于用户操作,相当于时间向前推进,这时候更新这个演出的预计完成时间 + baseDuration -= delayTime; + clearTimeout(setBlockingStateTimeout); + setBlockingStateTimeout = setTimeout(() => { + isBlocking = false; + }, baseDuration); if (introContainer) { const children = introContainer.childNodes[0].childNodes[0].childNodes as any; const len = children.length; children.forEach((node: HTMLDivElement, index: number) => { + // 当前语句的延迟显示时间 const currentDelay = Number(node.style.animationDelay.split('ms')[0]); + // 当前语句还没有显示,降低显示延迟,因为现在时间因为用户操作,相当于向前推进了 if (currentDelay > 0) { - node.style.animationDelay = `${currentDelay - 1500}ms`; + node.style.animationDelay = `${currentDelay - delayTime}ms`; } + // 最后一个元素了 if (index === len - 1) { + // 并且已经完全显示了,这时候进行下一步 if (currentDelay === 0) { clearTimeout(timeout); WebGAL.gameplay.performController.unmountPerform(performName); // 卸载函数发生在 nextSentence 生效前,所以不需要做下一行的操作。 // setTimeout(nextSentence, 0); } else { + // 还没有完全显示,但是因为时间的推进,要提前完成演出,更新用于结束演出的计时器 clearTimeout(timeout); - timeout = setTimeout(() => { - WebGAL.gameplay.performController.unmountPerform(performName); - setTimeout(nextSentence, 0); - }, currentDelay - 500); + // 如果 Hold 了,自然不要自动结束 + if (!isHold) { + timeout = setTimeout(() => { + WebGAL.gameplay.performController.unmountPerform(performName); + setTimeout(nextSentence, 0); + }, baseDuration); + } } } }); @@ -110,7 +139,6 @@ export const intro = (sentence: ISentence): IPerform => { */ WebGAL.eventBus.on('__NEXT', toNextIntroElement); - const introArray: Array = sentence.content.split(/\|/); const showIntro = introArray.map((e, i) => (
{
{showIntro}
); + // eslint-disable-next-line react/no-deprecated ReactDOM.render(intro, document.getElementById('introContainer')); const introContainer = document.getElementById('introContainer'); if (introContainer) { introContainer.style.display = 'block'; } + return { performName, - duration: 1000 + delayTime * introArray.length, + duration, isHoldOn: false, stopFunction: () => { const introContainer = document.getElementById('introContainer'); @@ -143,8 +173,8 @@ export const intro = (sentence: ISentence): IPerform => { } WebGAL.eventBus.off('__NEXT', toNextIntroElement); }, - blockingNext: () => true, - blockingAuto: () => true, + blockingNext: () => isBlocking, + blockingAuto: () => isBlocking, stopTimeout: undefined, // 暂时不用,后面会交给自动清除 goNextWhenOver: true, }; diff --git a/packages/webgal/src/Core/gameScripts/playVideo.tsx b/packages/webgal/src/Core/gameScripts/playVideo.tsx index 443490d05..ded813d0f 100644 --- a/packages/webgal/src/Core/gameScripts/playVideo.tsx +++ b/packages/webgal/src/Core/gameScripts/playVideo.tsx @@ -24,6 +24,7 @@ export const playVideo = (sentence: ISentence): IPerform => { blockingNextFlag = true; } + // eslint-disable-next-line react/no-deprecated ReactDOM.render(