From 76408247b06be9c464553001707db5f2599a756d Mon Sep 17 00:00:00 2001 From: zeyus Date: Tue, 22 Jul 2025 22:35:21 +0200 Subject: [PATCH] some basic movement --- assets/translations/da.json | 8 + assets/translations/de.json | 8 + assets/translations/en.json | 8 + lib/game/components/neighborhood.dart | 211 +++++++++++++++++--------- lib/game/components/player.dart | 119 ++++++++++----- lib/game/components/poop_bag.dart | 68 +++++---- lib/game/components/vision_cone.dart | 158 +++++++++++++++++++ lib/game/shitman_game.dart | 48 +++--- lib/settings/game.dart | 6 + lib/ui/in_game_ui.dart | 149 +++++++++++++++--- 10 files changed, 598 insertions(+), 185 deletions(-) create mode 100644 lib/game/components/vision_cone.dart diff --git a/assets/translations/da.json b/assets/translations/da.json index 5059418..85392c3 100644 --- a/assets/translations/da.json +++ b/assets/translations/da.json @@ -34,6 +34,14 @@ "debug_mode": "Debug Tilstand:", "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": { "placing_poop": "Placerer lortepose på position", "attempting_doorbell": "Forsøger at ringe på døren", diff --git a/assets/translations/de.json b/assets/translations/de.json index 1080788..1c8def9 100644 --- a/assets/translations/de.json +++ b/assets/translations/de.json @@ -34,6 +34,14 @@ "debug_mode": "Debug Modus:", "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": { "placing_poop": "Kacktüte wird platziert an Position", "attempting_doorbell": "Versuche zu klingeln", diff --git a/assets/translations/en.json b/assets/translations/en.json index 3b232e4..b4a233a 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -34,6 +34,14 @@ "debug_mode": "Debug Mode:", "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": { "placing_poop": "Placing poop bag at position", "attempting_doorbell": "Attempting to ring doorbell", diff --git a/lib/game/components/neighborhood.dart b/lib/game/components/neighborhood.dart index 9639c6a..41fd66a 100644 --- a/lib/game/components/neighborhood.dart +++ b/lib/game/components/neighborhood.dart @@ -1,12 +1,14 @@ import 'package:flame/components.dart'; import 'package:flutter/material.dart'; +import 'package:shitman/game/components/vision_cone.dart'; +import 'package:shitman/game/shitman_game.dart'; import 'dart:math'; class Neighborhood extends Component { static const double streetWidth = 60.0; static const double houseSize = 80.0; static const double yardSize = 40.0; - + List houses = []; late List streetPaths; @@ -19,44 +21,44 @@ class Neighborhood extends Component { void generateNeighborhood() { houses.clear(); removeAll(children); - + // Create a simple 3x3 grid of houses final random = Random(); - + for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { // Skip center for street intersection if (row == 1 && col == 1) continue; - + final housePosition = Vector2( col * (houseSize + streetWidth) + streetWidth, row * (houseSize + streetWidth) + streetWidth, ); - + final house = House( position: housePosition, isTarget: false, // Target will be set separately houseType: random.nextInt(3), // 3 different house types ); - + houses.add(house); add(house); } } - + // Generate street paths generateStreetPaths(); } void generateStreetPaths() { streetPaths = []; - + // Horizontal streets for (int i = 0; i < 4; i++) { streetPaths.add(Vector2(0, i * (houseSize + streetWidth))); streetPaths.add(Vector2(800, i * (houseSize + streetWidth))); } - + // Vertical streets for (int i = 0; i < 4; i++) { streetPaths.add(Vector2(i * (houseSize + streetWidth), 0)); @@ -73,32 +75,31 @@ class Neighborhood extends Component { @override void render(Canvas canvas) { super.render(canvas); - + // Draw streets - final streetPaint = Paint() - ..color = const Color(0xFF333333); - + final streetPaint = Paint()..color = const Color(0xFF333333); + // Horizontal streets for (int row = 0; row <= 3; row++) { canvas.drawRect( Rect.fromLTWH( - 0, - row * (houseSize + streetWidth) - streetWidth / 2, - 800, - streetWidth + 0, + row * (houseSize + streetWidth) - streetWidth / 2, + 800, + streetWidth, ), streetPaint, ); } - + // Vertical streets for (int col = 0; col <= 3; col++) { canvas.drawRect( Rect.fromLTWH( - col * (houseSize + streetWidth) - streetWidth / 2, - 0, - streetWidth, - 600 + col * (houseSize + streetWidth) - streetWidth / 2, + 0, + streetWidth, + 600, ), streetPaint, ); @@ -106,51 +107,84 @@ class Neighborhood extends Component { } } -class House extends RectangleComponent { +class House extends RectangleComponent with HasGameReference { bool isTarget; int houseType; bool hasLights = false; bool hasSecurityCamera = false; bool hasWatchDog = false; - + Vector2? doorPosition; Vector2? yardCenter; + VisionCone? visionCone; House({ required Vector2 position, required this.isTarget, required this.houseType, - }) : super( - position: position, - size: Vector2.all(Neighborhood.houseSize), - ); + }) : super(position: position, size: Vector2.all(Neighborhood.houseSize)); @override Future onLoad() async { await super.onLoad(); - + // Set house color based on type paint = Paint()..color = _getHouseColor(); - + // Calculate door and yard positions doorPosition = position + Vector2(size.x / 2, size.y); yardCenter = position + size / 2; - + // Randomly add security features final random = Random(); hasLights = random.nextBool(); hasSecurityCamera = random.nextDouble() < 0.3; 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() { switch (houseType) { 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: - 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: - 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: return const Color(0xFF696969); } @@ -159,58 +193,69 @@ class House extends RectangleComponent { @override void render(Canvas canvas) { super.render(canvas); - + // Draw door - final doorPaint = Paint() - ..color = const Color(0xFF654321); + final doorPaint = Paint()..color = const Color(0xFF654321); canvas.drawRect( Rect.fromLTWH(size.x / 2 - 8, size.y - 4, 16, 4), doorPaint, ); - + // Draw windows - final windowPaint = Paint() - ..color = hasLights ? const Color(0xFFFFFF00) : const Color(0xFF87CEEB); - + final windowPaint = + Paint() + ..color = + hasLights ? const Color(0xFFFFFF00) : const Color(0xFF87CEEB); + // Left window canvas.drawRect( Rect.fromLTWH(size.x * 0.2, size.y * 0.3, 12, 12), windowPaint, ); - + // Right window canvas.drawRect( Rect.fromLTWH(size.x * 0.7, size.y * 0.3, 12, 12), windowPaint, ); - + // Draw security features if (hasSecurityCamera) { - final cameraPaint = Paint() - ..color = const Color(0xFF000000); - canvas.drawCircle( - Offset(size.x * 0.9, size.y * 0.1), - 4, - cameraPaint, - ); + final cameraPaint = Paint()..color = const Color(0xFF000000); + canvas.drawCircle(Offset(size.x * 0.9, size.y * 0.1), 4, cameraPaint); } - + if (hasWatchDog) { // Draw dog house in yard - final dogHousePaint = Paint() - ..color = const Color(0xFF8B4513); - canvas.drawRect( - Rect.fromLTWH(-20, size.y + 10, 15, 15), - dogHousePaint, - ); + final dogHousePaint = Paint()..color = const Color(0xFF8B4513); + 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 if (isTarget) { - final targetPaint = Paint() - ..color = const Color(0xFFFF0000) - ..style = PaintingStyle.stroke - ..strokeWidth = 3.0; + final targetPaint = + Paint() + ..color = const Color(0xFFFF0000) + ..style = PaintingStyle.stroke + ..strokeWidth = 3.0; canvas.drawCircle( Offset(size.x / 2, size.y / 2), size.x / 2 + 10, @@ -220,17 +265,45 @@ class House extends RectangleComponent { } double getDetectionRadius() { - double radius = 50.0; - if (hasLights) radius += 20.0; - if (hasSecurityCamera) radius += 40.0; - if (hasWatchDog) radius += 30.0; + double radius = 30.0; // Reduced base radius + if (hasLights) radius += 10.0; // Reduced light bonus + if (hasSecurityCamera) radius += 20.0; // Reduced camera bonus + if (hasWatchDog) radius += 15.0; // Reduced dog bonus return radius; } bool canDetectPlayer(Vector2 playerPosition, double playerStealthLevel) { + // Basic radius detection final distance = (playerPosition - yardCenter!).length; 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; } -} \ No newline at end of file + + @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 + } + } + } +} diff --git a/lib/game/components/player.dart b/lib/game/components/player.dart index deaefbc..7f00404 100644 --- a/lib/game/components/player.dart +++ b/lib/game/components/player.dart @@ -1,52 +1,64 @@ import 'package:flame/components.dart'; -import 'package:flame/events.dart'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart'; import 'package:shitman/game/shitman_game.dart'; import 'package:shitman/game/components/poop_bag.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 { static const double speed = 100.0; static const double playerSize = 32.0; - + Vector2 velocity = Vector2.zero(); bool hasPoopBag = true; bool isHidden = false; double stealthLevel = 0.0; // 0.0 = fully visible, 1.0 = completely hidden PoopBag? placedPoopBag; + VisionCone? playerVisionCone; + double lastMovementDirection = 0.0; @override Future onLoad() async { await super.onLoad(); - + // Create a simple colored rectangle as player size = Vector2.all(playerSize); - position = Vector2(400, 300); // Start in center - + position = Vector2(200, 200); // Start at center intersection + // Set player color 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 keysPressed) { velocity = Vector2.zero(); - + // Movement controls - if (keysPressed.contains(LogicalKeyboardKey.arrowUp) || + if (keysPressed.contains(LogicalKeyboardKey.arrowUp) || keysPressed.contains(LogicalKeyboardKey.keyW)) { velocity.y -= speed; } - if (keysPressed.contains(LogicalKeyboardKey.arrowDown) || + if (keysPressed.contains(LogicalKeyboardKey.arrowDown) || keysPressed.contains(LogicalKeyboardKey.keyS)) { velocity.y += speed; } - if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) || + if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) || keysPressed.contains(LogicalKeyboardKey.keyA)) { velocity.x -= speed; } - if (keysPressed.contains(LogicalKeyboardKey.arrowRight) || + if (keysPressed.contains(LogicalKeyboardKey.arrowRight) || keysPressed.contains(LogicalKeyboardKey.keyD)) { velocity.x += speed; } @@ -62,7 +74,6 @@ class Player extends RectangleComponent with HasGameReference { } } - void updateStealthLevel(double dt) { // Simple stealth calculation - can be enhanced later // For now, player is more hidden when moving slowly or not at all @@ -71,36 +82,37 @@ class Player extends RectangleComponent with HasGameReference { } else { stealthLevel = (stealthLevel - dt * 1.5).clamp(0.0, 1.0); } - + isHidden = stealthLevel > 0.7; } void placePoop() { if (!hasPoopBag) return; - + debugPrint('Placing poop bag at $position'); - + // Create and place the poop bag placedPoopBag = PoopBag(); - placedPoopBag!.position = position + Vector2(playerSize / 2, playerSize + 10); + placedPoopBag!.position = + position + Vector2(playerSize / 2, playerSize + 10); game.world.add(placedPoopBag!); - + hasPoopBag = false; - + // Check if near target house checkMissionProgress(); } void ringDoorbell() { debugPrint('Attempting to ring doorbell'); - + // Check if near target house door if (game.targetHouse.isPlayerNearTarget(position)) { if (placedPoopBag != null) { // Light the poop bag on fire placedPoopBag!.lightOnFire(); debugPrint('Ding dong! Poop bag is lit! RUN!'); - + // Start escape timer - player has limited time to escape startEscapeSequence(); } else { @@ -140,28 +152,47 @@ class Player extends RectangleComponent with HasGameReference { @override void update(double dt) { super.update(dt); - + // Apply movement if (velocity.length > 0) { velocity = velocity.normalized() * speed; position += velocity * dt; - + + // Update movement direction for vision cone + lastMovementDirection = atan2(velocity.y, velocity.x); + // Keep player on screen (basic bounds checking) position.x = position.x.clamp(0, 800 - size.x); position.y = position.y.clamp(0, 600 - size.y); } - + // Update stealth level based on environment updateStealthLevel(dt); - + // Check for detection by houses 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() { - final neighborhood = game.world.children.whereType().firstOrNull; + final neighborhood = + game.world.children.whereType().firstOrNull; if (neighborhood == null) return; - + for (final house in neighborhood.houses) { if (house.canDetectPlayer(position, stealthLevel)) { getDetected(); @@ -173,21 +204,29 @@ class Player extends RectangleComponent with HasGameReference { @override void render(Canvas canvas) { // Update paint color based on stealth level - paint = Paint() - ..color = isHidden ? - const Color(0xFF00FF00).withOpacity(0.7) : // Green when hidden - const Color(0xFF0000FF).withOpacity(0.9); // Blue when visible - + paint = + Paint() + ..color = + isHidden + ? const Color(0xFF00FF00).withValues(alpha: 0.7) + : // Green when hidden + const Color( + 0xFF0000FF, + ).withValues(alpha: 0.9); // Blue when visible + super.render(canvas); - + // Draw stealth indicator in debug mode if (game.debugMode) { - final stealthPaint = Paint() - ..color = Color.lerp(const Color(0xFFFF0000), const Color(0xFF00FF00), stealthLevel)!; - canvas.drawRect( - Rect.fromLTWH(-5, -10, size.x + 10, 5), - stealthPaint, - ); + final stealthPaint = + Paint() + ..color = + Color.lerp( + const Color(0xFFFF0000), + const Color(0xFF00FF00), + stealthLevel, + )!; + canvas.drawRect(Rect.fromLTWH(-5, -10, size.x + 10, 5), stealthPaint); } } -} \ No newline at end of file +} diff --git a/lib/game/components/poop_bag.dart b/lib/game/components/poop_bag.dart index 2160337..22d9e3f 100644 --- a/lib/game/components/poop_bag.dart +++ b/lib/game/components/poop_bag.dart @@ -1,7 +1,6 @@ import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart'; import 'dart:math'; enum PoopBagState { placed, lit, burning, extinguished } @@ -11,17 +10,17 @@ class PoopBag extends CircleComponent { double burnTimer = 0.0; static const double burnDuration = 3.0; // seconds to burn static const double bagSize = 16.0; - + late Vector2 smokeOffset; List smokeParticles = []; @override Future onLoad() async { await super.onLoad(); - + radius = bagSize / 2; paint = Paint()..color = const Color(0xFF8B4513); // Brown color - + smokeOffset = Vector2(0, -radius - 5); } @@ -29,7 +28,7 @@ class PoopBag extends CircleComponent { if (state == PoopBagState.placed) { state = PoopBagState.lit; burnTimer = 0.0; - + // Add flame effect add( ScaleEffect.to( @@ -37,7 +36,7 @@ class PoopBag extends CircleComponent { EffectController(duration: 0.5, infinite: true, reverseDuration: 0.5), ), ); - + debugPrint('Poop bag is now on fire!'); } } @@ -45,22 +44,23 @@ class PoopBag extends CircleComponent { @override void update(double dt) { super.update(dt); - + if (state == PoopBagState.lit) { burnTimer += dt; - + // Generate smoke particles - if (burnTimer % 0.2 < dt) { // Every 0.2 seconds + if (burnTimer % 0.2 < dt) { + // Every 0.2 seconds generateSmokeParticle(); } - + // Check if fully burned if (burnTimer >= burnDuration) { state = PoopBagState.burning; extinguish(); } } - + // Update smoke particles smokeParticles.removeWhere((particle) { particle.update(dt); @@ -71,10 +71,10 @@ class PoopBag extends CircleComponent { void generateSmokeParticle() { final random = Random(); final particle = SmokeParticle( - position: position + smokeOffset + Vector2( - random.nextDouble() * 10 - 5, - random.nextDouble() * 5, - ), + position: + position + + smokeOffset + + Vector2(random.nextDouble() * 10 - 5, random.nextDouble() * 5), ); smokeParticles.add(particle); } @@ -82,26 +82,28 @@ class PoopBag extends CircleComponent { void extinguish() { state = PoopBagState.extinguished; removeAll(children.whereType()); - + // Change to burnt color paint = Paint()..color = const Color(0xFF2F2F2F); - + debugPrint('Poop bag has burned out'); } @override void render(Canvas canvas) { super.render(canvas); - + // Draw flame effect when lit if (state == PoopBagState.lit) { - final flamePaint = Paint() - ..color = Color.lerp( - const Color(0xFFFF4500), - const Color(0xFFFFD700), - sin(burnTimer * 10) * 0.5 + 0.5, - )!; - + final flamePaint = + Paint() + ..color = + Color.lerp( + const Color(0xFFFF4500), + const Color(0xFFFFD700), + sin(burnTimer * 10) * 0.5 + 0.5, + )!; + // Draw flickering flame canvas.drawCircle( Offset(0, -radius - 5), @@ -109,7 +111,7 @@ class PoopBag extends CircleComponent { flamePaint, ); } - + // Render smoke particles for (final particle in smokeParticles) { particle.render(canvas); @@ -127,8 +129,8 @@ class SmokeParticle { double life; double maxLife; bool shouldRemove = false; - - SmokeParticle({required this.position}) + + SmokeParticle({required this.position}) : velocity = Vector2( Random().nextDouble() * 20 - 10, -Random().nextDouble() * 30 - 20, @@ -140,7 +142,7 @@ class SmokeParticle { position += velocity * dt; velocity *= 0.98; // Slight air resistance life -= dt; - + if (life <= 0) { shouldRemove = true; } @@ -148,13 +150,13 @@ class SmokeParticle { void render(Canvas canvas) { final alpha = (life / maxLife).clamp(0.0, 1.0); - final smokePaint = Paint() - ..color = Color(0xFF666666).withOpacity(alpha * 0.3); - + final smokePaint = + Paint()..color = Color(0xFF666666).withValues(alpha: alpha * 0.3); + canvas.drawCircle( Offset(position.x, position.y), 6.0 * (1.0 - life / maxLife), smokePaint, ); } -} \ No newline at end of file +} diff --git a/lib/game/components/vision_cone.dart b/lib/game/components/vision_cone.dart new file mode 100644 index 0000000..750c832 --- /dev/null +++ b/lib/game/components/vision_cone.dart @@ -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 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 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 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); + } + +} \ No newline at end of file diff --git a/lib/game/shitman_game.dart b/lib/game/shitman_game.dart index 818b43a..ddbf4d6 100644 --- a/lib/game/shitman_game.dart +++ b/lib/game/shitman_game.dart @@ -15,15 +15,14 @@ import 'package:shitman/settings/app_settings.dart'; 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 Neighborhood neighborhood; late TargetHouse targetHouse; late CameraComponent gameCamera; - + GameState gameState = GameState.mainMenu; - @override - bool debugMode = false; int missionScore = 0; int totalMissions = 0; bool infiniteMode = false; @@ -32,17 +31,22 @@ class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollis Future onLoad() async { await super.onLoad(); await initSettings(); - + // Setup camera gameCamera = CameraComponent.withFixedResolution( world: world, width: 800, height: 600, ); - addAll([gameCamera, world]); - + camera = gameCamera; + addAll([world]); + // 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() { @@ -74,19 +78,19 @@ class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollis void initializeLevel() { // Clear previous level world.removeAll(world.children); - + // Create neighborhood neighborhood = Neighborhood(); world.add(neighborhood); - + // Create target house targetHouse = TargetHouse(); world.add(targetHouse); - + // Create player player = Player(); world.add(player); - + // Setup camera to follow player gameCamera.follow(player); } @@ -95,7 +99,7 @@ class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollis gameState = GameState.missionComplete; missionScore += 100; totalMissions++; - + if (infiniteMode) { // Generate new mission after delay Future.delayed(Duration(seconds: 2), () { @@ -111,34 +115,38 @@ class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollis } @override - KeyEventResult onKeyEvent(KeyEvent event, Set keysPressed) { + KeyEventResult onKeyEvent( + KeyEvent event, + Set keysPressed, + ) { + super.onKeyEvent(event, keysPressed); if (gameState != GameState.playing) return KeyEventResult.ignored; - + // Handle pause if (keysPressed.contains(LogicalKeyboardKey.escape)) { pauseGame(); overlays.add('PauseMenu'); return KeyEventResult.handled; } - + // Handle player input player.handleInput(keysPressed); - + // Handle action keys on key down if (event is KeyDownEvent) { player.handleAction(event.logicalKey); } - + return KeyEventResult.handled; } @override void update(double dt) { super.update(dt); - + // Only update game logic when playing if (gameState != GameState.playing) return; - + // Game-specific update logic here } } diff --git a/lib/settings/game.dart b/lib/settings/game.dart index 7bd0bc1..fdb21cd 100644 --- a/lib/settings/game.dart +++ b/lib/settings/game.dart @@ -7,5 +7,11 @@ final gameSettings = SettingsGroup( items: [ /// Debug mode, additional elements to help with development 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), ], ); diff --git a/lib/ui/in_game_ui.dart b/lib/ui/in_game_ui.dart index 79860fd..61a5da0 100644 --- a/lib/ui/in_game_ui.dart +++ b/lib/ui/in_game_ui.dart @@ -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'; final ShitmanGame game; SettingsUI(this.game, {super.key}); + @override + State createState() => _SettingsUIState(); +} + +class _SettingsUIState extends State 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 Widget build(BuildContext context) { return Material( @@ -185,8 +213,8 @@ class SettingsUI extends StatelessWidget with AppSettings { IconButton( icon: Icon(Icons.close, color: Colors.white), onPressed: () { - game.overlays.remove(SettingsUI.overlayID); - game.overlays.add(MainMenuUI.overlayID); + widget.game.overlays.remove(SettingsUI.overlayID); + widget.game.overlays.add(MainMenuUI.overlayID); }, ), ], @@ -194,29 +222,100 @@ class SettingsUI extends StatelessWidget with AppSettings { SizedBox(height: 24), // Language selector + Text('Language / Sprog / Sprache:', style: TextStyle(color: Colors.white)), + SizedBox(height: 8), + NesDropdownMenu( + 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( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Language / Sprog / Sprache:', style: TextStyle(color: Colors.white)), - DropdownButton( - value: context.locale.languageCode, - dropdownColor: Colors.black, - style: TextStyle(color: Colors.white), - items: [ - 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)); - } + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('settings.show_vision_cones'.tr(), style: TextStyle(color: Colors.white)), + Text('settings.vision_cones_help'.tr(), style: TextStyle(color: Colors.white60, fontSize: 11)), + ], + ), + ), + 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 Row( @@ -224,9 +323,13 @@ class SettingsUI extends StatelessWidget with AppSettings { children: [ Text('ui.debug_mode'.tr(), style: TextStyle(color: Colors.white)), NesCheckBox( - value: false, // TODO: Connect to settings - onChange: (value) { - // TODO: Update settings + value: debugMode, + onChange: (value) async { + 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( type: NesButtonType.primary, onPressed: () { - game.overlays.remove(SettingsUI.overlayID); - game.overlays.add(MainMenuUI.overlayID); + widget.game.overlays.remove(SettingsUI.overlayID); + widget.game.overlays.add(MainMenuUI.overlayID); }, child: Text('menu.back_to_menu'.tr()), ),