Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions packages/webgal/src/Core/controller/stage/pixi/PixiController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +897 to +899
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The catch block only contains a // @ts-ignore comment and then attempts to set the skin. It would be better to log the error and potentially provide a more graceful fallback, or rethrow the error if it's unrecoverable.

}

// @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?.();
}
}
Comment on lines +862 to +916
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function contains a lot of duplicated code and multiple // @ts-ignore comments. It would be better to extract the skin application logic into a separate, reusable function to improve readability and maintainability. Also, consider using optional chaining instead of multiple if statements to check for nested properties.


public changeModelExpressionByKey(key: string, expression: string) {
// logger.debug(`Applying expression ${expression} to ${key}`);
const target = this.figureObjects.find((e) => e.key === key && !e.isExiting);
Expand Down
43 changes: 42 additions & 1 deletion packages/webgal/src/Core/controller/stage/pixi/spine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This line reintroduces the original issue of checking the state for animations. It would be better to ensure that this logic is consistent with the new skin implementation.

let animationToPlay = '';
if (motionFromState?.skin) {
if (!applySpineSkin(figureSpine, motionFromState.skin)) {
logger.warn(`Spine skin not found: ${motionFromState.skin} on ${key}`);
}
}

if (
motionFromState &&
Expand Down Expand Up @@ -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;
}
Comment on lines +285 to +320
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This function duplicates the skin application logic found in PixiController.ts. Consolidate this logic into a single, reusable function to avoid redundancy and improve maintainability.

/**
 * Applies a skin to a Spine object.
 * @param spineObject The Spine object to apply the skin to.
 * @param skinName The name of the skin to apply.
 * @returns True if the skin was successfully applied, false otherwise.
 */
function applySpineSkin(spineObject: any, skinName: string): boolean {
  if (!spineObject?.skeleton) {
    logger.warn("Spine object has no skeleton.");
    return false;
  }

  const skeleton = spineObject.skeleton;
  const skeletonData = skeleton.data || spineObject.spineData;
  const skin = skeletonData?.findSkin?.(skinName) || skeletonData?.skins?.find((item: any) => item.name === skinName);

  if (!skin) {
    logger.warn(`Spine skin not found: ${skinName}`);
    return false;
  }

  try {
    if (typeof skeleton.setSkinByName === 'function') {
      skeleton.setSkinByName(skinName);
    } else {
      skeleton.setSkin(skin);
    }
  } catch (error) {
    logger.error(`Failed to set skin ${skinName}:`, error);
    skeleton.setSkin?.(skin);
  }

  if (typeof skeleton.setSlotsToSetupPose === 'function') {
    skeleton.setSlotsToSetupPose();
  } else {
    skeleton.setupPoseSlots?.();
  }

  return true;
}

7 changes: 4 additions & 3 deletions packages/webgal/src/Core/gameScripts/changeFigure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 }));
Expand All @@ -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 }));
Expand Down
3 changes: 3 additions & 0 deletions packages/webgal/src/Stage/MainStage/useSetFigure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/store/stageInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export interface IRunPerform {
export interface ILive2DMotion {
target: string;
motion: string;
skin?: string;
overrideBounds?: [number, number, number, number];
}

Expand Down
5 changes: 3 additions & 2 deletions packages/webgal/src/store/stageReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,16 +244,17 @@ const stageSlice = createSlice({
}
},
setLive2dMotion: (state, action: PayloadAction<ILive2DMotion>) => {
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;
}
},
Expand Down
Loading