mirror of
				https://github.com/KartKrewDev/RingRacers.git
				synced 2025-10-30 08:01:28 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			537 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			537 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // 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 <algorithm>
 | |
| 
 | |
| #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<Fixed> angle_vector(angle_t x)
 | |
| {
 | |
| 	return Vec2<Fixed> {FCOS(x), FSIN(x)};
 | |
| }
 | |
| 
 | |
| template <typename F>
 | |
| 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<int>& momz)
 | |
| 		{
 | |
| 			Particle* x = source->spawn_from<Particle>({}, 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<Mobj>({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<Mobj>({}, 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<sfxenum_t>(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<Kart>({}, 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;
 | |
| 		}
 | |
| 
 | |
| 		Mobj* p = player();
 | |
| 		bool pValid = Mobj::valid(p) && p->player;
 | |
| 		bool hasCustomHusk = pValid && skins[p->player->skin].sprites[SPR2_DKRT].numframes;
 | |
| 
 | |
| 		if(hasCustomHusk)
 | |
| 		{
 | |
| 			skin = (void*)(&skins[p->player->skin]);
 | |
| 			frame = 0;
 | |
| 		}
 | |
| 
 | |
| 		Particle::spew(this);
 | |
| 		scale(3*scale()/2);
 | |
| 
 | |
| 		if(hasCustomHusk){
 | |
| 			flags |= MF_NOSQUISH; //K_Squish() automates spritexscale/spriteyscale & this flag prevents that at the cost of no squish visual when the kart husk hits the ground
 | |
| 			fixed_t huskScale = FixedDiv(mapobjectscale, scale());
 | |
| 			spritexscale(FixedMul(spritexscale(), huskScale));
 | |
| 			spriteyscale(FixedMul(spriteyscale(), huskScale));
 | |
| 		}
 | |
| 
 | |
| 		health = 1;
 | |
| 		state(!hasCustomHusk ? S_KART_LEFTOVER_NOTIRES : S_KART_LEFTOVER_CUSTOM);
 | |
| 		cooldown(20);
 | |
| 		burning(burn_duration());
 | |
| 
 | |
| 		if (!cv_reducevfx.value)
 | |
| 		{
 | |
| 			voice(sfx_die00);
 | |
| 		}
 | |
| 
 | |
| 		if(pValid)
 | |
| 		{
 | |
| 			if((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<Fixed>& range, const Vec2<Fixed>& 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<Mobj>({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<fixed_t>(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<Particle>({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! State will change away from S_INVISIBLE inside destroy() where S_INVISIBLE was set in static spawn()
 | |
| 		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<statenum_t>(S_BADNIK_EXPLOSION_SHOCKWAVE1 + i));
 | |
| 		}
 | |
| 	}
 | |
| };
 | |
| 
 | |
| }; // namespace
 | |
| 
 | |
| void Obj_SpawnDestroyedKart(mobj_t *player)
 | |
| {
 | |
| 	Kart::spawn(static_cast<Mobj*>(player));
 | |
| }
 | |
| 
 | |
| void Obj_DestroyedKartThink(mobj_t *kart)
 | |
| {
 | |
| 	static_cast<Kart*>(kart)->think();
 | |
| }
 | |
| 
 | |
| boolean Obj_DestroyKart(mobj_t *kart)
 | |
| {
 | |
| 	return static_cast<Kart*>(kart)->destroy();
 | |
| }
 | |
| 
 | |
| void Obj_DestroyedKartParticleThink(mobj_t *part)
 | |
| {
 | |
| 	static_cast<Particle*>(part)->think();
 | |
| }
 | |
| 
 | |
| void Obj_DestroyedKartParticleLanding(mobj_t *part)
 | |
| {
 | |
| 	static_cast<Particle*>(part)->on_land();
 | |
| }
 | 
