mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2025-04-25 17:59:34 +00:00
971 lines
40 KiB
JavaScript
971 lines
40 KiB
JavaScript
"use strict";
|
|
/** @type {null | string[][]} */
|
|
var ActivityDictionary = null;
|
|
var ActivityOrgasmGameButtonX = 0;
|
|
var ActivityOrgasmGameButtonY = 0;
|
|
var ActivityOrgasmGameProgress = 0;
|
|
var ActivityOrgasmGameDifficulty = 0;
|
|
var ActivityOrgasmGameResistCount = 0;
|
|
var ActivityOrgasmGameTimer = 0;
|
|
var ActivityOrgasmResistLabel = "";
|
|
var ActivityOrgasmRuined = false; // If set to true, the orgasm will be ruined right before it happens
|
|
|
|
/** @type { ()=>void | undefined } */
|
|
let ActivityTranslateResolve = undefined;
|
|
|
|
/**
|
|
* Checks if the current room allows for activities. (They can only be done in certain rooms)
|
|
* @returns {boolean} - Whether or not activities can be done
|
|
*/
|
|
function ActivityAllowed() {
|
|
return (CurrentScreen == "ChatRoom" && !(ChatRoomData && ChatRoomData.BlockCategory && ChatRoomData.BlockCategory.includes("Arousal")))
|
|
|| (((CurrentScreen == "Private") || (CurrentScreen == "PrivateBed")) && LogQuery("RentRoom", "PrivateRoom")); }
|
|
|
|
/**
|
|
* Loads the activity dictionary that will be used throughout the game to output messages. Loads from cache first if possible.
|
|
* @return {void} - Nothing
|
|
*/
|
|
function ActivityDictionaryLoad() {
|
|
|
|
// Tries to read it from cache first
|
|
var FullPath = "Screens/Character/Preference/ActivityDictionary.csv";
|
|
var TranslationPath = FullPath.replace(".csv", "_" + TranslationLanguage + ".txt");
|
|
|
|
if (CommonCSVCache[FullPath]) {
|
|
ActivityDictionary = CommonCloneDeep(CommonCSVCache[FullPath]);
|
|
} else {
|
|
// Opens the file, parse it and returns the result in an object
|
|
CommonGet(FullPath, function () {
|
|
if (this.status == 200) {
|
|
CommonCSVCache[FullPath] = CommonParseCSV(this.responseText);
|
|
ActivityDictionary = CommonCloneDeep(CommonCSVCache[FullPath]);
|
|
ActivityTranslateResolve?.();
|
|
}
|
|
});
|
|
}
|
|
|
|
// If a translation file is available, we open the txt file and keep it in cache
|
|
if (TranslationAvailable(TranslationPath))
|
|
CommonGet(TranslationPath, function () {
|
|
if (this.status == 200) {
|
|
TranslationCache[TranslationPath] = TranslationParseTXT(this.responseText);
|
|
ActivityTranslate(TranslationPath);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Translates the activity dictionary.
|
|
* @param {string} CachePath - Path to the language cache.
|
|
*/
|
|
function ActivityTranslate(CachePath) {
|
|
if (!Array.isArray(TranslationCache[CachePath])) return;
|
|
const DoTranslate = () => {
|
|
for (let T = 0; T < ActivityDictionary.length; T++) {
|
|
if (ActivityDictionary[T][1]) {
|
|
let indexText = TranslationCache[CachePath].indexOf(ActivityDictionary[T][1].trim());
|
|
if (indexText >= 0) {
|
|
ActivityDictionary[T][1] = TranslationCache[CachePath][indexText + 1];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
if(ActivityDictionary) DoTranslate();
|
|
else ActivityTranslateResolve = DoTranslate;
|
|
}
|
|
|
|
/**
|
|
* Searches in the dictionary for a specific keyword's text
|
|
* @param {string} KeyWord - Tag of the activity description to search for
|
|
* @returns {string} - Description associated to the given keyword
|
|
*/
|
|
function ActivityDictionaryText(KeyWord) {
|
|
for (let D = 0; D < ActivityDictionary.length; D++)
|
|
if (ActivityDictionary[D][0] == KeyWord)
|
|
return ActivityDictionary[D][1].trim();
|
|
return "MISSING ACTIVITY DESCRIPTION FOR KEYWORD " + KeyWord;
|
|
}
|
|
|
|
/**
|
|
* Resolve a group name to the correct group for activities
|
|
* @param {IAssetFamily} family - The asset family for the named group
|
|
* @param {AssetGroupItemName} groupname - The name of the group to resolve
|
|
* @returns {AssetItemGroup | null} - The resolved group
|
|
*/
|
|
function ActivityGetGroupOrMirror(family, groupname) {
|
|
const group = AssetGroupGet(family, groupname);
|
|
if (group && group.MirrorActivitiesFrom != null) {
|
|
const mirror = AssetGroupGet(family, group.MirrorActivitiesFrom);
|
|
if (mirror) {
|
|
return mirror;
|
|
}
|
|
}
|
|
return group;
|
|
}
|
|
|
|
/**
|
|
* Gets all groups that mirror or are mirrored by the given group name for activities. The returned array includes the
|
|
* named group.
|
|
* @param {IAssetFamily} family - The asset family for the named group
|
|
* @param {AssetGroupName} groupName - The name of the group to resolve
|
|
* @returns {AssetGroup[]} - The group and all groups from the same family that mirror or are mirrored by it
|
|
*/
|
|
function ActivityGetAllMirrorGroups(family, groupName) {
|
|
return AssetActivityMirrorGroups.get(groupName) || [];
|
|
}
|
|
|
|
/**
|
|
* Check if any activities are possible for a character's given group.
|
|
* @param {Character} C - The character on which the check is done
|
|
* @param {AssetGroupItemName} GroupName - The group to check access on
|
|
* @returns {boolean} Whether any activity is possible
|
|
*/
|
|
function ActivityPossibleOnGroup(C, GroupName) {
|
|
const CharacterNotEnclosedOrSelfActivity = ((!C.IsEnclose() && !Player.IsEnclose()) || C.IsPlayer());
|
|
if (!CharacterNotEnclosedOrSelfActivity || !ActivityAllowed() || !CharacterHasArousalEnabled(C))
|
|
return false;
|
|
const Group = ActivityGetGroupOrMirror(C.AssetFamily, GroupName);
|
|
const Zone = PreferenceGetArousalZone(C, Group.Name);
|
|
return Zone && Zone.Factor > 0;
|
|
}
|
|
|
|
/**
|
|
* Check whether a given activity can be performed on a group
|
|
* @param {Character} char - The character being targeted
|
|
* @param {Activity} act - The activity to consider
|
|
* @param {AssetGroup} group - The group to check
|
|
* @returns {boolean} whether that activity's target is valid
|
|
*/
|
|
function ActivityHasValidTarget(char, act, group) {
|
|
let activities = AssetActivitiesForGroup(char.AssetFamily, group.Name, (char.IsPlayer() ? "self" : "other"));
|
|
return activities.some(a => a.Name === act.Name);
|
|
}
|
|
|
|
/**
|
|
* Check that an activity is permitted by an actor's settings.
|
|
* @param {Activity} A - The activity to consider
|
|
* @param {Character|PlayerCharacter} C - The character to check with
|
|
* @param {boolean} Other - Whether we look at doing to or being done on
|
|
* @returns {boolean} whether the activity is permitted
|
|
*/
|
|
function ActivityCheckPermissions(A, C, Other) {
|
|
if ((C.ArousalSettings == null) || (C.ArousalSettings.Activity == null)) return true;
|
|
return (PreferenceGetActivityFactor(C, A.Name, !Other) != 0);
|
|
}
|
|
|
|
/**
|
|
* Check that that a given prerequisite is met.
|
|
* @param {ActivityPrerequisite} prereq - The prerequisite to consider
|
|
* @param {Character|PlayerCharacter} acting - The character performing the activity
|
|
* @param {Character|PlayerCharacter} acted - The character being acted on
|
|
* @param {AssetGroup} group - The group being acted on
|
|
* @returns {boolean} whether the given activity's prerequisite are satisfied
|
|
*/
|
|
function ActivityCheckPrerequisite(prereq, acting, acted, group) {
|
|
switch (prereq) {
|
|
case "UseMouth":
|
|
return !acting.IsMouthBlocked() && acting.CanTalk();
|
|
case "UseTongue":
|
|
return !acting.IsMouthBlocked();
|
|
case "TargetMouthBlocked":
|
|
return acted.IsMouthBlocked();
|
|
case "IsGagged":
|
|
return acting.IsGagged();
|
|
case "TargetKneeling":
|
|
return acted.IsKneeling();
|
|
case "UseHands":
|
|
return acting.CanInteract() && !acting.Effect.includes("MergedFingers");
|
|
case "UseArms":
|
|
return acting.CanInteract() || (!InventoryGet(acting, "ItemArms") && !InventoryGroupIsBlocked(acting, "ItemArms"));
|
|
case "UseFeet":
|
|
return acting.CanWalk();
|
|
case "CantUseArms":
|
|
return !acting.CanInteract() && (!!InventoryGet(acting, "ItemArms") || InventoryGroupIsBlocked(acting, "ItemArms"));
|
|
case "CantUseFeet":
|
|
return !acting.CanWalk();
|
|
case "TargetCanUseTongue":
|
|
return !acted.IsMouthBlocked();
|
|
case "TargetMouthOpen":
|
|
if (group.Name === "ItemMouth")
|
|
return !InventoryGet(acted, "ItemMouth") || acted.IsMouthOpen();
|
|
break;
|
|
case "VulvaEmpty":
|
|
if (group.Name === "ItemVulva")
|
|
return acted.HasVagina() && !acted.IsVulvaFull();
|
|
break;
|
|
case "AssEmpty":
|
|
if (group.Name === "ItemButt")
|
|
return !acted.IsPlugged();
|
|
break;
|
|
case "MoveHead":
|
|
if (group.Name === "ItemHead")
|
|
return !acted.IsFixedHead();
|
|
break;
|
|
case "ZoneAccessible":
|
|
case "TargetZoneAccessible": {
|
|
// FIXME: The original ZoneAccessible should have been prefixed with Target, which is why those are reversed
|
|
// TargetZoneAccessible is only used for ReverseSuckItem, which is marked as reverse, adding in to the confusion
|
|
const actor = prereq.startsWith("Target") ? acting : acted;
|
|
return ActivityGetAllMirrorGroups(actor.AssetFamily, group.Name).some((g) => g.IsItem() ? !InventoryGroupIsBlocked(actor, g.Name, true) : true);
|
|
}
|
|
case "ZoneNaked":
|
|
case "TargetZoneNaked": {
|
|
// FIXME: Ditto
|
|
const actor = prereq.startsWith("Target") ? acting : acted;
|
|
if (group.Name === "ItemButt")
|
|
return InventoryPrerequisiteMessage(actor, "AccessButt") === "" && !(actor.IsPlugged() || actor.IsButtChaste());
|
|
else if (group.Name === "ItemVulva")
|
|
return (InventoryPrerequisiteMessage(actor, "AccessCrotch") === "") && !actor.IsVulvaChaste();
|
|
else if (group.Name === "ItemVulvaPiercings")
|
|
return (InventoryPrerequisiteMessage(actor, "AccessCrotch") === "") && !actor.IsVulvaChaste();
|
|
else if (group.Name === "ItemBreast" || group.Name === "ItemNipples")
|
|
return (InventoryPrerequisiteMessage(actor, "AccessBreast") === "") && !actor.IsBreastChaste();
|
|
else if (group.Name === "ItemBoots")
|
|
return InventoryPrerequisiteMessage(actor, "NakedFeet") === "";
|
|
else if (group.Name === "ItemHands")
|
|
return InventoryPrerequisiteMessage(actor, "NakedHands") === "";
|
|
break;
|
|
}
|
|
case "CanUsePenis":
|
|
if (acting.HasPenis())
|
|
return InventoryPrerequisiteMessage(acting, "AccessVulva") === "";
|
|
break;
|
|
case "Sisters":
|
|
return !acting.HasPenis() && !acted.HasPenis() && acting.IsSiblingOfCharacter(acted);
|
|
case "Brothers":
|
|
return acting.HasPenis() && acted.HasPenis() && acting.IsSiblingOfCharacter(acted);
|
|
case "SiblingsWithDifferentGender":
|
|
return (acting.HasPenis() != acted.HasPenis()) && acting.IsSiblingOfCharacter(acted);
|
|
case "Collared":
|
|
return acted.WearingCollar();
|
|
default:
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check that an activity's prerequisites are met.
|
|
* @param {Activity} activity - The activity to consider
|
|
* @param {Character|PlayerCharacter} acting - The character performing the activity
|
|
* @param {Character|PlayerCharacter} acted - The character being acted on
|
|
* @param {AssetGroup} group - The group being acted on
|
|
* @returns {boolean} whether the given activity's prerequisite are satisfied
|
|
*/
|
|
function ActivityCheckPrerequisites(activity, acting, acted, group) {
|
|
if (!activity.Prerequisite)
|
|
return true;
|
|
|
|
const reverse = activity.Reverse;
|
|
return activity.Prerequisite.every((pre) => ActivityCheckPrerequisite(pre, (!reverse ? acting : acted), (!reverse ? acted : acting), group));
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {ItemActivity[]} allowed
|
|
* @param {Character} acting
|
|
* @param {Character} acted
|
|
* @param {ActivityNameItem} needsItem
|
|
* @param {Activity} activity
|
|
* @param {AssetGroup} targetGroup
|
|
*/
|
|
function ActivityGenerateItemActivitiesFromNeed(allowed, acting, acted, needsItem, activity, targetGroup) {
|
|
const reverse = activity.Reverse;
|
|
const items = CharacterItemsForActivity(!reverse ? acting : acted, needsItem);
|
|
if (items.length === 0) return true;
|
|
|
|
let handled = false;
|
|
for (const item of items) {
|
|
const typeList = CommonIsObject(item.Property?.TypeRecord) ? PropertyTypeRecordToStrings(item.Property.TypeRecord) : [null];
|
|
|
|
/** @type {ItemActivityRestriction} */
|
|
let blocked = null;
|
|
if (typeList.some((type) => InventoryIsAllowedLimited(acted, item, type))) {
|
|
blocked = "limited";
|
|
} else if (typeList.some((type) => InventoryBlockedOrLimited(acted, item, type))) {
|
|
blocked = "blocked";
|
|
} else if (InventoryGroupIsBlocked(acting, /** @type {AssetGroupItemName} */(item.Asset.Group.Name))) {
|
|
blocked = "unavail";
|
|
}
|
|
|
|
// FIXME: workaround for reverse activities because those are in a non-activity group
|
|
const isStrapon = item.Asset.Group.Name === "ItemDevices" && ["StrapOnSmooth", "StrapOnStuds"].includes(item.Asset.Name);
|
|
const isPenis = item.Asset.Group.Name === "Pussy" && item.Asset.Name === "Penis";
|
|
|
|
if (InventoryItemHasEffect(item, "UseRemote")) {
|
|
// That item actually needs a remote, so handle it separately
|
|
} else if (reverse && acted.FocusGroup.Name !== item.Asset.Group.Name && !(isStrapon || isPenis)) {
|
|
// This is a reverse activity, but we're targetting the wrong slot, just skip
|
|
handled = true;
|
|
} else {
|
|
allowed.push({ Activity: activity, Item: item, Blocked: blocked, Group: targetGroup.MirrorActivitiesFrom ?? targetGroup.Name });
|
|
handled = true;
|
|
}
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
/**
|
|
* Builds the allowed activities on a group given the character's settings.
|
|
* @param {Character} character - The character for which to build the activity dialog options
|
|
* @param {AssetGroupItemName} groupname - The group to check
|
|
* @return {ItemActivity[]} - The list of allowed activities
|
|
*/
|
|
function ActivityAllowedForGroup(character, groupname) {
|
|
// If we are not within interaction range, don't allow any activities
|
|
if(InventoryIsBlockedByDistance(character)) return [];
|
|
|
|
// Get the group and all possible activities
|
|
let activities = AssetAllActivities(character.AssetFamily);
|
|
let group = ActivityGetGroupOrMirror(character.AssetFamily, groupname);
|
|
if (!activities || !group) return [];
|
|
|
|
// Make sure the target player zone is allowed for an activity
|
|
if (!ActivityPossibleOnGroup(character, groupname))
|
|
return [];
|
|
|
|
const targetedItem = InventoryGet(character, groupname);
|
|
|
|
/** @type {ItemActivity[]} */
|
|
let allowed = [];
|
|
|
|
activities.forEach(activity => {
|
|
// Validate that this activity can be done
|
|
if (!ActivityHasValidTarget(character, activity, group))
|
|
return;
|
|
|
|
// Make sure all the prerequisites are met
|
|
if (!ActivityCheckPrerequisites(activity, Player, character, group))
|
|
return;
|
|
|
|
// Ensure this activity is permitted for both actors
|
|
if (!ActivityCheckPermissions(activity, Player, true)
|
|
|| !ActivityCheckPermissions(activity, character, false))
|
|
return;
|
|
|
|
// All checks complete, this activity is allowed
|
|
|
|
let handled = false;
|
|
let needsItem = activity.Prerequisite.find(p => p.startsWith("Needs-"));
|
|
|
|
if (needsItem) {
|
|
const needsItemActivity = /** @type {ActivityNameItem} */(needsItem.substring(6));
|
|
handled = ActivityGenerateItemActivitiesFromNeed(allowed, Player, character, needsItemActivity, activity, group);
|
|
}
|
|
|
|
if (activity.Name === "ShockItem" && InventoryItemHasEffect(targetedItem, "ReceiveShock")) {
|
|
let remote = Player.Appearance.find(a => InventoryItemHasEffect(a, "TriggerShock"));
|
|
if (remote) {
|
|
allowed.push({ Activity: activity, Item: remote, Group: group.Name });
|
|
handled = true;
|
|
}
|
|
}
|
|
|
|
if (!handled) {
|
|
allowed.push({ Activity: activity, Group: group.Name });
|
|
}
|
|
});
|
|
|
|
// Sort allowed activities by their group declaration order
|
|
return allowed.sort((a, b) => Math.sign(ActivityFemale3DCGOrdering.indexOf(a.Activity.Name) - ActivityFemale3DCGOrdering.indexOf(b.Activity.Name)));
|
|
}
|
|
|
|
/**
|
|
* Returns TRUE if an activity can be done
|
|
* @param {Character} C - The character to evaluate
|
|
* @param {ActivityName} Activity - The name of the activity
|
|
* @param {AssetGroupItemName} Group - The name of the group
|
|
* @return {boolean} - TRUE if the activity can be done
|
|
*/
|
|
function ActivityCanBeDone(C, Activity, Group) {
|
|
let ActList = ActivityAllowedForGroup(C, Group);
|
|
for (let A = 0; A < ActList.length; A++)
|
|
if (ActList[A].Activity.Name == Activity)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Calculates the effect of an activity performed on a zone
|
|
* @param {Character} S - The character performing the activity
|
|
* @param {Character} C - The character on which the activity is performed
|
|
* @param {ActivityName | Activity} A - The activity performed
|
|
* @param {AssetGroupItemName} Z - The group/zone name where the activity was performed
|
|
* @param {number} [Count=1] - If the activity is done repeatedly, this defines the number of times, the activity is done.
|
|
* If you don't want an activity to modify arousal, set this parameter to '0'
|
|
* @param {Asset} [Asset] - The asset used to perform the activity
|
|
* @return {void} - Nothing
|
|
*/
|
|
function ActivityEffect(S, C, A, Z, Count, Asset) {
|
|
|
|
// Converts from activity name to the activity object
|
|
if (typeof A === "string") A = AssetGetActivity(C.AssetFamily, A);
|
|
if ((A == null) || (typeof A === "string")) return;
|
|
if ((Count == null) || (Count == undefined) || (Count == 0)) Count = 1;
|
|
|
|
// Calculates the next progress factor
|
|
var Factor = (PreferenceGetActivityFactor(C, A.Name, (C.IsPlayer())) * 5) - 10; // Check how much the character likes the activity, from -10 to +10
|
|
Factor = Factor + (PreferenceGetZoneFactor(C, Z) * 5) - 10; // The zone used also adds from -10 to +10
|
|
Factor = Factor + Math.floor((Math.random() * 8)); // Random 0 to 7 bonus
|
|
if ((C.ID != S.ID) && (((!C.IsPlayer()) && C.IsLoverOfPlayer()) || ((C.IsPlayer()) && S.IsLoverOfPlayer()))) Factor = Factor + Math.floor((Math.random() * 8)); // Another random 0 to 7 bonus if the target is the player's lover
|
|
Factor = Factor + ActivityFetishFactor(C) * 2; // Adds a fetish factor based on the character preferences
|
|
Factor = Factor + Math.round(Factor * (Count - 1) / 3); // if the action is done repeatedly, we apply a multiplication factor based on the count
|
|
|
|
// Grab the relevant expression from either the asset or the activity
|
|
const expression = Asset && Asset.ActivityExpression && Asset.ActivityExpression[A.Name] ? Asset.ActivityExpression[A.Name] : A.ActivityExpression;
|
|
if (Array.isArray(expression))
|
|
InventoryExpressionTriggerApply(C, expression);
|
|
|
|
ActivitySetArousalTimer(C, A, Z, Factor);
|
|
|
|
}
|
|
|
|
/**
|
|
* Used for arousal events that are not activities, such as stimulation events
|
|
* @param {Character} S - The character performing the activity
|
|
* @param {Character} C - The character on which the activity is performed
|
|
* @param {number} Amount - The base amount of arousal to add
|
|
* @param {AssetGroupItemName} Z - The group/zone name where the activity was performed
|
|
* @param {number} [Count=1] - If the activity is done repeatedly, this defines the number of times, the activity is done.
|
|
* If you don't want an activity to modify arousal, set this parameter to '0'
|
|
* @param {Asset} [Asset] - The asset used to perform the activity
|
|
* @return {void} - Nothing
|
|
*/
|
|
function ActivityEffectFlat(S, C, Amount, Z, Count, Asset) {
|
|
|
|
// Converts from activity name to the activity object
|
|
if ((Amount == null) || (typeof Amount != "number")) return;
|
|
if ((Count == null) || (Count == undefined) || (Count == 0)) Count = 1;
|
|
|
|
// Calculates the next progress factor
|
|
var Factor = Amount; // Check how much the character likes the activity, from -10 to +10
|
|
Factor = Factor + (PreferenceGetZoneFactor(C, Z) * 5) - 10; // The zone used also adds from -10 to +10
|
|
Factor = Factor + ActivityFetishFactor(C) * 2; // Adds a fetish factor based on the character preferences
|
|
Factor = Factor + Math.round(Factor * (Count - 1) / 3); // if the action is done repeatedly, we apply a multiplication factor based on the count
|
|
|
|
ActivitySetArousalTimer(C, null, Z, Factor, Asset);
|
|
}
|
|
|
|
/**
|
|
* Syncs the player arousal with everyone in chatroom
|
|
* @param {Character} C - The character for which to sync the arousal data
|
|
* @return {void} - Nothing
|
|
*/
|
|
function ActivityChatRoomArousalSync(C) {
|
|
if ((C.IsPlayer()) && (CurrentScreen == "ChatRoom"))
|
|
ServerSend("ChatRoomCharacterArousalUpdate", { OrgasmTimer: C.ArousalSettings.OrgasmTimer, Progress: C.ArousalSettings.Progress, ProgressTimer: C.ArousalSettings.ProgressTimer, OrgasmCount: C.ArousalSettings.OrgasmCount });
|
|
}
|
|
|
|
/**
|
|
* Sets the character arousal level and validates the value
|
|
* @param {Character} C - The character for which to set the arousal progress of
|
|
* @param {number} Progress - Progress to set for the character (Ranges from 0 to 100)
|
|
* @return {void} - Nothing
|
|
*/
|
|
function ActivitySetArousal(C, Progress) {
|
|
if ((C.ArousalSettings.Progress == null) || (typeof C.ArousalSettings.Progress !== "number") || isNaN(C.ArousalSettings.Progress)) C.ArousalSettings.Progress = 0;
|
|
if ((Progress == null) || (Progress < 0)) Progress = 0;
|
|
if (Progress > 100) Progress = 100;
|
|
if (Progress == 0) C.ArousalSettings.OrgasmTimer = 0;
|
|
if (C.ArousalSettings.Progress != Progress) {
|
|
C.ArousalSettings.Progress = Progress;
|
|
C.ArousalSettings.ProgressTimer = 0;
|
|
ActivityChatRoomArousalSync(C);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets an activity progress on a timer, activities are capped at MaxProgress
|
|
* @param {Character} C - The character for which to set the timer for
|
|
* @param {null | Activity} Activity - The activity for which the timer is for
|
|
* @param {AssetGroupItemName | "ActivityOnOther"} Zone - The target zone of the activity
|
|
* @param {number} Progress - Progress to set
|
|
* @param {Asset} [Asset] - The asset used to perform the activity
|
|
* @return {void} - Nothing
|
|
*/
|
|
function ActivitySetArousalTimer(C, Activity, Zone, Progress, Asset) {
|
|
|
|
// If there's already a progress timer running, we add it's value but divide it by 2 to lessen the impact, the progress must be between -25 and 25
|
|
if ((C.ArousalSettings.ProgressTimer == null) || (typeof C.ArousalSettings.ProgressTimer !== "number") || isNaN(C.ArousalSettings.ProgressTimer)) C.ArousalSettings.ProgressTimer = 0;
|
|
Progress = Math.round((C.ArousalSettings.ProgressTimer / 2) + Progress);
|
|
if (Progress < -25) Progress = -25;
|
|
if (Progress > 25) Progress = 25;
|
|
|
|
// Make sure we do not allow orgasms if the activity (MaxProgress or MaxProgressSelf) or the zone (AllowOrgasm) doesn't allow it
|
|
let Max = ((Activity == null || Activity.MaxProgress == null) || (Activity.MaxProgress > 100)) ? 100 : Activity.MaxProgress;
|
|
if (Max > 95 && Zone !== "ActivityOnOther" && !PreferenceGetZoneOrgasm(C, Zone)) Max = 95;
|
|
// For activities on other, it cannot go over 2/3
|
|
if (Max > 67 && Zone === "ActivityOnOther") {
|
|
if (["PenetrateSlow", "PenetrateFast"].includes(Activity.Name) && Asset && Asset.Group.Name === "Pussy" && Asset.Name === "Penis") {
|
|
// If it's a penis penetration, don't cap it. This makes the cap either 100 or 95, depending on the character orgasm setting
|
|
Max = PreferenceGetZoneOrgasm(Player, "ItemVulva") ? 100 : 95;
|
|
} else {
|
|
Max = Activity.MaxProgressSelf != null ? Activity.MaxProgressSelf : 67;
|
|
}
|
|
}
|
|
|
|
if (Progress > 0 && (C.ArousalSettings.Progress + Progress) > Max)
|
|
Progress = (Max - C.ArousalSettings.Progress >= 0) ? Max - C.ArousalSettings.Progress : 0;
|
|
|
|
// If we must apply a progress timer change, we publish it
|
|
if (C.ArousalSettings.ProgressTimer !== Progress) {
|
|
C.ArousalSettings.ProgressTimer = Progress;
|
|
ActivityChatRoomArousalSync(C);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Draws the arousal progress bar at the given coordinates for every orgasm timer.
|
|
* @param {number} X - Position on the X axis
|
|
* @param {number} Y - Position on the Y axis
|
|
* @return {void} - Nothing
|
|
*/
|
|
function ActivityOrgasmProgressBar(X, Y) {
|
|
var Pos = 0;
|
|
if ((ActivityOrgasmGameTimer != null) && (ActivityOrgasmGameTimer > 0) && (CurrentTime < Player.ArousalSettings.OrgasmTimer))
|
|
Pos = ((Player.ArousalSettings.OrgasmTimer - CurrentTime) / ActivityOrgasmGameTimer) * 100;
|
|
if (Pos < 0) Pos = 0;
|
|
if (Pos > 100) Pos = 100;
|
|
DrawProgressBar(X, Y, 900, 25, Pos);
|
|
}
|
|
|
|
/**
|
|
* Ends the orgasm early if progress is close or progress is sufficient
|
|
* @return {void} - Nothing
|
|
*/
|
|
function ActivityOrgasmControl() {
|
|
if ((ActivityOrgasmGameTimer != null) && (ActivityOrgasmGameTimer > 0) && (CurrentTime < Player.ArousalSettings.OrgasmTimer)) {
|
|
// Ruin the orgasm
|
|
if (ActivityOrgasmGameProgress >= ActivityOrgasmGameDifficulty - 1 || CurrentTime > Player.ArousalSettings.OrgasmTimer - 500) {
|
|
if (CurrentScreen == "ChatRoom") {
|
|
if (CurrentTime > Player.ArousalSettings.OrgasmTimer - 500) {
|
|
if (Player.ArousalSettings.OrgasmStage == 0) {
|
|
if ((CurrentScreen == "ChatRoom"))
|
|
ChatRoomMessage({ Content: "OrgasmFailPassive" + (Math.floor(Math.random() * 3)).toString(), Type: "Action", Sender: Player.MemberNumber });
|
|
} else {
|
|
if ((CurrentScreen == "ChatRoom")) {
|
|
const Dictionary = new DictionaryBuilder()
|
|
.sourceCharacter(Player)
|
|
.build();
|
|
ServerSend("ChatRoomChat", { Content: "OrgasmFailTimeout" + (Math.floor(Math.random() * 3)).toString(), Type: "Activity", Dictionary: Dictionary });
|
|
ActivityChatRoomArousalSync(Player);
|
|
}
|
|
}
|
|
} else {
|
|
if ((CurrentScreen == "ChatRoom")) {
|
|
|
|
const Dictionary = new DictionaryBuilder()
|
|
.sourceCharacter(Player)
|
|
.build();
|
|
ServerSend("ChatRoomChat", { Content: ("OrgasmFailResist" + (Math.floor(Math.random() * 3))).toString(), Type: "Activity", Dictionary: Dictionary });
|
|
ActivityChatRoomArousalSync(Player);
|
|
}
|
|
}
|
|
}
|
|
ActivityOrgasmGameResistCount++;
|
|
ActivityOrgasmStop(Player, 65 + Math.ceil(Math.random()*20));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Increases the player's willpower when resisting an orgasm.
|
|
* @param {Character} C - The character currently resisting
|
|
* @return {void} - Nothing
|
|
*/
|
|
function ActivityOrgasmWillpowerProgress(C) {
|
|
if ((C.IsPlayer()) && (ActivityOrgasmGameProgress > 0)) {
|
|
SkillProgress(Player, "Willpower", ActivityOrgasmGameProgress);
|
|
ActivityOrgasmGameProgress = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts an orgasm for a given character, lasts between 5 to 15 seconds and can be displayed in a chatroom.
|
|
* @param {Character} C - Character for which an orgasm is starting
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ActivityOrgasmStart(C) {
|
|
if ((C.IsPlayer()) || C.IsNpc()) {
|
|
if (C.IsPlayer() && !ActivityOrgasmRuined) ActivityOrgasmGameResistCount = 0;
|
|
AsylumGGTSTOrgasm(C);
|
|
PrivateBedOrgasm(C);
|
|
ActivityOrgasmWillpowerProgress(C);
|
|
if (!ActivityOrgasmRuined) {
|
|
C.ArousalSettings.OrgasmTimer = CurrentTime + (Math.random() * 10000) + 5000;
|
|
C.ArousalSettings.OrgasmStage = 2;
|
|
C.ArousalSettings.OrgasmCount = (C.ArousalSettings.OrgasmCount == null) ? 1 : C.ArousalSettings.OrgasmCount + 1;
|
|
ActivityOrgasmGameTimer = C.ArousalSettings.OrgasmTimer - CurrentTime;
|
|
if ((C.IsPlayer()) && (CurrentScreen == "ChatRoom")) {
|
|
|
|
const Dictionary = new DictionaryBuilder()
|
|
.sourceCharacter(Player)
|
|
.build();
|
|
ServerSend("ChatRoomChat", { Content: "Orgasm" + (Math.floor(Math.random() * 10)).toString(), Type: "Activity", Dictionary: Dictionary });
|
|
ActivityChatRoomArousalSync(C);
|
|
}
|
|
} else {
|
|
ActivityOrgasmStop(Player, 65 + Math.ceil(Math.random() * 20));
|
|
if ((C.IsPlayer()) && (CurrentScreen == "ChatRoom")) {
|
|
let ChatModifier = C.ArousalSettings.OrgasmStage == 1 ? "Timeout" : "Surrender";
|
|
const Dictionary = new DictionaryBuilder()
|
|
.sourceCharacter(Player)
|
|
.build();
|
|
ServerSend("ChatRoomChat", { Content: ("OrgasmFail" + ChatModifier + (Math.floor(Math.random() * 3))).toString(), Type: "Activity", Dictionary: Dictionary });
|
|
ActivityChatRoomArousalSync(C);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Triggered when an orgasm needs to be stopped
|
|
* @param {Character} C - Character for which to stop the orgasm
|
|
* @param {number} Progress - Arousal level to set the character at once the orgasm ends
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ActivityOrgasmStop(C, Progress) {
|
|
if ((C.IsPlayer()) || C.IsNpc()) {
|
|
ActivityOrgasmWillpowerProgress(C);
|
|
C.ArousalSettings.OrgasmTimer = 0;
|
|
C.ArousalSettings.OrgasmStage = 0;
|
|
ActivitySetArousal(C, Progress);
|
|
ActivityTimerProgress(C, 0);
|
|
ActivityChatRoomArousalSync(C);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates an orgasm button and progresses in the orgasm mini-game. Handles the resets and success/failures
|
|
* @param {number} Progress - Progress of the currently running mini-game
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ActivityOrgasmGameGenerate(Progress) {
|
|
|
|
// If we must reset the mini-game
|
|
if (Progress == 0) {
|
|
Player.ArousalSettings.OrgasmStage = 1;
|
|
Player.ArousalSettings.OrgasmTimer = CurrentTime + 5000 + (SkillGetLevel(Player, "Willpower") * 1000);
|
|
ActivityOrgasmGameTimer = Player.ArousalSettings.OrgasmTimer - CurrentTime;
|
|
ActivityOrgasmGameDifficulty = (6 + (ActivityOrgasmGameResistCount * 2)) * (CommonIsMobile ? 1.5 : 1);
|
|
ActivityOrgasmGameDifficulty = ActivityOrgasmGameDifficulty + InventoryCraftCount(Player, "Arousing") * (CommonIsMobile ? 3 : 2);
|
|
ActivityOrgasmGameDifficulty = ActivityOrgasmGameDifficulty - InventoryCraftCount(Player, "Dull") * (CommonIsMobile ? 3 : 2);
|
|
}
|
|
|
|
// Runs the game or finish it if the threshold is reached, it can trigger a chatroom message for everyone to see
|
|
if (Progress >= ActivityOrgasmGameDifficulty) {
|
|
if (CurrentScreen == "ChatRoom") {
|
|
const Dictionary = new DictionaryBuilder()
|
|
.sourceCharacter(Player)
|
|
.build();
|
|
ServerSend("ChatRoomChat", { Content: "OrgasmResist" + (Math.floor(Math.random() * 10)).toString(), Type: "Activity", Dictionary: Dictionary });
|
|
AsylumGGTSOrgasmResist();
|
|
}
|
|
ActivityOrgasmGameResistCount++;
|
|
ActivityOrgasmStop(Player, 70);
|
|
} else {
|
|
ActivityOrgasmResistLabel = TextGet("OrgasmResist") + " (" + (ActivityOrgasmGameDifficulty - Progress).toString() + ")";
|
|
ActivityOrgasmGameProgress = Progress;
|
|
ActivityOrgasmGameButtonX = 50 + Math.floor(Math.random() * 650);
|
|
ActivityOrgasmGameButtonY = 50 + Math.floor(Math.random() * 836);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Triggers an orgasm for the player or an NPC which lasts from 5 to 15 seconds
|
|
* @param {Character} C - Character for which an orgasm was triggered
|
|
* @param {boolean} [Bypass=false] - If true, this will do a ruined orgasm rather than a real one
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ActivityOrgasmPrepare(C, Bypass) {
|
|
if (C.IsPlayer())
|
|
ActivityOrgasmRuined = false;
|
|
|
|
if (C.Effect.includes("DenialMode")) {
|
|
C.ArousalSettings.Progress = 99;
|
|
if (C.IsPlayer() && (Bypass || C.Effect.includes("RuinOrgasms"))) ActivityOrgasmRuined = true;
|
|
else return;
|
|
}
|
|
|
|
if (C.IsEdged() || ((InventoryCraftCount(Player, "Edging")) > 0)) {
|
|
C.ArousalSettings.Progress = 95;
|
|
if (C.IsPlayer() && Bypass) ActivityOrgasmRuined = true;
|
|
else return;
|
|
}
|
|
|
|
if (C.IsPlayer() && ActivityOrgasmRuined) {
|
|
ActivityOrgasmGameGenerate(0); // Resets the game
|
|
}
|
|
|
|
if ((C.IsPlayer()) || C.IsNpc()) {
|
|
|
|
// Starts the timer and exits from dialog if necessary
|
|
C.ArousalSettings.OrgasmTimer = (C.IsPlayer()) ? CurrentTime + 5000 : CurrentTime + (Math.random() * 10000) + 5000;
|
|
C.ArousalSettings.OrgasmStage = (C.IsPlayer()) ? 0 : 2;
|
|
if (C.IsNpc()) PrivateBedOrgasm(C);
|
|
if (C.IsPlayer()) ActivityOrgasmGameTimer = C.ArousalSettings.OrgasmTimer - CurrentTime;
|
|
if ((CurrentCharacter != null) && ((C.IsPlayer()) || (CurrentCharacter.ID == C.ID))) DialogLeave();
|
|
ActivityChatRoomArousalSync(C);
|
|
|
|
// If an NPC orgasmed, it will raise her love based on the horny trait
|
|
if (C.IsNpc())
|
|
if ((C.Love == null) || (C.Love < 60) || (C.IsOwner()) || (C.IsOwnedByPlayer()) || C.IsLoverOfPlayer())
|
|
NPCLoveChange(C, Math.floor((NPCTraitGet(C, "Horny") + 100) / 20) + 1);
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a character's facial expressions based on their arousal level if their settings allow it.
|
|
* @param {Character} C - Character for which to set the facial expressions
|
|
* @param {number} Progress - Current arousal progress
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ActivityExpression(C, Progress) {
|
|
|
|
// Floors the progress to the nearest 10 to pick the expression
|
|
Progress = Math.floor(Progress / 10) * 10;
|
|
|
|
// The blushes goes to red progressively
|
|
/** @type {null | ExpressionNameMap["Blush"]} */
|
|
var Blush = null;
|
|
if ((Progress == 10) || (Progress == 30) || (Progress == 50) || (Progress == 70)) Blush = "Low";
|
|
if ((Progress == 60) || (Progress == 80) || (Progress == 90)) Blush = "Medium";
|
|
if (Progress == 100) Blush = "High";
|
|
|
|
// The eyebrows position changes
|
|
/** @type {null | ExpressionNameMap["Eyebrows"]} */
|
|
var Eyebrows = null;
|
|
if ((Progress == 20) || (Progress == 30)) Eyebrows = "Raised";
|
|
if ((Progress == 50) || (Progress == 60)) Eyebrows = "Lowered";
|
|
if ((Progress == 80) || (Progress == 90)) Eyebrows = "Soft";
|
|
|
|
// Drool can activate at a few stages
|
|
/** @type {null | ExpressionNameMap["Fluids"]} */
|
|
var Fluids = null;
|
|
if ((Progress == 40) || (C.ArousalSettings.Progress == 70)) Fluids = "DroolLow";
|
|
if (Progress == 100) Fluids = "DroolMedium";
|
|
|
|
// Eyes can activate at a few stages
|
|
/** @type {null | ExpressionNameMap["Eyes"]} */
|
|
var Eyes = null;
|
|
if (Progress == 20) Eyes = "Dazed";
|
|
if (Progress == 70) Eyes = "Horny";
|
|
if (Progress == 90) Eyes = "Surprised";
|
|
if (Progress == 100) Eyes = "Closed";
|
|
|
|
// Find the expression in the character appearance and alters it
|
|
for (let A = 0; A < C.Appearance.length; A++) {
|
|
if (C.Appearance[A].Asset.Group.Name == "Blush") C.Appearance[A].Property = { Expression: Blush };
|
|
if (C.Appearance[A].Asset.Group.Name == "Eyebrows") C.Appearance[A].Property = { Expression: Eyebrows };
|
|
if (C.Appearance[A].Asset.Group.Name == "Fluids") C.Appearance[A].Property = { Expression: Fluids };
|
|
if (C.Appearance[A].Asset.Group.Name == "Eyes") C.Appearance[A].Property = { Expression: Eyes };
|
|
if (C.Appearance[A].Asset.Group.Name == "Eyes2") C.Appearance[A].Property = { Expression: Eyes };
|
|
}
|
|
|
|
// Penis gets hard at arousal 30+
|
|
let Penis = InventoryGet(C, "Pussy");
|
|
if ((Penis != null) && (Penis.Asset != null) && (Penis.Asset.Name == "Penis")) {
|
|
if (Progress < 30) Penis.Property = { Expression: null };
|
|
else Penis.Property = { Expression: "Hard" };
|
|
}
|
|
|
|
// Refreshes the character
|
|
CharacterRefresh(C, false);
|
|
|
|
}
|
|
|
|
/**
|
|
* With time, we increase or decrease the arousal. Validates the result to keep it within 0 to 100 and triggers an orgasm when it reaches 100
|
|
* @param {Character} C - Character for which the timer is progressing
|
|
* @param {number} Progress - Progress made (from -100 to 100)
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ActivityTimerProgress(C, Progress) {
|
|
|
|
// Changes the current arousal progress value
|
|
C.ArousalSettings.Progress = C.ArousalSettings.Progress + Progress;
|
|
// Decrease the vibratorlevel to 0 if not being aroused, while also updating the change time to reset the vibrator animation
|
|
if (Progress < 0) {
|
|
if (C.ArousalSettings.VibratorLevel != 0) {
|
|
C.ArousalSettings.VibratorLevel = 0;
|
|
C.ArousalSettings.ChangeTime = CommonTime();
|
|
}
|
|
}
|
|
|
|
if (C.ArousalSettings.Progress < 0) C.ArousalSettings.Progress = 0;
|
|
if (C.ArousalSettings.Progress > 100) C.ArousalSettings.Progress = 100;
|
|
|
|
// Update the recent change time, so that on other player's screens the character's arousal meter will vibrate again when vibes start
|
|
if (C.ArousalSettings.Progress == 0) {
|
|
C.ArousalSettings.ChangeTime = CommonTime();
|
|
}
|
|
|
|
// Out of orgasm mode, it can affect facial expressions at every 10 steps
|
|
if ((C.ArousalSettings.OrgasmTimer == null) || (typeof C.ArousalSettings.OrgasmTimer !== "number") || isNaN(C.ArousalSettings.OrgasmTimer) || (C.ArousalSettings.OrgasmTimer < CurrentTime))
|
|
if (((C.ArousalSettings.AffectExpression == null) || C.ArousalSettings.AffectExpression) && ((C.ArousalSettings.Progress + ((Progress < 0) ? 1 : 0)) % 10 == 0))
|
|
ActivityExpression(C, C.ArousalSettings.Progress);
|
|
|
|
// Can trigger an orgasm
|
|
if (C.ArousalSettings.Progress == 100) ActivityOrgasmPrepare(C);
|
|
|
|
}
|
|
|
|
/**
|
|
* Set the current vibrator level for drawing purposes
|
|
* @param {Character} C - Character for which the timer is progressing
|
|
* @param {0 | 1 | 2 | 3 | 4} Level - Level from 0 to 4 (higher = more vibration)
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ActivityVibratorLevel(C, Level) {
|
|
if (C.ArousalSettings != null) {
|
|
if (Level != C.ArousalSettings.VibratorLevel) {
|
|
C.ArousalSettings.VibratorLevel = Level;
|
|
C.ArousalSettings.ChangeTime = CommonTime();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Calculates the progress one character does on another right away
|
|
* @param {Character} Source - The character who performed the activity
|
|
* @param {Character} Target - The character on which the activity was performed
|
|
* @param {Activity} Activity - The activity performed
|
|
* @param {AssetGroup} Group - The group on which the activity is performed
|
|
* @param {Asset} [Asset] - The asset used to perform the activity
|
|
* @returns {void} - Nothing
|
|
*/
|
|
function ActivityRunSelf(Source, Target, Activity, Group, Asset) {
|
|
if (((Player.ArousalSettings.Active == "Hybrid") || (Player.ArousalSettings.Active == "Automatic")) && Source.IsPlayer() && !Target.IsPlayer()) {
|
|
var Factor = (PreferenceGetActivityFactor(Player, Activity.Name, false) * 5) - 10; // Check how much the player likes the activity, from -10 to +10
|
|
Factor = Factor + Math.floor((Math.random() * 8)); // Random 0 to 7 bonus
|
|
if (Target.IsLoverOfPlayer()) Factor = Factor + Math.floor((Math.random() * 8)); // Another random 0 to 7 bonus if the target is the player's lover
|
|
ActivitySetArousalTimer(Player, Activity, "ActivityOnOther", Factor, Asset);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build the chat tag needed for lookup in ActivityDictionary.csv
|
|
* @param {Character} character
|
|
* @param {AssetGroup} group
|
|
* @param {Activity} activity
|
|
*/
|
|
function ActivityBuildChatTag(character, group, activity, is_label = false) {
|
|
const groupMap = {"ItemVulva":"ItemPenis", "ItemVulvaPiercings": "ItemGlans"};
|
|
const realGroup = character.HasPenis() && groupMap[group.Name] ? groupMap[group.Name] : group.Name;
|
|
|
|
return `${is_label ? "Label-" : ""}${(character.IsPlayer() ? "ChatSelf" : "ChatOther")}-${realGroup}-${activity.Name}`;
|
|
}
|
|
|
|
/**
|
|
* Launches a sexual activity for a character and sends the chatroom message if applicable.
|
|
* @param {Character} actor - Character which is performing the activity
|
|
* @param {Character} acted - Character on which the activity was triggered
|
|
* @param {AssetItemGroup} targetGroup - The group targetted by the activity
|
|
* @param {ItemActivity} ItemActivity - The activity performed, with its optional item used
|
|
* @param {boolean} sendMessage - Whether to send a message to the chat or not
|
|
*/
|
|
function ActivityRun(actor, acted, targetGroup, ItemActivity, sendMessage=true) {
|
|
const Activity = ItemActivity.Activity;
|
|
const UsedAsset = ItemActivity && ItemActivity.Item ? ItemActivity.Item.Asset : null;
|
|
|
|
let group = ActivityGetGroupOrMirror(acted.AssetFamily, targetGroup.Name);
|
|
// If the player does the activity on herself or an NPC, we calculate the result right away
|
|
if ((acted.ArousalSettings.Active == "Hybrid") || (acted.ArousalSettings.Active == "Automatic"))
|
|
if (acted.IsPlayer() || acted.IsNpc())
|
|
ActivityEffect(actor, acted, Activity, group.Name, 0, UsedAsset);
|
|
|
|
if (acted.IsPlayer()) {
|
|
if (Activity.MakeSound) {
|
|
PropertyAutoPunishHandled = new Set();
|
|
}
|
|
}
|
|
if (actor.IsPlayer()) {
|
|
PropertyPunishActivityCache.add(Activity.Name);
|
|
}
|
|
|
|
// If the player does the activity on someone else, we calculate the progress for the player right away
|
|
ActivityRunSelf(actor, acted, Activity, group, UsedAsset);
|
|
|
|
// The text result can be outputted in the chatroom or in the NPC dialog
|
|
if (CurrentScreen == "ChatRoom" && sendMessage) {
|
|
|
|
// Publishes the activity to the chatroom
|
|
const dictionary = new DictionaryBuilder()
|
|
.sourceCharacter(actor)
|
|
.targetCharacter(acted)
|
|
.focusGroup(group.Name);
|
|
if (ItemActivity.Item) {
|
|
dictionary.asset(ItemActivity.Item.Asset, "ActivityAsset", ItemActivity.Item.Craft && ItemActivity.Item.Craft.Name);
|
|
}
|
|
|
|
const Dictionary = dictionary.build();
|
|
Dictionary.push({ ActivityName: Activity.Name });
|
|
ServerSend("ChatRoomChat", { Content: ActivityBuildChatTag(acted, group, Activity), Type: "Activity", Dictionary });
|
|
|
|
// If the activity is a stimulation trigger, run it if the target is the player
|
|
if (acted.IsPlayer() && Activity.StimulationAction) {
|
|
ChatRoomStimulationMessage(Activity.StimulationAction);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a used asset should trigger an activity/arousal progress on the target character
|
|
* @param {Character} Source - The character who used the item
|
|
* @param {Character} Target - The character on which the item was used
|
|
* @param {Asset} Asset - Asset used
|
|
* @return {void} - Nothing
|
|
*/
|
|
function ActivityArousalItem(Source, Target, Asset) {
|
|
var AssetActivity = Asset.DynamicActivity(Source);
|
|
if (AssetActivity != null) {
|
|
var Activity = AssetGetActivity(Target.AssetFamily, AssetActivity);
|
|
if (Source.IsPlayer() && !Target.IsPlayer()) ActivityRunSelf(Source, Target, Activity, Asset.Group);
|
|
if (PreferenceArousalAtLeast(Target, "Hybrid") && (Target.IsPlayer() || Target.IsNpc()))
|
|
ActivityEffect(Source, Target, AssetActivity, /** @type {AssetGroupItemName} */ (Asset.Group.Name));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the character is wearing an item tagged with the fetish type name and returns the love factor for it
|
|
* @param {Character} C - The character to validate
|
|
* @param {FetishName} Type - The fetish type name
|
|
* @return {number} - From -2 (hate it) to 2 (adore it) based on the player preferences
|
|
*/
|
|
function ActivityFetishItemFactor(C, Type) {
|
|
const Factor = (PreferenceGetFetishFactor(C, Type) - 2);
|
|
if (Factor === 0) {
|
|
return Factor;
|
|
}
|
|
|
|
for (const item of C.Appearance) {
|
|
const fetish = [
|
|
...InventoryGetItemProperty(item, "Fetish"),
|
|
...(item.Asset.Fetish || []),
|
|
];
|
|
if (fetish.includes(Type)) {
|
|
return Factor;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Loops in all fetishes for a character and calculates the total fetish factor
|
|
* @param {Character} C - The character to validate
|
|
* @return {number} - The negative/positive number will have negative/positive impact on arousal
|
|
*/
|
|
function ActivityFetishFactor(C) {
|
|
var Factor = 0;
|
|
if ((C.ArousalSettings != null) && (C.ArousalSettings.Fetish != null) && (typeof C.ArousalSettings.Fetish == "string"))
|
|
for (let F of FetishFemale3DCG)
|
|
if (PreferenceGetFetishFactor(C, F.Name) != 2)
|
|
Factor = Factor + F.GetFactor(C);
|
|
return Factor;
|
|
}
|