/*
  Maelstrom: Open Source version of the classic game by Ambrosia Software
  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
*/

/* Here we define all of the strange and wonderous objects in the game */


class Prize : public Object {

public:
	Prize(int X, int Y, int xVel, int yVel);
	~Prize() { }

	virtual int IsDangerous(void) {
		return(0);
	}

	/* When we are run over, we give prizes! */
	int BeenRunOver(Object *ship) {
		int i;

		if ( ! ship->IsPlayer() || ! ship->Alive() )
			return(0);

		switch (FastRandom(NUM_PRIZES)) {
			case 0:
				/* -- They got machine guns! */
				ship->SetSpecial(MACHINE_GUNS);
				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_MACHINE_GUNS");
				AddSteamTimelineEvent(STEAM_TIMELINE_EVENT_PRIZE_MACHINE_GUNS);
				break;
			case 1:
				/* -- They got Air brakes */
				ship->SetSpecial(AIR_BRAKES);
				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_AIR_BRAKES");
				AddSteamTimelineEvent(STEAM_TIMELINE_EVENT_PRIZE_AIR_BRAKES);
				break;
			case 2:
				/* -- They might get Lucky */
				ship->SetSpecial(LUCKY_IRISH);
				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_LUCK");
				AddSteamTimelineEvent(STEAM_TIMELINE_EVENT_PRIZE_LUCK);
				break;
			case 3:
				/* -- They triple fire */
				ship->SetSpecial(TRIPLE_FIRE);
				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_TRIPLE_FIRE");
				AddSteamTimelineEvent(STEAM_TIMELINE_EVENT_PRIZE_TRIPLE_FIRE);
				break;
			case 4:
				/* -- They got long range */
				ship->SetSpecial(LONG_RANGE);
				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_LONG_RANGE");
				AddSteamTimelineEvent(STEAM_TIMELINE_EVENT_PRIZE_LONG_RANGE);
				break;
			case 5:
				/* -- They got more shields */
				ship->IncrShieldLevel((MAX_SHIELD/5)+
						FastRandom(MAX_SHIELD/2));
				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_SHIELDS");
				AddSteamTimelineEvent(STEAM_TIMELINE_EVENT_PRIZE_SHIELDS);
				break;
			case 6:
				/* -- Put 'em on ICE */
				sound->PlaySound(gFreezeSound, 4);
				gFreezeTime = FREEZE_DURATION;
				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_FREEZING");
				AddSteamTimelineEvent(STEAM_TIMELINE_EVENT_PRIZE_FREEZING);
				break;
			case 7:
				/* Blow up everything */
				sound->PlaySound(gNovaBoom, 5);
				OBJ_LOOP(i, gNumSprites) {
					if ( gSprites[i] == this )
						continue;
					if (gSprites[i]->BeenDamaged(1) < 0) {
						delete gSprites[i];
						gSprites[i] = gSprites[gNumSprites];
					}
				}
				OBJ_LOOP(i, MAX_PLAYERS) {
					if (!gPlayers[i]->IsValid()) {
						continue;
					}
					gPlayers[i]->CutThrust(SHAKE_DURATION);
				}
				gShakeTime = SHAKE_DURATION;
				UnlockSinglePlayerAchievement("ACHIEVEMENT_PRIZE_EXPLOSION");
				AddSteamTimelineEvent(STEAM_TIMELINE_EVENT_PRIZE_EXPLOSION);
				break;
		}
		sound->PlaySound(gGotPrize, 4);
		return(1);
	}

	int BeenTimedOut(void) {
		/* If we time out, we explode, then die. */
		if ( Exploding )
			return(-1);
		else
			return(Explode());
	}

	void ExplodeSound(void) {
		sound->PlaySound(gIdiotSound, 4);
	}
};


class Multiplier : public Object {

public:
	Multiplier(int X, int Y, int Mult);
	~Multiplier() { }

	int BeenShot(Object *ship, Shot *shot) {
		ship->Multiplier(multiplier);
		sound->PlaySound(gMultShotSound, 4);
		return(1);
	}
	int BeenDamaged(int damage) {
		return(0);
	}
	int BeenTimedOut(void) {
		sound->PlaySound(gMultiplierGone, 4);
		return(-1);
	}
	void Shake() { }

protected:
	int multiplier;
};


class Nova : public Object {

public:
	Nova(int X, int Y);
	~Nova() { }

