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