From e2cb1ff8556afff6c0cbaef00d3ba7d4ec2c035c Mon Sep 17 00:00:00 2001 From: Yuriy Mayatnikov Date: Thu, 18 Jun 2026 16:00:35 +0300 Subject: [PATCH] =?UTF-8?q?feat(assets):=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=B0=20PNG=20=D0=B8=D0=B7=20src/assets=20=D1=81?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5=D0=B4=D1=83=D1=80=D0=BD=D1=8B?= =?UTF-8?q?=D0=BC=20=D1=84=D0=BE=D0=BB=D0=B1=D1=8D=D0=BA=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assets.ts: текстуры грузятся из src/assets/.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) --- build.ts | 10 ++++++++++ dev.ts | 6 ++++++ src/assets/tear.png | Bin 0 -> 1421 bytes src/render/assets.ts | 37 ++++++++++++++++++++++--------------- 4 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 src/assets/tear.png 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 0000000000000000000000000000000000000000..0e82bd6816a7024a21c254ef165543a5e4f2c4f7 GIT binary patch literal 1421 zcmV;81#=J+I%^bo$M_nOEKKu)9o6cUN~;*Z*Ht{pRgd&iLOs0cSh``W3s^c6VKzuxq9SpPHHC8*JFBX9e)DHw%- zT*Py_8f1(@KrZ6w#<&ox6JR6#?nj@>?47G^pvSlns}cZsA{`L}9uneMTfGqLJ{GGI zpy015$nXL9?Ctj@$cR}`jR2yr$a(B(BLV?eUJ`lrVAT)G)mPOls78QBzkTcheuY&8 z637Cp)4SkyocC^1dsPTH)StZe;0X50iz1gb17a3%C&YENo(NS4$VH#XN3gLUi9xSf z&}IzdQc|3NP0^2_$O8J5g2{p^cR_IiwnZP{2xUME3A9L!EGSAqpXdV=$(4cXi)TSm z0&>xhkydbPAIc!k)w~Oe5zrO=%P*|qUK@;&1;q%k$ZSM;^w~MvCkx`(?kRZ}6d{0T z_%7)mLDS~SK=sA$f+7TnOxJ$$DuD3Wl>T9Xe#J7-Zk0F1DF=CrjlQ+LDgEub*tcb% zx|US!yM5dVu$Lb{Y7!7z9p@g1Wx#I&Y{X5JR^)rN?gZHA10XtTQ#7A3F*>}8$ z8=FlhkqVE$xAdQ%7nOv#lm6@aT70nM>DIQ_<8Vh8B82tU*8j{ziB<2t%y(X1c|Na4 zn?4HBrMA%x6&C{3iMIZ2lvFQoT;w}1WI}!>uT5;XXXaP;(+(!Us}}pYD{HUF9%m0G zz&f5w(cd44JYL#Wr~LY%*Wi-6O! z!=^pHrhDw!X$wHUITo?erhoT;k%v#ZP2Jb|+-#2I+`7KH;soHxum6ht@{h=G|E=U$ zqq_@6)01U#8XCy##qK6amJZ{K@H)v37icjuJ*gaZNnefs*Y2+{4@!Ntd# zu6gw9B%hMc+&)s3030N`kbp1-G&0u=>60Z#KYhoxAd8bln|(ncBD=PO39wG!+*^@q;(-+B1;!%wgD+Y zt_uN88H6k#N+nV?1;02sy19+8KJzXFaFC|tx3tKuk3XPs6-$7g<(6@71T+x|Spf1| zpL|H;A{IObo3rQ+7B>R$#NGvzLh3kcb`@+Oai3@OvFSf#}N+NN7#$Rz1a4Q zK6YIRz+mHGTM7U`nA=4tp=95@vAv6#t!v#0;KV;pj&Bex`UpV2ygCzF#$CeG1K_Ns zgv`1*HqF?rTZ90-BU+XObPxbIKq8D0`a$&RkpLHLlmd@W#R$N7BmoeNqW}^CJVGCd z^G!R~OZ5gensJe)C;@m300b~85zJ>K04M5Ps+oCLRdE8Mw}62N1vXicu?+VFq{K0E zDjN0SZ+Y?DZEZCItRpKj7U5Qhl!d<5B6dx!{wM@^f*We(); + 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 {