	int BeenTimedOut(void) {
		if ( ! Exploding ) {
			int i;

			int was_exploding = 0;
			OBJ_LOOP(i, gNumSprites) {
				if (gSprites[i]->IsRock() && gSprites[i]->IsExploding()) {
					++was_exploding;
				}
			}

			sound->PlaySound(gNovaBoom, 5);
			OBJ_LOOP(i, gNumSprites) {
				if ( gSprites[i] == this )
					continue;
				if (gSprites[i]->BeenDamaged(1) < 0) {
					delete gSprites[i];
					gSprites[i] = gSprites[gNumSprites];
				}
			}
			OBJ_LOOP(i, MAX_PLAYERS) {
				if (!gPlayers[i]->IsValid()) {
					continue;
				}
				gPlayers[i]->CutThrust(SHAKE_DURATION);
			}
			gShakeTime = SHAKE_DURATION;

			int is_exploding = 0;
			OBJ_LOOP(i, gNumSprites) {
				if (gSprites[i]->IsRock() && gSprites[i]->IsExploding()) {
					++is_exploding;
				}
			}
			if (is_exploding > was_exploding && is_exploding == gNumRocks) {
				UnlockSinglePlayerAchievement("ACHIEVEMENT_SUPERNOVA");
			}
		}
		return(-1);
	}
	void Shake() { }
};

class Bonus : public Object {

public:
	Bonus(int X, int Y, int xVel, int yVel, int Bonus);
	~Bonus() { }

	int BeenShot(Object *ship, Shot *shot) {

		/* Increment the ship's bonus. :) */
		ship->IncrBonus(bonus);
		sound->PlaySound(gBonusShot, 4);

		/* Display point bonus */
		shootable = 0;
		Set_Blit(gPointBlit);
		phasetime = NO_PHASE_CHANGE;
		phase = (bonus/1000);
		Set_TTL(POINT_DURATION);
		xvec = yvec = 0;

		bonus = 0;
		return(0);
	}
	int BeenDamaged(int damage) {
		return(0);
	}
	int BeenTimedOut(void) {
		if ( bonus )
			sound->PlaySound(gMultiplierGone, 4);
		return(-1);
	}
	void Shake() { }

protected:
	int bonus;
};


class Shrapnel : public Object {

public:
	Shrapnel(int X, int Y, int xVel, int yVel, Blit *blit);
	~Shrapnel() { }

	int BeenDamaged(int damage) {
		return(0);
	}
};


class DamagedShip : public Object {

public:
	DamagedShip(int X, int Y, int xVel, int yVel);
	~DamagedShip() { }

	virtual int IsDangerous(void) {
		return(0);
	}

	int BeenRunOver(Object *ship) {
		if (!ship->IsGhost()) {
			ship->IncrLives(1);
			sound->PlaySound(gSavedShipSound, 4);
			UnlockSinglePlayerAchievement("ACHIEVEMENT_GOOD_SAMARITAN");
			AddSteamTimelineEvent(STEAM_TIMELINE_EVENT_FREE_LIFE);
		}
		return(1);
	}

	int BeenTimedOut(void) {
		if ( ! Exploding )
			return(Explode());
		else
			return(-1);
	}

	int Explode(void) {
		/* Create some shrapnel */
		int newsprite, xVel, yVel, rx;

		/* Don't do anything if we're already exploding */
		if ( Exploding ) {
			return(0);
		}

		/* Type 1 shrapnel */
		rx = (SCALE_FACTOR);
		xVel = yVel = 0;
	
		while (xVel == 0)
			xVel = FastRandom(rx / 2) + SCALE_FACTOR;
		while (yVel == 0)
			yVel = FastRandom(rx) - (rx / 2);
		if (yVel > 0)
			yVel += SCALE_FACTOR;
		else
			yVel -= SCALE_FACTOR;
	
		newsprite = gNumSprites;
		gSprites[newsprite]=new Shrapnel(x, y, xVel, yVel, gShrapnel1[0]);

		/* Type 2 shrapnel */
		rx = (SCALE_FACTOR);
		xVel = yVel = 0;

		while (xVel == 0)
			xVel = FastRandom(rx / 2) + SCALE_FACTOR;
		xVel *= -1;
		while (yVel == 0)
			yVel = FastRandom(rx) - (rx / 2);
		if (yVel > 0)
			yVel += SCALE_FACTOR;
		else
			yVel -= SCALE_FACTOR;
		
		newsprite = gNumSprites;
		gSprites[newsprite]=new Shrapnel(x, y, xVel, yVel, gShrapnel2[0]);

		/* Finish our explosion */
		Exploding = 1;
		solid = 0;
		shootable = 0;
		phase = 0;
		nextphase = 0;
		phasetime = 2;
		xvec = yvec = 0;
		myblit = gShipExplosion;
		TTL = (myblit->numFrames*phasetime);
		ExplodeSound();
		return(0);
	}

	void ExplodeSound(void) {
		sound->PlaySound(gShipHitSound, 5);
	}
};


class Gravity : public Object {

public:
	Gravity(int X, int Y);
	~Gravity() { }

