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"; "use strict";
const BackgroundsStringsPath = "Backgrounds/Backgrounds.csv"; const BackgroundsStringsPath = "Backgrounds/Backgrounds.csv";
@ -319,7 +318,7 @@ const BackgroundsList = [
* @returns {string} * @returns {string}
*/ */
function BackgroundsTextGet(msg) { 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"; "use strict";
var BackgroundSelectionBackground = "Introduction"; var BackgroundSelectionBackground = "Introduction";
@ -7,15 +6,21 @@ var BackgroundSelectionList = [];
/** @type {BackgroundTag[]} */ /** @type {BackgroundTag[]} */
var BackgroundSelectionTagList = []; var BackgroundSelectionTagList = [];
var BackgroundSelectionIndex = 0; var BackgroundSelectionIndex = 0;
/** @type {string | null} */ /** @type {string} */
var BackgroundSelectionSelect = null; var BackgroundSelectionSelect = /** @type {never} */ (null);
var BackgroundSelectionSize = 12; var BackgroundSelectionSize = 12;
var BackgroundSelectionOffset = 0; var BackgroundSelectionOffset = 0;
/** @type {null | ((selection: string, setBackground: boolean) => void)} */ /** @type {null | ((selection: string, setBackground: boolean) => void)} */
var BackgroundSelectionCallback = null; var BackgroundSelectionCallback = null;
/** @type {never} */ /**
* @type {never}
* @deprecated
*/
var BackgroundSelectionReturnScreen; var BackgroundSelectionReturnScreen;
/** @type {never} */ /**
* @type {never}
* @deprecated
*/
var BackgroundSelectionAll; var BackgroundSelectionAll;
/** @type {string[]} */ /** @type {string[]} */
var BackgroundSelectionView = []; var BackgroundSelectionView = [];
@ -81,7 +86,7 @@ async function BackgroundSelectionLoad() {
parent: document.body, parent: document.body,
}); });
TextScreenCache.loadedPromise.then(() => { TextScreenCache?.loadedPromise.then(() => {
const searchFilter = ElementCreateSearchInput( const searchFilter = ElementCreateSearchInput(
Background.elementID.searchFilter, Background.elementID.searchFilter,
() => BackgroundSelectionList.map(i => BackgroundsTextGet(i)).sort(), () => BackgroundSelectionList.map(i => BackgroundsTextGet(i)).sort(),

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
//#region VARIABLES //#region VARIABLES
var FriendListBackground = "BrickWall"; var FriendListBackground = "BrickWall";
@ -420,7 +419,7 @@ function FriendListBeep(MemberNumber, data = null) {
if (FriendListBeepTarget === -1) { if (FriendListBeepTarget === -1) {
ElementCreateDiv(FriendListIDs.beepList); 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 beepTitle = data === null ? 'Send Beep' : data.Sent ? 'Sent Beep' : 'Received Beep';
const userName = Player.FriendNames.get(MemberNumber) ?? data?.MemberName; const userName = Player.FriendNames.get(MemberNumber) ?? data?.MemberName;
const userCaption = `${userName} [${MemberNumber}]`; const userCaption = `${userName} [${MemberNumber}]`;
@ -504,7 +503,10 @@ function FriendListBeep(MemberNumber, data = null) {
'Reply' 'Reply'
], ],
eventListeners: { eventListeners: {
click: () => FriendListBeep(data?.MemberNumber) click: () => {
if (typeof data?.MemberNumber === "number")
FriendListBeep(data.MemberNumber);
}
} }
} }
] ]
@ -554,7 +556,7 @@ function FriendListBeepMenuSend() {
*/ */
async function FriendListShowBeep(i) { async function FriendListShowBeep(i) {
const beep = FriendListBeepLog[i]; const beep = FriendListBeepLog[i];
if (!beep) return; if (typeof beep?.MemberNumber !== "number") return;
FriendListModeIndex = 1; FriendListModeIndex = 1;
await FriendListShow(); await FriendListShow();
FriendListBeep(beep.MemberNumber, beep); FriendListBeep(beep.MemberNumber, beep);
@ -564,10 +566,10 @@ async function FriendListShowBeep(i) {
//#region FRIEND LIST //#region FRIEND LIST
/** /**
* Exits the friendlist * Exits the friendlist
* @param {string} room The room to search for * @param {string | undefined} room The room to search for
*/ */
async function FriendListChatSearch(room) { 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 // 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 // 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 // overwrite the underlying search string so it searches for that
@ -626,9 +628,10 @@ function FriendListLoadFriendList(data) {
}; };
const sortingSymbol = FriendListSortingDirection === "Asc" ? "↑" : "↓"; const sortingSymbol = FriendListSortingDirection === "Asc" ? "↑" : "↓";
const friendListScrollPercent = ElementGetScrollPercentage(FriendListIDs.friendList) || 0; const friendListScrollPercent = ElementGetScrollPercentage(FriendListIDs.friendList) || 0;
const friendList = document.getElementById(FriendListIDs.friendList); const friendList = /** @type {HTMLElement} */ (document.getElementById(FriendListIDs.friendList));
friendList.innerHTML = ""; friendList.innerHTML = "";
/** @type {HTMLDivElement[]} */
const FriendListContent = []; const FriendListContent = [];
const mode = FriendListMode[FriendListModeIndex]; const mode = FriendListMode[FriendListModeIndex];
@ -659,7 +662,7 @@ function FriendListLoadFriendList(data) {
"friend-list-relation-type": "RelationType", "friend-list-relation-type": "RelationType",
}; };
CommonEntries(columnHeaders).forEach(([id, modeName]) => { CommonEntries(columnHeaders).forEach(([id, modeName]) => {
const elem = document.getElementById(id); const elem = /** @type {HTMLElement} */ (document.getElementById(id));
const elemSortingSymbol = FriendListSortingMode === modeName ? sortingSymbol : "↕"; const elemSortingSymbol = FriendListSortingMode === modeName ? sortingSymbol : "↕";
elem.textContent = `${TextGet(modeName)} ${elemSortingSymbol}`; elem.textContent = `${TextGet(modeName)} ${elemSortingSymbol}`;
switch (elemSortingSymbol) { switch (elemSortingSymbol) {
@ -682,8 +685,8 @@ function FriendListLoadFriendList(data) {
for (const friend of data) { for (const friend of data) {
const originalChatRoomName = friend.ChatRoomName || ''; const originalChatRoomName = friend.ChatRoomName || '';
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`); const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`);
const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') || undefined); const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') ?? "");
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatSearchGetSpace() === (friend.ChatRoomSpace || ''); const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatSearchGetSpace() === (friend.ChatRoomSpace ?? "");
const canBeep = true; const canBeep = true;
friendRawData.push({ friendRawData.push({
@ -711,9 +714,9 @@ function FriendListLoadFriendList(data) {
for (let i = FriendListBeepLog.length - 1; i >= 0; i--) { for (let i = FriendListBeepLog.length - 1; i >= 0; i--) {
const beepData = FriendListBeepLog[i]; const beepData = FriendListBeepLog[i];
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${beepData.ChatRoomSpace || "F"}`); 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 = ''; let beepCaption = '';
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatSearchGetSpace() === (beepData.ChatRoomSpace || ''); const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatSearchGetSpace() === (beepData.ChatRoomSpace ?? "");
const rawBeepCaption = []; const rawBeepCaption = [];
if (beepData.Sent) { if (beepData.Sent) {
@ -731,7 +734,7 @@ function FriendListLoadFriendList(data) {
friendRawData.push({ friendRawData.push({
memberName: beepData.MemberName, memberName: beepData.MemberName,
memberNumber: beepData.MemberNumber, memberNumber: beepData.MemberNumber,
relationType: FriendListGetRelationType(beepData.MemberNumber), relationType: FriendListGetRelationType(beepData.MemberNumber ?? 0),
pending: false, pending: false,
chatRoom: { chatRoom: {
name: beepData.ChatRoomName, name: beepData.ChatRoomName,
@ -799,7 +802,7 @@ function FriendListLoadFriendList(data) {
tag: "td", tag: "td",
classList: ['friend-list-column', 'MemberNumber'], classList: ['friend-list-column', 'MemberNumber'],
children: [ children: [
friend.memberNumber.toString() `${friend.memberNumber ?? ""}`
], ],
}, },
] ]
@ -917,7 +920,7 @@ function FriendListLoadFriendList(data) {
innerHTML: friend.chatRoom.caption, innerHTML: friend.chatRoom.caption,
style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined }, style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined },
eventListeners: { eventListeners: {
click: () => FriendListChatSearch(friend.chatRoom.name), click: () => FriendListChatSearch(friend.chatRoom?.name),
}, },
}), }),
); );
@ -929,7 +932,10 @@ function FriendListLoadFriendList(data) {
row.append( row.append(
ElementButton.Create( ElementButton.Create(
`friend-list-beep-${friend.memberNumber}`, `friend-list-beep-${friend.memberNumber}`,
() => FriendListBeep(friend.memberNumber), () => {
if (typeof friend.memberNumber === "number")
FriendListBeep(friend.memberNumber);
},
{ noStyling: true }, { noStyling: true },
{ button: { { button: {
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content'], classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content'],
@ -939,7 +945,7 @@ function FriendListLoadFriendList(data) {
), ),
ElementButton.Create( ElementButton.Create(
`friend-list-show-beep-${friend.memberNumber}`, `friend-list-show-beep-${friend.memberNumber}`,
() => FriendListShowBeep(friend.beep.beepIndex), () => { if (friend.beep?.beepIndex) FriendListShowBeep(friend.beep.beepIndex); },
{ noStyling: true }, { noStyling: true },
{ {
button: { button: {
@ -983,7 +989,10 @@ function FriendListLoadFriendList(data) {
row.appendChild(ElementButton.Create( row.appendChild(ElementButton.Create(
`friend-list-delete-${friend.memberNumber}`, `friend-list-delete-${friend.memberNumber}`,
() => FriendListDelete(friend.memberNumber), () => {
if (typeof friend.memberNumber === "number")
FriendListDelete(friend.memberNumber);
},
{ noStyling: true }, { noStyling: true },
{ button: { { button: {
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'], classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
@ -1044,6 +1053,7 @@ function FriendListAddFriends() {
return; return;
}; };
/** @type {number[]} */
const addedMembers = []; const addedMembers = [];
memberNumbers.forEach((memberNumber) => { memberNumbers.forEach((memberNumber) => {
if (!CommonIsNonNegativeInteger(memberNumber)) return; if (!CommonIsNonNegativeInteger(memberNumber)) return;
@ -1074,8 +1084,8 @@ function FriendListChangeMode(modeIndex) {
else if (FriendListModeIndex >= FriendListMode.length) FriendListModeIndex = 0; else if (FriendListModeIndex >= FriendListMode.length) FriendListModeIndex = 0;
FriendListSortingMode = 'None'; FriendListSortingMode = 'None';
FriendListSortingDirection = 'Asc'; FriendListSortingDirection = 'Asc';
document.getElementById(FriendListIDs.root).dataset.mode = FriendListMode[FriendListModeIndex]; document.getElementById(FriendListIDs.root)?.setAttribute("data-mode", FriendListMode[FriendListModeIndex]);
document.getElementById(FriendListIDs.modeTitle).textContent = TextGet(FriendListMode[FriendListModeIndex]); document.getElementById(FriendListIDs.modeTitle)?.replaceChildren(TextGet(FriendListMode[FriendListModeIndex]));
ServerSend("AccountQuery", { Query: "OnlineFriends" }); ServerSend("AccountQuery", { Query: "OnlineFriends" });
} }
@ -1092,8 +1102,8 @@ function FriendListSort(sortingMode, sortingDirection) {
const items = friendlist.children; const items = friendlist.children;
const sortedItems = Array.from(items).sort((elmA, elmB) => { const sortedItems = Array.from(items).sort((elmA, elmB) => {
const contentA = elmA.querySelector(`.${sortingMode}`)?.textContent; const contentA = elmA.querySelector(`.${sortingMode}`)?.textContent ?? "";
const contentB = elmB.querySelector(`.${sortingMode}`)?.textContent; const contentB = elmB.querySelector(`.${sortingMode}`)?.textContent ?? "";
const numberA = Number.parseInt(contentA, 10); const numberA = Number.parseInt(contentA, 10);
const numberB = Number.parseInt(contentB, 10); const numberB = Number.parseInt(contentB, 10);
if (!isNaN(numberA) && !isNaN(numberB)) { if (!isNaN(numberA) && !isNaN(numberB)) {
@ -1119,9 +1129,9 @@ function FriendListSearchByProperties(text) {
const items = friendlist.children; const items = friendlist.children;
Array.from(items).forEach((element) => element.toggleAttribute("hidden", true)); Array.from(items).forEach((element) => element.toggleAttribute("hidden", true));
const searchedItems = Array.from(items).filter(item => { const searchedItems = Array.from(items).filter(item => {
return item.querySelector('.MemberName')?.textContent.toLowerCase().includes(text.toLowerCase()) || return (item.querySelector('.MemberName')?.textContent ?? "").toLowerCase().includes(text.toLowerCase()) ||
item.querySelector('.MemberNickname')?.textContent.toLowerCase().includes(text.toLowerCase()) || // NYI (item.querySelector('.MemberNickname')?.textContent ?? "").toLowerCase().includes(text.toLowerCase()) || // NYI
item.querySelector('.MemberNumber')?.textContent.includes(text); (item.querySelector('.MemberNumber')?.textContent ?? "").includes(text);
}); });
searchedItems.forEach((item) => { searchedItems.forEach((item) => {
item.toggleAttribute("hidden", false); item.toggleAttribute("hidden", false);
@ -1142,7 +1152,7 @@ function FriendListSearchByProperties(text) {
* @returns {string} - The innerHTML with the searched text highlighted * @returns {string} - The innerHTML with the searched text highlighted
*/ */
function FriendListHighlightProperty(element, text) { function FriendListHighlightProperty(element, text) {
const textContent = element.textContent; const textContent = element.textContent ?? "";
if (!text) return textContent; if (!text) return textContent;
const regex = new RegExp(text.toLowerCase(), 'gi'); const regex = new RegExp(text.toLowerCase(), 'gi');

View file

@ -1,7 +1,11 @@
// @ts-strict-ignore
"use strict"; "use strict";
var InformationSheetBackground = "Sheet"; 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; var InformationSheetSelection = null;
/** @type {ScreenSpecifier | null} */ /** @type {ScreenSpecifier | null} */
var InformationSheetReturnScreen = null; var InformationSheetReturnScreen = null;
@ -12,6 +16,8 @@ var InformationSheetSecondScreen = false;
* @type {ScreenLoadHandler} * @type {ScreenLoadHandler}
*/ */
async function InformationSheetLoad() { async function InformationSheetLoad() {
if (!InformationSheetSelection) throw new Error("No character selected");
TextPrefetch("Character", "FriendList"); TextPrefetch("Character", "FriendList");
TextPrefetch("Character", "Preference"); TextPrefetch("Character", "Preference");
TextPrefetch("Character", "Title"); TextPrefetch("Character", "Title");
@ -72,7 +78,7 @@ async function InformationSheetLoad() {
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function InformationSheetRun() { function InformationSheetRun() {
if (!InformationSheetSelection) return;
// Draw the character base values // Draw the character base values
const C = InformationSheetSelection; const C = InformationSheetSelection;
const CurrentTitle = TitleGet(C); const CurrentTitle = TitleGet(C);
@ -102,7 +108,7 @@ function InformationSheetRun() {
currentY += spacingLarge; 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 memberLabel = TextGet(C.IsBirthday() ? "Birthday" : "MemberFor");
const creationFormatted = CommonFormatDurationRange(CurrentTime, C.Creation, { showFull: true, includeYears: true, includeMonths: true, includeDays: true }); const creationFormatted = CommonFormatDurationRange(CurrentTime, C.Creation, { showFull: true, includeYears: true, includeMonths: true, includeDays: true });
const memberForLabel = `${memberLabel} ${creationFormatted}`; const memberForLabel = `${memberLabel} ${creationFormatted}`;
@ -145,18 +151,19 @@ function InformationSheetRun() {
} }
currentY += spacing; currentY += spacing;
const Love = C.Love ?? 0;
let relationshipQualifier = ""; let relationshipQualifier = "";
if (C.Love >= 100) relationshipQualifier = "RelationshipPerfect"; if (Love >= 100) relationshipQualifier = "RelationshipPerfect";
else if (C.Love >= 75) relationshipQualifier = "RelationshipGreat"; else if (Love >= 75) relationshipQualifier = "RelationshipGreat";
else if (C.Love >= 50) relationshipQualifier = "RelationshipGood"; else if (Love >= 50) relationshipQualifier = "RelationshipGood";
else if (C.Love >= 25) relationshipQualifier = "RelationshipFair"; else if (Love >= 25) relationshipQualifier = "RelationshipFair";
else if (C.Love > -25) relationshipQualifier = "RelationshipNeutral"; else if (Love > -25) relationshipQualifier = "RelationshipNeutral";
else if (C.Love > -50) relationshipQualifier = "RelationshipPoor"; else if (Love > -50) relationshipQualifier = "RelationshipPoor";
else if (C.Love > -75) relationshipQualifier = "RelationshipBad"; else if (Love > -75) relationshipQualifier = "RelationshipBad";
else if (C.Love > -100) relationshipQualifier = "RelationshipHorrible"; else if (Love > -100) relationshipQualifier = "RelationshipHorrible";
else relationshipQualifier = "RelationshipAtrocious"; 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"); DrawTextFit(loveLine, 550, currentY, 450, "Black", "Gray");
currentY += spacing; currentY += spacing;
} }
@ -254,18 +261,19 @@ function InformationSheetRun() {
const lovership = C.GetLovership(); const lovership = C.GetLovership();
if (lovership.length < 1) DrawText(TextGet("None"), 1200, 200, "Black", "Gray"); if (lovership.length < 1) DrawText(TextGet("None"), 1200, 200, "Black", "Gray");
for (let [L, lover] of lovership.entries()) { 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})` : ""}`; const relationStageLabel = `${TextGet(`${stageText}With`)} ${lover.Name}${lover.MemberNumber ? ` (${lover.MemberNumber})` : ""}`;
DrawTextFit(relationStageLabel, 1200, 200 + L * 150, 600, "Black", "Gray"); 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"); DrawTextFit(relationDurationLabel, 1200, 260 + L * 150, 600, "Black", "Gray");
const hoverY = 260 + L * 150 - 20; const hoverY = 260 + L * 150 - 20;
if (MouseIn(1200, hoverY, 600, 40)) { if (MouseIn(1200, hoverY, 600, 40)) {
const relationStartDate = new Date(lover.Start).toLocaleString(undefined, { const relationStartDate = new Date(loveStart).toLocaleString(undefined, {
dateStyle: "medium", dateStyle: "medium",
timeStyle: "short", timeStyle: "short",
}); });
const relationDurationLabelShort = CommonFormatDurationRange(CurrentTime, lover.Start, { showFull: true }); const relationDurationLabelShort = CommonFormatDurationRange(CurrentTime, loveStart, { showFull: true });
DrawHoverElements.push(() => { DrawHoverElements.push(() => {
DrawButtonHover(1200, hoverY, 450, 40, `${relationStartDate} ${relationDurationLabelShort}`); DrawButtonHover(1200, hoverY, 450, 40, `${relationStartDate} ${relationDurationLabelShort}`);
@ -287,21 +295,22 @@ function InformationSheetRun() {
currentY += spacing; currentY += spacing;
} }
if (playerLove) { 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()}`; const loverStageLabel = `${TextGet(`${stageText}With`)} ${C.LoverName()}`;
DrawText(loverStageLabel, 550, currentY, "Black", "Gray"); DrawText(loverStageLabel, 550, currentY, "Black", "Gray");
currentY += spacing; 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"); DrawText(loverDurationLabel, 550, currentY, "Black", "Gray");
const y = currentY - 20; const y = currentY - 20;
if (MouseIn(550, y, 450, 40)) if (MouseIn(550, y, 450, 40))
DrawHoverElements.push(() => { DrawHoverElements.push(() => {
const loverStartDate = new Date(playerLove.Start).toLocaleString(undefined, { const loverStartDate = new Date(loveStart).toLocaleString(undefined, {
dateStyle: "medium", dateStyle: "medium",
timeStyle: "short", 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); DrawButtonHover(550, y, 450, 40, loverDurationTooltip);
}); });
currentY += spacing; currentY += spacing;
@ -338,11 +347,10 @@ function InformationSheetRun() {
// After one week we show the traits, after two weeks we show the level // After one week we show the traits, after two weeks we show the level
if (CurrentTime >= NPCEventGet(C, "PrivateRoomEntry") * CheatFactor("AutoShowTraits", 0) + 604800000) { if (CurrentTime >= NPCEventGet(C, "PrivateRoomEntry") * CheatFactor("AutoShowTraits", 0) + 604800000) {
let Pos = 0; let Pos = 0;
for (let T = 0; T < C.Trait.length; T++) for (const trait of C.Trait ?? []) {
if ((C.Trait[T].Value != null) && (C.Trait[T].Value != 0)) { 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");
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++;
Pos++; }
}
} else DrawText(TextGet("TraitUnknown"), 1000, 200, "Black", "Gray"); } else DrawText(TextGet("TraitUnknown"), 1000, 200, "Black", "Gray");
} }
@ -355,9 +363,9 @@ function InformationSheetRun() {
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function InformationSheetSecondScreenRun() { function InformationSheetSecondScreenRun() {
if (!InformationSheetSelection) return;
// For current player and online characters // For current player and online characters
var C = InformationSheetSelection; const C = InformationSheetSelection;
if (C.IsPlayer() || C.IsOnline()) { if (C.IsPlayer() || C.IsOnline()) {
const lineHeight = 55; const lineHeight = 55;
// Draw the reputation section // Draw the reputation section
@ -417,7 +425,8 @@ function InformationSheetSecondScreenRun() {
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function InformationSheetClick() { function InformationSheetClick() {
var C = InformationSheetSelection; if (!InformationSheetSelection) return;
const C = InformationSheetSelection;
if (MouseIn(1815, 75, 90, 90)) InformationSheetExit(); if (MouseIn(1815, 75, 90, 90)) InformationSheetExit();
if (C.IsPlayer()) { if (C.IsPlayer()) {
if (MouseIn(1815, 190, 90, 90) && !TitleIsForced(TitleGet(C))) CommonSetScreen("Character", "Title"); if (MouseIn(1815, 190, 90, 90) && !TitleIsForced(TitleGet(C))) CommonSetScreen("Character", "Title");
@ -467,6 +476,7 @@ function InformationSheetLoadCharacter(C) {
} }
function InformationSheetResize() { function InformationSheetResize() {
if (!InformationSheetSelection) return;
const C = InformationSheetSelection; const C = InformationSheetSelection;
if (C.IsPlayer()) { if (C.IsPlayer()) {
ElementSetPosition("AllowedInteractions-dropdown-container", 550, 800); ElementSetPosition("AllowedInteractions-dropdown-container", 550, 800);

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
var LoginBackground = "Dressing"; var LoginBackground = "Dressing";
/** /**
@ -77,7 +76,7 @@ var LoginErrorMessage = "";
* The dummy on the login screen. * The dummy on the login screen.
* *
* Lifetime bound to the screen. * Lifetime bound to the screen.
* @type {NPCCharacter} */ * @type {NPCCharacter | null} */
var LoginCharacter; var LoginCharacter;
/* DEBUG: To measure FPS - uncomment this and change the + 4000 to + 40 /* DEBUG: To measure FPS - uncomment this and change the + 4000 to + 40
@ -108,10 +107,11 @@ const LoginIDs = Object.freeze({
*/ */
function LoginDoNextThankYou() { function LoginDoNextThankYou() {
LoginThankYou = CommonRandomItemFromList(LoginThankYou, LoginThankYouList); LoginThankYou = CommonRandomItemFromList(LoginThankYou, LoginThankYouList);
CharacterRelease(LoginCharacter, false); const char = /** @type {NPCCharacter} */ (LoginCharacter);
CharacterAppearanceFullRandom(LoginCharacter); CharacterRelease(char, false);
if (InventoryGet(LoginCharacter, "ItemNeck") != null) InventoryRemove(LoginCharacter, "ItemNeck", false); CharacterAppearanceFullRandom(char);
CharacterFullRandomRestrain(LoginCharacter); if (InventoryGet(char, "ItemNeck") != null) InventoryRemove(char, "ItemNeck", false);
CharacterFullRandomRestrain(char);
LoginThankYouNext = CommonTime() + 4000; LoginThankYouNext = CommonTime() + 4000;
} }
@ -343,7 +343,7 @@ async function LoginLoad() {
ActivityDictionaryLoad(); ActivityDictionaryLoad();
AssetLoadDescription("Female3DCG"); AssetLoadDescription("Female3DCG");
const timer = TimerCreate(() => { const timer = TimerCreate(() => {
TextScreenCache.loadedPromise.then(() => { TextScreenCache?.loadedPromise.then(() => {
LoginReloadLanguageText(); LoginReloadLanguageText();
timer?.(); timer?.();
}); });
@ -387,16 +387,17 @@ function LoginRun() {
// Draw the login controls // Draw the login controls
const status = ElementWrap(LoginIDs.status); const status = ElementWrap(LoginIDs.status);
const statusNewText = LoginGetStatus() ?? TextGet("EnterNamePassword"); const statusNewText = LoginGetStatus() ?? TextGet("EnterNamePassword");
if (status.textContent !== statusNewText) { if (status && status.textContent !== statusNewText) {
status.textContent = statusNewText; status.textContent = statusNewText;
} }
ElementWrap(LoginIDs.login).toggleAttribute("disabled", !CanLogin); ElementWrap(LoginIDs.login)?.toggleAttribute("disabled", !CanLogin);
ElementWrap(LoginIDs.register).toggleAttribute("disabled", !CanLogin); ElementWrap(LoginIDs.register)?.toggleAttribute("disabled", !CanLogin);
ElementWrap(LoginIDs.passwordReset).toggleAttribute("disabled", !CanLogin); ElementWrap(LoginIDs.passwordReset)?.toggleAttribute("disabled", !CanLogin);
ElementWrap(LoginIDs.cheats).style.display = CheatAllow ? "" : "none"; const cheatElem = ElementWrap(LoginIDs.cheats);
if (cheatElem) cheatElem.style.display = CheatAllow ? "" : "none";
// Draw the character and thank you bubble // 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(); if (LoginThankYouNext < CommonTime()) LoginDoNextThankYou();
DrawImage("Screens/" + CurrentModule + "/" + CurrentScreen + "/Bubble.png", 1400, 16); DrawImage("Screens/" + CurrentModule + "/" + CurrentScreen + "/Bubble.png", 1400, 16);
DrawText(TextGet("ThankYou") + " " + LoginThankYou, 1625, 53, "Black", "Gray"); DrawText(TextGet("ThankYou") + " " + LoginThankYou, 1625, 53, "Black", "Gray");
@ -445,19 +446,19 @@ function LoginUnload() {
ElementRemove(LoginIDs.cheats); ElementRemove(LoginIDs.cheats);
ElementRemove(LoginIDs.language); ElementRemove(LoginIDs.language);
CharacterDelete(LoginCharacter); CharacterDelete(/** @type {NPCCharacter} */ (LoginCharacter));
LoginCharacter = null; LoginCharacter = null;
} }
/** @type {ScreenFunctions["Resize"]} */ /** @type {ScreenResizeHandler} */
function LoginResize(load) { function LoginResize(load) {
ElementPositionFixed(LoginIDs.welcome, 500, 50, 1000, null); ElementPositionFixed(LoginIDs.welcome, 500, 50, 1000);
ElementPositionFixed(LoginIDs.status, 500, 100, 1000, null); ElementPositionFixed(LoginIDs.status, 500, 100, 1000);
ElementPositionFixed(LoginIDs.nameLabel, 500, 200, 1000, null); ElementPositionFixed(LoginIDs.nameLabel, 500, 200, 1000);
ElementPositionFixed(LoginIDs.name, 750, 260, 500, null); ElementPositionFixed(LoginIDs.name, 750, 260, 500);
ElementPositionFixed(LoginIDs.passwordLabel, 500, 330, 1000, null); ElementPositionFixed(LoginIDs.passwordLabel, 500, 330, 1000);
ElementPositionFixed(LoginIDs.password, 750, 390, 500, null); ElementPositionFixed(LoginIDs.password, 750, 390, 500);
ElementSetPosition(LoginIDs.login, 1000, 490); ElementSetPosition(LoginIDs.login, 1000, 490);
ElementSetFontSize(LoginIDs.login, "auto"); ElementSetFontSize(LoginIDs.login, "auto");
@ -470,16 +471,42 @@ function LoginResize(load) {
} }
function LoginReloadLanguageText() { function LoginReloadLanguageText() {
ElementWrap(LoginIDs.welcome).textContent = TextGet("Welcome"); const welcome = ElementWrap(LoginIDs.welcome);
ElementWrap(LoginIDs.status).textContent = LoginGetStatus() ?? TextGet("EnterNamePassword"); if (welcome) {
ElementWrap(LoginIDs.nameLabel).textContent = TextGet("AccountName"); welcome.textContent = TextGet("Welcome");
ElementWrap(LoginIDs.passwordLabel).textContent = TextGet("Password"); }
const status = ElementWrap(LoginIDs.status);
ElementWrap(LoginIDs.newCharacter).textContent = TextGet("CreateNewCharacter"); if (status) {
ElementWrap(LoginIDs.login).querySelector("span").textContent = TextGet("Login"); status.textContent = LoginGetStatus() ?? TextGet("EnterNamePassword");
ElementWrap(LoginIDs.register).querySelector("span").textContent = TextGet("NewCharacter"); }
ElementWrap(LoginIDs.passwordReset).querySelector("span").textContent = TextGet("PasswordReset"); const nameLabel = ElementWrap(LoginIDs.nameLabel);
ElementWrap(LoginIDs.cheats).querySelector("span").textContent = TextGet("Cheats"); 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 * 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) { function LoginPerformCraftingFixups(Crafting) {
if (!Crafting || !CommonIsArray(Crafting)) return; if (!Crafting || !CommonIsArray(Crafting)) return;
@ -904,6 +931,7 @@ function LoginDifficulty(applyDefaults) {
function LoginExtremeItemSettings(applyDefaults) { function LoginExtremeItemSettings(applyDefaults) {
const LimitedAssets = new Set(MainHallStrongLocks.map(i => `ItemMisc/${i}`)); const LimitedAssets = new Set(MainHallStrongLocks.map(i => `ItemMisc/${i}`));
for (const [name, permission] of CommonEntries(Player.PermissionItems)) { for (const [name, permission] of CommonEntries(Player.PermissionItems)) {
if (!permission) continue;
permission.Hidden = false; permission.Hidden = false;
const limitedAllowed = LimitedAssets.has(name); const limitedAllowed = LimitedAssets.has(name);
@ -996,7 +1024,7 @@ function LoginSetupPlayer(C) {
FullRooms: true, FullRooms: true,
ShowLocked: true, ShowLocked: true,
SearchDescriptions: false, SearchDescriptions: false,
MapTypes: undefined, MapTypes: "",
RoomMinSize: 2, RoomMinSize: 2,
RoomMaxSize: 20, RoomMaxSize: 20,
FilterTerms: "", FilterTerms: "",
@ -1004,12 +1032,14 @@ function LoginSetupPlayer(C) {
if (C.RoomSearchLanguage != null) { if (C.RoomSearchLanguage != null) {
C.ChatSearchSettings.Language = C.RoomSearchLanguage; C.ChatSearchSettings.Language = C.RoomSearchLanguage;
C.RoomSearchLanguage = undefined; C.RoomSearchLanguage = undefined;
// @ts-ignore Strict-TS: This ought to be deleted server-side
ServerAccountUpdate.QueueData({ RoomSearchLanguage: null }); ServerAccountUpdate.QueueData({ RoomSearchLanguage: null });
} }
updateSearchSettings = true; updateSearchSettings = true;
} }
if (C.ChatSearchFilterTerms) { if (C.ChatSearchFilterTerms) {
C.ChatSearchSettings.FilterTerms = C.ChatSearchFilterTerms; C.ChatSearchSettings.FilterTerms = C.ChatSearchFilterTerms;
// @ts-ignore Strict-TS: This ought to be deleted server-side
ServerAccountUpdate.QueueData({ ChatSearchFilterTerms: null }); ServerAccountUpdate.QueueData({ ChatSearchFilterTerms: null });
updateSearchSettings = true; updateSearchSettings = true;
} }

View file

@ -58,6 +58,9 @@ function OnlineProfileLoadTextArea(element) {
* @type {ScreenLoadHandler} * @type {ScreenLoadHandler}
*/ */
async function OnlineProfileLoad() { async function OnlineProfileLoad() {
if (!InformationSheetSelection) {
throw new Error('Missing "InformationSheetSelection" data');
}
OnlineProfileTextDesc = typeof InformationSheetSelection.Description === "string" ? InformationSheetSelection.Description : ""; OnlineProfileTextDesc = typeof InformationSheetSelection.Description === "string" ? InformationSheetSelection.Description : "";
OnlineProfileTextOwnersNotes = typeof InformationSheetSelection.Ownership?.Notes === "string" ? InformationSheetSelection.Ownership.Notes : ""; OnlineProfileTextOwnersNotes = typeof InformationSheetSelection.Ownership?.Notes === "string" ? InformationSheetSelection.Ownership.Notes : "";
OnlineProfileNotesAvailable = InformationSheetSelection.IsFullyOwnedByPlayer() || OnlineProfileTextOwnersNotes != ""; OnlineProfileNotesAvailable = InformationSheetSelection.IsFullyOwnedByPlayer() || OnlineProfileTextOwnersNotes != "";
@ -85,6 +88,7 @@ function OnlineProfileUnload() {
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function OnlineProfileRun() { function OnlineProfileRun() {
if (!InformationSheetSelection) return;
// Sets the screen controls // Sets the screen controls
let legend = ""; let legend = "";
let maxlen = 0; let maxlen = 0;
@ -122,6 +126,7 @@ function OnlineProfileRun() {
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function OnlineProfileClick() { function OnlineProfileClick() {
if (!InformationSheetSelection) return;
if (OnlineProfileNotesAvailable && MouseIn(1620, 60, 90, 90)) { if (OnlineProfileNotesAvailable && MouseIn(1620, 60, 90, 90)) {
/* Toggle between Description and Owner's Notes. */ /* Toggle between Description and Owner's Notes. */
const ev = ElementValue("DescriptionInput").trim(); const ev = ElementValue("DescriptionInput").trim();
@ -149,6 +154,7 @@ function OnlineProfileClick() {
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function OnlineProfileExit(Save=false) { function OnlineProfileExit(Save=false) {
if (!InformationSheetSelection) return;
if (Save) { if (Save) {
const ev = ElementValue("DescriptionInput").trim(); const ev = ElementValue("DescriptionInput").trim();
if (OnlineProfileMode == "Description") { if (OnlineProfileMode == "Description") {

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ArousalActiveName[]} */ /** @type {ArousalActiveName[]} */
@ -10,8 +9,11 @@ var PreferenceArousalVisibleIndex = 0;
/** @type {ArousalAffectStutterName[]} */ /** @type {ArousalAffectStutterName[]} */
var PreferenceArousalAffectStutterList = ["None", "Arousal", "Vibration", "All"]; var PreferenceArousalAffectStutterList = ["None", "Arousal", "Vibration", "All"];
var PreferenceArousalAffectStutterIndex = 0; var PreferenceArousalAffectStutterIndex = 0;
/** @type {null | ActivityName[]} */ /**
var PreferenceArousalActivityList = null; * Initialized by {@link PreferenceSubscreenArousalLoad}
* @type {ActivityName[]}
*/
var PreferenceArousalActivityList;
var PreferenceArousalActivityIndex = 0; var PreferenceArousalActivityIndex = 0;
/** @type {never} */ /** @type {never} */
var PreferenceArousalActivityFactorSelf; var PreferenceArousalActivityFactorSelf;
@ -19,8 +21,11 @@ var PreferenceArousalActivityFactorSelf;
var PreferenceArousalActivityFactorOther; var PreferenceArousalActivityFactorOther;
/** @type {never} */ /** @type {never} */
var PreferenceArousalZoneFactor; var PreferenceArousalZoneFactor;
/** @type {null | FetishName[]} */ /**
var PreferenceArousalFetishList = null; * Initialized by {@link PreferenceSubscreenArousalLoad}
* @type {FetishName[]}
*/
var PreferenceArousalFetishList;
var PreferenceArousalFetishIndex = 0; var PreferenceArousalFetishIndex = 0;
/** @type {never} */ /** @type {never} */
var PreferenceArousalFetishFactor; var PreferenceArousalFetishFactor;
@ -202,8 +207,10 @@ function PreferenceSubscreenArousalClick() {
if ((Player.FocusGroup != null) && MouseIn(550, 853, 600, 64)) { if ((Player.FocusGroup != null) && MouseIn(550, 853, 600, 64)) {
const step = MouseX <= 850 ? -1 : +1; const step = MouseX <= 850 ? -1 : +1;
const zone = PreferenceGetArousalZone(Player, Player.FocusGroup.Name); const zone = PreferenceGetArousalZone(Player, Player.FocusGroup.Name);
const factor = /** @type {ArousalFactor} */ (CommonModulo(zone.Factor + step, 5)); if (zone) {
PreferenceSetArousalZone(Player, zone.Name, factor); const factor = /** @type {ArousalFactor} */ (CommonModulo(zone.Factor + step, 5));
PreferenceSetArousalZone(Player, zone.Name, factor);
}
} }
// Arousal zone orgasm check box // Arousal zone orgasm check box

View file

@ -18,7 +18,7 @@ const PreferenceExtensionsIDs = Object.freeze({
function PreferenceSubscreenExtensionsLoad() { function PreferenceSubscreenExtensionsLoad() {
PreferenceExtensionsDisplay = Object.keys(PreferenceExtensionsSettings).map( PreferenceExtensionsDisplay = Object.keys(PreferenceExtensionsSettings).map(
k => ( k => (
s=>({ s => ({
Button: typeof s.ButtonText === "function" ? s.ButtonText() : s.ButtonText, Button: typeof s.ButtonText === "function" ? s.ButtonText() : s.ButtonText,
Image: s.Image && (typeof s.Image === "function" ? s.Image() : s.Image), Image: s.Image && (typeof s.Image === "function" ? s.Image() : s.Image),
click: () => { click: () => {

View file

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

View file

@ -15,12 +15,15 @@ var PreferenceGraphicsFontList = ["Arial", "TimesNewRoman", "Papyrus", "ComicSan
/** @type {WebGLPowerPreference[]} */ /** @type {WebGLPowerPreference[]} */
var PreferenceGraphicsPowerModes = ["low-power", "default", "high-performance"]; var PreferenceGraphicsPowerModes = ["low-power", "default", "high-performance"];
var PreferenceGraphicsFontIndex = 0; var PreferenceGraphicsFontIndex = 0;
/** @type {null | number} */ /** @type {number} */
var PreferenceGraphicsAnimationQualityIndex = null; var PreferenceGraphicsAnimationQualityIndex = -1;
/** @type {null | number} */ /** @type {number} */
var PreferenceGraphicsPowerModeIndex = null; var PreferenceGraphicsPowerModeIndex = -1;
/** @type {null | WebGLContextAttributes} */ /**
var PreferenceGraphicsWebGLOptions = null; * Tied to the screen's lifetime
* @type {WebGLContextAttributes}
*/
var PreferenceGraphicsWebGLOptions;
var PreferenceGraphicsAnimationQualityList = [10000, 2000, 200, 100, 50, 0]; var PreferenceGraphicsAnimationQualityList = [10000, 2000, 200, 100, 50, 0];
var PreferenceGraphicsFrameLimit = [0, 10, 15, 30, 60]; var PreferenceGraphicsFrameLimit = [0, 10, 15, 30, 60];

View file

@ -88,7 +88,7 @@ function PreferenceSubscreenNotificationsRun() {
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function PreferenceNotificationsDrawSetting(Left, Top, Text, Setting) { 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; const Enabled = Setting.AlertType > 0;
if (Enabled) { if (Enabled) {
DrawButton(Left + 200, Top, 64, 64, "", "White", "Icons/Audio" + Setting.Audio.toString() + ".png"); DrawButton(Left + 200, Top, 64, 64, "", "White", "Icons/Audio" + Setting.Audio.toString() + ".png");

View file

@ -1,8 +1,7 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {null | string[]} */ /** @type {string[]} */
var PreferenceOnlineDefaultBackgroundList = null; var PreferenceOnlineDefaultBackgroundList = /** @type {never} */ (null);
var PreferenceOnlineDefaultBackgroundIndex = 0; var PreferenceOnlineDefaultBackgroundIndex = 0;
var PreferenceOnlineDefaultBackground = ""; var PreferenceOnlineDefaultBackground = "";
@ -75,7 +74,7 @@ function PreferenceSubscreenOnlineLoad() {
dropdown dropdown
] ]
}); });
ElementWrap(PreferenceIDs.subscreen).append(grid); ElementWrap(PreferenceIDs.subscreen)?.append(grid);
const subtitle = ElementCreate({ const subtitle = ElementCreate({
tag: "label", tag: "label",
@ -105,7 +104,7 @@ function PreferenceSubscreenOnlineLoad() {
selection 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 * 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); DrawCharacter(Player, 50, 50, 0.9);
MainCanvas.textAlign = "center"; MainCanvas.textAlign = "center";
PreferencePageChangeDraw(1595, 75, 2); PreferencePageChangeDraw(1595, 75, 2);
ElementWrap(PreferenceSubscreenOnlineIDs.grid).toggleAttribute("hidden", PreferencePageCurrent !== 1); ElementWrap(PreferenceSubscreenOnlineIDs.grid)?.toggleAttribute("hidden", PreferencePageCurrent !== 1);
ElementWrap(PreferenceSubscreenOnlineIDs.grid2).toggleAttribute("hidden", PreferencePageCurrent !== 2); ElementWrap(PreferenceSubscreenOnlineIDs.grid2)?.toggleAttribute("hidden", PreferencePageCurrent !== 2);
if (PreferencePageCurrent === 2) { if (PreferencePageCurrent === 2) {
DrawImageResize("Backgrounds/" + PreferenceOnlineDefaultBackground + ".jpg", 500, 210, 300, 185); DrawImageResize("Backgrounds/" + PreferenceOnlineDefaultBackground + ".jpg", 500, 210, 300, 185);

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** /**
* The background to use for the settings screen * The background to use for the settings screen
@ -14,9 +13,9 @@ var PreferenceMessage = "";
/** /**
* The currently active subscreen * The currently active subscreen
* *
* @type {PreferenceSubscreen?} * @type {PreferenceSubscreen | null}
*/ */
var PreferenceSubscreen = null; var PreferenceSubscreen;
/** /**
* All the base settings screens * All the base settings screens
@ -239,10 +238,14 @@ function PreferenceRun() {
// Backward-compatibility: automatically substitute strings for the actual subscreen // Backward-compatibility: automatically substitute strings for the actual subscreen
if (typeof PreferenceSubscreen === "string") { if (typeof PreferenceSubscreen === "string") {
const subscreenName = PreferenceSubscreen === "" ? "Main" : PreferenceSubscreen; const subscreenName = PreferenceSubscreen === "" ? "Main" : PreferenceSubscreen;
PreferenceSubscreen = PreferenceSubscreens.find(s => s.name === subscreenName); const screen = PreferenceSubscreens.find(s => s.name === subscreenName);
if (!PreferenceSubscreen) PreferenceSubscreen = PreferenceSubscreens.find(s => s.name === "Main"); 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} * @type {ScreenExitHandler}
*/ */
function PreferenceExit() { 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 // If we are in a subscreen, the only exit is to the main preference screen
PreferenceSubscreenExit(); PreferenceSubscreenExit();
return; return;
@ -307,12 +310,12 @@ function PreferenceUnload() {
/** @type {ScreenResizeHandler} */ /** @type {ScreenResizeHandler} */
function PreferenceResize(onLoad) { function PreferenceResize(onLoad) {
PreferenceSubscreenResize?.(onLoad); PreferenceSubscreenResize?.(onLoad);
PreferenceSubscreen.resize?.(onLoad); PreferenceSubscreen?.resize?.(onLoad);
} }
/** @type {KeyboardEventListener} */ /** @type {KeyboardEventListener} */
function PreferenceKeyUp(event) { 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; return PreferenceSubscreen?.keyUp?.(event) ?? false;
} }
@ -334,6 +337,7 @@ function PreferenceSubscreenCreateSubscreen(subscreenName) {
return subscreen; return subscreen;
} }
/** @type {ScreenResizeHandler} */
function PreferenceSubscreenResize(onLoad) { function PreferenceSubscreenResize(onLoad) {
ElementPositionFixed(PreferenceIDs.subscreen, 0, 0, 2000, 1000); ElementPositionFixed(PreferenceIDs.subscreen, 0, 0, 2000, 1000);
ElementPositionFixed(PreferenceIDs.exit, 1815, 75, 90, 90); 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 * Exit from a specific subscreen by running its handler and checking its validity
*/ */
async function PreferenceSubscreenExit() { async function PreferenceSubscreenExit() {
const validExit = await PreferenceSubscreen.exit?.(); const validExit = await PreferenceSubscreen?.exit?.();
// Only when the results is false (not undefined) // Only when the results is false (not undefined)
// The exit is just a exit of the subscreen's substate, return to block more exit. // 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 * @namespace
*/ */
var PreferenceActivityEnjoymentDefault = { var PreferenceActivityEnjoymentDefault = {
/** @type {ActivityName | undefined} */ Name: /** @type {never} */ (undefined),
Name: undefined,
/** @type {ArousalFactor} */ /** @type {ArousalFactor} */
Self: 2, Self: 2,
/** @type {ArousalFactor} */ /** @type {ArousalFactor} */
@ -445,8 +448,7 @@ var PreferenceActivityEnjoymentDefault = {
* @namespace * @namespace
*/ */
var PreferenceArousalFetishDefault = { var PreferenceArousalFetishDefault = {
/** @type {FetishName | undefined} */ Name: /** @type {never} */ (undefined),
Name: undefined,
/** @type {ArousalFactor} */ /** @type {ArousalFactor} */
Factor: 2, Factor: 2,
}; };
@ -457,8 +459,7 @@ var PreferenceArousalFetishDefault = {
* @namespace * @namespace
*/ */
var PreferenceArousalZoneDefault = { var PreferenceArousalZoneDefault = {
/** @type {AssetGroupItemName | undefined} */ Name: /** @type {never} */ (undefined),
Name: undefined,
/** @type {ArousalFactor} */ /** @type {ArousalFactor} */
Factor: 2, Factor: 2,
/** @type {boolean} */ /** @type {boolean} */
@ -506,7 +507,9 @@ function PreferenceArousalUpdateValidation() {
const activities = AssetAllActivities("Female3DCG") const activities = AssetAllActivities("Female3DCG")
.filter(a => a.ActivityID != null) .filter(a => a.ActivityID != null)
.map(({ Name }) => ({ ...PreferenceActivityEnjoymentDefault, Name })) .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 PreferenceArousalSettingsDefault.Activity = activities
.map((act) => PreferenceArousalActivityToChar(act.Self, act.Other)) .map((act) => PreferenceArousalActivityToChar(act.Self, act.Other))
.join(""); .join("");
@ -519,7 +522,9 @@ function PreferenceArousalUpdateValidation() {
Orgasm: PreferenceArousalZoneOrgasmDefault.includes(group.Name), Orgasm: PreferenceArousalZoneOrgasmDefault.includes(group.Name),
})) }))
.filter(({ Name }) => Name !== undefined) .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 PreferenceArousalSettingsDefault.Zone = zones
.map(act => PreferenceArousalZoneToChar(act.Factor, act.Orgasm)) .map(act => PreferenceArousalZoneToChar(act.Factor, act.Orgasm))
.join(""); .join("");
@ -527,7 +532,9 @@ function PreferenceArousalUpdateValidation() {
const fetishes = AssetAllFetishes("Female3DCG") const fetishes = AssetAllFetishes("Female3DCG")
.filter(f => f.FetishID != null) .filter(f => f.FetishID != null)
.map(({ Name }) => ({ ...PreferenceArousalFetishDefault, Name })) .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 PreferenceArousalSettingsDefault.Fetish = fetishes
.map(fet => PreferenceArousalFetishToChar(fet.Factor)) .map(fet => PreferenceArousalFetishToChar(fet.Factor))
.join(""); .join("");
@ -614,7 +621,7 @@ var PreferenceArousalSettingsValidate = {
if (!CommonIsObject(oldZone) || typeof oldZone.Name !== "string" || typeof oldZone.Factor !== "number" || typeof oldZone.Orgasm !== "boolean") continue; if (!CommonIsObject(oldZone) || typeof oldZone.Name !== "string" || typeof oldZone.Factor !== "number" || typeof oldZone.Orgasm !== "boolean") continue;
const group = AssetGroupGet(C.AssetFamily, oldZone.Name); 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); 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. * Namespace with default values for {@link VisualSettingsType} properties.
* @type {Required<VisualSettingsType>} * @type {VisualSettingsType}
* @namespace * @namespace
*/ */
var PreferenceVisualSettingsDefault = { var PreferenceVisualSettingsDefault = {
@ -1017,8 +1024,6 @@ var PreferenceOnlineSettingsValidate = {
DefaultChatRoomBackground: (arg, C) => { DefaultChatRoomBackground: (arg, C) => {
return typeof arg === "string" ? arg : PreferenceOnlineSettingsDefault.DefaultChatRoomBackground; 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"; "use strict";
const PreferenceSubscreenRestrictionIDs = Object.freeze({ const PreferenceSubscreenRestrictionIDs = Object.freeze({
@ -22,7 +21,7 @@ function PreferenceSubscreenRestrictionLoad() {
attributes: { id: PreferenceSubscreenRestrictionIDs.hint }, attributes: { id: PreferenceSubscreenRestrictionIDs.hint },
children: [hintText] children: [hintText]
}); });
ElementWrap(PreferenceIDs.subscreen).append(hintLabel); ElementWrap(PreferenceIDs.subscreen)?.append(hintLabel);
const pairs = settingsList.map(s => { const pairs = settingsList.map(s => {
return ElementCheckbox.CreateLabelled(s, TextGet(`Restriction${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"; "use strict";
const PreferenceSubscreenSecurityIDs = Object.freeze({ const PreferenceSubscreenSecurityIDs = Object.freeze({

View file

@ -1,15 +1,17 @@
// @ts-strict-ignore
"use strict"; "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 PreferenceVisibilityGroupList = [];
var PreferenceVisibilityGroupIndex = 0; var PreferenceVisibilityGroupIndex = 0;
var PreferenceVisibilityAssetIndex = 0; var PreferenceVisibilityAssetIndex = 0;
var PreferenceVisibilityHideChecked = false; var PreferenceVisibilityHideChecked = false;
var PreferenceVisibilityBlockChecked = false; var PreferenceVisibilityBlockChecked = false;
var PreferenceVisibilityCanBlock = true; var PreferenceVisibilityCanBlock = true;
/** @type {null | Asset} */ /**
var PreferenceVisibilityPreviewAsset = null; * Bound to screen lifetime
* @type {Asset}
*/
var PreferenceVisibilityPreviewAsset;
var PreferenceVisibilityResetClicked = false; var PreferenceVisibilityResetClicked = false;
/** @type {Partial<Record<`${AssetGroupName}/${string}`, ItemPermissions>>} */ /** @type {Partial<Record<`${AssetGroupName}/${string}`, ItemPermissions>>} */
var PreferenceVisibilityRecord = {}; var PreferenceVisibilityRecord = {};
@ -19,7 +21,7 @@ var PreferenceVisibilityRecord = {};
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function PreferenceSubscreenVisibilityLoad() { function PreferenceSubscreenVisibilityLoad() {
ElementWrap(PreferenceIDs.exit).hidden = true; ElementWrap(PreferenceIDs.exit)?.toggleAttribute("hidden", true);
PreferenceVisibilityRecord = { ...Player.PermissionItems }; PreferenceVisibilityRecord = { ...Player.PermissionItems };
PreferenceVisibilityGroupList = []; PreferenceVisibilityGroupList = [];
const hideableGroups = AssetGroup.filter(g => AssetGroupIsHideable(g)); const hideableGroups = AssetGroup.filter(g => AssetGroupIsHideable(g));
@ -140,7 +142,7 @@ function PreferenceSubscreenVisibilityClick() {
// Reset button // Reset button
if (MouseIn(500, PreferenceVisibilityResetClicked ? 780 : 700, 300, 64)) { if (MouseIn(500, PreferenceVisibilityResetClicked ? 780 : 700, 300, 64)) {
if (PreferenceVisibilityResetClicked) { if (PreferenceVisibilityResetClicked) {
Object.values(Player.PermissionItems).forEach(i => i.Hidden = false); Object.values(Player.PermissionItems).forEach(i => { if (i) i.Hidden = false; });
PreferenceVisibilityExit(true); PreferenceVisibilityExit(true);
} }
else PreferenceVisibilityResetClicked = true; else PreferenceVisibilityResetClicked = true;
@ -150,11 +152,12 @@ function PreferenceSubscreenVisibilityClick() {
// Confirm button // Confirm button
if (MouseIn(1720, 60, 90, 90)) { if (MouseIn(1720, 60, 90, 90)) {
CommonEntries(PreferenceVisibilityRecord).forEach(([key, permission]) => { for (const [key, permission] of CommonEntries(PreferenceVisibilityRecord)) {
Player.PermissionItems[key] ??= permission; if (!permission) continue;
Player.PermissionItems[key] ??= PreferencePermissionGetDefault();
Player.PermissionItems[key].Hidden = permission.Hidden; Player.PermissionItems[key].Hidden = permission.Hidden;
Player.PermissionItems[key].Permission = permission.Permission; Player.PermissionItems[key].Permission = permission.Permission;
}); }
PreferenceVisibilityExit(true); PreferenceVisibilityExit(true);
} }

View file

@ -1,8 +1,11 @@
// @ts-strict-ignore // @ts-strict-ignore
"use strict"; "use strict";
var TitleBackground = "Sheet"; var TitleBackground = "Sheet";
/** @type {null | TitleName} */ /**
var TitleSelectedTitle = null; * Bound to screen lifetime
* @type {TitleName}
*/
var TitleSelectedTitle;
/** @type {null | NicknameStatus} */ /** @type {null | NicknameStatus} */
var TitleNicknameStatus = null; var TitleNicknameStatus = null;
/** @deprecated */ /** @deprecated */

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {{ Name: TitleName; Requirement: () => boolean; Earned?: boolean, Force?: boolean }[]} */ /** @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: "MasterKidnapper", Requirement: function () { return (ReputationGet("Kidnap") >= 100); }, Earned: true },
{ Name: "Patient", Requirement: function () { return ((ReputationGet("Asylum") <= -50) && (ReputationGet("Asylum") > -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: "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: "Nurse", Requirement: function () { return ((ReputationGet("Asylum") >= 50) && (ReputationGet("Asylum") < 100)); }, Earned: true },
{ Name: "Doctor", Requirement: function () { return (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 }, { 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"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */
@ -22,7 +21,7 @@ function AssetsBodyMarkingsBodyWritingsAfterDrawHook(data, originalFunction, {
width: Width, width: Width,
}; };
switch (CA.Property.TypeRecord.s) { switch (CA.Property?.TypeRecord?.s) {
case 0: // Print case 0: // Print
drawOptions.fontFamily = "Ananda Black"; drawOptions.fontFamily = "Ananda Black";
break; break;
@ -85,10 +84,11 @@ function AssetsBodyMarkingsBodyWritingsAfterDrawHook(data, originalFunction, {
} }
TextItem.Init(data, C, CA, false, false); 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 // We draw the desired info on that canvas
const ctx = TempCanvas.getContext('2d'); const ctx = TempCanvas.getContext('2d');
if (!ctx) return;
DynamicDrawText(text1, ctx, Width / 2, Height / 2 - 10, drawOptions); DynamicDrawText(text1, ctx, Width / 2, Height / 2 - 10, drawOptions);
DynamicDrawText(text2, ctx, Width / 2, Height / 2, drawOptions); DynamicDrawText(text2, ctx, Width / 2, Height / 2, drawOptions);
DynamicDrawText(text3, ctx, Width / 2, Height / 2 + 10, drawOptions); DynamicDrawText(text3, ctx, Width / 2, Height / 2 + 10, drawOptions);

View file

@ -1,7 +1,6 @@
// @ts-strict-ignore
"use strict"; "use strict";
const AssetsClothCheerleaderTopData = { const AssetsClothCheerleaderTopData = /** @type {const} */ ({
_Small: { _Small: {
shearFactor: 0.78, shearFactor: 0.78,
width: 100, width: 100,
@ -22,7 +21,7 @@ const AssetsClothCheerleaderTopData = {
width: 130, width: 130,
yOffset: 84, yOffset: 84,
} }
}; });
/** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */
function AssetsClothCheerleaderTopAfterDrawHook(data, originalFunction, { function AssetsClothCheerleaderTopAfterDrawHook(data, originalFunction, {
@ -54,14 +53,15 @@ function AssetsClothCheerleaderTopAfterDrawHook(data, originalFunction, {
} }
TextItem.Init(data, C, CA, false, false); 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 height = 48;
const width = sizeData.width; const width = sizeData.width;
const flatCanvas = AnimationGenerateTempCanvas(C, A, width, height); const flatCanvas = AnimationGenerateTempCanvas(C, A, width, height);
const ctx = flatCanvas.getContext("2d"); const ctx = flatCanvas.getContext("2d");
if (!ctx) return;
DynamicDrawTextArc(text, ctx, width / 2, height / 2, { DynamicDrawTextArc(text, ctx, width / 2, height / 2, {
fontSize: 48, fontSize: 48,

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */
@ -18,7 +17,7 @@ function AssetsClothAccessoryBibAfterDrawHook(data, originalFunction, {
const TempCanvas = AnimationGenerateTempCanvas(C, A, Width, Height); const TempCanvas = AnimationGenerateTempCanvas(C, A, Width, Height);
TextItem.Init(data, C, CA, false, false); 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 isAlone = !text1 || !text2;
const drawOptions = { const drawOptions = {
@ -30,6 +29,7 @@ function AssetsClothAccessoryBibAfterDrawHook(data, originalFunction, {
// We draw the desired info on that canvas // We draw the desired info on that canvas
let ctx = TempCanvas.getContext('2d'); let ctx = TempCanvas.getContext('2d');
if (!ctx) return;
DynamicDrawText(text1, ctx, Width / 2, Height / (isAlone ? 2 : 2.5), drawOptions); DynamicDrawText(text1, ctx, Width / 2, Height / (isAlone ? 2 : 2.5), drawOptions);
DynamicDrawText(text2, ctx, Width / 2, Height / (isAlone ? 2 : 1.5), drawOptions); DynamicDrawText(text2, ctx, Width / 2, Height / (isAlone ? 2 : 1.5), drawOptions);

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */
@ -23,7 +22,7 @@ function AssetsFaceMarkingsFaceWritingsAfterDrawHook(data, originalFunction, {
width: Width, width: Width,
}; };
switch (CA.Property.TypeRecord.s) { switch (CA.Property?.TypeRecord?.s) {
case 0: // Print case 0: // Print
drawOptions.fontFamily = "Ananda Black"; drawOptions.fontFamily = "Ananda Black";
break; break;
@ -60,10 +59,11 @@ function AssetsFaceMarkingsFaceWritingsAfterDrawHook(data, originalFunction, {
} }
TextItem.Init(data, C, CA, false, false); 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 // We draw the desired info on that canvas
const ctx = TempCanvas.getContext('2d'); const ctx = TempCanvas.getContext('2d');
if (!ctx) return;
DynamicDrawText(text1, ctx, Width / 2, Height / 2 - 10, drawOptions); DynamicDrawText(text1, ctx, Width / 2, Height / 2 - 10, drawOptions);
DynamicDrawText(text2, ctx, Width / 2 + offset, Height / 2, drawOptions); DynamicDrawText(text2, ctx, Width / 2 + offset, Height / 2, drawOptions);
DynamicDrawText(text3, ctx, Width / 2 + offset * 2, Height / 2 + 10, drawOptions); DynamicDrawText(text3, ctx, Width / 2 + offset * 2, Height / 2 + 10, drawOptions);

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
// How to make your item futuristic! // How to make your item futuristic!
@ -51,6 +50,7 @@ var FuturisticAccessChastityGroups = ["ItemPelvis", "ItemTorso", "ItemButt", "It
*/ */
function FuturisticAccess(data, OriginalFunction, DeniedFunction) { function FuturisticAccess(data, OriginalFunction, DeniedFunction) {
const C = CharacterGetCurrent(); const C = CharacterGetCurrent();
if (!C) return false;
if (InventoryItemFuturisticValidate(C) !== "") { if (InventoryItemFuturisticValidate(C) !== "") {
DialogExtendedMessage = AssetTextGet("FuturisticItemLoginScreen"); DialogExtendedMessage = AssetTextGet("FuturisticItemLoginScreen");
DeniedFunction(data); DeniedFunction(data);
@ -107,9 +107,7 @@ function FuturisticAccessExit() {
* @type {ExtendedItemScriptHookCallbacks.Validate<ExtendedItemData<any>, any>} * @type {ExtendedItemScriptHookCallbacks.Validate<ExtendedItemData<any>, any>}
*/ */
function FuturisticAccessValidate(Data, OriginalFunction, C, Item, Option, CurrentOption, permitExisting) { function FuturisticAccessValidate(Data, OriginalFunction, C, Item, Option, CurrentOption, permitExisting) {
let futureString = InventoryItemFuturisticValidate(C, Item, CurrentOption.ChangeWhenLocked); return InventoryItemFuturisticValidate(C, Item, CurrentOption.ChangeWhenLocked) ?? OriginalFunction?.(C, Item, Option, CurrentOption, permitExisting);
if (futureString) return futureString;
else return OriginalFunction(C, Item, Option, CurrentOption, permitExisting);
} }
// Load the futuristic item ACCESS DENIED screen // Load the futuristic item ACCESS DENIED screen
@ -155,15 +153,16 @@ function InventoryItemFuturisticClickAccessDenied(data) {
if (NoArch.Click(data)) { if (NoArch.Click(data)) {
return; return;
} }
const C = CharacterGetCurrent();
if (!C) return;
if (MouseIn(1400, 800, 200, 64)) { if (MouseIn(1400, 800, 200, 64)) {
const elem = /** @type {null | HTMLInputElement} */(document.getElementById("PasswordField")); const elem = /** @type {HTMLInputElement | null} */(document.getElementById("PasswordField"));
if (elem?.disabled ?? true) { if (!elem || (elem?.disabled ?? true)) {
return; return;
} }
const pw = elem.value.toUpperCase(); const pw = elem.value.toUpperCase();
const C = CharacterGetCurrent();
if (DialogFocusItem && DialogFocusItem.Property && DialogFocusItem.Property.LockedBy == "PasswordPadlock" && pw == DialogFocusItem.Property.Password) { if (DialogFocusItem && DialogFocusItem.Property && DialogFocusItem.Property.LockedBy == "PasswordPadlock" && pw == DialogFocusItem.Property.Password) {
CommonPadlockUnlock(C, DialogFocusItem); CommonPadlockUnlock(C, DialogFocusItem);
DialogLeaveFocusItem(); DialogLeaveFocusItem();
@ -176,7 +175,7 @@ function InventoryItemFuturisticClickAccessDenied(data) {
} else { } else {
FuturisticAccessDeniedMessage = AssetTextGet("CantChangeWhileLockedFuturistic"); FuturisticAccessDeniedMessage = AssetTextGet("CantChangeWhileLockedFuturistic");
AudioPlayInstantSound("Audio/AccessDenied.mp3"); 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. * 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 {Character} C - The character to validate the option
* @param {Item} Item - The equipped item * @param {Item | null} Item - The equipped item
* @param {boolean} changeWhenLocked - See {@link ExtendedItemOption.ChangeWhenLocked} * @param {boolean} [changeWhenLocked] - See {@link ExtendedItemOption.ChangeWhenLocked}
* @returns {string} - Returns false and sets DialogExtendedMessage, if the chosen option is not possible. * @returns {string} - Returns false and sets DialogExtendedMessage, if the chosen option is not possible.
*/ */
function InventoryItemFuturisticValidate(C, Item = DialogFocusItem, changeWhenLocked=false) { 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. * @param {Character} C - The character that got denied access.
*/ */
function InventoryItemFuturisticPublishAccessDenied(C) { function InventoryItemFuturisticPublishAccessDenied(C) {
if (!C.FocusGroup) return;
const Dictionary = new DictionaryBuilder() const Dictionary = new DictionaryBuilder()
.sourceCharacter(Player) .sourceCharacter(Player)
.destinationCharacter(C) .destinationCharacter(C)

View file

@ -1,10 +1,10 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData>} */
function InventoryItemArmsFullLatexSuitDrawHook(Data, OriginalFunction) { function InventoryItemArmsFullLatexSuitDrawHook(Data, OriginalFunction) {
OriginalFunction(); OriginalFunction();
const C = CharacterGetCurrent(); const C = CharacterGetCurrent();
if (!C) return;
const CanEquip = InventoryGet(C, "ItemVulva") == null; const CanEquip = InventoryGet(C, "ItemVulva") == null;
ExtendedItemCustomDraw( ExtendedItemCustomDraw(
`${Data.dialogPrefix.option}Wand`, `${Data.dialogPrefix.option}Wand`,
@ -17,8 +17,9 @@ function InventoryItemArmsFullLatexSuitDrawHook(Data, OriginalFunction) {
/** @type {ExtendedItemScriptHookCallbacks.Click<TypedItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Click<TypedItemData>} */
function InventoryItemArmsFullLatexSuitClickHook(Data, OriginalFunction) { function InventoryItemArmsFullLatexSuitClickHook(Data, OriginalFunction) {
OriginalFunction(); OriginalFunction();
const C = CharacterGetCurrent();
if (!C) return;
if (MouseIn(...ExtendedXY[6][4], 225, 275)) { if (MouseIn(...ExtendedXY[6][4], 225, 275)) {
const C = CharacterGetCurrent();
const VulvaItem = InventoryGet(C, "ItemVulva"); const VulvaItem = InventoryGet(C, "ItemVulva");
const Worn = (C.IsPlayer() && VulvaItem != null && VulvaItem.Asset.Name === "FullLatexSuitWand"); const Worn = (C.IsPlayer() && VulvaItem != null && VulvaItem.Asset.Name === "FullLatexSuitWand");
ExtendedItemCustomClick("Wand", () => InventoryItemArmsFullLatexSuitSetWand(Data, C), Worn); ExtendedItemCustomClick("Wand", () => InventoryItemArmsFullLatexSuitSetWand(Data, C), Worn);

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemCallbacks.BeforeDraw} */ /** @type {ExtendedItemCallbacks.BeforeDraw} */
@ -15,6 +14,5 @@ function AssetsItemArmsHempRopeBeforeDraw(data) {
Y: data.Y + 30, Y: data.Y + 30,
}; };
} }
return data;
return null;
} }

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemCallbacks.BeforeDraw} */ /** @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"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.Load<TypedItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Load<TypedItemData>} */
function InventoryItemArmsTransportJacketLoadHook(Data, OriginalFunction) { function InventoryItemArmsTransportJacketLoadHook(Data, OriginalFunction) {
if (!DialogFocusItem) return;
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT); const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
if (textData === null) { if (textData === null) {
return; return;
@ -15,6 +15,7 @@ function InventoryItemArmsTransportJacketLoadHook(Data, OriginalFunction) {
/** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData>} */
function InventoryItemArmsTransportJacketDrawHook(Data, OriginalFunction) { function InventoryItemArmsTransportJacketDrawHook(Data, OriginalFunction) {
if (!DialogFocusItem) return;
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT); const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
if (textData === null) { if (textData === null) {
return; return;
@ -41,13 +42,14 @@ function InventoryItemArmsTransportJacketPublishActionHook(data, originalFunctio
return; return;
} }
case "TypedItemOption": case "TypedItemOption":
originalFunction(C, item, newOption, previousOption); originalFunction?.(C, item, newOption, previousOption);
return; return;
} }
} }
/** @type {ExtendedItemScriptHookCallbacks.Exit<TypedItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Exit<TypedItemData>} */
function InventoryItemArmsTransportJacketExitHook(Data, OriginalFunction) { function InventoryItemArmsTransportJacketExitHook(Data, OriginalFunction) {
if (!DialogFocusItem) return;
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT); const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
if (textData !== null) { if (textData !== null) {
TextItem.Exit(textData); TextItem.Exit(textData);
@ -64,9 +66,10 @@ function AssetsItemArmsTransportJacketAfterDraw(
const height = 60; const height = 60;
const flatCanvas = AnimationGenerateTempCanvas(C, A, width, height); const flatCanvas = AnimationGenerateTempCanvas(C, A, width, height);
const flatCtx = flatCanvas.getContext("2d"); const flatCtx = flatCanvas.getContext("2d");
if (!flatCtx) return;
TextItem.Init(data, C, CA, false, false); TextItem.Init(data, C, CA, false, false);
const text = CA.Property.Text; const text = CA.Property?.Text ?? "";
DynamicDrawText(text, flatCtx, width / 2, height / 2, { DynamicDrawText(text, flatCtx, width / 2, height / 2, {
fontSize: 40, fontSize: 40,

View file

@ -1,36 +1,38 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData | ModularItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData | ModularItemData>} */
function InventoryItemBreastForbiddenChastityBraDrawHook(data, originalFunction) { function InventoryItemBreastForbiddenChastityBraDrawHook(data, originalFunction) {
originalFunction(); originalFunction();
if (!DialogFocusItem) return;
if (data.archetype === ExtendedArchetype.MODULAR && data.currentModule !== "ShockModule") { if (data.archetype === ExtendedArchetype.MODULAR && data.currentModule !== "ShockModule") {
return; return;
} }
const { TriggerCount, ShowText, PunishOrgasm, PunishStandup, PunishStruggle } = DialogFocusItem.Property ?? {};
MainCanvas.textAlign = "right"; MainCanvas.textAlign = "right";
DrawText(AssetTextGet("ShockCount"), 1500, 575, "White", "Gray"); DrawText(AssetTextGet("ShockCount"), 1500, 575, "White", "Gray");
MainCanvas.textAlign = "left"; MainCanvas.textAlign = "left";
DrawText(`${DialogFocusItem.Property.TriggerCount}`, 1510, 575, "White", "Gray"); DrawText(`${TriggerCount}`, 1510, 575, "White", "Gray");
MainCanvas.textAlign = "center"; MainCanvas.textAlign = "center";
ExtendedItemCustomDraw("ResetShockCount", 1635, 550, null, false, false); ExtendedItemCustomDraw("ResetShockCount", 1635, 550, null, false, false);
ExtendedItemCustomDraw("TriggerShock", 1635, 625, null, false, false); ExtendedItemCustomDraw("TriggerShock", 1635, 625, null, false, false);
MainCanvas.textAlign = "left"; MainCanvas.textAlign = "left";
ExtendedItemDrawCheckbox( ExtendedItemDrawCheckbox(
"ShowText", 1100, 618, DialogFocusItem.Property.ShowText, "ShowText", 1100, 618, !!ShowText,
{ text: AssetTextGet("ShowMessageInChat"), textColor: "White", changeWhenLocked: false }, { text: AssetTextGet("ShowMessageInChat"), textColor: "White", changeWhenLocked: false },
); );
ExtendedItemDrawCheckbox( ExtendedItemDrawCheckbox(
"PunishOrgasm", 1100, 700, DialogFocusItem.Property.PunishOrgasm, "PunishOrgasm", 1100, 700, !!PunishOrgasm,
{ text: AssetTextGet("ForbiddenChastityBraPunishOrgasm"), textColor: "White", changeWhenLocked: false }, { text: AssetTextGet("ForbiddenChastityBraPunishOrgasm"), textColor: "White", changeWhenLocked: false },
); );
ExtendedItemDrawCheckbox( ExtendedItemDrawCheckbox(
"PunishStandup", 1100, 770, DialogFocusItem.Property.PunishStandup, "PunishStandup", 1100, 770, !!PunishStandup,
{ text: AssetTextGet("ForbiddenChastityBraPunishStandup"), textColor: "White", changeWhenLocked: false }, { text: AssetTextGet("ForbiddenChastityBraPunishStandup"), textColor: "White", changeWhenLocked: false },
); );
ExtendedItemDrawCheckbox( ExtendedItemDrawCheckbox(
"PunishStruggle", 1100, 840, DialogFocusItem.Property.PunishStruggle, "PunishStruggle", 1100, 840, !!PunishStruggle,
{ text: AssetTextGet("ForbiddenChastityBraPunishStruggle"), textColor: "White", changeWhenLocked: false }, { text: AssetTextGet("ForbiddenChastityBraPunishStruggle"), textColor: "White", changeWhenLocked: false },
); );
MainCanvas.textAlign = "center"; MainCanvas.textAlign = "center";
@ -44,6 +46,7 @@ function InventoryItemBreastForbiddenChastityBraClickHook(data, originalFunction
} }
const C = CharacterGetCurrent(); const C = CharacterGetCurrent();
if (!C || !DialogFocusItem) return;
if (MouseIn(1635, 550, 225, 55)) { if (MouseIn(1635, 550, 225, 55)) {
ExtendedItemCustomClick("ResetShockCount", InventoryItemNeckAccessoriesCollarShockUnitResetCount, false, false); ExtendedItemCustomClick("ResetShockCount", InventoryItemNeckAccessoriesCollarShockUnitResetCount, false, false);
return; return;
@ -52,7 +55,7 @@ function InventoryItemBreastForbiddenChastityBraClickHook(data, originalFunction
return; return;
} }
if (!ExtendedItemPermissionMode) { if (!ExtendedItemPermissionMode) {
const property = DialogFocusItem.Property; const property = /** @type {ItemProperties} */ (DialogFocusItem?.Property);
if (MouseIn(1100, 618, 64, 64)) { if (MouseIn(1100, 618, 64, 64)) {
ExtendedItemCustomClickAndPush(C, DialogFocusItem, "ShowText", () => property.ShowText = !property.ShowText, false, false); ExtendedItemCustomClickAndPush(C, DialogFocusItem, "ShowText", () => property.ShowText = !property.ShowText, false, false);
} else if (MouseIn(1100, 700, 64, 64)) { } else if (MouseIn(1100, 700, 64, 64)) {
@ -74,12 +77,12 @@ function InventoryItemBreastForbiddenChastityBraClickHook(data, originalFunction
function AssetsItemBreastForbiddenChastityBraScriptDrawHook(data, originalFunction, drawData) { function AssetsItemBreastForbiddenChastityBraScriptDrawHook(data, originalFunction, drawData) {
const persistentData = drawData.PersistentData(); const persistentData = drawData.PersistentData();
/** @type {ItemProperties} */ /** @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.UpdateTime !== "number") persistentData.UpdateTime = CommonTime() + 4000;
if (typeof persistentData.LastMessageLen !== "number") persistentData.LastMessageLen = (ChatRoomLastMessage) ? ChatRoomLastMessage.length : 0; if (typeof persistentData.LastMessageLen !== "number") persistentData.LastMessageLen = (ChatRoomLastMessage) ? ChatRoomLastMessage.length : 0;
if (typeof persistentData.CheckTime !== "number") persistentData.CheckTime = CommonTime(); if (typeof persistentData.CheckTime !== "number") persistentData.CheckTime = CommonTime();
if (typeof persistentData.DisplayCount !== "number") persistentData.DisplayCount = 0; 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; if (typeof property.NextShockTime !== "number") property.NextShockTime = 0;
const canShock = typeof property.ShockLevel === "number"; const canShock = typeof property.ShockLevel === "number";
@ -88,14 +91,14 @@ function AssetsItemBreastForbiddenChastityBraScriptDrawHook(data, originalFuncti
if (lastMsgIndex >= 0 && ChatRoomChatLog[lastMsgIndex].Time > persistentData.CheckTime) if (lastMsgIndex >= 0 && ChatRoomChatLog[lastMsgIndex].Time > persistentData.CheckTime)
persistentData.UpdateTime = Math.min(persistentData.UpdateTime, CommonTime() + 200); // Trigger if the user speaks 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; const newlyTriggered = isTriggered && persistentData.DisplayCount == 0;
if (newlyTriggered) if (newlyTriggered)
persistentData.UpdateTime = Math.min(persistentData.UpdateTime, CommonTime()); persistentData.UpdateTime = Math.min(persistentData.UpdateTime, CommonTime());
if (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) { if (canShock) {
AssetsItemPelvisObedienceBeltUpdate(drawData, persistentData.CheckTime); AssetsItemPelvisObedienceBeltUpdate(drawData, persistentData.CheckTime);
} }
@ -105,7 +108,7 @@ function AssetsItemBreastForbiddenChastityBraScriptDrawHook(data, originalFuncti
// Set CheckTime to last processed chat message time // Set CheckTime to last processed chat message time
persistentData.CheckTime = (lastMsgIndex >= 0 ? ChatRoomChatLog[lastMsgIndex].Time : 0); 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; const wasBlinking = property.BlinkState;
property.BlinkState = wasBlinking && !newlyTriggered ? false : true; property.BlinkState = wasBlinking && !newlyTriggered ? false : true;
const timeFactor = isTriggered ? 12 : 1; const timeFactor = isTriggered ? 12 : 1;

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** /**
@ -42,6 +41,7 @@ function InventoryItemBreastFuturisticBraDrawHook(Data, OriginalFunction) {
const Prefix = Data.dialogPrefix.option; const Prefix = Data.dialogPrefix.option;
const C = CharacterGetCurrent(); const C = CharacterGetCurrent();
if (!C) return;
const {bpm, breathing, temp} = InventoryItemBreastFuturisticBraUpdate(C); const {bpm, breathing, temp} = InventoryItemBreastFuturisticBraUpdate(C);
DrawText(`${AssetTextGet(`${Prefix}Desc`)} ${C.MemberNumber}`, 1500, 625, "White", "Gray"); DrawText(`${AssetTextGet(`${Prefix}Desc`)} ${C.MemberNumber}`, 1500, 625, "White", "Gray");
@ -69,6 +69,7 @@ function AssetsItemBreastFuturisticBraBeforeDraw(data) {
const ShowHeart = data.PersistentData().ShowHeart; const ShowHeart = data.PersistentData().ShowHeart;
return { Opacity: ShowHeart ? 1 : 0 }; return { Opacity: ShowHeart ? 1 : 0 };
} }
return data;
} }
/** @type {ExtendedItemCallbacks.AfterDraw<FuturisticBraPersistentData>} */ /** @type {ExtendedItemCallbacks.AfterDraw<FuturisticBraPersistentData>} */
@ -92,6 +93,7 @@ function AssetsItemBreastFuturisticBraAfterDraw({
// We draw the desired info on that canvas // We draw the desired info on that canvas
let context = TempCanvas.getContext('2d'); let context = TempCanvas.getContext('2d');
if (!context) return;
context.font = "bold 14px sansserif"; context.font = "bold 14px sansserif";
context.fillStyle = "Black"; context.fillStyle = "Black";
context.textAlign = "center"; context.textAlign = "center";

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.Draw<ModularItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Draw<ModularItemData>} */
@ -6,7 +5,7 @@ function InventoryItemButtInflVibeButtPlugDrawHook(Data, OriginalFunction) {
OriginalFunction(); OriginalFunction();
if (Data.currentModule === ModularItemBase) { if (Data.currentModule === ModularItemBase) {
const [InflateLevel, Intensity] = ModularItemParseCurrent(Data, DialogFocusItem.Property.TypeRecord); const [InflateLevel, Intensity] = ModularItemParseCurrent(Data, DialogFocusItem?.Property?.TypeRecord);
// Display option information // Display option information
MainCanvas.save(); MainCanvas.save();

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.AfterDraw<TextItemData>} */
@ -11,10 +10,11 @@ function AssetsItemDevicesDollBoxAfterDrawHook(data, originalFunction,
const width = 400; const width = 400;
const tempCanvas = AnimationGenerateTempCanvas(C, A, width, height); const tempCanvas = AnimationGenerateTempCanvas(C, A, width, height);
const ctx = tempCanvas.getContext("2d"); const ctx = tempCanvas.getContext("2d");
if (!ctx) return;
// One line of text will be centered // One line of text will be centered
TextItem.Init(data, C, CA, false, false); 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 isAlone = !text1 || !text2;
const drawOptions = { const drawOptions = {

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** /**
@ -6,13 +5,14 @@
*/ */
/** @type {ExtendedItemScriptHookCallbacks.BeforeDraw<VibratingItemData, FuckMachinePersistentData>} */ /** @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(); const Data = PersistentData();
if (typeof Data.DildoState !== "number") Data.DildoState = 0; if (typeof Data.DildoState !== "number") Data.DildoState = 0;
if (typeof Data.Modifier !== "number") Data.Modifier = 1; if (typeof Data.Modifier !== "number") Data.Modifier = 1;
if (L === "Dildo") return { Y: Y + Data.DildoState }; if (L === "Dildo") return { Y: Y + Data.DildoState };
if (L !== "Pole") return; if (L !== "Pole") return drawData;
const Properties = Property || {}; const Properties = Property || {};
const Intensity = typeof Properties.Intensity === "number" ? Properties.Intensity : -1; const Intensity = typeof Properties.Intensity === "number" ? Properties.Intensity : -1;

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** /**
@ -6,13 +5,13 @@
*/ */
/** @type {ExtendedItemCallbacks.BeforeDraw<FuturisticCratePersistentData>} */ /** @type {ExtendedItemCallbacks.BeforeDraw<FuturisticCratePersistentData>} */
function AssetsItemDevicesFuturisticCrateBeforeDraw({ PersistentData, L, X, Y, Property }) { function AssetsItemDevicesFuturisticCrateBeforeDraw(drawData) {
const { PersistentData, L, Y, Property } = drawData;
const Data = PersistentData(); const Data = PersistentData();
if (typeof Data.DildoState !== "number") Data.DildoState = 0; if (typeof Data.DildoState !== "number") Data.DildoState = 0;
if (typeof Data.Modifier !== "number") Data.Modifier = 1; if (typeof Data.Modifier !== "number") Data.Modifier = 1;
//if (L === "DevicePleasureHolder") return { Y: Y + Data.DildoState }; if (L !== "DevicePleasureHolder") return drawData;
if (L !== "DevicePleasureHolder") return;
const Properties = Property || {}; const Properties = Property || {};
const Intensity = typeof Properties.Intensity === "number" ? Properties.Intensity : -1; 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>} */ /** @type {ExtendedItemScriptHookCallbacks.ScriptDraw<VibratingItemData, FuturisticCratePersistentData>} */
function AssetsItemDevicesFuturisticCrateScriptDrawHook(data, originalFunction, drawData) { function AssetsItemDevicesFuturisticCrateScriptDrawHook(data, originalFunction, drawData) {
originalFunction(drawData); originalFunction?.(drawData);
const Data = drawData.PersistentData(); const Data = drawData.PersistentData();
const Properties = drawData.Item.Property || {}; const Properties = drawData.Item.Property || {};

View file

@ -1,8 +1,8 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.Load<ModularItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Load<ModularItemData>} */
function InventoryItemDevicesKabeshiriWallLoadHook(data, originalFunction) { function InventoryItemDevicesKabeshiriWallLoadHook(data, originalFunction) {
if (!DialogFocusItem) return;
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT); const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
if (textData === null) { if (textData === null) {
return; return;
@ -14,12 +14,13 @@ function InventoryItemDevicesKabeshiriWallLoadHook(data, originalFunction) {
/** @type {ExtendedItemScriptHookCallbacks.Draw<ModularItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Draw<ModularItemData>} */
function InventoryItemDevicesKabeshiriWallDrawHook(data, originalFunction) { function InventoryItemDevicesKabeshiriWallDrawHook(data, originalFunction) {
if (!DialogFocusItem) return;
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT); const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
if (textData === null) { if (textData === null) {
return; 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) { if (data.currentModule !== ModularItemBase) {
elements.forEach(el => el.toggleAttribute("hidden", true)); elements.forEach(el => el.toggleAttribute("hidden", true));
} else { } else {
@ -41,13 +42,14 @@ function InventoryItemDevicesKabeshiriWallPublishActionHook(data, originalFuncti
return; return;
} }
case "ModularItemOption": case "ModularItemOption":
originalFunction(C, item, newOption, previousOption); originalFunction?.(C, item, newOption, previousOption);
return; return;
} }
} }
/** @type {ExtendedItemScriptHookCallbacks.Exit<ModularItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Exit<ModularItemData>} */
function InventoryItemDevicesKabeshiriWallExitHook(data, originalFunction) { function InventoryItemDevicesKabeshiriWallExitHook(data, originalFunction) {
if (!DialogFocusItem) return;
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT); const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
if (textData !== null) { if (textData !== null) {
TextItem.Exit(textData); TextItem.Exit(textData);
@ -68,10 +70,11 @@ function AssetsItemDevicesKabeshiriWallAfterDrawHook(
const width = 1000; const width = 1000;
const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height); const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height);
const ctx = tmpCanvas.getContext("2d"); const ctx = tmpCanvas.getContext("2d");
if (!ctx) return;
TextItem.Init(data, C, CA, false, false); TextItem.Init(data, C, CA, false, false);
const text1 = CA.Property.Text; const text1 = CA.Property?.Text ?? "";
const text2 = CA.Property.Text2; const text2 = CA.Property?.Text2 ?? "";
DynamicDrawTextArc(text1, ctx, 200, 490, { DynamicDrawTextArc(text1, ctx, 200, 490, {
fontSize: 20, fontSize: 20,

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** /**
@ -6,12 +5,13 @@
*/ */
/** @type {ExtendedItemCallbacks.BeforeDraw<KennelPersistentData>} */ /** @type {ExtendedItemCallbacks.BeforeDraw<KennelPersistentData>} */
function AssetsItemDevicesKennelBeforeDraw({ PersistentData, L, Property }) { function AssetsItemDevicesKennelBeforeDraw(drawData) {
if (L !== "Door") return; const { PersistentData, L, Property } = drawData;
if (L !== "Door") return drawData;
const Data = PersistentData(); const Data = PersistentData();
const Properties = Property || {}; Data.DoorState ??= 0;
const Door = Properties.Door || false; const Door = Property.Door || false;
if (Data.DoorState >= 11 || Data.DoorState <= 1) Data.MustChange = false; if (Data.DoorState >= 11 || Data.DoorState <= 1) Data.MustChange = false;
@ -21,6 +21,7 @@ function AssetsItemDevicesKennelBeforeDraw({ PersistentData, L, Property }) {
Data.DrawRequested = false; Data.DrawRequested = false;
if (Data.DoorState < 11 && Data.DoorState > 1) return { LayerType: "A" + Data.DoorState }; if (Data.DoorState < 11 && Data.DoorState > 1) return { LayerType: "A" + Data.DoorState };
} }
return drawData;
} }
/** @type {ExtendedItemCallbacks.ScriptDraw<KennelPersistentData>} */ /** @type {ExtendedItemCallbacks.ScriptDraw<KennelPersistentData>} */
@ -46,7 +47,7 @@ function AssetsItemDevicesKennelScriptDraw({ C, PersistentData, Item }) {
* @returns {string} * @returns {string}
*/ */
function InventoryItemDevicesKennelGetAudio(C) { 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(); let isSelf = C.IsPlayer();
return isSelf && wasWorn ? "CageStruggle" : "CageEquip"; return isSelf && wasWorn ? "CageStruggle" : "CageEquip";
} }

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
var ItemDevicesLuckyWheelMinTexts = 2; var ItemDevicesLuckyWheelMinTexts = 2;
var ItemDevicesLuckyWheelMaxTexts = 8; var ItemDevicesLuckyWheelMaxTexts = 8;
@ -50,8 +49,10 @@ function InventoryItemDevicesLuckyWheelInitHook(data, originalFunction, characte
/** @type {ExtendedItemScriptHookCallbacks.Load<NoArchItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Load<NoArchItemData>} */
function InventoryItemDevicesLuckyWheelg0LoadHook(data, originalFunction) { function InventoryItemDevicesLuckyWheelg0LoadHook(data, originalFunction) {
originalFunction(); originalFunction();
for (let num = 0; num < DialogFocusItem.Property.Texts.length; num++) { if (!DialogFocusItem) return;
const input = ElementCreateInput(`LuckyWheelText${num}`, "input", DialogFocusItem.Property.Texts[num] || "", ItemDevicesLuckyWheelMaxTextLength); const texts = (DialogFocusItem.Property?.Texts ?? []);
for (let num = 0; num < texts.length; num++) {
const input = ElementCreateInput(`LuckyWheelText${num}`, "input", texts[num], ItemDevicesLuckyWheelMaxTextLength);
if (input) { if (input) {
input.pattern = DynamicDrawTextInputPattern; input.pattern = DynamicDrawTextInputPattern;
input.addEventListener("change", InventoryItemDevicesLuckyWheelUpdate); input.addEventListener("change", InventoryItemDevicesLuckyWheelUpdate);
@ -69,19 +70,21 @@ var ItemDevicesLuckyWheelRowLength = 350;
function InventoryItemDevicesLuckyWheelg0DrawHook(data, originalFunction) { function InventoryItemDevicesLuckyWheelg0DrawHook(data, originalFunction) {
originalFunction(); originalFunction();
if (!DialogFocusItem) return;
// Section labels & remove buttons grid // Section labels & remove buttons grid
let top = ItemDevicesLuckyWheelRowTop; let top = ItemDevicesLuckyWheelRowTop;
let left = ItemDevicesLuckyWheelRowLeft; 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 topRow = (num % (ItemDevicesLuckyWheelMaxTexts / 2) * ItemDevicesLuckyWheelRowHeight);
let leftCol = Math.floor(num / (ItemDevicesLuckyWheelMaxTexts / 2)) * ItemDevicesLuckyWheelRowLength; let leftCol = Math.floor(num / (ItemDevicesLuckyWheelMaxTexts / 2)) * ItemDevicesLuckyWheelRowLength;
ElementPosition(`LuckyWheelText${num}`, left + leftCol, top + topRow, 300); 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); 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); 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>} */ /** @type {ExtendedItemScriptHookCallbacks.Click<NoArchItemData>} */
function InventoryItemDevicesLuckyWheelg0ClickHook(data, originalFunction) { function InventoryItemDevicesLuckyWheelg0ClickHook(data, originalFunction) {
originalFunction(); originalFunction();
if (!DialogFocusItem) return;
const texts = (DialogFocusItem?.Property?.Texts ?? []);
if (MouseIn(1360, 720, 120, 48)) { 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 label = ItemDevicesLuckyWheelLabelForNum(last + 1);
const input = ElementCreateInput(`LuckyWheelText${last}`, "input", label, ItemDevicesLuckyWheelMaxTextLength); const input = ElementCreateInput(`LuckyWheelText${last}`, "input", label, ItemDevicesLuckyWheelMaxTextLength);
if (input) { if (input) {
input.pattern = DynamicDrawTextInputPattern; input.pattern = DynamicDrawTextInputPattern;
input.addEventListener("change", InventoryItemDevicesLuckyWheelUpdate); input.addEventListener("change", InventoryItemDevicesLuckyWheelUpdate);
} }
DialogFocusItem.Property.Texts.push(label); DialogFocusItem.Property?.Texts?.push(label);
InventoryItemDevicesLuckyWheelUpdate(); InventoryItemDevicesLuckyWheelUpdate();
return; return;
} }
if (MouseIn(1530, 720, 120, 48)) { 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; const num = texts.length - 1;
DialogFocusItem.Property.Texts.splice(num, 1); DialogFocusItem?.Property?.Texts?.splice(num, 1);
ElementRemove(`LuckyWheelText${num}`); ElementRemove(`LuckyWheelText${num}`);
InventoryItemDevicesLuckyWheelUpdate(); InventoryItemDevicesLuckyWheelUpdate();
return; return;
@ -117,20 +122,24 @@ function InventoryItemDevicesLuckyWheelg0ClickHook(data, originalFunction) {
/** @type {ExtendedItemScriptHookCallbacks.Exit<NoArchItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Exit<NoArchItemData>} */
function InventoryItemDevicesLuckyWheelg0ExitHook(data, originalFunction) { 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++) { for (let num = 0; num < ItemDevicesLuckyWheelMaxTexts; num++) {
if (num < DialogFocusItem.Property.Texts.length) { if (num < texts.length) {
const text = ElementValue(`LuckyWheelText${num}`); const text = ElementValue(`LuckyWheelText${num}`);
if (text != DialogFocusItem.Property.Texts[num]) { if (text != texts[num]) {
DialogFocusItem.Property.Texts[num] = text; texts[num] = text;
} }
} }
ElementRemove(`LuckyWheelText${num}`); ElementRemove(`LuckyWheelText${num}`);
} }
const C = CharacterGetCurrent();
ChatRoomCharacterItemUpdate(C); ChatRoomCharacterItemUpdate(C);
CharacterRefresh(C, true, false); CharacterRefresh(C, true, false);
@ -139,15 +148,20 @@ function InventoryItemDevicesLuckyWheelg0ExitHook(data, originalFunction) {
} }
function InventoryItemDevicesLuckyWheelUpdate() { function InventoryItemDevicesLuckyWheelUpdate() {
CharacterRefresh(CharacterGetCurrent(), false); const C = CharacterGetCurrent();
if (!C) return;
CharacterRefresh(C, false);
} }
function InventoryItemDevicesLuckyWheelTrigger() { function InventoryItemDevicesLuckyWheelTrigger() {
const randomAngle = Math.round(Math.random() * 360);
DialogFocusItem.Property.TargetAngle = randomAngle;
ChatRoomCharacterItemUpdate(CharacterGetCurrent());
const C = 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() const Dictionary = new DictionaryBuilder()
.sourceCharacter(Player) .sourceCharacter(Player)
.destinationCharacter(C) .destinationCharacter(C)
@ -163,7 +177,7 @@ function InventoryItemDevicesLuckyWheelTrigger() {
function InventoryItemDevicesLuckyWheelStoppedTurning(C, Item, Angle) { function InventoryItemDevicesLuckyWheelStoppedTurning(C, Item, Angle) {
if (!C.IsPlayer() || Item.Asset.Name !== "LuckyWheel") return; 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)); storedTexts = storedTexts.map(T => T.substring(0, ItemDevicesLuckyWheelMaxTextLength));
const nbTexts = Math.max(Math.min(ItemDevicesLuckyWheelMaxTextLength, storedTexts.length), ItemDevicesLuckyWheelMinTexts); const nbTexts = Math.max(Math.min(ItemDevicesLuckyWheelMaxTextLength, storedTexts.length), ItemDevicesLuckyWheelMinTexts);
const sectorAngleSize = 360 / nbTexts; const sectorAngleSize = 360 / nbTexts;
@ -223,7 +237,7 @@ function AssetsItemDevicesLuckyWheelScriptDraw({ C, PersistentData, Item }) {
} }
/** @type {ExtendedItemCallbacks.AfterDraw<LuckyWheelPersistentData>} */ /** @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 height = 500;
const width = 500; const width = 500;
@ -234,8 +248,11 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
if (!Data.Spinning) if (!Data.Spinning)
return; return;
Data.LightStep ??= 0;
const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height); const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height);
const ctx = tmpCanvas.getContext("2d"); const ctx = tmpCanvas.getContext("2d");
if (!ctx) return;
if (C.IsInverted()) { if (C.IsInverted()) {
ctx.rotate(Math.PI); ctx.rotate(Math.PI);
@ -243,7 +260,7 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
Y -= 500; Y -= 500;
} }
if (Data.AnimationSpeed < 2 * ItemDevicesLuckyWheelAnimationMinSpeed) { if ((Data.AnimationSpeed ?? 1) < 2 * ItemDevicesLuckyWheelAnimationMinSpeed) {
// Start blinking // Start blinking
Data.LightStep = (++Data.LightStep) % 2; Data.LightStep = (++Data.LightStep) % 2;
@ -266,9 +283,8 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
if (L === "Text") { if (L === "Text") {
const Data = PersistentData(); const Data = PersistentData();
const CurrentAngle = Data.AnimationAngleState; const CurrentAngle = Data.AnimationAngleState ?? 0;
const Properties = Property || {}; const Properties = Property || {};
const Item = InventoryGet(C, A.Group.Name);
DynamicDrawLoadFont(ItemDevicesLuckyWheelFont); DynamicDrawLoadFont(ItemDevicesLuckyWheelFont);
@ -279,9 +295,11 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
// Draw // Draw
const diameter = height / 2; const diameter = height / 2;
/** @type {(degrees: number) => number} */
const degreeToRadians = (degrees) => degrees * Math.PI / 180; const degreeToRadians = (degrees) => degrees * Math.PI / 180;
const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height); const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height);
const ctx = tmpCanvas.getContext("2d"); const ctx = tmpCanvas.getContext("2d");
if (!ctx) return;
if (C.IsInverted()) { if (C.IsInverted()) {
ctx.rotate(Math.PI); ctx.rotate(Math.PI);
@ -296,6 +314,7 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
ctx.rotate(degreeToRadians(CurrentAngle)); ctx.rotate(degreeToRadians(CurrentAngle));
ctx.translate(-diameter, -diameter); ctx.translate(-diameter, -diameter);
/** @type {Record<number, number>} */
const SectionsPerNumTexts = { const SectionsPerNumTexts = {
2: 2, 2: 2,
3: 3, 3: 3,
@ -308,12 +327,12 @@ function AssetsItemDevicesLuckyWheelAfterDraw({ C, PersistentData, A, X, Y, L, P
/** @type {readonly BCColor[]} */ /** @type {readonly BCColor[]} */
let itemColors; let itemColors;
if (typeof Item.Color === "string") { if (typeof CA.Color === "string") {
itemColors = Array(Item.Asset.ColorableLayerCount).fill(Item.Color); itemColors = Array(CA.Asset.ColorableLayerCount).fill(CA.Color);
} else if (CommonIsArray(Item.Color)) { } else if (CommonIsArray(CA.Color)) {
itemColors = Item.Color; itemColors = CA.Color;
} else { } else {
itemColors = Item.Asset.DefaultColor; itemColors = CA.Asset.DefaultColor;
} }
// Draw the background // Draw the background

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** /**
@ -13,13 +12,14 @@ function AssetsItemDevicesPetBowlAfterDrawHook(data, originalFunction,
// Fetch the text property and assert that it matches the character // Fetch the text property and assert that it matches the character
// and length requirements // and length requirements
TextItem.Init(data, C, CA, false, false); 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 // Prepare a temporary canvas to draw the text to
const height = 60; const height = 60;
const width = 130; const width = 130;
const tempCanvas = AnimationGenerateTempCanvas(C, A, width, height); const tempCanvas = AnimationGenerateTempCanvas(C, A, width, height);
const ctx = tempCanvas.getContext("2d"); const ctx = tempCanvas.getContext("2d");
if (!ctx) return;
// Reposition and orient the text when hanging upside-down // Reposition and orient the text when hanging upside-down
if (C.IsInverted()) { if (C.IsInverted()) {

View file

@ -1,8 +1,8 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.Load<TypedItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Load<TypedItemData>} */
function InventoryItemDevicesWoodenBoxLoadHook(Data, OriginalFunction) { function InventoryItemDevicesWoodenBoxLoadHook(Data, OriginalFunction) {
if (!DialogFocusItem) return;
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT); const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
if (textData === null) { if (textData === null) {
return; return;
@ -14,6 +14,7 @@ function InventoryItemDevicesWoodenBoxLoadHook(Data, OriginalFunction) {
/** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Draw<TypedItemData>} */
function InventoryItemDevicesWoodenBoxDrawHook(Data, OriginalFunction) { function InventoryItemDevicesWoodenBoxDrawHook(Data, OriginalFunction) {
if (!DialogFocusItem) return;
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT); const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
if (textData === null) { if (textData === null) {
return; return;
@ -25,6 +26,7 @@ function InventoryItemDevicesWoodenBoxDrawHook(Data, OriginalFunction) {
/** @type {ExtendedItemScriptHookCallbacks.Exit<TypedItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Exit<TypedItemData>} */
function InventoryItemDevicesWoodenBoxExitHook(data, originalFunction) { function InventoryItemDevicesWoodenBoxExitHook(data, originalFunction) {
if (!DialogFocusItem) return;
const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT); const textData = ExtendedItemGetData(DialogFocusItem.Asset, ExtendedArchetype.TEXT);
if (textData === null) { if (textData === null) {
return; return;
@ -34,7 +36,7 @@ function InventoryItemDevicesWoodenBoxExitHook(data, originalFunction) {
PropertyOpacityExit(data, originalFunction, false); PropertyOpacityExit(data, originalFunction, false);
// Apply extra opacity-specific effects // Apply extra opacity-specific effects
const Property = DialogFocusItem.Property; const Property = DialogFocusItem.Property ??= {};
const Transparent = CommonIsNumeric(Property.Opacity) ? Property.Opacity < 0.15 : false; const Transparent = CommonIsNumeric(Property.Opacity) ? Property.Opacity < 0.15 : false;
if (Transparent) { if (Transparent) {
delete Property.Effect; delete Property.Effect;
@ -43,6 +45,7 @@ function InventoryItemDevicesWoodenBoxExitHook(data, originalFunction) {
} }
const C = CharacterGetCurrent(); const C = CharacterGetCurrent();
if (!C) return;
CharacterRefresh(C, true, false); CharacterRefresh(C, true, false);
ChatRoomCharacterItemUpdate(C, DialogFocusItem.Asset.Group.Name); ChatRoomCharacterItemUpdate(C, DialogFocusItem.Asset.Group.Name);
} }
@ -51,7 +54,7 @@ function InventoryItemDevicesWoodenBoxExitHook(data, originalFunction) {
function InventoryItemDevicesWoodenBoxPublishActionHook(data, originalFunction, C, item, newOption, previousOption) { function InventoryItemDevicesWoodenBoxPublishActionHook(data, originalFunction, C, item, newOption, previousOption) {
switch (newOption.OptionType) { switch (newOption.OptionType) {
case "TypedItemOption": case "TypedItemOption":
originalFunction(C, item, newOption, previousOption); originalFunction?.(C, item, newOption, previousOption);
return; return;
case "TextItemOption": { case "TextItemOption": {
const textData = ExtendedItemGetData(item.Asset, ExtendedArchetype.TEXT); const textData = ExtendedItemGetData(item.Asset, ExtendedArchetype.TEXT);
@ -79,9 +82,10 @@ function AssetsItemDevicesWoodenBoxAfterDrawHook(
const width = 310; const width = 310;
const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height); const tmpCanvas = AnimationGenerateTempCanvas(C, A, width, height);
const ctx = tmpCanvas.getContext("2d"); const ctx = tmpCanvas.getContext("2d");
if (!ctx) return;
TextItem.Init(data, C, CA, false, false); TextItem.Init(data, C, CA, false, false);
const text = CA.Property.Text; const text = CA.Property?.Text ?? "";
let from; let from;
let to; let to;

View file

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

View file

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

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.SetOption<ModularItemData, ModularItemOption>} */ /** @type {ExtendedItemScriptHookCallbacks.SetOption<ModularItemData, ModularItemOption>} */
@ -13,13 +12,13 @@ function InventoryItemHandheldPlushiesSetOptionHook(
refresh, refresh,
) { ) {
// Toggle the new option within the active module as per usual // 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 // Set the options within all other modules to 0
const currentModuleName = newOption.ModuleName; const currentModuleName = newOption.ModuleName;
const currentOptionIndices = ModularItemParseCurrent( const currentOptionIndices = ModularItemParseCurrent(
data, data,
item.Property.TypeRecord, item.Property?.TypeRecord ?? null,
); );
for (const [ for (const [
otherModuleIndex, otherModuleIndex,
@ -31,7 +30,7 @@ function InventoryItemHandheldPlushiesSetOptionHook(
const otherOldOption = const otherOldOption =
data.modules[otherModuleIndex].Options[otherOptionIndex]; data.modules[otherModuleIndex].Options[otherOptionIndex];
const otherNewOption = data.modules[otherModuleIndex].Options[0]; 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); CharacterRefresh(C, push, false);

View file

@ -1,9 +1,8 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.Validate<ModularItemData, ModularItemOption>} */ /** @type {ExtendedItemScriptHookCallbacks.Validate<ModularItemData, ModularItemOption>} */
function ItemHeadDroneMaskValidateHook(data, originalFunction, C, item, newOption, previousOption, permitExisting) { 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()) { if (C.IsSimple()) {
return ret; return ret;
} }
@ -35,9 +34,11 @@ function AssetsItemHeadDroneMaskAfterDrawHook(data, originalFunction, {
let XOffset = 67; let XOffset = 67;
let YOffset = 89; let YOffset = 89;
const TempCanvas = AnimationGenerateTempCanvas(C, A, Width, Height); const TempCanvas = AnimationGenerateTempCanvas(C, A, Width, Height);
let ctx = TempCanvas.getContext('2d');
if (!ctx) return;
TextItem.Init(data, C, CA, false, false); TextItem.Init(data, C, CA, false, false);
const text = CA.Property.Text; const text = CA.Property?.Text ?? "";
const isAlone = !text; const isAlone = !text;
const drawOptions = { const drawOptions = {
@ -48,7 +49,6 @@ function AssetsItemHeadDroneMaskAfterDrawHook(data, originalFunction, {
}; };
// Draw the text onto the canvas // Draw the text onto the canvas
let ctx = TempCanvas.getContext('2d');
DynamicDrawText(text, ctx, Width/2, Height/ (isAlone? 2: 2.5), drawOptions); DynamicDrawText(text, ctx, Width/2, Height/ (isAlone? 2: 2.5), drawOptions);
//And print the canvas onto the character based on the above positions //And print the canvas onto the character based on the above positions

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** /**
@ -10,16 +9,18 @@ function AssetsItemHoodCanvasHoodAfterDrawHook(data, originalFunction,
{ C, A, CA, X, Y, L, drawCanvas, drawCanvasBlink, AlphaMasks, Color }, { C, A, CA, X, Y, L, drawCanvas, drawCanvasBlink, AlphaMasks, Color },
) { ) {
if (L === "Text") { 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 // Prepare a temporary canvas to draw the text to
const height = 50; const height = 50;
const width = 120; const width = 120;
const tempCanvas = AnimationGenerateTempCanvas(C, A, width, height); const tempCanvas = AnimationGenerateTempCanvas(C, A, width, height);
const ctx = tempCanvas.getContext("2d"); 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, { DynamicDrawTextArc(text, ctx, width / 2, height / 2, {
fontSize: 36, fontSize: 36,

View file

@ -1,6 +1,6 @@
// @ts-strict-ignore
"use strict"; "use strict";
let CombinationPadlockPlayerIsBlind = false; let CombinationPadlockPlayerIsBlind = false;
/** @type {number | null} */
let CombinationPadlockBlindCombinationOffset = null; let CombinationPadlockBlindCombinationOffset = null;
let CombinationPadlockCombinationLastValue = ""; let CombinationPadlockCombinationLastValue = "";
let CombinationPadlockNewCombinationLastValue = ""; let CombinationPadlockNewCombinationLastValue = "";
@ -10,6 +10,9 @@ let CombinationPadlockLoaded = false;
function InventoryItemMiscCombinationPadlockLoadHook(data, originalFunction) { function InventoryItemMiscCombinationPadlockLoadHook(data, originalFunction) {
originalFunction(); originalFunction();
const C = CharacterGetCurrent();
if (!C || !C.FocusGroup) return;
CombinationPadlockPlayerIsBlind = Player.IsBlind(); CombinationPadlockPlayerIsBlind = Player.IsBlind();
// Only update on initial load, not update loads // Only update on initial load, not update loads
if (!CombinationPadlockLoaded) { if (!CombinationPadlockLoaded) {
@ -24,7 +27,6 @@ function InventoryItemMiscCombinationPadlockLoadHook(data, originalFunction) {
CombinationPadlockBlindCombinationOffset = null; CombinationPadlockBlindCombinationOffset = null;
} }
var C = CharacterGetCurrent();
// Only create the inputs if the zone isn't blocked // Only create the inputs if the zone isn't blocked
if (!InventoryGroupIsBlocked(C, C.FocusGroup.Name)) { if (!InventoryGroupIsBlocked(C, C.FocusGroup.Name)) {
@ -37,11 +39,11 @@ function InventoryItemMiscCombinationPadlockLoadHook(data, originalFunction) {
combinationInput.addEventListener("input", InventoryItemMiscCombinationPadlockModifyInput); combinationInput.addEventListener("input", InventoryItemMiscCombinationPadlockModifyInput);
// the current code is shown for owners, lovers and the member whose number is on the padlock // the current code is shown for owners, lovers and the member whose number is on the padlock
if ( if (
Player.MemberNumber === DialogFocusSourceItem.Property.LockMemberNumber || Player.MemberNumber === DialogFocusSourceItem?.Property?.LockMemberNumber ||
C.IsOwnedByPlayer() || C.IsOwnedByPlayer() ||
C.IsLoverOfPlayer() C.IsLoverOfPlayer()
) { ) {
combinationInput.setAttribute("placeholder", DialogFocusSourceItem.Property.CombinationNumber); combinationInput.setAttribute("placeholder", DialogFocusSourceItem?.Property?.CombinationNumber);
} }
} else { } else {
/** @type {HTMLInputElement} */(document.getElementById('CombinationNumber')).type = CombinationPadlockPlayerIsBlind ? "password" : "text"; /** @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) { function InventoryItemMiscCombinationPadlockModifyInput(e) {
const target = /** @type {HTMLInputElement} */ (e.target);
const clumsiness = Player.GetClumsiness(); const clumsiness = Player.GetClumsiness();
// If the player is either blind or impaired by restraints, modify the input accordingly // If the player is either blind or impaired by restraints, modify the input accordingly
if (CombinationPadlockPlayerIsBlind || clumsiness > 0) { if (CombinationPadlockPlayerIsBlind || clumsiness > 0) {
const previousValue = CombinationPadlockCombinationLastValue; const previousValue = CombinationPadlockCombinationLastValue;
const newValue = e.target.value; const newValue = target.value;
let prefix = ""; let prefix = "";
let suffix = ""; let suffix = "";
for (let i = 0; i < previousValue.length && previousValue[i] === newValue[i]; i++) { 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); 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>} */ /** @type {ExtendedItemScriptHookCallbacks.Draw<NoArchItemData>} */
function InventoryItemMiscCombinationPadlockDrawHook(data, originalFunction) { function InventoryItemMiscCombinationPadlockDrawHook(data, originalFunction) {
originalFunction(); originalFunction();
var C = CharacterGetCurrent(); const C = CharacterGetCurrent();
if (!C || !C.FocusGroup || !DialogFocusItem) return;
const playerBlind = Player.IsBlind(); const playerBlind = Player.IsBlind();
if (playerBlind !== CombinationPadlockPlayerIsBlind) { if (playerBlind !== CombinationPadlockPlayerIsBlind) {
InventoryItemMiscCombinationPadlockDrawHook(data, originalFunction); InventoryItemMiscCombinationPadlockDrawHook(data, originalFunction);
@ -149,14 +154,15 @@ function InventoryItemMiscCombinationPadlockClickHook(data, originalFunction) {
return; return;
} }
var C = CharacterGetCurrent(); const C = CharacterGetCurrent();
if (!C || !C.FocusGroup) return;
// If the zone is blocked, cannot interact with the lock // If the zone is blocked, cannot interact with the lock
if (InventoryGroupIsBlocked(C, C.FocusGroup.Name)) return; if (InventoryGroupIsBlocked(C, C.FocusGroup.Name)) return;
// Opens the padlock // Opens the padlock
if (MouseIn(1600, 771, 350, 64)) { if (MouseIn(1600, 771, 350, 64)) {
if (ElementValue("CombinationNumber") == DialogFocusSourceItem.Property.CombinationNumber) { if (ElementValue("CombinationNumber") == DialogFocusSourceItem?.Property?.CombinationNumber) {
CommonPadlockUnlock(C, DialogFocusSourceItem); CommonPadlockUnlock(C, DialogFocusSourceItem);
DialogLeaveFocusItem(); DialogLeaveFocusItem();
} }
@ -175,7 +181,7 @@ function InventoryItemMiscCombinationPadlockClickHook(data, originalFunction) {
// Changes the code // Changes the code
else if (MouseIn(1600, 871, 350, 64)) { else if (MouseIn(1600, 871, 350, 64)) {
// Succeeds to change // Succeeds to change
if (ElementValue("CombinationNumber") == DialogFocusSourceItem.Property.CombinationNumber) { if (ElementValue("CombinationNumber") == DialogFocusSourceItem?.Property?.CombinationNumber) {
var NewCode = ElementValue("NewCombinationNumber"); var NewCode = ElementValue("NewCombinationNumber");
// We only accept code made of digits and of 4 numbers // We only accept code made of digits and of 4 numbers
if (ValidationCombinationNumberRegex.test(NewCode)) { if (ValidationCombinationNumberRegex.test(NewCode)) {

View file

@ -1,13 +1,15 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {ExtendedItemScriptHookCallbacks.Draw<NoArchItemData>} */ /** @type {ExtendedItemScriptHookCallbacks.Draw<NoArchItemData>} */
function InventoryItemMiscExclusivePadlockDrawHook(data, originalFunction) { function InventoryItemMiscExclusivePadlockDrawHook(data, originalFunction) {
originalFunction(); originalFunction();
const C = CharacterGetCurrent();
if (!C || !DialogFocusItem) return;
DrawText(AssetTextGet(DialogFocusItem.Asset.Group.Name + DialogFocusItem.Asset.Name + "Intro"), 1500, 600, "white", "gray"); 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"); 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); msg = CommonStringSubstitute(msg, subst);
DrawText(msg, 1500, 700, "white", "gray"); DrawText(msg, 1500, 700, "white", "gray");

View file

@ -144,6 +144,7 @@ let KDDefaultKB = {
}; };
let KinkyDungeonRootDirectory = "Screens/MiniGame/KinkyDungeon/"; let KinkyDungeonRootDirectory = "Screens/MiniGame/KinkyDungeon/";
/** @type {Character} */
let KinkyDungeonPlayerCharacter = null; // Other player object let KinkyDungeonPlayerCharacter = null; // Other player object
let KinkyDungeonGameData = null; // Data sent by other player let KinkyDungeonGameData = null; // Data sent by other player
let KinkyDungeonGameDataNullTimer = 4000; // If data is null, we query this often 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 {Character} C - Character on which the action is done.
* @param {string} Action - Action modifier * @param {string} Action - Action modifier
* @param {Item | null} PrevItem - The item that has been removed. * @param {Item | null | undefined} PrevItem - The item that has been removed.
* @param {Item | null} NextItem - The item that has been added. * @param {Item | null | undefined} NextItem - The item that has been added.
* @returns {boolean} - whether we published anything to the chat. * @returns {boolean} - whether we published anything to the chat.
*/ */
function ChatRoomPublishAction(C, Action, PrevItem, NextItem) { function ChatRoomPublishAction(C, Action, PrevItem, NextItem) {

View file

@ -2332,7 +2332,6 @@ function ChatRoomMapViewClick() {
Y = 10 + 70 * (count % 13); Y = 10 + 70 * (count % 13);
X = 10 + 70 * Math.floor(count / 13); X = 10 + 70 * Math.floor(count / 13);
if (MouseIn(X, Y, 60, 60)) { if (MouseIn(X, Y, 60, 60)) {
// @ts-ignore
if ((Obj.AssetName == null) || (Obj.AssetGroup == null) || InventoryAvailable(Player, Obj.AssetName, Obj.AssetGroup)) if ((Obj.AssetName == null) || (Obj.AssetGroup == null) || InventoryAvailable(Player, Obj.AssetName, Obj.AssetGroup))
ChatRoomMapViewEditObject = CommonCloneDeep(Obj); ChatRoomMapViewEditObject = CommonCloneDeep(Obj);
return; return;

View file

@ -576,7 +576,6 @@ async function ChatSearchLoad() {
ElementCreateSettingsLabel(TextGet("Lobby")), ElementCreateSettingsLabel(TextGet("Lobby")),
ElementCreateRadioButtonGroup( ElementCreateRadioButtonGroup(
"chat-search-search-menu-room-lobby-radio-group", "chat-search-search-menu-room-lobby-radio-group",
// @ts-ignore
(ev, key) => { (ev, key) => {
Player.ChatSearchSettings.Space = ChatSearchSpace = key; Player.ChatSearchSettings.Space = ChatSearchSpace = key;
ChatSearchUpdateSearchSettings(); ChatSearchUpdateSearchSettings();

View file

@ -573,7 +573,7 @@ function MainHallOpenChangelog() {
function MainHallMaidReleasePlayer() { function MainHallMaidReleasePlayer() {
if (MainHallMaid.CanInteract()) { if (MainHallMaid.CanInteract()) {
for (let D = 0; D < MainHallMaid.Dialog.length; D++) 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"); MainHallMaid.Dialog[D].Result = DialogFind(MainHallMaid, "AlreadyReleased");
CharacterRelease(Player); CharacterRelease(Player);
for (let L = 0; L < MainHallStrongLocks.length; L++) for (let L = 0; L < MainHallStrongLocks.length; L++)
@ -591,7 +591,7 @@ function MainHallMaidReleasePlayer() {
function MainHallMaidAngry() { function MainHallMaidAngry() {
if ((ReputationGet("Dominant") < 30) && !MainHallIsHeadMaid) { if ((ReputationGet("Dominant") < 30) && !MainHallIsHeadMaid) {
for (let D = 0; D < MainHallMaid.Dialog.length; D++) 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"); MainHallMaid.Dialog[D].Result = DialogFind(MainHallMaid, "LearnedLesson");
ReputationProgress("Dominant", 1); ReputationProgress("Dominant", 1);
InventoryWearRandom(Player, "ItemMouth"); InventoryWearRandom(Player, "ItemMouth");

View file

@ -1,6 +1,6 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {null | string[][]} */ /** @type {string[][]} */
// @ts-ignore Strict-TS: Lying here because that's loaded by Login
var ActivityDictionary = null; var ActivityDictionary = null;
var ActivityOrgasmGameButtonX = 0; var ActivityOrgasmGameButtonX = 0;
var ActivityOrgasmGameButtonY = 0; var ActivityOrgasmGameButtonY = 0;
@ -11,13 +11,14 @@ var ActivityOrgasmGameTimer = 0;
var ActivityOrgasmResistLabel = ""; var ActivityOrgasmResistLabel = "";
var ActivityOrgasmRuined = false; // If set to true, the orgasm will be ruined right before it happens 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 ActivityTranslateResolve = undefined;
let ActivityDebug = false; let ActivityDebug = false;
/** /**
* Debug logging function for activities * Debug logging function for activities
* @param {any[]} args
*/ */
function ActivityLog(...args) { function ActivityLog(...args) {
if (ActivityDebug) { if (ActivityDebug) {
@ -137,8 +138,9 @@ function ActivityPossibleOnGroup(C, GroupName) {
if (!CharacterNotEnclosedOrSelfActivity || !ActivityAllowed() || !CharacterHasArousalEnabled(C)) if (!CharacterNotEnclosedOrSelfActivity || !ActivityAllowed() || !CharacterHasArousalEnabled(C))
return false; return false;
const Group = ActivityGetGroupOrMirror(C.AssetFamily, GroupName); const Group = ActivityGetGroupOrMirror(C.AssetFamily, GroupName);
if (!Group) return false;
const Zone = PreferenceGetArousalZone(C, Group.Name); 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) => { return items.reduce((activities, item) => {
const typeList = CommonIsObject(item.Property?.TypeRecord) ? PropertyTypeRecordToStrings(item.Property.TypeRecord) : [null]; const typeList = CommonIsObject(item.Property?.TypeRecord) ? PropertyTypeRecordToStrings(item.Property.TypeRecord) : [null];
/** @type {ItemActivityRestriction} */ /** @type {ItemActivityRestriction | undefined} */
let blocked = null; let blocked = undefined;
if (typeList.some((type) => InventoryIsAllowedLimited(acted, item, type))) { if (typeList.some((type) => InventoryIsAllowedLimited(acted, item, type))) {
blocked = "limited"; blocked = "limited";
} else if (typeList.some((type) => InventoryBlockedOrLimited(acted, item, type))) { } else if (typeList.some((type) => InventoryBlockedOrLimited(acted, item, type))) {
@ -313,7 +315,7 @@ function ActivityGenerateItemActivitiesFromNeed(acting, acted, needsItem, activi
return activities; return activities;
} }
return activities; return activities;
}, []); }, /** @type {ItemActivity[]} */ ([]));
} }
/** /**
@ -341,7 +343,6 @@ function ActivityAllowedForGroup(character, groupname) {
const targetedItem = InventoryGet(character, groupname); const targetedItem = InventoryGet(character, groupname);
/** @type {ItemActivity[]} */
let allowed = activities.reduce((allowedActivities, activity) => { let allowed = activities.reduce((allowedActivities, activity) => {
// Validate that this activity can be done // Validate that this activity can be done
if (!ActivityHasValidTarget(character, activity, group)) { if (!ActivityHasValidTarget(character, activity, group)) {
@ -377,7 +378,7 @@ function ActivityAllowedForGroup(character, groupname) {
return [...allowedActivities, ...ActivityGenerateItemActivitiesFromNeed(Player, character, targetNeedsItemActivity, activity, group, true)]; 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")); let remote = Player.Appearance.find(a => InventoryItemHasEffect(a, "TriggerShock"));
if (remote) { if (remote) {
ActivityLog(`${Player.Name} on ${character.Name}, act: ${activity.Name}: can trigger shock, adding in`); 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`); ActivityLog(`${Player.Name} on ${character.Name}, act: ${activity.Name}: not handled by item stuff, adding in`);
return allowedActivities.concat({ Activity: activity, Group: group.Name }); return allowedActivities.concat({ Activity: activity, Group: group.Name });
}, []); }, /** @type {ItemActivity[]} */ ([]));
ActivityLog(`${Player.Name} on ${character.Name}: allowed activities for group ${groupname} lookup complete`, allowed); 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 {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. * @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' * 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 * @return {void} - Nothing
*/ */
function ActivityEffect(S, C, A, Z, Count, Asset) { function ActivityEffect(S, C, A, Z, Count, Asset) {
// Converts from activity name to the activity object // Converts from activity name to the activity object
if (typeof A === "string") A = AssetGetActivity(C.AssetFamily, A); const act = typeof A === "string" ? AssetGetActivity(C.AssetFamily, A) : A;
if ((A == null) || (typeof A === "string")) return; if (!act) return;
if ((Count == null) || (Count == undefined) || (Count == 0)) Count = 1; Count = CommonClamp(Count ?? 1, 1, Infinity);
// Calculates the next progress factor // 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 + (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 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 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 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 // 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)) if (Array.isArray(expression))
InventoryExpressionTriggerApply(C, 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 * @return {void} - Nothing
*/ */
function ActivityChatRoomArousalSync(C) { function ActivityChatRoomArousalSync(C) {
if (C.IsPlayer() && ServerPlayerIsInChatRoom()) if (!C.IsPlayer() && !ServerPlayerIsInChatRoom()) return;
ServerSend("ChatRoomCharacterArousalUpdate", { OrgasmTimer: C.ArousalSettings.OrgasmTimer, Progress: C.ArousalSettings.Progress, ProgressTimer: C.ArousalSettings.ProgressTimer, OrgasmCount: C.ArousalSettings.OrgasmCount }); 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 {null | Activity} Activity - The activity for which the timer is for
* @param {AssetGroupItemName | "ActivityOnOther"} Zone - The target zone of the activity * @param {AssetGroupItemName | "ActivityOnOther"} Zone - The target zone of the activity
* @param {number} Progress - Progress to set * @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 * @return {void} - Nothing
*/ */
function ActivitySetArousalTimer(C, Activity, Zone, Progress, Asset) { 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; if (Max > 95 && Zone !== "ActivityOnOther" && !PreferenceGetZoneOrgasm(C, Zone)) Max = 95;
// For activities on other, it cannot go over 2/3 // For activities on other, it cannot go over 2/3
if (Max > 67 && Zone === "ActivityOnOther") { 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 // 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; Max = PreferenceGetZoneOrgasm(Player, "ItemVulva") ? 100 : 95;
} else { } 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 {Character} Target - The character on which the activity was performed
* @param {Activity} Activity - The activity performed * @param {Activity} Activity - The activity performed
* @param {AssetGroup} Group - The group on which the activity is 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 * @returns {void} - Nothing
*/ */
function ActivityRunSelf(Source, Target, Activity, Group, Asset) { function ActivityRunSelf(Source, Target, Activity, Group, Asset) {
@ -875,7 +876,8 @@ function ActivityRunSelf(Source, Target, Activity, Group, Asset) {
* @param {Activity} activity * @param {Activity} activity
*/ */
function ActivityBuildChatTag(character, group, activity, is_label = false) { 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; const realGroup = character.HasPenis() && groupMap[group.Name] ? groupMap[group.Name] : group.Name;
return `${is_label ? "Label-" : ""}${(character.IsPlayer() ? "ChatSelf" : "ChatOther")}-${realGroup}-${activity.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; const UsedAsset = ItemActivity && ItemActivity.Item ? ItemActivity.Item.Asset : null;
let group = ActivityGetGroupOrMirror(acted.AssetFamily, targetGroup.Name); 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 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.ArousalSettings.Active == "Hybrid") || (acted.ArousalSettings.Active == "Automatic"))
if (acted.IsPlayer() || acted.IsNpc()) if (acted.IsPlayer() || acted.IsNpc())
@ -943,13 +946,13 @@ function ActivityRun(actor, acted, targetGroup, ItemActivity, sendMessage=true)
* @return {void} - Nothing * @return {void} - Nothing
*/ */
function ActivityArousalItem(Source, Target, Asset) { function ActivityArousalItem(Source, Target, Asset) {
var AssetActivity = Asset.DynamicActivity(Source); const AssetActivity = Asset.DynamicActivity(Source);
if (AssetActivity != null) { if (!AssetActivity) return;
var Activity = AssetGetActivity(Target.AssetFamily, AssetActivity); const Activity = AssetGetActivity(Target.AssetFamily, AssetActivity);
if (Source.IsPlayer() && !Target.IsPlayer()) ActivityRunSelf(Source, Target, Activity, Asset.Group); if (!Activity) return;
if (PreferenceArousalAtLeast(Target, "Hybrid") && (Target.IsPlayer() || Target.IsNpc())) if (Source.IsPlayer() && !Target.IsPlayer()) ActivityRunSelf(Source, Target, Activity, Asset.Group);
ActivityEffect(Source, Target, AssetActivity, /** @type {AssetGroupItemName} */ (Asset.Group.Name)); 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) { for (const item of C.Appearance) {
const fetish = [ const fetish = [
...InventoryGetItemProperty(item, "Fetish"), ...(InventoryGetItemProperty(item, "Fetish") ?? []),
...(item.Asset.Fetish || []), ...(item.Asset.Fetish ?? []),
]; ];
if (fetish.includes(Type)) { if (fetish.includes(Type)) {
return Factor; return Factor;

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
var AfkTimerTimout = 5 * 60 * 1000; // 5 minutes var AfkTimerTimout = 5 * 60 * 1000; // 5 minutes
@ -61,12 +60,12 @@ function AfkTimerSetEnabled(Enabled) {
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function AfkTimerSetIsAfk() { function AfkTimerSetIsAfk() {
if (CurrentScreen != "ChatRoom") return; if (!ServerPlayerIsInChatRoom()) return;
if (AfkTimerIsSet) return; if (AfkTimerIsSet) return;
if (AfkTimerLastEvent === 0 || AfkTimerLastEvent + AfkTimerTimout > CommonTime()) return; if (AfkTimerLastEvent === 0 || AfkTimerLastEvent + AfkTimerTimout > CommonTime()) return;
// save the current Emoticon, if there is any // save the current Emoticon, if there is any
if (InventoryGet(Player, "Emoticon") && InventoryGet(Player, "Emoticon").Property && AfkTimerOldEmoticon == null) { if (InventoryGet(Player, "Emoticon") && InventoryGet(Player, "Emoticon")?.Property && AfkTimerOldEmoticon == null) {
AfkTimerOldEmoticon = /** @type {ExpressionNameMap["Emoticon"]} */(InventoryGet(Player, "Emoticon").Property.Expression); AfkTimerOldEmoticon = /** @type {ExpressionNameMap["Emoticon"]} */(InventoryGet(Player, "Emoticon")?.Property?.Expression);
} }
CharacterSetFacialExpression(Player, "Emoticon", "Afk"); CharacterSetFacialExpression(Player, "Emoticon", "Afk");
AfkTimerIsSet = true; AfkTimerIsSet = true;

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
var AudioDialog = new Audio(); var AudioDialog = new Audio();
@ -588,11 +587,11 @@ function AudioPlaySoundForChatMessage(data, sender, msg, metadata) {
if (!data || !sender || !metadata || !["Activity", "Action", "ServerMessage"].includes(data.Type)) if (!data || !sender || !metadata || !["Activity", "Action", "ServerMessage"].includes(data.Type))
return false; return false;
if (AudioShouldSilenceSound(ChatRoomMessageInvolvesPlayer(data))) return; if (AudioShouldSilenceSound(ChatRoomMessageInvolvesPlayer(data))) return false;
// Instant actions can trigger a sound depending on the action. // Instant actions can trigger a sound depending on the action.
let Action = AudioActions.find(CA => CA.IsAction && CA.IsAction(data)); let Action = AudioActions.find(CA => CA.IsAction && CA.IsAction(data));
/** @type AudioSoundEffect */ /** @type {AudioSoundEffect | null} */
let soundEffect = null; let soundEffect = null;
if (Action) { if (Action) {
let snd = Action.GetSoundEffect(data, metadata); let snd = Action.GetSoundEffect(data, metadata);
@ -616,7 +615,7 @@ function AudioPlaySoundForChatMessage(data, sender, msg, metadata) {
/** /**
* Low-level function to play a sound effect. * Low-level function to play a sound effect.
* @param {AudioSoundEffect|string} soundEffect * @param {AudioSoundEffect|string|null} soundEffect
* @param {number} [volumeModifier] * @param {number} [volumeModifier]
* @returns {boolean} if a sound was played or not. * @returns {boolean} if a sound was played or not.
*/ */
@ -657,7 +656,7 @@ function AudioPlaySoundEffect(soundEffect, volumeModifier) {
* @returns {boolean} Whether a sound was played. * @returns {boolean} Whether a sound was played.
*/ */
function AudioPlaySoundForAsset(character, asset) { function AudioPlaySoundForAsset(character, asset) {
if (AudioShouldSilenceSound()) return; if (AudioShouldSilenceSound()) return false;
let sound = AudioGetSoundFromAsset(character, asset.Group.Name, asset.Name); let sound = AudioGetSoundFromAsset(character, asset.Group.Name, asset.Name);
return AudioPlaySoundEffect(sound, 0); return AudioPlaySoundEffect(sound, 0);
@ -669,7 +668,7 @@ function AudioPlaySoundForAsset(character, asset) {
* @param {Character} character * @param {Character} character
* @param {AssetGroupName} groupName * @param {AssetGroupName} groupName
* @param {string} assetName * @param {string} assetName
* @returns {AudioSoundEffect?} * @returns {AudioSoundEffect | null}
*/ */
function AudioGetSoundFromAsset(character, groupName, assetName) { function AudioGetSoundFromAsset(character, groupName, assetName) {
let asset = AssetGet(character.AssetFamily, groupName, assetName); let asset = AssetGet(character.AssetFamily, groupName, assetName);
@ -680,7 +679,7 @@ function AudioGetSoundFromAsset(character, groupName, assetName) {
sound = asset.DynamicAudio(character); 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 * Processes which sound should be played for items
* @param {ServerChatRoomMessage} data - Data content triggering the potential sound * @param {ServerChatRoomMessage} data - Data content triggering the potential sound
* @param {IChatRoomMessageMetadata} metadata - The chat message metadata * @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) { function AudioGetSoundFromChatMessage(data, metadata) {
const sender = metadata.SourceCharacter; const sender = metadata.SourceCharacter;
@ -710,34 +709,35 @@ function AudioGetSoundFromChatMessage(data, metadata) {
if (data.Type === "Activity" && metadata.ActivityAsset) { if (data.Type === "Activity" && metadata.ActivityAsset) {
let item = InventoryGet(sender, metadata.ActivityAsset.Group.Name); 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 // 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); 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]; const soundEffect = item.Asset.ActivityAudio[idx];
if (!soundEffect) return; if (!soundEffect) return null;
return [soundEffect, 0]; return [soundEffect, 0];
} else if (data.Type === "Action") { } else if (data.Type === "Action") {
const NextAsset = metadata.Assets && metadata.Assets.NextAsset; const NextAsset = metadata.Assets && metadata.Assets.NextAsset;
if (!NextAsset) return; if (!NextAsset) return null;
return AudioGetSoundFromAsset(sender, NextAsset.Group.Name, NextAsset.Name); return AudioGetSoundFromAsset(sender, NextAsset.Group.Name, NextAsset.Name);
} }
return null;
} }
/** /**
* Processes the sound for vibrators * Processes the sound for vibrators
* @param {ServerChatRoomMessage} data - Represents the chat message received * @param {ServerChatRoomMessage} data - Represents the chat message received
* @param {IChatRoomMessageMetadata} metadata - The metadata from the recieved message * @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) { function AudioVibratorSounds(data, metadata) {
var Sound = ""; var Sound = "";

View file

@ -1,6 +1,5 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type Character[] */ /** @type {Character[]} */
var Character = []; var Character = [];
var CharacterNextId = 0; var CharacterNextId = 0;
@ -164,6 +163,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
HeightRatio: 1, HeightRatio: 1,
HasHiddenItems: false, HasHiddenItems: false,
SavedColors: GetDefaultSavedColors(), SavedColors: GetDefaultSavedColors(),
// @ts-ignore Strict-TS: not sure why this is null here
ActiveExpression: null, ActiveExpression: null,
PoseMapping: {}, PoseMapping: {},
@ -294,7 +294,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
return this._BlindLevel; return this._BlindLevel;
}, },
GetBlurLevel: function() { GetBlurLevel: function() {
if ((this.IsPlayer() && this.GraphicsSettings && !this.GraphicsSettings.AllowBlur) || CommonPhotoMode) { if ((this.IsPlayer() && !this.GraphicsSettings.AllowBlur) || CommonPhotoMode) {
return 0; return 0;
} }
let blurLevel = 0; let blurLevel = 0;
@ -343,7 +343,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
}, },
GetSlowLevel: function () { GetSlowLevel: function () {
// Respect immunity setting for the player // Respect immunity setting for the player
if (this.IsPlayer() && /** @type {PlayerCharacter} */(this).RestrictionSettings.SlowImmunity) if (this.IsPlayer() && this.RestrictionSettings.SlowImmunity)
return 0; return 0;
let slowness = 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.Owner && this.Owner.trim().startsWith("NPC-")) return "npc";
if (this.IsPlayer()) { if (this.IsPlayer()) {
// NPC-owner while in trial // 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 (trialing && trialing !== this) return "npc";
} }
if (AsylumGGTSGetLevel(this) >= 6) return "ggts"; if (AsylumGGTSGetLevel(this) >= 6) return "ggts";
@ -415,7 +416,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
return false; return false;
} }
case "online": case "online":
return this.Ownership.MemberNumber === C.MemberNumber; return this.Ownership?.MemberNumber === C.MemberNumber;
case "player": case "player":
return true; return true;
default: default:
@ -428,8 +429,8 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
case "npc": case "npc":
return !!PrivateCharacter.find(c => NPCEventGet(c, "PlayerCollaring") > 0); return !!PrivateCharacter.find(c => NPCEventGet(c, "PlayerCollaring") > 0);
case "player": case "player":
return (NPCEventGet(this, "NPCCollaring") > 0); return (NPCEventGet(/** @type {NPCCharacter} */(this), "NPCCollaring") > 0);
case "online": return this.Ownership.Stage >= 1; case "online": return (this.Ownership?.Stage ?? 0) >= 1;
default: default:
return false; return false;
} }
@ -451,7 +452,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
const privateOwner = PrivateCharacter.find(c => NPCEventGet(c, "EndSubTrial") > 0); const privateOwner = PrivateCharacter.find(c => NPCEventGet(c, "EndSubTrial") > 0);
return privateOwner?.Name ?? name ?? ""; 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); case "player": return CharacterNickname(Player);
default: default:
return ""; return "";
@ -459,7 +460,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
}, },
OwnerNumber: function () { OwnerNumber: function () {
if (this.IsOwned() === "online") if (this.IsOwned() === "online")
return this.Ownership.MemberNumber; return this.Ownership?.MemberNumber ?? -1; // this.IsOwned() makes it impossible
return -1; return -1;
}, },
HasOwnerNotes: function () { HasOwnerNotes: function () {
@ -471,11 +472,12 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
OwnedSince: function () { OwnedSince: function () {
switch (this.IsOwned()) { switch (this.IsOwned()) {
case "online": 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": { case "player": {
let Time = NPCEventGet(this, "NPCCollaring"); let Time = NPCEventGet(/** @type {NPCCharacter} */(this), "NPCCollaring");
if (Time > 0) return Math.floor((CurrentTime - Time) / 86400000); if (Time > 0) return Math.floor((CurrentTime - Time) / 86400000);
Time = NPCEventGet(this, "EndDomTrial"); Time = NPCEventGet(/** @type {NPCCharacter} */(this), "EndDomTrial");
if (Time > 0) { if (Time > 0) {
if (Time > CurrentTime) if (Time > CurrentTime)
return Math.ceil((Time - CurrentTime) / 86400000); return Math.ceil((Time - CurrentTime) / 86400000);
@ -504,7 +506,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
OwnedSinceMs: function () { OwnedSinceMs: function () {
switch (this.IsOwned()) { switch (this.IsOwned()) {
case "online": case "online":
return this.Ownership.Start; return this.Ownership?.Start ?? 0;
case "player": { case "player": {
let Time = NPCEventGet(this, "NPCCollaring"); let Time = NPCEventGet(this, "NPCCollaring");
if (Time > 0) return Time; if (Time > 0) return Time;
@ -536,12 +538,12 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
const loves = this.GetLovership(); const loves = this.GetLovership();
if (C.IsNpc()) { if (C.IsNpc()) {
const Love = loves.find(l => !l.MemberNumber && l.Name === C.Name); const Love = loves.find(l => !l.MemberNumber && l.Name === C.Name);
if (Love == null) return false; if (!Love) return false;
return Love.Start > 0; return (Love.Start ?? 0) > 0;
} }
return ( 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)) 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 () { IsPlayer: function () {
return this.Type === CharacterType.PLAYER; return this.Type === CharacterType.PLAYER;
}, },
get X() { return this.Position?.X;}, get X() { return this.Position?.X ?? -1; },
get Y() { return this.Position?.Y;}, get Y() { return this.Position?.Y ?? -1; },
set X(value) { set X(value) {
this.Position = { X: value, Y: this.Y }; this.Position = { X: value, Y: this.Y };
}, },
@ -661,11 +663,15 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
this.Position = { X: this.X, Y: value }; this.Position = { X: this.X, Y: value };
}, },
get Position() { get Position() {
if (this?.MapData?.Pos == undefined) return null; if (!this.MapData?.Pos) return null;
if (this?.MapData?.Pos?.X === null || this?.MapData?.Pos?.Y === null) 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}; 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) return;
if (!this.MapData.Pos) return; if (!this.MapData.Pos) return;
this.MapData.Pos = ChatRoomMapViewValidatePosition({X, Y}); this.MapData.Pos = ChatRoomMapViewValidatePosition({X, Y});
@ -673,9 +679,9 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
ChatRoomMapViewUpdatePlayerFlag(); ChatRoomMapViewUpdatePlayerFlag();
}, },
IsBirthday: function () { IsBirthday: function () {
if ((this.Creation === null) || (CurrentTime === null)) return false; if (!this.Creation) return false;
const creation = new Date(this.Creation), const creation = new Date(this.Creation);
current = new Date(CurrentTime); const current = new Date(CurrentTime);
return (creation.getUTCDate() === current.getUTCDate()) && return (creation.getUTCDate() === current.getUTCDate()) &&
(creation.getUTCMonth() === current.getUTCMonth()) && (creation.getUTCMonth() === current.getUTCMonth()) &&
@ -749,7 +755,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
return this.Attribute.includes(attribute); return this.Attribute.includes(attribute);
}, },
GetGenders: function () { 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 () { GetPronouns: function () {
const pronounItem = InventoryGet(this, "Pronouns"); 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` // 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"]} */({}), { const activeExpression = Object.defineProperties(/** @type {Character["ActiveExpression"]} */({}), {
setWithoutReload: { setWithoutReload: {
/**
* @param {string} key
* @param {any} value
*/
value: function (key, value) { this[key] = value; }, value: function (key, value) { this[key] = value; },
enumerable: false, enumerable: false,
}, },
deleteWithoutReload: { deleteWithoutReload: {
/**
* @param {string} key
*/
value: function (key) { delete this[key]; }, value: function (key) { delete this[key]; },
enumerable: false, enumerable: false,
}, },
@ -874,6 +887,7 @@ function CharacterCreate(CharacterAssetFamily, Type, CharacterID) {
function CharacterGenerateRandomName() { function CharacterGenerateRandomName() {
// Get the list of all currently known names // Get the list of all currently known names
/** @type {string[]} */
const CurrentNames = []; const CurrentNames = [];
CurrentNames.push(...Character.map(c => c.Name)); CurrentNames.push(...Character.map(c => c.Name));
CurrentNames.push(...PrivateCharacter.map(c => c.Name)); CurrentNames.push(...PrivateCharacter.map(c => c.Name));
@ -917,6 +931,10 @@ function CharacterBuildDialog(C, CSV, functionPrefix, reload=true) {
C.Dialog = []; C.Dialog = [];
/**
* @param {string} fieldContents
* @returns {string | null}
*/
function parseField(fieldContents) { function parseField(fieldContents) {
if (typeof fieldContents !== "string") return null; if (typeof fieldContents !== "string") return null;
const str = fieldContents; const str = fieldContents;
@ -931,7 +949,7 @@ function CharacterBuildDialog(C, CSV, functionPrefix, reload=true) {
// Creates a dialog object // Creates a dialog object
/** @type {DialogLine} */ /** @type {DialogLine} */
const D = { const D = {
Stage: parseField(L[0]), Stage: parseField(L[0]) ?? "",
NextStage: parseField(L[1]), NextStage: parseField(L[1]),
Option: parseField(L[2]), Option: parseField(L[2]),
Result: parseField(L[3]), 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 // 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 // @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; 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. * 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 {Character} C - Character for which to build the dialog objects
* @param {DialogInfo} [info] * @param {DialogInfo<any>} [info]
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function CharacterLoadCSVDialog(C, info) { function CharacterLoadCSVDialog(C, info) {
/** @type {DialogInfo<any>} */
let dialog;
if (!info && !C.DialogInfo) { if (!info && !C.DialogInfo) {
console.error(`cannot refresh dialog for character ${C.ID}`); console.error(`cannot refresh dialog for character ${C.ID}`);
return; return;
} else if (info) { } else if (info) {
C.DialogInfo = info; dialog = C.DialogInfo = info;
} else { } else {
// Just refresh the info we have // 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() { function buildDialog() {
CharacterBuildDialog(C, CommonCSVCache[FullPath], C.DialogInfo.screen); CharacterBuildDialog(C, CommonCSVCache[FullPath], dialog.screen);
// Translate the dialog if needed and perform substitutions // Translate the dialog if needed and perform substitutions
TranslationLoadDialog(C, () => { TranslationLoadDialog(C, () => {
@ -1029,9 +1050,8 @@ function CharacterArchetypeClothes(C, Archetype, ForceColor) {
if (Outfit == 0) { if (Outfit == 0) {
InventoryWear(C, "MaidOutfit2", "Cloth"); InventoryWear(C, "MaidOutfit2", "Cloth");
InventoryWear(C, "MaidHairband1", "Hat"); InventoryWear(C, "MaidHairband1", "Hat");
} else if (Outfit == 1) { } else if (Math.random() > 0.75) {
InventoryWear(C, "MaidLatex", "Cloth"); InventoryWear(C, "MaidLatex", "Cloth", ['#202020', '#B0B0B0', 'Default']);
InventoryGet(C, "Cloth").Color = ['#202020', '#B0B0B0', 'Default'];
InventoryWear(C, "MaidLatexHairband", "Hat"); InventoryWear(C, "MaidLatexHairband", "Hat");
} else if (Outfit == 2) { } else if (Outfit == 2) {
InventoryWear(C, "MaidDress3", "Cloth"); InventoryWear(C, "MaidDress3", "Cloth");
@ -1117,25 +1137,27 @@ function CharacterArchetypeClothes(C, Archetype, ForceColor) {
// Rope bunny archetype // Rope bunny archetype
if (Archetype == "Bunny") { if (Archetype == "Bunny") {
CharacterNaked(C); 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, "BunnyCollarCuffs", "ClothAccessory");
InventoryWear(C, CommonRandomItemFromList(null, ["BunnyEars1", "BunnyEars2"]), "HairAccessory1"); InventoryWear(C, CommonRandomItemFromList("", ["BunnyEars1", "BunnyEars2"]), "HairAccessory1");
InventoryWear(C, "BunnyTailStrap", "TailStraps"); InventoryWear(C, "BunnyTailStrap", "TailStraps");
if (Math.random() > 0.5) InventoryWear(C, "Pantyhose1", "Socks"); 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 // Succubus archetype
if (Archetype == "Succubus") { if (Archetype == "Succubus") {
CharacterNaked(C); CharacterNaked(C);
let Color = CommonRandomItemFromList(null, /** @type {const} */(["Default", "#222222", "#BBBBBB", "#882222"])); let Color = CommonRandomItemFromList(null, /** @type {const} */(["Default", "#222222", "#BBBBBB", "#882222"]));
InventoryWear(C, CommonRandomItemFromList(null, ["BondageDress1", "BondageDress2", "CorsetDress", "EveningGown", "Dress3"]), "Cloth", Color); InventoryWear(C, CommonRandomItemFromList("", ["BondageDress1", "BondageDress2", "CorsetDress", "EveningGown", "Dress3"]), "Cloth", Color);
InventoryWear(C, CommonRandomItemFromList(null, ["CatEye", "CatEye2", "LargeSolid", "SuperstarBlurred", "UndershadowedSolid"]), "EyeShadow", Color); InventoryWear(C, CommonRandomItemFromList("", ["CatEye", "CatEye2", "LargeSolid", "SuperstarBlurred", "UndershadowedSolid"]), "EyeShadow", Color);
if (Math.random() > 0.5) InventoryWear(C, CommonRandomItemFromList(null, ["GradientPantyhose", "Socks5", "Stockings1", "Stockings2"]), "Socks", Color); if (Math.random() > 0.5) {
InventoryWear(C, "SuccubusHorns", "HairAccessory1", Color); InventoryWear(C, CommonRandomItemFromList("", ["GradientPantyhose", "Socks5", "Stockings1", "Stockings2"]), "Socks", Color);
InventoryWear(C, CommonRandomItemFromList(null, ["SuccubusTailStrap", "SuccubusHeartTailStrap"]), "TailStraps", Color); InventoryWear(C, "SuccubusHorns", "HairAccessory1", Color);
InventoryWear(C, CommonRandomItemFromList(null, ["BatWings", "DevilWings", "SuccubusWings"]), "Wings", Color); }
InventoryWear(C, CommonRandomItemFromList(null, ["AnkleStrapShoes", "StilettoHeels", "Shoes5", "CustomHeels", "ThighBoots"]), "Shoes", 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. * 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 * @template {ModuleType} T
* @param {string} CharacterID - The unique identifier for the NPC * @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 | T} module
* @param {null | ModuleScreens[T]} screen * @param {null | ModuleScreens[T]} screen
* @returns {NPCCharacter} - The randomly generated NPC * @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 // Checks if the NPC already exists and returns it if it's the case
const duplicate = Character.find(c => c.CharacterID === CharacterID); const duplicate = Character.find(c => c.CharacterID === CharacterID);
if (duplicate) return duplicate; if (duplicate) return /** @type {NPCCharacter} */ (duplicate);
// Randomize the new character // Randomize the new character
const C = CharacterCreate("Female3DCG", CharacterType.NPC, CharacterID); const C = /** @type {NPCCharacter} */ (CharacterCreate("Female3DCG", CharacterType.NPC, CharacterID));
C.AccountName = NPCType; C.AccountName = NPCType;
CharacterLoadCSVDialog(C, { module: module ?? CurrentModule, screen: screen ?? CurrentScreen, name: NPCType }); CharacterLoadCSVDialog(C, { module: module ?? CurrentModule, screen: screen ?? CurrentScreen, name: NPCType });
C.Name = CharacterGenerateRandomName(); C.Name = CharacterGenerateRandomName();
@ -1226,8 +1248,8 @@ function CharacterOnlineRefresh(Char, data, SourceMemberNumber) {
const oldPronouns = Char.GetPronouns(); const oldPronouns = Char.GetPronouns();
const currentAppearance = Char.Appearance; const currentAppearance = Char.Appearance;
LoginPerformAppearanceFixups(data.Appearance); LoginPerformAppearanceFixups(data.Appearance ?? []);
ServerAppearanceLoadFromBundle(Char, "Female3DCG", data.Appearance, SourceMemberNumber); ServerAppearanceLoadFromBundle(Char, "Female3DCG", data.Appearance ?? [], SourceMemberNumber);
CharacterAppearanceResolveSync(Char, currentAppearance); CharacterAppearanceResolveSync(Char, currentAppearance);
if (Char.IsPlayer()) LoginValidCollar(); if (Char.IsPlayer()) LoginValidCollar();
@ -1257,13 +1279,13 @@ function CharacterOnlineRefresh(Char, data, SourceMemberNumber) {
function CharacterLoadOnline(data, SourceMemberNumber) { function CharacterLoadOnline(data, SourceMemberNumber) {
// Check if the character already exists to reuse it // 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); 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 // 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 // whether to refresh or not; our currently in-memory character has it decoded, so we have to decode
// this as well. // this as well.
data.Description = ServerAccountDataSyncedValidate.Description(data.Description, Char); data.Description = ServerAccountDataSyncedValidate.Description(data.Description, Player);
if (Array.isArray(data.WhiteList)) { if (Array.isArray(data.WhiteList)) {
data.WhiteList.sort((a, b) => a - b); data.WhiteList.sort((a, b) => a - b);
@ -1296,30 +1318,24 @@ function CharacterLoadOnline(data, SourceMemberNumber) {
} else { } else {
// If we must add a character, we refresh it // If we must add a character, we refresh it
var Refresh = true; let Refresh = !ChatRoomData?.Character.some(c => c.ID.toString() === data.ID.toString());
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;
}
// Flags "refresh" if we need to redraw the character // Flags "refresh" if we need to redraw the character
if (!Refresh) 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; Refresh = true;
else else
for (let C = 0; C < ChatRoomData.Character.length; C++) for (let C = 0; C < ChatRoomData.Character.length; C++)
if (ChatRoomData.Character[C].ID == data.ID) 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; Refresh = true;
else else
for (let A = 0; A < data.Appearance.length && !Refresh; A++) { for (let A = 0; A < (data.Appearance?.length ?? 0) && !Refresh; A++) {
const Old = ChatRoomData.Character[C].Appearance[A]; const Old = ChatRoomData?.Character[C]?.Appearance?.[A];
const New = data.Appearance[A]; const New = data.Appearance?.[A];
if ((New.Name != Old.Name) || (New.Group != Old.Group) || (New.Color != Old.Color)) Refresh = true; 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) && (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; 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 // 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(); const attributes = new Set();
C.Attribute = []; C.Attribute = [];
for (const item of C.Appearance) { for (const item of C.Appearance) {
const itemAttrs = InventoryGetItemProperty(item, "Attribute"); const itemAttrs = InventoryGetItemProperty(item, "Attribute") ?? [];
for (const attribute of itemAttrs) { for (const attribute of itemAttrs) {
attributes.add(attribute); attributes.add(attribute);
} }
@ -1434,15 +1450,17 @@ function CharacterLoadAttributes(C) {
/** /**
* Returns a list of effects for a character from some or all groups * Returns a list of effects for a character from some or all groups
* @param {Character} C - The character to check * @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 * @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 * @returns {EffectName[]} - A list of effects
*/ */
function CharacterGetEffects(C, Groups = null, AllowDuplicates = false) { function CharacterGetEffects(C, Groups = undefined, AllowDuplicates = false) {
/** @type {EffectName[]} */
let totalEffects = []; let totalEffects = [];
C.Appearance C.Appearance
.filter(A => !Array.isArray(Groups) || Groups.length == 0 || Groups.includes(A.Asset.Group.Name)) .filter(A => !Array.isArray(Groups) || Groups.length == 0 || Groups.includes(A.Asset.Group.Name))
.forEach(item => { .forEach(item => {
/** @type {EffectName[]} */
let itemEffects = []; let itemEffects = [];
if (item.Property && Array.isArray(item.Property.Effect)) { if (item.Property && Array.isArray(item.Property.Effect)) {
CommonArrayConcatDedupe(itemEffects, item.Property.Effect); CommonArrayConcatDedupe(itemEffects, item.Property.Effect);
@ -1472,7 +1490,7 @@ function CharacterLoadTints(C) {
/** @type {ResolvedTintDefinition[]} */ /** @type {ResolvedTintDefinition[]} */
const tints = []; const tints = [];
for (const item of C.Appearance) { 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; C.Tints = tints;
} }
@ -1569,7 +1587,7 @@ function CharacterRefresh(C, Push = true, RefreshDialog = true) {
C.RunScripts = ( C.RunScripts = (
!C.IsOnline() !C.IsOnline()
|| C.IsPlayer() || C.IsPlayer()
|| !(Player.OnlineSettings && Player.OnlineSettings.DisableAnimations) || !Player.OnlineSettings.DisableAnimations
) && ( ) && (
!C.IsGhosted() !C.IsGhosted()
); );
@ -1578,7 +1596,7 @@ function CharacterRefresh(C, Push = true, RefreshDialog = true) {
if (C.IsPlayer()) { if (C.IsPlayer()) {
// Grab the first custom background that we can find // Grab the first custom background that we can find
const customBGItem = C.Appearance.find(item => item.Property?.CustomBlindBackground); 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) { 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` // 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 // 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 = Object.assign(
ItemColorItem, ItemColorItem,
{ Color: ItemColorSanitizeColor(ItemColorItem), Property: ItemColorSanitizeProperty(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 // Replace the focus items from underneath us so we get the updated data
if (wasLock) { [DialogFocusItem, DialogFocusSourceItem] = wasLock ? [lock, focusItem] : [focusItem, null];
DialogFocusItem = lock;
DialogFocusSourceItem = focusItem;
} else {
DialogFocusItem = focusItem;
}
// Reset the cached extended item requirement checks // Reset the cached extended item requirement checks
if (DialogFocusItem.Asset.Extended) { if (/** @type {Item} */ (DialogFocusItem).Asset.Extended) {
ExtendedItemRequirementCheckMessageMemo.clearCache(); ExtendedItemRequirementCheckMessageMemo.clearCache();
} }
} else if (DialogMenuMode === "colorItem") { } 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) { if (itemRemovedOrDifferent) {
ItemColorCancelAndExit(); ItemColorCancelAndExit();
DialogChangeMode("items"); DialogChangeMode("items");
@ -1773,7 +1786,7 @@ function CharacterRandomUnderwear(C) {
var Color = ""; var Color = "";
for (const G of AssetGroup) for (const G of AssetGroup)
if ((G.Category == "Appearance") && G.Underwear && (G.IsDefault || (Math.random() < 0.2))) { 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 const Group = G.Asset
.filter(A => InventoryAvailable(C, A.Name, G.Name)); .filter(A => InventoryAvailable(C, A.Name, G.Name));
if (Group.length > 0) if (Group.length > 0)
@ -1837,7 +1850,7 @@ function CharacterRelease(C, Refresh) {
*/ */
function CharacterReleaseFromLock(C, LockName) { function CharacterReleaseFromLock(C, LockName) {
for (let A = 0; A < C.Appearance.length; A++) 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]); InventoryUnlock(C, C.Appearance[A]);
} }
@ -1848,7 +1861,7 @@ function CharacterReleaseFromLock(C, LockName) {
*/ */
function CharacterReleaseNoLock(C) { function CharacterReleaseNoLock(C) {
for (let E = C.Appearance.length - 1; E >= 0; E--) 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); C.Appearance.splice(E, 1);
} }
CharacterRefresh(C); CharacterRefresh(C);
@ -1865,7 +1878,8 @@ function CharacterReleaseTotal(C, refresh=true) {
if (C.Appearance[E].Asset.Group.Category != "Appearance") { if (C.Appearance[E].Asset.Group.Category != "Appearance") {
if (C.IsOwned() && C.Appearance[E].Asset.Name == "SlaveCollar") { 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) // 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); C.Appearance[E].Property = CommonCloneDeep(InventoryItemNeckSlaveCollarTypes[0].Property);
} }
} }
@ -1911,12 +1925,12 @@ function CharacterFullRandomRestrain(C, Ratio, Refresh) {
} }
// Apply each item if needed // Apply each item if needed
if (InventoryGet(C, "ItemArms") == null) InventoryWearRandom(C, "ItemArms", null, false); if (!InventoryGet(C, "ItemArms")) InventoryWearRandom(C, "ItemArms", undefined, false);
if ((Math.random() >= RatioRare) && (InventoryGet(C, "ItemHead") == null)) InventoryWearRandom(C, "ItemHead", null, false); if ((Math.random() >= RatioRare) && !InventoryGet(C, "ItemHead")) InventoryWearRandom(C, "ItemHead", undefined, false);
if ((Math.random() >= RatioNormal) && (InventoryGet(C, "ItemMouth") == null)) InventoryWearRandom(C, "ItemMouth", null, false); if ((Math.random() >= RatioNormal) && !InventoryGet(C, "ItemMouth")) InventoryWearRandom(C, "ItemMouth", undefined, false);
if ((Math.random() >= RatioRare) && (InventoryGet(C, "ItemNeck") == null)) InventoryWearRandom(C, "ItemNeck", null, false); if ((Math.random() >= RatioRare) && !InventoryGet(C, "ItemNeck")) InventoryWearRandom(C, "ItemNeck", undefined, false);
if ((Math.random() >= RatioNormal) && (InventoryGet(C, "ItemLegs") == null)) InventoryWearRandom(C, "ItemLegs", null, false); if ((Math.random() >= RatioNormal) && !InventoryGet(C, "ItemLegs")) InventoryWearRandom(C, "ItemLegs", undefined, false);
if ((Math.random() >= RatioNormal) && !C.IsKneeling() && (InventoryGet(C, "ItemFeet") == null)) InventoryWearRandom(C, "ItemFeet", null, false); if ((Math.random() >= RatioNormal) && !C.IsKneeling() && !InventoryGet(C, "ItemFeet")) InventoryWearRandom(C, "ItemFeet", undefined, false);
if (Refresh || Refresh == null) CharacterRefresh(C); 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 {Character} C - Character for which to set the expression of
* @param {ExpressionGroupName | "Eyes1"} AssetGroup - Asset group for the expression * @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 {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 {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 * @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"; name = "Eyes1";
} }
const color = item.Color; 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) { function CharacterIsExpressionDisallowed(C, Item, Expression) {
if (!C || !Item) return "Internal error: missing character or item"; if (!C || !Item) return "Internal error: missing character or item";
const allowedExpr = InventoryGetItemProperty(Item, "AllowExpression", true); const allowedExpr = InventoryGetItemProperty(Item, "AllowExpression", true) ?? [];
const exprPres = InventoryGetItemProperty(Item, "ExpressionPrerequisite", true); const exprPres = InventoryGetItemProperty(Item, "ExpressionPrerequisite", true) ?? [];
const exprPre = exprPres[allowedExpr.indexOf(Expression)]; const exprPre = exprPres[allowedExpr.indexOf(Expression)];
const prereqMessage = !exprPre ? null : InventoryPrerequisiteMessage(C, exprPre, Item.Asset); 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 * 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 * @returns {string} - The compressed wardrobe
*/ */
function CharacterCompressWardrobe(Wardrobe) { function CharacterCompressWardrobe(Wardrobe) {
if (CommonIsArray(Wardrobe) && (Wardrobe.length > 0)) { if (CommonIsArray(Wardrobe) && (Wardrobe.length > 0)) {
var CompressedWardrobe = []; var CompressedWardrobe = [];
for (let W = 0; W < Wardrobe.length; W++) { for (const outfit of Wardrobe) {
/** @type {WardrobeItemBundle[]} */ /** @type {WardrobeItemBundle[]} */
var Arr = []; const Arr = [];
if (Wardrobe[W] != null) for (const bundle of outfit ?? []) {
for (let A = 0; A < Wardrobe[W].length; A++) Arr.push([bundle.Name, bundle.Group, bundle.Color, bundle.Property]);
Arr.push([Wardrobe[W][A].Name, Wardrobe[W][A].Group, Wardrobe[W][A].Color, Wardrobe[W][A].Property]); }
CompressedWardrobe.push(Arr); CompressedWardrobe.push(Arr);
} }
return LZString.compressToUTF16(JSON.stringify(CompressedWardrobe)); return LZString.compressToUTF16(JSON.stringify(CompressedWardrobe));
@ -2112,7 +2126,7 @@ function CharacterDecompressWardrobe(Wardrobe) {
*/ */
function CharacterHasItemWithAttribute(C, Attribute) { function CharacterHasItemWithAttribute(C, Attribute) {
return C.Appearance.some(item => { 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) { function CharacterItemsForActivity(C, Activity) {
return C.Appearance.filter(item => { 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.IsItem()) continue;
if (Group.ArousalZoneID != null) { if (Group.ArousalZoneID != null) {
let Zone = PreferenceGetArousalZone(C, Group.Name); let Zone = PreferenceGetArousalZone(C, Group.Name);
if (Zone.Orgasm && (Zone.Factor > 0)) if (Zone && Zone.Orgasm && (Zone.Factor > 0))
OrgasmZones.push(Zone.Name); 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 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) { function CharacterHasBlockedItem(C, BlockList) {
if ((BlockList == null) || !CommonIsArray(BlockList) || (BlockList.length == 0)) return false; if ((BlockList == null) || !CommonIsArray(BlockList) || (BlockList.length == 0)) return false;
for (let B = 0; B < BlockList.length; B++) return BlockList.some(category => C.Appearance.some(item => item.Asset.Category?.some(itemCategory => category === itemCategory)));
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;
} }
/** /**
@ -2337,7 +2347,7 @@ function CharacterCheckHooks(C, IgnoreHooks) {
// Fancy logic is to use a different hook for when the character is focused // Fancy logic is to use a different hook for when the character is focused
const layerVisibilityHook = () => { const layerVisibilityHook = () => {
const inDialog = (CurrentCharacter != null); const inDialog = (CurrentCharacter != null);
C.AppearanceLayers = C.AppearanceLayers.filter((Layer) => ( C.AppearanceLayers = C.AppearanceLayers?.filter((Layer) => (
!Layer.Visibility || !Layer.Visibility ||
(Layer.Visibility == "Player" && C.IsPlayer()) || (Layer.Visibility == "Player" && C.IsPlayer()) ||
(Layer.Visibility == "AllExceptPlayerDialog" && !(inDialog && 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} FromC - The character from which to pick the item
* @param {Character} ToC - The character on which we must put 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 {AssetGroupName} Group - The item group to transfer (Cloth, Hat, etc.)
* @param {boolean} [Refresh] - Perform a character refresh
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function CharacterTransferItem(FromC, ToC, Group, Refresh) { function CharacterTransferItem(FromC, ToC, Group, Refresh=true) {
let Item = InventoryGet(FromC, Group); let Item = InventoryGet(FromC, Group);
if (Item == null) return; 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); if (Refresh) CharacterRefresh(ToC);
} }
@ -2404,11 +2415,13 @@ function CharacterClearOwnership(C, push=true) {
if (C.IsPlayer()) { if (C.IsPlayer()) {
const ownerType = C.IsOwned(); const ownerType = C.IsOwned();
switch (ownerType) { switch (ownerType) {
case "online": case "online": {
ServerSend("AccountOwnership", { MemberNumber: C.Ownership.MemberNumber, Action: "Break" }); const number = C.Ownership?.MemberNumber ?? -1; // Can't happen; protected by `C.IsOwned()`
ServerSend("AccountOwnership", { MemberNumber: number, Action: "Break" });
C.Owner = ""; C.Owner = "";
C.Ownership = null; C.Ownership = null;
break; break;
}
case "npc": case "npc":
C.Owner = ""; C.Owner = "";
@ -2463,7 +2476,8 @@ function CharacterPronoun(C, DialogKey, HideIdentity) {
*/ */
function CharacterPronounDescription(C) { function CharacterPronounDescription(C) {
const pronounAsset = AssetGet(C.AssetFamily, "Pronouns", C.GetPronouns()); 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. * Note that changing any nickname but yours (ie. Player) is not supported.
* *
* @param {Character} C - The character to change the nickname of. * @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. * @return {null | NicknameStatus} null if the nickname was valid, or an explanation for why the nickname was rejected.
*/ */
function CharacterSetNickname(C, Nick, fromOwner = false) { function CharacterSetNickname(C, Nick, fromOwner = false) {
if (!C.IsPlayer()) return null; if (!C.IsPlayer()) return null;
Nick = Nick.trim(); Nick = (Nick ?? "").trim();
// Same nickname, or setting an empty nickname with no nickname already // Same nickname, or setting an empty nickname with no nickname already
if (C.Nickname === Nick || Nick.length === 0 && !C.Nickname) return null; if (C.Nickname === Nick || Nick.length === 0 && !C.Nickname) return null;
@ -2506,6 +2520,8 @@ function CharacterSetNickname(C, Nick, fromOwner = false) {
} }
C.Nickname = Nick; 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 }); ServerAccountUpdate.QueueData({ Nickname: Nick });
if (ServerPlayerIsInChatRoom()) { if (ServerPlayerIsInChatRoom()) {
@ -2559,6 +2575,7 @@ function CharacterSetOwnersNotes(C, notes = undefined) {
C.Ownership.Notes = undefined; C.Ownership.Notes = undefined;
} }
// @ts-ignore Strict-TS: Only OnlineCharacters have a MemberNumber
ServerSend("AccountOwnership", { MemberNumber: C.MemberNumber, Action: "UpdateNotes", Notes }); ServerSend("AccountOwnership", { MemberNumber: C.MemberNumber, Action: "UpdateNotes", Notes });
} }
} }
@ -2610,18 +2627,13 @@ function CharacterRefreshLeash(C) {
* @returns {Item} * @returns {Item}
*/ */
function CharacterScriptGet(C) { function CharacterScriptGet(C) {
let script = InventoryGet(C, "ItemScript"); let script = InventoryGet(C, "ItemScript") ?? /** @type {Item} */ (InventoryWear(C, "Script", "ItemScript"));
if (!script) {
InventoryWear(C, "Script", "ItemScript");
script = InventoryGet(C, "ItemScript");
}
script.Property = script.Property || {}; script.Property = script.Property || {};
// Propagate change and try to reload the item. If the script permissions // Propagate change and try to reload the item. If the script permissions
// on the target were wrong, then it'll be null // on the target were wrong, then it'll be null
CharacterScriptRefresh(C); CharacterScriptRefresh(C);
script = InventoryGet(C, "ItemScript"); script = /** @type {Item} */ (InventoryGet(C, "ItemScript"));
return script; 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. * 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 * @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) { function CommonDynamicFunctionParams(FunctionName) {
@ -424,23 +424,21 @@ function CommonDynamicFunctionParams(FunctionName) {
for (let P = 0; P < Params.length; P++) for (let P = 0; P < Params.length; P++)
Params[P] = Params[P].trim().replace('"', '').replace('"', ''); Params[P] = Params[P].trim().replace('"', '').replace('"', '');
FunctionName = FunctionName.substring(0, openParenthesisIndex); 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 // If it's really a function, we continue
/** @type {Record<string, any>} */ /** @type {Record<string, any>} */
const namespace = window; const namespace = window;
const func = namespace[FunctionName]; const func = namespace[FunctionName];
if (typeof func === "function") { 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;
} else {
// Log the error in the console
console.log("Trying to launch invalid function: " + FunctionName);
return false;
} }
// 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"; "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; ControllerButtonMapping[btnIdx] = buttonsMapping[btnIdx] ?? -1;
} }
for (const axisIdx of Object.keys(ControllerAxisMapping)) { for (const axisIdx of CommonKeys(ControllerAxisMapping)) {
ControllerAxisMapping[axisIdx] = axisMapping[axisIdx] ?? -1; 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) { function handleAxis(axisId, handler) {
const padAxisId = ControllerAxisMapping[axisId]; const padAxisId = ControllerAxisMapping[axisId];
const val = axes[padAxisId] ?? undefined; const val = axes[padAxisId] ?? undefined;
@ -342,12 +347,13 @@ function ControllerProcessAxis(axes) {
function ControllerManagedByGame(buttons) { function ControllerManagedByGame(buttons) {
// Map the gamepad button indexes to the game's button names // Map the gamepad button indexes to the game's button names
/** @type {GamepadButton[]} */
const mappedButtons = []; const mappedButtons = [];
for (const btnId of Object.values(ControllerButton)) { for (const btnId of Object.values(ControllerButton)) {
const padBtnId = ControllerButtonMapping[btnId]; const padBtnId = ControllerButtonMapping[btnId];
if (padBtnId === -1) continue; if (padBtnId === -1) continue;
// Grab either the actual button or make a dummy, in case the player has it unmapped // 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 // If the screen manages the controller, we call it
@ -373,6 +379,8 @@ function ControllerProcessButton(buttons) {
/** /**
* Helper function to process a button press * Helper function to process a button press
* @param {ControllerButton} btnId
* @param {() => void} handler
*/ */
function handleButton(btnId, handler) { function handleButton(btnId, handler) {
if (ControllerButtonsWaitRelease) return; if (ControllerButtonsWaitRelease) return;
@ -394,7 +402,7 @@ function ControllerProcessButton(buttons) {
handleButton(ControllerButton.A, () => { handleButton(ControllerButton.A, () => {
if (!ControllerDPadAsAxisWorkaround) return; if (!ControllerDPadAsAxisWorkaround) return;
// Trigger a fake click event // @ts-ignore Strict-TS: Trigger a fake click event
CommonClick(null); CommonClick(null);
}); });
handleButton(ControllerButton.B, () => { handleButton(ControllerButton.B, () => {
@ -467,8 +475,10 @@ function ControllerCalibrationNextStage(skip = false) {
if (skip) { if (skip) {
// We're skipping, unset the value for that input // We're skipping, unset the value for that input
if (isAxis) { if (isAxis) {
// @ts-ignore Strict-TS: the initialization above and the check below should ensure we stay in bounds
ControllerAxisMapping[stage] = -1; ControllerAxisMapping[stage] = -1;
} else { } else {
// @ts-ignore Strict-TS: the initialization above and the check below should ensure we stay in bounds
ControllerButtonMapping[stage] = -1; ControllerButtonMapping[stage] = -1;
} }
} }
@ -557,6 +567,7 @@ function ControllerCalibrationStageLabel() {
case ControllerAxis.StickRH: case ControllerAxis.StickRH:
return TextGet("MoveRightStickRight"); return TextGet("MoveRightStickRight");
} }
return "";
} }
const ControllerCalibrationLowWatermark = 0.05; const ControllerCalibrationLowWatermark = 0.05;

View file

@ -55,8 +55,8 @@ var DialogTightenLoosenItem = null;
* @type {Item|null} * @type {Item|null}
*/ */
var DialogFocusSourceItem = null; var DialogFocusSourceItem = null;
/** @type {null | ReturnType<typeof setTimeout>} */ /** @type {ReturnType<typeof setTimeout>} */
var DialogFocusItemColorizationRedrawTimer = null; var DialogFocusItemColorizationRedrawTimer = /** @type {never} */ (null);
/** /**
* The list of currently visible menu item buttons. * The list of currently visible menu item buttons.
* @type {DialogMenuButton[]} * @type {DialogMenuButton[]}
@ -70,7 +70,7 @@ var DialogMenuMode = null;
/** /**
* The group that was selected before we entered the expression coloring screen * 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; var DialogExpressionPreviousMode = null;
@ -106,17 +106,17 @@ var DialogGamingReturnScreen = null;
var DialogButtonDisabledTester = /Disabled(For\w+)?$/u; var DialogButtonDisabledTester = /Disabled(For\w+)?$/u;
/** /**
* The attempted action that's leading the player to struggle. * The attempted action that's leading the player to struggle.
* @type {DialogStruggleActionType?} * @type {DialogStruggleActionType | null}
*/ */
let DialogStruggleAction = null; let DialogStruggleAction = null;
/** /**
* The item we're struggling out of, or swapping from. * The item we're struggling out of, or swapping from.
* @type {Item} * @type {Item | null}
*/ */
let DialogStrugglePrevItem = null; let DialogStrugglePrevItem = null;
/** /**
* The item we're swapping to. * The item we're swapping to.
* @type {Item} * @type {Item | null}
*/ */
let DialogStruggleNextItem = null; let DialogStruggleNextItem = null;
/** Whether we went through the struggle selection screen or went straight through. */ /** Whether we went through the struggle selection screen or went straight through. */
@ -151,7 +151,6 @@ var DialogFavoriteStateDetails = [
{ {
TargetFavorite: false, TargetFavorite: false,
PlayerFavorite: false, PlayerFavorite: false,
Icon: null,
UsableOrder: DialogSortOrder.Usable, UsableOrder: DialogSortOrder.Usable,
UnusableOrder: DialogSortOrder.Unusable UnusableOrder: DialogSortOrder.Unusable
}, },
@ -182,11 +181,11 @@ var DialogLeaveFocusItemHandlers = {
DialogTightenLoosenItem: { DialogTightenLoosenItem: {
Crafting: (item) => { Crafting: (item) => {
// Subtract deterministic modifiers so that only the difficulty factor remains // Subtract deterministic modifiers so that only the difficulty factor remains
CraftingSelectedItem.DifficultyFactor = ( /** @type {CraftingItemSelected} */ (CraftingSelectedItem).DifficultyFactor = (
item.Difficulty (item.Difficulty ?? 0)
- SkillGetLevel(Player, "Bondage") - SkillGetLevel(Player, "Bondage")
- item.Asset.Difficulty - item.Asset.Difficulty
- (item.Craft.Effects?.Secure ?? 0) * 4 - (item.Craft?.Effects?.Secure ?? 0) * 4
); );
item.Difficulty = item.Asset.Difficulty; item.Difficulty = item.Asset.Difficulty;
CraftingModeSet("Name"); CraftingModeSet("Name");
@ -226,8 +225,10 @@ var DialogLeaveFocusItemHandlers = {
* @returns {Character} - The actual character * @returns {Character} - The actual character
*/ */
function DialogGetCharacter(C) { 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.toUpperCase().trim() == "PLAYER") ? Player : CurrentCharacter;
}
return C; return C;
} }
@ -335,9 +336,8 @@ function DialogLogQuery(LogType, LogGroup) { return LogQuery(LogType, LogGroup);
/** /**
* Sets the AllowItem flag on the current character * Sets the AllowItem flag on the current character
* @param {string} Allow - The flag to set. Either "TRUE" or "FALSE" * @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 * 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 * 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 * @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 * 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 * 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 * @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 * 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 * @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 * 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 * @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 * 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. * Can be omitted to bring the character back to the standing position.
* @returns {void} - Nothing * @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 * 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 * @returns {boolean} - Returns true, if the prerequisite is met, false otherwise
*/ */
function DialogPrerequisite(dialog) { function DialogPrerequisite(dialog) {
if (dialog.Prerequisite == null) const prereq = dialog.Prerequisite?.trim();
return true; if (!prereq) return true;
else if (dialog.Prerequisite.indexOf("Player.") == 0)
return Player[dialog.Prerequisite.substring(7, 250).replace("()", "").trim()](); const match = prereq.match(/^(!?)(Player|CurrentCharacter)\.(\w+)\(\)$/);
else if (dialog.Prerequisite.indexOf("!Player.") == 0) if (match) {
return !Player[dialog.Prerequisite.substring(8, 250).replace("()", "").trim()](); const [, neg, target, method] = match;
else if (dialog.Prerequisite.indexOf("CurrentCharacter.") == 0) const obj = target === "Player" ? Player : CurrentCharacter;
return CurrentCharacter[dialog.Prerequisite.substring(17, 250).replace("()", "").trim()]();
else if (dialog.Prerequisite.indexOf("!CurrentCharacter.") == 0) if (typeof obj[method] !== "function") {
return !CurrentCharacter[dialog.Prerequisite.substring(18, 250).replace("()", "").trim()](); console.error(
else if (dialog.Prerequisite.indexOf("(") >= 0) `DialogPrerequisite: Method '${method}' not found on ${target}`,
return !!CommonDynamicFunctionParams(dialog.Prerequisite); { prereq, dialog }
else if (dialog.Prerequisite.substring(0, 1) != "!") );
return !!window[CurrentScreen + dialog.Prerequisite.trim()]; return false;
else }
return !window[CurrentScreen + dialog.Prerequisite.substr(1, 250).trim()];
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 (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) { if (lock && lock.Asset.ExclusiveUnlock) {
// Locks with exclusive access (intricate, high-sec) // 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 // 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 // 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); let UnlockName = /** @type {EffectName} */("Unlock" + Item.Asset.Name);
if ((Item.Property != null) && (Item.Property.LockedBy != null)) 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 * @returns {string} - The name of the current dialog, if such a dialog exists, any empty string otherwise
*/ */
function DialogIntro(C) { 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 ?? ""; return dialog?.Result ?? "";
} }
@ -813,7 +848,8 @@ function DialogLeave(options=null) {
*/ */
function DialogRemove() { function DialogRemove() {
const C = CurrentCharacter; 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) { if (dialogIndex !== -1) {
C.Dialog.splice(dialogIndex, 1); C.Dialog.splice(dialogIndex, 1);
document.querySelector(`.dialog-dialog-button[data-index="${dialogIndex}"]`)?.remove(); document.querySelector(`.dialog-dialog-button[data-index="${dialogIndex}"]`)?.remove();
@ -827,11 +863,12 @@ function DialogRemove() {
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function DialogRemoveGroup(GroupName) { function DialogRemoveGroup(GroupName) {
if (!CurrentCharacter) return;
const GroupNameUpper = GroupName.trim().toUpperCase(); const GroupNameUpper = GroupName.trim().toUpperCase();
document.querySelectorAll(`.dialog-dialog-button[data-group="${GroupNameUpper}" i]`).forEach(e => e.remove()); 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())); document.querySelectorAll(".dialog-dialog-button").forEach((e, i) => e.setAttribute("data-index", i.toString()));
for (let D = CurrentCharacter.Dialog.length - 1; D >= 0; D--) 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); CurrentCharacter.Dialog.splice(D, 1);
} }
} }
@ -863,8 +900,9 @@ function DialogMenuBack() {
break; break;
case "colorExpression": { case "colorExpression": {
if (!DialogExpressionPreviousMode) return;
const { mode, group } = DialogExpressionPreviousMode; const { mode, group } = DialogExpressionPreviousMode;
DialogChangeMode(mode || "dialog"); DialogChangeMode(mode ?? "dialog");
DialogChangeFocusToGroup(Player, group); DialogChangeFocusToGroup(Player, group);
DialogExpressionPreviousMode = null; DialogExpressionPreviousMode = null;
} }
@ -911,7 +949,7 @@ function DialogMenuBack() {
* Returns whether the current mode shows items. * Returns whether the current mode shows items.
*/ */
function DialogModeShowsInventory() { 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 * @returns {void} - Nothing
*/ */
function DialogLeaveItemMenu() { 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 * Returns settings for an item based on whether the player and target have favorited it, if any
* @param {Character} C - The targeted character * @param {Character} C - The targeted character
* @param {Asset} asset - The asset to check favorite settings for * @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 * @returns {FavoriteState} - The details to use for the asset
*/ */
function DialogGetFavoriteStateDetails(C, asset, type = null) { function DialogGetFavoriteStateDetails(C, asset, type = null) {
const isTargetFavorite = InventoryIsFavorite(C, asset.Name, asset.Group.Name, type); const isTargetFavorite = InventoryIsFavorite(C, asset.Name, asset.Group.Name, type);
const isPlayerFavorite = !C.IsPlayer() && InventoryIsFavorite(Player, 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 * @returns {InventoryIcon[]} - A list of icon names
*/ */
function DialogGetAssetIcons(asset) { function DialogGetAssetIcons(asset) {
/** @type {InventoryIcon[]} */
let icons = []; let icons = [];
icons = icons.concat(asset.PreviewIcons); icons = icons.concat(asset.PreviewIcons);
if (asset.OwnerOnly) icons.push("OwnerOnly"); if (asset.OwnerOnly) icons.push("OwnerOnly");
@ -1154,7 +1195,7 @@ const DialogEffectIcons = /** @type {const} */({
const craftingProperty = item.Craft?.Effects; const craftingProperty = item.Craft?.Effects;
return DialogEffectIcons.GetEffectIcons(effects, craftingProperty); 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) { GetEffectIcons: function (effects, craftEffect) {
/** @type {InventoryIcon[]} */ /** @type {InventoryIcon[]} */
const icons = []; const icons = [];
@ -1202,7 +1243,7 @@ const DialogEffectIcons = /** @type {const} */({
_GetDeafIcon(effect) { _GetDeafIcon(effect) {
/** @type {InventoryIcon[]} */ /** @type {InventoryIcon[]} */
const keys = ["DeafLight", "DeafNormal", "DeafHeavy"]; 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} */ /** @type {(level?: number) => null | InventoryIcon} */
_GagLevelToIcon: function (level) { _GagLevelToIcon: function (level) {
@ -1220,11 +1261,11 @@ const DialogEffectIcons = /** @type {const} */({
}, },
/** @type {(level?: number) => null | InventoryIcon} */ /** @type {(level?: number) => null | InventoryIcon} */
_BlindLevelToIcon: function (level) { _BlindLevelToIcon: function (level) {
if (!level || level < CharacterBlindLevels.get("BlindLight")) { if (!level || level < (CharacterBlindLevels.get("BlindLight") ?? 0)) {
return null; return null;
} else if (level <= CharacterBlindLevels.get("BlindLight")) { } else if (level <= (CharacterBlindLevels.get("BlindLight") ?? 0)) {
return "BlindLight"; return "BlindLight";
} else if (level <= CharacterBlindLevels.get("BlindHeavy")) { } else if (level <= (CharacterBlindLevels.get("BlindHeavy") ?? 0)) {
return "BlindNormal"; return "BlindNormal";
} else { } else {
return "BlindHeavy"; return "BlindHeavy";
@ -1300,7 +1341,7 @@ function DialogCanInspectLock(Item) {
if (!Item) return false; if (!Item) return false;
const lockedBy = InventoryGetItemProperty(Item, "LockedBy"); 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 = []; DialogMenuButton = [];
// Hide "Exit" button for the screens where // Hide "Exit" button for the screens where
if (!["colorExpression", "colorItem"].includes(DialogMenuMode)) if (DialogMenuMode && !["colorExpression", "colorItem"].includes(DialogMenuMode))
DialogMenuButton = ["Exit"]; DialogMenuButton = ["Exit"];
// There's no group focused, hence no menu to draw // There's no group focused, hence no menu to draw
if (C.FocusGroup == null) return; if (C.FocusGroup == null) return;
/** The item in the current slot */ /** 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 ItemBlockedOrLimited = !!Item && InventoryBlockedOrLimited(C, Item);
const IsItemLocked = InventoryItemHasEffect(Item, "Lock", true); const IsItemLocked = InventoryItemHasEffect(Item, "Lock", true);
const IsGroupBlocked = InventoryGroupIsBlocked(C, C.FocusGroup.Name); const IsGroupBlocked = InventoryGroupIsBlocked(C, C.FocusGroup.Name);
@ -1446,7 +1487,7 @@ function DialogMenuButtonBuild(C) {
} }
if (!DialogMenuButton.includes("Use") && canUseRemoteState !== "InvalidItem") { if (!DialogMenuButton.includes("Use") && canUseRemoteState !== "InvalidItem") {
/** @type {DialogMenuButton} */ /** @type {DialogMenuButton | null} */
let button = null; let button = null;
switch (canUseRemoteState) { switch (canUseRemoteState) {
case "Available": case "Available":
@ -1576,7 +1617,7 @@ function DialogInventoryBuild(C, resetOffset=false, locks=false, reload=true) {
return; 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 // In item permission mode we add all the enable items except the ones already on, unless on Extreme difficulty
if (DialogMenuMode === "permissions") { if (DialogMenuMode === "permissions") {
@ -1585,7 +1626,7 @@ function DialogInventoryBuild(C, resetOffset=false, locks=false, reload=true) {
continue; continue;
if (A.Wear) { 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); DialogInventoryAdd(Player, { Asset: A }, isWorn, DialogSortOrder.Enabled);
} else if (A.IsLock) { } else if (A.IsLock) {
const LockIsWorn = InventoryCharacterIsWearingLock(C, /** @type {AssetLockType} */ (A.Name)); 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) * @param {number} Slot - Index of saved expression (0 to 4)
*/ */
function DialogFacialExpressionsLoad(Slot) { function DialogFacialExpressionsLoad(Slot) {
const expressions = Player.SavedExpressions && Player.SavedExpressions[Slot]; const expressions = Player.SavedExpressions[Slot];
if (expressions != null) { if (!expressions) return;
expressions.forEach(e => { expressions.forEach(e => {
CharacterSetFacialExpression(Player, e.Group, e.CurrentExpression); CharacterSetFacialExpression(Player, e.Group, e.CurrentExpression ?? null);
Player.ActiveExpression.setWithoutReload(e.Group, e.CurrentExpression); Player.ActiveExpression.setWithoutReload(e.Group, e.CurrentExpression ?? null);
}); });
if (DialogSelfMenuSelected === "Expression" && DialogSelfMenuMapping.Expression.C.IsPlayer()) { if (DialogSelfMenuSelected === "Expression" && DialogSelfMenuMapping.Expression.C.IsPlayer()) {
DialogSelfMenuMapping.Expression.Reload(); DialogSelfMenuMapping.Expression.Reload();
}
} }
} }
@ -1718,7 +1758,7 @@ function DialogBuildSavedExpressionsMenu() {
); );
for (let x = 0; x < expression.length; x++) { 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); CharacterRefresh(PreviewCharacter, false, false);
@ -1736,11 +1776,11 @@ function DialogBuildSavedExpressionsMenu() {
function DialogMenuButtonClick() { function DialogMenuButtonClick() {
// Hack because those panes handle their menu icons themselves // 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 // Gets the current character and item
/** The focused character */ /** The focused character */
const C = CharacterGetCurrent(); const C = /** @type {Character} */ (CharacterGetCurrent());
/** The focused item */ /** The focused item */
const Item = C.FocusGroup ? InventoryGet(C, C.FocusGroup.Name) : null; const Item = C.FocusGroup ? InventoryGet(C, C.FocusGroup.Name) : null;
@ -1762,9 +1802,11 @@ function DialogMenuButtonClick() {
} }
// Remote Icon - Pops the item extension // Remote Icon - Pops the item extension
else if (button === "Remote" && DialogCanUseRemoteState(C, Item) === "Available") { else if (button === "Remote") {
DialogExtendItem(Item); if (Item && DialogCanUseRemoteState(C, Item) === "Available") {
return true; DialogExtendItem(Item);
return true;
}
} }
// Lock Icon - Rebuilds the inventory list with locking items // Lock Icon - Rebuilds the inventory list with locking items
@ -1779,7 +1821,7 @@ function DialogMenuButtonClick() {
else if (button === "Unlock" && Item) { else if (button === "Unlock" && Item) {
// Check that this is not one of the sticky-locked items // Check that this is not one of the sticky-locked items
const isNotStickyLock = InventoryItemHasEffect(Item, "Lock", true) && !InventoryItemHasEffect(Item, "Lock", false); 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); InventoryUnlock(C, C.FocusGroup.Name, false);
if (ChatRoomPublishAction(C, "ActionUnlock", Item, null)) { if (ChatRoomPublishAction(C, "ActionUnlock", Item, null)) {
DialogLeave(); DialogLeave();
@ -1933,7 +1975,7 @@ function DialogAllowItemClick(CurrentItem, ClickItem) {
* @returns {ItemPermissionMode} - Nothing * @returns {ItemPermissionMode} - Nothing
*/ */
function DialogPermissionsClick(ClickItem, CurrentItem=null) { 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); return DialogInventoryTogglePermission(ClickItem, worn);
} }
@ -1973,7 +2015,7 @@ function DialogItemClick(ClickItem, C, CurrentItem=null) {
DialogStruggleStart(C, "ActionUnlock", CurrentItem, null); DialogStruggleStart(C, "ActionUnlock", CurrentItem, null);
} else if (ClickItem.Asset.Name === "VibratorRemote" || ClickItem.Asset.Name === "LoversVibratorRemote") { } else if (ClickItem.Asset.Name === "VibratorRemote" || ClickItem.Asset.Name === "LoversVibratorRemote") {
// The vibrating egg remote can open the vibrating egg's extended dialog // 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 { } else {
// Runs the activity arousal process if activated, & publishes the item action text to the chatroom // Runs the activity arousal process if activated, & publishes the item action text to the chatroom
DialogPublishAction(C, "ActionUse", ClickItem); DialogPublishAction(C, "ActionUse", ClickItem);
@ -2001,6 +2043,7 @@ function DialogItemClick(ClickItem, C, CurrentItem=null) {
* @param {null | Item} equippedItem * @param {null | Item} equippedItem
*/ */
function DialogActivityClick(C, clickedActivity, equippedItem) { function DialogActivityClick(C, clickedActivity, equippedItem) {
if (!C.FocusGroup) return;
if (C.IsNpc() && clickedActivity.Item) { if (C.IsNpc() && clickedActivity.Item) {
let Line = C.FocusGroup.Name + clickedActivity.Item.Asset.DynamicName(Player); let Line = C.FocusGroup.Name + clickedActivity.Item.Asset.DynamicName(Player);
let D = DialogFind(C, Line, null, false); let D = DialogFind(C, Line, null, false);
@ -2045,6 +2088,7 @@ function DialogInventoryTogglePermission(item, worn) {
*/ */
function DialogChangeMode(mode, reset=false) { function DialogChangeMode(mode, reset=false) {
const C = CharacterGetCurrent(); const C = CharacterGetCurrent();
if (!C) return;
// Handle changing to the expression color picker having to restore the selected mode & group // Handle changing to the expression color picker having to restore the selected mode & group
if (mode === "colorExpression" && (!DialogExpressionPreviousMode || DialogExpressionPreviousMode.mode !== "colorExpression")) { if (mode === "colorExpression" && (!DialogExpressionPreviousMode || DialogExpressionPreviousMode.mode !== "colorExpression")) {
@ -2145,7 +2189,7 @@ function DialogChangeMode(mode, reset=false) {
/** /**
* Change the given character's focused group. * Change the given character's focused group.
* @param {Character} C - The character to change the focus of. * @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) { function DialogChangeFocusToGroup(C, Group) {
/** @type {null | AssetGroup} */ /** @type {null | AssetGroup} */
@ -2173,12 +2217,12 @@ function DialogChangeFocusToGroup(C, Group) {
AudioDialogStop(); AudioDialogStop();
// Stop any struggling minigame // Stop any struggling minigame
if(StruggleMinigameIsRunning()) { if (StruggleMinigameIsRunning()) {
StruggleMinigameStop(); StruggleMinigameStop();
} }
// If we're in the two-character dialog, clear their focused group // If we're in the two-character dialog, clear their focused group
if (!CurrentCharacter.IsPlayer()) { if (CurrentCharacter && !CurrentCharacter?.IsPlayer()) {
Player.FocusGroup = null; Player.FocusGroup = null;
CurrentCharacter.FocusGroup = null; CurrentCharacter.FocusGroup = null;
} }
@ -2211,19 +2255,21 @@ function DialogClick(event) {
// Gets the current character // Gets the current character
let C = CharacterGetCurrent(); let C = CharacterGetCurrent();
if (!C) return;
// Check if the user clicked on one of the top menu icons // Check if the user clicked on one of the top menu icons
if (DialogMenuButtonClick()) return; if (DialogMenuButtonClick()) return;
// User clicked on the interacted character or herself, check if we need to update the menu // 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()) { 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; let X = MouseX < 500 ? 0 : 500;
for (const Group of AssetGroup) { for (const Group of AssetGroup) {
if (!Group.IsItem()) continue; 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) { if (Zone) {
DialogChangeFocusToGroup(C, Group); DialogChangeFocusToGroup(C, Group);
C = clickedChar;
break; 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 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)) { 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; CharacterAppearanceForceUpCharacter = CharacterAppearanceForceUpCharacter == CurrentCharacter.MemberNumber ? -1 : CurrentCharacter.MemberNumber;
return; return;
} }
@ -2390,11 +2437,12 @@ function DialogFindFacialExpressionMenuGroup(ExpressionGroup) {
/** /**
* Displays the given text for 5 seconds * Displays the given text for 5 seconds
* @param {string} status - The text to be displayed * @param {string} status - The text to be displayed
* @param {number} timer - the number of milliseconds to display the message for * @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 {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 * @returns {void} - Nothing
*/ */
function DialogSetStatus(status, timer=0, replace=null, id=null) { function DialogSetStatus(status, timer=0, replace, id) {
id ??= DialogMenuMapping[DialogMenuMode]?.ids.status; id ??= DialogMenuMapping[DialogMenuMode]?.ids.status;
const elem = id ? document.getElementById(id) : null; const elem = id ? document.getElementById(id) : null;
if (!elem) { if (!elem) {
@ -2403,7 +2451,8 @@ function DialogSetStatus(status, timer=0, replace=null, id=null) {
replace ??= {}; replace ??= {};
if (replace.group || replace.asset) { 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) { if (replace.asset) {
status = status.replaceAll("AssetName", replace.asset.Description); status = status.replaceAll("AssetName", replace.asset.Description);
@ -2454,7 +2503,7 @@ function DialogStatusClear() {
const timeoutID = elem?.getAttribute("data-timeout-id"); const timeoutID = elem?.getAttribute("data-timeout-id");
if (timeoutID) { if (timeoutID) {
clearTimeout(Number.parseInt(timeoutID, 10)); clearTimeout(Number.parseInt(timeoutID, 10));
DialogStatusTimerHandler(elem); if (elem) DialogStatusTimerHandler(elem);
} }
} }
@ -2467,11 +2516,12 @@ function DialogStatusClear() {
*/ */
function DialogExtendItem(Item, SourceItem) { function DialogExtendItem(Item, SourceItem) {
const C = CharacterGetCurrent(); const C = CharacterGetCurrent();
if (!C) return;
if (AsylumGGTSControlItem(C, Item)) return; if (AsylumGGTSControlItem(C, Item)) return;
if (InventoryBlockedOrLimited(C, Item)) return; if (InventoryBlockedOrLimited(C, Item)) return;
DialogChangeMode("extended"); DialogChangeMode("extended");
DialogFocusItem = Item; DialogFocusItem = Item;
DialogFocusSourceItem = SourceItem; DialogFocusSourceItem = SourceItem ?? null;
ExtendedItemInit(C, Item.Asset.IsLock ? SourceItem : Item, false, true); ExtendedItemInit(C, Item.Asset.IsLock ? SourceItem : Item, false, true);
CommonDynamicFunction("Inventory" + Item.Asset.Group.Name + Item.Asset.Name + "Load()"); CommonDynamicFunction("Inventory" + Item.Asset.Group.Name + Item.Asset.Name + "Load()");
} }
@ -2483,6 +2533,7 @@ function DialogExtendItem(Item, SourceItem) {
*/ */
function DialogSetTightenLoosenItem(Item) { function DialogSetTightenLoosenItem(Item) {
const C = CharacterGetCurrent(); const C = CharacterGetCurrent();
if (!C) return;
if (AsylumGGTSControlItem(C, Item)) return; if (AsylumGGTSControlItem(C, Item)) return;
if (InventoryBlockedOrLimited(C, Item)) return; if (InventoryBlockedOrLimited(C, Item)) return;
DialogChangeMode("tighten"); DialogChangeMode("tighten");
@ -2839,7 +2890,7 @@ class DialogMenu {
} }
/** @type {ScreenLoadHandler} */ /** @type {ScreenLoadHandler} */
Load() { async Load() {
if (this._initPropertyNames.some(p => this._initProperties?.[p] == null)) { if (this._initPropertyNames.some(p => this._initProperties?.[p] == null)) {
console.error( console.error(
`Aborting, one or more uninitialized properties in ${this.mode} subscreen`, `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 currentProp = CommonPick(/** @type {Partial<PropType>} */(this._initProperties ?? {}), this._initPropertyNames);
const newProp = CommonPick(properties, this._initPropertyNames); const newProp = CommonPick(properties, this._initPropertyNames);
for (const k of Object.keys(newProp)) { for (const k of Object.keys(newProp)) {
// @ts-ignore Strict-TS: direct property access to initialize
newProp[k] ??= currentProp[k]; newProp[k] ??= currentProp[k];
} }
@ -3105,7 +3157,7 @@ class DialogMenu {
* Return the underlying item or activity object of the passed grid button. * Return the underlying item or activity object of the passed grid button.
* @abstract * @abstract
* @param {HTMLButtonElement} button - The clicked button * @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) { _GetClickedObject(button) {
throw new Error("Trying to all an abstract method"); throw new Error("Trying to all an abstract method");
@ -3263,8 +3315,8 @@ class _DialogFocusMenu extends DialogMenu {
} }
/** @type {DialogMenu["Load"]} */ /** @type {DialogMenu["Load"]} */
Load() { async Load() {
super.Load(); await super.Load();
document.getElementById(this.ids.root)?.setAttribute("data-group", this.focusGroup.Name); document.getElementById(this.ids.root)?.setAttribute("data-group", this.focusGroup.Name);
return; return;
} }
@ -3378,7 +3430,7 @@ class _DialogItemMenu extends _DialogFocusMenu {
/** @type {null | Asset} */ /** @type {null | Asset} */
let asset = null; let asset = null;
let showIcon = false; let showIcon = false;
let textContent = options.status; let textContent = options.status ?? "";
if (textContent == null) { if (textContent == null) {
switch (this.mode) { switch (this.mode) {
case "locked": { case "locked": {
@ -3489,6 +3541,7 @@ class _DialogItemMenu extends _DialogFocusMenu {
/** @type {_DialogFocusMenu["_ReloadIcon"]} */ /** @type {_DialogFocusMenu["_ReloadIcon"]} */
_ReloadIcon(root, icon, properties, options) { _ReloadIcon(root, icon, properties, options) {
const grid = document.getElementById(this.ids.grid); const grid = document.getElementById(this.ids.grid);
if (!grid) return;
const dataAttr = ["data-craft", "data-hidden", "data-vibrating"]; const dataAttr = ["data-craft", "data-hidden", "data-vibrating"];
const checkedButton = grid.querySelector(".dialog-grid-button[aria-checked='true']"); const checkedButton = grid.querySelector(".dialog-grid-button[aria-checked='true']");
if (checkedButton) { if (checkedButton) {
@ -3508,7 +3561,7 @@ class _DialogItemMenu extends _DialogFocusMenu {
* @type {DialogMenu<string, DialogInventoryItem>["_GetClickedObject"]} * @type {DialogMenu<string, DialogInventoryItem>["_GetClickedObject"]}
*/ */
_GetClickedObject(button) { _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"); return equippedItem ? null : InterfaceTextGet("NoItemEquipped");
}, },
InventoryDoesItemAllowLock: (C, clickedLock, equippedItem) => { 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"]} */ /** @type {DialogMenu<string, DialogInventoryItem>["_GetClickedObject"]} */
_GetClickedObject(button) { _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"]} */ /** @type {DialogMenu<string, DialogInventoryItem>["_ClickButton"]} */
@ -3753,7 +3806,7 @@ class _DialogPermissionMenu extends _DialogFocusMenu {
/** @type {DialogMenu<string, DialogInventoryItem>["_GetClickedObject"]} */ /** @type {DialogMenu<string, DialogInventoryItem>["_GetClickedObject"]} */
_GetClickedObject(button) { _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"]} */ /** @type {DialogMenu<string, DialogInventoryItem>["_ClickButton"]} */
@ -3865,7 +3918,7 @@ class _DialogActivitiesMenu extends _DialogFocusMenu {
/** @type {DialogMenu<string, ItemActivity>["_GetClickedObject"]} */ /** @type {DialogMenu<string, ItemActivity>["_GetClickedObject"]} */
_GetClickedObject(button) { _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"]} */ /** @type {DialogMenu<string, ItemActivity>["_ClickButton"]} */
@ -3957,7 +4010,7 @@ class _DialogCraftedMenu extends _DialogFocusMenu {
_ReloadIcon(root, icon, properties, options) { _ReloadIcon(root, icon, properties, options) {
const { C, focusGroup } = properties; const { C, focusGroup } = properties;
const ids = this.ids; const ids = this.ids;
const item = InventoryGet(C, focusGroup.Name); const item = /** @type {Item} */ (InventoryGet(C, focusGroup.Name));
icon.innerHTML = ""; icon.innerHTML = "";
[ [
@ -4102,7 +4155,7 @@ class _DialogDialogMenu extends DialogMenu {
super.Exit(); super.Exit();
} }
/** @type {ScreenFunctions["Unload"]} */ /** @type {VoidHandler} */
Unload() { Unload() {
super.Unload(); super.Unload();
} }
@ -4139,7 +4192,7 @@ class _DialogDialogMenu extends DialogMenu {
continue; 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); button.toggleAttribute("hidden", unload);
if (!unload) { if (!unload) {
button.querySelector(".button-label")?.replaceChildren(SpeechTransformDialog(Player, dialog.Option)); button.querySelector(".button-label")?.replaceChildren(SpeechTransformDialog(Player, dialog.Option));
@ -4179,7 +4232,7 @@ class _DialogDialogMenu extends DialogMenu {
/** @type {DialogMenu<string, DialogLine>["_GetClickedObject"]} */ /** @type {DialogMenu<string, DialogLine>["_GetClickedObject"]} */
_GetClickedObject(button) { _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; 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 // A dialog option can change the conversation stage, show text or launch a custom function
if ((Player.CanTalk() && C.CanTalk()) || SpeechFullEmote(clickedDialog.Option)) { if ((Player.CanTalk() && C.CanTalk()) || SpeechFullEmote(clickedDialog.Option)) {
C._CurrentDialog = clickedDialog.Result; C._CurrentDialog = clickedDialog.Result;
if (clickedDialog.NextStage != null) { if (clickedDialog.NextStage !== null) {
C._Stage = clickedDialog.NextStage; C._Stage = clickedDialog.NextStage;
this.Reload(); this.Reload();
} else { } else {
@ -4203,8 +4256,12 @@ class _DialogDialogMenu extends DialogMenu {
DialogSetStatus(C.CurrentDialog); DialogSetStatus(C.CurrentDialog);
} }
if (typeof clickedDialog.Function === "string") { if (clickedDialog.Function) {
CommonDynamicFunctionParams(clickedDialog.Function); try {
CommonDynamicFunctionParams(clickedDialog.Function);
} catch (err) {
console.error("_ClickButton: Failed dynamic expression", clickedDialog.Function, err);
}
} }
} else if (clickedDialog.Function?.trim() === "DialogLeave()") { } else if (clickedDialog.Function?.trim() === "DialogLeave()") {
DialogLeave(); DialogLeave();
@ -4374,7 +4431,7 @@ class _DialogExpressionMenu extends _DialogSelfMenu {
/** @satisfies {DialogMenu<ModeType, ExpressionPair>["clickStatusCallbacks"]} */ /** @satisfies {DialogMenu<ModeType, ExpressionPair>["clickStatusCallbacks"]} */
clickStatusCallbacks = { clickStatusCallbacks = {
CharacterIsExpressionAllowed: (C, clickedItem, equippedItem) => { 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; return status ? InterfaceTextGet(`Prerequisite${status}`) : null;
}, },
}; };
@ -4438,7 +4495,7 @@ class _DialogExpressionMenu extends _DialogSelfMenu {
} else { } else {
colorButton?.setAttribute("aria-disabled", "true"); colorButton?.setAttribute("aria-disabled", "true");
} }
if (colorButton.getAttribute("aria-disabled") === "true" && ItemColorState) { if (colorButton?.getAttribute("aria-disabled") === "true" && ItemColorState) {
ItemColorCancelAndExit(); ItemColorCancelAndExit();
} }
}, },
@ -4480,28 +4537,29 @@ class _DialogExpressionMenu extends _DialogSelfMenu {
/** @type {DialogMenu.MenuButtonData<{ C: PlayerCharacter }>} */ /** @type {DialogMenu.MenuButtonData<{ C: PlayerCharacter }>} */
blink: { blink: {
click(button, ev, { C }) { 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} */ /** @type {string} */
let state; let state;
switch (level) { switch (level) {
case 1: case 1:
state = "None"; state = "None";
CharacterSetFacialExpression(C, "Eyes", C.ActiveExpression.Eyes, null); CharacterSetFacialExpression(C, "Eyes", C.ActiveExpression.Eyes);
break; break;
case 2: case 2:
state = "Left"; state = "Left";
CharacterSetFacialExpression(C, "Eyes1", C.ActiveExpression.Eyes, null); CharacterSetFacialExpression(C, "Eyes1", C.ActiveExpression.Eyes);
CharacterSetFacialExpression(C, "Eyes2", "Closed", null); CharacterSetFacialExpression(C, "Eyes2", "Closed");
break; break;
case 3: case 3:
state = "Both"; state = "Both";
CharacterSetFacialExpression(C, "Eyes", "Closed", null); CharacterSetFacialExpression(C, "Eyes", "Closed");
break; break;
case 4: case 4:
state = "Right"; state = "Right";
CharacterSetFacialExpression(C, "Eyes1", "Closed", null); CharacterSetFacialExpression(C, "Eyes1", "Closed");
CharacterSetFacialExpression(C, "Eyes2", C.ActiveExpression.Eyes, null); CharacterSetFacialExpression(C, "Eyes2", C.ActiveExpression.Eyes);
break; break;
} }
button.setAttribute("aria-valuetext", state); button.setAttribute("aria-valuetext", state);
@ -4589,7 +4647,7 @@ class _DialogExpressionMenu extends _DialogSelfMenu {
name: group, name: group,
"aria-expanded": "false", "aria-expanded": "false",
"aria-owns": `${ids.grid}-${group}`, "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}`, id: `${ids.grid}-${group}`,
role: "radiogroup", role: "radiogroup",
"aria-required": "true", "aria-required": "true",
"aria-label": AssetGroupGet("Female3DCG", group).Description, "aria-label": AssetGroupGet("Female3DCG", group)?.Description,
}, },
dataAttributes: { name: group }, dataAttributes: { name: group },
}; };
@ -4743,7 +4801,7 @@ class _DialogExpressionMenu extends _DialogSelfMenu {
} }
CharacterSetFacialExpression(C, clickedObj.Group, clickedObj.Expression); 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 thisImg = button.querySelector("img.button-image");
const controllerImg = document.querySelector(`#${this.ids.menuLeft} [role="menuitemradio"][name="${clickedObj.Group}"] .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]) => { const children = rules.map(([name, group, logValue]) => {
if (!LogQuery(name, group)) { const val = LogValue(name, group);
if (val === null || val === undefined) {
return null; return null;
} else { } else {
return ElementCreate({ return ElementCreate({
@ -5260,7 +5319,7 @@ class _DialogOwnerRulesMenu extends _DialogSelfMenu {
dataAttributes: { name, group }, dataAttributes: { name, group },
children: [ children: [
InterfaceTextGet(`RulesMenu${name}`), 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 * 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 {Character} C - The character whose dialog option*
* @param {string} KeyWord1 - The key word to search for * @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. * keyword was not found.
* @param {boolean} [ReturnPrevious=true] - If specified, returns the previous dialog, if neither of the the two key words were 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 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. * is replaced with the player's name and 'DestinationCharacter' with the current character's name.
*/ */
function DialogFindAutoReplace(C, KeyWord1, KeyWord2, ReturnPrevious) { 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("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. * Draw the up/down arrow to bump a character up and down if they're hidden.
*/ */
function DialogDrawRepositionButton() { function DialogDrawRepositionButton() {
if (!CurrentCharacter.FocusGroup) return; if (!CurrentCharacter || !CurrentCharacter.FocusGroup) return;
let drawButton = ""; let drawButton = "";
if (CharacterAppearanceForceUpCharacter == CurrentCharacter.MemberNumber) { if (CharacterAppearanceForceUpCharacter == CurrentCharacter.MemberNumber) {
@ -5378,7 +5440,9 @@ function DialogDrawRepositionButton() {
* @param {Character} C The character currently focused. * @param {Character} C The character currently focused.
*/ */
function DialogDrawTopMenu(C) { function DialogDrawTopMenu(C) {
if (!C.FocusGroup) return;
const FocusItem = InventoryGet(C, C.FocusGroup.Name); const FocusItem = InventoryGet(C, C.FocusGroup.Name);
if (!FocusItem) return;
for (let I = DialogMenuButton.length - 1; I >= 0; I--) { for (let I = DialogMenuButton.length - 1; I >= 0; I--) {
const ButtonColor = DialogGetMenuButtonColor(DialogMenuButton[I]); const ButtonColor = DialogGetMenuButtonColor(DialogMenuButton[I]);
@ -5498,7 +5562,7 @@ function DialogDraw() {
// Customization can be used in dialog if screen is online chat room // Customization can be used in dialog if screen is online chat room
if (ServerPlayerIsInChatRoom() && ChatRoomCustomized) { if (ServerPlayerIsInChatRoom() && ChatRoomCustomized) {
const drawBGToRect = DrawShowChatRoomCustomBackground() ? { x: 0, y: 0, w: 2000, h: 1000 } : null; 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 // Check that there's actually a character selected
@ -5529,7 +5593,7 @@ function DialogDraw() {
const FocusItem = InventoryGet(C, C.FocusGroup?.Name); const FocusItem = InventoryGet(C, C.FocusGroup?.Name);
// Draws the top menu text & icons // 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); DialogDrawTopMenu(C);
// If the player is struggling or lockpicking // If the player is struggling or lockpicking
@ -5543,7 +5607,7 @@ function DialogDraw() {
DrawItemPreview(DialogStrugglePrevItem, C, 1200, 100); DrawItemPreview(DialogStrugglePrevItem, C, 1200, 100);
DrawItemPreview(DialogStruggleNextItem, C, 1200, 100); DrawItemPreview(DialogStruggleNextItem, C, 1200, 100);
} else if (DialogStrugglePrevItem || DialogStruggleNextItem) { } else if (DialogStrugglePrevItem || DialogStruggleNextItem) {
const item = DialogStrugglePrevItem || DialogStruggleNextItem; const item = /** @type {Item} */ (DialogStrugglePrevItem ?? DialogStruggleNextItem);
DrawItemPreview(item, C, 1387, 100); DrawItemPreview(item, C, 1387, 100);
} }
@ -5584,7 +5648,7 @@ function DialogDraw() {
DialogChangeMode("items"); DialogChangeMode("items");
} }
} else if ((DialogMenuMode === "colorItem" || DialogMenuMode === "colorExpression") && FocusItem) { } 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; return;
} else if (DialogMenuMode === "colorDefault") { } else if (DialogMenuMode === "colorDefault") {
return; return;
@ -5676,8 +5740,8 @@ function DialogActualNameForGroup(C, G) {
* *
* @param {Character} C * @param {Character} C
* @param {DialogStruggleActionType} Action * @param {DialogStruggleActionType} Action
* @param {Item} PrevItem * @param {Item | null} PrevItem
* @param {Item} NextItem * @param {Item | null} NextItem
*/ */
function DialogStruggleStart(C, Action, PrevItem, 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)) if ((NextItem.Craft != null) && CommonIsColor(NextItem.Craft.Color))
Color = 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); CharacterRefresh(C, true, true);
@ -5803,7 +5867,7 @@ function DialogStruggleStop(C, Game, { Progress, PrevItem, NextItem, Skill, Atte
ChatRoomPublishAction(C, DialogStruggleAction, PrevItem, NextItem); ChatRoomPublishAction(C, DialogStruggleAction, PrevItem, NextItem);
DialogChangeMode("items"); DialogChangeMode("items");
} else if ( } else if (
NextItem !== null NextItem != null
&& NextItem.Asset.Extended && NextItem.Asset.Extended
&& ( && (
NextItem.Craft == null NextItem.Craft == null

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** /**
@ -50,6 +49,7 @@ class DictionaryBuilder {
sourceCharacter(character) { sourceCharacter(character) {
if (!this._condition) return this; if (!this._condition) return this;
/** @type {SourceCharacterDictionaryEntry} */ /** @type {SourceCharacterDictionaryEntry} */
// @ts-ignore Strict-TS: Character doesn't have a MemberNumber
const entry = { SourceCharacter: character.MemberNumber }; const entry = { SourceCharacter: character.MemberNumber };
if (character.IsPlayer() && ChatRoomMapViewHasSuperPowers() && ChatRoomMapViewIsActive()) { if (character.IsPlayer() && ChatRoomMapViewHasSuperPowers() && ChatRoomMapViewIsActive()) {
entry.HasSuperPowers = true; entry.HasSuperPowers = true;
@ -86,6 +86,7 @@ class DictionaryBuilder {
if (!this._condition) return this; if (!this._condition) return this;
/** @type {TargetCharacterDictionaryEntry} */ /** @type {TargetCharacterDictionaryEntry} */
// @ts-ignore Strict-TS: Character doesn't have a MemberNumber
const entry = {TargetCharacter: character.MemberNumber}; const entry = {TargetCharacter: character.MemberNumber};
if (this._targetIndex) { if (this._targetIndex) {
entry.Index = this._targetIndex; entry.Index = this._targetIndex;
@ -223,11 +224,11 @@ class DictionaryBuilder {
* @param {number} [count] - The number of times the activity is done * @param {number} [count] - The number of times the activity is done
* @returns * @returns
*/ */
performActivity(name, group, item = null, count = 1) { performActivity(name, group, item, count = 1) {
this._addEntry({ ActivityName: name }); this._addEntry({ ActivityName: name });
this.focusGroup(group.Name); this.focusGroup(group.Name);
if (item) { if (item) {
this.asset(item.Asset, "UsedAsset", item.Craft && item.Craft.Name); this.asset(item.Asset, "UsedAsset", item.Craft?.Name);
} }
if (count > 1) if (count > 1)
this._addEntry({ ActivityCounter: count }); this._addEntry({ ActivityCounter: count });
@ -247,6 +248,7 @@ class DictionaryBuilder {
/** /**
* Adds a changeKey dictionary entry * Adds a changeKey dictionary entry
* @param {("gold" | "silver" | "bronze")[]} keys * @param {("gold" | "silver" | "bronze")[]} keys
* @param {boolean} giveKey
* @returns * @returns
*/ */
mapViewChangeKey(keys, giveKey) { mapViewChangeKey(keys, giveKey) {

View file

@ -12,7 +12,7 @@
* @type {object} * @type {object}
* @property {number} [fontSize] - The target font size. Note that if space is constrained, the actual drawn font size will be reduced * @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. * 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. * (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 * @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} * {@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 icons = Array.from(button.querySelectorAll(".button-icon"));
const iconNamesOld = icons.map(el => el.getAttribute("data-name")); const iconNamesOld = icons.map(el => el.getAttribute("data-name"));
/** @type {(InventoryIcon | null)[]} */ /** @type {(InventoryIcon | null | undefined)[]} */
const iconNamesNew = [ const iconNamesNew = [
DialogGetFavoriteStateDetails(C ?? Player, asset)?.Icon, DialogGetFavoriteStateDetails(C ?? Player, asset)?.Icon,
InventoryBlockedOrLimited(C ?? Player, item) ? "Blocked" : null, InventoryBlockedOrLimited(C ?? Player, item) ? "Blocked" : null,

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** @type {LogRecord[]} */ /** @type {LogRecord[]} */
var Log = []; var Log = [];
@ -15,25 +14,20 @@ var Log = [];
function LogAdd(NewLogName, NewLogGroup, NewLogValue, Push) { function LogAdd(NewLogName, NewLogGroup, NewLogValue, Push) {
// Makes sure the value is numeric // 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 // Checks to make sure we don't duplicate a log
var AddToLog = true; const entry = Log.find(l => l.Group === NewLogGroup && l.Name === NewLogName);
for (let L = 0; L < Log.length; L++) if (entry) {
if ((Log[L].Name == NewLogName) && (Log[L].Group == NewLogGroup)) { entry.Value = NewLogValue;
Log[L].Value = NewLogValue; } else {
AddToLog = false; /** @type {LogRecord} */
break; const newEntry = {
}
// Adds a new log object if we need to
if (AddToLog) {
var NewLog = {
Name: NewLogName, Name: NewLogName,
Group: NewLogGroup, Group: NewLogGroup,
Value: NewLogValue Value: NewLogValue
}; };
Log.push(NewLog); Log.push(newEntry);
} }
// Sends the log to the server // 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. * @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) { function LogQuery(QueryLogName, QueryLogGroup) {
for (let L = 0; L < Log.length; L++) const entry = Log.find(l => l.Group === QueryLogGroup && l.Name === QueryLogName);
if ((Log[L].Name == QueryLogName) && (Log[L].Group == QueryLogGroup)) if (!entry) return false;
if ((Log[L].Value == null) || (Log[L].Value >= CurrentTime))
return true; // Loose null-check here in case there's a null or an undefined stuck in there
return false; return entry.Value == null || entry.Value >= CurrentTime;
} }
/** /**
@ -133,13 +127,12 @@ function LogContain(LogName, LogGroup, ID) {
* @template {LogGroupType} T * @template {LogGroupType} T
* @param {LogNameType[T]} QueryLogName - The name of the log to query the value * @param {LogNameType[T]} QueryLogName - The name of the log to query the value
* @param {T} QueryLogGroup - The name of the log's group * @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) { function LogValue(QueryLogName, QueryLogGroup) {
for (let L = 0; L < Log.length; L++) const entry = Log.find(l => l.Group === QueryLogGroup && l.Name === QueryLogName);
if ((Log[L].Name == QueryLogName) && (Log[L].Group == QueryLogGroup)) if (!entry) return null;
return Log[L].Value; return entry.Value;
return null;
} }
/** /**

View file

@ -665,7 +665,7 @@ function InventoryAllow(C, asset, prerequisites = asset.Prerequisite, setDialog
/** /**
* Gets the current item / cloth worn a specific area (AssetGroup) * Gets the current item / cloth worn a specific area (AssetGroup)
* @param {Character} C - The character on which we must check the appearance * @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 * @returns {Item|null} - Returns the appearance which is the item / cloth asset, color and properties
*/ */
function InventoryGet(C, AssetGroup) { function InventoryGet(C, AssetGroup) {
@ -1390,7 +1390,7 @@ function InventoryDoesItemAllowLock(item) {
/** /**
* Applies a lock to an appearance item of a character * Applies a lock to an appearance item of a character
* @param {Character} C - The character on which the lock must be applied * @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 {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 {null|Character|string} [AppliedBy] - The character applying the lock, or message to show
* @param {boolean} [Update=true] - Whether or not to update the character * @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 {Character} C - The character on which we check the permissions
* @param {string} AssetName - The asset / item name to scan * @param {string} AssetName - The asset / item name to scan
* @param {AssetGroupName} AssetGroup - The asset group 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 * @returns {boolean} - TRUE if asset / item is blocked
*/ */
function InventoryIsPermissionBlocked(C, AssetName, AssetGroup, AssetType) { 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 {Character} C - The character on which we check the permissions
* @param {string} AssetName - The asset / item name to scan * @param {string} AssetName - The asset / item name to scan
* @param {AssetGroupName} AssetGroup - The asset group 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 * @returns {boolean} - TRUE if asset / item is a favorite
*/ */
function InventoryIsFavorite(C, AssetName, AssetGroup, AssetType) { 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 {Character} C - The character on which we check the permissions
* @param {string} AssetName - The asset / item name to scan * @param {string} AssetName - The asset / item name to scan
* @param {AssetGroupName} AssetGroup - The asset group 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 * @returns {boolean} - TRUE if asset / item is limited
*/ */
function InventoryIsPermissionLimited(C, AssetName, AssetGroup, AssetType) { 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 * 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 {Character} C - The character on which we check the limited permissions for the item
* @param {Item} Item - The item being interacted with * @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 * @returns {boolean} - TRUE if item is allowed
*/ */
function InventoryCheckLimitedPermission(C, Item, ItemType) { 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 * 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 {Character} C - The character on which we check the permissions
* @param {Item} Item - The item being interacted with * @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 * @returns {boolean} - Returns TRUE if the item cannot be used
*/ */
function InventoryBlockedOrLimited(C, Item, ItemType) { function InventoryBlockedOrLimited(C, Item, ItemType) {
@ -1711,7 +1711,7 @@ function InventoryBlockedOrLimited(C, Item, ItemType) {
* used by the player) * used by the player)
* @param {Character} C - The character whose permissions to check * @param {Character} C - The character whose permissions to check
* @param {Item} item - The item 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 * @returns {boolean} - Returns TRUE if the given item & type is limited but allowed for the player
*/ */
function InventoryIsAllowedLimited(C, item, type) { function InventoryIsAllowedLimited(C, item, type) {

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
'use strict'; 'use strict';
var KeybindingDefaults = { var KeybindingDefaults = {
@ -35,7 +34,7 @@ var KeybindingDefaults = {
(document.activeElement === null (document.activeElement === null
|| document.activeElement === document.body || document.activeElement === document.body
|| document.activeElement instanceof HTMLDialogElement) || document.activeElement instanceof HTMLDialogElement)
&& document.activeElement.id !== "InputChat" && document.activeElement?.id !== "InputChat"
}, },
{ {
id: 'isInChatRoom', 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 * 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 {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 * @returns {number[]} - An array of numbers representing the currently selected options for each of the item's modules
*/ */
function ModularItemParseCurrent({ asset, modules }, typeRecord) { function ModularItemParseCurrent({ asset, modules }, typeRecord) {

View file

@ -99,7 +99,7 @@ function NPCTraitKeepBestOption(C, Group) {
let Best = -1; let Best = -1;
let Pos = -1; let Pos = -1;
for (let D = 0; D < C.Dialog.length; D++) 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); var Value = NPCTraitGetOptionValue(C.Dialog[D].Trait, C.Trait);
if (Value > Best) { Best = Value; Pos = D; } 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 we found the best possibility, we remove all the others
if (Pos >= 0) if (Pos >= 0)
for (let D = 0; D < C.Dialog.length; D++) 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); C.Dialog.splice(D, 1);
Pos--; Pos--;
D--; D--;
@ -124,8 +124,8 @@ function NPCTraitDialog(C) {
// For each dialog option // For each dialog option
for (let D = 0; D < C.Dialog.length; D++) { 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].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].Function !== null) C.Dialog[D].Function = C.Dialog[D].Function.replace("MainHall", "");
} }
} }

View file

@ -1,4 +1,3 @@
// @ts-strict-ignore
"use strict"; "use strict";
/** /**
@ -187,16 +186,20 @@ function PreferenceGetZoneFactor(C, ZoneName) {
* Sets the arousal zone data for a specific body zone on the player * 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 {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 {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 {null | 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 | boolean} [CanOrgasm] - Sets, if the character can cum from the given zone (true) or not (false)
* @returns {void} - Nothing * @returns {void} - Nothing
*/ */
function PreferenceSetArousalZone(C, ZoneName, Factor = null, CanOrgasm = null) { function PreferenceSetArousalZone(C, ZoneName, Factor, CanOrgasm) {
// Gets the zone object // Gets the zone object
let Zone = PreferenceGetArousalZone(C, ZoneName); let Zone = PreferenceGetArousalZone(C, ZoneName);
if (!Zone) return; if (!Zone) return;
const Group = AssetGroupGet(C.AssetFamily, ZoneName); 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") { if (typeof Factor === "number") {
Zone.Factor = Factor; Zone.Factor = Factor;
@ -323,6 +326,7 @@ function PreferenceInitPlayer(C, data) {
"ControllerDPadRight", "ControllerDPadRight",
]; ];
for (const old of oldKeys) { for (const old of oldKeys) {
// @ts-ignore Strict-TS: key-based access to delete old properties
delete data.ControllerSettings[old]; delete data.ControllerSettings[old];
} }
// @ts-expect-error we don't have all the buttons // @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.OnlineSharedSettings = ValidationApplyRecord(data.OnlineSharedSettings, C, PreferenceOnlineSharedSettingsValidate, true);
C.RestrictionSettings = ValidationApplyRecord(data.RestrictionSettings, C, PreferenceRestrictionSettingsValidate); C.RestrictionSettings = ValidationApplyRecord(data.RestrictionSettings, C, PreferenceRestrictionSettingsValidate);
C.VisualSettings = ValidationApplyRecord(data.VisualSettings, C, PreferenceVisualSettingsValidate); 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); C.NotificationSettings = ValidationApplyRecord(data.NotificationSettings, C, PreferenceNotificationSettingsValidate);
// Forces some preferences depending on difficulty // Forces some preferences depending on difficulty
@ -414,7 +397,8 @@ function PreferenceInitPlayer(C, data) {
for (const [prop, stringPrefBefore] of CommonEntries(PrefBefore)) for (const [prop, stringPrefBefore] of CommonEntries(PrefBefore))
if (JSON.stringify(C[prop]) !== stringPrefBefore) 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)) if (CommonVersionUpdated && (toUpdate != null) && (toUpdate.OnlineSharedSettings != null))
toUpdate.OnlineSharedSettings.GameVersion = GameVersion; toUpdate.OnlineSharedSettings.GameVersion = GameVersion;
@ -437,23 +421,3 @@ function PreferenceInitNotificationSetting(setting, audio, defaultAlertType) {
Audio: audio, 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 {ServerAccountDataSynced["ChatSearchSettings"]} arg
* @param {Character} C * @param {Character} C
* @returns {ServerAccountDataSynced["ChatSearchSettings"]} * @returns {ChatRoomSearchSettings}
*/ */
ChatSearchSettings: (arg, C) => { ChatSearchSettings: (arg, C) => {
return ValidationApplyRecord(arg, C, ServerChatRoomSearchSettingsValidate, false); 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 {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 {StruggleKnownMinigames} MiniGame - The minigame to start
* @param {Item | null} PrevItem - The item currently being present on the character, or null if none * @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 * @param {StruggleCompletionCallback} Completion - A callback that will be called when the minigame ends
*/ */
function StruggleMinigameStart(C, MiniGame, PrevItem, NextItem, Completion) { function StruggleMinigameStart(C, MiniGame, PrevItem, NextItem, Completion) {
@ -743,8 +743,8 @@ function StruggleStrengthProcess(Decrease) {
* escapee being bound in a way. * escapee being bound in a way.
* *
* @param {Character} C - The character who tries to struggle * @param {Character} C - The character who tries to struggle
* @param {Item} PrevItem - The item, the character wants to struggle out of * @param {Item | null} PrevItem - The item, the character wants to struggle out of
* @param {Item} [NextItem] - The item that should substitute the first one * @param {Item | null} [NextItem] - The item that should substitute the first one
* @returns {{difficulty: number; auto: number; timer: number; }} - Nothing * @returns {{difficulty: number; auto: number; timer: number; }} - Nothing
*/ */
function StruggleStrengthGetDifficulty(C, PrevItem, NextItem) { function StruggleStrengthGetDifficulty(C, PrevItem, NextItem) {

View file

@ -1127,8 +1127,11 @@ function TranslationString(S, T) {
*/ */
function TranslationDialogArray(C, T) { function TranslationDialogArray(C, T) {
for (let D = 0; D < C.Dialog.length; D++) { for (let D = 0; D < C.Dialog.length; D++) {
C.Dialog[D].Option = TranslationString(C.Dialog[D].Option, T); const { Option, Result } = C.Dialog[D];
C.Dialog[D].Result = TranslationString(C.Dialog[D].Result, T); 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>>; dataAttributes?: Partial<Record<string, number | string | boolean>>;
/** CSS style declarations that will be set on the HTML element (see {@link HTMLElement.style}). */ /** 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}). */ /** 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 }; 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}). */ /** 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. */ /** 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; reset?: boolean;
/** The to-be assigned custom status message */ /** 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 */ /** Display the {@link ReloadOptions.status} message on a timer; units are in ms */
statusTimer?: number; statusTimer?: number;
/** /**
@ -1796,13 +1796,13 @@ type ScriptPermissions = Record<ScriptPermissionProperty, ScriptPermission>;
interface DialogLine { interface DialogLine {
Stage: string; Stage: string;
NextStage: string; NextStage: string | null;
Option: string; Option: string | null;
Result: string; Result: string | null;
Function: string; Function: string | null;
Prerequisite: string; Prerequisite: string | null;
Group: string; Group: string | null;
Trait: string; Trait: string | null;
} }
interface DialogInfo<T extends ModuleType> { interface DialogInfo<T extends ModuleType> {
@ -2076,8 +2076,8 @@ interface Character {
CanPickLocks: () => boolean; CanPickLocks: () => boolean;
IsEdged: () => boolean; IsEdged: () => boolean;
IsPlayer: () => this is PlayerCharacter; IsPlayer: () => this is PlayerCharacter;
get X(): number | null; get X(): number;
get Y(): number | null; get Y(): number;
set X(X: number); set X(X: number);
set Y(Y: number); set Y(Y: number);
get Position(): ChatRoomMapPos | null; get Position(): ChatRoomMapPos | null;
@ -2115,19 +2115,19 @@ interface Character {
/** /**
* Check if the player is ghosting the given target character (or member number) * 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) * 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) * 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) * 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 * Check if this character is ghosted by the player
*/ */
@ -2389,7 +2389,7 @@ interface PlayerCharacter extends Character {
GhostList: number[]; GhostList: number[];
Wardrobe: (ItemBundle[] | null)[]; Wardrobe: (ItemBundle[] | null)[];
WardrobeCharacterNames: string[]; WardrobeCharacterNames: string[];
SavedExpressions?: ({ Group: ExpressionGroupName, CurrentExpression?: ExpressionName }[] | null)[]; SavedExpressions: ({ Group: ExpressionGroupName, CurrentExpression?: ExpressionName }[] | null)[];
SavedColors: HSVColor[]; SavedColors: HSVColor[];
FriendList: number[]; FriendList: number[];
FriendNames: Map<number, string>; 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 ShowRoomCustomization: 0 | 1 | 2 | 3; // 0 - Never, 1 - No by default, 2 - Yes by default, 3 - Always
FriendListAutoRefresh: boolean; FriendListAutoRefresh: boolean;
DefaultChatRoomBackground: string; DefaultChatRoomBackground: string;
/**
* @deprecated
*/
SearchShowsFullRooms: never;
} }
/** Pandora Player extension */ /** Pandora Player extension */
@ -4726,14 +4722,14 @@ interface ColorPickerInitOptions {
dispatch?: boolean; dispatch?: boolean;
} }
//#end region // #end region
// #region Log // #region Log
interface LogRecord { interface LogRecord {
Name: LogNameType[LogGroupType]; Name: LogNameType[LogGroupType];
Group: LogGroupType; Group: LogGroupType;
Value: number; Value: number | undefined;
} }
/** The logging groups as supported by the {@link LogRecord.Group} */ /** The logging groups as supported by the {@link LogRecord.Group} */
@ -4825,7 +4821,7 @@ interface LogNameType {
interface FavoriteState { interface FavoriteState {
TargetFavorite: boolean; TargetFavorite: boolean;
PlayerFavorite: boolean; PlayerFavorite: boolean;
Icon: FavoriteIcon; Icon?: FavoriteIcon;
UsableOrder: DialogSortOrder; UsableOrder: DialogSortOrder;
UnusableOrder: DialogSortOrder; UnusableOrder: DialogSortOrder;
} }
@ -4922,9 +4918,9 @@ interface ArousalSettingsType {
Activity: string; Activity: string;
Zone: string; Zone: string;
Fetish: string; Fetish: string;
OrgasmTimer?: number; OrgasmTimer: number;
OrgasmStage?: 0 | 1 | 2; OrgasmStage: 0 | 1 | 2;
OrgasmCount?: number; OrgasmCount: number;
DisableAdvancedVibes: boolean; DisableAdvancedVibes: boolean;
} }