"use strict"; var LoginBackground = "Dressing"; /** @type {null | string[][]} */ var LoginCredits = null; var LoginCreditsPosition = 0; var LoginThankYou = ""; /* eslint-disable */ var LoginThankYouList = [ "Abysseia", "AlexG", "ArashiSama", "BlueWinter", "bryce", "Canti", "Christian", "Christoffer", "Claudia33", "Cm382714", "Desch", "Dragokahn", "dynilath", "Edwin", "Glurak", "Greendragon", "john", "Laurie", "luke", "Lyall", "Marashu", "Michel", "Mindtie", "Misa", "Nightcore", "Patrick", "René", "Robin", "Schrödingers", "Setsu95", "Sticks", "Sunny", "Tam", "Tarram1010", "Teli", "Thkdt", "Troubadix", "Troy", "Varo", "WhiteSniper", "XDWolfie", "Xepherio", ]; /* eslint-enable */ var LoginThankYouNext = 0; var LoginSubmitted = false; var LoginQueuePosition = -1; /** The server login status */ var LoginErrorMessage = ""; /** @type {NPCCharacter} */ var LoginCharacter = null; /* DEBUG: To measure FPS - uncomment this and change the + 4000 to + 40 var LoginLastCT = 0; var LoginFrameCount = 0; var LoginFrameTotalTime = 0;*/ /** * Loads the next thank you bubble * @returns {void} Nothing */ function LoginDoNextThankYou() { LoginThankYou = CommonRandomItemFromList(LoginThankYou, LoginThankYouList); CharacterRelease(LoginCharacter, false); CharacterAppearanceFullRandom(LoginCharacter); if (InventoryGet(LoginCharacter, "ItemNeck") != null) InventoryRemove(LoginCharacter, "ItemNeck", false); CharacterFullRandomRestrain(LoginCharacter); LoginThankYouNext = CommonTime() + 4000; } /** * Draw the credits * @returns {void} Nothing */ function LoginDrawCredits() { /* DEBUG: To measure FPS - uncomment this and change the + 4000 to + 40 var CT = CommonTime(); if (CT - LoginLastCT < 10000) { LoginFrameCount++; if (LoginFrameCount > 1000) LoginFrameTotalTime = LoginFrameTotalTime + CT - LoginLastCT; } LoginLastCT = CT; if (LoginFrameCount > 1000) DrawText("Average FPS: " + (1000 / (LoginFrameTotalTime / (LoginFrameCount - 1000))).toFixed(2).toString(), 1000, 975, "white"); else DrawText("Calculating Average FPS...", 1000, 975, "white");*/ // For each credits in the list LoginCreditsPosition += (TimerRunInterval * 60) / 1000; if (LoginCreditsPosition > LoginCredits.length * 25 || LoginCreditsPosition < 0) LoginCreditsPosition = 0; MainCanvas.font = "30px Arial"; for (let C = 0; C < LoginCredits.length; C++) { // Sets the Y position (it scrolls from bottom to top) var Y = 800 - Math.floor(LoginCreditsPosition * 2) + (C * 50); // Draw the text if it's in drawing range if ((Y > 0) && (Y <= 999)) { // The "CreditTypeRepeat" starts scrolling again, other credit types are translated var Cred = LoginCredits[C][0].trim(); if (Cred == "CreditTypeRepeat") { LoginCreditsPosition = 0; return; } else { if (Cred.substr(0, 10) == "CreditType") DrawText(TextGet(Cred), 320, Y, "white"); else { if (Cred.indexOf("|") == -1) DrawText(Cred, 320, Y, "white"); else { DrawText(Cred.substring(0, Cred.indexOf("|")), 180, Y, "white"); DrawText(Cred.substring(Cred.indexOf("|") + 1, 1000), 460, Y, "white"); } } } } } // Restore the canvas font MainCanvas.font = CommonGetFont(36); } var LoginEventListeners = { /** * @private * @type {(this: HTMLInputElement, ev: KeyboardEvent) => void} */ _KeyDownInputName: function _KeyDownInputName(ev) { if (CommonKey.IsPressed(ev, "Enter")) { document.getElementById("InputPassword")?.focus(); ev.stopPropagation(); } }, /** * @private * @type {(this: HTMLInputElement, ev: KeyboardEvent) => void} */ _KeyDownInputPassword: function _KeyDownInputPassword(ev) { if (CommonKey.IsPressed(ev, "Enter")) { const Name = ElementValue("InputName"); const Password = ElementValue("InputPassword"); LoginDoLogin(Name, Password); ev.stopPropagation(); } }, }; /** * Loads the character login screen * @returns {void} Nothing */ function LoginLoad() { // Resets the player and other characters Character = []; CharacterNextId = 0; // Create a blank character for our player. Its actual ID will be set when LoginResponse happens Player = /** @type {PlayerCharacter} */ (CharacterCreate("Female3DCG", CharacterType.SIMPLE, "")); LoginCharacter = CharacterLoadNPC("NPC_Login"); LoginDoNextThankYou(); LoginStatusReset(); if (LoginCredits == null) CommonReadCSV("LoginCredits", CurrentModule, CurrentScreen, "GameCredits"); ActivityDictionaryLoad(); OnlneGameDictionaryLoad(); const form = ElementCreateForm("Login"); const username = ElementCreateInput("InputName", "text", "", "20", form); username.setAttribute("autocomplete", "username"); username.setAttribute("enterkeyhint", "next"); username.addEventListener("keydown", LoginEventListeners._KeyDownInputName); username.focus(); const pass = ElementCreateInput("InputPassword", "password", "", "20", form); pass.setAttribute("autocomplete", "current-password"); pass.setAttribute("enterkeyhint", "go"); pass.addEventListener("keydown", LoginEventListeners._KeyDownInputPassword); const languages = TranslationDictionary.map(l => { return { children: [l.Icon, " ", l.LanguageName], attributes: { selected: l.LanguageCode === TranslationLanguage ? "" : undefined, lang: l.LanguageCode, value: l.LanguageCode, }, }; }); ElementCreateDropdown("LanguageDropdown", languages, function(event) { TranslationSwitchLanguage(/** @type {"" | "TW" | ServerChatRoomLanguage} */(this.value) || "EN"); TextLoad(); ActivityDictionaryLoad(); AssetLoadDescription("Female3DCG"); }); TextPrefetchFile(BackgroundsStringsPath); TextPrefetchFile(AssetStringsPath); TextPrefetchFile(InterfaceStringsPath); TextPrefetch("Room", "MainHall"); DrawGetImage("Backgrounds/MainHall.jpg"); } /** * Runs the character login screen * @returns {void} Nothing */ function LoginRun() { // Draw the credits if (LoginCredits != null) LoginDrawCredits(); const CanLogin = ServerIsConnected && !LoginSubmitted; // Draw the login controls DrawText(TextGet("Welcome"), 1000, 50, "White", "Black"); DrawText(LoginGetStatus() ?? TextGet("EnterNamePassword"), 1000, 100, "White", "Black"); DrawText(TextGet("AccountName"), 1000, 200, "White", "Black"); DrawText(TextGet("Password"), 1000, 350, "White", "Black"); DrawButton(725, 500, 255, 60, TextGet("Login"), CanLogin ? "White" : "Grey", ""); DrawText(TextGet("CreateNewCharacter"), 1000, 670, "White", "Black"); DrawButton(825, 710, 350, 60, TextGet("NewCharacter"), CanLogin ? "White" : "Grey", ""); if (CheatAllow) DrawButton(825, 800, 350, 60, TextGet("Cheats"), "White", ""); DrawButton(825, 890, 350, 60, TextGet("PasswordReset"), CanLogin ? "White" : "Grey", ""); // Draw the character and thank you bubble DrawCharacter(LoginCharacter, 1400, 100, 0.9); if (LoginThankYouNext < CommonTime()) LoginDoNextThankYou(); DrawImage("Screens/" + CurrentModule + "/" + CurrentScreen + "/Bubble.png", 1400, 16); DrawText(TextGet("ThankYou") + " " + LoginThankYou, 1625, 53, "Black", "Gray"); } /** @type {ScreenFunctions["Resize"]} */ function LoginResize(load) { ElementPosition("InputPassword", 1000, 410, 500); ElementPosition("InputName", 1000, 260, 500); ElementPosition("LanguageDropdown", 1145, 530, 255, 60); const langSelect = document.getElementById("LanguageDropdown"); langSelect?.style.setProperty("font-family", `"Twemoji Country Flags", ${langSelect.style.fontFamily}`); } /** * The list of item fixups to apply on login. * * Those are applied by the login code, after the player's item lists are set up * but before the inventory and appearance are loaded from the server's data, * and applies the specified asset fixups by swapping Old with New in the list * of owned items, in the various player item lists, and in the appearance. * * If you're only moving items around, it should work just fine as long as * the `Old` and `New` asset definitions are compatible. * If it's an asset merge (say 3 into one typed asset), it will either set * the fixed up item to the specified `Option` or the first one if unspecified. * * @type {{ Old: { Group: string, Name: string | '*' }, New: { Group: AssetGroupName, Name?: string, Option?: string } }[]} */ let LoginInventoryFixups = [ { Old: { Group: "ItemLegs", Name: "WoodenHorse" }, New: { Group: "ItemDevices", Name: "WoodenHorse" } }, { Old: { Group: "ItemVulvaPiercings", Name: "WeightedClitPiercing" }, New: { Group: "ItemVulvaPiercings", Name: "RoundClitPiercing", Option: "Weight" } }, { Old: { Group: "ItemVulvaPiercings", Name: "BellClitPiercing" }, New: { Group: "ItemVulvaPiercings", Name: "RoundClitPiercing", Option: "Bell" } }, { Old: { Group: "ItemArms", Name: "BitchSuitExposed"}, New: { Group: "ItemArms", Name: "BitchSuit", Option: "Exposed" } }, { Old: { Group: "ItemHands", Name: "SpankingToysBaguette" }, New: { Group: "ItemHandheld", Name: "Baguette"} }, { Old: { Group: "ItemHands", Name: "SpankingToysBallgag" }, New: { Group: "ItemHandheld", Name: "Ballgag"} }, { Old: { Group: "ItemHands", Name: "SpankingToysBelt" }, New: { Group: "ItemHandheld", Name: "Belt"} }, { Old: { Group: "ItemHands", Name: "SpankingToysBroom" }, New: { Group: "ItemHandheld", Name: "Broom"} }, { Old: { Group: "ItemHands", Name: "SpankingToysCandleWax" }, New: { Group: "ItemHandheld", Name: "CandleWax"} }, { Old: { Group: "ItemHands", Name: "SpankingToysCane" }, New: { Group: "ItemHandheld", Name: "Cane"} }, { Old: { Group: "ItemHands", Name: "SpankingToysCattleProd" }, New: { Group: "ItemHandheld", Name: "CattleProd"} }, { Old: { Group: "ItemHands", Name: "SpankingToysCrop" }, New: { Group: "ItemHandheld", Name: "Crop"} }, { Old: { Group: "ItemHands", Name: "SpankingToysElectricToothbrush" }, New: { Group: "ItemHandheld", Name: "ElectricToothbrush"} }, { Old: { Group: "ItemHands", Name: "SpankingToysFeather" }, New: { Group: "ItemHandheld", Name: "Feather"} }, { Old: { Group: "ItemHands", Name: "SpankingToysFeatherDuster" }, New: { Group: "ItemHandheld", Name: "FeatherDuster"} }, { Old: { Group: "ItemHands", Name: "SpankingToysFlogger" }, New: { Group: "ItemHandheld", Name: "Flogger"} }, { Old: { Group: "ItemHands", Name: "SpankingToysGavel" }, New: { Group: "ItemHandheld", Name: "Gavel"} }, { Old: { Group: "ItemHands", Name: "SpankingToysGlassEmpty" }, New: { Group: "ItemHandheld", Name: "GlassEmpty"} }, { Old: { Group: "ItemHands", Name: "SpankingToysGlassFilled" }, New: { Group: "ItemHandheld", Name: "GlassFilled"} }, { Old: { Group: "ItemHands", Name: "SpankingToysHairbrush" }, New: { Group: "ItemHandheld", Name: "Hairbrush"} }, { Old: { Group: "ItemHands", Name: "SpankingToysHeartCrop" }, New: { Group: "ItemHandheld", Name: "HeartCrop"} }, { Old: { Group: "ItemHands", Name: "SpankingToysIceCube" }, New: { Group: "ItemHandheld", Name: "IceCube"} }, { Old: { Group: "ItemHands", Name: "KeyProp" }, New: { Group: "ItemHandheld", Name: "KeyProp"} }, { Old: { Group: "ItemHands", Name: "SpankingToysLargeDildo" }, New: { Group: "ItemHandheld", Name: "LargeDildo"} }, { Old: { Group: "ItemHands", Name: "SpankingToysLongDuster" }, New: { Group: "ItemHandheld", Name: "LongDuster"} }, { Old: { Group: "ItemHands", Name: "SpankingToysLongSock" }, New: { Group: "ItemHandheld", Name: "LongSock"} }, { Old: { Group: "ItemHands", Name: "MedicalInjector" }, New: { Group: "ItemHandheld", Name: "MedicalInjector"} }, { Old: { Group: "ItemHands", Name: "SpankingToysLotion" }, New: { Group: "ItemHandheld", Name: "Lotion"} }, { Old: { Group: "ItemHands", Name: "SpankingToysPaddle" }, New: { Group: "ItemHandheld", Name: "Paddle"} }, { Old: { Group: "ItemHands", Name: "SpankingToysPanties" }, New: { Group: "ItemHandheld", Name: "Panties"} }, { Old: { Group: "ItemHands", Name: "SpankingToysPetToy" }, New: { Group: "ItemHandheld", Name: "PetToy"} }, { Old: { Group: "ItemHands", Name: "SpankingToysPhone1" }, New: { Group: "ItemHandheld", Name: "Phone1"} }, { Old: { Group: "ItemHands", Name: "SpankingToysPhone2" }, New: { Group: "ItemHandheld", Name: "Phone2"} }, { Old: { Group: "ItemHands", Name: "SpankingToysPlasticWrap" }, New: { Group: "ItemHandheld", Name: "PlasticWrap"} }, { Old: { Group: "ItemHands", Name: "SpankingToysPotionBottle" }, New: { Group: "ItemHandheld", Name: "PotionBottle"} }, { Old: { Group: "ItemHands", Name: "SpankingToysRainbowWand" }, New: { Group: "ItemHandheld", Name: "RainbowWand"} }, { Old: { Group: "ItemHands", Name: "SpankingToysRopeCoilLong" }, New: { Group: "ItemHandheld", Name: "RopeCoilLong"} }, { Old: { Group: "ItemHands", Name: "SpankingToysRopeCoilShort" }, New: { Group: "ItemHandheld", Name: "RopeCoilShort"} }, { Old: { Group: "ItemHands", Name: "SpankingToysRuler" }, New: { Group: "ItemHandheld", Name: "Ruler"} }, { Old: { Group: "ItemHands", Name: "SpankingToysScissors" }, New: { Group: "ItemHandheld", Name: "Scissors"} }, { Old: { Group: "ItemHands", Name: "SpankingToysShockRemote" }, New: { Group: "ItemHandheld", Name: "ShockRemote"} }, { Old: { Group: "ItemHands", Name: "SpankingToysShockWand" }, New: { Group: "ItemHandheld", Name: "ShockWand"} }, { Old: { Group: "ItemHands", Name: "SpankingToysSmallDildo" }, New: { Group: "ItemHandheld", Name: "SmallDildo"} }, { Old: { Group: "ItemHands", Name: "SpankingToysSmallVibratingWand" }, New: { Group: "ItemHandheld", Name: "SmallVibratingWand"} }, { Old: { Group: "ItemHands", Name: "SpankingToysSpatula" }, New: { Group: "ItemHandheld", Name: "Spatula"} }, { Old: { Group: "ItemHands", Name: "SpankingToysSword" }, New: { Group: "ItemHandheld", Name: "Sword"} }, { Old: { Group: "ItemHands", Name: "SpankingToysTapeRoll" }, New: { Group: "ItemHandheld", Name: "TapeRoll"} }, { Old: { Group: "ItemHands", Name: "SpankingToysTennisRacket" }, New: { Group: "ItemHandheld", Name: "TennisRacket"} }, { Old: { Group: "ItemHands", Name: "SpankingToysForSaleSign" }, New: { Group: "ItemHandheld", Name: "ForSaleSign"} }, { Old: { Group: "ItemHands", Name: "SpankingToysToothbrush" }, New: { Group: "ItemHandheld", Name: "Toothbrush"} }, { Old: { Group: "ItemHands", Name: "SpankingToysTowel" }, New: { Group: "ItemHandheld", Name: "Towel"} }, { Old: { Group: "ItemHands", Name: "SpankingToysVibeRemote" }, New: { Group: "ItemHandheld", Name: "VibeRemote"} }, { Old: { Group: "ItemHands", Name: "SpankingToysVibratingWand" }, New: { Group: "ItemHandheld", Name: "VibratingWand"} }, { Old: { Group: "ItemHands", Name: "SpankingToysVibrator" }, New: { Group: "ItemHandheld", Name: "Vibrator"} }, { Old: { Group: "ItemHands", Name: "SpankingToysWartenbergWheel" }, New: { Group: "ItemHandheld", Name: "WartenbergWheel"} }, { Old: { Group: "ItemHands", Name: "SpankingToysWhip" }, New: { Group: "ItemHandheld", Name: "Whip"} }, { Old: { Group: "ItemHands", Name: "SpankingToysWhipPaddle" }, New: { Group:"ItemHandheld", Name:"WhipPaddle" } }, { Old: { Group: "Pussy", Name: "PussyLight1" }, New: { Group: "Pussy", Name: "Pussy1" } }, { Old: { Group: "Pussy", Name: "PussyLight2" }, New: { Group: "Pussy", Name: "Pussy2" } }, { Old: { Group: "Pussy", Name: "PussyLight3" }, New: { Group: "Pussy", Name: "Pussy3" } }, { Old: { Group: "Pussy", Name: "PussyDark1" }, New: { Group: "Pussy", Name: "Pussy1" } }, { Old: { Group: "Pussy", Name: "PussyDark2" }, New: { Group: "Pussy", Name: "Pussy2" } }, { Old: { Group: "Pussy", Name: "PussyDark3" }, New: { Group: "Pussy", Name: "Pussy3" } }, { Old: { Group: "LeftAnklet", Name: "*" }, New: { Group: "AnkletLeft" }}, { Old: { Group: "RightAnklet", Name: "*" }, New: { Group: "AnkletRight" }}, { Old: { Group: "LeftHand", Name: "*" }, New: { Group: "HandAccessoryLeft" }}, { Old: { Group: "RightHand", Name: "*" }, New: { Group: "HandAccessoryRight" }}, ]; /** * Perform the inventory fixups needed. * @param {InventoryBundle[]} Inventory - The server-provided inventory object */ function LoginPerformInventoryFixups(Inventory) { // Skip fixups on new characters if (!Inventory) return; for (const fixup of LoginInventoryFixups) { // For every asset fixup to do, update the inventory const item = Inventory.find(i => i.Group === fixup.Old.Group && (i.Name === fixup.Old.Name || fixup.Old.Name === "*")); if (item) { item.Group = fixup.New.Group; if (fixup.New.Name) { item.Name = fixup.New.Name; } } } } /** * Perform the appearance fixups needed. * TODO: only typed items are supported. * @param {ItemBundle[]} Appearance - The server-provided appearance object * @return {boolean} */ function LoginPerformAppearanceFixups(Appearance) { if (!Appearance) return; let fixedUp = false; for (const fixup of LoginInventoryFixups) { const idx = Appearance.findIndex(a => a.Group === fixup.Old.Group && (a.Name === fixup.Old.Name || fixup.Old.Name === "*")); if (idx != -1) { // The item is currently worn, remove it let worn = Appearance[idx]; Appearance.splice(idx, 1); // There're already something else in that slot, preserve it if (Appearance.find(a => a.Group === fixup.New.Group)) { continue; } // Set up the new item and its properties worn.Group = fixup.New.Group; if (fixup.New.Name) { worn.Name = fixup.New.Name; } const asset = AssetGet("Female3DCG", worn.Group, worn.Name); let opt = null; if (asset.Archetype) { switch (asset.Archetype) { case ExtendedArchetype.TYPED: { const opts = TypedItemGetOptions(worn.Group, worn.Name); if (typeof fixup.New.Option === "undefined") opt = opts[0]; else opt = opts.find(o => o.Name === fixup.New.Option); if (!opt) { console.error(`Unknown option ${fixup.New.Option}`); continue; } } break; } // Replace old previous properties with the wanted ones if (opt && opt.Property) worn.Property = Object.assign(opt.Property); } else if (asset.Extended) { // Old-style extended item } else { delete worn.Property; } // Push back the updated data Appearance.push(worn); fixedUp = true; } } return fixedUp; } /** * Perform the crafting fixups needed * @param {readonly CraftingItem[]} Crafting - The server-provided, uncompressed crafting data */ function LoginPerformCraftingFixups(Crafting) { if (!Crafting || !CommonIsArray(Crafting)) return; for (const fixup of LoginInventoryFixups) { // Move crafts over to the new name for (const craft of Crafting) { if (!craft || craft.Item !== fixup.Old.Name) continue; craft.Item = fixup.New.Name; } } } /** * Make sure the slave collar is equipped or unequipped based on the owner * @returns {void} Nothing */ function LoginValidCollar() { let item = InventoryGet(Player, "ItemNeck"); if (!Player.IsFullyOwned() || LogQuery("ReleasedCollar", "OwnerRule")) { // Not owned, or currently released from the collar, make sure we don't have it equipped if (item && item.Asset.Name == "SlaveCollar") { InventoryRemove(Player, "ItemNeck"); item = null; if (CurrentScreen == "ChatRoom") { ChatRoomCharacterItemUpdate(Player, "ItemNeck"); ChatRoomCharacterItemUpdate(Player, "ItemNeckAccessories"); ChatRoomCharacterItemUpdate(Player, "ItemNeckRestraints"); } } } if (Player.IsFullyOwned() && !LogQuery("ReleasedCollar", "OwnerRule")) { // Owned, but not currently released from the collar if (item && !["SlaveCollar", "ClubSlaveCollar"].includes(item.Asset.Name)) { // We're wearing something else, yank that InventoryRemove(Player, "ItemNeck"); item = null; if (CurrentScreen == "ChatRoom") ChatRoomCharacterItemUpdate(Player, "ItemNeck"); } if (!item) { // Now make sure we're wearing the slave collar InventoryWear(Player, "SlaveCollar", "ItemNeck"); if (CurrentScreen == "ChatRoom") ChatRoomCharacterItemUpdate(Player, "ItemNeck"); } } // Check if we should switch to the club slave collar if (LogQuery("ClubSlave", "Management") && !InventoryIsWorn(Player, "ClubSlaveCollar", "ItemNeck")) InventoryWear(Player, "ClubSlaveCollar", "ItemNeck"); } /** * Adds or confiscates Club Mistress items from the player. Only players that are club Mistresses can have the Mistress * Padlock & Key * @returns {void} Nothing */ function LoginMistressItems() { if (LogQuery("ClubMistress", "Management")) { InventoryAdd(Player, "MistressGloves", "Gloves", false); InventoryAdd(Player, "MistressBoots", "Shoes", false); InventoryAdd(Player, "MistressTop", "Cloth", false); InventoryAdd(Player, "MistressBottom", "ClothLower", false); InventoryAdd(Player, "MistressPadlock", "ItemMisc", false); InventoryAdd(Player, "MistressPadlockKey", "ItemMisc", false); InventoryAdd(Player, "MistressTimerPadlock", "ItemMisc", false); InventoryAdd(Player, "DeluxeBoots", "Shoes", false); } else { InventoryDelete(Player, "MistressPadlock", "ItemMisc", false); InventoryDelete(Player, "MistressPadlockKey", "ItemMisc", false); InventoryDelete(Player, "MistressTimerPadlock", "ItemMisc", false); InventoryDelete(Player, "MistressGloves", "Gloves", false); InventoryDelete(Player, "MistressBoots", "Shoes", false); InventoryDelete(Player, "MistressTop", "Cloth", false); InventoryDelete(Player, "MistressBottom", "ClothLower", false); InventoryDelete(Player, "DeluxeBoots", "Shoes", false); } } /** * Give the matching RewardMemberNumber Club Card to the player * @returns {void} Nothing */ function LoginClubCard() { // Loops in all the cards to see if there's a card that matches the player MembeNumber for (let Card of ClubCardList) if (Player.MemberNumber === Card.RewardMemberNumber) { // Make sure the proper objects are created Player.Game.ClubCard ??= { Deck: [], Reward: "" }; if (!CommonIsArray(Player.Game.ClubCard.Deck)) Player.Game.ClubCard.Deck = []; if (typeof Player.Game.ClubCard.Reward !== "string") Player.Game.ClubCard.Reward = ""; // If the player doesn't have that card, we add it let Char = String.fromCharCode(Card.ID); if (Player.Game.ClubCard.Reward.indexOf(Char) < 0) { Player.Game.ClubCard.Reward = Player.Game.ClubCard.Reward + Char; ServerAccountUpdate.QueueData({ Game: Player.Game }, true); } return; } } /** * Adds or confiscates pony equipment from the player. Only players that are ponies or trainers can have the pony * equipment. * @returns {void} Nothing */ function LoginStableItems() { if (LogQuery("JoinedStable", "PonyExam") || LogQuery("JoinedStable", "TrainerExam")) { InventoryAdd(Player, "HarnessPonyBits", "ItemMouth", false); InventoryAdd(Player, "HarnessPonyBits", "ItemMouth2", false); InventoryAdd(Player, "HarnessPonyBits", "ItemMouth3", false); InventoryAdd(Player, "PonyBoots", "Shoes", false); InventoryAdd(Player, "PonyBoots", "ItemBoots", false); InventoryAdd(Player, "PonyHood", "ItemHood", false); InventoryAdd(Player, "HoofMittens", "ItemHands", false); } else { InventoryDelete(Player, "HarnessPonyBits", "ItemMouth", false); InventoryDelete(Player, "HarnessPonyBits", "ItemMouth2", false); InventoryDelete(Player, "HarnessPonyBits", "ItemMouth3", false); InventoryDelete(Player, "PonyBoots", "Shoes", false); InventoryDelete(Player, "PonyBoots", "ItemBoots", false); InventoryDelete(Player, "PonyHood", "ItemHood", false); InventoryDelete(Player, "HoofMittens", "ItemHands", false); } } /** * Adds or confiscates maid items from the player. Only players that have joined the Maid Sorority can have these items. * @returns {void} - Nothing */ function LoginMaidItems() { if (LogQuery("JoinedSorority", "Maid")) { InventoryAdd(Player, "MaidOutfit1", "Cloth", false); InventoryAdd(Player, "MaidOutfit2", "Cloth", false); InventoryAdd(Player, "MaidHairband1", "Cloth", false); InventoryAdd(Player, "MaidApron1", "Cloth", false); InventoryAdd(Player, "MaidApron2", "Cloth", false); InventoryAdd(Player, "FrillyApron", "ClothAccessory", false); InventoryAdd(Player, "MaidHairband1", "Hat", false); InventoryAdd(Player, "ServingTray", "ItemMisc", false); } else { InventoryDelete(Player, "MaidOutfit1", "Cloth", false); InventoryDelete(Player, "MaidOutfit2", "Cloth", false); InventoryDelete(Player, "MaidHairband1", "Cloth", false); InventoryDelete(Player, "MaidApron1", "Cloth", false); InventoryDelete(Player, "MaidApron2", "Cloth", false); InventoryDelete(Player, "FrillyApron", "ClothAccessory", false); InventoryDelete(Player, "MaidHairband1", "Hat", false); InventoryDelete(Player, "ServingTray", "ItemMisc", false); } } /** * Ensures lover-exclusive items are removed if the player has no lovers. * @returns {void} Nothing */ function LoginLoversItems() { const LoversNumbers = Player.GetLoversNumbers(); // check to remove love leather collar slave collar if no lover // Note that as the Slave collar isn't yet an archetypal asset, that's why it gets skipped by validation if (LoversNumbers.length < 1) { const Collar = InventoryGet(Player,"ItemNeck"); if (Collar && Collar.Property && (Collar.Asset.Name == "SlaveCollar") && (Collar.Property.Type == "LoveLeatherCollar")) { Collar.Property = CommonCloneDeep(InventoryItemNeckSlaveCollarTypes[0].Property); Collar.Color = "Default"; } } } /** * Adds or removes Asylum items. Only players that have previously maxed out their patient or nurse reputation are * eligible for their own set of Asylum restraints outside the Asylum. * @returns {void} - Nothing */ function LoginAsylumItems() { if (LogQuery("ReputationMaxed", "Asylum")) { InventoryAddMany(Player, [ {Name: "MedicalBedRestraints", Group: "ItemArms"}, {Name: "MedicalBedRestraints", Group: "ItemLegs"}, {Name: "MedicalBedRestraints", Group: "ItemFeet"}, ], false); } else { InventoryDelete(Player, "MedicalBedRestraints", "ItemArms", false); InventoryDelete(Player, "MedicalBedRestraints", "ItemLegs", false); InventoryDelete(Player, "MedicalBedRestraints", "ItemFeet", false); } } /** * Adds items if specific cheats are enabled * @returns {void} - Nothing */ function LoginCheatItems() { if (CheatFactor("FreeCollegeOutfit", 0) == 0) { InventoryAdd(Player, "CollegeOutfit1", "Cloth"); InventoryAdd(Player, "CollegeSkirt", "ClothLower"); } } /** * Checks every owned item to see if its BuyGroup contains an item the player does not have. This allows the player to * collect any items that have been added to the game which are in a BuyGroup that they have already purchased. * @returns {void} Nothing */ function LoginValideBuyGroups() { for (let A = 0; A < Asset.length; A++) if ((Asset[A].BuyGroup != null) && InventoryAvailable(Player, Asset[A].Name, Asset[A].Group.Name)) for (let B = 0; B < Asset.length; B++) if ((Asset[B] != null) && (Asset[B].BuyGroup != null) && (Asset[B].BuyGroup == Asset[A].BuyGroup) && !InventoryAvailable(Player, Asset[B].Name, Asset[B].Group.Name)) InventoryAdd(Player, Asset[B].Name, Asset[B].Group.Name, false); } /** * Makes sure the difficulty restrictions are applied to the player * @param {boolean} applyDefaults - If changing to the difficulty, set this to True to set LimitedItems to the default settings * @returns {void} Nothing */ function LoginDifficulty(applyDefaults) { // If Extreme mode, the player cannot control her blocked items if (Player.GetDifficulty() >= 3) { LoginExtremeItemSettings(applyDefaults); ServerPlayerBlockItemsSync(); } } /** * Set the item permissions for the Extreme difficulty * @param {boolean} applyDefaults - When initially changing to extreme/whitelist, TRUE sets strong locks to limited permissions. When enforcing * settings, FALSE allows them to remain as they are since the player could have changed them to fully open. * @returns {void} Nothing */ function LoginExtremeItemSettings(applyDefaults) { const LimitedAssets = new Set(MainHallStrongLocks.map(i => `ItemMisc/${i}`)); for (const [name, permission] of CommonEntries(Player.PermissionItems)) { permission.Hidden = false; const limitedAllowed = LimitedAssets.has(name); for (const [type, typePermission] of Object.entries(permission.TypePermissions)) { if (typePermission !== "Favorite") { delete permission.TypePermissions[type]; } } switch (permission.Permission) { case "Block": permission.Permission = "Default"; break; case "Limited": if (!limitedAllowed) { permission.Permission = "Default"; } break; case "Default": // If the item permissions are 3 = "Owner/Lover/Whitelist" don't limit the locks, since that just blocks whitelisted players if (limitedAllowed && applyDefaults && Player.ItemPermission <= 3) { permission.Permission = "Limited"; } break; } } } /** * Handles server response, when login has been queued * @param {number} Pos - The position in queue * @returns {void} Nothing */ function LoginQueue(Pos) { if (typeof Pos !== "number") return; LoginQueuePosition = Pos; } /** * Fixes the Owner property on the player object if it's wrongly set * @returns {void} Nothing */ function LoginFixOwner() { if ((Player.IsOwned() === false) && (Player.Owner !== "") && !Player.Owner.trim().startsWith("NPC-")) { Player.Owner = ""; ServerAccountUpdate.QueueData({ Owner: Player.Owner }); } } /** * Sets the player character info from the server data * @param {ServerAccountData} C */ function LoginSetupPlayer(C) { Player.Name = C.Name; Player.AccountName = C.AccountName; Player.AssetFamily = C.AssetFamily ?? "Female3DCG"; Player.Title = C.Title; Player.Nickname = typeof C.Nickname === "string" ? C.Nickname : ""; Player.Money = CommonIsNumeric(C.Money) ? C.Money : 0; Player.Owner = typeof C.Owner === "string" ? C.Owner : ""; Player.Game = CommonIsObject(C.Game) ? C.Game : {}; if (typeof C.Description === "string" && C.Description.startsWith(ONLINE_PROFILE_DESCRIPTION_COMPRESSION_MAGIC)) { const desc = LZString.decompressFromUTF16(C.Description.substring(1)) ?? ""; Player.Description = desc.substring(0, 10000); } else { Player.Description = ""; } Player.Creation = C.Creation; Player.Wardrobe = C.Wardrobe ? CharacterDecompressWardrobe(C.Wardrobe) : []; WardrobeFixLength(); Player.WardrobeCharacterNames = C.WardrobeCharacterNames ?? []; Player.CharacterID = C.ID.toString(); Player.OnlineID = C.ID.toString(); Player.MemberNumber = C.MemberNumber; Player.Difficulty = ServerAccountDataSyncedValidate.Difficulty(C.Difficulty, Player); const { permissions, shouldSync } = ServerUnPackItemPermissions(C, Player.Difficulty.Level >= 3); Player.PermissionItems = permissions; if (shouldSync) { ServerPlayerBlockItemsSync(); } Player.ChatSearchFilterTerms = C.ChatSearchFilterTerms ?? ""; // Sets the default language when creating or searching for chat rooms ChatAdminDefaultLanguage = C.RoomCreateLanguage ?? "EN"; if (ChatAdminDefaultLanguage == null) ChatAdminDefaultLanguage = ChatAdminLanguageList[0]; if (ChatAdminLanguageList.indexOf(ChatAdminDefaultLanguage) < 0) ChatAdminDefaultLanguage = ChatAdminLanguageList[0]; ChatSearchLanguage = C.RoomSearchLanguage ?? ""; if (ChatSearchLanguage == null) ChatSearchLanguage = ""; if (ChatSearchLanguage && ChatAdminLanguageList.indexOf(ChatSearchLanguage) < 0) ChatSearchLanguage = ""; // Load the last chat room Player.LastMapData = C.LastMapData ?? undefined; if (C.LastChatRoom != null && typeof C.LastChatRoom !== "string") { // Backward compatability: Convert old-style "Private" to "Visibility" and "Locked" to "Access" // @ts-ignore if (typeof C.LastChatRoom.Private === "boolean") // @ts-ignore C.LastChatRoom.Visibility = C.LastChatRoom.Private ? ChatRoomVisibilityMode.UNLISTED : ChatRoomVisibilityMode.PUBLIC; // @ts-ignore if (typeof C.LastChatRoom.Locked === "boolean") // @ts-ignore C.LastChatRoom.Access = C.LastChatRoom.Locked ? ChatRoomAccessMode.ADMIN : ChatRoomAccessMode.PUBLIC; if (ChatRoomValidateProperties(C.LastChatRoom)) Player.LastChatRoom = C.LastChatRoom; } else if (typeof C.LastChatRoom === "string" && C.LastChatRoom) { // Backward compatibility: Automatically convert old-style data to a real ChatRoom object /** @type {ChatRoomSettings} */ const room = { Name: C.LastChatRoom, Description: C.LastChatRoomDesc, Admin: typeof C.LastChatRoomAdmin == "string" ? CommonConvertStringToArray(C.LastChatRoomAdmin) : [], Whitelist: typeof C.LastChatRoomWhitelist == "string" ? CommonConvertStringToArray(C.LastChatRoomWhitelist) : [], Ban: typeof C.LastChatRoomBan == "string" ? CommonConvertStringToArray(C.LastChatRoomBan) : [], Background: C.LastChatRoomBG, Limit: C.LastChatRoomSize, Game: "", Visibility: C.LastChatRoomPrivate ? ChatRoomVisibilityMode.UNLISTED : ChatRoomVisibilityMode.PUBLIC, Access: ChatRoomVisibilityMode.PUBLIC, BlockCategory: C.LastChatRoomBlockCategory, Space: C.LastChatRoomSpace, Language: C.LastChatRoomLanguage, Custom: C.LastChatRoomCustom, MapData: C.LastChatRoomMapData, }; if (ChatRoomValidateProperties(room)) { Player.LastChatRoom = room; } } // Loads the ownership data Player.Ownership = ServerAccountDataSyncedValidate.Ownership(C.Ownership); if (Player.Ownership != null) { Player.Owner = (Player.Ownership.Stage == 1) ? Player.Ownership.Name : ""; } // @ts-expect-error: FIXME: Property 'Name' is optional in type 'ServerLovership' but required in type 'Lovership' // Ensures lovership data is compatible and converts lovers to lovership Player.Lovership = Array.isArray(C.Lovership) ? C.Lovership : C.Lovership != undefined ? [C.Lovership] : []; if ((C.Lover != null) && (C.Lover != "undefined") && C.Lover.startsWith("NPC-")) { Player.Lover = C.Lover; ServerPlayerSync(); } // Calls the preference init to make sure the preferences are loaded correctly PreferenceInitPlayer(Player, C); Player.ItemPermission = C.ItemPermission ?? 2; Player.KinkyDungeonExploredLore = C.KinkyDungeonExploredLore; Player.SavedExpressions = C.SavedExpressions ?? []; PreferenceValidateArousalData(Player); if (!Array.isArray(Player.SavedExpressions)) Player.SavedExpressions = []; if (Player.SavedExpressions.length < 5) for (let x = Player.SavedExpressions.length; x < 5; x++) Player.SavedExpressions.push(null); // Load Favorited Colors if (!CommonIsArray(C.SavedColors)) { Player.SavedColors = []; for (let i = 0; i < ColorPickerNumSaved; i++) { if (typeof Player.SavedColors[i] != "object" || isNaN(Player.SavedColors[i].H) || isNaN(Player.SavedColors[i].S) || isNaN(Player.SavedColors[i].V)) Player.SavedColors[i] = GetDefaultSavedColors()[i]; } Player.SavedColors.length = ColorPickerNumSaved; } else { Player.SavedColors = C.SavedColors ?? []; } // Loads the online lists Player.WhiteList = Array.isArray(C.WhiteList) ? C.WhiteList : []; Player.BlackList = Array.isArray(C.BlackList) ? C.BlackList : []; Player.FriendList = Array.isArray(C.FriendList) ? C.FriendList : []; Player.GhostList = Array.isArray(C.GhostList) ? C.GhostList : []; // Attempt to parse friend namespace let friendNames = undefined; if (typeof C.FriendNames === "string") { try { const data = LZString.decompressFromUTF16(C.FriendNames); if (data === null) throw new Error(); const json = /** @type {[number, string][]} */(JSON.parse(data)); friendNames = new Map(json); } catch (err) { console.warn("An error occured while parsing friendnames, entries have been reset."); } } Player.FriendNames = friendNames ?? new Map(); let submissivesList = undefined; if (typeof C.SubmissivesList === "string") { try { const data = LZString.decompressFromUTF16(C.SubmissivesList); if (data === null) throw new Error(); const json = JSON.parse(data); if (!CommonIsArray(json)) throw new Error(); const numbers = /** @type {number[]} */(json.filter(Number)); submissivesList = new Set(numbers); } catch (err) { console.warn("An error occured while parsing submissives, entries have been reset."); } } Player.SubmissivesList = submissivesList ?? new Set(); Player.Infiltration = C.Infiltration; LoginDifficulty(false); // Loads the crafting data const CraftingDecompressed = CraftingDecompressServerData(C.Crafting); // Loads the inventory data if (typeof C.InventoryData === "string" && C.InventoryData !== "") { // We keep track of that to be able to tell if there's been changes in the inventory Player.InventoryData = C.InventoryData; } const loadedInventory = InventoryLoad(C.Inventory ?? "", C.InventoryData ?? ""); LoginPerformInventoryFixups(loadedInventory); const fixedUp = LoginPerformAppearanceFixups(C.Appearance ?? []); LoginPerformCraftingFixups(CraftingDecompressed); InventoryAddMany(Player, loadedInventory, false); ServerPlayerInventorySync(); const updated = ServerAppearanceLoadFromBundle(Player, C.AssetFamily, C.Appearance ?? [], C.MemberNumber); if (fixedUp || updated) { // Refresh the character server-side if we had to tweak the appearance ServerPlayerAppearanceSync(); } // Loads the current appearance, log, reputation and skills LogLoad(C.Log ?? []); ReputationLoad(C.Reputation); SkillLoad(C.Skill); Player.ConfiscatedItems = C.ConfiscatedItems ?? []; Player.ExtensionSettings = C.ExtensionSettings ?? {}; if (Player.VisualSettings.PrivateRoomBackground) PrivateBackground = Player.VisualSettings.PrivateRoomBackground; if (Player.VisualSettings.MainHallBackground) MainHallBackground = Player.VisualSettings.MainHallBackground; PrivateCharacterMax = 4 + (LogQuery("Expansion", "PrivateRoom") ? 4 : 0) + (LogQuery("SecondExpansion", "PrivateRoom") ? 4 : 0); CharacterRefresh(Player, false); if (ManagementIsClubSlave()) CharacterNaked(Player); // Starts the game in the main hall while loading characters in the private room PrivateCharacter = []; PrivateCharacter.push(Player); let shouldSyncPrivate = false; if (C.PrivateCharacter != null) for (let P = 0; P < C.PrivateCharacter.length; P++) shouldSyncPrivate = PrivateLoadCharacter(C.PrivateCharacter[P]) || shouldSyncPrivate; if (shouldSyncPrivate) { ServerPrivateCharacterSync(); } SarahSetStatus(); // Fixes a few items var InventoryBeforeFixes = InventoryStringify(Player); LoginFixOwner(); LoginValidCollar(); LoginMistressItems(); LoginClubCard(); LoginStableItems(); LoginMaidItems(); LoginLoversItems(); LoginAsylumItems(); LoginCheatItems(); LoginValideBuyGroups(); PrisonRestoreConfiscatedItems(); CraftingLoadServer(CraftingDecompressed); // Only run this _after_ `Player.Inventory` is fully ready if (InventoryBeforeFixes != InventoryStringify(Player)) ServerPlayerInventorySync(); CharacterAppearanceValidate(Player); AsylumGGTSSAddItems(); ChatRoomCustomized = ((Player.OnlineSettings != null) && (Player.OnlineSettings.ShowRoomCustomization != null) && (Player.OnlineSettings.ShowRoomCustomization >= 2)); if (Player.Crafting.length > 80) Player.Crafting = Player.Crafting.slice(0, 80); } /** * Handles player login response data * @param {ServerLoginResponse} C - The Login response data - this will either be the player's character data if the * login was successful, or a string error message if the login failed. * @returns {void} Nothing */ function LoginResponse(C) { LoginCheckES2020(); if (typeof C === "string") { LoginSetStatus(C, true); return; } // The response must contain a name, an ID and an account name if (!(CommonIsObject(C) && typeof C.AccountName === "string" && !!C.AccountName.length && typeof C.Name === "string" && !!C.Name.length && typeof C.ID === "string" && !!C.ID.length)) { LoginSetStatus("ErrorLoadingCharacterData", true); return; } // In relog mode, we jump back to the previous screen, keeping the current game flow if (RelogData) { Player.CharacterID = C.ID.toString(); Player.OnlineID = C.ID.toString(); CurrentCharacter = RelogData.Character; if (RelogData.ChatRoomName) { CommonSetScreen("Online", "ChatSearch"); ServerSend("ChatRoomJoin", { Name: RelogData.ChatRoomName }); } else { const screen = /** @type {ScreenSpecifier} */([RelogData.Module, RelogData.Screen]); CommonSetScreen(...screen); } return; } CharacterCreatePlayer(); // In regular mode, we set the account properties for a new club session LoginSetupPlayer(C); // Enables the AFK timer for the current player only AfkTimerSetEnabled(Player.OnlineSettings.EnableAfkTimer); ActivitySetArousal(Player, 0); ActivityTimerProgress(Player, 0); NotificationLoad(); if (ManagementIsClubSlave()) CharacterNaked(Player); // We're done loading, now start the player in whatever screen is appropriate if (LogQuery("Locked", "Cell")) { // The player has been locked up, they must log back in the cell CommonSetScreen("Room", "Cell"); } else if ((Player.Infiltration?.Punishment?.Timer ?? 0) > CurrentTime) { // The player must log back in Pandora's Box prison PandoraWillpower = 0; InfiltrationDifficulty = Player.Infiltration.Punishment.Difficulty; CommonSetScreen("Room", "PandoraPrison"); } else if (LogQuery("Committed", "Asylum") || LogQuery("Isolated", "Asylum") || (AsylumGGTSGetLevel(Player) >= 6)) { // The player must log back in the asylum if (AsylumGGTSGetLevel(Player) <= 5) { AsylumEntranceWearPatientClothes(Player, true); } else { AsylumGGTSDroneDress(Player); } CommonSetScreen("Room", "AsylumBedroom"); } else if (LogValue("ForceGGTS", "Asylum") > 0) { // The owner is forcing the player to do GGTS CommonSetScreen("Room", "AsylumEntrance"); } else if (LogQuery("SleepCage", "Rule") && Player.IsOwned() === "npc" && PrivateOwnerInRoom()) { // The player must start in her room, in her cage InventoryRemove(Player, "ItemFeet"); InventoryRemove(Player, "ItemLegs"); Player.Cage = true; PoseSetActive(Player, "Kneel", true); CommonSetScreen("Room", "Private"); } else { CommonSetScreen("Room", "MainHall"); } } /** Check if the player's browser has ES2020 support */ function LoginCheckES2020() { try { // eslint-disable-next-line no-eval eval("({})?.foo"); // eslint-disable-next-line no-eval eval("null ?? null"); // eslint-disable-next-line no-eval eval("let a = null; a ??= 1;"); if (Array.prototype.flatMap === undefined) { throw new Error("Array.prototype.flatMap is undefined"); } } catch (error) { console.error(error); throw new Error(`Outdated browser error: If you see this message, it means that your browser lacks ES2020 support and will not be working in Bondage Club`); } } /** * Handles player click events on the character login screen * @returns {void} Nothing */ function LoginClick() { // Opens the cheat panel if (CheatAllow && MouseIn(825, 800, 350, 60)) { CommonSetScreen("Character", "Cheat"); } // Opens the password reset screen if (ServerIsConnected && MouseIn(825, 890, 350, 60)) { CommonSetScreen("Character", "PasswordReset"); } // If we must create a new character if (ServerIsConnected && MouseIn(825, 710, 350, 60)) { CommonSetScreen("Character", "Disclaimer"); } // Try to login if (MouseIn(775, 500, 200, 60)) { const Name = ElementValue("InputName"); const Password = ElementValue("InputPassword"); LoginDoLogin(Name, Password); } } /** * Handles player keyboard events on the character login screen, "enter" will login * @type {KeyboardEventListener} */ function LoginKeyDown(ev) { if (CommonKey.IsPressed(ev, "Enter")) { const Name = ElementValue("InputName"); const Password = ElementValue("InputPassword"); LoginDoLogin(Name, Password); return true; } return false; } /** * Attempt to log the user in based on their input username and password * @param {string} Name * @param {string} Password * @returns {void} Nothing */ function LoginDoLogin(Name, Password) { // Ensure the login request is not sent twice if (LoginSubmitted || !ServerIsConnected) return; if (!ServerAccountPasswordRegex.test(Name) || !ServerAccountPasswordRegex.test(Password)) { LoginSetStatus("InvalidNamePassword"); return; } LoginStatusReset(); LoginSubmitted = true; ServerSend("AccountLogin", { AccountName: Name, Password: Password }); } /** * Resets the login submission state * @returns {void} Nothing */ function LoginStatusReset() { LoginSubmitted = false; LoginQueuePosition = -1; LoginErrorMessage = ""; } /** * * @param {string} [ErrorMessage] - the login error message to set if the login is invalid - if not specified, will clear the login error message */ function LoginSetStatus(ErrorMessage = "", reset = false) { if (reset) { LoginStatusReset(); } LoginErrorMessage = ErrorMessage ?? ""; } /** * Retrieves the correct message key based on the current state of the login page * @returns {string | null} The current login status, or null if we're not currently attempting to log in */ function LoginGetStatus() { if (LoginErrorMessage) { return TextGet(LoginErrorMessage); } else if (!ServerIsConnected) { return TextGet("ConnectingToServer"); } else if (LoginQueuePosition !== -1) { return TextGet("LoginQueueWait").replace("QUEUE_POS", `${LoginQueuePosition}`); } else if (LoginSubmitted) { return TextGet("ValidatingNamePassword"); } else { return null; } } /** * Exit function - called when leaving the login page * @type {ScreenFunctions["Exit"]} */ function LoginExit() { } /** * Unload function - called when the login page unloads */ function LoginUnload() { ElementRemove("InputName"); ElementRemove("InputPassword"); ElementRemove("LanguageDropdown"); CharacterDelete(LoginCharacter); LoginCharacter = null; }