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
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,7 @@
.text {
overflow: hidden;
}

.read {
color: rgb(237, 162, 162);
}
118 changes: 118 additions & 0 deletions packages/webgal/src/Core/Modules/readHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* 已读历史记录
*/

import { webgalStore } from "@/store/store";
import { SceneManager } from "./scene";
import { setReadHistory } from "@/store/userDataReducer";
import { setStage } from "@/store/stageReducer";
import { setStorage } from "../controller/storage/storageController";

export class ReadHistoryManager {
private history: Map<string, Uint8Array> = new Map();

private load: boolean = false;

private readonly sceneManager: SceneManager;

public constructor(sceneManager: SceneManager) {
this.sceneManager = sceneManager;
}

private loadReadHistory() {
const readHistory = webgalStore.getState().userData.readHistory;

Object.entries(readHistory).forEach(([key, value]) => {
try {
const uint8 = Uint8Array.from(Buffer.from(value, 'base64'));
this.history.set(key, uint8);
} catch {
// 浏览器环境下没有 Buffer 时的兜底逻辑
const binary = atob(value);
const uint8 = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
uint8[i] = binary.charCodeAt(i);
}
this.history.set(key, uint8);
}
});

this.load = true;
}

private checkLoad() {
if (!this.load) {
this.loadReadHistory();
}
}

private saveReadHistory(key: string) {
const bitset = this.history.get(key)!;

try {
const base64 = Buffer.from(bitset).toString('base64');
webgalStore.dispatch(setReadHistory({
key,
value: base64,
}));
} catch {
// 浏览器环境下没有 Buffer 时的兜底逻辑
let binary = '';
const len = bitset.length;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bitset[i]);
}
const base64 = btoa(binary);
webgalStore.dispatch(setReadHistory({
key,
value: base64,
}));
}
setStorage();
}

private addReadHistory() {
const scenarioName = this.sceneManager.sceneData.currentScene.sceneName;
const index = this.sceneManager.sceneData.currentSentenceId;

if (!this.history.has(scenarioName)) {
const length = this.sceneManager.sceneData.currentScene.sentenceList.length;
this.history.set(scenarioName, new Uint8Array(Math.ceil(length / 8)));
}
let bitset = this.history.get(scenarioName)!;

// 处理因剧本更新可能导致的 index 溢出问题
const requiredIndex = index >> 3;
if (requiredIndex >= bitset.length) {
const length = this.sceneManager.sceneData.currentScene.sentenceList.length;
const newBitset = new Uint8Array(Math.ceil(length / 8));
newBitset.set(bitset);
bitset = newBitset;
this.history.set(scenarioName, bitset);
}

bitset[requiredIndex] |= (1 << (index & 7));

this.saveReadHistory(scenarioName);
}

public checkIsRead() {
this.checkLoad();

const scenarioName = this.sceneManager.sceneData.currentScene.sceneName;
const index = this.sceneManager.sceneData.currentSentenceId;
Comment on lines +102 to +103
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

If scenarioName is an empty string (which can happen during initialization or transition), the manager will attempt to record history for an invalid key. It's better to add a guard clause.

Suggested change
const scenarioName = this.sceneManager.sceneData.currentScene.sceneName;
const index = this.sceneManager.sceneData.currentSentenceId;
const scenarioName = this.sceneManager.sceneData.currentScene.sceneName;
if (!scenarioName) return;
const index = this.sceneManager.sceneData.currentSentenceId;


let isRead = false;
if (this.history.has(scenarioName)) {
const bitset = this.history.get(scenarioName)!;
isRead = (bitset[index >> 3] & (1 << (index & 7))) !== 0;
}
webgalStore.dispatch(setStage({
key: 'isRead',
value: isRead,
}));
if (!isRead) {
this.addReadHistory();
}
}
}
7 changes: 6 additions & 1 deletion packages/webgal/src/Core/controller/gamePlay/fastSkip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styles from '@/UI/BottomControlPanel/bottomControlPanel.module.scss';
import { nextSentence } from '@/Core/controller/gamePlay/nextSentence';

import { WebGAL } from '@/Core/WebGAL';
import { webgalStore } from "@/store/store";
import { SYSTEM_CONFIG } from '@/config';

