feat(wolfenstein): add level-new map, improve HUD weapon icon contrast, and update e1m1 items

- Added new "level-new" level data (33x24 grid) with doors, enemies, items, and objects
- Removed one ammo-clip from e1m1 and added two health-large pickups
- Introduced shared HUD row anchor constants for consistent positioning
- Updated HUD to display level name alongside episode/mission info
- Added gray rounded-rect background behind weapon icon in HUD for better contrast against steel-blue bar
- Repositioned weapon icon to dynamically sit left of ammo text based on rendered width
- Updated WolfensteinArt to generate the new HUD icon background texture
This commit is contained in:
Brian Fertig 2026-08-22 21:08:45 -06:00
parent 8b213ea887
commit 9fa3896b5b
4 changed files with 935 additions and 81 deletions

View File

@ -1031,11 +1031,6 @@
"x": 17.5,
"y": 24.5
},
{
"type": "ammo-clip",
"x": 16.5,
"y": 24.5
},
{
"type": "ammo-clip",
"x": 15.5,
@ -1045,6 +1040,16 @@
"type": "pistol",
"x": 4.5,
"y": 4.5
},
{
"type": "health-large",
"x": 19.5,
"y": 2.5
},
{
"type": "health-large",
"x": 16.5,
"y": 24.5
}
],
"exit": {

File diff suppressed because it is too large Load Diff

View File

@ -55,6 +55,7 @@ export function ensureSprites(scene) {
paintWeaponFists(scene);
paintHudBar(scene);
paintProfile(scene);
paintHudIconBg(scene);
}
function paintGuard(scene) {
@ -348,3 +349,22 @@ function paintProfile(scene) {
g.generateTexture('wolf-profile', S, S);
g.destroy();
}
// A mid-tone gray rounded-rect backing dropped behind the HUD bar's weapon
// icon (WolfensteinGame._updateHud repositions the Image using this texture
// each frame, hugging the ammo text) — the pickups-sheet icons read poorly
// straight against the steel-blue plate (several are themselves blue-gray
// metal), so this gives them a neutral, higher-contrast plate of their own
// to sit on, with a subtle bevel matching the bar's own raised-plate style.
function paintHudIconBg(scene) {
const S = 60;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0x6b6f75, 1);
g.fillRoundedRect(0, 0, S, S, 10);
g.lineStyle(2, 0x9a9ea4, 0.8);
g.strokeRoundedRect(1, 1, S - 2, S - 2, 9);
g.lineStyle(2, 0x2c2e31, 0.8);
g.strokeRoundedRect(0, 0, S - 1, S - 1, 10);
g.generateTexture('wolf-hud-icon-bg', S, S);
g.destroy();
}

View File

@ -31,6 +31,12 @@ const FONT = 'm6x11, "Julius Sans One"';
// the shared warm-cream COLORS.textHex, per the "white lettering" ask.
const HUD_TEXT_VALUE = '#ffffff';
const HUD_TEXT_LABEL = '#c9def0';
// Shared row anchors — module-level (not local to _buildHud) so _updateHud
// can reposition the weapon icon relative to the same value-row y each
// frame without redriving the bar-center math a second time.
const HUD_BAR_Y = GAME_HEIGHT - 70; // bar vertical center, screen y=1010
const HUD_LABEL_Y = HUD_BAR_Y - 38; // ~972 — episode/mission, INVENTORY: + key icons
const HUD_VALUE_Y = HUD_BAR_Y + 32; // ~1042 — health, ammo + weapon icon
const SAVE_KEY = 'wolfenstein-save';
const SAVE_SLOT_COUNT = 4;
@ -386,7 +392,8 @@ export default class WolfensteinGame extends Phaser.Scene {
_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}`;
const name = this.state?.levelMeta?.name ? ` · ${this.state.levelMeta.name.toUpperCase()}` : '';
return `EPISODE ${idx >= 0 ? idx + 1 : '?'} · MISSION ${this.meta.missionIndex + 1}${name}`;
}
// 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';
@ -413,18 +420,15 @@ export default class WolfensteinGame extends Phaser.Scene {
// 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.image(GAME_WIDTH / 2, y, 'wolf-hud-bar').setDepth(20);
objs.bar = this.add.image(GAME_WIDTH / 2, HUD_BAR_Y, 'wolf-hud-bar').setDepth(20);
// Left panel: episode/mission (see _hudEpisodeMissionText) above health.
objs.episodeMission = this.add.text(80, labelY, '', {
// Left panel: episode/mission/name (see _hudEpisodeMissionText) above health.
objs.episodeMission = this.add.text(80, HUD_LABEL_Y, '', {
fontFamily: FONT, fontSize: '22px', color: HUD_TEXT_LABEL, letterSpacing: 2,
}).setOrigin(0, 0.5).setDepth(21);
objs.health = this.add.text(80, valueY, '', {
objs.health = this.add.text(80, HUD_VALUE_Y, '', {
fontFamily: FONT, fontSize: '36px', fontStyle: 'bold', color: HUD_TEXT_VALUE,
}).setOrigin(0, 0.5).setDepth(21);
@ -434,11 +438,11 @@ export default class WolfensteinGame extends Phaser.Scene {
// 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')
.image(GAME_WIDTH / 2, HUD_BAR_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 —
// Right panel: INVENTORY: + up to 3 held-key icons above the equipped
// weapon's icon + ammo. 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
@ -453,7 +457,7 @@ export default class WolfensteinGame extends Phaser.Scene {
this.rules.items.filter((i) => i.kind === 'key').map((i) => [i.color, i.frame]),
);
objs.inventoryLabel = this.add.text(1725, labelY, 'INVENTORY:', {
objs.inventoryLabel = this.add.text(1725, HUD_LABEL_Y, '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
@ -463,21 +467,28 @@ export default class WolfensteinGame extends Phaser.Scene {
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])
.image(keyX[color], HUD_LABEL_Y, '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, '', {
// Ammo stays right-anchored at a fixed edge (unchanged behavior, just a
// repositioned x); the weapon icon — on its own mid-gray rounded plate
// for contrast against the steel-blue bar — sits immediately to its
// LEFT. Because ammo text length varies ("" for melee up to "AMMO 200"),
// the icon+plate are repositioned every _updateHud tick relative to the
// ammo text's own actual rendered width, rather than a second fixed
// coordinate that a long count could grow into. Both start 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 the correct frame, position, and visibility together before
// either is ever actually seen. Background created first so it renders
// behind the icon at the same depth (Phaser draws same-depth objects in
// creation order).
objs.ammo = this.add.text(1840, HUD_VALUE_Y, '', {
fontFamily: FONT, fontSize: '36px', fontStyle: 'bold', color: HUD_TEXT_VALUE,
}).setOrigin(1, 0.5).setDepth(21);
objs.weaponIconBg = this.add.image(1720, HUD_VALUE_Y, 'wolf-hud-icon-bg').setDepth(21).setVisible(false);
objs.weaponIcon = this.add.image(1720, HUD_VALUE_Y, 'wolfenstein-pickups').setDisplaySize(44, 44).setDepth(21).setVisible(false);
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);
@ -500,10 +511,11 @@ 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).
// Same reasoning — would otherwise flash frame 0's pistol icon (and
// its background plate) for a tick regardless of what's actually
// equipped (e.g. a fresh fists-only campaign start).
this.hud.weaponIcon.setVisible(false);
this.hud.weaponIconBg.setVisible(false);
}
}
@ -526,7 +538,19 @@ export default class WolfensteinGame extends Phaser.Scene {
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.weaponIconBg.setVisible(showWeaponIcon);
if (showWeaponIcon) {
this.hud.weaponIcon.setFrame(iconFrame);
// Icon + its background plate sit immediately left of the ammo
// text's own rendered left edge — recomputed every tick since that
// edge moves with the ammo count's actual digit width (e.g. "AMMO 8"
// vs "AMMO 200"), rather than a second fixed coordinate a long count
// could grow into and collide with.
const ammoLeftEdge = this.hud.ammo.x - this.hud.ammo.width;
const iconX = ammoLeftEdge - 16 - this.hud.weaponIconBg.displayWidth / 2;
this.hud.weaponIconBg.setX(iconX);
this.hud.weaponIcon.setX(iconX);
}
this.hud.lockHint.setVisible(!this._locked);
this.hud.key_blue.setVisible(p.keys.includes('blue'));