	int Move(int Frozen) {
		int i;

		/* Don't gravitize while exploding */
		if ( Exploding )
			return(Object::Move(Frozen));

		/* Warp the courses of the players */
		OBJ_LOOP(i, MAX_PLAYERS) {
			if (!gPlayers[i]->IsValid()) {
				continue;
			}
			int X, Y, xAccel, yAccel;

			if ( ! gPlayers[i]->Alive() )
				continue;

			/* Gravitize! */
			gPlayers[i]->GetPos(&X, &Y);

			if ( ((X>>SPRITE_PRECISION)+(SPRITES_WIDTH/2)) <=
				((x>>SPRITE_PRECISION)+(SPRITES_WIDTH/2)) )
				xAccel = GRAVITY_MOVE;
			else
				xAccel = -GRAVITY_MOVE;

			if ( ((Y>>SPRITE_PRECISION)+(SPRITES_WIDTH/2)) <=
				((y>>SPRITE_PRECISION)+(SPRITES_WIDTH/2)) )
				yAccel = GRAVITY_MOVE;
			else
				yAccel = -GRAVITY_MOVE;

			gPlayers[i]->Accelerate(xAccel, yAccel);
		}

		/* Phase normally */
		return(Object::Move(Frozen));
	}
	void Shake() { }
};


class Homing : public Object {

public:
	Homing(int X, int Y, int xVel, int yVel);
	~Homing() { }

	/* This is duplicated in the Shenobi class */
	virtual int AcquireTarget(void) {
		int targets[MAX_PLAYERS];
		int numTargets = 0;

		for ( int i=0; i < MAX_PLAYERS; ++i ) {
			if (!gPlayers[i]->IsValid()) {
				continue;
			}
			if ( gPlayers[i]->Alive() ) {
				targets[numTargets++] = i;
			}
		}
		if (numTargets > 0) {
			return targets[FastRandom(numTargets)];
		}
		return -1;
	}

	int Move(int Frozen) {
		if ( ((target >= 0) && gPlayers[target]->Alive()) ||
					((target=AcquireTarget()) >= 0) ) {
			int X, Y, xAccel=0, yAccel=0;

			gPlayers[target]->GetPos(&X, &Y);
			if ( ((X>>SPRITE_PRECISION)+(SPRITES_WIDTH/2)) <=
				((x>>SPRITE_PRECISION)+(SPRITES_WIDTH/2)) )
				xAccel -= HOMING_MOVE;
			else 
				xAccel += HOMING_MOVE;
			if ( ((Y>>SPRITE_PRECISION)+(SPRITES_WIDTH/2)) <=
				((y>>SPRITE_PRECISION)+(SPRITES_WIDTH/2)) )
				yAccel -= HOMING_MOVE;
			else 
				yAccel += HOMING_MOVE;
			Accelerate(xAccel, yAccel);
		}
		return(Object::Move(Frozen));
	}

protected:
	int target;
};

class Rock : public Object {
public:
	Rock(int X, int Y, int Xvec, int Yvec, Blit *blit, int PhaseTime) : Object(X, Y, Xvec, Yvec, blit, PhaseTime) {
		++gNumRocks;
	}

	virtual int IsRock(void) {
		return(1);
	}

	virtual int BeenRunOver(Object *ship) {
		int was_exploding = ship->IsExploding();
		int result = Object::BeenRunOver(ship);
		if ( ship->IsPlayer() && !was_exploding && ship->IsExploding() && gNumRocks == 1 ) {
			UnlockSinglePlayerAchievement("ACHIEVEMENT_USING_YOUR_NOGGIN");
		}
		return result;
    }

	virtual int Explode() {
		int result = Object::Explode();

		--gNumRocks;

		if (gNumSmallRocksDestroyed == 0) {
			int i;

			int hot_mess = 0;
			OBJ_LOOP(i, gNumSprites) {
				if (gSprites[i]->IsExploding()) {
					continue;
				}
				if (gSprites[i]->IsSmallRock()) {
					++hot_mess;
					continue;
				}
				if (gSprites[i]->IsMediumRock() || gSprites[i]->IsLargeRock()) {
					hot_mess = false;
					break;
				}
			}
			if (hot_mess >= 3) {
				UnlockSinglePlayerAchievement("ACHIEVEMENT_HOT_MESS");
			}
		}
		return result;
	}
};

class SmallRock : public Rock {

public:
	SmallRock(int X, int Y, int xVel, int yVel, int phaseFreq);

	virtual int IsSmallRock(void) {
		return(1);
	}

	int Explode() {
		/* Don't do anything if we're already exploding */
		if ( Exploding ) {
			return(0);
		}

		++gNumSmallRocksDestroyed;

		/* Speed things up. :-) */
		if ( --gBoomDelay < BOOM_MIN )
			gBoomDelay = BOOM_MIN;
#ifdef SERIOUS_DEBUG
error("-   Small rock! (%d)\n", gNumRocks);
#endif

		return(Rock::Explode());
	}
};

