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,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
}
}
}
}

View file

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

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

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;
@ -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
} }
} }

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))), ),
], 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()),
), ),