// stats stab Class DemolitionistStatsTab : DemolitionistMenuTab { DemolitionistMenuList lists[4]; int section; String sname[4]; int lwidth; SWWMStats stats; int ofs[4], maxofs[4]; double smofs[4]; bool drag; LevelStat clstat; override DemolitionistMenuTab Init( DemolitionistMenu master ) { title = StringTable.Localize("$SWWM_STATTAB"); section = 0; lwidth = 0; for ( int i=0; i<4; i++ ) { sname[i] = StringTable.Localize("$SWWM_STATTAB"..i); lwidth = max(lwidth,master.mSmallFont.StringWidth(sname[i])); lists[i] = new("DemolitionistMenuList"); lists[i].master = master; lists[i].selected = -1; } lwidth += 16; // account for padding of 8px on each side stats = Demolitionist(players[consoleplayer].mo)?Demolitionist(players[consoleplayer].mo).mystats:null; if ( !stats ) return Super.Init(master); // because these types of stats don't actually change while the menu is open, we can initialize their lists ONLY ONCE here if ( swwm_uniqstats ) { for ( int i=0; i tagb; } return a.kills < b.kills; } private int partition_mstats( Array a, int l, int h ) { DemolitionistMenuListItem pv = a[h]; int i = (l-1); for ( int j=l; j<=(h-1); j++ ) { if ( CmpMonsterKill(DemolitionistMenuKillItem(pv).s,DemolitionistMenuKillItem(a[j]).s) ) { i++; DemolitionistMenuListItem tmp = a[j]; a[j] = a[i]; a[i] = tmp; } } DemolitionistMenuListItem tmp = a[h]; a[h] = a[i+1]; a[i+1] = tmp; return i+1; } private void qsort_mstats( Array a, int l, int h ) { if ( l >= h ) return; int p = partition_mstats(a,l,h); qsort_mstats(a,l,p-1); qsort_mstats(a,p+1,h); } private bool CmpAchievement( SWWMAchievementInfo a, SWWMAchievementInfo b ) { bool adone = !!(a.state), bdone = !!(b.state); double afactor = adone?1.:0., bfactor = bdone?1.:0.; if ( a.maxval ) { int cur = 0; if ( a.bitfield ) { for ( int i=0; i b.baseindex; if ( !adone && !bdone ) { // progress sort? if ( afactor != bfactor ) return afactor < bfactor; // sort by base index return a.baseindex > b.baseindex; } // state sort return bdone; } private int partition_achievements( Array a, int l, int h ) { DemolitionistMenuListItem pv = a[h]; int i = (l-1); for ( int j=l; j<=(h-1); j++ ) { if ( CmpAchievement(DemolitionistMenuAchievementItem(pv).a,DemolitionistMenuAchievementItem(a[j]).a) ) { i++; DemolitionistMenuListItem tmp = a[j]; a[j] = a[i]; a[i] = tmp; } } DemolitionistMenuListItem tmp = a[h]; a[h] = a[i+1]; a[i+1] = tmp; return i+1; } private void qsort_achievements( Array a, int l, int h ) { if ( l >= h ) return; int p = partition_achievements(a,l,h); qsort_achievements(a,l,p-1); qsort_achievements(a,p+1,h); } override void Ticker() { if ( !stats ) return; // update list contents String str; int w; switch ( section ) { case 0: if ( lists[0].items.Size() == 0 ) // allocate first { for ( int i=0; i<31; i++ ) { let li = new("DemolitionistMenuListItem").Init(master,""); li.ypos = i*16; lists[0].items.Push(li); } } maxofs[0] = int(16*lists[0].items.Size()-(master.ws.y-46)); // oof int thour = ((level.totaltime-stats.lastspawn)/(3600*GameTicRate)); int tmin = ((level.totaltime-stats.lastspawn)/(60*GameTicRate))%60; int tsec = ((level.totaltime-stats.lastspawn)/GameTicRate)%60; str = String.Format("\cx%s\c-%02d\cu:\c-%02d\cu:\c-%02d",StringTable.Localize("$SWWM_STATUPTIME"),thour,tmin,tsec); lists[0].items[0].label = str; if ( stats.grounddist > 32000. ) str = String.Format("\cx%s\c-%g\cu%s\c-",StringTable.Localize("$SWWM_STATONFOOT"),stats.grounddist/32000.,StringTable.Localize("$SWWM_UNIT_KILOMETER")); else str = String.Format("\cx%s\c-%g\cu%s\c-",StringTable.Localize("$SWWM_STATONFOOT"),stats.grounddist/32.,StringTable.Localize("$SWWM_UNIT_METER")); lists[0].items[1].label = str; if ( stats.airdist > 32000. ) str = String.Format("\cx%s\c-%g\cu%s\c-",StringTable.Localize("$SWWM_STATFLIGHT"),stats.airdist/32000.,StringTable.Localize("$SWWM_UNIT_KILOMETER")); else str = String.Format("\cx%s\c-%g\cu%s\c-",StringTable.Localize("$SWWM_STATFLIGHT"),stats.airdist/32.,StringTable.Localize("$SWWM_UNIT_METER")); lists[0].items[2].label = str; if ( stats.swimdist > 32000. ) str = String.Format("\cx%s\c-%g\cu%s\c-",StringTable.Localize("$SWWM_STATSWIM"),stats.swimdist/32000.,StringTable.Localize("$SWWM_UNIT_KILOMETER")); else str = String.Format("\cx%s\c-%g\cu%s\c-",StringTable.Localize("$SWWM_STATSWIM"),stats.swimdist/32.,StringTable.Localize("$SWWM_UNIT_METER")); lists[0].items[3].label = str; if ( stats.teledist > 32000. ) str = String.Format("\cx%s\c-%g\cu%s\c-",StringTable.Localize("$SWWM_STATTELE"),stats.teledist/32000.,StringTable.Localize("$SWWM_UNIT_KILOMETER")); else str = String.Format("\cx%s\c-%g\cu%s\c-",StringTable.Localize("$SWWM_STATTELE"),stats.teledist/32.,StringTable.Localize("$SWWM_UNIT_METER")); lists[0].items[4].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATBOOST"),stats.boostcount); lists[0].items[5].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATDASH"),stats.dashcount); lists[0].items[6].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATSTOMP"),stats.stompcount); lists[0].items[7].label = str; str = String.Format("\cx%s\c-%g\cu%s\c-",StringTable.Localize("$SWWM_STATFUEL"),stats.fuelusage,StringTable.Localize("$SWWM_UNIT_LITER")); lists[0].items[8].label = str; str = String.Format("\cx%s\c-%g\cu%s\c-",StringTable.Localize("$SWWM_STATSPEED"),(stats.topspeed*3600.*GameTicRate)/32000.,StringTable.Localize("$SWWM_UNIT_KPH")); lists[0].items[9].label = str; thour = (stats.airtime/(3600*GameTicRate)); tmin = (stats.airtime/(60*GameTicRate))%60; tsec = (stats.airtime/GameTicRate)%60; str = String.Format("\cx%s\c-%02d\cu:\c-%02d\cu:\c-%02d",StringTable.Localize("$SWWM_STATAIRTIME"),thour,tmin,tsec); lists[0].items[10].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATPARRY"),stats.parries); lists[0].items[11].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATPPARRY"),stats.pparries); lists[0].items[12].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATWPONCH"),stats.wponch); lists[0].items[13].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATBUSTS"),stats.busts); lists[0].items[14].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATBUTTS"),stats.buttslams); lists[0].items[15].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATPATS"),stats.pats); lists[0].items[16].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATKISS"),stats.smooch); lists[0].items[17].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATFRIENDS"),stats.befriend); lists[0].items[18].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATITEMS"),stats.items); lists[0].items[19].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATSECRETS"),stats.secrets); lists[0].items[20].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATKILLS"),stats.kills); lists[0].items[21].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATDEATHS"),stats.deaths); lists[0].items[22].label = str; if ( stats.hdamagedealt > 0 ) str = str = String.Format("\cx%s\c-%d%09d",StringTable.Localize("$SWWM_STATDDEALT"),stats.hdamagedealt,stats.damagedealt); else str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATDDEALT"),stats.damagedealt); lists[0].items[23].label = str; if ( stats.hdamagetaken > 0 ) str = String.Format("\cx%s\c-%d%09d",StringTable.Localize("$SWWM_STATDTAKEN"),stats.hdamagetaken,stats.damagetaken); else str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATDTAKEN"),stats.damagetaken); lists[0].items[24].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATTDEALT"),stats.topdealt); lists[0].items[25].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATTTAKEN"),stats.toptaken); lists[0].items[26].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATMKILL"),stats.mkill); lists[0].items[27].label = str; str = String.Format("\cx%s\c-%d",StringTable.Localize("$SWWM_STATSKILL"),stats.skill); lists[0].items[28].label = str; str = String.Format("\cx%s\c-",StringTable.Localize("$SWWM_STATFAVWEAP")); if ( stats.favweapon == -1 ) str = str.."N/A"; else if ( stats.wstats[stats.favweapon].w == 'SWWMWeapon' ) str = str..StringTable.Localize("$SWWM_YOURSELF"); else if ( stats.wstats[stats.favweapon].w == 'SWWMGesture' ) str = str..StringTable.Localize("$SWWM_DOKIDOKI"); else if ( stats.wstats[stats.favweapon].w == 'SWWMItemGesture' ) str = str..StringTable.Localize("$T_FROGGY"); else if ( stats.wstats[stats.favweapon].w == 'Weapon' ) str = str..StringTable.Localize("$SWWM_GRAVKILL"); else if ( stats.wstats[stats.favweapon].w == 'DoomWeapon' ) str = str..StringTable.Localize("$SWWM_PARRYKILL"); else { let def = GetDefaultByType(stats.wstats[stats.favweapon].w); str = str..def.GetTag(); } lists[0].items[29].label = str; str = String.Format("\cx%s\cu¥\c-%09d",StringTable.Localize("$SWWM_STATHISCORE"),stats.hiscore); lists[0].items[30].label = str; break; case 1: // theoretically we can assume that the monster stats list will never shrink in the middle of gameplay, only grow // also the array only ever has elements pushed, never inserted in-between // if this somehow breaks, I don't know what the hell can do that if ( lists[1].items.Size() < stats.mstats.Size() ) { int oldindex = lists[1].items.Size(); lists[1].items.Resize(stats.mstats.Size()); for ( int i=oldindex; i 0 ) w -= 8; for ( int i=0; i 0 ) w -= 8; int len[4], maxlen[4]; for ( int i=0; i<4; i++ ) maxlen[i] = 0; for ( int i=0; i maxlen[0] ) maxlen[0] = len[0]; if ( l.s.stotal > 0 ) { str = String.Format("S %d/%d",l.s.scount,l.s.stotal); len[1] = master.mTinyFont.StringWidth(str); if ( len[1] > maxlen[1] ) maxlen[1] = len[1]; } if ( l.s.itotal > 0 ) { str = String.Format("I %d/%d",l.s.icount,l.s.itotal); len[2] = master.mTinyFont.StringWidth(str); if ( len[2] > maxlen[2] ) maxlen[2] = len[2]; } if ( l.s.ktotal > 0 ) { str = String.Format("K %d/%d",l.s.kcount,l.s.ktotal); len[3] = master.mTinyFont.StringWidth(str); if ( len[3] > maxlen[3] ) maxlen[3] = len[3]; } } // second pass to propagate the "max lengths" for ( int i=0; i 0 ) w -= 8; for ( int i=0; i 3 ) section = 0; smofs[section] = ofs[section]; break; case MK_DOWN: if ( Scroll(16) ) master.MenuSound("menu/demoscroll"); break; case MK_UP: if ( Scroll(-16) ) master.MenuSound("menu/demoscroll"); break; } } override void MouseInput( Vector2 pos, int btn ) { // global events switch ( btn ) { case MB_DRAG: if ( drag ) SetOffset(pos.y); break; case MB_RELEASE: drag = false; break; } // mouse on left side if ( pos.x < lwidth ) { if ( btn != MB_LEFT ) return; for ( int i=0; i<4; i++ ) { if ( pos.x < 9 ) continue; if ( pos.x > 9+master.mSmallFont.StringWidth(sname[i]) ) continue; if ( pos.y < 23+16*i ) continue; if ( pos.y > 36+16*i ) continue; if ( section != i ) { master.MenuSound("menu/demoscroll"); smofs[section] = ofs[section]; section = i; smofs[section] = ofs[section]; } break; } return; } switch ( btn ) { case MB_LEFT: // see if we're clicking the scrollbar (if it exists) if ( (maxofs[section] > 0) && (pos.x > (master.ws.x-8)) ) { SetOffset(pos.y); master.MenuSound("menu/demoscroll"); drag = true; break; } break; case MB_WHEELUP: if ( Scroll(-8) ) master.MenuSound("menu/demoscroll"); break; case MB_WHEELDOWN: if ( Scroll(8) ) master.MenuSound("menu/demoscroll"); break; } } override void Drawer( double fractic ) { if ( !stats ) return; double xx = 9; double yy = 23; for ( int i=0; i<4; i++ ) { Screen.DrawText(master.mSmallFont,(i==section)?Font.CR_FIRE:Font.CR_WHITE,master.origin.x+xx,master.origin.y+yy,sname[i],DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); yy += 16; } master.DrawVSeparator(lwidth,14,master.ws.y-28); if ( lists[section].items.Size() == 0 ) { String str = StringTable.Localize("$SWWM_NOSTAT"); xx = lwidth+int((master.ws.x-lwidth)-master.mSmallFont.StringWidth(str))/2; yy = int(master.ws.y-master.mSmallFont.GetHeight())/2; Screen.DrawText(master.mSmallFont,Font.CR_FIRE,master.origin.x+xx,master.origin.y+yy,str,DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); return; } double ssmofs = (smofs[section]~==ofs[section])?smofs[section]:(smofs[section]*(1.-fractic)+((smofs[section]*.6)+(ofs[section]*.4))*fractic); if ( section == 3 ) { // achievement drawer has different margins xx = lwidth+12; yy = 26; Screen.SetClipRect(int((master.origin.x+lwidth+12)*master.hs),int((master.origin.y+26)*master.hs),int((master.ws.x-(lwidth+24))*master.hs),int((master.ws.y-52)*master.hs)); lists[section].Drawer((xx,yy-ssmofs)); } else { xx = lwidth+9; yy = 23; Screen.SetClipRect(int((master.origin.x+lwidth+9)*master.hs),int((master.origin.y+23)*master.hs),int((master.ws.x-(lwidth+18))*master.hs),int((master.ws.y-46)*master.hs)); lists[section].Drawer((xx,yy-ssmofs)); } Screen.ClearClipRect(); if ( maxofs[section] > 0 ) { xx = master.ws.x-8; master.DrawVSeparator(xx,14,master.ws.y-28); xx += 2; yy = floor(ssmofs*((master.ws.y-39)/maxofs[section]))+14; Screen.DrawText(master.mSmallFont,Font.CR_FIRE,master.origin.x+xx,master.origin.y+yy,"▮",DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); } } } // monster kill stat item Class DemolitionistMenuKillItem : DemolitionistMenuListItem { MonsterKill s; int width; DemolitionistMenuKillItem Init( DemolitionistMenu master, MonsterKill s, int width = 0 ) { Super.Init(master,""); self.s = s; self.width = width; let m = GetDefaultByType(s.m); self.label = m.GetTag(FallbackTag); if ( self.label == FallbackTag ) { self.label = m.GetClassName(); SWWMUtility.BeautifyClassName(self.label); } return self; } override int GetWidth() { return width; } override void Drawer( Vector2 pos, bool selected ) { Screen.DrawText(master.mSmallFont,Font.CR_FIRE,master.origin.x+pos.x,master.origin.y+pos.y,label,DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); String str = String.Format("%d",s.kills); Screen.DrawText(master.mSmallFont,Font.CR_WHITE,master.origin.x+pos.x+width-master.mSmallFont.StringWidth(str),master.origin.y+pos.y,str,DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); } } // map stat item Class DemolitionistMenuMapStatItem : DemolitionistMenuListItem { LevelStat s; int width; int maxlen[4]; DemolitionistMenuMapStatItem Init( DemolitionistMenu master, LevelStat s, int width = 0 ) { Super.Init(master,""); self.s = s; self.width = width; self.label = s.hub?s.levelname:String.Format("%s - %s",s.mapname.MakeUpper(),s.levelname); return self; } override int GetWidth() { return width; } override void Drawer( Vector2 pos, bool selected ) { String str = label; if ( selected ) str = "\cd▸\c- "..str; bool smallname = master.mSmallFont.StringWidth(str)>(width-(maxlen[3]+maxlen[2]+maxlen[1]+maxlen[0]+24)); Screen.DrawText(smallname?master.mTinyFont:master.mSmallFont,Font.CR_FIRE,master.origin.x+pos.x,master.origin.y+pos.y+smallname*2,str,DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); double xx = pos.x+width; double yy = pos.y+2; int sec = Thinker.Tics2Seconds(s.time); str = String.Format("%02d\cu:\c-%02d\cu:\c-%02d",sec/3600,(sec%3600)/60,sec%60); Screen.DrawText(master.mTinyFont,((s.suck>0)&&(sec>=(s.suck*3600)))?Font.CR_RED:(sec<=s.par)?Font.CR_GOLD:Font.CR_WHITE,master.origin.x+xx-master.mTinyFont.StringWidth(str),master.origin.y+yy,str,DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); Screen.DrawText(master.mTinyFont,Font.CR_FIRE,master.origin.x+xx-maxlen[0],master.origin.y+yy,"T",DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); if ( maxlen[0] > 0 ) xx -= maxlen[0]+8; if ( s.stotal > 0 ) { str = String.Format("%d\cu/\c-%d",s.scount,s.stotal); Screen.DrawText(master.mTinyFont,(s.scount>=s.stotal)?Font.CR_GOLD:Font.CR_WHITE,master.origin.x+xx-master.mTinyFont.StringWidth(str),master.origin.y+yy,str,DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); Screen.DrawText(master.mTinyFont,Font.CR_FIRE,master.origin.x+xx-maxlen[1],master.origin.y+yy,"S",DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); } if ( maxlen[1] > 0 ) xx -= maxlen[1]+8; if ( s.itotal > 0 ) { str = String.Format("%d\cu/\c-%d",s.icount,s.itotal); Screen.DrawText(master.mTinyFont,(s.icount>=s.itotal)?Font.CR_GOLD:Font.CR_WHITE,master.origin.x+xx-master.mTinyFont.StringWidth(str),master.origin.y+yy,str,DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); Screen.DrawText(master.mTinyFont,Font.CR_FIRE,master.origin.x+xx-maxlen[2],master.origin.y+yy,"I",DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); } if ( maxlen[2] > 0 ) xx -= maxlen[2]+8; if ( s.ktotal > 0 ) { str = String.Format("%d\cu/\c-%d",s.kcount,s.ktotal); Screen.DrawText(master.mTinyFont,(s.kcount>=s.ktotal)?Font.CR_GOLD:Font.CR_WHITE,master.origin.x+xx-master.mTinyFont.StringWidth(str),master.origin.y+yy,str,DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); Screen.DrawText(master.mTinyFont,Font.CR_FIRE,master.origin.x+xx-maxlen[3],master.origin.y+yy,"K",DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true); } } } // achievement item Class DemolitionistMenuAchievementItem : DemolitionistMenuListItem { SWWMAchievementInfo a; TextureID AchievementUnknown, BaseBox, BarTex[3]; bool ShouldObscure; bool bHidden; int width; BrokenLines l; String oldstr; DemolitionistMenuAchievementItem Init( DemolitionistMenu master, SWWMAchievementInfo a, int width = 0 ) { Super.Init(master,""); self.a = a; self.width = width; AchievementUnknown = TexMan.CheckForTexture("graphics/Achievements/HiddenAchievement.png"); BaseBox = TexMan.CheckForTexture("graphics/Achievements/NoAchievement.png"); BarTex[0] = TexMan.CheckForTexture("graphics/Achievements/BarAchievementBase.png"); BarTex[1] = TexMan.CheckForTexture("graphics/Achievements/BarAchievementProgress.png"); BarTex[2] = TexMan.CheckForTexture("graphics/Achievements/BarAchievementDone.png"); ShouldObscure = (swwm_filterachievements==1); Update(); return self; } // cache state/progress to not call it every frame void Update() { a.state = master.shnd.achievementstate.At(a.basename).ToInt(); if ( !a.maxval ) return; a.val = master.shnd.achievementprogress.At(a.basename).ToInt(); if ( !a.bitfield ) return; int val = 0; for ( int i=0; i