- Rotate slider ball image along the curve.
- Show download speed and time remaining when hovering over the download info button.
- Maintain aspect ratio of beatmap background image during gameplay.
- Clear beatmap background image cache after reaching OsuFile.MAX_CACHE_SIZE.
- Multiply hit circle size by OsuHitObject.getXMultiplier().
- Scale MENU_BUTTON_BG based on SongMenu.MAX_SONG_BUTTONS.
- Fixed download search TextField retaining focus even after leaving the downloads menu.
- Include Unicode title/artist strings in searches.
- Parse x,y beatmap coordinates as floats (instead of integers), and only read Kiai time if present.
- Added 1600x1200 resolution.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2015-02-09 21:40:38 -05:00
parent 8d00a0f81e
commit f56c02864b
15 changed files with 125 additions and 18 deletions

View File

@ -18,6 +18,8 @@
package itdelatrisu.opsu;
import itdelatrisu.opsu.states.SongMenu;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@ -381,7 +383,7 @@ public enum GameImage {
MENU_BUTTON_BG ("menu-button-background", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
return img.getScaledCopy(w / 2, h / 6);
return img.getScaledCopy(w / 2, h / SongMenu.MAX_SONG_BUTTONS);
}
},
MENU_TAB ("selection-tab", "png", false, false) {

View File

@ -393,6 +393,7 @@ public class Options {
RES_1440_900 (1440, 900),
RES_1600_900 (1600, 900),
RES_1680_1050 (1680, 1050),
RES_1600_1200 (1600, 1200),
RES_1920_1080 (1920, 1080),
RES_1920_1200 (1920, 1200),
RES_2560_1440 (2560, 1440),

View File

@ -34,6 +34,9 @@ public class OsuFile implements Comparable<OsuFile> {
/** Map of all loaded background images. */
private static HashMap<OsuFile, Image> bgImageMap = new HashMap<OsuFile, Image>();
/** Maximum number of cached images before all get erased. */
private static final int MAX_CACHE_SIZE = 10;
/** The OSU File object associated with this OsuFile. */
private File file;
@ -250,19 +253,34 @@ public class OsuFile implements Comparable<OsuFile> {
* @param width the container width
* @param height the container height
* @param alpha the alpha value
* @param stretch if true, stretch to screen dimensions; otherwise, maintain aspect ratio
* @return true if successful, false if any errors were produced
*/
public boolean drawBG(int width, int height, float alpha) {
public boolean drawBG(int width, int height, float alpha, boolean stretch) {
if (bg == null)
return false;
try {
Image bgImage = bgImageMap.get(this);
if (bgImage == null) {
bgImage = new Image(bg).getScaledCopy(width, height);
bgImage = new Image(bg);
if (bgImageMap.size() > MAX_CACHE_SIZE)
clearImageCache();
bgImageMap.put(this, bgImage);
}
// fit image to screen
int swidth = width;
int sheight = height;
if (!stretch) {
if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
else
swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight());
}
bgImage = bgImage.getScaledCopy(swidth, sheight);
bgImage.setAlpha(alpha);
bgImage.draw();
bgImage.drawCentered(width / 2, height / 2);
} catch (Exception e) {
Log.warn(String.format("Failed to get background image '%s'.", bg), e);
bg = null; // don't try to load the file again until a restart

View File

@ -150,7 +150,9 @@ public class OsuGroupNode {
// search: title, artist, creator, source, version, tags (first OsuFile)
if (osu.title.toLowerCase().contains(query) ||
osu.titleUnicode.toLowerCase().contains(query) ||
osu.artist.toLowerCase().contains(query) ||
osu.artistUnicode.toLowerCase().contains(query) ||
osu.creator.toLowerCase().contains(query) ||
osu.source.toLowerCase().contains(query) ||
osu.version.toLowerCase().contains(query) ||

View File

@ -148,8 +148,8 @@ public class OsuHitObject {
String tokens[] = line.split(",");
// common fields
this.x = Integer.parseInt(tokens[0]) * xMultiplier + xOffset;
this.y = Integer.parseInt(tokens[1]) * yMultiplier + yOffset;
this.x = Float.parseFloat(tokens[0]) * xMultiplier + xOffset;
this.y = Float.parseFloat(tokens[1]) * yMultiplier + yOffset;
this.time = Integer.parseInt(tokens[2]);
this.type = Integer.parseInt(tokens[3]);
this.hitSound = Byte.parseByte(tokens[4]);

View File

@ -65,7 +65,8 @@ public class OsuTimingPoint {
this.sampleTypeCustom = Byte.parseByte(tokens[4]);
this.sampleVolume = Integer.parseInt(tokens[5]);
// this.inherited = (Integer.parseInt(tokens[6]) == 1);
this.kiai = (Integer.parseInt(tokens[7]) == 1);
if (tokens.length > 7)
this.kiai = (Integer.parseInt(tokens[7]) == 1);
} catch (ArrayIndexOutOfBoundsException e) {
Log.debug(String.format("Error parsing timing point: '%s'", line));
}

View File

@ -44,6 +44,9 @@ public class Download {
/** Read timeout, in ms. */
public static final int READ_TIMEOUT = 10000;
/** Time between download speed and ETA updates, in ms. */
private static final int UPDATE_INTERVAL = 1000;
/** Download statuses. */
public enum Status {
WAITING ("Waiting"),
@ -90,6 +93,18 @@ public class Download {
/** The download status. */
private Status status = Status.WAITING;
/** Time when lastReadSoFar was updated. */
private long lastReadSoFarTime = -1;
/** Last readSoFar amount. */
private long lastReadSoFar = -1;
/** Last calculated download speed string. */
private String lastDownloadSpeed;
/** Last calculated ETA string. */
private String lastTimeRemaining;
/**
* Constructor.
* @param remoteURL the download URL
@ -234,6 +249,60 @@ public class Download {
}
}
/**
* Returns the last calculated download speed, or null if not downloading.
*/
public String getDownloadSpeed() {
updateReadSoFar();
return lastDownloadSpeed;
}
/**
* Returns the last calculated ETA, or null if not downloading.
*/
public String getTimeRemaining() {
updateReadSoFar();
return lastTimeRemaining;
}
/**
* Updates the last readSoFar and related fields.
*/
private void updateReadSoFar() {
// only update while downloading
if (status != Status.DOWNLOADING) {
this.lastDownloadSpeed = null;
this.lastTimeRemaining = null;
return;
}
// update download speed and ETA
if (System.currentTimeMillis() > lastReadSoFarTime + UPDATE_INTERVAL) {
long readSoFar = readSoFar();
long readSoFarTime = System.currentTimeMillis();
long dlspeed = (readSoFar - lastReadSoFar) * 1000 / (readSoFarTime - lastReadSoFarTime);
if (dlspeed > 0) {
this.lastDownloadSpeed = String.format("%s/s", Utils.bytesToString(dlspeed));
long t = (contentLength - readSoFar) / dlspeed;
if (t >= 3600)
this.lastTimeRemaining = String.format("%dh%dm%ds", t / 3600, (t / 60) % 60, t % 60);
else
this.lastTimeRemaining = String.format("%dm%ds", t / 60, t % 60);
} else {
this.lastDownloadSpeed = String.format("%s/s", Utils.bytesToString(0));
this.lastTimeRemaining = "?";
}
this.lastReadSoFarTime = readSoFarTime;
this.lastReadSoFar = readSoFar;
}
// first call
else if (lastReadSoFarTime <= 0) {
this.lastReadSoFar = readSoFar();
this.lastReadSoFarTime = System.currentTimeMillis();
}
}
/**
* Cancels the download, if running.
*/

View File

@ -348,9 +348,13 @@ public class DownloadNode {
info = status.getName();
else if (status == Download.Status.WAITING)
info = String.format("%s...", status.getName());
else
info = String.format("%s: %.1f%% (%s/%s)", status.getName(), progress,
Utils.bytesToString(download.readSoFar()), Utils.bytesToString(download.contentLength()));
else {
if (hover)
info = String.format("%s: %s left (%s)", status.getName(), download.getTimeRemaining(), download.getDownloadSpeed());
else
info = String.format("%s: %.1f%% (%s/%s)", status.getName(), progress,
Utils.bytesToString(download.readSoFar()), Utils.bytesToString(download.contentLength()));
}
Utils.FONT_BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
Utils.FONT_DEFAULT.drawString(textX, y + marginY + Utils.FONT_BOLD.getLineHeight(), info, Color.white);

View File

@ -56,7 +56,7 @@ public class Circle implements HitObject {
*/
public static void init(GameContainer container, float circleSize) {
int diameter = (int) (96 - (circleSize * 8));
diameter = diameter * container.getWidth() / 640; // convert from Osupixels (640x480)
diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480)
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameter, diameter));
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameter, diameter));
GameImage.APPROACHCIRCLE.setImage(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(diameter, diameter));

View File

@ -99,7 +99,7 @@ public class Slider implements HitObject {
*/
public static void init(GameContainer container, float circleSize, OsuFile osu) {
int diameter = (int) (96 - (circleSize * 8));
diameter = diameter * container.getWidth() / 640; // convert from Osupixels (640x480)
diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480)
// slider ball
Image[] sliderBallImages;
@ -211,9 +211,13 @@ public class Slider implements HitObject {
Utils.drawCentered(GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale), x, y, color);
} else {
float[] c = curve.pointAt(getT(trackPosition, false));
float[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
// slider ball
Utils.drawCentered(sliderBall, c[0], c[1]);
Image sliderBallFrame = sliderBall.getCurrentFrame();
float angle = (float) (Math.atan2(c2[1] - c[1], c2[0] - c[0]) * 180 / Math.PI);
sliderBallFrame.setRotation(angle);
sliderBallFrame.drawCentered(c[0], c[1]);
// follow circle
if (followCircleActive)

View File

@ -660,6 +660,12 @@ public class DownloadsMenu extends BasicGameState {
pageDir = Page.RESET;
}
@Override
public void leave(GameContainer container, StateBasedGame game)
throws SlickException {
search.setFocus(false);
}
/**
* Resets the search timer, but respects the minimum request interval.
*/

View File

@ -173,7 +173,7 @@ public class Game extends BasicGameState {
// background
g.setBackground(Color.black);
float dimLevel = Options.getBackgroundDim();
if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel)) {
if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel, false)) {
Image playfield = GameImage.PLAYFIELD.getImage();
playfield.setAlpha(dimLevel);
playfield.draw();

View File

@ -95,7 +95,7 @@ public class GameRanking extends BasicGameState {
OsuFile osu = MusicController.getOsuFile();
// background
if (!osu.drawBG(width, height, 0.7f))
if (!osu.drawBG(width, height, 0.7f, true))
g.setBackground(Utils.COLOR_BLACK_ALPHA);
// ranking screen elements

View File

@ -168,7 +168,7 @@ public class MainMenu extends BasicGameState {
// draw background
OsuFile osu = MusicController.getOsuFile();
if (Options.isDynamicBackgroundEnabled() &&
osu != null && osu.drawBG(width, height, bgAlpha))
osu != null && osu.drawBG(width, height, bgAlpha, true))
;
else {
Image bg = GameImage.MENU_BG.getImage();

View File

@ -68,7 +68,7 @@ import org.newdawn.slick.state.transition.FadeOutTransition;
*/
public class SongMenu extends BasicGameState {
/** The max number of song buttons to be shown on each screen. */
private static final int MAX_SONG_BUTTONS = 6;
public static final int MAX_SONG_BUTTONS = 6;
/** The max number of score buttons to be shown at a time. */
public static final int MAX_SCORE_BUTTONS = 7;
@ -239,7 +239,7 @@ public class SongMenu extends BasicGameState {
// background
if (focusNode != null)
focusNode.osuFiles.get(focusNode.osuFileIndex).drawBG(width, height, 1.0f);
focusNode.osuFiles.get(focusNode.osuFileIndex).drawBG(width, height, 1.0f, true);
// header setup
float lowerBound = height * 0.15f;