// shoppin' Class DemolitionistStoreTab : DemolitionistMenuTab { DemolitionistMenuList invlist[2]; bool bDisabled; bool bSell; int ofs, maxofs, maxw; double smofs; bool drag; private bool CmpInventoryClass( Class a, Class b ) { int ta = 0, tb = 0; let da = GetDefaultByType(a); let db = GetDefaultByType(b); if ( a is 'Weapon' ) ta = 4; else if ( a is 'HammerspaceEmbiggener' ) ta = 3; else if ( a is 'Ammo' ) ta = 2; else if ( a is 'MagAmmo' ) ta = 1; else if ( (a is 'PowerupGiver') || (a is 'AmmoFabricator') || da.bBIGPOWERUP ) ta = 5; else if ( (a is 'Health') || (a is 'HealthPickup') || (a is 'SWWMHealth') ) ta = 7; else if ( (a is 'Armor') || (a is 'SWWMSpareArmor') ) ta = 6; if ( b is 'Weapon' ) tb = 4; else if ( b is 'HammerspaceEmbiggener' ) tb = 3; else if ( b is 'Ammo' ) tb = 2; else if ( b is 'MagAmmo' ) ta = 1; else if ( (b is 'PowerupGiver') || (b is 'AmmoFabricator') || db.bBIGPOWERUP ) tb = 5; else if ( (b is 'Health') || (b is 'HealthPickup') || (b is 'SWWMHealth') ) tb = 7; else if ( (b is 'Armor') || (b is 'SWWMSpareArmor') ) tb = 6; if ( ta == tb ) { // sort by unit price if ( abs(da.Stamina) == abs(db.Stamina) ) { // sort alphabetically return da.GetTag() > db.GetTag(); } return abs(da.Stamina) > abs(db.Stamina); } return ta < tb; } private int partition_store( Array a, int l, int h ) { DemolitionistMenuListItem pv = a[h]; int i = (l-1); for ( int j=l; j<=(h-1); j++ ) { if ( CmpInventoryClass(DemolitionistMenuStoreItem(pv).inv,DemolitionistMenuStoreItem(a[j]).inv) ) { 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_store( Array a, int l, int h ) { if ( l >= h ) return; int p = partition_store(a,l,h); qsort_store(a,l,p-1); qsort_store(a,p+1,h); } override DemolitionistMenuTab Init( DemolitionistMenu master ) { title = StringTable.Localize("$SWWM_STORETAB"); bDisabled = (deathmatch||(G_SkillPropertyInt(SKILLP_ACSReturn)>=4)||players[consoleplayer].mo.FindInventory("SWWMStoreDisabler")); return Super.Init(master); } override void OnDestroy() { Super.OnDestroy(); if ( invlist[0] ) invlist[0].Destroy(); if ( invlist[1] ) invlist[1].Destroy(); } override void OnSelect() { bSell = master.shnd.menustate.At("WasSelling").ToInt(); smofs = ofs; } override void OnDeselect() { master.shnd.menustate.Insert("WasSelling",String.Format("%d",bSell)); smofs = ofs; } private bool FilterStore( Class type, Inventory cur, readonly inv ) { // no collectibles if ( type is 'SWWMCollectible' ) return true; // no barriers outside doom if ( !(gameinfo.gametype&GAME_DOOM) && (type is 'EBarrier') ) return true; // no gravity/tether outside raven if ( !(gameinfo.gametype&GAME_RAVEN) && ((type is 'GravitySuppressor') || (type is 'SafetyTether')) ) return true; // can't sell candygun spares if we already own a Candy Gun if ( bSell && (type is 'CandyGunSpares') && players[consoleplayer].mo.FindInventory("CandyGun") ) return true; // skip items we don't own or are depleted if selling if ( bSell && (!cur || (cur.Amount <= 0)) ) return true; else if ( !bSell ) { // skip maxed items if buying if ( cur && (cur.Amount >= cur.MaxAmount) ) return true; // ignore ammo for weapons not owned if buying bool notownedammo = false; if ( type is 'Ammo' ) { notownedammo = true; let amo = (Class)(type); for ( Inventory inv=players[consoleplayer].mo.Inv; inv; inv=inv.inv ) { if ( !(inv is 'Weapon') ) continue; if ( (Weapon(inv).AmmoType1 == amo) || (Weapon(inv).AmmoType2 == amo) ) { notownedammo = false; break; } if ( (inv is 'SWWMWeapon') && SWWMWeapon(inv).UsesAmmo(amo) ) { notownedammo = false; break; } } } if ( notownedammo ) return true; } // skip unimplemented weapons if ( type is 'Weapon' ) { let ready = inv.FindState("Ready"); if ( !ready || !ready.ValidateSpriteFrame() ) return true; } // ignore child ammos if ( (type is 'Ammo') && (type.GetParentClass() != 'SWWMAmmo') ) return true; if ( (type is 'MagAmmo') && (type.GetParentClass() != 'MagAmmo') ) return true; // items must have a set price if ( inv.Stamina == 0 ) return true; // items with negative stamina can only be sold if ( (inv.Stamina < 0) && !bSell ) return true; return false; } override void Ticker() { if ( bDisabled ) return; // do nothing bool mustsort = false; bool skipsel = false; // only update active list to reduce perf hit if ( !invlist[bSell] ) { mustsort = true; skipsel = true; // initialize invlist[bSell] = new("DemolitionistMenuList"); invlist[bSell].master = master; invlist[bSell].selected = 0; } // check if current entries must be deleted for ( int i=0; i i ) invlist[bSell].selected = max(0,invlist[bSell].selected-1); i--; } foreach ( cls:AllActorClasses ) { let type = (Class)(cls); if ( !type ) continue; let cur = players[consoleplayer].mo.FindInventory(type); let inv = GetDefaultByType(type); if ( FilterStore(type,cur,inv) ) continue; // skip if it's already there bool skipme = false; foreach ( i:invlist[bSell].items ) { if ( DemolitionistMenuStoreItem(i).inv != type ) continue; skipme = true; break; } if ( skipme ) continue; // insert sorted by unit price bool greater = false; for ( int j=0; j abs(inv2.Stamina) ) continue; greater = true; invlist[bSell].items.Insert(j,new("DemolitionistMenuStoreItem").Init(master,type,bSell)); mustsort = true; break; } if ( greater ) continue; invlist[bSell].items.Push(new("DemolitionistMenuStoreItem").Init(master,type,bSell)); mustsort = true; } // don't do anything if empty if ( invlist[bSell].items.Size() <= 0 ) return; // sort inventory if ( mustsort ) { DemolitionistMenuListItem csel = invlist[bSell].items[invlist[bSell].selected]; qsort_store(invlist[bSell].items,0,invlist[bSell].items.Size()-1); let idx = invlist[bSell].items.Find(csel); if ( !skipsel && (idx != invlist[bSell].items.Size()) && (idx != invlist[bSell].selected) ) invlist[bSell].selected = idx; } // rearrange all item positions maxofs = 0; maxw = 0; foreach ( i:invlist[bSell].items ) { let fw = DemolitionistMenuStoreItem(i).GetFullWidth(); if ( fw > maxw ) maxw = fw; } int xx = 0; int yy = 0; foreach ( i:invlist[bSell].items ) { maxofs = max(maxofs,xx+maxw-16); DemolitionistMenuStoreItem(i).width = maxw; i.xpos = xx; i.ypos = yy; yy += 16; if ( yy > (master.ws.y-62) ) { xx += maxw; yy = 0; } } maxofs = max(0,maxofs-int(master.ws.x-18)); if ( ofs > maxofs ) smofs = ofs = maxofs; // update smooth scroll smofs = (smofs*.6)+(ofs*.4); if ( abs(smofs-ofs) < (1./master.hs) ) smofs = ofs; // tick the list invlist[bSell].Ticker(); } // called when sending a scroll input // returns true if the position actually changed // speed: how many pixels to move (either back or forward) bool Scroll( int speed ) { if ( maxofs <= 0 ) return false; int oldofs = ofs; ofs = clamp(ofs+speed,0,maxofs); return (ofs != oldofs); } // called when clicking on our scrollbar // returns true if the position actually changed // x: relative click position bool SetOffset( double x ) { if ( maxofs <= 0 ) return false; int oldofs = ofs; ofs = clamp(int(round((x-5.)/((master.ws.x-10.)/maxofs))),0,maxofs); return (ofs != oldofs); } // called when keyboard scrolling void KBScroll() { if ( maxofs <= 0 ) return; int minx = (invlist[bSell].items[invlist[bSell].selected].xpos+maxw-16)-int(master.ws.x-18); int maxx = invlist[bSell].items[invlist[bSell].selected].xpos; if ( ofs < minx ) ofs = clamp(minx,0,maxofs); else if ( ofs > maxx ) ofs = clamp(maxx,0,maxofs); } override void MenuInput( int key ) { if ( bDisabled ) return; // do nothing if ( key == MK_BACK ) { bSell = !bSell; master.MenuSound(bSell?"menu/demosel":"menu/democlose"); if ( invlist[bSell] && (invlist[bSell].items.Size() > 0) ) { invlist[bSell].selected = 0; KBScroll(); } return; } if ( !invlist[bSell] || (invlist[bSell].items.Size() <= 0) ) return; switch ( key ) { case MK_LEFT: int prev = max(0,invlist[bSell].selected-22); if ( prev == invlist[bSell].selected ) break; invlist[bSell].selected = prev; master.MenuSound("menu/demoscroll"); KBScroll(); break; case MK_RIGHT: int next = min(invlist[bSell].items.Size()-1,invlist[bSell].selected+22); if ( next == invlist[bSell].selected ) break; invlist[bSell].selected = next; master.MenuSound("menu/demoscroll"); KBScroll(); break; case MK_DOWN: if ( invlist[bSell].selected < invlist[bSell].items.Size()-1 ) { invlist[bSell].selected++; master.MenuSound("menu/demoscroll"); KBScroll(); } break; case MK_UP: if ( invlist[bSell].selected > 0 ) { invlist[bSell].selected--; master.MenuSound("menu/demoscroll"); KBScroll(); } break; case MK_ENTER: if ( invlist[bSell].selected >= invlist[bSell].items.Size() ) break; DemolitionistMenuStoreItem(invlist[bSell].items[invlist[bSell].selected]).BuySellItem(); break; } } override void MouseInput( Vector2 pos, int btn ) { if ( bDisabled ) return; // do nothing if ( btn == MB_RIGHT ) { // just toggle buy/sell MenuInput(MK_BACK); return; } if ( !invlist[bSell] || (invlist[bSell].items.Size() <= 0) ) return; switch ( btn ) { case MB_LEFT: // see if we're clicking the scrollbar (if it exists) if ( pos.y > (master.ws.y-21) ) { if ( SetOffset(pos.x) ) { master.MenuSound("menu/demoscroll"); drag = true; } break; } // find which element we clicked for ( int i=0; i inv; int col; bool bSell; String pricelabel; int width; DemolitionistMenuStoreItem Init( DemolitionistMenu master, Class i, bool bSell = false ) { Super.Init(master,""); inv = i; col = Font.CR_WHITE; let def = GetDefaultByType(i); if ( i is 'Weapon' ) col = SWWMUtility.IsVIPItemClass(i)?Font.FindFontColor('VIPGold'):Font.CR_GOLD; else if ( i is 'MagAmmo' ) col = SWWMUtility.IsVIPItemClass(i)?Font.FindFontColor('VIPTan'):Font.CR_TAN; else if ( (i is 'BackpackItem') || (i is 'HammerspaceEmbiggener') ) col = Font.CR_DARKBROWN; else if ( i is 'Ammo' ) col = SWWMUtility.IsVIPItemClass(i)?Font.FindFontColor('VIPBrown'):Font.CR_BROWN; else if ( (i is 'PowerupGiver') || (i is 'AmmoFabricator') || def.bBIGPOWERUP ) col = SWWMUtility.IsVIPItemClass(i)?Font.FindFontColor('VIPPurple'):Font.CR_PURPLE; else if ( (i is 'Health') || (i is 'HealthPickup') || (i is 'SWWMHealth') ) col = Font.CR_RED; else if ( (i is 'Armor') || (i is 'SWWMSpareArmor') ) col = Font.CR_GREEN; self.bSell = bSell; UpdateLabel(); return self; } // used for the store display calculation int GetFullWidth() { return master.mSmallFont.StringWidth(label)+96+master.mSmallFont.StringWidth(pricelabel); } // so the prices don't get cut off override Vector2 GetDrawBounds() { return (width-16,GetHeight()); } // also checks for clicking on the price override bool CheckBounds( double x, double y ) { if ( Super.CheckBounds(x,y) ) return true; int ofs = (width-16)-master.mSmallFont.StringWidth(pricelabel); if ( x < xpos+ofs ) return false; if ( y < ypos ) return false; if ( x > xpos+width ) return false; if ( y > ypos+GetHeight() ) return false; return true; } // get the score to pay/receive and the units to buy/sell int, int GetPriceUnits() { let def = GetDefaultByType(inv); let cur = players[consoleplayer].mo.FindInventory(inv); int amt = def.Amount; int price = int(abs(def.Stamina)*(1.+.75*(amt-1))); if ( inv is 'Ammo' ) { int maxamt; if ( bSell ) maxamt = cur.Amount; // we can sell ALL our ammo else maxamt = cur?(cur.MaxAmount-cur.Amount):def.MaxAmount; // get the largest affordable child pickup amount (that we need, or we can sell) foreach ( cls:AllActorClasses ) { let inv2 = (Class)(cls); if ( !inv2 || (inv2.GetParentClass() != inv) ) continue; let def2 = GetDefaultByType(inv2); int cprice = int(abs(def.Stamina)*(1.+.75*(def2.Amount-1))); if ( (def2.Amount > amt) && (def2.Amount <= maxamt) && (bSell || SWWMCredits.CanTake(players[consoleplayer],cprice)) ) { price = cprice; amt = def2.Amount; } } } else if ( inv is 'MagAmmo' ) { // never bought, only sold int maxamt = cur.Amount; // get the largest affordable child pickup amount (that we can sell) foreach ( cls:AllActorClasses ) { let inv2 = (Class)(cls); if ( !inv2 || (inv2.GetParentClass() != inv) ) continue; let def2 = GetDefaultByType(inv2); int cprice = int(abs(def.Stamina)*(1.+.75*(def2.Amount-1))); if ( (def2.Amount > amt) && (def2.Amount <= maxamt) ) { price = cprice; amt = def2.Amount; } } } if ( bSell && (inv is 'Weapon') ) { let w = GetDefaultByType((Class)(inv)); // subtract price of given ammo, as we're only selling the weapon itself if ( w.AmmoType1 && (w.AmmoGive1 > 0) ) { let am1 = GetDefaultByType(w.AmmoType1); if ( am1.Stamina != 0 ) price -= int(abs(am1.Stamina)*(1.+.75*(w.AmmoGive1-1))); } if ( w.AmmoType2 && (w.AmmoGive2 > 0) ) { let am2 = GetDefaultByType(w.AmmoType2); if ( am2.Stamina != 0 ) price -= int(abs(am2.Stamina)*(1.+.75*(w.AmmoGive2-1))); } } // sell at half price if ( bSell ) price = int(abs(def.Stamina)*amt)/2; return price, amt; } // formatted name private void UpdateLabel() { int price, amt; [price, amt] = GetPriceUnits(); let def = GetDefaultByType(inv); if ( bSell ) { pricelabel = String.Format("\cd¥%d\c-",price); int cur = (inv is 'CandyGun')?(players[consoleplayer].mo.CountInv("CandyGunSpares")+1):players[consoleplayer].mo.CountInv(inv); if ( (cur > 1) || (inv is 'Ammo') || (inv is 'MagAmmo') ) label = String.Format("%s (%d/%d)",def.GetTag(),amt,cur); else label = def.GetTag(); } else { if ( price > master.muns ) pricelabel = String.Format("\cm¥%d\c-",price); else pricelabel = String.Format("\cx¥%d\cx",price); if ( (amt > 1) || (inv is 'Ammo') || (inv is 'MagAmmo') ) label = String.Format("%dx %s",amt,def.GetTag()); else label = def.GetTag(); } } override void Ticker() { UpdateLabel(); } override void Drawer( Vector2 pos, bool selected ) { Screen.DrawText(master.mSmallFont,col,master.origin.x+pos.x,master.origin.y+pos.y,label,DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true,DTA_ColorOverlay,selected?Color(0,0,0,0):Color(96,0,0,0)); Screen.DrawText(master.mSmallFont,Font.CR_UNTRANSLATED,master.origin.x+pos.x+(width-16)-master.mSmallFont.StringWidth(pricelabel),master.origin.y+pos.y,pricelabel,DTA_VirtualWidthF,master.ss.x,DTA_VirtualHeightF,master.ss.y,DTA_KeepRatio,true,DTA_ColorOverlay,selected?Color(0,0,0,0):Color(96,0,0,0)); } void BuySellItem() { int price, amt; [price, amt] = GetPriceUnits(); if ( bSell ) { EventHandler.SendNetworkEvent(String.Format("swwmstoretake.%s",inv.GetClassName()),consoleplayer,price,amt); master.MenuSound("menu/buyinv"); return; } if ( !SWWMCredits.CanTake(players[consoleplayer],price) ) { master.MenuSound("menu/noinvuse"); master.tmsg = StringTable.Localize("$SWWM_STOREMUNS"); master.tmsgtic = Menu.MenuTime()+70; return; } if ( (inv is 'SWWMWeapon') && swwm_swapweapons ) { // check swapweapon let wpn = GetDefaultByType((Class)(inv)); let sw = wpn.HasSwapWeapon(players[consoleplayer].mo); if ( sw ) { master.MenuSound("menu/noinvuse"); master.tmsg = String.Format(StringTable.Localize("$SWWM_STORESWAP"),sw.GetTag()); master.tmsgtic = Menu.MenuTime()+70; return; } } let cur = players[consoleplayer].mo.FindInventory(inv); int camt, max; if ( cur ) { camt = cur.Amount; max = cur.MaxAmount; } else { camt = 0; max = GetDefaultByType(inv).MaxAmount; } if ( (max > 0) && (camt >= max) ) { master.MenuSound("menu/noinvuse"); master.tmsg = StringTable.Localize("$SWWM_STOREFULL"); master.tmsgtic = Menu.MenuTime()+70; return; } EventHandler.SendNetworkEvent(String.Format("swwmstoregive.%s",inv.GetClassName()),consoleplayer,price,amt); master.MenuSound("menu/buyinv"); } }