swwmgz_m/zscript/kbase/swwm_kbase.zsc

624 lines
19 KiB
Text

// internal "knowledge base" and more
Class MenuTransaction
{
enum ETransactionType
{
TT_ITEMUSE,
TT_ITEMDROP
};
int uid, type;
Class<Inventory> used;
bool result, usedup;
}
enum EKMenuKey
{
MK_DOWN,
MK_UP,
MK_LEFT,
MK_RIGHT,
MK_ENTER,
MK_BACK
};
enum EKMouseButton
{
MB_LEFT,
MB_RIGHT,
MB_WHEELUP,
MB_WHEELDOWN,
MB_DRAG,
MB_RELEASE
}
Class DemolitionistMenu : GenericMenu
{
TextureID FancyBg, FrameTex, VSepTex, HSepTex;
// for resolution scaling and such
double hs;
Vector2 ss, ws, origin;
// temporary bottom messages, such as "not enough money"
String tmsg;
int tmsgtic;
// money owned, for store
int muns;
// other text
String clockstr, munstr;
// for checks (duh)
Array<MenuTransaction> checklist;
int lasttuid;
SWWMHandler hnd;
SWWMStaticHandler shnd;
// seeeeecret
int kcode;
// crimey clock stuff
int c_year, c_month, c_day, c_hour, c_minute;
String c_tz;
// mouse stuff
Vector2 curmouse;
bool isrclick;
// somehow Drawer can be called while closing prematurely, which is big bollocks
bool isclosing, forceclose;
// the tabs
Array<DemolitionistMenuTab> tabs;
int curtab;
int oldtab; // used for returning from help tab
// fonts
Font mSmallFont, mTinyFont;
// for open/close animation
double animtimer;
int GenTUID()
{
return lasttuid++;
}
// tab functions
int FindTabType( Class<DemolitionistMenuTab> t, bool nothidden = false )
{
for ( int i=0; i<tabs.Size(); i++ )
{
if ( !(tabs[i] is t) ) continue;
if ( nothidden && tabs[i].bHidden ) continue;
return i;
}
return -1;
}
int GetFirstTab()
{
for ( int i=0; i<tabs.Size(); i++ )
{
if ( tabs[i].bHidden ) continue;
return i;
}
return -1;
}
int GetLastTab()
{
for ( int i=tabs.Size()-1; i>=0; i-- )
{
if ( tabs[i].bHidden ) continue;
return i;
}
return -1;
}
int GetNextTab()
{
int lst = GetLastTab();
if ( lst == -1 ) return -1;
if ( curtab >= lst ) return GetFirstTab();
for ( int i=curtab+1; i<tabs.Size(); i++ )
{
if ( tabs[i].bHidden ) continue;
return i;
}
return -1;
}
int GetPrevTab()
{
int fst = GetFirstTab();
if ( fst == -1 ) return -1;
if ( curtab <= fst ) return GetLastTab();
for ( int i=curtab-1; i>=0; i-- )
{
if ( tabs[i].bHidden ) continue;
return i;
}
return -1;
}
private void DoClose()
{
EventHandler.SendNetworkEvent("swwmclearalltransactions",consoleplayer);
for ( int i=0; i<tabs.Size(); i++ )
{
if ( i == curtab )
{
shnd.menustate.Insert("LastTab",tabs[i].GetClassName());
tabs[i].OnDeselect();
}
tabs[i].Destroy();
}
}
override void Init( Menu parent )
{
Super.Init(parent);
Animated = DontDim = DontBlur = true;
// can't open this menu outside of the game, if dead, or if the game is currently paused
// also can't if you're not the Demolitionist
if ( (gamestate != GS_LEVEL) || (players[consoleplayer].Health <= 0) || !(players[consoleplayer].mo is 'Demolitionist') || paused )
{
animtimer = -1.;
isclosing = true;
return;
}
FancyBg = TexMan.CheckForTexture("graphics/tempbg.png");
FrameTex = TexMan.CheckForTexture("graphics/KBase/FrameTex.png");
VSepTex = TexMan.CheckForTexture("graphics/KBase/VSepTex.png");
HSepTex = TexMan.CheckForTexture("graphics/KBase/HSepTex.png");
mSmallFont = Font.GetFont('TewiFont');
mTinyFont = Font.GetFont('MiniwiFont');
// note that we can assume 640x400 will always be the smallest resolution allowed by gzdoom, but we still need to handle widescreen
double sw = max(Screen.GetWidth(),640);
double sh = max(Screen.GetHeight(),400);
hs = max(min(floor(sw/640.),floor(sh/266.)),1.);
ss = (sw,sh)/hs;
ws.x = 640.;
double th = 640*(ss.y/ss.x);
ws.y = round(th);
origin = (int(ss.x-ws.x)/2,int(ss.y-ws.y)/2);
MenuSound("menu/demoopen");
tmsg = StringTable.Localize("$SWWM_MAINCONTROLS");
tmsgtic = MenuTime()+50;
lasttuid = Random[TUID]();
hnd = SWWMHandler(EventHandler.Find("SWWMHandler"));
shnd = SWWMStaticHandler(StaticEventHandler.Find("SWWMStaticHandler"));
SetClock(c_year,c_month,c_day,c_hour,c_minute,c_tz);
clockstr = CrimeTime(c_year,c_month,c_day,c_hour,c_minute,c_tz);
static const class<DemolitionistMenuTab> deftabs[] =
{
'DemolitionistMissionTab',
'DemolitionistStatsTab',
'DemolitionistInventoryTab',
'DemolitionistKeychainTab',
'DemolitionistLibraryTab',
'DemolitionistStoreTab',
//'DemolitionistGameTab', // disabled until 1.5
'DemolitionistHelpTab',
'DemolitionistSecretTab'
};
// since GZDoom 4.10 we have to "pre-tick" tabs as soon as they're initialized
for ( int i=0; i<deftabs.Size()-2; i++ )
{
let t = DemolitionistMenuTab(new(deftabs[i])).Init(self);
t.Ticker();
tabs.Push(t);
}
// custom tabs go before the help and secret tabs
foreach ( cls:AllClasses )
{
let ct = (Class<DemolitionistMenuTabCustom>)(cls);
if ( !ct || (ct.GetParentClass() != 'DemolitionistMenuTabCustom') ) continue;
let t = DemolitionistMenuTab(new(ct)).Init(self);
t.Ticker();
tabs.Push(t);
}
for ( int i=deftabs.Size()-2; i<deftabs.Size(); i++ )
{
let t = DemolitionistMenuTab(new(deftabs[i])).Init(self);
t.Ticker();
tabs.Push(t);
}
if ( shnd.menustate )
{
Class<DemolitionistMenuTab> saved = shnd.menustate.At("LastTab");
if ( saved ) curtab = FindTabType(saved,true);
}
else
{
shnd.menustate = Dictionary.Create();
curtab = -1;
}
if ( curtab == -1 ) curtab = GetFirstTab();
tabs[curtab].OnSelect();
}
override bool MenuEvent( int mkey, bool fromcontroller )
{
if ( isclosing || (animtimer < 1.) ) return false;
// pachinko code only handled if the tab lacks direct input
if ( !tabs[curtab].bDirectInput )
{
switch ( kcode )
{
case 0:
case 1:
if ( mkey == MKEY_UP ) kcode++;
else kcode = 0;
break;
case 2:
case 3:
if ( mkey == MKEY_DOWN ) kcode++;
else kcode = 0;
break;
case 4:
case 6:
if ( mkey == MKEY_LEFT ) kcode++;
else kcode = 0;
break;
case 5:
case 7:
if ( mkey == MKEY_RIGHT ) kcode++;
else kcode = 0;
break;
case 10:
if ( mkey == MKEY_ENTER )
{
int secret = FindTabType('DemolitionistSecretTab');
if ( curtab != secret )
{
MenuSound("menu/demosecret");
tabs[curtab].OnDeselect();
curtab = secret;
tabs[curtab].OnSelect();
}
}
default:
kcode = 0;
break;
}
}
switch ( mkey )
{
case MKEY_BACK:
isclosing = true;
MenuSound("menu/democlose");
return true;
case MKEY_PAGEDOWN:
int next = GetNextTab();
if ( next != curtab )
{
MenuSound("menu/demotab");
tabs[curtab].OnDeselect();
curtab = next;
tabs[curtab].OnSelect();
}
return true;
case MKEY_PAGEUP:
int prev = GetPrevTab();
if ( prev != curtab )
{
MenuSound("menu/demotab");
tabs[curtab].OnDeselect();
curtab = prev;
tabs[curtab].OnSelect();
}
return true;
case MKEY_DOWN:
tabs[curtab].MenuInput(MK_DOWN);
return true;
case MKEY_UP:
tabs[curtab].MenuInput(MK_UP);
return true;
case MKEY_LEFT:
tabs[curtab].MenuInput(MK_LEFT);
return true;
case MKEY_RIGHT:
tabs[curtab].MenuInput(MK_RIGHT);
return true;
case MKEY_ENTER:
tabs[curtab].MenuInput(MK_ENTER);
return true;
case MKEY_CLEAR:
tabs[curtab].MenuInput(MK_BACK);
return true;
}
return Super.MenuEvent(mkey,fromcontroller);
}
override void Ticker()
{
Super.Ticker();
if ( (players[consoleplayer].Health <= 0) && !isclosing )
{
MenuSound("menu/democlose");
isclosing = true;
}
if ( (gamestate != GS_LEVEL) || paused || (isclosing && (animtimer <= 0.)) )
{
// ded (or force close)
DoClose();
Close();
return;
}
if ( swwm_menupause ) menuactive = Menu.On;
else menuactive = Menu.OnNoPause;
// forcibly tick hud (mainly so interpolators can still update in the background)
if ( !multiplayer && (menuactive == Menu.On) )
{
let bar = SWWMStatusBar(StatusBar);
if ( bar ) bar.TickInterpolators();
}
CheckTransactions();
// update time string
clockstr = CrimeTime(c_year,c_month,c_day,c_hour,c_minute,c_tz);
// update money
muns = SWWMCredits.Get(players[consoleplayer]);
munstr = String.Format("\cg¥\c-%09d",muns);
if ( isclosing )
{
if ( animtimer >= 1. ) DontDim = DontBlur = true;
animtimer -= .1;
return;
}
if ( animtimer < 1. )
{
animtimer += .1;
if ( animtimer < 1. ) return; // ensure tabs tick before first draw
else DontDim = DontBlur = false;
}
if ( (tabs.Size() <= 0) || (curtab == -1) || !tabs[curtab] ) return;
tabs[curtab].Ticker();
}
override bool MouseEvent( int type, int mx, int my )
{
if ( isclosing || (animtimer < 1.) ) return false;
bool res = Super.MouseEvent(type,mx,my);
Vector2 mpos = (mx/hs,my/hs)-origin;
if ( type == MOUSE_Click )
{
// outside clickable area
if ( (mpos.x < 0) || (mpos.x > ws.x) || (mpos.y < 0) || (mpos.y > ws.y-14) ) return res;
else if ( mpos.y < 14 )
{
if ( isrclick ) return res;
// check which tab we're clicking
int xx = 0;
String str;
int len;
for ( int i=0; i<tabs.Size(); i++ )
{
if ( tabs[i].bHidden ) continue;
str = tabs[i].title;
len = mSmallFont.StringWidth(str)+10;
if ( (mpos.x >= xx) && (mpos.x < xx+len) )
{
if ( curtab == i ) break;
MenuSound("menu/demotab");
tabs[curtab].OnDeselect();
curtab = i;
tabs[curtab].OnSelect();
break;
}
xx += len;
}
return res;
}
else if ( (tabs.Size() <= 0) || (curtab == -1) || !tabs[curtab] ) return res;
tabs[curtab].MouseInput(mpos,isrclick?MB_RIGHT:MB_LEFT);
}
else if ( type == MOUSE_Move )
{
if ( (tabs.Size() <= 0) || (curtab == -1) || !tabs[curtab] ) return res;
tabs[curtab].MouseInput(mpos,MB_DRAG);
}
else if ( type == MOUSE_Release )
{
if ( (tabs.Size() <= 0) || (curtab == -1) || !tabs[curtab] ) return res;
tabs[curtab].MouseInput(mpos,MB_RELEASE);
}
return res;
}
override bool OnUiEvent( UIEvent ev )
{
if ( isclosing || (animtimer < 1.) ) return false;
if ( tabs[curtab].bDirectInput && ((ev.type == UIEvent.Type_KeyDown) || (ev.type == UIEvent.Type_KeyUp)) )
return tabs[curtab].DirectInput(ev);
switch ( ev.type )
{
case UIEvent.Type_KeyDown:
if ( ev.keychar == UiEvent.Key_F1 )
{
int help = FindTabType('DemolitionistHelpTab');
if ( curtab == help )
{
tabs[curtab].OnDeselect();
curtab = oldtab;
tabs[curtab].OnSelect();
}
else
{
int secret = FindTabType('DemolitionistSecretTab');
if ( curtab == secret ) oldtab = GetLastTab();
else oldtab = curtab;
tabs[curtab].OnDeselect();
curtab = help;
tabs[curtab].OnSelect();
}
MenuSound("menu/demotab");
return true;
}
switch ( kcode )
{
case 8:
if ( ev.keystring ~== "B" )
kcode++;
else kcode = 0;
break;
case 9:
if ( ev.keystring ~== "A" ) kcode++;
else kcode = 0;
break;
default:
kcode = 0;
break;
}
break;
case UIEvent.Type_WheelDown:
tabs[curtab].MouseInput((curmouse/hs)-origin,MB_WHEELDOWN);
return true;
case UIEvent.Type_WheelUp:
tabs[curtab].MouseInput((curmouse/hs)-origin,MB_WHEELUP);
return true;
case UIEvent.Type_LButtonDown:
{
isrclick = false;
bool res = MouseEvent(MOUSE_Click,ev.MouseX,ev.MouseY);
if ( res ) SetCapture(true);
return res;
}
case UIEvent.Type_RButtonDown:
{
isrclick = true;
bool res = MouseEvent(MOUSE_Click,ev.MouseX,ev.MouseY);
if ( res ) SetCapture(true);
return res;
}
case UIEvent.Type_LButtonUp:
case UIEvent.Type_RButtonUp:
if ( mMouseCapture )
{
SetCapture(false);
return MouseEvent(MOUSE_Release,ev.MouseX,ev.MouseY);
}
break;
case UIEvent.Type_MouseMove:
curmouse = (ev.MouseX,ev.MouseY);
if ( mMouseCapture || (m_use_mouse == 1) )
return MouseEvent(MOUSE_Move,ev.MouseX,ev.MouseY);
break;
}
return false;
}
// fundamental drawing functions (good god these are hacky)
void DrawFrame( double x, double y, double w, double h, bool shadow = true )
{
x += origin.x;
y += origin.y;
Screen.DrawTexture(FrameTex,false,x,y,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,0.,DTA_SrcY,0.,DTA_SrcWidth,1.,DTA_SrcHeight,1.,DTA_DestWidth,1,DTA_DestHeight,1);
if ( w > 2 ) Screen.DrawTexture(FrameTex,false,x+1,y,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,1.,DTA_SrcY,0.,DTA_SrcWidth,1.,DTA_SrcHeight,1.,DTA_DestWidth,int(w-2),DTA_DestHeight,1);
if ( h > 2 ) Screen.DrawTexture(FrameTex,false,x,y+1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,0.,DTA_SrcY,1.,DTA_SrcWidth,1.,DTA_SrcHeight,1.,DTA_DestWidth,1,DTA_DestHeight,int(h-2));
Screen.DrawTexture(FrameTex,false,(x+w)-1,y,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,2.,DTA_SrcY,0.,DTA_SrcWidth,1.,DTA_SrcHeight,1.,DTA_DestWidth,1,DTA_DestHeight,1);
Screen.DrawTexture(FrameTex,false,x,(y+h)-1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,0.,DTA_SrcY,2.,DTA_SrcWidth,1.,DTA_SrcHeight,1.,DTA_DestWidth,1,DTA_DestHeight,1);
if ( shadow )
{
if ( h > 2 ) Screen.DrawTexture(FrameTex,false,(x+w)-1,y+1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,2.,DTA_SrcY,1.,DTA_SrcWidth,2.,DTA_SrcHeight,1.,DTA_DestWidth,2,DTA_DestHeight,int(h-2));
if ( w > 2 ) Screen.DrawTexture(FrameTex,false,x+1,(y+h)-1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,1.,DTA_SrcY,2.,DTA_SrcWidth,1.,DTA_SrcHeight,2.,DTA_DestWidth,int(w-2),DTA_DestHeight,2);
Screen.DrawTexture(FrameTex,false,(x+w)-1,(y+h)-1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,2.,DTA_SrcY,2.,DTA_SrcWidth,2.,DTA_SrcHeight,2.,DTA_DestWidth,2,DTA_DestHeight,2);
}
else
{
if ( h > 2 ) Screen.DrawTexture(FrameTex,false,(x+w)-1,y+1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,2.,DTA_SrcY,1.,DTA_SrcWidth,1.,DTA_SrcHeight,1.,DTA_DestWidth,1,DTA_DestHeight,int(h-2));
if ( w > 2 ) Screen.DrawTexture(FrameTex,false,x+1,(y+h)-1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,1.,DTA_SrcY,2.,DTA_SrcWidth,1.,DTA_SrcHeight,1.,DTA_DestWidth,int(w-2),DTA_DestHeight,1);
Screen.DrawTexture(FrameTex,false,(x+w)-1,(y+h)-1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,2.,DTA_SrcY,2.,DTA_SrcWidth,1.,DTA_SrcHeight,1.,DTA_DestWidth,1,DTA_DestHeight,1);
}
}
// these ones thankfully only need three drawtexture calls each
void DrawVSeparator( double x, double y, double h )
{
x += origin.x;
y += origin.y;
Screen.DrawTexture(VSepTex,false,x-1,y,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcY,0.,DTA_SrcHeight,1.,DTA_DestHeight,1);
Screen.DrawTexture(VSepTex,false,x-1,y+1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcY,1.,DTA_SrcHeight,1.,DTA_DestHeight,int(h-2));
Screen.DrawTexture(VSepTex,false,x-1,(y+h)-1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcY,2.,DTA_SrcHeight,1.,DTA_DestHeight,1);
}
void DrawHSeparator( double x, double y, double w )
{
x += origin.x;
y += origin.y;
Screen.DrawTexture(HSepTex,false,x,y-1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,0.,DTA_SrcWidth,1.,DTA_DestWidth,1);
Screen.DrawTexture(HSepTex,false,x+1,y-1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,1.,DTA_SrcWidth,1.,DTA_DestWidth,int(w-2));
Screen.DrawTexture(HSepTex,false,(x+w)-1,y-1,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_SrcX,2.,DTA_SrcWidth,1.,DTA_DestWidth,1);
}
override void Drawer()
{
if ( animtimer < 0 ) return;
// animated frame (more math)
if ( isclosing || (animtimer < 1.) )
{
double intp, xfact, yfact;
if ( isclosing )
{
intp = animtimer-.1*System.GetTimeFrac();
xfact = clamp(intp*2.,0.,1.)**2.;
yfact = clamp(intp,0.,1.)**5.;
}
else
{
intp = animtimer+.1*System.GetTimeFrac();
xfact = clamp(intp*2,0.,1.)**2.;
yfact = clamp(intp,0.,1.)**5.;
}
double rwsx = int(SWWMUtility.Lerp(2,ws.x,xfact));
double rwsy = int(SWWMUtility.Lerp(2,ws.y,yfact));
if ( (rwsx == 2) && (rwsy == 2) ) return;
double rox = int(ss.x-rwsx)/2;
double roy = int(ss.y-rwsy)/2;
// copy the menu dim below the window during animations, so the transition looks smoother
Screen.Dim((dimamount<0)?Color("Black"):dimcolor,(dimamount<0)?.5:dimamount,int(rox*hs),int(roy*hs),int(rwsx*hs),int(rwsy*hs));
// draw the background and main frame
if ( swwm_fuzz )
{
// fuzz was designed for 16:10, so we'll have to extend it at taller ratios
int count = int(ceil(ws.y/400.));
Screen.SetClipRect(int(rox*hs),int(roy*hs),int(rwsx*hs),int(rwsy*hs));
for ( int i=0; i<count; i++ )
Screen.DrawTexture(FancyBg,false,origin.x,origin.y+400.*i,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_LegacyRenderStyle,STYLE_Add,DTA_Alpha,.5);
Screen.ClearClipRect();
}
Screen.Dim("Black",.8,int(rox*hs),int(roy*hs),int(rwsx*hs),int(rwsy*hs));
DrawFrame(rox-origin.x,roy-origin.y,rwsx,rwsy,true);
return;
}
// draw the background and main frame
if ( swwm_fuzz )
{
// fuzz was designed for 16:10, so we'll have to extend it at taller ratios
int count = int(ceil(ws.y/400.));
Screen.SetClipRect(int(origin.x*hs),int(origin.y*hs),int(ws.x*hs),int(ws.y*hs));
for ( int i=0; i<count; i++ )
Screen.DrawTexture(FancyBg,false,origin.x,origin.y+400.*i,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true,DTA_LegacyRenderStyle,STYLE_Add,DTA_Alpha,.5);
Screen.ClearClipRect();
}
Screen.Dim("Black",.8,int(origin.x*hs),int(origin.y*hs),int(ws.x*hs),int(ws.y*hs));
DrawFrame(0,0,ws.x,ws.y,true);
// draw top and bottom separators
DrawHSeparator(0,14,ws.x);
DrawHSeparator(0,ws.y-14,ws.x);
double xx = 5, yy = 1;
String str;
// draw tab listing
for ( int i=0; i<tabs.Size(); i++ )
{
if ( tabs[i].bHidden ) continue;
str = tabs[i].title;
Screen.DrawText(mSmallFont,(curtab==i)?Font.CR_FIRE:Font.CR_DARKGRAY,origin.x+xx,origin.y+yy,str,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true);
xx += mSmallFont.StringWidth(str)+10;
DrawVSeparator(xx-5,0,14);
}
// draw money
xx = 637-mSmallFont.StringWidth(munstr);
Screen.DrawText(mSmallFont,Font.CR_FIRE,origin.x+xx,origin.y+yy,munstr,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true);
yy = ws.y-13;
// draw clock / messages
if ( MenuTime() < tmsgtic ) str = tmsg;
else str = clockstr;
xx = 4;
Screen.DrawText(mSmallFont,Font.CR_WHITE,origin.x+xx,origin.y+yy,str,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true);
// draw os version
str = "DemolitionOS v1.0";
xx = 637-mSmallFont.StringWidth(str);
Screen.DrawText(mSmallFont,Font.CR_WHITE,origin.x+xx,origin.y+yy,str,DTA_VirtualWidthF,ss.x,DTA_VirtualHeightF,ss.y,DTA_KeepRatio,true);
// draw tab contents
if ( (tabs.Size() <= 0) || (curtab == -1) || !tabs[curtab] ) return;
tabs[curtab].Drawer(System.GetTimeFrac());
}
}