//Copyright Paul Reiche, Fred Ford. 1992-2002

/*
 *  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.
 *
 *  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 "tactrans.h"

#include "battlecontrols.h"
#include "build.h"
#include "collide.h"
#include "globdata.h"
#include "element.h"
#include "ship.h"
#include "status.h"
#include "battle.h"
#include "init.h"
#include "supermelee/pickmele.h"
#ifdef NETPLAY
#	include "supermelee/netplay/netmelee.h"
#	include "supermelee/netplay/netmisc.h"
#	include "supermelee/netplay/notify.h"
#	include "supermelee/netplay/proto/ready.h"
#	include "supermelee/netplay/packet.h"
#	include "supermelee/netplay/packetq.h"
#endif
#include "races.h"
#include "encount.h"
#include "settings.h"
#include "sounds.h"
#include "libs/mathlib.h"


static void cleanup_dead_ship (ELEMENT *ElementPtr);

static BOOLEAN dittyIsPlaying;
static STARSHIP *winnerStarShip;
		// Indicates which ship is the winner of the current battle.
		// The winner will be last to pick the next ship.


BOOLEAN
OpponentAlive (STARSHIP *TestStarShipPtr)
{
	HELEMENT hElement, hSuccElement;

	for (hElement = GetHeadElement (); hElement; hElement = hSuccElement)
	{
		ELEMENT *ElementPtr;
		STARSHIP *StarShipPtr;

		LockElement (hElement, &ElementPtr);
		hSuccElement = GetSuccElement (ElementPtr);
		GetElementStarShip (ElementPtr, &StarShipPtr);
		UnlockElement (hElement);

		if (StarShipPtr && StarShipPtr != TestStarShipPtr
				&& StarShipPtr->RaceDescPtr->ship_info.crew_level == 0)
			return FALSE;
	}

	return TRUE;
}

static void
PlayDitty (STARSHIP *ship)
{
	PlayMusic (ship->RaceDescPtr->ship_data.victory_ditty, FALSE, 3);
	dittyIsPlaying = TRUE;
}

void
StopDitty (void)
{
	if (dittyIsPlaying)
		StopMusic ();
	dittyIsPlaying = FALSE;
}

static BOOLEAN
DittyPlaying (void)
{
	if (!dittyIsPlaying)
		return FALSE;

	dittyIsPlaying = PLRPlaying ((MUSIC_REF)~0);
	return dittyIsPlaying;
}

void
ResetWinnerStarShip (void)
{
	winnerStarShip = NULL;
}

#ifdef NETPLAY
static void
readyToEnd2Callback (NetConnection *conn, void *arg)
{
	NetConnection_setState (conn, NetState_endingBattle2);
	(void) arg;
}

static void
readyToEndCallback (NetConnection *conn, void *arg)
{
	// This callback function gets called from inside the function that
	// updates the frame counter, but this is not a problem as the
	// ending frame count will at least be 1 greater than the current
	// frame count.

	BattleStateData *battleStateData;
	battleStateData = (BattleStateData *) NetConnection_getStateData(conn);

#ifdef NETPLAY_DEBUG
	fprintf (stderr, "Both sides are ready to end the battle; starting "
			"end-of-battle synchronisation.\n");
#endif
	NetConnection_setState (conn, NetState_endingBattle);
	if (battleFrameCount + 1 > battleStateData->endFrameCount)
		battleStateData->endFrameCount = battleFrameCount + 1;
	Netplay_Notify_frameCount (conn, battleFrameCount + 1);
			// The +1 is to ensure that after the remote side receives the
			// frame count it will still receive one more frame data packet,
			// so it will know in advance when the last frame data packet
			// will come so it won't block. It also ensures that the
			// local frame counter won't go past the sent number, which
			// could happen when the function triggering the call to this
			// function is the frame update function which might update
			// the frame counter one more time.
	flushPacketQueue (conn);
#ifdef NETPLAY_DEBUG
    fprintf (stderr, "NETPLAY: [%d] ==> Sent battleFrameCount %d.\n",
			NetConnection_getPlayerNr(conn), battleFrameCount + 1);
#endif
	Netplay_localReady(conn, readyToEnd2Callback, NULL, false);
	(void) arg;
}

/*
 * When one player's ship dies, there's a delay before the next ship
 * can be chosen. This time depends on the time the ditty is playing
 * and may differ for each side.
 * To synchronise the time, the following protocol is followed:
 * 1. (NetState_inBattle) The Ready protocol is used to let either
 *    party know when they're ready to stop the battle.
 * 2. (NetState_endingBattle) Each party sends the frame number of when
 *    it wants to end the battle, and continues until that point, where
 *    it waits until it has received the frame number of the other party.
 * 3. After a player has both sent and received a frame count, the
 *    simulation continues for each party, until the maximum of both
 *    frame counts has been achieved.
 * 4. The Ready protocol is used to let each side signal that it has
 *    reached the target frame count.
 * 5. The battle ends.
 */
