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

/* Třídá WorldDisplay spravuje:
      - pohybující se dlaždicovou podlahu reprezentovanou jediným obrázkem
      - nepřístupné oblasti na podlaze
      - překážky okupující některé dlaždice
      - předměty na některých dlaždicích
      - sprity hráče a vetřelce

   Dále má na starosti komunikaci mezi hráčem a sprity, takže může sledovat/řídit
   jejich interakci.

   Používá 3 hlavní datové struktury:
      - pole obstacles[][] uchovávající info o tom, které dlaždice
        jsou v nepřístupné oblasti nebo obsahují překážku

      - objekty typu WorldItem, které uchovávají překážky, předměty a
        sprity uspořádané dle řádku pro snazší vykreslování

      - počítadlo numPickups pro zazamenání, kolik předmětů ve hře dosud
        zbývá

    Metody spadají do 5 hlavních skupin:
      - načítání údajů o podlaze         // údaje vztahující se k obrázku podlahy
      - načítání údajů o objektech herního světa // údaje o nepřístupných oblastech, 
                                                 // překážkách, předmětech      
      - vztahující se k předmětům
      - vztahující se ke spritům
      - ostatní, např. draw()
*/

import java.awt.*;
import java.util.*;
import java.awt.image.*;
import java.io.*;


public class WorldDisplay
{
  private final static String WORLD_DIR = "World/";

  // typy okupantů dlaždice
  public static final int BLOCK = 0;   
  public static final int PICKUP = 1;   
  public static final int SPRITE = 2;   


  // rozměry herního světa vyjádřené jako počty dlaždic
  private int numXTiles, numYTiles;

  // maximální šířka/výška dlaždice v pixelech (including any space to next tile)
  private int tileWidth, tileHeight;

  // souřadnice začátku prvního sudého řádku
  /* souřadnice levého horního rohu obdélníka obsahujícího kosočterec */
  private int evenRowX, evenRowY;

  // souřadnice začátku prvního lichého řádku
  private int oddRowX, oddRowY;

  /* Posun v pixelech pro vykreslení levého horního rohu obrázku
     podlahy (a jejího obsahu) vzhledem k levému hornímu rohu (0,0)
     panelu JPanel. */
  private int xOffset, yOffset;   

  private ImagesLoader imsLoader;
  private AlienTilesPanel atPanel;

  private BufferedImage floorIm;    // obrázek podlahy (pozadí)

  private boolean obstacles[][];    
      // specifikuje, které dlaždice jsou neprůchozí nebo obsahují překážky
      // předměty se za překážky nepovaždují -- sprity jimi mohou procházet

  private WorldItems wItems;   
   /* Objet typu WorldItems spravuje seznam věcí, které se objeví na dalždicové podlaze:
      překážky, předměty a sprity, včetně jejich polohy. Seznam je uspořádán odzadu dopředu dle
      řádků, aby se scéna vykreslovala ve správném z-ovém pořadí.
   */

  private int numPickups = 0;    // aktuální počet předmětů v herním světě

  private int blocksCounter = 0;   
      // počítadlo, s jehož pomocí se označují překážky při ukládání do objektu typu WorldItems

  private PlayerSprite player;    // sprity
  private AlienSprite aliens[];



  public WorldDisplay(ImagesLoader imsLd, AlienTilesPanel atp)
  {
   imsLoader = imsLd;
   atPanel = atp;

   xOffset = 0; yOffset = 0;
   loadFloorInfo("worldInfo.txt");

   /* Objekt typu WorldItems používá údaje o podlaze pro určení,
      kam se mají herní objekty v rámci panelu JPanel vykreslit. */
   wItems = new WorldItems(tileWidth, tileHeight,
                             evenRowX, evenRowY, oddRowX, oddRowY);
   initObstacles();

   loadWorldObjects("worldObjs.txt");
     /* The world objects information is stored in the obstacles[][]
        array and the WorldItems object. */
  }  // konec metody: WorldDisplay()


  // ----------------- načti údaje podlahy ------------------

