Abstracted more functionality into the base rasterizer
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -16,6 +16,10 @@ class Player {
|
|||||||
int ammo = 8;
|
int ammo = 8;
|
||||||
int score = 0;
|
int score = 0;
|
||||||
|
|
||||||
|
// Damage flash
|
||||||
|
double damageFlash = 0.0; // 0.0 is none, 1.0 is maximum red
|
||||||
|
final double damageFlashFadeSpeed = 0.05; // How fast it fades per tick
|
||||||
|
|
||||||
// Inventory
|
// Inventory
|
||||||
bool hasGoldKey = false;
|
bool hasGoldKey = false;
|
||||||
bool hasSilverKey = false;
|
bool hasSilverKey = false;
|
||||||
@@ -40,17 +44,25 @@ class Player {
|
|||||||
// How fast the weapon drops/raises per tick
|
// How fast the weapon drops/raises per tick
|
||||||
final double switchSpeed = 30.0;
|
final double switchSpeed = 30.0;
|
||||||
|
|
||||||
Player({
|
Player({required this.x, required this.y, required this.angle}) {
|
||||||
required this.x,
|
|
||||||
required this.y,
|
|
||||||
required this.angle,
|
|
||||||
}) {
|
|
||||||
currentWeapon = weapons[WeaponType.pistol]!;
|
currentWeapon = weapons[WeaponType.pistol]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper getter to interface with the RaycasterPainter
|
// Helper getter to interface with the RaycasterPainter
|
||||||
Coordinate2D get position => Coordinate2D(x, y);
|
Coordinate2D get position => Coordinate2D(x, y);
|
||||||
|
|
||||||
|
// --- General Update ---
|
||||||
|
|
||||||
|
void tick(Duration elapsed) {
|
||||||
|
// Fade the damage flash over time
|
||||||
|
if (damageFlash > 0.0) {
|
||||||
|
// Assuming 60fps, we fade it out
|
||||||
|
damageFlash = math.max(0.0, damageFlash - damageFlashFadeSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWeaponSwitch();
|
||||||
|
}
|
||||||
|
|
||||||
// --- Weapon Switching & Animation Logic ---
|
// --- Weapon Switching & Animation Logic ---
|
||||||
|
|
||||||
void updateWeaponSwitch() {
|
void updateWeaponSwitch() {
|
||||||
@@ -95,6 +107,11 @@ class Player {
|
|||||||
|
|
||||||
void takeDamage(int damage) {
|
void takeDamage(int damage) {
|
||||||
health = math.max(0, health - damage);
|
health = math.max(0, health - damage);
|
||||||
|
|
||||||
|
// Spike the damage flash based on how much damage was taken
|
||||||
|
// A 10 damage hit gives a 0.5 flash, a 20 damage hit maxes it out at 1.0
|
||||||
|
damageFlash = math.min(1.0, damageFlash + (damage * 0.05));
|
||||||
|
|
||||||
if (health <= 0) {
|
if (health <= 0) {
|
||||||
print("YOU DIED!");
|
print("YOU DIED!");
|
||||||
} else {
|
} else {
|
||||||
@@ -184,10 +201,7 @@ class Player {
|
|||||||
if (switchState != WeaponSwitchState.idle) return;
|
if (switchState != WeaponSwitchState.idle) return;
|
||||||
|
|
||||||
// We pass the isFiring state to handle automatic vs semi-auto behavior
|
// We pass the isFiring state to handle automatic vs semi-auto behavior
|
||||||
bool shotFired = currentWeapon.fire(
|
bool shotFired = currentWeapon.fire(currentTime, currentAmmo: ammo);
|
||||||
currentTime,
|
|
||||||
currentAmmo: ammo,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shotFired && currentWeapon.type != WeaponType.knife) {
|
if (shotFired && currentWeapon.type != WeaponType.knife) {
|
||||||
ammo--;
|
ammo--;
|
||||||
|
|||||||
@@ -1,6 +1,362 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
|
||||||
|
|
||||||
abstract class Rasterizer {
|
abstract class Rasterizer {
|
||||||
dynamic render(WolfEngine engine, FrameBuffer buffer);
|
late List<double> zBuffer;
|
||||||
|
late int width;
|
||||||
|
late int height;
|
||||||
|
late int viewHeight;
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
/// The main entry point called by the game loop.
|
||||||
|
/// Orchestrates the mathematical rendering pipeline.
|
||||||
|
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||||
|
width = buffer.width;
|
||||||
|
height = buffer.height;
|
||||||
|
// The 3D view typically takes up the top 80% of the screen
|
||||||
|
viewHeight = (height * 0.8).toInt();
|
||||||
|
zBuffer = List.filled(width, 0.0);
|
||||||
|
|
||||||
|
// 1. Setup the frame (clear screen, draw floor/ceiling)
|
||||||
|
prepareFrame(engine);
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
dynamic finalizeFrame();
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// CORE ENGINE MATH (Shared across all renderers)
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
void _castWalls(WolfEngine engine) {
|
||||||
|
final Player player = engine.player;
|
||||||
|
final SpriteMap map = engine.currentLevel;
|
||||||
|
final List<Sprite> wallTextures = engine.data.walls;
|
||||||
|
|
||||||
|
final Map<String, double> 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 < width; x++) {
|
||||||
|
double cameraX = 2 * x / width - 1.0;
|
||||||
|
Coordinate2D rayDir = dir + (plane * cameraX);
|
||||||
|
|
||||||
|
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;
|
||||||
|
Set<String> 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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
// PUSHWALL LOGIC
|
||||||
|
else if (activePushwall != null &&
|
||||||
|
mapX == activePushwall.x &&
|
||||||
|
mapY == activePushwall.y) {
|
||||||
|
hit = true;
|
||||||
|
hitWallId = map[mapY][mapX];
|
||||||
|
|
||||||
|
double pOffset = activePushwall.offset;
|
||||||
|
int pDirX = activePushwall.dirX;
|
||||||
|
int pDirY = activePushwall.dirY;
|
||||||
|
|
||||||
|
perpWallDist = (side == 0)
|
||||||
|
? (sideDistX - deltaDistX)
|
||||||
|
: (sideDistY - deltaDistY);
|
||||||
|
|
||||||
|
if (side == 0 && pDirX != 0) {
|
||||||
|
if (pDirX == stepX) {
|
||||||
|
double intersect = perpWallDist + pOffset * deltaDistX;
|
||||||
|
if (intersect < sideDistY) {
|
||||||
|
perpWallDist = intersect;
|
||||||
|
} else {
|
||||||
|
side = 1;
|
||||||
|
perpWallDist = sideDistY - deltaDistY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
perpWallDist -= (1.0 - pOffset) * deltaDistX;
|
||||||
|
}
|
||||||
|
} else if (side == 1 && pDirY != 0) {
|
||||||
|
if (pDirY == stepY) {
|
||||||
|
double intersect = perpWallDist + pOffset * deltaDistY;
|
||||||
|
if (intersect < sideDistX) {
|
||||||
|
perpWallDist = intersect;
|
||||||
|
} else {
|
||||||
|
side = 0;
|
||||||
|
perpWallDist = sideDistX - deltaDistX;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
perpWallDist -= (1.0 - pOffset) * deltaDistY;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
double wallFraction = (side == 0)
|
||||||
|
? player.y + perpWallDist * rayDir.y
|
||||||
|
: player.x + perpWallDist * rayDir.x;
|
||||||
|
wallFraction -= wallFraction.floor();
|
||||||
|
if (side == 0) {
|
||||||
|
if (pDirY == 1 && wallFraction < pOffset) hit = false;
|
||||||
|
if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
||||||
|
if (hit) textureOffset = pOffset * pDirY;
|
||||||
|
} else {
|
||||||
|
if (pDirX == 1 && wallFraction < pOffset) hit = false;
|
||||||
|
if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
||||||
|
if (hit) textureOffset = pOffset * pDirX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hit) continue;
|
||||||
|
customDistCalculated = true;
|
||||||
|
} else {
|
||||||
|
hit = true;
|
||||||
|
hitWallId = map[mapY][mapX];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitOutOfBounds) continue;
|
||||||
|
|
||||||
|
if (!customDistCalculated) {
|
||||||
|
perpWallDist = (side == 0)
|
||||||
|
? (sideDistX - deltaDistX)
|
||||||
|
: (sideDistY - deltaDistY);
|
||||||
|
}
|
||||||
|
if (perpWallDist < 0.1) perpWallDist = 0.1;
|
||||||
|
|
||||||
|
// Save for sprite depth checks
|
||||||
|
zBuffer[x] = perpWallDist;
|
||||||
|
|
||||||
|
// Calculate Texture X Coordinate
|
||||||
|
double wallX = (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 = (viewHeight / perpWallDist).toInt();
|
||||||
|
int drawStart = (-columnHeight ~/ 2 + viewHeight ~/ 2).clamp(
|
||||||
|
0,
|
||||||
|
viewHeight,
|
||||||
|
);
|
||||||
|
int drawEnd = (columnHeight ~/ 2 + viewHeight ~/ 2).clamp(0, viewHeight);
|
||||||
|
|
||||||
|
// Tell the implementation to draw this column
|
||||||
|
drawWallColumn(
|
||||||
|
x,
|
||||||
|
drawStart,
|
||||||
|
drawEnd,
|
||||||
|
columnHeight,
|
||||||
|
texture,
|
||||||
|
texX,
|
||||||
|
perpWallDist,
|
||||||
|
side,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _castSprites(WolfEngine engine) {
|
||||||
|
final Player player = engine.player;
|
||||||
|
final List<Entity> activeSprites = List.from(engine.entities);
|
||||||
|
|
||||||
|
// 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 = ((width / 2) * (1 + transformX / transformY))
|
||||||
|
.toInt();
|
||||||
|
int spriteHeight = (viewHeight / transformY).abs().toInt();
|
||||||
|
|
||||||
|
// Scale width based on the aspectMultiplier (useful for ASCII)
|
||||||
|
int spriteWidth = (spriteHeight * aspectMultiplier).toInt();
|
||||||
|
|
||||||
|
int drawStartY = -spriteHeight ~/ 2 + viewHeight ~/ 2;
|
||||||
|
int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2;
|
||||||
|
int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
|
||||||
|
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
|
||||||
|
|
||||||
|
int clipStartX = math.max(0, drawStartX);
|
||||||
|
int clipEndX = math.min(width - 1, 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(
|
||||||
|
stripe,
|
||||||
|
drawStartY,
|
||||||
|
drawEndY,
|
||||||
|
spriteHeight,
|
||||||
|
texture,
|
||||||
|
texX,
|
||||||
|
transformY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,430 +2,187 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
||||||
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
|
|
||||||
|
|
||||||
class SoftwareRasterizer implements Rasterizer {
|
class SoftwareRasterizer extends Rasterizer {
|
||||||
// Pre-calculated VGA colors for ceiling and floor
|
late FrameBuffer _buffer;
|
||||||
final int ceilingColor = ColorPalette.vga32Bit[25];
|
late WolfEngine _engine;
|
||||||
final int floorColor = ColorPalette.vga32Bit[29];
|
|
||||||
|
// Intercept the base render call to store our references
|
||||||
|
@override
|
||||||
|
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||||
|
_engine = engine;
|
||||||
|
_buffer = buffer;
|
||||||
|
return super.render(engine, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void render(WolfEngine engine, FrameBuffer buffer) {
|
void prepareFrame(WolfEngine engine) {
|
||||||
_clearScreen(buffer);
|
// Top half is ceiling color (25), bottom half is floor color (29)
|
||||||
|
int ceilingColor = ColorPalette.vga32Bit[25];
|
||||||
|
int floorColor = ColorPalette.vga32Bit[29];
|
||||||
|
|
||||||
List<double> zBuffer = List.filled(buffer.width, 0.0);
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
|
int color = (y < viewHeight / 2) ? ceilingColor : floorColor;
|
||||||
_castWalls(engine, buffer, zBuffer);
|
for (int x = 0; x < width; x++) {
|
||||||
_castSprites(engine, buffer, zBuffer);
|
_buffer.pixels[y * width + x] = color;
|
||||||
|
|
||||||
// NEW: Draw the weapon on top of the 3D world
|
|
||||||
_drawWeapon(engine, buffer);
|
|
||||||
|
|
||||||
// NEW: Apply the full-screen damage tint last
|
|
||||||
_drawDamageFlash(engine, buffer);
|
|
||||||
|
|
||||||
_drawHud(engine, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _clearScreen(FrameBuffer buffer) {
|
|
||||||
const int viewHeight = 160;
|
|
||||||
int halfScreen = (buffer.width * viewHeight) ~/ 2;
|
|
||||||
// Only clear the top 160 rows!
|
|
||||||
buffer.pixels.fillRange(0, halfScreen, ceilingColor);
|
|
||||||
buffer.pixels.fillRange(halfScreen, buffer.width * viewHeight, floorColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _castWalls(WolfEngine engine, FrameBuffer buffer, List<double> zBuffer) {
|
|
||||||
final double fov = math.pi / 3;
|
|
||||||
final Player player = engine.player;
|
|
||||||
final SpriteMap map = engine.currentLevel;
|
|
||||||
|
|
||||||
// Fetch dynamic states from the managers
|
|
||||||
final Map<String, double> doorOffsets = engine.doorManager
|
|
||||||
.getOffsetsForRenderer();
|
|
||||||
final Pushwall? activePushwall = engine.pushwallManager.activePushwall;
|
|
||||||
|
|
||||||
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 < buffer.width; x++) {
|
|
||||||
double cameraX = 2 * x / buffer.width - 1.0;
|
|
||||||
Coordinate2D rayDir = dir + (plane * cameraX);
|
|
||||||
|
|
||||||
int mapX = player.x.toInt();
|
|
||||||
int mapY = player.y.toInt();
|
|
||||||
|
|
||||||
double sideDistX;
|
|
||||||
double sideDistY;
|
|
||||||
double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs();
|
|
||||||
double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs();
|
|
||||||
double perpWallDist = 0.0;
|
|
||||||
|
|
||||||
int stepX;
|
|
||||||
int stepY;
|
|
||||||
bool hit = false;
|
|
||||||
bool hitOutOfBounds = false;
|
|
||||||
int side = 0;
|
|
||||||
int hitWallId = 0;
|
|
||||||
|
|
||||||
double textureOffset = 0.0;
|
|
||||||
bool customDistCalculated = false;
|
|
||||||
Set<String> 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) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hit = true;
|
|
||||||
hitWallId = map[mapY][mapX];
|
|
||||||
textureOffset = currentOffset;
|
|
||||||
}
|
|
||||||
// PUSHWALL LOGIC
|
|
||||||
else if (activePushwall != null &&
|
|
||||||
mapX == activePushwall.x &&
|
|
||||||
mapY == activePushwall.y) {
|
|
||||||
hit = true;
|
|
||||||
hitWallId = map[mapY][mapX];
|
|
||||||
|
|
||||||
double pOffset = activePushwall.offset;
|
|
||||||
int pDirX = activePushwall.dirX;
|
|
||||||
int pDirY = activePushwall.dirY;
|
|
||||||
|
|
||||||
perpWallDist = (side == 0)
|
|
||||||
? (sideDistX - deltaDistX)
|
|
||||||
: (sideDistY - deltaDistY);
|
|
||||||
|
|
||||||
if (side == 0 && pDirX != 0) {
|
|
||||||
if (pDirX == stepX) {
|
|
||||||
double intersect = perpWallDist + pOffset * deltaDistX;
|
|
||||||
if (intersect < sideDistY) {
|
|
||||||
perpWallDist = intersect;
|
|
||||||
} else {
|
|
||||||
side = 1;
|
|
||||||
perpWallDist = sideDistY - deltaDistY;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
perpWallDist -= (1.0 - pOffset) * deltaDistX;
|
|
||||||
}
|
|
||||||
} else if (side == 1 && pDirY != 0) {
|
|
||||||
if (pDirY == stepY) {
|
|
||||||
double intersect = perpWallDist + pOffset * deltaDistY;
|
|
||||||
if (intersect < sideDistX) {
|
|
||||||
perpWallDist = intersect;
|
|
||||||
} else {
|
|
||||||
side = 0;
|
|
||||||
perpWallDist = sideDistX - deltaDistX;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
perpWallDist -= (1.0 - pOffset) * deltaDistY;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
double wallFraction = (side == 0)
|
|
||||||
? player.y + perpWallDist * rayDir.y
|
|
||||||
: player.x + perpWallDist * rayDir.x;
|
|
||||||
wallFraction -= wallFraction.floor();
|
|
||||||
|
|
||||||
if (side == 0) {
|
|
||||||
if (pDirY == 1 && wallFraction < pOffset) hit = false;
|
|
||||||
if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
|
||||||
if (hit) textureOffset = pOffset * pDirY;
|
|
||||||
} else {
|
|
||||||
if (pDirX == 1 && wallFraction < pOffset) hit = false;
|
|
||||||
if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
|
||||||
if (hit) textureOffset = pOffset * pDirX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hit) continue;
|
|
||||||
customDistCalculated = true;
|
|
||||||
}
|
|
||||||
// STANDARD WALL
|
|
||||||
else {
|
|
||||||
hit = true;
|
|
||||||
hitWallId = map[mapY][mapX];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hitOutOfBounds) continue;
|
|
||||||
|
|
||||||
if (!customDistCalculated) {
|
|
||||||
if (side == 0) {
|
|
||||||
perpWallDist = (sideDistX - deltaDistX);
|
|
||||||
} else {
|
|
||||||
perpWallDist = (sideDistY - deltaDistY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the distance so sprites know if they are occluded
|
|
||||||
zBuffer[x] = perpWallDist;
|
|
||||||
|
|
||||||
double wallX = (side == 0)
|
|
||||||
? player.y + perpWallDist * rayDir.y
|
|
||||||
: player.x + perpWallDist * rayDir.x;
|
|
||||||
wallX -= wallX.floor();
|
|
||||||
|
|
||||||
_drawTexturedColumn(
|
|
||||||
x,
|
|
||||||
perpWallDist,
|
|
||||||
wallX,
|
|
||||||
side,
|
|
||||||
hitWallId,
|
|
||||||
engine.data.walls, // Wall textures
|
|
||||||
textureOffset,
|
|
||||||
buffer,
|
|
||||||
player.angle,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawTexturedColumn(
|
@override
|
||||||
|
void drawWallColumn(
|
||||||
int x,
|
int x,
|
||||||
double distance,
|
int drawStart,
|
||||||
double wallX,
|
int drawEnd,
|
||||||
|
int columnHeight,
|
||||||
|
Sprite texture,
|
||||||
|
int texX,
|
||||||
|
double perpWallDist,
|
||||||
int side,
|
int side,
|
||||||
int hitWallId,
|
|
||||||
List<Sprite> textures,
|
|
||||||
double textureOffset,
|
|
||||||
FrameBuffer buffer,
|
|
||||||
double playerAngle,
|
|
||||||
) {
|
) {
|
||||||
const int viewHeight = 160;
|
|
||||||
if (distance <= 0.01) distance = 0.01;
|
|
||||||
|
|
||||||
int lineHeight = (viewHeight / distance).toInt();
|
|
||||||
|
|
||||||
int drawStart = -lineHeight ~/ 2 + viewHeight ~/ 2;
|
|
||||||
if (drawStart < 0) drawStart = 0;
|
|
||||||
|
|
||||||
int drawEnd = lineHeight ~/ 2 + viewHeight ~/ 2;
|
|
||||||
if (drawEnd >= viewHeight) drawEnd = viewHeight - 1;
|
|
||||||
|
|
||||||
int texNum;
|
|
||||||
if (hitWallId >= 90) {
|
|
||||||
texNum = 98.clamp(0, textures.length - 1);
|
|
||||||
} else {
|
|
||||||
texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2);
|
|
||||||
if (side == 1) texNum += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63);
|
|
||||||
if (side == 0 && math.cos(playerAngle) > 0) texX = 63 - texX;
|
|
||||||
if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX;
|
|
||||||
|
|
||||||
double step = 64.0 / lineHeight;
|
|
||||||
double texPos = (drawStart - viewHeight / 2 + lineHeight / 2) * step;
|
|
||||||
|
|
||||||
Sprite texture = textures[texNum];
|
|
||||||
|
|
||||||
for (int y = drawStart; y < drawEnd; y++) {
|
for (int y = drawStart; y < drawEnd; y++) {
|
||||||
int texY = texPos.toInt() & 63;
|
// Calculate which Y pixel of the texture to sample
|
||||||
texPos += step;
|
double relativeY =
|
||||||
|
(y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
|
||||||
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||||
|
|
||||||
int colorByte = texture.pixels[texX * 64 + texY];
|
int colorByte = texture.pixels[texX * 64 + texY];
|
||||||
int color32 = ColorPalette.vga32Bit[colorByte];
|
int pixelColor = ColorPalette.vga32Bit[colorByte];
|
||||||
|
|
||||||
buffer.pixels[y * buffer.width + x] = color32;
|
// Darken Y-side walls for faux directional lighting
|
||||||
|
if (side == 1) {
|
||||||
|
pixelColor = _shadeColor(pixelColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
_buffer.pixels[y * width + x] = pixelColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _castSprites(
|
@override
|
||||||
WolfEngine engine,
|
void drawSpriteStripe(
|
||||||
FrameBuffer buffer,
|
int stripeX,
|
||||||
List<double> zBuffer,
|
int drawStartY,
|
||||||
|
int drawEndY,
|
||||||
|
int spriteHeight,
|
||||||
|
Sprite texture,
|
||||||
|
int texX,
|
||||||
|
double transformY,
|
||||||
) {
|
) {
|
||||||
const int viewHeight = 160;
|
for (
|
||||||
final Player player = engine.player;
|
int y = math.max(0, drawStartY);
|
||||||
final List<Entity> activeSprites = List.from(engine.entities);
|
y < math.min(viewHeight, drawEndY);
|
||||||
|
y++
|
||||||
|
) {
|
||||||
|
double relativeY = (y - drawStartY) / spriteHeight;
|
||||||
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||||
|
|
||||||
// Sort entities from furthest to closest (Painter's Algorithm)
|
int colorByte = texture.pixels[texX * 64 + texY];
|
||||||
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(
|
// 255 is the "transparent" color index in VGA Wolfenstein
|
||||||
math.cos(player.angle),
|
if (colorByte != 255) {
|
||||||
math.sin(player.angle),
|
_buffer.pixels[y * width + stripeX] = ColorPalette.vga32Bit[colorByte];
|
||||||
);
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (transformY > 0) {
|
|
||||||
int spriteScreenX = ((buffer.width / 2) * (1 + transformX / transformY))
|
|
||||||
.toInt();
|
|
||||||
int spriteHeight = (viewHeight / transformY).abs().toInt();
|
|
||||||
|
|
||||||
// In 1x1 buffer pixels, the width of the sprite is equal to its height
|
|
||||||
int spriteWidth = spriteHeight;
|
|
||||||
|
|
||||||
int drawStartY = -spriteHeight ~/ 2 + viewHeight ~/ 2;
|
|
||||||
if (drawStartY < 0) drawStartY = 0;
|
|
||||||
|
|
||||||
int drawEndY = spriteHeight ~/ 2 + viewHeight ~/ 2;
|
|
||||||
if (drawEndY >= buffer.height) drawEndY = viewHeight - 1;
|
|
||||||
|
|
||||||
int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
|
|
||||||
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
|
|
||||||
|
|
||||||
int clipStartX = math.max(0, drawStartX);
|
|
||||||
int clipEndX = math.min(buffer.width - 1, drawEndX);
|
|
||||||
|
|
||||||
int safeIndex = entity.spriteIndex.clamp(
|
|
||||||
0,
|
|
||||||
engine.data.sprites.length - 1,
|
|
||||||
);
|
|
||||||
Sprite spritePixels = engine.data.sprites[safeIndex];
|
|
||||||
|
|
||||||
for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
|
|
||||||
// Z-Buffer Check! Only draw the vertical stripe if it's in front of the wall
|
|
||||||
if (transformY < zBuffer[stripe]) {
|
|
||||||
int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63);
|
|
||||||
|
|
||||||
double step = 64.0 / spriteHeight;
|
|
||||||
double texPos =
|
|
||||||
(drawStartY - viewHeight / 2 + spriteHeight / 2) * step;
|
|
||||||
|
|
||||||
for (int y = drawStartY; y < drawEndY; y++) {
|
|
||||||
int texY = texPos.toInt() & 63;
|
|
||||||
texPos += step;
|
|
||||||
|
|
||||||
int colorByte = spritePixels.pixels[texX * 64 + texY];
|
|
||||||
|
|
||||||
if (colorByte != 255) {
|
|
||||||
// 255 is transparent
|
|
||||||
buffer.pixels[y * buffer.width + stripe] =
|
|
||||||
ColorPalette.vga32Bit[colorByte];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawWeapon(WolfEngine engine, FrameBuffer buffer) {
|
@override
|
||||||
const int viewHeight = 160;
|
void drawWeapon(WolfEngine engine) {
|
||||||
int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
|
int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
|
||||||
engine.data.sprites.length,
|
engine.data.sprites.length,
|
||||||
);
|
);
|
||||||
Sprite weaponSprite = engine.data.sprites[spriteIndex];
|
Sprite weaponSprite = engine.data.sprites[spriteIndex];
|
||||||
|
|
||||||
// Dropped the scale from 4 to 2 (a 50% reduction in size)
|
int weaponWidth = (width * 0.5).toInt();
|
||||||
const int scale = 2;
|
int weaponHeight = (viewHeight * 0.8).toInt();
|
||||||
const int weaponWidth = 64 * scale;
|
|
||||||
const int weaponHeight = 64 * scale;
|
|
||||||
|
|
||||||
int startX = (buffer.width ~/ 2) - (weaponWidth ~/ 2);
|
int startX = (width ~/ 2) - (weaponWidth ~/ 2);
|
||||||
|
|
||||||
// Kept the grounding to the bottom of the screen
|
|
||||||
int startY =
|
int startY =
|
||||||
viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 2);
|
viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 4);
|
||||||
|
|
||||||
for (int x = 0; x < 64; x++) {
|
for (int dy = 0; dy < weaponHeight; dy++) {
|
||||||
for (int y = 0; y < 64; y++) {
|
for (int dx = 0; dx < weaponWidth; dx++) {
|
||||||
int colorByte = weaponSprite.pixels[x * 64 + y];
|
int texX = (dx * 64 ~/ weaponWidth).clamp(0, 63);
|
||||||
|
int texY = (dy * 64 ~/ weaponHeight).clamp(0, 63);
|
||||||
|
|
||||||
|
int colorByte = weaponSprite.pixels[texX * 64 + texY];
|
||||||
if (colorByte != 255) {
|
if (colorByte != 255) {
|
||||||
int color32 = ColorPalette.vga32Bit[colorByte];
|
int drawX = startX + dx;
|
||||||
|
int drawY = startY + dy;
|
||||||
for (int sx = 0; sx < scale; sx++) {
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
|
||||||
for (int sy = 0; sy < scale; sy++) {
|
_buffer.pixels[drawY * width + drawX] =
|
||||||
int drawX = startX + (x * scale) + sx;
|
ColorPalette.vga32Bit[colorByte];
|
||||||
int drawY = startY + (y * scale) + sy;
|
|
||||||
|
|
||||||
if (drawX >= 0 &&
|
|
||||||
drawX < buffer.width &&
|
|
||||||
drawY >= 0 &&
|
|
||||||
drawY < buffer.height) {
|
|
||||||
buffer.pixels[drawY * buffer.width + drawX] = color32;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawDamageFlash(WolfEngine engine, FrameBuffer buffer) {
|
@override
|
||||||
if (engine.damageFlashOpacity <= 0) return;
|
void drawHud(WolfEngine engine) {
|
||||||
|
int statusBarIndex = engine.data.vgaImages.indexWhere(
|
||||||
|
(img) => img.width == 320 && img.height == 40,
|
||||||
|
);
|
||||||
|
if (statusBarIndex == -1) return;
|
||||||
|
|
||||||
int alpha = (engine.damageFlashOpacity * 256).toInt().clamp(0, 256);
|
// 1. Draw Background
|
||||||
int invAlpha = 256 - alpha;
|
_blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160);
|
||||||
|
|
||||||
for (int i = 0; i < buffer.pixels.length; i++) {
|
// 2. Draw Stats (100% mathematically accurate right-aligned coordinates)
|
||||||
int color = buffer.pixels[i];
|
_drawNumber(1, 32, 176, engine.data.vgaImages); // Floor
|
||||||
|
_drawNumber(engine.player.score, 96, 176, engine.data.vgaImages); // Score
|
||||||
|
_drawNumber(3, 120, 176, engine.data.vgaImages); // Lives
|
||||||
|
_drawNumber(
|
||||||
|
engine.player.health,
|
||||||
|
192,
|
||||||
|
176,
|
||||||
|
engine.data.vgaImages,
|
||||||
|
); // Health
|
||||||
|
_drawNumber(engine.player.ammo, 232, 176, engine.data.vgaImages); // Ammo
|
||||||
|
|
||||||
int r = color & 0xFF;
|
// 3. Draw BJ's Face & Current Weapon
|
||||||
int g = (color >> 8) & 0xFF;
|
_drawFace(engine);
|
||||||
int b = (color >> 16) & 0xFF;
|
_drawWeaponIcon(engine);
|
||||||
int a = (color >> 24) & 0xFF;
|
}
|
||||||
|
|
||||||
// Blend with Red
|
@override
|
||||||
r = ((r * invAlpha) + (255 * alpha)) >> 8;
|
FrameBuffer finalizeFrame() {
|
||||||
g = (g * invAlpha) >> 8;
|
// If the player took damage, overlay a red tint across the 3D view
|
||||||
b = (b * invAlpha) >> 8;
|
if (_engine.player.damageFlash > 0) {
|
||||||
|
_applyDamageFlash();
|
||||||
|
}
|
||||||
|
return _buffer; // Return the fully painted pixel array
|
||||||
|
}
|
||||||
|
|
||||||
buffer.pixels[i] = (a << 24) | (b << 16) | (g << 8) | r;
|
// ===========================================================================
|
||||||
|
// PRIVATE HELPER METHODS
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
/// Maps the planar VGA image data directly to 32-bit pixels.
|
||||||
|
/// (Assuming a 1:1 scale, which is standard for the 320x200 software renderer).
|
||||||
|
void _blitVgaImage(VgaImage image, int startX, int startY) {
|
||||||
|
int planeWidth = image.width ~/ 4;
|
||||||
|
int planeSize = planeWidth * image.height;
|
||||||
|
|
||||||
|
for (int dy = 0; dy < image.height; dy++) {
|
||||||
|
for (int dx = 0; dx < image.width; dx++) {
|
||||||
|
int drawX = startX + dx;
|
||||||
|
int drawY = startY + dy;
|
||||||
|
|
||||||
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) {
|
||||||
|
int srcX = dx.clamp(0, image.width - 1);
|
||||||
|
int srcY = dy.clamp(0, image.height - 1);
|
||||||
|
|
||||||
|
int plane = srcX % 4;
|
||||||
|
int sx = srcX ~/ 4;
|
||||||
|
int index = (plane * planeSize) + (srcY * planeWidth) + sx;
|
||||||
|
|
||||||
|
int colorByte = image.pixels[index];
|
||||||
|
if (colorByte != 255) {
|
||||||
|
_buffer.pixels[drawY * width + drawX] =
|
||||||
|
ColorPalette.vga32Bit[colorByte];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,26 +190,22 @@ class SoftwareRasterizer implements Rasterizer {
|
|||||||
int value,
|
int value,
|
||||||
int rightAlignX,
|
int rightAlignX,
|
||||||
int startY,
|
int startY,
|
||||||
FrameBuffer buffer,
|
|
||||||
List<VgaImage> vgaImages,
|
List<VgaImage> vgaImages,
|
||||||
) {
|
) {
|
||||||
// The yellow 8x16 HUD digits start exactly at 96
|
|
||||||
const int zeroIndex = 96;
|
const int zeroIndex = 96;
|
||||||
String numStr = value.toString();
|
String numStr = value.toString();
|
||||||
|
|
||||||
int currentX = rightAlignX - (numStr.length * 8);
|
int currentX = rightAlignX - (numStr.length * 8);
|
||||||
|
|
||||||
for (int i = 0; i < numStr.length; i++) {
|
for (int i = 0; i < numStr.length; i++) {
|
||||||
int digit = int.parse(numStr[i]);
|
int digit = int.parse(numStr[i]);
|
||||||
|
|
||||||
if (zeroIndex + digit < vgaImages.length) {
|
if (zeroIndex + digit < vgaImages.length) {
|
||||||
_blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY, buffer);
|
_blitVgaImage(vgaImages[zeroIndex + digit], currentX, startY);
|
||||||
}
|
}
|
||||||
currentX += 8;
|
currentX += 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawFace(WolfEngine engine, FrameBuffer buffer) {
|
void _drawFace(WolfEngine engine) {
|
||||||
int health = engine.player.health;
|
int health = engine.player.health;
|
||||||
int faceIndex;
|
int faceIndex;
|
||||||
|
|
||||||
@@ -464,12 +217,11 @@ class SoftwareRasterizer implements Rasterizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (faceIndex < engine.data.vgaImages.length) {
|
if (faceIndex < engine.data.vgaImages.length) {
|
||||||
// Exactly X=136. This will perfectly smother the background face.
|
_blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164);
|
||||||
_blitVgaImage(engine.data.vgaImages[faceIndex], 136, 164, buffer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawWeaponIcon(WolfEngine engine, FrameBuffer buffer) {
|
void _drawWeaponIcon(WolfEngine engine) {
|
||||||
int weaponIndex = 89; // Default to Pistol
|
int weaponIndex = 89; // Default to Pistol
|
||||||
|
|
||||||
if (engine.player.hasChainGun) {
|
if (engine.player.hasChainGun) {
|
||||||
@@ -479,84 +231,41 @@ class SoftwareRasterizer implements Rasterizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (weaponIndex < engine.data.vgaImages.length) {
|
if (weaponIndex < engine.data.vgaImages.length) {
|
||||||
// Exactly X=256
|
_blitVgaImage(engine.data.vgaImages[weaponIndex], 256, 164);
|
||||||
_blitVgaImage(engine.data.vgaImages[weaponIndex], 256, 164, buffer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _drawHud(WolfEngine engine, FrameBuffer buffer) {
|
/// Darkens a 32-bit 0xAABBGGRR color by roughly 30% without touching Alpha
|
||||||
int statusBarIndex = engine.data.vgaImages.indexWhere(
|
int _shadeColor(int color) {
|
||||||
(img) => img.width == 320 && img.height == 40,
|
int r = (color & 0xFF) * 7 ~/ 10;
|
||||||
);
|
int g = ((color >> 8) & 0xFF) * 7 ~/ 10;
|
||||||
|
int b = ((color >> 16) & 0xFF) * 7 ~/ 10;
|
||||||
if (statusBarIndex == -1) return;
|
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||||
|
|
||||||
// 1. Draw Background
|
|
||||||
_blitVgaImage(engine.data.vgaImages[statusBarIndex], 0, 160, buffer);
|
|
||||||
|
|
||||||
// 2. Draw Stats (100% mathematically accurate right-aligned coordinates)
|
|
||||||
_drawNumber(1, 32, 176, buffer, engine.data.vgaImages); // Floor
|
|
||||||
_drawNumber(
|
|
||||||
engine.player.score,
|
|
||||||
96,
|
|
||||||
176,
|
|
||||||
buffer,
|
|
||||||
engine.data.vgaImages,
|
|
||||||
); // Score
|
|
||||||
_drawNumber(3, 120, 176, buffer, engine.data.vgaImages); // Lives
|
|
||||||
_drawNumber(
|
|
||||||
engine.player.health,
|
|
||||||
192,
|
|
||||||
176,
|
|
||||||
buffer,
|
|
||||||
engine.data.vgaImages,
|
|
||||||
); // Health
|
|
||||||
_drawNumber(
|
|
||||||
engine.player.ammo,
|
|
||||||
232,
|
|
||||||
176,
|
|
||||||
buffer,
|
|
||||||
engine.data.vgaImages,
|
|
||||||
); // Ammo
|
|
||||||
|
|
||||||
// 3. Draw BJ's Face & Current Weapon
|
|
||||||
_drawFace(engine, buffer);
|
|
||||||
_drawWeaponIcon(engine, buffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _blitVgaImage(
|
/// Tints the top 80% of the screen red based on player.damageFlash intensity
|
||||||
VgaImage image,
|
void _applyDamageFlash() {
|
||||||
int startX,
|
// Grab the intensity (0.0 to 1.0)
|
||||||
int startY,
|
double intensity = _engine.player.damageFlash;
|
||||||
FrameBuffer buffer,
|
|
||||||
) {
|
|
||||||
// Wolfenstein 3D VGA images are stored in "Mode Y" Planar format.
|
|
||||||
// We must de-interleave the 4 planes to draw them correctly!
|
|
||||||
int planeWidth = image.width ~/ 4;
|
|
||||||
int planeSize = planeWidth * image.height;
|
|
||||||
|
|
||||||
for (int y = 0; y < image.height; y++) {
|
// Calculate how much to boost red and drop green/blue
|
||||||
for (int x = 0; x < image.width; x++) {
|
int redBoost = (150 * intensity).toInt();
|
||||||
int drawX = startX + x;
|
double colorDrop = 1.0 - (0.5 * intensity);
|
||||||
int drawY = startY + y;
|
|
||||||
|
|
||||||
if (drawX >= 0 &&
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
drawX < buffer.width &&
|
for (int x = 0; x < width; x++) {
|
||||||
drawY >= 0 &&
|
int index = y * width + x;
|
||||||
drawY < buffer.height) {
|
int color = _buffer.pixels[index];
|
||||||
// Planar to Linear coordinate conversion
|
|
||||||
int plane = x % 4;
|
|
||||||
int sx = x ~/ 4;
|
|
||||||
int index = (plane * planeSize) + (y * planeWidth) + sx;
|
|
||||||
|
|
||||||
int colorByte = image.pixels[index];
|
int r = color & 0xFF;
|
||||||
|
int g = (color >> 8) & 0xFF;
|
||||||
|
int b = (color >> 16) & 0xFF;
|
||||||
|
|
||||||
if (colorByte != 255) {
|
r = (r + redBoost).clamp(0, 255);
|
||||||
// 255 is transparent
|
g = (g * colorDrop).toInt();
|
||||||
buffer.pixels[drawY * buffer.width + drawX] =
|
b = (b * colorDrop).toInt();
|
||||||
ColorPalette.vga32Bit[colorByte];
|
|
||||||
}
|
_buffer.pixels[index] = (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ class WolfEngine {
|
|||||||
int _currentLevelIndex = 0;
|
int _currentLevelIndex = 0;
|
||||||
int? _returnLevelIndex;
|
int? _returnLevelIndex;
|
||||||
|
|
||||||
double damageFlashOpacity = 0.0;
|
|
||||||
bool isInitialized = false;
|
bool isInitialized = false;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
@@ -57,8 +56,8 @@ class WolfEngine {
|
|||||||
|
|
||||||
doorManager.update(elapsed);
|
doorManager.update(elapsed);
|
||||||
pushwallManager.update(elapsed, currentLevel);
|
pushwallManager.update(elapsed, currentLevel);
|
||||||
|
player.tick(elapsed);
|
||||||
|
|
||||||
player.updateWeaponSwitch();
|
|
||||||
player.angle += inputResult.dAngle;
|
player.angle += inputResult.dAngle;
|
||||||
|
|
||||||
if (player.angle < 0) player.angle += 2 * math.pi;
|
if (player.angle < 0) player.angle += 2 * math.pi;
|
||||||
@@ -74,10 +73,6 @@ class WolfEngine {
|
|||||||
|
|
||||||
_updateEntities(elapsed);
|
_updateEntities(elapsed);
|
||||||
|
|
||||||
if (damageFlashOpacity > 0) {
|
|
||||||
damageFlashOpacity = math.max(0.0, damageFlashOpacity - 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
player.updateWeapon(
|
player.updateWeapon(
|
||||||
currentTime: elapsed.inMilliseconds,
|
currentTime: elapsed.inMilliseconds,
|
||||||
entities: entities,
|
entities: entities,
|
||||||
@@ -87,7 +82,6 @@ class WolfEngine {
|
|||||||
|
|
||||||
void _loadLevel() {
|
void _loadLevel() {
|
||||||
entities.clear();
|
entities.clear();
|
||||||
damageFlashOpacity = 0.0;
|
|
||||||
|
|
||||||
final episode = data.episodes[_currentEpisodeIndex];
|
final episode = data.episodes[_currentEpisodeIndex];
|
||||||
activeLevel = episode.levels[_currentLevelIndex];
|
activeLevel = episode.levels[_currentLevelIndex];
|
||||||
@@ -279,7 +273,6 @@ class WolfEngine {
|
|||||||
tryOpenDoor: doorManager.tryOpenDoor,
|
tryOpenDoor: doorManager.tryOpenDoor,
|
||||||
onDamagePlayer: (int damage) {
|
onDamagePlayer: (int damage) {
|
||||||
player.takeDamage(damage);
|
player.takeDamage(damage);
|
||||||
damageFlashOpacity = 0.5;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:math' as math;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||||
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
||||||
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
|
|
||||||
|
|
||||||
class ColoredChar {
|
class ColoredChar {
|
||||||
final String char;
|
final String char;
|
||||||
@@ -11,231 +10,329 @@ class ColoredChar {
|
|||||||
ColoredChar(this.char, this.color);
|
ColoredChar(this.char, this.color);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AsciiRasterizer {
|
class AsciiRasterizer extends Rasterizer {
|
||||||
static const String _charset = "@%#*+=-:. ";
|
static const String _charset = "@%#*+=-:. ";
|
||||||
|
|
||||||
// NEW: Helper to safely convert and artificially boost your raw memory colors
|
late List<List<ColoredChar>> _screen;
|
||||||
|
late WolfEngine _engine;
|
||||||
|
|
||||||
|
// Terminal characters are usually twice as tall as they are wide.
|
||||||
|
// We override the base multiplier to squish sprites horizontally.
|
||||||
|
@override
|
||||||
|
double get aspectMultiplier => 0.6;
|
||||||
|
|
||||||
|
// --- HELPER: Color Conversion ---
|
||||||
Color _vgaToColor(int vgaColor, {double brightnessBoost = 2.0}) {
|
Color _vgaToColor(int vgaColor, {double brightnessBoost = 2.0}) {
|
||||||
int r = vgaColor & 0xFF;
|
int r = vgaColor & 0xFF;
|
||||||
int g = (vgaColor >> 8) & 0xFF;
|
int g = (vgaColor >> 8) & 0xFF;
|
||||||
int b = (vgaColor >> 16) & 0xFF;
|
int b = (vgaColor >> 16) & 0xFF;
|
||||||
|
|
||||||
// Apply the boost and clamp to 255 to prevent color overflow
|
|
||||||
r = (r * brightnessBoost).toInt().clamp(0, 255);
|
r = (r * brightnessBoost).toInt().clamp(0, 255);
|
||||||
g = (g * brightnessBoost).toInt().clamp(0, 255);
|
g = (g * brightnessBoost).toInt().clamp(0, 255);
|
||||||
b = (b * brightnessBoost).toInt().clamp(0, 255);
|
b = (b * brightnessBoost).toInt().clamp(0, 255);
|
||||||
|
|
||||||
// Force Alpha to 255 (fully opaque)
|
|
||||||
return Color.fromARGB(255, r, g, b);
|
return Color.fromARGB(255, r, g, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<List<ColoredChar>> render(WolfEngine engine, FrameBuffer framebuffer) {
|
// Intercept the base render call to initialize our text grid
|
||||||
final int width = framebuffer.width;
|
@override
|
||||||
final int height = framebuffer.height;
|
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||||
|
_engine = engine;
|
||||||
|
_screen = List.generate(
|
||||||
|
buffer.height,
|
||||||
|
(_) => List.filled(buffer.width, ColoredChar(' ', Colors.black)),
|
||||||
|
);
|
||||||
|
return super.render(engine, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
// Grab ceiling and floor colors from the original palette
|
@override
|
||||||
|
void prepareFrame(WolfEngine engine) {
|
||||||
final Color ceilingColor = _vgaToColor(ColorPalette.vga32Bit[25]);
|
final Color ceilingColor = _vgaToColor(ColorPalette.vga32Bit[25]);
|
||||||
final Color floorColor = _vgaToColor(ColorPalette.vga32Bit[29]);
|
final Color floorColor = _vgaToColor(ColorPalette.vga32Bit[29]);
|
||||||
|
|
||||||
final List<List<ColoredChar>> screen = List.generate(
|
for (int y = 0; y < height; y++) {
|
||||||
height,
|
for (int x = 0; x < width; x++) {
|
||||||
(_) => List.filled(width, ColoredChar(' ', ceilingColor)),
|
if (y < viewHeight / 2) {
|
||||||
);
|
_screen[y][x] = ColoredChar(' ', ceilingColor);
|
||||||
|
} else if (y < viewHeight) {
|
||||||
final List<double> zBuffer = List.filled(width, 0.0);
|
_screen[y][x] = ColoredChar('.', floorColor);
|
||||||
|
|
||||||
final Player player = engine.player;
|
|
||||||
final SpriteMap map = engine.currentLevel;
|
|
||||||
final List<Sprite> wallTextures = engine.data.walls;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 1. CAST WALLS
|
|
||||||
for (int x = 0; x < width; x++) {
|
|
||||||
double cameraX = 2 * x / width - 1.0;
|
|
||||||
Coordinate2D rayDir = dir + (plane * cameraX);
|
|
||||||
|
|
||||||
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;
|
|
||||||
int stepX, stepY, side = 0, hitWallId = 0;
|
|
||||||
bool hit = false;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (map[mapY][mapX] > 0) {
|
|
||||||
hit = true;
|
|
||||||
hitWallId = map[mapY][mapX];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double perpWallDist = (side == 0)
|
|
||||||
? (sideDistX - deltaDistX)
|
|
||||||
: (sideDistY - deltaDistY);
|
|
||||||
if (perpWallDist < 0.1) perpWallDist = 0.1;
|
|
||||||
|
|
||||||
zBuffer[x] = perpWallDist;
|
|
||||||
|
|
||||||
double wallX = (side == 0)
|
|
||||||
? player.y + perpWallDist * rayDir.y
|
|
||||||
: player.x + perpWallDist * rayDir.x;
|
|
||||||
wallX -= wallX.floor();
|
|
||||||
int texX = (wallX * 64).toInt().clamp(0, 63);
|
|
||||||
|
|
||||||
int texNum = ((hitWallId - 1) * 2).clamp(0, wallTextures.length - 2);
|
|
||||||
if (side == 1) texNum += 1;
|
|
||||||
Sprite texture = wallTextures[texNum];
|
|
||||||
|
|
||||||
int columnHeight = (height / perpWallDist).toInt();
|
|
||||||
int drawStart = (-columnHeight ~/ 2 + height ~/ 2).clamp(0, height);
|
|
||||||
int drawEnd = (columnHeight ~/ 2 + height ~/ 2).clamp(0, height);
|
|
||||||
|
|
||||||
double brightness = (1.5 / (perpWallDist + 1.0)).clamp(0.0, 1.0);
|
|
||||||
String wallChar =
|
|
||||||
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
|
|
||||||
0,
|
|
||||||
_charset.length - 1,
|
|
||||||
)];
|
|
||||||
|
|
||||||
for (int y = 0; y < height; y++) {
|
|
||||||
if (y >= drawStart && y < drawEnd) {
|
|
||||||
double relativeY = (y - drawStart) / (drawEnd - drawStart);
|
|
||||||
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
|
||||||
|
|
||||||
int colorByte = texture.pixels[texX * 64 + texY];
|
|
||||||
|
|
||||||
// Use our new color conversion!
|
|
||||||
Color pixelColor = _vgaToColor(ColorPalette.vga32Bit[colorByte]);
|
|
||||||
|
|
||||||
// Optional: slightly darken the Y-side walls for a faux-lighting effect
|
|
||||||
// if (side == 1) {
|
|
||||||
// pixelColor = Color.fromARGB(
|
|
||||||
// 255,
|
|
||||||
// (pixelColor.r * 0.7).toInt(),
|
|
||||||
// (pixelColor.g * 0.7).toInt(),
|
|
||||||
// (pixelColor.b * 0.7).toInt(),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
screen[y][x] = ColoredChar(wallChar, pixelColor);
|
|
||||||
} else if (y >= drawEnd) {
|
|
||||||
// Floor
|
|
||||||
screen[y][x] = ColoredChar('.', floorColor);
|
|
||||||
} else {
|
|
||||||
// Ceiling
|
|
||||||
screen[y][x] = ColoredChar(' ', ceilingColor);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. CAST SPRITES (Enemies/Items)
|
@override
|
||||||
final List<Entity> activeSprites = List.from(engine.entities);
|
void drawWallColumn(
|
||||||
activeSprites.sort((a, b) {
|
int x,
|
||||||
double distA = player.position.distanceTo(a.position);
|
int drawStart,
|
||||||
double distB = player.position.distanceTo(b.position);
|
int drawEnd,
|
||||||
return distB.compareTo(distA);
|
int columnHeight,
|
||||||
});
|
Sprite texture,
|
||||||
|
int texX,
|
||||||
for (Entity entity in activeSprites) {
|
double perpWallDist,
|
||||||
Coordinate2D spritePos = entity.position - player.position;
|
int side,
|
||||||
|
) {
|
||||||
double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y);
|
double brightness = (1.5 / (perpWallDist + 1.0)).clamp(0.0, 1.0);
|
||||||
double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y);
|
String wallChar =
|
||||||
double transformY =
|
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
|
||||||
invDet * (-plane.y * spritePos.x + plane.x * spritePos.y);
|
|
||||||
|
|
||||||
if (transformY > 0) {
|
|
||||||
int spriteScreenX = ((width / 2) * (1 + transformX / transformY))
|
|
||||||
.toInt();
|
|
||||||
int spriteHeight = (height / transformY).abs().toInt();
|
|
||||||
int spriteWidth = (spriteHeight * (width / height) * 0.6).toInt();
|
|
||||||
|
|
||||||
int drawStartY = -spriteHeight ~/ 2 + height ~/ 2;
|
|
||||||
int drawEndY = spriteHeight ~/ 2 + height ~/ 2;
|
|
||||||
int drawStartX = -spriteWidth ~/ 2 + spriteScreenX;
|
|
||||||
int drawEndX = spriteWidth ~/ 2 + spriteScreenX;
|
|
||||||
|
|
||||||
int clipStartX = math.max(0, drawStartX);
|
|
||||||
int clipEndX = math.min(width - 1, drawEndX);
|
|
||||||
|
|
||||||
int safeIndex = entity.spriteIndex.clamp(
|
|
||||||
0,
|
0,
|
||||||
engine.data.sprites.length - 1,
|
_charset.length - 1,
|
||||||
|
)];
|
||||||
|
|
||||||
|
for (int y = drawStart; y < drawEnd; y++) {
|
||||||
|
double relativeY =
|
||||||
|
(y - (-columnHeight ~/ 2 + viewHeight ~/ 2)) / columnHeight;
|
||||||
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||||
|
|
||||||
|
int colorByte = texture.pixels[texX * 64 + texY];
|
||||||
|
Color pixelColor = _vgaToColor(ColorPalette.vga32Bit[colorByte]);
|
||||||
|
|
||||||
|
// Faux directional lighting
|
||||||
|
if (side == 1) {
|
||||||
|
pixelColor = Color.fromARGB(
|
||||||
|
255,
|
||||||
|
(pixelColor.r * 0.9).toInt().clamp(0, 255),
|
||||||
|
(pixelColor.g * 0.9).toInt().clamp(0, 255),
|
||||||
|
(pixelColor.b * 0.9).toInt().clamp(0, 255),
|
||||||
);
|
);
|
||||||
Sprite spritePixels = engine.data.sprites[safeIndex];
|
}
|
||||||
|
|
||||||
double brightness = (1.5 / (transformY + 1.0)).clamp(0.0, 1.0);
|
_screen[y][x] = ColoredChar(wallChar, pixelColor);
|
||||||
String spriteChar =
|
}
|
||||||
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
|
}
|
||||||
0,
|
|
||||||
_charset.length - 1,
|
|
||||||
)];
|
|
||||||
|
|
||||||
for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
|
@override
|
||||||
if (transformY < zBuffer[stripe]) {
|
void drawSpriteStripe(
|
||||||
int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63);
|
int stripeX,
|
||||||
|
int drawStartY,
|
||||||
|
int drawEndY,
|
||||||
|
int spriteHeight,
|
||||||
|
Sprite texture,
|
||||||
|
int texX,
|
||||||
|
double transformY,
|
||||||
|
) {
|
||||||
|
double brightness = (1.5 / (transformY + 1.0)).clamp(0.0, 1.0);
|
||||||
|
String spriteChar =
|
||||||
|
_charset[((1.0 - brightness) * (_charset.length - 1)).toInt().clamp(
|
||||||
|
0,
|
||||||
|
_charset.length - 1,
|
||||||
|
)];
|
||||||
|
|
||||||
for (
|
for (
|
||||||
int y = math.max(0, drawStartY);
|
int y = math.max(0, drawStartY);
|
||||||
y < math.min(height, drawEndY);
|
y < math.min(viewHeight, drawEndY);
|
||||||
y++
|
y++
|
||||||
) {
|
) {
|
||||||
double relativeY = (y - drawStartY) / (drawEndY - drawStartY);
|
double relativeY = (y - drawStartY) / spriteHeight;
|
||||||
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||||
|
|
||||||
int colorByte = spritePixels.pixels[texX * 64 + texY];
|
int colorByte = texture.pixels[texX * 64 + texY];
|
||||||
|
if (colorByte != 255) {
|
||||||
|
_screen[y][stripeX] = ColoredChar(
|
||||||
|
spriteChar,
|
||||||
|
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (colorByte != 255) {
|
@override
|
||||||
// Apply the safe color conversion here as well
|
void drawWeapon(WolfEngine engine) {
|
||||||
Color pixelColor = _vgaToColor(
|
int spriteIndex = engine.player.currentWeapon.getCurrentSpriteIndex(
|
||||||
ColorPalette.vga32Bit[colorByte],
|
engine.data.sprites.length,
|
||||||
);
|
);
|
||||||
screen[y][stripe] = ColoredChar(spriteChar, pixelColor);
|
Sprite weaponSprite = engine.data.sprites[spriteIndex];
|
||||||
}
|
|
||||||
}
|
int weaponWidth = (width * 0.5).toInt();
|
||||||
|
int weaponHeight = (viewHeight * 0.8).toInt();
|
||||||
|
|
||||||
|
int startX = (width ~/ 2) - (weaponWidth ~/ 2);
|
||||||
|
int startY =
|
||||||
|
viewHeight - weaponHeight + (engine.player.weaponAnimOffset ~/ 4);
|
||||||
|
|
||||||
|
for (int dy = 0; dy < weaponHeight; dy++) {
|
||||||
|
for (int dx = 0; dx < weaponWidth; dx++) {
|
||||||
|
int texX = (dx * 64 ~/ weaponWidth).clamp(0, 63);
|
||||||
|
int texY = (dy * 64 ~/ weaponHeight).clamp(0, 63);
|
||||||
|
|
||||||
|
int colorByte = weaponSprite.pixels[texX * 64 + texY];
|
||||||
|
if (colorByte != 255) {
|
||||||
|
int drawX = startX + dx;
|
||||||
|
int drawY = startY + dy;
|
||||||
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
|
||||||
|
_screen[drawY][drawX] = ColoredChar(
|
||||||
|
'@',
|
||||||
|
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return screen;
|
@override
|
||||||
|
void drawHud(WolfEngine engine) {
|
||||||
|
int statusBarIndex = engine.data.vgaImages.indexWhere(
|
||||||
|
(img) => img.width == 320 && img.height == 40,
|
||||||
|
);
|
||||||
|
if (statusBarIndex == -1) return;
|
||||||
|
|
||||||
|
// 1. Draw Background
|
||||||
|
_blitVgaImageAscii(engine.data.vgaImages[statusBarIndex], 0, 160);
|
||||||
|
|
||||||
|
// 2. Draw Stats
|
||||||
|
_drawNumberAscii(1, 32, 176, engine.data.vgaImages); // Floor
|
||||||
|
_drawNumberAscii(
|
||||||
|
engine.player.score,
|
||||||
|
96,
|
||||||
|
176,
|
||||||
|
engine.data.vgaImages,
|
||||||
|
); // Score
|
||||||
|
_drawNumberAscii(3, 120, 176, engine.data.vgaImages); // Lives
|
||||||
|
_drawNumberAscii(
|
||||||
|
engine.player.health,
|
||||||
|
192,
|
||||||
|
176,
|
||||||
|
engine.data.vgaImages,
|
||||||
|
); // Health
|
||||||
|
_drawNumberAscii(
|
||||||
|
engine.player.ammo,
|
||||||
|
232,
|
||||||
|
176,
|
||||||
|
engine.data.vgaImages,
|
||||||
|
); // Ammo
|
||||||
|
|
||||||
|
// 3. Draw BJ's Face & Current Weapon
|
||||||
|
_drawFaceAscii(engine);
|
||||||
|
_drawWeaponIconAscii(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawNumberAscii(
|
||||||
|
int value,
|
||||||
|
int rightAlignX,
|
||||||
|
int startY,
|
||||||
|
List<VgaImage> vgaImages,
|
||||||
|
) {
|
||||||
|
const int zeroIndex = 96;
|
||||||
|
String numStr = value.toString();
|
||||||
|
int currentX = rightAlignX - (numStr.length * 8);
|
||||||
|
|
||||||
|
for (int i = 0; i < numStr.length; i++) {
|
||||||
|
int digit = int.parse(numStr[i]);
|
||||||
|
if (zeroIndex + digit < vgaImages.length) {
|
||||||
|
_blitVgaImageAscii(vgaImages[zeroIndex + digit], currentX, startY);
|
||||||
|
}
|
||||||
|
currentX += 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawFaceAscii(WolfEngine engine) {
|
||||||
|
int health = engine.player.health;
|
||||||
|
int faceIndex;
|
||||||
|
|
||||||
|
if (health <= 0) {
|
||||||
|
faceIndex = 127;
|
||||||
|
} else {
|
||||||
|
int healthTier = ((100 - health) ~/ 16).clamp(0, 6);
|
||||||
|
faceIndex = 106 + (healthTier * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faceIndex < engine.data.vgaImages.length) {
|
||||||
|
_blitVgaImageAscii(engine.data.vgaImages[faceIndex], 136, 164);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawWeaponIconAscii(WolfEngine engine) {
|
||||||
|
int weaponIndex = 89;
|
||||||
|
if (engine.player.hasChainGun) {
|
||||||
|
weaponIndex = 91;
|
||||||
|
} else if (engine.player.hasMachineGun) {
|
||||||
|
weaponIndex = 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weaponIndex < engine.data.vgaImages.length) {
|
||||||
|
_blitVgaImageAscii(engine.data.vgaImages[weaponIndex], 256, 164);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic finalizeFrame() {
|
||||||
|
if (_engine.player.damageFlash > 0.0) {
|
||||||
|
_applyDamageFlash();
|
||||||
|
}
|
||||||
|
return _screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PRIVATE HUD DRAWING HELPERS ---
|
||||||
|
|
||||||
|
void _blitVgaImageAscii(VgaImage image, int startX_320, int startY_200) {
|
||||||
|
int planeWidth = image.width ~/ 4;
|
||||||
|
int planeSize = planeWidth * image.height;
|
||||||
|
|
||||||
|
double scaleX = width / 320.0;
|
||||||
|
double scaleY = height / 200.0;
|
||||||
|
|
||||||
|
int destStartX = (startX_320 * scaleX).toInt();
|
||||||
|
int destStartY = (startY_200 * scaleY).toInt();
|
||||||
|
int destWidth = (image.width * scaleX).toInt();
|
||||||
|
int destHeight = (image.height * scaleY).toInt();
|
||||||
|
|
||||||
|
for (int dy = 0; dy < destHeight; dy++) {
|
||||||
|
for (int dx = 0; dx < destWidth; dx++) {
|
||||||
|
int drawX = destStartX + dx;
|
||||||
|
int drawY = destStartY + dy;
|
||||||
|
|
||||||
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) {
|
||||||
|
int srcX = (dx / scaleX).toInt().clamp(0, image.width - 1);
|
||||||
|
int srcY = (dy / scaleY).toInt().clamp(0, image.height - 1);
|
||||||
|
|
||||||
|
int plane = srcX % 4;
|
||||||
|
int sx = srcX ~/ 4;
|
||||||
|
int index = (plane * planeSize) + (srcY * planeWidth) + sx;
|
||||||
|
|
||||||
|
int colorByte = image.pixels[index];
|
||||||
|
if (colorByte != 255) {
|
||||||
|
// Using '█' for UI to make it look solid
|
||||||
|
_screen[drawY][drawX] = ColoredChar(
|
||||||
|
'█',
|
||||||
|
_vgaToColor(
|
||||||
|
ColorPalette.vga32Bit[colorByte],
|
||||||
|
brightnessBoost: 1.5,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DAMAGE FLASH ---
|
||||||
|
|
||||||
|
void _applyDamageFlash() {
|
||||||
|
double intensity = _engine.player.damageFlash;
|
||||||
|
int redBoost = (150 * intensity).toInt();
|
||||||
|
double colorDrop = 1.0 - (0.5 * intensity);
|
||||||
|
|
||||||
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
Color c = _screen[y][x].color;
|
||||||
|
|
||||||
|
int r = ((c.r * 255).round().clamp(0, 255) + redBoost).clamp(0, 255);
|
||||||
|
int g = ((c.g * 255).round().clamp(0, 255) * colorDrop).toInt().clamp(
|
||||||
|
0,
|
||||||
|
255,
|
||||||
|
);
|
||||||
|
int b = ((c.b * 255).round().clamp(0, 255) * colorDrop).toInt().clamp(
|
||||||
|
0,
|
||||||
|
255,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace the existing character with a red-tinted version
|
||||||
|
_screen[y][x] = ColoredChar(
|
||||||
|
_screen[y][x].char,
|
||||||
|
Color.fromARGB(255, r, g, b),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user