some basic movement
Some checks failed
/ build-web (push) Has been cancelled

This commit is contained in:
zeyus 2025-07-22 22:35:21 +02:00
parent bc128cef3d
commit 76408247b0
Signed by: zeyus
GPG key ID: A836639BA719C614
10 changed files with 598 additions and 185 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -1,5 +1,7 @@
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 {
@ -75,8 +77,7 @@ class Neighborhood extends Component {
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++) {
@ -85,7 +86,7 @@ class Neighborhood extends Component {
0, 0,
row * (houseSize + streetWidth) - streetWidth / 2, row * (houseSize + streetWidth) - streetWidth / 2,
800, 800,
streetWidth streetWidth,
), ),
streetPaint, streetPaint,
); );
@ -98,7 +99,7 @@ class Neighborhood extends Component {
col * (houseSize + streetWidth) - streetWidth / 2, col * (houseSize + streetWidth) - streetWidth / 2,
0, 0,
streetWidth, streetWidth,
600 600,
), ),
streetPaint, streetPaint,
); );
@ -106,7 +107,7 @@ 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;
@ -115,15 +116,13 @@ class House extends RectangleComponent {
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 {
@ -141,16 +140,51 @@ class House extends RectangleComponent {
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);
} }
@ -161,16 +195,17 @@ class House extends RectangleComponent {
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(
@ -186,28 +221,38 @@ class House extends RectangleComponent {
// 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 =
Paint()
..color = const Color(0xFFFF0000) ..color = const Color(0xFFFF0000)
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 3.0; ..strokeWidth = 3.0;
@ -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
}
}
} }
} }

View file

@ -1,11 +1,11 @@
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;
@ -16,6 +16,8 @@ class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
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 {
@ -23,12 +25,22 @@ class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
// 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();
@ -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
@ -82,7 +93,8 @@ class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
// 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;
@ -146,6 +158,9 @@ class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
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);
@ -156,10 +171,26 @@ class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
// 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) {
@ -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);
} }
} }
} }

View file

@ -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 }
@ -50,7 +49,8 @@ class PoopBag extends CircleComponent {
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();
} }
@ -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);
} }
@ -95,8 +95,10 @@ class PoopBag extends CircleComponent {
// 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()
..color =
Color.lerp(
const Color(0xFFFF4500), const Color(0xFFFF4500),
const Color(0xFFFFD700), const Color(0xFFFFD700),
sin(burnTimer * 10) * 0.5 + 0.5, sin(burnTimer * 10) * 0.5 + 0.5,
@ -148,8 +150,8 @@ 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),

View 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);
}
}

View file

@ -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;
@ -39,10 +38,15 @@ class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollis
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
try {
debugMode = appSettings.getBool('game.debug_mode'); debugMode = appSettings.getBool('game.debug_mode');
} catch (e) {
debugMode = false; // Fallback if settings not ready
}
} }
void startGame() { void startGame() {
@ -111,7 +115,11 @@ 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

View file

@ -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),
], ],
); );

View file

@ -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))),
], ],
onChanged: (String? newValue) { ),
if (newValue != null) { ),
context.setLocale(Locale(newValue)); NesCheckBox(
} value: showVisionCones,
onChange: (value) async {
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()),
), ),