265 lines
No EOL
6.8 KiB
Dart
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;
|
|
}
|
|
}
|
|
} |