  private void loadFloorInfo(String wFNm)
  /* formát vstupních řádků je následující:
        image <name>          // název obrázku s podlahou
        numTiles  x  y        // počet dlaždic v x-ových a y-ových směrech
        dimTile w h           // šířka a výška jediné dlaždice
        evenRow x y           // začátek prvního sudého řádku
        oddRow x y            // začátek prvního lichého řádku
        nebo prázdné řádky 
        nebio komentáře.
  */
  { 
    String worldFNm = WORLD_DIR + wFNm;
    System.out.println("Čtený soubor: " + worldFNm);
    try {
      InputStream in = this.getClass().getResourceAsStream(worldFNm);
      BufferedReader br = new BufferedReader( new InputStreamReader(in));
      // BufferedReader br = new BufferedReader( new FileReader(worldFNm));
      String line;
      String[] tokens;
      while((line = br.readLine()) != null) {
        if (line.length() == 0)  // prázdný řádek
          continue;
        if (line.startsWith("//"))   // komentář
          continue;

        // System.out.println("Řádek: " + line);
        tokens = line.split("\\s+");
        if (tokens[0].equals("image"))
          floorIm = imsLoader.getImage(tokens[1]);  // načti obrázek
        else if (tokens[0].equals("numTiles")) {
          numXTiles = getNumber(tokens[1]);
          numYTiles = getNumber(tokens[2]);
        }
        else if (tokens[0].equals("dimTile")){
          tileWidth = getNumber(tokens[1]);
          tileHeight = getNumber(tokens[2]);
        }
        else if (tokens[0].equals("evenRow")){
          evenRowX = getNumber(tokens[1]);
          evenRowY = getNumber(tokens[2]);
        }
        else if (tokens[0].equals("oddRow")){
          oddRowX = getNumber(tokens[1]);
          oddRowY = getNumber(tokens[2]);
        }
        else
          System.out.println("Řádek nerozpoznán: " + line);
      }
      br.close();
    } 
    catch (IOException e) 
    { System.out.println("Chyba při čtení souboru: " + worldFNm);
      System.exit(1);
    }
  }  // konec metody: loadFloorInfo()



  private int getNumber(String token)
  // extrahuje číslo nebo vrátí 0
  { int num = 0;
    try {
      num = Integer.parseInt(token);
    }
    catch (NumberFormatException ex){ 
      System.out.println("Nesprávný formát pro " + token); 
    }
    return num;
  }  // konec metody: getNumber()


  // ----------------- načítání údajů o objektech herního světa ----------------


  private void loadWorldObjects(String woFNm)
  /* Existují tři druhy herních objektů: neprůchozí oblasti (n),
     překážky (b), předměty (p), které se umísťují na dané souřadnice
     dlaždice (x,y). Názvy překážek a předmětů odkazují na příslušná
     jména souborů.

     Formát vstupního řádku je následující:
        n <x1>-<y1> <x2>-<y2> .....
           .... #

        b <blockName>
          <x1>-<y1> <x2>-<y2> .....
           .... #

        p <pickupName> <x>-<y>

        nebo prázdné řádky 
        nebio komentáře.
  */
  { 
    String objsFNm = WORLD_DIR + woFNm;
    System.out.println("Čtený soubor: " + objsFNm);
    try {
      InputStream in = this.getClass().getResourceAsStream(objsFNm);
      BufferedReader br = new BufferedReader( new InputStreamReader(in));
      // BufferedReader br = new BufferedReader( new FileReader(objsFNm));
      String line;
      char ch;
      while((line = br.readLine()) != null) {
        if (line.length() == 0)  // prázdný řádek
          continue;
        if (line.startsWith("//"))   // komentář
          continue;
        ch = Character.toLowerCase( line.charAt(0) );
        if (ch == 'n')  // začátek souřadnic neprůchodních oblastí
          getObstacles(line.substring(1), br);   // přeskoč 'n' na začátku
        else if (ch == 'b')  // začátek souřadnic překážek
          getBlocks(line, br);
        else if (ch == 'p')  // začátek údajů o předmětech
          getPickup(line);
        else
          System.out.println("Řádek nerozpoznán: " + line);
      }
      br.close();
    } 
    catch (IOException e) 
    { System.out.println("Chyba při čtení souboru: " + objsFNm);
      System.exit(1);
    }
  }  // konec metody: loadWorldObjects()


  private void getObstacles(String line, BufferedReader br)
  /* Načte několik řádků se souřadnicemi dokud řádek neobsahuje znak #:
          <x1>-<y1> (x2>-<y2> .....
           .... #
  */
  { boolean reachedEnd = getObstaclesLine(line);
    try {
      while (!reachedEnd) {
        line = br.readLine();
        if (line == null) { 
          System.out.println("Neočekávaný konec údajů o překážkách");
          System.exit(1);
        }
        reachedEnd = getObstaclesLine(line);
      }
    }
    catch (IOException e) 
    { System.out.println("Chyba při čtení údajů o překážkách");
      System.exit(1);
    }
  } // konec metody: getObstacles


  private boolean getObstaclesLine(String line)
  /*  Formát řádku:
          <x1>-<y1> (x2>-<y2> .....   [ # ]
      Každý vede k nastavení prvku obstacles[x][y] na true
  */
  { 
    StringTokenizer tokens = new StringTokenizer(line);
    String token;
    Point coord;
    while (tokens.hasMoreTokens()) {
      token = tokens.nextToken();
      if (token.equals("#"))   // konec souřadnic
        return true;
      coord = getCoord(token);
      obstacles[coord.x][coord.y] = true;    // nastav překážku
      // System.out.println("Přidána souřadnice (" + coord.x + "," + coord.y + ")"); 
    }
    return false;
  }  // konec metody: getObstaclesLine()


