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

/* Hra se na panelu JPanel zobrazuje za použití aktivního vykreslování
   s pomocí časovače z knihony Java 3D.

   Tato třída připraví herní svět a sprity, které po každé periodě dané jistým
   počtem nanosekund aktualizuje a vykresluje.

   Složitost herního světa: pohybující se dlaždicovou podlahu obsahující
   předměty, překážky a pohybující se sprity má na starosti objekt typu WorldDisplay.

   Hra začíná jednoduchou úvodní obrazovkou, která se v průběhu hry
   objeví také jako okno s nápovědou, při jehož zobrazení dojde k pozastavení hry.   

   Hra se ovládá z klávesníce, události myši se neřeší.

   Konec hry (proměnná gameOver = true) nastane, pokud byl hráč zasažen stanoveným počtem
   úderů, nebo pokud sebral všechny předměty (pohár, květináč a budík).
   
*/

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


public class AlienTilesPanel extends JPanel implements Runnable
{
  private static final int PWIDTH = 800;   // velikost panelu
  private static final int PHEIGHT = 400; 

  private static final int NO_DELAYS_PER_YIELD = 16;
  /* Počet snímků s prodlevou 0 ms před tím, než dojde k útlumu animačního vlákna
   */
  private static final int MAX_FRAME_SKIPS = 5;
    // počet snímků, které lze v animačním cyklu přeskočit
    // tj. hra se aktualizuje ale nevykreslí

  // soubory s údaji o obrázcích a klipech
  private static final String IMS_INFO = "imsInfo.txt";
  private static final String SNDS_FILE = "clipsInfo.txt";

  // pozadí bude světle modré
  private static final Color lightBlue = new Color(0.17f, 0.87f, 1.0f);

  private Thread animator;           // vlákno provádějící animaci
  private volatile boolean running = false;   // pro pozastavení animačního vlákna
  private volatile boolean isPaused = false;

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


  private AlienTiles alienTop;
  private ClipsLoader clipsLoader;

  // herní entity
  private WorldDisplay world;
  private PlayerSprite player;
  private AlienSprite aliens[]; 

  private long gameStartTime;   // kdy hra začala
  private int timeSpentInGame;

  // pro ukončení 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;


  public AlienTilesPanel(AlienTiles at, long period)
  {
    alienTop = at;
    this.period = period;

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

    setFocusable(true);
    requestFocus();    // vstup od klávesnice je náš, přijí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); 

    // vytvoř herní svět, hráče a vetřelce
    createWorld(imsLoader); 

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

    // připrav nezbytnosti pro zobrazování zpráv
    msgsFont = new Font("SansSerif", Font.BOLD, 24);
    metrics = this.getFontMetrics(msgsFont);
  }  // konec metody: AlienTilesPanel()


  private void createWorld(ImagesLoader imsLoader)
  // vytvoř herní svět, hráče a vetřelce 
  {
    world = new WorldDisplay(imsLoader, this);  // herní svět, objekt typu WorldDisplay

    player = new PlayerSprite(7,12, PWIDTH, PHEIGHT, clipsLoader, imsLoader, 
                                       world, this);  // start on tile (7,12)

    aliens = new AlienSprite[4];

    aliens[0] = new AlienAStarSprite(10, 11, PWIDTH, PHEIGHT, imsLoader, world);
    aliens[1] = new AlienQuadSprite(6, 21, PWIDTH, PHEIGHT, imsLoader, world);
    aliens[2] = new AlienQuadSprite(14, 20, PWIDTH, PHEIGHT, imsLoader, world);
    aliens[3] = new AlienAStarSprite(34, 34, PWIDTH, PHEIGHT, imsLoader, world);
       // použij 2 'AStar' a 2 'Quad' sprity
       // 4. vetřelec je umístěn do nelegální lokace (34,34)

    world.addSprites(player, aliens);  // řekni hernímu světu o našich spritech
  }  // konec metody: createWorld()


  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 nastavena na true
      }
    }

    // klávesy ovlivňující dění ve hře
    if (!isPaused && !gameOver) {
      // přesuň hráče na základě stisku klávesy na numerické klávesnici
      if (keyCode == KeyEvent.VK_NUMPAD7)
        player.move(TiledSprite.NW);   // jdi na severozápad
      else if (keyCode == KeyEvent.VK_NUMPAD9)
        player.move(TiledSprite.NE);   // severovýchod
      else if (keyCode == KeyEvent.VK_NUMPAD3)
        player.move(TiledSprite.SE);   // jihovýchod
      else if (keyCode == KeyEvent.VK_NUMPAD1)
        player.move(TiledSprite.SW);   // jihozápad
      else if (keyCode == KeyEvent.VK_NUMPAD5)
        player.standStill();           // zůstaň na místě
      else if (keyCode == KeyEvent.VK_NUMPAD2)
        player.tryPickup();      // pokus se z této dlaždice sebrat předmět
    }
  }  // konec metody: processKey()


  public void gameOver()
  /* Tuto metodu mohou volat objekty typu PlayerSprite nebo WorldDisplay. 
     PlayerSprite ji volá při dostatečném počtu zásahů a WorldDisplay
     poté, co došlo k sebrání všech předmětů.
  */
  { 
    if (!gameOver) {
      gameOver = true; 
      score = (int) ((J3DTimer.getValue() - gameStartTime)/1000000000L);
      clipsLoader.play("applause", false);
    }
  } // konec metody: gameOver()



  public void addNotify()
  // před spušťením vlastní hry vyčkej, 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ť animační 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 při aktivaci / de-ikonifikaci okna JFrame
  { if (!showHelp)
      isPaused = false;  
  } 


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


  public void stopGame() 
  // volá se při 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 příliš dlouho, aktualizuj herní stav bez vykreslení,
         aby se rychlost aktualizací za sekundu přiblížila požadované hodnotě 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) {
      for(int i=0; i < aliens.length; i++)
        aliens[i].update();    // update all the aliens
    } 
  }  // 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();
    }

    // světle modré pozadí
    dbg.setColor(lightBlue);
    dbg.fillRect(0, 0, PWIDTH, PHEIGHT);

    // vykresli herní prvky: pořadí je důležité
    world.draw(dbg);  
    /* Třída WorldDisplay kreslí herní svět: dlaždicovou podlahu,
       překážky, předměty a sprity. */

    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)
  // Zobrazí čas strávený hraním, počet zásahů a počet zbývajících předmětů
  {
    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("Čas: "+ timeSpentInGame + " sekund", 15, 25);
    g.drawString( player.getHitStatus(), 15, 50);     // zeptej se objektu typu Player
    g.drawString( world.getPickupsStatus(), 15, 75);  // zeptej se objektu typu WorldDisplay
	g.setColor(Color.black);
  }  // konec metody: reportStats()



  private void gameOverMessage(Graphics g)
  // V panelu vycentruj zprávu o ukončení hry.
  {
    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.red);
    g.setFont(msgsFont);
	g.drawString(msg, x, y);
    g.setColor(Color.black);
  }  // 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.
      // (on Linux, this fixes event queue problems)
      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: AlienTilesPanel
