level.dart (8494B)
1 import 'dart:async'; 2 3 import 'package:flutter/material.dart'; 4 import 'package:audioplayers/audioplayers.dart'; 5 import 'package:flutter/services.dart'; 6 import 'package:sense_the_rhythm/models/arrow_direction.dart'; 7 import 'package:sense_the_rhythm/models/input_direction.dart'; 8 import 'package:sense_the_rhythm/models/note.dart'; 9 import 'package:sense_the_rhythm/utils/esense_input.dart'; 10 import 'package:sense_the_rhythm/utils/simfile.dart'; 11 import 'package:sense_the_rhythm/widgets/arrows.dart'; 12 import 'package:sense_the_rhythm/screens/game_over.dart'; 13 14 class Level extends StatefulWidget { 15 const Level(this.simfile, {super.key}); 16 final Simfile simfile; 17 18 @override 19 State<Level> createState() => _LevelState(); 20 } 21 22 class _LevelState extends State<Level> with SingleTickerProviderStateMixin { 23 final _player = AudioPlayer(); 24 bool _isPlaying = true; 25 Duration? _duration; 26 Duration? _position; 27 28 StreamSubscription? _durationSubscription; 29 StreamSubscription? _positionSubscription; 30 StreamSubscription? _buttonSubscription; 31 32 final FocusNode _focusNode = FocusNode(); 33 final InputDirection _inputDirection = InputDirection(); 34 35 String _hitOrMissMessage = 'Play!'; 36 37 final List<Note> _notes = []; 38 39 late AnimationController _animationController; 40 late Animation<double> _animation; 41 42 @override 43 void setState(VoidCallback fn) { 44 // Subscriptions only can be closed asynchronously, 45 // therefore events can occur after widget has been disposed. 46 if (mounted) { 47 super.setState(fn); 48 } 49 } 50 51 @override 52 void initState() { 53 super.initState(); 54 ESenseInput.instance.resetAngles(); 55 56 _animationController = AnimationController( 57 vsync: this, 58 duration: Duration(seconds: 2), 59 ); 60 _animation = 61 Tween<double>(begin: 1.0, end: 0.0).animate(_animationController); 62 _animationController.forward(); 63 64 // Use initial values from player 65 _player.getDuration().then( 66 (value) => setState(() { 67 _duration = value; 68 }), 69 ); 70 _player.getCurrentPosition().then( 71 (value) => setState(() { 72 _position = value; 73 }), 74 ); 75 76 // listen for new values from player 77 _durationSubscription = 78 _player.onDurationChanged.listen((Duration duration) { 79 setState(() => _duration = duration); 80 }); 81 82 _positionSubscription = 83 _player.onPositionChanged.listen((Duration position) { 84 setState(() => _position = position); 85 for (final note in _notes) { 86 _noteHitCheck(note, position); 87 } 88 }); 89 90 // go to GameOverStats when level finishes 91 _player.onPlayerComplete.listen((void _) { 92 Route route = MaterialPageRoute( 93 builder: (context) => GameOverStats( 94 simfile: widget.simfile, 95 notes: _notes, 96 )); 97 Navigator.pushReplacement(context, route); 98 }); 99 100 // listen for esense button and pause/resume 101 if (ESenseInput.instance.connected) { 102 _buttonSubscription = ESenseInput.instance.buttonEvents().listen((event) { 103 if (!event.pressed) { 104 _pauseResume(); 105 } 106 }); 107 } 108 109 // convert beats to notes 110 widget.simfile.chartSimplest?.beats.forEach((time, noteData) { 111 int arrowIndex = noteData.indexOf('1'); 112 if (arrowIndex < 0 || arrowIndex > 3) { 113 return; 114 } 115 _notes.add(Note(time: time, direction: ArrowDirection.values[arrowIndex])); 116 }); 117 118 _player.play(DeviceFileSource(widget.simfile.audioPath!)); 119 } 120 121 @override 122 void dispose() { 123 _animationController.dispose(); 124 _durationSubscription?.cancel(); 125 _positionSubscription?.cancel(); 126 _buttonSubscription?.cancel(); 127 _player.dispose(); 128 super.dispose(); 129 } 130 131 /// toggle between pause and resume 132 void _pauseResume() { 133 if (_isPlaying) { 134 _player.pause(); 135 setState(() { 136 _isPlaying = false; 137 }); 138 } else { 139 _player.resume(); 140 setState(() { 141 _isPlaying = true; 142 }); 143 } 144 } 145 146 /// checks if the [note] is hit on [time] with the correct InputDirection 147 void _noteHitCheck(Note note, Duration time) { 148 note.position = note.time - time.inMilliseconds / 60000.0; 149 if (note.wasHit != null) { 150 return; 151 } 152 153 // you have +- half a second to hit 154 if (note.position.abs() < 0.5 * 1.0 / 60.0) { 155 // combine keyboard and esense input 156 InputDirection esenseDirection = 157 ESenseInput.instance.getInputDirection(note.direction); 158 _inputDirection.up |= esenseDirection.up; 159 _inputDirection.down |= esenseDirection.down; 160 _inputDirection.left |= esenseDirection.left; 161 _inputDirection.right |= esenseDirection.right; 162 163 // check if input matches arrow direction 164 bool keypressCorrect = false; 165 switch (note.direction) { 166 case ArrowDirection.up: 167 keypressCorrect = _inputDirection.up; 168 break; 169 case ArrowDirection.down: 170 keypressCorrect = _inputDirection.down; 171 break; 172 case ArrowDirection.right: 173 keypressCorrect = _inputDirection.right; 174 break; 175 case ArrowDirection.left: 176 keypressCorrect = _inputDirection.left; 177 break; 178 } 179 if (keypressCorrect) { 180 print("you hit!"); 181 note.wasHit = true; 182 _animationController.reset(); 183 _animationController.forward(); 184 _inputDirection.reset(); 185 setState(() { 186 _hitOrMissMessage = 'Great!'; 187 }); 188 } 189 } else if (note.position < -0.5 * 1.0 / 60.0) { 190 print("Missed"); 191 note.wasHit = false; 192 _animationController.reset(); 193 _animationController.forward(); 194 _inputDirection.reset(); 195 setState(() { 196 _hitOrMissMessage = 'Missed'; 197 }); 198 } 199 } 200 201 /// sets the InputDirection based on the arrow keys 202 void _keyboardHandler(event) { 203 bool isDown = false; 204 if (event is KeyDownEvent) { 205 isDown = true; 206 } else if (event is KeyUpEvent) { 207 isDown = false; 208 } else { 209 return; 210 } 211 switch (event.logicalKey) { 212 case LogicalKeyboardKey.arrowUp: 213 _inputDirection.up = isDown; 214 break; 215 case LogicalKeyboardKey.arrowDown: 216 _inputDirection.down = isDown; 217 break; 218 case LogicalKeyboardKey.arrowLeft: 219 _inputDirection.left = isDown; 220 break; 221 case LogicalKeyboardKey.arrowRight: 222 _inputDirection.right = isDown; 223 break; 224 } 225 } 226 227 @override 228 Widget build(BuildContext context) { 229 return KeyboardListener( 230 focusNode: _focusNode, 231 autofocus: true, 232 onKeyEvent: _keyboardHandler, 233 child: Scaffold( 234 appBar: AppBar( 235 leading: IconButton( 236 icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow), 237 onPressed: _pauseResume, 238 ), 239 title: Text(widget.simfile.tags['TITLE']!), 240 actions: [ 241 IconButton( 242 icon: Icon(Icons.close), 243 onPressed: () => Navigator.pop(context)) 244 ], 245 bottom: PreferredSize( 246 preferredSize: Size(double.infinity, 1.0), 247 child: LinearProgressIndicator( 248 value: (_duration != null && 249 _position != null && 250 _position!.inMilliseconds > 0 && 251 _position!.inMilliseconds < _duration!.inMilliseconds) 252 ? _position!.inMilliseconds / _duration!.inMilliseconds 253 : 0.0, 254 )), 255 ), 256 body: Stack(children: [ 257 Arrows(notes: _notes), 258 Positioned( 259 top: 50, 260 width: MediaQuery.of(context).size.width, 261 left: 0, 262 child: FadeTransition( 263 opacity: _animation, 264 child: Text( 265 _hitOrMissMessage, 266 textScaler: TextScaler.linear(4), 267 textAlign: TextAlign.center, 268 ), 269 ), 270 ), 271 Positioned( 272 left: MediaQuery.of(context).size.width / 2 - 50, 273 bottom: 50, 274 child: Container( 275 width: 100, 276 height: 100, 277 decoration: BoxDecoration( 278 shape: BoxShape.circle, 279 border: Border.all(color: Colors.black, width: 10)), 280 ), 281 ), 282 ])), 283 ); 284 } 285 }