Merge branch 'ts/strictify' into 'master'

More TS-strictification

See merge request BondageProjects/Bondage-College!6125
This commit is contained in:
BondageProjects 2026-04-24 21:48:25 -04:00
commit f306359d62
70 changed files with 905 additions and 749 deletions

View file

@ -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";
}
/**

View file

@ -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(),

View file

@ -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('<', '&lt;').replaceAll('>', '&gt;') || undefined);
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatSearchGetSpace() === (friend.ChatRoomSpace || '');
const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') ?? "");
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('<', '&lt;').replaceAll('>', '&gt;') || undefined);
const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') ?? "");
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');

View file

@ -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);

View file

@ -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;
}

View file

@ -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") {

View file

@ -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

View file

@ -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: () => {

View file

@ -171,7 +171,6 @@ function PreferenceSubscreenGeneralRun() {
MainCanvas.textAlign = "left";
if (PreferenceMessage != "") DrawText(TextGet(PreferenceMessage), 920, 125, "Red", "Black");
MainCanvas.textAlign = "center";
}
/**

View file

@ -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];

View file

@ -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");

View file

@ -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);

View file

@ -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,
};
/**

View file

@ -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);
}
/**

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict";
const PreferenceSubscreenSecurityIDs = Object.freeze({

View file

@ -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);
}

View file

@ -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 */

View file

@ -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 },

View file

@ -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);

View file

@ -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,

View file

@ -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);

View file

@ -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);

View file

@ -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)

View file

@ -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);

View file

@ -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;
}

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict";
/** @type {ExtendedItemCallbacks.BeforeDraw} */
@ -11,5 +10,5 @@ function AssetsItemArmsNylonRopeBeforeDraw(data) {
};
}
return null;
return data;
}

View file

@ -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,

View file

@ -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;

View file

@ -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";

View file

@ -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();

View file

@ -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 = {

View file

@ -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;

View file

@ -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 || {};

View file

@ -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,

View file

@ -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";
}

View file

@ -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

View file

@ -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()) {

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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

View file

@ -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,

View file

@ -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)) {

View file

@ -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");

View file

@ -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

View file

@ -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) {

View file

@ -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;

View file

@ -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();

View file

@ -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");

View file

@ -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;

View file

@ -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;

View file

@ -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 = "";

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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

View file

@ -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) {

View file

@ -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}

View file

@ -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,

View file

@ -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;
}
/**

View file

@ -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) {

View file

@ -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',

View file

@ -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) {

View file

@ -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", "");
}
}

View file

@ -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];
}
}

View file

@ -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);

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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;
}