bondage-college-mirr/BondageClub/Scripts/Inventory.js
BondageProjects b8278e3b8f Merge branch 'fix/cleanup-list-access' into 'master'
Move all the access list checks into a single function

See merge request 
2025-03-30 23:55:52 +00:00

1803 lines
76 KiB
JavaScript

"use strict";
/**
* Add a new item by group & name to character inventory
* @param {Character} C - The character that gets the new item added to her inventory
* @param {string} NewItemName - The name of the new item to add
* @param {AssetGroupName} NewItemGroup - The group name of the new item to add
* @param {boolean} [Push=true] - Set to TRUE to push to the server
*/
function InventoryAdd(C, NewItemName, NewItemGroup, Push) {
// First, we check if the inventory already exists, exit if it's the case
if (InventoryAvailable(C, NewItemName, NewItemGroup)) {
return;
}
// Create the new item for current character's asset family, group name and item name
var NewItem = InventoryItemCreate(C, NewItemGroup, NewItemName);
// Only add the item if we found the asset
if (NewItem) {
// Pushes the new item to the inventory queue
C.Inventory.push(NewItem);
// Sends the new item to the server if it's for the current player
if ((C.IsPlayer()) && ((Push == null) || Push))
ServerPlayerInventorySync();
}
}
/**
* Adds multiple new items by group & name to the character inventory
* @param {Character} C - The character that gets the new items added to her inventory
* @param {InventoryBundle[]} NewItems - The new items to add
* @param {Boolean} [Push=true] - Set to TRUE to push to the server, pushed by default
*/
function InventoryAddMany(C, NewItems, Push) {
// Return if data is invalid
if (C == null || !Array.isArray(NewItems)) return;
var ShouldSync = false;
// Add each items
for (let NI = 0; NI < NewItems.length; NI++) {
// First, we check if the item already exists in the inventory, continue if it's the case
if (!InventoryAvailable(C, NewItems[NI].Name, NewItems[NI].Group)) {
// Create the new item for current character's asset family, group name and item name
var NewItem = InventoryItemCreate(C, NewItems[NI].Group, NewItems[NI].Name);
// Only add the item if we found the asset
if (NewItem) {
// Pushes the new item to the inventory and flag the refresh
C.Inventory.push(NewItem);
ShouldSync = true;
}
}
}
// Sends the new item(s) to the server if it's for the current player and an item was added
if ((C.IsPlayer()) && ((Push == null) || Push) && ShouldSync) ServerPlayerInventorySync();
}
/**
* Creates a new item for a character based on asset group and name
* @param {Character} C - The character to create the item for
* @param {AssetGroupName} Group - The name of the asset group the item belongs to
* @param {string} Name - The name of the asset for the item
* @return {InventoryItem | null} A new item for character using the specified asset name, or null if the specified asset could not be
* found in the named group
*/
function InventoryItemCreate(C, Group, Name) {
var NewItemAsset = AssetGet(C.AssetFamily, Group, Name);
if (NewItemAsset) return { Name, Group, Asset: NewItemAsset };
return null;
}
/**
* Deletes an item from the character inventory
* @param {Character} C - The character on which we should remove the item
* @param {string} DelItemName - The name of the item to delete
* @param {AssetGroupName} DelItemGroup - The group name of the item to delete
* @param {boolean} [Push=true] - Set to TRUE to push to the server
* @return {InventoryItem}
*/
function InventoryDelete(C, DelItemName, DelItemGroup, Push) {
let item = null;
// First, we remove the item from the player inventory
for (let I = 0; I < C.Inventory.length; I++)
if ((C.Inventory[I].Name == DelItemName) && (C.Inventory[I].Group == DelItemGroup)) {
item = C.Inventory.splice(I, 1);
break;
}
// Next, we call the player account service to remove the item
if ((C.IsPlayer()) && ((Push == null) || Push))
ServerPlayerInventorySync();
return item ? item[0] : null;
}
/**
* Deletes all currently-owned items from a given group.
*
* @param {Character} C - The character to remove items from
* @param {AssetGroupName} group - The group name to clear
* @param {boolean} [push=true] - Whether to send an update to the server
* @return {InventoryItem[]} The list of deleted items
*/
function InventoryDeleteGroup(C, group, push) {
const deleted = [];
for (let I = 0; I < C.Inventory.length; I++) {
let item = C.Inventory[I];
if (item.Group != group) continue;
deleted.push(item);
C.Inventory.splice(I, 1);
// Move back one to not skip over items
I--;
}
if (deleted.length > 0 && (C.IsPlayer()) && ((push == null) || push))
ServerPlayerInventorySync();
return deleted;
}
/**
* Loads the current inventory for a character, can be loaded from an object of Name/Group or a compressed array using LZString
* @param {string | readonly ItemBundle[] | Partial<Record<AssetGroupName, readonly string[]>>} Inventory - An array of Name / Group of items to load
* @return {InventoryBundle[]}
*/
function InventoryLoad(Inventory, InventoryData) {
if (typeof InventoryData === "string" && InventoryData !== "") {
return InventoryLoadCompressedData(InventoryData);
}
/** @type {InventoryBundle[]} */
// Since InventoryData wasn't found (R104 and before), we load the old-style inventory object
if (Inventory == null) return [];
const items = [];
if (typeof Inventory === "string") {
try {
const Inv = JSON.parse(LZString.decompressFromUTF16(Inventory));
for (let I = 0; I < Inv.length; I++) {
items.push({ Group: Inv[I][1], Name: Inv[I][0] });
}
} catch(err) {
console.log("Error while loading compressed inventory, no inventory loaded.");
}
}
if (CommonIsArray(Inventory)) {
for (let I = 0; I < Inventory.length; I++) {
items.push(Inventory[I]);
}
} else if (typeof Inventory === "object") {
for (const G of CommonKeys(Inventory)) {
for (const A of Inventory[G]) {
items.push({ Group: G, Name: A });
}
}
}
return items;
}
/**
* Decompress inventory data into an item bundle
* @param {string} Data - The string of data to load
* @return {InventoryBundle[]}
*/
function InventoryLoadCompressedData(Data) {
/** @type {ItemBundle[]} */
const items = [];
// For each character in the string
let Pos = 0;
let LastID = -1;
while (Pos < Data.length) {
// Get the InventoryID of that character, if it's a range separator (58), we stay on that position for the full range
let ID = Data.charCodeAt(Pos);
if (ID == 58) {
ID = LastID + 1;
if ((Pos + 1 < Data.length) && (ID + 1 < Data.charCodeAt(Pos + 1))) Pos--;
}
// Adds all the assets linked to that inventory ID
const A = Asset.find(a => a.InventoryID === ID);
if (A) {
items.push({ Group: A.Group.Name, Name: A.Name });
} else {
console.warn(`unable to find asset for InventoryID: ${ID}, skipping`);
}
// Keeps the last ID for ranges and jump to the next position
LastID = ID;
Pos++;
}
return items;
}
/**
* Creates the inventory data string from current inventory
* @param {Character} C - The character on which we should load the inventory data string
*/
function InventoryDataBuild(C) {
// Loops in the character inventory to build a list of IDs
let IDList = [];
for (let I of C.Inventory)
if ((I.Asset.InventoryID != null) && (IDList.indexOf(I.Asset.InventoryID) < 0))
IDList.push(I.Asset.InventoryID);
// Sort the IDs from lowest to highest
IDList.sort(function(a, b) { return a - b; });
// Replace ranges by 58 (:), 101, 102, 103, 104 becomes 101, 58, 104
if (IDList.length >= 3)
for (let I = 1; I < IDList.length - 1; I++) {
if ((IDList[I - 1] + 1 == IDList[I]) && (IDList[I + 1] - 1 == IDList[I]))
IDList[I] = 58;
else
if ((IDList[I - 1] == 58) && (IDList[I + 1] - 1 == IDList[I])) {
IDList.splice(I, 1);
I--;
}
}
// Builds the final compressed string
let Data = "";
for (let I of IDList)
Data = Data + String.fromCharCode(I);
return Data;
}
/**
* Checks if the character has the inventory available
* @param {Character} C - The character on which we should remove the item
* @param {string} Name - The name of the item to validate
* @param {AssetGroupName} Group - The group name of the item to validate
*/
function InventoryAvailable(C, Name, Group) {
if (C.Inventory.find(i => i.Group === Group && i.Name === Name)) {
return true;
}
let asset = AssetGet(C.AssetFamily, Group, Name);
if (!asset) {
return false;
} else if (asset.AvailableLocations.some(loc => CurrentScreen.startsWith(loc) || ChatRoomSpace === loc)) {
return true;
} else if (asset.Value === 0) {
return true;
} else if (asset.BuyGroup != null) {
return Asset.filter(a => a.BuyGroup === asset.BuyGroup).some(a => a.Value === 0);
} else {
return false;
}
}
/**
* Returns an error message if a prerequisite clashes with the character's items and clothes
* @param {Character} C - The character on which we check for prerequisites
* @param {AssetPrerequisite} Prerequisite - The name of the prerequisite
* @param {null | Asset} asset - The asset (if any) for whom the prerequisite is checked
* @returns {string} - The error tag, can be converted to an error message
*/
function InventoryPrerequisiteMessage(C, Prerequisite, asset=null) {
switch (Prerequisite) {
// Basic pose prerequisites
case "CanBaseUpper": return !PoseAvailable(C, "BodyUpper", "BaseUpper") ? "CannotBaseUpper" : "";
case "CanBackBoxTie": return !PoseAvailable(C, "BodyUpper", "BackBoxTie") ? "CannotBackBoxTie" : "";
case "CanBackCuffs": return !PoseAvailable(C, "BodyUpper", "BackCuffs") ? "CannotBackCuffs" : "";
case "CanBackElbowTouch": return !PoseAvailable(C, "BodyUpper", "BackElbowTouch") ? "CannotBackElbowTouch" : "";
case "CanOverTheHead": return !PoseAvailable(C, "BodyUpper", "OverTheHead") ? "CannotOverTheHead" : "";
case "CanYoked": return !PoseAvailable(C, "BodyUpper", "Yoked") ? "CannotYoked" : "";
case "CanTapedHands": return !PoseAvailable(C, "BodyHands", "TapedHands") ? "CannotTapedHands" : "";
case "CanBaseLower": return !PoseAvailable(C, "BodyLower", "BaseLower") ? "CannotBaseLower" : "";
case "CanKneel": return !PoseAvailable(C, "BodyLower", "Kneel") ? "CannotKneel" : "";
case "CanKneelingSpread": return !PoseAvailable(C, "BodyLower", "KneelingSpread") ? "CannotKneelingSpread" : "";
case "CanLegsClosed": return !PoseAvailable(C, "BodyLower", "LegsClosed") ? "CannotLegsClosed" : "";
case "CanLegsOpen": return !PoseAvailable(C, "BodyLower", "LegsOpen") ? "CannotLegsOpen" : "";
case "CanSpread": return !PoseAvailable(C, "BodyLower", "Spread") ? "CannotSpread" : "";
case "CanHogtied": return !PoseAvailable(C, "BodyFull", "Hogtied") ? "CannotHogtied" : "";
case "CanAllFours": return !PoseAvailable(C, "BodyFull", "AllFours") ? "CannotAllFours" : "";
case "CanSuspension": return !PoseAvailable(C, "BodyAddon", "Suspension") ? "CannotSuspension" : "";
// Basic prerequisites that can apply to many items
case "NoItemFeet": return (InventoryGet(C, "ItemFeet") != null) ? "MustFreeFeetFirst" : "";
case "NoItemArms": return (InventoryGet(C, "ItemArms") != null) ? "MustFreeArmsFirst" : "";
case "NoItemLegs": return (InventoryGet(C, "ItemLegs") != null) ? "MustFreeLegsFirst" : "";
case "NoItemHands": return (InventoryGet(C, "ItemHands") != null) ? "MustFreeHandsFirst" : "";
case "NotKneeling": {
return (
PoseAllKneeling.some(p => C.PoseMapping.BodyLower === p)
&& !PoseAllStanding.some(p => PoseAvailable(C, "BodyLower", p))
) ? "MustStandUpFirst" : "";
}
case "NotMounted": return C.Effect.includes("Mounted") ? "CannotBeUsedWhenMounted" : "";
case "NotSuspended": return C.IsSuspended() ? "RemoveSuspensionForItem" : "";
case "NotLifted": return C.Effect.includes("Lifted") ? "RemoveSuspensionForItem" : "";
case "NotChaste": return C.Effect.includes("Chaste") ? "RemoveChastityFirst" : "";
case "NotChained": return C.Effect.includes("IsChained") ? "RemoveChainForItem" : "";
case "Collared": return (InventoryGet(C, "ItemNeck") == null) ? "MustCollaredFirst" : "";
case "CannotBeSuited": return InventoryIsItemInList(C, "ItemArms", ["FullLatexSuit"]) ? "CannotBeSuited" : "";
case "CannotHaveWand": return InventoryIsItemInList(C, "ItemVulva", ["WandBelt", "HempRopeBelt"]) ? "CannotHaveWand" : "";
case "OnBed": return !C.Effect.includes("OnBed") ? "MustBeOnBed" : "";
case "CuffedArms": return !C.Effect.includes("CuffedArms") ? "MustBeArmCuffedFirst" : "";
case "CuffedLegs": return !C.Effect.includes("CuffedFeet") ? "MustBeFeetCuffedFirst" : "";
case "CuffedFeet": return !C.Effect.includes("CuffedFeet") ? "MustBeFeetCuffedFirst" : "";
case "CuffedArmsOrEmpty": return (InventoryGet(C, "ItemArms") != null && !C.Effect.includes("CuffedArms")) ? "MustFreeArmsFirst" : "";
case "CuffedLegsOrEmpty": return (InventoryGet(C, "ItemLegs") != null && !C.Effect.includes("CuffedLegs")) ? "MustFreeLegsFirst" : "";
case "CuffedFeetOrEmpty": return (InventoryGet(C, "ItemFeet") != null && !C.Effect.includes("CuffedFeet")) ? "MustFreeFeetFirst" : "";
case "NoOuterClothes": return InventoryHasItemInAnyGroup(C, ["Cloth", "ClothLower"]) ? "RemoveClothesForItem" : "";
case "NoClothLower": return InventoryHasItemInAnyGroup(C, ["ClothLower"]) ? "RemoveClothesForItem" : "";
case "NoMaidTray": return InventoryIsItemInList(C, "ItemMisc", ["WoodenMaidTray", "WoodenMaidTrayFull"]) ? "CannotBeUsedWhileServingDrinks" : "";
case "CanBeCeilingTethered": return InventoryHasItemInAnyGroup(C, ["ItemArms", "ItemTorso", "ItemTorso2", "ItemPelvis"]) ? "" : "AddItemsToUse";
// Checks for body
case "HasBreasts": return !InventoryIsItemInList(C, "BodyUpper", ["XLarge", "Large", "Normal", "Small"]) ? "MustHaveBreasts" : "";
case "HasFlatChest": return !InventoryIsItemInList(C, "BodyUpper", ["FlatSmall", "FlatMedium"]) ? "MustHaveFlatChest" : "";
// Checks for genitalia
case "HasVagina": return !InventoryIsItemInList(C, "Pussy", ["Pussy1", "Pussy2", "Pussy3"]) ? "MustHaveVagina" : "";
case "HasPenis": return !InventoryIsItemInList(C, "Pussy", ["Penis"]) ? "MustHavePenis" : "";
case "CanHaveErection": {
if (!InventoryIsItemInList(C, "Pussy", ["Penis"]))
return "MustHavePenis";
if (C.HasEffect("Chaste"))
return "CantHaveErection";
return "";
}
case "CanBeLimp": {
if (!InventoryIsItemInList(C, "Pussy", ["Penis"]))
return "MustHavePenis";
if (C.HasEffect("ForcedErection"))
return "CantBeLimp";
return "";
}
// Checks for chastity cages, in case of penis protruding items.
case "NoChastityCage": return InventoryIsItemInList(C, "ItemVulva", ["TechnoChastityCage", "PlasticChastityCage1", "PlasticChastityCage2", "FlatChastityCage", "ChastityPouch"]) ? "MustRemoveChastityCage" : "";
case "NoErection": return C.HasEffect("ForcedErection") ? "MustNotHaveForcedErection" : "";
// Checks for penis protruding items, to block chastity cages.
case "AccessFullPenis": return InventoryIsItemInList(C, "ItemArms", ["HighSecurityStraitJacket"]) ? "MustHaveFullPenisAccess" : "";
// Checks for torso access based on clothes
case "AccessTorso": return !InventoryDoItemsExposeGroup(C, "ItemTorso", ["Cloth"]) ? "RemoveClothesForItem" : "";
// Breast items can be blocked by clothes
case "AccessBreast": return !InventoryDoItemsExposeGroup(C, "ItemBreast", ["Cloth"]) || !InventoryDoItemsExposeGroup(
C, "ItemBreast", ["Bra"]) ? "RemoveClothesForItem" : "";
case "AccessBreastSuitZip": return !InventoryDoItemsExposeGroup(C, "ItemNipplesPiercings", ["Cloth"]) || !InventoryDoItemsExposeGroup(
C, "ItemNipplesPiercings", ["Suit"]) ? "UnZipSuitForItem" : "";
// Vulva/Butt items can be blocked by clothes, panties and some socks, as well as chastity
case "AccessButt":
case "AccessVulva": {
const target = Prerequisite === "AccessVulva" ? "ItemVulva" : "ItemButt";
if (InventoryDoItemsBlockGroup(C, target, ["Cloth", "Socks"]) || !InventoryDoItemsExposeGroup(C, target, ["ClothLower", "Panties"]))
return "RemoveClothesForItem";
// Some chastity belts have removable vulva shields. This checks for those for items that wish to add something externally.
if (InventoryDoItemsBlockGroup(C, target, ["ItemPelvis", "ItemVulvaPiercings"])
|| Prerequisite === "AccessVulva" && C.IsVulvaChaste()
|| Prerequisite === "AccessButt" && C.IsButtChaste())
return "RemoveChastityFirst";
return "";
}
case "AccessCrotch":
return (InventoryDoItemsBlockGroup(C, "ItemPelvis", ["Cloth", "ClothLower", "Socks"])
|| (!InventoryDoItemsExposeGroup(C, "ItemVulva", ["ClothLower", "Panties"])
&& !InventoryDoItemsExposeGroup(C, "ItemVulvaPiercings", ["ClothLower", "Panties"])
&& !InventoryDoItemsExposeGroup(C, "ItemButt", ["ClothLower", "Panties"])))
? "RemoveClothesForItem" : "";
case "CanCoverVulva":
return C.HasEffect("VulvaShaft") ? "CantCloseOnShaft" : "";
// Items that require access to a certain character's zone
case "AccessMouth": return InventoryPrerequisiteConflicts.GagEffect(C, ["BlockMouth"], asset);
case "BlockedMouth": return InventoryPrerequisiteConflicts.GagEffect(C, ["BlockMouth"], asset, { errMessage: "MustBeUsedOverGag", invert: true });
case "HoodEmpty": return InventoryGet(C, "ItemHood") ? "CannotBeUsedOverMask" : "";
case "EyesEmpty": return InventoryGet(C, "ItemHead") ? "CannotBeUsedOverHood" : "";
// Ensure crotch is empty
case "VulvaEmpty": return (InventoryGet(C, "ItemVulva") != null) ? "MustFreeVulvaFirst" : "";
case "ClitEmpty": return ((InventoryGet(C, "ItemVulvaPiercings") != null)) ? "MustFreeClitFirst" : "";
case "ButtEmpty": return ((InventoryGet(C, "ItemButt") != null)) ? "MustFreeButtFirst" : "";
// For body parts that must be naked
case "NakedFeet": return InventoryHasItemInAnyGroup(C, ["ItemBoots", "Socks", "Shoes"]) ? "RemoveClothesForItem" : "";
case "NakedHands": return InventoryHasItemInAnyGroup(C, ["ItemHands", "Gloves"]) ? "RemoveClothesForItem" : "";
// Display Frame
case "DisplayFrame": return InventoryHasItemInAnyGroup(C, ["ItemArms", "ItemLegs", "ItemFeet", "ItemBoots"])
? "RemoveRestraintsFirst"
: InventoryHasItemInAnyGroup(C, ["Cloth", "ClothLower", "Shoes"])
? "RemoveClothesForItem" : "";
// Gas mask (Or face covering items going below the chin)
case "GasMask": return InventoryIsItemInList(C, "ItemArms", ["Pillory"]) || InventoryIsItemInList(C, "ItemDevices", ["TheDisplayFrame"]) ? "RemoveRestraintsFirst" : "";
case "NotMasked": return InventoryIsItemInList(C, "ItemHood", ["OldGasMask"]) ? "RemoveFaceMaskFirst" : "";
// Blocked remotes on self
case "RemotesAllowed": return LogQuery("BlockRemoteSelf", "OwnerRule") && C.IsPlayer() ? "OwnerBlockedRemotes" : "";
// Layered Gags, prevent gags from being equipped over other gags they are incompatible with
case "GagUnique": return InventoryPrerequisiteConflicts.GagPrerequisite(C, ["GagFlat", "GagCorset", "GagUnique"], asset);
case "GagCorset": return InventoryPrerequisiteConflicts.GagPrerequisite(C, ["GagCorset"], asset);
// There's something in the mouth that's too large to allow that item on
case "NotProtrudingFromMouth": return InventoryPrerequisiteConflicts.GagEffect(C, ["ProtrudingMouth"], asset, { errMessage: "CannotBeUsedOverGag" });
case "NeedsNippleRings": return !InventoryIsItemInList(C, "ItemNipplesPiercings", ["RoundPiercing"]) ? "NeedsNippleRings" : "";
case "CanAttachMittens": return !CharacterHasItemWithAttribute(C, "CanAttachMittens") ? "CantAttachMittens" : "";
case "NeedsChestHarness": return !CharacterHasItemWithAttribute(C, "IsChestHarness") ? "NeedsChestHarness" : "";
case "NeedsHipHarness": return !CharacterHasItemWithAttribute(C, "IsHipHarness") ? "NeedsHipHarness" : "";
// Returns no message, indicating that all prerequisites are fine
case "GagFlat": return "";
default: {
console.warn(`Unknown asset prerequisite "${Prerequisite}"`);
return "";
}
}
}
/**
* Prerequisite utility function that returns TRUE if the given character has an item equipped in the provided group
* whose name matches one of the names in the provided list.
* @param {Character} C - The character for whom to check equipped items
* @param {AssetGroupName} ItemGroup - The name of the item group to check
* @param {readonly string[]} ItemList - A list of item names to check against
* @returns {boolean} - TRUE if the character has an item from the item list equipped in the named slot, FALSE
* otherwise
*/
function InventoryIsItemInList(C, ItemGroup, ItemList) {
const Item = InventoryGet(C, ItemGroup);
return Item && ItemList.includes(Item.Asset.Name);
}
/**
* Prerequisite utility function that returns TRUE if the given character has an item equipped in the provided group
* which has the provided prerequisite.
* @param {Character} C - The character whose items should be checked
* @param {AssetGroupName} ItemGroup - The name of the item group to check
* @param {AssetPrerequisite} Prerequisite - The name of the prerequisite to look for
* @returns {boolean} - TRUE if the character has an item equipped in the named slot which has the named prerequisite,
* FALSE otherwise
*/
function InventoryDoesItemHavePrerequisite(C, ItemGroup, Prerequisite) {
const Item = InventoryGet(C, ItemGroup);
return Item && Item.Asset.Prerequisite && Item.Asset.Prerequisite.includes(Prerequisite);
}
/**
* Prerequisite utility function to check whether the target group for the given character is blocked by any of the
* given groups to check.
* @param {Character} C - The character whose items should be checked
* @param {AssetGroupItemName} TargetGroup - The name of the group that should be checked for being blocked
* @param {readonly AssetGroupName[]} GroupsToCheck - The name(s) of the groups whose items should be checked
* @returns {boolean} - TRUE if the character has an item equipped in any of the given groups to check which blocks the
* target group, FALSE otherwise.
*/
function InventoryDoItemsBlockGroup(C, TargetGroup, GroupsToCheck) {
return GroupsToCheck.some((Group) => {
const Item = InventoryGet(C, Group);
return Item && InventoryGetItemProperty(Item, "Block").includes(TargetGroup);
});
}
/**
* Prerequisite utility function to check whether the target group for the given character is exposed by all of the
* given groups to check.
* @param {Character} C - The character whose items should be checked
* @param {AssetGroupItemName} TargetGroup - The name of the group that should be checked for being exposed
* @param {readonly AssetGroupName[]} GroupsToCheck - The name(s) of the groups whose items should be checked
* @returns {boolean} - FALSE if the character has an item equipped in ANY of the given groups to check that does not
* expose the target group. Returns TRUE otherwise.
*/
function InventoryDoItemsExposeGroup(C, TargetGroup, GroupsToCheck) {
return GroupsToCheck.every((Group) => {
const Item = InventoryGet(C, Group);
return !Item || InventoryGetItemProperty(Item, "Expose").includes(TargetGroup);
});
}
/**
* Prerequisite utility function that returns TRUE if the given character has an item equipped in any of the named group
* slots.
* @param {Character} C - The character whose items should be checked
* @param {readonly AssetGroupName[]} GroupList - The list of groups to check for items in
* @returns {boolean} - TRUE if the character has any item equipped in any of the named groups, FALSE otherwise.
*/
function InventoryHasItemInAnyGroup(C, GroupList) {
return GroupList.some(GroupName => !!InventoryGet(C, GroupName));
}
// FIXME: The `asset` parameters should not accept `null`, but we cannot easily guarantee that it isn't due to
// `InventoryPrerequisiteMessage` not always having access to the asset (e.g. when checking action
// prerequisites, for which there really *is* no relevant asset)
/**
* Namespace with functions for identifying asset prerequisite conflicts.
* @namespace
*/
var InventoryPrerequisiteConflicts = {
/**
* The effective "layering" priorities of gags and (potentially) gag-like objects.
* Used for prerequisite checks that only apply to lower gags.
* @type {Partial<Record<AssetGroupName, number>>}
*/
GagPriorities: {
ItemNeck: 0,
ItemMouth: 1,
ItemMouth2: 2,
ItemMouth3: 3,
ItemHead: 4,
Mask: 5,
ItemHood: 6,
},
/**
* @private
* @template {keyof PropertiesArray} T
* @param {T} fieldName
* @param {Character} C - The character on which we check for prerequisites
* @param {PropertiesArray[T]} blockingPrereqs - The prerequisites we check for on lower gags
* @param {Asset} asset - The new gag
* @param {Object} options
* @param {string} [options.errMessage] - The to-be returned message if the gag is blocked
* @param {boolean} [options.invert] - Whether the prerequisite check should be inverted (_i.e._ if "not any" instead of "any")
* @returns {string} - Returns the error message if the gag is blocked, or an empty string if not
*/
_GagCheck: function _GagCheck(fieldName, C, blockingPrereqs, asset=null, options=null) {
/** @type {readonly unknown[]} */
const blockingPrereqsUnknown = blockingPrereqs;
const errMessage = options?.errMessage ?? "CannotBeUsedOverGag";
const invert = options?.invert ?? false;
/** @type {number | undefined} */
const gagPriority = InventoryPrerequisiteConflicts.GagPriorities[asset?.Group?.Name];
const gagItems = CommonKeys(InventoryPrerequisiteConflicts.GagPriorities).map(group => InventoryGet(C, group));
// Find all lower gags in which there is a prerequisite that blocks the new gag
const prereqConflicts = gagItems.slice(0, gagPriority).filter(item => {
/** @type {readonly unknown[]} */
const prereqs = InventoryGetItemProperty(item, fieldName);
return prereqs.some((p) => blockingPrereqsUnknown.includes(p));
}).filter((item) => item != null);
const hasConflict = invert ? prereqConflicts.length === 0 : prereqConflicts.length > 0;
return hasConflict ? errMessage : "";
},
/**
* Check if there are any lower gags with prerequisites that block the new gag from being applied
* @param {Character} C - The character on which we check for prerequisites
* @param {readonly AssetPrerequisite[]} blockingPrereqs - The prerequisites we check for on lower gags
* @param {Asset} asset - The new gag
* @param {Object} options
* @param {string} [options.errMessage] - The to-be returned message if the gag is blocked
* @param {boolean} [options.invert] - Whether the prerequisite check should be inverted (_i.e._ if "not any" instead of "any")
* @returns {string} - Returns the error message if the gag is blocked, or an empty string if not
*/
GagPrerequisite: function GagPrerequisite(C, blockingPrereqs, asset=null, options=null) {
return InventoryPrerequisiteConflicts._GagCheck("Prerequisite", C, blockingPrereqs, asset, options);
},
/**
* Check if there are any lower gags with effects that block the new gag from being applied
* @param {Character} C - The character on which we check for prerequisites
* @param {readonly EffectName[]} blockingEffects - The prerequisites we check for on lower gags
* @param {Asset} asset - The new gag
* @param {Object} options
* @param {string} [options.errMessage] - The to-be returned message if the gag is blocked
* @param {boolean} [options.invert] - Whether the prerequisite check should be inverted (_i.e._ if "not any" instead of "any")
* @returns {string} - Returns the error message if the gag is blocked, or an empty string if not
*/
GagEffect: function GagEffect(C, blockingEffects, asset=null, options=null) {
return InventoryPrerequisiteConflicts._GagCheck("Effect", C, /** @type {EffectName[]} */(blockingEffects), asset, options);
},
};
/**
* Returns an error message if we cannot add the item, no other items must block that prerequisite; `null` is returned otherwise
* @param {Character} C - The character on which we check for prerequisites
* @param {Asset} asset - The asset for which prerequisites should be checked. Any item equipped in the asset's group
* will be ignored for the purposes of the prerequisite check.
* @param {AssetPrerequisite | readonly AssetPrerequisite[]} [prerequisites=asset.Prerequisite] - An array of prerequisites or a string for a single
* prerequisite. If nothing is provided, the asset's default prerequisites will be used
* @param {readonly AssetPoseName[]} [allowActivePose=asset.AllowActivePose]
* @returns {string | null} - An error message (if any)
*/
function InventoryDisallow(C, asset, prerequisites = asset.Prerequisite, allowActivePose = asset.AllowActivePose) {
// Prerequisite can be a string
if (typeof prerequisites === "string") {
prerequisites = [prerequisites];
}
// If prerequisite isn't a valid array, return true
if (!CommonIsArray(prerequisites)) {
return null;
}
// Create/load a simple character for prerequisite checking
const checkCharacter = CharacterLoadSimple("InventoryAllow");
checkCharacter.Appearance = C.Appearance.filter((item) => item.Asset.Group.Name !== asset.Group.Name);
CharacterLoadEffect(checkCharacter);
PoseRefresh(checkCharacter);
let Msg = "";
const posePrereq = /** @type {PosePrerequisite[]} */(prerequisites.filter(p => p.slice(3) in PoseRecord));
if (posePrereq.length === 0) {
prerequisites.some(prereq => (Msg = InventoryPrerequisiteMessage(checkCharacter, prereq, asset)));
} else {
// In the case of poses the `SetPose`-based prerequisite can fall back to any `AllowActivePose` member of the same category
const poseMapping = PoseToMapping.Array(allowActivePose, "Assset.AllowActivePose");
for (const prereq of prerequisites) {
if (CommonIncludes(posePrereq, prereq)) {
const setPose = PoseRecord[/** @type {AssetPoseName} */(prereq.slice(3))];
const allowedActivePoses = [...(poseMapping[setPose.Category] || [setPose.Name]), ...(poseMapping.BodyFull || [])];
const messages = allowedActivePoses.map(p => InventoryPrerequisiteMessage(checkCharacter, `Can${p}`, asset));
if (messages.every(Boolean)) {
Msg = messages[0];
}
} else {
Msg = InventoryPrerequisiteMessage(checkCharacter, prereq, asset);
}
if (Msg) {
break;
}
}
}
return Msg ? InterfaceTextGet(`Prerequisite${Msg}`) : null;
}
/**
* Returns TRUE if we can add the item, no other items must block that prerequisite
* @param {Character} C - The character on which we check for prerequisites
* @param {Asset} asset - The asset for which prerequisites should be checked. Any item equipped in the asset's group
* will be ignored for the purposes of the prerequisite check.
* @param {AssetPrerequisite | readonly AssetPrerequisite[]} [prerequisites=asset.Prerequisite] - An array of prerequisites or a string for a single
* prerequisite. If nothing is provided, the asset's default prerequisites will be used
* @param {boolean} [setDialog=true] - If TRUE, set the screen dialog message at the same time
* @param {readonly AssetPoseName[]} [allowActivePose=asset.AllowActivePose]
* @returns {boolean} - TRUE if the item can be added to the character
*/
function InventoryAllow(C, asset, prerequisites = asset.Prerequisite, setDialog = true, allowActivePose = asset.AllowActivePose) {
const errMessage = InventoryDisallow(C, asset, prerequisites, allowActivePose);
// If no error message was found, we return TRUE, if a message was found, we can show it in the dialog
if (errMessage && setDialog) DialogSetStatus(InterfaceTextGet(`Prerequisite${errMessage}`), DialogTextDefaultDuration);
return errMessage === null;
}
/**
* Gets the current item / cloth worn a specific area (AssetGroup)
* @param {Character} C - The character on which we must check the appearance
* @param {AssetGroupName} AssetGroup - The name of the asset group to scan
* @returns {Item|null} - Returns the appearance which is the item / cloth asset, color and properties
*/
function InventoryGet(C, AssetGroup) {
for (let A = 0; A < C.Appearance.length; A++)
if ((C.Appearance[A].Asset != null) && (C.Appearance[A].Asset.Group.Family == C.AssetFamily) && (C.Appearance[A].Asset.Group.Name == AssetGroup))
return C.Appearance[A];
return null;
}
/**
* Applies crafted properties to the item used
* @param {null | Character} Source - The character that used the item (if any)
* @param {Character} Target - The character on which the item is used
* @param {AssetGroupItemName} GroupName - The name of the asset group to scan
* @param {CraftingItem} Craft - The crafted properties to apply
* @param {Boolean} Refresh - TRUE if we must refresh the character
* @param {Boolean} PreConfigureItem - TRUE if the default, pre-configured item state of the crafted item must be (re-)applied
* @param {Boolean} CraftWarn - Whether a warning should logged whenever the crafting validation fails
* @returns {void}
*/
function InventoryCraft(Source, Target, GroupName, Craft, Refresh, PreConfigureItem=true, CraftWarn=true) {
// Gets the item first
if ((Target == null) || (GroupName == null)) return;
let Item = InventoryGet(Target, GroupName);
if ((Item == null) || !CraftingValidate(Craft, Item.Asset, CraftWarn, Source?.IsPlayer())) return;
Item.Craft ??= Craft;
Item.Property ??= {};
Item.Difficulty ??= Item.Asset.Difficulty;
// Sets the crafter name and ID
if (Source) {
Item.Craft.MemberNumber = Source.MemberNumber;
Item.Craft.MemberName = CharacterNickname(Source);
}
// Abort; the properties below are pre-configured by crafted items the first time they're applied,
// but should otherwise not be touched by this function lest they undo any further item customization applied after the fact
if (!PreConfigureItem) {
return;
}
// Applies the color schema, separated by commas
Item.Color = Craft.Color.replace(" ", "").split(",");
// Set extended item properties
if (Item.Asset.Extended) {
ExtendedItemSetOptionByRecord(
Target, Item, Craft.TypeRecord,
{ push: false, refresh: false, C_Source: Source, properties: Craft.ItemProperty },
);
} else {
Object.assign(Item.Property, Craft.ItemProperty ?? {});
}
// Applies a lock to the item
if (Craft.Lock != "") {
InventoryLock(Target, Item, Craft.Lock, Source?.MemberNumber, false);
}
// Update the item's difficulty
Item.Difficulty += Craft.DifficultyFactor ?? 0;
// Apply Property-specific effects such as difficulty modifiers and facial expressions
switch (Craft.Property) {
case "Secure":
Item.Difficulty += 4;
break;
case "Loose":
Item.Difficulty -= 4;
break;
case "Decoy":
Item.Difficulty = -50;
break;
case "Painful":
InventoryExpressionTriggerApply(Target, [
{ Group: "Blush", Name: "ShortBreath", Timer: 10 },
{ Group: "Eyes", Name: "Angry", Timer: 10 },
{ Group: "Eyes2", Name: "Angry", Timer: 10 },
{ Group: "Eyebrows", Name: "Angry", Timer: 10 },
]);
Item.Property.Fetish = CommonArrayConcatDedupe(Item.Property.Fetish || [], ["Masochism"]);
break;
case "Comfy":
InventoryExpressionTriggerApply(Target, [
{ Group: "Blush", Name: "Low", Timer: 10 },
{ Group: "Eyes", Name: "Horny", Timer: 10 },
{ Group: "Eyes2", Name: "Horny", Timer: 10 },
{ Group: "Eyebrows", Name: "Raised", Timer: 10 },
]);
break;
}
// Refreshes the character if needed
if (Refresh) {
CharacterRefresh(Target, true);
}
}
/**
* Returns the number of items on a character with a specific property
* @param {Character} C - The character to validate
* @param {CraftingPropertyType} Property - The property to count
* @returns {Number} - The number of times the property is found
*/
function InventoryCraftCount(C, Property) {
let Count = 0;
if ((C != null) && (C.Appearance != null))
for (let A of C.Appearance)
if ((A.Craft != null) && (A.Craft.Property != null) && (A.Craft.Property == Property))
Count++;
return Count;
}
/**
* Returns TRUE if an item as the specified crafted property
* @param {Item} Item - The item to validate
* @param {CraftingPropertyType} Property - The property to check
* @returns {boolean} - TRUE if the property matches
*/
function InventoryCraftPropertyIs(Item, Property) {
if ((Item == null) || (Item.Craft == null) || (Item.Craft.Property == null) || (Property == null)) return false;
return (Item.Craft.Property == Property);
}
/**
* Sets the craft and type on the item, uses the achetype properties if possible.
* Note that appearance changes are _not_ pushed to the server.
* @deprecated Use {@link InventoryCraft} instead (or use {@link InventoryWear} directly if appropriate)
*/
var InventoryWearCraft = /** @type {never} */(function() { return; });
/**
* Makes the character wear an item on a body area
* @param {Character} C - The character that must wear the item
* @param {string} AssetName - The name of the asset to wear
* @param {AssetGroupName} AssetGroup - The name of the asset group to wear
* @param {string | string[]} [ItemColor] - The hex color of the item, can be undefined or "Default"
* @param {number} [Difficulty] - The difficulty, on top of the base asset difficulty, to assign to the item
* @param {number} [MemberNumber] - The member number of the character putting the item on - defaults to -1
* @param {CraftingItem} [Craft] - The crafting properties of the item
* @param {boolean} [Refresh] - Whether to refresh the character and push the changes to the server
* @returns {null | Item} - Thew newly created item or `null` if the asset does not exist
*/
function InventoryWear(C, AssetName, AssetGroup, ItemColor, Difficulty, MemberNumber, Craft, Refresh=true) {
const A = AssetGet(C.AssetFamily, AssetGroup, AssetName);
if (!A) return null;
const color = (ItemColor == null || ItemColor == "Default") ? [...A.DefaultColor] : ItemColor;
const item = CharacterAppearanceSetItem(C, AssetGroup, A, color, Difficulty, MemberNumber, false);
/**
* TODO: grant tighter control over setting expressions.
* As expressions handle a "first come, first served" principle this can currently override extended
* item option-specific expressions and crafting property-specific expressions (see {@link InventoryCraft})
*/
if (A.ExpressionTrigger != null) {
InventoryExpressionTriggerApply(C, A.ExpressionTrigger);
}
if (Craft) {
// Restore the item color if it has been explicitly passed; keep using the `Craft.Color`-assigned color otherwise
const C_Source = Character.find(c => c.MemberNumber === MemberNumber) ?? null;
InventoryCraft(C_Source, C, /** @type {AssetGroupItemName} */(AssetGroup), Craft, false);
if (ItemColor != null) {
item.Color = color;
}
}
if (Refresh) {
CharacterRefresh(C, true);
}
return item;
}
/**
* Sets the difficulty to remove an item for a body area
* @param {Character} C - The character that is wearing the item
* @param {AssetGroupItemName} AssetGroup - The name of the asset group
* @param {number} Difficulty - The new difficulty level to escape from the item
*/
function InventorySetDifficulty(C, AssetGroup, Difficulty) {
if ((Difficulty >= 0) && (Difficulty <= 100))
for (let A = 0; A < C.Appearance.length; A++)
if ((C.Appearance[A].Asset != null) && (C.Appearance[A].Asset.Group.Name == AssetGroup))
C.Appearance[A].Difficulty = Difficulty;
if ((CurrentModule != "Character") && (C.IsPlayer())) ServerPlayerAppearanceSync();
}
/**
* Returns TRUE if there's already a locked item at a given body area
* @param {Character} C - The character that is wearing the item
* @param {AssetGroupItemName} AssetGroup - The name of the asset group (body area)
* @param {Boolean} CheckProperties - Set to TRUE to check for additionnal properties
* @returns {Boolean} - TRUE if the item is locked
*/
function InventoryLocked(C, AssetGroup, CheckProperties) {
var I = InventoryGet(C, AssetGroup);
return ((I != null) && InventoryItemHasEffect(I, "Lock", CheckProperties));
}
/**
* Makes the character wear a random item from a body area
* @param {Character} C - The character that must wear the item
* @param {AssetGroupName} GroupName - The name of the asset group (body area)
* @param {number} [Difficulty] - The difficulty, on top of the base asset difficulty, to assign to the item
* @param {boolean} [Refresh=true] - Do not call CharacterRefresh if false
* @param {boolean} [MustOwn=false] - If TRUE, only assets that the character owns can be worn. Otherwise any asset can
* be used
* @param {boolean} [Extend=true] - Whether or not to randomly extend the item (i.e. set the item type), provided it has
* an archetype that supports random extension
* @param {readonly string[]} [AllowedAssets=null] - A list of assets from which one must be selected
* @param {boolean} [IgnoreRequirements=false] - If True, the group being blocked and prerequisites will not prevent the item being added.
* NOTE: Long-term this should be replaced with better checks before calling this function.
* @returns {Item | null} - The equipped item (if any)
*/
function InventoryWearRandom(C, GroupName, Difficulty, Refresh = true, MustOwn = false, Extend = true, AllowedAssets = null, IgnoreRequirements = false) {
if (InventoryLocked(C, /** @type {AssetGroupItemName} */(GroupName), true)) {
return;
}
// Finds the asset group and make sure it's not blocked
const Group = AssetGroupGet(C.AssetFamily, GroupName);
if (!IgnoreRequirements && (!Group || InventoryGroupIsBlocked(C, /** @type {AssetGroupItemName} */(GroupName)))) {
return;
}
const IsClothes = Group.Clothing;
let AssetList = null;
if (AllowedAssets) {
AssetList = AllowedAssets.map(assetName => Asset.find(A => A.Group.Name == GroupName && A.Name == assetName));
}
// Restrict the options to assets owned by the character
if (MustOwn) {
CharacterAppearanceBuildAssets(C);
if (AssetList) {
AssetList.filter(A => CharacterAppearanceAssets.some(CAA => CAA.Group.Name == A.Group.Name && CAA.Name == A.Name));
} else {
AssetList = CharacterAppearanceAssets;
}
}
// Get and apply a random asset
const SelectedAsset = InventoryGetRandom(C, GroupName, AssetList, IgnoreRequirements);
// Pick a random color for clothes from their schema
const SelectedColor = IsClothes ? SelectedAsset.Group.ColorSchema[Math.floor(Math.random() * SelectedAsset.Group.ColorSchema.length)] : null;
let item = CharacterAppearanceSetItem(C, GroupName, SelectedAsset, SelectedColor, Difficulty, null, false);
if (Extend) {
item = InventoryRandomExtend(C, GroupName);
}
if (Refresh) {
CharacterRefresh(C);
}
return item;
}
/**
* Randomly extends an item (sets an item type, etc.) on a character
* @param {Character} C - The character wearing the item
* @param {AssetGroupName} GroupName - The name of the item's group
* @param {null | Character} [C_Source] - The character setting the new item option. If `null`, assume that it is _not_ the player character.
* @returns {Item | null} - The equipped item (if any)
*/
function InventoryRandomExtend(C, GroupName, C_Source=null) {
const Item = InventoryGet(C, GroupName);
if (!Item || !Item.Asset.Archetype) {
return;
}
switch (Item.Asset.Archetype) {
case ExtendedArchetype.TYPED:
TypedItemSetRandomOption(C, Item, false, C_Source);
break;
default:
// Archetype does not yet support random extension
break;
}
return Item;
}
/**
* Select a random asset from a group, narrowed to the most preferable available options (i.e
* unblocked/visible/unlimited) based on their binary "rank"
* @param {Character} C - The character to pick the asset for
* @param {AssetGroupName} GroupName - The asset group to pick the asset from. Set to an empty string to not filter by group.
* @param {readonly Asset[]} [AllowedAssets] - Optional parameter: A list of assets from which one can be selected. If not provided,
* the full list of all assets is used.
* @param {boolean} [IgnorePrerequisites=false] - If True, skip the step to check whether prerequisites are met
* NOTE: Long-term this should be replaced with better checks before calling this function.
* @returns {Asset|null} - The randomly selected asset or `null` if none found
*/
function InventoryGetRandom(C, GroupName, AllowedAssets, IgnorePrerequisites = false) {
/** @type {{ Asset: Asset, Rank: number }[]} */
var List = [];
var AssetList = AllowedAssets || Asset;
var RandomOnly = (AllowedAssets == null);
var MinRank = Math.pow(2, 10);
var BlockedRank = Math.pow(2, 2);
var HiddenRank = Math.pow(2, 1);
var LimitedRank = Math.pow(2, 0);
for (let A = 0; A < AssetList.length; A++)
if (((AssetList[A].Group.Name == GroupName && AssetList[A].Wear) || !GroupName)
&& (RandomOnly == false || AssetList[A].Random)
&& AssetList[A].Enable
&& (IgnorePrerequisites || InventoryAllow(C, AssetList[A], undefined, false)))
{
var CurrRank = 0;
if (InventoryIsPermissionBlocked(C, AssetList[A].Name, AssetList[A].Group.Name)) {
if (BlockedRank > MinRank) continue;
else CurrRank += BlockedRank;
}
if (CharacterAppearanceItemIsHidden(AssetList[A].Name, GroupName)) {
if (HiddenRank > MinRank) continue;
else CurrRank += HiddenRank;
}
if (InventoryIsPermissionLimited(C, AssetList[A].Name, AssetList[A].Group.Name)) {
if (LimitedRank > MinRank) continue;
else CurrRank += LimitedRank;
}
MinRank = Math.min(MinRank, CurrRank);
List.push({ Asset: AssetList[A], Rank: CurrRank });
}
var PreferredList = List.filter(L => L.Rank == MinRank);
if (PreferredList.length == 0) return null;
return PreferredList[Math.floor(Math.random() * PreferredList.length)].Asset;
}
/**
* Removes a specific item from a character body area
* @param {Character} C - The character on which we must remove the item
* @param {AssetGroupName} AssetGroup - The name of the asset group (body area)
* @param {boolean} [Refresh] - Whether or not to trigger a character refresh. Defaults to false
*/
function InventoryRemove(C, AssetGroup, Refresh) {
const lastblindlevel = Player.GetBlindLevel();
DrawLastDarkFactor = CharacterGetDarkFactor(Player);
// First loop to find the item and any sub item to remove with it
for (let E = 0; E < C.Appearance.length; E++)
if (C.Appearance[E].Asset.Group.Name == AssetGroup) {
let AssetToRemove = C.Appearance[E].Asset;
let AssetToCheck = null;
for (let R = 0; R < AssetToRemove.RemoveItemOnRemove.length; R++) {
AssetToCheck = AssetToRemove.RemoveItemOnRemove[R];
if (!AssetToCheck.Name) {
// Just try to force remove a group, if no item is specified
InventoryRemove(C, AssetToCheck.Group, false);
} else {
let AssetFound = InventoryGet(C, AssetToCheck.Group);
// If a name is specified check if the item is worn
if (AssetFound && (AssetFound.Asset.Name == AssetToCheck.Name))
// If there is no type check or there is a type check and the item type matches, remove it
if (AssetToCheck.TypeRecord) {
if (
AssetFound.Property
&& AssetFound.Property.TypeRecord
&& CommonObjectIsSubset(AssetToCheck.TypeRecord, AssetFound.Property.TypeRecord)
) {
InventoryRemove(C, AssetToCheck.Group, false);
}
} else {
InventoryRemove(C, AssetToCheck.Group, false);
}
}
}
}
// Second loop to find the item again, and remove it from the character appearance
for (let E = 0; E < C.Appearance.length; E++)
if (C.Appearance[E].Asset.Group.Name == AssetGroup) {
C.Appearance.splice(E, 1);
if (Refresh || Refresh == null) CharacterRefresh(C);
if (Player.GraphicsSettings && Player.GraphicsSettings.DoBlindFlash) {
if (Refresh == false) CharacterLoadEffect(C); // update Effect to get the new blind level
if (lastblindlevel > 0 && Player.GetBlindLevel() === 0) {
DrawBlindFlash(lastblindlevel);
}
}
return;
}
}
/**
* Returns TRUE if the body area (Asset Group) for a character is blocked for either restraints (default) or activities and cannot be used
* @param {Character} C - The character on which we validate the group
* @param {null | AssetGroupItemName} [GroupName] - The name of the asset group (body area), defaults to `C.FocusGroup`
* @param {boolean} [Activity=false] - if TRUE check if activity is allowed on the asset group
* @returns {boolean} - TRUE if the group is blocked
*/
function InventoryGroupIsBlockedForCharacter(C, GroupName=null, Activity=false) {
GroupName ??= C.FocusGroup.Name;
const restraints = C.Appearance.filter(i => i.Asset.Group.IsItem());
if (Activity && !restraints.some(i => i.Asset.AllowActivityOn.includes(GroupName) || i.Property?.AllowActivityOn?.includes(GroupName))) {
Activity = false;
}
// Items can block each other (hoods blocks gags, belts blocks eggs, etc.)
if (!Activity && restraints.some(i => i.Asset.Block?.includes(GroupName) || i.Property?.Block?.includes(GroupName))) {
return true;
}
// If another character is enclosed, items other than the enclosing one cannot be used
if (!C.IsPlayer() && C.IsEnclose()) {
return !restraints.some(i => i.Asset.Group.Name == GroupName && InventoryItemHasEffect(i, "Enclose", true));
} else {
return false;
}
}
/**
* Returns TRUE if no item can be used by the player on the character because of the map distance
* @param {Character} C - The character on which we validate the distance
* @returns {boolean} - TRUE if distance is too far
*/
function InventoryIsBlockedByDistance(C) {
if ((C == null) || C.IsPlayer()) return false;
if ((CurrentScreen !== "ChatRoom") || !ChatRoomIsViewActive(ChatRoomMapViewName)) return false;
if (ChatRoomMapViewHasSuperPowers() || ChatRoomMapViewCharacterOnInteractionRange(C)) return false;
return true;
}
/**
* Returns TRUE if the body area is blocked by an owner rule
* @param {Character} C - The character on which we validate the group
* @param {AssetGroupName} [GroupName] - The name of the asset group (body area)
* @returns {boolean} - TRUE if the group is blocked
*/
function InventoryGroupIsBlockedByOwnerRule(C, GroupName) {
if (!C.IsPlayer()) return false;
if (!Player.IsOwned()) return false;
if (CurrentCharacter == null) return false;
if (GroupName == null) GroupName = C.FocusGroup.Name;
const Dict = [
["A", "ItemBoots"],
["B", "ItemFeet"],
["C", "ItemLegs"],
["D", "ItemVulva"],
["E", "ItemVulvaPiercings"],
["F", "ItemButt"],
["G", "ItemPelvis"],
["H", "ItemTorso"],
["I", "ItemTorso2"],
["J", "ItemNipples"],
["K", "ItemNipplesPiercings"],
["L", "ItemBreast"],
["M", "ItemHands"],
["N", "ItemArms"],
["O", "ItemNeck"],
["P", "ItemNeckAccessories"],
["Q", "ItemNeckRestraints"],
["R", "ItemMouth"],
["S", "ItemMouth2"],
["T", "ItemMouth3"],
["U", "ItemNose"],
["V", "ItemEars"],
["W", "ItemHead"],
["X", "ItemHood"],
["0", "ItemMisc"],
["1", "ItemDevices"],
["2", "ItemAddon"]
];
for (let D of Dict)
if (D[1] == GroupName)
return LogContain("BlockItemGroup", "OwnerRule", D[0]);
return false;
}
/**
* Returns an error message if the body area (Asset Group) for a character is blocked, and `null` otherwise.
*
* Similar to {@link InventoryGroupIsBlockedForCharacter} but also checks for map range and owner rules.
* @param {Character} C - The character on which we validate the group
* @param {null | AssetGroupItemName} [GroupName] - The name of the asset group (body area)
* @param {boolean} [Activity] - if TRUE check if activity is allowed on the asset group
* @returns {null | string} - The error message (if any)
*/
function InventoryGroupIsAvailable(C, GroupName=null, Activity=false) {
// Checks for regular blocks and enclose effects
if (InventoryGroupIsBlockedForCharacter(C, GroupName, Activity)) {
return InterfaceTextGet("ZoneBlocked");
}
// Check for map range
if (InventoryIsBlockedByDistance(C)) {
return InterfaceTextGet("ZoneBlockedRange");
}
// Checks if there's an owner rule that blocks the group
if (InventoryGroupIsBlockedByOwnerRule(C, GroupName)) {
return InterfaceTextGet("ZoneBlockedOwner");
}
// Nothing is preventing the group from being used
return null;
}
/**
* Returns TRUE if the body area (Asset Group) for a character is blocked and cannot be used.
*
* Similar to {@link InventoryGroupIsBlockedForCharacter} but also checks for map range and owner rules.
* @param {Character} C - The character on which we validate the group
* @param {null | AssetGroupItemName} [GroupName] - The name of the asset group (body area)
* @param {boolean} [Activity] - if TRUE check if activity is allowed on the asset group
* @returns {boolean} - TRUE if the group is blocked
*/
function InventoryGroupIsBlocked(C, GroupName=null, Activity=false) {
return InventoryGroupIsAvailable(C, GroupName, Activity) !== null;
}
/**
* Returns TRUE if an item has a specific effect
* @param {Item} Item - The item from appearance that must be validated
* @param {EffectName} [Effect] - The name of the effect to validate, can be undefined to check for any effect
* @param {boolean} [CheckProperties=true] - If properties should be checked (defaults to `true`)
* @returns {boolean} `true` if the effect is on the item
*/
function InventoryItemHasEffect(Item, Effect, CheckProperties = true) {
if (!Item) return false;
if (!Effect) {
return !!(
(Item.Asset && Array.isArray(Item.Asset.Effect) && Item.Asset.Effect.length > 0) ||
(CheckProperties && Item.Property && Array.isArray(Item.Property.Effect) && Item.Property.Effect.length > 0)
);
} else {
return !!(
(Item.Asset && Array.isArray(Item.Asset.Effect) && Item.Asset.Effect.includes(Effect)) ||
(CheckProperties && Item.Property && Array.isArray(Item.Property.Effect) && Item.Property.Effect.includes(Effect))
);
}
}
/**
* Returns TRUE if an item lock is pickable
* @param {Item} Item - The item from appearance that must be validated
* @returns {Boolean} - TRUE if PickDifficulty is on the item
*/
function InventoryItemIsPickable(Item) {
if (!Item) return null;
const lock = InventoryGetLock(Item);
if (lock && lock.Asset && lock.Asset.PickDifficulty && lock.Asset.PickDifficulty > 0) return true;
else return false;
}
/** @satisfies {Set<keyof PropertiesArray>} */
const PropertiesArrayLike = new Set(/** @type {const} */([
"Alpha", "Attribute", "Block", "Category", "DefaultColor", "Effect", "Expose", "ExpressionTrigger", "Prerequisite", "Require", "Fetish", "Tint",
"AllowActivePose", "SetPose",
"AllowActivity", "AllowActivityOn",
"Hide", "HideItem", "HideItemExclude", "UnHide",
"MemberNumberList", "Texts",
"AllowBlock", "AllowEffect", "AllowExpression", "AllowHide", "AllowHideItem",
"AvailableLocations", "ExpressionPrerequisite",
]));
/** @satisfies {Set<keyof PropertiesRecord>} */
const PropertiesObjectLike = new Set(/** @type {const} */([
"ActivityExpression", "PoseMapping", "RemoveItemOnRemove", "TypeRecord", "AllowLockType",
]));
/**
* Returns the value of a given property of an appearance item, prioritizes the Property object.
* @template {keyof ItemProperties | keyof Asset | keyof AssetGroup} Name
* @param {Item} Item - The appearance item to scan
* @param {Name} PropertyName - The property name to get.
* @param {boolean} [CheckGroup=false] - Whether or not to fall back to the item's group if the property is not found on
* Property or Asset.
* @returns {(ItemProperties & Asset & AssetGroup)[Name] | undefined} - The value of the requested property for the given item.
* Returns either undefined, an empty array or object if the property or the item itself does not exist.
*/
function InventoryGetItemProperty(Item, PropertyName, CheckGroup=false) {
/** @type {(ItemProperties & Asset & AssetGroup)[Name] | undefined} */
let value = undefined;
// @ts-expect-error Transparently return an empty array, which confuses TS
if (PropertiesArrayLike.has(PropertyName)) value = [];
// @ts-expect-error Transparently return an empty object, which confuses TS
else if (PropertiesObjectLike.has(PropertyName)) value = {};
if (!Item || !PropertyName || !Item.Asset) return value;
const PropertyRecords = /** @type {(ItemProperties & Asset & AssetGroup)[]} */(
[Item.Property || {}, Item.Asset, (CheckGroup) ? Item.Asset.Group : {}]
);
for (const record of PropertyRecords) {
// If the collected property doesn't exist, or is set to null, fall-through so
// we return the type-appropriate default
if (record[PropertyName] !== undefined && record[PropertyName] !== null) {
return record[PropertyName];
}
}
return value;
}
/**
* Apply an item's expression trigger to a character if able
* @param {Character} C - The character to update
* @param {readonly ExpressionTrigger[]} expressions - The expression change to apply to each group
*/
function InventoryExpressionTriggerApply(C, expressions) {
// NPCs and simple character usually lack `OnlineSharedSettings.ItemsAffectExpressions` unless explicitly added
let expressionsAllowed = C.OnlineSharedSettings && C.OnlineSharedSettings.ItemsAffectExpressions;
if (expressionsAllowed === undefined) {
expressionsAllowed = true;
}
if (expressionsAllowed) {
expressions.forEach(expression => {
const targetGroupItem = InventoryGet(C, expression.Group);
if (!targetGroupItem || !targetGroupItem.Property || !targetGroupItem.Property.Expression) {
CharacterSetFacialExpression(C, expression.Group, expression.Name, expression.Timer);
}
});
}
}
/**
* Returns the padlock item that locks another item
* @param {Item} Item - The item from appearance that must be scanned
* @returns {Item} - A padlock item or NULL if none
*/
function InventoryGetLock(Item) {
if ((Item == null) || (Item.Property == null) || (Item.Property.LockedBy == null)) return null;
for (let A = 0; A < Asset.length; A++)
if (Asset[A].IsLock && (Asset[A].Name == Item.Property.LockedBy))
return { Asset: Asset[A] };
return null;
}
/**
* Returns TRUE if the item has an OwnerOnly flag, such as the owner padlock
* @param {Item} Item - The item from appearance that must be scanned
* @returns {Boolean} - TRUE if owner only
*/
function InventoryOwnerOnlyItem(Item) {
if (Item == null) return false;
if (Item.Asset.OwnerOnly) return true;
if (Item.Asset.Group.Category == "Item") {
var Lock = InventoryGetLock(Item);
if ((Lock != null) && (Lock.Asset.OwnerOnly != null) && Lock.Asset.OwnerOnly) return true;
}
return false;
}
/**
* Returns TRUE if the item has a LoverOnly flag, such as the lover padlock
* @param {Item} Item - The item from appearance that must be scanned
* @returns {Boolean} - TRUE if lover only
*/
function InventoryLoverOnlyItem(Item) {
if (Item == null) return false;
if (Item.Asset.LoverOnly) return true;
if (Item.Asset.Group.Category == "Item") {
var Lock = InventoryGetLock(Item);
if ((Lock != null) && (Lock.Asset.LoverOnly != null) && Lock.Asset.LoverOnly) return true;
}
return false;
}
/**
* Returns TRUE if the item has a FamilyOnly flag, such as the D/s family padlock
* @param {Item} Item - The item from appearance that must be scanned
* @returns {Boolean} - TRUE if family only
*/
function InventoryFamilyOnlyItem(Item) {
if (Item == null) return false;
if (Item.Asset.FamilyOnly) return true;
if (Item.Asset.Group.Category == "Item") {
var Lock = InventoryGetLock(Item);
if ((Lock != null) && (Lock.Asset.FamilyOnly != null) && Lock.Asset.FamilyOnly) return true;
}
return false;
}
/**
* Returns TRUE if the character is wearing at least one restraint that's locked with an extra lock
* @param {Character} C - The character to scan
* @returns {Boolean} - TRUE if one restraint with an extra lock is found
*/
function InventoryCharacterHasLockedRestraint(C) {
if (C.Appearance != null)
for (let A = 0; A < C.Appearance.length; A++)
if (C.Appearance[A].Asset.IsRestraint && (InventoryGetLock(C.Appearance[A]) != null))
return true;
return false;
}
/**
*
* @param {Character} C - The character to scan
* @param {AssetLockType} LockName - The type of lock to check for
* @returns {Boolean} - Returns TRUE if any item has the specified lock locked onto it
*/
function InventoryCharacterIsWearingLock(C, LockName) {
for (let A = 0; A < C.Appearance.length; A++)
if ((C.Appearance[A].Property != null) && (C.Appearance[A].Property.LockedBy == LockName))
return true;
return false;
}
/**
* Returns TRUE if the character is wearing at least one item that's a restraint with a OwnerOnly flag, such as the
* owner padlock
* @param {Character} C - The character to scan
* @returns {Boolean} - TRUE if one owner only restraint is found
*/
function InventoryCharacterHasOwnerOnlyRestraint(C) {
if (!C.IsOwned()) return false;
if (C.Appearance != null)
for (let A = 0; A < C.Appearance.length; A++)
if (C.Appearance[A].Asset.IsRestraint && InventoryOwnerOnlyItem(C.Appearance[A]))
return true;
return false;
}
/**
* Returns TRUE if the character is wearing at least one item that's a restraint with a LoverOnly flag, such as the
* lover padlock
* @param {Character} C - The character to scan
* @returns {Boolean} - TRUE if one lover only restraint is found
*/
function InventoryCharacterHasLoverOnlyRestraint(C) {
if (C.GetLoversNumbers().length == 0) return false;
if (C.Appearance != null)
for (let A = 0; A < C.Appearance.length; A++) {
if (C.Appearance[A].Asset.IsRestraint && InventoryLoverOnlyItem(C.Appearance[A]))
return true;
}
return false;
}
/**
* Returns TRUE if the character is wearing at least one item that's a restraint with a FamilyOnly flag
* @param {Character} C - The character to scan
* @returns {Boolean} - TRUE if one family only restraint is found
*/
function InventoryCharacterHasFamilyOnlyRestraint(C) {
if (C.Appearance != null)
for (let A = 0; A < C.Appearance.length; A++) {
if (C.Appearance[A].Asset.IsRestraint && InventoryFamilyOnlyItem(C.Appearance[A]))
return true;
}
return false;
}
/**
* Returns TRUE if at least one item on the character can be locked
* @param {Character} C - The character to scan
* @returns {Boolean} - TRUE if at least one item can be locked
*/
function InventoryHasLockableItems(C) {
return C.Appearance.some((item) => InventoryDoesItemAllowLock(item) && InventoryGetLock(item) == null);
}
/**
* Determines whether an item in its current state permits locks.
* @param {Item} item - The item to check
* @returns {boolean} - TRUE if the asset's current type permits locks
*/
function InventoryDoesItemAllowLock(item) {
const asset = item.Asset;
const typeRecord = item.Property && item.Property.TypeRecord;
if (asset.AllowLockType != null && CommonIsObject(typeRecord)) {
const entries = Object.entries(typeRecord);
return entries.some(([k, i]) => asset.AllowLockType[k] && asset.AllowLockType[k].has(i));
} else {
return asset.AllowLock;
}
}
/**
* Applies a lock to an appearance item of a character
* @param {Character} C - The character on which the lock must be applied
* @param {Item|AssetGroupName} Item - The item from appearance to lock
* @param {Item|AssetLockType} Lock - The asset of the lock or the name of the lock asset
* @param {null|number|string} [MemberNumber] - The member number to put on the lock, or message to show
* @param {boolean} [Update=true] - Whether or not to update the character
*/
function InventoryLock(C, Item, Lock, MemberNumber, Update = true) {
if (typeof Item === 'string') Item = InventoryGet(C, Item);
if (typeof Lock === 'string') Lock = { Asset: AssetGet(C.AssetFamily, "ItemMisc", Lock) };
if (!Item || !Lock || !Lock.Asset || !Lock.Asset.IsLock || !InventoryDoesItemAllowLock(Item)) return;
// Protect against using the PortalLink lock on incompatible items
if (Lock.Asset.Name === "PortalLinkPadlock" && !InventoryGetItemProperty(Item, "Attribute").includes("PortalLinkLockable"))
return;
if (Item.Property == null) Item.Property = {};
if (Item.Property.Effect == null) Item.Property.Effect = [];
if (Item.Property.Effect.indexOf("Lock") < 0) Item.Property.Effect.push("Lock");
if (!Item.Property.MemberNumberListKeys && Lock.Asset.Name == "HighSecurityPadlock") Item.Property.MemberNumberListKeys = "" + MemberNumber;
Item.Property.LockedBy = /** @type AssetLockType */(Lock.Asset.Name);
if (MemberNumber != null) Item.Property.LockMemberNumber = MemberNumber;
/** @type {Parameters<ExtendedItemCallbacks.Init>} */
const args = [C, Item, false, false];
CommonCallFunctionByName(`Inventory${Lock.Asset.Group.Name}${Lock.Asset.Name}Init`, ...args);
if (Update) {
if (Lock.Asset.RemoveTimer > 0) TimerInventoryRemoveSet(C, Item.Asset.Group.Name, Lock.Asset.RemoveTimer);
CharacterRefresh(C, true, false);
}
}
/**
* Unlocks an item and removes all related properties
* @param {Character} C - The character on which the item must be unlocked
* @param {Item|AssetGroupItemName} Item - The item from appearance to unlock
* @param RefreshDialog — Refreshes the character dialog
*/
function InventoryUnlock(C, Item, refreshDialog=true) {
if (typeof Item === 'string') Item = InventoryGet(C, Item);
if (Item?.Property?.Effect && ValidationDeleteLock(Item.Property, false)) {
CharacterRefresh(C, true, refreshDialog);
}
}
/**
* Applies a random lock on an item
* @param {Character} C - The character on which the item must be locked
* @param {Item} Item - The item from appearance to lock
* @param {Boolean} FromOwner - Set to TRUE if the source is the owner, to apply owner locks
*/
function InventoryLockRandom(C, Item, FromOwner) {
if (InventoryDoesItemAllowLock(Item)) {
var List = [];
for (let A = 0; A < Asset.length; A++)
if (Asset[A].IsLock && Asset[A].Random && !Asset[A].LoverOnly && !Asset[A].FamilyOnly && (FromOwner || !Asset[A].OwnerOnly))
List.push(Asset[A]);
if (List.length > 0) {
var Lock = { Asset: InventoryGetRandom(C, null, List) };
InventoryLock(C, Item, Lock);
}
}
}
/**
* Applies random locks on each character items that can be locked
* @param {Character} C - The character on which the items must be locked
* @param {Boolean} FromOwner - Set to TRUE if the source is the owner, to apply owner locks
*/
function InventoryFullLockRandom(C, FromOwner) {
for (let I = 0; I < C.Appearance.length; I++)
if (InventoryGetLock(C.Appearance[I]) == null)
InventoryLockRandom(C, C.Appearance[I], FromOwner);
}
/**
* Applies a specific lock on each character items that can be locked
* @param {Character} C - The character on which the items must be locked
* @param {AssetLockType} LockType - The lock type to apply
*/
function InventoryFullLock(C, LockType) {
if ((C != null) && (LockType != null))
for (let I = 0; I < C.Appearance.length; I++)
if (InventoryDoesItemAllowLock(C.Appearance[I]))
InventoryLock(C, C.Appearance[I], LockType, null);
}
/**
* Removes all common keys from the player inventory
*/
function InventoryConfiscateKey() {
InventoryDelete(Player, "MetalCuffsKey", "ItemMisc");
InventoryDelete(Player, "MetalPadlockKey", "ItemMisc");
InventoryDelete(Player, "IntricatePadlockKey", "ItemMisc");
InventoryDelete(Player, "HighSecurityPadlockKey", "ItemMisc");
InventoryDelete(Player, "Lockpicks", "ItemMisc");
}
/**
* Removes the remotes of the vibrators from the player inventory
*/
function InventoryConfiscateRemote() {
InventoryDelete(Player, "VibratorRemote", "ItemVulva");
InventoryDelete(Player, "VibratorRemote", "ItemNipples");
InventoryDelete(Player, "LoversVibratorRemote", "ItemVulva");
InventoryDelete(Player, "VibeRemote", "ItemHandheld");
}
/**
* Returns TRUE if the item is worn by the character
* @param {Character} C - The character to scan
* @param {String} AssetName - The asset / item name to scan
* @param {AssetGroupName} AssetGroup - The asset group name to scan
* @returns {Boolean} - TRUE if item is worn
*/
function InventoryIsWorn(C, AssetName, AssetGroup) {
if ((C != null) && (C.Appearance != null) && Array.isArray(C.Appearance))
for (let A = 0; A < C.Appearance.length; A++)
if ((C.Appearance[A].Asset.Name == AssetName) && (C.Appearance[A].Asset.Group.Name == AssetGroup))
return true;
return false;
}
/**
* Set the item's permission to a specific value for the player.
* @param {AssetGroupName} groupName
* @param {string} assetName
* @param {ItemPermissionMode} permissionType
* @param {null | string} type - The relevant extended item option identifier of the item (if any)
* @param {boolean} push - Whether to push the permission changes to the server
* @returns {void} - Nothing
*/
function InventorySetPermission(groupName, assetName, permissionType, type=null, push=false) {
let update = false;
const itemPermissions = Player.PermissionItems[`${groupName}/${assetName}`] ??= PreferencePermissionGetDefault();
if (type == null) {
update = itemPermissions.Permission !== permissionType;
itemPermissions.Permission = permissionType;
} else {
update = itemPermissions.TypePermissions[type] !== permissionType;
itemPermissions.TypePermissions[type] = permissionType;
}
if (push && update) {
ServerPlayerBlockItemsSync();
}
}
/**
* Toggles an item's permission for the player
* @param {Item} Item - Appearance item to toggle
* @param {string} [Type] - The relevant extended item option identifier of the item (if any)
* @param {boolean} [Worn] - True if the player is changing permissions for an item they're wearing or if it's the first option
* @param {boolean} push - Whether to push the permission changes to the server
* @returns {ItemPermissionMode} - The new item permission
*/
function InventoryTogglePermission(Item, Type=null, Worn=false, push=true) {
const onExtreme = Player.GetDifficulty() >= 3;
const blockAllowed = !Worn && !onExtreme;
const limitedAllowed = !Worn && (!onExtreme || MainHallStrongLocks.includes(/** @type {AssetLockType} */(Item.Asset.Name)));
const assetName = Item.Asset.Name;
const groupName = Item.Asset.Group.Name;
const key = /** @type {const} */(`${groupName}/${assetName}`);
const permission = (Type ? Player.PermissionItems[key]?.TypePermissions[Type] : Player.PermissionItems[key]?.Permission) ?? "Default";
switch (permission) {
case "Default":
InventorySetPermission(groupName, assetName, "Favorite", Type, push);
return "Favorite";
case "Favorite":
if (blockAllowed) {
InventorySetPermission(groupName, assetName, "Block", Type, push);
return "Block";
} else if (limitedAllowed) {
InventorySetPermission(groupName, assetName, "Limited", Type, push);
return "Limited";
} else {
InventorySetPermission(groupName, assetName, "Default", Type, push);
return "Default";
}
case "Block":
if (limitedAllowed) {
InventorySetPermission(groupName, assetName, "Limited", Type, push);
return "Limited";
} else {
InventorySetPermission(groupName, assetName, "Default", Type, push);
return "Default";
}
case "Limited":
InventorySetPermission(groupName, assetName, "Default", Type, push);
return "Default";
}
}
/**
* Returns TRUE if a specific item / asset is blocked by the character item permissions
* @param {Character} C - The character on which we check the permissions
* @param {string} AssetName - The asset / item name to scan
* @param {AssetGroupName} AssetGroup - The asset group name to scan
* @param {string} [AssetType] - The asset type to scan
* @returns {boolean} - TRUE if asset / item is blocked
*/
function InventoryIsPermissionBlocked(C, AssetName, AssetGroup, AssetType) {
if (!AssetType) {
return C.PermissionItems[`${AssetGroup}/${AssetName}`]?.Permission === "Block";
} else {
return C.PermissionItems[`${AssetGroup}/${AssetName}`]?.TypePermissions?.[AssetType] === "Block";
}
}
/**
* Returns TRUE if a specific item / asset is favorited by the character item permissions
* @param {Character} C - The character on which we check the permissions
* @param {string} AssetName - The asset / item name to scan
* @param {AssetGroupName} AssetGroup - The asset group name to scan
* @param {string} [AssetType] - The asset type to scan
* @returns {boolean} - TRUE if asset / item is a favorite
*/
function InventoryIsFavorite(C, AssetName, AssetGroup, AssetType) {
if (!AssetType) {
return C.PermissionItems[`${AssetGroup}/${AssetName}`]?.Permission === "Favorite";
} else {
return C.PermissionItems[`${AssetGroup}/${AssetName}`]?.TypePermissions?.[AssetType] === "Favorite";
}
}
/**
* Returns TRUE if a specific item / asset is limited by the character item permissions
* @param {Character} C - The character on which we check the permissions
* @param {string} AssetName - The asset / item name to scan
* @param {AssetGroupName} AssetGroup - The asset group name to scan
* @param {string} [AssetType] - The asset type to scan
* @returns {boolean} - TRUE if asset / item is limited
*/
function InventoryIsPermissionLimited(C, AssetName, AssetGroup, AssetType) {
if (!AssetType) {
return C.PermissionItems[`${AssetGroup}/${AssetName}`]?.Permission === "Limited";
} else {
return C.PermissionItems[`${AssetGroup}/${AssetName}`]?.TypePermissions?.[AssetType] === "Limited";
}
}
/**
* Returns TRUE if the item is not limited, if the player is an owner or a lover of the character, or on their whitelist
* @param {Character} C - The character on which we check the limited permissions for the item
* @param {Item} Item - The item being interacted with
* @param {String} [ItemType] - The asset type to scan
* @returns {Boolean} - TRUE if item is allowed
*/
function InventoryCheckLimitedPermission(C, Item, ItemType) {
if (!InventoryIsPermissionLimited(C, Item.Asset.DynamicName(Player), Item.Asset.Group.Name, ItemType)) return true;
if ((C.IsPlayer()) || C.IsLoverOfPlayer() || C.IsOwnedByPlayer()) return true;
if ((C.ItemPermission < 3) && C.HasOnWhitelist(Player)) return true;
return false;
}
/**
* Returns TRUE if a specific item / asset is blocked or limited for the player by the character item permissions
* @param {Character} C - The character on which we check the permissions
* @param {Item} Item - The item being interacted with
* @param {string | null} [ItemType] - The asset type to scan
* @returns {Boolean} - Returns TRUE if the item cannot be used
*/
function InventoryBlockedOrLimited(C, Item, ItemType) {
let Blocked = InventoryIsPermissionBlocked(C, Item.Asset.DynamicName(Player), Item.Asset.Group.Name, ItemType);
let Limited = !InventoryCheckLimitedPermission(C, Item, ItemType);
return Blocked || Limited;
}
/**
* Determines whether a given item is an allowed limited item for the player (i.e. has limited permissions, but can be
* used by the player)
* @param {Character} C - The character whose permissions to check
* @param {Item} item - The item to check
* @param {string | undefined} [type] - the item type to check
* @returns {boolean} - Returns TRUE if the given item & type is limited but allowed for the player
*/
function InventoryIsAllowedLimited(C, item, type) {
return !InventoryBlockedOrLimited(C, item, type) &&
InventoryIsPermissionLimited(C, item.Asset.Name, item.Asset.Group.Name, type);
}
/**
* Returns TRUE if the item is a key, having the effect of unlocking other items
* @param {Item} Item - The item to validate
* @returns {Boolean} - TRUE if item is a key
*/
function InventoryIsKey(Item) {
if ((Item == null) || (Item.Asset == null) || (Item.Asset.Effect == null)) return false;
for (let E = 0; E < Item.Asset.Effect.length; E++)
if (Item.Asset.Effect[E].substr(0, 7) == "Unlock")
return true;
return false;
}
/**
* Serialises the provided character's inventory into a string for easy comparisons, inventory items are uniquely
* identified by their name and group
* @param {PlayerCharacter} C - The character whose inventory we should serialise
* @return {string} - A simple string representation of the character's inventory
*/
function InventoryStringify(C) {
if (!C || !Array.isArray(C.Inventory)) return "";
return C.Inventory.map(({ Name, Group }) => Group + Name ).join();
}
/**
* Returns TRUE if the inventory category is blocked in the current chat room
* @param {readonly AssetCategory[]} Category - An array of string containing all the categories to validate
* @return {boolean} - TRUE if it's blocked
*/
function InventoryChatRoomAllow(Category) {
if ((CurrentScreen == "ChatRoom") && (Category != null) && (Category.length > 0) && (ChatRoomData != null) && (ChatRoomData.BlockCategory != null) && (ChatRoomData.BlockCategory.length > 0))
for (let C = 0; C < Category.length; C++)
if (ChatRoomData.BlockCategory.indexOf(Category[C]) >= 0)
return false;
return true;
}
/**
* Applies a preset expression from being shocked to the character if able
* @param {Character} C - The character to update
* @returns {void} - Nothing
*/
function InventoryShockExpression(C) {
/** @type {ExpressionTrigger[]} */
const expressions = [
{ Group: "Eyebrows", Name: "Soft", Timer: 10 },
{ Group: "Blush", Name: "Medium", Timer: 15 },
{ Group: "Eyes", Name: "Closed", Timer: 5 },
];
InventoryExpressionTriggerApply(C, expressions);
}
/**
* Extracts all lock-related properties from an item's property object
* @param {ItemProperties} property - The property object to extract from
* @returns {ItemProperties} - A property object containing only the lock-related properties from the provided property
* object
*/
function InventoryExtractLockProperties(property) {
/** @type {ItemProperties} */
const lockProperties = {};
for (const key of Object.keys(property)) {
if (ValidationAllLockProperties.includes(key)) {
lockProperties[key] = CommonCloneDeep(property[key]);
}
}
return lockProperties;
}