static bool
readyForBattleEndPlayer (NetConnection *conn)
{
	BattleStateData *battleStateData;
	battleStateData = (BattleStateData *) NetConnection_getStateData(conn);

	if (NetConnection_getState (conn) == NetState_interBattle ||
			NetConnection_getState (conn) == NetState_inSetup)
	{
		// This connection is already ready. The entire synchronisation
		// protocol has already been done for this connection.
		return true;
	}

	if (NetConnection_getState (conn) == NetState_inBattle)
	{
		if (Netplay_isLocalReady(conn))
		{
			// We've already sent notice that we are ready, but we're
			// still waiting for the other side to say it's ready too.
			return false;
		}

		// We haven't yet told the other side we're ready. We do so now.
		Netplay_localReady (conn, readyToEndCallback, NULL, true);
				// This may set the state to endingBattle.

		if (NetConnection_getState (conn) == NetState_inBattle)
			return false;
	}

	assert (NetConnection_getState (conn) == NetState_endingBattle ||
			NetConnection_getState (conn) == NetState_endingBattle2);
	
	// Keep the simulation going as long as the target frame count
	// hasn't been reached yet. Note that if the connection state is
	// NetState_endingBattle, then we haven't yet received the
	// remote frame count, so the target frame count may still rise.
	if (battleFrameCount < battleStateData->endFrameCount)
		return false;

	if (NetConnection_getState (conn) == NetState_endingBattle)
	{
		// We have reached the target frame count, but we don't know
		// the remote target frame count yet. So we wait until it has
		// come in.
		waitReady (conn);
		// TODO: check whether all connections are still connected.
		assert (NetConnection_getState (conn) == NetState_endingBattle2);

		// Continue the simulation if the battleFrameCount has gone up.
		if (battleFrameCount < battleStateData->endFrameCount)
			return false;
	}

	// We are ready and wait for the other party to become ready too.
	negotiateReady (conn, true, NetState_interBattle);

	return true;	
}
#endif

bool
battleEndReadyHuman (HumanInputContext *context)
{
	(void) context;
	return true;
}

bool
battleEndReadyComputer (ComputerInputContext *context)
{
	(void) context;
	return true;
}

#ifdef NETPLAY
bool
battleEndReadyNetwork (NetworkInputContext *context)
{
	return readyForBattleEndPlayer (netConnections[context->playerNr]);
}
#endif

// Returns true iff this side is ready to end the battle.
static inline bool
readyForBattleEnd (void)
{
#ifndef NETPLAY
#if DEMO_MODE
	// In Demo mode, the saved journal should be replayed with frame
	// accuracy. PLRPlaying () isn't consistent enough.
	return true;
#else  /* !DEMO_MODE */
	return !DittyPlaying ();
#endif  /* !DEMO_MODE */
#else  /* defined (NETPLAY) */
	int playerI;

	if (DittyPlaying ())
		return false;

	for (playerI = 0; playerI < NUM_PLAYERS; playerI++)
		if (!PlayerInput[playerI]->handlers->battleEndReady (
				PlayerInput[playerI]))
			return false;

	return true;
#endif  /* defined (NETPLAY) */
}

static void
preprocess_dead_ship (ELEMENT *DeadShipPtr)
{
	ProcessSound ((SOUND)~0, NULL);
	(void)DeadShipPtr; // unused argument
}

