@jjlmoya/utils-games-development 1.46.0 → 1.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/src/entries.ts +5 -1
  3. package/src/tests/locale_completeness.test.ts +1 -1
  4. package/src/tests/tool_validation.test.ts +2 -2
  5. package/src/tool/itchioGameTester/audit.ts +51 -0
  6. package/src/tool/itchioGameTester/bibliography.astro +6 -0
  7. package/src/tool/itchioGameTester/bibliography.ts +14 -0
  8. package/src/tool/itchioGameTester/client.ts +249 -0
  9. package/src/tool/itchioGameTester/component.astro +104 -0
  10. package/src/tool/itchioGameTester/entry.ts +28 -0
  11. package/src/tool/itchioGameTester/i18n/de.ts +109 -0
  12. package/src/tool/itchioGameTester/i18n/en.ts +166 -0
  13. package/src/tool/itchioGameTester/i18n/es.ts +174 -0
  14. package/src/tool/itchioGameTester/i18n/fr.ts +109 -0
  15. package/src/tool/itchioGameTester/i18n/id.ts +109 -0
  16. package/src/tool/itchioGameTester/i18n/it.ts +109 -0
  17. package/src/tool/itchioGameTester/i18n/ja.ts +109 -0
  18. package/src/tool/itchioGameTester/i18n/ko.ts +109 -0
  19. package/src/tool/itchioGameTester/i18n/nl.ts +109 -0
  20. package/src/tool/itchioGameTester/i18n/pl.ts +109 -0
  21. package/src/tool/itchioGameTester/i18n/pt.ts +109 -0
  22. package/src/tool/itchioGameTester/i18n/ru.ts +109 -0
  23. package/src/tool/itchioGameTester/i18n/sv.ts +109 -0
  24. package/src/tool/itchioGameTester/i18n/tr.ts +109 -0
  25. package/src/tool/itchioGameTester/i18n/zh.ts +109 -0
  26. package/src/tool/itchioGameTester/index.ts +11 -0
  27. package/src/tool/itchioGameTester/itchio-game-tester.css +273 -0
  28. package/src/tool/itchioGameTester/logic.test.ts +56 -0
  29. package/src/tool/itchioGameTester/logic.ts +266 -0
  30. package/src/tool/itchioGameTester/seo.astro +15 -0
  31. package/src/tool/itchioGameTester/ui.ts +22 -0
  32. package/src/tools.ts +2 -1
