Sense_the_Rhythm/lib/screens/level.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)),
),
),
])),
);
}
}