Skip to content
Merged
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
5 changes: 2 additions & 3 deletions packages/webgal/src/Core/gameScripts/choose/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import ReactDOM from 'react-dom';
import React from 'react';
import styles from './choose.module.scss';
import { webgalStore } from '@/store/store';
import { textFont } from '@/store/userDataInterface';
import { PerformController } from '@/Core/Modules/perform/performController';
import { useSEByWebgalStore } from '@/hooks/useSoundEffect';
import { WebGAL } from '@/Core/WebGAL';
import { whenChecker } from '@/Core/controller/gamePlay/scriptExecutor';
import useEscape from '@/hooks/useEscape';
import useApplyStyle from '@/hooks/useApplyStyle';
import { Provider } from 'react-redux';
import { useFontFamily } from '@/hooks/useFontFamily';

class ChooseOption {
/**
Expand Down Expand Up @@ -81,8 +81,7 @@ export const choose = (sentence: ISentence): IPerform => {
};

function Choose(props: { chooseOptions: ChooseOption[] }) {
const fontFamily = webgalStore.getState().userData.optionData.textboxFont;
const font = fontFamily === textFont.song ? '"思源宋体", serif' : '"WebgalUI", serif';
const font = useFontFamily();
const { playSeEnter, playSeClick } = useSEByWebgalStore();
const applyStyle = useApplyStyle('Stage/Choose/choose.scss');
// 运行时计算JSX.Element[]
Expand Down
5 changes: 2 additions & 3 deletions packages/webgal/src/Core/gameScripts/getUserInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import ReactDOM from 'react-dom';
import React from 'react';
import styles from './getUserInput.module.scss';
import { webgalStore } from '@/store/store';
import { textFont } from '@/store/userDataInterface';
import { PerformController } from '@/Core/Modules/perform/performController';
import { useSEByWebgalStore } from '@/hooks/useSoundEffect';
import { WebGAL } from '@/Core/WebGAL';
import { getStringArgByKey } from '@/Core/util/getSentenceArg';
import { nextSentence } from '@/Core/controller/gamePlay/nextSentence';
import { setStageVar } from '@/store/stageReducer';
import { getCurrentFontFamily } from '@/hooks/useFontFamily';

/**
* 显示选择枝
Expand All @@ -27,8 +27,7 @@ export const getUserInput = (sentence: ISentence): IPerform => {
buttonText = buttonText === '' ? 'OK' : buttonText;
const defaultValue = getStringArgByKey(sentence, 'defaultValue');

const fontFamily = webgalStore.getState().userData.optionData.textboxFont;
const font = fontFamily === textFont.song ? '"思源宋体", serif' : '"WebgalUI", serif';
const font = getCurrentFontFamily();

const { playSeEnter, playSeClick } = useSEByWebgalStore();
const chooseElements = (
Expand Down
2 changes: 2 additions & 0 deletions packages/webgal/src/Core/initializeScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import PixiStage from '@/Core/controller/stage/pixi/PixiController';
import axios from 'axios';
import { __INFO } from '@/config/info';
import { WebGAL } from '@/Core/WebGAL';
import { loadTemplate } from '@/Core/util/coreInitialFunction/templateLoader';

const u = navigator.userAgent;
export const isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); // 判断是否是 iOS终端
Expand All @@ -26,6 +27,7 @@ export const initializeScript = (): void => {
logger.info(`WebGAL v${__INFO.version}`);
logger.info('Github: https://github.com/OpenWebGAL/WebGAL ');
logger.info('Made with ❤ by OpenWebGAL');
loadTemplate();

Choose a reason for hiding this comment

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

high

loadTemplate 是一个异步函数,但在这里它被同步调用(“fire-and-forget”模式)。这可能会导致竞态条件:在 loadTemplate 完成加载模板和字体之前,后续的初始化代码(如 sceneFetcher)可能已经开始执行。如果场景依赖于模板中定义的自定义字体,字体可能无法被正确应用,或导致内容闪烁(FOUT)。为了保证初始化顺序的正确性,建议将 initializeScript 函数修改为 async 函数,并使用 await 等待 loadTemplate 完成。

// 激活强制缩放
// 在调整窗口大小时重新计算宽高,设计稿按照 1600*900。
if (isIOS) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import axios from 'axios';
import { logger } from '@/Core/util/logger';
import { WebGAL } from '@/Core/WebGAL';
import { TemplateFontDescriptor, WebgalTemplate } from '@/types/template';
import { buildFontOptionsFromTemplate } from '@/Core/util/fonts/fontOptions';
import { webgalStore } from '@/store/store';
import { setFontOptions } from '@/store/GUIReducer';
import { setOptionData } from '@/store/userDataReducer';

const TEMPLATE_PATH = './game/template/template.json';
const TEMPLATE_FONT_STYLE_SELECTOR = 'style[data-webgal-template-fonts]';

export async function loadTemplate(): Promise<WebgalTemplate | null> {
try {
const { data } = await axios.get<WebgalTemplate>(TEMPLATE_PATH);
WebGAL.template = data;
const fonts = data.fonts ?? [];
injectTemplateFonts(fonts);
updateFontOptions(fonts);
return data;
} catch (error) {
logger.warn('加载模板文件失败', error);
updateFontOptions([]);
return null;
}
}

function updateFontOptions(fonts: TemplateFontDescriptor[]): void {
const options = buildFontOptionsFromTemplate(fonts);
webgalStore.dispatch(setFontOptions(options));
const currentIndex = webgalStore.getState().userData.optionData.textboxFont ?? 0;
if (options.length === 0) return;
if (currentIndex >= options.length) {
webgalStore.dispatch(setOptionData({ key: 'textboxFont', value: 0 }));
}
}

function injectTemplateFonts(fonts: TemplateFontDescriptor[]): void {
if (!fonts.length) return;
const rules = fonts.map((font) => generateFontFaceRule(font)).filter((rule): rule is string => Boolean(rule));

if (!rules.length) return;

const styleElement = document.createElement('style');
styleElement.setAttribute('data-webgal-template-fonts', 'true');
styleElement.appendChild(document.createTextNode(rules.join('\n')));

const head = document.head;
if (!head) return;

const existing = head.querySelector<HTMLStyleElement>(TEMPLATE_FONT_STYLE_SELECTOR);
existing?.remove();

head.appendChild(styleElement);
}

function generateFontFaceRule(font: TemplateFontDescriptor): string | null {
const fontFamily = font['font-family'];
if (!fontFamily || !font.url || !font.type) {
logger.warn('忽略无效的模板字体配置', font);
return null;
}

const src = resolveTemplateAssetPath(font.url);
const weight = font.weight !== undefined ? `font-weight: ${font.weight};` : '';
const style = font.style ? `font-style: ${font.style};` : '';
const display = `font-display: ${font.display ?? 'swap'};`;

return `@font-face { font-family: '${fontFamily}'; src: url('${src}') format('${font.type}'); ${weight} ${style} ${display} }`;
}

function resolveTemplateAssetPath(path: string): string {
if (/^(https?:)?\/\//i.test(path) || path.startsWith('data:')) {
return path;
}
const normalized = path.replace(/^[./]+/, '');

Choose a reason for hiding this comment

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

medium

resolveTemplateAssetPath 函数中的路径规范化逻辑 path.replace(/^[./]+/, '') 会将以 ../ 开头的路径(例如 ../fonts/myfont.woff)静默地处理成 fonts/myfont.woff。这可能会让模板制作者感到困惑,因为他们可能期望路径能够回溯到上级目录。虽然这可能是出于安全考虑,将资源限制在模板目录内,但这种隐式行为容易引发错误。建议添加一个警告日志,当检测到路径中包含 .. 时通知用户,以避免意外行为。

return `./game/template/${normalized}`;
}
46 changes: 46 additions & 0 deletions packages/webgal/src/Core/util/fonts/fontOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FontOption } from '@/store/guiInterface';
import { TemplateFontDescriptor } from '@/types/template';

export const DEFAULT_FONT_OPTIONS: FontOption[] = [
{
family: `'思源宋体', serif`,
source: 'default',
labelKey: 'textFont.options.siYuanSimSun',
},
{
family: `'WebgalUI', serif`,
source: 'default',
labelKey: 'textFont.options.SimHei',
},
{
family: `'LXGW', serif`,
source: 'default',
labelKey: 'textFont.options.lxgw',
},
];

export const FALLBACK_FONT_FAMILY = DEFAULT_FONT_OPTIONS[1].family;

export function buildFontOptionsFromTemplate(fonts: TemplateFontDescriptor[]): FontOption[] {
const templateOptions: FontOption[] = fonts.map((font) => ({
family: formatFontFamily(font['font-family']),
source: 'template',
label: font['font-family'],
}));

const combined = [...templateOptions, ...DEFAULT_FONT_OPTIONS];

const seen = new Set<string>();
return combined.filter((option) => {
if (seen.has(option.family)) return false;
seen.add(option.family);
return true;
});
}

export function formatFontFamily(fontFamily: string): string {
const trimmed = fontFamily.trim();
const needsQuote = /\s/.test(trimmed);
const normalized = needsQuote ? `'${trimmed}'` : trimmed;
return `${normalized}, serif`;
}
2 changes: 2 additions & 0 deletions packages/webgal/src/Core/webgalCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AnimationManager } from '@/Core/Modules/animations';
import { Gameplay } from './Modules/gamePlay';
import { Events } from '@/Core/Modules/events';
import { SteamIntegration } from '@/Core/integration/steamIntegration';
import { WebgalTemplate } from '@/types/template';

export class WebgalCore {
public sceneManager = new SceneManager();
Expand All @@ -15,4 +16,5 @@ export class WebgalCore {
public gameKey = '';
public events = new Events();
public steam = new SteamIntegration();
public template: WebgalTemplate | null = null;
}
33 changes: 16 additions & 17 deletions packages/webgal/src/UI/Menu/Options/Display/Display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import styles from '@/UI/Menu/Options/options.module.scss';
import useFullScreen from '@/hooks/useFullScreen';
import useTrans from '@/hooks/useTrans';
import { RootState } from '@/store/store';
import { textFont, textSize } from '@/store/userDataInterface';
import { textSize } from '@/store/userDataInterface';
import { setOptionData } from '@/store/userDataReducer';
import { useDispatch, useSelector } from 'react-redux';
import { OptionSlider } from '../OptionSlider';
Expand All @@ -16,6 +16,15 @@ export function Display() {
const dispatch = useDispatch();
const t = useTrans('menu.options.pages.display.options.');
const { isSupported: isFullscreenSupported, enter: enterFullscreen, exit: exitFullscreen } = useFullScreen();
const fontOptions = useSelector((state: RootState) => state.GUI.fontOptions);
const fontOptionTexts = fontOptions.map((option) => {
if (option.labelKey) return t(option.labelKey);
if (option.label) return option.label;
return option.family;
});
const currentFontIndex = fontOptions.length
? Math.min(userDataState.optionData.textboxFont, fontOptions.length - 1)
: 0;

return (
<div className={styles.Options_main_content_half}>
Expand Down Expand Up @@ -50,22 +59,12 @@ export function Display() {
</NormalOption>
<NormalOption key="textFont" title={t('textFont.title')}>
<NormalButton
textList={t('textFont.options.siYuanSimSun', 'textFont.options.SimHei', 'textFont.options.lxgw')}
functionList={[
() => {
dispatch(setOptionData({ key: 'textboxFont', value: textFont.song }));
setStorage();
},
() => {
dispatch(setOptionData({ key: 'textboxFont', value: textFont.hei }));
setStorage();
},
() => {
dispatch(setOptionData({ key: 'textboxFont', value: textFont.lxgw }));
setStorage();
},
]}
currentChecked={userDataState.optionData.textboxFont}
textList={fontOptionTexts}
functionList={fontOptions.map((_, index) => () => {
dispatch(setOptionData({ key: 'textboxFont', value: index }));
setStorage();
})}
currentChecked={currentFontIndex}
/>
</NormalOption>
<NormalOption key="textSpeed" title={t('textSpeed.title')}>
Expand Down
31 changes: 18 additions & 13 deletions packages/webgal/src/hooks/useFontFamily.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { useSelector } from 'react-redux';
import { RootState } from '@/store/store';
import { textFont } from '@/store/userDataInterface';
import { match } from '@/Core/util/match';
import { RootState, webgalStore } from '@/store/store';
import { FALLBACK_FONT_FAMILY } from '@/Core/util/fonts/fontOptions';

export function useFontFamily() {
const fontFamily = useSelector((state: RootState) => state.userData.optionData.textboxFont);
export function useFontFamily(): string {
return useSelector(selectFontFamily);
}

function getFont() {
return match(fontFamily)
.with(textFont.song, () => '"思源宋体", serif')
.with(textFont.lxgw, () => '"LXGW", serif')
.with(textFont.hei, () => '"WebgalUI", serif')
.default(() => '"WebgalUI", serif');
}
export function getCurrentFontFamily(): string {
return selectFontFamily(webgalStore.getState());
}

return getFont();
export function selectFontFamily(state: RootState): string {
const index = state.userData.optionData.textboxFont ?? 0;
const fonts = state.GUI.fontOptions;
if (fonts[index]) {
return fonts[index].family;
}
if (fonts.length > 0) {
return fonts[0].family;
}
return FALLBACK_FONT_FAMILY;
}
20 changes: 14 additions & 6 deletions packages/webgal/src/store/GUIReducer.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
/**
* @file 记录当前GUI的状态信息,引擎初始化时会重置。
* @author Mahiru
*/
import { getStorage } from '@/Core/controller/storage/storageController';
import { GuiAsset, IGuiState, MenuPanelTag, setAssetPayload, setVisibilityPayload } from '@/store/guiInterface';
import {
FontOption,
GuiAsset,
IGuiState,
MenuPanelTag,
setAssetPayload,
setVisibilityPayload,
} from '@/store/guiInterface';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { key } from 'localforage';
import { DEFAULT_FONT_OPTIONS } from '@/Core/util/fonts/fontOptions';

/**
* 初始GUI状态表
*/
const initState: IGuiState = {
fontOptions: [...DEFAULT_FONT_OPTIONS],
showBacklog: false,
showStarter: true,
showTitle: true,
Expand Down Expand Up @@ -80,6 +84,9 @@ const GUISlice = createSlice({
setFontOptimization: (state, action: PayloadAction<boolean>) => {
state.fontOptimization = action.payload;
},
setFontOptions: (state, action: PayloadAction<FontOption[]>) => {
state.fontOptions = [...action.payload];
},
},
});

Expand All @@ -90,6 +97,7 @@ export const {
setLogoImage,
setEnableAppreciationMode,
setFontOptimization,
setFontOptions,
} = GUISlice.actions;
export default GUISlice.reducer;

Expand Down
14 changes: 11 additions & 3 deletions packages/webgal/src/store/guiInterface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { IWebGalTextBoxTheme } from '@/Stage/themeInterface';

/**
* 当前Menu页面显示的Tag
*/
Expand All @@ -13,6 +11,7 @@ export enum MenuPanelTag {
* @interface IGuiState GUI状态接口
*/
export interface IGuiState {
fontOptions: FontOption[];
showStarter: boolean; // 是否显示初始界面(用于使得bgm可以播放)
showTitle: boolean; // 是否显示标题界面
showMenuPanel: boolean; // 是否显示Menu界面
Expand All @@ -35,7 +34,7 @@ export interface IGuiState {

export type componentsVisibility = Pick<
IGuiState,
Exclude<keyof IGuiState, 'currentMenuTag' | 'titleBg' | 'titleBgm' | 'logoImage' | 'theme'>
Exclude<keyof IGuiState, 'currentMenuTag' | 'titleBg' | 'titleBgm' | 'logoImage' | 'theme' | 'fontOptions'>
>;
// 标题资源
export type GuiAsset = Pick<IGuiState, 'titleBgm' | 'titleBg'>;
Expand All @@ -58,3 +57,12 @@ export interface setAssetPayload {
}

export type GuiStore = IGuiStore;

export type FontOptionSource = 'default' | 'template';

export interface FontOption {
family: string;
source: FontOptionSource;
labelKey?: string;
label?: string;
}
Loading