diff --git a/src/deh_tables.c b/src/deh_tables.c index c803b55fe..3cce64a49 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -4556,6 +4556,15 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi "S_SPECIAL_UFO_STEM", "S_GACHABOM", + "S_GACHABOM_DEAD", + + "S_GACHABOM_EXPLOSION_1", + "S_GACHABOM_EXPLOSION_2", + "S_GACHABOM_EXPLOSION_3A", + "S_GACHABOM_EXPLOSION_3B", + "S_GACHABOM_EXPLOSION_4", + "S_GACHABOM_WAITING", + "S_GACHABOM_RETURNING", }; // RegEx to generate this from info.h: ^\tMT_([^,]+), --> \t"MT_\1", @@ -5415,6 +5424,7 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_SINKTRAIL", "MT_GACHABOM", + "MT_GACHABOM_REBOUND", "MT_DUELBOMB", // Duel mode bombs diff --git a/src/info.c b/src/info.c index b7bddd784..05ce6aa36 100644 --- a/src/info.c +++ b/src/info.c @@ -810,6 +810,7 @@ char sprnames[NUMSPRITES + 1][5] = "UQMK", "GBOM", + "GCHX", // First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later "VIEW", @@ -5216,6 +5217,15 @@ state_t states[NUMSTATES] = {SPR_UFOS, 0, -1, {NULL}, 0, 0, S_NULL}, // S_SPECIAL_UFO_STEM {SPR_GBOM, FF_ANIMATE, -1, {NULL}, 3, 1, S_NULL}, // S_GACHABOM + {SPR_GBOM, FF_INVERT, 2, {NULL}, 0, 0, S_NULL}, // S_GACHABOM_DEAD + + {SPR_NULL, 0, 1, {NULL}, 0, 0, S_GACHABOM_EXPLOSION_2}, + {SPR_GCHX, 0|FF_PAPERSPRITE|FF_ANIMATE, 14, {NULL}, 6, 2, S_GACHABOM_EXPLOSION_3A}, // S_GACHABOM_EXPLOSION_2 + {SPR_GCHX, 6|FF_PAPERSPRITE|FF_ANIMATE, 4, {NULL}, 1, 2, S_GACHABOM_EXPLOSION_3B}, // S_GACHABOM_EXPLOSION_3A + {SPR_NULL, 0|FF_PAPERSPRITE, 0, {A_Repeat}, 8, S_GACHABOM_EXPLOSION_3A, S_GACHABOM_EXPLOSION_4}, // S_GACHABOM_EXPLOSION_3B + {SPR_GCHX, 6|FF_PAPERSPRITE|FF_ANIMATE|FF_REVERSEANIM, 14, {NULL}, 6, 2, S_GACHABOM_WAITING}, // S_GACHABOM_EXPLOSION_4 + {SPR_GBOM, FF_INVERT, 8, {A_SetScale}, FRACUNIT, 0, S_GACHABOM_RETURNING}, // S_GACHABOM_WAITING + {SPR_GBOM, FF_INVERT, -1, {A_SetScale}, FRACUNIT/2, 1, S_NULL}, // S_GACHABOM_RETURNING }; mobjinfo_t mobjinfo[NUMMOBJTYPES] = @@ -24573,7 +24583,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = sfx_None, // painsound S_NULL, // meleestate S_NULL, // missilestate - S_GACHABOM, // deathstate + S_GACHABOM_DEAD, // deathstate S_NULL, // xdeathstate sfx_s3k5d, // deathsound 28*FRACUNIT, // speed @@ -24587,6 +24597,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL // raisestate }, + { // MT_GACHABOM_REBOUND + -1, // doomednum + S_GACHABOM_EXPLOSION_1, // spawnstate + 1, // spawnhealth + S_NULL, // seestate + sfx_None, // seesound + 8, // reactiontime + sfx_None, // attacksound + S_NULL, // painstate + 0, // painchance + sfx_None, // painsound + S_NULL, // meleestate + S_NULL, // missilestate + S_GACHABOM_DEAD, // deathstate + S_NULL, // xdeathstate + sfx_None, // deathsound + 0, // speed + 16*FRACUNIT, // radius + 32*FRACUNIT, // height + 0, // display offset + 100, // mass + 0, // damage + sfx_None, // activesound + MF_NOBLOCKMAP|MF_NOGRAVITY|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOCLIPTHING|MF_DONTENCOREMAP|MF_NOSQUISH, // flags + S_NULL // raisestate + }, + { // MT_DUELBOMB 2050, // doomednum S_SPB1, // spawnstate diff --git a/src/info.h b/src/info.h index 7c770562a..ea28b56c8 100644 --- a/src/info.h +++ b/src/info.h @@ -1363,6 +1363,7 @@ typedef enum sprite SPR_UQMK, SPR_GBOM, + SPR_GCHX, // First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later SPR_VIEW, @@ -5647,6 +5648,15 @@ typedef enum state S_SPECIAL_UFO_STEM, S_GACHABOM, + S_GACHABOM_DEAD, + + S_GACHABOM_EXPLOSION_1, + S_GACHABOM_EXPLOSION_2, + S_GACHABOM_EXPLOSION_3A, + S_GACHABOM_EXPLOSION_3B, + S_GACHABOM_EXPLOSION_4, + S_GACHABOM_WAITING, + S_GACHABOM_RETURNING, S_FIRSTFREESLOT, S_LASTFREESLOT = S_FIRSTFREESLOT + NUMSTATEFREESLOTS - 1, @@ -6525,6 +6535,7 @@ typedef enum mobj_type MT_SINKTRAIL, MT_GACHABOM, + MT_GACHABOM_REBOUND, MT_DUELBOMB, // Duel mode bombs diff --git a/src/k_objects.h b/src/k_objects.h index 07a906a84..5deb7749f 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -133,6 +133,10 @@ boolean Obj_RandomItemSpawnIn(mobj_t *mobj); fixed_t Obj_RandomItemScale(fixed_t oldScale); void Obj_RandomItemSpawn(mobj_t *mobj); +/* Gachabom Rebound */ +void Obj_GachaBomReboundThink(mobj_t *mobj); +void Obj_SpawnGachaBomRebound(mobj_t *source, mobj_t *target); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index 2fe1f0937..0c6fd74e9 100644 --- a/src/objects/CMakeLists.txt +++ b/src/objects/CMakeLists.txt @@ -19,4 +19,5 @@ target_sources(SRB2SDL2 PRIVATE random-item.c instawhip.c block.c + gachabom-rebound.cpp ) diff --git a/src/objects/gachabom-rebound.cpp b/src/objects/gachabom-rebound.cpp new file mode 100644 index 000000000..07aec4868 --- /dev/null +++ b/src/objects/gachabom-rebound.cpp @@ -0,0 +1,233 @@ +#include + +#include "../d_player.h" +#include "../k_objects.h" +#include "../m_fixed.h" +#include "../info.h" +#include "../p_local.h" +#include "../r_main.h" +#include "../tables.h" + +/* An object may not be visible on the same tic: + 1) that it spawned + 2) that it cycles to the next state */ +#define BUFFER_TICS (2) + +#define rebound_target(o) ((o)->target) +#define rebound_mode(o) ((o)->threshold) +#define rebound_timer(o) ((o)->reactiontime) + +namespace +{ + +constexpr int kReboundSpeed = 128; +constexpr int kReboundAcceptPause = 17; +constexpr int kReboundAcceptDuration = 8; +constexpr int kOrbitRadius = 0; + +enum class Mode : int +{ + kAlpha = 0, + kBeta, + kOrbit, +}; + +fixed_t z_center(const mobj_t* mobj) +{ + return mobj->z + (mobj->height / 2); +} + +bool rebound_is_returning(const mobj_t* mobj) +{ + return mobj->state == &states[S_GACHABOM_RETURNING]; +} + +fixed_t distance_to_target(const mobj_t* mobj) +{ + const mobj_t* target = rebound_target(mobj); + const fixed_t zDelta = z_center(target) - mobj->z; + + return FixedHypot(FixedHypot(target->x - mobj->x, target->y - mobj->y), zDelta); +} + +bool award_target(mobj_t* mobj) +{ + mobj_t* target = rebound_target(mobj); + player_t* player = target->player; + + if (mobj->fuse) + { + return false; + } + + if (player == nullptr) + { + return true; + } + + if ((player->itemtype == KITEM_GACHABOM || player->itemtype == KITEM_NONE) && !player->itemRoulette.active) + { + rebound_timer(mobj)--; + + if (rebound_timer(mobj) < 1) + { + player->itemtype = KITEM_GACHABOM; + player->itemamount++; + + return true; + } + } + + return false; +} + +void chase_rebound_target(mobj_t* mobj) +{ + const mobj_t* target = rebound_target(mobj); + const fixed_t zDelta = z_center(target) - mobj->z; + const fixed_t distance = distance_to_target(mobj); + const fixed_t travelDistance = kReboundSpeed * mapobjectscale; + + if (distance <= travelDistance) + { + rebound_mode(mobj) = static_cast(Mode::kOrbit); + + // Freeze + mobj->momx = 0; + mobj->momy = 0; + mobj->momz = 0; + } + else + { + const angle_t facing = R_PointToAngle2(mobj->x, mobj->y, target->x, target->y); + + P_InstaThrust(mobj, facing, travelDistance); + mobj->angle = facing; + + // This has a nice effect of "jumping up" rather quickly + mobj->momz = zDelta / 4; + + const tic_t t = distance_to_target(mobj) / travelDistance; + const fixed_t newSpeed = std::abs(mobj->scale - mobj->destscale) / std::max(t, 1u); + + if (newSpeed > mobj->scalespeed) + { + mobj->scalespeed = newSpeed; + } + } +} + +void orbit_target(mobj_t* mobj) +{ + if (award_target(mobj)) + { + mobj->fuse = kReboundAcceptDuration + BUFFER_TICS; + mobj->destscale = 0; + mobj->scalespeed = mobj->scale / kReboundAcceptDuration; + } + + const mobj_t* target = rebound_target(mobj); + const fixed_t rad = (2 * (mobj->radius + target->radius)) + (kOrbitRadius * mapobjectscale); + + P_MoveOrigin(mobj, + target->x - FixedMul(FCOS(mobj->angle), rad), + target->y - FixedMul(FSIN(mobj->angle), rad), + target->z + (target->height / 2)); + + constexpr angle_t kOrbitSpeed = ANGLE_MAX / (kReboundAcceptPause + kReboundAcceptDuration); + + mobj->angle -= 2 * kOrbitSpeed; +} + +// Copied from MT_BANANA_SPARK +void squish(mobj_t* mobj) +{ + if (leveltime & 1) + { + mobj->spritexscale = mobj->spriteyscale = FRACUNIT; + } + else + { + if ((leveltime / 2) & 1) + { + mobj->spriteyscale = 3*FRACUNIT; + } + else + { + mobj->spritexscale = 3*FRACUNIT; + } + } +} + +void spawn_afterimages(mobj_t* mobj) +{ + mobj_t* ghost = P_SpawnGhostMobj(mobj); + + // Flickers every frame + ghost->extravalue1 = 1; + ghost->extravalue2 = 2; + + // No transparency + ghost->renderflags = 0; + + ghost->tics = 8; +} + +}; // namespace + +void Obj_GachaBomReboundThink(mobj_t* mobj) +{ + if (P_MobjWasRemoved(rebound_target(mobj))) + { + P_RemoveMobj(mobj); + return; + } + + if (static_cast(rebound_mode(mobj)) == Mode::kOrbit) + { + // Ready to be delivered to the player + orbit_target(mobj); + squish(mobj); + } + else if (rebound_is_returning(mobj)) + { + // Travelling back + chase_rebound_target(mobj); + squish(mobj); + spawn_afterimages(mobj); + } + + // Now the gummy animation is over + if (mobj->sprite == SPR_GBOM) + { + if (static_cast(rebound_mode(mobj)) == Mode::kBeta) + { + // Only the alpha object remains + P_RemoveMobj(mobj); + return; + } + } +} + +void Obj_SpawnGachaBomRebound(mobj_t* source, mobj_t* target) +{ + auto spawn = [&](angle_t angle, Mode mode) + { + mobj_t *x = P_SpawnMobjFromMobjUnscaled(source, 0, 0, target->height / 2, MT_GACHABOM_REBOUND); + + x->color = target->color; + x->angle = angle; + + P_InstaScale(x, 2 * x->scale); + + rebound_mode(x) = static_cast(mode); + rebound_timer(x) = kReboundAcceptPause; + + P_SetTarget(&rebound_target(x), target); + }; + + spawn(0, Mode::kAlpha); + spawn(ANGLE_45, Mode::kBeta); + spawn(ANGLE_90, Mode::kBeta); + spawn(ANGLE_135, Mode::kBeta); +} diff --git a/src/objects/orbinaut.c b/src/objects/orbinaut.c index 6975386d8..e5ba9569d 100644 --- a/src/objects/orbinaut.c +++ b/src/objects/orbinaut.c @@ -277,9 +277,20 @@ boolean Obj_OrbinautJawzCollide(mobj_t *t1, mobj_t *t2) S_StartSound(t1, t1->info->deathsound); P_KillMobj(t1, t2, t2, DMG_NORMAL); - P_SetObjectMomZ(t1, 24*FRACUNIT, false); + if (t1->type == MT_GACHABOM) + { + // Instead of flying out at an angle when + // destroyed, spawn an explosion and eventually + // return to sender. The original Gachabom will be + // removed next tic (see deathstate). + Obj_SpawnGachaBomRebound(t1, orbinaut_owner(t1)); + } + else + { + P_SetObjectMomZ(t1, 24*FRACUNIT, false); - P_InstaThrust(t1, bounceangle, 16*FRACUNIT); + P_InstaThrust(t1, bounceangle, 16*FRACUNIT); + } } if (sprung) diff --git a/src/p_mobj.c b/src/p_mobj.c index 491921adb..160a0b019 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -9522,6 +9522,14 @@ static boolean P_MobjRegularThink(mobj_t *mobj) } } break; + case MT_GACHABOM_REBOUND: + Obj_GachaBomReboundThink(mobj); + + if (P_MobjWasRemoved(mobj)) + { + return false; + } + break; default: // check mobj against possible water content, before movement code P_MobjCheckWater(mobj);