package com.fteams.siftrain.controller;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.utils.Array;
import com.fteams.siftrain.World;
import com.fteams.siftrain.assets.Assets;
import com.fteams.siftrain.assets.GlobalConfiguration;
import com.fteams.siftrain.assets.Results;
import com.fteams.siftrain.objects.AccuracyMarker;
import com.fteams.siftrain.objects.AccuracyPopup;
import com.fteams.siftrain.objects.CircleMark;
import com.fteams.siftrain.objects.TapZone;
import com.fteams.siftrain.screens.ResultsScreen;
import com.fteams.siftrain.util.SongUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class WorldController implements Music.OnCompletionListener {
private World world;
private final Array<CircleMark> marks;
private final Array<TapZone> tapZones;
// osu! style accuracy marker on top
private final Array<AccuracyMarker> accuracyMarkers;
// the text on screen "perfect", ..., "miss"
private final Array<AccuracyPopup> accuracyPopups;
public boolean done;
private boolean hasMusic;
public int combo;
private int badCount;
private int goodCount;
private int greatCount;
private int perfectCount;
private int missCount;
private int largestCombo;
private List<CircleMark.Accuracy> accuracyList;
Map<Integer, Integer> pointerToZoneId = new HashMap<>();
private boolean leftMark;
Float aPosition;
Float bPosition;
float songStart;
boolean songStarted;
boolean isABRepeatMode;
private Music theSong;
private Integer syncMode;
private boolean rewinding;
public WorldController(World world) {
this.world = world;
this.marks = world.getMarks();
this.tapZones = world.getZones();
this.accuracyMarkers = world.getAccuracyMarkers();
this.combo = 0;
this.badCount = 0;
this.goodCount = 0;
this.greatCount = 0;
this.perfectCount = 0;
this.missCount = 0;
this.largestCombo = 0;
this.accuracyList = new ArrayList<>();
this.accuracyPopups = world.getAccuracyPopups();
this.leftMark = true;
this.songStart = world.delay;
this.songStarted = false;
this.mtime = 0f;
this.lastmtime = 0f;
this.time = 0f;
this.oldTime = 0f;
this.timeSyncAcc = 0f;
this.syncMode = GlobalConfiguration.syncMode;
this.isABRepeatMode = GlobalConfiguration.playbackMode != null && GlobalConfiguration.playbackMode.equals(SongUtils.GAME_MODE_ABREPEAT);
if (GlobalConfiguration.playbackRate == null || GlobalConfiguration.playbackRate.compareTo(1.0f) == 0) {
theSong = SongLoader.loadSongFile();
}
if (isABRepeatMode) {
aPosition = GlobalConfiguration.aTime;
// set a buffer of 3 seconds previous to the fragment we're practicing
aPosition = aPosition - 3f < 0.0f ? 0.0f : aPosition - 3f;
bPosition = GlobalConfiguration.bTime;
if (GlobalConfiguration.playbackRate != null && GlobalConfiguration.playbackRate.compareTo(1.0f) != 0)
aPosition = aPosition / GlobalConfiguration.playbackRate;
if (GlobalConfiguration.playbackRate != null && GlobalConfiguration.playbackRate.compareTo(1.0f) != 0)
bPosition = bPosition / GlobalConfiguration.playbackRate;
time = aPosition;
}
this.hasMusic = theSong != null;
this.rewinding = false;
}
private void resetMarks() {
this.rewinding = true;
for (CircleMark circle : marks) {
circle.reset();
}
this.combo = 0;
this.badCount = 0;
this.goodCount = 0;
this.greatCount = 0;
this.perfectCount = 0;
this.missCount = 0;
this.largestCombo = 0;
this.timeSyncAcc = 0f;
accuracyMarkers.clear();
accuracyPopups.clear();
this.rewinding = false;
}
@Override
public void onCompletion(Music music) {
if (isABRepeatMode && !done) {
resetMarks();
if (hasMusic) {
theSong.pause();
theSong.setPosition(aPosition);
theSong.play();
lastmtime = theSong.getPosition();
time = lastmtime + world.delay;
timeSyncAcc = 0;
} else {
time = aPosition;
}
return;
}
if (hasMusic) {
music.dispose();
}
done = true;
if (this.largestCombo < this.combo) {
this.largestCombo = combo;
}
Results.bads = badCount;
Results.goods = goodCount;
Results.greats = greatCount;
Results.perfects = perfectCount;
Results.miss = missCount;
Results.combo = largestCombo;
Results.accuracy = calculateAccuracy();
Results.normalizedAccuracy = calculateNormalizedAccuracy();
accuracyMarkers.clear();
accuracyPopups.clear();
marks.clear();
tapZones.clear();
((Game) Gdx.app.getApplicationListener()).setScreen(new ResultsScreen());
}
private int countType(Array<CircleMark> marks, Integer effect) {
int sum = 0;
for (CircleMark mark : marks) {
if ((mark.effect & effect) != 0) {
sum++;
}
}
return sum;
}
private float calculateNormalizedAccuracy() {
if (accuracyList.size() == 0)
return 0f;
float sum = 0f;
for (CircleMark.Accuracy accuracy : accuracyList) {
sum += Results.getAccuracyMultiplierForAccuracy(accuracy);
}
return sum / accuracyList.size();
}
public float mtime;
public float time;
public float lastmtime;
public float oldTime;
public float timeSyncAcc;
public void update(float delta) {
if (!world.started)
return;
if (world.paused)
return;
if (rewinding)
return;
// some song files may start immediately and the beatmaps may have notes which start
// immediately with the songs, so give them a small lead-in to spawn the notes
if (!songStarted) {
songStart -= delta;
if (songStart <= 0) {
songStarted = true;
if (hasMusic) {
theSong.setLooping(false);
theSong.setOnCompletionListener(this);
theSong.setVolume(GlobalConfiguration.songVolume / 100f);
theSong.play();
if (aPosition != null)
theSong.setPosition(aPosition);
lastmtime = theSong.getPosition();
time = lastmtime + world.delay;
timeSyncAcc = 0;
} else {
if (aPosition != null) {
lastmtime = aPosition;
time = lastmtime + world.delay;
timeSyncAcc = 0;
}
}
}
}
// sync music and beatmap if there's music
sync(delta);
for (CircleMark mark : marks) {
mark.update(time);
}
for (AccuracyMarker marker : world.getAccuracyMarkers()) {
marker.update(delta);
}
for (AccuracyPopup popup : world.getAccuracyPopups()) {
popup.update(delta);
}
processInput();
}
private void sync(float delta) {
float theTime = time;
if (hasMusic) {
switch (syncMode) {
case 0: {
mtime = theSong.getPosition();
if (mtime <= 0f && !songStarted) {
time += delta;
// use the first 300 ms of the song to sync
} else if (songStarted && mtime < 0.3f) {
time = mtime + world.delay;
lastmtime = mtime;
// if we haven't synced in a while
} else if (timeSyncAcc > 0.5f) {
lastmtime = mtime;
time = mtime + world.delay;
timeSyncAcc = 0f;
// if the time didn't update we interpolate the delta
} else if (lastmtime == mtime) {
time += delta;
timeSyncAcc += delta;
// if the new reading is behind the previous one, we interpolate the delta
} else if (mtime < lastmtime) {
time = lastmtime + world.delay + delta;
lastmtime = lastmtime + delta;
timeSyncAcc += delta;
// if the new reading is way ahead, we interpolate the delta
} else if (mtime > oldTime + 2 * delta) {
time = lastmtime + world.delay + delta;
lastmtime = lastmtime + delta;
timeSyncAcc += delta;
} else {
lastmtime = mtime;
time = mtime + world.delay;
timeSyncAcc = 0f;
}
// smoothen transitions if the new time is ahead or behind of the time + delta
float theDiff = time - (theTime + delta);
time = theTime + delta + theDiff * 1 / Gdx.graphics.getFramesPerSecond();
break;
}
case 1: {
mtime = theSong.getPosition();
if (mtime <= lastmtime) {
time += delta;
} else {
time = mtime + world.delay;
}
break;
}
case 2: {
mtime = theSong.getPosition();
if (timeSyncAcc < 0.5f) {
if (mtime <= lastmtime) {
time += delta;
} else {
time = mtime + world.delay;
lastmtime = mtime;
}
} else {
time += delta;
}
timeSyncAcc += delta;
break;
}
default:
time += delta;
break;
}
} else
// otherwise just play the beatmap
{
time += delta;
}
oldTime = time;
}
private float calculateAccuracy() {
if (world.getAccuracyMarkers().size == 0)
return 0f;
float sum = 0f;
List<Float> high = new ArrayList<>();
List<Float> low = new ArrayList<>();
for (AccuracyMarker hit : world.getAccuracyMarkers()) {
sum += hit.getTime();
}
float average = sum / world.getAccuracyMarkers().size;
for (AccuracyMarker value : world.getAccuracyMarkers()) {
if (value.getTime() >= average) {
high.add(value.getTime());
} else {
low.add(value.getTime());
}
}
Results.minAccuracy = calcAverage(low);
Results.maxAccuracy = calcAverage(high);
Results.unstableRating = 10 * calcDeviation(world.getAccuracyMarkers());
return sum / world.getAccuracyMarkers().size;
}
private float calcAverage(List<Float> values) {
if (values.size() == 0)
return 0;
float sum = 0;
for (Float value : values) {
sum += value;
}
return sum / values.size();
}
private float calcDeviation(Array<AccuracyMarker> values) {
if (values.size == 0)
return 0f;
float sum = 0f;
for (AccuracyMarker value : values) {
sum += value.getTime();
}
float mean = sum / values.size;
sum = 0f;
for (AccuracyMarker value : values) {
sum += (value.getTime() - mean) * (value.getTime() - mean);
}
return (float) Math.sqrt(sum / (values.size - 1));
}
private void processAccuracy(CircleMark.Accuracy accuracy, CircleMark.Accuracy accuracy2, boolean isHold) {
if (!isHold) {
if (accuracy == CircleMark.Accuracy.BAD) {
badCount++;
if (combo > largestCombo) {
largestCombo = combo;
}
combo = 0;
world.combo = 0;
} else if (accuracy == CircleMark.Accuracy.GOOD) {
goodCount++;
if (combo > largestCombo) {
largestCombo = combo;
}
combo = 0;
world.combo = 0;
} else if (accuracy == CircleMark.Accuracy.GREAT) {
greatCount++;
combo++;
world.combo = combo;
} else if (accuracy == CircleMark.Accuracy.PERFECT) {
perfectCount++;
combo++;
world.combo = combo;
} else {
missCount++;
if (combo > largestCombo) {
largestCombo = combo;
}
combo = 0;
world.combo = 0;
}
} else {
// no combo break
CircleMark.Accuracy lowest = accuracy.compareTo(accuracy2) >= 0 ? accuracy2 : accuracy;
if (lowest == CircleMark.Accuracy.BAD) {
badCount++;
} else if (lowest == CircleMark.Accuracy.GOOD) {
goodCount++;
} else if (lowest == CircleMark.Accuracy.GREAT) {
greatCount++;
} else if (lowest == CircleMark.Accuracy.PERFECT) {
perfectCount++;
} else {
missCount++;
}
if (accuracy2.compareTo(CircleMark.Accuracy.GOOD) > 0) {
combo++;
world.combo = combo;
} else {
if (combo > largestCombo) {
largestCombo = combo;
}
combo = 0;
world.combo = 0;
}
}
}
private void playSoundForAccuracy(CircleMark.Accuracy accuracy) {
if (accuracy == CircleMark.Accuracy.PERFECT) {
Assets.perfectSound.play(GlobalConfiguration.feedbackVolume / 100f);
}
if (accuracy == CircleMark.Accuracy.GREAT) {
Assets.greatSound.play(GlobalConfiguration.feedbackVolume / 100f);
}
if (accuracy == CircleMark.Accuracy.GOOD) {
Assets.goodSound.play(GlobalConfiguration.feedbackVolume / 100f);
}
if (accuracy == CircleMark.Accuracy.BAD) {
Assets.badSound.play(GlobalConfiguration.feedbackVolume / 100f);
}
}
public void pressed(int screenX, int screenY, int pointer, int button, float ppuX, float ppuY, int width, int height) {
playMusicOnDemand();
int matchedId = getTapZoneForCoordinates(screenX, screenY, ppuX, ppuY, width, height, pointer);
if (matchedId == -1) {
return;
}
hit(matchedId);
}
public void released(int screenX, int screenY, int pointer, int button, float ppuX, float ppuY, int width, int height) {
int matchedId = -1;
for (TapZone zone : tapZones) {
if (zone.getId().equals(pointerToZoneId.get(pointer))) {
matchedId = zone.getId();
pointerToZoneId.remove(pointer);
zone.setState(TapZone.State.STATE_PRESSED, false);
}
}
if (matchedId == -1) {
return;
}
release(matchedId);
}
private void playMusicOnDemand() {
if (!world.started) {
world.started = true;
if (hasMusic) {
theSong.setLooping(false);
theSong.setOnCompletionListener(this);
theSong.setVolume(GlobalConfiguration.songVolume / 100f);
}
} else {
if (world.paused) {
world.paused = false;
if (hasMusic) {
theSong.setPosition(lastmtime);
time = lastmtime + world.delay;
theSong.play();
}
}
}
}
private int getTapZoneForCoordinates(int screenX, int screenY, float ppuX, float ppuY, int width, int height, int pointer) {
float centerX = world.offsetX + width / 2;
float centerY = world.offsetY + height * 0.25f;
float relativeX = (screenX - centerX) / ppuX;
float relativeY = (-screenY + centerY) / ppuY;
float circleRadius = 400 * 0.1f;
float relativeDistance = (float) Math.sqrt(relativeX * relativeX + relativeY * relativeY);
float relativeAngle = (float) Math.acos(relativeX / relativeDistance);
int matchedId = -1;
for (TapZone zone : tapZones) {
float x = zone.getPosition().x;
float y = zone.getPosition().y;
float tapZoneDistance = (float) Math.sqrt(x * x + y * y);
if (tapZoneDistance - circleRadius * 2 < relativeDistance && relativeDistance < tapZoneDistance + circleRadius * 2) {
float tapAngle = (float) Math.acos(x / tapZoneDistance);
if (tapAngle - Math.PI / 16 < relativeAngle && relativeAngle < tapAngle + Math.PI / 16 && relativeY < circleRadius) {
matchedId = zone.getId();
zone.setState(TapZone.State.STATE_PRESSED, true);
pointerToZoneId.put(pointer, matchedId);
}
}
}
return matchedId;
}
private void hit(int matchedId) {
for (CircleMark mark : marks) {
if (!mark.waiting) {
continue;
}
if (mark.notePosition == (matchedId)) {
CircleMark.Accuracy accuracy = mark.hit();
// if we tap too early, ignore this tap
if (accuracy == CircleMark.Accuracy.NONE)
continue;
playSoundForAccuracy(accuracy);
if (!mark.hold) {
processAccuracy(accuracy, null, false);
leftMark = !leftMark;
}
if (mark.hold && accuracy.compareTo(CircleMark.Accuracy.GOOD) <= 0) {
if (combo > largestCombo) {
largestCombo = combo;
}
combo = 0;
world.combo = 0;
}
accuracyPopups.add(new AccuracyPopup(accuracy, mark.accuracyHitStartTime < 0));
accuracyMarkers.add(new AccuracyMarker(mark.accuracyHitStartTime));
accuracyList.add(accuracy);
// 1 mark per tap
break;
}
}
}
private void release(int matchedId) {
for (CircleMark mark : marks) {
if (!mark.hold) {
continue;
}
if (!mark.waiting) {
continue;
}
if (matchedId == mark.notePosition) {
CircleMark.Accuracy accuracy = mark.release();
// releasing in the same zone as an upcoming hold can cause 'None' results
if (accuracy == CircleMark.Accuracy.NONE)
continue;
if (accuracy != CircleMark.Accuracy.MISS) {
playSoundForAccuracy(accuracy);
leftMark = !leftMark;
accuracyMarkers.add(new AccuracyMarker(mark.accuracyHitEndTime));
}
accuracyPopups.add(new AccuracyPopup(accuracy, accuracy != CircleMark.Accuracy.MISS && mark.accuracyHitEndTime < 0));
processAccuracy(mark.accuracyStart, accuracy, true);
accuracyList.add(accuracy);
// 1 mark per release
break;
}
}
}
private void processInput() {
boolean done = true;
for (CircleMark mark : marks) {
if (done && !mark.isDone()) {
done = false;
}
if (!mark.processed && mark.isDone()) {
mark.processed = true;
if (!mark.hold) {
if (mark.accuracyStart == CircleMark.Accuracy.MISS) {
accuracyPopups.add(new AccuracyPopup(CircleMark.Accuracy.MISS, false));
processAccuracy(mark.accuracyStart, null, false);
accuracyList.add(mark.accuracyStart);
}
} else {
if (mark.accuracyStart == CircleMark.Accuracy.MISS) {
accuracyPopups.add(new AccuracyPopup(CircleMark.Accuracy.MISS, false));
processAccuracy(mark.accuracyStart, null, false);
accuracyList.add(mark.accuracyStart);
} else if (mark.accuracyEnd == CircleMark.Accuracy.MISS) {
processAccuracy(mark.accuracyEnd, null, false);
accuracyPopups.add(new AccuracyPopup(CircleMark.Accuracy.MISS, false));
}
}
}
}
if (isABRepeatMode) {
if (time + world.delay >= bPosition) {
this.onCompletion(theSong);
}
}
if (done && !hasMusic) {
this.onCompletion(null);
}
if (time > world.getDuration() / (GlobalConfiguration.playbackRate == null ? 1.0f : GlobalConfiguration.playbackRate) + world.delay) {
if (!hasMusic)
this.onCompletion(null);
}
}
public void back() {
if (world.started) {
// if the game was paused and we pressed back again, we skip to the results screen
if (world.paused) {
this.done = true;
this.onCompletion(theSong);
return;
}
world.paused = true;
if (hasMusic) {
theSong.pause();
lastmtime = theSong.getPosition();
time = lastmtime + world.delay;
timeSyncAcc = 0;
}
}
}
}