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
1 change: 1 addition & 0 deletions packages/webgal/public/game/config.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Title_img:WebGAL_New_Enter_Image.webp;
Title_bgm:s_Title.mp3;
Game_Logo:WebGalEnter.webp;
Enable_Appreciation:true;
Enable_Continue:true;
2 changes: 2 additions & 0 deletions packages/webgal/src/Core/controller/gamePlay/backToTitle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { stopAuto } from '@/Core/controller/gamePlay/autoPlay';
import { stopFast } from '@/Core/controller/gamePlay/fastSkip';
import { setEbg } from '@/Core/gameScripts/changeBg/setEbg';
import { stageStateManager } from '@/Core/Modules/stage/stageStateManager';
import { removeFastSaveGameRecord } from '../storage/fastSaveLoad';

export const backToTitle = () => {
if (webgalStore.getState().GUI.showTitle) return;
const dispatch = webgalStore.dispatch;
removeFastSaveGameRecord();
stopAllPerform();
stopAuto();
stopFast();
Expand Down
10 changes: 7 additions & 3 deletions packages/webgal/src/Core/controller/storage/fastSaveLoad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export function initKey() {
* 用于紧急回避时的数据存储 & 快速保存
*/
export async function fastSaveGame() {
const showTitle = webgalStore.getState().GUI.showTitle;
if (showTitle) {
// 如果在标题界面,不进行快速保存
return;
}
const saveData: ISaveData = generateCurrentStageData(-1, false);
const newSaveData = cloneDeep(saveData);
webgalStore.dispatch(saveActions.setFastSave(newSaveData));
Expand All @@ -48,6 +53,7 @@ export async function loadFastSaveGame() {
if (!loadFile) {
return;
}
removeFastSaveGameRecord();
loadGameFromStageData(loadFile);
}

Expand All @@ -56,7 +62,5 @@ export async function loadFastSaveGame() {
*/
export async function removeFastSaveGameRecord() {
webgalStore.dispatch(saveActions.resetFastSave());
await setStorageAsync();
// await localforage.setItem(isFastSaveKey, false);
// await localforage.setItem(fastSaveGameKey, null);
await dumpFastSaveToStorage();
}
1 change: 1 addition & 0 deletions packages/webgal/src/Core/controller/storage/saveGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function generateCurrentStageData(index: number, isSavePreviewImage = tru
backlog: saveBacklog, // 舞台数据
index: index, // 存档的序号
saveTime: new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString('chinese', { hour12: false }), // 保存时间
saveTimestamp: Date.now(),
// 场景数据
sceneData: {
currentSentenceId: WebGAL.sceneManager.sceneData.currentSentenceId, // 当前语句ID
Expand Down
20 changes: 20 additions & 0 deletions packages/webgal/src/Core/controller/storage/savesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { webgalStore } from '@/store/store';
import { saveActions } from '@/store/savesReducer';
import { ISaveData } from '@/store/userDataInterface';

export const MAX_SAVE_SIZE = 200;

export function dumpSavesToStorage(startIndex: number, endIndex: number) {
for (let i = startIndex; i <= endIndex; i++) {
const save = webgalStore.getState().saveData.saveData[i];
Expand All @@ -23,6 +25,24 @@ export function getSavesFromStorage(startIndex: number, endIndex: number) {
}
}

function getSaveSortTime(save: ISaveData) {
if (typeof save.saveTimestamp === 'number') {
return save.saveTimestamp;
}
const parsedTime = Date.parse(save.saveTime);
return Number.isNaN(parsedTime) ? 0 : parsedTime;
}

export function getLatestSaveData(saveDatas: Array<ISaveData | null | undefined>) {
return saveDatas
.filter((save): save is ISaveData => Boolean(save))
.sort((a, b) => {
const timeDiff = getSaveSortTime(b) - getSaveSortTime(a);
if (timeDiff !== 0) return timeDiff;
return b.index - a.index;
})[0];
}

export async function dumpFastSaveToStorage() {
const save = webgalStore.getState().saveData.quickSaveData;
await localforage.setItem(`${WebGAL.gameKey}-saves-fast`, save);
Expand Down
4 changes: 2 additions & 2 deletions packages/webgal/src/Core/gameScripts/end.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { setVisibility } from '@/store/GUIReducer';
import { playBgm } from '@/Core/controller/stage/playBgm';
import { WebGAL } from '@/Core/WebGAL';
import { dumpToStorageFast } from '@/Core/controller/storage/storageController';
import { saveActions } from '@/store/savesReducer';
import { removeFastSaveGameRecord } from '../controller/storage/fastSaveLoad';

/**
* 结束游戏
Expand All @@ -24,7 +24,7 @@ export const end = (sentence: ISentence): IPerform => {
setTimeout(() => {
WebGAL.sceneManager.resetScene();
}, 5);
dispatch(saveActions.resetFastSave());
removeFastSaveGameRecord();
dumpToStorageFast();
sceneFetcher(sceneUrl).then((rawScene) => {
// 场景写入到运行时
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Live2D, WebGAL } from '@/Core/WebGAL';
import { WebgalParser } from '@/Core/parser/sceneParser';
import { getStorageAsync, setStorage } from '@/Core/controller/storage/storageController';
import { initKey } from '@/Core/controller/storage/fastSaveLoad';
import { getFastSaveFromStorage, getSavesFromStorage } from '@/Core/controller/storage/savesController';
import { getFastSaveFromStorage, getSavesFromStorage, MAX_SAVE_SIZE } from '@/Core/controller/storage/savesController';
import { logger } from '@/Core/util/logger';
import axios from 'axios';
import { IGameVar } from '@/Core/Modules/stage/stageInterface';
Expand All @@ -26,7 +26,7 @@ export const infoFetcher = (url: string): Promise<IGameVar> => {
initKey();
await getStorageAsync();
getFastSaveFromStorage();
getSavesFromStorage(0, 0);
getSavesFromStorage(0, MAX_SAVE_SIZE);
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

这里存在一个差一错误 (off-by-one error)。如果 MAX_SAVE_SIZE 为 200,且 getSavesFromStorage 内部使用 i <= endIndex 进行循环,则会处理 201 个存档位(0 到 200)。建议将结束索引改为 MAX_SAVE_SIZE - 1

Suggested change
getSavesFromStorage(0, MAX_SAVE_SIZE);
getSavesFromStorage(0, MAX_SAVE_SIZE - 1);

// 存储 config.txt 中的配置,用于清除所有数据时还原配置
const gameConfigInit: IGameVar = {};
// 按照游戏的配置开始设置对应的状态
Expand Down
56 changes: 44 additions & 12 deletions packages/webgal/src/UI/Title/Title.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '@/store/store';
import { fullScreenOption } from '@/store/userDataInterface';
Expand All @@ -9,13 +10,17 @@ import useApplyStyle from '@/hooks/useApplyStyle';
import { keyboard } from '@/hooks/useHotkey';
import useConfigData from '@/hooks/useConfigData';
import { playBgm } from '@/Core/controller/stage/playBgm';
import { continueGame, startGame } from '@/Core/controller/gamePlay/startContinueGame';
import { startGame } from '@/Core/controller/gamePlay/startContinueGame';
import { loadGameFromStageData } from '@/Core/controller/storage/loadGame';
import { getLatestSaveData } from '@/Core/controller/storage/savesController';
import { showGlogalDialog } from '../GlobalDialog/GlobalDialog';
import styles from './title.module.scss';
import { loadFastSaveGame, removeFastSaveGameRecord } from '@/Core/controller/storage/fastSaveLoad';

/** 标题页 */
export default function Title() {
const userDataState = useSelector((state: RootState) => state.userData);
const userSaveData = useSelector((state: RootState) => state.saveData);
const GUIState = useSelector((state: RootState) => state.GUI);
const dispatch = useDispatch();
const fullScreen = userDataState.optionData.fullScreen;
Expand All @@ -24,6 +29,11 @@ export default function Title() {
const t = useTrans('title.');
const tCommon = useTrans('common.');
const { playSeEnter, playSeClick } = useSoundEffect();
const saveData = userSaveData.saveData;
const fastSaveData = userSaveData.quickSaveData;
const enableContinue = userDataState.globalGameVar.Enable_Continue !== false;
const latestSave = useMemo(() => getLatestSaveData(saveData), [saveData]);
const [fastSaveLoaded, setFastSaveLoaded] = useState(false);

const applyStyle = useApplyStyle('title');
useConfigData(); // 监听基础ConfigData变化
Expand All @@ -38,6 +48,24 @@ export default function Title() {
</div>
);

useEffect(() => {
if (!fastSaveLoaded && (!GUIState.isShowLogo || GUIState.logoImage.length === 0) && GUIState.showTitle && fastSaveData) {
showGlogalDialog({
title: t('fastSaveLoadConfirm'),
leftText: tCommon('yes'),
rightText: tCommon('no'),
leftFunc: () => {
loadFastSaveGame();
setFastSaveLoaded(true);
},
Comment on lines +57 to +60
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

与“继续游戏”按钮类似,紧急回避存档恢复对话框在用户选择“是”并加载存档时,也缺少了隐藏标题界面的逻辑。如果不隐藏标题界面,用户将无法看到恢复后的游戏内容。

        leftFunc: () => {
          dispatch(setVisibility({ component: 'showTitle', visibility: false }));
          loadFastSaveGame();
          setFastSaveLoaded(true);
        },

rightFunc: () => {
removeFastSaveGameRecord();
setFastSaveLoaded(true);
}
});
}
}, [GUIState.isShowLogo, GUIState.logoImage, GUIState.showTitle, fastSaveData, fastSaveLoaded]);

return (
<>
{GUIState.showTitle && <div className={applyStyle('Title_backup_background', styles.Title_backup_background)} />}
Expand Down Expand Up @@ -72,17 +100,21 @@ export default function Title() {
>
{renderButtonText(t('start.title'))}
</div>
<div
className={applyStyle('Title_button', styles.Title_button)}
onClick={async () => {
playSeClick();
dispatch(setVisibility({ component: 'showTitle', visibility: false }));
continueGame();
}}
onMouseEnter={playSeEnter}
>
{renderButtonText(t('continue.title'))}
</div>
{enableContinue && (
<div
className={`${applyStyle('Title_button', styles.Title_button)} ${
!latestSave ? applyStyle('Title_button_disabled', styles.Title_button_disabled) : ''
}`}
onClick={() => {
if (!latestSave) return;
playSeClick();
loadGameFromStageData(latestSave);
}}
Comment on lines +108 to +112
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

“继续游戏”按钮逻辑中缺少了隐藏标题界面的 dispatch 调用。这会导致在加载存档数据后,标题界面仍然显示在最上层,用户无法看到游戏画面。建议添加 dispatch(setVisibility({ component: 'showTitle', visibility: false }));

Suggested change
onClick={() => {
if (!latestSave) return;
playSeClick();
loadGameFromStageData(latestSave);
}}
onClick={() => {
if (!latestSave) return;
playSeClick();
dispatch(setVisibility({ component: 'showTitle', visibility: false }));
loadGameFromStageData(latestSave);
}}

onMouseEnter={latestSave ? playSeEnter : undefined}
>
{renderButtonText(t('continue.title'))}
</div>
)}
<div
className={applyStyle('Title_button', styles.Title_button)}
onClick={() => {
Expand Down
4 changes: 2 additions & 2 deletions packages/webgal/src/hooks/useConfigData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getFastSaveFromStorage, getSavesFromStorage } from '@/Core/controller/storage/savesController';
import { getFastSaveFromStorage, getSavesFromStorage, MAX_SAVE_SIZE } from '@/Core/controller/storage/savesController';
import { getStorage } from '@/Core/controller/storage/storageController';
import { setEbg } from '@/Core/gameScripts/changeBg/setEbg';
import { assetSetter, fileType } from '@/Core/util/gameAssetsAccess/assetSetter';
Expand Down Expand Up @@ -49,7 +49,7 @@ const useConfigData = () => {
WebGAL.gameKey = val;
getStorage();
getFastSaveFromStorage();
getSavesFromStorage(0, 0);
getSavesFromStorage(0, MAX_SAVE_SIZE);
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

同样存在差一错误 (off-by-one error)。建议将结束索引改为 MAX_SAVE_SIZE - 1 以确保只加载预期的 200 个存档位。

Suggested change
getSavesFromStorage(0, MAX_SAVE_SIZE);
getSavesFromStorage(0, MAX_SAVE_SIZE - 1);

break;
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/store/userDataInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface ISaveData {
backlog: Array<IBacklogItem>; // 舞台数据
index: number; // 存档的序号
saveTime: string; // 保存时间
saveTimestamp?: number; // 保存时间戳,用于排序
sceneData: ISaveScene; // 场景数据
previewImage: string;
}
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ const de = {
subtitle: '',
tips: 'Sind Sie sicher, dass Sie das Spiel beenden möchten?',
},
fastSaveLoadConfirm: 'Temporärer Fortschritt erkannt. Möchten Sie ihn wiederherstellen?',
},

gaming: {
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ const en = {
subtitle: '',
tips: 'Are you sure you want to exit?',
},
fastSaveLoadConfirm: 'Temporary progress detected. Restore it?',
},

gaming: {
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/translations/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ const fr = {
subtitle: '',
tips: 'Êtes-vous sûr de vouloir quitter ?',
},
fastSaveLoadConfirm: 'Une progression temporaire a été détectée. La restaurer ?',
},

gaming: {
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/translations/jp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ const jp = {
subtitle: 'EXIT',
tips: 'ゲームを終了しますか?',
},
fastSaveLoadConfirm: '一時保存された進行状況を検出しました。復元しますか?',
},

gaming: {
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/translations/pt-br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ const ptBr = {
subtitle: '',
tips: 'Deseja realmente sair?',
},
fastSaveLoadConfirm: 'Progresso temporário detectado. Deseja restaurá-lo?',
},

gaming: {
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/translations/zh-cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const zhCn = {
subtitle: 'EXIT',
tips: '确定要退出游戏吗?',
},
fastSaveLoadConfirm: '检测到快速回避存档,是否加载?',
},

gaming: {
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/translations/zh-tw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const zhTw = {
subtitle: 'EXIT',
tips: '確定要退出遊戲嗎?',
},
fastSaveLoadConfirm: '偵測到暫存進度,是否恢復?',
},

gaming: {
Expand Down
Loading