
// Penguin3D.java
// Andrew Davison, ad@coe.psu.ac.th, Feb 2023

/* A penguin is moved around a 3D scene filled with boxes.
   The scene illustrates:
     * building a circular floor using a mesh;
     * a skydome;
          -- the dome and floor radius are the same;
     * a OBJ model loaded in the g3db format;
     * collision detection/recovery with the boxes;
     * use of DirectionalShadowLight;
     * the penguin is controlled with the arrow keys;
     * the camera is controlled with the mouse by
       using the CameraInputController class;
     * HUD information 
       (e.g. the current (x,z) position of the penguin);
     * a 'thump' sound effect upon a collision;

     * The Sprite3D class is based on BaseActor3D from: 
         Java Game Development with LibGDX, 2nd ed.
         Lee Stemkoski, Apress, 2018
         Chapter 16

   Usage
   -----
   Test as a desktop app by calling:
    > gradlew desktop:run

   Can be compiled into an GDX 'applet' using:
    > gradlew html:superdev
      -- takes around 25 secs
      -- access in a browser at http://localhost:8080
    and
    > gradlew html:dist
      -- takes around 40 secs
      -- results are stored in test\html\build\dist
      -- then copied to a server
*/


package com.mygdx.game;

import java.util.ArrayList;

import com.badlogic.gdx.*;
import com.badlogic.gdx.graphics.*;
import com.badlogic.gdx.graphics.Texture.*;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;

import com.badlogic.gdx.graphics.g3d.*;
import com.badlogic.gdx.graphics.g3d.environment.*;
import com.badlogic.gdx.graphics.g3d.attributes.*;
import com.badlogic.gdx.graphics.g3d.loader.*;

import com.badlogic.gdx.scenes.scene2d.*;
import com.badlogic.gdx.scenes.scene2d.ui.*;

import com.badlogic.gdx.math.*;

import com.badlogic.gdx.utils.*;
import com.badlogic.gdx.graphics.g3d.utils.*;

import com.badlogic.gdx.audio.*;



public class Penguin3D extends ApplicationAdapter
{
  private static final float MIN_DELAY = 1/30f;

  private static final float WORLD_RADIUS = 30.0f;

  private static final float SPEED = 3.0f;   // of the penguin
  private static final float ROT_SPEED = 70.0f;

  private static final long SOUND_GAP = 2000L;  
       // a 2 secs interval between the playing of a sound

  private PerspectiveCamera camera;
  private Environment environment;
  private ModelBatch modelBatch;

  private DirectionalShadowLight shadowLight;
  private ModelBatch shadowBatch;

  private Skin skin;
  private Stage uiStage;
  private Label posLabel;  // display's the penguin's (x,z) position
  private CameraInputController camController;

  private Model floorModel;
  private ModelInstance floor;
  private ArrayList<Sprite3D> sprites;
  private ArrayList<Sprite3D> obstacles;
  private Sprite3D pingu;  // the penguin

  private Sound thump;
  private long startTime = 0L;  // for playing of a sound


  @Override
  public void create()
  {
    Gdx.input.setCatchKey(Input.Keys.UP, true);
    Gdx.input.setCatchKey(Input.Keys.DOWN, true);
    Gdx.input.setCatchKey(Input.Keys.LEFT, true);
    Gdx.input.setCatchKey(Input.Keys.RIGHT, true);
        // to stop arrow key scrolling on a webpage

    skin = new Skin(Gdx.files.internal("uiskin/uiskin.json"));

    float winWidth = (float) Gdx.graphics.getWidth();
    float winHeight = (float) Gdx.graphics.getHeight();

    // lighting
    environment = new Environment();
    environment.set( new ColorAttribute(ColorAttribute.AmbientLight, 
                             0.7f, 0.7f, 0.7f, 1f) );

    shadowLight = new DirectionalShadowLight(1024, 1024, 
                                        30f, 30f, 1f, 100f);
          /* int shadowMapWidth, int shadowMapHeight, 
             float shadowViewportWidth, float shadowViewportHeight, 
             float shadowNear, float shadowFar
          */
    // DirectionalLight dLight = new DirectionalLight();
    Color lightColor = new Color(0.9f, 0.9f, 0.9f, 1);
    Vector3 lightVector = new Vector3(-1.0f, -0.75f, -0.25f);
    shadowLight.set(lightColor, lightVector);

    environment.add(shadowLight);
    environment.shadowMap = shadowLight;

    // camera
    camera = new PerspectiveCamera(75, winWidth, winHeight);
    camera.position.set(1.5f, 2.2f, 8);
    camera.lookAt(0,0,0);
    camera.near = 0.1f;
    camera.far = WORLD_RADIUS*4;
    camera.update();
    camController = new CameraInputController(camera);
    Gdx.input.setInputProcessor(camController);

    shadowBatch = new ModelBatch(new DepthShaderProvider());

    // prepare 3D models
    modelBatch = new ModelBatch();
    sprites = new ArrayList<Sprite3D>();
    obstacles = new ArrayList<Sprite3D>();
    buildScene();

    // add GUI
    uiStage = new Stage();
    uiStage.addActor(buildGUI());

    thump = Gdx.audio.newSound( Gdx.files.internal("thump.wav"));
  }  // end of create()


