This commit is contained in:
parent
bc128cef3d
commit
76408247b0
10 changed files with 598 additions and 185 deletions
|
@ -34,6 +34,14 @@
|
||||||
"debug_mode": "Debug Tilstand:",
|
"debug_mode": "Debug Tilstand:",
|
||||||
"close": "Luk"
|
"close": "Luk"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"gameplay": "Gameplay",
|
||||||
|
"accessibility": "Tilgængelighed",
|
||||||
|
"show_vision_cones": "Vis Synsfelt",
|
||||||
|
"show_detection_radius": "Vis Detektionsområder",
|
||||||
|
"vision_cones_help": "Viser synsfelt for fjender og spiller",
|
||||||
|
"detection_help": "Viser detektionsområder omkring sikkerhedsfunktioner"
|
||||||
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"placing_poop": "Placerer lortepose på position",
|
"placing_poop": "Placerer lortepose på position",
|
||||||
"attempting_doorbell": "Forsøger at ringe på døren",
|
"attempting_doorbell": "Forsøger at ringe på døren",
|
||||||
|
|
|
@ -34,6 +34,14 @@
|
||||||
"debug_mode": "Debug Modus:",
|
"debug_mode": "Debug Modus:",
|
||||||
"close": "Schließen"
|
"close": "Schließen"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"gameplay": "Gameplay",
|
||||||
|
"accessibility": "Barrierefreiheit",
|
||||||
|
"show_vision_cones": "Sichtfelder Anzeigen",
|
||||||
|
"show_detection_radius": "Erkennungsbereiche Anzeigen",
|
||||||
|
"vision_cones_help": "Zeigt Sichtfelder für Feinde und Spieler",
|
||||||
|
"detection_help": "Zeigt Erkennungsbereiche um Sicherheitsmerkmale"
|
||||||
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"placing_poop": "Kacktüte wird platziert an Position",
|
"placing_poop": "Kacktüte wird platziert an Position",
|
||||||
"attempting_doorbell": "Versuche zu klingeln",
|
"attempting_doorbell": "Versuche zu klingeln",
|
||||||
|
|
|
@ -34,6 +34,14 @@
|
||||||
"debug_mode": "Debug Mode:",
|
"debug_mode": "Debug Mode:",
|
||||||
"close": "Close"
|
"close": "Close"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"gameplay": "Gameplay",
|
||||||
|
"accessibility": "Accessibility",
|
||||||
|
"show_vision_cones": "Show Vision Cones",
|
||||||
|
"show_detection_radius": "Show Detection Areas",
|
||||||
|
"vision_cones_help": "Shows field of view for enemies and player",
|
||||||
|
"detection_help": "Shows detection areas around security features"
|
||||||
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"placing_poop": "Placing poop bag at position",
|
"placing_poop": "Placing poop bag at position",
|
||||||
"attempting_doorbell": "Attempting to ring doorbell",
|
"attempting_doorbell": "Attempting to ring doorbell",
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shitman/game/components/vision_cone.dart';
|
||||||
|
import 'package:shitman/game/shitman_game.dart';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
class Neighborhood extends Component {
|
class Neighborhood extends Component {
|
||||||
static const double streetWidth = 60.0;
|
static const double streetWidth = 60.0;
|
||||||
static const double houseSize = 80.0;
|
static const double houseSize = 80.0;
|
||||||
static const double yardSize = 40.0;
|
static const double yardSize = 40.0;
|
||||||
|
|
||||||
List<House> houses = [];
|
List<House> houses = [];
|
||||||
late List<Vector2> streetPaths;
|
late List<Vector2> streetPaths;
|
||||||
|
|
||||||
|
@ -19,44 +21,44 @@ class Neighborhood extends Component {
|
||||||
void generateNeighborhood() {
|
void generateNeighborhood() {
|
||||||
houses.clear();
|
houses.clear();
|
||||||
removeAll(children);
|
removeAll(children);
|
||||||
|
|
||||||
// Create a simple 3x3 grid of houses
|
// Create a simple 3x3 grid of houses
|
||||||
final random = Random();
|
final random = Random();
|
||||||
|
|
||||||
for (int row = 0; row < 3; row++) {
|
for (int row = 0; row < 3; row++) {
|
||||||
for (int col = 0; col < 3; col++) {
|
for (int col = 0; col < 3; col++) {
|
||||||
// Skip center for street intersection
|
// Skip center for street intersection
|
||||||
if (row == 1 && col == 1) continue;
|
if (row == 1 && col == 1) continue;
|
||||||
|
|
||||||
final housePosition = Vector2(
|
final housePosition = Vector2(
|
||||||
col * (houseSize + streetWidth) + streetWidth,
|
col * (houseSize + streetWidth) + streetWidth,
|
||||||
row * (houseSize + streetWidth) + streetWidth,
|
row * (houseSize + streetWidth) + streetWidth,
|
||||||
);
|
);
|
||||||
|
|
||||||
final house = House(
|
final house = House(
|
||||||
position: housePosition,
|
position: housePosition,
|
||||||
isTarget: false, // Target will be set separately
|
isTarget: false, // Target will be set separately
|
||||||
houseType: random.nextInt(3), // 3 different house types
|
houseType: random.nextInt(3), // 3 different house types
|
||||||
);
|
);
|
||||||
|
|
||||||
houses.add(house);
|
houses.add(house);
|
||||||
add(house);
|
add(house);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate street paths
|
// Generate street paths
|
||||||
generateStreetPaths();
|
generateStreetPaths();
|
||||||
}
|
}
|
||||||
|
|
||||||
void generateStreetPaths() {
|
void generateStreetPaths() {
|
||||||
streetPaths = [];
|
streetPaths = [];
|
||||||
|
|
||||||
// Horizontal streets
|
// Horizontal streets
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
streetPaths.add(Vector2(0, i * (houseSize + streetWidth)));
|
streetPaths.add(Vector2(0, i * (houseSize + streetWidth)));
|
||||||
streetPaths.add(Vector2(800, i * (houseSize + streetWidth)));
|
streetPaths.add(Vector2(800, i * (houseSize + streetWidth)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical streets
|
// Vertical streets
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
streetPaths.add(Vector2(i * (houseSize + streetWidth), 0));
|
streetPaths.add(Vector2(i * (houseSize + streetWidth), 0));
|
||||||
|
@ -73,32 +75,31 @@ class Neighborhood extends Component {
|
||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
super.render(canvas);
|
super.render(canvas);
|
||||||
|
|
||||||
// Draw streets
|
// Draw streets
|
||||||
final streetPaint = Paint()
|
final streetPaint = Paint()..color = const Color(0xFF333333);
|
||||||
..color = const Color(0xFF333333);
|
|
||||||
|
|
||||||
// Horizontal streets
|
// Horizontal streets
|
||||||
for (int row = 0; row <= 3; row++) {
|
for (int row = 0; row <= 3; row++) {
|
||||||
canvas.drawRect(
|
canvas.drawRect(
|
||||||
Rect.fromLTWH(
|
Rect.fromLTWH(
|
||||||
0,
|
0,
|
||||||
row * (houseSize + streetWidth) - streetWidth / 2,
|
row * (houseSize + streetWidth) - streetWidth / 2,
|
||||||
800,
|
800,
|
||||||
streetWidth
|
streetWidth,
|
||||||
),
|
),
|
||||||
streetPaint,
|
streetPaint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical streets
|
// Vertical streets
|
||||||
for (int col = 0; col <= 3; col++) {
|
for (int col = 0; col <= 3; col++) {
|
||||||
canvas.drawRect(
|
canvas.drawRect(
|
||||||
Rect.fromLTWH(
|
Rect.fromLTWH(
|
||||||
col * (houseSize + streetWidth) - streetWidth / 2,
|
col * (houseSize + streetWidth) - streetWidth / 2,
|
||||||
0,
|
0,
|
||||||
streetWidth,
|
streetWidth,
|
||||||
600
|
600,
|
||||||
),
|
),
|
||||||
streetPaint,
|
streetPaint,
|
||||||
);
|
);
|
||||||
|
@ -106,51 +107,84 @@ class Neighborhood extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class House extends RectangleComponent {
|
class House extends RectangleComponent with HasGameReference<ShitmanGame> {
|
||||||
bool isTarget;
|
bool isTarget;
|
||||||
int houseType;
|
int houseType;
|
||||||
bool hasLights = false;
|
bool hasLights = false;
|
||||||
bool hasSecurityCamera = false;
|
bool hasSecurityCamera = false;
|
||||||
bool hasWatchDog = false;
|
bool hasWatchDog = false;
|
||||||
|
|
||||||
Vector2? doorPosition;
|
Vector2? doorPosition;
|
||||||
Vector2? yardCenter;
|
Vector2? yardCenter;
|
||||||
|
VisionCone? visionCone;
|
||||||
|
|
||||||
House({
|
House({
|
||||||
required Vector2 position,
|
required Vector2 position,
|
||||||
required this.isTarget,
|
required this.isTarget,
|
||||||
required this.houseType,
|
required this.houseType,
|
||||||
}) : super(
|
}) : super(position: position, size: Vector2.all(Neighborhood.houseSize));
|
||||||
position: position,
|
|
||||||
size: Vector2.all(Neighborhood.houseSize),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
await super.onLoad();
|
await super.onLoad();
|
||||||
|
|
||||||
// Set house color based on type
|
// Set house color based on type
|
||||||
paint = Paint()..color = _getHouseColor();
|
paint = Paint()..color = _getHouseColor();
|
||||||
|
|
||||||
// Calculate door and yard positions
|
// Calculate door and yard positions
|
||||||
doorPosition = position + Vector2(size.x / 2, size.y);
|
doorPosition = position + Vector2(size.x / 2, size.y);
|
||||||
yardCenter = position + size / 2;
|
yardCenter = position + size / 2;
|
||||||
|
|
||||||
// Randomly add security features
|
// Randomly add security features
|
||||||
final random = Random();
|
final random = Random();
|
||||||
hasLights = random.nextBool();
|
hasLights = random.nextBool();
|
||||||
hasSecurityCamera = random.nextDouble() < 0.3;
|
hasSecurityCamera = random.nextDouble() < 0.3;
|
||||||
hasWatchDog = random.nextDouble() < 0.2;
|
hasWatchDog = random.nextDouble() < 0.2;
|
||||||
|
|
||||||
|
// Create vision cone for houses with security cameras
|
||||||
|
if (hasSecurityCamera) {
|
||||||
|
_createVisionCone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createVisionCone() {
|
||||||
|
// Create vision cone facing towards the street
|
||||||
|
final cameraPosition = Vector2(size.x * 0.9, size.y * 0.1);
|
||||||
|
final direction = _getOptimalCameraDirection();
|
||||||
|
|
||||||
|
visionCone = VisionCone(
|
||||||
|
origin: cameraPosition,
|
||||||
|
direction: direction,
|
||||||
|
range: 120.0,
|
||||||
|
fov: pi / 2, // 90 degrees
|
||||||
|
color: Colors.red,
|
||||||
|
opacity: 0.2,
|
||||||
|
);
|
||||||
|
add(visionCone!);
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getOptimalCameraDirection() {
|
||||||
|
// Point camera towards street/center area
|
||||||
|
final centerOfMap = Vector2(400, 300);
|
||||||
|
final houseCenter = position + size / 2;
|
||||||
|
final toCenter = centerOfMap - houseCenter;
|
||||||
|
return atan2(toCenter.y, toCenter.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color _getHouseColor() {
|
Color _getHouseColor() {
|
||||||
switch (houseType) {
|
switch (houseType) {
|
||||||
case 0:
|
case 0:
|
||||||
return isTarget ? const Color(0xFFFF6B6B) : const Color(0xFF8B4513); // Brown/Red if target
|
return isTarget
|
||||||
|
? const Color(0xFFFF6B6B)
|
||||||
|
: const Color(0xFF8B4513); // Brown/Red if target
|
||||||
case 1:
|
case 1:
|
||||||
return isTarget ? const Color(0xFFFF6B6B) : const Color(0xFF4682B4); // Blue/Red if target
|
return isTarget
|
||||||
|
? const Color(0xFFFF6B6B)
|
||||||
|
: const Color(0xFF4682B4); // Blue/Red if target
|
||||||
case 2:
|
case 2:
|
||||||
return isTarget ? const Color(0xFFFF6B6B) : const Color(0xFF228B22); // Green/Red if target
|
return isTarget
|
||||||
|
? const Color(0xFFFF6B6B)
|
||||||
|
: const Color(0xFF228B22); // Green/Red if target
|
||||||
default:
|
default:
|
||||||
return const Color(0xFF696969);
|
return const Color(0xFF696969);
|
||||||
}
|
}
|
||||||
|
@ -159,58 +193,69 @@ class House extends RectangleComponent {
|
||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
super.render(canvas);
|
super.render(canvas);
|
||||||
|
|
||||||
// Draw door
|
// Draw door
|
||||||
final doorPaint = Paint()
|
final doorPaint = Paint()..color = const Color(0xFF654321);
|
||||||
..color = const Color(0xFF654321);
|
|
||||||
canvas.drawRect(
|
canvas.drawRect(
|
||||||
Rect.fromLTWH(size.x / 2 - 8, size.y - 4, 16, 4),
|
Rect.fromLTWH(size.x / 2 - 8, size.y - 4, 16, 4),
|
||||||
doorPaint,
|
doorPaint,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw windows
|
// Draw windows
|
||||||
final windowPaint = Paint()
|
final windowPaint =
|
||||||
..color = hasLights ? const Color(0xFFFFFF00) : const Color(0xFF87CEEB);
|
Paint()
|
||||||
|
..color =
|
||||||
|
hasLights ? const Color(0xFFFFFF00) : const Color(0xFF87CEEB);
|
||||||
|
|
||||||
// Left window
|
// Left window
|
||||||
canvas.drawRect(
|
canvas.drawRect(
|
||||||
Rect.fromLTWH(size.x * 0.2, size.y * 0.3, 12, 12),
|
Rect.fromLTWH(size.x * 0.2, size.y * 0.3, 12, 12),
|
||||||
windowPaint,
|
windowPaint,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Right window
|
// Right window
|
||||||
canvas.drawRect(
|
canvas.drawRect(
|
||||||
Rect.fromLTWH(size.x * 0.7, size.y * 0.3, 12, 12),
|
Rect.fromLTWH(size.x * 0.7, size.y * 0.3, 12, 12),
|
||||||
windowPaint,
|
windowPaint,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw security features
|
// Draw security features
|
||||||
if (hasSecurityCamera) {
|
if (hasSecurityCamera) {
|
||||||
final cameraPaint = Paint()
|
final cameraPaint = Paint()..color = const Color(0xFF000000);
|
||||||
..color = const Color(0xFF000000);
|
canvas.drawCircle(Offset(size.x * 0.9, size.y * 0.1), 4, cameraPaint);
|
||||||
canvas.drawCircle(
|
|
||||||
Offset(size.x * 0.9, size.y * 0.1),
|
|
||||||
4,
|
|
||||||
cameraPaint,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasWatchDog) {
|
if (hasWatchDog) {
|
||||||
// Draw dog house in yard
|
// Draw dog house in yard
|
||||||
final dogHousePaint = Paint()
|
final dogHousePaint = Paint()..color = const Color(0xFF8B4513);
|
||||||
..color = const Color(0xFF8B4513);
|
canvas.drawRect(Rect.fromLTWH(-20, size.y + 10, 15, 15), dogHousePaint);
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(-20, size.y + 10, 15, 15),
|
|
||||||
dogHousePaint,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw detection radius if setting is enabled
|
||||||
|
try {
|
||||||
|
if (game.appSettings.getBool('game.show_detection_radius')) {
|
||||||
|
final radiusPaint =
|
||||||
|
Paint()
|
||||||
|
..color = const Color(0xFFFF9800).withValues(alpha: 0.2)
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 2.0;
|
||||||
|
canvas.drawCircle(
|
||||||
|
Offset(size.x / 2, size.y / 2),
|
||||||
|
getDetectionRadius(),
|
||||||
|
radiusPaint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Settings not ready, skip rendering
|
||||||
|
}
|
||||||
|
|
||||||
// Draw target indicator
|
// Draw target indicator
|
||||||
if (isTarget) {
|
if (isTarget) {
|
||||||
final targetPaint = Paint()
|
final targetPaint =
|
||||||
..color = const Color(0xFFFF0000)
|
Paint()
|
||||||
..style = PaintingStyle.stroke
|
..color = const Color(0xFFFF0000)
|
||||||
..strokeWidth = 3.0;
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 3.0;
|
||||||
canvas.drawCircle(
|
canvas.drawCircle(
|
||||||
Offset(size.x / 2, size.y / 2),
|
Offset(size.x / 2, size.y / 2),
|
||||||
size.x / 2 + 10,
|
size.x / 2 + 10,
|
||||||
|
@ -220,17 +265,45 @@ class House extends RectangleComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
double getDetectionRadius() {
|
double getDetectionRadius() {
|
||||||
double radius = 50.0;
|
double radius = 30.0; // Reduced base radius
|
||||||
if (hasLights) radius += 20.0;
|
if (hasLights) radius += 10.0; // Reduced light bonus
|
||||||
if (hasSecurityCamera) radius += 40.0;
|
if (hasSecurityCamera) radius += 20.0; // Reduced camera bonus
|
||||||
if (hasWatchDog) radius += 30.0;
|
if (hasWatchDog) radius += 15.0; // Reduced dog bonus
|
||||||
return radius;
|
return radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canDetectPlayer(Vector2 playerPosition, double playerStealthLevel) {
|
bool canDetectPlayer(Vector2 playerPosition, double playerStealthLevel) {
|
||||||
|
// Basic radius detection
|
||||||
final distance = (playerPosition - yardCenter!).length;
|
final distance = (playerPosition - yardCenter!).length;
|
||||||
final detectionRadius = getDetectionRadius() * (1.0 - playerStealthLevel);
|
final detectionRadius = getDetectionRadius() * (1.0 - playerStealthLevel);
|
||||||
|
|
||||||
return distance < detectionRadius;
|
bool radiusDetection = distance < detectionRadius;
|
||||||
|
|
||||||
|
// Vision cone detection for security cameras
|
||||||
|
bool visionDetection = false;
|
||||||
|
if (hasSecurityCamera && visionCone != null) {
|
||||||
|
visionDetection =
|
||||||
|
visionCone!.canSee(playerPosition) &&
|
||||||
|
visionCone!.hasLineOfSight(playerPosition, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
return radiusDetection || visionDetection;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@override
|
||||||
|
void update(double dt) {
|
||||||
|
super.update(dt);
|
||||||
|
|
||||||
|
// Update vision cone visibility based on settings
|
||||||
|
if (visionCone != null) {
|
||||||
|
try {
|
||||||
|
final showVisionCones = game.appSettings.getBool(
|
||||||
|
'game.show_vision_cones',
|
||||||
|
);
|
||||||
|
visionCone!.updateOpacity(showVisionCones ? 0.3 : 0.0);
|
||||||
|
} catch (e) {
|
||||||
|
visionCone!.updateOpacity(0.0); // Hide if settings not ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,52 +1,64 @@
|
||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame/events.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:shitman/game/shitman_game.dart';
|
import 'package:shitman/game/shitman_game.dart';
|
||||||
import 'package:shitman/game/components/poop_bag.dart';
|
import 'package:shitman/game/components/poop_bag.dart';
|
||||||
import 'package:shitman/game/components/neighborhood.dart';
|
import 'package:shitman/game/components/neighborhood.dart';
|
||||||
|
import 'package:shitman/game/components/vision_cone.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
|
class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
|
||||||
static const double speed = 100.0;
|
static const double speed = 100.0;
|
||||||
static const double playerSize = 32.0;
|
static const double playerSize = 32.0;
|
||||||
|
|
||||||
Vector2 velocity = Vector2.zero();
|
Vector2 velocity = Vector2.zero();
|
||||||
bool hasPoopBag = true;
|
bool hasPoopBag = true;
|
||||||
bool isHidden = false;
|
bool isHidden = false;
|
||||||
double stealthLevel = 0.0; // 0.0 = fully visible, 1.0 = completely hidden
|
double stealthLevel = 0.0; // 0.0 = fully visible, 1.0 = completely hidden
|
||||||
PoopBag? placedPoopBag;
|
PoopBag? placedPoopBag;
|
||||||
|
VisionCone? playerVisionCone;
|
||||||
|
double lastMovementDirection = 0.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
await super.onLoad();
|
await super.onLoad();
|
||||||
|
|
||||||
// Create a simple colored rectangle as player
|
// Create a simple colored rectangle as player
|
||||||
size = Vector2.all(playerSize);
|
size = Vector2.all(playerSize);
|
||||||
position = Vector2(400, 300); // Start in center
|
position = Vector2(200, 200); // Start at center intersection
|
||||||
|
|
||||||
// Set player color
|
// Set player color
|
||||||
paint = Paint()..color = const Color(0xFF0000FF); // Blue player
|
paint = Paint()..color = const Color(0xFF0000FF); // Blue player
|
||||||
}
|
|
||||||
|
|
||||||
|
// Create player vision cone
|
||||||
|
playerVisionCone = VisionCone(
|
||||||
|
origin: size / 2, // Relative to player center
|
||||||
|
direction: lastMovementDirection,
|
||||||
|
range: 80.0,
|
||||||
|
fov: pi / 2, // 90 degrees
|
||||||
|
color: Colors.blue,
|
||||||
|
opacity: 0.0, // Start hidden
|
||||||
|
);
|
||||||
|
add(playerVisionCone!);
|
||||||
|
}
|
||||||
|
|
||||||
void handleInput(Set<LogicalKeyboardKey> keysPressed) {
|
void handleInput(Set<LogicalKeyboardKey> keysPressed) {
|
||||||
velocity = Vector2.zero();
|
velocity = Vector2.zero();
|
||||||
|
|
||||||
// Movement controls
|
// Movement controls
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowUp) ||
|
if (keysPressed.contains(LogicalKeyboardKey.arrowUp) ||
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyW)) {
|
keysPressed.contains(LogicalKeyboardKey.keyW)) {
|
||||||
velocity.y -= speed;
|
velocity.y -= speed;
|
||||||
}
|
}
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowDown) ||
|
if (keysPressed.contains(LogicalKeyboardKey.arrowDown) ||
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyS)) {
|
keysPressed.contains(LogicalKeyboardKey.keyS)) {
|
||||||
velocity.y += speed;
|
velocity.y += speed;
|
||||||
}
|
}
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) ||
|
if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) ||
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyA)) {
|
keysPressed.contains(LogicalKeyboardKey.keyA)) {
|
||||||
velocity.x -= speed;
|
velocity.x -= speed;
|
||||||
}
|
}
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowRight) ||
|
if (keysPressed.contains(LogicalKeyboardKey.arrowRight) ||
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyD)) {
|
keysPressed.contains(LogicalKeyboardKey.keyD)) {
|
||||||
velocity.x += speed;
|
velocity.x += speed;
|
||||||
}
|
}
|
||||||
|
@ -62,7 +74,6 @@ class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void updateStealthLevel(double dt) {
|
void updateStealthLevel(double dt) {
|
||||||
// Simple stealth calculation - can be enhanced later
|
// Simple stealth calculation - can be enhanced later
|
||||||
// For now, player is more hidden when moving slowly or not at all
|
// For now, player is more hidden when moving slowly or not at all
|
||||||
|
@ -71,36 +82,37 @@ class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
|
||||||
} else {
|
} else {
|
||||||
stealthLevel = (stealthLevel - dt * 1.5).clamp(0.0, 1.0);
|
stealthLevel = (stealthLevel - dt * 1.5).clamp(0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
isHidden = stealthLevel > 0.7;
|
isHidden = stealthLevel > 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
void placePoop() {
|
void placePoop() {
|
||||||
if (!hasPoopBag) return;
|
if (!hasPoopBag) return;
|
||||||
|
|
||||||
debugPrint('Placing poop bag at $position');
|
debugPrint('Placing poop bag at $position');
|
||||||
|
|
||||||
// Create and place the poop bag
|
// Create and place the poop bag
|
||||||
placedPoopBag = PoopBag();
|
placedPoopBag = PoopBag();
|
||||||
placedPoopBag!.position = position + Vector2(playerSize / 2, playerSize + 10);
|
placedPoopBag!.position =
|
||||||
|
position + Vector2(playerSize / 2, playerSize + 10);
|
||||||
game.world.add(placedPoopBag!);
|
game.world.add(placedPoopBag!);
|
||||||
|
|
||||||
hasPoopBag = false;
|
hasPoopBag = false;
|
||||||
|
|
||||||
// Check if near target house
|
// Check if near target house
|
||||||
checkMissionProgress();
|
checkMissionProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ringDoorbell() {
|
void ringDoorbell() {
|
||||||
debugPrint('Attempting to ring doorbell');
|
debugPrint('Attempting to ring doorbell');
|
||||||
|
|
||||||
// Check if near target house door
|
// Check if near target house door
|
||||||
if (game.targetHouse.isPlayerNearTarget(position)) {
|
if (game.targetHouse.isPlayerNearTarget(position)) {
|
||||||
if (placedPoopBag != null) {
|
if (placedPoopBag != null) {
|
||||||
// Light the poop bag on fire
|
// Light the poop bag on fire
|
||||||
placedPoopBag!.lightOnFire();
|
placedPoopBag!.lightOnFire();
|
||||||
debugPrint('Ding dong! Poop bag is lit! RUN!');
|
debugPrint('Ding dong! Poop bag is lit! RUN!');
|
||||||
|
|
||||||
// Start escape timer - player has limited time to escape
|
// Start escape timer - player has limited time to escape
|
||||||
startEscapeSequence();
|
startEscapeSequence();
|
||||||
} else {
|
} else {
|
||||||
|
@ -140,28 +152,47 @@ class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
|
||||||
@override
|
@override
|
||||||
void update(double dt) {
|
void update(double dt) {
|
||||||
super.update(dt);
|
super.update(dt);
|
||||||
|
|
||||||
// Apply movement
|
// Apply movement
|
||||||
if (velocity.length > 0) {
|
if (velocity.length > 0) {
|
||||||
velocity = velocity.normalized() * speed;
|
velocity = velocity.normalized() * speed;
|
||||||
position += velocity * dt;
|
position += velocity * dt;
|
||||||
|
|
||||||
|
// Update movement direction for vision cone
|
||||||
|
lastMovementDirection = atan2(velocity.y, velocity.x);
|
||||||
|
|
||||||
// Keep player on screen (basic bounds checking)
|
// Keep player on screen (basic bounds checking)
|
||||||
position.x = position.x.clamp(0, 800 - size.x);
|
position.x = position.x.clamp(0, 800 - size.x);
|
||||||
position.y = position.y.clamp(0, 600 - size.y);
|
position.y = position.y.clamp(0, 600 - size.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stealth level based on environment
|
// Update stealth level based on environment
|
||||||
updateStealthLevel(dt);
|
updateStealthLevel(dt);
|
||||||
|
|
||||||
// Check for detection by houses
|
// Check for detection by houses
|
||||||
checkForDetection();
|
checkForDetection();
|
||||||
|
|
||||||
|
// Update player vision cone
|
||||||
|
if (playerVisionCone != null) {
|
||||||
|
playerVisionCone!.updatePosition(size / 2, lastMovementDirection);
|
||||||
|
|
||||||
|
// Update vision cone visibility based on settings
|
||||||
|
try {
|
||||||
|
final showVisionCones = game.appSettings.getBool(
|
||||||
|
'game.show_vision_cones',
|
||||||
|
);
|
||||||
|
playerVisionCone!.updateOpacity(showVisionCones ? 0.2 : 0.0);
|
||||||
|
} catch (e) {
|
||||||
|
playerVisionCone!.updateOpacity(0.0); // Hide if settings not ready
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkForDetection() {
|
void checkForDetection() {
|
||||||
final neighborhood = game.world.children.whereType<Neighborhood>().firstOrNull;
|
final neighborhood =
|
||||||
|
game.world.children.whereType<Neighborhood>().firstOrNull;
|
||||||
if (neighborhood == null) return;
|
if (neighborhood == null) return;
|
||||||
|
|
||||||
for (final house in neighborhood.houses) {
|
for (final house in neighborhood.houses) {
|
||||||
if (house.canDetectPlayer(position, stealthLevel)) {
|
if (house.canDetectPlayer(position, stealthLevel)) {
|
||||||
getDetected();
|
getDetected();
|
||||||
|
@ -173,21 +204,29 @@ class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
|
||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
// Update paint color based on stealth level
|
// Update paint color based on stealth level
|
||||||
paint = Paint()
|
paint =
|
||||||
..color = isHidden ?
|
Paint()
|
||||||
const Color(0xFF00FF00).withOpacity(0.7) : // Green when hidden
|
..color =
|
||||||
const Color(0xFF0000FF).withOpacity(0.9); // Blue when visible
|
isHidden
|
||||||
|
? const Color(0xFF00FF00).withValues(alpha: 0.7)
|
||||||
|
: // Green when hidden
|
||||||
|
const Color(
|
||||||
|
0xFF0000FF,
|
||||||
|
).withValues(alpha: 0.9); // Blue when visible
|
||||||
|
|
||||||
super.render(canvas);
|
super.render(canvas);
|
||||||
|
|
||||||
// Draw stealth indicator in debug mode
|
// Draw stealth indicator in debug mode
|
||||||
if (game.debugMode) {
|
if (game.debugMode) {
|
||||||
final stealthPaint = Paint()
|
final stealthPaint =
|
||||||
..color = Color.lerp(const Color(0xFFFF0000), const Color(0xFF00FF00), stealthLevel)!;
|
Paint()
|
||||||
canvas.drawRect(
|
..color =
|
||||||
Rect.fromLTWH(-5, -10, size.x + 10, 5),
|
Color.lerp(
|
||||||
stealthPaint,
|
const Color(0xFFFF0000),
|
||||||
);
|
const Color(0xFF00FF00),
|
||||||
|
stealthLevel,
|
||||||
|
)!;
|
||||||
|
canvas.drawRect(Rect.fromLTWH(-5, -10, size.x + 10, 5), stealthPaint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame/effects.dart';
|
import 'package:flame/effects.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
enum PoopBagState { placed, lit, burning, extinguished }
|
enum PoopBagState { placed, lit, burning, extinguished }
|
||||||
|
@ -11,17 +10,17 @@ class PoopBag extends CircleComponent {
|
||||||
double burnTimer = 0.0;
|
double burnTimer = 0.0;
|
||||||
static const double burnDuration = 3.0; // seconds to burn
|
static const double burnDuration = 3.0; // seconds to burn
|
||||||
static const double bagSize = 16.0;
|
static const double bagSize = 16.0;
|
||||||
|
|
||||||
late Vector2 smokeOffset;
|
late Vector2 smokeOffset;
|
||||||
List<SmokeParticle> smokeParticles = [];
|
List<SmokeParticle> smokeParticles = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
await super.onLoad();
|
await super.onLoad();
|
||||||
|
|
||||||
radius = bagSize / 2;
|
radius = bagSize / 2;
|
||||||
paint = Paint()..color = const Color(0xFF8B4513); // Brown color
|
paint = Paint()..color = const Color(0xFF8B4513); // Brown color
|
||||||
|
|
||||||
smokeOffset = Vector2(0, -radius - 5);
|
smokeOffset = Vector2(0, -radius - 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +28,7 @@ class PoopBag extends CircleComponent {
|
||||||
if (state == PoopBagState.placed) {
|
if (state == PoopBagState.placed) {
|
||||||
state = PoopBagState.lit;
|
state = PoopBagState.lit;
|
||||||
burnTimer = 0.0;
|
burnTimer = 0.0;
|
||||||
|
|
||||||
// Add flame effect
|
// Add flame effect
|
||||||
add(
|
add(
|
||||||
ScaleEffect.to(
|
ScaleEffect.to(
|
||||||
|
@ -37,7 +36,7 @@ class PoopBag extends CircleComponent {
|
||||||
EffectController(duration: 0.5, infinite: true, reverseDuration: 0.5),
|
EffectController(duration: 0.5, infinite: true, reverseDuration: 0.5),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint('Poop bag is now on fire!');
|
debugPrint('Poop bag is now on fire!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,22 +44,23 @@ class PoopBag extends CircleComponent {
|
||||||
@override
|
@override
|
||||||
void update(double dt) {
|
void update(double dt) {
|
||||||
super.update(dt);
|
super.update(dt);
|
||||||
|
|
||||||
if (state == PoopBagState.lit) {
|
if (state == PoopBagState.lit) {
|
||||||
burnTimer += dt;
|
burnTimer += dt;
|
||||||
|
|
||||||
// Generate smoke particles
|
// Generate smoke particles
|
||||||
if (burnTimer % 0.2 < dt) { // Every 0.2 seconds
|
if (burnTimer % 0.2 < dt) {
|
||||||
|
// Every 0.2 seconds
|
||||||
generateSmokeParticle();
|
generateSmokeParticle();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if fully burned
|
// Check if fully burned
|
||||||
if (burnTimer >= burnDuration) {
|
if (burnTimer >= burnDuration) {
|
||||||
state = PoopBagState.burning;
|
state = PoopBagState.burning;
|
||||||
extinguish();
|
extinguish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update smoke particles
|
// Update smoke particles
|
||||||
smokeParticles.removeWhere((particle) {
|
smokeParticles.removeWhere((particle) {
|
||||||
particle.update(dt);
|
particle.update(dt);
|
||||||
|
@ -71,10 +71,10 @@ class PoopBag extends CircleComponent {
|
||||||
void generateSmokeParticle() {
|
void generateSmokeParticle() {
|
||||||
final random = Random();
|
final random = Random();
|
||||||
final particle = SmokeParticle(
|
final particle = SmokeParticle(
|
||||||
position: position + smokeOffset + Vector2(
|
position:
|
||||||
random.nextDouble() * 10 - 5,
|
position +
|
||||||
random.nextDouble() * 5,
|
smokeOffset +
|
||||||
),
|
Vector2(random.nextDouble() * 10 - 5, random.nextDouble() * 5),
|
||||||
);
|
);
|
||||||
smokeParticles.add(particle);
|
smokeParticles.add(particle);
|
||||||
}
|
}
|
||||||
|
@ -82,26 +82,28 @@ class PoopBag extends CircleComponent {
|
||||||
void extinguish() {
|
void extinguish() {
|
||||||
state = PoopBagState.extinguished;
|
state = PoopBagState.extinguished;
|
||||||
removeAll(children.whereType<Effect>());
|
removeAll(children.whereType<Effect>());
|
||||||
|
|
||||||
// Change to burnt color
|
// Change to burnt color
|
||||||
paint = Paint()..color = const Color(0xFF2F2F2F);
|
paint = Paint()..color = const Color(0xFF2F2F2F);
|
||||||
|
|
||||||
debugPrint('Poop bag has burned out');
|
debugPrint('Poop bag has burned out');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
super.render(canvas);
|
super.render(canvas);
|
||||||
|
|
||||||
// Draw flame effect when lit
|
// Draw flame effect when lit
|
||||||
if (state == PoopBagState.lit) {
|
if (state == PoopBagState.lit) {
|
||||||
final flamePaint = Paint()
|
final flamePaint =
|
||||||
..color = Color.lerp(
|
Paint()
|
||||||
const Color(0xFFFF4500),
|
..color =
|
||||||
const Color(0xFFFFD700),
|
Color.lerp(
|
||||||
sin(burnTimer * 10) * 0.5 + 0.5,
|
const Color(0xFFFF4500),
|
||||||
)!;
|
const Color(0xFFFFD700),
|
||||||
|
sin(burnTimer * 10) * 0.5 + 0.5,
|
||||||
|
)!;
|
||||||
|
|
||||||
// Draw flickering flame
|
// Draw flickering flame
|
||||||
canvas.drawCircle(
|
canvas.drawCircle(
|
||||||
Offset(0, -radius - 5),
|
Offset(0, -radius - 5),
|
||||||
|
@ -109,7 +111,7 @@ class PoopBag extends CircleComponent {
|
||||||
flamePaint,
|
flamePaint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render smoke particles
|
// Render smoke particles
|
||||||
for (final particle in smokeParticles) {
|
for (final particle in smokeParticles) {
|
||||||
particle.render(canvas);
|
particle.render(canvas);
|
||||||
|
@ -127,8 +129,8 @@ class SmokeParticle {
|
||||||
double life;
|
double life;
|
||||||
double maxLife;
|
double maxLife;
|
||||||
bool shouldRemove = false;
|
bool shouldRemove = false;
|
||||||
|
|
||||||
SmokeParticle({required this.position})
|
SmokeParticle({required this.position})
|
||||||
: velocity = Vector2(
|
: velocity = Vector2(
|
||||||
Random().nextDouble() * 20 - 10,
|
Random().nextDouble() * 20 - 10,
|
||||||
-Random().nextDouble() * 30 - 20,
|
-Random().nextDouble() * 30 - 20,
|
||||||
|
@ -140,7 +142,7 @@ class SmokeParticle {
|
||||||
position += velocity * dt;
|
position += velocity * dt;
|
||||||
velocity *= 0.98; // Slight air resistance
|
velocity *= 0.98; // Slight air resistance
|
||||||
life -= dt;
|
life -= dt;
|
||||||
|
|
||||||
if (life <= 0) {
|
if (life <= 0) {
|
||||||
shouldRemove = true;
|
shouldRemove = true;
|
||||||
}
|
}
|
||||||
|
@ -148,13 +150,13 @@ class SmokeParticle {
|
||||||
|
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
final alpha = (life / maxLife).clamp(0.0, 1.0);
|
final alpha = (life / maxLife).clamp(0.0, 1.0);
|
||||||
final smokePaint = Paint()
|
final smokePaint =
|
||||||
..color = Color(0xFF666666).withOpacity(alpha * 0.3);
|
Paint()..color = Color(0xFF666666).withValues(alpha: alpha * 0.3);
|
||||||
|
|
||||||
canvas.drawCircle(
|
canvas.drawCircle(
|
||||||
Offset(position.x, position.y),
|
Offset(position.x, position.y),
|
||||||
6.0 * (1.0 - life / maxLife),
|
6.0 * (1.0 - life / maxLife),
|
||||||
smokePaint,
|
smokePaint,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
158
lib/game/components/vision_cone.dart
Normal file
158
lib/game/components/vision_cone.dart
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
class VisionCone extends PositionComponent {
|
||||||
|
double direction; // Angle in radians
|
||||||
|
final double range;
|
||||||
|
final double fov; // Field of view angle in radians
|
||||||
|
final Color color;
|
||||||
|
double opacity;
|
||||||
|
|
||||||
|
List<Vector2> visionVertices = [];
|
||||||
|
late Paint fillPaint;
|
||||||
|
late Paint strokePaint;
|
||||||
|
|
||||||
|
VisionCone({
|
||||||
|
required Vector2 origin,
|
||||||
|
required this.direction,
|
||||||
|
this.range = 150.0,
|
||||||
|
this.fov = pi / 3, // 60 degrees
|
||||||
|
this.color = Colors.red,
|
||||||
|
this.opacity = 0.3,
|
||||||
|
}) : super(position: origin);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> onLoad() async {
|
||||||
|
await super.onLoad();
|
||||||
|
|
||||||
|
fillPaint = Paint()
|
||||||
|
..color = color.withValues(alpha: opacity)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
strokePaint = Paint()
|
||||||
|
..color = color.withValues(alpha: opacity + 0.2)
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 2.0;
|
||||||
|
|
||||||
|
_calculateVisionCone();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePosition(Vector2 newOrigin, double newDirection) {
|
||||||
|
position.setFrom(newOrigin);
|
||||||
|
direction = newDirection;
|
||||||
|
_calculateVisionCone();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _calculateVisionCone() {
|
||||||
|
visionVertices.clear();
|
||||||
|
|
||||||
|
// Start from the origin (0,0 relative to this component)
|
||||||
|
visionVertices.add(Vector2.zero());
|
||||||
|
|
||||||
|
// Calculate the two edge rays of the vision cone
|
||||||
|
final leftAngle = direction - fov / 2;
|
||||||
|
final rightAngle = direction + fov / 2;
|
||||||
|
|
||||||
|
// Create points along the vision cone arc
|
||||||
|
const int arcPoints = 10;
|
||||||
|
for (int i = 0; i <= arcPoints; i++) {
|
||||||
|
final angle = leftAngle + (rightAngle - leftAngle) * (i / arcPoints);
|
||||||
|
final point = Vector2(cos(angle), sin(angle)) * range;
|
||||||
|
visionVertices.add(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the cone back to origin
|
||||||
|
visionVertices.add(Vector2.zero());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a point is within the vision cone
|
||||||
|
bool canSee(Vector2 point) {
|
||||||
|
// Get the world position of this vision cone by adding parent position
|
||||||
|
final parentPosition = (parent as PositionComponent?)?.position ?? Vector2.zero();
|
||||||
|
final worldPosition = parentPosition + position;
|
||||||
|
final toPoint = point - worldPosition;
|
||||||
|
final distance = toPoint.length;
|
||||||
|
|
||||||
|
|
||||||
|
// Check if within range
|
||||||
|
if (distance > range) return false;
|
||||||
|
|
||||||
|
// Check if within angle
|
||||||
|
final angleToPoint = atan2(toPoint.y, toPoint.x);
|
||||||
|
final angleDiff = _normalizeAngle(angleToPoint - direction);
|
||||||
|
|
||||||
|
return angleDiff.abs() <= fov / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normalize angle to [-pi, pi]
|
||||||
|
double _normalizeAngle(double angle) {
|
||||||
|
while (angle > pi) {
|
||||||
|
angle -= 2 * pi;
|
||||||
|
}
|
||||||
|
while (angle < -pi) {
|
||||||
|
angle += 2 * pi;
|
||||||
|
}
|
||||||
|
return angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raycast from origin to target, checking for obstructions
|
||||||
|
bool hasLineOfSight(Vector2 target, List<Component> obstacles) {
|
||||||
|
// Simple line-of-sight check - can be enhanced with proper raycasting
|
||||||
|
final parentPosition = (parent as PositionComponent?)?.position ?? Vector2.zero();
|
||||||
|
final worldPosition = parentPosition + position;
|
||||||
|
final rayDirection = target - worldPosition;
|
||||||
|
final distance = rayDirection.length;
|
||||||
|
final normalizedDir = rayDirection.normalized();
|
||||||
|
|
||||||
|
// Check points along the ray for obstacles
|
||||||
|
const double stepSize = 5.0;
|
||||||
|
for (double step = stepSize; step < distance; step += stepSize) {
|
||||||
|
final checkPoint = worldPosition + normalizedDir * step;
|
||||||
|
|
||||||
|
// Check if this point intersects with any obstacles
|
||||||
|
for (final obstacle in obstacles) {
|
||||||
|
if (obstacle is RectangleComponent) {
|
||||||
|
if (obstacle.containsPoint(checkPoint)) {
|
||||||
|
return false; // Line of sight blocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Clear line of sight
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateOpacity(double newOpacity) {
|
||||||
|
opacity = newOpacity;
|
||||||
|
fillPaint = Paint()
|
||||||
|
..color = color.withValues(alpha: opacity)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
strokePaint = Paint()
|
||||||
|
..color = color.withValues(alpha: opacity + 0.2)
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void render(Canvas canvas) {
|
||||||
|
if (visionVertices.length < 3 || opacity <= 0.0) return;
|
||||||
|
|
||||||
|
// Create path for vision cone
|
||||||
|
final path = Path();
|
||||||
|
path.moveTo(visionVertices[0].x, visionVertices[0].y);
|
||||||
|
|
||||||
|
for (int i = 1; i < visionVertices.length; i++) {
|
||||||
|
path.lineTo(visionVertices[i].x, visionVertices[i].y);
|
||||||
|
}
|
||||||
|
path.close();
|
||||||
|
|
||||||
|
// Draw filled vision cone
|
||||||
|
canvas.drawPath(path, fillPaint);
|
||||||
|
|
||||||
|
// Draw vision cone outline
|
||||||
|
canvas.drawPath(path, strokePaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,15 +15,14 @@ import 'package:shitman/settings/app_settings.dart';
|
||||||
|
|
||||||
enum GameState { mainMenu, playing, paused, gameOver, missionComplete }
|
enum GameState { mainMenu, playing, paused, gameOver, missionComplete }
|
||||||
|
|
||||||
class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollisionDetection, AppSettings {
|
class ShitmanGame extends FlameGame
|
||||||
|
with HasKeyboardHandlerComponents, HasCollisionDetection, AppSettings {
|
||||||
late Player player;
|
late Player player;
|
||||||
late Neighborhood neighborhood;
|
late Neighborhood neighborhood;
|
||||||
late TargetHouse targetHouse;
|
late TargetHouse targetHouse;
|
||||||
late CameraComponent gameCamera;
|
late CameraComponent gameCamera;
|
||||||
|
|
||||||
GameState gameState = GameState.mainMenu;
|
GameState gameState = GameState.mainMenu;
|
||||||
@override
|
|
||||||
bool debugMode = false;
|
|
||||||
int missionScore = 0;
|
int missionScore = 0;
|
||||||
int totalMissions = 0;
|
int totalMissions = 0;
|
||||||
bool infiniteMode = false;
|
bool infiniteMode = false;
|
||||||
|
@ -32,17 +31,22 @@ class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollis
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
await super.onLoad();
|
await super.onLoad();
|
||||||
await initSettings();
|
await initSettings();
|
||||||
|
|
||||||
// Setup camera
|
// Setup camera
|
||||||
gameCamera = CameraComponent.withFixedResolution(
|
gameCamera = CameraComponent.withFixedResolution(
|
||||||
world: world,
|
world: world,
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
);
|
);
|
||||||
addAll([gameCamera, world]);
|
camera = gameCamera;
|
||||||
|
addAll([world]);
|
||||||
|
|
||||||
// Initialize debug mode from settings
|
// Initialize debug mode from settings
|
||||||
debugMode = appSettings.getBool('game.debug_mode');
|
try {
|
||||||
|
debugMode = appSettings.getBool('game.debug_mode');
|
||||||
|
} catch (e) {
|
||||||
|
debugMode = false; // Fallback if settings not ready
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void startGame() {
|
void startGame() {
|
||||||
|
@ -74,19 +78,19 @@ class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollis
|
||||||
void initializeLevel() {
|
void initializeLevel() {
|
||||||
// Clear previous level
|
// Clear previous level
|
||||||
world.removeAll(world.children);
|
world.removeAll(world.children);
|
||||||
|
|
||||||
// Create neighborhood
|
// Create neighborhood
|
||||||
neighborhood = Neighborhood();
|
neighborhood = Neighborhood();
|
||||||
world.add(neighborhood);
|
world.add(neighborhood);
|
||||||
|
|
||||||
// Create target house
|
// Create target house
|
||||||
targetHouse = TargetHouse();
|
targetHouse = TargetHouse();
|
||||||
world.add(targetHouse);
|
world.add(targetHouse);
|
||||||
|
|
||||||
// Create player
|
// Create player
|
||||||
player = Player();
|
player = Player();
|
||||||
world.add(player);
|
world.add(player);
|
||||||
|
|
||||||
// Setup camera to follow player
|
// Setup camera to follow player
|
||||||
gameCamera.follow(player);
|
gameCamera.follow(player);
|
||||||
}
|
}
|
||||||
|
@ -95,7 +99,7 @@ class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollis
|
||||||
gameState = GameState.missionComplete;
|
gameState = GameState.missionComplete;
|
||||||
missionScore += 100;
|
missionScore += 100;
|
||||||
totalMissions++;
|
totalMissions++;
|
||||||
|
|
||||||
if (infiniteMode) {
|
if (infiniteMode) {
|
||||||
// Generate new mission after delay
|
// Generate new mission after delay
|
||||||
Future.delayed(Duration(seconds: 2), () {
|
Future.delayed(Duration(seconds: 2), () {
|
||||||
|
@ -111,34 +115,38 @@ class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollis
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
KeyEventResult onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
|
KeyEventResult onKeyEvent(
|
||||||
|
KeyEvent event,
|
||||||
|
Set<LogicalKeyboardKey> keysPressed,
|
||||||
|
) {
|
||||||
|
super.onKeyEvent(event, keysPressed);
|
||||||
if (gameState != GameState.playing) return KeyEventResult.ignored;
|
if (gameState != GameState.playing) return KeyEventResult.ignored;
|
||||||
|
|
||||||
// Handle pause
|
// Handle pause
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.escape)) {
|
if (keysPressed.contains(LogicalKeyboardKey.escape)) {
|
||||||
pauseGame();
|
pauseGame();
|
||||||
overlays.add('PauseMenu');
|
overlays.add('PauseMenu');
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle player input
|
// Handle player input
|
||||||
player.handleInput(keysPressed);
|
player.handleInput(keysPressed);
|
||||||
|
|
||||||
// Handle action keys on key down
|
// Handle action keys on key down
|
||||||
if (event is KeyDownEvent) {
|
if (event is KeyDownEvent) {
|
||||||
player.handleAction(event.logicalKey);
|
player.handleAction(event.logicalKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void update(double dt) {
|
void update(double dt) {
|
||||||
super.update(dt);
|
super.update(dt);
|
||||||
|
|
||||||
// Only update game logic when playing
|
// Only update game logic when playing
|
||||||
if (gameState != GameState.playing) return;
|
if (gameState != GameState.playing) return;
|
||||||
|
|
||||||
// Game-specific update logic here
|
// Game-specific update logic here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,5 +7,11 @@ final gameSettings = SettingsGroup(
|
||||||
items: [
|
items: [
|
||||||
/// Debug mode, additional elements to help with development
|
/// Debug mode, additional elements to help with development
|
||||||
BoolSetting(key: 'debug_mode', defaultValue: false),
|
BoolSetting(key: 'debug_mode', defaultValue: false),
|
||||||
|
|
||||||
|
/// Show vision cones for enemies and player (accessibility & debugging)
|
||||||
|
BoolSetting(key: 'show_vision_cones', defaultValue: false),
|
||||||
|
|
||||||
|
/// Show detection radius circles around houses
|
||||||
|
BoolSetting(key: 'show_detection_radius', defaultValue: false),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -154,12 +154,40 @@ class MainMenuUI extends StatelessWidget with AppSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsUI extends StatelessWidget with AppSettings {
|
class SettingsUI extends StatefulWidget with AppSettings {
|
||||||
static const String overlayID = 'Settings';
|
static const String overlayID = 'Settings';
|
||||||
final ShitmanGame game;
|
final ShitmanGame game;
|
||||||
|
|
||||||
SettingsUI(this.game, {super.key});
|
SettingsUI(this.game, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsUI> createState() => _SettingsUIState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsUIState extends State<SettingsUI> with AppSettings {
|
||||||
|
bool showVisionCones = false;
|
||||||
|
bool showDetectionRadius = false;
|
||||||
|
bool debugMode = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadSettings() {
|
||||||
|
try {
|
||||||
|
showVisionCones = widget.game.appSettings.getBool('game.show_vision_cones');
|
||||||
|
showDetectionRadius = widget.game.appSettings.getBool('game.show_detection_radius');
|
||||||
|
debugMode = widget.game.appSettings.getBool('game.debug_mode');
|
||||||
|
} catch (e) {
|
||||||
|
// Settings not ready, use defaults
|
||||||
|
showVisionCones = false;
|
||||||
|
showDetectionRadius = false;
|
||||||
|
debugMode = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return Material(
|
||||||
|
@ -185,8 +213,8 @@ class SettingsUI extends StatelessWidget with AppSettings {
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.close, color: Colors.white),
|
icon: Icon(Icons.close, color: Colors.white),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
game.overlays.remove(SettingsUI.overlayID);
|
widget.game.overlays.remove(SettingsUI.overlayID);
|
||||||
game.overlays.add(MainMenuUI.overlayID);
|
widget.game.overlays.add(MainMenuUI.overlayID);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -194,29 +222,100 @@ class SettingsUI extends StatelessWidget with AppSettings {
|
||||||
SizedBox(height: 24),
|
SizedBox(height: 24),
|
||||||
|
|
||||||
// Language selector
|
// Language selector
|
||||||
|
Text('Language / Sprog / Sprache:', style: TextStyle(color: Colors.white)),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
NesDropdownMenu<String>(
|
||||||
|
initialValue: context.locale.languageCode,
|
||||||
|
entries: const [
|
||||||
|
NesDropdownMenuEntry(
|
||||||
|
value: 'en',
|
||||||
|
label: 'English',
|
||||||
|
),
|
||||||
|
NesDropdownMenuEntry(
|
||||||
|
value: 'da',
|
||||||
|
label: 'Dansk',
|
||||||
|
),
|
||||||
|
NesDropdownMenuEntry(
|
||||||
|
value: 'de',
|
||||||
|
label: 'Deutsch',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (String? value) {
|
||||||
|
if (value != null) {
|
||||||
|
context.setLocale(Locale(value));
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Gameplay Settings Section
|
||||||
|
Text(
|
||||||
|
'settings.gameplay'.tr(),
|
||||||
|
style: TextStyle(color: Colors.orange, fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Vision Cones toggle
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text('Language / Sprog / Sprache:', style: TextStyle(color: Colors.white)),
|
Expanded(
|
||||||
DropdownButton<String>(
|
child: Column(
|
||||||
value: context.locale.languageCode,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
dropdownColor: Colors.black,
|
children: [
|
||||||
style: TextStyle(color: Colors.white),
|
Text('settings.show_vision_cones'.tr(), style: TextStyle(color: Colors.white)),
|
||||||
items: [
|
Text('settings.vision_cones_help'.tr(), style: TextStyle(color: Colors.white60, fontSize: 11)),
|
||||||
DropdownMenuItem(value: 'en', child: Text('English', style: TextStyle(color: Colors.white))),
|
],
|
||||||
DropdownMenuItem(value: 'da', child: Text('Dansk', style: TextStyle(color: Colors.white))),
|
),
|
||||||
DropdownMenuItem(value: 'de', child: Text('Deutsch', style: TextStyle(color: Colors.white))),
|
),
|
||||||
],
|
NesCheckBox(
|
||||||
onChanged: (String? newValue) {
|
value: showVisionCones,
|
||||||
if (newValue != null) {
|
onChange: (value) async {
|
||||||
context.setLocale(Locale(newValue));
|
setState(() {
|
||||||
}
|
showVisionCones = value;
|
||||||
|
});
|
||||||
|
await widget.game.appSettings.setBool('game.show_vision_cones', value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Detection Radius toggle
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('settings.show_detection_radius'.tr(), style: TextStyle(color: Colors.white)),
|
||||||
|
Text('settings.detection_help'.tr(), style: TextStyle(color: Colors.white60, fontSize: 11)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NesCheckBox(
|
||||||
|
value: showDetectionRadius,
|
||||||
|
onChange: (value) async {
|
||||||
|
setState(() {
|
||||||
|
showDetectionRadius = value;
|
||||||
|
});
|
||||||
|
await widget.game.appSettings.setBool('game.show_detection_radius', value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Accessibility Section
|
||||||
|
Text(
|
||||||
|
'settings.accessibility'.tr(),
|
||||||
|
style: TextStyle(color: Colors.orange, fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
|
||||||
// Debug mode toggle
|
// Debug mode toggle
|
||||||
Row(
|
Row(
|
||||||
|
@ -224,9 +323,13 @@ class SettingsUI extends StatelessWidget with AppSettings {
|
||||||
children: [
|
children: [
|
||||||
Text('ui.debug_mode'.tr(), style: TextStyle(color: Colors.white)),
|
Text('ui.debug_mode'.tr(), style: TextStyle(color: Colors.white)),
|
||||||
NesCheckBox(
|
NesCheckBox(
|
||||||
value: false, // TODO: Connect to settings
|
value: debugMode,
|
||||||
onChange: (value) {
|
onChange: (value) async {
|
||||||
// TODO: Update settings
|
setState(() {
|
||||||
|
debugMode = value;
|
||||||
|
});
|
||||||
|
await widget.game.appSettings.setBool('game.debug_mode', value);
|
||||||
|
widget.game.debugMode = value;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -237,8 +340,8 @@ class SettingsUI extends StatelessWidget with AppSettings {
|
||||||
child: NesButton(
|
child: NesButton(
|
||||||
type: NesButtonType.primary,
|
type: NesButtonType.primary,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
game.overlays.remove(SettingsUI.overlayID);
|
widget.game.overlays.remove(SettingsUI.overlayID);
|
||||||
game.overlays.add(MainMenuUI.overlayID);
|
widget.game.overlays.add(MainMenuUI.overlayID);
|
||||||
},
|
},
|
||||||
child: Text('menu.back_to_menu'.tr()),
|
child: Text('menu.back_to_menu'.tr()),
|
||||||
),
|
),
|
||||||
|
|
Loading…
Add table
Reference in a new issue