2014-06-30 04:17:04 +02:00
|
|
|
/*
|
|
|
|
* opsu! - an open-source osu! client
|
|
|
|
* Copyright (C) 2014 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.GUIMenuButton;
|
|
|
|
import itdelatrisu.opsu.GameScore;
|
|
|
|
import itdelatrisu.opsu.MusicController;
|
|
|
|
import itdelatrisu.opsu.Opsu;
|
|
|
|
import itdelatrisu.opsu.OsuFile;
|
|
|
|
import itdelatrisu.opsu.OsuHitObject;
|
|
|
|
import itdelatrisu.opsu.OsuTimingPoint;
|
2014-07-01 07:14:03 +02:00
|
|
|
import itdelatrisu.opsu.SoundController;
|
2014-07-02 01:32:03 +02:00
|
|
|
import itdelatrisu.opsu.Utils;
|
2014-06-30 04:17:04 +02:00
|
|
|
import itdelatrisu.opsu.objects.Circle;
|
|
|
|
import itdelatrisu.opsu.objects.Slider;
|
|
|
|
import itdelatrisu.opsu.objects.Spinner;
|
|
|
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
import java.util.Stack;
|
|
|
|
|
|
|
|
import org.lwjgl.input.Keyboard;
|
|
|
|
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;
|
|
|
|
import org.newdawn.slick.util.Log;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* "Game" state.
|
|
|
|
*/
|
|
|
|
public class Game extends BasicGameState {
|
|
|
|
/**
|
|
|
|
* Game restart states.
|
|
|
|
*/
|
|
|
|
public static final byte
|
|
|
|
RESTART_FALSE = 0,
|
|
|
|
RESTART_NEW = 1, // first time loading song
|
|
|
|
RESTART_MANUAL = 2, // retry
|
|
|
|
RESTART_LOSE = 3; // health is zero: no-continue/force restart
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Current restart state.
|
|
|
|
*/
|
|
|
|
private static byte restart;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The associated OsuFile object.
|
|
|
|
*/
|
|
|
|
private static OsuFile osu;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The associated GameScore object (holds all score data).
|
|
|
|
*/
|
|
|
|
private static GameScore score;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Current hit object index in OsuHitObject[] array.
|
|
|
|
*/
|
|
|
|
private int objectIndex = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This map's hit circles objects, keyed by objectIndex.
|
|
|
|
*/
|
|
|
|
private HashMap<Integer, Circle> circles;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This map's slider objects, keyed by objectIndex.
|
|
|
|
*/
|
|
|
|
private HashMap<Integer, Slider> sliders;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This map's spinner objects, keyed by objectIndex.
|
|
|
|
*/
|
|
|
|
private HashMap<Integer, Spinner> spinners;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delay time, in milliseconds, before song starts.
|
|
|
|
*/
|
|
|
|
private static 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;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Time, in milliseconds, between the first and last hit object.
|
|
|
|
*/
|
|
|
|
private int mapLength;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Current break index in breaks ArrayList.
|
|
|
|
*/
|
|
|
|
private int breakIndex;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Warning arrows, pointing right and left.
|
|
|
|
*/
|
|
|
|
private Image warningArrowR, warningArrowL;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Section pass and fail images (displayed at start of break, when necessary).
|
|
|
|
*/
|
|
|
|
private Image breakStartPass, breakStartFail;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Break start time (0 if not in break).
|
|
|
|
*/
|
|
|
|
private int breakTime = 0;
|
|
|
|
|
2014-07-01 07:14:03 +02:00
|
|
|
/**
|
|
|
|
* Whether the break sound has been played.
|
|
|
|
*/
|
|
|
|
private boolean breakSound;
|
|
|
|
|
2014-06-30 04:17:04 +02:00
|
|
|
/**
|
|
|
|
* Skip button (displayed at song start, when necessary).
|
|
|
|
*/
|
|
|
|
private GUIMenuButton skipButton;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Minimum time before start of song, in milliseconds, to process skip-related actions.
|
|
|
|
*/
|
|
|
|
private final int skipOffsetTime = 2000;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Current timing point index in timingPoints ArrayList.
|
|
|
|
*/
|
|
|
|
private int timingPointIndex;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Current beat lengths (base value and inherited value).
|
|
|
|
*/
|
|
|
|
private float beatLengthBase, beatLength;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Countdown-related images.
|
|
|
|
*/
|
|
|
|
private Image
|
|
|
|
countdownReady, // "READY?" text
|
|
|
|
countdown3, // "3" text
|
|
|
|
countdown1, // "2" text
|
|
|
|
countdown2, // "1" text
|
|
|
|
countdownGo; // "GO!" text
|
|
|
|
|
2014-07-01 07:14:03 +02:00
|
|
|
/**
|
|
|
|
* Whether the countdown sound has been played.
|
|
|
|
*/
|
|
|
|
private boolean
|
|
|
|
countdownReadySound, countdown3Sound, countdown1Sound,
|
|
|
|
countdown2Sound, countdownGoSound;
|
|
|
|
|
2014-06-30 04:17:04 +02:00
|
|
|
/**
|
|
|
|
* Glowing hit circle outline which must be clicked when returning from pause menu.
|
|
|
|
*/
|
|
|
|
private Image hitCircleSelect;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
// spinners have fixed properties, and only need to be initialized once
|
|
|
|
Spinner.init(container);
|
|
|
|
|
|
|
|
// breaks
|
|
|
|
breakStartPass = new Image("section-pass.png");
|
|
|
|
breakStartFail = new Image("section-fail.png");
|
|
|
|
warningArrowR = new Image("play-warningarrow.png");
|
|
|
|
warningArrowL = warningArrowR.getFlippedCopy(true, false);
|
|
|
|
|
|
|
|
// skip button
|
|
|
|
Image skip = new Image("play-skip.png");
|
|
|
|
float skipScale = (height * 0.1f) / skip.getHeight();
|
|
|
|
skip = skip.getScaledCopy(skipScale);
|
|
|
|
skipButton = new GUIMenuButton(skip,
|
|
|
|
width - (skip.getWidth() / 2f),
|
|
|
|
height - (skip.getHeight() / 2f));
|
|
|
|
|
|
|
|
// countdown
|
|
|
|
float countdownHeight = height / 3f;
|
|
|
|
countdownReady = new Image("ready.png");
|
|
|
|
countdownReady = countdownReady.getScaledCopy(countdownHeight / countdownReady.getHeight());
|
|
|
|
countdown3 = new Image("count3.png");
|
|
|
|
countdown3 = countdown3.getScaledCopy(countdownHeight / countdown3.getHeight());
|
|
|
|
countdown2 = new Image("count2.png");
|
|
|
|
countdown2 = countdown2.getScaledCopy(countdownHeight / countdown2.getHeight());
|
|
|
|
countdown1 = new Image("count1.png");
|
|
|
|
countdown1 = countdown1.getScaledCopy(countdownHeight / countdown1.getHeight());
|
|
|
|
countdownGo = new Image("go.png");
|
|
|
|
countdownGo = countdownGo.getScaledCopy(countdownHeight / countdownGo.getHeight());
|
|
|
|
|
|
|
|
// hit circle select
|
|
|
|
hitCircleSelect = new Image("hitcircleselect.png");
|
|
|
|
|
|
|
|
// create the associated GameScore object
|
|
|
|
score = new GameScore(width, height);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
|
|
|
throws SlickException {
|
|
|
|
int width = container.getWidth();
|
|
|
|
int height = container.getHeight();
|
|
|
|
|
|
|
|
// background
|
2014-07-03 00:24:19 +02:00
|
|
|
g.setBackground(Color.black);
|
|
|
|
osu.drawBG(width, height, Options.getBackgroundDim());
|
2014-06-30 04:17:04 +02:00
|
|
|
|
|
|
|
int trackPosition = MusicController.getPosition();
|
|
|
|
if (pauseTime > -1) // returning from pause screen
|
|
|
|
trackPosition = pauseTime;
|
|
|
|
|
|
|
|
// break periods
|
|
|
|
if (osu.breaks != null && breakIndex < osu.breaks.size()) {
|
|
|
|
if (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);
|
|
|
|
}
|
|
|
|
|
|
|
|
score.drawGameElements(g, mapLength, true, objectIndex == 0);
|
|
|
|
|
|
|
|
if (breakLength >= 8000 &&
|
|
|
|
trackPosition - breakTime > 2000 &&
|
|
|
|
trackPosition - breakTime < 5000) {
|
|
|
|
// show break start
|
2014-07-01 07:14:03 +02:00
|
|
|
if (score.getHealth() >= 50) {
|
2014-06-30 04:17:04 +02:00
|
|
|
breakStartPass.drawCentered(width / 2f, height / 2f);
|
2014-07-01 07:14:03 +02:00
|
|
|
if (!breakSound) {
|
|
|
|
SoundController.playSound(SoundController.SOUND_SECTIONPASS);
|
|
|
|
breakSound = true;
|
|
|
|
}
|
|
|
|
} else {
|
2014-06-30 04:17:04 +02:00
|
|
|
breakStartFail.drawCentered(width / 2f, height / 2f);
|
2014-07-01 07:14:03 +02:00
|
|
|
if (!breakSound) {
|
|
|
|
SoundController.playSound(SoundController.SOUND_SECTIONFAIL);
|
|
|
|
breakSound = true;
|
|
|
|
}
|
|
|
|
}
|
2014-06-30 04:17:04 +02:00
|
|
|
} else if (breakLength >= 4000) {
|
|
|
|
// show break end (flash twice for 500ms)
|
|
|
|
int endTimeDiff = endTime - trackPosition;
|
|
|
|
if ((endTimeDiff > 1500 && endTimeDiff < 2000) ||
|
|
|
|
(endTimeDiff > 500 && endTimeDiff < 1000)) {
|
|
|
|
warningArrowR.draw(width * 0.15f, height * 0.15f);
|
|
|
|
warningArrowR.draw(width * 0.15f, height * 0.75f);
|
|
|
|
warningArrowL.draw(width * 0.75f, height * 0.15f);
|
|
|
|
warningArrowL.draw(width * 0.75f, height * 0.75f);
|
|
|
|
}
|
|
|
|
}
|
2014-07-02 01:32:03 +02:00
|
|
|
|
|
|
|
Utils.drawFPS();
|
|
|
|
Utils.drawCursor();
|
2014-06-30 04:17:04 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// game elements
|
|
|
|
score.drawGameElements(g, mapLength, false, objectIndex == 0);
|
|
|
|
|
|
|
|
// first object...
|
|
|
|
if (objectIndex == 0) {
|
|
|
|
// skip beginning
|
|
|
|
if (osu.objects[objectIndex].time - skipOffsetTime > 5000 &&
|
|
|
|
trackPosition < osu.objects[objectIndex].time - skipOffsetTime)
|
|
|
|
skipButton.draw();
|
|
|
|
|
|
|
|
// mod icons
|
|
|
|
if (trackPosition < osu.objects[objectIndex].time) {
|
|
|
|
for (int i = Options.MOD_MAX - 1; i >= 0; i--) {
|
|
|
|
if (Options.isModActive(i)) {
|
|
|
|
Image modImage = Options.getModImage(i);
|
|
|
|
modImage.draw(
|
|
|
|
(width * 0.85f) + ((i - (Options.MOD_MAX / 2)) * modImage.getWidth() / 3f),
|
|
|
|
height / 10f
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isLeadIn())
|
|
|
|
trackPosition = leadInTime * -1; // render approach circles during song lead-in
|
|
|
|
|
|
|
|
// countdown
|
|
|
|
if (osu.countdown > 0) { // TODO: implement half/double rate settings
|
|
|
|
int timeDiff = osu.objects[0].time - trackPosition;
|
|
|
|
if (timeDiff >= 500 && timeDiff < 3000) {
|
2014-07-01 07:14:03 +02:00
|
|
|
if (timeDiff >= 1500) {
|
2014-06-30 04:17:04 +02:00
|
|
|
countdownReady.drawCentered(width / 2, height / 2);
|
2014-07-01 07:14:03 +02:00
|
|
|
if (!countdownReadySound) {
|
|
|
|
SoundController.playSound(SoundController.SOUND_READY);
|
|
|
|
countdownReadySound = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (timeDiff < 2000) {
|
2014-06-30 04:17:04 +02:00
|
|
|
countdown3.draw(0, 0);
|
2014-07-01 07:14:03 +02:00
|
|
|
if (!countdown3Sound) {
|
|
|
|
SoundController.playSound(SoundController.SOUND_COUNT3);
|
|
|
|
countdown3Sound = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (timeDiff < 1500) {
|
2014-06-30 04:17:04 +02:00
|
|
|
countdown2.draw(width - countdown2.getWidth(), 0);
|
2014-07-01 07:14:03 +02:00
|
|
|
if (!countdown2Sound) {
|
|
|
|
SoundController.playSound(SoundController.SOUND_COUNT2);
|
|
|
|
countdown2Sound = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (timeDiff < 1000) {
|
2014-06-30 04:17:04 +02:00
|
|
|
countdown1.drawCentered(width / 2, height / 2);
|
2014-07-01 07:14:03 +02:00
|
|
|
if (!countdown1Sound) {
|
|
|
|
SoundController.playSound(SoundController.SOUND_COUNT1);
|
|
|
|
countdown1Sound = true;
|
|
|
|
}
|
|
|
|
}
|
2014-06-30 04:17:04 +02:00
|
|
|
} else if (timeDiff >= -500 && timeDiff < 500) {
|
|
|
|
countdownGo.setAlpha((timeDiff < 0) ? 1 - (timeDiff / -1000f) : 1);
|
|
|
|
countdownGo.drawCentered(width / 2, height / 2);
|
2014-07-01 07:14:03 +02:00
|
|
|
if (!countdownGoSound) {
|
|
|
|
SoundController.playSound(SoundController.SOUND_GO);
|
|
|
|
countdownGoSound = true;
|
|
|
|
}
|
2014-06-30 04:17:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// draw hit objects in reverse order, or else overlapping objects are unreadable
|
|
|
|
Stack<Integer> stack = new Stack<Integer>();
|
|
|
|
for (int i = objectIndex; i < osu.objects.length && osu.objects[i].time < trackPosition + approachTime; i++)
|
|
|
|
stack.add(i);
|
|
|
|
|
|
|
|
while (!stack.isEmpty()) {
|
|
|
|
int i = stack.pop();
|
|
|
|
OsuHitObject hitObject = osu.objects[i];
|
|
|
|
|
|
|
|
if ((hitObject.type & OsuHitObject.TYPE_CIRCLE) > 0)
|
|
|
|
circles.get(i).draw(trackPosition);
|
|
|
|
else if ((hitObject.type & OsuHitObject.TYPE_SLIDER) > 0)
|
|
|
|
sliders.get(i).draw(trackPosition, stack.isEmpty());
|
|
|
|
else if ((hitObject.type & OsuHitObject.TYPE_SPINNER) > 0) {
|
|
|
|
if (stack.isEmpty()) // only draw spinner at objectIndex
|
|
|
|
spinners.get(i).draw(trackPosition, g);
|
|
|
|
else
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// draw OsuHitObjectResult objects
|
|
|
|
score.drawHitResults(trackPosition);
|
|
|
|
|
|
|
|
// returning from pause screen
|
|
|
|
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
|
|
|
|
// darken the screen
|
2014-07-02 01:32:03 +02:00
|
|
|
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
2014-06-30 04:17:04 +02:00
|
|
|
g.fillRect(0, 0, width, height);
|
|
|
|
|
|
|
|
// draw glowing hit select circle and pulse effect
|
|
|
|
int circleRadius = Circle.getHitCircle().getWidth();
|
|
|
|
Image cursorCircle = hitCircleSelect.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);
|
|
|
|
}
|
2014-07-02 01:32:03 +02:00
|
|
|
|
|
|
|
Utils.drawFPS();
|
|
|
|
Utils.drawCursor();
|
2014-06-30 04:17:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void update(GameContainer container, StateBasedGame game, int delta)
|
|
|
|
throws SlickException {
|
|
|
|
if (isLeadIn()) { // stop updating during song lead-in
|
|
|
|
leadInTime -= delta;
|
|
|
|
if (!isLeadIn())
|
|
|
|
MusicController.playAt(0, false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// returning from pause screen: must click previous mouse position
|
|
|
|
if (pauseTime > -1) {
|
|
|
|
// paused during lead-in or break: continue immediately
|
|
|
|
if (pausedMouseX < 0 && pausedMouseY < 0) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// map complete!
|
|
|
|
if (objectIndex >= osu.objects.length) {
|
|
|
|
game.enterState(Opsu.STATE_GAMERANKING, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
int trackPosition = MusicController.getPosition();
|
|
|
|
|
|
|
|
// timing points
|
|
|
|
if (timingPointIndex < osu.timingPoints.size()) {
|
|
|
|
OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex);
|
|
|
|
if (trackPosition >= timingPoint.time) {
|
|
|
|
if (timingPoint.velocity >= 0)
|
|
|
|
beatLengthBase = beatLength = timingPoint.beatLength;
|
|
|
|
else
|
|
|
|
beatLength = beatLengthBase * (timingPoint.velocity / -100f);
|
2014-07-01 07:14:03 +02:00
|
|
|
SoundController.setSampleSet(timingPoint.sampleType);
|
|
|
|
SoundController.setSampleVolume(timingPoint.sampleVolume);
|
2014-06-30 04:17:04 +02:00
|
|
|
timingPointIndex++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// song beginning
|
|
|
|
if (objectIndex == 0) {
|
|
|
|
if (trackPosition < osu.objects[0].time)
|
|
|
|
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;
|
2014-07-01 07:14:03 +02:00
|
|
|
breakSound = false;
|
2014-06-30 04:17:04 +02:00
|
|
|
breakIndex++;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// pause game if focus lost
|
|
|
|
if (!container.hasFocus() && !Options.isModActive(Options.MOD_AUTO)) {
|
|
|
|
if (pauseTime < 0) {
|
|
|
|
pausedMouseX = input.getMouseX();
|
|
|
|
pausedMouseY = input.getMouseY();
|
|
|
|
pausePulse = 0f;
|
|
|
|
}
|
|
|
|
if (MusicController.isPlaying() || isLeadIn())
|
|
|
|
pauseTime = trackPosition;
|
|
|
|
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
|
|
|
}
|
|
|
|
|
|
|
|
// drain health
|
|
|
|
score.changeHealth(delta / -200f);
|
|
|
|
if (!score.isAlive()) {
|
|
|
|
// game over, force a restart
|
|
|
|
restart = RESTART_LOSE;
|
|
|
|
game.enterState(Opsu.STATE_GAMEPAUSEMENU);
|
|
|
|
}
|
|
|
|
|
|
|
|
score.updateComboBurst(delta);
|
|
|
|
|
|
|
|
// update objects (loop in unlikely event of any skipped indexes)
|
|
|
|
while (objectIndex < osu.objects.length && trackPosition > osu.objects[objectIndex].time) {
|
|
|
|
OsuHitObject hitObject = osu.objects[objectIndex];
|
|
|
|
|
|
|
|
// check if we've already passed the next object's start time
|
|
|
|
boolean overlap = (objectIndex + 1 < osu.objects.length &&
|
|
|
|
trackPosition > osu.objects[objectIndex + 1].time - hitResultOffset[GameScore.HIT_300]);
|
|
|
|
|
|
|
|
// check completion status of the hit object
|
|
|
|
boolean done = false;
|
|
|
|
if ((hitObject.type & OsuHitObject.TYPE_CIRCLE) > 0)
|
|
|
|
done = circles.get(objectIndex).update(overlap);
|
|
|
|
else if ((hitObject.type & OsuHitObject.TYPE_SLIDER) > 0)
|
|
|
|
done = sliders.get(objectIndex).update(overlap, delta, input.getMouseX(), input.getMouseY());
|
|
|
|
else if ((hitObject.type & OsuHitObject.TYPE_SPINNER) > 0)
|
|
|
|
done = spinners.get(objectIndex).update(overlap, delta, input.getMouseX(), input.getMouseY());
|
|
|
|
|
|
|
|
// increment object index?
|
|
|
|
if (done)
|
|
|
|
objectIndex++;
|
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getID() { return state; }
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void keyPressed(int key, char c) {
|
|
|
|
switch (key) {
|
|
|
|
case Input.KEY_ESCAPE:
|
|
|
|
// pause game
|
|
|
|
int trackPosition = MusicController.getPosition();
|
|
|
|
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= osu.objects[0].time) {
|
|
|
|
pausedMouseX = input.getMouseX();
|
|
|
|
pausedMouseY = input.getMouseY();
|
|
|
|
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
|
|
|
|
skipIntro();
|
|
|
|
break;
|
|
|
|
case Input.KEY_Z:
|
|
|
|
// left-click
|
|
|
|
if (!Keyboard.isRepeatEvent())
|
|
|
|
mousePressed(Input.MOUSE_LEFT_BUTTON, input.getMouseX(), input.getMouseY());
|
|
|
|
break;
|
|
|
|
case Input.KEY_X:
|
|
|
|
// right-click
|
|
|
|
if (!Keyboard.isRepeatEvent())
|
|
|
|
mousePressed(Input.MOUSE_RIGHT_BUTTON, input.getMouseX(), input.getMouseY());
|
|
|
|
break;
|
|
|
|
case Input.KEY_F12:
|
2014-07-02 01:32:03 +02:00
|
|
|
Utils.takeScreenShot();
|
2014-06-30 04:17:04 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void mousePressed(int button, int x, int y) {
|
|
|
|
if (button == Input.MOUSE_MIDDLE_BUTTON)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// returning from pause screen
|
|
|
|
if (pauseTime > -1) {
|
|
|
|
double distance = Math.hypot(pausedMouseX - x, pausedMouseY - y);
|
|
|
|
int circleRadius = Circle.getHitCircle().getWidth() / 2;
|
|
|
|
if (distance < circleRadius) {
|
|
|
|
// unpause the game
|
|
|
|
pauseTime = -1;
|
|
|
|
pausedMouseX = -1;
|
|
|
|
pausedMouseY = -1;
|
|
|
|
if (!Game.isLeadIn())
|
|
|
|
MusicController.resume();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (objectIndex >= osu.objects.length) // nothing left to do here
|
|
|
|
return;
|
|
|
|
|
|
|
|
OsuHitObject hitObject = osu.objects[objectIndex];
|
|
|
|
|
|
|
|
// skip beginning
|
|
|
|
if (skipButton.contains(x, y)) {
|
|
|
|
if (skipIntro())
|
|
|
|
return; // successfully skipped
|
|
|
|
}
|
|
|
|
|
|
|
|
// "auto" mod: ignore user actions
|
|
|
|
if (Options.isModActive(Options.MOD_AUTO))
|
|
|
|
return;
|
|
|
|
|
|
|
|
// circles
|
|
|
|
if ((hitObject.type & OsuHitObject.TYPE_CIRCLE) > 0) {
|
|
|
|
boolean hit = circles.get(objectIndex).mousePressed(x, y);
|
|
|
|
if (hit)
|
|
|
|
objectIndex++;
|
|
|
|
}
|
|
|
|
|
|
|
|
// sliders
|
|
|
|
else if ((hitObject.type & OsuHitObject.TYPE_SLIDER) > 0)
|
|
|
|
sliders.get(objectIndex).mousePressed(x, y);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void enter(GameContainer container, StateBasedGame game)
|
|
|
|
throws SlickException {
|
2014-07-02 09:02:11 +02:00
|
|
|
if (restart == RESTART_NEW)
|
|
|
|
osu = MusicController.getOsuFile();
|
|
|
|
|
2014-06-30 04:17:04 +02:00
|
|
|
if (osu == null || osu.objects == null)
|
|
|
|
throw new RuntimeException("Running game with no OsuFile loaded.");
|
|
|
|
|
2014-07-02 07:53:42 +02:00
|
|
|
// grab the mouse (not working for touchscreen)
|
|
|
|
// container.setMouseGrabbed(true);
|
2014-07-02 01:32:03 +02:00
|
|
|
|
2014-06-30 04:17:04 +02:00
|
|
|
// restart the game
|
|
|
|
if (restart != RESTART_FALSE) {
|
|
|
|
// new game
|
|
|
|
if (restart == RESTART_NEW) {
|
|
|
|
setMapModifiers();
|
|
|
|
|
|
|
|
// calculate map length (TODO: end on slider?)
|
|
|
|
OsuHitObject lastObject = osu.objects[osu.objects.length - 1];
|
|
|
|
int endTime;
|
|
|
|
if ((lastObject.type & OsuHitObject.TYPE_SPINNER) > 0)
|
|
|
|
endTime = lastObject.endTime;
|
|
|
|
else
|
|
|
|
endTime = lastObject.time;
|
|
|
|
mapLength = endTime - osu.objects[0].time;
|
|
|
|
}
|
|
|
|
|
|
|
|
// initialize object maps
|
|
|
|
circles = new HashMap<Integer, Circle>();
|
|
|
|
sliders = new HashMap<Integer, Slider>();
|
|
|
|
spinners = new HashMap<Integer, Spinner>();
|
|
|
|
|
|
|
|
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].type & OsuHitObject.TYPE_NEWCOMBO) > 0)
|
|
|
|
comboEnd = true;
|
|
|
|
|
|
|
|
if ((hitObject.type & OsuHitObject.TYPE_CIRCLE) > 0) {
|
|
|
|
circles.put(i, new Circle(hitObject, this, score, osu.combo[hitObject.comboIndex], comboEnd));
|
|
|
|
} else if ((hitObject.type & OsuHitObject.TYPE_SLIDER) > 0) {
|
|
|
|
sliders.put(i, new Slider(hitObject, this, score, osu.combo[hitObject.comboIndex], comboEnd));
|
|
|
|
} else if ((hitObject.type & OsuHitObject.TYPE_SPINNER) > 0) {
|
|
|
|
spinners.put(i, new Spinner(hitObject, this, score));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-01 07:14:03 +02:00
|
|
|
// reset data
|
2014-06-30 04:17:04 +02:00
|
|
|
MusicController.setPosition(0);
|
|
|
|
MusicController.pause();
|
|
|
|
score.clear();
|
|
|
|
objectIndex = 0;
|
|
|
|
breakIndex = 0;
|
|
|
|
breakTime = 0;
|
2014-07-01 07:14:03 +02:00
|
|
|
breakSound = false;
|
2014-06-30 04:17:04 +02:00
|
|
|
timingPointIndex = 0;
|
|
|
|
pauseTime = -1;
|
|
|
|
pausedMouseX = -1;
|
|
|
|
pausedMouseY = -1;
|
2014-07-01 07:14:03 +02:00
|
|
|
countdownReadySound = false;
|
|
|
|
countdown3Sound = false;
|
|
|
|
countdown1Sound = false;
|
|
|
|
countdown2Sound = false;
|
|
|
|
countdownGoSound = false;
|
2014-06-30 04:17:04 +02:00
|
|
|
|
|
|
|
// load the first timingPoint
|
2014-07-01 07:14:03 +02:00
|
|
|
if (!osu.timingPoints.isEmpty()) {
|
|
|
|
OsuTimingPoint timingPoint = osu.timingPoints.get(0);
|
|
|
|
if (timingPoint.velocity >= 0) {
|
|
|
|
beatLengthBase = beatLength = timingPoint.beatLength;
|
|
|
|
SoundController.setSampleSet(timingPoint.sampleType);
|
|
|
|
SoundController.setSampleVolume(timingPoint.sampleVolume);
|
|
|
|
timingPointIndex++;
|
|
|
|
}
|
2014-06-30 04:17:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
leadInTime = osu.audioLeadIn + approachTime;
|
|
|
|
restart = RESTART_FALSE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-02 07:53:42 +02:00
|
|
|
// @Override
|
|
|
|
// public void leave(GameContainer container, StateBasedGame game)
|
|
|
|
// throws SlickException {
|
|
|
|
// container.setMouseGrabbed(false);
|
|
|
|
// }
|
2014-07-02 01:32:03 +02:00
|
|
|
|
2014-06-30 04:17:04 +02:00
|
|
|
/**
|
|
|
|
* Skips the beginning of a track.
|
|
|
|
* @return true if skipped, false otherwise
|
|
|
|
*/
|
|
|
|
private boolean skipIntro() {
|
|
|
|
int trackPosition = MusicController.getPosition();
|
|
|
|
if (objectIndex == 0 &&
|
|
|
|
osu.objects[0].time - skipOffsetTime > 4000 &&
|
|
|
|
trackPosition < osu.objects[0].time - skipOffsetTime) {
|
|
|
|
if (isLeadIn()) {
|
|
|
|
leadInTime = 0;
|
|
|
|
MusicController.resume();
|
|
|
|
}
|
|
|
|
MusicController.setPosition(osu.objects[0].time - skipOffsetTime);
|
2014-07-01 07:14:03 +02:00
|
|
|
SoundController.playSound(SoundController.SOUND_MENUHIT);
|
2014-06-30 04:17:04 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if an input key is pressed (mouse left/right, keyboard Z/X).
|
|
|
|
*/
|
|
|
|
public boolean isInputKeyPressed() {
|
|
|
|
return (input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) ||
|
|
|
|
input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON) ||
|
|
|
|
input.isKeyDown(Input.KEY_Z) || input.isKeyDown(Input.KEY_X));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set map modifiers.
|
|
|
|
*/
|
|
|
|
private void setMapModifiers() {
|
|
|
|
try {
|
|
|
|
// map-based properties, so re-initialize each game
|
2014-06-30 10:03:39 +02:00
|
|
|
float circleSize = osu.circleSize;
|
|
|
|
float approachRate = osu.approachRate;
|
|
|
|
float overallDifficulty = osu.overallDifficulty;
|
|
|
|
float HPDrainRate = osu.HPDrainRate;
|
2014-06-30 04:17:04 +02:00
|
|
|
if (Options.isModActive(Options.MOD_HARD_ROCK)) { // hard rock modifiers
|
2014-06-30 10:03:39 +02:00
|
|
|
circleSize = Math.max(circleSize - 1, 0);
|
|
|
|
approachRate = Math.min(approachRate + 3, 10);
|
|
|
|
overallDifficulty = Math.min(overallDifficulty + 3, 10);
|
|
|
|
HPDrainRate = Math.min(HPDrainRate + 3, 10);
|
2014-06-30 04:17:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Circle.init(container, circleSize);
|
|
|
|
Slider.init(container, circleSize, osu);
|
|
|
|
|
|
|
|
// approachRate (hit object approach time)
|
|
|
|
if (approachRate < 5)
|
2014-06-30 10:03:39 +02:00
|
|
|
approachTime = (int) (1800 - (approachRate * 120));
|
2014-06-30 04:17:04 +02:00
|
|
|
else
|
2014-06-30 10:03:39 +02:00
|
|
|
approachTime = (int) (1200 - ((approachRate - 5) * 150));
|
2014-06-30 04:17:04 +02:00
|
|
|
|
|
|
|
// overallDifficulty (hit result time offsets)
|
|
|
|
hitResultOffset = new int[GameScore.HIT_MAX];
|
2014-06-30 10:03:39 +02:00
|
|
|
hitResultOffset[GameScore.HIT_300] = (int) (78 - (overallDifficulty * 6));
|
|
|
|
hitResultOffset[GameScore.HIT_100] = (int) (138 - (overallDifficulty * 8));
|
|
|
|
hitResultOffset[GameScore.HIT_50] = (int) (198 - (overallDifficulty * 10));
|
|
|
|
hitResultOffset[GameScore.HIT_MISS] = (int) (500 - (overallDifficulty * 10));
|
2014-06-30 04:17:04 +02:00
|
|
|
|
|
|
|
// HPDrainRate (health change), overallDifficulty (scoring)
|
|
|
|
score.setDrainRate(HPDrainRate);
|
|
|
|
score.setDifficulty(overallDifficulty);
|
|
|
|
} catch (SlickException e) {
|
|
|
|
Log.error("Error while setting map modifiers.", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets/returns whether entering the state will restart it.
|
|
|
|
*/
|
|
|
|
public static void setRestart(byte restart) { Game.restart = restart; }
|
|
|
|
public static byte getRestart() { return Game.restart; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the associated GameScore object.
|
|
|
|
*/
|
|
|
|
public static GameScore getGameScore() { return score; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns whether or not the track is in the lead-in time state.
|
|
|
|
*/
|
|
|
|
public static 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 GameScore.HIT_* constants).
|
|
|
|
*/
|
|
|
|
public int[] getHitResultOffsets() { return hitResultOffset; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the beat length.
|
|
|
|
*/
|
|
|
|
public float getBeatLength() { return beatLength; }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the slider multiplier given by the current timing point.
|
|
|
|
*/
|
|
|
|
public float getTimingPointMultiplier() { return beatLength / beatLengthBase; }
|
|
|
|
}
|