From 706d1b6978cb579ff73410cf2181a32c5018dc7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Lu=C3=ADs=20Vaz=20Silva?= Date: Mon, 19 May 2025 02:52:17 -0300 Subject: [PATCH] Scriptified scoreboard drawing --- src/common/2d/v_draw.cpp | 45 ++ .../scripting/interface/stringformat.cpp | 46 ++ src/ct_chat.cpp | 4 +- src/d_netinfo.cpp | 6 + src/hu_scores.cpp | 344 +------------- src/hu_stuff.h | 10 +- src/scripting/vmthunks.cpp | 7 + src/wi_stuff.cpp | 63 --- wadsrc/static/zscript.txt | 1 + wadsrc/static/zscript/actors/player/player.zs | 2 + wadsrc/static/zscript/constants.zs | 2 + wadsrc/static/zscript/doombase.zs | 2 + wadsrc/static/zscript/engine/base.zs | 11 + .../zscript/ui/statscreen/statscreen.zs | 39 +- .../static/zscript/ui/statusbar/scoreboard.zs | 449 ++++++++++++++++++ .../static/zscript/ui/statusbar/statusbar.zs | 1 + 16 files changed, 617 insertions(+), 415 deletions(-) create mode 100644 wadsrc/static/zscript/ui/statusbar/scoreboard.zs diff --git a/src/common/2d/v_draw.cpp b/src/common/2d/v_draw.cpp index 247a2916d..eb9529925 100644 --- a/src/common/2d/v_draw.cpp +++ b/src/common/2d/v_draw.cpp @@ -1840,6 +1840,51 @@ DEFINE_ACTION_FUNCTION(_Screen, DrawLineFrame) return 0; } +DEFINE_ACTION_FUNCTION(_Screen, GetTextureWidth) +{ + PARAM_PROLOGUE; + PARAM_INT(textureId); + PARAM_BOOL(animated); + + FGameTexture *tex = textureId >= 1 ? TexMan.GetGameTexture(FSetTextureID(textureId), animated) : nullptr; + + ACTION_RETURN_FLOAT(tex ? tex->GetDisplayWidth() : -1); +} + +DEFINE_ACTION_FUNCTION(_Screen, GetTextureHeight) +{ + PARAM_PROLOGUE; + PARAM_INT(textureId); + PARAM_BOOL(animated); + + FGameTexture *tex = textureId >= 1 ? TexMan.GetGameTexture(FSetTextureID(textureId), animated) : nullptr; + + ACTION_RETURN_FLOAT(tex ? tex->GetDisplayHeight() : -1); +} + + +DEFINE_ACTION_FUNCTION(_Screen, GetTextureLeftOffset) +{ + PARAM_PROLOGUE; + PARAM_INT(textureId); + PARAM_BOOL(animated); + + FGameTexture *tex = textureId >= 1 ? TexMan.GetGameTexture(FSetTextureID(textureId), animated) : nullptr; + + ACTION_RETURN_FLOAT(tex ? tex->GetDisplayLeftOffset() : -1); +} + +DEFINE_ACTION_FUNCTION(_Screen, GetTextureTopOffset) +{ + PARAM_PROLOGUE; + PARAM_INT(textureId); + PARAM_BOOL(animated); + + FGameTexture *tex = textureId >= 1 ? TexMan.GetGameTexture(FSetTextureID(textureId), animated) : nullptr; + + ACTION_RETURN_FLOAT(tex ? tex->GetDisplayTopOffset() : -1); +} + DEFINE_ACTION_FUNCTION(FCanvas, DrawLineFrame) { PARAM_SELF_PROLOGUE(FCanvas); diff --git a/src/common/scripting/interface/stringformat.cpp b/src/common/scripting/interface/stringformat.cpp index 3d7a3c428..b5b88a695 100644 --- a/src/common/scripting/interface/stringformat.cpp +++ b/src/common/scripting/interface/stringformat.cpp @@ -564,6 +564,52 @@ DEFINE_ACTION_FUNCTION_NATIVE(FStringStruct, Substitute, StringSubst) return 0; } +static int StringCompare(FString *self, const FString &other) +{ + return self->Compare(other); +} + +DEFINE_ACTION_FUNCTION_NATIVE(FStringStruct, Compare, StringCompare) +{ + PARAM_SELF_STRUCT_PROLOGUE(FString); + PARAM_STRING(other); + ACTION_RETURN_INT(StringCompare(self, other)); +} + +static int StringCompareNoCase(FString *self, const FString &other) +{ + return self->CompareNoCase(other); +} + +DEFINE_ACTION_FUNCTION_NATIVE(FStringStruct, CompareNoCase, StringCompareNoCase) +{ + PARAM_SELF_STRUCT_PROLOGUE(FString); + PARAM_STRING(other); + ACTION_RETURN_INT(StringCompareNoCase(self, other)); +} + +static int StringIsEmpty(FString *self) +{ + return self->IsEmpty(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(FStringStruct, IsEmpty, StringIsEmpty) +{ + PARAM_SELF_STRUCT_PROLOGUE(FString); + ACTION_RETURN_INT(StringIsEmpty(self)); +} + +static int StringIsNotEmpty(FString *self) +{ + return self->IsNotEmpty(); +} + +DEFINE_ACTION_FUNCTION_NATIVE(FStringStruct, IsNotEmpty, StringIsNotEmpty) +{ + PARAM_SELF_STRUCT_PROLOGUE(FString); + ACTION_RETURN_INT(StringIsNotEmpty(self)); +} + static void StringStripRight(FString* self, const FString& junk) { if (junk.IsNotEmpty()) self->StripRight(junk); diff --git a/src/ct_chat.cpp b/src/ct_chat.cpp index 20636e859..e8ebcca45 100644 --- a/src/ct_chat.cpp +++ b/src/ct_chat.cpp @@ -45,6 +45,7 @@ #include "c_buttons.h" #include "d_buttons.h" #include "v_draw.h" +#include "r_utility.h" enum { @@ -241,6 +242,7 @@ void CT_PasteChat(const char *clip) void CT_Drawer (void) { + auto &vp = r_viewpoint; auto drawer = twod; FFont *displayfont = NewConsoleFont; @@ -254,7 +256,7 @@ void CT_Drawer (void) { // todo: check for summary screen } - if (!skipit) HU_DrawScores (&players[consoleplayer]); + if (!skipit) HU_DrawScores (consoleplayer, vp.TicFrac); } if (chatmodeon) { diff --git a/src/d_netinfo.cpp b/src/d_netinfo.cpp index bac6eddfe..c528aa5d1 100644 --- a/src/d_netinfo.cpp +++ b/src/d_netinfo.cpp @@ -232,6 +232,12 @@ DEFINE_ACTION_FUNCTION(_PlayerInfo, GetDisplayColor) ACTION_RETURN_INT(c); } +DEFINE_ACTION_FUNCTION(_PlayerInfo, GetAverageLatency) +{ + PARAM_SELF_STRUCT_PROLOGUE(player_t); + ACTION_RETURN_INT(ClientStates[self - players].AverageLatency); +} + // Find out which teams are present. If there is only one, // then another team should be chosen at random. // diff --git a/src/hu_scores.cpp b/src/hu_scores.cpp index f88621917..be93c6180 100644 --- a/src/hu_scores.cpp +++ b/src/hu_scores.cpp @@ -67,6 +67,7 @@ static void HU_DoDrawScores (player_t *, player_t *[MAXPLAYERS]); static void HU_DrawTimeRemaining (int y); static void HU_DrawPlayer(player_t *, bool, int, int, int, int, int, int, int, int, int); +static void HU_DrawColorBar(int x, int y, int height, int playernum); // EXTERNAL DATA DECLARATIONS ---------------------------------------------- @@ -132,8 +133,6 @@ void HU_SortPlayers */ bool SB_ForceActive = false; -static FFont *displayFont; -static int FontScale; // PRIVATE DATA DEFINITIONS ------------------------------------------------ @@ -146,345 +145,12 @@ static int FontScale; // //========================================================================== -void HU_DrawScores(player_t* player) +void HU_DrawScores(int player, double ticFrac) { - displayFont = NewSmallFont; - FontScale = max(screen->GetHeight() / 400, 1); - - int numPlayers = 0; - for (int i = 0; i < MAXPLAYERS; ++i) - numPlayers += playeringame[i]; - - if (numPlayers > 8) - FontScale = static_cast(ceil(FontScale * 0.75)); - - if (deathmatch) + IFVIRTUALPTR(StatusBar, DBaseStatusBar, Scoreboard_DrawScores) { - if (teamplay) - { - if (!sb_teamdeathmatch_enable) - return; - } - else if (!sb_deathmatch_enable) - { - return; - } - } - else if (!multiplayer || !sb_cooperative_enable) - { - return; - } - - player_t* sortedPlayers[MAXPLAYERS]; - if (player->camera && player->camera->player) - player = player->camera->player; - - sortedPlayers[MAXPLAYERS - 1] = player; - for (int i = 0, j = 0; j < MAXPLAYERS - 1; ++i, ++j) - { - if (&players[i] == player) - ++i; - sortedPlayers[j] = &players[i]; - } - - if (teamplay && deathmatch) - qsort(sortedPlayers, MAXPLAYERS, sizeof(player_t*), compareteams); - else - qsort(sortedPlayers, MAXPLAYERS, sizeof(player_t*), comparepoints); - - HU_DoDrawScores(player, sortedPlayers); -} - -//========================================================================== -// -// HU_GetPlayerWidths -// -// Returns the widest player name and class icon. -// -//========================================================================== - -void HU_GetPlayerWidths(int& maxNameWidth, int& maxScoreWidth, int& maxIconHeight) -{ - constexpr char NameLabel[] = "Name"; - - displayFont = NewSmallFont; - maxNameWidth = displayFont->StringWidth(NameLabel); - maxScoreWidth = 0; - maxIconHeight = 0; - - for (int i = 0; i < MAXPLAYERS; ++i) - { - if (!playeringame[i]) - continue; - - int width = displayFont->StringWidth(players[i].userinfo.GetName(16)); - if (width > maxNameWidth) - maxNameWidth = width; - - auto icon = FSetTextureID(players[i].mo->IntVar(NAME_ScoreIcon)); - if (icon.isValid()) - { - auto pic = TexMan.GetGameTexture(icon); - width = int(pic->GetDisplayWidth() - pic->GetDisplayLeftOffset() + 2.5); - if (width > maxScoreWidth) - maxScoreWidth = width; - - // The icon's top offset does not count toward its height, because - // zdoom.pk3's standard Hexen class icons are designed that way. - int height = int(pic->GetDisplayHeight() - pic->GetDisplayTopOffset() + 0.5); - if (height > maxIconHeight) - maxIconHeight = height; - } - } -} - -//========================================================================== -// -// HU_DoDrawScores -// -//========================================================================== - -static void HU_DrawFontScaled(double x, double y, int color, const char *text) -{ - DrawText(twod, displayFont, color, x / FontScale, y / FontScale, text, DTA_VirtualWidth, twod->GetWidth() / FontScale, DTA_VirtualHeight, twod->GetHeight() / FontScale, TAG_END); -} - -static void HU_DoDrawScores(player_t* player, player_t* sortedPlayers[MAXPLAYERS]) -{ - int color = sb_cooperative_headingcolor; - if (deathmatch) - { - if (teamplay) - color = sb_teamdeathmatch_headingcolor; - else - color = sb_deathmatch_headingcolor; - } - - int maxNameWidth, maxScoreWidth, maxIconHeight; - HU_GetPlayerWidths(maxNameWidth, maxScoreWidth, maxIconHeight); - int height = displayFont->GetHeight() * FontScale; - int lineHeight = max(height, maxIconHeight * CleanYfac); - int yPadding = (lineHeight - height + 1) / 2; - - int bottom = StatusBar->GetTopOfStatusbar(); - int y = max(48 * CleanYfac, (bottom - MAXPLAYERS * (height + CleanYfac + 1)) / 2); - - HU_DrawTimeRemaining(bottom - height); - - if (teamplay && deathmatch) - { - y -= (BigFont->GetHeight() + 8) * CleanYfac; - - for (size_t i = 0u; i < Teams.Size(); ++i) - { - Teams[i].m_iPlayerCount = 0; - Teams[i].m_iScore = 0; - } - - int numTeams = 0; - for (int i = 0; i < MAXPLAYERS; ++i) - { - if (playeringame[sortedPlayers[i]-players] && FTeam::IsValid(sortedPlayers[i]->userinfo.GetTeam())) - { - if (Teams[sortedPlayers[i]->userinfo.GetTeam()].m_iPlayerCount++ == 0) - ++numTeams; - - Teams[sortedPlayers[i]->userinfo.GetTeam()].m_iScore += sortedPlayers[i]->fragcount; - } - } - - int scoreXWidth = twod->GetWidth() / max(8, numTeams); - int numScores = 0; - for (size_t i = 0u; i < Teams.Size(); ++i) - { - if (Teams[i].m_iPlayerCount) - ++numScores; - } - - int scoreX = (twod->GetWidth() - scoreXWidth * (numScores - 1)) / 2; - for (size_t i = 0u; i < Teams.Size(); ++i) - { - if (!Teams[i].m_iPlayerCount) - continue; - - char score[80]; - mysnprintf(score, countof(score), "%d", Teams[i].m_iScore); - - DrawText(twod, BigFont, Teams[i].GetTextColor(), - scoreX - BigFont->StringWidth(score)*CleanXfac/2, y, score, - DTA_CleanNoMove, true, TAG_DONE); - - scoreX += scoreXWidth; - } - - y += (BigFont->GetHeight() + 8) * CleanYfac; - } - - const char* text_color = GStrings.GetString("SCORE_COLOR"), - *text_frags = GStrings.GetString(deathmatch ? "SCORE_FRAGS" : "SCORE_KILLS"), - *text_name = GStrings.GetString("SCORE_NAME"), - *text_delay = GStrings.GetString("SCORE_DELAY"); - - int col2 = (displayFont->StringWidth(text_color) + 16) * FontScale; - int col3 = col2 + (displayFont->StringWidth(text_frags) + 16) * FontScale; - int col4 = col3 + maxScoreWidth * FontScale; - int col5 = col4 + (maxNameWidth + 16) * FontScale; - int x = (twod->GetWidth() >> 1) - (((displayFont->StringWidth(text_delay) * FontScale) + col5) >> 1); - - //HU_DrawFontScaled(x, y, color, text_color); - HU_DrawFontScaled(x + col2, y, color, text_frags); - HU_DrawFontScaled(x + col4, y, color, text_name); - HU_DrawFontScaled(x + col5, y, color, text_delay); - - y += height + 6 * CleanYfac; - bottom -= height; - - for (int i = 0; i < MAXPLAYERS && y <= bottom; ++i) - { - if (!playeringame[sortedPlayers[i] - players]) - continue; - - HU_DrawPlayer(sortedPlayers[i], player == sortedPlayers[i], x, col2, col3, col4, col5, maxNameWidth, y, yPadding, lineHeight); - y += lineHeight + CleanYfac; - } -} - -//========================================================================== -// -// HU_DrawTimeRemaining -// -//========================================================================== - -static void HU_DrawTimeRemaining (int y) -{ - if (deathmatch && timelimit && gamestate == GS_LEVEL) - { - char str[80]; - int timeleft = (int)(timelimit * TICRATE * 60) - primaryLevel->maptime; - int hours, minutes, seconds; - - if (timeleft < 0) - timeleft = 0; - - hours = timeleft / (TICRATE * 3600); - timeleft -= hours * TICRATE * 3600; - minutes = timeleft / (TICRATE * 60); - timeleft -= minutes * TICRATE * 60; - seconds = timeleft / TICRATE; - - if (hours) - mysnprintf (str, countof(str), "Level ends in %d:%02d:%02d", hours, minutes, seconds); - else - mysnprintf (str, countof(str), "Level ends in %d:%02d", minutes, seconds); - - HU_DrawFontScaled(twod->GetWidth() / 2 - displayFont->StringWidth(str) / 2 * FontScale, y, CR_GRAY, str); - } -} - -//========================================================================== -// -// HU_DrawPlayer -// -//========================================================================== - -static void HU_DrawPlayer (player_t *player, bool highlight, int col1, int col2, int col3, int col4, int col5, int maxnamewidth, int y, int ypadding, int height) -{ - int color; - char str[80]; - - if (highlight) - { - // The teamplay mode uses colors to show teams, so we need some - // other way to do highlighting. And it may as well be used for - // all modes for the sake of consistancy. - Dim(twod, MAKERGB(200,245,255), 0.125f, col1 - 12*FontScale, y - 1, col5 + (maxnamewidth + 24)*FontScale, height + 2); - } - - col2 += col1; - col3 += col1; - col4 += col1; - col5 += col1; - - color = HU_GetRowColor(player, highlight); - HU_DrawColorBar(col1, y, height, (int)(player - players)); - mysnprintf (str, countof(str), "%d", deathmatch ? player->fragcount : player->killcount); - - HU_DrawFontScaled(col2, y + ypadding, color, player->playerstate == PST_DEAD && !deathmatch ? "DEAD" : str); - - auto icon = FSetTextureID(player->mo->IntVar(NAME_ScoreIcon)); - if (icon.isValid()) - { - DrawTexture(twod, icon, false, col3, y, - DTA_CleanNoMove, true, - TAG_DONE); - } - - HU_DrawFontScaled(col4, y + ypadding, color, player->userinfo.GetName()); - - mysnprintf(str, countof(str), "%u", ClientStates[player - players].AverageLatency); - - HU_DrawFontScaled(col5, y + ypadding, color, str); - - const int team = player->userinfo.GetTeam(); - if (team != TEAM_NONE && teamplay && Teams[team].GetLogo().IsNotEmpty ()) - { - auto pic = TexMan.GetGameTextureByName(Teams[player->userinfo.GetTeam()].GetLogo().GetChars ()); - DrawTexture(twod, pic, col1 - (pic->GetDisplayWidth() + 2) * CleanXfac, y, - DTA_CleanNoMove, true, TAG_DONE); - } -} - -//========================================================================== -// -// HU_DrawColorBar -// -//========================================================================== - -void HU_DrawColorBar(int x, int y, int height, int playernum) -{ - float h, s, v, r, g, b; - - D_GetPlayerColor (playernum, &h, &s, &v, NULL); - HSVtoRGB (&r, &g, &b, h, s, v); - - ClearRect(twod, x, y, x + 24*FontScale, y + height, -1, - MAKEARGB(255,clamp(int(r*255.f),0,255), - clamp(int(g*255.f),0,255), - clamp(int(b*255.f),0,255))); -} - -//========================================================================== -// -// HU_GetRowColor -// -//========================================================================== - -int HU_GetRowColor(player_t *player, bool highlight) -{ - if (teamplay && deathmatch) - { - if (FTeam::IsValid (player->userinfo.GetTeam())) - return Teams[player->userinfo.GetTeam()].GetTextColor(); - else - return CR_GREY; - } - else - { - if (!highlight) - { - if (demoplayback && player == &players[consoleplayer]) - { - return CR_GOLD; - } - else - { - return deathmatch ? sb_deathmatch_otherplayercolor : sb_cooperative_otherplayercolor; - } - } - else - { - return deathmatch ? sb_deathmatch_yourplayercolor : sb_cooperative_yourplayercolor; - } + VMValue params[] = { (DObject*)StatusBar, player, ticFrac }; + VMCall(func, params, countof(params), nullptr, 0); } } diff --git a/src/hu_stuff.h b/src/hu_stuff.h index 01cf429cc..d9f05089d 100644 --- a/src/hu_stuff.h +++ b/src/hu_stuff.h @@ -42,16 +42,8 @@ void CT_Drawer (void); // [RH] Draw deathmatch scores -void HU_DrawScores(player_t* me); -void HU_GetPlayerWidths(int& maxNameWidth, int& maxScoreWidth, int& maxIconHeight); -void HU_DrawColorBar(int x, int y, int height, int playernum); -int HU_GetRowColor(player_t *player, bool hightlight); +void HU_DrawScores(int me, double ticFrac); extern bool SB_ForceActive; -// Sorting routines - -int comparepoints(const void *arg1, const void *arg2); -int compareteams(const void *arg1, const void *arg2); - #endif diff --git a/src/scripting/vmthunks.cpp b/src/scripting/vmthunks.cpp index 17b44a27d..8db1e73ec 100644 --- a/src/scripting/vmthunks.cpp +++ b/src/scripting/vmthunks.cpp @@ -2369,6 +2369,13 @@ DEFINE_ACTION_FUNCTION_NATIVE(FLevelLocals, GetUDMFString, ZGetUDMFString) ACTION_RETURN_STRING(GetUDMFString(self, type, index, key)); } +DEFINE_ACTION_FUNCTION(FLevelLocals, PlayerNum) +{ + PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); + PARAM_POINTER(player, player_t); + ACTION_RETURN_INT(self->PlayerNum(player)); +} + DEFINE_ACTION_FUNCTION(FLevelLocals, GetChecksum) { PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals); diff --git a/src/wi_stuff.cpp b/src/wi_stuff.cpp index 8a5c7945c..99fc66b3d 100644 --- a/src/wi_stuff.cpp +++ b/src/wi_stuff.cpp @@ -1219,69 +1219,6 @@ DObject* WI_Start(wbstartstruct_t *wbstartstruct) // //==================================================================== -DEFINE_ACTION_FUNCTION(DStatusScreen, GetPlayerWidths) -{ - PARAM_PROLOGUE; - int maxnamewidth, maxscorewidth, maxiconheight; - HU_GetPlayerWidths(maxnamewidth, maxscorewidth, maxiconheight); - if (numret > 0) ret[0].SetInt(maxnamewidth); - if (numret > 1) ret[1].SetInt(maxscorewidth); - if (numret > 2) ret[2].SetInt(maxiconheight); - return min(numret, 3); -} - -//==================================================================== -// -// -// -//==================================================================== - -DEFINE_ACTION_FUNCTION(DStatusScreen, GetRowColor) -{ - PARAM_PROLOGUE; - PARAM_POINTER(p, player_t); - PARAM_BOOL(highlight); - ACTION_RETURN_INT(HU_GetRowColor(p, highlight)); -} - -//==================================================================== -// -// -// -//==================================================================== - -DEFINE_ACTION_FUNCTION(DStatusScreen, GetSortedPlayers) -{ - PARAM_PROLOGUE; - PARAM_POINTER(array, TArray); - PARAM_BOOL(teamplay); - - player_t *sortedplayers[MAXPLAYERS]; - // Sort all players - for (int i = 0; i < MAXPLAYERS; i++) - { - sortedplayers[i] = &players[i]; - } - - if (teamplay) - qsort(sortedplayers, MAXPLAYERS, sizeof(player_t *), compareteams); - else - qsort(sortedplayers, MAXPLAYERS, sizeof(player_t *), comparepoints); - - array->Resize(MAXPLAYERS); - for (unsigned i = 0; i < MAXPLAYERS; i++) - { - (*array)[i] = int(sortedplayers[i] - players); - } - return 0; -} - -//==================================================================== -// -// -// -//==================================================================== - DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, skills); DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, sitems); DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, ssecret); diff --git a/wadsrc/static/zscript.txt b/wadsrc/static/zscript.txt index 371063dab..55469c4e4 100644 --- a/wadsrc/static/zscript.txt +++ b/wadsrc/static/zscript.txt @@ -308,6 +308,7 @@ version "4.15.1" #include "zscript/ui/statusbar/sbarinfowrapper.zs" #include "zscript/ui/statusbar/statusbar.zs" #include "zscript/ui/statusbar/strife_sbar.zs" +#include "zscript/ui/statusbar/scoreboard.zs" #include "zscript/ui/intermission.zs" diff --git a/wadsrc/static/zscript/actors/player/player.zs b/wadsrc/static/zscript/actors/player/player.zs index eda846144..7eab28a19 100644 --- a/wadsrc/static/zscript/actors/player/player.zs +++ b/wadsrc/static/zscript/actors/player/player.zs @@ -3002,6 +3002,8 @@ struct PlayerInfo native play // self is what internally is known as player_t native clearscope bool GetClassicFlight() const; native void SendPitchLimits(); native clearscope bool HasWeaponsInSlot(int slot) const; + + native clearscope int GetAverageLatency() const; // The actual implementation is on PlayerPawn where it can be overridden. Use that directly in the future. deprecated("3.7", "MorphPlayer() should be used on a PlayerPawn object") bool MorphPlayer(PlayerInfo activator, class spawnType, int duration, EMorphFlags style, class enterFlash = "TeleportFog", class exitFlash = "TeleportFog") diff --git a/wadsrc/static/zscript/constants.zs b/wadsrc/static/zscript/constants.zs index d2122f9fe..ba2598093 100644 --- a/wadsrc/static/zscript/constants.zs +++ b/wadsrc/static/zscript/constants.zs @@ -1,6 +1,8 @@ // for flag changer functions. const FLAG_NO_CHANGE = -1; const MAXPLAYERS = 64; +const TEAM_NONE = 255; +const TEAM_MAXIMUM = 16; enum EStateUseFlags { diff --git a/wadsrc/static/zscript/doombase.zs b/wadsrc/static/zscript/doombase.zs index 91c06ff48..4c5d4799c 100644 --- a/wadsrc/static/zscript/doombase.zs +++ b/wadsrc/static/zscript/doombase.zs @@ -587,6 +587,8 @@ struct LevelLocals native native clearscope int ActorOnLineSide(Actor mo, Line l) const; native clearscope int BoxOnLineSide(Vector2 pos, double radius, Line l) const; + native clearscope int PlayerNum(PlayerInfo player) const; + native String GetChecksum() const; native void ChangeSky(TextureID sky1, TextureID sky2 ); diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs index ee9161656..63e315649 100644 --- a/wadsrc/static/zscript/engine/base.zs +++ b/wadsrc/static/zscript/engine/base.zs @@ -578,6 +578,11 @@ struct Screen native native static void ClearStencil(); native static void SetTransform(Shape2DTransform transform); native static void ClearTransform(); + + native static double GetTextureWidth(TextureID texture, bool animated = false); + native static double GetTextureHeight(TextureID texture, bool animated = false); + native static double GetTextureLeftOffset(TextureID texture, bool animated = false); + native static double GetTextureTopOffset(TextureID texture, bool animated = false); } struct Font native @@ -948,6 +953,12 @@ struct StringStruct native unsafe(internal) native void StripLeft(String junk = ""); native void StripRight(String junk = ""); native void StripLeftRight(String junk = ""); + + native int Compare(String other) const; // strcmp + native int CompareNoCase(String other) const; // stricmp + + native bool IsEmpty() const; // strcmp + native bool IsNotEmpty() const; // stricmp } struct Translation version("2.4") diff --git a/wadsrc/static/zscript/ui/statscreen/statscreen.zs b/wadsrc/static/zscript/ui/statscreen/statscreen.zs index 3ae9cf447..94343f80d 100644 --- a/wadsrc/static/zscript/ui/statscreen/statscreen.zs +++ b/wadsrc/static/zscript/ui/statscreen/statscreen.zs @@ -967,7 +967,40 @@ class StatusScreen : ScreenJob abstract version("2.5") protected virtual void updateStats() {} protected virtual void drawStats() {} - native static int, int, int GetPlayerWidths(); - native static Color GetRowColor(PlayerInfo player, bool highlight); - native static void GetSortedPlayers(in out Array sorted, bool teamplay); + static int, int, int GetPlayerWidths() + { + int maxNameWidth; + int maxScoreWidth; + int maxIconHeight; + + StatusBar.Scoreboard_GetPlayerWidths(maxNameWidth, maxScoreWidth, maxIconHeight); + + return maxNameWidth, maxScoreWidth, maxIconHeight; + } + + static Color GetRowColor(PlayerInfo player, bool highlight) + { + return StatusBar.Scoreboard_GetRowColor(player, highlight); + } + + static void GetSortedPlayers(in out Array sorted, bool teamplay) + { + sorted.clear(); + for(int i = 0; i < MAXPLAYERS; i++) + { + if(playeringame[i]) + { + sorted.Push(i); + } + } + + if(teamplay) + { + StatusBar.Scoreboard_SortPlayers(sorted, BaseStatusBar.Scoreboard_CompareByTeams); + } + else + { + StatusBar.Scoreboard_SortPlayers(sorted, BaseStatusBar.Scoreboard_CompareByPoints); + } + } } diff --git a/wadsrc/static/zscript/ui/statusbar/scoreboard.zs b/wadsrc/static/zscript/ui/statusbar/scoreboard.zs new file mode 100644 index 000000000..7b8952770 --- /dev/null +++ b/wadsrc/static/zscript/ui/statusbar/scoreboard.zs @@ -0,0 +1,449 @@ + +extend class BaseStatusBar +{ + Font scoreboardFont; + + virtual void InitScoreboard() + { + scoreboardFont = NewSmallFont; + } + + static clearscope int Scoreboard_CompareByTeams(int playerA, int playerB) + { + // Compare first by teams, then by frags, then by name. + PlayerInfo p1 = players[playerA]; + PlayerInfo p2 = players[playerB]; + + int diff = p1.GetTeam() - p2.GetTeam(); + + if(diff == 0) + { + diff = p2.fragcount - p1.fragcount; + if(diff == 0) + { + diff = p1.GetUserName().CompareNoCase(p2.GetUserName()); + } + } + return diff; + } + + static clearscope int Scoreboard_CompareByPoints(int playerA, int playerB) + { + // Compare first by frags/kills, then by name. + PlayerInfo p1 = players[playerA]; + PlayerInfo p2 = players[playerB]; + + int diff = deathmatch ? (p2.fragcount - p1.fragcount) : (p2.killcount - p1.killcount); + + if(diff == 0) + { + diff = p1.GetUserName().CompareNoCase(p2.GetUserName()); + } + return diff; + } + + static void Scoreboard_SortPlayers(out Array players, Function compareFunc) + { + Array unsorted; + unsorted.Move(players); + + players.Push(unsorted[0]); + + for(int i = 1; i < unsorted.Size(); i++) + { + bool inserted = false; + + for(int j = 0; j < players.Size(); j++) + { + if(compareFunc.Call(players[j], unsorted[i]) > 0) + { + players.Insert(j, unsorted[i]); + inserted = true; + break; + } + } + + if(!inserted) + { + players.Push(unsorted[i]); + } + } + + } + + virtual void Scoreboard_DrawScores(int playerNum, double ticFrac) + { + if(deathmatch) + { + if(teamplay) + { + if(!sb_teamdeathmatch_enable) + return; + } + else if(!sb_deathmatch_enable) + { + return; + } + } + else if(!multiplayer || !sb_cooperative_enable) + { + return; + } + + if(!scoreboardFont) InitScoreboard(); + + PlayerInfo player = players[playernum]; + + if(player.camera && player.camera.player) + { + player = player.camera.player; + playerNum = Level.PlayerNum(player); + } + + Array sortedPlayers; + + for(int i = 0; i < MAXPLAYERS; i++) + { + if(playeringame[i]) + { + sortedPlayers.Push(i); + } + } + + /* + console.printf("SortedPlayers was ["); + + foreach(p : sortedPlayers) + { + console.printf("%d", p); + } + + console.printf("]"); + */ + + if(teamplay && deathmatch) + { + Scoreboard_SortPlayers(sortedPlayers, Scoreboard_CompareByTeams); + } + else + { + Scoreboard_SortPlayers(sortedPlayers, Scoreboard_CompareByPoints); + } + + /* + console.printf("SortedPlayers is now ["); + + foreach(p : sortedPlayers) + { + console.printf("%d", p); + } + + console.printf("]"); + */ + + int numPlayers = sortedPlayers.size(); + + int FontScale = max(screen.GetHeight() / 400, 1); + + if(numPlayers > 8) + FontScale = int(ceil(FontScale * 0.75)); + + Scoreboard_DoDrawScores(player, sortedPlayers, FontScale, ticFrac); + } + + //========================================================================== + // + // HU_DrawFontScaled + // + //========================================================================== + + void Scoreboard_DrawFontScaled(double x, double y, Color color, String text, int FontScale) + { + screen.DrawText(scoreboardFont, color, x / FontScale, y / FontScale, text, DTA_VirtualWidth, screen.GetWidth() / FontScale, DTA_VirtualHeight, screen.GetHeight() / FontScale); + } + + //========================================================================== + // + // HU_DoDrawScores + // + //========================================================================== + + virtual void Scoreboard_DoDrawScores(PlayerInfo player, Array sortedPlayers, int FontScale, double ticFrac) + { + Color _color = sb_cooperative_headingcolor; + if(deathmatch) + { + if (teamplay) + _color = sb_teamdeathmatch_headingcolor; + else + _color = sb_deathmatch_headingcolor; + } + + int maxNameWidth, maxScoreWidth, maxIconHeight; + Scoreboard_GetPlayerWidths(maxNameWidth, maxScoreWidth, maxIconHeight); + int height = scoreboardFont.GetHeight() * FontScale; + int lineHeight = max(height, maxIconHeight * CleanYfac); + int yPadding = (lineHeight - height + 1) / 2; + + int bottom = GetTopOfStatusbar(); + int y = max(48 * CleanYfac, (bottom - MAXPLAYERS * (height + CleanYfac + 1)) / 2); + + Scoreboard_DrawTimeRemaining(bottom - height, FontScale); + + Array teamPlayerCounts; + Array teamScores; + + teamPlayerCounts.Resize(Teams.Size()); + teamScores.Resize(Teams.Size()); + + if(teamplay && deathmatch) + { + y -= (BigFont.GetHeight() + 8) * CleanYfac; + + int numTeams = 0; + for(int i = 0; i < MAXPLAYERS; ++i) + { + PlayerInfo p = players[sortedPlayers[i]]; + if (playeringame[sortedPlayers[i]] && Team.IsValid(p.GetTeam())) + { + if (teamPlayerCounts[p.GetTeam()]++ == 0) + ++numTeams; + + teamScores[p.GetTeam()] += p.fragcount; + } + } + + int scoreXWidth = screen.GetWidth() / max(8, numTeams); + int numScores = 0; + for(int i = 0; i < Teams.Size(); ++i) + { + if (teamPlayerCounts[i]) + ++numScores; + } + + int scoreX = (screen.GetWidth() - scoreXWidth * (numScores - 1)) / 2; + for(int i = 0; i < Teams.Size(); ++i) + { + if (!teamPlayerCounts[i]) + continue; + + String score = String.Format("%d", teamScores[i]); + + screen.DrawText(BigFont, Teams[i].GetTextColor(), + scoreX - BigFont.StringWidth(score)*CleanXfac/2, y, score, + DTA_CleanNoMove, true); + + scoreX += scoreXWidth; + } + + y += (BigFont.GetHeight() + 8) * CleanYfac; + } + + String text_color = StringTable.Localize("$SCORE_COLOR"), + text_frags = StringTable.Localize(deathmatch ? "$SCORE_FRAGS" : "$SCORE_KILLS"), + text_name = StringTable.Localize("$SCORE_NAME"), + text_delay = StringTable.Localize("$SCORE_DELAY"); + + int col2 = (scoreboardFont.StringWidth(text_color) + 16) * FontScale; + int col3 = col2 + (scoreboardFont.StringWidth(text_frags) + 16) * FontScale; + int col4 = col3 + maxScoreWidth * FontScale; + int col5 = col4 + (maxNameWidth + 16) * FontScale; + int x = (screen.GetWidth() >> 1) - (((scoreboardFont.StringWidth(text_delay) * FontScale) + col5) >> 1); + + //Scoreboard_DrawFontScaled(x, y, _color, text_color); + Scoreboard_DrawFontScaled(x + col2, y, _color, text_frags, FontScale); + Scoreboard_DrawFontScaled(x + col4, y, _color, text_name, FontScale); + Scoreboard_DrawFontScaled(x + col5, y, _color, text_delay, FontScale); + + y += height + 6 * CleanYfac; + bottom -= height; + + for(int i = 0; i < sortedPlayers.Size() && y <= bottom; ++i) + { + Scoreboard_DrawPlayer(players[sortedPlayers[i]], Level.PlayerNum(player) == sortedPlayers[i], x, col2, col3, col4, col5, maxNameWidth, y, yPadding, lineHeight, FontScale); + y += lineHeight + CleanYfac; + } + } + + //========================================================================== + // + // HU_DrawTimeRemaining + // + //========================================================================== + + void Scoreboard_DrawTimeRemaining(int y, int FontScale) + { + if(deathmatch && timelimit && gamestate == GS_LEVEL) + { + int timeleft = int(timelimit * GameTicRate * 60) - Level.maptime; + + int hours, minutes, seconds; + + if(timeleft < 0) timeleft = 0; + + hours = timeleft / (GameTicRate * 3600); + timeleft -= hours * GameTicRate * 3600; + minutes = timeleft / (GameTicRate * 60); + timeleft -= minutes * GameTicRate * 60; + seconds = timeleft / GameTicRate; + + String str; + if(hours) + { + str = String.Format("Level ends in %d:%02d:%02d", hours, minutes, seconds); + } + else + { + str = String.Format("Level ends in %d:%02d", minutes, seconds); + } + + Scoreboard_DrawFontScaled(screen.GetWidth() / 2 - scoreboardFont.StringWidth(str) / 2 * FontScale, y, Font.CR_GRAY, str, FontScale); + } + } + + + //========================================================================== + // + // HU_DrawPlayer + // + //========================================================================== + + void Scoreboard_DrawPlayer(PlayerInfo player, bool highlight, int col1, int col2, int col3, int col4, int col5, int maxnamewidth, int y, int ypadding, int height, int FontScale) + { + String str; + + if(highlight) + { + // The teamplay mode uses colors to show teams, so we need some + // other way to do highlighting. And it may as well be used for + // all modes for the sake of consistancy. + screen.Dim(Color(200,245,255), 0.125f, col1 - 12 * FontScale, y - 1, col5 + (maxnamewidth + 24) * FontScale, height + 2); + } + + col2 += col1; + col3 += col1; + col4 += col1; + col5 += col1; + + Color _color = Scoreboard_GetRowColor(player, highlight); + Scoreboard_DrawColorBar(col1, y, height, player, FontScale); + + str = String.Format("%d", deathmatch ? player.fragcount : player.killcount); + + Scoreboard_DrawFontScaled(col2, y + ypadding, _color, player.playerstate == PST_DEAD && !deathmatch ? "DEAD" : str, FontScale); + + TextureID icon = player.mo.ScoreIcon; + + if(icon.isValid()) + { + screen.DrawTexture(icon, false, col3, y, DTA_CleanNoMove, true); + } + + Scoreboard_DrawFontScaled(col4, y + ypadding, _color, player.GetUserName(), FontScale); + + str = String.Format("%d", player.GetAverageLatency()); + + Scoreboard_DrawFontScaled(col5, y + ypadding, _color, str, FontScale); + + int team = player.GetTeam(); + + if(team != TEAM_NONE && teamplay && Teams[team].GetLogoName().IsNotEmpty()) + { + TextureID pic = Teams[team].GetLogo(); + screen.DrawTexture(pic, col1 - (screen.GetTextureWidth(pic) + 2) * CleanXfac, y, DTA_CleanNoMove, true); + } + } + + //========================================================================== + // + // HU_DrawColorBar + // + //========================================================================== + + static void Scoreboard_DrawColorBar(int x, int y, int height, PlayerInfo player, int FontScale) + { + Screen.Clear(x, y, x + 24 * FontScale, y + height, player.GetDisplayColor()); + } + + //========================================================================== + // + // HU_GetRowColor + // + //========================================================================== + + static Color Scoreboard_GetRowColor(PlayerInfo player, bool highlight) + { + if(teamplay && deathmatch) + { + if(Team.IsValid(player.GetTeam())) + { + Color teamColor = Teams[player.GetTeam()].GetTextColor(); + return teamColor; + } + else + { + return Font.CR_GREY; + } + } + else + { + if(!highlight) + { + if(demoplayback && Level.PlayerNum(player) == consoleplayer) + { + return Font.CR_GOLD; + } + else + { + return deathmatch ? sb_deathmatch_otherplayercolor : sb_cooperative_otherplayercolor; + } + } + else + { + return deathmatch ? sb_deathmatch_yourplayercolor : sb_cooperative_yourplayercolor; + } + } + } + + //========================================================================== + // + // HU_GetPlayerWidths + // + // Returns the widest player name and class icon. + // + //========================================================================== + + void Scoreboard_GetPlayerWidths(int& maxNameWidth, int& maxScoreWidth, int& maxIconHeight) + { + if(!scoreboardFont) InitScoreboard(); + + maxNameWidth = scoreboardFont.StringWidth("Name"); + maxScoreWidth = 0; + maxIconHeight = 0; + + for (int i = 0; i < MAXPLAYERS; ++i) + { + if (!playeringame[i]) + continue; + + int width = scoreboardFont.StringWidth(players[i].GetUserName(16)); + if (width > maxNameWidth) + maxNameWidth = width; + + TextureID icon = players[i].mo.ScoreIcon; + if (icon.isValid()) + { + width = int(screen.GetTextureWidth(icon) - screen.GetTextureLeftOffset(icon) + 2.5); + if (width > maxScoreWidth) + maxScoreWidth = width; + + // The icon's top offset does not count toward its height, because + // zdoom.pk3's standard Hexen class icons are designed that way. + int height = int(screen.GetTextureHeight(icon) - screen.GetTextureTopOffset(icon) + 0.5); + if (height > maxIconHeight) + maxIconHeight = height; + } + } + } + +} \ No newline at end of file diff --git a/wadsrc/static/zscript/ui/statusbar/statusbar.zs b/wadsrc/static/zscript/ui/statusbar/statusbar.zs index fa2b66c6b..b4dc83739 100644 --- a/wadsrc/static/zscript/ui/statusbar/statusbar.zs +++ b/wadsrc/static/zscript/ui/statusbar/statusbar.zs @@ -214,6 +214,7 @@ class BaseStatusBar : StatusBarCore native virtual void Init() { + InitScoreboard(); } virtual void Draw (int state, double TicFrac)