This commit is contained in:
parent
67aaa9589f
commit
f7a08a5099
16 changed files with 788 additions and 112 deletions
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"game": {
|
"game": {
|
||||||
"title": "SHITMAN",
|
"title": "SHITMAN",
|
||||||
"subtitle": "Lorteprank Mesteren",
|
"subtitle": "Hitman, but shit",
|
||||||
"description": "Snig rundt og efterlad brændende lorteposer\nudea at blive opdaget!"
|
"description": "Snig rundt og efterlad brændende lorteposer\ndiskret og stinkende"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"start_mission": "START MISSION",
|
"start_mission": "START MISSION",
|
||||||
|
@ -40,7 +40,15 @@
|
||||||
"show_vision_cones": "Vis Synsfelt",
|
"show_vision_cones": "Vis Synsfelt",
|
||||||
"show_detection_radius": "Vis Detektionsområder",
|
"show_detection_radius": "Vis Detektionsområder",
|
||||||
"vision_cones_help": "Viser synsfelt for fjender og spiller",
|
"vision_cones_help": "Viser synsfelt for fjender og spiller",
|
||||||
"detection_help": "Viser detektionsområder omkring sikkerhedsfunktioner"
|
"detection_help": "Viser detektionsområder omkring sikkerhedsfunktioner",
|
||||||
|
"touch_controls": "Berøringskontroller",
|
||||||
|
"touch_controls_help": "Aktivér virtuel joystick og knapper på skærmen",
|
||||||
|
"language_title": "Sprog",
|
||||||
|
"language": {
|
||||||
|
"english": "Engelsk",
|
||||||
|
"danish": "Dansk",
|
||||||
|
"german": "Tysk"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"placing_poop": "Placerer lortepose på position",
|
"placing_poop": "Placerer lortepose på position",
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"game": {
|
"game": {
|
||||||
"title": "SHITMAN",
|
"title": "SHITMAN",
|
||||||
"subtitle": "Der Kackhaufen-Streich-Meister",
|
"subtitle": "Hitman, but shit",
|
||||||
"description": "Schleich herum und hinterlasse brennende Kacktüten\nohne erwischt zu werden!"
|
"description": "Schleich herum und hinterlasse brennende Kacktüten\nheimlich und stinkend"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"start_mission": "MISSION STARTEN",
|
"start_mission": "MISSION STARTEN",
|
||||||
|
@ -40,7 +40,15 @@
|
||||||
"show_vision_cones": "Sichtfelder Anzeigen",
|
"show_vision_cones": "Sichtfelder Anzeigen",
|
||||||
"show_detection_radius": "Erkennungsbereiche Anzeigen",
|
"show_detection_radius": "Erkennungsbereiche Anzeigen",
|
||||||
"vision_cones_help": "Zeigt Sichtfelder für Feinde und Spieler",
|
"vision_cones_help": "Zeigt Sichtfelder für Feinde und Spieler",
|
||||||
"detection_help": "Zeigt Erkennungsbereiche um Sicherheitsmerkmale"
|
"detection_help": "Zeigt Erkennungsbereiche um Sicherheitsmerkmale",
|
||||||
|
"touch_controls": "Touch-Steuerung",
|
||||||
|
"touch_controls_help": "Aktiviere virtuellen Joystick und Tasten auf dem Bildschirm",
|
||||||
|
"language_title": "Sprache",
|
||||||
|
"language": {
|
||||||
|
"english": "Englisch",
|
||||||
|
"danish": "Dänisch",
|
||||||
|
"german": "Deutsch"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"placing_poop": "Kacktüte wird platziert an Position",
|
"placing_poop": "Kacktüte wird platziert an Position",
|
||||||
|
@ -54,4 +62,4 @@
|
||||||
"poop_burning": "Kacktüte brennt jetzt!",
|
"poop_burning": "Kacktüte brennt jetzt!",
|
||||||
"poop_extinguished": "Kacktüte ist ausgebrannt"
|
"poop_extinguished": "Kacktüte ist ausgebrannt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"game": {
|
"game": {
|
||||||
"title": "SHITMAN",
|
"title": "SHITMAN",
|
||||||
"subtitle": "The Poop Prank Master",
|
"subtitle": "Hitman, but shit",
|
||||||
"description": "Sneak around and leave flaming poop bags\nwithout getting caught!"
|
"description": "Sneak around and leave flaming poop bags\nstealthy and stinky."
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"start_mission": "START MISSION",
|
"start_mission": "START MISSION",
|
||||||
|
@ -40,7 +40,15 @@
|
||||||
"show_vision_cones": "Show Vision Cones",
|
"show_vision_cones": "Show Vision Cones",
|
||||||
"show_detection_radius": "Show Detection Areas",
|
"show_detection_radius": "Show Detection Areas",
|
||||||
"vision_cones_help": "Shows field of view for enemies and player",
|
"vision_cones_help": "Shows field of view for enemies and player",
|
||||||
"detection_help": "Shows detection areas around security features"
|
"detection_help": "Shows detection areas around security features",
|
||||||
|
"touch_controls": "Touch Controls",
|
||||||
|
"touch_controls_help": "Enable on-screen virtual joystick and buttons",
|
||||||
|
"language_title": "Language",
|
||||||
|
"language": {
|
||||||
|
"english": "English",
|
||||||
|
"danish": "Dansk",
|
||||||
|
"german": "Deutsch"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"placing_poop": "Placing poop bag at position",
|
"placing_poop": "Placing poop bag at position",
|
||||||
|
|
|
@ -22,6 +22,12 @@
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>CFBundleLocalizations</key>
|
||||||
|
<array>
|
||||||
|
<string>en</string>
|
||||||
|
<string>da</string>
|
||||||
|
<string>de</string>
|
||||||
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/material.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/poop_bag.dart';
|
||||||
import 'package:shitman/game/components/house_components.dart';
|
import 'package:shitman/game/components/house_components.dart';
|
||||||
import 'package:shitman/game/components/vision_cone.dart';
|
import 'package:shitman/game/components/vision_cone.dart';
|
||||||
import 'package:shitman/game/components/base.dart';
|
import 'package:shitman/game/components/base.dart';
|
||||||
import 'package:shitman/game/levels/operation_shitstorm.dart';
|
import 'package:shitman/game/levels/operation_shitstorm.dart';
|
||||||
|
import 'package:shitman/game/input_manager.dart';
|
||||||
import 'package:shitman/settings/app_settings.dart';
|
import 'package:shitman/settings/app_settings.dart';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
@ -22,12 +22,15 @@ class Player extends InteractiveShit with Ambulatory, AppSettings {
|
||||||
PoopBag? placedPoopBag;
|
PoopBag? placedPoopBag;
|
||||||
VisionCone? playerVisionCone;
|
VisionCone? playerVisionCone;
|
||||||
double lastMovementDirection = 0.0;
|
double lastMovementDirection = 0.0;
|
||||||
|
|
||||||
// Mission state
|
// Mission state
|
||||||
bool isEscaping = false;
|
bool isEscaping = false;
|
||||||
double escapeTimeLimit = 30.0; // 30 seconds to escape
|
double escapeTimeLimit = 30.0; // 30 seconds to escape
|
||||||
double escapeTimeRemaining = 0.0;
|
double escapeTimeRemaining = 0.0;
|
||||||
|
|
||||||
|
// Input manager for handling both keyboard and touch input
|
||||||
|
final InputManager inputManager = InputManager();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
await super.onLoad();
|
await super.onLoad();
|
||||||
|
@ -55,34 +58,22 @@ class Player extends InteractiveShit with Ambulatory, AppSettings {
|
||||||
// Don't handle input if caught
|
// Don't handle input if caught
|
||||||
if (isCaught) {
|
if (isCaught) {
|
||||||
velocity = Vector2.zero();
|
velocity = Vector2.zero();
|
||||||
|
inputManager.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
velocity = Vector2.zero();
|
// Update input manager with keyboard input
|
||||||
|
inputManager.setKeyboardMovement(keysPressed);
|
||||||
|
|
||||||
// Movement controls
|
// Apply movement from input manager
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowUp) ||
|
final movement = inputManager.movementInput;
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyW)) {
|
velocity = movement * speed;
|
||||||
velocity.y -= speed;
|
|
||||||
}
|
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowDown) ||
|
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyS)) {
|
|
||||||
velocity.y += speed;
|
|
||||||
}
|
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) ||
|
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyA)) {
|
|
||||||
velocity.x -= speed;
|
|
||||||
}
|
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowRight) ||
|
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyD)) {
|
|
||||||
velocity.x += speed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleAction(LogicalKeyboardKey key) {
|
void handleAction(LogicalKeyboardKey key) {
|
||||||
// Don't handle actions if caught
|
// Don't handle actions if caught
|
||||||
if (isCaught) return;
|
if (isCaught) return;
|
||||||
|
|
||||||
// Action controls
|
// Action controls
|
||||||
if (key == LogicalKeyboardKey.space) {
|
if (key == LogicalKeyboardKey.space) {
|
||||||
placePoop();
|
placePoop();
|
||||||
|
@ -92,6 +83,27 @@ class Player extends InteractiveShit with Ambulatory, AppSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle touch input from virtual controls
|
||||||
|
void handleTouchMovement(Vector2 input) {
|
||||||
|
if (isCaught) return;
|
||||||
|
inputManager.setTouchMovement(input);
|
||||||
|
|
||||||
|
// Apply movement from input manager
|
||||||
|
final movement = inputManager.movementInput;
|
||||||
|
velocity = movement * speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle touch actions
|
||||||
|
void handleTouchPoopBag() {
|
||||||
|
if (isCaught) return;
|
||||||
|
placePoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleTouchDoorbell() {
|
||||||
|
if (isCaught) return;
|
||||||
|
ringDoorbell();
|
||||||
|
}
|
||||||
|
|
||||||
void updateStealthLevel(double dt) {
|
void updateStealthLevel(double dt) {
|
||||||
// Simple stealth calculation - can be enhanced later
|
// Simple stealth calculation - can be enhanced later
|
||||||
// For now, player is more hidden when moving slowly or not at all
|
// For now, player is more hidden when moving slowly or not at all
|
||||||
|
@ -125,7 +137,8 @@ class Player extends InteractiveShit with Ambulatory, AppSettings {
|
||||||
appLog.fine('Attempting to ring doorbell');
|
appLog.fine('Attempting to ring doorbell');
|
||||||
|
|
||||||
// Check if near any target house door
|
// Check if near any target house door
|
||||||
final currentLevel = game.world.children.whereType<OperationShitstorm>().firstOrNull;
|
final currentLevel =
|
||||||
|
game.world.children.whereType<OperationShitstorm>().firstOrNull;
|
||||||
if (currentLevel != null && currentLevel.isPlayerNearTarget(position)) {
|
if (currentLevel != null && currentLevel.isPlayerNearTarget(position)) {
|
||||||
if (placedPoopBag != null) {
|
if (placedPoopBag != null) {
|
||||||
// Light the poop bag on fire
|
// Light the poop bag on fire
|
||||||
|
@ -146,25 +159,27 @@ class Player extends InteractiveShit with Ambulatory, AppSettings {
|
||||||
appLog.info('Escape sequence started! Get to the edge of the map!');
|
appLog.info('Escape sequence started! Get to the edge of the map!');
|
||||||
isEscaping = true;
|
isEscaping = true;
|
||||||
escapeTimeRemaining = escapeTimeLimit;
|
escapeTimeRemaining = escapeTimeLimit;
|
||||||
|
|
||||||
// TODO: Show escape timer UI
|
// TODO: Show escape timer UI
|
||||||
// TODO: Highlight escape routes on the map
|
// TODO: Highlight escape routes on the map
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if player is at an escape point (edge of the map)
|
/// Check if player is at an escape point (edge of the map)
|
||||||
bool isAtEscapePoint() {
|
bool isAtEscapePoint() {
|
||||||
const double escapeZoneThreshold = 50.0;
|
const double escapeZoneThreshold = 50.0;
|
||||||
|
|
||||||
// Check if near any edge of the level
|
// Check if near any edge of the level
|
||||||
return position.x < escapeZoneThreshold ||
|
return position.x < escapeZoneThreshold ||
|
||||||
position.y < escapeZoneThreshold ||
|
position.y < escapeZoneThreshold ||
|
||||||
position.x > (5 * 100) - escapeZoneThreshold || // 5x5 grid * 100 cell size
|
position.x >
|
||||||
position.y > (5 * 100) - escapeZoneThreshold;
|
(5 * 100) - escapeZoneThreshold || // 5x5 grid * 100 cell size
|
||||||
|
position.y > (5 * 100) - escapeZoneThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
void checkMissionProgress() {
|
void checkMissionProgress() {
|
||||||
// Check if near target house and has placed poop bag
|
// Check if near target house and has placed poop bag
|
||||||
final currentLevel = game.world.children.whereType<OperationShitstorm>().firstOrNull;
|
final currentLevel =
|
||||||
|
game.world.children.whereType<OperationShitstorm>().firstOrNull;
|
||||||
final targetPos = currentLevel?.getTargetPosition();
|
final targetPos = currentLevel?.getTargetPosition();
|
||||||
if (targetPos != null && placedPoopBag != null) {
|
if (targetPos != null && placedPoopBag != null) {
|
||||||
final distance = (position - targetPos).length;
|
final distance = (position - targetPos).length;
|
||||||
|
@ -204,14 +219,14 @@ class Player extends InteractiveShit with Ambulatory, AppSettings {
|
||||||
// Handle escape sequence
|
// Handle escape sequence
|
||||||
if (isEscaping) {
|
if (isEscaping) {
|
||||||
escapeTimeRemaining -= dt;
|
escapeTimeRemaining -= dt;
|
||||||
|
|
||||||
// Check if time ran out
|
// Check if time ran out
|
||||||
if (escapeTimeRemaining <= 0) {
|
if (escapeTimeRemaining <= 0) {
|
||||||
appLog.warning('Time ran out! Mission failed!');
|
appLog.warning('Time ran out! Mission failed!');
|
||||||
getDetected();
|
getDetected();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if player reached escape point (edge of map)
|
// Check if player reached escape point (edge of map)
|
||||||
if (isAtEscapePoint()) {
|
if (isAtEscapePoint()) {
|
||||||
appLog.info('Successfully escaped! Mission complete!');
|
appLog.info('Successfully escaped! Mission complete!');
|
||||||
|
@ -242,10 +257,11 @@ class Player extends InteractiveShit with Ambulatory, AppSettings {
|
||||||
|
|
||||||
void checkForDetection() {
|
void checkForDetection() {
|
||||||
// Check for detection from houses in any active level
|
// Check for detection from houses in any active level
|
||||||
final houses = game.world.children
|
final houses =
|
||||||
.expand((component) => component.children)
|
game.world.children
|
||||||
.whereType<HouseComponent>();
|
.expand((component) => component.children)
|
||||||
|
.whereType<HouseComponent>();
|
||||||
|
|
||||||
for (final house in houses) {
|
for (final house in houses) {
|
||||||
if (house.detectsPlayer(position, stealthLevel)) {
|
if (house.detectsPlayer(position, stealthLevel)) {
|
||||||
getDetected();
|
getDetected();
|
||||||
|
@ -257,10 +273,16 @@ class Player extends InteractiveShit with Ambulatory, AppSettings {
|
||||||
@override
|
@override
|
||||||
void render(Canvas canvas) {
|
void render(Canvas canvas) {
|
||||||
// Draw player as rectangle with color based on stealth level
|
// Draw player as rectangle with color based on stealth level
|
||||||
final playerPaint = Paint()
|
final playerPaint =
|
||||||
..color = isHidden
|
Paint()
|
||||||
? const Color(0xFF00FF00).withValues(alpha: 0.7) // Green when hidden
|
..color =
|
||||||
: const Color(0xFF0000FF).withValues(alpha: 0.9); // Blue when visible
|
isHidden
|
||||||
|
? const Color(0xFF00FF00).withValues(
|
||||||
|
alpha: 0.7,
|
||||||
|
) // Green when hidden
|
||||||
|
: const Color(
|
||||||
|
0xFF0000FF,
|
||||||
|
).withValues(alpha: 0.9); // Blue when visible
|
||||||
|
|
||||||
canvas.drawRect(size.toRect(), playerPaint);
|
canvas.drawRect(size.toRect(), playerPaint);
|
||||||
|
|
||||||
|
@ -268,12 +290,14 @@ class Player extends InteractiveShit with Ambulatory, AppSettings {
|
||||||
try {
|
try {
|
||||||
final debugMode = appSettings.getBool('game.debug_mode');
|
final debugMode = appSettings.getBool('game.debug_mode');
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
final stealthPaint = Paint()
|
final stealthPaint =
|
||||||
..color = Color.lerp(
|
Paint()
|
||||||
const Color(0xFFFF0000),
|
..color =
|
||||||
const Color(0xFF00FF00),
|
Color.lerp(
|
||||||
stealthLevel,
|
const Color(0xFFFF0000),
|
||||||
)!;
|
const Color(0xFF00FF00),
|
||||||
|
stealthLevel,
|
||||||
|
)!;
|
||||||
canvas.drawRect(Rect.fromLTWH(-5, -10, size.x + 10, 5), stealthPaint);
|
canvas.drawRect(Rect.fromLTWH(-5, -10, size.x + 10, 5), stealthPaint);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -293,13 +317,13 @@ class Player extends InteractiveShit with Ambulatory, AppSettings {
|
||||||
lastMovementDirection = 0.0;
|
lastMovementDirection = 0.0;
|
||||||
placedPoopBag = null;
|
placedPoopBag = null;
|
||||||
position = Vector2(200, 200); // Reset to starting position
|
position = Vector2(200, 200); // Reset to starting position
|
||||||
|
|
||||||
// Reset vision cone
|
// Reset vision cone
|
||||||
if (playerVisionCone != null) {
|
if (playerVisionCone != null) {
|
||||||
await playerVisionCone!.reset();
|
await playerVisionCone!.reset();
|
||||||
playerVisionCone!.updatePosition(size / 2, lastMovementDirection);
|
playerVisionCone!.updatePosition(size / 2, lastMovementDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
appLog.fine('Player reset to initial state');
|
appLog.fine('Player reset to initial state');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
87
lib/game/input_manager.dart
Normal file
87
lib/game/input_manager.dart
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// Manages input from both keyboard and touch controls
|
||||||
|
class InputManager {
|
||||||
|
// Movement state
|
||||||
|
Vector2 _movementInput = Vector2.zero();
|
||||||
|
|
||||||
|
// Action states
|
||||||
|
bool _poopBagPressed = false;
|
||||||
|
bool _doorbellPressed = false;
|
||||||
|
|
||||||
|
/// Set movement input from touch controls (-1 to 1 range)
|
||||||
|
void setTouchMovement(Vector2 input) {
|
||||||
|
_movementInput = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set movement input from keyboard
|
||||||
|
void setKeyboardMovement(Set<LogicalKeyboardKey> keysPressed) {
|
||||||
|
Vector2 keyboardInput = Vector2.zero();
|
||||||
|
|
||||||
|
if (keysPressed.contains(LogicalKeyboardKey.arrowUp) ||
|
||||||
|
keysPressed.contains(LogicalKeyboardKey.keyW)) {
|
||||||
|
keyboardInput.y -= 1;
|
||||||
|
}
|
||||||
|
if (keysPressed.contains(LogicalKeyboardKey.arrowDown) ||
|
||||||
|
keysPressed.contains(LogicalKeyboardKey.keyS)) {
|
||||||
|
keyboardInput.y += 1;
|
||||||
|
}
|
||||||
|
if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) ||
|
||||||
|
keysPressed.contains(LogicalKeyboardKey.keyA)) {
|
||||||
|
keyboardInput.x -= 1;
|
||||||
|
}
|
||||||
|
if (keysPressed.contains(LogicalKeyboardKey.arrowRight) ||
|
||||||
|
keysPressed.contains(LogicalKeyboardKey.keyD)) {
|
||||||
|
keyboardInput.x += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_movementInput = keyboardInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trigger poop bag action from touch
|
||||||
|
void setTouchPoopBag(bool pressed) {
|
||||||
|
if (pressed && !_poopBagPressed) {
|
||||||
|
_poopBagPressed = true;
|
||||||
|
} else if (!pressed) {
|
||||||
|
_poopBagPressed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trigger doorbell action from touch
|
||||||
|
void setTouchDoorbell(bool pressed) {
|
||||||
|
if (pressed && !_doorbellPressed) {
|
||||||
|
_doorbellPressed = true;
|
||||||
|
} else if (!pressed) {
|
||||||
|
_doorbellPressed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if poop bag action was triggered this frame
|
||||||
|
bool consumePoopBagAction() {
|
||||||
|
if (_poopBagPressed) {
|
||||||
|
_poopBagPressed = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if doorbell action was triggered this frame
|
||||||
|
bool consumeDoorbellAction() {
|
||||||
|
if (_doorbellPressed) {
|
||||||
|
_doorbellPressed = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current movement input
|
||||||
|
Vector2 get movementInput => _movementInput;
|
||||||
|
|
||||||
|
/// Reset all input states
|
||||||
|
void reset() {
|
||||||
|
_movementInput = Vector2.zero();
|
||||||
|
_poopBagPressed = false;
|
||||||
|
_doorbellPressed = false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart';
|
||||||
import 'package:shitman/game/components/player.dart';
|
import 'package:shitman/game/components/player.dart';
|
||||||
import 'package:shitman/game/levels/operation_shitstorm.dart';
|
import 'package:shitman/game/levels/operation_shitstorm.dart';
|
||||||
import 'package:shitman/game/shitman_world.dart';
|
import 'package:shitman/game/shitman_world.dart';
|
||||||
|
import 'package:shitman/ui/touch_controls.dart';
|
||||||
import 'package:shitman/settings/app_settings.dart';
|
import 'package:shitman/settings/app_settings.dart';
|
||||||
|
|
||||||
/// Shitman Game
|
/// Shitman Game
|
||||||
|
@ -49,9 +50,33 @@ class ShitmanGame extends FlameGame
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _initializeTouchControls() {
|
||||||
|
try {
|
||||||
|
final touchControlsEnabled = appSettings.getBool('ui.touch_enabled');
|
||||||
|
if (touchControlsEnabled) {
|
||||||
|
overlays.add('TouchControls');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If setting doesn't exist, auto-detect based on platform
|
||||||
|
if (TouchDetection.isTouchDevice) {
|
||||||
|
// Enable touch controls by default on touch devices
|
||||||
|
try {
|
||||||
|
appSettings.setBool('ui.touch_enabled', true);
|
||||||
|
overlays.add('TouchControls');
|
||||||
|
} catch (e) {
|
||||||
|
// Settings not ready, just add overlay
|
||||||
|
overlays.add('TouchControls');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> startGame() async {
|
Future<void> startGame() async {
|
||||||
gameState = GameState.playing;
|
gameState = GameState.playing;
|
||||||
await initializeLevel();
|
await initializeLevel();
|
||||||
|
|
||||||
|
// Initialize touch controls when game starts
|
||||||
|
_initializeTouchControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startInfiniteMode() async {
|
Future<void> startInfiniteMode() async {
|
||||||
|
@ -65,14 +90,19 @@ class ShitmanGame extends FlameGame
|
||||||
gameState = GameState.mainMenu;
|
gameState = GameState.mainMenu;
|
||||||
world.removeAll(world.children);
|
world.removeAll(world.children);
|
||||||
missionScore = 0;
|
missionScore = 0;
|
||||||
|
|
||||||
|
// Remove touch controls when stopping game
|
||||||
|
overlays.remove('TouchControls');
|
||||||
}
|
}
|
||||||
|
|
||||||
void pauseGame() {
|
void pauseGame() {
|
||||||
gameState = GameState.paused;
|
gameState = GameState.paused;
|
||||||
|
// Keep touch controls visible during pause for consistency
|
||||||
}
|
}
|
||||||
|
|
||||||
void resumeGame() {
|
void resumeGame() {
|
||||||
gameState = GameState.playing;
|
gameState = GameState.playing;
|
||||||
|
// Touch controls remain visible
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initializeLevel() async {
|
Future<void> initializeLevel() async {
|
||||||
|
@ -81,7 +111,7 @@ class ShitmanGame extends FlameGame
|
||||||
|
|
||||||
// Create the Operation Shitstorm level
|
// Create the Operation Shitstorm level
|
||||||
currentLevel = OperationShitstorm();
|
currentLevel = OperationShitstorm();
|
||||||
|
|
||||||
// Add and wait for the level to load
|
// Add and wait for the level to load
|
||||||
await world.add(currentLevel);
|
await world.add(currentLevel);
|
||||||
|
|
||||||
|
@ -120,8 +150,8 @@ class ShitmanGame extends FlameGame
|
||||||
Set<LogicalKeyboardKey> keysPressed,
|
Set<LogicalKeyboardKey> keysPressed,
|
||||||
) {
|
) {
|
||||||
super.onKeyEvent(event, keysPressed);
|
super.onKeyEvent(event, keysPressed);
|
||||||
if (gameState != GameState.playing &&
|
if (gameState != GameState.playing &&
|
||||||
gameState != GameState.missionComplete &&
|
gameState != GameState.missionComplete &&
|
||||||
gameState != GameState.gameOver) {
|
gameState != GameState.gameOver) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
|
@ -147,6 +177,42 @@ class ShitmanGame extends FlameGame
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle touch movement from virtual joystick
|
||||||
|
void handleTouchMovement(Vector2 input) {
|
||||||
|
if (gameState == GameState.playing) {
|
||||||
|
player.handleTouchMovement(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle poop bag action from touch
|
||||||
|
void handleTouchPoopBag() {
|
||||||
|
if (gameState == GameState.playing) {
|
||||||
|
player.handleTouchPoopBag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle doorbell action from touch
|
||||||
|
void handleTouchDoorbell() {
|
||||||
|
if (gameState == GameState.playing) {
|
||||||
|
player.handleTouchDoorbell();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle touch controls on/off
|
||||||
|
void toggleTouchControls(bool enabled) {
|
||||||
|
try {
|
||||||
|
appSettings.setBool('ui.touch_enabled', enabled);
|
||||||
|
if (enabled && (gameState == GameState.playing || gameState == GameState.paused || gameState == GameState.missionComplete || gameState == GameState.gameOver)) {
|
||||||
|
// Only show touch controls during actual gameplay states
|
||||||
|
overlays.add('TouchControls');
|
||||||
|
} else {
|
||||||
|
overlays.remove('TouchControls');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Handle settings error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void update(double dt) {
|
void update(double dt) {
|
||||||
super.update(dt);
|
super.update(dt);
|
||||||
|
|
|
@ -8,6 +8,8 @@ import 'package:nes_ui/nes_ui.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:shitman/game/shitman_game.dart';
|
import 'package:shitman/game/shitman_game.dart';
|
||||||
import 'package:shitman/ui/in_game_ui.dart';
|
import 'package:shitman/ui/in_game_ui.dart';
|
||||||
|
import 'package:shitman/ui/touch_controls_overlay.dart';
|
||||||
|
import 'package:shitman/settings/app_settings.dart';
|
||||||
|
|
||||||
class AnyInputScrollBehavior extends MaterialScrollBehavior {
|
class AnyInputScrollBehavior extends MaterialScrollBehavior {
|
||||||
// Override behavior methods and getters like dragDevices
|
// Override behavior methods and getters like dragDevices
|
||||||
|
@ -19,18 +21,30 @@ class AnyInputScrollBehavior extends MaterialScrollBehavior {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
LicenseRegistry.addLicense(() async* {
|
LicenseRegistry.addLicense(() async* {
|
||||||
final license = await rootBundle.loadString('google_fonts/OFL.txt');
|
final license = await rootBundle.loadString('google_fonts/OFL.txt');
|
||||||
yield LicenseEntryWithLineBreaks(['google_fonts'], license);
|
yield LicenseEntryWithLineBreaks(['google_fonts'], license);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load saved language preference
|
||||||
|
final appSettings = AppSettings();
|
||||||
|
await appSettings.initSettings();
|
||||||
|
String savedLanguage = 'en';
|
||||||
|
try {
|
||||||
|
savedLanguage = appSettings.appSettings.getString('ui.language');
|
||||||
|
} catch (e) {
|
||||||
|
// Use default if not found
|
||||||
|
}
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
EasyLocalization(
|
EasyLocalization(
|
||||||
supportedLocales: [Locale('en'), Locale('da'), Locale('de')],
|
supportedLocales: [Locale('en'), Locale('da'), Locale('de')],
|
||||||
path: 'assets/translations',
|
path: 'assets/translations',
|
||||||
fallbackLocale: Locale('en'),
|
fallbackLocale: Locale('en'),
|
||||||
startLocale: Locale('en'),
|
startLocale: Locale(savedLanguage),
|
||||||
useOnlyLangCode: true,
|
useOnlyLangCode: true,
|
||||||
useFallbackTranslations: true,
|
useFallbackTranslations: true,
|
||||||
child: Shitman(),
|
child: Shitman(),
|
||||||
|
@ -38,10 +52,29 @@ void main() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Shitman extends StatelessWidget {
|
class Shitman extends StatefulWidget {
|
||||||
const Shitman({super.key});
|
const Shitman({super.key});
|
||||||
|
|
||||||
// This widget is the root of your application.
|
@override
|
||||||
|
State<Shitman> createState() => _ShitmanState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShitmanState extends State<Shitman> {
|
||||||
|
late ThemeData _currentTheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentTheme = flutterNesTheme(brightness: Brightness.dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
// Recreate theme when locale changes to avoid lerp issues
|
||||||
|
_currentTheme = flutterNesTheme(brightness: Brightness.dark);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
@ -49,8 +82,9 @@ class Shitman extends StatelessWidget {
|
||||||
supportedLocales: context.supportedLocales,
|
supportedLocales: context.supportedLocales,
|
||||||
scrollBehavior: AnyInputScrollBehavior(),
|
scrollBehavior: AnyInputScrollBehavior(),
|
||||||
locale: context.locale,
|
locale: context.locale,
|
||||||
theme: flutterNesTheme(brightness: Brightness.dark),
|
theme: _currentTheme,
|
||||||
themeMode: ThemeMode.dark,
|
themeMode: ThemeMode.dark,
|
||||||
|
themeAnimationDuration: Duration.zero, // Disable theme animation
|
||||||
home: GameWidget(
|
home: GameWidget(
|
||||||
game: ShitmanGame(),
|
game: ShitmanGame(),
|
||||||
initialActiveOverlays: const ['MainMenu'],
|
initialActiveOverlays: const ['MainMenu'],
|
||||||
|
@ -63,6 +97,8 @@ class Shitman extends StatelessWidget {
|
||||||
SettingsUI(game as ShitmanGame),
|
SettingsUI(game as ShitmanGame),
|
||||||
PauseMenuUI.overlayID: (context, game) =>
|
PauseMenuUI.overlayID: (context, game) =>
|
||||||
PauseMenuUI(game as ShitmanGame),
|
PauseMenuUI(game as ShitmanGame),
|
||||||
|
TouchControlsOverlayWidget.overlayID: (context, game) =>
|
||||||
|
TouchControlsOverlayWidget(game: game as ShitmanGame),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'game.dart';
|
import 'game.dart';
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
import 'settings_manager.dart';
|
import 'settings_manager.dart';
|
||||||
|
import 'ui.dart';
|
||||||
|
|
||||||
mixin class AppSettings {
|
mixin class AppSettings {
|
||||||
final Settings appSettings = Settings();
|
final Settings appSettings = Settings();
|
||||||
|
@ -11,6 +12,7 @@ mixin class AppSettings {
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
appSettings.register(gameSettings);
|
appSettings.register(gameSettings);
|
||||||
appSettings.register(colorSettings);
|
appSettings.register(colorSettings);
|
||||||
|
appSettings.register(uiSettings);
|
||||||
await appSettings.init();
|
await appSettings.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,4 +42,3 @@ export 'exceptions.dart';
|
||||||
export 'setting.dart';
|
export 'setting.dart';
|
||||||
export 'settings_group.dart';
|
export 'settings_group.dart';
|
||||||
export 'settings_store.dart';
|
export 'settings_store.dart';
|
||||||
export 'game.dart';
|
|
||||||
|
|
21
lib/settings/ui.dart
Normal file
21
lib/settings/ui.dart
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import 'setting.dart';
|
||||||
|
import 'settings_group.dart';
|
||||||
|
|
||||||
|
/// Color settings, themes, etc.
|
||||||
|
final uiSettings = SettingsGroup(
|
||||||
|
key: 'ui',
|
||||||
|
items: [
|
||||||
|
/// Base color for game UI
|
||||||
|
BoolSetting(
|
||||||
|
key: 'touch_enabled',
|
||||||
|
defaultValue: false,
|
||||||
|
userConfigurable: true,
|
||||||
|
),
|
||||||
|
/// Selected language code
|
||||||
|
StringSetting(
|
||||||
|
key: 'language',
|
||||||
|
defaultValue: 'en',
|
||||||
|
userConfigurable: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
|
@ -33,7 +33,10 @@ class InGameUI extends StatelessWidget with AppSettings {
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.visibility_off, size: 16),
|
Icon(Icons.visibility_off, size: 16),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text('gameplay.hidden'.tr(), style: TextStyle(color: Colors.green)),
|
Text(
|
||||||
|
'gameplay.hidden'.tr(),
|
||||||
|
style: TextStyle(color: Colors.green),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -43,7 +46,10 @@ class InGameUI extends StatelessWidget with AppSettings {
|
||||||
backgroundColor: Colors.black87,
|
backgroundColor: Colors.black87,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text('gameplay.find_target'.tr(), style: TextStyle(color: Colors.white)),
|
child: Text(
|
||||||
|
'gameplay.find_target'.tr(),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Pause button
|
// Pause button
|
||||||
|
@ -54,7 +60,7 @@ class InGameUI extends StatelessWidget with AppSettings {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Bottom controls hint
|
// Bottom controls hint
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 20,
|
bottom: 20,
|
||||||
|
@ -67,9 +73,18 @@ class InGameUI extends StatelessWidget with AppSettings {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text('controls.move'.tr(), style: TextStyle(color: Colors.white)),
|
Text(
|
||||||
Text('controls.place_bag'.tr(), style: TextStyle(color: Colors.white)),
|
'controls.move'.tr(),
|
||||||
Text('controls.ring_bell'.tr(), style: TextStyle(color: Colors.white)),
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'controls.place_bag'.tr(),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'controls.ring_bell'.tr(),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -110,7 +125,7 @@ class MainMenuUI extends StatelessWidget with AppSettings {
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 16),
|
style: TextStyle(color: Colors.white70, fontSize: 16),
|
||||||
),
|
),
|
||||||
SizedBox(height: 40),
|
SizedBox(height: 40),
|
||||||
|
|
||||||
// Menu buttons
|
// Menu buttons
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
|
@ -140,7 +155,7 @@ class MainMenuUI extends StatelessWidget with AppSettings {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 40),
|
SizedBox(height: 40),
|
||||||
Text(
|
Text(
|
||||||
'game.description'.tr(),
|
'game.description'.tr(),
|
||||||
|
@ -168,6 +183,7 @@ class _SettingsUIState extends State<SettingsUI> with AppSettings {
|
||||||
bool showVisionCones = false;
|
bool showVisionCones = false;
|
||||||
bool showDetectionRadius = false;
|
bool showDetectionRadius = false;
|
||||||
bool debugMode = false;
|
bool debugMode = false;
|
||||||
|
bool touchControlsEnabled = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -177,14 +193,22 @@ class _SettingsUIState extends State<SettingsUI> with AppSettings {
|
||||||
|
|
||||||
void _loadSettings() {
|
void _loadSettings() {
|
||||||
try {
|
try {
|
||||||
showVisionCones = widget.game.appSettings.getBool('game.show_vision_cones');
|
showVisionCones = widget.game.appSettings.getBool(
|
||||||
showDetectionRadius = widget.game.appSettings.getBool('game.show_detection_radius');
|
'game.show_vision_cones',
|
||||||
|
);
|
||||||
|
showDetectionRadius = widget.game.appSettings.getBool(
|
||||||
|
'game.show_detection_radius',
|
||||||
|
);
|
||||||
debugMode = widget.game.appSettings.getBool('game.debug_mode');
|
debugMode = widget.game.appSettings.getBool('game.debug_mode');
|
||||||
|
touchControlsEnabled = widget.game.appSettings.getBool(
|
||||||
|
'ui.touch_enabled',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Settings not ready, use defaults
|
// Settings not ready, use defaults
|
||||||
showVisionCones = false;
|
showVisionCones = false;
|
||||||
showDetectionRadius = false;
|
showDetectionRadius = false;
|
||||||
debugMode = false;
|
debugMode = false;
|
||||||
|
touchControlsEnabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,9 +230,9 @@ class _SettingsUIState extends State<SettingsUI> with AppSettings {
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'menu.settings'.tr(),
|
'menu.settings'.tr(),
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(
|
||||||
color: Colors.white,
|
context,
|
||||||
),
|
).textTheme.headlineMedium?.copyWith(color: Colors.white),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.close, color: Colors.white),
|
icon: Icon(Icons.close, color: Colors.white),
|
||||||
|
@ -220,43 +244,58 @@ class _SettingsUIState extends State<SettingsUI> with AppSettings {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 24),
|
SizedBox(height: 24),
|
||||||
|
|
||||||
// Language selector
|
// Language selector
|
||||||
Text('Language / Sprog / Sprache:', style: TextStyle(color: Colors.white)),
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'settings.language_title'.tr(),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Icon(Icons.language, color: Colors.white),
|
||||||
|
],
|
||||||
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
NesDropdownMenu<String>(
|
NesDropdownMenu<String>(
|
||||||
initialValue: context.locale.languageCode,
|
initialValue: context.locale.languageCode,
|
||||||
entries: const [
|
entries: [
|
||||||
NesDropdownMenuEntry(
|
NesDropdownMenuEntry(
|
||||||
value: 'en',
|
value: 'en',
|
||||||
label: 'English',
|
label: 'settings.language.english'.tr(),
|
||||||
),
|
),
|
||||||
NesDropdownMenuEntry(
|
NesDropdownMenuEntry(
|
||||||
value: 'da',
|
value: 'da',
|
||||||
label: 'Dansk',
|
label: 'settings.language.danish'.tr(),
|
||||||
),
|
),
|
||||||
NesDropdownMenuEntry(
|
NesDropdownMenuEntry(
|
||||||
value: 'de',
|
value: 'de',
|
||||||
label: 'Deutsch',
|
label: 'settings.language.german'.tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
onChanged: (String? value) {
|
onChanged: (String? value) async {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
context.setLocale(Locale(value));
|
context.setLocale(Locale(value));
|
||||||
setState(() {});
|
// Save language preference
|
||||||
|
await widget.game.appSettings.setString('ui.language', value);
|
||||||
|
setState(() {}); // Simple setState to rebuild UI
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
|
|
||||||
// Gameplay Settings Section
|
// Gameplay Settings Section
|
||||||
Text(
|
Text(
|
||||||
'settings.gameplay'.tr(),
|
'settings.gameplay'.tr(),
|
||||||
style: TextStyle(color: Colors.orange, fontSize: 16, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Colors.orange,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
|
|
||||||
// Vision Cones toggle
|
// Vision Cones toggle
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
@ -265,8 +304,17 @@ class _SettingsUIState extends State<SettingsUI> with AppSettings {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('settings.show_vision_cones'.tr(), style: TextStyle(color: Colors.white)),
|
Text(
|
||||||
Text('settings.vision_cones_help'.tr(), style: TextStyle(color: Colors.white60, fontSize: 11)),
|
'settings.show_vision_cones'.tr(),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'settings.vision_cones_help'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white60,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -276,14 +324,17 @@ class _SettingsUIState extends State<SettingsUI> with AppSettings {
|
||||||
setState(() {
|
setState(() {
|
||||||
showVisionCones = value;
|
showVisionCones = value;
|
||||||
});
|
});
|
||||||
await widget.game.appSettings.setBool('game.show_vision_cones', value);
|
await widget.game.appSettings.setBool(
|
||||||
|
'game.show_vision_cones',
|
||||||
|
value,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
|
|
||||||
// Detection Radius toggle
|
// Detection Radius toggle
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
@ -291,8 +342,17 @@ class _SettingsUIState extends State<SettingsUI> with AppSettings {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('settings.show_detection_radius'.tr(), style: TextStyle(color: Colors.white)),
|
Text(
|
||||||
Text('settings.detection_help'.tr(), style: TextStyle(color: Colors.white60, fontSize: 11)),
|
'settings.show_detection_radius'.tr(),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'settings.detection_help'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white60,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -302,39 +362,92 @@ class _SettingsUIState extends State<SettingsUI> with AppSettings {
|
||||||
setState(() {
|
setState(() {
|
||||||
showDetectionRadius = value;
|
showDetectionRadius = value;
|
||||||
});
|
});
|
||||||
await widget.game.appSettings.setBool('game.show_detection_radius', value);
|
await widget.game.appSettings.setBool(
|
||||||
|
'game.show_detection_radius',
|
||||||
|
value,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
|
|
||||||
// Accessibility Section
|
// Accessibility Section
|
||||||
Text(
|
Text(
|
||||||
'settings.accessibility'.tr(),
|
'settings.accessibility'.tr(),
|
||||||
style: TextStyle(color: Colors.orange, fontSize: 16, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Colors.orange,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
|
|
||||||
// Debug mode toggle
|
// Debug mode toggle
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text('ui.debug_mode'.tr(), style: TextStyle(color: Colors.white)),
|
Text(
|
||||||
|
'ui.debug_mode'.tr(),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
NesCheckBox(
|
NesCheckBox(
|
||||||
value: debugMode,
|
value: debugMode,
|
||||||
onChange: (value) async {
|
onChange: (value) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
debugMode = value;
|
debugMode = value;
|
||||||
});
|
});
|
||||||
await widget.game.appSettings.setBool('game.debug_mode', value);
|
await widget.game.appSettings.setBool(
|
||||||
|
'game.debug_mode',
|
||||||
|
value,
|
||||||
|
);
|
||||||
widget.game.debugMode = value;
|
widget.game.debugMode = value;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Touch controls toggle
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'settings.touch_controls'.tr(),
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'settings.touch_controls_help'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white60,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
NesCheckBox(
|
||||||
|
value: touchControlsEnabled,
|
||||||
|
onChange: (value) async {
|
||||||
|
setState(() {
|
||||||
|
touchControlsEnabled = value;
|
||||||
|
});
|
||||||
|
await widget.game.appSettings.setBool(
|
||||||
|
'ui.touch_enabled',
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
widget.game.toggleTouchControls(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
SizedBox(height: 40),
|
SizedBox(height: 40),
|
||||||
Center(
|
Center(
|
||||||
child: NesButton(
|
child: NesButton(
|
||||||
|
@ -375,12 +488,12 @@ class PauseMenuUI extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'ui.paused'.tr(),
|
'ui.paused'.tr(),
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(
|
||||||
color: Colors.white,
|
context,
|
||||||
),
|
).textTheme.headlineMedium?.copyWith(color: Colors.white),
|
||||||
),
|
),
|
||||||
SizedBox(height: 24),
|
SizedBox(height: 24),
|
||||||
|
|
||||||
NesButton(
|
NesButton(
|
||||||
type: NesButtonType.primary,
|
type: NesButtonType.primary,
|
||||||
onPressed: () => game.overlays.remove(PauseMenuUI.overlayID),
|
onPressed: () => game.overlays.remove(PauseMenuUI.overlayID),
|
||||||
|
|
265
lib/ui/touch_controls.dart
Normal file
265
lib/ui/touch_controls.dart
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
|
||||||
|
/// A joystick thumb that shows movement direction
|
||||||
|
class JoystickThumb extends StatefulWidget {
|
||||||
|
JoystickThumb({
|
||||||
|
super.key,
|
||||||
|
this.size = 20,
|
||||||
|
Vector2? offset,
|
||||||
|
this.color = Colors.white,
|
||||||
|
}) : offset = offset ?? Vector2.zero();
|
||||||
|
|
||||||
|
final double size;
|
||||||
|
final Vector2 offset;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<JoystickThumb> createState() => _JoystickThumbState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _JoystickThumbState extends State<JoystickThumb> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Positioned(
|
||||||
|
left: widget.offset.x,
|
||||||
|
top: widget.offset.y,
|
||||||
|
child: Container(
|
||||||
|
width: widget.size,
|
||||||
|
height: widget.size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: widget.color,
|
||||||
|
border: Border.all(color: Colors.black26, width: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOffset(Vector2 offset) {
|
||||||
|
setState(() {
|
||||||
|
widget.offset.setFrom(offset);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Virtual joystick for movement
|
||||||
|
class VirtualJoystick extends StatelessWidget {
|
||||||
|
VirtualJoystick({
|
||||||
|
super.key,
|
||||||
|
required this.onMove,
|
||||||
|
this.size = 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Function(Vector2) onMove;
|
||||||
|
final double size;
|
||||||
|
final GlobalKey<_JoystickThumbState> _key = GlobalKey();
|
||||||
|
|
||||||
|
Vector2 get _centerPosition => Vector2(size / 2 - (size / 6), size / 2 - (size / 6));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onPanUpdate: (details) {
|
||||||
|
final offset = details.localPosition;
|
||||||
|
final center = Offset(size / 2, size / 2);
|
||||||
|
final distance = offset - center;
|
||||||
|
final angle = distance.direction;
|
||||||
|
final magnitude = distance.distance;
|
||||||
|
|
||||||
|
if (magnitude <= size / 2) {
|
||||||
|
final position = Vector2(
|
||||||
|
magnitude * cos(angle),
|
||||||
|
magnitude * sin(angle), // Fixed Y-axis
|
||||||
|
);
|
||||||
|
// Calculate thumb position relative to center
|
||||||
|
final thumbPos = Vector2(
|
||||||
|
size / 2 + distance.dx - (size / 6), // Center thumb in joystick
|
||||||
|
size / 2 + distance.dy - (size / 6),
|
||||||
|
);
|
||||||
|
onMove(position * 2 / size); // Normalize to -1 to 1
|
||||||
|
_key.currentState?.setOffset(thumbPos);
|
||||||
|
} else {
|
||||||
|
final position = Vector2(
|
||||||
|
(size / 2) * cos(angle),
|
||||||
|
(size / 2) * sin(angle), // Fixed Y-axis
|
||||||
|
);
|
||||||
|
// Calculate thumb position at edge
|
||||||
|
final thumbPos = Vector2(
|
||||||
|
size / 2 + (size / 2) * cos(angle) - (size / 6),
|
||||||
|
size / 2 + (size / 2) * sin(angle) - (size / 6),
|
||||||
|
);
|
||||||
|
onMove(position * 2 / size); // Normalize to -1 to 1
|
||||||
|
_key.currentState?.setOffset(thumbPos);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPanCancel: () => {
|
||||||
|
onMove(Vector2.zero()),
|
||||||
|
_key.currentState?.setOffset(_centerPosition),
|
||||||
|
},
|
||||||
|
onPanEnd: (_) => {
|
||||||
|
onMove(Vector2.zero()),
|
||||||
|
_key.currentState?.setOffset(_centerPosition),
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.black.withValues(alpha: 0.3),
|
||||||
|
border: Border.all(color: Colors.white.withValues(alpha: 0.5), width: 2),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
JoystickThumb(
|
||||||
|
key: _key,
|
||||||
|
size: size / 3,
|
||||||
|
offset: _centerPosition, // Start at center
|
||||||
|
color: Colors.white.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Touch button for actions
|
||||||
|
class TouchButton extends StatefulWidget {
|
||||||
|
const TouchButton({
|
||||||
|
super.key,
|
||||||
|
required this.onPressed,
|
||||||
|
this.onReleased,
|
||||||
|
required this.icon,
|
||||||
|
this.size = 60,
|
||||||
|
this.color = Colors.white,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final VoidCallback? onReleased;
|
||||||
|
final IconData icon;
|
||||||
|
final double size;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TouchButton> createState() => _TouchButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TouchButtonState extends State<TouchButton> {
|
||||||
|
bool _isPressed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTapDown: (_) {
|
||||||
|
setState(() => _isPressed = true);
|
||||||
|
widget.onPressed();
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
},
|
||||||
|
onTapUp: (_) {
|
||||||
|
setState(() => _isPressed = false);
|
||||||
|
widget.onReleased?.call();
|
||||||
|
},
|
||||||
|
onTapCancel: () {
|
||||||
|
setState(() => _isPressed = false);
|
||||||
|
widget.onReleased?.call();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: widget.size,
|
||||||
|
height: widget.size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: _isPressed
|
||||||
|
? Colors.white.withValues(alpha: 0.8)
|
||||||
|
: Colors.black.withValues(alpha: 0.3),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white.withValues(alpha: 0.5),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
widget.icon,
|
||||||
|
size: widget.size * 0.4,
|
||||||
|
color: _isPressed ? Colors.black : Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main touch controls overlay
|
||||||
|
class TouchControlsOverlay extends StatelessWidget {
|
||||||
|
const TouchControlsOverlay({
|
||||||
|
super.key,
|
||||||
|
required this.onMove,
|
||||||
|
required this.onPoopBag,
|
||||||
|
required this.onDoorbell,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Function(Vector2) onMove;
|
||||||
|
final VoidCallback onPoopBag;
|
||||||
|
final VoidCallback onDoorbell;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
// Movement joystick (bottom left)
|
||||||
|
Positioned(
|
||||||
|
left: 20,
|
||||||
|
bottom: 20,
|
||||||
|
child: VirtualJoystick(
|
||||||
|
onMove: onMove,
|
||||||
|
size: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Action buttons (bottom right)
|
||||||
|
Positioned(
|
||||||
|
right: 20,
|
||||||
|
bottom: 80,
|
||||||
|
child: TouchButton(
|
||||||
|
onPressed: onPoopBag,
|
||||||
|
icon: Icons.eco, // Poop bag icon
|
||||||
|
size: 70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Positioned(
|
||||||
|
right: 20,
|
||||||
|
bottom: 20,
|
||||||
|
child: TouchButton(
|
||||||
|
onPressed: onDoorbell,
|
||||||
|
icon: Icons.notifications,
|
||||||
|
size: 70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Optional: escape timer display when escaping
|
||||||
|
// TODO: Add escape timer UI here
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility to detect if device supports touch
|
||||||
|
class TouchDetection {
|
||||||
|
static bool get isTouchDevice {
|
||||||
|
if (kIsWeb) {
|
||||||
|
// On web, assume no touch controls by default
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Platform.isAndroid || Platform.isIOS;
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback: assume no touch if platform detection fails
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
lib/ui/touch_controls_overlay.dart
Normal file
24
lib/ui/touch_controls_overlay.dart
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shitman/game/shitman_game.dart';
|
||||||
|
import 'package:shitman/ui/touch_controls.dart';
|
||||||
|
|
||||||
|
/// Touch controls overlay widget
|
||||||
|
class TouchControlsOverlayWidget extends StatelessWidget {
|
||||||
|
static const String overlayID = 'TouchControls';
|
||||||
|
|
||||||
|
const TouchControlsOverlayWidget({
|
||||||
|
super.key,
|
||||||
|
required this.game,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ShitmanGame game;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TouchControlsOverlay(
|
||||||
|
onMove: (input) => game.handleTouchMovement(input),
|
||||||
|
onPoopBag: () => game.handleTouchPoopBag(),
|
||||||
|
onDoorbell: () => game.handleTouchDoorbell(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -105,6 +105,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.2"
|
version: "0.0.2"
|
||||||
|
easy_shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: easy_shared_preferences
|
||||||
|
sha256: "47baab4cc8f48b85fa1a5c3613932b47c52449288ba16acdf52f01350aeb6555"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
equatable:
|
equatable:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -18,6 +18,7 @@ dependencies:
|
||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
meta: ^1.16.0
|
meta: ^1.16.0
|
||||||
logging: ^1.3.0
|
logging: ^1.3.0
|
||||||
|
easy_shared_preferences: ^1.0.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Add table
Reference in a new issue