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
Binary file added docs/assets/screenshots/playwright/world-size.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/getting-started/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
210 changes: 110 additions & 100 deletions docs/getting-started/pattern-demo.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ This guide is the in-repo user guide for PhaserForge. It turns the current workf
## Troubleshooting

- [GitHub Pages Publish Troubleshooting](./troubleshooting/github-pages-publish)

## Music Credits
- [List](./reference/credits)

26 changes: 26 additions & 0 deletions docs/reference/credits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Credits:

- In Dreams by Scott Buckley | www.scottbuckley.com.au

Music promoted by https://www.chosic.com/free-music/all/

Attribution 4.0 International (CC BY 4.0)

https://creativecommons.org/licenses/by/4.0/

- Simulacra by Scott Buckley | www.scottbuckley.com.au

Music promoted by https://www.chosic.com/free-music/all/

Creative Commons CC BY 4.0

https://creativecommons.org/licenses/by/4.0/

- The Soul-Crushing Monotony Of Isolation (Instrumental Mix) by Punch Deck | https://soundcloud.com/punch-deck

Music promoted by https://www.chosic.com/free-music/all/

Creative Commons Attribution 3.0 Unported License

https://creativecommons.org/licenses/by/3.0/deed.en_US

Binary file added res/audio/Simulacra-chosic.com_.mp3
Binary file not shown.
Binary file not shown.
Binary file added res/audio/sb_indreams(chosic.com).mp3
Binary file not shown.
136 changes: 111 additions & 25 deletions src/editor/AssetsDock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,92 @@ import { useEffect, useMemo, useRef, useState, type ChangeEvent } from 'react';
import type { ProjectSpec } from '../model/types';
import type { EditorAction, Selection } from './EditorStore';
import { getAssetReferences, type AssetKind } from './assetReferences';
import { assetIdBaseFromOriginalName, getDemoPackAssetKind } from './demoPackAssets';
import { ASSET_DRAG_MIME } from './dragAssets';
import { fileToDataUrl } from './fileDataUrl';
import { loadImageMetadataFromFile, type LoadedImageMetadata } from './imageMetadata';

const DEMO_PACK_IMAGES = import.meta.glob('../../res/images/*.png', {
eager: true,
query: '?url',
import: 'default',
}) as Record<string, string>;
const DEMO_PACK_ASSETS = {
...import.meta.glob('../../res/images/*.png', {
eager: true,
query: '?url',
import: 'default',
}),
...import.meta.glob('../../res/images/*.jpg', {
eager: true,
query: '?url',
import: 'default',
}),
...import.meta.glob('../../res/images/*.jpeg', {
eager: true,
query: '?url',
import: 'default',
}),
...import.meta.glob('../../res/images/*.webp', {
eager: true,
query: '?url',
import: 'default',
}),
...import.meta.glob('../../res/audio/*.mp3', {
eager: true,
query: '?url',
import: 'default',
}),
...import.meta.glob('../../res/audio/*.ogg', {
eager: true,
query: '?url',
import: 'default',
}),
...import.meta.glob('../../res/audio/*.wav', {
eager: true,
query: '?url',
import: 'default',
}),
...import.meta.glob('../../res/fonts/*.ttf', {
eager: true,
query: '?url',
import: 'default',
}),
...import.meta.glob('../../res/fonts/*.otf', {
eager: true,
query: '?url',
import: 'default',
}),
...import.meta.glob('../../res/fonts/*.woff', {
eager: true,
query: '?url',
import: 'default',
}),
...import.meta.glob('../../res/fonts/*.woff2', {
eager: true,
query: '?url',
import: 'default',
}),
} as Record<string, string>;

const DEMO_PACK_FONT_EXTENSIONS = /\.(ttf|otf|woff|woff2)$/i;

const DEMO_PACK_IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|webp)$/i;

const DEMO_PACK_AUDIO_EXTENSIONS = /\.(mp3|ogg|wav)$/i;

const DEVICE_FONT_EXTENSIONS = /\.(ttf|otf|woff|woff2)$/i;

function isFontFilename(name: string): boolean {
return DEVICE_FONT_EXTENSIONS.test(name);
}

function isDemoPackImageFilename(path: string): boolean {
return DEMO_PACK_IMAGE_EXTENSIONS.test(path);
}

function isDemoPackAudioFilename(path: string): boolean {
return DEMO_PACK_AUDIO_EXTENSIONS.test(path);
}

function isDemoPackFontFilename(path: string): boolean {
return DEMO_PACK_FONT_EXTENSIONS.test(path);
}

async function readAsDataUrl(file: File): Promise<string> {
return fileToDataUrl(file);
Expand Down Expand Up @@ -47,17 +124,6 @@ function usageBadgesForAudio(project: ProjectSpec, assetId: string): Array<'MUS'
];
}