  private Point getCoord(String token)
  // Formát <x>-<y>; vrátí jej jako objekt typu Point
  {
    int x = 0;
    int y = 0;
    String[] results = token.split("-");
    if (results.length != 2)
      System.out.println("Nepsrávné souřadnice v " + token);
    else {
      try {
        x = Integer.parseInt(results[0]);
        y = Integer.parseInt(results[1]);
      }
      catch (NumberFormatException ex){ 
        System.out.println("Nepsrávný formát souřadnic v " + token); 
      }
    }

    if (x >= numXTiles) {
      System.out.println("souřadnice x je příliš velká v " + token); 
      x = numXTiles-1;
    }
    if (y >= numYTiles) {
      System.out.println("souřadnice y je příliš velká v " + token); 
      x = numYTiles-1;
    }

    return new Point(x,y);
  }  // konec metody: getCoord()



  private void getBlocks(String line, BufferedReader br)
  /*  b <blockName>
          <x1>-<y1> (x2>-<y2> .....
           .... #

      Každý <blockName> se uloží s unikátním jménem
      (<blockName> + blocksCounter)
      společně s obrázke uloženým v Images/<blockName>.gif
  */
  {
    boolean reachedEnd = false;
    StringTokenizer tokens = new StringTokenizer(line);
    tokens.nextToken();     // přeskoč 'b'
    String blockName = tokens.nextToken();   // název překážky
    BufferedImage blockIm = imsLoader.getImage(blockName);

    try {
      while (!reachedEnd) {
        line = br.readLine();
        if (line == null) { 
          System.out.println("Neočekávaný konec údajů o překážkách");
          System.exit(1);
        }
        reachedEnd = getBlocksLine(line, blockName, blockIm);
      }
    }
    catch (IOException e) 
    { System.out.println("Chyba při čtení údajů o překážkách");
      System.exit(1);
    }
  }  // konec metody: getBlocks()


  private boolean getBlocksLine(String line, String blockName, BufferedImage im)
  /*  Formát:
          <x1>-<y1> (x2>-<y2> .....   [ # ]
  
      Každá souřadnice je uložena jako nový záznam v objektu typu WorldItems a 
      obstacles[x][y] je nastaveno na true.
  */
  { StringTokenizer tokens = new StringTokenizer(line);
    String token;
    Point coord;
    while (tokens.hasMoreTokens()) {
      token = tokens.nextToken();
      if (token.equals("#"))   // konec souřadnic
        return true;
      coord = getCoord(token);

      // ulož do objektu typu WorldItems a přidej překážku
      wItems.addItem( blockName+blocksCounter, BLOCK, coord.x, coord.y, im);
      obstacles[coord.x][coord.y] = true;
      // System.out.println("Přidáno " + blockName + blocksCounter + 
      //                            " na (" + coord.x + "," + coord.y + ")");
      blocksCounter++;
    }
    return false;
  }  // konec metody: getBlocksLine()


  private void getPickup(String line)
  /*  Formát:
        p <pickupName> <x>-<y>

      Souřadnice je uložena jako nový záznam do objektu typu WorldItems.
  */
  {
    StringTokenizer tokens = new StringTokenizer(line);
    tokens.nextToken();     // přeskoč 'p'

    String pickupName = tokens.nextToken();   // název předmětu
    BufferedImage pickupIm = imsLoader.getImage(pickupName);

    Point coord = getCoord(tokens.nextToken());
    wItems.addItem( pickupName, PICKUP, coord.x, coord.y, pickupIm);
    // System.out.println("Přidáno " + pickupName + 
    //                           " na (" + coord.x + "," + coord.y + ")"); 
    numPickups++;
  }  // konec metody: getPickup()


  // ---------------------------- ostatní -----------------------


  private void initObstacles()
  // překázky ze začátku v herním světe nejsou
  {
    obstacles = new boolean[numXTiles][numYTiles];
    for(int i=0; i < numXTiles; i++)
      for(int j=0; j < numYTiles; j++)
        obstacles[i][j] = false;
  }  // konec metody: initObstacles()


  public boolean validTileLoc(int x, int y)
  // Je souřadnice dlaždice (x,y) na síti dlaždic a neobsahuje překážku?
  {
    if ((x < 0) || (x >= numXTiles) || (y < 0) || (y >= numYTiles))
      return false;
    if (obstacles[x][y])
      return false;
    return true;
  }  // konec metody: validTileLoc()


