shitman/lib/ui/touch_controls.dart
zeyus f7a08a5099
All checks were successful
/ build-web (push) Successful in 3m24s
Added touch controls.
2025-08-04 11:50:47 +02:00

265 lines
No EOL
6.8 KiB
Dart

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