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>
@@ -27,11 +27,14 @@
|
||||
/* Стартовое меню — поверх холстов, перекрывает сцену своим фоном. */
|
||||
#menu {
|
||||
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;
|
||||
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-presets { display: flex; flex-direction: column; gap: 10px; width: 360px; max-width: 90%; }
|
||||
.menu-preset {
|
||||
@@ -55,7 +58,7 @@
|
||||
|
||||
<!-- Стартовое меню. Кнопки уровней генерирует StartMenu из PRESETS. -->
|
||||
<div id="menu">
|
||||
<h1>Биндим Фигняшку</h1>
|
||||
<h1><img id="menu-logo" src="assets/logo.png" alt="Биндим Фигняшку" /></h1>
|
||||
<p class="menu-sub">Выбери уровень</p>
|
||||
<div id="menu-presets"></div>
|
||||
<p class="menu-hint">WASD — движение · Стрелки — стрельба · Tab — оружие · Esc — меню</p>
|
||||
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 934 B |
|
After Width: | Height: | Size: 621 B |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 305 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
@@ -9,6 +9,18 @@ import type { Renderer } from './Renderer';
|
||||
*/
|
||||
export class HudOverlay implements Renderer {
|
||||
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) {
|
||||
// Буфер увеличиваем под плотность пикселей (чёткий текст на HiDPI),
|
||||
@@ -40,20 +52,12 @@ export class HudOverlay implements Renderer {
|
||||
const p = game.player;
|
||||
const room = game.curRoom;
|
||||
|
||||
// Полоса здоровья.
|
||||
const bx = 20, by = 20, bw = 140, bh = 14;
|
||||
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);
|
||||
// Здоровье: сердечки (2 HP = сердце), с фолбэком на полосу.
|
||||
const healthBottom = this.drawHealth(p.hp, p.maxHp);
|
||||
|
||||
// Название текущего уровня (правил).
|
||||
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;
|
||||
@@ -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.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);
|
||||
// Иконка оружия слева в рамке (если ассет есть).
|
||||
const icon = this.img(ranged ? 'icon-ranged' : 'icon-melee');
|
||||
if (icon) ctx.drawImage(icon, CW / 2 - 90, my - 14, 26, 26);
|
||||
|
||||
// Счётчик врагов / подсказка зачистки.
|
||||
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 {
|
||||
const ctx = this.ctx;
|
||||
const ox = CW - 180, oy = 12, cell = 14, gap = 2, cs = cell + gap;
|
||||
|
||||