From 67aaa9589f9455149aaa96ac4e04270b3a8d465a Mon Sep 17 00:00:00 2001 From: zeyus Date: Sun, 27 Jul 2025 17:41:51 +0200 Subject: [PATCH] updated level...player can now "complete" (no ui) --- lib/attributes/resetable.dart | 8 + lib/game/components/base.dart | 74 +++++ lib/game/components/house_components.dart | 149 ++++++++++ lib/game/components/level_components.dart | 77 +++++ lib/game/components/neighborhood.dart | 97 ++++-- lib/game/components/player.dart | 181 +++++++---- lib/game/components/poop_bag.dart | 74 ++--- lib/game/components/road_components.dart | 239 +++++++++++++++ lib/game/components/security_components.dart | 297 +++++++++++++++++++ lib/game/components/target_house.dart | 21 +- lib/game/components/vision_cone.dart | 110 ++++--- lib/game/levels/operation_shitstorm.dart | 226 ++++++++++++++ lib/game/levels/shit_level.dart | 124 ++++++++ lib/game/shitman_game.dart | 49 +-- lib/game/shitman_world.dart | 105 +++++++ lib/services/log_service.dart | 258 ++++++++++++++++ lib/ui/in_game_ui.dart | 6 +- pubspec.lock | 10 +- pubspec.yaml | 4 +- web/manifest.json | 4 +- 20 files changed, 1914 insertions(+), 199 deletions(-) create mode 100644 lib/attributes/resetable.dart create mode 100644 lib/game/components/base.dart create mode 100644 lib/game/components/house_components.dart create mode 100644 lib/game/components/level_components.dart create mode 100644 lib/game/components/road_components.dart create mode 100644 lib/game/components/security_components.dart create mode 100644 lib/game/levels/operation_shitstorm.dart create mode 100644 lib/game/levels/shit_level.dart create mode 100644 lib/game/shitman_world.dart create mode 100644 lib/services/log_service.dart diff --git a/lib/attributes/resetable.dart b/lib/attributes/resetable.dart new file mode 100644 index 0000000..3133ea7 --- /dev/null +++ b/lib/attributes/resetable.dart @@ -0,0 +1,8 @@ +import "dart:async"; + +import "package:meta/meta.dart"; + +abstract mixin class Resetable { + @mustBeOverridden + FutureOr reset(); +} diff --git a/lib/game/components/base.dart b/lib/game/components/base.dart new file mode 100644 index 0000000..e09f81d --- /dev/null +++ b/lib/game/components/base.dart @@ -0,0 +1,74 @@ +import 'package:flame/components.dart'; +import 'package:flutter/widgets.dart'; +import 'package:shitman/attributes/resetable.dart'; +import 'package:shitman/game/shitman_game.dart'; +import 'package:shitman/services/log_service.dart'; + +/// Base class for all components in the Shitman game. +/// This class can be extended to create specific game components. +abstract class ShitComponent extends PositionComponent + with Resetable, HasGameReference, AppLogging { + bool get isStationary => false; + + ShitComponent({ + super.position, + super.size, + super.scale, + super.angle, + super.nativeAngle = 0, + super.anchor, + super.children, + super.priority, + super.key, + }); + + @override + Future onLoad() async { + await super.onLoad(); + // Additional initialization logic can go here + } +} + +abstract class DecorativeShit extends ShitComponent {} + +abstract class InteractiveShit extends ShitComponent {} + +mixin Stationary on ShitComponent { + /// Whether the item is stationary + @override + final bool isStationary = true; +} + +mixin Ambulatory on ShitComponent { + /// Whether the item is stationary + @override + final bool isStationary = false; + + /// Method to handle ambulatory behavior + void handleAmbulatory() { + // Logic for ambulatory items, e.g., moving around + } +} + +mixin Collectible on InteractiveShit { + bool _isCollected = false; + bool _isCollectible = true; + + /// Whether the item is collected + bool get isCollected => _isCollected; + + /// Whether the item can be collected + bool get isCollectible => _isCollectible; + + /// Set the item as collectible + void setCollectible(bool value) { + _isCollectible = value; + } + + /// Method to collect the item + @mustCallSuper + void collect() { + _isCollected = true; + // Additional logic for collecting the item + } +} diff --git a/lib/game/components/house_components.dart b/lib/game/components/house_components.dart new file mode 100644 index 0000000..5b84417 --- /dev/null +++ b/lib/game/components/house_components.dart @@ -0,0 +1,149 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:shitman/game/components/level_components.dart'; +import 'package:shitman/attributes/resetable.dart'; + +/// Base house component +class HouseComponent extends StructureComponent { + static const double houseSize = 80.0; + + HouseType houseType; + Vector2? doorPosition; + Vector2? yardCenter; + + List securitySystems = []; + + HouseComponent({ + required super.gridPosition, + required this.houseType, + }) { + size = Vector2.all(houseSize); + } + + @override + Future onLoad() async { + await super.onLoad(); + + // Calculate door and yard positions + doorPosition = position + Vector2(size.x / 2, size.y); + yardCenter = position + size / 2; + + appLog.fine('House loaded at $gridPosition (type: $houseType)'); + } + + Color _getHouseColor() { + switch (houseType) { + case HouseType.suburban: + return const Color(0xFF8B4513); // Brown + case HouseType.modern: + return const Color(0xFF4682B4); // Blue + case HouseType.cottage: + return const Color(0xFF228B22); // Green + case HouseType.apartment: + return const Color(0xFF696969); // Gray + } + } + + @override + void render(Canvas canvas) { + // Draw house with color based on type + final housePaint = Paint()..color = _getHouseColor(); + canvas.drawRect(size.toRect(), housePaint); + + // Draw door + 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 = 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, + ); + } + + /// Add a security system to this house + void addSecuritySystem(Component security) { + securitySystems.add(security); + add(security); + } + + /// Remove all security systems + void clearSecuritySystems() { + for (final security in securitySystems) { + remove(security); + } + securitySystems.clear(); + } + + /// Check if player is detected by any security system + bool detectsPlayer(Vector2 playerPosition, double playerStealthLevel) { + // Basic detection based on distance to house center + if (yardCenter == null) return false; + final distance = (playerPosition - yardCenter!).length; + final detectionRadius = 40.0 * (1.0 - playerStealthLevel); + return distance < detectionRadius; + } + + @override + Future reset() async { + // Reset all security systems + for (final security in securitySystems) { + if (security is Resetable) { + await (security as Resetable).reset(); + } + } + appLog.fine('House reset at $gridPosition'); + } +} + +/// Target house variant with special highlighting +class TargetHouseComponent extends HouseComponent { + bool isActiveTarget = false; + + TargetHouseComponent({ + required super.gridPosition, + required super.houseType, + }); + + void setAsTarget(bool isTarget) { + isActiveTarget = isTarget; + } + + @override + void render(Canvas canvas) { + super.render(canvas); + + // Draw target indicator if this is the active target + if (isActiveTarget) { + 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, + targetPaint, + ); + } + } + + @override + Future reset() async { + await super.reset(); + isActiveTarget = false; + } +} + +enum HouseType { suburban, modern, cottage, apartment } \ No newline at end of file diff --git a/lib/game/components/level_components.dart b/lib/game/components/level_components.dart new file mode 100644 index 0000000..5daa2cb --- /dev/null +++ b/lib/game/components/level_components.dart @@ -0,0 +1,77 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:shitman/game/components/base.dart'; + +/// Base class for all level building block components +abstract class LevelComponent extends DecorativeShit with Stationary { + Vector2 gridPosition; + + LevelComponent({required this.gridPosition}) { + size = Vector2.all(80.0); + } + + /// Convert grid position to world position + Vector2 getWorldPosition(double cellSize) { + return Vector2(gridPosition.x * cellSize, gridPosition.y * cellSize); + } +} + +/// Base class for road components +abstract class RoadComponent extends LevelComponent { + static const double roadWidth = 60.0; + + RoadComponent({required super.gridPosition}); + + @override + void render(Canvas canvas) { + final roadPaint = Paint()..color = const Color(0xFF333333); + canvas.drawRect(size.toRect(), roadPaint); + + // Draw road markings + final markingPaint = Paint() + ..color = const Color(0xFFFFFFFF) + ..strokeWidth = 2.0; + + renderRoadMarkings(canvas, markingPaint); + } + + /// Override in subclasses to render specific road markings + void renderRoadMarkings(Canvas canvas, Paint paint); + + @override + Future reset() async { + // Default implementation for road components + } +} + +/// Base class for structure components (houses, etc.) +abstract class StructureComponent extends LevelComponent { + StructureComponent({required super.gridPosition}); + + @override + Future reset() async { + // Default implementation for structure components + } +} + +/// Base class for security components +abstract class SecurityComponent extends InteractiveShit { + double detectionRange; + bool isActive; + + SecurityComponent({ + required Vector2 position, + required this.detectionRange, + this.isActive = true, + }) { + this.position = position; + } + + /// Check if player is detected by this security component + bool detectsPlayer(Vector2 playerPosition, double playerStealthLevel); + + /// Get the detection radius considering stealth + double getEffectiveDetectionRange(double playerStealthLevel) { + return detectionRange * (1.0 - playerStealthLevel); + } +} \ No newline at end of file diff --git a/lib/game/components/neighborhood.dart b/lib/game/components/neighborhood.dart index 41fd66a..235bc2b 100644 --- a/lib/game/components/neighborhood.dart +++ b/lib/game/components/neighborhood.dart @@ -1,10 +1,11 @@ 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 'package:shitman/game/components/base.dart'; +import 'package:shitman/settings/app_settings.dart'; import 'dart:math'; -class Neighborhood extends Component { +class Neighborhood extends DecorativeShit with Stationary, AppSettings { static const double streetWidth = 60.0; static const double houseSize = 80.0; static const double yardSize = 40.0; @@ -15,7 +16,9 @@ class Neighborhood extends Component { @override Future onLoad() async { await super.onLoad(); + await initSettings(); generateNeighborhood(); + appLog.fine('Neighborhood loaded with ${houses.length} houses'); } void generateNeighborhood() { @@ -74,8 +77,6 @@ class Neighborhood extends Component { @override void render(Canvas canvas) { - super.render(canvas); - // Draw streets final streetPaint = Paint()..color = const Color(0xFF333333); @@ -105,9 +106,22 @@ class Neighborhood extends Component { ); } } + + @override + Future reset() async { + appLog.fine('Resetting neighborhood'); + + // Reset all houses + for (final house in houses) { + await house.reset(); + } + + // Regenerate neighborhood if needed + generateNeighborhood(); + } } -class House extends RectangleComponent with HasGameReference { +class House extends DecorativeShit with Stationary, AppSettings { bool isTarget; int houseType; bool hasLights = false; @@ -122,14 +136,15 @@ class House extends RectangleComponent with HasGameReference { required Vector2 position, required this.isTarget, required this.houseType, - }) : super(position: position, size: Vector2.all(Neighborhood.houseSize)); + }) { + this.position = position; + size = Vector2.all(Neighborhood.houseSize); + } @override Future onLoad() async { await super.onLoad(); - - // Set house color based on type - paint = Paint()..color = _getHouseColor(); + await initSettings(); // Calculate door and yard positions doorPosition = position + Vector2(size.x / 2, size.y); @@ -145,6 +160,8 @@ class House extends RectangleComponent with HasGameReference { if (hasSecurityCamera) { _createVisionCone(); } + + appLog.fine('House loaded at $position (type: $houseType, target: $isTarget)'); } void _createVisionCone() { @@ -192,7 +209,9 @@ class House extends RectangleComponent with HasGameReference { @override void render(Canvas canvas) { - super.render(canvas); + // Draw house with color based on type and target status + final housePaint = Paint()..color = _getHouseColor(); + canvas.drawRect(size.toRect(), housePaint); // Draw door final doorPaint = Paint()..color = const Color(0xFF654321); @@ -202,10 +221,8 @@ class House extends RectangleComponent with HasGameReference { ); // 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( @@ -233,12 +250,11 @@ class House extends RectangleComponent with HasGameReference { // 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; + if (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(), @@ -251,11 +267,10 @@ class House extends RectangleComponent with HasGameReference { // 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, @@ -297,13 +312,39 @@ class House extends RectangleComponent with HasGameReference { // Update vision cone visibility based on settings if (visionCone != null) { try { - final showVisionCones = game.appSettings.getBool( - 'game.show_vision_cones', - ); + final showVisionCones = appSettings.getBool('game.show_vision_cones'); visionCone!.updateOpacity(showVisionCones ? 0.3 : 0.0); } catch (e) { visionCone!.updateOpacity(0.0); // Hide if settings not ready } } } + + @override + Future reset() async { + isTarget = false; + hasLights = false; + hasSecurityCamera = false; + hasWatchDog = false; + + // Reset vision cone + if (visionCone != null) { + await visionCone!.reset(); + removeAll(children.whereType()); + visionCone = null; + } + + // Regenerate security features + final random = Random(); + hasLights = random.nextBool(); + hasSecurityCamera = random.nextDouble() < 0.3; + hasWatchDog = random.nextDouble() < 0.2; + + // Recreate vision cone if needed + if (hasSecurityCamera) { + _createVisionCone(); + } + + appLog.fine('House reset at $position'); + } } diff --git a/lib/game/components/player.dart b/lib/game/components/player.dart index 7f00404..c7bc2de 100644 --- a/lib/game/components/player.dart +++ b/lib/game/components/player.dart @@ -3,33 +3,40 @@ import 'package:flutter/services.dart'; import 'package:flutter/material.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/house_components.dart'; import 'package:shitman/game/components/vision_cone.dart'; +import 'package:shitman/game/components/base.dart'; +import 'package:shitman/game/levels/operation_shitstorm.dart'; +import 'package:shitman/settings/app_settings.dart'; import 'dart:math'; -class Player extends RectangleComponent with HasGameReference { +class Player extends InteractiveShit with Ambulatory, AppSettings { static const double speed = 100.0; static const double playerSize = 32.0; Vector2 velocity = Vector2.zero(); bool hasPoopBag = true; bool isHidden = false; + bool isCaught = false; double stealthLevel = 0.0; // 0.0 = fully visible, 1.0 = completely hidden PoopBag? placedPoopBag; VisionCone? playerVisionCone; double lastMovementDirection = 0.0; + + // Mission state + bool isEscaping = false; + double escapeTimeLimit = 30.0; // 30 seconds to escape + double escapeTimeRemaining = 0.0; @override Future onLoad() async { await super.onLoad(); + await initSettings(); // Create a simple colored rectangle as player size = Vector2.all(playerSize); 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 @@ -40,9 +47,17 @@ class Player extends RectangleComponent with HasGameReference { opacity: 0.0, // Start hidden ); add(playerVisionCone!); + + appLog.fine('Player loaded at position $position'); } void handleInput(Set keysPressed) { + // Don't handle input if caught + if (isCaught) { + velocity = Vector2.zero(); + return; + } + velocity = Vector2.zero(); // Movement controls @@ -65,6 +80,9 @@ class Player extends RectangleComponent with HasGameReference { } void handleAction(LogicalKeyboardKey key) { + // Don't handle actions if caught + if (isCaught) return; + // Action controls if (key == LogicalKeyboardKey.space) { placePoop(); @@ -89,7 +107,7 @@ class Player extends RectangleComponent with HasGameReference { void placePoop() { if (!hasPoopBag) return; - debugPrint('Placing poop bag at $position'); + appLog.fine('Placing poop bag at $position'); // Create and place the poop bag placedPoopBag = PoopBag(); @@ -104,48 +122,62 @@ class Player extends RectangleComponent with HasGameReference { } void ringDoorbell() { - debugPrint('Attempting to ring doorbell'); + appLog.fine('Attempting to ring doorbell'); - // Check if near target house door - if (game.targetHouse.isPlayerNearTarget(position)) { + // Check if near any target house door + final currentLevel = game.world.children.whereType().firstOrNull; + if (currentLevel != null && currentLevel.isPlayerNearTarget(position)) { if (placedPoopBag != null) { // Light the poop bag on fire placedPoopBag!.lightOnFire(); - debugPrint('Ding dong! Poop bag is lit! RUN!'); + appLog.info('Ding dong! Poop bag is lit! RUN!'); // Start escape timer - player has limited time to escape startEscapeSequence(); } else { - debugPrint('Need to place poop bag first!'); + appLog.fine('Need to place poop bag first!'); } } else { - debugPrint('Not near target house door'); + appLog.fine('Not near target house door'); } } void startEscapeSequence() { - // TODO: Implement escape mechanics - // For now, automatically complete mission after a delay - Future.delayed(Duration(seconds: 3), () { - if (game.gameState == GameState.playing) { - game.completeCurrentMission(); - } - }); + appLog.info('Escape sequence started! Get to the edge of the map!'); + isEscaping = true; + escapeTimeRemaining = escapeTimeLimit; + + // TODO: Show escape timer UI + // TODO: Highlight escape routes on the map + } + + /// Check if player is at an escape point (edge of the map) + bool isAtEscapePoint() { + const double escapeZoneThreshold = 50.0; + + // Check if near any edge of the level + return position.x < escapeZoneThreshold || + position.y < escapeZoneThreshold || + position.x > (5 * 100) - escapeZoneThreshold || // 5x5 grid * 100 cell size + position.y > (5 * 100) - escapeZoneThreshold; } void checkMissionProgress() { // Check if near target house and has placed poop bag - final targetPos = game.targetHouse.getTargetPosition(); + final currentLevel = game.world.children.whereType().firstOrNull; + final targetPos = currentLevel?.getTargetPosition(); if (targetPos != null && placedPoopBag != null) { final distance = (position - targetPos).length; if (distance < 80) { - debugPrint('Near target house with poop bag placed!'); + appLog.fine('Near target house with poop bag placed!'); } } } void getDetected() { - debugPrint('Player detected! Mission failed!'); + appLog.warning('Player detected! Mission failed!'); + isCaught = true; + velocity = Vector2.zero(); // Stop movement immediately game.failMission(); } @@ -169,8 +201,30 @@ class Player extends RectangleComponent with HasGameReference { // Update stealth level based on environment updateStealthLevel(dt); - // Check for detection by houses - checkForDetection(); + // Handle escape sequence + if (isEscaping) { + escapeTimeRemaining -= dt; + + // Check if time ran out + if (escapeTimeRemaining <= 0) { + appLog.warning('Time ran out! Mission failed!'); + getDetected(); + return; + } + + // Check if player reached escape point (edge of map) + if (isAtEscapePoint()) { + appLog.info('Successfully escaped! Mission complete!'); + isEscaping = false; + game.completeCurrentMission(); + return; + } + } + + // Check for detection by houses (only if not already caught) + if (!isCaught) { + checkForDetection(); + } // Update player vision cone if (playerVisionCone != null) { @@ -178,9 +232,7 @@ class Player extends RectangleComponent with HasGameReference { // Update vision cone visibility based on settings try { - final showVisionCones = game.appSettings.getBool( - 'game.show_vision_cones', - ); + final showVisionCones = appSettings.getBool('game.show_vision_cones'); playerVisionCone!.updateOpacity(showVisionCones ? 0.2 : 0.0); } catch (e) { playerVisionCone!.updateOpacity(0.0); // Hide if settings not ready @@ -189,12 +241,13 @@ class Player extends RectangleComponent with HasGameReference { } void checkForDetection() { - final neighborhood = - game.world.children.whereType().firstOrNull; - if (neighborhood == null) return; - - for (final house in neighborhood.houses) { - if (house.canDetectPlayer(position, stealthLevel)) { + // Check for detection from houses in any active level + final houses = game.world.children + .expand((component) => component.children) + .whereType(); + + for (final house in houses) { + if (house.detectsPlayer(position, stealthLevel)) { getDetected(); break; } @@ -203,30 +256,50 @@ 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).withValues(alpha: 0.7) - : // Green when hidden - const Color( - 0xFF0000FF, - ).withValues(alpha: 0.9); // Blue when visible + // Draw player as rectangle with color based on stealth level + final playerPaint = 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); + canvas.drawRect(size.toRect(), playerPaint); // 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); + try { + final debugMode = appSettings.getBool('game.debug_mode'); + if (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); + } + } catch (e) { + // Continue without debug rendering if settings not available } } + + @override + Future reset() async { + hasPoopBag = true; + isHidden = false; + isCaught = false; + isEscaping = false; + escapeTimeRemaining = 0.0; + stealthLevel = 0.0; + velocity = Vector2.zero(); + lastMovementDirection = 0.0; + placedPoopBag = null; + position = Vector2(200, 200); // Reset to starting position + + // Reset vision cone + if (playerVisionCone != null) { + await playerVisionCone!.reset(); + playerVisionCone!.updatePosition(size / 2, lastMovementDirection); + } + + appLog.fine('Player reset to initial state'); + } } diff --git a/lib/game/components/poop_bag.dart b/lib/game/components/poop_bag.dart index 22d9e3f..1260b57 100644 --- a/lib/game/components/poop_bag.dart +++ b/lib/game/components/poop_bag.dart @@ -1,18 +1,23 @@ import 'package:flame/components.dart'; import 'package:flame/effects.dart'; +import 'package:flame/particles.dart'; import 'package:flutter/material.dart'; import 'dart:math'; +import 'package:shitman/game/components/base.dart'; + enum PoopBagState { placed, lit, burning, extinguished } -class PoopBag extends CircleComponent { +class PoopBag extends ShitComponent { PoopBagState state = PoopBagState.placed; double burnTimer = 0.0; static const double burnDuration = 3.0; // seconds to burn static const double bagSize = 16.0; + double radius = bagSize / 2; + late Paint paint; late Vector2 smokeOffset; - List smokeParticles = []; + List smokeParticles = []; @override Future onLoad() async { @@ -37,7 +42,7 @@ class PoopBag extends CircleComponent { ), ); - debugPrint('Poop bag is now on fire!'); + appLog.finest('Poop bag is now on fire!'); } } @@ -76,7 +81,15 @@ class PoopBag extends CircleComponent { smokeOffset + Vector2(random.nextDouble() * 10 - 5, random.nextDouble() * 5), ); - smokeParticles.add(particle); + smokeParticles.add( + particle.accelerated( + acceleration: Vector2( + Random().nextDouble() * 20 - 10, + -Random().nextDouble() * 30 - 20, + ), + position: particle.position, + ), + ); } void extinguish() { @@ -86,13 +99,14 @@ class PoopBag extends CircleComponent { // Change to burnt color paint = Paint()..color = const Color(0xFF2F2F2F); - debugPrint('Poop bag has burned out'); + appLog.finest('Poop bag has burned out'); } @override void render(Canvas canvas) { super.render(canvas); - + // Draw the poop bag + canvas.drawCircle(Offset(0, 0), radius, paint); // Draw flame effect when lit if (state == PoopBagState.lit) { final flamePaint = @@ -121,42 +135,30 @@ class PoopBag extends CircleComponent { bool isNearPosition(Vector2 targetPosition, {double threshold = 30.0}) { return (position - targetPosition).length < threshold; } + + @override + Future reset() async { + state = PoopBagState.placed; + burnTimer = 0.0; + smokeParticles.clear(); + paint = Paint()..color = const Color(0xFF8B4513); + removeAll(children.whereType()); + appLog.finest('Poop bag reset to placed state'); + } } -class SmokeParticle { +class SmokeParticle extends Particle { Vector2 position; - Vector2 velocity; - double life; - double maxLife; - bool shouldRemove = false; - SmokeParticle({required this.position}) - : velocity = Vector2( - Random().nextDouble() * 20 - 10, - -Random().nextDouble() * 30 - 20, - ), - life = 2.0, - maxLife = 2.0; - - void update(double dt) { - position += velocity * dt; - velocity *= 0.98; // Slight air resistance - life -= dt; - - if (life <= 0) { - shouldRemove = true; - } - } + SmokeParticle({required this.position}) : super(lifespan: 2.0); + @override void render(Canvas canvas) { - final alpha = (life / maxLife).clamp(0.0, 1.0); - final smokePaint = - Paint()..color = Color(0xFF666666).withValues(alpha: alpha * 0.3); + final paint = + Paint() + ..color = const Color(0xFF808080).withValues(alpha: 0.5) + ..style = PaintingStyle.fill; - canvas.drawCircle( - Offset(position.x, position.y), - 6.0 * (1.0 - life / maxLife), - smokePaint, - ); + canvas.drawCircle(Offset(position.x, position.y), 3.0, paint); } } diff --git a/lib/game/components/road_components.dart b/lib/game/components/road_components.dart new file mode 100644 index 0000000..35fd7fc --- /dev/null +++ b/lib/game/components/road_components.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:shitman/game/components/level_components.dart'; +import 'dart:math'; + +/// Horizontal road segment +class HorizontalRoad extends RoadComponent { + HorizontalRoad({required super.gridPosition}); + + @override + void renderRoadMarkings(Canvas canvas, Paint paint) { + // Draw center line + final centerY = size.y / 2; + for (double x = 5; x < size.x; x += 15) { + canvas.drawLine( + Offset(x, centerY - 1), + Offset(x + 8, centerY - 1), + paint, + ); + } + } + + @override + Future reset() async { + // Nothing to reset for road components + } +} + +/// Vertical road segment +class VerticalRoad extends RoadComponent { + VerticalRoad({required super.gridPosition}); + + @override + void renderRoadMarkings(Canvas canvas, Paint paint) { + // Draw center line + final centerX = size.x / 2; + for (double y = 5; y < size.y; y += 15) { + canvas.drawLine( + Offset(centerX - 1, y), + Offset(centerX - 1, y + 8), + paint, + ); + } + } + + @override + Future reset() async { + // Nothing to reset for road components + } +} + +/// Road intersection (crossroads) +class IntersectionRoad extends RoadComponent { + IntersectionRoad({required super.gridPosition}); + + @override + void renderRoadMarkings(Canvas canvas, Paint paint) { + // Draw intersection markings - just corner lines + const offset = 10.0; + + // Top-left corner + canvas.drawLine( + const Offset(offset, offset), + Offset(offset + 15, offset), + paint, + ); + canvas.drawLine( + const Offset(offset, offset), + Offset(offset, offset + 15), + paint, + ); + + // Top-right corner + canvas.drawLine( + Offset(size.x - offset - 15, offset), + Offset(size.x - offset, offset), + paint, + ); + canvas.drawLine( + Offset(size.x - offset, offset), + Offset(size.x - offset, offset + 15), + paint, + ); + + // Bottom-left corner + canvas.drawLine( + Offset(offset, size.y - offset - 15), + Offset(offset, size.y - offset), + paint, + ); + canvas.drawLine( + Offset(offset, size.y - offset), + Offset(offset + 15, size.y - offset), + paint, + ); + + // Bottom-right corner + canvas.drawLine( + Offset(size.x - offset, size.y - offset - 15), + Offset(size.x - offset, size.y - offset), + paint, + ); + canvas.drawLine( + Offset(size.x - offset - 15, size.y - offset), + Offset(size.x - offset, size.y - offset), + paint, + ); + } + + @override + Future reset() async { + // Nothing to reset for road components + } +} + +/// Corner road (L-shaped) +class CornerRoad extends RoadComponent { + final CornerDirection direction; + + CornerRoad({required super.gridPosition, required this.direction}); + + @override + void renderRoadMarkings(Canvas canvas, Paint paint) { + const curveRadius = 10.0; + + switch (direction) { + case CornerDirection.topLeft: + // Draw curve from top to left + canvas.drawArc( + Rect.fromLTWH(0, 0, curveRadius * 2, curveRadius * 2), + 0, + pi / 2, + false, + paint..style = PaintingStyle.stroke, + ); + break; + case CornerDirection.topRight: + // Draw curve from top to right + canvas.drawArc( + Rect.fromLTWH( + size.x - curveRadius * 2, + 0, + curveRadius * 2, + curveRadius * 2, + ), + pi / 2, + pi / 2, + false, + paint..style = PaintingStyle.stroke, + ); + break; + case CornerDirection.bottomLeft: + // Draw curve from bottom to left + canvas.drawArc( + Rect.fromLTWH( + 0, + size.y - curveRadius * 2, + curveRadius * 2, + curveRadius * 2, + ), + 3 * pi / 2, + pi / 2, + false, + paint..style = PaintingStyle.stroke, + ); + break; + case CornerDirection.bottomRight: + // Draw curve from bottom to right + canvas.drawArc( + Rect.fromLTWH( + size.x - curveRadius * 2, + size.y - curveRadius * 2, + curveRadius * 2, + curveRadius * 2, + ), + pi, + pi / 2, + false, + paint..style = PaintingStyle.stroke, + ); + break; + } + } + + @override + Future reset() async { + // Nothing to reset for road components + } +} + +/// Dead end road +class DeadEndRoad extends RoadComponent { + final DeadEndDirection direction; + + DeadEndRoad({required super.gridPosition, required this.direction}); + + @override + void renderRoadMarkings(Canvas canvas, Paint paint) { + // Draw a line across the dead end + switch (direction) { + case DeadEndDirection.north: + canvas.drawLine( + const Offset(10, 10), + Offset(size.x - 10, 10), + paint..strokeWidth = 4.0, + ); + break; + case DeadEndDirection.south: + canvas.drawLine( + Offset(10, size.y - 10), + Offset(size.x - 10, size.y - 10), + paint..strokeWidth = 4.0, + ); + break; + case DeadEndDirection.east: + canvas.drawLine( + Offset(size.x - 10, 10), + Offset(size.x - 10, size.y - 10), + paint..strokeWidth = 4.0, + ); + break; + case DeadEndDirection.west: + canvas.drawLine( + const Offset(10, 10), + Offset(10, size.y - 10), + paint..strokeWidth = 4.0, + ); + break; + } + } + + @override + Future reset() async { + // Nothing to reset for road components + } +} + +enum CornerDirection { topLeft, topRight, bottomLeft, bottomRight } + +enum DeadEndDirection { north, south, east, west } diff --git a/lib/game/components/security_components.dart b/lib/game/components/security_components.dart new file mode 100644 index 0000000..efe6766 --- /dev/null +++ b/lib/game/components/security_components.dart @@ -0,0 +1,297 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:shitman/game/components/level_components.dart'; +import 'package:shitman/game/components/vision_cone.dart'; +import 'package:shitman/settings/app_settings.dart'; +import 'dart:math'; + +/// PIR sensor triggered light +class PIRSensorComponent extends SecurityComponent with AppSettings { + bool isTriggered = false; + bool lightsOn = false; + bool _showDetectionRadius = true; + + PIRSensorComponent({ + required super.position, + super.detectionRange = 40.0, + }) { + size = Vector2(8, 8); + } + + @override + Future onLoad() async { + await super.onLoad(); + await initSettings(); + + // Initialize detection radius visibility from settings + try { + _showDetectionRadius = appSettings.getBool('game.show_detection_radius'); + } catch (e) { + _showDetectionRadius = true; // Default to visible + } + } + + @override + bool detectsPlayer(Vector2 playerPosition, double playerStealthLevel) { + final distance = (playerPosition - position).length; + final effectiveRange = getEffectiveDetectionRange(playerStealthLevel); + + bool detected = distance < effectiveRange; + + if (detected && !isTriggered) { + isTriggered = true; + lightsOn = true; + appLog.fine('PIR sensor triggered at $position'); + } else if (!detected && isTriggered) { + // Cool down period before turning off + Future.delayed(const Duration(seconds: 5), () { + isTriggered = false; + lightsOn = false; + }); + } + + return detected; + } + + @override + void render(Canvas canvas) { + // Draw detection radius if enabled + if (_showDetectionRadius) { + 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), + detectionRange, + radiusPaint, + ); + } + + // Draw PIR sensor as small circle + final sensorPaint = Paint() + ..color = isTriggered ? const Color(0xFFFF0000) : const Color(0xFF000000); + canvas.drawCircle(Offset(size.x / 2, size.y / 2), 4, sensorPaint); + + // Draw light effect if triggered + if (lightsOn) { + final lightPaint = Paint() + ..color = const Color(0xFFFFFF00).withValues(alpha: 0.3); + canvas.drawCircle( + Offset(size.x / 2, size.y / 2), + detectionRange, + lightPaint, + ); + } + } + + /// Update detection radius visibility (call when settings change) + void updateDetectionRadiusVisibility(bool visible) { + _showDetectionRadius = visible; + } + + /// Refresh visibility from current settings + void refreshVisibilityFromSettings() { + try { + final newVisibility = appSettings.getBool('game.show_detection_radius'); + updateDetectionRadiusVisibility(newVisibility); + } catch (e) { + // Settings not ready, keep current state + } + } + + @override + Future reset() async { + isTriggered = false; + lightsOn = false; + } +} + +/// Security camera with vision cone +class SecurityCameraComponent extends SecurityComponent with AppSettings { + late VisionCone visionCone; + double direction; + bool _currentVisionConeVisibility = true; + + SecurityCameraComponent({ + required super.position, + required this.direction, + super.detectionRange = 120.0, + }) { + size = Vector2(8, 8); + } + + @override + Future onLoad() async { + await super.onLoad(); + await initSettings(); + + // Create vision cone with initial visibility based on settings + try { + _currentVisionConeVisibility = appSettings.getBool('game.show_vision_cones'); + } catch (e) { + _currentVisionConeVisibility = true; // Default to visible + } + + visionCone = VisionCone( + origin: Vector2.zero(), + direction: direction, + range: detectionRange, + fov: pi / 2, // 90 degrees + color: Colors.red, + opacity: _currentVisionConeVisibility ? 0.3 : 0.0, + ); + add(visionCone); + } + + @override + bool detectsPlayer(Vector2 playerPosition, double playerStealthLevel) { + return visionCone.canSee(playerPosition) && + visionCone.hasLineOfSight(playerPosition, []); + } + + @override + void render(Canvas canvas) { + // Draw camera as black circle + final cameraPaint = Paint()..color = const Color(0xFF000000); + canvas.drawCircle(Offset(size.x / 2, size.y / 2), 4, cameraPaint); + } + + /// Call this method when settings change to update vision cone visibility + void updateVisionConeVisibility(bool visible) { + if (_currentVisionConeVisibility != visible) { + _currentVisionConeVisibility = visible; + visionCone.updateOpacity(visible ? 0.3 : 0.0); + } + } + + /// Refresh visibility from current settings (call when settings might have changed) + void refreshVisibilityFromSettings() { + try { + final newVisibility = appSettings.getBool('game.show_vision_cones'); + updateVisionConeVisibility(newVisibility); + } catch (e) { + // Settings not ready, keep current state + } + } + + @override + Future reset() async { + await visionCone.reset(); + } +} + +/// Guard dog with patrol area +class GuardDogComponent extends SecurityComponent with AppSettings { + Vector2 patrolCenter; + double patrolRadius; + double currentAngle = 0; + bool isPatrolling = true; + bool _showDetectionRadius = true; + + GuardDogComponent({ + required super.position, + required this.patrolCenter, + this.patrolRadius = 30.0, + super.detectionRange = 50.0, + }) { + size = Vector2(12, 12); + } + + @override + Future onLoad() async { + await super.onLoad(); + await initSettings(); + + // Initialize detection radius visibility from settings + try { + _showDetectionRadius = appSettings.getBool('game.show_detection_radius'); + } catch (e) { + _showDetectionRadius = true; // Default to visible + } + } + + @override + bool detectsPlayer(Vector2 playerPosition, double playerStealthLevel) { + final distance = (playerPosition - position).length; + final effectiveRange = getEffectiveDetectionRange(playerStealthLevel); + + return distance < effectiveRange; + } + + @override + void update(double dt) { + super.update(dt); + + if (isPatrolling) { + // Simple circular patrol + currentAngle += dt * 0.5; // Rotation speed + final offset = Vector2( + cos(currentAngle) * patrolRadius, + sin(currentAngle) * patrolRadius, + ); + position = patrolCenter + offset; + } + } + + @override + void render(Canvas canvas) { + // Draw detection radius if enabled + if (_showDetectionRadius) { + 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), + detectionRange, + radiusPaint, + ); + } + + // Draw dog house + final dogHousePaint = Paint()..color = const Color(0xFF8B4513); + canvas.drawRect( + Rect.fromLTWH(0, 0, size.x, size.y), + dogHousePaint, + ); + + // Draw dog (simple circle) + final dogPaint = Paint()..color = const Color(0xFF654321); + canvas.drawCircle( + Offset(size.x / 2, size.y / 2), + 4, + dogPaint, + ); + } + + void stopPatrol() { + isPatrolling = false; + } + + void startPatrol() { + isPatrolling = true; + } + + /// Update detection radius visibility (call when settings change) + void updateDetectionRadiusVisibility(bool visible) { + _showDetectionRadius = visible; + } + + /// Refresh visibility from current settings + void refreshVisibilityFromSettings() { + try { + final newVisibility = appSettings.getBool('game.show_detection_radius'); + updateDetectionRadiusVisibility(newVisibility); + } catch (e) { + // Settings not ready, keep current state + } + } + + @override + Future reset() async { + currentAngle = 0; + isPatrolling = true; + position = patrolCenter; + } +} \ No newline at end of file diff --git a/lib/game/components/target_house.dart b/lib/game/components/target_house.dart index 371d286..564e9f2 100644 --- a/lib/game/components/target_house.dart +++ b/lib/game/components/target_house.dart @@ -1,11 +1,11 @@ import 'package:flame/components.dart'; -import 'package:flutter/foundation.dart'; +import 'package:shitman/game/components/base.dart'; import 'package:shitman/game/components/neighborhood.dart'; -class TargetHouse extends Component { +class TargetHouse extends ShitComponent { House? currentTarget; bool missionActive = false; - + @override Future onLoad() async { await super.onLoad(); @@ -27,7 +27,7 @@ class TargetHouse extends Component { if (currentTarget != null) { currentTarget!.isTarget = true; missionActive = true; - debugPrint('New target selected at ${currentTarget!.position}'); + appLog.finest('New target selected at ${currentTarget!.position}'); } } @@ -41,7 +41,7 @@ class TargetHouse extends Component { bool isPlayerNearTarget(Vector2 playerPosition, {double threshold = 50.0}) { if (currentTarget?.doorPosition == null) return false; - + final distance = (playerPosition - currentTarget!.doorPosition!).length; return distance < threshold; } @@ -49,4 +49,13 @@ class TargetHouse extends Component { Vector2? getTargetPosition() { return currentTarget?.doorPosition; } -} \ No newline at end of file + + @override + Future reset() async { + // Reset target house state + currentTarget?.isTarget = false; + currentTarget = null; + missionActive = false; + appLog.finest('Target house reset'); + } +} diff --git a/lib/game/components/vision_cone.dart b/lib/game/components/vision_cone.dart index 750c832..7044643 100644 --- a/lib/game/components/vision_cone.dart +++ b/lib/game/components/vision_cone.dart @@ -2,17 +2,19 @@ import 'package:flame/components.dart'; import 'package:flutter/material.dart'; import 'dart:math'; -class VisionCone extends PositionComponent { +import 'package:shitman/game/components/base.dart'; + +class VisionCone extends ShitComponent { 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, @@ -25,35 +27,37 @@ class VisionCone extends PositionComponent { @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; - + + 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++) { @@ -61,30 +65,30 @@ class VisionCone extends PositionComponent { 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 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) { @@ -95,21 +99,22 @@ class VisionCone extends PositionComponent { } 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 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) { @@ -119,40 +124,51 @@ class VisionCone extends PositionComponent { } } } - + 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; + 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 + + @override + Future reset() async { + // Reset vision cone state + direction = 0.0; + opacity = 0.3; + visionVertices.clear(); + _calculateVisionCone(); + appLog.finest('Vision cone reset'); + } +} diff --git a/lib/game/levels/operation_shitstorm.dart b/lib/game/levels/operation_shitstorm.dart new file mode 100644 index 0000000..892d4be --- /dev/null +++ b/lib/game/levels/operation_shitstorm.dart @@ -0,0 +1,226 @@ +import 'package:flame/components.dart'; +import 'package:shitman/game/levels/shit_level.dart'; +import 'package:shitman/game/components/level_components.dart'; +import 'package:shitman/game/components/road_components.dart'; +import 'package:shitman/game/components/house_components.dart'; +import 'package:shitman/game/components/security_components.dart'; +import 'dart:math'; + +/// Level 1: Operation Shitstorm +/// A grid-based neighborhood with various house types and security systems +class OperationShitstorm extends ShitLevel { + static const double cellSize = 100.0; + static const int gridWidth = 5; + static const int gridHeight = 5; + + List> levelGrid = []; + List houses = []; + TargetHouseComponent? currentTarget; + + OperationShitstorm() : super(levelName: "Operation: Shitstorm", difficulty: 1); + + @override + Future initializeLevelComponents() async { + appLog.fine('Initializing Operation Shitstorm level layout'); + + // Initialize the grid + levelGrid = List.generate( + gridHeight, + (row) => List.generate(gridWidth, (col) => null), + ); + + // Create the level layout + await createLevelLayout(); + + // Add all components to the level + await addComponentsToLevel(); + + // Initialize core components (player, etc.) + await super.initializeLevelComponents(); + + // Select a random target house + selectRandomTarget(); + + appLog.fine('Operation Shitstorm level initialized'); + } + + Future createLevelLayout() async { + // Create a simple grid layout: + // H = House, R = Road (various types), I = Intersection + // Layout pattern: + // H R H R H + // R I R I R + // H R H R H + // R I R I R + // H R H R H + + for (int row = 0; row < gridHeight; row++) { + for (int col = 0; col < gridWidth; col++) { + final gridPos = Vector2(col.toDouble(), row.toDouble()); + + if (row % 2 == 0) { // Even rows: Houses and vertical roads + if (col % 2 == 0) { + // House position + levelGrid[row][col] = createRandomHouse(gridPos); + } else { + // Vertical road + levelGrid[row][col] = VerticalRoad(gridPosition: gridPos); + } + } else { // Odd rows: Horizontal roads and intersections + if (col % 2 == 0) { + // Horizontal road + levelGrid[row][col] = HorizontalRoad(gridPosition: gridPos); + } else { + // Intersection + levelGrid[row][col] = IntersectionRoad(gridPosition: gridPos); + } + } + } + } + } + + HouseComponent createRandomHouse(Vector2 gridPos) { + final random = Random(); + final houseTypes = HouseType.values; + final selectedType = houseTypes[random.nextInt(houseTypes.length)]; + + final house = HouseComponent( + gridPosition: gridPos, + houseType: selectedType, + ); + + // Randomly add security systems to some houses + if (random.nextDouble() < 0.4) { // 40% chance of security + addRandomSecurityToHouse(house, random); + } + + houses.add(house); + return house; + } + + void addRandomSecurityToHouse(HouseComponent house, Random random) { + final securityTypes = random.nextInt(3); // 0-2 different security types + + switch (securityTypes) { + case 0: + // PIR sensor + final pirSensor = PIRSensorComponent( + position: Vector2(house.size.x * 0.9, house.size.y * 0.1), + ); + house.addSecuritySystem(pirSensor); + break; + case 1: + // Security camera + final camera = SecurityCameraComponent( + position: Vector2(house.size.x * 0.9, house.size.y * 0.1), + direction: _getOptimalCameraDirection(house), + ); + house.addSecuritySystem(camera); + break; + case 2: + // Guard dog + final dog = GuardDogComponent( + position: Vector2(-20, house.size.y + 10), + patrolCenter: Vector2(-20, house.size.y + 10), + ); + house.addSecuritySystem(dog); + break; + } + } + + double _getOptimalCameraDirection(HouseComponent house) { + // Point camera towards center of level + final levelCenter = Vector2(gridWidth * cellSize / 2, gridHeight * cellSize / 2); + final houseCenter = house.getWorldPosition(cellSize) + house.size / 2; + final toCenter = levelCenter - houseCenter; + return atan2(toCenter.y, toCenter.x); + } + + Future addComponentsToLevel() async { + for (int row = 0; row < gridHeight; row++) { + for (int col = 0; col < gridWidth; col++) { + final component = levelGrid[row][col]; + if (component != null) { + // Set world position based on grid position + component.position = component.getWorldPosition(cellSize); + add(component); + } + } + } + } + + void selectRandomTarget() { + if (houses.isEmpty) return; + + // Clear previous target + currentTarget?.setAsTarget(false); + + // Convert a random house to a target house + final random = Random(); + final randomHouse = houses[random.nextInt(houses.length)]; + + // Remove the old house and create a new target house at the same position + remove(randomHouse); + houses.remove(randomHouse); + + currentTarget = TargetHouseComponent( + gridPosition: randomHouse.gridPosition, + houseType: randomHouse.houseType, + ); + currentTarget!.position = randomHouse.position; + currentTarget!.setAsTarget(true); + + // Copy security systems + for (final security in randomHouse.securitySystems) { + currentTarget!.addSecuritySystem(security); + } + + add(currentTarget!); + houses.add(currentTarget!); + + appLog.info('Target selected at grid position ${currentTarget!.gridPosition}'); + } + + @override + Future onLevelStart() async { + appLog.info('Starting Operation: Shitstorm'); + // Level-specific start logic can be added here + } + + @override + Future onLevelEnd() async { + appLog.info('Operation: Shitstorm completed'); + // Level-specific end logic can be added here + } + + @override + Future reset() async { + await super.reset(); + + // Clear level-specific data + levelGrid.clear(); + houses.clear(); + currentTarget = null; + + // Recreate the level + await createLevelLayout(); + await addComponentsToLevel(); + selectRandomTarget(); + + appLog.fine('Operation Shitstorm level reset'); + } + + /// Get the current target house position + Vector2? getTargetPosition() { + return currentTarget?.doorPosition; + } + + /// Check if player is near the target + bool isPlayerNearTarget(Vector2 playerPosition, {double threshold = 50.0}) { + final targetPos = getTargetPosition(); + if (targetPos == null) return false; + + final distance = (playerPosition - targetPos).length; + return distance < threshold; + } +} \ No newline at end of file diff --git a/lib/game/levels/shit_level.dart b/lib/game/levels/shit_level.dart new file mode 100644 index 0000000..f46e044 --- /dev/null +++ b/lib/game/levels/shit_level.dart @@ -0,0 +1,124 @@ +import 'package:flame/components.dart'; +import 'package:shitman/attributes/resetable.dart'; +import 'package:shitman/game/shitman_game.dart'; +import 'package:shitman/game/components/base.dart'; +import 'package:shitman/game/components/player.dart'; +import 'package:shitman/services/log_service.dart'; +import 'package:shitman/settings/app_settings.dart'; + +class ShitLevel extends PositionComponent + with HasGameReference, Resetable, AppLogging, AppSettings { + /// Base class for game levels. + /// This can be extended to create specific levels with unique layouts + + String levelName; + int difficulty; + bool _isActive = false; + + // Core game components + late Player player; + + ShitLevel({required this.levelName, this.difficulty = 1}); + + /// Whether the level is currently active + bool get isActive => _isActive; + + @override + Future onLoad() async { + await super.onLoad(); + await initSettings(); + + // Initialize level-specific components + await initializeLevelComponents(); + + appLog.fine('Level loaded: $levelName (difficulty: $difficulty)'); + } + + /// Initialize the core components for this level + Future initializeLevelComponents() async { + appLog.fine('Initializing level components for $levelName'); + + // Create player + player = Player(); + add(player); + + // Setup camera to follow player if game reference is available + try { + game.camera.follow(player); + } catch (e) { + appLog.warning('Unable to setup camera following: $e'); + } + + appLog.fine('Level components initialized'); + } + + /// Start the level gameplay + Future startLevel() async { + if (_isActive) return; + + _isActive = true; + appLog.info('Starting level: $levelName'); + + // Any level-specific start logic can be added here + await onLevelStart(); + } + + /// End the level gameplay + Future endLevel() async { + if (!_isActive) return; + + _isActive = false; + appLog.info('Ending level: $levelName'); + + // Any level-specific end logic can be added here + await onLevelEnd(); + } + + /// Override this method in subclasses for custom start logic + Future onLevelStart() async { + // Default implementation does nothing + } + + /// Override this method in subclasses for custom end logic + Future onLevelEnd() async { + // Default implementation does nothing + } + + /// Get all components in this level that implement Resetable + List getResetableComponents() { + return children.whereType().toList(); + } + + /// Get all ShitComponents in this level + List getAllShitComponents() { + return children.whereType().toList(); + } + + /// Reset all components in this level + Future resetAllComponents() async { + appLog.fine('Resetting all components in level: $levelName'); + + final resetableComponents = getResetableComponents(); + for (final component in resetableComponents) { + await component.reset(); + } + } + + @override + Future reset() async { + appLog.fine('Resetting level: $levelName'); + + _isActive = false; + + // Reset all child components first + await resetAllComponents(); + + // Override in subclasses for additional reset logic + await onLevelReset(); + } + + /// Override this method in subclasses for custom reset logic + Future onLevelReset() async { + // Default implementation does nothing + } +} diff --git a/lib/game/shitman_game.dart b/lib/game/shitman_game.dart index ddbf4d6..c3d743e 100644 --- a/lib/game/shitman_game.dart +++ b/lib/game/shitman_game.dart @@ -4,8 +4,8 @@ import 'package:flame/events.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:shitman/game/components/player.dart'; -import 'package:shitman/game/components/neighborhood.dart'; -import 'package:shitman/game/components/target_house.dart'; +import 'package:shitman/game/levels/operation_shitstorm.dart'; +import 'package:shitman/game/shitman_world.dart'; import 'package:shitman/settings/app_settings.dart'; /// Shitman Game @@ -18,8 +18,7 @@ enum GameState { mainMenu, playing, paused, gameOver, missionComplete } class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollisionDetection, AppSettings { late Player player; - late Neighborhood neighborhood; - late TargetHouse targetHouse; + late OperationShitstorm currentLevel; late CameraComponent gameCamera; GameState gameState = GameState.mainMenu; @@ -31,6 +30,7 @@ class ShitmanGame extends FlameGame Future onLoad() async { await super.onLoad(); await initSettings(); + world = ShitmanWorld(); // Setup camera gameCamera = CameraComponent.withFixedResolution( @@ -49,16 +49,16 @@ class ShitmanGame extends FlameGame } } - void startGame() { + Future startGame() async { gameState = GameState.playing; - initializeLevel(); + await initializeLevel(); } - void startInfiniteMode() { + Future startInfiniteMode() async { infiniteMode = true; overlays.remove('MainMenu'); overlays.add('InGameUI'); - startGame(); + await startGame(); } void stopGame() { @@ -75,21 +75,21 @@ class ShitmanGame extends FlameGame gameState = GameState.playing; } - void initializeLevel() { + Future initializeLevel() async { // Clear previous level world.removeAll(world.children); - // Create neighborhood - neighborhood = Neighborhood(); - world.add(neighborhood); + // Create the Operation Shitstorm level + currentLevel = OperationShitstorm(); + + // Add and wait for the level to load + await world.add(currentLevel); - // Create target house - targetHouse = TargetHouse(); - world.add(targetHouse); + // Start the level + await currentLevel.startLevel(); - // Create player - player = Player(); - world.add(player); + // Get the player from the level + player = currentLevel.player; // Setup camera to follow player gameCamera.follow(player); @@ -101,9 +101,9 @@ class ShitmanGame extends FlameGame totalMissions++; if (infiniteMode) { - // Generate new mission after delay + // Generate new mission after delay, but keep allowing input Future.delayed(Duration(seconds: 2), () { - initializeLevel(); + currentLevel.selectRandomTarget(); gameState = GameState.playing; }); } @@ -120,7 +120,11 @@ class ShitmanGame extends FlameGame Set keysPressed, ) { super.onKeyEvent(event, keysPressed); - if (gameState != GameState.playing) return KeyEventResult.ignored; + if (gameState != GameState.playing && + gameState != GameState.missionComplete && + gameState != GameState.gameOver) { + return KeyEventResult.ignored; + } // Handle pause if (keysPressed.contains(LogicalKeyboardKey.escape)) { @@ -137,6 +141,9 @@ class ShitmanGame extends FlameGame player.handleAction(event.logicalKey); } + // Mission completion is now handled by the player's escape sequence + // No automatic completion when near target + return KeyEventResult.handled; } diff --git a/lib/game/shitman_world.dart b/lib/game/shitman_world.dart new file mode 100644 index 0000000..be05a36 --- /dev/null +++ b/lib/game/shitman_world.dart @@ -0,0 +1,105 @@ +import 'package:flame/components.dart'; +import 'package:shitman/attributes/resetable.dart'; +import 'package:shitman/game/levels/shit_level.dart'; +import 'package:shitman/game/components/base.dart'; +import 'package:shitman/services/log_service.dart'; +import 'package:shitman/settings/app_settings.dart'; + +class ShitmanWorld extends World with Resetable, AppLogging, AppSettings { + ShitLevel? _currentLevel; + bool _isInitialized = false; + + /// Get the current level + ShitLevel? get currentLevel => _currentLevel; + + /// Check if world is initialized + bool get isInitialized => _isInitialized; + + @override + Future onLoad() async { + await super.onLoad(); + await initSettings(); + _isInitialized = true; + appLog.fine('ShitmanWorld initialized'); + } + + /// Load a specific level + Future loadLevel(ShitLevel level) async { + appLog.fine('Loading level: ${level.levelName}'); + + // Reset current level if one exists + if (_currentLevel != null) { + await _currentLevel!.reset(); + _currentLevel!.removeFromParent(); + } + + // Set and load new level + _currentLevel = level; + add(_currentLevel!); + await _currentLevel!.onLoad(); + _currentLevel!.startLevel(); + + appLog.info('Level loaded: ${level.levelName}'); + } + + /// Unload the current level + Future unloadLevel() async { + if (_currentLevel == null) return; + + appLog.fine('Unloading level: ${_currentLevel!.levelName}'); + + _currentLevel!.endLevel(); + await _currentLevel!.reset(); + _currentLevel!.removeFromParent(); + _currentLevel = null; + + appLog.fine('Level unloaded'); + } + + /// Reset all components in the world that implement Resetable + Future resetAllComponents() async { + appLog.fine('Resetting all world components'); + + final resetableComponents = children.whereType().toList(); + for (final component in resetableComponents) { + await component.reset(); + } + + // Also reset the current level + if (_currentLevel != null) { + await _currentLevel!.reset(); + } + + appLog.fine('All components reset'); + } + + /// Get all components of a specific type + List getComponentsOfType() { + return children.whereType().toList(); + } + + /// Get all ShitComponents + List getAllShitComponents() { + return children.whereType().toList(); + } + + /// Clear all components from the world + Future clearWorld() async { + appLog.fine('Clearing world'); + + // Reset all resetable components before removing + await resetAllComponents(); + + // Remove all children + removeAll(children); + _currentLevel = null; + + appLog.fine('World cleared'); + } + + @override + Future reset() async { + appLog.fine('Resetting ShitmanWorld'); + await clearWorld(); + } +} diff --git a/lib/services/log_service.dart b/lib/services/log_service.dart new file mode 100644 index 0000000..580964e --- /dev/null +++ b/lib/services/log_service.dart @@ -0,0 +1,258 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; + +mixin class AppLogging { + final LogService appLog = LogService(); +} + +class LogEntry { + final DateTime timestamp; + final Level level; + final String message; + final String? logger; + final Object? error; + final StackTrace? stackTrace; + + LogEntry({ + required this.timestamp, + required this.level, + required this.message, + this.logger, + this.error, + this.stackTrace, + }); + + String format() { + final buffer = StringBuffer(); + buffer.write('${timestamp.toIso8601String()} '); + buffer.write('[${level.name.toUpperCase().padRight(8)}] '); + if (logger != null) buffer.write('$logger: '); + buffer.write(message); + if (error != null) buffer.write(' | Error: $error'); + if (stackTrace != null) buffer.write('\n$stackTrace'); + return buffer.toString(); + } +} + +class LogService { + static final LogService _instance = LogService._internal(); + static LogService get instance => _instance; + static const String defaultLoggerName = 'ShitMan-Game'; + + final Queue _buffer = Queue(); + final StreamController _controller = + StreamController.broadcast(); + + Timer? _flushTimer; + Isolate? _fileIsolate; + SendPort? _fileSendPort; + + bool _isFileLoggingEnabled = false; + String? _logFilePath; + Level _minLevel = Level.INFO; + + static const int _bufferSize = 100; + static const Duration _flushInterval = Duration(seconds: 1); + + factory LogService() => _instance; + + LogService._internal() { + _initializeLogger(); + _setupPeriodicFlush(); + } + + void _initializeLogger() { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + final level = record.level; + if (level >= _minLevel) { + _addEntry( + LogEntry( + timestamp: record.time, + level: level, + message: record.message, + logger: record.loggerName, + error: record.error, + stackTrace: record.stackTrace, + ), + ); + } + }); + } + + void _setupPeriodicFlush() { + _flushTimer = Timer.periodic(_flushInterval, (_) => _flushBuffer()); + } + + void _addEntry(LogEntry entry) { + _buffer.add(entry); + _controller.add(entry); + + if (kDebugMode) { + debugPrint(entry.format()); + } + + if (_buffer.length >= _bufferSize) { + _flushBuffer(); + } + } + + void _flushBuffer() { + if (_buffer.isEmpty || !_isFileLoggingEnabled || _fileSendPort == null) { + return; + } + + final entries = List.from(_buffer); + _buffer.clear(); + + _fileSendPort!.send(entries); + } + + Future enableFileLogging({String? customPath}) async { + if (_isFileLoggingEnabled) return; + + try { + _logFilePath = customPath ?? 'logs/shitman.log'; + + final receivePort = ReceivePort(); + _fileIsolate = await Isolate.spawn(_fileLoggingIsolate, [ + receivePort.sendPort, + _logFilePath!, + ]); + + _fileSendPort = await receivePort.first as SendPort; + _isFileLoggingEnabled = true; + } catch (e) { + debugPrint('Failed to enable file logging: $e'); + } + } + + static void _fileLoggingIsolate(List args) async { + final SendPort mainSendPort = args[0]; + final String logFilePath = args[1]; + + final receivePort = ReceivePort(); + mainSendPort.send(receivePort.sendPort); + + IOSink? logFile; + + try { + final file = File(logFilePath); + await file.parent.create(recursive: true); + logFile = file.openWrite(mode: FileMode.append); + + await for (final data in receivePort) { + if (data is List) { + for (final entry in data) { + logFile.writeln(entry.format()); + } + await logFile.flush(); + } + } + } catch (e) { + debugPrint('File logging isolate error: $e'); + } finally { + await logFile?.close(); + } + } + + void setMinLevel(Level level) { + _minLevel = level; + } + + Stream get logStream => _controller.stream; + + void log( + String message, { + Level level = Level.INFO, + String? logger, + Object? error, + StackTrace? stackTrace, + }) { + Logger(logger ?? defaultLoggerName).log(level, message, error, stackTrace); + } + + void finest( + String message, { + String? logger, + Object? error, + StackTrace? stackTrace, + }) { + Logger(logger ?? defaultLoggerName).finest(message, error, stackTrace); + } + + void finer( + String message, { + String? logger, + Object? error, + StackTrace? stackTrace, + }) { + Logger(logger ?? defaultLoggerName).finer(message, error, stackTrace); + } + + void fine( + String message, { + String? logger, + Object? error, + StackTrace? stackTrace, + }) { + Logger(logger ?? defaultLoggerName).fine(message, error, stackTrace); + } + + void config( + String message, { + String? logger, + Object? error, + StackTrace? stackTrace, + }) { + Logger(logger ?? defaultLoggerName).config(message, error, stackTrace); + } + + void info( + String message, { + String? logger, + Object? error, + StackTrace? stackTrace, + }) { + Logger(logger ?? defaultLoggerName).info(message, error, stackTrace); + } + + void warning( + String message, { + String? logger, + Object? error, + StackTrace? stackTrace, + }) { + Logger(logger ?? defaultLoggerName).warning(message, error, stackTrace); + } + + void severe( + String message, { + String? logger, + Object? error, + StackTrace? stackTrace, + }) { + Logger(logger ?? defaultLoggerName).severe(message, error, stackTrace); + } + + void shout( + String message, { + String? logger, + Object? error, + StackTrace? stackTrace, + }) { + Logger(logger ?? defaultLoggerName).shout(message, error, stackTrace); + } + + Future dispose() async { + _flushTimer?.cancel(); + _flushBuffer(); + await _controller.close(); + _fileIsolate?.kill(); + } +} diff --git a/lib/ui/in_game_ui.dart b/lib/ui/in_game_ui.dart index 61a5da0..818b75e 100644 --- a/lib/ui/in_game_ui.dart +++ b/lib/ui/in_game_ui.dart @@ -116,10 +116,10 @@ class MainMenuUI extends StatelessWidget with AppSettings { children: [ NesButton( type: NesButtonType.primary, - onPressed: () { + onPressed: () async { game.overlays.remove(MainMenuUI.overlayID); game.overlays.add(InGameUI.overlayID); - game.startGame(); + await game.startGame(); }, child: Text('menu.start_mission'.tr()), ), @@ -135,7 +135,7 @@ class MainMenuUI extends StatelessWidget with AppSettings { SizedBox(height: 16), NesButton( type: NesButtonType.normal, - onPressed: () => game.startInfiniteMode(), + onPressed: () async => await game.startInfiniteMode(), child: Text('menu.infinite_mode'.tr()), ), ], diff --git a/pubspec.lock b/pubspec.lock index 44260c0..5273740 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -269,6 +269,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -286,7 +294,7 @@ packages: source: hosted version: "0.11.1" meta: - dependency: transitive + dependency: "direct main" description: name: meta sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c diff --git a/pubspec.yaml b/pubspec.yaml index bc17fc0..712bd3d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: shitman -description: "Hitman, but with shit." +description: "Hitman, but shit." publish_to: 'none' version: 1.0.0+1 @@ -16,6 +16,8 @@ dependencies: google_fonts: ^6.2.1 easy_localization: ^3.0.7+1 shared_preferences: ^2.2.2 + meta: ^1.16.0 + logging: ^1.3.0 dev_dependencies: flutter_test: diff --git a/web/manifest.json b/web/manifest.json index 18b3966..ca0c570 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -5,7 +5,7 @@ "display": "standalone", "background_color": "#200E29", "theme_color": "#ab519f", - "description": "Hitman, but with shit.", + "description": "Hitman, but shit.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ @@ -32,4 +32,4 @@ "purpose": "maskable" } ] -} \ No newline at end of file +}