void
cleanup_dead_ship (ELEMENT *DeadShipPtr)
{
	STARSHIP *DeadStarShipPtr;

	ProcessSound ((SOUND)~0, NULL);

	GetElementStarShip (DeadShipPtr, &DeadStarShipPtr);
	{
		// Ship explosion has finished, or ship has just warped out
		// if DeadStarShipPtr->crew_level != 0
		BOOLEAN MusicStarted;
		HELEMENT hElement, hSuccElement;

		/* Record crew left after the battle */
		DeadStarShipPtr->crew_level =
				DeadStarShipPtr->RaceDescPtr->ship_info.crew_level;

		MusicStarted = FALSE;

		for (hElement = GetHeadElement (); hElement; hElement = hSuccElement)
		{
			ELEMENT *ElementPtr;
			STARSHIP *StarShipPtr;

			LockElement (hElement, &ElementPtr);
			hSuccElement = GetSuccElement (ElementPtr);
			GetElementStarShip (ElementPtr, &StarShipPtr);
					// Get the STARSHIP that this ELEMENT belongs to.

			if (StarShipPtr == DeadStarShipPtr)
			{
				// This element belongs to the dead ship; it may be the
				// ship's own element.
				SetElementStarShip (ElementPtr, 0);

				if (!(ElementPtr->state_flags & CREW_OBJECT)
						|| ElementPtr->preprocess_func != crew_preprocess)
				{
					// Set the element up for deletion.
					SetPrimType (&DisplayArray[ElementPtr->PrimIndex],
							NO_PRIM);
					ElementPtr->life_span = 0;
					ElementPtr->state_flags =
							NONSOLID | DISAPPEARING | FINITE_LIFE;
					ElementPtr->preprocess_func = 0;
					ElementPtr->postprocess_func = 0;
					ElementPtr->death_func = 0;
					ElementPtr->collision_func = 0;
				}
			}

			if (StarShipPtr
					&& (StarShipPtr->cur_status_flags & PLAY_VICTORY_DITTY))
			{
				// StarShipPtr points to the remaining ship.
				MusicStarted = TRUE;
				PlayDitty (StarShipPtr);
				StarShipPtr->cur_status_flags &= ~PLAY_VICTORY_DITTY;
			}

			UnlockElement (hElement);
		}

#define MIN_DITTY_FRAME_COUNT  ((ONE_SECOND * 3) / BATTLE_FRAME_RATE)
		// The ship will be "alive" for at least 2 more frames to make sure
		// the elements it owns (set up for deletion above) expire first.
		// Ditty does NOT play in the following circumstances:
		//  * The winning ship dies before the loser finishes exploding
		//  * At the moment the losing ship dies, the winner has started
		//    the warp out sequence
		DeadShipPtr->life_span = MusicStarted ? MIN_DITTY_FRAME_COUNT : 1;
		if (DeadStarShipPtr == winnerStarShip)
		{	// This ship died but won the battle. We need to keep it alive
			// longer than the dead opponent ship so that the winning player
			// picks last.
			DeadShipPtr->life_span = MIN_DITTY_FRAME_COUNT + 1;
		}
		DeadShipPtr->death_func = new_ship;
		DeadShipPtr->preprocess_func = preprocess_dead_ship;
		DeadShipPtr->state_flags &= ~DISAPPEARING;
		// XXX: this increment was originally done by another piece of code
		//   just below this one. I am almost sure it is not needed, but it
		//   keeps the original framecount.
		++DeadShipPtr->life_span;
		SetElementStarShip (DeadShipPtr, DeadStarShipPtr);
	}
}

static void
setMinShipLifeSpan (ELEMENT *ship, COUNT life_span)
{
	if (ship->death_func == new_ship)
	{	// The ship has finished exploding or warping out, and now
		// we can work with the remaining element
		assert (ship->state_flags & FINITE_LIFE);
		assert (!(ship->state_flags & DISAPPEARING));
		if (ship->life_span < life_span)
			ship->life_span = life_span;
	}
}

static void
setMinStarShipLifeSpan (STARSHIP *starShip, COUNT life_span)
{
	ELEMENT *ship;

	LockElement (starShip->hShip, &ship);
	setMinShipLifeSpan (ship, life_span);
	UnlockElement (starShip->hShip);
}

static void
checkOtherShipLifeSpan (ELEMENT *deadShip)
{
	STARSHIP *deadStarShip;

	GetElementStarShip (deadShip, &deadStarShip);

	if (winnerStarShip != NULL && deadStarShip != winnerStarShip
			&& winnerStarShip->RaceDescPtr->ship_info.crew_level == 0)
	{	// The opponent ship also died but won anyway (e.g. Glory device)
		// We need to keep the opponent ship alive longer so that the
		// winning player picks last.
		setMinStarShipLifeSpan (winnerStarShip, deadShip->life_span + 1);
	}
	else if (winnerStarShip == NULL)
	{	// Both died at the same time, or the loser has already expired
		HELEMENT hElement, hNextElement;

		// Find the other dead ship(s) and keep them alive for at least as
		// long as this ship.
		for (hElement = GetHeadElement (); hElement; hElement = hNextElement)
		{
			ELEMENT *element;
			STARSHIP *starShip;

			LockElement (hElement, &element);
			hNextElement = GetSuccElement (element);
			GetElementStarShip (element, &starShip);
			
			if (starShip != NULL && element != deadShip
					&& starShip->RaceDescPtr->ship_info.crew_level == 0)
			{	// This is another dead ship
				setMinShipLifeSpan (element, deadShip->life_span);
			}

			UnlockElement (hElement);
		}
	}
}

