Skip to content

Commit 1a74acd

Browse files
authored
Merge branch 'dev' into chore/align-webgal-engine-manifest-v3
2 parents 5b0fb42 + 0c7fccb commit 1a74acd

13 files changed

Lines changed: 212 additions & 90 deletions

File tree

packages/webgal/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "webgal-engine",
3-
"version": "4.5.18",
3+
"version": "4.5.19",
44
"scripts": {
55
"dev": "vite --host --port 3000",
66
"build": "node scripts/update-engine-version.js && cross-env NODE_ENV=production tsc && vite build --base=./",

packages/webgal/public/webgal-engine.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
"schemaVersion": "1.0.0",
33
"id": "open-webgal.webgal",
44
"name": "WebGAL",
5-
"version": "4.5.18",
5+
"version": "4.5.19",
66
"type": "official",
7-
"webgalVersion": "4.5.18",
7+
"webgalVersion": "4.5.19",
88
"description": "界面美观、功能强大、易于开发的全新网页端视觉小说引擎",
99
"descriptions": {
1010
"en": "A brand new web Visual Novel engine with a beautiful interface, powerful features, and easy development",

packages/webgal/public/webgal-serviceworker.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,33 @@ function isCriticalGameRequest(request) {
3434
return CRITICAL_PATHS.some((prefix) => url.pathname.startsWith(prefix));
3535
}
3636

37-
async function cacheFirst(request) {
37+
// Stale-while-revalidate: return cached response immediately, then update cache in background.
38+
async function staleWhileRevalidate(request) {
3839
const cache = await caches.open(CACHE_NAME);
3940
const cached = await cache.match(request.url);
41+
42+
const fetchAndUpdate = async () => {
43+
try {
44+
const response = await fetch(request);
45+
if (response.ok) {
46+
await cache.put(request.url, response.clone());
47+
}
48+
return response;
49+
} catch (e) {
50+
return null;
51+
}
52+
};
53+
4054
if (cached) {
41-
logOnce(`hit:${request.url}`, 'cache hit:', new URL(request.url).pathname);
55+
logOnce(`hit:${request.url}`, 'cache hit (revalidating):', new URL(request.url).pathname);
56+
// Revalidate in background — don't await
57+
fetchAndUpdate();
4258
return cached;
4359
}
4460

61+
// No cache — must wait for network
4562
const response = await fetch(request);
46-
if (response.ok && response.status === 200) {
63+
if (response.ok) {
4764
await cache.put(request.url, response.clone());
4865
logOnce(`cache:${request.url}`, 'cached:', new URL(request.url).pathname);
4966
}
@@ -62,7 +79,7 @@ self.addEventListener('fetch', (event) => {
6279
}
6380

6481
event.respondWith(
65-
cacheFirst(request).catch(() => {
82+
staleWhileRevalidate(request).catch(() => {
6683
return fetch(request);
6784
}),
6885
);

packages/webgal/src/Core/controller/stage/pixi/PixiController.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,62 @@ export default class PixiStage {
859859
}
860860
}
861861

862+
public changeSpineSkinByKey(key: string, skin: string) {
863+
if (!skin) return;
864+
865+
const target = this.figureObjects.find((e) => e.key === key && !e.isExiting);
866+
if (target?.sourceType !== 'spine') return;
867+
868+
const container = target.pixiContainer;
869+
if (!container) return;
870+
const sprite = container.children[0] as PIXI.Container;
871+
if (sprite?.children?.[0]) {
872+
const spineObject = sprite.children[0];
873+
// @ts-ignore
874+
const skeleton = spineObject.skeleton;
875+
// @ts-ignore
876+
const skeletonData = skeleton?.data ?? spineObject.spineData;
877+
const skinObject =
878+
// @ts-ignore
879+
skeletonData?.findSkin?.(skin) ??
880+
// @ts-ignore
881+
skeletonData?.skins?.find((item: any) => item.name === skin);
882+
883+
if (!skeleton || !skinObject) {
884+
logger.warn(`Spine skin not found: ${skin} on ${key}`);
885+
return;
886+
}
887+
888+
try {
889+
// @ts-ignore
890+
if (typeof skeleton.setSkinByName === 'function') {
891+
// @ts-ignore
892+
skeleton.setSkinByName(skin);
893+
} else {
894+
// @ts-ignore
895+
skeleton.setSkin(skinObject);
896+
}
897+
} catch (error) {
898+
// @ts-ignore
899+
skeleton.setSkin?.(skinObject);
900+
}
901+
902+
// @ts-ignore
903+
if (typeof skeleton.setSlotsToSetupPose === 'function') {
904+
// @ts-ignore
905+
skeleton.setSlotsToSetupPose();
906+
} else {
907+
// @ts-ignore
908+
skeleton.setupPoseSlots?.();
909+
}
910+
911+
// @ts-ignore
912+
spineObject.state?.apply?.(skeleton);
913+
// @ts-ignore
914+
skeleton.updateWorldTransform?.();
915+
}
916+
}
917+
862918
public changeModelExpressionByKey(key: string, expression: string) {
863919
// logger.debug(`Applying expression ${expression} to ${key}`);
864920
const target = this.figureObjects.find((e) => e.key === key && !e.isExiting);

packages/webgal/src/Core/controller/stage/pixi/spine.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,13 @@ export async function addSpineFigureImpl(
111111
figureSpine.pivot.set(spineCenterX, spineCenterY);
112112
figureSpine.interactive = false;
113113

114-
// 检查状态中是否有指定的动画
115114
const motionFromState = webgalStore.getState().stage.live2dMotion.find((e) => e.target === key);
116115
let animationToPlay = '';
116+
if (motionFromState?.skin) {
117+
if (!applySpineSkin(figureSpine, motionFromState.skin)) {
118+
logger.warn(`Spine skin not found: ${motionFromState.skin} on ${key}`);
119+
}
120+
}
117121

118122
if (
119123
motionFromState &&
@@ -277,3 +281,40 @@ export async function addSpineBgImpl(this: PixiStage, key: string, url: string)
277281
await setup();
278282
}
279283
}
284+
285+
function applySpineSkin(spineObject: any, skinName: string) {
286+
// @ts-ignore
287+
const skeleton = spineObject.skeleton;
288+
// @ts-ignore
289+
const skeletonData = skeleton?.data ?? spineObject.spineData;
290+
const skin =
291+
// @ts-ignore
292+
skeletonData?.findSkin?.(skinName) ??
293+
// @ts-ignore
294+
skeletonData?.skins?.find((item: any) => item.name === skinName);
295+
if (!skeleton || !skin) {
296+
return false;
297+
}
298+
try {
299+
// @ts-ignore
300+
if (typeof skeleton.setSkinByName === 'function') {
301+
// @ts-ignore
302+
skeleton.setSkinByName(skinName);
303+
} else {
304+
// @ts-ignore
305+
skeleton.setSkin(skin);
306+
}
307+
} catch (error) {
308+
// @ts-ignore
309+
skeleton.setSkin?.(skin);
310+
}
311+
// @ts-ignore
312+
if (typeof skeleton.setSlotsToSetupPose === 'function') {
313+
// @ts-ignore
314+
skeleton.setSlotsToSetupPose();
315+
} else {
316+
// @ts-ignore
317+
skeleton.setupPoseSlots?.();
318+
}
319+
return true;
320+
}

packages/webgal/src/Core/gameScripts/changeFigure.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function changeFigure(sentence: ISentence): IPerform {
5252

5353
// live2d 或 spine 相关
5454
let motion = getStringArgByKey(sentence, 'motion') ?? '';
55+
const skin = getStringArgByKey(sentence, 'skin') ?? '';
5556
let expression = getStringArgByKey(sentence, 'expression') ?? '';
5657
const boundsFromArgs = getStringArgByKey(sentence, 'bounds') ?? '';
5758
let bounds = getOverrideBoundsArr(boundsFromArgs);
@@ -234,7 +235,7 @@ export function changeFigure(sentence: ISentence): IPerform {
234235
focus = focus ?? cloneDeep(baseFocusParam);
235236
zIndex = Math.max(zIndex, 0);
236237
blendMode = blendMode ?? 'normal';
237-
dispatch(stageActions.setLive2dMotion({ target: key, motion, overrideBounds: bounds }));
238+
dispatch(stageActions.setLive2dMotion({ target: key, motion, skin, overrideBounds: bounds }));
238239
dispatch(stageActions.setLive2dExpression({ target: key, expression }));
239240
dispatch(stageActions.setLive2dBlink({ target: key, blink }));
240241
dispatch(stageActions.setLive2dFocus({ target: key, focus }));
@@ -243,8 +244,8 @@ export function changeFigure(sentence: ISentence): IPerform {
243244
} else {
244245
// 当 url 没有发生变化时,即没有新立绘替换
245246
// 应当保留旧立绘的状态,仅在需要时更新
246-
if (motion || bounds) {
247-
dispatch(stageActions.setLive2dMotion({ target: key, motion, overrideBounds: bounds }));
247+
if (motion || skin || bounds) {
248+
dispatch(stageActions.setLive2dMotion({ target: key, motion, skin, overrideBounds: bounds }));
248249
}
249250
if (expression) {
250251
dispatch(stageActions.setLive2dExpression({ target: key, expression }));

packages/webgal/src/Core/gameScripts/getUserInput/index.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { getStringArgByKey } from '@/Core/util/getSentenceArg';
1313
import { nextSentence } from '@/Core/controller/gamePlay/nextSentence';
1414
import { setStageVar } from '@/store/stageReducer';
1515
import { getCurrentFontFamily } from '@/hooks/useFontFamily';
16+
import { logger } from '@/Core/util/logger';
17+
import { tryToRegex } from '@/Core/util/global';
18+
import { showGlogalDialog } from '@/UI/GlobalDialog/GlobalDialog';
1619

1720
/**
1821
* 显示选择枝
@@ -26,6 +29,10 @@ export const getUserInput = (sentence: ISentence): IPerform => {
2629
let buttonText = getStringArgByKey(sentence, 'buttonText') ?? '';
2730
buttonText = buttonText === '' ? 'OK' : buttonText;
2831
const defaultValue = getStringArgByKey(sentence, 'defaultValue');
32+
const rule = getStringArgByKey(sentence, 'rule');
33+
const ruleFlag = getStringArgByKey(sentence, 'ruleFlag');
34+
const ruleText = getStringArgByKey(sentence, 'ruleText');
35+
const ruleButtonText = getStringArgByKey(sentence, 'ruleButtonText') ?? 'OK';
2936

3037
const font = getCurrentFontFamily();
3138

@@ -39,6 +46,20 @@ export const getUserInput = (sentence: ISentence): IPerform => {
3946
onMouseEnter={playSeEnter}
4047
onClick={() => {
4148
const userInput: HTMLInputElement = document.getElementById('user-input') as HTMLInputElement;
49+
if (rule) {
50+
const reg = tryToRegex(rule, ruleFlag);
51+
if (reg && !reg.test(userInput.value)) {
52+
if (ruleText)
53+
showGlogalDialog({
54+
title: ruleText.replaceAll(/\$0/g, userInput.value),
55+
leftText: ruleButtonText,
56+
});
57+
return;
58+
}
59+
if (!reg) {
60+
logger.warn(`getUserInput: rule ${rule} is not a valid regex`);
61+
}
62+
}
4263
if (userInput) {
4364
webgalStore.dispatch(
4465
setStageVar({
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function tryToRegex(str: string, flag: string | null): RegExp | false {
2+
try {
3+
return new RegExp(str, flag || '');
4+
} catch (e) {
5+
return false;
6+
}
7+
}

packages/webgal/src/Stage/MainStage/useSetFigure.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export function useSetFigure(stageState: IStageState) {
2626
*/
2727
useEffect(() => {
2828
for (const motion of live2dMotion) {
29+
if (motion.skin) {
30+
WebGAL.gameplay.pixiStage?.changeSpineSkinByKey(motion.target, motion.skin);
31+
}
2932
WebGAL.gameplay.pixiStage?.changeModelMotionByKey(motion.target, motion.motion);
3033
}
3134
}, [live2dMotion]);

packages/webgal/src/UI/GlobalDialog/GlobalDialog.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,22 @@ export default function GlobalDialog() {
1313
interface IShowGlobalDialogProps {
1414
title: string;
1515
leftText: string;
16-
rightText: string;
17-
leftFunc: Function;
18-
rightFunc: Function;
16+
rightText?: string;
17+
leftFunc?: Function;
18+
rightFunc?: Function;
1919
}
2020

2121
export function showGlogalDialog(props: IShowGlobalDialogProps) {
2222
const { playSeClick, playSeEnter } = useSEByWebgalStore();
2323
webgalStore.dispatch(setVisibility({ component: 'showGlobalDialog', visibility: true }));
2424
const handleLeft = () => {
2525
playSeClick();
26-
props.leftFunc();
26+
props.leftFunc?.();
2727
hideGlobalDialog();
2828
};
2929
const handleRight = () => {
3030
playSeClick();
31-
props.rightFunc();
31+
props.rightFunc?.();
3232
hideGlobalDialog();
3333
};
3434
const renderElement = (
@@ -37,12 +37,16 @@ export function showGlogalDialog(props: IShowGlobalDialogProps) {
3737
<div className={styles.glabalDialog_container_inner}>
3838
<div className={styles.title}>{props.title}</div>
3939
<div className={styles.button_list}>
40-
<div className={styles.button} onClick={handleLeft} onMouseEnter={playSeEnter}>
41-
{props.leftText}
42-
</div>
43-
<div className={styles.button} onClick={handleRight} onMouseEnter={playSeEnter}>
44-
{props.rightText}
45-
</div>
40+
{props.leftText && (
41+
<div className={styles.button} onClick={handleLeft} onMouseEnter={playSeEnter}>
42+
{props.leftText}
43+
</div>
44+
)}
45+
{props.rightText && (
46+
<div className={styles.button} onClick={handleRight} onMouseEnter={playSeEnter}>
47+
{props.rightText}
48+
</div>
49+
)}
4650
</div>
4751
</div>
4852
</div>

0 commit comments

Comments
 (0)