De-coupled remaining aspects of game into packages
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
31
packages/wolf_3d_renderer/.gitignore
vendored
Normal file
31
packages/wolf_3d_renderer/.gitignore
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
/build/
|
||||
/coverage/
|
||||
10
packages/wolf_3d_renderer/.metadata
Normal file
10
packages/wolf_3d_renderer/.metadata
Normal file
@@ -0,0 +1,10 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
|
||||
channel: "stable"
|
||||
|
||||
project_type: package
|
||||
3
packages/wolf_3d_renderer/CHANGELOG.md
Normal file
3
packages/wolf_3d_renderer/CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
||||
1
packages/wolf_3d_renderer/LICENSE
Normal file
1
packages/wolf_3d_renderer/LICENSE
Normal file
@@ -0,0 +1 @@
|
||||
TODO: Add your license here.
|
||||
39
packages/wolf_3d_renderer/README.md
Normal file
39
packages/wolf_3d_renderer/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
<!--
|
||||
This README describes the package. If you publish this package to pub.dev,
|
||||
this README's contents appear on the landing page for your package.
|
||||
|
||||
For information about how to write a good package README, see the guide for
|
||||
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
|
||||
|
||||
For general information about developing packages, see the Dart guide for
|
||||
[creating packages](https://dart.dev/guides/libraries/create-packages)
|
||||
and the Flutter guide for
|
||||
[developing packages and plugins](https://flutter.dev/to/develop-packages).
|
||||
-->
|
||||
|
||||
TODO: Put a short description of the package here that helps potential users
|
||||
know whether this package might be useful for them.
|
||||
|
||||
## Features
|
||||
|
||||
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||
|
||||
## Getting started
|
||||
|
||||
TODO: List prerequisites and provide or point to information on how to
|
||||
start using the package.
|
||||
|
||||
## Usage
|
||||
|
||||
TODO: Include short and useful examples for package users. Add longer examples
|
||||
to `/example` folder.
|
||||
|
||||
```dart
|
||||
const like = 'sample';
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
TODO: Tell users more about the package: where to find more information, how to
|
||||
contribute to the package, how to file issues, what response they can expect
|
||||
from the package authors, and more.
|
||||
4
packages/wolf_3d_renderer/analysis_options.yaml
Normal file
4
packages/wolf_3d_renderer/analysis_options.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
293
packages/wolf_3d_renderer/lib/color_palette.dart
Normal file
293
packages/wolf_3d_renderer/lib/color_palette.dart
Normal file
@@ -0,0 +1,293 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension WolfPaletteMatch on Color {
|
||||
/// Finds the index of the closest color in the wolfPalette
|
||||
int findClosestIndex(List<Color> palette) {
|
||||
int closestIndex = 0;
|
||||
double minDistance = double.infinity;
|
||||
|
||||
for (int i = 0; i < palette.length; i++) {
|
||||
final Color pColor = palette[i];
|
||||
|
||||
// Calculate squared Euclidean distance (skipping sqrt for performance)
|
||||
double distance =
|
||||
pow(r - pColor.r, 2).toDouble() +
|
||||
pow(g - pColor.g, 2).toDouble() +
|
||||
pow(b - pColor.b, 2).toDouble();
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
return closestIndex;
|
||||
}
|
||||
|
||||
/// Returns the actual Color object from the palette that matches best
|
||||
Color toWolfColor(List<Color> palette) {
|
||||
return palette[findClosestIndex(palette)];
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ColorPalette {
|
||||
static const List<Color> vga = [
|
||||
Color(0xFF000000),
|
||||
Color(0xFF0000AA),
|
||||
Color(0xFF00AA00),
|
||||
Color(0xFF00AAAA),
|
||||
Color(0xFFAA0000),
|
||||
Color(0xFFAA00AA),
|
||||
Color(0xFFAA5500),
|
||||
Color(0xFFAAAAAA),
|
||||
Color(0xFF555555),
|
||||
Color(0xFF5555FF),
|
||||
Color(0xFF55FF55),
|
||||
Color(0xFF55FFFF),
|
||||
Color(0xFFFF5555),
|
||||
Color(0xFFFF55FF),
|
||||
Color(0xFFFFFF55),
|
||||
Color(0xFFFFFFFF),
|
||||
Color(0xFFEEEEEE),
|
||||
Color(0xFFDEDEDE),
|
||||
Color(0xFFD2D2D2),
|
||||
Color(0xFFC2C2C2),
|
||||
Color(0xFFB6B6B6),
|
||||
Color(0xFFAAAAAA),
|
||||
Color(0xFF999999),
|
||||
Color(0xFF8D8D8D),
|
||||
Color(0xFF7D7D7D),
|
||||
Color(0xFF717171),
|
||||
Color(0xFF656565),
|
||||
Color(0xFF555555),
|
||||
Color(0xFF484848),
|
||||
Color(0xFF383838),
|
||||
Color(0xFF2C2C2C),
|
||||
Color(0xFF202020),
|
||||
Color(0xFFFF0000),
|
||||
Color(0xFFEE0000),
|
||||
Color(0xFFE20000),
|
||||
Color(0xFFD60000),
|
||||
Color(0xFFCA0000),
|
||||
Color(0xFFBE0000),
|
||||
Color(0xFFB20000),
|
||||
Color(0xFFA50000),
|
||||
Color(0xFF990000),
|
||||
Color(0xFF890000),
|
||||
Color(0xFF7D0000),
|
||||
Color(0xFF710000),
|
||||
Color(0xFF650000),
|
||||
Color(0xFF590000),
|
||||
Color(0xFF4C0000),
|
||||
Color(0xFF400000),
|
||||
Color(0xFFFFDADA),
|
||||
Color(0xFFFFBABA),
|
||||
Color(0xFFFF9D9D),
|
||||
Color(0xFFFF7D7D),
|
||||
Color(0xFFFF5D5D),
|
||||
Color(0xFFFF4040),
|
||||
Color(0xFFFF2020),
|
||||
Color(0xFFFF0000),
|
||||
Color(0xFFFFAA5D),
|
||||
Color(0xFFFF9940),
|
||||
Color(0xFFFF8920),
|
||||
Color(0xFFFF7900),
|
||||
Color(0xFFE66D00),
|
||||
Color(0xFFCE6100),
|
||||
Color(0xFFB65500),
|
||||
Color(0xFF9D4C00),
|
||||
Color(0xFFFFFFDA),
|
||||
Color(0xFFFFFFBA),
|
||||
Color(0xFFFFFF9D),
|
||||
Color(0xFFFFFF7D),
|
||||
Color(0xFFFFFA5D),
|
||||
Color(0xFFFFF640),
|
||||
Color(0xFFFFF620),
|
||||
Color(0xFFFFF600),
|
||||
Color(0xFFE6DA00),
|
||||
Color(0xFFCEC600),
|
||||
Color(0xFFB6AE00),
|
||||
Color(0xFF9D9D00),
|
||||
Color(0xFF858500),
|
||||
Color(0xFF716D00),
|
||||
Color(0xFF595500),
|
||||
Color(0xFF404000),
|
||||
Color(0xFFD2FF5D),
|
||||
Color(0xFFC6FF40),
|
||||
Color(0xFFB6FF20),
|
||||
Color(0xFFA1FF00),
|
||||
Color(0xFF91E600),
|
||||
Color(0xFF81CE00),
|
||||
Color(0xFF75B600),
|
||||
Color(0xFF619D00),
|
||||
Color(0xFFDAFFDA),
|
||||
Color(0xFFBEFFBA),
|
||||
Color(0xFF9DFF9D),
|
||||
Color(0xFF81FF7D),
|
||||
Color(0xFF61FF5D),
|
||||
Color(0xFF40FF40),
|
||||
Color(0xFF20FF20),
|
||||
Color(0xFF00FF00),
|
||||
Color(0xFF00FF00),
|
||||
Color(0xFF00EE00),
|
||||
Color(0xFF00E200),
|
||||
Color(0xFF00D600),
|
||||
Color(0xFF04CA00),
|
||||
Color(0xFF04BE00),
|
||||
Color(0xFF04B200),
|
||||
Color(0xFF04A500),
|
||||
Color(0xFF049900),
|
||||
Color(0xFF048900),
|
||||
Color(0xFF047D00),
|
||||
Color(0xFF047100),
|
||||
Color(0xFF046500),
|
||||
Color(0xFF045900),
|
||||
Color(0xFF044C00),
|
||||
Color(0xFF044000),
|
||||
Color(0xFFDAFFFF),
|
||||
Color(0xFFBAFFFF),
|
||||
Color(0xFF9DFFFF),
|
||||
Color(0xFF7DFFFA),
|
||||
Color(0xFF5DFFFF),
|
||||
Color(0xFF40FFFF),
|
||||
Color(0xFF20FFFF),
|
||||
Color(0xFF00FFFF),
|
||||
Color(0xFF00E6E6),
|
||||
Color(0xFF00CECE),
|
||||
Color(0xFF00B6B6),
|
||||
Color(0xFF009D9D),
|
||||
Color(0xFF008585),
|
||||
Color(0xFF007171),
|
||||
Color(0xFF005959),
|
||||
Color(0xFF004040),
|
||||
Color(0xFF5DBEFF),
|
||||
Color(0xFF40B2FF),
|
||||
Color(0xFF20AAFF),
|
||||
Color(0xFF009DFF),
|
||||
Color(0xFF008DE6),
|
||||
Color(0xFF007DCE),
|
||||
Color(0xFF006DB6),
|
||||
Color(0xFF005D9D),
|
||||
Color(0xFFDADADA),
|
||||
Color(0xFFBABEFF),
|
||||
Color(0xFF9D9DFF),
|
||||
Color(0xFF7D81FF),
|
||||
Color(0xFF5D61FF),
|
||||
Color(0xFF4040FF),
|
||||
Color(0xFF2024FF),
|
||||
Color(0xFF0004FF),
|
||||
Color(0xFF0000FF),
|
||||
Color(0xFF0000EE),
|
||||
Color(0xFF0000E2),
|
||||
Color(0xFF0000D6),
|
||||
Color(0xFF0000CA),
|
||||
Color(0xFF0000BE),
|
||||
Color(0xFF0000B2),
|
||||
Color(0xFF0000A5),
|
||||
Color(0xFF000099),
|
||||
Color(0xFF000089),
|
||||
Color(0xFF00007D),
|
||||
Color(0xFF000071),
|
||||
Color(0xFF000065),
|
||||
Color(0xFF000059),
|
||||
Color(0xFF00004C),
|
||||
Color(0xFF000040),
|
||||
Color(0xFF282828),
|
||||
Color(0xFFFFE234),
|
||||
Color(0xFFFFD624),
|
||||
Color(0xFFFFCE18),
|
||||
Color(0xFFFFC208),
|
||||
Color(0xFFFFB600),
|
||||
Color(0xFFB620FF),
|
||||
Color(0xFFAA00FF),
|
||||
Color(0xFF9900E6),
|
||||
Color(0xFF8100CE),
|
||||
Color(0xFF7500B6),
|
||||
Color(0xFF61009D),
|
||||
Color(0xFF500085),
|
||||
Color(0xFF440071),
|
||||
Color(0xFF340059),
|
||||
Color(0xFF280040),
|
||||
Color(0xFFFFDAFF),
|
||||
Color(0xFFFFBAFF),
|
||||
Color(0xFFFF9DFF),
|
||||
Color(0xFFFF7DFF),
|
||||
Color(0xFFFF5DFF),
|
||||
Color(0xFFFF40FF),
|
||||
Color(0xFFFF20FF),
|
||||
Color(0xFFFF00FF),
|
||||
Color(0xFFE200E6),
|
||||
Color(0xFFCA00CE),
|
||||
Color(0xFFB600B6),
|
||||
Color(0xFF9D009D),
|
||||
Color(0xFF850085),
|
||||
Color(0xFF6D0071),
|
||||
Color(0xFF590059),
|
||||
Color(0xFF400040),
|
||||
Color(0xFFFFEADE),
|
||||
Color(0xFFFFE2D2),
|
||||
Color(0xFFFFDAC6),
|
||||
Color(0xFFFFD6BE),
|
||||
Color(0xFFFFCEB2),
|
||||
Color(0xFFFFC6A5),
|
||||
Color(0xFFFFBE9D),
|
||||
Color(0xFFFFBA91),
|
||||
Color(0xFFFFB281),
|
||||
Color(0xFFFA571F),
|
||||
Color(0xFFFF9D61),
|
||||
Color(0xFFF2955D),
|
||||
Color(0xFFEA8D59),
|
||||
Color(0xFFDE8955),
|
||||
Color(0xFFD28150),
|
||||
Color(0xFFCA7D4C),
|
||||
Color(0xFFBE7948),
|
||||
Color(0xFFB67144),
|
||||
Color(0xFFAA6940),
|
||||
Color(0xFFA1653C),
|
||||
Color(0xFF9D6138),
|
||||
Color(0xFF915D34),
|
||||
Color(0xFF895930),
|
||||
Color(0xFF81502C),
|
||||
Color(0xFF754C28),
|
||||
Color(0xFF6D4824),
|
||||
Color(0xFF5D4020),
|
||||
Color(0xFF553C1C),
|
||||
Color(0xFF483818),
|
||||
Color(0xFF403018),
|
||||
Color(0xFF382C14),
|
||||
Color(0xFF28200C),
|
||||
Color(0xFF610065),
|
||||
Color(0xFF006565),
|
||||
Color(0xFF006161),
|
||||
Color(0xFF00001C),
|
||||
Color(0xFF00002C),
|
||||
Color(0xFF302410),
|
||||
Color(0xFF480048),
|
||||
Color(0xFF500050),
|
||||
Color(0xFF000034),
|
||||
Color(0xFF1C1C1C),
|
||||
Color(0xFF4C4C4C),
|
||||
Color(0xFF5D5D5D),
|
||||
Color(0xFF404040),
|
||||
Color(0xFF303030),
|
||||
Color(0xFF343434),
|
||||
Color(0xFFDAF6F6),
|
||||
Color(0xFFBAEAEA),
|
||||
Color(0xFF9DDEDE),
|
||||
Color(0xFF75CACA),
|
||||
Color(0xFF48C2C2),
|
||||
Color(0xFF20B6B6),
|
||||
Color(0xFF20B2B2),
|
||||
Color(0xFF00A5A5),
|
||||
Color(0xFF009999),
|
||||
Color(0xFF008D8D),
|
||||
Color(0xFF008585),
|
||||
Color(0xFF007D7D),
|
||||
Color(0xFF007979),
|
||||
Color(0xFF007575),
|
||||
Color(0xFF007171),
|
||||
Color(0xFF006D6D),
|
||||
Color(0xFF990089),
|
||||
];
|
||||
}
|
||||
91
packages/wolf_3d_renderer/lib/hud.dart
Normal file
91
packages/wolf_3d_renderer/lib/hud.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wolf_3d_engine/wolf_3d_engine.dart';
|
||||
|
||||
class Hud extends StatelessWidget {
|
||||
final Player player;
|
||||
|
||||
const Hud({super.key, required this.player});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// We'll give the HUD a fixed height relative to the screen
|
||||
return Container(
|
||||
height: 100,
|
||||
color: const Color(0xFF323232), // Classic dark grey status bar
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildStatColumn("FLOOR", "1"),
|
||||
_buildStatColumn("SCORE", "${player.score}"),
|
||||
_buildStatColumn("LIVES", "3"),
|
||||
_buildFace(),
|
||||
_buildStatColumn(
|
||||
"HEALTH",
|
||||
"${player.health}%",
|
||||
color: _getHealthColor(),
|
||||
),
|
||||
_buildStatColumn("AMMO", "${player.ammo}"),
|
||||
_buildWeaponIcon(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatColumn(
|
||||
String label,
|
||||
String value, {
|
||||
Color color = Colors.white,
|
||||
}) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(color: color, fontSize: 20, fontFamily: 'monospace'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFace() {
|
||||
// For now, we'll use a simple icon. Later we can map VSWAP indices
|
||||
// for BJ Blazkowicz's face based on health percentage.
|
||||
return Container(
|
||||
width: 60,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blueGrey[800],
|
||||
border: Border.all(color: Colors.grey, width: 2),
|
||||
),
|
||||
child: const Icon(Icons.face, color: Colors.white, size: 40),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWeaponIcon() {
|
||||
IconData weaponIcon = Icons.horizontal_rule; // Default Knife/Pistol
|
||||
if (player.hasChainGun) weaponIcon = Icons.reorder;
|
||||
if (player.hasMachineGun) weaponIcon = Icons.view_headline;
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text("WEAPON", style: TextStyle(color: Colors.red, fontSize: 10)),
|
||||
Icon(weaponIcon, color: Colors.white),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getHealthColor() {
|
||||
if (player.health > 50) return Colors.white;
|
||||
if (player.health > 25) return Colors.orange;
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
369
packages/wolf_3d_renderer/lib/raycast_painter.dart
Normal file
369
packages/wolf_3d_renderer/lib/raycast_painter.dart
Normal file
@@ -0,0 +1,369 @@
|
||||
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_engine/wolf_3d_engine.dart';
|
||||
import 'package:wolf_3d_entities/wolf_3d_entities.dart';
|
||||
import 'package:wolf_3d_renderer/color_palette.dart';
|
||||
|
||||
class RaycasterPainter extends CustomPainter {
|
||||
final Level map;
|
||||
final List<Sprite> textures;
|
||||
final Player player;
|
||||
final double fov;
|
||||
final Map<String, double> doorOffsets;
|
||||
final Pushwall? activePushwall;
|
||||
final List<Sprite> sprites;
|
||||
final List<Entity> entities;
|
||||
|
||||
RaycasterPainter({
|
||||
required this.map,
|
||||
required this.textures,
|
||||
required this.player,
|
||||
required this.fov,
|
||||
required this.doorOffsets,
|
||||
this.activePushwall,
|
||||
required this.sprites,
|
||||
required this.entities,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Paint bgPaint = Paint()..isAntiAlias = false;
|
||||
|
||||
// 1. Draw Ceiling & Floor
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height / 2),
|
||||
bgPaint..color = Colors.blueGrey[900]!,
|
||||
);
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, size.height / 2, size.width, size.height / 2),
|
||||
bgPaint..color = Colors.brown[900]!,
|
||||
);
|
||||
|
||||
const int renderWidth = 320;
|
||||
double columnWidth = size.width / renderWidth;
|
||||
|
||||
final Paint columnPaint = Paint()
|
||||
..isAntiAlias = false
|
||||
..strokeWidth = columnWidth + 0.5;
|
||||
|
||||
List<double> zBuffer = List.filled(renderWidth, 0.0);
|
||||
|
||||
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 < renderWidth; x++) {
|
||||
double cameraX = 2 * x / renderWidth - 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; // Replaces doorOffset to handle both
|
||||
bool customDistCalculated = false; // Flag to skip standard distance
|
||||
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 passed through the open door gap
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
// Did we hit the face that is being pushed deeper?
|
||||
if (side == 0 && pDirX != 0) {
|
||||
if (pDirX == stepX) {
|
||||
double intersect = perpWallDist + pOffset * deltaDistX;
|
||||
if (intersect < sideDistY) {
|
||||
perpWallDist = intersect; // Hit the recessed front face
|
||||
} else {
|
||||
side =
|
||||
1; // Missed the front face, hit the newly exposed side!
|
||||
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 {
|
||||
// We hit the side of the sliding block. Did the ray slip behind it?
|
||||
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; // Stick the texture to the block
|
||||
}
|
||||
} else {
|
||||
if (pDirX == 1 && wallFraction < pOffset) hit = false;
|
||||
if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false;
|
||||
if (hit) {
|
||||
textureOffset =
|
||||
pOffset * pDirX; // Stick the texture to the block
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hit) continue; // The ray slipped past! Keep looping.
|
||||
customDistCalculated = true; // Lock in our custom distance math
|
||||
}
|
||||
// --- STANDARD WALL ---
|
||||
else {
|
||||
hit = true;
|
||||
hitWallId = map[mapY][mapX];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hitOutOfBounds) continue;
|
||||
|
||||
// Apply standard math ONLY if we didn't calculate a sub-tile pushwall distance
|
||||
if (!customDistCalculated) {
|
||||
if (side == 0) {
|
||||
perpWallDist = (sideDistX - deltaDistX);
|
||||
} else {
|
||||
perpWallDist = (sideDistY - deltaDistY);
|
||||
}
|
||||
}
|
||||
|
||||
zBuffer[x] = perpWallDist;
|
||||
|
||||
double wallX = (side == 0)
|
||||
? player.y + perpWallDist * rayDir.y
|
||||
: player.x + perpWallDist * rayDir.x;
|
||||
wallX -= wallX.floor();
|
||||
|
||||
double drawX = x * columnWidth;
|
||||
|
||||
_drawTexturedColumn(
|
||||
canvas,
|
||||
drawX,
|
||||
perpWallDist,
|
||||
wallX,
|
||||
side,
|
||||
size,
|
||||
hitWallId,
|
||||
textures,
|
||||
textureOffset,
|
||||
columnPaint,
|
||||
);
|
||||
}
|
||||
|
||||
// --- 2. DRAW SPRITES ---
|
||||
// (Keep your existing sprite rendering logic exactly the same)
|
||||
List<Entity> activeSprites = List.from(entities);
|
||||
|
||||
activeSprites.sort((a, b) {
|
||||
double distA = player.position.distanceTo(a.position);
|
||||
double distB = player.position.distanceTo(b.position);
|
||||
return distB.compareTo(distA);
|
||||
});
|
||||
|
||||
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 = ((renderWidth / 2) * (1 + transformX / transformY))
|
||||
.toInt();
|
||||
int spriteHeight = (size.height / transformY).abs().toInt();
|
||||
int spriteColumnWidth = (spriteHeight / columnWidth).toInt();
|
||||
|
||||
int drawStartX = -spriteColumnWidth ~/ 2 + spriteScreenX;
|
||||
int drawEndX = spriteColumnWidth ~/ 2 + spriteScreenX;
|
||||
|
||||
int clipStartX = math.max(0, drawStartX);
|
||||
int clipEndX = math.min(renderWidth - 1, drawEndX);
|
||||
|
||||
for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
|
||||
if (transformY < zBuffer[stripe]) {
|
||||
double texXDouble = (stripe - drawStartX) * 64 / spriteColumnWidth;
|
||||
int texX = texXDouble.toInt().clamp(0, 63);
|
||||
|
||||
double startY = (size.height / 2) - (spriteHeight / 2);
|
||||
double stepY = spriteHeight / 64.0;
|
||||
double drawX = stripe * columnWidth;
|
||||
|
||||
int safeIndex = entity.spriteIndex.clamp(0, sprites.length - 1);
|
||||
Sprite spritePixels = sprites[safeIndex];
|
||||
|
||||
for (int ty = 0; ty < 64; ty++) {
|
||||
int colorByte = spritePixels[texX][ty];
|
||||
|
||||
if (colorByte != 255) {
|
||||
double endY = startY + stepY + 0.5;
|
||||
|
||||
if (endY > 0 && startY < size.height) {
|
||||
columnPaint.color = ColorPalette.vga[colorByte];
|
||||
canvas.drawLine(
|
||||
Offset(drawX, startY),
|
||||
Offset(drawX, endY),
|
||||
columnPaint,
|
||||
);
|
||||
}
|
||||
}
|
||||
startY += stepY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _drawTexturedColumn(
|
||||
Canvas canvas,
|
||||
double drawX,
|
||||
double distance,
|
||||
double wallX,
|
||||
int side,
|
||||
Size size,
|
||||
int hitWallId,
|
||||
List<Sprite> textures,
|
||||
double textureOffset,
|
||||
Paint paint,
|
||||
) {
|
||||
if (distance <= 0.01) distance = 0.01;
|
||||
|
||||
double wallHeight = size.height / distance;
|
||||
int drawStart = ((size.height / 2) - (wallHeight / 2)).toInt();
|
||||
|
||||
int texNum;
|
||||
int texX;
|
||||
|
||||
if (hitWallId >= 90) {
|
||||
// DOORS
|
||||
texNum = 98.clamp(0, textures.length - 1);
|
||||
texX = ((wallX - textureOffset) * 64).toInt().clamp(0, 63);
|
||||
} else {
|
||||
// WALLS & PUSHWALLS
|
||||
texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2);
|
||||
if (side == 1) texNum += 1;
|
||||
// We apply the modulo % 1.0 to handle negative texture offsets smoothly!
|
||||
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;
|
||||
|
||||
double startY = drawStart.toDouble();
|
||||
double stepY = wallHeight / 64.0;
|
||||
|
||||
for (int ty = 0; ty < 64; ty++) {
|
||||
int colorByte = textures[texNum][texX][ty];
|
||||
|
||||
paint.color = ColorPalette.vga[colorByte];
|
||||
|
||||
double endY = startY + stepY + 0.5;
|
||||
|
||||
if (endY > 0 && startY < size.height) {
|
||||
canvas.drawLine(Offset(drawX, startY), Offset(drawX, endY), paint);
|
||||
}
|
||||
startY += stepY;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(RaycasterPainter oldDelegate) => true;
|
||||
}
|
||||
54
packages/wolf_3d_renderer/lib/weapon_painter.dart
Normal file
54
packages/wolf_3d_renderer/lib/weapon_painter.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_renderer/color_palette.dart';
|
||||
|
||||
class WeaponPainter extends CustomPainter {
|
||||
final Sprite? sprite;
|
||||
|
||||
// Initialize a reusable Paint object and disable anti-aliasing to keep the
|
||||
// pixels perfectly sharp and chunky.
|
||||
final Paint _paint = Paint()
|
||||
..isAntiAlias = false
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
WeaponPainter({required this.sprite});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (sprite == null) return;
|
||||
|
||||
// Calculate width and height separately in case the container isn't a
|
||||
// perfect square
|
||||
double pixelWidth = size.width / 64;
|
||||
double pixelHeight = size.height / 64;
|
||||
|
||||
for (int x = 0; x < 64; x++) {
|
||||
for (int y = 0; y < 64; y++) {
|
||||
int colorByte = sprite![x][y];
|
||||
|
||||
if (colorByte != 255) {
|
||||
// 255 is our transparent magenta
|
||||
_paint.color = ColorPalette.vga[colorByte];
|
||||
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(
|
||||
x * pixelWidth,
|
||||
y * pixelHeight,
|
||||
// Add a tiny 0.5 overlap to completely eliminate visual seams
|
||||
pixelWidth + 0.5,
|
||||
pixelHeight + 0.5,
|
||||
),
|
||||
_paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant WeaponPainter oldDelegate) {
|
||||
// ONLY repaint if the actual animation frame (sprite) has changed!
|
||||
// This saves massive amounts of CPU when the player is just walking around.
|
||||
return oldDelegate.sprite != sprite;
|
||||
}
|
||||
}
|
||||
538
packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart
Normal file
538
packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart
Normal file
@@ -0,0 +1,538 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.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_entities/wolf_3d_entities.dart';
|
||||
import 'package:wolf_3d_input/wolf_3d_input.dart';
|
||||
import 'package:wolf_3d_renderer/hud.dart';
|
||||
import 'package:wolf_3d_renderer/raycast_painter.dart';
|
||||
import 'package:wolf_3d_renderer/weapon_painter.dart';
|
||||
|
||||
class WolfRenderer extends StatefulWidget {
|
||||
const WolfRenderer(
|
||||
this.data, {
|
||||
required this.difficulty,
|
||||
required this.startingEpisode,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final WolfensteinData data;
|
||||
final Difficulty difficulty;
|
||||
final int startingEpisode;
|
||||
|
||||
@override
|
||||
State<WolfRenderer> createState() => _WolfRendererState();
|
||||
}
|
||||
|
||||
class _WolfRendererState extends State<WolfRenderer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final InputManager inputManager = InputManager();
|
||||
final DoorManager doorManager = DoorManager();
|
||||
final PushwallManager pushwallManager = PushwallManager();
|
||||
|
||||
late Ticker _gameLoop;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
late Level currentLevel;
|
||||
late WolfLevel activeLevel;
|
||||
|
||||
final double fov = math.pi / 3;
|
||||
|
||||
late Player player;
|
||||
|
||||
bool _isLoading = true;
|
||||
|
||||
double damageFlashOpacity = 0.0;
|
||||
|
||||
late int _currentEpisodeIndex;
|
||||
late int _currentLevelIndex;
|
||||
int? _returnLevelIndex;
|
||||
|
||||
List<Entity> entities = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initGame();
|
||||
}
|
||||
|
||||
Future<void> _initGame() async {
|
||||
// 1. Setup our starting indices
|
||||
_currentEpisodeIndex = widget.startingEpisode;
|
||||
_currentLevelIndex = 0;
|
||||
|
||||
// 2. Load the first floor!
|
||||
_loadLevel();
|
||||
|
||||
_gameLoop = createTicker(_tick)..start();
|
||||
_focusNode.requestFocus();
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _loadLevel() {
|
||||
// 1. Clean up the previous level's state
|
||||
entities.clear();
|
||||
damageFlashOpacity = 0.0;
|
||||
|
||||
// 2. Grab the exact level from our new Episode hierarchy
|
||||
final episode = widget.data.episodes[_currentEpisodeIndex];
|
||||
activeLevel = episode.levels[_currentLevelIndex];
|
||||
|
||||
// 3. DEEP COPY the wall grid! If we don't do this, destroying walls/doors
|
||||
// will permanently corrupt the map data in the Wolf3d singleton.
|
||||
currentLevel = List.generate(64, (y) => List.from(activeLevel.wallGrid[y]));
|
||||
final Level objectLevel = activeLevel.objectGrid;
|
||||
|
||||
// 4. Initialize Managers
|
||||
doorManager.initDoors(currentLevel);
|
||||
pushwallManager.initPushwalls(currentLevel, objectLevel);
|
||||
|
||||
// 6. Spawn Player and Entities
|
||||
for (int y = 0; y < 64; y++) {
|
||||
for (int x = 0; x < 64; x++) {
|
||||
int objId = objectLevel[y][x];
|
||||
|
||||
if (!MapObject.shouldSpawn(objId, widget.difficulty)) continue;
|
||||
|
||||
if (objId >= MapObject.playerNorth && objId <= MapObject.playerWest) {
|
||||
double spawnAngle = 0.0;
|
||||
if (objId == MapObject.playerNorth) {
|
||||
spawnAngle = 3 * math.pi / 2;
|
||||
} else if (objId == MapObject.playerEast) {
|
||||
spawnAngle = 0.0;
|
||||
} else if (objId == MapObject.playerSouth) {
|
||||
spawnAngle = math.pi / 2;
|
||||
} else if (objId == MapObject.playerWest) {
|
||||
spawnAngle = math.pi;
|
||||
}
|
||||
|
||||
player = Player(x: x + 0.5, y: y + 0.5, angle: spawnAngle);
|
||||
} else {
|
||||
Entity? newEntity = EntityRegistry.spawn(
|
||||
objId,
|
||||
x + 0.5,
|
||||
y + 0.5,
|
||||
widget.difficulty,
|
||||
widget.data.sprites.length,
|
||||
isSharewareMode: widget.data.version == GameVersion.shareware,
|
||||
);
|
||||
if (newEntity != null) entities.add(newEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Clear non-solid blocks from the collision grid
|
||||
for (int y = 0; y < 64; y++) {
|
||||
for (int x = 0; x < 64; x++) {
|
||||
int id = currentLevel[y][x];
|
||||
if (!((id >= 1 && id <= 63) || (id >= 90 && id <= 101))) {
|
||||
currentLevel[y][x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_bumpPlayerIfStuck();
|
||||
debugPrint("Loaded Floor: ${_currentLevelIndex + 1} - ${activeLevel.name}");
|
||||
}
|
||||
|
||||
void _onLevelCompleted({bool isSecretExit = false}) {
|
||||
setState(() {
|
||||
final currentEpisode = widget.data.episodes[_currentEpisodeIndex];
|
||||
|
||||
if (isSecretExit) {
|
||||
// Save the next normal map index so we can return to it later
|
||||
_returnLevelIndex = _currentLevelIndex + 1;
|
||||
_currentLevelIndex = 9; // Jump to the secret map
|
||||
debugPrint("Found the Secret Exit!");
|
||||
} else {
|
||||
// Are we currently ON the secret map, and need to return?
|
||||
if (_currentLevelIndex == 9 && _returnLevelIndex != null) {
|
||||
_currentLevelIndex = _returnLevelIndex!;
|
||||
_returnLevelIndex = null;
|
||||
} else {
|
||||
_currentLevelIndex++; // Normal progression
|
||||
}
|
||||
}
|
||||
|
||||
// Did we just beat the last map in the episode (Map 9) or the secret map (Map 10)?
|
||||
if (_currentLevelIndex >= currentEpisode.levels.length ||
|
||||
_currentLevelIndex > 9) {
|
||||
debugPrint("Episode Completed! You win!");
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
_loadLevel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_gameLoop.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _bumpPlayerIfStuck() {
|
||||
int pX = player.x.toInt();
|
||||
int pY = player.y.toInt();
|
||||
|
||||
if (pY < 0 ||
|
||||
pY >= currentLevel.length ||
|
||||
pX < 0 ||
|
||||
pX >= currentLevel[0].length ||
|
||||
currentLevel[pY][pX] > 0) {
|
||||
double shortestDist = double.infinity;
|
||||
Coordinate2D nearestSafeSpot = Coordinate2D(1.5, 1.5);
|
||||
|
||||
for (int y = 0; y < currentLevel.length; y++) {
|
||||
for (int x = 0; x < currentLevel[y].length; x++) {
|
||||
if (currentLevel[y][x] == 0) {
|
||||
Coordinate2D safeSpot = Coordinate2D(x + 0.5, y + 0.5);
|
||||
double dist = safeSpot.distanceTo(player.position);
|
||||
|
||||
if (dist < shortestDist) {
|
||||
shortestDist = dist;
|
||||
nearestSafeSpot = safeSpot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
player.x = nearestSafeSpot.x;
|
||||
player.y = nearestSafeSpot.y;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isWalkable(int x, int y) {
|
||||
if (currentLevel[y][x] == 0) return true;
|
||||
if (currentLevel[y][x] >= 90) {
|
||||
return doorManager.isDoorOpenEnough(x, y);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- ORCHESTRATOR ---
|
||||
void _tick(Duration elapsed) {
|
||||
// 1. Process intentions and receive movement vectors
|
||||
final inputResult = _processInputs(elapsed);
|
||||
|
||||
doorManager.update(elapsed);
|
||||
pushwallManager.update(elapsed, currentLevel);
|
||||
|
||||
// 2. Explicit State Updates
|
||||
player.updateWeaponSwitch();
|
||||
|
||||
player.angle += inputResult.dAngle;
|
||||
|
||||
// Keep the angle neatly clamped between 0 and 2*PI
|
||||
if (player.angle < 0) player.angle += 2 * math.pi;
|
||||
if (player.angle >= 2 * math.pi) player.angle -= 2 * math.pi;
|
||||
|
||||
final Coordinate2D validatedPos = _calculateValidatedPosition(
|
||||
player.position,
|
||||
inputResult.movement,
|
||||
);
|
||||
|
||||
player.x = validatedPos.x;
|
||||
player.y = validatedPos.y;
|
||||
|
||||
_updateEntities(elapsed);
|
||||
|
||||
// Explicit reassignment from a pure(r) function
|
||||
damageFlashOpacity = _calculateScreenEffects(damageFlashOpacity);
|
||||
|
||||
// 3. Combat
|
||||
player.updateWeapon(
|
||||
currentTime: elapsed.inMilliseconds,
|
||||
entities: entities,
|
||||
isWalkable: _isWalkable,
|
||||
);
|
||||
|
||||
// 4. Render
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
// Returns a Record containing both movement delta and rotation delta
|
||||
({Coordinate2D movement, double dAngle}) _processInputs(Duration elapsed) {
|
||||
inputManager.update();
|
||||
|
||||
const double moveSpeed = 0.14;
|
||||
const double turnSpeed = 0.10;
|
||||
|
||||
Coordinate2D movement = const Coordinate2D(0, 0);
|
||||
double dAngle = 0.0;
|
||||
|
||||
if (inputManager.requestedWeapon != null) {
|
||||
player.requestWeaponSwitch(inputManager.requestedWeapon!);
|
||||
}
|
||||
|
||||
if (inputManager.isFiring) {
|
||||
player.fire(elapsed.inMilliseconds);
|
||||
} else {
|
||||
player.releaseTrigger();
|
||||
}
|
||||
|
||||
// Calculate intended rotation
|
||||
if (inputManager.isTurningLeft) dAngle -= turnSpeed;
|
||||
if (inputManager.isTurningRight) dAngle += turnSpeed;
|
||||
|
||||
// Calculate intended movement based on CURRENT angle
|
||||
Coordinate2D forwardVec = Coordinate2D(
|
||||
math.cos(player.angle),
|
||||
math.sin(player.angle),
|
||||
);
|
||||
|
||||
if (inputManager.isMovingForward) {
|
||||
movement += forwardVec * moveSpeed;
|
||||
}
|
||||
if (inputManager.isMovingBackward) {
|
||||
movement -= forwardVec * moveSpeed;
|
||||
}
|
||||
|
||||
if (inputManager.isInteracting) {
|
||||
// 1. Calculate the tile exactly 1 block in front of the player
|
||||
int targetX = (player.x + math.cos(player.angle)).toInt();
|
||||
int targetY = (player.y + math.sin(player.angle)).toInt();
|
||||
|
||||
// Ensure we don't check outside the map bounds
|
||||
if (targetX >= 0 && targetX < 64 && targetY >= 0 && targetY < 64) {
|
||||
// 2. Check the WALL grid for the physical switch texture
|
||||
int wallId = currentLevel[targetY][targetX];
|
||||
if (wallId == MapObject.normalElevatorSwitch) {
|
||||
// Player hit the switch!
|
||||
_onLevelCompleted(isSecretExit: false);
|
||||
return (movement: const Coordinate2D(0, 0), dAngle: 0.0);
|
||||
} else if (wallId == MapObject.secretElevatorSwitch) {
|
||||
_onLevelCompleted(isSecretExit: true);
|
||||
return (movement: const Coordinate2D(0, 0), dAngle: 0.0);
|
||||
}
|
||||
|
||||
// 3. Check the OBJECT grid for invisible floor triggers
|
||||
// (Some custom maps use these instead of wall switches)
|
||||
int objId = activeLevel.objectGrid[targetY][targetX];
|
||||
if (objId == MapObject.normalExitTrigger) {
|
||||
_onLevelCompleted(isSecretExit: false);
|
||||
return (movement: movement, dAngle: dAngle);
|
||||
} else if (objId == MapObject.secretExitTrigger) {
|
||||
_onLevelCompleted(isSecretExit: true);
|
||||
return (movement: movement, dAngle: dAngle);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. If it wasn't an elevator, try opening a door or pushing a wall
|
||||
doorManager.handleInteraction(player.x, player.y, player.angle);
|
||||
pushwallManager.handleInteraction(
|
||||
player.x,
|
||||
player.y,
|
||||
player.angle,
|
||||
currentLevel,
|
||||
);
|
||||
}
|
||||
|
||||
return (movement: movement, dAngle: dAngle);
|
||||
}
|
||||
|
||||
Coordinate2D _calculateValidatedPosition(
|
||||
Coordinate2D currentPos,
|
||||
Coordinate2D movement,
|
||||
) {
|
||||
const double margin = 0.3;
|
||||
double newX = currentPos.x;
|
||||
double newY = currentPos.y;
|
||||
|
||||
// Calculate potential new coordinates
|
||||
Coordinate2D target = currentPos + movement;
|
||||
|
||||
// Validate X (allows sliding along walls)
|
||||
if (movement.x != 0) {
|
||||
int checkX = (movement.x > 0)
|
||||
? (target.x + margin).toInt()
|
||||
: (target.x - margin).toInt();
|
||||
|
||||
if (_isWalkable(checkX, currentPos.y.toInt())) {
|
||||
newX = target.x;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Y
|
||||
if (movement.y != 0) {
|
||||
int checkY = (movement.y > 0)
|
||||
? (target.y + margin).toInt()
|
||||
: (target.y - margin).toInt();
|
||||
|
||||
if (_isWalkable(newX.toInt(), checkY)) {
|
||||
newY = target.y;
|
||||
}
|
||||
}
|
||||
|
||||
return Coordinate2D(newX, newY);
|
||||
}
|
||||
|
||||
void _updateEntities(Duration elapsed) {
|
||||
List<Entity> itemsToRemove = [];
|
||||
List<Entity> itemsToAdd = []; // NEW: Buffer for dropped items
|
||||
|
||||
for (Entity entity in entities) {
|
||||
if (entity is Enemy) {
|
||||
// 1. Get Intent (Now passing tryOpenDoor!)
|
||||
final intent = entity.update(
|
||||
elapsedMs: elapsed.inMilliseconds,
|
||||
playerPosition: player.position,
|
||||
isWalkable: _isWalkable,
|
||||
tryOpenDoor: doorManager.tryOpenDoor,
|
||||
onDamagePlayer: (int damage) {
|
||||
player.takeDamage(damage);
|
||||
damageFlashOpacity = 0.5;
|
||||
},
|
||||
);
|
||||
|
||||
// 2. Update Angle
|
||||
entity.angle = intent.newAngle;
|
||||
|
||||
// 3. Resolve Movement
|
||||
// We NO LONGER use _calculateValidatedPosition here!
|
||||
// The enemy's internal getValidMovement already did the math perfectly.
|
||||
entity.x += intent.movement.x;
|
||||
entity.y += intent.movement.y;
|
||||
|
||||
// 4. Handle Item Drops & Score (Matches KillActor in C code)
|
||||
if (entity.state == EntityState.dead &&
|
||||
entity.isDying &&
|
||||
!entity.hasDroppedItem) {
|
||||
entity.hasDroppedItem = true;
|
||||
|
||||
// Map ID 44 is usually the Ammo Clip in the Object Grid/Registry
|
||||
Entity? droppedAmmo = EntityRegistry.spawn(
|
||||
MapObject.ammoClip,
|
||||
entity.x,
|
||||
entity.y,
|
||||
widget.difficulty,
|
||||
widget.data.sprites.length,
|
||||
);
|
||||
|
||||
if (droppedAmmo != null) {
|
||||
itemsToAdd.add(droppedAmmo);
|
||||
}
|
||||
|
||||
// You will need to add a `bool hasDroppedItem = false;` to your base Enemy class.
|
||||
|
||||
if (entity.runtimeType.toString() == 'BrownGuard') {
|
||||
// Example: Spawn an ammo clip where the guard died
|
||||
// itemsToAdd.add(Collectible(x: entity.x, y: entity.y, type: CollectibleType.ammoClip));
|
||||
} else if (entity.runtimeType.toString() == 'Dog') {
|
||||
// Dogs don't drop items, but maybe they give different points!
|
||||
}
|
||||
}
|
||||
} else if (entity is Collectible) {
|
||||
if (player.position.distanceTo(entity.position) < 0.5) {
|
||||
if (player.tryPickup(entity)) {
|
||||
itemsToRemove.add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up dead items and add new drops
|
||||
if (itemsToRemove.isNotEmpty) {
|
||||
entities.removeWhere((e) => itemsToRemove.contains(e));
|
||||
}
|
||||
if (itemsToAdd.isNotEmpty) {
|
||||
entities.addAll(itemsToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
// Takes an input and returns a value instead of implicitly changing state
|
||||
double _calculateScreenEffects(double currentOpacity) {
|
||||
if (currentOpacity > 0) {
|
||||
return math.max(0.0, currentOpacity - 0.05);
|
||||
}
|
||||
return currentOpacity;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator(color: Colors.teal));
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: KeyboardListener(
|
||||
focusNode: _focusNode,
|
||||
autofocus: true,
|
||||
onKeyEvent: (_) {},
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 10,
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomPaint(
|
||||
size: Size(
|
||||
constraints.maxWidth,
|
||||
constraints.maxHeight,
|
||||
),
|
||||
painter: RaycasterPainter(
|
||||
map: currentLevel,
|
||||
textures: widget.data.walls,
|
||||
player: player,
|
||||
fov: fov,
|
||||
doorOffsets: doorManager.getOffsetsForRenderer(),
|
||||
entities: entities,
|
||||
sprites: widget.data.sprites,
|
||||
activePushwall: pushwallManager.activePushwall,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -20,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, player.weaponAnimOffset),
|
||||
child: SizedBox(
|
||||
width: 500,
|
||||
height: 500,
|
||||
child: CustomPaint(
|
||||
painter: WeaponPainter(
|
||||
sprite:
|
||||
widget.data.sprites[player
|
||||
.currentWeapon
|
||||
.getCurrentSpriteIndex(
|
||||
widget.data.sprites.length,
|
||||
)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (damageFlashOpacity > 0)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.red.withValues(
|
||||
alpha: damageFlashOpacity,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Hud(player: player),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
60
packages/wolf_3d_renderer/pubspec.yaml
Normal file
60
packages/wolf_3d_renderer/pubspec.yaml
Normal file
@@ -0,0 +1,60 @@
|
||||
name: wolf_3d_renderer
|
||||
description: "A new Flutter package project."
|
||||
version: 0.0.1
|
||||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.1
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
resolution: workspace
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
wolf_3d_data_types: any
|
||||
wolf_3d_engine: any
|
||||
wolf_3d_entities: any
|
||||
wolf_3d_input: 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
|
||||
Reference in New Issue
Block a user