mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2025-04-25 17:59:34 +00:00
6391 lines
240 KiB
JavaScript
6391 lines
240 KiB
JavaScript
"use strict";
|
||
|
||
/**
|
||
* An enum for the options for chat room spaces
|
||
* @satisfies {Record<ChatRoomSpaceLabel, ServerChatRoomSpace>}
|
||
*/
|
||
const ChatRoomSpaceType = /** @type {const} */({
|
||
MIXED: "X",
|
||
FEMALE_ONLY: "",
|
||
MALE_ONLY: "M",
|
||
ASYLUM: "Asylum",
|
||
});
|
||
/** @type {Record<ChatRoomVisibilityModeLabel, ServerChatRoomRole[]>} */
|
||
const ChatRoomVisibilityMode = {
|
||
PUBLIC: ["All"],
|
||
ADMIN_WHITELIST: ["Admin", "Whitelist"],
|
||
ADMIN: ["Admin"],
|
||
UNLISTED: [],
|
||
};
|
||
/** @type {Record<ChatRoomAccessModeLabel, ServerChatRoomRole[]>} */
|
||
const ChatRoomAccessMode = {
|
||
PUBLIC: ["All"],
|
||
ADMIN_WHITELIST: ["Admin", "Whitelist"],
|
||
ADMIN: ["Admin"],
|
||
};
|
||
|
||
var ChatRoomBackground = "";
|
||
/**
|
||
* The data for the current chatroom, as recieved from the server.
|
||
* @type {null | ServerChatRoomData}
|
||
*/
|
||
let ChatRoomData = null;
|
||
/**
|
||
* The list of chatroom characters.
|
||
* This is unpacked characters from the data recieved from the server in {@link ChatRoomData.Character}.
|
||
* @type {Character[]}
|
||
*/
|
||
var ChatRoomCharacter = [];
|
||
/** @type {ChatRoomChatLogEntry[]} */
|
||
var ChatRoomChatLog = [];
|
||
var ChatRoomLastMessage = [""];
|
||
var ChatRoomLastMessageIndex = 0;
|
||
/** @type {number} */
|
||
var ChatRoomTargetMemberNumber = -1;
|
||
/** @type {ChatRoomOwnershipOption} */
|
||
var ChatRoomOwnershipOption = "";
|
||
/** @type {ChatRoomLovershipOption} */
|
||
var ChatRoomLovershipOption = "";
|
||
/** @deprecated Check whether ChatRoomData is set or not instead */
|
||
var ChatRoomPlayerCanJoin = false;
|
||
var ChatRoomMoneyForOwner = 0;
|
||
/** @type {number[]} */
|
||
var ChatRoomQuestGiven = [];
|
||
/** @type {ServerChatRoomSpace} */
|
||
var ChatRoomSpace = ChatRoomSpaceType.MIXED;
|
||
/** @type {ServerChatRoomGame} */
|
||
var ChatRoomGame = "";
|
||
var ChatRoomHelpSeen = false;
|
||
var ChatRoomAllowCharacterUpdate = true;
|
||
var ChatRoomStruggleAssistBonus = 0;
|
||
var ChatRoomStruggleAssistTimer = 0;
|
||
var ChatRoomStruggleData = null;
|
||
/**
|
||
* The timer started when a slowed player attempts to leave
|
||
* @type {number}
|
||
*/
|
||
var ChatRoomSlowtimer = 0;
|
||
/**
|
||
* Whether someone attempted to stop the player in the middle of a slow-leave
|
||
* @type {boolean}
|
||
*/
|
||
var ChatRoomSlowStop = false;
|
||
/**
|
||
* Default position of the chat log field
|
||
* @deprecated - superseded by and incorporated into {@link ChatRoomDivRect}
|
||
*/
|
||
var ChatRoomChatLogRect = /** @type {never} */([1005, 66, 988, 835]);
|
||
/**
|
||
* Default position of the chat input field
|
||
* @deprecated - superseded by and incorporated into {@link ChatRoomDivRect}
|
||
*/
|
||
var ChatRoomChatInputRect = /** @type {never} */([1453, 953, 895, 90]);
|
||
/**
|
||
* Default position of the chat input length label
|
||
* @deprecated - superseded by and incorporated into {@link ChatRoomDivRect}
|
||
*/
|
||
var ChatRoomChatLengthLabelRect = /** @type {never} */([1764, 970, 120, 82]);
|
||
|
||
/**
|
||
* Default position of the entire chat panel
|
||
* @type {RectTuple}
|
||
*/
|
||
var ChatRoomDivRect = [1005, 60, 988, 940];
|
||
|
||
var ChatRoomChatHidden = false;
|
||
/**
|
||
* The chatroom characters that were drawn in the last frame.
|
||
* Used for limiting the "fov". Characters come from {@link ChatRoomCharacter}
|
||
* @type {Character[]}
|
||
*/
|
||
var ChatRoomCharacterDrawlist = [];
|
||
/**
|
||
* If non-empty, ChatRoomCharacterDrawlist will be filtered (after immersion removals) to only include the player and these character(s).
|
||
* Used for the /focus command. List will be automatically removed if characters are removed from the room.
|
||
* @type {Character[]}
|
||
*/
|
||
var ChatRoomDrawFocusList = [];
|
||
/**
|
||
* The list of characters currently impacted (not drawn) by sensory deprivation in the chat room
|
||
* Used as a check for whether to apply further sense dep effects to a given character (i.e. name removal, message, hiding, etc). Characters from {@link ChatRoomCharacter}
|
||
* @type {Character[]}
|
||
*/
|
||
var ChatRoomImpactedBySenseDep = [];
|
||
var ChatRoomSenseDepBypass = false;
|
||
var ChatRoomGetUpTimer = 0;
|
||
/**
|
||
* The complete data to update a recreated room with once the creation is successful
|
||
* @type {ChatRoomSettings}
|
||
* */
|
||
var ChatRoomNewRoomToUpdate = null;
|
||
var ChatRoomNewRoomToUpdateTimer = 0;
|
||
/**
|
||
* The list of MemberNumbers whose characters we're holding the leash of
|
||
* @type {number[]}
|
||
*/
|
||
var ChatRoomLeashList = [];
|
||
/**
|
||
* The MemberNumber of the character holding our leash
|
||
* @type {number|null}
|
||
*/
|
||
var ChatRoomLeashPlayer = null;
|
||
/**
|
||
* The room name to join when being leashed
|
||
* @type {string}
|
||
*/
|
||
var ChatRoomJoinLeash = "";
|
||
var ChatRoomCustomized = false;
|
||
var ChatRoomCustomBackground = "";
|
||
var ChatRoomCustomFilter = "";
|
||
var ChatRoomCustomSizeMode = null;
|
||
|
||
/**
|
||
* The list of chat room views
|
||
* @type {Record<string, ChatRoomView>}
|
||
*/
|
||
var ChatRoomViews = {
|
||
Character: {
|
||
Run: ChatRoomCharacterViewRun,
|
||
Draw: ChatRoomCharacterViewDraw,
|
||
DrawUi: ChatRoomCharacterViewDrawUi,
|
||
DisplayMessage: ChatRoomCharacterViewDisplayMessage,
|
||
Click: ChatRoomCharacterViewClick,
|
||
KeyDown: ChatRoomCharacterViewKeyDown,
|
||
CanLeave: ChatRoomCharacterViewCanLeave,
|
||
Screenshot: ChatRoomCharacterViewScreenshot
|
||
},
|
||
Map: {
|
||
Activate: ChatRoomMapViewActivate,
|
||
Deactivate: ChatRoomMapViewDeactivate,
|
||
Run: ChatRoomMapViewRun,
|
||
Draw: ChatRoomMapViewDraw,
|
||
DrawUi: ChatRoomMapViewDrawUi,
|
||
DisplayMessage: ChatRoomMapViewDisplayMessage,
|
||
Click: ChatRoomMapViewClick,
|
||
MouseDown: ChatRoomMapViewMouseDown,
|
||
MouseUp: ChatRoomMapViewMouseUp,
|
||
MouseMove: ChatRoomMapViewMouseMove,
|
||
MouseWheel: ChatRoomMapViewMouseWheel,
|
||
KeyDown: ChatRoomMapViewKeyDown,
|
||
SyncRoomProperties: ChatRoomMapViewSyncRoomProperties,
|
||
CanStartWhisper: ChatRoomMapViewCanStartWhisper,
|
||
CanLeave: ChatRoomMapViewCanLeave,
|
||
Screenshot: ChatRoomMapViewScreenshot
|
||
}
|
||
};
|
||
/**
|
||
* The active chat room view
|
||
* @type {ChatRoomView}
|
||
*/
|
||
var ChatRoomActiveView = ChatRoomViews[ChatRoomCharacterViewName];
|
||
|
||
/**
|
||
* Chances of a chat message popping up reminding you of some stimulation.
|
||
*
|
||
* @type {Record<StimulationAction, StimulationEvent>}
|
||
*/
|
||
const ChatRoomStimulationEvents = {
|
||
Kneel: {
|
||
Chance: 0.1,
|
||
ArousalScaling: 0.8,
|
||
VibeScaling: 0.0,
|
||
InflationScaling: 0.1,
|
||
},
|
||
Walk: {
|
||
Chance: 0.33,
|
||
ArousalScaling: 0.67,
|
||
VibeScaling: 0.8,
|
||
InflationScaling: 0.5,
|
||
},
|
||
Struggle: {
|
||
Chance: 0.05,
|
||
ArousalScaling: 0.2,
|
||
VibeScaling: 0.3,
|
||
InflationScaling: 0.2,
|
||
},
|
||
StruggleFail: {
|
||
Chance: 0.4,
|
||
ArousalScaling: 0.4,
|
||
VibeScaling: 0.3,
|
||
InflationScaling: 0.4,
|
||
},
|
||
Talk: {
|
||
Chance: 0,
|
||
TalkChance: 0.3,
|
||
ArousalScaling: 0.22,
|
||
},
|
||
};
|
||
|
||
const ChatRoomArousalMsg_Chance = {
|
||
"Kneel" : 0.1,
|
||
"Walk" : 0.33,
|
||
"StruggleFail" : 0.4,
|
||
"StruggleAction" : 0.05,
|
||
"Gag" : 0,
|
||
};
|
||
const ChatRoomArousalMsg_ChanceScaling = {
|
||
"Kneel" : 0.8,
|
||
"Walk" : 0.67,
|
||
"StruggleFail" : 0.4,
|
||
"StruggleAction" : 0.2,
|
||
"Gag" : 0,
|
||
};
|
||
const ChatRoomArousalMsg_ChanceVibeMod = {
|
||
"Kneel" : 0.0,
|
||
"Walk" : 0.8,
|
||
"StruggleFail" : 0.6,
|
||
"StruggleAction" : 0.3,
|
||
"Gag" : 0,
|
||
};
|
||
const ChatRoomArousalMsg_ChanceInflationMod = {
|
||
"Kneel" : 0.1,
|
||
"Walk" : 0.5,
|
||
"StruggleFail" : 0.4,
|
||
"StruggleAction" : 0.2,
|
||
"Gag" : 0,
|
||
};
|
||
const ChatRoomArousalMsg_ChanceGagMod = {
|
||
"Kneel" : 0,
|
||
"Walk" : 0,
|
||
"StruggleFail" : 0,
|
||
"StruggleAction" : 0,
|
||
"Gag" : 0.3,
|
||
};
|
||
|
||
var ChatRoomHideIconState = 0;
|
||
/**
|
||
* The list of buttons in the top-right
|
||
* @type {string[]}
|
||
* */
|
||
var ChatRoomMenuButtons = [];
|
||
let ChatRoomFontSize = 30;
|
||
const ChatRoomFontSizes = {
|
||
Small: 28,
|
||
Medium: 36,
|
||
Large: 44,
|
||
};
|
||
|
||
/** Sets whether an add/remove for one list automatically triggers an add/remove for another list */
|
||
const ChatRoomListOperationTriggers = () => [
|
||
{
|
||
list: Player.WhiteList, adding: true, triggers: [
|
||
{ list: Player.BlackList, add: false },
|
||
{ list: Player.GhostList, add: false }
|
||
]
|
||
},
|
||
{
|
||
list: Player.BlackList, adding: true, triggers: [
|
||
{ list: Player.WhiteList, add: false }
|
||
]
|
||
},
|
||
{
|
||
list: Player.GhostList, adding: true, triggers: [
|
||
{ list: Player.WhiteList, add: false },
|
||
{ list: Player.BlackList, add: true }
|
||
]
|
||
},
|
||
{
|
||
list: Player.GhostList, adding: false, triggers: [
|
||
{ list: Player.BlackList, add: false }
|
||
]
|
||
}
|
||
];
|
||
|
||
/**
|
||
* Chat room resize manager object: Handles resize events for the chat log.
|
||
* @constant
|
||
* The chat room resize manager object. Contains the functions and properties required to handle
|
||
* resize events.
|
||
*/
|
||
let ChatRoomResizeManager = {
|
||
atStart: true, // Is this the first event in a chain of resize events?
|
||
/** @type {null | number} */
|
||
timer: null, // Timer that triggers the end function after no resize events have been received recently.
|
||
timeOut: 200, // The amount of milliseconds that has to pass before the chain of resize events is considered over and the timer is called.
|
||
ChatRoomScrollPercentage: 0, // Height of the chat log scroll bar before the first resize event occurs, as a percentage.
|
||
ChatLogScrolledToEnd: false, // Is the chat log scrolled all the way to the end before the first resize event occurs?
|
||
|
||
// Triggered by resize event
|
||
ChatRoomResizeEvent : function() {
|
||
if (document.getElementById("TextAreaChatLog")?.style.display === "none") {
|
||
return;
|
||
}
|
||
if(ChatRoomResizeManager.atStart) { // Run code for the first resize event in a chain of resize events.
|
||
ChatRoomResizeManager.ChatRoomScrollPercentage = ElementGetScrollPercentage("TextAreaChatLog");
|
||
ChatRoomResizeManager.ChatLogScrolledToEnd = ElementIsScrolledToEnd("TextAreaChatLog");
|
||
ChatRoomResizeManager.atStart = false;
|
||
}
|
||
|
||
// Reset timer if an event was received recently.
|
||
if (ChatRoomResizeManager.timer) clearTimeout(ChatRoomResizeManager.timer);
|
||
ChatRoomResizeManager.timer = setTimeout(ChatRoomResizeManager.ChatRoomResizeEventsEnd, ChatRoomResizeManager.timeOut);
|
||
},
|
||
|
||
// Triggered by ChatRoomResizeManager.timer at the end of a chain of resize events
|
||
ChatRoomResizeEventsEnd : function(){
|
||
var TextAreaChatLog = document.getElementById("TextAreaChatLog");
|
||
|
||
if (TextAreaChatLog != null) {
|
||
// Scrolls to the position held before the resize events.
|
||
if (ChatRoomResizeManager.ChatLogScrolledToEnd) ElementScrollToEnd("TextAreaChatLog"); // Prevents drift away from the end of the chat log.
|
||
else TextAreaChatLog.scrollTop = (ChatRoomResizeManager.ChatRoomScrollPercentage * TextAreaChatLog.scrollHeight) - TextAreaChatLog.clientHeight;
|
||
}
|
||
ChatRoomResizeManager.atStart = true;
|
||
},
|
||
};
|
||
|
||
/**
|
||
* Activates the chat room view with the passed name
|
||
* @param {string} viewName - The name of the view to activate
|
||
* @returns {void}
|
||
*/
|
||
function ChatRoomActivateView(viewName)
|
||
{
|
||
if(!(viewName in ChatRoomViews)) { throw Error(`Tried to change to not existing view ${viewName}`); }
|
||
if(ChatRoomActiveView.Deactivate) ChatRoomActiveView.Deactivate();
|
||
ChatRoomActiveView = ChatRoomViews[viewName];
|
||
if(ChatRoomActiveView.Activate) ChatRoomActiveView.Activate();
|
||
}
|
||
|
||
/**
|
||
* Indicates if the chat room view with the passed name is active or not
|
||
* @param {string} viewName - The name of the view to check
|
||
* @returns {boolean} - TRUE if the chat room character view is active, false if not
|
||
*/
|
||
function ChatRoomIsViewActive(viewName)
|
||
{
|
||
if(!(viewName in ChatRoomViews)) { return false; }
|
||
return ChatRoomActiveView === ChatRoomViews[viewName];
|
||
}
|
||
|
||
/**
|
||
* Checks if the player can add the current character to her whitelist.
|
||
* @returns {boolean} - TRUE if the current character is not in the player's whitelist nor blacklist.
|
||
*/
|
||
function ChatRoomCanAddWhiteList() { return ((CurrentCharacter != null) && (CurrentCharacter.MemberNumber != null) && (Player.WhiteList.indexOf(CurrentCharacter.MemberNumber) < 0) && (Player.BlackList.indexOf(CurrentCharacter.MemberNumber) < 0)); }
|
||
/**
|
||
* Checks if the player can add the current character to her blacklist.
|
||
* @returns {boolean} - TRUE if the current character is not in the player's whitelist nor blacklist.
|
||
*/
|
||
function ChatRoomCanAddBlackList() { return ((CurrentCharacter != null) && (CurrentCharacter.MemberNumber != null) && (Player.WhiteList.indexOf(CurrentCharacter.MemberNumber) < 0) && (Player.BlackList.indexOf(CurrentCharacter.MemberNumber) < 0)); }
|
||
/**
|
||
* Checks if the player can remove the current character from her whitelist.
|
||
* @returns {boolean} - TRUE if the current character is in the player's whitelist, but not her blacklist.
|
||
*/
|
||
function ChatRoomCanRemoveWhiteList() { return ((CurrentCharacter != null) && (CurrentCharacter.MemberNumber != null) && (Player.WhiteList.indexOf(CurrentCharacter.MemberNumber) >= 0)); }
|
||
/**
|
||
* Checks if the player can remove the current character from her blacklist.
|
||
* @returns {boolean} - TRUE if the current character is in the player's blacklist, but not her whitelist.
|
||
*/
|
||
function ChatRoomCanRemoveBlackList() { return ((CurrentCharacter != null) && (CurrentCharacter.MemberNumber != null) && (Player.BlackList.indexOf(CurrentCharacter.MemberNumber) >= 0)); }
|
||
/**
|
||
* Checks if the player can add the current character to her friendlist
|
||
* @returns {boolean} - TRUE if the current character is not in the player's friendlist yet.
|
||
*/
|
||
function ChatRoomCanAddFriend() { return ((CurrentCharacter != null) && (CurrentCharacter.MemberNumber != null) && (Player.FriendList.indexOf(CurrentCharacter.MemberNumber) < 0)); }
|
||
/**
|
||
* Checks if the player can remove the current character from her friendlist.
|
||
* @returns {boolean} - TRUE if the current character is in the player's friendlist.
|
||
*/
|
||
function ChatRoomCanRemoveFriend() { return ((CurrentCharacter != null) && (CurrentCharacter.MemberNumber != null) && (Player.FriendList.indexOf(CurrentCharacter.MemberNumber) >= 0)); }
|
||
/**
|
||
* Checks if the player can add the current character to her ghostlist
|
||
* @returns {boolean} - TRUE if the current character is not in the player's ghostlist yet.
|
||
*/
|
||
function ChatRoomCanAddGhost() { return ((CurrentCharacter != null) && (CurrentCharacter.MemberNumber != null) && (Player.GhostList.indexOf(CurrentCharacter.MemberNumber) < 0)); }
|
||
/**
|
||
* Checks if the player can remove the current character from her ghostlist.
|
||
* @returns {boolean} - TRUE if the current character is in the player's ghostlist.
|
||
*/
|
||
function ChatRoomCanRemoveGhost() { return ((CurrentCharacter != null) && (CurrentCharacter.MemberNumber != null) && (Player.GhostList.indexOf(CurrentCharacter.MemberNumber) >= 0)); }
|
||
/**
|
||
* Checks if the player can change the current character's clothes
|
||
* @returns {boolean} - TRUE if the player can change the character's clothes and is allowed to.
|
||
*/
|
||
function DialogCanChangeClothes() {
|
||
return (
|
||
["ChatRoom", "MainHall", "Private", "Photographic"].includes(CurrentScreen)
|
||
&& CurrentCharacter != null
|
||
&& Player.CanChangeClothesOn(CurrentCharacter)
|
||
&& !InventoryIsBlockedByDistance(CurrentCharacter)
|
||
);
|
||
}
|
||
/**
|
||
* Checks if the specified owner option is available.
|
||
* @param {ChatRoomOwnershipOption} Option - The option to check for availability
|
||
* @returns {boolean} - TRUE if the current ownership option is the specified one.
|
||
*/
|
||
function ChatRoomOwnershipOptionIs(Option) { return (Option == ChatRoomOwnershipOption); }
|
||
/**
|
||
* Checks if the specified lover option is available.
|
||
* @param {ChatRoomLovershipOption} Option - The option to check for availability
|
||
* @returns {boolean} - TRUE if the current lover option is the specified one.
|
||
*/
|
||
function ChatRoomLovershipOptionIs(Option) { return (Option == ChatRoomLovershipOption); }
|
||
/**
|
||
* Returns TRUE if the room customization button can be used
|
||
* @returns {boolean} - TRUE if can be used
|
||
*/
|
||
function ChatRoomCustomizationButton() { return (((ChatRoomData != null) && (ChatRoomData.Custom != null)) && ((Player.OnlineSettings == null) || (Player.OnlineSettings.ShowRoomCustomization == null) || (Player.OnlineSettings.ShowRoomCustomization == 1) || (Player.OnlineSettings.ShowRoomCustomization == 2))); }
|
||
/**
|
||
* Checks if the player can take a drink from the current character's tray.
|
||
* @returns {boolean} - TRUE if the current character is wearing a drinks tray and the player can interact.
|
||
*/
|
||
function ChatRoomCanTakeDrink() { return ((CurrentCharacter != null) && (CurrentCharacter.MemberNumber != null) && !CurrentCharacter.IsPlayer() && Player.CanInteract() && (InventoryGet(CurrentCharacter, "ItemMisc") != null) && (InventoryGet(CurrentCharacter, "ItemMisc").Asset.Name == "WoodenMaidTrayFull")); }
|
||
/**
|
||
* Checks if the current character is owned by the player.
|
||
* @returns {boolean} - TRUE if the current character is owned by the player.
|
||
*/
|
||
function ChatRoomIsCollaredByPlayer() { return (CurrentCharacter != null && CurrentCharacter.IsOwnedByPlayer() && CurrentCharacter.IsFullyOwned()); }
|
||
/**
|
||
* Checks if the current character is owned by the player. (Including trial)
|
||
* @returns {boolean} - TRUE if the current character is owned by the player.
|
||
*/
|
||
function ChatRoomIsOwnedByPlayer() { return (CurrentCharacter != null && CurrentCharacter.IsOwnedByPlayer()); }
|
||
/**
|
||
* Checks if the current character is wearing any collar.
|
||
* @returns {boolean} - TRUE if the current character is owned by the player.
|
||
*/
|
||
function ChatRoomIsWearingCollar() { return CurrentCharacter != null && InventoryGet(CurrentCharacter, "ItemNeck") !== null; }
|
||
/**
|
||
* Checks if the current character is lover of the player.
|
||
* @returns {boolean} - TRUE if the current character is lover of the player.
|
||
*/
|
||
function ChatRoomIsLoverOfPlayer() { return ((CurrentCharacter != null) && CurrentCharacter.GetLoversNumbers().includes(Player.MemberNumber)); }
|
||
/**
|
||
* Checks if the current character can serve drinks.
|
||
* @returns {boolean} - TRUE if the character is a maid and is free.
|
||
*/
|
||
function ChatRoomCanServeDrink() { return ((CurrentCharacter != null) && CurrentCharacter.CanWalk() && (ReputationCharacterGet(CurrentCharacter, "Maid") > 0) && CurrentCharacter.CanTalk()); }
|
||
/**
|
||
* Checks if the player can give a money envelope to her owner
|
||
* @returns {boolean} - TRUE if the current character is the owner of the player, and the player has the envelope
|
||
*/
|
||
function ChatRoomCanGiveMoneyForOwner() { return (CurrentCharacter != null && Player.IsOwnedByCharacter(CurrentCharacter) && Player.IsFullyOwned() && ChatRoomMoneyForOwner > 0); }
|
||
/**
|
||
* Checks if a given character is a chatroom admin.
|
||
* @param {Character} C - The character to check
|
||
* @returns {boolean} - TRUE if the character is an admin of the current chatroom.
|
||
*/
|
||
function ChatRoomCharacterIsAdmin(C) { return ((ChatRoomData != null && ChatRoomData.Admin != null) && (C != null && C.MemberNumber != null) && (ChatRoomData.Admin.indexOf(C.MemberNumber) >= 0)); }
|
||
/**
|
||
* Checks if the player is a chatroom admin.
|
||
* @returns {boolean} - TRUE if the player is an admin of the current chatroom.
|
||
*/
|
||
function ChatRoomPlayerIsAdmin() { return ChatRoomCharacterIsAdmin(Player); }
|
||
/**
|
||
* Checks if the current character is an admin of the chatroom.
|
||
* @returns {boolean} - TRUE if the current character is an admin.
|
||
*/
|
||
function ChatRoomCurrentCharacterIsAdmin() { return ChatRoomCharacterIsAdmin(CurrentCharacter); }
|
||
/**
|
||
* Checks if a given character is in the chatroom whitelist.
|
||
* @param {Character} C - The character to check
|
||
* @returns {boolean} - TRUE if the character is in the chatroom whitelist.
|
||
*/
|
||
function ChatRoomCharacterIsWhitelisted(C) { return ((ChatRoomData != null && ChatRoomData.Whitelist != null) && (C != null && C.MemberNumber != null) && (ChatRoomData.Whitelist.indexOf(C.MemberNumber) >= 0)); }
|
||
/**
|
||
* Checks if the player is in the chatroom whitelist.
|
||
* @returns {boolean} - TRUE if the player is in the chatroom whitelist.
|
||
*/
|
||
function ChatRoomPlayerIsWhitelisted() { return ChatRoomCharacterIsWhitelisted(Player); }
|
||
/**
|
||
* Checks if the current character is in the chatroom whitelist.
|
||
* @returns {boolean} - TRUE if the current character is in the chatroom whitelist.
|
||
*/
|
||
function ChatRoomCurrentCharacterIsWhitelisted() { return ChatRoomCharacterIsWhitelisted(CurrentCharacter); }
|
||
/**
|
||
* Checks if the player is currently in focus mode (non-empty {@link ChatRoomDrawFocusList})
|
||
* @returns {boolean} - TRUE if the player is in focus mode
|
||
*/
|
||
function ChatRoomPlayerIsInDrawFocus() { return ChatRoomDrawFocusList.length > 0; }
|
||
/**
|
||
* Checks if the current character is in the player's focuslist (/focus).
|
||
* @returns {boolean} - TRUE if the current character is in the player's focuslist.
|
||
*/
|
||
function ChatRoomCurrentCharacterInDrawFocusList() { return ((CurrentCharacter != null) && (ChatRoomDrawFocusList.indexOf(CurrentCharacter) >= 0)); }
|
||
/**
|
||
* Checks if the room allows the photograph feature to be used.
|
||
* @returns {boolean} - TRUE if the player can take a photo.
|
||
*/
|
||
function DialogCanTakePhotos() { return (ChatRoomData && ChatRoomData.BlockCategory && !ChatRoomData.BlockCategory.includes("Photos")) || !ChatRoomData; }
|
||
/**
|
||
* Checks if the current character has a lucky wheel to spin
|
||
* @returns {boolean} - TRUE if the player can take a photo.
|
||
*/
|
||
function ChatRoomCanStartWheelFortune() { return (CurrentCharacter != null) && InventoryIsWorn(CurrentCharacter, "WheelFortune", "ItemDevices"); }
|
||
/**
|
||
* Starts the current character lucky wheel
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomStartWheelFortune() {
|
||
if ((CurrentCharacter == null) || !InventoryIsWorn(CurrentCharacter, "WheelFortune", "ItemDevices")) return;
|
||
WheelFortuneReturnScreen = CommonGetScreen();
|
||
WheelFortuneBackground = ChatRoomData.Background;
|
||
WheelFortuneCharacter = CurrentCharacter;
|
||
DialogLeave();
|
||
CommonSetScreen("MiniGame", "WheelFortune");
|
||
}
|
||
/**
|
||
* If the player is owner and wearing a wheel of fortune, she can force her sub to spin it
|
||
* @returns {boolean} - TRUE if the player can take a photo.
|
||
*/
|
||
function ChatRoomCanForceWheelFortune() { return (CurrentCharacter != null) && CurrentCharacter.IsOwnedByPlayer() && InventoryIsWorn(Player, "WheelFortune", "ItemDevices"); }
|
||
|
||
/**
|
||
* Checks if the player can start searching a player
|
||
* @returns {boolean} - Returns TRUE if the player can start searching a player
|
||
*/
|
||
function ChatRoomCanTakeSuitcase() {
|
||
return ChatRoomCarryingBounty(CurrentCharacter) && !CurrentCharacter.CanInteract();
|
||
}
|
||
/**
|
||
* Checks if the player can start searching a player
|
||
* @returns {boolean} - Returns TRUE if the player can start searching a player
|
||
*/
|
||
function ChatRoomCanTakeSuitcaseOpened() {
|
||
return ChatRoomCarryingBountyOpened(CurrentCharacter) && !CurrentCharacter.CanInteract();
|
||
}
|
||
/**
|
||
* Checks if the player carries a bounty
|
||
* @param {Character} C - The character to search
|
||
* @returns {boolean} - Returns TRUE if the player can start searching a player
|
||
*/
|
||
function ChatRoomCarryingBounty(C) {
|
||
return (ReputationGet("Kidnap") > 0 && Player.CanInteract() && C.AllowItem != false && InventoryIsWorn(C,"BountySuitcase", "ItemMisc"));
|
||
}
|
||
/**
|
||
* Checks if the player carries an opened bounty
|
||
* @param {Character} C - The character to search
|
||
* @returns {boolean} - Returns TRUE if the player can start searching a player
|
||
*/
|
||
function ChatRoomCarryingBountyOpened(C) {
|
||
return (ReputationGet("Kidnap") > 0 && Player.CanInteract() && C.AllowItem != false && InventoryIsWorn(C,"BountySuitcaseEmpty", "ItemMisc"));
|
||
}
|
||
/**
|
||
* Checks if the player can start searching a player but the player is unbound
|
||
* @returns {boolean} - Returns TRUE if the player can start searching a player
|
||
*/
|
||
function ChatRoomCantTakeSuitcase() {
|
||
return (Player.CanInteract() && CurrentCharacter.CanInteract() && CurrentCharacter.AllowItem && InventoryIsWorn(CurrentCharacter,"BountySuitcase", "ItemMisc"));
|
||
}
|
||
/**
|
||
* Checks if the player can start searching a player but the player is unbound
|
||
* @returns {boolean} - Returns TRUE if the player can start searching a player
|
||
*/
|
||
function ChatRoomCantTakeSuitcaseOpened() {
|
||
return (Player.CanInteract() && CurrentCharacter.CanInteract() && CurrentCharacter.AllowItem && InventoryIsWorn(CurrentCharacter,"BountySuitcaseEmpty", "ItemMisc"));
|
||
}
|
||
/**
|
||
* Attempts to take the suitcase from the current player
|
||
* @returns {void}
|
||
*/
|
||
function ChatRoomTryToTakeSuitcase() {
|
||
ServerSend("ChatRoomChat", { Content: "TakeSuitcase", Type: "Hidden", Target: CurrentCharacter.MemberNumber});
|
||
|
||
if (KidnapLeagueOnlineBountyTarget == 0) {
|
||
KidnapLeagueOnlineBountyTargetStartedTime = CommonTime();
|
||
}
|
||
KidnapLeagueOnlineBountyTarget = CurrentCharacter.MemberNumber;
|
||
DialogLeave();
|
||
}
|
||
/**
|
||
* Receives money from the suitcase
|
||
* @returns {void}
|
||
*/
|
||
function ChatRoomReceiveSuitcaseMoney() {
|
||
let money = Math.max(1, Math.ceil(15 * Math.min(1, Math.max(0, (CommonTime() - KidnapLeagueOnlineBountyTargetStartedTime)/KidnapLeagueSearchFinishDuration))));
|
||
CharacterChangeMoney(Player, money);
|
||
const Dictionary = new DictionaryBuilder()
|
||
.text("MONEYAMOUNT", Math.ceil(money).toString())
|
||
.build();
|
||
|
||
ChatRoomMessage({ Content: "OnlineBountySuitcaseFinish", Type: "Action", Dictionary: Dictionary, Sender: Player.MemberNumber });
|
||
KidnapLeagueOnlineBountyTarget = 0;
|
||
KidnapLeagueOnlineBountyTargetStartedTime = 0;
|
||
}
|
||
|
||
/**
|
||
* Checks if the player can give the target character her high security keys.
|
||
* @returns {boolean} - TRUE if the player can interact and is allowed to interact with the current character.
|
||
*/
|
||
function ChatRoomCanGiveHighSecurityKeys() {
|
||
if (Player.Appearance != null)
|
||
for (let A = 0; A < Player.Appearance.length; A++)
|
||
if (Player.Appearance[A].Asset && Player.Appearance[A].Property && InventoryGetLock(Player.Appearance[A]) && InventoryGetLock(Player.Appearance[A]).Asset.ExclusiveUnlock
|
||
&& (Player.Appearance[A].Property.MemberNumberListKeys)
|
||
&& (Player.Appearance[A].Property.MemberNumberListKeys
|
||
&& CommonConvertStringToArray("" + Player.Appearance[A].Property.MemberNumberListKeys).indexOf(Player.MemberNumber) >= 0
|
||
&& CommonConvertStringToArray("" + Player.Appearance[A].Property.MemberNumberListKeys).indexOf(CurrentCharacter.MemberNumber) < 0)) // Make sure you have a lock they dont have the keys to
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Checks if the player can give the target character her high security keys, while also removing the ones from her
|
||
* possession
|
||
* @returns {boolean} - TRUE if the player can interact and is allowed to interact with the current character.
|
||
*/
|
||
function ChatRoomCanGiveHighSecurityKeysAll() {
|
||
if (Player.Appearance != null)
|
||
for (let A = 0; A < Player.Appearance.length; A++)
|
||
if (Player.Appearance[A].Asset && Player.Appearance[A].Property && InventoryGetLock(Player.Appearance[A]) && InventoryGetLock(Player.Appearance[A]).Asset.ExclusiveUnlock
|
||
&& (Player.Appearance[A].Property.MemberNumberListKeys || (!Player.Appearance[A].Property.MemberNumberListKeys && Player.Appearance[A].Property.LockMemberNumber == Player.MemberNumber))
|
||
&& (!Player.Appearance[A].Property.MemberNumberListKeys
|
||
|| (CommonConvertStringToArray("" + Player.Appearance[A].Property.MemberNumberListKeys).indexOf(Player.MemberNumber) >= 0))) // Make sure you have a lock they dont have the keys to
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
function ChatRoomGiveHighSecurityKeys() {
|
||
var C = Player;
|
||
if (C.Appearance != null)
|
||
for (let A = 0; A < C.Appearance.length; A++)
|
||
if (C.Appearance[A].Asset && C.Appearance[A].Property && InventoryGetLock(Player.Appearance[A]) && InventoryGetLock(Player.Appearance[A]).Asset.ExclusiveUnlock
|
||
&& C.Appearance[A].Property.MemberNumberListKeys
|
||
&& CommonConvertStringToArray("" + C.Appearance[A].Property.MemberNumberListKeys).indexOf(Player.MemberNumber) >= 0
|
||
&& CommonConvertStringToArray("" + C.Appearance[A].Property.MemberNumberListKeys).indexOf(CurrentCharacter.MemberNumber) < 0) // Make sure you have a lock they dont have the keys to
|
||
C.Appearance[A].Property.MemberNumberListKeys = C.Appearance[A].Property.MemberNumberListKeys + "," + CurrentCharacter.MemberNumber;
|
||
CharacterRefresh(Player);
|
||
ChatRoomCharacterUpdate(Player);
|
||
}
|
||
function ChatRoomGiveHighSecurityKeysAll() {
|
||
var C = Player;
|
||
if (C.Appearance != null)
|
||
for (let A = 0; A < C.Appearance.length; A++)
|
||
if (C.Appearance[A].Asset && C.Appearance[A].Property && InventoryGetLock(Player.Appearance[A]) && InventoryGetLock(Player.Appearance[A]).Asset.ExclusiveUnlock
|
||
&& (C.Appearance[A].Property.MemberNumberListKeys || (!C.Appearance[A].Property.MemberNumberListKeys && C.Appearance[A].Property.LockMemberNumber == Player.MemberNumber))
|
||
&& (!C.Appearance[A].Property.MemberNumberListKeys || (C.Appearance[A].Property.MemberNumberListKeys
|
||
&& CommonConvertStringToArray("" + C.Appearance[A].Property.MemberNumberListKeys).indexOf(Player.MemberNumber) >= 0))) // Make sure you have a lock they dont have the keys to
|
||
{
|
||
if (C.Appearance[A].Property.MemberNumberListKeys) {
|
||
var list = CommonConvertStringToArray("" + C.Appearance[A].Property.MemberNumberListKeys);
|
||
|
||
if (list) {
|
||
list = list.filter(x => x !== Player.MemberNumber);
|
||
if (list.indexOf(CurrentCharacter.MemberNumber) < 0)
|
||
list.push(CurrentCharacter.MemberNumber);
|
||
C.Appearance[A].Property.MemberNumberListKeys = "" +
|
||
CommonConvertArrayToString(list); // Convert to array and back; can only save strings on server
|
||
}
|
||
}
|
||
C.Appearance[A].Property.LockMemberNumber = CurrentCharacter.MemberNumber;
|
||
}
|
||
CharacterRefresh(Player);
|
||
ChatRoomCharacterUpdate(Player);
|
||
}
|
||
|
||
/**
|
||
* Checks if the player can help the current character by giving them a lockpick
|
||
* @returns {boolean} - TRUE if the player can interact and is allowed to interact with the current character.
|
||
*/
|
||
function ChatRoomCanGiveLockpicks() {
|
||
return Player.CanInteract() ? InventoryAvailable(Player, "Lockpicks", "ItemMisc") : false;
|
||
}
|
||
|
||
/**
|
||
* Checks if the player can help the current character by giving her lockpicks
|
||
* @returns {boolean} - TRUE if the player can interact and is allowed to interact with the current character.
|
||
*/
|
||
function ChatRoomCanAssistStruggle() { return CurrentCharacter.AllowItem && !CurrentCharacter.CanInteract(); }
|
||
|
||
/**
|
||
* Checks if the character options menu is available.
|
||
* @returns {boolean} - Whether or not the player can interact with the target character
|
||
*/
|
||
function DialogCanPerformCharacterAction() {
|
||
if (InventoryIsBlockedByDistance(CurrentCharacter)) return false;
|
||
return (
|
||
ChatRoomCanAssistStand() || ChatRoomCanAssistKneel() || ChatRoomCanAssistStruggle() ||
|
||
ChatRoomCanHoldLeash() || ChatRoomCanStopHoldLeash() ||
|
||
DialogCanTakePhotos() ||
|
||
ChatRoomCanGiveLockpicks() || ChatRoomCanGiveHighSecurityKeys() || ChatRoomCanGiveHighSecurityKeysAll() ||
|
||
DialogHasGamingHeadset() || DialogCanWatchKinkyDungeon()
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Returns TRUE if the player can enter in whisper mode with the currently focused character
|
||
* @returns {boolean} - TRUE is whipser can be started
|
||
*/
|
||
function ChatRoomCanStartWhisper() {
|
||
if ((CurrentCharacter == null) || CurrentCharacter.IsPlayer()) return false;
|
||
if (CurrentCharacter.MemberNumber === ChatRoomTargetMemberNumber) return false;
|
||
if(ChatRoomActiveView.CanStartWhisper) return ChatRoomActiveView.CanStartWhisper(CurrentCharacter);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Enters whisper mode with the current character
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomStartWhisper() {
|
||
if (CurrentCharacter == null) return;
|
||
ChatRoomSetTarget(CurrentCharacter.MemberNumber);
|
||
DialogLeave();
|
||
}
|
||
|
||
/**
|
||
* Returns TRUE if the player can leave whisper mode with the currently focused character
|
||
* @returns {boolean} - TRUE is whipser can exited
|
||
*/
|
||
function ChatRoomCanStopWhisper() {
|
||
if (CurrentCharacter == null) return false;
|
||
return (CurrentCharacter.MemberNumber === ChatRoomTargetMemberNumber);
|
||
}
|
||
|
||
/**
|
||
* Leaves whisper mode
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomStopWhisper() {
|
||
ChatRoomSetTarget(-1);
|
||
DialogLeave();
|
||
}
|
||
|
||
/**
|
||
* Checks if the target character can be helped back on her feet. This is different than CurrentCharacter.CanKneel()
|
||
* because it listens for the current active pose and removes certain checks that are not required for someone else to
|
||
* help a person kneel down.
|
||
* @returns {boolean} - Whether or not the target character can stand
|
||
*/
|
||
function ChatRoomCanAssistStand() {
|
||
return (
|
||
Player.CanInteract()
|
||
&& CurrentCharacter.AllowItem
|
||
&& PoseAllStanding.some(p => PoseAvailable(CurrentCharacter, "BodyLower", p))
|
||
&& CurrentCharacter.IsKneeling()
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Checks if the target character can be helped down on her knees. This is different than CurrentCharacter.CanKneel()
|
||
* because it listens for the current active pose and removes certain checks that are not required for someone else to
|
||
* help a person kneel down.
|
||
* @returns {boolean} - Whether or not the target character can stand
|
||
*/
|
||
|
||
function ChatRoomCanAssistKneel() {
|
||
return (
|
||
Player.CanInteract()
|
||
&& CurrentCharacter.AllowItem
|
||
&& PoseAllKneeling.some(p => PoseAvailable(CurrentCharacter, "BodyLower", p))
|
||
&& !CurrentCharacter.IsKneeling()
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Checks if the player character is kneeling and can stand up.
|
||
* Will return true if there are any available transitions from {@link PoseAllKneeling} to {@link PoseAllStanding}.
|
||
* @returns {boolean} - Whether or not the player character can stand
|
||
*/
|
||
function ChatRoomCanAttemptStand() {
|
||
return Player.IsKneeling() && PoseAllStanding.some(p => PoseAvailable(Player, "BodyLower", p));
|
||
}
|
||
|
||
/**
|
||
* Checks if the player character is standing and can kneel down.
|
||
* Will return true if there are any available transitions from {@link PoseAllStanding} to {@link PoseAllKneeling}.
|
||
* @returns {boolean} - Whether or not the player character can stand
|
||
*/
|
||
function ChatRoomCanAttemptKneel() {
|
||
return Player.IsStanding() && PoseAllKneeling.some(p => PoseAvailable(Player, "BodyLower", p));
|
||
}
|
||
|
||
/**
|
||
* Checks if the player can stop the current character from leaving.
|
||
* @returns {boolean} - TRUE if the current character is slowed down and can be interacted with.
|
||
*/
|
||
function ChatRoomCanStopSlowPlayer() { return (CurrentCharacter.IsSlow() && Player.CanInteract() && CurrentCharacter.AllowItem ); }
|
||
|
||
/**
|
||
* Checks if the player can interrupt another character from struggling.
|
||
* @returns {boolean} - TRUE if the player can do it
|
||
*/
|
||
function ChatRoomCanInterruptOnlineStruggle() { return (Player.CanInteract() && !CurrentCharacter.IsPlayer() && CurrentCharacter.AllowItem); }
|
||
|
||
/**
|
||
* Checks if the player can grab the targeted player's leash
|
||
* @returns {boolean} - TRUE if the player can interact and is allowed to interact with the current character.
|
||
*/
|
||
function ChatRoomCanHoldLeash() { return CurrentCharacter.AllowItem && Player.CanInteract() && CurrentCharacter.OnlineSharedSettings && CurrentCharacter.OnlineSharedSettings.AllowPlayerLeashing != false && ChatRoomLeashList.indexOf(CurrentCharacter.MemberNumber) < 0
|
||
&& ChatRoomCanBeLeashed(CurrentCharacter);}
|
||
/**
|
||
* Checks if the player can let go of the targeted player's leash
|
||
* @returns {boolean} - TRUE if the player can interact and is allowed to interact with the current character.
|
||
*/
|
||
function ChatRoomCanStopHoldLeash() {
|
||
if (CurrentCharacter.AllowItem && Player.CanInteract() && CurrentCharacter.OnlineSharedSettings && CurrentCharacter.OnlineSharedSettings.AllowPlayerLeashing != false && ChatRoomLeashList.indexOf(CurrentCharacter.MemberNumber) >= 0) {
|
||
if (ChatRoomCanBeLeashed(CurrentCharacter)) {
|
||
return true;
|
||
} else {
|
||
ChatRoomLeashList.splice(ChatRoomLeashList.indexOf(CurrentCharacter.MemberNumber), 1);
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Checks if the given character is a valid leash target for the player
|
||
* @param {Character} C - The character to search
|
||
* @returns {boolean} - TRUE if the player can be leashed
|
||
*/
|
||
function ChatRoomCanBeLeashed(C) {
|
||
return ChatRoomCanBeLeashedBy(Player.MemberNumber, C);
|
||
}
|
||
|
||
/**
|
||
* Checks if the targeted player is a valid leash target for the source member number
|
||
* @param {number} sourceMemberNumber - Member number of the source player
|
||
* @param {Character} C - Target player
|
||
* @returns {boolean} - TRUE if the player can be leashed
|
||
*/
|
||
function ChatRoomCanBeLeashedBy(sourceMemberNumber, C) {
|
||
if ((ChatRoomData && ChatRoomData.BlockCategory && ChatRoomData.BlockCategory.indexOf("Leashing") < 0) || !ChatRoomData) {
|
||
// Have to not be tethered, and need a leash
|
||
var canLeash = false;
|
||
var isTrapped = false;
|
||
var neckLock = null;
|
||
for (let A = 0; A < C.Appearance.length; A++)
|
||
if ((C.Appearance[A].Asset != null) && (C.Appearance[A].Asset.Group.Family == C.AssetFamily)) {
|
||
if (InventoryItemHasEffect(C.Appearance[A], "Leash", true)) {
|
||
canLeash = true;
|
||
if (C.Appearance[A].Asset.Group.Name == "ItemNeckRestraints")
|
||
neckLock = InventoryGetLock(C.Appearance[A]);
|
||
} else if (InventoryItemHasEffect(C.Appearance[A], "Tethered", true) || InventoryItemHasEffect(C.Appearance[A], "Mounted", true) || InventoryItemHasEffect(C.Appearance[A], "Enclose", true) || InventoryItemHasEffect(C.Appearance[A], "OneWayEnclose", true)){
|
||
isTrapped = true;
|
||
}
|
||
}
|
||
|
||
// TODO: Use `DialogHasKey` here after refactoring it with access to both the source and destination character
|
||
const isOwner = C.IsOwnedByMemberNumber(sourceMemberNumber);
|
||
const isLover = C.IsLoverOfMemberNumber(sourceMemberNumber);
|
||
const isFamily = C.IsFamilyOfPlayer();
|
||
if (canLeash && !isTrapped) {
|
||
if (
|
||
sourceMemberNumber == 0
|
||
|| !neckLock
|
||
|| (!neckLock.Asset.OwnerOnly && !neckLock.Asset.LoverOnly && !neckLock.Asset.FamilyOnly)
|
||
|| (neckLock.Asset.OwnerOnly && isOwner)
|
||
|| (neckLock.Asset.LoverOnly && (isOwner || isLover))
|
||
|| (neckLock.Asset.FamilyOnly && (isOwner || isLover || isFamily))
|
||
) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Checks if the player has waited long enough to be able to call the maids
|
||
* @returns {boolean} - TRUE if the current character has been in the last chat room for more than 30 minutes
|
||
*/
|
||
function DialogCanCallMaids() { return (CurrentScreen == "ChatRoom" && (ChatRoomData && ChatRoomData.Game == "" && !(LogValue("Committed", "Asylum") >= CurrentTime)) && !Player.CanWalk()) && !MainHallIsMaidsDisabled();}
|
||
|
||
|
||
/**
|
||
* Checks if the player has waited long enough to be able to call the maids
|
||
* @returns {boolean} - TRUE if the current character has been in the last chat room for more than 30 minutes
|
||
*/
|
||
function DialogCanCallMaidsPunishmentOn() { return (DialogCanCallMaids() && (!Player.RestrictionSettings || !Player.RestrictionSettings.BypassNPCPunishments));}
|
||
|
||
|
||
/**
|
||
* Checks if the player has waited long enough to be able to call the maids
|
||
* @returns {boolean} - TRUE if the current character has been in the last chat room for more than 30 minutes
|
||
*/
|
||
function DialogCanCallMaidsPunishmentOff() { return (DialogCanCallMaids() && Player.RestrictionSettings && Player.RestrictionSettings.BypassNPCPunishments);}
|
||
|
||
/**
|
||
* Namespace with functions for creating chat room separators.
|
||
* @namespace
|
||
*/
|
||
var ChatRoomSep = {
|
||
/**
|
||
* The most recently created chat room separator
|
||
* @type {null | HTMLDivElement}
|
||
*/
|
||
ActiveElem: null,
|
||
|
||
/**
|
||
* @private
|
||
* @type {(this: HTMLDivElement, event: ClipboardEvent) => void}
|
||
*/
|
||
_CopyHeader: function _CopyHeader(ev) {
|
||
let txt = "";
|
||
for (const el of this.childNodes) {
|
||
if (!(el instanceof Element)) {
|
||
txt += el.textContent;
|
||
continue;
|
||
} else if (el.classList.contains("chat-room-no-copy")) {
|
||
continue;
|
||
}
|
||
txt += el.textContent || (el.getAttribute("aria-label") ?? "");
|
||
}
|
||
ev.clipboardData.setData("text/plain", txt);
|
||
ev.preventDefault();
|
||
},
|
||
|
||
/**
|
||
* Click event listener for collapsing one or more chat room separators
|
||
* @private
|
||
* @type {(this: HTMLButtonElement, event: MouseEvent | TouchEvent) => Promise<void>}
|
||
*/
|
||
_ClickCollapse: async function _ClickCollapse(event) {
|
||
const mode = this.getAttribute("aria-expanded") === "true" ? "Collapse" : "Uncollapse";
|
||
const roomSep = /** @type {HTMLDivElement} */(this.parentElement.parentElement);
|
||
if (event.shiftKey) {
|
||
// (un)collapse all separators if the `shift` key is held down while clicking
|
||
const roomSeps = /** @type {HTMLDivElement[]} */(Array.from(roomSep.parentElement.getElementsByClassName("chat-room-sep")));
|
||
roomSeps.forEach(e => ChatRoomSep[mode](e));
|
||
} else {
|
||
ChatRoomSep[mode](roomSep);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Click event listener for scrolling towards chat room seperator
|
||
* @private
|
||
* @type {(this: HTMLButtonElement, event: MouseEvent | TouchEvent) => Promise<void>}
|
||
*/
|
||
_ClickScrollUp: async function _ClickScrollUp(event) {
|
||
// Workaround as we can't directly use `chatArea.scrollIntoView` due to the seperators position being sticky
|
||
const chatRoomSep = /** @type {HTMLDivElement} */(this.parentElement.parentElement);
|
||
const sibbling = /** @type {null | HTMLElement} */(chatRoomSep.nextSibling);
|
||
if (sibbling) {
|
||
const chatArea = /** @type {HTMLDivElement} */(chatRoomSep.parentElement);
|
||
chatArea.scroll({ top: sibbling.offsetTop - chatRoomSep.offsetHeight });
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Return a {@link HTMLElement.InnerHTML} representation of the passed button's room name
|
||
* @private
|
||
* @param {HTMLButtonElement} button
|
||
* @returns {(string | HTMLElement)[]}
|
||
*/
|
||
_GetDisplayName: function _GetDisplayName(button) {
|
||
const namespace = FriendListIconMapping[button.dataset.space];
|
||
return [
|
||
namespace ? [
|
||
ElementCreate({
|
||
tag: "div",
|
||
attributes: { role: "img", "aria-label": InterfaceTextGet(`ChatRoomSpace${button.dataset.space || "F"}`) },
|
||
classList: ["chat-room-sep-image"],
|
||
style: { mask: `url(${namespace.src}) center/contain` },
|
||
eventListeners: { copy: ChatRoomSep._CopyImage },
|
||
}),
|
||
" - ",
|
||
] : undefined,
|
||
button.dataset.private === "true" ? [
|
||
ElementCreate({
|
||
tag: "div",
|
||
attributes: { role: "img", "aria-label": InterfaceTextGet("Private") },
|
||
classList: ["chat-room-sep-image"],
|
||
style: { mask: `url(${FriendListIconMapping.Private.src}) center/contain` },
|
||
eventListeners: { copy: ChatRoomSep._CopyImage },
|
||
}),
|
||
" - ",
|
||
] : undefined,
|
||
ChatRoomHTMLEntities(ChatSearchMuffle(button.dataset.room)),
|
||
button.dataset.messages ? [
|
||
ElementCreate({
|
||
tag: "span",
|
||
classList: ["chat-room-no-copy"],
|
||
children: [" ✉", { tag: "sup", children: [button.dataset.messages] }],
|
||
}),
|
||
] : undefined,
|
||
].flat().filter(Boolean);
|
||
},
|
||
|
||
/**
|
||
* Create a dividing element serving as seperator for different chat rooms
|
||
* @param {boolean} appendChat - Whether to assign {@link ChatRoomSep.ActiveElem} and append the returned `<div>` to the chat log
|
||
* @returns {HTMLDivElement} - The created `<div>` element
|
||
*/
|
||
Create: function Create(appendChat=true) {
|
||
const now = Date.now();
|
||
const elem = ElementCreate({
|
||
tag: "div",
|
||
classList: ["ChatMessage", "ChatMessageAction", "chat-room-sep"],
|
||
style: { ["--label-color"]: Player.LabelColor },
|
||
dataAttributes: { time: ChatRoomCurrentTime(), sender: (Player.MemberNumber ?? "").toString() },
|
||
children: [
|
||
{
|
||
tag: "div",
|
||
classList: ["chat-room-sep-div"],
|
||
children: [
|
||
ElementButton.Create(
|
||
`chat-room-sep-collapse-${now}`, this._ClickCollapse, { noStyling: true },
|
||
{
|
||
button: {
|
||
children: ["˅"],
|
||
classList: ["chat-room-sep-collapse"],
|
||
attributes: { "aria-expanded": "true" },
|
||
},
|
||
},
|
||
),
|
||
ElementButton.Create(
|
||
`chat-room-sep-header-${now}`, this._ClickScrollUp, { noStyling: true },
|
||
{ button: { classList: ["chat-room-sep-header"], eventListeners: { copy: ChatRoomSep._CopyHeader } } },
|
||
),
|
||
],
|
||
},
|
||
],
|
||
});
|
||
if (appendChat) {
|
||
ChatRoomAppendChat(elem);
|
||
ChatRoomSep.ActiveElem?.classList.remove("chat-room-sep-last");
|
||
ChatRoomSep.ActiveElem = elem;
|
||
elem.classList.add("chat-room-sep-last");
|
||
}
|
||
return elem;
|
||
},
|
||
|
||
/**
|
||
* Return a {@link HTMLElement.innerHTML} representation of the separators room name
|
||
* @param {HTMLDivElement} roomSep - The chat room separator
|
||
* @returns {(string | HTMLElement)[]}
|
||
*/
|
||
GetDisplayName: function GetDisplayName(roomSep) {
|
||
const button = /** @type {HTMLButtonElement} */(roomSep?.querySelector(".chat-room-sep-header"));
|
||
return button ? ChatRoomSep._GetDisplayName(button) : [];
|
||
},
|
||
|
||
/**
|
||
* Return whether the passed room separator is collapsed OR NOT
|
||
* @param {HTMLDivElement} roomSep - The chat room separator
|
||
* @returns {boolean}
|
||
*/
|
||
IsCollapsed: function IsCollapsed(roomSep) {
|
||
const button = /** @type {HTMLButtonElement} */(roomSep?.querySelector(".chat-room-sep-collapse"));
|
||
return !!button && button.getAttribute("aria-expanded") === "false";
|
||
},
|
||
|
||
/**
|
||
* Uncollapse the passed room separator
|
||
* @param {HTMLDivElement} roomSep - The chat room separator
|
||
*/
|
||
Uncollapse: async function Uncollapse(roomSep) {
|
||
const button = /** @type {HTMLButtonElement} */(roomSep?.querySelector(".chat-room-sep-collapse"));
|
||
if (!button || button.getAttribute("aria-expanded") === "true") {
|
||
return;
|
||
}
|
||
|
||
button.setAttribute("aria-expanded", "true");
|
||
button.innerText = "˅";
|
||
let sibbling = /** @type {null | HTMLElement} */(roomSep.nextSibling);
|
||
while (sibbling && !sibbling.classList.contains("chat-room-sep")) {
|
||
sibbling.style.display = "";
|
||
sibbling = /** @type {null | HTMLElement} */(sibbling.nextSibling);
|
||
}
|
||
|
||
const headerButton = /** @type {HTMLButtonElement} */(button.nextSibling);
|
||
if (headerButton) {
|
||
headerButton.dataset.messages = "";
|
||
headerButton.replaceChildren(...ChatRoomSep.GetDisplayName(roomSep));
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Collapse the passed room separator
|
||
* @param {HTMLDivElement} roomSep - The chat room separator
|
||
*/
|
||
Collapse: async function Collapse(roomSep) {
|
||
const button = /** @type {HTMLButtonElement} */(roomSep?.querySelector(".chat-room-sep-collapse"));
|
||
if (!button || button.getAttribute("aria-expanded") === "false") {
|
||
return;
|
||
}
|
||
|
||
button.setAttribute("aria-expanded", "false");
|
||
button.innerText = "˃";
|
||
let sibbling = /** @type {null | HTMLElement} */(roomSep.nextSibling);
|
||
while (sibbling && !sibbling.classList.contains("chat-room-sep")) {
|
||
sibbling.style.display = "none";
|
||
sibbling = /** @type {null | HTMLElement} */(sibbling.nextSibling);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Set the room-specific of the currently active chat room separator
|
||
* @param {HTMLDivElement} roomSep - The chat room separator
|
||
* @param {Pick<ServerChatRoomData, "Name" | "Visibility" | "Space">} data - The data of the room
|
||
*/
|
||
SetRoomData: async function SetRoomData(roomSep, data) {
|
||
const button = /** @type {HTMLButtonElement} */(roomSep?.querySelector(".chat-room-sep-header"));
|
||
if (!button) {
|
||
return;
|
||
}
|
||
|
||
button.dataset.room = data.Name;
|
||
button.dataset.space = data.Space;
|
||
button.dataset.private = ChatRoomDataIsPrivate(data).toString();
|
||
button.replaceChildren(...ChatRoomSep.GetDisplayName(roomSep));
|
||
},
|
||
|
||
/** Update all the displayed room names based on the player's degree of sensory deprivation. */
|
||
UpdateDisplayNames: async function UpdateDisplayNames() {
|
||
const roomLabels = /** @type {HTMLButtonElement[]} */(Array.from(document.querySelectorAll("#TextAreaChatLog .chat-room-sep-header")));
|
||
roomLabels.forEach(e => e.replaceChildren(...ChatRoomSep._GetDisplayName(e)));
|
||
},
|
||
};
|
||
|
||
/**
|
||
* Creates the chat room input elements.
|
||
* @returns {HTMLDivElement}
|
||
*/
|
||
function ChatRoomCreateElement() {
|
||
// NOTE: If you want to add any custom buttons, make sure append them to the `#chat-room-buttons` grid
|
||
|
||
let elem = /** @type {null | HTMLDivElement} */(document.getElementById("chat-room-div"));
|
||
if (!elem) {
|
||
elem = ElementCreate({
|
||
tag: "div",
|
||
attributes: {
|
||
id: "chat-room-div",
|
||
"screen-generated": CurrentScreen,
|
||
},
|
||
parent: document.body,
|
||
classList: ["HideOnPopup"],
|
||
children: [
|
||
{
|
||
tag: "div",
|
||
attributes: { id: "TextAreaChatLog", role: "log" },
|
||
classList: ["scroll-box"],
|
||
},
|
||
{
|
||
tag: "div",
|
||
attributes: { id: "chat-room-bot" },
|
||
children: [
|
||
{
|
||
tag: "div",
|
||
attributes: {id: "chat-room-reply-indicator"},
|
||
classList: ["hidden"],
|
||
children: [
|
||
{ tag: "span", attributes: { id: "chat-room-reply-indicator-text" }, children: [TextGet("ChatRoomReply")] },
|
||
ElementButton.Create(
|
||
"chat-room-reply-indicator-close",
|
||
ChatRoomMessageReplyStop,
|
||
{ noStyling: true },
|
||
),
|
||
],
|
||
},
|
||
{
|
||
tag: "textarea",
|
||
attributes: {
|
||
id: "InputChat",
|
||
name: "InputChat",
|
||
maxLength: 10000,
|
||
autocomplete: "off",
|
||
enterkeyhint: "send",
|
||
},
|
||
eventListeners: {
|
||
input: ChatRoomChatInputChangeHandler,
|
||
keyup: (key) => {ChatRoomStatusUpdateTalk(key); ChatRoomStopReplyOnEscape(key);},
|
||
},
|
||
},
|
||
{
|
||
tag: "span",
|
||
attributes: { id: "InputChatLength" },
|
||
},
|
||
{ tag: "div", attributes: { id: "chat-room-buttons-div" } },
|
||
ElementButton.Create(
|
||
"chat-room-buttons-collapse",
|
||
function() {
|
||
const displayState = this.getAttribute("aria-expanded") === "true" ? "none" : "";
|
||
this.setAttribute("aria-expanded", displayState ? "false" : "true");
|
||
this.innerText = displayState ? "<" : ">";
|
||
const elements = /** @type {NodeListOf<HTMLElement>} */(document.querySelectorAll("#chat-room-buttons > *"));
|
||
elements.forEach(b => {
|
||
if (b.id !== "chat-room-send") {
|
||
b.style.display = displayState;
|
||
}
|
||
});
|
||
},
|
||
{ noStyling: true },
|
||
{
|
||
button: {
|
||
attributes: { "aria-expanded": "false", "aria-control": "chat-room-buttons" },
|
||
children: ["<"],
|
||
},
|
||
},
|
||
),
|
||
ElementMenu.Create(
|
||
"chat-room-buttons",
|
||
[
|
||
ElementButton.Create(
|
||
"chat-room-send", () => ChatRoomSendChat(), { noStyling: true },
|
||
{ button: { classList: ["chat-room-button"] } },
|
||
),
|
||
],
|
||
{ direction: "rtl" },
|
||
),
|
||
],
|
||
},
|
||
],
|
||
});
|
||
if (CommonIsMobile) {
|
||
ElementClickTimeout(elem);
|
||
}
|
||
|
||
ChatRoomRefreshChatSettings();
|
||
ElementFocus("InputChat");
|
||
window.addEventListener("resize", ChatRoomResizeManager.ChatRoomResizeEvent);
|
||
} else if (ChatRoomChatHidden) {
|
||
if (CommonIsMobile) {
|
||
ElementClickTimeout(elem);
|
||
}
|
||
elem.style.display = "";
|
||
ElementScrollToEnd("TextAreaChatLog");
|
||
ChatRoomRefreshChatSettings();
|
||
ElementFocus("InputChat");
|
||
}
|
||
ChatRoomChatHidden = false;
|
||
|
||
return elem;
|
||
}
|
||
|
||
// const ChatRoomUIElementNames = ["InputChat", "TextAreaChatLog", "InputChatLength"];
|
||
|
||
/** Hide the UI elements of the chatroom screen */
|
||
function ChatRoomShowElements() {
|
||
const elem = document.getElementById("chat-room-div");
|
||
if (elem) {
|
||
if (CommonIsMobile) {
|
||
ElementClickTimeout(elem);
|
||
}
|
||
elem.style.display = "";
|
||
|
||
// We can't accurately keep track of the scroll height within the likes of `ChatRoomAppendChat` when an element is hidden via `display: none`,
|
||
// so as a compromise just scroll to the very bottom of the chat log when unhiding (which should be desireable in the majority cases)
|
||
ElementScrollToEnd("TextAreaChatLog");
|
||
}
|
||
ChatRoomResize(false);
|
||
ChatRoomChatHidden = false;
|
||
}
|
||
|
||
/** Show the UI elements of the chatroom screen */
|
||
function ChatRoomHideElements() {
|
||
const elem = document.getElementById("chat-room-div");
|
||
if (elem) {
|
||
elem.style.display = "none";
|
||
}
|
||
ChatRoomChatHidden = true;
|
||
}
|
||
|
||
/**
|
||
* Append an element to the chatroom's chat log, scroll it down and restore focus
|
||
* @param {HTMLElement} div
|
||
*/
|
||
function ChatRoomAppendChat(div) {
|
||
const chatLog = document.getElementById("TextAreaChatLog");
|
||
if (!chatLog) {
|
||
return;
|
||
}
|
||
|
||
// Is the message from the player and was it explicitly sent? (e.g. a non-automatic message)
|
||
const isPlayerMessage = (
|
||
div.dataset.sender === Player.MemberNumber.toString()
|
||
&& !div.classList.contains("ChatMessageNonDialogue")
|
||
);
|
||
|
||
// If the current room separator is collapsed then either uncollapse it
|
||
if (ChatRoomSep.IsCollapsed(ChatRoomSep.ActiveElem)) {
|
||
if (isPlayerMessage) {
|
||
ChatRoomSep.Uncollapse(ChatRoomSep.ActiveElem);
|
||
} else {
|
||
div.style.display = "none";
|
||
const button = /** @type {HTMLButtonElement} */(ChatRoomSep.ActiveElem.querySelector(".chat-room-sep-header"));
|
||
if (button) {
|
||
button.dataset.messages = ((Number.parseInt(button.dataset.messages, 10) || 0) + 1).toString();
|
||
button.replaceChildren(...ChatRoomSep.GetDisplayName(ChatRoomSep.ActiveElem));
|
||
}
|
||
}
|
||
}
|
||
|
||
const isScrolledToEnd = ElementIsScrolledToEnd("TextAreaChatLog");
|
||
chatLog.appendChild(div);
|
||
if (isPlayerMessage || isScrolledToEnd) ElementScrollToEnd("TextAreaChatLog");
|
||
if (document.activeElement.id == "InputChat") ElementFocus("InputChat");
|
||
}
|
||
|
||
/**
|
||
* Loads the chat room screen by displaying the proper inputs.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomLoad() {
|
||
ChatRoomRefreshFontSize();
|
||
ChatRoomCreateElement();
|
||
|
||
// Add a horizontal line to the chatlog the first time one enters the room; skip the very first room
|
||
if (ChatRoomCharacter.length === 0) {
|
||
const elem = ChatRoomSep.Create();
|
||
elem.style.display = (Player.ChatSettings?.PreserveChat ?? true) ? "" : "none";
|
||
}
|
||
|
||
ChatRoomCharacterUpdate(Player);
|
||
ActivityChatRoomArousalSync(Player);
|
||
if (!ChatRoomData || Player.LastChatRoom && ChatRoomData.Name !== Player.LastChatRoom.Name) {
|
||
ChatRoomHideIconState = 0;
|
||
}
|
||
ChatRoomMenuBuild();
|
||
ChatRoomCharacterViewInitialize = true;
|
||
TextPrefetch("Character", "FriendList");
|
||
TextPrefetch("Online", "ChatAdmin");
|
||
TextPrefetch("Room", "Shop2");
|
||
|
||
if (CommonVersionUpdated) {
|
||
CommonGet("./changelog.html", (xhr) => {
|
||
if (xhr.status === 200) {
|
||
const changelog = CommandsChangelog.Publish(xhr.responseText);
|
||
changelog.querySelectorAll(".chat-room-changelog-button-collapse[data-level='2']").forEach(e => e.dispatchEvent(new Event("click")));
|
||
CommonVersionUpdated = false;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Removes all elements that can be open in the chat room
|
||
*/
|
||
function ChatRoomClearAllElements() {
|
||
// Dialog
|
||
DialogLeave();
|
||
|
||
// Chatroom
|
||
if (Player.ChatSettings?.PreserveChat ?? true) {
|
||
ChatRoomHideElements();
|
||
} else {
|
||
ElementRemove("chat-room-div");
|
||
window.removeEventListener("resize", ChatRoomResizeManager.ChatRoomResizeEvent);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Starts the chatroom selection screen.
|
||
* @template {ModuleType} T
|
||
* @param {ServerChatRoomSpace} Space - Name of the chatroom space
|
||
* @param {ServerChatRoomGame} Game - Name of the chatroom game to play
|
||
* @param {ModuleScreens[T] | null} LeaveRoom - Name of the room to go back to when exiting chatsearch.
|
||
* @param {T | null} LeaveSpace - Name of the space to go back to when exiting chatsearch.
|
||
* @param {string} Background - Name of the background to use in chatsearch.
|
||
* @param {BackgroundTag[]} BackgroundTagList - List of available backgrounds in the chatroom space.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomStart(Space, Game, LeaveRoom, LeaveSpace, Background, BackgroundTagList) {
|
||
if (!LeaveRoom || !LeaveSpace) {
|
||
if (Player.GenderSettings.AutoJoinSearch.Female || Player.GenderSettings.AutoJoinSearch.Male) {
|
||
ChatSearchReturnScreen = ["Room", "MainHall"];
|
||
} else {
|
||
ChatSearchReturnScreen = ["Online", "ChatSelect"];
|
||
}
|
||
} else {
|
||
ChatSearchReturnScreen = /** @type {ScreenSpecifier} */([LeaveSpace, LeaveRoom]);
|
||
}
|
||
|
||
ChatRoomSpace = Space;
|
||
ChatRoomGame = Game;
|
||
ChatSearchBackground = Background;
|
||
ChatAdminBackgroundList = BackgroundsGenerateList(BackgroundTagList);
|
||
BackgroundSelectionTagList = BackgroundTagList;
|
||
CommonSetScreen("Online", "ChatSearch");
|
||
|
||
}
|
||
|
||
/**
|
||
* Create the list of chat room menu buttons
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomMenuBuild() {
|
||
ChatRoomMenuButtons = [];
|
||
ChatRoomMenuButtons.push("Exit");
|
||
ChatRoomMenuButtons.push("Kneel");
|
||
ChatRoomMenuButtons.push("Icons");
|
||
if (ChatRoomPlayerIsInDrawFocus()) ChatRoomMenuButtons.push("ClearFocus");
|
||
if (DialogCanTakePhotos()) ChatRoomMenuButtons.push("Camera");
|
||
ChatRoomMenuButtons.push("Cut");
|
||
if (ChatRoomMapViewIsActive() && ((ChatRoomData == null) || (ChatRoomData.MapData == null) || (ChatRoomData.MapData.Type == null) || (ChatRoomData.MapData.Type != "Always"))) ChatRoomMenuButtons.push("CharacterView");
|
||
if (ChatRoomCharacterViewIsActive() && ((ChatRoomData == null) || (ChatRoomData.MapData == null) || (ChatRoomData.MapData.Type == null) || (ChatRoomData.MapData.Type != "Never"))) ChatRoomMenuButtons.push("MapView");
|
||
if (ChatRoomCharacterViewIsActive() && (ChatRoomCharacterViewCharacterCountTotal != null) && (ChatRoomCharacterViewCharacterCountTotal > 10) && (ChatRoomCharacterViewOffset == 0)) ChatRoomMenuButtons.push("NextCharacters");
|
||
if (ChatRoomCharacterViewIsActive() && (ChatRoomCharacterViewCharacterCountTotal != null) && (ChatRoomCharacterViewCharacterCountTotal > 10) && (ChatRoomCharacterViewOffset == 10)) ChatRoomMenuButtons.push("PreviousCharacters");
|
||
if (ChatRoomCustomizationButton() && !ChatRoomCustomized) ChatRoomMenuButtons.push("CustomizationOn");
|
||
if (ChatRoomCustomizationButton() && ChatRoomCustomized) ChatRoomMenuButtons.push("CustomizationOff");
|
||
ChatRoomMenuButtons.push("Dress");
|
||
ChatRoomMenuButtons.push("Profile");
|
||
if ((ChatRoomGame !== "") && (ChatRoomGame !== "GGTS") && (ChatRoomGame !== "Prison")) ChatRoomMenuButtons.push("GameOption");
|
||
ChatRoomMenuButtons.push("RoomAdmin");
|
||
}
|
||
|
||
/**
|
||
* Checks if the player's owner is inside the chatroom.
|
||
* @returns {boolean} - Returns TRUE if the player's owner is inside the room.
|
||
*/
|
||
function ChatRoomOwnerInside() {
|
||
return ChatRoomCharacter.some(c => Player.IsOwnedByCharacter(c));
|
||
}
|
||
|
||
/**
|
||
* Updates the temporary drawing arrays for characters, to handle things that are dependent on the drawn chat room
|
||
* characters rather than the ones actually present
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomUpdateDisplay() {
|
||
// The number of characters to show in the room
|
||
const RenderSingle = Player.GameplaySettings.SensDepChatLog == "SensDepExtreme" && Player.GetBlindLevel() >= 3 && !Player.Effect.includes("VRAvatars");
|
||
ChatRoomCharacterDrawlist = ChatRoomCharacter;
|
||
ChatRoomSenseDepBypass = false;
|
||
if (Player.Effect.includes("VRAvatars")) {
|
||
ChatRoomImpactedBySenseDep = ChatRoomCharacterDrawlist.slice();
|
||
ChatRoomCharacterDrawlist = [];
|
||
ChatRoomSenseDepBypass = true;
|
||
for (let CC = 0; CC < ChatRoomCharacter.length; CC++) {
|
||
if (ChatRoomCharacter[CC].Effect.includes("VRAvatars")) {
|
||
ChatRoomCharacterDrawlist.push(ChatRoomCharacter[CC]);
|
||
ChatRoomImpactedBySenseDep.splice(ChatRoomImpactedBySenseDep.indexOf(ChatRoomCharacter[CC]), 1);
|
||
}
|
||
}
|
||
} else if (Player.GetBlindLevel() > 0 && Player.GetBlindLevel() < 3 && Player.ImmersionSettings.BlindAdjacent) {
|
||
ChatRoomImpactedBySenseDep = ChatRoomCharacterDrawlist.slice();
|
||
// We hide all players except those who are adjacent
|
||
ChatRoomCharacterDrawlist = [];
|
||
ChatRoomSenseDepBypass = true;
|
||
let PlayerIndex = -1;
|
||
// First find the player index
|
||
for (let CC = 0; CC < ChatRoomCharacter.length; CC++) {
|
||
if (ChatRoomCharacter[CC].IsPlayer()) {
|
||
PlayerIndex = CC;
|
||
break;
|
||
}
|
||
}
|
||
// Then filter the characters
|
||
for (let CC = 0; CC < ChatRoomCharacter.length; CC++) {
|
||
if (Math.abs(CC - PlayerIndex) <= 1) {
|
||
ChatRoomCharacterDrawlist.push(ChatRoomCharacter[CC]);
|
||
ChatRoomImpactedBySenseDep.splice(ChatRoomImpactedBySenseDep.indexOf(ChatRoomCharacter[CC]), 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
// If we're in focus (focuslist exists), filter out any players not in it (/focus)
|
||
if (ChatRoomPlayerIsInDrawFocus()) {
|
||
ChatRoomCharacterDrawlist = ChatRoomCharacterDrawlist.filter(C => C.IsPlayer() || ChatRoomDrawFocusList.includes(C));
|
||
}
|
||
|
||
// Keeps the current character count and total
|
||
ChatRoomCharacterViewCharacterCount = RenderSingle ? 1 : ChatRoomCharacterDrawlist.length;
|
||
ChatRoomCharacterViewCharacterCountTotal = RenderSingle ? 1 : ChatRoomCharacterDrawlist.length;
|
||
|
||
// In character view, we can only display 10 characters
|
||
if (ChatRoomCharacterViewIsActive()) {
|
||
|
||
// Make sure the drawing offset isn't set, if 10 characters or less in the room
|
||
if ((ChatRoomCharacterViewOffset !== 0) && (ChatRoomCharacterViewCharacterCount <= 10)) ChatRoomCharacterViewOffset = 0;
|
||
|
||
// Over 10 characters we cut based on the offest
|
||
if (ChatRoomCharacterViewCharacterCount > 10) {
|
||
if (ChatRoomCharacterViewOffset !== 0)
|
||
ChatRoomCharacterDrawlist = ChatRoomCharacterDrawlist.slice((ChatRoomCharacterViewCharacterCount - 10) * -1);
|
||
else
|
||
ChatRoomCharacterDrawlist = ChatRoomCharacterDrawlist.slice(0, 10);
|
||
ChatRoomCharacterViewCharacterCount = RenderSingle ? 1 : ChatRoomCharacterDrawlist.length;
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Draws the status bubble next to the character
|
||
* @param {Character} C - The status bubble to draw
|
||
* @param {number} X - Screen X position
|
||
* @param {number} Y - Screen Y position
|
||
* @param {number} Zoom - Screen zoom
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function DrawStatus(C, X, Y, Zoom) {
|
||
if ((Player.OnlineSettings != null) && (Player.OnlineSettings.ShowStatus != null) && (Player.OnlineSettings.ShowStatus == false)) return;
|
||
if (ChatRoomHideIconState >= 2) return;
|
||
if ((C.ArousalSettings != null) && (C.ArousalSettings.OrgasmTimer != null) && (C.ArousalSettings.OrgasmTimer > 0)) {
|
||
DrawImageResize("Icons/Status/Orgasm" + (Math.floor(CommonTime() / 1000) % 3).toString() + ".png", X + 225 * Zoom, Y + 920 * Zoom, 50 * Zoom, 30 * Zoom);
|
||
return;
|
||
}
|
||
if ((C.StatusTimer != null) && (C.StatusTimer < CommonTime())) {
|
||
C.StatusTimer = null;
|
||
C.Status = null;
|
||
return;
|
||
}
|
||
if ((C.Status == null) || (C.Status == "")) return;
|
||
DrawImageResize("Icons/Status/" + C.Status + (Math.floor(CommonTime() / 1000) % 3).toString() + ".png", X + 225 * Zoom, Y + 920 * Zoom, 50 * Zoom, 30 * Zoom);
|
||
}
|
||
|
||
/**
|
||
* Draws the status icons of a character
|
||
* @param {Character} C The target character
|
||
* @param {number} CharX Character's X position on canvas
|
||
* @param {number} CharY Character's Y position on canvas
|
||
* @param {number} Zoom Room zoom
|
||
*/
|
||
function ChatRoomDrawCharacterStatusIcons(C, CharX, CharY, Zoom)
|
||
{
|
||
if (Player.WhiteList.includes(C.MemberNumber)) {
|
||
DrawImageResize("Icons/Small/WhiteList.png", CharX + 70 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
} else if (Player.BlackList.includes(C.MemberNumber)) {
|
||
DrawImageResize("Icons/Small/BlackList.png", CharX + 70 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
}
|
||
// Unobtrusive but prominent warnings to alert user they are in focus mode -- drawn on player and all focused (visible) characters
|
||
if (ChatRoomPlayerIsInDrawFocus() && (C.IsPlayer() || ChatRoomDrawFocusList.includes(C)))
|
||
{
|
||
DrawImageResize("Icons/Small/FocusEnabledWarning.png", CharX + 30 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
DrawImageResize("Icons/Small/FocusEnabledWarning.png", CharX + 30 * Zoom, CharY + 950 * Zoom, 40 * Zoom, 40 * Zoom);
|
||
}
|
||
if (Player.GhostList.includes(C.MemberNumber)) {
|
||
DrawImageResize("Icons/Small/GhostList.png", CharX + 110 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
} else if (Player.FriendList.includes(C.MemberNumber)) {
|
||
DrawImageResize("Icons/Small/FriendList.png", CharX + 110 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
}
|
||
if (C.IsBirthday())
|
||
{
|
||
DrawImageResize("Icons/Small/Birthday.png", CharX + 150 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
}
|
||
else if (!C.IsPlayer() && C.IsOwner() && (Player.IsOwned() == "online"))
|
||
{
|
||
DrawImageResize("Icons/Small/Owner.png", CharX + 150 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
}
|
||
else if (C.IsLoverOfPlayer())
|
||
{
|
||
DrawImageResize("Icons/Small/Lover.png", CharX + 150 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
}
|
||
else if (C.IsFamilyOfPlayer())
|
||
{
|
||
DrawImageResize("Icons/Small/Family.png", CharX + 150 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
}
|
||
if (ChatRoomCarryingBounty(C))
|
||
{
|
||
DrawImageResize("Icons/Small/Money.png", CharX + 310 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
}
|
||
if (C.OnlineSharedSettings && C.OnlineSharedSettings.GameVersion !== GameVersion)
|
||
{
|
||
DrawImageResize("Icons/Small/Warning.png", CharX + 350 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
}
|
||
if (Array.isArray(ChatRoomData.Admin) && ChatRoomData.Admin.includes(C.MemberNumber))
|
||
{
|
||
DrawImageResize("Icons/Small/Admin.png", CharX + 390 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
} else if (Array.isArray(ChatRoomData.Whitelist) && ChatRoomData.Whitelist.includes(C.MemberNumber)) {
|
||
DrawImageResize("Icons/Small/RoomWhitelist.png", CharX + 390 * Zoom, CharY, 40 * Zoom, 40 * Zoom);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Select the character (open dialog) and clear other chatroom displays.
|
||
* @param {Character} C - The character to focus on. Does nothing if null.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomFocusCharacter(C) {
|
||
if (ChatRoomOwnerPresenceRule("BlockAccessSelf", C)) return;
|
||
if (ChatRoomOwnerPresenceRule("BlockAccessOther", C)) return;
|
||
if (C == null) return;
|
||
ChatRoomHideElements();
|
||
ChatRoomBackground = ChatRoomData.Background;
|
||
C.AllowItem = C.IsPlayer() || ServerChatRoomGetAllowItem(Player, C);
|
||
ChatRoomOwnershipOption = "";
|
||
ChatRoomLovershipOption = "";
|
||
if (!C.IsPlayer()) ServerSend("ChatRoomAllowItem", { MemberNumber: C.MemberNumber });
|
||
if (C.IsOwnedByPlayer() || C.IsLoverOfPlayer()) ServerSend("ChatRoomChat", { Content: "RuleInfoGet", Type: "Hidden", Target: C.MemberNumber });
|
||
CharacterSetCurrent(C);
|
||
}
|
||
|
||
/**
|
||
* Sends the request to the server to check the current character's relationship status.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomCheckRelationships() {
|
||
var C = CharacterGetCurrent();
|
||
if (!C.IsPlayer()) ServerSend("AccountOwnership", { MemberNumber: C.MemberNumber });
|
||
if (!C.IsPlayer()) ServerSend("AccountLovership", { MemberNumber: C.MemberNumber });
|
||
}
|
||
|
||
/**
|
||
* Displays /help content to the player if it's their first visit to a chatroom this session
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomFirstTimeHelp() {
|
||
if (!ChatRoomHelpSeen) {
|
||
if (!Player.ChatSettings || Player.ChatSettings.ShowChatHelp)
|
||
ChatRoomMessage({ Content: "ChatRoomHelp", Type: "Action", Sender: Player.MemberNumber });
|
||
ChatRoomHelpSeen = true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Sets the current whisper target and flags a target update
|
||
* @param {number} MemberNumber - The target member number to set. -1 to unselect
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSetTarget(MemberNumber) {
|
||
MemberNumber ??= -1;
|
||
if (MemberNumber === ChatRoomTargetMemberNumber) return;
|
||
|
||
ChatRoomTargetMemberNumber = MemberNumber;
|
||
let target;
|
||
if (ChatRoomTargetMemberNumber >= 0) {
|
||
target = ChatRoomCharacter.find(c => c.MemberNumber === ChatRoomTargetMemberNumber);
|
||
} else {
|
||
target = null;
|
||
}
|
||
|
||
let placeholder;
|
||
if (target) {
|
||
placeholder = `${TextGetInScope("Screens/Online/ChatRoom/Text_ChatRoom.csv", "WhisperTo")} ${CharacterNickname(target)}`;
|
||
} else {
|
||
placeholder = TextGetInScope("Screens/Online/ChatRoom/Text_ChatRoom.csv", "PublicChat");
|
||
}
|
||
ElementSetAttribute("InputChat", "placeholder", placeholder);
|
||
}
|
||
|
||
/**
|
||
* Updates the chat input's placeholder text to reflect the current whisper target
|
||
*
|
||
* @deprecated No-oped in R104, automatically handled when the target is set
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomTarget() {
|
||
// No-oped in R104
|
||
}
|
||
|
||
/**
|
||
* Updates the account to set the last chat room
|
||
* @param {ChatRoom|null} room - room to set it to. null to reset.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSetLastChatRoom(room) {
|
||
if (room !== null && ChatRoomValidateProperties(room)) {
|
||
if (!ChatRoomNewRoomToUpdate) {
|
||
Player.LastChatRoom = ChatRoomGetSettings(room);
|
||
ServerAccountUpdate.QueueData({ LastChatRoom: Player.LastChatRoom });
|
||
}
|
||
} else {
|
||
Player.LastChatRoom = null;
|
||
Player.LastMapData = null;
|
||
ServerAccountUpdate.QueueData({ LastChatRoom: null, LastMapData: null });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Triggers a chat room message for stimulation events.
|
||
*
|
||
* Chance is calculated for worn items can cause stimulation (things like plugs
|
||
* and crotch ropes), then one is randomly selected in the list and if it passes
|
||
* a random chance check, it will send a player-only message.
|
||
*
|
||
* @param {StimulationAction} Action - The action that happened
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomStimulationMessage(Action) {
|
||
if (CurrentScreen !== "ChatRoom"
|
||
|| Player.ImmersionSettings && !Player.ImmersionSettings.StimulationEvents
|
||
|| !["Kneel", "Walk", "Struggle", "StruggleFail", "Talk"].includes(Action))
|
||
return;
|
||
|
||
const eventData = ChatRoomStimulationEvents[Action];
|
||
if (!eventData) return;
|
||
|
||
const arousal = Player.ArousalSettings && Player.ArousalSettings.Progress || 0;
|
||
// Tracking for the PlugBoth event
|
||
let isFilled = false;
|
||
let isPlugged = false;
|
||
|
||
// We go through every stimulating item and gather their effects
|
||
/** @type {StimulationEventItem[]} */
|
||
const events = [];
|
||
for (let A of Player.Appearance) {
|
||
// First handle single items
|
||
const filled = InventoryItemHasEffect(A, "FillVulva", true);
|
||
const plugged = InventoryItemHasEffect(A, "IsPlugged", true);
|
||
const gagged = InventoryItemHasEffect(A, "GagTotal", true) || InventoryItemHasEffect(A, "GagTotal2", true);
|
||
const wearsCrotchRope = InventoryItemHasEffect(A, "CrotchRope", true);
|
||
const canWiggle = InventoryItemHasEffect(A, "Wiggling");
|
||
|
||
// Track modifiers for vibrating and inflated toys
|
||
const inflated = InventoryGetItemProperty(A, "InflateLevel", true) || 0;
|
||
const vibrating = InventoryItemHasEffect(A, "Vibrating", true);
|
||
const vibeIntensity = InventoryGetItemProperty(A, "Intensity", true) || 0;
|
||
|
||
if (wearsCrotchRope && eventData.Chance > 0) {
|
||
let chance = eventData.Chance;
|
||
chance += eventData.ArousalScaling * arousal / 100;
|
||
events.push({ chance: chance, arousal: 2, item: A, event: "CrotchRope" });
|
||
}
|
||
|
||
if (gagged && eventData.TalkChance > 0) {
|
||
events.push({ chance: eventData.TalkChance, arousal: 12, item: A, event: "Talk" });
|
||
}
|
||
|
||
if ((filled || plugged) && eventData.Chance > 0) {
|
||
/** @type {StimulationEventType} */
|
||
let name = filled ? "PlugFront" : "PlugBack";
|
||
let chance = eventData.Chance;
|
||
chance += eventData.ArousalScaling * arousal / 100;
|
||
let evtArousal = 1;
|
||
if (vibrating) {
|
||
chance += eventData.VibeScaling * (vibeIntensity + 1);
|
||
evtArousal += (vibeIntensity + 1);
|
||
}
|
||
events.push({ chance: chance, arousal: evtArousal, item: A, event: name });
|
||
isFilled = isFilled || filled;
|
||
isPlugged = isPlugged || plugged;
|
||
}
|
||
|
||
if (vibrating && eventData.Chance > 0) {
|
||
let chance = eventData.Chance;
|
||
chance += eventData.VibeScaling * (vibeIntensity + 1);
|
||
chance += eventData.ArousalScaling * arousal / 100;
|
||
events.push({ chance: chance, arousal: (vibeIntensity + 1), item: A, event: "Vibe" });
|
||
}
|
||
|
||
if (inflated > 0 && eventData.Chance > 0) {
|
||
let chance = eventData.Chance;
|
||
chance += eventData.InflationScaling * inflated / 4;
|
||
chance += eventData.ArousalScaling * arousal / 100;
|
||
events.push({ chance: chance, arousal: inflated / 2, item: A, event: "Inflated" });
|
||
}
|
||
|
||
if (canWiggle && eventData.Chance > 0) {
|
||
let chance = eventData.Chance;
|
||
chance += eventData.ArousalScaling * arousal / 100;
|
||
events.push({ chance: chance, arousal: 1, item: A, event: "Wiggling" });
|
||
}
|
||
}
|
||
|
||
// If the player is both plugged and filled, insert a special event for that
|
||
if (isFilled && isPlugged) {
|
||
// Dummy item
|
||
const pluggingItems = Player.Appearance.filter(item => InventoryItemHasEffect(item, "FillVulva", true) || InventoryItemHasEffect(item, "IsPlugged", true));
|
||
let A = CommonRandomItemFromList(undefined, pluggingItems);
|
||
let chance = eventData.Chance;
|
||
chance += eventData.ArousalScaling * arousal / 100;
|
||
events.push({ chance: chance, arousal: 2, item: A, event: "PlugBoth" });
|
||
}
|
||
|
||
if (!events.length)
|
||
return;
|
||
|
||
// Pick a random event, and check it
|
||
const event = CommonRandomItemFromList(undefined, events);
|
||
|
||
const dice = Math.random();
|
||
if (dice > event.chance)
|
||
return;
|
||
|
||
// We have a trigger message, send it out!
|
||
if ((Player.ChatSettings != null) && (Player.ChatSettings.ShowActivities != null) && !Player.ChatSettings.ShowActivities) return;
|
||
|
||
let group = event.item.Asset.Group;
|
||
if (event.item.Asset.ArousalZone) {
|
||
group = AssetGroupGet(Player.AssetFamily, event.item.Asset.ArousalZone);
|
||
}
|
||
|
||
// Increase player arousal to the zone
|
||
if (!Player.IsEdged() && arousal < 70 - event.arousal && event.event != "Talk")
|
||
ActivityEffectFlat(Player, Player, event.arousal, group.Name, 1);
|
||
const duration = (Math.random() + event.arousal / 2.4) * 500;
|
||
DrawFlashScreen("#FFB0B0", duration, 140);
|
||
if (Player.ArousalSettings?.AffectExpression) {
|
||
CharacterSetFacialExpression(Player, "Blush", "VeryHigh", Math.ceil(duration / 250));
|
||
}
|
||
|
||
var index = Math.floor(Math.random() * 3);
|
||
const Dictionary = [
|
||
{ Tag: "AssetGroup", Text: group.Description.toLowerCase() },
|
||
{ Tag: "AssetName", Text: (event.item.Asset.DynamicDescription ? event.item.Asset.DynamicDescription(Player) : event.item.Asset.Description).toLowerCase() },
|
||
];
|
||
ChatRoomMessage({ Content: "ChatRoomStimulationMessage" + event.event + index.toString(), Type: "Action", Sender: Player.MemberNumber, Dictionary: Dictionary, });
|
||
}
|
||
|
||
/**
|
||
* @type {ScreenFunctions["Resize"]}
|
||
*/
|
||
function ChatRoomResize(load) {
|
||
const chatInput = document.getElementById("InputChat");
|
||
if (chatInput) {
|
||
ElementPositionFix("chat-room-div", ChatRoomFontSize, ...ChatRoomDivRect);
|
||
chatInput.dispatchEvent(new Event("input"));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @type {ScreenFunctions["Unload"]}
|
||
*/
|
||
function ChatRoomUnload() {
|
||
ChatRoomHideElements();
|
||
}
|
||
|
||
/**
|
||
* Draws arousal screen filter
|
||
* @param {number} y1 - Y to draw filter at.
|
||
* @param {number} h - Height of filter
|
||
* @param {number} Width - Width of filter
|
||
* @param {number} ArousalOverride - Override to the existing arousal value
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomDrawArousalScreenFilter(y1, h, Width, ArousalOverride, Color = '255, 100, 176', AlphaBonus = 0) {
|
||
let Progress = (ArousalOverride) ? ArousalOverride : Player.ArousalSettings.Progress;
|
||
let amplitude = 0.24 * Math.min(1, 2 - 1.5 * Progress/100); // Amplitude of the oscillation
|
||
let percent = Progress/100.0;
|
||
let level = Math.min(0.5, percent) + 0.5 * Math.pow(Math.max(0, percent*2 - 1), 4);
|
||
let oscillation = Math.sin(CommonTime() / 1000 % Math.PI);
|
||
let alpha = Math.min(1.0, AlphaBonus + 0.6 * level * (0.99 - amplitude + amplitude * oscillation));
|
||
|
||
if (Player.ArousalSettings.VFXFilter == "VFXFilterHeavy") {
|
||
const Grad = MainCanvas.createLinearGradient(0, y1, 0, h);
|
||
let alphamin = Math.max(0, alpha / 2 - 0.05);
|
||
Grad.addColorStop(0, `rgba(${Color}, ${alpha})`);
|
||
Grad.addColorStop(0.1 + 0.2*percent * (1.2 + 0.2 * oscillation), `rgba(${Color}, ${alphamin})`);
|
||
Grad.addColorStop(0.5, `rgba(${Color}, ${alphamin/2})`);
|
||
Grad.addColorStop(0.9 - 0.2*percent * (1.2 + 0.2 * oscillation), `rgba(${Color}, ${alphamin})`);
|
||
Grad.addColorStop(1, `rgba(${Color}, ${alpha})`);
|
||
MainCanvas.fillStyle = Grad;
|
||
MainCanvas.fillRect(0, y1, Width, h);
|
||
} else {
|
||
if (Player.ArousalSettings.VFXFilter != "VFXFilterMedium") {
|
||
alpha = (Progress >= 91) ? 0.25 : 0;
|
||
} else alpha /= 2;
|
||
if (alpha > 0)
|
||
DrawRect(0, y1, Width, h, `rgba(${Color}, ${alpha})`);
|
||
}
|
||
}
|
||
/**
|
||
* Draws vibration screen filter for the specified player
|
||
* @param {number} y1 - Y to draw filter at.
|
||
* @param {number} h - Height of filter
|
||
* @param {number} Width - Width of filter
|
||
* @param {Character} C - Player to draw it for
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomVibrationScreenFilter(y1, h, Width, C) {
|
||
let VibratorLevelLower = 0;
|
||
let VibratorLevelUpper = 0;
|
||
for (let A = 0; A < C.Appearance.length; A++) {
|
||
if (C.Appearance[A] && C.Appearance[A].Property) {
|
||
let property = C.Appearance[A].Property;
|
||
if (property.Effect && property.Effect.includes("Vibrating") && property.Intensity >= 0) {
|
||
let intensity = property.Intensity + 1;
|
||
let group = (C.Appearance[A].Asset && C.Appearance[A].Asset.Group) ? C.Appearance[A].Asset.Group.Name : "";
|
||
if (group == "ItemVulva" || group == "ItemPelvis" || group == "ItemButt" || group == "ItemVulvaPiercings") {
|
||
VibratorLevelLower += (100 -VibratorLevelLower) * 0.7*Math.min(1, intensity/4);
|
||
} else {
|
||
VibratorLevelUpper += (100 -VibratorLevelUpper) * 0.7*Math.min(1, intensity/4);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
ChatRoomDrawVibrationScreenFilter(y1, h, Width, VibratorLevelLower, VibratorLevelUpper);
|
||
}
|
||
|
||
/**
|
||
* Draws vibration screen filter
|
||
* @param {number} y1 - Y to draw filter at.
|
||
* @param {number} h - Height of filter
|
||
* @param {number} Width - Width of filter
|
||
* @param {number} VibratorLower - 1-100 Strength of the vibrator "Down There"
|
||
* @param {number} VibratorSides - 1-100 Strength of the vibrator at the breasts/nipples
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomDrawVibrationScreenFilter(y1, h, Width, VibratorLower, VibratorSides) {
|
||
let amplitude = 0.24; // Amplitude of the oscillation
|
||
let percentLower = VibratorLower/100.0;
|
||
let percentSides = VibratorSides/100.0;
|
||
let level = Math.min(0.5, Math.max(percentLower, percentSides)) + 0.5 * Math.pow(Math.max(0, Math.max(percentLower, percentSides)*2 - 1), 4);
|
||
let oscillation = Math.sin(CommonTime() / 1000 % Math.PI);
|
||
if (Player.ArousalSettings.VFXVibrator != "VFXVibratorAnimated") oscillation = 0;
|
||
let alpha = 0.6 * level * (0.99 - amplitude + amplitude * oscillation);
|
||
|
||
if (VibratorLower > 0) {
|
||
const Grad = MainCanvas.createRadialGradient(Width/2, y1, 0, Width/2, y1, h);
|
||
let alphamin = Math.max(0, alpha / 2 - 0.05);
|
||
let modifier = (Player.ArousalSettings.VFXVibrator == "VFXVibratorAnimated") ? Math.random() * 0.01: 0;
|
||
Grad.addColorStop(VibratorLower / 100 * (0.7 + modifier), `rgba(255, 100, 176, 0)`);
|
||
Grad.addColorStop(VibratorLower / 100 * (0.85 - 0.1*percentLower * (0.5 * oscillation)), `rgba(255, 100, 176, ${alphamin})`);
|
||
Grad.addColorStop(1, `rgba(255, 100, 176, ${alpha})`);
|
||
MainCanvas.fillStyle = Grad;
|
||
MainCanvas.fillRect(0, y1, Width, h);
|
||
}
|
||
if (VibratorSides > 0) {
|
||
let Grad = MainCanvas.createRadialGradient(0, 0, 0, 0, 0, Math.sqrt(h*h + Width*Width));
|
||
let alphamin = Math.max(0, alpha / 2 - 0.05);
|
||
let modifier = (Player.ArousalSettings.VFXVibrator == "VFXVibratorAnimated") ? Math.random() * 0.01: 0;
|
||
Grad.addColorStop(VibratorSides / 100 * (0.8 + modifier), `rgba(255, 100, 176, 0)`);
|
||
Grad.addColorStop(VibratorSides / 100 * (0.9 - 0.07*percentSides * (0.5 * oscillation)), `rgba(255, 100, 176, ${alphamin})`);
|
||
Grad.addColorStop(1, `rgba(255, 100, 176, ${alpha})`);
|
||
MainCanvas.fillStyle = Grad;
|
||
MainCanvas.fillRect(0, y1, Width, h);
|
||
|
||
Grad = MainCanvas.createRadialGradient(Width, 0, 0, Width, 0, Math.sqrt(h*h + Width*Width));
|
||
modifier = (Player.ArousalSettings.VFXVibrator == "VFXVibratorAnimated") ? Math.random() * 0.01: 0;
|
||
Grad.addColorStop(VibratorSides / 100 * (0.8 + modifier), `rgba(255, 100, 176, 0)`);
|
||
Grad.addColorStop(VibratorSides / 100 * (0.9 - 0.07*percentSides * (0.5 * oscillation)), `rgba(255, 100, 176, ${alphamin})`);
|
||
Grad.addColorStop(1, `rgba(255, 100, 176, ${alpha})`);
|
||
MainCanvas.fillStyle = Grad;
|
||
MainCanvas.fillRect(0, y1, Width, h);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Runs the chatroom online bounty loop.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomUpdateOnlineBounty() {
|
||
if (KidnapLeagueSearchingPlayers.length > 0) {
|
||
let misc = InventoryGet(Player, "ItemMisc");
|
||
if (misc && misc.Asset && (misc.Asset.Name == "BountySuitcase" || misc.Asset.Name == "BountySuitcaseEmpty")) {
|
||
if (KidnapLeagueSearchFinishTime > 0 && CommonTime() > KidnapLeagueSearchFinishTime) {
|
||
for (let C = 0; C < ChatRoomCharacter.length; C++) {
|
||
if (KidnapLeagueSearchingPlayers.includes(ChatRoomCharacter[C].MemberNumber)) {
|
||
ServerSend("ChatRoomChat", { Content: "ReceiveSuitcaseMoney", Type: "Hidden", Target: ChatRoomCharacter[C].MemberNumber });
|
||
}
|
||
}
|
||
if (misc.Asset.Name == "BountySuitcase") {
|
||
InventoryRemove(Player, "ItemMisc");
|
||
InventoryWear(Player, "BountySuitcaseEmpty", "ItemMisc");
|
||
ChatRoomMessage({ Content: "OnlineBountySuitcaseEnd", Type: "Action", Sender: Player.MemberNumber });
|
||
KidnapLeagueSearchFinishTime = 0;
|
||
KidnapLeagueSearchingPlayers = [];
|
||
ChatRoomCharacterItemUpdate(Player, "ItemMisc");
|
||
} else {
|
||
ChatRoomMessage({ Content: "OnlineBountySuitcaseEndOpened", Type: "Action", Sender: Player.MemberNumber });
|
||
KidnapLeagueSearchFinishTime = 0;
|
||
KidnapLeagueSearchingPlayers = [];
|
||
if (!misc.Property) misc.Property = {};
|
||
if (!misc.Property.Iterations) misc.Property.Iterations = 0;
|
||
misc.Property.Iterations = misc.Property.Iterations + 1;
|
||
ChatRoomCharacterItemUpdate(Player, "ItemMisc");
|
||
}
|
||
|
||
}
|
||
let KidnapLeagueSearchingPlayersNew = [];
|
||
for (let C = 0; C < ChatRoomCharacter.length; C++) {
|
||
if (ChatRoomCharacter[C].CanInteract() && KidnapLeagueSearchingPlayers.includes(ChatRoomCharacter[C].MemberNumber)) {
|
||
KidnapLeagueSearchingPlayersNew.push(ChatRoomCharacter[C].MemberNumber);
|
||
}
|
||
}
|
||
KidnapLeagueSearchingPlayers = KidnapLeagueSearchingPlayersNew;
|
||
} else {
|
||
KidnapLeagueSearchingPlayers = [];
|
||
KidnapLeagueSearchFinishTime = 0;
|
||
}
|
||
} else {
|
||
if (KidnapLeagueSearchFinishTime > 0) {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.build();
|
||
if (InventoryIsWorn(Player, "BountySuitcase", "ItemMisc"))
|
||
ChatRoomPublishCustomAction("OnlineBountySuitcaseEndEarly", true, Dictionary);
|
||
else if (InventoryIsWorn(Player, "BountySuitcaseEmpty", "ItemMisc"))
|
||
ChatRoomPublishCustomAction("OnlineBountySuitcaseEndEarlyOpened", true, Dictionary);
|
||
}
|
||
|
||
KidnapLeagueSearchFinishTime = 0;
|
||
}
|
||
if (!Player.CanInteract()) {
|
||
KidnapLeagueOnlineBountyTarget = 0;
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* Updates the local view of character status
|
||
* @param {Character} C - The character whose status we're updating
|
||
* @param {string | null} Status - The new status to use
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomStatusUpdateLocalCharacter(C, Status) {
|
||
C.Status = ((Status == "") || (Status == "null")) ? null : Status;
|
||
C.StatusTimer = (Status == "Talk") ? CommonTime() + 5000 : null;
|
||
}
|
||
|
||
/**
|
||
* Updates the player status if needed and sends that new status in a chat message
|
||
* @param {string | null} Status - The new status to use
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomStatusUpdate(Status) {
|
||
if (Status == Player.Status) return;
|
||
if ((Player.OnlineSettings != null) && (Player.OnlineSettings.SendStatus != null) && (Player.OnlineSettings.SendStatus == false) && (Status != null)) return;
|
||
ChatRoomStatusUpdateLocalCharacter(Player, Status);
|
||
ServerSend("ChatRoomChat", { Content: ((Status == null) ? "null" : Status), Type: "Status" });
|
||
}
|
||
|
||
let ChatRoomStatusDeadKeys = [
|
||
"Shift", "Control", "Alt", "Meta", "Fn", "Escape", "Dead",
|
||
"ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown",
|
||
];
|
||
|
||
/**
|
||
* Stops the reply when the escape key is pressed
|
||
* @param {KeyboardEvent} key
|
||
*/
|
||
function ChatRoomStopReplyOnEscape(key) {
|
||
if (key.key == "Escape") {
|
||
ChatRoomMessageReplyStop();
|
||
}
|
||
}
|
||
/**
|
||
* Sends the "Talk" status to other players if the player typed in the text box and there's a value in it
|
||
* @param {KeyboardEvent} Key
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomStatusUpdateTalk(Key) {
|
||
// Prevent dead keys from doing anything with the status
|
||
if (ChatRoomStatusDeadKeys.includes(Key.key))
|
||
return;
|
||
|
||
const text = ElementValue("InputChat");
|
||
let talking = true;
|
||
// Not talking if no chat input
|
||
if (!text) {
|
||
talking = false;
|
||
}
|
||
// Don't send a public update if whispering someone
|
||
else if (ChatRoomTargetMemberNumber >= 0) {
|
||
talking = false;
|
||
}
|
||
// Not talking if entering a command that does not end up in chat
|
||
else if (text.startsWith("/") && !text.startsWith("//") && !text.startsWith("/me ") && !text.startsWith("/action ")) {
|
||
talking = false;
|
||
}
|
||
// No longer talking if pressed enter
|
||
else if (Key.key == "Enter") {
|
||
talking = false;
|
||
}
|
||
// Not talking if only one character in input: require at least 2 characters to prevent misclicks etc. from triggering unnecessary status updates
|
||
else if (text.length <= 2) {
|
||
talking = false;
|
||
}
|
||
ChatRoomStatusUpdate(talking ? "Talk" : null);
|
||
}
|
||
|
||
/**
|
||
* Handler called when the chat input field changes
|
||
* @this {HTMLTextAreaElement}
|
||
* @param {Event} event
|
||
*/
|
||
function ChatRoomChatInputChangeHandler(event) {
|
||
const label = document.getElementById("InputChatLength");
|
||
const chatLog = document.getElementById("TextAreaChatLog");
|
||
const parent = document.getElementById("chat-room-div");
|
||
if (!label || !chatLog || !parent) return;
|
||
|
||
const length = this.value.length;
|
||
if (length <= ServerChatMessageMaxLength - 100) {
|
||
label.textContent = "";
|
||
} else {
|
||
label.textContent = `${ServerChatMessageMaxLength - length}`;
|
||
if (length > ServerChatMessageMaxLength) {
|
||
label.classList.add("invalid");
|
||
} else {
|
||
label.classList.remove("invalid");
|
||
}
|
||
}
|
||
|
||
this.style.height = "100%";
|
||
this.style.height = `${this.scrollHeight}px`;
|
||
|
||
// Check if the text log + input completely fill the parent element (within a gap-accounting error marigin)
|
||
// and then resize the text log + input accordingly
|
||
const chatLogHeight = chatLog.getBoundingClientRect().height;
|
||
const parentHeight = parent.getBoundingClientRect().height;
|
||
const inputHeight = this.getBoundingClientRect().height;
|
||
const ratio = (inputHeight + chatLogHeight) / parentHeight;
|
||
const needsUpdate = ratio > 1.005 || ratio < 0.995;
|
||
if (needsUpdate) {
|
||
const percentage = 100 * (inputHeight / parentHeight);
|
||
if (percentage >= 9 && percentage <= 35) {
|
||
const isScrolledToEnd = ElementIsScrolledToEnd("TextAreaChatLog");
|
||
|
||
// At ~34% the maximum resize height has been reached and there is no point in trying to expand it any further
|
||
parent.style.gridTemplateRows = `${100 - 0.4 - percentage}% ${percentage}%`;
|
||
if (isScrolledToEnd) {
|
||
ElementScrollToEnd("TextAreaChatLog");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Checks if status has expired or is otherwise no longer valid and resets status if so
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomStatusCheckExpiration() {
|
||
if (Player.StatusTimer) {
|
||
if (Player.StatusTimer < CommonTime())
|
||
ChatRoomStatusUpdate(null);
|
||
} else {
|
||
const isCrawling = (Player.Status == "Crawl") && ChatRoomIsLeavingSlowly();
|
||
if (!isCrawling && (ChatRoomStruggleData == null))
|
||
ChatRoomStatusUpdate(null);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clears the room customization data
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomCustomizationClear() {
|
||
ChatRoomCustomBackground = "";
|
||
ChatRoomCustomFilter = "";
|
||
ChatAdminRoomCustomizationPlayMusic("");
|
||
ChatRoomCustomSizeMode = null;
|
||
}
|
||
|
||
/**
|
||
* Use the room customization features if needed
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomCustomizationRun() {
|
||
if ((Player.ImmersionSettings != null) && (Player.OnlineSettings.ShowRoomCustomization != null) && (Player.OnlineSettings.ShowRoomCustomization >= 3)) ChatRoomCustomized = true;
|
||
if (ChatRoomCustomized) {
|
||
if ((Player.ImmersionSettings == null) || (Player.OnlineSettings.ShowRoomCustomization == null) || (Player.OnlineSettings.ShowRoomCustomization >= 1)) {
|
||
ChatAdminRoomCustomizationProcess(ChatRoomData.Custom, null, null);
|
||
} else {
|
||
ChatRoomCustomized = false;
|
||
ChatRoomCustomizationClear();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Runs the chatroom screen.
|
||
* @type {ScreenFunctions["Run"]}
|
||
*/
|
||
function ChatRoomRun(time) {
|
||
|
||
// Handles online GGTS, bounty & customizations
|
||
AsylumGGTSProcess();
|
||
PandoraPenitentiaryRun();
|
||
ChatRoomUpdateOnlineBounty();
|
||
ChatRoomCustomizationRun();
|
||
ChatRoomMenuBuild();
|
||
|
||
// Draws the chat room controls
|
||
ChatRoomStatusCheckExpiration();
|
||
ChatRoomUpdateDisplay();
|
||
ChatRoomCreateElement();
|
||
ChatRoomFirstTimeHelp();
|
||
ChatRoomBackground = "";
|
||
DrawRect(0, 0, 2000, 1000, "Black");
|
||
|
||
// Draw the room characters or the map depending on the map type
|
||
if ((ChatRoomData.MapData == null) || (ChatRoomData.MapData.Type == null) || (ChatRoomData.MapData.Type == "Never")) ChatRoomActivateView(ChatRoomCharacterViewName);
|
||
if ((ChatRoomData.MapData != null) && (ChatRoomData.MapData.Type != null) && (ChatRoomData.MapData.Type == "Always")) ChatRoomActivateView(ChatRoomMapViewName);
|
||
ChatRoomActiveView.Run(time);
|
||
ChatRoomActiveView.Draw();
|
||
if (!ChatRoomDrawArousalOverlay() && ChatRoomActiveView.DrawUi) ChatRoomActiveView.DrawUi();
|
||
|
||
// Draws the chat elements in the bottom right
|
||
if (ChatRoomChatHidden) ChatRoomShowElements();
|
||
if (CurrentTime > ChatRoomGetUpTimer) ChatRoomGetUpTimer = 0;
|
||
|
||
// Draw the buttons at the top-right
|
||
if (ChatRoomStruggleData == null) ChatRoomMenuDraw();
|
||
else ChatRoomStruggleDraw();
|
||
|
||
// Process attempts at leaving slowly
|
||
ChatRoomProcessSlowLeave();
|
||
|
||
// Runs any needed online game script
|
||
OnlineGameRun();
|
||
|
||
// Clear notifications if needed
|
||
ChatRoomNotificationReset();
|
||
|
||
// Recreates the chatroom with the stored chatroom data if necessary
|
||
ChatRoomRecreate();
|
||
}
|
||
|
||
/**
|
||
* Runs the arousal overlay.
|
||
* @returns {boolean} - Returns true if the orgasm overlay is active and false otherwise.
|
||
*/
|
||
function ChatRoomDrawArousalOverlay()
|
||
{
|
||
let orgasmScreen = false;
|
||
// In orgasm mode, we add a pink filter and different controls depending on the stage. The pink filter shows a little above 90
|
||
if ((Player.ArousalSettings != null) && (Player.ArousalSettings.Active != null) && (Player.ArousalSettings.Active != "Inactive") && (Player.ArousalSettings.Active != "NoMeter")) {
|
||
if ((Player.ArousalSettings.OrgasmTimer != null) && (typeof Player.ArousalSettings.OrgasmTimer === "number") && !isNaN(Player.ArousalSettings.OrgasmTimer) && (Player.ArousalSettings.OrgasmTimer > 0)) {
|
||
DrawRect(0, 0, 1003, 1000, "#FFB0B0B0");
|
||
DrawRect(1003, 0, 993, 63, "#FFB0B0B0");
|
||
if (Player.ArousalSettings.OrgasmStage == null) Player.ArousalSettings.OrgasmStage = 0;
|
||
if (Player.ArousalSettings.OrgasmStage == 0) {
|
||
DrawText(TextGet("OrgasmComing"), 500, 410, "White", "Black");
|
||
DrawButton(200, 532, 250, 64, TextGet("OrgasmTryResist"), "White");
|
||
DrawButton(550, 532, 250, 64, TextGet("OrgasmSurrender"), "White");
|
||
}
|
||
if (Player.ArousalSettings.OrgasmStage == 1) DrawButton(ActivityOrgasmGameButtonX, ActivityOrgasmGameButtonY, 250, 64, ActivityOrgasmResistLabel, "White");
|
||
if (ActivityOrgasmRuined) ActivityOrgasmControl();
|
||
if (Player.ArousalSettings.OrgasmStage == 2) DrawText(TextGet("OrgasmRecovering"), 500, 500, "White", "Black");
|
||
orgasmScreen = true;
|
||
ActivityOrgasmProgressBar(50, 970);
|
||
} else if ((Player.ArousalSettings.Progress != null) && (Player.ArousalSettings.Progress >= 1) && (Player.ArousalSettings.Progress <= 99) && !CommonPhotoMode) {
|
||
let y1 = 0;
|
||
let h = 1000;
|
||
|
||
if (ChatRoomCharacterViewCharacterCount == 3) {y1 = 50; h = 900;}
|
||
else if (ChatRoomCharacterViewCharacterCount == 4) {y1 = 150; h = 700;}
|
||
else if (ChatRoomCharacterViewCharacterCount == 5) {y1 = 250; h = 500;}
|
||
|
||
ChatRoomDrawArousalScreenFilter(y1, h, 1003, Player.ArousalSettings.Progress);
|
||
}
|
||
}
|
||
|
||
if (Player.ArousalSettings.VFXVibrator == "VFXVibratorSolid" || Player.ArousalSettings.VFXVibrator == "VFXVibratorAnimated") {
|
||
let y1 = 0;
|
||
let h = 1000;
|
||
|
||
if (ChatRoomCharacterViewCharacterCount == 3) {y1 = 50; h = 900;}
|
||
else if (ChatRoomCharacterViewCharacterCount == 4) {y1 = 150; h = 700;}
|
||
else if (ChatRoomCharacterViewCharacterCount == 5) {y1 = 250; h = 500;}
|
||
|
||
ChatRoomVibrationScreenFilter(y1, h, 1003, Player);
|
||
}
|
||
|
||
return orgasmScreen;
|
||
}
|
||
|
||
/**
|
||
* Draws the chat room struggle progress bar and buttons
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomStruggleDraw() {
|
||
|
||
// Increments the progress based on the time
|
||
let Time = CommonTime();
|
||
if (ChatRoomStruggleData.Timer <= 0) ChatRoomStruggleData.Timer = 1;
|
||
let Progress = (Time - ChatRoomStruggleData.LastRun) / (ChatRoomStruggleData.Timer * 5);
|
||
if (!ChatRoomStruggleData.LoosenMode && (ChatRoomStruggleData.Difficulty < 4) && (Math.random() >= 1 - ChatRoomStruggleData.Progress / 1000)) Progress = Progress * (ChatRoomStruggleData.Difficulty - 4);
|
||
ChatRoomStruggleData.Progress = ChatRoomStruggleData.Progress + Progress;
|
||
let MaxProgress = (!ChatRoomStruggleData.LoosenMode && (ChatRoomStruggleData.Difficulty < -6)) ? 100 + ChatRoomStruggleData.Difficulty : 100;
|
||
if (MaxProgress < 50) MaxProgress = 50;
|
||
if (MaxProgress > 100) MaxProgress = 100;
|
||
if (ChatRoomStruggleData.Progress > MaxProgress) ChatRoomStruggleData.Progress = MaxProgress;
|
||
if (ChatRoomStruggleData.Progress < 0) ChatRoomStruggleData.Progress = 0;
|
||
ChatRoomStruggleData.LastRun = Time;
|
||
|
||
// At 100 progress, we complete the struggle
|
||
if (ChatRoomStruggleData.Progress == 100) {
|
||
if (ChatRoomStruggleData.LoosenMode) StruggleChatRoomLoosenComplete();
|
||
else StruggleChatRoomSuccess();
|
||
return;
|
||
}
|
||
|
||
// Draw the controls, we allow loosening the item beyond progress 50% if the difficulty allows it
|
||
if ((Player.Status == null) || (Player.Status !== "Talk")) ChatRoomStatusUpdate("Struggle");
|
||
let Impossible = ((ChatRoomStruggleData.Difficulty < -6) && (ChatRoomStruggleData.Start + 60000 <= Time));
|
||
if (!ChatRoomStruggleData.AllowLoosen && !ChatRoomStruggleData.LoosenMode)
|
||
ChatRoomStruggleData.AllowLoosen = ((ChatRoomStruggleData.Progress >= 50) && (ChatRoomStruggleData.Difficulty >= -9) && (ChatRoomStruggleData.Difficulty < 0) && (ChatRoomStruggleData.Item != null) && (ChatRoomStruggleData.Item.Asset != null) && (ChatRoomStruggleData.Item.Asset != null) && ChatRoomStruggleData.Item.Asset.AllowTighten && !InventoryItemHasEffect(ChatRoomStruggleData.Item, "Lock"));
|
||
let Text = "Struggling";
|
||
if (Impossible) Text = "Impossible";
|
||
if (ChatRoomStruggleData.LoosenMode) Text = "Loosening";
|
||
let Path = `${AssetGetPreviewPath(ChatRoomStruggleData.Item.Asset)}/${ChatRoomStruggleData.Item.Asset.Name}.png`;
|
||
DrawImageResize(Path, 1010, 5, 50, 50);
|
||
DrawText(TextGet("ChatRoomStruggle" + Text), 1175, 30, "White", "Silver");
|
||
if (ChatRoomStruggleData.AllowLoosen) {
|
||
DrawProgressBar(1300, 3, 390, 55, ChatRoomStruggleData.Progress);
|
||
DrawButton(1700, 0, 145, 60, TextGet("ChatRoomStruggleLoosen"), "White");
|
||
DrawButton(1850, 0, 145, 60, TextGet("ChatRoomStruggleGiveUp"), "White");
|
||
} else {
|
||
DrawProgressBar(1300, 3, 490, 55, ChatRoomStruggleData.Progress);
|
||
DrawButton(1800, 0, 195, 60, TextGet("ChatRoomStruggleGiveUp"), "White");
|
||
}
|
||
|
||
// Generates a struggle animation
|
||
if (Player.OnlineSharedSettings && Player.OnlineSharedSettings.ItemsAffectExpressions && ChatRoomStruggleData.NextAnim <= Time) {
|
||
|
||
// The animation gets stronger the more time passes
|
||
let Sec = Math.round((Time - ChatRoomStruggleData.Start) / 1000) + (ChatRoomStruggleData.LoosenMode ? 15 : 0);
|
||
if (Sec <= 15) {
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Eyes", CommonRandomItemFromList("Afk", [null, "Dazed", "Shy"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Blush", CommonRandomItemFromList("Afk", [null, "Low"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Mouth", CommonRandomItemFromList("Afk", [null, "Grin", "Smirk", "HalfOpen"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Eyebrows", CommonRandomItemFromList("Afk", [null, "OneRaised", "Raised"]));
|
||
} else if (Sec <= 30) {
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Eyes", CommonRandomItemFromList("Afk", [null, "Dazed", "Shy", "Sad", "Closed"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Blush", CommonRandomItemFromList("Afk", [null, "Low", "Medium"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Mouth", CommonRandomItemFromList("Afk", [null, "Grin", "Smirk", "HalfOpen", "Open", "Pout"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Eyebrows", CommonRandomItemFromList("Afk", [null, "OneRaised", "Raised", "Lowered"]));
|
||
} else if (Sec <= 45) {
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Eyes", CommonRandomItemFromList("Afk", [null, "Dazed", "Shy", "Horny", "Lewd"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Blush", CommonRandomItemFromList("Afk", ["Low", "Medium", "High"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Mouth", CommonRandomItemFromList("Afk", [null, "HalfOpen", "Open", "Pout", "Moan", "LipBite"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Eyebrows", CommonRandomItemFromList("Afk", ["OneRaised", "Raised", "Lowered", "Soft"]));
|
||
} else if (Sec <= 60) {
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Eyes", CommonRandomItemFromList("Afk", [null, "Horny", "Lewd", "Sad", "Daydream", "Surprised"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Blush", CommonRandomItemFromList("Afk", ["Medium", "High", "VeryHigh"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Mouth", CommonRandomItemFromList("Afk", [null, "Open", "Pout", "Moan", "LipBite", "Pained", "Sad"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Eyebrows", CommonRandomItemFromList("Afk", [null, "Lowered", "Soft", "Harsh", "Angry"]));
|
||
} else {
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Eyes", CommonRandomItemFromList("Afk", [null, "Horny", "Daydream", "Sad", "Surprised", "Angry", "Closed"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Blush", CommonRandomItemFromList("Afk", ["High", "VeryHigh", "Extreme"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Mouth", CommonRandomItemFromList("Afk", [null, "Open", "Frown", "Moan", "LipBite", "Pained", "Sad", "TonguePinch"]));
|
||
if (Math.random() >= 0.5) CharacterSetFacialExpression(Player, "Eyebrows", CommonRandomItemFromList("Afk", ["Raised", "Lowered", "Soft", "Harsh", "Angry"]));
|
||
}
|
||
|
||
// Sets the next animation time
|
||
ChatRoomStruggleData.NextAnim = Time + 5000 + Math.floor(Math.random() * 5000);
|
||
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* Draws the chat room menu buttons
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomMenuDraw() {
|
||
const Space = 992 / (ChatRoomMenuButtons.length);
|
||
for (let [idx, name] of Object.entries(ChatRoomMenuButtons)) {
|
||
let color = "White";
|
||
let suffix = "";
|
||
let hoverText = "";
|
||
const buttonX = 1005 + Space * Number(idx);
|
||
switch (name) {
|
||
case "Exit":
|
||
if (!ChatRoomCanLeave()) {
|
||
color = "Pink";
|
||
} else if (ChatRoomIsLeavingSlowly()) {
|
||
color = "Yellow";
|
||
name = "Cancel";
|
||
} else {
|
||
color = Player.IsSlow() ? "Yellow" : "White";
|
||
}
|
||
break;
|
||
case "Icons":
|
||
suffix = ChatRoomHideIconState.toString();
|
||
break;
|
||
case "Kneel": {
|
||
let status = 0;
|
||
if (Player.IsKneeling()) {
|
||
status = Math.max(...PoseAllStanding.map(p => PoseCanChangeUnaidedStatus(Player, p)));
|
||
} else if (Player.IsStanding()) {
|
||
status = Math.max(...PoseAllKneeling.map(p => PoseCanChangeUnaidedStatus(Player, p)));
|
||
}
|
||
|
||
switch (status) {
|
||
case PoseChangeStatus.NEVER:
|
||
case PoseChangeStatus.NEVER_WITHOUT_AID:
|
||
color = "Pink";
|
||
break;
|
||
case PoseChangeStatus.ALWAYS_WITH_STRUGGLE:
|
||
color = ChatRoomGetUpTimer === 0 ? "Yellow" : "Pink";
|
||
break;
|
||
case PoseChangeStatus.ALWAYS:
|
||
color = "White";
|
||
break;
|
||
}
|
||
break;
|
||
}
|
||
case "Dress":
|
||
if (!Player.CanChangeOwnClothes()) {
|
||
color = "Pink";
|
||
// Exploded version of `CanChangeClothesOn`
|
||
if (Player.IsRestrained()) {
|
||
hoverText = TextGet("MenuDressIsRestrained");
|
||
} else if (ManagementIsClubSlave()) {
|
||
hoverText = TextGet("MenuDressIsClubSlave");
|
||
} else if (!OnlineGameAllowChange()) {
|
||
hoverText = TextGet("MenuDressGameBlocked");
|
||
} else if (!AsylumGGTSAllowChange(Player)) {
|
||
hoverText = TextGet("MenuDressGGTSBlocked");
|
||
} else if (LogQuery("BlockChange", "Rule")) {
|
||
hoverText = TextGet("MenuDressRuleBlocked");
|
||
} else if (LogQuery("BlockChange", "OwnerRule") && !Player.IsFullyOwned()) {
|
||
hoverText = TextGet("MenuDressOwnerBlocked");
|
||
}
|
||
} else {
|
||
color = "White";
|
||
}
|
||
break;
|
||
}
|
||
hoverText ??= TextGet("Menu" + name);
|
||
DrawButton(buttonX, 2, Space - 2, 60, "", color, null, hoverText);
|
||
|
||
const imageWidth = Math.min(114, Space - 4);
|
||
DrawImageEx("Icons/Rectangle/" + name + suffix + ".png", MainCanvas, buttonX + 1 + (Space - 4 - imageWidth) / 2, 4,
|
||
{ SourcePos: [(114 - imageWidth) / 2, 0, imageWidth, 56] }
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Redirects the Mouse Down event to the map if needed
|
||
* @param {MouseEvent | TouchEvent} event
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomMouseDown(event) {
|
||
if (ChatRoomActiveView.MouseDown) return ChatRoomActiveView.MouseDown(event);
|
||
}
|
||
|
||
/**
|
||
* Redirects the Mouse Up event to the map if needed
|
||
* @param {MouseEvent | TouchEvent} event
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomMouseUp(event) {
|
||
if (ChatRoomActiveView.MouseUp) return ChatRoomActiveView.MouseUp(event);
|
||
}
|
||
|
||
/**
|
||
* Redirects the Mouse Move event to the map if needed
|
||
* @param {MouseEvent | TouchEvent} event
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomMouseMove(event) {
|
||
if (ChatRoomActiveView.MouseMove) return ChatRoomActiveView.MouseMove(event);
|
||
}
|
||
|
||
/**
|
||
* Redirects the Mouse Wheel event to the map if needed
|
||
* @type {ScreenFunctions["MouseWheel"]}
|
||
*/
|
||
function ChatRoomMouseWheel(event) {
|
||
if (ChatRoomActiveView.MouseWheel) return ChatRoomActiveView.MouseWheel(event);
|
||
}
|
||
|
||
/**
|
||
* Handles clicks the chatroom screen.
|
||
* @type {ScreenFunctions["Click"]}
|
||
*/
|
||
function ChatRoomClick(event) {
|
||
|
||
// In orgasm mode, we do not allow any clicks expect the chat
|
||
if ((Player.ArousalSettings != null) && (Player.ArousalSettings.OrgasmTimer != null) && (typeof Player.ArousalSettings.OrgasmTimer === "number") && !isNaN(Player.ArousalSettings.OrgasmTimer) && (Player.ArousalSettings.OrgasmTimer > 0)) {
|
||
|
||
// On stage 0, the player can choose to resist the orgasm or not. At 1, the player plays a mini-game to fight her orgasm
|
||
if (MouseIn(200, 532, 250, 68) && (Player.ArousalSettings.OrgasmStage == 0)) ActivityOrgasmGameGenerate(0);
|
||
if (MouseIn(550, 532, 250, 68) && (Player.ArousalSettings.OrgasmStage == 0)) ActivityOrgasmStart(Player);
|
||
if ((MouseX >= ActivityOrgasmGameButtonX) && (MouseX <= ActivityOrgasmGameButtonX + 250) && (MouseY >= ActivityOrgasmGameButtonY) && (MouseY <= ActivityOrgasmGameButtonY + 64) && (Player.ArousalSettings.OrgasmStage == 1)) ActivityOrgasmGameGenerate(ActivityOrgasmGameProgress + 1);
|
||
return;
|
||
|
||
}
|
||
|
||
// In the left part of the screen
|
||
if (MouseIn(0, 0, 1000, 1000))
|
||
if (ChatRoomActiveView.Click)
|
||
return ChatRoomActiveView.Click(event);
|
||
|
||
// When the user clicks a menu button in the top-right
|
||
if ((MouseYIn(0, 62)) && (ChatRoomStruggleData == null))
|
||
return ChatRoomMenuClick(event);
|
||
|
||
// If the user wants to stop struggling (smaller button)
|
||
if (MouseIn(1700, 0, 145, 60) && (ChatRoomStruggleData != null) && ChatRoomStruggleData.AllowLoosen)
|
||
return StruggleChatRoomLoosenStart();
|
||
|
||
// If the user wants to stop struggling (smaller button)
|
||
if (MouseIn(1850, 0, 145, 60) && (ChatRoomStruggleData != null) && ChatRoomStruggleData.AllowLoosen)
|
||
return StruggleChatRoomStop();
|
||
|
||
// If the user wants to stop struggling (big button)
|
||
if (MouseIn(1800, 0, 195, 60) && (ChatRoomStruggleData != null) && !ChatRoomStruggleData.AllowLoosen)
|
||
return StruggleChatRoomStop();
|
||
|
||
}
|
||
|
||
/**
|
||
* The handler for the "Kneel" top menu button
|
||
*/
|
||
function ChatRoomToggleKneel() {
|
||
let status = 0;
|
||
if (Player.IsKneeling()) {
|
||
status = Math.max(...PoseAllStanding.map(p => PoseCanChangeUnaidedStatus(Player, p)));
|
||
} else if (Player.IsStanding()) {
|
||
status = Math.max(...PoseAllKneeling.map(p => PoseCanChangeUnaidedStatus(Player, p)));
|
||
}
|
||
|
||
switch (status) {
|
||
case PoseChangeStatus.ALWAYS: {
|
||
const PlayerIsKneeling = Player.IsKneeling();
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: PlayerIsKneeling ? "StandUp" : "KneelDown", Type: "Action", Dictionary });
|
||
PoseSetActive(Player, PlayerIsKneeling ? "BaseLower" : "Kneel");
|
||
ChatRoomStimulationMessage("Kneel");
|
||
ServerSend("ChatRoomCharacterPoseUpdate", { Pose: Player.ActivePose });
|
||
return;
|
||
}
|
||
|
||
case PoseChangeStatus.ALWAYS_WITH_STRUGGLE: {
|
||
if (ChatRoomGetUpTimer !== 0) {
|
||
return;
|
||
}
|
||
|
||
// If the player can theoretically get up, we start a minigame!
|
||
let diff = 0;
|
||
if (Player.IsBlind()) diff += 1;
|
||
if (Player.IsKneeling()) diff += 2;
|
||
if (Player.IsDeaf()) diff += 1;
|
||
if (InventoryGet(Player, "ItemTorso") || InventoryGroupIsBlocked(Player, "ItemTorso")) diff += 1;
|
||
if (InventoryGroupIsBlocked(Player, "ItemHands")) diff += 1;
|
||
if (InventoryGet(Player, "ItemArms")) diff += 1;
|
||
if (InventoryGet(Player, "ItemLegs") || InventoryGroupIsBlocked(Player, "ItemLegs")) diff += 1;
|
||
if (InventoryGet(Player, "ItemFeet") || InventoryGroupIsBlocked(Player, "ItemFeet")) diff += 1;
|
||
if (InventoryGet(Player, "ItemBoots")) diff += 2;
|
||
|
||
// 9 is the maximum difficulty, beyond 9 the struggle mini-game doesn't work
|
||
if (diff > 9) diff = 9;
|
||
|
||
// Starts the mini-game
|
||
MiniGameStart("GetUp", diff, "ChatRoomAttemptStandMinigameEnd");
|
||
return;
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* The handler for the "Wardrobe" button
|
||
*/
|
||
function ChatRoomOpenWardrobeScreen() {
|
||
if (Player.CanChangeOwnClothes()) {
|
||
ChatRoomAppearanceLoadCharacter(Player);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* The handler for the "Profile" button
|
||
*/
|
||
function ChatRoomOpenInformationScreen() {
|
||
ChatRoomStatusUpdate("Preference");
|
||
InformationSheetLoadCharacter(Player);
|
||
}
|
||
|
||
/**
|
||
* The handler for the "Admin" button
|
||
*/
|
||
function ChatRoomOpenAdminScreen() {
|
||
if ((ChatRoomData != null) && ChatRoomIsPrivate() && (ChatSearchReturnToScreen == "AsylumGGTS")) {
|
||
AsylumGGTSMessage("NoAdminPrivate");
|
||
return;
|
||
}
|
||
if ((ChatRoomData != null) && ChatRoomIsLocked() && (ChatRoomData.Game == "GGTS")) {
|
||
AsylumGGTSMessage("NoAdminLocked");
|
||
return;
|
||
}
|
||
|
||
ChatRoomStatusUpdate("Preference");
|
||
ChatAdminShowEdit(ChatRoomData);
|
||
}
|
||
|
||
/**
|
||
* Process chat room menu button clicks
|
||
* @type {ScreenFunctions["Click"]}
|
||
*/
|
||
function ChatRoomMenuClick(event) {
|
||
const Space = 992 / (ChatRoomMenuButtons.length);
|
||
for (let B = 0; B < ChatRoomMenuButtons.length; B++) {
|
||
if (MouseXIn(1005 + Space * B, Space - 2)) {
|
||
switch (ChatRoomMenuButtons[B]) {
|
||
case "Exit": {
|
||
ChatRoomAttemptLeave();
|
||
break;
|
||
}
|
||
case "Cut": {
|
||
const roomSeps = document.querySelectorAll("#TextAreaChatLog .chat-room-sep");
|
||
switch (roomSeps.length) {
|
||
case 0:
|
||
// No room separators? Move along
|
||
break;
|
||
case 1: {
|
||
// We got one room separator (the current room): remove all but 20 messages
|
||
const roomSep = roomSeps[0];
|
||
const parent = roomSep.parentElement;
|
||
while (roomSep.nextSibling && parent.childElementCount > 20) {
|
||
parent.removeChild(roomSep.nextSibling);
|
||
}
|
||
break;
|
||
}
|
||
default: {
|
||
// We got multiple room separators:
|
||
// remove all content belong the first room or, if shift is pressed, all but the last room
|
||
const roomSep = event.shiftKey ? roomSeps[roomSeps.length - 1] : roomSeps[1];
|
||
const parent = roomSep.parentElement;
|
||
while (roomSep.previousSibling) {
|
||
parent.removeChild(roomSep.previousSibling);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
ElementScrollToEnd("TextAreaChatLog");
|
||
break;
|
||
}
|
||
case "GameOption":
|
||
// The cut button can become the game option button if there's an online game going on
|
||
CommonSetScreen("Online", /** @type {ModuleScreens["Online"]} */("Game" + ChatRoomGame));
|
||
break;
|
||
case "Kneel":
|
||
ChatRoomToggleKneel();
|
||
break;
|
||
case "Icons":
|
||
// When the user toggles icon visibility
|
||
ChatRoomHideIconState += 1;
|
||
if (ChatRoomHideIconState > 3) ChatRoomHideIconState = 0;
|
||
break;
|
||
case "ClearFocus":
|
||
// When the user wants to exit focus mode (clear their focus list)
|
||
ChatRoomDrawFocusListClear();
|
||
break;
|
||
case "Camera":
|
||
// When the user takes a photo of the room
|
||
ChatRoomPhotoFullRoom();
|
||
break;
|
||
case "Dress":
|
||
ChatRoomOpenWardrobeScreen();
|
||
break;
|
||
case "Profile":
|
||
ChatRoomOpenInformationScreen();
|
||
break;
|
||
case "RoomAdmin":
|
||
ChatRoomOpenAdminScreen();
|
||
break;
|
||
case "CustomizationOn":
|
||
ChatRoomCustomized = true;
|
||
ChatRoomCustomizationClear();
|
||
break;
|
||
case "CustomizationOff":
|
||
ChatRoomCustomized = false;
|
||
ChatRoomCustomizationClear();
|
||
break;
|
||
case "CharacterView":
|
||
ChatRoomActivateView(ChatRoomCharacterViewName);
|
||
break;
|
||
case "MapView":
|
||
ChatRoomActivateView(ChatRoomMapViewName);
|
||
break;
|
||
case "NextCharacters":
|
||
ChatRoomCharacterViewOffset = 10;
|
||
ChatRoomUpdateDisplay();
|
||
break;
|
||
case "PreviousCharacters":
|
||
ChatRoomCharacterViewOffset = 0;
|
||
ChatRoomUpdateDisplay();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function ChatRoomAttemptStandMinigameEnd() {
|
||
|
||
if (MiniGameVictory) {
|
||
if (MiniGameType == "GetUp"){
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: (!Player.IsKneeling()) ? "KneelDownPass" : "StandUpPass", Type: "Action", Dictionary });
|
||
PoseSetActive(Player, (!Player.IsKneeling()) ? "Kneel" : null, true);
|
||
ServerSend("ChatRoomCharacterPoseUpdate", { Pose: Player.ActivePose });
|
||
}
|
||
} else {
|
||
if (MiniGameType == "GetUp") {
|
||
ChatRoomGetUpTimer = CurrentTime + 15000;
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: (!Player.IsKneeling()) ? "KneelDownFail" : "StandUpFail", Type: "Action", Dictionary });
|
||
if (!Player.IsKneeling()) {
|
||
CharacterSetFacialExpression(Player, "Eyebrows", "Soft", 15);
|
||
CharacterSetFacialExpression(Player, "Blush", "Medium", 15);
|
||
CharacterSetFacialExpression(Player, "Eyes", "Dizzy", 15);
|
||
}
|
||
}
|
||
}
|
||
|
||
CommonSetScreen("Online", "ChatRoom");
|
||
}
|
||
|
||
/**
|
||
* Checks if the given chat room visibility property causes it to be "private" (has any form of visibility control)
|
||
* @param {ServerChatRoomData | ServerChatRoomSearchData | ServerChatRoomSettings} room - The visibility property to check
|
||
* @returns {boolean} - Returns TRUE if the room is private. FALSE otherwise, or visibility is not set.
|
||
*/
|
||
function ChatRoomDataIsPrivate(room) {
|
||
return room.Visibility && !CommonArraysEqual(room?.Visibility, ChatRoomVisibilityMode.PUBLIC) || room.Private;
|
||
}
|
||
|
||
/**
|
||
* Checks if the current chat room is "private" (has any form of visibility control)
|
||
* @returns {boolean} - Returns TRUE if the room is private. FALSE otherwise, or if room doesn't exist.
|
||
*/
|
||
function ChatRoomIsPrivate() {
|
||
return ChatRoomDataIsPrivate(ChatRoomData);
|
||
}
|
||
|
||
/**
|
||
* Checks if the given chat room access property causes it to be "locked" (has any form of access control)
|
||
* @param {ServerChatRoomData | ServerChatRoomSearchData | ServerChatRoomSettings} room - The access property to check
|
||
* @returns {boolean} - Returns TRUE if the room is locked. FALSE otherwise, or access is not set.
|
||
*/
|
||
function ChatRoomDataIsLocked(room) {
|
||
return room.Access && !CommonArraysEqual(room?.Access, ChatRoomAccessMode.PUBLIC) || room.Locked;
|
||
}
|
||
|
||
/**
|
||
* Checks if the current chat room is "locked" (has any form of access control)
|
||
* @returns {boolean} - Returns TRUE if the room is locked. FALSE otherwise, or if room doesn't exist.
|
||
*/
|
||
function ChatRoomIsLocked() {
|
||
return ChatRoomDataIsLocked(ChatRoomData);
|
||
}
|
||
|
||
/**
|
||
* Checks if a given character has the needed permissions for a specific role
|
||
* @param {Character} C - The character to check
|
||
* @param {ServerChatRoomData} room - The roles to check
|
||
* @param {"visible"|"access"} role
|
||
* @returns {boolean} - Returns TRUE if the character matches any role. FALSE otherwise.
|
||
*/
|
||
function ChatRoomCharacterHasAnyRole(C, room, role) {
|
||
if (C == null || room == null) return false;
|
||
let perms;
|
||
switch (role) {
|
||
case "visible":
|
||
perms = room.Visibility;
|
||
break;
|
||
case "access":
|
||
perms = room.Access;
|
||
break;
|
||
default:
|
||
return false;
|
||
}
|
||
if (perms.includes("All")) return true;
|
||
if (perms.includes("Admin") && ChatRoomCharacterIsAdmin(C)) return true;
|
||
if (perms.includes("Whitelist") && ChatRoomCharacterIsWhitelisted(C)) return true;
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Checks if a given character can see the room (meets the visibility list requirements)
|
||
* @param {Character} C - The character to check
|
||
* @returns {boolean} - Returns TRUE if the player can see the room.
|
||
*/
|
||
function ChatRoomCharacterCanSeeRoom(C) { return ChatRoomCharacterHasAnyRole(C, ChatRoomData, "visible"); }
|
||
|
||
/**
|
||
* Checks if the player can see the room (meets the visibility list requirements)
|
||
* @returns {boolean} - Returns TRUE if the player can see the room.
|
||
*/
|
||
function ChatRoomPlayerCanSeeRoom() { return ChatRoomCharacterCanSeeRoom(Player); }
|
||
|
||
/**
|
||
* Checks if the current character can see the room (meets the visibility list requirements)
|
||
* @returns {boolean} - Returns TRUE if the current character can see the room.
|
||
*/
|
||
function ChatRoomCurrentCharacterCanSeeRoom() { return ChatRoomCharacterCanSeeRoom(CurrentCharacter); }
|
||
|
||
/**
|
||
* Checks if a given character has access to the room (can enter/leave) (meets the access list requirements)
|
||
* @param {Character} C - The character to check
|
||
* @returns {boolean} - Returns TRUE if the player has access to the room.
|
||
*/
|
||
function ChatRoomCharacterCanAccessRoom(C) { return ChatRoomCharacterHasAnyRole(C, ChatRoomData, "access"); }
|
||
|
||
/**
|
||
* Checks if the player has access to the room (can enter/leave) (meets the access list requirements)
|
||
* @returns {boolean} - Returns TRUE if the player has access to the room.
|
||
*/
|
||
function ChatRoomPlayerCanAccessRoom() { return ChatRoomCharacterCanAccessRoom(Player); }
|
||
|
||
/**
|
||
* Checks if the current character has access to the room (can enter/leave) (meets the access list requirements)
|
||
* @returns {boolean} - Returns TRUE if the current character has access to the room.
|
||
*/
|
||
function ChatRoomCurrentCharacterCanAccessRoom() { return ChatRoomCharacterCanAccessRoom(CurrentCharacter); }
|
||
|
||
/**
|
||
* Checks if the player can leave the chatroom.
|
||
* @returns {boolean} - Returns TRUE if the player can leave the current chat room.
|
||
*/
|
||
function ChatRoomCanLeave() {
|
||
if (ChatRoomLeashPlayer != null) { // Cannot leave if leashed
|
||
if (ChatRoomCanBeLeashedBy(0, Player)) {
|
||
return false;
|
||
} else ChatRoomLeashPlayer = null;
|
||
}
|
||
if (!Player.CanWalk()) return false; // Cannot leave if cannot walk
|
||
if ((ChatRoomData.Game == "Prison") && PandoraPenitentiaryIsInmate(Player)) return false; // Cannot leave if locked down in Pandora Penitentiary
|
||
if (ChatRoomIsLocked() && (ChatRoomData.Game == "GGTS")) return false; // GGTS game can forbid anyone to leave
|
||
if (ChatRoomActiveView.CanLeave && !ChatRoomActiveView.CanLeave()) return false;
|
||
if (ChatRoomPlayerCanAccessRoom()) return true;
|
||
for (let C = 0; C < ChatRoomCharacter.length; C++)
|
||
if (ChatRoomData.Admin.indexOf(ChatRoomCharacter[C].MemberNumber) >= 0)
|
||
return false; // Cannot leave if the room is locked and there's an administrator inside
|
||
return true; // Can leave if the room is locked and there's no administrator inside
|
||
}
|
||
|
||
/** When slowed, we can't leave quicker than this */
|
||
const ChatRoomSlowLeaveMinTime = 5000;
|
||
|
||
/**
|
||
* Calculates the slow leave duration
|
||
*
|
||
* @param {number} level - The slow level
|
||
* @param {number} skill - The evasion skill level
|
||
* @param {number} min - The minimum cap
|
||
* @returns {number} The slow leave duration
|
||
*/
|
||
function ChatRoomSlowLeaveDuration(level, skill, min) {
|
||
const timerLength = min + 2000 * level;
|
||
const evasionModifier = 500 * skill;
|
||
const leaveTime = Math.round(Math.max(min, timerLength - evasionModifier));
|
||
return leaveTime;
|
||
}
|
||
|
||
function ChatRoomAttemptLeave() {
|
||
// Check that we can actually exit the room
|
||
if (!ChatRoomCanLeave()) return;
|
||
|
||
if (Player.IsSlow()) {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.build();
|
||
if (ChatRoomSlowtimer !== 0) {
|
||
// Timer is already running, cancel it
|
||
ServerSend("ChatRoomChat", { Content: "SlowLeaveCancel", Type: "Action", Dictionary });
|
||
ChatRoomSlowLeaveCancel();
|
||
} else {
|
||
ServerSend("ChatRoomChat", { Content: "SlowLeaveAttempt", Type: "Action", Dictionary });
|
||
ChatRoomStatusUpdate("Crawl");
|
||
ChatRoomSlowtimer = CurrentTime + ChatRoomSlowLeaveDuration(Player.GetSlowLevel(), SkillGetLevel(Player, "Evasion"), ChatRoomSlowLeaveMinTime);
|
||
}
|
||
} else {
|
||
ChatRoomLeave();
|
||
CommonSetScreen("Online", "ChatSearch");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Whether the player is currently leaving slowly
|
||
*/
|
||
function ChatRoomIsLeavingSlowly() {
|
||
return ChatRoomSlowtimer !== 0 && CurrentTime < ChatRoomSlowtimer;
|
||
}
|
||
|
||
/**
|
||
* Cancel our attempt at leaving slowly
|
||
*/
|
||
function ChatRoomSlowLeaveCancel() {
|
||
if (!ChatRoomIsLeavingSlowly()) return;
|
||
ChatRoomSlowtimer = 0;
|
||
ChatRoomSlowStop = false;
|
||
}
|
||
|
||
/**
|
||
* Processing function for the slow-leave "state machine"
|
||
*/
|
||
function ChatRoomProcessSlowLeave() {
|
||
if (ChatRoomSlowtimer === 0) {
|
||
// Timer isn't running, skip
|
||
return;
|
||
}
|
||
|
||
if (!ChatRoomCanLeave()) {
|
||
// Something blocked us from leaving
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "SlowLeaveInterrupt", Type: "Action", Dictionary });
|
||
ServerSend("ChatRoomChat", { Content: "SlowLeaveInterrupt", Type: "Hidden", Dictionary });
|
||
ChatRoomSlowLeaveCancel();
|
||
} else if (ChatRoomSlowStop) {
|
||
// We were stopped by someone, reset
|
||
console.log(`stopped from leaving`);
|
||
ChatRoomSlowLeaveCancel();
|
||
} else if (!Player.IsSlow() || CurrentTime > ChatRoomSlowtimer) {
|
||
// We're not slowed anymore, or the slow timer expired, exit
|
||
ChatRoomLeave();
|
||
CommonSetScreen("Online", "ChatSearch");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Make the player exit from the current chatroom
|
||
* @param {boolean} clearCharacters - Whether the online character cache should be cleared
|
||
*/
|
||
function ChatRoomLeave(clearCharacters = false) {
|
||
ChatRoomCustomizationClear();
|
||
ChatRoomSlowtimer = 0;
|
||
ChatRoomSlowStop = false;
|
||
DialogLentLockpicks = false;
|
||
ChatRoomData = null;
|
||
Player.MapData = null;
|
||
ChatRoomGame = "";
|
||
ChatRoomCharacter = [];
|
||
ChatRoomDrawFocusList = [];
|
||
ChatRoomMapViewTileFog = "";
|
||
ChatRoomMapViewObjectFog = "";
|
||
ChatRoomSetTarget(-1);
|
||
ChatRoomClearAllElements();
|
||
ChatRoomSetLastChatRoom(null);
|
||
ServerSend("ChatRoomLeave", "");
|
||
if (clearCharacters)
|
||
CharacterDeleteAllOnline();
|
||
}
|
||
|
||
/**
|
||
* Keyboard handler for keys common to all subviews
|
||
* @type {KeyboardEventListener}
|
||
*/
|
||
function ChatRoomCommonKeyDown(event) {
|
||
if (CommonKey.IsPressed(event, "Escape")) {
|
||
// Escape toggles between main view and chat
|
||
ElementScrollToEnd("TextAreaChatLog");
|
||
ElementFocus("InputChat");
|
||
return true;
|
||
} else if (CommonKey.IsPressed(event, "k", CommonKey.ALT)) {
|
||
if (!event.repeat) ChatRoomToggleKneel();
|
||
return true;
|
||
} else if (CommonKey.IsPressed(event, "i", CommonKey.ALT)) {
|
||
if (CurrentCharacter) { DialogLeave(); }
|
||
ChatRoomOpenInformationScreen();
|
||
return true;
|
||
} else if (CommonKey.IsPressed(event, "b", CommonKey.ALT)) {
|
||
if (CurrentCharacter) { DialogLeave(); }
|
||
ChatRoomOpenWardrobeScreen();
|
||
return true;
|
||
} else if (CommonKey.IsPressed(event, "h", CommonKey.ALT)) {
|
||
if (CurrentCharacter) { DialogLeave(); }
|
||
ChatRoomOpenAdminScreen();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Handles keyboard shortcuts in the chatroom screen.
|
||
* @type {KeyboardEventListener}
|
||
*/
|
||
function ChatRoomKeyDown(event) {
|
||
const inputChat = /** @type {HTMLTextAreaElement} */(document.getElementById("InputChat"));
|
||
const chatHasFocus = inputChat && document.activeElement === inputChat;
|
||
const modifiers = CommonKey.GetModifiers(event);
|
||
|
||
if (chatHasFocus) {
|
||
// Why are these room-level event listeners!?
|
||
if (!modifiers) {
|
||
switch (event.key) {
|
||
case "Tab":
|
||
// Autocomplete whatever the chat contains
|
||
if (inputChat.value.length !== 0) {
|
||
CommandAutoComplete(inputChat.value);
|
||
return true;
|
||
}
|
||
break;
|
||
case "Enter":
|
||
// The ENTER key sends the chat.
|
||
ChatRoomSendChat();
|
||
return true;
|
||
case "PageUp":
|
||
case "PageDown":
|
||
// On page up/down, we scroll through chat history
|
||
ChatRoomScrollHistory(event.key === "PageUp");
|
||
return true;
|
||
case "Escape":
|
||
// Escape toggles between main view and chat
|
||
ElementFocus("MainCanvas");
|
||
return true;
|
||
}
|
||
}
|
||
} else if (ChatRoomCommonKeyDown(event)) {
|
||
return true;
|
||
} else if (ChatRoomActiveView.KeyDown && ChatRoomActiveView.KeyDown(event)) {
|
||
return true;
|
||
}
|
||
|
||
if (CommonKey.IsPressed(event, "Escape", CommonKey.Shift)) {
|
||
// Try to leave the room on Shift-Escape
|
||
ChatRoomAttemptLeave();
|
||
}
|
||
|
||
if (
|
||
document.activeElement === null
|
||
|| document.activeElement === document.body
|
||
|| document.activeElement.id === "MainCanvas"
|
||
) {
|
||
const chatLog = /** @type {HTMLDivElement} */(document.getElementById("TextAreaChatLog"));
|
||
if (!chatLog) {
|
||
return false;
|
||
}
|
||
|
||
if (CommonKey.NavigationKeyDown(chatLog, event, (el) => el.clientHeight * 0.05)) {
|
||
return true;
|
||
} else if (CommonKey.IsPressed(event, "v", CommonKey.CTRL)) {
|
||
return false;
|
||
} else if ((event.key.length === 1 || event.key === "Enter") && !modifiers) {
|
||
// Move focus to the chat
|
||
ElementFocus("InputChat");
|
||
// We have to lie here so that the first keystroke without
|
||
// focus still goes through, but we want to eat Enter so
|
||
// that a newline doesn't get through
|
||
return event.key === "Enter";
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Scroll through the chat history
|
||
*
|
||
* @param {boolean} up Whether to scroll up or down
|
||
* @returns {void}
|
||
*/
|
||
function ChatRoomScrollHistory(up) {
|
||
if (up && ChatRoomLastMessageIndex > 0) {
|
||
ChatRoomLastMessageIndex--;
|
||
} else if (!up && ChatRoomLastMessageIndex < ChatRoomLastMessage.length) {
|
||
ChatRoomLastMessageIndex++;
|
||
}
|
||
ElementValue("InputChat", ChatRoomLastMessage[ChatRoomLastMessageIndex]);
|
||
}
|
||
|
||
/**
|
||
* Sends the chat message to the room
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSendChat() {
|
||
const inputChat = /** @type {null | HTMLTextAreaElement} */(document.getElementById("InputChat"));
|
||
if (!inputChat) {
|
||
return;
|
||
}
|
||
|
||
// If there's a message to send
|
||
let msg = inputChat.value.trim();
|
||
if (!msg.length) return;
|
||
|
||
// Keeps the chat log in memory so it can be accessed with pageup/pagedown
|
||
ChatRoomLastMessage.push(msg);
|
||
ChatRoomLastMessageIndex = ChatRoomLastMessage.length;
|
||
|
||
// Handle a possible command
|
||
const parsed = CommandParse(msg);
|
||
if (typeof parsed === "string") {
|
||
msg = parsed;
|
||
} else {
|
||
return;
|
||
}
|
||
|
||
// Maximum of 1000 characters sent
|
||
if (msg.length > ServerChatMessageMaxLength) return;
|
||
|
||
// Emote parsing
|
||
if (msg.startsWith("*") || (Player.ChatSettings.MuStylePoses && msg.startsWith(":") && msg.length > 3)) {
|
||
if (msg.match(/^\*\d*%/)) {
|
||
ChatRoomSendAttemptEmote(msg);
|
||
} else {
|
||
ChatRoomSendEmote(msg);
|
||
}
|
||
inputChat.value = "";
|
||
inputChat.dispatchEvent(new Event("input"));
|
||
return;
|
||
}
|
||
|
||
// GGTS chat parser for completing tasks
|
||
AsylumGGTSChatToParse = msg;
|
||
|
||
let clearChat = false;
|
||
// Check if this is a whisper
|
||
let whisperStatus = ChatRoomSendWhisper(ChatRoomTargetMemberNumber, msg);
|
||
if (whisperStatus === "target-gone") {
|
||
ChatRoomSendLocal(`<span style="color: red">${TextGet("WhisperTargetGone")}</span>`);
|
||
return;
|
||
} else if (whisperStatus === "target-out-of-range") {
|
||
ChatRoomSendLocal(`<span style="color: red">${TextGet("WhisperTargetOutOfRange")}</span>`);
|
||
return;
|
||
} else if (whisperStatus) {
|
||
clearChat = true;
|
||
} else {
|
||
// This must be a regular chat message
|
||
const status = ChatRoomSendChatMessage(msg);
|
||
if (status) {
|
||
clearChat = true;
|
||
}
|
||
}
|
||
|
||
if (clearChat) {
|
||
inputChat.value = "";
|
||
inputChat.dispatchEvent(new Event("input"));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Send a player message to the server
|
||
*
|
||
* This function automatically formats and sends the message with all the information
|
||
* needed to reconstruct it on the receiver.
|
||
*
|
||
* @param {"Chat"|"Whisper"|"Emote"} type
|
||
* @param {string} msg
|
||
* @param {string} [replyId]
|
||
* @return {ServerChatRoomMessage}
|
||
*/
|
||
function ChatRoomGenerateChatRoomChatMessage(type, msg, replyId) {
|
||
/** @type {ChatMessageDictionary} */
|
||
const Dictionary = [];
|
||
if (type === "Chat" || type === "Whisper") {
|
||
// Ensure the last OOC content range is closed with a )
|
||
const lastRange = SpeechGetOOCRanges(msg).pop();
|
||
if (
|
||
Player.ChatSettings.OOCAutoClose
|
||
&& lastRange !== undefined
|
||
&& msg.charAt(lastRange.start + lastRange.length - 1) !== ")"
|
||
&& lastRange.start + lastRange.length === msg.length
|
||
&& lastRange.length !== 1) {
|
||
msg += ")";
|
||
}
|
||
|
||
// Chat messages are always garbled, unless the no speech restriction is lifted
|
||
let process = {
|
||
effects: [],
|
||
text: msg,
|
||
};
|
||
|
||
process = SpeechTransformProcess(Player, msg, SpeechTransformSenderEffects);
|
||
|
||
// We always garble, but if "no speech garbling" is enabled,
|
||
// we also send down the ungarbled message if it's different
|
||
const originalMsg = Player.RestrictionSettings.NoSpeechGarble && msg !== process.text ? msg : undefined;
|
||
Dictionary.push({ Effects: process.effects, Original: originalMsg });
|
||
}
|
||
|
||
if (replyId) {
|
||
Dictionary.push({ ReplyId: replyId, Tag: "ReplyId" });
|
||
ChatRoomMessageReplyStop();
|
||
}
|
||
return { Content: msg, Type: type, Dictionary };
|
||
}
|
||
|
||
/**
|
||
* Send a specific chat message to the room
|
||
* @param {string} msg
|
||
* @returns
|
||
*/
|
||
function ChatRoomSendChatMessage(msg) {
|
||
// Regular chat can be prevented with an owner presence rule, also validates for forbidden words
|
||
if (ChatRoomOwnerPresenceRule("BlockTalk", null)) return false;
|
||
if (!ChatRoomOwnerForbiddenWordCheck(msg)) return false;
|
||
const replyId = ChatRoomMessageGetReplyId();
|
||
const data = ChatRoomGenerateChatRoomChatMessage("Chat", msg, replyId);
|
||
ServerSend("ChatRoomChat", data);
|
||
|
||
ChatRoomStimulationMessage("Talk");
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Send a whisper to a specific character
|
||
* @param {number} targetNumber
|
||
* @param {string} msg
|
||
*/
|
||
function ChatRoomSendWhisper(targetNumber, msg) {
|
||
if (targetNumber < 0) return false;
|
||
const targetChar = ChatRoomCharacter.find(C => C.MemberNumber === targetNumber);
|
||
if (!targetChar) return "target-gone";
|
||
|
||
if (ChatRoomMapViewIsActive() && !ChatRoomMapViewCharacterOnWhisperRange(targetChar)) {
|
||
return "target-out-of-range";
|
||
}
|
||
|
||
const replyId = ChatRoomMessageGetReplyId();
|
||
const data = ChatRoomGenerateChatRoomChatMessage("Whisper", msg, replyId);
|
||
data.Target = targetNumber;
|
||
ServerSend("ChatRoomChat", data);
|
||
|
||
// We're self-whispering, and that would cause two messages to show up
|
||
// (one added below, the other added when it gets received).
|
||
// Skip this one if that's the case
|
||
if (targetChar.IsPlayer()) return true;
|
||
data.Sender = Player.MemberNumber;
|
||
|
||
ChatRoomMessage(data);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Sends message to user with HTML tags
|
||
* @param {string} Content - InnerHTML for the message
|
||
* @param {number} [Timeout] - total time to display the message in ms
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSendLocal(Content, Timeout) {
|
||
ChatRoomMessage({
|
||
Sender: Player.MemberNumber,
|
||
Type: "LocalMessage",
|
||
Content, Timeout,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Removes (*) (/me) (/action) then sends message as emote
|
||
* @param {string} msg - Emote message
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSendEmote(msg) {
|
||
// Emotes can be prevented with an owner presence rule
|
||
if (ChatRoomOwnerPresenceRule("BlockEmote", null)) return;
|
||
if (Player.ChatSettings.MuStylePoses && msg.startsWith(":")) msg = msg.substring(1);
|
||
else {
|
||
msg = msg.replace(/^\*/, "").replace(/\*$/, "");
|
||
if (msg.startsWith(CommandsKey + "me ")) msg = msg.replace(CommandsKey + "me ", "");
|
||
if (msg.startsWith(CommandsKey + "action ")) msg = msg.replace(CommandsKey + "action ", "*");
|
||
}
|
||
msg = msg.trim();
|
||
if (msg == "" || msg == "*") return;
|
||
const replyId = ChatRoomMessageGetReplyId();
|
||
const data = ChatRoomGenerateChatRoomChatMessage("Emote", msg, replyId);
|
||
ServerSend("ChatRoomChat", data);
|
||
}
|
||
|
||
/**
|
||
* Sends message as attempt emote. Emote can be failed or succeeded.
|
||
* @param {string} msg - Emote message
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSendAttemptEmote(msg) {
|
||
// Emotes can be prevented with an owner presence rule
|
||
if (ChatRoomOwnerPresenceRule("BlockEmote", null)) return;
|
||
const match = msg.match(/^(\*|\/attempt )(\d{0,2}|100)%(.+)/);
|
||
|
||
if (!match) {
|
||
ServerSend("ChatRoomChat", { Content: msg, Type: "Emote" });
|
||
return;
|
||
}
|
||
|
||
msg = msg.replace(/^(\*|\/attempt )(\d{0,2}|100)%/, "");
|
||
msg = msg.trim();
|
||
if (msg == "") return;
|
||
|
||
const chance = parseInt(match[2] ? match[2] : 50);
|
||
const random = parseInt(Math.random() * 100);
|
||
|
||
const attemptSucceeded = random <= chance;
|
||
msg += attemptSucceeded ? ": ✔" : ": ❌";
|
||
msg += " (" + chance + "%)";
|
||
|
||
const replyId = ChatRoomMessageGetReplyId();
|
||
const data = ChatRoomGenerateChatRoomChatMessage("Emote", msg, replyId);
|
||
ServerSend("ChatRoomChat", data);
|
||
return;
|
||
}
|
||
|
||
/**
|
||
* Publishes common player actions (add, remove, swap) to the rest of the chatroom.
|
||
*
|
||
* Note that this will *not* update the server database,
|
||
* which requires either {@link CharacterRefresh}, {@link ServerPlayerAppearanceSync} or {@link ChatRoomCharacterUpdate}.
|
||
*
|
||
* @param {Character} C - Character on which the action is done.
|
||
* @param {string} Action - Action modifier
|
||
* @param {Item} PrevItem - The item that has been removed.
|
||
* @param {Item} NextItem - The item that has been added.
|
||
* @returns {boolean} - whether we published anything to the chat.
|
||
*/
|
||
function ChatRoomPublishAction(C, Action, PrevItem, NextItem) {
|
||
// Make sure we're in a chat room
|
||
if (CurrentScreen !== "ChatRoom")
|
||
return false;
|
||
|
||
const dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.destinationCharacter(C)
|
||
.targetCharacter(C);
|
||
|
||
if (PrevItem != null) {
|
||
dictionary.asset(PrevItem.Asset, "PrevAsset", PrevItem.Craft && PrevItem.Craft.Name);
|
||
}
|
||
if (NextItem != null) {
|
||
dictionary.asset(NextItem.Asset, "NextAsset", NextItem.Craft && NextItem.Craft.Name);
|
||
}
|
||
if (C.FocusGroup != null) {
|
||
dictionary.focusGroup(C.FocusGroup.Name);
|
||
}
|
||
|
||
// Prepares the item packet to be sent to other players in the chatroom
|
||
ChatRoomCharacterItemUpdate(C);
|
||
|
||
// Sends the result to the server and leaves the dialog if we need to
|
||
ServerSend("ChatRoomChat", { Content: Action, Type: "Action", Dictionary: dictionary.build() });
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Updates an item on character for everyone in a chat room - replaces ChatRoomCharacterUpdate to cut on the lag.
|
||
*
|
||
* Note that this will *not* update the server database,
|
||
* which requires either {@link CharacterRefresh}, {@link ServerPlayerAppearanceSync} or {@link ChatRoomCharacterUpdate}.
|
||
*
|
||
* @param {Character} C - Character to update.
|
||
* @param {AssetGroupName} [Group] - Item group to update.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomCharacterItemUpdate(C, Group) {
|
||
if ((Group == null) && (C.FocusGroup != null)) Group = C.FocusGroup.Name;
|
||
if ((CurrentScreen == "ChatRoom") && (Group != null)) {
|
||
// Single item updates aren't sent back to the source member, so update the ChatRoomData accordingly
|
||
if (ChatRoomData && ChatRoomData.Character) {
|
||
const characterIndex = ChatRoomData.Character.findIndex((char) => char.MemberNumber === C.MemberNumber);
|
||
if (characterIndex !== -1) {
|
||
// @ts-expect-error FIXME: This is mistaking raw server data with unpacked character data
|
||
ChatRoomData.Character[characterIndex] = C;
|
||
}
|
||
}
|
||
|
||
const Item = InventoryGet(C, Group);
|
||
/** @type {ServerCharacterItemUpdate} */
|
||
const P = {
|
||
Target: C.MemberNumber,
|
||
Group: Group,
|
||
Name: (Item != null) ? Item.Asset.Name : undefined,
|
||
Color: (Item != null && Item.Color != null) ? Item.Color : "Default",
|
||
Difficulty: (Item != null) ? Item.Difficulty - Item.Asset.Difficulty : SkillGetWithRatio(Player, "Bondage"),
|
||
Property: ((Item != null) && (Item.Property != null)) ? Item.Property : undefined,
|
||
Craft: ((Item != null) && (Item.Craft != null)) ? Item.Craft : undefined,
|
||
};
|
||
ServerSend("ChatRoomCharacterItemUpdate", P);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Updates an item on a character for everyone in a chat room, for expression changes only.
|
||
*
|
||
* Note that this will *not* update the server database,
|
||
* which requires either {@link CharacterRefresh}, {@link ServerPlayerAppearanceSync} or {@link ChatRoomCharacterUpdate}.
|
||
*
|
||
* @param {Character} C
|
||
* @param {ExpressionGroupName} Group
|
||
*/
|
||
function ChatRoomCharacterExpressionUpdate(C, Group) {
|
||
if (!ServerPlayerIsInChatRoom() || !C.IsPlayer()) return;
|
||
const item = InventoryGet(C, Group);
|
||
if (!item) return;
|
||
|
||
const Name = item.Property ? item.Property.Expression : null;
|
||
ServerSend("ChatRoomCharacterExpressionUpdate", { Name, Group, Appearance: ServerAppearanceBundle(C.Appearance) });
|
||
}
|
||
|
||
/**
|
||
* Publishes a custom action to the rest of the chatroom.
|
||
*
|
||
* Note that this will *not* update the server database,
|
||
* which requires either {@link CharacterRefresh}, {@link ServerPlayerAppearanceSync} or {@link ChatRoomCharacterUpdate}.
|
||
*
|
||
* @param {string} msg - Tag of the action to send
|
||
* @param {boolean} LeaveDialog - Whether or not the dialog should be left.
|
||
* @param {ChatMessageDictionary} Dictionary - Dictionary of tags and data to send
|
||
* to the room.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomPublishCustomAction(msg, LeaveDialog, Dictionary) {
|
||
if (CurrentScreen == "ChatRoom") {
|
||
ServerSend("ChatRoomChat", { Content: msg, Type: "Action", Dictionary });
|
||
const C = CharacterGetCurrent();
|
||
if (C) ChatRoomCharacterItemUpdate(C);
|
||
if (LeaveDialog && (C != null)) DialogLeave();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Pushes the new character data/appearance to everyone else in a chat room and updates the server database.
|
||
*
|
||
* Note that this will *not* update the server database,
|
||
* which requires either {@link CharacterRefresh} or {@link ServerPlayerAppearanceSync}.
|
||
*
|
||
* @param {Character} C - Character to update.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomCharacterUpdate(C) {
|
||
if (ChatRoomAllowCharacterUpdate) {
|
||
const data = {
|
||
ID: C.CharacterID,
|
||
ActivePose: C.ActivePose,
|
||
Appearance: ServerAppearanceBundle(C.Appearance)
|
||
};
|
||
ServerSend("ChatRoomCharacterUpdate", data);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Regex used to split out a string at word boundaries
|
||
*
|
||
* Note that due to a bug in Safari, we can't use the one that handles chinese characters properly.
|
||
*/
|
||
// const mentionNameSplitter = /(?<=[^\w\u0000\u007F])|(?=[^\w\u0000\u007F])/gu;
|
||
const mentionNameSplitter = /\b/gu;
|
||
|
||
/**
|
||
* Checks if the message contains mentions of the character. Case-insensitive.
|
||
* @param {Character} C - Character to check mentions of
|
||
* @param {string} msg - The message to check for mentions
|
||
* @returns {boolean} - msg contains mention of C
|
||
*/
|
||
function ChatRoomMessageMentionsCharacter(C, msg) {
|
||
const normalizeText = (text) => text.normalize('NFKC').toLowerCase();
|
||
const nameParts = normalizeText(C.Name).split(mentionNameSplitter);
|
||
const nickParts = C.Nickname ? normalizeText(C.Nickname).split(mentionNameSplitter) : [];
|
||
const msgParts = normalizeText(msg).split(mentionNameSplitter);
|
||
for (let i = 0; i < msgParts.length - (nameParts.length - 1); i++) {
|
||
if (msgParts[i] === nameParts[0]) {
|
||
let match = true;
|
||
for (let j = 0; j < nameParts.length; j++) {
|
||
if (msgParts[i + j] !== nameParts[j]) {
|
||
match = false;
|
||
break;
|
||
}
|
||
}
|
||
if (match) return true;
|
||
}
|
||
if (msgParts[i] === nickParts[0]) {
|
||
let match = true;
|
||
for (let j = 0; j < nickParts.length; j++) {
|
||
if (msgParts[i + j] !== nickParts[j]) {
|
||
match = false;
|
||
break;
|
||
}
|
||
}
|
||
if (match) return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Escapes a given string.
|
||
* @param {string} str - Text to escape.
|
||
* @returns {string} - Escaped string.
|
||
*/
|
||
function ChatRoomHTMLEntities(str) {
|
||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
/**
|
||
* Check is the player is either the sender of a message, or its target.
|
||
*
|
||
* @param {ServerChatRoomMessage} data - The chat message to check for involvment.
|
||
* @returns {boolean} true if the player is involved, false otherwise.
|
||
*/
|
||
function ChatRoomMessageInvolvesPlayer(data) {
|
||
return (data.Sender == Player.MemberNumber
|
||
|| Array.isArray(data.Dictionary) && data.Dictionary.some(d => {
|
||
return IsCharacterReferenceDictionaryEntry(d) && d.MemberNumber === Player.MemberNumber
|
||
|| IsTargetCharacterDictionaryEntry(d) && d.TargetCharacter === Player.MemberNumber
|
||
|| IsSourceCharacterDictionaryEntry(d) && d.SourceCharacter === Player.MemberNumber;
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Checks whether the given character's interactions are impacted by the player's sensory-deprivation.
|
||
*
|
||
* @param {Character} character - The character to check.
|
||
* @returns true if the player is sensory-deprived from character, false otherwise.
|
||
*/
|
||
function ChatRoomIsCharacterImpactedBySensoryDeprivation(character) {
|
||
return PreferenceIsPlayerInSensDep() && character.MemberNumber != Player.MemberNumber && (!ChatRoomSenseDepBypass || ChatRoomImpactedBySenseDep.includes(character));
|
||
}
|
||
|
||
/** @type {ChatRoomMessageExtractor[]} */
|
||
var ChatRoomMessageExtractors = [
|
||
ChatRoomMessageDefaultMetadataExtractor,
|
||
];
|
||
|
||
/**
|
||
* Global list of handlers for incoming messages.
|
||
* @type {ChatRoomMessageHandler[]}
|
||
* */
|
||
var ChatRoomMessageHandlers = [
|
||
{
|
||
Description: "Reset minigame on room updates",
|
||
Priority: -210,
|
||
Callback: (data, _sender, _msg) => {
|
||
if (data.Type === "Action" && data.Content === "ServerUpdateRoom")
|
||
OnlineGameReset();
|
||
return false;
|
||
}
|
||
},
|
||
{
|
||
Description: "Ghosted player handling",
|
||
Priority: -200,
|
||
Callback: (data, _sender, _msg, __metadata) => {
|
||
if (data.Type === "Action" && data.Content === "ServerUpdateRoom")
|
||
return false;
|
||
|
||
if (Player.GhostList.indexOf(data.Sender) >= 0)
|
||
return true;
|
||
}
|
||
},
|
||
{
|
||
Description: "Process status messages",
|
||
Priority: -100,
|
||
Callback: (data, sender, _msg) => {
|
||
if (data.Type == "Status") {
|
||
ChatRoomStatusUpdateLocalCharacter(sender, data.Content);
|
||
return true;
|
||
}
|
||
}
|
||
},
|
||
{
|
||
Description: "Break leash after a server disconnect",
|
||
Priority: -100,
|
||
Callback: (data, sender, _msg) => {
|
||
if (data.Type === "Action" && data.Content.startsWith("ServerDisconnect") && sender.MemberNumber == ChatRoomLeashPlayer)
|
||
ChatRoomLeashPlayer = null;
|
||
return false;
|
||
}
|
||
},
|
||
{
|
||
Description: "Process hidden messages",
|
||
Priority: -1,
|
||
Callback: (data, sender, _msg) => {
|
||
if (data.Type === "Hidden")
|
||
return ChatRoomMessageProcessHidden(data, sender);
|
||
}
|
||
},
|
||
{
|
||
Description: "Emote messages formatting",
|
||
Priority: 0,
|
||
Callback: (data, _sender, msg, metadata) => {
|
||
if (data.Type === "Emote") {
|
||
if (msg.indexOf('*') === 0) {
|
||
// **-message, yank starting *
|
||
msg = msg.substring(1);
|
||
} else {
|
||
// *-message, prepend sender name and a space if needed
|
||
const sep = (msg.indexOf("'") === 0 || msg.indexOf(",") === 0);
|
||
msg = metadata.senderName + (!sep ? " " : "") + msg;
|
||
}
|
||
}
|
||
|
||
return { msg: msg };
|
||
}
|
||
},
|
||
{
|
||
Description: "Sensory-deprivation processing",
|
||
Priority: 100,
|
||
Callback: (data, sender, msg, metadata) => {
|
||
|
||
// Emotes parsing - Player is under sensory-dep, replace every character name from the message with the placeholder
|
||
if (data.Type == "Emote") {
|
||
if (ChatRoomIsCharacterImpactedBySensoryDeprivation(sender)) {
|
||
metadata.senderName = InterfaceTextGet("Someone");
|
||
msg = SpeechAnonymize(msg, ChatRoomCharacter);
|
||
}
|
||
}
|
||
|
||
// Chat parsing - Garble the sender name and adds more garble when deaf
|
||
if (data.Type == "Chat") {
|
||
if (ChatRoomIsCharacterImpactedBySensoryDeprivation(sender)) {
|
||
if (Player.GetDeafLevel() >= 4)
|
||
metadata.senderName = InterfaceTextGet("Someone");
|
||
else {
|
||
const s = SpeechTransformProcess(sender, metadata.senderName, SpeechTransformReceiverEffects);
|
||
metadata.senderName = s.text;
|
||
}
|
||
}
|
||
if (Player.GetDeafLevel() > 0) {
|
||
const s = SpeechTransformProcess(sender, msg, SpeechTransformReceiverEffects);
|
||
msg = s.text;
|
||
}
|
||
}
|
||
|
||
return { msg: msg };
|
||
|
||
}
|
||
},
|
||
{
|
||
Description: "Save chats and whispers to the chat log",
|
||
Priority: 110,
|
||
Callback: (data, sender, msg, metadata) => {
|
||
if (data.Type == "Chat" || data.Type == "Whisper") {
|
||
if (ChatRoomMapViewIsActive() && !ChatRoomMapViewCharacterIsHearable(sender)) {
|
||
// Respect the map view hearing range
|
||
return false;
|
||
}
|
||
const Original = data.Content;
|
||
if (Player.GetDeafLevel() > 0) {
|
||
const s = SpeechTransformProcess(sender, data.Content, SpeechTransformReceiverEffects);
|
||
data.Content = s.text;
|
||
}
|
||
ChatRoomChatLog.push({ Chat: data.Content, Garbled: msg, Original, SenderName: metadata.senderName, SenderMemberNumber: sender.MemberNumber, Time: CommonTime() });
|
||
if (ChatRoomChatLog.length > 6) ChatRoomChatLog.splice(0, 1);
|
||
}
|
||
return false;
|
||
}
|
||
},
|
||
{
|
||
Description: "Handle action visual effects",
|
||
Priority: 120,
|
||
Callback: (data, sender, msg, metadata) => {
|
||
let intensity = null;
|
||
if (data.Type === "Action" && metadata.ShockIntensity >= 0) {
|
||
intensity = metadata.ShockIntensity;
|
||
} else if (data.Type === "Activity" && data.Content.includes("ShockItem")) {
|
||
intensity = 1.5;
|
||
if (metadata.FocusGroup) {
|
||
let item = InventoryGet(Player, metadata.FocusGroup.Name);
|
||
if (item && item.Property && item.Property.ShockLevel != null) {
|
||
intensity = 1.5 * item.Property.ShockLevel;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (intensity !== null && metadata.TargetCharacter.IsPlayer()) {
|
||
const duration = (Math.random() + intensity) * 500;
|
||
DrawFlashScreen("#FFFFFF", duration, 500);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
},
|
||
{
|
||
Description: "Hide automatic actions that don't involve the player, per preferences",
|
||
Priority: 200,
|
||
Callback: (data, sender, msg, metadata) => {
|
||
if (data.Type !== "Action")
|
||
return false;
|
||
|
||
const IsPlayerInvolved = ChatRoomMessageInvolvesPlayer(data);
|
||
if (metadata.Automatic && !IsPlayerInvolved && !Player.ChatSettings.ShowAutomaticMessages)
|
||
return true;
|
||
return false;
|
||
}
|
||
},
|
||
{
|
||
Description: "Handle stimulation events",
|
||
Priority: 210,
|
||
Callback: (data, sender, msg, metadata) => {
|
||
if (data.Type !== "Action")
|
||
return false;
|
||
|
||
const IsPlayerInvolved = ChatRoomMessageInvolvesPlayer(data);
|
||
if (["HelpKneelDown", "HelpStandUp"].includes(data.Content) && IsPlayerInvolved)
|
||
ChatRoomStimulationMessage("Kneel");
|
||
|
||
return false;
|
||
}
|
||
},
|
||
{
|
||
Description: "Arousal processing",
|
||
Priority: 210,
|
||
Callback: (data, sender, msg, metadata) => {
|
||
if (
|
||
!["Action", "ServerMessage", "Activity"].includes(data.Type)
|
||
|| !metadata.ActivityName
|
||
|| !metadata.FocusGroup
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
const {ActivityCounter, ActivityName, ActivityAsset, FocusGroup, TargetMemberNumber} = metadata;
|
||
|
||
const arousalEnabled = (Player.ArousalSettings && (Player.ArousalSettings.Active === "Hybrid" || Player.ArousalSettings.Active === "Automatic"));
|
||
|
||
AsylumGGTSActivity(sender, metadata.TargetCharacter, metadata.ActivityName, FocusGroup.Name, metadata.ActivityCounter);
|
||
|
||
// If another player is using an item which applies an activity on the current player, apply the effect here
|
||
if (
|
||
arousalEnabled
|
||
&& ActivityName
|
||
&& TargetMemberNumber
|
||
&& TargetMemberNumber === Player.MemberNumber
|
||
&& sender.MemberNumber !== Player.MemberNumber
|
||
) {
|
||
ActivityEffect(sender, Player, ActivityName, FocusGroup.Name, ActivityCounter, ActivityAsset);
|
||
}
|
||
return false;
|
||
}
|
||
},
|
||
{
|
||
Description: "Hide anything per sensory deprivation rules",
|
||
Priority: 300,
|
||
Callback: (data, sender, msg, metadata) => {
|
||
const IsPlayerInvolved = ChatRoomMessageInvolvesPlayer(data);
|
||
|
||
const IsPlayerInSensoryDep = Player.ImmersionSettings.SenseDepMessages
|
||
&& PreferenceIsPlayerInSensDep()
|
||
&& Player.GetDeafLevel() >= 4
|
||
&& (!ChatRoomSenseDepBypass || ChatRoomImpactedBySenseDep.includes(sender));
|
||
|
||
// When the player is in total sensory deprivation, hide non-whisper messages if the player is not involved
|
||
const IsPlayerMentioned = ChatRoomMessageMentionsCharacter(Player, msg);
|
||
switch (data.Type) {
|
||
case "Whisper":
|
||
return false;
|
||
case "Chat":
|
||
return IsPlayerInSensoryDep && !IsPlayerInvolved && !IsPlayerMentioned && !SpeechGetOOCRanges(msg).length;
|
||
default:
|
||
return IsPlayerInSensoryDep && !IsPlayerInvolved && !IsPlayerMentioned;
|
||
}
|
||
}
|
||
},
|
||
{
|
||
Description: "Hide sexual activity messages, per preferences",
|
||
Priority: 310,
|
||
Callback: (data, sender, msg, metadata) => {
|
||
if (data.Type === "Activity" && (Player.ChatSettings != null) && (Player.ChatSettings.ShowActivities != null) && !Player.ChatSettings.ShowActivities)
|
||
return true;
|
||
return false;
|
||
}
|
||
},
|
||
{
|
||
Description: "Audio system hook for sound effects",
|
||
Priority: 500,
|
||
Callback: (data, sender, msg, metadata) => AudioPlaySoundForChatMessage(data, sender, msg, metadata),
|
||
},
|
||
{
|
||
Description: "Raise a notification if required",
|
||
Priority: 500,
|
||
Callback: (data, sender, msg, metadata) => {
|
||
const IsPlayerInvolved = ChatRoomMessageInvolvesPlayer(data);
|
||
if ((["Action", "Activity"].includes(data.Type) && IsPlayerInvolved && Player.NotificationSettings.ChatMessage.Activity)
|
||
|| (data.Type === "Chat" && Player.NotificationSettings.ChatMessage.Normal)
|
||
|| (data.Type === "Whisper" && Player.NotificationSettings.ChatMessage.Whisper)
|
||
|| (Player.NotificationSettings.ChatMessage.Mention && ChatRoomMessageMentionsCharacter(Player, msg)))
|
||
ChatRoomNotificationRaiseChatMessage(sender, msg);
|
||
return false;
|
||
}
|
||
},
|
||
{
|
||
Description: "Push message to the chat",
|
||
Priority: 500,
|
||
Callback: (data, sender, msg, metadata) => {
|
||
ChatRoomMessageDisplay(data, msg, sender, metadata);
|
||
return false;
|
||
}
|
||
}
|
||
];
|
||
|
||
/**
|
||
* Adds a function to the list of message extractors.
|
||
*
|
||
* @see ChatRoomMessageExtractor for more info.
|
||
*
|
||
* @param {ChatRoomMessageExtractor} func - The extractor to register
|
||
*/
|
||
function ChatRoomRegisterMessageExtractor(func) {
|
||
if (typeof func !== "function") {
|
||
console.error("Invalid message extractor registration");
|
||
return;
|
||
}
|
||
|
||
ChatRoomMessageExtractors.push(func);
|
||
}
|
||
|
||
/**
|
||
* Adds a function to the list of message handlers
|
||
*
|
||
* @see ChatRoomMessageHandler for more info.
|
||
*
|
||
* @param {ChatRoomMessageHandler} handler - The handler to register
|
||
*/
|
||
function ChatRoomRegisterMessageHandler(handler) {
|
||
if (!handler || typeof handler.Priority !== "number" || typeof handler.Callback !== "function") {
|
||
console.error("Invalid message handler registration");
|
||
return;
|
||
}
|
||
|
||
ChatRoomMessageHandlers.push(handler);
|
||
}
|
||
|
||
/**
|
||
* Performs the processing for an hidden message.
|
||
*
|
||
* @param {ServerChatRoomMessage} data
|
||
* @param {Character} SenderCharacter
|
||
* @returns {boolean}
|
||
*/
|
||
function ChatRoomMessageProcessHidden(data, SenderCharacter) {
|
||
if (data.Content == "RuleInfoGet") ChatRoomGetLoadRules(SenderCharacter);
|
||
else if (data.Content == "RuleInfoSet") {
|
||
// @ts-expect-error That message uses .Dictionary as storage for LogRecord[]
|
||
ChatRoomSetLoadRules(SenderCharacter, data.Dictionary);
|
||
}
|
||
else if (data.Content.startsWith("StruggleAssist")) {
|
||
let A = parseInt(data.Content.substr("StruggleAssist".length));
|
||
if ((A >= 1) && (A <= 7)) {
|
||
ChatRoomStruggleAssistTimer = CurrentTime + 60000;
|
||
ChatRoomStruggleAssistBonus = A;
|
||
}
|
||
}
|
||
else if (data.Content == "SlowStop") {
|
||
ChatRoomSlowtimer = CurrentTime + 45000;
|
||
ChatRoomSlowStop = true;
|
||
}
|
||
else if (data.Content == "OnlineStruggleInterrupt") {
|
||
StruggleChatRoomEndAmination();
|
||
}
|
||
else if (data.Content.startsWith("MaidDrinkPick")) {
|
||
let A = parseInt(data.Content.substr("MaidDrinkPick".length));
|
||
if ((A == 0) || (A == 5) || (A == 10)) MaidQuartersOnlineDrinkPick(data.Sender, A);
|
||
}
|
||
else if (data.Content.startsWith("PayQuest")) {
|
||
const money = parseInt(data.Content.substring(8));
|
||
ChatRoomPayQuest(data.Sender, money);
|
||
}
|
||
else if (data.Content.startsWith("OwnerRule") || data.Content.startsWith("LoverRule")) {
|
||
ChatRoomSetRule(data);
|
||
}
|
||
else if (data.Content == "HoldLeash") {
|
||
ChatRoomDoHoldLeash(SenderCharacter);
|
||
}
|
||
else if (data.Content == "StopHoldLeash") {
|
||
ChatRoomDoStopHoldLeash(SenderCharacter);
|
||
}
|
||
else if (data.Content == "PingHoldLeash") {
|
||
ChatRoomDoPingLeashedPlayers(SenderCharacter);
|
||
}
|
||
else if (data.Content == "RemoveLeash") {
|
||
ChatRoomDoRemoveLeash(SenderCharacter);
|
||
}
|
||
else if (data.Content == "GiveLockpicks") DialogLentLockpicks = true;
|
||
else if (data.Content == "RequestFullKinkyDungeonData") {
|
||
if (CurrentScreen == "KinkyDungeon") {
|
||
KinkyDungeonStreamingPlayers.push(SenderCharacter.MemberNumber);
|
||
KinkyDungeonSendData(KinkyDungeonPackData(true, true, true, true));
|
||
}
|
||
}
|
||
else if (data.Content == "TakeSuitcase") {
|
||
if (!Player.CanInteract() && ServerChatRoomGetAllowItem(SenderCharacter, Player)) {
|
||
let misc = InventoryGet(Player, "ItemMisc");
|
||
if (KidnapLeagueSearchingPlayers.length == 0) {
|
||
if (misc && misc.Asset && misc.Asset.Name == "BountySuitcase") {
|
||
KidnapLeagueSearchFinishTime = CommonTime() + KidnapLeagueSearchFinishDuration;
|
||
ChatRoomPublishCustomAction("OnlineBountySuitcaseStart", true, [
|
||
{ Tag: "SourceCharacter", Text: CharacterNickname(SenderCharacter), MemberNumber: SenderCharacter.MemberNumber },
|
||
{ Tag: "DestinationCharacterName", Text: CharacterNickname(Player), MemberNumber: Player.MemberNumber },
|
||
]);
|
||
} else if (misc && misc.Asset && misc.Asset.Name == "BountySuitcaseEmpty") {
|
||
KidnapLeagueSearchFinishTime = CommonTime() + KidnapLeagueSearchFinishDuration;
|
||
ChatRoomPublishCustomAction("OnlineBountySuitcaseStartOpened", true, [
|
||
{ Tag: "SourceCharacter", Text: CharacterNickname(SenderCharacter), MemberNumber: SenderCharacter.MemberNumber },
|
||
{ Tag: "DestinationCharacterName", Text: CharacterNickname(Player), MemberNumber: Player.MemberNumber },
|
||
]);
|
||
}
|
||
} else {
|
||
ServerSend("ChatRoomGame", {
|
||
OnlineBounty: {
|
||
finishTime: KidnapLeagueSearchFinishTime,
|
||
target: SenderCharacter.MemberNumber,
|
||
}
|
||
});
|
||
}
|
||
if (!KidnapLeagueSearchingPlayers.includes(SenderCharacter.MemberNumber)) {
|
||
KidnapLeagueSearchingPlayers.push(SenderCharacter.MemberNumber);
|
||
}
|
||
|
||
}
|
||
}
|
||
else if (data.Content == "ReceiveSuitcaseMoney") {
|
||
ChatRoomReceiveSuitcaseMoney();
|
||
} else if (data.Content.substr(0, 4) == "GGTS") {
|
||
AsylumGGTSHiddenMessage(SenderCharacter, data.Content, data);
|
||
} else if (data.Content.substr(0, 7) == "Pandora") {
|
||
PandoraPenitentiaryHiddenMessage(SenderCharacter, data.Content);
|
||
} else if (data.Content.startsWith("PortalLink")) {
|
||
PortalLinkProcessMessage(SenderCharacter, data);
|
||
}
|
||
else if (data.Content == "ClubCardAdminResetGameHidden") {
|
||
ClubCardResetGameStatus();
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Extracts the metadata and message substitutions from a message's dictionary.
|
||
*
|
||
* @param {ServerChatRoomMessage} data - The message to parse.
|
||
* @param {Character} SenderCharacter - The resolved character that sent that message.
|
||
* @returns {{ metadata: IChatRoomMessageMetadata, substitutions: CommonSubtituteSubstitution[] }}
|
||
*/
|
||
function ChatRoomMessageDefaultMetadataExtractor(data, SenderCharacter) {
|
||
/** @type {CommonSubtituteSubstitution[]} */
|
||
const substitutions = [];
|
||
/** @type {IChatRoomMessageMetadata} */
|
||
const meta = {};
|
||
|
||
meta.senderName = CharacterNickname(SenderCharacter);
|
||
|
||
if (!data.Dictionary) {
|
||
return { metadata: meta, substitutions };
|
||
}
|
||
|
||
// Loop through dictionary entries and extract message metadata & collect substitutions where possible
|
||
for (let entry of data.Dictionary) {
|
||
if (IsSourceCharacterDictionaryEntry(entry)) {
|
||
const {SourceCharacter} = entry;
|
||
const C = Character.find((c) => c.MemberNumber === SourceCharacter);
|
||
if (C) {
|
||
meta.SourceCharacter = C;
|
||
}
|
||
} else if (IsTargetCharacterDictionaryEntry(entry)) {
|
||
const {TargetCharacter, Index} = entry;
|
||
const C = Character.find((c) => c.MemberNumber === TargetCharacter);
|
||
if (C) {
|
||
if (Index) {
|
||
if (!meta.AdditionalTargets) meta.AdditionalTargets = {};
|
||
meta.AdditionalTargets[Index] = C;
|
||
} else {
|
||
meta.TargetCharacter = C;
|
||
meta.TargetMemberNumber = C.MemberNumber;
|
||
}
|
||
}
|
||
} else if (IsCharacterReferenceDictionaryEntry(entry)) {
|
||
const {Tag, MemberNumber} = entry;
|
||
const C = Character.find((c) => c.MemberNumber === MemberNumber);
|
||
if (C) {
|
||
switch (Tag) {
|
||
case "SourceCharacter":
|
||
meta.SourceCharacter = C;
|
||
break;
|
||
case "TargetCharacter":
|
||
case "TargetCharacterName":
|
||
case "DestinationCharacter":
|
||
case "DestinationCharacterName":
|
||
meta.TargetCharacter = C;
|
||
meta.TargetMemberNumber = C.MemberNumber;
|
||
}
|
||
}
|
||
} else if (IsAssetReferenceDictionaryEntry(entry)) {
|
||
const { Tag, GroupName, AssetName, CraftName } = entry;
|
||
const asset = Asset.find(a => a.Name === AssetName && (!GroupName || a.Group.Name === GroupName));
|
||
if (asset) {
|
||
if (!meta.Assets) meta.Assets = {};
|
||
meta.Assets[Tag] = asset;
|
||
if (CraftName) {
|
||
if (!meta.CraftingNames) meta.CraftingNames = {};
|
||
meta.CraftingNames[Tag] = CraftName;
|
||
}
|
||
}
|
||
} else if (IsGroupReferenceDictionaryEntry(entry)) {
|
||
const group = AssetGroupGet("Female3DCG", entry.GroupName);
|
||
if (group) {
|
||
if (!meta.Groups) meta.Groups = {};
|
||
if (!meta.Groups[entry.Tag]) meta.Groups[entry.Tag] = [];
|
||
meta.Groups[entry.Tag].push(group);
|
||
}
|
||
} else if (IsFocusGroupDictionaryEntry(entry)) {
|
||
const group = /** @type {AssetItemGroup} */ (AssetGroupGet("Female3DCG", entry.FocusGroupName));
|
||
if (group) {
|
||
meta.FocusGroup = group;
|
||
meta.GroupName = group.Name;
|
||
}
|
||
} else if (IsAssetGroupNameDictionaryEntry(entry)) {
|
||
const group = /** @type {AssetItemGroup} */ (AssetGroupGet("Female3DCG", entry.AssetGroupName));
|
||
if (group) {
|
||
meta.FocusGroup = group;
|
||
meta.GroupName = group.Name;
|
||
}
|
||
} else if (IsAutomaticEventDictionaryEntry(entry)) {
|
||
meta.Automatic = true;
|
||
} else if (IsShockEventDictionaryEntry(entry)) {
|
||
meta.ShockIntensity = entry.ShockIntensity;
|
||
} else if (IsActivityCounterDictionaryEntry(entry)) {
|
||
meta.ActivityCounter = entry.ActivityCounter;
|
||
} else if (IsActivityNameDictionaryEntry(entry)) {
|
||
meta.ActivityName = entry.ActivityName;
|
||
} else if (IsTextDictionaryEntry(entry)) {
|
||
let {Tag, Text} = entry;
|
||
if (Tag === "ChatRoomName") {
|
||
Text = ChatSearchMuffle(Text);
|
||
meta.ChatRoomName = Text;
|
||
}
|
||
substitutions.push([Tag, Text.toString()]);
|
||
} else if (IsTextLookupDictionaryEntry(entry)) {
|
||
let text = AssetTextGet(entry.TextToLookUp);
|
||
if (!text || text.startsWith(TEXT_NOT_FOUND_PREFIX)) {
|
||
text = InterfaceTextGet(entry.TextToLookUp);
|
||
} else {
|
||
// We lower-case here to blend in asset names in messages
|
||
text = text.toLowerCase();
|
||
}
|
||
substitutions.push([entry.Tag, text]);
|
||
} else if (IsMsgIdDictionaryEntry(entry)) {
|
||
meta.MsgId = entry.MsgId;
|
||
} else if (IsReplyIdDictionaryEntry(entry)) {
|
||
meta.ReplyId = entry.ReplyId;
|
||
}
|
||
}
|
||
|
||
// Now collect any additional substitutions from the complete metadata
|
||
|
||
// If there's a source character, add substitutions for the SourceCharacter tag
|
||
if (meta.SourceCharacter) {
|
||
substitutions.push(...ChatRoomGetSourceCharacterSubstitutions(data, meta.SourceCharacter));
|
||
}
|
||
|
||
// If there's a target character, add substitutions for the various target character tags
|
||
if (meta.TargetCharacter) {
|
||
const isSelf = SenderCharacter.MemberNumber === meta.TargetMemberNumber;
|
||
substitutions.push(...ChatRoomGetTargetCharacterSubstitutions(meta.TargetCharacter, isSelf));
|
||
}
|
||
|
||
if (meta.AdditionalTargets) {
|
||
for (const [index, C] of Object.entries(meta.AdditionalTargets)) {
|
||
const isSelf = SenderCharacter.MemberNumber === C.MemberNumber;
|
||
substitutions.push(...ChatRoomGetTargetCharacterSubstitutions(C, isSelf, Number(index)));
|
||
}
|
||
}
|
||
|
||
// If there's a focus group, add a substitution for the group name
|
||
if (meta.FocusGroup) {
|
||
substitutions.push(...ChatRoomGetFocusGroupSubstitutions(data, meta.FocusGroup, meta.TargetCharacter));
|
||
}
|
||
|
||
// If there are referenced groups, add a substitution for the collected groups under their pluralized tag
|
||
if (meta.Groups) {
|
||
for (const [tag, groups] of Object.entries(meta.Groups)) {
|
||
const groupNames = groups.map(group => {
|
||
if (meta.TargetCharacter) {
|
||
return DialogActualNameForGroup(meta.TargetCharacter, group).toLowerCase();
|
||
}
|
||
return group.Description;
|
||
});
|
||
groupNames.sort((a, b) => a.localeCompare(b));
|
||
substitutions.push([tag + "s", CommonArrayJoinPretty(groupNames)]);
|
||
}
|
||
}
|
||
|
||
// If there are referenced assets, substitute asset names
|
||
if (meta.Assets) {
|
||
const character = meta.SourceCharacter || Player;
|
||
// Go over the asset references and collect appropriate substitutions
|
||
for (const [tag, asset] of Object.entries(meta.Assets)) {
|
||
if (tag === "ActivityAsset") {
|
||
meta.ActivityAsset = asset;
|
||
}
|
||
|
||
const craftingName = meta.CraftingNames && meta.CraftingNames[tag];
|
||
const assetName = asset.DynamicDescription(character).toLowerCase();
|
||
if (craftingName) {
|
||
substitutions.push([tag, `${craftingName} (${assetName})`]);
|
||
} else {
|
||
substitutions.push([tag, assetName]);
|
||
}
|
||
}
|
||
}
|
||
|
||
return { metadata: meta, substitutions };
|
||
}
|
||
|
||
/**
|
||
* Gets a set of dictionary substitutions used when the given character is the source character of a chat message.
|
||
* @param {ServerChatRoomMessage} data - The raw message data
|
||
* @param {Character} character - The source character
|
||
* @returns {CommonSubtituteSubstitution[]} - A list of dictionary substitutions that should be applied
|
||
*/
|
||
function ChatRoomGetSourceCharacterSubstitutions(data, character) {
|
||
/** @type {CommonSubtituteSubstitution[]} */
|
||
const substitutions = [];
|
||
const isServerEnterLeave = ["ServerEnter", "ServerLeave", "ServerDisconnect"].includes(data.Content);
|
||
let name = CharacterNickname(character);
|
||
const hideIdentity = ChatRoomHideIdentity(character);
|
||
|
||
// Alter server messages to show both the name and the nickname
|
||
if (isServerEnterLeave) {
|
||
if (name !== character.Name) {
|
||
name += ` [${character.Name}]`;
|
||
}
|
||
substitutions.push(["SourceCharacter", name]);
|
||
} else if (hideIdentity) {
|
||
name = InterfaceTextGet("Someone");
|
||
}
|
||
|
||
substitutions.push(["SourceCharacter", name]);
|
||
|
||
const pronounRepls = ChatRoomPronounSubstitutions(character, "Pronoun", hideIdentity);
|
||
substitutions.push(...pronounRepls);
|
||
return substitutions;
|
||
}
|
||
|
||
/**
|
||
* Gets a set of dictionary substitutions used when the given character is the target character of a chat message.
|
||
* @param {Character} character - The target character
|
||
* @param {boolean} isSelf - If true, indicates that the target character is also the sender of the message (i.e. is
|
||
* doing something to themselves)
|
||
* @param {number} [index] - If the character is an additional target, the index that the substitution tags should be
|
||
* given
|
||
* @returns {CommonSubtituteSubstitution[]} - A list of dictionary substitutions that should be applied
|
||
*/
|
||
function ChatRoomGetTargetCharacterSubstitutions(character, isSelf, index) {
|
||
/** @type {CommonSubtituteSubstitution[]} */
|
||
const substitutions = [];
|
||
const hideIdentity = ChatRoomHideIdentity(character);
|
||
const pronounPossessive = CharacterPronoun(character, "Possessive", hideIdentity);
|
||
const pronounSelf = CharacterPronoun(character, "Self", hideIdentity);
|
||
let destinationCharacter;
|
||
let destinationCharacterName;
|
||
let targetCharacter;
|
||
let targetCharacterName;
|
||
if (hideIdentity) {
|
||
const someone = InterfaceTextGet("Someone").toLowerCase();
|
||
destinationCharacter = isSelf ? pronounPossessive : someone;
|
||
destinationCharacterName = someone;
|
||
targetCharacter = isSelf ? pronounSelf : someone;
|
||
targetCharacterName = someone;
|
||
} else {
|
||
const name = CharacterNickname(character);
|
||
destinationCharacterName = `${name}${InterfaceTextGet("'s")}`;
|
||
destinationCharacter = isSelf ? pronounPossessive : destinationCharacterName;
|
||
targetCharacter = isSelf ? pronounSelf : name;
|
||
targetCharacterName = name;
|
||
}
|
||
|
||
const suffix = index ? `${index}` : '';
|
||
|
||
substitutions.push(
|
||
[`DestinationCharacter${suffix}`, destinationCharacter],
|
||
[`DestinationCharacterName${suffix}`, destinationCharacterName],
|
||
[`TargetCharacter${suffix}`, targetCharacter],
|
||
[`TargetCharacterName${suffix}`, targetCharacterName],
|
||
);
|
||
|
||
const pronounRepls = ChatRoomPronounSubstitutions(character, "TargetPronoun", hideIdentity);
|
||
substitutions.push(...pronounRepls);
|
||
return substitutions;
|
||
}
|
||
|
||
/**
|
||
* Gets a set of dictionary substitutions used for the focus group
|
||
* @param {ServerChatRoomMessage} data - The raw message data
|
||
* @param {AssetGroup} focusGroup - The group being acted upon by the chat message
|
||
* @param {Character} targetCharacter - The target character of the message
|
||
* @returns {[string,string][]} - A list of dictionary substitutions that should be applied
|
||
*/
|
||
function ChatRoomGetFocusGroupSubstitutions(data, focusGroup, targetCharacter) {
|
||
if (targetCharacter) {
|
||
return [["FocusAssetGroup", DialogActualNameForGroup(targetCharacter, focusGroup).toLowerCase()]];
|
||
} else {
|
||
console.warn(`Received message "${data.Content}" with focus group but no target character, assuming target's biology...`);
|
||
return [["FocusAssetGroup", focusGroup.Description]];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Extracts all metadata and substitutions requested by a message.
|
||
*
|
||
* This goes through ChatRoomMessageExtractors and calls them in order
|
||
* on the recieved message, collecting their output (metadata & tag substitutions).
|
||
*
|
||
* @param {ServerChatRoomMessage} data
|
||
* @param {Character} sender
|
||
* @returns {{ metadata?: IChatRoomMessageMetadata, substitutions?: CommonSubtituteSubstitution[] }}
|
||
*/
|
||
function ChatRoomMessageRunExtractors(data, sender) {
|
||
if (!data || !sender) return {};
|
||
|
||
let metadata = {};
|
||
let substitutions = [];
|
||
|
||
ChatRoomMessageExtractors.forEach(extractor => {
|
||
let extracted = extractor(data, sender);
|
||
|
||
if (extracted.metadata && typeof extracted.metadata === "object")
|
||
Object.assign(metadata, extracted.metadata);
|
||
if (extracted.substitutions && Array.isArray(extracted.substitutions))
|
||
substitutions = substitutions.concat(extracted.substitutions);
|
||
});
|
||
|
||
return { metadata, substitutions };
|
||
}
|
||
|
||
/**
|
||
* Run the message handlers on a given message.
|
||
*
|
||
* This runs a message and its metadata through the prioritized list
|
||
* of ChatRoomMessageHandlers, and stops processing if one of them
|
||
* requests it, ignoring the rest.
|
||
*
|
||
* @param {"pre"|"post"} type - The type of processing to perform
|
||
* @param {ServerChatRoomMessage} data - The recieved message
|
||
* @param {Character} sender - The actual message sender character object
|
||
* @param {string} msg - The escaped message, likely different from data.Contents
|
||
* @param {IChatRoomMessageMetadata} [metadata] - The message metadata, only available for post-handlers
|
||
* @returns {boolean | string}
|
||
*/
|
||
function ChatRoomMessageRunHandlers(type, data, sender, msg, metadata) {
|
||
if (!['pre', 'post'].includes(type) || !data || !sender) return;
|
||
|
||
// Gather the handlers for the requested processing and sort by priority
|
||
const handlers = ChatRoomMessageHandlers.filter(proc => (type === "pre" && proc.Priority < 0 || type === "post" && proc.Priority >= 0));
|
||
handlers.sort((a, b) => a.Priority - b.Priority);
|
||
|
||
// Go through the handlers and show them the message
|
||
const originalMsg = msg;
|
||
const skips = [];
|
||
for (const handler of handlers) {
|
||
// Check if one of the handlers wanted us to skip an oncoming handler
|
||
if (skips.some(s => s(handler)))
|
||
continue;
|
||
|
||
const ret = handler.Callback(data, sender, msg, metadata);
|
||
|
||
if (typeof ret === "boolean") {
|
||
// Handler wishes to filter, and true means we should stop
|
||
if (ret)
|
||
return true;
|
||
// Fallthrough, keep processing
|
||
} else if (typeof ret === "object") {
|
||
// Handler wishes to transform, collect their result and continue
|
||
const { msg: newMsg, skip: skip } = ret;
|
||
if (newMsg) msg = newMsg;
|
||
if (skip) skips.push(skip);
|
||
}
|
||
}
|
||
|
||
// If the message was transformed, return it, otherwise just say we're fine
|
||
return msg === originalMsg ? false : msg;
|
||
}
|
||
|
||
/**
|
||
* Handles the reception of a chatroom message.
|
||
*
|
||
* @see ChatRoomMessageHandler for more information
|
||
* @param {ServerChatRoomMessage} data - Message object containing things like the message type, sender, content, etc.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomMessage(data) {
|
||
|
||
// Make sure the message is valid (needs a Sender and Content)
|
||
if (typeof data !== "object" || typeof data.Content !== "string" || typeof data.Sender !== "number")
|
||
return;
|
||
|
||
// Make sure the sender is in the room
|
||
const SenderCharacter = ChatRoomCharacter.find(c => c.MemberNumber == data.Sender);
|
||
if (!SenderCharacter) return;
|
||
|
||
// Make a copy of the message for the purpose of substitutions
|
||
let msg = String(data.Content);
|
||
|
||
const preHandlers = ChatRoomMessageRunHandlers("pre", data, SenderCharacter, msg);
|
||
if (typeof preHandlers === "boolean" && preHandlers)
|
||
return;
|
||
else if (typeof preHandlers === "string")
|
||
msg = preHandlers;
|
||
|
||
// Hidden messages don't go any further
|
||
if (data.Type === "Hidden") return;
|
||
|
||
// Metadata extracted from the message's dictionary
|
||
const { metadata, substitutions } = ChatRoomMessageRunExtractors(data, SenderCharacter);
|
||
|
||
// Substitute actions and server messages for their fulltext version
|
||
switch (data.Type) {
|
||
case "Action":
|
||
{
|
||
let text = AssetTextGet(msg);
|
||
if (!text || text.startsWith(TEXT_NOT_FOUND_PREFIX)) {
|
||
text = InterfaceTextGet(msg);
|
||
}
|
||
msg = text;
|
||
}
|
||
break;
|
||
|
||
case "ServerMessage":
|
||
msg = InterfaceTextGet("ServerMessage" + msg);
|
||
break;
|
||
|
||
case "Activity":
|
||
msg = ActivityDictionaryText(msg);
|
||
break;
|
||
}
|
||
|
||
// Apply requested substitutions
|
||
msg = CommonStringSubstitute(msg, substitutions);
|
||
const messageEntry = /** @type {MessageEffectEntry} */(data.Dictionary?.find(e => IsMessageEffectDictionaryEntry(e)));
|
||
if (messageEntry?.Original && Player.ImmersionSettings.ShowUngarbledMessages) {
|
||
metadata.OriginalMsg = CommonStringSubstitute(messageEntry.Original, substitutions);
|
||
}
|
||
|
||
ChatRoomMessageRunHandlers("post", data, SenderCharacter, msg, metadata);
|
||
}
|
||
|
||
/**
|
||
* @this {HTMLButtonElement}
|
||
*/
|
||
function ChatRoomMessageNameClick() {
|
||
// When clicking on the reply button we always want to message the conversation partner,
|
||
// regardless of whether they or the player were the sender
|
||
const sender = Number.parseInt(this.parentElement?.dataset.sender, 10);
|
||
const target = Number.parseInt(this.parentElement?.dataset.target, 10);
|
||
const memberNumber = sender === Player.MemberNumber && !Number.isNaN(target) ? target : sender;
|
||
const chatInput = /** @type {null | HTMLTextAreaElement} */(document.getElementById("InputChat"));
|
||
if (!chatInput || !ChatRoomCharacter.some(C => C.MemberNumber === memberNumber)) {
|
||
ChatRoomSendLocal(`${TextGet("CommandNoWhisperTarget")} ${memberNumber}.`, 30_000);
|
||
return;
|
||
}
|
||
|
||
chatInput.value = `/whisper ${memberNumber} ${chatInput.value.replace(/\/whisper\s*\d+ ?/u, "")}`;
|
||
chatInput.focus();
|
||
}
|
||
// ----- Replies
|
||
/**
|
||
* Returns the HTML element for the message with the given ID
|
||
* @param {string} id
|
||
* @returns {HTMLElement | null}
|
||
*/
|
||
function ChatRoomMessageGetById(id) {
|
||
const chatLog = document.getElementById("TextAreaChatLog");
|
||
if (!chatLog) return null;
|
||
return chatLog.querySelector(`[msgid="${id}"]`)?.parentElement;
|
||
}
|
||
|
||
/**
|
||
* Returns the ID of the message that this message is a reply to
|
||
* @returns {string | null}
|
||
*/
|
||
function ChatRoomMessageGetReplyId() {
|
||
const chatInput = /** @type {null | HTMLTextAreaElement} */(document.getElementById("InputChat"));
|
||
if (!chatInput) return;
|
||
return chatInput.getAttribute("reply-id");
|
||
}
|
||
/**
|
||
* Returns the name of the character that this message is a reply to.
|
||
* We get the wrong name when replying to reply that's what this is for.
|
||
* @param {string} msgId
|
||
* @param {boolean} isWhisper
|
||
* @returns {string | null}
|
||
*/
|
||
function ChatRoomMessageGetReplyName(msgId, isWhisper=false) {
|
||
const message = ChatRoomMessageGetById(msgId);
|
||
if (message) {
|
||
if (isWhisper) {
|
||
const sender = Number(message.getAttribute("data-sender"));
|
||
return CharacterNickname(ChatRoomCharacter.find(C => C.MemberNumber === sender));
|
||
}
|
||
const names = message.querySelectorAll(".ChatMessageName");
|
||
const name = names[names.length - 1];
|
||
return name?.textContent;
|
||
}
|
||
return null;
|
||
}
|
||
/**
|
||
* @param {string} msgId
|
||
* @returns {string | null}
|
||
*/
|
||
function ChatRoomMessageGetReplyContent(msgId) {
|
||
const message = ChatRoomMessageGetById(msgId);
|
||
if (message) {
|
||
const contents = message.querySelectorAll(".chat-room-message-content");
|
||
const content = contents[contents.length - 1];
|
||
return content.textContent;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
|
||
/**
|
||
* Figures out the type of the message with the given ID
|
||
* @param {string} msgId
|
||
* @returns {"Chat" | "Whisper"}
|
||
*/
|
||
function ChatRoomMessageGetType(msgId) {
|
||
if (!msgId) return "Chat";
|
||
if (ChatRoomMessageGetById(msgId)?.classList?.contains("ChatMessageWhisper")) return "Whisper";
|
||
return "Chat";
|
||
}
|
||
/**
|
||
* Closes the reply.
|
||
*/
|
||
function ChatRoomMessageReplyStop() {
|
||
const chatInput = /** @type {null | HTMLTextAreaElement} */(document.getElementById("InputChat"));
|
||
const replyIndicator = document.getElementById("chat-room-reply-indicator");
|
||
chatInput.removeAttribute("reply-id");
|
||
replyIndicator.classList.add("hidden");
|
||
}
|
||
|
||
/**
|
||
* Sets the reply to the message with the given ID
|
||
* @param {string} msgId
|
||
*/
|
||
function ChatRoomMessageSetReply(msgId) {
|
||
const chatInput = /** @type {null | HTMLTextAreaElement} */(document.getElementById("InputChat"));
|
||
chatInput.setAttribute("reply-id", msgId);
|
||
const replyMessage = ChatRoomMessageGetById(msgId);
|
||
const type = ChatRoomMessageGetType(msgId);
|
||
const isWhisper = type === "Whisper";
|
||
if (isWhisper) {
|
||
const receiver = Number(replyMessage.getAttribute("data-sender")) === Player.MemberNumber ? Number(replyMessage.getAttribute("data-target")) : Number(replyMessage.getAttribute("data-sender"));
|
||
chatInput.value = `/whisper ${receiver} ${chatInput.value.replace(/\/whisper\s*\d+ ?/u, "")}`;
|
||
}
|
||
const replyIndicator = document.getElementById("chat-room-reply-indicator");
|
||
const replyIndicatorText = document.getElementById("chat-room-reply-indicator-text");
|
||
const replyName = ChatRoomMessageGetReplyName(msgId, isWhisper);
|
||
replyIndicatorText.textContent = `${TextGet("ChatRoomReply")}: ${replyName && `${replyName}` || "a message"}`;
|
||
replyIndicator.classList.remove("hidden");
|
||
chatInput.focus();
|
||
}
|
||
|
||
/**
|
||
* Creates the HTML element for a reply message
|
||
* @param {string} msgId
|
||
* @param {string} displayMessage
|
||
* @returns {HTMLSpanElement | string}
|
||
*/
|
||
function ChatRoomMessageCreateReplyMessageElement(msgId, displayMessage) {
|
||
if (!msgId) {
|
||
return displayMessage;
|
||
}
|
||
return ElementCreate({
|
||
tag: "span",
|
||
classList: ["chat-room-message-content"],
|
||
attributes: { "tabindex": -1, "msgid": msgId },
|
||
children: [displayMessage],
|
||
eventListeners: {
|
||
click: (e) => {
|
||
ChatRoomMessageSetReply(msgId);
|
||
e.stopPropagation();
|
||
},
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Update the Chat log with the recieved message
|
||
*
|
||
* @param {ServerChatRoomMessage} data
|
||
* @param {string} msg
|
||
* @param {Character} SenderCharacter
|
||
* @param {IChatRoomMessageMetadata} metadata
|
||
* @returns {HTMLDivElement}
|
||
*/
|
||
function ChatRoomMessageDisplay(data, msg, SenderCharacter, metadata) {
|
||
// Censored words are filtered out, ¶¶¶ indicates that we must not display anything on screen
|
||
const displayMessage = CommonCensor(ChatRoomActiveView.DisplayMessage(data, msg, SenderCharacter, metadata) ?? "¶¶¶");
|
||
if (displayMessage == "¶¶¶") return;
|
||
|
||
// Prepares the HTML tags
|
||
/** @type {(string | Node)[]} */
|
||
const divChildren = [];
|
||
/** @type {undefined | string} */
|
||
let innerHTML = undefined;
|
||
let reply = undefined;
|
||
if (metadata.ReplyId) {
|
||
const replyMessage = ChatRoomMessageGetById(metadata.ReplyId);
|
||
const type = ChatRoomMessageGetType(metadata.ReplyId);
|
||
const isWhisper = type === "Whisper";
|
||
reply = ElementButton.Create(
|
||
null, () => {if (replyMessage) replyMessage.scrollIntoView();}, { noStyling: true },
|
||
{
|
||
button: {
|
||
classList: ["chat-room-message-reply", "truncated-text"],
|
||
attributes: { "tabindex": -1 },
|
||
style: { "--label-color": SenderCharacter.LabelColor },
|
||
children: replyMessage ? [ChatRoomMessageGetReplyName(metadata.ReplyId, isWhisper), ": ", ChatRoomMessageGetReplyContent(metadata.ReplyId)] : [TextGet("ChatRoomNoMessageFound")],
|
||
},
|
||
},
|
||
);
|
||
}
|
||
switch (data.Type) {
|
||
case "Chat":
|
||
divChildren.push(
|
||
reply,
|
||
ElementButton.Create(
|
||
null, ChatRoomMessageNameClick, { noStyling: true },
|
||
{
|
||
button: {
|
||
classList: ["ChatMessageName"],
|
||
attributes: { "tabindex": -1 },
|
||
style: { "--label-color": SenderCharacter.LabelColor },
|
||
children: [CharacterNickname(SenderCharacter)],
|
||
},
|
||
},
|
||
),
|
||
": ",
|
||
ChatRoomMessageCreateReplyMessageElement(metadata.MsgId,displayMessage)
|
||
);
|
||
break;
|
||
case "Whisper": {
|
||
const whisperTarget = SenderCharacter.IsPlayer() ? ChatRoomCharacter.find(c => c.MemberNumber == data.Target) : SenderCharacter;
|
||
divChildren.push(
|
||
reply,
|
||
ElementButton.Create(
|
||
null, ChatRoomMessageNameClick, { noStyling: true },
|
||
{ button: { classList: ["ReplyButton"], children: ["\u21a9\ufe0f"] } },
|
||
),
|
||
SenderCharacter.IsPlayer() ? TextGet("WhisperTo") : TextGetInScope("Screens/Online/ChatRoom/Text_ChatRoom.csv", "WhisperFrom"),
|
||
" ",
|
||
ElementButton.Create(
|
||
null, ChatRoomMessageNameClick, { noStyling: true },
|
||
{
|
||
button: {
|
||
classList: ["ChatMessageName"],
|
||
attributes: { "tabindex": -1 },
|
||
style: { "--label-color": whisperTarget.LabelColor },
|
||
children: [CharacterNickname(whisperTarget)],
|
||
},
|
||
},
|
||
),
|
||
": ",
|
||
ChatRoomMessageCreateReplyMessageElement(metadata.MsgId,displayMessage)
|
||
);
|
||
|
||
if (!whisperTarget.IsPlayer()) {
|
||
document.querySelector(`
|
||
#TextAreaChatLog .ChatMessageWhisper[data-sender="${whisperTarget.MemberNumber}"] > .ReplyButton:not([tabindex='-1']),
|
||
#TextAreaChatLog .ChatMessageWhisper[data-target="${whisperTarget.MemberNumber}"] > .ReplyButton:not([tabindex='-1'])
|
||
`)?.setAttribute("tabindex", "-1");
|
||
}
|
||
break;
|
||
}
|
||
|
||
case "Action":
|
||
case "Activity":
|
||
divChildren.push(reply,ChatRoomMessageCreateReplyMessageElement(metadata.MsgId,`(${displayMessage})`));
|
||
break;
|
||
|
||
case "ServerMessage":
|
||
divChildren.push(ElementCreate({ tag: "br" }), displayMessage);
|
||
break;
|
||
|
||
case "LocalMessage":
|
||
// Local messages can have HTML embedded in them
|
||
innerHTML = data.Content;
|
||
break;
|
||
|
||
case "Emote":
|
||
divChildren.push(reply,ChatRoomMessageCreateReplyMessageElement(metadata.MsgId,`*${displayMessage}*`));
|
||
break;
|
||
|
||
default:
|
||
console.warn(`unknown message type ${data.Type}, ignoring`);
|
||
return;
|
||
}
|
||
|
||
if (metadata.OriginalMsg) {
|
||
divChildren.push(ElementCreate({ tag: "br"}), `[${metadata.OriginalMsg}]`);
|
||
}
|
||
|
||
// Checks if the message is a notification about the user entering or leaving the room
|
||
const classList = ["ChatMessage", `ChatMessage${data.Type}`];
|
||
if (data.Type === "Action" && ["ServerEnter", "ServerLeave", "ServerDisconnect", "ServerBan", "ServerKick"].some(m => data.Content.startsWith(m)))
|
||
classList.push("ChatMessageEnterLeave");
|
||
if ((data.Type != "Chat" && data.Type != "Whisper" && data.Type != "Emote"))
|
||
classList.push("ChatMessageNonDialogue");
|
||
|
||
// Adds the message and scrolls down unless the user has scrolled up
|
||
const div = ElementCreate({
|
||
tag: "div",
|
||
classList,
|
||
dataAttributes: {
|
||
time: ChatRoomCurrentTime(),
|
||
sender: data.Sender,
|
||
target: data.Target,
|
||
},
|
||
innerHTML,
|
||
children: divChildren,
|
||
style: {
|
||
"--label-color": ["Emote", "Action", "Activity"].includes(data.Type) ? SenderCharacter.LabelColor : undefined,
|
||
},
|
||
});
|
||
|
||
if (typeof data.Timeout === 'number' && data.Timeout > 0) setTimeout(() => div.remove(), data.Timeout);
|
||
|
||
// Returns the focus on the chat box
|
||
ChatRoomAppendChat(div);
|
||
return div;
|
||
}
|
||
|
||
/**
|
||
* Whether to replace message details which reveal information about an unseen/unheard character
|
||
* @param {Character} C - The character whose identity should remain unknown
|
||
* @returns {boolean} - Whether the character details should be hidden
|
||
*/
|
||
function ChatRoomHideIdentity(C) {
|
||
return PreferenceIsPlayerInSensDep()
|
||
&& C.MemberNumber != Player.MemberNumber
|
||
&& (!ChatRoomSenseDepBypass || ChatRoomImpactedBySenseDep.includes(C));
|
||
}
|
||
|
||
/**
|
||
* Adds a character into the chat room.
|
||
* @param {Character} newCharacter - The new character to be added to the chat room.
|
||
* @param {ServerChatRoomSyncCharacterResponse["Character"]} newRawCharacter - The raw character data of the new character as it was received from the server.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomAddCharacterToChatRoom(newCharacter, newRawCharacter)
|
||
{
|
||
if (ChatRoomData == null || newCharacter == null || newRawCharacter == null) { return; }
|
||
|
||
// Update the chat room characters
|
||
let characterIndex = ChatRoomCharacter.findIndex(x => x.MemberNumber == newCharacter.MemberNumber);
|
||
if (characterIndex >= 0) // If we found an existing entry...
|
||
{
|
||
// Update it
|
||
ChatRoomCharacter[characterIndex] = newCharacter;
|
||
}
|
||
else // If we didn't update existing data...
|
||
{
|
||
// Push a new entry
|
||
ChatRoomCharacter.push(newCharacter);
|
||
}
|
||
|
||
// Update chat room data backup
|
||
characterIndex = ChatRoomData.Character.findIndex(x => x.MemberNumber == newRawCharacter.MemberNumber);
|
||
if (characterIndex >= 0) // If we found an existing entry...
|
||
{
|
||
// Update it
|
||
ChatRoomData.Character[characterIndex] = newRawCharacter;
|
||
}
|
||
else // If we didn't update existing data...
|
||
{
|
||
// Push a new entry
|
||
ChatRoomData.Character.push(newRawCharacter);
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* Handles the reception of the complete room data from the server.
|
||
* @param {unknown} obj - Room object containing the updated chatroom data.
|
||
* @returns {obj is ChatRoom} - Returns true if the passed properties are valid and false if they're invalid.
|
||
*/
|
||
function ChatRoomValidateProperties(obj)
|
||
{
|
||
const room = /** @type {ChatRoom} */ (obj);
|
||
return room != null && typeof room === "object"
|
||
&& typeof room.Name === "string"
|
||
&& typeof room.Description === "string"
|
||
&& Array.isArray(room.Admin) && room.Admin.every(n => typeof n === "number")
|
||
&& Array.isArray(room.Whitelist) && room.Whitelist.every(n => typeof n === "number")
|
||
&& Array.isArray(room.Ban) && room.Ban.every(n => typeof n === "number")
|
||
&& typeof room.Background === "string"
|
||
&& typeof room.Limit === "number"
|
||
&& typeof room.Game === "string"
|
||
&& Array.isArray(room.Visibility) && room.Visibility.every(v => typeof v === "string")
|
||
&& Array.isArray(room.Access) && room.Access.every(s => typeof s === "string")
|
||
&& Array.isArray(room.BlockCategory) && room.BlockCategory.every(c => typeof c === "string")
|
||
&& (!room.Character || Array.isArray(room.Character) && room.Character.every(c => typeof c === "object"))
|
||
&& typeof room.Language === "string"
|
||
&& typeof room.Space === "string";
|
||
}
|
||
|
||
/**
|
||
* Handles the reception of the data for a room we've just entered.
|
||
*
|
||
* This only happens once per ChatRoom "lifetime".
|
||
*
|
||
* @param {ServerChatRoomSyncMessage} data - Room object containing the new chatroom data.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSync(data) {
|
||
if (data == null || (typeof data !== "object") || typeof data.SourceMemberNumber !== "number") {
|
||
return;
|
||
}
|
||
|
||
// Split out the sender from the rest of the chatroom data
|
||
const senderNumber = data.SourceMemberNumber;
|
||
delete data.SourceMemberNumber;
|
||
|
||
// Validate the actual chatroom properties
|
||
if (!ChatRoomValidateProperties(data)) {
|
||
// Instantly leave the chat room again
|
||
ChatRoomLeave();
|
||
CommonSetScreen("Online", "ChatSearch");
|
||
ChatSearchMessage = "ErrorInvalidRoomProperties";
|
||
return;
|
||
} else if (ChatRoomData) {
|
||
// We're somehow already in a room. Bail out in case our idea of the room we're in doesn't match what the server thinks
|
||
ChatRoomLeave();
|
||
CommonSetScreen("Online", "ChatSearch");
|
||
ChatSearchMessage = "ResponseAlreadyInRoom";
|
||
return;
|
||
}
|
||
|
||
// Set our chat room data to what the server sent us
|
||
ChatRoomData = data;
|
||
|
||
// Loads the room
|
||
if (!ServerPlayerIsInChatRoom() || CurrentScreen === "ChatAdmin") {
|
||
CommonSetScreen("Online", "ChatRoom");
|
||
}
|
||
|
||
if (ChatRoomSep.ActiveElem) {
|
||
ChatRoomSep.SetRoomData(ChatRoomSep.ActiveElem, data);
|
||
}
|
||
|
||
// Load the characters
|
||
ChatRoomCharacter = [];
|
||
for (let C = 0; C < data.Character.length; C++) {
|
||
// Treat chatroom updates from ourselves as if the updated characters had sent them
|
||
const sourceMemberNumber = senderNumber === Player.MemberNumber ? data.Character[C].MemberNumber : senderNumber;
|
||
const Char = CharacterLoadOnline(data.Character[C], sourceMemberNumber);
|
||
ChatRoomCharacter.push(Char);
|
||
}
|
||
|
||
// Reset when creating/joining a room, in case it wasn't by ChatRoomLeave() for whatever reason
|
||
ChatRoomDrawFocusList = [];
|
||
|
||
// If there's a game running in that chatroom, save it and perform a reset
|
||
if (ChatRoomData.Game != null) {
|
||
ChatRoomGame = ChatRoomData.Game;
|
||
OnlineGameReset();
|
||
}
|
||
|
||
ChatRoomPingLeashedPlayers();
|
||
|
||
// Check whether the player's last chatroom data needs updating
|
||
ChatRoomCheckForLastChatRoomUpdates();
|
||
|
||
// Reloads the online game statuses if needed
|
||
OnlineGameLoadStatus();
|
||
|
||
// The allowed menu actions may have changed
|
||
ChatRoomMenuBuild();
|
||
}
|
||
|
||
|
||
/**
|
||
* Handles the reception of the character data of a single player from the server.
|
||
* @param {ServerChatRoomSyncCharacterResponse} data - object containing the character's data.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSyncCharacter(data) {
|
||
if (ChatRoomData == null || data == null || (typeof data !== "object")) {
|
||
return;
|
||
}
|
||
|
||
const newCharacter = CharacterLoadOnline(data.Character, data.SourceMemberNumber);
|
||
ChatRoomAddCharacterToChatRoom(newCharacter, data.Character);
|
||
|
||
}
|
||
|
||
/**
|
||
* Handles the reception of the character data of a newly joined player from the server.
|
||
* @param {ServerChatRoomSyncMemberJoinResponse} data - object containing the joined character's data.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSyncMemberJoin(data) {
|
||
if (ChatRoomData == null || data == null || (typeof data !== "object")) {
|
||
return;
|
||
}
|
||
|
||
//Load the character to the chat room
|
||
const newCharacter = CharacterLoadOnline(data.Character, data.SourceMemberNumber);
|
||
ChatRoomAddCharacterToChatRoom(newCharacter, data.Character);
|
||
|
||
if (Array.isArray(data.WhiteListedBy)) {
|
||
for (const MemberNumber of data.WhiteListedBy) {
|
||
for (const character of Character) {
|
||
if (character.MemberNumber === MemberNumber && Array.isArray(character.WhiteList) && !character.IsPlayer()) {
|
||
if (!character.WhiteList.includes(newCharacter.MemberNumber)) {
|
||
character.WhiteList.push(newCharacter.MemberNumber);
|
||
character.WhiteList.sort((a, b) => a - b);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (Array.isArray(data.BlackListedBy)) {
|
||
for (const MemberNumber of data.BlackListedBy) {
|
||
for (const character of Character) {
|
||
if (character.MemberNumber === MemberNumber && Array.isArray(character.BlackList) && !character.IsPlayer()) {
|
||
if (!character.BlackList.includes(newCharacter.MemberNumber)) {
|
||
character.BlackList.push(newCharacter.MemberNumber);
|
||
character.BlackList.sort((a, b) => a - b);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// After Join Actions
|
||
if (ChatRoomNotificationRaiseChatJoin(newCharacter)) {
|
||
NotificationRaise(NotificationEventType.CHATJOIN, { characterName: newCharacter.Name });
|
||
}
|
||
if (ChatRoomLeashList.includes(newCharacter.MemberNumber)) {
|
||
// Ping to make sure they are still leashed
|
||
ServerSend("ChatRoomChat", { Content: "PingHoldLeash", Type: "Hidden", Target: newCharacter.MemberNumber });
|
||
}
|
||
// Check whether the player's last chatroom data needs updating
|
||
ChatRoomCheckForLastChatRoomUpdates();
|
||
// The allowed menu actions may have changed
|
||
ChatRoomMenuBuild();
|
||
|
||
}
|
||
|
||
/**
|
||
* Handles the reception of the leave notification of a player from the server.
|
||
* @param {ServerChatRoomLeaveResponse} data - Room object containing the leaving character's member number.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSyncMemberLeave(data) {
|
||
if (ChatRoomData == null || data == null || (typeof data !== "object")) {
|
||
return;
|
||
}
|
||
|
||
// Remove the character
|
||
ChatRoomCharacter = ChatRoomCharacter.filter(x => x.MemberNumber != data.SourceMemberNumber);
|
||
ChatRoomDrawFocusList = ChatRoomDrawFocusList.filter(x => x.MemberNumber != data.SourceMemberNumber);
|
||
ChatRoomData.Character = ChatRoomData.Character.filter(x => x.MemberNumber != data.SourceMemberNumber);
|
||
|
||
// Check whether the player's last chatroom data needs updating
|
||
ChatRoomCheckForLastChatRoomUpdates();
|
||
|
||
// The allowed menu actions may have changed
|
||
ChatRoomMenuBuild();
|
||
|
||
//For ClubCard, in case one of the players disconnects from the server, the other player sends a message about it to the game chat.
|
||
if (CurrentScreen === "ClubCard") ClubCardCheckDisconnected(data.SourceMemberNumber);
|
||
}
|
||
|
||
/**
|
||
* Handles the reception of the room properties from the server.
|
||
* @param {ServerChatRoomSyncPropertiesMessage} data - Room object containing the updated chatroom properties.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSyncRoomProperties(data) {
|
||
if (ChatRoomData == null || data == null || typeof data !== "object" || typeof data.SourceMemberNumber !== "number") {
|
||
return;
|
||
}
|
||
|
||
// Split out the sender from the rest of the chatroom data
|
||
delete data.SourceMemberNumber;
|
||
|
||
if(ChatRoomValidateProperties(data) == false) // If the room data we received is invalid...
|
||
{
|
||
ChatRoomLeave();
|
||
ChatSearchMessage = "ErrorInvalidRoomProperties";
|
||
CommonSetScreen("Online", "ChatSearch");
|
||
return;
|
||
}
|
||
|
||
// Copy the received properties to chat room data
|
||
// This uses Object.assign because the character data is missing in that case
|
||
Object.assign(ChatRoomData, data);
|
||
if (ChatRoomSep.ActiveElem) {
|
||
ChatRoomSep.SetRoomData(ChatRoomSep.ActiveElem, data);
|
||
}
|
||
|
||
if (ChatRoomData.Game != null) ChatRoomGame = ChatRoomData.Game;
|
||
|
||
// Check whether the player's last chatroom data needs updating
|
||
ChatRoomCheckForLastChatRoomUpdates();
|
||
|
||
// Reloads the online game statuses if needed
|
||
OnlineGameLoadStatus();
|
||
|
||
// The allowed menu actions may have changed
|
||
ChatRoomMenuBuild();
|
||
|
||
if(ChatRoomActiveView.SyncRoomProperties) ChatRoomActiveView.SyncRoomProperties(data);
|
||
|
||
// Update our leash state
|
||
CharacterRefreshLeash(Player);
|
||
}
|
||
|
||
/**
|
||
* Handles the swapping of two players by a room administrator.
|
||
* @param {ServerChatRoomReorderResponse} data - Object containing the member numbers of the swapped characters.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSyncReorderPlayers(data) {
|
||
if (ChatRoomData == null || data == null || (typeof data !== "object")) {
|
||
return;
|
||
}
|
||
|
||
let newChatRoomCharacter = [];
|
||
let newChatRoomDataCharacter = [];
|
||
let index = 0;
|
||
|
||
for(let i=0; i<data.PlayerOrder.length; i++) // For every player to reorder...
|
||
{
|
||
//Chat Room Characters
|
||
index = ChatRoomCharacter.findIndex(x => x.MemberNumber == data.PlayerOrder[i]);
|
||
newChatRoomCharacter.push(ChatRoomCharacter.splice(index, 1)[0]);
|
||
|
||
//Chat Room Data Backup
|
||
index = ChatRoomData.Character.findIndex(x => x.MemberNumber == data.PlayerOrder[i]);
|
||
newChatRoomDataCharacter.push(ChatRoomData.Character.splice(index, 1)[0]);
|
||
|
||
}
|
||
|
||
if(ChatRoomCharacter.length > 0) // If we forgot about some characters for some reason...
|
||
{
|
||
//Push the missed entries to the end
|
||
Array.prototype.push.apply(newChatRoomCharacter, ChatRoomCharacter);
|
||
}
|
||
if(ChatRoomData.Character.length > 0) // If we forgot about some entries for some reason...
|
||
{
|
||
//Push the missed entries to the end
|
||
Array.prototype.push.apply(newChatRoomDataCharacter, ChatRoomData.Character);
|
||
}
|
||
|
||
//Update the origin arrays
|
||
ChatRoomCharacter = newChatRoomCharacter;
|
||
ChatRoomData.Character = newChatRoomDataCharacter;
|
||
|
||
}
|
||
|
||
/**
|
||
* Updates a single character in the chatroom
|
||
* @param {ServerChatRoomSyncCharacterResponse} data - Data object containing the new character data.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSyncSingle(data) {
|
||
|
||
// Sets the chat room character data
|
||
if (ChatRoomData == null || data == null || typeof data !== "object") return;
|
||
if ((data.Character == null) || (typeof data.Character !== "object")) return;
|
||
for (let C = 0; C < ChatRoomCharacter.length; C++)
|
||
if (ChatRoomCharacter[C].MemberNumber == data.Character.MemberNumber)
|
||
ChatRoomCharacter[C] = CharacterLoadOnline(data.Character, data.SourceMemberNumber);
|
||
|
||
// Keeps a copy of the previous version
|
||
for (let C = 0; C < ChatRoomData.Character.length; C++)
|
||
if (ChatRoomData.Character[C].MemberNumber == data.Character.MemberNumber)
|
||
ChatRoomData.Character[C] = data.Character;
|
||
|
||
}
|
||
|
||
/**
|
||
* Updates a single character's expression in the chatroom.
|
||
* @param {ServerCharacterExpressionResponse} data - Data object containing the new character expression data.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSyncExpression(data) {
|
||
if (
|
||
ChatRoomData == null
|
||
|| !CommonIsObject(data)
|
||
|| typeof data.Group !== "string"
|
||
|| (typeof data.Name !== "string" && data.Name != null)
|
||
) return;
|
||
|
||
const character = ChatRoomCharacter.find(c => c.MemberNumber === data.MemberNumber);
|
||
if (!character) return;
|
||
|
||
const expr = /** @type {ExpressionName} */(data.Name);
|
||
|
||
// Changes the facial expression if the group exists and allows it
|
||
const item = character.Appearance.find(i => (
|
||
i.Asset.Group.Name === data.Group
|
||
&& i.Asset.Group.AllowExpression
|
||
&& (data.Name == null || i.Asset.Group.AllowExpression.includes(expr))
|
||
));
|
||
if (!item) return;
|
||
|
||
if (!item.Property) item.Property = {};
|
||
if (item.Property.Expression !== expr) {
|
||
item.Property.Expression = expr;
|
||
CharacterRefresh(character, false);
|
||
}
|
||
|
||
// Update the cached copy in the chatroom
|
||
const roomCharacter = ChatRoomData.Character.find(c => c.MemberNumber === data.MemberNumber);
|
||
if (roomCharacter) {
|
||
// @ts-expect-error FIXME: This is mistaking raw server data with unpacked character data
|
||
roomCharacter.Appearance = character.Appearance;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Updates a single character's pose in the chatroom.
|
||
* @param {ServerCharacterPoseResponse} data - Data object containing the new character pose data.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSyncPose(data) {
|
||
if (ChatRoomData == null || data == null || typeof data !== "object") return;
|
||
const character = ChatRoomCharacter.find(c => c.MemberNumber === data.MemberNumber);
|
||
if (!character) return;
|
||
|
||
// Sets the active pose; data is validated by the `ActivePose` setter
|
||
character.ActivePose = /** @type {AssetPoseName[]} */(data.Pose);
|
||
CharacterRefresh(character, false);
|
||
|
||
// Update the cached copy in the chatroom
|
||
const roomCharacter = ChatRoomData.Character.find(c => c.MemberNumber === data.MemberNumber);
|
||
if (roomCharacter) {
|
||
roomCharacter.ActivePose = [...character.ActivePose];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Updates a single character's arousal progress in the chatroom.
|
||
* @param {ServerCharacterArousalResponse} data - Data object containing the new character arousal data.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSyncArousal(data) {
|
||
if (ChatRoomData == null || data == null || typeof data !== "object") return;
|
||
const character = ChatRoomCharacter.find(c => c.MemberNumber === data.MemberNumber);
|
||
if (!character || !character.ArousalSettings) return;
|
||
|
||
// Sets the orgasm count & progress
|
||
character.ArousalSettings.OrgasmTimer = data.OrgasmTimer;
|
||
character.ArousalSettings.OrgasmCount = data.OrgasmCount;
|
||
character.ArousalSettings.Progress = data.Progress;
|
||
character.ArousalSettings.ProgressTimer = data.ProgressTimer;
|
||
if ((character.ArousalSettings.AffectExpression == null) || character.ArousalSettings.AffectExpression)
|
||
ActivityExpression(character, character.ArousalSettings.Progress);
|
||
|
||
// Update the cached copy in the chatroom
|
||
const roomCharacter = ChatRoomData.Character.find(c => c.MemberNumber === data.MemberNumber);
|
||
if (roomCharacter && roomCharacter.ArousalSettings) {
|
||
roomCharacter.ArousalSettings.OrgasmTimer = data.OrgasmTimer;
|
||
roomCharacter.ArousalSettings.OrgasmCount = data.OrgasmCount;
|
||
roomCharacter.ArousalSettings.Progress = data.Progress;
|
||
roomCharacter.ArousalSettings.ProgressTimer = data.ProgressTimer;
|
||
// @ts-expect-error FIXME: This is mistaking raw server data with unpacked character data
|
||
roomCharacter.Appearance = character.Appearance;
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Updates a single item on a specific character in the chatroom.
|
||
* @param {ServerChatRoomSyncItemResponse} data - Data object containing the data pertaining to the singular item to update.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomSyncItem(data) {
|
||
if (ChatRoomData == null || (data == null) || (typeof data !== "object") || (data.Source == null) || (typeof data.Source !== "number") || (data.Item == null) || (typeof data.Item !== "object") || (data.Item.Target == null) || (typeof data.Item.Target !== "number") || (data.Item.Group == null) || (typeof data.Item.Group !== "string")) return;
|
||
for (let C = 0; C < ChatRoomCharacter.length; C++)
|
||
if (ChatRoomCharacter[C].MemberNumber === data.Item.Target) {
|
||
|
||
const updateParams = ValidationCreateDiffParams(ChatRoomCharacter[C], data.Source);
|
||
const previousItem = InventoryGet(ChatRoomCharacter[C], data.Item.Group);
|
||
// Go home TS, you're drunk: this explicit downcast should not be necessary, but here we are
|
||
const newItem = data.Item.Name == null ? null : ServerBundledItemToAppearanceItem(ChatRoomCharacter[C].AssetFamily, /** @type {ServerCharacterItemUpdate & { Name: string }} */(data.Item));
|
||
|
||
let { item, valid } = ValidationResolveAppearanceDiff(data.Item.Group, previousItem, newItem, updateParams);
|
||
|
||
ChatRoomAllowCharacterUpdate = false;
|
||
|
||
if (!item || (previousItem && previousItem.Asset.Name !== item.Asset.Name)) {
|
||
InventoryRemove(ChatRoomCharacter[C], data.Item.Group, false);
|
||
}
|
||
|
||
item: if (item) {
|
||
|
||
// Puts the item on the character and apply the craft & property
|
||
const wornItem = CharacterAppearanceSetItem(ChatRoomCharacter[C], item.Asset.Group.Name, item.Asset, item.Color, item.Difficulty, null, false);
|
||
if (wornItem == null) {
|
||
valid = false;
|
||
break item;
|
||
}
|
||
if (item.Craft && CraftingValidate(item.Craft, item.Asset, false, false)) {
|
||
wornItem.Craft = item.Craft;
|
||
}
|
||
wornItem.Property = item.Property;
|
||
|
||
/** @type {AppearanceDiffMap} */
|
||
const diffMap = {};
|
||
for (const appearanceItem of ChatRoomCharacter[C].Appearance) {
|
||
const groupName = appearanceItem.Asset.Group.Name;
|
||
if (groupName === data.Item.Group) {
|
||
diffMap[groupName] = [previousItem, appearanceItem];
|
||
} else {
|
||
diffMap[groupName] = [appearanceItem, appearanceItem];
|
||
}
|
||
}
|
||
|
||
const cyclicBlockSanitizationResult = ValidationResolveCyclicBlocks(ChatRoomCharacter[C].Appearance, diffMap);
|
||
ChatRoomCharacter[C].Appearance = cyclicBlockSanitizationResult.appearance;
|
||
valid = valid && cyclicBlockSanitizationResult.valid;
|
||
}
|
||
|
||
ChatRoomAllowCharacterUpdate = true;
|
||
|
||
// If the update was invalid, send a correction update
|
||
if (ChatRoomCharacter[C].IsPlayer() && !valid) {
|
||
console.warn(`Invalid appearance update to group ${data.Item.Group}. Updating with sanitized appearance.`);
|
||
ChatRoomCharacterUpdate(ChatRoomCharacter[C]);
|
||
} else {
|
||
CharacterRefresh(ChatRoomCharacter[C]);
|
||
}
|
||
|
||
// Keeps the change in the chat room data and allows the character to be updated again
|
||
for (let R = 0; R < ChatRoomData.Character.length; R++) {
|
||
if (ChatRoomData.Character[R].MemberNumber == data.Item.Target)
|
||
// @ts-expect-error FIXME: This is mistaking raw server data with unpacked character data
|
||
ChatRoomData.Character[R].Appearance = ChatRoomCharacter[C].Appearance;
|
||
}
|
||
|
||
return;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Refreshes the chat log elements for Player
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomRefreshChatSettings() {
|
||
if (Player.ChatSettings) {
|
||
for (let property in Player.ChatSettings)
|
||
ElementSetDataAttribute("TextAreaChatLog", property, Player.ChatSettings[property]);
|
||
|
||
ChatRoomSep.UpdateDisplayNames();
|
||
if (Player.GameplaySettings &&
|
||
(Player.GameplaySettings.SensDepChatLog == "SensDepNames" || Player.GameplaySettings.SensDepChatLog == "SensDepTotal" || Player.GameplaySettings.SensDepChatLog == "SensDepExtreme") &&
|
||
(Player.GetDeafLevel() >= 3) &&
|
||
(Player.GetBlindLevel() >= 3)) {
|
||
ElementSetDataAttribute("TextAreaChatLog", "EnterLeave", "Hidden");
|
||
}
|
||
if (Player.GameplaySettings && (Player.GameplaySettings.SensDepChatLog == "SensDepTotal" || Player.GameplaySettings.SensDepChatLog == "SensDepExtreme") && (Player.GetDeafLevel() >= 3) && (Player.GetBlindLevel() >= 3)) {
|
||
ElementSetDataAttribute("TextAreaChatLog", "DisplayTimestamps", "false");
|
||
ElementSetDataAttribute("TextAreaChatLog", "ColorNames", "false");
|
||
ElementSetDataAttribute("TextAreaChatLog", "ColorActions", "false");
|
||
ElementSetDataAttribute("TextAreaChatLog", "ColorEmotes", "false");
|
||
ElementSetDataAttribute("TextAreaChatLog", "ColorActivities", "false");
|
||
ElementSetDataAttribute("TextAreaChatLog", "MemberNumbers", "Never");
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Shows the current character's profile (Information Sheet screen)
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function DialogViewProfile() {
|
||
if (CurrentCharacter != null) {
|
||
const C = CurrentCharacter;
|
||
DialogLeave();
|
||
InformationSheetLoadCharacter(C);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Brings the player into the main hall and starts the maid punishment sequence
|
||
* @returns {void}
|
||
*/
|
||
function DialogCallMaids() {
|
||
ChatRoomLeave(true);
|
||
CommonSetScreen("Room", "MainHall");
|
||
if (!Player.RestrictionSettings || !Player.RestrictionSettings.BypassNPCPunishments) {
|
||
MainHallPunishFromChatroom();
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Triggered when the player assists another player to struggle out, the bonus is evasion / 2 + 1, with penalties if
|
||
* the player is restrained.
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomStruggleAssist() {
|
||
var Bonus = SkillGetLevelReal(Player, "Evasion") / 2 + 1;
|
||
if (!Player.CanInteract()) {
|
||
if (InventoryItemHasEffect(InventoryGet(Player, "ItemArms"), "Block", true)) Bonus = Bonus / 1.5;
|
||
if (InventoryItemHasEffect(InventoryGet(Player, "ItemHands"), "Block", true)) Bonus = Bonus / 1.5;
|
||
if (!Player.CanTalk()) Bonus = Bonus / 1.25;
|
||
}
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.targetCharacter(CurrentCharacter)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "StruggleAssist", Type: "Action", Dictionary });
|
||
ServerSend("ChatRoomChat", { Content: "StruggleAssist" + Math.round(Bonus).toString(), Type: "Hidden", Target: CurrentCharacter.MemberNumber });
|
||
DialogLeave();
|
||
}
|
||
|
||
/**
|
||
|
||
* Triggered when the player assists another player to by giving lockpicks
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomGiveLockpicks() {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.targetCharacter(CurrentCharacter)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "GiveLockpicks", Type: "Action", Dictionary });
|
||
ServerSend("ChatRoomChat", { Content: "GiveLockpicks", Type: "Hidden", Target: CurrentCharacter.MemberNumber });
|
||
DialogLeave();
|
||
}
|
||
|
||
/**
|
||
* Triggered when the player grabs another player's leash
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomHoldLeash() {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.targetCharacter(CurrentCharacter)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "HoldLeash", Type: "Action", Dictionary });
|
||
ServerSend("ChatRoomChat", { Content: "HoldLeash", Type: "Hidden", Target: CurrentCharacter.MemberNumber });
|
||
if (ChatRoomLeashList.indexOf(CurrentCharacter.MemberNumber) < 0)
|
||
ChatRoomLeashList.push(CurrentCharacter.MemberNumber);
|
||
DialogLeave();
|
||
}
|
||
|
||
/**
|
||
* Handle the reply to a leash being held
|
||
* @param {Character} SenderCharacter
|
||
*/
|
||
function ChatRoomDoHoldLeash(SenderCharacter) {
|
||
if (ChatRoomCanBeLeashedBy(SenderCharacter.MemberNumber, Player)) {
|
||
if (SenderCharacter.MemberNumber != ChatRoomLeashPlayer && ChatRoomLeashPlayer != null) {
|
||
// Someone that isn't our current leasher picked up our leash. Inform them that we dropped their leash
|
||
ServerSend("ChatRoomChat", { Content: "RemoveLeash", Type: "Hidden", Target: ChatRoomLeashPlayer });
|
||
}
|
||
// Set the character as our new leasher
|
||
ChatRoomLeashPlayer = SenderCharacter.MemberNumber;
|
||
} else {
|
||
ServerSend("ChatRoomChat", { Content: "RemoveLeash", Type: "Hidden", Target: SenderCharacter.MemberNumber });
|
||
}
|
||
CharacterRefreshLeash(Player);
|
||
}
|
||
|
||
/**
|
||
* Triggered when the player lets go of another player's leash
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomStopHoldLeash() {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.targetCharacter(CurrentCharacter)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "StopHoldLeash", Type: "Action", Dictionary });
|
||
ServerSend("ChatRoomChat", { Content: "StopHoldLeash", Type: "Hidden", Target: CurrentCharacter.MemberNumber });
|
||
if (ChatRoomLeashList.indexOf(CurrentCharacter.MemberNumber) >= 0)
|
||
ChatRoomLeashList.splice(ChatRoomLeashList.indexOf(CurrentCharacter.MemberNumber), 1);
|
||
DialogLeave();
|
||
}
|
||
|
||
/**
|
||
* Handle the reply to a leash being released
|
||
* @param {Character} SenderCharacter
|
||
*/
|
||
function ChatRoomDoStopHoldLeash(SenderCharacter) {
|
||
if (SenderCharacter.MemberNumber == ChatRoomLeashPlayer) {
|
||
ChatRoomLeashPlayer = null;
|
||
CharacterRefreshLeash(Player);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Triggered when a dom enters the room
|
||
* @returns {void} - Nothing.
|
||
*/
|
||
function ChatRoomPingLeashedPlayers() {
|
||
if (ChatRoomLeashList && ChatRoomLeashList.length > 0) {
|
||
for (let P = 0; P < ChatRoomLeashList.length; P++) {
|
||
ServerSend("ChatRoomChat", { Content: "PingHoldLeash", Type: "Hidden", Target: ChatRoomLeashList[P] });
|
||
ServerSend("AccountBeep", { MemberNumber: ChatRoomLeashList[P], BeepType:"Leash"});
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle the reply to a leash ping
|
||
* @param {Character} SenderCharacter
|
||
*/
|
||
function ChatRoomDoPingLeashedPlayers(SenderCharacter) {
|
||
// The dom will ping all players on her leash list and ones that no longer have her as their leasher will remove it
|
||
if (SenderCharacter.MemberNumber != ChatRoomLeashPlayer || !ChatRoomCanBeLeashedBy(SenderCharacter.MemberNumber, Player)) {
|
||
ServerSend("ChatRoomChat", { Content: "RemoveLeash", Type: "Hidden", Target: SenderCharacter.MemberNumber });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle the reply to a leash being broken
|
||
* @param {Character} SenderCharacter
|
||
*/
|
||
function ChatRoomDoRemoveLeash(SenderCharacter) {
|
||
if (ChatRoomLeashList.indexOf(SenderCharacter.MemberNumber) >= 0) {
|
||
ChatRoomLeashList.splice(ChatRoomLeashList.indexOf(SenderCharacter.MemberNumber), 1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Triggered when a character makes another character kneel/stand.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomKneelStandAssist() {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.targetCharacter(CurrentCharacter)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: !CurrentCharacter.IsKneeling() ? "HelpKneelDown" : "HelpStandUp", Type: "Action", Dictionary });
|
||
PoseSetActive(CurrentCharacter, !CurrentCharacter.IsKneeling() ? "Kneel" : "BaseLower", false);
|
||
ChatRoomCharacterUpdate(CurrentCharacter);
|
||
}
|
||
|
||
/**
|
||
* Triggered when a character stops another character from leaving.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomStopLeave() {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.targetCharacter(CurrentCharacter)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "SlowStop", Type: "Action", Dictionary });
|
||
ServerSend("ChatRoomChat", { Content: "SlowStop", Type: "Hidden", Target: CurrentCharacter.MemberNumber });
|
||
DialogLeave();
|
||
}
|
||
|
||
/**
|
||
* Triggered when a character interrupt online struggles on another character.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomOnlineStruggleInterrupt() {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.targetCharacter(CurrentCharacter)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "OnlineStruggleInterrupt", Type: "Action", Dictionary });
|
||
ServerSend("ChatRoomChat", { Content: "OnlineStruggleInterrupt", Type: "Hidden", Target: CurrentCharacter.MemberNumber });
|
||
DialogLeave();
|
||
}
|
||
|
||
/**
|
||
* Sends an administrative command to the server for the chat room from the character dialog.
|
||
* @param {"Move"|"Kick"|"Ban"} ActionType - Type of action performed.
|
||
* @param {boolean | string} [Publish=true] - Whether or not the action should be published.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomAdminAction(ActionType, Publish) {
|
||
if ((CurrentCharacter != null) && (CurrentCharacter.MemberNumber != null) && ChatRoomPlayerIsAdmin()) {
|
||
if (ActionType == "Move") {
|
||
ChatRoomCharacterViewMoveTarget = CurrentCharacter.MemberNumber;
|
||
} else {
|
||
ServerSend("ChatRoomAdmin", { MemberNumber: CurrentCharacter.MemberNumber, Action: ActionType, Publish: (Publish !== false && Publish !== "false") });
|
||
}
|
||
DialogLeave();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Sends an administrative command to the server from the chat text field.
|
||
* @param {"Ban"|"Unban"|"Kick"|"Promote"|"Demote"|"Whitelist"|"Unwhitelist"} ActionType - Type of action performed.
|
||
* @param {string} Argument - Target number of the action.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomAdminChatAction(ActionType, Argument) {
|
||
if (ChatRoomPlayerIsAdmin()) {
|
||
var C = parseInt(Argument);
|
||
if (!isNaN(C) && (C > 0) && (C != Player.MemberNumber))
|
||
ServerSend("ChatRoomAdmin", { MemberNumber: C, Action: ActionType });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Gets the player's current time as a string.
|
||
* @returns {string} - The player's current local time as a string.
|
||
*/
|
||
function ChatRoomCurrentTime() {
|
||
var D = new Date();
|
||
return ("0" + D.getHours()).substr(-2) + ":" + ("0" + D.getMinutes()).substr(-2);
|
||
}
|
||
|
||
/**
|
||
* Adds or removes an online member to/from a specific list. (From the dialog menu)
|
||
* @param {"Add" | "Remove"} Operation - Operation to perform.
|
||
* @param {"WhiteList" | "FriendList" | "BlackList" | "GhostList" | "DrawFocusList"} ListType - Name of the list to alter. (Whitelist, friendlist, blacklist, ghostlist, drawfocuslist)
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomListManage(Operation, ListType) {
|
||
if (CurrentCharacter) {
|
||
const isAdd = Operation === "Add";
|
||
if (ListType === "DrawFocusList"){
|
||
if (isAdd) ChatRoomDrawFocusListAdd([CurrentCharacter]);
|
||
else ChatRoomDrawFocusListRemove([CurrentCharacter]);
|
||
} else if (CurrentCharacter.MemberNumber && Array.isArray(Player[ListType])) {
|
||
ChatRoomListUpdate(Player[ListType], isAdd, CurrentCharacter.MemberNumber);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Adds or removes an online member to/from a specific list from a typed message.
|
||
* @param {number[]|null} List - List to add to or remove from.
|
||
* @param {boolean} Adding - If TRUE adding to the list, if FALSE removing from the list.
|
||
* @param {string} Argument - Member number to add/remove.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomListManipulation(List, Adding, Argument) {
|
||
var C = parseInt(Argument);
|
||
if (!isNaN(C) && (C > 0) && (C != Player.MemberNumber)) {
|
||
ChatRoomListUpdate(List, Adding, C);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Updates character lists for the player and saves the change
|
||
* @param {number[]} list - The array of member numbers to update
|
||
* @param {boolean} adding - If TRUE adding to the list, if FALSE removing from the list
|
||
* @param {number} memberNumber - The member number to add/remove
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomListUpdate(list, adding, memberNumber) {
|
||
if (adding && list.indexOf(memberNumber) < 0) {
|
||
list.push(memberNumber);
|
||
}
|
||
else if (!adding && list.indexOf(memberNumber) >= 0) {
|
||
list.splice(list.indexOf(memberNumber), 1);
|
||
}
|
||
|
||
const triggeredOperations = ChatRoomListOperationTriggers().find(w => w.list == list && w.adding == adding);
|
||
if (triggeredOperations) {
|
||
triggeredOperations.triggers.forEach(op => {
|
||
ChatRoomListUpdate(op.list, op.add, memberNumber);
|
||
});
|
||
}
|
||
|
||
if (list == Player.GhostList) {
|
||
const C = Character.find(Char => Char.MemberNumber == memberNumber);
|
||
if (C) {
|
||
CharacterRefresh(C, false);
|
||
}
|
||
}
|
||
|
||
ServerPlayerRelationsSync();
|
||
setTimeout(() => ChatRoomCharacterUpdate(Player), 5000);
|
||
}
|
||
|
||
/**
|
||
* Adds a list of character(s) into the Focus List
|
||
* @param {Character[]} characters - Characters to add
|
||
* @param {boolean} [enableMessage=true] - If enabled, will tell (warn) the user when the focus feature becomes enabled (empty -> non-empty)
|
||
* @returns {Character[]} - Characters that were actually added (not already in the list)
|
||
*/
|
||
function ChatRoomDrawFocusListAdd(characters, enableMessage=true) {
|
||
const added = [];
|
||
const wasInFocus = ChatRoomPlayerIsInDrawFocus();
|
||
characters.forEach(c => {
|
||
if (!ChatRoomDrawFocusList.includes(c)) {
|
||
ChatRoomDrawFocusList.push(c);
|
||
added.push(c);
|
||
}
|
||
});
|
||
if (enableMessage && !wasInFocus && ChatRoomPlayerIsInDrawFocus()) {
|
||
const enableMessageText = TextGet("FocusEnabled").replaceAll('FocusEnabledWarningIcon', TextGet("FocusEnabledWarningIcon"));
|
||
ChatRoomSendLocal(`<span style="color: deepskyblue">${enableMessageText}</span>`, 60_000);
|
||
}
|
||
return added;
|
||
}
|
||
|
||
/**
|
||
* Removes a list of character(s) from the Focus List
|
||
* @param {Character[]} characters - Characters to remove
|
||
* @returns {Character[]} - Characters that were actually removed (were in the list)
|
||
*/
|
||
function ChatRoomDrawFocusListRemove(characters) {
|
||
const removed = [];
|
||
characters.forEach(c => {
|
||
if (ChatRoomDrawFocusList.includes(c)) {
|
||
ChatRoomDrawFocusList.splice(ChatRoomDrawFocusList.indexOf(c), 1);
|
||
removed.push(c);
|
||
}
|
||
});
|
||
return removed;
|
||
}
|
||
|
||
/**
|
||
* Clears the Focus List
|
||
*/
|
||
function ChatRoomDrawFocusListClear() {
|
||
ChatRoomDrawFocusList = [];
|
||
}
|
||
|
||
/**
|
||
* Handles reception of data pertaining to if applying an item is allowed.
|
||
* @param {ServerChatRoomAllowItemResponse} data - Data object containing if the player is allowed to interact with a character.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomAllowItem(data) {
|
||
if ((data != null) && (typeof data === "object") && (data.MemberNumber != null) && (typeof data.MemberNumber === "number") && (data.AllowItem != null) && (typeof data.AllowItem === "boolean"))
|
||
if (CurrentCharacter != null && CurrentCharacter.MemberNumber == data.MemberNumber && data.AllowItem !== CurrentCharacter.AllowItem) {
|
||
console.warn(`ChatRoomGetAllowItem mismatch trying to access ${CurrentCharacter.Name} (${CurrentCharacter.MemberNumber})`);
|
||
CurrentCharacter.AllowItem = data.AllowItem;
|
||
CharacterSetCurrent(CurrentCharacter);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Opens the appearance editor for the given character
|
||
* @param {Character} C
|
||
*/
|
||
function ChatRoomAppearanceLoadCharacter(C) {
|
||
const screen = CommonGetScreen();
|
||
const inChatRoom = ServerPlayerIsInChatRoom();
|
||
if (inChatRoom) {
|
||
ChatRoomStatusUpdate("Wardrobe");
|
||
}
|
||
CharacterAppearanceLoadCharacter(C, (ready) => {
|
||
CommonSetScreen(...screen);
|
||
if (inChatRoom) {
|
||
ChatRoomShowElements();
|
||
ChatRoomStatusUpdate(null);
|
||
if (ready && !C.IsPlayer()) {
|
||
// Send a notification if we've changed clothes on someone else
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.destinationCharacter(C)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "ChangeClothes", Type: "Action", Dictionary: Dictionary });
|
||
ChatRoomCharacterUpdate(C);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Triggered when the player wants to change another player's outfit.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function DialogChangeClothes() {
|
||
const C = CurrentCharacter;
|
||
DialogLeave();
|
||
ChatRoomAppearanceLoadCharacter(C);
|
||
}
|
||
|
||
/**
|
||
* Triggered when the player selects an ownership dialog option. (It can change money and reputation)
|
||
* @param {"Propose" | "Accept" | "Release" | "Break"} RequestType - Type of request being performed.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSendOwnershipRequest(RequestType) {
|
||
if ((ChatRoomOwnershipOption == "CanOfferEndTrial") && (RequestType == "Propose")) {
|
||
CharacterChangeMoney(Player, -100);
|
||
DialogChangeReputation("Dominant", 10);
|
||
}
|
||
if ((ChatRoomOwnershipOption == "CanEndTrial") && (RequestType == "Accept")) DialogChangeReputation("Dominant", -20);
|
||
ChatRoomOwnershipOption = "";
|
||
ServerSend("AccountOwnership", { MemberNumber: CurrentCharacter.MemberNumber, Action: RequestType });
|
||
if (RequestType == "Accept") DialogLeave();
|
||
}
|
||
|
||
/**
|
||
* Triggered when the player selects an lovership dialog option. (It can change money and reputation)
|
||
* @param {"Propose" | "Accept" | "Release" | "Break"} RequestType - Type of request being performed.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSendLovershipRequest(RequestType) {
|
||
if ((ChatRoomLovershipOption == "CanOfferBeginWedding") && (RequestType == "Propose")) CharacterChangeMoney(Player, -100);
|
||
if ((ChatRoomLovershipOption == "CanBeginWedding") && (RequestType == "Accept")) CharacterChangeMoney(Player, -100);
|
||
ChatRoomLovershipOption = "";
|
||
ServerSend("AccountLovership", { MemberNumber: CurrentCharacter.MemberNumber, Action: RequestType });
|
||
if (RequestType == "Accept" || RequestType === "Break") DialogLeave();
|
||
}
|
||
|
||
/**
|
||
* Triggered when the player picks a drink from a character's maid tray.
|
||
* @param {string} DrinkType - Drink chosen.
|
||
* @param {number} Money - Cost of the drink.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomDrinkPick(DrinkType, Money) {
|
||
if (ChatRoomCanTakeDrink()) {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.targetCharacter(CurrentCharacter)
|
||
.destinationCharacter(CurrentCharacter)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "MaidDrinkPick" + DrinkType, Type: "Action", Dictionary });
|
||
ServerSend("ChatRoomChat", { Content: "MaidDrinkPick" + Money.toString(), Type: "Hidden", Target: CurrentCharacter.MemberNumber });
|
||
CharacterChangeMoney(Player, Money * -1);
|
||
DialogLeave();
|
||
}
|
||
}
|
||
|
||
/** @type {(RuleType: LogNameType[keyof LogNameType], Option: "Quest" | "Leave") => void} */
|
||
function ChatRoomSendLoverRule(RuleType, Option) { ChatRoomSendRule(RuleType, Option, "Lover"); }
|
||
/** @type {(RuleType: LogNameType[keyof LogNameType], Option: "Quest" | "Leave") => void} */
|
||
function ChatRoomSendOwnerRule(RuleType, Option) { ChatRoomSendRule(RuleType, Option, "Owner"); }
|
||
/** @type {(RuleType: LogNameAdvanced) => void} */
|
||
function ChatRoomAdvancedRule(RuleType) { AdvancedRuleOpen(RuleType); }
|
||
function ChatRoomForbiddenWords() { ForbiddenWordsOpen(); }
|
||
|
||
/**
|
||
* Sends a rule / restriction / punishment to the player's slave/lover client, it will be handled on the slave/lover's
|
||
* side when received.
|
||
* @param {LogNameType[keyof LogNameType]} RuleType - The rule selected.
|
||
* @param {"Quest" | "Leave"} Option - If the rule is a quest or we should just leave the dialog.
|
||
* @param {"Owner" | "Lover"} Sender - Type of the sender
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSendRule(RuleType, Option, Sender) {
|
||
ServerSend("ChatRoomChat", { Content: Sender + "Rule" + RuleType, Type: "Hidden", Target: CurrentCharacter.MemberNumber });
|
||
if (Option == "Quest") {
|
||
if (ChatRoomQuestGiven.indexOf(CurrentCharacter.MemberNumber) >= 0) ChatRoomQuestGiven.splice(ChatRoomQuestGiven.indexOf(CurrentCharacter.MemberNumber), 1);
|
||
ChatRoomQuestGiven.push(CurrentCharacter.MemberNumber);
|
||
}
|
||
if ((Option == "Leave") || (Option == "Quest")) DialogLeave();
|
||
}
|
||
|
||
/**
|
||
* @param {LogNameType["LoverRule"]} RuleType
|
||
* @returns {boolean}
|
||
*/
|
||
function ChatRoomGetLoverRule(RuleType) { return ChatRoomGetRule(RuleType, "Lover"); }
|
||
|
||
/**
|
||
* @param {LogNameType["OwnerRule"]} RuleType
|
||
* @returns {boolean}
|
||
*/
|
||
function ChatRoomGetOwnerRule(RuleType) { return ChatRoomGetRule(RuleType, "Owner"); }
|
||
|
||
/**
|
||
* Return TRUE if the current character allows to change her own nickname by her owner
|
||
* @returns {boolean}
|
||
*/
|
||
function ChatRoomCanChangeNickname() { return (CurrentCharacter != null) && (CurrentCharacter.OnlineSharedSettings != null) && (CurrentCharacter.OnlineSharedSettings.AllowRename !== false) && CurrentCharacter.IsOwnedByPlayer(); }
|
||
|
||
/**
|
||
* Enters the submissive character nickname edit/lock screen
|
||
* @returns {void}
|
||
*/
|
||
function ChatRoomChangeNickname() {
|
||
NicknameManagementTarget = CurrentCharacter;
|
||
DialogLeave();
|
||
CommonSetScreen("Online", "NicknameManagement");
|
||
}
|
||
|
||
/**
|
||
* Gets a rule from the current character
|
||
* @param {LogNameType["OwnerRule" | "LoverRule"]} RuleType - The name of the rule to retrieve.
|
||
* @param {"Owner" | "Lover"} Sender - Type of the sender
|
||
* @returns {boolean} - The owner or lover rule corresponding to the requested rule name
|
||
*/
|
||
function ChatRoomGetRule(RuleType, Sender) {
|
||
return LogQueryRemote(CurrentCharacter, RuleType, `${Sender}Rule`);
|
||
}
|
||
|
||
|
||
/**
|
||
* Processes a rule sent to the player from her owner or from her lover.
|
||
* @param {ServerChatRoomMessage} data - Received rule data object.
|
||
* @returns {void}
|
||
*/
|
||
function ChatRoomSetRule(data) {
|
||
|
||
// Only works if the sender is the player, and the player is fully collared
|
||
if (data != null && Player.IsFullyOwned() && Player.IsOwnedByMemberNumber(data.Sender)) {
|
||
|
||
// Wardrobe/changing rules
|
||
if (data.Content == "OwnerRuleChangeAllow") LogDelete("BlockChange", "OwnerRule");
|
||
if (data.Content == "OwnerRuleChangeBlock1Hour") LogAdd("BlockChange", "OwnerRule", CurrentTime + 3600000);
|
||
if (data.Content == "OwnerRuleChangeBlock1Day") LogAdd("BlockChange", "OwnerRule", CurrentTime + 86400000);
|
||
if (data.Content == "OwnerRuleChangeBlock1Week") LogAdd("BlockChange", "OwnerRule", CurrentTime + 604800000);
|
||
if (data.Content == "OwnerRuleChangeBlock") LogAdd("BlockChange", "OwnerRule", CurrentTime + 1000000000000);
|
||
|
||
// Owner presence rules
|
||
if (data.Content == "OwnerRuleTalkAllow") LogDelete("BlockTalk", "OwnerRule");
|
||
if (data.Content == "OwnerRuleTalkBlock") LogAdd("BlockTalk", "OwnerRule");
|
||
if (data.Content == "OwnerRuleEmoteAllow") LogDelete("BlockEmote", "OwnerRule");
|
||
if (data.Content == "OwnerRuleEmoteBlock") LogAdd("BlockEmote", "OwnerRule");
|
||
if (data.Content == "OwnerRuleWhisperAllow") LogDelete("BlockWhisper", "OwnerRule");
|
||
if (data.Content == "OwnerRuleWhisperBlock") { LogAdd("BlockWhisper", "OwnerRule"); ChatRoomSetTarget(-1); }
|
||
if (data.Content == "OwnerRuleChangePoseAllow") LogDelete("BlockChangePose", "OwnerRule");
|
||
if (data.Content == "OwnerRuleChangePoseBlock") LogAdd("BlockChangePose", "OwnerRule");
|
||
if (data.Content == "OwnerRuleAccessSelfAllow") LogDelete("BlockAccessSelf", "OwnerRule");
|
||
if (data.Content == "OwnerRuleAccessSelfBlock") LogAdd("BlockAccessSelf", "OwnerRule");
|
||
if (data.Content == "OwnerRuleAccessOtherAllow") LogDelete("BlockAccessOther", "OwnerRule");
|
||
if (data.Content == "OwnerRuleAccessOtherBlock") LogAdd("BlockAccessOther", "OwnerRule");
|
||
|
||
// Key rules
|
||
if (data.Content == "OwnerRuleKeyAllow") LogDelete("BlockKey", "OwnerRule");
|
||
if (data.Content == "OwnerRuleKeyConfiscate") { InventoryConfiscateKey(); DialogLentLockpicks = false; }
|
||
if (data.Content == "OwnerRuleKeyBlock") LogAdd("BlockKey", "OwnerRule");
|
||
if (data.Content == "OwnerRuleKeyAllowFamily") LogDelete("BlockFamilyKey", "OwnerRule");
|
||
if (data.Content == "OwnerRuleKeyBlockFamily") LogAdd("BlockFamilyKey", "OwnerRule");
|
||
if (data.Content == "OwnerRuleSelfOwnerLockAllow") LogDelete("BlockOwnerLockSelf", "OwnerRule");
|
||
if (data.Content == "OwnerRuleSelfOwnerLockBlock") LogAdd("BlockOwnerLockSelf", "OwnerRule");
|
||
|
||
// Remote rules
|
||
if (data.Content == "OwnerRuleRemoteAllow") LogDelete("BlockRemote", "OwnerRule");
|
||
if (data.Content == "OwnerRuleRemoteAllowSelf") LogDelete("BlockRemoteSelf", "OwnerRule");
|
||
if (data.Content == "OwnerRuleRemoteConfiscate") InventoryConfiscateRemote();
|
||
if (data.Content == "OwnerRuleRemoteBlock") LogAdd("BlockRemote", "OwnerRule");
|
||
if (data.Content == "OwnerRuleRemoteBlockSelf") LogAdd("BlockRemoteSelf", "OwnerRule");
|
||
|
||
// Sent to timer cell
|
||
let TimerCell = 0;
|
||
if (data.Content == "OwnerRuleTimerCell5") TimerCell = 5;
|
||
if (data.Content == "OwnerRuleTimerCell15") TimerCell = 15;
|
||
if (data.Content == "OwnerRuleTimerCell30") TimerCell = 30;
|
||
if (data.Content == "OwnerRuleTimerCell60") TimerCell = 60;
|
||
if (TimerCell > 0) {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.targetCharacterName(Player)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "ActionGrabbedForCell", Type: "Action", Dictionary });
|
||
ChatRoomLeave(true);
|
||
CellLock(TimerCell);
|
||
}
|
||
|
||
// Sent to GGTS
|
||
let GGTS = 0;
|
||
if (data.Content == "OwnerRuleGGTS5") GGTS = 5;
|
||
if (data.Content == "OwnerRuleGGTS15") GGTS = 15;
|
||
if (data.Content == "OwnerRuleGGTS30") GGTS = 30;
|
||
if (data.Content == "OwnerRuleGGTS60") GGTS = 60;
|
||
if (data.Content == "OwnerRuleGGTS90") GGTS = 90;
|
||
if (data.Content == "OwnerRuleGGTS120") GGTS = 120;
|
||
if (data.Content == "OwnerRuleGGTS180") GGTS = 180;
|
||
if (GGTS > 0) {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.targetCharacterName(Player)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "ActionGrabbedForGGTS", Type: "Action", Dictionary });
|
||
ChatRoomLeave(true);
|
||
AsylumGGTSLock(GGTS, TextGet("GGTSIntro"));
|
||
}
|
||
|
||
// Nickname changing and locking
|
||
if (data.Content == "OwnerRuleNicknameAllow") LogDelete("BlockNickname", "OwnerRule");
|
||
if (data.Content == "OwnerRuleNicknameBlock") LogAdd("BlockNickname", "OwnerRule");
|
||
if (data.Content.startsWith("OwnerRuleNicknameNew")) {
|
||
let NewNick = data.Content.substring(20);
|
||
CharacterSetNickname(Player, NewNick);
|
||
data.Content = "OwnerRuleNicknameNew";
|
||
}
|
||
|
||
// Collar rules
|
||
if (data.Content == "OwnerRuleCollarRelease") {
|
||
if ((InventoryGet(Player, "ItemNeck") != null) && (InventoryGet(Player, "ItemNeck").Asset.Name == "SlaveCollar")) {
|
||
InventoryRemove(Player, "ItemNeck");
|
||
ChatRoomCharacterItemUpdate(Player, "ItemNeck");
|
||
const Dictionary = new DictionaryBuilder()
|
||
.destinationCharacterName(Player)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "PlayerOwnerCollarRelease", Type: "Action", Dictionary });
|
||
}
|
||
LogAdd("ReleasedCollar", "OwnerRule");
|
||
}
|
||
if (data.Content == "OwnerRuleCollarWear") {
|
||
if ((InventoryGet(Player, "ItemNeck") == null) || ((InventoryGet(Player, "ItemNeck") != null) && (InventoryGet(Player, "ItemNeck").Asset.Name != "SlaveCollar"))) {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.targetCharacterName(CurrentCharacter)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "PlayerOwnerCollarWear", Type: "Action", Dictionary });
|
||
}
|
||
LogDelete("ReleasedCollar", "OwnerRule");
|
||
LoginValidCollar();
|
||
}
|
||
|
||
// Advanced rules - Block screens
|
||
if (data.Content.startsWith("OwnerRuleBlockScreen")) {
|
||
LogDeleteStarting("BlockScreen", "OwnerRule");
|
||
LogAdd(`BlockScreen${data.Content.substring("OwnerRuleBlockScreen".length, 100)}`, "OwnerRule");
|
||
data.Content = "OwnerRuleBlockScreen";
|
||
}
|
||
|
||
// Advanced rules - Block appearance zones
|
||
if (data.Content.startsWith("OwnerRuleBlockAppearance")) {
|
||
LogDeleteStarting("BlockAppearance", "OwnerRule");
|
||
LogAdd(`BlockAppearance${data.Content.substring("OwnerRuleBlockAppearance".length, 100)}`, "OwnerRule");
|
||
data.Content = "OwnerRuleBlockAppearance";
|
||
}
|
||
|
||
// Advanced rules - Block item groups
|
||
if (data.Content.startsWith("OwnerRuleBlockItemGroup")) {
|
||
LogDeleteStarting("BlockItemGroup", "OwnerRule");
|
||
LogAdd(`BlockItemGroup${data.Content.substring("OwnerRuleBlockItemGroup".length, 100)}`, "OwnerRule");
|
||
data.Content = "OwnerRuleBlockItemGroup";
|
||
}
|
||
|
||
// Advanced rules - Forbidden Words List
|
||
if (data.Content.startsWith("OwnerRuleForbiddenWords")) {
|
||
LogDeleteStarting("ForbiddenWords", "OwnerRule");
|
||
LogAdd(`ForbiddenWords${data.Content.substring("OwnerRuleForbiddenWords".length, 10000)}`, "OwnerRule");
|
||
data.Content = "OwnerRuleForbiddenWords";
|
||
}
|
||
|
||
// Forced labor
|
||
if (data.Content == "OwnerRuleLaborMaidDrinks" && Player.CanTalk()) {
|
||
PoseSetActive(Player, null);
|
||
var D = TextGet("ActionGrabbedToServeDrinksIntro");
|
||
const Dictionary = new DictionaryBuilder()
|
||
.targetCharacterName(CurrentCharacter)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "ActionGrabbedToServeDrinks", Type: "Action", Dictionary });
|
||
ChatRoomLeave();
|
||
CommonSetScreen("Room", "MaidQuarters");
|
||
CharacterSetCurrent(MaidQuartersMaid);
|
||
MaidQuartersMaid.CurrentDialog = D;
|
||
MaidQuartersMaid.Stage = "205";
|
||
MaidQuartersOnlineDrinkFromOwner = true;
|
||
}
|
||
|
||
// Forced Wheel of Fortune
|
||
if (data.Content == "OwnerRuleForceWheelFortune") {
|
||
for (let C of ChatRoomCharacter)
|
||
if (C.IsOwner())
|
||
CharacterSetCurrent(C);
|
||
if ((CurrentCharacter == null) || !CurrentCharacter.IsOwner() || !InventoryIsWorn(CurrentCharacter, "WheelFortune", "ItemDevices")) return;
|
||
WheelFortuneReturnScreen = CommonGetScreen();
|
||
WheelFortuneBackground = ChatRoomData.Background;
|
||
WheelFortuneCharacter = CurrentCharacter;
|
||
WheelFortuneForced = true;
|
||
DialogLeave();
|
||
CommonSetScreen("MiniGame", "WheelFortune");
|
||
}
|
||
|
||
ChatRoomGetLoadRules(data.Sender);
|
||
|
||
// Switches it to a server message to announce the new rule to the player
|
||
data.Type = "ServerMessage";
|
||
ChatRoomMessage(data);
|
||
}
|
||
|
||
// Only works if the sender is the lover of the player
|
||
if ((data != null) && Player.GetLoversNumbers().includes(data.Sender)) {
|
||
if (data.Content == "LoverRuleSelfLoverLockAllow") LogDelete("BlockLoverLockSelf", "LoverRule");
|
||
if (data.Content == "LoverRuleSelfLoverLockBlock") LogAdd("BlockLoverLockSelf", "LoverRule");
|
||
if (data.Content == "LoverRuleOwnerLoverLockAllow") LogDelete("BlockLoverLockOwner", "LoverRule");
|
||
if (data.Content == "LoverRuleOwnerLoverLockBlock") LogAdd("BlockLoverLockOwner", "LoverRule");
|
||
|
||
ChatRoomGetLoadRules(data.Sender);
|
||
|
||
// Switches it to a server message to announce the new rule to the player
|
||
data.Type = "ServerMessage";
|
||
ChatRoomMessage(data);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Sends quest money to the player's owner.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomGiveMoneyForOwner() {
|
||
if (ChatRoomCanGiveMoneyForOwner()) {
|
||
const Dictionary = new DictionaryBuilder()
|
||
.targetCharacterName(CurrentCharacter)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "ActionGiveEnvelopeToOwner", Type: "Action", Dictionary });
|
||
ServerSend("ChatRoomChat", { Content: "PayQuest" + ChatRoomMoneyForOwner.toString(), Type: "Hidden", Target: CurrentCharacter.MemberNumber });
|
||
ChatRoomMoneyForOwner = 0;
|
||
DialogLeave();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handles the reception of quest data, when payment is received.
|
||
* @param {number} questGiverNumber
|
||
* @param {number} paymentAmount
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomPayQuest(questGiverNumber, paymentAmount) {
|
||
if (ChatRoomQuestGiven.indexOf(questGiverNumber) < 0) return;
|
||
|
||
if (paymentAmount == null || isNaN(paymentAmount)) return;
|
||
|
||
if (paymentAmount < 0) paymentAmount = 0;
|
||
if (paymentAmount > 30) paymentAmount = 30;
|
||
CharacterChangeMoney(Player, paymentAmount);
|
||
ChatRoomQuestGiven.splice(ChatRoomQuestGiven.indexOf(questGiverNumber), 1);
|
||
}
|
||
|
||
/**
|
||
* Triggered when online game data comes in
|
||
* @param {ServerChatRoomGameBountyUpdateRequest["OnlineBounty"]} data - Game data to process, sent to the current game handler.
|
||
* @param {number} sender
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomOnlineBountyHandleData(data, sender) {
|
||
if (data.finishTime && data.target == Player.MemberNumber) {
|
||
let senderChar = ChatRoomCharacter.find(c => c.MemberNumber == sender);
|
||
const remaining = Math.max(1, Math.ceil((data.finishTime - CommonTime()) / 60000));
|
||
const content = ChatRoomCarryingBountyOpened(senderChar) ? "OnlineBountySuitcaseOngoingOpened" : "OnlineBountySuitcaseOngoing";
|
||
const dict = new DictionaryBuilder()
|
||
.sourceCharacter(senderChar)
|
||
.targetCharacter(Player)
|
||
.text("TIMEREMAINING", remaining.toString())
|
||
.build();
|
||
ChatRoomMessage({ Content: content, Type: "Action", Dictionary: dict, Sender: sender });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Triggered when a game message comes in, we forward it to the current online game being played.
|
||
* @param {ServerChatRoomGameResponse} data - Game data to process, sent to the current game handler.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomGameResponse(data) {
|
||
if (data.Data.KinkyDungeon) {
|
||
if (CurrentScreen == "KinkyDungeon") {
|
||
KinkyDungeonHandleData(data.Data.KinkyDungeon, data.Sender);
|
||
}
|
||
} else if (KidnapLeagueOnlineBountyTarget && data.Data.OnlineBounty)
|
||
ChatRoomOnlineBountyHandleData(data.Data.OnlineBounty, data.Sender);
|
||
else if (ChatRoomGame == "LARP") GameLARPProcess(data);
|
||
else if (ChatRoomGame == "MagicBattle") GameMagicBattleProcess(data);
|
||
else if (ChatRoomGame == "ClubCard") GameClubCardProcess(data);
|
||
}
|
||
|
||
/**
|
||
* Triggered when the player uses the /safeword command, we revert the character if safewords are enabled, and display
|
||
* a warning in chat if not.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSafewordChatCommand() {
|
||
if (DialogChatRoomCanSafeword())
|
||
ChatRoomSafewordRevert();
|
||
else if (CurrentScreen == "ChatRoom") {
|
||
/** @type {ServerChatRoomMessage} */
|
||
var msg = {Sender: Player.MemberNumber, Content: "SafewordDisabled", Type: "Action"};
|
||
ChatRoomMessage(msg);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Triggered when the player activates her safeword to revert, we swap her appearance to the state when she entered the
|
||
* chat room lobby, minimum permission becomes whitelist and up.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSafewordRevert() {
|
||
if (ChatSearchSafewordAppearance != null) {
|
||
Player.Appearance = ChatSearchSafewordAppearance.slice(0);
|
||
Player.ActivePoseMapping = ChatSearchSafewordPose;
|
||
CharacterRefresh(Player);
|
||
ChatRoomCharacterUpdate(Player);
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "ActionActivateSafewordRevert", Type: "Action", Dictionary });
|
||
if (Player.ItemPermission < 3) {
|
||
Player.ItemPermission = 3;
|
||
ServerAccountUpdate.QueueData({ ItemPermission: Player.ItemPermission }, true);
|
||
setTimeout(() => ChatRoomCharacterUpdate(Player), 5000);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Triggered when the player activates her safeword and wants to be released, we remove all bondage from her and return
|
||
* her to the chat search screen.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSafewordRelease() {
|
||
PandoraPenitentiarySafewordRooms.push(ChatRoomData.Name);
|
||
CharacterReleaseTotal(Player);
|
||
CharacterRefresh(Player);
|
||
const Dictionary = new DictionaryBuilder()
|
||
.sourceCharacter(Player)
|
||
.build();
|
||
ServerSend("ChatRoomChat", { Content: "ActionActivateSafewordRelease", Type: "Action", Dictionary });
|
||
ChatRoomLeave();
|
||
CommonSetScreen("Online", "ChatSearch");
|
||
}
|
||
|
||
/**
|
||
* Concatenates the list of users to ban.
|
||
* @param {("BlackList" | "GhostList")[]} [IncludesTypes] - The types of lists to concatenate to the banlist
|
||
* @param {readonly number[]} [ExistingList] - The existing Banlist, if applicable
|
||
* @returns {number[]} Complete array of members to ban
|
||
*/
|
||
function ChatRoomConcatenateBanList(IncludesTypes, ExistingList) {
|
||
var BanList = Array.isArray(ExistingList) ? ExistingList : [];
|
||
IncludesTypes.forEach(Type => {
|
||
if (Type == "BlackList") BanList = BanList.concat(Player.BlackList);
|
||
else if (Type == "GhostList") BanList = BanList.concat(Player.GhostList);
|
||
});
|
||
return BanList.filter((MemberNumber, Idx, Arr) => Arr.indexOf(MemberNumber) == Idx);
|
||
}
|
||
|
||
/**
|
||
* Concatenates the list of users for the whitelist.
|
||
* @param {("Owner" | "Lovers" | "Friends" | "Whitelist")[]} [IncludesTypes] - The types of lists to concatenate to the whitelist
|
||
* @param {readonly number[]} [ExistingList] - The existing Whitelist, if applicable
|
||
* @returns {number[]} Complete array of whitelisted members
|
||
*/
|
||
function ChatRoomConcatenateWhitelist(IncludesTypes, ExistingList) {
|
||
var Whitelist = Array.isArray(ExistingList) ? ExistingList : [];
|
||
IncludesTypes.forEach(Type => {
|
||
if (Type == "Owner" && Player.OwnerNumber() !== -1) Whitelist = Whitelist.concat(Player.OwnerNumber());
|
||
else if (Type == "Lovers") Whitelist = Whitelist.concat(Player.GetLoversNumbers(true));
|
||
else if (Type == "Friends") Whitelist = Whitelist.concat(Player.FriendList);
|
||
else if (Type == "Whitelist") Whitelist = Whitelist.concat(Player.WhiteList);
|
||
});
|
||
return Whitelist.filter((MemberNumber, Idx, Arr) => Arr.indexOf(MemberNumber) == Idx);
|
||
}
|
||
|
||
/**
|
||
* Concatenates the list of users for Admin list.
|
||
* @param {("Owner" | "Lovers")[]} [IncludesTypes] - The types of lists to concatenate to the adminlist
|
||
* @param {readonly number[]} [ExistingList] - The existing Admin list, if applicable
|
||
* @returns {number[]} Complete array of admin members
|
||
*/
|
||
function ChatRoomConcatenateAdminList(IncludesTypes, ExistingList) {
|
||
var AdminList = Array.isArray(ExistingList) ? ExistingList : [];
|
||
IncludesTypes.forEach(Type => {
|
||
if (Type == "Owner" && Player.OwnerNumber() !== -1) AdminList = AdminList.concat(Player.OwnerNumber());
|
||
else if (Type == "Lovers") AdminList = AdminList.concat(Player.GetLoversNumbers(true));
|
||
});
|
||
return AdminList.filter((MemberNumber, Idx, Arr) => Arr.indexOf(MemberNumber) == Idx);
|
||
}
|
||
|
||
/**
|
||
* Handles a request from another player to read the player's log entries that they are permitted to read. Lovers and
|
||
* owners can read certain entries from the player's log.
|
||
* @param {Character|number} C - A character object representing the requester, or the account number of the requester.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomGetLoadRules(C) {
|
||
if (typeof C === "number") {
|
||
C = ChatRoomCharacter.find(CC => CC.MemberNumber == C);
|
||
}
|
||
if (C == null) return;
|
||
if (Player.IsOwnedByCharacter(C)) {
|
||
ServerSend("ChatRoomChat", {
|
||
Content: "RuleInfoSet",
|
||
Type: "Hidden",
|
||
Target: C.MemberNumber,
|
||
// @ts-expect-error That message uses .Dictionary as storage for LogRecord[]
|
||
Dictionary: LogGetOwnerReadableRules(C.IsLoverOfPlayer()),
|
||
});
|
||
} else if (C.IsLoverOfPlayer()) {
|
||
ServerSend("ChatRoomChat", {
|
||
Content: "RuleInfoSet",
|
||
Type: "Hidden",
|
||
Target: C.MemberNumber,
|
||
// @ts-expect-error That message uses .Dictionary as storage for LogRecord[]
|
||
Dictionary: LogGetLoverReadableRules(),
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handles a response from another player containing the rules that the current player is allowed to read.
|
||
* @param {Character} C - Character to set the rules on
|
||
* @param {readonly LogRecord[]} Rule - An array of rules that the current player can read.
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomSetLoadRules(C, Rule) {
|
||
if (Array.isArray(Rule)) C.Rule = Rule;
|
||
}
|
||
|
||
/**
|
||
* Take a screenshot of all characters in the chatroom
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomPhotoFullRoom() {
|
||
ChatRoomActiveView.Screenshot();
|
||
}
|
||
|
||
/**
|
||
* Take a screenshot of the player and current character
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function DialogPhotoCurrentCharacters() {
|
||
ChatRoomPhoto(0, 0, 1000, 1000, [Player, CurrentCharacter]);
|
||
}
|
||
|
||
/**
|
||
* Take a screenshot of the player
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function DialogPhotoPlayer() {
|
||
ChatRoomPhoto(500, 0, 500, 1000, [Player]);
|
||
}
|
||
|
||
/**
|
||
* Take a screenshot in a chatroom, temporary removing emoticons
|
||
* @param {number} Left - Position of the area to capture from the left of the canvas
|
||
* @param {number} Top - Position of the area to capture from the top of the canvas
|
||
* @param {number} Width - Width of the area to capture
|
||
* @param {number} Height - Height of the area to capture
|
||
* @param {readonly Character[]} Characters - The characters that will be included in the screenshot
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomPhoto(Left, Top, Width, Height, Characters) {
|
||
// Temporarily remove AFK emoticons
|
||
let CharsToReset = [];
|
||
for (let CR = 0; CR < Characters.length; CR++) {
|
||
let C = Characters[CR];
|
||
let Emoticon = C.Appearance.find(A => A.Asset.Group.Name == "Emoticon");
|
||
if (Emoticon && Emoticon.Property && Emoticon.Property.Expression == "Afk") {
|
||
CharsToReset.push(C);
|
||
Emoticon.Property.Expression = null;
|
||
CharacterRefresh(C, false);
|
||
}
|
||
}
|
||
|
||
// Take the photo
|
||
CommonTakePhoto(Left, Top, Width, Height);
|
||
|
||
// Revert temporary changes
|
||
for (let CR = 0; CR < CharsToReset.length; CR++) {
|
||
let C = CharsToReset[CR];
|
||
C.Appearance.find(A => A.Asset.Group.Name == "Emoticon").Property.Expression = "Afk";
|
||
CharacterRefresh(C, false);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns whether the most recent chat message is on screen
|
||
* @returns {boolean} - TRUE if the screen has focus and the chat log is scrolled to the bottom
|
||
*/
|
||
function ChatRoomNotificationNewMessageVisible() {
|
||
return document.hasFocus() && ElementIsScrolledToEnd("TextAreaChatLog");
|
||
}
|
||
|
||
/**
|
||
* Raise a notification for the new chat message if required
|
||
* @param {Character} C - The character that sent the message
|
||
* @param {string} msg - The text of the message
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomNotificationRaiseChatMessage(C, msg) {
|
||
if (!C.IsPlayer()
|
||
&& Player.NotificationSettings.ChatMessage.AlertType !== NotificationAlertType.NONE
|
||
&& !ChatRoomNotificationNewMessageVisible())
|
||
{
|
||
NotificationRaise(NotificationEventType.CHATMESSAGE, { body: msg, character: C, useCharAsIcon: true });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Resets any previously raised Chat Message or Chatroom Join notifications if required
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomNotificationReset() {
|
||
if (CurrentScreen !== "ChatRoom" || ChatRoomNotificationNewMessageVisible()) {
|
||
NotificationReset(NotificationEventType.CHATMESSAGE);
|
||
}
|
||
if (document.hasFocus()) NotificationReset(NotificationEventType.CHATJOIN);
|
||
}
|
||
|
||
/**
|
||
* Returns whether a notification should be raised for the character entering a chatroom
|
||
* @param {Character} C - The character that entered the room
|
||
* @returns {boolean} - Whether a notification should be raised
|
||
*/
|
||
function ChatRoomNotificationRaiseChatJoin(C) {
|
||
let raise = false;
|
||
if (!document.hasFocus()) {
|
||
const settings = Player.NotificationSettings.ChatJoin;
|
||
if (settings.AlertType === NotificationAlertType.NONE) raise = false;
|
||
else if (PreferenceIsPlayerInSensDep()) raise = false;
|
||
else if (!settings.Owner && !settings.Lovers && !settings.Friendlist && !settings.Subs) raise = true;
|
||
else if (settings.Owner && Player.IsOwnedByMemberNumber(C.MemberNumber)) raise = true;
|
||
else if (settings.Lovers && C.IsLoverOfPlayer()) raise = true;
|
||
else if (settings.Friendlist && Player.FriendList.includes(C.MemberNumber)) raise = true;
|
||
else if (settings.Subs && C.IsOwnedByPlayer()) raise = true;
|
||
}
|
||
return raise;
|
||
}
|
||
|
||
/**
|
||
* Updates the chatroom with the player's stored chatroom data if needed (happens when entering a recreated chatroom for
|
||
* the first time)
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomRecreate() {
|
||
if (CurrentTime > ChatRoomNewRoomToUpdateTimer && ChatRoomNewRoomToUpdate && Player.ImmersionSettings && Player.ImmersionSettings.ReturnToChatRoomAdmin &&
|
||
Player.ImmersionSettings.ReturnToChatRoom && Player.LastChatRoom.Admin) {
|
||
// Add the player if they are not an admin
|
||
if (!Player.LastChatRoom.Admin.includes(Player.MemberNumber) && ChatRoomDataIsPrivate(Player.LastChatRoom)) {
|
||
Player.LastChatRoom.Admin.push(Player.MemberNumber);
|
||
}
|
||
|
||
ServerSend("ChatRoomAdmin", { MemberNumber: Player.ID, Room: Player.LastChatRoom, Action: "Update" });
|
||
ChatRoomNewRoomToUpdate = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Checks whether or not the player's last chatroom data needs updating
|
||
* @returns {void} - Nothing
|
||
*/
|
||
function ChatRoomCheckForLastChatRoomUpdates() {
|
||
const Blacklist = Player.BlackList || [];
|
||
// Check whether the chatroom contains at least one "safe" character (a friend, owner, or non-blacklisted player)
|
||
const ContainsSafeCharacters = ChatRoomCharacter.length === 1 || ChatRoomCharacter.some((Char) => {
|
||
return !Char.IsPlayer() && (
|
||
Player.FriendList.includes(Char.MemberNumber) ||
|
||
Player.IsOwnedByMemberNumber(Char.MemberNumber) ||
|
||
!Blacklist.includes(Char.MemberNumber)
|
||
);
|
||
});
|
||
|
||
if (!ChatRoomData || !ContainsSafeCharacters) {
|
||
// If the room only contains blacklisted characters, do not save the room data
|
||
ChatRoomSetLastChatRoom(null);
|
||
} else if (Player.ImmersionSettings && ChatRoomDataChanged()) {
|
||
// Otherwise save the chatroom data if it has changed
|
||
ChatRoomSetLastChatRoom(ChatRoomData);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Determines whether or not the current chatroom data differs from the locally stored chatroom data
|
||
* @returns {boolean} - TRUE if the stored chatroom data is different from the current chatroom data, FALSE otherwise
|
||
*/
|
||
function ChatRoomDataChanged() {
|
||
const prev = Player.LastChatRoom;
|
||
const curr = ChatRoomData;
|
||
|
||
if (typeof curr.Custom != typeof prev) return true;
|
||
if ((typeof curr.Custom === "object") && (typeof prev === "object")) {
|
||
if ((curr.Custom == null) != (prev == null)) return true;
|
||
if (JSON.stringify(curr.Custom) !== JSON.stringify(prev)) return true;
|
||
}
|
||
|
||
return prev !== curr || prev.Name !== curr.Name ||
|
||
prev.Background !== curr.Background ||
|
||
prev.Limit !== curr.Limit ||
|
||
prev.Language !== curr.Language ||
|
||
prev.Visibility !== curr.Visibility ||
|
||
prev.Access !== curr.Access ||
|
||
prev.Description !== curr.Description ||
|
||
!CommonArraysEqual(prev.Admin, curr.Admin) ||
|
||
!CommonArraysEqual(prev.Whitelist, curr.Whitelist) ||
|
||
!CommonArraysEqual(prev.Ban, curr.Ban) ||
|
||
!CommonArraysEqual(prev.BlockCategory, curr.BlockCategory) ||
|
||
prev.Space !== curr.Space ||
|
||
JSON.stringify(prev.MapData) !== JSON.stringify(curr.MapData);
|
||
}
|
||
|
||
function ChatRoomRefreshFontSize() {
|
||
ChatRoomFontSize = ChatRoomFontSizes[Player.ChatSettings.FontSize || "Medium"];
|
||
}
|
||
|
||
/**
|
||
* Checks if the message can be sent as chat or the player should be warned
|
||
* @deprecated No replacement; OOC should always be allowed through
|
||
* @param {string} Message - User input
|
||
* @param {Character} WhisperTarget
|
||
* @returns {boolean}
|
||
*/
|
||
function ChatRoomShouldBlockGaggedOOCMessage(Message, WhisperTarget) {
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Validates that the words said in the local chat are not breaking any forbidden words rule
|
||
* @param {string} Message - The message typed by the player
|
||
* @returns {boolean} - Returns FALSE if we must block the message from being sent
|
||
*/
|
||
function ChatRoomOwnerForbiddenWordCheck(Message) {
|
||
|
||
// Exits right away if not owned
|
||
if (CurrentScreen != "ChatRoom") return true;
|
||
if (!Player.IsOwned()) return true;
|
||
if (LogQuery("BlockTalkForbiddenWords", "OwnerRule")) return false;
|
||
|
||
// Gets the forbidden words list from the log
|
||
let ForbiddenList = [];
|
||
for (let L of Log)
|
||
if ((L.Group == "OwnerRule") && L.Name.startsWith("ForbiddenWords"))
|
||
ForbiddenList = L.Name.substring("ForbiddenWords".length, 10000).split("|");
|
||
if (ForbiddenList.length <= 1) return true;
|
||
|
||
// Gets the consequence for saying the forbidden word
|
||
let Consequence = ForbiddenList[0].trim();
|
||
if (ForbiddenWordsConsequenceList.indexOf(Consequence) < 0) Consequence = "";
|
||
ForbiddenList.splice(0, 1);
|
||
if (Consequence == "") return true;
|
||
|
||
// Prepares an array of all words said
|
||
let M = Message.trim().toUpperCase();
|
||
M = M.replace(/-/g, "");
|
||
M = M.replace(/ /g, "|");
|
||
M = M.replace(/,/g, "|");
|
||
M = M.replace(/\./g, "|");
|
||
let WordList = M.split("|");
|
||
if (WordList.length <= 0) return true;
|
||
|
||
// For each word said, we check if that word is forbidden
|
||
let FoundWord = "";
|
||
for (let W of WordList)
|
||
if ((W != "") && (ForbiddenList.indexOf(W) >= 0)) {
|
||
FoundWord = W;
|
||
break;
|
||
}
|
||
if (FoundWord == "") return true;
|
||
|
||
// If we must block the message
|
||
if (Consequence == "Block") {
|
||
ChatRoomMessage({Type: "ServerMessage", Content: "ForbiddenWordsBlocked", Sender: Player.MemberNumber});
|
||
return false;
|
||
}
|
||
|
||
// If we must mute the player after she said the words
|
||
if (Consequence.startsWith("Mute")) {
|
||
let Minutes = parseInt(Consequence.substring(4, 100));
|
||
if (isNaN(Minutes)) Minutes = 5;
|
||
if ((Minutes != 5) && (Minutes != 15) && (Minutes != 30)) Minutes = 5;
|
||
ChatRoomMessage({Type: "ServerMessage", Content: "ForbiddenWordsMute" + Minutes.toString(), Sender: Player.MemberNumber});
|
||
LogAdd("BlockTalkForbiddenWords", "OwnerRule", CurrentTime + Minutes * 60 * 1000);
|
||
return true;
|
||
}
|
||
|
||
// If no valid consquence, we continue
|
||
return true;
|
||
|
||
}
|
||
|
||
/**
|
||
* Returns TRUE if the owner presence rule is enforced for the current player
|
||
* @param {LogNameType["OwnerRule"]} RuleName - The name of the rule to validate (BlockWhisper, BlockTalk, etc.)
|
||
* @param {Character} Target - The target character
|
||
* @returns {boolean} - TRUE if the rule is enforced
|
||
*/
|
||
function ChatRoomOwnerPresenceRule(RuleName, Target) {
|
||
|
||
if (!LogQuery(RuleName, "OwnerRule")) return false; // FALSE if the rule isn't set
|
||
if (!Player.IsFullyOwned()) return false; // FALSE if the player isn't fully collared
|
||
if (!ChatRoomOwnerInside()) return false; // FALSE if the owner isn't inside
|
||
|
||
// Some rules are only on with specific targets
|
||
let Rule = true;
|
||
if (RuleName == "BlockAccessSelf") Rule = ((Target != null) && (Player.MemberNumber === Target.MemberNumber)); // Block access self only if target is herself
|
||
if (RuleName == "BlockAccessOther") Rule = ((Target != null) && (Player.MemberNumber !== Target.MemberNumber)); // Block access other only if target is another member
|
||
if (RuleName == "BlockWhisper") Rule = ((Target != null) && !Player.IsOwnedByCharacter(Target)); // Block whisper doesn't block whispering to the owner
|
||
|
||
// Shows a warning message in the chat log if the rule is enforced
|
||
if (Rule) {
|
||
const div = document.createElement("div");
|
||
div.setAttribute('class', 'ChatMessage ChatMessageServerMessage');
|
||
div.setAttribute('data-time', ChatRoomCurrentTime());
|
||
div.setAttribute('data-sender', Player.MemberNumber.toString());
|
||
div.innerHTML = "<b><i>" + TextGet("OwnerPresence" + RuleName) + "</i></b>";
|
||
ChatRoomAppendChat(div);
|
||
}
|
||
|
||
// If all validations passed, we enforce the rule
|
||
return Rule;
|
||
|
||
}
|
||
|
||
/**
|
||
* Replaces pronoun-related tags with the relevant text for the character
|
||
* @param {Character} C - The character that the message key relates to
|
||
* @param {string} key - Key for the dialog entry to use
|
||
* @param {boolean} hideIdentity - Whether to hide details revealing the character's identity
|
||
* @returns {CommonSubtituteSubstitution[]} - The replacement pronoun text for keywords in the original message
|
||
*/
|
||
function ChatRoomPronounSubstitutions(C, key, hideIdentity) {
|
||
/** @type {(match: string, offset: number, repl: string, string: string) => string} */
|
||
function replacer(match, offset, repl, string) {
|
||
// We matched at the start of the string, easy
|
||
if (offset === 0 || offset === 1 && string[0] === "(") return CommonStringTitlecase(repl);
|
||
|
||
// Harder, walk backward from the match, checking for a sentence separator
|
||
let pos;
|
||
for (pos = offset - 1; pos >= 0; pos--) {
|
||
if (string[pos] !== " ") break;
|
||
}
|
||
// We hit the beginning of the string, or we found a sentence separator
|
||
if (string[pos] === undefined || string[pos].match(/[.?()!…]/)) {
|
||
repl = CommonStringTitlecase(repl);
|
||
}
|
||
return repl;
|
||
}
|
||
|
||
/** @type {CommonSubtituteSubstitution[]} */
|
||
let repls = [];
|
||
for (const pronounType of ["Possessive", "Self", "Subject", "Object"]) {
|
||
repls.push([key + pronounType, CharacterPronoun(C, pronounType, hideIdentity), replacer]);
|
||
}
|
||
return repls;
|
||
}
|
||
|
||
/**
|
||
* Gets only the settings/configurable properties of a chat room.
|
||
* @param {ChatRoom} room
|
||
* @return {ChatRoomSettings}
|
||
*/
|
||
function ChatRoomGetSettings(room) {
|
||
/** @type {(keyof ChatRoomSettings)[]} */
|
||
const keys = ["Name", "Description", "Admin", "Whitelist", "Ban", "Background", "Limit", "Game", "Visibility", "Access", "BlockCategory", "Language", "Space", "MapData", "Custom"];
|
||
return /** @type {ChatRoomSettings} */(Object.fromEntries(keys.map(k => [k, room[k]])));
|
||
}
|