package net.kennux.cubicworld.entity;
import net.kennux.cubicworld.CubicWorld;
import net.kennux.cubicworld.inventory.PlayerInventory;
import net.kennux.cubicworld.networking.packet.inventory.ClientDropItem;
import net.kennux.cubicworld.networking.packet.inventory.ClientItemMove;
import net.kennux.cubicworld.util.Mathf;
import net.kennux.cubicworld.util.Time;
import net.kennux.cubicworld.voxel.VoxelCollision;
import net.kennux.cubicworld.voxel.VoxelWorld;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.decals.DecalBatch;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.collision.BoundingBox;
/**
* <pre>
* The player controller for simulating physics of a player.
* This class is most likely a slightly modified version of ACharacterEntity.
* It uses the same functions for collision detection, it is only more specialized for player controlling.
* </pre>
*
* @author KennuX
*
*/
public class PlayerController extends AEntity
{
/**
* The player's camera.
*/
private PerspectiveCamera camera;
/**
* The cubic world instance.
*/
public VoxelWorld voxelWorld;
/**
* The currently playing footstep loop.
* -1 if not playing.
*/
private long footstepSoundInstance = -1;
/**
* The currently playing footstep sound.
*/
private Sound currentFootstepSound;
/**
* The player inventory instance.
* It will get set in the ServerPlayerSpawn packet interpretation on the client side.
*/
private PlayerInventory playerInventory;
/**
* The player's velocity.
*/
private Vector3 velocity;
public PlayerController(VoxelWorld voxelWorld, PerspectiveCamera camera)
{
this.voxelWorld = voxelWorld;
this.velocity = new Vector3(Vector3.Zero);
this.camera = camera;
this.camera.update(true);
this.playerInventory = new PlayerInventory();
}
/**
* Drops an item from the stack located in the given slot from the player inventory.
*
* @param itemSlot
*/
public void dropItem(int itemSlot)
{
// Send item drop packet
ClientDropItem dropItemPacket = new ClientDropItem();
dropItemPacket.itemSlotId = itemSlot;
CubicWorld.getClient().client.sendPacket(dropItemPacket);
}
/**
* Returns the bounding box for the current entity's position.
*
* @return
*/
public BoundingBox getBoundingBox()
{
return this.getBoundingBox(this.getPosition());
}
/**
* <pre>
* Returns the bounding box of this character entity at a given position
* (bottom center).
* It will calculate the minimum and maximum vector in this way:
*
* min = pos + Vector3(characterWidth/2, 0, characterDepth / 2)
* max = pos + Vector3(characterWidth/2, characterHeight, characterDepth / 2)
* </pre>
*
* @return
*/
public BoundingBox getBoundingBox(Vector3 position)
{
// Construct bounding box position is the center of the collider, this constructs the minimum position vector and the maximum position vector
Vector3 minimumPos = new Vector3(position.x - (this.getCharacterWidth() / 2.0f), position.y, position.z - (this.getCharacterDepth() / 2.0f));
Vector3 maximumPos = new Vector3(position.x + (this.getCharacterWidth() / 2.0f), position.y + (this.getCharacterHeight()), position.z + (this.getCharacterDepth() / 2.0f));
// Instantiate bounding box
return new BoundingBox(minimumPos, maximumPos);
}
/**
* The depth of the player in voxels.
*/
protected int getCharacterDepth()
{
return 1;
}
/**
* The height of the player in voxels.
*/
protected float getCharacterHeight()
{
return 1.75f;
}
/**
* The width of the player in voxels.
*/
protected int getCharacterWidth()
{
return 1;
}
/**
* The maximum speed per axis.
* Standard is 3 which means speed can be theoretically between (-3,-3,-3) and (3,3,3).
*
* @return
*/
protected float getMaximumSpeed()
{
return 3f;
}
/**
* <pre>
* Returns the player inventory instance.
* DON'T do any manipulation of the inventory data local!
*
* If you want to manipulate player inventory data use functions of the playercontroller!
* This is needed to keep the inventory in sync with the server.
* </pre>
*
* @return the playerInventory
*/
public PlayerInventory getPlayerInventory()
{
return this.playerInventory;
}
/**
* Adds a force impulse towards the given direction.
*
* @param direction
*/
public void impulse(Vector3 direction, float strength)
{
Vector3 dirS = new Vector3(direction.nor());
dirS.x *= strength;
dirS.y *= strength;
dirS.z *= strength;
this.velocity.add(dirS);
}
/**
* Does nothing!
*/
@Override
public void init()
{
}
/**
* Performs a grounding check.
*
* @return
*/
public boolean isGrounded()
{
// Perform collision check for y - 0.15
Vector3 nPos = new Vector3(this.getPosition());
nPos.y -= 0.15f;
return this.voxelWorld.intersects(this.getBoundingBox(nPos));
}
/**
* Moves to the given direction.
* normalizes and multiplicates the direction with the delta time
*
* @param direction
*/
public void move(Vector3 direction)
{
direction.y = 0;
Vector3 dirD = new Vector3(direction);
dirD.x *= Time.getDeltaTime();
dirD.z *= Time.getDeltaTime();
this.velocity.add(dirD);
}
/**
* Moves an item in the player inventory from sourceSlot to targetSlot.
*
* @param sourceSlot
* @param targetSlot
*/
public void moveItem(int sourceSlotId, int targetSlotId)
{
// Send item move packet
CubicWorld.getClient().client.sendPacket(ClientItemMove.createPlayerInventoryMove(sourceSlotId, targetSlotId));
}
/**
* TODO Weapon / item view rendering.
*/
@Override
public void render(Camera camera, ModelBatch modelBatch, DecalBatch decalBatch, SpriteBatch spriteBatch, BitmapFont bitmapFont)
{
// This cannot get rendered!
}
/**
* <pre>
* Detects collisions and handles physic movement simulation.
* It does the following things:
*
* - Check if the player is grounded (if no, apply gravitation)
* - Calculate the new position by adding velocity to the current position
* - Check if there are voxels at the new position which could intersect with the player bounding box
* -> if yes, reset position and velocity
* - Calculate deceleration
* - Update position
* - Update camera
* </pre>
*
*/
@Override
public void update()
{
boolean grounded = this.isGrounded();
// Physics
if (!grounded)
{
this.velocity.y += Time.getDeltaTime() * -9.81f;
}
// Max check
if (this.velocity.x > 0)
this.velocity.x = Mathf.min(this.velocity.x, this.getMaximumSpeed());
else if (this.velocity.x < 0)
this.velocity.x = Mathf.max(this.velocity.x, -this.getMaximumSpeed());
if (this.velocity.y > 0)
this.velocity.y = Mathf.min(this.velocity.y, this.getMaximumSpeed());
else if (this.velocity.y < 0)
this.velocity.y = Mathf.max(this.velocity.y, -this.getMaximumSpeed());
if (this.velocity.z > 0)
this.velocity.z = Mathf.min(this.velocity.z, this.getMaximumSpeed());
else if (this.velocity.z < 0)
this.velocity.z = Mathf.max(this.velocity.z, -this.getMaximumSpeed());
boolean moving = new Vector3(this.velocity).add(0, -this.velocity.y, 0).len() > 0.1f;
if (grounded && moving)
{
// Grounded, footstep sound handling
// Get voxel beyond player's position
Vector3 voxelPos = this.voxelWorld.getVoxelspacePosition(this.getPosition());
voxelPos.y -= 1;
Sound footstepSound = null;
if (this.voxelWorld.hasVoxel((int) voxelPos.x, (int) voxelPos.y, (int) voxelPos.z))
{
footstepSound = this.voxelWorld.getVoxel((int) voxelPos.x, (int) voxelPos.y, (int) voxelPos.z).voxelType.getFootstepSound();
}
if (footstepSound != null)
{
if (this.footstepSoundInstance == -1 && this.currentFootstepSound == null)
{
this.footstepSoundInstance = footstepSound.loop(1.0f);
this.currentFootstepSound = footstepSound;
}
// Stop playing and play other sound
else if (this.footstepSoundInstance != -1 && this.currentFootstepSound != null && this.currentFootstepSound != footstepSound)
{
this.currentFootstepSound.stop(this.footstepSoundInstance);
this.footstepSoundInstance = footstepSound.loop(1.0f);
this.currentFootstepSound = footstepSound;
}
}
}
if ((!grounded || !moving) && this.footstepSoundInstance != -1)
{
this.currentFootstepSound.stop(this.footstepSoundInstance);
this.currentFootstepSound = null;
this.footstepSoundInstance = -1;
}
// Move
Vector3 newPos = new Vector3(this.getPosition()).add(this.velocity);
// Collision check for the new player position
VoxelCollision collision = this.voxelWorld.collisionCheck(this.getBoundingBox(newPos));
// Check for collision
if (collision != null)
{
// Intersection! reset collided axis!
if (collision.collisionX)
this.velocity.x = 0;
else if (collision.collisionY)
this.velocity.y = 0;
else if (collision.collisionZ)
this.velocity.z = 0;
newPos = this.getPosition();
}
// Deceleration
// TODO May reimplement?
if (this.velocity.x > 0)
{
this.velocity.x -= 20.0f * Time.getDeltaTime();
if (this.velocity.x < 0)
this.velocity.x = 0;
}
else if (this.velocity.x < 0)
{
this.velocity.x += 20.0f * Time.getDeltaTime();
if (this.velocity.x > 0)
this.velocity.x = 0;
}
if (this.velocity.y > 0)
{
this.velocity.y -= 9.0f * Time.getDeltaTime();
if (this.velocity.y < 0)
this.velocity.y = 0;
}
else if (this.velocity.y < 0)
{
this.velocity.y += 9.0f * Time.getDeltaTime();
if (this.velocity.y > 0)
this.velocity.y = 0;
}
if (this.velocity.z > 0)
{
this.velocity.z -= 20.0f * Time.getDeltaTime();
if (this.velocity.z < 0)
this.velocity.z = 0;
}
else if (this.velocity.z < 0)
{
this.velocity.z += 20.0f * Time.getDeltaTime();
if (this.velocity.z > 0)
this.velocity.z = 0;
}
this.setPosition(newPos);
// Calculate camera position
this.interpolatePosition(true);
Vector3 camPos = new Vector3(this.getPosition());
camPos.y += this.getCharacterHeight();
this.camera.position.set(camPos);
this.camera.update(true);
}
}