updated level...player can now "complete" (no ui)
All checks were successful
/ build-web (push) Successful in 4m5s

This commit is contained in:
zeyus 2025-07-27 17:41:51 +02:00
parent 2f13bb91f7
commit 67aaa9589f
Signed by: zeyus
GPG key ID: A836639BA719C614
20 changed files with 1914 additions and 199 deletions

View file

@ -0,0 +1,8 @@
import "dart:async";
import "package:meta/meta.dart";
abstract mixin class Resetable {
@mustBeOverridden
FutureOr<void> reset();
}

View file

@ -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<ShitmanGame>, 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<void> 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
}
}

View file

@ -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<Component> securitySystems = [];
HouseComponent({
required super.gridPosition,
required this.houseType,
}) {
size = Vector2.all(houseSize);
}
@override
Future<void> 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<void> 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<void> reset() async {
await super.reset();
isActiveTarget = false;
}
}
enum HouseType { suburban, modern, cottage, apartment }

View file

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

View file

@ -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<void> 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<void> 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<ShitmanGame> {
class House extends DecorativeShit with Stationary, AppSettings {
bool isTarget;
int houseType;
bool hasLights = false;
@ -122,14 +136,15 @@ class House extends RectangleComponent with HasGameReference<ShitmanGame> {
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<void> 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<ShitmanGame> {
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<ShitmanGame> {
@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<ShitmanGame> {
);
// 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<ShitmanGame> {
// 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<ShitmanGame> {
// 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<ShitmanGame> {
// 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<void> reset() async {
isTarget = false;
hasLights = false;
hasSecurityCamera = false;
hasWatchDog = false;
// Reset vision cone
if (visionCone != null) {
await visionCone!.reset();
removeAll(children.whereType<VisionCone>());
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');
}
}

View file

@ -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<ShitmanGame> {
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<void> 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<ShitmanGame> {
opacity: 0.0, // Start hidden
);
add(playerVisionCone!);
appLog.fine('Player loaded at position $position');
}
void handleInput(Set<LogicalKeyboardKey> 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<ShitmanGame> {
}
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<ShitmanGame> {
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<ShitmanGame> {
}
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<OperationShitstorm>().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<OperationShitstorm>().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<ShitmanGame> {
// 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<ShitmanGame> {
// 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<ShitmanGame> {
}
void checkForDetection() {
final neighborhood =
game.world.children.whereType<Neighborhood>().firstOrNull;
if (neighborhood == null) return;
// Check for detection from houses in any active level
final houses = game.world.children
.expand((component) => component.children)
.whereType<HouseComponent>();
for (final house in neighborhood.houses) {
if (house.canDetectPlayer(position, stealthLevel)) {
for (final house in houses) {
if (house.detectsPlayer(position, stealthLevel)) {
getDetected();
break;
}
@ -203,30 +256,50 @@ class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
@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<void> 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');
}
}

View file

@ -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<SmokeParticle> smokeParticles = [];
List<Particle> smokeParticles = [];
@override
Future<void> 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<void> reset() async {
state = PoopBagState.placed;
burnTimer = 0.0;
smokeParticles.clear();
paint = Paint()..color = const Color(0xFF8B4513);
removeAll(children.whereType<Effect>());
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);
}
}

View file

@ -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<void> 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<void> 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<void> 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<void> 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<void> reset() async {
// Nothing to reset for road components
}
}
enum CornerDirection { topLeft, topRight, bottomLeft, bottomRight }
enum DeadEndDirection { north, south, east, west }

View file

@ -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<void> 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<void> 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<void> 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<void> 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<void> 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<void> reset() async {
currentAngle = 0;
isPatrolling = true;
position = patrolCenter;
}
}

View file

@ -1,8 +1,8 @@
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;
@ -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}');
}
}
@ -49,4 +49,13 @@ class TargetHouse extends Component {
Vector2? getTargetPosition() {
return currentTarget?.doorPosition;
}
@override
Future<void> reset() async {
// Reset target house state
currentTarget?.isTarget = false;
currentTarget = null;
missionActive = false;
appLog.finest('Target house reset');
}
}

View file

@ -2,7 +2,9 @@ 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
@ -26,14 +28,16 @@ class VisionCone extends PositionComponent {
Future<void> onLoad() async {
await super.onLoad();
fillPaint = Paint()
..color = color.withValues(alpha: opacity)
..style = PaintingStyle.fill;
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;
strokePaint =
Paint()
..color = color.withValues(alpha: opacity + 0.2)
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
_calculateVisionCone();
}
@ -69,12 +73,12 @@ class VisionCone extends PositionComponent {
/// 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;
@ -99,7 +103,8 @@ class VisionCone extends PositionComponent {
/// 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 parentPosition =
(parent as PositionComponent?)?.position ?? Vector2.zero();
final worldPosition = parentPosition + position;
final rayDirection = target - worldPosition;
final distance = rayDirection.length;
@ -125,14 +130,16 @@ class VisionCone extends PositionComponent {
void updateOpacity(double newOpacity) {
opacity = newOpacity;
fillPaint = Paint()
..color = color.withValues(alpha: opacity)
..style = PaintingStyle.fill;
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;
strokePaint =
Paint()
..color = color.withValues(alpha: opacity + 0.2)
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
}
@override
@ -155,4 +162,13 @@ class VisionCone extends PositionComponent {
canvas.drawPath(path, strokePaint);
}
@override
Future<void> reset() async {
// Reset vision cone state
direction = 0.0;
opacity = 0.3;
visionVertices.clear();
_calculateVisionCone();
appLog.finest('Vision cone reset');
}
}

View file

@ -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<List<LevelComponent?>> levelGrid = [];
List<HouseComponent> houses = [];
TargetHouseComponent? currentTarget;
OperationShitstorm() : super(levelName: "Operation: Shitstorm", difficulty: 1);
@override
Future<void> 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<void> 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<void> 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<void> onLevelStart() async {
appLog.info('Starting Operation: Shitstorm');
// Level-specific start logic can be added here
}
@override
Future<void> onLevelEnd() async {
appLog.info('Operation: Shitstorm completed');
// Level-specific end logic can be added here
}
@override
Future<void> 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;
}
}

View file

@ -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<ShitmanGame>, 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<void> 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<void> 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<void> 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<void> 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<void> onLevelStart() async {
// Default implementation does nothing
}
/// Override this method in subclasses for custom end logic
Future<void> onLevelEnd() async {
// Default implementation does nothing
}
/// Get all components in this level that implement Resetable
List<Resetable> getResetableComponents() {
return children.whereType<Resetable>().toList();
}
/// Get all ShitComponents in this level
List<ShitComponent> getAllShitComponents() {
return children.whereType<ShitComponent>().toList();
}
/// Reset all components in this level
Future<void> resetAllComponents() async {
appLog.fine('Resetting all components in level: $levelName');
final resetableComponents = getResetableComponents();
for (final component in resetableComponents) {
await component.reset();
}
}
@override
Future<void> 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<void> onLevelReset() async {
// Default implementation does nothing
}
}

View file

@ -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<void> 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<void> startGame() async {
gameState = GameState.playing;
initializeLevel();
await initializeLevel();
}
void startInfiniteMode() {
Future<void> 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<void> initializeLevel() async {
// Clear previous level
world.removeAll(world.children);
// Create neighborhood
neighborhood = Neighborhood();
world.add(neighborhood);
// Create the Operation Shitstorm level
currentLevel = OperationShitstorm();
// Create target house
targetHouse = TargetHouse();
world.add(targetHouse);
// Add and wait for the level to load
await world.add(currentLevel);
// Create player
player = Player();
world.add(player);
// Start the level
await currentLevel.startLevel();
// 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<LogicalKeyboardKey> 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;
}

105
lib/game/shitman_world.dart Normal file
View file

@ -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<void> onLoad() async {
await super.onLoad();
await initSettings();
_isInitialized = true;
appLog.fine('ShitmanWorld initialized');
}
/// Load a specific level
Future<void> 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<void> 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<void> resetAllComponents() async {
appLog.fine('Resetting all world components');
final resetableComponents = children.whereType<Resetable>().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<T> getComponentsOfType<T extends Component>() {
return children.whereType<T>().toList();
}
/// Get all ShitComponents
List<ShitComponent> getAllShitComponents() {
return children.whereType<ShitComponent>().toList();
}
/// Clear all components from the world
Future<void> 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<void> reset() async {
appLog.fine('Resetting ShitmanWorld');
await clearWorld();
}
}

View file

@ -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<LogEntry> _buffer = Queue<LogEntry>();
final StreamController<LogEntry> _controller =
StreamController<LogEntry>.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<LogEntry>.from(_buffer);
_buffer.clear();
_fileSendPort!.send(entries);
}
Future<void> 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<dynamic> 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<LogEntry>) {
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<LogEntry> 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<void> dispose() async {
_flushTimer?.cancel();
_flushBuffer();
await _controller.close();
_fileIsolate?.kill();
}
}

View file

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

View file

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

View file

@ -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:

View file

@ -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": [