//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 "../ship.h" #include "pkunk.h" #include "resinst.h" #include "uqm/globdata.h" #include "uqm/tactrans.h" #include "libs/mathlib.h" // Core characteristics #define MAX_CREW 8 #define MAX_ENERGY 12 #define ENERGY_REGENERATION 0 #define ENERGY_WAIT 0 #define MAX_THRUST 64 #define THRUST_INCREMENT 16 #define THRUST_WAIT 0 #define TURN_WAIT 0 #define SHIP_MASS 1 // Triple Miniguns #define WEAPON_ENERGY_COST 1 #define WEAPON_WAIT 0 #define PKUNK_OFFSET 15 #define MISSILE_OFFSET 1 #define MISSILE_SPEED DISPLAY_TO_WORLD (24) #define MISSILE_LIFE 5 #define MISSILE_HITS 1 #define MISSILE_DAMAGE 1 // Taunt #define SPECIAL_ENERGY_COST 2 #define SPECIAL_WAIT 16 // Respawn #define PHOENIX_LIFE 12 #define START_PHOENIX_COLOR BUILD_COLOR (MAKE_RGB15 (0x1F, 0x15, 0x00), 0x7A) #define TRANSITION_LIFE 1 #define TRANSITION_SPEED DISPLAY_TO_WORLD (20) static RACE_DESC pkunk_desc = { { /* SHIP_INFO */ FIRES_FORE | FIRES_LEFT | FIRES_RIGHT, 20, /* Super Melee cost */ MAX_CREW, MAX_CREW, MAX_ENERGY, MAX_ENERGY, PKUNK_RACE_STRINGS, PKUNK_ICON_MASK_PMAP_ANIM, PKUNK_MICON_MASK_PMAP_ANIM, NULL, NULL, NULL }, { /* FLEET_STUFF */ 666 / SPHERE_RADIUS_INCREMENT * 2, /* Initial SoI radius */ { /* Known location (center of SoI) */ 502, 401, }, }, { MAX_THRUST, THRUST_INCREMENT, ENERGY_REGENERATION, WEAPON_ENERGY_COST, SPECIAL_ENERGY_COST, ENERGY_WAIT, TURN_WAIT, THRUST_WAIT, WEAPON_WAIT, 0, /* SPECIAL_WAIT */ SHIP_MASS, }, { { PKUNK_BIG_MASK_PMAP_ANIM, PKUNK_MED_MASK_PMAP_ANIM, PKUNK_SML_MASK_PMAP_ANIM, }, { BUG_BIG_MASK_PMAP_ANIM, BUG_MED_MASK_PMAP_ANIM, BUG_SML_MASK_PMAP_ANIM, }, { NULL_RESOURCE, NULL_RESOURCE, NULL_RESOURCE, }, { PKUNK_CAPTAIN_MASK_PMAP_ANIM, NULL, NULL, NULL, NULL, NULL }, PKUNK_VICTORY_SONG, PKUNK_SHIP_SOUNDS, { NULL, NULL, NULL }, { NULL, NULL, NULL }, { NULL, NULL, NULL }, NULL, NULL }, { 0, CLOSE_RANGE_WEAPON + 1, NULL, }, (UNINIT_FUNC *) NULL, (PREPROCESS_FUNC *) NULL, (POSTPROCESS_FUNC *) NULL, (INIT_WEAPON_FUNC *) NULL, 0, 0, /* CodeRef */ }; // Private per-instance ship data typedef struct { HELEMENT hPhoenix; ElementProcessFunc *saved_preprocess_func; ElementProcessFunc *saved_postprocess_func; ElementProcessFunc *saved_death_func; } PKUNK_DATA; // Local typedef typedef PKUNK_DATA CustomShipData_t; // Retrieve race-specific ship data from a race desc static CustomShipData_t * GetCustomShipData (RACE_DESC *pRaceDesc) { return pRaceDesc->data; } // Set the race-specific data in a race desc // (Re)Allocates its own storage for the data. static void SetCustomShipData (RACE_DESC *pRaceDesc, const CustomShipData_t *data) { if (pRaceDesc->data == data) return; // no-op if (pRaceDesc->data) // Out with the old { HFree (pRaceDesc->data); pRaceDesc->data = NULL; } if (data) // In with the new { CustomShipData_t* newData = HMalloc (sizeof (*data)); *newData = *data; pRaceDesc->data = newData; } } static void animate (ELEMENT *ElementPtr) { if (ElementPtr->turn_wait > 0) --ElementPtr->turn_wait; else { ElementPtr->next.image.frame = IncFrameIndex (ElementPtr->current.image.frame); ElementPtr->state_flags |= CHANGING; ElementPtr->turn_wait = ElementPtr->next_turn; } } static COUNT initialize_bug_missile (ELEMENT *ShipPtr, HELEMENT MissileArray[]) { COUNT i; STARSHIP *StarShipPtr; MISSILE_BLOCK MissileBlock; GetElementStarShip (ShipPtr, &StarShipPtr); MissileBlock.cx = ShipPtr->next.location.x; MissileBlock.cy = ShipPtr->next.location.y; MissileBlock.farray = StarShipPtr->RaceDescPtr->ship_data.weapon; MissileBlock.index = 0; MissileBlock.sender = ShipPtr->playerNr; MissileBlock.flags = IGNORE_SIMILAR; MissileBlock.pixoffs = PKUNK_OFFSET; MissileBlock.speed = MISSILE_SPEED; MissileBlock.hit_points = MISSILE_HITS; MissileBlock.damage = MISSILE_DAMAGE; MissileBlock.life = MISSILE_LIFE; MissileBlock.preprocess_func = NULL; MissileBlock.blast_offs = MISSILE_OFFSET; for (i = 0; i < 3; ++i) { MissileBlock.face = StarShipPtr->ShipFacing + (ANGLE_TO_FACING (QUADRANT) * i); if (i == 2) MissileBlock.face += ANGLE_TO_FACING (QUADRANT); MissileBlock.face = NORMALIZE_FACING (MissileBlock.face); if ((MissileArray[i] = initialize_missile (&MissileBlock))) { SIZE dx, dy; ELEMENT *MissilePtr; LockElement (MissileArray[i], &MissilePtr); GetCurrentVelocityComponents (&ShipPtr->velocity, &dx, &dy); DeltaVelocityComponents (&MissilePtr->velocity, dx, dy); MissilePtr->current.location.x -= VELOCITY_TO_WORLD (dx); MissilePtr->current.location.y -= VELOCITY_TO_WORLD (dy); MissilePtr->preprocess_func = animate; UnlockElement (MissileArray[i]); } } return (3); } static void pkunk_intelligence (ELEMENT *ShipPtr, EVALUATE_DESC *ObjectsOfConcern, COUNT ConcernCounter) { STARSHIP *StarShipPtr; PKUNK_DATA *PkunkData; GetElementStarShip (ShipPtr, &StarShipPtr); PkunkData = GetCustomShipData (StarShipPtr->RaceDescPtr); if (PkunkData->hPhoenix && (StarShipPtr->control & STANDARD_RATING)) { RemoveElement (PkunkData->hPhoenix); FreeElement (PkunkData->hPhoenix); PkunkData->hPhoenix = 0; } if (StarShipPtr->RaceDescPtr->ship_info.energy_level < StarShipPtr->RaceDescPtr->ship_info.max_energy && (StarShipPtr->special_counter == 0 || (BYTE)TFB_Random () < 20)) StarShipPtr->ship_input_state |= SPECIAL; else StarShipPtr->ship_input_state &= ~SPECIAL; ship_intelligence (ShipPtr, ObjectsOfConcern, ConcernCounter); } static void pkunk_preprocess (ELEMENT *ElementPtr); static void new_pkunk (ELEMENT *ElementPtr) { STARSHIP *StarShipPtr; PKUNK_DATA *PkunkData; GetElementStarShip (ElementPtr, &StarShipPtr); PkunkData = GetCustomShipData (StarShipPtr->RaceDescPtr); ElementPtr->state_flags = APPEARING | PLAYER_SHIP | IGNORE_SIMILAR; ElementPtr->mass_points = SHIP_MASS; // Restore the element processing callbacks after the explosion. // The callbacks were changed for the explosion sequence ElementPtr->preprocess_func = PkunkData->saved_preprocess_func; ElementPtr->postprocess_func = PkunkData->saved_postprocess_func; ElementPtr->death_func = PkunkData->saved_death_func; // preprocess_func() is called during the phoenix transition and // then cleared, so we need to restore it StarShipPtr->RaceDescPtr->preprocess_func = pkunk_preprocess; StarShipPtr->RaceDescPtr->ship_info.crew_level = MAX_CREW; StarShipPtr->RaceDescPtr->ship_info.energy_level = MAX_ENERGY; /* fix vux impairment */ StarShipPtr->RaceDescPtr->characteristics.max_thrust = MAX_THRUST; StarShipPtr->RaceDescPtr->characteristics.thrust_increment = THRUST_INCREMENT; StarShipPtr->RaceDescPtr->characteristics.turn_wait = TURN_WAIT; StarShipPtr->RaceDescPtr->characteristics.thrust_wait = THRUST_WAIT; StarShipPtr->RaceDescPtr->characteristics.special_wait = 0; StarShipPtr->ship_input_state = 0; // Pkunk wins in a simultaneous destruction if it reincarnates StarShipPtr->cur_status_flags &= PLAY_VICTORY_DITTY; StarShipPtr->old_status_flags = 0; StarShipPtr->energy_counter = 0; StarShipPtr->weapon_counter = 0; StarShipPtr->special_counter = 0; ElementPtr->crew_level = 0; ElementPtr->turn_wait = 0; ElementPtr->thrust_wait = 0; ElementPtr->life_span = NORMAL_LIFE; StarShipPtr->ShipFacing = NORMALIZE_FACING (TFB_Random ()); ElementPtr->current.image.farray = StarShipPtr->RaceDescPtr->ship_data.ship; ElementPtr->current.image.frame = SetAbsFrameIndex ( StarShipPtr->RaceDescPtr->ship_data.ship[0], StarShipPtr->ShipFacing); SetPrimType (&(GLOBAL (DisplayArray))[ElementPtr->PrimIndex], STAMP_PRIM); do { ElementPtr->current.location.x = WRAP_X (DISPLAY_ALIGN_X (TFB_Random ())); ElementPtr->current.location.y = WRAP_Y (DISPLAY_ALIGN_Y (TFB_Random ())); } while (CalculateGravity (ElementPtr) || TimeSpaceMatterConflict (ElementPtr)); // XXX: Hack: Set hTarget!=0 so that ship_preprocess() does not // call ship_transition() for us. ElementPtr->hTarget = StarShipPtr->hShip; } // This function is called when the ship dies but reincarnates. // The generic ship_death() function is not called for the ship in this case. static void pkunk_reincarnation_death (ELEMENT *ShipPtr) { // Simulate ship death StopAllBattleMusic (); StartShipExplosion (ShipPtr, true); // Once the explosion ends, we will get a brand new ship ShipPtr->death_func = new_pkunk; } static void intercept_pkunk_death (ELEMENT *ElementPtr) { STARSHIP *StarShipPtr; PKUNK_DATA *PkunkData; ELEMENT *ShipPtr; GetElementStarShip (ElementPtr, &StarShipPtr); PkunkData = GetCustomShipData (StarShipPtr->RaceDescPtr); if (StarShipPtr->RaceDescPtr->ship_info.crew_level != 0) { // Ship not dead yet. // Keep the Phoenix element alive. ElementPtr->state_flags &= ~DISAPPEARING; ElementPtr->life_span = 1; return; } LockElement (StarShipPtr->hShip, &ShipPtr); // GRAVITY_MASS() indicates a warp-out here. If Pkunk dies while warping // out, there is no reincarnation. if (!GRAVITY_MASS (ShipPtr->mass_points + 1)) { // XXX: Hack: Set mass_points to indicate a reincarnation to // FindAliveStarShip() ShipPtr->mass_points = MAX_SHIP_MASS + 1; // Save the various element processing callbacks before the // explosion happens, because we were not the ones who set // these callbacks and they are about to be changed. PkunkData->saved_preprocess_func = ShipPtr->preprocess_func; PkunkData->saved_postprocess_func = ShipPtr->postprocess_func; PkunkData->saved_death_func = ShipPtr->death_func; ShipPtr->death_func = pkunk_reincarnation_death; } UnlockElement (StarShipPtr->hShip); } static void spawn_phoenix_trail (ELEMENT *ElementPtr) { static const Color colorTable[] = { 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 colorTableCount = sizeof colorTable / sizeof colorTable[0]; ElementPtr->colorCycleIndex++; if (ElementPtr->colorCycleIndex != colorTableCount) { ElementPtr->life_span = TRANSITION_LIFE; SetPrimColor (&(GLOBAL (DisplayArray))[ElementPtr->PrimIndex], colorTable[ElementPtr->colorCycleIndex]); ElementPtr->state_flags &= ~DISAPPEARING; ElementPtr->state_flags |= CHANGING; } // else, the element disappears. } static void phoenix_transition (ELEMENT *ElementPtr) { 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 ())) { COUNT angle; PutElement (hShipImage); LockElement (hShipImage, &ShipImagePtr); ShipImagePtr->playerNr = NEUTRAL_PLAYER_NUM; ShipImagePtr->state_flags = APPEARING | FINITE_LIFE | NONSOLID; ShipImagePtr->life_span = TRANSITION_LIFE; SetPrimType (&(GLOBAL (DisplayArray))[ShipImagePtr->PrimIndex], STAMPFILL_PRIM); SetPrimColor ( &(GLOBAL (DisplayArray))[ShipImagePtr->PrimIndex], START_PHOENIX_COLOR); ShipImagePtr->colorCycleIndex = 0; ShipImagePtr->current.image = ElementPtr->current.image; ShipImagePtr->current.location = ElementPtr->current.location; if (!(ElementPtr->state_flags & PLAYER_SHIP)) { angle = ElementPtr->mass_points; ShipImagePtr->current.location.x += COSINE (angle, TRANSITION_SPEED); ShipImagePtr->current.location.y += SINE (angle, TRANSITION_SPEED); ElementPtr->preprocess_func = NULL; } else { angle = FACING_TO_ANGLE (StarShipPtr->ShipFacing); 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->mass_points = (BYTE)angle; ShipImagePtr->preprocess_func = phoenix_transition; ShipImagePtr->death_func = spawn_phoenix_trail; SetElementStarShip (ShipImagePtr, StarShipPtr); UnlockElement (hShipImage); } UnlockElement (StarShipPtr->hShip); } static void pkunk_preprocess (ELEMENT *ElementPtr) { STARSHIP *StarShipPtr; PKUNK_DATA *PkunkData; GetElementStarShip (ElementPtr, &StarShipPtr); PkunkData = GetCustomShipData (StarShipPtr->RaceDescPtr); if (ElementPtr->state_flags & APPEARING) { HELEMENT hPhoenix = 0; if (TFB_Random () & 1) hPhoenix = AllocElement (); // The hPhoenix element is created and placed at the head of the // queue so that it is preprocessed before any of the ships' elements // are, and so before death_func() is called for the dead Pkunk. // hPhoenix detects when the Pkunk ship dies and tweaks the ship, // starting the death + reincarnation sequence. if (hPhoenix) { ELEMENT *PhoenixPtr; LockElement (hPhoenix, &PhoenixPtr); PhoenixPtr->playerNr = ElementPtr->playerNr; PhoenixPtr->state_flags = FINITE_LIFE | NONSOLID | IGNORE_SIMILAR; PhoenixPtr->life_span = 1; PhoenixPtr->death_func = intercept_pkunk_death; SetElementStarShip (PhoenixPtr, StarShipPtr); UnlockElement (hPhoenix); InsertElement (hPhoenix, GetHeadElement ()); } PkunkData->hPhoenix = hPhoenix; // XXX: Hack: new_pkunk() sets hTarget!=0 which indicates a // reincarnation to us. if (ElementPtr->hTarget == 0) { // A brand new ship is preprocessed only once StarShipPtr->RaceDescPtr->preprocess_func = 0; } else { // Start the reincarnation sequence COUNT angle, facing; ProcessSound (SetAbsSoundIndex ( StarShipPtr->RaceDescPtr->ship_data.ship_sounds, 1 ), ElementPtr); ElementPtr->life_span = PHOENIX_LIFE; SetPrimType (&(GLOBAL (DisplayArray))[ElementPtr->PrimIndex], NO_PRIM); ElementPtr->state_flags |= NONSOLID | FINITE_LIFE | CHANGING; facing = StarShipPtr->ShipFacing; for (angle = OCTANT; angle < FULL_CIRCLE; angle += QUADRANT) { StarShipPtr->ShipFacing = NORMALIZE_FACING ( facing + ANGLE_TO_FACING (angle) ); phoenix_transition (ElementPtr); } StarShipPtr->ShipFacing = facing; } } if (StarShipPtr->RaceDescPtr->preprocess_func) { StarShipPtr->cur_status_flags &= ~(LEFT | RIGHT | THRUST | WEAPON | SPECIAL); if (ElementPtr->life_span == NORMAL_LIFE) { ElementPtr->current.image.frame = ElementPtr->next.image.frame = SetEquFrameIndex ( ElementPtr->current.image.farray[0], ElementPtr->current.image.frame); SetPrimType (&(GLOBAL (DisplayArray))[ElementPtr->PrimIndex], STAMP_PRIM); InitIntersectStartPoint (ElementPtr); InitIntersectEndPoint (ElementPtr); InitIntersectFrame (ElementPtr); ZeroVelocityComponents (&ElementPtr->velocity); ElementPtr->state_flags &= ~(NONSOLID | FINITE_LIFE); ElementPtr->state_flags |= CHANGING; StarShipPtr->RaceDescPtr->preprocess_func = 0; } } } static COUNT LastSound = 0; static void pkunk_postprocess (ELEMENT *ElementPtr) { STARSHIP *StarShipPtr; GetElementStarShip (ElementPtr, &StarShipPtr); if (StarShipPtr->RaceDescPtr->characteristics.special_wait) --StarShipPtr->RaceDescPtr->characteristics.special_wait; else if ((StarShipPtr->cur_status_flags & SPECIAL) && StarShipPtr->RaceDescPtr->ship_info.energy_level < StarShipPtr->RaceDescPtr->ship_info.max_energy) { COUNT CurSound; do { CurSound = 2 + ((COUNT)TFB_Random () % (GetSoundCount (StarShipPtr->RaceDescPtr->ship_data.ship_sounds) - 2)); } while (CurSound == LastSound); ProcessSound (SetAbsSoundIndex ( StarShipPtr->RaceDescPtr->ship_data.ship_sounds, CurSound ), ElementPtr); LastSound = CurSound; DeltaEnergy (ElementPtr, SPECIAL_ENERGY_COST); StarShipPtr->RaceDescPtr->characteristics.special_wait = SPECIAL_WAIT; } } static void uninit_pkunk (RACE_DESC *pRaceDesc) { SetCustomShipData (pRaceDesc, NULL); } RACE_DESC* init_pkunk (void) { RACE_DESC *RaceDescPtr; // The caller of this func will copy the struct static RACE_DESC new_pkunk_desc; PKUNK_DATA empty_data; memset (&empty_data, 0, sizeof (empty_data)); pkunk_desc.uninit_func = uninit_pkunk; pkunk_desc.preprocess_func = pkunk_preprocess; pkunk_desc.postprocess_func = pkunk_postprocess; pkunk_desc.init_weapon_func = initialize_bug_missile; pkunk_desc.cyborg_control.intelligence_func = pkunk_intelligence; /* copy initial ship settings to the new descriptor */ new_pkunk_desc = pkunk_desc; SetCustomShipData (&new_pkunk_desc, &empty_data); RaceDescPtr = &new_pkunk_desc; LastSound = 0; // We need to reinitialise it at least each battle, to ensure // that NetPlay is synchronised if one player played another // game before playing against a networked opponent. return (RaceDescPtr); }