bondage-college-mirr/BondageClub/Scripts/VibratorMode.js
2024-05-09 16:48:11 +02:00

791 lines
29 KiB
JavaScript

"use strict";
/**
* An enum for the possible vibrator modes
* @readonly
* @type {{OFF: "Off", LOW: "Low", MEDIUM: "Medium", HIGH: "High", MAXIMUM: "Maximum", RANDOM: "Random", ESCALATE: "Escalate", TEASE: "Tease", DENY: "Deny", EDGE: "Edge"}}
*/
var VibratorMode = {
OFF: "Off",
LOW: "Low",
MEDIUM: "Medium",
HIGH: "High",
MAXIMUM: "Maximum",
RANDOM: "Random",
ESCALATE: "Escalate",
TEASE: "Tease",
DENY: "Deny",
EDGE: "Edge",
};
/**
* An enum for the possible vibrator states when a vibrator is in a state machine mode
* @type {{DEFAULT: "Default", DENY: "Deny", ORGASM: "Orgasm", REST: "Rest"}}
*/
var VibratorModeState = {
DEFAULT: "Default",
DENY: "Deny",
ORGASM: "Orgasm",
REST: "Rest",
};
/**
* An enum for the vibrator configuration sets that a vibrator can have
* @type {{STANDARD: "Standard", ADVANCED: "Advanced"}}
*/
var VibratorModeSet = {
STANDARD: "Standard",
ADVANCED: "Advanced",
};
/**
* A record of the various available vibrator sets of vibrator modes
* @type {{
* Standard: VibratingItemOptionConfig[],
* Advanced: VibratingItemOptionConfig[],
* }}
* @constant
*/
var VibratorModeOptions = {
[VibratorModeSet.STANDARD]: [
{
Name: "Off",
Property: {
Mode: VibratorMode.OFF,
Intensity: -1,
Effect: ["Egged"]
},
},
{
Name: "Low",
Property: {
Mode: VibratorMode.LOW,
Intensity: 0,
Effect: ["Egged", "Vibrating"],
},
},
{
Name: "Medium",
Property: {
Mode: VibratorMode.MEDIUM,
Intensity: 1,
Effect: ["Egged", "Vibrating"],
},
},
{
Name: "High",
Property: {
Mode: VibratorMode.HIGH,
Intensity: 2,
Effect: ["Egged", "Vibrating"],
},
},
{
Name: "Maximum",
Property: {
Mode: VibratorMode.MAXIMUM,
Intensity: 3,
Effect: ["Egged", "Vibrating"],
},
},
],
[VibratorModeSet.ADVANCED]: [
{
Name: "Random",
Property: {
Mode: VibratorMode.RANDOM,
Intensity: 0,
Effect: ["Egged"],
},
},
{
Name: "Escalate",
Property: {
Mode: VibratorMode.ESCALATE,
Intensity: 0,
Effect: ["Egged", "Vibrating"],
},
},
{
Name: "Tease",
Property: {
Mode: VibratorMode.TEASE,
Intensity: 0,
Effect: ["Egged"],
},
},
{
Name: "Deny",
Property: {
Mode: VibratorMode.DENY,
Intensity: 0,
Effect: ["Egged", "Edged"],
},
},
{
Name: "Edge",
Property: {
Mode: VibratorMode.EDGE,
Intensity: 0,
Effect: ["Egged", "Vibrating", "Edged"],
},
},
],
};
/**
* An alias for the vibrators OFF mode. See {@link VibratorModeOptions}.
*/
const VibratorModeOff = VibratorModeOptions[VibratorModeSet.STANDARD][0];
/** A list with all advanced vibrator mode-names. */
const VibratorModesAdvanced = VibratorModeOptions[VibratorModeSet.ADVANCED].map(o => o.Property.Mode);
/**
* A lookup for the vibrator configurations for each registered vibrator item
* @const
* @type {Record<string, VibratingItemData>}
*/
const VibratorModeDataLookup = {};
/**
* Registers a vibrator item. This automatically creates the item's load, draw, click and scriptDraw functions.
* @param {Asset} asset - The asset being registered
* @param {VibratingItemConfig} config - The item's vibrator item configuration
* @param {null | ExtendedItemOption} parentOption - The extended item option of the super screen that this archetype was initialized from (if any)
* @returns {VibratingItemData} - The generated extended item data for the asset
*/
function VibratorModeRegister(asset, config, parentOption=null) {
const data = VibratorModeCreateData(asset, config, parentOption);
/** @type {ExtendedItemCallbackStruct<VibratingItemOption>} */
const defaultCallbacks = {
load: () => ExtendedItemLoad(data),
click: () => TypedItemClick(data),
draw: () => TypedItemDraw(data),
validate: (...args) => VibratorModeValidate(data, ...args),
publishAction: (...args) => VibratorModePublishAction(data, ...args),
init: (...args) => VibratorModeInit(data, ...args),
scriptDraw: (...args) => VibratorModeScriptDraw(data, ...args),
setOption: (...args) => ExtendedItemSetOption(data, ...args),
};
ExtendedItemCreateCallbacks(data, defaultCallbacks);
VibratorModeSetAssetProperties(data);
return data;
}
/**
* Sets an extended item's type and properties to the option provided.
* @param {VibratingItemData} data - The extended item data
* @param {Character} C - The character on whom the item is equipped
* @param {Item} item - The item whose type to set
* @param {VibratingItemOption} newOption - The to-be applied extended item option
* @param {VibratingItemOption} previousOption - The previously applied extended item option
* @param {boolean} [push] - Whether or not appearance updates should be persisted (only applies if the character is the
* player) - defaults to false.
*/
function VibratorModeSetOption(data, C, item, newOption, previousOption, push=false) {
ExtendedItemSetOption(data, C, item, newOption, previousOption, false);
switch (newOption.Name) {
case "Random":
item.Property.Intensity = CommonRandomItemFromList(null, [-1, 0, 1, 2, 3]);
item.Property.Effect = CommonArrayConcatDedupe(
item.Property.Effect,
item.Property.Intensity >= 0 ? ["Egged", "Vibrating"] : ["Egged"],
);
break;
case "Tease":
case "Deny":
item.Property.Intensity = CommonRandomItemFromList(null, [0, 1, 2, 3]);
item.Property.Effect = CommonArrayConcatDedupe(item.Property.Effect, ["Vibrating"]);
break;
case "Edge":
item.Property.Intensity = CommonRandomItemFromList(null, [0, 1]);
break;
}
CharacterRefresh(C, push, false);
}
/**
* Parse the passed typed item draw data as passed via the extended item config
* @param {readonly VibratorModeSet[]} modeSet - The vibrator mode sets for the item
* @param {ExtendedItemConfigDrawData<{ drawImage?: false }> | undefined} drawData - The to-be parsed draw data
* @param {number} y - The y-coordinate at which to start drawing the controls
* @return {ExtendedItemDrawData<ElementMetaData.Vibrating>} - The parsed draw data
*/
function VibratorModeGetDrawData(modeSet, drawData, y=450) {
/** @type {ElementData<ElementMetaData.Vibrating>[]} */
const elementData = [];
modeSet.forEach((modeName) => {
const options = VibratorModeOptions[modeName];
options.forEach((_, i) => {
const x = 1135 + (i % 3) * 250;
if (i % 3 === 0) {
y += 80;
}
elementData.push({
position: [x, y, 225, 50],
drawImage: false,
hidden: false,
imagePath: null,
});
});
y += 40;
});
return ExtendedItemGetDrawData(drawData, { elementData, itemsPerPage: elementData.length });
}
/**
* Generates an asset's vibrating item data
* @param {Asset} asset - The asset to generate vibrating item data for
* @param {VibratingItemConfig} config - The item's extended item configuration
* @param {null | ExtendedItemOption} parentOption - The parent extended item option of the super screens (if any)
* @returns {VibratingItemData} - The generated vibrating item data for the asset
*/
function VibratorModeCreateData(
asset,
{
Options,
ScriptHooks,
BaselineProperty,
Dictionary,
DialogPrefix,
DrawData,
ChatTags,
AllowEffect,
Name,
},
parentOption=null,
) {
const modeSet = Array.isArray(Options) ? Options : Object.values(VibratorModeSet);
const name = Name != null ? Name : (parentOption == null ? ExtendedArchetype.VIBRATING : parentOption.Name);
const key = `${asset.Group.Name}${asset.Name}${parentOption == null ? "" : name}`;
DialogPrefix = DialogPrefix || {};
const data = VibratorModeDataLookup[key] = {
archetype: ExtendedArchetype.VIBRATING,
key,
asset,
parentOption,
name,
options: null, // Initialized further down below
modeSet: modeSet,
functionPrefix: `Inventory${key}`,
dynamicAssetsFunctionPrefix: `Assets${asset.Group.Name}${asset.Name}`,
scriptHooks: ExtendedItemParseScriptHooks(ScriptHooks || {}),
dialogPrefix: {
header: DialogPrefix.Header || VibratorModeDialogPrefix,
chat: DialogPrefix.Chat || "VibeModeAction",
option: DialogPrefix.Option || "VibeMode",
},
chatSetting: "default",
baselineProperty: CommonIsObject(BaselineProperty) ? BaselineProperty : null,
dictionary: Array.isArray(Dictionary) ? Dictionary : [],
chatTags: Array.isArray(ChatTags) ? ChatTags : [
CommonChatTags.SOURCE_CHAR,
CommonChatTags.DEST_CHAR,
CommonChatTags.ASSET_NAME,
],
drawData: VibratorModeGetDrawData(modeSet, DrawData),
allowEffect: Array.isArray(AllowEffect) ? AllowEffect : [],
};
data.options = VibratorModeGetOptions(data, modeSet);
return data;
}
/** @type {ExtendedItemHeaderCallback<VibratingItemData>} */
function VibratorModeDialogPrefix(data, C, item) {
return InterfaceTextGet(`Intensity${item.Property.Intensity}`);
}
/**
* Construct all extended item options for a given list of modes.
* @param {VibratingItemData} data - The extended item data
* @param {readonly VibratorModeSet[]} modeSet - The vibrator mods
* @returns {VibratingItemOption[]} - The generated vibrating item options
*/
function VibratorModeGetOptions(data, modeSet) {
/** @type {VibratingItemOption[]} */
const options = [];
let i = -1;
for (const mode of modeSet) {
for (const _protoOption of VibratorModeOptions[mode]) {
i += 1;
const protoOption = ExtendedItemParseOptions(_protoOption, data.asset);
options.push({
...CommonOmit(protoOption, ["ArchetypeConfig"]),
OptionType: "VibratingItemOption",
ParentData: data,
Property: {
...CommonCloneDeep(protoOption.Property),
TypeRecord: { [data.name]: i },
},
});
}
}
return options;
}
/**
* @param {VibratingItemData} data
* @param {Character} C - The character on whom the item is equipped
* @param {Item} item - The item whose options are being validated
* @param {VibratingItemOption} newOption - The new option
* @param {VibratingItemOption} previousOption - The previously applied option
* @param {boolean} permitExisting - Determines whether the validation should allow the new option and previous option
* to be identical. Defaults to `false`.
* @returns {string|undefined} - undefined or an empty string if the validation passes. Otherwise, returns a string
* message informing the player of the requirements that are not met.
*/
function VibratorModeValidate(data, C, item, newOption, previousOption, permitExisting=false) {
if (
newOption.Property
&& VibratorModesAdvanced.includes(newOption.Property.Mode)
&& C.ArousalSettings
&& C.ArousalSettings.DisableAdvancedVibes
) {
return InterfaceTextGet("ExtendedItemNoItemPermission");
} else {
return ExtendedItemValidate(data, C, item, newOption, previousOption, permitExisting);
}
}
/**
* Publish a vibrator action and exit the dialog of applicable
* @param {VibratingItemData} data
* @param {Character} C - The character wearing the item
* @param {Item} item - The item in question
* @param {VibratingItemOption} newOption - The newly selected option
* @param {VibratingItemOption} previousOption - The currently selected option
*/
function VibratorModePublishAction(data, C, item, newOption, previousOption) {
const [newProperty, prevProperty, chatPrefix] = [newOption.Property, previousOption.Property, data.dialogPrefix.chat];
const chatData = {
C,
previousOption,
newOption,
previousIndex: data.options.indexOf(previousOption),
newIndex: data.options.indexOf(newOption),
};
const dictionary = ExtendedItemBuildChatMessageDictionary(chatData, data, item);
const newIsAdvanced = VibratorModesAdvanced.includes(newOption.Name);
const prevIsAdvanced = VibratorModesAdvanced.includes(previousOption.Name);
let message = (typeof chatPrefix === "function") ? chatPrefix(chatData) : chatPrefix;
if (!newIsAdvanced && !prevIsAdvanced) { // standard -> standard
const direction = newProperty.Intensity > prevProperty.Intensity ? "Increase" : "Decrease";
message += `${direction}To${newProperty.Intensity}`;
} else if (newIsAdvanced) { // standard/advanced -> advanced
message += newOption.Name;
} else { // advanced -> standard
message += `IncreaseTo${newProperty.Intensity}`;
}
ChatRoomPublishCustomAction(message, false, dictionary.build());
}
/**
* Sets asset properties common to all vibrating items
* @param {VibratingItemData} data - The vibrating item data for the asset
* @returns {void} - Nothing
*/
function VibratorModeSetAssetProperties(data) {
const asset = /** @type {Mutable<Asset>} */(data.asset);
asset.Extended = true;
TypedItemGenerateAllowEffect(data);
}
/**
* @typedef {{ Mode?: VibratorMode, ChangeTime?: number, LastChange?: number }} VibratorModePersistentData
*/
/**
* Common dynamic script draw function for vibrators. This function is called every frame. TO make use of dynamic script draw on vibrators,
* ensure your item has a `Assets<AssetGroup><AssetName>ScriptDraw` function defined that calls this function, and that your asset
* definition in Female3DCG.js has `DynamicScriptDraw: true` set. See the Heart Piercings for examples.
* @param {VibratingItemData} data
* @param {DynamicScriptCallbackData<VibratorModePersistentData>} drawData
*/
function VibratorModeScriptDraw(data, drawData) {
var C = drawData.C;
// Only run vibrator updates on the player and NPCs
if (!C.IsPlayer() && C.MemberNumber !== null) return;
var Item = drawData.Item;
// No need to update the vibrator if it has no mode
if (!Item.Property || !Item.Property.Mode) return;
var PersistentData = drawData.PersistentData();
var ModeChanged = Item.Property.Mode !== PersistentData.Mode;
if (ModeChanged || typeof PersistentData.ChangeTime !== "number") PersistentData.ChangeTime = CommonTime() + 60000;
if (ModeChanged || typeof PersistentData.LastChange !== "number") PersistentData.LastChange = CommonTime();
if (ModeChanged) PersistentData.Mode = Item.Property.Mode;
if (CommonTime() > PersistentData.ChangeTime) {
const updateMode = VibratorModeUpdate[Item.Property.Mode];
if (updateMode) {
updateMode(data, C, Item, PersistentData);
}
PersistentData.Mode = Item.Property.Mode;
}
}
/**
* Vibrator update function for vibrator state machine modes
* @param {VibratingItemData} data - The vibrating item data
* @param {Character} C - The character that the item is equipped on
* @param {Item} item - The item that is being updated
* @param {VibratorModePersistentData} persistentData - Persistent animation data for the item
* @param {readonly VibratorModeState[]} transitionsFromDefault - The possible vibrator states that may be transitioned to from
* the default state
* @returns {void} - Nothing
*/
function VibratorModeUpdateStateBased(data, C, item, persistentData, transitionsFromDefault) {
var Arousal = C.ArousalSettings.Progress;
var TimeSinceLastChange = CommonTime() - persistentData.LastChange;
var OldState = item.Property.State || VibratorModeState.DEFAULT;
var OldIntensity = item.Property.Intensity;
const NewStateAndIntensity = VibratorModeStateUpdate[OldState](
C,
Arousal,
TimeSinceLastChange,
OldIntensity,
transitionsFromDefault,
);
var State = NewStateAndIntensity.State;
var Intensity = NewStateAndIntensity.Intensity;
if (!State) State = VibratorModeState.DEFAULT;
if (typeof Intensity !== "number" || Intensity < -1 || Intensity > 3) Intensity = OldIntensity;
/** @type {EffectName[]} */
var Effect = ["Egged"];
if (State === VibratorModeState.DENY || item.Property.Mode === VibratorMode.DENY) Effect.push("Edged");
if (Intensity !== -1) Effect.push("Vibrating");
const option = data.options.find(o => o.Name === persistentData.Mode) || VibratorModeOff;
ExtendedItemSetProperty(C, item, option.Property, { Mode: persistentData.Mode, TypeRecord: { ...option.Property.TypeRecord }, State, Intensity, Effect }, false);
Object.assign(persistentData, {
ChangeTime: CommonTime() + 5000,
LastChange: Intensity !== OldIntensity ? CommonTime() : persistentData.LastChange,
});
VibratorModePublish(data, C, item, OldIntensity, Intensity);
}
/**
* Namespace with helper functions for {@link VibratorModeScriptDraw}
* @type {Partial<Record<VibratorMode, (data: VibratingItemData, C: Character, item: Item, persistentData: VibratorModePersistentData) => void>>}
* @namespace
*/
const VibratorModeUpdate = {
/**
* Vibrator update function for the Random mode
* @param {VibratingItemData} data - The vibrating item data
* @param {Character} C - The character that the item is equipped on
* @param {Item} item - The item that is being updated
* @param {VibratorModePersistentData} persistentData - Persistent animation data for the item
* @returns {void} - Nothing
*/
Random: function (data, C, item, persistentData) {
const oldIntensity = item.Property.Intensity;
/** @type {VibratorIntensity} */
const newIntensity = CommonRandomItemFromList(oldIntensity, [-1, 0, 1, 2, 3]);
/** @type {EffectName[]} */
const effect = newIntensity === -1 ? ["Egged"] : ["Egged", "Vibrating"];
const option = data.options.find(o => o.Name === persistentData.Mode) || VibratorModeOff;
ExtendedItemSetProperty(
C, item, option.Property,
{
Mode: persistentData.Mode,
TypeRecord: { ...option.Property.TypeRecord },
Intensity: newIntensity,
Effect: effect,
},
);
// Next update in 1-3 minutes
const oneMinute = 60000;
persistentData.ChangeTime = Math.floor(CommonTime() + oneMinute + Math.random() * 2 * oneMinute);
VibratorModePublish(data, C, item, oldIntensity, newIntensity);
},
/**
* Vibrator update function for the Escalate mode
* @param {VibratingItemData} data - The vibrating item data
* @param {Character} C - The character that the item is equipped on
* @param {Item} item - The item that is being updated
* @param {VibratorModePersistentData} persistentData - Persistent animation data for the item
* @returns {void} - Nothing
*/
Escalate: function (data, C, item, persistentData) {
const oldIntensity = item.Property.Intensity;
const newIntensity = /** @type {VibratorIntensity} */((oldIntensity + 1) % 4);
const option = data.options.find(o => o.Name === persistentData.Mode) || VibratorModeOff;
ExtendedItemSetProperty(
C, item, option.Property,
{
Mode: persistentData.Mode,
TypeRecord: { ...option.Property.TypeRecord },
Intensity: newIntensity,
Effect: ["Egged", "Vibrating"]
},
);
// As intensity increases, time between updates decreases
const timeFactor = Math.pow((5 - newIntensity), 1.8);
const timeToNextUpdate = (8000 + Math.random() * 4000) * timeFactor;
persistentData.ChangeTime = Math.floor(CommonTime() + timeToNextUpdate);
VibratorModePublish(data, C, item, oldIntensity, newIntensity);
},
/**
* Vibrator update function for the Tease mode
* @param {VibratingItemData} data - The vibrating item data
* @param {Character} C - The character that the item is equipped on
* @param {Item} item - The item that is being updated
* @param {VibratorModePersistentData} persistentData - Persistent animation data for the item
* @returns {void} - Nothing
*/
Tease: function (data, C, item, persistentData) {
// Tease mode allows orgasm and denial states once arousal gets high enough
VibratorModeUpdateStateBased(data, C, item, persistentData, [VibratorModeState.DENY, VibratorModeState.ORGASM]);
},
/**
* Vibrator update function for the Deny mode
* @param {VibratingItemData} data - The vibrating item data
* @param {Character} C - The character that the item is equipped on
* @param {Item} item - The item that is being updated
* @param {VibratorModePersistentData} persistentData - Persistent animation data for the item
* @returns {void} - Nothing
*/
Deny: function (data, C, item, persistentData) {
VibratorModeUpdateStateBased(data, C, item, persistentData, [VibratorModeState.DENY]);
},
/**
* Vibrator update function for the Edge mode
* @param {VibratingItemData} data - The vibrating item data
* @param {Character} C - The character that the item is equipped on
* @param {Item} item - The item that is being updated
* @param {VibratorModePersistentData} persistentData - Persistent animation data for the item
* @returns {void} - Nothing
*/
Edge: function (data, C, item, persistentData) {
const oldIntensity = item.Property.Intensity;
const newIntensity = /** @type {VibratorIntensity} */(Math.min(item.Property.Intensity + 1, 3));
const option = data.options.find(o => o.Name === persistentData.Mode) || VibratorModeOff;
ExtendedItemSetProperty(
C, item, option.Property,
{
Mode: persistentData.Mode,
TypeRecord: { ...option.Property.TypeRecord },
Intensity: newIntensity,
Effect: ["Egged", "Vibrating", "Edged"],
},
);
if (newIntensity === 3) {
// If we've hit max intensity, no more changes needed
persistentData.ChangeTime = Infinity;
} else {
const oneMinute = 60000;
// Next update 1-2 minutes from now
persistentData.ChangeTime = Math.floor(CommonTime() + oneMinute + Math.random() * oneMinute);
}
VibratorModePublish(data, C, item, oldIntensity, newIntensity);
},
};
/**
* Namespace with helper functions for {@link VibratorModeUpdateStateBased}
* @namespace
*/
const VibratorModeStateUpdate = {
/**
* Vibrator update function for vibrator state machine modes in the Default state
* @param {Character} C - The character that the item is equipped on
* @param {number} arousal - The current arousal of the character
* @param {number} timeSinceLastChange - The time in milliseconds since the vibrator intensity was last changed
* @param {VibratorIntensity} oldIntensity - The current intensity of the vibrating item
* @param {readonly VibratorModeState[]} transitionsFromDefault - The possible vibrator states that may be transitioned to from
* the default state
* @returns {StateAndIntensity} - The updated state and intensity of the vibrator
*/
Default: function (C, arousal, timeSinceLastChange, oldIntensity, transitionsFromDefault) {
const oneMinute = 60000;
/** @type {VibratorModeState} */
let state = VibratorModeState.DEFAULT;
let newIntensity = oldIntensity;
// If arousal is high, decide whether to deny or orgasm, based on provided transitions
if (arousal > 90) state = CommonRandomItemFromList(VibratorModeState.DEFAULT, transitionsFromDefault);
// If it's been at least a minute since the last intensity change, there's a small chance to change intensity
if (timeSinceLastChange > oneMinute && Math.random() < 0.1) newIntensity = CommonRandomItemFromList(oldIntensity, [0, 1, 2, 3]);
return { State: state, Intensity: newIntensity };
},
/**
* Vibrator update function for vibrator state machine modes in the Deny state
* @param {Character} C - The character that the item is equipped on
* @param {number} arousal - The current arousal of the character
* @param {number} timeSinceLastChange - The time in milliseconds since the vibrator intensity was last changed
* @param {VibratorIntensity} oldIntensity - The current intensity of the vibrating item
* the default state
* @returns {StateAndIntensity} - The updated state and intensity of the vibrator
*/
Deny: function (C, arousal, timeSinceLastChange, oldIntensity) {
const oneMinute = 60000;
/** @type {VibratorModeState} */
let state = VibratorModeState.DENY;
let newIntensity = oldIntensity;
if (arousal >= 95 && timeSinceLastChange > oneMinute && Math.random() < 0.2) {
if (C.IsEdged()) {
// In deny mode, there's a small chance to change to give a fake orgasm and then go to rest mode after a minute
// Here we give the fake orgasm, passing a special parameter that indicates we bypass the usual restriction on Edge
ActivityOrgasmPrepare(C, true);
}
// Set the vibrator to rest
state = VibratorModeState.REST;
newIntensity = -1;
} else if (arousal >= 95) {
// If arousal is too high, change intensity back down to tease
newIntensity = 0;
} else if (timeSinceLastChange > oneMinute && Math.random() < 0.1) {
// Otherwise, there's a small chance to change intensity if it's been more than a minute since the last change
newIntensity = CommonRandomItemFromList(oldIntensity, [0, 1, 2, 3]);
}
return { State: state, Intensity: newIntensity };
},
/**
* Vibrator update function for vibrator state machine modes in the Orgasm state
* @param {Character} C - The character that the item is equipped on
* @param {number} arousal - The current arousal of the character
* @param {number} timeSinceLastChange - The time in milliseconds since the vibrator intensity was last changed
* @param {VibratorIntensity} oldIntensity - The current intensity of the vibrating item
* the default state
* @returns {StateAndIntensity} - The updated state and intensity of the vibrator
*/
Orgasm: function (C, arousal, timeSinceLastChange, oldIntensity) {
const OneMinute = 60000;
/** @type {VibratorModeState} */
let state = VibratorModeState.ORGASM;
let newIntensity = oldIntensity;
if (C.ArousalSettings.OrgasmStage > 0) {
// If we're in orgasm mode and the player is either resisting or mid-orgasm, change back to either rest or default mode
state = Math.random() < 0.75 ? VibratorModeState.REST : VibratorModeState.DEFAULT;
} else if (timeSinceLastChange > OneMinute && Math.random() < 0.1) {
// Otherwise, if it's been over a minute since the last intensity change, there's a small chance to change intensity
newIntensity = CommonRandomItemFromList(oldIntensity, [0, 1, 2, 3]);
}
return { State: state, Intensity: newIntensity };
},
/**
* Vibrator update function for vibrator state machine modes in the Rest state
* @param {Character} C - The character that the item is equipped on
* @param {number} arousal - The current arousal of the character
* @param {number} timeSinceLastChange - The time in milliseconds since the vibrator intensity was last changed
* @param {VibratorIntensity} oldIntensity - The current intensity of the vibrating item
* the default state
* @returns {StateAndIntensity} - The updated state and intensity of the vibrator
*/
Rest: function(C, arousal, timeSinceLastChange, oldIntensity) {
const fiveMinutes = 5 * 60000;
const tenMinutes = 10 * 60000;
/** @type {VibratorModeState} */
let state = VibratorModeState.REST;
/** @type {VibratorIntensity} */
let newIntensity = -1;
if (timeSinceLastChange > fiveMinutes && Math.random() < Math.pow((timeSinceLastChange - fiveMinutes) / tenMinutes, 2)) {
// Rest between 5 and 15 minutes (probably of change gets increasingly more likely as time approaches 15 minutes)
state = VibratorModeState.DEFAULT;
newIntensity = CommonRandomItemFromList(oldIntensity, [0, 1, 2, 3]);
}
return { State: state, Intensity: newIntensity };
},
};
/**
* Publishes a chatroom message for an automatic change in vibrator intensity. Does nothing if the vibrator's intensity
* did not change.
* @param {VibratingItemData} data
* @param {Character} C - The character that the vibrator is equipped on
* @param {Item} item - The vibrator item
* @param {number} oldIntensity - The previous intensity of the vibrator
* @param {number} newIntensity - The new intensity of the vibrator
* @returns {void} - Nothing
*/
function VibratorModePublish(data, C, item, oldIntensity, newIntensity) {
// If the intensity hasn't changed, don't publish a chat message
if (oldIntensity === newIntensity) return;
const option = data.options.find(o => o.Name === item.Property.Mode) || data.options[0];
const optionIndex = data.options.indexOf(option);
const chatData = {
C,
previousOption: option,
newOption: option,
previousIndex: optionIndex,
newIndex: optionIndex,
};
const dictionary = ExtendedItemBuildChatMessageDictionary(chatData, data, item).markAutomatic().build();
const chatPrefix = data.dialogPrefix.chat;
const prefix = (typeof chatPrefix === "function") ? chatPrefix(chatData) : chatPrefix;
const direction = newIntensity > oldIntensity ? "Increase" : "Decrease";
if (CurrentScreen == "ChatRoom") {
ServerSend(
"ChatRoomChat",
{ Content: `${prefix}${direction}To${newIntensity}`, Type: "Action", Dictionary: dictionary },
);
CharacterLoadEffect(C);
ChatRoomCharacterItemUpdate(C, item.Asset.Group.Name);
ActivityChatRoomArousalSync(C);
}
}
/**
* Initialize the vibrating item properties
* @param {VibratingItemData} 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 VibratorModeInit(data, C, item, push=true, refresh=true) {
return TypedItemInit(data, C, item, push, refresh, "Mode");
}
/**
* An alias for {@link TypedItemSetOptionByName}.
* @type {typeof TypedItemSetOptionByName}
*/
function VibratorModeSetOptionByName(...args) {
return TypedItemSetOptionByName(...args);
}
/**
* Return the (standard) vibrator mode one would get by incrementing/decrementing the passed mode.
* @param {VibratorMode} mode - The current vibrator mode
* @param {boolean} decrement - Whether the mode should be decremented rather than incremented
* @returns {VibratorMode} The new vibrator mode
*/
function VibratorModeIntensityIncrement(mode, decrement=false) {
const options = VibratorModeOptions[VibratorModeSet.STANDARD];
let index = options.findIndex(o => o.Name === mode);
index += decrement ? -1 : 1;
const optionNew = options[index];
if (optionNew === undefined) {
return decrement ? VibratorMode.OFF : VibratorMode.MAXIMUM;
} else {
return optionNew.Name;
}
}