mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2026-04-26 11:31:00 +00:00
Merge branch 'ts/strictify' into 'master'
More TS-strictification See merge request BondageProjects/Bondage-College!6125
This commit is contained in:
commit
f306359d62
70 changed files with 905 additions and 749 deletions
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
const BackgroundsStringsPath = "Backgrounds/Backgrounds.csv";
|
||||
|
|
@ -319,7 +318,7 @@ const BackgroundsList = [
|
|||
* @returns {string}
|
||||
*/
|
||||
function BackgroundsTextGet(msg) {
|
||||
return TextAllScreenCache.get(BackgroundsStringsPath).get(msg);
|
||||
return TextAllScreenCache.get(BackgroundsStringsPath)?.get(msg) ?? "MISSING BACKGROUND CACHE";
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
var BackgroundSelectionBackground = "Introduction";
|
||||
|
|
@ -7,15 +6,21 @@ var BackgroundSelectionList = [];
|
|||
/** @type {BackgroundTag[]} */
|
||||
var BackgroundSelectionTagList = [];
|
||||
var BackgroundSelectionIndex = 0;
|
||||
/** @type {string | null} */
|
||||
var BackgroundSelectionSelect = null;
|
||||
/** @type {string} */
|
||||
var BackgroundSelectionSelect = /** @type {never} */ (null);
|
||||
var BackgroundSelectionSize = 12;
|
||||
var BackgroundSelectionOffset = 0;
|
||||
/** @type {null | ((selection: string, setBackground: boolean) => void)} */
|
||||
var BackgroundSelectionCallback = null;
|
||||
/** @type {never} */
|
||||
/**
|
||||
* @type {never}
|
||||
* @deprecated
|
||||
*/
|
||||
var BackgroundSelectionReturnScreen;
|
||||
/** @type {never} */
|
||||
/**
|
||||
* @type {never}
|
||||
* @deprecated
|
||||
*/
|
||||
var BackgroundSelectionAll;
|
||||
/** @type {string[]} */
|
||||
var BackgroundSelectionView = [];
|
||||
|
|
@ -81,7 +86,7 @@ async function BackgroundSelectionLoad() {
|
|||
parent: document.body,
|
||||
});
|
||||
|
||||
TextScreenCache.loadedPromise.then(() => {
|
||||
TextScreenCache?.loadedPromise.then(() => {
|
||||
const searchFilter = ElementCreateSearchInput(
|
||||
Background.elementID.searchFilter,
|
||||
() => BackgroundSelectionList.map(i => BackgroundsTextGet(i)).sort(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
//#region VARIABLES
|
||||
var FriendListBackground = "BrickWall";
|
||||
|
|
@ -420,7 +419,7 @@ function FriendListBeep(MemberNumber, data = null) {
|
|||
if (FriendListBeepTarget === -1) {
|
||||
ElementCreateDiv(FriendListIDs.beepList);
|
||||
}
|
||||
const FriendListBeepElement = document.getElementById(FriendListIDs.beepList);
|
||||
const FriendListBeepElement = /** @type {HTMLElement} */ (document.getElementById(FriendListIDs.beepList));
|
||||
const beepTitle = data === null ? 'Send Beep' : data.Sent ? 'Sent Beep' : 'Received Beep';
|
||||
const userName = Player.FriendNames.get(MemberNumber) ?? data?.MemberName;
|
||||
const userCaption = `${userName} [${MemberNumber}]`;
|
||||
|
|
@ -504,7 +503,10 @@ function FriendListBeep(MemberNumber, data = null) {
|
|||
'Reply'
|
||||
],
|
||||
eventListeners: {
|
||||
click: () => FriendListBeep(data?.MemberNumber)
|
||||
click: () => {
|
||||
if (typeof data?.MemberNumber === "number")
|
||||
FriendListBeep(data.MemberNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -554,7 +556,7 @@ function FriendListBeepMenuSend() {
|
|||
*/
|
||||
async function FriendListShowBeep(i) {
|
||||
const beep = FriendListBeepLog[i];
|
||||
if (!beep) return;
|
||||
if (typeof beep?.MemberNumber !== "number") return;
|
||||
FriendListModeIndex = 1;
|
||||
await FriendListShow();
|
||||
FriendListBeep(beep.MemberNumber, beep);
|
||||
|
|
@ -564,10 +566,10 @@ async function FriendListShowBeep(i) {
|
|||
//#region FRIEND LIST
|
||||
/**
|
||||
* Exits the friendlist
|
||||
* @param {string} room The room to search for
|
||||
* @param {string | undefined} room The room to search for
|
||||
*/
|
||||
async function FriendListChatSearch(room) {
|
||||
if (FriendListReturn?.Screen !== "ChatSearch") return;
|
||||
if (FriendListReturn?.Screen !== "ChatSearch" || !room) return;
|
||||
// XXX: can't use `ChatSearchQuery(room)` here, as `ChatSearchLoad` will trigger a query
|
||||
// before us, which will eat the timeout and cause the empty search to win. So just
|
||||
// overwrite the underlying search string so it searches for that
|
||||
|
|
@ -626,9 +628,10 @@ function FriendListLoadFriendList(data) {
|
|||
};
|
||||
const sortingSymbol = FriendListSortingDirection === "Asc" ? "↑" : "↓";
|
||||
const friendListScrollPercent = ElementGetScrollPercentage(FriendListIDs.friendList) || 0;
|
||||
const friendList = document.getElementById(FriendListIDs.friendList);
|
||||
const friendList = /** @type {HTMLElement} */ (document.getElementById(FriendListIDs.friendList));
|
||||
friendList.innerHTML = "";
|
||||
|
||||
/** @type {HTMLDivElement[]} */
|
||||
const FriendListContent = [];
|
||||
|
||||
const mode = FriendListMode[FriendListModeIndex];
|
||||
|
|
@ -659,7 +662,7 @@ function FriendListLoadFriendList(data) {
|
|||
"friend-list-relation-type": "RelationType",
|
||||
};
|
||||
CommonEntries(columnHeaders).forEach(([id, modeName]) => {
|
||||
const elem = document.getElementById(id);
|
||||
const elem = /** @type {HTMLElement} */ (document.getElementById(id));
|
||||
const elemSortingSymbol = FriendListSortingMode === modeName ? sortingSymbol : "↕";
|
||||
elem.textContent = `${TextGet(modeName)} ${elemSortingSymbol}`;
|
||||
switch (elemSortingSymbol) {
|
||||
|
|
@ -682,8 +685,8 @@ function FriendListLoadFriendList(data) {
|
|||
for (const friend of data) {
|
||||
const originalChatRoomName = friend.ChatRoomName || '';
|
||||
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`);
|
||||
const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined);
|
||||
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatSearchGetSpace() === (friend.ChatRoomSpace || '');
|
||||
const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') ?? "");
|
||||
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatSearchGetSpace() === (friend.ChatRoomSpace ?? "");
|
||||
const canBeep = true;
|
||||
|
||||
friendRawData.push({
|
||||
|
|
@ -711,9 +714,9 @@ function FriendListLoadFriendList(data) {
|
|||
for (let i = FriendListBeepLog.length - 1; i >= 0; i--) {
|
||||
const beepData = FriendListBeepLog[i];
|
||||
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${beepData.ChatRoomSpace || "F"}`);
|
||||
const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined);
|
||||
const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') ?? "");
|
||||
let beepCaption = '';
|
||||
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatSearchGetSpace() === (beepData.ChatRoomSpace || '');
|
||||
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatSearchGetSpace() === (beepData.ChatRoomSpace ?? "");
|
||||
|
||||
const rawBeepCaption = [];
|
||||
if (beepData.Sent) {
|
||||
|
|
@ -731,7 +734,7 @@ function FriendListLoadFriendList(data) {
|
|||
friendRawData.push({
|
||||
memberName: beepData.MemberName,
|
||||
memberNumber: beepData.MemberNumber,
|
||||
relationType: FriendListGetRelationType(beepData.MemberNumber),
|
||||
relationType: FriendListGetRelationType(beepData.MemberNumber ?? 0),
|
||||
pending: false,
|
||||
chatRoom: {
|
||||
name: beepData.ChatRoomName,
|
||||
|
|
@ -799,7 +802,7 @@ function FriendListLoadFriendList(data) {
|
|||
tag: "td",
|
||||
classList: ['friend-list-column', 'MemberNumber'],
|
||||
children: [
|
||||
friend.memberNumber.toString()
|
||||
`${friend.memberNumber ?? ""}`
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
@ -917,7 +920,7 @@ function FriendListLoadFriendList(data) {
|
|||
innerHTML: friend.chatRoom.caption,
|
||||
style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined },
|
||||
eventListeners: {
|
||||
click: () => FriendListChatSearch(friend.chatRoom.name),
|
||||
click: () => FriendListChatSearch(friend.chatRoom?.name),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -929,7 +932,10 @@ function FriendListLoadFriendList(data) {
|
|||
row.append(
|
||||
ElementButton.Create(
|
||||
`friend-list-beep-${friend.memberNumber}`,
|
||||
() => FriendListBeep(friend.memberNumber),
|
||||
() => {
|
||||
if (typeof friend.memberNumber === "number")
|
||||
FriendListBeep(friend.memberNumber);
|
||||
},
|
||||
{ noStyling: true },
|
||||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content'],
|
||||
|
|
@ -939,7 +945,7 @@ function FriendListLoadFriendList(data) {
|
|||
),
|
||||
ElementButton.Create(
|
||||
`friend-list-show-beep-${friend.memberNumber}`,
|
||||
() => FriendListShowBeep(friend.beep.beepIndex),
|
||||
() => { if (friend.beep?.beepIndex) FriendListShowBeep(friend.beep.beepIndex); },
|
||||
{ noStyling: true },
|
||||
{
|
||||
button: {
|
||||
|
|
@ -983,7 +989,10 @@ function FriendListLoadFriendList(data) {
|
|||
|
||||
row.appendChild(ElementButton.Create(
|
||||
`friend-list-delete-${friend.memberNumber}`,
|
||||
() => FriendListDelete(friend.memberNumber),
|
||||
() => {
|
||||
if (typeof friend.memberNumber === "number")
|
||||
FriendListDelete(friend.memberNumber);
|
||||
},
|
||||
{ noStyling: true },
|
||||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
|
||||
|
|
@ -1044,6 +1053,7 @@ function FriendListAddFriends() {
|
|||
return;
|
||||
};
|
||||
|
||||
/** @type {number[]} */
|
||||
const addedMembers = [];
|
||||
memberNumbers.forEach((memberNumber) => {
|
||||
if (!CommonIsNonNegativeInteger(memberNumber)) return;
|
||||
|
|
@ -1074,8 +1084,8 @@ function FriendListChangeMode(modeIndex) {
|
|||
else if (FriendListModeIndex >= FriendListMode.length) FriendListModeIndex = 0;
|
||||
FriendListSortingMode = 'None';
|
||||
FriendListSortingDirection = 'Asc';
|
||||
document.getElementById(FriendListIDs.root).dataset.mode = FriendListMode[FriendListModeIndex];
|
||||
document.getElementById(FriendListIDs.modeTitle).textContent = TextGet(FriendListMode[FriendListModeIndex]);
|
||||
document.getElementById(FriendListIDs.root)?.setAttribute("data-mode", FriendListMode[FriendListModeIndex]);
|
||||
document.getElementById(FriendListIDs.modeTitle)?.replaceChildren(TextGet(FriendListMode[FriendListModeIndex]));
|
||||
ServerSend("AccountQuery", { Query: "OnlineFriends" });
|
||||
}
|
||||
|
||||
|
|
@ -1092,8 +1102,8 @@ function FriendListSort(sortingMode, sortingDirection) {
|
|||
|
||||
const items = friendlist.children;
|
||||
const sortedItems = Array.from(items).sort((elmA, elmB) => {
|
||||
const contentA = elmA.querySelector(`.${sortingMode}`)?.textContent;
|
||||
const contentB = elmB.querySelector(`.${sortingMode}`)?.textContent;
|
||||
const contentA = elmA.querySelector(`.${sortingMode}`)?.textContent ?? "";
|
||||
const contentB = elmB.querySelector(`.${sortingMode}`)?.textContent ?? "";
|
||||
const numberA = Number.parseInt(contentA, 10);
|
||||
const numberB = Number.parseInt(contentB, 10);
|
||||
if (!isNaN(numberA) && !isNaN(numberB)) {
|
||||
|
|
@ -1119,9 +1129,9 @@ function FriendListSearchByProperties(text) {
|
|||
const items = friendlist.children;
|
||||
Array.from(items).forEach((element) => element.toggleAttribute("hidden", true));
|
||||
const searchedItems = Array.from(items).filter(item => {
|
||||
return item.querySelector('.MemberName')?.textContent.toLowerCase().includes(text.toLowerCase()) ||
|
||||
item.querySelector('.MemberNickname')?.textContent.toLowerCase().includes(text.toLowerCase()) || // NYI
|
||||
item.querySelector('.MemberNumber')?.textContent.includes(text);
|
||||
return (item.querySelector('.MemberName')?.textContent ?? "").toLowerCase().includes(text.toLowerCase()) ||
|
||||
(item.querySelector('.MemberNickname')?.textContent ?? "").toLowerCase().includes(text.toLowerCase()) || // NYI
|
||||
(item.querySelector('.MemberNumber')?.textContent ?? "").includes(text);
|
||||
});
|
||||
searchedItems.forEach((item) => {
|
||||
item.toggleAttribute("hidden", false);
|
||||
|
|
@ -1142,7 +1152,7 @@ function FriendListSearchByProperties(text) {
|
|||
* @returns {string} - The innerHTML with the searched text highlighted
|
||||
*/
|
||||
function FriendListHighlightProperty(element, text) {
|
||||
const textContent = element.textContent;
|
||||
const textContent = element.textContent ?? "";
|
||||
if (!text) return textContent;
|
||||
const regex = new RegExp(text.toLowerCase(), 'gi');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
var InformationSheetBackground = "Sheet";
|
||||
/** @type {null | Character | NPCCharacter} */
|
||||
/**
|
||||
* The character we're showing the information of.
|
||||
*
|
||||
* Also used by OnlineProfile.js
|
||||
* @type {null | Character | NPCCharacter}
|
||||
*/
|
||||
var InformationSheetSelection = null;
|
||||
/** @type {ScreenSpecifier | null} */
|
||||
var InformationSheetReturnScreen = null;
|
||||
|
|
@ -12,6 +16,8 @@ var InformationSheetSecondScreen = false;
|
|||
* @type {ScreenLoadHandler}
|
||||
*/
|
||||
async function InformationSheetLoad() {
|
||||
if (!InformationSheetSelection) throw new Error("No character selected");
|
||||
|
||||
TextPrefetch("Character", "FriendList");
|
||||
TextPrefetch("Character", "Preference");
|
||||
TextPrefetch("Character", "Title");
|
||||
|
|
@ -72,7 +78,7 @@ async function InformationSheetLoad() {
|
|||
* @returns {void} - Nothing
|
||||
*/
|
||||
function InformationSheetRun() {
|
||||
|
||||
if (!InformationSheetSelection) return;
|
||||
// Draw the character base values
|
||||
const C = InformationSheetSelection;
|
||||
const CurrentTitle = TitleGet(C);
|
||||
|
|
@ -102,7 +108,7 @@ function InformationSheetRun() {
|
|||
|
||||
currentY += spacingLarge;
|
||||
|
||||
if ((C.IsPlayer() || C.IsOnline()) && C.Creation !== null) {
|
||||
if ((C.IsPlayer() || C.IsOnline()) && C.Creation !== undefined) {
|
||||
const memberLabel = TextGet(C.IsBirthday() ? "Birthday" : "MemberFor");
|
||||
const creationFormatted = CommonFormatDurationRange(CurrentTime, C.Creation, { showFull: true, includeYears: true, includeMonths: true, includeDays: true });
|
||||
const memberForLabel = `${memberLabel} ${creationFormatted}`;
|
||||
|
|
@ -145,18 +151,19 @@ function InformationSheetRun() {
|
|||
}
|
||||
currentY += spacing;
|
||||
|
||||
const Love = C.Love ?? 0;
|
||||
let relationshipQualifier = "";
|
||||
if (C.Love >= 100) relationshipQualifier = "RelationshipPerfect";
|
||||
else if (C.Love >= 75) relationshipQualifier = "RelationshipGreat";
|
||||
else if (C.Love >= 50) relationshipQualifier = "RelationshipGood";
|
||||
else if (C.Love >= 25) relationshipQualifier = "RelationshipFair";
|
||||
else if (C.Love > -25) relationshipQualifier = "RelationshipNeutral";
|
||||
else if (C.Love > -50) relationshipQualifier = "RelationshipPoor";
|
||||
else if (C.Love > -75) relationshipQualifier = "RelationshipBad";
|
||||
else if (C.Love > -100) relationshipQualifier = "RelationshipHorrible";
|
||||
if (Love >= 100) relationshipQualifier = "RelationshipPerfect";
|
||||
else if (Love >= 75) relationshipQualifier = "RelationshipGreat";
|
||||
else if (Love >= 50) relationshipQualifier = "RelationshipGood";
|
||||
else if (Love >= 25) relationshipQualifier = "RelationshipFair";
|
||||
else if (Love > -25) relationshipQualifier = "RelationshipNeutral";
|
||||
else if (Love > -50) relationshipQualifier = "RelationshipPoor";
|
||||
else if (Love > -75) relationshipQualifier = "RelationshipBad";
|
||||
else if (Love > -100) relationshipQualifier = "RelationshipHorrible";
|
||||
else relationshipQualifier = "RelationshipAtrocious";
|
||||
|
||||
let loveLine = TextGet("Relationship") + " " + C.Love.toString() + " " + TextGet(relationshipQualifier);
|
||||
let loveLine = TextGet("Relationship") + " " + Love.toString() + " " + TextGet(relationshipQualifier);
|
||||
DrawTextFit(loveLine, 550, currentY, 450, "Black", "Gray");
|
||||
currentY += spacing;
|
||||
}
|
||||
|
|
@ -254,18 +261,19 @@ function InformationSheetRun() {
|
|||
const lovership = C.GetLovership();
|
||||
if (lovership.length < 1) DrawText(TextGet("None"), 1200, 200, "Black", "Gray");
|
||||
for (let [L, lover] of lovership.entries()) {
|
||||
const stageText = stageQualifier[lover.Stage];
|
||||
const loveStart = lover.Start ?? 0;
|
||||
const stageText = stageQualifier[lover.Stage ?? 0];
|
||||
const relationStageLabel = `${TextGet(`${stageText}With`)} ${lover.Name}${lover.MemberNumber ? ` (${lover.MemberNumber})` : ""}`;
|
||||
DrawTextFit(relationStageLabel, 1200, 200 + L * 150, 600, "Black", "Gray");
|
||||
const relationDurationLabel = `${TextGet("For")} ${CommonFormatDurationRange(CurrentTime, lover.Start, { showFull: true, includeYears: true, includeMonths: true, includeDays: true })}`;
|
||||
const relationDurationLabel = `${TextGet("For")} ${CommonFormatDurationRange(CurrentTime, loveStart, { showFull: true, includeYears: true, includeMonths: true, includeDays: true })}`;
|
||||
DrawTextFit(relationDurationLabel, 1200, 260 + L * 150, 600, "Black", "Gray");
|
||||
const hoverY = 260 + L * 150 - 20;
|
||||
if (MouseIn(1200, hoverY, 600, 40)) {
|
||||
const relationStartDate = new Date(lover.Start).toLocaleString(undefined, {
|
||||
const relationStartDate = new Date(loveStart).toLocaleString(undefined, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
const relationDurationLabelShort = CommonFormatDurationRange(CurrentTime, lover.Start, { showFull: true });
|
||||
const relationDurationLabelShort = CommonFormatDurationRange(CurrentTime, loveStart, { showFull: true });
|
||||
|
||||
DrawHoverElements.push(() => {
|
||||
DrawButtonHover(1200, hoverY, 450, 40, `${relationStartDate} – ${relationDurationLabelShort}`);
|
||||
|
|
@ -287,21 +295,22 @@ function InformationSheetRun() {
|
|||
currentY += spacing;
|
||||
}
|
||||
if (playerLove) {
|
||||
const stageText = stageQualifier[playerLove.Stage];
|
||||
const stageText = stageQualifier[playerLove.Stage ?? 0];
|
||||
const loveStart = playerLove.Start ?? 0;
|
||||
|
||||
const loverStageLabel = `${TextGet(`${stageText}With`)} ${C.LoverName()}`;
|
||||
DrawText(loverStageLabel, 550, currentY, "Black", "Gray");
|
||||
currentY += spacing;
|
||||
const loverDurationLabel = `${TextGet("For")} ${CommonFormatDurationRange(CurrentTime, playerLove.Start, { showFull: true, includeYears: true, includeMonths: true, includeDays: true })}`;
|
||||
const loverDurationLabel = `${TextGet("For")} ${CommonFormatDurationRange(CurrentTime, loveStart, { showFull: true, includeYears: true, includeMonths: true, includeDays: true })}`;
|
||||
DrawText(loverDurationLabel, 550, currentY, "Black", "Gray");
|
||||
const y = currentY - 20;
|
||||
if (MouseIn(550, y, 450, 40))
|
||||
DrawHoverElements.push(() => {
|
||||
const loverStartDate = new Date(playerLove.Start).toLocaleString(undefined, {
|
||||
const loverStartDate = new Date(loveStart).toLocaleString(undefined, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
const loverDurationTooltip = `${loverStartDate} – ${CommonFormatDurationRange(CurrentTime, playerLove.Start, { showFull: true })}`;
|
||||
const loverDurationTooltip = `${loverStartDate} – ${CommonFormatDurationRange(CurrentTime, loveStart, { showFull: true })}`;
|
||||
DrawButtonHover(550, y, 450, 40, loverDurationTooltip);
|
||||
});
|
||||
currentY += spacing;
|
||||
|
|
@ -338,11 +347,10 @@ function InformationSheetRun() {
|
|||
// After one week we show the traits, after two weeks we show the level
|
||||
if (CurrentTime >= NPCEventGet(C, "PrivateRoomEntry") * CheatFactor("AutoShowTraits", 0) + 604800000) {
|
||||
let Pos = 0;
|
||||
for (let T = 0; T < C.Trait.length; T++)
|
||||
if ((C.Trait[T].Value != null) && (C.Trait[T].Value != 0)) {
|
||||
DrawText(TextGet("Trait" + ((C.Trait[T].Value > 0) ? C.Trait[T].Name : NPCTraitReverse(C.Trait[T].Name))) + " " + ((CurrentTime >= NPCEventGet(C, "PrivateRoomEntry") * CheatFactor("AutoShowTraits", 0) + 1209600000) ? Math.abs(C.Trait[T].Value).toString() : "??"), 1000, 200 + Pos * 75, "Black", "Gray");
|
||||
Pos++;
|
||||
}
|
||||
for (const trait of C.Trait ?? []) {
|
||||
DrawText(TextGet("Trait" + ((trait.Value > 0) ? trait.Name : NPCTraitReverse(trait.Name))) + " " + ((CurrentTime >= NPCEventGet(C, "PrivateRoomEntry") * CheatFactor("AutoShowTraits", 0) + 1209600000) ? Math.abs(trait.Value).toString() : "??"), 1000, 200 + Pos * 75, "Black", "Gray");
|
||||
Pos++;
|
||||
}
|
||||
} else DrawText(TextGet("TraitUnknown"), 1000, 200, "Black", "Gray");
|
||||
|
||||
}
|
||||
|
|
@ -355,9 +363,9 @@ function InformationSheetRun() {
|
|||
* @returns {void} - Nothing
|
||||
*/
|
||||
function InformationSheetSecondScreenRun() {
|
||||
|
||||
if (!InformationSheetSelection) return;
|
||||
// For current player and online characters
|
||||
var C = InformationSheetSelection;
|
||||
const C = InformationSheetSelection;
|
||||
if (C.IsPlayer() || C.IsOnline()) {
|
||||
const lineHeight = 55;
|
||||
// Draw the reputation section
|
||||
|
|
@ -417,7 +425,8 @@ function InformationSheetSecondScreenRun() {
|
|||
* @returns {void} - Nothing
|
||||
*/
|
||||
function InformationSheetClick() {
|
||||
var C = InformationSheetSelection;
|
||||
if (!InformationSheetSelection) return;
|
||||
const C = InformationSheetSelection;
|
||||
if (MouseIn(1815, 75, 90, 90)) InformationSheetExit();
|
||||
if (C.IsPlayer()) {
|
||||
if (MouseIn(1815, 190, 90, 90) && !TitleIsForced(TitleGet(C))) CommonSetScreen("Character", "Title");
|
||||
|
|
@ -467,6 +476,7 @@ function InformationSheetLoadCharacter(C) {
|
|||
}
|
||||
|
||||
function InformationSheetResize() {
|
||||
if (!InformationSheetSelection) return;
|
||||
const C = InformationSheetSelection;
|
||||
if (C.IsPlayer()) {
|
||||
ElementSetPosition("AllowedInteractions-dropdown-container", 550, 800);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
var LoginBackground = "Dressing";
|
||||
/**
|
||||
|
|
@ -77,7 +76,7 @@ var LoginErrorMessage = "";
|
|||
* The dummy on the login screen.
|
||||
*
|
||||
* Lifetime bound to the screen.
|
||||
* @type {NPCCharacter} */
|
||||
* @type {NPCCharacter | null} */
|
||||
var LoginCharacter;
|
||||
|
||||
/* DEBUG: To measure FPS - uncomment this and change the + 4000 to + 40
|
||||
|
|
@ -108,10 +107,11 @@ const LoginIDs = Object.freeze({
|
|||
*/
|
||||
function LoginDoNextThankYou() {
|
||||
LoginThankYou = CommonRandomItemFromList(LoginThankYou, LoginThankYouList);
|
||||
CharacterRelease(LoginCharacter, false);
|
||||
CharacterAppearanceFullRandom(LoginCharacter);
|
||||
if (InventoryGet(LoginCharacter, "ItemNeck") != null) InventoryRemove(LoginCharacter, "ItemNeck", false);
|
||||
CharacterFullRandomRestrain(LoginCharacter);
|
||||
const char = /** @type {NPCCharacter} */ (LoginCharacter);
|
||||
CharacterRelease(char, false);
|
||||
CharacterAppearanceFullRandom(char);
|
||||
if (InventoryGet(char, "ItemNeck") != null) InventoryRemove(char, "ItemNeck", false);
|
||||
CharacterFullRandomRestrain(char);
|
||||
LoginThankYouNext = CommonTime() + 4000;
|
||||
}
|
||||
|
||||
|
|
@ -343,7 +343,7 @@ async function LoginLoad() {
|
|||
ActivityDictionaryLoad();
|
||||
AssetLoadDescription("Female3DCG");
|
||||
const timer = TimerCreate(() => {
|
||||
TextScreenCache.loadedPromise.then(() => {
|
||||
TextScreenCache?.loadedPromise.then(() => {
|
||||
LoginReloadLanguageText();
|
||||
timer?.();
|
||||
});
|
||||
|
|
@ -387,16 +387,17 @@ function LoginRun() {
|
|||
// Draw the login controls
|
||||
const status = ElementWrap(LoginIDs.status);
|
||||
const statusNewText = LoginGetStatus() ?? TextGet("EnterNamePassword");
|
||||
if (status.textContent !== statusNewText) {
|
||||
if (status && status.textContent !== statusNewText) {
|
||||
status.textContent = statusNewText;
|
||||
}
|
||||
ElementWrap(LoginIDs.login).toggleAttribute("disabled", !CanLogin);
|
||||
ElementWrap(LoginIDs.register).toggleAttribute("disabled", !CanLogin);
|
||||
ElementWrap(LoginIDs.passwordReset).toggleAttribute("disabled", !CanLogin);
|
||||
ElementWrap(LoginIDs.cheats).style.display = CheatAllow ? "" : "none";
|
||||
ElementWrap(LoginIDs.login)?.toggleAttribute("disabled", !CanLogin);
|
||||
ElementWrap(LoginIDs.register)?.toggleAttribute("disabled", !CanLogin);
|
||||
ElementWrap(LoginIDs.passwordReset)?.toggleAttribute("disabled", !CanLogin);
|
||||
const cheatElem = ElementWrap(LoginIDs.cheats);
|
||||
if (cheatElem) cheatElem.style.display = CheatAllow ? "" : "none";
|
||||
|
||||
// Draw the character and thank you bubble
|
||||
DrawCharacter(LoginCharacter, 1400, 100, 0.9);
|
||||
DrawCharacter(/** @type {NPCCharacter} */ (LoginCharacter), 1400, 100, 0.9);
|
||||
if (LoginThankYouNext < CommonTime()) LoginDoNextThankYou();
|
||||
DrawImage("Screens/" + CurrentModule + "/" + CurrentScreen + "/Bubble.png", 1400, 16);
|
||||
DrawText(TextGet("ThankYou") + " " + LoginThankYou, 1625, 53, "Black", "Gray");
|
||||
|
|
@ -445,19 +446,19 @@ function LoginUnload() {
|
|||
ElementRemove(LoginIDs.cheats);
|
||||
ElementRemove(LoginIDs.language);
|
||||
|
||||
CharacterDelete(LoginCharacter);
|
||||
CharacterDelete(/** @type {NPCCharacter} */ (LoginCharacter));
|
||||
LoginCharacter = null;
|
||||
}
|
||||
|
||||
/** @type {ScreenFunctions["Resize"]} */
|
||||
/** @type {ScreenResizeHandler} */
|
||||
function LoginResize(load) {
|
||||
ElementPositionFixed(LoginIDs.welcome, 500, 50, 1000, null);
|
||||
ElementPositionFixed(LoginIDs.status, 500, 100, 1000, null);
|
||||
ElementPositionFixed(LoginIDs.welcome, 500, 50, 1000);
|
||||
ElementPositionFixed(LoginIDs.status, 500, 100, 1000);
|
||||
|
||||
ElementPositionFixed(LoginIDs.nameLabel, 500, 200, 1000, null);
|
||||
ElementPositionFixed(LoginIDs.name, 750, 260, 500, null);
|
||||
ElementPositionFixed(LoginIDs.passwordLabel, 500, 330, 1000, null);
|
||||
ElementPositionFixed(LoginIDs.password, 750, 390, 500, null);
|
||||
ElementPositionFixed(LoginIDs.nameLabel, 500, 200, 1000);
|
||||
ElementPositionFixed(LoginIDs.name, 750, 260, 500);
|
||||
ElementPositionFixed(LoginIDs.passwordLabel, 500, 330, 1000);
|
||||
ElementPositionFixed(LoginIDs.password, 750, 390, 500);
|
||||
|
||||
ElementSetPosition(LoginIDs.login, 1000, 490);
|
||||
ElementSetFontSize(LoginIDs.login, "auto");
|
||||
|
|
@ -470,16 +471,42 @@ function LoginResize(load) {
|
|||
}
|
||||
|
||||
function LoginReloadLanguageText() {
|
||||
ElementWrap(LoginIDs.welcome).textContent = TextGet("Welcome");
|
||||
ElementWrap(LoginIDs.status).textContent = LoginGetStatus() ?? TextGet("EnterNamePassword");
|
||||
ElementWrap(LoginIDs.nameLabel).textContent = TextGet("AccountName");
|
||||
ElementWrap(LoginIDs.passwordLabel).textContent = TextGet("Password");
|
||||
|
||||
ElementWrap(LoginIDs.newCharacter).textContent = TextGet("CreateNewCharacter");
|
||||
ElementWrap(LoginIDs.login).querySelector("span").textContent = TextGet("Login");
|
||||
ElementWrap(LoginIDs.register).querySelector("span").textContent = TextGet("NewCharacter");
|
||||
ElementWrap(LoginIDs.passwordReset).querySelector("span").textContent = TextGet("PasswordReset");
|
||||
ElementWrap(LoginIDs.cheats).querySelector("span").textContent = TextGet("Cheats");
|
||||
const welcome = ElementWrap(LoginIDs.welcome);
|
||||
if (welcome) {
|
||||
welcome.textContent = TextGet("Welcome");
|
||||
}
|
||||
const status = ElementWrap(LoginIDs.status);
|
||||
if (status) {
|
||||
status.textContent = LoginGetStatus() ?? TextGet("EnterNamePassword");
|
||||
}
|
||||
const nameLabel = ElementWrap(LoginIDs.nameLabel);
|
||||
if (nameLabel) {
|
||||
nameLabel.textContent = TextGet("AccountName");
|
||||
}
|
||||
const passwordLabel = ElementWrap(LoginIDs.passwordLabel);
|
||||
if (passwordLabel) {
|
||||
passwordLabel.textContent = TextGet("Password");
|
||||
}
|
||||
const newCharacter = ElementWrap(LoginIDs.newCharacter);
|
||||
if (newCharacter) {
|
||||
newCharacter.textContent = TextGet("CreateNewCharacter");
|
||||
}
|
||||
const login = ElementWrap(LoginIDs.login)?.querySelector("span");
|
||||
if (login) {
|
||||
login.textContent = TextGet("Login");
|
||||
}
|
||||
const register = ElementWrap(LoginIDs.register)?.querySelector("span");
|
||||
if (register) {
|
||||
register.textContent = TextGet("NewCharacter");
|
||||
}
|
||||
const passwordReset = ElementWrap(LoginIDs.passwordReset)?.querySelector("span");
|
||||
if (passwordReset) {
|
||||
passwordReset.textContent = TextGet("PasswordReset");
|
||||
}
|
||||
const cheats = ElementWrap(LoginIDs.cheats)?.querySelector("span");
|
||||
if (cheats) {
|
||||
cheats.textContent = TextGet("Cheats");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -659,7 +686,7 @@ function LoginPerformAppearanceFixups(Appearance) {
|
|||
|
||||
/**
|
||||
* Perform the crafting fixups needed
|
||||
* @param {readonly CraftingItem[]} Crafting - The server-provided, uncompressed crafting data
|
||||
* @param {readonly (CraftingItem | null)[]} Crafting - The server-provided, uncompressed crafting data
|
||||
*/
|
||||
function LoginPerformCraftingFixups(Crafting) {
|
||||
if (!Crafting || !CommonIsArray(Crafting)) return;
|
||||
|
|
@ -904,6 +931,7 @@ function LoginDifficulty(applyDefaults) {
|
|||
function LoginExtremeItemSettings(applyDefaults) {
|
||||
const LimitedAssets = new Set(MainHallStrongLocks.map(i => `ItemMisc/${i}`));
|
||||
for (const [name, permission] of CommonEntries(Player.PermissionItems)) {
|
||||
if (!permission) continue;
|
||||
permission.Hidden = false;
|
||||
const limitedAllowed = LimitedAssets.has(name);
|
||||
|
||||
|
|
@ -996,7 +1024,7 @@ function LoginSetupPlayer(C) {
|
|||
FullRooms: true,
|
||||
ShowLocked: true,
|
||||
SearchDescriptions: false,
|
||||
MapTypes: undefined,
|
||||
MapTypes: "",
|
||||
RoomMinSize: 2,
|
||||
RoomMaxSize: 20,
|
||||
FilterTerms: "",
|
||||
|
|
@ -1004,12 +1032,14 @@ function LoginSetupPlayer(C) {
|
|||
if (C.RoomSearchLanguage != null) {
|
||||
C.ChatSearchSettings.Language = C.RoomSearchLanguage;
|
||||
C.RoomSearchLanguage = undefined;
|
||||
// @ts-ignore Strict-TS: This ought to be deleted server-side
|
||||
ServerAccountUpdate.QueueData({ RoomSearchLanguage: null });
|
||||
}
|
||||
updateSearchSettings = true;
|
||||
}
|
||||
if (C.ChatSearchFilterTerms) {
|
||||
C.ChatSearchSettings.FilterTerms = C.ChatSearchFilterTerms;
|
||||
// @ts-ignore Strict-TS: This ought to be deleted server-side
|
||||
ServerAccountUpdate.QueueData({ ChatSearchFilterTerms: null });
|
||||
updateSearchSettings = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ function OnlineProfileLoadTextArea(element) {
|
|||
* @type {ScreenLoadHandler}
|
||||
*/
|
||||
async function OnlineProfileLoad() {
|
||||
if (!InformationSheetSelection) {
|
||||
throw new Error('Missing "InformationSheetSelection" data');
|
||||
}
|
||||
OnlineProfileTextDesc = typeof InformationSheetSelection.Description === "string" ? InformationSheetSelection.Description : "";
|
||||
OnlineProfileTextOwnersNotes = typeof InformationSheetSelection.Ownership?.Notes === "string" ? InformationSheetSelection.Ownership.Notes : "";
|
||||
OnlineProfileNotesAvailable = InformationSheetSelection.IsFullyOwnedByPlayer() || OnlineProfileTextOwnersNotes != "";
|
||||
|
|
@ -85,6 +88,7 @@ function OnlineProfileUnload() {
|
|||
* @returns {void} - Nothing
|
||||
*/
|
||||
function OnlineProfileRun() {
|
||||
if (!InformationSheetSelection) return;
|
||||
// Sets the screen controls
|
||||
let legend = "";
|
||||
let maxlen = 0;
|
||||
|
|
@ -122,6 +126,7 @@ function OnlineProfileRun() {
|
|||
* @returns {void} - Nothing
|
||||
*/
|
||||
function OnlineProfileClick() {
|
||||
if (!InformationSheetSelection) return;
|
||||
if (OnlineProfileNotesAvailable && MouseIn(1620, 60, 90, 90)) {
|
||||
/* Toggle between Description and Owner's Notes. */
|
||||
const ev = ElementValue("DescriptionInput").trim();
|
||||
|
|
@ -149,6 +154,7 @@ function OnlineProfileClick() {
|
|||
* @returns {void} - Nothing
|
||||
*/
|
||||
function OnlineProfileExit(Save=false) {
|
||||
if (!InformationSheetSelection) return;
|
||||
if (Save) {
|
||||
const ev = ElementValue("DescriptionInput").trim();
|
||||
if (OnlineProfileMode == "Description") {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ArousalActiveName[]} */
|
||||
|
|
@ -10,8 +9,11 @@ var PreferenceArousalVisibleIndex = 0;
|
|||
/** @type {ArousalAffectStutterName[]} */
|
||||
var PreferenceArousalAffectStutterList = ["None", "Arousal", "Vibration", "All"];
|
||||
var PreferenceArousalAffectStutterIndex = 0;
|
||||
/** @type {null | ActivityName[]} */
|
||||
var PreferenceArousalActivityList = null;
|
||||
/**
|
||||
* Initialized by {@link PreferenceSubscreenArousalLoad}
|
||||
* @type {ActivityName[]}
|
||||
*/
|
||||
var PreferenceArousalActivityList;
|
||||
var PreferenceArousalActivityIndex = 0;
|
||||
/** @type {never} */
|
||||
var PreferenceArousalActivityFactorSelf;
|
||||
|
|
@ -19,8 +21,11 @@ var PreferenceArousalActivityFactorSelf;
|
|||
var PreferenceArousalActivityFactorOther;
|
||||
/** @type {never} */
|
||||
var PreferenceArousalZoneFactor;
|
||||
/** @type {null | FetishName[]} */
|
||||
var PreferenceArousalFetishList = null;
|
||||
/**
|
||||
* Initialized by {@link PreferenceSubscreenArousalLoad}
|
||||
* @type {FetishName[]}
|
||||
*/
|
||||
var PreferenceArousalFetishList;
|
||||
var PreferenceArousalFetishIndex = 0;
|
||||
/** @type {never} */
|
||||
var PreferenceArousalFetishFactor;
|
||||
|
|
@ -202,8 +207,10 @@ function PreferenceSubscreenArousalClick() {
|
|||
if ((Player.FocusGroup != null) && MouseIn(550, 853, 600, 64)) {
|
||||
const step = MouseX <= 850 ? -1 : +1;
|
||||
const zone = PreferenceGetArousalZone(Player, Player.FocusGroup.Name);
|
||||
const factor = /** @type {ArousalFactor} */ (CommonModulo(zone.Factor + step, 5));
|
||||
PreferenceSetArousalZone(Player, zone.Name, factor);
|
||||
if (zone) {
|
||||
const factor = /** @type {ArousalFactor} */ (CommonModulo(zone.Factor + step, 5));
|
||||
PreferenceSetArousalZone(Player, zone.Name, factor);
|
||||
}
|
||||
}
|
||||
|
||||
// Arousal zone orgasm check box
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const PreferenceExtensionsIDs = Object.freeze({
|
|||
function PreferenceSubscreenExtensionsLoad() {
|
||||
PreferenceExtensionsDisplay = Object.keys(PreferenceExtensionsSettings).map(
|
||||
k => (
|
||||
s=>({
|
||||
s => ({
|
||||
Button: typeof s.ButtonText === "function" ? s.ButtonText() : s.ButtonText,
|
||||
Image: s.Image && (typeof s.Image === "function" ? s.Image() : s.Image),
|
||||
click: () => {
|
||||
|
|
|
|||
|
|
@ -171,7 +171,6 @@ function PreferenceSubscreenGeneralRun() {
|
|||
MainCanvas.textAlign = "left";
|
||||
if (PreferenceMessage != "") DrawText(TextGet(PreferenceMessage), 920, 125, "Red", "Black");
|
||||
MainCanvas.textAlign = "center";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,12 +15,15 @@ var PreferenceGraphicsFontList = ["Arial", "TimesNewRoman", "Papyrus", "ComicSan
|
|||
/** @type {WebGLPowerPreference[]} */
|
||||
var PreferenceGraphicsPowerModes = ["low-power", "default", "high-performance"];
|
||||
var PreferenceGraphicsFontIndex = 0;
|
||||
/** @type {null | number} */
|
||||
var PreferenceGraphicsAnimationQualityIndex = null;
|
||||
/** @type {null | number} */
|
||||
var PreferenceGraphicsPowerModeIndex = null;
|
||||
/** @type {null | WebGLContextAttributes} */
|
||||
var PreferenceGraphicsWebGLOptions = null;
|
||||
/** @type {number} */
|
||||
var PreferenceGraphicsAnimationQualityIndex = -1;
|
||||
/** @type {number} */
|
||||
var PreferenceGraphicsPowerModeIndex = -1;
|
||||
/**
|
||||
* Tied to the screen's lifetime
|
||||
* @type {WebGLContextAttributes}
|
||||
*/
|
||||
var PreferenceGraphicsWebGLOptions;
|
||||
var PreferenceGraphicsAnimationQualityList = [10000, 2000, 200, 100, 50, 0];
|
||||
var PreferenceGraphicsFrameLimit = [0, 10, 15, 30, 60];
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ function PreferenceSubscreenNotificationsRun() {
|
|||
* @returns {void} - Nothing
|
||||
*/
|
||||
function PreferenceNotificationsDrawSetting(Left, Top, Text, Setting) {
|
||||
DrawBackNextButton(Left, Top, 164, 64, TextGet("NotificationsAlertType" + Setting.AlertType.toString()), "White", null, () => "", () => "");
|
||||
DrawBackNextButton(Left, Top, 164, 64, TextGet("NotificationsAlertType" + Setting.AlertType.toString()), "White", undefined, () => "", () => "");
|
||||
const Enabled = Setting.AlertType > 0;
|
||||
if (Enabled) {
|
||||
DrawButton(Left + 200, Top, 64, 64, "", "White", "Icons/Audio" + Setting.Audio.toString() + ".png");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {null | string[]} */
|
||||
var PreferenceOnlineDefaultBackgroundList = null;
|
||||
/** @type {string[]} */
|
||||
var PreferenceOnlineDefaultBackgroundList = /** @type {never} */ (null);
|
||||
var PreferenceOnlineDefaultBackgroundIndex = 0;
|
||||
var PreferenceOnlineDefaultBackground = "";
|
||||
|
||||
|
|
@ -75,7 +74,7 @@ function PreferenceSubscreenOnlineLoad() {
|
|||
dropdown
|
||||
]
|
||||
});
|
||||
ElementWrap(PreferenceIDs.subscreen).append(grid);
|
||||
ElementWrap(PreferenceIDs.subscreen)?.append(grid);
|
||||
|
||||
const subtitle = ElementCreate({
|
||||
tag: "label",
|
||||
|
|
@ -105,7 +104,7 @@ function PreferenceSubscreenOnlineLoad() {
|
|||
selection
|
||||
]
|
||||
});
|
||||
ElementWrap(PreferenceIDs.subscreen).append(grid2);
|
||||
ElementWrap(PreferenceIDs.subscreen)?.append(grid2);
|
||||
}
|
||||
/**
|
||||
* Sets the online preferences for the player. Redirected to from the main Run function if the player is in the online
|
||||
|
|
@ -116,8 +115,8 @@ function PreferenceSubscreenOnlineRun() {
|
|||
DrawCharacter(Player, 50, 50, 0.9);
|
||||
MainCanvas.textAlign = "center";
|
||||
PreferencePageChangeDraw(1595, 75, 2);
|
||||
ElementWrap(PreferenceSubscreenOnlineIDs.grid).toggleAttribute("hidden", PreferencePageCurrent !== 1);
|
||||
ElementWrap(PreferenceSubscreenOnlineIDs.grid2).toggleAttribute("hidden", PreferencePageCurrent !== 2);
|
||||
ElementWrap(PreferenceSubscreenOnlineIDs.grid)?.toggleAttribute("hidden", PreferencePageCurrent !== 1);
|
||||
ElementWrap(PreferenceSubscreenOnlineIDs.grid2)?.toggleAttribute("hidden", PreferencePageCurrent !== 2);
|
||||
|
||||
if (PreferencePageCurrent === 2) {
|
||||
DrawImageResize("Backgrounds/" + PreferenceOnlineDefaultBackground + ".jpg", 500, 210, 300, 185);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
/**
|
||||
* The background to use for the settings screen
|
||||
|
|
@ -14,9 +13,9 @@ var PreferenceMessage = "";
|
|||
/**
|
||||
* The currently active subscreen
|
||||
*
|
||||
* @type {PreferenceSubscreen?}
|
||||
* @type {PreferenceSubscreen | null}
|
||||
*/
|
||||
var PreferenceSubscreen = null;
|
||||
var PreferenceSubscreen;
|
||||
|
||||
/**
|
||||
* All the base settings screens
|
||||
|
|
@ -239,10 +238,14 @@ function PreferenceRun() {
|
|||
// Backward-compatibility: automatically substitute strings for the actual subscreen
|
||||
if (typeof PreferenceSubscreen === "string") {
|
||||
const subscreenName = PreferenceSubscreen === "" ? "Main" : PreferenceSubscreen;
|
||||
PreferenceSubscreen = PreferenceSubscreens.find(s => s.name === subscreenName);
|
||||
if (!PreferenceSubscreen) PreferenceSubscreen = PreferenceSubscreens.find(s => s.name === "Main");
|
||||
const screen = PreferenceSubscreens.find(s => s.name === subscreenName);
|
||||
if (screen) {
|
||||
PreferenceSubscreen = screen;
|
||||
} else {
|
||||
PreferenceSubscreen = /** @type {PreferenceSubscreen} */ (PreferenceSubscreens.find(s => s.name === "Main"));
|
||||
}
|
||||
}
|
||||
PreferenceSubscreen.run();
|
||||
PreferenceSubscreen?.run();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -262,7 +265,7 @@ function PreferenceClick() {
|
|||
* @type {ScreenExitHandler}
|
||||
*/
|
||||
function PreferenceExit() {
|
||||
if (PreferenceSubscreen.name !== "Main") {
|
||||
if (PreferenceSubscreen?.name !== "Main") {
|
||||
// If we are in a subscreen, the only exit is to the main preference screen
|
||||
PreferenceSubscreenExit();
|
||||
return;
|
||||
|
|
@ -307,12 +310,12 @@ function PreferenceUnload() {
|
|||
/** @type {ScreenResizeHandler} */
|
||||
function PreferenceResize(onLoad) {
|
||||
PreferenceSubscreenResize?.(onLoad);
|
||||
PreferenceSubscreen.resize?.(onLoad);
|
||||
PreferenceSubscreen?.resize?.(onLoad);
|
||||
}
|
||||
|
||||
/** @type {KeyboardEventListener} */
|
||||
function PreferenceKeyUp(event) {
|
||||
// @ts-expect-error: TS, please stop pretending that `void` and `undefined` are distinct
|
||||
// @ts-ignore Strict-TS: TS, please stop pretending that `void` and `undefined` are distinct
|
||||
return PreferenceSubscreen?.keyUp?.(event) ?? false;
|
||||
}
|
||||
|
||||
|
|
@ -334,6 +337,7 @@ function PreferenceSubscreenCreateSubscreen(subscreenName) {
|
|||
return subscreen;
|
||||
}
|
||||
|
||||
/** @type {ScreenResizeHandler} */
|
||||
function PreferenceSubscreenResize(onLoad) {
|
||||
ElementPositionFixed(PreferenceIDs.subscreen, 0, 0, 2000, 1000);
|
||||
ElementPositionFixed(PreferenceIDs.exit, 1815, 75, 90, 90);
|
||||
|
|
@ -344,7 +348,7 @@ function PreferenceSubscreenResize(onLoad) {
|
|||
* Exit from a specific subscreen by running its handler and checking its validity
|
||||
*/
|
||||
async function PreferenceSubscreenExit() {
|
||||
const validExit = await PreferenceSubscreen.exit?.();
|
||||
const validExit = await PreferenceSubscreen?.exit?.();
|
||||
|
||||
// Only when the results is false (not undefined)
|
||||
// The exit is just a exit of the subscreen's substate, return to block more exit.
|
||||
|
|
@ -431,8 +435,7 @@ function PreferenceGetNextIndex(List, Index) {
|
|||
* @namespace
|
||||
*/
|
||||
var PreferenceActivityEnjoymentDefault = {
|
||||
/** @type {ActivityName | undefined} */
|
||||
Name: undefined,
|
||||
Name: /** @type {never} */ (undefined),
|
||||
/** @type {ArousalFactor} */
|
||||
Self: 2,
|
||||
/** @type {ArousalFactor} */
|
||||
|
|
@ -445,8 +448,7 @@ var PreferenceActivityEnjoymentDefault = {
|
|||
* @namespace
|
||||
*/
|
||||
var PreferenceArousalFetishDefault = {
|
||||
/** @type {FetishName | undefined} */
|
||||
Name: undefined,
|
||||
Name: /** @type {never} */ (undefined),
|
||||
/** @type {ArousalFactor} */
|
||||
Factor: 2,
|
||||
};
|
||||
|
|
@ -457,8 +459,7 @@ var PreferenceArousalFetishDefault = {
|
|||
* @namespace
|
||||
*/
|
||||
var PreferenceArousalZoneDefault = {
|
||||
/** @type {AssetGroupItemName | undefined} */
|
||||
Name: undefined,
|
||||
Name: /** @type {never} */ (undefined),
|
||||
/** @type {ArousalFactor} */
|
||||
Factor: 2,
|
||||
/** @type {boolean} */
|
||||
|
|
@ -506,7 +507,9 @@ function PreferenceArousalUpdateValidation() {
|
|||
const activities = AssetAllActivities("Female3DCG")
|
||||
.filter(a => a.ActivityID != null)
|
||||
.map(({ Name }) => ({ ...PreferenceActivityEnjoymentDefault, Name }))
|
||||
.sort(({ Name: aName }, { Name: bName }) => AssetGetActivity("Female3DCG", aName).ActivityID - AssetGetActivity("Female3DCG", bName).ActivityID);
|
||||
.sort(({ Name: aName }, { Name: bName }) =>
|
||||
// @ts-ignore Strict-TS: We're guaranteed to only have known activities
|
||||
AssetGetActivity("Female3DCG", aName).ActivityID - AssetGetActivity("Female3DCG", bName).ActivityID);
|
||||
PreferenceArousalSettingsDefault.Activity = activities
|
||||
.map((act) => PreferenceArousalActivityToChar(act.Self, act.Other))
|
||||
.join("");
|
||||
|
|
@ -519,7 +522,9 @@ function PreferenceArousalUpdateValidation() {
|
|||
Orgasm: PreferenceArousalZoneOrgasmDefault.includes(group.Name),
|
||||
}))
|
||||
.filter(({ Name }) => Name !== undefined)
|
||||
.sort(({ Name: aName }, { Name: bName }) => AssetGroupGet("Female3DCG", aName).ArousalZoneID - AssetGroupGet("Female3DCG", bName).ArousalZoneID);
|
||||
.sort(({ Name: aName }, { Name: bName }) =>
|
||||
// @ts-ignore Strict-TS: We're guaranteed to only have arousal groups in there
|
||||
AssetGroupGet("Female3DCG", aName).ArousalZoneID - AssetGroupGet("Female3DCG", bName).ArousalZoneID);
|
||||
PreferenceArousalSettingsDefault.Zone = zones
|
||||
.map(act => PreferenceArousalZoneToChar(act.Factor, act.Orgasm))
|
||||
.join("");
|
||||
|
|
@ -527,7 +532,9 @@ function PreferenceArousalUpdateValidation() {
|
|||
const fetishes = AssetAllFetishes("Female3DCG")
|
||||
.filter(f => f.FetishID != null)
|
||||
.map(({ Name }) => ({ ...PreferenceArousalFetishDefault, Name }))
|
||||
.sort(({ Name: aName }, { Name: bName }) => AssetGetFetish("Female3DCG", aName).FetishID - AssetGetFetish("Female3DCG", bName).FetishID);
|
||||
.sort(({ Name: aName }, { Name: bName }) =>
|
||||
// @ts-ignore Strict-TS: We're guaranteed to only have known fetishes
|
||||
AssetGetFetish("Female3DCG", aName).FetishID - AssetGetFetish("Female3DCG", bName).FetishID);
|
||||
PreferenceArousalSettingsDefault.Fetish = fetishes
|
||||
.map(fet => PreferenceArousalFetishToChar(fet.Factor))
|
||||
.join("");
|
||||
|
|
@ -614,7 +621,7 @@ var PreferenceArousalSettingsValidate = {
|
|||
if (!CommonIsObject(oldZone) || typeof oldZone.Name !== "string" || typeof oldZone.Factor !== "number" || typeof oldZone.Orgasm !== "boolean") continue;
|
||||
|
||||
const group = AssetGroupGet(C.AssetFamily, oldZone.Name);
|
||||
if (!group || !group.IsItem()) return;
|
||||
if (!group || !group.IsItem() || group.ArousalZoneID === undefined) continue;
|
||||
|
||||
newZone = newZone.substring(0, group.ArousalZoneID) + PreferenceArousalZoneToChar(oldZone.Factor, oldZone.Orgasm) + newZone.substring(group.ArousalZoneID + 1);
|
||||
}
|
||||
|
|
@ -801,7 +808,7 @@ var PreferenceChatSettingsValidate = {
|
|||
|
||||
/**
|
||||
* Namespace with default values for {@link VisualSettingsType} properties.
|
||||
* @type {Required<VisualSettingsType>}
|
||||
* @type {VisualSettingsType}
|
||||
* @namespace
|
||||
*/
|
||||
var PreferenceVisualSettingsDefault = {
|
||||
|
|
@ -1017,8 +1024,6 @@ var PreferenceOnlineSettingsValidate = {
|
|||
DefaultChatRoomBackground: (arg, C) => {
|
||||
return typeof arg === "string" ? arg : PreferenceOnlineSettingsDefault.DefaultChatRoomBackground;
|
||||
},
|
||||
// @ts-expect-error Deprecated
|
||||
SearchShowsFullRooms: (arg) => undefined,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
const PreferenceSubscreenRestrictionIDs = Object.freeze({
|
||||
|
|
@ -22,7 +21,7 @@ function PreferenceSubscreenRestrictionLoad() {
|
|||
attributes: { id: PreferenceSubscreenRestrictionIDs.hint },
|
||||
children: [hintText]
|
||||
});
|
||||
ElementWrap(PreferenceIDs.subscreen).append(hintLabel);
|
||||
ElementWrap(PreferenceIDs.subscreen)?.append(hintLabel);
|
||||
|
||||
const pairs = settingsList.map(s => {
|
||||
return ElementCheckbox.CreateLabelled(s, TextGet(`Restriction${s}`),
|
||||
|
|
@ -46,7 +45,7 @@ function PreferenceSubscreenRestrictionLoad() {
|
|||
]
|
||||
});
|
||||
|
||||
ElementWrap(PreferenceIDs.subscreen).append(grid);
|
||||
ElementWrap(PreferenceIDs.subscreen)?.append(grid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
const PreferenceSubscreenSecurityIDs = Object.freeze({
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {{ Group: AssetGroup, Assets: { Asset: Asset, Hidden: boolean, Blocked: boolean, Limited: boolean}[]}[]} */
|
||||
/** @type {{ Group: AssetGroup, Assets: { Asset: Asset, Hidden: boolean, Blocked: boolean, Limited: boolean }[]}[]} */
|
||||
var PreferenceVisibilityGroupList = [];
|
||||
var PreferenceVisibilityGroupIndex = 0;
|
||||
var PreferenceVisibilityAssetIndex = 0;
|
||||
var PreferenceVisibilityHideChecked = false;
|
||||
var PreferenceVisibilityBlockChecked = false;
|
||||
var PreferenceVisibilityCanBlock = true;
|
||||
/** @type {null | Asset} */
|
||||
var PreferenceVisibilityPreviewAsset = null;
|
||||
/**
|
||||
* Bound to screen lifetime
|
||||
* @type {Asset}
|
||||
*/
|
||||
var PreferenceVisibilityPreviewAsset;
|
||||
var PreferenceVisibilityResetClicked = false;
|
||||
/** @type {Partial<Record<`${AssetGroupName}/${string}`, ItemPermissions>>} */
|
||||
var PreferenceVisibilityRecord = {};
|
||||
|
|
@ -19,7 +21,7 @@ var PreferenceVisibilityRecord = {};
|
|||
* @returns {void} - Nothing
|
||||
*/
|
||||
function PreferenceSubscreenVisibilityLoad() {
|
||||
ElementWrap(PreferenceIDs.exit).hidden = true;
|
||||
ElementWrap(PreferenceIDs.exit)?.toggleAttribute("hidden", true);
|
||||
PreferenceVisibilityRecord = { ...Player.PermissionItems };
|
||||
PreferenceVisibilityGroupList = [];
|
||||
const hideableGroups = AssetGroup.filter(g => AssetGroupIsHideable(g));
|
||||
|
|
@ -140,7 +142,7 @@ function PreferenceSubscreenVisibilityClick() {
|
|||
// Reset button
|
||||
if (MouseIn(500, PreferenceVisibilityResetClicked ? 780 : 700, 300, 64)) {
|
||||
if (PreferenceVisibilityResetClicked) {
|
||||
Object.values(Player.PermissionItems).forEach(i => i.Hidden = false);
|
||||
Object.values(Player.PermissionItems).forEach(i => { if (i) i.Hidden = false; });
|
||||
PreferenceVisibilityExit(true);
|
||||
}
|
||||
else PreferenceVisibilityResetClicked = true;
|
||||
|
|
@ -150,11 +152,12 @@ function PreferenceSubscreenVisibilityClick() {
|
|||
|
||||
// Confirm button
|
||||
if (MouseIn(1720, 60, 90, 90)) {
|
||||
CommonEntries(PreferenceVisibilityRecord).forEach(([key, permission]) => {
|
||||
Player.PermissionItems[key] ??= permission;
|
||||
for (const [key, permission] of CommonEntries(PreferenceVisibilityRecord)) {
|
||||
if (!permission) continue;
|
||||
Player.PermissionItems[key] ??= PreferencePermissionGetDefault();
|
||||
Player.PermissionItems[key].Hidden = permission.Hidden;
|
||||
Player.PermissionItems[key].Permission = permission.Permission;
|
||||
});
|
||||
}
|
||||
PreferenceVisibilityExit(true);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
var TitleBackground = "Sheet";
|
||||
/** @type {null | TitleName} */
|
||||
var TitleSelectedTitle = null;
|
||||
/**
|
||||
* Bound to screen lifetime
|
||||
* @type {TitleName}
|
||||
*/
|
||||
var TitleSelectedTitle;
|
||||
/** @type {null | NicknameStatus} */
|
||||
var TitleNicknameStatus = null;
|
||||
/** @deprecated */
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {{ Name: TitleName; Requirement: () => boolean; Earned?: boolean, Force?: boolean }[]} */
|
||||
|
|
@ -28,7 +27,7 @@ var TitleList = [
|
|||
{ Name: "MasterKidnapper", Requirement: function () { return (ReputationGet("Kidnap") >= 100); }, Earned: true },
|
||||
{ Name: "Patient", Requirement: function () { return ((ReputationGet("Asylum") <= -50) && (ReputationGet("Asylum") > -100)); }, Earned: true },
|
||||
{ Name: "PermanentPatient", Requirement: function () { return (ReputationGet("Asylum") <= -100); }, Earned: true },
|
||||
{ Name: "EscapedPatient", Requirement: function () { return (LogValue("Escaped", "Asylum") >= CurrentTime); }, Force: true },
|
||||
{ Name: "EscapedPatient", Requirement: function () { return ((LogValue("Escaped", "Asylum") ?? 0) >= CurrentTime); }, Force: true },
|
||||
{ Name: "Nurse", Requirement: function () { return ((ReputationGet("Asylum") >= 50) && (ReputationGet("Asylum") < 100)); }, Earned: true },
|
||||
{ Name: "Doctor", Requirement: function () { return (ReputationGet("Asylum") >= 100); }, Earned: true },
|
||||
{ Name: "AnimeGirl", Requirement: function () { return InventoryAvailable(Player, "AnimeGirl", "Cloth") && !Player.GenderSettings.HideTitles.Female; }, Earned: true },
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */
|
||||
|
|
@ -22,7 +21,7 @@ function AssetsBodyMarkingsBodyWritingsAfterDrawHook(data, originalFunction, {
|
|||
width: Width,
|
||||
};
|
||||
|
||||
switch (CA.Property.TypeRecord.s) {
|
||||
switch (CA.Property?.TypeRecord?.s) {
|
||||
case 0: // Print
|
||||
drawOptions.fontFamily = "Ananda Black";
|
||||
break;
|
||||
|
|
@ -85,10 +84,11 @@ function AssetsBodyMarkingsBodyWritingsAfterDrawHook(data, originalFunction, {
|
|||
}
|
||||
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const [text1, text2, text3] = [CA.Property.Text, CA.Property.Text2, CA.Property.Text3];
|
||||
const [text1, text2, text3] = [CA.Property.Text ?? "", CA.Property.Text2 ?? "", CA.Property.Text3 ?? ""];
|
||||
|
||||
// We draw the desired info on that canvas
|
||||
const ctx = TempCanvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
DynamicDrawText(text1, ctx, Width / 2, Height / 2 - 10, drawOptions);
|
||||
DynamicDrawText(text2, ctx, Width / 2, Height / 2, drawOptions);
|
||||
DynamicDrawText(text3, ctx, Width / 2, Height / 2 + 10, drawOptions);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
const AssetsClothCheerleaderTopData = {
|
||||
const AssetsClothCheerleaderTopData = /** @type {const} */ ({
|
||||
_Small: {
|
||||
shearFactor: 0.78,
|
||||
width: 100,
|
||||
|
|
@ -22,7 +21,7 @@ const AssetsClothCheerleaderTopData = {
|
|||
width: 130,
|
||||
yOffset: 84,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */
|
||||
function AssetsClothCheerleaderTopAfterDrawHook(data, originalFunction, {
|
||||
|
|
@ -54,14 +53,15 @@ function AssetsClothCheerleaderTopAfterDrawHook(data, originalFunction, {
|
|||
}
|
||||
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const text = CA.Property.Text;
|
||||
const text = CA.Property?.Text ?? "";
|
||||
|
||||
const sizeData = AssetsClothCheerleaderTopData[G] || AssetsClothCheerleaderTopData._Small;
|
||||
const sizeData = AssetsClothCheerleaderTopData[/** @type {keyof typeof AssetsClothCheerleaderTopData} */ (G)] ?? AssetsClothCheerleaderTopData._Small;
|
||||
|
||||
const height = 48;
|
||||
const width = sizeData.width;
|
||||
const flatCanvas = AnimationGenerateTempCanvas(C, A, width, height);
|
||||
const ctx = flatCanvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
DynamicDrawTextArc(text, ctx, width / 2, height / 2, {
|
||||
fontSize: 48,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */
|
||||
|
|
@ -18,7 +17,7 @@ function AssetsClothAccessoryBibAfterDrawHook(data, originalFunction, {
|
|||
const TempCanvas = AnimationGenerateTempCanvas(C, A, Width, Height);
|
||||
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const [text1, text2] = [CA.Property.Text, CA.Property.Text2];
|
||||
const [text1, text2] = [CA.Property?.Text ?? "", CA.Property?.Text2 ?? ""];
|
||||
const isAlone = !text1 || !text2;
|
||||
|
||||
const drawOptions = {
|
||||
|
|
@ -30,6 +29,7 @@ function AssetsClothAccessoryBibAfterDrawHook(data, originalFunction, {
|
|||
|
||||
// We draw the desired info on that canvas
|
||||
let ctx = TempCanvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
DynamicDrawText(text1, ctx, Width / 2, Height / (isAlone ? 2 : 2.5), drawOptions);
|
||||
DynamicDrawText(text2, ctx, Width / 2, Height / (isAlone ? 2 : 1.5), drawOptions);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */
|
||||
|
|
@ -23,7 +22,7 @@ function AssetsFaceMarkingsFaceWritingsAfterDrawHook(data, originalFunction, {
|
|||
width: Width,
|
||||
};
|
||||
|
||||
switch (CA.Property.TypeRecord.s) {
|
||||
switch (CA.Property?.TypeRecord?.s) {
|
||||
case 0: // Print
|
||||
drawOptions.fontFamily = "Ananda Black";
|
||||
break;
|
||||
|
|
@ -60,10 +59,11 @@ function AssetsFaceMarkingsFaceWritingsAfterDrawHook(data, originalFunction, {
|
|||
}
|
||||
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const [text1, text2, text3] = [CA.Property.Text, CA.Property.Text2, CA.Property.Text3];
|
||||
const [text1, text2, text3] = [CA.Property.Text ?? "", CA.Property.Text2 ?? "", CA.Property.Text3 ?? ""];
|
||||
|
||||
// We draw the desired info on that canvas
|
||||
const ctx = TempCanvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
DynamicDrawText(text1, ctx, Width / 2, Height / 2 - 10, drawOptions);
|
||||
DynamicDrawText(text2, ctx, Width / 2 + offset, Height / 2, drawOptions);
|
||||
DynamicDrawText(text3, ctx, Width / 2 + offset * 2, Height / 2 + 10, drawOptions);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
// How to make your item futuristic!
|
||||
|
|
@ -51,6 +50,7 @@ var FuturisticAccessChastityGroups = ["ItemPelvis", "ItemTorso", "ItemButt", "It
|
|||
*/
|
||||
function FuturisticAccess(data, OriginalFunction, DeniedFunction) {
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C) return false;
|
||||
if (InventoryItemFuturisticValidate(C) !== "") {
|
||||
DialogExtendedMessage = AssetTextGet("FuturisticItemLoginScreen");
|
||||
DeniedFunction(data);
|
||||
|
|
@ -107,9 +107,7 @@ function FuturisticAccessExit() {
|
|||
* @type {ExtendedItemScriptHookCallbacks.Validate<ExtendedItemData<any>, any>}
|
||||
*/
|
||||
function FuturisticAccessValidate(Data, OriginalFunction, C, Item, Option, CurrentOption, permitExisting) {
|
||||
let futureString = InventoryItemFuturisticValidate(C, Item, CurrentOption.ChangeWhenLocked);
|
||||
if (futureString) return futureString;
|
||||
else return OriginalFunction(C, Item, Option, CurrentOption, permitExisting);
|
||||
return InventoryItemFuturisticValidate(C, Item, CurrentOption.ChangeWhenLocked) ?? OriginalFunction?.(C, Item, Option, CurrentOption, permitExisting);
|
||||
}
|
||||
|
||||
// Load the futuristic item ACCESS DENIED screen
|
||||
|
|
@ -155,15 +153,16 @@ function InventoryItemFuturisticClickAccessDenied(data) {
|
|||
if (NoArch.Click(data)) {
|
||||
return;
|
||||
}
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C) return;
|
||||
|
||||
if (MouseIn(1400, 800, 200, 64)) {
|
||||
const elem = /** @type {null | HTMLInputElement} */(document.getElementById("PasswordField"));
|
||||
if (elem?.disabled ?? true) {
|
||||
const elem = /** @type {HTMLInputElement | null} */(document.getElementById("PasswordField"));
|
||||
if (!elem || (elem?.disabled ?? true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pw = elem.value.toUpperCase();
|
||||
const C = CharacterGetCurrent();
|
||||
if (DialogFocusItem && DialogFocusItem.Property && DialogFocusItem.Property.LockedBy == "PasswordPadlock" && pw == DialogFocusItem.Property.Password) {
|
||||
CommonPadlockUnlock(C, DialogFocusItem);
|
||||
DialogLeaveFocusItem();
|
||||
|
|
@ -176,7 +175,7 @@ function InventoryItemFuturisticClickAccessDenied(data) {
|
|||
} else {
|
||||
FuturisticAccessDeniedMessage = AssetTextGet("CantChangeWhileLockedFuturistic");
|
||||
AudioPlayInstantSound("Audio/AccessDenied.mp3");
|
||||
InventoryItemFuturisticPublishAccessDenied(CharacterGetCurrent());
|
||||
InventoryItemFuturisticPublishAccessDenied(C);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -184,8 +183,8 @@ function InventoryItemFuturisticClickAccessDenied(data) {
|
|||
/**
|
||||
* Validates, if the chosen option is possible. Sets the global variable 'DialogExtendedMessage' to the appropriate error message, if not.
|
||||
* @param {Character} C - The character to validate the option
|
||||
* @param {Item} Item - The equipped item
|
||||
* @param {boolean} changeWhenLocked - See {@link ExtendedItemOption.ChangeWhenLocked}
|
||||
* @param {Item | null} Item - The equipped item
|
||||
* @param {boolean} [changeWhenLocked] - See {@link ExtendedItemOption.ChangeWhenLocked}
|
||||
* @returns {string} - Returns false and sets DialogExtendedMessage, if the chosen option is not possible.
|
||||
*/
|
||||
function InventoryItemFuturisticValidate(C, Item = DialogFocusItem, changeWhenLocked=false) {
|
||||
|
|
@ -222,6 +221,7 @@ function InventoryItemFuturisticValidate(C, Item = DialogFocusItem, changeWhenLo
|
|||
* @param {Character} C - The character that got denied access.
|
||||
*/
|
||||
function InventoryItemFuturisticPublishAccessDenied(C) {
|
||||
if (!C.FocusGroup) return;
|
||||
const Dictionary = new DictionaryBuilder()
|
||||
.sourceCharacter(Player)
|
||||
.destinationCharacter(C)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData>} */
|
||||
function InventoryItemArmsFullLatexSuitDrawHook(Data, OriginalFunction) {
|
||||
OriginalFunction();
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C) return;
|
||||
const CanEquip = InventoryGet(C, "ItemVulva") == null;
|
||||
ExtendedItemCustomDraw(
|
||||
`${Data.dialogPrefix.option}Wand`,
|
||||
|
|
@ -17,8 +17,9 @@ function InventoryItemArmsFullLatexSuitDrawHook(Data, OriginalFunction) {
|
|||
/** @type {ExtendedItemScriptHookCallbacks.Click<TypedItemData>} */
|
||||
function InventoryItemArmsFullLatexSuitClickHook(Data, OriginalFunction) {
|
||||
OriginalFunction();
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C) return;
|
||||
if (MouseIn(...ExtendedXY[6][4], 225, 275)) {
|
||||
const C = CharacterGetCurrent();
|
||||
const VulvaItem = InventoryGet(C, "ItemVulva");
|
||||
const Worn = (C.IsPlayer() && VulvaItem != null && VulvaItem.Asset.Name === "FullLatexSuitWand");
|
||||
ExtendedItemCustomClick("Wand", () => InventoryItemArmsFullLatexSuitSetWand(Data, C), Worn);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemCallbacks.BeforeDraw} */
|
||||
|
|
@ -15,6 +14,5 @@ function AssetsItemArmsHempRopeBeforeDraw(data) {
|
|||
Y: data.Y + 30,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
return data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemCallbacks.BeforeDraw} */
|
||||
|
|
@ -11,5 +10,5 @@ function AssetsItemArmsNylonRopeBeforeDraw(data) {
|
|||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
return data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Load<TypedItemData>} */
|
||||
function InventoryItemArmsTransportJacketLoadHook(Data, OriginalFunction) {
|
||||
if (!DialogFocusItem) return;
|
||||
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
|
||||
if (textData === null) {
|
||||
return;
|
||||
|
|
@ -15,6 +15,7 @@ function InventoryItemArmsTransportJacketLoadHook(Data, OriginalFunction) {
|
|||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData>} */
|
||||
function InventoryItemArmsTransportJacketDrawHook(Data, OriginalFunction) {
|
||||
if (!DialogFocusItem) return;
|
||||
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
|
||||
if (textData === null) {
|
||||
return;
|
||||
|
|
@ -41,13 +42,14 @@ function InventoryItemArmsTransportJacketPublishActionHook(data, originalFunctio
|
|||
return;
|
||||
}
|
||||
case "TypedItemOption":
|
||||
originalFunction(C, item, newOption, previousOption);
|
||||
originalFunction?.(C, item, newOption, previousOption);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Exit<TypedItemData>} */
|
||||
function InventoryItemArmsTransportJacketExitHook(Data, OriginalFunction) {
|
||||
if (!DialogFocusItem) return;
|
||||
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
|
||||
if (textData !== null) {
|
||||
TextItem.Exit(textData);
|
||||
|
|
@ -64,9 +66,10 @@ function AssetsItemArmsTransportJacketAfterDraw(
|
|||
const height = 60;
|
||||
const flatCanvas = AnimationGenerateTempCanvas(C, A, width, height);
|
||||
const flatCtx = flatCanvas.getContext("2d");
|
||||
if (!flatCtx) return;
|
||||
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const text = CA.Property.Text;
|
||||
const text = CA.Property?.Text ?? "";
|
||||
|
||||
DynamicDrawText(text, flatCtx, width / 2, height / 2, {
|
||||
fontSize: 40,
|
||||
|
|
|
|||
|
|
@ -1,36 +1,38 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData | ModularItemData>} */
|
||||
function InventoryItemBreastForbiddenChastityBraDrawHook(data, originalFunction) {
|
||||
originalFunction();
|
||||
if (!DialogFocusItem) return;
|
||||
if (data.archetype === ExtendedArchetype.MODULAR && data.currentModule !== "ShockModule") {
|
||||
return;
|
||||
}
|
||||
|
||||
const { TriggerCount, ShowText, PunishOrgasm, PunishStandup, PunishStruggle } = DialogFocusItem.Property ?? {};
|
||||
|
||||
MainCanvas.textAlign = "right";
|
||||
DrawText(AssetTextGet("ShockCount"), 1500, 575, "White", "Gray");
|
||||
MainCanvas.textAlign = "left";
|
||||
DrawText(`${DialogFocusItem.Property.TriggerCount}`, 1510, 575, "White", "Gray");
|
||||
DrawText(`${TriggerCount}`, 1510, 575, "White", "Gray");
|
||||
|
||||
MainCanvas.textAlign = "center";
|
||||
ExtendedItemCustomDraw("ResetShockCount", 1635, 550, null, false, false);
|
||||
ExtendedItemCustomDraw("TriggerShock", 1635, 625, null, false, false);
|
||||
MainCanvas.textAlign = "left";
|
||||
ExtendedItemDrawCheckbox(
|
||||
"ShowText", 1100, 618, DialogFocusItem.Property.ShowText,
|
||||
"ShowText", 1100, 618, !!ShowText,
|
||||
{ text: AssetTextGet("ShowMessageInChat"), textColor: "White", changeWhenLocked: false },
|
||||
);
|
||||
ExtendedItemDrawCheckbox(
|
||||
"PunishOrgasm", 1100, 700, DialogFocusItem.Property.PunishOrgasm,
|
||||
"PunishOrgasm", 1100, 700, !!PunishOrgasm,
|
||||
{ text: AssetTextGet("ForbiddenChastityBraPunishOrgasm"), textColor: "White", changeWhenLocked: false },
|
||||
);
|
||||
ExtendedItemDrawCheckbox(
|
||||
"PunishStandup", 1100, 770, DialogFocusItem.Property.PunishStandup,
|
||||
"PunishStandup", 1100, 770, !!PunishStandup,
|
||||
{ text: AssetTextGet("ForbiddenChastityBraPunishStandup"), textColor: "White", changeWhenLocked: false },
|
||||
);
|
||||
ExtendedItemDrawCheckbox(
|
||||
"PunishStruggle", 1100, 840, DialogFocusItem.Property.PunishStruggle,
|
||||
"PunishStruggle", 1100, 840, !!PunishStruggle,
|
||||
{ text: AssetTextGet("ForbiddenChastityBraPunishStruggle"), textColor: "White", changeWhenLocked: false },
|
||||
);
|
||||
MainCanvas.textAlign = "center";
|
||||
|
|
@ -44,6 +46,7 @@ function InventoryItemBreastForbiddenChastityBraClickHook(data, originalFunction
|
|||
}
|
||||
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C || !DialogFocusItem) return;
|
||||
if (MouseIn(1635, 550, 225, 55)) {
|
||||
ExtendedItemCustomClick("ResetShockCount", InventoryItemNeckAccessoriesCollarShockUnitResetCount, false, false);
|
||||
return;
|
||||
|
|
@ -52,7 +55,7 @@ function InventoryItemBreastForbiddenChastityBraClickHook(data, originalFunction
|
|||
return;
|
||||
}
|
||||
if (!ExtendedItemPermissionMode) {
|
||||
const property = DialogFocusItem.Property;
|
||||
const property = /** @type {ItemProperties} */ (DialogFocusItem?.Property);
|
||||
if (MouseIn(1100, 618, 64, 64)) {
|
||||
ExtendedItemCustomClickAndPush(C, DialogFocusItem, "ShowText", () => property.ShowText = !property.ShowText, false, false);
|
||||
} else if (MouseIn(1100, 700, 64, 64)) {
|
||||
|
|
@ -74,12 +77,12 @@ function InventoryItemBreastForbiddenChastityBraClickHook(data, originalFunction
|
|||
function AssetsItemBreastForbiddenChastityBraScriptDrawHook(data, originalFunction, drawData) {
|
||||
const persistentData = drawData.PersistentData();
|
||||
/** @type {ItemProperties} */
|
||||
const property = (drawData.Item.Property = drawData.Item.Property || {});
|
||||
const property = (drawData.Item.Property ??= {});
|
||||
if (typeof persistentData.UpdateTime !== "number") persistentData.UpdateTime = CommonTime() + 4000;
|
||||
if (typeof persistentData.LastMessageLen !== "number") persistentData.LastMessageLen = (ChatRoomLastMessage) ? ChatRoomLastMessage.length : 0;
|
||||
if (typeof persistentData.CheckTime !== "number") persistentData.CheckTime = CommonTime();
|
||||
if (typeof persistentData.DisplayCount !== "number") persistentData.DisplayCount = 0;
|
||||
if (typeof persistentData.LastTriggerCount !== "number") persistentData.LastTriggerCount = property.TriggerCount;
|
||||
if (typeof persistentData.LastTriggerCount !== "number") persistentData.LastTriggerCount = property.TriggerCount ?? 0;
|
||||
if (typeof property.NextShockTime !== "number") property.NextShockTime = 0;
|
||||
const canShock = typeof property.ShockLevel === "number";
|
||||
|
||||
|
|
@ -88,14 +91,14 @@ function AssetsItemBreastForbiddenChastityBraScriptDrawHook(data, originalFuncti
|
|||
if (lastMsgIndex >= 0 && ChatRoomChatLog[lastMsgIndex].Time > persistentData.CheckTime)
|
||||
persistentData.UpdateTime = Math.min(persistentData.UpdateTime, CommonTime() + 200); // Trigger if the user speaks
|
||||
|
||||
const isTriggered = persistentData.LastTriggerCount < property.TriggerCount;
|
||||
const isTriggered = persistentData.LastTriggerCount < (property.TriggerCount ?? 0);
|
||||
const newlyTriggered = isTriggered && persistentData.DisplayCount == 0;
|
||||
if (newlyTriggered)
|
||||
persistentData.UpdateTime = Math.min(persistentData.UpdateTime, CommonTime());
|
||||
|
||||
if (persistentData.UpdateTime < CommonTime()) {
|
||||
|
||||
if (drawData.C.IsPlayer() && CommonTime() > drawData.Item.Property.NextShockTime) {
|
||||
if (drawData.C.IsPlayer() && CommonTime() > (drawData.Item.Property.NextShockTime ?? 0)) {
|
||||
if (canShock) {
|
||||
AssetsItemPelvisObedienceBeltUpdate(drawData, persistentData.CheckTime);
|
||||
}
|
||||
|
|
@ -105,7 +108,7 @@ function AssetsItemBreastForbiddenChastityBraScriptDrawHook(data, originalFuncti
|
|||
// Set CheckTime to last processed chat message time
|
||||
persistentData.CheckTime = (lastMsgIndex >= 0 ? ChatRoomChatLog[lastMsgIndex].Time : 0);
|
||||
|
||||
if (persistentData.LastTriggerCount > property.TriggerCount) persistentData.LastTriggerCount = 0;
|
||||
if (persistentData.LastTriggerCount > (property.TriggerCount ?? 0)) persistentData.LastTriggerCount = 0;
|
||||
const wasBlinking = property.BlinkState;
|
||||
property.BlinkState = wasBlinking && !newlyTriggered ? false : true;
|
||||
const timeFactor = isTriggered ? 12 : 1;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
|
|
@ -42,6 +41,7 @@ function InventoryItemBreastFuturisticBraDrawHook(Data, OriginalFunction) {
|
|||
|
||||
const Prefix = Data.dialogPrefix.option;
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C) return;
|
||||
const {bpm, breathing, temp} = InventoryItemBreastFuturisticBraUpdate(C);
|
||||
|
||||
DrawText(`${AssetTextGet(`${Prefix}Desc`)} ${C.MemberNumber}`, 1500, 625, "White", "Gray");
|
||||
|
|
@ -69,6 +69,7 @@ function AssetsItemBreastFuturisticBraBeforeDraw(data) {
|
|||
const ShowHeart = data.PersistentData().ShowHeart;
|
||||
return { Opacity: ShowHeart ? 1 : 0 };
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/** @type {ExtendedItemCallbacks.AfterDraw<FuturisticBraPersistentData>} */
|
||||
|
|
@ -92,6 +93,7 @@ function AssetsItemBreastFuturisticBraAfterDraw({
|
|||
|
||||
// We draw the desired info on that canvas
|
||||
let context = TempCanvas.getContext('2d');
|
||||
if (!context) return;
|
||||
context.font = "bold 14px sansserif";
|
||||
context.fillStyle = "Black";
|
||||
context.textAlign = "center";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Draw<ModularItemData>} */
|
||||
|
|
@ -6,7 +5,7 @@ function InventoryItemButtInflVibeButtPlugDrawHook(Data, OriginalFunction) {
|
|||
OriginalFunction();
|
||||
|
||||
if (Data.currentModule === ModularItemBase) {
|
||||
const [InflateLevel, Intensity] = ModularItemParseCurrent(Data, DialogFocusItem.Property.TypeRecord);
|
||||
const [InflateLevel, Intensity] = ModularItemParseCurrent(Data, DialogFocusItem?.Property?.TypeRecord);
|
||||
|
||||
// Display option information
|
||||
MainCanvas.save();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */
|
||||
|
|
@ -11,10 +10,11 @@ function AssetsItemDevicesDollBoxAfterDrawHook(data, originalFunction,
|
|||
const width = 400;
|
||||
const tempCanvas = AnimationGenerateTempCanvas(C, A, width, height);
|
||||
const ctx = tempCanvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// One line of text will be centered
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const [text1, text2] = [CA.Property.Text, CA.Property.Text2];
|
||||
const [text1, text2] = [CA.Property?.Text ?? "", CA.Property?.Text2 ?? ""];
|
||||
const isAlone = !text1 || !text2;
|
||||
|
||||
const drawOptions = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
|
|
@ -6,13 +5,14 @@
|
|||
*/
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.BeforeDraw<VibratingItemData, FuckMachinePersistentData>} */
|
||||
function AssetsItemDevicesFuckMachineBeforeDrawHook(data, originalFunction, { PersistentData, L, Y, Property }) {
|
||||
function AssetsItemDevicesFuckMachineBeforeDrawHook(data, originalFunction, drawData) {
|
||||
const { PersistentData, L, Y, Property } = drawData;
|
||||
const Data = PersistentData();
|
||||
if (typeof Data.DildoState !== "number") Data.DildoState = 0;
|
||||
if (typeof Data.Modifier !== "number") Data.Modifier = 1;
|
||||
|
||||
if (L === "Dildo") return { Y: Y + Data.DildoState };
|
||||
if (L !== "Pole") return;
|
||||
if (L !== "Pole") return drawData;
|
||||
|
||||
const Properties = Property || {};
|
||||
const Intensity = typeof Properties.Intensity === "number" ? Properties.Intensity : -1;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
|
|
@ -6,13 +5,13 @@
|
|||
*/
|
||||
|
||||
/** @type {ExtendedItemCallbacks.BeforeDraw<FuturisticCratePersistentData>} */
|
||||
function AssetsItemDevicesFuturisticCrateBeforeDraw({ PersistentData, L, X, Y, Property }) {
|
||||
function AssetsItemDevicesFuturisticCrateBeforeDraw(drawData) {
|
||||
const { PersistentData, L, Y, Property } = drawData;
|
||||
const Data = PersistentData();
|
||||
if (typeof Data.DildoState !== "number") Data.DildoState = 0;
|
||||
if (typeof Data.Modifier !== "number") Data.Modifier = 1;
|
||||
|
||||
//if (L === "DevicePleasureHolder") return { Y: Y + Data.DildoState };
|
||||
if (L !== "DevicePleasureHolder") return;
|
||||
if (L !== "DevicePleasureHolder") return drawData;
|
||||
|
||||
const Properties = Property || {};
|
||||
const Intensity = typeof Properties.Intensity === "number" ? Properties.Intensity : -1;
|
||||
|
|
@ -39,7 +38,7 @@ function AssetsItemDevicesFuturisticCrateBeforeDraw({ PersistentData, L, X, Y, P
|
|||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.ScriptDraw<VibratingItemData, FuturisticCratePersistentData>} */
|
||||
function AssetsItemDevicesFuturisticCrateScriptDrawHook(data, originalFunction, drawData) {
|
||||
originalFunction(drawData);
|
||||
originalFunction?.(drawData);
|
||||
|
||||
const Data = drawData.PersistentData();
|
||||
const Properties = drawData.Item.Property || {};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Load<ModularItemData>} */
|
||||
function InventoryItemDevicesKabeshiriWallLoadHook(data, originalFunction) {
|
||||
if (!DialogFocusItem) return;
|
||||
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
|
||||
if (textData === null) {
|
||||
return;
|
||||
|
|
@ -14,12 +14,13 @@ function InventoryItemDevicesKabeshiriWallLoadHook(data, originalFunction) {
|
|||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Draw<ModularItemData>} */
|
||||
function InventoryItemDevicesKabeshiriWallDrawHook(data, originalFunction) {
|
||||
if (!DialogFocusItem) return;
|
||||
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
|
||||
if (textData === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = textData.textNames.map(i => document.getElementById(PropertyGetID(i, DialogFocusItem)));
|
||||
const elements = textData.textNames.map(i => document.getElementById(PropertyGetID(i, /** @type {Item} */ (DialogFocusItem)))).filter(Boolean);
|
||||
if (data.currentModule !== ModularItemBase) {
|
||||
elements.forEach(el => el.toggleAttribute("hidden", true));
|
||||
} else {
|
||||
|
|
@ -41,13 +42,14 @@ function InventoryItemDevicesKabeshiriWallPublishActionHook(data, originalFuncti
|
|||
return;
|
||||
}
|
||||
case "ModularItemOption":
|
||||
originalFunction(C, item, newOption, previousOption);
|
||||
originalFunction?.(C, item, newOption, previousOption);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Exit<ModularItemData>} */
|
||||
function InventoryItemDevicesKabeshiriWallExitHook(data, originalFunction) {
|
||||
if (!DialogFocusItem) return;
|
||||
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
|
||||
if (textData !== null) {
|
||||
TextItem.Exit(textData);
|
||||
|
|
@ -68,10 +70,11 @@ function AssetsItemDevicesKabeshiriWallAfterDrawHook(
|
|||
const width = 1000;
|
||||
const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height);
|
||||
const ctx = tmpCanvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const text1 = CA.Property.Text;
|
||||
const text2 = CA.Property.Text2;
|
||||
const text1 = CA.Property?.Text ?? "";
|
||||
const text2 = CA.Property?.Text2 ?? "";
|
||||
|
||||
DynamicDrawTextArc(text1, ctx, 200, 490, {
|
||||
fontSize: 20,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
|
|
@ -6,12 +5,13 @@
|
|||
*/
|
||||
|
||||
/** @type {ExtendedItemCallbacks.BeforeDraw<KennelPersistentData>} */
|
||||
function AssetsItemDevicesKennelBeforeDraw({ PersistentData, L, Property }) {
|
||||
if (L !== "Door") return;
|
||||
function AssetsItemDevicesKennelBeforeDraw(drawData) {
|
||||
const { PersistentData, L, Property } = drawData;
|
||||
if (L !== "Door") return drawData;
|
||||
|
||||
const Data = PersistentData();
|
||||
const Properties = Property || {};
|
||||
const Door = Properties.Door || false;
|
||||
Data.DoorState ??= 0;
|
||||
const Door = Property.Door || false;
|
||||
|
||||
if (Data.DoorState >= 11 || Data.DoorState <= 1) Data.MustChange = false;
|
||||
|
||||
|
|
@ -21,6 +21,7 @@ function AssetsItemDevicesKennelBeforeDraw({ PersistentData, L, Property }) {
|
|||
Data.DrawRequested = false;
|
||||
if (Data.DoorState < 11 && Data.DoorState > 1) return { LayerType: "A" + Data.DoorState };
|
||||
}
|
||||
return drawData;
|
||||
}
|
||||
|
||||
/** @type {ExtendedItemCallbacks.ScriptDraw<KennelPersistentData>} */
|
||||
|
|
@ -46,7 +47,7 @@ function AssetsItemDevicesKennelScriptDraw({ C, PersistentData, Item }) {
|
|||
* @returns {string}
|
||||
*/
|
||||
function InventoryItemDevicesKennelGetAudio(C) {
|
||||
let wasWorn = InventoryGet(C, "ItemDevices") && InventoryGet(C, "ItemDevices").Asset.Name === "Kennel";
|
||||
let wasWorn = InventoryGet(C, "ItemDevices")?.Asset.Name === "Kennel";
|
||||
let isSelf = C.IsPlayer();
|
||||
return isSelf && wasWorn ? "CageStruggle" : "CageEquip";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
var ItemDevicesLuckyWheelMinTexts = 2;
|
||||
var ItemDevicesLuckyWheelMaxTexts = 8;
|
||||
|
|
@ -50,8 +49,10 @@ function InventoryItemDevicesLuckyWheelInitHook(data, originalFunction, characte
|
|||
/** @type {ExtendedItemScriptHookCallbacks.Load<NoArchItemData>} */
|
||||
function InventoryItemDevicesLuckyWheelg0LoadHook(data, originalFunction) {
|
||||
originalFunction();
|
||||
for (let num = 0; num < DialogFocusItem.Property.Texts.length; num++) {
|
||||
const input = ElementCreateInput(`LuckyWheelText${num}`, "input", DialogFocusItem.Property.Texts[num] || "", ItemDevicesLuckyWheelMaxTextLength);
|
||||
if (!DialogFocusItem) return;
|
||||
const texts = (DialogFocusItem.Property?.Texts ?? []);
|
||||
for (let num = 0; num < texts.length; num++) {
|
||||
const input = ElementCreateInput(`LuckyWheelText${num}`, "input", texts[num], ItemDevicesLuckyWheelMaxTextLength);
|
||||
if (input) {
|
||||
input.pattern = DynamicDrawTextInputPattern;
|
||||
input.addEventListener("change", InventoryItemDevicesLuckyWheelUpdate);
|
||||
|
|
@ -69,19 +70,21 @@ var ItemDevicesLuckyWheelRowLength = 350;
|
|||
function InventoryItemDevicesLuckyWheelg0DrawHook(data, originalFunction) {
|
||||
originalFunction();
|
||||
|
||||
if (!DialogFocusItem) return;
|
||||
// Section labels & remove buttons grid
|
||||
let top = ItemDevicesLuckyWheelRowTop;
|
||||
let left = ItemDevicesLuckyWheelRowLeft;
|
||||
for (let num = 0; num < DialogFocusItem.Property.Texts.length; num++) {
|
||||
const texts = (DialogFocusItem.Property?.Texts ?? []);
|
||||
for (let num = 0; num < texts.length; num++) {
|
||||
let topRow = (num % (ItemDevicesLuckyWheelMaxTexts / 2) * ItemDevicesLuckyWheelRowHeight);
|
||||
let leftCol = Math.floor(num / (ItemDevicesLuckyWheelMaxTexts / 2)) * ItemDevicesLuckyWheelRowLength;
|
||||
ElementPosition(`LuckyWheelText${num}`, left + leftCol, top + topRow, 300);
|
||||
}
|
||||
|
||||
const disabledAdd = DialogFocusItem.Property.Texts.length >= ItemDevicesLuckyWheelMaxTexts;
|
||||
const disabledAdd = texts.length >= ItemDevicesLuckyWheelMaxTexts;
|
||||
DrawButton(1360, 720, 120, 48, AssetTextGet("LuckyWheelAddSection"), disabledAdd ? "#888" : "white", null, null, disabledAdd);
|
||||
|
||||
const disabledRemove = DialogFocusItem.Property.Texts.length <= ItemDevicesLuckyWheelMinTexts;
|
||||
const disabledRemove = texts.length <= ItemDevicesLuckyWheelMinTexts;
|
||||
DrawButton(1530, 720, 120, 48, AssetTextGet("LuckyWheelRemoveSection"), disabledRemove ? "#888" : "white", null, null, disabledRemove);
|
||||
|
||||
}
|
||||
|
|
@ -89,26 +92,28 @@ function InventoryItemDevicesLuckyWheelg0DrawHook(data, originalFunction) {
|
|||
/** @type {ExtendedItemScriptHookCallbacks.Click<NoArchItemData>} */
|
||||
function InventoryItemDevicesLuckyWheelg0ClickHook(data, originalFunction) {
|
||||
originalFunction();
|
||||
if (!DialogFocusItem) return;
|
||||
const texts = (DialogFocusItem?.Property?.Texts ?? []);
|
||||
if (MouseIn(1360, 720, 120, 48)) {
|
||||
if (DialogFocusItem.Property.Texts.length >= ItemDevicesLuckyWheelMaxTexts) return;
|
||||
if (texts.length >= ItemDevicesLuckyWheelMaxTexts) return;
|
||||
|
||||
let last = DialogFocusItem.Property.Texts.length;
|
||||
let last = texts.length;
|
||||
const label = ItemDevicesLuckyWheelLabelForNum(last + 1);
|
||||
const input = ElementCreateInput(`LuckyWheelText${last}`, "input", label, ItemDevicesLuckyWheelMaxTextLength);
|
||||
if (input) {
|
||||
input.pattern = DynamicDrawTextInputPattern;
|
||||
input.addEventListener("change", InventoryItemDevicesLuckyWheelUpdate);
|
||||
}
|
||||
DialogFocusItem.Property.Texts.push(label);
|
||||
DialogFocusItem.Property?.Texts?.push(label);
|
||||
InventoryItemDevicesLuckyWheelUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (MouseIn(1530, 720, 120, 48)) {
|
||||
if (DialogFocusItem.Property.Texts.length <= ItemDevicesLuckyWheelMinTexts) return;
|
||||
if (texts.length <= ItemDevicesLuckyWheelMinTexts) return;
|
||||
|
||||
const num = DialogFocusItem.Property.Texts.length - 1;
|
||||
DialogFocusItem.Property.Texts.splice(num, 1);
|
||||
const num = texts.length - 1;
|
||||
DialogFocusItem?.Property?.Texts?.splice(num, 1);
|
||||
ElementRemove(`LuckyWheelText${num}`);
|
||||
InventoryItemDevicesLuckyWheelUpdate();
|
||||
return;
|
||||
|
|
@ -117,20 +122,24 @@ function InventoryItemDevicesLuckyWheelg0ClickHook(data, originalFunction) {
|
|||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Exit<NoArchItemData>} */
|
||||
function InventoryItemDevicesLuckyWheelg0ExitHook(data, originalFunction) {
|
||||
if (!DialogFocusItem) return;
|
||||
const C = CharacterGetCurrent();
|
||||
if (!DialogFocusItem || !C) return;
|
||||
|
||||
DialogFocusItem.Property ??= {};
|
||||
DialogFocusItem.Property.Texts ??= [];
|
||||
|
||||
const texts = DialogFocusItem.Property.Texts;
|
||||
for (let num = 0; num < ItemDevicesLuckyWheelMaxTexts; num++) {
|
||||
if (num < DialogFocusItem.Property.Texts.length) {
|
||||
if (num < texts.length) {
|
||||
const text = ElementValue(`LuckyWheelText${num}`);
|
||||
if (text != DialogFocusItem.Property.Texts[num]) {
|
||||
DialogFocusItem.Property.Texts[num] = text;
|
||||
if (text != texts[num]) {
|
||||
texts[num] = text;
|
||||
}
|
||||
}
|
||||
|
||||
ElementRemove(`LuckyWheelText${num}`);
|
||||
}
|
||||
|
||||
const C = CharacterGetCurrent();
|
||||
ChatRoomCharacterItemUpdate(C);
|
||||
CharacterRefresh(C, true, false);
|
||||
|
||||
|
|
@ -139,15 +148,20 @@ function InventoryItemDevicesLuckyWheelg0ExitHook(data, originalFunction) {
|
|||
}
|
||||
|
||||
function InventoryItemDevicesLuckyWheelUpdate() {
|
||||
CharacterRefresh(CharacterGetCurrent(), false);
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C) return;
|
||||
CharacterRefresh(C, false);
|
||||
}
|
||||
|
||||
function InventoryItemDevicesLuckyWheelTrigger() {
|
||||
const randomAngle = Math.round(Math.random() * 360);
|
||||
DialogFocusItem.Property.TargetAngle = randomAngle;
|
||||
ChatRoomCharacterItemUpdate(CharacterGetCurrent());
|
||||
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C || !DialogFocusItem) return;
|
||||
|
||||
const randomAngle = Math.round(Math.random() * 360);
|
||||
DialogFocusItem.Property ??= {};
|
||||
DialogFocusItem.Property.TargetAngle = randomAngle;
|
||||
ChatRoomCharacterItemUpdate(C);
|
||||
|
||||
const Dictionary = new DictionaryBuilder()
|
||||
.sourceCharacter(Player)
|
||||
.destinationCharacter(C)
|
||||
|
|
@ -163,7 +177,7 @@ function InventoryItemDevicesLuckyWheelTrigger() {
|
|||
function InventoryItemDevicesLuckyWheelStoppedTurning(C, Item, Angle) {
|
||||
if (!C.IsPlayer() || Item.Asset.Name !== "LuckyWheel") return;
|
||||
|
||||
let storedTexts = Item.Property.Texts && Array.isArray(Item.Property.Texts) ? Item.Property.Texts.filter(T => typeof T === "string") : [];
|
||||
let storedTexts = Array.isArray(Item.Property?.Texts) ? Item.Property.Texts.filter(T => typeof T === "string") : [];
|
||||
storedTexts = storedTexts.map(T => T.substring(0, ItemDevicesLuckyWheelMaxTextLength));
|
||||
const nbTexts = Math.max(Math.min(ItemDevicesLuckyWheelMaxTextLength, storedTexts.length), ItemDevicesLuckyWheelMinTexts);
|
||||
const sectorAngleSize = 360 / nbTexts;
|
||||
|
|
@ -223,7 +237,7 @@ function AssetsItemDevicesLuckyWheelScriptDraw({ C, PersistentData, Item }) {
|
|||
}
|
||||
|
||||
/** @type {ExtendedItemCallbacks.AfterDraw<LuckyWheelPersistentData>} */
|
||||
function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, Property, drawCanvas, drawCanvasBlink, AlphaMasks, Color, Opacity }) {
|
||||
function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, CA, X, Y, L, Property, drawCanvas, drawCanvasBlink, AlphaMasks, Color, Opacity }) {
|
||||
const height = 500;
|
||||
const width = 500;
|
||||
|
||||
|
|
@ -234,8 +248,11 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
|
|||
if (!Data.Spinning)
|
||||
return;
|
||||
|
||||
Data.LightStep ??= 0;
|
||||
|
||||
const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height);
|
||||
const ctx = tmpCanvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
if (C.IsInverted()) {
|
||||
ctx.rotate(Math.PI);
|
||||
|
|
@ -243,7 +260,7 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
|
|||
Y -= 500;
|
||||
}
|
||||
|
||||
if (Data.AnimationSpeed < 2 * ItemDevicesLuckyWheelAnimationMinSpeed) {
|
||||
if ((Data.AnimationSpeed ?? 1) < 2 * ItemDevicesLuckyWheelAnimationMinSpeed) {
|
||||
// Start blinking
|
||||
Data.LightStep = (++Data.LightStep) % 2;
|
||||
|
||||
|
|
@ -266,9 +283,8 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
|
|||
|
||||
if (L === "Text") {
|
||||
const Data = PersistentData();
|
||||
const CurrentAngle = Data.AnimationAngleState;
|
||||
const CurrentAngle = Data.AnimationAngleState ?? 0;
|
||||
const Properties = Property || {};
|
||||
const Item = InventoryGet(C, A.Group.Name);
|
||||
|
||||
DynamicDrawLoadFont(ItemDevicesLuckyWheelFont);
|
||||
|
||||
|
|
@ -279,9 +295,11 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
|
|||
|
||||
// Draw
|
||||
const diameter = height / 2;
|
||||
/** @type {(degrees: number) => number} */
|
||||
const degreeToRadians = (degrees) => degrees * Math.PI / 180;
|
||||
const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height);
|
||||
const ctx = tmpCanvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
if (C.IsInverted()) {
|
||||
ctx.rotate(Math.PI);
|
||||
|
|
@ -296,6 +314,7 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
|
|||
ctx.rotate(degreeToRadians(CurrentAngle));
|
||||
ctx.translate(-diameter, -diameter);
|
||||
|
||||
/** @type {Record<number, number>} */
|
||||
const SectionsPerNumTexts = {
|
||||
2: 2,
|
||||
3: 3,
|
||||
|
|
@ -308,12 +327,12 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
|
|||
|
||||
/** @type {readonly BCColor[]} */
|
||||
let itemColors;
|
||||
if (typeof Item.Color === "string") {
|
||||
itemColors = Array(Item.Asset.ColorableLayerCount).fill(Item.Color);
|
||||
} else if (CommonIsArray(Item.Color)) {
|
||||
itemColors = Item.Color;
|
||||
if (typeof CA.Color === "string") {
|
||||
itemColors = Array(CA.Asset.ColorableLayerCount).fill(CA.Color);
|
||||
} else if (CommonIsArray(CA.Color)) {
|
||||
itemColors = CA.Color;
|
||||
} else {
|
||||
itemColors = Item.Asset.DefaultColor;
|
||||
itemColors = CA.Asset.DefaultColor;
|
||||
}
|
||||
|
||||
// Draw the background
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
|
|
@ -13,13 +12,14 @@ function AssetsItemDevicesPetBowlAfterDrawHook(data, originalFunction,
|
|||
// Fetch the text property and assert that it matches the character
|
||||
// and length requirements
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const text = CA.Property.Text;
|
||||
const text = CA.Property?.Text ?? "";
|
||||
|
||||
// Prepare a temporary canvas to draw the text to
|
||||
const height = 60;
|
||||
const width = 130;
|
||||
const tempCanvas = AnimationGenerateTempCanvas(C, A, width, height);
|
||||
const ctx = tempCanvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Reposition and orient the text when hanging upside-down
|
||||
if (C.IsInverted()) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Load<TypedItemData>} */
|
||||
function InventoryItemDevicesWoodenBoxLoadHook(Data, OriginalFunction) {
|
||||
if (!DialogFocusItem) return;
|
||||
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
|
||||
if (textData === null) {
|
||||
return;
|
||||
|
|
@ -14,6 +14,7 @@ function InventoryItemDevicesWoodenBoxLoadHook(Data, OriginalFunction) {
|
|||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData>} */
|
||||
function InventoryItemDevicesWoodenBoxDrawHook(Data, OriginalFunction) {
|
||||
if (!DialogFocusItem) return;
|
||||
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
|
||||
if (textData === null) {
|
||||
return;
|
||||
|
|
@ -25,6 +26,7 @@ function InventoryItemDevicesWoodenBoxDrawHook(Data, OriginalFunction) {
|
|||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Exit<TypedItemData>} */
|
||||
function InventoryItemDevicesWoodenBoxExitHook(data, originalFunction) {
|
||||
if (!DialogFocusItem) return;
|
||||
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
|
||||
if (textData === null) {
|
||||
return;
|
||||
|
|
@ -34,7 +36,7 @@ function InventoryItemDevicesWoodenBoxExitHook(data, originalFunction) {
|
|||
PropertyOpacityExit(data, originalFunction, false);
|
||||
|
||||
// Apply extra opacity-specific effects
|
||||
const Property = DialogFocusItem.Property;
|
||||
const Property = DialogFocusItem.Property ??= {};
|
||||
const Transparent = CommonIsNumeric(Property.Opacity) ? Property.Opacity < 0.15 : false;
|
||||
if (Transparent) {
|
||||
delete Property.Effect;
|
||||
|
|
@ -43,6 +45,7 @@ function InventoryItemDevicesWoodenBoxExitHook(data, originalFunction) {
|
|||
}
|
||||
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C) return;
|
||||
CharacterRefresh(C, true, false);
|
||||
ChatRoomCharacterItemUpdate(C, DialogFocusItem.Asset.Group.Name);
|
||||
}
|
||||
|
|
@ -51,7 +54,7 @@ function InventoryItemDevicesWoodenBoxExitHook(data, originalFunction) {
|
|||
function InventoryItemDevicesWoodenBoxPublishActionHook(data, originalFunction, C, item, newOption, previousOption) {
|
||||
switch (newOption.OptionType) {
|
||||
case "TypedItemOption":
|
||||
originalFunction(C, item, newOption, previousOption);
|
||||
originalFunction?.(C, item, newOption, previousOption);
|
||||
return;
|
||||
case "TextItemOption": {
|
||||
const textData = ExtendedItemGetData(item.Asset, ExtendedArchetype.TEXT);
|
||||
|
|
@ -79,9 +82,10 @@ function AssetsItemDevicesWoodenBoxAfterDrawHook(
|
|||
const width = 310;
|
||||
const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height);
|
||||
const ctx = tmpCanvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const text = CA.Property.Text;
|
||||
const text = CA.Property?.Text ?? "";
|
||||
|
||||
let from;
|
||||
let to;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemCallbacks.BeforeDraw} */
|
||||
|
|
@ -10,5 +9,5 @@ function AssetsItemFeetHempRopeBeforeDraw(data) {
|
|||
Y: data.Y -170,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
return data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemCallbacks.BeforeDraw} */
|
||||
|
|
@ -11,5 +10,5 @@ function AssetsItemFeetNylonRopeBeforeDraw(data) {
|
|||
Y: data.Y -170,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
return data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.SetOption<ModularItemData, ModularItemOption>} */
|
||||
|
|
@ -13,13 +12,13 @@ function InventoryItemHandheldPlushiesSetOptionHook(
|
|||
refresh,
|
||||
) {
|
||||
// Toggle the new option within the active module as per usual
|
||||
originalFunction(C, item, newOption, previousOption, false, false);
|
||||
originalFunction?.(C, item, newOption, previousOption, false, false);
|
||||
|
||||
// Set the options within all other modules to 0
|
||||
const currentModuleName = newOption.ModuleName;
|
||||
const currentOptionIndices = ModularItemParseCurrent(
|
||||
data,
|
||||
item.Property.TypeRecord,
|
||||
item.Property?.TypeRecord ?? null,
|
||||
);
|
||||
for (const [
|
||||
otherModuleIndex,
|
||||
|
|
@ -31,7 +30,7 @@ function InventoryItemHandheldPlushiesSetOptionHook(
|
|||
const otherOldOption =
|
||||
data.modules[otherModuleIndex].Options[otherOptionIndex];
|
||||
const otherNewOption = data.modules[otherModuleIndex].Options[0];
|
||||
originalFunction(C, item, otherNewOption, otherOldOption, false, false);
|
||||
originalFunction?.(C, item, otherNewOption, otherOldOption, false, false);
|
||||
}
|
||||
}
|
||||
CharacterRefresh(C, push, false);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Validate<ModularItemData, ModularItemOption>} */
|
||||
function ItemHeadDroneMaskValidateHook(data, originalFunction, C, item, newOption, previousOption, permitExisting) {
|
||||
let ret = originalFunction(C, item, newOption, previousOption, permitExisting);
|
||||
let ret = originalFunction?.(C, item, newOption, previousOption, permitExisting) ?? "";
|
||||
if (C.IsSimple()) {
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -35,9 +34,11 @@ function AssetsItemHeadDroneMaskAfterDrawHook(data, originalFunction, {
|
|||
let XOffset = 67;
|
||||
let YOffset = 89;
|
||||
const TempCanvas = AnimationGenerateTempCanvas(C, A, Width, Height);
|
||||
let ctx = TempCanvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const text = CA.Property.Text;
|
||||
const text = CA.Property?.Text ?? "";
|
||||
const isAlone = !text;
|
||||
|
||||
const drawOptions = {
|
||||
|
|
@ -48,7 +49,6 @@ function AssetsItemHeadDroneMaskAfterDrawHook(data, originalFunction, {
|
|||
};
|
||||
|
||||
// Draw the text onto the canvas
|
||||
let ctx = TempCanvas.getContext('2d');
|
||||
DynamicDrawText(text, ctx, Width/2, Height/ (isAlone? 2: 2.5), drawOptions);
|
||||
|
||||
//And print the canvas onto the character based on the above positions
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
|
|
@ -10,16 +9,18 @@ function AssetsItemHoodCanvasHoodAfterDrawHook(data, originalFunction,
|
|||
{ C, A, CA, X, Y, L, drawCanvas, drawCanvasBlink, AlphaMasks, Color },
|
||||
) {
|
||||
if (L === "Text") {
|
||||
// Fetch the text property and assert that it matches the character
|
||||
// and length requirements
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const text = CA.Property.Text;
|
||||
|
||||
// Prepare a temporary canvas to draw the text to
|
||||
const height = 50;
|
||||
const width = 120;
|
||||
const tempCanvas = AnimationGenerateTempCanvas(C, A, width, height);
|
||||
const ctx = tempCanvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Fetch the text property and assert that it matches the character
|
||||
// and length requirements
|
||||
TextItem.Init(data, C, CA, false, false);
|
||||
const text = CA.Property?.Text ?? "";
|
||||
|
||||
DynamicDrawTextArc(text, ctx, width / 2, height / 2, {
|
||||
fontSize: 36,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
let CombinationPadlockPlayerIsBlind = false;
|
||||
/** @type {number | null} */
|
||||
let CombinationPadlockBlindCombinationOffset = null;
|
||||
let CombinationPadlockCombinationLastValue = "";
|
||||
let CombinationPadlockNewCombinationLastValue = "";
|
||||
|
|
@ -10,6 +10,9 @@ let CombinationPadlockLoaded = false;
|
|||
function InventoryItemMiscCombinationPadlockLoadHook(data, originalFunction) {
|
||||
originalFunction();
|
||||
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C || !C.FocusGroup) return;
|
||||
|
||||
CombinationPadlockPlayerIsBlind = Player.IsBlind();
|
||||
// Only update on initial load, not update loads
|
||||
if (!CombinationPadlockLoaded) {
|
||||
|
|
@ -24,7 +27,6 @@ function InventoryItemMiscCombinationPadlockLoadHook(data, originalFunction) {
|
|||
CombinationPadlockBlindCombinationOffset = null;
|
||||
}
|
||||
|
||||
var C = CharacterGetCurrent();
|
||||
|
||||
// Only create the inputs if the zone isn't blocked
|
||||
if (!InventoryGroupIsBlocked(C, C.FocusGroup.Name)) {
|
||||
|
|
@ -37,11 +39,11 @@ function InventoryItemMiscCombinationPadlockLoadHook(data, originalFunction) {
|
|||
combinationInput.addEventListener("input", InventoryItemMiscCombinationPadlockModifyInput);
|
||||
// the current code is shown for owners, lovers and the member whose number is on the padlock
|
||||
if (
|
||||
Player.MemberNumber === DialogFocusSourceItem.Property.LockMemberNumber ||
|
||||
Player.MemberNumber === DialogFocusSourceItem?.Property?.LockMemberNumber ||
|
||||
C.IsOwnedByPlayer() ||
|
||||
C.IsLoverOfPlayer()
|
||||
) {
|
||||
combinationInput.setAttribute("placeholder", DialogFocusSourceItem.Property.CombinationNumber);
|
||||
combinationInput.setAttribute("placeholder", DialogFocusSourceItem?.Property?.CombinationNumber);
|
||||
}
|
||||
} else {
|
||||
/** @type {HTMLInputElement} */(document.getElementById('CombinationNumber')).type = CombinationPadlockPlayerIsBlind ? "password" : "text";
|
||||
|
|
@ -59,15 +61,16 @@ function InventoryItemMiscCombinationPadlockLoadHook(data, originalFunction) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {Event & { target: { value: string }}} e
|
||||
* @param {Event} e
|
||||
*/
|
||||
function InventoryItemMiscCombinationPadlockModifyInput(e) {
|
||||
const target = /** @type {HTMLInputElement} */ (e.target);
|
||||
const clumsiness = Player.GetClumsiness();
|
||||
|
||||
// If the player is either blind or impaired by restraints, modify the input accordingly
|
||||
if (CombinationPadlockPlayerIsBlind || clumsiness > 0) {
|
||||
const previousValue = CombinationPadlockCombinationLastValue;
|
||||
const newValue = e.target.value;
|
||||
const newValue = target.value;
|
||||
let prefix = "";
|
||||
let suffix = "";
|
||||
for (let i = 0; i < previousValue.length && previousValue[i] === newValue[i]; i++) {
|
||||
|
|
@ -87,17 +90,19 @@ function InventoryItemMiscCombinationPadlockModifyInput(e) {
|
|||
return String((Number(digit) + offset) % 10);
|
||||
});
|
||||
|
||||
e.target.value = prefix + inserted + suffix;
|
||||
target.value = prefix + inserted + suffix;
|
||||
}
|
||||
|
||||
CombinationPadlockCombinationLastValue = e.target.value;
|
||||
CombinationPadlockCombinationLastValue = target.value;
|
||||
}
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Draw<NoArchItemData>} */
|
||||
function InventoryItemMiscCombinationPadlockDrawHook(data, originalFunction) {
|
||||
originalFunction();
|
||||
|
||||
var C = CharacterGetCurrent();
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C || !C.FocusGroup || !DialogFocusItem) return;
|
||||
|
||||
const playerBlind = Player.IsBlind();
|
||||
if (playerBlind !== CombinationPadlockPlayerIsBlind) {
|
||||
InventoryItemMiscCombinationPadlockDrawHook(data, originalFunction);
|
||||
|
|
@ -149,14 +154,15 @@ function InventoryItemMiscCombinationPadlockClickHook(data, originalFunction) {
|
|||
return;
|
||||
}
|
||||
|
||||
var C = CharacterGetCurrent();
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C || !C.FocusGroup) return;
|
||||
|
||||
// If the zone is blocked, cannot interact with the lock
|
||||
if (InventoryGroupIsBlocked(C, C.FocusGroup.Name)) return;
|
||||
|
||||
// Opens the padlock
|
||||
if (MouseIn(1600, 771, 350, 64)) {
|
||||
if (ElementValue("CombinationNumber") == DialogFocusSourceItem.Property.CombinationNumber) {
|
||||
if (ElementValue("CombinationNumber") == DialogFocusSourceItem?.Property?.CombinationNumber) {
|
||||
CommonPadlockUnlock(C, DialogFocusSourceItem);
|
||||
DialogLeaveFocusItem();
|
||||
}
|
||||
|
|
@ -175,7 +181,7 @@ function InventoryItemMiscCombinationPadlockClickHook(data, originalFunction) {
|
|||
// Changes the code
|
||||
else if (MouseIn(1600, 871, 350, 64)) {
|
||||
// Succeeds to change
|
||||
if (ElementValue("CombinationNumber") == DialogFocusSourceItem.Property.CombinationNumber) {
|
||||
if (ElementValue("CombinationNumber") == DialogFocusSourceItem?.Property?.CombinationNumber) {
|
||||
var NewCode = ElementValue("NewCombinationNumber");
|
||||
// We only accept code made of digits and of 4 numbers
|
||||
if (ValidationCombinationNumberRegex.test(NewCode)) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/** @type {ExtendedItemScriptHookCallbacks.Draw<NoArchItemData>} */
|
||||
function InventoryItemMiscExclusivePadlockDrawHook(data, originalFunction) {
|
||||
originalFunction();
|
||||
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C || !DialogFocusItem) return;
|
||||
|
||||
DrawText(AssetTextGet(DialogFocusItem.Asset.Group.Name + DialogFocusItem.Asset.Name + "Intro"), 1500, 600, "white", "gray");
|
||||
let msg = AssetTextGet(DialogFocusItem.Asset.Group.Name + DialogFocusItem.Asset.Name + "Detail");
|
||||
const subst = ChatRoomPronounSubstitutions(CurrentCharacter, "TargetPronoun", false);
|
||||
const subst = ChatRoomPronounSubstitutions(C, "TargetPronoun", false);
|
||||
msg = CommonStringSubstitute(msg, subst);
|
||||
DrawText(msg, 1500, 700, "white", "gray");
|
||||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ let KDDefaultKB = {
|
|||
};
|
||||
|
||||
let KinkyDungeonRootDirectory = "Screens/MiniGame/KinkyDungeon/";
|
||||
/** @type {Character} */
|
||||
let KinkyDungeonPlayerCharacter = null; // Other player object
|
||||
let KinkyDungeonGameData = null; // Data sent by other player
|
||||
let KinkyDungeonGameDataNullTimer = 4000; // If data is null, we query this often
|
||||
|
|
|
|||
|
|
@ -3413,8 +3413,8 @@ function ChatRoomSendAttemptEmote(msg) {
|
|||
*
|
||||
* @param {Character} C - Character on which the action is done.
|
||||
* @param {string} Action - Action modifier
|
||||
* @param {Item | null} PrevItem - The item that has been removed.
|
||||
* @param {Item | null} NextItem - The item that has been added.
|
||||
* @param {Item | null | undefined} PrevItem - The item that has been removed.
|
||||
* @param {Item | null | undefined} NextItem - The item that has been added.
|
||||
* @returns {boolean} - whether we published anything to the chat.
|
||||
*/
|
||||
function ChatRoomPublishAction(C, Action, PrevItem, NextItem) {
|
||||
|
|
|
|||
|
|
@ -2332,7 +2332,6 @@ function ChatRoomMapViewClick() {
|
|||
Y = 10 + 70 * (count % 13);
|
||||
X = 10 + 70 * Math.floor(count / 13);
|
||||
if (MouseIn(X, Y, 60, 60)) {
|
||||
// @ts-ignore
|
||||
if ((Obj.AssetName == null) || (Obj.AssetGroup == null) || InventoryAvailable(Player, Obj.AssetName, Obj.AssetGroup))
|
||||
ChatRoomMapViewEditObject = CommonCloneDeep(Obj);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -576,7 +576,6 @@ async function ChatSearchLoad() {
|
|||
ElementCreateSettingsLabel(TextGet("Lobby")),
|
||||
ElementCreateRadioButtonGroup(
|
||||
"chat-search-search-menu-room-lobby-radio-group",
|
||||
// @ts-ignore
|
||||
(ev, key) => {
|
||||
Player.ChatSearchSettings.Space = ChatSearchSpace = key;
|
||||
ChatSearchUpdateSearchSettings();
|
||||
|
|
|
|||
|
|
@ -573,7 +573,7 @@ function MainHallOpenChangelog() {
|
|||
function MainHallMaidReleasePlayer() {
|
||||
if (MainHallMaid.CanInteract()) {
|
||||
for (let D = 0; D < MainHallMaid.Dialog.length; D++)
|
||||
if ((MainHallMaid.Dialog[D].Stage == "0") && (MainHallMaid.Dialog[D].Option == null))
|
||||
if ((MainHallMaid.Dialog[D].Stage == "0") && (MainHallMaid.Dialog[D].Option === null))
|
||||
MainHallMaid.Dialog[D].Result = DialogFind(MainHallMaid, "AlreadyReleased");
|
||||
CharacterRelease(Player);
|
||||
for (let L = 0; L < MainHallStrongLocks.length; L++)
|
||||
|
|
@ -591,7 +591,7 @@ function MainHallMaidReleasePlayer() {
|
|||
function MainHallMaidAngry() {
|
||||
if ((ReputationGet("Dominant") < 30) && !MainHallIsHeadMaid) {
|
||||
for (let D = 0; D < MainHallMaid.Dialog.length; D++)
|
||||
if ((MainHallMaid.Dialog[D].Stage == "PlayerGagged") && (MainHallMaid.Dialog[D].Option == null))
|
||||
if ((MainHallMaid.Dialog[D].Stage == "PlayerGagged") && (MainHallMaid.Dialog[D].Option === null))
|
||||
MainHallMaid.Dialog[D].Result = DialogFind(MainHallMaid, "LearnedLesson");
|
||||
ReputationProgress("Dominant", 1);
|
||||
InventoryWearRandom(Player, "ItemMouth");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
/** @type {null | string[][]} */
|
||||
/** @type {string[][]} */
|
||||
// @ts-ignore Strict-TS: Lying here because that's loaded by Login
|
||||
var ActivityDictionary = null;
|
||||
var ActivityOrgasmGameButtonX = 0;
|
||||
var ActivityOrgasmGameButtonY = 0;
|
||||
|
|
@ -11,13 +11,14 @@ var ActivityOrgasmGameTimer = 0;
|
|||
var ActivityOrgasmResistLabel = "";
|
||||
var ActivityOrgasmRuined = false; // If set to true, the orgasm will be ruined right before it happens
|
||||
|
||||
/** @type { ()=>void | undefined } */
|
||||
/** @type { (() => void) | undefined } */
|
||||
let ActivityTranslateResolve = undefined;
|
||||
|
||||
let ActivityDebug = false;
|
||||
|
||||
/**
|
||||
* Debug logging function for activities
|
||||
* @param {any[]} args
|
||||
*/
|
||||
function ActivityLog(...args) {
|
||||
if (ActivityDebug) {
|
||||
|
|
@ -137,8 +138,9 @@ function ActivityPossibleOnGroup(C, GroupName) {
|
|||
if (!CharacterNotEnclosedOrSelfActivity || !ActivityAllowed() || !CharacterHasArousalEnabled(C))
|
||||
return false;
|
||||
const Group = ActivityGetGroupOrMirror(C.AssetFamily, GroupName);
|
||||
if (!Group) return false;
|
||||
const Zone = PreferenceGetArousalZone(C, Group.Name);
|
||||
return Zone && Zone.Factor > 0;
|
||||
return !!Zone && Zone.Factor > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -293,8 +295,8 @@ function ActivityGenerateItemActivitiesFromNeed(acting, acted, needsItem, activi
|
|||
return items.reduce((activities, item) => {
|
||||
const typeList = CommonIsObject(item.Property?.TypeRecord) ? PropertyTypeRecordToStrings(item.Property.TypeRecord) : [null];
|
||||
|
||||
/** @type {ItemActivityRestriction} */
|
||||
let blocked = null;
|
||||
/** @type {ItemActivityRestriction | undefined} */
|
||||
let blocked = undefined;
|
||||
if (typeList.some((type) => InventoryIsAllowedLimited(acted, item, type))) {
|
||||
blocked = "limited";
|
||||
} else if (typeList.some((type) => InventoryBlockedOrLimited(acted, item, type))) {
|
||||
|
|
@ -313,7 +315,7 @@ function ActivityGenerateItemActivitiesFromNeed(acting, acted, needsItem, activi
|
|||
return activities;
|
||||
}
|
||||
return activities;
|
||||
}, []);
|
||||
}, /** @type {ItemActivity[]} */ ([]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -341,7 +343,6 @@ function ActivityAllowedForGroup(character, groupname) {
|
|||
|
||||
const targetedItem = InventoryGet(character, groupname);
|
||||
|
||||
/** @type {ItemActivity[]} */
|
||||
let allowed = activities.reduce((allowedActivities, activity) => {
|
||||
// Validate that this activity can be done
|
||||
if (!ActivityHasValidTarget(character, activity, group)) {
|
||||
|
|
@ -377,7 +378,7 @@ function ActivityAllowedForGroup(character, groupname) {
|
|||
return [...allowedActivities, ...ActivityGenerateItemActivitiesFromNeed(Player, character, targetNeedsItemActivity, activity, group, true)];
|
||||
}
|
||||
|
||||
if (activity.Name === "ShockItem" && InventoryItemHasEffect(targetedItem, "ReceiveShock")) {
|
||||
if (activity.Name === "ShockItem" && targetedItem && InventoryItemHasEffect(targetedItem, "ReceiveShock")) {
|
||||
let remote = Player.Appearance.find(a => InventoryItemHasEffect(a, "TriggerShock"));
|
||||
if (remote) {
|
||||
ActivityLog(`${Player.Name} on ${character.Name}, act: ${activity.Name}: can trigger shock, adding in`);
|
||||
|
|
@ -387,7 +388,7 @@ function ActivityAllowedForGroup(character, groupname) {
|
|||
|
||||
ActivityLog(`${Player.Name} on ${character.Name}, act: ${activity.Name}: not handled by item stuff, adding in`);
|
||||
return allowedActivities.concat({ Activity: activity, Group: group.Name });
|
||||
}, []);
|
||||
}, /** @type {ItemActivity[]} */ ([]));
|
||||
|
||||
ActivityLog(`${Player.Name} on ${character.Name}: allowed activities for group ${groupname} lookup complete`, allowed);
|
||||
|
||||
|
|
@ -418,18 +419,18 @@ function ActivityCanBeDone(C, Activity, Group) {
|
|||
* @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
|
||||
* @param {Asset | null} [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;
|
||||
const act = typeof A === "string" ? AssetGetActivity(C.AssetFamily, A) : A;
|
||||
if (!act) return;
|
||||
Count = CommonClamp(Count ?? 1, 1, Infinity);
|
||||
|
||||
// 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
|
||||
var Factor = (PreferenceGetActivityFactor(C, act.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
|
||||
|
|
@ -437,11 +438,11 @@ function ActivityEffect(S, C, A, Z, Count, Asset) {
|
|||
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;
|
||||
const expression = Asset?.ActivityExpression?.[act.Name] ?? act.ActivityExpression;
|
||||
if (Array.isArray(expression))
|
||||
InventoryExpressionTriggerApply(C, expression);
|
||||
|
||||
ActivitySetArousalTimer(C, A, Z, Factor);
|
||||
ActivitySetArousalTimer(C, act, Z, Factor);
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -477,8 +478,8 @@ function ActivityEffectFlat(S, C, Amount, Z, Count, Asset) {
|
|||
* @return {void} - Nothing
|
||||
*/
|
||||
function ActivityChatRoomArousalSync(C) {
|
||||
if (C.IsPlayer() && ServerPlayerIsInChatRoom())
|
||||
ServerSend("ChatRoomCharacterArousalUpdate", { OrgasmTimer: C.ArousalSettings.OrgasmTimer, Progress: C.ArousalSettings.Progress, ProgressTimer: C.ArousalSettings.ProgressTimer, OrgasmCount: C.ArousalSettings.OrgasmCount });
|
||||
if (!C.IsPlayer() && !ServerPlayerIsInChatRoom()) return;
|
||||
ServerSend("ChatRoomCharacterArousalUpdate", { OrgasmTimer: C.ArousalSettings.OrgasmTimer, Progress: C.ArousalSettings.Progress, ProgressTimer: C.ArousalSettings.ProgressTimer, OrgasmCount: C.ArousalSettings.OrgasmCount });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -505,7 +506,7 @@ function ActivitySetArousal(C, Progress) {
|
|||
* @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
|
||||
* @param {Asset | null} [Asset] - The asset used to perform the activity
|
||||
* @return {void} - Nothing
|
||||
*/
|
||||
function ActivitySetArousalTimer(C, Activity, Zone, Progress, Asset) {
|
||||
|
|
@ -521,11 +522,11 @@ function ActivitySetArousalTimer(C, Activity, Zone, Progress, Asset) {
|
|||
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 (["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;
|
||||
Max = Activity?.MaxProgressSelf ?? 67;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -856,7 +857,7 @@ function ActivityVibratorLevel(C, Level) {
|
|||
* @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
|
||||
* @param {Asset | null} [Asset] - The asset used to perform the activity
|
||||
* @returns {void} - Nothing
|
||||
*/
|
||||
function ActivityRunSelf(Source, Target, Activity, Group, Asset) {
|
||||
|
|
@ -875,7 +876,8 @@ function ActivityRunSelf(Source, Target, Activity, Group, Asset) {
|
|||
* @param {Activity} activity
|
||||
*/
|
||||
function ActivityBuildChatTag(character, group, activity, is_label = false) {
|
||||
const groupMap = {"ItemVulva":"ItemPenis", "ItemVulvaPiercings": "ItemGlans"};
|
||||
/** @type {Partial<Record<AssetGroupName, string>>} */
|
||||
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}`;
|
||||
|
|
@ -895,6 +897,7 @@ function ActivityRun(actor, acted, targetGroup, ItemActivity, sendMessage=true)
|
|||
const UsedAsset = ItemActivity && ItemActivity.Item ? ItemActivity.Item.Asset : null;
|
||||
|
||||
let group = ActivityGetGroupOrMirror(acted.AssetFamily, targetGroup.Name);
|
||||
if (!group) return;
|
||||
// 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())
|
||||
|
|
@ -943,13 +946,13 @@ function ActivityRun(actor, acted, targetGroup, ItemActivity, sendMessage=true)
|
|||
* @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));
|
||||
}
|
||||
const AssetActivity = Asset.DynamicActivity(Source);
|
||||
if (!AssetActivity) return;
|
||||
const Activity = AssetGetActivity(Target.AssetFamily, AssetActivity);
|
||||
if (!Activity) return;
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -966,8 +969,8 @@ function ActivityFetishItemFactor(C, Type) {
|
|||
|
||||
for (const item of C.Appearance) {
|
||||
const fetish = [
|
||||
...InventoryGetItemProperty(item, "Fetish"),
|
||||
...(item.Asset.Fetish || []),
|
||||
...(InventoryGetItemProperty(item, "Fetish") ?? []),
|
||||
...(item.Asset.Fetish ?? []),
|
||||
];
|
||||
if (fetish.includes(Type)) {
|
||||
return Factor;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
var AfkTimerTimout = 5 * 60 * 1000; // 5 minutes
|
||||
|
|
@ -61,12 +60,12 @@ function AfkTimerSetEnabled(Enabled) {
|
|||
* @returns {void} - Nothing
|
||||
*/
|
||||
function AfkTimerSetIsAfk() {
|
||||
if (CurrentScreen != "ChatRoom") return;
|
||||
if (!ServerPlayerIsInChatRoom()) return;
|
||||
if (AfkTimerIsSet) return;
|
||||
if (AfkTimerLastEvent === 0 || AfkTimerLastEvent + AfkTimerTimout > CommonTime()) return;
|
||||
// save the current Emoticon, if there is any
|
||||
if (InventoryGet(Player, "Emoticon") && InventoryGet(Player, "Emoticon").Property && AfkTimerOldEmoticon == null) {
|
||||
AfkTimerOldEmoticon = /** @type {ExpressionNameMap["Emoticon"]} */(InventoryGet(Player, "Emoticon").Property.Expression);
|
||||
if (InventoryGet(Player, "Emoticon") && InventoryGet(Player, "Emoticon")?.Property && AfkTimerOldEmoticon == null) {
|
||||
AfkTimerOldEmoticon = /** @type {ExpressionNameMap["Emoticon"]} */(InventoryGet(Player, "Emoticon")?.Property?.Expression);
|
||||
}
|
||||
CharacterSetFacialExpression(Player, "Emoticon", "Afk");
|
||||
AfkTimerIsSet = true;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
var AudioDialog = new Audio();
|
||||
|
||||
|
|
@ -588,11 +587,11 @@ function AudioPlaySoundForChatMessage(data, sender, msg, metadata) {
|
|||
if (!data || !sender || !metadata || !["Activity", "Action", "ServerMessage"].includes(data.Type))
|
||||
return false;
|
||||
|
||||
if (AudioShouldSilenceSound(ChatRoomMessageInvolvesPlayer(data))) return;
|
||||
if (AudioShouldSilenceSound(ChatRoomMessageInvolvesPlayer(data))) return false;
|
||||
|
||||
// Instant actions can trigger a sound depending on the action.
|
||||
let Action = AudioActions.find(CA => CA.IsAction && CA.IsAction(data));
|
||||
/** @type AudioSoundEffect */
|
||||
/** @type {AudioSoundEffect | null} */
|
||||
let soundEffect = null;
|
||||
if (Action) {
|
||||
let snd = Action.GetSoundEffect(data, metadata);
|
||||
|
|
@ -616,7 +615,7 @@ function AudioPlaySoundForChatMessage(data, sender, msg, metadata) {
|
|||
|
||||
/**
|
||||
* Low-level function to play a sound effect.
|
||||
* @param {AudioSoundEffect|string} soundEffect
|
||||
* @param {AudioSoundEffect|string|null} soundEffect
|
||||
* @param {number} [volumeModifier]
|
||||
* @returns {boolean} if a sound was played or not.
|
||||
*/
|
||||
|
|
@ -657,7 +656,7 @@ function AudioPlaySoundEffect(soundEffect, volumeModifier) {
|
|||
* @returns {boolean} Whether a sound was played.
|
||||
*/
|
||||
function AudioPlaySoundForAsset(character, asset) {
|
||||
if (AudioShouldSilenceSound()) return;
|
||||
if (AudioShouldSilenceSound()) return false;
|
||||
|
||||
let sound = AudioGetSoundFromAsset(character, asset.Group.Name, asset.Name);
|
||||
return AudioPlaySoundEffect(sound, 0);
|
||||
|
|
@ -669,7 +668,7 @@ function AudioPlaySoundForAsset(character, asset) {
|
|||
* @param {Character} character
|
||||
* @param {AssetGroupName} groupName
|
||||
* @param {string} assetName
|
||||
* @returns {AudioSoundEffect?}
|
||||
* @returns {AudioSoundEffect | null}
|
||||
*/
|
||||
function AudioGetSoundFromAsset(character, groupName, assetName) {
|
||||
let asset = AssetGet(character.AssetFamily, groupName, assetName);
|
||||
|
|
@ -680,7 +679,7 @@ function AudioGetSoundFromAsset(character, groupName, assetName) {
|
|||
sound = asset.DynamicAudio(character);
|
||||
}
|
||||
|
||||
return [sound, 0];
|
||||
return sound ? [sound, 0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -702,7 +701,7 @@ function AudioGetFileName(sound) {
|
|||
* Processes which sound should be played for items
|
||||
* @param {ServerChatRoomMessage} data - Data content triggering the potential sound
|
||||
* @param {IChatRoomMessageMetadata} metadata - The chat message metadata
|
||||
* @returns {AudioSoundEffect | undefined} - The name of the sound to play, followed by the noise modifier
|
||||
* @returns {AudioSoundEffect | null} - The name of the sound to play, followed by the noise modifier
|
||||
*/
|
||||
function AudioGetSoundFromChatMessage(data, metadata) {
|
||||
const sender = metadata.SourceCharacter;
|
||||
|
|
@ -710,34 +709,35 @@ function AudioGetSoundFromChatMessage(data, metadata) {
|
|||
|
||||
if (data.Type === "Activity" && metadata.ActivityAsset) {
|
||||
let item = InventoryGet(sender, metadata.ActivityAsset.Group.Name);
|
||||
if (!item || item.Asset.Name !== metadata.ActivityAsset.Name) return;
|
||||
if (!item || item.Asset.Name !== metadata.ActivityAsset.Name) return null;
|
||||
|
||||
// Workaround for the shock remote; select the item on the target instead
|
||||
if (item.Asset.Name === "ShockRemote" && metadata.FocusGroup) {
|
||||
if (item.Asset.Name === "ShockRemote" && metadata.FocusGroup && metadata.TargetCharacter) {
|
||||
item = InventoryGet(metadata.TargetCharacter, metadata.FocusGroup.Name);
|
||||
}
|
||||
|
||||
if (!item || !item.Asset.ActivityAudio) return;
|
||||
if (!item || !item.Asset.ActivityAudio) return null;
|
||||
|
||||
const idx = item.Asset.AllowActivity.findIndex(a => a === metadata.ActivityName);
|
||||
const idx = item.Asset.AllowActivity?.findIndex(a => a === metadata.ActivityName) ?? -1;
|
||||
const soundEffect = item.Asset.ActivityAudio[idx];
|
||||
|
||||
if (!soundEffect) return;
|
||||
if (!soundEffect) return null;
|
||||
|
||||
return [soundEffect, 0];
|
||||
} else if (data.Type === "Action") {
|
||||
const NextAsset = metadata.Assets && metadata.Assets.NextAsset;
|
||||
if (!NextAsset) return;
|
||||
if (!NextAsset) return null;
|
||||
|
||||
return AudioGetSoundFromAsset(sender, NextAsset.Group.Name, NextAsset.Name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the sound for vibrators
|
||||
* @param {ServerChatRoomMessage} data - Represents the chat message received
|
||||
* @param {IChatRoomMessageMetadata} metadata - The metadata from the recieved message
|
||||
* @returns {[string, number]} - The name of the sound to play, followed by the noise modifier
|
||||
* @returns {[string, number] | null} - The name of the sound to play, followed by the noise modifier
|
||||
*/
|
||||
function AudioVibratorSounds(data, metadata) {
|
||||
var Sound = "";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
/** @type Character[] */
|
||||
/** @type {Character[]} */
|
||||
var Character = [];
|
||||
var CharacterNextId = 0;
|
||||
|
||||
|
|
@ -164,6 +163,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
HeightRatio: 1,
|
||||
HasHiddenItems: false,
|
||||
SavedColors: GetDefaultSavedColors(),
|
||||
// @ts-ignore Strict-TS: not sure why this is null here
|
||||
ActiveExpression: null,
|
||||
|
||||
PoseMapping: {},
|
||||
|
|
@ -294,7 +294,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
return this._BlindLevel;
|
||||
},
|
||||
GetBlurLevel: function() {
|
||||
if ((this.IsPlayer() && this.GraphicsSettings && !this.GraphicsSettings.AllowBlur) || CommonPhotoMode) {
|
||||
if ((this.IsPlayer() && !this.GraphicsSettings.AllowBlur) || CommonPhotoMode) {
|
||||
return 0;
|
||||
}
|
||||
let blurLevel = 0;
|
||||
|
|
@ -343,7 +343,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
},
|
||||
GetSlowLevel: function () {
|
||||
// Respect immunity setting for the player
|
||||
if (this.IsPlayer() && /** @type {PlayerCharacter} */(this).RestrictionSettings.SlowImmunity)
|
||||
if (this.IsPlayer() && this.RestrictionSettings.SlowImmunity)
|
||||
return 0;
|
||||
|
||||
let slowness = 0;
|
||||
|
|
@ -394,7 +394,8 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
if (this.Owner && this.Owner.trim().startsWith("NPC-")) return "npc";
|
||||
if (this.IsPlayer()) {
|
||||
// NPC-owner while in trial
|
||||
let trialing = PrivateCharacter.find(c => NPCEventGet(c, "EndSubTrial") > 0);
|
||||
// Cast here because PrivateCharacter[0] is actually the player
|
||||
let trialing = /** @type {PlayerCharacter | NPCCharacter} */ (PrivateCharacter.find(c => NPCEventGet(c, "EndSubTrial") > 0));
|
||||
if (trialing && trialing !== this) return "npc";
|
||||
}
|
||||
if (AsylumGGTSGetLevel(this) >= 6) return "ggts";
|
||||
|
|
@ -415,7 +416,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
return false;
|
||||
}
|
||||
case "online":
|
||||
return this.Ownership.MemberNumber === C.MemberNumber;
|
||||
return this.Ownership?.MemberNumber === C.MemberNumber;
|
||||
case "player":
|
||||
return true;
|
||||
default:
|
||||
|
|
@ -428,8 +429,8 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
case "npc":
|
||||
return !!PrivateCharacter.find(c => NPCEventGet(c, "PlayerCollaring") > 0);
|
||||
case "player":
|
||||
return (NPCEventGet(this, "NPCCollaring") > 0);
|
||||
case "online": return this.Ownership.Stage >= 1;
|
||||
return (NPCEventGet(/** @type {NPCCharacter} */(this), "NPCCollaring") > 0);
|
||||
case "online": return (this.Ownership?.Stage ?? 0) >= 1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
@ -451,7 +452,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
const privateOwner = PrivateCharacter.find(c => NPCEventGet(c, "EndSubTrial") > 0);
|
||||
return privateOwner?.Name ?? name ?? "";
|
||||
}
|
||||
case "online": return this.Ownership.Name;
|
||||
case "online": return this.Ownership?.Name ?? ""; // this.IsOwned() makes it impossible
|
||||
case "player": return CharacterNickname(Player);
|
||||
default:
|
||||
return "";
|
||||
|
|
@ -459,7 +460,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
},
|
||||
OwnerNumber: function () {
|
||||
if (this.IsOwned() === "online")
|
||||
return this.Ownership.MemberNumber;
|
||||
return this.Ownership?.MemberNumber ?? -1; // this.IsOwned() makes it impossible
|
||||
return -1;
|
||||
},
|
||||
HasOwnerNotes: function () {
|
||||
|
|
@ -471,11 +472,12 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
OwnedSince: function () {
|
||||
switch (this.IsOwned()) {
|
||||
case "online":
|
||||
return Math.floor((CurrentTime - this.Ownership.Start) / 86400000);
|
||||
// this.IsOwned() makes it impossible
|
||||
return Math.floor((CurrentTime - (this.Ownership?.Start ?? 0)) / 86400000);
|
||||
case "player": {
|
||||
let Time = NPCEventGet(this, "NPCCollaring");
|
||||
let Time = NPCEventGet(/** @type {NPCCharacter} */(this), "NPCCollaring");
|
||||
if (Time > 0) return Math.floor((CurrentTime - Time) / 86400000);
|
||||
Time = NPCEventGet(this, "EndDomTrial");
|
||||
Time = NPCEventGet(/** @type {NPCCharacter} */(this), "EndDomTrial");
|
||||
if (Time > 0) {
|
||||
if (Time > CurrentTime)
|
||||
return Math.ceil((Time - CurrentTime) / 86400000);
|
||||
|
|
@ -504,7 +506,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
OwnedSinceMs: function () {
|
||||
switch (this.IsOwned()) {
|
||||
case "online":
|
||||
return this.Ownership.Start;
|
||||
return this.Ownership?.Start ?? 0;
|
||||
case "player": {
|
||||
let Time = NPCEventGet(this, "NPCCollaring");
|
||||
if (Time > 0) return Time;
|
||||
|
|
@ -536,12 +538,12 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
const loves = this.GetLovership();
|
||||
if (C.IsNpc()) {
|
||||
const Love = loves.find(l => !l.MemberNumber && l.Name === C.Name);
|
||||
if (Love == null) return false;
|
||||
return Love.Start > 0;
|
||||
if (!Love) return false;
|
||||
return (Love.Start ?? 0) > 0;
|
||||
}
|
||||
|
||||
return (
|
||||
this.IsLoverOfMemberNumber(C.MemberNumber) ||
|
||||
this.IsLoverOfMemberNumber(/** @type {number} */ (C.MemberNumber)) ||
|
||||
this.IsNpc() && (((this.Lover != null) && (this.Lover.trim() == C.Name)) || (NPCEventGet(this, "Girlfriend") > 0))
|
||||
);
|
||||
},
|
||||
|
|
@ -652,8 +654,8 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
IsPlayer: function () {
|
||||
return this.Type === CharacterType.PLAYER;
|
||||
},
|
||||
get X() { return this.Position?.X;},
|
||||
get Y() { return this.Position?.Y;},
|
||||
get X() { return this.Position?.X ?? -1; },
|
||||
get Y() { return this.Position?.Y ?? -1; },
|
||||
set X(value) {
|
||||
this.Position = { X: value, Y: this.Y };
|
||||
},
|
||||
|
|
@ -661,11 +663,15 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
this.Position = { X: this.X, Y: value };
|
||||
},
|
||||
get Position() {
|
||||
if (this?.MapData?.Pos == undefined) return null;
|
||||
if (this?.MapData?.Pos?.X === null || this?.MapData?.Pos?.Y === null) return null;
|
||||
return { X: this.MapData.Pos.X, Y: this.MapData.Pos.Y};
|
||||
if (!this.MapData?.Pos) return null;
|
||||
if (this.MapData?.Pos?.X === null || this.MapData?.Pos?.Y === null) return null;
|
||||
return { X: this.MapData.Pos.X, Y: this.MapData.Pos.Y };
|
||||
},
|
||||
set Position({X,Y}) {
|
||||
set Position(pos) {
|
||||
if (pos === null) {
|
||||
return;
|
||||
}
|
||||
const { X, Y } = pos;
|
||||
if (!this.MapData) return;
|
||||
if (!this.MapData.Pos) return;
|
||||
this.MapData.Pos = ChatRoomMapViewValidatePosition({X, Y});
|
||||
|
|
@ -673,9 +679,9 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
ChatRoomMapViewUpdatePlayerFlag();
|
||||
},
|
||||
IsBirthday: function () {
|
||||
if ((this.Creation === null) || (CurrentTime === null)) return false;
|
||||
const creation = new Date(this.Creation),
|
||||
current = new Date(CurrentTime);
|
||||
if (!this.Creation) return false;
|
||||
const creation = new Date(this.Creation);
|
||||
const current = new Date(CurrentTime);
|
||||
|
||||
return (creation.getUTCDate() === current.getUTCDate()) &&
|
||||
(creation.getUTCMonth() === current.getUTCMonth()) &&
|
||||
|
|
@ -749,7 +755,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
return this.Attribute.includes(attribute);
|
||||
},
|
||||
GetGenders: function () {
|
||||
return this.Appearance.map(asset => asset.Asset.Gender).filter(a => a);
|
||||
return this.Appearance.map(asset => asset.Asset.Gender).filter(Boolean);
|
||||
},
|
||||
GetPronouns: function () {
|
||||
const pronounItem = InventoryGet(this, "Pronouns");
|
||||
|
|
@ -834,10 +840,17 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
// Keep these two methods non-enumerable such that they do not interfere with the likes of `Object.keys`
|
||||
const activeExpression = Object.defineProperties(/** @type {Character["ActiveExpression"]} */({}), {
|
||||
setWithoutReload: {
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
*/
|
||||
value: function (key, value) { this[key] = value; },
|
||||
enumerable: false,
|
||||
},
|
||||
deleteWithoutReload: {
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
value: function (key) { delete this[key]; },
|
||||
enumerable: false,
|
||||
},
|
||||
|
|
@ -874,6 +887,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
|
|||
function CharacterGenerateRandomName() {
|
||||
|
||||
// Get the list of all currently known names
|
||||
/** @type {string[]} */
|
||||
const CurrentNames = [];
|
||||
CurrentNames.push(...Character.map(c => c.Name));
|
||||
CurrentNames.push(...PrivateCharacter.map(c => c.Name));
|
||||
|
|
@ -917,6 +931,10 @@ function CharacterBuildDialog(C, CSV, functionPrefix, reload=true) {
|
|||
|
||||
C.Dialog = [];
|
||||
|
||||
/**
|
||||
* @param {string} fieldContents
|
||||
* @returns {string | null}
|
||||
*/
|
||||
function parseField(fieldContents) {
|
||||
if (typeof fieldContents !== "string") return null;
|
||||
const str = fieldContents;
|
||||
|
|
@ -931,7 +949,7 @@ function CharacterBuildDialog(C, CSV, functionPrefix, reload=true) {
|
|||
// Creates a dialog object
|
||||
/** @type {DialogLine} */
|
||||
const D = {
|
||||
Stage: parseField(L[0]),
|
||||
Stage: parseField(L[0]) ?? "",
|
||||
NextStage: parseField(L[1]),
|
||||
Option: parseField(L[2]),
|
||||
Result: parseField(L[3]),
|
||||
|
|
@ -942,7 +960,7 @@ function CharacterBuildDialog(C, CSV, functionPrefix, reload=true) {
|
|||
};
|
||||
|
||||
// Prefix with the current screen unless this is a Dialog function or an online character
|
||||
if (D.Function && D.Function !== "") {
|
||||
if (D.Function) {
|
||||
// @ts-expect-error Not sure why the online || player check errors here
|
||||
D.Function = (D.Function.startsWith("Dialog") ? "" : (C.IsOnline() || C.IsPlayer()) ? "ChatRoom" : functionPrefix) + D.Function;
|
||||
}
|
||||
|
|
@ -958,24 +976,27 @@ function CharacterBuildDialog(C, CSV, functionPrefix, reload=true) {
|
|||
/**
|
||||
* Loads the content of a CSV file to build the character dialog. Can override the current screen.
|
||||
* @param {Character} C - Character for which to build the dialog objects
|
||||
* @param {DialogInfo} [info]
|
||||
* @param {DialogInfo<any>} [info]
|
||||
* @returns {void} - Nothing
|
||||
*/
|
||||
function CharacterLoadCSVDialog(C, info) {
|
||||
|
||||
/** @type {DialogInfo<any>} */
|
||||
let dialog;
|
||||
if (!info && !C.DialogInfo) {
|
||||
console.error(`cannot refresh dialog for character ${C.ID}`);
|
||||
return;
|
||||
} else if (info) {
|
||||
C.DialogInfo = info;
|
||||
dialog = C.DialogInfo = info;
|
||||
} else {
|
||||
// Just refresh the info we have
|
||||
dialog = /** @type {DialogInfo<any>} */ (C.DialogInfo);
|
||||
}
|
||||
|
||||
const FullPath = ScreenFileGetDialog(C.DialogInfo.name, C.DialogInfo.module, C.DialogInfo.screen);
|
||||
const FullPath = ScreenFileGetDialog(dialog.name, dialog.module, dialog.screen);
|
||||
|
||||
function buildDialog() {
|
||||
CharacterBuildDialog(C, CommonCSVCache[FullPath], C.DialogInfo.screen);
|
||||
CharacterBuildDialog(C, CommonCSVCache[FullPath], dialog.screen);
|
||||
|
||||
// Translate the dialog if needed and perform substitutions
|
||||
TranslationLoadDialog(C, () => {
|
||||
|
|
@ -1029,9 +1050,8 @@ function CharacterArchetypeClothes(C, Archetype, ForceColor) {
|
|||
if (Outfit == 0) {
|
||||
InventoryWear(C, "MaidOutfit2", "Cloth");
|
||||
InventoryWear(C, "MaidHairband1", "Hat");
|
||||
} else if (Outfit == 1) {
|
||||
InventoryWear(C, "MaidLatex", "Cloth");
|
||||
InventoryGet(C, "Cloth").Color = ['#202020', '#B0B0B0', 'Default'];
|
||||
} else if (Math.random() > 0.75) {
|
||||
InventoryWear(C, "MaidLatex", "Cloth", ['#202020', '#B0B0B0', 'Default']);
|
||||
InventoryWear(C, "MaidLatexHairband", "Hat");
|
||||
} else if (Outfit == 2) {
|
||||
InventoryWear(C, "MaidDress3", "Cloth");
|
||||
|
|
@ -1117,25 +1137,27 @@ function CharacterArchetypeClothes(C, Archetype, ForceColor) {
|
|||
// Rope bunny archetype
|
||||
if (Archetype == "Bunny") {
|
||||
CharacterNaked(C);
|
||||
InventoryWear(C, CommonRandomItemFromList(null, ["BunnySuit", "LatexBunnySuit"]), "Bra", CommonRandomItemFromList(null, ["Default", "#BBBBBB", "#222222", "#882222", "#BB8888", "#BB00BB"]));
|
||||
InventoryWear(C, CommonRandomItemFromList("", ["BunnySuit", "LatexBunnySuit"]), "Bra", CommonRandomItemFromList(null, ["Default", "#BBBBBB", "#222222", "#882222", "#BB8888", "#BB00BB"]));
|
||||
InventoryWear(C, "BunnyCollarCuffs", "ClothAccessory");
|
||||
InventoryWear(C, CommonRandomItemFromList(null, ["BunnyEars1", "BunnyEars2"]), "HairAccessory1");
|
||||
InventoryWear(C, CommonRandomItemFromList("", ["BunnyEars1", "BunnyEars2"]), "HairAccessory1");
|
||||
InventoryWear(C, "BunnyTailStrap", "TailStraps");
|
||||
if (Math.random() > 0.5) InventoryWear(C, "Pantyhose1", "Socks");
|
||||
InventoryWear(C, CommonRandomItemFromList(null, ["AnkleStrapShoes", "StilettoHeels", "Shoes5"]), "Shoes");
|
||||
InventoryWear(C, CommonRandomItemFromList("", ["AnkleStrapShoes", "StilettoHeels", "Shoes5"]), "Shoes");
|
||||
}
|
||||
|
||||
// Succubus archetype
|
||||
if (Archetype == "Succubus") {
|
||||
CharacterNaked(C);
|
||||
let Color = CommonRandomItemFromList(null, /** @type {const} */(["Default", "#222222", "#BBBBBB", "#882222"]));
|
||||
InventoryWear(C, CommonRandomItemFromList(null, ["BondageDress1", "BondageDress2", "CorsetDress", "EveningGown", "Dress3"]), "Cloth", Color);
|
||||
InventoryWear(C, CommonRandomItemFromList(null, ["CatEye", "CatEye2", "LargeSolid", "SuperstarBlurred", "UndershadowedSolid"]), "EyeShadow", Color);
|
||||
if (Math.random() > 0.5) InventoryWear(C, CommonRandomItemFromList(null, ["GradientPantyhose", "Socks5", "Stockings1", "Stockings2"]), "Socks", Color);
|
||||
InventoryWear(C, "SuccubusHorns", "HairAccessory1", Color);
|
||||
InventoryWear(C, CommonRandomItemFromList(null, ["SuccubusTailStrap", "SuccubusHeartTailStrap"]), "TailStraps", Color);
|
||||
InventoryWear(C, CommonRandomItemFromList(null, ["BatWings", "DevilWings", "SuccubusWings"]), "Wings", Color);
|
||||
InventoryWear(C, CommonRandomItemFromList(null, ["AnkleStrapShoes", "StilettoHeels", "Shoes5", "CustomHeels", "ThighBoots"]), "Shoes", Color);
|
||||
InventoryWear(C, CommonRandomItemFromList("", ["BondageDress1", "BondageDress2", "CorsetDress", "EveningGown", "Dress3"]), "Cloth", Color);
|
||||
InventoryWear(C, CommonRandomItemFromList("", ["CatEye", "CatEye2", "LargeSolid", "SuperstarBlurred", "UndershadowedSolid"]), "EyeShadow", Color);
|
||||
if (Math.random() > 0.5) {
|
||||
InventoryWear(C, CommonRandomItemFromList("", ["GradientPantyhose", "Socks5", "Stockings1", "Stockings2"]), "Socks", Color);
|
||||
InventoryWear(C, "SuccubusHorns", "HairAccessory1", Color);
|
||||
}
|
||||
InventoryWear(C, CommonRandomItemFromList("", ["SuccubusTailStrap", "SuccubusHeartTailStrap"]), "TailStraps", Color);
|
||||
InventoryWear(C, CommonRandomItemFromList("", ["BatWings", "DevilWings", "SuccubusWings"]), "Wings", Color);
|
||||
InventoryWear(C, CommonRandomItemFromList("", ["AnkleStrapShoes", "StilettoHeels", "Shoes5", "CustomHeels", "ThighBoots"]), "Shoes", Color);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1144,7 +1166,7 @@ function CharacterArchetypeClothes(C, Archetype, ForceColor) {
|
|||
* Loads an NPC into the character array. The appearance is randomized, and a type can be provided to dress them in a given style.
|
||||
* @template {ModuleType} T
|
||||
* @param {string} CharacterID - The unique identifier for the NPC
|
||||
* @param {string} [NPCType] - The dialog used by the NPC. Defaults to CharacterID if not specified.
|
||||
* @param {null | string} [NPCType] - The dialog used by the NPC. Defaults to CharacterID if not specified.
|
||||
* @param {null | T} module
|
||||
* @param {null | ModuleScreens[T]} screen
|
||||
* @returns {NPCCharacter} - The randomly generated NPC
|
||||
|
|
@ -1154,10 +1176,10 @@ function CharacterLoadNPC(CharacterID, NPCType=null, module=null, screen=null) {
|
|||
|
||||
// Checks if the NPC already exists and returns it if it's the case
|
||||
const duplicate = Character.find(c => c.CharacterID === CharacterID);
|
||||
if (duplicate) return duplicate;
|
||||
if (duplicate) return /** @type {NPCCharacter} */ (duplicate);
|
||||
|
||||
// Randomize the new character
|
||||
const C = CharacterCreate("Female3DCG", CharacterType.NPC, CharacterID);
|
||||
const C = /** @type {NPCCharacter} */ (CharacterCreate("Female3DCG", CharacterType.NPC, CharacterID));
|
||||
C.AccountName = NPCType;
|
||||
CharacterLoadCSVDialog(C, { module: module ?? CurrentModule, screen: screen ?? CurrentScreen, name: NPCType });
|
||||
C.Name = CharacterGenerateRandomName();
|
||||
|
|
@ -1226,8 +1248,8 @@ function CharacterOnlineRefresh(Char, data, SourceMemberNumber) {
|
|||
|
||||
const oldPronouns = Char.GetPronouns();
|
||||
const currentAppearance = Char.Appearance;
|
||||
LoginPerformAppearanceFixups(data.Appearance);
|
||||
ServerAppearanceLoadFromBundle(Char, "Female3DCG", data.Appearance, SourceMemberNumber);
|
||||
LoginPerformAppearanceFixups(data.Appearance ?? []);
|
||||
ServerAppearanceLoadFromBundle(Char, "Female3DCG", data.Appearance ?? [], SourceMemberNumber);
|
||||
CharacterAppearanceResolveSync(Char, currentAppearance);
|
||||
|
||||
if (Char.IsPlayer()) LoginValidCollar();
|
||||
|
|
@ -1257,13 +1279,13 @@ function CharacterOnlineRefresh(Char, data, SourceMemberNumber) {
|
|||
function CharacterLoadOnline(data, SourceMemberNumber) {
|
||||
|
||||
// Check if the character already exists to reuse it
|
||||
/** @type {Character} */
|
||||
/** @type {Character | undefined} */
|
||||
let Char = data.ID.toString() == Player.CharacterID ? Player : Character.find(c => c.CharacterID === data.ID);
|
||||
|
||||
// We have to do that validation here because Description is one of the keys we check to decide
|
||||
// whether to refresh or not; our currently in-memory character has it decoded, so we have to decode
|
||||
// this as well.
|
||||
data.Description = ServerAccountDataSyncedValidate.Description(data.Description, Char);
|
||||
data.Description = ServerAccountDataSyncedValidate.Description(data.Description, Player);
|
||||
|
||||
if (Array.isArray(data.WhiteList)) {
|
||||
data.WhiteList.sort((a, b) => a - b);
|
||||
|
|
@ -1296,30 +1318,24 @@ function CharacterLoadOnline(data, SourceMemberNumber) {
|
|||
} else {
|
||||
|
||||
// If we must add a character, we refresh it
|
||||
var Refresh = true;
|
||||
if (ChatRoomData.Character != null)
|
||||
for (let C = 0; C < ChatRoomData.Character.length; C++)
|
||||
if (ChatRoomData.Character[C].ID.toString() == data.ID.toString()) {
|
||||
Refresh = false;
|
||||
break;
|
||||
}
|
||||
let Refresh = !ChatRoomData?.Character.some(c => c.ID.toString() === data.ID.toString());
|
||||
|
||||
// Flags "refresh" if we need to redraw the character
|
||||
if (!Refresh)
|
||||
if ((Char.Description != data.Description) || (Char.Title != data.Title) || (Char.Nickname != data.Nickname) || (Char.LabelColor != data.LabelColor) || (ChatRoomData == null) || (ChatRoomData.Character == null))
|
||||
if ((Char.Description != data.Description) || (Char.Title != data.Title) || (Char.Nickname != data.Nickname) || (Char.LabelColor != data.LabelColor) || (ChatRoomData?.Character == null))
|
||||
Refresh = true;
|
||||
else
|
||||
for (let C = 0; C < ChatRoomData.Character.length; C++)
|
||||
if (ChatRoomData.Character[C].ID == data.ID)
|
||||
if (ChatRoomData.Character[C].Appearance.length != data.Appearance.length)
|
||||
if (ChatRoomData?.Character[C]?.Appearance?.length != data.Appearance?.length)
|
||||
Refresh = true;
|
||||
else
|
||||
for (let A = 0; A < data.Appearance.length && !Refresh; A++) {
|
||||
const Old = ChatRoomData.Character[C].Appearance[A];
|
||||
const New = data.Appearance[A];
|
||||
if ((New.Name != Old.Name) || (New.Group != Old.Group) || (New.Color != Old.Color)) Refresh = true;
|
||||
else if ((New.Property != null) && (Old.Property != null) && (JSON.stringify(New.Property) != JSON.stringify(Old.Property))) Refresh = true;
|
||||
else if (((New.Property != null) && (Old.Property == null)) || ((New.Property == null) && (Old.Property != null))) Refresh = true;
|
||||
for (let A = 0; A < (data.Appearance?.length ?? 0) && !Refresh; A++) {
|
||||
const Old = ChatRoomData?.Character[C]?.Appearance?.[A];
|
||||
const New = data.Appearance?.[A];
|
||||
if ((New?.Name !== Old?.Name) || (New?.Group !== Old?.Group) || JSON.stringify(New?.Color) !== JSON.stringify(Old?.Color)) Refresh = true;
|
||||
else if ((New?.Property != null) && (Old?.Property != null) && (JSON.stringify(New?.Property) !== JSON.stringify(Old.Property))) Refresh = true;
|
||||
else if (((New?.Property != null) && (Old?.Property == null)) || ((New?.Property == null) && (Old?.Property != null))) Refresh = true;
|
||||
}
|
||||
|
||||
// Flags "refresh" if the ownership or lovership or inventory or blockitems or limiteditems has changed
|
||||
|
|
@ -1423,7 +1439,7 @@ function CharacterLoadAttributes(C) {
|
|||
const attributes = new Set();
|
||||
C.Attribute = [];
|
||||
for (const item of C.Appearance) {
|
||||
const itemAttrs = InventoryGetItemProperty(item, "Attribute");
|
||||
const itemAttrs = InventoryGetItemProperty(item, "Attribute") ?? [];
|
||||
for (const attribute of itemAttrs) {
|
||||
attributes.add(attribute);
|
||||
}
|
||||
|
|
@ -1434,15 +1450,17 @@ function CharacterLoadAttributes(C) {
|
|||
/**
|
||||
* Returns a list of effects for a character from some or all groups
|
||||
* @param {Character} C - The character to check
|
||||
* @param {readonly AssetGroupName[]} [Groups=null] - Optional: The list of groups to consider. If none defined, check all groups
|
||||
* @param {readonly AssetGroupName[] | undefined} [Groups=null] - Optional: The list of groups to consider. If none defined, check all groups
|
||||
* @param {boolean} [AllowDuplicates=false] - Optional: If true, keep duplicates of the same effect provided they're taken from different groups
|
||||
* @returns {EffectName[]} - A list of effects
|
||||
*/
|
||||
function CharacterGetEffects(C, Groups = null, AllowDuplicates = false) {
|
||||
function CharacterGetEffects(C, Groups = undefined, AllowDuplicates = false) {
|
||||
/** @type {EffectName[]} */
|
||||
let totalEffects = [];
|
||||
C.Appearance
|
||||
.filter(A => !Array.isArray(Groups) || Groups.length == 0 || Groups.includes(A.Asset.Group.Name))
|
||||
.forEach(item => {
|
||||
/** @type {EffectName[]} */
|
||||
let itemEffects = [];
|
||||
if (item.Property && Array.isArray(item.Property.Effect)) {
|
||||
CommonArrayConcatDedupe(itemEffects, item.Property.Effect);
|
||||
|
|
@ -1472,7 +1490,7 @@ function CharacterLoadTints(C) {
|
|||
/** @type {ResolvedTintDefinition[]} */
|
||||
const tints = [];
|
||||
for (const item of C.Appearance) {
|
||||
tints.push(...InventoryGetItemProperty(item, "Tint").map(({Color, Strength, DefaultColor}) => ({Color, Strength, DefaultColor, Item: item})));
|
||||
tints.push(...(InventoryGetItemProperty(item, "Tint") ?? []).map(({Color, Strength, DefaultColor}) => ({Color, Strength, DefaultColor, Item: item})));
|
||||
}
|
||||
C.Tints = tints;
|
||||
}
|
||||
|
|
@ -1569,7 +1587,7 @@ function CharacterRefresh(C, Push = true, RefreshDialog = true) {
|
|||
C.RunScripts = (
|
||||
!C.IsOnline()
|
||||
|| C.IsPlayer()
|
||||
|| !(Player.OnlineSettings && Player.OnlineSettings.DisableAnimations)
|
||||
|| !Player.OnlineSettings.DisableAnimations
|
||||
) && (
|
||||
!C.IsGhosted()
|
||||
);
|
||||
|
|
@ -1578,7 +1596,7 @@ function CharacterRefresh(C, Push = true, RefreshDialog = true) {
|
|||
if (C.IsPlayer()) {
|
||||
// Grab the first custom background that we can find
|
||||
const customBGItem = C.Appearance.find(item => item.Property?.CustomBlindBackground);
|
||||
C.CustomBackground = customBGItem ? customBGItem.Property.CustomBlindBackground : undefined;
|
||||
C.CustomBackground = customBGItem ? customBGItem.Property?.CustomBlindBackground : undefined;
|
||||
}
|
||||
|
||||
if (C.IsPlayer() && Push) {
|
||||
|
|
@ -1598,7 +1616,7 @@ function CharacterRefresh(C, Push = true, RefreshDialog = true) {
|
|||
|
||||
// Ensure that any color and/or opacity changes that occur while one is wearing `ItemColorItem`
|
||||
// are sanitized, ensuring that aforementioned properties are represented via their array-based variant
|
||||
if (C.Appearance.includes(ItemColorItem)) {
|
||||
if (ItemColorItem && C.Appearance.includes(ItemColorItem)) {
|
||||
ItemColorItem = Object.assign(
|
||||
ItemColorItem,
|
||||
{ Color: ItemColorSanitizeColor(ItemColorItem), Property: ItemColorSanitizeProperty(ItemColorItem) },
|
||||
|
|
@ -1654,19 +1672,14 @@ function CharacterRefreshDialog(C) {
|
|||
}
|
||||
|
||||
// Replace the focus items from underneath us so we get the updated data
|
||||
if (wasLock) {
|
||||
DialogFocusItem = lock;
|
||||
DialogFocusSourceItem = focusItem;
|
||||
} else {
|
||||
DialogFocusItem = focusItem;
|
||||
}
|
||||
[DialogFocusItem, DialogFocusSourceItem] = wasLock ? [lock, focusItem] : [focusItem, null];
|
||||
|
||||
// Reset the cached extended item requirement checks
|
||||
if (DialogFocusItem.Asset.Extended) {
|
||||
if (/** @type {Item} */ (DialogFocusItem).Asset.Extended) {
|
||||
ExtendedItemRequirementCheckMessageMemo.clearCache();
|
||||
}
|
||||
} else if (DialogMenuMode === "colorItem") {
|
||||
const itemRemovedOrDifferent = !focusItem || InventoryGetItemProperty(ItemColorItem, "Name") !== InventoryGetItemProperty(focusItem, "Name");
|
||||
const itemRemovedOrDifferent = !focusItem || ItemColorItem && InventoryGetItemProperty(ItemColorItem, "Name") !== InventoryGetItemProperty(focusItem, "Name");
|
||||
if (itemRemovedOrDifferent) {
|
||||
ItemColorCancelAndExit();
|
||||
DialogChangeMode("items");
|
||||
|
|
@ -1773,7 +1786,7 @@ function CharacterRandomUnderwear(C) {
|
|||
var Color = "";
|
||||
for (const G of AssetGroup)
|
||||
if ((G.Category == "Appearance") && G.Underwear && (G.IsDefault || (Math.random() < 0.2))) {
|
||||
if (Color == "") Color = CommonRandomItemFromList(null, G.ColorSchema);
|
||||
if (Color == "") Color = CommonRandomItemFromList("Default", G.ColorSchema);
|
||||
const Group = G.Asset
|
||||
.filter(A => InventoryAvailable(C, A.Name, G.Name));
|
||||
if (Group.length > 0)
|
||||
|
|
@ -1837,7 +1850,7 @@ function CharacterRelease(C, Refresh) {
|
|||
*/
|
||||
function CharacterReleaseFromLock(C, LockName) {
|
||||
for (let A = 0; A < C.Appearance.length; A++)
|
||||
if ((C.Appearance[A].Property != null) && (C.Appearance[A].Property.LockedBy == LockName))
|
||||
if (C.Appearance[A].Property?.LockedBy === LockName)
|
||||
InventoryUnlock(C, C.Appearance[A]);
|
||||
}
|
||||
|
||||
|
|
@ -1848,7 +1861,7 @@ function CharacterReleaseFromLock(C, LockName) {
|
|||
*/
|
||||
function CharacterReleaseNoLock(C) {
|
||||
for (let E = C.Appearance.length - 1; E >= 0; E--)
|
||||
if (C.Appearance[E].Asset.IsRestraint && ((C.Appearance[E].Property == null) || (C.Appearance[E].Property.LockedBy == null))) {
|
||||
if (C.Appearance[E].Asset.IsRestraint && !C.Appearance[E].Property?.LockedBy) {
|
||||
C.Appearance.splice(E, 1);
|
||||
}
|
||||
CharacterRefresh(C);
|
||||
|
|
@ -1865,7 +1878,8 @@ function CharacterReleaseTotal(C, refresh=true) {
|
|||
if (C.Appearance[E].Asset.Group.Category != "Appearance") {
|
||||
if (C.IsOwned() && C.Appearance[E].Asset.Name == "SlaveCollar") {
|
||||
// Reset slave collar to the default model if it has a gameplay effect (such as gagging the player)
|
||||
if (C.Appearance[E].Property && C.Appearance[E].Property.Effect && C.Appearance[E].Property.Effect.length > 0) {
|
||||
const effects = InventoryGetItemProperty(C.Appearance[E], "Effect");
|
||||
if (effects?.length) {
|
||||
C.Appearance[E].Property = CommonCloneDeep(InventoryItemNeckSlaveCollarTypes[0].Property);
|
||||
}
|
||||
}
|
||||
|
|
@ -1911,12 +1925,12 @@ function CharacterFullRandomRestrain(C, Ratio, Refresh) {
|
|||
}
|
||||
|
||||
// Apply each item if needed
|
||||
if (InventoryGet(C, "ItemArms") == null) InventoryWearRandom(C, "ItemArms", null, false);
|
||||
if ((Math.random() >= RatioRare) && (InventoryGet(C, "ItemHead") == null)) InventoryWearRandom(C, "ItemHead", null, false);
|
||||
if ((Math.random() >= RatioNormal) && (InventoryGet(C, "ItemMouth") == null)) InventoryWearRandom(C, "ItemMouth", null, false);
|
||||
if ((Math.random() >= RatioRare) && (InventoryGet(C, "ItemNeck") == null)) InventoryWearRandom(C, "ItemNeck", null, false);
|
||||
if ((Math.random() >= RatioNormal) && (InventoryGet(C, "ItemLegs") == null)) InventoryWearRandom(C, "ItemLegs", null, false);
|
||||
if ((Math.random() >= RatioNormal) && !C.IsKneeling() && (InventoryGet(C, "ItemFeet") == null)) InventoryWearRandom(C, "ItemFeet", null, false);
|
||||
if (!InventoryGet(C, "ItemArms")) InventoryWearRandom(C, "ItemArms", undefined, false);
|
||||
if ((Math.random() >= RatioRare) && !InventoryGet(C, "ItemHead")) InventoryWearRandom(C, "ItemHead", undefined, false);
|
||||
if ((Math.random() >= RatioNormal) && !InventoryGet(C, "ItemMouth")) InventoryWearRandom(C, "ItemMouth", undefined, false);
|
||||
if ((Math.random() >= RatioRare) && !InventoryGet(C, "ItemNeck")) InventoryWearRandom(C, "ItemNeck", undefined, false);
|
||||
if ((Math.random() >= RatioNormal) && !InventoryGet(C, "ItemLegs")) InventoryWearRandom(C, "ItemLegs", undefined, false);
|
||||
if ((Math.random() >= RatioNormal) && !C.IsKneeling() && !InventoryGet(C, "ItemFeet")) InventoryWearRandom(C, "ItemFeet", undefined, false);
|
||||
|
||||
if (Refresh || Refresh == null) CharacterRefresh(C);
|
||||
|
||||
|
|
@ -1931,7 +1945,7 @@ function CharacterFullRandomRestrain(C, Ratio, Refresh) {
|
|||
*
|
||||
* @param {Character} C - Character for which to set the expression of
|
||||
* @param {ExpressionGroupName | "Eyes1"} AssetGroup - Asset group for the expression
|
||||
* @param {ExpressionName} Expression - Name of the expression to use
|
||||
* @param {ExpressionName | undefined} Expression - Name of the expression to use
|
||||
* @param {number} [Timer] - Optional: time the expression will last, in seconds. Will send a null expression to expression queue. If expression to set is null, this is ignored.
|
||||
* @param {ItemColor} [Color] - Optional: color of the expression to set
|
||||
* @param {boolean} [fromQueue] - Internal: used to skip queuing the expression change if it comes from the queued expressions
|
||||
|
|
@ -1993,7 +2007,7 @@ function CharacterResetFacialExpression(C) {
|
|||
name = "Eyes1";
|
||||
}
|
||||
const color = item.Color;
|
||||
CharacterSetFacialExpression(C, name, null, null, color);
|
||||
CharacterSetFacialExpression(C, name, null, undefined, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2008,8 +2022,8 @@ function CharacterResetFacialExpression(C) {
|
|||
function CharacterIsExpressionDisallowed(C, Item, Expression) {
|
||||
if (!C || !Item) return "Internal error: missing character or item";
|
||||
|
||||
const allowedExpr = InventoryGetItemProperty(Item, "AllowExpression", true);
|
||||
const exprPres = InventoryGetItemProperty(Item, "ExpressionPrerequisite", true);
|
||||
const allowedExpr = InventoryGetItemProperty(Item, "AllowExpression", true) ?? [];
|
||||
const exprPres = InventoryGetItemProperty(Item, "ExpressionPrerequisite", true) ?? [];
|
||||
const exprPre = exprPres[allowedExpr.indexOf(Expression)];
|
||||
const prereqMessage = !exprPre ? null : InventoryPrerequisiteMessage(C, exprPre, Item.Asset);
|
||||
|
||||
|
|
@ -2047,18 +2061,18 @@ function CharacterGetCurrent() {
|
|||
|
||||
/**
|
||||
* Compresses a character wardrobe from an array to a LZ string to use less storage space
|
||||
* @param {readonly ItemBundle[][]} Wardrobe - Uncompressed wardrobe
|
||||
* @param {readonly (ItemBundle[] | null)[]} Wardrobe - Uncompressed wardrobe
|
||||
* @returns {string} - The compressed wardrobe
|
||||
*/
|
||||
function CharacterCompressWardrobe(Wardrobe) {
|
||||
if (CommonIsArray(Wardrobe) && (Wardrobe.length > 0)) {
|
||||
var CompressedWardrobe = [];
|
||||
for (let W = 0; W < Wardrobe.length; W++) {
|
||||
for (const outfit of Wardrobe) {
|
||||
/** @type {WardrobeItemBundle[]} */
|
||||
var Arr = [];
|
||||
if (Wardrobe[W] != null)
|
||||
for (let A = 0; A < Wardrobe[W].length; A++)
|
||||
Arr.push([Wardrobe[W][A].Name, Wardrobe[W][A].Group, Wardrobe[W][A].Color, Wardrobe[W][A].Property]);
|
||||
const Arr = [];
|
||||
for (const bundle of outfit ?? []) {
|
||||
Arr.push([bundle.Name, bundle.Group, bundle.Color, bundle.Property]);
|
||||
}
|
||||
CompressedWardrobe.push(Arr);
|
||||
}
|
||||
return LZString.compressToUTF16(JSON.stringify(CompressedWardrobe));
|
||||
|
|
@ -2112,7 +2126,7 @@ function CharacterDecompressWardrobe(Wardrobe) {
|
|||
*/
|
||||
function CharacterHasItemWithAttribute(C, Attribute) {
|
||||
return C.Appearance.some(item => {
|
||||
return InventoryGetItemProperty(item, "Attribute").includes(Attribute);
|
||||
return InventoryGetItemProperty(item, "Attribute")?.includes(Attribute);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2124,7 +2138,7 @@ function CharacterHasItemWithAttribute(C, Attribute) {
|
|||
*/
|
||||
function CharacterItemsForActivity(C, Activity) {
|
||||
return C.Appearance.filter(item => {
|
||||
return InventoryGetItemProperty(item, "AllowActivity").includes(Activity);
|
||||
return InventoryGetItemProperty(item, "AllowActivity")?.includes(Activity);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2155,7 +2169,7 @@ function CharacterIsEdged(C) {
|
|||
if (!Group.IsItem()) continue;
|
||||
if (Group.ArousalZoneID != null) {
|
||||
let Zone = PreferenceGetArousalZone(C, Group.Name);
|
||||
if (Zone.Orgasm && (Zone.Factor > 0))
|
||||
if (Zone && Zone.Orgasm && (Zone.Factor > 0))
|
||||
OrgasmZones.push(Zone.Name);
|
||||
}
|
||||
}
|
||||
|
|
@ -2172,7 +2186,7 @@ function CharacterIsEdged(C) {
|
|||
);
|
||||
|
||||
// Return true if every vibrating item on an orgasm zone has the "Edged" effect
|
||||
return !!VibratingItems.length && VibratingItems.every(Item => Item.Property.Effect && Item.Property.Effect.includes("Edged"));
|
||||
return !!VibratingItems.length && VibratingItems.every(Item => Item.Property?.Effect?.includes("Edged"));
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -2184,11 +2198,7 @@ function CharacterIsEdged(C) {
|
|||
*/
|
||||
function CharacterHasBlockedItem(C, BlockList) {
|
||||
if ((BlockList == null) || !CommonIsArray(BlockList) || (BlockList.length == 0)) return false;
|
||||
for (let B = 0; B < BlockList.length; B++)
|
||||
for (let A = 0; A < C.Appearance.length; A++)
|
||||
if ((C.Appearance[A].Asset != null) && (C.Appearance[A].Asset.Category != null) && (C.Appearance[A].Asset.Category.indexOf(BlockList[B]) >= 0))
|
||||
return true;
|
||||
return false;
|
||||
return BlockList.some(category => C.Appearance.some(item => item.Asset.Category?.some(itemCategory => category === itemCategory)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2337,7 +2347,7 @@ function CharacterCheckHooks(C, IgnoreHooks) {
|
|||
// Fancy logic is to use a different hook for when the character is focused
|
||||
const layerVisibilityHook = () => {
|
||||
const inDialog = (CurrentCharacter != null);
|
||||
C.AppearanceLayers = C.AppearanceLayers.filter((Layer) => (
|
||||
C.AppearanceLayers = C.AppearanceLayers?.filter((Layer) => (
|
||||
!Layer.Visibility ||
|
||||
(Layer.Visibility == "Player" && C.IsPlayer()) ||
|
||||
(Layer.Visibility == "AllExceptPlayerDialog" && !(inDialog && C.IsPlayer())) ||
|
||||
|
|
@ -2368,12 +2378,13 @@ function CharacterCheckHooks(C, IgnoreHooks) {
|
|||
* @param {Character} FromC - The character from which to pick the item
|
||||
* @param {Character} ToC - The character on which we must put the item
|
||||
* @param {AssetGroupName} Group - The item group to transfer (Cloth, Hat, etc.)
|
||||
* @param {boolean} [Refresh] - Perform a character refresh
|
||||
* @returns {void} - Nothing
|
||||
*/
|
||||
function CharacterTransferItem(FromC, ToC, Group, Refresh) {
|
||||
function CharacterTransferItem(FromC, ToC, Group, Refresh=true) {
|
||||
let Item = InventoryGet(FromC, Group);
|
||||
if (Item == null) return;
|
||||
InventoryWear(ToC, Item.Asset.Name, Group, Item.Color, Item.Difficulty);
|
||||
InventoryWear(ToC, Item.Asset.Name, Group, Item.Color, Item.Difficulty, undefined, undefined, false);
|
||||
if (Refresh) CharacterRefresh(ToC);
|
||||
}
|
||||
|
||||
|
|
@ -2404,11 +2415,13 @@ function CharacterClearOwnership(C, push=true) {
|
|||
if (C.IsPlayer()) {
|
||||
const ownerType = C.IsOwned();
|
||||
switch (ownerType) {
|
||||
case "online":
|
||||
ServerSend("AccountOwnership", { MemberNumber: C.Ownership.MemberNumber, Action: "Break" });
|
||||
case "online": {
|
||||
const number = C.Ownership?.MemberNumber ?? -1; // Can't happen; protected by `C.IsOwned()`
|
||||
ServerSend("AccountOwnership", { MemberNumber: number, Action: "Break" });
|
||||
C.Owner = "";
|
||||
C.Ownership = null;
|
||||
break;
|
||||
}
|
||||
|
||||
case "npc":
|
||||
C.Owner = "";
|
||||
|
|
@ -2463,7 +2476,8 @@ function CharacterPronoun(C, DialogKey, HideIdentity) {
|
|||
*/
|
||||
function CharacterPronounDescription(C) {
|
||||
const pronounAsset = AssetGet(C.AssetFamily, "Pronouns", C.GetPronouns());
|
||||
return pronounAsset.Description;
|
||||
// Pronouns is part of the default appearance
|
||||
return /** @type {string} */ (pronounAsset?.Description);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2483,13 +2497,13 @@ function CharacterCanChangeNickname(C) {
|
|||
* Note that changing any nickname but yours (ie. Player) is not supported.
|
||||
*
|
||||
* @param {Character} C - The character to change the nickname of.
|
||||
* @param {string} Nick - The name to use as the new nickname. An empty string uses the character's real name.
|
||||
* @param {null | string} Nick - The name to use as the new nickname. An empty string uses the character's real name.
|
||||
* @return {null | NicknameStatus} null if the nickname was valid, or an explanation for why the nickname was rejected.
|
||||
*/
|
||||
function CharacterSetNickname(C, Nick, fromOwner = false) {
|
||||
if (!C.IsPlayer()) return null;
|
||||
|
||||
Nick = Nick.trim();
|
||||
Nick = (Nick ?? "").trim();
|
||||
// Same nickname, or setting an empty nickname with no nickname already
|
||||
if (C.Nickname === Nick || Nick.length === 0 && !C.Nickname) return null;
|
||||
|
||||
|
|
@ -2506,6 +2520,8 @@ function CharacterSetNickname(C, Nick, fromOwner = false) {
|
|||
}
|
||||
C.Nickname = Nick;
|
||||
}
|
||||
// @ts-ignore Strict-TS: Types only say 'undefined', but we need `null`
|
||||
// to survive JSON serialization to the server
|
||||
ServerAccountUpdate.QueueData({ Nickname: Nick });
|
||||
|
||||
if (ServerPlayerIsInChatRoom()) {
|
||||
|
|
@ -2559,6 +2575,7 @@ function CharacterSetOwnersNotes(C, notes = undefined) {
|
|||
C.Ownership.Notes = undefined;
|
||||
}
|
||||
|
||||
// @ts-ignore Strict-TS: Only OnlineCharacters have a MemberNumber
|
||||
ServerSend("AccountOwnership", { MemberNumber: C.MemberNumber, Action: "UpdateNotes", Notes });
|
||||
}
|
||||
}
|
||||
|
|
@ -2610,18 +2627,13 @@ function CharacterRefreshLeash(C) {
|
|||
* @returns {Item}
|
||||
*/
|
||||
function CharacterScriptGet(C) {
|
||||
let script = InventoryGet(C, "ItemScript");
|
||||
if (!script) {
|
||||
InventoryWear(C, "Script", "ItemScript");
|
||||
script = InventoryGet(C, "ItemScript");
|
||||
}
|
||||
|
||||
let script = InventoryGet(C, "ItemScript") ?? /** @type {Item} */ (InventoryWear(C, "Script", "ItemScript"));
|
||||
script.Property = script.Property || {};
|
||||
// Propagate change and try to reload the item. If the script permissions
|
||||
// on the target were wrong, then it'll be null
|
||||
CharacterScriptRefresh(C);
|
||||
|
||||
script = InventoryGet(C, "ItemScript");
|
||||
script = /** @type {Item} */ (InventoryGet(C, "ItemScript"));
|
||||
return script;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ function CommonDynamicFunction(FunctionName) {
|
|||
/**
|
||||
* Calls a dynamic function with parameters (if it exists), also allow ! in front to reverse the result. The dynamic function is the provided function name in the dialog option object and it is prefixed by the current screen.
|
||||
* @param {string} FunctionName - Function name to call dynamically
|
||||
* @returns {unknown | boolean} - Returns what the dynamic function returns or FALSE if the function does not exist
|
||||
* @returns {unknown} - Returns what the dynamic function returns, or throws if it can't be called
|
||||
*/
|
||||
function CommonDynamicFunctionParams(FunctionName) {
|
||||
|
||||
|
|
@ -424,23 +424,21 @@ function CommonDynamicFunctionParams(FunctionName) {
|
|||
for (let P = 0; P < Params.length; P++)
|
||||
Params[P] = Params[P].trim().replace('"', '').replace('"', '');
|
||||
FunctionName = FunctionName.substring(0, openParenthesisIndex);
|
||||
if ((FunctionName.indexOf("Dialog") != 0) && (FunctionName.indexOf("Inventory") != 0) && (FunctionName.indexOf(CurrentScreen) != 0)) FunctionName = CurrentScreen + FunctionName;
|
||||
if (["Dialog", "Inventory", CurrentScreen].every(s => !FunctionName.startsWith(s))) {
|
||||
FunctionName = CurrentScreen + FunctionName;
|
||||
}
|
||||
|
||||
// If it's really a function, we continue
|
||||
/** @type {Record<string, any>} */
|
||||
const namespace = window;
|
||||
const func = namespace[FunctionName];
|
||||
if (typeof func === "function") {
|
||||
|
||||
// Launches the function with the params and returns the result
|
||||
const res = func(...Params);
|
||||
return Reverse ? !res : res;
|
||||
} else {
|
||||
|
||||
// Log the error in the console
|
||||
console.log("Trying to launch invalid function: " + FunctionName);
|
||||
return false;
|
||||
if (typeof func !== "function") {
|
||||
throw new Error("CommonDynamicFunctionParams: Invalid function name: " + FunctionName);
|
||||
}
|
||||
|
||||
// Launches the function with the params and returns the result
|
||||
const res = func(...Params);
|
||||
return Reverse ? !res : res;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
|
|
@ -210,10 +209,10 @@ function ControllerLoadMapping(buttonsMapping, axisMapping) {
|
|||
}
|
||||
}
|
||||
}
|
||||
for (const btnIdx of Object.keys(ControllerButtonMapping)) {
|
||||
for (const btnIdx of CommonKeys(ControllerButtonMapping)) {
|
||||
ControllerButtonMapping[btnIdx] = buttonsMapping[btnIdx] ?? -1;
|
||||
}
|
||||
for (const axisIdx of Object.keys(ControllerAxisMapping)) {
|
||||
for (const axisIdx of CommonKeys(ControllerAxisMapping)) {
|
||||
ControllerAxisMapping[axisIdx] = axisMapping[axisIdx] ?? -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -306,6 +305,12 @@ function ControllerProcessAxis(axes) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ControllerAxis} axisId
|
||||
* @param {(val: number) => void} handler
|
||||
* @returns
|
||||
*/
|
||||
function handleAxis(axisId, handler) {
|
||||
const padAxisId = ControllerAxisMapping[axisId];
|
||||
const val = axes[padAxisId] ?? undefined;
|
||||
|
|
@ -342,12 +347,13 @@ function ControllerProcessAxis(axes) {
|
|||
function ControllerManagedByGame(buttons) {
|
||||
|
||||
// Map the gamepad button indexes to the game's button names
|
||||
/** @type {GamepadButton[]} */
|
||||
const mappedButtons = [];
|
||||
for (const btnId of Object.values(ControllerButton)) {
|
||||
const padBtnId = ControllerButtonMapping[btnId];
|
||||
if (padBtnId === -1) continue;
|
||||
// Grab either the actual button or make a dummy, in case the player has it unmapped
|
||||
mappedButtons[btnId] ??= buttons[padBtnId] ?? { pressed: false, repeat: false };
|
||||
mappedButtons[btnId] ??= buttons[padBtnId] ?? { pressed: false, repeat: false, touched: false, value: 0 };
|
||||
}
|
||||
|
||||
// If the screen manages the controller, we call it
|
||||
|
|
@ -373,6 +379,8 @@ function ControllerProcessButton(buttons) {
|
|||
|
||||
/**
|
||||
* Helper function to process a button press
|
||||
* @param {ControllerButton} btnId
|
||||
* @param {() => void} handler
|
||||
*/
|
||||
function handleButton(btnId, handler) {
|
||||
if (ControllerButtonsWaitRelease) return;
|
||||
|
|
@ -394,7 +402,7 @@ function ControllerProcessButton(buttons) {
|
|||
|
||||
handleButton(ControllerButton.A, () => {
|
||||
if (!ControllerDPadAsAxisWorkaround) return;
|
||||
// Trigger a fake click event
|
||||
// @ts-ignore Strict-TS: Trigger a fake click event
|
||||
CommonClick(null);
|
||||
});
|
||||
handleButton(ControllerButton.B, () => {
|
||||
|
|
@ -467,8 +475,10 @@ function ControllerCalibrationNextStage(skip = false) {
|
|||
if (skip) {
|
||||
// We're skipping, unset the value for that input
|
||||
if (isAxis) {
|
||||
// @ts-ignore Strict-TS: the initialization above and the check below should ensure we stay in bounds
|
||||
ControllerAxisMapping[stage] = -1;
|
||||
} else {
|
||||
// @ts-ignore Strict-TS: the initialization above and the check below should ensure we stay in bounds
|
||||
ControllerButtonMapping[stage] = -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -557,6 +567,7 @@ function ControllerCalibrationStageLabel() {
|
|||
case ControllerAxis.StickRH:
|
||||
return TextGet("MoveRightStickRight");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const ControllerCalibrationLowWatermark = 0.05;
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ var DialogTightenLoosenItem = null;
|
|||
* @type {Item|null}
|
||||
*/
|
||||
var DialogFocusSourceItem = null;
|
||||
/** @type {null | ReturnType<typeof setTimeout>} */
|
||||
var DialogFocusItemColorizationRedrawTimer = null;
|
||||
/** @type {ReturnType<typeof setTimeout>} */
|
||||
var DialogFocusItemColorizationRedrawTimer = /** @type {never} */ (null);
|
||||
/**
|
||||
* The list of currently visible menu item buttons.
|
||||
* @type {DialogMenuButton[]}
|
||||
|
|
@ -70,7 +70,7 @@ var DialogMenuMode = null;
|
|||
|
||||
/**
|
||||
* The group that was selected before we entered the expression coloring screen
|
||||
* @type {{mode: DialogMenuMode, group: AssetItemGroup}}
|
||||
* @type {{mode: DialogMenuMode, group: AssetItemGroup} | null}
|
||||
*/
|
||||
var DialogExpressionPreviousMode = null;
|
||||
|
||||
|
|
@ -106,17 +106,17 @@ var DialogGamingReturnScreen = null;
|
|||
var DialogButtonDisabledTester = /Disabled(For\w+)?$/u;
|
||||
/**
|
||||
* The attempted action that's leading the player to struggle.
|
||||
* @type {DialogStruggleActionType?}
|
||||
* @type {DialogStruggleActionType | null}
|
||||
*/
|
||||
let DialogStruggleAction = null;
|
||||
/**
|
||||
* The item we're struggling out of, or swapping from.
|
||||
* @type {Item}
|
||||
* @type {Item | null}
|
||||
*/
|
||||
let DialogStrugglePrevItem = null;
|
||||
/**
|
||||
* The item we're swapping to.
|
||||
* @type {Item}
|
||||
* @type {Item | null}
|
||||
*/
|
||||
let DialogStruggleNextItem = null;
|
||||
/** Whether we went through the struggle selection screen or went straight through. */
|
||||
|
|
@ -151,7 +151,6 @@ var DialogFavoriteStateDetails = [
|
|||
{
|
||||
TargetFavorite: false,
|
||||
PlayerFavorite: false,
|
||||
Icon: null,
|
||||
UsableOrder: DialogSortOrder.Usable,
|
||||
UnusableOrder: DialogSortOrder.Unusable
|
||||
},
|
||||
|
|
@ -182,11 +181,11 @@ var DialogLeaveFocusItemHandlers = {
|
|||
DialogTightenLoosenItem: {
|
||||
Crafting: (item) => {
|
||||
// Subtract deterministic modifiers so that only the difficulty factor remains
|
||||
CraftingSelectedItem.DifficultyFactor = (
|
||||
item.Difficulty
|
||||
/** @type {CraftingItemSelected} */ (CraftingSelectedItem).DifficultyFactor = (
|
||||
(item.Difficulty ?? 0)
|
||||
- SkillGetLevel(Player, "Bondage")
|
||||
- item.Asset.Difficulty
|
||||
- (item.Craft.Effects?.Secure ?? 0) * 4
|
||||
- (item.Craft?.Effects?.Secure ?? 0) * 4
|
||||
);
|
||||
item.Difficulty = item.Asset.Difficulty;
|
||||
CraftingModeSet("Name");
|
||||
|
|
@ -226,8 +225,10 @@ var DialogLeaveFocusItemHandlers = {
|
|||
* @returns {Character} - The actual character
|
||||
*/
|
||||
function DialogGetCharacter(C) {
|
||||
if (typeof C === "string")
|
||||
if (typeof C === "string") {
|
||||
// @ts-ignore Strict-TS: force-cast here because there's a bunch of side-effects to having this return `null`
|
||||
return (C.toUpperCase().trim() == "PLAYER") ? Player : CurrentCharacter;
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
|
|
@ -335,9 +336,8 @@ function DialogLogQuery(LogType, LogGroup) { return LogQuery(LogType, LogGroup);
|
|||
/**
|
||||
* Sets the AllowItem flag on the current character
|
||||
* @param {string} Allow - The flag to set. Either "TRUE" or "FALSE"
|
||||
* @returns {boolean} - The boolean version of the flag
|
||||
*/
|
||||
function DialogAllowItem(Allow) { return CurrentCharacter.AllowItem = (Allow.toUpperCase().trim() == "TRUE"); }
|
||||
function DialogAllowItem(Allow) { if (CurrentCharacter) CurrentCharacter.AllowItem = (Allow.toUpperCase().trim() == "TRUE"); }
|
||||
|
||||
/**
|
||||
* Returns the value of the AllowItem flag of a given character
|
||||
|
|
@ -350,9 +350,10 @@ function DialogDoAllowItem(C) { return !DialogDontAllowItemPermission(C); }
|
|||
|
||||
/**
|
||||
* Returns TRUE if the AllowItem flag doesn't allow putting an item on the current character
|
||||
* @param {string | Character} C
|
||||
* @returns {boolean} - The reversed value of the given character's AllowItem flag
|
||||
*/
|
||||
function DialogDontAllowItemPermission(C) { return !DialogGetCharacter(C ? C : CurrentCharacter).AllowItem; }
|
||||
function DialogDontAllowItemPermission(C) { return !DialogGetCharacter(C).AllowItem; }
|
||||
|
||||
/**
|
||||
* Returns TRUE if no item can be used by the player on the current character because of the map distance
|
||||
|
|
@ -377,19 +378,19 @@ function DialogIsKneeling(C) { return DialogGetCharacter(C).IsKneeling(); }
|
|||
* Determines if the player is owned by the current character
|
||||
* @returns {boolean} - Returns true, if the player is owned by the current character, false otherwise
|
||||
*/
|
||||
function DialogIsOwner() { return CurrentCharacter.IsOwner(); }
|
||||
function DialogIsOwner() { return CurrentCharacter?.IsOwner() ?? false; }
|
||||
|
||||
/**
|
||||
* Determines, if the current character is the player's lover
|
||||
* @returns {boolean} - Returns true, if the current character is one of the player's lovers
|
||||
*/
|
||||
function DialogIsLover() { return CurrentCharacter.IsLoverOfCharacter(Player); }
|
||||
function DialogIsLover() { return CurrentCharacter?.IsLoverOfCharacter(Player) ?? false; }
|
||||
|
||||
/**
|
||||
* Determines if the current character is owned by the player
|
||||
* @returns {boolean} - Returns true, if the current character is owned by the player, false otherwise
|
||||
*/
|
||||
function DialogIsProperty() { return CurrentCharacter.IsOwnedByPlayer(); }
|
||||
function DialogIsProperty() { return CurrentCharacter?.IsOwnedByPlayer() ?? false; }
|
||||
|
||||
/**
|
||||
* Checks, if a given character is currently restrained
|
||||
|
|
@ -431,7 +432,7 @@ function DialogCanInteract(C) { return DialogGetCharacter(C).CanInteract(); }
|
|||
* Can be omitted to bring the character back to the standing position.
|
||||
* @returns {void} - Nothing
|
||||
*/
|
||||
function DialogSetPose(C, NewPose) { PoseSetActive((C.toUpperCase().trim() == "PLAYER") ? Player : CurrentCharacter, NewPose || null, true); }
|
||||
function DialogSetPose(C, NewPose) { PoseSetActive(DialogGetCharacter(C), NewPose || null, true); }
|
||||
|
||||
/**
|
||||
* Checks, whether a given skill of the player is greater or equal a given value
|
||||
|
|
@ -529,22 +530,56 @@ function DialogGGTSInteraction(Interaction) {
|
|||
* @returns {boolean} - Returns true, if the prerequisite is met, false otherwise
|
||||
*/
|
||||
function DialogPrerequisite(dialog) {
|
||||
if (dialog.Prerequisite == null)
|
||||
return true;
|
||||
else if (dialog.Prerequisite.indexOf("Player.") == 0)
|
||||
return Player[dialog.Prerequisite.substring(7, 250).replace("()", "").trim()]();
|
||||
else if (dialog.Prerequisite.indexOf("!Player.") == 0)
|
||||
return !Player[dialog.Prerequisite.substring(8, 250).replace("()", "").trim()]();
|
||||
else if (dialog.Prerequisite.indexOf("CurrentCharacter.") == 0)
|
||||
return CurrentCharacter[dialog.Prerequisite.substring(17, 250).replace("()", "").trim()]();
|
||||
else if (dialog.Prerequisite.indexOf("!CurrentCharacter.") == 0)
|
||||
return !CurrentCharacter[dialog.Prerequisite.substring(18, 250).replace("()", "").trim()]();
|
||||
else if (dialog.Prerequisite.indexOf("(") >= 0)
|
||||
return !!CommonDynamicFunctionParams(dialog.Prerequisite);
|
||||
else if (dialog.Prerequisite.substring(0, 1) != "!")
|
||||
return !!window[CurrentScreen + dialog.Prerequisite.trim()];
|
||||
else
|
||||
return !window[CurrentScreen + dialog.Prerequisite.substr(1, 250).trim()];
|
||||
const prereq = dialog.Prerequisite?.trim();
|
||||
if (!prereq) return true;
|
||||
|
||||
const match = prereq.match(/^(!?)(Player|CurrentCharacter)\.(\w+)\(\)$/);
|
||||
if (match) {
|
||||
const [, neg, target, method] = match;
|
||||
const obj = target === "Player" ? Player : CurrentCharacter;
|
||||
|
||||
if (typeof obj[method] !== "function") {
|
||||
console.error(
|
||||
`DialogPrerequisite: Method '${method}' not found on ${target}`,
|
||||
{ prereq, dialog }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = obj[method]();
|
||||
return neg ? !result : result;
|
||||
}
|
||||
|
||||
if (prereq.indexOf("(") > 0) {
|
||||
try {
|
||||
return !!CommonDynamicFunctionParams(prereq);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`DialogPrerequisite: Failed dynamic expression`,
|
||||
{ prereq, dialog, err }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const negated = prereq.startsWith("!");
|
||||
const key = (negated ? prereq.slice(1) : prereq).trim();
|
||||
const globalKey = CurrentScreen + key;
|
||||
|
||||
if (globalKey in window) {
|
||||
const value = !!window[globalKey];
|
||||
return negated ? !value : value;
|
||||
}
|
||||
|
||||
console.error(
|
||||
`DialogPrerequisite: Unrecognized prerequisite format`,
|
||||
{
|
||||
prereq,
|
||||
dialog,
|
||||
}
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -624,11 +659,11 @@ function DialogHasKey(C, Item) {
|
|||
if (C.IsLoverOfPlayer() && InventoryAvailable(Player, "LoversPadlockKey", "ItemMisc") && Item.Asset.Enable && Item.Property && Item.Property.LockedBy && !Item.Property.LockedBy.startsWith("Owner")) return true;
|
||||
if (lock && lock.Asset.ExclusiveUnlock) {
|
||||
// Locks with exclusive access (intricate, high-sec)
|
||||
const allowedMembers = CommonConvertStringToArray(Item.Property.MemberNumberListKeys);
|
||||
const allowedMembers = CommonConvertStringToArray(Item.Property?.MemberNumberListKeys ?? "");
|
||||
// High-sec, check if we're in the keyholder list
|
||||
if (Item.Property.MemberNumberListKeys != null) return allowedMembers.includes(Player.MemberNumber);
|
||||
if (Item.Property?.MemberNumberListKeys != null) return allowedMembers.includes(Player.MemberNumber);
|
||||
// Intricate, check that we added that lock
|
||||
if (Item.Property.LockMemberNumber == Player.MemberNumber) return true;
|
||||
if (Item.Property?.LockMemberNumber == Player.MemberNumber) return true;
|
||||
}
|
||||
let UnlockName = /** @type {EffectName} */("Unlock" + Item.Asset.Name);
|
||||
if ((Item.Property != null) && (Item.Property.LockedBy != null))
|
||||
|
|
@ -740,7 +775,7 @@ function DialogAllowItemScreenException() {
|
|||
* @returns {string} - The name of the current dialog, if such a dialog exists, any empty string otherwise
|
||||
*/
|
||||
function DialogIntro(C) {
|
||||
const dialog = C?.Dialog.find(d => d.Stage == C.Stage && d.Option == null && d.Result != null && DialogPrerequisite(d));
|
||||
const dialog = C?.Dialog.find(d => d.Stage == C.Stage && d.Option === null && d.Result !== null && DialogPrerequisite(d));
|
||||
return dialog?.Result ?? "";
|
||||
}
|
||||
|
||||
|
|
@ -813,7 +848,8 @@ function DialogLeave(options=null) {
|
|||
*/
|
||||
function DialogRemove() {
|
||||
const C = CurrentCharacter;
|
||||
const dialogIndex = C.Dialog.findIndex(dialog => dialog.Stage === C.Stage && dialog.Option === C.ClickedOption && dialog.Option != null && DialogPrerequisite(dialog));
|
||||
if (!C) return;
|
||||
const dialogIndex = C.Dialog.findIndex(dialog => dialog.Stage === C.Stage && dialog.Option === C.ClickedOption && dialog.Option !== null && DialogPrerequisite(dialog));
|
||||
if (dialogIndex !== -1) {
|
||||
C.Dialog.splice(dialogIndex, 1);
|
||||
document.querySelector(`.dialog-dialog-button[data-index="${dialogIndex}"]`)?.remove();
|
||||
|
|
@ -827,11 +863,12 @@ function DialogRemove() {
|
|||
* @returns {void} - Nothing
|
||||
*/
|
||||
function DialogRemoveGroup(GroupName) {
|
||||
if (!CurrentCharacter) return;
|
||||
const GroupNameUpper = GroupName.trim().toUpperCase();
|
||||
document.querySelectorAll(`.dialog-dialog-button[data-group="${GroupNameUpper}" i]`).forEach(e => e.remove());
|
||||
document.querySelectorAll(".dialog-dialog-button").forEach((e, i) => e.setAttribute("data-index", i.toString()));
|
||||
for (let D = CurrentCharacter.Dialog.length - 1; D >= 0; D--)
|
||||
if ((CurrentCharacter.Dialog[D].Group != null) && (CurrentCharacter.Dialog[D].Group.trim().toUpperCase() == GroupNameUpper)) {
|
||||
if (CurrentCharacter.Dialog[D].Group?.trim().toUpperCase() === GroupNameUpper) {
|
||||
CurrentCharacter.Dialog.splice(D, 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -863,8 +900,9 @@ function DialogMenuBack() {
|
|||
break;
|
||||
|
||||
case "colorExpression": {
|
||||
if (!DialogExpressionPreviousMode) return;
|
||||
const { mode, group } = DialogExpressionPreviousMode;
|
||||
DialogChangeMode(mode || "dialog");
|
||||
DialogChangeMode(mode ?? "dialog");
|
||||
DialogChangeFocusToGroup(Player, group);
|
||||
DialogExpressionPreviousMode = null;
|
||||
}
|
||||
|
|
@ -911,7 +949,7 @@ function DialogMenuBack() {
|
|||
* Returns whether the current mode shows items.
|
||||
*/
|
||||
function DialogModeShowsInventory() {
|
||||
return ["items", "activities", "locking", "permissions"].includes(DialogMenuMode);
|
||||
return DialogMenuMode && ["items", "activities", "locking", "permissions"].includes(DialogMenuMode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -929,7 +967,9 @@ function DialogIsInWardrobe() {
|
|||
* @returns {void} - Nothing
|
||||
*/
|
||||
function DialogLeaveItemMenu() {
|
||||
DialogChangeFocusToGroup(CharacterGetCurrent(), null);
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C) return;
|
||||
DialogChangeFocusToGroup(C, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1081,13 +1121,13 @@ function DialogInventoryCreateItem(C, item, isWorn, sortOrder) {
|
|||
* Returns settings for an item based on whether the player and target have favorited it, if any
|
||||
* @param {Character} C - The targeted character
|
||||
* @param {Asset} asset - The asset to check favorite settings for
|
||||
* @param {string} [type=null] - The type of the asset to check favorite settings for
|
||||
* @param {string | null} [type] - The type of the asset to check favorite settings for
|
||||
* @returns {FavoriteState} - The details to use for the asset
|
||||
*/
|
||||
function DialogGetFavoriteStateDetails(C, asset, type = null) {
|
||||
const isTargetFavorite = InventoryIsFavorite(C, asset.Name, asset.Group.Name, type);
|
||||
const isPlayerFavorite = !C.IsPlayer() && InventoryIsFavorite(Player, asset.Name, asset.Group.Name, type);
|
||||
return DialogFavoriteStateDetails.find(F => F.TargetFavorite == isTargetFavorite && F.PlayerFavorite == isPlayerFavorite);
|
||||
return /** @type {FavoriteState} */ (DialogFavoriteStateDetails.find(F => F.TargetFavorite == isTargetFavorite && F.PlayerFavorite == isPlayerFavorite));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1117,6 +1157,7 @@ function DialogGetLockIcon(item, isWorn) {
|
|||
* @returns {InventoryIcon[]} - A list of icon names
|
||||
*/
|
||||
function DialogGetAssetIcons(asset) {
|
||||
/** @type {InventoryIcon[]} */
|
||||
let icons = [];
|
||||
icons = icons.concat(asset.PreviewIcons);
|
||||
if (asset.OwnerOnly) icons.push("OwnerOnly");
|
||||
|
|
@ -1154,7 +1195,7 @@ const DialogEffectIcons = /** @type {const} */({
|
|||
const craftingProperty = item.Craft?.Effects;
|
||||
return DialogEffectIcons.GetEffectIcons(effects, craftingProperty);
|
||||
},
|
||||
/** @type {(effects: Iterable<EffectName>, craftEffect?: Partial<Record<CraftingPropertyType, number>>) => null | InventoryIcon[]} */
|
||||
/** @type {(effects: Iterable<EffectName>, craftEffect?: Partial<Record<CraftingPropertyType, number>>) => InventoryIcon[]} */
|
||||
GetEffectIcons: function (effects, craftEffect) {
|
||||
/** @type {InventoryIcon[]} */
|
||||
const icons = [];
|
||||
|
|
@ -1202,7 +1243,7 @@ const DialogEffectIcons = /** @type {const} */({
|
|||
_GetDeafIcon(effect) {
|
||||
/** @type {InventoryIcon[]} */
|
||||
const keys = ["DeafLight", "DeafNormal", "DeafHeavy"];
|
||||
return keys.find(k => DialogEffectIcons.Table[k].includes(effect));
|
||||
return keys.find(k => DialogEffectIcons.Table[k]?.includes(effect));
|
||||
},
|
||||
/** @type {(level?: number) => null | InventoryIcon} */
|
||||
_GagLevelToIcon: function (level) {
|
||||
|
|
@ -1220,11 +1261,11 @@ const DialogEffectIcons = /** @type {const} */({
|
|||
},
|
||||
/** @type {(level?: number) => null | InventoryIcon} */
|
||||
_BlindLevelToIcon: function (level) {
|
||||
if (!level || level < CharacterBlindLevels.get("BlindLight")) {
|
||||
if (!level || level < (CharacterBlindLevels.get("BlindLight") ?? 0)) {
|
||||
return null;
|
||||
} else if (level <= CharacterBlindLevels.get("BlindLight")) {
|
||||
} else if (level <= (CharacterBlindLevels.get("BlindLight") ?? 0)) {
|
||||
return "BlindLight";
|
||||
} else if (level <= CharacterBlindLevels.get("BlindHeavy")) {
|
||||
} else if (level <= (CharacterBlindLevels.get("BlindHeavy") ?? 0)) {
|
||||
return "BlindNormal";
|
||||
} else {
|
||||
return "BlindHeavy";
|
||||
|
|
@ -1300,7 +1341,7 @@ function DialogCanInspectLock(Item) {
|
|||
if (!Item) return false;
|
||||
|
||||
const lockedBy = InventoryGetItemProperty(Item, "LockedBy");
|
||||
return !Player.IsBlind() || ["SafewordPadlock", "CombinationPadlock"].includes(lockedBy);
|
||||
return !Player.IsBlind() || !!lockedBy && ["SafewordPadlock", "CombinationPadlock"].includes(lockedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1335,14 +1376,14 @@ function DialogMenuButtonBuild(C) {
|
|||
DialogMenuButton = [];
|
||||
|
||||
// Hide "Exit" button for the screens where
|
||||
if (!["colorExpression", "colorItem"].includes(DialogMenuMode))
|
||||
if (DialogMenuMode && !["colorExpression", "colorItem"].includes(DialogMenuMode))
|
||||
DialogMenuButton = ["Exit"];
|
||||
|
||||
// There's no group focused, hence no menu to draw
|
||||
if (C.FocusGroup == null) return;
|
||||
|
||||
/** The item in the current slot */
|
||||
const Item = InventoryGet(C, C.FocusGroup.Name);
|
||||
const Item = /** @type {Item} */ (InventoryGet(C, C.FocusGroup.Name));
|
||||
const ItemBlockedOrLimited = !!Item && InventoryBlockedOrLimited(C, Item);
|
||||
const IsItemLocked = InventoryItemHasEffect(Item, "Lock", true);
|
||||
const IsGroupBlocked = InventoryGroupIsBlocked(C, C.FocusGroup.Name);
|
||||
|
|
@ -1446,7 +1487,7 @@ function DialogMenuButtonBuild(C) {
|
|||
}
|
||||
|
||||
if (!DialogMenuButton.includes("Use") && canUseRemoteState !== "InvalidItem") {
|
||||
/** @type {DialogMenuButton} */
|
||||
/** @type {DialogMenuButton | null} */
|
||||
let button = null;
|
||||
switch (canUseRemoteState) {
|
||||
case "Available":
|
||||
|
|
@ -1576,7 +1617,7 @@ function DialogInventoryBuild(C, resetOffset=false, locks=false, reload=true) {
|
|||
return;
|
||||
}
|
||||
|
||||
const CurItem = C.Appearance.find(A => A.Asset.Group.Name == C.FocusGroup.Name && A.Asset.DynamicAllowInventoryAdd(C));
|
||||
const CurItem = C.Appearance.find(A => A.Asset.Group.Name == C.FocusGroup?.Name && A.Asset.DynamicAllowInventoryAdd(C));
|
||||
|
||||
// In item permission mode we add all the enable items except the ones already on, unless on Extreme difficulty
|
||||
if (DialogMenuMode === "permissions") {
|
||||
|
|
@ -1585,7 +1626,7 @@ function DialogInventoryBuild(C, resetOffset=false, locks=false, reload=true) {
|
|||
continue;
|
||||
|
||||
if (A.Wear) {
|
||||
const isWorn = CurItem && CurItem.Asset.Name === A.Name && CurItem.Asset.Group.Name === A.Group.Name;
|
||||
const isWorn = CurItem?.Asset.Name === A.Name && CurItem?.Asset.Group.Name === A.Group.Name;
|
||||
DialogInventoryAdd(Player, { Asset: A }, isWorn, DialogSortOrder.Enabled);
|
||||
} else if (A.IsLock) {
|
||||
const LockIsWorn = InventoryCharacterIsWearingLock(C, /** @type {AssetLockType} */ (A.Name));
|
||||
|
|
@ -1684,15 +1725,14 @@ function DialogInventoryStringified(C) {
|
|||
* @param {number} Slot - Index of saved expression (0 to 4)
|
||||
*/
|
||||
function DialogFacialExpressionsLoad(Slot) {
|
||||
const expressions = Player.SavedExpressions && Player.SavedExpressions[Slot];
|
||||
if (expressions != null) {
|
||||
expressions.forEach(e => {
|
||||
CharacterSetFacialExpression(Player, e.Group, e.CurrentExpression);
|
||||
Player.ActiveExpression.setWithoutReload(e.Group, e.CurrentExpression);
|
||||
});
|
||||
if (DialogSelfMenuSelected === "Expression" && DialogSelfMenuMapping.Expression.C.IsPlayer()) {
|
||||
DialogSelfMenuMapping.Expression.Reload();
|
||||
}
|
||||
const expressions = Player.SavedExpressions[Slot];
|
||||
if (!expressions) return;
|
||||
expressions.forEach(e => {
|
||||
CharacterSetFacialExpression(Player, e.Group, e.CurrentExpression ?? null);
|
||||
Player.ActiveExpression.setWithoutReload(e.Group, e.CurrentExpression ?? null);
|
||||
});
|
||||
if (DialogSelfMenuSelected === "Expression" && DialogSelfMenuMapping.Expression.C.IsPlayer()) {
|
||||
DialogSelfMenuMapping.Expression.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1718,7 +1758,7 @@ function DialogBuildSavedExpressionsMenu() {
|
|||
);
|
||||
|
||||
for (let x = 0; x < expression.length; x++) {
|
||||
CharacterSetFacialExpression(PreviewCharacter, expression[x].Group, expression[x].CurrentExpression);
|
||||
CharacterSetFacialExpression(PreviewCharacter, expression[x].Group, expression[x].CurrentExpression ?? null);
|
||||
}
|
||||
CharacterRefresh(PreviewCharacter, false, false);
|
||||
|
||||
|
|
@ -1736,11 +1776,11 @@ function DialogBuildSavedExpressionsMenu() {
|
|||
function DialogMenuButtonClick() {
|
||||
|
||||
// Hack because those panes handle their menu icons themselves
|
||||
if (["colorExpression", "colorItem", "extended", "layering", "tighten"].includes(DialogMenuMode)) return false;
|
||||
if (DialogMenuMode && ["colorExpression", "colorItem", "extended", "layering", "tighten"].includes(DialogMenuMode)) return false;
|
||||
|
||||
// Gets the current character and item
|
||||
/** The focused character */
|
||||
const C = CharacterGetCurrent();
|
||||
const C = /** @type {Character} */ (CharacterGetCurrent());
|
||||
/** The focused item */
|
||||
const Item = C.FocusGroup ? InventoryGet(C, C.FocusGroup.Name) : null;
|
||||
|
||||
|
|
@ -1762,9 +1802,11 @@ function DialogMenuButtonClick() {
|
|||
}
|
||||
|
||||
// Remote Icon - Pops the item extension
|
||||
else if (button === "Remote" && DialogCanUseRemoteState(C, Item) === "Available") {
|
||||
DialogExtendItem(Item);
|
||||
return true;
|
||||
else if (button === "Remote") {
|
||||
if (Item && DialogCanUseRemoteState(C, Item) === "Available") {
|
||||
DialogExtendItem(Item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Lock Icon - Rebuilds the inventory list with locking items
|
||||
|
|
@ -1779,7 +1821,7 @@ function DialogMenuButtonClick() {
|
|||
else if (button === "Unlock" && Item) {
|
||||
// Check that this is not one of the sticky-locked items
|
||||
const isNotStickyLock = InventoryItemHasEffect(Item, "Lock", true) && !InventoryItemHasEffect(Item, "Lock", false);
|
||||
if (C.FocusGroup.IsItem() && isNotStickyLock && (!C.IsPlayer() || C.CanInteract())) {
|
||||
if (C.FocusGroup?.IsItem() && isNotStickyLock && (!C.IsPlayer() || C.CanInteract())) {
|
||||
InventoryUnlock(C, C.FocusGroup.Name, false);
|
||||
if (ChatRoomPublishAction(C, "ActionUnlock", Item, null)) {
|
||||
DialogLeave();
|
||||
|
|
@ -1933,7 +1975,7 @@ function DialogAllowItemClick(CurrentItem, ClickItem) {
|
|||
* @returns {ItemPermissionMode} - Nothing
|
||||
*/
|
||||
function DialogPermissionsClick(ClickItem, CurrentItem=null) {
|
||||
const worn = (ClickItem.Worn || (CurrentItem && (CurrentItem.Asset.Name == ClickItem.Asset.Name)));
|
||||
const worn = ClickItem.Worn || !!CurrentItem && CurrentItem.Asset.Name == ClickItem.Asset.Name;
|
||||
return DialogInventoryTogglePermission(ClickItem, worn);
|
||||
}
|
||||
|
||||
|
|
@ -1973,7 +2015,7 @@ function DialogItemClick(ClickItem, C, CurrentItem=null) {
|
|||
DialogStruggleStart(C, "ActionUnlock", CurrentItem, null);
|
||||
} else if (ClickItem.Asset.Name === "VibratorRemote" || ClickItem.Asset.Name === "LoversVibratorRemote") {
|
||||
// The vibrating egg remote can open the vibrating egg's extended dialog
|
||||
if (DialogCanUseRemoteState(C, CurrentItem) === "Available") { DialogExtendItem(CurrentItem); }
|
||||
if (CurrentItem && DialogCanUseRemoteState(C, CurrentItem) === "Available") { DialogExtendItem(CurrentItem); }
|
||||
} else {
|
||||
// Runs the activity arousal process if activated, & publishes the item action text to the chatroom
|
||||
DialogPublishAction(C, "ActionUse", ClickItem);
|
||||
|
|
@ -2001,6 +2043,7 @@ function DialogItemClick(ClickItem, C, CurrentItem=null) {
|
|||
* @param {null | Item} equippedItem
|
||||
*/
|
||||
function DialogActivityClick(C, clickedActivity, equippedItem) {
|
||||
if (!C.FocusGroup) return;
|
||||
if (C.IsNpc() && clickedActivity.Item) {
|
||||
let Line = C.FocusGroup.Name + clickedActivity.Item.Asset.DynamicName(Player);
|
||||
let D = DialogFind(C, Line, null, false);
|
||||
|
|
@ -2045,6 +2088,7 @@ function DialogInventoryTogglePermission(item, worn) {
|
|||
*/
|
||||
function DialogChangeMode(mode, reset=false) {
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C) return;
|
||||
|
||||
// Handle changing to the expression color picker having to restore the selected mode & group
|
||||
if (mode === "colorExpression" && (!DialogExpressionPreviousMode || DialogExpressionPreviousMode.mode !== "colorExpression")) {
|
||||
|
|
@ -2145,7 +2189,7 @@ function DialogChangeMode(mode, reset=false) {
|
|||
/**
|
||||
* Change the given character's focused group.
|
||||
* @param {Character} C - The character to change the focus of.
|
||||
* @param {AssetItemGroup|string} Group - The group that should gain focus.
|
||||
* @param {AssetItemGroup|string|null} Group - The group that should gain focus. `null` deselects the current group
|
||||
*/
|
||||
function DialogChangeFocusToGroup(C, Group) {
|
||||
/** @type {null | AssetGroup} */
|
||||
|
|
@ -2173,12 +2217,12 @@ function DialogChangeFocusToGroup(C, Group) {
|
|||
AudioDialogStop();
|
||||
|
||||
// Stop any struggling minigame
|
||||
if(StruggleMinigameIsRunning()) {
|
||||
if (StruggleMinigameIsRunning()) {
|
||||
StruggleMinigameStop();
|
||||
}
|
||||
|
||||
// If we're in the two-character dialog, clear their focused group
|
||||
if (!CurrentCharacter.IsPlayer()) {
|
||||
if (CurrentCharacter && !CurrentCharacter?.IsPlayer()) {
|
||||
Player.FocusGroup = null;
|
||||
CurrentCharacter.FocusGroup = null;
|
||||
}
|
||||
|
|
@ -2211,19 +2255,21 @@ function DialogClick(event) {
|
|||
|
||||
// Gets the current character
|
||||
let C = CharacterGetCurrent();
|
||||
if (!C) return;
|
||||
|
||||
// Check if the user clicked on one of the top menu icons
|
||||
if (DialogMenuButtonClick()) return;
|
||||
|
||||
// User clicked on the interacted character or herself, check if we need to update the menu
|
||||
if (MouseIn(0, 0, 1000, 1000) && (CurrentCharacter.AllowItem || (MouseX < 500)) && (!CurrentCharacter.IsPlayer() || (MouseX > 500)) && DialogIntro(Player) && DialogAllowItemScreenException()) {
|
||||
C = (MouseX < 500) ? Player : CurrentCharacter;
|
||||
const clickedChar = (MouseX < 500) ? Player : CurrentCharacter;
|
||||
let X = MouseX < 500 ? 0 : 500;
|
||||
for (const Group of AssetGroup) {
|
||||
if (!Group.IsItem()) continue;
|
||||
const Zone = Group.Zone.find(Z => DialogClickedInZone(C, Z, 1, X, 0, C.HeightRatio));
|
||||
const Zone = Group.Zone.find(Z => DialogClickedInZone(clickedChar, Z, 1, X, 0, clickedChar.HeightRatio));
|
||||
if (Zone) {
|
||||
DialogChangeFocusToGroup(C, Group);
|
||||
C = clickedChar;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -2237,6 +2283,7 @@ function DialogClick(event) {
|
|||
|
||||
// If the user clicked the Up button, move the character up to the top of the screen
|
||||
if ((CurrentCharacter.HeightModifier < -90 || CurrentCharacter.HeightModifier > 30) && (CurrentCharacter.FocusGroup != null) && MouseIn(510, 50, 90, 90)) {
|
||||
// @ts-ignore Strict-TS: CharacterAppearanceForceUpCharacter must change. Only online characters have member numbers
|
||||
CharacterAppearanceForceUpCharacter = CharacterAppearanceForceUpCharacter == CurrentCharacter.MemberNumber ? -1 : CurrentCharacter.MemberNumber;
|
||||
return;
|
||||
}
|
||||
|
|
@ -2390,11 +2437,12 @@ function DialogFindFacialExpressionMenuGroup(ExpressionGroup) {
|
|||
/**
|
||||
* Displays the given text for 5 seconds
|
||||
* @param {string} status - The text to be displayed
|
||||
* @param {number} timer - the number of milliseconds to display the message for
|
||||
* @param {null | { asset?: Asset, group?: AssetGroup, C?: Character }} replace - Attempt to perform replacements within the `status` text
|
||||
* @param {number} [timer] - the number of milliseconds to display the message for
|
||||
* @param {null | { asset?: Asset | null, group?: AssetGroup | null, C?: Character | null }} [replace] - Attempt to perform replacements within the `status` text
|
||||
* @param {string | null} [id]
|
||||
* @returns {void} - Nothing
|
||||
*/
|
||||
function DialogSetStatus(status, timer=0, replace=null, id=null) {
|
||||
function DialogSetStatus(status, timer=0, replace, id) {
|
||||
id ??= DialogMenuMapping[DialogMenuMode]?.ids.status;
|
||||
const elem = id ? document.getElementById(id) : null;
|
||||
if (!elem) {
|
||||
|
|
@ -2403,7 +2451,8 @@ function DialogSetStatus(status, timer=0, replace=null, id=null) {
|
|||
|
||||
replace ??= {};
|
||||
if (replace.group || replace.asset) {
|
||||
status = status.replaceAll("GroupName", DialogActualNameForGroup(replace.C ?? Player, replace.group ?? replace.asset.Group).toLowerCase());
|
||||
const groupName = replace.group ?? /** @type {Asset} */ (replace.asset).Group;
|
||||
status = status.replaceAll("GroupName", DialogActualNameForGroup(replace.C ?? Player, groupName).toLowerCase());
|
||||
}
|
||||
if (replace.asset) {
|
||||
status = status.replaceAll("AssetName", replace.asset.Description);
|
||||
|
|
@ -2454,7 +2503,7 @@ function DialogStatusClear() {
|
|||
const timeoutID = elem?.getAttribute("data-timeout-id");
|
||||
if (timeoutID) {
|
||||
clearTimeout(Number.parseInt(timeoutID, 10));
|
||||
DialogStatusTimerHandler(elem);
|
||||
if (elem) DialogStatusTimerHandler(elem);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2467,11 +2516,12 @@ function DialogStatusClear() {
|
|||
*/
|
||||
function DialogExtendItem(Item, SourceItem) {
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C) return;
|
||||
if (AsylumGGTSControlItem(C, Item)) return;
|
||||
if (InventoryBlockedOrLimited(C, Item)) return;
|
||||
DialogChangeMode("extended");
|
||||
DialogFocusItem = Item;
|
||||
DialogFocusSourceItem = SourceItem;
|
||||
DialogFocusSourceItem = SourceItem ?? null;
|
||||
ExtendedItemInit(C, Item.Asset.IsLock ? SourceItem : Item, false, true);
|
||||
CommonDynamicFunction("Inventory" + Item.Asset.Group.Name + Item.Asset.Name + "Load()");
|
||||
}
|
||||
|
|
@ -2483,6 +2533,7 @@ function DialogExtendItem(Item, SourceItem) {
|
|||
*/
|
||||
function DialogSetTightenLoosenItem(Item) {
|
||||
const C = CharacterGetCurrent();
|
||||
if (!C) return;
|
||||
if (AsylumGGTSControlItem(C, Item)) return;
|
||||
if (InventoryBlockedOrLimited(C, Item)) return;
|
||||
DialogChangeMode("tighten");
|
||||
|
|
@ -2839,7 +2890,7 @@ class DialogMenu {
|
|||
}
|
||||
|
||||
/** @type {ScreenLoadHandler} */
|
||||
Load() {
|
||||
async Load() {
|
||||
if (this._initPropertyNames.some(p => this._initProperties?.[p] == null)) {
|
||||
console.error(
|
||||
`Aborting, one or more uninitialized properties in ${this.mode} subscreen`,
|
||||
|
|
@ -2975,6 +3026,7 @@ class DialogMenu {
|
|||
const currentProp = CommonPick(/** @type {Partial<PropType>} */(this._initProperties ?? {}), this._initPropertyNames);
|
||||
const newProp = CommonPick(properties, this._initPropertyNames);
|
||||
for (const k of Object.keys(newProp)) {
|
||||
// @ts-ignore Strict-TS: direct property access to initialize
|
||||
newProp[k] ??= currentProp[k];
|
||||
}
|
||||
|
||||
|
|
@ -3105,7 +3157,7 @@ class DialogMenu {
|
|||
* Return the underlying item or activity object of the passed grid button.
|
||||
* @abstract
|
||||
* @param {HTMLButtonElement} button - The clicked button
|
||||
* @returns {ClickedObj | undefined} - The button's underlying item or activity object
|
||||
* @returns {ClickedObj | null} - The button's underlying item or activity object
|
||||
*/
|
||||
_GetClickedObject(button) {
|
||||
throw new Error("Trying to all an abstract method");
|
||||
|
|
@ -3263,8 +3315,8 @@ class _DialogFocusMenu extends DialogMenu {
|
|||
}
|
||||
|
||||
/** @type {DialogMenu["Load"]} */
|
||||
Load() {
|
||||
super.Load();
|
||||
async Load() {
|
||||
await super.Load();
|
||||
document.getElementById(this.ids.root)?.setAttribute("data-group", this.focusGroup.Name);
|
||||
return;
|
||||
}
|
||||
|
|
@ -3378,7 +3430,7 @@ class _DialogItemMenu extends _DialogFocusMenu {
|
|||
/** @type {null | Asset} */
|
||||
let asset = null;
|
||||
let showIcon = false;
|
||||
let textContent = options.status;
|
||||
let textContent = options.status ?? "";
|
||||
if (textContent == null) {
|
||||
switch (this.mode) {
|
||||
case "locked": {
|
||||
|
|
@ -3489,6 +3541,7 @@ class _DialogItemMenu extends _DialogFocusMenu {
|
|||
/** @type {_DialogFocusMenu["_ReloadIcon"]} */
|
||||
_ReloadIcon(root, icon, properties, options) {
|
||||
const grid = document.getElementById(this.ids.grid);
|
||||
if (!grid) return;
|
||||
const dataAttr = ["data-craft", "data-hidden", "data-vibrating"];
|
||||
const checkedButton = grid.querySelector(".dialog-grid-button[aria-checked='true']");
|
||||
if (checkedButton) {
|
||||
|
|
@ -3508,7 +3561,7 @@ class _DialogItemMenu extends _DialogFocusMenu {
|
|||
* @type {DialogMenu<string, DialogInventoryItem>["_GetClickedObject"]}
|
||||
*/
|
||||
_GetClickedObject(button) {
|
||||
return DialogInventory[Number.parseInt(button.getAttribute("data-index"), 10)];
|
||||
return DialogInventory[Number.parseInt(button.getAttribute("data-index") ?? "-1", 10)];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3546,7 +3599,7 @@ class _DialogLockingMenu extends _DialogFocusMenu {
|
|||
return equippedItem ? null : InterfaceTextGet("NoItemEquipped");
|
||||
},
|
||||
InventoryDoesItemAllowLock: (C, clickedLock, equippedItem) => {
|
||||
return InventoryDoesItemAllowLock(equippedItem) ? null : InterfaceTextGet("AccessBlocked");
|
||||
return equippedItem && InventoryDoesItemAllowLock(equippedItem) ? null : InterfaceTextGet("AccessBlocked");
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -3635,7 +3688,7 @@ class _DialogLockingMenu extends _DialogFocusMenu {
|
|||
|
||||
/** @type {DialogMenu<string, DialogInventoryItem>["_GetClickedObject"]} */
|
||||
_GetClickedObject(button) {
|
||||
return DialogInventory[Number.parseInt(button.getAttribute("data-index"), 10)];
|
||||
return DialogInventory[Number.parseInt(button.getAttribute("data-index") ?? "-1", 10)];
|
||||
}
|
||||
|
||||
/** @type {DialogMenu<string, DialogInventoryItem>["_ClickButton"]} */
|
||||
|
|
@ -3753,7 +3806,7 @@ class _DialogPermissionMenu extends _DialogFocusMenu {
|
|||
|
||||
/** @type {DialogMenu<string, DialogInventoryItem>["_GetClickedObject"]} */
|
||||
_GetClickedObject(button) {
|
||||
return DialogInventory[Number.parseInt(button.getAttribute("data-index"), 10)];
|
||||
return DialogInventory[Number.parseInt(button.getAttribute("data-index") ?? "-1", 10)];
|
||||
}
|
||||
|
||||
/** @type {DialogMenu<string, DialogInventoryItem>["_ClickButton"]} */
|
||||
|
|
@ -3865,7 +3918,7 @@ class _DialogActivitiesMenu extends _DialogFocusMenu {
|
|||
|
||||
/** @type {DialogMenu<string, ItemActivity>["_GetClickedObject"]} */
|
||||
_GetClickedObject(button) {
|
||||
return DialogActivity[Number.parseInt(button.getAttribute("data-index"), 10)];
|
||||
return DialogActivity[Number.parseInt(button.getAttribute("data-index") ?? "-1", 10)];
|
||||
}
|
||||
|
||||
/** @type {DialogMenu<string, ItemActivity>["_ClickButton"]} */
|
||||
|
|
@ -3957,7 +4010,7 @@ class _DialogCraftedMenu extends _DialogFocusMenu {
|
|||
_ReloadIcon(root, icon, properties, options) {
|
||||
const { C, focusGroup } = properties;
|
||||
const ids = this.ids;
|
||||
const item = InventoryGet(C, focusGroup.Name);
|
||||
const item = /** @type {Item} */ (InventoryGet(C, focusGroup.Name));
|
||||
|
||||
icon.innerHTML = "";
|
||||
[
|
||||
|
|
@ -4102,7 +4155,7 @@ class _DialogDialogMenu extends DialogMenu {
|
|||
super.Exit();
|
||||
}
|
||||
|
||||
/** @type {ScreenFunctions["Unload"]} */
|
||||
/** @type {VoidHandler} */
|
||||
Unload() {
|
||||
super.Unload();
|
||||
}
|
||||
|
|
@ -4139,7 +4192,7 @@ class _DialogDialogMenu extends DialogMenu {
|
|||
continue;
|
||||
}
|
||||
|
||||
const unload = !(dialog.Stage === C.Stage && dialog.Option != null && DialogPrerequisite(dialog));
|
||||
const unload = !(dialog.Stage === C.Stage && dialog.Option !== null && DialogPrerequisite(dialog));
|
||||
button.toggleAttribute("hidden", unload);
|
||||
if (!unload) {
|
||||
button.querySelector(".button-label")?.replaceChildren(SpeechTransformDialog(Player, dialog.Option));
|
||||
|
|
@ -4179,7 +4232,7 @@ class _DialogDialogMenu extends DialogMenu {
|
|||
|
||||
/** @type {DialogMenu<string, DialogLine>["_GetClickedObject"]} */
|
||||
_GetClickedObject(button) {
|
||||
const index = Number.parseInt(button.getAttribute("data-index"), 10);
|
||||
const index = Number.parseInt(button.getAttribute("data-index") ?? "-1", 10);
|
||||
return this.C?.Dialog[index] ?? null;
|
||||
}
|
||||
|
||||
|
|
@ -4195,7 +4248,7 @@ class _DialogDialogMenu extends DialogMenu {
|
|||
// A dialog option can change the conversation stage, show text or launch a custom function
|
||||
if ((Player.CanTalk() && C.CanTalk()) || SpeechFullEmote(clickedDialog.Option)) {
|
||||
C._CurrentDialog = clickedDialog.Result;
|
||||
if (clickedDialog.NextStage != null) {
|
||||
if (clickedDialog.NextStage !== null) {
|
||||
C._Stage = clickedDialog.NextStage;
|
||||
this.Reload();
|
||||
} else {
|
||||
|
|
@ -4203,8 +4256,12 @@ class _DialogDialogMenu extends DialogMenu {
|
|||
DialogSetStatus(C.CurrentDialog);
|
||||
}
|
||||
|
||||
if (typeof clickedDialog.Function === "string") {
|
||||
CommonDynamicFunctionParams(clickedDialog.Function);
|
||||
if (clickedDialog.Function) {
|
||||
try {
|
||||
CommonDynamicFunctionParams(clickedDialog.Function);
|
||||
} catch (err) {
|
||||
console.error("_ClickButton: Failed dynamic expression", clickedDialog.Function, err);
|
||||
}
|
||||
}
|
||||
} else if (clickedDialog.Function?.trim() === "DialogLeave()") {
|
||||
DialogLeave();
|
||||
|
|
@ -4374,7 +4431,7 @@ class _DialogExpressionMenu extends _DialogSelfMenu {
|
|||
/** @satisfies {DialogMenu<ModeType, ExpressionPair>["clickStatusCallbacks"]} */
|
||||
clickStatusCallbacks = {
|
||||
CharacterIsExpressionAllowed: (C, clickedItem, equippedItem) => {
|
||||
const status = CharacterIsExpressionDisallowed(C, equippedItem, clickedItem.Expression);
|
||||
const status = !!equippedItem && CharacterIsExpressionDisallowed(C, equippedItem, clickedItem.Expression);
|
||||
return status ? InterfaceTextGet(`Prerequisite${status}`) : null;
|
||||
},
|
||||
};
|
||||
|
|
@ -4438,7 +4495,7 @@ class _DialogExpressionMenu extends _DialogSelfMenu {
|
|||
} else {
|
||||
colorButton?.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
if (colorButton.getAttribute("aria-disabled") === "true" && ItemColorState) {
|
||||
if (colorButton?.getAttribute("aria-disabled") === "true" && ItemColorState) {
|
||||
ItemColorCancelAndExit();
|
||||
}
|
||||
},
|
||||
|
|
@ -4480,28 +4537,29 @@ class _DialogExpressionMenu extends _DialogSelfMenu {
|
|||
/** @type {DialogMenu.MenuButtonData<{ C: PlayerCharacter }>} */
|
||||
blink: {
|
||||
click(button, ev, { C }) {
|
||||
const level = Number.parseInt(button.getAttribute("aria-valuenow"), 10);
|
||||
const val = CommonParseInt(button.getAttribute("aria-valuenow") ?? "", 10) ?? 1;
|
||||
const level = /** @type {1 | 2 | 3 | 4} */ (CommonClamp(val, 1, 4));
|
||||
|
||||
/** @type {string} */
|
||||
let state;
|
||||
switch (level) {
|
||||
case 1:
|
||||
state = "None";
|
||||
CharacterSetFacialExpression(C, "Eyes", C.ActiveExpression.Eyes, null);
|
||||
CharacterSetFacialExpression(C, "Eyes", C.ActiveExpression.Eyes);
|
||||
break;
|
||||
case 2:
|
||||
state = "Left";
|
||||
CharacterSetFacialExpression(C, "Eyes1", C.ActiveExpression.Eyes, null);
|
||||
CharacterSetFacialExpression(C, "Eyes2", "Closed", null);
|
||||
CharacterSetFacialExpression(C, "Eyes1", C.ActiveExpression.Eyes);
|
||||
CharacterSetFacialExpression(C, "Eyes2", "Closed");
|
||||
break;
|
||||
case 3:
|
||||
state = "Both";
|
||||
CharacterSetFacialExpression(C, "Eyes", "Closed", null);
|
||||
CharacterSetFacialExpression(C, "Eyes", "Closed");
|
||||
break;
|
||||
case 4:
|
||||
state = "Right";
|
||||
CharacterSetFacialExpression(C, "Eyes1", "Closed", null);
|
||||
CharacterSetFacialExpression(C, "Eyes2", C.ActiveExpression.Eyes, null);
|
||||
CharacterSetFacialExpression(C, "Eyes1", "Closed");
|
||||
CharacterSetFacialExpression(C, "Eyes2", C.ActiveExpression.Eyes);
|
||||
break;
|
||||
}
|
||||
button.setAttribute("aria-valuetext", state);
|
||||
|
|
@ -4589,7 +4647,7 @@ class _DialogExpressionMenu extends _DialogSelfMenu {
|
|||
name: group,
|
||||
"aria-expanded": "false",
|
||||
"aria-owns": `${ids.grid}-${group}`,
|
||||
"aria-label": AssetGroupGet("Female3DCG", group).Description,
|
||||
"aria-label": AssetGroupGet("Female3DCG", group)?.Description,
|
||||
},
|
||||
}},
|
||||
)),
|
||||
|
|
@ -4607,7 +4665,7 @@ class _DialogExpressionMenu extends _DialogSelfMenu {
|
|||
id: `${ids.grid}-${group}`,
|
||||
role: "radiogroup",
|
||||
"aria-required": "true",
|
||||
"aria-label": AssetGroupGet("Female3DCG", group).Description,
|
||||
"aria-label": AssetGroupGet("Female3DCG", group)?.Description,
|
||||
},
|
||||
dataAttributes: { name: group },
|
||||
};
|
||||
|
|
@ -4743,7 +4801,7 @@ class _DialogExpressionMenu extends _DialogSelfMenu {
|
|||
}
|
||||
|
||||
CharacterSetFacialExpression(C, clickedObj.Group, clickedObj.Expression);
|
||||
C.ActiveExpression.setWithoutReload(clickedObj.Group, clickedObj.Expression ?? undefined);
|
||||
C.ActiveExpression.setWithoutReload(clickedObj.Group, clickedObj.Expression);
|
||||
|
||||
const thisImg = button.querySelector("img.button-image");
|
||||
const controllerImg = document.querySelector(`#${this.ids.menuLeft} [role="menuitemradio"][name="${clickedObj.Group}"] .button-image`);
|
||||
|
|
@ -5252,7 +5310,8 @@ class _DialogOwnerRulesMenu extends _DialogSelfMenu {
|
|||
];
|
||||
|
||||
const children = rules.map(([name, group, logValue]) => {
|
||||
if (!LogQuery(name, group)) {
|
||||
const val = LogValue(name, group);
|
||||
if (val === null || val === undefined) {
|
||||
return null;
|
||||
} else {
|
||||
return ElementCreate({
|
||||
|
|
@ -5260,7 +5319,7 @@ class _DialogOwnerRulesMenu extends _DialogSelfMenu {
|
|||
dataAttributes: { name, group },
|
||||
children: [
|
||||
InterfaceTextGet(`RulesMenu${name}`),
|
||||
(logValue ? ` ${TimerToString(LogValue(name, group) - CurrentTime)}` : null),
|
||||
(logValue ? ` ${TimerToString(val - CurrentTime)}` : null),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
@ -5319,7 +5378,7 @@ function DialogFindPlayer(KeyWord) {
|
|||
* Searches in the dialog for a specific stage keyword and returns that dialog option if we find it
|
||||
* @param {Character} C - The character whose dialog option*
|
||||
* @param {string} KeyWord1 - The key word to search for
|
||||
* @param {string} [KeyWord2] - An optionally given second key word. is only looked for, if specified and the first
|
||||
* @param {string | null} [KeyWord2] - An optionally given second key word. is only looked for, if specified and the first
|
||||
* keyword was not found.
|
||||
* @param {boolean} [ReturnPrevious=true] - If specified, returns the previous dialog, if neither of the the two key words were found
|
||||
ns should be searched
|
||||
|
|
@ -5349,16 +5408,19 @@ function DialogFind(C, KeyWord1, KeyWord2, ReturnPrevious = true) {
|
|||
* is replaced with the player's name and 'DestinationCharacter' with the current character's name.
|
||||
*/
|
||||
function DialogFindAutoReplace(C, KeyWord1, KeyWord2, ReturnPrevious) {
|
||||
return DialogFind(C, KeyWord1, KeyWord2, ReturnPrevious)
|
||||
const line = DialogFind(C, KeyWord1, KeyWord2, ReturnPrevious);
|
||||
const current = CharacterGetCurrent();
|
||||
if (!current) return line;
|
||||
return line
|
||||
.replace("SourceCharacter", CharacterNickname(Player))
|
||||
.replace("DestinationCharacter", CharacterNickname(CharacterGetCurrent()));
|
||||
.replace("DestinationCharacter", CharacterNickname(current));
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the up/down arrow to bump a character up and down if they're hidden.
|
||||
*/
|
||||
function DialogDrawRepositionButton() {
|
||||
if (!CurrentCharacter.FocusGroup) return;
|
||||
if (!CurrentCharacter || !CurrentCharacter.FocusGroup) return;
|
||||
|
||||
let drawButton = "";
|
||||
if (CharacterAppearanceForceUpCharacter == CurrentCharacter.MemberNumber) {
|
||||
|
|
@ -5378,7 +5440,9 @@ function DialogDrawRepositionButton() {
|
|||
* @param {Character} C The character currently focused.
|
||||
*/
|
||||
function DialogDrawTopMenu(C) {
|
||||
if (!C.FocusGroup) return;
|
||||
const FocusItem = InventoryGet(C, C.FocusGroup.Name);
|
||||
if (!FocusItem) return;
|
||||
|
||||
for (let I = DialogMenuButton.length - 1; I >= 0; I--) {
|
||||
const ButtonColor = DialogGetMenuButtonColor(DialogMenuButton[I]);
|
||||
|
|
@ -5498,7 +5562,7 @@ function DialogDraw() {
|
|||
// Customization can be used in dialog if screen is online chat room
|
||||
if (ServerPlayerIsInChatRoom() && ChatRoomCustomized) {
|
||||
const drawBGToRect = DrawShowChatRoomCustomBackground() ? { x: 0, y: 0, w: 2000, h: 1000 } : null;
|
||||
ChatAdminRoomCustomizationProcess(ChatRoomData.Custom, drawBGToRect, true);
|
||||
ChatAdminRoomCustomizationProcess(/** @type {ServerChatRoomData} */ (ChatRoomData).Custom, drawBGToRect, true);
|
||||
}
|
||||
|
||||
// Check that there's actually a character selected
|
||||
|
|
@ -5529,7 +5593,7 @@ function DialogDraw() {
|
|||
const FocusItem = InventoryGet(C, C.FocusGroup?.Name);
|
||||
|
||||
// Draws the top menu text & icons
|
||||
if (!["extended", "tighten", "colorDefault", "colorExpression", "colorItem", "layering", "dialog"].includes(DialogMenuMode))
|
||||
if (DialogMenuMode && !["extended", "tighten", "colorDefault", "colorExpression", "colorItem", "layering", "dialog"].includes(DialogMenuMode))
|
||||
DialogDrawTopMenu(C);
|
||||
|
||||
// If the player is struggling or lockpicking
|
||||
|
|
@ -5543,7 +5607,7 @@ function DialogDraw() {
|
|||
DrawItemPreview(DialogStrugglePrevItem, C, 1200, 100);
|
||||
DrawItemPreview(DialogStruggleNextItem, C, 1200, 100);
|
||||
} else if (DialogStrugglePrevItem || DialogStruggleNextItem) {
|
||||
const item = DialogStrugglePrevItem || DialogStruggleNextItem;
|
||||
const item = /** @type {Item} */ (DialogStrugglePrevItem ?? DialogStruggleNextItem);
|
||||
DrawItemPreview(item, C, 1387, 100);
|
||||
}
|
||||
|
||||
|
|
@ -5584,7 +5648,7 @@ function DialogDraw() {
|
|||
DialogChangeMode("items");
|
||||
}
|
||||
} else if ((DialogMenuMode === "colorItem" || DialogMenuMode === "colorExpression") && FocusItem) {
|
||||
ItemColorDraw(C, C.FocusGroup.Name, 1090, 15, 885, 970);
|
||||
ItemColorDraw(C, FocusItem.Asset.Group.Name, 1090, 15, 885, 970);
|
||||
return;
|
||||
} else if (DialogMenuMode === "colorDefault") {
|
||||
return;
|
||||
|
|
@ -5676,8 +5740,8 @@ function DialogActualNameForGroup(C, G) {
|
|||
*
|
||||
* @param {Character} C
|
||||
* @param {DialogStruggleActionType} Action
|
||||
* @param {Item} PrevItem
|
||||
* @param {Item} NextItem
|
||||
* @param {Item | null} PrevItem
|
||||
* @param {Item | null} NextItem
|
||||
*/
|
||||
function DialogStruggleStart(C, Action, PrevItem, NextItem) {
|
||||
|
||||
|
|
@ -5768,7 +5832,7 @@ function DialogStruggleStop(C, Game, { Progress, PrevItem, NextItem, Skill, Atte
|
|||
if ((NextItem.Craft != null) && CommonIsColor(NextItem.Craft.Color))
|
||||
Color = NextItem.Craft.Color;
|
||||
|
||||
NextItem = InventoryWear(C, NextItem.Asset.Name, NextItem.Asset.Group.Name, Color, SkillGetWithRatio(Player, "Bondage"), Player.MemberNumber, NextItem.Craft, false);
|
||||
NextItem = InventoryWear(C, NextItem.Asset.Name, NextItem.Asset.Group.Name, Color, SkillGetWithRatio(Player, "Bondage"), Player.MemberNumber, NextItem.Craft, false) ?? undefined;
|
||||
}
|
||||
|
||||
CharacterRefresh(C, true, true);
|
||||
|
|
@ -5803,7 +5867,7 @@ function DialogStruggleStop(C, Game, { Progress, PrevItem, NextItem, Skill, Atte
|
|||
ChatRoomPublishAction(C, DialogStruggleAction, PrevItem, NextItem);
|
||||
DialogChangeMode("items");
|
||||
} else if (
|
||||
NextItem !== null
|
||||
NextItem != null
|
||||
&& NextItem.Asset.Extended
|
||||
&& (
|
||||
NextItem.Craft == null
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
|
|
@ -50,6 +49,7 @@ class DictionaryBuilder {
|
|||
sourceCharacter(character) {
|
||||
if (!this._condition) return this;
|
||||
/** @type {SourceCharacterDictionaryEntry} */
|
||||
// @ts-ignore Strict-TS: Character doesn't have a MemberNumber
|
||||
const entry = { SourceCharacter: character.MemberNumber };
|
||||
if (character.IsPlayer() && ChatRoomMapViewHasSuperPowers() && ChatRoomMapViewIsActive()) {
|
||||
entry.HasSuperPowers = true;
|
||||
|
|
@ -86,6 +86,7 @@ class DictionaryBuilder {
|
|||
if (!this._condition) return this;
|
||||
|
||||
/** @type {TargetCharacterDictionaryEntry} */
|
||||
// @ts-ignore Strict-TS: Character doesn't have a MemberNumber
|
||||
const entry = {TargetCharacter: character.MemberNumber};
|
||||
if (this._targetIndex) {
|
||||
entry.Index = this._targetIndex;
|
||||
|
|
@ -223,11 +224,11 @@ class DictionaryBuilder {
|
|||
* @param {number} [count] - The number of times the activity is done
|
||||
* @returns
|
||||
*/
|
||||
performActivity(name, group, item = null, count = 1) {
|
||||
performActivity(name, group, item, count = 1) {
|
||||
this._addEntry({ ActivityName: name });
|
||||
this.focusGroup(group.Name);
|
||||
if (item) {
|
||||
this.asset(item.Asset, "UsedAsset", item.Craft && item.Craft.Name);
|
||||
this.asset(item.Asset, "UsedAsset", item.Craft?.Name);
|
||||
}
|
||||
if (count > 1)
|
||||
this._addEntry({ ActivityCounter: count });
|
||||
|
|
@ -247,6 +248,7 @@ class DictionaryBuilder {
|
|||
/**
|
||||
* Adds a changeKey dictionary entry
|
||||
* @param {("gold" | "silver" | "bronze")[]} keys
|
||||
* @param {boolean} giveKey
|
||||
* @returns
|
||||
*/
|
||||
mapViewChangeKey(keys, giveKey) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
* @type {object}
|
||||
* @property {number} [fontSize] - The target font size. Note that if space is constrained, the actual drawn font size will be reduced
|
||||
* automatically to fit. Defaults to 30px.
|
||||
* @property {string} [fontFamily] - The desired font family to draw text in. This can be a single font name, or a full CSS font stack
|
||||
* @property {string | null} [fontFamily] - The desired font family to draw text in. This can be a single font name, or a full CSS font stack
|
||||
* (e.g. "'Helvetica', 'Arial', sans-serif"). Defaults to the player's chosen global font.
|
||||
* @property {CanvasTextAlign} [textAlign] - The text alignment to use. Can be any valid
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/text-align text alignment}. Not applicable to the {@link DynamicDrawTextArc}
|
||||
|
|
|
|||
|
|
@ -1814,7 +1814,7 @@ var ElementButton = {
|
|||
|
||||
const icons = Array.from(button.querySelectorAll(".button-icon"));
|
||||
const iconNamesOld = icons.map(el => el.getAttribute("data-name"));
|
||||
/** @type {(InventoryIcon | null)[]} */
|
||||
/** @type {(InventoryIcon | null | undefined)[]} */
|
||||
const iconNamesNew = [
|
||||
DialogGetFavoriteStateDetails(C ?? Player, asset)?.Icon,
|
||||
InventoryBlockedOrLimited(C ?? Player, item) ? "Blocked" : null,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
/** @type {LogRecord[]} */
|
||||
var Log = [];
|
||||
|
|
@ -15,25 +14,20 @@ var Log = [];
|
|||
function LogAdd(NewLogName, NewLogGroup, NewLogValue, Push) {
|
||||
|
||||
// Makes sure the value is numeric
|
||||
if (NewLogValue != null) NewLogValue = parseInt(NewLogValue);
|
||||
if (typeof NewLogValue === "string") NewLogValue = CommonParseInt(NewLogValue) ?? undefined;
|
||||
|
||||
// Checks to make sure we don't duplicate a log
|
||||
var AddToLog = true;
|
||||
for (let L = 0; L < Log.length; L++)
|
||||
if ((Log[L].Name == NewLogName) && (Log[L].Group == NewLogGroup)) {
|
||||
Log[L].Value = NewLogValue;
|
||||
AddToLog = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Adds a new log object if we need to
|
||||
if (AddToLog) {
|
||||
var NewLog = {
|
||||
const entry = Log.find(l => l.Group === NewLogGroup && l.Name === NewLogName);
|
||||
if (entry) {
|
||||
entry.Value = NewLogValue;
|
||||
} else {
|
||||
/** @type {LogRecord} */
|
||||
const newEntry = {
|
||||
Name: NewLogName,
|
||||
Group: NewLogGroup,
|
||||
Value: NewLogValue
|
||||
};
|
||||
Log.push(NewLog);
|
||||
Log.push(newEntry);
|
||||
}
|
||||
|
||||
// Sends the log to the server
|
||||
|
|
@ -104,11 +98,11 @@ function LogDeleteGroup(DelLogGroup, Push) {
|
|||
* @returns {boolean} - Returns TRUE if there is an existing log matching the Name/Group with no value or a value above the current time in ms.
|
||||
*/
|
||||
function LogQuery(QueryLogName, QueryLogGroup) {
|
||||
for (let L = 0; L < Log.length; L++)
|
||||
if ((Log[L].Name == QueryLogName) && (Log[L].Group == QueryLogGroup))
|
||||
if ((Log[L].Value == null) || (Log[L].Value >= CurrentTime))
|
||||
return true;
|
||||
return false;
|
||||
const entry = Log.find(l => l.Group === QueryLogGroup && l.Name === QueryLogName);
|
||||
if (!entry) return false;
|
||||
|
||||
// Loose null-check here in case there's a null or an undefined stuck in there
|
||||
return entry.Value == null || entry.Value >= CurrentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -133,13 +127,12 @@ function LogContain(LogName, LogGroup, ID) {
|
|||
* @template {LogGroupType} T
|
||||
* @param {LogNameType[T]} QueryLogName - The name of the log to query the value
|
||||
* @param {T} QueryLogGroup - The name of the log's group
|
||||
* @returns {number | null} - Returns the value of the log which is a date represented in ms or undefined. Returns null if no matching log is found.
|
||||
* @returns {number | null | undefined} - Returns the value of the log which is a date represented in ms or undefined. Returns null if no matching log is found.
|
||||
*/
|
||||
function LogValue(QueryLogName, QueryLogGroup) {
|
||||
for (let L = 0; L < Log.length; L++)
|
||||
if ((Log[L].Name == QueryLogName) && (Log[L].Group == QueryLogGroup))
|
||||
return Log[L].Value;
|
||||
return null;
|
||||
const entry = Log.find(l => l.Group === QueryLogGroup && l.Name === QueryLogName);
|
||||
if (!entry) return null;
|
||||
return entry.Value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -665,7 +665,7 @@ function InventoryAllow(C, asset, prerequisites = asset.Prerequisite, setDialog
|
|||
/**
|
||||
* Gets the current item / cloth worn a specific area (AssetGroup)
|
||||
* @param {Character} C - The character on which we must check the appearance
|
||||
* @param {AssetGroupName} AssetGroup - The name of the asset group to scan
|
||||
* @param {AssetGroupName|null|undefined} AssetGroup - The name of the asset group to scan
|
||||
* @returns {Item|null} - Returns the appearance which is the item / cloth asset, color and properties
|
||||
*/
|
||||
function InventoryGet(C, AssetGroup) {
|
||||
|
|
@ -1390,7 +1390,7 @@ function InventoryDoesItemAllowLock(item) {
|
|||
/**
|
||||
* Applies a lock to an appearance item of a character
|
||||
* @param {Character} C - The character on which the lock must be applied
|
||||
* @param {Item|AssetGroupName} ItemOrGroupName - The item from appearance to lock
|
||||
* @param {Item|AssetGroupName|null} ItemOrGroupName - The item from appearance to lock
|
||||
* @param {Item|AssetLockType} LockOrLockType - The asset of the lock or the name of the lock asset
|
||||
* @param {null|Character|string} [AppliedBy] - The character applying the lock, or message to show
|
||||
* @param {boolean} [Update=true] - Whether or not to update the character
|
||||
|
|
@ -1635,7 +1635,7 @@ function InventoryTogglePermission(Item, Type=null, Worn=false, push=true) {
|
|||
* @param {Character} C - The character on which we check the permissions
|
||||
* @param {string} AssetName - The asset / item name to scan
|
||||
* @param {AssetGroupName} AssetGroup - The asset group name to scan
|
||||
* @param {string} [AssetType] - The asset type to scan
|
||||
* @param {string | null} [AssetType] - The asset type to scan
|
||||
* @returns {boolean} - TRUE if asset / item is blocked
|
||||
*/
|
||||
function InventoryIsPermissionBlocked(C, AssetName, AssetGroup, AssetType) {
|
||||
|
|
@ -1652,7 +1652,7 @@ function InventoryIsPermissionBlocked(C, AssetName, AssetGroup, AssetType) {
|
|||
* @param {Character} C - The character on which we check the permissions
|
||||
* @param {string} AssetName - The asset / item name to scan
|
||||
* @param {AssetGroupName} AssetGroup - The asset group name to scan
|
||||
* @param {string} [AssetType] - The asset type to scan
|
||||
* @param {string | null} [AssetType] - The asset type to scan
|
||||
* @returns {boolean} - TRUE if asset / item is a favorite
|
||||
*/
|
||||
function InventoryIsFavorite(C, AssetName, AssetGroup, AssetType) {
|
||||
|
|
@ -1668,7 +1668,7 @@ function InventoryIsFavorite(C, AssetName, AssetGroup, AssetType) {
|
|||
* @param {Character} C - The character on which we check the permissions
|
||||
* @param {string} AssetName - The asset / item name to scan
|
||||
* @param {AssetGroupName} AssetGroup - The asset group name to scan
|
||||
* @param {string} [AssetType] - The asset type to scan
|
||||
* @param {string | null} [AssetType] - The asset type to scan
|
||||
* @returns {boolean} - TRUE if asset / item is limited
|
||||
*/
|
||||
function InventoryIsPermissionLimited(C, AssetName, AssetGroup, AssetType) {
|
||||
|
|
@ -1683,7 +1683,7 @@ function InventoryIsPermissionLimited(C, AssetName, AssetGroup, AssetType) {
|
|||
* Returns TRUE if the item is not limited, if the player is an owner or a lover of the character, or on their whitelist
|
||||
* @param {Character} C - The character on which we check the limited permissions for the item
|
||||
* @param {Item} Item - The item being interacted with
|
||||
* @param {string} [ItemType] - The asset type to scan
|
||||
* @param {string | null} [ItemType] - The asset type to scan
|
||||
* @returns {boolean} - TRUE if item is allowed
|
||||
*/
|
||||
function InventoryCheckLimitedPermission(C, Item, ItemType) {
|
||||
|
|
@ -1697,7 +1697,7 @@ function InventoryCheckLimitedPermission(C, Item, ItemType) {
|
|||
* Returns TRUE if a specific item / asset is blocked or limited for the player by the character item permissions
|
||||
* @param {Character} C - The character on which we check the permissions
|
||||
* @param {Item} Item - The item being interacted with
|
||||
* @param {string | undefined} [ItemType] - The asset type to scan
|
||||
* @param {string | undefined | null} [ItemType] - The asset type to scan
|
||||
* @returns {boolean} - Returns TRUE if the item cannot be used
|
||||
*/
|
||||
function InventoryBlockedOrLimited(C, Item, ItemType) {
|
||||
|
|
@ -1711,7 +1711,7 @@ function InventoryBlockedOrLimited(C, Item, ItemType) {
|
|||
* used by the player)
|
||||
* @param {Character} C - The character whose permissions to check
|
||||
* @param {Item} item - The item to check
|
||||
* @param {string | undefined} [type] - the item type to check
|
||||
* @param {string | undefined | null} [type] - the item type to check
|
||||
* @returns {boolean} - Returns TRUE if the given item & type is limited but allowed for the player
|
||||
*/
|
||||
function InventoryIsAllowedLimited(C, item, type) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
'use strict';
|
||||
|
||||
var KeybindingDefaults = {
|
||||
|
|
@ -35,7 +34,7 @@ var KeybindingDefaults = {
|
|||
(document.activeElement === null
|
||||
|| document.activeElement === document.body
|
||||
|| document.activeElement instanceof HTMLDialogElement)
|
||||
&& document.activeElement.id !== "InputChat"
|
||||
&& document.activeElement?.id !== "InputChat"
|
||||
},
|
||||
{
|
||||
id: 'isInChatRoom',
|
||||
|
|
|
|||
|
|
@ -565,7 +565,7 @@ function ModularItemModuleTransition(newModule, data) {
|
|||
/**
|
||||
* 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
|
||||
* @param {null | undefined | 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) {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ function NPCTraitKeepBestOption(C, Group) {
|
|||
let Best = -1;
|
||||
let Pos = -1;
|
||||
for (let D = 0; D < C.Dialog.length; D++)
|
||||
if ((C.Dialog[D].Group != null) && (C.Dialog[D].Group == Group)) {
|
||||
if (C.Dialog[D].Group === Group) {
|
||||
var Value = NPCTraitGetOptionValue(C.Dialog[D].Trait, C.Trait);
|
||||
if (Value > Best) { Best = Value; Pos = D; }
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ function NPCTraitKeepBestOption(C, Group) {
|
|||
// If we found the best possibility, we remove all the others
|
||||
if (Pos >= 0)
|
||||
for (let D = 0; D < C.Dialog.length; D++)
|
||||
if ((D != Pos) && (C.Dialog[D].Group != null) && (C.Dialog[D].Group == Group)) {
|
||||
if ((D != Pos) && C.Dialog[D].Group === Group) {
|
||||
C.Dialog.splice(D, 1);
|
||||
Pos--;
|
||||
D--;
|
||||
|
|
@ -124,8 +124,8 @@ function NPCTraitDialog(C) {
|
|||
|
||||
// For each dialog option
|
||||
for (let D = 0; D < C.Dialog.length; D++) {
|
||||
if (C.Dialog[D].Group != null) NPCTraitKeepBestOption(C, C.Dialog[D].Group);
|
||||
if (C.Dialog[D].Function != null) C.Dialog[D].Function = C.Dialog[D].Function.replace("MainHall", "");
|
||||
if (C.Dialog[D].Group !== null) NPCTraitKeepBestOption(C, C.Dialog[D].Group);
|
||||
if (C.Dialog[D].Function !== null) C.Dialog[D].Function = C.Dialog[D].Function.replace("MainHall", "");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-strict-ignore
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
|
|
@ -187,16 +186,20 @@ function PreferenceGetZoneFactor(C, ZoneName) {
|
|||
* Sets the arousal zone data for a specific body zone on the player
|
||||
* @param {Character} C - The character, for whom the love factor of a particular zone should be set
|
||||
* @param {AssetGroupItemName} ZoneName - The name of the zone, the factor should be set for
|
||||
* @param {ArousalFactor} Factor - The factor of the zone (0 is horrible, 2 is normal, 4 is great)
|
||||
* @param {boolean} CanOrgasm - Sets, if the character can cum from the given zone (true) or not (false)
|
||||
* @param {null | ArousalFactor} [Factor] - The factor of the zone (0 is horrible, 2 is normal, 4 is great)
|
||||
* @param {null | boolean} [CanOrgasm] - Sets, if the character can cum from the given zone (true) or not (false)
|
||||
* @returns {void} - Nothing
|
||||
*/
|
||||
function PreferenceSetArousalZone(C, ZoneName, Factor = null, CanOrgasm = null) {
|
||||
function PreferenceSetArousalZone(C, ZoneName, Factor, CanOrgasm) {
|
||||
// Gets the zone object
|
||||
let Zone = PreferenceGetArousalZone(C, ZoneName);
|
||||
if (!Zone) return;
|
||||
|
||||
const Group = AssetGroupGet(C.AssetFamily, ZoneName);
|
||||
if (!Group || !Group.ArousalZoneID) {
|
||||
console.error('PreferenceSetArousalZone: Invalid group name or missing group ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof Factor === "number") {
|
||||
Zone.Factor = Factor;
|
||||
|
|
@ -323,6 +326,7 @@ function PreferenceInitPlayer(C, data) {
|
|||
"ControllerDPadRight",
|
||||
];
|
||||
for (const old of oldKeys) {
|
||||
// @ts-ignore Strict-TS: key-based access to delete old properties
|
||||
delete data.ControllerSettings[old];
|
||||
}
|
||||
// @ts-expect-error we don't have all the buttons
|
||||
|
|
@ -348,27 +352,6 @@ function PreferenceInitPlayer(C, data) {
|
|||
C.OnlineSharedSettings = ValidationApplyRecord(data.OnlineSharedSettings, C, PreferenceOnlineSharedSettingsValidate, true);
|
||||
C.RestrictionSettings = ValidationApplyRecord(data.RestrictionSettings, C, PreferenceRestrictionSettingsValidate);
|
||||
C.VisualSettings = ValidationApplyRecord(data.VisualSettings, C, PreferenceVisualSettingsValidate);
|
||||
|
||||
// Convert old version of notification settings
|
||||
let NS = /** @type {Partial<NotificationSettingsType>} */ (data.NotificationSettings ?? {});
|
||||
|
||||
if (typeof NS.Beeps !== "object") NS.Beeps = PreferenceInitNotificationSetting(NS.Beeps, NotificationAudioType.FIRST, NotificationAlertType.POPUP);
|
||||
// @ts-expect-error object gets its missing properties afterwards
|
||||
if (typeof NS.ChatMessage !== "object") NS.ChatMessage = PreferenceInitNotificationSetting(NS.ChatMessage, NotificationAudioType.FIRST);
|
||||
if (typeof NS.ChatMessage.Normal !== "boolean") NS.ChatMessage.Normal = true;
|
||||
if (typeof NS.ChatMessage.Whisper !== "boolean") NS.ChatMessage.Whisper = true;
|
||||
if (typeof NS.ChatMessage.Activity !== "boolean") NS.ChatMessage.Activity = false;
|
||||
// @ts-expect-error object gets its missing properties afterwards
|
||||
if (typeof NS.ChatJoin !== "object") NS.ChatJoin = PreferenceInitNotificationSetting(NS.ChatJoin, NotificationAudioType.FIRST);
|
||||
if (typeof NS.ChatJoin.Owner !== "boolean") NS.ChatJoin.Owner = false;
|
||||
if (typeof NS.ChatJoin.Lovers !== "boolean") NS.ChatJoin.Lovers = false;
|
||||
if (typeof NS.ChatJoin.Friendlist !== "boolean") NS.ChatJoin.Friendlist = false;
|
||||
if (typeof NS.ChatJoin.Subs !== "boolean") NS.ChatJoin.Subs = false;
|
||||
if (typeof NS.Disconnect !== "object") NS.Disconnect = PreferenceInitNotificationSetting(NS.Disconnect, NotificationAudioType.FIRST);
|
||||
if (typeof NS.Larp !== "object") NS.Larp = PreferenceInitNotificationSetting(NS.Larp, NotificationAudioType.FIRST, NotificationAlertType.NONE);
|
||||
if (typeof NS.Test !== "object") NS.Test = PreferenceInitNotificationSetting(NS.Test, NotificationAudioType.FIRST, NotificationAlertType.TITLEPREFIX);
|
||||
data.NotificationSettings = /** @type {NotificationSettingsType} */ (NS);
|
||||
|
||||
C.NotificationSettings = ValidationApplyRecord(data.NotificationSettings, C, PreferenceNotificationSettingsValidate);
|
||||
|
||||
// Forces some preferences depending on difficulty
|
||||
|
|
@ -414,7 +397,8 @@ function PreferenceInitPlayer(C, data) {
|
|||
|
||||
for (const [prop, stringPrefBefore] of CommonEntries(PrefBefore))
|
||||
if (JSON.stringify(C[prop]) !== stringPrefBefore)
|
||||
toUpdate[/** @type {string} */(prop)] = data[prop];
|
||||
// @ts-expect-error Comparing objects key by key
|
||||
toUpdate[prop] = data[prop];
|
||||
|
||||
if (CommonVersionUpdated && (toUpdate != null) && (toUpdate.OnlineSharedSettings != null))
|
||||
toUpdate.OnlineSharedSettings.GameVersion = GameVersion;
|
||||
|
|
@ -437,23 +421,3 @@ function PreferenceInitNotificationSetting(setting, audio, defaultAlertType) {
|
|||
Audio: audio,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a named preference from one preference object to another if not already migrated
|
||||
* @param {object} from - The preference object to migrate from
|
||||
* @param {object} to - The preference object to migrate to
|
||||
* @param {string} prefName - The name of the preference to migrate
|
||||
* @param {any} defaultValue - The default value for the preference if it doesn't exist
|
||||
* @returns {void} - Nothing
|
||||
*/
|
||||
function PreferenceMigrate(from, to, prefName, defaultValue) {
|
||||
// Check that there's something to migrate (new characters) and that
|
||||
// we're not already migrated.
|
||||
|
||||
if (typeof from !== "object" || typeof to !== "object") return;
|
||||
if (to[prefName] == null) {
|
||||
to[prefName] = from[prefName];
|
||||
if (to[prefName] == null) to[prefName] = defaultValue;
|
||||
if (from[prefName] != null) delete from[prefName];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1421,7 +1421,7 @@ var ServerAccountDataSyncedValidate = {
|
|||
/**
|
||||
* @param {ServerAccountDataSynced["ChatSearchSettings"]} arg
|
||||
* @param {Character} C
|
||||
* @returns {ServerAccountDataSynced["ChatSearchSettings"]}
|
||||
* @returns {ChatRoomSearchSettings}
|
||||
*/
|
||||
ChatSearchSettings: (arg, C) => {
|
||||
return ValidationApplyRecord(arg, C, ServerChatRoomSearchSettingsValidate, false);
|
||||
|
|
|
|||
|
|
@ -507,7 +507,7 @@ function StruggleMinigameIsRunning() {
|
|||
* @param {Character} C - The character currently doing the struggling, either on itself (ie. as Player), or on someone else.
|
||||
* @param {StruggleKnownMinigames} MiniGame - The minigame to start
|
||||
* @param {Item | null} PrevItem - The item currently being present on the character, or null if none
|
||||
* @param {Item} NextItem - The item currently being added on the character, or null if it's a removal
|
||||
* @param {Item | null} NextItem - The item currently being added on the character, or null if it's a removal
|
||||
* @param {StruggleCompletionCallback} Completion - A callback that will be called when the minigame ends
|
||||
*/
|
||||
function StruggleMinigameStart(C, MiniGame, PrevItem, NextItem, Completion) {
|
||||
|
|
@ -743,8 +743,8 @@ function StruggleStrengthProcess(Decrease) {
|
|||
* escapee being bound in a way.
|
||||
*
|
||||
* @param {Character} C - The character who tries to struggle
|
||||
* @param {Item} PrevItem - The item, the character wants to struggle out of
|
||||
* @param {Item} [NextItem] - The item that should substitute the first one
|
||||
* @param {Item | null} PrevItem - The item, the character wants to struggle out of
|
||||
* @param {Item | null} [NextItem] - The item that should substitute the first one
|
||||
* @returns {{difficulty: number; auto: number; timer: number; }} - Nothing
|
||||
*/
|
||||
function StruggleStrengthGetDifficulty(C, PrevItem, NextItem) {
|
||||
|
|
|
|||
|
|
@ -1127,8 +1127,11 @@ function TranslationString(S, T) {
|
|||
*/
|
||||
function TranslationDialogArray(C, T) {
|
||||
for (let D = 0; D < C.Dialog.length; D++) {
|
||||
C.Dialog[D].Option = TranslationString(C.Dialog[D].Option, T);
|
||||
C.Dialog[D].Result = TranslationString(C.Dialog[D].Result, T);
|
||||
const { Option, Result } = C.Dialog[D];
|
||||
if (Option)
|
||||
C.Dialog[D].Option = TranslationString(Option, T);
|
||||
if (Result)
|
||||
C.Dialog[D].Result = TranslationString(Result, T);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
48
BondageClub/Scripts/Typedef.d.ts
vendored
48
BondageClub/Scripts/Typedef.d.ts
vendored
|
|
@ -118,7 +118,7 @@ type HTMLOptions<T extends keyof HTMLElementTagNameMap> = {
|
|||
*/
|
||||
dataAttributes?: Partial<Record<string, number | string | boolean>>;
|
||||
/** CSS style declarations that will be set on the HTML element (see {@link HTMLElement.style}). */
|
||||
style?: Record<string, string>;
|
||||
style?: Record<string, string | undefined>;
|
||||
/** Event listeners that will be attached to the HTML element (see {@link HTMLElement.addEventListener}). */
|
||||
eventListeners?: { [k in keyof HTMLElementEventMap]?: (this: HTMLElementTagNameMap[T], event: HTMLElementEventMap[k]) => any };
|
||||
/** The elements parent (if any) to which it will be attached (see {@link HTMLElement.parentElement}). */
|
||||
|
|
@ -405,7 +405,7 @@ declare namespace DialogMenu {
|
|||
/** Whether to hard reset and reconstruct the button grid, rather than just re-evaluating the existing button's states via a soft reset. */
|
||||
reset?: boolean;
|
||||
/** The to-be assigned custom status message */
|
||||
status?: string;
|
||||
status?: string | null;
|
||||
/** Display the {@link ReloadOptions.status} message on a timer; units are in ms */
|
||||
statusTimer?: number;
|
||||
/**
|
||||
|
|
@ -1796,13 +1796,13 @@ type ScriptPermissions = Record<ScriptPermissionProperty, ScriptPermission>;
|
|||
|
||||
interface DialogLine {
|
||||
Stage: string;
|
||||
NextStage: string;
|
||||
Option: string;
|
||||
Result: string;
|
||||
Function: string;
|
||||
Prerequisite: string;
|
||||
Group: string;
|
||||
Trait: string;
|
||||
NextStage: string | null;
|
||||
Option: string | null;
|
||||
Result: string | null;
|
||||
Function: string | null;
|
||||
Prerequisite: string | null;
|
||||
Group: string | null;
|
||||
Trait: string | null;
|
||||
}
|
||||
|
||||
interface DialogInfo<T extends ModuleType> {
|
||||
|
|
@ -2076,8 +2076,8 @@ interface Character {
|
|||
CanPickLocks: () => boolean;
|
||||
IsEdged: () => boolean;
|
||||
IsPlayer: () => this is PlayerCharacter;
|
||||
get X(): number | null;
|
||||
get Y(): number | null;
|
||||
get X(): number;
|
||||
get Y(): number;
|
||||
set X(X: number);
|
||||
set Y(Y: number);
|
||||
get Position(): ChatRoomMapPos | null;
|
||||
|
|
@ -2115,19 +2115,19 @@ interface Character {
|
|||
/**
|
||||
* Check if the player is ghosting the given target character (or member number)
|
||||
*/
|
||||
HasOnGhostlist: (this: PlayerCharacter, target?: Character | number) => boolean;
|
||||
HasOnGhostlist: (this: PlayerCharacter, target: Character | number) => boolean;
|
||||
/**
|
||||
* Check if the player is blacklisting the given target character (or member number)
|
||||
*/
|
||||
HasOnBlacklist: (target?: Character | number) => boolean;
|
||||
HasOnBlacklist: (target: Character | number) => boolean;
|
||||
/**
|
||||
* Check if the player is whitelisting the given target character (or member number)
|
||||
*/
|
||||
HasOnWhitelist: (target?: Character | number) => boolean;
|
||||
HasOnWhitelist: (target: Character | number) => boolean;
|
||||
/**
|
||||
* Check if the player is friend with the given target character (or member number)
|
||||
*/
|
||||
HasOnFriendlist: (this: PlayerCharacter, target?: Character | number) => boolean;
|
||||
HasOnFriendlist: (this: PlayerCharacter, target: Character | number) => boolean;
|
||||
/**
|
||||
* Check if this character is ghosted by the player
|
||||
*/
|
||||
|
|
@ -2389,7 +2389,7 @@ interface PlayerCharacter extends Character {
|
|||
GhostList: number[];
|
||||
Wardrobe: (ItemBundle[] | null)[];
|
||||
WardrobeCharacterNames: string[];
|
||||
SavedExpressions?: ({ Group: ExpressionGroupName, CurrentExpression?: ExpressionName }[] | null)[];
|
||||
SavedExpressions: ({ Group: ExpressionGroupName, CurrentExpression?: ExpressionName }[] | null)[];
|
||||
SavedColors: HSVColor[];
|
||||
FriendList: number[];
|
||||
FriendNames: Map<number, string>;
|
||||
|
|
@ -2561,10 +2561,6 @@ interface PlayerOnlineSettings {
|
|||
ShowRoomCustomization: 0 | 1 | 2 | 3; // 0 - Never, 1 - No by default, 2 - Yes by default, 3 - Always
|
||||
FriendListAutoRefresh: boolean;
|
||||
DefaultChatRoomBackground: string;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
SearchShowsFullRooms: never;
|
||||
}
|
||||
|
||||
/** Pandora Player extension */
|
||||
|
|
@ -4726,14 +4722,14 @@ interface ColorPickerInitOptions {
|
|||
dispatch?: boolean;
|
||||
}
|
||||
|
||||
//#end region
|
||||
// #end region
|
||||
|
||||
// #region Log
|
||||
|
||||
interface LogRecord {
|
||||
Name: LogNameType[LogGroupType];
|
||||
Group: LogGroupType;
|
||||
Value: number;
|
||||
Value: number | undefined;
|
||||
}
|
||||
|
||||
/** The logging groups as supported by the {@link LogRecord.Group} */
|
||||
|
|
@ -4825,7 +4821,7 @@ interface LogNameType {
|
|||
interface FavoriteState {
|
||||
TargetFavorite: boolean;
|
||||
PlayerFavorite: boolean;
|
||||
Icon: FavoriteIcon;
|
||||
Icon?: FavoriteIcon;
|
||||
UsableOrder: DialogSortOrder;
|
||||
UnusableOrder: DialogSortOrder;
|
||||
}
|
||||
|
|
@ -4922,9 +4918,9 @@ interface ArousalSettingsType {
|
|||
Activity: string;
|
||||
Zone: string;
|
||||
Fetish: string;
|
||||
OrgasmTimer?: number;
|
||||
OrgasmStage?: 0 | 1 | 2;
|
||||
OrgasmCount?: number;
|
||||
OrgasmTimer: number;
|
||||
OrgasmStage: 0 | 1 | 2;
|
||||
OrgasmCount: number;
|
||||
DisableAdvancedVibes: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue