feat: Add Spear of Destiny demo support with dedicated asset registry and entity definitions

- Introduced SpearDemoAssetRegistry for managing assets specific to the Spear of Destiny demo.
- Created SpearDemoEntityModule to define enemy animations with adjusted sprite ranges.
- Implemented SpearDemoHudModule and SpearDemoMenuPicModule for HUD and menu assets.
- Added SpearDemoSfxModule for sound effect mappings specific to the demo version.
- Updated enemy classes (Guard, Mutant, Officer, SS) to support custom animation sets.
- Modified entity registry to accept a custom AssetRegistry for spawning entities.
- Enhanced rendering with CRT phosphor bloom effect in GLSL shaders.
- Adjusted ASCII and software renderer layouts for improved UI spacing.
- Added tests for SpearDemoAssetRegistry to ensure correct asset resolution and enemy spawning.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 10:37:50 +01:00
parent 528d6276b1
commit a84c677845
28 changed files with 641 additions and 70 deletions
@@ -8,6 +8,8 @@ uniform vec2 uTexel;
uniform float uEffectsEnabled;
// Engine time in seconds used to animate scanline travel.
uniform float uTime;
// 1.0 enables CRT phosphor bloom glow, 0.0 disables it.
uniform float uBloomEnabled;
// Source frame produced by the software renderer.
uniform sampler2D uTexture;
@@ -46,6 +48,7 @@ void main() {
texture(uTexture, bleedUv1).rgb * 0.28 +
texture(uTexture, bleedUv2).rgb * 0.14 +
texture(uTexture, bleedUv3).rgb * 0.06;
float edgeBleedLuma = luma(edgeBleedColor);
// Approximate concave bezel depth by measuring how far this fragment is
// from the emissive screen boundary in aspect-corrected UV space.
@@ -57,20 +60,24 @@ void main() {
vec2 clampedCentered = clampedUv * 2.0 - 1.0;
float cornerFactor = smoothstep(0.60, 1.15, length(clampedCentered));
float verticalShade = 0.88 + 0.07 * (1.0 - (FlutterFragCoord().y / uResolution.y));
float depthShade = 1.0 - smoothstep(0.0, 0.058, overflow) * 0.34;
float grain = sin(FlutterFragCoord().x * 0.21 + FlutterFragCoord().y * 0.11) * 0.006;
float moldedHighlight = smoothstep(0.072, 0.0, overflow) * 0.028;
float verticalShade = 0.88 + 0.07 * (1.0 - (FlutterFragCoord().y / uResolution.y));
float depthShade = 1.0 - smoothstep(0.0, 0.058, overflow) * 0.34;
float grain = sin(FlutterFragCoord().x * 0.21 + FlutterFragCoord().y * 0.11) * 0.006;
float moldedHighlight = smoothstep(0.072, 0.0, overflow) * 0.028;
// Deeper arcade-style profile: tighter, scene-tinted bleed rolloff.
float bezelGlow = exp(-bezelDistance * 82.0) * mix(1.0, 0.56, cornerFactor);
float innerLip = exp(-bezelDistance * 170.0) * 0.10;
float bleedStrength = smoothstep(0.12, 0.0, overflow) * (0.78 - cornerFactor * 0.26);
// Deeper arcade-style profile: tighter, scene-tinted bleed rolloff.
float bezelGlow = exp(-bezelDistance * 82.0) * mix(1.0, 0.56, cornerFactor);
float innerLip = exp(-bezelDistance * 170.0) * 0.10;
float bleedStrength = smoothstep(0.12, 0.0, overflow) * (0.78 - cornerFactor * 0.26);
float bloomBezelBoost = 1.0 +
uBloomEnabled * smoothstep(0.16, 0.82, edgeBleedLuma) * 0.75;
float bloomLipBoost = 1.0 +
uBloomEnabled * smoothstep(0.10, 0.68, edgeBleedLuma) * 0.45;
vec3 bezelColor =
vec3(0.225, 0.225, 0.215) * verticalShade * depthShade +
edgeBleedColor * bezelGlow * bleedStrength * 1.12 +
edgeBleedColor * innerLip * 0.36 +
edgeBleedColor * bezelGlow * bleedStrength * 1.12 * bloomBezelBoost +
edgeBleedColor * innerLip * 0.36 * bloomLipBoost +
vec3(moldedHighlight) +
vec3(grain);
fragColor = vec4(bezelColor, 1.0);
@@ -127,5 +134,42 @@ void main() {
outColor *= mix(0.62, 1.0, vignette);
}
if (uBloomEnabled > 0.5) {
// CRT phosphor bloom: bright areas spread a soft luminance glow.
// Sample a three-ring cross pattern directly from the source texture so
// the spread is measured in source-texel space and stays resolution-stable.
vec2 s1 = uTexel * 3.0;
vec2 s2 = uTexel * 7.0;
vec2 s3 = uTexel * 13.0;
vec3 glow = vec3(0.0);
// Inner ring — weight 1.0 each
glow += texture(uTexture, uv + vec2( s1.x, 0.0)).rgb;
glow += texture(uTexture, uv + vec2(-s1.x, 0.0)).rgb;
glow += texture(uTexture, uv + vec2( 0.0, s1.y)).rgb;
glow += texture(uTexture, uv + vec2( 0.0, -s1.y)).rgb;
// Mid ring — weight 0.5 each
glow += texture(uTexture, uv + vec2( s2.x, 0.0)).rgb * 0.5;
glow += texture(uTexture, uv + vec2(-s2.x, 0.0)).rgb * 0.5;
glow += texture(uTexture, uv + vec2( 0.0, s2.y)).rgb * 0.5;
glow += texture(uTexture, uv + vec2( 0.0, -s2.y)).rgb * 0.5;
// Outer ring — weight 0.25 each
glow += texture(uTexture, uv + vec2( s3.x, 0.0)).rgb * 0.25;
glow += texture(uTexture, uv + vec2(-s3.x, 0.0)).rgb * 0.25;
glow += texture(uTexture, uv + vec2( 0.0, s3.y)).rgb * 0.25;
glow += texture(uTexture, uv + vec2( 0.0, -s3.y)).rgb * 0.25;
// Normalize: 4*1.0 + 4*0.5 + 4*0.25 = 7.0
glow /= 7.0;
// Only bright pixels contribute — gate the bloom contribution on luma.
float glowLuma = luma(glow);
float bloomStrength = smoothstep(0.18, 0.82, glowLuma);
// Add bloom additively then apply a gentle Reinhard-style tone-map to
// prevent over-saturation while keeping dark areas clean.
outColor = outColor + glow * bloomStrength * 0.42;
outColor = outColor / (outColor + vec3(0.75)) * 1.75;
}
fragColor = vec4(outColor, centerSample.a);
}