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:
@@ -159,6 +159,7 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
supportedModes: supportedModes,
|
supportedModes: supportedModes,
|
||||||
supportsAsciiThemes: true,
|
supportsAsciiThemes: true,
|
||||||
supportsHardwareEffects: true,
|
supportsHardwareEffects: true,
|
||||||
|
supportsBloom: true,
|
||||||
supportsFpsCounter: true,
|
supportsFpsCounter: true,
|
||||||
),
|
),
|
||||||
rendererSettings: const WolfRendererSettings(
|
rendererSettings: const WolfRendererSettings(
|
||||||
@@ -298,6 +299,7 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
return WolfGlslRenderer(
|
return WolfGlslRenderer(
|
||||||
engine: _engine,
|
engine: _engine,
|
||||||
effectsEnabled: settings.hardwareEffectsEnabled,
|
effectsEnabled: settings.hardwareEffectsEnabled,
|
||||||
|
bloomEnabled: settings.bloomEnabled,
|
||||||
onKeyEvent: _handleRendererKeyEvent,
|
onKeyEvent: _handleRendererKeyEvent,
|
||||||
onUnavailable: _onGlslUnavailable,
|
onUnavailable: _onGlslUnavailable,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ enum WolfRendererMode {
|
|||||||
enum WolfRendererOptionId {
|
enum WolfRendererOptionId {
|
||||||
asciiTheme,
|
asciiTheme,
|
||||||
hardwareEffects,
|
hardwareEffects,
|
||||||
|
crtBloom,
|
||||||
fpsCounter,
|
fpsCounter,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,12 +28,14 @@ class WolfRendererCapabilities {
|
|||||||
required this.supportedModes,
|
required this.supportedModes,
|
||||||
this.supportsAsciiThemes = false,
|
this.supportsAsciiThemes = false,
|
||||||
this.supportsHardwareEffects = false,
|
this.supportsHardwareEffects = false,
|
||||||
|
this.supportsBloom = false,
|
||||||
this.supportsFpsCounter = false,
|
this.supportsFpsCounter = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Set<WolfRendererMode> supportedModes;
|
final Set<WolfRendererMode> supportedModes;
|
||||||
final bool supportsAsciiThemes;
|
final bool supportsAsciiThemes;
|
||||||
final bool supportsHardwareEffects;
|
final bool supportsHardwareEffects;
|
||||||
|
final bool supportsBloom;
|
||||||
final bool supportsFpsCounter;
|
final bool supportsFpsCounter;
|
||||||
|
|
||||||
bool supportsMode(WolfRendererMode mode) => supportedModes.contains(mode);
|
bool supportsMode(WolfRendererMode mode) => supportedModes.contains(mode);
|
||||||
@@ -44,6 +47,7 @@ class WolfRendererSettings {
|
|||||||
this.mode = WolfRendererMode.software,
|
this.mode = WolfRendererMode.software,
|
||||||
this.asciiThemeId = asciiThemeBlocks,
|
this.asciiThemeId = asciiThemeBlocks,
|
||||||
this.hardwareEffectsEnabled = false,
|
this.hardwareEffectsEnabled = false,
|
||||||
|
this.bloomEnabled = false,
|
||||||
this.fpsCounterEnabled = false,
|
this.fpsCounterEnabled = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,12 +62,14 @@ class WolfRendererSettings {
|
|||||||
final WolfRendererMode mode;
|
final WolfRendererMode mode;
|
||||||
final String asciiThemeId;
|
final String asciiThemeId;
|
||||||
final bool hardwareEffectsEnabled;
|
final bool hardwareEffectsEnabled;
|
||||||
|
final bool bloomEnabled;
|
||||||
final bool fpsCounterEnabled;
|
final bool fpsCounterEnabled;
|
||||||
|
|
||||||
WolfRendererSettings copyWith({
|
WolfRendererSettings copyWith({
|
||||||
WolfRendererMode? mode,
|
WolfRendererMode? mode,
|
||||||
String? asciiThemeId,
|
String? asciiThemeId,
|
||||||
bool? hardwareEffectsEnabled,
|
bool? hardwareEffectsEnabled,
|
||||||
|
bool? bloomEnabled,
|
||||||
bool? fpsCounterEnabled,
|
bool? fpsCounterEnabled,
|
||||||
}) {
|
}) {
|
||||||
return WolfRendererSettings(
|
return WolfRendererSettings(
|
||||||
@@ -71,6 +77,7 @@ class WolfRendererSettings {
|
|||||||
asciiThemeId: asciiThemeId ?? this.asciiThemeId,
|
asciiThemeId: asciiThemeId ?? this.asciiThemeId,
|
||||||
hardwareEffectsEnabled:
|
hardwareEffectsEnabled:
|
||||||
hardwareEffectsEnabled ?? this.hardwareEffectsEnabled,
|
hardwareEffectsEnabled ?? this.hardwareEffectsEnabled,
|
||||||
|
bloomEnabled: bloomEnabled ?? this.bloomEnabled,
|
||||||
fpsCounterEnabled: fpsCounterEnabled ?? this.fpsCounterEnabled,
|
fpsCounterEnabled: fpsCounterEnabled ?? this.fpsCounterEnabled,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -96,6 +103,7 @@ class WolfRendererSettings {
|
|||||||
'mode': mode.name,
|
'mode': mode.name,
|
||||||
'asciiThemeId': asciiThemeId,
|
'asciiThemeId': asciiThemeId,
|
||||||
'hardwareEffectsEnabled': hardwareEffectsEnabled,
|
'hardwareEffectsEnabled': hardwareEffectsEnabled,
|
||||||
|
'bloomEnabled': bloomEnabled,
|
||||||
'fpsCounterEnabled': fpsCounterEnabled,
|
'fpsCounterEnabled': fpsCounterEnabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -117,6 +125,7 @@ class WolfRendererSettings {
|
|||||||
mode: mode,
|
mode: mode,
|
||||||
asciiThemeId: asciiThemeId,
|
asciiThemeId: asciiThemeId,
|
||||||
hardwareEffectsEnabled: json['hardwareEffectsEnabled'] == true,
|
hardwareEffectsEnabled: json['hardwareEffectsEnabled'] == true,
|
||||||
|
bloomEnabled: json['bloomEnabled'] == true,
|
||||||
fpsCounterEnabled: json['fpsCounterEnabled'] == true,
|
fpsCounterEnabled: json['fpsCounterEnabled'] == true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,9 +284,24 @@ class WolfEngine {
|
|||||||
if (!rendererCapabilities.supportsHardwareEffects) {
|
if (!rendererCapabilities.supportsHardwareEffects) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final bool nextEnabled = !rendererSettings.hardwareEffectsEnabled;
|
||||||
updateRendererSettings(
|
updateRendererSettings(
|
||||||
rendererSettings.copyWith(
|
rendererSettings.copyWith(
|
||||||
hardwareEffectsEnabled: !rendererSettings.hardwareEffectsEnabled,
|
hardwareEffectsEnabled: nextEnabled,
|
||||||
|
bloomEnabled: nextEnabled ? rendererSettings.bloomEnabled : false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggles CRT bloom glow for hardware renderer hosts.
|
||||||
|
void toggleBloom() {
|
||||||
|
if (!rendererCapabilities.supportsBloom ||
|
||||||
|
!rendererSettings.hardwareEffectsEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateRendererSettings(
|
||||||
|
rendererSettings.copyWith(
|
||||||
|
bloomEnabled: !rendererSettings.bloomEnabled,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -328,6 +343,15 @@ class WolfEngine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!rendererSettings.hardwareEffectsEnabled &&
|
||||||
|
rendererSettings.bloomEnabled) {
|
||||||
|
rendererSettings = rendererSettings.copyWith(bloomEnabled: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rendererCapabilities.supportsBloom && rendererSettings.bloomEnabled) {
|
||||||
|
rendererSettings = rendererSettings.copyWith(bloomEnabled: false);
|
||||||
|
}
|
||||||
|
|
||||||
if (!rendererCapabilities.supportsFpsCounter &&
|
if (!rendererCapabilities.supportsFpsCounter &&
|
||||||
rendererSettings.fpsCounterEnabled) {
|
rendererSettings.fpsCounterEnabled) {
|
||||||
rendererSettings = rendererSettings.copyWith(fpsCounterEnabled: false);
|
rendererSettings = rendererSettings.copyWith(fpsCounterEnabled: false);
|
||||||
@@ -364,6 +388,18 @@ class WolfEngine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeMode == WolfRendererMode.hardware &&
|
||||||
|
rendererCapabilities.supportsBloom) {
|
||||||
|
options.add(
|
||||||
|
WolfMenuRendererOptionEntry(
|
||||||
|
id: WolfRendererOptionId.crtBloom,
|
||||||
|
label: 'CRT BLOOM',
|
||||||
|
isEnabled: rendererSettings.hardwareEffectsEnabled,
|
||||||
|
isChecked: rendererSettings.bloomEnabled,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (rendererCapabilities.supportsFpsCounter) {
|
if (rendererCapabilities.supportsFpsCounter) {
|
||||||
options.add(
|
options.add(
|
||||||
WolfMenuRendererOptionEntry(
|
WolfMenuRendererOptionEntry(
|
||||||
@@ -628,6 +664,9 @@ class WolfEngine {
|
|||||||
case WolfRendererOptionId.hardwareEffects:
|
case WolfRendererOptionId.hardwareEffects:
|
||||||
toggleHardwareEffects();
|
toggleHardwareEffects();
|
||||||
break;
|
break;
|
||||||
|
case WolfRendererOptionId.crtBloom:
|
||||||
|
toggleBloom();
|
||||||
|
break;
|
||||||
case WolfRendererOptionId.fpsCounter:
|
case WolfRendererOptionId.fpsCounter:
|
||||||
toggleFpsCounter();
|
toggleFpsCounter();
|
||||||
break;
|
break;
|
||||||
@@ -651,6 +690,9 @@ class WolfEngine {
|
|||||||
case WolfRendererOptionId.hardwareEffects:
|
case WolfRendererOptionId.hardwareEffects:
|
||||||
toggleHardwareEffects();
|
toggleHardwareEffects();
|
||||||
break;
|
break;
|
||||||
|
case WolfRendererOptionId.crtBloom:
|
||||||
|
toggleBloom();
|
||||||
|
break;
|
||||||
case WolfRendererOptionId.fpsCounter:
|
case WolfRendererOptionId.fpsCounter:
|
||||||
toggleFpsCounter();
|
toggleFpsCounter();
|
||||||
break;
|
break;
|
||||||
@@ -758,6 +800,7 @@ class WolfEngine {
|
|||||||
difficulty!,
|
difficulty!,
|
||||||
data.sprites.length,
|
data.sprites.length,
|
||||||
isSharewareMode: data.version == GameVersion.shareware,
|
isSharewareMode: data.version == GameVersion.shareware,
|
||||||
|
registry: data.registry,
|
||||||
);
|
);
|
||||||
if (newEntity != null) entities.add(newEntity);
|
if (newEntity != null) entities.add(newEntity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ abstract class Collectible extends Entity {
|
|||||||
double y,
|
double y,
|
||||||
Difficulty difficulty, {
|
Difficulty difficulty, {
|
||||||
bool isSharewareMode = false,
|
bool isSharewareMode = false,
|
||||||
|
AssetRegistry? registry,
|
||||||
}) {
|
}) {
|
||||||
return switch (objId) {
|
return switch (objId) {
|
||||||
MapObject.goldKey || MapObject.silverKey => KeyCollectible(
|
MapObject.goldKey || MapObject.silverKey => KeyCollectible(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class DeadAardwolf extends Decorative {
|
|||||||
double y,
|
double y,
|
||||||
Difficulty difficulty, {
|
Difficulty difficulty, {
|
||||||
bool isSharewareMode = false,
|
bool isSharewareMode = false,
|
||||||
|
AssetRegistry? registry,
|
||||||
}) {
|
}) {
|
||||||
if (objId == 125) {
|
if (objId == 125) {
|
||||||
return DeadAardwolf(x: x, y: y);
|
return DeadAardwolf(x: x, y: y);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class DeadGuard extends Decorative {
|
|||||||
double y,
|
double y,
|
||||||
Difficulty difficulty, {
|
Difficulty difficulty, {
|
||||||
bool isSharewareMode = false,
|
bool isSharewareMode = false,
|
||||||
|
AssetRegistry? registry,
|
||||||
}) {
|
}) {
|
||||||
if (objId == 124) {
|
if (objId == 124) {
|
||||||
return DeadGuard(x: x, y: y);
|
return DeadGuard(x: x, y: y);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
|
||||||
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
import 'package:wolf_3d_dart/src/entities/entity.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
|
||||||
class Decorative extends Entity {
|
class Decorative extends Entity {
|
||||||
Decorative({
|
Decorative({
|
||||||
@@ -41,6 +41,7 @@ class Decorative extends Entity {
|
|||||||
double y,
|
double y,
|
||||||
Difficulty difficulty, {
|
Difficulty difficulty, {
|
||||||
bool isSharewareMode = false,
|
bool isSharewareMode = false,
|
||||||
|
AssetRegistry? registry,
|
||||||
}) {
|
}) {
|
||||||
// 2. Standard props (Table, Lamp, etc) use the tiered check
|
// 2. Standard props (Table, Lamp, etc) use the tiered check
|
||||||
if (!MapObject.isDifficultyAllowed(objId, difficulty)) return null;
|
if (!MapObject.isDifficultyAllowed(objId, difficulty)) return null;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class HansGrosse extends Enemy {
|
|||||||
double y,
|
double y,
|
||||||
Difficulty difficulty, {
|
Difficulty difficulty, {
|
||||||
bool isSharewareMode = false,
|
bool isSharewareMode = false,
|
||||||
|
AssetRegistry? registry,
|
||||||
}) {
|
}) {
|
||||||
if (objId == MapObject.bossHansGrosse) {
|
if (objId == MapObject.bossHansGrosse) {
|
||||||
return HansGrosse(
|
return HansGrosse(
|
||||||
|
|||||||
@@ -25,14 +25,26 @@ class Dog extends Enemy {
|
|||||||
required super.y,
|
required super.y,
|
||||||
required super.angle,
|
required super.angle,
|
||||||
required super.mapId,
|
required super.mapId,
|
||||||
|
super.animationSet,
|
||||||
Difficulty difficulty = Difficulty.medium,
|
Difficulty difficulty = Difficulty.medium,
|
||||||
}) : super(
|
}) : super(
|
||||||
spriteIndex: EnemyType.dog.animations.walking.start,
|
spriteIndex: (animationSet ?? EnemyType.dog.animations).walking.start,
|
||||||
state: EntityState.patrolling,
|
state: EntityState.patrolling,
|
||||||
) {
|
) {
|
||||||
health = type.hitPointsFor(difficulty);
|
health = type.hitPointsFor(difficulty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
EnemyAnimation animationForState(EntityState state) {
|
||||||
|
return switch (state) {
|
||||||
|
EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking,
|
||||||
|
EntityState.attacking => EnemyAnimation.attacking,
|
||||||
|
EntityState.pain => EnemyAnimation.pain,
|
||||||
|
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
||||||
|
_ => EnemyAnimation.idle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
({Coordinate2D movement, double newAngle}) update({
|
({Coordinate2D movement, double newAngle}) update({
|
||||||
required int elapsedMs,
|
required int elapsedMs,
|
||||||
@@ -314,15 +326,9 @@ class Dog extends Enemy {
|
|||||||
diff -= 2 * math.pi;
|
diff -= 2 * math.pi;
|
||||||
}
|
}
|
||||||
|
|
||||||
EnemyAnimation currentAnim = switch (state) {
|
final currentAnim = animationForState(state);
|
||||||
EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking,
|
|
||||||
EntityState.attacking => EnemyAnimation.attacking,
|
|
||||||
EntityState.pain => EnemyAnimation.pain,
|
|
||||||
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
|
||||||
_ => EnemyAnimation.idle,
|
|
||||||
};
|
|
||||||
|
|
||||||
spriteIndex = type.getSpriteFromAnimation(
|
spriteIndex = spriteForAnimation(
|
||||||
animation: currentAnim,
|
animation: currentAnim,
|
||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
lastActionTime: lastActionTime,
|
lastActionTime: lastActionTime,
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ abstract class Enemy extends Entity {
|
|||||||
required super.x,
|
required super.x,
|
||||||
required super.y,
|
required super.y,
|
||||||
required super.spriteIndex,
|
required super.spriteIndex,
|
||||||
|
EnemyAnimationMap? animationSet,
|
||||||
super.angle,
|
super.angle,
|
||||||
super.state,
|
super.state,
|
||||||
super.mapId,
|
super.mapId,
|
||||||
super.lastActionTime,
|
super.lastActionTime,
|
||||||
});
|
}) : _animationSetOverride = animationSet;
|
||||||
|
|
||||||
|
EnemyAnimationMap? _animationSetOverride;
|
||||||
|
|
||||||
/// The current "Tic" count remaining for the active animation frame.
|
/// The current "Tic" count remaining for the active animation frame.
|
||||||
int _ticCount = 0;
|
int _ticCount = 0;
|
||||||
@@ -66,6 +69,66 @@ abstract class Enemy extends Entity {
|
|||||||
/// The sound played once when this enemy starts dying.
|
/// The sound played once when this enemy starts dying.
|
||||||
SoundEffect get deathSound => type.deathSound;
|
SoundEffect get deathSound => type.deathSound;
|
||||||
|
|
||||||
|
EnemyAnimationMap get animationSet =>
|
||||||
|
_animationSetOverride ?? type.animations;
|
||||||
|
|
||||||
|
EnemyAnimation animationForState(EntityState state) {
|
||||||
|
return switch (state) {
|
||||||
|
EntityState.patrolling => EnemyAnimation.walking,
|
||||||
|
EntityState.ambushing => EnemyAnimation.idle,
|
||||||
|
EntityState.attacking => EnemyAnimation.attacking,
|
||||||
|
EntityState.pain => EnemyAnimation.pain,
|
||||||
|
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
||||||
|
_ => EnemyAnimation.idle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAnimationSet(
|
||||||
|
EnemyAnimationMap set, {
|
||||||
|
bool alignSpriteToState = false,
|
||||||
|
}) {
|
||||||
|
_animationSetOverride = set;
|
||||||
|
if (alignSpriteToState) {
|
||||||
|
final anim = animationForState(state);
|
||||||
|
spriteIndex = animationSet.getRange(anim).start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int spriteForAnimation({
|
||||||
|
required EnemyAnimation animation,
|
||||||
|
required int elapsedMs,
|
||||||
|
required int lastActionTime,
|
||||||
|
double angleDiff = 0,
|
||||||
|
int? walkFrameOverride,
|
||||||
|
}) {
|
||||||
|
final range = animationSet.getRange(animation);
|
||||||
|
|
||||||
|
int octant = ((angleDiff + (math.pi / 8)) / (math.pi / 4)).floor() % 8;
|
||||||
|
if (octant < 0) octant += 8;
|
||||||
|
|
||||||
|
return switch (animation) {
|
||||||
|
EnemyAnimation.idle => range.start + octant,
|
||||||
|
EnemyAnimation.walking => () {
|
||||||
|
int framesPerAngle = range.length ~/ 8;
|
||||||
|
if (framesPerAngle < 1) framesPerAngle = 1;
|
||||||
|
int frame = walkFrameOverride ?? (elapsedMs ~/ 150) % framesPerAngle;
|
||||||
|
return range.start + (frame * 8) + octant;
|
||||||
|
}(),
|
||||||
|
EnemyAnimation.attacking => () {
|
||||||
|
int time = elapsedMs - lastActionTime;
|
||||||
|
int mappedFrame = (time ~/ 150).clamp(0, range.length - 1);
|
||||||
|
return range.start + mappedFrame;
|
||||||
|
}(),
|
||||||
|
EnemyAnimation.pain => range.start,
|
||||||
|
EnemyAnimation.dying => () {
|
||||||
|
int time = elapsedMs - lastActionTime;
|
||||||
|
int mappedFrame = (time ~/ 150).clamp(0, range.length - 1);
|
||||||
|
return range.start + mappedFrame;
|
||||||
|
}(),
|
||||||
|
EnemyAnimation.dead => range.start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensures enemies drop only one item (like ammo or a key) upon death.
|
/// Ensures enemies drop only one item (like ammo or a key) upon death.
|
||||||
bool hasDroppedItem = false;
|
bool hasDroppedItem = false;
|
||||||
|
|
||||||
@@ -555,6 +618,7 @@ abstract class Enemy extends Entity {
|
|||||||
double y,
|
double y,
|
||||||
Difficulty difficulty, {
|
Difficulty difficulty, {
|
||||||
bool isSharewareMode = false,
|
bool isSharewareMode = false,
|
||||||
|
AssetRegistry? registry,
|
||||||
}) {
|
}) {
|
||||||
// Filter out decorative bodies or player starts
|
// Filter out decorative bodies or player starts
|
||||||
if (objId == MapObject.deadGuard || objId == MapObject.deadAardwolf) {
|
if (objId == MapObject.deadGuard || objId == MapObject.deadAardwolf) {
|
||||||
@@ -572,20 +636,33 @@ abstract class Enemy extends Entity {
|
|||||||
|
|
||||||
EnemyType? matchedType;
|
EnemyType? matchedType;
|
||||||
int? normalizedId;
|
int? normalizedId;
|
||||||
|
EnemyAnimationMap? matchedAnimationSet;
|
||||||
|
|
||||||
// Find which enemy type claims this ID for the current difficulty
|
// Find which enemy type claims this ID for the current difficulty
|
||||||
for (final type in EnemyType.values) {
|
for (final type in EnemyType.values) {
|
||||||
if (isSharewareMode && !type.existsInShareware) continue;
|
final animationSet = _resolveAnimationSet(type, registry);
|
||||||
|
if (animationSet == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registry == null && isSharewareMode && !type.existsInShareware) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
normalizedId = type.mapData.getNormalizedId(objId, difficulty);
|
normalizedId = type.mapData.getNormalizedId(objId, difficulty);
|
||||||
if (normalizedId != null) {
|
if (normalizedId != null) {
|
||||||
matchedType = type;
|
matchedType = type;
|
||||||
|
matchedAnimationSet = animationSet;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no type claimed it, or the difficulty was too low, abort spawn
|
// If no type claimed it, or the difficulty was too low, abort spawn
|
||||||
if (matchedType == null || normalizedId == null) return null;
|
if (matchedType == null ||
|
||||||
|
normalizedId == null ||
|
||||||
|
matchedAnimationSet == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve spawn orientation using the NORMALIZED ID (0-7 offset from base)
|
// Resolve spawn orientation using the NORMALIZED ID (0-7 offset from base)
|
||||||
// This prevents offset math bugs (like the Mutant's 18-ID shift) from breaking facing directions.
|
// This prevents offset math bugs (like the Mutant's 18-ID shift) from breaking facing directions.
|
||||||
@@ -606,13 +683,14 @@ abstract class Enemy extends Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return the specific instance
|
// Return the specific instance
|
||||||
return switch (matchedType) {
|
final spawned = switch (matchedType) {
|
||||||
EnemyType.guard => Guard(
|
EnemyType.guard => Guard(
|
||||||
x: x,
|
x: x,
|
||||||
y: y,
|
y: y,
|
||||||
angle: spawnAngle,
|
angle: spawnAngle,
|
||||||
mapId: objId,
|
mapId: objId,
|
||||||
difficulty: difficulty,
|
difficulty: difficulty,
|
||||||
|
animationSet: matchedAnimationSet,
|
||||||
),
|
),
|
||||||
EnemyType.dog => Dog(
|
EnemyType.dog => Dog(
|
||||||
x: x,
|
x: x,
|
||||||
@@ -620,6 +698,7 @@ abstract class Enemy extends Entity {
|
|||||||
angle: spawnAngle,
|
angle: spawnAngle,
|
||||||
mapId: objId,
|
mapId: objId,
|
||||||
difficulty: difficulty,
|
difficulty: difficulty,
|
||||||
|
animationSet: matchedAnimationSet,
|
||||||
),
|
),
|
||||||
EnemyType.ss => SS(
|
EnemyType.ss => SS(
|
||||||
x: x,
|
x: x,
|
||||||
@@ -627,6 +706,7 @@ abstract class Enemy extends Entity {
|
|||||||
angle: spawnAngle,
|
angle: spawnAngle,
|
||||||
mapId: objId,
|
mapId: objId,
|
||||||
difficulty: difficulty,
|
difficulty: difficulty,
|
||||||
|
animationSet: matchedAnimationSet,
|
||||||
),
|
),
|
||||||
EnemyType.mutant => Mutant(
|
EnemyType.mutant => Mutant(
|
||||||
x: x,
|
x: x,
|
||||||
@@ -634,6 +714,7 @@ abstract class Enemy extends Entity {
|
|||||||
angle: spawnAngle,
|
angle: spawnAngle,
|
||||||
mapId: objId,
|
mapId: objId,
|
||||||
difficulty: difficulty,
|
difficulty: difficulty,
|
||||||
|
animationSet: matchedAnimationSet,
|
||||||
),
|
),
|
||||||
EnemyType.officer => Officer(
|
EnemyType.officer => Officer(
|
||||||
x: x,
|
x: x,
|
||||||
@@ -641,7 +722,33 @@ abstract class Enemy extends Entity {
|
|||||||
angle: spawnAngle,
|
angle: spawnAngle,
|
||||||
mapId: objId,
|
mapId: objId,
|
||||||
difficulty: difficulty,
|
difficulty: difficulty,
|
||||||
|
animationSet: matchedAnimationSet,
|
||||||
),
|
),
|
||||||
}..state = spawnState;
|
};
|
||||||
|
|
||||||
|
spawned
|
||||||
|
..state = spawnState
|
||||||
|
..setAnimationSet(matchedAnimationSet, alignSpriteToState: true);
|
||||||
|
|
||||||
|
return spawned;
|
||||||
|
}
|
||||||
|
|
||||||
|
static EnemyAnimationMap? _resolveAnimationSet(
|
||||||
|
EnemyType type,
|
||||||
|
AssetRegistry? registry,
|
||||||
|
) {
|
||||||
|
if (registry == null) {
|
||||||
|
return type.animations;
|
||||||
|
}
|
||||||
|
|
||||||
|
final key = switch (type) {
|
||||||
|
EnemyType.guard => EntityKey.guard,
|
||||||
|
EnemyType.dog => EntityKey.dog,
|
||||||
|
EnemyType.ss => EntityKey.ss,
|
||||||
|
EnemyType.mutant => EntityKey.mutant,
|
||||||
|
EnemyType.officer => EntityKey.officer,
|
||||||
|
};
|
||||||
|
|
||||||
|
return registry.entities.resolve(key)?.animations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ class Guard extends Enemy {
|
|||||||
required super.y,
|
required super.y,
|
||||||
required super.angle,
|
required super.angle,
|
||||||
required super.mapId,
|
required super.mapId,
|
||||||
|
super.animationSet,
|
||||||
Difficulty difficulty = Difficulty.medium,
|
Difficulty difficulty = Difficulty.medium,
|
||||||
}) : super(
|
}) : super(
|
||||||
spriteIndex: EnemyType.guard.animations.idle.start,
|
spriteIndex: (animationSet ?? EnemyType.guard.animations).idle.start,
|
||||||
state: EntityState.idle,
|
state: EntityState.idle,
|
||||||
) {
|
) {
|
||||||
health = type.hitPointsFor(difficulty);
|
health = type.hitPointsFor(difficulty);
|
||||||
@@ -150,14 +151,15 @@ class Guard extends Enemy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EnemyAnimation currentAnim = switch (state) {
|
EnemyAnimation currentAnim = switch (state) {
|
||||||
EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking,
|
EntityState.patrolling => EnemyAnimation.walking,
|
||||||
|
EntityState.ambushing => EnemyAnimation.idle,
|
||||||
EntityState.attacking => EnemyAnimation.attacking,
|
EntityState.attacking => EnemyAnimation.attacking,
|
||||||
EntityState.pain => EnemyAnimation.pain,
|
EntityState.pain => EnemyAnimation.pain,
|
||||||
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
||||||
_ => EnemyAnimation.idle,
|
_ => EnemyAnimation.idle,
|
||||||
};
|
};
|
||||||
|
|
||||||
spriteIndex = type.getSpriteFromAnimation(
|
spriteIndex = spriteForAnimation(
|
||||||
animation: currentAnim,
|
animation: currentAnim,
|
||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
lastActionTime: lastActionTime,
|
lastActionTime: lastActionTime,
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ class Mutant extends Enemy {
|
|||||||
required super.y,
|
required super.y,
|
||||||
required super.angle,
|
required super.angle,
|
||||||
required super.mapId,
|
required super.mapId,
|
||||||
|
super.animationSet,
|
||||||
Difficulty difficulty = Difficulty.medium,
|
Difficulty difficulty = Difficulty.medium,
|
||||||
}) : super(
|
}) : super(
|
||||||
spriteIndex: EnemyType.mutant.animations.idle.start,
|
spriteIndex: (animationSet ?? EnemyType.mutant.animations).idle.start,
|
||||||
state: EntityState.idle,
|
state: EntityState.idle,
|
||||||
) {
|
) {
|
||||||
health = type.hitPointsFor(difficulty);
|
health = type.hitPointsFor(difficulty);
|
||||||
@@ -65,14 +66,15 @@ class Mutant extends Enemy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EnemyAnimation currentAnim = switch (state) {
|
EnemyAnimation currentAnim = switch (state) {
|
||||||
EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking,
|
EntityState.patrolling => EnemyAnimation.walking,
|
||||||
|
EntityState.ambushing => EnemyAnimation.idle,
|
||||||
EntityState.attacking => EnemyAnimation.attacking,
|
EntityState.attacking => EnemyAnimation.attacking,
|
||||||
EntityState.pain => EnemyAnimation.pain,
|
EntityState.pain => EnemyAnimation.pain,
|
||||||
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
||||||
_ => EnemyAnimation.idle,
|
_ => EnemyAnimation.idle,
|
||||||
};
|
};
|
||||||
|
|
||||||
spriteIndex = type.getSpriteFromAnimation(
|
spriteIndex = spriteForAnimation(
|
||||||
animation: currentAnim,
|
animation: currentAnim,
|
||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
lastActionTime: lastActionTime,
|
lastActionTime: lastActionTime,
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ class Officer extends Enemy {
|
|||||||
required super.y,
|
required super.y,
|
||||||
required super.angle,
|
required super.angle,
|
||||||
required super.mapId,
|
required super.mapId,
|
||||||
|
super.animationSet,
|
||||||
Difficulty difficulty = Difficulty.medium,
|
Difficulty difficulty = Difficulty.medium,
|
||||||
}) : super(
|
}) : super(
|
||||||
spriteIndex: EnemyType.officer.animations.idle.start,
|
spriteIndex: (animationSet ?? EnemyType.officer.animations).idle.start,
|
||||||
state: EntityState.idle,
|
state: EntityState.idle,
|
||||||
) {
|
) {
|
||||||
health = type.hitPointsFor(difficulty);
|
health = type.hitPointsFor(difficulty);
|
||||||
@@ -65,14 +66,15 @@ class Officer extends Enemy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EnemyAnimation currentAnim = switch (state) {
|
EnemyAnimation currentAnim = switch (state) {
|
||||||
EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking,
|
EntityState.patrolling => EnemyAnimation.walking,
|
||||||
|
EntityState.ambushing => EnemyAnimation.idle,
|
||||||
EntityState.attacking => EnemyAnimation.attacking,
|
EntityState.attacking => EnemyAnimation.attacking,
|
||||||
EntityState.pain => EnemyAnimation.pain,
|
EntityState.pain => EnemyAnimation.pain,
|
||||||
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
||||||
_ => EnemyAnimation.idle,
|
_ => EnemyAnimation.idle,
|
||||||
};
|
};
|
||||||
|
|
||||||
spriteIndex = type.getSpriteFromAnimation(
|
spriteIndex = spriteForAnimation(
|
||||||
animation: currentAnim,
|
animation: currentAnim,
|
||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
lastActionTime: lastActionTime,
|
lastActionTime: lastActionTime,
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ class SS extends Enemy {
|
|||||||
required super.y,
|
required super.y,
|
||||||
required super.angle,
|
required super.angle,
|
||||||
required super.mapId,
|
required super.mapId,
|
||||||
|
super.animationSet,
|
||||||
Difficulty difficulty = Difficulty.medium,
|
Difficulty difficulty = Difficulty.medium,
|
||||||
}) : super(
|
}) : super(
|
||||||
spriteIndex: EnemyType.ss.animations.idle.start,
|
spriteIndex: (animationSet ?? EnemyType.ss.animations).idle.start,
|
||||||
state: EntityState.idle,
|
state: EntityState.idle,
|
||||||
) {
|
) {
|
||||||
health = type.hitPointsFor(difficulty);
|
health = type.hitPointsFor(difficulty);
|
||||||
@@ -64,14 +65,15 @@ class SS extends Enemy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EnemyAnimation currentAnim = switch (state) {
|
EnemyAnimation currentAnim = switch (state) {
|
||||||
EntityState.patrolling || EntityState.ambushing => EnemyAnimation.walking,
|
EntityState.patrolling => EnemyAnimation.walking,
|
||||||
|
EntityState.ambushing => EnemyAnimation.idle,
|
||||||
EntityState.attacking => EnemyAnimation.attacking,
|
EntityState.attacking => EnemyAnimation.attacking,
|
||||||
EntityState.pain => EnemyAnimation.pain,
|
EntityState.pain => EnemyAnimation.pain,
|
||||||
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
EntityState.dead => isDying ? EnemyAnimation.dying : EnemyAnimation.dead,
|
||||||
_ => EnemyAnimation.idle,
|
_ => EnemyAnimation.idle,
|
||||||
};
|
};
|
||||||
|
|
||||||
spriteIndex = type.getSpriteFromAnimation(
|
spriteIndex = spriteForAnimation(
|
||||||
animation: currentAnim,
|
animation: currentAnim,
|
||||||
elapsedMs: elapsedMs,
|
elapsedMs: elapsedMs,
|
||||||
lastActionTime: lastActionTime,
|
lastActionTime: lastActionTime,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ typedef EntitySpawner =
|
|||||||
double y,
|
double y,
|
||||||
Difficulty difficulty, {
|
Difficulty difficulty, {
|
||||||
bool isSharewareMode,
|
bool isSharewareMode,
|
||||||
|
AssetRegistry? registry,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The central factory for instantiating all dynamic objects in a Wolf3D level.
|
/// The central factory for instantiating all dynamic objects in a Wolf3D level.
|
||||||
@@ -52,6 +53,7 @@ abstract class EntityRegistry {
|
|||||||
Difficulty difficulty,
|
Difficulty difficulty,
|
||||||
int maxSprites, {
|
int maxSprites, {
|
||||||
bool isSharewareMode = false,
|
bool isSharewareMode = false,
|
||||||
|
AssetRegistry? registry,
|
||||||
}) {
|
}) {
|
||||||
if (objId == 0) return null;
|
if (objId == 0) return null;
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ abstract class EntityRegistry {
|
|||||||
y,
|
y,
|
||||||
difficulty,
|
difficulty,
|
||||||
isSharewareMode: isSharewareMode,
|
isSharewareMode: isSharewareMode,
|
||||||
|
registry: registry,
|
||||||
);
|
);
|
||||||
if (entity != null) return entity;
|
if (entity != null) return entity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:wolf_3d_dart/src/data_types/game_version.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/asset_registry.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/built_in_music_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/spear_demo_entity_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/spear_demo_hud_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/spear_demo_menu_module.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/spear_demo_sfx_module.dart';
|
||||||
|
|
||||||
|
/// Built-in [AssetRegistry] for Spear of Destiny demo/shareware (`.SDM`).
|
||||||
|
class SpearDemoAssetRegistry extends AssetRegistry {
|
||||||
|
SpearDemoAssetRegistry()
|
||||||
|
: super(
|
||||||
|
sfx: const SpearDemoSfxModule(),
|
||||||
|
music: const BuiltInMusicModule(GameVersion.spearOfDestinyDemo),
|
||||||
|
entities: const SpearDemoEntityModule(),
|
||||||
|
hud: const SpearDemoHudModule(),
|
||||||
|
menu: const SpearDemoMenuPicModule(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:wolf_3d_dart/src/data_types/enemy_animation.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/data_types/sprite_frame_range.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/keys/entity_key.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/modules/entity_asset_module.dart';
|
||||||
|
|
||||||
|
/// Built-in entity module for Spear of Destiny demo/shareware (`.SDM`).
|
||||||
|
///
|
||||||
|
/// The sprite enum in the original source inserts four additional static
|
||||||
|
/// sprites before enemy animations for SPEAR builds, so all Wolf3D enemy
|
||||||
|
/// animation ranges are shifted by +4.
|
||||||
|
class SpearDemoEntityModule extends EntityAssetModule {
|
||||||
|
const SpearDemoEntityModule();
|
||||||
|
|
||||||
|
static final Map<EntityKey, EntityAssetDefinition> _defs = {
|
||||||
|
EntityKey.guard: const EntityAssetDefinition(
|
||||||
|
availableInShareware: true,
|
||||||
|
animations: EnemyAnimationMap(
|
||||||
|
idle: SpriteFrameRange(54, 61),
|
||||||
|
walking: SpriteFrameRange(62, 93),
|
||||||
|
dying: SpriteFrameRange(94, 97),
|
||||||
|
pain: SpriteFrameRange(98, 98),
|
||||||
|
dead: SpriteFrameRange(99, 99),
|
||||||
|
attacking: SpriteFrameRange(100, 102),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
EntityKey.dog: const EntityAssetDefinition(
|
||||||
|
availableInShareware: true,
|
||||||
|
animations: EnemyAnimationMap(
|
||||||
|
idle: SpriteFrameRange(103, 110),
|
||||||
|
walking: SpriteFrameRange(103, 134),
|
||||||
|
attacking: SpriteFrameRange(139, 141),
|
||||||
|
pain: SpriteFrameRange(0, 0),
|
||||||
|
dying: SpriteFrameRange(135, 137),
|
||||||
|
dead: SpriteFrameRange(138, 138),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
EntityKey.ss: const EntityAssetDefinition(
|
||||||
|
availableInShareware: true,
|
||||||
|
animations: EnemyAnimationMap(
|
||||||
|
idle: SpriteFrameRange(142, 149),
|
||||||
|
walking: SpriteFrameRange(150, 181),
|
||||||
|
attacking: SpriteFrameRange(188, 190),
|
||||||
|
pain: SpriteFrameRange(186, 186),
|
||||||
|
dying: SpriteFrameRange(182, 185),
|
||||||
|
dead: SpriteFrameRange(187, 187),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
EntityAssetDefinition? resolve(EntityKey key) => _defs[key];
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:wolf_3d_dart/src/registry/keys/hud_key.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/modules/hud_module.dart';
|
||||||
|
|
||||||
|
/// Built-in HUD module for Spear of Destiny demo/shareware (`.SDM`).
|
||||||
|
///
|
||||||
|
/// VGA picture indices are derived from `GFXV_SDM.H` (`chunkId - STARTPICS`).
|
||||||
|
class SpearDemoHudModule extends HudModule {
|
||||||
|
const SpearDemoHudModule();
|
||||||
|
|
||||||
|
static const List<HudKey> _faceBands = [
|
||||||
|
HudKey.faceHealthy,
|
||||||
|
HudKey.faceScratched,
|
||||||
|
HudKey.faceHurt,
|
||||||
|
HudKey.faceWounded,
|
||||||
|
HudKey.faceBadlyWounded,
|
||||||
|
HudKey.faceDying,
|
||||||
|
HudKey.faceNearDeath,
|
||||||
|
];
|
||||||
|
|
||||||
|
static final Map<HudKey, int> _indices = {
|
||||||
|
HudKey.statusBar: 73, // STATUSBARPIC (76 - 3)
|
||||||
|
HudKey.digit0: 84, // N_0PIC
|
||||||
|
HudKey.digit1: 85,
|
||||||
|
HudKey.digit2: 86,
|
||||||
|
HudKey.digit3: 87,
|
||||||
|
HudKey.digit4: 88,
|
||||||
|
HudKey.digit5: 89,
|
||||||
|
HudKey.digit6: 90,
|
||||||
|
HudKey.digit7: 91,
|
||||||
|
HudKey.digit8: 92,
|
||||||
|
HudKey.digit9: 93,
|
||||||
|
HudKey.faceHealthy: 94, // FACE1APIC
|
||||||
|
HudKey.faceScratched: 97,
|
||||||
|
HudKey.faceHurt: 100,
|
||||||
|
HudKey.faceWounded: 103,
|
||||||
|
HudKey.faceBadlyWounded: 106,
|
||||||
|
HudKey.faceDying: 109,
|
||||||
|
HudKey.faceNearDeath: 112,
|
||||||
|
HudKey.faceDead: 122, // BJOUCHPIC
|
||||||
|
HudKey.pistolIcon: 77, // GUNPIC
|
||||||
|
HudKey.machineGunIcon: 78, // MACHINEGUNPIC
|
||||||
|
HudKey.chainGunIcon: 79, // GATLINGGUNPIC
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
HudAssetRef? resolve(HudKey key) {
|
||||||
|
final index = _indices[key];
|
||||||
|
return index != null ? HudAssetRef(index) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
HudKey faceKeyForHealth(int health) {
|
||||||
|
if (health <= 0) return HudKey.faceDead;
|
||||||
|
final band = ((100 - health) ~/ 16).clamp(0, _faceBands.length - 1);
|
||||||
|
return _faceBands[band];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:wolf_3d_dart/src/data_types/difficulty.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/keys/menu_pic_key.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/modules/menu_pic_module.dart';
|
||||||
|
|
||||||
|
/// Built-in menu-picture module for Spear of Destiny demo/shareware (`.SDM`).
|
||||||
|
///
|
||||||
|
/// Picture indices are derived from `GFXV_SDM.H` (`chunkId - STARTPICS`).
|
||||||
|
class SpearDemoMenuPicModule extends MenuPicModule {
|
||||||
|
const SpearDemoMenuPicModule();
|
||||||
|
|
||||||
|
static final Map<MenuPicKey, int> _indices = {
|
||||||
|
MenuPicKey.title: 71, // TITLE1PIC
|
||||||
|
MenuPicKey.credits: 75, // CREDITSPIC
|
||||||
|
MenuPicKey.pg13: 74, // PG13PIC
|
||||||
|
|
||||||
|
MenuPicKey.controlBackground: 12, // C_CONTROLPIC
|
||||||
|
MenuPicKey.footer: 1, // C_MOUSELBACKPIC
|
||||||
|
MenuPicKey.heading: 0, // C_BACKDROPPIC
|
||||||
|
MenuPicKey.optionsLabel: 13, // C_OPTIONSPIC
|
||||||
|
MenuPicKey.customizeLabel: 6, // C_CUSTOMIZEPIC
|
||||||
|
|
||||||
|
MenuPicKey.cursorActive: 2, // C_CURSOR1PIC
|
||||||
|
MenuPicKey.cursorInactive: 3, // C_CURSOR2PIC
|
||||||
|
MenuPicKey.markerSelected: 5, // C_SELECTEDPIC
|
||||||
|
MenuPicKey.markerUnselected: 4, // C_NOTSELECTEDPIC
|
||||||
|
|
||||||
|
MenuPicKey.difficultyBaby: 18, // C_BABYMODEPIC
|
||||||
|
MenuPicKey.difficultyEasy: 19, // C_EASYPIC
|
||||||
|
MenuPicKey.difficultyNormal: 20, // C_NORMALPIC
|
||||||
|
MenuPicKey.difficultyHard: 21, // C_HARDPIC
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
MenuPicRef? resolve(MenuPicKey key) {
|
||||||
|
final index = _indices[key];
|
||||||
|
return index != null ? MenuPicRef(index) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
MenuPicKey episodeKey(int episodeIndex) {
|
||||||
|
return MenuPicKey.episode1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
MenuPicKey difficultyKey(Difficulty difficulty) {
|
||||||
|
return switch (difficulty) {
|
||||||
|
Difficulty.baby => MenuPicKey.difficultyBaby,
|
||||||
|
Difficulty.easy => MenuPicKey.difficultyEasy,
|
||||||
|
Difficulty.medium => MenuPicKey.difficultyNormal,
|
||||||
|
Difficulty.hard => MenuPicKey.difficultyHard,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:wolf_3d_dart/src/registry/keys/sfx_key.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/modules/sfx_module.dart';
|
||||||
|
|
||||||
|
/// Built-in digitized SFX slot mapping for Spear of Destiny demo/shareware.
|
||||||
|
///
|
||||||
|
/// Slots follow the original SPEAR/SPEARDEMO DigiMap table in `WL_MAIN.C`.
|
||||||
|
/// Only sounds present in the SDM digitized table are mapped.
|
||||||
|
class SpearDemoSfxModule extends SfxModule {
|
||||||
|
const SpearDemoSfxModule();
|
||||||
|
|
||||||
|
static final Map<SoundEffect, int> _slots = {
|
||||||
|
SoundEffect.openDoor: 3,
|
||||||
|
SoundEffect.closeDoor: 2,
|
||||||
|
SoundEffect.pushWall: 13,
|
||||||
|
|
||||||
|
SoundEffect.pistolFire: 5,
|
||||||
|
SoundEffect.machineGunFire: 4,
|
||||||
|
SoundEffect.chainGunFire: 6,
|
||||||
|
SoundEffect.enemyFire: 17,
|
||||||
|
|
||||||
|
SoundEffect.getChainGun: 38,
|
||||||
|
|
||||||
|
SoundEffect.guardHalt: 0,
|
||||||
|
SoundEffect.ssAlert: 7,
|
||||||
|
SoundEffect.deathScream1: 10,
|
||||||
|
SoundEffect.deathScream2: 11,
|
||||||
|
SoundEffect.deathScream3: 23,
|
||||||
|
SoundEffect.ssDeath: 28,
|
||||||
|
|
||||||
|
SoundEffect.bossActive: 8,
|
||||||
|
SoundEffect.hansGrosseDeath: 24,
|
||||||
|
|
||||||
|
SoundEffect.levelComplete: 22,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
SoundAssetRef? resolve(SoundEffect key) {
|
||||||
|
final slot = _slots[key];
|
||||||
|
return slot == null ? null : SoundAssetRef(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'package:wolf_3d_dart/src/data_types/game_version.dart';
|
|||||||
import 'package:wolf_3d_dart/src/registry/asset_registry.dart';
|
import 'package:wolf_3d_dart/src/registry/asset_registry.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/retail_asset_registry.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/retail_asset_registry.dart';
|
||||||
import 'package:wolf_3d_dart/src/registry/built_in/shareware_asset_registry.dart';
|
import 'package:wolf_3d_dart/src/registry/built_in/shareware_asset_registry.dart';
|
||||||
|
import 'package:wolf_3d_dart/src/registry/built_in/spear_demo_asset_registry.dart';
|
||||||
|
|
||||||
/// The input used by [AssetRegistryResolver] to select or build a registry.
|
/// The input used by [AssetRegistryResolver] to select or build a registry.
|
||||||
class RegistrySelectionContext {
|
class RegistrySelectionContext {
|
||||||
@@ -47,8 +48,9 @@ class BuiltInAssetRegistryResolver implements AssetRegistryResolver {
|
|||||||
case DataVersion.version14Retail:
|
case DataVersion.version14Retail:
|
||||||
return RetailAssetRegistry();
|
return RetailAssetRegistry();
|
||||||
case DataVersion.version14Shareware:
|
case DataVersion.version14Shareware:
|
||||||
case DataVersion.spearOfDestinyShareware:
|
|
||||||
return SharewareAssetRegistry(strictOriginalShareware: true);
|
return SharewareAssetRegistry(strictOriginalShareware: true);
|
||||||
|
case DataVersion.spearOfDestinyShareware:
|
||||||
|
return SpearDemoAssetRegistry();
|
||||||
case _:
|
case _:
|
||||||
break; // fall through to GameVersion family check
|
break; // fall through to GameVersion family check
|
||||||
}
|
}
|
||||||
@@ -61,9 +63,7 @@ class BuiltInAssetRegistryResolver implements AssetRegistryResolver {
|
|||||||
return SharewareAssetRegistry();
|
return SharewareAssetRegistry();
|
||||||
case GameVersion.spearOfDestiny:
|
case GameVersion.spearOfDestiny:
|
||||||
case GameVersion.spearOfDestinyDemo:
|
case GameVersion.spearOfDestinyDemo:
|
||||||
// Spear-specific modules are not yet implemented; retail layout is
|
return SpearDemoAssetRegistry();
|
||||||
// the closest structural match and will be replaced in a later phase.
|
|
||||||
return RetailAssetRegistry();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -649,8 +649,8 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
);
|
);
|
||||||
final selectedMarker = art.selectedMarker;
|
final selectedMarker = art.selectedMarker;
|
||||||
final unselectedMarker = art.unselectedMarker;
|
final unselectedMarker = art.unselectedMarker;
|
||||||
const int rowYStart = 66;
|
const int rowYStart = 64;
|
||||||
const int rowStep = 18;
|
const int rowStep = 16;
|
||||||
const int cursorX = 62;
|
const int cursorX = 62;
|
||||||
const int markerX = 92;
|
const int markerX = 92;
|
||||||
const int textX = 122;
|
const int textX = 122;
|
||||||
@@ -664,11 +664,11 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
? 0
|
? 0
|
||||||
: ((modeCount - 1) * rowStep) + 12;
|
: ((modeCount - 1) * rowStep) + 12;
|
||||||
final int modesPanelHeight = math.max(56, modesContentHeight + 14);
|
final int modesPanelHeight = math.max(56, modesContentHeight + 14);
|
||||||
final int sectionHeaderY = modesPanelY + modesPanelHeight + 6;
|
final int sectionHeaderY = modesPanelY + modesPanelHeight + 4;
|
||||||
final int optionsPanelY = sectionHeaderY + 14;
|
final int optionsPanelY = sectionHeaderY + 14;
|
||||||
final int optionsContentHeight = optionCount <= 0
|
final int optionsContentHeight = optionCount <= 0
|
||||||
? 0
|
? 0
|
||||||
: ((optionCount - 1) * 15) + 12;
|
: ((optionCount - 1) * 14) + 10;
|
||||||
final int optionsPanelHeight = math.max(30, optionsContentHeight + 10);
|
final int optionsPanelHeight = math.max(30, optionsContentHeight + 10);
|
||||||
|
|
||||||
_fillRect320(46, modesPanelY, 228, modesPanelHeight, panelColor);
|
_fillRect320(46, modesPanelY, 228, modesPanelHeight, panelColor);
|
||||||
@@ -706,7 +706,7 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
y200: sectionHeaderY,
|
y200: sectionHeaderY,
|
||||||
);
|
);
|
||||||
|
|
||||||
const int optionsRowStep = 15;
|
const int optionsRowStep = 14;
|
||||||
final int optionsRowsHeight = optionCount <= 0
|
final int optionsRowsHeight = optionCount <= 0
|
||||||
? 0
|
? 0
|
||||||
: ((optionCount - 1) * optionsRowStep) + 10;
|
: ((optionCount - 1) * optionsRowStep) + 10;
|
||||||
|
|||||||
@@ -450,26 +450,8 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
const int modesPanelX = 46;
|
const int modesPanelX = 46;
|
||||||
const int modesPanelY = 52;
|
const int modesPanelY = 52;
|
||||||
const int modesPanelW = 228;
|
const int modesPanelW = 228;
|
||||||
const int modesPanelH = 74;
|
|
||||||
_fillCanonicalRect(
|
|
||||||
modesPanelX,
|
|
||||||
modesPanelY,
|
|
||||||
modesPanelW,
|
|
||||||
modesPanelH,
|
|
||||||
panelColor,
|
|
||||||
);
|
|
||||||
|
|
||||||
const int optionsPanelX = 46;
|
const int optionsPanelX = 46;
|
||||||
const int optionsPanelY = 146;
|
|
||||||
const int optionsPanelW = 228;
|
const int optionsPanelW = 228;
|
||||||
const int optionsPanelH = 42;
|
|
||||||
_fillCanonicalRect(
|
|
||||||
optionsPanelX,
|
|
||||||
optionsPanelY,
|
|
||||||
optionsPanelW,
|
|
||||||
optionsPanelH,
|
|
||||||
panelColor,
|
|
||||||
);
|
|
||||||
|
|
||||||
final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
|
final VgaImage? heading = art.customizeLabel ?? art.optionsLabel;
|
||||||
if (heading != null) {
|
if (heading != null) {
|
||||||
@@ -490,8 +472,8 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||||
);
|
);
|
||||||
|
|
||||||
const int rowYStart = 66;
|
const int rowYStart = 64;
|
||||||
const int rowStep = 18;
|
const int rowStep = 16;
|
||||||
const int cursorX = 62;
|
const int cursorX = 62;
|
||||||
const int markerX = 92;
|
const int markerX = 92;
|
||||||
const int textX = 122;
|
const int textX = 122;
|
||||||
@@ -500,6 +482,38 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
final optionEntries = engine.menuManager.rendererOptionEntries;
|
final optionEntries = engine.menuManager.rendererOptionEntries;
|
||||||
final int modeCount = entries.length;
|
final int modeCount = entries.length;
|
||||||
final int selectedIndex = engine.menuManager.selectedChangeViewIndex;
|
final int selectedIndex = engine.menuManager.selectedChangeViewIndex;
|
||||||
|
final int modesContentHeight = entries.isEmpty
|
||||||
|
? 0
|
||||||
|
: ((entries.length - 1) * rowStep) + 12;
|
||||||
|
final int modesPanelH = math.max(56, modesContentHeight + 14);
|
||||||
|
final int sectionHeaderY = modesPanelY + modesPanelH + 4;
|
||||||
|
final int optionsPanelY = sectionHeaderY + 14;
|
||||||
|
const int optionsRowStep = 14;
|
||||||
|
final int optionsContentHeight = optionEntries.isEmpty
|
||||||
|
? 0
|
||||||
|
: ((optionEntries.length - 1) * optionsRowStep) + 10;
|
||||||
|
final int optionsPanelH = math.max(
|
||||||
|
30,
|
||||||
|
math.min(
|
||||||
|
_menuFooterY - 6 - optionsPanelY,
|
||||||
|
optionsContentHeight + 10,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_fillCanonicalRect(
|
||||||
|
modesPanelX,
|
||||||
|
modesPanelY,
|
||||||
|
modesPanelW,
|
||||||
|
modesPanelH,
|
||||||
|
panelColor,
|
||||||
|
);
|
||||||
|
_fillCanonicalRect(
|
||||||
|
optionsPanelX,
|
||||||
|
optionsPanelY,
|
||||||
|
optionsPanelW,
|
||||||
|
optionsPanelH,
|
||||||
|
panelColor,
|
||||||
|
);
|
||||||
|
|
||||||
for (int i = 0; i < entries.length; i++) {
|
for (int i = 0; i < entries.length; i++) {
|
||||||
final bool isSelected = i == selectedIndex;
|
final bool isSelected = i == selectedIndex;
|
||||||
@@ -528,12 +542,15 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
|
|
||||||
_drawMenuSectionHeader(
|
_drawMenuSectionHeader(
|
||||||
text: engine.menuManager.rendererOptionsTitle,
|
text: engine.menuManager.rendererOptionsTitle,
|
||||||
y200: 132,
|
y200: sectionHeaderY,
|
||||||
textColor: ColorPalette.vga32Bit[8],
|
textColor: ColorPalette.vga32Bit[8],
|
||||||
);
|
);
|
||||||
|
|
||||||
const int optionsRowStart = 159;
|
final int optionsRowsHeight = optionEntries.isEmpty
|
||||||
const int optionsRowStep = 15;
|
? 0
|
||||||
|
: ((optionEntries.length - 1) * optionsRowStep) + 10;
|
||||||
|
final int optionsRowStart =
|
||||||
|
optionsPanelY + ((optionsPanelH - optionsRowsHeight) ~/ 2).clamp(0, 200);
|
||||||
for (int i = 0; i < optionEntries.length; i++) {
|
for (int i = 0; i < optionEntries.length; i++) {
|
||||||
final int optionIndex = modeCount + i;
|
final int optionIndex = modeCount + i;
|
||||||
final bool isSelected = optionIndex == selectedIndex;
|
final bool isSelected = optionIndex == selectedIndex;
|
||||||
|
|||||||
@@ -152,6 +152,20 @@ class WolfAudio implements EngineAudio {
|
|||||||
Future<void> playSoundEffect(SoundEffect effect) async {
|
Future<void> playSoundEffect(SoundEffect effect) async {
|
||||||
final data = activeGame;
|
final data = activeGame;
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
final resolved = data.registry.sfx.resolve(effect);
|
||||||
|
if (resolved != null) {
|
||||||
|
await playSoundEffectId(resolved.slotIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spear demo/shareware has a much smaller digitized table than retail.
|
||||||
|
// If a sound is not explicitly mapped for that variant, skip it rather
|
||||||
|
// than probing an invalid or unrelated slot.
|
||||||
|
if (data.version == GameVersion.spearOfDestinyDemo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await playSoundEffectId(effect.idFor(data.version));
|
await playSoundEffectId(effect.idFor(data.version));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export 'src/registry/built_in/retail_asset_registry.dart'
|
|||||||
show RetailAssetRegistry;
|
show RetailAssetRegistry;
|
||||||
export 'src/registry/built_in/shareware_asset_registry.dart'
|
export 'src/registry/built_in/shareware_asset_registry.dart'
|
||||||
show SharewareAssetRegistry;
|
show SharewareAssetRegistry;
|
||||||
|
export 'src/registry/built_in/spear_demo_asset_registry.dart'
|
||||||
|
show SpearDemoAssetRegistry;
|
||||||
export 'src/registry/keys/entity_key.dart' show EntityKey;
|
export 'src/registry/keys/entity_key.dart' show EntityKey;
|
||||||
export 'src/registry/keys/hud_key.dart' show HudKey;
|
export 'src/registry/keys/hud_key.dart' show HudKey;
|
||||||
export 'src/registry/keys/menu_pic_key.dart' show MenuPicKey;
|
export 'src/registry/keys/menu_pic_key.dart' show MenuPicKey;
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_entities.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('SpearDemoAssetRegistry', () {
|
||||||
|
test('resolves SDM menu and HUD VGA indices', () {
|
||||||
|
final registry = SpearDemoAssetRegistry();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
registry.menu.resolve(MenuPicKey.controlBackground)?.pictureIndex,
|
||||||
|
12,
|
||||||
|
);
|
||||||
|
expect(registry.menu.resolve(MenuPicKey.optionsLabel)?.pictureIndex, 13);
|
||||||
|
expect(registry.menu.resolve(MenuPicKey.episode1), isNull);
|
||||||
|
|
||||||
|
expect(registry.hud.resolve(HudKey.statusBar)?.vgaIndex, 73);
|
||||||
|
expect(registry.hud.resolve(HudKey.digit0)?.vgaIndex, 84);
|
||||||
|
expect(registry.hud.resolve(HudKey.pistolIcon)?.vgaIndex, 77);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolves SDM enemy sprite ranges with +4 SPEAR shift', () {
|
||||||
|
final registry = SpearDemoAssetRegistry();
|
||||||
|
|
||||||
|
final guard = registry.entities.resolve(EntityKey.guard);
|
||||||
|
final dog = registry.entities.resolve(EntityKey.dog);
|
||||||
|
|
||||||
|
expect(guard?.animations?.idle.start, 54);
|
||||||
|
expect(guard?.animations?.attacking.end, 102);
|
||||||
|
expect(dog?.animations?.walking.start, 103);
|
||||||
|
expect(dog?.animations?.dead.start, 138);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses SDM digitized sound slot mapping', () {
|
||||||
|
final registry = SpearDemoAssetRegistry();
|
||||||
|
|
||||||
|
expect(registry.sfx.resolve(SoundEffect.openDoor)?.slotIndex, 3);
|
||||||
|
expect(registry.sfx.resolve(SoundEffect.closeDoor)?.slotIndex, 2);
|
||||||
|
expect(registry.sfx.resolve(SoundEffect.pushWall)?.slotIndex, 13);
|
||||||
|
expect(registry.sfx.resolve(SoundEffect.levelComplete)?.slotIndex, 22);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('BuiltInAssetRegistryResolver Spear SDM selection', () {
|
||||||
|
test('uses Spear demo registry for exact SDM identity', () {
|
||||||
|
const resolver = BuiltInAssetRegistryResolver();
|
||||||
|
|
||||||
|
final registry = resolver.resolve(
|
||||||
|
const RegistrySelectionContext(
|
||||||
|
gameVersion: GameVersion.spearOfDestinyDemo,
|
||||||
|
dataVersion: DataVersion.spearOfDestinyShareware,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(registry, isA<SpearDemoAssetRegistry>());
|
||||||
|
expect(registry.hud.resolve(HudKey.statusBar)?.vgaIndex, 73);
|
||||||
|
expect(registry.sfx.resolve(SoundEffect.openDoor)?.slotIndex, 3);
|
||||||
|
expect(
|
||||||
|
registry.entities.resolve(EntityKey.guard)?.animations?.idle.start,
|
||||||
|
54,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runtime enemy spawn consumes registry animation ranges', () {
|
||||||
|
final registry = SpearDemoAssetRegistry();
|
||||||
|
|
||||||
|
final enemy = Enemy.spawn(
|
||||||
|
MapObject.guardStart,
|
||||||
|
4.5,
|
||||||
|
5.5,
|
||||||
|
Difficulty.medium,
|
||||||
|
registry: registry,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(enemy, isA<Guard>());
|
||||||
|
expect(enemy?.state, EntityState.ambushing);
|
||||||
|
expect(enemy?.spriteIndex, 54); // SDM guard idle start
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -14,6 +14,9 @@ class WolfGlslRenderer extends BaseWolfRenderer {
|
|||||||
/// Whether CRT-like post effects are enabled in the shader pass.
|
/// Whether CRT-like post effects are enabled in the shader pass.
|
||||||
final bool effectsEnabled;
|
final bool effectsEnabled;
|
||||||
|
|
||||||
|
/// Whether CRT phosphor bloom is enabled in the shader pass.
|
||||||
|
final bool bloomEnabled;
|
||||||
|
|
||||||
/// Callback when shader loading fails and software fallback should be used.
|
/// Callback when shader loading fails and software fallback should be used.
|
||||||
final VoidCallback? onUnavailable;
|
final VoidCallback? onUnavailable;
|
||||||
|
|
||||||
@@ -21,6 +24,7 @@ class WolfGlslRenderer extends BaseWolfRenderer {
|
|||||||
const WolfGlslRenderer({
|
const WolfGlslRenderer({
|
||||||
required super.engine,
|
required super.engine,
|
||||||
this.effectsEnabled = false,
|
this.effectsEnabled = false,
|
||||||
|
this.bloomEnabled = false,
|
||||||
super.onKeyEvent,
|
super.onKeyEvent,
|
||||||
this.onUnavailable,
|
this.onUnavailable,
|
||||||
super.key,
|
super.key,
|
||||||
@@ -116,6 +120,7 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
|||||||
frame: _renderedFrame!,
|
frame: _renderedFrame!,
|
||||||
shader: _shader!,
|
shader: _shader!,
|
||||||
effectsEnabled: widget.effectsEnabled,
|
effectsEnabled: widget.effectsEnabled,
|
||||||
|
bloomEnabled: widget.bloomEnabled,
|
||||||
elapsedSeconds: widget.engine.timeAliveMs / 1000.0,
|
elapsedSeconds: widget.engine.timeAliveMs / 1000.0,
|
||||||
),
|
),
|
||||||
child: const SizedBox.expand(),
|
child: const SizedBox.expand(),
|
||||||
@@ -153,12 +158,14 @@ class _GlslFramePainter extends CustomPainter {
|
|||||||
final ui.Image frame;
|
final ui.Image frame;
|
||||||
final ui.FragmentShader shader;
|
final ui.FragmentShader shader;
|
||||||
final bool effectsEnabled;
|
final bool effectsEnabled;
|
||||||
|
final bool bloomEnabled;
|
||||||
final double elapsedSeconds;
|
final double elapsedSeconds;
|
||||||
|
|
||||||
_GlslFramePainter({
|
_GlslFramePainter({
|
||||||
required this.frame,
|
required this.frame,
|
||||||
required this.shader,
|
required this.shader,
|
||||||
required this.effectsEnabled,
|
required this.effectsEnabled,
|
||||||
|
required this.bloomEnabled,
|
||||||
required this.elapsedSeconds,
|
required this.elapsedSeconds,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -173,6 +180,7 @@ class _GlslFramePainter extends CustomPainter {
|
|||||||
..setFloat(3, texelY)
|
..setFloat(3, texelY)
|
||||||
..setFloat(4, effectsEnabled ? 1.0 : 0.0)
|
..setFloat(4, effectsEnabled ? 1.0 : 0.0)
|
||||||
..setFloat(5, elapsedSeconds)
|
..setFloat(5, elapsedSeconds)
|
||||||
|
..setFloat(6, bloomEnabled ? 1.0 : 0.0)
|
||||||
..setImageSampler(0, frame);
|
..setImageSampler(0, frame);
|
||||||
|
|
||||||
final Paint paint = Paint()
|
final Paint paint = Paint()
|
||||||
@@ -187,6 +195,7 @@ class _GlslFramePainter extends CustomPainter {
|
|||||||
return oldDelegate.frame != frame ||
|
return oldDelegate.frame != frame ||
|
||||||
oldDelegate.shader != shader ||
|
oldDelegate.shader != shader ||
|
||||||
oldDelegate.effectsEnabled != effectsEnabled ||
|
oldDelegate.effectsEnabled != effectsEnabled ||
|
||||||
|
oldDelegate.bloomEnabled != bloomEnabled ||
|
||||||
oldDelegate.elapsedSeconds != elapsedSeconds;
|
oldDelegate.elapsedSeconds != elapsedSeconds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ uniform vec2 uTexel;
|
|||||||
uniform float uEffectsEnabled;
|
uniform float uEffectsEnabled;
|
||||||
// Engine time in seconds used to animate scanline travel.
|
// Engine time in seconds used to animate scanline travel.
|
||||||
uniform float uTime;
|
uniform float uTime;
|
||||||
|
// 1.0 enables CRT phosphor bloom glow, 0.0 disables it.
|
||||||
|
uniform float uBloomEnabled;
|
||||||
// Source frame produced by the software renderer.
|
// Source frame produced by the software renderer.
|
||||||
uniform sampler2D uTexture;
|
uniform sampler2D uTexture;
|
||||||
|
|
||||||
@@ -46,6 +48,7 @@ void main() {
|
|||||||
texture(uTexture, bleedUv1).rgb * 0.28 +
|
texture(uTexture, bleedUv1).rgb * 0.28 +
|
||||||
texture(uTexture, bleedUv2).rgb * 0.14 +
|
texture(uTexture, bleedUv2).rgb * 0.14 +
|
||||||
texture(uTexture, bleedUv3).rgb * 0.06;
|
texture(uTexture, bleedUv3).rgb * 0.06;
|
||||||
|
float edgeBleedLuma = luma(edgeBleedColor);
|
||||||
|
|
||||||
// Approximate concave bezel depth by measuring how far this fragment is
|
// Approximate concave bezel depth by measuring how far this fragment is
|
||||||
// from the emissive screen boundary in aspect-corrected UV space.
|
// from the emissive screen boundary in aspect-corrected UV space.
|
||||||
@@ -66,11 +69,15 @@ void main() {
|
|||||||
float bezelGlow = exp(-bezelDistance * 82.0) * mix(1.0, 0.56, cornerFactor);
|
float bezelGlow = exp(-bezelDistance * 82.0) * mix(1.0, 0.56, cornerFactor);
|
||||||
float innerLip = exp(-bezelDistance * 170.0) * 0.10;
|
float innerLip = exp(-bezelDistance * 170.0) * 0.10;
|
||||||
float bleedStrength = smoothstep(0.12, 0.0, overflow) * (0.78 - cornerFactor * 0.26);
|
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 bezelColor =
|
||||||
vec3(0.225, 0.225, 0.215) * verticalShade * depthShade +
|
vec3(0.225, 0.225, 0.215) * verticalShade * depthShade +
|
||||||
edgeBleedColor * bezelGlow * bleedStrength * 1.12 +
|
edgeBleedColor * bezelGlow * bleedStrength * 1.12 * bloomBezelBoost +
|
||||||
edgeBleedColor * innerLip * 0.36 +
|
edgeBleedColor * innerLip * 0.36 * bloomLipBoost +
|
||||||
vec3(moldedHighlight) +
|
vec3(moldedHighlight) +
|
||||||
vec3(grain);
|
vec3(grain);
|
||||||
fragColor = vec4(bezelColor, 1.0);
|
fragColor = vec4(bezelColor, 1.0);
|
||||||
@@ -127,5 +134,42 @@ void main() {
|
|||||||
outColor *= mix(0.62, 1.0, vignette);
|
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);
|
fragColor = vec4(outColor, centerSample.a);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user