
// JackPanel.java
// Andrew Davison, April 2005, ad@fivedots.coe.psu.ac.th

/* Kreslící povrch hry. Používá aktivní vykreslování na panel
   typu JPanel s pomocí časovače knihovny Java 3D.

   Připraví pozadí a sprity, ketré bude aktualizovat a vykreslovat
   po každém intervalu specifikovaném v nanosekundách.

   Pozadí tvoří série pásů (pohybující se obrázky, které navazují samy na sebe),
   a pás s cihličkami, po kterých se pohybuje objekt typu JumpingSprite
   (říkáme mu jack).

   Jack se ve skutečnosti horizontálně nepohybuje. Tento pohyb je způsobem
   iluzí pohybu pozadí.

   Dále se hře vyskytuje sprite ohnivé koule, která se snaží zasáhnout Jacka.
   Letí horizontálně z pravého k levému okraji panelu. Po MAX_HITS zásazích
   hra končí. Každý zásah doprovází animace exploze a zvukový efekt.

   Hra začíná jednoduchou úvodní obrazovkou, která během hry slouží jako
   okno s nápovědou. Při jejím zobrazení se hra pozastaví.

   Hra se ovládá pouze z klávesnice, události myši se nezachytávají.
*/

import javax.swing.*;
import java.awt.image.*;
import java.awt.event.*;
import java.awt.*;
import com.sun.j3d.utils.timer.J3DTimer;


