diff --git a/assets/images/wolfenstein/profile.png b/assets/images/wolfenstein/profile.png new file mode 100644 index 0000000..715103a Binary files /dev/null and b/assets/images/wolfenstein/profile.png differ diff --git a/assets/images/wolfenstein/profile.psd b/assets/images/wolfenstein/profile.psd new file mode 100644 index 0000000..92a7c46 Binary files /dev/null and b/assets/images/wolfenstein/profile.psd differ diff --git a/data/wolfenstein-artwork.json b/data/wolfenstein-artwork.json index 20307b6..7254b7d 100644 --- a/data/wolfenstein-artwork.json +++ b/data/wolfenstein-artwork.json @@ -6,7 +6,8 @@ "doors": { "key": "wolfenstein-doors", "path": "assets/images/wolfenstein/doors.png", "frameWidth": 64, "frameHeight": 64 }, "wallArt": { "key": "wolfenstein-wall-art", "path": "assets/images/wolfenstein/wall-art.png", "frameWidth": 64, "frameHeight": 64 }, "objects": { "key": "wolfenstein-objects", "path": "assets/images/wolfenstein/objects.png", "frameWidth": 64, "frameHeight": 64 }, - "pickups": { "key": "wolfenstein-pickups", "path": "assets/images/wolfenstein/pickups.png", "frameWidth": 64, "frameHeight": 64 } + "pickups": { "key": "wolfenstein-pickups", "path": "assets/images/wolfenstein/pickups.png", "frameWidth": 64, "frameHeight": 64 }, + "profile": { "key": "wolfenstein-profile", "path": "assets/images/wolfenstein/profile.png", "frameWidth": 128, "frameHeight": 128 } }, "artwork": [ { "key": "wolfenstein-bullet", "path": null }, diff --git a/src/games/wolfenstein/WolfensteinArt.js b/src/games/wolfenstein/WolfensteinArt.js index 7254bde..8bea1dc 100644 --- a/src/games/wolfenstein/WolfensteinArt.js +++ b/src/games/wolfenstein/WolfensteinArt.js @@ -21,6 +21,24 @@ export const WALL_COLORS = { 10: 0xe8ddb8, // cream tile }; +// Displayed size (px) of the live health-portrait Image in the HUD bar — +// exported so WolfensteinGame's setDisplaySize can never drift out of sync +// with the recessed "socket" baked into paintHudBar's wolf-hud-bar texture +// below, which is sized around this same constant. +export const HUD_PORTRAIT_SIZE = 118; + +// "Blue steel plate" HUD bar palette (see paintHudBar) — kept local to this +// file rather than added to src/config.js's shared warm-gold COLORS, since +// it's a deliberately distinct look scoped to this one bar, the same "own +// literal, not worth the coupling" tradeoff KEY_COLORS already makes in +// WolfensteinGame.js/WolfensteinEditor.js. +const STEEL_LIGHT = 0x5d7fa3; // top of the plate's vertical gradient (overhead sheen) +const STEEL_DARK = 0x1c2c3d; // bottom of the plate's vertical gradient +const STEEL_MID = 0x3d5875; // divider-strip / bezel-ring body fill +const STEEL_HILITE = 0x9fc2e0; // crisp top bevel edge, rivet highlight flecks, ring outlines +const STEEL_SHADOW = 0x0b141d; // crisp bottom bevel edge, recess floors, rivet shade +const RIVET_BODY = 0x24384a; + export function ensureSprites(scene) { if (scene.textures.exists('wolf-guard')) return; paintGuard(scene); @@ -35,6 +53,8 @@ export function ensureSprites(scene) { paintWeaponGatling(scene); paintWeaponPlasmarifle(scene); paintWeaponFists(scene); + paintHudBar(scene); + paintProfile(scene); } function paintGuard(scene) { @@ -213,3 +233,118 @@ function paintObject(scene) { g.generateTexture('wolf-object', S, S); g.destroy(); } + +/** A dark base + body-colored disc + a small offset highlight fleck — reused at every rivet position across the bar (divider strips, portrait bezel corners, bookend corners). */ +function drawRivet(g, x, y, r = 4) { + g.fillStyle(STEEL_SHADOW, 0.9); + g.fillCircle(x, y, r); + g.fillStyle(RIVET_BODY, 1); + g.fillCircle(x, y, r * 0.75); + g.fillStyle(STEEL_HILITE, 0.8); + g.fillCircle(x - r * 0.25, y - r * 0.25, r * 0.25); +} + +/** + * The bottom HUD bar's background — final art in its own right (like + * paintSparkle/paintBullet), not a placeholder awaiting a real PNG. A + * "blue steel plate" styled after the original Wolfenstein 3D's status + * bar: a vertical steel gradient base, crisp top-highlight/bottom-shadow + * bevel edges (raised-plate look), two riveted divider strips splitting it + * into left-info / center-portrait / right-info sub-panels, a recessed + * "readout" backing behind each side's text rows, and a recessed bezel + * "socket" at center sized around HUD_PORTRAIT_SIZE for the live health + * portrait (WolfensteinGame._buildHud) to sit in. All measurements here are + * local canvas coordinates (0,0 top-left of this 1920x140 bake) — the + * caller places the resulting image at its natural size centered on the + * bar's actual screen position, so local (x,y) maps 1:1 to screen + * (x, y + 940). + */ +function paintHudBar(scene) { + const W = 1920, H = 140; + const g = scene.make.graphics({ x: 0, y: 0, add: false }); + + // Base plate: light at top (overhead sheen) to dark at bottom. + g.fillGradientStyle(STEEL_LIGHT, STEEL_LIGHT, STEEL_DARK, STEEL_DARK, 1); + g.fillRect(0, 0, W, H); + // Two faint diagonal specular sheens across the upper half — a cheap + // brushed-metal touch, not meant to read as a distinct shape on its own. + g.fillStyle(0xffffff, 0.05); + g.fillTriangle(0, 0, W * 0.4, 0, W * 0.15, H * 0.5); + g.fillTriangle(W * 0.55, 0, W, 0, W * 0.85, H * 0.4); + + // Crisp top-highlight / bottom-shadow bevel edges. + g.fillStyle(STEEL_HILITE, 0.9); + g.fillRect(0, 0, W, 3); + g.fillStyle(STEEL_SHADOW, 0.9); + g.fillRect(0, H - 3, W, 3); + + // Recessed "readout" backing behind each side's text rows — separates the + // text zones from the raised bevel/rivet zones, reads as a sunken + // instrument panel rather than text just floating on the bare plate. + const backingY = 10, backingH = H - 20; + g.fillStyle(STEEL_SHADOW, 0.35); + g.fillRoundedRect(56, backingY, 846 - 56, backingH, 8); + g.fillRoundedRect(1074, backingY, 1864 - 1074, backingH, 8); + g.lineStyle(1, STEEL_HILITE, 0.25); + g.strokeRoundedRect(56, backingY, 846 - 56, backingH, 8); + g.strokeRoundedRect(1074, backingY, 1864 - 1074, backingH, 8); + + // Divider strips separating left / portrait / right sub-panels, each a + // raised bar with its own light/dark side bevels and two rivets. + for (const dx of [856, 1044]) { + g.fillStyle(STEEL_MID, 1); + g.fillRect(dx, 0, 20, H); + g.fillStyle(STEEL_HILITE, 0.7); + g.fillRect(dx, 0, 2, H); + g.fillStyle(STEEL_SHADOW, 0.7); + g.fillRect(dx + 18, 0, 2, H); + drawRivet(g, dx + 10, 26); + drawRivet(g, dx + 10, H - 26); + } + + // Portrait bezel: a raised outer ring (4 corner rivets) around a + // recessed inner socket exactly HUD_PORTRAIT_SIZE square, centered. + const bw = 136, bh = 136, bx = W / 2 - bw / 2, by = 2; + g.fillStyle(STEEL_MID, 1); + g.fillRoundedRect(bx, by, bw, bh, 12); + g.lineStyle(2, STEEL_HILITE, 0.8); + g.strokeRoundedRect(bx, by, bw, bh, 12); + + const margin = (bw - HUD_PORTRAIT_SIZE) / 2; + const sx = bx + margin, sy = by + margin; + g.fillStyle(STEEL_SHADOW, 0.85); + g.fillRoundedRect(sx, sy, HUD_PORTRAIT_SIZE, HUD_PORTRAIT_SIZE, 6); + g.lineStyle(2, STEEL_SHADOW, 1); + g.strokeRoundedRect(sx, sy, HUD_PORTRAIT_SIZE, HUD_PORTRAIT_SIZE, 6); + + drawRivet(g, bx + 8, by + 12); + drawRivet(g, bx + bw - 8, by + 12); + drawRivet(g, bx + 8, by + bh - 12); + drawRivet(g, bx + bw - 8, by + bh - 12); + + // Bookend rivets near the far left/right edges, slightly larger — gives + // the whole plate a structural, riveted-steel-panel finish. + drawRivet(g, 34, 22, 5); drawRivet(g, 34, H - 22, 5); + drawRivet(g, W - 34, 22, 5); drawRivet(g, W - 34, H - 22, 5); + + g.generateTexture('wolf-hud-bar', W, H); + g.destroy(); +} + +// Single-frame fallback face if wolfenstein-profile isn't loaded — same +// "one generic placeholder regardless of variant" idiom as paintPickup/ +// paintObject, since a static fallback has no health value to react to +// anyway. WolfensteinGame._updateHud skips the per-health frame swap +// entirely when this is what's showing (see this._profileHasFrames). +function paintProfile(scene) { + const S = 128; + const g = scene.make.graphics({ x: 0, y: 0, add: false }); + g.fillStyle(0x1a2230, 1); + g.fillRect(0, 0, S, S); + g.fillStyle(0x5a4632, 1); // hair + g.fillRoundedRect(S * 0.2, S * 0.12, S * 0.6, S * 0.24, 8); + g.fillStyle(0xd8b48a, 1); // skin tone, matches paintGuard's face color + g.fillCircle(S * 0.5, S * 0.56, S * 0.3); + g.generateTexture('wolf-profile', S, S); + g.destroy(); +} diff --git a/src/games/wolfenstein/WolfensteinGame.js b/src/games/wolfenstein/WolfensteinGame.js index 06527bb..f92d5dc 100644 --- a/src/games/wolfenstein/WolfensteinGame.js +++ b/src/games/wolfenstein/WolfensteinGame.js @@ -15,14 +15,23 @@ import * as Logic from './WolfensteinLogic.js'; import WolfensteinView, { VIEW_H } from './WolfensteinView.js'; import * as Screens from './WolfensteinScreens.js'; import { makeCamera } from './WolfensteinRaycaster.js'; +import { ensureSprites, HUD_PORTRAIT_SIZE } from './WolfensteinArt.js'; +import { applyArcadeCRTOverlay } from '../../ui/ArcadeCRTOverlay.js'; -// Same blue/red/yellow palette as WolfensteinView's DOOR_COLOR_HEX and -// WolfensteinEditor's LOCK_COLORS — kept as its own literal here rather than -// imported, same "not worth the coupling for a 3-entry table" tradeoff those -// two make with each other. -const KEY_COLORS = { blue: 0x2a5adf, red: 0xd82a2a, yellow: 0xe0b820 }; const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1); +// The bottom HUD bar + its immediate gameplay-time neighbors (door/lock +// hints, toast) use the repo's shared retro-arcade pixel font, matching the +// "blue steel plate" bar's own military-tactical-display look — same +// fallback-chain convention every other m6x11-using game here uses (see +// e.g. TotalAnnihilation's TAHud.js `FONT` export). Menu/editor screens are +// untouched and keep "Julius Sans One". +const FONT = 'm6x11, "Julius Sans One"'; +// White/ice-blue lettering for the new bar specifically — deliberately NOT +// the shared warm-cream COLORS.textHex, per the "white lettering" ask. +const HUD_TEXT_VALUE = '#ffffff'; +const HUD_TEXT_LABEL = '#c9def0'; + const SAVE_KEY = 'wolfenstein-save'; const SAVE_SLOT_COUNT = 4; const saveSlotKey = (i) => `wolfenstein-save-slot-${i}`; @@ -55,6 +64,19 @@ export default class WolfensteinGame extends Phaser.Scene { this._bindPointerLock(); + // Whole-scene retro-arcade dressing (scanlines + curved-CRT vignette), + // matching sibling "Arcade, Console & PC" games — tuned toward a + // subdued tactical-display feel (shallower curve/vignette than the + // module's own defaults, steel-blue tint) rather than a bouncy arcade + // cabinet look, to fit a serious FPS. Destroyed in _teardown(). + this.crt = applyArcadeCRTOverlay(this, { + scanlineTint: 0x8fc7ff, + accentTint: 0x5d7fa3, + curveAmount: 0.22, + curveVignetteStrength: 0.3, + curveAberration: 0.0025, + }); + this.hud = this._buildHud(); this._setHudVisible(false); @@ -170,6 +192,7 @@ export default class WolfensteinGame extends Phaser.Scene { const model = Logic.buildLevelModel(levelJson); this.state = Logic.createState(model, this.rules, meta.carry ?? null); this.meta = meta; + this.hud.episodeMission.setText(this._hudEpisodeMissionText()); this.view?.destroy(); this.view = new WolfensteinView(this, this.rules); this.swapScreen(null); @@ -189,6 +212,7 @@ export default class WolfensteinGame extends Phaser.Scene { mode: 'campaign', campaignId: restored.levelMeta.campaignId, missionIndex: restored.levelMeta.missionIndex, carry: this._extractCarry(restored.player), }; + this.hud.episodeMission.setText(this._hudEpisodeMissionText()); this.view?.destroy(); this.view = new WolfensteinView(this, this.rules); this.swapScreen(null); @@ -350,26 +374,117 @@ export default class WolfensteinGame extends Phaser.Scene { // ------------------------------------------------------------- HUD + /** + * Static per-level text for the left panel's label row — computed once + * (see _beginLevel/_resumeState), not every frame, since neither the + * episode/mission numbering nor a Test Play level's name changes + * mid-level. Episode number comes from the campaign's own POSITION in + * this.campaigns.campaigns (index+1) rather than parsing meta.campaignId + * (e.g. "episode1") — more robust if campaign ids are ever renamed, and + * the same array showCampaignList/_onMissionWon already use. + */ + _hudEpisodeMissionText() { + if (this.meta?.mode === 'campaign') { + const idx = this.campaigns.campaigns.findIndex((c) => c.id === this.meta.campaignId); + return `EPISODE ${idx >= 0 ? idx + 1 : '?'} · MISSION ${this.meta.missionIndex + 1}`; + } + // Test Play (and any future non-campaign mode) has no campaign/mission numbering. + return this.state?.levelMeta?.name ? this.state.levelMeta.name.toUpperCase() : 'TEST LEVEL'; + } + + /** + * "Blue steel plate" status bar, styled after the original Wolfenstein + * 3D's — a baked riveted-metal background (WolfensteinArt.paintHudBar) + * with a health-reactive portrait centered, episode/mission + health on + * the left, and ammo + the equipped weapon's icon + held-key inventory + * on the right. The left/right text below sits over paintHudBar's own + * baked recessed "readout" panels at local-x 56-846 / 1074-1864 — which + * map 1:1 to these same screen-x values, since the bar image is centered + * at its natural 1920px width — so the label row (y=972) and value row + * (y=1042) positions below must stay in sync with that texture if either + * one changes. + */ _buildHud() { - const y = GAME_HEIGHT - 70; + // Needed here — not just inside WolfensteinView's own constructor, + // which doesn't run until a level actually begins — because this HUD + // is built once in create(), before any level/view exists, and now + // needs wolf-hud-bar/wolf-profile already baked. Idempotent (guarded + // by an existence check), so the later call from WolfensteinView is a + // cheap no-op. + ensureSprites(this); + + const y = GAME_HEIGHT - 70; // bar vertical center, screen y=1010 + const labelY = y - 38; // ~972 — episode/mission, INVENTORY: + key icons + const valueY = y + 32; // ~1042 — health, ammo + weapon icon const objs = {}; - objs.bar = this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT - 70, GAME_WIDTH, 140, 0x0a0806, 0.92).setDepth(20); - objs.health = this.add.text(80, y, '', { fontFamily: '"Julius Sans One"', fontSize: '34px', color: COLORS.textHex }).setOrigin(0, 0.5).setDepth(21); - objs.weapon = this.add.text(GAME_WIDTH / 2, y, '', { fontFamily: '"Julius Sans One"', fontSize: '34px', color: COLORS.textHex }).setOrigin(0.5).setDepth(21); - objs.ammo = this.add.text(GAME_WIDTH - 80, y, '', { fontFamily: '"Julius Sans One"', fontSize: '34px', color: COLORS.textHex }).setOrigin(1, 0.5).setDepth(21); - objs.toast = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT - 170, '', { fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.goldHex }).setOrigin(0.5).setDepth(21).setAlpha(0); + + objs.bar = this.add.image(GAME_WIDTH / 2, y, 'wolf-hud-bar').setDepth(20); + + // Left panel: episode/mission (see _hudEpisodeMissionText) above health. + objs.episodeMission = this.add.text(80, labelY, '', { + fontFamily: FONT, fontSize: '22px', color: HUD_TEXT_LABEL, letterSpacing: 2, + }).setOrigin(0, 0.5).setDepth(21); + objs.health = this.add.text(80, valueY, '', { + fontFamily: FONT, fontSize: '36px', fontStyle: 'bold', color: HUD_TEXT_VALUE, + }).setOrigin(0, 0.5).setDepth(21); + + // Center: health-reactive portrait, sitting in wolf-hud-bar's own baked + // recessed socket (sized around HUD_PORTRAIT_SIZE). _profileHasFrames + // gates the per-health frame swap in _updateHud — the single-frame + // fallback texture has nothing to swap to. + this._profileHasFrames = this.textures.exists('wolfenstein-profile'); + objs.portrait = this.add + .image(GAME_WIDTH / 2, y, this._profileHasFrames ? 'wolfenstein-profile' : 'wolf-profile') + .setDisplaySize(HUD_PORTRAIT_SIZE, HUD_PORTRAIT_SIZE).setDepth(21); + + // Right panel: INVENTORY: + up to 3 held-key icons above ammo + the + // equipped weapon's icon. Both icon rows reuse wolfenstein-pickups — + // already loaded for world pickups, same art the player already + // recognizes, no separate HUD-icon asset needed. Frame lookups are + // built from rules data (not re-hardcoded numbers) so the HUD can never + // disagree with data/wolfenstein-rules.json's own frame assignments — + // plain instance fields, NOT inside `objs`/`this.hud`, since + // _setHudVisible blindly calls .setVisible() on every this.hud value. + this._pickupsTextureOk = this.textures.exists('wolfenstein-pickups'); + this._weaponIconFrame = Object.fromEntries( + this.rules.items.filter((i) => i.kind === 'weapon').map((i) => [i.grantsWeapon, i.frame]), + ); + this._keyIconFrame = Object.fromEntries( + this.rules.items.filter((i) => i.kind === 'key').map((i) => [i.color, i.frame]), + ); + + objs.inventoryLabel = this.add.text(1725, labelY, 'INVENTORY:', { + fontFamily: FONT, fontSize: '22px', color: HUD_TEXT_LABEL, letterSpacing: 2, + }).setOrigin(1, 0.5).setDepth(21); + // Anchored near the right edge (not adjacent to the label's own + // right-aligned width) so the label can never grow into these — same + // "each independently anchored, never collide" idea the label/value + // rows on both sides already lean on. + const keyX = { blue: 1751, red: 1785, yellow: 1819 }; + for (const color of ['blue', 'red', 'yellow']) { + objs[`key_${color}`] = this.add + .image(keyX[color], labelY, 'wolfenstein-pickups', this._keyIconFrame[color]) + .setDisplaySize(30, 30).setDepth(21).setVisible(false); + } + + // Icon sits to the RIGHT of the ammo text's right-edge anchor, so a + // longer ammo count (up to "AMMO 200") can only grow further left, + // never toward — let alone under — the icon. Starts hidden (like the + // key icons above) rather than showing frame 0's pistol icon for one + // tick regardless of what's actually equipped — _updateHud's first + // pass sets both the correct frame and visibility together before this + // is ever actually seen. + objs.weaponIcon = this.add.image(1815, valueY, 'wolfenstein-pickups').setDisplaySize(48, 48).setDepth(21).setVisible(false); + objs.ammo = this.add.text(1790, valueY, '', { + fontFamily: FONT, fontSize: '36px', fontStyle: 'bold', color: HUD_TEXT_VALUE, + }).setOrigin(1, 0.5).setDepth(21); + + objs.toast = this.add.text(GAME_WIDTH / 2, GAME_HEIGHT - 170, '', { fontFamily: FONT, fontSize: '26px', color: COLORS.goldHex }).setOrigin(0.5).setDepth(21).setAlpha(0); objs.crosshair = this.add.text(GAME_WIDTH / 2, VIEW_H / 2, '+', { fontSize: '38px', color: COLORS.textHex }).setOrigin(0.5).setDepth(15); - objs.lockHint = this.add.text(GAME_WIDTH / 2, 60, 'Click to aim', { fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex }).setOrigin(0.5).setDepth(21); + objs.lockHint = this.add.text(GAME_WIDTH / 2, 60, 'Click to aim', { fontFamily: FONT, fontSize: '22px', color: COLORS.mutedHex }).setOrigin(0.5).setDepth(21); // Doors no longer open automatically on approach (see openNearestDoor) — // without this, there's no way to discover that Space/E is the interact key. - objs.doorHint = this.add.text(GAME_WIDTH / 2, VIEW_H / 2 + 46, '[SPACE/E] Open', { fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.goldHex }).setOrigin(0.5).setDepth(21); - // A small keyring above the health readout — one square per color, - // hidden until that key is actually picked up this level (see - // _updateHud). Flat individually-keyed objects, not a Container/group, - // because _setHudVisible below just iterates Object.values(this.hud). - objs.key_blue = this.add.rectangle(80, y - 42, 22, 22, KEY_COLORS.blue).setStrokeStyle(2, 0x000000).setDepth(21).setVisible(false); - objs.key_red = this.add.rectangle(112, y - 42, 22, 22, KEY_COLORS.red).setStrokeStyle(2, 0x000000).setDepth(21).setVisible(false); - objs.key_yellow = this.add.rectangle(144, y - 42, 22, 22, KEY_COLORS.yellow).setStrokeStyle(2, 0x000000).setDepth(21).setVisible(false); + objs.doorHint = this.add.text(GAME_WIDTH / 2, VIEW_H / 2 + 46, '[SPACE/E] Open', { fontFamily: FONT, fontSize: '22px', color: COLORS.goldHex }).setOrigin(0.5).setDepth(21); return objs; } @@ -385,15 +500,34 @@ export default class WolfensteinGame extends Phaser.Scene { this.hud.key_blue.setVisible(false); this.hud.key_red.setVisible(false); this.hud.key_yellow.setVisible(false); + // Same reasoning — would otherwise flash frame 0's pistol icon for a + // tick regardless of what's actually equipped (e.g. a fresh + // fists-only campaign start). + this.hud.weaponIcon.setVisible(false); } } _updateHud() { const p = this.state.player; - this.hud.health.setText(`HP ${Math.ceil(p.health)}`); - this.hud.weapon.setText(p.weapon.toUpperCase()); + this.hud.health.setText(`HEALTH ${Math.ceil(p.health)}`); + // >=66 calm, >=33 bloodied, else badly wounded — matches profile.png's + // 3 frames. Skipped when only the single-frame fallback is loaded (see + // _buildHud's _profileHasFrames), which has nothing to swap to. + if (this._profileHasFrames) { + this.hud.portrait.setFrame(p.health >= 66 ? 0 : p.health >= 33 ? 1 : 2); + } + const w = this.rules.weaponById[p.weapon]; this.hud.ammo.setText(w.kind === 'projectile' ? `AMMO ${p.ammo[w.ammoType] ?? 0}` : ''); + // Fists has no world-pickup icon (it was never something you find lying + // around) — _weaponIconFrame simply has no entry for it, so this hides + // the icon right alongside the ammo text blanking above, no separate + // fists special-case needed. + const iconFrame = this._weaponIconFrame[p.weapon]; + const showWeaponIcon = this._pickupsTextureOk && iconFrame != null; + this.hud.weaponIcon.setVisible(showWeaponIcon); + if (showWeaponIcon) this.hud.weaponIcon.setFrame(iconFrame); + this.hud.lockHint.setVisible(!this._locked); this.hud.key_blue.setVisible(p.keys.includes('blue')); this.hud.key_red.setVisible(p.keys.includes('red')); @@ -489,5 +623,6 @@ export default class WolfensteinGame extends Phaser.Scene { document.removeEventListener('mousemove', this._onMouseMove); this._exitLock(); this.view?.destroy(); + this.crt?.destroy(); } }