286 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			286 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| 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)),
 | |
|               ),
 | |
|             ),
 | |
|           ])),
 | |
|     );
 | |
|   }
 | |
| }
 |