// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2025 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. //----------------------------------------------------------------------------- // Original Lua script by Lat // Hardcoded by jartha #include #include #include #include "../math/fixed.hpp" #include "../math/line_segment.hpp" #include "../math/vec.hpp" #include "../mobj.hpp" #include "../mobj_list_view.hpp" #include "../d_player.h" #include "../doomtype.h" #include "../info.h" #include "../k_kart.h" #include "../k_objects.h" #include "../m_random.h" #include "../p_local.h" #include "../p_maputl.h" #include "../p_pspr.h" #include "../r_defs.h" using srb2::math::Fixed; using srb2::math::LineSegment; using srb2::math::Vec2; using srb2::Mobj; using srb2::MobjListView; namespace { using frame_layout = std::array; // -z, +z, -x, -y, +x, +y struct SA2CrateConfig { static constexpr spritenum_t kSprite = SPR_SABX; static constexpr frame_layout kFrames = {3, 2, 0, 0, 0, 0}; static constexpr statenum_t kDefaultDebris = S_SA2_CRATE_DEBRIS; static constexpr sfxenum_t kDefaultSound = sfx_cratew; }; struct IceCapBlockConfig { static constexpr spritenum_t kSprite = SPR_ICBL; static constexpr frame_layout kFrames = {6, 6, 0, 0, 0, 0}; static constexpr statenum_t kDefaultDebris = S_ICECAPBLOCK_DEBRIS; static constexpr sfxenum_t kDefaultSound = sfx_s3k82; }; struct Graphic : Mobj { void hnext() = delete; Graphic* next() const { return Mobj::hnext(); } void next(Graphic* n) { Mobj::hnext(n); } Graphic* dress(spritenum_t sprite, UINT32 frame) { this->sprite = sprite; this->frame = frame; this->renderflags |= RF_NOSPLATBILLBOARD; return this; } Graphic* xy(fixed_t x, fixed_t y) { this->sproff2d({x, y}); return this; } Graphic* z(fixed_t z) { this->sprzoff(z); return this; } Graphic* turn(angle_t angle) { this->angle = angle; return this; } }; struct Side : Graphic { bool valid() const { return Mobj::valid() && Mobj::valid(owner()); } void think() { if (!valid()) { remove(); return; } move_origin(owner()); renderflags = owner()->renderflags; } }; struct Toucher : Mobj { bool boosting() const { return player && (player->sneakertimer || player->panelsneakertimer || player->weaksneakertimer || K_PlayerCanPunt(player)); } }; struct AnyBox : Graphic { template bool visit(F&& visitor); void update() { visit([](auto box) { box->mobj_t::eflags &= ~MFE_ONGROUND; }); } }; template struct Box : AnyBox { static constexpr Fixed kIntendedSize = 128*FRACUNIT; static constexpr Vec2 kScrunch = {4*FRACUNIT/5, 6*FRACUNIT/5}; void extravalue1() = delete; statenum_t debris_state() const { return static_cast(mobj_t::extravalue1); } void debris_state(statenum_t n) { mobj_t::extravalue1 = n; } void extravalue2() = delete; sfxenum_t debris_sound() const {return static_cast(mobj_t::extravalue2); } void debris_sound(sfxenum_t n) {mobj_t::extravalue2 = n; } auto gfx() { return MobjListView(static_cast(this), [](Graphic* g) { return g->next(); }); } void init() { scale(scale() * (kIntendedSize / Fixed {info->height})); Graphic* node = this; int i = 0; auto dress = [&](Graphic* g, UINT32 ff) { return g->dress(Config::kSprite, Config::kFrames[i++] | ff); }; auto side = [&](UINT32 ff) { Side* side = spawn_from({}, MT_BOX_SIDE); side->owner(this); node->next(side); // link node = side; return dress(side, ff); }; dress(this, FF_FLOORSPRITE)->turn(0); // bottom (me) side(FF_FLOORSPRITE)->z(height); // top // sides side(FF_PAPERSPRITE)->xy(-radius, 0)->turn(ANGLE_270); side(FF_PAPERSPRITE)->xy(0, -radius); side(FF_PAPERSPRITE)->xy(+radius, 0)->turn(ANGLE_90); side(FF_PAPERSPRITE)->xy(0, +radius)->turn(ANGLE_180); debris_state(Config::kDefaultDebris); debris_sound(Config::kDefaultSound); } bool think() { if (fuse) { fuse--; renderflags ^= RF_DONTDRAW; if (!fuse) { update_nearby(); remove(); return false; } } return true; } void touch(Toucher* toucher) { if (fuse) { return; } P_DamageMobj(this, toucher, nullptr, 1, DMG_NORMAL); if (!toucher->boosting()) { toucher->solid_bounce(this); } } bool damage_valid(const Mobj* inflictor) const { return !fuse && Mobj::valid(inflictor); } bool damage(Mobj* inflictor) { if (!damage_valid(inflictor)) { return false; } inflictor->hitlag(3); fuse = 10; // scrunch crate sides for (Graphic* g : gfx()) { if (g->frame & FF_PAPERSPRITE) { g->frame++; g->spritescale(kScrunch); } else { g->spritescale(kScrunch.x); } // reset interp g->mobj_t::old_spritexscale = g->spritexscale(); g->mobj_t::old_spriteyscale = g->spriteyscale(); g->sproff2d(g->sproff2d() * kScrunch.x); g->sprzoff(g->sprzoff() * kScrunch.y); } debris(inflictor); update_nearby(); return true; } private: void debris(Mobj* inflictor) { if (debris_state() >= NUMSTATES) { return; } auto rng = [&](int x, int y) { return P_RandomRange(PR_DECORATION, x, y) * scale(); }; auto rng_xyz = [&](int x) { // note: determinate random argument eval order auto rand_z = rng(0, x); auto rand_y = rng(-x, x); auto rand_x = rng(-x, x); return std::tuple(rand_x, rand_y, rand_z); }; auto spawn = [&](bool playsound) { auto [x, y, z] = rng_xyz(info->height / FRACUNIT); Mobj* p = spawn_from({x, y, z}, MT_BOX_DEBRIS); p->scale_between(scale() / 2, scale()); p->state(debris_state()); std::tie(x, y, z) = rng_xyz(4); p->momx = (inflictor->momx / 8) + x; p->momy = (inflictor->momy / 8) + y; p->momz = (Fixed::hypot(inflictor->momx, inflictor->momy) / 4) + z; if (playsound && debris_sound()) { p->voice(debris_sound()); } }; spawn(true); spawn(false); spawn(false); spawn(false); spawn(false); spawn(false); } void update_nearby() const { LineSegment search = aabb(); Vec2 org{bmaporgx, bmaporgy}; search.a -= org + MAXRADIUS; search.b -= org - MAXRADIUS; search.a.x = static_cast(search.a.x) >> MAPBLOCKSHIFT; search.b.x = static_cast(search.b.x) >> MAPBLOCKSHIFT; search.a.y = static_cast(search.a.y) >> MAPBLOCKSHIFT; search.b.y = static_cast(search.b.y) >> MAPBLOCKSHIFT; BMBOUNDFIX(search.a.x, search.b.x, search.b.x, search.b.y); for (INT32 bx = search.a.x; bx <= search.b.x; ++bx) { for (INT32 by = search.a.y; by <= search.b.y; ++by) { P_BlockThingsIterator( bx, by, [](mobj_t* thing) { static_cast(thing)->update(); return BMIT_CONTINUE; } ); } } } }; struct Crate : Box { static constexpr int kMetalFrameStart = 8; void thing_args() = delete; bool metal() const { return mobj_t::thing_args[0]; } void init() { Box::init(); if (metal()) { for (Graphic* g : gfx()) { g->frame += kMetalFrameStart; } debris_state(S_SA2_CRATE_DEBRIS_METAL); debris_sound(sfx_cratem); } } bool damage(Toucher* inflictor) { if (!Box::damage_valid(inflictor)) { return false; } if (metal() && !inflictor->boosting()) { crush(inflictor); return false; } return Box::damage(inflictor); } private: bool clip2d(const Toucher* inflictor) const { LineSegment a = aabb(); LineSegment b = inflictor->aabb(); return a.a.x < b.b.x && b.a.x < a.b.x && a.a.y < b.b.y && b.a.y < a.b.y; } void crush(Toucher* inflictor) { if (!momz) { return; } if ((momz < 0 ? mobj_t::z - inflictor->floorz : inflictor->ceilingz - top()) > inflictor->height) { return; } if (!clip2d(inflictor)) { // Bumping the side of a falling crate should not // kill you. // Note: this check is imperfect. That's why // everything is guarded by momz anyway. return; } P_DamageMobj(inflictor, this, nullptr, 1, DMG_CRUSHED); } }; struct Ice : Box { }; template bool AnyBox::visit(F&& visitor) { switch (type) { case MT_SA2_CRATE: visitor(static_cast(this)); break; case MT_ICECAPBLOCK: visitor(static_cast(this)); break; default: return false; } return true; } }; // namespace void Obj_BoxSideThink(mobj_t* mobj) { static_cast(mobj)->think(); } void Obj_TryCrateInit(mobj_t* mobj) { static_cast(mobj)->visit([&](auto box) { box->init(); }); } boolean Obj_TryCrateThink(mobj_t* mobj) { bool c = false; static_cast(mobj)->visit([&](auto box) { c = box->think(); }); return c; } void Obj_TryCrateTouch(mobj_t* special, mobj_t* toucher) { static_cast(special)->visit([&](auto box) { box->touch(static_cast(toucher)); }); } boolean Obj_TryCrateDamage(mobj_t* target, mobj_t* inflictor) { bool c = false; static_cast(target)->visit([&](auto box) { c = box->damage(static_cast(inflictor)); }); return c; } boolean Obj_SA2CrateIsMetal(mobj_t* mobj) { return static_cast(mobj)->metal(); }