fertig-classic-games/src/games/wolfenstein/WolfensteinArt.js

351 lines
14 KiB
JavaScript

// Procedural placeholder art — the TAArt.js "stand-in keyed by kind" idiom.
// Everything here is generated Graphics -> texture, so the game renders with
// zero art files. Swap in painted sprites via data/wolfenstein-artwork.json
// (see assetManifest.js's wolfenstein entry) — nothing in WolfensteinView.js
// needs to change when that happens; it just starts finding real textures
// instead of falling back to these.
// Type 9 is deliberately absent — reserved for DOOR_WALL_TYPE
// (WolfensteinLogic.js), never a plain wall type, so every wall addition
// here skips straight from 8 to 10.
export const WALL_COLORS = {
1: 0x8a8a8a, // stone
2: 0x8a5a3a, // wood
3: 0x3a5a8a, // blue-tile
4: 0x5a8a3a, // green-tile
5: 0xa87c4a, // wood planks (plain vertical-plank timber, distinct from type 2's paneled wood)
6: 0x9cc2cf, // blue plaster (pale blue-gray plaster over a wood wainscot)
7: 0xb0aca4, // gray plaster (same plaster-over-wainscot as type 6, warm gray instead of blue)
8: 0xa8362a, // red brick
9: 0xb08040, // door (WolfensteinLogic.DOOR_WALL_TYPE)
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);
paintBullet(scene);
paintMuzzleFlash(scene);
paintObject(scene);
paintPickup(scene);
paintSparkle(scene);
paintWeaponPistol(scene);
paintWeaponShotgun(scene);
paintWeaponMachinegun(scene);
paintWeaponGatling(scene);
paintWeaponPlasmarifle(scene);
paintWeaponFists(scene);
paintHudBar(scene);
paintProfile(scene);
}
function paintGuard(scene) {
const S = 128;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0x2b3a2b, 1);
g.fillRect(S * 0.28, S * 0.15, S * 0.44, S * 0.7);
g.fillStyle(0xd8b48a, 1);
g.fillCircle(S * 0.5, S * 0.14, S * 0.13);
g.fillStyle(0x1a2a1a, 1);
g.fillRect(S * 0.18, S * 0.4, S * 0.14, S * 0.32);
g.generateTexture('wolf-guard', S, S);
g.destroy();
}
// One flat placeholder for every pickup frame (weapon/ammo/health alike),
// same "single generic stand-in, real art per-frame lands later" idiom as
// paintObject — a gold rounded square reads reasonably as "an item" for any
// of the 11 pickups.png frames until each is painted for real.
function paintPickup(scene) {
const S = 64;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0xd4a017, 1);
g.fillRoundedRect(S * 0.15, S * 0.15, S * 0.7, S * 0.7, 8);
g.lineStyle(3, 0x000000, 0.4);
g.strokeRoundedRect(S * 0.15, S * 0.15, S * 0.7, S * 0.7, 8);
g.generateTexture('wolf-pickup', S, S);
g.destroy();
}
// A tiny 4-point star used as the twinkling glint drawn around pickups (see
// WolfensteinView._drawPickupSparkles) — additive-blended, so only its
// bright core/spikes matter; no outline needed since it's never seen against
// a light background at full opacity the way wolf-pickup is.
function paintSparkle(scene) {
const S = 16, c = S / 2;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0xfff2a8, 1);
g.fillCircle(c, c, S * 0.14);
g.fillStyle(0xffffff, 0.85);
g.fillTriangle(c, 0, c - S * 0.1, c, c + S * 0.1, c);
g.fillTriangle(c, S, c - S * 0.1, c, c + S * 0.1, c);
g.fillTriangle(0, c, c, c - S * 0.1, c, c + S * 0.1);
g.fillTriangle(S, c, c, c - S * 0.1, c, c + S * 0.1);
g.generateTexture('wolf-sparkle', S, S);
g.destroy();
}
// A flat solid dot read as a blob at any size. A real round has a dark
// brass/copper body (so it reads against bright walls, not just dark ones),
// a hot tracer core smaller than the body (implies motion/heat, not just a
// colored ball), and a tiny off-center highlight for a touch of roundness —
// three concentric circles instead of one, still cheap to generate.
function paintBullet(scene) {
const S = 12;
const cx = S / 2, cy = S / 2;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0x8a5a2a, 1);
g.fillCircle(cx, cy, S * 0.42);
g.lineStyle(1, 0x3a2410, 0.8);
g.strokeCircle(cx, cy, S * 0.42);
g.fillStyle(0xfff2a8, 1);
g.fillCircle(cx, cy, S * 0.22);
g.fillStyle(0xffffff, 0.9);
g.fillCircle(cx - S * 0.08, cy - S * 0.08, S * 0.07);
g.generateTexture('wolf-bullet', S, S);
g.destroy();
}
function paintMuzzleFlash(scene) {
const S = 48;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0xfff2a8, 0.9);
g.fillCircle(S / 2, S / 2, S / 2);
g.generateTexture('wolf-muzzle', S, S);
g.destroy();
}
// Small bottom-anchored placeholder (not a full GAME_WIDTH x GAME_HEIGHT
// canvas like the real weapon_pistol.png — see WolfensteinView's
// hasRealWeaponArt branch, which anchors this one bottom-center instead of
// at the origin).
function paintWeaponPistol(scene) {
const W = 360, H = 260;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0x1a1a1a, 1);
g.fillRect(W * 0.38, H * 0.05, W * 0.14, H * 0.55);
g.fillStyle(0x3a3a3a, 1);
g.fillRect(W * 0.28, H * 0.5, W * 0.34, H * 0.16);
g.fillStyle(0x1a120a, 1);
g.fillRect(W * 0.34, H * 0.64, W * 0.16, H * 0.32);
g.generateTexture('wolf-weapon-pistol', W, H);
g.destroy();
}
// Same bottom-anchored placeholder convention as paintWeaponPistol, one
// simple distinct silhouette per new weapon so they're at least tellable
// apart before real art lands.
function paintWeaponShotgun(scene) {
const W = 360, H = 260;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0x5a3a1a, 1); // wood stock
g.fillRect(W * 0.3, H * 0.55, W * 0.4, H * 0.4);
g.fillStyle(0x2a2a2a, 1); // long double barrel
g.fillRect(W * 0.36, H * 0.02, W * 0.28, H * 0.58);
g.generateTexture('wolf-weapon-shotgun', W, H);
g.destroy();
}
function paintWeaponMachinegun(scene) {
const W = 360, H = 260;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0x2a2a2a, 1); // wide body
g.fillRect(W * 0.22, H * 0.15, W * 0.56, H * 0.4);
g.fillStyle(0x1a1a1a, 1); // barrel
g.fillRect(W * 0.4, H * 0.0, W * 0.16, H * 0.2);
g.fillStyle(0x0f0f0f, 1); // magazine
g.fillRect(W * 0.42, H * 0.55, W * 0.14, H * 0.42);
g.generateTexture('wolf-weapon-machinegun', W, H);
g.destroy();
}
function paintWeaponGatling(scene) {
const W = 360, H = 260;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0x3a3a3a, 1); // housing
g.fillRect(W * 0.28, H * 0.32, W * 0.44, H * 0.5);
g.fillStyle(0x1a1a1a, 1); // ring of barrels
const cx = W * 0.5, cy = H * 0.14, r = W * 0.16;
for (let i = 0; i < 6; i++) {
const a = (i / 6) * Math.PI * 2;
g.fillCircle(cx + Math.cos(a) * r * 0.5, cy + Math.sin(a) * r * 0.5, r * 0.16);
}
g.generateTexture('wolf-weapon-gatling', W, H);
g.destroy();
}
function paintWeaponPlasmarifle(scene) {
const W = 360, H = 260;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0x2a2a3a, 1); // body
g.fillRect(W * 0.3, H * 0.3, W * 0.4, H * 0.55);
g.fillStyle(0x3ad8ff, 0.85); // glowing plasma core
g.fillRoundedRect(W * 0.36, H * 0.05, W * 0.28, H * 0.3, 8);
g.generateTexture('wolf-weapon-plasmarifle', W, H);
g.destroy();
}
// Fists' one and only placeholder — unlike every projectile weapon above,
// melee has no separate "hit" texture here: WolfensteinView only swaps to
// the real weapon_fists_hit.png art on a swing (see _drawWeapon's
// FISTS_HIT_MS window), and falls back to this same idle silhouette the
// rest of the time if that real art isn't loaded, rather than inventing a
// synthetic punch pose nobody asked for.
function paintWeaponFists(scene) {
const W = 360, H = 260;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0xd8b48a, 1); // skin tone, matches paintGuard's face color
g.fillRoundedRect(W * 0.08, H * 0.15, W * 0.22, H * 0.5, 10);
g.fillRoundedRect(W * 0.7, H * 0.15, W * 0.22, H * 0.5, 10);
g.generateTexture('wolf-weapon-fists', W, H);
g.destroy();
}
// One flat placeholder for every object frame (same "single generic
// stand-in, real art per-frame lands later" idiom as wolf-guard) — a
// rounded block reads reasonably as "a solid obstacle" regardless of which
// actual prop (bush, statue, ...) a given frame turns out to be.
function paintObject(scene) {
const S = 64;
const g = scene.make.graphics({ x: 0, y: 0, add: false });
g.fillStyle(0x3a6b3a, 1);
g.fillRoundedRect(S * 0.12, S * 0.1, S * 0.76, S * 0.85, 6);
g.lineStyle(3, 0x1f3a1f, 0.8);
g.strokeRoundedRect(S * 0.12, S * 0.1, S * 0.76, S * 0.85, 6);
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();
}