f810965921
Can watch HalfTime on half speed. Reset pitch on leaving the game state. Load songInfo when entering select song menu. Playback button fades in on hover instead of expands. Reverted spinner changes. It should be fixed separately.
1706 lines
53 KiB
Java
1706 lines
53 KiB
Java
/*
|
|
* opsu! - an open-source osu! client
|
|
* Copyright (C) 2014, 2015 Jeffrey Han
|
|
*
|
|
* opsu! is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* opsu! is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package itdelatrisu.opsu.states;
|
|
|
|
import itdelatrisu.opsu.ErrorHandler;
|
|
import itdelatrisu.opsu.GameData;
|
|
import itdelatrisu.opsu.GameImage;
|
|
import itdelatrisu.opsu.GameMod;
|
|
import itdelatrisu.opsu.MenuButton;
|
|
import itdelatrisu.opsu.Opsu;
|
|
import itdelatrisu.opsu.Options;
|
|
import itdelatrisu.opsu.OsuFile;
|
|
import itdelatrisu.opsu.OsuHitObject;
|
|
import itdelatrisu.opsu.OsuParser;
|
|
import itdelatrisu.opsu.OsuTimingPoint;
|
|
import itdelatrisu.opsu.ScoreData;
|
|
import itdelatrisu.opsu.UI;
|
|
import itdelatrisu.opsu.Utils;
|
|
import itdelatrisu.opsu.audio.HitSound;
|
|
import itdelatrisu.opsu.audio.MusicController;
|
|
import itdelatrisu.opsu.audio.SoundController;
|
|
import itdelatrisu.opsu.audio.SoundEffect;
|
|
import itdelatrisu.opsu.db.OsuDB;
|
|
import itdelatrisu.opsu.db.ScoreDB;
|
|
import itdelatrisu.opsu.objects.Circle;
|
|
import itdelatrisu.opsu.objects.DummyObject;
|
|
import itdelatrisu.opsu.objects.HitObject;
|
|
import itdelatrisu.opsu.objects.Slider;
|
|
import itdelatrisu.opsu.objects.Spinner;
|
|
import itdelatrisu.opsu.replay.Replay;
|
|
import itdelatrisu.opsu.replay.ReplayFrame;
|
|
|
|
import java.io.File;
|
|
import java.util.LinkedList;
|
|
import java.util.Stack;
|
|
|
|
import itdelatrisu.opsu.replay.PlaybackSpeed;
|
|
import org.lwjgl.input.Keyboard;
|
|
import org.lwjgl.opengl.Display;
|
|
import org.newdawn.slick.Animation;
|
|
import org.newdawn.slick.Color;
|
|
import org.newdawn.slick.GameContainer;
|
|
import org.newdawn.slick.Graphics;
|
|
import org.newdawn.slick.Image;
|
|
import org.newdawn.slick.Input;
|
|
import org.newdawn.slick.SlickException;
|
|
import org.newdawn.slick.state.BasicGameState;
|
|
import org.newdawn.slick.state.StateBasedGame;
|
|
import org.newdawn.slick.state.transition.EmptyTransition;
|
|
import org.newdawn.slick.state.transition.FadeInTransition;
|
|
import org.newdawn.slick.state.transition.FadeOutTransition;
|
|
|
|
/**
|
|
* "Game" state.
|
|
*/
|
|
public class Game extends BasicGameState {
|
|
/** Game restart states. */
|
|
public enum Restart {
|
|
/** No restart. */
|
|
FALSE,
|
|
/** First time loading the song. */
|
|
NEW,
|
|
/** Manual retry. */
|
|
MANUAL,
|
|
/** Replay. */
|
|
REPLAY,
|
|
/** Health is zero: no-continue/force restart. */
|
|
LOSE
|
|
}
|
|
|
|
/** Minimum time before start of song, in milliseconds, to process skip-related actions. */
|
|
private static final int SKIP_OFFSET = 2000;
|
|
|
|
/** Tolerance in case if hit object is not snapped to the grid. */
|
|
private static final float STACK_LENIENCE = 3f;
|
|
|
|
/** Stack time window of the previous object, in ms. */
|
|
private static final int STACK_TIMEOUT = 1000;
|
|
|
|
/** Stack position offset modifier. */
|
|
private static final float STACK_OFFSET_MODIFIER = 0.05f;
|
|
|
|
/** The associated OsuFile object. */
|
|
private OsuFile osu;
|
|
|
|
/** The associated GameData object. */
|
|
private GameData data;
|
|
|
|
/** Current hit object index in OsuHitObject[] array. */
|
|
private int objectIndex = 0;
|
|
|
|
/** The map's HitObjects, indexed by objectIndex. */
|
|
private HitObject[] hitObjects;
|
|
|
|
/** Delay time, in milliseconds, before song starts. */
|
|
private int leadInTime;
|
|
|
|
/** Hit object approach time, in milliseconds. */
|
|
private int approachTime;
|
|
|
|
/** Time offsets for obtaining each hit result (indexed by HIT_* constants). */
|
|
private int[] hitResultOffset;
|
|
|
|
/** Current restart state. */
|
|
private Restart restart;
|
|
|
|
/** Current break index in breaks ArrayList. */
|
|
private int breakIndex;
|
|
|
|
/** Break start time (0 if not in break). */
|
|
private int breakTime = 0;
|
|
|
|
/** Whether the break sound has been played. */
|
|
private boolean breakSound;
|
|
|
|
/** Skip button (displayed at song start, when necessary). */
|
|
private MenuButton skipButton;
|
|
|
|
/** Playback button (displayed in replays). */
|
|
private MenuButton playbackButton;
|
|
|
|
/** Current timing point index in timingPoints ArrayList. */
|
|
private int timingPointIndex;
|
|
|
|
/** Current beat lengths (base value and inherited value). */
|
|
private float beatLengthBase, beatLength;
|
|
|
|
/** Whether the countdown sound has been played. */
|
|
private boolean
|
|
countdownReadySound, countdown3Sound, countdown1Sound,
|
|
countdown2Sound, countdownGoSound;
|
|
|
|
/** Mouse coordinates before game paused. */
|
|
private int pausedMouseX = -1, pausedMouseY = -1;
|
|
|
|
/** Track position when game paused. */
|
|
private int pauseTime = -1;
|
|
|
|
/** Value for handling hitCircleSelect pulse effect (expanding, alpha level). */
|
|
private float pausePulse;
|
|
|
|
/** Whether a checkpoint has been loaded during this game. */
|
|
private boolean checkpointLoaded = false;
|
|
|
|
/** Number of deaths, used if "Easy" mod is enabled. */
|
|
private byte deaths = 0;
|
|
|
|
/** Track position at death, used if "Easy" mod is enabled. */
|
|
private int deathTime = -1;
|
|
|
|
/** Number of retries. */
|
|
private int retries = 0;
|
|
|
|
/** Whether or not this game is a replay. */
|
|
private boolean isReplay = false;
|
|
|
|
/** The replay, if any. */
|
|
private Replay replay;
|
|
|
|
/** The current replay frame index. */
|
|
private int replayIndex = 0;
|
|
|
|
/** The replay cursor coordinates. */
|
|
private int replayX, replayY;
|
|
|
|
/** Whether a replay key is currently pressed. */
|
|
private boolean replayKeyPressed;
|
|
|
|
/** The replay skip time, or -1 if none. */
|
|
private int replaySkipTime = -1;
|
|
|
|
/** The last replay frame time. */
|
|
private int lastReplayTime = 0;
|
|
|
|
/** The keys from the previous replay frame. */
|
|
private int lastReplayKeys = 0;
|
|
|
|
/** The last game keys pressed. */
|
|
private int lastKeysPressed = ReplayFrame.KEY_NONE;
|
|
|
|
/** The previous game mod state (before the replay). */
|
|
private int previousMods = 0;
|
|
|
|
/** The list of current replay frames (for recording replays). */
|
|
private LinkedList<ReplayFrame> replayFrames;
|
|
|
|
/** The offscreen image rendered to. */
|
|
private Image offscreen;
|
|
|
|
/** The offscreen graphics. */
|
|
private Graphics gOffscreen;
|
|
|
|
/** The current flashlight area radius. */
|
|
private int flashlightRadius;
|
|
|
|
/** The cursor coordinates using the "auto" or "relax" mods. */
|
|
private int autoMouseX = 0, autoMouseY = 0;
|
|
|
|
/** Whether or not the cursor should be pressed using the "auto" mod. */
|
|
private boolean autoMousePressed;
|
|
|
|
// game-related variables
|
|
private GameContainer container;
|
|
private StateBasedGame game;
|
|
private Input input;
|
|
private int state;
|
|
|
|
public Game(int state) {
|
|
this.state = state;
|
|
}
|
|
|
|
@Override
|
|
public void init(GameContainer container, StateBasedGame game)
|
|
throws SlickException {
|
|
this.container = container;
|
|
this.game = game;
|
|
input = container.getInput();
|
|
|
|
int width = container.getWidth();
|
|
int height = container.getHeight();
|
|
|
|
// create offscreen graphics
|
|
offscreen = new Image(width, height);
|
|
gOffscreen = offscreen.getGraphics();
|
|
gOffscreen.setBackground(Color.black);
|
|
|
|
// create the associated GameData object
|
|
data = new GameData(width, height);
|
|
}
|
|
|
|
@Override
|
|
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
|
throws SlickException {
|
|
int width = container.getWidth();
|
|
int height = container.getHeight();
|
|
int trackPosition = MusicController.getPosition();
|
|
if (pauseTime > -1) // returning from pause screen
|
|
trackPosition = pauseTime;
|
|
else if (deathTime > -1) // "Easy" mod: health bar increasing
|
|
trackPosition = deathTime;
|
|
int firstObjectTime = osu.objects[0].getTime();
|
|
int timeDiff = firstObjectTime - trackPosition;
|
|
|
|
g.setBackground(Color.black);
|
|
|
|
// "flashlight" mod: initialize offscreen graphics
|
|
if (GameMod.FLASHLIGHT.isActive()) {
|
|
gOffscreen.clear();
|
|
Graphics.setCurrent(gOffscreen);
|
|
}
|
|
|
|
// background
|
|
float dimLevel = Options.getBackgroundDim();
|
|
if (trackPosition < firstObjectTime) {
|
|
if (timeDiff < approachTime)
|
|
dimLevel += (1f - dimLevel) * ((float) timeDiff / Math.min(approachTime, firstObjectTime));
|
|
else
|
|
dimLevel = 1f;
|
|
}
|
|
if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel, false)) {
|
|
Image playfield = GameImage.PLAYFIELD.getImage();
|
|
playfield.setAlpha(dimLevel);
|
|
playfield.draw();
|
|
playfield.setAlpha(1f);
|
|
}
|
|
|
|
if (GameMod.FLASHLIGHT.isActive())
|
|
Graphics.setCurrent(g);
|
|
|
|
// "auto" and "autopilot" mods: move cursor automatically
|
|
// TODO: this should really be in update(), not render()
|
|
autoMouseX = width / 2;
|
|
autoMouseY = height / 2;
|
|
autoMousePressed = false;
|
|
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
|
float[] autoXY = null;
|
|
if (isLeadIn()) {
|
|
// lead-in
|
|
float progress = Math.max((float) (leadInTime - osu.audioLeadIn) / approachTime, 0f);
|
|
autoMouseY = (int) (height / (2f - progress));
|
|
} else if (objectIndex == 0 && trackPosition < firstObjectTime) {
|
|
// before first object
|
|
timeDiff = firstObjectTime - trackPosition;
|
|
if (timeDiff < approachTime) {
|
|
float[] xy = hitObjects[0].getPointAt(trackPosition);
|
|
autoXY = getPointAt(autoMouseX, autoMouseY, xy[0], xy[1], 1f - ((float) timeDiff / Math.min(approachTime, firstObjectTime)));
|
|
}
|
|
} else if (objectIndex < osu.objects.length) {
|
|
// normal object
|
|
int objectTime = osu.objects[objectIndex].getTime();
|
|
if (trackPosition < objectTime) {
|
|
float[] xyStart = hitObjects[objectIndex - 1].getPointAt(trackPosition);
|
|
int startTime = hitObjects[objectIndex - 1].getEndTime();
|
|
if (osu.breaks != null && breakIndex < osu.breaks.size()) {
|
|
// starting a break: keep cursor at previous hit object position
|
|
if (breakTime > 0 || objectTime > osu.breaks.get(breakIndex))
|
|
autoXY = xyStart;
|
|
|
|
// after a break ends: move startTime to break end time
|
|
else if (breakIndex > 1) {
|
|
int lastBreakEndTime = osu.breaks.get(breakIndex - 1);
|
|
if (objectTime > lastBreakEndTime && startTime < lastBreakEndTime)
|
|
startTime = lastBreakEndTime;
|
|
}
|
|
}
|
|
if (autoXY == null) {
|
|
float[] xyEnd = hitObjects[objectIndex].getPointAt(trackPosition);
|
|
int totalTime = objectTime - startTime;
|
|
autoXY = getPointAt(xyStart[0], xyStart[1], xyEnd[0], xyEnd[1], (float) (trackPosition - startTime) / totalTime);
|
|
|
|
// hit circles: show a mouse press
|
|
int offset300 = hitResultOffset[GameData.HIT_300];
|
|
if ((osu.objects[objectIndex].isCircle() && objectTime - trackPosition < offset300) ||
|
|
(osu.objects[objectIndex - 1].isCircle() && trackPosition - osu.objects[objectIndex - 1].getTime() < offset300))
|
|
autoMousePressed = true;
|
|
}
|
|
} else {
|
|
autoXY = hitObjects[objectIndex].getPointAt(trackPosition);
|
|
autoMousePressed = true;
|
|
}
|
|
} else {
|
|
// last object
|
|
autoXY = hitObjects[objectIndex - 1].getPointAt(trackPosition);
|
|
}
|
|
|
|
// set mouse coordinates
|
|
if (autoXY != null) {
|
|
autoMouseX = (int) autoXY[0];
|
|
autoMouseY = (int) autoXY[1];
|
|
}
|
|
}
|
|
|
|
// "flashlight" mod: restricted view of hit objects around cursor
|
|
if (GameMod.FLASHLIGHT.isActive()) {
|
|
// render hit objects offscreen
|
|
Graphics.setCurrent(gOffscreen);
|
|
int trackPos = (isLeadIn()) ? (leadInTime - Options.getMusicOffset()) * -1 : trackPosition;
|
|
drawHitObjects(gOffscreen, trackPos);
|
|
|
|
// restore original graphics context
|
|
gOffscreen.flush();
|
|
Graphics.setCurrent(g);
|
|
|
|
// draw alpha map around cursor
|
|
g.setDrawMode(Graphics.MODE_ALPHA_MAP);
|
|
g.clearAlphaMap();
|
|
int mouseX, mouseY;
|
|
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
|
|
mouseX = pausedMouseX;
|
|
mouseY = pausedMouseY;
|
|
} else if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
|
mouseX = autoMouseX;
|
|
mouseY = autoMouseY;
|
|
} else if (isReplay) {
|
|
mouseX = replayX;
|
|
mouseY = replayY;
|
|
} else {
|
|
mouseX = input.getMouseX();
|
|
mouseY = input.getMouseY();
|
|
}
|
|
int alphaRadius = flashlightRadius * 256 / 215;
|
|
int alphaX = mouseX - alphaRadius / 2;
|
|
int alphaY = mouseY - alphaRadius / 2;
|
|
GameImage.ALPHA_MAP.getImage().draw(alphaX, alphaY, alphaRadius, alphaRadius);
|
|
|
|
// blend offscreen image
|
|
g.setDrawMode(Graphics.MODE_ALPHA_BLEND);
|
|
g.setClip(alphaX, alphaY, alphaRadius, alphaRadius);
|
|
g.drawImage(offscreen, 0, 0);
|
|
g.clearClip();
|
|
g.setDrawMode(Graphics.MODE_NORMAL);
|
|
}
|
|
|
|
// break periods
|
|
if (osu.breaks != null && breakIndex < osu.breaks.size() && breakTime > 0) {
|
|
int endTime = osu.breaks.get(breakIndex);
|
|
int breakLength = endTime - breakTime;
|
|
|
|
// letterbox effect (black bars on top/bottom)
|
|
if (osu.letterboxInBreaks && breakLength >= 4000) {
|
|
g.setColor(Color.black);
|
|
g.fillRect(0, 0, width, height * 0.125f);
|
|
g.fillRect(0, height * 0.875f, width, height * 0.125f);
|
|
}
|
|
|
|
data.drawGameElements(g, true, objectIndex == 0);
|
|
|
|
if (breakLength >= 8000 &&
|
|
trackPosition - breakTime > 2000 &&
|
|
trackPosition - breakTime < 5000) {
|
|
// show break start
|
|
if (data.getHealth() >= 50) {
|
|
GameImage.SECTION_PASS.getImage().drawCentered(width / 2f, height / 2f);
|
|
if (!breakSound) {
|
|
SoundController.playSound(SoundEffect.SECTIONPASS);
|
|
breakSound = true;
|
|
}
|
|
} else {
|
|
GameImage.SECTION_FAIL.getImage().drawCentered(width / 2f, height / 2f);
|
|
if (!breakSound) {
|
|
SoundController.playSound(SoundEffect.SECTIONFAIL);
|
|
breakSound = true;
|
|
}
|
|
}
|
|
} else if (breakLength >= 4000) {
|
|
// show break end (flash twice for 500ms)
|
|
int endTimeDiff = endTime - trackPosition;
|
|
if ((endTimeDiff > 1500 && endTimeDiff < 2000) ||
|
|
(endTimeDiff > 500 && endTimeDiff < 1000)) {
|
|
Image arrow = GameImage.WARNINGARROW.getImage();
|
|
arrow.setRotation(0);
|
|
arrow.draw(width * 0.15f, height * 0.15f);
|
|
arrow.draw(width * 0.15f, height * 0.75f);
|
|
arrow.setRotation(180);
|
|
arrow.draw(width * 0.75f, height * 0.15f);
|
|
arrow.draw(width * 0.75f, height * 0.75f);
|
|
}
|
|
}
|
|
}
|
|
|
|
// non-break
|
|
else {
|
|
// game elements
|
|
data.drawGameElements(g, false, objectIndex == 0);
|
|
|
|
// skip beginning
|
|
if (objectIndex == 0 &&
|
|
trackPosition < osu.objects[0].getTime() - SKIP_OFFSET)
|
|
skipButton.draw();
|
|
|
|
// show retries
|
|
if (retries >= 2 && timeDiff >= -1000) {
|
|
int retryHeight = Math.max(
|
|
GameImage.SCOREBAR_BG.getImage().getHeight(),
|
|
GameImage.SCOREBAR_KI.getImage().getHeight()
|
|
);
|
|
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
|
|
if (timeDiff < -500)
|
|
Utils.COLOR_WHITE_FADE.a = (1000 + timeDiff) / 500f;
|
|
Utils.FONT_MEDIUM.drawString(
|
|
2 + (width / 100), retryHeight,
|
|
String.format("%d retries and counting...", retries),
|
|
Utils.COLOR_WHITE_FADE
|
|
);
|
|
Utils.COLOR_WHITE_FADE.a = oldAlpha;
|
|
}
|
|
|
|
if (isLeadIn())
|
|
trackPosition = (leadInTime - Options.getMusicOffset()) * -1; // render approach circles during song lead-in
|
|
|
|
// countdown
|
|
if (osu.countdown > 0) { // TODO: implement half/double rate settings
|
|
timeDiff = firstObjectTime - trackPosition;
|
|
if (timeDiff >= 500 && timeDiff < 3000) {
|
|
if (timeDiff >= 1500) {
|
|
GameImage.COUNTDOWN_READY.getImage().drawCentered(width / 2, height / 2);
|
|
if (!countdownReadySound) {
|
|
SoundController.playSound(SoundEffect.READY);
|
|
countdownReadySound = true;
|
|
}
|
|
}
|
|
if (timeDiff < 2000) {
|
|
GameImage.COUNTDOWN_3.getImage().draw(0, 0);
|
|
if (!countdown3Sound) {
|
|
SoundController.playSound(SoundEffect.COUNT3);
|
|
countdown3Sound = true;
|
|
}
|
|
}
|
|
if (timeDiff < 1500) {
|
|
GameImage.COUNTDOWN_2.getImage().draw(width - GameImage.COUNTDOWN_2.getImage().getWidth(), 0);
|
|
if (!countdown2Sound) {
|
|
SoundController.playSound(SoundEffect.COUNT2);
|
|
countdown2Sound = true;
|
|
}
|
|
}
|
|
if (timeDiff < 1000) {
|
|
GameImage.COUNTDOWN_1.getImage().drawCentered(width / 2, height / 2);
|
|
if (!countdown1Sound) {
|
|
SoundController.playSound(SoundEffect.COUNT1);
|
|
countdown1Sound = true;
|
|
}
|
|
}
|
|
} else if (timeDiff >= -500 && timeDiff < 500) {
|
|
Image go = GameImage.COUNTDOWN_GO.getImage();
|
|
go.setAlpha((timeDiff < 0) ? 1 - (timeDiff / -1000f) : 1);
|
|
go.drawCentered(width / 2, height / 2);
|
|
if (!countdownGoSound) {
|
|
SoundController.playSound(SoundEffect.GO);
|
|
countdownGoSound = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// draw hit objects
|
|
if (!GameMod.FLASHLIGHT.isActive())
|
|
drawHitObjects(g, trackPosition);
|
|
}
|
|
|
|
if (GameMod.AUTO.isActive())
|
|
GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f);
|
|
|
|
// returning from pause screen
|
|
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
|
|
// darken the screen
|
|
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
|
g.fillRect(0, 0, width, height);
|
|
|
|
// draw glowing hit select circle and pulse effect
|
|
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth();
|
|
Image cursorCircle = GameImage.HITCIRCLE_SELECT.getImage().getScaledCopy(circleRadius, circleRadius);
|
|
cursorCircle.setAlpha(1.0f);
|
|
cursorCircle.drawCentered(pausedMouseX, pausedMouseY);
|
|
Image cursorCirclePulse = cursorCircle.getScaledCopy(1f + pausePulse);
|
|
cursorCirclePulse.setAlpha(1f - pausePulse);
|
|
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
|
|
}
|
|
|
|
if (isReplay || GameMod.AUTO.isActive())
|
|
playbackButton.draw();
|
|
|
|
if (isReplay)
|
|
UI.draw(g, replayX, replayY, replayKeyPressed);
|
|
else if (GameMod.AUTO.isActive())
|
|
UI.draw(g, autoMouseX, autoMouseY, autoMousePressed);
|
|
else if (GameMod.AUTOPILOT.isActive())
|
|
UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed());
|
|
else
|
|
UI.draw(g);
|
|
|
|
}
|
|
|
|
@Override
|
|
public void update(GameContainer container, StateBasedGame game, int delta)
|
|
throws SlickException {
|
|
UI.update(delta);
|
|
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
|
skipButton.hoverUpdate(delta, mouseX, mouseY);
|
|
if (isReplay || GameMod.AUTO.isActive())
|
|
playbackButton.hoverUpdate(delta, mouseX, mouseY);
|
|
int trackPosition = MusicController.getPosition();
|
|
|
|
// returning from pause screen: must click previous mouse position
|
|
if (pauseTime > -1) {
|
|
// paused during lead-in or break, or "relax" or "autopilot": continue immediately
|
|
if ((pausedMouseX < 0 && pausedMouseY < 0) ||
|
|
(GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) {
|
|
pauseTime = -1;
|
|
if (!isLeadIn())
|
|
MusicController.resume();
|
|
}
|
|
|
|
// focus lost: go back to pause screen
|
|
else if (!container.hasFocus()) {
|
|
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
|
pausePulse = 0f;
|
|
}
|
|
|
|
// advance pulse animation
|
|
else {
|
|
pausePulse += delta / 750f;
|
|
if (pausePulse > 1f)
|
|
pausePulse = 0f;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// replays: skip intro
|
|
if (isReplay && replaySkipTime > -1 && trackPosition >= replaySkipTime) {
|
|
if (skipIntro())
|
|
trackPosition = MusicController.getPosition();
|
|
}
|
|
|
|
// "flashlight" mod: calculate visible area radius
|
|
updateFlashlightRadius(delta, trackPosition);
|
|
|
|
// stop updating during song lead-in
|
|
if (isLeadIn()) {
|
|
leadInTime -= delta;
|
|
if (!isLeadIn())
|
|
MusicController.resume();
|
|
return;
|
|
}
|
|
|
|
// normal game update
|
|
if (!isReplay)
|
|
addReplayFrameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
|
|
|
|
// watching replay
|
|
else {
|
|
// out of frames, use previous data
|
|
if (replayIndex >= replay.frames.length)
|
|
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
|
|
|
|
// update and run replay frames
|
|
while (replayIndex < replay.frames.length && trackPosition >= replay.frames[replayIndex].getTime()) {
|
|
ReplayFrame frame = replay.frames[replayIndex];
|
|
replayX = frame.getScaledX();
|
|
replayY = frame.getScaledY();
|
|
replayKeyPressed = frame.isKeyPressed();
|
|
lastKeysPressed = frame.getKeys();
|
|
runReplayFrame(frame);
|
|
replayIndex++;
|
|
}
|
|
mouseX = replayX;
|
|
mouseY = replayY;
|
|
}
|
|
|
|
data.updateDisplays(delta);
|
|
}
|
|
|
|
/**
|
|
* Updates the game.
|
|
* @param mouseX the mouse x coordinate
|
|
* @param mouseY the mouse y coordinate
|
|
* @param delta the delta interval
|
|
* @param trackPosition the track position
|
|
* @param keys the keys that are pressed
|
|
*/
|
|
private void updateGame(int mouseX, int mouseY, int delta, int trackPosition, int keys) {
|
|
// "Easy" mod: multiple "lives"
|
|
if (GameMod.EASY.isActive() && deathTime > -1) {
|
|
if (data.getHealth() < 99f)
|
|
data.changeHealth(delta / 10f);
|
|
else {
|
|
MusicController.resume();
|
|
deathTime = -1;
|
|
}
|
|
}
|
|
|
|
// map complete!
|
|
if (objectIndex >= hitObjects.length || (MusicController.trackEnded() && objectIndex > 0)) {
|
|
// track ended before last object was processed: force a hit result
|
|
if (MusicController.trackEnded() && objectIndex < hitObjects.length)
|
|
hitObjects[objectIndex].update(true, delta, mouseX, mouseY, false, trackPosition);
|
|
|
|
// if checkpoint used, skip ranking screen
|
|
if (checkpointLoaded)
|
|
game.closeRequested();
|
|
|
|
// go to ranking screen
|
|
else {
|
|
boolean unranked = (GameMod.AUTO.isActive() || GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive());
|
|
((GameRanking) game.getState(Opsu.STATE_GAMERANKING)).setGameData(data);
|
|
if (isReplay)
|
|
data.setReplay(replay);
|
|
else if (replayFrames != null) {
|
|
// finalize replay frames with start/skip frames
|
|
if (!replayFrames.isEmpty())
|
|
replayFrames.getFirst().setTimeDiff(replaySkipTime * -1);
|
|
replayFrames.addFirst(ReplayFrame.getStartFrame(replaySkipTime));
|
|
replayFrames.addFirst(ReplayFrame.getStartFrame(0));
|
|
Replay r = data.getReplay(replayFrames.toArray(new ReplayFrame[replayFrames.size()]), osu);
|
|
if (r != null && !unranked)
|
|
r.save();
|
|
}
|
|
ScoreData score = data.getScoreData(osu);
|
|
|
|
// add score to database
|
|
if (!unranked && !isReplay)
|
|
ScoreDB.addScore(score);
|
|
|
|
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// timing points
|
|
if (timingPointIndex < osu.timingPoints.size()) {
|
|
OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex);
|
|
if (trackPosition >= timingPoint.getTime()) {
|
|
setBeatLength(timingPoint);
|
|
HitSound.setDefaultSampleSet(timingPoint.getSampleType());
|
|
SoundController.setSampleVolume(timingPoint.getSampleVolume());
|
|
timingPointIndex++;
|
|
}
|
|
}
|
|
|
|
// song beginning
|
|
if (objectIndex == 0 && trackPosition < osu.objects[0].getTime())
|
|
return; // nothing to do here
|
|
|
|
// break periods
|
|
if (osu.breaks != null && breakIndex < osu.breaks.size()) {
|
|
int breakValue = osu.breaks.get(breakIndex);
|
|
if (breakTime > 0) { // in a break period
|
|
if (trackPosition < breakValue)
|
|
return;
|
|
else {
|
|
// break is over
|
|
breakTime = 0;
|
|
breakIndex++;
|
|
}
|
|
} else if (trackPosition >= breakValue) {
|
|
// start a break
|
|
breakTime = breakValue;
|
|
breakSound = false;
|
|
breakIndex++;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// pause game if focus lost
|
|
if (!container.hasFocus() && !GameMod.AUTO.isActive() && !isReplay) {
|
|
if (pauseTime < 0) {
|
|
pausedMouseX = mouseX;
|
|
pausedMouseY = mouseY;
|
|
pausePulse = 0f;
|
|
}
|
|
if (MusicController.isPlaying() || isLeadIn())
|
|
pauseTime = trackPosition;
|
|
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
|
}
|
|
|
|
// drain health
|
|
data.changeHealth(delta * -1 * GameData.HP_DRAIN_MULTIPLIER);
|
|
if (!data.isAlive()) {
|
|
// "Easy" mod
|
|
if (GameMod.EASY.isActive() && !GameMod.SUDDEN_DEATH.isActive()) {
|
|
deaths++;
|
|
if (deaths < 3) {
|
|
deathTime = trackPosition;
|
|
MusicController.pause();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// game over, force a restart
|
|
if (!isReplay) {
|
|
restart = Restart.LOSE;
|
|
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
|
}
|
|
}
|
|
|
|
// update objects (loop in unlikely event of any skipped indexes)
|
|
boolean keyPressed = keys != ReplayFrame.KEY_NONE;
|
|
while (objectIndex < hitObjects.length && trackPosition > osu.objects[objectIndex].getTime()) {
|
|
// check if we've already passed the next object's start time
|
|
boolean overlap = (objectIndex + 1 < hitObjects.length &&
|
|
trackPosition > osu.objects[objectIndex + 1].getTime() - hitResultOffset[GameData.HIT_300]);
|
|
|
|
// update hit object and check completion status
|
|
if (hitObjects[objectIndex].update(overlap, delta, mouseX, mouseY, keyPressed, trackPosition))
|
|
objectIndex++; // done, so increment object index
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int getID() { return state; }
|
|
|
|
@Override
|
|
public void keyPressed(int key, char c) {
|
|
int trackPosition = MusicController.getPosition();
|
|
int mouseX = input.getMouseX();
|
|
int mouseY = input.getMouseY();
|
|
|
|
// game keys
|
|
if (!Keyboard.isRepeatEvent()) {
|
|
int keys = ReplayFrame.KEY_NONE;
|
|
if (key == Options.getGameKeyLeft())
|
|
keys = ReplayFrame.KEY_K1;
|
|
else if (key == Options.getGameKeyRight())
|
|
keys = ReplayFrame.KEY_K2;
|
|
if (keys != ReplayFrame.KEY_NONE)
|
|
gameKeyPressed(keys, mouseX, mouseY, trackPosition);
|
|
}
|
|
|
|
switch (key) {
|
|
case Input.KEY_ESCAPE:
|
|
// "auto" mod or watching replay: go back to song menu
|
|
if (GameMod.AUTO.isActive() || isReplay) {
|
|
game.closeRequested();
|
|
break;
|
|
}
|
|
|
|
// pause game
|
|
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= osu.objects[0].getTime()) {
|
|
pausedMouseX = mouseX;
|
|
pausedMouseY = mouseY;
|
|
pausePulse = 0f;
|
|
}
|
|
if (MusicController.isPlaying() || isLeadIn())
|
|
pauseTime = trackPosition;
|
|
game.enterState(Opsu.STATE_GAMEPAUSEMENU, new EmptyTransition(), new FadeInTransition(Color.black));
|
|
break;
|
|
case Input.KEY_SPACE:
|
|
// skip intro
|
|
skipIntro();
|
|
break;
|
|
case Input.KEY_R:
|
|
// restart
|
|
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
|
|
try {
|
|
if (trackPosition < osu.objects[0].getTime())
|
|
retries--; // don't count this retry (cancel out later increment)
|
|
restart = Restart.MANUAL;
|
|
enter(container, game);
|
|
skipIntro();
|
|
} catch (SlickException e) {
|
|
ErrorHandler.error("Failed to restart game.", e, false);
|
|
}
|
|
}
|
|
break;
|
|
case Input.KEY_S:
|
|
// save checkpoint
|
|
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
|
|
if (isLeadIn())
|
|
break;
|
|
|
|
int position = (pauseTime > -1) ? pauseTime : trackPosition;
|
|
if (Options.setCheckpoint(position / 1000)) {
|
|
SoundController.playSound(SoundEffect.MENUCLICK);
|
|
UI.sendBarNotification("Checkpoint saved.");
|
|
}
|
|
}
|
|
break;
|
|
case Input.KEY_L:
|
|
// load checkpoint
|
|
if (input.isKeyDown(Input.KEY_RCONTROL) || input.isKeyDown(Input.KEY_LCONTROL)) {
|
|
int checkpoint = Options.getCheckpoint();
|
|
if (checkpoint == 0 || checkpoint > osu.endTime)
|
|
break; // invalid checkpoint
|
|
try {
|
|
restart = Restart.MANUAL;
|
|
enter(container, game);
|
|
checkpointLoaded = true;
|
|
if (isLeadIn()) {
|
|
leadInTime = 0;
|
|
MusicController.resume();
|
|
}
|
|
SoundController.playSound(SoundEffect.MENUHIT);
|
|
UI.sendBarNotification("Checkpoint loaded.");
|
|
|
|
// skip to checkpoint
|
|
MusicController.setPosition(checkpoint);
|
|
while (objectIndex < hitObjects.length &&
|
|
osu.objects[objectIndex++].getTime() <= checkpoint)
|
|
;
|
|
objectIndex--;
|
|
lastReplayTime = osu.objects[objectIndex].getTime();
|
|
} catch (SlickException e) {
|
|
ErrorHandler.error("Failed to load checkpoint.", e, false);
|
|
}
|
|
}
|
|
break;
|
|
case Input.KEY_UP:
|
|
UI.changeVolume(1);
|
|
break;
|
|
case Input.KEY_DOWN:
|
|
UI.changeVolume(-1);
|
|
break;
|
|
case Input.KEY_F7:
|
|
Options.setNextFPS(container);
|
|
break;
|
|
case Input.KEY_F10:
|
|
Options.toggleMouseDisabled();
|
|
break;
|
|
case Input.KEY_F12:
|
|
Utils.takeScreenShot();
|
|
break;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void mousePressed(int button, int x, int y) {
|
|
// watching replay
|
|
if (isReplay || GameMod.AUTO.isActive()) {
|
|
// allow skip button
|
|
if (button != Input.MOUSE_MIDDLE_BUTTON && skipButton.contains(x, y)) {
|
|
skipIntro();
|
|
return;
|
|
}
|
|
if (button != Input.MOUSE_MIDDLE_BUTTON && playbackButton.contains(x, y)) {
|
|
PlaybackSpeed playbackSpeed = PlaybackSpeed.next();
|
|
playbackButton = playbackSpeed.getButton();
|
|
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (Options.isMouseDisabled())
|
|
return;
|
|
|
|
// mouse wheel: pause the game
|
|
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
|
|
int trackPosition = MusicController.getPosition();
|
|
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= osu.objects[0].getTime()) {
|
|
pausedMouseX = x;
|
|
pausedMouseY = y;
|
|
pausePulse = 0f;
|
|
}
|
|
if (MusicController.isPlaying() || isLeadIn())
|
|
pauseTime = trackPosition;
|
|
game.enterState(Opsu.STATE_GAMEPAUSEMENU, new EmptyTransition(), new FadeInTransition(Color.black));
|
|
return;
|
|
}
|
|
|
|
// game keys
|
|
int keys = ReplayFrame.KEY_NONE;
|
|
if (button == Input.MOUSE_LEFT_BUTTON)
|
|
keys = ReplayFrame.KEY_M1;
|
|
else if (button == Input.MOUSE_RIGHT_BUTTON)
|
|
keys = ReplayFrame.KEY_M2;
|
|
if (keys != ReplayFrame.KEY_NONE)
|
|
gameKeyPressed(keys, x, y, MusicController.getPosition());
|
|
}
|
|
|
|
/**
|
|
* Handles a game key pressed event.
|
|
* @param keys the game keys pressed
|
|
* @param x the mouse x coordinate
|
|
* @param y the mouse y coordinate
|
|
* @param trackPosition the track position
|
|
*/
|
|
private void gameKeyPressed(int keys, int x, int y, int trackPosition) {
|
|
// returning from pause screen
|
|
if (pauseTime > -1) {
|
|
double distance = Math.hypot(pausedMouseX - x, pausedMouseY - y);
|
|
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
|
|
if (distance < circleRadius) {
|
|
// unpause the game
|
|
pauseTime = -1;
|
|
pausedMouseX = -1;
|
|
pausedMouseY = -1;
|
|
if (!isLeadIn())
|
|
MusicController.resume();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// skip beginning
|
|
if (skipButton.contains(x, y)) {
|
|
if (skipIntro())
|
|
return; // successfully skipped
|
|
}
|
|
|
|
// "auto" and "relax" mods: ignore user actions
|
|
if (GameMod.AUTO.isActive() || GameMod.RELAX.isActive())
|
|
return;
|
|
|
|
// send a game key press
|
|
if (!isReplay && keys != ReplayFrame.KEY_NONE) {
|
|
lastKeysPressed |= keys; // set keys bits
|
|
addReplayFrameAndRun(x, y, lastKeysPressed, trackPosition);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void mouseReleased(int button, int x, int y) {
|
|
if (Options.isMouseDisabled())
|
|
return;
|
|
|
|
if (button == Input.MOUSE_MIDDLE_BUTTON)
|
|
return;
|
|
|
|
int keys = ReplayFrame.KEY_NONE;
|
|
if (button == Input.MOUSE_LEFT_BUTTON)
|
|
keys = ReplayFrame.KEY_M1;
|
|
else if (button == Input.MOUSE_RIGHT_BUTTON)
|
|
keys = ReplayFrame.KEY_M2;
|
|
if (keys != ReplayFrame.KEY_NONE)
|
|
gameKeyReleased(keys, x, y, MusicController.getPosition());
|
|
}
|
|
|
|
@Override
|
|
public void keyReleased(int key, char c) {
|
|
int keys = ReplayFrame.KEY_NONE;
|
|
if (key == Options.getGameKeyLeft())
|
|
keys = ReplayFrame.KEY_K1;
|
|
else if (key == Options.getGameKeyRight())
|
|
keys = ReplayFrame.KEY_K2;
|
|
if (keys != ReplayFrame.KEY_NONE)
|
|
gameKeyReleased(keys, input.getMouseX(), input.getMouseY(), MusicController.getPosition());
|
|
}
|
|
|
|
/**
|
|
* Handles a game key released event.
|
|
* @param keys the game keys released
|
|
* @param x the mouse x coordinate
|
|
* @param y the mouse y coordinate
|
|
* @param trackPosition the track position
|
|
*/
|
|
private void gameKeyReleased(int keys, int x, int y, int trackPosition) {
|
|
if (!isReplay && keys != ReplayFrame.KEY_NONE && !isLeadIn() && pauseTime == -1) {
|
|
lastKeysPressed &= ~keys; // clear keys bits
|
|
addReplayFrameAndRun(x, y, lastKeysPressed, trackPosition);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void mouseWheelMoved(int newValue) {
|
|
if (Options.isMouseWheelDisabled() || Options.isMouseDisabled())
|
|
return;
|
|
|
|
UI.changeVolume((newValue < 0) ? -1 : 1);
|
|
}
|
|
|
|
@Override
|
|
public void enter(GameContainer container, StateBasedGame game)
|
|
throws SlickException {
|
|
UI.enter();
|
|
|
|
if (osu == null || osu.objects == null)
|
|
throw new RuntimeException("Running game with no OsuFile loaded.");
|
|
|
|
// grab the mouse (not working for touchscreen)
|
|
// container.setMouseGrabbed(true);
|
|
|
|
// restart the game
|
|
if (restart != Restart.FALSE) {
|
|
if (restart == Restart.NEW) {
|
|
// new game
|
|
loadImages();
|
|
setMapModifiers();
|
|
retries = 0;
|
|
} else if (restart == Restart.MANUAL) {
|
|
// retry
|
|
retries++;
|
|
} else if (restart == Restart.REPLAY)
|
|
retries = 0;
|
|
|
|
// reset game data
|
|
resetGameData();
|
|
|
|
// initialize object maps
|
|
for (int i = 0; i < osu.objects.length; i++) {
|
|
OsuHitObject hitObject = osu.objects[i];
|
|
|
|
// is this the last note in the combo?
|
|
boolean comboEnd = false;
|
|
if (i + 1 < osu.objects.length && osu.objects[i + 1].isNewCombo())
|
|
comboEnd = true;
|
|
|
|
Color color = osu.combo[hitObject.getComboIndex()];
|
|
|
|
// pass beatLength to hit objects
|
|
int hitObjectTime = hitObject.getTime();
|
|
int timingPointIndex = 0;
|
|
while (timingPointIndex < osu.timingPoints.size()) {
|
|
OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex);
|
|
if (timingPoint.getTime() > hitObjectTime)
|
|
break;
|
|
setBeatLength(timingPoint);
|
|
timingPointIndex++;
|
|
}
|
|
|
|
try {
|
|
if (hitObject.isCircle())
|
|
hitObjects[i] = new Circle(hitObject, this, data, color, comboEnd);
|
|
else if (hitObject.isSlider())
|
|
hitObjects[i] = new Slider(hitObject, this, data, color, comboEnd);
|
|
else if (hitObject.isSpinner())
|
|
hitObjects[i] = new Spinner(hitObject, this, data);
|
|
} catch (Exception e) {
|
|
// try to handle the error gracefully: substitute in a dummy HitObject
|
|
ErrorHandler.error(String.format("Failed to create %s at index %d:\n%s",
|
|
hitObject.getTypeName(), i, hitObject.toString()), e, true);
|
|
hitObjects[i] = new DummyObject(hitObject);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// stack calculations
|
|
calculateStacks();
|
|
|
|
// load the first timingPoint
|
|
if (!osu.timingPoints.isEmpty()) {
|
|
OsuTimingPoint timingPoint = osu.timingPoints.get(0);
|
|
if (!timingPoint.isInherited()) {
|
|
beatLengthBase = beatLength = timingPoint.getBeatLength();
|
|
HitSound.setDefaultSampleSet(timingPoint.getSampleType());
|
|
SoundController.setSampleVolume(timingPoint.getSampleVolume());
|
|
timingPointIndex++;
|
|
}
|
|
}
|
|
|
|
// unhide cursor for "auto" mod and replays
|
|
if (GameMod.AUTO.isActive() || isReplay)
|
|
UI.showCursor();
|
|
|
|
// load replay frames
|
|
if (isReplay) {
|
|
// load mods
|
|
previousMods = GameMod.getModState();
|
|
GameMod.loadModState(replay.mods);
|
|
|
|
PlaybackSpeed.reset();
|
|
|
|
// load initial data
|
|
replayX = container.getWidth() / 2;
|
|
replayY = container.getHeight() / 2;
|
|
replayKeyPressed = false;
|
|
replaySkipTime = -1;
|
|
for (replayIndex = 0; replayIndex < replay.frames.length; replayIndex++) {
|
|
ReplayFrame frame = replay.frames[replayIndex];
|
|
if (frame.getY() < 0) { // skip time (?)
|
|
if (frame.getTime() >= 0 && replayIndex > 0)
|
|
replaySkipTime = frame.getTime();
|
|
} else if (frame.getTime() == 0) {
|
|
replayX = frame.getScaledX();
|
|
replayY = frame.getScaledY();
|
|
replayKeyPressed = frame.isKeyPressed();
|
|
} else
|
|
break;
|
|
}
|
|
}
|
|
|
|
// initialize replay-recording structures
|
|
else {
|
|
lastKeysPressed = ReplayFrame.KEY_NONE;
|
|
replaySkipTime = -1;
|
|
replayFrames = new LinkedList<ReplayFrame>();
|
|
replayFrames.add(new ReplayFrame(0, 0, input.getMouseX(), input.getMouseY(), 0));
|
|
}
|
|
|
|
leadInTime = osu.audioLeadIn + approachTime;
|
|
restart = Restart.FALSE;
|
|
|
|
// needs to play before setting position to resume without lag later
|
|
MusicController.play(false);
|
|
MusicController.setPosition(0);
|
|
MusicController.setPitch(GameMod.getSpeedMultiplier());
|
|
MusicController.pause();
|
|
}
|
|
|
|
skipButton.resetHover();
|
|
if (isReplay || GameMod.AUTO.isActive())
|
|
playbackButton.resetHover();
|
|
}
|
|
|
|
@Override
|
|
public void leave(GameContainer container, StateBasedGame game)
|
|
throws SlickException {
|
|
// container.setMouseGrabbed(false);
|
|
|
|
// re-hide cursor
|
|
if (GameMod.AUTO.isActive() || isReplay)
|
|
UI.hideCursor();
|
|
|
|
// replays
|
|
if (isReplay)
|
|
GameMod.loadModState(previousMods);
|
|
|
|
// reset playback speed
|
|
MusicController.setPitch(1f);
|
|
}
|
|
|
|
/**
|
|
* Draws hit objects, hit results, and follow points.
|
|
* @param g the graphics context
|
|
* @param trackPosition the track position
|
|
*/
|
|
private void drawHitObjects(Graphics g, int trackPosition) {
|
|
// include previous object in follow points
|
|
int lastObjectIndex = -1;
|
|
if (objectIndex > 0 && objectIndex < osu.objects.length &&
|
|
trackPosition < osu.objects[objectIndex].getTime() && !osu.objects[objectIndex - 1].isSpinner())
|
|
lastObjectIndex = objectIndex - 1;
|
|
|
|
// draw hit objects in reverse order, or else overlapping objects are unreadable
|
|
Stack<Integer> stack = new Stack<Integer>();
|
|
for (int index = objectIndex; index < hitObjects.length && osu.objects[index].getTime() < trackPosition + approachTime; index++) {
|
|
stack.add(index);
|
|
|
|
// draw follow points
|
|
if (!Options.isFollowPointEnabled())
|
|
continue;
|
|
if (osu.objects[index].isSpinner()) {
|
|
lastObjectIndex = -1;
|
|
continue;
|
|
}
|
|
if (lastObjectIndex != -1 && !osu.objects[index].isNewCombo()) {
|
|
// calculate points
|
|
final int followPointInterval = container.getHeight() / 14;
|
|
int lastObjectEndTime = hitObjects[lastObjectIndex].getEndTime() + 1;
|
|
int objectStartTime = osu.objects[index].getTime();
|
|
float[] startXY = hitObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
|
|
float[] endXY = hitObjects[index].getPointAt(objectStartTime);
|
|
float xDiff = endXY[0] - startXY[0];
|
|
float yDiff = endXY[1] - startXY[1];
|
|
float dist = (float) Math.hypot(xDiff, yDiff);
|
|
int numPoints = (int) ((dist - GameImage.HITCIRCLE.getImage().getWidth()) / followPointInterval);
|
|
if (numPoints > 0) {
|
|
// set the image angle
|
|
Image followPoint = GameImage.FOLLOWPOINT.getImage();
|
|
float angle = (float) Math.toDegrees(Math.atan2(yDiff, xDiff));
|
|
followPoint.setRotation(angle);
|
|
|
|
// draw points
|
|
float progress = 0f, alpha = 1f;
|
|
if (lastObjectIndex < objectIndex)
|
|
progress = (float) (trackPosition - lastObjectEndTime) / (objectStartTime - lastObjectEndTime);
|
|
else {
|
|
alpha = Utils.clamp((1f - ((objectStartTime - trackPosition) / (float) approachTime)) * 2f, 0, 1);
|
|
followPoint.setAlpha(alpha);
|
|
}
|
|
|
|
float step = 1f / (numPoints + 1);
|
|
float t = step;
|
|
for (int i = 0; i < numPoints; i++) {
|
|
float x = startXY[0] + xDiff * t;
|
|
float y = startXY[1] + yDiff * t;
|
|
float nextT = t + step;
|
|
if (lastObjectIndex < objectIndex) { // fade the previous trail
|
|
if (progress < nextT) {
|
|
if (progress > t)
|
|
followPoint.setAlpha(1f - ((progress - t + step) / (step * 2f)));
|
|
else if (progress > t - step)
|
|
followPoint.setAlpha(1f - ((progress - (t - step)) / (step * 2f)));
|
|
else
|
|
followPoint.setAlpha(1f);
|
|
followPoint.draw(x, y);
|
|
}
|
|
} else
|
|
followPoint.draw(x, y);
|
|
t = nextT;
|
|
}
|
|
followPoint.setAlpha(1f);
|
|
}
|
|
}
|
|
lastObjectIndex = index;
|
|
}
|
|
|
|
while (!stack.isEmpty())
|
|
hitObjects[stack.pop()].draw(g, trackPosition);
|
|
|
|
// draw OsuHitObjectResult objects
|
|
data.drawHitResults(trackPosition);
|
|
}
|
|
|
|
/**
|
|
* Loads all required data from an OsuFile.
|
|
* @param osu the OsuFile to load
|
|
*/
|
|
public void loadOsuFile(OsuFile osu) {
|
|
this.osu = osu;
|
|
Display.setTitle(String.format("%s - %s", game.getTitle(), osu.toString()));
|
|
if (osu.timingPoints == null || osu.combo == null)
|
|
OsuDB.load(osu, OsuDB.LOAD_ARRAY);
|
|
OsuParser.parseHitObjects(osu);
|
|
HitSound.setDefaultSampleSet(osu.sampleSet);
|
|
}
|
|
|
|
/**
|
|
* Resets all game data and structures.
|
|
*/
|
|
public void resetGameData() {
|
|
hitObjects = new HitObject[osu.objects.length];
|
|
data.clear();
|
|
objectIndex = 0;
|
|
breakIndex = 0;
|
|
breakTime = 0;
|
|
breakSound = false;
|
|
timingPointIndex = 0;
|
|
beatLengthBase = beatLength = 1;
|
|
pauseTime = -1;
|
|
pausedMouseX = -1;
|
|
pausedMouseY = -1;
|
|
countdownReadySound = false;
|
|
countdown3Sound = false;
|
|
countdown1Sound = false;
|
|
countdown2Sound = false;
|
|
countdownGoSound = false;
|
|
checkpointLoaded = false;
|
|
deaths = 0;
|
|
deathTime = -1;
|
|
replayFrames = null;
|
|
lastReplayTime = 0;
|
|
autoMouseX = 0;
|
|
autoMouseY = 0;
|
|
autoMousePressed = false;
|
|
flashlightRadius = container.getHeight() * 2 / 3;
|
|
|
|
System.gc();
|
|
}
|
|
|
|
/**
|
|
* Skips the beginning of a track.
|
|
* @return true if skipped, false otherwise
|
|
*/
|
|
private synchronized boolean skipIntro() {
|
|
int firstObjectTime = osu.objects[0].getTime();
|
|
int trackPosition = MusicController.getPosition();
|
|
if (objectIndex == 0 && trackPosition < firstObjectTime - SKIP_OFFSET) {
|
|
if (isLeadIn()) {
|
|
leadInTime = 0;
|
|
MusicController.resume();
|
|
}
|
|
MusicController.setPosition(firstObjectTime - SKIP_OFFSET);
|
|
MusicController.setPitch(GameMod.getSpeedMultiplier());
|
|
replaySkipTime = (isReplay) ? -1 : trackPosition;
|
|
if (isReplay) {
|
|
replayX = (int) skipButton.getX();
|
|
replayY = (int) skipButton.getY();
|
|
}
|
|
SoundController.playSound(SoundEffect.MENUHIT);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Loads all game images.
|
|
*/
|
|
private void loadImages() {
|
|
int width = container.getWidth();
|
|
int height = container.getHeight();
|
|
|
|
// set images
|
|
File parent = osu.getFile().getParentFile();
|
|
for (GameImage img : GameImage.values()) {
|
|
if (img.isSkinnable()) {
|
|
img.setDefaultImage();
|
|
img.setSkinImage(parent);
|
|
}
|
|
}
|
|
|
|
// skip button
|
|
if (GameImage.SKIP.getImages() != null) {
|
|
Animation skip = GameImage.SKIP.getAnimation(120);
|
|
skipButton = new MenuButton(skip, width - skip.getWidth() / 2f, height - (skip.getHeight() / 2f));
|
|
} else {
|
|
Image skip = GameImage.SKIP.getImage();
|
|
skipButton = new MenuButton(skip, width - skip.getWidth() / 2f, height - (skip.getHeight() / 2f));
|
|
}
|
|
skipButton.setHoverExpand(1.1f, MenuButton.Expand.UP_LEFT);
|
|
|
|
if (isReplay || GameMod.AUTO.isActive())
|
|
playbackButton = PlaybackSpeed.NORMAL.getButton();
|
|
|
|
// load other images...
|
|
((GamePauseMenu) game.getState(Opsu.STATE_GAMEPAUSEMENU)).loadImages();
|
|
data.loadImages();
|
|
}
|
|
|
|
/**
|
|
* Set map modifiers.
|
|
*/
|
|
private void setMapModifiers() {
|
|
// map-based properties, re-initialized each game
|
|
float circleSize = osu.circleSize;
|
|
float approachRate = osu.approachRate;
|
|
float overallDifficulty = osu.overallDifficulty;
|
|
float HPDrainRate = osu.HPDrainRate;
|
|
|
|
// "Hard Rock" modifiers
|
|
if (GameMod.HARD_ROCK.isActive()) {
|
|
circleSize = Math.min(circleSize * 1.4f, 10);
|
|
approachRate = Math.min(approachRate * 1.4f, 10);
|
|
overallDifficulty = Math.min(overallDifficulty * 1.4f, 10);
|
|
HPDrainRate = Math.min(HPDrainRate * 1.4f, 10);
|
|
}
|
|
|
|
// "Easy" modifiers
|
|
else if (GameMod.EASY.isActive()) {
|
|
circleSize /= 2f;
|
|
approachRate /= 2f;
|
|
overallDifficulty /= 2f;
|
|
HPDrainRate /= 2f;
|
|
}
|
|
|
|
// fixed difficulty overrides
|
|
if (Options.getFixedCS() > 0f)
|
|
circleSize = Options.getFixedCS();
|
|
if (Options.getFixedAR() > 0f)
|
|
approachRate = Options.getFixedAR();
|
|
if (Options.getFixedOD() > 0f)
|
|
overallDifficulty = Options.getFixedOD();
|
|
if (Options.getFixedHP() > 0f)
|
|
HPDrainRate = Options.getFixedHP();
|
|
|
|
// Stack modifier scales with hit object size
|
|
// StackOffset = HitObjectRadius / 10
|
|
int diameter = (int) (104 - (circleSize * 8));
|
|
OsuHitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER);
|
|
|
|
// initialize objects
|
|
Circle.init(container, circleSize);
|
|
Slider.init(container, circleSize, osu);
|
|
Spinner.init(container);
|
|
|
|
// approachRate (hit object approach time)
|
|
if (approachRate < 5)
|
|
approachTime = (int) (1800 - (approachRate * 120));
|
|
else
|
|
approachTime = (int) (1200 - ((approachRate - 5) * 150));
|
|
|
|
// overallDifficulty (hit result time offsets)
|
|
hitResultOffset = new int[GameData.HIT_MAX];
|
|
hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6));
|
|
hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8));
|
|
hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10));
|
|
hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10));
|
|
|
|
// HPDrainRate (health change), overallDifficulty (scoring)
|
|
data.setDrainRate(HPDrainRate);
|
|
data.setDifficulty(overallDifficulty);
|
|
data.setHitResultOffset(hitResultOffset);
|
|
}
|
|
|
|
/**
|
|
* Sets/returns whether entering the state will restart it.
|
|
*/
|
|
public void setRestart(Restart restart) { this.restart = restart; }
|
|
public Restart getRestart() { return restart; }
|
|
|
|
/**
|
|
* Returns whether or not the track is in the lead-in time state.
|
|
*/
|
|
public boolean isLeadIn() { return leadInTime > 0; }
|
|
|
|
/**
|
|
* Returns the object approach time, in milliseconds.
|
|
*/
|
|
public int getApproachTime() { return approachTime; }
|
|
|
|
/**
|
|
* Returns an array of hit result offset times, in milliseconds (indexed by GameData.HIT_* constants).
|
|
*/
|
|
public int[] getHitResultOffsets() { return hitResultOffset; }
|
|
|
|
/**
|
|
* Returns the beat length.
|
|
*/
|
|
public float getBeatLength() { return beatLength; }
|
|
|
|
/**
|
|
* Sets the beat length fields based on a given timing point.
|
|
*/
|
|
private void setBeatLength(OsuTimingPoint timingPoint) {
|
|
if (!timingPoint.isInherited())
|
|
beatLengthBase = beatLength = timingPoint.getBeatLength();
|
|
else
|
|
beatLength = beatLengthBase * timingPoint.getSliderMultiplier();
|
|
}
|
|
|
|
/**
|
|
* Returns the slider multiplier given by the current timing point.
|
|
*/
|
|
public float getTimingPointMultiplier() { return beatLength / beatLengthBase; }
|
|
|
|
/**
|
|
* Sets a replay to view, or resets the replay if null.
|
|
* @param replay the replay
|
|
*/
|
|
public void setReplay(Replay replay) {
|
|
if (replay == null) {
|
|
this.isReplay = false;
|
|
this.replay = null;
|
|
} else {
|
|
if (replay.frames == null) {
|
|
ErrorHandler.error("Attempting to set a replay with no frames.", null, false);
|
|
return;
|
|
}
|
|
this.isReplay = true;
|
|
this.replay = replay;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a replay frame to the list, if possible, and runs it.
|
|
* @param x the cursor x coordinate
|
|
* @param y the cursor y coordinate
|
|
* @param keys the keys pressed
|
|
* @param time the time of the replay Frame
|
|
*/
|
|
public synchronized void addReplayFrameAndRun(int x, int y, int keys, int time){
|
|
// "auto" and "autopilot" mods: use automatic cursor coordinates
|
|
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
|
x = autoMouseX;
|
|
y = autoMouseY;
|
|
}
|
|
|
|
ReplayFrame frame = addReplayFrame(x, y, keys, time);
|
|
if (frame != null)
|
|
runReplayFrame(frame);
|
|
}
|
|
|
|
/**
|
|
* Runs a replay frame.
|
|
* @param frame the frame to run
|
|
*/
|
|
private void runReplayFrame(ReplayFrame frame){
|
|
int keys = frame.getKeys();
|
|
int replayX = frame.getScaledX();
|
|
int replayY = frame.getScaledY();
|
|
int deltaKeys = (keys & ~lastReplayKeys); // keys that turned on
|
|
if (deltaKeys != ReplayFrame.KEY_NONE) // send a key press
|
|
sendGameKeyPress(deltaKeys, replayX, replayY, frame.getTime());
|
|
else if (keys != lastReplayKeys)
|
|
; // do nothing
|
|
else
|
|
updateGame(replayX, replayY, frame.getTimeDiff(), frame.getTime(), keys);
|
|
lastReplayKeys = keys;
|
|
}
|
|
|
|
/**
|
|
* Sends a game key press and updates the hit objects.
|
|
* @param trackPosition the track position
|
|
* @param x the cursor x coordinate
|
|
* @param y the cursor y coordinate
|
|
* @param keys the keys that are pressed
|
|
*/
|
|
private void sendGameKeyPress(int keys, int x, int y, int trackPosition) {
|
|
if (objectIndex >= hitObjects.length) // nothing to do here
|
|
return;
|
|
|
|
OsuHitObject hitObject = osu.objects[objectIndex];
|
|
|
|
// circles
|
|
if (hitObject.isCircle() && hitObjects[objectIndex].mousePressed(x, y, trackPosition))
|
|
objectIndex++; // circle hit
|
|
|
|
// sliders
|
|
else if (hitObject.isSlider())
|
|
hitObjects[objectIndex].mousePressed(x, y, trackPosition);
|
|
}
|
|
|
|
/**
|
|
* Adds a replay frame to the list, if possible.
|
|
* @param x the cursor x coordinate
|
|
* @param y the cursor y coordinate
|
|
* @param keys the keys pressed
|
|
* @param time the time of the replay frame
|
|
* @return a ReplayFrame representing the data
|
|
*/
|
|
private ReplayFrame addReplayFrame(int x, int y, int keys, int time) {
|
|
int timeDiff = time - lastReplayTime;
|
|
lastReplayTime = time;
|
|
int cx = (int) ((x - OsuHitObject.getXOffset()) / OsuHitObject.getXMultiplier());
|
|
int cy = (int) ((y - OsuHitObject.getYOffset()) / OsuHitObject.getYMultiplier());
|
|
ReplayFrame frame = new ReplayFrame(timeDiff, time, cx, cy, keys);
|
|
if (replayFrames != null)
|
|
replayFrames.add(frame);
|
|
return frame;
|
|
}
|
|
|
|
/**
|
|
* Returns the point at the t value between a start and end point.
|
|
* @param startX the starting x coordinate
|
|
* @param startY the starting y coordinate
|
|
* @param endX the ending x coordinate
|
|
* @param endY the ending y coordinate
|
|
* @param t the t value [0, 1]
|
|
* @return the [x,y] coordinates
|
|
*/
|
|
private float[] getPointAt(float startX, float startY, float endX, float endY, float t) {
|
|
// "autopilot" mod: move quicker between objects
|
|
if (GameMod.AUTOPILOT.isActive())
|
|
t = Utils.clamp(t * 2f, 0f, 1f);
|
|
|
|
float[] xy = new float[2];
|
|
xy[0] = startX + (endX - startX) * t;
|
|
xy[1] = startY + (endY - startY) * t;
|
|
return xy;
|
|
}
|
|
|
|
/**
|
|
* Updates the current visible area radius (if the "flashlight" mod is enabled).
|
|
* @param delta the delta interval
|
|
* @param trackPosition the track position
|
|
*/
|
|
private void updateFlashlightRadius(int delta, int trackPosition) {
|
|
if (!GameMod.FLASHLIGHT.isActive())
|
|
return;
|
|
|
|
int width = container.getWidth(), height = container.getHeight();
|
|
boolean firstObject = (objectIndex == 0 && trackPosition < osu.objects[0].getTime());
|
|
if (isLeadIn()) {
|
|
// lead-in: expand area
|
|
float progress = Math.max((float) (leadInTime - osu.audioLeadIn) / approachTime, 0f);
|
|
flashlightRadius = width - (int) ((width - (height * 2 / 3)) * progress);
|
|
} else if (firstObject) {
|
|
// before first object: shrink area
|
|
int timeDiff = osu.objects[0].getTime() - trackPosition;
|
|
flashlightRadius = width;
|
|
if (timeDiff < approachTime) {
|
|
float progress = (float) timeDiff / approachTime;
|
|
flashlightRadius -= (width - (height * 2 / 3)) * (1 - progress);
|
|
}
|
|
} else {
|
|
// gameplay: size based on combo
|
|
int targetRadius;
|
|
int combo = data.getComboStreak();
|
|
if (combo < 100)
|
|
targetRadius = height * 2 / 3;
|
|
else if (combo < 200)
|
|
targetRadius = height / 2;
|
|
else
|
|
targetRadius = height / 3;
|
|
if (osu.breaks != null && breakIndex < osu.breaks.size() && breakTime > 0) {
|
|
// breaks: expand at beginning, shrink at end
|
|
flashlightRadius = targetRadius;
|
|
int endTime = osu.breaks.get(breakIndex);
|
|
int breakLength = endTime - breakTime;
|
|
if (breakLength > approachTime * 3) {
|
|
float progress = 1f;
|
|
if (trackPosition - breakTime < approachTime)
|
|
progress = (float) (trackPosition - breakTime) / approachTime;
|
|
else if (endTime - trackPosition < approachTime)
|
|
progress = (float) (endTime - trackPosition) / approachTime;
|
|
flashlightRadius += (width - flashlightRadius) * progress;
|
|
}
|
|
} else if (flashlightRadius != targetRadius) {
|
|
// radius size change
|
|
float radiusDiff = height * delta / 2000f;
|
|
if (flashlightRadius > targetRadius) {
|
|
flashlightRadius -= radiusDiff;
|
|
if (flashlightRadius < targetRadius)
|
|
flashlightRadius = targetRadius;
|
|
} else {
|
|
flashlightRadius += radiusDiff;
|
|
if (flashlightRadius > targetRadius)
|
|
flashlightRadius = targetRadius;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Performs stacking calculations on all hit objects, and updates their
|
|
* positions if necessary.
|
|
* @author peppy (https://gist.github.com/peppy/1167470)
|
|
*/
|
|
private void calculateStacks() {
|
|
// reverse pass for stack calculation
|
|
for (int i = hitObjects.length - 1; i > 0; i--) {
|
|
OsuHitObject hitObjectI = osu.objects[i];
|
|
|
|
// already calculated
|
|
if (hitObjectI.getStack() != 0 || hitObjectI.isSpinner())
|
|
continue;
|
|
|
|
// search for hit objects in stack
|
|
for (int n = i - 1; n >= 0; n--) {
|
|
OsuHitObject hitObjectN = osu.objects[n];
|
|
if (hitObjectN.isSpinner())
|
|
continue;
|
|
|
|
// check if in range stack calculation
|
|
float timeI = hitObjectI.getTime() - (STACK_TIMEOUT * osu.stackLeniency);
|
|
float timeN = hitObjectN.isSlider() ? hitObjects[n].getEndTime() : hitObjectN.getTime();
|
|
if (timeI > timeN)
|
|
break;
|
|
|
|
// possible special case: if slider end in the stack,
|
|
// all next hit objects in stack move right down
|
|
if (hitObjectN.isSlider()) {
|
|
float[] p1 = hitObjects[i].getPointAt(hitObjectI.getTime());
|
|
float[] p2 = hitObjects[n].getPointAt(hitObjects[n].getEndTime());
|
|
float distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
|
|
|
|
// check if hit object part of this stack
|
|
if (distance < STACK_LENIENCE * OsuHitObject.getXMultiplier()) {
|
|
int offset = hitObjectI.getStack() - hitObjectN.getStack() + 1;
|
|
for (int j = n + 1; j <= i; j++) {
|
|
OsuHitObject hitObjectJ = osu.objects[j];
|
|
p1 = hitObjects[j].getPointAt(hitObjectJ.getTime());
|
|
distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
|
|
|
|
// hit object below slider end
|
|
if (distance < STACK_LENIENCE * OsuHitObject.getXMultiplier())
|
|
hitObjectJ.setStack(hitObjectJ.getStack() - offset);
|
|
}
|
|
break; // slider end always start of the stack: reset calculation
|
|
}
|
|
}
|
|
|
|
// not a special case: stack moves up left
|
|
float distance = Utils.distance(
|
|
hitObjectI.getX(), hitObjectI.getY(),
|
|
hitObjectN.getX(), hitObjectN.getY()
|
|
);
|
|
if (distance < STACK_LENIENCE) {
|
|
hitObjectN.setStack(hitObjectI.getStack() + 1);
|
|
hitObjectI = hitObjectN;
|
|
}
|
|
}
|
|
}
|
|
|
|
// update hit object positions
|
|
for (int i = 0; i < hitObjects.length; i++) {
|
|
if (osu.objects[i].getStack() != 0)
|
|
hitObjects[i].updatePosition();
|
|
}
|
|
}
|
|
}
|