"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;
}