diff --git a/src/d_player.h b/src/d_player.h index d0de6c444..e455774fa 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -804,6 +804,8 @@ struct player_t UINT8 dashRingPullTics; // Timer during which the player is pulled towards a dash ring UINT8 dashRingPushTics; // Timer during which the player displays effects and has no gravity after being thrust by a dash ring + boolean pullup; // True if the player is attached to a pullup hook + tic_t ebrakefor; // Ebrake timer, used for visuals. UINT16 faultflash; // Used for misc FAULT visuals diff --git a/src/deh_tables.c b/src/deh_tables.c index 72d26628a..b6e82e0d0 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -3933,6 +3933,9 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_CABOTRON", "MT_CABOTRONSTAR", "MT_STARSTREAM", + + "MT_IPULLUP", + "MT_PULLUPHOOK", }; const char *const MOBJFLAG_LIST[] = { diff --git a/src/info.c b/src/info.c index db2ab0ef1..18bda54fe 100644 --- a/src/info.c +++ b/src/info.c @@ -762,6 +762,10 @@ char sprnames[NUMSPRITES + 1][5] = "DIEM", // smoke "DIEN", // explosion + // Pulley + "HCCH", + "HCHK", + // First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later "VIEW", }; @@ -22014,6 +22018,58 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = MF_SCENERY|MF_NOCLIPTHING|MF_NOCLIPHEIGHT|MF_NOGRAVITY, // flags S_NULL // raisestate }, + { // MT_IPULLUP + 3444, // doomednum + S_INVISIBLE, // 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 + 32*FRACUNIT, // radius + 32*FRACUNIT, // height + 0, // dispoffset + 0, // mass + 0, // damage + sfx_None, // activesound + MF_NOBLOCKMAP|MF_NOGRAVITY|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_SCENERY, // flags + S_NULL // raisestate + }, + { // MT_PULLUPHOOK + 3444, // doomednum + S_INVISIBLE, // 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 + 64*FRACUNIT, // radius + 64*FRACUNIT, // height + 0, // dispoffset + 0, // mass + 0, // damage + sfx_None, // activesound + MF_NOCLIPHEIGHT|MF_SPECIAL|MF_NOGRAVITY|MF_NOCLIP, // flags + S_NULL // raisestate + }, }; diff --git a/src/info.h b/src/info.h index e0a224ebe..d35d2da19 100644 --- a/src/info.h +++ b/src/info.h @@ -1297,6 +1297,10 @@ typedef enum sprite SPR_DIEM, // smoke SPR_DIEN, // explosion + // Pulley + SPR_HCCH, + SPR_HCHK, + // First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later SPR_VIEW, @@ -4980,6 +4984,9 @@ typedef enum mobj_type MT_CABOTRONSTAR, MT_STARSTREAM, + MT_IPULLUP, + MT_PULLUPHOOK, + MT_FIRSTFREESLOT, MT_LASTFREESLOT = MT_FIRSTFREESLOT + NUMMOBJFREESLOTS - 1, NUMMOBJTYPES diff --git a/src/k_objects.h b/src/k_objects.h index 8fc9351bc..6ab2ccaa3 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -425,6 +425,10 @@ boolean Obj_DestroyKart(mobj_t *kart); void Obj_DestroyedKartParticleThink(mobj_t *part); void Obj_DestroyedKartParticleLanding(mobj_t *part); +/* Pulley */ +void Obj_PulleyThink(mobj_t *root); +void Obj_PulleyHookTouch(mobj_t *special, mobj_t *toucher); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index 31a0f6122..ddc96ef41 100644 --- a/src/objects/CMakeLists.txt +++ b/src/objects/CMakeLists.txt @@ -57,6 +57,7 @@ target_sources(SRB2SDL2 PRIVATE powerup-spinner.cpp adventure-air-booster.c destroyed-kart.cpp + pulley.cpp ) add_subdirectory(versus) diff --git a/src/objects/pulley.cpp b/src/objects/pulley.cpp new file mode 100644 index 000000000..3963a6f71 --- /dev/null +++ b/src/objects/pulley.cpp @@ -0,0 +1,292 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2024 by "Lat'" +// Copyright (C) 2024 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. +//----------------------------------------------------------------------------- + +#include + +#include "objects.hpp" + +#include "../d_player.h" + +using namespace srb2::objects; + +namespace +{ + +struct Pulley; + +struct Hook : Mobj +{ + void target() = delete; + Mobj* player() const { return Mobj::target(); } + void player(Mobj* n) { Mobj::target(n); } + + void tracer() = delete; + Pulley* pulley() const; + void pulley(Pulley* n); + + void touch(Mobj* toucher); +}; + +struct Pulley : Mobj +{ + // hook states to keep the code clean :) + enum class Mode : INT32 + { + kNull = 0, // not set + kIdle = 1, // wait for player + kHook = 2, // player hooked, short delay before we start pulling + kPull = 3, // pulling the player upwards + kDown = 4, // player has been flung, go back down + }; + + static constexpr tic_t kPullupDelay = TICRATE/4; + static Fixed max_pullup_speed() { return 32*mapobjectscale; } + + // how far down do we extend the hook from our current + // position? + void cvmem() = delete; + Fixed height() const { return mobj_t::cvmem; } + void height(Fixed n) { mobj_t::cvmem = n; } + + // just makes it easier + Fixed bottom() const { return z - height(); } + + void target() = delete; + Hook* hook() const { return Mobj::target(); } + void hook(Hook* n) { Mobj::target(n); } + + void extravalue1() = delete; + Mode mode() const { return static_cast(mobj_t::extravalue1); } + void mode(Mode n) { mobj_t::extravalue1 = static_cast(n); } + + void thing_args() = delete; + bool trick_bit() const { return mobj_t::thing_args[0] & 1; } + + void extravalue2() = delete; + tic_t ticker() const { return mobj_t::extravalue2; } + void ticker(tic_t n) { mobj_t::extravalue2 = n; } + + void tracer() = delete; + Mobj* rope() const { return Mobj::tracer(); } + void rope(Mobj* n) { Mobj::tracer(n); } + + void init() + { + if (Mobj::valid(hook())) + hook()->remove(); + + if (Mobj::valid(rope())) + rope()->remove(); + + if (!spawnpoint) + return; // what the fuck + + height(spawnpoint->angle * FRACUNIT); + + // spawn the hook: + if (Hook* h = spawn({x, y, bottom()}, MT_PULLUPHOOK)) + { + h->sprite = SPR_HCHK; + h->frame = 0; + h->color = SKINCOLOR_RED; + + hook(h); // don't lose track of that. + h->pulley(this); // point to daddy + + // set idle state + mode(Mode::kIdle); + } + + if (Mobj* h = spawn({x, y, bottom()}, MT_THOK)) + { + // jartha note: this visual has been completely replaced vs the old lua version + h->sprite = SPR_HCCH; + h->frame = 0; + h->tics = -1; + rope(h); + animate(); + h->mobj_t::old_spriteyscale = h->mobj_t::spriteyscale; + } + } + + bool think() + { + if (mode() == Mode::kNull) + { + init(); + return true; + } + + if (!Mobj::valid(hook())) + { + mode(Mode::kNull); // wtf! force respawn hook + return false; + } + + // handle functionality: + auto mode_handler = [&] + { + switch (mode()) + { + case Mode::kHook: + return think_hook(); + case Mode::kPull: + return think_pull(); + case Mode::kDown: + return think_down(); + default: + return true; + } + }; + if (!mode_handler()) + return false; + + // handle the hook visuals here + animate(); + return true; + } + +private: + bool think_player() + { + // Hook the player and ensure they remain in place! + Mobj* pmo = hook()->player(); + if (!Mobj::valid(pmo) || !pmo->player) + { + mode(Mode::kNull); // reset hook + return false; + } + + pmo->flags |= MF_NOGRAVITY; + pmo->move_origin(hook()); + pmo->angle = angle; + return true; + }; + + bool think_hook() + { + if (!think_player()) + return false; + + // wait .5 second before pulling + ticker(ticker() + 1); + if (ticker() > kPullupDelay) + { + mode(Mode::kPull); + ticker(0); // (don't forget to reset that...) + hook()->momz = flip(mapobjectscale/4); + } + return true; + } + + bool think_pull() + { + if (!think_player()) + return false; + + hook()->momz = hook()->momz * 14 / 10; + if (std::abs(hook()->momz) > max_pullup_speed()) + hook()->momz = flip(max_pullup_speed()); + + // reaching the top + if (hook()->z > z) + apex(); + return true; + } + + void apex() + { + mode(Mode::kDown); + + Mobj* pmo = hook()->player(); + P_ResetPlayer(pmo->player); + + // special flag sets trick panel state + if (trick_bit()) // tyron 2023-10-30 spooky no look UDMF fix + { + K_DoPogoSpring(pmo, 32*FRACUNIT, 0); + pmo->player->trickpanel = TRICKSTATE_READY; + // jartha note: trickdelay does not exist, maybe it got replaced at some point? + //pmo->player->trickdelay = 8; + } + + pmo->momz = hook()->momz; + pmo->player->pullup = false; + pmo->flags &= ~MF_NOGRAVITY; + + hook()->momz = 0; // stop! + hook()->player(nullptr); // this looks stupid, but anyway this makes the hook forget about the player + } + + bool think_down() + { + // go back down slowly. + hook()->momz = -24 * mapobjectscale; + if (hook()->z < bottom()) + { + // jartha note: lua discrepancy: setting z in lua does P_CheckPosition. Is it fine to skip that? + hook()->z = bottom(); + hook()->momz = 0; + + mode(Mode::kIdle); // aaand we're ready again. + } + return true; + } + + void animate() + { + if (!Mobj::valid(rope()) || !Mobj::valid(hook())) + return; + + rope()->z = hook()->top(); + rope()->spriteyscale(Fixed {std::max(0, z - hook()->top())} / std::max(1, 32 * rope()->scale())); + } +}; + +Pulley* Hook::pulley() const +{ + return Mobj::tracer(); +} + +void Hook::pulley(Pulley* n) +{ + Mobj::tracer(n); +} + +void Hook::touch(Mobj* toucher) +{ + if (Mobj::valid(player())) + return; // nope + if (!Mobj::valid(pulley())) + return; // wtf + if (pulley()->mode() != Pulley::Mode::kIdle) + return; // hook is busy + if (toucher->player->pullup) + return; // Already hooked! + + player(toucher); + pulley()->mode(Pulley::Mode::kHook); + + P_ResetPlayer(toucher->player); // stop everything we're doing + toucher->player->pullup = true; + pulley()->angle = toucher->angle; +} + +}; // namespace + +void Obj_PulleyThink(mobj_t *root) +{ + static_cast(root)->think(); +} + +void Obj_PulleyHookTouch(mobj_t *special, mobj_t *toucher) +{ + static_cast(special)->touch(static_cast(toucher)); +} diff --git a/src/p_inter.c b/src/p_inter.c index 67d864e91..55ec1a6fc 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -1058,6 +1058,10 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) Obj_SSBumperTouchSpecial(special, toucher); return; + case MT_PULLUPHOOK: + Obj_PulleyHookTouch(special, toucher); + return; + default: // SOC or script pickup P_SetTarget(&special->target, toucher); break; diff --git a/src/p_mobj.c b/src/p_mobj.c index ebff9ca18..3607f4ec5 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -6566,6 +6566,11 @@ static void P_MobjSceneryThink(mobj_t *mobj) Obj_TickPowerUpSpinner(mobj); return; } + case MT_IPULLUP: + { + Obj_PulleyThink(mobj); + return; + } default: if (mobj->fuse) { // Scenery object fuse! Very basic! @@ -10604,6 +10609,8 @@ fixed_t P_GetMobjDefaultScale(mobj_t *mobj) return 4*FRACUNIT; case MT_SCRIPT_THING_ORB: return 2*FRACUNIT; + case MT_PULLUPHOOK: + return 2*FRACUNIT; default: break; } diff --git a/src/p_saveg.c b/src/p_saveg.c index a9256ef8d..90e575b19 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -540,6 +540,8 @@ static void P_NetArchivePlayers(savebuffer_t *save) WRITEUINT8(save->p, players[i].dashRingPullTics); WRITEUINT8(save->p, players[i].dashRingPushTics); + WRITEUINT8(save->p, players[i].pullup); + WRITEUINT32(save->p, players[i].ebrakefor); WRITEUINT32(save->p, players[i].roundscore); @@ -1130,6 +1132,8 @@ static void P_NetUnArchivePlayers(savebuffer_t *save) players[i].dashRingPullTics = READUINT8(save->p); players[i].dashRingPushTics = READUINT8(save->p); + players[i].pullup = READUINT8(save->p); + players[i].ebrakefor = READUINT32(save->p); players[i].roundscore = READUINT32(save->p);