summaryrefslogtreecommitdiff
path: root/lib/screens/level.dart
diff options
context:
space:
mode:
Diffstat (limited to 'lib/screens/level.dart')
-rw-r--r--lib/screens/level.dart285
1 files changed, 285 insertions, 0 deletions
diff --git a/lib/screens/level.dart b/lib/screens/level.dart
new file mode 100644
index 0000000..000c43c
--- /dev/null
+++ b/lib/screens/level.dart
@@ -0,0 +1,285 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:audioplayers/audioplayers.dart';
+import 'package:flutter/services.dart';
+import 'package:sense_the_rhythm/models/arrow_direction.dart';
+import 'package:sense_the_rhythm/models/input_direction.dart';
+import 'package:sense_the_rhythm/models/note.dart';
+import 'package:sense_the_rhythm/utils/esense_input.dart';
+import 'package:sense_the_rhythm/utils/simfile.dart';
+import 'package:sense_the_rhythm/widgets/arrows.dart';
+import 'package:sense_the_rhythm/screens/game_over.dart';
+
+class Level extends StatefulWidget {
+ const Level(this.simfile, {super.key});
+ final Simfile simfile;
+
+ @override
+ State<Level> createState() => _LevelState();
+}
+
+class _LevelState extends State<Level> with SingleTickerProviderStateMixin {
+ final _player = AudioPlayer();
+ bool _isPlaying = true;
+ Duration? _duration;
+ Duration? _position;
+
+ StreamSubscription? _durationSubscription;
+ StreamSubscription? _positionSubscription;
+ StreamSubscription? _buttonSubscription;
+
+ final FocusNode _focusNode = FocusNode();
+ final InputDirection _inputDirection = InputDirection();
+
+ String _hitOrMissMessage = 'Play!';
+
+ final List<Note> _notes = [];
+
+ late AnimationController _animationController;
+ late Animation<double> _animation;
+
+ @override
+ void setState(VoidCallback fn) {
+ // Subscriptions only can be closed asynchronously,
+ // therefore events can occur after widget has been disposed.
+ if (mounted) {
+ super.setState(fn);
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ ESenseInput.instance.resetAngles();
+
+ _animationController = AnimationController(
+ vsync: this,
+ duration: Duration(seconds: 2),
+ );
+ _animation =
+ Tween<double>(begin: 1.0, end: 0.0).animate(_animationController);
+ _animationController.forward();
+
+ // Use initial values from player
+ _player.getDuration().then(
+ (value) => setState(() {
+ _duration = value;
+ }),
+ );
+ _player.getCurrentPosition().then(
+ (value) => setState(() {
+ _position = value;
+ }),
+ );
+
+ // listen for new values from player
+ _durationSubscription =
+ _player.onDurationChanged.listen((Duration duration) {
+ setState(() => _duration = duration);
+ });
+
+ _positionSubscription =
+ _player.onPositionChanged.listen((Duration position) {
+ setState(() => _position = position);
+ for (final note in _notes) {
+ _noteHitCheck(note, position);
+ }
+ });
+
+ // go to GameOverStats when level finishes
+ _player.onPlayerComplete.listen((void _) {
+ Route route = MaterialPageRoute(
+ builder: (context) => GameOverStats(
+ simfile: widget.simfile,
+ notes: _notes,
+ ));
+ Navigator.pushReplacement(context, route);
+ });
+
+ // listen for esense button and pause/resume
+ if (ESenseInput.instance.connected) {
+ _buttonSubscription = ESenseInput.instance.buttonEvents().listen((event) {
+ if (!event.pressed) {
+ _pauseResume();
+ }
+ });
+ }
+
+ // convert beats to notes
+ widget.simfile.chartSimplest?.beats.forEach((time, noteData) {
+ int arrowIndex = noteData.indexOf('1');
+ if (arrowIndex < 0 || arrowIndex > 3) {
+ return;
+ }
+ _notes.add(Note(time: time, direction: ArrowDirection.values[arrowIndex]));
+ });
+
+ _player.play(DeviceFileSource(widget.simfile.audioPath!));
+ }
+
+ @override
+ void dispose() {
+ _animationController.dispose();
+ _durationSubscription?.cancel();
+ _positionSubscription?.cancel();
+ _buttonSubscription?.cancel();
+ _player.dispose();
+ super.dispose();
+ }
+
+ /// toggle between pause and resume
+ void _pauseResume() {
+ if (_isPlaying) {
+ _player.pause();
+ setState(() {
+ _isPlaying = false;
+ });
+ } else {
+ _player.resume();
+ setState(() {
+ _isPlaying = true;
+ });
+ }
+ }
+
+ /// checks if the [note] is hit on [time] with the correct InputDirection
+ void _noteHitCheck(Note note, Duration time) {
+ note.position = note.time - time.inMilliseconds / 60000.0;
+ if (note.wasHit != null) {
+ return;
+ }
+
+ // you have +- half a second to hit
+ if (note.position.abs() < 0.5 * 1.0 / 60.0) {
+ // combine keyboard and esense input
+ InputDirection esenseDirection =
+ ESenseInput.instance.getInputDirection(note.direction);
+ _inputDirection.up |= esenseDirection.up;
+ _inputDirection.down |= esenseDirection.down;
+ _inputDirection.left |= esenseDirection.left;
+ _inputDirection.right |= esenseDirection.right;
+
+ // check if input matches arrow direction
+ bool keypressCorrect = false;
+ switch (note.direction) {
+ case ArrowDirection.up:
+ keypressCorrect = _inputDirection.up;
+ break;
+ case ArrowDirection.down:
+ keypressCorrect = _inputDirection.down;
+ break;
+ case ArrowDirection.right:
+ keypressCorrect = _inputDirection.right;
+ break;
+ case ArrowDirection.left:
+ keypressCorrect = _inputDirection.left;
+ break;
+ }
+ if (keypressCorrect) {
+ print("you hit!");
+ note.wasHit = true;
+ _animationController.reset();
+ _animationController.forward();
+ _inputDirection.reset();
+ setState(() {
+ _hitOrMissMessage = 'Great!';
+ });
+ }
+ } else if (note.position < -0.5 * 1.0 / 60.0) {
+ print("Missed");
+ note.wasHit = false;
+ _animationController.reset();
+ _animationController.forward();
+ _inputDirection.reset();
+ setState(() {
+ _hitOrMissMessage = 'Missed';
+ });
+ }
+ }
+
+ /// sets the InputDirection based on the arrow keys
+ void _keyboardHandler(event) {
+ bool isDown = false;
+ if (event is KeyDownEvent) {
+ isDown = true;
+ } else if (event is KeyUpEvent) {
+ isDown = false;
+ } else {
+ return;
+ }
+ switch (event.logicalKey) {
+ case LogicalKeyboardKey.arrowUp:
+ _inputDirection.up = isDown;
+ break;
+ case LogicalKeyboardKey.arrowDown:
+ _inputDirection.down = isDown;
+ break;
+ case LogicalKeyboardKey.arrowLeft:
+ _inputDirection.left = isDown;
+ break;
+ case LogicalKeyboardKey.arrowRight:
+ _inputDirection.right = isDown;
+ break;
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return KeyboardListener(
+ focusNode: _focusNode,
+ autofocus: true,
+ onKeyEvent: _keyboardHandler,
+ child: Scaffold(
+ appBar: AppBar(
+ leading: IconButton(
+ icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
+ onPressed: _pauseResume,
+ ),
+ title: Text(widget.simfile.tags['TITLE']!),
+ actions: [
+ IconButton(
+ icon: Icon(Icons.close),
+ onPressed: () => Navigator.pop(context))
+ ],
+ bottom: PreferredSize(
+ preferredSize: Size(double.infinity, 1.0),
+ child: LinearProgressIndicator(
+ value: (_duration != null &&
+ _position != null &&
+ _position!.inMilliseconds > 0 &&
+ _position!.inMilliseconds < _duration!.inMilliseconds)
+ ? _position!.inMilliseconds / _duration!.inMilliseconds
+ : 0.0,
+ )),
+ ),
+ body: Stack(children: [
+ Arrows(notes: _notes),
+ Positioned(
+ top: 50,
+ width: MediaQuery.of(context).size.width,
+ left: 0,
+ child: FadeTransition(
+ opacity: _animation,
+ child: Text(
+ _hitOrMissMessage,
+ textScaler: TextScaler.linear(4),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ),
+ Positioned(
+ left: MediaQuery.of(context).size.width / 2 - 50,
+ bottom: 50,
+ child: Container(
+ width: 100,
+ height: 100,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.black, width: 10)),
+ ),
+ ),
+ ])),
+ );
+ }
+}