feat(wolfenstein): add POV pistol viewmodel with procedural fallback
Introduce a first-person weapon viewmodel for the pistol that peeks up from behind the HUD, completing the classic FPS look. The viewmodel features a subtle, smoothed bob and sway animation that responds to player movement (forward/strafe), ramping in and out to avoid snapping. - Add `paintWeaponPistol` to `WolfensteinArt` as a small procedural placeholder (360x260) for when real art is absent. - Update `WolfensteinView` to handle both real and procedural weapon art with appropriate origins and base positions. - Implement `_drawWeapon` with bob/sway logic and smooth strength transitions. - Register the new sprite key in `wolfenstein-artwork.json`. - Document the new `wolfenstein-weapon-pistol` asset in `sprites.md`, including its size, depth, and behavior.
This commit is contained in:
parent
965e4308c9
commit
c86ccb9d1f
|
|
@ -11,6 +11,7 @@
|
|||
{ "key": "wolfenstein-item-health", "path": null },
|
||||
{ "key": "wolfenstein-bullet", "path": null },
|
||||
{ "key": "wolfenstein-muzzle", "path": null },
|
||||
{ "key": "wolfenstein-title", "path": null }
|
||||
{ "key": "wolfenstein-title", "path": null },
|
||||
{ "key": "wolfenstein-weapon-pistol", "path": "assets/images/wolfenstein/weapon_pistol.png" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export function ensureSprites(scene) {
|
|||
paintItem(scene, 'wolf-item-health', 0xe06c75);
|
||||
paintBullet(scene);
|
||||
paintMuzzleFlash(scene);
|
||||
paintWeaponPistol(scene);
|
||||
}
|
||||
|
||||
function paintGuard(scene) {
|
||||
|
|
@ -64,3 +65,20 @@ function paintMuzzleFlash(scene) {
|
|||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,18 @@ const DEATH_FALL_MS = 350;
|
|||
const DEATH_GROUND_MS = 2000;
|
||||
const DEATH_FADE_MS = 800;
|
||||
|
||||
// POV weapon viewmodel — sits above the 3D view canvas (depth 10) and the
|
||||
// sprite billboards, below the bottom HUD bar (depth 20, see
|
||||
// WolfensteinGame._buildHud) so it peeks up from behind the ammo bar like a
|
||||
// classic FPS viewmodel, and below the crosshair (depth 15).
|
||||
const WEAPON_DEPTH = 12;
|
||||
// Bob/sway share one phase accumulator that only advances while the player
|
||||
// is moving; WEAPON_BOB_SPEED is radians of that phase per ms.
|
||||
const WEAPON_BOB_SPEED = 0.012;
|
||||
const WEAPON_BOB_AMP_Y = 16; // px, vertical footstep bounce (|sin|, so it's a bounce not a swing)
|
||||
const WEAPON_SWAY_AMP_X = 10; // px, horizontal side-to-side sway
|
||||
const WEAPON_BOB_SMOOTH_MS = 220; // how fast bob strength ramps in/out on start/stop
|
||||
|
||||
export default class WolfensteinView {
|
||||
constructor(scene, rules) {
|
||||
this.scene = scene;
|
||||
|
|
@ -77,11 +89,31 @@ export default class WolfensteinView {
|
|||
? scene.textures.get('wolfenstein-doors') : null;
|
||||
this.guardTexture = scene.textures.exists('wolfenstein-guard-sheet')
|
||||
? scene.textures.get('wolfenstein-guard-sheet') : null;
|
||||
|
||||
// Real weapon_pistol.png is authored at GAME_WIDTH x GAME_HEIGHT
|
||||
// (1920x1080) with the gun already positioned bottom-center against a
|
||||
// transparent background, so it's just placed at the origin. The
|
||||
// procedural fallback is a small bottom-anchored placeholder instead
|
||||
// (see WolfensteinArt.paintWeaponPistol), so the two branches need
|
||||
// different origin/base-position setup.
|
||||
this.hasRealWeaponArt = scene.textures.exists('wolfenstein-weapon-pistol');
|
||||
this.weaponKey = this.hasRealWeaponArt ? 'wolfenstein-weapon-pistol' : 'wolf-weapon-pistol';
|
||||
if (this.hasRealWeaponArt) {
|
||||
this.weaponImage = scene.add.image(0, 0, this.weaponKey).setOrigin(0, 0).setDepth(WEAPON_DEPTH).setVisible(false);
|
||||
this._weaponBaseX = 0; this._weaponBaseY = 0;
|
||||
} else {
|
||||
this.weaponImage = scene.add.image(VIEW_W / 2, VIEW_H, this.weaponKey).setOrigin(0.5, 1).setDepth(WEAPON_DEPTH).setVisible(false);
|
||||
this._weaponBaseX = VIEW_W / 2; this._weaponBaseY = VIEW_H;
|
||||
}
|
||||
this._weaponBobPhase = 0;
|
||||
this._weaponBobStrength = 0;
|
||||
this._lastWeaponNow = null;
|
||||
}
|
||||
|
||||
render(state, camera) {
|
||||
this._drawWalls(state.map, state.doors, camera);
|
||||
this._drawSprites(state, camera);
|
||||
this._drawWeapon(state);
|
||||
}
|
||||
|
||||
_drawWalls(map, doors, camera) {
|
||||
|
|
@ -234,6 +266,37 @@ export default class WolfensteinView {
|
|||
for (const [key, img] of this.spritePool) if (!live.has(key)) img.setVisible(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* POV pistol viewmodel. Visible only while pistol is the equipped weapon
|
||||
* (`p.weapon`) — fists has no viewmodel art, so switching to it just hides
|
||||
* this image rather than swapping textures. Bob (vertical "footstep"
|
||||
* bounce) and sway (horizontal drift) are both driven off one phase
|
||||
* accumulator that only advances while the player has forward/strafe
|
||||
* input held; `_weaponBobStrength` is lerped toward 1 while moving and 0
|
||||
* while still, so starting/stopping fades the motion in/out instead of
|
||||
* snapping to it.
|
||||
*/
|
||||
_drawWeapon(state) {
|
||||
const p = state.player;
|
||||
const img = this.weaponImage;
|
||||
if (p.weapon !== 'pistol' || p.dead) { img.setVisible(false); return; }
|
||||
|
||||
const now = this.scene.time.now;
|
||||
const dt = this._lastWeaponNow != null ? Math.max(0, now - this._lastWeaponNow) : 0;
|
||||
this._lastWeaponNow = now;
|
||||
|
||||
const moving = p.moveForward !== 0 || p.moveStrafe !== 0;
|
||||
const smoothT = Math.min(1, dt / WEAPON_BOB_SMOOTH_MS);
|
||||
this._weaponBobStrength += ((moving ? 1 : 0) - this._weaponBobStrength) * smoothT;
|
||||
if (moving) this._weaponBobPhase += dt * WEAPON_BOB_SPEED;
|
||||
|
||||
const bobY = Math.abs(Math.sin(this._weaponBobPhase)) * WEAPON_BOB_AMP_Y * this._weaponBobStrength;
|
||||
const swayX = Math.sin(this._weaponBobPhase * 0.5) * WEAPON_SWAY_AMP_X * this._weaponBobStrength;
|
||||
|
||||
img.setPosition(this._weaponBaseX + swayX, this._weaponBaseY + bobY);
|
||||
img.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Falling -> on-the-ground -> fade-out sequence for a just-killed guard,
|
||||
* keyed by enemy id in `this.deathAnim` (first call after `e.dead` flips
|
||||
|
|
@ -325,6 +388,7 @@ export default class WolfensteinView {
|
|||
for (const img of this.spritePool.values()) img.destroy();
|
||||
this.spritePool.clear();
|
||||
this.image?.destroy();
|
||||
this.weaponImage?.destroy();
|
||||
if (this.scene.textures.exists('wolf-3dview')) this.scene.textures.remove('wolf-3dview');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,6 +182,31 @@ same aspect ratio is what matters most):
|
|||
`TAScreens.js`'s style before `ta-background` was painted). If added,
|
||||
size it to the shared canvas, **1920×1080**, and code would need to draw
|
||||
it behind that panel.
|
||||
- `wolfenstein-weapon-pistol` — **wired up as of 2026-08-21.** POV pistol
|
||||
viewmodel. Unlike the other entries here, this one is sized to the full
|
||||
shared canvas, **1920×1080 (`GAME_WIDTH`×`GAME_HEIGHT`), transparent
|
||||
background, gun art pre-positioned bottom-center** — `WolfensteinView`
|
||||
just places the whole image at the origin `(0, 0)` rather than treating it
|
||||
as a small floating icon, so the gun's position in the frame comes from
|
||||
where it's drawn in the PNG, not from any offset in code. Depth 12: above
|
||||
the 3D view canvas and sprite billboards (10), below the crosshair (15)
|
||||
and bottom HUD bar (20) — the lower part of the gun art tucks behind the
|
||||
ammo bar exactly like a classic FPS viewmodel. Shown only while
|
||||
`state.player.weapon === 'pistol'` (fists has no viewmodel art, so
|
||||
switching to fists just hides this image rather than swapping textures).
|
||||
`WolfensteinView._drawWeapon` adds a subtle bob (vertical bounce,
|
||||
`Math.abs(Math.sin(phase))`) and sway (horizontal drift, `Math.sin(phase *
|
||||
0.5)`) while the player has forward/strafe input held, both driven off one
|
||||
shared phase accumulator (`WEAPON_BOB_SPEED`) that freezes when the player
|
||||
stops moving; a `WEAPON_BOB_SMOOTH_MS` lerp ramps the motion's strength in
|
||||
and out instead of snapping, so starting/stopping a step doesn't jerk the
|
||||
gun. Falls back to `WolfensteinArt.paintWeaponPistol` (a small 360×260
|
||||
procedural placeholder, bottom-center anchored via `setOrigin(0.5, 1)`
|
||||
instead of the origin) if the sheet isn't loaded — that's the one entry in
|
||||
this file where the real-art and placeholder branches use genuinely
|
||||
different Phaser image setup (origin/base position), not just a different
|
||||
texture key, because the real PNG carries its own positioning and the
|
||||
placeholder can't.
|
||||
|
||||
## Sound effects
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue