From 915df1ade3456b0ca1eef1642c40170bcf86308e Mon Sep 17 00:00:00 2001 From: Akirami <66513481+A-kirami@users.noreply.github.com> Date: Thu, 25 Jun 2026 10:15:31 +0800 Subject: [PATCH 1/3] fix: resolve the transform calculation order of the -next chain in fast preview --- .../src/Core/controller/gamePlay/scriptExecutor.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts index 8bfbb5cd8..9dd4ab7c2 100644 --- a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts +++ b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts @@ -35,6 +35,16 @@ export const whenChecker = (whenValue: string | undefined): boolean => { return !!strIf(valExp); }; +function settleFastPreviewPendingStateBeforeNext() { + if (!WebGAL.gameplay.isFastPreview) { + return; + } + + // fast preview 的 -next 链不会在语句之间 commit,需要先结算当前句会影响后续演算的 pending 终态。 + WebGAL.gameplay.performController.discardUncommittedNonHoldPerforms(true); + WebGAL.gameplay.performController.clearNonHoldPerformsFromStageState(); +} + /** * 语句执行器 * 执行语句,同步场景状态,并根据情况立即执行下一句或者加入backlog @@ -148,6 +158,7 @@ export const scriptExecutor = (depth = 0) => { if (isNext && !hasPendingBlockingStateCalculationPerform && !WebGAL.sceneManager.lockSceneWrite) { WebGAL.sceneManager.sceneData.currentSentenceId++; saveBacklogIfNeeded(); + settleFastPreviewPendingStateBeforeNext(); scriptExecutor(depth + 1); return; } From 71c5639ec18d002807c4a04a705ecd4fbf037a68 Mon Sep 17 00:00:00 2001 From: Akirami <66513481+A-kirami@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:04:20 +0800 Subject: [PATCH 2/3] fix: resolve the settlement order of the `-next` chain when using fast preview --- packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts index 9dd4ab7c2..6333987a1 100644 --- a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts +++ b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts @@ -157,8 +157,8 @@ export const scriptExecutor = (depth = 0) => { // 执行“下一句”。只有需要外部输入才能确定后续状态的演出,才会阻塞状态演算。 if (isNext && !hasPendingBlockingStateCalculationPerform && !WebGAL.sceneManager.lockSceneWrite) { WebGAL.sceneManager.sceneData.currentSentenceId++; - saveBacklogIfNeeded(); settleFastPreviewPendingStateBeforeNext(); + saveBacklogIfNeeded(); scriptExecutor(depth + 1); return; } From 89439eb686e1333cdc9176e5e1c51ce4cc5141f4 Mon Sep 17 00:00:00 2001 From: Mahiru Date: Sat, 25 Jul 2026 15:29:14 +0800 Subject: [PATCH 3/3] refactor animation register --- .../src/Core/Modules/animationFunctions.ts | 103 ++++++------------ packages/webgal/src/Core/Modules/backlog.ts | 6 +- .../Core/Modules/perform/performController.ts | 12 +- .../Core/Modules/perform/performInterface.ts | 2 - .../Core/controller/gamePlay/nextSentence.ts | 2 +- .../controller/gamePlay/scriptExecutor.ts | 11 -- .../controller/stage/pixi/PixiController.ts | 83 +------------- .../stage/pixi/animations/timeline.ts | 7 -- .../stage/pixi/syncPixiStageState.ts | 65 ++++++----- .../controller/storage/jumpFromBacklog.ts | 4 +- .../src/Core/controller/storage/loadGame.ts | 3 +- .../src/Core/gameScripts/SCRIPT_AUTHORING.md | 63 +++-------- .../src/Core/gameScripts/changeBg/index.ts | 57 +++++----- .../src/Core/gameScripts/changeFigure.ts | 56 +++++----- .../webgal/src/Core/gameScripts/label/jmp.ts | 2 +- .../src/Core/gameScripts/setAnimation.ts | 6 +- .../Core/gameScripts/setComplexAnimation.ts | 7 +- .../src/Core/gameScripts/setTempAnimation.ts | 6 +- .../src/Core/gameScripts/setTransform.ts | 6 +- .../webgal/src/Core/gameScripts/showVars.ts | 4 +- .../runtime/previewSyncSceneCommand.ts | 4 +- 21 files changed, 165 insertions(+), 344 deletions(-) diff --git a/packages/webgal/src/Core/Modules/animationFunctions.ts b/packages/webgal/src/Core/Modules/animationFunctions.ts index 1ba79e854..e46dd5b9e 100644 --- a/packages/webgal/src/Core/Modules/animationFunctions.ts +++ b/packages/webgal/src/Core/Modules/animationFunctions.ts @@ -1,4 +1,3 @@ -import { generateUniversalSoftInAnimationObj } from '@/Core/controller/stage/pixi/animations/universalSoftIn'; import { logger } from '@/Core/util/logger'; import { generateUniversalSoftOffAnimationObj } from '@/Core/controller/stage/pixi/animations/universalSoftOff'; import cloneDeep from 'lodash/cloneDeep'; @@ -8,12 +7,7 @@ import { WebGAL } from '@/Core/WebGAL'; import PixiStage, { IAnimationObject } from '@/Core/controller/stage/pixi/PixiController'; import { IUserAnimation } from './animations'; import { pickBy } from 'lodash'; -import { - DEFAULT_BG_IN_DURATION, - DEFAULT_BG_OUT_DURATION, - DEFAULT_FIG_IN_DURATION, - DEFAULT_FIG_OUT_DURATION, -} from '../constants'; +import { DEFAULT_BG_OUT_DURATION, DEFAULT_FIG_OUT_DURATION } from '../constants'; import { stageStateManager } from '@/Core/Modules/stage/stageStateManager'; import { AnimationFrame } from '@/Core/Modules/animations'; @@ -24,11 +18,10 @@ export function getAnimationObject( duration: number, writeDefault: boolean, writeFullEffect = true, - syncEndStateToStageState = true, ) { const mappedEffects = getAnimationTimeline(animationName, target, writeDefault, writeFullEffect); if (mappedEffects) { - return generateTimelineObj(mappedEffects, target, duration, syncEndStateToStageState); + return generateTimelineObj(mappedEffects, target, duration); } return null; } @@ -104,73 +97,43 @@ export function getAnimateDuration(animationName: string) { return 0; } -// eslint-disable-next-line max-params -export function getEnterExitAnimation( +/** + * 取退出动画。 + * + * 入场动画不在这里产出:它由 changeFigure/changeBg 作为普通演出返回, + * 终态在演算期写入 effects,因此不需要视图层反推该播哪个动画。 + */ +export function getExitAnimation( target: string, - type: 'enter' | 'exit', isBg = false, realTarget?: string, // 用于立绘和背景移除时,以当前时间打上特殊标记 ): { duration: number; animation: IAnimationObject | null; } { - if (type === 'enter') { - let duration = DEFAULT_FIG_IN_DURATION; - if (isBg) { - duration = DEFAULT_BG_IN_DURATION; - } - const animationSettings = stageStateManager - .getCalculationStageState() - .animationSettings.find((setting) => setting.target === target); - duration = animationSettings?.enterDuration ?? duration; - // 走默认动画 - let animation: IAnimationObject | null = generateUniversalSoftInAnimationObj(realTarget ?? target, duration); - - const transformState = stageStateManager.getCalculationStageState().effects; - const targetEffect = transformState.find((effect) => effect.target === target); - - const animationName = animationSettings?.enterAnimationName; - if (animationName && !targetEffect) { - logger.debug('取代默认进入动画', target); - animation = getAnimationObject( - animationName, - realTarget ?? target, - getAnimateDuration(animationName), - false, - !(animationSettings?.enterAnimationIgnoreDefault ?? false), - ); - duration = getAnimateDuration(animationName); - } - return { duration, animation }; - } else { - // exit - let duration = DEFAULT_FIG_OUT_DURATION; - if (isBg) { - duration = DEFAULT_BG_OUT_DURATION; - } - const animationSettings = stageStateManager - .getCalculationStageState() - .animationSettings.find((setting) => setting.target === target); - duration = animationSettings?.exitDuration ?? duration; - // 走默认动画 - let animation: IAnimationObject | null = generateUniversalSoftOffAnimationObj(realTarget ?? target, duration); - const animationName = animationSettings?.exitAnimationName; - if (animationName) { - logger.debug('取代默认退出动画', target); - animation = getAnimationObject( - animationName, - realTarget ?? target, - getAnimateDuration(animationName), - false, - !(animationSettings?.exitAnimationIgnoreDefault ?? false), - ); - duration = getAnimateDuration(animationName); - } - if (animationSettings) { - // 退出动画拿完后,删了这个设定 - stageStateManager.removeAnimationSettingsByTargetOff(target); - logger.debug('删除退出动画设定', target); - } - return { duration, animation }; + let duration = isBg ? DEFAULT_BG_OUT_DURATION : DEFAULT_FIG_OUT_DURATION; + const animationSettings = stageStateManager + .getCalculationStageState() + .animationSettings.find((setting) => setting.target === target); + duration = animationSettings?.exitDuration ?? duration; + // 走默认动画 + let animation: IAnimationObject | null = generateUniversalSoftOffAnimationObj(realTarget ?? target, duration); + const animationName = animationSettings?.exitAnimationName; + if (animationName) { + logger.debug('取代默认退出动画', target); + animation = getAnimationObject( + animationName, + realTarget ?? target, + getAnimateDuration(animationName), + false, + !(animationSettings?.exitAnimationIgnoreDefault ?? false), + ); + duration = getAnimateDuration(animationName); + } + if (animationSettings) { + // 退出动画拿完后,删了这个设定 + stageStateManager.removeAnimationSettingsByTargetOff(target); + logger.debug('删除退出动画设定', target); } + return { duration, animation }; } diff --git a/packages/webgal/src/Core/Modules/backlog.ts b/packages/webgal/src/Core/Modules/backlog.ts index 349e84829..0d43ea054 100644 --- a/packages/webgal/src/Core/Modules/backlog.ts +++ b/packages/webgal/src/Core/Modules/backlog.ts @@ -1,7 +1,7 @@ /** * 当前的backlog */ -import { IEffect, IStageState } from '@/Core/Modules/stage/stageInterface'; +import { IStageState } from '@/Core/Modules/stage/stageInterface'; import { ISaveScene } from '@/store/userDataInterface'; import cloneDeep from 'lodash/cloneDeep'; @@ -28,10 +28,6 @@ export class BacklogManager { return this.backlog; } - public editLastBacklogItemEffect(effects: IEffect[]) { - this.backlog[this.backlog.length - 1].currentStageState.effects = effects; - } - public makeBacklogEmpty() { this.backlog.splice(0, this.backlog.length); // 清空backlog } diff --git a/packages/webgal/src/Core/Modules/perform/performController.ts b/packages/webgal/src/Core/Modules/perform/performController.ts index a9ba6969b..6531a3397 100644 --- a/packages/webgal/src/Core/Modules/perform/performController.ts +++ b/packages/webgal/src/Core/Modules/perform/performController.ts @@ -94,16 +94,8 @@ export class PerformController { }); } - public discardUncommittedNonHoldPerforms(settleDiscardedState = false) { - this.pendingPerformList = this.pendingPerformList.filter(({ perform }) => { - if (perform.isHoldOn) { - return true; - } - if (settleDiscardedState) { - perform.settleStateOnDiscard?.(); - } - return false; - }); + public discardUncommittedNonHoldPerforms() { + this.pendingPerformList = this.pendingPerformList.filter(({ perform }) => perform.isHoldOn); } public hasPendingBlockingStateCalculationPerform() { diff --git a/packages/webgal/src/Core/Modules/perform/performInterface.ts b/packages/webgal/src/Core/Modules/perform/performInterface.ts index 5e635e407..858cc4c97 100644 --- a/packages/webgal/src/Core/Modules/perform/performInterface.ts +++ b/packages/webgal/src/Core/Modules/perform/performInterface.ts @@ -23,8 +23,6 @@ export interface IPerform { blockingAuto: () => boolean; // 是否阻塞同一轮 -next 继续演算;只有需要外部输入才能确定后续状态的演出需要覆盖。 blockingStateCalculation?: () => boolean; - // 未 commit 的演出被丢弃时,将它的终态同步到演算状态 - settleStateOnDiscard?: () => void; // 演出自然结束或被卸载后触发内部继续推进;推进前仍会等待 blockingNext 解除。 goNextWhenOver?: boolean; // 跳过由 nextSentence/continueSentence 引发的非 hold 演出回收。 diff --git a/packages/webgal/src/Core/controller/gamePlay/nextSentence.ts b/packages/webgal/src/Core/controller/gamePlay/nextSentence.ts index 52fcbd89c..6d00bfc56 100644 --- a/packages/webgal/src/Core/controller/gamePlay/nextSentence.ts +++ b/packages/webgal/src/Core/controller/gamePlay/nextSentence.ts @@ -60,7 +60,7 @@ export const forward = (options: ForwardOptions = {}) => { return false; } - WebGAL.gameplay.performController.discardUncommittedNonHoldPerforms(WebGAL.gameplay.isFastPreview); + WebGAL.gameplay.performController.discardUncommittedNonHoldPerforms(); WebGAL.gameplay.performController.clearNonHoldPerformsFromStageState(); WebGAL.gameplay.performController.beginCollectingPerforms(); try { diff --git a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts index d2bc6c5c8..fa6debe69 100644 --- a/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts +++ b/packages/webgal/src/Core/controller/gamePlay/scriptExecutor.ts @@ -45,16 +45,6 @@ export const whenChecker = (whenValue: string | undefined): boolean => { return !!strIf(valExp); }; -function settleFastPreviewPendingStateBeforeNext() { - if (!WebGAL.gameplay.isFastPreview) { - return; - } - - // fast preview 的 -next 链不会在语句之间 commit,需要先结算当前句会影响后续演算的 pending 终态。 - WebGAL.gameplay.performController.discardUncommittedNonHoldPerforms(true); - WebGAL.gameplay.performController.clearNonHoldPerformsFromStageState(); -} - /** * 语句执行器 * 执行语句,同步场景状态,并根据情况立即执行下一句或者加入backlog @@ -171,7 +161,6 @@ export const scriptExecutor = (depth = 0, options: ScriptExecutionOptions = {}) // 执行“下一句”。只有需要外部输入才能确定后续状态的演出,才会阻塞状态演算。 if (isNext && !hasPendingBlockingStateCalculationPerform && !WebGAL.sceneManager.lockSceneWrite) { WebGAL.sceneManager.sceneData.currentSentenceId++; - settleFastPreviewPendingStateBeforeNext(); saveBacklogIfNeeded(); scriptExecutor(depth + 1, options); return; diff --git a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts index da395ffcd..794a92f62 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts @@ -1,5 +1,5 @@ -import { IEffect, IFigureAssociatedAnimation, IFigureMetadata, ITransform } from '@/Core/Modules/stage/stageInterface'; -import { Live2D, WebGAL } from '@/Core/WebGAL'; +import { IFigureAssociatedAnimation, IFigureMetadata, ITransform } from '@/Core/Modules/stage/stageInterface'; +import { Live2D } from '@/Core/WebGAL'; import { baseBlinkParam, baseFocusParam, BlinkParam, FocusParam } from '@/Core/live2DCore'; import { isIOS } from '@/Core/initializeScript'; import { WebGALPixiContainer } from '@/Core/controller/stage/pixi/WebGALPixiContainer'; @@ -24,12 +24,9 @@ export interface IAnimationObject { } interface IStageAnimationObject { - // 唯一标识 - uuid: string; // 一般与作用目标有关 key: string; targetKey?: string; - type: 'common' | 'preset'; animationObject: IAnimationObject; } @@ -79,7 +76,6 @@ export default class PixiStage { public readonly mainStageContainer: WebGALPixiContainer; public readonly foregroundEffectsContainer: PIXI.Container; public readonly backgroundEffectsContainer: PIXI.Container; - public notUpdateBacklogEffects = false; public readonly figureContainer: PIXI.Container; public figureObjects = this.createReactiveList([]); public stageWidth = SCREEN_CONSTANTS.width; @@ -219,52 +215,13 @@ export default class PixiStage { */ public registerAnimation(animationObject: IAnimationObject | null, key: string, target = 'default') { if (!animationObject) return; - this.stageAnimations.push({ uuid: uuid(), animationObject, key: key, targetKey: target, type: 'common' }); + this.stageAnimations.push({ animationObject, key: key, targetKey: target }); // 上锁 this.lockStageObject(target); animationObject.setStartState(); this.currentApp?.ticker.add(animationObject.tickerFunc); } - /** - * 注册预设动画 - * @param animationObject - * @param key - * @param target - * @param currentEffects - */ - // eslint-disable-next-line max-params - public registerPresetAnimation( - animationObject: IAnimationObject | null, - key: string, - target = 'default', - currentEffects: IEffect[], - ) { - if (!animationObject) return; - const effect = currentEffects.find((effect) => effect.target === target); - if (effect) { - const targetPixiContainer = this.getStageObjByKey(target); - if (targetPixiContainer) { - const container = targetPixiContainer.pixiContainer; - if (container) PixiStage.assignTransform(container, effect.transform); - } - this.requestRender(); - return; - } - this.stageAnimations.push({ uuid: uuid(), animationObject, key: key, targetKey: target, type: 'preset' }); - // 上锁 - this.lockStageObject(target); - animationObject.setStartState(); - this.currentApp?.ticker.add(animationObject.tickerFunc); - } - - public stopPresetAnimationOnTarget(target: string) { - const targetPresetAnimations = this.stageAnimations.find((e) => e.targetKey === target && e.type === 'preset'); - if (targetPresetAnimations) { - this.removeAnimation(targetPresetAnimations.key); - } - } - /** * 移除动画 * @param key @@ -311,29 +268,6 @@ export default class PixiStage { } } - public removeAnimationWithSetEffects(key: string) { - const index = this.stageAnimations.findIndex((e) => e.key === key); - if (index >= 0) { - const thisTickerFunc = this.stageAnimations[index]; - this.currentApp?.ticker.remove(thisTickerFunc.animationObject.tickerFunc); - thisTickerFunc.animationObject.setEndState(); - const endStateEffect = thisTickerFunc.animationObject.getEndStateEffect?.() ?? {}; - this.unlockStageObject(thisTickerFunc.targetKey ?? 'default'); - if (thisTickerFunc.targetKey) { - const target = this.getStageObjByKey(thisTickerFunc.targetKey); - if (target) { - let effect: IEffect = { - target: thisTickerFunc.targetKey, - transform: endStateEffect, - }; - stageStateManager.updateEffect(effect); - // if (!this.notUpdateBacklogEffects) updateCurrentBacklogEffects(stageStateManager.getViewStageState().effects); - } - } - this.stageAnimations.splice(index, 1); - } - } - // eslint-disable-next-line max-params public performMouthSyncAnimation( key: string, @@ -1279,14 +1213,3 @@ export default class PixiStage { }); } } - -function updateCurrentBacklogEffects(newEffects: IEffect[]) { - /** - * 更新当前 backlog 条目的 effects 记录 - */ - setTimeout(() => { - WebGAL.backlogManager.editLastBacklogItemEffect(cloneDeep(newEffects)); - }, 50); - - stageStateManager.setStageAndCommit('effects', newEffects); -} 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 2e1e9b9b3..8c3b4890f 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/animations/timeline.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/animations/timeline.ts @@ -5,7 +5,6 @@ import omitBy from 'lodash/omitBy'; import isUndefined from 'lodash/isUndefined'; import PixiStage, { IAnimationObject } from '@/Core/controller/stage/pixi/PixiController'; import { AnimationFrame } from '@/Core/Modules/animations'; -import { stageStateManager } from '@/Core/Modules/stage/stageStateManager'; /** * 动画创建模板 @@ -17,7 +16,6 @@ export function generateTimelineObj( timeline: Array, targetKey: string, duration: number, - syncEndStateToStageState = true, ): IAnimationObject { const target = WebGAL.gameplay.pixiStage!.getStageObjByKey(targetKey); let currentDelay = 0; @@ -63,11 +61,6 @@ export function generateTimelineObj( }); } - if (syncEndStateToStageState) { - const { duration: sliceDuration, ease, ...endState } = getEndStateEffect(); - stageStateManager.updateEffect({ target: targetKey, transform: endState }); - } - /** * 在此书写为动画设置初态的操作 */ diff --git a/packages/webgal/src/Core/controller/stage/pixi/syncPixiStageState.ts b/packages/webgal/src/Core/controller/stage/pixi/syncPixiStageState.ts index 4046064bc..f74a3f440 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/syncPixiStageState.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/syncPixiStageState.ts @@ -1,9 +1,9 @@ import type { IEffect, IStageState, ITransform } from '@/Core/Modules/stage/stageInterface'; import type { IResolvedStageCommitOptions } from '@/Core/Modules/stage/stageStateManager'; -import { DEFAULT_BG_OUT_DURATION } from '@/Core/constants'; +import { DEFAULT_BG_IN_DURATION, DEFAULT_BG_OUT_DURATION, DEFAULT_FIG_IN_DURATION } from '@/Core/constants'; import { WebGAL } from '@/Core/WebGAL'; import type { IStageObject } from '@/Core/controller/stage/pixi/PixiController'; -import { getEnterExitAnimation } from '@/Core/Modules/animationFunctions'; +import { getAnimateDuration, getExitAnimation } from '@/Core/Modules/animationFunctions'; import { logger } from '@/Core/util/logger'; import { setEbg } from '@/Core/gameScripts/changeBg/setEbg'; import { applyTransformToPixiContainer } from '@/Core/controller/stage/pixi/stageEffectTransform'; @@ -12,13 +12,20 @@ interface ISyncFigureSlotPayload { key: string; sourceUrl: string; position: 'left' | 'center' | 'right'; - stageState: IStageState; skipAnimation: boolean; } -interface IRemoveFigOptions { - effects: IEffect[]; - skipAnimation: boolean; +/** + * 取入场过渡时长。 + * + * 入场动画本身由 changeBg/changeFigure 作为演出产出,这里只需要时长来同步其他视觉元素。 + */ +function getEnterDuration(stageState: IStageState, target: string, isBg: boolean): number { + const animationSettings = stageState.animationSettings.find((setting) => setting.target === target); + if (animationSettings?.enterAnimationName) { + return getAnimateDuration(animationSettings.enterAnimationName); + } + return animationSettings?.enterDuration ?? (isBg ? DEFAULT_BG_IN_DURATION : DEFAULT_FIG_IN_DURATION); } export function syncPixiStageState(stageState: IStageState, options: IResolvedStageCommitOptions) { @@ -75,14 +82,8 @@ function syncBg(stageState: IStageState, skipAnimation: boolean) { } addBg(thisBgKey, bgName); logger.debug('重设背景'); - const { duration, animation } = getEnterExitAnimation(thisBgKey, 'enter', true); - if (skipAnimation || WebGAL.gameplay.skipAnimation) { - setEbg(bgName, 0); - } else { - setEbg(bgName, duration); - pixiStage.registerPresetAnimation(animation, 'bg-main-softin', thisBgKey, stageState.effects); - setTimeout(() => pixiStage.removeAnimationWithSetEffects('bg-main-softin'), duration); - } + const isSkipAnimation = skipAnimation || WebGAL.gameplay.skipAnimation; + setEbg(bgName, isSkipAnimation ? 0 : getEnterDuration(stageState, thisBgKey, true)); return; } @@ -92,18 +93,17 @@ function syncBg(stageState: IStageState, skipAnimation: boolean) { } function syncFigures(stageState: IStageState, skipAnimation: boolean) { - syncFigureSlot({ key: 'fig-center', sourceUrl: stageState.figName, position: 'center', stageState, skipAnimation }); - syncFigureSlot({ key: 'fig-left', sourceUrl: stageState.figNameLeft, position: 'left', stageState, skipAnimation }); + syncFigureSlot({ key: 'fig-center', sourceUrl: stageState.figName, position: 'center', skipAnimation }); + syncFigureSlot({ key: 'fig-left', sourceUrl: stageState.figNameLeft, position: 'left', skipAnimation }); syncFigureSlot({ key: 'fig-right', sourceUrl: stageState.figNameRight, position: 'right', - stageState, skipAnimation, }); for (const fig of stageState.freeFigure) { - syncFigureSlot({ key: fig.key, sourceUrl: fig.name, position: fig.basePosition, stageState, skipAnimation }); + syncFigureSlot({ key: fig.key, sourceUrl: fig.name, position: fig.basePosition, skipAnimation }); } const currentFigures = WebGAL.gameplay.pixiStage?.getFigureObjects(); @@ -119,12 +119,12 @@ function syncFigures(stageState: IStageState, skipAnimation: boolean) { continue; } if (!freeFigureKeys.has(existFigure.key)) { - removeFig(existFigure, `${existFigure.key}-softin`, { effects: stageState.effects, skipAnimation }); + removeFig(existFigure, `${existFigure.key}-softin`, skipAnimation); } } } -function syncFigureSlot({ key, sourceUrl, position, stageState, skipAnimation }: ISyncFigureSlotPayload) { +function syncFigureSlot({ key, sourceUrl, position, skipAnimation }: ISyncFigureSlotPayload) { const pixiStage = WebGAL.gameplay.pixiStage; if (!pixiStage) return; const softInAniKey = `${key}-softin`; @@ -133,20 +133,16 @@ function syncFigureSlot({ key, sourceUrl, position, stageState, skipAnimation }: if (sourceUrl !== '') { if (currentFigure?.sourceUrl === sourceUrl) return; if (currentFigure) { - removeFig(currentFigure, softInAniKey, { effects: stageState.effects, skipAnimation }); + removeFig(currentFigure, softInAniKey, skipAnimation); } + // 入场动画由 changeFigure 作为演出产出,这里只负责创建舞台对象 addFigure(key, sourceUrl, position); logger.debug(`${key} 立绘已重设`); - const { duration, animation } = getEnterExitAnimation(key, 'enter'); - if (!skipAnimation && !WebGAL.gameplay.skipAnimation) { - pixiStage.registerPresetAnimation(animation, softInAniKey, key, stageState.effects); - setTimeout(() => pixiStage.removeAnimationWithSetEffects(softInAniKey), duration); - } return; } if (currentFigure) { - removeFig(currentFigure, softInAniKey, { effects: stageState.effects, skipAnimation }); + removeFig(currentFigure, softInAniKey, skipAnimation); } } @@ -189,7 +185,7 @@ function syncFigureMetaData(stageState: IStageState) { function removeBg(bgObject: IStageObject, skipAnimation: boolean): number { const pixiStage = WebGAL.gameplay.pixiStage; if (!pixiStage) return DEFAULT_BG_OUT_DURATION; - pixiStage.removeAnimationWithSetEffects('bg-main-softin'); + pixiStage.removeAnimation('bg-main-softin'); if (skipAnimation || WebGAL.gameplay.skipAnimation) { pixiStage.removeStageObjectByKey(bgObject.key); return 0; @@ -199,7 +195,7 @@ function removeBg(bgObject: IStageObject, skipAnimation: boolean): number { const bgKey = bgObject.key; const bgAniKey = bgObject.key + '-softoff'; pixiStage.removeStageObjectByKey(oldBgKey); - const { duration, animation } = getEnterExitAnimation('bg-main-off', 'exit', true, bgKey); + const { duration, animation } = getExitAnimation('bg-main-off', true, bgKey); pixiStage.registerAnimation(animation, bgAniKey, bgKey); setTimeout(() => { pixiStage.removeAnimation(bgAniKey); @@ -208,11 +204,11 @@ function removeBg(bgObject: IStageObject, skipAnimation: boolean): number { return duration; } -function removeFig(figObj: IStageObject, enterTikerKey: string, options: IRemoveFigOptions) { +function removeFig(figObj: IStageObject, enterTikerKey: string, skipAnimation: boolean) { const pixiStage = WebGAL.gameplay.pixiStage; if (!pixiStage) return; - pixiStage.removeAnimationWithSetEffects(enterTikerKey); - if (options.skipAnimation || WebGAL.gameplay.skipAnimation) { + pixiStage.removeAnimation(enterTikerKey); + if (skipAnimation || WebGAL.gameplay.skipAnimation) { logger.debug('快速模式,立刻关闭立绘'); pixiStage.removeStageObjectByKey(figObj.key); return; @@ -223,8 +219,9 @@ function removeFig(figObj: IStageObject, enterTikerKey: string, options: IRemove const figKey = figObj.key; pixiStage.removeStageObjectByKey(oldFigKey); const leaveKey = figKey + '-softoff'; - const { duration, animation } = getEnterExitAnimation(figLeaveAniKey, 'exit', false, figKey); - pixiStage.registerPresetAnimation(animation, leaveKey, figKey, options.effects); + // 退出对象的 key 带时间戳,永远不在 effects 白名单里,因此与背景一样走普通动画通道即可 + const { duration, animation } = getExitAnimation(figLeaveAniKey, false, figKey); + pixiStage.registerAnimation(animation, leaveKey, figKey); setTimeout(() => { pixiStage.removeAnimation(leaveKey); pixiStage.removeStageObjectByKey(figKey); diff --git a/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts b/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts index 2fd69d8ff..ea44570ca 100644 --- a/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts +++ b/packages/webgal/src/Core/controller/storage/jumpFromBacklog.ts @@ -75,7 +75,7 @@ export const jumpFromBacklog = (index: number, refetchScene = true) => { stageStateManager.replaceCalculationStageState(newStageState); // 恢复演出 - setTimeout(restorePerform, 0); + restorePerform(); // 关闭backlog界面 dispatch(setVisibility({ component: 'showBacklog', visibility: false })); @@ -84,5 +84,5 @@ export const jumpFromBacklog = (index: number, refetchScene = true) => { dispatch(setVisibility({ component: 'showTextBox', visibility: true })); // 重新渲染 - setTimeout(() => WebGAL.gameplay.pixiStage?.requestRender(), 100); + WebGAL.gameplay.pixiStage?.requestRender(); }; diff --git a/packages/webgal/src/Core/controller/storage/loadGame.ts b/packages/webgal/src/Core/controller/storage/loadGame.ts index dd0900b15..a93608445 100644 --- a/packages/webgal/src/Core/controller/storage/loadGame.ts +++ b/packages/webgal/src/Core/controller/storage/loadGame.ts @@ -61,8 +61,7 @@ export function loadGameFromStageData(stageData: ISaveData) { stageStateManager.replaceCalculationStageState(newStageState); // 恢复演出 - setTimeout(() => restorePerform(true), 0); - // restorePerform(); + restorePerform(true); dispatch(setVisibility({ component: 'showTitle', visibility: false })); dispatch(setVisibility({ component: 'showMenuPanel', visibility: false })); diff --git a/packages/webgal/src/Core/gameScripts/SCRIPT_AUTHORING.md b/packages/webgal/src/Core/gameScripts/SCRIPT_AUTHORING.md index cf69aef44..4ca48a267 100644 --- a/packages/webgal/src/Core/gameScripts/SCRIPT_AUTHORING.md +++ b/packages/webgal/src/Core/gameScripts/SCRIPT_AUTHORING.md @@ -59,61 +59,26 @@ export function someCommand(sentence: ISentence): IPerform { - `blockingNext`:是否阻塞用户下一步。 - `blockingAuto`:是否阻塞自动播放。 - `blockingStateCalculation`:是否阻塞继续演算后续状态。只有需要外部输入才能确定后续状态时才使用,例如选项和用户输入。 -- `settleStateOnDiscard`:pending perform 被“结算式丢弃”时的补偿钩子。它不是正常生命周期,不在 commit、start、stop、自然结束时执行。 -## settleStateOnDiscard +## 演算状态只允许顺序写 -`settleStateOnDiscard` 只解决一个窄问题:某条历史命令的 pending perform 在 commit 前被跳过,但它的最终状态又必须影响后续演算。 +演算状态(`calculationStageState`)的写入必须发生在命令函数阶段,顺序与语句顺序一致。 -当前触发点是 `forward()` 开头: +**不要把状态写入推迟到 `startFunction`、`stopFunction`、动画结束回调或 `setTimeout` 里。** 这类"延迟写"注册于语句 N,却执行于一个与语句顺序无关的时点,会覆盖语句 N+1 已经写好的状态。历史上 `settleStateOnDiscard` 就是为了给这种延迟写打补丁而存在的,现已连同问题一起移除。 -```ts -performController.discardUncommittedNonHoldPerforms(WebGAL.gameplay.isFastPreview); -``` - -也就是说,只有调用方传入 `settleDiscardedState = true` 时,被丢弃的非 hold pending perform 才会执行 `settleStateOnDiscard`。目前这个 true 只用于实时预览快进。普通 discard 不会执行这个钩子。 - -实现要求: - -- 必须同步执行,不能等待 loader、timer、动画帧或网络。 -- 必须幂等,重复调用不能把状态越写越偏。 -- 只写 `calculationStageState` 中可恢复、可被后续命令依赖的状态。 -- 不要操作 Pixi 对象、DOM、音频实例、ticker。 -- 不要调用 `commit()`。 - -什么时候需要实现: +正确的分工: -- 命令返回一个非 hold perform。 -- 命令的最终状态没有在命令函数阶段直接写入。 -- 这个最终状态对后续命令有意义。 -- 如果在命令函数阶段直接写入终态,会破坏当前行的正常视觉表现。 +- **命令函数阶段**:算出终态并写入演算状态。终态必须是同步可算的——如果算不出来,说明这个状态不该由这条命令负责。 +- **`startFunction`**:只做运行时动作(注册 Pixi 动画、播放媒体、挂载 UI),不写演算状态。 +- **`stopFunction`**:只清理运行时动作。 -什么时候不需要实现: +这样一条命令的 perform 无论被启动、被提前结算还是被丢弃,演算状态都一样,快速预览与正常播放自然一致。 -- 命令已经在函数阶段写好了后续命令需要的状态,例如普通 `setAnimation`、`setTransform`。 -- 命令只产生一次性声音、日志、UI 提示,后续演算不依赖它。 -- perform 是 hold,并且本来就应该保留到后续状态里。 +## 快速预览为什么仍然特殊 -## 快速预览为什么特殊 +实时预览快进会连续调用 `forward()`,中间不 commit,只在到达目标位置后提交一次。前一轮 `forward()` 收集到的非 hold pending perform,会在下一轮 `forward()` 开头被丢弃,因此不会执行 `startFunction` 和 `stopFunction`。 -实时预览快进会连续调用 `forward()`,中间不 commit,只在到达目标位置后提交一次。 - -这会带来一个差异:前一轮 `forward()` 收集到的非 hold pending perform,在下一轮 `forward()` 开头会被丢弃。被丢弃的 perform 不会执行: - -- `startFunction` -- `stopFunction` -- Pixi 注册动画后的结束回调 -- `setTimeout` 自动卸载逻辑 - -所以,如果某个命令把终态延迟到了这些阶段,快速预览历史行就会丢状态。 - -这次 `changeFigure -transform` 的问题就是这个类型: - -1. `changeFigure` 创建新立绘,并把 transform 做成进入动画。 -2. 正常播放时,进入动画由 Pixi sync 注册,终态会在动画结束或 preset 结算时写入 `effects`。 -3. 快速预览时,这条 enter perform 作为历史行被丢弃,没有机会注册进入动画。 -4. 后续 `setAnimation -parallel` 读取不到 figure 的 position,只能从默认 transform 开始算。 -5. 因此 `changeFigure` 需要在 `settleStateOnDiscard` 中把进入动画终态补写到 `calculationStageState.effects`。 +这只影响**视觉**,不影响演算状态——前提是命令遵守上面那条规则。如果某个命令把状态写进了这两个回调,快速预览就会丢状态,这是命令实现的 bug,不是预览机制的缺陷。 ## 动画命令 @@ -126,7 +91,9 @@ performController.discardUncommittedNonHoldPerforms(WebGAL.gameplay.isFastPrevie `-parallel` 下只能写动画实际控制的字段。例如只改 `scale` 的并行动画不应该把 `position` 重置成默认值。生成 timeline 或写终态时要使用局部字段合并,而不是完整覆盖目标 transform。 -新背景、新立绘的进入动画是特殊情况。当前行正常播放时不能无条件提前写入目标 `effects`,因为 `registerPresetAnimation()` 会把已有 effect 解释为目标已经结算,于是直接应用终态并跳过进入动画。它们应该在正常路径交给 Pixi preset 动画结算,在快速预览历史行被丢弃时再通过 `settleStateOnDiscard` 补写终态。 +新背景、新立绘的进入动画不是特殊情况,遵循同一条规则:`changeBg`、`changeFigure` 在命令函数阶段调用 `applyAnimationEndState()` 写入终态,进入动画由 returned perform 的 `startFunction` 用 `registerAnimation()` 注册。因此不存在"延迟结算",快速预览与正常播放读到的演算状态一致。 + +进入动画的演出名与 `setTransform` 等共用 `animation-`,同一目标上的动画冲突由演出去重统一裁决:后写的命令顶掉先写的,不需要按 `effects` 是否存在来猜测。视图层(`syncPixiStageState`)只负责创建和移除舞台对象,不再补写进入动画。 ## 参数和资源 @@ -148,7 +115,7 @@ JSON 参数必须 try/catch。解析失败时应回退到旧语义或安全默 - 后续命令需要读取的状态是否已经写入 `calculationStageState`? - perform 在快速预览历史行中被丢弃时,最终状态是否仍然正确? -- `settleStateOnDiscard` 是否只处理 pending discard 补偿,没有混入正常生命周期逻辑? +- 是否有任何演算状态的写入被推迟到了 `startFunction`、`stopFunction` 或定时器里? - `startFunction` 是否只依赖已 commit 的状态? - `stopFunction` 是否只清理已经启动过的运行时动作? - `-next`、`-continue`、`-parallel`、`-keep` 下状态是否一致? diff --git a/packages/webgal/src/Core/gameScripts/changeBg/index.ts b/packages/webgal/src/Core/gameScripts/changeBg/index.ts index ded9f103c..2a6d30976 100644 --- a/packages/webgal/src/Core/gameScripts/changeBg/index.ts +++ b/packages/webgal/src/Core/gameScripts/changeBg/index.ts @@ -8,6 +8,7 @@ import { unlockCgInUserData } from '@/store/userDataReducer'; import { logger } from '@/Core/util/logger'; import { ITransform } from '@/Core/Modules/stage/stageInterface'; import { generateTransformAnimationObj } from '@/Core/controller/stage/pixi/animations/generateTransformAnimationObj'; +import { generateTimelineObj } from '@/Core/controller/stage/pixi/animations/timeline'; import { AnimationFrame, IUserAnimation } from '@/Core/Modules/animations'; import cloneDeep from 'lodash/cloneDeep'; import { applyAnimationEndState, getAnimateDuration } from '@/Core/Modules/animationFunctions'; @@ -51,6 +52,8 @@ export const changeBg = (sentence: ISentence): IPerform => { * 删掉相关 Effects,因为已经移除了 */ if (isUrlChanged) { + // 必须先卸载旧的动画演出:它的 stopFunction 会写回终态,晚于清空 effects 会把旧变换复活 + WebGAL.gameplay.performController.unmountPerform(`animation-bg-main`, true); stageStateManager.removeEffectByTargetId(`bg-main`); stageStateManager.removeAnimationSettingsByTarget(`bg-main`); } @@ -109,41 +112,41 @@ export const changeBg = (sentence: ISentence): IPerform => { stageStateManager.updateAnimationSettings({ target: 'bg-main', key: 'exitDuration', value: exitDuration }); } + stageStateManager.setStage('bgName', sentence.content); + /** - * 背景状态后处理 + * 入场动画 + * + * 终态在演算期写入 effects,演出只负责视觉过渡,因此不需要任何延迟结算。 + * 与 setTransform 共用 `animation-bg-main` 演出名,同目标的动画冲突由演出去重统一裁决。 */ - function postBgStateSet() { - if (isUrlChanged) { - // 当 URL 发生变化时,清理旧的 hold 动画 - WebGAL.gameplay.performController.unmountPerform(`animation-bg-main`, true); - } - } - - postBgStateSet(); - stageStateManager.setStage('bgName', sentence.content); + const isEntering = isUrlChanged && sentence.content !== ''; + const enterAnimationSetting = isEntering + ? stageStateManager.getCalculationStageState().animationSettings.find((setting) => setting.target === 'bg-main') + : undefined; + const enterAnimationName = enterAnimationSetting?.enterAnimationName; + const enterAnimationTimeline = enterAnimationName + ? applyAnimationEndState( + enterAnimationName, + 'bg-main', + false, + !(enterAnimationSetting?.enterAnimationIgnoreDefault ?? false), + ) + : null; + const enterAnimationDuration = enterAnimationName ? getAnimateDuration(enterAnimationName) : 0; + const enterAnimationKey = 'bg-main-softin'; return { - performName: `bg-main-${sentence.content}`, + performName: isEntering ? `animation-bg-main` : `bg-main-${sentence.content}`, duration, isHoldOn: false, - settleStateOnDiscard: () => { - if (sentence.content === '' || !isUrlChanged) { - return; - } - const animationSetting = stageStateManager - .getCalculationStageState() - .animationSettings.find((setting) => setting.target === 'bg-main'); - if (animationSetting?.enterAnimationName) { - applyAnimationEndState( - animationSetting.enterAnimationName, - 'bg-main', - false, - !(animationSetting.enterAnimationIgnoreDefault ?? false), - ); - } + startFunction: () => { + if (!enterAnimationTimeline || WebGAL.gameplay.skipAnimation) return; + const animationObject = generateTimelineObj(enterAnimationTimeline, 'bg-main', enterAnimationDuration); + WebGAL.gameplay.pixiStage?.registerAnimation(animationObject, enterAnimationKey, 'bg-main'); }, stopFunction: () => { - WebGAL.gameplay.pixiStage?.stopPresetAnimationOnTarget('bg-main'); + WebGAL.gameplay.pixiStage?.removeAnimation(enterAnimationKey); }, blockingNext: () => false, blockingAuto: () => true, diff --git a/packages/webgal/src/Core/gameScripts/changeFigure.ts b/packages/webgal/src/Core/gameScripts/changeFigure.ts index 54c07859d..73a3c627b 100644 --- a/packages/webgal/src/Core/gameScripts/changeFigure.ts +++ b/packages/webgal/src/Core/gameScripts/changeFigure.ts @@ -5,6 +5,7 @@ import { getBooleanArgByKey, getNumberArgByKey, getStringArgByKey } from '@/Core import { IFreeFigure, IStageState, ITransform } from '@/Core/Modules/stage/stageInterface'; import { AnimationFrame, IUserAnimation } from '@/Core/Modules/animations'; import { generateTransformAnimationObj } from '@/Core/controller/stage/pixi/animations/generateTransformAnimationObj'; +import { generateTimelineObj } from '@/Core/controller/stage/pixi/animations/timeline'; import { assetSetter, fileType } from '@/Core/util/gameAssetsAccess/assetSetter'; import { logger } from '@/Core/util/logger'; import { applyAnimationEndState, getAnimateDuration } from '@/Core/Modules/animationFunctions'; @@ -30,19 +31,13 @@ export function changeFigure(sentence: ISentence): IPerform { // 根据参数设置指定位置 let pos: 'center' | 'left' | 'right' = 'center'; - let mouthAnimationKey = 'mouthAnimation'; - let eyesAnimationKey = 'blinkAnimation'; const leftFromArgs = getBooleanArgByKey(sentence, 'left') ?? false; const rightFromArgs = getBooleanArgByKey(sentence, 'right') ?? false; if (leftFromArgs) { pos = 'left'; - mouthAnimationKey = 'mouthAnimationLeft'; - eyesAnimationKey = 'blinkAnimationLeft'; } if (rightFromArgs) { pos = 'right'; - mouthAnimationKey = 'mouthAnimationRight'; - eyesAnimationKey = 'blinkAnimationRight'; } // id 与 自由立绘 @@ -148,6 +143,8 @@ export function changeFigure(sentence: ISentence): IPerform { * 处理 Effects */ if (isUrlChanged) { + // 必须先卸载旧的动画演出:它的 stopFunction 会写回终态,晚于清空 effects 会把旧变换复活 + WebGAL.gameplay.performController.unmountPerform(`animation-${id}`, true); stageStateManager.removeEffectByTargetId(id); stageStateManager.removeAnimationSettingsByTarget(id); const oldStageObject = WebGAL.gameplay.pixiStage?.getStageObjByKey(id); @@ -216,8 +213,6 @@ export function changeFigure(sentence: ISentence): IPerform { if (isUrlChanged) { // 当 url 发生变化时,即发生新立绘替换 // 应当赋予一些参数以默认值,防止从旧立绘的状态获取数据 - // 并且关闭一些 hold 动画 - WebGAL.gameplay.performController.unmountPerform(`animation-${key}`, true); bounds = bounds ?? [0, 0, 0, 0]; blink = blink ?? cloneDeep(baseBlinkParam); focus = focus ?? cloneDeep(baseFocusParam); @@ -282,28 +277,39 @@ export function changeFigure(sentence: ISentence): IPerform { stageStateManager.setStage(dispatchMap[pos], content); } + /** + * 入场动画 + * + * 终态在演算期写入 effects,演出只负责视觉过渡,因此不需要任何延迟结算。 + * 与 setTransform 共用 `animation-${key}` 演出名,同目标的动画冲突由演出去重统一裁决。 + */ + const isEntering = isUrlChanged && content !== ''; + const enterAnimationSetting = isEntering + ? stageStateManager.getCalculationStageState().animationSettings.find((setting) => setting.target === key) + : undefined; + const enterAnimationName = enterAnimationSetting?.enterAnimationName; + const enterAnimationTimeline = enterAnimationName + ? applyAnimationEndState( + enterAnimationName, + key, + false, + !(enterAnimationSetting?.enterAnimationIgnoreDefault ?? false), + ) + : null; + const enterAnimationDuration = enterAnimationName ? getAnimateDuration(enterAnimationName) : 0; + const enterAnimationKey = `${key}-softin`; + return { - performName: `enter-${key}`, + performName: isEntering ? `animation-${key}` : `enter-${key}`, duration, isHoldOn: false, - settleStateOnDiscard: () => { - if (content === '' || !isUrlChanged) { - return; - } - const animationSetting = stageStateManager - .getCalculationStageState() - .animationSettings.find((setting) => setting.target === key); - if (animationSetting?.enterAnimationName) { - applyAnimationEndState( - animationSetting.enterAnimationName, - key, - false, - !(animationSetting.enterAnimationIgnoreDefault ?? false), - ); - } + startFunction: () => { + if (!enterAnimationTimeline || WebGAL.gameplay.skipAnimation) return; + const animationObject = generateTimelineObj(enterAnimationTimeline, key, enterAnimationDuration); + WebGAL.gameplay.pixiStage?.registerAnimation(animationObject, enterAnimationKey, key); }, stopFunction: () => { - WebGAL.gameplay.pixiStage?.stopPresetAnimationOnTarget(key); + WebGAL.gameplay.pixiStage?.removeAnimation(enterAnimationKey); }, blockingNext: () => false, blockingAuto: () => true, diff --git a/packages/webgal/src/Core/gameScripts/label/jmp.ts b/packages/webgal/src/Core/gameScripts/label/jmp.ts index 53bd56a73..01fae15f0 100644 --- a/packages/webgal/src/Core/gameScripts/label/jmp.ts +++ b/packages/webgal/src/Core/gameScripts/label/jmp.ts @@ -4,6 +4,6 @@ import { jumpToLabel } from '@/Core/gameScripts/label/jumpToLabel'; export const jmp = (labelName: string, autoNext = true) => { const isJumped = jumpToLabel(labelName); if (isJumped && autoNext) { - setTimeout(continueSentence, 1); + continueSentence(); } }; diff --git a/packages/webgal/src/Core/gameScripts/setAnimation.ts b/packages/webgal/src/Core/gameScripts/setAnimation.ts index 24af0fefa..3db4f43da 100644 --- a/packages/webgal/src/Core/gameScripts/setAnimation.ts +++ b/packages/webgal/src/Core/gameScripts/setAnimation.ts @@ -34,9 +34,8 @@ export const setAnimation = (sentence: ISentence): IPerform => { if (keep && keepAnimationStopped) { return; } - WebGAL.gameplay.pixiStage?.stopPresetAnimationOnTarget(target); const animationObj: IAnimationObject | null = animationTimeline - ? generateTimelineObj(animationTimeline, target, animationDuration, false) + ? generateTimelineObj(animationTimeline, target, animationDuration) : null; if (animationObj) { logger.debug(`动画${animationName}作用在${target}`, animationDuration); @@ -49,7 +48,8 @@ export const setAnimation = (sentence: ISentence): IPerform => { keepAnimationStopped = true; return; } - WebGAL.gameplay.pixiStage?.removeAnimationWithSetEffects(key); + // 终态已在命令函数阶段写入 effects,这里只把容器推到终态,不回写演算状态 + WebGAL.gameplay.pixiStage?.removeAnimation(key); }; return { diff --git a/packages/webgal/src/Core/gameScripts/setComplexAnimation.ts b/packages/webgal/src/Core/gameScripts/setComplexAnimation.ts index 0d4435fa9..650d24c9e 100644 --- a/packages/webgal/src/Core/gameScripts/setComplexAnimation.ts +++ b/packages/webgal/src/Core/gameScripts/setComplexAnimation.ts @@ -13,7 +13,6 @@ import { stageStateManager } from '@/Core/Modules/stage/stageStateManager'; * @param sentence */ export const setComplexAnimation = (sentence: ISentence): IPerform => { - const startDialogKey = stageStateManager.getCalculationStageState().currentDialogKey; const animationName = sentence.content; const animationDuration = getNumberArgByKey(sentence, 'duration') ?? 0; const target = getStringArgByKey(sentence, 'target') ?? '0'; @@ -31,13 +30,11 @@ export const setComplexAnimation = (sentence: ISentence): IPerform => { startFunction = () => { logger.debug(`动画${animationName}作用在${target}`, animationDuration); const animationObj: IAnimationObject = animationFunction(target, animationDuration); - WebGAL.gameplay.pixiStage?.stopPresetAnimationOnTarget(target); WebGAL.gameplay.pixiStage?.registerAnimation(animationObj, key, target); }; stopFunction = () => { - const endDialogKey = stageStateManager.getCalculationStageState().currentDialogKey; - const isHasNext = startDialogKey !== endDialogKey; - WebGAL.gameplay.pixiStage?.removeAnimationWithSetEffects(key); + // 终态已在命令函数阶段写入 effects,这里只把容器推到终态,不回写演算状态 + WebGAL.gameplay.pixiStage?.removeAnimation(key); }; } return { diff --git a/packages/webgal/src/Core/gameScripts/setTempAnimation.ts b/packages/webgal/src/Core/gameScripts/setTempAnimation.ts index dc06c9f96..d0789ff74 100644 --- a/packages/webgal/src/Core/gameScripts/setTempAnimation.ts +++ b/packages/webgal/src/Core/gameScripts/setTempAnimation.ts @@ -43,9 +43,8 @@ export const setTempAnimation = (sentence: ISentence): IPerform => { if (keep && keepAnimationStopped) { return; } - WebGAL.gameplay.pixiStage?.stopPresetAnimationOnTarget(target); const animationObj: IAnimationObject | null = animationTimeline - ? generateTimelineObj(animationTimeline, target, animationDuration, false) + ? generateTimelineObj(animationTimeline, target, animationDuration) : null; if (animationObj) { logger.debug(`动画${animationName}作用在${target}`, animationDuration); @@ -58,7 +57,8 @@ export const setTempAnimation = (sentence: ISentence): IPerform => { keepAnimationStopped = true; return; } - WebGAL.gameplay.pixiStage?.removeAnimationWithSetEffects(key); + // 终态已在命令函数阶段写入 effects,这里只把容器推到终态,不回写演算状态 + WebGAL.gameplay.pixiStage?.removeAnimation(key); }; return { diff --git a/packages/webgal/src/Core/gameScripts/setTransform.ts b/packages/webgal/src/Core/gameScripts/setTransform.ts index ff72934fd..b18d23142 100644 --- a/packages/webgal/src/Core/gameScripts/setTransform.ts +++ b/packages/webgal/src/Core/gameScripts/setTransform.ts @@ -49,9 +49,8 @@ export const setTransform = (sentence: ISentence): IPerform => { if (keep && keepAnimationStopped) { return; } - WebGAL.gameplay.pixiStage?.stopPresetAnimationOnTarget(target); const animationObj: IAnimationObject | null = animationTimeline - ? generateTimelineObj(animationTimeline, target, animationDuration, false) + ? generateTimelineObj(animationTimeline, target, animationDuration) : null; if (animationObj) { logger.debug(`动画${animationName}作用在${target}`, animationDuration); @@ -64,7 +63,8 @@ export const setTransform = (sentence: ISentence): IPerform => { keepAnimationStopped = true; return; } - WebGAL.gameplay.pixiStage?.removeAnimationWithSetEffects(key); + // 终态已在命令函数阶段写入 effects,这里只把容器推到终态,不回写演算状态 + WebGAL.gameplay.pixiStage?.removeAnimation(key); }; return { diff --git a/packages/webgal/src/Core/gameScripts/showVars.ts b/packages/webgal/src/Core/gameScripts/showVars.ts index 6346c2b0c..014eb526a 100644 --- a/packages/webgal/src/Core/gameScripts/showVars.ts +++ b/packages/webgal/src/Core/gameScripts/showVars.ts @@ -30,9 +30,7 @@ export const showVars = (sentence: ISentence): IPerform => { duration: endDelay, isHoldOn: false, startFunction: () => { - setTimeout(() => { - WebGAL.events.textSettle.emit(); - }, 0); + WebGAL.events.textSettle.emit(); }, stopFunction: () => { WebGAL.events.textSettle.emit(); diff --git a/packages/webgal/src/Core/util/syncWithEditor/runtime/previewSyncSceneCommand.ts b/packages/webgal/src/Core/util/syncWithEditor/runtime/previewSyncSceneCommand.ts index 46dc17248..a2a7161fb 100644 --- a/packages/webgal/src/Core/util/syncWithEditor/runtime/previewSyncSceneCommand.ts +++ b/packages/webgal/src/Core/util/syncWithEditor/runtime/previewSyncSceneCommand.ts @@ -130,7 +130,7 @@ export async function runFastPreview( return; } - WebGAL.gameplay.performController.discardUncommittedNonHoldPerforms(true); + WebGAL.gameplay.performController.discardUncommittedNonHoldPerforms(); WebGAL.gameplay.performController.clearNonHoldPerformsFromStageState(); options.onBeforeTargetScriptExecute?.(); didRunBeforeTargetScriptExecute = true; @@ -198,7 +198,7 @@ export async function runFastPreview( } if (settleMode === 'immediate') { - WebGAL.gameplay.performController.discardUncommittedNonHoldPerforms(true); + WebGAL.gameplay.performController.discardUncommittedNonHoldPerforms(); WebGAL.gameplay.performController.clearNonHoldPerformsFromStageState(); }