blankart/src/k_hud_track.cpp
NepDisk 59b0a77cf1 Port updated HUD tracking system from RR
Minus tooltips since we don't have the RR c++ draw namespace yet...
Also this cleans up both object trackers that are used to be only called on object spawn and removal.
2026-03-06 01:15:23 -05:00

664 lines
13 KiB
C++

// BLANKART
//-----------------------------------------------------------------------------
// Copyright (C) 2025 by Kart Krew.
// Copyright (C) 2026 by Team BlanKart.
//
// 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 <functional>
#include <cstddef>
#include <optional>
#include <variant>
#include <vector>
#include "core/static_vec.hpp"
#include "cxxutil.hpp"
#include "g_game.h"
#include "k_battle.h"
#include "k_hud.h"
#include "k_kart.h"
#include "k_objects.h"
#include "k_specialstage.h"
#include "m_fixed.h"
#include "p_local.h"
#include "p_mobj.h"
#include "r_draw.h"
#include "r_fps.h"
#include "r_main.h"
#include "st_stuff.h"
#include "v_video.h"
#ifdef WIN32
#undef near
#undef far
#endif
consvar_t cv_kartdebughudtracker = CVAR_INIT ("kartdebughudtracker", "Off", CV_SAVE, CV_OnOff, NULL);
using namespace srb2;
extern "C" consvar_t cv_debughudtracker;
namespace
{
enum class Visibility
{
kVisible,
kTransparent,
kFlicker,
};
struct TargetTracking
{
static constexpr int kMaxLayers = 2;
struct Animation
{
int frames;
int tics_per_frame;
StaticVec<patch_t**, kMaxLayers> layers;
int32_t video_flags = 0;
};
struct Graphics
{
struct SplitscreenPair
{
Animation p1;
std::optional<Animation> p4;
};
SplitscreenPair near;
std::optional<SplitscreenPair> far;
};
mobj_t* mobj;
trackingResult_t result;
fixed_t camDist;
bool foreground;
playertagtype_t nametag;
skincolornum_t color() const
{
switch (mobj->type)
{
default:
return K_GetHudColor();
}
}
Animation animation() const
{
const fixed_t farDistance = 1280 * mapobjectscale;
bool useNear = (camDist < farDistance);
Graphics gfx = graphics();
Graphics::SplitscreenPair& pair = useNear || !gfx.far ? gfx.near : *gfx.far;
Animation& anim = r_splitscreen <= 1 || !pair.p4 ? pair.p1 : *pair.p4;
return anim;
}
bool uses_off_screen_arrow() const
{
switch (mobj->type)
{
case MT_WAYPOINT:
return false;
case MT_RANDOMITEM:
if (itembreaker && (mobj->flags2 & MF2_BOSSNOTRAP) && !(mobj->flags2 & MF2_BOSSFLEE))
return true;
else
return false;
break;
default:
return true;
}
}
// Special exception because the tracking math sometimes fails.
bool can_object_be_offscreen() const
{
switch (mobj->type)
{
case MT_RANDOMITEM:
if (itembreaker && (mobj->flags2 & MF2_BOSSNOTRAP) && !(mobj->flags2 & MF2_BOSSFLEE))
return true;
else
return false;
break;
default:
return true;
}
}
const uint8_t* colormap() const
{
const skincolornum_t clr = color();
if (clr != SKINCOLOR_NONE)
{
return R_GetTranslationColormap(TC_RAINBOW, clr, GTC_CACHE);
}
return nullptr;
}
bool is_player_nametag_on_screen() const
{
const player_t* player = mobj->player;
if (nametag == PLAYERTAG_NONE)
{
return false;
}
if (player->spectator)
{
// Not in-game
return false;
}
if (mobj->renderflags & K_GetPlayerDontDrawFlag(stplyr))
{
// Invisible on this screen
return false;
}
if (camDist > 8192*mapobjectscale)
{
// Too far away
return false;
}
if (!P_CheckSight(stplyr->mo, const_cast<mobj_t*>(mobj)))
{
// Can't see
return false;
}
return true;
}
private:
Graphics graphics() const
{
using layers = decltype(Animation::layers);
switch (mobj->type)
{
default:
return {
{ // Near
{8, 2, {kp_itemtarget_near[0]}}, // 1P
{{8, 2, {kp_itemtarget_near[1]}}}, // 4P
},
{{ // Far
{2, 6, foreground ?
layers {kp_itemtarget_far[0], kp_itemtarget_far_text} :
layers {kp_itemtarget_far[0]}}, // 1P
{{2, 6, {kp_itemtarget_far[1]}}}, // 4P
}},
};
}
}
};
bool is_player_tracking_target(player_t *player = stplyr)
{
if ((gametyperules & (GTR_BUMPERS|GTR_CLOSERPLAYERS)) != (GTR_BUMPERS|GTR_CLOSERPLAYERS))
{
return false;
}
/*if (K_Cooperative())
{
return false;
}*/
if (player == nullptr)
{
return false;
}
if (player->spectator)
{
return false;
}
return K_IsPlayerWanted(player);
}
bool is_object_tracking_target(mobj_t* mobj)
{
switch (mobj->type)
{
case MT_RANDOMITEM:
if (itembreaker && (mobj->flags2 & MF2_BOSSNOTRAP) && !(mobj->flags2 & MF2_BOSSFLEE))
return true;
else
return false;
case MT_PLAYER:
return mobj->player != stplyr && is_player_tracking_target(mobj->player);
// FALLTHRU
default:
return false;
}
}
Visibility is_object_visible(const mobj_t* mobj)
{
switch (mobj->type)
{
default:
// Transparent when not visible.
return P_CheckSight(stplyr->mo, const_cast<mobj_t*>(mobj)) ? Visibility::kVisible : Visibility::kTransparent;
}
}
void K_DrawTargetTracking(const TargetTracking& target)
{
if (target.nametag != PLAYERTAG_NONE)
{
K_DrawPlayerTag(target.result.x, target.result.y, target.mobj->player, target.nametag, target.foreground);
return;
}
const trackingResult_t& result = target.result;
Visibility visibility = is_object_visible(target.mobj);
if (visibility == Visibility::kFlicker && (leveltime & 1))
{
return;
}
const uint8_t* colormap = target.colormap();
int32_t timer = 0;
if (target.can_object_be_offscreen() && result.onScreen == false)
{
// Off-screen, draw alongside the borders of the screen.
// Probably the most complicated thing.
if (target.uses_off_screen_arrow() == false)
{
return;
}
int32_t scrVal = 240;
vector2_t screenSize = {};
int32_t borderSize = 7;
vector2_t borderWin = {};
vector2_t borderDir = {};
fixed_t borderLen = FRACUNIT;
vector2_t arrowDir = {};
vector2_t arrowPos = {};
patch_t* arrowPatch = nullptr;
int32_t arrowFlags = 0;
vector2_t targetPos = {};
patch_t* targetPatch = nullptr;
timer = (leveltime / 3);
screenSize.x = vid.scaledwidth;
screenSize.y = vid.scaledheight;
if (r_splitscreen >= 2)
{
// Half-wide screens
screenSize.x >>= 1;
borderSize >>= 1;
}
if (r_splitscreen >= 1)
{
// Half-tall screens
screenSize.y >>= 1;
}
scrVal = std::max(screenSize.x, screenSize.y) - 80;
borderWin.x = screenSize.x - borderSize;
borderWin.y = screenSize.y - borderSize;
arrowDir.x = 0;
arrowDir.y = P_MobjFlip(target.mobj) * FRACUNIT;
// Simply pointing towards the result doesn't work, so inaccurate hack...
borderDir.x = FixedMul(
FixedMul(
FINESINE((result.angle >> ANGLETOFINESHIFT) & FINEMASK),
FINECOSINE((-result.pitch >> ANGLETOFINESHIFT) & FINEMASK)
),
result.fov
);
borderDir.y = FixedMul(FINESINE((-result.pitch >> ANGLETOFINESHIFT) & FINEMASK), result.fov);
borderLen = R_PointToDist2(0, 0, borderDir.x, borderDir.y);
if (borderLen > 0)
{
borderDir.x = FixedDiv(borderDir.x, borderLen);
borderDir.y = FixedDiv(borderDir.y, borderLen);
}
else
{
// Eh just put it at the bottom.
borderDir.x = 0;
borderDir.y = FRACUNIT;
}
if (target.mobj->type == MT_RANDOMITEM
/*|| target.mobj->type == MT_CDUFO*/)
{
targetPatch = kp_itemtarget_icon[(timer/2) & 1];
}
if (abs(borderDir.x) > abs(borderDir.y))
{
// Horizontal arrow
arrowPatch = kp_itemtarget_arrow[1][(timer/2) & 1];
arrowDir.y = 0;
if (borderDir.x < 0)
{
// LEFT
arrowDir.x = -FRACUNIT;
}
else
{
// RIGHT
arrowDir.x = FRACUNIT;
}
}
else
{
// Vertical arrow
arrowPatch = kp_itemtarget_arrow[0][(timer/2) & 1];
arrowDir.x = 0;
if (borderDir.y < 0)
{
// UP
arrowDir.y = -FRACUNIT;
}
else
{
// DOWN
arrowDir.y = FRACUNIT;
}
}
arrowPos.x = (screenSize.x >> 1) + FixedMul(scrVal, borderDir.x);
arrowPos.y = (screenSize.y >> 1) + FixedMul(scrVal, borderDir.y);
arrowPos.x = std::clamp(arrowPos.x, borderSize, borderWin.x) * FRACUNIT;
arrowPos.y = std::clamp(arrowPos.y, borderSize, borderWin.y) * FRACUNIT;
if (targetPatch)
{
targetPos.x = arrowPos.x - (arrowDir.x * 12);
targetPos.y = arrowPos.y - (arrowDir.y * 12);
targetPos.x -= (targetPatch->width << FRACBITS) >> 1;
targetPos.y -= (targetPatch->height << FRACBITS) >> 1;
}
arrowPos.x -= (arrowPatch->width << FRACBITS) >> 1;
arrowPos.y -= (arrowPatch->height << FRACBITS) >> 1;
if (arrowDir.x < 0)
{
arrowPos.x += arrowPatch->width << FRACBITS;
arrowFlags |= V_FLIP;
}
if (arrowDir.y < 0)
{
arrowPos.y += arrowPatch->height << FRACBITS;
arrowFlags |= V_VFLIP;
}
if (targetPatch)
{
V_DrawFixedPatch(targetPos.x, targetPos.y, FRACUNIT, V_SPLITSCREEN, targetPatch, colormap);
}
V_DrawFixedPatch(arrowPos.x, arrowPos.y, FRACUNIT, V_SPLITSCREEN | arrowFlags, arrowPatch, colormap);
}
else
{
// Draw simple overlay.
vector2_t targetPos = {result.x, result.y};
INT32 trans = [&]
{
switch (visibility)
{
case Visibility::kTransparent:
return V_30TRANS;
default:
return target.foreground ? 0 : V_80TRANS;
}
}();
TargetTracking::Animation anim = target.animation();
for (patch_t** array : anim.layers)
{
patch_t* patch = array[(leveltime / anim.tics_per_frame) % anim.frames];
V_DrawFixedPatch(
targetPos.x - (((anim.video_flags & V_FLIP) ? -1 : 1) * (patch->width << (FRACBITS-1))),
targetPos.y - (((anim.video_flags & V_VFLIP) ? -1 : 1) * (patch->height << (FRACBITS-1))),
FRACUNIT,
V_SPLITSCREEN | anim.video_flags | trans,
patch,
colormap
);
};
}
}
void K_CullTargetList(std::vector<TargetTracking>& targetList)
{
constexpr int kBlockWidth = 20;
constexpr int kBlockHeight = 10;
constexpr int kXBlocks = BASEVIDWIDTH / kBlockWidth;
constexpr int kYBlocks = BASEVIDHEIGHT / kBlockHeight;
UINT8 map[kXBlocks][kYBlocks] = {};
constexpr fixed_t kTrackerRadius = 30*FRACUNIT/2; // just an approximation of common HUD tracker
int debugColorCycle = 0;
std::for_each(
targetList.rbegin(),
targetList.rend(),
[&](TargetTracking& tr)
{
if (tr.result.onScreen == false)
{
return;
}
fixed_t x1, x2, y1, y2;
UINT8 bit = 1;
// TODO: there should be some generic system
// instead of this special case.
if (tr.nametag == PLAYERTAG_NAME)
{
const player_t* p = tr.mobj->player;
x1 = tr.result.x;
x2 = tr.result.x + ((6 + V_ThinStringWidth(player_names[p - players], 0)) * FRACUNIT);
y1 = tr.result.y - (30 * FRACUNIT);
y2 = tr.result.y - (4 * FRACUNIT);
bit = 2; // nametags will cull on a separate plane
}
else if (tr.nametag == PLAYERTAG_RIVAL || tr.nametag == PLAYERTAG_CPU)
{
x1 = tr.result.x - (14 * FRACUNIT);
x2 = tr.result.x + (14 * FRACUNIT);
y1 = tr.result.y - (20 * FRACUNIT);
y2 = tr.result.y - (4 * FRACUNIT);
bit = 2; // nametags will cull on a separate plane
}
else if (tr.nametag != PLAYERTAG_NONE)
{
return;
}
else
{
x1 = tr.result.x - kTrackerRadius;
x2 = tr.result.x + kTrackerRadius;
y1 = tr.result.y - kTrackerRadius;
y2 = tr.result.y + kTrackerRadius;
}
x1 = std::max<INT32>(x1 / kBlockWidth / FRACUNIT, 0);
x2 = std::min<INT32>(x2 / kBlockWidth / FRACUNIT, kXBlocks - 1);
y1 = std::max<INT32>(y1 / kBlockHeight / FRACUNIT, 0);
y2 = std::min<INT32>(y2 / kBlockHeight / FRACUNIT, kYBlocks - 1);
bool allMine = true;
for (fixed_t x = x1; x <= x2; ++x)
{
for (fixed_t y = y1; y <= y2; ++y)
{
if (map[x][y] & bit)
{
allMine = false;
}
else
{
map[x][y] |= bit;
if (cv_kartdebughudtracker.value)
{
V_DrawFill(
x * kBlockWidth,
y * kBlockHeight,
kBlockWidth,
kBlockHeight,
(39 + debugColorCycle) | V_SPLITSCREEN
);
}
}
}
}
if (allMine)
{
// This tracker claims every square
tr.foreground = true;
}
if (++debugColorCycle > 8)
{
debugColorCycle = 0;
}
}
);
}
}; // namespace
void K_drawTargetHUD(const vector3_t* origin, player_t* player)
{
std::vector<TargetTracking> targetList;
mobj_t* mobj = nullptr;
mobj_t* next = nullptr;
for (mobj = misccap; mobj; mobj = next)
{
next = mobj->itnext;
if (mobj->health <= 0)
{
continue;
}
bool tracking = is_object_tracking_target(mobj);
playertagtype_t nametag = mobj->player ? K_WhichPlayerTag(mobj->player) : PLAYERTAG_NONE;
if (tracking == false && nametag == PLAYERTAG_NONE)
{
continue;
}
vector3_t pos = {
R_InterpolateFixed(mobj->old_x, mobj->x) + mobj->sprxoff,
R_InterpolateFixed(mobj->old_y, mobj->y) + mobj->spryoff,
R_InterpolateFixed(mobj->old_z, mobj->z) + mobj->sprzoff + (mobj->height >> 1),
};
TargetTracking tr;
tr.mobj = mobj;
tr.camDist = R_PointToDist2(origin->x, origin->y, pos.x, pos.y);
tr.foreground = false;
tr.nametag = PLAYERTAG_NONE;
if (tracking)
{
K_ObjectTracking(&tr.result, &pos, false);
targetList.push_back(tr);
}
if (!mobj->player)
{
continue;
}
tr.nametag = nametag;
if (tr.is_player_nametag_on_screen())
{
fixed_t headOffset = 36*mobj->scale;
if (stplyr->mo->eflags & MFE_VERTICALFLIP)
{
pos.z -= headOffset;
}
else
{
pos.z += headOffset;
}
K_ObjectTracking(&tr.result, &pos, false);
if (tr.result.onScreen == true)
{
targetList.push_back(tr);
}
}
}
// Sort by distance from camera. Further trackers get
// drawn first so nearer ones draw over them.
std::sort(targetList.begin(), targetList.end(), [](const auto& a, const auto& b) { return a.camDist > b.camDist; });
K_CullTargetList(targetList);
std::for_each(targetList.cbegin(), targetList.cend(), K_DrawTargetTracking);
}