function assetIdBaseFromOriginalName(name: string | undefined, fallbackBase: string = 'asset'): string {
const raw = (name ?? '').trim();
const withoutExt = raw.replace(/\.[a-z0-9]+$/i, '');
const base = withoutExt.length > 0 ? withoutExt : fallbackBase;
return base
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '') || fallbackBase;
}

async function readUrlAsDataUrl(url: string): Promise<{ dataUrl: string; mimeType?: string }> {
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to load asset (${res.status})`);
Expand Down Expand Up @@ -153,7 +219,7 @@ export function AssetsDock({
try {
for (const file of files) {
const lower = file.name.toLowerCase();
const isFont = /\.(ttf|otf|woff|woff2)$/.test(lower);
const isFont = isFontFilename(lower);
if (file.type.startsWith('image/')) {
const dataUrl = await readAsDataUrl(file);
const meta = toLoadedImage(await loadImageMetadataFromFile(file, dataUrl));
Expand All @@ -180,19 +246,39 @@ export function AssetsDock({
if (demoPackImporting) return;
setDemoPackImporting(true);
try {
const urls = Object.entries(DEMO_PACK_IMAGES)
const urls = Object.entries(DEMO_PACK_ASSETS)
.map(([path, url]) => ({ path, url }))
.sort((a, b) => a.path.localeCompare(b.path));
for (const { path, url } of urls) {
const kind = getDemoPackAssetKind(path);
if (!kind) continue;
const filename = path.split('/').pop() ?? 'image.png';
const assetId = assetIdBaseFromOriginalName(filename, 'image');
const { dataUrl, mimeType } = await readUrlAsDataUrl(url);
const meta = toLoadedImage(await loadImageMetadataFromFile(new File([], filename), dataUrl));
dispatch({
type: 'ensure-image-asset-from-file',
assetId,
file: { dataUrl, originalName: filename, mimeType, width: meta.width, height: meta.height },
} as any);
if (kind === 'image' && isDemoPackImageFilename(path)) {
const assetId = assetIdBaseFromOriginalName(filename, 'image');
const meta = toLoadedImage(await loadImageMetadataFromFile(new File([], filename), dataUrl));
dispatch({
type: 'ensure-image-asset-from-file',
assetId,
file: { dataUrl, originalName: filename, mimeType, width: meta.width, height: meta.height },
} as any);
continue;
}
if (kind === 'audio' && isDemoPackAudioFilename(path)) {
dispatch({
type: 'ensure-audio-asset-from-file',
assetId: assetIdBaseFromOriginalName(filename, 'sound'),
file: { dataUrl, originalName: filename, mimeType },
} as any);
continue;
}
if (kind === 'font' && isDemoPackFontFilename(path)) {
dispatch({
type: 'ensure-font-asset-from-file',
assetId: assetIdBaseFromOriginalName(filename, 'font'),
file: { dataUrl, originalName: filename, mimeType },
} as any);
}
}
} catch (err) {
setImportError(err instanceof Error ? err.message : 'Failed to import demo pack');
Expand Down
63 changes: 63 additions & 0 deletions src/editor/EditorStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ export type EditorAction =
| { type: 'ensure-image-asset-from-file'; assetId: Id; file: { dataUrl: string; originalName?: string; mimeType?: string; width?: number; height?: number } }
| { type: 'add-spritesheet-asset-from-file'; file: { dataUrl: string; originalName?: string; mimeType?: string }; grid: { frameWidth: number; frameHeight: number; columns: number; rows: number } }
| { type: 'add-font-asset-from-file'; file: { dataUrl: string; originalName?: string; mimeType?: string } }
| { type: 'ensure-font-asset-from-file'; assetId: Id; file: { dataUrl: string; originalName?: string; mimeType?: string } }
| { type: 'set-asset-display-name'; assetKind: 'image' | 'spritesheet' | 'audio' | 'font'; assetId: Id; name?: string }
| { type: 'remove-asset'; assetKind: 'image' | 'spritesheet' | 'audio' | 'font'; assetId: Id }
| { type: 'create-entity-from-asset'; assetKind: 'image' | 'spritesheet'; assetId: Id; at?: { x: number; y: number } }
Expand All @@ -281,6 +282,7 @@ export type EditorAction =
| { kind: 'entity-sprite'; sceneId: Id; entityId: Id };
}
| { type: 'add-audio-asset-from-file'; file: { dataUrl: string; originalName?: string; mimeType?: string } }
| { type: 'ensure-audio-asset-from-file'; assetId: Id; file: { dataUrl: string; originalName?: string; mimeType?: string } }
| { type: 'remove-audio-asset'; assetId: Id }
| { type: 'set-scene-music'; music: GameSceneSpec['music'] | undefined }
| { type: 'set-scene-ambience'; ambience: NonNullable<GameSceneSpec['ambience']> }
Expand Down Expand Up @@ -738,11 +740,13 @@ function isUndoableAction(action: EditorAction): boolean {
case 'add-image-asset-from-file':
case 'add-spritesheet-asset-from-file':
case 'add-font-asset-from-file':
case 'ensure-font-asset-from-file':
case 'set-asset-display-name':
case 'remove-asset':
case 'create-entity-from-asset':
case 'assign-asset-to-target':
case 'add-audio-asset-from-file':
case 'ensure-audio-asset-from-file':
case 'remove-audio-asset':
case 'set-scene-music':
case 'set-scene-ambience':
Expand Down Expand Up @@ -779,9 +783,11 @@ function getHistoryScope(action: EditorAction): HistoryScope {
case 'add-image-asset-from-file':
case 'add-spritesheet-asset-from-file':
case 'add-font-asset-from-file':
case 'ensure-font-asset-from-file':
case 'set-asset-display-name':
case 'remove-asset':
case 'add-audio-asset-from-file':
case 'ensure-audio-asset-from-file':
case 'remove-audio-asset':
case 'create-input-map':
case 'duplicate-input-map':
Expand Down Expand Up @@ -1413,6 +1419,36 @@ function applyAction(state: EditorState, action: EditorAction): EditorState {
error: undefined,
};
}
case 'ensure-audio-asset-from-file': {
const assetId = (action.assetId ?? '').trim();
if (!assetId) return state;
const sounds = state.project.audio?.sounds ?? {};
if (sounds[assetId]) return state;
const nextProject: ProjectSpec = {
...state.project,
audio: {
...state.project.audio,
sounds: {
...sounds,
[assetId]: {
id: assetId,
source: {
kind: 'embedded',
dataUrl: action.file.dataUrl,
...(action.file.originalName ? { originalName: action.file.originalName } : {}),
...(action.file.mimeType ? { mimeType: action.file.mimeType } : {}),
},
},
},
},
};
return {
...state,
project: nextProject,
dirty: true,
error: undefined,
};
}
case 'remove-audio-asset': {
const sounds = state.project.audio?.sounds ?? {};
if (!sounds[action.assetId]) return state;
Expand Down Expand Up @@ -1778,6 +1814,33 @@ function applyAction(state: EditorState, action: EditorAction): EditorState {
};
return { ...state, project: nextProject, dirty: true, error: undefined };
}
case 'ensure-font-asset-from-file': {
const assetId = (action.assetId ?? '').trim();
if (!assetId) return state;
const fonts = state.project.assets.fonts ?? {};
if (fonts[assetId]) return state;
const rawName = (action.file.originalName ?? assetId).replace(/\.[a-z0-9]+$/i, '').trim();
const nextProject: ProjectSpec = {
...state.project,
assets: {
...state.project.assets,
fonts: {
...fonts,
[assetId]: {
id: assetId,
...(rawName ? { name: rawName } : {}),
source: {
kind: 'embedded',
dataUrl: action.file.dataUrl,
...(action.file.originalName ? { originalName: action.file.originalName } : {}),
...(action.file.mimeType ? { mimeType: action.file.mimeType } : {}),
},
},
},
},
};
return { ...state, project: nextProject, dirty: true, error: undefined };
}
case 'set-asset-display-name': {
const nextName = (action.name ?? '').trim();
const name = nextName.length > 0 ? nextName : undefined;
Expand Down
27 changes: 27 additions & 0 deletions src/editor/demoPackAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type DemoPackAssetKind = 'image' | 'audio' | 'font';

const DEMO_PACK_IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'webp']);
const DEMO_PACK_AUDIO_EXTENSIONS = new Set(['mp3', 'ogg', 'wav']);
const DEMO_PACK_FONT_EXTENSIONS = new Set(['ttf', 'otf', 'woff', 'woff2']);

export function assetIdBaseFromOriginalName(name: string | undefined, fallbackBase: string = 'asset'): string {
const raw = (name ?? '').trim();
const withoutExt = raw.replace(/\.[a-z0-9]+$/i, '');
const base = withoutExt.length > 0 ? withoutExt : fallbackBase;
return base
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '') || fallbackBase;
}

export function getDemoPackAssetKind(path: string): DemoPackAssetKind | null {
const normalized = path.toLowerCase();
const match = normalized.match(/\.([a-z0-9]+)$/);
const extension = match?.[1];
if (!extension) return null;
if (normalized.includes('/res/images/') && DEMO_PACK_IMAGE_EXTENSIONS.has(extension)) return 'image';
if (normalized.includes('/res/audio/') && DEMO_PACK_AUDIO_EXTENSIONS.has(extension)) return 'audio';
if (normalized.includes('/res/fonts/') && DEMO_PACK_FONT_EXTENSIONS.has(extension)) return 'font';
return null;
}
Loading
Loading