// This function is called when dead ship element's life_span reaches 0
void
new_ship (ELEMENT *DeadShipPtr)
{
	STARSHIP *DeadStarShipPtr;

	GetElementStarShip (DeadShipPtr, &DeadStarShipPtr);

	if (!readyForBattleEnd ())
	{
		DeadShipPtr->state_flags &= ~DISAPPEARING;
		++DeadShipPtr->life_span;

		// Keep the winner alive longer, or in a simultaneous destruction
		// tie, keep the other dead ship alive so that readyForBattleEnd()
		// is called for only one ship at a time.
		// When a ship has been destroyed, each side of a network
		// connection waits until the other side is ready.
		// When two ships die at the same time, this is handled for one
		// ship after the other.
		checkOtherShipLifeSpan (DeadShipPtr);
		return;
	}

	// Once a ship is being picked, we do not care about the winner anymore
	winnerStarShip = NULL;

	{
		BOOLEAN RestartMusic;

		StopDitty ();
		StopMusic ();
		StopSound ();

		SetElementStarShip (DeadShipPtr, 0);
		RestartMusic = OpponentAlive (DeadStarShipPtr);

		free_ship (DeadStarShipPtr->RaceDescPtr, TRUE, TRUE);
		DeadStarShipPtr->RaceDescPtr = 0;
		
		// Graphics are batched while the draw queue is processed,
		// but we are going to draw the ship selection box now
		UnbatchGraphics ();

#ifdef NETPLAY
		initBattleStateDataConnections ();
		{
			bool allOk =
					negotiateReadyConnections (true, NetState_interBattle);
					// We are already in NetState_interBattle, but all
					// sides just need to pass this checkpoint before
					// going on.
			if (!allOk)
			{
				// Some network connection has been reset.
				GLOBAL (CurrentActivity) &= ~IN_BATTLE;
				BatchGraphics ();
				return;
			}
		}
#endif  /* NETPLAY */

		if (!FleetIsInfinite (DeadStarShipPtr->playerNr))
		{	// This may be a dead ship (crew_level == 0) or a warped out ship
			UpdateShipFragCrew (DeadStarShipPtr);
			// Deactivate the ship (cannot be selected)
			DeadStarShipPtr->SpeciesID = NO_ID;
		}

		if (GetNextStarShip (DeadStarShipPtr, DeadStarShipPtr->playerNr))
		{
#ifdef NETPLAY
			{
				bool allOk =
						negotiateReadyConnections (true, NetState_inBattle);
				if (!allOk)
				{
					// Some network connection has been reset.
					GLOBAL (CurrentActivity) &= ~IN_BATTLE;
					BatchGraphics ();
					return;
				}
			}
#endif
			if (RestartMusic)
				BattleSong (TRUE);
		}
		else if (battle_counter[0] == 0 || battle_counter[1] == 0)
		{
			// One player is out of ships. The battle is over.
			GLOBAL (CurrentActivity) &= ~IN_BATTLE;
		}
#ifdef NETPLAY
		else
		{
			// Battle has been aborted.
			GLOBAL (CurrentActivity) |= CHECK_ABORT;
		}
#endif
		BatchGraphics ();
	}
}

