package rabbitescape.ui.swing; import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.util.Map; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import static rabbitescape.engine.i18n.Translation.t; import rabbitescape.engine.LevelWinListener; import rabbitescape.engine.Token; import rabbitescape.engine.World; import rabbitescape.engine.config.Config; import rabbitescape.engine.solution.PlaceTokenAction; import rabbitescape.engine.solution.SolutionDemo; import rabbitescape.engine.solution.SolutionInterpreter; import rabbitescape.engine.solution.SolutionRecorder; import rabbitescape.engine.solution.SolutionRecorderTemplate; import rabbitescape.engine.util.Util; import rabbitescape.render.GameLaunch; import rabbitescape.render.androidlike.Sound; import rabbitescape.render.gameloop.GameLoop; import rabbitescape.render.gameloop.GeneralPhysics; import rabbitescape.render.gameloop.Physics; import rabbitescape.render.gameloop.WaterAnimation; import rabbitescape.ui.swing.SwingGameInit.WhenUiReady; public class SwingGameLaunch implements GameLaunch { /** * A loop that just draws the game window when it's behind the * intro dialog. */ class MiniGameLoop implements Runnable { public boolean running = true; @Override public void run() { while ( running ) { graphics.draw( physics.frameNumber() ); try { Thread.sleep( 500 ); } catch ( InterruptedException e ) { e.printStackTrace(); } } } } public static final String NOT_DEMO_MODE = "NOT_DEMO_MODE"; public final World world; private final GeneralPhysics physics; public final SwingGraphics graphics; private final GameUi jframe; private final GameLoop loop; private final MainJFrame frame; public final SolutionRecorderTemplate solutionRecorder; private final SwingPlayback swingPlayback; private final FrameDumper frameDumper; /** * @param solutionIndex natural number values indicate demo mode. It is the index of the * solution from the rel file to play. */ public SwingGameLaunch( SwingGameInit init, World world, LevelWinListener winListener, Sound sound, Config config, PrintStream debugout, String solution, boolean frameDumping ) { this.world = world; SolutionInterpreter solutionInterpreter = createSolutionInterpreter( solution, world ); this.frame = init.frame; this.solutionRecorder = new SolutionRecorder(); if ( frameDumping ) { this.frameDumper = new FrameDumper(); } else { this.frameDumper = FrameDumper.createInactiveDumper(); } this.swingPlayback = new SwingPlayback( this ); WaterAnimation waterAnimation = new WaterAnimation( world ) ; this.physics = new GeneralPhysics( world, waterAnimation, winListener, false, solutionRecorder, solutionInterpreter, swingPlayback, frameDumping ); // This blocks until the UI is ready: WhenUiReady uiPieces = init.waitForUi.waitForUi(); this.jframe = uiPieces.jframe; this.graphics = new SwingGraphics( world, uiPieces.jframe, uiPieces.bitmapCache, sound, frameDumper, waterAnimation ); // Used for redraw after window is resized. frame.setGraphics( graphics ); jframe.setGameLaunch( this ); sound.setMusic( world.music ); loop = new GameLoop( new SwingInput(), physics, waterAnimation, graphics, config, debugout ); } public GameUi getUi() { return jframe; } public void toggleSpeed() { physics.fast = !physics.fast; } private static SolutionInterpreter createSolutionInterpreter( String solution, World world ) { if ( solution.equals( NOT_DEMO_MODE ) ) { return SolutionInterpreter.getNothingPlaying(); } SolutionDemo demo = new SolutionDemo( solution, world ); return new SolutionInterpreter( demo.solution ); } public void stop() { loop.pleaseStop(); } @Override public void run( String[] args ) { showIntroDialog(); // After the dialog has gone, return keyboard // focus so keystrokes are not lost. frame.getRootPane().grabFocus(); loop.run(); System.out.println( solutionRecorder.getRecord() ); } private static class AnswerHolder { public int answer; } /** * Must not be called from within event loop. */ private int showDialog( final String title, final Object message, final Object[] options ) { final AnswerHolder holder = new AnswerHolder(); runSwingCodeWithGameLoopBehind( new Runnable() { @Override public void run() { holder.answer = JOptionPane.showOptionDialog( frame, message, title, JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, null, options, options[ options.length - 1 ] ); } } ); return holder.answer; } /** * Must be called from within the event loop. */ private boolean askExplodeAll() { MiniGameLoop bgDraw = new MiniGameLoop(); new Thread( bgDraw ).start(); try { String[] buttons = new String[] { t( "Cancel" ), t( "Explode!" ) }; int ret = JOptionPane.showOptionDialog( frame, t( "Do you want to explode your rabbits?" ), t( "Explode all rabbits?" ), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, buttons, buttons[1] ); return ( ret == 1 ); } finally { bgDraw.running = false; } } private void runSwingCodeWithGameLoopBehind( Runnable doRun ) { MiniGameLoop bgDraw = new MiniGameLoop(); new Thread( bgDraw ).start(); try { SwingUtilities.invokeAndWait( doRun ); } catch ( InvocationTargetException e ) { throw new RuntimeException( e ); } catch ( InterruptedException e ) { // Continue if interrupted } finally { bgDraw.running = false; } } /** * Not called from within event loop. */ private void showIntroDialog() { if (inDemoMode()) { return; } Util.Function<String, String> insertNewlines = new Util.Function<String, String>() { @Override public String apply( String inp ) { return Util.wrapToNewline( inp, DialogText.lineLength ); } }; showDialogs( world.name, Util.concat( new Object[] { DialogText.introText( this.frame, world ) }, Util.map( insertNewlines, world.hints, new String[3] ) ) ); } private void showDialogs( String title, Object[] messages ) { // Keep showing dialogs until we click start int retVal = 0; while ( retVal == 0 ) { for ( int i = 0; i < messages.length; ++i ) { if ( "".equals( messages[i] ) ) { // No more messages break; } Object nextMessage = nextMessage( messages, i ); Object[] options = nextOptions( nextMessage, i ); retVal = showDialog( title, messages[i], options ); if ( options.length == 1 ) { // There was only 1 option - we clicked Start retVal = 1; } if ( retVal != 0 ) { // We clicked Start or closed dialog break; } } } } private Object[] nextOptions( Object nextMessage, int i ) { if ( "".equals( nextMessage ) ) { if ( i == 0 ) { // There were no hints at all return new Object[] { t( "Start" ) }; } else { // Go back to level description return new Object[] { t( "Info" ), t( "Start" ) }; } } else { // Another hint to come return new Object[] { t( hintName( i ) ), t( "Start" ) }; } } private String hintName( int i ) { if ( i == 0 ) { return "Hint"; } else { return "Hint " + ( i + 1 ); } } private Object nextMessage( Object[] messages, int i ) { if ( i >= messages.length - 1 ) { return ""; } else { return messages[ i + 1 ]; } } /** * Not called from within event loop. */ private void showWonDialog() { if (inDemoMode()) { try { Thread.sleep( 2000 ); } catch ( InterruptedException e ) { // Ignore } return; } showDialog( t( "You won!" ), t( "Saved: ${num_saved} Needed: ${num_to_save}", DialogText.statsValues( world ) ), new Object[] { t( "Ok" ) } ); } /** * Not called from within event loop. */ private void showLostDialog() { if (inDemoMode()) { try { Thread.sleep( 2000 ); } catch ( InterruptedException e ) { // Ignore } return; } showDialog( t( "You lost!" ), t( "Saved: ${num_saved} Needed: ${num_to_save}", DialogText.statsValues( world ) ), new Object[] { t( "Ok" ) } ); } @Override public void showResult() { switch ( world.completionState() ) { case WON: { showWonDialog(); break; } case LOST: { showLostDialog(); break; } default: { // Maybe the user clicked back - do nothing here } } jframe.exit(); } public void checkExplodeAll() { world.setPaused( true ); boolean explode = askExplodeAll(); world.setPaused( false ); if ( explode ) { world.changes.explodeAllRabbits(); } } public int addToken( int tileX, int tileY, Token.Type ability ) { int prev = world.abilities.get( ability ); int now = physics.addToken( tileX, tileY, ability ); if ( now != prev ) { graphics.playSound( "place_token" ); } if ( prev != now ) { solutionRecorder.append( new PlaceTokenAction( tileX, tileY ) ); } return now; } public Map<Token.Type, Integer> getAbilities() { return world.abilities; } public void addStatsChangedListener( Physics.StatsChangedListener listener ) { physics.addStatsChangedListener( listener ); } private boolean inDemoMode() { return ( !physics.solutionInterpreter.emptySteps ); } }