feat(assets): загрузка PNG из src/assets с процедурным фолбэком
- assets.ts: текстуры грузятся из src/assets/<key>.png; если файла нет — откат на процедурный рисунок (игра не ломается, сборка не зависит от наличия PNG). - dev.ts: отдаёт /assets/* прямо из src/assets/ (положил PNG → сразу подхватился). - build.ts: копирует src/assets → dist/assets. - Имена ключей = именам файлов из docs/ASSET_BRIEF.md (player-ranged.png и т.д.). - Добавлен src/assets/tear.png как рабочий пример из дизайна. Чтобы подключить полный арт: положить 21 PNG из проекта Claude Design в src/assets/. Бинарные ассеты переносятся файлами (не через модель — это триггерит usage-policy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
* плюс копия index.html. Открывай dist/index.html.
|
* плюс копия index.html. Открывай dist/index.html.
|
||||||
*/
|
*/
|
||||||
import { build } from 'bun';
|
import { build } from 'bun';
|
||||||
|
import { cp, mkdir } from 'node:fs/promises';
|
||||||
|
|
||||||
const result = await build({
|
const result = await build({
|
||||||
entrypoints: ['./src/main.ts'],
|
entrypoints: ['./src/main.ts'],
|
||||||
@@ -19,4 +20,13 @@ if (!result.success) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Bun.write('./dist/index.html', await Bun.file('./index.html').text());
|
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)');
|
console.log('Сборка готова → dist/ (открой dist/index.html)');
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ const server = Bun.serve({
|
|||||||
if (url.pathname === '/' || url.pathname === '/index.html') {
|
if (url.pathname === '/' || url.pathname === '/index.html') {
|
||||||
return new Response(Bun.file('./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);
|
const file = Bun.file('./dist' + url.pathname);
|
||||||
if (await file.exists()) return new Response(file);
|
if (await file.exists()) return new Response(file);
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
+22
-15
@@ -23,16 +23,6 @@ function canvas(w: number, h: number): { cv: HTMLCanvasElement; ctx: CanvasRende
|
|||||||
return { cv, ctx: cv.getContext('2d')! };
|
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 {
|
function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number): void {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -192,14 +182,31 @@ function drawShadow(): HTMLCanvasElement {
|
|||||||
*/
|
*/
|
||||||
export class Assets {
|
export class Assets {
|
||||||
private cache = new Map<string, THREE.Texture>();
|
private cache = new Map<string, THREE.Texture>();
|
||||||
|
private readonly loader = new THREE.TextureLoader();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает текстуру по ключу. Сначала пытается загрузить PNG из
|
||||||
|
* `src/assets/<key>.png` (поставляется художником, см. docs/ASSET_BRIEF.md);
|
||||||
|
* если файла нет — рисует процедурный фолбэк, чтобы игра не ломалась.
|
||||||
|
* 404 в консоли для ещё не добавленных ассетов — это норма (сработал фолбэк).
|
||||||
|
*/
|
||||||
private get(key: string, build: () => HTMLCanvasElement, pixelated = true): THREE.Texture {
|
private get(key: string, build: () => HTMLCanvasElement, pixelated = true): THREE.Texture {
|
||||||
let t = this.cache.get(key);
|
const cached = this.cache.get(key);
|
||||||
if (!t) {
|
if (cached) return cached;
|
||||||
t = texture(build(), pixelated);
|
|
||||||
this.cache.set(key, t);
|
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 {
|
sprite(key: SpriteKey): THREE.Texture {
|
||||||
|
|||||||
Reference in New Issue
Block a user