static void
explosion_preprocess (ELEMENT *ShipPtr)
{
	BYTE i;

	i = (NUM_EXPLOSION_FRAMES * 3) - ShipPtr->life_span;
	switch (i)
	{
		case 25:
			ShipPtr->preprocess_func = NULL;
		case 0:
		case 1:
		case 2:
		case 20:
		case 21:
		case 22:
		case 23:
		case 24:
			i = 1;
			break;
		case 3:
		case 4:
		case 5:
		case 18:
		case 19:
			i = 2;
			break;
		case 15:
			SetPrimType (&DisplayArray[ShipPtr->PrimIndex], NO_PRIM);
			ShipPtr->state_flags |= CHANGING;
		default:
			i = 3;
			break;
	}

	do
	{
		HELEMENT hElement;

		hElement = AllocElement ();
		if (hElement)
		{
			COUNT angle, dist;
			DWORD rand_val;
			ELEMENT *ElementPtr;
			extern FRAME explosion[];

			PutElement (hElement);
			LockElement (hElement, &ElementPtr);
			ElementPtr->playerNr = NEUTRAL_PLAYER_NUM;
			ElementPtr->state_flags = APPEARING | FINITE_LIFE | NONSOLID;
			ElementPtr->life_span = 9;
			SetPrimType (&DisplayArray[ElementPtr->PrimIndex], STAMP_PRIM);
			ElementPtr->current.image.farray = explosion;
			ElementPtr->current.image.frame = explosion[0];
			rand_val = TFB_Random ();
			angle = LOBYTE (HIWORD (rand_val));
			dist = DISPLAY_TO_WORLD (LOBYTE (LOWORD (rand_val)) % 8);
			if (HIBYTE (LOWORD (rand_val)) < 256 * 1 / 3)
				dist += DISPLAY_TO_WORLD (8);
			ElementPtr->current.location.x =
					ShipPtr->current.location.x + COSINE (angle, dist);
			ElementPtr->current.location.y =
					ShipPtr->current.location.y + SINE (angle, dist);
			ElementPtr->preprocess_func = animation_preprocess;
			rand_val = TFB_Random ();
			angle = LOBYTE (LOWORD (rand_val));
			dist = WORLD_TO_VELOCITY (
					DISPLAY_TO_WORLD (HIBYTE (LOWORD (rand_val)) % 5));
			SetVelocityComponents (&ElementPtr->velocity,
					COSINE (angle, dist), SINE (angle, dist));
			UnlockElement (hElement);
		}
	} while (--i);
}

void
StopAllBattleMusic (void)
{
	StopDitty ();
	StopMusic ();
}

STARSHIP *
FindAliveStarShip (ELEMENT *deadShip)
{
	STARSHIP *aliveShip = NULL;
	HELEMENT hElement, hNextElement;

	// Find the remaining ship, if any, and see if it is still alive.
	for (hElement = GetHeadElement (); hElement; hElement = hNextElement)
	{
		ELEMENT *ElementPtr;

		LockElement (hElement, &ElementPtr);
		if ((ElementPtr->state_flags & PLAYER_SHIP)
				&& ElementPtr != deadShip
						/* and not running away */
				&& ElementPtr->mass_points <= MAX_SHIP_MASS + 1)
		{
			GetElementStarShip (ElementPtr, &aliveShip);
			assert (aliveShip != NULL);
			if (aliveShip->RaceDescPtr->ship_info.crew_level == 0
					/* reincarnating Pkunk is not actually dead */
					&& ElementPtr->mass_points != MAX_SHIP_MASS + 1)
			{
				aliveShip = NULL;
			}

			UnlockElement (hElement);
			break;
		}
		hNextElement = GetSuccElement (ElementPtr);
		UnlockElement (hElement);
	}
	
	return aliveShip;
}

STARSHIP *
GetWinnerStarShip (void)
{
	return winnerStarShip;
}

void
SetWinnerStarShip (STARSHIP *winner)
{
	if (winner == NULL)
		return; // nothing to do
	
	winner->cur_status_flags |= PLAY_VICTORY_DITTY;

	// The winner is set once per battle. If both ships die, this function is
	// called twice, once for each ship. We need to preserve the winner
	// determined on the first call.
	if (winnerStarShip == NULL)
		winnerStarShip = winner;
}

void
RecordShipDeath (ELEMENT *deadShip)
{
	STARSHIP *deadStarShip;

	GetElementStarShip (deadShip, &deadStarShip);
	assert (deadStarShip != NULL);

	if (deadShip->mass_points <= MAX_SHIP_MASS)
	{	// Not running away.
		// When a ship tries to run away, it is (dis)counted in DoRunAway(),
		// so when it dies while running away, we will not count it again
		assert (deadStarShip->playerNr >= 0);
		battle_counter[deadStarShip->playerNr]--;
	}

	if (LOBYTE (GLOBAL (CurrentActivity)) == SUPER_MELEE)
		MeleeShipDeath (deadStarShip);
}

void
StartShipExplosion (ELEMENT *ShipPtr, bool playSound)
{
	STARSHIP *StarShipPtr;

	GetElementStarShip (ShipPtr, &StarShipPtr);

	ZeroVelocityComponents (&ShipPtr->velocity);

	DeltaEnergy (ShipPtr,
			-(SIZE)StarShipPtr->RaceDescPtr->ship_info.energy_level);

	ShipPtr->life_span = NUM_EXPLOSION_FRAMES * 3;
	ShipPtr->state_flags &= ~DISAPPEARING;
	ShipPtr->state_flags |= FINITE_LIFE | NONSOLID;
	ShipPtr->preprocess_func = explosion_preprocess;
	ShipPtr->postprocess_func = PostProcessStatus;
	ShipPtr->death_func = cleanup_dead_ship;
	ShipPtr->hTarget = 0;

	if (playSound)
	{
		PlaySound (SetAbsSoundIndex (GameSounds, SHIP_EXPLODES),
				CalcSoundPosition (ShipPtr), ShipPtr, GAME_SOUND_PRIORITY + 1);
	}
}