/**
Expand Down Expand Up @@ -36,12 +37,16 @@ export const stopFast = () => {
/**
* 开启快进
*/
export const startFast = () => {
export const startFast = (force = false) => {
if (isFast()) {
return;
}
WebGAL.gameplay.isFast = true;
const skipAll = force || webgalStore.getState().userData.optionData.skipAll;
WebGAL.gameplay.fastInterval = setInterval(() => {
if (!skipAll && !webgalStore.getState().stage.isRead) {
stopFast();
}
nextSentence();
}, SYSTEM_CONFIG.fast_timeout);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export const scriptExecutor = () => {
nextSentence();
return;
}
WebGAL.readHistoryManager.checkIsRead();
runScript(currentScript);
// 是否要进行下一句
let isNext = getBooleanArgByKey(currentScript, 'next') ?? false;
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/Core/controller/stage/resetStage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { initState, resetStageState, setStage } from '@/store/stageReducer';
import { webgalStore } from '@/store/store';
import cloneDeep from 'lodash/cloneDeep';
import { WebGAL } from '@/Core/WebGAL';
import { saveActions } from '@/store/savesReducer';

export const resetStage = (resetBacklog: boolean, resetSceneAndVar = true) => {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export const jumpFromBacklog = (index: number, refetchScene = true) => {
// 恢复舞台状态
const newStageState: IStageState = cloneDeep(backlogFile.currentStageState);

// 确保原先未读的文本在使用 backlog 时能正确显示为已读文本
newStageState.isRead = true;

dispatch(resetStageState(newStageState));

// 恢复演出
Expand Down
2 changes: 2 additions & 0 deletions packages/webgal/src/Core/controller/storage/loadGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export function loadGameFromStageData(stageData: ISaveData) {

// 恢复舞台状态
const newStageState = cloneDeep(loadFile.nowStageState);
// 确保原先未读的文本在 load 时能正确显示为已读文本
newStageState.isRead = true;
const dispatch = webgalStore.dispatch;
dispatch(resetStageState(newStageState));

Expand Down
2 changes: 2 additions & 0 deletions packages/webgal/src/Core/webgalCore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BacklogManager } from '@/Core/Modules/backlog';
import { ReadHistoryManager } from './Modules/readHistory';
import mitt from 'mitt';
import { SceneManager } from '@/Core/Modules/scene';
import { AnimationManager } from '@/Core/Modules/animations';
Expand All @@ -11,6 +12,7 @@ import { IWebGALStyleObj } from 'webgal-parser/build/types/styleParser';
export class WebgalCore {
public sceneManager = new SceneManager();
public backlogManager = new BacklogManager(this.sceneManager);
public readHistoryManager = new ReadHistoryManager(this.sceneManager);
public animationManager = new AnimationManager();
public gameplay = new Gameplay();
public gameName = '';
Expand Down
3 changes: 2 additions & 1 deletion packages/webgal/src/Stage/TextBox/IMSSTextbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default function IMSSTextbox(props: ITextboxProps) {
textDelay,
currentConcatDialogPrev,
currentDialogKey,
isRead,
isText,
isSafari,
isFirefox: boolean,
Expand Down Expand Up @@ -167,7 +168,7 @@ export default function IMSSTextbox(props: ITextboxProps) {
>
<span className={styles.zhanwei + styleAllText}>
{e}
<span className={applyStyle('outer', styles.outer) + styleClassName + styleAllText}>{e}</span>
<span className={applyStyle('outer', styles.outer) + `${isRead ? ` ${applyStyle('read', styles.read)}` : ''}` + styleClassName + styleAllText}>{e}</span>
{isUseStroke && <span className={applyStyle('inner', styles.inner) + styleAllText}>{e}</span>}
</span>
</span>
Expand Down
2 changes: 2 additions & 0 deletions packages/webgal/src/Stage/TextBox/TextBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const TextBox = () => {
const textDuration = useTextAnimationDuration(userDataState.optionData.textSpeed);
let size = getTextSize(userDataState.optionData.textSize) + '%';
const font = useFontFamily();
const isRead = stageState.isRead;
const isText = stageState.showText !== '' || stageState.showName !== '';
let textSizeState = userDataState.optionData.textSize;
if (isText && stageState.showTextSize !== -1) {
Expand Down Expand Up @@ -88,6 +89,7 @@ export const TextBox = () => {
return (
<Textbox
textArray={textArray}
isRead={isRead}
isText={isText}
textDelay={textDelay}
showName={showName}
Expand Down
4 changes: 4 additions & 0 deletions packages/webgal/src/Stage/TextBox/textbox.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,7 @@ $height: 330px;
//line-height: 2em;
overflow: hidden;
}

.read {
color: rgb(237, 162, 162);
}
1 change: 1 addition & 0 deletions packages/webgal/src/Stage/TextBox/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface ITextboxProps {
textDelay: number;
currentConcatDialogPrev: string;
currentDialogKey: string;
isRead: boolean;
isText: boolean;
isSafari: boolean;
isFirefox: boolean;
Expand Down
13 changes: 13 additions & 0 deletions packages/webgal/src/UI/Menu/Options/System/System.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ export function System() {
}}
/>
</NormalOption>
<NormalOption key="skipAll" title={t('skipAll.title')}>
<NormalButton
textList={t('skipAll.options.read', 'skipAll.options.all')}
functionList={[() => {
dispatch(setOptionData({ key: 'skipAll', value: false }));
setStorage();
}, () => {
dispatch(setOptionData({ key: 'skipAll', value: true }));
setStorage();
}]}
currentChecked={userDataState.optionData.skipAll ? 1 : 0}
/>
</NormalOption>
<NormalOption key="option7" title={t('language.title')}>
<NormalButton
currentChecked={userDataState.optionData.language}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const TextPreview = (props: any) => {
lineLimit: 3,
isUseStroke: true,
textboxOpacity: textboxOpacity,
isRead: false,
};

return (
Expand Down
3 changes: 2 additions & 1 deletion packages/webgal/src/hooks/useHotkey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ export function useSkip() {
const isCtrlKey = useCallback((e) => e.keyCode === 17, []);
const handleCtrlKeydown = useCallback((e) => {
if (isCtrlKey(e) && isGameActive()) {
startFast();
// 按下 ctrl 键快进时,强制全文快进
startFast(true);
}
}, []);
const handleCtrlKeyup = useCallback((e) => {
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 @@ -215,6 +215,7 @@ export interface IStageState {
// 自由立绘
freeFigure: Array<IFreeFigure>;
figureAssociatedAnimation: Array<IFigureAssociatedAnimation>;
isRead: boolean; // 是否已读
showText: string; // 文字
showTextSize: number; // 文字
showName: string; // 人物名
Expand Down
1 change: 1 addition & 0 deletions packages/webgal/src/store/stageReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const initState: IStageState = {
figNameRight: '', // 立绘_右 文件地址(相对或绝对)
freeFigure: [],
figureAssociatedAnimation: [],
isRead: false,
showText: '', // 文字
showTextSize: -1,
showName: '', // 人物名
Expand Down
2 changes: 2 additions & 0 deletions packages/webgal/src/store/userDataInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface IOptionData {
language: language;
voiceInterruption: voiceOption; // 是否中断语音
fullScreen: fullScreenOption;
skipAll: boolean; // 快进已读/快进全文
}

/**
Expand Down Expand Up @@ -91,6 +92,7 @@ export interface IUserData {
optionData: IOptionData; // 用户设置选项数据
appreciationData: IAppreciation;
gameConfigInit: IGameVar;
readHistory: Record<string, string>;
}

export interface ISetUserDataPayload {
Expand Down
6 changes: 6 additions & 0 deletions packages/webgal/src/store/userDataReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const initialOptionSet: IOptionData = {
language: language.zhCn,
voiceInterruption: voiceOption.no,
fullScreen: fullScreenOption.off,
skipAll: false,
};

// 初始化用户数据
Expand All @@ -47,6 +48,7 @@ export const initState: IUserData = {
cg: [],
},
gameConfigInit: {},
readHistory: {},
};

const userDataSlice = createSlice({
Expand Down Expand Up @@ -142,6 +144,9 @@ const userDataSlice = createSlice({
const { gameConfigInit } = state;
Object.assign(state, { ...cloneDeep(initState), globalGameVar: cloneDeep(gameConfigInit), gameConfigInit });
},
setReadHistory: (state, action: PayloadAction<Record<'key' | 'value', string>>) => {
state.readHistory[action.payload.key] = action.payload.value;
},
},
});

Expand All @@ -156,6 +161,7 @@ export const {
unlockBgmInUserData,
resetOptionSet,
resetAllData,
setReadHistory,
} = userDataSlice.actions;
export default userDataSlice.reducer;

Expand Down
7 changes: 7 additions & 0 deletions packages/webgal/src/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ const de = {
contributors: 'Contributors',
website: 'Website',
},
skipAll: {
title: 'Schnellvorlauf-Modus',
options: {
read: 'Gelesen',
all: 'Alle',
}
}
},
},
display: {
Expand Down
7 changes: 7 additions & 0 deletions packages/webgal/src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ const en = {
contributors: 'Contributors',
website: 'Website',
},
skipAll: {
title: 'Skip Mode',
options: {
read: 'Read',
all: 'All',
}
}
},
},
display: {
Expand Down
7 changes: 7 additions & 0 deletions packages/webgal/src/translations/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ const fr = {
contributors: 'Contributeurs',
website: 'Site web',
},
skipAll: {
title: 'Mode Avance Rapide',
options: {
read: 'Lu',
all: 'Tout',
}
}
},
},
display: {
Expand Down
Loading
Loading