package org.erikaredmark.monkeyshines.menu; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferStrategy; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.sound.sampled.Clip; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; import org.erikaredmark.monkeyshines.World; import org.erikaredmark.monkeyshines.WorldStatistics; import org.erikaredmark.monkeyshines.global.SoundUtils; /** * * This is the screen that appears after a level has been finished properly, when it now must tally up * the score. * <p/> * It is CRTICIAL that the game timer be STOPPED before using this class. * <p/> * This class takes control of the thread that creates it and draws to the provided surface, in specific * hardcoded intervals, different game statistics, before finally reliquishing control back to the thread. * It is presumed that the graphics surface passed is visible; a callback is used to allow any parent objects * to realise they must redraw for each new statistic. * <p/> * This object must be disposed after using to clean up the graphics and sound resources that are loaded * for its operation. * * @author Erika Redmark * */ public final class EndGameBonusAnimation { private static final String CLASS_NAME = "org.erikaredmark.monkeyshines.menu.EndGameBonusSurface"; private static final Logger LOGGER = Logger.getLogger(CLASS_NAME); private static final int ALL_FIELDS_X = 470; private static final int FRUIT_COLLECTED_Y = 198; private static final int FRUIT_BONUS_Y = 238; private static final int TIME_BONUS_Y = 278; private static final int SCORE_Y = 318; private static final int TOTAL_SCORE_Y = 368; // Not intended to be instantiated outside of this class. Instantiated in method context and // state incremented until done. private EndGameBonusAnimation() { try { // Okay if we can't play sounds. Not okay if background doesn't load. background = ImageIO.read(EndGameBonusAnimation.class.getResourceAsStream("/resources/graphics/mainmenu/endgame/bonusTally.png") ); tallySwoosh = SoundUtils.clipFromOggStream(EndGameBonusAnimation.class.getResourceAsStream("/resources/sounds/mainmenu/endgame/bonusTally.ogg"), "bonusTally.ogg"); } catch (UnsupportedAudioFileException | IOException e) { throw new RuntimeException("Missing resource in .jar file: " + e.getMessage(), e); } catch (LineUnavailableException e) { LOGGER.log(Level.WARNING, "Could not play end game tally sounds: " + e.getMessage() + ". Check .jar integrity.", e); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Could not play end game tally sounds, unexpected exception " + e.getMessage(), e); } assert background != null; animationState = AnimationState.FRUIT_COLLECTED; }; // Mutates the g2d instance to set it to the right font, if that font was successfully loaded. // In either case, it sets up the font for display on the tally page. private static void setupTallyFont(Graphics2D g2d) { // Font chicago = CoreResource.INSTANCE.getChicago(); // if (chicago != null) { // g2d.setFont(chicago); // } g2d.setColor(Color.GREEN); } /** * * Primary object cycles through each state in order before being disposed. Each state draws a different thing to a different * place on the provided g2d instance. State is transitioned explicitly. This is intended to handle drawing to both * volatile and non-volatile surfaces, as drawing to volatile surfaces may have to be repeated for state transition if video * memory was lost. * * @author Erika Redmark * */ private enum AnimationState { // Note: All draw strings use Font Metrics for Right Alignment. FRUIT_COLLECTED { @Override public void drawState(Graphics2D g2d, WorldStatistics stats) { setupTallyFont(g2d); String stringToDraw = String.valueOf(stats.getFuritCollectedPercent() ) + "%"; int rightAlign = ALL_FIELDS_X - g2d.getFontMetrics().stringWidth(stringToDraw); g2d.drawString(stringToDraw, rightAlign, FRUIT_COLLECTED_Y); } @Override public AnimationState nextState() { return FRUIT_BONUS; } @Override public int waitTime() { return 1500; } }, FRUIT_BONUS { @Override public void drawState(Graphics2D g2d, WorldStatistics stats) { setupTallyFont(g2d); String stringToDraw = String.valueOf(stats.getFruitBonus() ); int rightAlign = ALL_FIELDS_X - g2d.getFontMetrics().stringWidth(stringToDraw); g2d.drawString(stringToDraw, rightAlign, FRUIT_BONUS_Y); } @Override public AnimationState nextState() { return TIME_BONUS; } @Override public int waitTime() { return 1500; } }, TIME_BONUS { @Override public void drawState(Graphics2D g2d, WorldStatistics stats) { setupTallyFont(g2d); String stringToDraw = String.valueOf(stats.getTimeBonus() ); int rightAlign = ALL_FIELDS_X - g2d.getFontMetrics().stringWidth(stringToDraw); g2d.drawString(stringToDraw, rightAlign, TIME_BONUS_Y); } @Override public AnimationState nextState() { return SCORE; } @Override public int waitTime() { return 1500; } }, SCORE { @Override public void drawState(Graphics2D g2d, WorldStatistics stats) { setupTallyFont(g2d); String stringToDraw = String.valueOf(stats.getRawScore() ); int rightAlign = ALL_FIELDS_X - g2d.getFontMetrics().stringWidth(stringToDraw); g2d.drawString(stringToDraw, rightAlign, SCORE_Y); } @Override public AnimationState nextState() { return TOTAL_SCORE; } @Override public int waitTime() { return 2000; } }, TOTAL_SCORE { @Override public void drawState(Graphics2D g2d, WorldStatistics stats) { setupTallyFont(g2d); String stringToDraw = String.valueOf(stats.getTotalScore() ); int rightAlign = ALL_FIELDS_X - g2d.getFontMetrics().stringWidth(stringToDraw); g2d.drawString(stringToDraw, rightAlign, TOTAL_SCORE_Y); } @Override public AnimationState nextState() { return null; } @Override public int waitTime() { return 4000; } }; /** * * Draws this state to the given graphics context. The appropriate data will be taken from the statistics. * * @param g2d * graphics surface to draw on * * @param stats * */ public abstract void drawState(Graphics2D g2d, WorldStatistics stats); /** * * Returns the next state in sequence. Returns {@code null} at the last state. * */ public abstract AnimationState nextState(); /** * * Returns the amount of time, in milliseconds, that should elapse before the next state change. * */ public abstract int waitTime(); } /** * * Runs the bonus tally part of the game by confiscating the graphics context and drawing the * tally animation on it. This method will return once the animation is complete. This does * not return anything useful and serves only as an indication to the player how well they * did. All total score data is calculated and saved once the game timer stops in the World, so * this does not mutate that instance in any way either. * <p/> * World timer must be stopped before calling this method. * <p/> * This this method doesn't not actually return an instance of the object, the actual underlying * instance data and all other resources will be disposed of upon completion of this method. * * @param g2d * graphics context to draw animation to * * @param completedWorld * the completed world (game timer stopped) of which this object will get the actual * tally data for display * * @param repaintCallback * callback for each frame of animation, typically used to alert whatever drawing system * that an update has been made and needs to be redrawn * */ public static void runOn(Graphics2D g2d, World completedWorld, Runnable repaintCallback) { EndGameBonusAnimation animation = new EndGameBonusAnimation(); final WorldStatistics stats = completedWorld.getStatistics(); // Background need only be drawn once. g2d.drawImage(animation.background, 0, 0, null); while (animation.animationState != null) { animation.animationState.drawState(g2d, stats); repaintCallback.run(); if (animation.tallySwoosh.isActive() ) animation.tallySwoosh.stop(); animation.tallySwoosh.setFramePosition(0); animation.tallySwoosh.start(); try { Thread.sleep(animation.animationState.waitTime() ); } catch (InterruptedException e) { LOGGER.log(Level.WARNING, CLASS_NAME + ": Thread interrupted whilst waiting between animations on Tally screen due to: " + e.getMessage(), e); // Not to worry, just means this will jump by faster than normal. Not desirable but certainly not crash/do-over worthy. } animation.animationState = animation.animationState.nextState(); } // Animation complete, nothing more to do. Cede control back to caller. } /** * * See {@link runOn(Graphics2D, World, Runnable)}. * <p/> * Performs the same function, but assumes a volatile surface with a buffer strategy. This is required since * video memory contents may have to be refreshed suddenly. * * @param buffer * buffer strategy for the volatile drawing * * @param completedWorld * the completed world (game timer stopped) of which this object will get the actual * tally data for display * */ public static void runOnVolatile(BufferStrategy buffer, World completedWorld) { EndGameBonusAnimation animation = new EndGameBonusAnimation(); final WorldStatistics stats = completedWorld.getStatistics(); List<AnimationState> previousStates = new ArrayList<>(5); while (animation.animationState != null) { // We must ALWAYS draw the background. Buffer strategy may use multiple buffers // and only by drawing completely will we prevent the last frame from the game // being blitted accidentally. Because of this, we must blit the current state // and all previous states. do { do { Graphics2D g2d = (Graphics2D) buffer.getDrawGraphics(); try { g2d.drawImage(animation.background, 0, 0, null); for (AnimationState state : previousStates) { state.drawState(g2d, stats); } animation.animationState.drawState(g2d, stats); } finally { g2d.dispose(); } } while (buffer.contentsRestored() ); buffer.show(); } while (buffer.contentsLost() ); if (animation.tallySwoosh.isActive() ) animation.tallySwoosh.stop(); animation.tallySwoosh.setFramePosition(0); animation.tallySwoosh.start(); try { Thread.sleep(animation.animationState.waitTime() ); } catch (InterruptedException e) { LOGGER.log(Level.WARNING, CLASS_NAME + ": Thread interrupted whilst waiting between animations on Tally screen due to: " + e.getMessage(), e); // Not to worry, just means this will jump by faster than normal. Not desirable but certainly not crash/do-over worthy. } previousStates.add(animation.animationState); animation.animationState = animation.animationState.nextState(); } } private BufferedImage background; // Played when a stat is displayed on the tally, with a non-zero value private Clip tallySwoosh; private AnimationState animationState; }