void
ship_death (ELEMENT *ShipPtr)
{
	STARSHIP *StarShipPtr;
	STARSHIP *winner;

	GetElementStarShip (ShipPtr, &StarShipPtr);

	StopAllBattleMusic ();

	// If the winning ship dies before the ditty starts, do not play it.
	// e.g. a ship can die after the opponent begins exploding but
	// before the explosion is over.
	StarShipPtr->cur_status_flags &= ~PLAY_VICTORY_DITTY;

	StartShipExplosion (ShipPtr, true);

	winner = FindAliveStarShip (ShipPtr);
	SetWinnerStarShip (winner);
	RecordShipDeath (ShipPtr);
}

#define START_ION_COLOR BUILD_COLOR (MAKE_RGB15 (0x1F, 0x15, 0x00), 0x7A)

// Called from the death_func of an element for an ion trail pixel, or a
// ship shadow (when warping in/out).
static void
cycle_ion_trail (ELEMENT *ElementPtr)
{
	static const Color colorTab[] =
	{
		BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x15, 0x00), 0x7a),
		BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x11, 0x00), 0x7b),
		BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x0E, 0x00), 0x7c),
		BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x0A, 0x00), 0x7d),
		BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x07, 0x00), 0x7e),
		BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x03, 0x00), 0x7f),
		BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x00, 0x00), 0x2a),
		BUILD_COLOR (MAKE_RGB15_INIT (0x1B, 0x00, 0x00), 0x2b),
		BUILD_COLOR (MAKE_RGB15_INIT (0x17, 0x00, 0x00), 0x2c),
		BUILD_COLOR (MAKE_RGB15_INIT (0x13, 0x00, 0x00), 0x2d),
		BUILD_COLOR (MAKE_RGB15_INIT (0x0F, 0x00, 0x00), 0x2e),
		BUILD_COLOR (MAKE_RGB15_INIT (0x0B, 0x00, 0x00), 0x2f),
	};
	const size_t colorTabCount = sizeof colorTab / sizeof colorTab[0];
			
	assert (!(ElementPtr->state_flags & PLAYER_SHIP));

	ElementPtr->colorCycleIndex++;
	if (ElementPtr->colorCycleIndex != colorTabCount)
	{
		ElementPtr->life_span = ElementPtr->thrust_wait;
				// Reset the life span.
		
		SetPrimColor (&DisplayArray[ElementPtr->PrimIndex],
				colorTab[ElementPtr->colorCycleIndex]);

		ElementPtr->state_flags &= ~DISAPPEARING;
		ElementPtr->state_flags |= CHANGING;
	} // else, the element disappears.
}

void
spawn_ion_trail (ELEMENT *ElementPtr)
{
	HELEMENT hIonElement;

	assert (ElementPtr->state_flags & PLAYER_SHIP);

	hIonElement = AllocElement ();
	if (hIonElement)
	{
#define ION_LIFE 1
		COUNT angle;
		RECT r;
		ELEMENT *IonElementPtr;
		STARSHIP *StarShipPtr;

		GetElementStarShip (ElementPtr, &StarShipPtr);
		angle = FACING_TO_ANGLE (StarShipPtr->ShipFacing) + HALF_CIRCLE;
		GetFrameRect (StarShipPtr->RaceDescPtr->ship_data.ship[0], &r);
		r.extent.height = DISPLAY_TO_WORLD (r.extent.height + r.corner.y);

		InsertElement (hIonElement, GetHeadElement ());
		LockElement (hIonElement, &IonElementPtr);
		IonElementPtr->playerNr = NEUTRAL_PLAYER_NUM;
		IonElementPtr->state_flags = APPEARING | FINITE_LIFE | NONSOLID;
		IonElementPtr->thrust_wait = ION_LIFE;
		IonElementPtr->life_span = IonElementPtr->thrust_wait;
				// When the element "dies", in the death_func
				// 'cycle_ion_trail', it is given new life a number of
				// times, by setting life_span to thrust_wait.
		SetPrimType (&DisplayArray[IonElementPtr->PrimIndex], POINT_PRIM);
		SetPrimColor (&DisplayArray[IonElementPtr->PrimIndex],
				START_ION_COLOR);
		IonElementPtr->colorCycleIndex = 0;
		IonElementPtr->current.image.frame =
				DecFrameIndex (stars_in_space);
		IonElementPtr->current.image.farray = &stars_in_space;
		IonElementPtr->current.location = ElementPtr->current.location;
		IonElementPtr->current.location.x +=
				(COORD)COSINE (angle, r.extent.height);
		IonElementPtr->current.location.y +=
				(COORD)SINE (angle, r.extent.height);
		IonElementPtr->death_func = cycle_ion_trail;

		SetElementStarShip (IonElementPtr, StarShipPtr);

		{
			/* normally done during preprocess, but because
			 * object is being inserted at head rather than
			 * appended after tail it may never get preprocessed.
			 */
			IonElementPtr->next = IonElementPtr->current;
			--IonElementPtr->life_span;
			IonElementPtr->state_flags |= PRE_PROCESS;
		}

		UnlockElement (hIonElement);
	}
}

