diff --git a/build.ts b/build.ts index 6c2fd7e..0c171dc 100644 --- a/build.ts +++ b/build.ts @@ -3,6 +3,7 @@ * плюс копия index.html. Открывай dist/index.html. */ import { build } from 'bun'; +import { cp, mkdir } from 'node:fs/promises'; const result = await build({ entrypoints: ['./src/main.ts'], @@ -19,4 +20,13 @@ if (!result.success) { } await Bun.write('./dist/index.html', await Bun.file('./index.html').text()); + +// Копируем картинки в dist/assets (если папка есть). +try { + await mkdir('./dist/assets', { recursive: true }); + await cp('./src/assets', './dist/assets', { recursive: true }); +} catch { + // src/assets ещё нет — не страшно, рендер откатится на процедурную графику. +} + console.log('Сборка готова → dist/ (открой dist/index.html)'); diff --git a/dev.ts b/dev.ts index 310f3da..7130d19 100644 --- a/dev.ts +++ b/dev.ts @@ -22,6 +22,12 @@ const server = Bun.serve({ if (url.pathname === '/' || url.pathname === '/index.html') { return new Response(Bun.file('./index.html')); } + // Картинки отдаём прямо из src/assets/ — положил PNG → сразу подхватился (без пересборки). + if (url.pathname.startsWith('/assets/')) { + const asset = Bun.file('./src' + url.pathname); + if (await asset.exists()) return new Response(asset); + return new Response('Not found', { status: 404 }); + } const file = Bun.file('./dist' + url.pathname); if (await file.exists()) return new Response(file); return new Response('Not found', { status: 404 }); diff --git a/src/assets/tear.png b/src/assets/tear.png new file mode 100644 index 0000000..0e82bd6 Binary files /dev/null and b/src/assets/tear.png differ diff --git a/src/render/assets.ts b/src/render/assets.ts index 167fc64..6c55946 100644 --- a/src/render/assets.ts +++ b/src/render/assets.ts @@ -23,16 +23,6 @@ function canvas(w: number, h: number): { cv: HTMLCanvasElement; ctx: CanvasRende return { cv, ctx: cv.getContext('2d')! }; } -function texture(cv: HTMLCanvasElement, pixelated = true): THREE.CanvasTexture { - const t = new THREE.CanvasTexture(cv); - t.colorSpace = THREE.SRGBColorSpace; - if (pixelated) { - t.magFilter = THREE.NearestFilter; - t.minFilter = THREE.NearestFilter; - } - return t; -} - /** Скруглённый прямоугольник (хелпер рисования). */ function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number): void { ctx.beginPath(); @@ -192,14 +182,31 @@ function drawShadow(): HTMLCanvasElement { */ export class Assets { private cache = new Map(); + private readonly loader = new THREE.TextureLoader(); + /** + * Возвращает текстуру по ключу. Сначала пытается загрузить PNG из + * `src/assets/.png` (поставляется художником, см. docs/ASSET_BRIEF.md); + * если файла нет — рисует процедурный фолбэк, чтобы игра не ломалась. + * 404 в консоли для ещё не добавленных ассетов — это норма (сработал фолбэк). + */ private get(key: string, build: () => HTMLCanvasElement, pixelated = true): THREE.Texture { - let t = this.cache.get(key); - if (!t) { - t = texture(build(), pixelated); - this.cache.set(key, t); + const cached = this.cache.get(key); + if (cached) return cached; + + const tex = this.loader.load( + `assets/${key}.png`, + undefined, + undefined, + () => { tex.image = build() as unknown as HTMLImageElement; tex.needsUpdate = true; }, // PNG нет → процедурный фолбэк + ); + tex.colorSpace = THREE.SRGBColorSpace; + if (pixelated) { + tex.magFilter = THREE.NearestFilter; + tex.minFilter = THREE.NearestFilter; } - return t; + this.cache.set(key, tex); + return tex; } sprite(key: SpriteKey): THREE.Texture {