diff options
Diffstat (limited to 'lib/screens/level.dart')
-rw-r--r-- | lib/screens/level.dart | 285 |
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)), + ), + ), + ])), + ); + } +} |