diff --git a/src/deh_tables.c b/src/deh_tables.c index 36239071c..c39d4fb67 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -3005,6 +3005,10 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi // MT_STARSTREAM "S_STARSTREAM", + + // MT_SCRIPT_THING + "S_TALKPOINT", + "S_TALKPOINT_ORB", }; // RegEx to generate this from info.h: ^\tMT_([^,]+), --> \t"MT_\1", @@ -3726,6 +3730,7 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t "MT_CHECKPOINT_END", "MT_SCRIPT_THING", + "MT_SCRIPT_THING_ORB", "MT_RIDEROID", "MT_RIDEROIDNODE", diff --git a/src/info.c b/src/info.c index bc5068769..beeb54ccc 100644 --- a/src/info.c +++ b/src/info.c @@ -734,6 +734,9 @@ char sprnames[NUMSPRITES + 1][5] = "SENC", "SEAS", + // Tutorial + "TLKP", // Talk Point + // First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later "VIEW", }; @@ -3515,6 +3518,10 @@ state_t states[NUMSTATES] = // MT_STARSTREAM {SPR_SEAS, FF_ANIMATE|0, 30, {NULL}, 29, 1, S_NULL}, // S_STARSTREAM + + // MT_SCRIPT_THING + {SPR_TLKP, 0|FF_SEMIBRIGHT|FF_PAPERSPRITE, -1, {NULL}, 0, 0, S_NULL}, // S_TALKPOINT + {SPR_TLKP, 1|FF_FULLBRIGHT, -1, {NULL}, 0, 0, S_NULL}, // S_TALKPOINT_ORB }; mobjinfo_t mobjinfo[NUMMOBJTYPES] = @@ -18517,7 +18524,34 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] = { // MT_SCRIPT_THING 4096, // doomednum - S_INVISIBLE, // spawnstate + S_TALKPOINT, // 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 + 16*FRACUNIT, // radius + 16*FRACUNIT, // height + 0, // display offset + 0, // mass + 0, // damage + sfx_None, // activesound + MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOCLIPTHING|MF_NOGRAVITY|MF_SCENERY, // flags + S_NULL // raisestate + }, + + { // MT_SCRIPT_THING_ORB + -1, // doomednum + S_TALKPOINT_ORB, // spawnstate 1000, // spawnhealth S_NULL, // seestate sfx_None, // seesound diff --git a/src/info.h b/src/info.h index da42752e4..d6992b557 100644 --- a/src/info.h +++ b/src/info.h @@ -1269,6 +1269,9 @@ typedef enum sprite SPR_SENC, // Cabotron SPR_SEAS, // Starstream + // Tutorial + SPR_TLKP, // Talk Point + // First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later SPR_VIEW, @@ -4002,6 +4005,10 @@ typedef enum state // MT_STARSTREAM S_STARSTREAM, + // MT_SCRIPT_THING + S_TALKPOINT, + S_TALKPOINT_ORB, + S_FIRSTFREESLOT, S_LASTFREESLOT = S_FIRSTFREESLOT + NUMSTATEFREESLOTS - 1, NUMSTATES @@ -4742,6 +4749,7 @@ typedef enum mobj_type MT_CHECKPOINT_END, MT_SCRIPT_THING, + MT_SCRIPT_THING_ORB, MT_RIDEROID, MT_RIDEROIDNODE, diff --git a/src/k_objects.h b/src/k_objects.h index a14dd7dd1..fa708ae5c 100644 --- a/src/k_objects.h +++ b/src/k_objects.h @@ -392,6 +392,11 @@ void Obj_SSCabotronMobjSpawn(mobj_t* mo); void Obj_SSCabotronMobjThink(mobj_t* mo); void Obj_SSCabotronStarMobjThink(mobj_t* mo); +/* Talk Point */ +void Obj_TalkPointInit(mobj_t* mo); +void Obj_TalkPointThink(mobj_t* mo); +void Obj_TalkPointOrbThink(mobj_t* mo); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index 31c92d74f..fce16395a 100644 --- a/src/objects/CMakeLists.txt +++ b/src/objects/CMakeLists.txt @@ -53,6 +53,7 @@ target_sources(SRB2SDL2 PRIVATE cloud.c waterfall-particle.c sealed-star.c + talk-point.cpp ) add_subdirectory(versus) diff --git a/src/objects/talk-point.cpp b/src/objects/talk-point.cpp new file mode 100644 index 000000000..af510f7d5 --- /dev/null +++ b/src/objects/talk-point.cpp @@ -0,0 +1,231 @@ +// DR. ROBOTNIK'S RING RACERS +//------------------------------- +// 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 + +#include "objects.hpp" + +#include "../doomdef.h" +#include "../g_game.h" +#include "../p_spec.h" +#include "../tables.h" + +using namespace srb2::objects; + +namespace +{ + +Vec2 angle_vector(angle_t x) +{ + return Vec2 {FCOS(x), FSIN(x)}; +} + +struct TalkPoint : Mobj +{ + static constexpr angle_t kSpinSpeed = ANGLE_11hh; + static constexpr tic_t kCollectDuration = 12; + + void thing_args() = delete; + Fixed radius() const { return mobj_t::thing_args[0] * FRACUNIT; } + bool oneshot() const { return !mobj_t::thing_args[1]; } + bool disabled() const { return mobj_t::thing_args[2]; } + bool invisible() const { return mobj_t::thing_args[3]; } + + void extravalue1() = delete; + tic_t collect() const { return mobj_t::extravalue1; } + void collect(tic_t n) { mobj_t::extravalue1 = n; } + + // This value scales up as the radius gets larger + Fixed scaling() const { return std::min(FRACUNIT, Fixed {320 * mapobjectscale} / radius()); } + + void init(); + + void think() + { + if (disabled()) + { + // turned off + return; + } + + if (collect() > 0) + { + collect(collect() - 1); + + if (collect() == 0) + { + remove(); + return; + } + + // Collect animation: stretch upward, spin faster, fade out + spriteyscale(kCollectDuration * FRACUNIT / collect()); + angle += kCollectDuration * kSpinSpeed / collect(); + renderflags = (renderflags & ~RF_TRANSMASK) | + ((9 - (collect() * 9 / kCollectDuration)) << RF_TRANSSHIFT); + return; + } + + angle += kSpinSpeed; + + for (UINT8 i = 0; i < MAXPLAYERS; i++) + { + if (playeringame[i] == false || players[i].spectator == true) + { + continue; + } + + Mobj* player_mobj = static_cast(players[i].mo); + if (!Mobj::valid(player_mobj)) + { + continue; + } + + Fixed dist = (pos2d() - player_mobj->pos2d()).magnitude(); + if (dist < radius()) + { + P_ActivateThingSpecial(this, player_mobj); + + if (oneshot()) + { + // Collect animation: appear brighter during fade-out + renderflags |= RF_FULLBRIGHT; + collect(kCollectDuration); + } + + if (!invisible() && P_IsDisplayPlayer(&players[i])) + { + S_StartSound(nullptr, sfx_hint); + } + + break; + } + } + } +}; + +struct Orb : Mobj +{ + static constexpr std::array kColors = { + SKINCOLOR_NONE, + SKINCOLOR_JAWZ, + SKINCOLOR_AQUAMARINE, + SKINCOLOR_LIME, + SKINCOLOR_BANANA, + SKINCOLOR_CREAMSICLE, + SKINCOLOR_DAWN, + SKINCOLOR_TAFFY, + SKINCOLOR_VIOLET, + SKINCOLOR_RUST, + }; + + void target() = delete; + TalkPoint* origin() const { return Mobj::target(); } + void origin(TalkPoint* n) { Mobj::target(n); } + + void extravalue1() = delete; + angle_t speed() const { return mobj_t::extravalue1; } + void speed(angle_t n) { mobj_t::extravalue1 = n; } + + bool valid() const { return Mobj::valid(origin()) && origin()->valid(); } + + static Orb* spawn(TalkPoint* origin, UINT8 index, angle_t angle) + { + Orb* orb = Mobj::spawn({origin->pos2d() + radius_vector(origin, angle), origin->z}, MT_SCRIPT_THING_ORB); + orb->origin(origin); + + orb->color = kColors[index % kColors.size()]; // cycle colors + orb->colorized = true; + + orb->angle = angle; + orb->sprzoff(20 * mapobjectscale); // roughly eye level with the player + orb->speed(FixedAngle(Fixed {45*FRACUNIT/8} * origin->scaling())); // slower with larger radius + + return orb; + } + + void think() + { + if (!valid()) + { + remove(); + return; + } + + // rotate + angle += speed(); + move_origin({origin()->pos2d() + radius_vector(), origin()->z}); + + // bob + spriteyoffset(8 * FSIN(angle + (leveltime * speed()))); + } + +private: + static vec2 radius_vector(TalkPoint* origin, angle_t angle) + { + // Collect animation: orbs fly outward + return angle_vector(angle) * origin->radius() * origin->spriteyscale(); + } + + vec2 radius_vector() const { return radius_vector(origin(), angle); } +}; + +void TalkPoint::init() +{ + if (invisible()) + { + renderflags |= RF_DONTDRAW; + return; + } + + if (scaling() == 0) + { + return; + } + + // Orbs get spaced further apart as the radius increases + fixed_t orb_rad = Fixed {88 * mapobjectscale} / scaling(); + if (orb_rad == 0) + { + return; + } + + // Spawn more orbs within a larger radius + INT32 count = (Fixed {M_TAU_FIXED} * radius()) / orb_rad; + if (count == 0) + { + return; + } + + angle_t step = ANGLE_MAX / count; + angle_t angle = 0; + for (INT32 i = 0; i < count; ++i) + { + Orb::spawn(this, i, angle); + angle += step; + } +} + +}; // namespace + +void Obj_TalkPointInit(mobj_t* mo) +{ + static_cast(mo)->init(); +} + +void Obj_TalkPointThink(mobj_t* mo) +{ + static_cast(mo)->think(); +} + +void Obj_TalkPointOrbThink(mobj_t* mo) +{ + static_cast(mo)->think(); +} diff --git a/src/p_mobj.c b/src/p_mobj.c index dabd3ff6a..34fbb9b75 100644 --- a/src/p_mobj.c +++ b/src/p_mobj.c @@ -6458,48 +6458,11 @@ static void P_MobjSceneryThink(mobj_t *mobj) Obj_CheckpointThink(mobj); break; case MT_SCRIPT_THING: - { - if (mobj->thing_args[2] != 0) - { - // turned off - break; - } - - UINT8 i; - for (i = 0; i < MAXPLAYERS; i++) - { - if (playeringame[i] == false || players[i].spectator == true) - { - continue; - } - - player_t *player = &players[i]; - if (P_MobjWasRemoved(player->mo) == true) - { - continue; - } - - fixed_t dist = R_PointToDist2( - mobj->x, mobj->y, - player->mo->x, player->mo->y - ); - - if (dist < mobj->thing_args[0] * FRACUNIT) - { - P_ActivateThingSpecial(mobj, player->mo); - - if (mobj->thing_args[1] == 0) - { - P_RemoveMobj(mobj); - return; - } - - break; - } - } - - break; - } + Obj_TalkPointThink(mobj); + return; + case MT_SCRIPT_THING_ORB: + Obj_TalkPointOrbThink(mobj); + return; case MT_SPIKEDTARGET: { if (P_MobjWasRemoved(mobj->target) || (mobj->target->health <= 0) || (mobj->target->z == mobj->target->floorz)) @@ -10605,6 +10568,10 @@ fixed_t P_GetMobjDefaultScale(mobj_t *mobj) return 4*FRACUNIT; case MT_WALLSPIKE: return 2*FRACUNIT; + case MT_SCRIPT_THING: + return 4*FRACUNIT; + case MT_SCRIPT_THING_ORB: + return 2*FRACUNIT; default: break; } @@ -13881,6 +13848,11 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj) Obj_SSWindowMapThingSpawn(mobj, mthing); break; } + case MT_SCRIPT_THING: + { + Obj_TalkPointInit(mobj); + break; + } default: break; } diff --git a/src/sounds.c b/src/sounds.c index f40be3c58..81322c55b 100644 --- a/src/sounds.c +++ b/src/sounds.c @@ -1263,6 +1263,8 @@ sfxinfo_t S_sfx[NUMSFX] = {"ssbmpr", false, 64, 8, -1, NULL, 0, -1, -1, LUMPERROR, "UNDESCRIBED SSBMPR"}, // SF_X4AWAYSOUND {"chcrun", false, 64, 0, -1, NULL, 0, -1, -1, LUMPERROR, "UNDESCRIBED CHCRUN"}, + {"hint", false, 64, 0, -1, NULL, 0, -1, -1, LUMPERROR, "Hint Ring"}, + // Damage sounds {"dmga1", false, 255, 8, -1, NULL, 0, -1, -1, LUMPERROR, "Damaged"}, {"dmga2", false, 255, 8, -1, NULL, 0, -1, -1, LUMPERROR, "Damaged"}, diff --git a/src/sounds.h b/src/sounds.h index bfbd06887..b53450224 100644 --- a/src/sounds.h +++ b/src/sounds.h @@ -1338,6 +1338,9 @@ typedef enum sfx_ssbmpr, sfx_chcrun, + // Tutorial Hint + sfx_hint, + // Damage sounds sfx_dmga1, sfx_dmga2,