diff --git a/src/k_objects.h b/src/k_objects.h index 85595b4cc..7050ddff4 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -214,6 +214,17 @@ void Obj_EmeraldFlareThink(mobj_t *flare); void Obj_BeginEmeraldOrbit(mobj_t *emerald, mobj_t *target, fixed_t radius, INT32 revolution_time, tic_t fuse); void Obj_GiveEmerald(mobj_t *emerald); +/* Checkpoints */ +void Obj_ResetCheckpoints(void); +void Obj_LinkCheckpoint(mobj_t *end); +void Obj_UnlinkCheckpoint(mobj_t *end); +void Obj_CheckpointThink(mobj_t *end); +void Obj_CrossCheckpoints(player_t *player, fixed_t old_x, fixed_t old_y); +mobj_t *Obj_FindCheckpoint(INT32 id); +boolean Obj_GetCheckpointRespawnPosition(const mobj_t *checkpoint, vector3_t *return_pos); +angle_t Obj_GetCheckpointRespawnAngle(const mobj_t *checkpoint); +void Obj_ActivateCheckpointInstantly(mobj_t* mobj); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index c13c88f4d..c3b01f4a0 100644 --- a/src/objects/CMakeLists.txt +++ b/src/objects/CMakeLists.txt @@ -28,4 +28,5 @@ target_sources(SRB2SDL2 PRIVATE dash-rings.c sneaker-panel.c emerald.c + checkpoint.cpp ) diff --git a/src/objects/checkpoint.cpp b/src/objects/checkpoint.cpp new file mode 100644 index 000000000..903dcd349 --- /dev/null +++ b/src/objects/checkpoint.cpp @@ -0,0 +1,598 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by James Robert Roman +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include +#include + +#include + +#include "../doomdef.h" +#include "../doomtype.h" +#include "../info.h" +#include "../k_color.h" +#include "../k_kart.h" +#include "../k_objects.h" +#include "../m_bbox.h" +#include "../m_fixed.h" +#include "../m_random.h" +#include "../p_local.h" +#include "../p_maputl.h" +#include "../p_mobj.h" +#include "../p_setup.h" +#include "../p_tick.h" +#include "../r_defs.h" +#include "../r_main.h" +#include "../s_sound.h" +#include "../sounds.h" +#include "../tables.h" + +#define checkpoint_id(o) ((o)->thing_args[0]) +#define checkpoint_other(o) ((o)->target) +#define checkpoint_orb(o) ((o)->tracer) +#define checkpoint_arm(o) ((o)->hnext) +#define checkpoint_var(o) ((o)->movedir) +#define checkpoint_speed(o) ((o)->movecount) +#define checkpoint_speed_multiplier(o) ((o)->movefactor) +#define checkpoint_reverse(o) ((o)->reactiontime) + +namespace +{ + +struct LineOnDemand : line_t +{ + LineOnDemand(fixed_t x1, fixed_t y1, fixed_t x2, fixed_t y2) : + line_t { + .v1 = &v1_data_, + .dx = x2 - x1, + .dy = y2 - y1, + .bbox = {std::max(y1, y2), std::min(y1, y2), std::min(x1, x2), std::max(x1, x2)}, + }, + v1_data_ {.x = x1, .y = y1} + { + } + + LineOnDemand(fixed_t x1, fixed_t y1, fixed_t x2, fixed_t y2, fixed_t r) : LineOnDemand(x1, y1, x2, y2) + { + bbox[BOXTOP] += r; + bbox[BOXBOTTOM] -= r; + bbox[BOXLEFT] -= r; + bbox[BOXRIGHT] += r; + } + + bool overlaps(const LineOnDemand& other) const + { + return bbox[BOXTOP] >= other.bbox[BOXBOTTOM] && bbox[BOXBOTTOM] <= other.bbox[BOXTOP] && + bbox[BOXLEFT] <= other.bbox[BOXRIGHT] && bbox[BOXRIGHT] >= other.bbox[BOXLEFT]; + } + +private: + vertex_t v1_data_; +}; + +struct Checkpoint : mobj_t +{ + static constexpr int kArmLength = 59; + static constexpr int kOrbRadius = 21; + static constexpr int kCookieRadius = 19; + static constexpr int kOrbitDistance = kCookieRadius + kArmLength + kOrbRadius; + static constexpr fixed_t kBaseSpeed = FRACUNIT/35; + static constexpr fixed_t kMinSpeedMultiplier = FRACUNIT/2; + static constexpr fixed_t kMaxSpeedMultiplier = 3*FRACUNIT/2; + static constexpr fixed_t kSpeedMultiplierRange = kMaxSpeedMultiplier - kMinSpeedMultiplier; + static constexpr fixed_t kMinPivotDelay = FRACUNIT/2; + static constexpr int kSparkleOffset = 10; + static constexpr int kSparkleZ = 34; + static constexpr int kSparkleRadius = 12; + static constexpr int kSparkleAroundRadius = 128; + static constexpr int kSparkleAroundCircumfrence = kSparkleAroundRadius * M_TAU_FIXED; + static constexpr int kSparkleAroundCount = kSparkleAroundCircumfrence / kSparkleRadius / FRACUNIT; + + struct Orb : mobj_t + { + void afterimages() + { + mobj_t* ghost = P_SpawnGhostMobj(this); + + // Flickers every frame + ghost->extravalue1 = 1; + ghost->extravalue2 = 2; + + ghost->tics = 8; + } + }; + + struct Arm : mobj_t {}; + + INT32 id() const { return checkpoint_id(this); } + + Checkpoint* other() const { return static_cast(checkpoint_other(this)); } + void other(Checkpoint* n) { P_SetTarget(&checkpoint_other(this), n); } + + Orb* orb() const { return static_cast(checkpoint_orb(this)); } + void orb(Orb* n) { P_SetTarget(&checkpoint_orb(this), n); } + + Arm* arm() const { return static_cast(checkpoint_arm(this)); } + void arm(Arm* n) { P_SetTarget(&checkpoint_arm(this), n); } + + fixed_t var() const { return checkpoint_var(this); } + void var(fixed_t n) { checkpoint_var(this) = n; } + + fixed_t speed() const { return checkpoint_speed(this); } + void speed(fixed_t n) { checkpoint_speed(this) = n; } + + fixed_t speed_multiplier() const { return checkpoint_speed_multiplier(this); } + void speed_multiplier(fixed_t n) { checkpoint_speed_multiplier(this) = n; } + + bool reverse() const { return checkpoint_reverse(this); } + void reverse(bool n) { checkpoint_reverse(this) = n; } + + // Valid to use as an alpha. + bool valid() const + { + auto f = [](const mobj_t* th) { return !P_MobjWasRemoved(th); }; + return f(this) && f(other()) && f(orb()) && f(arm()); + } + + bool activated() const { return var(); } + + // Line between A and B things. + LineOnDemand crossing_line() const { return LineOnDemand(x, y, other()->x, other()->y, radius); } + + // Middle between A and B. + vector3_t center_position() const + { + return {x + ((other()->x - x) / 2), y + ((other()->y - y) / 2), z + ((other()->z - z) / 2)}; + } + + void gingerbread() + { + P_InstaScale(this, 3 * scale / 2); + + orb(new_piece(S_CHECKPOINT_ORB_DEAD)); + orb()->whiteshadow = true; + + arm(new_piece(S_CHECKPOINT_ARM)); + + deactivate(); + } + + void animate() + { + orient(); + pull(); + + if (speed()) + { + var(var() + speed()); + + if (!clip_var()) + { + speed(speed() - FixedDiv(speed() / 50, std::max(speed_multiplier(), 1))); + } + } + else if (!activated()) + { + sparkle_between(0); + } + } + + void twirl(angle_t dir, fixed_t multiplier) + { + var(0); + speed_multiplier(std::clamp(multiplier, kMinSpeedMultiplier, kMaxSpeedMultiplier)); + speed(FixedDiv(kBaseSpeed, speed_multiplier())); + reverse(AngleDeltaSigned(angle_to_other(), dir) > 0); + + sparkle_between(FixedMul(80 * mapobjectscale, multiplier)); + } + + void untwirl() + { + speed_multiplier(kMinSpeedMultiplier); + speed(FixedDiv(-(kBaseSpeed), speed_multiplier())); + } + + void activate() + { + var(FRACUNIT - 1); + speed(0); + P_SetMobjState(orb(), S_CHECKPOINT_ORB_LIVE); + orb()->shadowscale = 0; + } + + void deactivate() + { + var(0); + speed(0); + P_SetMobjState(orb(), S_CHECKPOINT_ORB_DEAD); + orb()->shadowscale = FRACUNIT/2; + } + + void sparkle_around_center() + { + const vector3_t pos = center_position(); + + fixed_t mom = 5 * scale; + + for (angle_t a = 0;;) + { + spawn_sparkle({pos.x + FixedMul(mom, FCOS(a)), pos.y + FixedMul(mom, FSIN(a)), pos.z}, mom, 20 * scale, a); + + angle_t turn = a + (ANGLE_MAX / kSparkleAroundCount); + + if (turn < a) // overflowed a full 360 degrees + { + break; + } + + a = turn; + } + } + +private: + static angle_t to_angle(fixed_t f) { return FixedAngle((f & FRACMASK) * 360); } + + template + T* new_piece(statenum_t state) + { + mobj_t* x = P_SpawnMobjFromMobj(this, 0, 0, 0, MT_THOK); + + P_SetMobjState(x, state); + + return static_cast(x); + } + + angle_t angle_to_other() const { return R_PointToAngle2(x, y, other()->x, other()->y); } + angle_t facing_angle() const { return angle_to_other() + ANGLE_90; } + + angle_t pivot() const + { + fixed_t pos = FixedMul( + FixedDiv(speed_multiplier() - kMinSpeedMultiplier, kSpeedMultiplierRange), + kMinPivotDelay + ); + + return to_angle(FixedDiv(std::max(var(), pos) - pos, FRACUNIT - pos)) / 4; + } + + void orient() + { + angle_t facing = facing_angle(); + + if (speed() >= 0) + { + fixed_t range = FRACUNIT + FixedRound((speed_multiplier() - kMinSpeedMultiplier) * 6); + + angle = facing + (to_angle(FixedMul(var(), range)) * (reverse() ? -1 : 1)); + } + + arm()->angle = angle - ANGLE_90; + arm()->rollangle = -(ANGLE_90) + pivot(); + } + + void pull() + { + fixed_t r = kOrbitDistance * scale; + fixed_t xy = FixedMul(r, FCOS(pivot())); + + P_MoveOrigin( + orb(), + x + FixedMul(xy, FCOS(arm()->angle)), + y + FixedMul(xy, FSIN(arm()->angle)), + P_GetMobjHead(this) + (FixedMul(r, FSIN(pivot())) * P_MobjFlip(this)) + ); + + P_MoveOrigin(arm(), orb()->x, orb()->y, orb()->z); + + if (speed()) + { + orb()->afterimages(); + } + } + + void spawn_sparkle(const vector3_t& pos, fixed_t xy_momentum, fixed_t z_momentum, angle_t dir) + { + auto rng = [=](int units) { return P_RandomRange(PR_DECORATION, -(units) * scale, +(units) * scale); }; + + // From K_DrawDraftCombiring + mobj_t* p = P_SpawnMobjFromMobjUnscaled( + this, + (pos.x - x) + rng(12), + (pos.y - y) + rng(12), + (pos.z - z) + rng(24), + MT_SIGNSPARKLE + ); + + P_SetMobjState(p, static_cast(S_CHECKPOINT_SPARK1 + (leveltime % 11))); + + p->colorized = true; + + if (xy_momentum) + { + P_Thrust(p, dir, xy_momentum); + p->momz = P_RandomKey(PR_DECORATION, std::max(z_momentum, 1)); + p->destscale = 0; + p->scalespeed = p->scale / 35; + p->color = SKINCOLOR_ULTRAMARINE; + p->fuse = 0; + + // Something lags at the start of the level. The + // timing is inconsistent, so this value is + // vibes-based. + constexpr int kIntroDelay = 8; + + if (leveltime < kIntroDelay) + { + p->hitlag = kIntroDelay; + } + } + else + { + p->color = K_RainbowColor(leveltime); + p->fuse = 2; + } + } + + void sparkle_between(fixed_t momentum) + { + angle_t a = angle_to_other(); + + if (a < ANGLE_180) + { + // Let's only do it for one of the two. + return; + } + + angle_t dir = a - (reverse() ? ANGLE_90 : -(ANGLE_90)); + + fixed_t r = kSparkleRadius * scale; + fixed_t ofs = (kSparkleOffset * scale) + r; + fixed_t between = R_PointToDist2(x, y, other()->x, other()->y); + + for (; ofs < between; ofs += 2 * r) + { + spawn_sparkle( + {x + FixedMul(ofs, FCOS(a)), y + FixedMul(ofs, FSIN(a)), z + (kSparkleZ * scale)}, + momentum, + momentum / 2, + dir + ); + } + } + + bool clip_var() + { + if (speed() > 0) + { + if (var() >= FRACUNIT) + { + activate(); + return true; + } + } + else + { + if (var() < 0) + { + deactivate(); + return true; + } + } + + return false; + } +}; + +struct CheckpointManager +{ + auto begin() { return vec_.begin(); } + auto end() { return vec_.end(); } + + auto find(INT32 id) { return std::find_if(begin(), end(), [id](Checkpoint* chk) { return chk->id() == id; }); } + + void push_back(Checkpoint* chk) { vec_.push_back(chk); } + + void erase(const Checkpoint* chk) + { + if (auto it = std::find(vec_.begin(), vec_.end(), chk); it != end()) + { + vec_.erase(it); + } + } + +private: + std::vector vec_; +}; + +CheckpointManager g_checkpoints; + +}; // namespace + +void Obj_ResetCheckpoints(void) +{ + g_checkpoints = {}; +} + +void Obj_LinkCheckpoint(mobj_t* end) +{ + auto chk = static_cast(end); + + if (chk->spawnpoint && chk->id() == 0) + { + auto msg = fmt::format( + "Checkpoint thing (index #{}, thing type {}) has an invalid ID! ID must not be 0.\n", + chk->spawnpoint - mapthings, + chk->spawnpoint->type + ); + CONS_Alert(CONS_WARNING, msg.c_str()); + return; + } + + if (auto it = g_checkpoints.find(chk->id()); it != g_checkpoints.end()) + { + Checkpoint* other = *it; + + if (chk->spawnpoint && other->spawnpoint && chk->spawnpoint->angle != other->spawnpoint->angle) + { + auto msg = fmt::format( + "Checkpoints things with ID {} (index #{} and #{}, thing type {}) do not have matching angles.\n", + chk->id(), + chk->spawnpoint - mapthings, + other->spawnpoint - mapthings, + chk->spawnpoint->type + ); + CONS_Alert(CONS_WARNING, msg.c_str()); + return; + } + + other->other(chk); + chk->other(other); + } + else + { + g_checkpoints.push_back(chk); + } + + chk->gingerbread(); +} + +void Obj_UnlinkCheckpoint(mobj_t* end) +{ + auto chk = static_cast(end); + + g_checkpoints.erase(chk); + + P_RemoveMobj(chk->orb()); +} + +void Obj_CheckpointThink(mobj_t* end) +{ + auto chk = static_cast(end); + + if (!chk->valid()) + { + return; + } + + chk->animate(); +} + +void Obj_CrossCheckpoints(player_t* player, fixed_t old_x, fixed_t old_y) +{ + LineOnDemand ray(old_x, old_y, player->mo->x, player->mo->y, player->mo->radius); + + auto it = std::find_if( + g_checkpoints.begin(), + g_checkpoints.end(), + [&](const Checkpoint* chk) + { + if (!chk->valid()) + { + return false; + } + + LineOnDemand gate = chk->crossing_line(); + + // Check if the bounding boxes of the two lines + // overlap. This relies on the player movement not + // being so large that it creates an oversized box, + // but thankfully that doesn't seem to happen, under + // normal circumstances. + if (!ray.overlaps(gate)) + { + return false; + } + + INT32 side = P_PointOnLineSide(player->mo->x, player->mo->y, &gate); + INT32 oldside = P_PointOnLineSide(old_x, old_y, &gate); + + if (side == oldside) + { + // Did not cross. + return false; + } + + return true; + } + ); + + if (it == g_checkpoints.end()) + { + return; + } + + Checkpoint* chk = *it; + + if (chk->activated()) + { + return; + } + + for (Checkpoint* chk : g_checkpoints) + { + if (chk->valid()) + { + // Swing down any previously passed checkpoints. + // TODO: this could look weird in multiplayer if + // other players cross different checkpoints. + chk->untwirl(); + chk->other()->untwirl(); + } + } + + angle_t direction = R_PointToAngle2(old_x, old_y, player->mo->x, player->mo->y); + fixed_t speed_multiplier = FixedDiv(player->speed, K_GetKartSpeed(player, false, false)); + + chk->twirl(direction, speed_multiplier); + chk->other()->twirl(direction, speed_multiplier); + + S_StartSound(player->mo, sfx_s3k63); + + player->checkpointId = chk->id(); +} + +mobj_t *Obj_FindCheckpoint(INT32 id) +{ + auto it = g_checkpoints.find(id); + + return it != g_checkpoints.end() ? *it : nullptr; +} + +boolean Obj_GetCheckpointRespawnPosition(const mobj_t* mobj, vector3_t* return_pos) +{ + auto chk = static_cast(mobj); + + if (!chk->valid()) + { + return false; + } + + *return_pos = chk->center_position(); + + return true; +} + +angle_t Obj_GetCheckpointRespawnAngle(const mobj_t* mobj) +{ + auto chk = static_cast(mobj); + + return chk->spawnpoint ? FixedAngle(chk->spawnpoint->angle * FRACUNIT): 0u; +} + +void Obj_ActivateCheckpointInstantly(mobj_t* mobj) +{ + auto chk = static_cast(mobj); + + if (chk->valid()) + { + chk->sparkle_around_center(); // only do it for one + chk->activate(); + chk->other()->activate(); + } +} diff --git a/src/p_map.c b/src/p_map.c index bafc67d2e..92a2a65d4 100644 --- a/src/p_map.c +++ b/src/p_map.c @@ -3088,6 +3088,16 @@ boolean P_TryMove(mobj_t *thing, fixed_t x, fixed_t y, boolean allowdropoff, Try P_CrossSpecialLine(ld, oldside, thing); } } + + // Currently this just iterates all checkpoints. + // Pretty shitty way to do it, but only players can + // cross it, so it's good enough. Works as long as the + // move doesn't cross multiple -- it can only evaluate + // one. + if (thing->player) + { + Obj_CrossCheckpoints(thing->player, oldx, oldy); + } } if (result != NULL) diff --git a/src/p_mobj.c b/src/p_mobj.c index c03ca8070..767060313 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -6728,6 +6728,9 @@ static void P_MobjSceneryThink(mobj_t *mobj) case MT_ARKARROW: Obj_ArkArrowThink(mobj); break; + case MT_CHECKPOINT_END: + Obj_CheckpointThink(mobj); + break; case MT_SCRIPT_THING: { if (mobj->thing_args[2] != 0) @@ -10416,6 +10419,7 @@ static void P_DefaultMobjShadowScale(mobj_t *thing) case MT_CDUFO: case MT_BATTLEUFO: case MT_SPRAYCAN: + case MT_CHECKPOINT_END: thing->shadowscale = FRACUNIT; break; case MT_SMALLMACE: @@ -11293,6 +11297,11 @@ void P_RemoveMobj(mobj_t *mobj) Obj_UnlinkBattleUFOSpawner(mobj); break; } + case MT_CHECKPOINT_END: + { + Obj_UnlinkCheckpoint(mobj); + break; + } default: { break; @@ -13633,6 +13642,11 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj) Obj_SneakerPanelSpawnerSetup(mobj, mthing); break; } + case MT_CHECKPOINT_END: + { + Obj_LinkCheckpoint(mobj); + break; + } default: break; } diff --git a/src/p_tick.c b/src/p_tick.c index cc0659dba..737ff9ba2 100644 --- a/src/p_tick.c +++ b/src/p_tick.c @@ -236,6 +236,7 @@ void P_InitThinkers(void) } Obj_ResetUFOSpawners(); + Obj_ResetCheckpoints(); } // Adds a new thinker at the end of the list.