  private void buildScene()
  /* add models for: the skydome, floor, penguin, four boxes
  */
  {
    Sprite3D skydome = makeDome("sky-sphere.jpg");
    sprites.add(skydome);

    makeFloor("checkerboard.png");

    pingu = loadPenguin(2, 0, 2);
    sprites.add(pingu);

    Sprite3D brownBox = makeBox(0, 0.5f, 0, Color.BROWN, "crate.jpg");
    sprites.add(brownBox);
    obstacles.add(brownBox);

    Sprite3D redBox = makeBox(5, 0.5f, 0, Color.RED, "crate.jpg");
    sprites.add(redBox);
    obstacles.add(redBox);

    Sprite3D greenBox = makeBox(-5, 0.5f, 0, Color.GREEN, "crate.jpg");
    sprites.add(greenBox);
    obstacles.add(greenBox);

    Sprite3D blueBox = makeBox(0,0.5f,5, Color.BLUE, "crate.jpg");
    sprites.add(blueBox);
    obstacles.add(blueBox);
  }  // end of buildScene()


  private Sprite3D makeBox(float x, float y, float z,
                          Color col, String texFnm)
  // a colored, textured box with collision detection enabled
  {
    ModelBuilder modelBuilder = new ModelBuilder();
    Material mat = new Material();
    int usage = Usage.Position + Usage.ColorPacked + Usage.Normal +
                Usage.TextureCoordinates;
    Model model = modelBuilder.createBox(1,1,1, mat, usage);
    Vector3 pos = new Vector3(0,0,0);
    ModelInstance mi = new ModelInstance(model, pos);

    Sprite3D sprite = new Sprite3D(model, mi, x, y, z);
    sprite.setColor(col);
    sprite.loadTexture(texFnm);

    sprite.setBaseRectangle();   // for collision detection

    return sprite;
  }  // end of makeBox()


  private void makeFloor(String texFnm)
  // a circular textured mesh
  {
    ModelBuilder builder = new ModelBuilder();
    Material mat = new Material(new IntAttribute(IntAttribute.CullFace));
         // no face culling, so the underside of the floor is solid
    builder.begin();
    MeshPartBuilder part = builder.part("floor", GL30.GL_TRIANGLES, 
         Usage.Position | Usage.TextureCoordinates | Usage.Normal, 
         mat);
    part.circle(WORLD_RADIUS, 40, 0, 0, 0,  0, 1, 0);
         // float radius, int divisions, float centerX, float centerY, 
         // float centerZ, float normalX, float normalY, float normalZ)
    floorModel = builder.end();

    Texture tex = new Texture(Gdx.files.internal(texFnm), true);
    tex.setFilter(TextureFilter.Linear, TextureFilter.Linear);
    floorModel.materials.get(0).set(
                    TextureAttribute.createDiffuse(tex));

    floor = new ModelInstance(floorModel);
  }  // end of makeFloor()


  private Sprite3D makeDome(String texFnm)
  /* a textured sphere, with the same radius as the floor,
     implemented as a Sprite3D although it doesn't move
  */
  {
    ModelBuilder builder = new ModelBuilder();
    Material mat = new Material();
    int usage = Usage.Position + Usage.ColorPacked + Usage.Normal +
                Usage.TextureCoordinates;

    Model model = builder.createSphere(1,1,1, 32,32, mat, usage);
    Vector3 pos = new Vector3(0,0,0);
    ModelInstance mi = new ModelInstance(model, pos);

    Sprite3D dome = new Sprite3D(model, mi, 0, 0, 0);
    dome.loadTexture(texFnm);
    float size = 2*WORLD_RADIUS;
    dome.setScale(size, size,-size);
      // when scaling, the negative z-value inverts the sphere,
      // so that the texture is rendered on the inside

    return dome;
  }  // end of makeBox()


