feat: Add bonus flash effect for player pickups and update rendering logic
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -22,6 +22,10 @@ class Player {
|
|||||||
double damageFlash = 0.0; // 0.0 is none, 1.0 is maximum red
|
double damageFlash = 0.0; // 0.0 is none, 1.0 is maximum red
|
||||||
final double damageFlashFadeSpeed = 0.05; // How fast it fades per tick
|
final double damageFlashFadeSpeed = 0.05; // How fast it fades per tick
|
||||||
|
|
||||||
|
// Bonus flash
|
||||||
|
double bonusFlash = 0.0; // 0.0 is none, 1.0 is maximum white
|
||||||
|
final double bonusFlashFadeSpeed = 0.05; // How fast it fades per tick
|
||||||
|
|
||||||
// Inventory
|
// Inventory
|
||||||
bool hasGoldKey = false;
|
bool hasGoldKey = false;
|
||||||
bool hasSilverKey = false;
|
bool hasSilverKey = false;
|
||||||
@@ -62,6 +66,10 @@ class Player {
|
|||||||
damageFlash = math.max(0.0, damageFlash - damageFlashFadeSpeed);
|
damageFlash = math.max(0.0, damageFlash - damageFlashFadeSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bonusFlash > 0.0) {
|
||||||
|
bonusFlash = math.max(0.0, bonusFlash - bonusFlashFadeSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
updateWeaponSwitch();
|
updateWeaponSwitch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +139,10 @@ class Player {
|
|||||||
health = newHealth;
|
health = newHealth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void triggerBonusFlash() {
|
||||||
|
bonusFlash = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
void addAmmo(int amount) {
|
void addAmmo(int amount) {
|
||||||
final int newAmmo = math.min(99, ammo + amount);
|
final int newAmmo = math.min(99, ammo + amount);
|
||||||
if (ammo < 99) {
|
if (ammo < 99) {
|
||||||
@@ -211,6 +223,8 @@ class Player {
|
|||||||
requestWeaponSwitch(weaponType);
|
requestWeaponSwitch(weaponType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
triggerBonusFlash();
|
||||||
|
|
||||||
return effect.pickupSoundEffect;
|
return effect.pickupSoundEffect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2094,12 +2094,20 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
dynamic finalizeFrame() {
|
dynamic finalizeFrame() {
|
||||||
if (engine.difficulty != null && engine.player.damageFlash > 0.0) {
|
if (engine.difficulty != null) {
|
||||||
|
if (engine.player.damageFlash > 0.0) {
|
||||||
if (_usesTerminalLayout) {
|
if (_usesTerminalLayout) {
|
||||||
_applyDamageFlashToScene();
|
_applyDamageFlashToScene();
|
||||||
} else {
|
} else {
|
||||||
_applyDamageFlash();
|
_applyDamageFlash();
|
||||||
}
|
}
|
||||||
|
} else if (engine.player.bonusFlash > 0.0) {
|
||||||
|
if (_usesTerminalLayout) {
|
||||||
|
_applyBonusFlashToScene();
|
||||||
|
} else {
|
||||||
|
_applyBonusFlash();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (_usesTerminalLayout) {
|
if (_usesTerminalLayout) {
|
||||||
_composeTerminalScene();
|
_composeTerminalScene();
|
||||||
@@ -2275,6 +2283,44 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _applyBonusFlash() {
|
||||||
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
final ColoredChar cell = _screen[y][x];
|
||||||
|
_screen[y][x] = ColoredChar(
|
||||||
|
cell.char,
|
||||||
|
_applyBonusFlashToColor(cell.rawColor),
|
||||||
|
cell.rawBackgroundColor == null
|
||||||
|
? null
|
||||||
|
: _applyBonusFlashToColor(cell.rawBackgroundColor!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyBonusFlashToScene() {
|
||||||
|
for (int y = 0; y < _terminalPixelHeight; y++) {
|
||||||
|
for (int x = projectionOffsetX; x < _viewportRightX; x++) {
|
||||||
|
_scenePixels[y][x] = _applyBonusFlashToColor(_scenePixels[y][x]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int _applyBonusFlashToColor(int color) {
|
||||||
|
final double intensity = engine.player.bonusFlash;
|
||||||
|
final double whiteMix = 0.65 * intensity;
|
||||||
|
|
||||||
|
int r = color & 0xFF;
|
||||||
|
int g = (color >> 8) & 0xFF;
|
||||||
|
int b = (color >> 16) & 0xFF;
|
||||||
|
|
||||||
|
r = (r + ((255 - r) * whiteMix)).round().clamp(0, 255);
|
||||||
|
g = (g + ((255 - g) * whiteMix)).round().clamp(0, 255);
|
||||||
|
b = (b + ((255 - b) * whiteMix)).round().clamp(0, 255);
|
||||||
|
|
||||||
|
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||||
|
}
|
||||||
|
|
||||||
int _scaleColor(int color, double brightness) {
|
int _scaleColor(int color, double brightness) {
|
||||||
int r = ((color & 0xFF) * brightness).toInt().clamp(0, 255);
|
int r = ((color & 0xFF) * brightness).toInt().clamp(0, 255);
|
||||||
int g = (((color >> 8) & 0xFF) * brightness).toInt().clamp(0, 255);
|
int g = (((color >> 8) & 0xFF) * brightness).toInt().clamp(0, 255);
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ abstract class RendererBackend<T>
|
|||||||
_drawHudKeySlot(
|
_drawHudKeySlot(
|
||||||
engine,
|
engine,
|
||||||
vgaImages,
|
vgaImages,
|
||||||
startX: 30,
|
startX: 240,
|
||||||
startY: 164,
|
startY: 164,
|
||||||
hasKey: engine.player.hasGoldKey,
|
hasKey: engine.player.hasGoldKey,
|
||||||
presentKey: HudKey.goldKeyIcon,
|
presentKey: HudKey.goldKeyIcon,
|
||||||
@@ -256,7 +256,7 @@ abstract class RendererBackend<T>
|
|||||||
_drawHudKeySlot(
|
_drawHudKeySlot(
|
||||||
engine,
|
engine,
|
||||||
vgaImages,
|
vgaImages,
|
||||||
startX: 30,
|
startX: 240,
|
||||||
startY: 180,
|
startY: 180,
|
||||||
hasKey: engine.player.hasSilverKey,
|
hasKey: engine.player.hasSilverKey,
|
||||||
presentKey: HudKey.silverKeyIcon,
|
presentKey: HudKey.silverKeyIcon,
|
||||||
|
|||||||
@@ -1336,11 +1336,16 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
sb.write('\x1bPq');
|
sb.write('\x1bPq');
|
||||||
sb.write('"1;1;$_outputWidth;$_outputHeight');
|
sb.write('"1;1;$_outputWidth;$_outputHeight');
|
||||||
|
|
||||||
double damageIntensity = engine.difficulty == null
|
final bool gameplayActive = engine.difficulty != null;
|
||||||
? 0.0
|
final double damageIntensity = gameplayActive
|
||||||
: engine.player.damageFlash;
|
? engine.player.damageFlash
|
||||||
int redBoost = (150 * damageIntensity).toInt();
|
: 0.0;
|
||||||
double colorDrop = 1.0 - (0.5 * damageIntensity);
|
final double bonusIntensity = gameplayActive && damageIntensity <= 0.0
|
||||||
|
? engine.player.bonusFlash
|
||||||
|
: 0.0;
|
||||||
|
final int redBoost = (150 * damageIntensity).toInt();
|
||||||
|
final double colorDrop = 1.0 - (0.5 * damageIntensity);
|
||||||
|
final double whiteMix = 0.65 * bonusIntensity;
|
||||||
|
|
||||||
for (int i = 0; i < 256; i++) {
|
for (int i = 0; i < 256; i++) {
|
||||||
int color = ColorPalette.vga32Bit[i];
|
int color = ColorPalette.vga32Bit[i];
|
||||||
@@ -1352,6 +1357,10 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
r = (r + redBoost).clamp(0, 255);
|
r = (r + redBoost).clamp(0, 255);
|
||||||
g = (g * colorDrop).toInt().clamp(0, 255);
|
g = (g * colorDrop).toInt().clamp(0, 255);
|
||||||
b = (b * colorDrop).toInt().clamp(0, 255);
|
b = (b * colorDrop).toInt().clamp(0, 255);
|
||||||
|
} else if (bonusIntensity > 0) {
|
||||||
|
r = (r + ((255 - r) * whiteMix)).round().clamp(0, 255);
|
||||||
|
g = (g + ((255 - g) * whiteMix)).round().clamp(0, 255);
|
||||||
|
b = (b + ((255 - b) * whiteMix)).round().clamp(0, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
int sixelR = (r * 100) ~/ 255;
|
int sixelR = (r * 100) ~/ 255;
|
||||||
|
|||||||
@@ -1397,9 +1397,12 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
FrameBuffer finalizeFrame() {
|
FrameBuffer finalizeFrame() {
|
||||||
// If the player took damage, overlay a red tint across the 3D view
|
if (engine.difficulty != null) {
|
||||||
if (engine.difficulty != null && engine.player.damageFlash > 0) {
|
if (engine.player.damageFlash > 0) {
|
||||||
_applyDamageFlash();
|
_applyDamageFlash();
|
||||||
|
} else if (engine.player.bonusFlash > 0) {
|
||||||
|
_applyBonusFlash();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return _buffer; // Return the fully painted pixel array
|
return _buffer; // Return the fully painted pixel array
|
||||||
}
|
}
|
||||||
@@ -1467,4 +1470,26 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _applyBonusFlash() {
|
||||||
|
final double intensity = engine.player.bonusFlash;
|
||||||
|
final double whiteMix = 0.65 * intensity;
|
||||||
|
|
||||||
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
final int index = y * width + x;
|
||||||
|
final int color = _buffer.pixels[index];
|
||||||
|
|
||||||
|
int r = color & 0xFF;
|
||||||
|
int g = (color >> 8) & 0xFF;
|
||||||
|
int b = (color >> 16) & 0xFF;
|
||||||
|
|
||||||
|
r = (r + ((255 - r) * whiteMix)).round().clamp(0, 255);
|
||||||
|
g = (g + ((255 - g) * whiteMix)).round().clamp(0, 255);
|
||||||
|
b = (b + ((255 - b) * whiteMix)).round().clamp(0, 255);
|
||||||
|
|
||||||
|
_buffer.pixels[index] = (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,5 +102,18 @@ void main() {
|
|||||||
}
|
}
|
||||||
expect(player.lives, 9);
|
expect(player.lives, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('successful pickups trigger bonus flash and it fades over time', () {
|
||||||
|
final player = Player(x: 1.5, y: 1.5, angle: 0)..health = 50;
|
||||||
|
final food = HealthCollectible(x: 1, y: 1, mapId: MapObject.food);
|
||||||
|
|
||||||
|
expect(player.bonusFlash, 0.0);
|
||||||
|
player.tryPickup(food);
|
||||||
|
expect(player.bonusFlash, 1.0);
|
||||||
|
|
||||||
|
player.tick(const Duration(milliseconds: 16));
|
||||||
|
expect(player.bonusFlash, lessThan(1.0));
|
||||||
|
expect(player.bonusFlash, greaterThan(0.0));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,17 +46,38 @@ void main() {
|
|||||||
expect(expectedNoKeyIndex, isNotNull);
|
expect(expectedNoKeyIndex, isNotNull);
|
||||||
|
|
||||||
final goldKeyCall = renderer.drawCalls.firstWhere(
|
final goldKeyCall = renderer.drawCalls.firstWhere(
|
||||||
(call) => call.startY == 164 && call.startX == 30,
|
(call) => call.startY == 164 && call.startX == 240,
|
||||||
orElse: () => throw StateError('Gold key slot was not rendered.'),
|
orElse: () => throw StateError('Gold key slot was not rendered.'),
|
||||||
);
|
);
|
||||||
final silverKeyCall = renderer.drawCalls.firstWhere(
|
final silverKeyCall = renderer.drawCalls.firstWhere(
|
||||||
(call) => call.startY == 180 && call.startX == 30,
|
(call) => call.startY == 180 && call.startX == 240,
|
||||||
orElse: () => throw StateError('Silver key slot was not rendered.'),
|
orElse: () => throw StateError('Silver key slot was not rendered.'),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(goldKeyCall.imageIndex, expectedGoldKeyIndex);
|
expect(goldKeyCall.imageIndex, expectedGoldKeyIndex);
|
||||||
expect(silverKeyCall.imageIndex, expectedNoKeyIndex);
|
expect(silverKeyCall.imageIndex, expectedNoKeyIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('standard VGA HUD face follows current health band', () {
|
||||||
|
final engine = _buildEngine();
|
||||||
|
engine.init();
|
||||||
|
engine.player.health = 20;
|
||||||
|
|
||||||
|
final renderer = _HudProbeRenderer(vgaImages: engine.data.vgaImages);
|
||||||
|
renderer.drawHudForTest(engine);
|
||||||
|
|
||||||
|
final expectedFaceIndex = engine.data.registry.hud
|
||||||
|
.faceForHealth(engine.player.health)
|
||||||
|
?.vgaIndex;
|
||||||
|
expect(expectedFaceIndex, isNotNull);
|
||||||
|
|
||||||
|
final faceCall = renderer.drawCalls.firstWhere(
|
||||||
|
(call) => call.startY == 164 && call.startX == 136,
|
||||||
|
orElse: () => throw StateError('Face slot was not rendered.'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(faceCall.imageIndex, expectedFaceIndex);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HudProbeRenderer extends RendererBackend<int> {
|
class _HudProbeRenderer extends RendererBackend<int> {
|
||||||
|
|||||||
Reference in New Issue
Block a user