feat: инвентарь 2 слота, 4 новых оружия, 3 врага, босс-фазы, иконки пикапов

This commit is contained in:
Volodia
2026-06-19 13:09:28 +03:00
parent d2d1d2381d
commit 01c1f71159
19 changed files with 946 additions and 74 deletions
+59 -11
View File
@@ -1,4 +1,4 @@
import { CW, CH, OY, RH, MODE_RANGED } from '../config';
import { CW, CH, OY, RH } from '../config';
import type { Game } from '../core/Game';
import type { Renderer } from './Renderer';
@@ -36,6 +36,11 @@ export class HudOverlay implements Renderer {
const ctx = this.ctx;
ctx.clearRect(0, 0, CW, CH);
if (game.inventoryOpen) {
this.drawInventory(game);
return;
}
this.drawHud(game);
this.drawMinimap(game);
@@ -55,22 +60,21 @@ export class HudOverlay implements Renderer {
// Здоровье: сердечки (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}`, 20, healthBottom + 14);
const floorLabel = game.rules.endless ? ` | Этаж ${game.floor}` : '';
ctx.fillText(`${game.rules.name}${floorLabel}`, 20, healthBottom + 14);
// Индикатор режима боя.
// Индикатор оружия.
const my = CH - 46;
const ranged = p.mode === MODE_RANGED;
const mText = ranged ? 'ДАЛЬНИЙ' : 'БЛИЖНИЙ';
const mCol = ranged ? '#4488cc' : '#cc6644';
const w = p.currentWeapon;
const wCol = w.type === 'ranged' ? '#4488cc' : '#cc6644';
ctx.textAlign = 'center';
ctx.fillStyle = '#0d0d0d'; ctx.fillRect(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.strokeStyle = wCol; ctx.lineWidth = 2; ctx.strokeRect(CW / 2 - 95, my - 18, 190, 34);
ctx.fillStyle = wCol; ctx.font = 'bold 17px monospace'; ctx.fillText(`[ ${w.name} ]`, 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');
const icon = this.img(w.type === 'ranged' ? 'icon-ranged' : 'icon-melee');
if (icon) ctx.drawImage(icon, CW / 2 - 90, my - 14, 26, 26);
// Счётчик врагов / подсказка зачистки.
@@ -167,4 +171,48 @@ export class HudOverlay implements Renderer {
ctx.fillStyle = '#666'; ctx.font = '14px monospace';
ctx.fillText('[Esc] в меню', CW / 2, CH / 2 + 68);
}
private drawInventory(game: import('../core/Game').Game): void {
const ctx = this.ctx;
ctx.fillStyle = 'rgba(0,0,0,0.85)'; ctx.fillRect(0, 0, CW, CH);
ctx.fillStyle = '#ddd'; ctx.font = 'bold 28px monospace'; ctx.textAlign = 'center';
ctx.fillText('ИНВЕНТАРЬ (2 слота)', CW / 2, 50);
const p = game.player;
const iw = 340, ih = 56, gap = 16;
const total = iw * 2 + gap;
const ox = CW / 2 - total / 2;
const oy = 100;
for (let i = 0; i < 2; i++) {
const w = p.weapons[i];
const x = ox + i * (iw + gap);
const y = oy;
const selected = i === p.equipped;
ctx.fillStyle = selected ? '#1a2a1a' : '#111';
ctx.fillRect(x, y, iw, ih);
ctx.strokeStyle = selected ? '#4c4' : '#333';
ctx.lineWidth = selected ? 2 : 1;
ctx.strokeRect(x, y, iw, ih);
ctx.fillStyle = '#888'; ctx.font = '14px monospace'; ctx.textAlign = 'left';
ctx.fillText(`[${i + 1}]`, x + 12, y + 34);
ctx.fillStyle = selected ? '#4c4' : '#ccc'; ctx.font = 'bold 16px monospace';
ctx.fillText(w.name, x + 48, y + 34);
ctx.fillStyle = '#666'; ctx.font = '12px monospace';
ctx.textAlign = 'right';
const t = w.type === 'ranged' ? 'ДАЛЬНИЙ' : 'БЛИЖНИЙ';
ctx.fillText(`${t} DMG:${w.damage} CD:${w.cooldown}`, x + iw - 12, y + 34);
const icon = this.img(w.type === 'ranged' ? 'icon-ranged' : 'icon-melee');
if (icon) ctx.drawImage(icon, x + iw - 44, y + 16, 24, 24);
}
ctx.fillStyle = '#555'; ctx.font = '13px monospace'; ctx.textAlign = 'center';
ctx.fillText('[E] закрыть | [1] [2] слот | [Tab] переключить', CW / 2, CH - 30);
}
}
+80 -6
View File
@@ -8,6 +8,7 @@ import type { Game } from '../core/Game';
import type { Room } from '../core/world/Room';
import type { Enemy } from '../core/entities/Enemy';
import type { Projectile } from '../core/entities/Projectile';
import type { WeaponId } from '../core/weapons';
import type { Renderer } from './Renderer';
import { DEFAULT_THEME, type Theme } from './theme';
import { Assets, type SpriteKey } from './assets';
@@ -53,6 +54,7 @@ export class ThreeRenderer implements Renderer {
private readonly playerMat: Record<'ranged' | 'melee', THREE.MeshBasicMaterial>;
private readonly enemyMatKey: Record<Enemy['type'], SpriteKey> = {
normal: 'enemy-normal', fast: 'enemy-fast', boss: 'enemy-boss',
charger: 'enemy-charger', tank: 'enemy-tank', shooter: 'enemy-shooter',
};
// Группа статичной геометрии комнаты (пол + стены + двери).
@@ -69,6 +71,10 @@ export class ThreeRenderer implements Renderer {
private readonly effects: Effect[] = [];
private lastAtkCD = 0;
private chestMesh: THREE.Mesh | null = null;
private pickupMesh: THREE.Mesh | null = null;
private currentPickupWeapon: WeaponId | null = null;
constructor(canvas: HTMLCanvasElement, theme: Theme = DEFAULT_THEME) {
this.theme = theme;
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
@@ -130,6 +136,8 @@ export class ThreeRenderer implements Renderer {
this.syncEnemies(room, alpha);
this.syncTears(room, alpha);
this.syncSwing(game);
this.syncChest(room);
this.syncPickup(room);
this.updateEffects();
this.renderer.render(this.scene, this.camera);
@@ -239,18 +247,27 @@ export class ThreeRenderer implements Renderer {
const x = lerp(e.prevX, e.x, alpha);
const z = lerp(e.prevY, e.y, alpha);
const pop = 1 + 0.18 * (e.hitTimer / Math.max(1, MELEE.life)); // «дёргается» при попадании
const pop = 1 + 0.18 * (e.hitTimer / Math.max(1, MELEE.life));
const w = e.w * SPRITE_SCALE * pop;
const h = w * SPRITE_ASPECT;
v.sprite.scale.set(w, h, 1);
v.sprite.position.set(x, h / 2, z);
this.placeShadow(v.shadow, x, z, e.w);
// Искра в момент попадания (hitTimer вырос).
if (e.hitTimer > v.lastHit) {
this.spawnEffect(this.assets.spark(), this.theme.flash, x, e.w * 0.6, z, e.w * 0.9, 8, { vy: 0.6, grow: 1.06 });
// Увеличенный эффект для смены фазы босса (15+ тиков)
const size = e.hitTimer >= 12 ? e.w * 1.5 : e.w * 0.9;
const life = e.hitTimer >= 12 ? 16 : 8;
this.spawnEffect(this.assets.puff(), 0xff6600, x, e.w * 0.6, z, size, life, { vy: 0.8, grow: 1.06 });
}
v.lastHit = e.hitTimer;
// Цвет подкраски по фазе босса.
let phaseTint = 0xffffff;
if (e.burnTimer > 0) phaseTint = 0xff6644;
else if (e.type === 'boss' && e.phase === 2) phaseTint = 0xff8844;
else if (e.type === 'boss' && e.phase >= 3) phaseTint = 0xff3300;
(v.sprite.material as THREE.MeshBasicMaterial).color.setHex(phaseTint);
}
// Уборка: исчезнувшие враги. Если враг мёртв — «пуф» на месте гибели.
@@ -272,15 +289,21 @@ export class ThreeRenderer implements Renderer {
live.add(t);
let mesh = this.tearMeshes.get(t);
if (!mesh) {
const tex =
t.type === 'fireball' ? this.assets.fireball() :
t.type === 'beam' ? this.assets.sprite('beam') :
this.assets.tear();
mesh = new THREE.Mesh(this.vGeo, new THREE.MeshBasicMaterial({
map: this.assets.tear(), transparent: true, blending: THREE.AdditiveBlending, depthWrite: false,
map: tex, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false,
}));
const s = PROJECTILE.radius * 4;
const s = t.type === 'fireball' ? PROJECTILE.radius * 5 :
t.type === 'beam' ? 50 :
PROJECTILE.radius * 4;
mesh.scale.set(s, s, 1);
this.scene.add(mesh);
this.tearMeshes.set(t, mesh);
}
mesh.position.set(lerp(t.prevX, t.x, alpha), TEAR_Y, lerp(t.prevY, t.y, alpha));
mesh.position.set(lerp(t.prevX, t.x, alpha), t.type === 'beam' ? 10 : TEAR_Y, lerp(t.prevY, t.y, alpha));
}
for (const [t, mesh] of this.tearMeshes) {
if (live.has(t)) continue;
@@ -299,6 +322,55 @@ export class ThreeRenderer implements Renderer {
(this.swingMesh.material as THREE.MeshBasicMaterial).opacity = 0.8 * (s.life / MELEE.life);
}
private syncChest(room: Room): void {
if (room.chest?.alive) {
if (!this.chestMesh) {
const mat = new THREE.MeshBasicMaterial({
map: this.assets.sprite('chest'), transparent: true, alphaTest: 0.3, side: THREE.DoubleSide,
});
this.chestMesh = new THREE.Mesh(this.vGeo, mat);
this.scene.add(this.chestMesh);
}
const c = room.chest;
const w = c.w * 1.2;
const h = w * (64 / 48);
this.chestMesh.scale.set(w, h, 1);
this.chestMesh.position.set(c.x, h / 2, c.y);
} else if (this.chestMesh) {
this.scene.remove(this.chestMesh);
(this.chestMesh.material as THREE.Material).dispose();
this.chestMesh = null;
}
}
private syncPickup(room: Room): void {
if (room.pickup) {
const wid = room.pickup.weaponId;
if (!this.pickupMesh || this.currentPickupWeapon !== wid) {
if (this.pickupMesh) {
this.scene.remove(this.pickupMesh);
(this.pickupMesh.material as THREE.Material).dispose();
}
const mat = new THREE.MeshBasicMaterial({
map: this.assets.weaponIcon(wid), transparent: true, alphaTest: 0.3, side: THREE.DoubleSide,
});
this.pickupMesh = new THREE.Mesh(this.vGeo, mat);
this.scene.add(this.pickupMesh);
this.currentPickupWeapon = wid;
}
const p = room.pickup;
const w = p.w * 1.6;
const h = w * (48 / 48);
this.pickupMesh.scale.set(w, h, 1);
this.pickupMesh.position.set(p.x, h / 2 + Math.sin(Date.now() / 200) * 3, p.y);
} else if (this.pickupMesh) {
this.scene.remove(this.pickupMesh);
(this.pickupMesh.material as THREE.Material).dispose();
this.pickupMesh = null;
this.currentPickupWeapon = null;
}
}
// ── Эффекты (частицы-биллборды) ───────────────────────────
private spawnEffect(
@@ -362,6 +434,8 @@ export class ThreeRenderer implements Renderer {
for (const m of this.tearMeshes.values()) (m.material as THREE.Material).dispose();
for (const fx of this.effects) (fx.mesh.material as THREE.Material).dispose();
(this.swingMesh.material as THREE.Material).dispose();
if (this.chestMesh) (this.chestMesh.material as THREE.Material).dispose();
if (this.pickupMesh) (this.pickupMesh.material as THREE.Material).dispose();
this.floorMat.dispose();
this.wallMat.dispose();
this.shadowMat.dispose();
+207 -1
View File
@@ -1,4 +1,5 @@
import * as THREE from 'three';
import type { WeaponId } from '../core/weapons';
/**
* assets.ts — поставщик текстур. Каждая текстура грузится из
@@ -14,7 +15,9 @@ import * as THREE from 'three';
export type SpriteKey =
| 'player-ranged' | 'player-melee'
| 'enemy-normal' | 'enemy-fast' | 'enemy-boss';
| 'enemy-normal' | 'enemy-fast' | 'enemy-boss'
| 'enemy-charger' | 'enemy-tank' | 'enemy-shooter'
| 'chest' | 'pickup' | 'fireball' | 'beam';
function canvas(w: number, h: number): { cv: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
const cv = document.createElement('canvas');
@@ -166,6 +169,183 @@ function drawDoor(open: boolean): HTMLCanvasElement {
return cv;
}
/** Сундук: тёмный ящик с золотым ободком. */
function drawChest(): HTMLCanvasElement {
const { cv, ctx } = canvas(48, 48);
ctx.fillStyle = '#5a3a1a';
roundRect(ctx, 4, 4, 40, 40, 4);
ctx.fill();
ctx.strokeStyle = '#8a6a2a';
ctx.lineWidth = 3;
roundRect(ctx, 4, 4, 40, 40, 4);
ctx.stroke();
ctx.fillStyle = '#c9a84a';
ctx.fillRect(12, 18, 24, 10);
ctx.fillStyle = '#8a6a2a';
ctx.fillRect(22, 14, 4, 18);
ctx.fillStyle = '#3a220a';
ctx.beginPath(); ctx.arc(24, 24, 4, 0, Math.PI * 2); ctx.fill();
return cv;
}
/** Пикап оружия: парящий ромб со свечением. */
function drawPickup(): HTMLCanvasElement {
const { cv, ctx } = canvas(32, 32);
const g = ctx.createRadialGradient(16, 16, 1, 16, 16, 15);
g.addColorStop(0, '#ffdd88');
g.addColorStop(0.4, '#cc8822');
g.addColorStop(1, 'rgba(200,100,0,0)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, 32, 32);
ctx.fillStyle = '#ffcc44';
ctx.beginPath();
ctx.moveTo(16, 4); ctx.lineTo(28, 16); ctx.lineTo(16, 28); ctx.lineTo(4, 16); ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#aa6600';
ctx.lineWidth = 1.5;
ctx.stroke();
return cv;
}
/** Зарядчик: красный, агрессивный вид, щель глаза. */
function drawCharger(): HTMLCanvasElement {
return drawCharacter({ head: '#cc4422', body: '#882211', outline: '#330a04', eye: '#ffaa00', small: false });
}
/** Танк: большой, тёмный, тяжёлый. */
function drawTank(): HTMLCanvasElement {
return drawCharacter({ head: '#554433', body: '#443322', outline: '#1a110a', eye: '#ff6622', horns: true });
}
/** Стрелок: синеватый, с «шапкой». */
function drawShooter(): HTMLCanvasElement {
return drawCharacter({ head: '#4488aa', body: '#336688', outline: '#122436', eye: '#aaddff', small: false });
}
/** Лазерный луч: яркая бело-голубая полоса. */
function drawBeam(): HTMLCanvasElement {
const { cv, ctx } = canvas(64, 64);
const g = ctx.createRadialGradient(32, 32, 2, 32, 32, 30);
g.addColorStop(0, '#ffffff');
g.addColorStop(0.2, '#88ddff');
g.addColorStop(0.5, '#4488ff');
g.addColorStop(1, 'rgba(0,50,200,0)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, 64, 64);
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.fillRect(20, 28, 24, 8);
return cv;
}
/** Огненный шар: красно-оранжевый с бликом. */
function drawFireball(): HTMLCanvasElement {
const { cv, ctx } = canvas(32, 32);
const g = ctx.createRadialGradient(16, 16, 1, 16, 16, 14);
g.addColorStop(0, '#ffee88');
g.addColorStop(0.3, '#ff6622');
g.addColorStop(0.7, '#cc2200');
g.addColorStop(1, 'rgba(100,0,0,0)');
ctx.fillStyle = g;
ctx.beginPath(); ctx.arc(16, 16, 14, 0, Math.PI * 2); ctx.fill();
return cv;
}
/** Иконка оружия: слеза (голубая капля). */
function drawWeaponTears(): HTMLCanvasElement {
const { cv, ctx } = canvas(48, 48);
const g = ctx.createRadialGradient(24, 24, 2, 24, 24, 20);
g.addColorStop(0, '#dff0ff'); g.addColorStop(0.5, '#6699cc'); g.addColorStop(1, 'rgba(40,80,140,0)');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(24, 24, 20, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#aaccee'; ctx.beginPath(); ctx.arc(20, 18, 6, 0, Math.PI * 2); ctx.fill();
return cv;
}
/** Иконка оружия: кулак. */
function drawWeaponMelee(): HTMLCanvasElement {
const { cv, ctx } = canvas(48, 48);
ctx.fillStyle = '#8a6a3a';
roundRect(ctx, 10, 14, 28, 24, 6); ctx.fill();
ctx.strokeStyle = '#4a2a0a'; ctx.lineWidth = 2; roundRect(ctx, 10, 14, 28, 24, 6); ctx.stroke();
ctx.fillStyle = '#6a4a1a'; ctx.fillRect(14, 20, 8, 8); ctx.fillRect(26, 20, 8, 8);
ctx.fillRect(18, 30, 12, 6);
ctx.fillStyle = '#5a3a0a'; ctx.fillRect(20, 6, 8, 12);
return cv;
}
/** Иконка оружия: дробовик — три точки. */
function drawWeaponShotgun(): HTMLCanvasElement {
const { cv, ctx } = canvas(48, 48);
for (const [x, y] of [[24, 16], [16, 30], [32, 30]]) {
const g = ctx.createRadialGradient(x, y, 1, x, y, 10);
g.addColorStop(0, '#ffcc44'); g.addColorStop(0.5, '#cc6622'); g.addColorStop(1, 'rgba(150,60,0,0)');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(x, y, 10, 0, Math.PI * 2); ctx.fill();
}
return cv;
}
/** Иконка оружия: топор. */
function drawWeaponAxe(): HTMLCanvasElement {
const { cv, ctx } = canvas(48, 48);
ctx.fillStyle = '#777';
ctx.beginPath(); ctx.moveTo(8, 16); ctx.lineTo(38, 12); ctx.lineTo(40, 22); ctx.lineTo(30, 22); ctx.lineTo(30, 38); ctx.lineTo(16, 38); ctx.lineTo(16, 22); ctx.lineTo(6, 22); ctx.closePath(); ctx.fill();
ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = '#5a3a0a'; ctx.fillRect(22, 34, 4, 12);
return cv;
}
/** Иконка оружия: посох. */
function drawWeaponStaff(): HTMLCanvasElement {
const { cv, ctx } = canvas(48, 48);
ctx.fillStyle = '#6a4a2a'; ctx.fillRect(22, 6, 4, 36);
ctx.fillStyle = '#ff4422';
ctx.beginPath(); ctx.arc(24, 10, 10, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#ffcc44';
ctx.beginPath(); ctx.arc(24, 10, 5, 0, Math.PI * 2); ctx.fill();
return cv;
}
/** Иконка оружия: хлыст. */
function drawWeaponWhip(): HTMLCanvasElement {
const { cv, ctx } = canvas(48, 48);
ctx.strokeStyle = '#8a6a3a'; ctx.lineWidth = 4; ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(10, 38); ctx.quadraticCurveTo(18, 12, 38, 14); ctx.stroke();
ctx.strokeStyle = '#5a3a0a'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(10, 38); ctx.quadraticCurveTo(18, 12, 38, 14); ctx.stroke();
ctx.fillStyle = '#4a2a0a'; ctx.fillRect(6, 34, 8, 8);
return cv;
}
/** Иконка оружия: бомба. */
function drawWeaponBomb(): HTMLCanvasElement {
const { cv, ctx } = canvas(48, 48);
ctx.fillStyle = '#333'; ctx.beginPath(); ctx.arc(24, 24, 16, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#555'; ctx.beginPath(); ctx.arc(24, 24, 10, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#cc4422'; ctx.fillRect(22, 4, 4, 8);
ctx.fillStyle = '#ff8844'; ctx.beginPath(); ctx.arc(24, 4, 4, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = '#222'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(24, 24, 16, 0, Math.PI * 2); ctx.stroke();
return cv;
}
/** Иконка оружия: бумеранг. */
function drawWeaponBoomerang(): HTMLCanvasElement {
const { cv, ctx } = canvas(48, 48);
ctx.fillStyle = '#8a6a3a';
ctx.beginPath(); ctx.moveTo(8, 36); ctx.lineTo(22, 16); ctx.lineTo(40, 6); ctx.lineTo(38, 18); ctx.lineTo(22, 28); ctx.lineTo(18, 36); ctx.closePath(); ctx.fill();
ctx.strokeStyle = '#4a2a0a'; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = '#a08850'; ctx.fillRect(8, 30, 12, 8);
return cv;
}
/** Иконка оружия: лазер. */
function drawWeaponLaser(): HTMLCanvasElement {
const { cv, ctx } = canvas(48, 48);
const g = ctx.createLinearGradient(8, 24, 40, 24);
g.addColorStop(0, 'rgba(100,180,255,0.2)'); g.addColorStop(0.3, '#88ddff'); g.addColorStop(0.5, '#ffffff'); g.addColorStop(0.7, '#88ddff'); g.addColorStop(1, 'rgba(100,180,255,0.2)');
ctx.fillStyle = g; ctx.fillRect(8, 18, 32, 12);
ctx.fillStyle = 'rgba(255,255,255,0.8)'; ctx.fillRect(12, 22, 24, 4);
return cv;
}
/** Мягкая тень-«пятно» под сущностью. */
function drawShadow(): HTMLCanvasElement {
const { cv, ctx } = canvas(64, 32);
@@ -217,6 +397,13 @@ export class Assets {
case 'enemy-normal': return drawCharacter({ head: '#c08a5a', body: '#9a5a36', outline: '#3a2210', eye: '#2a1c0c' });
case 'enemy-fast': return drawCharacter({ head: '#bb3030', body: '#992222', outline: '#4a0e0e', eye: '#ffdddd', small: true });
case 'enemy-boss': return drawCharacter({ head: '#7a1414', body: '#5a0a0a', outline: '#250303', eye: '#ff4444', horns: true });
case 'enemy-charger': return drawCharger();
case 'enemy-tank': return drawTank();
case 'enemy-shooter': return drawShooter();
case 'chest': return drawChest();
case 'pickup': return drawPickup();
case 'fireball': return drawFireball();
case 'beam': return drawBeam();
}
});
}
@@ -229,6 +416,25 @@ export class Assets {
muzzle(): THREE.Texture { return this.get('muzzle', () => drawGlow('#fffbe0', 'rgba(255,200,60,0.7)'), false); }
spark(): THREE.Texture { return this.get('spark', () => drawGlow('#ffffff', 'rgba(255,230,170,0.6)'), false); }
puff(): THREE.Texture { return this.get('puff', () => drawGlow('rgba(220,220,230,0.9)', 'rgba(120,120,140,0.4)'), false); }
chest(): THREE.Texture { return this.get('chest', drawChest, false); }
pickup(): THREE.Texture { return this.get('pickup', drawPickup, false); }
fireball(): THREE.Texture { return this.get('fireball', drawFireball, false); }
weaponIcon(id: WeaponId): THREE.Texture {
return this.get(`weapon-icon-${id}`, () => {
switch (id) {
case 'tears': return drawWeaponTears();
case 'melee': return drawWeaponMelee();
case 'shotgun': return drawWeaponShotgun();
case 'axe': return drawWeaponAxe();
case 'staff': return drawWeaponStaff();
case 'whip': return drawWeaponWhip();
case 'bomb': return drawWeaponBomb();
case 'boomerang': return drawWeaponBoomerang();
case 'laser': return drawWeaponLaser();
}
});
}
dispose(): void {
for (const t of this.cache.values()) t.dispose();