  private Sprite3D loadPenguin(float x, float y, float z)
  /* The penguin model is stored in g3db and OBJ formats,
     but the OBJ's texture is not visible in the HTML version.
     Collision detection is enabled, and will be tested
     against the boxes. 
  */
  {
/*  ObjLoader loader = new ObjLoader();
    Model model = loader.loadModel(
           Gdx.files.internal("penguin/penguin.obj"), true);
*/
    G3dModelLoader loader = new G3dModelLoader(new UBJsonReader());
    Model model = loader.loadModel(
           Gdx.files.internal("penguin/penguin.g3db"));

    ModelInstance mi = new ModelInstance(model);

    Sprite3D sprite = new Sprite3D(model, mi, x, y, z);
    sprite.setScale(2, 2, 2);
    // sprite.moveBy(0, -0.5f, 0);
    sprite.setBasePolygon();  // for collision detection

    return sprite;
  }  // end of loadPenguin()


  private Table buildGUI()
  /* the table holds three labels: the title, the changing
     penguin (x,z) position, and help
  */
  {
    Table t = new Table();
    t.setFillParent(true);
    t.align(Align.top | Align.left);
    t.pad(20);

    Label title = new Label("Penguin3D", skin);
    title.setColor(Color.CYAN);
    title.setFontScale(2);
    t.add(title).expandX().left().top();

    posLabel = new Label("Pos:", skin);
    posLabel.setFontScale(1.5f);
    t.add(posLabel).width(200).right().bottom();

    t.row();

    Label help = new Label("Press the arrow keys to move the penguin.\n" +
                           "Use the mouse to move the camera.", skin);
    t.add(help).width(150).left();

    return t;
  }  // end of buildGUI()



  @Override
  public void render()
  {
    int winWidth = Gdx.graphics.getWidth();
    int winHeight = Gdx.graphics.getHeight();

    float delta = Gdx.graphics.getDeltaTime();
    float dt = Math.min(delta, MIN_DELAY);

    // move the penguin according to the arrow keys
    if (Gdx.input.isKeyPressed(Input.Keys.UP))
      pingu.moveForward( -SPEED * dt);

    if (Gdx.input.isKeyPressed(Input.Keys.DOWN))
      pingu.moveForward(SPEED * dt);

    if (Gdx.input.isKeyPressed(Input.Keys.LEFT))
      pingu.turnBy(ROT_SPEED * dt);

    if (Gdx.input.isKeyPressed(Input.Keys.RIGHT))
      pingu.turnBy( -ROT_SPEED * dt);

    // display the penguin's position
    Vector3 pos = pingu.getPosition();
    posLabel.setText("Pos: (" + roundToN(pos.x, 2) + ", " + 
                                roundToN(pos.z, 2) + ")");

    // update the scene
    camController.update();
    uiStage.act(dt);
    for (Sprite3D sprite : sprites)
      sprite.act(dt);

    // check for a collision between the penguin and any boxes
    for (Sprite3D ob : obstacles) {
      if (pingu.overlaps(ob)) {
        long currTime = TimeUtils.millis();
        if ((currTime - startTime) > SOUND_GAP) {
          // there is a SOUND_GAP interval between sound playing
          thump.play(1f);
          startTime = currTime;
        }
      }
      pingu.preventOverlap(ob);  // undo the collision move 
    }

    // draw the scene
    Gdx.gl.glViewport(0, 0, winWidth, winHeight);
    Gdx.gl.glClearColor(0.5f, 0.5f, 0.5f, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT + GL20.GL_DEPTH_BUFFER_BIT);

    // add shadows to the penguin and obstacles
    shadowLight.begin(Vector3.Zero, camera.direction);
    shadowBatch.begin(shadowLight.getCamera());
    pingu.draw(shadowBatch, environment);
    for (Sprite3D s : obstacles)
      s.draw(shadowBatch, environment);
    shadowBatch.end();
    shadowLight.end();

    // draw the floor and the sprites
    modelBatch.begin(camera);
    modelBatch.render(floor, environment);
    for (Sprite3D s : sprites)
      s.draw(modelBatch, environment);
    modelBatch.end();

    uiStage.draw();
  }   // end of render()


  @Override
  public void resize(int w, int h)
  // not used since the app is a fixed size
  {  uiStage.getViewport().update(w, h, true); }


  @Override
  public void dispose()
  {
    modelBatch.dispose();
    floorModel.dispose();
    for (Sprite3D s : sprites)
      s.dispose();
  }


  private float roundToN(float value, int nPlaces) 
  // round value to the specified number of decimal places
  {
    if (nPlaces < 0)
      return value;

    float scale = 1;
    for (int i=0; i < nPlaces; i++)
       scale = 10*scale;
    return MathUtils.roundPositive(value * scale)/scale;
  } // end of roundToN()


}  // end of Penguin3D class
