/*
* 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.world;
import aphelion.client.graphics.screen.Camera;
import aphelion.client.RENDER_LAYER;
import aphelion.client.graphics.RenderDelay;
import aphelion.client.graphics.world.event.ActorDiedTracker;
import aphelion.client.graphics.world.event.EventTracker;
import aphelion.client.graphics.world.event.ProjectileExplosionTracker;
import aphelion.client.net.SingleGameConnection;
import aphelion.shared.event.LoopEvent;
import aphelion.shared.event.TickEvent;
import aphelion.shared.net.game.ActorListener;
import aphelion.shared.net.game.NetworkedActor;
import aphelion.shared.physics.Collision;
import aphelion.shared.physics.entities.ProjectilePublic;
import aphelion.shared.physics.PhysicsEnvironment;
import aphelion.shared.physics.entities.ActorPublic;
import aphelion.shared.physics.events.Event;
import aphelion.shared.physics.events.pub.ActorDiedPublic;
import aphelion.shared.physics.events.pub.EventPublic;
import aphelion.shared.physics.events.pub.ProjectileExplosionPublic;
import aphelion.shared.physics.valueobjects.PhysicsPositionVector;
import aphelion.shared.physics.valueobjects.PhysicsShipPosition;
import aphelion.shared.resource.ResourceDB;
import aphelion.shared.swissarmyknife.AttachmentConsumer;
import aphelion.shared.swissarmyknife.EmptyIterator;
import aphelion.shared.swissarmyknife.FilteredIterator;
import aphelion.shared.swissarmyknife.LinkedListHead;
import aphelion.shared.swissarmyknife.Point;
import java.util.HashMap;
import java.util.Iterator;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Takes care of tracking graphics state for entities (ships, projectiles).
* @author Joris
*/
public class MapEntities implements TickEvent, LoopEvent, Animator, ActorListener
{
private static final AttachmentConsumer<ProjectilePublic, Projectile> projectileAttachment
= new AttachmentConsumer<>(aphelion.shared.physics.entities.Projectile.attachmentManager);
/** pid -> ActorShip */
private final HashMap<Integer,ActorShip> actorShips = new HashMap<>(64);
private PhysicsEnvironment physicsEnv;
private final ResourceDB resourceDB;
private @Nullable ActorShip localShip;
private final LinkedListHead<MapAnimation> animations[] = new LinkedListHead[RENDER_LAYER.values().length];
private RenderDelay renderDelay;
private static final AttachmentConsumer<EventPublic, EventTracker> eventTrackers
= new AttachmentConsumer<>(Event.attachmentManager);
private SingleGameConnection connection;
public final Collision collision = new Collision(); // used for animations
public MapEntities(@Nonnull ResourceDB db)
{
this.resourceDB = db;
for (int i = 0; i < animations.length; i++)
{
animations[i] = new LinkedListHead<>();
}
}
public void addShip(@Nonnull ActorShip en)
{
actorShips.put(en.pid, en);
if (en.isLocalPlayer())
{
assert localShip == null; // since the getter is singular
localShip = en;
}
}
public void removeShip(@Nullable ActorShip en)
{
if (en == null) { return; }
actorShips.remove(en.pid);
if (localShip == en)
{
localShip = null;
}
}
public @Nullable ActorShip getLocalShip()
{
return localShip; // may be null
}
public @Nonnull Iterator<ActorShip> shipIterator()
{
return actorShips.values().iterator();
}
public @Nonnull Iterable<ActorShip> ships()
{
return new Iterable<ActorShip>()
{
@Override
public Iterator<ActorShip> iterator()
{
return shipIterator();
}
};
}
public @Nonnull Iterator<ActorShip> shipNoLocalIterator()
{
Iterator<ActorShip> it = new Iterator<ActorShip>()
{
Iterator<ActorShip> wrapped;
ActorShip next;
{
wrapped = actorShips.values().iterator();
advanceUntilCorrect();
}
@Override
public boolean hasNext()
{
return next != null;
}
@Override
public ActorShip next()
{
ActorShip ret = next;
advanceUntilCorrect();
return ret;
}
@Override
public void remove()
{
wrapped.remove();
}
private void advanceUntilCorrect()
{
while (wrapped.hasNext())
{
next = wrapped.next();
if (!next.isLocalPlayer())
{
return;
}
}
next = null;
}
};
return it;
}
public @Nonnull Iterable<ActorShip> shipsNoLocal()
{
return new Iterable<ActorShip>()
{
@Override
public Iterator<ActorShip> iterator()
{
return shipNoLocalIterator();
}
};
}
public @Nullable ActorShip getActorShip(int pid)
{
return actorShips.get(pid);
}
public @Nonnull Iterator<Projectile> projectileIterator(final boolean includeNonExist)
{
if (physicsEnv == null)
{
return new EmptyIterator<>();
}
Iterator<Projectile> it = new FilteredIterator<Projectile, ProjectilePublic>(physicsEnv.projectileIterator())
{
@Override
public Projectile filter(ProjectilePublic next)
{
Projectile projectile = physicsProjectileToGraphics(next);
if (!includeNonExist && !projectile.isExists())
{
return null;
}
return projectile;
}
};
return it;
}
public @Nonnull Iterable<Projectile> projectiles(final boolean includeNonExist)
{
return new Iterable<Projectile>()
{
@Override
public Iterator<Projectile> iterator()
{
return projectileIterator(includeNonExist);
}
};
}
public @Nonnull Projectile physicsProjectileToGraphics(@Nonnull ProjectilePublic proj)
{
if (proj == null) { throw new NullPointerException(); }
Projectile projectile = projectileAttachment.get(proj);
if (projectile == null)
{
projectile = new Projectile(resourceDB, proj);
projectileAttachment.set(proj, projectile);
// caveat: if a timewarp destroys and recreates a projectile,
// this data is lost
}
return projectile;
}
public @Nullable ActorShip findNearestActor(Point pos, boolean includeLocal)
{
Iterator<ActorShip> it = actorShips.values().iterator();
Point diff = new Point();
ActorShip nearest = null;
float nearest_dist = 0;
while (it.hasNext())
{
ActorShip ship = it.next();
if (ship.isLocalPlayer() && !includeLocal)
{
continue;
}
if (!ship.isExists())
{
continue;
}
diff.set(ship.pos);
diff.sub(pos);
float dist = diff.lengthSquared();
if (nearest == null || dist < nearest_dist)
{
nearest_dist = dist;
nearest = ship;
}
}
return nearest;
}
@Override
public void addAnimation(@Nonnull RENDER_LAYER layer, @Nonnull MapAnimation animation, @Nullable Camera camera)
{
animation.animating = true;
animations[layer.id].append(animation.link);
animation.camera = camera;
}
public @Nonnull Iterator<MapAnimation> animationIterator(@Nonnull final RENDER_LAYER layer, @Nullable final Camera camera)
{
return new FilteredIterator<MapAnimation, MapAnimation>(animations[layer.id].iterator())
{
@Override
public MapAnimation filter(MapAnimation next)
{
if (next.camera == null || next.camera == camera)
{
return next;
}
return null;
}
};
}
public @Nonnull Iterable<MapAnimation> animations(@Nonnull final RENDER_LAYER layer, @Nullable final Camera camera)
{
return new Iterable<MapAnimation>()
{
@Override
public Iterator<MapAnimation> iterator()
{
return animationIterator(layer, camera);
}
};
}
@Override
public void tick(long tick)
{
if (renderDelay != null)
{
renderDelay.tick(tick);
}
Iterator<ActorShip> itActor = actorShips.values().iterator();
while (itActor.hasNext())
{
itActor.next().tick(tick);
}
Iterator<Projectile> itProj = projectileIterator(true);
while (itProj.hasNext())
{
itProj.next().tick(tick);
}
for (LinkedListHead<MapAnimation> animationList : animations)
{
for (MapAnimation anim : animationList)
{
anim.tick(tick);
}
}
}
@Override
public void loop(long systemNanoTime, long sourceNanoTime)
{
for (int i = 0; i < animations.length; ++i)
{
Iterator<MapAnimation> animIt = animations[i].iterator();
while (animIt.hasNext())
{
MapAnimation animation = animIt.next();
if (animation.isDone())
{
animation.animating = false;
animIt.remove();
}
}
}
}
@Override
public void newActor(@Nonnull NetworkedActor actor)
{
this.addShip(new ActorShip(this.resourceDB, actor, physicsEnv.getActor(actor.pid, true), this));
}
@Override
public void actorModified(@Nonnull NetworkedActor actor)
{
}
@Override
public void removedActor(@Nonnull NetworkedActor actor)
{
ActorShip ship = this.getActorShip(actor.pid);
assert ship != null;
this.removeShip(ship);
}
public void tryInitialize(@Nullable PhysicsEnvironment physicsEnv_, @Nullable SingleGameConnection connection_)
{
if (physicsEnv_ != null)
{
this.physicsEnv = physicsEnv_;
}
if (connection_ != null)
{
this.connection = connection_;
}
if (renderDelay == null)
{
if (physicsEnv != null)
{
renderDelay = new RenderDelay(physicsEnv, this);
}
}
if (renderDelay != null)
{
if (connection != null && !renderDelay.isSubscribed())
{
renderDelay.subscribeListeners(connection);
}
if (!renderDelay.isInitialized() && localShip != null && localShip.getActor() != null)
{
renderDelay.init(localShip.getActor());
}
}
}
public @Nullable RenderDelay getRenderDelay()
{
return renderDelay;
}
public void updateGraphicsFromPhysics()
{
if (this.physicsEnv == null)
{
throw new IllegalStateException();
}
Iterator<ActorShip> shipIt = this.shipIterator();
while (shipIt.hasNext())
{
updateShipFromPhysics(shipIt.next());
}
Iterator<Projectile> projectileIt = this.projectileIterator(true);
while (projectileIt.hasNext())
{
updateProjectileFromPhysics(projectileIt.next());
}
for (EventPublic event : physicsEnv.eventIterable())
{
if (event instanceof ProjectileExplosionPublic)
{
ProjectileExplosionTracker tracker = (ProjectileExplosionTracker) eventTrackers.get(event);
if (tracker == null)
{
tracker = new ProjectileExplosionTracker(resourceDB, physicsEnv, this);
eventTrackers.set(event, tracker);
}
tracker.update((ProjectileExplosionPublic) event);
}
else if (event instanceof ActorDiedPublic)
{
ActorDiedTracker tracker = (ActorDiedTracker) eventTrackers.get(event);
if (tracker == null)
{
tracker = new ActorDiedTracker(resourceDB, physicsEnv, this);
eventTrackers.set(event, tracker);
}
tracker.update((ActorDiedPublic) event);
}
}
}
private void updateShipFromPhysics(@Nonnull ActorShip actorShip)
{
PhysicsShipPosition actorPos = new PhysicsShipPosition();
Point localActorPos = new Point();
ActorPublic physicsActor = actorShip.getActor();
if (localShip != null && localShip.getActor() != null
&& localShip.getActor().getPosition(actorPos))
{
localActorPos.set(actorPos.x, actorPos.y);
}
if (renderDelay == null)
{
actorShip.renderingAt_tick = physicsEnv.getTick();
}
else
{
renderDelay.calculateRenderAtTick(actorShip);
}
boolean existed = actorShip.isExists();
actorShip.setExists(true);
if (physicsActor.isNonExistent(actorShip.renderingAt_tick))
{
actorShip.setExists(false);
}
if (physicsActor.isDead(actorShip.renderingAt_tick))
{
actorShip.setExists(false);
}
if (physicsActor.getHistoricPosition(actorPos, actorShip.renderingAt_tick, true))
{
actorShip.setRealPositionFromPhysics(actorPos.x, actorPos.y);
if (actorShip.isLocalPlayer())
{
actorShip.setPositionFromPhysics(actorPos.x, actorPos.y);
}
else
{
actorShip.setPositionFromPhysics(actorPos.smooth_x, actorPos.smooth_y);
}
actorShip.setRotationFromPhysics(actorPos.rot_snapped);
}
else
{
actorShip.setExists(false);
}
if (physicsActor.getPosition(actorPos))
{
actorShip.setShadowPositionFromPhysics(actorPos.x, actorPos.y);
}
if (actorShip != localShip)
{
actorShip.updateDistanceToLocal(localActorPos);
}
if (!actorShip.isLocalPlayer() && actorShip.isExists() && !existed)
{
actorShip.setAlpha(0, ActorShip.SPAWN_ALPHA_VELOCITY);
}
}
private void updateProjectileFromPhysics(@Nonnull Projectile projectile)
{
PhysicsPositionVector projectilePos = new PhysicsPositionVector();
PhysicsPositionVector historicProjectilePos = new PhysicsPositionVector();
ProjectilePublic physicsProjectile = projectile.getPhysicsProjectile();
physicsProjectile.getPosition(projectilePos);
projectile.setShadowPositionFromPhysics(projectilePos.pos.x, projectilePos.pos.y);
if (renderDelay == null)
{
projectile.renderingAt_tick = physicsEnv.getTick();
}
else
{
renderDelay.calculateRenderAtTick(projectile);
}
boolean existedAnyTime = projectile.hasExistedAnyTime();
boolean existedPreviousFrame = projectile.isExists();
projectile.setExists(true);
if (physicsProjectile.isNonExistent(projectile.renderingAt_tick))
{
projectile.setExists(false);
}
if (physicsProjectile.getHistoricPosition(
historicProjectilePos,
projectile.renderingAt_tick,
true))
{
projectile.setPositionFromPhysics(historicProjectilePos.pos.x, historicProjectilePos.pos.y);
}
else
{
projectile.setExists(false);
}
if (!existedPreviousFrame && projectile.isExists() && existedAnyTime)
{
// projectile is back because of a timewarp
projectile.setAlpha(0);
}
if (projectile.isExists() && projectile.getAlpha() < 1)
{
float vel = Projectile.TIMEWARP_ALPHA_VELOCITY_MIN;
// if the projectile is closer to the local player
// fade it in faster
if (localShip != null)
{
float dist = localShip.pos.distanceSquared(projectile.pos);
float smooth_dist = Projectile.TIMEWARP_ALPHA_VELOCITY_LOCAL_DIST_SMOOTHING;
if (dist < smooth_dist)
{
float max = Projectile.TIMEWARP_ALPHA_VELOCITY_MAX;
float min = Projectile.TIMEWARP_ALPHA_VELOCITY_MIN;
vel = (max - min) * (1 - dist / smooth_dist) + min;
}
}
projectile.setAlphaVelocity(vel);
}
}
}