diff --git a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts index 9bf9333ab..7b1e9fc81 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/PixiController.ts @@ -859,6 +859,62 @@ export default class PixiStage { } } + public changeSpineSkinByKey(key: string, skin: string) { + if (!skin) return; + + const target = this.figureObjects.find((e) => e.key === key && !e.isExiting); + if (target?.sourceType !== 'spine') return; + + const container = target.pixiContainer; + if (!container) return; + const sprite = container.children[0] as PIXI.Container; + if (sprite?.children?.[0]) { + const spineObject = sprite.children[0]; + // @ts-ignore + const skeleton = spineObject.skeleton; + // @ts-ignore + const skeletonData = skeleton?.data ?? spineObject.spineData; + const skinObject = + // @ts-ignore + skeletonData?.findSkin?.(skin) ?? + // @ts-ignore + skeletonData?.skins?.find((item: any) => item.name === skin); + + if (!skeleton || !skinObject) { + logger.warn(`Spine skin not found: ${skin} on ${key}`); + return; + } + + try { + // @ts-ignore + if (typeof skeleton.setSkinByName === 'function') { + // @ts-ignore + skeleton.setSkinByName(skin); + } else { + // @ts-ignore + skeleton.setSkin(skinObject); + } + } catch (error) { + // @ts-ignore + skeleton.setSkin?.(skinObject); + } + + // @ts-ignore + if (typeof skeleton.setSlotsToSetupPose === 'function') { + // @ts-ignore + skeleton.setSlotsToSetupPose(); + } else { + // @ts-ignore + skeleton.setupPoseSlots?.(); + } + + // @ts-ignore + spineObject.state?.apply?.(skeleton); + // @ts-ignore + skeleton.updateWorldTransform?.(); + } + } + public changeModelExpressionByKey(key: string, expression: string) { // logger.debug(`Applying expression ${expression} to ${key}`); const target = this.figureObjects.find((e) => e.key === key && !e.isExiting); diff --git a/packages/webgal/src/Core/controller/stage/pixi/spine.ts b/packages/webgal/src/Core/controller/stage/pixi/spine.ts index 8ba72e547..28e084ad4 100644 --- a/packages/webgal/src/Core/controller/stage/pixi/spine.ts +++ b/packages/webgal/src/Core/controller/stage/pixi/spine.ts @@ -111,9 +111,13 @@ export async function addSpineFigureImpl( figureSpine.pivot.set(spineCenterX, spineCenterY); figureSpine.interactive = false; - // 检查状态中是否有指定的动画 const motionFromState = webgalStore.getState().stage.live2dMotion.find((e) => e.target === key); let animationToPlay = ''; + if (motionFromState?.skin) { + if (!applySpineSkin(figureSpine, motionFromState.skin)) { + logger.warn(`Spine skin not found: ${motionFromState.skin} on ${key}`); + } + } if ( motionFromState && @@ -277,3 +281,40 @@ export async function addSpineBgImpl(this: PixiStage, key: string, url: string) await setup(); } } + +function applySpineSkin(spineObject: any, skinName: string) { + // @ts-ignore + const skeleton = spineObject.skeleton; + // @ts-ignore + const skeletonData = skeleton?.data ?? spineObject.spineData; + const skin = + // @ts-ignore + skeletonData?.findSkin?.(skinName) ?? + // @ts-ignore + skeletonData?.skins?.find((item: any) => item.name === skinName); + if (!skeleton || !skin) { + return false; + } + try { + // @ts-ignore + if (typeof skeleton.setSkinByName === 'function') { + // @ts-ignore + skeleton.setSkinByName(skinName); + } else { + // @ts-ignore + skeleton.setSkin(skin); + } + } catch (error) { + // @ts-ignore + skeleton.setSkin?.(skin); + } + // @ts-ignore + if (typeof skeleton.setSlotsToSetupPose === 'function') { + // @ts-ignore + skeleton.setSlotsToSetupPose(); + } else { + // @ts-ignore + skeleton.setupPoseSlots?.(); + } + return true; +} diff --git a/packages/webgal/src/Core/gameScripts/changeFigure.ts b/packages/webgal/src/Core/gameScripts/changeFigure.ts index 702fce3a5..6c6767a84 100644 --- a/packages/webgal/src/Core/gameScripts/changeFigure.ts +++ b/packages/webgal/src/Core/gameScripts/changeFigure.ts @@ -52,6 +52,7 @@ export function changeFigure(sentence: ISentence): IPerform { // live2d 或 spine 相关 let motion = getStringArgByKey(sentence, 'motion') ?? ''; + const skin = getStringArgByKey(sentence, 'skin') ?? ''; let expression = getStringArgByKey(sentence, 'expression') ?? ''; const boundsFromArgs = getStringArgByKey(sentence, 'bounds') ?? ''; let bounds = getOverrideBoundsArr(boundsFromArgs); @@ -234,7 +235,7 @@ export function changeFigure(sentence: ISentence): IPerform { focus = focus ?? cloneDeep(baseFocusParam); zIndex = Math.max(zIndex, 0); blendMode = blendMode ?? 'normal'; - dispatch(stageActions.setLive2dMotion({ target: key, motion, overrideBounds: bounds })); + dispatch(stageActions.setLive2dMotion({ target: key, motion, skin, overrideBounds: bounds })); dispatch(stageActions.setLive2dExpression({ target: key, expression })); dispatch(stageActions.setLive2dBlink({ target: key, blink })); dispatch(stageActions.setLive2dFocus({ target: key, focus })); @@ -243,8 +244,8 @@ export function changeFigure(sentence: ISentence): IPerform { } else { // 当 url 没有发生变化时,即没有新立绘替换 // 应当保留旧立绘的状态,仅在需要时更新 - if (motion || bounds) { - dispatch(stageActions.setLive2dMotion({ target: key, motion, overrideBounds: bounds })); + if (motion || skin || bounds) { + dispatch(stageActions.setLive2dMotion({ target: key, motion, skin, overrideBounds: bounds })); } if (expression) { dispatch(stageActions.setLive2dExpression({ target: key, expression })); diff --git a/packages/webgal/src/Stage/MainStage/useSetFigure.ts b/packages/webgal/src/Stage/MainStage/useSetFigure.ts index 311e93556..3d103e834 100644 --- a/packages/webgal/src/Stage/MainStage/useSetFigure.ts +++ b/packages/webgal/src/Stage/MainStage/useSetFigure.ts @@ -26,6 +26,9 @@ export function useSetFigure(stageState: IStageState) { */ useEffect(() => { for (const motion of live2dMotion) { + if (motion.skin) { + WebGAL.gameplay.pixiStage?.changeSpineSkinByKey(motion.target, motion.skin); + } WebGAL.gameplay.pixiStage?.changeModelMotionByKey(motion.target, motion.motion); } }, [live2dMotion]); diff --git a/packages/webgal/src/store/stageInterface.ts b/packages/webgal/src/store/stageInterface.ts index fd319d71f..12142f1c1 100644 --- a/packages/webgal/src/store/stageInterface.ts +++ b/packages/webgal/src/store/stageInterface.ts @@ -177,6 +177,7 @@ export interface IRunPerform { export interface ILive2DMotion { target: string; motion: string; + skin?: string; overrideBounds?: [number, number, number, number]; } diff --git a/packages/webgal/src/store/stageReducer.ts b/packages/webgal/src/store/stageReducer.ts index 57e1ffe42..ac382f44a 100644 --- a/packages/webgal/src/store/stageReducer.ts +++ b/packages/webgal/src/store/stageReducer.ts @@ -244,16 +244,17 @@ const stageSlice = createSlice({ } }, setLive2dMotion: (state, action: PayloadAction) => { - const { target, motion, overrideBounds } = action.payload; + const { target, motion, skin, overrideBounds } = action.payload; const index = state.live2dMotion.findIndex((e) => e.target === target); if (index < 0) { // Add a new motion - state.live2dMotion.push({ target, motion, overrideBounds }); + state.live2dMotion.push({ target, motion, skin, overrideBounds }); } else { // Update the existing motion state.live2dMotion[index].motion = motion; + state.live2dMotion[index].skin = skin; state.live2dMotion[index].overrideBounds = overrideBounds; } },