feat(art): подключены дизайнерские ассеты + сердечки/иконки/лого в UI

- Добавлены 21 PNG из Claude Design в src/assets/ (спрайты игрока и врагов,
  пол, стены, двери, снаряд, эффекты, тень, сердечки, иконки оружия, лого, фон меню).
- HudOverlay: здоровье сердечками (2 HP = сердце) + иконка режима оружия;
  фолбэк на полосу/без иконки, если картинок нет.
- index.html: лого и фон-подземелье в стартовом меню (с тёмным затемнением).
- Мир/персонажи/двери теперь рисуются дизайнерскими спрайтами (через assets.ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 16:08:40 +03:00
co-authored by Claude Opus 4.8
parent e2cb1ff855
commit 35cb8de33a
23 changed files with 54 additions and 14 deletions
+6 -3
View File
@@ -27,11 +27,14 @@
/* Стартовое меню — поверх холстов, перекрывает сцену своим фоном. */ /* Стартовое меню — поверх холстов, перекрывает сцену своим фоном. */
#menu { #menu {
position: absolute; inset: 0; z-index: 10; position: absolute; inset: 0; z-index: 10;
background: #0a0a0fee; /* фон-картинка с тёмным затемнением для читаемости; фолбэк — тёмный цвет */
background: linear-gradient(rgba(10,10,15,0.7), rgba(10,10,15,0.82)),
url(assets/menu-bg.png) center / cover no-repeat, #0a0a0f;
display: flex; flex-direction: column; align-items: center; justify-content: center; display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 14px; padding: 24px; gap: 14px; padding: 24px;
} }
#menu h1 { font-size: 40px; letter-spacing: 4px; color: #c9b27a; } #menu h1 { font-size: 40px; letter-spacing: 4px; color: #c9b27a; margin-bottom: 4px; }
#menu-logo { display: block; width: min(560px, 80%); height: auto; }
.menu-sub { color: #888; margin-bottom: 6px; } .menu-sub { color: #888; margin-bottom: 6px; }
#menu-presets { display: flex; flex-direction: column; gap: 10px; width: 360px; max-width: 90%; } #menu-presets { display: flex; flex-direction: column; gap: 10px; width: 360px; max-width: 90%; }
.menu-preset { .menu-preset {
@@ -55,7 +58,7 @@
<!-- Стартовое меню. Кнопки уровней генерирует StartMenu из PRESETS. --> <!-- Стартовое меню. Кнопки уровней генерирует StartMenu из PRESETS. -->
<div id="menu"> <div id="menu">
<h1>Биндим Фигняшку</h1> <h1><img id="menu-logo" src="assets/logo.png" alt="Биндим Фигняшку" /></h1>
<p class="menu-sub">Выбери уровень</p> <p class="menu-sub">Выбери уровень</p>
<div id="menu-presets"></div> <div id="menu-presets"></div>
<p class="menu-hint">WASD — движение · Стрелки — стрельба · Tab — оружие · Esc — меню</p> <p class="menu-hint">WASD — движение · Стрелки — стрельба · Tab — оружие · Esc — меню</p>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

+48 -11
View File
@@ -9,6 +9,18 @@ import type { Renderer } from './Renderer';
*/ */
export class HudOverlay implements Renderer { export class HudOverlay implements Renderer {
private readonly ctx: CanvasRenderingContext2D; private readonly ctx: CanvasRenderingContext2D;
private readonly images = new Map<string, HTMLImageElement>();
/** Лениво грузит PNG из assets/<name>.png; возвращает картинку, только когда она готова. */
private img(name: string): HTMLImageElement | null {
let im = this.images.get(name);
if (!im) {
im = new Image();
im.src = `assets/${name}.png`;
this.images.set(name, im);
}
return im.complete && im.naturalWidth > 0 ? im : null;
}
constructor(canvas: HTMLCanvasElement) { constructor(canvas: HTMLCanvasElement) {
// Буфер увеличиваем под плотность пикселей (чёткий текст на HiDPI), // Буфер увеличиваем под плотность пикселей (чёткий текст на HiDPI),
@@ -40,20 +52,12 @@ export class HudOverlay implements Renderer {
const p = game.player; const p = game.player;
const room = game.curRoom; const room = game.curRoom;
// Полоса здоровья. // Здоровье: сердечки (2 HP = сердце), с фолбэком на полосу.
const bx = 20, by = 20, bw = 140, bh = 14; const healthBottom = this.drawHealth(p.hp, p.maxHp);
ctx.fillStyle = '#111'; ctx.fillRect(bx, by, bw, bh);
ctx.fillStyle = '#2a0a0a'; ctx.fillRect(bx + 2, by + 2, bw - 4, bh - 4);
const hpRatio = Math.max(0, p.hp / p.maxHp);
ctx.fillStyle = hpRatio > 0.5 ? '#993333' : hpRatio > 0.25 ? '#994422' : '#663322';
ctx.fillRect(bx + 2, by + 2, (bw - 4) * hpRatio, bh - 4);
ctx.strokeStyle = '#333'; ctx.lineWidth = 1; ctx.strokeRect(bx, by, bw, bh);
ctx.fillStyle = '#bbb'; ctx.font = '10px monospace'; ctx.textAlign = 'center';
ctx.fillText(`HP ${p.hp}/${p.maxHp}`, bx + bw / 2, by + bh - 3);
// Название текущего уровня (правил). // Название текущего уровня (правил).
ctx.textAlign = 'left'; ctx.fillStyle = '#667'; ctx.font = '11px monospace'; ctx.textAlign = 'left'; ctx.fillStyle = '#667'; ctx.font = '11px monospace';
ctx.fillText(`Уровень: ${game.rules.name}`, bx, by + bh + 14); ctx.fillText(`Уровень: ${game.rules.name}`, 20, healthBottom + 14);
// Индикатор режима боя. // Индикатор режима боя.
const my = CH - 46; const my = CH - 46;
@@ -65,6 +69,9 @@ export class HudOverlay implements Renderer {
ctx.strokeStyle = mCol; ctx.lineWidth = 2; ctx.strokeRect(CW / 2 - 95, my - 18, 190, 34); ctx.strokeStyle = mCol; ctx.lineWidth = 2; ctx.strokeRect(CW / 2 - 95, my - 18, 190, 34);
ctx.fillStyle = mCol; ctx.font = 'bold 17px monospace'; ctx.fillText(`[ ${mText} ]`, CW / 2, my + 8); ctx.fillStyle = mCol; ctx.font = 'bold 17px monospace'; ctx.fillText(`[ ${mText} ]`, CW / 2, my + 8);
ctx.fillStyle = '#555'; ctx.font = '11px monospace'; ctx.fillText('[Tab] сменить оружие', CW / 2, my - 26); ctx.fillStyle = '#555'; ctx.font = '11px monospace'; ctx.fillText('[Tab] сменить оружие', CW / 2, my - 26);
// Иконка оружия слева в рамке (если ассет есть).
const icon = this.img(ranged ? 'icon-ranged' : 'icon-melee');
if (icon) ctx.drawImage(icon, CW / 2 - 90, my - 14, 26, 26);
// Счётчик врагов / подсказка зачистки. // Счётчик врагов / подсказка зачистки.
ctx.textAlign = 'left'; ctx.textAlign = 'left';
@@ -87,6 +94,36 @@ export class HudOverlay implements Renderer {
} }
} }
/** Рисует здоровье сердечками (2 HP = сердце); фолбэк — полоса. Возвращает нижний Y. */
private drawHealth(hp: number, maxHp: number): number {
const ctx = this.ctx;
const x0 = 20, y0 = 16;
const full = this.img('heart-full');
const half = this.img('heart-half');
const empty = this.img('heart-empty');
if (full && half && empty) {
const sz = 26, gap = 2;
const slots = Math.ceil(maxHp / 2);
for (let i = 0; i < slots; i++) {
const rem = hp - i * 2;
const im = rem >= 2 ? full : rem === 1 ? half : empty;
ctx.drawImage(im, x0 + i * (sz + gap), y0, sz, sz);
}
return y0 + sz;
}
// Фолбэк — полоса HP.
const by = 20, bw = 140, bh = 14;
ctx.fillStyle = '#111'; ctx.fillRect(x0, by, bw, bh);
ctx.fillStyle = '#2a0a0a'; ctx.fillRect(x0 + 2, by + 2, bw - 4, bh - 4);
const ratio = Math.max(0, hp / maxHp);
ctx.fillStyle = ratio > 0.5 ? '#993333' : ratio > 0.25 ? '#994422' : '#663322';
ctx.fillRect(x0 + 2, by + 2, (bw - 4) * ratio, bh - 4);
ctx.strokeStyle = '#333'; ctx.lineWidth = 1; ctx.strokeRect(x0, by, bw, bh);
ctx.fillStyle = '#bbb'; ctx.font = '10px monospace'; ctx.textAlign = 'center';
ctx.fillText(`HP ${hp}/${maxHp}`, x0 + bw / 2, by + bh - 3);
return by + bh;
}
private drawMinimap(game: Game): void { private drawMinimap(game: Game): void {
const ctx = this.ctx; const ctx = this.ctx;
const ox = CW - 180, oy = 12, cell = 14, gap = 2, cs = cell + gap; const ox = CW - 180, oy = 12, cell = 14, gap = 2, cs = cell + gap;