//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 "lander.h" #include "lifeform.h" #include "scan.h" #include "../cons_res.h" #include "../controls.h" #include "../colors.h" #include "../process.h" #include "../units.h" #include "../gamestr.h" #include "../nameref.h" #include "../resinst.h" #include "../setup.h" #include "../sounds.h" #include "../element.h" #include "libs/graphics/gfx_common.h" #include "libs/mathlib.h" #include "libs/log.h" //define SPIN_ON_LAUNCH to let the planet spin while // the lander animation is playing #define SPIN_ON_LAUNCH // PLANET_SIDE_RATE governs how fast the lander, // bio and planet effects will be // We're using the 3DO speed, which is 35 FPS // The PC speed was 30 FPS. // Remember that all values need to evenly divide // ONE_SECOND. #define PLANET_SIDE_RATE (ONE_SECOND / 35) // This is a derived type from INPUT_STATE_DESC. typedef struct LanderInputState LanderInputState; struct LanderInputState { // Fields required by DoInput() BOOLEAN (*InputFunc) (LanderInputState *pMS); BOOLEAN Initialized; TimeCount NextTime; // Frame rate control }; FRAME LanderFrame[8]; static SOUND LanderSounds; MUSIC_REF LanderMusic; #define NUM_ORBIT_THEMES 5 static MUSIC_REF OrbitMusic[NUM_ORBIT_THEMES]; const LIFEFORM_DESC CreatureData[] = { {SPEED_MOTIONLESS | DANGER_HARMLESS, MAKE_BYTE (1, 1)}, // Roto-Dendron {SPEED_MOTIONLESS | DANGER_HARMLESS, MAKE_BYTE (6, 1)}, // Macrocillia {SPEED_MOTIONLESS | DANGER_WEAK, MAKE_BYTE (3, 1)}, // Splort Wort {SPEED_MOTIONLESS | DANGER_NORMAL, MAKE_BYTE (5, 3)}, // Whackin' Bush {SPEED_MOTIONLESS | DANGER_HARMLESS, MAKE_BYTE (2, 10)}, // Slot Machine Tree {BEHAVIOR_UNPREDICTABLE | SPEED_SLOW | DANGER_HARMLESS, MAKE_BYTE (1, 2)}, // Neon Worm {BEHAVIOR_FLEE | AWARENESS_MEDIUM | SPEED_SLOW | DANGER_HARMLESS, MAKE_BYTE (8, 5)}, // Stiletto Urchin {BEHAVIOR_HUNT | AWARENESS_LOW | SPEED_SLOW | DANGER_WEAK, MAKE_BYTE (2, 2)}, // Deluxe Blob {BEHAVIOR_UNPREDICTABLE | SPEED_SLOW | DANGER_NORMAL, MAKE_BYTE (3, 8)}, // Glowing Medusa {BEHAVIOR_HUNT | AWARENESS_MEDIUM | SPEED_SLOW | DANGER_MONSTROUS, MAKE_BYTE (10, 15)}, // Carousel Beast {BEHAVIOR_HUNT | AWARENESS_MEDIUM | SPEED_MEDIUM | DANGER_WEAK, MAKE_BYTE (3, 3)}, // Mysterious Bees {BEHAVIOR_FLEE | AWARENESS_MEDIUM | SPEED_MEDIUM | DANGER_HARMLESS, MAKE_BYTE (2, 1)}, // Hopping Blobby {BEHAVIOR_UNPREDICTABLE | SPEED_MEDIUM | DANGER_WEAK, MAKE_BYTE (2, 2)}, // Blood Monkey {BEHAVIOR_HUNT | AWARENESS_HIGH | SPEED_MEDIUM | DANGER_NORMAL, MAKE_BYTE (4, 6)}, // Yompin Yiminy {BEHAVIOR_UNPREDICTABLE | SPEED_MEDIUM | DANGER_MONSTROUS, MAKE_BYTE (9, 12)}, // Amorphous Trandicula {BEHAVIOR_HUNT | AWARENESS_HIGH | SPEED_FAST | DANGER_WEAK, MAKE_BYTE (3, 1)}, // Crazy Weasel {BEHAVIOR_FLEE | AWARENESS_HIGH | SPEED_FAST | DANGER_HARMLESS, MAKE_BYTE (1, 1)}, // Merry Whumpet {BEHAVIOR_HUNT | AWARENESS_LOW | SPEED_FAST | DANGER_NORMAL, MAKE_BYTE (7, 8)}, // Fungal Squid {BEHAVIOR_FLEE | AWARENESS_HIGH | SPEED_FAST | DANGER_WEAK, MAKE_BYTE (15, 2)}, // Penguin Cyclops {BEHAVIOR_FLEE | AWARENESS_LOW | SPEED_FAST | DANGER_WEAK, MAKE_BYTE (1, 1)}, // Chicken {BEHAVIOR_UNPREDICTABLE | SPEED_SLOW | DANGER_WEAK, MAKE_BYTE (6, 2)}, // Bubble Vine {BEHAVIOR_FLEE | AWARENESS_HIGH | SPEED_SLOW | DANGER_WEAK, MAKE_BYTE (4, 2)}, // Bug-Eyed Bait {SPEED_MOTIONLESS | DANGER_WEAK, MAKE_BYTE (8, 5)}, // Goo Burger {SPEED_MOTIONLESS | DANGER_MONSTROUS, MAKE_BYTE (1, 1)}, // Evil One {BEHAVIOR_UNPREDICTABLE | SPEED_SLOW | DANGER_HARMLESS, MAKE_BYTE (0, 1)}, // Brainbox Bulldozers {BEHAVIOR_HUNT | AWARENESS_HIGH | SPEED_FAST | DANGER_MONSTROUS, MAKE_BYTE (15, 15)}, // Zex's Beauty }; extern PRIM_LINKS DisplayLinks; #define DAMAGE_CYCLE 6 // XXX: There are actually only 9 explosion images. // The last frame is drawn twice. #define EXPLOSION_LIFE 10 // How long to wait after the lander explodes, so that the full // gravity of the player's situation sinks in #define EXPLOSION_WAIT (ONE_SECOND * 2) #define EXPLOSION_WAIT_FRAMES (EXPLOSION_WAIT / PLANET_SIDE_RATE) // The actual number of frame that the explosion and wait takes is: // EXPLOSION_LIFE * 3 + EXPLOSION_WAIT_FRAMES #define DEATH_EXPLOSION 0 // TODO: redefine these in terms of CONTEXT width/height #define SURFACE_WIDTH SIS_SCREEN_WIDTH #define SURFACE_HEIGHT (SIS_SCREEN_HEIGHT - MAP_HEIGHT - MAP_BORDER_HEIGHT) #define REPAIR_LANDER (1 << 7) #define REPAIR_TRANSITION (1 << 6) #define KILL_CREW (1 << 5) #define ADD_AT_END (1 << 4) #define REPAIR_COUNT (0xf) #define LANDER_SPEED_DENOM 10 static BYTE lander_flags; static POINT curLanderLoc; static int crew_left; static int shieldHit; // which shield was hit, assuming it helped static int damage_index; // number of lander damage frames left static int explosion_index; // lander explosion progression. Semantics are similar to an // inverse of ELEMENT.life_span static int turn_wait; // thus named for similar semantics to ELEMENT.turn_wait static int weapon_wait; // semantics similar to STARSHIP.weapon_counter // TODO: We may want to make the PLANETSIDE_DESC fields into static vars static PLANETSIDE_DESC *planetSideDesc; #define ON_THE_GROUND 0 static Color DamageColorCycle (Color c, COUNT i) { static const Color damage_tab[DAMAGE_CYCLE + 1] = { WHITE_COLOR_INIT, BUILD_COLOR (MAKE_RGB15_INIT (0x1B, 0x00, 0x00), 0x2A), BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x07, 0x00), 0x7E), BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x0E, 0x00), 0x7C), BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x15, 0x00), 0x7A), BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x1C, 0x00), 0x78), BUILD_COLOR (MAKE_RGB15_INIT (0x1F, 0x1F, 0x0A), 0x0E), }; if (i) c = damage_tab[i]; else if (sameColor(c, WHITE_COLOR)) c = damage_tab[6]; else if (sameColor(c, BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x0A), 0x0E))) c = damage_tab[5]; else if (sameColor(c, BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1C, 0x00), 0x78))) c = damage_tab[4]; else if (sameColor(c, BUILD_COLOR (MAKE_RGB15 (0x1F, 0x15, 0x00), 0x7A))) c = damage_tab[3]; else if (sameColor(c, BUILD_COLOR (MAKE_RGB15 (0x1F, 0x0E, 0x00), 0x7C))) c = damage_tab[2]; else if (sameColor(c, BUILD_COLOR (MAKE_RGB15 (0x1F, 0x07, 0x00), 0x7E))) c = damage_tab[1]; else c = damage_tab[0]; return c; } static HELEMENT AddGroundDisaster (COUNT which_disaster); void object_animation (ELEMENT *ElementPtr) { COUNT frame_index, angle; PRIMITIVE *pPrim; pPrim = &DisplayArray[ElementPtr->PrimIndex]; if (GetPrimType (pPrim) == STAMPFILL_PRIM && !((ElementPtr->state_flags & FINITE_LIFE) && ElementPtr->mass_points == EARTHQUAKE_DISASTER)) { Color c; c = DamageColorCycle (GetPrimColor (pPrim), 0); if (sameColor(c, WHITE_COLOR)) { SetPrimType (pPrim, STAMP_PRIM); if (ElementPtr->hit_points == 0) { ZeroVelocityComponents (&ElementPtr->velocity); pPrim->Object.Stamp.frame = SetAbsFrameIndex (pPrim->Object.Stamp.frame, 0); PlaySound (SetAbsSoundIndex (LanderSounds, LIFEFORM_CANNED), NotPositional (), NULL, GAME_SOUND_PRIORITY); } } SetPrimColor (pPrim, c); } frame_index = GetFrameIndex (pPrim->Object.Stamp.frame) + 1; if (LONIBBLE (ElementPtr->turn_wait)) --ElementPtr->turn_wait; else { ElementPtr->turn_wait += HINIBBLE (ElementPtr->turn_wait); pPrim->Object.Stamp.frame = IncFrameIndex (pPrim->Object.Stamp.frame); if (ElementPtr->state_flags & FINITE_LIFE) { /* A natural disaster */ if (ElementPtr->mass_points == DEATH_EXPLOSION) { // Lander explosion ++explosion_index; if (explosion_index >= EXPLOSION_LIFE) { // XXX: The last frame is drawn twice pPrim->Object.Stamp.frame = DecFrameIndex (pPrim->Object.Stamp.frame); } } else if (ElementPtr->mass_points == EARTHQUAKE_DISASTER) { SIZE s; if (frame_index >= 13) s = 0; else s = (14 - frame_index) >> 1; // XXX: Was 0x8000 the background flag on 3DO? //SetPrimColor (pPrim, BUILD_COLOR (0x8000 | MAKE_RGB15 (0x1F, 0x1F, 0x1F), s)); SetPrimColor (pPrim, BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x1F), s)); if (frame_index == 13) PlaySound (SetAbsSoundIndex (LanderSounds, EARTHQUAKE_DISASTER), NotPositional (), NULL, GAME_SOUND_PRIORITY); } if (ElementPtr->mass_points == LAVASPOT_DISASTER && frame_index == 5 && TFB_Random () % 100 < 90) { HELEMENT hLavaElement; /* Change lava-spot direction of travel */ hLavaElement = AddGroundDisaster (LAVASPOT_DISASTER); if (hLavaElement) { ELEMENT *LavaElementPtr; angle = FACING_TO_ANGLE (ElementPtr->facing); LockElement (hLavaElement, &LavaElementPtr); LavaElementPtr->next.location = ElementPtr->next.location; LavaElementPtr->next.location.x += COSINE (angle, 4); LavaElementPtr->next.location.y += SINE (angle, 4); if (LavaElementPtr->next.location.y < 0) LavaElementPtr->next.location.y = 0; else if (LavaElementPtr->next.location.y >= (MAP_HEIGHT << MAG_SHIFT)) LavaElementPtr->next.location.y = (MAP_HEIGHT << MAG_SHIFT) - 1; if (LavaElementPtr->next.location.x < 0) LavaElementPtr->next.location.x += MAP_WIDTH << MAG_SHIFT; else LavaElementPtr->next.location.x %= MAP_WIDTH << MAG_SHIFT; LavaElementPtr->facing = NORMALIZE_FACING ( ElementPtr->facing + (TFB_Random () % 3 - 1)); UnlockElement (hLavaElement); } } } else if (!(frame_index & 3) && ElementPtr->hit_points) { BYTE index; COUNT speed; index = ElementPtr->mass_points & ~CREATURE_AWARE; speed = CreatureData[index].Attributes & SPEED_MASK; if (speed) { SIZE dx, dy; COUNT old_angle; dx = curLanderLoc.x - ElementPtr->next.location.x; if (dx < 0 && dx < -(MAP_WIDTH << (MAG_SHIFT - 1))) dx += MAP_WIDTH << MAG_SHIFT; else if (dx > (MAP_WIDTH << (MAG_SHIFT - 1))) dx -= MAP_WIDTH << MAG_SHIFT; dy = curLanderLoc.y - ElementPtr->next.location.y; angle = ARCTAN (dx, dy); if (dx < 0) dx = -dx; if (dy < 0) dy = -dy; if (dx >= SURFACE_WIDTH || dy >= SURFACE_WIDTH || dx * dx + dy * dy >= SURFACE_WIDTH * SURFACE_WIDTH) ElementPtr->mass_points &= ~CREATURE_AWARE; else if (!(ElementPtr->mass_points & CREATURE_AWARE)) { BYTE DetectPercent; DetectPercent = (((BYTE)(CreatureData[index].Attributes & AWARENESS_MASK) >> AWARENESS_SHIFT) + 1) * (30 / 6); // XXX: Shouldn't this be dependent on // PLANET_SIDE_RATE somehow? And why is it // written as '30 / 6' instead of 5? Does the 30 // specify the (PC) framerate? That doesn't make // sense; I would expect it to be in the // denominator. And even then, it wouldn't give // the same results with different frame rates, // as repeating 'random(x / 30)' 30 times doesn't // generally have the same result as repeating // 'random(x / 35)' 25 times. - SvdB if (TFB_Random () % 100 < DetectPercent) { ElementPtr->thrust_wait = 0; ElementPtr->mass_points |= CREATURE_AWARE; } } if (ElementPtr->next.location.y == 0 || ElementPtr->next.location.y == (MAP_HEIGHT << MAG_SHIFT) - 1) ElementPtr->thrust_wait = 0; old_angle = GetVelocityTravelAngle (&ElementPtr->velocity); if (ElementPtr->thrust_wait) { --ElementPtr->thrust_wait; angle = old_angle; } else if (!(ElementPtr->mass_points & CREATURE_AWARE) || (CreatureData[index].Attributes & BEHAVIOR_MASK) == BEHAVIOR_UNPREDICTABLE) { COUNT rand_val; rand_val = TFB_Random (); angle = NORMALIZE_ANGLE (LOBYTE (rand_val)); ElementPtr->thrust_wait = (HIBYTE (rand_val) >> 2) + 10; } else if ((CreatureData[index].Attributes & BEHAVIOR_MASK) == BEHAVIOR_FLEE) { if (ElementPtr->next.location.y == 0 || ElementPtr->next.location.y == (MAP_HEIGHT << MAG_SHIFT) - 1) { if (angle & (HALF_CIRCLE - 1)) angle = HALF_CIRCLE - angle; else if (old_angle == QUADRANT || old_angle == (FULL_CIRCLE - QUADRANT)) angle = old_angle; else angle = ((TFB_Random () & 1) * HALF_CIRCLE) - QUADRANT; ElementPtr->thrust_wait = 5; } angle = NORMALIZE_ANGLE (angle + HALF_CIRCLE); } switch (speed) { case SPEED_SLOW: speed = WORLD_TO_VELOCITY (2 * 1) >> 2; break; case SPEED_MEDIUM: speed = WORLD_TO_VELOCITY (2 * 1) >> 1; break; case SPEED_FAST: speed = WORLD_TO_VELOCITY (2 * 1) * 9 / 10; break; } SetVelocityComponents (&ElementPtr->velocity, COSINE (angle, speed), SINE (angle, speed)); } } } if ((ElementPtr->state_flags & FINITE_LIFE) && ElementPtr->mass_points == DEATH_EXPLOSION && GetSuccLink (DisplayLinks) != ElementPtr->PrimIndex) lander_flags |= ADD_AT_END; } #define NUM_CREW_COLS 6 #define NUM_CREW_ROWS 2 static void DeltaLanderCrew (SIZE crew_delta, COUNT which_disaster) { STAMP s; CONTEXT OldContext; if (crew_delta > 0) { // Filling up the crew bar when landing. crew_delta = crew_left; crew_left += 1; s.frame = SetAbsFrameIndex (LanderFrame[0], 55); } else /* if (crew_delta < 0) */ { if (crew_left < 1) return; // irrelevant -- all dead shieldHit = GET_GAME_STATE (LANDER_SHIELDS); shieldHit &= 1 << which_disaster; if (!shieldHit || TFB_Random () % 100 >= 95) { // No shield, or it did not help shieldHit = 0; --crew_left; } damage_index = DAMAGE_CYCLE; if (shieldHit) return; crew_delta = crew_left; s.frame = SetAbsFrameIndex (LanderFrame[0], 56); PlaySound (SetAbsSoundIndex (LanderSounds, LANDER_INJURED), NotPositional (), NULL, GAME_SOUND_PRIORITY); } s.origin.x = 11 + (6 * (crew_delta % NUM_CREW_COLS)); s.origin.y = 35 - (6 * (crew_delta / NUM_CREW_COLS)); OldContext = SetContext (RadarContext); DrawStamp (&s); SetContext (OldContext); } static void FillLanderHold (PLANETSIDE_DESC *pPSD, COUNT scan, COUNT NumRetrieved) { COUNT start_count; STAMP s; CONTEXT OldContext; PlaySound (SetAbsSoundIndex (LanderSounds, LANDER_PICKUP), NotPositional (), NULL, GAME_SOUND_PRIORITY); if (scan == BIOLOGICAL_SCAN) { start_count = pPSD->BiologicalLevel; s.frame = SetAbsFrameIndex (LanderFrame[0], 41); pPSD->BiologicalLevel += NumRetrieved; } else { start_count = pPSD->ElementLevel; pPSD->ElementLevel += NumRetrieved; if (GET_GAME_STATE (IMPROVED_LANDER_CARGO)) { start_count >>= 1; NumRetrieved = (pPSD->ElementLevel >> 1) - start_count; } s.frame = SetAbsFrameIndex (LanderFrame[0], 43); } s.origin.x = 0; s.origin.y = -(int)start_count; if (!(start_count & 1)) s.frame = IncFrameIndex (s.frame); OldContext = SetContext (RadarContext); while (NumRetrieved--) { if (start_count++ & 1) s.frame = IncFrameIndex (s.frame); else s.frame = DecFrameIndex (s.frame); DrawStamp (&s); --s.origin.y; } SetContext (OldContext); } // returns true iff the node was picked up. static bool pickupMineralNode (PLANETSIDE_DESC *pPSD, COUNT NumRetrieved, ELEMENT *ElementPtr, const INTERSECT_CONTROL *LanderControl, const INTERSECT_CONTROL *ElementControl) { BYTE EType; UNICODE ch; UNICODE *pStr; if (pPSD->ElementLevel >= pPSD->MaxElementLevel) { // Lander full PlaySound (SetAbsSoundIndex (LanderSounds, LANDER_FULL), NotPositional (), NULL, GAME_SOUND_PRIORITY); return false; } if (pPSD->ElementLevel + NumRetrieved > pPSD->MaxElementLevel) { // Deposit could only be picked up partially. NumRetrieved = (COUNT)(pPSD->MaxElementLevel - pPSD->ElementLevel); } FillLanderHold (pPSD, MINERAL_SCAN, NumRetrieved); EType = ElementPtr->turn_wait; pPSD->ElementAmounts[ElementCategory (EType)] += NumRetrieved; pPSD->NumFrames = NUM_TEXT_FRAMES; sprintf (pPSD->AmountBuf, "%u", NumRetrieved); pStr = GAME_STRING (EType + ELEMENTS_STRING_BASE); pPSD->MineralText[0].baseline.x = (SURFACE_WIDTH >> 1) + (ElementControl->EndPoint.x - LanderControl->EndPoint.x); pPSD->MineralText[0].baseline.y = (SURFACE_HEIGHT >> 1) + (ElementControl->EndPoint.y - LanderControl->EndPoint.y); pPSD->MineralText[0].CharCount = (COUNT)~0; pPSD->MineralText[1].pStr = pStr; while ((ch = *pStr++) && ch != ' ') ; if (ch == '\0') { pPSD->MineralText[1].CharCount = (COUNT)~0; pPSD->MineralText[2].CharCount = 0; } else /* ch == ' ' */ { // Name contains a space. Print over // two lines. pPSD->MineralText[1].CharCount = utf8StringCountN( pPSD->MineralText[1].pStr, pStr - 1); pPSD->MineralText[2].pStr = pStr; pPSD->MineralText[2].CharCount = (COUNT)~0; } return true; } static bool pickupBioNode (PLANETSIDE_DESC *pPSD, COUNT NumRetrieved) { if (pPSD->BiologicalLevel >= MAX_SCROUNGED) { // Lander is full. PlaySound (SetAbsSoundIndex (LanderSounds, LANDER_FULL), NotPositional (), NULL, GAME_SOUND_PRIORITY); return false; } if (pPSD->BiologicalLevel + NumRetrieved > MAX_SCROUNGED) { // Node could only be picked up partially. NumRetrieved = (COUNT)(MAX_SCROUNGED - pPSD->BiologicalLevel); } FillLanderHold (pPSD, BIOLOGICAL_SCAN, NumRetrieved); return true; } static void shotCreature (ELEMENT *ElementPtr, BYTE value, INTERSECT_CONTROL *LanderControl, PRIMITIVE *pPrim) { if (ElementPtr->hit_points == 0) { // Creature is already canned. return; } --ElementPtr->hit_points; if (ElementPtr->hit_points == 0) { // Can creature. ElementPtr->mass_points = value; DisplayArray[ElementPtr->PrimIndex].Object.Stamp.frame = pSolarSysState->PlanetSideFrame[0]; } else if (CreatureData[ElementPtr->mass_points & ~CREATURE_AWARE] .Attributes & SPEED_MASK) { COUNT angle; angle = FACING_TO_ANGLE (GetFrameIndex ( LanderControl->IntersectStamp.frame) - ANGLE_TO_FACING (FULL_CIRCLE)); DeltaVelocityComponents (&ElementPtr->velocity, COSINE (angle, WORLD_TO_VELOCITY (1)), SINE (angle, WORLD_TO_VELOCITY (1))); ElementPtr->thrust_wait = 0; ElementPtr->mass_points |= CREATURE_AWARE; } SetPrimType (pPrim, STAMPFILL_PRIM); SetPrimColor (pPrim, WHITE_COLOR); PlaySound (SetAbsSoundIndex (LanderSounds, LANDER_HITS), NotPositional (), NULL, GAME_SOUND_PRIORITY); } static void CheckObjectCollision (COUNT index) { INTERSECT_CONTROL LanderControl; DRAWABLE LanderHandle; PRIMITIVE *pPrim; PRIMITIVE *pLanderPrim; PLANETSIDE_DESC *pPSD = planetSideDesc; if (index != END_OF_LIST) { pLanderPrim = &DisplayArray[index]; LanderControl.IntersectStamp = pLanderPrim->Object.Stamp; index = GetPredLink (GetPrimLinks (pLanderPrim)); } else { pLanderPrim = 0; LanderControl.IntersectStamp.origin.x = SURFACE_WIDTH >> 1; LanderControl.IntersectStamp.origin.y = SURFACE_HEIGHT >> 1; LanderControl.IntersectStamp.frame = LanderFrame[0]; index = GetSuccLink (DisplayLinks); } LanderControl.EndPoint = LanderControl.IntersectStamp.origin; LanderHandle = GetFrameParentDrawable (LanderControl.IntersectStamp.frame); for (; index != END_OF_LIST; index = GetPredLink (GetPrimLinks (pPrim))) { INTERSECT_CONTROL ElementControl; HELEMENT hElement, hNextElement; pPrim = &DisplayArray[index]; ElementControl.IntersectStamp = pPrim->Object.Stamp; ElementControl.EndPoint = ElementControl.IntersectStamp.origin; if (GetFrameParentDrawable (ElementControl.IntersectStamp.frame) == LanderHandle) { CheckObjectCollision (index); continue; } if (!DrawablesIntersect (&LanderControl, &ElementControl, MAX_TIME_VALUE)) continue; for (hElement = GetHeadElement (); hElement; hElement = hNextElement) { ELEMENT *ElementPtr; LockElement (hElement, &ElementPtr); hNextElement = GetSuccElement (ElementPtr); if (&DisplayArray[ElementPtr->PrimIndex] == pLanderPrim) { ElementPtr->state_flags |= DISAPPEARING; UnlockElement (hElement); continue; } if (&DisplayArray[ElementPtr->PrimIndex] != pPrim || ElementPtr->playerNr != PS_NON_PLAYER) { UnlockElement (hElement); continue; } { COUNT scan, NumRetrieved; SIZE which_node; scan = LOBYTE (ElementPtr->scan_node); if (pLanderPrim == 0) { /* Collision of lander with another object */ if (crew_left == 0 || pPSD->InTransit) break; if (ElementPtr->state_flags & FINITE_LIFE) { /* A natural disaster */ scan = ElementPtr->mass_points; switch (scan) { case EARTHQUAKE_DISASTER: case LAVASPOT_DISASTER: if (TFB_Random () % 100 < 25) DeltaLanderCrew (-1, scan); break; } UnlockElement (hElement); continue; } else if (scan == ENERGY_SCAN) { // noop; handled by generation funcs, see below } else if (scan == BIOLOGICAL_SCAN && ElementPtr->hit_points) { BYTE danger_vals[] = { 0, 6, 13, 26 }; int creatureIndex = ElementPtr->mass_points & ~CREATURE_AWARE; int dangerLevel = (CreatureData[creatureIndex].Attributes & DANGER_MASK) >> DANGER_SHIFT; if (TFB_Random () % 128 < danger_vals[dangerLevel]) { PlaySound (SetAbsSoundIndex ( LanderSounds, BIOLOGICAL_DISASTER), NotPositional (), NULL, GAME_SOUND_PRIORITY); DeltaLanderCrew (-1, BIOLOGICAL_DISASTER); } UnlockElement (hElement); continue; } NumRetrieved = ElementPtr->mass_points; } else if (ElementPtr->state_flags & FINITE_LIFE) { /* Collision of a stun bolt with a natural disaster */ UnlockElement (hElement); continue; } else { BYTE value; if (scan == ENERGY_SCAN) { /* Collision of a stun bolt with an energy node */ UnlockElement (hElement); break; } if (scan == BIOLOGICAL_SCAN && (value = LONIBBLE (CreatureData[ ElementPtr->mass_points & ~CREATURE_AWARE ].ValueAndHitPoints))) { /* Collision of a stun bolt with a viable creature */ shotCreature (ElementPtr, value, &LanderControl, pPrim); UnlockElement (hElement); break; } NumRetrieved = 0; } if (NumRetrieved) { switch (scan) { case ENERGY_SCAN: break; case MINERAL_SCAN: if (!pickupMineralNode (pPSD, NumRetrieved, ElementPtr, &LanderControl, &ElementControl)) continue; break; case BIOLOGICAL_SCAN: if (!pickupBioNode (pPSD, NumRetrieved)) continue; break; } } which_node = HIBYTE (ElementPtr->scan_node) - 1; if (callPickupForScanType (pSolarSysState, pSolarSysState->pOrbitalDesc, which_node, scan)) { // Node retrieved, remove from the surface setNodeRetrieved (&pSolarSysState->SysInfo.PlanetInfo, scan, which_node); SET_GAME_STATE (PLANETARY_CHANGE, 1); ElementPtr->state_flags |= DISAPPEARING; } UnlockElement (hElement); } } } } static void lightning_process (ELEMENT *ElementPtr) { PRIMITIVE *pPrim; pPrim = &DisplayArray[ElementPtr->PrimIndex]; if (LONIBBLE (ElementPtr->turn_wait)) --ElementPtr->turn_wait; else { COUNT num_frames; num_frames = GetFrameCount (pPrim->Object.Stamp.frame) - 7; if (GetFrameIndex (pPrim->Object.Stamp.frame) >= num_frames) { /* Advance to the next surface strike effect frame */ // XXX: This is unused, we never get here pPrim->Object.Stamp.frame = IncFrameIndex (pPrim->Object.Stamp.frame); } else { SIZE s; // XXX: Color cycling is largely unused, because the color // never actually changes RGB values (see MAKE_RGB15 below). // This did, however, work in DOS SC2 version (fade effect). s = 7 - ((SIZE)ElementPtr->cycle - (SIZE)ElementPtr->life_span); if (s < 0) s = 0; // XXX: Was 0x8000 the background flag on 3DO? //SetPrimColor (pPrim, BUILD_COLOR (0x8000 | MAKE_RGB15 (0x1F, 0x1F, 0x1F), s)); SetPrimColor (pPrim, BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x1F), s)); if (ElementPtr->mass_points == LIGHTNING_DISASTER) { /* This one always strikes the lander and can hurt */ if (crew_left && TFB_Random () % 100 < 10 && !planetSideDesc->InTransit) lander_flags |= KILL_CREW; ElementPtr->next.location = curLanderLoc; } pPrim->Object.Stamp.frame = SetAbsFrameIndex (pPrim->Object.Stamp.frame, TFB_Random () % num_frames); } ElementPtr->turn_wait += HINIBBLE (ElementPtr->turn_wait); } if (GetSuccLink (DisplayLinks) != ElementPtr->PrimIndex) lander_flags |= ADD_AT_END; } static void AddLightning (void) { HELEMENT hLightningElement; hLightningElement = AllocElement (); if (hLightningElement) { DWORD rand_val; ELEMENT *LightningElementPtr; LockElement (hLightningElement, &LightningElementPtr); LightningElementPtr->playerNr = PS_NON_PLAYER; LightningElementPtr->state_flags = FINITE_LIFE; LightningElementPtr->preprocess_func = lightning_process; if (TFB_Random () % 100 >= 25) LightningElementPtr->mass_points = 0; /* harmless */ else LightningElementPtr->mass_points = LIGHTNING_DISASTER; rand_val = TFB_Random (); LightningElementPtr->life_span = 10 + (HIWORD (rand_val) % 10) + 1; LightningElementPtr->next.location.x = (curLanderLoc.x + ((MAP_WIDTH << MAG_SHIFT) - ((SURFACE_WIDTH >> 1) - 6)) + (LOBYTE (rand_val) % (SURFACE_WIDTH - 12)) ) % (MAP_WIDTH << MAG_SHIFT); LightningElementPtr->next.location.y = (curLanderLoc.y + ((MAP_HEIGHT << MAG_SHIFT) - ((SURFACE_HEIGHT >> 1) - 6)) + (HIBYTE (rand_val) % (SURFACE_HEIGHT - 12)) ) % (MAP_HEIGHT << MAG_SHIFT); LightningElementPtr->cycle = LightningElementPtr->life_span; SetPrimType (&DisplayArray[LightningElementPtr->PrimIndex], STAMPFILL_PRIM); SetPrimColor (&DisplayArray[LightningElementPtr->PrimIndex], WHITE_COLOR); DisplayArray[LightningElementPtr->PrimIndex].Object.Stamp.frame = LanderFrame[2]; UnlockElement (hLightningElement); PutElement (hLightningElement); PlaySound (SetAbsSoundIndex (LanderSounds, LIGHTNING_DISASTER), NotPositional (), NULL, GAME_SOUND_PRIORITY); } } static HELEMENT AddGroundDisaster (COUNT which_disaster) { HELEMENT hGroundDisasterElement; hGroundDisasterElement = AllocElement (); if (hGroundDisasterElement) { DWORD rand_val; ELEMENT *GroundDisasterElementPtr; PRIMITIVE *pPrim; LockElement (hGroundDisasterElement, &GroundDisasterElementPtr); pPrim = &DisplayArray[GroundDisasterElementPtr->PrimIndex]; GroundDisasterElementPtr->mass_points = which_disaster; GroundDisasterElementPtr->playerNr = PS_NON_PLAYER; GroundDisasterElementPtr->state_flags = FINITE_LIFE; GroundDisasterElementPtr->preprocess_func = object_animation; rand_val = TFB_Random (); GroundDisasterElementPtr->next.location.x = (curLanderLoc.x + ((MAP_WIDTH << MAG_SHIFT) - (SURFACE_WIDTH * 3 / 8)) + (LOWORD (rand_val) % (SURFACE_WIDTH * 3 / 4)) ) % (MAP_WIDTH << MAG_SHIFT); GroundDisasterElementPtr->next.location.y = (curLanderLoc.y + ((MAP_HEIGHT << MAG_SHIFT) - (SURFACE_HEIGHT * 3 / 8)) + (HIWORD (rand_val) % (SURFACE_HEIGHT * 3 / 4)) ) % (MAP_HEIGHT << MAG_SHIFT); if (which_disaster == EARTHQUAKE_DISASTER) { SetPrimType (pPrim, STAMPFILL_PRIM); pPrim->Object.Stamp.frame = LanderFrame[1]; GroundDisasterElementPtr->turn_wait = MAKE_BYTE (2, 2); } else /* if (which_disaster == LAVASPOT_DISASTER) */ { SetPrimType (pPrim, STAMP_PRIM); GroundDisasterElementPtr->facing = NORMALIZE_FACING (TFB_Random ()); pPrim->Object.Stamp.frame = LanderFrame[3]; GroundDisasterElementPtr->turn_wait = MAKE_BYTE (0, 0); } GroundDisasterElementPtr->life_span = GetFrameCount (pPrim->Object.Stamp.frame) * (LONIBBLE (GroundDisasterElementPtr->turn_wait) + 1) - 1; UnlockElement (hGroundDisasterElement); PutElement (hGroundDisasterElement); } return (hGroundDisasterElement); } // This function replaces the ELEMENT manipulations typically done by // PreProcess() and PostProcess() in process.c. Lander code does not // call RedrawQueue() & Co and thus does not reap the benefits (or curses, // depending how you look at it) of automatic flags processing. static void BuildObjectList (void) { DWORD rand_val; POINT org; HELEMENT hElement, hNextElement; PLANETSIDE_DESC *pPSD = planetSideDesc; DisplayLinks = MakeLinks (END_OF_LIST, END_OF_LIST); lander_flags &= ~KILL_CREW; rand_val = TFB_Random (); if (LOBYTE (HIWORD (rand_val)) < pPSD->FireChance) { AddGroundDisaster (LAVASPOT_DISASTER); PlaySound (SetAbsSoundIndex (LanderSounds, LAVASPOT_DISASTER), NotPositional (), NULL, GAME_SOUND_PRIORITY); } if (HIBYTE (LOWORD (rand_val)) < pPSD->TectonicsChance) AddGroundDisaster (EARTHQUAKE_DISASTER); if (LOBYTE (LOWORD (rand_val)) < pPSD->WeatherChance) AddLightning (); org = curLanderLoc; for (hElement = GetHeadElement (); hElement; hElement = hNextElement) { SIZE dx, dy; ELEMENT *ElementPtr; LockElement (hElement, &ElementPtr); if (ElementPtr->life_span == 0 || (ElementPtr->state_flags & DISAPPEARING)) { hNextElement = GetSuccElement (ElementPtr); UnlockElement (hElement); RemoveElement (hElement); FreeElement (hElement); continue; } else if (ElementPtr->state_flags & FINITE_LIFE) --ElementPtr->life_span; lander_flags &= ~ADD_AT_END; if (ElementPtr->preprocess_func) (*ElementPtr->preprocess_func) (ElementPtr); GetNextVelocityComponents (&ElementPtr->velocity, &dx, &dy, 1); if (dx || dy) { ElementPtr->next.location.x += dx; ElementPtr->next.location.y += dy; /* if not lander's shot */ if (ElementPtr->playerNr != PS_HUMAN_PLAYER) { if (ElementPtr->next.location.y < 0) ElementPtr->next.location.y = 0; else if (ElementPtr->next.location.y >= (MAP_HEIGHT << MAG_SHIFT)) ElementPtr->next.location.y = (MAP_HEIGHT << MAG_SHIFT) - 1; } if (ElementPtr->next.location.x < 0) ElementPtr->next.location.x += MAP_WIDTH << MAG_SHIFT; else ElementPtr->next.location.x %= MAP_WIDTH << MAG_SHIFT; // XXX: APPEARING flag is set by scan.c for scanned blips if (ElementPtr->state_flags & APPEARING) { // Update the location of a moving object on the scan map ElementPtr->current.location.x = ElementPtr->next.location.x >> MAG_SHIFT; ElementPtr->current.location.y = ElementPtr->next.location.y >> MAG_SHIFT; } } { PRIMITIVE *pPrim; pPrim = &DisplayArray[ElementPtr->PrimIndex]; pPrim->Object.Stamp.origin.x = ElementPtr->next.location.x - org.x + (SURFACE_WIDTH >> 1); if (pPrim->Object.Stamp.origin.x >= (MAP_WIDTH << MAG_SHIFT) - (SURFACE_WIDTH * 3 / 2)) pPrim->Object.Stamp.origin.x -= MAP_WIDTH << MAG_SHIFT; else if (pPrim->Object.Stamp.origin.x <= -((MAP_WIDTH << MAG_SHIFT) - (SURFACE_WIDTH * 3 / 2))) pPrim->Object.Stamp.origin.x += MAP_WIDTH << MAG_SHIFT; pPrim->Object.Stamp.origin.y = ElementPtr->next.location.y - org.y + (SURFACE_HEIGHT >> 1); if (lander_flags & ADD_AT_END) InsertPrim (&DisplayLinks, ElementPtr->PrimIndex, END_OF_LIST); else InsertPrim (&DisplayLinks, ElementPtr->PrimIndex, GetPredLink (DisplayLinks)); } hNextElement = GetSuccElement (ElementPtr); UnlockElement (hElement); } } static void ScrollPlanetSide (SIZE dx, SIZE dy, int landingOffset) { POINT new_pt; STAMP lander_s, shadow_s, shield_s; CONTEXT OldContext; new_pt.y = curLanderLoc.y + dy; if (new_pt.y < 0) { new_pt.y = 0; dy = new_pt.y - curLanderLoc.y; dx = 0; ZeroVelocityComponents (&GLOBAL (velocity)); } else if (new_pt.y > (MAP_HEIGHT << MAG_SHIFT) - 1) { new_pt.y = (MAP_HEIGHT << MAG_SHIFT) - 1; dy = new_pt.y - curLanderLoc.y; dx = 0; ZeroVelocityComponents (&GLOBAL (velocity)); } new_pt.x = curLanderLoc.x + dx; if (new_pt.x < 0) new_pt.x += MAP_WIDTH << MAG_SHIFT; else if (new_pt.x >= MAP_WIDTH << MAG_SHIFT) new_pt.x -= MAP_WIDTH << MAG_SHIFT; curLanderLoc = new_pt; OldContext = SetContext (PlanetContext); BatchGraphics (); // Display planet area, accounting for horizontal wrapping if // near the edges. { STAMP s; ClearDrawable (); s.origin.x = -new_pt.x + (SURFACE_WIDTH >> 1); s.origin.y = -new_pt.y + (SURFACE_HEIGHT >> 1); s.frame = pSolarSysState->Orbit.TopoZoomFrame; DrawStamp (&s); s.origin.x += MAP_WIDTH << MAG_SHIFT; DrawStamp (&s); s.origin.x -= MAP_WIDTH << (MAG_SHIFT + 1); DrawStamp (&s); } BuildObjectList (); DrawBatch (DisplayArray, DisplayLinks, 0); // Draw the lander while is still alive and keep drawing for a few // frames while it is exploding if (crew_left || damage_index || explosion_index < 3) { lander_s.origin.x = SURFACE_WIDTH >> 1; lander_s.origin.y = (SURFACE_HEIGHT >> 1) + landingOffset; lander_s.frame = LanderFrame[0]; if (landingOffset != ON_THE_GROUND) { // Landing, draw a shadow shadow_s.origin.x = lander_s.origin.y + (SURFACE_WIDTH >> 1) - (SURFACE_HEIGHT >> 1);//2; shadow_s.origin.y = lander_s.origin.y; shadow_s.frame = lander_s.frame; SetContextForeGroundColor (BLACK_COLOR); DrawFilledStamp (&shadow_s); } if (damage_index == 0) { // No damage -- normal lander DrawStamp (&lander_s); } else if (shieldHit) { // Was protected by a shield --damage_index; if (damage_index > 0) { shield_s.origin = lander_s.origin; shield_s.frame = SetEquFrameIndex ( LanderFrame[4], lander_s.frame); // XXX: Shouldn't this color-cycle with damage_index? // damage_index is used, but only as a VGA index! /*SetContextForeGroundColor (BUILD_COLOR ( MAKE_RGB15 (0x1F, 0x1F, 0x1F) | 0x8000, damage_index));*/ SetContextForeGroundColor ( BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x1F), damage_index)); DrawFilledStamp (&shield_s); } DrawStamp (&lander_s); } else { // Direct hit, no shield --damage_index; SetContextForeGroundColor ( DamageColorCycle (BLACK_COLOR, damage_index)); DrawFilledStamp (&lander_s); } } if (landingOffset == ON_THE_GROUND && crew_left && GetPredLink (DisplayLinks) != END_OF_LIST) CheckObjectCollision (END_OF_LIST); { PLANETSIDE_DESC *pPSD = planetSideDesc; if (pPSD->NumFrames) { --pPSD->NumFrames; SetContextForeGroundColor (pPSD->ColorCycle[pPSD->NumFrames >> 1]); pPSD->MineralText[0].baseline.x -= dx; pPSD->MineralText[0].baseline.y -= dy; font_DrawText (&pPSD->MineralText[0]); pPSD->MineralText[1].baseline.x = pPSD->MineralText[0].baseline.x; pPSD->MineralText[1].baseline.y = pPSD->MineralText[0].baseline.y + 7; font_DrawText (&pPSD->MineralText[1]); pPSD->MineralText[2].baseline.x = pPSD->MineralText[1].baseline.x; pPSD->MineralText[2].baseline.y = pPSD->MineralText[1].baseline.y + 7; font_DrawText (&pPSD->MineralText[2]); } } RedrawSurfaceScan (&new_pt); if (lander_flags & KILL_CREW) DeltaLanderCrew (-1, LIGHTNING_DISASTER); UnbatchGraphics (); SetContext (OldContext); } static void animationInterframe (TimeCount *TimeIn, COUNT periods) { #define ANIM_FRAME_RATE (ONE_SECOND / 30) for ( ; periods; --periods) { RotatePlanetSphere (TRUE); SleepThreadUntil (*TimeIn + ANIM_FRAME_RATE); *TimeIn = GetTimeCounter (); } } static void AnimateLaunch (FRAME farray) { RECT r; STAMP s; COUNT num_frames; TimeCount NextTime; SetContext (PlanetContext); r.corner.x = 0; r.corner.y = 0; r.extent.width = 0; r.extent.height = 0; s.origin.x = 0; s.origin.y = 0; s.frame = farray; for (num_frames = GetFrameCount (s.frame); num_frames; --num_frames) { NextTime = GetTimeCounter () + (ONE_SECOND / 22); BatchGraphics (); RepairBackRect (&r); #ifdef SPIN_ON_LAUNCH RotatePlanetSphere (FALSE); #else DrawDefaultPlanetSphere (); #endif DrawStamp (&s); UnbatchGraphics (); GetFrameRect (s.frame, &r); s.frame = IncFrameIndex (s.frame); SleepThreadUntil (NextTime); } RepairBackRect (&r); } static void AnimateLanderWarmup (void) { SIZE num_crew; STAMP s; CONTEXT OldContext; TimeCount TimeIn = GetTimeCounter (); OldContext = SetContext (RadarContext); s.origin.x = 0; s.origin.y = 0; s.frame = SetAbsFrameIndex (LanderFrame[0], (ANGLE_TO_FACING (FULL_CIRCLE) << 1) + 1); DrawStamp (&s); animationInterframe (&TimeIn, 2); for (num_crew = 0; num_crew < (NUM_CREW_COLS * NUM_CREW_ROWS) && GLOBAL_SIS (CrewEnlisted); ++num_crew) { animationInterframe (&TimeIn, 1); DeltaSISGauges (-1, 0, 0); DeltaLanderCrew (1, 0); } animationInterframe (&TimeIn, 2); if (GET_GAME_STATE (IMPROVED_LANDER_SHOT)) s.frame = SetAbsFrameIndex (s.frame, 58); else s.frame = SetAbsFrameIndex (s.frame, (ANGLE_TO_FACING (FULL_CIRCLE) << 1) + 2); DrawStamp (&s); animationInterframe (&TimeIn, 2); if (GET_GAME_STATE (IMPROVED_LANDER_SPEED)) s.frame = SetAbsFrameIndex (s.frame, 57); else { s.frame = SetAbsFrameIndex (s.frame, (ANGLE_TO_FACING (FULL_CIRCLE) << 1) + 3); DrawStamp (&s); animationInterframe (&TimeIn, 2); s.frame = IncFrameIndex (s.frame); } DrawStamp (&s); if (GET_GAME_STATE (IMPROVED_LANDER_CARGO)) { animationInterframe (&TimeIn, 2); s.frame = SetAbsFrameIndex (s.frame, 59); DrawStamp (&s); } animationInterframe (&TimeIn, 2); PlaySound (SetAbsSoundIndex (LanderSounds, LANDER_DEPARTS), NotPositional (), NULL, GAME_SOUND_PRIORITY + 1); } static void InitPlanetSide (POINT pt) { // Adjust landing location by a random jitter. #define RANDOM_MISS 64 // Jitter the X landing point. pt.x -= RANDOM_MISS - TFB_Random () % (RANDOM_MISS << 1); if (pt.x < 0) pt.x += (MAP_WIDTH << MAG_SHIFT); else if (pt.x >= (MAP_WIDTH << MAG_SHIFT)) pt.x -= (MAP_WIDTH << MAG_SHIFT); // Jitter the Y landing point. pt.y -= RANDOM_MISS - TFB_Random () % (RANDOM_MISS << 1); if (pt.y < 0) pt.y = 0; else if (pt.y >= (MAP_HEIGHT << MAG_SHIFT)) pt.y = (MAP_HEIGHT << MAG_SHIFT) - 1; curLanderLoc = pt; SetContext (PlanetContext); SetContextFont (TinyFont); { RECT r; GetContextClipRect (&r); SetTransitionSource (&r); BatchGraphics (); { STAMP s; // Note - This code is the same as in ScrollPlanetSize, // Display planet area, accounting for horizontal wrapping if // near the edges. ClearDrawable (); s.origin.x = -pt.x + (SURFACE_WIDTH >> 1); s.origin.y = -pt.y + (SURFACE_HEIGHT >> 1); s.frame = pSolarSysState->Orbit.TopoZoomFrame; DrawStamp (&s); s.origin.x += MAP_WIDTH << MAG_SHIFT; DrawStamp (&s); s.origin.x -= MAP_WIDTH << (MAG_SHIFT + 1); DrawStamp (&s); } ScreenTransition (3, &r); UnbatchGraphics (); } SET_GAME_STATE (PLANETARY_LANDING, 1); } static void LanderFire (SIZE facing) { #define SHUTTLE_FIRE_WAIT 15 HELEMENT hWeaponElement; SIZE wdx, wdy; ELEMENT *WeaponElementPtr; COUNT angle; hWeaponElement = AllocElement (); if (hWeaponElement == NULL) return; LockElement (hWeaponElement, &WeaponElementPtr); WeaponElementPtr->playerNr = PS_HUMAN_PLAYER; WeaponElementPtr->mass_points = 1; WeaponElementPtr->life_span = 12; WeaponElementPtr->state_flags = FINITE_LIFE; WeaponElementPtr->next.location = curLanderLoc; SetPrimType (&DisplayArray[WeaponElementPtr->PrimIndex], STAMP_PRIM); DisplayArray[WeaponElementPtr->PrimIndex].Object.Stamp.frame = SetAbsFrameIndex (LanderFrame[0], /* shot images immediately follow the lander images */ facing + ANGLE_TO_FACING (FULL_CIRCLE)); if (!CurrentInputState.key[PlayerControls[0]][KEY_UP]) { wdx = 0; wdy = 0; } else { GetCurrentVelocityComponents (&GLOBAL (velocity), &wdx, &wdy); } angle = FACING_TO_ANGLE (facing); SetVelocityComponents ( &WeaponElementPtr->velocity, COSINE (angle, WORLD_TO_VELOCITY (2 * 3)) + wdx, SINE (angle, WORLD_TO_VELOCITY (2 * 3)) + wdy); UnlockElement (hWeaponElement); InsertElement (hWeaponElement, GetHeadElement ()); PlaySound (SetAbsSoundIndex (LanderSounds, LANDER_SHOOTS), NotPositional (), NULL, GAME_SOUND_PRIORITY); } static BOOLEAN LanderExplosion (void) { HELEMENT hExplosionElement; ELEMENT *ExplosionElementPtr; hExplosionElement = AllocElement (); if (!hExplosionElement) return FALSE; LockElement (hExplosionElement, &ExplosionElementPtr); ExplosionElementPtr->playerNr = PS_HUMAN_PLAYER; ExplosionElementPtr->mass_points = DEATH_EXPLOSION; ExplosionElementPtr->state_flags = FINITE_LIFE; ExplosionElementPtr->next.location = curLanderLoc; ExplosionElementPtr->preprocess_func = object_animation; // Animation advances every 3rd frame ExplosionElementPtr->turn_wait = MAKE_BYTE (2, 2); ExplosionElementPtr->life_span = EXPLOSION_LIFE * (LONIBBLE (ExplosionElementPtr->turn_wait) + 1); SetPrimType (&DisplayArray[ExplosionElementPtr->PrimIndex], STAMP_PRIM); DisplayArray[ExplosionElementPtr->PrimIndex].Object.Stamp.frame = SetAbsFrameIndex (LanderFrame[0], 46); UnlockElement (hExplosionElement); InsertElement (hExplosionElement, GetHeadElement ()); PlaySound (SetAbsSoundIndex (LanderSounds, LANDER_DESTROYED), NotPositional (), NULL, GAME_SOUND_PRIORITY + 1); return TRUE; } static BOOLEAN DoPlanetSide (LanderInputState *pMS) { SIZE dx = 0; SIZE dy = 0; #define SHUTTLE_TURN_WAIT 2 if (GLOBAL (CurrentActivity) & CHECK_ABORT) return (FALSE); if (!pMS->Initialized) { COUNT landerSpeedNumer; COUNT angle; pMS->Initialized = TRUE; turn_wait = 0; weapon_wait = 0; angle = FACING_TO_ANGLE (GetFrameIndex (LanderFrame[0])); landerSpeedNumer = GET_GAME_STATE (IMPROVED_LANDER_SPEED) ? WORLD_TO_VELOCITY (2 * 14) : WORLD_TO_VELOCITY (2 * 8); #ifdef FAST_FAST landerSpeedNumer = WORLD_TO_VELOCITY (48); #endif SetVelocityComponents (&GLOBAL (velocity), COSINE (angle, landerSpeedNumer) / LANDER_SPEED_DENOM, SINE (angle, landerSpeedNumer) / LANDER_SPEED_DENOM); return TRUE; } else if (crew_left /* alive and taking off */ && ((CurrentInputState.key[PlayerControls[0]][KEY_ESCAPE] || CurrentInputState.key[PlayerControls[0]][KEY_SPECIAL]) || planetSideDesc->InTransit)) { return FALSE; } else if (!crew_left && !damage_index) { // Dead, damage dealt, and exploding if (explosion_index > EXPLOSION_LIFE + EXPLOSION_WAIT_FRAMES) return FALSE; if (explosion_index > EXPLOSION_LIFE) { // Keep going until the wait expires ++explosion_index; } else if (explosion_index == 0) { // Start the explosion animation if (LanderExplosion ()) { // Advance the state only once we've got the element ++explosion_index; } else { // We could not allocate because the queue was full, but // we will get another chance on the next iteration log_add (log_Warning, "DoPlanetSide(): could not" " allocate explosion element!"); } } } else { if (crew_left) { SIZE index = GetFrameIndex (LanderFrame[0]); if (turn_wait) --turn_wait; else if (CurrentInputState.key[PlayerControls[0]][KEY_LEFT] || CurrentInputState.key[PlayerControls[0]][KEY_RIGHT]) { COUNT landerSpeedNumer; COUNT angle; if (CurrentInputState.key[PlayerControls[0]][KEY_LEFT]) --index; else ++index; index = NORMALIZE_FACING (index); LanderFrame[0] = SetAbsFrameIndex (LanderFrame[0], index); angle = FACING_TO_ANGLE (index); landerSpeedNumer = GET_GAME_STATE (IMPROVED_LANDER_SPEED) ? WORLD_TO_VELOCITY (2 * 14) : WORLD_TO_VELOCITY (2 * 8); #ifdef FAST_FAST landerSpeedNumer = WORLD_TO_VELOCITY (48); #endif SetVelocityComponents (&GLOBAL (velocity), COSINE (angle, landerSpeedNumer) / LANDER_SPEED_DENOM, SINE (angle, landerSpeedNumer) / LANDER_SPEED_DENOM); turn_wait = SHUTTLE_TURN_WAIT; } if (!CurrentInputState.key[PlayerControls[0]][KEY_UP]) { dx = 0; dy = 0; } else GetNextVelocityComponents (&GLOBAL (velocity), &dx, &dy, 1); if (weapon_wait) --weapon_wait; else if (CurrentInputState.key[PlayerControls[0]][KEY_WEAPON]) { LanderFire (index); weapon_wait = SHUTTLE_FIRE_WAIT; if (GET_GAME_STATE (IMPROVED_LANDER_SHOT)) weapon_wait >>= 1; } } } ScrollPlanetSide (dx, dy, ON_THE_GROUND); SleepThreadUntil (pMS->NextTime); // NOTE: The rate is not stabilized pMS->NextTime = GetTimeCounter () + PLANET_SIDE_RATE; return TRUE; } void FreeLanderData (void) { COUNT i; COUNT landerFrameCount; if (LanderFrame[0] == NULL) return; for (i = 0; i < NUM_ORBIT_THEMES; ++i) { DestroyMusic (OrbitMusic[i]); OrbitMusic[i] = 0; } DestroySound (ReleaseSound (LanderSounds)); LanderSounds = 0; landerFrameCount = sizeof (LanderFrame) / sizeof (LanderFrame[0]); for (i = 0; i < landerFrameCount; ++i) { DestroyDrawable (ReleaseDrawable (LanderFrame[i])); LanderFrame[i] = 0; } } void LoadLanderData (void) { if (LanderFrame[0] != 0) return; LanderFrame[0] = CaptureDrawable (LoadGraphic (LANDER_MASK_PMAP_ANIM)); LanderFrame[1] = CaptureDrawable (LoadGraphic (QUAKE_MASK_PMAP_ANIM)); LanderFrame[2] = CaptureDrawable (LoadGraphic (LIGHTNING_MASK_ANIM)); LanderFrame[3] = CaptureDrawable (LoadGraphic (LAVA_MASK_PMAP_ANIM)); LanderFrame[4] = CaptureDrawable (LoadGraphic (LANDER_SHIELD_MASK_ANIM)); LanderFrame[5] = CaptureDrawable (LoadGraphic (LANDER_LAUNCH_MASK_PMAP_ANIM)); LanderFrame[6] = CaptureDrawable (LoadGraphic (LANDER_RETURN_MASK_PMAP_ANIM)); LanderFrame[7] = CaptureDrawable (LoadGraphic (ORBIT_VIEW_ANIM)); LanderSounds = CaptureSound (LoadSound (LANDER_SOUNDS)); { COUNT i; for (i = 0; i < NUM_ORBIT_THEMES; ++i) OrbitMusic[i] = load_orbit_theme (i); } } void SetPlanetMusic (BYTE planet_type) { LanderMusic = OrbitMusic[planet_type % NUM_ORBIT_THEMES]; } static void ReturnToOrbit (void) { CONTEXT OldContext; RECT r; OldContext = SetContext (PlanetContext); GetContextClipRect (&r); SetTransitionSource (&r); BatchGraphics (); DrawStarBackGround (); DrawPlanetSurfaceBorder (); RedrawSurfaceScan (NULL); ScreenTransition (3, &r); UnbatchGraphics (); SetContext (OldContext); } static void IdlePlanetSide (LanderInputState *inputState, TimeCount howLong) { #define IDLE_OFFSET TimeCount TimeOut = GetTimeCounter () + howLong; while (GetTimeCounter () < TimeOut) { // 10 to clear the lander off of the screen ScrollPlanetSide (0, 0, -(SURFACE_HEIGHT / 2 + 10)); SleepThreadUntil (inputState->NextTime); inputState->NextTime += PLANET_SIDE_RATE; } } static void LandingTakeoffSequence (LanderInputState *inputState, BOOLEAN landing) { // We cannot solve a quadratic equation in a macro, so use a sensible max #define MAX_OFFSETS 20 // 10 to clear the lander off of the screen #define DISTANCE_COVERED (SURFACE_HEIGHT / 2 + 10) int landingOfs[MAX_OFFSETS]; int start; int end; int delta; int index; // Produce smooth acceleration deltas from a simple 1..x progression delta = 0; for (index = 0; index < MAX_OFFSETS && delta < DISTANCE_COVERED; ++index) { delta += index + 1; landingOfs[index] = -delta; } assert (delta >= DISTANCE_COVERED && "Increase MAX_OFFSETS!"); if (landing) { start = index - 1; end = -1; delta = -1; } else { // takeoff start = 0; end = index; delta = +1; } if (landing) IdlePlanetSide (inputState, ONE_SECOND); // Draw the landing/takeoff lander positions for (index = start; index != end; index += delta) { ScrollPlanetSide (0, 0, landingOfs[index]); SleepThreadUntil (inputState->NextTime); inputState->NextTime += PLANET_SIDE_RATE; } if (!landing) IdlePlanetSide (inputState, ONE_SECOND / 2); } void SetLanderTakeoff (void) { assert (planetSideDesc != NULL); if (planetSideDesc) planetSideDesc->InTransit = TRUE; } // Returns whether the lander is still alive at the end of the sequence bool KillLanderCrewSeq (COUNT numKilled, DWORD period) { TimeCount TimeOut; COUNT i; TimeOut = GetTimeCounter (); for (i = 0; i < numKilled && crew_left; ++i) { TimeOut += period; DeltaLanderCrew (-1, LANDER_INJURED); SleepThreadUntil (TimeOut); } return crew_left > 0; } // Maps a temperature to a (0-7) hazard rating. // Thermal hazards aren't exposed to the user as a hazard number, // but the code still works with them that way. #define ARRAY_SIZE(array) (sizeof(array) / sizeof (*array)) unsigned GetThermalHazardRating (int temp) { static const int tempBreakpoints[] = { 50, 100, 150, 250, 350, 550, 800 }; const size_t numBreakpoints = ARRAY_SIZE (tempBreakpoints); unsigned i; for (i = 0; i < numBreakpoints; ++i) { if (temp < tempBreakpoints[i]) return i; } return numBreakpoints; } // Given a hazard type and rating, return the chance (out of 256) of the hazard // being generated. static BYTE GetHazardChance (int hazardType, unsigned HazardRating) { static const BYTE TectonicsChanceTab[] = {0*3, 0*3, 1*3, 2*3, 4*3, 8*3, 16*3, 32*3}; static const BYTE WeatherChanceTab [] = {0*3, 0*3, 1*3, 2*3, 3*3, 6*3, 12*3, 24*3}; static const BYTE FireChanceTab [] = {0*3, 0*3, 1*3, 2*3, 4*3, 12*3, 24*3, 48*3}; switch (hazardType) { case EARTHQUAKE_DISASTER: return TectonicsChanceTab[HazardRating]; case LIGHTNING_DISASTER: return WeatherChanceTab[HazardRating]; case LAVASPOT_DISASTER: return FireChanceTab[HazardRating]; } return 0; } void PlanetSide (POINT planetLoc) { SIZE index; LanderInputState landerInputState; PLANETSIDE_DESC PSD; memset (&PSD, 0, sizeof (PSD)); PSD.InTransit = TRUE; // Set our chances of hazards occurring. PSD.TectonicsChance = GetHazardChance (EARTHQUAKE_DISASTER, pSolarSysState->SysInfo.PlanetInfo.Tectonics); PSD.WeatherChance = GetHazardChance (LIGHTNING_DISASTER, pSolarSysState->SysInfo.PlanetInfo.Weather); PSD.FireChance = GetHazardChance (LAVASPOT_DISASTER, GetThermalHazardRating ( pSolarSysState->SysInfo.PlanetInfo.SurfaceTemperature)); PSD.ElementLevel = GetStorageBayCapacity () - GLOBAL_SIS (TotalElementMass); PSD.MaxElementLevel = MAX_SCROUNGED; if (GET_GAME_STATE (IMPROVED_LANDER_CARGO)) PSD.MaxElementLevel <<= 1; if (PSD.ElementLevel < PSD.MaxElementLevel) PSD.MaxElementLevel = PSD.ElementLevel; PSD.ElementLevel = 0; PSD.MineralText[0].align = ALIGN_CENTER; PSD.MineralText[0].pStr = PSD.AmountBuf; PSD.MineralText[1] = PSD.MineralText[0]; PSD.MineralText[2] = PSD.MineralText[1]; PSD.ColorCycle[0] = BUILD_COLOR (MAKE_RGB15 (0x1F, 0x03, 0x00), 0x7F); PSD.ColorCycle[1] = BUILD_COLOR (MAKE_RGB15 (0x1F, 0x0A, 0x00), 0x7D); PSD.ColorCycle[2] = BUILD_COLOR (MAKE_RGB15 (0x1F, 0x11, 0x00), 0x7B); PSD.ColorCycle[3] = BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x00), 0x71); for (index = 4; index < (NUM_TEXT_FRAMES >> 1) - 4; ++index) { PSD.ColorCycle[index] = BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x1F), 0x0F); } PSD.ColorCycle[(NUM_TEXT_FRAMES >> 1) - 4] = BUILD_COLOR (MAKE_RGB15 (0x1F, 0x1F, 0x00), 0x71); PSD.ColorCycle[(NUM_TEXT_FRAMES >> 1) - 3] = BUILD_COLOR (MAKE_RGB15 (0x1F, 0x11, 0x00), 0x7B); PSD.ColorCycle[(NUM_TEXT_FRAMES >> 1) - 2] = BUILD_COLOR (MAKE_RGB15 (0x1F, 0x0A, 0x00), 0x7D); PSD.ColorCycle[(NUM_TEXT_FRAMES >> 1) - 1] = BUILD_COLOR (MAKE_RGB15 (0x1F, 0x03, 0x00), 0x7F); planetSideDesc = &PSD; index = NORMALIZE_FACING (TFB_Random ()); LanderFrame[0] = SetAbsFrameIndex (LanderFrame[0], index); crew_left = 0; damage_index = 0; explosion_index = 0; AnimateLanderWarmup (); AnimateLaunch (LanderFrame[5]); InitPlanetSide (planetLoc); landerInputState.NextTime = GetTimeCounter () + PLANET_SIDE_RATE; LandingTakeoffSequence (&landerInputState, TRUE); PSD.InTransit = FALSE; landerInputState.Initialized = FALSE; landerInputState.InputFunc = DoPlanetSide; SetMenuSounds (MENU_SOUND_NONE, MENU_SOUND_NONE); DoInput (&landerInputState, FALSE); if (!(GLOBAL (CurrentActivity) & CHECK_ABORT)) { if (crew_left == 0) { --GLOBAL_SIS (NumLanders); DrawLanders (); ReturnToOrbit (); } else { PSD.InTransit = TRUE; PlaySound (SetAbsSoundIndex (LanderSounds, LANDER_RETURNS), NotPositional (), NULL, GAME_SOUND_PRIORITY + 1); LandingTakeoffSequence (&landerInputState, FALSE); ReturnToOrbit (); AnimateLaunch (LanderFrame[6]); DeltaSISGauges (crew_left, 0, 0); if (PSD.ElementLevel) { for (index = 0; index < NUM_ELEMENT_CATEGORIES; ++index) { GLOBAL_SIS (ElementAmounts[index]) += PSD.ElementAmounts[index]; GLOBAL_SIS (TotalElementMass) += PSD.ElementAmounts[index]; } DrawStorageBays (FALSE); } GLOBAL_SIS (TotalBioMass) += PSD.BiologicalLevel; } } planetSideDesc = NULL; { HELEMENT hElement, hNextElement; for (hElement = GetHeadElement (); hElement; hElement = hNextElement) { ELEMENT *ElementPtr; LockElement (hElement, &ElementPtr); hNextElement = _GetSuccLink (ElementPtr); if (ElementPtr->state_flags & FINITE_LIFE) { UnlockElement (hElement); RemoveElement (hElement); FreeElement (hElement); continue; } UnlockElement (hElement); } } ZeroVelocityComponents (&GLOBAL (velocity)); SetMenuSounds (MENU_SOUND_ARROWS, MENU_SOUND_SELECT); } void InitLander (BYTE LanderFlags) { RECT r; SetContext (RadarContext); BatchGraphics (); r.corner.x = 0; r.corner.y = 0; r.extent.width = RADAR_WIDTH; r.extent.height = RADAR_HEIGHT; SetContextForeGroundColor (BLACK_COLOR); DrawFilledRectangle (&r); if (GLOBAL_SIS (NumLanders) || LanderFlags) { BYTE ShieldFlags, capacity_shift; COUNT free_space; STAMP s; s.origin.x = 0; /* set up powered-down lander */ s.origin.y = 0; s.frame = SetAbsFrameIndex (LanderFrame[0], ANGLE_TO_FACING (FULL_CIRCLE) << 1); DrawStamp (&s); if (LanderFlags == 0) { ShieldFlags = GET_GAME_STATE (LANDER_SHIELDS); capacity_shift = GET_GAME_STATE (IMPROVED_LANDER_CARGO); } else { ShieldFlags = (unsigned char)(LanderFlags & ((1 << EARTHQUAKE_DISASTER) | (1 << BIOLOGICAL_DISASTER) | (1 << LIGHTNING_DISASTER) | (1 << LAVASPOT_DISASTER))); s.frame = IncFrameIndex (s.frame); DrawStamp (&s); if (LanderFlags & (1 << (4 + 0))) s.frame = SetAbsFrameIndex (s.frame, 57); else { s.frame = SetAbsFrameIndex (s.frame, (ANGLE_TO_FACING (FULL_CIRCLE) << 1) + 3); DrawStamp (&s); s.frame = IncFrameIndex (s.frame); } DrawStamp (&s); if (!(LanderFlags & (1 << (4 + 1)))) capacity_shift = 0; else { capacity_shift = 1; s.frame = SetAbsFrameIndex (s.frame, 59); DrawStamp (&s); } if (LanderFlags & (1 << (4 + 2))) s.frame = SetAbsFrameIndex (s.frame, 58); else s.frame = SetAbsFrameIndex (s.frame, (ANGLE_TO_FACING (FULL_CIRCLE) << 1) + 2); DrawStamp (&s); } free_space = GetStorageBayCapacity () - GLOBAL_SIS (TotalElementMass); if ((int)free_space < (int)(MAX_SCROUNGED << capacity_shift)) { r.corner.x = 1; r.extent.width = 4; r.extent.height = MAX_SCROUNGED - (free_space >> capacity_shift) + 1; SetContextForeGroundColor (BLACK_COLOR); DrawFilledRectangle (&r); } s.frame = SetAbsFrameIndex (s.frame, 37); if (ShieldFlags & (1 << EARTHQUAKE_DISASTER)) DrawStamp (&s); s.frame = IncFrameIndex (s.frame); if (ShieldFlags & (1 << BIOLOGICAL_DISASTER)) DrawStamp (&s); s.frame = IncFrameIndex (s.frame); if (ShieldFlags & (1 << LIGHTNING_DISASTER)) DrawStamp (&s); s.frame = IncFrameIndex (s.frame); if (ShieldFlags & (1 << LAVASPOT_DISASTER)) DrawStamp (&s); } UnbatchGraphics (); }