Fixed ASCII rasterizer, abstracted out input and audio, and created CLI client (untested)
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
96
bin/cli_main.dart
Normal file
96
bin/cli_main.dart
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:wolf_3d_data/wolf_3d_data.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_input/wolf_3d_input.dart';
|
||||||
|
import 'package:wolf_3d_synth/wolf_3d_synth.dart';
|
||||||
|
|
||||||
|
// Helper to gracefully exit and restore the terminal
|
||||||
|
void exitCleanly(int code) {
|
||||||
|
stdout.write('\x1b[0m'); // Reset color
|
||||||
|
stdout.write('\x1b[2J\x1b[H'); // Clear screen
|
||||||
|
stdout.write('\x1b[?25h'); // SHOW the cursor again
|
||||||
|
exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
stdin.echoMode = false;
|
||||||
|
stdin.lineMode = false;
|
||||||
|
|
||||||
|
// HIDE the blinking cursor and clear the screen to prep the canvas
|
||||||
|
stdout.write('\x1b[?25l\x1b[2J');
|
||||||
|
|
||||||
|
// ... (Keep your game discovery and instantiation exactly the same) ...
|
||||||
|
final availableGames = await WolfensteinLoader.discover(
|
||||||
|
directoryPath: 'assets/shareware',
|
||||||
|
recursive: true,
|
||||||
|
);
|
||||||
|
final data = availableGames.values.first;
|
||||||
|
|
||||||
|
final input = CliInput();
|
||||||
|
final cliAudio = CliSilentAudio();
|
||||||
|
|
||||||
|
final engine = WolfEngine(
|
||||||
|
data: data,
|
||||||
|
difficulty: Difficulty.bringEmOn,
|
||||||
|
startingEpisode: 0,
|
||||||
|
audio: cliAudio,
|
||||||
|
onGameWon: () {
|
||||||
|
exitCleanly(0);
|
||||||
|
print("YOU WON!");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final rasterizer = AsciiRasterizer();
|
||||||
|
final buffer = FrameBuffer(120, 40);
|
||||||
|
|
||||||
|
engine.init();
|
||||||
|
|
||||||
|
stdin.listen((List<int> bytes) {
|
||||||
|
if (bytes.contains(113) || bytes.contains(27)) {
|
||||||
|
exitCleanly(0);
|
||||||
|
}
|
||||||
|
input.handleKey(bytes);
|
||||||
|
});
|
||||||
|
|
||||||
|
Stopwatch stopwatch = Stopwatch()..start();
|
||||||
|
Duration lastTick = Duration.zero;
|
||||||
|
|
||||||
|
Timer.periodic(const Duration(milliseconds: 33), (timer) {
|
||||||
|
// 1. Terminal Size Safety Check
|
||||||
|
if (stdout.hasTerminal) {
|
||||||
|
if (stdout.terminalColumns < 120 || stdout.terminalLines < 40) {
|
||||||
|
// Clear the screen and print the warning at the top left
|
||||||
|
stdout.write('\x1b[2J\x1b[H');
|
||||||
|
stdout.write('\x1b[31m[ ERROR ] TERMINAL TOO SMALL\x1b[0m\n\n');
|
||||||
|
stdout.write(
|
||||||
|
'Wolfenstein 3D requires a minimum resolution of 120x40.\n',
|
||||||
|
);
|
||||||
|
stdout.write(
|
||||||
|
'Current size: \x1b[33m${stdout.terminalColumns}x${stdout.terminalLines}\x1b[0m\n\n',
|
||||||
|
);
|
||||||
|
stdout.write('Please resize your window to resume the game...');
|
||||||
|
|
||||||
|
// Prevent the engine from simulating a massive time jump when resumed
|
||||||
|
lastTick = stopwatch.elapsed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Normal Game Loop
|
||||||
|
Duration currentTick = stopwatch.elapsed;
|
||||||
|
Duration elapsed = currentTick - lastTick;
|
||||||
|
lastTick = currentTick;
|
||||||
|
|
||||||
|
// Move cursor to top-left (0,0) before drawing the frame
|
||||||
|
stdout.write('\x1b[H');
|
||||||
|
|
||||||
|
engine.tick(elapsed, input.currentInput);
|
||||||
|
rasterizer.render(engine, buffer);
|
||||||
|
rasterizer.finalizeFrame();
|
||||||
|
|
||||||
|
stdout.write(rasterizer.toAnsiString());
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
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_flutter/wolf_3d.dart';
|
import 'package:wolf_3d_flutter/wolf_3d.dart';
|
||||||
import 'package:wolf_3d_renderer/wolf_3d_renderer.dart';
|
import 'package:wolf_3d_renderer/wolf_3d_ascii_renderer.dart';
|
||||||
|
|
||||||
class DifficultyScreen extends StatefulWidget {
|
class DifficultyScreen extends StatefulWidget {
|
||||||
const DifficultyScreen({
|
const DifficultyScreen({
|
||||||
@@ -26,7 +26,7 @@ class _DifficultyScreenState extends State<DifficultyScreen> {
|
|||||||
|
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => WolfRenderer(
|
builder: (_) => WolfAsciiRenderer(
|
||||||
Wolf3d.I.activeGame,
|
Wolf3d.I.activeGame,
|
||||||
difficulty: difficulty,
|
difficulty: difficulty,
|
||||||
startingEpisode: Wolf3d.I.activeEpisode,
|
startingEpisode: Wolf3d.I.activeEpisode,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
@@ -34,8 +33,17 @@ abstract class AsciiThemes {
|
|||||||
|
|
||||||
class ColoredChar {
|
class ColoredChar {
|
||||||
final String char;
|
final String char;
|
||||||
final Color color;
|
final int rawColor; // Stores the AABBGGRR integer from the palette
|
||||||
ColoredChar(this.char, this.color);
|
|
||||||
|
ColoredChar(this.char, this.rawColor);
|
||||||
|
|
||||||
|
// Safely extract the exact RGB channels regardless of framework
|
||||||
|
int get r => rawColor & 0xFF;
|
||||||
|
int get g => (rawColor >> 8) & 0xFF;
|
||||||
|
int get b => (rawColor >> 16) & 0xFF;
|
||||||
|
|
||||||
|
// Outputs standard AARRGGBB for Flutter's Color(int) constructor
|
||||||
|
int get argb => (0xFF000000) | (r << 16) | (g << 8) | b;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AsciiRasterizer extends Rasterizer {
|
class AsciiRasterizer extends Rasterizer {
|
||||||
@@ -49,38 +57,29 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
@override
|
@override
|
||||||
double get aspectMultiplier => 0.6;
|
double get aspectMultiplier => 0.6;
|
||||||
|
|
||||||
// --- HELPER: Color Conversion ---
|
|
||||||
Color _vgaToColor(int vgaColor) {
|
|
||||||
int r = vgaColor & 0xFF;
|
|
||||||
int g = (vgaColor >> 8) & 0xFF;
|
|
||||||
int b = (vgaColor >> 16) & 0xFF;
|
|
||||||
|
|
||||||
return Color.fromARGB(255, r, g, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intercept the base render call to initialize our text grid
|
// Intercept the base render call to initialize our text grid
|
||||||
@override
|
@override
|
||||||
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
dynamic render(WolfEngine engine, FrameBuffer buffer) {
|
||||||
_engine = engine;
|
_engine = engine;
|
||||||
_screen = List.generate(
|
_screen = List.generate(
|
||||||
buffer.height,
|
buffer.height,
|
||||||
(_) => List.filled(buffer.width, ColoredChar(' ', Colors.black)),
|
(_) =>
|
||||||
|
List.filled(buffer.width, ColoredChar(' ', ColorPalette.vga32Bit[0])),
|
||||||
);
|
);
|
||||||
return super.render(engine, buffer);
|
return super.render(engine, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void prepareFrame(WolfEngine engine) {
|
void prepareFrame(WolfEngine engine) {
|
||||||
final Color ceilingColor = _vgaToColor(ColorPalette.vga32Bit[25]);
|
// Just grab the raw ints!
|
||||||
final Color floorColor = _vgaToColor(ColorPalette.vga32Bit[29]);
|
final int ceilingColor = ColorPalette.vga32Bit[25];
|
||||||
|
final int floorColor = ColorPalette.vga32Bit[29];
|
||||||
|
|
||||||
for (int y = 0; y < height; y++) {
|
for (int y = 0; y < height; y++) {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
if (y < viewHeight / 2) {
|
if (y < viewHeight / 2) {
|
||||||
// Fetch the solid character from the theme
|
|
||||||
_screen[y][x] = ColoredChar(activeTheme.solid, ceilingColor);
|
_screen[y][x] = ColoredChar(activeTheme.solid, ceilingColor);
|
||||||
} else if (y < viewHeight) {
|
} else if (y < viewHeight) {
|
||||||
// Fetch the solid character from the theme
|
|
||||||
_screen[y][x] = ColoredChar(activeTheme.solid, floorColor);
|
_screen[y][x] = ColoredChar(activeTheme.solid, floorColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,7 +97,7 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
double perpWallDist,
|
double perpWallDist,
|
||||||
int side,
|
int side,
|
||||||
) {
|
) {
|
||||||
double brightness = (4.0 / (perpWallDist + 1.0));
|
double brightness = calculateDepthBrightness(perpWallDist);
|
||||||
String wallChar = activeTheme.getByBrightness(brightness);
|
String wallChar = activeTheme.getByBrightness(brightness);
|
||||||
|
|
||||||
for (int y = drawStart; y < drawEnd; y++) {
|
for (int y = drawStart; y < drawEnd; y++) {
|
||||||
@@ -107,16 +106,11 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
int texY = (relativeY * 64).toInt().clamp(0, 63);
|
||||||
|
|
||||||
int colorByte = texture.pixels[texX * 64 + texY];
|
int colorByte = texture.pixels[texX * 64 + texY];
|
||||||
Color pixelColor = _vgaToColor(ColorPalette.vga32Bit[colorByte]);
|
int pixelColor = ColorPalette.vga32Bit[colorByte]; // Raw int
|
||||||
|
|
||||||
// Faux directional lighting
|
// Faux directional lighting using your new base class method
|
||||||
if (side == 1) {
|
if (side == 1) {
|
||||||
pixelColor = Color.fromARGB(
|
pixelColor = shadeColor(pixelColor);
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_screen[y][x] = ColoredChar(wallChar, pixelColor);
|
_screen[y][x] = ColoredChar(wallChar, pixelColor);
|
||||||
@@ -133,7 +127,8 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
int texX,
|
int texX,
|
||||||
double transformY,
|
double transformY,
|
||||||
) {
|
) {
|
||||||
double brightness = (4.0 / (transformY + 1.0)).clamp(0.0, 1.0);
|
// Ask the base class for the depth brightness
|
||||||
|
double brightness = calculateDepthBrightness(transformY);
|
||||||
String spriteChar = activeTheme.getByBrightness(brightness);
|
String spriteChar = activeTheme.getByBrightness(brightness);
|
||||||
|
|
||||||
for (
|
for (
|
||||||
@@ -148,7 +143,7 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
if (colorByte != 255) {
|
if (colorByte != 255) {
|
||||||
_screen[y][stripeX] = ColoredChar(
|
_screen[y][stripeX] = ColoredChar(
|
||||||
spriteChar,
|
spriteChar,
|
||||||
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
|
ColorPalette.vga32Bit[colorByte],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +175,7 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < viewHeight) {
|
||||||
_screen[drawY][drawX] = ColoredChar(
|
_screen[drawY][drawX] = ColoredChar(
|
||||||
activeTheme.solid,
|
activeTheme.solid,
|
||||||
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
|
ColorPalette.vga32Bit[colorByte],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,10 +305,9 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
|
|
||||||
int colorByte = image.pixels[index];
|
int colorByte = image.pixels[index];
|
||||||
if (colorByte != 255) {
|
if (colorByte != 255) {
|
||||||
// Using '█' for UI to make it look solid
|
|
||||||
_screen[drawY][drawX] = ColoredChar(
|
_screen[drawY][drawX] = ColoredChar(
|
||||||
activeTheme.solid,
|
activeTheme.solid,
|
||||||
_vgaToColor(ColorPalette.vga32Bit[colorByte]),
|
ColorPalette.vga32Bit[colorByte],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +316,6 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- DAMAGE FLASH ---
|
// --- DAMAGE FLASH ---
|
||||||
|
|
||||||
void _applyDamageFlash() {
|
void _applyDamageFlash() {
|
||||||
double intensity = _engine.player.damageFlash;
|
double intensity = _engine.player.damageFlash;
|
||||||
int redBoost = (150 * intensity).toInt();
|
int redBoost = (150 * intensity).toInt();
|
||||||
@@ -330,24 +323,55 @@ class AsciiRasterizer extends Rasterizer {
|
|||||||
|
|
||||||
for (int y = 0; y < viewHeight; y++) {
|
for (int y = 0; y < viewHeight; y++) {
|
||||||
for (int x = 0; x < width; x++) {
|
for (int x = 0; x < width; x++) {
|
||||||
Color c = _screen[y][x].color;
|
ColoredChar cell = _screen[y][x];
|
||||||
|
|
||||||
int r = ((c.r * 255).round().clamp(0, 255) + redBoost).clamp(0, 255);
|
// Use our safe getters!
|
||||||
int g = ((c.g * 255).round().clamp(0, 255) * colorDrop).toInt().clamp(
|
int r = cell.r;
|
||||||
0,
|
int g = cell.g;
|
||||||
255,
|
int b = cell.b;
|
||||||
);
|
|
||||||
int b = ((c.b * 255).round().clamp(0, 255) * colorDrop).toInt().clamp(
|
|
||||||
0,
|
|
||||||
255,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Replace the existing character with a red-tinted version
|
r = (r + redBoost).clamp(0, 255);
|
||||||
_screen[y][x] = ColoredChar(
|
g = (g * colorDrop).toInt().clamp(0, 255);
|
||||||
_screen[y][x].char,
|
b = (b * colorDrop).toInt().clamp(0, 255);
|
||||||
Color.fromARGB(255, r, g, b),
|
|
||||||
);
|
// Pack back into the native AABBGGRR format that ColoredChar expects
|
||||||
|
int newRawColor = (0xFF000000) | (b << 16) | (g << 8) | r;
|
||||||
|
|
||||||
|
_screen[y][x] = ColoredChar(cell.char, newRawColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts the current frame to a single printable ANSI string
|
||||||
|
String toAnsiString() {
|
||||||
|
StringBuffer buffer = StringBuffer();
|
||||||
|
|
||||||
|
int lastR = -1;
|
||||||
|
int lastG = -1;
|
||||||
|
int lastB = -1;
|
||||||
|
|
||||||
|
for (int y = 0; y < _screen.length; y++) {
|
||||||
|
List<ColoredChar> row = _screen[y];
|
||||||
|
for (ColoredChar cell in row) {
|
||||||
|
if (cell.r != lastR || cell.g != lastG || cell.b != lastB) {
|
||||||
|
buffer.write('\x1b[38;2;${cell.r};${cell.g};${cell.b}m');
|
||||||
|
lastR = cell.r;
|
||||||
|
lastG = cell.g;
|
||||||
|
lastB = cell.b;
|
||||||
|
}
|
||||||
|
buffer.write(cell.char);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only print a newline if we are NOT on the very last row.
|
||||||
|
// This stops the terminal from scrolling down!
|
||||||
|
if (y < _screen.length - 1) {
|
||||||
|
buffer.write('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the terminal color at the very end
|
||||||
|
buffer.write('\x1b[0m');
|
||||||
|
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +80,17 @@ abstract class Rasterizer {
|
|||||||
/// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list).
|
/// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list).
|
||||||
dynamic finalizeFrame();
|
dynamic finalizeFrame();
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// CORE ENGINE MATH (Shared across all renderers)
|
// CORE ENGINE MATH (Shared across all renderers)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -359,4 +370,12 @@ abstract class Rasterizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class SoftwareRasterizer extends Rasterizer {
|
|||||||
|
|
||||||
// Darken Y-side walls for faux directional lighting
|
// Darken Y-side walls for faux directional lighting
|
||||||
if (side == 1) {
|
if (side == 1) {
|
||||||
pixelColor = _shadeColor(pixelColor);
|
pixelColor = shadeColor(pixelColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buffer.pixels[y * width + x] = pixelColor;
|
_buffer.pixels[y * width + x] = pixelColor;
|
||||||
@@ -235,14 +235,6 @@ class SoftwareRasterizer extends Rasterizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tints the top 80% of the screen red based on player.damageFlash intensity
|
/// Tints the top 80% of the screen red based on player.damageFlash intensity
|
||||||
void _applyDamageFlash() {
|
void _applyDamageFlash() {
|
||||||
// Grab the intensity (0.0 to 1.0)
|
// Grab the intensity (0.0 to 1.0)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
/// More dartdocs go here.
|
/// More dartdocs go here.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
export 'src/ascii_rasterizer.dart';
|
||||||
export 'src/engine_audio.dart';
|
export 'src/engine_audio.dart';
|
||||||
export 'src/engine_input.dart';
|
export 'src/engine_input.dart';
|
||||||
export 'src/managers/door_manager.dart';
|
export 'src/managers/door_manager.dart';
|
||||||
|
|||||||
38
packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart
Normal file
38
packages/wolf_3d_flutter/lib/wolf_3d_input_flutter.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
|
||||||
|
import 'package:wolf_3d_input/wolf_3d_input.dart';
|
||||||
|
|
||||||
|
class Wolf3dFlutterInput extends Wolf3dInput {
|
||||||
|
Set<LogicalKeyboardKey> _previousKeys = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update() {
|
||||||
|
final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed;
|
||||||
|
final newlyPressedKeys = pressedKeys.difference(_previousKeys);
|
||||||
|
|
||||||
|
isMovingForward = pressedKeys.contains(LogicalKeyboardKey.keyW);
|
||||||
|
isMovingBackward = pressedKeys.contains(LogicalKeyboardKey.keyS);
|
||||||
|
isTurningLeft = pressedKeys.contains(LogicalKeyboardKey.keyA);
|
||||||
|
isTurningRight = pressedKeys.contains(LogicalKeyboardKey.keyD);
|
||||||
|
|
||||||
|
isInteracting = newlyPressedKeys.contains(LogicalKeyboardKey.space);
|
||||||
|
|
||||||
|
isFiring =
|
||||||
|
pressedKeys.contains(LogicalKeyboardKey.controlLeft) &&
|
||||||
|
!pressedKeys.contains(LogicalKeyboardKey.space);
|
||||||
|
|
||||||
|
requestedWeapon = null;
|
||||||
|
for (final LogicalKeyboardKey key in newlyPressedKeys) {
|
||||||
|
if (key == LogicalKeyboardKey.digit1) requestedWeapon = WeaponType.knife;
|
||||||
|
if (key == LogicalKeyboardKey.digit2) requestedWeapon = WeaponType.pistol;
|
||||||
|
if (key == LogicalKeyboardKey.digit3) {
|
||||||
|
requestedWeapon = WeaponType.machineGun;
|
||||||
|
}
|
||||||
|
if (key == LogicalKeyboardKey.digit4) {
|
||||||
|
requestedWeapon = WeaponType.chainGun;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_previousKeys = Set.from(pressedKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ dependencies:
|
|||||||
wolf_3d_data_types: any
|
wolf_3d_data_types: any
|
||||||
wolf_3d_synth: any
|
wolf_3d_synth: any
|
||||||
wolf_3d_engine: any
|
wolf_3d_engine: any
|
||||||
|
wolf_3d_entities: any
|
||||||
|
wolf_3d_input: any
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
47
packages/wolf_3d_input/lib/src/cli_input.dart
Normal file
47
packages/wolf_3d_input/lib/src/cli_input.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
|
||||||
|
import 'package:wolf_3d_input/src/wolf_3d_input.dart';
|
||||||
|
|
||||||
|
class CliInput extends Wolf3dInput {
|
||||||
|
// Pending buffer for asynchronous stdin events
|
||||||
|
bool _pForward = false;
|
||||||
|
bool _pBackward = false;
|
||||||
|
bool _pLeft = false;
|
||||||
|
bool _pRight = false;
|
||||||
|
bool _pFire = false;
|
||||||
|
bool _pInteract = false;
|
||||||
|
WeaponType? _pWeapon;
|
||||||
|
|
||||||
|
/// Call this directly from the stdin listener to queue inputs for the next frame
|
||||||
|
void handleKey(List<int> bytes) {
|
||||||
|
String char = String.fromCharCodes(bytes).toLowerCase();
|
||||||
|
|
||||||
|
if (char == 'w') _pForward = true;
|
||||||
|
if (char == 's') _pBackward = true;
|
||||||
|
if (char == 'a') _pLeft = true;
|
||||||
|
if (char == 'd') _pRight = true;
|
||||||
|
|
||||||
|
if (char == 'f' || char == ' ') _pFire = true;
|
||||||
|
if (char == 'e') _pInteract = true;
|
||||||
|
|
||||||
|
if (char == '1') _pWeapon = WeaponType.knife;
|
||||||
|
if (char == '2') _pWeapon = WeaponType.pistol;
|
||||||
|
if (char == '3') _pWeapon = WeaponType.machineGun;
|
||||||
|
if (char == '4') _pWeapon = WeaponType.chainGun;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void update() {
|
||||||
|
// 1. Move pending inputs to the active state
|
||||||
|
isMovingForward = _pForward;
|
||||||
|
isMovingBackward = _pBackward;
|
||||||
|
isTurningLeft = _pLeft;
|
||||||
|
isTurningRight = _pRight;
|
||||||
|
isFiring = _pFire;
|
||||||
|
isInteracting = _pInteract;
|
||||||
|
requestedWeapon = _pWeapon;
|
||||||
|
|
||||||
|
// 2. Wipe the pending slate clean for the next frame
|
||||||
|
_pForward = _pBackward = _pLeft = _pRight = _pFire = _pInteract = false;
|
||||||
|
_pWeapon = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/wolf_3d_input/lib/src/wolf_3d_input.dart
Normal file
27
packages/wolf_3d_input/lib/src/wolf_3d_input.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
||||||
|
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
|
||||||
|
|
||||||
|
abstract class Wolf3dInput {
|
||||||
|
bool isMovingForward = false;
|
||||||
|
bool isMovingBackward = false;
|
||||||
|
bool isTurningLeft = false;
|
||||||
|
bool isTurningRight = false;
|
||||||
|
bool isInteracting = false;
|
||||||
|
bool isFiring = false;
|
||||||
|
WeaponType? requestedWeapon;
|
||||||
|
|
||||||
|
/// Called once per frame by the game loop to refresh the state.
|
||||||
|
void update();
|
||||||
|
|
||||||
|
/// Exports the current state as a clean DTO for the engine.
|
||||||
|
/// Subclasses do not need to override this.
|
||||||
|
EngineInput get currentInput => EngineInput(
|
||||||
|
isMovingForward: isMovingForward,
|
||||||
|
isMovingBackward: isMovingBackward,
|
||||||
|
isTurningLeft: isTurningLeft,
|
||||||
|
isTurningRight: isTurningRight,
|
||||||
|
isFiring: isFiring,
|
||||||
|
isInteracting: isInteracting,
|
||||||
|
requestedWeapon: requestedWeapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,58 +1,4 @@
|
|||||||
import 'package:flutter/services.dart';
|
library;
|
||||||
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
|
||||||
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
|
|
||||||
|
|
||||||
class WolfInput {
|
export 'src/cli_input.dart';
|
||||||
Set<LogicalKeyboardKey> _previousKeys = {};
|
export 'src/wolf_3d_input.dart';
|
||||||
|
|
||||||
bool isMovingForward = false;
|
|
||||||
bool isMovingBackward = false;
|
|
||||||
bool isTurningLeft = false;
|
|
||||||
bool isTurningRight = false;
|
|
||||||
bool isInteracting = false;
|
|
||||||
bool isFiring = false;
|
|
||||||
WeaponType? requestedWeapon;
|
|
||||||
|
|
||||||
void update() {
|
|
||||||
final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed;
|
|
||||||
final newlyPressedKeys = pressedKeys.difference(_previousKeys);
|
|
||||||
|
|
||||||
isMovingForward = pressedKeys.contains(LogicalKeyboardKey.keyW);
|
|
||||||
isMovingBackward = pressedKeys.contains(LogicalKeyboardKey.keyS);
|
|
||||||
isTurningLeft = pressedKeys.contains(LogicalKeyboardKey.keyA);
|
|
||||||
isTurningRight = pressedKeys.contains(LogicalKeyboardKey.keyD);
|
|
||||||
|
|
||||||
isInteracting = newlyPressedKeys.contains(LogicalKeyboardKey.space);
|
|
||||||
|
|
||||||
isFiring =
|
|
||||||
pressedKeys.contains(LogicalKeyboardKey.controlLeft) &&
|
|
||||||
!pressedKeys.contains(LogicalKeyboardKey.space);
|
|
||||||
|
|
||||||
requestedWeapon = null;
|
|
||||||
for (final LogicalKeyboardKey key in newlyPressedKeys) {
|
|
||||||
switch (key) {
|
|
||||||
case LogicalKeyboardKey.digit1:
|
|
||||||
requestedWeapon = WeaponType.knife;
|
|
||||||
case LogicalKeyboardKey.digit2:
|
|
||||||
requestedWeapon = WeaponType.pistol;
|
|
||||||
case LogicalKeyboardKey.digit3:
|
|
||||||
requestedWeapon = WeaponType.machineGun;
|
|
||||||
case LogicalKeyboardKey.digit4:
|
|
||||||
requestedWeapon = WeaponType.chainGun;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_previousKeys = Set.from(pressedKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exports the current state as a clean DTO for the engine
|
|
||||||
EngineInput get currentInput => EngineInput(
|
|
||||||
isMovingForward: isMovingForward,
|
|
||||||
isMovingBackward: isMovingBackward,
|
|
||||||
isTurningLeft: isTurningLeft,
|
|
||||||
isTurningRight: isTurningRight,
|
|
||||||
isFiring: isFiring,
|
|
||||||
isInteracting: isInteracting,
|
|
||||||
requestedWeapon: requestedWeapon,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,54 +5,9 @@ homepage:
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.1
|
sdk: ^3.11.1
|
||||||
flutter: ">=1.17.0"
|
|
||||||
|
|
||||||
resolution: workspace
|
resolution: workspace
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
wolf_3d_entities: any
|
wolf_3d_entities: any
|
||||||
wolf_3d_engine: any
|
wolf_3d_engine: any
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
flutter_test:
|
|
||||||
sdk: flutter
|
|
||||||
flutter_lints: ^6.0.0
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
|
||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
|
||||||
flutter:
|
|
||||||
|
|
||||||
# To add assets to your package, add an assets section, like this:
|
|
||||||
# assets:
|
|
||||||
# - images/a_dot_burr.jpeg
|
|
||||||
# - images/a_dot_ham.jpeg
|
|
||||||
#
|
|
||||||
# For details regarding assets in packages, see
|
|
||||||
# https://flutter.dev/to/asset-from-package
|
|
||||||
#
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
|
||||||
|
|
||||||
# To add custom fonts to your package, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts in packages, see
|
|
||||||
# https://flutter.dev/to/font-from-package
|
|
||||||
|
|||||||
@@ -2,3 +2,6 @@ include: package:flutter_lints/flutter.yaml
|
|||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|
||||||
|
formatter:
|
||||||
|
trailing_commas: preserve
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.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_flutter/wolf_3d_input_flutter.dart';
|
||||||
import 'package:wolf_3d_input/wolf_3d_input.dart';
|
import 'package:wolf_3d_input/wolf_3d_input.dart';
|
||||||
import 'package:wolf_3d_renderer/ascii_rasterizer.dart';
|
|
||||||
|
|
||||||
class WolfAsciiRenderer extends StatefulWidget {
|
class WolfAsciiRenderer extends StatefulWidget {
|
||||||
const WolfAsciiRenderer(
|
const WolfAsciiRenderer(
|
||||||
@@ -25,7 +25,7 @@ class WolfAsciiRenderer extends StatefulWidget {
|
|||||||
|
|
||||||
class _WolfAsciiRendererState extends State<WolfAsciiRenderer>
|
class _WolfAsciiRendererState extends State<WolfAsciiRenderer>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
final WolfInput inputManager = WolfInput();
|
final Wolf3dInput inputManager = Wolf3dFlutterInput();
|
||||||
late final WolfEngine engine;
|
late final WolfEngine engine;
|
||||||
late Ticker _gameLoop;
|
late Ticker _gameLoop;
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
@@ -116,11 +116,11 @@ class AsciiFrameWidget extends StatelessWidget {
|
|||||||
children: frameData.map((row) {
|
children: frameData.map((row) {
|
||||||
List<TextSpan> optimizedSpans = [];
|
List<TextSpan> optimizedSpans = [];
|
||||||
if (row.isNotEmpty) {
|
if (row.isNotEmpty) {
|
||||||
Color currentColor = row[0].color;
|
Color currentColor = Color(row[0].argb);
|
||||||
StringBuffer currentSegment = StringBuffer(row[0].char);
|
StringBuffer currentSegment = StringBuffer(row[0].char);
|
||||||
|
|
||||||
for (int i = 1; i < row.length; i++) {
|
for (int i = 1; i < row.length; i++) {
|
||||||
if (row[i].color == currentColor) {
|
if (Color(row[i].argb) == currentColor) {
|
||||||
currentSegment.write(row[i].char);
|
currentSegment.write(row[i].char);
|
||||||
} else {
|
} else {
|
||||||
optimizedSpans.add(
|
optimizedSpans.add(
|
||||||
@@ -129,7 +129,7 @@ class AsciiFrameWidget extends StatelessWidget {
|
|||||||
style: TextStyle(color: currentColor),
|
style: TextStyle(color: currentColor),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
currentColor = row[i].color;
|
currentColor = Color(row[i].argb);
|
||||||
currentSegment = StringBuffer(row[i].char);
|
currentSegment = StringBuffer(row[i].char);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.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_flutter/wolf_3d_input_flutter.dart';
|
||||||
import 'package:wolf_3d_input/wolf_3d_input.dart';
|
import 'package:wolf_3d_input/wolf_3d_input.dart';
|
||||||
|
|
||||||
class WolfRenderer extends StatefulWidget {
|
class WolfRenderer extends StatefulWidget {
|
||||||
@@ -26,7 +27,7 @@ class WolfRenderer extends StatefulWidget {
|
|||||||
|
|
||||||
class _WolfRendererState extends State<WolfRenderer>
|
class _WolfRendererState extends State<WolfRenderer>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
final WolfInput inputManager = WolfInput();
|
final Wolf3dInput inputManager = Wolf3dFlutterInput();
|
||||||
late final WolfEngine engine;
|
late final WolfEngine engine;
|
||||||
late Ticker _gameLoop;
|
late Ticker _gameLoop;
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ dependencies:
|
|||||||
wolf_3d_engine: any
|
wolf_3d_engine: any
|
||||||
wolf_3d_entities: any
|
wolf_3d_entities: any
|
||||||
wolf_3d_input: any
|
wolf_3d_input: any
|
||||||
|
wolf_3d_flutter: any
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
34
packages/wolf_3d_synth/lib/src/silent_renderer.dart
Normal file
34
packages/wolf_3d_synth/lib/src/silent_renderer.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||||
|
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
||||||
|
|
||||||
|
class CliSilentAudio implements EngineAudio {
|
||||||
|
@override
|
||||||
|
WolfensteinData? activeGame;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init() async {
|
||||||
|
// No-op for CLI
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playMenuMusic() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playLevelMusic(WolfLevel level) {
|
||||||
|
// Optional: Print a log so you know it's working!
|
||||||
|
// print("🎵 Playing music for: ${level.name} 🎵");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void stopMusic() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void playSoundEffect(int sfxId) {
|
||||||
|
// Optional: You could use the terminal 'bell' character here
|
||||||
|
// to actually make a system beep when a sound plays!
|
||||||
|
// stdout.write('\x07');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {}
|
||||||
|
}
|
||||||
@@ -3,4 +3,5 @@
|
|||||||
/// More dartdocs go here.
|
/// More dartdocs go here.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
export 'src/silent_renderer.dart' show CliSilentAudio;
|
||||||
export 'src/wolf_3d_audio.dart' show WolfAudio;
|
export 'src/wolf_3d_audio.dart' show WolfAudio;
|
||||||
|
|||||||
Reference in New Issue
Block a user