NoneNoname / Bondage Club Extension

// ==UserScript==
// @name         Bondage Club Extension
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  UI changes, fixes
// @author       NoneNoname
// @license      MIT
// @include      http://*.bondageprojects.com/college/*/BondageClub/
// @include      https://*.bondageprojects.com/college/*/BondageClub/*
// @require      https://raw.githubusercontent.com/blueimp/JavaScript-MD5/master/js/md5.min.js
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  Asset.forEach(e => {
    e.SelfBondage = true;
    e.PropertyLocked = e.Name == "CollarNameTag";
  });
  AssetGroup.forEach(e => {
    if (["Hat", "HairAccessory1", "HairAccessory2", "Wings", "TailStraps"].indexOf(e.Name) >= 0) e.KeepNaked = true;
  });
  AssetGroup.forEach(e => {
    if (e.IsDefault && ["Cloth", "ClothLower", "Bra", "Panties", "Socks", "Shoes"].indexOf(e.Name) >= 0) e.IsDefault = false;
  });

  window.LoginValidCollar = function LoginValidCollar() {
    if ((InventoryGet(Player, "ItemNeck") == null) && (Player.Owner != "")) InventoryWear(Player, "SlaveCollar", "ItemNeck");
  }

  DrawBackNextButton = function DrawBackNextButton(Left, Top, Width, Height, Label, Color, Image, BackText, NextText, ThreeWay) {
    var SplitWidth = Width / 2;
    if (ThreeWay) SplitWidth = Width / 3
    var Split = Left + SplitWidth;
    // Draw the button rectangle (makes half of the background cyan colored if the mouse is over it)
    MainCanvas.beginPath();
    MainCanvas.rect(Left, Top, Width, Height);
    MainCanvas.fillStyle = Color;
    MainCanvas.fillRect(Left, Top, Width, Height);
    if (!CommonIsMobile && (MouseX >= Left) && (MouseX <= Left + Width) && (MouseY >= Top) && (MouseY <= Top + Height)) {
      MainCanvas.fillStyle = "Cyan";
      if (ThreeWay && (MouseX > Left + SplitWidth * 2)) MainCanvas.fillRect(Left + SplitWidth * 2, Top, SplitWidth, Height);
      else if (MouseX > Left + SplitWidth) MainCanvas.fillRect(Left + SplitWidth, Top, SplitWidth, Height);
      else MainCanvas.fillRect(Left, Top, SplitWidth, Height);
    }
    MainCanvas.lineWidth = '2';
    MainCanvas.strokeStyle = 'black';
    MainCanvas.stroke();
    MainCanvas.closePath();

    // Draw the text or image
    DrawTextFit(Label, Left + Width / 2, Top + (Height / 2) + 1, Width - 4, "black");
    if ((Image != null) && (Image != "")) DrawImageResize(Image, Left + 2, Top + 2, Width - 4, Height - 4);

    if (CommonIsMobile && ThreeWay) return;

    // Draw the back arrow
    MainCanvas.beginPath();
    MainCanvas.fillStyle = "black";
    MainCanvas.moveTo(Left + 15, Top + Height / 5);
    MainCanvas.lineTo(Left + 5, Top + Height / 2);
    MainCanvas.lineTo(Left + 15, Top + Height - Height / 5);
    MainCanvas.stroke();
    MainCanvas.closePath();

    // Draw the next arrow
    MainCanvas.beginPath();
    MainCanvas.fillStyle = "black";
    MainCanvas.moveTo(Left + Width - 15, Top + Height / 5);
    MainCanvas.lineTo(Left + Width - 5, Top + Height / 2);
    MainCanvas.lineTo(Left + Width - 15, Top + Height - Height / 5);
    MainCanvas.stroke();
    MainCanvas.closePath();

    if (CommonIsMobile) return;

    if (BackText == null) BackText = () => "MISSING VALUE FOR: BACK TEXT";
    if (NextText == null) NextText = () => "MISSING VALUE FOR: NEXT TEXT";
    if (ThreeWay && typeof ThreeWay !== 'function') ThreeWay = () => "MISSING VALUE FOR: THREEWAY TEXT";

    // Draw the hovering text
    if ((MouseX >= Left) && (MouseX <= Left + Width) && (MouseY >= Top) && (MouseY <= Top + Height)) {
      Left = (MouseX > 1000) ? Left - 475 : Left + 115;
      Top = Top + (Height - 65) / 2;
      MainCanvas.beginPath();
      MainCanvas.rect(Left, Top, 450, 65);
      MainCanvas.fillStyle = "#FFFF88";
      MainCanvas.fillRect(Left, Top, 450, 65);
      MainCanvas.fill();
      MainCanvas.lineWidth = '2';
      MainCanvas.strokeStyle = 'black';
      MainCanvas.stroke();
      MainCanvas.closePath();
      if (ThreeWay && (MouseX > Split && MouseX <= Split + SplitWidth)) DrawTextFit(ThreeWay(), Left + 225, Top + 33, 444, "black");
      else DrawTextFit((MouseX > Split) ? NextText() : BackText(), Left + 225, Top + 33, 444, "black");
    }
  }

  MainHallRun = function MainHallRun() {

    // If the player is dressed up while being a club slave, the maid intercepts her
    if ((CurrentCharacter == null) && ManagementIsClubSlave() && LogQuery("BlockChange", "Rule") && !Player.IsNaked() && (MainHallMaid.Dialog != null) && (MainHallMaid.Dialog.length > 0)) {
      MainHallMaid.Stage = "50";
      MainHallMaid.CurrentDialog = DialogFind(MainHallMaid, "ClubSlaveMustBeNaked");
      CharacterRelease(MainHallMaid);
      CharacterSetCurrent(MainHallMaid);
      MainHallStartEventTimer = null;
      MainHallNextEventTimer = null;
      return;
    }

    // If the player is a Mistress but her Dominant reputation has fallen
    if ((CurrentCharacter == null) && LogQuery("ClubMistress", "Management") && (ReputationGet("Dominant") < 50) && Player.CanTalk() && (MainHallMaid.Dialog != null) && (MainHallMaid.Dialog.length > 0)) {
      CommonSetScreen("Room", "Management");
      CharacterSetCurrent(MainHallMaid);
      CurrentScreen = "MainHall";
      MainHallMaid.Stage = "60";
      MainHallMaid.CurrentDialog = DialogFind(MainHallMaid, "MistressExpulsionIntro");
      return;
    }

    // Draws the character and main hall buttons
    DrawCharacter(Player, 750, 0, 1);

    // Char, Dressing, Exit & Chat
    DrawButton(1645, 25, 90, 90, "", "White", "Icons/Character.png", TextGet("Profile"));
    if (Player.CanChange()) DrawButton(1765, 25, 90, 90, "", "White", "Icons/Dress.png", TextGet("Appearance"));
    DrawButton(1885, 25, 90, 90, "", "White", "Icons/Exit.png", TextGet("Exit"));
    DrawButton(1645, 145, 90, 90, "", "White", "Icons/Chat.png", TextGet("ChatRooms"));

    // The options below are only available if the player can move
    if (Player.CanWalk()) {

      // Shop & Private Room
      DrawButton(1765, 145, 90, 90, "", "White", "Icons/Shop.png", TextGet("Shop"));
      if (!LogQuery("LockOutOfPrivateRoom", "Rule")) DrawButton(1885, 145, 90, 90, "", "White", "Icons/Private.png", TextGet("PrivateRoom"));

      // Introduction, Maid & Management
      DrawButton(1645, 265, 90, 90, "", "White", "Icons/Introduction.png", TextGet("IntroductionClass"));
      DrawButton(1765, 265, 90, 90, "", "White", "Icons/Maid.png", TextGet("MaidQuarters"));
      DrawButton(1885, 265, 90, 90, "", "White", "Icons/Management.png", TextGet("ClubManagement"));

      // Kidnap League, Dojo, Explore/Sarah
      DrawButton(1645, 385, 90, 90, "", "White", "Icons/Kidnap.png", TextGet("KidnapLeague"));
      DrawButton(1765, 385, 90, 90, "", "White", "Icons/Dojo.png", TextGet("ShibariDojo"));
      if (SarahRoomAvailable) DrawButton(1885, 385, 90, 90, "", "White", "Icons/Explore.png", TextGet(SarahRoomLabel()));

      // Cell, Slave Market & Look for trouble
      DrawButton(1645, 505, 90, 90, "", "White", "Icons/Question.png", TextGet("LookForTrouble"));
      DrawButton(1765, 505, 90, 90, "", "White", "Icons/Gavel.png", TextGet("SlaveMarket"));
      DrawButton(1885, 505, 90, 90, "", "White", "Icons/Cell.png", TextGet("Cell"));

      // Asylum & College
      if (!ManagementIsClubSlave()) DrawButton(1765, 625, 90, 90, "", "White", "Icons/College.png", TextGet("College"));
      DrawButton(1885, 625, 90, 90, "", "White", "Icons/Asylum.png", TextGet("Asylum"));

      // Draws the custom content rooms - Gambling, Prison & Photographic
      DrawButton(265, 25, 90, 90, "", "White", "Icons/Camera.png", TextGet("Photographic"));
      DrawButton(145, 25, 90, 90, "", "White", "Icons/Cage.png", TextGet("Prison"));
      DrawButton(25, 25, 90, 90, "", "White", "Icons/Random.png", TextGet("Gambling"));

      // Stable, Magic-Theater & Nursery
      DrawButton(265, 145, 90, 90, "", "White", "Icons/Diaper.png", TextGet("Nursery"));
      DrawButton(145, 145, 90, 90, "", "White", "Icons/Magic.png", TextGet("Magic"));
      DrawButton(25, 145, 90, 90, "", "White", "Icons/Horse.png", TextGet("Stable"));

      // Cafe
      DrawButton(25, 265, 90, 90, "", "White", "Icons/Refreshsments.png", TextGet("Cafe"));
    }

    // Check if there's a new maid rescue event to trigger
    if ((!Player.CanInteract() || !Player.CanWalk() || !Player.CanTalk())) {
      if (MainHallNextEventTimer == null) {
        MainHallStartEventTimer = CommonTime();
        MainHallNextEventTimer = CommonTime() + 40000 + Math.floor(Math.random() * 40000);
      }
    }
    else {
      MainHallStartEventTimer = null;
      MainHallNextEventTimer = null;
    }

    // If we must send a maid to rescue the player
    if ((MainHallNextEventTimer != null) && (CommonTime() >= MainHallNextEventTimer)) {
      MainHallMaid.Stage = "0";
      CharacterRelease(MainHallMaid);
      CharacterSetCurrent(MainHallMaid);
      MainHallStartEventTimer = null;
      MainHallNextEventTimer = null;
    }

    // If we must show a progress bar for the rescue maid.  If not, we show the number of online players
    if ((!Player.CanInteract() || !Player.CanWalk() || !Player.CanTalk()) && (MainHallStartEventTimer != null) && (MainHallNextEventTimer != null)) {
      DrawText(TextGet("RescueIsComing"), 1750, 925, "White", "Black");
      DrawProgressBar(1525, 955, 450, 35, (1 - ((MainHallNextEventTimer - CommonTime()) / (MainHallNextEventTimer - MainHallStartEventTimer))) * 100);
    }
    else DrawText(TextGet("OnlinePlayers") + " " + CurrentOnlinePlayers.toString(), 1750, 960, "White", "Black");

  }

  let WardrobeSize = 12;

  let AppearanceAssets;
  let AppearanceUndo = [];
  let AppearanceWardrobeOffset = 0;
  let AppearanceMode = "";
  let AppearanceHeight = 65;
  let AppearanceSpace = 30;
  let AppearanceItem = null;
  let AppearanceColorUndo = true;
  let AppearanceItemUndo = true;
  let AppearanceItemsOffset = 0;
  let AppearanceTempWardrobe = [];
  let AppearanceWardrobeShouldUndo = true;

  CharacterAppearanceWardrobeLoad = function CharacterAppearanceWardrobeLoad(C) {
    if (Player.Wardrobe == null || Player.Wardrobe.length < WardrobeSize) {
      WardrobeLoadCharacters(true);
    }
    else {
      WardrobeLoadCharacterNames();
    }
    ElementCreateInput("InputWardrobeName", "text", C.Name, "20");
    AppearanceMode = "Wardrobe";
    AppearanceWardrobeShouldUndo = true;
    CharacterAppearanceWardrobeText = window.TextGet("WardrobeNameInfo");
  }

  AppearanceLoad = function AppearanceLoad() {
    if (!CharacterAppearanceSelection) CharacterAppearanceSelection = Player;

    const C = CharacterAppearanceSelection;
    AppearanceUndo = [];
    AppearanceTempWardrobe = [];

    AppearanceBuildAssets(C);
    AppearanceMode = "";
    CharacterAppearanceBackup = C.Appearance.map(Item => Object.assign({}, Item));
  }

  // Run the character appearance selection screen
  AppearanceRun = function AppearanceRun() {
    const C = CharacterAppearanceSelection;

    // Draw the background and the character twice
    if (CharacterAppearanceHeaderText == "") {
      if (C.ID == 0) CharacterAppearanceHeaderText = TextGet("SelectYourAppearance");
      else CharacterAppearanceHeaderText = TextGet("SelectSomeoneAppearance").replace("TargetCharacterName", C.Name);
    }
    DrawCharacter(C, -600, (C.IsKneeling()) ? -1100 : -100, 4, false);
    DrawCharacter(C, 750, 0, 1);
    DrawText(CharacterAppearanceHeaderText, 400, 40, "White", "Black");

    if (AppearanceMode == "") {
      AppearanceNormalRun();
    }
    else if (AppearanceMode == "Wardrobe") {
      AppearanceWardrobeRun();
    }
    else if (AppearanceMode == "Color") {
      AppearanceColorRun();
    }
    else if (AppearanceMode == "Items") {
      AppearanceItemsRun();
    }

    // Draw the default buttons
    DrawButton(1768, 25, 90, 90, "", "White", "Icons/Cancel.png", TextGet("Cancel"));
    DrawButton(1885, 25, 90, 90, "", "White", "Icons/Accept.png", TextGet("Accept"));
  }

  // When the user clicks on the character appearance selection screen
  AppearanceClick = function AppearanceClick() {
    if (AppearanceMode == "") {
      AppearanceNormalClick();
    }
    else if (AppearanceMode == "Wardrobe") {
      AppearanceWardrobeClick();
    }
    else if (AppearanceMode == "Color") {
      AppearanceColorClick();
    }
    else if (AppearanceMode == "Items") {
      AppearanceItemsClick();
    }
  }

  // when the user press escape
  AppearanceExit = function AppearanceExit() {
    if (AppearanceMode == "") CharacterAppearanceExit(CharacterAppearanceSelection);
    else if (AppearanceMode == "Wardrobe") {
      AppearanceMode == "";
      ElementRemove("InputWardrobeName");
      AppearanceAssets.forEach(A => A.ReloadItem());
    }
    else if (AppearanceMode == "Color") {
      AppearanceMode == "";
      ElementRemove("InputColor");
      if (AppearanceColorUndo) AppearanceRunUndo();
      AppearanceItem = null;
    }
    else if (AppearanceMode == "Items") {
      if (AppearanceItemUndo) AppearanceRunUndo();
      AppearanceItem = null;
    }
  }

  class AppearanceAssetGroup {
    constructor(C, Group) {
      const m = new Map();

      const AddAsset = A => {
        m.set(A.Name, A);
      };

      Asset
        .filter(A => A.Value == 0)
        .filter(A => A.Group.Name == Group.Name)
        .forEach(AddAsset);

      Player.Inventory
        .map(I => I.Asset)
        .filter(A => A)
        .filter(A => A.Group.Name == Group.Name)
        .forEach(AddAsset);

      C.Inventory
        .map(I => I.Asset)
        .filter(A => A)
        .filter(A => A.Group.Name == Group.Name)
        .forEach(AddAsset);

      this.C = C;
      this.Group = Group;
      this.Assets = [];
      this.Item = this.C.Appearance.find(I => I.Asset.Group.Name == this.Group.Name);
      this.Color = (this.Item && this.Item.Color) || "None";

      this.CanChange = this.C.ID == 0 || this.Group.Clothing;

      m.forEach(A => this.Assets.push(A));
    }

    NextColor() {
      if (this.Item) {
        let I = this.Group.ColorSchema.indexOf(this.Color) + 1;
        if (I < 0 || I >= this.Group.ColorSchema.length) I = 0;
        this.SetColor(this.Group.ColorSchema[I], true);
      }
    }
    SetColor(Color, Undo) {
      if (this.Item) {
        if (Undo) {
          const G = this;
          const OldColor = this.Color;
          AppearanceUndo.push(() => G.SetColor(OldColor));
        }
        this.Item.Color = Color;
        this.Color = Color;
        CharacterLoadCanvas(this.C);
        this.ReloadItem();
      }
    }
    SetItem(AssetName, Undo) {
      if (Undo) {
        const G = this;
        const AssetName = this.Item && this.Item.Asset.Name;
        AppearanceUndo.push(() => G.SetItem(AssetName));
      }
      if (AssetName) InventoryWear(this.C, AssetName, this.Group.Name, this.Color == "None" ? null : this.Color);
      else InventoryRemove(this.C, this.Group.Name);
      AppearanceAssets.forEach(A => A.ReloadItem());
    }
    GetNextItem() {
      if (this.Item) {
        const I = this.Assets.findIndex(A => A.Group.Name == this.Group.Name && A.Name == this.Item.Asset.Name) + 1;
        if (I >= this.Assets.length) {
          return this.Group.AllowNone ? null : this.Assets[0];
        }
        else {
          return this.Assets[I];
        }
      }
      else {
        return this.Assets[0];
      }
    }
    GetPrevItem() {
      if (this.Item) {
        const I = this.Assets.findIndex(A => A.Group.Name == this.Group.Name && A.Name == this.Item.Asset.Name) - 1;
        if (I < 0) {
          return this.Group.AllowNone ? null : this.Assets[this.Assets.length - 1];
        }
        else {
          return this.Assets[I];
        }
      }
      else {
        return this.Assets[this.Assets.length - 1];
      }
    }
    SetNextPrevItem(Prev) {
      let Item;
      if (Prev) {
        Item = this.GetPrevItem();
      }
      else {
        Item = this.GetNextItem();
      }
      this.SetItem(Item && Item.Name, true);
    }
    Strip(Undo) {
      if (this.CanStrip()) {
        this.SetItem(null, Undo == null || Undo);
      }
    }
    CanStrip() {
      return this.CanChange && this.Item && this.Group.AllowNone; // && !this.Group.KeepNaked
    }
    ReloadItem() {
      this.Item = this.C.Appearance.find(I => I.Asset.Group.Name == this.Group.Name);
      this.Color = (this.Item && this.Item.Color) || this.Color || "None";
    }
  }

  function AppearanceBuildAssets(C) {
    AppearanceAssets = [];

    AssetGroup
      .filter(G => G.Family == C.AssetFamily)
      .filter(G => G.Category == "Appearance")
      .filter(G => G.AllowCustomize)
      .forEach(G => AppearanceAssets.push(new AppearanceAssetGroup(C, G)));
  }

  function AppearanceRunUndo(NoPop) {
    if (AppearanceUndo.length > 0) {
      if (NoPop) AppearanceUndo[AppearanceUndo.length - 1]();
      else AppearanceUndo.pop()();
    }
  }

  function AppearanceNormalRun() {
    const C = CharacterAppearanceSelection;

    // Draw the top buttons with images
    if (AppearanceUndo.length > 0) DrawButton(1183, 25, 90, 90, "", "White", "Icons/Magic.png", "Undo");
    if (C.ID == 0) {
      DrawButton(1300, 25, 90, 90, "", "White", "Icons/" + ((LogQuery("Wardrobe", "PrivateRoom")) ? "Wardrobe" : "Reset") + ".png", TextGet(LogQuery("Wardrobe", "PrivateRoom") ? "Wardrobe" : "ResetClothes"));
      DrawButton(1417, 25, 90, 90, "", "White", "Icons/Random.png", TextGet("Random"));
    }
    else if (LogQuery("Wardrobe", "PrivateRoom")) DrawButton(1417, 25, 90, 90, "", "White", "Icons/Wardrobe.png", TextGet("Wardrobe"));
    DrawButton(1534, 25, 90, 90, "", "White", "Icons/Naked.png", TextGet("Naked"));
    DrawButton(1651, 25, 90, 90, "", "White", "Icons/Next.png", TextGet("Next"));

    let offset = AppearanceHeight + AppearanceSpace;

    CharacterAppearanceNumPerPage = parseInt(900 / offset);

    for (let A = CharacterAppearanceOffset; A < AppearanceAssets.length && A < CharacterAppearanceOffset + CharacterAppearanceNumPerPage; A++) {
      if (AppearanceAssets[A].CanStrip()) {
        DrawButton(1210, 145 + (A - CharacterAppearanceOffset) * offset, 65, AppearanceHeight, "", "White", "Icons/Small/Naked.png", TextGet("StripItem"));
      }
      if (AppearanceAssets[A].CanChange)
        DrawBackNextButton(1300, 145 + (A - CharacterAppearanceOffset) * offset, 400, AppearanceHeight, AppearanceAssets[A].Group.Description + ": " + CharacterAppearanceGetCurrentValue(C, AppearanceAssets[A].Group.Name, "Description"), "White", null,
          () => {
            const Item = AppearanceAssets[A].GetPrevItem();
            return Item ? Item.Description : "None"
          },
          () => {
            const Item = AppearanceAssets[A].GetNextItem();
            return Item ? Item.Description : "None"
          },
          () => "Show All Items In Group"
        );
      else DrawButton(1300, 145 + (A - CharacterAppearanceOffset) * offset, 400, AppearanceHeight, AppearanceAssets[A].Group.Description + ": " + CharacterAppearanceGetCurrentValue(C, AppearanceAssets[A].Group.Name, "Description"), "#AAAAAA");
      const Color = AppearanceAssets[A].Color;
      DrawButton(1725, 145 + (A - CharacterAppearanceOffset) * offset, 160, AppearanceHeight, Color, ((Color.indexOf("#") == 0) ? Color : "White"));
      if (AppearanceAssets[A].Item != null && AppearanceAssets[A].CanChange) DrawButton(1910, 145 + (A - CharacterAppearanceOffset) * offset, 65, AppearanceHeight, "", "White", AppearanceAssets[A].Group.AllowColorize ? "Icons/Color.png" : "Icons/ColorBlocked.png");
    }
  }

  function AppearanceNormalClick() {
    const C = CharacterAppearanceSelection;

    let offset = AppearanceHeight + AppearanceSpace;

    // If we must remove/restore to default the item
    if ((MouseX >= 1210) && (MouseX < 1275) && (MouseY >= 145) && (MouseY < 975))
      for (let A = CharacterAppearanceOffset; A < AppearanceAssets.length && A < CharacterAppearanceOffset + CharacterAppearanceNumPerPage; A++)
        if (AppearanceAssets[A].CanStrip())
          if ((MouseY >= 145 + (A - CharacterAppearanceOffset) * offset) && (MouseY <= 145 + AppearanceHeight + (A - CharacterAppearanceOffset) * offset))
            AppearanceAssets[A].Strip();

    // If we must switch to the next item in the assets
    if ((MouseX >= 1300) && (MouseX < 1700) && (MouseY >= 145) && (MouseY < 975))
      for (let A = CharacterAppearanceOffset; A < AppearanceAssets.length && A < CharacterAppearanceOffset + CharacterAppearanceNumPerPage; A++)
        if ((MouseY >= 145 + (A - CharacterAppearanceOffset) * offset) && (MouseY <= 145 + AppearanceHeight + (A - CharacterAppearanceOffset) * offset)) {
          if (AppearanceAssets[A].CanChange) {
            if (!CommonIsMobile && MouseX < 1300 + (400 / 3)) {
              AppearanceAssets[A].SetNextPrevItem(true);
            }
            else if (!CommonIsMobile && MouseX >= 1300 + (400 / 3) * 2) {
              AppearanceAssets[A].SetNextPrevItem(false);
            }
            else {
              AppearanceItem = AppearanceAssets[A];
              AppearanceItem.SetItem(AppearanceItem.Item && AppearanceItem.Item.Asset.Name, true);
              AppearanceItemsOffset = 0;
              AppearanceItemUndo = true;
              AppearanceMode = "Items";
            }
          }
        }

    // If we must switch to the next color in the assets
    if ((MouseX >= 1725) && (MouseX < 1885) && (MouseY >= 145) && (MouseY < 975))
      for (let A = CharacterAppearanceOffset; A < AppearanceAssets.length && A < CharacterAppearanceOffset + CharacterAppearanceNumPerPage; A++)
        if ((MouseY >= 145 + (A - CharacterAppearanceOffset) * offset) && (MouseY <= 145 + AppearanceHeight + (A - CharacterAppearanceOffset) * offset) && AppearanceAssets[A].Item != null)
          if (AppearanceAssets[A].CanChange)
            AppearanceAssets[A].NextColor(C, AppearanceAssets[A].Name);

    // If we must open the color panel
    if ((MouseX >= 1910) && (MouseX < 1975) && (MouseY >= 145) && (MouseY < 975))
      for (let A = CharacterAppearanceOffset; A < AppearanceAssets.length && A < CharacterAppearanceOffset + CharacterAppearanceNumPerPage; A++)
        if (AppearanceAssets[A].Group.AllowColorize)
          if ((MouseY >= 145 + (A - CharacterAppearanceOffset) * offset) && (MouseY <= 145 + AppearanceHeight + (A - CharacterAppearanceOffset) * offset) && AppearanceAssets[A].Item != null) {
            if (AppearanceAssets[A].CanChange) {
              AppearanceItem = AppearanceAssets[A];
              AppearanceItem.SetColor(AppearanceItem.Color == "None" ? "Default" : AppearanceItem.Color, true);
              ElementCreateInput("InputColor", "text", ((AppearanceAssets[A].Color == "Default") || (AppearanceAssets[A].Color == "None")) ? "#" : AppearanceAssets[A].Color, "7");
              AppearanceColorUndo = true;
              AppearanceMode = "Color";
            }
          }

    // If we must set back the default outfit or set a random outfit
    if ((MouseX >= 1183) && (MouseX < 1273) && (MouseY >= 25) && (MouseY < 115)) AppearanceRunUndo();
    if ((MouseX >= 1300) && (MouseX < 1390) && (MouseY >= 25) && (MouseY < 115) && (C.ID == 0) && !LogQuery("Wardrobe", "PrivateRoom")) CharacterAppearanceSetDefault(C);
    if ((MouseX >= 1300) && (MouseX < 1390) && (MouseY >= 25) && (MouseY < 115) && (C.ID == 0) && LogQuery("Wardrobe", "PrivateRoom")) CharacterAppearanceWardrobeLoad(C);
    if ((MouseX >= 1417) && (MouseX < 1507) && (MouseY >= 25) && (MouseY < 115) && (C.ID == 0)) CharacterAppearanceFullRandom(C);
    if ((MouseX >= 1417) && (MouseX < 1507) && (MouseY >= 25) && (MouseY < 115) && (C.ID != 0) && LogQuery("Wardrobe", "PrivateRoom")) CharacterAppearanceWardrobeLoad(C);
    if ((MouseX >= 1534) && (MouseX < 1624) && (MouseY >= 25) && (MouseY < 115)) {
      AppearanceAssets.filter(A => A.CanStrip() && !A.Group.KeepNaked).forEach(A => A.Strip());
    }
    if ((MouseX >= 1651) && (MouseX < 1741) && (MouseY >= 25) && (MouseY < 115)) CharacterAppearanceMoveOffset(CharacterAppearanceNumPerPage);
    if ((MouseX >= 1768) && (MouseX < 1858) && (MouseY >= 25) && (MouseY < 115)) CharacterAppearanceExit(C);
    if ((MouseX >= 1885) && (MouseX < 1975) && (MouseY >= 25) && (MouseY < 115)) CharacterAppearanceReady(C);
  }

  function AppearanceItemsRun() {
    if (AppearanceItem == null) {
      AppearanceMode = "";
      return;
    }

    let offset = AppearanceHeight + AppearanceSpace;
    CharacterAppearanceNumPerPage = parseInt(900 / offset) * 2;

    if (AppearanceItem.Assets.length > CharacterAppearanceNumPerPage) DrawButton(1534, 25, 90, 90, "", "White", "Icons/Next.png", TextGet("Next"));
    if (AppearanceItem.CanStrip()) DrawButton(1651, 25, 90, 90, "", "White", "Icons/Naked.png", TextGet("StripItem"));

    // Creates buttons for all groups
    for (let A = AppearanceItemsOffset; A * 2 < AppearanceItem.Assets.length && A * 2 < AppearanceItemsOffset * 2 + CharacterAppearanceNumPerPage; A++) {
      const ItemName = AppearanceItem.Item && AppearanceItem.Item.Asset && AppearanceItem.Item.Asset.Name;
      DrawButton(1250, 145 + (A - AppearanceItemsOffset) * offset, 350, AppearanceHeight, AppearanceItem.Assets[A * 2].Description, AppearanceItem.Assets[A * 2].Name == ItemName ? "Pink" : "White");
      if (A * 2 + 1 >= AppearanceItem.Assets.length || A * 2 + 1 >= AppearanceItemsOffset * 2 + CharacterAppearanceNumPerPage) break;
      DrawButton(1630, 145 + (A - AppearanceItemsOffset) * offset, 350, AppearanceHeight, AppearanceItem.Assets[A * 2 + 1].Description, AppearanceItem.Assets[A * 2 + 1].Name == ItemName ? "Pink" : "White");
    }
  }

  function AppearanceItemsClick() {
    let offset = AppearanceHeight + AppearanceSpace;

    if ((MouseX >= 1250) && (MouseX < 1600) && (MouseY >= 145) && (MouseY < 975))
      for (let A = AppearanceItemsOffset; A * 2 < AppearanceItem.Assets.length && A * 2 < AppearanceItemsOffset * 2 + CharacterAppearanceNumPerPage; A++) {
        if ((MouseY >= 145 + (A - AppearanceItemsOffset) * offset) && (MouseY <= 145 + AppearanceHeight + (A - AppearanceItemsOffset) * offset)) {
          AppearanceItem.SetItem(AppearanceItem.Assets[A * 2].Name);
        }
      }

    if ((MouseX >= 1630) && (MouseX < 1980) && (MouseY >= 145) && (MouseY < 975))
      for (let A = AppearanceItemsOffset; A * 2 + 1 < AppearanceItem.Assets.length && A * 2 + 1 < AppearanceItemsOffset * 2 + CharacterAppearanceNumPerPage; A++) {
        if ((MouseY >= 145 + (A - AppearanceItemsOffset) * offset) && (MouseY <= 145 + AppearanceHeight + (A - AppearanceItemsOffset) * offset)) {
          AppearanceItem.SetItem(AppearanceItem.Assets[A * 2 + 1].Name);
        }
      }

    if ((MouseX >= 1651) && (MouseX < 1741) && (MouseY >= 25) && (MouseY < 115) && AppearanceItem.CanStrip()) AppearanceItem.Strip(false);

    if ((MouseX >= 1534) && (MouseX < 1624) && (MouseY >= 25) && (MouseY < 115) && AppearanceItem.Assets.length > CharacterAppearanceNumPerPage) {
      AppearanceItemsOffset += CharacterAppearanceNumPerPage / 2;
      if (AppearanceItemsOffset * 2 + 1 >= AppearanceItem.Assets.length) AppearanceItemsOffset = 0;
    }

    if ((MouseX >= 1768) && (MouseX < 1858) && (MouseY >= 25) && (MouseY < 115)) {
      AppearanceExit();
    }

    if ((MouseX >= 1885) && (MouseX < 1975) && (MouseY >= 25) && (MouseY < 115)) {
      AppearanceItemUndo = false;
      AppearanceExit();
    }
  }

  function AppearanceColorRun() {
    if (document.getElementById("InputColor")) ElementPosition("InputColor", 1450, 65, 300);
    else AppearanceMode = "";
    DrawButton(1610, 37, 65, 65, "", "White", "Icons/Color.png");
    DrawImage("Backgrounds/ColorPicker.png", 1300, 145);
  }

  function AppearanceColorClick() {
    if (AppearanceItem == null) {
      AppearanceMode = "";
      return;
    }
    // Can set a color manually from the text field
    if ((MouseX >= 1610) && (MouseX < 1675) && (MouseY >= 37) && (MouseY < 102))
      if (CommonIsColor(ElementValue("InputColor")))
        AppearanceItem.SetColor(ElementValue("InputColor").toLowerCase());

    // In color picker mode, we can pick a color from the color image
    if ((MouseX >= 1300) && (MouseX < 1975) && (MouseY >= 145) && (MouseY < 975)) {
      const Color = DrawRGBToHex(MainCanvas.getImageData(MouseX, MouseY, 1, 1).data);
      AppearanceItem.SetColor(Color);
      ElementValue("InputColor", Color);
    }

    if ((MouseX >= 1768) && (MouseX < 1858) && (MouseY >= 25) && (MouseY < 115)) {
      AppearanceExit();
    }

    if ((MouseX >= 1885) && (MouseX < 1975) && (MouseY >= 25) && (MouseY < 115)) {
      AppearanceColorUndo = false;
      AppearanceExit();
    }

    if (AppearanceMode != "Color") ElementRemove("InputColor");
  }

  function AppearanceWardrobeRun() {
    const C = CharacterAppearanceSelection;
    // Draw the wardrobe top controls & buttons
    if (!AppearanceWardrobeShouldUndo) DrawButton(1300, 25, 90, 90, "", "White", "Icons/Magic.png", TextGet("Undo"));
    DrawButton(1417, 25, 90, 90, "", "White", "Icons/Dress.png", TextGet("DressManually"));
    DrawButton(1534, 25, 90, 90, "", "White", "Icons/Naked.png", TextGet("Naked"));
    DrawButton(1651, 25, 90, 90, "", "White", "Icons/Next.png", TextGet("Next"));
    DrawText(CharacterAppearanceWardrobeText, 1645, 220, "White", "Gray");
    if (document.getElementById("InputWardrobeName")) ElementPosition("InputWardrobeName", 1645, 315, 690);
    else AppearanceMode = "";

    // Draw 6 wardrobe options
    for (let W = AppearanceWardrobeOffset; W < Player.Wardrobe.length && W < AppearanceWardrobeOffset + 6; W++) {
      DrawButton(1300, 430 + (W - AppearanceWardrobeOffset) * 95, 500, 65, "", "White", "");
      DrawTextFit((W + 1).toString() + (W < 9 ? ":  " : ": ") + Player.WardrobeCharacterNames[W], 1550, 463 + (W - AppearanceWardrobeOffset) * 95, 496, "Black");
      DrawButton(1820, 430 + (W - AppearanceWardrobeOffset) * 95, 160, 65, "Save", "White", "");
    }
  }

  function AppearanceWardrobeClick() {
    const C = CharacterAppearanceSelection;
    if ((MouseX >= 1651) && (MouseX < 1741) && (MouseY >= 25) && (MouseY < 115)) {
      AppearanceWardrobeOffset += 6;
      if (AppearanceWardrobeOffset >= Player.Wardrobe.length) AppearanceWardrobeOffset = 0;
    }
    if ((MouseX >= 1300) && (MouseX < 1800) && (MouseY >= 430) && (MouseY < 970))
      for (let W = AppearanceWardrobeOffset; W < Player.Wardrobe.length && W < AppearanceWardrobeOffset + 6; W++)
        if ((MouseY >= 430 + (W - AppearanceWardrobeOffset) * 95) && (MouseY <= 495 + (W - AppearanceWardrobeOffset) * 95)) {
          if (AppearanceWardrobeShouldUndo) {
            const Undo = AppearanceTempWardrobe[AppearanceTempWardrobe.length] = WardrobeSaveData(C);
            AppearanceUndo.push(() => {
              WardrobeLoadData(C, Undo);
              AppearanceTempWardrobe.pop();
            });
            AppearanceWardrobeShouldUndo = false;
          }
          WardrobeFastLoad(C, W, false);
        }

    if ((MouseX >= 1820) && (MouseX < 1975) && (MouseY >= 430) && (MouseY < 970))
      for (let W = AppearanceWardrobeOffset; W < Player.Wardrobe.length && W < AppearanceWardrobeOffset + 6; W++)
        if ((MouseY >= 430 + (W - AppearanceWardrobeOffset) * 95) && (MouseY <= 495 + (W - AppearanceWardrobeOffset) * 95)) {
          WardrobeFastSave(C, W);
          const LS = /^[a-zA-Z ]+$/;
          const Name = ElementValue("InputWardrobeName").trim();
          if (Name.match(LS) || Name.length == 0) {
            WardrobeSetCharacterName(W, Name);
            CharacterAppearanceWardrobeText = TextGet("WardrobeNameInfo");
          }
          else {
            CharacterAppearanceWardrobeText = TextGet("WardrobeNameError");
          }
        }

    if ((MouseX >= 1300) && (MouseX < 1390) && (MouseY >= 25) && (MouseY < 115) && !AppearanceWardrobeShouldUndo) {
      AppearanceRunUndo();
      AppearanceWardrobeShouldUndo = true;
    }
    if ((MouseX >= 1417) && (MouseX < 1507) && (MouseY >= 25) && (MouseY < 115)) {
      AppearanceMode = "";
      ElementRemove("InputWardrobeName");
    }
    if ((MouseX >= 1534) && (MouseX < 1624) && (MouseY >= 25) && (MouseY < 115)) CharacterAppearanceNaked(C);
    if ((MouseX >= 1768) && (MouseX < 1858) && (MouseY >= 25) && (MouseY < 115)) CharacterAppearanceExit(C);
    if ((MouseX >= 1885) && (MouseX < 1975) && (MouseY >= 25) && (MouseY < 115)) CharacterAppearanceReady(C);
  }

  let ChatCreateMode = "";
  let ChatCreateName = "";
  let ChatCreateDescription = "";
  let ChatCreateSize = "";
  let ChatCreateOffset = 0;

  // When the chat creation screens loads
  function ChatCreateLoad() {

    // If the current background isn't valid, we pick the first one
    if (ChatCreateBackgroundList.indexOf(ChatCreateBackgroundSelect) < 0) {
      ChatCreateBackgroundIndex = 0;
      ChatCreateBackgroundSelect = ChatCreateBackgroundList[0];
      ChatCreateBackground = ChatCreateBackgroundSelect + "Dark";
    }

    // Prepares the controls to create a room
    ElementRemove("InputSearch");
    ElementCreateInput("InputName", "text", "", "20");
    ElementCreateInput("InputDescription", "text", "", "100");
    ElementCreateInput("InputSize", "text", "10", "2");
    ChatCreateMessage = "";
    ChatCreatePrivate = false;

    ChatCreateMode = "";
    ChatCreateOffset = 0;
  }

  // When the chat creation screen runs
  ChatCreateRun = function ChatCreateRun() {

    if (ChatCreateMode == "") {
      // Draw the controls
      if (ChatCreateMessage == "") ChatCreateMessage = "EnterRoomInfo";
      DrawText(TextGet(ChatCreateMessage), 1000, 60, "White", "Gray");
      DrawText(TextGet("RoomName"), 1000, 150, "White", "Gray");
      ElementPosition("InputName", 1000, 200, 500);
      DrawText(TextGet("RoomDescription"), 1000, 300, "White", "Gray");
      ElementPosition("InputDescription", 1000, 350, 1500);
      DrawText(TextGet("RoomPrivate"), 970, 460, "White", "Gray");
      DrawButton(1300, 428, 64, 64, "", "White", ChatCreatePrivate ? "Icons/Checked.png" : "");
      DrawText(TextGet("RoomSize"), 930, 568, "White", "Gray");
      ElementPosition("InputSize", 1400, 560, 150);
      DrawText(TextGet("RoomBackground"), 650, 672, "White", "Gray");
      DrawButton(1300, 640, 300, 65, "Show All", "White");
      DrawBackNextButton(900, 640, 350, 65, TextGet(ChatCreateBackgroundSelect), "White", null,
        () => TextGet((ChatCreateBackgroundIndex == 0) ? ChatCreateBackgroundList[ChatCreateBackgroundList.length - 1] : ChatCreateBackgroundList[ChatCreateBackgroundIndex - 1]),
        () => TextGet((ChatCreateBackgroundIndex >= ChatCreateBackgroundList.length - 1) ? ChatCreateBackgroundList[0] : ChatCreateBackgroundList[ChatCreateBackgroundIndex + 1]));
      DrawButton(600, 800, 300, 65, TextGet("Create"), "White");
      DrawButton(1100, 800, 300, 65, TextGet("Cancel"), "White");
    }
    else if (ChatCreateMode == "ShowGrid") {
      DrawButton(1885, 25, 90, 90, "", "White", "Icons/Exit.png");
      DrawButton(1785, 25, 90, 90, "", "White", "Icons/Next.png");
      let X = 45;
      let Y = 170;
      for (var i = 0;
        (i + ChatCreateOffset) < ChatCreateBackgroundList.length && i < 12; ++i) {
        DrawButton(X, Y, 450, 225, "", "White", "Backgrounds/" + ChatCreateBackgroundList[i + ChatCreateOffset] + ".jpg");
        X += 450 + 35;
        if (i % 4 == 3) {
          X = 45;
          Y += 225 + 35;
        }
      }
    }
  }

  // When the player clicks in the chat creation screen
  ChatCreateClick = function ChatCreateClick() {

    if (ChatCreateMode == "") {
      // When the private box is checked
      if ((MouseX >= 1300) && (MouseX < 1364) && (MouseY >= 428) && (MouseY < 492)) ChatCreatePrivate = !ChatCreatePrivate;

      // When we select a new background
      if ((MouseX >= 900) && (MouseX < 1250) && (MouseY >= 640) && (MouseY < 705)) {
        ChatCreateBackgroundIndex += MouseX < 1075 ? -1 : 1;
        if (ChatCreateBackgroundIndex >= ChatCreateBackgroundList.length) ChatCreateBackgroundIndex = 0;
        if (ChatCreateBackgroundIndex < 0) ChatCreateBackgroundIndex = ChatCreateBackgroundList.length - 1;
        ChatCreateBackgroundSelect = ChatCreateBackgroundList[ChatCreateBackgroundIndex];
        ChatCreateBackground = ChatCreateBackgroundSelect + "Dark";
      }

      // Show backgrounds in grid
      if ((MouseX >= 1300) && (MouseX < 1600) && (MouseY >= 640) && (MouseY < 705)) {
        ChatCreateMode = "ShowGrid";
        ChatCreateName = ElementValue("InputName");
        ChatCreateDescription = ElementValue("InputDescription");
        ChatCreateSize = ElementValue("InputSize");
        ElementRemove("InputName");
        ElementRemove("InputDescription");
        ElementRemove("InputSize");
      }

      // If the user wants to create a room
      if ((MouseX >= 600) && (MouseX < 900) && (MouseY >= 800) && (MouseY < 865)) {
        ChatCreateRoom();
      }

      // When the user cancels
      if ((MouseX >= 1100) && (MouseX < 1400) && (MouseY >= 800) && (MouseY < 865)) {
        ChatCreateExit();
      }
    }
    else if (ChatCreateMode == "ShowGrid") {
      if ((MouseX >= 1885) && (MouseX < 1975) && (MouseY >= 25) && (MouseY < 115)) {
        ChatCreateExit();
      }

      if ((MouseX >= 1785) && (MouseX < 1875) && (MouseY >= 25) && (MouseY < 115)) {
        ChatCreateOffset += 12;
        if (ChatCreateOffset >= ChatCreateBackgroundList.length) ChatCreateOffset = 0;
      }

      let X = 45;
      let Y = 170;
      for (var i = 0;
        (i + ChatCreateOffset) < ChatCreateBackgroundList.length && i < 12; ++i) {
        if ((MouseX >= X) && (MouseX < X + 450) && (MouseY >= Y) && (MouseY < Y + 225)) {
          ChatCreateBackgroundIndex = i + ChatCreateOffset;
          if (ChatCreateBackgroundIndex >= ChatCreateBackgroundList.length) ChatCreateBackgroundIndex = 0;
          if (ChatCreateBackgroundIndex < 0) ChatCreateBackgroundIndex = ChatCreateBackgroundList.length - 1;
          ChatCreateBackgroundSelect = ChatCreateBackgroundList[ChatCreateBackgroundIndex];
          ChatCreateBackground = ChatCreateBackgroundSelect + "Dark";
        }
        X += 450 + 35;
        if (i % 4 == 3) {
          X = 45;
          Y += 225 + 35;
        }
      }
    }
  }

  // When the user press "enter", we create the room
  ChatCreateKeyDown = function ChatCreateKeyDown() {
    if (KeyPress == 13 && ChatCreateMode == "") ChatCreateRoom();
    if (KeyPress == 13 && ChatCreateMode == "ShowGrid") ChatCreateExit();
  }

  // When the user exit from this screen
  ChatCreateExit = function ChatCreateExit() {
    if (ChatCreateMode == "") {
      ElementRemove("InputName");
      ElementRemove("InputDescription");
      ElementRemove("InputSize");
      CommonSetScreen("Online", "ChatSearch");
    }
    else if (ChatCreateMode == "ShowGrid") {
      ChatCreateMode = "";
      ElementCreateInput("InputName", "text", ChatCreateName, "20");
      ElementCreateInput("InputDescription", "text", ChatCreateDescription, "100");
      ElementCreateInput("InputSize", "text", ChatCreateSize, "2");
    }
  }

  // When the server sends a response
  ChatCreateResponse = function ChatCreateResponse(data) {
    if ((data != null) && (typeof data === "string") && (data != ""))
      ChatCreateMessage = "Response" + data;
  }

  // Creates the chat room
  ChatCreateRoom = function ChatCreateRoom() {
    ChatRoomPlayerCanJoin = true;
    var NewRoom = {
      Name: ElementValue("InputName").trim(),
      Description: ElementValue("InputDescription").trim(),
      Background: ChatCreateBackgroundSelect,
      Private: ChatCreatePrivate,
      Space: ChatRoomSpace,
      Limit: ElementValue("InputSize").trim()
    };
    ServerSend("ChatRoomCreate", NewRoom);
    ChatCreateMessage = "CreatingRoom";
  }

  let ChatAdminMode = "";
  let ChatAdminOffset = 0;

  // When the chat admin screens loads
  ChatAdminLoad = function ChatAdminLoad(BackgroundIsSet) {

    if (BackgroundIsSet != true) {
      // If the current room background isn't valid, we pick the first one
      ChatAdminBackgroundSelect = ChatRoomData.Background;
      if (ChatCreateBackgroundList.indexOf(ChatAdminBackgroundSelect) < 0) {
        ChatAdminBackgroundIndex = 0;
        ChatAdminBackgroundSelect = ChatCreateBackgroundList[0];
      }
      else ChatAdminBackgroundIndex = ChatCreateBackgroundList.indexOf(ChatAdminBackgroundSelect);
    }
    // Prepares the controls to edit a room
    ElementCreateInput("InputName", "text", ChatRoomData.Name, "20");
    document.getElementById("InputName").setAttribute("autocomplete", "off");
    ElementCreateInput("InputSize", "text", ChatRoomData.Limit.toString(), "2");
    document.getElementById("InputSize").setAttribute("autocomplete", "off");
    ElementCreateTextArea("InputDescription");
    document.getElementById("InputDescription").setAttribute("maxLength", 100);
    document.getElementById("InputDescription").setAttribute("autocomplete", "off");
    ElementValue("InputDescription", ChatRoomData.Description);
    ElementCreateTextArea("InputAdminList");
    document.getElementById("InputAdminList").setAttribute("maxLength", 250);
    document.getElementById("InputAdminList").setAttribute("autocomplete", "off");
    ElementValue("InputAdminList", CommonConvertArrayToString(ChatRoomData.Admin));
    ElementCreateTextArea("InputBanList");
    document.getElementById("InputBanList").setAttribute("maxLength", 250);
    document.getElementById("InputBanList").setAttribute("autocomplete", "off");
    ElementValue("InputBanList", CommonConvertArrayToString(ChatRoomData.Ban));
    ChatAdminPrivate = ChatRoomData.Private;
    ChatAdminLocked = ChatRoomData.Locked;

    // If the player isn't an admin, we disable the inputs
    if (!ChatRoomPlayerIsAdmin()) {
      document.getElementById("InputName").setAttribute("disabled", "disabled");
      document.getElementById("InputDescription").setAttribute("disabled", "disabled");
      document.getElementById("InputAdminList").setAttribute("disabled", "disabled");
      document.getElementById("InputBanList").setAttribute("disabled", "disabled");
      document.getElementById("InputSize").setAttribute("disabled", "disabled");
      ChatAdminMessage = "AdminOnly";
    }
    else ChatAdminMessage = "UseMemberNumbers";

  }

  // When the chat Admin screen runs
  ChatAdminRun = function ChatAdminRun() {
    if (ChatAdminMode == "") {
      // Draw the main controls
      DrawText(TextGet(ChatAdminMessage), 650, 870, "Black", "Gray");
      DrawText(TextGet("RoomName"), 535, 110, "Black", "Gray");
      ElementPosition("InputName", 535, 170, 820);
      DrawText(TextGet("RoomSize"), 1100, 110, "Black", "Gray");
      ElementPosition("InputSize", 1100, 170, 250);
      DrawText(TextGet("RoomDescription"), 675, 265, "Black", "Gray");
      ElementPosition("InputDescription", 675, 380, 1100, 160);
      DrawText(TextGet("RoomAdminList"), 390, 530, "Black", "Gray");
      ElementPosition("InputAdminList", 390, 680, 530, 230);
      DrawText(TextGet("RoomBanList"), 960, 530, "Black", "Gray");
      ElementPosition("InputBanList", 960, 680, 530, 230);

      // Background selection
      DrawImageResize("Backgrounds/" + ChatAdminBackgroundSelect + "Dark.jpg", 1300, 75, 600, 400);
      DrawBackNextButton(1350, 500, 500, 65, ChatAdminBackgroundSelect, ChatRoomPlayerIsAdmin() ? "White" : "#ebebe4", null,
        () => (ChatAdminBackgroundIndex == 0) ? ChatCreateBackgroundList[ChatCreateBackgroundList.length - 1] : ChatCreateBackgroundList[ChatAdminBackgroundIndex - 1],
        () => (ChatAdminBackgroundIndex >= ChatCreateBackgroundList.length - 1) ? ChatCreateBackgroundList[0] : ChatCreateBackgroundList[ChatAdminBackgroundIndex + 1]);

      // Private and Locked check boxes
      DrawText(TextGet("RoomPrivate"), 1514, 640, "Black", "Gray");
      DrawButton(1686, 608, 64, 64, "", ChatRoomPlayerIsAdmin() ? "White" : "#ebebe4", ChatAdminPrivate ? "Icons/Checked.png" : "");
      DrawText(TextGet("RoomLocked"), 1514, 740, "Black", "Gray");
      DrawButton(1686, 708, 64, 64, "", ChatRoomPlayerIsAdmin() ? "White" : "#ebebe4", ChatAdminLocked ? "Icons/Checked.png" : "");

      // Save & Cancel/Exit buttons + help text
      DrawButton(1325, 840, 250, 65, TextGet("Save"), ChatRoomPlayerIsAdmin() ? "White" : "#ebebe4");
      DrawButton(1625, 840, 250, 65, TextGet(ChatRoomPlayerIsAdmin() ? "Cancel" : "Exit"), "White");

    }
    else if (ChatAdminMode == "ShowGrid") {
      DrawButton(1885, 25, 90, 90, "", "White", "Icons/Exit.png");
      DrawButton(1785, 25, 90, 90, "", "White", "Icons/Next.png");
      let X = 45;
      let Y = 170;
      for (var i = 0;
        (i + ChatAdminOffset) < ChatCreateBackgroundList.length && i < 12; ++i) {
        DrawButton(X, Y, 450, 225, "", "White", "Backgrounds/" + ChatCreateBackgroundList[i + ChatAdminOffset] + ".jpg");
        X += 450 + 35;
        if (i % 4 == 3) {
          X = 45;
          Y += 225 + 35;
        }
      }
    }

  }

  // When the player clicks in the chat Admin screen
  ChatAdminClick = function ChatAdminClick() {

    // When the user cancels/exits
    if ((MouseX >= 1625) && (MouseX < 1875) && (MouseY >= 840) && (MouseY < 905)) ChatAdminExit();

    // All other controls are for administrators only
    if (ChatRoomPlayerIsAdmin()) {

      if (ChatAdminMode == "") {
        // When we select a new background
        if ((MouseX >= 1350) && (MouseX <= 1850) && (MouseY >= 500) && (MouseY <= 565)) {
          ChatAdminBackgroundIndex += ((MouseX < 1600) ? -1 : 1);
          if (ChatAdminBackgroundIndex >= ChatCreateBackgroundList.length) ChatAdminBackgroundIndex = 0;
          if (ChatAdminBackgroundIndex < 0) ChatAdminBackgroundIndex = ChatCreateBackgroundList.length - 1;
          ChatAdminBackgroundSelect = ChatCreateBackgroundList[ChatAdminBackgroundIndex];
        }

        // Private & Locked check boxes + save button
        if ((MouseX >= 1686) && (MouseX <= 1750) && (MouseY >= 608) && (MouseY <= 672)) ChatAdminPrivate = !ChatAdminPrivate;
        if ((MouseX >= 1686) && (MouseX <= 1750) && (MouseY >= 708) && (MouseY <= 772)) ChatAdminLocked = !ChatAdminLocked;
        if ((MouseX >= 1325) && (MouseX < 1575) && (MouseY >= 840) && (MouseY < 905) && ChatRoomPlayerIsAdmin()) ChatAdminUpdateRoom();

        if ((MouseX >= 1300) && (MouseX <= 1300 + 600) && (MouseY >= 75) && (MouseY <= 75 + 400)) {
          ChatAdminMode = "ShowGrid";
          ElementRemove("InputName");
          ElementRemove("InputDescription");
          ElementRemove("InputSize");
          ElementRemove("InputAdminList");
          ElementRemove("InputBanList");
        }

      }
      else if (ChatAdminMode == "ShowGrid") {
        if ((MouseX >= 1885) && (MouseX < 1975) && (MouseY >= 25) && (MouseY < 115)) {
          ChatAdminMode = "";
          ChatAdminLoad(true);
        }

        if ((MouseX >= 1785) && (MouseX < 1875) && (MouseY >= 25) && (MouseY < 115)) {
          ChatAdminOffset += 12;
          if (ChatAdminOffset >= ChatCreateBackgroundList.length) ChatAdminOffset = 0;
        }

        let X = 45;
        let Y = 170;
        for (var i = 0;
          (i + ChatAdminOffset) < ChatCreateBackgroundList.length && i < 12; ++i) {
          if ((MouseX >= X) && (MouseX < X + 450) && (MouseY >= Y) && (MouseY < Y + 225)) {
            ChatAdminBackgroundIndex = i + ChatAdminOffset;
            if (ChatAdminBackgroundIndex >= ChatCreateBackgroundList.length) ChatAdminBackgroundIndex = 0;
            if (ChatAdminBackgroundIndex < 0) ChatAdminBackgroundIndex = ChatCreateBackgroundList.length - 1;
            ChatAdminBackgroundSelect = ChatCreateBackgroundList[ChatAdminBackgroundIndex];
            ChatAdminMode = "";
            ChatAdminLoad(true);
          }
          X += 450 + 35;
          if (i % 4 == 3) {
            X = 45;
            Y += 225 + 35;
          }
        }
      }

    }
  }

  // When the user exit from this screen
  ChatAdminExit = function ChatAdminExit() {
    ElementRemove("InputName");
    ElementRemove("InputDescription");
    ElementRemove("InputSize");
    ElementRemove("InputAdminList");
    ElementRemove("InputBanList");
    CommonSetScreen("Online", "ChatRoom");
  }

  // When the server sends a response, if it was updated properly we exit, if not we show the error
  ChatAdminResponse = function ChatAdminResponse(data) {
    if ((data != null) && (typeof data === "string") && (data != ""))
      if (data === "Updated") ChatAdminExit();
      else ChatAdminMessage = "Response" + data;
  }

  // Sends the chat room update packet to the server and waits for the answer
  ChatAdminUpdateRoom = function ChatAdminUpdateRoom() {
    var UpdatedRoom = {
      Name: ElementValue("InputName").trim(),
      Description: ElementValue("InputDescription").trim(),
      Background: ChatAdminBackgroundSelect,
      Limit: ElementValue("InputSize").trim(),
      Admin: ChatAdminToNumbers(ElementValue("InputAdminList").trim()),
      Ban: ChatAdminToNumbers(ElementValue("InputBanList").trim()).filter(n => n != Player.MemberNumber && n != 65),
      Private: ChatAdminPrivate,
      Locked: ChatAdminLocked
    };
    if (!UpdatedRoom.Admin.includes(Player.MemberNumber)) UpdatedRoom.Admin.push(Player.MemberNumber);
    ServerSend("ChatRoomAdmin", {
      MemberNumber: Player.ID,
      Room: UpdatedRoom,
      Action: "Update"
    });
    ChatAdminMessage = "UpdatingRoom";
  }

  function ChatAdminToNumbers(str) {
    str = str.trim();
    return (str == "") ? [] : [...new Set(str.split(",").map(Number).filter(isFinite).filter(n => n >= 0))];
  }

  ServerAccountBeep = function ServerAccountBeep(data) {
    if ((data != null) && (typeof data === "object") && !Array.isArray(data) && (data.MemberNumber != null) && (typeof data.MemberNumber === "number") && (data.MemberName != null) && (typeof data.MemberName === "string")) {

      const AllowLesh = Player.AllowLeash == true && Player.OwnerNumber != null;

      if (AllowLesh && Player.SendBeepToOwner && Player.CurrentChatRoom != null && data.MemberNumber != Player.OwnerNumber && ChatRoomData.Character.some(c => c.MemberNumber == Player.OwnerNumber)) {
        let msg = DialogFind(Player, "BeepFrom") + " " + data.MemberName + " (" + data.MemberNumber.toString() + ")";
        if (data.ChatRoomName != null) msg = msg + " " + DialogFind(Player, "InRoom") + " \"" + data.ChatRoomName + "\"";
        ServerSend("ChatRoomChat", {
          Content: msg,
          Type: "Whisper",
          Target: Player.OwnerNumber
        });
        return;
      }

      if (AllowLesh && ServerBeep.Timer >= CurrentTime && data.MemberNumber != Player.OwnerNumber) return;

      ServerBeep.MemberNumber = data.MemberNumber;
      ServerBeep.MemberName = data.MemberName;
      ServerBeep.ChatRoomName = data.ChatRoomName;

      if (AllowLesh && ServerBeep.MemberNumber == Player.OwnerNumber) {
        if (ServerBeep.Timer >= CurrentTime) {
          if (Player.CurrentChatRoom != null && ServerBeep.ChatRoomName != null && Player.CurrentChatRoom != ServerBeep.ChatRoomName) {
            ChatRoomLeave();
          }
          else if (Player.CurrentChatRoom == null && ServerBeep.ChatRoomName != null) {
            ChatRoomJoin(ServerBeep.ChatRoomName);
          }
          else {
            ServerBeep.Timer = CurrentTime;
            return;
          }
        }
        ServerBeep.Timer = CurrentTime + 30000;
      }
      else {
        ServerBeep.Timer = CurrentTime + 10000;
      }

      ServerBeep.Message = DialogFind(Player, "BeepFrom") + " " + ServerBeep.MemberName + " (" + ServerBeep.MemberNumber.toString() + ")";
      if (ServerBeep.ChatRoomName != null)
        ServerBeep.Message = ServerBeep.Message + " " + DialogFind(Player, "InRoom") + " \"" + ServerBeep.ChatRoomName + "\"";
      FriendListBeepLog.push({
        MemberNumber: data.MemberNumber,
        MemberName: data.MemberName,
        ChatRoomName: data.ChatRoomName,
        Sent: false,
        Time: new Date()
      });

      if (CurrentScreen == "FriendList") ServerSend("AccountQuery", {
        Query: "OnlineFriends"
      });

      if (window.location.protocol.includes("https") && Notification) {
        if (Notification.permission !== "granted") {
          Notification.requestPermission();
        }
        else {
          let n = new Notification("Bondage Club Beep", {
            body: ServerBeep.Message
          });
          n.onclick = function (x) {
            window.focus();
            this.close();
          };
        }
      }

      if (ServerBeepAudio && Player.AudioSettings && Player.AudioSettings.PlayBeeps) {
        if (typeof Player.AudioSettings.Volume === 'number') ServerBeepAudio.volume = Player.AudioSettings.Volume;
        ServerBeepAudio.play();
      }
    }
  }

  // Bundle an asset in wardrobe format
  WardrobeAssetBundle = function WardrobeAssetBundle(A) {
    return {
      Name: A.Asset.Name,
      Group: A.Asset.Group.Name,
      Color: A.Color,
      Alpha: A.Alpha
    };
  }

  // Load character appearance from wardrobe, only load clothes on others
  WardrobeFastLoad = function WardrobeFastLoad(C, W, Update) {
    if (Player.Wardrobe != null && Player.Wardrobe[W] != null) {
      let AddAll = C.ID == 0 || C.AccountName.indexOf("Wardrobe-") == 0;
      C.Appearance = C.Appearance
        .filter(a => a.Asset.Group.Category != "Appearance" || (!a.Asset.Group.Clothing && !AddAll))
      Player.Wardrobe[W]
        .filter(w => w.Name != null && w.Group != null)
        .filter(w => C.Appearance.find(a => a.Asset.Group.Name == w.Group) == null)
        .forEach(w => {
          let A = Asset.find(a =>
            a.Group.Name == w.Group &&
            a.Group.Category == "Appearance" &&
            (AddAll || a.Group.Clothing) &&
            a.Name == w.Name &&
            (a.Value == 0 || InventoryAvailable(Player, a.Name, a.Group.Name)));
          if (A != null) CharacterAppearanceSetItem(C, w.Group, A, w.Color, 0, false);
        });
      // Adds any critical appearance asset that could be missing, adds the default one
      AssetGroup
        .filter(g => g.Category == "Appearance" && !g.AllowNone)
        .forEach(g => {
          if (C.Appearance.find(a => a.Asset.Group.Name == g.Name) == null) {
            C.Appearance.push({
              Asset: Asset.find(a => a.Group.Name == g.Name),
              Difficulty: 0,
              Color: g.ColorSchema[0]
            });
          }
        });
      CharacterLoadCanvas(C);
      if (Update == null || Update) {
        if (C.ID == 0 && C.OnlineID != null) ServerPlayerAppearanceSync();
        if (C.ID == 0 || C.AccountName.indexOf("Online-") == 0) ChatRoomCharacterUpdate(C);
      }
    }
  }

  // Saves character appearance in Player's wardrobe, use Player's body as base for others
  WardrobeFastSave = function WardrobeFastSave(C, W, Push) {
    if (Player.Wardrobe != null) {
      let AddAll = C.ID == 0 || C.AccountName.indexOf("Wardrobe-") == 0;
      Player.Wardrobe[W] = C.Appearance
        .filter(a => a.Asset.Group.Category == "Appearance")
        .filter(a => AddAll || a.Asset.Group.Clothing)
        .map(WardrobeAssetBundle);
      if (!AddAll) {
        // Using Player's body as base
        Player.Wardrobe[W] = Player.Wardrobe[W].concat(Player.Appearance
          .filter(a => a.Asset.Group.Category == "Appearance")
          .filter(a => !a.Asset.Group.Clothing)
          .map(WardrobeAssetBundle));
      }
      let WC = W - WardrobeOffset;
      if (WC >= 0 && WC < 12 && WardrobeCharacter != null && WardrobeCharacter[WC] != null && C.AccountName != WardrobeCharacter[WC].AccountName) {
        WardrobeFastLoad(WardrobeCharacter[WC], W);
      }
      if (Push == null || Push) ServerSend("AccountUpdate", {
        Wardrobe: Player.Wardrobe
      });
    }
  }

  function WardrobeLoadData(C, Data, LoadAll, AllInventory) {
    const AddAll = LoadAll || C.ID == 0 || C.AccountName.indexOf("Wardrobe-") == 0;
    C.Appearance = C.Appearance
      .filter(a => a.Asset.Group.Category != "Appearance" || (!a.Asset.Group.Clothing && !AddAll))
    Data
      .filter(w => w.Name != null && w.Group != null)
      .filter(w => C.Appearance.find(a => a.Asset.Group.Name == w.Group) == null)
      .forEach(w => {
        let A = Asset.find(a =>
          a.Group.Name == w.Group &&
          a.Group.Category == "Appearance" &&
          (AddAll || a.Group.Clothing) &&
          a.Name == w.Name &&
          (AllInventory == true || a.Value == 0 || InventoryAvailable(Player, a.Name, a.Group.Name)));
        if (A != null) CharacterAppearanceSetItem(C, w.Group, A, w.Color, 0, false);
      });
    // Adds any critical appearance asset that could be missing, adds the default one
    AssetGroup
      .filter(g => g.Category == "Appearance" && !g.AllowNone && !C.Appearance.some(a => a.Asset.Group.Name == g.Name))
      .forEach(g => C.Appearance.push({
        Asset: Asset.find(a => a.Group.Name == g.Name),
        Difficulty: 0,
        Color: g.ColorSchema[0]
      }));
    CharacterLoadCanvas(C);

    if (C.ID == 0 && C.OnlineID != null) ServerPlayerAppearanceSync();
    if (C.ID == 0 || C.AccountName.indexOf("Online-") == 0) ChatRoomCharacterUpdate(C);
  }

  function WardrobeSaveData(C, SaveAll) {
    const AddAll = SaveAll == true || C.ID == 0 || C.AccountName.indexOf("Wardrobe-") == 0;
    let Data = C.Appearance
      .filter(a => a.Asset.Group.Category == "Appearance")
      .filter(a => AddAll || a.Asset.Group.Clothing)
      .map(WardrobeAssetBundle);
    if (!AddAll) {
      // Using Player's body as base
      Data = Data.concat(Player.Appearance
        .filter(a => a.Asset.Group.Category == "Appearance")
        .filter(a => !a.Asset.Group.Clothing)
        .map(WardrobeAssetBundle));
    }
    return Data;
  }

  function GetGUID(C) {
    return md5("NServerId: " + C.MemberNumber.toString() + ":" + ((C.ID == 0) ? Player.OnlineID : C.AccountName.replace("Online-", "")));
  }

  function PlayerLoadGUID() {
    Player.Appearance.forEach(a => {
      if (a.Asset.Group.Name == "Eyes") {
        if (a.Property == null) {
          a.Property = {
            GUID: GetGUID(Player)
          };
        }
        else {
          a.Property.GUID = GetGUID(Player);
        }
      }
    });
  }

  function ChatRoomSelfUpdate() {
    //if (Player.ProtectClothes != false || LogQuery("BlockChange", "Rule")) WardrobeFastSave(Player, 11);
    PlayerLoadGUID();
  }

  function IsCharacterOnNServer(C) {
    if (C.OnNServer == null) {
      let G = GetGUID(C);
      let Eyes = InventoryGet(C, "Eyes");
      let P = Eyes && Eyes.Property && Eyes.Property.GUID;
      C.OnNServer = (G == P);
    }
    return C.OnNServer;
  }

  function GetCookie(cname) {
    let name = cname + "=";
    let decodedCookie = decodeURIComponent(document.cookie);
    let ca = decodedCookie.split(';');
    for (let i = 0; i < ca.length; i++) {
      let c = ca[i];
      while (c.charAt(0) == ' ') {
        c = c.substring(1);
      }
      if (c.indexOf(name) == 0) {
        return c.substring(name.length, c.length);
      }
    }
    return "";
  }

  function SetCookie(cname, cvalue, exhours) {
    let d = new Date();
    d.setTime(d.getTime() + (exhours * 60 * 60 * 1000));
    let expires = "expires=" + d.toUTCString();
    document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
  }

  // Chat room keyboard shortcuts
  ChatRoomKeyDown = function ChatRoomKeyDown() {

    // The ENTER key sends the chat
    if (KeyPress == 13) {
      event.preventDefault();
      ChatRoomSendChat();
    }

    // On page up, we show the previous chat typed
    if (KeyPress == 33) {
      if (ChatRoomLastMessage.length > 0) {
        var msg = ElementValue("InputChat").trim();
        if (ChatRoomLastMessageIndex == ChatRoomLastMessage.length || (msg != "" && ChatRoomLastMessage[ChatRoomLastMessageIndex] != msg)) {
          ChatRoomLastMessage.push(msg);
        }
        ChatRoomLastMessageIndex--;
      }
      ElementValue("InputChat", ChatRoomLastMessage[ChatRoomLastMessageIndex]);
    }

    // On page down, we show the next chat typed
    if (KeyPress == 34) {
      if (ChatRoomLastMessage.length > 0) {
        var msg = ElementValue("InputChat").trim();
        if (ChatRoomLastMessageIndex == ChatRoomLastMessage.length || (msg != "" && ChatRoomLastMessage[ChatRoomLastMessageIndex] != msg)) {
          ChatRoomLastMessage.push(msg);
        }
      }
      ChatRoomLastMessageIndex++;
      if (ChatRoomLastMessageIndex > ChatRoomLastMessage.length - 1) ChatRoomLastMessageIndex = ChatRoomLastMessage.length - 1;
      ElementValue("InputChat", ChatRoomLastMessage[ChatRoomLastMessageIndex]);
    }
  }

  // Pushes the new character data/appearance to the server
  ChatRoomCharacterUpdate = function ChatRoomCharacterUpdate(C) {
    if (C.ID == 0) ChatRoomSelfUpdate();
    var data = {
      ID: (C.ID == 0) ? Player.OnlineID : C.AccountName.replace("Online-", ""),
      ActivePose: C.ActivePose,
      Appearance: ServerAppearanceBundle(C.Appearance)
    };
    ServerSend("ChatRoomCharacterUpdate", data);
  }

})();