class MediumRock : public Rock {

public:
	MediumRock(int X, int Y, int xVel, int yVel, int phaseFreq);

	virtual int IsMediumRock(void) {
		return(1);
	}

	int Explode() {
		int newrocks;
		int  newsprite = gNumSprites;
		
		/* Don't do anything if we're already exploding */
		if ( Exploding ) {
			return(0);
		}

		/* Create 0-3 new rocks */
		newrocks = FastRandom(3);
		do {
			int	xVel, yVel, phaseFreq, rx;

			rx = (VEL_FACTOR + (gWave / 6)) * (SCALE_FACTOR);

			xVel = yVel = 0;
			while (xVel == 0)
				xVel = FastRandom(rx) - (rx / 2);
			if (xVel > 0)
				xVel += (2 * SCALE_FACTOR);
			else
				xVel -= (2 * SCALE_FACTOR);

			while (yVel == 0)
				yVel = FastRandom(rx) - (rx / 2);
			if (yVel > 0)
				yVel += (2 * SCALE_FACTOR);
			else
				yVel -= (2 * SCALE_FACTOR);

			phaseFreq = (FastRandom(3) + 1);

			gSprites[newsprite++] =
				new SmallRock(x, y, xVel, yVel, phaseFreq);
		} while ( newrocks-- );

		/* Speed things up. :-) */
		if ( --gBoomDelay < BOOM_MIN )
			gBoomDelay = BOOM_MIN;
#ifdef SERIOUS_DEBUG
error("--  Medium rock! (%d)\n", gNumRocks);
#endif

		return(Rock::Explode());
	}
};


class LargeRock : public Rock {

public:
	LargeRock(int X, int Y, int xVel, int yVel, int phaseFreq);

	virtual int IsLargeRock(void) {
		return(1);
	}

	int Explode() {
		int newrocks;
		int  newsprite = gNumSprites;
		
		/* Don't do anything if we're already exploding */
		if ( Exploding ) {
			return(0);
		}

		/* Create 0-3 new rocks */
		newrocks = FastRandom(3);
		do {
			int	xVel, yVel, phaseFreq, rx;

			rx = (VEL_FACTOR + (gWave / 6)) * (SCALE_FACTOR);

			xVel = yVel = 0;
			while (xVel == 0)
				xVel = FastRandom(rx) - (rx / 2);
			if (xVel > 0)
				xVel += (1 * SCALE_FACTOR);
			else
				xVel -= (1 * SCALE_FACTOR);

			while (yVel == 0)
				yVel = FastRandom(rx) - (rx / 2);
			if (yVel > 0)
				yVel += (1 * SCALE_FACTOR);
			else
				yVel -= (1 * SCALE_FACTOR);

			phaseFreq = (FastRandom(3) + 2);

			gSprites[newsprite++] =
				new MediumRock(x, y, xVel, yVel, phaseFreq);
		} while ( newrocks-- );

		/* Speed things up. :-) */
		if ( --gBoomDelay < BOOM_MIN )
			gBoomDelay = BOOM_MIN;
#ifdef SERIOUS_DEBUG
error("--- Large rock! (%d)\n", gNumRocks);
#endif

		return(Rock::Explode());
	}
};


class SteelRoid : public Object {

public:
	SteelRoid(int X, int Y, int xVel, int yVel);
	~SteelRoid() { }

	int BeenShot(Object *ship, Shot *shot) {
        /* Every time we're hit, we grant points */
        if ( (HitPoints - shot->damage) > 0 ) {
            ship->IncrScore(Points);
        }
        return Object::BeenShot(ship, shot);
    }

	int BeenRunOver(Object *ship) {
        /* Every time we're hit, we grant points */
        if ( (HitPoints - 1) > 0 ) {
            ship->IncrScore(Points);
        }
        return Object::BeenRunOver(ship);
    }

	int Explode(void) {
		int newsprite;

		/* Don't do anything if we're already exploding */
		if ( Exploding ) {
			return(0);
		}

		Set_HitPoints(STEEL_SPECIAL);
		switch (FastRandom(10)) {

			/* Turn into an Asteroid */
			case 0:
				sound->PlaySound(gFunk, 4);
				newsprite = gNumSprites;
				gSprites[newsprite] = new LargeRock(x, y,
							xvec, yvec, phasetime);
				return(1);

			/* Blow up! */
			case 1:
				UnlockSinglePlayerAchievement("ACHIEVEMENT_STEEL_BALLS");
				return(Object::Explode());

			/* Turn into a homing mine */
			case 2:
				sound->PlaySound(gHomingAppears, 4);
				newsprite = gNumSprites;
				gSprites[newsprite] =
					new Homing(x, y, xvec, yvec);
				return(1);

			default:
				break;
		}
		return(0);
	}
};
