feat: add comments
This commit is contained in:
parent
28d0fe7d8c
commit
e030b60c25
|
@ -4,6 +4,7 @@ class InputDirection {
|
||||||
bool left = false;
|
bool left = false;
|
||||||
bool right = false;
|
bool right = false;
|
||||||
|
|
||||||
|
/// reset all directions to false
|
||||||
void reset() {
|
void reset() {
|
||||||
up = false;
|
up = false;
|
||||||
down = false;
|
down = false;
|
||||||
|
|
|
@ -87,6 +87,7 @@ class _LevelState extends State<Level> with SingleTickerProviderStateMixin {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// go to GameOverStats when level finishes
|
||||||
player.onPlayerComplete.listen((void _) {
|
player.onPlayerComplete.listen((void _) {
|
||||||
Route route = MaterialPageRoute(
|
Route route = MaterialPageRoute(
|
||||||
builder: (context) => GameOverStats(
|
builder: (context) => GameOverStats(
|
||||||
|
@ -105,6 +106,7 @@ class _LevelState extends State<Level> with SingleTickerProviderStateMixin {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convert beats to notes
|
||||||
widget.simfile.chartSimplest?.beats.forEach((time, noteData) {
|
widget.simfile.chartSimplest?.beats.forEach((time, noteData) {
|
||||||
int arrowIndex = noteData.indexOf('1');
|
int arrowIndex = noteData.indexOf('1');
|
||||||
if (arrowIndex < 0 || arrowIndex > 3) {
|
if (arrowIndex < 0 || arrowIndex > 3) {
|
||||||
|
@ -126,6 +128,7 @@ class _LevelState extends State<Level> with SingleTickerProviderStateMixin {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// toggle between pause and resume
|
||||||
void _pauseResume() {
|
void _pauseResume() {
|
||||||
if (_isPlaying) {
|
if (_isPlaying) {
|
||||||
player.pause();
|
player.pause();
|
||||||
|
@ -140,18 +143,24 @@ class _LevelState extends State<Level> with SingleTickerProviderStateMixin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// checks if the [note] is hit on [time] with the correct InputDirection
|
||||||
void _noteHitCheck(Note note, Duration time) {
|
void _noteHitCheck(Note note, Duration time) {
|
||||||
note.position = note.time - time.inMilliseconds / 60000.0;
|
note.position = note.time - time.inMilliseconds / 60000.0;
|
||||||
if (note.wasHit != null) {
|
if (note.wasHit != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// you have +- half a second to hit
|
||||||
if (note.position.abs() < 0.5 * 1.0 / 60.0) {
|
if (note.position.abs() < 0.5 * 1.0 / 60.0) {
|
||||||
|
// combine keyboard and esense input
|
||||||
InputDirection esenseDirection =
|
InputDirection esenseDirection =
|
||||||
ESenseInput.instance.getInputDirection(note.direction);
|
ESenseInput.instance.getInputDirection(note.direction);
|
||||||
inputDirection.up |= esenseDirection.up;
|
inputDirection.up |= esenseDirection.up;
|
||||||
inputDirection.down |= esenseDirection.down;
|
inputDirection.down |= esenseDirection.down;
|
||||||
inputDirection.left |= esenseDirection.left;
|
inputDirection.left |= esenseDirection.left;
|
||||||
inputDirection.right |= esenseDirection.right;
|
inputDirection.right |= esenseDirection.right;
|
||||||
|
|
||||||
|
// check if input matches arrow direction
|
||||||
bool keypressCorrect = false;
|
bool keypressCorrect = false;
|
||||||
switch (note.direction) {
|
switch (note.direction) {
|
||||||
case ArrowDirection.up:
|
case ArrowDirection.up:
|
||||||
|
@ -189,6 +198,7 @@ class _LevelState extends State<Level> with SingleTickerProviderStateMixin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// sets the InputDirection based on the arrow keys
|
||||||
void _keyboardHandler(event) {
|
void _keyboardHandler(event) {
|
||||||
bool isDown = false;
|
bool isDown = false;
|
||||||
if (event is KeyDownEvent) {
|
if (event is KeyDownEvent) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ class _LevelSelectionState extends State<LevelSelection> {
|
||||||
loadFolderPath();
|
loadFolderPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// gets folder path from persistent storage and updates state with loaded simfiles
|
||||||
Future<void> loadFolderPath() async {
|
Future<void> loadFolderPath() async {
|
||||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
final String? stepmaniaCoursesPathSetting =
|
final String? stepmaniaCoursesPathSetting =
|
||||||
|
@ -44,6 +45,7 @@ class _LevelSelectionState extends State<LevelSelection> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// open folder selection dialog and save selected folder in persistent storage
|
||||||
Future<void> selectFolder() async {
|
Future<void> selectFolder() async {
|
||||||
await Permission.manageExternalStorage.request();
|
await Permission.manageExternalStorage.request();
|
||||||
String? selectedFolder = await FilePicker.platform.getDirectoryPath();
|
String? selectedFolder = await FilePicker.platform.getDirectoryPath();
|
||||||
|
@ -57,6 +59,7 @@ class _LevelSelectionState extends State<LevelSelection> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// load all simfiles from a [directoryPath]
|
||||||
Future<List<Simfile>> listFilesAndFolders(String directoryPath) async {
|
Future<List<Simfile>> listFilesAndFolders(String directoryPath) async {
|
||||||
final directory = Directory(directoryPath);
|
final directory = Directory(directoryPath);
|
||||||
try {
|
try {
|
||||||
|
@ -86,6 +89,7 @@ class _LevelSelectionState extends State<LevelSelection> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// filter stepmaniaCoursesFolders based on [input]
|
||||||
void filterLevels(String input) {
|
void filterLevels(String input) {
|
||||||
setState(() {
|
setState(() {
|
||||||
stepmaniaCoursesFoldersFiltered = stepmaniaCoursesFolders
|
stepmaniaCoursesFoldersFiltered = stepmaniaCoursesFolders
|
||||||
|
|
|
@ -8,9 +8,12 @@ import 'package:sense_the_rhythm/models/arrow_direction.dart';
|
||||||
import 'package:sense_the_rhythm/models/input_direction.dart';
|
import 'package:sense_the_rhythm/models/input_direction.dart';
|
||||||
|
|
||||||
class ESenseInput {
|
class ESenseInput {
|
||||||
|
// create singleton that is available on all widgets so it does not have to be
|
||||||
|
// carried down in the widget tree
|
||||||
static final instance = ESenseInput._();
|
static final instance = ESenseInput._();
|
||||||
|
|
||||||
ESenseManager eSenseManager = ESenseManager('unknown');
|
ESenseManager eSenseManager = ESenseManager('unknown');
|
||||||
|
// valuenotifier allows widgets to rerender when the value changes
|
||||||
ValueNotifier<String> deviceStatus = ValueNotifier('Disconnected');
|
ValueNotifier<String> deviceStatus = ValueNotifier('Disconnected');
|
||||||
StreamSubscription? subscription;
|
StreamSubscription? subscription;
|
||||||
|
|
||||||
|
@ -29,8 +32,11 @@ class ESenseInput {
|
||||||
_listenToESense();
|
_listenToESense();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ask and check if permissions are enabled and granted
|
||||||
Future<bool> _askForPermissions() async {
|
Future<bool> _askForPermissions() async {
|
||||||
|
// is desktop
|
||||||
if (!Platform.isAndroid && !Platform.isIOS) return false;
|
if (!Platform.isAndroid && !Platform.isIOS) return false;
|
||||||
|
// is bluetooth even enabled?
|
||||||
if (!await Permission.bluetooth.serviceStatus.isEnabled) {
|
if (!await Permission.bluetooth.serviceStatus.isEnabled) {
|
||||||
deviceStatus.value = "Bluetooth is disabled!";
|
deviceStatus.value = "Bluetooth is disabled!";
|
||||||
return false;
|
return false;
|
||||||
|
@ -55,6 +61,7 @@ class ESenseInput {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// listen to connectionEvents and set deviceStatus
|
||||||
void _listenToESense() {
|
void _listenToESense() {
|
||||||
// if you want to get the connection events when connecting,
|
// if you want to get the connection events when connecting,
|
||||||
// set up the listener BEFORE connecting...
|
// set up the listener BEFORE connecting...
|
||||||
|
@ -89,12 +96,14 @@ class ESenseInput {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// get eSenseEvent stream only containung button events
|
||||||
Stream<ButtonEventChanged> buttonEvents() {
|
Stream<ButtonEventChanged> buttonEvents() {
|
||||||
return eSenseManager.eSenseEvents
|
return eSenseManager.eSenseEvents
|
||||||
.where((event) => event.runtimeType == ButtonEventChanged)
|
.where((event) => event.runtimeType == ButtonEventChanged)
|
||||||
.cast();
|
.cast();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// sets sampling rate and listens to sensorEvents
|
||||||
void _startListenToSensorEvents() async {
|
void _startListenToSensorEvents() async {
|
||||||
// // any changes to the sampling frequency must be done BEFORE listening to sensor events
|
// // any changes to the sampling frequency must be done BEFORE listening to sensor events
|
||||||
print('setting sampling frequency...');
|
print('setting sampling frequency...');
|
||||||
|
@ -115,11 +124,14 @@ class ESenseInput {
|
||||||
sampling = true;
|
sampling = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// cancels the sensorEvents listening
|
||||||
void _pauseListenToSensorEvents() async {
|
void _pauseListenToSensorEvents() async {
|
||||||
subscription?.cancel();
|
subscription?.cancel();
|
||||||
sampling = false;
|
sampling = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// add up all new gyro [data] in the form of deg/s multiplied by scaling factor
|
||||||
|
/// to get real angles
|
||||||
void _parseGyroData(List<int> data) {
|
void _parseGyroData(List<int> data) {
|
||||||
// Float value in deg/s = Gyro value / Gyro scale factor
|
// Float value in deg/s = Gyro value / Gyro scale factor
|
||||||
// The default configuration is +- 500deg/s for the gyroscope.
|
// The default configuration is +- 500deg/s for the gyroscope.
|
||||||
|
@ -131,6 +143,7 @@ class ESenseInput {
|
||||||
// print('${z.toDouble() / 500.0 * (1.0 / 10.0)}');
|
// print('${z.toDouble() / 500.0 * (1.0 / 10.0)}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// nulls all angles and reset inputDirection
|
||||||
void resetAngles() {
|
void resetAngles() {
|
||||||
inputDirection.reset();
|
inputDirection.reset();
|
||||||
x = 0;
|
x = 0;
|
||||||
|
@ -138,26 +151,29 @@ class ESenseInput {
|
||||||
z = 0;
|
z = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// get InputDirection by checking if angels are in defined ranges and
|
||||||
|
/// calibrating based on the [expect]ed direction from ArrowDirection
|
||||||
InputDirection getInputDirection(ArrowDirection expect) {
|
InputDirection getInputDirection(ArrowDirection expect) {
|
||||||
|
// check if angle is in range
|
||||||
inputDirection.up = z > 180 && z < 340;
|
inputDirection.up = z > 180 && z < 340;
|
||||||
inputDirection.down = z > 20 && z < 180;
|
inputDirection.down = z > 20 && z < 180;
|
||||||
inputDirection.left = y > 0 && y < 180;
|
inputDirection.left = y > 0 && y < 180;
|
||||||
inputDirection.right = y > 180 && y < 360;
|
inputDirection.right = y > 180 && y < 360;
|
||||||
|
|
||||||
|
// calibrate based on expected directoin from ArrowDirection
|
||||||
if (expect == ArrowDirection.up && inputDirection.up ||
|
if (expect == ArrowDirection.up && inputDirection.up ||
|
||||||
expect == ArrowDirection.down && inputDirection.down) {
|
expect == ArrowDirection.down && inputDirection.down) {
|
||||||
y = 0;
|
y = 0;
|
||||||
print("ehit");
|
|
||||||
}
|
}
|
||||||
if (expect == ArrowDirection.left && inputDirection.left ||
|
if (expect == ArrowDirection.left && inputDirection.left ||
|
||||||
expect == ArrowDirection.right && inputDirection.right) {
|
expect == ArrowDirection.right && inputDirection.right) {
|
||||||
z = 0;
|
z = 0;
|
||||||
print("ehit");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return inputDirection;
|
return inputDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// connect to ESense with [deviceName] by first asking for permissions
|
||||||
Future<void> connectToESense(String deviceName) async {
|
Future<void> connectToESense(String deviceName) async {
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
bool permissionSuccessfull = await _askForPermissions();
|
bool permissionSuccessfull = await _askForPermissions();
|
||||||
|
|
|
@ -55,6 +55,7 @@ class Simfile {
|
||||||
|
|
||||||
Simfile(this.simfilePath);
|
Simfile(this.simfilePath);
|
||||||
|
|
||||||
|
/// parses a chart tag with metadata [keys] and note data [value]
|
||||||
void _parseChart({required List<String> keys, required String value}) {
|
void _parseChart({required List<String> keys, required String value}) {
|
||||||
Chart chart = Chart();
|
Chart chart = Chart();
|
||||||
chart.chartType = keys[1];
|
chart.chartType = keys[1];
|
||||||
|
@ -63,6 +64,7 @@ class Simfile {
|
||||||
chart.numericalMeter = int.parse(keys[4]);
|
chart.numericalMeter = int.parse(keys[4]);
|
||||||
chart.radarValues = keys[5];
|
chart.radarValues = keys[5];
|
||||||
|
|
||||||
|
// find simplest chart
|
||||||
if (chartSimplest == null ||
|
if (chartSimplest == null ||
|
||||||
(chart.difficulty!.index <= chartSimplest!.difficulty!.index &&
|
(chart.difficulty!.index <= chartSimplest!.difficulty!.index &&
|
||||||
chart.numericalMeter! <= chartSimplest!.numericalMeter!)) {
|
chart.numericalMeter! <= chartSimplest!.numericalMeter!)) {
|
||||||
|
@ -78,8 +80,10 @@ class Simfile {
|
||||||
measures.add(measure);
|
measures.add(measure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for now only use the first bpm value
|
||||||
double bpm = bpms.entries.first.value;
|
double bpm = bpms.entries.first.value;
|
||||||
|
|
||||||
|
// calculate timing for all notes based on offset, bpm and measure
|
||||||
for (final (measureIndex, measure) in measures.indexed) {
|
for (final (measureIndex, measure) in measures.indexed) {
|
||||||
for (final (noteIndex, noteData) in measure.indexed) {
|
for (final (noteIndex, noteData) in measure.indexed) {
|
||||||
double beat = measureIndex * 4.0 +
|
double beat = measureIndex * 4.0 +
|
||||||
|
@ -95,10 +99,13 @@ class Simfile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// parse a tag based on a regex match [fieldData] and parsing the value based
|
||||||
|
/// on the key
|
||||||
void _parseTag(RegExpMatch fieldData) {
|
void _parseTag(RegExpMatch fieldData) {
|
||||||
List<String> keys =
|
List<String> keys =
|
||||||
fieldData[1]!.split(':').map((key) => key.trim()).toList();
|
fieldData[1]!.split(':').map((key) => key.trim()).toList();
|
||||||
String value = fieldData[2]!;
|
String value = fieldData[2]!;
|
||||||
|
|
||||||
if (keys[0] == "BPMS") {
|
if (keys[0] == "BPMS") {
|
||||||
for (final pairRaw in value.split(',')) {
|
for (final pairRaw in value.split(',')) {
|
||||||
List<String> pair = pairRaw.split('=');
|
List<String> pair = pairRaw.split('=');
|
||||||
|
@ -122,14 +129,18 @@ class Simfile {
|
||||||
_parseChart(keys: keys, value: value);
|
_parseChart(keys: keys, value: value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// load the simfile
|
||||||
Future<bool> load() async {
|
Future<bool> load() async {
|
||||||
directoryPath = File(simfilePath).parent.path;
|
directoryPath = File(simfilePath).parent.path;
|
||||||
lines = File(simfilePath).readAsStringSync();
|
lines = File(simfilePath).readAsStringSync();
|
||||||
|
|
||||||
|
// remove comments
|
||||||
RegExp commentsRegExp = RegExp(r'//.*$');
|
RegExp commentsRegExp = RegExp(r'//.*$');
|
||||||
lines = lines?.replaceAll(commentsRegExp, '');
|
lines = lines?.replaceAll(commentsRegExp, '');
|
||||||
|
// find all tags
|
||||||
RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);');
|
RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);');
|
||||||
|
|
||||||
|
// parse all tags
|
||||||
for (final fieldData in fieldDataRegExp.allMatches(lines!)) {
|
for (final fieldData in fieldDataRegExp.allMatches(lines!)) {
|
||||||
try {
|
try {
|
||||||
_parseTag(fieldData);
|
_parseTag(fieldData);
|
||||||
|
@ -138,11 +149,8 @@ class Simfile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String? musicFileName = tags["MUSIC"];
|
// searching for audio and banned in the directory is more robust than using
|
||||||
if (musicFileName == null) return false;
|
// values from metadata as they are wrong more often
|
||||||
String? bannerFileName = tags["BANNER"];
|
|
||||||
if (bannerFileName == null) return false;
|
|
||||||
|
|
||||||
for (FileSystemEntity entity in Directory(directoryPath!).listSync()) {
|
for (FileSystemEntity entity in Directory(directoryPath!).listSync()) {
|
||||||
if (entity.path.endsWith('.ogg')) {
|
if (entity.path.endsWith('.ogg')) {
|
||||||
audioPath = entity.path;
|
audioPath = entity.path;
|
||||||
|
@ -152,6 +160,11 @@ class Simfile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dont use this simfile of files are missing
|
||||||
|
if (audioPath == null) return false;
|
||||||
|
if (bannerPath == null) return false;
|
||||||
|
|
||||||
|
// get duration from audio
|
||||||
AudioPlayer audioplayer = AudioPlayer();
|
AudioPlayer audioplayer = AudioPlayer();
|
||||||
await audioplayer.setSource(DeviceFileSource(audioPath!));
|
await audioplayer.setSource(DeviceFileSource(audioPath!));
|
||||||
duration = await audioplayer.getDuration();
|
duration = await audioplayer.getDuration();
|
||||||
|
|
|
@ -21,6 +21,7 @@ class _ESenseConnectDialogState extends State<ESenseConnectDialog> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// rerender whenever the deviceStatus changes
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
valueListenable: widget.deviceStatus,
|
valueListenable: widget.deviceStatus,
|
||||||
builder: (BuildContext context, String deviceStatus, Widget? child) {
|
builder: (BuildContext context, String deviceStatus, Widget? child) {
|
||||||
|
|
|
@ -16,11 +16,13 @@ class LevelListEntry extends StatelessWidget {
|
||||||
|
|
||||||
final Simfile simfile;
|
final Simfile simfile;
|
||||||
|
|
||||||
|
/// navigates to level screen
|
||||||
void navigateToLevel(BuildContext context) {
|
void navigateToLevel(BuildContext context) {
|
||||||
Navigator.push(context,
|
Navigator.push(context,
|
||||||
MaterialPageRoute(builder: (BuildContext context) => Level(simfile)));
|
MaterialPageRoute(builder: (BuildContext context) => Level(simfile)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// opens ESenseConnectDialog
|
||||||
void openESenseConnectDialog(context) {
|
void openESenseConnectDialog(context) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -38,6 +40,7 @@ class LevelListEntry extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// when clocked on the level, warn if not connected to ESense
|
||||||
void tapHandler(BuildContext context) {
|
void tapHandler(BuildContext context) {
|
||||||
if (ESenseInput.instance.connected) {
|
if (ESenseInput.instance.connected) {
|
||||||
navigateToLevel(context);
|
navigateToLevel(context);
|
||||||
|
|
Loading…
Reference in a new issue