feat(wolfenstein): add blue steel plate HUD bar with portrait and inventory
Replace the flat dark rectangle status bar with a baked riveted-metal "blue steel plate" background styled after Wolfenstein 3D's original HUD: - Add `paintHudBar` to WolfensteinArt.js generating a 1920x140 texture with vertical steel gradient, bevel edges, recessed readout panels, divider strips with rivets, and a centered portrait socket sized around the new `HUD_PORTRAIT_SIZE` constant - Add `paintProfile` fallback face and wire up `profile.png` (128x128, 3 health frames) in wolfenstein-artwork.json - Rebuild `_buildHud` in WolfensteinGame.js: left panel shows episode/mission label + HEALTH value, center shows health-reactive portrait (frame swaps at 66/33 HP thresholds), right panel shows INVENTORY key icons + AMMO count with equipped weapon icon - Derive weapon/key icon frames from `rules.items` data instead of hardcoded values so HUD stays in sync with wolfenstein-rules.json - Add `_hudEpisodeMissionText()` helper for static per-level label text (campaign episode/mission or test level name) - Apply subdued CRT overlay (`applyArcadeCRTOverlay`) tuned for a tactical-display feel, destroyed on teardown - Switch HUD fonts to shared `m6x11` pixel font with white/ice-blue lettering; remove old KEY_COLORS constant and rectangular key indicators
This commit is contained in:
parent
0940cd7255
commit
8b213ea887
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue