updated level...player can now "complete" (no ui)
All checks were successful
/ build-web (push) Successful in 4m5s
All checks were successful
/ build-web (push) Successful in 4m5s
This commit is contained in:
parent
2f13bb91f7
commit
67aaa9589f
20 changed files with 1914 additions and 199 deletions
8
lib/attributes/resetable.dart
Normal file
8
lib/attributes/resetable.dart
Normal file
|
@ -0,0 +1,8 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:meta/meta.dart";
|
||||
|
||||
abstract mixin class Resetable {
|
||||
@mustBeOverridden
|
||||
FutureOr<void> reset();
|
||||
}
|
74
lib/game/components/base.dart
Normal file
74
lib/game/components/base.dart
Normal 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
|
||||
}
|
||||
}
|
149
lib/game/components/house_components.dart
Normal file
149
lib/game/components/house_components.dart
Normal 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 }
|
77
lib/game/components/level_components.dart
Normal file
77
lib/game/components/level_components.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
class House extends RectangleComponent with HasGameReference<ShitmanGame> {
|
||||
// Regenerate neighborhood if needed
|
||||
generateNeighborhood();
|
||||
}
|
||||
}
|
||||
|
||||
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,9 +250,8 @@ 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()
|
||||
if (appSettings.getBool('game.show_detection_radius')) {
|
||||
final radiusPaint = Paint()
|
||||
..color = const Color(0xFFFF9800).withValues(alpha: 0.2)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0;
|
||||
|
@ -251,8 +267,7 @@ class House extends RectangleComponent with HasGameReference<ShitmanGame> {
|
|||
|
||||
// Draw target indicator
|
||||
if (isTarget) {
|
||||
final targetPaint =
|
||||
Paint()
|
||||
final targetPaint = Paint()
|
||||
..color = const Color(0xFFFF0000)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3.0;
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
// 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(
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
239
lib/game/components/road_components.dart
Normal file
239
lib/game/components/road_components.dart
Normal 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 }
|
297
lib/game/components/security_components.dart
Normal file
297
lib/game/components/security_components.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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,11 +28,13 @@ class VisionCone extends PositionComponent {
|
|||
Future<void> onLoad() async {
|
||||
await super.onLoad();
|
||||
|
||||
fillPaint = Paint()
|
||||
fillPaint =
|
||||
Paint()
|
||||
..color = color.withValues(alpha: opacity)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
strokePaint = Paint()
|
||||
strokePaint =
|
||||
Paint()
|
||||
..color = color.withValues(alpha: opacity + 0.2)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0;
|
||||
|
@ -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,11 +130,13 @@ class VisionCone extends PositionComponent {
|
|||
|
||||
void updateOpacity(double newOpacity) {
|
||||
opacity = newOpacity;
|
||||
fillPaint = Paint()
|
||||
fillPaint =
|
||||
Paint()
|
||||
..color = color.withValues(alpha: opacity)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
strokePaint = Paint()
|
||||
strokePaint =
|
||||
Paint()
|
||||
..color = color.withValues(alpha: opacity + 0.2)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0;
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
226
lib/game/levels/operation_shitstorm.dart
Normal file
226
lib/game/levels/operation_shitstorm.dart
Normal 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;
|
||||
}
|
||||
}
|
124
lib/game/levels/shit_level.dart
Normal file
124
lib/game/levels/shit_level.dart
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
105
lib/game/shitman_world.dart
Normal 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();
|
||||
}
|
||||
}
|
258
lib/services/log_service.dart
Normal file
258
lib/services/log_service.dart
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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()),
|
||||
),
|
||||
],
|
||||
|
|
10
pubspec.lock
10
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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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": [
|
||||
|
|
Loading…
Add table
Reference in a new issue