diff --git a/res/alpha.png b/res/alpha.png new file mode 100644 index 00000000..35f9344e Binary files /dev/null and b/res/alpha.png differ diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index c0674719..33ab2ff9 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -917,22 +917,22 @@ public class GameData { * @param hit100 the number of 100s * @param hit50 the number of 50s * @param miss the number of misses + * @param silver whether or not a silver SS/S should be awarded (if applicable) * @return the current Grade */ - public static Grade getGrade(int hit300, int hit100, int hit50, int miss) { + public static Grade getGrade(int hit300, int hit100, int hit50, int miss, boolean silver) { int objectCount = hit300 + hit100 + hit50 + miss; if (objectCount < 1) // avoid division by zero return Grade.NULL; - // TODO: silvers float percent = getScorePercent(hit300, hit100, hit50, miss); float hit300ratio = hit300 * 100f / objectCount; - float hit50ratio = hit50 * 100f / objectCount; - boolean noMiss = (miss == 0); + float hit50ratio = hit50 * 100f / objectCount; + boolean noMiss = (miss == 0); if (percent >= 100f) - return Grade.SS; + return (silver) ? Grade.SSH : Grade.SS; else if (hit300ratio >= 90f && hit50ratio < 1.0f && noMiss) - return Grade.S; + return (silver) ? Grade.SH : Grade.S; else if ((hit300ratio >= 80f && noMiss) || hit300ratio >= 90f) return Grade.A; else if ((hit300ratio >= 70f && noMiss) || hit300ratio >= 80f) @@ -950,7 +950,8 @@ public class GameData { private Grade getGrade() { return getGrade( hitResultCount[HIT_300], hitResultCount[HIT_100], - hitResultCount[HIT_50], hitResultCount[HIT_MISS] + hitResultCount[HIT_50], hitResultCount[HIT_MISS], + (GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive()) ); } @@ -1031,6 +1032,11 @@ public class GameData { } } + /** + * Returns the current combo streak. + */ + public int getComboStreak() { return combo; } + /** * Increases the combo streak by one. */ diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 74592995..e8de7423 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -327,7 +327,9 @@ public enum GameImage { protected Image process_sub(Image img, int w, int h) { return REPOSITORY.process_sub(img, w, h); } - }; + }, + // TODO: ensure this image hasn't been modified (checksum?) + ALPHA_MAP ("alpha", "png", false, false); /** Image file types. */ private static final byte diff --git a/src/itdelatrisu/opsu/GameMod.java b/src/itdelatrisu/opsu/GameMod.java index dc7b1146..d29ac60a 100644 --- a/src/itdelatrisu/opsu/GameMod.java +++ b/src/itdelatrisu/opsu/GameMod.java @@ -47,7 +47,7 @@ public enum GameMod { // "Nightcore", "uguuuuuuuu"), HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f, false, "Hidden", "Play with no approach circles and fading notes for a slight score advantage."), - FLASHLIGHT (Category.HARD, 4, GameImage.MOD_FLASHLIGHT, "FL", 1024, Input.KEY_G, 1.12f, false, + FLASHLIGHT (Category.HARD, 4, GameImage.MOD_FLASHLIGHT, "FL", 1024, Input.KEY_G, 1.12f, "Flashlight", "Restricted view area."), RELAX (Category.SPECIAL, 0, GameImage.MOD_RELAX, "RL", 128, Input.KEY_Z, 0f, "Relax", "You don't need to click.\nGive your clicking/tapping finger a break from the heat of things.\n**UNRANKED**"), diff --git a/src/itdelatrisu/opsu/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java index 71ea5d74..fc25daf8 100644 --- a/src/itdelatrisu/opsu/ScoreData.java +++ b/src/itdelatrisu/opsu/ScoreData.java @@ -187,11 +187,12 @@ public class ScoreData implements Comparable { /** * Returns letter grade based on score data, * or Grade.NULL if no objects have been processed. - * @see GameData#getGrade(int, int, int, int) + * @see GameData#getGrade(int, int, int, int, boolean) */ public Grade getGrade() { if (grade == null) - grade = GameData.getGrade(hit300, hit100, hit50, miss); + grade = GameData.getGrade(hit300, hit100, hit50, miss, + ((mods & GameMod.HIDDEN.getBit()) > 0 || (mods & GameMod.FLASHLIGHT.getBit()) > 0)); return grade; } diff --git a/src/itdelatrisu/opsu/objects/Slider.java b/src/itdelatrisu/opsu/objects/Slider.java index 9b3aadb0..b526cc9a 100644 --- a/src/itdelatrisu/opsu/objects/Slider.java +++ b/src/itdelatrisu/opsu/objects/Slider.java @@ -94,6 +94,9 @@ public class Slider implements HitObject { /** Number of ticks hit and tick intervals so far. */ private int ticksHit = 0, tickIntervals = 1; + /** Container dimensions. */ + private static int containerWidth, containerHeight; + /** * Initializes the Slider data type with images and dimensions. * @param container the game container @@ -101,6 +104,9 @@ public class Slider implements HitObject { * @param osu the associated OsuFile object */ public static void init(GameContainer container, float circleSize, OsuFile osu) { + containerWidth = container.getWidth(); + containerHeight = container.getHeight(); + int diameter = (int) (104 - (circleSize * 8)); diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480) @@ -192,7 +198,7 @@ public class Slider implements HitObject { color.a = oldAlpha; // repeats - for(int tcurRepeat = currentRepeats; tcurRepeat<=currentRepeats+1; tcurRepeat++){ + for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) { if (hitObject.getRepeatCount() - 1 > tcurRepeat) { Image arrow = GameImage.REVERSEARROW.getImage(); if (tcurRepeat != currentRepeats) { @@ -232,8 +238,18 @@ public class Slider implements HitObject { sliderBallFrame.drawCentered(c[0], c[1]); // follow circle - if (followCircleActive) + if (followCircleActive) { GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c[0], c[1]); + + // "flashlight" mod: dim the screen + if (GameMod.FLASHLIGHT.isActive()) { + float oldAlphaBlack = Utils.COLOR_BLACK_ALPHA.a; + Utils.COLOR_BLACK_ALPHA.a = 0.75f; + g.setColor(Utils.COLOR_BLACK_ALPHA); + g.fillRect(0, 0, containerWidth, containerHeight); + Utils.COLOR_BLACK_ALPHA.a = oldAlphaBlack; + } + } } } diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index 5be10f80..d3abb3ec 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -193,6 +193,15 @@ public class Game extends BasicGameState { /** The list of current replay frames (for recording replays). */ private LinkedList replayFrames; + /** The offscreen image rendered to. */ + private Image offscreen; + + /** The offscreen graphics. */ + private Graphics gOffscreen; + + /** The current flashlight area radius. */ + private int flashlightRadius; + // game-related variables private GameContainer container; private StateBasedGame game; @@ -213,6 +222,11 @@ public class Game extends BasicGameState { 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); } @@ -222,9 +236,15 @@ public class Game extends BasicGameState { throws SlickException { int width = container.getWidth(); int height = container.getHeight(); + g.setBackground(Color.black); + + // "flashlight" mod: initialize offscreen graphics + if (GameMod.FLASHLIGHT.isActive()) { + gOffscreen.clear(); + Graphics.setCurrent(gOffscreen); + } // background - g.setBackground(Color.black); float dimLevel = Options.getBackgroundDim(); if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel, false)) { Image playfield = GameImage.PLAYFIELD.getImage(); @@ -233,6 +253,9 @@ public class Game extends BasicGameState { playfield.setAlpha(1f); } + if (GameMod.FLASHLIGHT.isActive()) + Graphics.setCurrent(g); + int trackPosition = MusicController.getPosition(); if (pauseTime > -1) // returning from pause screen trackPosition = pauseTime; @@ -257,144 +280,167 @@ public class Game extends BasicGameState { ); } + // "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 (isReplay) { + mouseX = replayX; + mouseY = replayY; + } else { + mouseX = input.getMouseX(); + mouseY = input.getMouseY(); + } + int alphaX = mouseX - flashlightRadius / 2; + int alphaY = mouseY - flashlightRadius / 2; + GameImage.ALPHA_MAP.getImage().draw(alphaX, alphaY, flashlightRadius, flashlightRadius); + + // blend offscreen image + g.setDrawMode(Graphics.MODE_ALPHA_BLEND); + g.setClip(alphaX, alphaY, flashlightRadius, flashlightRadius); + g.drawImage(offscreen, 0, 0); + g.clearClip(); + g.setDrawMode(Graphics.MODE_NORMAL); + } + // break periods - if (osu.breaks != null && breakIndex < osu.breaks.size()) { - if (breakTime > 0) { - int endTime = osu.breaks.get(breakIndex); - int breakLength = endTime - breakTime; + 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); - } - } - - if (GameMod.AUTO.isActive()) - GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f); - if (!isReplay) - UI.draw(g); - else - UI.draw(g, replayX, replayY, replayKeyPressed); - return; + // 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); } - } - // game elements - data.drawGameElements(g, false, objectIndex == 0); + data.drawGameElements(g, true, 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 (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; } } - 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; + } 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); } } } - // draw hit objects in reverse order, or else overlapping objects are unreadable - Stack stack = new Stack(); - for (int i = objectIndex; i < hitObjects.length && osu.objects[i].getTime() < trackPosition + approachTime; i++) - stack.add(i); + // 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(); - while (!stack.isEmpty()) - hitObjects[stack.pop()].draw(g, trackPosition); + // 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; + } - // draw OsuHitObjectResult objects - data.drawHitResults(trackPosition); + 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); @@ -434,6 +480,62 @@ public class Game extends BasicGameState { mouseY = replayY; } skipButton.hoverUpdate(delta, mouseX, mouseY); + int trackPosition = MusicController.getPosition(); + + // "flashlight" mod: calculate visible area radius + if (GameMod.FLASHLIGHT.isActive()) { + 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 = (int) (width / (1 + 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; + } + } + } + } if (isLeadIn()) { // stop updating during song lead-in leadInTime -= delta; @@ -515,8 +617,6 @@ public class Game extends BasicGameState { return; } - int trackPosition = MusicController.getPosition(); - // timing points if (timingPointIndex < osu.timingPoints.size()) { OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex); @@ -997,6 +1097,24 @@ public class Game extends BasicGameState { } } + /** + * Draws hit objects and hit results. + * @param g the graphics context + * @param trackPosition the track position + */ + private void drawHitObjects(Graphics g, int trackPosition) { + // draw hit objects in reverse order, or else overlapping objects are unreadable + Stack stack = new Stack(); + for (int i = objectIndex; i < hitObjects.length && osu.objects[i].getTime() < trackPosition + approachTime; i++) + stack.add(i); + + 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