From 3491bd0b1d7ff96e3cbbff0e921966815ab56705 Mon Sep 17 00:00:00 2001 From: James R Date: Tue, 27 Sep 2022 16:53:23 -0700 Subject: [PATCH] Add Battle monitors - Includes a struct definition for symmetrical objects made out of papersprite sides. - Dimensions of papersprite sides are looked up using sprite cache. - Monitors may contain multiple types of items. - Item RNG is deterministic from the time the monitor is spawned but the item types are not stored in memory. Instead the RNG seed is restored every time an item type needs to be determined. Item types need to be determined every time the icon on the monitor's screen changes and when the monitor is popped and drops all its items. - Monitors sparkle like emeralds if there is an emerald inside. - Monitors take damage from players simply bumping into them. The damage scales up with speed and weight. - Activating a lightning shield in proximity decimates the monitor into being able to be destroyed in one hit by anything thereafter. - All throwable / deployable items destroy a monitor in one hit. --- src/k_objects.h | 10 + src/objects/CMakeLists.txt | 1 + src/objects/monitor.c | 691 +++++++++++++++++++++++++++++++++++++ src/p_inter.c | 15 + src/p_map.c | 12 + src/p_mobj.c | 39 ++- 6 files changed, 765 insertions(+), 3 deletions(-) create mode 100644 src/objects/monitor.c diff --git a/src/k_objects.h b/src/k_objects.h index 0078aeae6..5ded33e2f 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -73,6 +73,16 @@ void Obj_UFOPieceRemoved(mobj_t *piece); mobj_t *Obj_CreateSpecialUFO(void); UINT32 K_GetSpecialUFODistance(void); +/* Monitors */ +mobj_t *Obj_SpawnMonitor(mobj_t *origin, UINT8 numItemTypes, UINT8 emerald); +void Obj_MonitorSpawnParts(mobj_t *monitor); +void Obj_MonitorPartThink(mobj_t *part); +fixed_t Obj_MonitorGetDamage(mobj_t *monitor, mobj_t *inflictor, UINT8 damagetype); +void Obj_MonitorOnDamage(mobj_t *monitor, mobj_t *inflictor, INT32 damage); +void Obj_MonitorOnDeath(mobj_t *monitor); +void Obj_MonitorShardThink(mobj_t *shard); +UINT32 Obj_MonitorGetEmerald(const mobj_t *monitor); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index 00eadea36..aa1b91079 100644 --- a/src/objects/CMakeLists.txt +++ b/src/objects/CMakeLists.txt @@ -10,4 +10,5 @@ target_sources(SRB2SDL2 PRIVATE duel-bomb.c broly.c ufo.c + monitor.c ) diff --git a/src/objects/monitor.c b/src/objects/monitor.c new file mode 100644 index 000000000..16967c771 --- /dev/null +++ b/src/objects/monitor.c @@ -0,0 +1,691 @@ +#include "../doomdef.h" +#include "../doomstat.h" +#include "../info.h" +#include "../k_objects.h" +#include "../p_local.h" +#include "../r_state.h" +#include "../k_kart.h" +#include "../k_battle.h" +#include "../m_random.h" +#include "../r_main.h" + +#define FINE90 (FINEANGLES/4) +#define FINE180 (FINEANGLES/2) +#define TRUETAN(n) FINETANGENT(FINE90 + (n)) // bruh + +#define HEALTHFACTOR (FRACUNIT/4) // Always takes at most, 4 hits. + +#define MONITOR_PART_DEFINE(dispoffset, nsides, ...) \ +{dispoffset, nsides, (statenum_t[]){__VA_ARGS__, 0}} + +static const struct monitor_part_config { + INT32 dispoffset; + UINT8 nsides; + statenum_t * states; +} monitor_parts[] = { + MONITOR_PART_DEFINE (0, 3, + S_MONITOR_SCREEN1A, + S_ITEMICON, + S_MONITOR_CRACKB, + S_MONITOR_CRACKA), + + MONITOR_PART_DEFINE (-5, 5, S_MONITOR_STAND), +}; + +#define monitor_rngseed(o) ((o)->movedir) +#define monitor_itemcount(o) ((o)->movecount) +#define monitor_spawntic(o) ((o)->reactiontime) +#define monitor_emerald(o) ((o)->extravalue1) +#define monitor_damage(o) ((o)->extravalue2) +#define monitor_rammingspeed(o) ((o)->movefactor) + +static inline UINT8 +get_monitor_itemcount (const mobj_t *monitor) +{ + // protects against divide by zero + return max(monitor_itemcount(monitor), 1); +} + +#define part_monitor(o) ((o)->target) +#define part_type(o) ((o)->extravalue1) +#define part_index(o) ((o)->extravalue2) +#define part_theta(o) ((o)->movedir) + +#define shard_can_roll(o) ((o)->extravalue1) + +static const sprcache_t * get_state_sprcache (statenum_t); + +static const sprcache_t * +get_sprcache +( spritenum_t sprite, + UINT8 frame) +{ + const spritedef_t *sprdef = &sprites[sprite]; + + if (frame < sprdef->numframes) + { + size_t lump = sprdef->spriteframes[frame].lumpid[0]; + + return &spritecachedinfo[lump]; + } + else + { + return get_state_sprcache(S_UNKNOWN); + } +} + +static const sprcache_t * +get_state_sprcache (statenum_t statenum) +{ + return get_sprcache(states[statenum].sprite, + states[statenum].frame & FF_FRAMEMASK); +} + +static inline fixed_t +get_inradius +( fixed_t length, + INT32 nsides) +{ + return FixedDiv(length, 2 * TRUETAN(FINE180 / nsides)); +} + +static inline void +center_item_sprite +( mobj_t * part, + fixed_t scale) +{ + part->spriteyoffset = 25*FRACUNIT; + part->spritexscale = scale; + part->spriteyscale = scale; +} + +static mobj_t * +spawn_part +( mobj_t * monitor, + statenum_t state) +{ + mobj_t *part = P_SpawnMobjFromMobj( + monitor, 0, 0, 0, MT_MONITOR_PART); + + P_SetMobjState(part, state); + P_SetTarget(&part_monitor(part), monitor); + + part_type(part) = state; + + switch (state) + { + case S_ITEMICON: + // The first frame of the monitor is TV static so + // this should be invisible on the first frame. + part->renderflags |= RF_DONTDRAW; + break; + + default: + break; + } + + return part; +} + +static void +spawn_part_side +( mobj_t * monitor, + fixed_t rad, + fixed_t ang, + const struct monitor_part_config * p, + size_t side) +{ + INT32 i = 0; + + while (p->states[i]) + { + mobj_t *part = spawn_part(monitor, p->states[i]); + + part->radius = rad; + part_theta(part) = ang; + + // add one point for each layer (back to front order) + part->dispoffset = p->dispoffset + i; + + part_index(part) = side; + + i++; + } +} + +static void +spawn_monitor_parts +( mobj_t * monitor, + const struct monitor_part_config *p) +{ + const sprcache_t *info = get_state_sprcache(p->states[0]); + const fixed_t width = FixedMul(monitor->scale, info->width); + const fixed_t rad = get_inradius(width, p->nsides); + const fixed_t angle_factor = ANGLE_MAX / p->nsides; + + INT32 i; + angle_t ang = 0; + + for (i = 0; i < p->nsides; ++i) + { + spawn_part_side(monitor, rad, ang, p, i); + ang += angle_factor; + } +} + +static inline boolean +can_shard_state_roll (statenum_t state) +{ + switch (state) + { + case S_MONITOR_BIG_SHARD: + case S_MONITOR_SMALL_SHARD: + return true; + + default: + return false; + } +} + +static void +spawn_shard +( mobj_t * part, + statenum_t state) +{ + mobj_t *monitor = part_monitor(part); + + // These divisions and multiplications are done on the + // offsets to give bigger increments of randomness. + + const fixed_t half = FixedDiv( + monitor->height, monitor->scale) / 2; + + const UINT16 rad = (monitor->radius / monitor->scale) / 4; + const UINT16 tall = (half / FRACUNIT) / 4; + + mobj_t *p = P_SpawnMobjFromMobj(monitor, + P_RandomRange(PR_ITEM_DEBRIS, -(rad), rad) * 8 * FRACUNIT, + P_RandomRange(PR_ITEM_DEBRIS, -(rad), rad) * 8 * FRACUNIT, + (half / 4) + P_RandomKey(PR_ITEM_DEBRIS, tall + 1) * 4 * FRACUNIT, + MT_MONITOR_SHARD); + + angle_t th = (part->angle + ANGLE_90); + + th -= P_RandomKey(PR_ITEM_DEBRIS, ANGLE_45) - ANGLE_22h; + + p->hitlag = 0; + + P_Thrust(p, th, 6 * p->scale + monitor_rammingspeed(monitor)); + p->momz = P_RandomRange(PR_ITEM_DEBRIS, 3, 10) * p->scale; + + P_SetMobjState(p, state); + + shard_can_roll(p) = can_shard_state_roll(state); + + if (shard_can_roll(p)) + { + p->rollangle = P_Random(PR_ITEM_DEBRIS); + } + + if (P_RandomChance(PR_ITEM_DEBRIS, FRACUNIT/2)) + { + p->renderflags |= RF_DONTDRAW; + } +} + +static void +spawn_debris (mobj_t *part) +{ + const mobj_t *monitor = part_monitor(part); + + fixed_t i; + + for (i = monitor->health; + i <= FRACUNIT; i += HEALTHFACTOR/2) + { + spawn_shard(part, S_MONITOR_BIG_SHARD); + spawn_shard(part, S_MONITOR_SMALL_SHARD); + spawn_shard(part, S_MONITOR_TWINKLE); + } +} + +static void +spawn_monitor_explosion (mobj_t *monitor) +{ + mobj_t *smoldering = P_SpawnMobjFromMobj(monitor, 0, 0, 0, MT_SMOLDERING); + + UINT8 i; + + // Note that a Broly Ki is purposefully not spawned. This + // is to reduce visual clutter since these monitors would + // probably get popped a lot. + + K_MineFlashScreen(monitor); + + P_SetScale(smoldering, (smoldering->destscale /= 3)); + smoldering->tics = TICRATE*3; + + for (i = 0; i < 8; ++i) + { + mobj_t *x = P_SpawnMobjFromMobj(monitor, 0, 0, 0, MT_BOOMEXPLODE); + x->hitlag = 0; + x->color = SKINCOLOR_WHITE; + x->momx = P_RandomRange(PR_EXPLOSION, -5, 5) * monitor->scale, + x->momy = P_RandomRange(PR_EXPLOSION, -5, 5) * monitor->scale, + x->momz = P_RandomRange(PR_EXPLOSION, 0, 6) * monitor->scale * P_MobjFlip(monitor); + P_SetScale(x, (x->destscale *= 3)); + } +} + +static void +kill_monitor_part (mobj_t *part) +{ + const statenum_t statenum = part_type(part); + + switch (statenum) + { + case S_ITEMICON: + P_RemoveMobj(part); + return; + + case S_MONITOR_STAND: + part->momx = 0; + part->momy = 0; + break; + + case S_MONITOR_SCREEN1A: + spawn_debris(part); + P_SetMobjState(part, S_MONITOR_SCREEN1B); + /*FALLTHRU*/ + + default: + /* To be clear, momx/y do not need to set because + those fields are set every tic to offset each + part. */ + part->momz = (part->height / 8) * P_MobjFlip(part); + } + + part->fuse = TICRATE; + part->flags &= ~(MF_NOGRAVITY); +} + +static inline UINT32 +restore_item_rng (UINT32 seed) +{ + const UINT32 oldseed = P_GetRandSeed(PR_ITEM_ROULETTE); + + P_SetRandSeedNet(PR_ITEM_ROULETTE, + P_GetInitSeed(PR_ITEM_ROULETTE), seed); + + return oldseed; +} + +static inline SINT8 +get_item_result (void) +{ + return K_GetTotallyRandomResult(0); +} + +static SINT8 +get_cycle_result +( const mobj_t * monitor, + size_t cycle) +{ + const size_t rem = cycle % + get_monitor_itemcount(monitor); + + SINT8 result; + size_t i; + + const UINT32 oldseed = restore_item_rng( + monitor_rngseed(monitor)); + + for (i = 0; i <= rem; ++i) + { + result = get_item_result(); + } + + restore_item_rng(oldseed); + + return result; +} + +static inline tic_t +get_age (const mobj_t *monitor) +{ + return (leveltime - monitor_spawntic(monitor)); +} + +static inline boolean +is_flickering (const mobj_t *part) +{ + const mobj_t *monitor = part_monitor(part); + + return monitor->fuse > 0 && monitor->fuse <= TICRATE; +} + +static void +flicker +( mobj_t * part, + UINT8 interval) +{ + const tic_t age = get_age(part_monitor(part)); + + if (age % interval) + { + part->renderflags |= RF_DONTDRAW; + } + else + { + part->renderflags &= ~(RF_DONTDRAW); + } +} + +static void +project_icon (mobj_t *part) +{ + const mobj_t *monitor = part_monitor(part); + const tic_t age = get_age(monitor); + + // Item displayed on monitor cycles every N tics + if (age % 64 == 0) + { + const SINT8 result = get_cycle_result(monitor, + part_index(part) + (age / 64)); + + K_UpdateMobjItemOverlay(part, + K_ItemResultToType(result), + K_ItemResultToAmount(result)); + + center_item_sprite(part, 5*FRACUNIT/4); + } + + flicker(part, is_flickering(part) ? 4 : 2); +} + +static void +translate (mobj_t *part) +{ + const angle_t ang = part_theta(part) + + part_monitor(part)->angle; + + part->angle = (ang - ANGLE_90); + + // Because of MF_NOCLIPTHING, no friction is applied. + // This object is teleported back to the monitor every + // tic so its position is in total only ever translated + // by this much. + part->momx = P_ReturnThrustX(NULL, ang, part->radius); + part->momy = P_ReturnThrustY(NULL, ang, part->radius); +} + +static inline fixed_t +get_damage_multiplier (const mobj_t *monitor) +{ + return FixedDiv(monitor_damage(monitor), HEALTHFACTOR); +} + +static inline boolean +has_state +( const mobj_t * mobj, + statenum_t state) +{ + return mobj->hitlag == 0 && + (size_t)(mobj->state - states) == (size_t)state; +} + +static mobj_t * +adjust_monitor_drop +( mobj_t * monitor, + mobj_t * drop) +{ + P_InstaThrust(drop, drop->angle, 4*mapobjectscale); + + drop->momz *= 8; + + K_FlipFromObject(drop, monitor); + + return drop; +} + +void +Obj_MonitorSpawnParts (mobj_t *monitor) +{ + const size_t nparts = + sizeof monitor_parts / sizeof *monitor_parts; + + size_t i; + + P_SetScale(monitor, (monitor->destscale *= 2)); + + monitor_itemcount(monitor) = 0; + monitor_rngseed(monitor) = P_GetRandSeed(PR_ITEM_ROULETTE); + monitor_spawntic(monitor) = leveltime; + monitor_emerald(monitor) = 0; + + for (i = 0; i < nparts; ++i) + { + spawn_monitor_parts(monitor, &monitor_parts[i]); + } +} + +mobj_t * +Obj_SpawnMonitor +( mobj_t * origin, + UINT8 numItemTypes, + UINT8 emerald) +{ + mobj_t *monitor = P_SpawnMobj(origin->x, origin->y, + origin->z, MT_MONITOR); + + monitor->angle = P_Random(PR_DECORATION); + + monitor_itemcount(monitor) = numItemTypes; + monitor_emerald(monitor) = emerald; + + monitor->color = K_GetChaosEmeraldColor(emerald); + + return monitor; +} + +void +Obj_MonitorPartThink (mobj_t *part) +{ + const statenum_t statenum = part_type(part); + + mobj_t *monitor = part_monitor(part); + + if (part->fuse > 0) + { + return; + } + + if (P_MobjWasRemoved(monitor)) + { + P_RemoveMobj(part); + return; + } + + if (has_state(monitor, monitor->info->deathstate)) + { + kill_monitor_part(part); + return; + } + + if (is_flickering(part)) + { + flicker(part, 2); + } + + if (monitor->hitlag) + { + const fixed_t shake = FixedMul( + 2 * get_damage_multiplier(monitor), + monitor->radius / 8); + + part->sprxoff = P_AltFlip(shake, 2); + part->spryoff = P_AltFlip(shake, 4); + } + else + { + part->sprxoff = 0; + part->spryoff = 0; + } + + switch (statenum) + { + case S_MONITOR_SCREEN1A: + if (has_state(monitor, monitor->info->painstate)) + { + spawn_debris(part); + } + break; + + case S_MONITOR_CRACKA: + case S_MONITOR_CRACKB: + if (monitor->health < monitor->info->spawnhealth) + { + part->sprite = SPR_IMON; // initially SPR_NULL + part->frame = part->state->frame + + (monitor->health / HEALTHFACTOR); + } + break; + + case S_ITEMICON: + project_icon(part); + break; + + default: + break; + } + + P_MoveOrigin(part, monitor->x, monitor->y, monitor->z); + + translate(part); +} + +fixed_t +Obj_MonitorGetDamage +( mobj_t * monitor, + mobj_t * inflictor, + UINT8 damagetype) +{ + fixed_t damage; + + switch (damagetype & DMG_TYPEMASK) + { + case DMG_VOLTAGE: + if (monitor->health < HEALTHFACTOR) + { + return HEALTHFACTOR; + } + else + { + // always reduce to final damage state + return (monitor->health - HEALTHFACTOR) + 1; + } + } + + if (inflictor == NULL) + { + return HEALTHFACTOR; + } + + if (inflictor->player) + { + const fixed_t weight = + K_GetMobjWeight(inflictor, monitor); + + // HEALTHFACTOR is the minimum damage that can be + // dealt but player's weight (and speed) can buff the hit. + damage = HEALTHFACTOR + + (FixedMul(weight, HEALTHFACTOR) / 9); + + if (inflictor->scale > mapobjectscale) + { + damage = P_ScaleFromMap(damage, inflictor->scale); + } + } + else + { + damage = FRACUNIT; // kill instantly + } + + return damage; +} + +void +Obj_MonitorOnDamage +( mobj_t * monitor, + mobj_t * inflictor, + INT32 damage) +{ + monitor->fuse = BATTLE_DESPAWN_TIME; + monitor_damage(monitor) = damage; + monitor_rammingspeed(monitor) = inflictor + ? FixedDiv(FixedHypot(inflictor->momx, inflictor->momy), 4 * inflictor->radius) : 0; + monitor->hitlag = + 6 * get_damage_multiplier(monitor) / FRACUNIT; +} + +void +Obj_MonitorOnDeath (mobj_t *monitor) +{ + const UINT8 itemcount = get_monitor_itemcount(monitor); + const angle_t ang = ANGLE_MAX / itemcount; + const SINT8 flip = P_MobjFlip(monitor); + + INT32 i; + + UINT32 sharedseed = restore_item_rng( + monitor_rngseed(monitor)); + + for (i = 0; i < itemcount; ++i) + { + const SINT8 result = get_item_result(); + const UINT32 localseed = restore_item_rng(sharedseed); + + adjust_monitor_drop(monitor, + K_CreatePaperItem( + monitor->x, monitor->y, monitor->z + (128 * mapobjectscale * flip), + i * ang, flip, + K_ItemResultToType(result), + K_ItemResultToAmount(result))); + + // K_CreatePaperItem may advance RNG, so update our + // copy of the seed afterward + sharedseed = restore_item_rng(localseed); + } + + restore_item_rng(sharedseed); + + if (monitor_emerald(monitor) != 0) + { + adjust_monitor_drop(monitor, + K_SpawnChaosEmerald(monitor->x, monitor->y, monitor->z + (128 * mapobjectscale * flip), + ang, flip, monitor_emerald(monitor))); + } + + spawn_monitor_explosion(monitor); + + // There is hitlag from being damaged, so remove + // tangibility RIGHT NOW. + monitor->flags &= ~(MF_SOLID); +} + +void +Obj_MonitorShardThink (mobj_t *shard) +{ + if (shard_can_roll(shard)) + { + shard->rollangle += ANGLE_45; + } + + shard->renderflags ^= RF_DONTDRAW; +} + +UINT32 +Obj_MonitorGetEmerald (const mobj_t *monitor) +{ + return monitor_emerald(monitor); +} diff --git a/src/p_inter.c b/src/p_inter.c index 9887b8bb7..399d74918 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -1666,6 +1666,10 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget break; } + case MT_MONITOR: + Obj_MonitorOnDeath(target); + break; + default: break; } @@ -2007,6 +2011,17 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da laglength = 0; // handled elsewhere } + switch (target->type) + { + case MT_MONITOR: + damage = Obj_MonitorGetDamage(target, inflictor, damagetype); + Obj_MonitorOnDamage(target, inflictor, damage); + break; + + default: + break; + } + // Everything above here can't be forced. if (!metalrecording) { diff --git a/src/p_map.c b/src/p_map.c index ef4819118..363b73997 100644 --- a/src/p_map.c +++ b/src/p_map.c @@ -1537,6 +1537,18 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) K_KartBouncing(tm.thing, thing); return BMIT_CONTINUE; } + else if (thing->type == MT_MONITOR) + { + // see if it went over / under + if (tm.thing->z > thing->z + thing->height) + return BMIT_CONTINUE; // overhead + if (tm.thing->z + tm.thing->height < thing->z) + return BMIT_CONTINUE; // underneath + + P_DamageMobj(thing, tm.thing, tm.thing, 1, DMG_NORMAL); + K_KartBouncing(tm.thing, thing); + return BMIT_CONTINUE; + } else if (thing->flags & MF_SOLID) { // see if it went over / under diff --git a/src/p_mobj.c b/src/p_mobj.c index 230454a6c..8d4009290 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -3077,6 +3077,17 @@ boolean P_SceneryZMovement(mobj_t *mo) P_RemoveMobj(mo); return false; } + break; + case MT_MONITOR_SHARD: + // Hits the ground + if ((mo->eflags & MFE_VERTICALFLIP) + ? (mo->ceilingz <= (mo->z + mo->height)) + : (mo->z <= mo->floorz)) + { + P_RemoveMobj(mo); + return false; + } + break; default: break; } @@ -6520,6 +6531,9 @@ static void P_MobjSceneryThink(mobj_t *mobj) return; } break; + case MT_MONITOR_SHARD: + Obj_MonitorShardThink(mobj); + break; case MT_VWREF: case MT_VWREB: { @@ -7382,6 +7396,12 @@ static boolean P_MobjRegularThink(mobj_t *mobj) break; } case MT_EMERALD: + { + if (mobj->threshold > 0) + mobj->threshold--; + } + /*FALLTHRU*/ + case MT_MONITOR: { if (battleovertime.enabled >= 10*TICRATE) { @@ -7394,6 +7414,14 @@ static boolean P_MobjRegularThink(mobj_t *mobj) } } + // Don't spawn sparkles on a monitor with no + // emerald inside + if (mobj->type == MT_MONITOR && + Obj_MonitorGetEmerald(mobj) == 0) + { + break; + } + if (leveltime % 3 == 0) { mobj_t *sparkle = P_SpawnMobjFromMobj( @@ -7407,9 +7435,6 @@ static boolean P_MobjRegularThink(mobj_t *mobj) sparkle->color = mobj->color; sparkle->momz += 8 * mobj->scale * P_MobjFlip(mobj); } - - if (mobj->threshold > 0) - mobj->threshold--; } break; case MT_DRIFTEXPLODE: @@ -9414,6 +9439,9 @@ static boolean P_MobjRegularThink(mobj_t *mobj) mobj->colorized = false; } break; + case MT_MONITOR_PART: + Obj_MonitorPartThink(mobj); + break; default: // check mobj against possible water content, before movement code P_MobjCheckWater(mobj); @@ -9519,6 +9547,7 @@ static boolean P_CanFlickerFuse(mobj_t *mobj) case MT_SNAPPER_HEAD: case MT_SNAPPER_LEG: case MT_MINECARTSEG: + case MT_MONITOR_PART: return true; case MT_RANDOMITEM: @@ -10620,6 +10649,10 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type) break; } + case MT_MONITOR: { + Obj_MonitorSpawnParts(mobj); + break; + } case MT_KARMAHITBOX: { const fixed_t rad = FixedMul(mobjinfo[MT_PLAYER].radius, mobj->scale);