  public void draw(Graphics g)
  /* Vykreslí herní svět (s celým jeho obsahem) na obrazovku.
     Objekt typu WorldItems již obsahuje jak překážky tak i přemděty.

     Musíme ještě přidat sprity a pak zavolat metodu draw(). Poté je zase
     odebereme, protože se do dalšího překreslení pravděpodobně přesunou a 
     bude nutné je přidat v nové pozici.
  */
  { g.drawImage(floorIm, xOffset, yOffset, null);   // vykresli obrázek podlahy
    wItems.positionSprites(player, aliens);         // přidej sprity
    wItems.draw(g, xOffset, yOffset);               // vykresli věci ve hře
    wItems.removeSprites();                         // odeber sprity
  } // konec metody: draw()



  // ------------------ metody vztahující se k předmětům --------------------

  public String overPickup(Point pt)
  /* Je lokace dlaždice, pt, okupována předmětem?
     Metoda findPickupName() vrátí null, není-li v dané pozici předmět.
     Tuto metoda volá objekt typu PlayerSprite. */
  { return wItems.findmickupName(pt);   }


  public void removePickup(String name)
  /* Smaž předmět s názvem name z objektu typu WorldItems a sniž počet
     předmětů ve hře. Oznam objektu typu AlienTilePanel, že hra skončila,
     pokud počet předmětů klesne na 0. Tuto metodu volá objekt typu PlayerSprite. */
  { 
    if (wItems.removePickup(name)) {  // zkus jej odebrat
      numPickups--;
      if (numPickups == 0)   // hráč již všechno posbíral
        atPanel.gameOver();
    }
    else
      System.out.println("Nemohu smazat neznámý předmět: " + name);
  } // konec metody: removePickup()


  public String getPickupsStatus()
  // Objekt typu AlienTilesPanel používá tuto metodu pro aktualizaci statistik na obrazovce
  { return "" + numPickups + " pickups left";  }

  public boolean hasPickupsLeft()
  // tuto metodu volá objekt typu AlienQuadSprite
  {  return numPickups != 0;  }

  public Point nearestPickup(Point pt)
  /* Vrátí předmět nejblíže dlaždici pt.
     Tuto metodu volá objekt typu AlienQuadSprite. */
  { return wItems.nearestPickup(pt); }


  // ----------------- metody vztahující se ke spritu ----------------------


  public void addSprites(PlayerSprite ps, AlienSprite as[])
  // přidej hráče a vetřelce
  { player = ps;
    aliens = as;
  }

  public Point getPlayerLoc()
  /* Tuto metodu používají sprity vetřelce pro nalezení lokace hráče. 
     To znamená, že objekt typu WorldDisplay ovládá přístup k údajům o hráči. */
  {  return player.getTileLoc(); }


  public void hitByAlien()
  /* Vetřelec oznámí objektu typu WorldDisplay, že hráč byl zasažen.
     Předej tuto informaci hráči. */
  { player.hitByAlien(); }


  public void playerHasMoved(Point newPt, int moveQuad)
  /* Hráč volá tuto metodu pro oznámení objektu typu WorldDisplay, že 
     se přesunul. Oznamí to také vetřelcům a aktualizuje posun obrazovky
     herního světa. */
  {
    for(int i=0; i < aliens.length; i++)
      aliens[i].playerHasMoved(newPt);   // oznam vetřelcům

    updateOffsets(moveQuad);   // aktualizuj posun herního světa
  }  // konec metody: playerHasMoved()


  private void updateOffsets(int moveQuad)
  /* xOffset a yOffset jsou posuny v pixelech pro vykreslení levého 
     horního rohu obrázku podlahy (a jejího obsahu) vzhledem k
     levému hornímu rohu (0,0) panelu JPanel.

     Posun se mění v opačním směru, než v jakém se pohybuje hráč. To znamená,
     že když se hráč přesouvá, ve skutečnosti se pohybuje podlaha i s celým
     svým obsahem.

     Parametr moveQuad určuje směr, ve kterém se hráč právě 'přesunul'.
  */
  {
    if (moveQuad == TiledSprite.SW) {   // posun směrem na NE
      xOffset += tileWidth/2;
      yOffset -= tileHeight/2;
    }
    else if (moveQuad == TiledSprite.NW) {  // posun směrem na SE
      xOffset += tileWidth/2;
      yOffset += tileHeight/2;
    }
    else if (moveQuad == TiledSprite.NE) {  // posun směrem na SW
      xOffset -= tileWidth/2;
      yOffset += tileHeight/2;
    }
    else if (moveQuad == TiledSprite.SE) {  // posun směrem na NW
      xOffset -= tileWidth/2;
      yOffset -= tileHeight/2;
    }
    else if (moveQuad == TiledSprite.STILL) {  // nedělej nic to offsets
    }
    else
      System.out.println("Neplatná hodnota parametru moveQuad");
  }  // konec metody: updateOffsets()


} // konec třídy: WorldDisplay
