qzdoom/wadsrc/static/zscript/ui/statscreen/statscreen.zs
Christoph Oelckers 2016f56a7e - added a new scaling mode to the level summary screen to scale to a given size.
In this mode the clean scaling factors are ignored and content is always scaled to fit the screen as efficently as possible.
For the default summary screens an option was added to use this mode, which is a lot closer to the original look of this screen.
It is not 100% identical because it still factors in the author, if given and long level names that may be broken into multiple lines of text.
2020-10-03 13:32:18 +02:00

943 lines
24 KiB
Text

// Note that the status screen needs to run in 'play' scope!
class InterBackground native play version("2.5")
{
native static InterBackground Create(wbstartstruct wbst);
native virtual bool LoadBackground(bool isenterpic);
native virtual void updateAnimatedBack();
native virtual void drawBackground(int CurState, bool drawsplat, bool snl_pointeron);
}
// This is obsolete. Hopefully this was never used...
struct PatchInfo play version("2.5")
{
Font mFont;
deprecated("3.8") TextureID mPatch;
int mColor;
void Init(GIFont gifont)
{
// Replace with the VGA-Unicode font if needed.
// The default settings for this are marked with a *.
// If some mod changes this it is assumed that it doesn't provide any localization for the map name in a language not supported by the font.
String s = gifont.fontname;
if (s.Left(1) != "*")
mFont = Font.GetFont(gifont.fontname);
else if (generic_ui)
mFont = NewSmallFont;
else
{
s = s.Mid(1);
mFont = Font.GetFont(s);
}
mColor = Font.FindFontColor(gifont.color);
if (mFont == NULL)
{
mFont = BigFont;
}
}
};
class StatusScreen abstract play version("2.5")
{
enum EValues
{
// GLOBAL LOCATIONS
TITLEY = 5,
// SINGPLE-PLAYER STUFF
SP_STATSX = 50,
SP_STATSY = 50,
SP_TIMEX = 8,
SP_TIMEY = (200 - 32),
// NET GAME STUFF
NG_STATSY = 50,
};
enum EState
{
NoState = -1,
StatCount,
ShowNextLoc,
LeavingIntermission
};
// States for single-player
enum ESPState
{
SP_KILLS = 0,
SP_ITEMS = 2,
SP_SECRET = 4,
SP_FRAGS = 6,
SP_TIME = 8,
};
const SHOWNEXTLOCDELAY = 4; // in seconds
InterBackground bg;
int acceleratestage; // used to accelerate or skip a stage
bool playerready[MAXPLAYERS];
int me; // wbs.pnum
int bcnt;
int CurState; // specifies current CurState
wbstartstruct wbs; // contains information passed into intermission
wbplayerstruct Plrs[MAXPLAYERS]; // wbs.plyr[]
int otherkills;
int cnt; // used for general timing
int cnt_otherkills;
int cnt_kills[MAXPLAYERS];
int cnt_items[MAXPLAYERS];
int cnt_secret[MAXPLAYERS];
int cnt_frags[MAXPLAYERS];
int cnt_deaths[MAXPLAYERS];
int cnt_time;
int cnt_total_time;
int cnt_par;
int cnt_pause;
int total_frags;
int total_deaths;
bool noautostartmap;
int dofrags;
int ng_state;
float shadowalpha;
PatchInfo mapname;
PatchInfo finished;
PatchInfo entering;
PatchInfo content;
PatchInfo author;
TextureID p_secret;
TextureID kills;
TextureID secret;
TextureID items;
TextureID timepic;
TextureID par;
TextureID sucks;
TextureID finishedPatch;
TextureID enteringPatch;
// [RH] Info to dynamically generate the level name graphics
String lnametexts[2];
String authortexts[2];
bool snl_pointeron;
int player_deaths[MAXPLAYERS];
int sp_state;
int cWidth, cHeight; // size of the canvas
int scalemode;
int wrapwidth; // size used to word wrap level names
int scaleFactorX, scaleFactorY;
//====================================================================
//
// Draws a single character with a shadow
//
//====================================================================
int DrawCharPatch(Font fnt, int charcode, int x, int y, int translation = Font.CR_UNTRANSLATED, bool nomove = false)
{
int width = fnt.GetCharWidth(charcode);
if (scalemode == -1) screen.DrawChar(fnt, translation, x, y, charcode, nomove ? DTA_CleanNoMove : DTA_Clean, true);
else screen.DrawChar(fnt, translation, x, y, charcode, DTA_FullscreenScale, scalemode, DTA_VirtualWidth, cwidth, DTA_VirtualHeight, cheight);
return x - width;
}
//====================================================================
//
//
//
//====================================================================
void DrawTexture(TextureID tex, double x, double y, bool nomove = false)
{
if (scalemode == -1) screen.DrawTexture(tex, true, x, y, nomove ? DTA_CleanNoMove : DTA_Clean, true);
else screen.DrawTexture(tex, true, x, y, DTA_FullscreenScale, scalemode, DTA_VirtualWidth, cwidth, DTA_VirtualHeight, cheight);
}
//====================================================================
//
//
//
//====================================================================
void DrawText(Font fnt, int color, double x, double y, String str, bool nomove = false, bool shadow = false)
{
if (scalemode == -1) screen.DrawText(fnt, color, x, y, str, nomove ? DTA_CleanNoMove : DTA_Clean, true, DTA_Shadow, shadow);
else screen.DrawText(fnt, color, x, y, str, DTA_FullscreenScale, scalemode, DTA_VirtualWidth, cwidth, DTA_VirtualHeight, cheight, DTA_Shadow, shadow);
}
//====================================================================
//
// Draws a level name with the big font
//
// x is no longer passed as a parameter because the text is now broken into several lines
// if it is too long
//
//====================================================================
int DrawName(int y, TextureID tex, String levelname)
{
// draw <LevelName>
if (tex.isValid())
{
let size = TexMan.GetScaledSize(tex);
DrawTexture(tex, (cwidth - size.X * scaleFactorX) /2, y, true);
if (size.Y > 50)
{ // Fix for Deus Vult II and similar wads that decide to make these hugely tall
// patches with vast amounts of empty space at the bottom.
size.Y = TexMan.CheckRealHeight(tex);
}
return y + int(Size.Y) * scaleFactorY;
}
else if (levelname.Length() > 0)
{
int h = 0;
int lumph = mapname.mFont.GetHeight() * scaleFactorY;
BrokenLines lines = mapname.mFont.BreakLines(levelname, wrapwidth / scaleFactorX);
int count = lines.Count();
for (int i = 0; i < count; i++)
{
DrawText(mapname.mFont, mapname.mColor, (cwidth - lines.StringWidth(i) * scaleFactorX) / 2, y + h, lines.StringAt(i), true);
h += lumph;
}
return y + h;
}
return 0;
}
//====================================================================
//
// Draws a level author's name with the given font
//
//====================================================================
int DrawAuthor(int y, String levelname)
{
if (levelname.Length() > 0)
{
int h = 0;
int lumph = author.mFont.GetHeight() * scaleFactorY;
BrokenLines lines = author.mFont.BreakLines(levelname, wrapwidth / scaleFactorX);
int count = lines.Count();
for (int i = 0; i < count; i++)
{
DrawText(author.mFont, author.mColor, (cwidth - lines.StringWidth(i) * scaleFactorX) / 2, y + h, lines.StringAt(i), true);
h += lumph;
}
return y + h;
}
return y;
}
//====================================================================
//
// Only kept so that mods that were accessing it continue to compile
//
//====================================================================
deprecated("3.8") int DrawPatchText(int y, PatchInfo pinfo, String stringname)
{
String string = Stringtable.Localize(stringname);
int midx = screen.GetWidth() / 2;
screen.DrawText(pinfo.mFont, pinfo.mColor, midx - pinfo.mFont.StringWidth(string) * CleanXfac/2, y, string, DTA_CleanNoMove, true);
return y + pinfo.mFont.GetHeight() * CleanYfac;
}
//====================================================================
//
// Draws a text, either as patch or as string from the string table
//
//====================================================================
int DrawPatchOrText(int y, PatchInfo pinfo, TextureID patch, String stringname)
{
String string = Stringtable.Localize(stringname);
int midx = cwidth / 2;
if (TexMan.OkForLocalization(patch, stringname))
{
let size = TexMan.GetScaledSize(patch);
DrawTexture(patch, midx - size.X * scaleFactorX/2, y, true);
return y + int(size.Y * scaleFactorY);
}
else
{
DrawText(pinfo.mFont, pinfo.mColor, midx - pinfo.mFont.StringWidth(string) * scaleFactorX/2, y, string, true);
return y + pinfo.mFont.GetHeight() * scaleFactorY;
}
}
//====================================================================
//
// Draws "<Levelname> Finished!"
//
// Either uses the specified patch or the big font
// A level name patch can be specified for all games now, not just Doom.
//
//====================================================================
virtual int drawLF ()
{
bool ispatch = wbs.LName0.isValid();
int oldy = TITLEY * scaleFactorY;
int h;
if (!ispatch)
{
let asc = mapname.mFont.GetMaxAscender(lnametexts[1]);
if (asc > TITLEY - 2)
{
oldy = (asc+2) * scaleFactorY;
}
}
int y = DrawName(oldy, wbs.LName0, lnametexts[0]);
// If the displayed info is made of patches we need some additional offsetting here.
if (ispatch)
{
int disp = 0;
// The offset getting applied here must at least be as tall as the largest ascender in the following text to avoid overlaps.
if (authortexts[0].length() == 0)
{
int h1 = BigFont.GetHeight() - BigFont.GetDisplacement();
int h2 = (y - oldy) / scaleFactorY / 4;
disp = min(h1, h2);
if (!TexMan.OkForLocalization(finishedPatch, "$WI_FINISHED"))
{
disp += finished.mFont.GetMaxAscender("$WI_FINISHED");
}
}
else
{
disp += author.mFont.GetMaxAscender(authortexts[0]);
}
y += disp * scaleFactorY;
}
y = DrawAuthor(y, authortexts[0]);
// draw "Finished!"
int statsy = multiplayer? NG_STATSY : SP_STATSY * scaleFactorY;
if (y < (statsy - finished.mFont.GetHeight()*3/4) * scaleFactorY)
{
// don't draw 'finished' if the level name is too tall
y = DrawPatchOrText(y, finished, finishedPatch, "$WI_FINISHED");
}
return y;
}
//====================================================================
//
// Draws "Entering <LevelName>"
//
// Either uses the specified patch or the big font
// A level name patch can be specified for all games now, not just Doom.
//
//====================================================================
virtual void drawEL ()
{
bool ispatch = TexMan.OkForLocalization(enteringPatch, "$WI_ENTERING");
int oldy = TITLEY * scaleFactorY;
if (!ispatch)
{
let asc = entering.mFont.GetMaxAscender("$WI_ENTERING");
if (asc > TITLEY - 2)
{
oldy = (asc+2) * scaleFactorY;
}
}
int y = DrawPatchOrText(oldy, entering, enteringPatch, "$WI_ENTERING");
// If the displayed info is made of patches we need some additional offsetting here.
if (ispatch)
{
int h1 = BigFont.GetHeight() - BigFont.GetDisplacement();
let size = TexMan.GetScaledSize(enteringPatch);
int h2 = int(size.Y);
let disp = min(h1, h2) / 4;
// The offset getting applied here must at least be as tall as the largest ascender in the following text to avoid overlaps.
if (!wbs.LName1.isValid())
{
disp += mapname.mFont.GetMaxAscender(lnametexts[1]);
}
y += disp * scaleFactorY;
}
y = DrawName(y, wbs.LName1, lnametexts[1]);
if (wbs.LName1.isValid() && authortexts[1].length() > 0)
{
// Consdider the ascender height of the following text.
y += author.mFont.GetMaxAscender(authortexts[1]) * scaleFactorY;
}
DrawAuthor(y, authortexts[1]);
}
//====================================================================
//
// Draws a number.
// If digits > 0, then use that many digits minimum,
// otherwise only use as many as necessary.
// x is the right edge of the number.
// Returns new x position, that is, the left edge of the number.
//
//====================================================================
int drawNum (Font fnt, int x, int y, int n, int digits, bool leadingzeros = true, int translation = Font.CR_UNTRANSLATED, bool nomove = false)
{
int fntwidth = fnt.StringWidth("3");
String text;
int len;
if (nomove && scalemode == -1)
{
fntwidth *= scaleFactorX;
}
text = String.Format("%d", n);
len = text.Length();
if (leadingzeros)
{
int filldigits = digits - len;
for(int i = 0; i < filldigits; i++)
{
text = "0" .. text;
}
len = text.Length();
}
for(int text_p = len-1; text_p >= 0; text_p--)
{
// Digits are centered in a box the width of the '3' character.
// Other characters (specifically, '-') are right-aligned in their cell.
int c = text.ByteAt(text_p);
if (c >= "0" && c <= "9")
{
x -= fntwidth;
DrawCharPatch(fnt, c, x + (fntwidth - fnt.GetCharWidth(c)) / 2, y, translation, nomove);
}
else
{
DrawCharPatch(fnt, c, x - fnt.GetCharWidth(c), y, translation, nomove);
x -= fntwidth;
}
}
if (len < digits)
{
x -= fntwidth * (digits - len);
}
return x;
}
//====================================================================
//
//
//
//====================================================================
void drawPercent (Font fnt, int x, int y, int p, int b, bool show_total = true, int color = Font.CR_UNTRANSLATED, bool nomove = false)
{
if (p < 0)
return;
if (wi_percents)
{
if (nomove && scalemode == -1)
{
x -= fnt.StringWidth("%") * scaleFactorX;
}
else
{
x -= fnt.StringWidth("%");
}
DrawText(fnt, color, x, y, "%", nomove);
if (nomove)
{
x -= 2*CleanXfac;
}
drawNum(fnt, x, y, b == 0 ? 100 : p * 100 / b, -1, false, color, nomove);
}
else
{
if (show_total)
{
x = drawNum(fnt, x, y, b, 2, false, color, nomove);
x -= fnt.StringWidth("/");
DrawText (fnt, color, x, y, "/", nomove);
}
drawNum (fnt, x, y, p, -1, false, color, nomove);
}
}
//====================================================================
//
// Display level completion time and par, or "sucks" message if overflow.
//
//====================================================================
void drawTimeFont (Font printFont, int x, int y, int t, int color)
{
bool sucky;
if (t < 0)
return;
int hours = t / 3600;
t -= hours * 3600;
int minutes = t / 60;
t -= minutes * 60;
int seconds = t;
// Why were these offsets hard coded? Half the WADs with custom patches
// I tested screwed up miserably in this function!
int num_spacing = printFont.GetCharWidth("3");
int colon_spacing = printFont.GetCharWidth(":");
x = drawNum (printFont, x, y, seconds, 2, true, color) - 1;
DrawCharPatch (printFont, ":", x -= colon_spacing, y, color);
x = drawNum (printFont, x, y, minutes, 2, hours!=0, color);
if (hours)
{
DrawCharPatch (printFont, ":", x -= colon_spacing, y, color);
drawNum (printFont, x, y, hours, 2, false, color);
}
}
void drawTime (int x, int y, int t, bool no_sucks=false)
{
drawTimeFont(IntermissionFont, x, y, t, Font.CR_UNTRANSLATED);
}
//====================================================================
//
// the 'scaled' drawers are for the multiplayer scoreboard
//
//====================================================================
void drawTextScaled (Font fnt, double x, double y, String text, double scale, int translation = Font.CR_UNTRANSLATED)
{
screen.DrawText(fnt, translation, x / scale, y / scale, text, DTA_VirtualWidthF, screen.GetWidth() / scale, DTA_VirtualHeightF, screen.GetHeight() / scale);
}
//====================================================================
//
//
//====================================================================
void drawNumScaled (Font fnt, int x, int y, double scale, int n, int digits, int translation = Font.CR_UNTRANSLATED)
{
String s = String.Format("%d", n);
drawTextScaled(fnt, x - fnt.StringWidth(s) * scale, y, s, scale, translation);
}
//====================================================================
//
//
//
//====================================================================
void drawPercentScaled (Font fnt, int x, int y, int p, int b, double scale, bool show_total = true, int color = Font.CR_UNTRANSLATED)
{
if (p < 0) return;
String s;
if (wi_percents)
{
s = String.Format("%d%%", b == 0 ? 100 : p * 100 / b);
}
else if (show_total)
{
s = String.Format("%d/%3d", p, b);
}
else
{
s = String.Format("%d", p);
}
drawTextScaled(fnt, x - fnt.StringWidth(s) * scale, y, s, scale, color);
}
//====================================================================
//
// Display the completed time scaled
//
//====================================================================
void drawTimeScaled (Font fnt, int x, int y, int t, double scale, int color = Font.CR_UNTRANSLATED)
{
if (t < 0)
return;
int hours = t / 3600;
t -= hours * 3600;
int minutes = t / 60;
t -= minutes * 60;
int seconds = t;
String s = (hours > 0 ? String.Format("%d:", hours) : "") .. String.Format("%02d:%02d", minutes, seconds);
drawTextScaled(fnt, x - fnt.StringWidth(s) * scale, y, s, scale, color);
}
//====================================================================
//
//
//
//====================================================================
virtual void End ()
{
CurState = LeavingIntermission;
}
//====================================================================
//
//
//
//====================================================================
bool autoSkip()
{
return wi_autoadvance > 0 && bcnt > (wi_autoadvance * Thinker.TICRATE);
}
//====================================================================
//
//
//
//====================================================================
protected virtual void initNoState ()
{
CurState = NoState;
acceleratestage = 0;
cnt = 10;
}
//====================================================================
//
//
//
//====================================================================
protected virtual void updateNoState ()
{
if (acceleratestage)
{
cnt = 0;
}
else
{
bool noauto = noautostartmap;
for (int i = 0; !noauto && i < MAXPLAYERS; ++i)
{
if (playeringame[i])
{
noauto |= players[i].GetNoAutostartMap();
}
}
if (!noauto || autoSkip())
{
cnt--;
}
}
if (cnt == 0)
{
End();
Level.WorldDone();
}
}
//====================================================================
//
//
//
//====================================================================
protected virtual void initShowNextLoc ()
{
if (wbs.next == "")
{
// Last map in episode - there is no next location!
End();
Level.WorldDone();
return;
}
CurState = ShowNextLoc;
acceleratestage = 0;
cnt = SHOWNEXTLOCDELAY * Thinker.TICRATE;
noautostartmap = bg.LoadBackground(true);
}
//====================================================================
//
//
//
//====================================================================
protected virtual void updateShowNextLoc ()
{
if (!--cnt || acceleratestage)
initNoState();
else
snl_pointeron = (cnt & 31) < 20;
}
//====================================================================
//
//
//
//====================================================================
protected virtual void drawShowNextLoc(void)
{
bg.drawBackground(CurState, true, snl_pointeron);
// draws which level you are entering..
drawEL ();
}
//====================================================================
//
//
//
//====================================================================
protected virtual void drawNoState ()
{
snl_pointeron = true;
drawShowNextLoc();
}
//====================================================================
//
//
//
//====================================================================
protected int fragSum (int playernum)
{
int i;
int frags = 0;
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i]
&& i!=playernum)
{
frags += Plrs[playernum].frags[i];
}
}
// JDC hack - negative frags.
frags -= Plrs[playernum].frags[playernum];
return frags;
}
//====================================================================
//
//
//
//====================================================================
static void PlaySound(Sound snd)
{
S_StartSound(snd, CHAN_VOICE, CHANF_MAYBE_LOCAL|CHANF_UI, 1, ATTN_NONE);
}
// ====================================================================
// checkForAccelerate
// Purpose: See if the player has hit either the attack or use key
// or mouse button. If so we set acceleratestage to 1 and
// all those display routines above jump right to the end.
// Args: none
// Returns: void
//
// ====================================================================
protected void checkForAccelerate(void)
{
int i;
// check for button presses to skip delays
for (i = 0; i < MAXPLAYERS; i++)
{
PlayerInfo player = players[i];
if (playeringame[i])
{
if ((player.cmd.buttons ^ player.oldbuttons) &&
((player.cmd.buttons & player.oldbuttons) == player.oldbuttons) && player.Bot == NULL)
{
acceleratestage = 1;
playerready[i] = true;
}
player.oldbuttons = player.buttons;
}
}
}
// ====================================================================
// Ticker
// Purpose: Do various updates every gametic, for stats, animation,
// checking that intermission music is running, etc.
// Args: none
// Returns: void
//
// ====================================================================
virtual void StartMusic()
{
Level.SetInterMusic(wbs.next);
}
//====================================================================
//
//
//
//====================================================================
virtual void Ticker(void)
{
// counter for general background animation
bcnt++;
if (bcnt == 1)
{
StartMusic();
}
checkForAccelerate();
bg.updateAnimatedBack();
switch (CurState)
{
case StatCount:
updateStats();
break;
case ShowNextLoc:
updateShowNextLoc();
break;
case NoState:
updateNoState();
break;
case LeavingIntermission:
// Hush, GCC.
break;
}
}
//====================================================================
//
//
//
//====================================================================
virtual void Drawer (void)
{
switch (CurState)
{
case StatCount:
// draw animated background
bg.drawBackground(CurState, false, false);
drawStats();
break;
case ShowNextLoc:
case LeavingIntermission: // this must still draw the screen once more for the wipe code to pick up.
drawShowNextLoc();
break;
default:
drawNoState();
break;
}
}
//====================================================================
//
//
//
//====================================================================
virtual void Start (wbstartstruct wbstartstruct)
{
wbs = wbstartstruct;
acceleratestage = 0;
cnt = bcnt = 0;
me = wbs.pnum;
otherkills = wbs.totalkills;
for (int i = 0; i < MAXPLAYERS; i++)
{
Plrs[i] = wbs.plyr[i];
otherkills -= Plrs[i].skills;
}
entering.Init(gameinfo.mStatscreenEnteringFont);
finished.Init(gameinfo.mStatscreenFinishedFont);
mapname.Init(gameinfo.mStatscreenMapNameFont);
content.Init(gameinfo.mStatscreenContentFont);
author.Init(gameinfo.mStatscreenAuthorFont);
Kills = TexMan.CheckForTexture("WIOSTK", TexMan.Type_MiscPatch); // "kills"
Secret = TexMan.CheckForTexture("WIOSTS", TexMan.Type_MiscPatch); // "scrt", not used
P_secret = TexMan.CheckForTexture("WISCRT2", TexMan.Type_MiscPatch); // "secret"
Items = TexMan.CheckForTexture("WIOSTI", TexMan.Type_MiscPatch); // "items"
Timepic = TexMan.CheckForTexture("WITIME", TexMan.Type_MiscPatch); // "time"
Sucks = TexMan.CheckForTexture("WISUCKS", TexMan.Type_MiscPatch); // "sucks"
Par = TexMan.CheckForTexture("WIPAR", TexMan.Type_MiscPatch); // "par"
enteringPatch = TexMan.CheckForTexture("WIENTER", TexMan.Type_MiscPatch); // "entering"
finishedPatch = TexMan.CheckForTexture("WIF", TexMan.Type_MiscPatch); // "finished"
lnametexts[0] = StringTable.Localize(wbstartstruct.thisname);
lnametexts[1] = StringTable.Localize(wbstartstruct.nextname);
authortexts[0] = StringTable.Localize(wbstartstruct.thisauthor);
authortexts[1] = StringTable.Localize(wbstartstruct.nextauthor);
bg = InterBackground.Create(wbs);
noautostartmap = bg.LoadBackground(false);
initStats();
wrapwidth = cwidth = screen.GetWidth();
cheight = screen.GetHeight();
scalemode = -1;
scaleFactorX = CleanXfac;
scaleFactorY = CleanYfac;
}
protected virtual void initStats() {}
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<int> sorted, bool teamplay);
}