mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2025-04-25 17:59:34 +00:00
Move all the access list checks into a single function See merge request BondageProjects/Bondage-College!5501
1803 lines
76 KiB
JavaScript
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;
|
|
}
|