@jjlmoya/utils-games-development 1.46.0 → 1.49.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.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +8 -1
- package/src/tests/locale_completeness.test.ts +1 -20
- package/src/tests/tool_validation.test.ts +2 -3
- package/src/tool/itchioGameTester/audit.ts +51 -0
- package/src/tool/itchioGameTester/bibliography.astro +6 -0
- package/src/tool/itchioGameTester/bibliography.ts +14 -0
- package/src/tool/itchioGameTester/client.ts +238 -0
- package/src/tool/itchioGameTester/component.astro +97 -0
- package/src/tool/itchioGameTester/entry.ts +28 -0
- package/src/tool/itchioGameTester/i18n/de.ts +107 -0
- package/src/tool/itchioGameTester/i18n/en.ts +164 -0
- package/src/tool/itchioGameTester/i18n/es.ts +172 -0
- package/src/tool/itchioGameTester/i18n/fr.ts +107 -0
- package/src/tool/itchioGameTester/i18n/id.ts +107 -0
- package/src/tool/itchioGameTester/i18n/it.ts +107 -0
- package/src/tool/itchioGameTester/i18n/ja.ts +107 -0
- package/src/tool/itchioGameTester/i18n/ko.ts +107 -0
- package/src/tool/itchioGameTester/i18n/nl.ts +107 -0
- package/src/tool/itchioGameTester/i18n/pl.ts +107 -0
- package/src/tool/itchioGameTester/i18n/pt.ts +107 -0
- package/src/tool/itchioGameTester/i18n/ru.ts +107 -0
- package/src/tool/itchioGameTester/i18n/sv.ts +107 -0
- package/src/tool/itchioGameTester/i18n/tr.ts +107 -0
- package/src/tool/itchioGameTester/i18n/zh.ts +107 -0
- package/src/tool/itchioGameTester/index.ts +11 -0
- package/src/tool/itchioGameTester/itchio-game-tester.css +273 -0
- package/src/tool/itchioGameTester/logic.test.ts +56 -0
- package/src/tool/itchioGameTester/logic.ts +266 -0
- package/src/tool/itchioGameTester/seo.astro +15 -0
- package/src/tool/itchioGameTester/ui.ts +20 -0
- package/src/tool/spriteSheetPacker/app-client.ts +248 -0
- package/src/tool/spriteSheetPacker/bibliography.astro +6 -0
- package/src/tool/spriteSheetPacker/bibliography.ts +12 -0
- package/src/tool/spriteSheetPacker/component.astro +229 -0
- package/src/tool/spriteSheetPacker/dropzone-client.ts +55 -0
- package/src/tool/spriteSheetPacker/entry.ts +28 -0
- package/src/tool/spriteSheetPacker/exporter.ts +116 -0
- package/src/tool/spriteSheetPacker/extractor-client.ts +127 -0
- package/src/tool/spriteSheetPacker/flipbook-client.ts +60 -0
- package/src/tool/spriteSheetPacker/i18n/de.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/en.ts +275 -0
- package/src/tool/spriteSheetPacker/i18n/es.ts +275 -0
- package/src/tool/spriteSheetPacker/i18n/fr.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/id.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/it.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/ja.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/ko.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/nl.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/pl.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/pt.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/ru.ts +266 -0
- package/src/tool/spriteSheetPacker/i18n/sv.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/tr.ts +270 -0
- package/src/tool/spriteSheetPacker/i18n/zh.ts +270 -0
- package/src/tool/spriteSheetPacker/index.ts +11 -0
- package/src/tool/spriteSheetPacker/logic.test.ts +155 -0
- package/src/tool/spriteSheetPacker/logic.ts +32 -0
- package/src/tool/spriteSheetPacker/packer-core.ts +121 -0
- package/src/tool/spriteSheetPacker/packer-ui.ts +86 -0
- package/src/tool/spriteSheetPacker/seo.astro +15 -0
- package/src/tool/spriteSheetPacker/sprite-sheet-packer.css +542 -0
- package/src/tool/spriteSheetPacker/types.ts +89 -0
- package/src/tool/spriteSheetPacker/ui.ts +42 -0
- package/src/tools.ts +4 -1
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { GamesCategoryEntry } from '../types';
|
|
2
2
|
import { steamCapsuleGenerator } from '../tool/steamCapsuleGenerator/entry';
|
|
3
|
+
import { spriteSheetPacker } from '../tool/spriteSheetPacker/entry';
|
|
3
4
|
|
|
4
5
|
export const gamesCategory: GamesCategoryEntry = {
|
|
5
6
|
icon: 'mdi:gamepad-variant',
|
|
6
|
-
tools: [steamCapsuleGenerator],
|
|
7
|
+
tools: [steamCapsuleGenerator, spriteSheetPacker],
|
|
7
8
|
i18n: {
|
|
8
9
|
de: () => import('./i18n/de').then((m) => m.content),
|
|
9
10
|
en: () => import('./i18n/en').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
|
|
2
2
|
export type { SteamCapsuleGeneratorUI, SteamCapsuleGeneratorLocaleContent } from './tool/steamCapsuleGenerator/entry';
|
|
3
|
+
export { itchioGameTester } from './tool/itchioGameTester/entry';
|
|
4
|
+
export type { ItchioGameTesterUI, ItchioGameTesterLocaleContent } from './tool/itchioGameTester/entry';
|
|
5
|
+
export { spriteSheetPacker } from './tool/spriteSheetPacker/entry';
|
|
6
|
+
export type { SpriteSheetPackerUI, SpriteSheetPackerLocaleContent } from './tool/spriteSheetPacker/entry';
|
|
3
7
|
export { gamesCategory } from './category';
|
|
4
8
|
import { steamCapsuleGenerator } from './tool/steamCapsuleGenerator/entry';
|
|
5
|
-
|
|
9
|
+
import { itchioGameTester } from './tool/itchioGameTester/entry';
|
|
10
|
+
import { spriteSheetPacker } from './tool/spriteSheetPacker/entry';
|
|
11
|
+
|
|
12
|
+
export const ALL_ENTRIES = [steamCapsuleGenerator, itchioGameTester, spriteSheetPacker];
|
|
@@ -1,27 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { ALL_TOOLS } from '../tools';
|
|
3
|
-
import type { ToolLocaleContent } from '../types';
|
|
4
3
|
|
|
5
4
|
describe('Locale Completeness Validation', () => {
|
|
6
|
-
ALL_TOOLS.forEach((tool) => {
|
|
7
|
-
describe(`Tool: ${tool.entry.id}`, () => {
|
|
8
|
-
Object.keys(tool.entry.i18n).forEach((locale) => {
|
|
9
|
-
describe(`Locale: ${locale}`, () => {
|
|
10
|
-
it('bibliography should be an array', async () => {
|
|
11
|
-
const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
|
|
12
|
-
const content = (await loader?.()) as ToolLocaleContent;
|
|
13
|
-
|
|
14
|
-
expect(
|
|
15
|
-
Array.isArray(content.bibliography),
|
|
16
|
-
`Tool "${tool.entry.id}" locale "${locale}" bibliography is not an array`,
|
|
17
|
-
).toBeTruthy();
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
5
|
it('one tool is registered', () => {
|
|
25
|
-
expect(ALL_TOOLS.length).toBe(
|
|
6
|
+
expect(ALL_TOOLS.length).toBe(3);
|
|
26
7
|
});
|
|
27
8
|
});
|
|
@@ -4,8 +4,8 @@ import { gamesCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 3 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(3);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('gamesCategory should be defined', () => {
|
|
@@ -14,4 +14,3 @@ describe('Tool Validation Suite', () => {
|
|
|
14
14
|
});
|
|
15
15
|
});
|
|
16
16
|
});
|
|
17
|
-
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { validateGameFiles, GameValidationIssue } from './logic';
|
|
2
|
+
|
|
3
|
+
function renderAuditIssue(i: GameValidationIssue): string {
|
|
4
|
+
let bg = '#e0f2fe';
|
|
5
|
+
let color = '#075985';
|
|
6
|
+
if (i.type === 'error') { bg = '#fee2e2'; color = '#991b1b'; }
|
|
7
|
+
else if (i.type === 'warning') { bg = '#fef9c3'; color = '#854d0e'; }
|
|
8
|
+
const details = i.details ? `<p style="margin: 0.25rem 0 0 0; font-size: 0.85rem;">${i.details}</p>` : '';
|
|
9
|
+
return `<div style="padding: 0.75rem; border-radius: 6px; background: ${bg}; color: ${color};"><strong>[${i.type.toUpperCase()}] ${i.message}</strong>${details}</div>`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function renderBadge(score: number): string {
|
|
13
|
+
if (score >= 90) return 'igt-audit-badge igt-badge-pass';
|
|
14
|
+
if (score >= 60) return 'igt-audit-badge igt-badge-warn';
|
|
15
|
+
return 'igt-audit-badge igt-badge-error';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderIssuesList(issuesList: HTMLElement, audit: ReturnType<typeof validateGameFiles>) {
|
|
19
|
+
if (audit.issues.length === 0) {
|
|
20
|
+
const r = document.getElementById('igt-root');
|
|
21
|
+
const msg = r?.getAttribute('data-no-issues-found') || 'All checks passed!';
|
|
22
|
+
issuesList.innerHTML = `<div class="igt-all-clear"><div class="igt-all-clear-icon"><svg width="13" height="13" viewBox="0 0 12 12" fill="none"><path d="M2 6l3 3 5-5" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></div><div class="igt-all-clear-body"><strong>${msg}</strong><span>Score: ${audit.score}% — ${audit.totalFiles} files inspected</span></div></div>`;
|
|
23
|
+
} else {
|
|
24
|
+
issuesList.innerHTML = audit.issues.map(renderAuditIssue).join('');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getAuditI18n() {
|
|
29
|
+
const r = document.getElementById('igt-root');
|
|
30
|
+
return {
|
|
31
|
+
engineLabel: r?.getAttribute('data-engine-detected') || 'Engine',
|
|
32
|
+
scoreLabel: r?.getAttribute('data-compatibility-score') || 'Score',
|
|
33
|
+
filesLabel: r?.getAttribute('data-files-inspected') || 'Files',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function displayAudit(audit: ReturnType<typeof validateGameFiles>) {
|
|
38
|
+
const panel = document.getElementById('igt-results-panel');
|
|
39
|
+
const engineTag = document.getElementById('igt-engine-tag');
|
|
40
|
+
const scoreBadge = document.getElementById('igt-score-badge');
|
|
41
|
+
const filesCount = document.getElementById('igt-files-count');
|
|
42
|
+
const issuesList = document.getElementById('igt-issues-list');
|
|
43
|
+
if (!panel || !engineTag || !scoreBadge || !filesCount || !issuesList) return;
|
|
44
|
+
const i18n = getAuditI18n();
|
|
45
|
+
panel.style.display = 'block';
|
|
46
|
+
engineTag.textContent = `${i18n.engineLabel}: ${audit.engine}`;
|
|
47
|
+
scoreBadge.textContent = `${i18n.scoreLabel}: ${audit.score}%`;
|
|
48
|
+
scoreBadge.className = renderBadge(audit.score);
|
|
49
|
+
filesCount.textContent = `${i18n.filesLabel}: ${audit.totalFiles}`;
|
|
50
|
+
renderIssuesList(issuesList, audit);
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const bibliographyEntries = [
|
|
2
|
+
{
|
|
3
|
+
name: 'Itch.io Official HTML5 Game Upload & Embed Specifications Guide',
|
|
4
|
+
url: 'https://itch.io/docs/creators/html5'
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
name: 'Godot Engine Official Web Export & SharedArrayBuffer Requirements',
|
|
8
|
+
url: 'https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_web.html'
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: 'Unity WebGL Official Player Resolution and Aspect Ratio Settings',
|
|
12
|
+
url: 'https://docs.unity3d.com/Manual/webgl-building.html'
|
|
13
|
+
}
|
|
14
|
+
];
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import JSZip from 'jszip';
|
|
2
|
+
import { validateGameFiles, generateItchioEmbedConfig, type WebGameFileInfo } from './logic';
|
|
3
|
+
import { displayAudit } from './audit';
|
|
4
|
+
|
|
5
|
+
const STATE_KEY = 'jjl_itchio_game_tester_state';
|
|
6
|
+
const currentViewport = { w: 960, h: 540 };
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
interface ScaleArgs {
|
|
10
|
+
frame: HTMLElement;
|
|
11
|
+
container: HTMLElement;
|
|
12
|
+
cW: number;
|
|
13
|
+
w: number;
|
|
14
|
+
h: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function applyScaleActive(args: ScaleArgs, sc: number) {
|
|
18
|
+
args.frame.style.transform = `scale(${sc})`;
|
|
19
|
+
args.container.style.width = `${Math.round(args.w * sc)}px`;
|
|
20
|
+
args.container.style.height = `${Math.round(args.h * sc)}px`;
|
|
21
|
+
args.container.style.overflow = 'hidden';
|
|
22
|
+
const notice = document.getElementById('igt-scale-notice');
|
|
23
|
+
if (notice) notice.style.display = 'block';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function applyScaleNone(args: ScaleArgs) {
|
|
27
|
+
args.frame.style.transform = 'none';
|
|
28
|
+
args.container.style.width = '';
|
|
29
|
+
args.container.style.height = '';
|
|
30
|
+
args.container.style.overflow = 'auto';
|
|
31
|
+
const notice = document.getElementById('igt-scale-notice');
|
|
32
|
+
if (notice) notice.style.display = 'none';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function applyTransformScale(args: ScaleArgs) {
|
|
36
|
+
const autoScale = document.getElementById('igt-auto-scale') as HTMLInputElement;
|
|
37
|
+
const sc = autoScale?.checked && args.w > args.cW ? args.cW / args.w : 1;
|
|
38
|
+
if (sc < 1) { applyScaleActive(args, sc); } else { applyScaleNone(args); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function applyViewportStyle(w: number, h: number) {
|
|
42
|
+
currentViewport.w = w;
|
|
43
|
+
currentViewport.h = h;
|
|
44
|
+
const frame = document.getElementById('igt-emulator-frame');
|
|
45
|
+
const container = document.getElementById('igt-emulator-container');
|
|
46
|
+
if (frame && container) {
|
|
47
|
+
frame.style.width = `${w}px`;
|
|
48
|
+
frame.style.height = `${h}px`;
|
|
49
|
+
applyTransformScale({ frame, container, cW: window.innerWidth || 1000, w, h });
|
|
50
|
+
}
|
|
51
|
+
const wInput = document.getElementById('igt-width-input') as HTMLInputElement;
|
|
52
|
+
const hInput = document.getElementById('igt-height-input') as HTMLInputElement;
|
|
53
|
+
if (wInput) wInput.value = w.toString();
|
|
54
|
+
if (hInput) hInput.value = h.toString();
|
|
55
|
+
localStorage.setItem(STATE_KEY, JSON.stringify({ w, h }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function bindInputEvents() {
|
|
59
|
+
const widthInput = document.getElementById('igt-width-input') as HTMLInputElement;
|
|
60
|
+
const heightInput = document.getElementById('igt-height-input') as HTMLInputElement;
|
|
61
|
+
const autoScale = document.getElementById('igt-auto-scale') as HTMLInputElement;
|
|
62
|
+
widthInput?.addEventListener('input', () => {
|
|
63
|
+
const w = parseInt(widthInput.value, 10) || 960;
|
|
64
|
+
const h = parseInt(heightInput.value, 10) || 540;
|
|
65
|
+
applyViewportStyle(w, h);
|
|
66
|
+
});
|
|
67
|
+
heightInput?.addEventListener('input', () => {
|
|
68
|
+
const h = parseInt(heightInput.value, 10) || 540;
|
|
69
|
+
const w = parseInt(widthInput.value, 10) || 960;
|
|
70
|
+
applyViewportStyle(w, h);
|
|
71
|
+
});
|
|
72
|
+
autoScale?.addEventListener('change', () => {
|
|
73
|
+
applyViewportStyle(parseInt(widthInput.value, 10) || 960, parseInt(heightInput.value, 10) || 540);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function bindPresetEvents() {
|
|
78
|
+
const resetBtn = document.getElementById('igt-reset-btn');
|
|
79
|
+
const presetsContainer = document.getElementById('igt-presets-container');
|
|
80
|
+
presetsContainer?.addEventListener('click', (e) => {
|
|
81
|
+
const target = e.target as HTMLElement;
|
|
82
|
+
if (target.classList.contains('igt-chip')) {
|
|
83
|
+
const w = parseInt(target.getAttribute('data-w') || '960', 10);
|
|
84
|
+
const h = parseInt(target.getAttribute('data-h') || '540', 10);
|
|
85
|
+
applyViewportStyle(w, h);
|
|
86
|
+
presetsContainer.querySelectorAll('.igt-chip').forEach((c) => c.classList.remove('active'));
|
|
87
|
+
target.classList.add('active');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
resetBtn?.addEventListener('click', () => { applyViewportStyle(960, 540); });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface DragState {
|
|
94
|
+
iframe: HTMLElement | null;
|
|
95
|
+
start: { x: number; y: number; w: number; h: number };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function attachDragListeners(st: DragState) {
|
|
99
|
+
let isResizing = true;
|
|
100
|
+
if (st.iframe) st.iframe.classList.add('igt-iframe-resizing');
|
|
101
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
102
|
+
if (!isResizing) return;
|
|
103
|
+
const constW = Math.max(320, Math.min(1920, st.start.w + (e.clientX - st.start.x)));
|
|
104
|
+
const constH = Math.max(240, Math.min(1080, st.start.h + (e.clientY - st.start.y)));
|
|
105
|
+
applyViewportStyle(constW, constH);
|
|
106
|
+
};
|
|
107
|
+
const onMouseUp = () => {
|
|
108
|
+
isResizing = false;
|
|
109
|
+
if (st.iframe) st.iframe.classList.remove('igt-iframe-resizing');
|
|
110
|
+
window.removeEventListener('mousemove', onMouseMove);
|
|
111
|
+
window.removeEventListener('mouseup', onMouseUp);
|
|
112
|
+
};
|
|
113
|
+
window.addEventListener('mousemove', onMouseMove);
|
|
114
|
+
window.addEventListener('mouseup', onMouseUp);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function bindLiveDragResizer() {
|
|
118
|
+
const handle = document.getElementById('igt-resize-handle');
|
|
119
|
+
const frame = document.getElementById('igt-emulator-frame');
|
|
120
|
+
const iframe = document.getElementById('igt-iframe');
|
|
121
|
+
if (!handle || !frame) return;
|
|
122
|
+
handle.addEventListener('mousedown', (e) => {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
attachDragListeners({ iframe, start: { x: e.clientX, y: e.clientY, w: currentViewport.w, h: currentViewport.h } });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function setupViewportController() {
|
|
129
|
+
const widthInput = document.getElementById('igt-width-input') as HTMLInputElement;
|
|
130
|
+
const heightInput = document.getElementById('igt-height-input') as HTMLInputElement;
|
|
131
|
+
const savedState = localStorage.getItem(STATE_KEY);
|
|
132
|
+
if (savedState) {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(savedState);
|
|
135
|
+
if (parsed.w) widthInput.value = parsed.w;
|
|
136
|
+
if (parsed.h) heightInput.value = parsed.h;
|
|
137
|
+
applyViewportStyle(parsed.w, parsed.h);
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
bindInputEvents();
|
|
141
|
+
bindPresetEvents();
|
|
142
|
+
bindLiveDragResizer();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getMimeType(filename: string): string {
|
|
146
|
+
const ext = filename.split('.').pop()?.toLowerCase() || '';
|
|
147
|
+
const map: Record<string, string> = { js: 'text/javascript', mjs: 'text/javascript', wasm: 'application/wasm', html: 'text/html', css: 'text/css', png: 'image/png', jpg: 'image/jpeg', svg: 'image/svg+xml' };
|
|
148
|
+
return map[ext] || 'application/octet-stream';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function buildZipBlobUrls(zip: JSZip) {
|
|
152
|
+
const blobUrls: Record<string, string> = {};
|
|
153
|
+
let indexPath = '';
|
|
154
|
+
const entries: Array<{ path: string; entry: JSZip.JSZipObject }> = [];
|
|
155
|
+
zip.forEach((path, entry) => entries.push({ path, entry }));
|
|
156
|
+
for (const { path, entry } of entries) {
|
|
157
|
+
if (!entry.dir) {
|
|
158
|
+
const blob = new Blob([await entry.async('arraybuffer')], { type: getMimeType(path) });
|
|
159
|
+
blobUrls[path] = URL.createObjectURL(blob);
|
|
160
|
+
if (path.toLowerCase().replace(/\\/g, '/').endsWith('index.html')) indexPath = path;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { blobUrls, indexPath };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function loadZipIntoIframe(zip: JSZip, iframe: HTMLIFrameElement) {
|
|
167
|
+
const { blobUrls, indexPath } = await buildZipBlobUrls(zip);
|
|
168
|
+
if (!indexPath || !blobUrls[indexPath]) return;
|
|
169
|
+
const indexFileObj = zip.file(indexPath);
|
|
170
|
+
if (!indexFileObj) return;
|
|
171
|
+
const text = await indexFileObj.async('string');
|
|
172
|
+
const doc = new DOMParser().parseFromString(text, 'text/html');
|
|
173
|
+
doc.querySelectorAll('script[src], link[rel="stylesheet"][href], img[src], audio[src], source[src]').forEach((el) => {
|
|
174
|
+
const attrName = el.hasAttribute('src') ? 'src' : 'href';
|
|
175
|
+
const val = el.getAttribute(attrName);
|
|
176
|
+
if (val) {
|
|
177
|
+
const cleanPath = val.replace(/^\.\//, '').replace(/^\//, '');
|
|
178
|
+
for (const [relPath, bUrl] of Object.entries(blobUrls)) {
|
|
179
|
+
if (relPath.endsWith(cleanPath) || relPath.endsWith(val)) { el.setAttribute(attrName, bUrl); break; }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
iframe.src = URL.createObjectURL(new Blob([doc.documentElement.outerHTML], { type: 'text/html' }));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
function processZipFile(zipFile: File, iframe: HTMLIFrameElement) {
|
|
189
|
+
JSZip.loadAsync(zipFile).then(async (zip) => {
|
|
190
|
+
const webFiles: WebGameFileInfo[] = [];
|
|
191
|
+
zip.forEach((path, entry) => webFiles.push({ name: path, size: 1024, isDirectory: entry.dir }));
|
|
192
|
+
displayAudit(validateGameFiles(webFiles));
|
|
193
|
+
await loadZipIntoIframe(zip, iframe);
|
|
194
|
+
}).catch(() => {});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function processFileList(files: FileList | File[], iframe: HTMLIFrameElement) {
|
|
198
|
+
const filesArr = Array.from(files);
|
|
199
|
+
if (filesArr.length === 0) return;
|
|
200
|
+
const zipFile = filesArr.find((f) => f.name.toLowerCase().endsWith('.zip'));
|
|
201
|
+
if (zipFile) { processZipFile(zipFile, iframe); return; }
|
|
202
|
+
const webFiles: WebGameFileInfo[] = filesArr.map((f) => ({ name: f.webkitRelativePath || f.name, size: f.size, isDirectory: false }));
|
|
203
|
+
displayAudit(validateGameFiles(webFiles));
|
|
204
|
+
const indexFile = filesArr.find((f) => f.name === 'index.html' || (f.webkitRelativePath && f.webkitRelativePath.endsWith('/index.html')));
|
|
205
|
+
if (indexFile && iframe) iframe.src = URL.createObjectURL(indexFile);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function setupCopyButton() {
|
|
209
|
+
const copyBtn = document.getElementById('igt-copy-btn');
|
|
210
|
+
if (!copyBtn) return;
|
|
211
|
+
copyBtn.addEventListener('click', () => {
|
|
212
|
+
const widthInput = document.getElementById('igt-width-input') as HTMLInputElement;
|
|
213
|
+
const heightInput = document.getElementById('igt-height-input') as HTMLInputElement;
|
|
214
|
+
const config = generateItchioEmbedConfig(parseInt(widthInput?.value || '960', 10), parseInt(heightInput?.value || '540', 10));
|
|
215
|
+
navigator.clipboard.writeText(config.codeSnippet);
|
|
216
|
+
const r = document.getElementById('igt-root');
|
|
217
|
+
copyBtn.textContent = r?.getAttribute('data-copied') || 'Copied!';
|
|
218
|
+
setTimeout(() => { copyBtn.textContent = r?.getAttribute('data-copy-settings') || 'Copy Settings'; }, 2000);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function setupFileHandling() {
|
|
223
|
+
const fileInput = document.getElementById('igt-file-input') as HTMLInputElement;
|
|
224
|
+
const dropzone = document.getElementById('igt-dropzone');
|
|
225
|
+
const iframe = document.getElementById('igt-iframe') as HTMLIFrameElement;
|
|
226
|
+
setupCopyButton();
|
|
227
|
+
if (dropzone) {
|
|
228
|
+
['dragenter', 'dragover'].forEach((ev) => dropzone.addEventListener(ev, (e) => { e.preventDefault(); dropzone.classList.add('igt-dragover'); }));
|
|
229
|
+
['dragleave', 'drop'].forEach((ev) => dropzone.addEventListener(ev, (e) => { e.preventDefault(); dropzone.classList.remove('igt-dragover'); }));
|
|
230
|
+
dropzone.addEventListener('drop', (e: DragEvent) => { if (e.dataTransfer?.files) processFileList(e.dataTransfer.files, iframe); });
|
|
231
|
+
}
|
|
232
|
+
fileInput?.addEventListener('change', () => { if (fileInput.files) processFileList(fileInput.files, iframe); });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
236
|
+
setupViewportController();
|
|
237
|
+
setupFileHandling();
|
|
238
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ItchioGameTesterUI } from './ui';
|
|
3
|
+
import './itchio-game-tester.css';
|
|
4
|
+
|
|
5
|
+
export interface Props {
|
|
6
|
+
ui: ItchioGameTesterUI;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { ui } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<div class="igt-container" id="igt-root"
|
|
13
|
+
data-engine-detected={ui.engineDetected}
|
|
14
|
+
data-compatibility-score={ui.compatibilityScore}
|
|
15
|
+
data-files-inspected={ui.filesInspected}
|
|
16
|
+
data-no-issues-found={ui.noIssuesFound}
|
|
17
|
+
data-copied={ui.copied}
|
|
18
|
+
data-copy-settings={ui.copySettings}
|
|
19
|
+
>
|
|
20
|
+
<div class="igt-card">
|
|
21
|
+
<div id="igt-dropzone" class="igt-dropzone" role="region" aria-label={ui.dropzoneTitle}>
|
|
22
|
+
<p class="igt-label" style="font-size: 1.1rem; margin-bottom: 0.5rem;">{ui.dropzoneTitle}</p>
|
|
23
|
+
<p style="font-size: 0.875rem; opacity: 0.8; margin-bottom: 1.25rem;">{ui.dropzoneHint}</p>
|
|
24
|
+
<div style="display: flex; justify-content: center;">
|
|
25
|
+
<label for="igt-file-input" class="igt-btn-primary" style="display: inline-flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
26
|
+
<svg style="width:18px;height:18px;fill:var(--text-base,#fff);" viewBox="0 0 24 24"><path d="M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z"/></svg>
|
|
27
|
+
{ui.chooseFiles}
|
|
28
|
+
<input type="file" id="igt-file-input" multiple style="display: none;" aria-label={ui.chooseFiles} />
|
|
29
|
+
</label>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div id="igt-results-panel" style="display: none; margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--border-base, #334155);">
|
|
34
|
+
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
|
|
35
|
+
<div>
|
|
36
|
+
<span id="igt-engine-tag" class="igt-chip active" style="font-size: 0.9rem;">{ui.engineDetected}: Custom HTML5</span>
|
|
37
|
+
<span id="igt-score-badge" class="igt-audit-badge igt-badge-pass" style="margin-left: 0.5rem;">Score: 100%</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<span id="igt-files-count" style="font-size: 0.875rem; opacity: 0.8;">{ui.filesInspected}: 0</span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div id="igt-issues-list" style="margin-top: 1rem; display: flex; flex-direction: column; gap: 0.5rem;"></div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div id="igt-preview-panel" style="margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--border-base, #334155);">
|
|
47
|
+
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem;">
|
|
48
|
+
<h3 class="igt-label" style="font-size: 1.1rem; margin: 0;">{ui.fitTest}</h3>
|
|
49
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
50
|
+
<button type="button" id="igt-reset-btn" class="igt-chip" aria-label={ui.resetViewport}>{ui.resetViewport}</button>
|
|
51
|
+
<button type="button" id="igt-copy-btn" class="igt-btn-primary" style="padding: 0.4rem 0.875rem; font-size: 0.85rem;" aria-label={ui.copySettings}>{ui.copySettings}</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="igt-controls-grid">
|
|
56
|
+
<div class="igt-input-group">
|
|
57
|
+
<label for="igt-width-input" class="igt-label">{ui.viewportWidth}</label>
|
|
58
|
+
<input type="number" id="igt-width-input" class="igt-input" min="320" max="1920" value="960" aria-label={ui.viewportWidth} />
|
|
59
|
+
</div>
|
|
60
|
+
<div class="igt-input-group">
|
|
61
|
+
<label for="igt-height-input" class="igt-label">{ui.viewportHeight}</label>
|
|
62
|
+
<input type="number" id="igt-height-input" class="igt-input" min="240" max="1080" value="540" aria-label={ui.viewportHeight} />
|
|
63
|
+
</div>
|
|
64
|
+
<div class="igt-input-group" style="grid-column: 1 / -1;">
|
|
65
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
66
|
+
<input type="checkbox" id="igt-auto-scale" checked aria-label={ui.autoScaleToggle} />
|
|
67
|
+
<label for="igt-auto-scale" class="igt-label" style="cursor: pointer; font-weight: 500;">{ui.autoScaleToggle}</label>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="igt-preset-chips" id="igt-presets-container">
|
|
73
|
+
<button type="button" class="igt-chip active" data-w="960" data-h="540" aria-label="Preset 960x540">960 x 540 (16:9)</button>
|
|
74
|
+
<button type="button" class="igt-chip" data-w="1280" data-h="720" aria-label="Preset 1280x720">1280 x 720 (16:9 HD)</button>
|
|
75
|
+
<button type="button" class="igt-chip" data-w="800" data-h="600" aria-label="Preset 800x600">800 x 600 (4:3)</button>
|
|
76
|
+
<button type="button" class="igt-chip" data-w="1024" data-h="768" aria-label="Preset 1024x768">1024 x 768 (4:3)</button>
|
|
77
|
+
<button type="button" class="igt-chip" data-w="960" data-h="600" aria-label="Preset 960x600">960 x 600 (Itch Desktop)</button>
|
|
78
|
+
<button type="button" class="igt-chip" data-w="800" data-h="800" aria-label="Preset 800x800">800 x 800 (1:1 Square)</button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div id="igt-scale-notice" class="igt-scale-notice">
|
|
82
|
+
<strong>[Artificial Zoom-Out Active]</strong> {ui.scaledNotice}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div id="igt-emulator-container" style="overflow: hidden; max-width: 100%; margin-top: 1.25rem;">
|
|
86
|
+
<div id="igt-emulator-frame" class="igt-emulator-frame" style="width: 960px; height: 540px; transform-origin: top left;">
|
|
87
|
+
<iframe id="igt-iframe" class="igt-iframe" title="Web Game Preview Frame"></iframe>
|
|
88
|
+
<div id="igt-resize-handle" class="igt-resize-handle" role="button" aria-label="Resize Viewport Handle"></div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<script>
|
|
96
|
+
import './client';
|
|
97
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GamesToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { ItchioGameTesterUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type { ItchioGameTesterUI };
|
|
5
|
+
|
|
6
|
+
export type ItchioGameTesterLocaleContent = ToolLocaleContent<ItchioGameTesterUI>;
|
|
7
|
+
|
|
8
|
+
export const itchioGameTester: GamesToolEntry<ItchioGameTesterUI> = {
|
|
9
|
+
id: 'itchio-game-tester',
|
|
10
|
+
icons: { bg: 'mdi:gamepad-variant', fg: 'mdi:aspect-ratio' },
|
|
11
|
+
i18n: {
|
|
12
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
13
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
14
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
15
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
16
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
17
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
18
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
19
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
20
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
21
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
22
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
23
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
24
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
25
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
26
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
import type { ItchioGameTesterUI } from '../ui';
|
|
3
|
+
import { bibliographyEntries } from '../bibliography';
|
|
4
|
+
|
|
5
|
+
export const content: ToolLocaleContent<ItchioGameTesterUI> = {
|
|
6
|
+
slug: 'itch-io-spiel-tester',
|
|
7
|
+
title: 'Itch.io Webspiel Inspektor und Echtzeit Auflösungs Optimierer',
|
|
8
|
+
description: 'HTML5-Exportdateien oder ZIP-Archive hochladen, Viewports live testen, Scrollbars beheben, Godot- und Unity-WebGL-Builds prüfen und Itch.io-Embed-Einstellungen generieren.',
|
|
9
|
+
ui: {
|
|
10
|
+
dropzoneTitle: 'Spiel-Build oder ZIP-Archiv hier ablegen',
|
|
11
|
+
dropzoneHint: 'Eine .ZIP-Datei, exportierten Ordner oder HTML5-Build-Dateien in diesen Bereich ziehen.',
|
|
12
|
+
chooseFiles: 'Spieldatei oder Ordner auswählen',
|
|
13
|
+
engineDetected: 'Erkannte Engine',
|
|
14
|
+
compatibilityScore: 'Itch.io-Kompatibilitätsbewertung',
|
|
15
|
+
viewportWidth: 'Viewport-Breite (px)',
|
|
16
|
+
viewportHeight: 'Viewport-Höhe (px)',
|
|
17
|
+
presets: 'Schnelle Auflösungs-Presets',
|
|
18
|
+
fitTest: 'Live-Layout- und Scrollbar-Test',
|
|
19
|
+
copySettings: 'Itch.io Embed-Einstellungen kopieren',
|
|
20
|
+
copied: 'In Zwischenablage kopiert',
|
|
21
|
+
embedMode: 'Embed-Modus',
|
|
22
|
+
scrollbars: 'Scrollbars aktivieren',
|
|
23
|
+
noIssuesFound: 'Alle Prüfungen erfolgreich bestanden. Das Paket ist zu 100% mit den Itch.io-Standards konform.',
|
|
24
|
+
filesInspected: 'Inspizierte Dateien',
|
|
25
|
+
resetViewport: 'Viewport zurücksetzen',
|
|
26
|
+
autoScaleToggle: 'Viewport automatisch auf Bildschirmbreite skalieren',
|
|
27
|
+
scaledNotice: 'Der Viewport überschreitet die Bildschirmbreite. Künstlicher Zoom-Out angewendet, damit der Canvas vollständig sichtbar ist. Deaktiviere die automatische Skalierung, um das echte Layout zu sehen.',
|
|
28
|
+
},
|
|
29
|
+
seo: [
|
|
30
|
+
{
|
|
31
|
+
type: 'title',
|
|
32
|
+
level: 2,
|
|
33
|
+
text: 'Formatierungsrichtlinien für Itch.io HTML5-Spielexporte'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'paragraph',
|
|
37
|
+
html: 'Das Veröffentlichen von HTML5- und WebGL-Spielen auf Itch.io erfordert eine präzise Konfiguration von Viewport-Dimensionen, Archivdateistrukturen und Cross-Origin-Sicherheitsheadern.'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: 'stats',
|
|
41
|
+
columns: 4,
|
|
42
|
+
items: [
|
|
43
|
+
{ label: 'Standard-Web-Seitenverhältnis', value: '16:9 Querformat' },
|
|
44
|
+
{ label: 'Klassische Itch-Auflösung', value: '960 x 540 px' },
|
|
45
|
+
{ label: 'Einstiegspunkt-Datei', value: 'index.html im Root' },
|
|
46
|
+
{ label: 'Godot 4 Anforderung', value: 'COOP / COEP Header' }
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'tip',
|
|
51
|
+
html: 'Beim Einbetten eines 1280x720 WebGL-Spiels auf Itch.io die Embed-Viewport-Dimensionen genau auf 1280x720 setzen.'
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
faq: [
|
|
55
|
+
{
|
|
56
|
+
question: 'Warum zeigt mein Godot 4-Spiel auf Itch.io einen schwarzen Bildschirm?',
|
|
57
|
+
answer: 'Godot 4 Web-Exporte verwenden WebAssembly-Multithreading, das SharedArrayBuffer-Unterstützung erfordert. Aktiviere dies in den Frame-Optionen deines Itch.io-Spiels.'
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
howTo: [
|
|
61
|
+
{ name: 'Spieldateien oder ZIP hochladen', text: 'ZIP-Archiv oder Build-Verzeichnis mit index.html per Drag & Drop hinzufügen.' },
|
|
62
|
+
{ name: 'Kompatibilitätsbericht prüfen', text: 'Automatischen Auditbericht auf root-index.html, Groß-/Kleinschreibung und Engine-Erkennung prüfen.' },
|
|
63
|
+
{ name: 'Viewport live anpassen', text: 'Auflösungsregler und Seitenverhältnis-Chips nutzen, um das Iframe-Embedding live zu testen.' },
|
|
64
|
+
{ name: 'Itch.io-Einstellungen kopieren', text: 'Auf Einstellungen kopieren klicken, um Breite und Höhe für die Itch.io-Einreichungsseite zu übernehmen.' }
|
|
65
|
+
],
|
|
66
|
+
schemas: [
|
|
67
|
+
{
|
|
68
|
+
'@context': 'https://schema.org',
|
|
69
|
+
'@type': 'SoftwareApplication',
|
|
70
|
+
name: 'Itch.io Webspiel Inspektor',
|
|
71
|
+
applicationCategory: 'DeveloperApplication',
|
|
72
|
+
operatingSystem: 'Any'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
'@context': 'https://schema.org',
|
|
76
|
+
'@type': 'FAQPage',
|
|
77
|
+
mainEntity: [
|
|
78
|
+
{
|
|
79
|
+
'@type': 'Question',
|
|
80
|
+
name: 'Warum zeigt mein Godot 4-Spiel auf Itch.io einen schwarzen Bildschirm?',
|
|
81
|
+
acceptedAnswer: {
|
|
82
|
+
'@type': 'Answer',
|
|
83
|
+
text: 'Godot 4 Web-Exporte verwenden WebAssembly-Multithreading, das SharedArrayBuffer-Unterstützung erfordert. Aktiviere dies in den Frame-Optionen deines Itch.io-Spiels.'
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
'@context': 'https://schema.org',
|
|
90
|
+
'@type': 'HowTo',
|
|
91
|
+
name: 'So prüfst und testest du deinen Itch.io Game Viewport',
|
|
92
|
+
step: [
|
|
93
|
+
{
|
|
94
|
+
'@type': 'HowToStep',
|
|
95
|
+
name: 'Spieldateien oder ZIP hochladen',
|
|
96
|
+
text: 'ZIP-Archiv oder Build-Verzeichnis mit index.html per Drag & Drop hinzufügen.'
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
'@type': 'HowToStep',
|
|
100
|
+
name: 'Viewport live anpassen',
|
|
101
|
+
text: 'Auflösungsregler und Seitenverhältnis-Chips nutzen, um das Iframe-Embedding live zu testen.'
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
bibliography: bibliographyEntries
|
|
107
|
+
};
|