feat: Implement map overlay toggle functionality and rendering across input and rendering systems
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ class EngineInput {
|
|||||||
final bool isMovingBackward;
|
final bool isMovingBackward;
|
||||||
final bool isTurningLeft;
|
final bool isTurningLeft;
|
||||||
final bool isTurningRight;
|
final bool isTurningRight;
|
||||||
|
final bool isMapToggle;
|
||||||
final bool isFiring;
|
final bool isFiring;
|
||||||
final bool isInteracting;
|
final bool isInteracting;
|
||||||
final bool isBack;
|
final bool isBack;
|
||||||
@@ -18,6 +19,7 @@ class EngineInput {
|
|||||||
this.isMovingBackward = false,
|
this.isMovingBackward = false,
|
||||||
this.isTurningLeft = false,
|
this.isTurningLeft = false,
|
||||||
this.isTurningRight = false,
|
this.isTurningRight = false,
|
||||||
|
this.isMapToggle = false,
|
||||||
this.isFiring = false,
|
this.isFiring = false,
|
||||||
this.isInteracting = false,
|
this.isInteracting = false,
|
||||||
this.isBack = false,
|
this.isBack = false,
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ class WolfEngine {
|
|||||||
/// Whether renderers should draw the FPS counter overlay.
|
/// Whether renderers should draw the FPS counter overlay.
|
||||||
bool showFpsCounter = false;
|
bool showFpsCounter = false;
|
||||||
|
|
||||||
|
/// Whether gameplay renderers should draw the fullscreen map overlay.
|
||||||
|
bool isMapOverlayVisible = false;
|
||||||
|
|
||||||
/// The episode index where the game session begins.
|
/// The episode index where the game session begins.
|
||||||
final int? startingEpisode;
|
final int? startingEpisode;
|
||||||
|
|
||||||
@@ -463,6 +466,10 @@ class WolfEngine {
|
|||||||
input.update();
|
input.update();
|
||||||
final currentInput = input.currentInput;
|
final currentInput = input.currentInput;
|
||||||
|
|
||||||
|
if (!isMenuOpen && currentInput.isMapToggle) {
|
||||||
|
isMapOverlayVisible = !isMapOverlayVisible;
|
||||||
|
}
|
||||||
|
|
||||||
if (difficulty != null && !_isMenuOverlayVisible && currentInput.isBack) {
|
if (difficulty != null && !_isMenuOverlayVisible && currentInput.isBack) {
|
||||||
_openPauseMenu();
|
_openPauseMenu();
|
||||||
menuManager.absorbInputState(currentInput);
|
menuManager.absorbInputState(currentInput);
|
||||||
@@ -711,6 +718,7 @@ class WolfEngine {
|
|||||||
if (!_hasActiveSession) {
|
if (!_hasActiveSession) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
isMapOverlayVisible = false;
|
||||||
_isMenuOverlayVisible = true;
|
_isMenuOverlayVisible = true;
|
||||||
menuManager.showMainMenu(hasResumableGame: true);
|
menuManager.showMainMenu(hasResumableGame: true);
|
||||||
}
|
}
|
||||||
@@ -724,6 +732,7 @@ class WolfEngine {
|
|||||||
difficulty = selectedDifficulty;
|
difficulty = selectedDifficulty;
|
||||||
_currentLevelIndex = 0;
|
_currentLevelIndex = 0;
|
||||||
_returnLevelIndex = null;
|
_returnLevelIndex = null;
|
||||||
|
isMapOverlayVisible = false;
|
||||||
_isMenuOverlayVisible = false;
|
_isMenuOverlayVisible = false;
|
||||||
_loadLevel(preservePlayerState: false);
|
_loadLevel(preservePlayerState: false);
|
||||||
_hasActiveSession = true;
|
_hasActiveSession = true;
|
||||||
@@ -731,6 +740,7 @@ class WolfEngine {
|
|||||||
|
|
||||||
void _endCurrentGame() {
|
void _endCurrentGame() {
|
||||||
difficulty = null;
|
difficulty = null;
|
||||||
|
isMapOverlayVisible = false;
|
||||||
_isMenuOverlayVisible = false;
|
_isMenuOverlayVisible = false;
|
||||||
_hasActiveSession = false;
|
_hasActiveSession = false;
|
||||||
_returnLevelIndex = null;
|
_returnLevelIndex = null;
|
||||||
@@ -756,6 +766,7 @@ class WolfEngine {
|
|||||||
|
|
||||||
/// Wipes the current world state and builds a new floor from map data.
|
/// Wipes the current world state and builds a new floor from map data.
|
||||||
void _loadLevel({required bool preservePlayerState}) {
|
void _loadLevel({required bool preservePlayerState}) {
|
||||||
|
isMapOverlayVisible = false;
|
||||||
entities.clear();
|
entities.clear();
|
||||||
_lastPatrolTileByEnemy.clear();
|
_lastPatrolTileByEnemy.clear();
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class CliInput extends Wolf3dInput {
|
|||||||
bool _pFire = false;
|
bool _pFire = false;
|
||||||
bool _pInteract = false;
|
bool _pInteract = false;
|
||||||
bool _pBack = false;
|
bool _pBack = false;
|
||||||
|
bool _pMapToggle = false;
|
||||||
WeaponType? _pWeapon;
|
WeaponType? _pWeapon;
|
||||||
|
|
||||||
/// Queues a raw terminal key sequence for the next engine frame.
|
/// Queues a raw terminal key sequence for the next engine frame.
|
||||||
@@ -94,6 +95,12 @@ class CliInput extends Wolf3dInput {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tab toggles fullscreen map overlay.
|
||||||
|
if (bytes.length == 1 && bytes[0] == 9) {
|
||||||
|
_pMapToggle = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String char = String.fromCharCodes(bytes).toLowerCase();
|
String char = String.fromCharCodes(bytes).toLowerCase();
|
||||||
|
|
||||||
if (char == 'w') _pForward = true;
|
if (char == 'w') _pForward = true;
|
||||||
@@ -119,6 +126,7 @@ class CliInput extends Wolf3dInput {
|
|||||||
isMovingBackward = _pBackward;
|
isMovingBackward = _pBackward;
|
||||||
isTurningLeft = _pLeft;
|
isTurningLeft = _pLeft;
|
||||||
isTurningRight = _pRight;
|
isTurningRight = _pRight;
|
||||||
|
isMapToggle = _pMapToggle;
|
||||||
isFiring = _pFire;
|
isFiring = _pFire;
|
||||||
isInteracting = _pInteract;
|
isInteracting = _pInteract;
|
||||||
isBack = _pBack;
|
isBack = _pBack;
|
||||||
@@ -127,6 +135,7 @@ class CliInput extends Wolf3dInput {
|
|||||||
// Reset the pending buffer so each keypress behaves like a frame impulse.
|
// Reset the pending buffer so each keypress behaves like a frame impulse.
|
||||||
_pForward = _pBackward = _pLeft = _pRight = _pFire = _pInteract = false;
|
_pForward = _pBackward = _pLeft = _pRight = _pFire = _pInteract = false;
|
||||||
_pBack = false;
|
_pBack = false;
|
||||||
|
_pMapToggle = false;
|
||||||
_pWeapon = null;
|
_pWeapon = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ abstract class Wolf3dInput {
|
|||||||
bool isMovingBackward = false;
|
bool isMovingBackward = false;
|
||||||
bool isTurningLeft = false;
|
bool isTurningLeft = false;
|
||||||
bool isTurningRight = false;
|
bool isTurningRight = false;
|
||||||
|
bool isMapToggle = false;
|
||||||
bool isInteracting = false;
|
bool isInteracting = false;
|
||||||
bool isBack = false;
|
bool isBack = false;
|
||||||
double? menuTapX;
|
double? menuTapX;
|
||||||
@@ -23,6 +24,7 @@ abstract class Wolf3dInput {
|
|||||||
isMovingBackward: isMovingBackward,
|
isMovingBackward: isMovingBackward,
|
||||||
isTurningLeft: isTurningLeft,
|
isTurningLeft: isTurningLeft,
|
||||||
isTurningRight: isTurningRight,
|
isTurningRight: isTurningRight,
|
||||||
|
isMapToggle: isMapToggle,
|
||||||
isFiring: isFiring,
|
isFiring: isFiring,
|
||||||
isInteracting: isInteracting,
|
isInteracting: isInteracting,
|
||||||
isBack: isBack,
|
isBack: isBack,
|
||||||
@@ -37,6 +39,7 @@ enum WolfInputAction {
|
|||||||
backward,
|
backward,
|
||||||
turnLeft,
|
turnLeft,
|
||||||
turnRight,
|
turnRight,
|
||||||
|
mapToggle,
|
||||||
fire,
|
fire,
|
||||||
interact,
|
interact,
|
||||||
back,
|
back,
|
||||||
|
|||||||
@@ -406,6 +406,93 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
_writeString(1, 0, ' ${fpsLabel(engine)} ', textColor, bgColor);
|
_writeString(1, 0, ' ${fpsLabel(engine)} ', textColor, bgColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawGameplayOverlay(WolfEngine engine) {
|
||||||
|
if (!engine.isMapOverlayVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int mapBgColor = ColorPalette.vga32Bit[0];
|
||||||
|
final int floorColor = ColorPalette.vga32Bit[8];
|
||||||
|
final int wallColor = ColorPalette.vga32Bit[7];
|
||||||
|
final int doorColor = ColorPalette.vga32Bit[14];
|
||||||
|
final int playerColor = ColorPalette.vga32Bit[10];
|
||||||
|
final int facingColor = ColorPalette.vga32Bit[12];
|
||||||
|
|
||||||
|
if (_usesTerminalLayout) {
|
||||||
|
_fillTerminalRect(
|
||||||
|
projectionOffsetX,
|
||||||
|
0,
|
||||||
|
projectionWidth,
|
||||||
|
_terminalPixelHeight,
|
||||||
|
mapBgColor,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_fillRect(0, 0, width, height, activeTheme.solid, mapBgColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int viewportX = _usesTerminalLayout ? projectionOffsetX : 0;
|
||||||
|
final int viewportWidth = _usesTerminalLayout ? projectionWidth : width;
|
||||||
|
final int viewportHeight = _usesTerminalLayout
|
||||||
|
? _terminalPixelHeight
|
||||||
|
: height;
|
||||||
|
final int viewportPadding = math.max(
|
||||||
|
3,
|
||||||
|
math.min(viewportWidth, viewportHeight) ~/ 24,
|
||||||
|
);
|
||||||
|
final int availableWidth = math.max(
|
||||||
|
1,
|
||||||
|
viewportWidth - (viewportPadding * 2),
|
||||||
|
);
|
||||||
|
final int availableHeight = math.max(
|
||||||
|
1,
|
||||||
|
viewportHeight - (viewportPadding * 2),
|
||||||
|
);
|
||||||
|
final int tileSize = math.max(
|
||||||
|
1,
|
||||||
|
math.min(availableWidth, availableHeight) ~/ 64,
|
||||||
|
);
|
||||||
|
final int mapPixelWidth = tileSize * 64;
|
||||||
|
final int mapPixelHeight = tileSize * 64;
|
||||||
|
final int mapStartX = viewportX + ((viewportWidth - mapPixelWidth) ~/ 2);
|
||||||
|
final int mapStartY = (viewportHeight - mapPixelHeight) ~/ 2;
|
||||||
|
|
||||||
|
for (int y = 0; y < 64; y++) {
|
||||||
|
for (int x = 0; x < 64; x++) {
|
||||||
|
final int tileId = engine.currentLevel[y][x];
|
||||||
|
final int color = tileId == 0
|
||||||
|
? floorColor
|
||||||
|
: (tileId >= 90 ? doorColor : wallColor);
|
||||||
|
_fillMapRect(
|
||||||
|
mapStartX + (x * tileSize),
|
||||||
|
mapStartY + (y * tileSize),
|
||||||
|
tileSize,
|
||||||
|
tileSize,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final int playerX = (mapStartX + (engine.player.x * tileSize)).floor();
|
||||||
|
final int playerY = (mapStartY + (engine.player.y * tileSize)).floor();
|
||||||
|
final int markerRadius = math.max(1, tileSize ~/ 2);
|
||||||
|
final int markerSize = (markerRadius * 2) + 1;
|
||||||
|
_fillMapRect(
|
||||||
|
playerX - markerRadius,
|
||||||
|
playerY - markerRadius,
|
||||||
|
markerSize,
|
||||||
|
markerSize,
|
||||||
|
playerColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
final int facingLength = math.max(4, (tileSize * 3) ~/ 2);
|
||||||
|
final int facingEndX =
|
||||||
|
(playerX + (math.cos(engine.player.angle) * facingLength)).round();
|
||||||
|
final int facingEndY =
|
||||||
|
(playerY + (math.sin(engine.player.angle) * facingLength)).round();
|
||||||
|
_drawMapLine(playerX, playerY, facingEndX, facingEndY, facingColor);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void drawMenu(WolfEngine engine) {
|
void drawMenu(WolfEngine engine) {
|
||||||
final int bgColor = _rgbToPaletteColor(
|
final int bgColor = _rgbToPaletteColor(
|
||||||
@@ -2053,6 +2140,48 @@ class AsciiRenderer extends CliRendererBackend<dynamic> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _fillMapRect(int startX, int startY, int w, int h, int color) {
|
||||||
|
for (int y = startY; y < startY + h; y++) {
|
||||||
|
for (int x = startX; x < startX + w; x++) {
|
||||||
|
_setMapPixel(x, y, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawMapLine(int x0, int y0, int x1, int y1, int color) {
|
||||||
|
final int dx = x1 - x0;
|
||||||
|
final int dy = y1 - y0;
|
||||||
|
final int steps = math.max(dx.abs(), dy.abs());
|
||||||
|
if (steps == 0) {
|
||||||
|
_setMapPixel(x0, y0, color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= steps; i++) {
|
||||||
|
final double t = i / steps;
|
||||||
|
final int x = (x0 + (dx * t)).round();
|
||||||
|
final int y = (y0 + (dy * t)).round();
|
||||||
|
_setMapPixel(x, y, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setMapPixel(int x, int y, int color) {
|
||||||
|
if (_usesTerminalLayout) {
|
||||||
|
if (x < projectionOffsetX || x >= _viewportRightX) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (y < 0 || y >= _terminalPixelHeight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_scenePixels[y][x] = color;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x < 0 || x >= width || y < 0 || y >= height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_screen[y][x] = ColoredChar(activeTheme.solid, color);
|
||||||
|
}
|
||||||
|
|
||||||
// --- DAMAGE FLASH ---
|
// --- DAMAGE FLASH ---
|
||||||
void _applyDamageFlash() {
|
void _applyDamageFlash() {
|
||||||
for (int y = 0; y < viewHeight; y++) {
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ abstract class RendererBackend<T>
|
|||||||
// 3. Draw 2D overlays.
|
// 3. Draw 2D overlays.
|
||||||
drawWeapon(engine);
|
drawWeapon(engine);
|
||||||
drawHud(engine);
|
drawHud(engine);
|
||||||
|
drawGameplayOverlay(engine);
|
||||||
if (engine.showFpsCounter) {
|
if (engine.showFpsCounter) {
|
||||||
drawFpsOverlay(engine);
|
drawFpsOverlay(engine);
|
||||||
}
|
}
|
||||||
@@ -140,6 +141,11 @@ abstract class RendererBackend<T>
|
|||||||
/// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list).
|
/// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list).
|
||||||
T finalizeFrame();
|
T finalizeFrame();
|
||||||
|
|
||||||
|
/// Draws gameplay overlays that should appear above world/HUD content.
|
||||||
|
///
|
||||||
|
/// Default implementation is a no-op.
|
||||||
|
void drawGameplayOverlay(WolfEngine engine) {}
|
||||||
|
|
||||||
/// Draws a non-world menu frame when the engine is awaiting configuration.
|
/// Draws a non-world menu frame when the engine is awaiting configuration.
|
||||||
///
|
///
|
||||||
/// Default implementation is a no-op for backends that don't support menus.
|
/// Default implementation is a no-op for backends that don't support menus.
|
||||||
|
|||||||
@@ -360,6 +360,71 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
_drawMenuText(label, 4, 3, textColor, scale: 1);
|
_drawMenuText(label, 4, 3, textColor, scale: 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawGameplayOverlay(WolfEngine engine) {
|
||||||
|
if (!engine.isMapOverlayVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int mapBgColor = 0;
|
||||||
|
const int floorColor = 8;
|
||||||
|
const int wallColor = 7;
|
||||||
|
const int doorColor = 14;
|
||||||
|
const int playerColor = 10;
|
||||||
|
const int facingColor = 12;
|
||||||
|
|
||||||
|
for (int i = 0; i < _screen.length; i++) {
|
||||||
|
_screen[i] = mapBgColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int viewportPadding = math.max(3, math.min(width, height) ~/ 24);
|
||||||
|
final int availableWidth = math.max(1, width - (viewportPadding * 2));
|
||||||
|
final int availableHeight = math.max(1, height - (viewportPadding * 2));
|
||||||
|
final int tileSize = math.max(
|
||||||
|
1,
|
||||||
|
math.min(availableWidth, availableHeight) ~/ 64,
|
||||||
|
);
|
||||||
|
final int mapPixelWidth = tileSize * 64;
|
||||||
|
final int mapPixelHeight = tileSize * 64;
|
||||||
|
final int mapStartX = (width - mapPixelWidth) ~/ 2;
|
||||||
|
final int mapStartY = (height - mapPixelHeight) ~/ 2;
|
||||||
|
|
||||||
|
for (int y = 0; y < 64; y++) {
|
||||||
|
for (int x = 0; x < 64; x++) {
|
||||||
|
final int tileId = engine.currentLevel[y][x];
|
||||||
|
final int color = tileId == 0
|
||||||
|
? floorColor
|
||||||
|
: (tileId >= 90 ? doorColor : wallColor);
|
||||||
|
_fillMapRect(
|
||||||
|
mapStartX + (x * tileSize),
|
||||||
|
mapStartY + (y * tileSize),
|
||||||
|
tileSize,
|
||||||
|
tileSize,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final int playerX = (mapStartX + (engine.player.x * tileSize)).floor();
|
||||||
|
final int playerY = (mapStartY + (engine.player.y * tileSize)).floor();
|
||||||
|
final int markerRadius = math.max(1, tileSize ~/ 2);
|
||||||
|
final int markerSize = (markerRadius * 2) + 1;
|
||||||
|
_fillMapRect(
|
||||||
|
playerX - markerRadius,
|
||||||
|
playerY - markerRadius,
|
||||||
|
markerSize,
|
||||||
|
markerSize,
|
||||||
|
playerColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
final int facingLength = math.max(4, (tileSize * 3) ~/ 2);
|
||||||
|
final int facingEndX =
|
||||||
|
(playerX + (math.cos(engine.player.angle) * facingLength)).round();
|
||||||
|
final int facingEndY =
|
||||||
|
(playerY + (math.sin(engine.player.angle) * facingLength)).round();
|
||||||
|
_drawMapLine(playerX, playerY, facingEndX, facingEndY, facingColor);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// Blits a VGA image into the Sixel index buffer HUD space (320x200).
|
/// Blits a VGA image into the Sixel index buffer HUD space (320x200).
|
||||||
void blitHudVgaImage(VgaImage image, int startX320, int startY200) {
|
void blitHudVgaImage(VgaImage image, int startX320, int startY200) {
|
||||||
@@ -1392,6 +1457,37 @@ class SixelRenderer extends CliRendererBackend<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _fillMapRect(int startX, int startY, int w, int h, int colorIndex) {
|
||||||
|
for (int y = startY; y < startY + h; y++) {
|
||||||
|
for (int x = startX; x < startX + w; x++) {
|
||||||
|
_setMapPixel(x, y, colorIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawMapLine(int x0, int y0, int x1, int y1, int colorIndex) {
|
||||||
|
final int dx = x1 - x0;
|
||||||
|
final int dy = y1 - y0;
|
||||||
|
final int steps = math.max(dx.abs(), dy.abs());
|
||||||
|
if (steps == 0) {
|
||||||
|
_setMapPixel(x0, y0, colorIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= steps; i++) {
|
||||||
|
final double t = i / steps;
|
||||||
|
final int x = (x0 + (dx * t)).round();
|
||||||
|
final int y = (y0 + (dy * t)).round();
|
||||||
|
_setMapPixel(x, y, colorIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setMapPixel(int x, int y, int colorIndex) {
|
||||||
|
if (x < 0 || x >= width || y < 0 || y >= height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_screen[(y * width) + x] = colorIndex;
|
||||||
|
}
|
||||||
|
|
||||||
/// Maps an RGB color to the nearest VGA palette index.
|
/// Maps an RGB color to the nearest VGA palette index.
|
||||||
int _rgbToPaletteIndex(int rgb) {
|
int _rgbToPaletteIndex(int rgb) {
|
||||||
return ColorPalette.findClosestPaletteIndex(rgb);
|
return ColorPalette.findClosestPaletteIndex(rgb);
|
||||||
|
|||||||
@@ -140,6 +140,69 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
_drawMenuText(fpsLabel(engine), panelX + 3, panelY + 1, textColor);
|
_drawMenuText(fpsLabel(engine), panelX + 3, panelY + 1, textColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void drawGameplayOverlay(WolfEngine engine) {
|
||||||
|
if (!engine.isMapOverlayVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int mapBgColor = ColorPalette.vga32Bit[0];
|
||||||
|
final int floorColor = ColorPalette.vga32Bit[8];
|
||||||
|
final int wallColor = ColorPalette.vga32Bit[7];
|
||||||
|
final int doorColor = ColorPalette.vga32Bit[14];
|
||||||
|
final int playerColor = ColorPalette.vga32Bit[10];
|
||||||
|
final int facingColor = ColorPalette.vga32Bit[12];
|
||||||
|
|
||||||
|
_fillMenuPanel(0, 0, width, height, mapBgColor);
|
||||||
|
|
||||||
|
final int viewportPadding = math.max(6, math.min(width, height) ~/ 24);
|
||||||
|
final int availableWidth = math.max(1, width - (viewportPadding * 2));
|
||||||
|
final int availableHeight = math.max(1, height - (viewportPadding * 2));
|
||||||
|
final int tileSize = math.max(
|
||||||
|
1,
|
||||||
|
math.min(availableWidth, availableHeight) ~/ 64,
|
||||||
|
);
|
||||||
|
final int mapPixelWidth = tileSize * 64;
|
||||||
|
final int mapPixelHeight = tileSize * 64;
|
||||||
|
final int mapStartX = (width - mapPixelWidth) ~/ 2;
|
||||||
|
final int mapStartY = (height - mapPixelHeight) ~/ 2;
|
||||||
|
|
||||||
|
for (int y = 0; y < 64; y++) {
|
||||||
|
for (int x = 0; x < 64; x++) {
|
||||||
|
final int tileId = engine.currentLevel[y][x];
|
||||||
|
final int color = tileId == 0
|
||||||
|
? floorColor
|
||||||
|
: (tileId >= 90 ? doorColor : wallColor);
|
||||||
|
_fillMenuPanel(
|
||||||
|
mapStartX + (x * tileSize),
|
||||||
|
mapStartY + (y * tileSize),
|
||||||
|
tileSize,
|
||||||
|
tileSize,
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final int playerX = (mapStartX + (engine.player.x * tileSize)).floor();
|
||||||
|
final int playerY = (mapStartY + (engine.player.y * tileSize)).floor();
|
||||||
|
final int markerRadius = math.max(1, tileSize ~/ 2);
|
||||||
|
final int markerSize = (markerRadius * 2) + 1;
|
||||||
|
_fillMenuPanel(
|
||||||
|
playerX - markerRadius,
|
||||||
|
playerY - markerRadius,
|
||||||
|
markerSize,
|
||||||
|
markerSize,
|
||||||
|
playerColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
final int facingLength = math.max(4, (tileSize * 3) ~/ 2);
|
||||||
|
final int facingEndX =
|
||||||
|
(playerX + (math.cos(engine.player.angle) * facingLength)).round();
|
||||||
|
final int facingEndY =
|
||||||
|
(playerY + (math.sin(engine.player.angle) * facingLength)).round();
|
||||||
|
_drawMapLine(playerX, playerY, facingEndX, facingEndY, facingColor);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// Blits a VGA image into the software framebuffer HUD space (320x200).
|
/// Blits a VGA image into the software framebuffer HUD space (320x200).
|
||||||
void blitHudVgaImage(VgaImage image, int startX320, int startY200) {
|
void blitHudVgaImage(VgaImage image, int startX320, int startY200) {
|
||||||
@@ -925,6 +988,29 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _drawMapLine(int x0, int y0, int x1, int y1, int color) {
|
||||||
|
final int dx = x1 - x0;
|
||||||
|
final int dy = y1 - y0;
|
||||||
|
final int steps = math.max(dx.abs(), dy.abs());
|
||||||
|
if (steps == 0) {
|
||||||
|
_setMapPixel(x0, y0, color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= steps; i++) {
|
||||||
|
final double t = i / steps;
|
||||||
|
final int x = (x0 + (dx * t)).round();
|
||||||
|
final int y = (y0 + (dy * t)).round();
|
||||||
|
_setMapPixel(x, y, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setMapPixel(int x, int y, int color) {
|
||||||
|
if (x < 0 || x >= width || y < 0 || y >= height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_buffer.pixels[(y * width) + x] = color;
|
||||||
|
}
|
||||||
|
|
||||||
String _gameTitle(GameVersion version) {
|
String _gameTitle(GameVersion version) {
|
||||||
switch (version) {
|
switch (version) {
|
||||||
case GameVersion.shareware:
|
case GameVersion.shareware:
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Map overlay toggle', () {
|
||||||
|
test('toggles in gameplay on map toggle pulse', () {
|
||||||
|
final input = _PulseInput();
|
||||||
|
final engine = _buildEngine(input: input, difficulty: Difficulty.medium);
|
||||||
|
|
||||||
|
engine.init();
|
||||||
|
expect(engine.isMapOverlayVisible, isFalse);
|
||||||
|
|
||||||
|
input.queueMapToggle = true;
|
||||||
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
|
expect(engine.isMapOverlayVisible, isTrue);
|
||||||
|
|
||||||
|
input.queueMapToggle = true;
|
||||||
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
|
expect(engine.isMapOverlayVisible, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not toggle while menu is open', () {
|
||||||
|
final input = _PulseInput();
|
||||||
|
final engine = _buildEngine(input: input, difficulty: null);
|
||||||
|
|
||||||
|
engine.init();
|
||||||
|
expect(engine.isMenuOpen, isTrue);
|
||||||
|
expect(engine.isMapOverlayVisible, isFalse);
|
||||||
|
|
||||||
|
input.queueMapToggle = true;
|
||||||
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
|
expect(engine.isMapOverlayVisible, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clears map overlay when pause menu opens', () {
|
||||||
|
final input = _PulseInput();
|
||||||
|
final engine = _buildEngine(input: input, difficulty: Difficulty.medium);
|
||||||
|
|
||||||
|
engine.init();
|
||||||
|
|
||||||
|
input.queueMapToggle = true;
|
||||||
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
|
expect(engine.isMapOverlayVisible, isTrue);
|
||||||
|
|
||||||
|
input.queueBack = true;
|
||||||
|
engine.tick(const Duration(milliseconds: 16));
|
||||||
|
expect(engine.isMenuOpen, isTrue);
|
||||||
|
expect(engine.isMapOverlayVisible, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PulseInput extends Wolf3dInput {
|
||||||
|
bool queueMapToggle = false;
|
||||||
|
bool queueBack = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update() {
|
||||||
|
isMapToggle = queueMapToggle;
|
||||||
|
isBack = queueBack;
|
||||||
|
|
||||||
|
queueMapToggle = false;
|
||||||
|
queueBack = false;
|
||||||
|
|
||||||
|
isMovingForward = false;
|
||||||
|
isMovingBackward = false;
|
||||||
|
isTurningLeft = false;
|
||||||
|
isTurningRight = false;
|
||||||
|
isInteracting = false;
|
||||||
|
isFiring = false;
|
||||||
|
requestedWeapon = null;
|
||||||
|
menuTapX = null;
|
||||||
|
menuTapY = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WolfEngine _buildEngine({
|
||||||
|
required Wolf3dInput input,
|
||||||
|
required Difficulty? difficulty,
|
||||||
|
}) {
|
||||||
|
final wallGrid = _buildGrid();
|
||||||
|
final objectGrid = _buildGrid();
|
||||||
|
_fillBoundaries(wallGrid, 2);
|
||||||
|
objectGrid[2][2] = MapObject.playerEast;
|
||||||
|
|
||||||
|
return WolfEngine(
|
||||||
|
data: WolfensteinData(
|
||||||
|
version: GameVersion.shareware,
|
||||||
|
dataVersion: DataVersion.unknown,
|
||||||
|
registry: RetailAssetRegistry(),
|
||||||
|
walls: [
|
||||||
|
_solidSprite(1),
|
||||||
|
_solidSprite(1),
|
||||||
|
_solidSprite(2),
|
||||||
|
_solidSprite(2),
|
||||||
|
],
|
||||||
|
sprites: List.generate(436, (_) => _solidSprite(255)),
|
||||||
|
sounds: const [],
|
||||||
|
adLibSounds: const [],
|
||||||
|
music: const [],
|
||||||
|
vgaImages: const [],
|
||||||
|
episodes: [
|
||||||
|
Episode(
|
||||||
|
name: 'Episode 1',
|
||||||
|
levels: [
|
||||||
|
WolfLevel(
|
||||||
|
name: 'Level 1',
|
||||||
|
wallGrid: wallGrid,
|
||||||
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
|
objectGrid: objectGrid,
|
||||||
|
music: Music.level01,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
difficulty: difficulty,
|
||||||
|
startingEpisode: 0,
|
||||||
|
frameBuffer: FrameBuffer(64, 64),
|
||||||
|
input: input,
|
||||||
|
onGameWon: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpriteMap _buildGrid() => List.generate(64, (_) => List.filled(64, 0));
|
||||||
|
|
||||||
|
void _fillBoundaries(SpriteMap grid, int wallId) {
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
grid[0][i] = wallId;
|
||||||
|
grid[63][i] = wallId;
|
||||||
|
grid[i][0] = wallId;
|
||||||
|
grid[i][63] = wallId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Sprite _solidSprite(int colorIndex) {
|
||||||
|
return Sprite(Uint8List.fromList(List.filled(64 * 64, colorIndex)));
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_input.dart';
|
||||||
|
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Map overlay rendering', () {
|
||||||
|
test('software renderer draws fullscreen map overlay when enabled', () {
|
||||||
|
final engine = _buildEngine();
|
||||||
|
engine.init();
|
||||||
|
|
||||||
|
final renderer = SoftwareRenderer();
|
||||||
|
final frameNormal = renderer.render(engine);
|
||||||
|
final List<int> normalPixels = List<int>.from(frameNormal.pixels);
|
||||||
|
|
||||||
|
engine.isMapOverlayVisible = true;
|
||||||
|
final frameMap = renderer.render(engine);
|
||||||
|
final List<int> mapPixels = List<int>.from(frameMap.pixels);
|
||||||
|
|
||||||
|
int changedPixels = 0;
|
||||||
|
for (int i = 0; i < mapPixels.length; i++) {
|
||||||
|
if (normalPixels[i] != mapPixels[i]) {
|
||||||
|
changedPixels++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(changedPixels, greaterThan(mapPixels.length ~/ 5));
|
||||||
|
expect(mapPixels.contains(ColorPalette.vga32Bit[10]), isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StaticInput extends Wolf3dInput {
|
||||||
|
@override
|
||||||
|
void update() {
|
||||||
|
isMovingForward = false;
|
||||||
|
isMovingBackward = false;
|
||||||
|
isTurningLeft = false;
|
||||||
|
isTurningRight = false;
|
||||||
|
isInteracting = false;
|
||||||
|
isBack = false;
|
||||||
|
isMapToggle = false;
|
||||||
|
isFiring = false;
|
||||||
|
requestedWeapon = null;
|
||||||
|
menuTapX = null;
|
||||||
|
menuTapY = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WolfEngine _buildEngine() {
|
||||||
|
final wallGrid = _buildGrid();
|
||||||
|
final objectGrid = _buildGrid();
|
||||||
|
|
||||||
|
_fillBoundaries(wallGrid, 2);
|
||||||
|
wallGrid[2][4] = 1;
|
||||||
|
wallGrid[2][5] = 90;
|
||||||
|
objectGrid[2][2] = MapObject.playerEast;
|
||||||
|
|
||||||
|
return WolfEngine(
|
||||||
|
data: WolfensteinData(
|
||||||
|
version: GameVersion.shareware,
|
||||||
|
dataVersion: DataVersion.unknown,
|
||||||
|
registry: RetailAssetRegistry(),
|
||||||
|
walls: [
|
||||||
|
_solidSprite(1),
|
||||||
|
_solidSprite(1),
|
||||||
|
_solidSprite(2),
|
||||||
|
_solidSprite(2),
|
||||||
|
],
|
||||||
|
sprites: List.generate(436, (_) => _solidSprite(255)),
|
||||||
|
sounds: const [],
|
||||||
|
adLibSounds: const [],
|
||||||
|
music: const [],
|
||||||
|
vgaImages: const [],
|
||||||
|
episodes: [
|
||||||
|
Episode(
|
||||||
|
name: 'Episode 1',
|
||||||
|
levels: [
|
||||||
|
WolfLevel(
|
||||||
|
name: 'Level 1',
|
||||||
|
wallGrid: wallGrid,
|
||||||
|
areaGrid: List.generate(64, (_) => List.filled(64, -1)),
|
||||||
|
objectGrid: objectGrid,
|
||||||
|
music: Music.level01,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
difficulty: Difficulty.medium,
|
||||||
|
startingEpisode: 0,
|
||||||
|
frameBuffer: FrameBuffer(96, 96),
|
||||||
|
input: _StaticInput(),
|
||||||
|
onGameWon: () {},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpriteMap _buildGrid() => List.generate(64, (_) => List.filled(64, 0));
|
||||||
|
|
||||||
|
void _fillBoundaries(SpriteMap grid, int wallId) {
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
grid[0][i] = wallId;
|
||||||
|
grid[63][i] = wallId;
|
||||||
|
grid[i][0] = wallId;
|
||||||
|
grid[i][63] = wallId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Sprite _solidSprite(int colorIndex) {
|
||||||
|
return Sprite(Uint8List.fromList(List.filled(64 * 64, colorIndex)));
|
||||||
|
}
|
||||||
@@ -70,6 +70,7 @@ class Wolf3dFlutterInput extends Wolf3dInput {
|
|||||||
LogicalKeyboardKey.keyD,
|
LogicalKeyboardKey.keyD,
|
||||||
LogicalKeyboardKey.arrowRight,
|
LogicalKeyboardKey.arrowRight,
|
||||||
},
|
},
|
||||||
|
WolfInputAction.mapToggle: {LogicalKeyboardKey.tab},
|
||||||
WolfInputAction.fire: {
|
WolfInputAction.fire: {
|
||||||
LogicalKeyboardKey.controlLeft,
|
LogicalKeyboardKey.controlLeft,
|
||||||
LogicalKeyboardKey.controlRight,
|
LogicalKeyboardKey.controlRight,
|
||||||
@@ -209,6 +210,7 @@ class Wolf3dFlutterInput extends Wolf3dInput {
|
|||||||
kbBackward || (_isMouseLookEnabled && _mouseDeltaY > 1.5);
|
kbBackward || (_isMouseLookEnabled && _mouseDeltaY > 1.5);
|
||||||
isTurningLeft = kbLeft || (_isMouseLookEnabled && _mouseDeltaX < -1.5);
|
isTurningLeft = kbLeft || (_isMouseLookEnabled && _mouseDeltaX < -1.5);
|
||||||
isTurningRight = kbRight || (_isMouseLookEnabled && _mouseDeltaX > 1.5);
|
isTurningRight = kbRight || (_isMouseLookEnabled && _mouseDeltaX > 1.5);
|
||||||
|
isMapToggle = _isNewlyPressed(WolfInputAction.mapToggle, newlyPressedKeys);
|
||||||
|
|
||||||
// Deltas are one-frame impulses, so consume them immediately after use.
|
// Deltas are one-frame impulses, so consume them immediately after use.
|
||||||
_mouseDeltaX = 0.0;
|
_mouseDeltaX = 0.0;
|
||||||
|
|||||||
Reference in New Issue
Block a user