diff --git a/src/deh_tables.c b/src/deh_tables.c index 8d767ddf1..9f6d064f5 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -4831,6 +4831,13 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi // MT_IVOBALL "S_IVOBALL", + + "S_SA2_CRATE_DEBRIS", + "S_SA2_CRATE_DEBRIS_E", + "S_SA2_CRATE_DEBRIS_F", + "S_SA2_CRATE_DEBRIS_G", + "S_SA2_CRATE_DEBRIS_H", + "S_SA2_CRATE_DEBRIS_METAL", }; // RegEx to generate this from info.h: ^\tMT_([^,]+), --> \t"MT_\1", @@ -6057,6 +6064,10 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_IVOBALL", "MT_PATROLIVOBALL", "MT_AIRIVOBALL", + + "MT_BOX_SIDE", + "MT_BOX_DEBRIS", + "MT_SA2_CRATE", }; const char *const MOBJFLAG_LIST[] = { diff --git a/src/info.c b/src/info.c index e66a1e06b..d7a8d7152 100644 --- a/src/info.c +++ b/src/info.c @@ -986,6 +986,8 @@ char sprnames[NUMSPRITES + 1][5] = "FBTN", "SFTR", + "SABX", + // First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later "VIEW", }; @@ -5677,6 +5679,13 @@ state_t states[NUMSTATES] = // MT_IVOBALL {SPR_BSPH, 2|FF_SEMIBRIGHT, -1, {NULL}, 0, 0, S_NULL}, // S_IVOBALL + + {SPR_UNKN, FF_FULLBRIGHT, -1, {A_RandomStateRange}, S_SA2_CRATE_DEBRIS_E, S_SA2_CRATE_DEBRIS_H, S_NULL}, // S_SA2_CRATE_DEBRIS + {SPR_SABX, 4, 70, {NULL}, 0, 0, S_NULL}, // S_SA2_CRATE_DEBRIS_E + {SPR_SABX, 5, 70, {NULL}, 0, 0, S_NULL}, // S_SA2_CRATE_DEBRIS_F + {SPR_SABX, 6, 70, {NULL}, 0, 0, S_NULL}, // S_SA2_CRATE_DEBRIS_G + {SPR_SABX, 7, 70, {NULL}, 0, 0, S_NULL}, // S_SA2_CRATE_DEBRIS_H + {SPR_SABX, 12, 70, {NULL}, 0, 0, S_NULL}, // S_SA2_CRATE_DEBRIS_METAL }; mobjinfo_t mobjinfo[NUMMOBJTYPES] = @@ -32200,6 +32209,84 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = MF_SCENERY|MF_SPECIAL|MF_NOGRAVITY, // flags S_NULL // raisestate }, + { // MT_BOX_SIDE + -1, // 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 + 40*FRACUNIT, // radius + 80*FRACUNIT, // height + 0, // dispoffset + 0, // mass + 0, // damage + sfx_None, // activesound + MF_NOBLOCKMAP|MF_NOGRAVITY|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_SCENERY|MF_NOCLIPTHING, // flags + S_NULL // raisestate + }, + { // MT_BOX_DEBRIS + -1, // 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 + 0*FRACUNIT, // radius + 0*FRACUNIT, // height + 0, // dispoffset + 0, // mass + 0, // damage + sfx_None, // activesound + MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_SCENERY|MF_NOCLIPTHING, // flags + S_NULL // raisestate + }, + { // MT_SA2_CRATE + 2529, // doomednum + S_INVISIBLE, // spawnstate + 1, // 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 + 40*FRACUNIT, // radius + 80*FRACUNIT, // height + 0, // dispoffset + 0, // mass + 0, // damage + sfx_None, // activesound + MF_SPECIAL|MF_SOLID|MF_SHOOTABLE|MF_SCENERY|MF_DONTPUNT, // flags + S_NULL // raisestate + }, }; diff --git a/src/info.h b/src/info.h index 9e6d02d8d..4b134d91a 100644 --- a/src/info.h +++ b/src/info.h @@ -1540,6 +1540,8 @@ typedef enum sprite SPR_FBTN, SPR_SFTR, + SPR_SABX, + // First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later SPR_VIEW, @@ -6102,6 +6104,13 @@ typedef enum state // MT_IVOBALL S_IVOBALL, + S_SA2_CRATE_DEBRIS, + S_SA2_CRATE_DEBRIS_E, + S_SA2_CRATE_DEBRIS_F, + S_SA2_CRATE_DEBRIS_G, + S_SA2_CRATE_DEBRIS_H, + S_SA2_CRATE_DEBRIS_METAL, + S_FIRSTFREESLOT, S_LASTFREESLOT = S_FIRSTFREESLOT + NUMSTATEFREESLOTS - 1, NUMSTATES @@ -7348,6 +7357,10 @@ typedef enum mobj_type MT_PATROLIVOBALL, MT_AIRIVOBALL, + MT_BOX_SIDE, + MT_BOX_DEBRIS, + MT_SA2_CRATE, + MT_FIRSTFREESLOT, MT_LASTFREESLOT = MT_FIRSTFREESLOT + NUMMOBJFREESLOTS - 1, NUMMOBJTYPES diff --git a/src/k_objects.h b/src/k_objects.h index 51e7eae96..31195d855 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -321,6 +321,13 @@ void Obj_PatrolIvoBallInit(mobj_t *mo); void Obj_PatrolIvoBallThink(mobj_t *mo); void Obj_PatrolIvoBallTouch(mobj_t *special, mobj_t *toucher); +/* SA2 Crates */ +void Obj_BoxSideThink(mobj_t *mo); +void Obj_TryCrateInit(mobj_t *mo); +boolean Obj_TryCrateThink(mobj_t *mo); +void Obj_TryCrateTouch(mobj_t *special, mobj_t *toucher); +void Obj_TryCrateDamage(mobj_t *target, mobj_t *inflictor); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index 6a420e1cf..19cb75116 100644 --- a/src/objects/CMakeLists.txt +++ b/src/objects/CMakeLists.txt @@ -44,6 +44,7 @@ target_sources(SRB2SDL2 PRIVATE mega-barrier.cpp frost-thrower.cpp ivoball.cpp + crate.cpp ) add_subdirectory(versus) diff --git a/src/objects/crate.cpp b/src/objects/crate.cpp new file mode 100644 index 000000000..4f877a5c4 --- /dev/null +++ b/src/objects/crate.cpp @@ -0,0 +1,376 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2023 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; +}; + +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 || K_PlayerCanPunt(player)); } +}; + +struct AnyBox : Graphic +{ + template + bool visit(F&& visitor); + + void update() + { + visit([](auto box) { box->mobj_t::z++; }); + } +}; + +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; } + + 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); // 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); + } + + 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); } + + void damage(Mobj* inflictor) + { + if (!damage_valid(inflictor)) + { + return; + } + + 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(); + } + +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) { return std::tuple(rng(-x, x), rng(-x, x), rng(0, x)); }; + + auto spawn = [&] + { + 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; + }; + + spawn(); + spawn(); + spawn(); + spawn(); + spawn(); + spawn(); + } + + 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); + } + } + + void damage(Toucher* inflictor) + { + if (!Box::damage_valid(inflictor)) + { + return; + } + + if (metal() && !inflictor->boosting()) + { + return; + } + + Box::damage(inflictor); + } +}; + +template +bool AnyBox::visit(F&& visitor) +{ + switch (type) + { + case MT_SA2_CRATE: + 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)); }); +} + +void Obj_TryCrateDamage(mobj_t* target, mobj_t* inflictor) +{ + static_cast(target)->visit([&](auto box) { box->damage(static_cast(inflictor)); }); +} diff --git a/src/p_inter.c b/src/p_inter.c index c60714c81..d7634f6ea 100644 --- a/src/p_inter.c +++ b/src/p_inter.c @@ -988,6 +988,12 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck) return; } + case MT_SA2_CRATE: + { + Obj_TryCrateTouch(special, toucher); + return; + } + default: // SOC or script pickup P_SetTarget(&special->target, toucher); break; @@ -2848,6 +2854,10 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da Obj_BallSwitchDamaged(target, inflictor, source); return false; + case MT_SA2_CRATE: + Obj_TryCrateDamage(target, inflictor); + return true; + default: break; } diff --git a/src/p_map.c b/src/p_map.c index 216d7c169..53ca852db 100644 --- a/src/p_map.c +++ b/src/p_map.c @@ -1200,8 +1200,6 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) return BMIT_CONTINUE; } - //} - if ((thing->type == MT_SPRINGSHELL || thing->type == MT_YELLOWSHELL) && thing->health > 0 && (tm.thing->player || (tm.thing->flags & MF_PUSHABLE)) && tm.thing->health > 0) { @@ -1634,6 +1632,30 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing) } } + switch (tm.thing->type) + { + case MT_SA2_CRATE: + // Let crates stack on top of solid objects (this + // includes other crates). + if (thing->flags & MF_SOLID) + { + fixed_t thingtop = thing->z + thing->height; + if (tm.thing->z > thing->z && thingtop > tm.floorz) + { + tm.floorz = thingtop; + tm.floorrover = NULL; + tm.floorslope = NULL; + tm.floorpic = -1; + tm.floorstep = 0; + return BMIT_CONTINUE; + } + } + break; + + default: + break; + } + // This code is causing conflicts for Ring Racers, // as solid objects cause bumping. If you need to // bring back this code for a moving platform-style diff --git a/src/p_mobj.c b/src/p_mobj.c index 2578f13af..a01c3173b 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -6836,6 +6836,19 @@ static void P_MobjSceneryThink(mobj_t *mobj) Obj_PatrolIvoBallThink(mobj); return; } + case MT_SA2_CRATE: + { + if (!Obj_TryCrateThink(mobj)) + { + return; + } + break; + } + case MT_BOX_SIDE: + { + Obj_BoxSideThink(mobj); + return; + } case MT_VWREF: case MT_VWREB: { @@ -14476,6 +14489,11 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj) Obj_PatrolIvoBallInit(mobj); break; } + case MT_SA2_CRATE: + { + Obj_TryCrateInit(mobj); + break; + } default: break; }