// Preprocess function for spawning a ship into or out of battle.
// Used when a new ship warps in, or a ship escapes by warping out, but not
// when a Pkunk ship is reborn.
void
ship_transition (ELEMENT *ElementPtr)
{
	if (ElementPtr->state_flags & PLAYER_SHIP)
	{
		if (ElementPtr->state_flags & APPEARING)
		{
			ElementPtr->life_span = HYPERJUMP_LIFE;
			ElementPtr->preprocess_func = ship_transition;
			ElementPtr->postprocess_func = NULL;
			SetPrimType (&DisplayArray[ElementPtr->PrimIndex], NO_PRIM);
			ElementPtr->state_flags |= NONSOLID | FINITE_LIFE | CHANGING;
		}
		else if (ElementPtr->life_span < HYPERJUMP_LIFE)
		{
			if (ElementPtr->life_span == NORMAL_LIFE
					&& ElementPtr->crew_level)
			{
				ElementPtr->current.image.frame =
						ElementPtr->next.image.frame =
						SetEquFrameIndex (
						ElementPtr->current.image.farray[0],
						ElementPtr->current.image.frame);
				SetPrimType (&DisplayArray[ElementPtr->PrimIndex], STAMP_PRIM);
				InitIntersectStartPoint (ElementPtr);
				InitIntersectEndPoint (ElementPtr);
				InitIntersectFrame (ElementPtr);
				ZeroVelocityComponents (&ElementPtr->velocity);
				ElementPtr->state_flags &= ~(NONSOLID | FINITE_LIFE);
				ElementPtr->state_flags |= CHANGING;

				ElementPtr->preprocess_func = ship_preprocess;
				ElementPtr->postprocess_func = ship_postprocess;
			}

			return;
		}
	}

	{
		HELEMENT hShipImage;
		ELEMENT *ShipImagePtr;
		STARSHIP *StarShipPtr;
		
		GetElementStarShip (ElementPtr, &StarShipPtr);
		LockElement (StarShipPtr->hShip, &ShipImagePtr);

		if (!(ShipImagePtr->state_flags & NONSOLID))
		{
			ElementPtr->preprocess_func = NULL;
		}
		else if ((hShipImage = AllocElement ()))
		{
#define TRANSITION_SPEED DISPLAY_TO_WORLD (40)
#define TRANSITION_LIFE 1
			COUNT angle;

			PutElement (hShipImage);

			angle = FACING_TO_ANGLE (StarShipPtr->ShipFacing);

			LockElement (hShipImage, &ShipImagePtr);
			ShipImagePtr->playerNr = NEUTRAL_PLAYER_NUM;
			ShipImagePtr->state_flags = APPEARING | FINITE_LIFE | NONSOLID;
			ShipImagePtr->thrust_wait = TRANSITION_LIFE;
			ShipImagePtr->life_span = ShipImagePtr->thrust_wait;
					// When the element "dies", in the death_func
					// 'cycle_ion_trail', it is given new life a number of
					// times, by setting life_span to thrust_wait.
			SetPrimType (&DisplayArray[ShipImagePtr->PrimIndex],
					STAMPFILL_PRIM);
			SetPrimColor (&DisplayArray[ShipImagePtr->PrimIndex],
					START_ION_COLOR);
			ShipImagePtr->colorCycleIndex = 0;
			ShipImagePtr->current.image = ElementPtr->current.image;
			ShipImagePtr->current.location = ElementPtr->current.location;
			if (!(ElementPtr->state_flags & PLAYER_SHIP))
			{
				ShipImagePtr->current.location.x +=
						COSINE (angle, TRANSITION_SPEED);
				ShipImagePtr->current.location.y +=
						SINE (angle, TRANSITION_SPEED);
				ElementPtr->preprocess_func = NULL;
			}
			else if (ElementPtr->crew_level)
			{
				ShipImagePtr->current.location.x -=
						COSINE (angle, TRANSITION_SPEED)
						* (ElementPtr->life_span - 1);
				ShipImagePtr->current.location.y -=
						SINE (angle, TRANSITION_SPEED)
						* (ElementPtr->life_span - 1);

				ShipImagePtr->current.location.x =
						WRAP_X (ShipImagePtr->current.location.x);
				ShipImagePtr->current.location.y =
						WRAP_Y (ShipImagePtr->current.location.y);
			}
			ShipImagePtr->preprocess_func = ship_transition;
			ShipImagePtr->death_func = cycle_ion_trail;
			SetElementStarShip (ShipImagePtr, StarShipPtr);

			UnlockElement (hShipImage);
		}

		UnlockElement (StarShipPtr->hShip);
	}
}

