feat: инвентарь 2 слота, 4 новых оружия, 3 врага, босс-фазы, иконки пикапов
This commit is contained in:
+59
-11
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user