From 02b6ac0b709a8d431fe3cfc0dd4f7f40e6afbf9f Mon Sep 17 00:00:00 2001 From: James R Date: Wed, 30 Aug 2023 21:05:44 -0700 Subject: [PATCH 1/6] Checkpoints: add states Duplicating S_SIGNSPARK1-11 because FUCK. --- src/deh_tables.c | 17 +++++++++++++++++ src/info.c | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/info.h | 21 +++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/src/deh_tables.c b/src/deh_tables.c index 0eed55fcf..f9da162ac 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -4653,6 +4653,22 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi "S_BATTLEUFO_BEAM2", "S_POWERUP_AURA", + + "S_CHECKPOINT", + "S_CHECKPOINT_ARM", + "S_CHECKPOINT_ORB_DEAD", + "S_CHECKPOINT_ORB_LIVE", + "S_CHECKPOINT_SPARK1", + "S_CHECKPOINT_SPARK2", + "S_CHECKPOINT_SPARK3", + "S_CHECKPOINT_SPARK4", + "S_CHECKPOINT_SPARK5", + "S_CHECKPOINT_SPARK6", + "S_CHECKPOINT_SPARK7", + "S_CHECKPOINT_SPARK8", + "S_CHECKPOINT_SPARK9", + "S_CHECKPOINT_SPARK10", + "S_CHECKPOINT_SPARK11", }; // RegEx to generate this from info.h: ^\tMT_([^,]+), --> \t"MT_\1", @@ -5803,6 +5819,7 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_POWERUP_AURA", + "MT_CHECKPOINT_END", "MT_SCRIPT_THING", }; diff --git a/src/info.c b/src/info.c index 1d2e52eff..2343b81e3 100644 --- a/src/info.c +++ b/src/info.c @@ -888,6 +888,10 @@ char sprnames[NUMSPRITES + 1][5] = "BUFO", // Battle/Power-UP UFO + "CPT1", // Checkpoint Orb + "CPT2", // Checkpoint Stick + "CPT3", // Checkpoint Base + // First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later "VIEW", }; @@ -5394,6 +5398,22 @@ state_t states[NUMSTATES] = {SPR_DEZL, 3|FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_NULL}, // S_BATTLEUFO_BEAM2 {SPR_RBOW, FF_PAPERSPRITE|FF_ADD|FF_FULLBRIGHT|FF_ANIMATE, -1, {NULL}, 14, 2, S_NULL}, // S_POWERUP_AURA + + {SPR_CPT3, 0, -1, {NULL}, 0, 0, S_NULL}, // S_CHECKPOINT + {SPR_CPT2, FF_PAPERSPRITE, -1, {NULL}, 0, 0, S_NULL}, // S_CHECKPOINT_ARM + {SPR_CPT1, FF_ADD|FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_NULL}, // S_CHECKPOINT_ORB_DEAD + {SPR_CPT1, FF_ADD|FF_FULLBRIGHT|FF_ANIMATE, -1, {NULL}, 2, 1, S_NULL}, // S_CHECKPOINT_ORB_LIVE + {SPR_SGNS, FF_ADD|FF_FULLBRIGHT, 1, {NULL}, 0, 0, S_CHECKPOINT_SPARK2}, // S_CHECKPOINT_SPARK1 + {SPR_SGNS, FF_ADD|FF_FULLBRIGHT|1, 1, {NULL}, 0, 0, S_CHECKPOINT_SPARK3}, // S_CHECKPOINT_SPARK2 + {SPR_SGNS, FF_ADD|FF_FULLBRIGHT|2, 1, {NULL}, 0, 0, S_CHECKPOINT_SPARK4}, // S_CHECKPOINT_SPARK3 + {SPR_SGNS, FF_ADD|FF_FULLBRIGHT|3, 1, {NULL}, 0, 0, S_CHECKPOINT_SPARK5}, // S_CHECKPOINT_SPARK4 + {SPR_SGNS, FF_ADD|FF_FULLBRIGHT|4, 1, {NULL}, 0, 0, S_CHECKPOINT_SPARK6}, // S_CHECKPOINT_SPARK5 + {SPR_SGNS, FF_ADD|FF_FULLBRIGHT|5, 1, {NULL}, 0, 0, S_CHECKPOINT_SPARK7}, // S_CHECKPOINT_SPARK6 + {SPR_SGNS, FF_ADD|FF_FULLBRIGHT|6, 1, {NULL}, 0, 0, S_CHECKPOINT_SPARK8}, // S_CHECKPOINT_SPARK7 + {SPR_SGNS, FF_ADD|FF_FULLBRIGHT|7, 1, {NULL}, 0, 0, S_CHECKPOINT_SPARK9}, // S_CHECKPOINT_SPARK8 + {SPR_SGNS, FF_ADD|FF_FULLBRIGHT|8, 1, {NULL}, 0, 0, S_CHECKPOINT_SPARK10}, // S_CHECKPOINT_SPARK9 + {SPR_SGNS, FF_ADD|FF_FULLBRIGHT|3, 1, {NULL}, 0, 0, S_CHECKPOINT_SPARK11}, // S_CHECKPOINT_SPARK10 + {SPR_SGNS, FF_ADD|FF_FULLBRIGHT|2, 1, {NULL}, 0, 0, S_CHECKPOINT_SPARK1}, // S_CHECKPOINT_SPARK11 }; mobjinfo_t mobjinfo[NUMMOBJTYPES] = @@ -30305,6 +30325,33 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = S_NULL // raisestate }, + { // MT_CHECKPOINT_END + 2030, // doomednum + S_CHECKPOINT, // spawnstate + 1000, // spawnhealth + S_NULL, // seestate + sfx_None, // seesound + 0, // reactiontime + sfx_None, // attacksound + S_NULL, // painstate + 0, // painchance + sfx_None, // painsound + S_NULL, // meleestate + S_NULL, // missilestate + S_NULL, // deathstate + S_NULL, // xdeathstate + sfx_None, // deathsound + 0, // speed + 19*FRACUNIT, // radius + 75*FRACUNIT, // height + 0, // display offset + 0, // mass + 0, // damage + sfx_None, // activesound + MF_NOBLOCKMAP|MF_NOGRAVITY|MF_NOCLIPHEIGHT|MF_SCENERY|MF_DONTENCOREMAP, // flags + S_NULL // raisestate + }, + { // MT_SCRIPT_THING 4096, // doomednum S_INVISIBLE, // spawnstate diff --git a/src/info.h b/src/info.h index fd41a7744..ef47c05bc 100644 --- a/src/info.h +++ b/src/info.h @@ -1442,6 +1442,10 @@ typedef enum sprite SPR_BUFO, // Battle/Power-UP UFO + SPR_CPT1, // Checkpoint Orb + SPR_CPT2, // Checkpoint Stick + SPR_CPT3, // Checkpoint Base + // First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later SPR_VIEW, @@ -5823,6 +5827,22 @@ typedef enum state S_POWERUP_AURA, + S_CHECKPOINT, + S_CHECKPOINT_ARM, + S_CHECKPOINT_ORB_DEAD, + S_CHECKPOINT_ORB_LIVE, + S_CHECKPOINT_SPARK1, + S_CHECKPOINT_SPARK2, + S_CHECKPOINT_SPARK3, + S_CHECKPOINT_SPARK4, + S_CHECKPOINT_SPARK5, + S_CHECKPOINT_SPARK6, + S_CHECKPOINT_SPARK7, + S_CHECKPOINT_SPARK8, + S_CHECKPOINT_SPARK9, + S_CHECKPOINT_SPARK10, + S_CHECKPOINT_SPARK11, + S_FIRSTFREESLOT, S_LASTFREESLOT = S_FIRSTFREESLOT + NUMSTATEFREESLOTS - 1, NUMSTATES @@ -6991,6 +7011,7 @@ typedef enum mobj_type MT_POWERUP_AURA, + MT_CHECKPOINT_END, MT_SCRIPT_THING, MT_FIRSTFREESLOT, From 11fee625f5dac777c46a577062356f1fa1285743 Mon Sep 17 00:00:00 2001 From: James R Date: Wed, 30 Aug 2023 21:08:11 -0700 Subject: [PATCH 2/6] P_MobjFlip, P_MobjWasRemoved: let mobj be const --- src/p_local.h | 4 ++-- src/p_mobj.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/p_local.h b/src/p_local.h index 0fff3a78d..4fab4fa74 100644 --- a/src/p_local.h +++ b/src/p_local.h @@ -263,7 +263,7 @@ void P_RecalcPrecipInSector(sector_t *sector); void P_PrecipitationEffects(void); void P_RemoveMobj(mobj_t *th); -boolean P_MobjWasRemoved(mobj_t *th); +boolean P_MobjWasRemoved(const mobj_t *th); void P_RemoveSavegameMobj(mobj_t *th); boolean P_SetPlayerMobjState(mobj_t *mobj, statenum_t state); boolean P_SetMobjState(mobj_t *mobj, statenum_t state); @@ -313,7 +313,7 @@ mobj_t *P_SPMAngle(mobj_t *source, mobjtype_t type, angle_t angle, UINT8 aimtype #define P_SpawnPlayerMissile(s,t,f) P_SPMAngle(s,t,s->angle,true,f) #define P_SpawnNameFinder(s,t) P_SPMAngle(s,t,s->angle,true,0) void P_ColorTeamMissile(mobj_t *missile, player_t *source); -SINT8 P_MobjFlip(mobj_t *mobj); +SINT8 P_MobjFlip(const mobj_t *mobj); fixed_t P_GetMobjGravity(mobj_t *mo); void P_CalcChasePostImg(player_t *player, camera_t *thiscam); diff --git a/src/p_mobj.c b/src/p_mobj.c index dad3be9d8..c03ca8070 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -567,7 +567,7 @@ static boolean P_SetPrecipMobjState(precipmobj_t *mobj, statenum_t state) // // Special utility to return +1 or -1 depending on mobj's gravity // -SINT8 P_MobjFlip(mobj_t *mobj) +SINT8 P_MobjFlip(const mobj_t *mobj) { if (mobj && mobj->eflags & MFE_VERTICALFLIP) return -1; @@ -11379,7 +11379,7 @@ void P_RemoveMobj(mobj_t *mobj) // This does not need to be added to Lua. // To test it in Lua, check mobj.valid -boolean P_MobjWasRemoved(mobj_t *mobj) +boolean P_MobjWasRemoved(const mobj_t *mobj) { if (mobj && mobj->thinker.function.acp1 == (actionf_p1)P_MobjThinker) return false; From e83923a365bba74aa9aa386cadb1f6ba7bdab7ab Mon Sep 17 00:00:00 2001 From: James R Date: Wed, 30 Aug 2023 21:15:40 -0700 Subject: [PATCH 3/6] Checkpoints: add object configuration, collision, animations This commit handles everything except actually respawning the player at a checkpoint. - Checkpoints are formed by two checkpoint things (2030): - thingarg0 - The ID for the checkpoint. Must be the same for these two things, and these two things only. ID cannot be 0. - angle - The direction the player is intended to face after respawning. Must be the same for both things. - Each checkpoint thing is a starpost with a stick and an orb at the end. - By default, the sticks are lowered to horizontal and face toward the opposite starpost. - Rainbow tether sparkles form a field between the two starposts. - When a player crosses between these two starposts, each spins in the direction that the player crossed. The sparkles also fly out in that direction. - Over time the sticks pivot upward. - When the starposts are done spinning, the sticks will be pointing straight upward. - Orb at the end of the stick begins flashing when the starpost is done spinnning. - Players may cross multiple checkpoints. - When this happens, any previously activated checkpoint will have its stick lowered back to horizontal, and its orb will stop flashing. --- src/k_objects.h | 11 + src/objects/CMakeLists.txt | 1 + src/objects/checkpoint.cpp | 598 +++++++++++++++++++++++++++++++++++++ src/p_map.c | 10 + src/p_mobj.c | 14 + src/p_tick.c | 1 + 6 files changed, 635 insertions(+) create mode 100644 src/objects/checkpoint.cpp 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. From 2bbf69ded34027b7f2d719f0ad9ff05c3271cbef Mon Sep 17 00:00:00 2001 From: James R Date: Wed, 30 Aug 2023 21:25:58 -0700 Subject: [PATCH 4/6] Checkpoints: add respawning behavior for players - Add checkpointId to player_t and netsave - checkpointId persists if the map is reloaded, but not if the map is changed or if "resetplayers" is ticked - Players spawn at the last touched checkpoint instead of at a player start - Tether sparkles fly out of the player in a circle --- src/d_player.h | 1 + src/g_game.c | 46 ++++++++++++++++++++++++++++++++++++++-------- src/p_saveg.c | 2 ++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/d_player.h b/src/d_player.h index 5e57cfc92..6a3598def 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -742,6 +742,7 @@ struct player_t UINT8 latestlap; UINT32 lapPoints; // Points given from laps INT32 cheatchecknum; // The number of the last cheatcheck you hit + INT32 checkpointId; // Players respawn here, objects/checkpoint.cpp UINT8 ctfteam; // 0 == Spectator, 1 == Red, 2 == Blue diff --git a/src/g_game.c b/src/g_game.c index ac05c0b6f..56ad5149b 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -71,6 +71,7 @@ #include "k_zvote.h" #include "music.h" #include "k_roulette.h" +#include "k_objects.h" #ifdef HAVE_DISCORDRPC #include "discord.h" @@ -2044,6 +2045,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) UINT16 nocontrol; INT32 khudfault; INT32 kickstartaccel; + INT32 checkpointId; boolean enteredGame; roundconditions_t roundconditions; @@ -2249,6 +2251,8 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) skyboxviewpoint = skyboxcenterpoint = NULL; } + checkpointId = players[player].checkpointId; + enteredGame = players[player].enteredGame; p = &players[player]; @@ -2305,6 +2309,7 @@ void G_PlayerReborn(INT32 player, boolean betweenmaps) p->karthud[khud_fault] = khudfault; p->nocontrol = nocontrol; p->kickstartaccel = kickstartaccel; + p->checkpointId = checkpointId; p->ringvolume = 255; @@ -2453,19 +2458,39 @@ void G_SpawnPlayer(INT32 playernum) void G_MovePlayerToSpawnOrCheatcheck(INT32 playernum) { -#if 0 - if (leveltime <= introtime && !players[playernum].spectator) - P_MovePlayerToSpawn(playernum, G_FindMapStart(playernum)); - else - P_MovePlayerToCheatcheck(playernum); -#else // Player's first spawn should be at the "map start". // I.e. level load or join mid game. if (leveltime > starttime && players[playernum].jointime > 1 && K_PodiumSequence() == false) + { P_MovePlayerToCheatcheck(playernum); + } else - P_MovePlayerToSpawn(playernum, G_FindMapStart(playernum)); -#endif + { + mobj_t *checkpoint; + vector3_t pos; + + if (players[playernum].checkpointId && + (checkpoint = Obj_FindCheckpoint(players[playernum].checkpointId)) && + Obj_GetCheckpointRespawnPosition(checkpoint, &pos)) + { + respawnvars_t *rsp = &players[playernum].respawn; + + rsp->wp = NULL; + rsp->pointx = pos.x; + rsp->pointy = pos.y; + rsp->pointz = pos.z; + + players[playernum].mo->angle = Obj_GetCheckpointRespawnAngle(checkpoint); + + Obj_ActivateCheckpointInstantly(checkpoint); + + P_MovePlayerToCheatcheck(playernum); + } + else + { + P_MovePlayerToSpawn(playernum, G_FindMapStart(playernum)); + } + } } mapthing_t *G_FindTeamStart(INT32 playernum) @@ -5669,6 +5694,11 @@ void G_InitNew(UINT8 pencoremode, INT32 map, boolean resetplayer, boolean skippr players[i].totalring = 0; players[i].score = 0; } + + if (resetplayer || map != gamemap) + { + players[i].checkpointId = 0; + } } // clear itemfinder, just in case diff --git a/src/p_saveg.c b/src/p_saveg.c index 48bbc31ed..c3db3804c 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -267,6 +267,7 @@ static void P_NetArchivePlayers(savebuffer_t *save) WRITEUINT8(save->p, players[i].latestlap); WRITEUINT32(save->p, players[i].lapPoints); WRITEINT32(save->p, players[i].cheatchecknum); + WRITEINT32(save->p, players[i].checkpointId); WRITEUINT8(save->p, players[i].ctfteam); @@ -777,6 +778,7 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) players[i].latestlap = READUINT8(save->p); players[i].lapPoints = READUINT32(save->p); players[i].cheatchecknum = READINT32(save->p); + players[i].checkpointId = READINT32(save->p); players[i].ctfteam = READUINT8(save->p); // 1 == Red, 2 == Blue From 467b5f831b4645bc8dd2db9882fb3748da0207ec Mon Sep 17 00:00:00 2001 From: toaster Date: Sat, 16 Sep 2023 23:22:59 +0100 Subject: [PATCH 5/6] Add GTR_CHECKPOINTS - Prevents Checkpoint from spawning - Prevents spawnpoint-handling code from occouring --- src/deh_tables.c | 1 + src/doomstat.h | 31 ++++++++++++++++--------------- src/g_game.c | 9 +++++---- src/p_mobj.c | 4 ++++ 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/deh_tables.c b/src/deh_tables.c index f9da162ac..f4112f614 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -5986,6 +5986,7 @@ const char *const GAMETYPERULE_LIST[] = { "KARMA", "ITEMARROWS", + "CHECKPOINTS", "PRISONS", "CATCHER", "ROLLINGSTART", diff --git a/src/doomstat.h b/src/doomstat.h index 7b519819a..3dd1eb1a4 100644 --- a/src/doomstat.h +++ b/src/doomstat.h @@ -622,25 +622,26 @@ enum GameTypeRules GTR_ITEMARROWS = 1<<9, // Show item box arrows above players // Bonus gametype rules - GTR_PRISONS = 1<<10, // Can enter Prison Break mode - GTR_CATCHER = 1<<11, // UFO Catcher (only works with GTR_CIRCUIT) - GTR_ROLLINGSTART = 1<<12, // Rolling start (only works with GTR_CIRCUIT) - GTR_SPECIALSTART = 1<<13, // White fade instant start - GTR_BOSS = 1<<14, // Boss intro and spawning + GTR_CHECKPOINTS = 1<<10, // Player respawns at specific checkpoints + GTR_PRISONS = 1<<11, // Can enter Prison Break mode + GTR_CATCHER = 1<<12, // UFO Catcher (only works with GTR_CIRCUIT) + GTR_ROLLINGSTART = 1<<13, // Rolling start (only works with GTR_CIRCUIT) + GTR_SPECIALSTART = 1<<14, // White fade instant start + GTR_BOSS = 1<<15, // Boss intro and spawning // General purpose rules - GTR_POINTLIMIT = 1<<15, // Reaching point limit ends the round - GTR_TIMELIMIT = 1<<16, // Reaching time limit ends the round - GTR_OVERTIME = 1<<17, // Allow overtime behavior - GTR_ENCORE = 1<<18, // Alternate Encore mirroring, scripting, and texture remapping + GTR_POINTLIMIT = 1<<16, // Reaching point limit ends the round + GTR_TIMELIMIT = 1<<17, // Reaching time limit ends the round + GTR_OVERTIME = 1<<18, // Allow overtime behavior + GTR_ENCORE = 1<<19, // Alternate Encore mirroring, scripting, and texture remapping - GTR_TEAMS = 1<<19, // Teams are forced on - GTR_NOTEAMS = 1<<20, // Teams are forced off - GTR_TEAMSTARTS = 1<<21, // Use team-based start positions + GTR_TEAMS = 1<<20, // Teams are forced on + GTR_NOTEAMS = 1<<21, // Teams are forced off + GTR_TEAMSTARTS = 1<<22, // Use team-based start positions - GTR_NOMP = 1<<22, // No multiplayer - GTR_NOCUPSELECT = 1<<23, // Your maps are not selected via cup. - GTR_NOPOSITION = 1<<24, // No POSITION + GTR_NOMP = 1<<23, // No multiplayer + GTR_NOCUPSELECT = 1<<24, // Your maps are not selected via cup. + GTR_NOPOSITION = 1<<25, // No POSITION // free: to and including 1<<31 }; diff --git a/src/g_game.c b/src/g_game.c index 56ad5149b..4769c8b8b 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -2469,9 +2469,10 @@ void G_MovePlayerToSpawnOrCheatcheck(INT32 playernum) mobj_t *checkpoint; vector3_t pos; - if (players[playernum].checkpointId && - (checkpoint = Obj_FindCheckpoint(players[playernum].checkpointId)) && - Obj_GetCheckpointRespawnPosition(checkpoint, &pos)) + if ((gametyperules & GTR_CHECKPOINTS) + && players[playernum].checkpointId + && (checkpoint = Obj_FindCheckpoint(players[playernum].checkpointId)) + && Obj_GetCheckpointRespawnPosition(checkpoint, &pos)) { respawnvars_t *rsp = &players[playernum].respawn; @@ -3057,7 +3058,7 @@ static gametype_t defaultgametypes[] = { "Tutorial", "GT_TUTORIAL", - GTR_NOMP|GTR_NOCUPSELECT|GTR_NOPOSITION, + GTR_CHECKPOINTS|GTR_NOMP|GTR_NOCUPSELECT|GTR_NOPOSITION, TOL_TUTORIAL, int_none, 0, diff --git a/src/p_mobj.c b/src/p_mobj.c index 767060313..f03b61df5 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -12318,6 +12318,10 @@ static boolean P_AllowMobjSpawn(mapthing_t* mthing, mobjtype_t i) if (modeattacking & ATTACKING_SPB) return false; break; + case MT_CHECKPOINT_END: + if (!(gametyperules & GTR_CHECKPOINTS)) + return false; + break; default: break; } From f9dfdf21529c3855bdd4fecdfd3bc5f70d18d550 Mon Sep 17 00:00:00 2001 From: toaster Date: Sat, 16 Sep 2023 23:31:01 +0100 Subject: [PATCH 6/6] GTR_CHECKPOINTS when alone: Force a level restart when player has been dead for a second --- src/p_user.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/p_user.c b/src/p_user.c index 54e05f0f1..8f4dc5090 100644 --- a/src/p_user.c +++ b/src/p_user.c @@ -2794,7 +2794,16 @@ static void P_DeathThink(player_t *player) if (playerGone == false && player->deadtimer > TICRATE) { - player->playerstate = PST_REBORN; + if (!netgame && !splitscreen + && player->bot == false + && (gametyperules & GTR_CHECKPOINTS)) + { + G_SetRetryFlag(); + } + else + { + player->playerstate = PST_REBORN; + } } // TODO: support splitscreen