//-----------------------------------------------------------------------------------
//
// Torque Network Library - ZAP example multiplayer vector graphics space game
// Copyright (C) 2004 GarageGames.com, Inc.
// For more information see http://www.opentnl.org
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// For use in products that are not compatible with the terms of the GNU
// General Public License, alternative licensing options are available
// from GarageGames.com.
//
// This program 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 General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
//------------------------------------------------------------------------------------
#include "ship.h"
#include "item.h"
#include "glutInclude.h"
#include "sparkManager.h"
#include "projectile.h"
#include "gameLoader.h"
#include "sfx.h"
#include "UI.h"
#include "UIMenus.h"
#include "UIGame.h"
#include "gameType.h"
#include "gameConnection.h"
#include "shipItems.h"
#include "gameWeapons.h"
#include "gameObjectRender.h"
#include <stdio.h>
namespace Zap
{
static Vector<GameObject *> fillVector;
//------------------------------------------------------------------------
TNL_IMPLEMENT_NETOBJECT(Ship);
Ship::Ship(StringTableEntry playerName, S32 team, Point p, F32 m) : MoveObject(p, CollisionRadius)
{
mObjectTypeMask = ShipType | MoveableType | CommandMapVisType | TurretTargetType;
mNetFlags.set(Ghostable);
for(U32 i = 0; i < MoveStateCount; i++)
{
mMoveState[i].pos = p;
mMoveState[i].angle = 0;
}
mTeam = team;
mHealth = 1.0;
mass = m;
hasExploded = false;
updateExtent();
mPlayerName = playerName;
for(S32 i=0; i<TrailCount; i++)
mTrail[i].reset();
mEnergy = EnergyMax;
for(S32 i = 0; i < ModuleCount; i++)
mModuleActive[i] = false;
mModule[0] = ModuleBoost;
mModule[1] = ModuleShield;
mWeapon[0] = WeaponPhaser;
mWeapon[1] = WeaponMineLayer;
mWeapon[2] = WeaponBurst;
mActiveWeapon = 0;
mCooldown = false;
}
void Ship::onGhostRemove()
{
Parent::onGhostRemove();
for(S32 i = 0; i < ModuleCount; i++)
mModuleActive[i] = false;
updateModuleSounds();
}
void Ship::processArguments(S32 argc, const char **argv)
{
if(argc != 5)
return;
Point pos;
pos.read(argv);
pos *= getGame()->getGridSize();
for(U32 i = 0; i < MoveStateCount; i++)
{
mMoveState[i].pos = pos;
mMoveState[i].angle = 0;
}
updateExtent();
}
void Ship::setActualPos(Point p)
{
mMoveState[ActualState].pos = p;
mMoveState[RenderState].pos = p;
setMaskBits(PositionMask | WarpPositionMask);
}
// process a move. This will advance the position of the ship, as well as adjust the velocity and angle.
void Ship::processMove(U32 stateIndex)
{
U32 msTime = mCurrentMove.time;
mMoveState[LastProcessState] = mMoveState[stateIndex];
F32 maxVel = isBoostActive() ? BoostMaxVelocity : MaxVelocity;
F32 time = mCurrentMove.time * 0.001;
Point requestVel(mCurrentMove.right - mCurrentMove.left, mCurrentMove.down - mCurrentMove.up);
requestVel *= maxVel;
F32 len = requestVel.len();
if(len > maxVel)
requestVel *= maxVel / len;
Point velDelta = requestVel - mMoveState[stateIndex].vel;
F32 accRequested = velDelta.len();
F32 maxAccel = (isBoostActive() ? BoostAcceleration : Acceleration) * time;
if(accRequested > maxAccel)
{
velDelta *= maxAccel / accRequested;
mMoveState[stateIndex].vel += velDelta;
}
else
mMoveState[stateIndex].vel = requestVel;
mMoveState[stateIndex].angle = mCurrentMove.angle;
move(time, stateIndex, false);
}
Point Ship::getAimVector()
{
return Point(cos(mMoveState[ActualState].angle), sin(mMoveState[ActualState].angle) );
}
void Ship::selectWeapon()
{
selectWeapon(mActiveWeapon + 1);
}
void Ship::selectWeapon(U32 weaponIdx)
{
mActiveWeapon = weaponIdx % ShipWeaponCount;
GameConnection *cc = getControllingClient();
if(cc)
{
Vector<StringTableEntry> e;
e.push_back(gWeapons[mWeapon[mActiveWeapon]].name);
static StringTableEntry msg("%e0 selected.");
cc->s2cDisplayMessageE(GameConnection::ColorAqua, SFXUIBoop, msg, e);
}
}
void Ship::processWeaponFire()
{
mFireTimer.update(mCurrentMove.time);
mWeaponFireDecloakTimer.update(mCurrentMove.time);
U32 curWeapon = mWeapon[mActiveWeapon];
if(mCurrentMove.fire && mFireTimer.getCurrent() == 0)
{
if(mEnergy >= gWeapons[curWeapon].minEnergy)
{
mEnergy -= gWeapons[curWeapon].drainEnergy;
mFireTimer.reset(gWeapons[curWeapon].fireDelay);
mWeaponFireDecloakTimer.reset(WeaponFireDecloakTime);
if(!isGhost())
{
Point dir = getAimVector();
createWeaponProjectiles(curWeapon, dir, mMoveState[ActualState].pos, mMoveState[ActualState].vel, CollisionRadius - 2, this);
}
}
}
}
void Ship::controlMoveReplayComplete()
{
// compute the delta between our current render position
// and the server position after client-side prediction has
// been run.
Point delta = mMoveState[ActualState].pos - mMoveState[RenderState].pos;
F32 deltaLen = delta.len();
// if the delta is either very small, or greater than the
// max interpolation threshold, just warp to the new position
if(deltaLen <= 0.5 || deltaLen > MaxControlObjectInterpDistance)
{
// if it's a large delta, get rid of the movement trails.
if(deltaLen > MaxControlObjectInterpDistance)
for(S32 i=0; i<TrailCount; i++)
mTrail[i].reset();
mMoveState[RenderState].pos = mMoveState[ActualState].pos;
mMoveState[RenderState].vel = mMoveState[ActualState].vel;
mInterpolating = false;
}
else
mInterpolating = true;
}
void Ship::idle(GameObject::IdleCallPath path)
{
// don't process exploded ships
if(hasExploded)
return;
if(path == GameObject::ServerIdleMainLoop && isControlled())
{
// if this is a controlled object in the server's main
// idle loop, process the render state forward -- this
// is what projectiles will collide against. This allows
// clients to properly lead other clients, instead of
// piecewise stepping only when packets arrive from the client.
processMove(RenderState);
setMaskBits(PositionMask);
}
else
{
// for all other cases, advance the actual state of the
// object with the current move.
processMove(ActualState);
if(path == GameObject::ServerIdleControlFromClient ||
path == GameObject::ClientIdleControlMain ||
path == GameObject::ClientIdleControlReplay)
{
// for different optimizer settings and different platforms
// the floating point calculations may come out slightly
// differently in the lowest mantissa bits. So normalize
// after each update the position and velocity, so that
// the control state update will not differ from client to server.
const F32 ShipVarNormalizeMultiplier = 128;
const F32 ShipVarNormalizeFraction = 1 / ShipVarNormalizeMultiplier;
mMoveState[ActualState].pos.scaleFloorDiv(ShipVarNormalizeMultiplier, ShipVarNormalizeFraction);
mMoveState[ActualState].vel.scaleFloorDiv(ShipVarNormalizeMultiplier, ShipVarNormalizeFraction);
}
if(path == GameObject::ServerIdleMainLoop ||
path == GameObject::ServerIdleControlFromClient)
{
// update the render state on the server to match
// the actual updated state, and mark the object
// as having changed Position state. An optimization
// here would check the before and after positions
// so as to not update unmoving ships.
mMoveState[RenderState] = mMoveState[ActualState];
setMaskBits(PositionMask);
}
else if(path == GameObject::ClientIdleControlMain ||
path == GameObject::ClientIdleMainRemote)
{
// on the client, update the interpolation of this object
// only if we are not replaying control moves.
mInterpolating = (getActualVel().lenSquared() < MoveObject::InterpMaxVelocity*MoveObject::InterpMaxVelocity);
updateInterpolation();
}
}
// update the object in the game's extents database.
updateExtent();
// if this is a move executing on the server and it's
// different from the last move, then mark the move to
// be updated to the ghosts.
if(path == GameObject::ServerIdleControlFromClient &&
!mCurrentMove.isEqualMove(&mLastMove))
setMaskBits(MoveMask);
mLastMove = mCurrentMove;
mSensorZoomTimer.update(mCurrentMove.time);
mCloakTimer.update(mCurrentMove.time);
bool engineerWasActive = isEngineerActive();
if(path == GameObject::ServerIdleControlFromClient ||
path == GameObject::ClientIdleControlMain ||
path == GameObject::ClientIdleControlReplay)
{
// process weapons and energy on controlled object objects
processWeaponFire();
processEnergy();
}
if(path == GameObject::ClientIdleMainRemote)
{
// for ghosts, find some repair targets for rendering the
// repair effect.
if(isRepairActive())
findRepairTargets();
}
if(path == GameObject::ServerIdleControlFromClient &&
isRepairActive())
{
repairTargets();
}
if(path == GameObject::ClientIdleControlMain ||
path == GameObject::ClientIdleMainRemote)
{
mWarpInTimer.update(mCurrentMove.time);
// Emit some particles, trail sections and update the turbo noise
emitMovementSparks();
for(U32 i=0; i<TrailCount; i++)
mTrail[i].tick(mCurrentMove.time);
updateModuleSounds();
}
}
bool Ship::findRepairTargets()
{
// We use the render position in findRepairTargets so that
// ships that are moving can repair each other (server) and
// so that ships don't render funny repair lines to interpolating
// ships (client)
Vector<GameObject *> hitObjects;
Point pos = getRenderPos();
Point extend(RepairRadius, RepairRadius);
Rect r(pos - extend, pos + extend);
findObjects(ShipType | EngineeredType, hitObjects, r);
mRepairTargets.clear();
for(S32 i = 0; i < hitObjects.size(); i++)
{
GameObject *s = hitObjects[i];
if(s->isDestroyed() || s->getHealth() >= 1)
continue;
if((s->getRenderPos() - pos).len() > (RepairRadius + CollisionRadius))
continue;
if(s->getTeam() != -1 && s->getTeam() != getTeam())
continue;
mRepairTargets.push_back(s);
}
return mRepairTargets.size() != 0;
}
void Ship::repairTargets()
{
F32 totalRepair = RepairHundredthsPerSecond * 0.01 * mCurrentMove.time * 0.001f;
// totalRepair /= mRepairTargets.size();
DamageInfo di;
di.damageAmount = -totalRepair;
di.damagingObject = this;
di.damageType = 0;
for(S32 i = 0; i < mRepairTargets.size(); i++)
mRepairTargets[i]->damageObject(&di);
}
void Ship::processEnergy()
{
static U32 gEnergyDrain[ModuleCount] =
{
Ship::EnergyShieldDrain,
Ship::EnergyBoostDrain,
Ship::EnergySensorDrain,
Ship::EnergyRepairDrain,
0,
Ship::EnergyCloakDrain,
};
bool modActive[ModuleCount];
for(S32 i = 0; i < ModuleCount; i++)
{
modActive[i] = mModuleActive[i];
mModuleActive[i] = false;
}
if(mEnergy > EnergyCooldownThreshold)
mCooldown = false;
for(S32 i = 0; i < ShipModuleCount; i++)
if(mCurrentMove.module[i] && !mCooldown)
mModuleActive[mModule[i]] = true;
// No boost if we're not moving.
if(mModuleActive[ModuleBoost] &&
mCurrentMove.up == 0 &&
mCurrentMove.down == 0 &&
mCurrentMove.left == 0 &&
mCurrentMove.right == 0)
{
mModuleActive[ModuleBoost] = false;
}
// No repair with no targets.
if(mModuleActive[ModuleRepair] && !findRepairTargets())
mModuleActive[ModuleRepair] = false;
// No cloak with nearby sensored people.
if(mModuleActive[ModuleCloak])
{
if(mWeaponFireDecloakTimer.getCurrent() != 0)
mModuleActive[ModuleCloak] = false;
else
{
Rect cloakCheck(getActualPos(), getActualPos());
cloakCheck.expand(Point(CloakCheckRadius, CloakCheckRadius));
fillVector.clear();
findObjects(ShipType, fillVector, cloakCheck);
if(fillVector.size() > 0)
{
for(S32 i=0; i<fillVector.size(); i++)
{
Ship *s = dynamic_cast<Ship*>(fillVector[i]);
if(!s) continue;
if(s->getTeam() != getTeam() && s->isSensorActive())
{
mModuleActive[ModuleCloak] = false;
break;
}
}
}
}
}
F32 scaleFactor = mCurrentMove.time * 0.001;
// Update things based on available energy...
bool anyActive = false;
for(S32 i = 0; i < ModuleCount; i++)
{
if(mModuleActive[i])
{
mEnergy -= S32(gEnergyDrain[i] * scaleFactor);
anyActive = true;
}
}
if(!anyActive && mEnergy <= EnergyCooldownThreshold)
mCooldown = true;
if(mEnergy < EnergyMax)
{
// If we're not doing anything, recharge.
if(!anyActive)
mEnergy += S32(EnergyRechargeRate * scaleFactor);
if(mEnergy <= 0)
{
mEnergy = 0;
for(S32 i = 0; i < ModuleCount; i++)
mModuleActive[i] = false;
mCooldown = true;
}
}
if(mEnergy >= EnergyMax)
mEnergy = EnergyMax;
for(S32 i = 0; i < ModuleCount;i++)
{
if(mModuleActive[i] != modActive[i])
{
if(i == ModuleSensor)
{
mSensorZoomTimer.reset(SensorZoomTime - mSensorZoomTimer.getCurrent(), SensorZoomTime);
mSensorStartTime = getGame()->getCurrentTime();
}
else if(i == ModuleCloak)
mCloakTimer.reset(CloakFadeTime - mCloakTimer.getCurrent(), CloakFadeTime);
setMaskBits(PowersMask);
}
}
}
void Ship::damageObject(DamageInfo *theInfo)
{
if(theInfo->damageAmount > 0)
{
if(!getGame()->getGameType()->objectCanDamageObject(theInfo->damagingObject, this))
return;
// Factor in shields
if(isShieldActive())// && mEnergy >= EnergyShieldHitDrain)
{
//mEnergy -= EnergyShieldHitDrain;
return;
}
}
mHealth -= theInfo->damageAmount;
setMaskBits(HealthMask);
if(mHealth <= 0)
{
mHealth = 0;
kill(theInfo);
}
else if(mHealth > 1)
mHealth = 1;
// Deal with grenades
if(theInfo->damageType == 1)
{
mMoveState[0].vel += theInfo->impulseVector;
}
}
void Ship::updateModuleSounds()
{
static S32 moduleSFXs[ModuleCount] =
{
SFXShieldActive,
SFXShipBoost,
SFXSensorActive,
SFXRepairActive,
-1, // No engineer pack, yo!
SFXCloakActive,
};
for(U32 i = 0; i < ModuleCount; i++)
{
if(mModuleActive[i])
{
if(mModuleSound[i].isValid())
mModuleSound[i]->setMovementParams(mMoveState[RenderState].pos, mMoveState[RenderState].vel);
else if(moduleSFXs[i] != -1)
mModuleSound[i] = SFXObject::play(moduleSFXs[i], mMoveState[RenderState].pos, mMoveState[RenderState].vel);
}
else
{
if(mModuleSound[i].isValid())
{
mModuleSound[i]->stop();
mModuleSound[i] = 0;
}
}
}
}
void Ship::writeControlState(BitStream *stream)
{
stream->write(mMoveState[ActualState].pos.x);
stream->write(mMoveState[ActualState].pos.y);
stream->write(mMoveState[ActualState].vel.x);
stream->write(mMoveState[ActualState].vel.y);
stream->writeRangedU32(mEnergy, 0, EnergyMax);
stream->writeFlag(mCooldown);
stream->writeRangedU32(mFireTimer.getCurrent(), 0, MaxFireDelay);
stream->writeRangedU32(mWeapon[mActiveWeapon], 0, WeaponCount);
}
void Ship::readControlState(BitStream *stream)
{
stream->read(&mMoveState[ActualState].pos.x);
stream->read(&mMoveState[ActualState].pos.y);
stream->read(&mMoveState[ActualState].vel.x);
stream->read(&mMoveState[ActualState].vel.y);
mEnergy = stream->readRangedU32(0, EnergyMax);
mCooldown = stream->readFlag();
U32 fireTimer = stream->readRangedU32(0, MaxFireDelay);
mFireTimer.reset(fireTimer);
mActiveWeapon = 0;
mWeapon[mActiveWeapon] = stream->readRangedU32(0, WeaponCount);
}
U32 Ship::packUpdate(GhostConnection *connection, U32 updateMask, BitStream *stream)
{
GameConnection *gameConnection = (GameConnection *) connection;
if(isInitialUpdate())
{
stream->writeFlag(getGame()->getCurrentTime() - getCreationTime() < 300);
stream->writeStringTableEntry(mPlayerName);
stream->write(mass);
stream->writeRangedU32(mTeam + 1, 0, getGame()->getTeamCount());
// now write all the mounts:
for(S32 i = 0; i < mMountedItems.size(); i++)
{
if(mMountedItems[i].isValid())
{
S32 index = connection->getGhostIndex(mMountedItems[i]);
if(index != -1)
{
stream->writeFlag(true);
stream->writeInt(index, GhostConnection::GhostIdBitSize);
}
}
}
stream->writeFlag(false);
}
if(stream->writeFlag(updateMask & HealthMask))
stream->writeFloat(mHealth, 6);
if(stream->writeFlag(updateMask & LoadoutMask))
{
stream->writeRangedU32(mModule[0], 0, ModuleCount);
stream->writeRangedU32(mModule[1], 0, ModuleCount);
}
stream->writeFlag(hasExploded);
bool shouldWritePosition = (updateMask & InitialMask) ||
gameConnection->getControlObject() != this;
stream->writeFlag(updateMask & WarpPositionMask);
if(!shouldWritePosition)
{
stream->writeFlag(false);
stream->writeFlag(false);
stream->writeFlag(false);
}
else
{
if(stream->writeFlag(updateMask & PositionMask))
{
gameConnection->writeCompressedPoint(mMoveState[RenderState].pos, stream);
writeCompressedVelocity(mMoveState[RenderState].vel, BoostMaxVelocity + 1, stream);
}
if(stream->writeFlag(updateMask & MoveMask))
{
mCurrentMove.pack(stream, NULL, false);
}
if(stream->writeFlag(updateMask & PowersMask))
{
for(S32 i = 0; i < ModuleCount; i++)
stream->writeFlag(mModuleActive[i]);
}
}
return 0;
}
void Ship::unpackUpdate(GhostConnection *connection, BitStream *stream)
{
bool positionChanged = false;
bool wasInitialUpdate = false;
bool playSpawnEffect = false;
if(isInitialUpdate())
{
wasInitialUpdate = true;
playSpawnEffect = stream->readFlag();
stream->readStringTableEntry(&mPlayerName);
stream->read(&mass);
mTeam = stream->readRangedU32(0, gClientGame->getTeamCount()) - 1;
// read mounted items:
while(stream->readFlag())
{
S32 index = stream->readInt(GhostConnection::GhostIdBitSize);
Item *theItem = (Item *) connection->resolveGhost(index);
theItem->mountToShip(this);
}
}
if(stream->readFlag())
mHealth = stream->readFloat(6);
if(stream->readFlag())
{
mModule[0] = stream->readRangedU32(0, ModuleCount);
mModule[1] = stream->readRangedU32(0, ModuleCount);
}
bool explode = stream->readFlag();
bool warp = stream->readFlag();
if(warp)
mWarpInTimer.reset(WarpFadeInTime);
if(stream->readFlag())
{
((GameConnection *) connection)->readCompressedPoint(mMoveState[ActualState].pos, stream);
readCompressedVelocity(mMoveState[ActualState].vel, BoostMaxVelocity + 1, stream);
positionChanged = true;
}
if(stream->readFlag())
{
mCurrentMove = Move();
mCurrentMove.unpack(stream, false);
}
if(stream->readFlag())
{
bool wasActive[ModuleCount];
for(S32 i = 0; i < ModuleCount; i++)
{
wasActive[i] = mModuleActive[i];
mModuleActive[i] = stream->readFlag();
if(i == ModuleSensor && wasActive[i] != mModuleActive[i])
{
mSensorZoomTimer.reset(SensorZoomTime - mSensorZoomTimer.getCurrent(), SensorZoomTime);
mSensorStartTime = gClientGame->getCurrentTime();
}
if(i == ModuleCloak && wasActive[i] != mModuleActive[i])
mCloakTimer.reset(CloakFadeTime - mCloakTimer.getCurrent(), CloakFadeTime);
}
}
mMoveState[ActualState].angle = mCurrentMove.angle;
if(positionChanged)
{
mCurrentMove.time = (U32) connection->getOneWayTime();
processMove(ActualState);
if(!warp)
{
mInterpolating = true;
// if the actual velocity is in the direction of the actual position
// then we'll set it into the render velocity
}
else
{
mInterpolating = false;
mMoveState[RenderState] = mMoveState[ActualState];
for(S32 i=0; i<TrailCount; i++)
mTrail[i].reset();
}
}
if(explode && !hasExploded)
{
hasExploded = true;
disableCollision();
if(!wasInitialUpdate)
emitShipExplosion(mMoveState[ActualState].pos);
}
if(playSpawnEffect)
{
FXManager::emitTeleportInEffect(mMoveState[ActualState].pos, 1);
SFXObject::play(SFXTeleportIn, mMoveState[ActualState].pos, Point());
}
}
F32 getAngleDiff(F32 a, F32 b)
{
// Figure out the shortest path from a to b...
// Restrict them to the range 0-360
while(a<0) a+=360;
while(a>360) a-=360;
while(b<0) b+=360;
while(b>360) b-=360;
if(fabs(b-a) > 180)
{
// Go the other way
return 360-(b-a);
}
else
{
return b-a;
}
}
bool Ship::carryingResource()
{
for(S32 i = mMountedItems.size() - 1; i >= 0; i--)
if(mMountedItems[i].isValid() && mMountedItems[i]->getObjectTypeMask() & ResourceItemType)
return true;
return false;
}
Item *Ship::unmountResource()
{
for(S32 i = mMountedItems.size() - 1; i >= 0; i--)
{
if(mMountedItems[i]->getObjectTypeMask() & ResourceItemType)
{
Item *ret = mMountedItems[i];
ret->dismount();
return ret;
}
}
return NULL;
}
void Ship::setLoadout(U32 module1, U32 module2, U32 weapon1, U32 weapon2, U32 weapon3)
{
if(module1 == mModule[0] && module2 == mModule[1]
&& weapon1 == mWeapon[0] && weapon2 == mWeapon[1] && weapon3 == mWeapon[2])
return;
U32 currentWeapon = mWeapon[mActiveWeapon];
mModule[0] = module1;
mModule[1] = module2;
mWeapon[0] = weapon1;
mWeapon[1] = weapon2;
mWeapon[2] = weapon3;
setMaskBits(LoadoutMask);
GameConnection *cc = getControllingClient();
if(cc)
{
static StringTableEntry msg("Ship loadout configuration updated.");
cc->s2cDisplayMessage(GameConnection::ColorAqua, SFXUIBoop, msg);
}
// Try to see if we can maintain the same weapon we had before.
U32 i;
for(i = 0; i < 3; i++)
{
if(mWeapon[i] == currentWeapon)
{
mActiveWeapon = i;
break;
}
}
if(i == 3)
selectWeapon(0);
if(mModule[0] != ModuleEngineer && mModule[1] != ModuleEngineer)
{
// drop any resources we may be carrying
for(S32 i = mMountedItems.size() - 1; i >= 0; i--)
if(mMountedItems[i]->getObjectTypeMask() & ResourceItemType)
mMountedItems[i]->dismount();
}
}
void Ship::kill()
{
deleteObject(KillDeleteDelay);
hasExploded = true;
setMaskBits(ExplosionMask);
disableCollision();
for(S32 i = mMountedItems.size() - 1; i >= 0; i--)
mMountedItems[i]->onMountDestroyed();
}
void Ship::kill(DamageInfo *theInfo)
{
if(isGhost())
return;
GameConnection *controllingClient = getControllingClient();
if(controllingClient)
{
GameType *gt = getGame()->getGameType();
if(gt)
gt->controlObjectForClientKilled(controllingClient, this, theInfo->damagingObject);
}
kill();
}
enum {
NumShipExplosionColors = 12,
};
Color ShipExplosionColors[NumShipExplosionColors] = {
Color(1, 0, 0),
Color(0.9, 0.5, 0),
Color(1, 1, 1),
Color(1, 1, 0),
Color(1, 0, 0),
Color(0.8, 1.0, 0),
Color(1, 0.5, 0),
Color(1, 1, 1),
Color(1, 0, 0),
Color(0.9, 0.5, 0),
Color(1, 1, 1),
Color(1, 1, 0),
};
void Ship::emitShipExplosion(Point pos)
{
SFXObject::play(SFXShipExplode, pos, Point());
F32 a, b;
a = Random::readF() * 0.4 + 0.5;
b = Random::readF() * 0.2 + 0.9;
F32 c, d;
c = Random::readF() * 0.15 + 0.125;
d = Random::readF() * 0.2 + 0.9;
FXManager::emitExplosion(mMoveState[ActualState].pos, 0.9, ShipExplosionColors, NumShipExplosionColors);
FXManager::emitBurst(pos, Point(a,c), Color(1,1,0.25), Color(1,0,0));
FXManager::emitBurst(pos, Point(b,d), Color(1,1,0), Color(0,0.75,0));
}
void Ship::emitMovementSparks()
{
U32 deltaT = mCurrentMove.time;
// Do nothing if we're under 0.1 vel
if(hasExploded || mMoveState[ActualState].vel.len() < 0.1)
return;
mSparkElapsed += deltaT;
if(mSparkElapsed <= 32)
return;
bool boostActive = isBoostActive();
bool cloakActive = isCloakActive();
Point corners[3];
Point shipDirs[3];
corners[0].set(-20, -15);
corners[1].set( 0, 25);
corners[2].set( 20, -15);
F32 th = FloatHalfPi - mMoveState[RenderState].angle;
F32 sinTh = sin(th);
F32 cosTh = cos(th);
F32 warpInScale = (WarpFadeInTime - mWarpInTimer.getCurrent()) / F32(WarpFadeInTime);
for(S32 i=0; i<3; i++)
{
shipDirs[i].x = corners[i].x * cosTh + corners[i].y * sinTh;
shipDirs[i].y = corners[i].y * cosTh - corners[i].x * sinTh;
shipDirs[i] *= warpInScale;
}
Point leftVec ( mMoveState[ActualState].vel.y, -mMoveState[ActualState].vel.x);
Point rightVec(-mMoveState[ActualState].vel.y, mMoveState[ActualState].vel.x);
leftVec.normalize();
rightVec.normalize();
S32 bestId = -1, leftId, rightId;
F32 bestDot = -1;
// Find the left-wards match
for(S32 i = 0; i < 3; i++)
{
F32 d = leftVec.dot(shipDirs[i]);
if(d >= bestDot)
{
bestDot = d;
bestId = i;
}
}
leftId = bestId;
Point leftPt = mMoveState[RenderState].pos + shipDirs[bestId];
// Find the right-wards match
bestId = -1;
bestDot = -1;
for(S32 i = 0; i < 3; i++)
{
F32 d = rightVec.dot(shipDirs[i]);
if(d >= bestDot)
{
bestDot = d;
bestId = i;
}
}
rightId = bestId;
Point rightPt = mMoveState[RenderState].pos + shipDirs[bestId];
// Stitch things up if we must...
if(leftId == mLastTrailPoint[0] && rightId == mLastTrailPoint[1])
{
mTrail[0].update(leftPt, boostActive, cloakActive);
mTrail[1].update(rightPt, boostActive, cloakActive);
mLastTrailPoint[0] = leftId;
mLastTrailPoint[1] = rightId;
}
else if(leftId == mLastTrailPoint[1] && rightId == mLastTrailPoint[0])
{
mTrail[1].update(leftPt, boostActive, cloakActive);
mTrail[0].update(rightPt, boostActive, cloakActive);
mLastTrailPoint[1] = leftId;
mLastTrailPoint[0] = rightId;
}
else
{
mTrail[0].update(leftPt, boostActive, cloakActive);
mTrail[1].update(rightPt, boostActive, cloakActive);
mLastTrailPoint[0] = leftId;
mLastTrailPoint[1] = rightId;
}
if(isCloakActive())
return;
// Finally, do some particles
Point velDir(mCurrentMove.right - mCurrentMove.left, mCurrentMove.down - mCurrentMove.up);
F32 len = velDir.len();
if(len > 0)
{
if(len > 1)
velDir *= 1 / len;
Point shipDirs[4];
shipDirs[0].set(cos(mMoveState[RenderState].angle), sin(mMoveState[RenderState].angle) );
shipDirs[1].set(-shipDirs[0]);
shipDirs[2].set(shipDirs[0].y, -shipDirs[0].x);
shipDirs[3].set(-shipDirs[0].y, shipDirs[0].x);
for(U32 i = 0; i < 4; i++)
{
F32 th = shipDirs[i].dot(velDir);
if(th > 0.1)
{
// shoot some sparks...
if(th >= 0.2*velDir.len())
{
Point chaos(TNL::Random::readF(),TNL::Random::readF());
chaos *= 5;
//interp give us some nice enginey colors...
Color dim(1, 0, 0);
Color light(1, 1, boostActive ? 1.f : 0.f);
Color thrust;
F32 t = TNL::Random::readF();
thrust.interp(t, dim, light);
FXManager::emitSpark(mMoveState[RenderState].pos - shipDirs[i] * 13,
-shipDirs[i] * 100 + chaos, thrust, 1.5 * Random::readF());
}
}
}
}
}
void Ship::render()
{
if(hasExploded)
return;
GameType *g = gClientGame->getGameType();
Color color;
if(g)
color = g->getShipColor(this);
F32 alpha = 1.0;
if(isCloakActive())
alpha = 1 - mCloakTimer.getFraction();
else
alpha = mCloakTimer.getFraction();
Point velDir(mCurrentMove.right - mCurrentMove.left, mCurrentMove.down - mCurrentMove.up);
F32 len = velDir.len();
F32 thrusts[4];
for(U32 i = 0; i < 4; i++)
thrusts[i] = 0;
if(len > 0)
{
if(len > 1)
velDir *= 1 / len;
Point shipDirs[4];
shipDirs[0].set(cos(mMoveState[RenderState].angle), sin(mMoveState[RenderState].angle) );
shipDirs[1].set(-shipDirs[0]);
shipDirs[2].set(shipDirs[0].y, -shipDirs[0].x);
shipDirs[3].set(-shipDirs[0].y, shipDirs[0].x);
for(U32 i = 0; i < 4; i++)
thrusts[i] = shipDirs[i].dot(velDir);
}
// Tweak side thrusters to show rotational force
F32 rotVel = getAngleDiff(mMoveState[LastProcessState].angle, mMoveState[RenderState].angle);
if(rotVel > 0.001)
thrusts[2] += 0.25;
else if(rotVel < -0.001)
thrusts[3] += 0.25;
if(isBoostActive())
{
for(U32 i = 0; i < 4; i++)
thrusts[i] *= 1.3;
}
// an angle of 0 means the ship is heading down the +X axis
// since we draw the ship pointing up the Y axis, we should rotate
// by the ship's angle, - 90 degrees
glPushMatrix();
glTranslatef(mMoveState[RenderState].pos.x, mMoveState[RenderState].pos.y, 0);
GameConnection *conn = gClientGame->getConnectionToServer();
if(conn && conn->getControlObject() != this)
{
const char *string = mPlayerName.getString();
glEnable(GL_BLEND);
F32 textAlpha = 0.5 * alpha;
F32 textSize = 14;
#ifdef TNL_OS_XBOX
textAlpha *= 1 - gClientGame->getCommanderZoomFraction();
textSize = 23;
#else
glLineWidth(1);
#endif
glColor4f(1,1,1,textAlpha);
UserInterface::drawString( U32( UserInterface::getStringWidth(textSize, string) * -0.5), 30, textSize, string );
glDisable(GL_BLEND);
glLineWidth(DefaultLineWidth);
}
else
{
if(alpha < 0.25)
alpha = 0.25;
}
F32 warpInScale = (WarpFadeInTime - mWarpInTimer.getCurrent()) / F32(WarpFadeInTime);
F32 rotAmount = 0;
if(warpInScale < 0.8)
rotAmount = (0.8 - warpInScale) * 540;
glRotatef(radiansToDegrees(mMoveState[RenderState].angle) - 90 + rotAmount, 0, 0, 1.0);
glScalef(warpInScale, warpInScale, 1);
renderShip(color, alpha, thrusts, mHealth, mRadius, isCloakActive(), isShieldActive());
glColor3f(1,1,1);
if(isSensorActive())
{
U32 delta = getGame()->getCurrentTime() - mSensorStartTime;
F32 radius = (delta & 0x1FF) * 0.002;
drawCircle(Point(), radius * Ship::CollisionRadius + 4);
}
glPopMatrix();
for(S32 i = 0; i < mMountedItems.size(); i++)
if(mMountedItems[i].isValid())
mMountedItems[i]->renderItem(mMoveState[RenderState].pos);
if(isRepairActive())
{
glLineWidth(3);
glColor3f(1,0,0);
// render repair rays to all the repairing objects
Point pos = mMoveState[RenderState].pos;
for(S32 i = 0; i < mRepairTargets.size(); i++)
{
if(mRepairTargets[i].getPointer() == this)
drawCircle(pos, RepairDisplayRadius);
else if(mRepairTargets[i])
{
glBegin(GL_LINES);
glVertex2f(pos.x, pos.y);
Point shipPos = mRepairTargets[i]->getRenderPos();
glVertex2f(shipPos.x, shipPos.y);
glEnd();
}
}
glLineWidth(DefaultLineWidth);
}
}
};
syntax highlighted by Code2HTML, v. 0.9.1