From 786ba4b450e7196d9f34731b168c5deaccfa6ee1 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Thu, 19 Mar 2026 11:38:07 +0100 Subject: [PATCH] Refactor rendering architecture and replace rasterizer with renderer - Introduced SoftwareRenderer as a pixel-accurate software rendering backend. - Removed the obsolete wolf_3d_rasterizer.dart file. - Created a new wolf_3d_renderer.dart file to centralize rendering exports. - Updated tests to accommodate the new rendering structure, including pushwall and projection sampling tests. - Modified the WolfAsciiRenderer and WolfFlutterRenderer to utilize the new SoftwareRenderer. - Enhanced enemy spawn tests to include new enemy states. Signed-off-by: Hans Kokx --- apps/wolf_3d_cli/lib/cli_game_loop.dart | 38 +- .../lib/src/data_types/image.dart | 2 +- .../lib/src/engine/managers/door_manager.dart | 27 +- .../src/entities/entities/enemies/enemy.dart | 4 +- .../lib/src/rasterizer/rasterizer.dart | 605 ------------------ .../lib/src/raycasting/projection.dart | 33 + .../lib/src/raycasting/raycaster.dart | 419 ++++++++++++ .../ascii_renderer.dart} | 18 +- .../cli_renderer_backend.dart} | 6 +- .../{rasterizer => rendering}/menu_font.dart | 0 .../lib/src/rendering/renderer_backend.dart | 231 +++++++ .../sixel_renderer.dart} | 10 +- .../software_renderer.dart} | 8 +- .../wolf_3d_dart/lib/wolf_3d_rasterizer.dart | 8 - .../wolf_3d_dart/lib/wolf_3d_renderer.dart | 10 + .../test/entities/enemy_spawn_test.dart | 14 + .../rasterizer/projection_sampling_test.dart | 4 +- .../rasterizer/pushwall_rasterizer_test.dart | 4 +- .../rendering/projection_sampling_test.dart | 85 +++ .../rendering/pushwall_renderer_test.dart | 90 +++ .../lib/wolf_3d_ascii_renderer.dart | 12 +- .../lib/wolf_3d_flutter_renderer.dart | 8 +- 22 files changed, 952 insertions(+), 684 deletions(-) delete mode 100644 packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart create mode 100644 packages/wolf_3d_dart/lib/src/raycasting/projection.dart create mode 100644 packages/wolf_3d_dart/lib/src/raycasting/raycaster.dart rename packages/wolf_3d_dart/lib/src/{rasterizer/ascii_rasterizer.dart => rendering/ascii_renderer.dart} (98%) rename packages/wolf_3d_dart/lib/src/{rasterizer/cli_rasterizer.dart => rendering/cli_renderer_backend.dart} (88%) rename packages/wolf_3d_dart/lib/src/{rasterizer => rendering}/menu_font.dart (100%) create mode 100644 packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart rename packages/wolf_3d_dart/lib/src/{rasterizer/sixel_rasterizer.dart => rendering/sixel_renderer.dart} (98%) rename packages/wolf_3d_dart/lib/src/{rasterizer/software_rasterizer.dart => rendering/software_renderer.dart} (98%) delete mode 100644 packages/wolf_3d_dart/lib/wolf_3d_rasterizer.dart create mode 100644 packages/wolf_3d_dart/lib/wolf_3d_renderer.dart create mode 100644 packages/wolf_3d_dart/test/rendering/projection_sampling_test.dart create mode 100644 packages/wolf_3d_dart/test/rendering/pushwall_renderer_test.dart diff --git a/apps/wolf_3d_cli/lib/cli_game_loop.dart b/apps/wolf_3d_cli/lib/cli_game_loop.dart index 3385caf..bb8b733 100644 --- a/apps/wolf_3d_cli/lib/cli_game_loop.dart +++ b/apps/wolf_3d_cli/lib/cli_game_loop.dart @@ -6,9 +6,9 @@ import 'dart:io'; 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_rasterizer.dart'; +import 'package:wolf_3d_dart/wolf_3d_renderer.dart'; -/// Runs the Wolf3D engine inside a terminal using CLI-specific rasterizers. +/// Runs the Wolf3D engine inside a terminal using CLI-specific renderers. /// /// The loop owns raw-stdin handling, renderer switching, terminal size checks, /// and frame pacing. It expects [engine.input] to be a [CliInput] instance so @@ -25,22 +25,22 @@ class CliGameLoop { 'CliGameLoop requires a CliInput instance.', ), - primaryRasterizer = AsciiRasterizer( - mode: AsciiRasterizerMode.terminalAnsi, + primaryRenderer = AsciiRenderer( + mode: AsciiRendererMode.terminalAnsi, ), - secondaryRasterizer = SixelRasterizer() { - _rasterizer = primaryRasterizer; + secondaryRenderer = SixelRenderer() { + _renderer = primaryRenderer; } final WolfEngine engine; - final CliRasterizer primaryRasterizer; - final CliRasterizer secondaryRasterizer; + final CliRendererBackend primaryRenderer; + final CliRendererBackend secondaryRenderer; final CliInput input; final void Function(int code) onExit; final Stopwatch _stopwatch = Stopwatch(); final Stream> _stdinStream = stdin.asBroadcastStream(); - late CliRasterizer _rasterizer; + late CliRendererBackend _renderer; StreamSubscription>? _stdinSubscription; Timer? _timer; bool _isRunning = false; @@ -52,9 +52,9 @@ class CliGameLoop { return; } - if (primaryRasterizer is SixelRasterizer) { - final sixel = primaryRasterizer as SixelRasterizer; - sixel.isSixelSupported = await SixelRasterizer.checkTerminalSixelSupport( + if (primaryRenderer is SixelRenderer) { + final sixel = primaryRenderer as SixelRenderer; + sixel.isSixelSupported = await SixelRenderer.checkTerminalSixelSupport( inputStream: _stdinStream, ); } @@ -116,11 +116,11 @@ class CliGameLoop { } if (bytes.contains(9)) { - // Tab swaps between rasterizers so renderer debugging stays available + // Tab swaps between renderers so renderer debugging stays available // without restarting the process. - _rasterizer = identical(_rasterizer, secondaryRasterizer) - ? primaryRasterizer - : secondaryRasterizer; + _renderer = identical(_renderer, secondaryRenderer) + ? primaryRenderer + : secondaryRenderer; stdout.write('\x1b[2J\x1b[H'); return; } @@ -136,7 +136,7 @@ class CliGameLoop { if (stdout.hasTerminal) { final int cols = stdout.terminalColumns; final int rows = stdout.terminalLines; - if (!_rasterizer.prepareTerminalFrame( + if (!_renderer.prepareTerminalFrame( engine, columns: cols, rows: rows, @@ -145,7 +145,7 @@ class CliGameLoop { // game does not keep advancing while the user resizes the terminal. stdout.write('\x1b[2J\x1b[H'); stdout.write( - _rasterizer.buildTerminalSizeWarning(columns: cols, rows: rows), + _renderer.buildTerminalSizeWarning(columns: cols, rows: rows), ); _lastTick = _stopwatch.elapsed; @@ -160,6 +160,6 @@ class CliGameLoop { stdout.write('\x1b[H'); engine.tick(elapsed); - stdout.write(_rasterizer.render(engine)); + stdout.write(_renderer.render(engine)); } } diff --git a/packages/wolf_3d_dart/lib/src/data_types/image.dart b/packages/wolf_3d_dart/lib/src/data_types/image.dart index 8388cfd..0425ed7 100644 --- a/packages/wolf_3d_dart/lib/src/data_types/image.dart +++ b/packages/wolf_3d_dart/lib/src/data_types/image.dart @@ -29,7 +29,7 @@ class VgaImage { /// /// Callers are expected to provide coordinates already clamped to the image /// bounds. The Wolf3D VGA format stores image bytes in 4 interleaved planes; - /// this helper centralizes that mapping so rasterizers do not duplicate it. + /// this helper centralizes that mapping so renderers do not duplicate it. int decodePixel(int srcX, int srcY) { final int planeWidth = width ~/ 4; final int planeSize = planeWidth * height; diff --git a/packages/wolf_3d_dart/lib/src/engine/managers/door_manager.dart b/packages/wolf_3d_dart/lib/src/engine/managers/door_manager.dart index d2bdf48..11d3242 100644 --- a/packages/wolf_3d_dart/lib/src/engine/managers/door_manager.dart +++ b/packages/wolf_3d_dart/lib/src/engine/managers/door_manager.dart @@ -9,13 +9,15 @@ import 'package:wolf_3d_dart/wolf_3d_entities.dart'; /// door's state changes (e.g., starts closing), the appropriate global /// game events (like sound effects) are triggered. class DoorManager { - /// A lookup table for doors, keyed by their grid coordinates: "$x,$y". - final Map doors = {}; + /// A lookup table for doors, keyed by packed grid coordinates. + final Map doors = {}; /// Callback used to trigger sound effects without tight coupling /// to a specific audio engine implementation. final void Function(int sfxId) onPlaySound; + static int _key(int x, int y) => ((y & 0xFFFF) << 16) | (x & 0xFFFF); + DoorManager({required this.onPlaySound}); /// Scans the [wallGrid] for tile IDs >= 90 and initializes [Door] instances. @@ -25,7 +27,7 @@ class DoorManager { for (int x = 0; x < wallGrid[y].length; x++) { int id = wallGrid[y][x]; if (id >= 90) { - doors['$x,$y'] = Door(x: x, y: y, mapId: id); + doors[_key(x, y)] = Door(x: x, y: y, mapId: id); } } } @@ -48,7 +50,7 @@ class DoorManager { int targetX = (playerX + math.cos(playerAngle)).toInt(); int targetY = (playerY + math.sin(playerAngle)).toInt(); - String key = '$targetX,$targetY'; + final int key = _key(targetX, targetY); if (doors.containsKey(key)) { if (doors[key]!.interact()) { onPlaySound(WolfSound.openDoor); @@ -58,7 +60,7 @@ class DoorManager { /// Attempted by AI entities to open a door blocking their path. void tryOpenDoor(int x, int y) { - String key = '$x,$y'; + final int key = _key(x, y); // AI only interacts if the door is currently fully closed (offset == 0). if (doors.containsKey(key) && doors[key]!.offset == 0.0) { if (doors[key]!.interact()) { @@ -67,21 +69,16 @@ class DoorManager { } } - // Helper method for the raycaster - Map getOffsetsForRenderer() { - Map offsets = {}; - for (var entry in doors.entries) { - if (entry.value.offset > 0.0) { - offsets[entry.key] = entry.value.offset; - } - } - return offsets; + /// Returns the current open offset for the door at [x],[y]. + /// Returns 0.0 when there is no active/opening door at that tile. + double doorOffsetAt(int x, int y) { + return doors[_key(x, y)]?.offset ?? 0.0; } /// Returns true if the door at [x], [y] is sufficiently open for /// an entity (player or enemy) to walk through. bool isDoorOpenEnough(int x, int y) { - String key = '$x,$y'; + final int key = _key(x, y); if (doors.containsKey(key)) { // 0.7 (70% open) is the standard collision threshold. return doors[key]!.offset > 0.7; diff --git a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart index c7caa41..416ee0e 100644 --- a/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart +++ b/packages/wolf_3d_dart/lib/src/entities/entities/enemies/enemy.dart @@ -387,7 +387,9 @@ abstract class Enemy extends Entity { if (matchedType.mapData.isPatrol(normalizedId)) { spawnState = EntityState.patrolling; } else if (matchedType.mapData.isStatic(normalizedId)) { - spawnState = EntityState.idle; + // Standing map placements are directional ambush actors in Wolf3D. + // Using ambushing keeps wake-up behavior aligned with placed facing. + spawnState = EntityState.ambushing; } else { return null; } diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart b/packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart deleted file mode 100644 index 0f8bf40..0000000 --- a/packages/wolf_3d_dart/lib/src/rasterizer/rasterizer.dart +++ /dev/null @@ -1,605 +0,0 @@ -import 'dart:math' as math; - -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_entities.dart'; - -/// Shared rendering pipeline and math utilities for all Wolf3D rasterizers. -/// -/// Subclasses implement draw primitives for their output target (software -/// framebuffer, ANSI text, Sixel, etc), while this base class coordinates -/// raycasting, sprite projection, and common HUD calculations. -abstract class Rasterizer { - late List zBuffer; - late int width; - late int height; - late int viewHeight; - - /// The current engine instance; set at the start of every [render] call. - late WolfEngine engine; - - /// A multiplier to adjust the width of sprites. - /// Pixel renderers usually keep this at 1.0. - /// ASCII renderers can override this (e.g., 0.6) to account for tall characters. - double get aspectMultiplier => 1.0; - - /// A multiplier to counteract tall pixel formats (like 1:2 terminal fonts). - /// Defaults to 1.0 (no squish) for standard pixel rendering. - double get verticalStretch => 1.0; - - /// The logical width of the projection area used for raycasting and sprites. - /// Most renderers use the full buffer width. - int get projectionWidth => width; - - /// Horizontal offset of the projection area within the output buffer. - int get projectionOffsetX => 0; - - /// The logical height of the 3D projection before a renderer maps rows to output pixels. - /// Most renderers use the visible view height. Terminal ASCII can override this to render - /// more vertical detail and collapse it into half-block glyphs. - int get projectionViewHeight => viewHeight; - - /// Whether the current terminal dimensions are supported by this renderer. - /// Default renderers accept all sizes. - bool isTerminalSizeSupported(int columns, int rows) => true; - - /// Human-readable requirement text used by the host app when size checks fail. - String get terminalSizeRequirement => 'Please resize your terminal window.'; - - /// The main entry point called by the game loop. - /// Orchestrates the mathematical rendering pipeline. - T render(WolfEngine engine) { - this.engine = engine; - width = engine.frameBuffer.width; - height = engine.frameBuffer.height; - // The 3D view typically takes up the top 80% of the screen - viewHeight = (height * 0.8).toInt(); - zBuffer = List.filled(projectionWidth, 0.0); - - // 1. Setup the frame (clear screen, draw floor/ceiling) - prepareFrame(engine); - - if (engine.difficulty == null) { - drawMenu(engine); - return finalizeFrame(); - } - - // 2. Do the heavy math for Raycasting Walls - _castWalls(engine); - - // 3. Do the heavy math for Projecting Sprites - _castSprites(engine); - - // 4. Draw 2D Overlays - drawWeapon(engine); - drawHud(engine); - - // 5. Finalize and return the frame data (Buffer or String/List) - return finalizeFrame(); - } - - // =========================================================================== - // ABSTRACT METHODS (Implemented by the child renderers) - // =========================================================================== - - /// Initialize buffers, clear the screen, and draw the floor/ceiling. - void prepareFrame(WolfEngine engine); - - /// Draw a single vertical column of a wall. - void drawWallColumn( - int x, - int drawStart, - int drawEnd, - int columnHeight, - Sprite texture, - int texX, - double perpWallDist, - int side, - ); - - /// Draw a single vertical stripe of a sprite (enemy/item). - void drawSpriteStripe( - int stripeX, - int drawStartY, - int drawEndY, - int spriteHeight, - Sprite texture, - int texX, - double transformY, - ); - - /// Draw the player's weapon overlay at the bottom of the 3D view. - void drawWeapon(WolfEngine engine); - - /// Draw the 2D status bar at the bottom 20% of the screen. - void drawHud(WolfEngine engine); - - /// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list). - T finalizeFrame(); - - /// Draws a non-world menu frame when the engine is awaiting configuration. - /// - /// Default implementation is a no-op for renderers that don't support menus. - void drawMenu(WolfEngine engine) {} - - /// Plots a VGA image into this renderer's HUD coordinate space. - /// - /// Coordinates are in the original 320x200 HUD space. Renderers that support - /// shared HUD composition should override this. - void blitHudVgaImage(VgaImage image, int startX320, int startY200) {} - - /// Shared Wolf3D VGA HUD sequence used by software/sixel/ASCII-full-HUD. - /// - /// Coordinates are intentionally in original 320x200 HUD space so each - /// renderer can scale/map them consistently via [blitHudVgaImage]. - void drawStandardVgaHud(WolfEngine engine) { - final List vgaImages = engine.data.vgaImages; - final int statusBarIndex = vgaImages.indexWhere( - (img) => img.width == 320 && img.height == 40, - ); - if (statusBarIndex == -1) return; - - blitHudVgaImage(vgaImages[statusBarIndex], 0, 160); - _drawHudNumber(vgaImages, 1, 32, 176); - _drawHudNumber(vgaImages, engine.player.score, 96, 176); - _drawHudNumber(vgaImages, 3, 120, 176); - _drawHudNumber(vgaImages, engine.player.health, 192, 176); - _drawHudNumber(vgaImages, engine.player.ammo, 232, 176); - _drawHudFace(engine, vgaImages); - _drawHudWeaponIcon(engine, vgaImages); - } - - void _drawHudNumber( - List vgaImages, - int value, - int rightAlignX, - int startY, - ) { - // HUD numbers are rendered with fixed-width VGA glyphs (8 px advance). - const int zeroIndex = 96; - final String numStr = value.toString(); - int currentX = rightAlignX - (numStr.length * 8); - - for (int i = 0; i < numStr.length; i++) { - final int digit = int.parse(numStr[i]); - final int imageIndex = zeroIndex + digit; - if (imageIndex < vgaImages.length) { - blitHudVgaImage(vgaImages[imageIndex], currentX, startY); - } - currentX += 8; - } - } - - void _drawHudFace(WolfEngine engine, List vgaImages) { - final int faceIndex = hudFaceVgaIndex(engine.player.health); - if (faceIndex < vgaImages.length) { - blitHudVgaImage(vgaImages[faceIndex], 136, 164); - } - } - - void _drawHudWeaponIcon(WolfEngine engine, List vgaImages) { - final int weaponIndex = hudWeaponVgaIndex(engine); - if (weaponIndex < vgaImages.length) { - blitHudVgaImage(vgaImages[weaponIndex], 256, 164); - } - } - - // =========================================================================== - // SHARED LIGHTING MATH - // =========================================================================== - - /// Calculates depth-based lighting falloff (0.0 to 1.0). - /// While the original Wolf3D didn't use depth fog, this provides a great - /// atmospheric effect for custom renderers (like ASCII dithering). - double calculateDepthBrightness(double distance) { - return (10.0 / (distance + 2.0)).clamp(0.0, 1.0); - } - - // =========================================================================== - // SHARED PROJECTION MATH - // =========================================================================== - - /// Returns the texture Y coordinate for the given screen row inside a wall - /// column. Works for both pixel and terminal renderers. - int wallTexY(int y, int columnHeight) { - // Anchor sampling to the same projection center used when computing - // drawStart/drawEnd. This keeps wall textures stable for renderers that - // use a taller logical projection (for example terminal ASCII mode). - final int projectionCenterY = projectionViewHeight ~/ 2; - final double relativeY = - (y - (-columnHeight ~/ 2 + projectionCenterY)) / columnHeight; - return (relativeY * 64).toInt().clamp(0, 63); - } - - /// Returns the texture Y coordinate for the given screen row inside a sprite - /// stripe. - int spriteTexY(int y, int drawStartY, int spriteHeight) { - final double relativeY = (y - drawStartY) / spriteHeight; - return (relativeY * 64).toInt().clamp(0, 63); - } - - /// Returns the screen-space bounds for the player's weapon overlay. - /// - /// [weaponWidth] and [weaponHeight] are in pixels; [startX]/[startY] are - /// the top-left draw origin. Uses [projectionWidth] so that renderers - /// with a narrower projection area (e.g. ASCII terminal) are handled - /// correctly. - ({int weaponWidth, int weaponHeight, int startX, int startY}) - weaponScreenBounds(WolfEngine engine) { - final int ww = (projectionWidth * 0.5).toInt(); - final int wh = (viewHeight * 0.8).toInt(); - final int sx = projectionOffsetX + (projectionWidth ~/ 2) - (ww ~/ 2); - final int sy = viewHeight - wh + (engine.player.weaponAnimOffset ~/ 4); - return (weaponWidth: ww, weaponHeight: wh, startX: sx, startY: sy); - } - - /// Returns the VGA image index for BJ's face sprite based on player health. - int hudFaceVgaIndex(int health) { - if (health <= 0) return 127; - return 106 + (((100 - health) ~/ 16).clamp(0, 6) * 3); - } - - /// Returns the VGA image index for the current weapon icon in the HUD. - int hudWeaponVgaIndex(WolfEngine engine) { - if (engine.player.hasChainGun) return 91; - if (engine.player.hasMachineGun) return 90; - return 89; - } - - ({double distance, int side, int hitWallId, double wallX})? - _intersectActivePushwall( - Player player, - Coordinate2D rayDir, - Pushwall activePushwall, - ) { - double minX = activePushwall.x.toDouble(); - double maxX = activePushwall.x + 1.0; - double minY = activePushwall.y.toDouble(); - double maxY = activePushwall.y + 1.0; - - if (activePushwall.dirX != 0) { - final double delta = activePushwall.dirX * activePushwall.offset; - minX += delta; - maxX += delta; - } - - if (activePushwall.dirY != 0) { - final double delta = activePushwall.dirY * activePushwall.offset; - minY += delta; - maxY += delta; - } - - const double epsilon = 1e-9; - - double tMinX = double.negativeInfinity; - double tMaxX = double.infinity; - if (rayDir.x.abs() < epsilon) { - if (player.x < minX || player.x > maxX) { - return null; - } - } else { - final double tx1 = (minX - player.x) / rayDir.x; - final double tx2 = (maxX - player.x) / rayDir.x; - tMinX = math.min(tx1, tx2); - tMaxX = math.max(tx1, tx2); - } - - double tMinY = double.negativeInfinity; - double tMaxY = double.infinity; - if (rayDir.y.abs() < epsilon) { - if (player.y < minY || player.y > maxY) { - return null; - } - } else { - final double ty1 = (minY - player.y) / rayDir.y; - final double ty2 = (maxY - player.y) / rayDir.y; - tMinY = math.min(ty1, ty2); - tMaxY = math.max(ty1, ty2); - } - - final double entryDistance = math.max(tMinX, tMinY); - final double exitDistance = math.min(tMaxX, tMaxY); - - if (exitDistance < 0 || entryDistance > exitDistance) { - return null; - } - - final double hitDistance = entryDistance >= 0 - ? entryDistance - : exitDistance; - if (hitDistance < 0) { - return null; - } - - final int side = tMinX > tMinY ? 0 : 1; - final double wallCoord = side == 0 - ? player.y + hitDistance * rayDir.y - : player.x + hitDistance * rayDir.x; - - return ( - distance: hitDistance, - side: side, - hitWallId: activePushwall.mapId, - wallX: wallCoord - wallCoord.floor(), - ); - } - - // =========================================================================== - // CORE ENGINE MATH (Shared across all renderers) - // =========================================================================== - - void _castWalls(WolfEngine engine) { - final Player player = engine.player; - final SpriteMap map = engine.currentLevel; - final List wallTextures = engine.data.walls; - final int sceneWidth = projectionWidth; - final int sceneHeight = projectionViewHeight; - - final Map doorOffsets = engine.doorManager - .getOffsetsForRenderer(); - final Pushwall? activePushwall = engine.pushwallManager.activePushwall; - - final double fov = math.pi / 3; - Coordinate2D dir = Coordinate2D( - math.cos(player.angle), - math.sin(player.angle), - ); - Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2); - - for (int x = 0; x < sceneWidth; x++) { - double cameraX = 2 * x / sceneWidth - 1.0; - Coordinate2D rayDir = dir + (plane * cameraX); - final pushwallHit = activePushwall == null - ? null - : _intersectActivePushwall(player, rayDir, activePushwall); - - int mapX = player.x.toInt(); - int mapY = player.y.toInt(); - - double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs(); - double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs(); - - double sideDistX, sideDistY, perpWallDist = 0.0; - int stepX, stepY, side = 0, hitWallId = 0; - bool hit = false, hitOutOfBounds = false, customDistCalculated = false; - double textureOffset = 0.0; - double? wallXOverride; - Set ignoredDoors = {}; - - if (rayDir.x < 0) { - stepX = -1; - sideDistX = (player.x - mapX) * deltaDistX; - } else { - stepX = 1; - sideDistX = (mapX + 1.0 - player.x) * deltaDistX; - } - if (rayDir.y < 0) { - stepY = -1; - sideDistY = (player.y - mapY) * deltaDistY; - } else { - stepY = 1; - sideDistY = (mapY + 1.0 - player.y) * deltaDistY; - } - - // DDA Loop - while (!hit) { - if (sideDistX < sideDistY) { - sideDistX += deltaDistX; - mapX += stepX; - side = 0; - } else { - sideDistY += deltaDistY; - mapY += stepY; - side = 1; - } - - if (mapY < 0 || - mapY >= map.length || - mapX < 0 || - mapX >= map[0].length) { - hit = true; - hitOutOfBounds = true; - } else if (map[mapY][mapX] > 0) { - if (activePushwall != null && - mapX == activePushwall.x && - mapY == activePushwall.y) { - continue; - } - - String mapKey = '$mapX,$mapY'; - - // DOOR LOGIC - if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) { - double currentOffset = doorOffsets[mapKey] ?? 0.0; - if (currentOffset > 0.0) { - double perpWallDistTemp = (side == 0) - ? (sideDistX - deltaDistX) - : (sideDistY - deltaDistY); - double wallXTemp = (side == 0) - ? player.y + perpWallDistTemp * rayDir.y - : player.x + perpWallDistTemp * rayDir.x; - wallXTemp -= wallXTemp.floor(); - if (wallXTemp < currentOffset) { - ignoredDoors.add(mapKey); - continue; // Ray passes through the open part of the door - } - } - hit = true; - hitWallId = map[mapY][mapX]; - textureOffset = currentOffset; - } else { - hit = true; - hitWallId = map[mapY][mapX]; - } - } - } - - if (hitOutOfBounds || !hit) { - if (pushwallHit == null) { - continue; - } - - customDistCalculated = true; - perpWallDist = pushwallHit.distance; - side = pushwallHit.side; - hitWallId = pushwallHit.hitWallId; - wallXOverride = pushwallHit.wallX; - textureOffset = 0.0; - hit = true; - hitOutOfBounds = false; - } - - if (!customDistCalculated) { - perpWallDist = (side == 0) - ? (sideDistX - deltaDistX) - : (sideDistY - deltaDistY); - } - - if (pushwallHit != null && pushwallHit.distance < perpWallDist) { - customDistCalculated = true; - perpWallDist = pushwallHit.distance; - side = pushwallHit.side; - hitWallId = pushwallHit.hitWallId; - wallXOverride = pushwallHit.wallX; - textureOffset = 0.0; - } - - if (perpWallDist < 0.1) perpWallDist = 0.1; - - // Save for sprite depth checks - zBuffer[x] = perpWallDist; - - // Calculate Texture X Coordinate - double wallX = - wallXOverride ?? - ((side == 0) - ? player.y + perpWallDist * rayDir.y - : player.x + perpWallDist * rayDir.x); - wallX -= wallX.floor(); - - int texNum; - if (hitWallId >= 90) { - texNum = 98.clamp(0, wallTextures.length - 1); - } else { - texNum = ((hitWallId - 1) * 2).clamp(0, wallTextures.length - 2); - if (side == 1) texNum += 1; - } - Sprite texture = wallTextures[texNum]; - - // Texture flipping for specific orientations - int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63); - if (side == 0 && math.cos(player.angle) > 0) texX = 63 - texX; - if (side == 1 && math.sin(player.angle) < 0) texX = 63 - texX; - - // Calculate drawing dimensions - int columnHeight = ((sceneHeight / perpWallDist) * verticalStretch) - .toInt(); - int drawStart = (-columnHeight ~/ 2 + sceneHeight ~/ 2).clamp( - 0, - sceneHeight, - ); - int drawEnd = (columnHeight ~/ 2 + sceneHeight ~/ 2).clamp( - 0, - sceneHeight, - ); - - // Tell the implementation to draw this column - drawWallColumn( - projectionOffsetX + x, - drawStart, - drawEnd, - columnHeight, - texture, - texX, - perpWallDist, - side, - ); - } - } - - void _castSprites(WolfEngine engine) { - final Player player = engine.player; - final List activeSprites = List.from(engine.entities); - final int sceneWidth = projectionWidth; - final int sceneHeight = projectionViewHeight; - - // Sort from furthest to closest (Painter's Algorithm) - activeSprites.sort((a, b) { - double distA = player.position.distanceTo(a.position); - double distB = player.position.distanceTo(b.position); - return distB.compareTo(distA); - }); - - Coordinate2D dir = Coordinate2D( - math.cos(player.angle), - math.sin(player.angle), - ); - Coordinate2D plane = - Coordinate2D(-dir.y, dir.x) * math.tan((math.pi / 3) / 2); - - for (Entity entity in activeSprites) { - Coordinate2D spritePos = entity.position - player.position; - - double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y); - double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y); - double transformY = - invDet * (-plane.y * spritePos.x + plane.x * spritePos.y); - - // Only process if the sprite is in front of the camera - if (transformY > 0) { - int spriteScreenX = ((sceneWidth / 2) * (1 + transformX / transformY)) - .toInt(); - int spriteHeight = ((sceneHeight / transformY).abs() * verticalStretch) - .toInt(); - int displayedSpriteHeight = - ((viewHeight / transformY).abs() * verticalStretch).toInt(); - - // Scale width based on the aspectMultiplier (useful for ASCII) - int spriteWidth = - (displayedSpriteHeight * aspectMultiplier / verticalStretch) - .toInt(); - - int drawStartY = -spriteHeight ~/ 2 + sceneHeight ~/ 2; - int drawEndY = spriteHeight ~/ 2 + sceneHeight ~/ 2; - int drawStartX = -spriteWidth ~/ 2 + spriteScreenX; - int drawEndX = spriteWidth ~/ 2 + spriteScreenX; - - int clipStartX = math.max(0, drawStartX); - int clipEndX = math.min(sceneWidth, drawEndX); - - int safeIndex = entity.spriteIndex.clamp( - 0, - engine.data.sprites.length - 1, - ); - Sprite texture = engine.data.sprites[safeIndex]; - - // Loop through the visible vertical stripes - for (int stripe = clipStartX; stripe < clipEndX; stripe++) { - // Check the Z-Buffer to see if a wall is in front of this stripe - if (transformY < zBuffer[stripe]) { - int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63); - - // Tell the implementation to draw this stripe - drawSpriteStripe( - projectionOffsetX + stripe, - drawStartY, - drawEndY, - spriteHeight, - texture, - texX, - transformY, - ); - } - } - } - } - } - - /// Darkens a 32-bit 0xAABBGGRR color by roughly 30% without touching Alpha - int shadeColor(int color) { - int r = (color & 0xFF) * 7 ~/ 10; - int g = ((color >> 8) & 0xFF) * 7 ~/ 10; - int b = ((color >> 16) & 0xFF) * 7 ~/ 10; - return (0xFF000000) | (b << 16) | (g << 8) | r; - } -} diff --git a/packages/wolf_3d_dart/lib/src/raycasting/projection.dart b/packages/wolf_3d_dart/lib/src/raycasting/projection.dart new file mode 100644 index 0000000..4c8728a --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/raycasting/projection.dart @@ -0,0 +1,33 @@ +import 'package:wolf_3d_dart/wolf_3d_engine.dart'; + +/// Shared projection helpers used by render backends and the raycaster. +mixin ProjectionMath { + int get projectionWidth; + int get projectionOffsetX; + int get projectionViewHeight; + int get viewHeight; + + /// Returns the texture Y coordinate for a wall sample row. + int wallTexY(int y, int columnHeight) { + final int projectionCenterY = projectionViewHeight ~/ 2; + final double relativeY = + (y - (-columnHeight ~/ 2 + projectionCenterY)) / columnHeight; + return (relativeY * 64).toInt().clamp(0, 63); + } + + /// Returns the texture Y coordinate for a sprite sample row. + int spriteTexY(int y, int drawStartY, int spriteHeight) { + final double relativeY = (y - drawStartY) / spriteHeight; + return (relativeY * 64).toInt().clamp(0, 63); + } + + /// Returns the screen-space bounds for the player's weapon overlay. + ({int weaponWidth, int weaponHeight, int startX, int startY}) + weaponScreenBounds(WolfEngine engine) { + final int ww = (projectionWidth * 0.5).toInt(); + final int wh = (viewHeight * 0.8).toInt(); + final int sx = projectionOffsetX + (projectionWidth ~/ 2) - (ww ~/ 2); + final int sy = viewHeight - wh + (engine.player.weaponAnimOffset ~/ 4); + return (weaponWidth: ww, weaponHeight: wh, startX: sx, startY: sy); + } +} diff --git a/packages/wolf_3d_dart/lib/src/raycasting/raycaster.dart b/packages/wolf_3d_dart/lib/src/raycasting/raycaster.dart new file mode 100644 index 0000000..cd2e105 --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/raycasting/raycaster.dart @@ -0,0 +1,419 @@ +import 'dart:math' as math; + +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_entities.dart'; + +abstract class RaycastBackend { + List get zBuffer; + int get projectionWidth; + int get projectionOffsetX; + int get projectionViewHeight; + int get viewHeight; + double get aspectMultiplier; + double get verticalStretch; + + int wallTexY(int y, int columnHeight); + int spriteTexY(int y, int drawStartY, int spriteHeight); + + void drawWallColumn( + int x, + int drawStart, + int drawEnd, + int columnHeight, + Sprite texture, + int texX, + double perpWallDist, + int side, + ); + + void drawSpriteStripe( + int stripeX, + int drawStartY, + int drawEndY, + int spriteHeight, + Sprite texture, + int texX, + double transformY, + ); +} + +/// Shared Wolf3D wall DDA and sprite projection math. +class Raycaster { + final List _spriteScratch = []; + + void castWorld(WolfEngine engine, RaycastBackend backend) { + _castWalls(engine, backend); + _castSprites(engine, backend); + } + + ({double distance, int side, int hitWallId, double wallX})? + _intersectActivePushwall( + Player player, + double rayDirX, + double rayDirY, + Pushwall activePushwall, + ) { + double minX = activePushwall.x.toDouble(); + double maxX = activePushwall.x + 1.0; + double minY = activePushwall.y.toDouble(); + double maxY = activePushwall.y + 1.0; + + if (activePushwall.dirX != 0) { + final double delta = activePushwall.dirX * activePushwall.offset; + minX += delta; + maxX += delta; + } + + if (activePushwall.dirY != 0) { + final double delta = activePushwall.dirY * activePushwall.offset; + minY += delta; + maxY += delta; + } + + const double epsilon = 1e-9; + + double tMinX = double.negativeInfinity; + double tMaxX = double.infinity; + if (rayDirX.abs() < epsilon) { + if (player.x < minX || player.x > maxX) { + return null; + } + } else { + final double tx1 = (minX - player.x) / rayDirX; + final double tx2 = (maxX - player.x) / rayDirX; + tMinX = math.min(tx1, tx2); + tMaxX = math.max(tx1, tx2); + } + + double tMinY = double.negativeInfinity; + double tMaxY = double.infinity; + if (rayDirY.abs() < epsilon) { + if (player.y < minY || player.y > maxY) { + return null; + } + } else { + final double ty1 = (minY - player.y) / rayDirY; + final double ty2 = (maxY - player.y) / rayDirY; + tMinY = math.min(ty1, ty2); + tMaxY = math.max(ty1, ty2); + } + + final double entryDistance = math.max(tMinX, tMinY); + final double exitDistance = math.min(tMaxX, tMaxY); + + if (exitDistance < 0 || entryDistance > exitDistance) { + return null; + } + + final double hitDistance = entryDistance >= 0 + ? entryDistance + : exitDistance; + if (hitDistance < 0) { + return null; + } + + final int side = tMinX > tMinY ? 0 : 1; + final double wallCoord = side == 0 + ? player.y + hitDistance * rayDirY + : player.x + hitDistance * rayDirX; + + return ( + distance: hitDistance, + side: side, + hitWallId: activePushwall.mapId, + wallX: wallCoord - wallCoord.floor(), + ); + } + + void _castWalls(WolfEngine engine, RaycastBackend backend) { + final Player player = engine.player; + final SpriteMap map = engine.currentLevel; + final List wallTextures = engine.data.walls; + final int sceneWidth = backend.projectionWidth; + final int sceneHeight = backend.projectionViewHeight; + final Pushwall? activePushwall = engine.pushwallManager.activePushwall; + + const double fovHalfTan = 0.5773502691896257; // tan(PI / 6) + final double cosAngle = math.cos(player.angle); + final double sinAngle = math.sin(player.angle); + + final double dirX = cosAngle; + final double dirY = sinAngle; + final double planeX = -dirY * fovHalfTan; + final double planeY = dirX * fovHalfTan; + + for (int x = 0; x < sceneWidth; x++) { + final double cameraX = 2 * x / sceneWidth - 1.0; + final double rayDirX = dirX + (planeX * cameraX); + final double rayDirY = dirY + (planeY * cameraX); + + final pushwallHit = activePushwall == null + ? null + : _intersectActivePushwall(player, rayDirX, rayDirY, activePushwall); + + int mapX = player.x.toInt(); + int mapY = player.y.toInt(); + + final double deltaDistX = (rayDirX == 0) ? 1e30 : (1.0 / rayDirX).abs(); + final double deltaDistY = (rayDirY == 0) ? 1e30 : (1.0 / rayDirY).abs(); + + late double sideDistX; + late double sideDistY; + double perpWallDist = 0.0; + int stepX; + int stepY; + int side = 0; + int hitWallId = 0; + bool hit = false; + bool hitOutOfBounds = false; + bool customDistCalculated = false; + double textureOffset = 0.0; + double? wallXOverride; + + if (rayDirX < 0) { + stepX = -1; + sideDistX = (player.x - mapX) * deltaDistX; + } else { + stepX = 1; + sideDistX = (mapX + 1.0 - player.x) * deltaDistX; + } + if (rayDirY < 0) { + stepY = -1; + sideDistY = (player.y - mapY) * deltaDistY; + } else { + stepY = 1; + sideDistY = (mapY + 1.0 - player.y) * deltaDistY; + } + + while (!hit) { + if (sideDistX < sideDistY) { + sideDistX += deltaDistX; + mapX += stepX; + side = 0; + } else { + sideDistY += deltaDistY; + mapY += stepY; + side = 1; + } + + if (mapY < 0 || + mapY >= map.length || + mapX < 0 || + mapX >= map[0].length) { + hit = true; + hitOutOfBounds = true; + } else if (map[mapY][mapX] > 0) { + if (activePushwall != null && + mapX == activePushwall.x && + mapY == activePushwall.y) { + continue; + } + + if (map[mapY][mapX] >= 90) { + final double currentOffset = engine.doorManager.doorOffsetAt( + mapX, + mapY, + ); + if (currentOffset > 0.0) { + final double perpWallDistTemp = side == 0 + ? (sideDistX - deltaDistX) + : (sideDistY - deltaDistY); + double wallXTemp = side == 0 + ? player.y + perpWallDistTemp * rayDirY + : player.x + perpWallDistTemp * rayDirX; + wallXTemp -= wallXTemp.floor(); + if (wallXTemp < currentOffset) { + continue; + } + } + hit = true; + hitWallId = map[mapY][mapX]; + textureOffset = currentOffset; + } else { + hit = true; + hitWallId = map[mapY][mapX]; + } + } + } + + if (hitOutOfBounds || !hit) { + if (pushwallHit == null) { + continue; + } + + customDistCalculated = true; + perpWallDist = pushwallHit.distance; + side = pushwallHit.side; + hitWallId = pushwallHit.hitWallId; + wallXOverride = pushwallHit.wallX; + textureOffset = 0.0; + } + + if (!customDistCalculated) { + perpWallDist = side == 0 + ? (sideDistX - deltaDistX) + : (sideDistY - deltaDistY); + } + + if (pushwallHit != null && pushwallHit.distance < perpWallDist) { + perpWallDist = pushwallHit.distance; + side = pushwallHit.side; + hitWallId = pushwallHit.hitWallId; + wallXOverride = pushwallHit.wallX; + textureOffset = 0.0; + } + + if (perpWallDist < 0.1) { + perpWallDist = 0.1; + } + + backend.zBuffer[x] = perpWallDist; + + double wallX = + wallXOverride ?? + (side == 0 + ? player.y + perpWallDist * rayDirY + : player.x + perpWallDist * rayDirX); + wallX -= wallX.floor(); + + int texNum; + if (hitWallId >= 90) { + texNum = 98.clamp(0, wallTextures.length - 1); + } else { + texNum = ((hitWallId - 1) * 2).clamp(0, wallTextures.length - 2); + if (side == 1) { + texNum += 1; + } + } + final Sprite texture = wallTextures[texNum]; + + int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63); + if (side == 0 && cosAngle > 0) { + texX = 63 - texX; + } + if (side == 1 && sinAngle < 0) { + texX = 63 - texX; + } + + final int columnHeight = + ((sceneHeight / perpWallDist) * backend.verticalStretch).toInt(); + final int drawStart = (-columnHeight ~/ 2 + sceneHeight ~/ 2).clamp( + 0, + sceneHeight, + ); + final int drawEnd = (columnHeight ~/ 2 + sceneHeight ~/ 2).clamp( + 0, + sceneHeight, + ); + + backend.drawWallColumn( + backend.projectionOffsetX + x, + drawStart, + drawEnd, + columnHeight, + texture, + texX, + perpWallDist, + side, + ); + } + } + + void _castSprites(WolfEngine engine, RaycastBackend backend) { + final Player player = engine.player; + final int sceneWidth = backend.projectionWidth; + final int sceneHeight = backend.projectionViewHeight; + final double playerX = player.position.x; + final double playerY = player.position.y; + + final double cosAngle = math.cos(player.angle); + final double sinAngle = math.sin(player.angle); + const double fovHalfTan = 0.5773502691896257; // tan(PI / 6) + + final double dirX = cosAngle; + final double dirY = sinAngle; + final double planeX = -dirY * fovHalfTan; + final double planeY = dirX * fovHalfTan; + + _spriteScratch.clear(); + for (final Entity entity in engine.entities) { + final double toSpriteX = entity.position.x - playerX; + final double toSpriteY = entity.position.y - playerY; + // Reject sprites behind the player before sort/projection work. + if ((toSpriteX * dirX + toSpriteY * dirY) > 0) { + _spriteScratch.add(entity); + } + } + + _spriteScratch.sort((a, b) { + final double adx = a.position.x - playerX; + final double ady = a.position.y - playerY; + final double bdx = b.position.x - playerX; + final double bdy = b.position.y - playerY; + final double distA = (adx * adx) + (ady * ady); + final double distB = (bdx * bdx) + (bdy * bdy); + return distB.compareTo(distA); + }); + + for (final Entity entity in _spriteScratch) { + final double spriteX = entity.position.x - player.position.x; + final double spriteY = entity.position.y - player.position.y; + + final double invDet = 1.0 / (planeX * dirY - dirX * planeY); + final double transformX = invDet * (dirY * spriteX - dirX * spriteY); + final double transformY = invDet * (-planeY * spriteX + planeX * spriteY); + + if (transformY <= 0) { + continue; + } + + final int spriteScreenX = + ((sceneWidth / 2) * (1 + transformX / transformY)).toInt(); + final int spriteHeight = + ((sceneHeight / transformY).abs() * backend.verticalStretch).toInt(); + final int displayedSpriteHeight = + ((backend.viewHeight / transformY).abs() * backend.verticalStretch) + .toInt(); + + final int spriteWidth = + (displayedSpriteHeight * + backend.aspectMultiplier / + backend.verticalStretch) + .toInt(); + + final int drawStartY = -spriteHeight ~/ 2 + sceneHeight ~/ 2; + final int drawEndY = spriteHeight ~/ 2 + sceneHeight ~/ 2; + final int drawStartX = -spriteWidth ~/ 2 + spriteScreenX; + final int drawEndX = spriteWidth ~/ 2 + spriteScreenX; + + final int clipStartX = math.max(0, drawStartX); + final int clipEndX = math.min(sceneWidth, drawEndX); + + final int safeIndex = entity.spriteIndex.clamp( + 0, + engine.data.sprites.length - 1, + ); + final Sprite texture = engine.data.sprites[safeIndex]; + + for (int stripe = clipStartX; stripe < clipEndX; stripe++) { + if (transformY < backend.zBuffer[stripe]) { + final int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp( + 0, + 63, + ); + + backend.drawSpriteStripe( + backend.projectionOffsetX + stripe, + drawStartY, + drawEndY, + spriteHeight, + texture, + texX, + transformY, + ); + } + } + } + } +} diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart b/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart similarity index 98% rename from packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart rename to packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart index 0e11c32..052716d 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/ascii_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/ascii_renderer.dart @@ -6,7 +6,7 @@ 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_menu.dart'; -import 'cli_rasterizer.dart'; +import 'cli_renderer_backend.dart'; import 'menu_font.dart'; class AsciiTheme { @@ -74,14 +74,14 @@ class ColoredChar { } } -enum AsciiRasterizerMode { +enum AsciiRendererMode { terminalAnsi, terminalGrid, } -/// Text-mode rasterizer that can render to ANSI escape output or a Flutter +/// Text-mode renderer that can render to ANSI escape output or a Flutter /// grid model of colored characters. -class AsciiRasterizer extends CliRasterizer { +class AsciiRenderer extends CliRendererBackend { static const double _targetAspectRatio = 4 / 3; static const int _terminalBackdropPaletteIndex = 153; static const int _minimumTerminalColumns = 80; @@ -93,21 +93,21 @@ class AsciiRasterizer extends CliRasterizer { static const int _menuHintLabelPaletteIndex = 4; static const int _menuHintBackgroundPaletteIndex = 0; - AsciiRasterizer({ + AsciiRenderer({ this.activeTheme = AsciiThemes.blocks, - this.mode = AsciiRasterizerMode.terminalGrid, + this.mode = AsciiRendererMode.terminalGrid, this.useTerminalLayout = true, this.aspectMultiplier = 1.0, this.verticalStretch = 1.0, }); AsciiTheme activeTheme = AsciiThemes.blocks; - final AsciiRasterizerMode mode; + final AsciiRendererMode mode; final bool useTerminalLayout; bool get _usesTerminalLayout => useTerminalLayout; - bool get _emitAnsi => mode == AsciiRasterizerMode.terminalAnsi; + bool get _emitAnsi => mode == AsciiRendererMode.terminalAnsi; late List> _screen; late List> _scenePixels; @@ -323,7 +323,7 @@ class AsciiRasterizer extends CliRasterizer { // --- PRIVATE HUD DRAWING HELPER --- - /// Injects a pure text string directly into the rasterizer grid + /// Injects a pure text string directly into the renderer grid void _writeString( int startX, int y, diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/cli_rasterizer.dart b/packages/wolf_3d_dart/lib/src/rendering/cli_renderer_backend.dart similarity index 88% rename from packages/wolf_3d_dart/lib/src/rasterizer/cli_rasterizer.dart rename to packages/wolf_3d_dart/lib/src/rendering/cli_renderer_backend.dart index ded98b4..61d9cd8 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/cli_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/cli_renderer_backend.dart @@ -1,9 +1,9 @@ import 'package:wolf_3d_dart/src/engine/wolf_3d_engine_base.dart'; -import 'rasterizer.dart'; +import 'renderer_backend.dart'; -/// Shared terminal orchestration for CLI rasterizers. -abstract class CliRasterizer extends Rasterizer { +/// Shared terminal orchestration for CLI renderers. +abstract class CliRendererBackend extends RendererBackend { /// Resolves the framebuffer dimensions required by this renderer. /// /// The default uses the full terminal size. diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/menu_font.dart b/packages/wolf_3d_dart/lib/src/rendering/menu_font.dart similarity index 100% rename from packages/wolf_3d_dart/lib/src/rasterizer/menu_font.dart rename to packages/wolf_3d_dart/lib/src/rendering/menu_font.dart diff --git a/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart b/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart new file mode 100644 index 0000000..2bd3cde --- /dev/null +++ b/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart @@ -0,0 +1,231 @@ +import 'package:wolf_3d_dart/src/raycasting/projection.dart'; +import 'package:wolf_3d_dart/src/raycasting/raycaster.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; +import 'package:wolf_3d_dart/wolf_3d_engine.dart'; + +/// Shared rendering pipeline for Wolf3D backends. +/// +/// Subclasses implement draw primitives for their output target (software +/// framebuffer, ANSI text, Sixel, etc), while this backend coordinates frame +/// orchestration and delegates DDA/sprite math to [Raycaster]. +abstract class RendererBackend + with ProjectionMath + implements RaycastBackend { + @override + List zBuffer = []; + late int width; + late int height; + @override + late int viewHeight; + + /// The current engine instance; set at the start of every [render] call. + late WolfEngine engine; + + final Raycaster _raycaster = Raycaster(); + + /// A multiplier to adjust the width of sprites. + /// Pixel renderers usually keep this at 1.0. + /// ASCII renderers can override this (e.g., 0.6) to account for tall characters. + @override + double get aspectMultiplier => 1.0; + + /// A multiplier to counteract tall pixel formats (like 1:2 terminal fonts). + /// Defaults to 1.0 (no squish) for standard pixel rendering. + @override + double get verticalStretch => 1.0; + + /// The logical width of the projection area used for raycasting and sprites. + /// Most renderers use the full buffer width. + @override + int get projectionWidth => width; + + /// Horizontal offset of the projection area within the output buffer. + @override + int get projectionOffsetX => 0; + + /// The logical height of the 3D projection before a renderer maps rows to output pixels. + /// Most renderers use the visible view height. Terminal ASCII can override this to render + /// more vertical detail and collapse it into half-block glyphs. + @override + int get projectionViewHeight => viewHeight; + + /// Whether the current terminal dimensions are supported by this renderer. + /// Default renderers accept all sizes. + bool isTerminalSizeSupported(int columns, int rows) => true; + + /// Human-readable requirement text used by the host app when size checks fail. + String get terminalSizeRequirement => 'Please resize your terminal window.'; + + void _ensureZBuffer() { + if (zBuffer.length != projectionWidth) { + zBuffer = List.filled(projectionWidth, 0.0); + return; + } + zBuffer.fillRange(0, zBuffer.length, 0.0); + } + + /// The main entry point called by the game loop. + /// Orchestrates the rendering pipeline. + T render(WolfEngine engine) { + this.engine = engine; + width = engine.frameBuffer.width; + height = engine.frameBuffer.height; + // The 3D view typically takes up the top 80% of the screen. + viewHeight = (height * 0.8).toInt(); + _ensureZBuffer(); + + // 1. Setup the frame (clear screen, draw floor/ceiling). + prepareFrame(engine); + + if (engine.difficulty == null) { + drawMenu(engine); + return finalizeFrame(); + } + + // 2. Do the heavy math for wall and sprite casting. + _raycaster.castWorld(engine, this); + + // 3. Draw 2D overlays. + drawWeapon(engine); + drawHud(engine); + + // 4. Finalize and return the frame data (Buffer or String/List). + return finalizeFrame(); + } + + // =========================================================================== + // ABSTRACT METHODS (Implemented by backend subclasses) + // =========================================================================== + + /// Initialize buffers, clear the screen, and draw the floor/ceiling. + void prepareFrame(WolfEngine engine); + + /// Draw a single vertical column of a wall. + @override + void drawWallColumn( + int x, + int drawStart, + int drawEnd, + int columnHeight, + Sprite texture, + int texX, + double perpWallDist, + int side, + ); + + /// Draw a single vertical stripe of a sprite (enemy/item). + @override + void drawSpriteStripe( + int stripeX, + int drawStartY, + int drawEndY, + int spriteHeight, + Sprite texture, + int texX, + double transformY, + ); + + /// Draw the player's weapon overlay at the bottom of the 3D view. + void drawWeapon(WolfEngine engine); + + /// Draw the 2D status bar at the bottom 20% of the screen. + void drawHud(WolfEngine engine); + + /// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list). + T finalizeFrame(); + + /// 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. + void drawMenu(WolfEngine engine) {} + + /// Plots a VGA image into this backend's HUD coordinate space. + /// + /// Coordinates are in the original 320x200 HUD space. Backends that support + /// shared HUD composition should override this. + void blitHudVgaImage(VgaImage image, int startX320, int startY200) {} + + /// Shared Wolf3D VGA HUD sequence used by software/sixel/ASCII-full-HUD. + /// + /// Coordinates are intentionally in original 320x200 HUD space so each + /// backend can scale/map them consistently via [blitHudVgaImage]. + void drawStandardVgaHud(WolfEngine engine) { + final List vgaImages = engine.data.vgaImages; + final int statusBarIndex = vgaImages.indexWhere( + (img) => img.width == 320 && img.height == 40, + ); + if (statusBarIndex == -1) return; + + blitHudVgaImage(vgaImages[statusBarIndex], 0, 160); + _drawHudNumber(vgaImages, 1, 32, 176); + _drawHudNumber(vgaImages, engine.player.score, 96, 176); + _drawHudNumber(vgaImages, 3, 120, 176); + _drawHudNumber(vgaImages, engine.player.health, 192, 176); + _drawHudNumber(vgaImages, engine.player.ammo, 232, 176); + _drawHudFace(engine, vgaImages); + _drawHudWeaponIcon(engine, vgaImages); + } + + void _drawHudNumber( + List vgaImages, + int value, + int rightAlignX, + int startY, + ) { + // HUD numbers are rendered with fixed-width VGA glyphs (8 px advance). + const int zeroIndex = 96; + final String numStr = value.toString(); + int currentX = rightAlignX - (numStr.length * 8); + + for (int i = 0; i < numStr.length; i++) { + final int digit = int.parse(numStr[i]); + final int imageIndex = zeroIndex + digit; + if (imageIndex < vgaImages.length) { + blitHudVgaImage(vgaImages[imageIndex], currentX, startY); + } + currentX += 8; + } + } + + void _drawHudFace(WolfEngine engine, List vgaImages) { + final int faceIndex = hudFaceVgaIndex(engine.player.health); + if (faceIndex < vgaImages.length) { + blitHudVgaImage(vgaImages[faceIndex], 136, 164); + } + } + + void _drawHudWeaponIcon(WolfEngine engine, List vgaImages) { + final int weaponIndex = hudWeaponVgaIndex(engine); + if (weaponIndex < vgaImages.length) { + blitHudVgaImage(vgaImages[weaponIndex], 256, 164); + } + } + + /// Calculates depth-based lighting falloff (0.0 to 1.0). + /// While the original Wolf3D didn't use depth fog, this provides a great + /// atmospheric effect for custom backends (like ASCII dithering). + double calculateDepthBrightness(double distance) { + return (10.0 / (distance + 2.0)).clamp(0.0, 1.0); + } + + /// Returns the VGA image index for BJ's face sprite based on player health. + int hudFaceVgaIndex(int health) { + if (health <= 0) return 127; + return 106 + (((100 - health) ~/ 16).clamp(0, 6) * 3); + } + + /// Returns the VGA image index for the current weapon icon in the HUD. + int hudWeaponVgaIndex(WolfEngine engine) { + if (engine.player.hasChainGun) return 91; + if (engine.player.hasMachineGun) return 90; + return 89; + } + + /// Darkens a 32-bit 0xAABBGGRR color by roughly 30% without touching alpha. + int shadeColor(int color) { + final int r = (color & 0xFF) * 7 ~/ 10; + final int g = ((color >> 8) & 0xFF) * 7 ~/ 10; + final int b = ((color >> 16) & 0xFF) * 7 ~/ 10; + return (0xFF000000) | (b << 16) | (g << 8) | r; + } +} diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart b/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart similarity index 98% rename from packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart rename to packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart index c29fb9b..91fa477 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/sixel_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/sixel_renderer.dart @@ -1,4 +1,4 @@ -/// Terminal rasterizer that encodes engine frames as Sixel graphics. +/// Terminal renderer that encodes engine frames as Sixel graphics. library; import 'dart:async'; @@ -11,15 +11,15 @@ 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_menu.dart'; -import 'cli_rasterizer.dart'; +import 'cli_renderer_backend.dart'; import 'menu_font.dart'; /// Renders the game into an indexed off-screen buffer and emits Sixel output. /// -/// The rasterizer adapts the engine framebuffer to the current terminal size, +/// The renderer adapts the engine framebuffer to the current terminal size, /// preserving a 4:3 presentation while falling back to size warnings when the /// terminal is too small. -class SixelRasterizer extends CliRasterizer { +class SixelRenderer extends CliRendererBackend { static const double _targetAspectRatio = 4 / 3; static const int _defaultLineHeightPx = 18; static const double _defaultCellWidthToHeight = 0.55; @@ -231,7 +231,7 @@ class SixelRasterizer extends CliRasterizer { final FrameBuffer scaledBuffer = _createScaledBuffer(originalBuffer); // Sixel output references palette indices directly, so there is no need to - // materialize a 32-bit RGBA buffer during the rasterization pass. + // materialize a 32-bit RGBA buffer during the rendering pass. _screen = Uint8List(scaledBuffer.width * scaledBuffer.height); engine.frameBuffer = scaledBuffer; try { diff --git a/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart b/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart similarity index 98% rename from packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart rename to packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart index d21937f..a8de772 100644 --- a/packages/wolf_3d_dart/lib/src/rasterizer/software_rasterizer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart @@ -1,17 +1,17 @@ import 'dart:math' as math; import 'package:wolf_3d_dart/src/menu/menu_manager.dart'; -import 'package:wolf_3d_dart/src/rasterizer/menu_font.dart'; -import 'package:wolf_3d_dart/src/rasterizer/rasterizer.dart'; +import 'package:wolf_3d_dart/src/rendering/menu_font.dart'; +import 'package:wolf_3d_dart/src/rendering/renderer_backend.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_menu.dart'; -/// Pixel-accurate software rasterizer that writes directly into [FrameBuffer]. +/// Pixel-accurate software renderer that writes directly into [FrameBuffer]. /// /// This is the canonical "modern framebuffer" implementation and serves as a /// visual reference for terminal renderers. -class SoftwareRasterizer extends Rasterizer { +class SoftwareRenderer extends RendererBackend { static const int _menuFooterY = 184; static const int _menuFooterHeight = 12; diff --git a/packages/wolf_3d_dart/lib/wolf_3d_rasterizer.dart b/packages/wolf_3d_dart/lib/wolf_3d_rasterizer.dart deleted file mode 100644 index 30e9e5f..0000000 --- a/packages/wolf_3d_dart/lib/wolf_3d_rasterizer.dart +++ /dev/null @@ -1,8 +0,0 @@ -library; - -export 'src/rasterizer/ascii_rasterizer.dart' - show AsciiRasterizer, AsciiRasterizerMode, ColoredChar; -export 'src/rasterizer/cli_rasterizer.dart'; -export 'src/rasterizer/rasterizer.dart'; -export 'src/rasterizer/sixel_rasterizer.dart'; -export 'src/rasterizer/software_rasterizer.dart'; diff --git a/packages/wolf_3d_dart/lib/wolf_3d_renderer.dart b/packages/wolf_3d_dart/lib/wolf_3d_renderer.dart new file mode 100644 index 0000000..cf3f581 --- /dev/null +++ b/packages/wolf_3d_dart/lib/wolf_3d_renderer.dart @@ -0,0 +1,10 @@ +library; + +export 'src/raycasting/projection.dart'; +export 'src/raycasting/raycaster.dart'; +export 'src/rendering/ascii_renderer.dart' + show AsciiRenderer, AsciiRendererMode, ColoredChar; +export 'src/rendering/cli_renderer_backend.dart'; +export 'src/rendering/renderer_backend.dart'; +export 'src/rendering/sixel_renderer.dart'; +export 'src/rendering/software_renderer.dart'; diff --git a/packages/wolf_3d_dart/test/entities/enemy_spawn_test.dart b/packages/wolf_3d_dart/test/entities/enemy_spawn_test.dart index e9cb54a..47d4395 100644 --- a/packages/wolf_3d_dart/test/entities/enemy_spawn_test.dart +++ b/packages/wolf_3d_dart/test/entities/enemy_spawn_test.dart @@ -40,6 +40,20 @@ void main() { expect(_spawnEnemy(140).angle, CardinalDirection.west.radians); expect(_spawnEnemy(141).angle, CardinalDirection.south.radians); }); + + test('spawns standing variants as ambushing', () { + expect(_spawnEnemy(134).state, EntityState.ambushing); + expect(_spawnEnemy(135).state, EntityState.ambushing); + expect(_spawnEnemy(136).state, EntityState.ambushing); + expect(_spawnEnemy(137).state, EntityState.ambushing); + }); + + test('spawns patrol variants as patrolling', () { + expect(_spawnEnemy(138).state, EntityState.patrolling); + expect(_spawnEnemy(139).state, EntityState.patrolling); + expect(_spawnEnemy(140).state, EntityState.patrolling); + expect(_spawnEnemy(141).state, EntityState.patrolling); + }); }); } diff --git a/packages/wolf_3d_dart/test/rasterizer/projection_sampling_test.dart b/packages/wolf_3d_dart/test/rasterizer/projection_sampling_test.dart index a017213..5b1d458 100644 --- a/packages/wolf_3d_dart/test/rasterizer/projection_sampling_test.dart +++ b/packages/wolf_3d_dart/test/rasterizer/projection_sampling_test.dart @@ -1,5 +1,5 @@ import 'package:test/test.dart'; -import 'package:wolf_3d_dart/src/rasterizer/rasterizer.dart'; +import 'package:wolf_3d_dart/src/rendering/renderer_backend.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart'; @@ -28,7 +28,7 @@ void main() { }); } -class _TestRasterizer extends Rasterizer { +class _TestRasterizer extends RendererBackend { _TestRasterizer({required this.customProjectionViewHeight}); final int customProjectionViewHeight; diff --git a/packages/wolf_3d_dart/test/rasterizer/pushwall_rasterizer_test.dart b/packages/wolf_3d_dart/test/rasterizer/pushwall_rasterizer_test.dart index ef70fc3..d6a73d3 100644 --- a/packages/wolf_3d_dart/test/rasterizer/pushwall_rasterizer_test.dart +++ b/packages/wolf_3d_dart/test/rasterizer/pushwall_rasterizer_test.dart @@ -4,7 +4,7 @@ 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_rasterizer.dart'; +import 'package:wolf_3d_dart/wolf_3d_renderer.dart'; void main() { group('Pushwall rasterization', () { @@ -64,7 +64,7 @@ void main() { ..offset = 0.5; engine.pushwallManager.activePushwall = pushwall; - final frame = SoftwareRasterizer().render(engine); + final frame = SoftwareRenderer().render(engine); final centerIndex = (frame.height ~/ 2) * frame.width + (frame.width ~/ 2); diff --git a/packages/wolf_3d_dart/test/rendering/projection_sampling_test.dart b/packages/wolf_3d_dart/test/rendering/projection_sampling_test.dart new file mode 100644 index 0000000..4a751ab --- /dev/null +++ b/packages/wolf_3d_dart/test/rendering/projection_sampling_test.dart @@ -0,0 +1,85 @@ +import 'package:test/test.dart'; +import 'package:wolf_3d_dart/src/rendering/renderer_backend.dart'; +import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; +import 'package:wolf_3d_dart/wolf_3d_engine.dart'; + +void main() { + group('Renderer wall texture sampling', () { + test('anchors wall texel sampling to projection height center', () { + final renderer = _TestRenderer(customProjectionViewHeight: 40); + renderer.configureViewGeometry(width: 64, height: 64, viewHeight: 20); + + // With sceneHeight=40 and columnHeight=20, projected wall spans y=10..30. + // Top pixel should sample from top texel row. + expect(renderer.wallTexY(10, 20), 0); + + // Bottom visible pixel should sample close to bottom texel row. + expect(renderer.wallTexY(29, 20), inInclusiveRange(60, 63)); + }); + + test('keeps legacy behavior when projection height equals view height', () { + final renderer = _TestRenderer(customProjectionViewHeight: 20); + renderer.configureViewGeometry(width: 64, height: 64, viewHeight: 20); + + // With sceneHeight=viewHeight=20 and columnHeight=20, top starts at y=0. + expect(renderer.wallTexY(0, 20), 0); + expect(renderer.wallTexY(19, 20), inInclusiveRange(60, 63)); + }); + }); +} + +class _TestRenderer extends RendererBackend { + _TestRenderer({required this.customProjectionViewHeight}); + + final int customProjectionViewHeight; + + @override + int get projectionViewHeight => customProjectionViewHeight; + + void configureViewGeometry({ + required int width, + required int height, + required int viewHeight, + }) { + this.width = width; + this.height = height; + this.viewHeight = viewHeight; + } + + @override + void prepareFrame(WolfEngine engine) {} + + @override + void drawWallColumn( + int x, + int drawStart, + int drawEnd, + int columnHeight, + Sprite texture, + int texX, + double perpWallDist, + int side, + ) {} + + @override + void drawSpriteStripe( + int stripeX, + int drawStartY, + int drawEndY, + int spriteHeight, + Sprite texture, + int texX, + double transformY, + ) {} + + @override + void drawWeapon(WolfEngine engine) {} + + @override + void drawHud(WolfEngine engine) {} + + @override + FrameBuffer finalizeFrame() { + return FrameBuffer(1, 1); + } +} diff --git a/packages/wolf_3d_dart/test/rendering/pushwall_renderer_test.dart b/packages/wolf_3d_dart/test/rendering/pushwall_renderer_test.dart new file mode 100644 index 0000000..57f2f52 --- /dev/null +++ b/packages/wolf_3d_dart/test/rendering/pushwall_renderer_test.dart @@ -0,0 +1,90 @@ +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('Pushwall rendering', () { + test('active pushwall occludes the wall behind it while sliding', () { + final wallGrid = _buildGrid(); + final objectGrid = _buildGrid(); + + _fillBoundaries(wallGrid, 2); + objectGrid[2][2] = MapObject.playerEast; + + wallGrid[2][4] = 1; + objectGrid[2][4] = MapObject.pushwallTrigger; + + wallGrid[2][6] = 2; + + final engine = WolfEngine( + data: WolfensteinData( + version: GameVersion.shareware, + walls: [ + _solidSprite(1), + _solidSprite(1), + _solidSprite(2), + _solidSprite(2), + ], + sprites: List.generate(436, (_) => _solidSprite(255)), + sounds: [], + adLibSounds: [], + music: [], + vgaImages: [], + episodes: [ + Episode( + name: 'Episode 1', + levels: [ + WolfLevel( + name: 'Test Level', + wallGrid: wallGrid, + objectGrid: objectGrid, + musicIndex: 0, + ), + ], + ), + ], + ), + difficulty: Difficulty.medium, + startingEpisode: 0, + frameBuffer: FrameBuffer(64, 64), + input: CliInput(), + onGameWon: () {}, + ); + + engine.init(); + + final pushwall = engine.pushwallManager.pushwalls['4,2']!; + pushwall + ..dirX = 1 + ..dirY = 0 + ..offset = 0.5; + engine.pushwallManager.activePushwall = pushwall; + + final frame = SoftwareRenderer().render(engine); + final centerIndex = + (frame.height ~/ 2) * frame.width + (frame.width ~/ 2); + + expect(frame.pixels[centerIndex], ColorPalette.vga32Bit[1]); + expect(frame.pixels[centerIndex], isNot(ColorPalette.vga32Bit[2])); + }); + }); +} + +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))); +} diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart index 46f0c85..d086ffa 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart @@ -1,8 +1,8 @@ -/// Flutter widget that renders Wolf3D frames using the ASCII rasterizer. +/// Flutter widget that renders Wolf3D frames using the ASCII renderer. library; import 'package:flutter/material.dart'; -import 'package:wolf_3d_dart/wolf_3d_rasterizer.dart'; +import 'package:wolf_3d_dart/wolf_3d_renderer.dart'; import 'package:wolf_3d_renderer/base_renderer.dart'; /// Displays the game using a text-mode approximation of the original renderer. @@ -22,8 +22,8 @@ class _WolfAsciiRendererState extends BaseWolfRendererState { static const int _renderHeight = 100; List> _asciiFrame = []; - final AsciiRasterizer _asciiRasterizer = AsciiRasterizer( - mode: AsciiRasterizerMode.terminalGrid, + final AsciiRenderer _asciiRenderer = AsciiRenderer( + mode: AsciiRendererMode.terminalGrid, ); @override @@ -45,7 +45,7 @@ class _WolfAsciiRendererState extends BaseWolfRendererState { @override void performRender() { setState(() { - _asciiFrame = _asciiRasterizer.render(widget.engine); + _asciiFrame = _asciiRenderer.render(widget.engine); }); } @@ -63,7 +63,7 @@ class _WolfAsciiRendererState extends BaseWolfRendererState { /// Paints a pre-rasterized ASCII frame using grouped text spans per color run. class AsciiFrameWidget extends StatelessWidget { - /// Two-dimensional text grid generated by [AsciiRasterizer.render]. + /// Two-dimensional text grid generated by [AsciiRenderer.render]. final List> frameData; /// Creates a widget that displays [frameData]. diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart index 30bfb6c..cd5b1a3 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart @@ -5,11 +5,11 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; -import 'package:wolf_3d_dart/wolf_3d_rasterizer.dart'; +import 'package:wolf_3d_dart/wolf_3d_renderer.dart'; import 'package:wolf_3d_renderer/base_renderer.dart'; import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart'; -/// Presents the software rasterizer output by decoding the shared framebuffer. +/// Presents the software renderer output by decoding the shared framebuffer. class WolfFlutterRenderer extends BaseWolfRenderer { /// Creates a pixel renderer bound to [engine]. const WolfFlutterRenderer({ @@ -25,7 +25,7 @@ class _WolfFlutterRendererState extends BaseWolfRendererState { static const int _renderWidth = 320; static const int _renderHeight = 200; - final SoftwareRasterizer _rasterizer = SoftwareRasterizer(); + final SoftwareRenderer _renderer = SoftwareRenderer(); ui.Image? _renderedFrame; bool _isRendering = false; @@ -51,7 +51,7 @@ class _WolfFlutterRendererState _isRendering = true; final FrameBuffer frameBuffer = widget.engine.frameBuffer; - _rasterizer.render(widget.engine); + _renderer.render(widget.engine); // Convert the engine-owned framebuffer into a GPU-friendly ui.Image on // the Flutter side while preserving nearest-neighbor pixel fidelity.