11<template >
22 <div class =" custom-game-speed-generator" >
33 <label class =" custom-game-speed-generator__label" for =" custom-game-speed-generator-input" >
4- Enter desired FPS
4+ {{ localizedMessages.desiredFpsLabel }}
55 </label >
66 <input
77 id =" custom-game-speed-generator-input"
1212 step =" 1"
1313 type =" number"
1414 />
15- <p class =" custom-game-speed-generator__caption" >Results (remember to replace N with your game speed number!): </p >
16- <div class =" language-ini vp-adaptive-theme" >
17- <button title = " Copy Code " class =" copy" ></button >
15+ <p class =" custom-game-speed-generator__caption" >{{ localizedMessages.resultsCaption }} </p >
16+ <div v-if = " highlightedCodeHtml " class =" language-ini vp-adaptive-theme" >
17+ <button :aria-label = " localizedMessages.copyCode " :title = " localizedMessages.copyCode " class =" copy" ></button >
1818 <span class =" lang" >ini</span >
1919 <pre
2020 class =" shiki shiki-themes github-light github-dark vp-code"
2121 tabindex =" 0"
22- ><code v-html =" highlightedCodeHtml" ></code ></pre >
22+ ><code v-html =" highlightedCodeHtml" />
23+ </pre >
2324 </div >
2425 </div >
2526</template >
2627
2728<script setup lang="ts">
2829import { computed , onBeforeUnmount , ref , watch } from ' vue'
30+ import { useData } from ' vitepress'
2931import { createHighlighterCore } from ' shiki/core'
3032import { createJavaScriptRegexEngine } from ' shiki/engine/javascript'
3133import ini from ' @shikijs/langs/ini'
3234import githubDark from ' @shikijs/themes/github-dark'
3335import githubLight from ' @shikijs/themes/github-light'
3436
37+ const localizedStrings = {
38+ ' en-US' : {
39+ desiredFpsLabel: ' Enter desired FPS' ,
40+ resultsCaption: ' Results (remember to replace N with your game speed number!):' ,
41+ copyCode: ' Copy Code' ,
42+ noResults: " Sorry, couldn't find anything!" ,
43+ or: ' Or' ,
44+ },
45+ ' zh-CN' : {
46+ desiredFpsLabel: ' 输入所需的 FPS' ,
47+ resultsCaption: ' 结果(别忘了把 N 替换成你的游戏速度编号):' ,
48+ copyCode: ' 复制代码' ,
49+ noResults: ' 抱歉,未找到任何结果!' ,
50+ or: ' 或' ,
51+ },
52+ } as const
53+
54+ const fallbackLocale = ' en-US'
55+ const minGameSpeedDelay = 0
56+ const maxStableGameSpeedDelay = 5
57+ const minChangeInterval = 1
58+ const maxChangeInterval = 40
59+
60+ type LocalizationLocale = keyof typeof localizedStrings
61+
3562type GameSpeedMatch = {
3663 defaultDelay: number
3764 changeDelay: number
3865 changeInterval: number
3966}
4067
68+ const { lang } = useData ()
4169const desiredFps = ref <number >(30 )
42- const highlightedCodeHtml = ref <string >(' ' )
43- let highlightRequestId = 0
4470
4571const highlighterPromise = createHighlighterCore ({
4672 langs: [ini ],
4773 themes: [githubLight , githubDark ],
4874 engine: createJavaScriptRegexEngine (),
4975})
5076
51- function calculateFps(changeDelay : number , defaultDelay : number , changeInterval : number ): number {
52- return (
53- (60 / (6 - changeDelay ) + (60 / (6 - defaultDelay )) * ((changeInterval - 1 ) / (6 - changeDelay ))) /
54- (1 + (changeInterval - 1 ) / (6 - changeDelay ))
55- )
56- }
77+ const localizedMessages = computed (() => {
78+ return localizedStrings [lang .value as LocalizationLocale ] || localizedStrings [fallbackLocale ]
79+ })
80+
81+ const matchingGameSpeedSettings = computed (() => {
82+ return findMatchingGameSpeedSettings (desiredFps .value )
83+ })
84+
85+ const resultText = computed (() => {
86+ if (! matchingGameSpeedSettings .value .length ) {
87+ return ` ; ${localizedMessages .value .noResults } `
88+ }
5789
58- const matches = computed <GameSpeedMatch []>(() => {
59- const fps = Number (desiredFps .value )
90+ const settings = matchingGameSpeedSettings .value
91+ .map ((match , index ) => formatGameSpeedMatch (match , index , localizedMessages .value .or ))
92+ .join (' \n ' )
93+
94+ return ` [General]\n CustomGS=true\n ;\n ${settings } `
95+ })
96+ const highlightedCodeHtml = ref <String | null >(null )
6097
98+ let highlightRequestId = 0
99+ watch (
100+ resultText ,
101+ async (code : string ) => {
102+ // Shiki is loaded asynchronously; ignore stale highlights if the user
103+ // changes FPS again before the previous highlight request finishes.
104+ const requestId = (highlightRequestId += 1 )
105+ highlightedCodeHtml .value = escapeHtml (code )
106+
107+ const highlighter = await highlighterPromise
108+ const highlightedHtml = highlighter .codeToHtml (code , {
109+ lang: ' ini' ,
110+ themes: {
111+ light: ' github-light' ,
112+ dark: ' github-dark' ,
113+ },
114+ })
115+
116+ if (requestId !== highlightRequestId ) {
117+ return
118+ }
119+
120+ highlightedCodeHtml .value = normalizeVitePressShikiTokenStyles (extractCodeHtml (highlightedHtml , code ))
121+ },
122+ { immediate: true },
123+ )
124+
125+ function findMatchingGameSpeedSettings(fps : number ): GameSpeedMatch [] {
61126 if (! Number .isFinite (fps )) {
62127 return []
63128 }
64129
65130 const result: GameSpeedMatch [] = []
66131
67- for (let defaultDelay = 0 ; defaultDelay <= 5 ; defaultDelay += 1 ) {
68- for (let changeDelay = 0 ; changeDelay <= 5 ; changeDelay += 1 ) {
69- for (let changeInterval = 1 ; changeInterval <= 40 ; changeInterval += 1 ) {
132+ // The game accepts delay values, not FPS directly. Try every stable
133+ // combination and keep the ones that round to the requested frame rate.
134+ for (let defaultDelay = minGameSpeedDelay ; defaultDelay <= maxStableGameSpeedDelay ; defaultDelay += 1 ) {
135+ for (let changeDelay = minGameSpeedDelay ; changeDelay <= maxStableGameSpeedDelay ; changeDelay += 1 ) {
136+ for (let changeInterval = minChangeInterval ; changeInterval <= maxChangeInterval ; changeInterval += 1 ) {
70137 if (Math .round (calculateFps (changeDelay , defaultDelay , changeInterval )) === fps ) {
71138 result .push ({ defaultDelay , changeDelay , changeInterval })
72139 }
@@ -75,36 +142,28 @@ const matches = computed<GameSpeedMatch[]>(() => {
75142 }
76143
77144 return result
78- })
79-
80- const resultText = computed (() => {
81- if (! matches .value .length ) {
82- return " // Sorry, couldn't find anything!"
83- }
145+ }
84146
85- return matches .value
86- .map ((match , index ) => {
87- const lines = [
88- ` CustomGSN.DefaultDelay=${match .defaultDelay } ` ,
89- ` CustomGSN.ChangeDelay=${match .changeDelay } ` ,
90- ` CustomGSN.ChangeInterval=${match .changeInterval } ` ,
91- ]
147+ function formatGameSpeedMatch(match : GameSpeedMatch , index : number , orLabel : string ): string {
148+ const lines = [
149+ ` CustomGSN.DefaultDelay=${match .defaultDelay } ` ,
150+ ` CustomGSN.ChangeDelay=${match .changeDelay } ` ,
151+ ` CustomGSN.ChangeInterval=${match .changeInterval } ` ,
152+ ]
92153
93- if (index > 0 ) {
94- lines .unshift (' // -- Or --' )
95- }
154+ if (index > 0 ) {
155+ lines .unshift (` ; -- ${ orLabel } --` )
156+ }
96157
97- return lines .join (' \n ' )
98- })
99- .join (' \n ' )
100- })
158+ return lines .join (' \n ' )
159+ }
101160
102161function escapeHtml(value : string ): string {
103162 return value .replace (/ &/ g , ' &' ).replace (/ </ g , ' <' ).replace (/ >/ g , ' >' ).replace (/ "/ g , ' "' )
104163}
105164
106- function extractCodeHtml(html : string ): string {
107- return html .match (/ <code>([\s\S ] * )<\/ code>/ )?.[1 ] || escapeHtml (resultText . value )
165+ function extractCodeHtml(html : string , fallbackCode : string ): string {
166+ return html .match (/ <code>([\s\S ] * )<\/ code>/ )?.[1 ] || escapeHtml (fallbackCode )
108167}
109168
110169function normalizeVitePressShikiTokenStyles(html : string ): string {
@@ -126,6 +185,8 @@ function normalizeVitePressShikiTokenStyles(html: string): string {
126185 const property = declaration .slice (0 , separatorIndex ).trim ()
127186 const value = declaration .slice (separatorIndex + 1 ).trim ()
128187
188+ // VitePress themes expect Shiki colors in CSS variables so the same
189+ // generated markup can switch between light and dark mode.
129190 if (property === ' color' ) {
130191 lightColor = value
131192 continue
@@ -142,31 +203,14 @@ function normalizeVitePressShikiTokenStyles(html: string): string {
142203 })
143204}
144205
145- const stopHighlightWatcher = watch (
146- resultText ,
147- async code => {
148- const requestId = (highlightRequestId += 1 )
149- highlightedCodeHtml .value = escapeHtml (code )
150-
151- const highlighter = await highlighterPromise
152- const highlightedHtml = highlighter .codeToHtml (code , {
153- lang: ' ini' ,
154- themes: {
155- light: ' github-light' ,
156- dark: ' github-dark' ,
157- },
158- })
159-
160- if (requestId !== highlightRequestId ) {
161- return
162- }
163-
164- highlightedCodeHtml .value = normalizeVitePressShikiTokenStyles (extractCodeHtml (highlightedHtml ))
165- },
166- { immediate: true },
167- )
168-
169- onBeforeUnmount (stopHighlightWatcher )
206+ function calculateFps(changeDelay : number , defaultDelay : number , changeInterval : number ): number {
207+ // CustomGS alternates one changed-delay frame with a run of default-delay
208+ // frames. This weighted average estimates the resulting visible FPS.
209+ return (
210+ (60 / (6 - changeDelay ) + (60 / (6 - defaultDelay )) * ((changeInterval - 1 ) / (6 - changeDelay ))) /
211+ (1 + (changeInterval - 1 ) / (6 - changeDelay ))
212+ )
213+ }
170214 </script >
171215
172216<style scoped>
0 commit comments