/*
* Aphelion
* Copyright (c) 2013 Joris van der Wel
*
* This file is part of Aphelion
*
* Aphelion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* Aphelion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Aphelion. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, the following supplemental terms apply, based on section 7 of
* the GNU Affero General Public License (version 3):
* a) Preservation of all legal notices and author attributions
* b) Prohibition of misrepresentation of the origin of this material, and
* modified versions are required to be marked in reasonable ways as
* different from the original version (for example by appending a copyright notice).
*
* Linking this library statically or dynamically with other modules is making a
* combined work based on this library. Thus, the terms and conditions of the
* GNU Affero General Public License cover the whole combination.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent modules,
* and to copy and distribute the resulting executable under terms of your
* choice, provided that you also meet, for each linked independent module,
* the terms and conditions of the license of that module. An independent
* module is a module which is not derived from or based on this library.
*/
package aphelion.client.graphics.screen;
import aphelion.client.graphics.Graph;
import aphelion.client.graphics.world.MapEntity;
import aphelion.shared.map.MapClassic;
import aphelion.shared.map.tile.TileType;
import aphelion.shared.resource.ResourceDB;
import aphelion.shared.swissarmyknife.Point;
import aphelion.shared.swissarmyknife.SwissArmyKnife;
import de.lessvoid.nifty.Nifty;
import de.lessvoid.nifty.spi.render.RenderFont;
import de.lessvoid.nifty.tools.Color;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.lwjgl.opengl.Display;
import org.newdawn.slick.Image;
import org.newdawn.slick.opengl.TextureImpl;
/**
*
* @author Joris
*/
public final class Camera
{
public final ResourceDB resourceDB;
Nifty nifty;
RenderFont playerFont;
// All dimension values are without zoom
public final Point dimension = new Point();
public int dimensionX; // integer version of the above
public int dimensionY;
public final Point dimensionHalf = new Point();
public final Point dimensionHalfTiles = new Point();
// Positions on the map:
public final Point pos = new Point(0,0); // center of the camera
public int posX;
public int posY;
public final Point leftTop = new Point(); // including zoom
public final Point rightBottom = new Point(); // including zoom
public final Point tilePos = new Point(0,0);
// Positions on the screen:
public final Point screenPos = new Point(0,0);
public int screenPosX;
public int screenPosY;
/**
* If the zoom is lower than this value, the camera is rendered as if it was a radar.
* This means one bitmap image for the entire map is used.
* And ships should be rendered without images
*/
public static final float RADAR_RENDERING = 1 / 8f;
public float zoom = 1; // higher is zooming in.
public float zoom_inverse = 1; // 1/zoom
public boolean radarRendering;
public Camera(@Nonnull ResourceDB resourceDB)
{
this.resourceDB = resourceDB;
setDimension(Display.getWidth(), Display.getHeight());
}
private void updateCornerValues()
{
this.leftTop.set(
pos.x - (this.dimensionHalf.x * zoom_inverse),
pos.y - (this.dimensionHalf.y * zoom_inverse));
this.rightBottom.set(
pos.x + (this.dimensionHalf.x * zoom_inverse),
pos.y + (this.dimensionHalf.y * zoom_inverse));
}
public void setPosition(float x, float y)
{
if (pos.x == x && pos.y == y)
{
return;
}
pos.set(x, y);
tilePos.set(pos);
tilePos.divide(16);
tilePos.floor();
posX = (int) pos.x;
posY = (int) pos.y;
updateCornerValues();
}
public void setPosition(@Nonnull Point pos)
{
setPosition(pos.x, pos.y);
}
public void clipPosition(float xMin, float yMin, float xMax, float yMax)
{
setPosition(
SwissArmyKnife.clip(
pos.x,
SwissArmyKnife.floor(xMin + dimensionHalf.x * zoom_inverse),
SwissArmyKnife.ceil(xMax - dimensionHalf.x * zoom_inverse)),
SwissArmyKnife.clip(
pos.y,
SwissArmyKnife.floor(yMin + dimensionHalf.y * zoom_inverse),
SwissArmyKnife.ceil(yMax - dimensionHalf.y * zoom_inverse))
);
}
public void setScreenPosition(float x, float y)
{
screenPos.set(x, y);
screenPosX = (int) screenPos.x;
screenPosY = (int) screenPos.y;
}
public void setScreenPosition(@Nonnull Point screenPos)
{
setScreenPosition(screenPos.x, screenPos.y);
}
public void setDimension(float width, float height)
{
if (dimension.x == width && dimension.y == height)
{
return;
}
width = SwissArmyKnife.ceil(width);
height = SwissArmyKnife.ceil(height);
// make sure we can draw pricesely at the center without point values
if (width % 2 == 1)
{
++width;
}
if (height % 2 == 1)
{
++height;
}
dimension.set(width, height);
dimensionX = (int) dimension.x;
dimensionY = (int) dimension.y;
dimensionHalf.set(dimension);
dimensionHalf.divide(2);
dimensionHalfTiles.set(dimensionHalf);
dimensionHalfTiles.divide(16);
updateCornerValues();
}
public void setDimension(@Nonnull Point dimension)
{
setDimension(dimension.x, dimension.y);
}
public void setZoom(float zoom)
{
assert zoom > 0;
this.zoom = zoom;
this.zoom_inverse = 1 / zoom;
updateCornerValues();
}
public void mapToScreenPosition(@Nonnull Point objectPos, @Nonnull Point result)
{
mapToScreenPosition(objectPos.x, objectPos.y, result);
}
public void mapToScreenPosition(float x, float y, @Nonnull Point result)
{
result.x = -(this.pos.x - x);
result.y = -(this.pos.y - y);
result.multiply(zoom);
result.add(this.dimensionHalf);
result.add(this.screenPos);
// avoid drawing at point values.
// this messes with textures especially those with alpha channels!
result.round();
}
public boolean isOnScreen(@Nonnull Point objectPos)
{
return isOnScreen(objectPos.x, objectPos.y);
}
public boolean isOnScreen(float x, float y)
{
if (x < this.leftTop.x || y < this.leftTop.y)
{
return false;
}
if (x > this.rightBottom.x || y > this.rightBottom.y)
{
return false;
}
return true;
}
public boolean isOnScreen(float xLow, float yLow, float xHigh, float yHigh)
{
if (xHigh < this.leftTop.x || yHigh < this.leftTop.y)
{
return false;
}
if (xLow > this.rightBottom.x || yLow > this.rightBottom.y)
{
return false;
}
return true;
}
public boolean isOnScreen(Point low, Point high)
{
return isOnScreen(low.x, low.y, high.x, high.y);
}
public void setGraphicsClip()
{
Graph.g.setClip((int)screenPos.x, (int)screenPos.y, (int)dimension.x, (int)dimension.y);
}
public void renderTiles(@Nonnull MapClassic map, @Nonnull TileType.TILE_LAYER layer)
{
int xStart, xEnd, yStart, yEnd;
int x, y;
TileType tile;
this.radarRendering = zoom <= RADAR_RENDERING;
if (radarRendering)
{
if (layer != TileType.TILE_LAYER.PLAIN)
{
return;
}
setGraphicsClip();
float imgX = leftTop.x / 16;
float imgY = leftTop.y / 16;
float imgX2 = rightBottom.x / 16;
float imgY2 = rightBottom.y / 16;
Image img = map.getRadarImage();
if (img != null)
{
img.draw(
screenPos.x, screenPos.y,
screenPos.x + dimension.x, screenPos.y + dimension.y,
imgX,
imgY,
imgX2,
imgY2
);
}
Graph.g.clearClip();
return;
}
setGraphicsClip();
Point tileRange = new Point(dimensionHalfTiles);
tileRange.multiply(zoom_inverse);
tileRange.add(zoom_inverse);
xStart = (int) SwissArmyKnife.floor(tilePos.x - tileRange.x);
xEnd = (int) SwissArmyKnife.ceil(tilePos.x + tileRange.x);
yStart = (int) SwissArmyKnife.floor(tilePos.y - tileRange.y);
yEnd = (int) SwissArmyKnife.ceil(tilePos.y + tileRange.y);
TileType mapEdge = map.getMapEdge();
for (x = xStart; x <= xEnd; ++x)
{
for (y = yStart; y <= yEnd; ++y)
{
if (x == -1 || y == -1 || x == 1024 || y == 1024)
{
if (layer == TileType.TILE_LAYER.PLAIN)
{
if (x >= -1 && y >= -1 && x <= 1024 && y <= 1024)
{
mapEdge.render(this, x, y, map);
}
}
}
else
{
tile = map.getTile(x, y);
if (tile != null && tile.layer == layer)
{
tile.render(this, x, y, map);
}
}
}
}
Graph.g.clearClip();
}
public <T extends MapEntity> void renderEntities(@Nonnull Iterable<T> entities)
{
this.radarRendering = zoom <= RADAR_RENDERING;
setGraphicsClip();
boolean needMore = true;
int iteration = 0;
while(needMore)
{
needMore = false;
for (T en : entities)
{
if (en.isWithinCameraRange(leftTop, rightBottom))
{
boolean r = en.render(this, iteration);
needMore = needMore || r;
}
else
{
en.noRender();
}
}
++iteration;
}
Graph.g.clearClip();
}
public void renderEntity(@Nullable MapEntity en)
{
if (en == null)
{
return;
}
boolean needMore = true;
int iteration = 0;
while(needMore)
{
needMore = false;
setGraphicsClip();
if (en.isWithinCameraRange(leftTop, rightBottom))
{
boolean r = en.render(this, iteration);
needMore = needMore || r;
}
else
{
en.noRender();
}
Graph.g.clearClip();
}
}
public int getFrequency()
{
return 0; // todo teams
}
public void renderPlayerText(@Nonnull String text, int x, int y, @Nonnull Color color)
{
if (playerFont == null || nifty == null)
{
// Without nifty, use slick with its default font
Graph.g.setColor(new org.newdawn.slick.Color(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
Graph.g.drawString(text, x, y);
}
else
{
TextureImpl.unbind();
nifty.getRenderEngine().setColor(color);
nifty.getRenderEngine().setFont(playerFont);
nifty.getRenderEngine().renderText(text, x, y, -1, -1, Color.NONE);
nifty.getRenderEngine().setColor(Color.WHITE);
Graph.g.setColor(org.newdawn.slick.Color.white);
TextureImpl.unbind();
}
}
public void renderPlayerText(@Nonnull String text, float x, float y, @Nonnull Color color)
{
renderPlayerText(text, (int) x, (int) y, color);
}
}