void
flee_preprocess (ELEMENT *ElementPtr)
{
	STARSHIP *StarShipPtr;

	if (--ElementPtr->turn_wait == 0)
	{
		static const Color colorTab[] =
		{
			BUILD_COLOR (MAKE_RGB15_INIT (0x0A, 0x00, 0x00), 0x2E),
			BUILD_COLOR (MAKE_RGB15_INIT (0x0E, 0x00, 0x00), 0x2D),
			BUILD_COLOR (MAKE_RGB15_INIT (0x13, 0x00, 0x00), 0x2C),
			BUILD_COLOR (MAKE_RGB15_INIT (0x17, 0x00, 0x00), 0x2B),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1B, 0x00, 0x00), 0x2A),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x00, 0x00), 0x29),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x04, 0x04), 0x28),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x0A, 0x0A), 0x27),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x0F, 0x0F), 0x26),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x13, 0x13), 0x25),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x19, 0x19), 0x24),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x13, 0x13), 0x25),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x0F, 0x0F), 0x26),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x0A, 0x0A), 0x27),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x04, 0x04), 0x28),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x00, 0x00), 0x29),
			BUILD_COLOR (MAKE_RGB15_INIT (0x1B, 0x00, 0x00), 0x2A),
			BUILD_COLOR (MAKE_RGB15_INIT (0x17, 0x00, 0x00), 0x2B),
			BUILD_COLOR (MAKE_RGB15_INIT (0x13, 0x00, 0x00), 0x2C),
			BUILD_COLOR (MAKE_RGB15_INIT (0x0E, 0x00, 0x00), 0x2D),
		};
		const size_t colorTabCount = sizeof colorTab / sizeof colorTab[0];

		ElementPtr->colorCycleIndex++;
		if (ElementPtr->colorCycleIndex == colorTabCount)
			ElementPtr->colorCycleIndex = 0;

		SetPrimColor (&DisplayArray[ElementPtr->PrimIndex],
				colorTab[ElementPtr->colorCycleIndex]);

		if (ElementPtr->colorCycleIndex == 0)
			--ElementPtr->thrust_wait;

		ElementPtr->turn_wait = ElementPtr->thrust_wait;
		if (ElementPtr->turn_wait)
		{
			ElementPtr->turn_wait = ((ElementPtr->turn_wait - 1) >> 1) + 1;
		}
		else if (ElementPtr->colorCycleIndex != (colorTabCount / 2))
		{
			ElementPtr->turn_wait = 1;
		}
		else
		{
			ElementPtr->death_func = cleanup_dead_ship;
			ElementPtr->crew_level = 0;

			ElementPtr->life_span = HYPERJUMP_LIFE + 1;
			ElementPtr->preprocess_func = ship_transition;
			ElementPtr->postprocess_func = NULL;
			SetPrimType (&DisplayArray[ElementPtr->PrimIndex], NO_PRIM);
			ElementPtr->state_flags |= NONSOLID | FINITE_LIFE | CHANGING;
		}
	}

	GetElementStarShip (ElementPtr, &StarShipPtr);
	StarShipPtr->cur_status_flags &=
			~(LEFT | RIGHT | THRUST | WEAPON | SPECIAL);
			// Ignore control input when fleeing.
	PreProcessStatus (ElementPtr);
}
