mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2025-04-23 00:39:19 +00:00
862 lines
37 KiB
JavaScript
862 lines
37 KiB
JavaScript
"use strict";
|
|
|
|
/**
|
|
* ModularItem.js
|
|
* --------------
|
|
* This file contains utilities related to modular extended items (for example the High Security Straitjacket). It is
|
|
* generally not necessary to call functions in this file directly - these are called from Asset.js when an item is
|
|
* first registered.
|
|
*
|
|
* A modular item is a typed item, but each type may be comprised of several independent "modules". For example, the
|
|
* High Security Straitjacket has 3 different modules: crotch panel (c), arms (a), and crotch straps (s), and each
|
|
* module can be configured independently. The resulting type then uses an abbreviated format which represents the
|
|
* module values comprising that type. Each module contains a number of options that may be chosen for that module.
|
|
*
|
|
* For example "c0a1s2" - represents the type where the crotch panel module uses option 0, the arms module uses option
|
|
* 1, and the crotch straps module uses option 2. The properties of the type will be derived from a combination of the
|
|
* properties of each of the type's module options. For example, difficulty will be calculated by summing up the
|
|
* difficulties for each of its module options.
|
|
*
|
|
* All dialogue for modular items should be added to `Dialog_Player.csv`. To implement a modular item, you need the
|
|
* following dialogue entries:
|
|
* * "<GroupName><AssetName>SelectBase" - This is the text that will be displayed on the module selection screen (e.g.
|
|
* `ItemArmsHighSecurityStraitJacketSelectBase` - "Configure Straitjacket")
|
|
* * For each module:
|
|
* * "<GroupName><AssetName>Select<ModuleName>" - This is the text that will be displayed on the module's subscreen
|
|
* (e.g. `ItemArmsHighSecurityStraitJacketSelectCrotch` - "Configure crotch panel")
|
|
* * "<GroupName><AssetName>Module<ModuleName>" - This is the text that will be used to describe the module (under
|
|
* the module's button) in the module selection screen (e.g. `ItemArmsHighSecurityStraitJacketModuleCrotch` -
|
|
* "Crotch Panel")
|
|
* * For each option:
|
|
* * "<GroupName><AssetName>Option<ModuleKey><OptionNumber>" - This is the text that will be used to describe the
|
|
* option (under the option's button) in the module subscreen for the module containing that option (e.g.
|
|
* `ItemArmsHighSecurityStraitJacketOptionc0` - "No crotch panel")
|
|
* * If the item's chat setting is configured to `PER_MODULE`, you will need a chatroom message for each module,
|
|
* which will be sent when that module changes. It should have the format "<GroupName><AssetName>Set<ModuleName>"
|
|
* (e.g. `ItemArmsHighSecurityStraitJacketSetCrotch` - "SourceCharacter changed the crotch panel on
|
|
* DestinationCharacter straitjacket")
|
|
* * If the item's chat setting is configured to `PER_OPTION`, you will need a chatroom message for each option, which
|
|
* will be sent when that option is selected. It should have the format
|
|
* "<GroupName><AssetName>Set<ModuleKey><OptionNumber>" (e.g. `ItemArmsHighSecurityStraitJacketSetc0` -
|
|
* "SourceCharacter removes the crotch panel from DestinationCharacter straitjacket")
|
|
*/
|
|
|
|
/**
|
|
* The keyword used for the base menu on modular items
|
|
* @const {string}
|
|
*/
|
|
const ModularItemBase = "Base";
|
|
|
|
/**
|
|
* A lookup for the modular item configurations for each registered modular item
|
|
* @const
|
|
* @type {Record<string, ModularItemData>}
|
|
* @see {@link ModularItemData}
|
|
*/
|
|
const ModularItemDataLookup = {};
|
|
|
|
/**
|
|
* An enum encapsulating the possible chatroom message settings for modular items
|
|
* - PER_MODULE - The item has one chatroom message per module (messages for individual options within a module are all
|
|
* the same)
|
|
* - PER_OPTION - The item has one chatroom message per option (for finer granularity - each individual option within a
|
|
* module can have its own chatroom message)
|
|
* @type {Record<"PER_MODULE"|"PER_OPTION", ModularItemChatSetting>}
|
|
*/
|
|
const ModularItemChatSetting = {
|
|
PER_OPTION: "default",
|
|
PER_MODULE: "perModule",
|
|
};
|
|
|
|
/**
|
|
* Registers a modular extended item. This automatically creates the item's load, draw and click functions.
|
|
* @param {Asset} asset - The asset being registered
|
|
* @param {ModularItemConfig} config - The item's modular item configuration
|
|
* @returns {ModularItemData} - The generated extended item data for the asset
|
|
*/
|
|
function ModularItemRegister(asset, config) {
|
|
const data = ModularItemCreateModularData(asset, config);
|
|
|
|
/** @type {ExtendedItemCallbackStruct<ModularItemOption>} */
|
|
const defaultCallbacks = {
|
|
load: () => ModularItemLoad(data),
|
|
click: () => ModularItemClick(data),
|
|
draw: () => ModularItemDraw(data),
|
|
validate: (...args) => ExtendedItemValidate(data, ...args),
|
|
publishAction: (...args) => ModularItemPublishAction(data, ...args),
|
|
init: (...args) => ModularItemInit(data, ...args),
|
|
setOption: (...args) => ExtendedItemSetOption(data, ...args),
|
|
};
|
|
ExtendedItemCreateCallbacks(data, defaultCallbacks);
|
|
ModularItemGenerateValidationProperties(data);
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Initialize the modular item properties
|
|
* @param {ModularItemData} Data - The item's extended item data
|
|
* @param {Item} Item - The item in question
|
|
* @param {Character} C - The character that has the item equiped
|
|
* @param {boolean} Push - Whether to push to changes to the server
|
|
* @param {boolean} Refresh - Whether to refresh the character. This should generally be `true`, with custom script hooks being a potential exception.
|
|
* @returns {boolean} Whether properties were initialized or not
|
|
*/
|
|
function ModularItemInit(Data, C, Item, Push=true, Refresh=true) {
|
|
if (!CommonIsObject(Item.Property)) {
|
|
Item.Property = { TypeRecord: {} };
|
|
} else if (!CommonIsObject(Item.Property.TypeRecord)) {
|
|
if (typeof Item.Property.Type === "string") {
|
|
Item.Property.TypeRecord = ExtendedItemTypeToRecord(Data.asset, Item.Property.Type);
|
|
} else {
|
|
Item.Property.TypeRecord = {};
|
|
}
|
|
}
|
|
|
|
validateType: if (
|
|
Data.modules.every(m => m.Options[Item.Property.TypeRecord[m.Key]] !== undefined)
|
|
&& !Data.modules.some(m => InventoryIsPermissionBlocked(C, Item.Asset.Name, Item.Asset.Group.Name, `${m.Key}${Item.Property.TypeRecord[m.Key]}`))
|
|
) {
|
|
if (!C.IsNpc && (!C.OnlineSharedSettings || C.OnlineSharedSettings.GameVersion !== GameVersion)) {
|
|
// We cannot reliably validate properties of people in different versions
|
|
break validateType;
|
|
}
|
|
|
|
// Check if all the expected properties are present; extra properties are ignored
|
|
const currentModuleValues = ModularItemParseCurrent(Data, Item.Property.TypeRecord);
|
|
const newProps = ModularItemMergeModuleValues(Data, currentModuleValues);
|
|
delete newProps.OverridePriority;
|
|
const baseLineProps = Object.entries(CommonCloneDeep(Data.baselineProperty || {})).filter(([k, v]) => {
|
|
const existingValue = Item.Property[k];
|
|
return existingValue == null && typeof existingValue !== typeof v;
|
|
});
|
|
|
|
// Make sure that the `Lock` effect persists if the `Effect` array is reset
|
|
const hasLock = Item.Property.Effect?.includes("Lock") && Item.Asset.AllowLock;
|
|
let update = false;
|
|
if (!CommonDeepIsSubset(newProps, Item.Property)) {
|
|
Item.Property = Object.assign(Item.Property, newProps);
|
|
update = true;
|
|
}
|
|
|
|
if (baseLineProps.length > 0) {
|
|
baseLineProps.forEach(([k, v]) => Item.Property[k] = v);
|
|
update = true;
|
|
}
|
|
|
|
if (!update) {
|
|
return false;
|
|
} else if (hasLock) {
|
|
Item.Property.Effect = CommonArrayConcatDedupe(Item.Property.Effect ?? [], ["Lock"]);
|
|
}
|
|
} else {
|
|
const currentModuleValues = ModularItemParseCurrent(Data, null);
|
|
Item.Property = ModularItemMergeModuleValues(Data, currentModuleValues, Data.baselineProperty);
|
|
}
|
|
|
|
if (Refresh) {
|
|
CharacterRefresh(C, Push, false);
|
|
}
|
|
if (Push) {
|
|
ChatRoomCharacterItemUpdate(C, Data.asset.Group.Name);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {ModularItemData} data
|
|
*/
|
|
function ModularItemLoad(data) {
|
|
DialogExtendedMessage = AssetTextGet(`${data.dialogPrefix.header}${data.currentModule}`);
|
|
}
|
|
|
|
/**
|
|
* @param {ModularItemData} data
|
|
*/
|
|
function ModularItemClick(data) {
|
|
const currentModule = data.currentModule || ModularItemBase;
|
|
return data.clickFunctions[currentModule]();
|
|
}
|
|
|
|
/**
|
|
* @param {ModularItemData} data
|
|
*/
|
|
function ModularItemDraw(data) {
|
|
const currentModule = data.currentModule || ModularItemBase;
|
|
return data.drawFunctions[currentModule]();
|
|
}
|
|
|
|
/**
|
|
* Parse the and pre-process the passed modules (and their options)
|
|
* @param {Asset} asset - The asset in question
|
|
* @param {readonly ModularItemModuleConfig[]} modules - An object describing a single module for a modular item.
|
|
* @param {boolean | undefined} [changeWhenLocked] - See {@link ModularItemConfig.ChangeWhenLocked}
|
|
* @returns {ModularItemModule[]} - The updated modules and options
|
|
*/
|
|
function ModularItemBuildModules(asset, modules, changeWhenLocked) {
|
|
return modules.map(protoMod => {
|
|
const drawImages = typeof protoMod.DrawImages === "boolean" ? protoMod.DrawImages : true;
|
|
|
|
/** @type {ModularItemModule} */
|
|
return {
|
|
...protoMod,
|
|
drawData: null, // Initialized later on in ModularItemCreateModularData
|
|
OptionType: "ModularItemModule",
|
|
DrawImages: drawImages,
|
|
Options: protoMod.Options.map((_protoOption, i) => {
|
|
const protoOption = ExtendedItemParseOptions(_protoOption, asset);
|
|
|
|
/** @type {ModularItemOption} */
|
|
const option = {
|
|
...CommonOmit(protoOption, ["ArchetypeConfig"]),
|
|
Name: `${protoMod.Key}${i}`,
|
|
OptionType: "ModularItemOption",
|
|
ModuleName: protoMod.Name,
|
|
Index: i,
|
|
ParentData: null, // Initialized later on in `ModularItemCreateModularData`,
|
|
Property: {
|
|
...(protoOption.Property || {}),
|
|
TypeRecord: { [protoMod.Key]: i },
|
|
}
|
|
};
|
|
|
|
if (typeof changeWhenLocked === "boolean" && typeof option.ChangeWhenLocked !== "boolean") {
|
|
option.ChangeWhenLocked = changeWhenLocked;
|
|
}
|
|
|
|
if (protoOption.ArchetypeConfig) {
|
|
option.ArchetypeData = AssetBuildExtended(asset, protoOption.ArchetypeConfig, AssetFemale3DCGExtended, option);
|
|
}
|
|
return option;
|
|
}),
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generates an asset's modular item data
|
|
* @param {Asset} asset - The asset to generate modular item data for
|
|
* @param {ModularItemConfig} config - The item's extended item configuration
|
|
* @param {null | ExtendedItemOption} parentOption - The parent extended item option of the super screens (if any)
|
|
* @returns {ModularItemData} - The generated modular item data for the asset
|
|
*/
|
|
function ModularItemCreateModularData(asset, {
|
|
Modules,
|
|
ChatSetting,
|
|
ChatTags,
|
|
ChangeWhenLocked,
|
|
DialogPrefix,
|
|
ScriptHooks,
|
|
Dictionary,
|
|
DrawData,
|
|
BaselineProperty,
|
|
AllowEffect,
|
|
DrawImages,
|
|
Name,
|
|
}, parentOption=null) {
|
|
// Set the name of all modular item options
|
|
// Use an external function as typescript does not like the inplace updating of an object's type
|
|
const ModulesParsed = ModularItemBuildModules(asset, Modules, ChangeWhenLocked);
|
|
// Only enable DrawImages in the base screen if all module-specific DrawImages are true
|
|
const BaseDrawImages = (typeof DrawImages !== "boolean") ? ModulesParsed.every((m) => m.DrawImages) : DrawImages;
|
|
|
|
const key = `${asset.Group.Name}${asset.Name}`;
|
|
const name = Name != null ? Name : (parentOption == null ? ExtendedArchetype.MODULAR : parentOption.Name);
|
|
DialogPrefix = DialogPrefix || {};
|
|
/** @type {ModularItemData} */
|
|
const data = ModularItemDataLookup[key] = {
|
|
archetype: ExtendedArchetype.MODULAR,
|
|
asset,
|
|
chatSetting: ChatSetting || ModularItemChatSetting.PER_OPTION,
|
|
key,
|
|
name,
|
|
typeCount: 1,
|
|
functionPrefix: `Inventory${key}`,
|
|
dynamicAssetsFunctionPrefix: `Assets${key}`,
|
|
dialogPrefix: {
|
|
header: DialogPrefix.Header || `${key}Select`,
|
|
module: DialogPrefix.Module || `${key}Module`,
|
|
option: DialogPrefix.Option || `${key}Option`,
|
|
chat: DialogPrefix.Chat || `${key}Set`,
|
|
},
|
|
chatTags: Array.isArray(ChatTags) ? ChatTags : [
|
|
CommonChatTags.SOURCE_CHAR,
|
|
CommonChatTags.DEST_CHAR,
|
|
],
|
|
modules: ModulesParsed,
|
|
currentModule: ModularItemBase,
|
|
pages: { [ModularItemBase]: 0 },
|
|
drawData: TypedItemGetDrawData(asset, DrawData, ModulesParsed, { drawImage: BaseDrawImages, imagePath: null }),
|
|
scriptHooks: ExtendedItemParseScriptHooks(ScriptHooks || {}),
|
|
drawFunctions: {},
|
|
clickFunctions: {},
|
|
baselineProperty: typeof BaselineProperty === "object" ? BaselineProperty : null,
|
|
dictionary: Array.isArray(Dictionary) ? Dictionary : [],
|
|
parentOption: null,
|
|
allowEffect: Array.isArray(AllowEffect) ? AllowEffect : [],
|
|
};
|
|
|
|
// Populate the modules drawdata
|
|
let i = -1;
|
|
for (const mod of ModulesParsed) {
|
|
i += 1;
|
|
/** @type {Partial<ElementData<ElementMetaData.Modular>>} */
|
|
const buttonDataOverride = CommonOmit(data.drawData.elementData[i], ["position", "imagePath", "drawImage"]);
|
|
if (typeof Modules[i].DrawImages === "boolean") {
|
|
buttonDataOverride.drawImage = Modules[i].DrawImages;
|
|
} else if (typeof DrawImages === "boolean") {
|
|
buttonDataOverride.drawImage = DrawImages;
|
|
} else {
|
|
buttonDataOverride.drawImage = true;
|
|
}
|
|
mod.drawData = TypedItemGetDrawData(asset, Modules[i].DrawData, mod.Options, buttonDataOverride);
|
|
for (const option of mod.Options) {
|
|
option.ParentData = data;
|
|
}
|
|
}
|
|
|
|
data.drawFunctions[ModularItemBase] = ModularItemCreateDrawBaseFunction(data);
|
|
data.clickFunctions[ModularItemBase] = ModularItemCreateClickBaseFunction(data);
|
|
for (const module of ModulesParsed) {
|
|
data.pages[module.Name] = 0;
|
|
data.drawFunctions[module.Name] = () => ModularItemDrawModule(module, data);
|
|
data.clickFunctions[module.Name] = () => ModularItemClickModule(module, data);
|
|
data.typeCount *= module.Options.length;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Creates a modular item's base draw function (for the module selection screen)
|
|
* @param {ModularItemData} data - The modular item data for the asset
|
|
* @returns {() => void} - The modular item's base draw function
|
|
*/
|
|
function ModularItemCreateDrawBaseFunction(data) {
|
|
return () => {
|
|
/** @type {ModularItemButtonDefinition[]} */
|
|
const buttonDefinitions = data.modules
|
|
.map((module, i) => {
|
|
const currentValues = ModularItemParseCurrent(data, DialogFocusItem.Property.TypeRecord);
|
|
const currentOption = module.Options[currentValues[i]];
|
|
return [module, currentOption, data.dialogPrefix.module];
|
|
});
|
|
return ModularItemDrawCommon(ModularItemBase, buttonDefinitions, data, data.drawData);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Maps a modular item option to a button definition for rendering the option's button.
|
|
* @param {ModularItemOption} option - The option to draw a button for
|
|
* @param {ModularItemModule} module - A reference to the option's parent module
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @param {number} currentOptionIndex - The currently selected option index for the module
|
|
* @returns {ModularItemButtonDefinition} - A button definition array representing the provided option
|
|
*/
|
|
function ModularItemMapOptionToButtonDefinition(option, module, { dialogPrefix }, currentOptionIndex) {
|
|
const currentOption = module.Options[currentOptionIndex];
|
|
return [option, currentOption, dialogPrefix.option];
|
|
}
|
|
|
|
/**
|
|
* Draws a module screen from the provided button definitions and modular item data.
|
|
* @param {string} moduleName - The name of the module whose page is being drawn
|
|
* @param {readonly ModularItemButtonDefinition[]} buttonDefinitions - A list of button definitions to draw
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @param {ExtendedItemDrawData<ElementMetaData.Modular>} drawData
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ModularItemDrawCommon(
|
|
moduleName,
|
|
buttonDefinitions,
|
|
data,
|
|
{ paginate, pageCount, elementData, itemsPerPage },
|
|
) {
|
|
if (ExtendedItemSubscreen) {
|
|
CommonCallFunctionByNameWarn(ExtendedItemFunctionPrefix() + ExtendedItemSubscreen + "Draw");
|
|
return;
|
|
}
|
|
|
|
ExtendedItemDrawHeader();
|
|
DrawText(DialogExtendedMessage, 1500, 375, "#fff", "808080");
|
|
|
|
// Permission mode toggle
|
|
DrawButton(
|
|
1775, 25, 90, 90, "", "White",
|
|
ExtendedItemPermissionMode ? "Icons/DialogNormalMode.png" : "Icons/DialogPermissionMode.png",
|
|
InterfaceTextGet(ExtendedItemPermissionMode ? "DialogMenuNormalMode" : "DialogMenuPermissionMode"),
|
|
);
|
|
|
|
const pageNumber = Math.min(pageCount - 1, data.pages[moduleName] || 0);
|
|
const pageStart = pageNumber * itemsPerPage;
|
|
const page = buttonDefinitions.slice(pageStart, pageStart + itemsPerPage);
|
|
|
|
for (const [i, [option, currentOption, prefix]] of page.entries()) {
|
|
ExtendedItemDrawButton(option, currentOption, prefix, elementData[i + pageStart]);
|
|
}
|
|
|
|
if (paginate) {
|
|
DrawButton(1665, 240, 90, 90, "", "White", "Icons/Prev.png");
|
|
DrawButton(1775, 240, 90, 90, "", "White", "Icons/Next.png");
|
|
}
|
|
|
|
// If the assets allows tightening / loosening
|
|
ExtendedItemTighten.Draw(data, DialogFocusItem, [1050, 220, 300, 65]);
|
|
}
|
|
|
|
/**
|
|
* Draws the extended item screen for a given module.
|
|
* @param {ModularItemModule} module - The module whose screen to draw
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ModularItemDrawModule(module, data) {
|
|
const moduleIndex = data.modules.indexOf(module);
|
|
const currentValues = ModularItemParseCurrent(data, DialogFocusItem.Property.TypeRecord);
|
|
const buttonDefinitions = module.Options.map(
|
|
(option) => ModularItemMapOptionToButtonDefinition(option, module, data, currentValues[moduleIndex]));
|
|
ModularItemDrawCommon(module.Name, buttonDefinitions, data, module.drawData);
|
|
}
|
|
|
|
/**
|
|
* Generates a click function for a modular item's module selection screen
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @returns {function(): void} - A click handler for the modular item's module selection screen
|
|
*/
|
|
function ModularItemCreateClickBaseFunction(data) {
|
|
const DrawData = data.drawData;
|
|
return () => {
|
|
const pageNumber = Math.min(DrawData.pageCount - 1, data.pages[ModularItemBase] ?? 0);
|
|
ModularItemClickCommon(
|
|
data,
|
|
DrawData,
|
|
() => {
|
|
DialogLeaveFocusItem();
|
|
},
|
|
i => {
|
|
const pageStart = pageNumber * DrawData.itemsPerPage;
|
|
const page = data.modules.slice(pageStart, pageStart + DrawData.itemsPerPage);
|
|
const module = page[i % DrawData.itemsPerPage];
|
|
if (module) {
|
|
if (CharacterGetCurrent().IsPlayer() && module.AllowSelfSelect === false) {
|
|
DialogExtendedMessage = InterfaceTextGet("CannotSelfSelect");
|
|
}
|
|
ModularItemModuleTransition(module.Name, data);
|
|
return true;
|
|
}
|
|
},
|
|
(delta) => ModularItemChangePage(ModularItemBase, delta, data, DrawData),
|
|
pageNumber,
|
|
);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A generic click handler for a module's screen
|
|
* @param {ModularItemModule} module - The module whose screen we are currently in
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ModularItemClickModule(module, data) {
|
|
const DrawData = module.drawData;
|
|
const pageNumber = Math.min(DrawData.pageCount - 1, data.pages[module.Name] ?? 0);
|
|
ModularItemClickCommon(
|
|
data,
|
|
DrawData,
|
|
() => ModularItemModuleTransition(ModularItemBase, data),
|
|
i => {
|
|
const pageStart = pageNumber * DrawData.itemsPerPage;
|
|
const page = module.Options.slice(pageStart, pageStart + DrawData.itemsPerPage);
|
|
const selected = page[i % DrawData.itemsPerPage];
|
|
if (selected) {
|
|
if (ExtendedItemPermissionMode) {
|
|
const C = CharacterGetCurrent();
|
|
const IsFirst = i === 0;
|
|
const Worn = C.IsPlayer() && DialogFocusItem.Property.TypeRecord[module.Key] === i;
|
|
InventoryTogglePermission(DialogFocusItem, selected.Name, IsFirst || Worn);
|
|
} else {
|
|
ModularItemSetType(module, i, data);
|
|
}
|
|
return true;
|
|
}
|
|
},
|
|
(delta) => ModularItemChangePage(module.Name, delta, data, DrawData),
|
|
pageNumber,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A common click handler for modular item screens. Note that pagination is not currently handled, but will be added
|
|
* in the future.
|
|
* @param {ModularItemData} data
|
|
* @param {ExtendedItemDrawData<ElementMetaData.Modular>} drawData
|
|
* @param {function(): void} exitCallback - A callback to be called when the exit button has been clicked
|
|
* @param {function(number): boolean} itemCallback - A callback to be called when an item has been clicked
|
|
* @param {function(number): void} paginateCallback - A callback to be called when a pagination button has been clicked
|
|
* @param {number} pageNumber - The currently shown page
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ModularItemClickCommon(data, { paginate, elementData, itemsPerPage }, exitCallback, itemCallback, paginateCallback, pageNumber) {
|
|
if (ExtendedItemSubscreen) {
|
|
CommonCallFunctionByNameWarn(ExtendedItemFunctionPrefix() + ExtendedItemSubscreen + "Click");
|
|
return;
|
|
}
|
|
|
|
// Exit button
|
|
if (MouseIn(1885, 25, 90, 90)) {
|
|
exitCallback();
|
|
ExtendedItemPermissionMode = false;
|
|
return;
|
|
} else if (MouseIn(1775, 25, 90, 90)) {
|
|
// Permission toggle button
|
|
if (ExtendedItemPermissionMode && CurrentScreen == "ChatRoom") {
|
|
ChatRoomCharacterUpdate(Player);
|
|
ExtendedItemRequirementCheckMessageMemo.clearCache();
|
|
}
|
|
ExtendedItemPermissionMode = !ExtendedItemPermissionMode;
|
|
return;
|
|
} else if (paginate) {
|
|
if (MouseIn(1665, 240, 90, 90)) return paginateCallback(-1);
|
|
else if (MouseIn(1775, 240, 90, 90)) return paginateCallback(1);
|
|
}
|
|
|
|
let i = pageNumber * itemsPerPage;
|
|
const elementDataSlice = elementData.slice(pageNumber * itemsPerPage, (1 + pageNumber) * itemsPerPage);
|
|
for (const { position, hidden } of elementDataSlice) {
|
|
if (!hidden && MouseIn(...position) && itemCallback(i)) {
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
// If the assets allows tightening / loosening
|
|
if (ExtendedItemTighten.Click(data, DialogFocusItem, [1050, 220, 300, 65])) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles page changing for modules
|
|
* @param {string} moduleName - The name of the module whose page should be modified
|
|
* @param {number} delta - The page delta to apply to the module's current page
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @param {ExtendedItemDrawData<ElementMetaData.Modular>} drawData
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ModularItemChangePage(moduleName, delta, data, { pageCount }) {
|
|
const currentPage = data.pages[moduleName];
|
|
data.pages[moduleName] = (currentPage + pageCount + delta) % pageCount;
|
|
}
|
|
|
|
/**
|
|
* Transitions between pages within a modular item's extended item menu
|
|
* @param {string} newModule - The name of the new module to transition to
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ModularItemModuleTransition(newModule, data) {
|
|
data.currentModule = newModule;
|
|
DialogExtendedMessage = AssetTextGet(data.dialogPrefix.header + newModule);
|
|
}
|
|
|
|
/**
|
|
* Parses the focus item's current type into an array representing the currently selected module options
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @param {null | TypeRecord} typeRecord - The type string for a modular item. If null, use a type string extracted from the selected module options
|
|
* @returns {number[]} - An array of numbers representing the currently selected options for each of the item's modules
|
|
*/
|
|
function ModularItemParseCurrent({ asset, modules }, typeRecord) {
|
|
if (!CommonIsObject(typeRecord)) {
|
|
return Array(modules.length).fill(0);
|
|
}
|
|
return modules.map(module => {
|
|
const index = typeRecord[module.Key];
|
|
if (module.Options[index] === undefined) {
|
|
console.warn(`${asset.Group.Name}:${asset.Name}: invalid key for module "${module.Key}": ${index}`);
|
|
return 0;
|
|
} else {
|
|
return index;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Merges all of the selected module options for a modular item into a single Property object to set on the item
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @param {readonly number[]} moduleValues - The numeric values representing the current options for each module
|
|
* @param {ItemProperties|null} BaselineProperty - Initial properties
|
|
* @returns {ItemProperties} - A property object created from combining each module of the modular item
|
|
*/
|
|
function ModularItemMergeModuleValues({ asset, modules }, moduleValues, BaselineProperty=null) {
|
|
const options = modules.map((module, i) => module.Options[moduleValues[i] || 0]);
|
|
const typeRecord = options.reduce(
|
|
(rec, option) => Object.assign(rec, option.Property.TypeRecord),
|
|
/** @type {TypeRecord} */({}),
|
|
);
|
|
|
|
/** @type {ItemProperties} */
|
|
const BaseLineProperties = {
|
|
TypeRecord: typeRecord,
|
|
Difficulty: 0,
|
|
Block: Array.isArray(asset.Block) ? asset.Block.slice() : [],
|
|
Effect: Array.isArray(asset.Effect) ? asset.Effect.slice() : [],
|
|
Hide: Array.isArray(asset.Hide) ? asset.Hide.slice() : [],
|
|
HideItem: Array.isArray(asset.HideItem) ? asset.HideItem.slice() : [],
|
|
AllowActivity: Array.isArray(asset.AllowActivity) ? asset.AllowActivity.slice() : [],
|
|
Attribute: Array.isArray(asset.Attribute) ? asset.Attribute.slice() : [],
|
|
};
|
|
if (asset.CustomBlindBackground) BaseLineProperties.CustomBlindBackground = asset.CustomBlindBackground;
|
|
if (asset.AllowTint) BaseLineProperties.Tint = CommonIsArray(asset.Tint) ? [...asset.Tint] : [];
|
|
|
|
if (BaselineProperty != null) {
|
|
ModularItemSanitizeProperties(BaselineProperty, BaseLineProperties, asset);
|
|
}
|
|
|
|
return options.reduce((mergedProperty, { Property }) => {
|
|
return ModularItemSanitizeProperties(Property, mergedProperty, asset);
|
|
}, BaseLineProperties);
|
|
}
|
|
|
|
/**
|
|
* Sanitize and merge all modular item properties
|
|
* @param {ItemProperties} Property - The to-be sanitized properties
|
|
* @param {ItemProperties} mergedProperty - The to-be returned object with the newly sanitized properties
|
|
* @param {Asset} Asset - The relevant asset
|
|
* @returns {ItemProperties} - The updated merged properties
|
|
*/
|
|
function ModularItemSanitizeProperties(Property, mergedProperty, Asset) {
|
|
Property = Property || {};
|
|
mergedProperty.Difficulty += (Property.Difficulty || 0);
|
|
if (Property.CustomBlindBackground) mergedProperty.CustomBlindBackground = Property.CustomBlindBackground;
|
|
if (Property.Block) CommonArrayConcatDedupe(mergedProperty.Block, Property.Block);
|
|
if (Property.Effect) CommonArrayConcatDedupe(mergedProperty.Effect, Property.Effect);
|
|
if (Property.Hide) CommonArrayConcatDedupe(mergedProperty.Hide, Property.Hide);
|
|
if (Property.HideItem) CommonArrayConcatDedupe(mergedProperty.HideItem, Property.HideItem);
|
|
if (Property.SetPose) mergedProperty.SetPose = CommonArrayConcatDedupe(mergedProperty.SetPose || [], Property.SetPose);
|
|
if (Property.AllowActivePose) mergedProperty.AllowActivePose = CommonArrayConcatDedupe(mergedProperty.AllowActivePose || [], Property.AllowActivePose);
|
|
if (Property.AllowActivity) CommonArrayConcatDedupe(mergedProperty.AllowActivity, Property.AllowActivity);
|
|
if (Property.Attribute) CommonArrayConcatDedupe(mergedProperty.Attribute, Property.Attribute);
|
|
if (typeof Property.OverridePriority === "number") mergedProperty.OverridePriority = Property.OverridePriority;
|
|
else if (CommonIsObject(Property.OverridePriority)) {
|
|
let valid = true;
|
|
for (const layerName of Object.keys(Property.OverridePriority)) {
|
|
if (!Asset.Layer.find(l => l.Name === layerName)) {
|
|
console.warn(`invalid OverridePriority property: unknown layer name ${layerName}`);
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
if (valid) {
|
|
if (CommonIsObject(mergedProperty.OverridePriority)) {
|
|
Object.assign(mergedProperty.OverridePriority, Property.OverridePriority);
|
|
} else {
|
|
mergedProperty.OverridePriority = Property.OverridePriority;
|
|
}
|
|
}
|
|
}
|
|
if (typeof Property.HeightModifier === "number") mergedProperty.HeightModifier = (mergedProperty.HeightModifier || 0) + Property.HeightModifier;
|
|
if (Property.OverrideHeight) mergedProperty.OverrideHeight = ModularItemMergeOverrideHeight(mergedProperty.OverrideHeight, Property.OverrideHeight);
|
|
if (Asset.AllowTint && Property.Tint) mergedProperty.Tint = CommonArrayConcatDedupe(mergedProperty.Tint, Property.Tint);
|
|
if (typeof Property.Door === "boolean") mergedProperty.Door = Property.Door;
|
|
if (typeof Property.Padding === "boolean") mergedProperty.Padding = Property.Padding;
|
|
if (typeof Property.ShockLevel === "number") mergedProperty.ShockLevel = Property.ShockLevel;
|
|
if (typeof Property.TriggerCount === "number") mergedProperty.TriggerCount = Property.TriggerCount;
|
|
if (typeof Property.OrgasmCount === "number") mergedProperty.OrgasmCount = Property.OrgasmCount;
|
|
if (typeof Property.RuinedOrgasmCount === "number") mergedProperty.RuinedOrgasmCount = Property.RuinedOrgasmCount;
|
|
if (typeof Property.TimeWorn === "number") mergedProperty.TimeWorn = Property.TimeWorn;
|
|
if (typeof Property.TimeSinceLastOrgasm === "number") mergedProperty.TimeSinceLastOrgasm = Property.TimeSinceLastOrgasm;
|
|
if (typeof Property.ShowText === "boolean") mergedProperty.ShowText = Property.ShowText;
|
|
if (typeof Property.InflateLevel === "number") mergedProperty.InflateLevel = Property.InflateLevel;
|
|
if (typeof Property.Intensity === "number") mergedProperty.Intensity = Property.Intensity;
|
|
if (typeof Property.Opacity === "number") mergedProperty.Opacity = Property.Opacity;
|
|
if (typeof Property.AutoPunishUndoTimeSetting === "number") mergedProperty.AutoPunishUndoTimeSetting = Property.AutoPunishUndoTimeSetting;
|
|
if (typeof Property.OriginalSetting === "number") mergedProperty.OriginalSetting = Property.OriginalSetting;
|
|
if (typeof Property.BlinkState === "boolean") mergedProperty.BlinkState = Property.BlinkState;
|
|
if (typeof Property.AutoPunishUndoTime === "number") mergedProperty.AutoPunishUndoTime = Property.AutoPunishUndoTime;
|
|
if (typeof Property.AutoPunish === "number") mergedProperty.AutoPunish = Property.AutoPunish;
|
|
if (typeof Property.Text === "string" && DynamicDrawTextRegex.test(Property.Text)) mergedProperty.Text = Property.Text;
|
|
if (typeof Property.Text2 === "string" && DynamicDrawTextRegex.test(Property.Text2)) mergedProperty.Text2 = Property.Text2;
|
|
if (typeof Property.Text3 === "string" && DynamicDrawTextRegex.test(Property.Text3)) mergedProperty.Text3 = Property.Text3;
|
|
if (typeof Property.PunishOrgasm === "boolean") mergedProperty.PunishOrgasm = Property.PunishOrgasm;
|
|
if (typeof Property.PunishStandup === "boolean") mergedProperty.PunishStandup = Property.PunishStandup;
|
|
if (typeof Property.NextShockTime === "number") mergedProperty.NextShockTime = Property.NextShockTime;
|
|
if (typeof Property.TargetAngle === "number") mergedProperty.TargetAngle = Property.TargetAngle;
|
|
if (typeof Property.PortalLinkCode === "string" && PortalLinkCodeRegex.test(Property.PortalLinkCode)) mergedProperty.PortalLinkCode = Property.PortalLinkCode;
|
|
if (Array.isArray(Property.Texts)) mergedProperty.Texts = Property.Texts;
|
|
return mergedProperty;
|
|
}
|
|
|
|
/**
|
|
* Generates the type string for a modular item from its modules and their current values.
|
|
* @param {AssetOverrideHeight} currentValue - The OverrideHeight for the future item
|
|
* @param {AssetOverrideHeight} newValue - The OverrideHeight being merged
|
|
* @returns {AssetOverrideHeight | undefined} - A string type generated from the selected option values for each module
|
|
*/
|
|
function ModularItemMergeOverrideHeight(currentValue, newValue) {
|
|
if (typeof newValue.Height === "number" && typeof newValue.Priority === "number" &&
|
|
(!currentValue || (currentValue.Priority < currentValue.Priority)))
|
|
return {Height: newValue.Height, Priority: newValue.Priority};
|
|
return currentValue;
|
|
}
|
|
|
|
/**
|
|
* Sets a modular item's type based on a change in a module's option selection.
|
|
* @param {ModularItemModule} module - The module that changed
|
|
* @param {number} index - The index of the newly chosen option within the module
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ModularItemSetType(module, index, data) {
|
|
const C = CharacterGetCurrent();
|
|
const newOption = module.Options[index];
|
|
const currentModuleValues = ModularItemParseCurrent(data, DialogFocusItem.Property.TypeRecord);
|
|
const moduleIndex = data.modules.indexOf(module);
|
|
const previousOption = module.Options[currentModuleValues[moduleIndex]];
|
|
|
|
const requirementMessage = ExtendedItemRequirementCheckMessage(data, C, DialogFocusItem, newOption, previousOption, true);
|
|
if (requirementMessage) {
|
|
DialogExtendedMessage = requirementMessage;
|
|
return;
|
|
}
|
|
|
|
// Do not sync appearance while in the wardrobe
|
|
const IsCloth = DialogFocusItem.Asset.Group.Clothing;
|
|
/** @type {Parameters<ExtendedItemCallbacks.SetOption<ModularItemOption>>} */
|
|
const optionArgs = [C, DialogFocusItem, newOption, previousOption, !IsCloth, true];
|
|
CommonCallFunctionByNameWarn(`${data.functionPrefix}SetOption`, ...optionArgs);
|
|
|
|
if (!IsCloth) {
|
|
const groupName = data.asset.Group.Name;
|
|
CharacterRefresh(C, true, false);
|
|
ChatRoomCharacterItemUpdate(C, groupName);
|
|
|
|
if (ServerPlayerIsInChatRoom()) {
|
|
/** @type {Parameters<ExtendedItemCallbacks.PublishAction<ModularItemOption>>} */
|
|
const args = [C, DialogFocusItem, newOption, previousOption];
|
|
CommonCallFunctionByNameWarn(`${data.functionPrefix}PublishAction`, ...args);
|
|
} else if (!C.IsPlayer()) {
|
|
C.CurrentDialog = DialogFind(C, `${data.key}${module.Key}${index}`, groupName);
|
|
}
|
|
}
|
|
|
|
// If the module's option has a subscreen, transition to that screen instead of the main page of the item.
|
|
if (newOption.HasSubscreen) {
|
|
ExtendedItemSubscreen = newOption.Name;
|
|
CommonCallFunctionByName(`${data.functionPrefix}${ExtendedItemSubscreen}Load`);
|
|
} else {
|
|
ModularItemModuleTransition(ModularItemBase, data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Publishes the chatroom message for a modular item when one of its modules has changed.
|
|
* @param {ModularItemData} data
|
|
* @param {Character} C
|
|
* @param {Item} item
|
|
* @param {ModularItemOption} newOption
|
|
* @param {ModularItemOption} previousOption
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ModularItemPublishAction(data, C, item, newOption, previousOption) {
|
|
if (newOption.Name === previousOption.Name) {
|
|
return;
|
|
}
|
|
|
|
const chatData = {
|
|
C,
|
|
newOption,
|
|
previousOption,
|
|
newIndex: newOption.Index,
|
|
previousIndex: previousOption.Index,
|
|
};
|
|
const dictionary = ExtendedItemBuildChatMessageDictionary(chatData, data, item);
|
|
|
|
let msg = (typeof data.dialogPrefix.chat === "function") ? data.dialogPrefix.chat(chatData) : data.dialogPrefix.chat;
|
|
switch (data.chatSetting) {
|
|
case ModularItemChatSetting.PER_OPTION:
|
|
msg += newOption.Name;
|
|
break;
|
|
case ModularItemChatSetting.PER_MODULE:
|
|
msg += newOption.ModuleName;
|
|
break;
|
|
}
|
|
ChatRoomPublishCustomAction(msg, false, dictionary.build());
|
|
}
|
|
|
|
/**
|
|
* Generates and sets the AllowLock and AllowLockType properties for an asset based on its modular item data. For types
|
|
* where two independent options declare conflicting AllowLock properties (i.e. one option declares AllowLock: false and
|
|
* another declares AllowLock: true), the resulting type will permit locking (i.e. true overrides false).
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ModularItemGenerateAllowLockType({ asset, modules }) {
|
|
const allowLockType = asset.AllowLockType || {};
|
|
let allowAll = true;
|
|
for (const module of modules) {
|
|
allowLockType[module.Name] = new Set();
|
|
for (const [i, option] of CommonEnumerate(module.Options)) {
|
|
const allowLock = typeof option.AllowLock === "boolean" ? option.AllowLock : asset.AllowLock;
|
|
if (allowLock) {
|
|
allowLockType[module.Name].add(i);
|
|
} else {
|
|
allowAll = false;
|
|
}
|
|
}
|
|
}
|
|
TypedItemSetAllowLockType(asset, allowLockType, allowAll);
|
|
}
|
|
|
|
/**
|
|
* Generates and assigns a modular asset's AllowType, AllowEffect and AllowBlock properties, along with the AllowTypes
|
|
* properties on the asset layers based on the values set in its module definitions.
|
|
* @param {ModularItemData} data - The modular item's data
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ModularItemGenerateValidationProperties(data) {
|
|
const asset = /** @type {Mutable<Asset>} */(data.asset);
|
|
const { modules } = data;
|
|
asset.Extended = true;
|
|
asset.AllowEffect = CommonIsArray(asset.AllowEffect) ? [...data.allowEffect, ...asset.AllowEffect] : [...data.allowEffect];
|
|
// @ts-ignore: ignore `readonly` while still building the asset
|
|
CommonArrayConcatDedupe(asset.AllowEffect, asset.Effect);
|
|
asset.AllowBlock = CommonIsArray(asset.Block) ? asset.Block.slice() : [];
|
|
asset.AllowHide = CommonIsArray(asset.Hide) ? asset.Hide.slice() : [];
|
|
asset.AllowHideItem = CommonIsArray(asset.HideItem) ? asset.HideItem.slice() : [];
|
|
for (const module of modules) {
|
|
for (const {Property} of module.Options) {
|
|
if (Property) {
|
|
// @ts-ignore: ignore `readonly` while still building the asset
|
|
if (Property.Effect) CommonArrayConcatDedupe(asset.AllowEffect, Property.Effect);
|
|
// @ts-ignore: ignore `readonly` while still building the asset
|
|
if (Property.Block) CommonArrayConcatDedupe(asset.AllowBlock, Property.Block);
|
|
// @ts-ignore: ignore `readonly` while still building the asset
|
|
if (Property.Hide) CommonArrayConcatDedupe(asset.AllowHide, Property.Hide);
|
|
// @ts-ignore: ignore `readonly` while still building the asset
|
|
if (Property.HideItem) CommonArrayConcatDedupe(asset.AllowHideItem, Property.HideItem);
|
|
if (Property.Tint && Array.isArray(Property.Tint) && Property.Tint.length > 0) asset.AllowTint = true;
|
|
}
|
|
}
|
|
}
|
|
ModularItemGenerateAllowLockType(data);
|
|
}
|
|
|
|
/**
|
|
* Hide an HTML element if a given module is not active.
|
|
* @param {ModularItemData} Data - The modular item data
|
|
* @param {string} ID - The id of the element
|
|
* @param {string} Module - The module that must be active
|
|
* @returns {boolean} Whether the module is active or not
|
|
*/
|
|
function ModularItemHideElement(Data, ID, Module) {
|
|
const Element = document.getElementById(ID);
|
|
if (Element == null) {
|
|
return Data.currentModule === Module;
|
|
}
|
|
|
|
if (Data.currentModule === Module) {
|
|
Element.style.display = "block";
|
|
return true;
|
|
} else {
|
|
Element.style.display = "none";
|
|
return false;
|
|
}
|
|
}
|