// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2022 by Sally "TehRealSalt" Cochenour // Copyright (C) 2022 by Kart Krew // // This program is free software distributed under the // terms of the GNU General Public License, version 2. // See the 'LICENSE' file for more details. //----------------------------------------------------------------------------- /// \file shrink.c /// \brief Shrink laser item code. #include "../doomdef.h" #include "../doomstat.h" #include "../info.h" #include "../k_kart.h" #include "../k_objects.h" #include "../m_random.h" #include "../p_local.h" #include "../r_main.h" #include "../s_sound.h" #include "../g_game.h" #include "../z_zone.h" #include "../k_waypoint.h" #define POHBEE_HOVER (256 << FRACBITS) #define POHBEE_SPEED (128 << FRACBITS) #define POHBEE_TIME (30 * TICRATE) #define POHBEE_DIST (4096 << FRACBITS) #define GUN_SWING (ANGLE_90 - ANG10) #define GUN_SWINGTIME (4 * TICRATE) #define CHAIN_SIZE (52) #define EXTRA_FOR_FIRST (7) enum { POHBEE_MODE_SPAWN, POHBEE_MODE_ACT, POHBEE_MODE_DESPAWN, }; #define pohbee_mode(o) ((o)->cusval) #define pohbee_timer(o) ((o)->reactiontime) #define pohbee_waypoint_cur(o) ((o)->extravalue1) #define pohbee_waypoint_dest(o) ((o)->extravalue2) #define pohbee_height(o) ((o)->movefactor) #define pohbee_owner(o) ((o)->target) #define pohbee_guns(o) ((o)->hnext) #define gun_offset(o) ((o)->movecount) #define gun_numsegs(o) ((o)->extravalue1) #define gun_pohbee(o) ((o)->target) #define gun_laser(o) ((o)->tracer) #define gun_chains(o) ((o)->hprev) #define chain_index(o) ((o)->extravalue1) enum { LASER_SHRINK, LASER_GROW, }; static skincolornum_t ShrinkLaserColor(mobj_t *pohbee) { UINT8 laserState = LASER_SHRINK; player_t *owner = NULL; if (pohbee_owner(pohbee) != NULL && P_MobjWasRemoved(pohbee_owner(pohbee)) == false) { owner = pohbee_owner(pohbee)->player; } if (owner != NULL && P_IsDisplayPlayer(owner) == true) { laserState = LASER_GROW; if (r_splitscreen > 0 && (leveltime & 1)) { // TODO: make this properly screen dependent, // instead of flashing. laserState = LASER_SHRINK; } } switch (laserState) { default: case LASER_SHRINK: return SKINCOLOR_KETCHUP; case LASER_GROW: return SKINCOLOR_SAPPHIRE; } } static boolean ShrinkLaserActive(mobj_t *pohbee) { return (pohbee_mode(pohbee) == POHBEE_MODE_ACT); } static void PohbeeMoveTo(mobj_t *pohbee, fixed_t destx, fixed_t desty, fixed_t destz) { pohbee->momx = destx - pohbee->x; pohbee->momy = desty - pohbee->y; pohbee->momz = destz - pohbee->z; } static fixed_t GenericDistance( fixed_t curx, fixed_t cury, fixed_t curz, fixed_t destx, fixed_t desty, fixed_t destz) { return P_AproxDistance(P_AproxDistance(destx - curx, desty - cury), destz - curz); } static fixed_t PohbeeWaypointZ(mobj_t *pohbee, mobj_t *dest) { return dest->z + (pohbee_height(pohbee) + FixedMul(POHBEE_HOVER, mapobjectscale) * P_MobjFlip(dest)); } static void PohbeeSpawn(mobj_t *pohbee) { waypoint_t *curWaypoint = NULL; waypoint_t *destWaypoint = NULL; fixed_t distLeft = INT32_MAX; fixed_t newX = pohbee->x; fixed_t newY = pohbee->y; fixed_t newZ = pohbee->z; boolean finalize = false; const boolean useshortcuts = false; const boolean huntbackwards = false; boolean pathfindsuccess = false; path_t pathtofinish = {0}; size_t pathIndex = 0; curWaypoint = K_GetWaypointFromIndex((size_t)pohbee_waypoint_cur(pohbee)); destWaypoint = K_GetWaypointFromIndex((size_t)pohbee_waypoint_dest(pohbee)); if (curWaypoint == NULL || destWaypoint == NULL) { // Waypoints aren't valid. // Just transition into the next state. pohbee_mode(pohbee) = POHBEE_MODE_ACT; return; } distLeft = FixedMul(POHBEE_SPEED, mapobjectscale); while (distLeft > 0) { fixed_t wpX = curWaypoint->mobj->x; fixed_t wpY = curWaypoint->mobj->y; fixed_t wpZ = PohbeeWaypointZ(pohbee, curWaypoint->mobj); fixed_t distToNext = GenericDistance( newX, newY, newZ, wpX, wpY, wpZ ); if (distToNext > distLeft) { // Only made it partially there. newX += FixedMul(FixedDiv(wpX - newX, distToNext), distLeft); newY += FixedMul(FixedDiv(wpY - newY, distToNext), distLeft); newZ += FixedMul(FixedDiv(wpZ - newZ, distToNext), distLeft); distLeft = 0; } else { // Close enough to the next waypoint, // move there and remove the distance. newX = wpX; newY = wpY; newZ = wpZ; distLeft -= distToNext; if (curWaypoint == destWaypoint) { // Reached the end. finalize = true; break; } // Create waypoint path to our destination. // Crazy over-engineered, just to catch when // waypoints are insanely close to each other :P if (pathfindsuccess == false) { pathfindsuccess = K_PathfindToWaypoint( curWaypoint, destWaypoint, &pathtofinish, useshortcuts, huntbackwards ); if (pathfindsuccess == false) { // Path isn't valid. // Just transition into the next state. finalize = true; break; } } pathIndex++; if (pathIndex >= pathtofinish.numnodes) { // Successfully reached the end of the path. finalize = true; break; } // Now moving to the next waypoint. curWaypoint = (waypoint_t *)pathtofinish.array[pathIndex].nodedata; pohbee_waypoint_cur(pohbee) = (INT32)K_GetWaypointHeapIndex(curWaypoint); } } PohbeeMoveTo(pohbee, newX, newY, newZ); pohbee->angle = K_MomentumAngle(pohbee); if (finalize == true) { // Move to next state pohbee_mode(pohbee) = POHBEE_MODE_ACT; } if (pathfindsuccess == true) { Z_Free(pathtofinish.array); } } static void PohbeeAct(mobj_t *pohbee) { pohbee_timer(pohbee)--; if (pohbee_timer(pohbee) <= 0) { // Move to next state pohbee_mode(pohbee) = POHBEE_MODE_DESPAWN; pohbee->fuse = 5*TICRATE; } } static void PohbeeDespawn(mobj_t *pohbee) { pohbee->momz = 16 * pohbee->scale * P_MobjFlip(pohbee); } static void DoGunSwing(mobj_t *gun, mobj_t *pohbee) { const angle_t angle = gun->angle + ANGLE_90; const tic_t swingTimer = leveltime + gun_offset(gun); const angle_t swingAmt = swingTimer * (ANGLE_MAX / GUN_SWINGTIME); const fixed_t swingCos = FINECOSINE(swingAmt >> ANGLETOFINESHIFT); const angle_t pitch = -ANGLE_90 + FixedMul(swingCos, GUN_SWING); const fixed_t dist = gun_numsegs(gun) * CHAIN_SIZE * gun->scale; fixed_t offsetX = FixedMul( dist, FixedMul( FINECOSINE(angle >> ANGLETOFINESHIFT), FINECOSINE(pitch >> ANGLETOFINESHIFT) ) ); fixed_t offsetY = FixedMul( dist, FixedMul( FINESINE(angle >> ANGLETOFINESHIFT), FINECOSINE(pitch >> ANGLETOFINESHIFT) ) ); fixed_t offsetZ = FixedMul( dist, FINESINE(pitch >> ANGLETOFINESHIFT) ); PohbeeMoveTo(gun, pohbee->x + offsetX, pohbee->y + offsetY, pohbee->z + offsetZ); } static void ShrinkLaserThinker(mobj_t *pohbee, mobj_t *gun, mobj_t *laser) { const fixed_t gunX = gun->x + gun->momx; const fixed_t gunY = gun->y + gun->momy; const fixed_t gunZ = P_GetMobjFeet(gun) + gun->momz; PohbeeMoveTo(laser, gunX, gunY, gun->floorz); if (ShrinkLaserActive(pohbee) == true) { mobj_t *particle = NULL; laser->renderflags &= ~RF_DONTDRAW; laser->color = gun->color; if (leveltime & 1) { laser->spritexscale = 5*FRACUNIT/2; } else { laser->spritexscale = FRACUNIT; } laser->spriteyscale = FixedDiv(FixedDiv(gunZ - gun->floorz, mapobjectscale), laser->info->height); particle = P_SpawnMobjFromMobj( laser, P_RandomRange(-16, 16) * FRACUNIT, P_RandomRange(-16, 16) * FRACUNIT, 0, MT_SHRINK_PARTICLE ); P_SetTarget(&gun_pohbee(particle), pohbee); particle->color = laser->color; P_SetScale(particle, particle->scale * 2); particle->destscale = 0; //particle->momz = 2 * particle->scale * P_MobjFlip(particle); } else { laser->renderflags |= RF_DONTDRAW; } } static void DoGunChains(mobj_t *gun, mobj_t *pohbee) { const fixed_t gunX = gun->x + gun->momx; const fixed_t gunY = gun->y + gun->momy; const fixed_t gunZ = P_GetMobjHead(gun) + gun->momz; const fixed_t beeX = pohbee->x + pohbee->momx; const fixed_t beeY = pohbee->y + pohbee->momy; const fixed_t beeZ = P_GetMobjFeet(pohbee) + pohbee->momz; const fixed_t offsetX = (beeX - gunX) / gun_numsegs(gun); const fixed_t offsetY = (beeY - gunY) / gun_numsegs(gun); const fixed_t offsetZ = (beeZ - gunZ) / gun_numsegs(gun); mobj_t *chain = NULL; fixed_t curX = gunX + (offsetX / 2); fixed_t curY = gunY + (offsetY / 2); fixed_t curZ = gunZ + (offsetZ / 2); chain = gun_chains(gun); while (chain != NULL && P_MobjWasRemoved(chain) == false) { PohbeeMoveTo(chain, curX, curY, curZ); curX += offsetX; curY += offsetY; curZ += offsetZ; chain = gun_chains(chain); } } static void ShrinkGunThinker(mobj_t *gun) { mobj_t *pohbee = gun_pohbee(gun); if (pohbee == NULL || P_MobjWasRemoved(pohbee) == true) { P_RemoveMobj(gun); return; } gun->angle = pohbee->angle; gun->color = ShrinkLaserColor(pohbee); DoGunSwing(gun, pohbee); if (gun_laser(gun) != NULL && P_MobjWasRemoved(gun_laser(gun)) == false) { ShrinkLaserThinker(pohbee, gun, gun_laser(gun)); } DoGunChains(gun, pohbee); } void Obj_PohbeeThinker(mobj_t *pohbee) { mobj_t *gun = NULL; pohbee->momx = pohbee->momy = pohbee->momz = 0; switch (pohbee_mode(pohbee)) { case POHBEE_MODE_SPAWN: PohbeeSpawn(pohbee); break; case POHBEE_MODE_ACT: PohbeeAct(pohbee); break; case POHBEE_MODE_DESPAWN: PohbeeDespawn(pohbee); break; default: // failsafe pohbee_mode(pohbee) = POHBEE_MODE_SPAWN; break; } gun = pohbee_guns(pohbee); while (gun != NULL && P_MobjWasRemoved(gun) == false) { ShrinkGunThinker(gun); gun = pohbee_guns(gun); } } void Obj_PohbeeRemoved(mobj_t *pohbee) { mobj_t *gun = pohbee_guns(pohbee); while (gun != NULL && P_MobjWasRemoved(gun) == false) { mobj_t *nextGun = pohbee_guns(gun); P_RemoveMobj(gun); gun = nextGun; } } void Obj_ShrinkGunRemoved(mobj_t *gun) { mobj_t *chain = NULL; if (gun_laser(gun) != NULL && P_MobjWasRemoved(gun_laser(gun)) == false) { P_RemoveMobj(gun_laser(gun)); } chain = gun_chains(gun); while (chain != NULL && P_MobjWasRemoved(chain) == false) { mobj_t *nextChain = gun_chains(chain); P_RemoveMobj(chain); chain = nextChain; } } boolean Obj_ShrinkLaserCollide(mobj_t *gun, mobj_t *victim) { mobj_t *pohbee = gun_pohbee(gun); mobj_t *owner = NULL; INT32 prevTimer = 0; if (pohbee == NULL || P_MobjWasRemoved(pohbee) == true) { return true; } if (ShrinkLaserActive(pohbee) == false) { return true; } if (victim->player->shrinkLaserDelay > 0) { victim->player->shrinkLaserDelay = TICRATE; return true; } victim->player->shrinkLaserDelay = TICRATE; owner = pohbee_owner(pohbee); prevTimer = victim->player->growshrinktimer; if (owner != NULL && victim == owner) { // Belongs to us. Give us Grow! if (prevTimer < 0) { // Take away Shrink. K_RemoveGrowShrink(victim->player); } else { victim->player->growshrinktimer += 3*TICRATE; S_StartSound(victim, sfx_kc5a); if (prevTimer <= 0) { victim->scalespeed = mapobjectscale/TICRATE; victim->destscale = FixedMul(mapobjectscale, GROW_SCALE); if (K_PlayerShrinkCheat(victim->player) == true) { victim->destscale = FixedMul(victim->destscale, SHRINK_SCALE); } if (victim->player->invincibilitytimer > 0) { ; // invincibility has priority in P_RestoreMusic, no point in starting here } else if (P_IsLocalPlayer(victim->player) == true) { S_ChangeMusicSpecial("kgrow"); } else //used to be "if (P_IsDisplayPlayer(victim->player) == false)" { S_StartSound(victim, (cv_kartinvinsfx.value ? sfx_alarmg : sfx_kgrow)); } P_RestoreMusic(victim->player); } } } else { if (prevTimer > 0) { // Take away Grow. K_RemoveGrowShrink(victim->player); } else { // Start shrinking! victim->player->growshrinktimer -= 5*TICRATE; S_StartSound(victim, sfx_kc59); if (prevTimer >= 0) { //K_DropItems(victim->player); victim->scalespeed = mapobjectscale/TICRATE; victim->destscale = FixedMul(mapobjectscale, SHRINK_SCALE); if (K_PlayerShrinkCheat(victim->player) == true) { victim->destscale = FixedMul(victim->destscale, SHRINK_SCALE); } } } } return true; } static waypoint_t *GetPohbeeWaypoint(waypoint_t *anchor, const UINT32 traveldist, const boolean huntbackwards) { const boolean useshortcuts = false; boolean pathfindsuccess = false; path_t pathtofinish = {0}; waypoint_t *ret = NULL; pathfindsuccess = K_PathfindThruCircuitSpawnable( anchor, traveldist, &pathtofinish, useshortcuts, huntbackwards ); if (pathfindsuccess == true) { ret = (waypoint_t *)pathtofinish.array[ pathtofinish.numnodes - 1 ].nodedata; Z_Free(pathtofinish.array); } else { ret = anchor; } return ret; } static waypoint_t *GetPohbeeStart(waypoint_t *anchor) { const UINT32 traveldist = FixedMul(POHBEE_DIST >> 1, mapobjectscale) / FRACUNIT; const boolean huntbackwards = true; return GetPohbeeWaypoint(anchor, traveldist, huntbackwards); } static waypoint_t *GetPohbeeEnd(waypoint_t *anchor) { const UINT32 traveldist = FixedMul(POHBEE_DIST, mapobjectscale) / FRACUNIT; const boolean huntbackwards = false; return GetPohbeeWaypoint(anchor, traveldist, huntbackwards); } static void CreatePohbee(player_t *owner, waypoint_t *start, waypoint_t *end, UINT8 numLasers) { mobj_t *pohbee = NULL; fixed_t size = 0; INT32 baseSegs = INT32_MAX; INT32 segVal = INT32_MAX; mobj_t *prevGun = NULL; size_t i, j; if (owner == NULL || owner->mo == NULL || P_MobjWasRemoved(owner->mo) == true || start == NULL || end == NULL || numLasers == 0) { // Invalid inputs return; } // Calculate number of chain segments added per laser. size = FixedMul(end->mobj->radius, 3*FRACUNIT/2); segVal = max(1, 1 + ((size / start->mobj->scale) / CHAIN_SIZE) / numLasers); baseSegs = segVal * numLasers; // Valid spawning conditions, // we can start creating each individual part. pohbee = P_SpawnMobjFromMobj(start->mobj, 0, 0, (baseSegs * CHAIN_SIZE * FRACUNIT) + POHBEE_HOVER * 3, MT_SHRINK_POHBEE); P_SetTarget(&pohbee_owner(pohbee), owner->mo); pohbee_mode(pohbee) = POHBEE_MODE_SPAWN; pohbee_timer(pohbee) = POHBEE_TIME; pohbee_height(pohbee) = size; pohbee_waypoint_cur(pohbee) = (INT32)K_GetWaypointHeapIndex(start); pohbee_waypoint_dest(pohbee) = (INT32)K_GetWaypointHeapIndex(end); prevGun = pohbee; for (i = 0; i < numLasers; i++) { const UINT8 numSegs = segVal * (i + 1); mobj_t *gun = P_SpawnMobjFromMobj(pohbee, 0, 0, 0, MT_SHRINK_GUN); mobj_t *laser = NULL; mobj_t *prevChain = NULL; P_SetTarget(&gun_pohbee(gun), pohbee); P_SetTarget(&pohbee_guns(prevGun), gun); gun_numsegs(gun) = numSegs; gun_offset(gun) = P_RandomKey(GUN_SWINGTIME); laser = P_SpawnMobjFromMobj(gun, 0, 0, 0, MT_SHRINK_LASER); P_SetTarget(&gun_laser(gun), laser); prevChain = gun; for (j = 0; j < numSegs; j++) { mobj_t *chain = P_SpawnMobjFromMobj(gun, 0, 0, 0, MT_SHRINK_CHAIN); P_SetTarget(&gun_chains(prevChain), chain); chain_index(chain) = j; prevChain = chain; } prevGun = gun; } } void Obj_CreateShrinkPohbees(player_t *owner) { UINT8 ownerPos = 1; struct { waypoint_t *start; waypoint_t *end; UINT8 lasers; boolean first; } pohbees[MAXPLAYERS]; size_t numPohbees = 0; size_t i, j; if (owner == NULL || owner->mo == NULL || P_MobjWasRemoved(owner->mo) == true) { return; } ownerPos = owner->position; for (i = 0; i < MAXPLAYERS; i++) { player_t *player = NULL; waypoint_t *endWaypoint = NULL; if (playeringame[i] == false) { // Not valid. continue; } player = &players[i]; if (player->spectator == true) { // Not playing. continue; } if (player->position > ownerPos) { // Too far behind. continue; } if (player->nextwaypoint == NULL) { // No waypoint? continue; } endWaypoint = GetPohbeeEnd(player->nextwaypoint); for (j = 0; j < numPohbees; j++) { if (pohbees[j].end == endWaypoint) { // Increment laser count for the already existing poh-bee, // if another one would occupy the same space. pohbees[j].lasers++; break; } } if (j == numPohbees) { // Push a new poh-bee pohbees[j].start = GetPohbeeStart(player->nextwaypoint); pohbees[j].end = endWaypoint; pohbees[j].lasers = 1; if (player->position == 1) { pohbees[j].first = true; } numPohbees++; } } for (i = 0; i < numPohbees; i++) { CreatePohbee(owner, pohbees[i].start, pohbees[i].end, pohbees[i].lasers); if (pohbees[i].first == true) { // Add a chain of extra ones for 1st place. waypoint_t *prev = pohbees[i].end; for (j = 0; j < EXTRA_FOR_FIRST; j++) { waypoint_t *new = GetPohbeeEnd(prev); CreatePohbee(owner, prev, new, 1); prev = new; } } } }