// DR. ROBOTNIK'S RING RACERS //----------------------------------------------------------------------------- // Copyright (C) 2024 by James Robert Roman. // 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 "../cxxutil.hpp" #include "objects.hpp" #include "../m_easing.h" #include "../m_random.h" #include "../r_skins.h" #include "../tables.h" using namespace srb2::objects; namespace { Vec2 angle_vector(angle_t x) { return Vec2 {FCOS(x), FSIN(x)}; } template void radial_generic(int ofs, int spokes, F&& f) { int ang = 360 / spokes; for (int i = 0; i < spokes; ++i) { f((ofs + (ang * i)) % 360); } } angle_t degr_to_angle(int degr) { return FixedAngle(degr * FRACUNIT); } struct Particle : Mobj { void extravalue1() = delete; UINT8 bounces() const { return mobj_t::extravalue1; } void bounces(UINT8 n) { mobj_t::extravalue1 = n; } void extravalue2() = delete; UINT8 counter() const { return mobj_t::extravalue2; } void counter(UINT8 n) { mobj_t::extravalue2 = n; } bool is_shrapnel() const { return sprite == SPR_KRBM; } static void spew(Mobj* source) { auto generic = [&](spritenum_t sprite, int degr, Fixed scale, int momx, const Vec2& momz) { Particle* x = source->spawn_from({}, MT_KART_PARTICLE); if (x) { x->sprite = sprite; x->color = source->color; x->frame = FF_SEMIBRIGHT; x->lightlevel = 112; x->scale(scale * x->scale()); x->instathrust(source->angle + degr_to_angle(degr), momx * mapobjectscale); x->momz = P_RandomRange(PR_ITEM_DEBRIS, momz.x, momz.y) * mapobjectscale * 2; x->angle = P_Random(PR_ITEM_DEBRIS); x->rollangle = P_Random(PR_ITEM_DEBRIS); x->renderflags |= RF_DONTDRAW; } return x; }; auto part = [&](spritenum_t sprite, int degr, Fixed scale) { return generic(sprite, degr, scale, 2, {8, 16}); }; auto radial = [&](spritenum_t sprite, int ofs, int spokes, Fixed scale) { radial_generic(ofs, spokes, [&](int ang) { part(sprite, ang, scale); }); }; constexpr Fixed kSmall = 3*FRACUNIT/2; constexpr Fixed kMedium = 7*FRACUNIT/4; constexpr Fixed kLarge = 2*FRACUNIT; part(SPR_DIEE, 0, kLarge); // steering wheel part(SPR_DIEK, 180 + 45, kLarge); // engine part(SPR_DIEG, 90, kLarge); // left pedal base part(SPR_DIED, -90, kLarge); // right pedal base radial(SPR_DIEI, 90, 2, kLarge); // wheel axle bars radial(SPR_DIEC, 90, 2, kLarge); // pedal tips radial(SPR_DIEA, 45, 4, kMedium); // tires radial(SPR_DIEH, 45, 4, kMedium); // struts / springs radial(SPR_DIEB, 360/12, 6, kSmall); // pipeframe bars radial(SPR_DIEJ, 360/16, 8, kSmall); // screws radial_generic(0, 6, [&](int degr) { generic(SPR_KRBM, degr, kSmall, 8, {22, 28}); }); // shrapnel // explosion radial_generic( 45, 4, [&](int degr) { if (Mobj* x = source->spawn_from({0, 0, source->height}, MT_KART_PARTICLE)) { x->flags |= MF_NOGRAVITY | MF_NOCLIPHEIGHT; x->height = 0; x->scale(2 * x->scale()); x->angle = degr_to_angle(degr); x->state(S_KART_XPL01); x->renderflags |= RF_REDUCEVFX; } } ); } void think() { if (!fuse && !momz) { // Getting stuck underneath a crusher... force // a landing so the fuse activates. on_land(); } if (state()->num() == S_BRAKEDRIFT) { renderflags ^= RF_DONTDRAW; return; } // explosion if (sprite == SPR_DIEN) { counter(counter() + 1); if (counter() > 6) { renderflags ^= RF_DONTDRAW; } return; } constexpr tic_t kReappear = 16; if (counter() < kReappear && !is_shrapnel()) { counter(counter() + 1); if (counter() == kReappear) { renderflags &= ~RF_DONTDRAW; } } angle += ANGLE_11hh; rollangle += ANGLE_11hh; if (is_shrapnel() && leveltime % 2 == 0) { if (Mobj* x = spawn_from({}, MT_BOOMEXPLODE)) { x->color = SKINCOLOR_RUBY; x->scale_between(x->scale() / 2, x->scale() * 8, x->scale() / 16); x->state(S_SLOWBOOM2); } } spritescale({FRACUNIT, FRACUNIT}); // unsquish } void on_land() { if (!fuse) { fuse = (is_shrapnel() ? 70 : 90); } auto squash = [&](int tics) { hitlag(tics); spritescale({2*FRACUNIT, FRACUNIT/2}); // squish }; switch (sprite) { case SPR_DIEB: // bar squash(2); break; case SPR_DIEH: // struts squash(4); break; case SPR_DIEI: // screws squash(1); break; case SPR_DIEK: // engine squash(5); break; default: break; } if (!is_shrapnel() && fuse > 7 && (bounces() & 1)) // 7 = 0.2/(1/35) { voice( static_cast(P_RandomRange(PR_ITEM_DEBRIS, sfx_die01, sfx_die03)), P_RandomRange(PR_ITEM_DEBRIS, 20, 40) * 255 / 100 ); } bounces(bounces() + 1); } }; struct Kart : Mobj { static constexpr tic_t kVibrateTimer = 70; static constexpr UINT32 kNoClipFlags = MF_NOCLIP | MF_NOCLIPTHING; static tic_t burn_duration() { return (gametyperules & GTR_CLOSERPLAYERS ? 10 : 20) * TICRATE; } void extravalue1() = delete; UINT8 weight() const { return mobj_t::extravalue1; } void weight(UINT8 n) { mobj_t::extravalue1 = n; } void extravalue2() = delete; tic_t timer() const { return mobj_t::extravalue2; } void timer(tic_t n) { mobj_t::extravalue2 = n; } void threshold() = delete; tic_t cooldown() const { return mobj_t::threshold; } void cooldown(tic_t n) { mobj_t::threshold = n; } void movecount() = delete; tic_t burning() const { return mobj_t::movecount; } void burning(tic_t n) { mobj_t::movecount = n; } void target() = delete; Mobj* player() const { return Mobj::target(); } void player(Mobj* n) { Mobj::target(n); } static void spawn(Mobj* target) { SRB2_ASSERT(target->player != nullptr); Kart* kart = target->spawn_from({}, MT_KART_LEFTOVER); if (!kart) return; kart->angle = target->angle; kart->color = target->color; P_SetObjectMomZ(kart, 20*FRACUNIT, false); kart->weight(target->player->kartweight); kart->flags |= kNoClipFlags; if (target->player->pflags & PF_NOCONTEST) target->tracer(kart); kart->state(S_INVISIBLE); kart->timer(kVibrateTimer); kart->exact_hitlag(15, true); kart->player(target); Obj_SpawnCustomBrolyKi(target, kart->hitlag() - 2, 32 * mapobjectscale, 0); target->exact_hitlag(kart->hitlag() + 1, true); target->frame |= FF_SEMIBRIGHT; target->lightlevel = 128; } void think() { if (burning() > 0) { burning(burning() - 1); fire(); } if (cooldown() > 0) { cooldown(cooldown() - 1); } if (timer() > 0) { timer(timer() - 1); animate(); } } bool destroy() { if (cooldown()) { // no-op P_DamageMobj return true; } if (health <= 1) { return false; } Particle::spew(this); scale(3 * scale() / 2); health = 1; state(S_KART_LEFTOVER_NOTIRES); cooldown(20); burning(burn_duration()); if (!cv_reducevfx.value) { voice(sfx_die00); } if (Mobj* p = player(); Mobj::valid(p)) { if (p->player && skins[p->player->skin].flags & SF_BADNIK) { P_SpawnBadnikExplosion(p); p->spritescale({2*FRACUNIT, 2*FRACUNIT}); p->flags |= MF_NOSQUISH; } p->state(S_KART_DEAD); } return true; } private: void fire() { auto spread = [&](const Vec2& range, const Vec2& zrange) { angle_t ang = P_Random(PR_ITEM_DEBRIS); Fixed r = P_RandomRange(PR_ITEM_DEBRIS, range.x, range.y) * mapobjectscale * 4; Fixed z = P_RandomRange(PR_ITEM_DEBRIS, zrange.x, zrange.y) * mapobjectscale * 4; return spawn_from({angle_vector(ang) * r, z}, MT_THOK); }; auto vfx = [&](fixed_t f) { if (Mobj* x = spread({16, 32}, {0, 0})) { x->state(S_KART_FIRE); x->lightlevel = 176; x->renderflags |= RF_ABSOLUTELIGHTLEVEL | RF_SEMIBRIGHT | RF_REDUCEVFX; } if (f < 3*FRACUNIT/4) { auto smoke = [&] { if (Mobj* x = spread({3, 6}, {0, 8})) { Fixed from = x->scale() / 3; Fixed to = 5 * x->scale() / 4; x->scale_between(from, to, (to - from) / 35); x->state(S_KART_SMOKE); x->lightlevel = -112; x->momz = 16 * mapobjectscale; } }; smoke(); smoke(); } }; UINT32 rf = RF_SEMIBRIGHT; if (burning() && P_IsObjectOnGround(this)) { fixed_t f = burning() * FRACUNIT / burn_duration(); if ((leveltime % std::max(1, Easing_OutCubic(f, 8, 1))) == 0) { vfx(f); } if (f < 3*FRACUNIT/4) { auto spark = [&](int degr) { angle_t ang = angle + degr_to_angle(degr); if (Particle* x = spawn_from({angle_vector(ang) * Fixed {radius}, 0}, MT_KART_PARTICLE)) { x->state(S_BRAKEDRIFT); x->fuse = 12; x->color = SKINCOLOR_PASTEL; x->angle = ang - ANGLE_90; x->scale(2 * x->scale() / 5); x->flags |= MF_NOGRAVITY | MF_NOCLIPHEIGHT; x->renderflags |= RF_ADD; } }; if (leveltime % 16 == 0) { radial_generic(45, 4, spark); } } if (leveltime & 1) { rf = RF_FULLBRIGHT; } voice_loop(sfx_kc51); } renderflags = (renderflags & ~RF_BRIGHTMASK) | rf; } void animate() { Mobj* p = player(); if (!Mobj::valid(p)) { return; } if (timer()) { // Vibration on the death sprite eases downward p->exact_hitlag(Easing_InCubic(timer() * FRACUNIT / kVibrateTimer, 2, 90), true); } else { flags &= ~kNoClipFlags; P_PlayDeathSound(p); } // First tick after hitlag: destroyed kart appears! if (state()->num() == S_INVISIBLE) { destroy(); } } static void P_SpawnBadnikExplosion(mobj_t *target) { UINT8 count = 24; angle_t ang = 0; angle_t step = ANGLE_MAX / count; fixed_t spd = 8 * mapobjectscale; for (UINT8 i = 0; i < count; ++i) { mobj_t *x = P_SpawnMobjFromMobjUnscaled( target, P_RandomRange(PR_EXPLOSION, -48, 48) * target->scale, P_RandomRange(PR_EXPLOSION, -48, 48) * target->scale, P_RandomRange(PR_EXPLOSION, -48, 48) * target->scale, MT_THOK ); x->hitlag = 0; P_InstaScale(x, 3 * x->scale / 2); P_InstaThrust(x, ang, spd); x->momz = P_RandomRange(PR_EXPLOSION, -4, 4) * mapobjectscale; P_SetMobjStateNF(x, S_BADNIK_EXPLOSION1); ang += step; } // burst effects (copied from MT_ITEMCAPSULE) ang = FixedAngle(360*P_RandomFixed(PR_ITEM_DEBRIS)); for (UINT8 i = 0; i < 2; i++) { mobj_t *blast = P_SpawnMobjFromMobj(target, 0, 0, target->info->height >> 1, MT_BATTLEBUMPER_BLAST); blast->hitlag = 0; blast->angle = ang + i*ANGLE_90; P_SetScale(blast, 2*blast->scale/3); blast->destscale = 6*blast->scale; blast->scalespeed = (blast->destscale - blast->scale) / 30; P_SetMobjStateNF(blast, static_cast(S_BADNIK_EXPLOSION_SHOCKWAVE1 + i)); } } }; }; // namespace void Obj_SpawnDestroyedKart(mobj_t *player) { Kart::spawn(static_cast(player)); } void Obj_DestroyedKartThink(mobj_t *kart) { static_cast(kart)->think(); } boolean Obj_DestroyKart(mobj_t *kart) { return static_cast(kart)->destroy(); } void Obj_DestroyedKartParticleThink(mobj_t *part) { static_cast(part)->think(); } void Obj_DestroyedKartParticleLanding(mobj_t *part) { static_cast(part)->on_land(); }