Implemented "Flashlight" mod.

- Restricted view area using alpha maps and offscreen drawing. (credits: davedes)
- Added silver grades.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2015-03-15 15:38:04 -04:00
parent f7c627e8a2
commit b5a6455d0a
7 changed files with 284 additions and 141 deletions

BIN
res/alpha.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -917,22 +917,22 @@ public class GameData {
* @param hit100 the number of 100s * @param hit100 the number of 100s
* @param hit50 the number of 50s * @param hit50 the number of 50s
* @param miss the number of misses * @param miss the number of misses
* @param silver whether or not a silver SS/S should be awarded (if applicable)
* @return the current Grade * @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; int objectCount = hit300 + hit100 + hit50 + miss;
if (objectCount < 1) // avoid division by zero if (objectCount < 1) // avoid division by zero
return Grade.NULL; return Grade.NULL;
// TODO: silvers
float percent = getScorePercent(hit300, hit100, hit50, miss); float percent = getScorePercent(hit300, hit100, hit50, miss);
float hit300ratio = hit300 * 100f / objectCount; float hit300ratio = hit300 * 100f / objectCount;
float hit50ratio = hit50 * 100f / objectCount; float hit50ratio = hit50 * 100f / objectCount;
boolean noMiss = (miss == 0); boolean noMiss = (miss == 0);
if (percent >= 100f) if (percent >= 100f)
return Grade.SS; return (silver) ? Grade.SSH : Grade.SS;
else if (hit300ratio >= 90f && hit50ratio < 1.0f && noMiss) else if (hit300ratio >= 90f && hit50ratio < 1.0f && noMiss)
return Grade.S; return (silver) ? Grade.SH : Grade.S;
else if ((hit300ratio >= 80f && noMiss) || hit300ratio >= 90f) else if ((hit300ratio >= 80f && noMiss) || hit300ratio >= 90f)
return Grade.A; return Grade.A;
else if ((hit300ratio >= 70f && noMiss) || hit300ratio >= 80f) else if ((hit300ratio >= 70f && noMiss) || hit300ratio >= 80f)
@ -950,7 +950,8 @@ public class GameData {
private Grade getGrade() { private Grade getGrade() {
return getGrade( return getGrade(
hitResultCount[HIT_300], hitResultCount[HIT_100], 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. * Increases the combo streak by one.
*/ */

View File

@ -327,7 +327,9 @@ public enum GameImage {
protected Image process_sub(Image img, int w, int h) { protected Image process_sub(Image img, int w, int h) {
return REPOSITORY.process_sub(img, w, 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. */ /** Image file types. */
private static final byte private static final byte

View File

@ -47,7 +47,7 @@ public enum GameMod {
// "Nightcore", "uguuuuuuuu"), // "Nightcore", "uguuuuuuuu"),
HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f, false, 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."), "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."), "Flashlight", "Restricted view area."),
RELAX (Category.SPECIAL, 0, GameImage.MOD_RELAX, "RL", 128, Input.KEY_Z, 0f, 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**"), "Relax", "You don't need to click.\nGive your clicking/tapping finger a break from the heat of things.\n**UNRANKED**"),

View File

@ -187,11 +187,12 @@ public class ScoreData implements Comparable<ScoreData> {
/** /**
* Returns letter grade based on score data, * Returns letter grade based on score data,
* or Grade.NULL if no objects have been processed. * 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() { public Grade getGrade() {
if (grade == null) 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; return grade;
} }

View File

@ -94,6 +94,9 @@ public class Slider implements HitObject {
/** Number of ticks hit and tick intervals so far. */ /** Number of ticks hit and tick intervals so far. */
private int ticksHit = 0, tickIntervals = 1; private int ticksHit = 0, tickIntervals = 1;
/** Container dimensions. */
private static int containerWidth, containerHeight;
/** /**
* Initializes the Slider data type with images and dimensions. * Initializes the Slider data type with images and dimensions.
* @param container the game container * @param container the game container
@ -101,6 +104,9 @@ public class Slider implements HitObject {
* @param osu the associated OsuFile object * @param osu the associated OsuFile object
*/ */
public static void init(GameContainer container, float circleSize, OsuFile osu) { public static void init(GameContainer container, float circleSize, OsuFile osu) {
containerWidth = container.getWidth();
containerHeight = container.getHeight();
int diameter = (int) (104 - (circleSize * 8)); int diameter = (int) (104 - (circleSize * 8));
diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480) diameter = (int) (diameter * OsuHitObject.getXMultiplier()); // convert from Osupixels (640x480)
@ -192,7 +198,7 @@ public class Slider implements HitObject {
color.a = oldAlpha; color.a = oldAlpha;
// repeats // repeats
for(int tcurRepeat = currentRepeats; tcurRepeat<=currentRepeats+1; tcurRepeat++){ for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) {
if (hitObject.getRepeatCount() - 1 > tcurRepeat) { if (hitObject.getRepeatCount() - 1 > tcurRepeat) {
Image arrow = GameImage.REVERSEARROW.getImage(); Image arrow = GameImage.REVERSEARROW.getImage();
if (tcurRepeat != currentRepeats) { if (tcurRepeat != currentRepeats) {
@ -232,8 +238,18 @@ public class Slider implements HitObject {
sliderBallFrame.drawCentered(c[0], c[1]); sliderBallFrame.drawCentered(c[0], c[1]);
// follow circle // follow circle
if (followCircleActive) if (followCircleActive) {
GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c[0], c[1]); 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;
}
}
} }
} }

View File

@ -193,6 +193,15 @@ public class Game extends BasicGameState {
/** The list of current replay frames (for recording replays). */ /** The list of current replay frames (for recording replays). */
private LinkedList<ReplayFrame> replayFrames; 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;
// game-related variables // game-related variables
private GameContainer container; private GameContainer container;
private StateBasedGame game; private StateBasedGame game;
@ -213,6 +222,11 @@ public class Game extends BasicGameState {
int width = container.getWidth(); int width = container.getWidth();
int height = container.getHeight(); int height = container.getHeight();
// create offscreen graphics
offscreen = new Image(width, height);
gOffscreen = offscreen.getGraphics();
gOffscreen.setBackground(Color.black);
// create the associated GameData object // create the associated GameData object
data = new GameData(width, height); data = new GameData(width, height);
} }
@ -222,9 +236,15 @@ public class Game extends BasicGameState {
throws SlickException { throws SlickException {
int width = container.getWidth(); int width = container.getWidth();
int height = container.getHeight(); int height = container.getHeight();
g.setBackground(Color.black);
// "flashlight" mod: initialize offscreen graphics
if (GameMod.FLASHLIGHT.isActive()) {
gOffscreen.clear();
Graphics.setCurrent(gOffscreen);
}
// background // background
g.setBackground(Color.black);
float dimLevel = Options.getBackgroundDim(); float dimLevel = Options.getBackgroundDim();
if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel, false)) { if (Options.isDefaultPlayfieldForced() || !osu.drawBG(width, height, dimLevel, false)) {
Image playfield = GameImage.PLAYFIELD.getImage(); Image playfield = GameImage.PLAYFIELD.getImage();
@ -233,6 +253,9 @@ public class Game extends BasicGameState {
playfield.setAlpha(1f); playfield.setAlpha(1f);
} }
if (GameMod.FLASHLIGHT.isActive())
Graphics.setCurrent(g);
int trackPosition = MusicController.getPosition(); int trackPosition = MusicController.getPosition();
if (pauseTime > -1) // returning from pause screen if (pauseTime > -1) // returning from pause screen
trackPosition = pauseTime; trackPosition = pauseTime;
@ -257,9 +280,45 @@ 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 // break periods
if (osu.breaks != null && breakIndex < osu.breaks.size()) { if (osu.breaks != null && breakIndex < osu.breaks.size() && breakTime > 0) {
if (breakTime > 0) {
int endTime = osu.breaks.get(breakIndex); int endTime = osu.breaks.get(breakIndex);
int breakLength = endTime - breakTime; int breakLength = endTime - breakTime;
@ -303,17 +362,10 @@ public class Game extends BasicGameState {
arrow.draw(width * 0.75f, height * 0.75f); 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;
}
} }
// non-break
else {
// game elements // game elements
data.drawGameElements(g, false, objectIndex == 0); data.drawGameElements(g, false, objectIndex == 0);
@ -385,16 +437,10 @@ public class Game extends BasicGameState {
} }
} }
// draw hit objects in reverse order, or else overlapping objects are unreadable // draw hit objects
Stack<Integer> stack = new Stack<Integer>(); if (!GameMod.FLASHLIGHT.isActive())
for (int i = objectIndex; i < hitObjects.length && osu.objects[i].getTime() < trackPosition + approachTime; i++) drawHitObjects(g, trackPosition);
stack.add(i); }
while (!stack.isEmpty())
hitObjects[stack.pop()].draw(g, trackPosition);
// draw OsuHitObjectResult objects
data.drawHitResults(trackPosition);
if (GameMod.AUTO.isActive()) if (GameMod.AUTO.isActive())
GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f); GameImage.UNRANKED.getImage().drawCentered(width / 2, height * 0.077f);
@ -434,6 +480,62 @@ public class Game extends BasicGameState {
mouseY = replayY; mouseY = replayY;
} }
skipButton.hoverUpdate(delta, mouseX, mouseY); 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 if (isLeadIn()) { // stop updating during song lead-in
leadInTime -= delta; leadInTime -= delta;
@ -515,8 +617,6 @@ public class Game extends BasicGameState {
return; return;
} }
int trackPosition = MusicController.getPosition();
// timing points // timing points
if (timingPointIndex < osu.timingPoints.size()) { if (timingPointIndex < osu.timingPoints.size()) {
OsuTimingPoint timingPoint = osu.timingPoints.get(timingPointIndex); 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<Integer> stack = new Stack<Integer>();
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. * Loads all required data from an OsuFile.
* @param osu the OsuFile to load * @param osu the OsuFile to load