public class JackPanel extends JPanel 
                     implements Runnable, ImagesPlayerWatcher
{
  private static final int PWIDTH = 500;   // velikost panelu
  private static final int PHEIGHT = 360; 

  private static final int NO_DELAYS_PER_YIELD = 16;
  /* Počet snímků s prodlevou 0 ms před tím, než animační vlákno přenechá
     svůj čas ostatním vláknům. */
  private static final int MAX_FRAME_SKIPS = 5;
    // počet snímků, které mohou být v jakémkoliv animačním cyklu přeskočeny
    // tj. aktualizuje se stav hry, ale nedojde k jejímu vykreslení

  // image, bricks map, clips loader information files
  private static final String IMS_INFO = "imsInfo.txt";
  private static final String BRICKS_INFO = "bricksInfo.txt";
  private static final String SNDS_FILE = "clipsInfo.txt";

  // jména zvukových klipů s explozemi
  private static final String[] exploNames ={"explo1", "explo2", "explo3"};

  private static final int MAX_HITS = 20;
    // kolikrát může být jack zasažen ohnivou koulí, než zemře (a skončí hra)


  private Thread animator;           // vlákno provádející animaci
  private volatile boolean running = false;   // pro pozastavení animacního vlákna
  private volatile boolean isPaused = false;

  private long period;                // interval mezi vykresleními - v nanosekundách

  private JumpingJack jackTop;
  private ClipsLoader clipsLoader;

  private JumperSprite jack;          // naše sprity
  private FireBallSprite fireball;
  private RibbonsManager ribsMan;     // objekt řídící pruhy obrázků
  private BricksManager bricksMan;    // objekt řídící cihličky

  private long gameStartTime;   // kdy hra zacala
  private int timeSpentInGame;

  // pro ukoncení hry
  private volatile boolean gameOver = false;
  private int score = 0;

  // zobrazování zpráv
  private Font msgsFont;
  private FontMetrics metrics;

  // vykreslování na povrch mimo obrázovku
  private Graphics dbg; 
  private Image dbImage = null;

  // pro zobrazení titulní/pomocné obrazovky
  private boolean showHelp;
  private BufferedImage helpIm;

  // souvisí s animací exploze
  private ImagesPlayer explosionPlayer = null;
  private boolean showExplosion = false;
  private int explWidth, explHeight;   // rozměry obrázku
  private int xExpl, yExpl;   // souřadnice, na než se obrázek vykresluje

  private int numHits = 0;   // kolikrát již byl Jack zasažen



  public JackPanel(JumpingJack jj, long period)
  {
    jackTop = jj;
    this.period = period;

    setDoubleBuffered(false);
    setBackground(Color.white);
    setPreferredSize( new Dimension(PWIDTH, PHEIGHT));

    setFocusable(true);
    requestFocus();    // vstup od klávesnice je náš, prijímáme její události

	addKeyListener( new KeyAdapter() {
       public void keyPressed(KeyEvent e)
       { processKey(e);  }
     });

    // inicializuj načítací objekty
    ImagesLoader imsLoader = new ImagesLoader(IMS_INFO); 
    clipsLoader = new ClipsLoader(SNDS_FILE); 

    // inicializuj herní entity
    bricksMan = new BricksManager(PWIDTH, PHEIGHT, BRICKS_INFO, imsLoader);
    int brickMoveSize = bricksMan.getMoveSize();

    ribsMan = new RibbonsManager(PWIDTH, PHEIGHT, brickMoveSize, imsLoader);

    jack = new JumperSprite(PWIDTH, PHEIGHT, brickMoveSize, bricksMan, 
                               imsLoader, (int)(period/1000000L) ); // v ms

    fireball = new FireBallSprite(PWIDTH, PHEIGHT, imsLoader, this, jack);


    // připrav animaci exploze
    explosionPlayer =  new ImagesPlayer("explosion", (int)(period/1000000L), 
                                                0.5, false, imsLoader);
    BufferedImage explosionIm = imsLoader.getImage("explosion");
    explWidth = explosionIm.getWidth();
    explHeight = explosionIm.getHeight();
    explosionPlayer.setWatcher(this);     // konec animace hlas zpět

    // připrav titulní/pomocnou obrazovku
    helpIm = imsLoader.getImage("title");
    showHelp = true;    // zobraz ji při spuštění
    isPaused = true;

    // nastav font pro zprávy
    msgsFont = new Font("SansSerif", Font.BOLD, 24);
    metrics = this.getFontMetrics(msgsFont);
  }  // konec metody: JackPanel()


  private void processKey(KeyEvent e)
  // řeší ukončení, nápovědu a klávesy ovlivňující dění ve hře
  {
    int keyCode = e.getKeyCode();

    // klávesy ukončující aplikaci
	// pro snadné ukončení z celoobrazovkové konfigurace
	// naslouchej klávesám esc, q, end, ctrl-c na herní obrazovce
    if ((keyCode == KeyEvent.VK_ESCAPE) || (keyCode == KeyEvent.VK_Q) ||
        (keyCode == KeyEvent.VK_END) ||
        ((keyCode == KeyEvent.VK_C) && e.isControlDown()) )
      running = false;

    // ovládání nápovědy
    if (keyCode == KeyEvent.VK_H) {
      if (showHelp) {  // nápověda se zobrazuje
        showHelp = false;  // vypnout
        isPaused = false;
      }
      else {  // nápověda se nezobrazuje
       showHelp = true;    // zobraz ji
       isPaused = true;    // proměnná isPaused již může být true
      }
    }

    // klávesy ovlivňující dění ve hře
    if (!isPaused && !gameOver) {
      // na základě stisku kurzorových kláves zahaj pohyb spritu a pruhů
      if (keyCode == KeyEvent.VK_LEFT) {
        jack.moveLeft(); 
        bricksMan.moveRight();   // cihličky a pásy se pohybují opačným směrem
        ribsMan.moveRight();
      }
      else if (keyCode == KeyEvent.VK_RIGHT) {
        jack.moveRight();
        bricksMan.moveLeft();
        ribsMan.moveLeft();
      } 
      else if (keyCode == KeyEvent.VK_UP)
        jack.jump();    // skákání nemá na cihličky/pásy žádný efekt
      else if (keyCode == KeyEvent.VK_DOWN) {
        jack.stayStill();
        bricksMan.stayStill();
        ribsMan.stayStill();
      }
    }
  }  // konec metody: processKey()


  public void showExplosion(int x, int y)
  // volá se ze spritu ohnivé koule při zásahu jacka v bodu (x,y)
  { if (!showExplosion) {  // v jednu chvíli je povolena jen jedna exploze
      showExplosion = true;
      xExpl = x - explWidth/2;   // (x,y) je střed exploze
      yExpl = y - explHeight/2;

      /* Přehraj zvukový efekt exploze, ale postupně je procházej.
         Tím se do hry přidá rozmanitost a obejde se omezení způsobené
         nemožností přehrát více instancí téhož klipu zaráz.*/
      clipsLoader.play( exploNames[numHits%exploNames.length], false);
      numHits++;
    } 
  } // konec metody: showExplosion()


  public void sequenceEnded(String imageName)
  // volá se z objektu typu ImagesPlayer, když skončí animace exploze
  {  showExplosion = false;
     explosionPlayer.restartAt(0);   // vynuluj animaci pro příští přehrávání

     if (numHits >= MAX_HITS) { 
       gameOver = true; 
       score = (int) ((J3DTimer.getValue() - gameStartTime)/1000000000L);
      clipsLoader.play("applause", false);

     }
  } // konec metody: sequenceEnded()



  public void addNotify()
  // před spuštením vlastní hry vyckej, než bude panel JPanel přidán do okna JFrame
  { super.addNotify();   // předej upozornění
    startGame();         // spusť vlákno
  }


  private void startGame()
  // inicializuj a spusť vlákno 
  { 
    if (animator == null || !running) {
      animator = new Thread(this);
	  animator.start();
    }
  } // konec metody: startGame()
    

  // ------------- metody životního cyklu hry ------------
  // volají se z metody okna JFrame implementující rozhraní WindowListener


  public void resumeGame()
  // volá se pri aktivaci / de-ikonifikaci okna JFrame
  { if (!showHelp)    // CHANGED
      isPaused = false;  
  } 


  public void pauseGame()
  // volá se pri deaktivaci / ikonifikaci okna JFrame
  { isPaused = true;   } 


  public void stopGame() 
  // volá se pri uzavírání okna JFrame
  {  running = false;   }

  // ----------------------------------------------

  public void run()
  /* Snímky animace vykreslujeme uvnitř cyklu while. */
  {
    long beforeTime, afterTime, timeDiff, sleepTime;
    long overSleepTime = 0L;
    int noDelays = 0;
    long excess = 0L;

    gameStartTime = J3DTimer.getValue();
    beforeTime = gameStartTime;

	running = true;

	while(running) {
	  gameUpdate();
      gameRender();
      paintScreen();

      afterTime = J3DTimer.getValue();
      timeDiff = afterTime - beforeTime;
      sleepTime = (period - timeDiff) - overSleepTime;  

      if (sleepTime > 0) {   // v této iteraci zbyl ještě nějaký čas
        try {
          Thread.sleep(sleepTime/1000000L);  // nanosekundy -> milisekundy
        }
        catch(InterruptedException ex){}
        overSleepTime = (J3DTimer.getValue() - afterTime) - sleepTime;
      }
      else {    // sleepTime <= 0; snímek trval déle než-li interval
        excess -= sleepTime;  // ulož překročený čas
        overSleepTime = 0L;

        if (++noDelays >= NO_DELAYS_PER_YIELD) {
          Thread.yield();   // dej jinému vláknu příležitost k běhu
          noDelays = 0;
        }
      }

      beforeTime = J3DTimer.getValue();

      /* Trvá-li snímek príliš dlouho, aktualizuj herní stav bez vykreslení,
         aby se rychlost aktualizací za sekundu priblížila požadované hodnote FPS. */
      int skips = 0;
      while((excess > period) && (skips < MAX_FRAME_SKIPS)) {
        excess -= period;
	    gameUpdate();    // aktualizuj stav hry, ale nevykresluj ji
        skips++;
      }
	}
    System.exit(0);   // rozlučme se z okýnkem
  } // konec metody: run()


  private void gameUpdate() 
  { 
    if (!isPaused && !gameOver) {
      if (jack.willHitBrick()) { // nejdříve zkontroluj, nedošlo-li ke kolizi
        jack.stayStill();    // znehybni Jacka a scenerii
        bricksMan.stayStill();
        ribsMan.stayStill();
      }
      ribsMan.update();   // aktualizuj pozadí a sprity
      bricksMan.update();
      jack.updateSprite();
      fireball.updateSprite();

      if (showExplosion)
        explosionPlayer.updateTick();  // aktualizuj animaci
    }
  }  // konec metody: gameUpdate()


  private void gameRender()
  {
    if (dbImage == null){
      dbImage = createImage(PWIDTH, PHEIGHT);
      if (dbImage == null) {
        System.out.println("dbImage má hodnotu null");
        return;
      }
      else
        dbg = dbImage.getGraphics();
    }

    // vykresli bíle pozadí
    dbg.setColor(Color.white);
    dbg.fillRect(0, 0, PWIDTH, PHEIGHT);

    // vykresli herní prvky: pořadí je důležité
    ribsMan.display(dbg);       // pásy herního pozadí
    bricksMan.display(dbg);     // cihličky
    jack.drawSprite(dbg);       // naše sprity
    fireball.drawSprite(dbg); 

    if (showExplosion)      // vykresli explozi (před Jackem)
      dbg.drawImage(explosionPlayer.getCurrentImage(), xExpl, yExpl, null);

    reportStats(dbg);

    if (gameOver)
      gameOverMessage(dbg);

    if (showHelp)    // úplně vpředu vykresli nápovědu (je-li zapnutá)
      dbg.drawImage(helpIm, (PWIDTH-helpIm.getWidth())/2, 
                          (PHEIGHT-helpIm.getHeight())/2, null);
  }  // konec metody: gameRender()


  private void reportStats(Graphics g)
  // Vypiš počet zásahů a čas strávený hraním
  {
    if (!gameOver)    // po skončení hry zastav zvyšování časovače
      timeSpentInGame = 
          (int) ((J3DTimer.getValue() - gameStartTime)/1000000000L);  // nanosekundy --> sekundy
	g.setColor(Color.red);
    g.setFont(msgsFont);
	g.drawString("Zásahů: " + numHits + "/" + MAX_HITS, 15, 25);
	g.drawString("Čas: + timeSpentInGame + " sekund", 15, 50);
	g.setColor(Color.black);
  }  // konec metody: reportStats()


  private void gameOverMessage(Graphics g)
  // Zprávu o konci hry zobraz uprostřed panelu.
  {
    String msg = "Hra skončila. Dosažený počet bodů: " + score;

	int x = (PWIDTH - metrics.stringWidth(msg))/2; 
	int y = (PHEIGHT - metrics.getHeight())/2;
	g.setColor(Color.black);
    g.setFont(msgsFont);
	g.drawString(msg, x, y);
  }  // konec metody: gameOverMessage()


  private void paintScreen()
  // obrázek z paměti na obrazovku přenášíme pomocí aktivního vykreslování
  { 
    Graphics g;
    try {
      g = this.getGraphics();
      if ((g != null) && (dbImage != null))
        g.drawImage(dbImage, 0, 0, null);
      // v některých systémech musíme použít synchronizaci obrazovky.
      // (na Linuxu takto opravíme problémy s frontou událostí)
      Toolkit.getDefaultToolkit().sync();
      g.dispose();
    }
    catch (Exception e)
    { System.out.println("Chyba při práci s grafickým kontextem: " + e);  }
  } // konec metody: paintScreen()


}  // konec třídy: JackPanel class