@@ -0,0 +1,266 @@
1
+ export interface WebGameFileInfo {
2
+ name: string;
3
+ size: number;
4
+ isDirectory: boolean;
5
+ }
6
+
7
+ export interface GameValidationIssue {
8
+ type: 'error' | 'warning' | 'info';
9
+ code: string;
10
+ message: string;
11
+ details?: string;
12
+ }
13
+
14
+ export type DetectedGameEngine = 'Godot 4' | 'Godot 3' | 'Unity WebGL' | 'Phaser / Construct' | 'Custom HTML5';
15
+
16
+ export interface GameValidationResult {
17
+ score: number;
18
+ engine: DetectedGameEngine;
19
+ hasIndexHtml: boolean;
20
+ indexHtmlPath: string;
21
+ totalFiles: number;
22
+ totalUncompressedSize: number;
23
+ issues: GameValidationIssue[];
24
+ fileList: WebGameFileInfo[];
25
+ }
26
+
27
+ export interface ResolutionPreset {
28
+ name: string;
29
+ width: number;
30
+ height: number;
31
+ ratio: string;
32
+ }
33
+
34
+ export const RESOLUTION_PRESETS: ResolutionPreset[] = [
35
+ { name: '16:9 Standard HD', width: 1280, height: 720, ratio: '16:9' },
36
+ { name: '16:9 Itch Classic', width: 960, height: 540, ratio: '16:9' },
37
+ { name: '16:9 Full HD', width: 1920, height: 1080, ratio: '16:9' },
38
+ { name: '4:3 VGA', width: 800, height: 600, ratio: '4:3' },
39
+ { name: '4:3 XGA', width: 1024, height: 768, ratio: '4:3' },
40
+ { name: '3:2 Classic', width: 960, height: 640, ratio: '3:2' },
41
+ { name: '1:1 Square', width: 800, height: 800, ratio: '1:1' },
42
+ { name: 'Itch Desktop Default', width: 960, height: 600, ratio: '16:10' },
43
+ ];
44
+
45
+ export function detectGameEngine(files: WebGameFileInfo[]): DetectedGameEngine {
46
+ const fileNames = files.map((f) => f.name.replace(/\\/g, '/'));
47
+
48
+ if (fileNames.some((n) => n.endsWith('.pck') || n.endsWith('.wasm'))) {
49
+ const isGodot4 = fileNames.some((n) => n.toLowerCase().includes('godot') || n.endsWith('.side.wasm'));
50
+ return isGodot4 ? 'Godot 4' : 'Godot 3';
51
+ }
52
+
53
+ if (fileNames.some((n) => n.includes('Build/') || n.endsWith('.framework.js') || n.endsWith('.data'))) {
54
+ return 'Unity WebGL';
55
+ }
56
+
57
+ if (fileNames.some((n) => n.includes('phaser') || n.includes('c2runtime') || n.includes('c3runtime'))) {
58
+ return 'Phaser / Construct';
59
+ }
60
+
61
+ return 'Custom HTML5';
62
+ }
63
+
64
+ function checkCaseConflict(normalizedPath: string, lowerCaseMap: Map<string, string>): GameValidationIssue | null {
65
+ const lowerPath = normalizedPath.toLowerCase();
66
+ const existing = lowerCaseMap.get(lowerPath);
67
+ if (existing && existing !== normalizedPath) {
68
+ return {
69
+ type: 'warning',
70
+ code: 'CASE_CONFLICT',
71
+ message: 'File name case sensitivity conflict detected',
72
+ details: `${normalizedPath} conflicts with ${existing}. Case differences cause 404 errors on Itch.io Linux/macOS servers.`
73
+ };
74
+ }
75
+ lowerCaseMap.set(lowerPath, normalizedPath);
76
+ return null;
77
+ }
78
+
79
+ function checkFileExtension(file: WebGameFileInfo): GameValidationIssue | null {
80
+ const ext = file.name.split('.').pop() || '';
81
+ if (ext && ext !== ext.toLowerCase() && !file.isDirectory) {
82
+ return {
83
+ type: 'warning',
84
+ code: 'UPPERCASE_EXTENSION',
85
+ message: 'Uppercase file extension detected',
86
+ details: `${file.name} has an uppercase extension .${ext}. Web servers may fail to send correct MIME types.`
87
+ };
88
+ }
89
+ return null;
90
+ }
91
+
92
+ function checkHeavyAudio(file: WebGameFileInfo): GameValidationIssue | null {
93
+ const ext = file.name.split('.').pop() || '';
94
+ if (ext.toLowerCase() === 'wav' && file.size > 5 * 1024 * 1024) {
95
+ return {
96
+ type: 'info',
97
+ code: 'HEAVY_AUDIO',
98
+ message: 'Large uncompressed WAV audio file',
99
+ details: `${file.name} is ${Math.round(file.size / (1024 * 1024))}MB. Convert to OGG or MP3 for faster web loading.`
100
+ };
101
+ }
102
+ return null;
103
+ }
104
+
105
+ function evaluateIndexHtml(hasIndexHtml: boolean, indexHtmlPath: string): { issue: GameValidationIssue | null; penalty: number } {
106
+ if (hasIndexHtml) {
107
+ return { issue: null, penalty: 0 };
108
+ }
109
+ if (indexHtmlPath !== '') {
110
+ return {
111
+ issue: {
112
+ type: 'error',
113
+ code: 'NESTED_INDEX',
114
+ message: 'index.html is nested inside a subfolder',
115
+ details: `Found index.html at ${indexHtmlPath}. Itch.io requires index.html to be located in the root directory.`
116
+ },
117
+ penalty: 50
118
+ };
119
+ }
120
+ return {
121
+ issue: {
122
+ type: 'error',
123
+ code: 'MISSING_INDEX',
124
+ message: 'Missing index.html file',
125
+ details: 'No index.html file was found in the uploaded package. Web builds require an index.html entry point.'
126
+ },
127
+ penalty: 80
128
+ };
129
+ }
130
+
131
+ interface ScanAccumulator {
132
+ score: number;
133
+ hasIndexHtml: boolean;
134
+ indexHtmlPath: string;
135
+ totalFiles: number;
136
+ totalUncompressedSize: number;
137
+ }
138
+
139
+ function checkIndexLocation(normalizedPath: string, acc: ScanAccumulator) {
140
+ if (normalizedPath === 'index.html') {
141
+ acc.hasIndexHtml = true;
142
+ acc.indexHtmlPath = normalizedPath;
143
+ } else if ((normalizedPath.endsWith('/index.html') || normalizedPath.endsWith('\\index.html')) && !acc.hasIndexHtml) {
144
+ acc.indexHtmlPath = normalizedPath;
145
+ }
146
+ }
147
+
148
+ function processSingleFile(file: WebGameFileInfo, lowerCaseMap: Map<string, string>, acc: ScanAccumulator, issues: GameValidationIssue[]) {
149
+ if (!file.isDirectory) {
150
+ acc.totalFiles++;
151
+ acc.totalUncompressedSize += file.size;
152
+ }
153
+
154
+ const normalizedPath = file.name.replace(/\\/g, '/');
155
+ const caseIssue = checkCaseConflict(normalizedPath, lowerCaseMap);
156
+ if (caseIssue) {
157
+ issues.push(caseIssue);
158
+ acc.score -= 10;
159
+ }
160
+
161
+ checkIndexLocation(normalizedPath, acc);
162
+
163
+ const extIssue = checkFileExtension(file);
164
+ if (extIssue) {
165
+ issues.push(extIssue);
166
+ acc.score -= 5;
167
+ }
168
+
169
+ const audioIssue = checkHeavyAudio(file);
170
+ if (audioIssue) {
171
+ issues.push(audioIssue);
172
+ acc.score -= 2;
173
+ }
174
+ }
175
+
176
+ function appendEngineAndSizeIssues(engine: DetectedGameEngine, totalUncompressedSize: number, score: number, issues: GameValidationIssue[]): number {
177
+ let updatedScore = score;
178
+ if (engine === 'Godot 4') {
179
+ issues.push({
180
+ type: 'info',
181
+ code: 'GODOT4_HEADERS',
182
+ message: 'Godot 4 Web export detected',
183
+ details: 'Ensure "SharedArrayBuffer support" or Cross-Origin Isolation headers are enabled in your Itch.io game edit page.'
184
+ });
185
+ }
186
+
187
+ if (totalUncompressedSize > 500 * 1024 * 1024) {
188
+ issues.push({
189
+ type: 'warning',
190
+ code: 'LARGE_BUILD',
191
+ message: 'Total build size exceeds 500MB',
192
+ details: 'Large web builds may experience high bounce rates or memory crashes on mobile browsers.'
193
+ });
194
+ updatedScore -= 15;
195
+ }
196
+ return updatedScore;
197
+ }
198
+
199
+ export function validateGameFiles(files: WebGameFileInfo[]): GameValidationResult {
200
+ const issues: GameValidationIssue[] = [];
201
+ const engine = detectGameEngine(files);
202
+ const lowerCaseMap = new Map<string, string>();
203
+ const acc: ScanAccumulator = { score: 100, hasIndexHtml: false, indexHtmlPath: '', totalFiles: 0, totalUncompressedSize: 0 };
204
+
205
+ for (const file of files) {
206
+ processSingleFile(file, lowerCaseMap, acc, issues);
207
+ }
208
+
209
+ const indexEval = evaluateIndexHtml(acc.hasIndexHtml, acc.indexHtmlPath);
210
+ if (indexEval.issue) {
211
+ issues.unshift(indexEval.issue);
212
+ acc.score -= indexEval.penalty;
213
+ }
214
+
215
+ const finalScore = appendEngineAndSizeIssues(engine, acc.totalUncompressedSize, acc.score, issues);
216
+
217
+ return {
218
+ score: Math.max(0, finalScore),
219
+ engine,
220
+ hasIndexHtml: acc.hasIndexHtml,
221
+ indexHtmlPath: acc.indexHtmlPath,
222
+ totalFiles: acc.totalFiles,
223
+ totalUncompressedSize: acc.totalUncompressedSize,
224
+ issues,
225
+ fileList: files,
226
+ };
227
+ }
228
+
229
+ export function calculateAspectRatio(width: number, height: number): string {
230
+ if (!width || !height) return '16:9';
231
+ const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b));
232
+ const divisor = gcd(width, height);
233
+ const wRatio = width / divisor;
234
+ const hRatio = height / divisor;
235
+
236
+ const ratioKey = `${wRatio}:${hRatio}`;
237
+ const knownRatios: Record<string, string> = {
238
+ '16:9': '16:9',
239
+ '4:3': '4:3',
240
+ '3:2': '3:2',
241
+ '16:10': '16:10',
242
+ '1:1': '1:1',
243
+ };
244
+
245
+ if (knownRatios[ratioKey]) {
246
+ return knownRatios[ratioKey];
247
+ }
248
+
249
+ const decimal = width / height;
250
+ if (Math.abs(decimal - 16 / 9) < 0.05) return '16:9 Approx';
251
+ if (Math.abs(decimal - 4 / 3) < 0.05) return '4:3 Approx';
252
+
253
+ return ratioKey;
254
+ }
255
+
256
+ export function generateItchioEmbedConfig(width: number, height: number, orientation: 'landscape' | 'portrait' = 'landscape') {
257
+ return {
258
+ viewportWidth: width,
259
+ viewportHeight: height,
260
+ embedMode: 'Embed in page',
261
+ scrollbars: false,
262
+ fullscreenButton: true,
263
+ orientation,
264
+ codeSnippet: `Dimensions: ${width}px x ${height}px\nEmbed Mode: Embed in page\nMobile Orientation: ${orientation}\nFullscreen Button: Enabled`
265
+ };
266
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { itchioGameTester } from './entry';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const loader = itchioGameTester.i18n[locale] ?? itchioGameTester.i18n.en;
12
+ const content = loader ? await loader() : null;
13
+ ---
14
+
15
+ {content && content.seo && content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,22 @@
1
+ export interface ItchioGameTesterUI extends Record<string, string> {
2
+ dropzoneTitle: string;
3
+ dropzoneHint: string;
4
+ chooseFiles: string;
5
+ engineDetected: string;
6
+ compatibilityScore: string;
7
+ viewportWidth: string;
8
+ viewportHeight: string;
9
+ aspectRatio: string;
10
+ lockAspectRatio: string;
11
+ presets: string;
12
+ fitTest: string;
13
+ copySettings: string;
14
+ copied: string;
15
+ embedMode: string;
16
+ scrollbars: string;
17
+ noIssuesFound: string;
18
+ filesInspected: string;
19
+ resetViewport: string;
20
+ autoScaleToggle: string;
21
+ scaledNotice: string;
22
+ }
package/src/tools.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export { ALL_ENTRIES } from './entries';
2
2
  import type { ToolDefinition } from './types';
3
3
  import { STEAM_CAPSULE_GENERATOR_TOOL } from './tool/steamCapsuleGenerator';
4
+ import { ITCHIO_GAME_TESTER_TOOL } from './tool/itchioGameTester';
4
5
 
5
6
  export const ALL_TOOLS: ToolDefinition[] = [
6
7
  STEAM_CAPSULE_GENERATOR_TOOL,
8
+ ITCHIO_GAME_TESTER_TOOL,
7
9
  ];
8
-