diff --git a/BondageClub/Assets/Female3DCG/AssetStrings.csv b/BondageClub/Assets/Female3DCG/AssetStrings.csv index 36421d330f..b4f1f66716 100644 --- a/BondageClub/Assets/Female3DCG/AssetStrings.csv +++ b/BondageClub/Assets/Female3DCG/AssetStrings.csv @@ -6448,7 +6448,8 @@ ItemHoodCreepyIronMaskSelectBase,Creepy Iron Mask main menu: ItemHoodCreepyIronMaskModuleMode,Mask style ItemHoodCreepyIronMaskSelectMode,Mask menu: ItemHoodCreepyIronMaskOptionm0,Just mask -ItemHoodCreepyIronMaskOptionm1,Full hood +ItemHoodCreepyIronMaskOptionm1,Semi-hood +ItemHoodCreepyIronMaskOptionm2,Full hood ItemHoodCreepyIronMaskModuleBlindfold,Blindfold ItemHoodCreepyIronMaskSelectBlindfold,Metal blindfold option: ItemHoodCreepyIronMaskOptionb0,None @@ -6462,8 +6463,13 @@ ItemHoodCreepyIronMaskModuleNose,Nose guard ItemHoodCreepyIronMaskSelectNose,Nose guard option: ItemHoodCreepyIronMaskOptionn0,None ItemHoodCreepyIronMaskOptionn1,Add nose guard +ItemHoodCreepyIronMaskModuleSpeech,Speech +ItemHoodCreepyIronMaskSelectSpeech,Speech option: +ItemHoodCreepyIronMaskOptionp0,Loose +ItemHoodCreepyIronMaskOptionp1,Tight ItemHoodCreepyIronMaskSetm0,SourceCharacter puts the Creepy Iron Mask on DestinationCharacterName head. ItemHoodCreepyIronMaskSetm1,SourceCharacter locks DestinationCharacterName head with the Creepy Iron Hood version. +ItemHoodCreepyIronMaskSetm2,SourceCharacter locks DestinationCharacterName head with the Creepy Iron Hood version. ItemHoodCreepyIronMaskSetb0,SourceCharacter removes a metal blindfold from DestinationCharacterName Creepy Iron Mask. ItemHoodCreepyIronMaskSetb1,SourceCharacter equips a perforated metal blindfold on DestinationCharacterName Creepy Iron Mask. ItemHoodCreepyIronMaskSetb2,SourceCharacter equips a full metal blindfold on DestinationCharacterName Creepy Iron Mask. @@ -6471,6 +6477,8 @@ ItemHoodCreepyIronMaskSets0,SourceCharacter removes a spiked collar from Destina ItemHoodCreepyIronMaskSets1,SourceCharacter equips a spiked collar on DestinationCharacterName neck. ItemHoodCreepyIronMaskSetn0,SourceCharacter removes a nose guard from DestinationCharacterName Creepy Iron Mask. ItemHoodCreepyIronMaskSetn1,SourceCharacter equips a nose guard on DestinationCharacterName Creepy Iron Mask. +ItemHoodCreepyIronMaskSetp0,SourceCharacter loosens DestinationCharacterName Creepy Iron Mask. +ItemHoodCreepyIronMaskSetp1,SourceCharacter tightens DestinationCharacterName Creepy Iron Mask. ItemMouthHorrorMuzzleSelect,Horror Muzzle style option menu: ItemMouthHorrorMuzzleNone,Plain ItemMouthHorrorMuzzleRivets,With rivets @@ -6521,12 +6529,12 @@ ItemHandheldKyosensuType3,Sun ItemHandheldKyosensuType4,Wave art ItemHandheldKyosensuType5,Moon ItemHandheldKyosensuType6,Flowers -ItemItemHandheldKyosensuSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Kyo-sensu fan. -ItemItemHandheldKyosensuSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Kyo-sensu fan. -ItemItemHandheldKyosensuSetType3,SourceCharacter chooses a sun art for DestinationCharacter Kyo-sensu fan. -ItemItemHandheldKyosensuSetType4,SourceCharacter chooses a wave art for DestinationCharacter Kyo-sensu fan. -ItemItemHandheldKyosensuSetType5,SourceCharacter chooses a moon art for DestinationCharacter Kyo-sensu fan. -ItemItemHandheldKyosensuSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Kyo-sensu fan. +ItemHandheldKyosensuSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Kyo-sensu fan. +ItemHandheldKyosensuSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Kyo-sensu fan. +ItemHandheldKyosensuSetType3,SourceCharacter chooses a sun art for DestinationCharacter Kyo-sensu fan. +ItemHandheldKyosensuSetType4,SourceCharacter chooses a wave art for DestinationCharacter Kyo-sensu fan. +ItemHandheldKyosensuSetType5,SourceCharacter chooses a moon art for DestinationCharacter Kyo-sensu fan. +ItemHandheldKyosensuSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Kyo-sensu fan. ItemHandheldUchiwaSelect,Choose your Uchiwa paper art: ItemHandheldUchiwaType1,Cherry blossoms ItemHandheldUchiwaType2,Lightning bolt @@ -6534,9 +6542,9 @@ ItemHandheldUchiwaType3,Sun ItemHandheldUchiwaType4,Wave art ItemHandheldUchiwaType5,Moon ItemHandheldUchiwaType6,Flowers -ItemItemHandheldUchiwaSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Uchiwa fan. -ItemItemHandheldUchiwaSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Uchiwa fan. -ItemItemHandheldUchiwaSetType3,SourceCharacter chooses a sun art for DestinationCharacter Uchiwa fan. -ItemItemHandheldUchiwaSetType4,SourceCharacter chooses a wave art for DestinationCharacter Uchiwa fan. -ItemItemHandheldUchiwaSetType5,SourceCharacter chooses a moon art for DestinationCharacter Uchiwa fan. -ItemItemHandheldUchiwaSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Uchiwa fan. \ No newline at end of file +ItemHandheldUchiwaSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Uchiwa fan. +ItemHandheldUchiwaSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Uchiwa fan. +ItemHandheldUchiwaSetType3,SourceCharacter chooses a sun art for DestinationCharacter Uchiwa fan. +ItemHandheldUchiwaSetType4,SourceCharacter chooses a wave art for DestinationCharacter Uchiwa fan. +ItemHandheldUchiwaSetType5,SourceCharacter chooses a moon art for DestinationCharacter Uchiwa fan. +ItemHandheldUchiwaSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Uchiwa fan. diff --git a/BondageClub/Assets/Female3DCG/Female3DCG.js b/BondageClub/Assets/Female3DCG/Female3DCG.js index 551ddeec31..a37b542a64 100644 --- a/BondageClub/Assets/Female3DCG/Female3DCG.js +++ b/BondageClub/Assets/Female3DCG/Female3DCG.js @@ -17386,36 +17386,6 @@ var AssetFemale3DCG = [ }, ], }, - { - Name: "AsylumBlindfold", - InventoryID: 1223, - BuyGroup: "AsylumMuzzle", - Top: 25, - Value: -1, - Difficulty: 3, - Time: 10, - DefaultColor: ["#BFBFB1", "#95866D", "#A0A0A0", "#877C66"], - Extended: true, - Random: true, - AllowLock: true, - AllowTighten: true, - DrawLocks: false, - Audio: "Buckle", - Hide: ["Glasses"], - ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ], - Layer: [ - { Name: "Main", Priority: 35, AllowColorize: true }, - { - Name: "Pad", - Priority: 34, - CreateLayerTypes: ["typed"], - AllowTypes: { typed: 1 }, - AllowColorize: true, - }, - { Name: "Metal", Priority: 35, AllowColorize: true }, - { Name: "Lock", Priority: 35, LockLayer: true, AllowColorize: true }, - ], - }, ], Color: [ "Default", @@ -38081,7 +38051,10 @@ var AssetFemale3DCG = [ AllowTighten: true, DrawLocks: false, Audio: "Buckle", - ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ], + ExpressionTrigger: [ + { Name: "Soft", Group: "Eyebrows", Timer: 15 }, + { Name: "Shy", Group: "Eyes", Timer: 15 }, + ], Layer: [ { Name: "Main", Priority: 35, AllowColorize: true }, { @@ -41813,7 +41786,10 @@ var AssetFemale3DCG = [ Effect: [E.BlockMouth], Prerequisite: ["GagFlat"], Hide: ["ItemNose"], - ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ], + ExpressionTrigger: [ + { Name: "Soft", Group: "Eyebrows", Timer: 15 }, + { Name: "Shy", Group: "Eyes", Timer: 15 }, + ], Layer: [ { Name: "Main", Priority: 38, AllowColorize: true }, { @@ -45775,7 +45751,10 @@ var AssetFemale3DCG = [ Effect: [E.BlockMouth], Prerequisite: ["GagFlat"], Hide: ["ItemNose"], - ExpressionTrigger: [ { Name: "Soft", Group: "Eyebrows", Timer: 15 },{ Name: "Shy", Group: "Eyes", Timer: 15 } ], + ExpressionTrigger: [ + { Name: "Soft", Group: "Eyebrows", Timer: 15 }, + { Name: "Shy", Group: "Eyes", Timer: 15 }, + ], Layer: [ { Name: "Main", Priority: 39, AllowColorize: true }, { @@ -47022,6 +47001,39 @@ var AssetFemale3DCG = [ }, ], }, + { + Name: "AsylumBlindfold", + InventoryID: 1223, + BuyGroup: "AsylumMuzzle", + Top: 25, + Value: -1, + Difficulty: 3, + Time: 10, + DefaultColor: ["#BFBFB1", "#95866D", "#A0A0A0", "#877C66"], + Extended: true, + Random: true, + AllowLock: true, + AllowTighten: true, + DrawLocks: false, + Audio: "Buckle", + Hide: ["Glasses"], + ExpressionTrigger: [ + { Name: "Soft", Group: "Eyebrows", Timer: 15 }, + { Name: "Shy", Group: "Eyes", Timer: 15 }, + ], + Layer: [ + { Name: "Main", Priority: 35, AllowColorize: true }, + { + Name: "Pad", + Priority: 34, + CreateLayerTypes: ["typed"], + AllowTypes: { typed: 1 }, + AllowColorize: true, + }, + { Name: "Metal", Priority: 35, AllowColorize: true }, + { Name: "Lock", Priority: 35, LockLayer: true, AllowColorize: true }, + ], + }, ], Color: [ "Default", diff --git a/BondageClub/Assets/Female3DCG/Female3DCGExtended.js b/BondageClub/Assets/Female3DCG/Female3DCGExtended.js index 840656d0a0..3b1e54ab50 100644 --- a/BondageClub/Assets/Female3DCG/Female3DCGExtended.js +++ b/BondageClub/Assets/Female3DCG/Female3DCGExtended.js @@ -9067,7 +9067,7 @@ var AssetFemale3DCGExtended = { // Semi-Hood Property: { Effect: [E.BlockMouth], - Hide: ["HairFront","HairAccessory1"], + Hide: ["HairFront", "HairAccessory1"], }, }, { diff --git a/BondageClub/Assets/Female3DCG/Female3DCG_Types.d.ts b/BondageClub/Assets/Female3DCG/Female3DCG_Types.d.ts index 823909e627..e481b21222 100644 --- a/BondageClub/Assets/Female3DCG/Female3DCG_Types.d.ts +++ b/BondageClub/Assets/Female3DCG/Female3DCG_Types.d.ts @@ -657,6 +657,7 @@ interface AssetDefinitionBase extends AssetCommonPropertiesGroupAsset, AssetComm AllowHideItem?: string[]; /** @deprecated */ AllowTypes?: never; + /** A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files. */ CreateLayerTypes?: string[]; /** * Whether an item can be tightened or not. @@ -819,7 +820,7 @@ interface AssetLayerDefinition extends AssetCommonPropertiesGroupAssetLayer, Ass /** Whether the layer is hidden in the Color Picker UI. Defaults to false. */ HideColoring?: boolean; - /** A record (or a list thereof) with all screen names + option indices that should make the layer visible */ + /** A record (or a list thereof) with all screen names + option indices, _i.e._ {@link TypeRecord} keys + values, that should make the layer visible */ AllowTypes?: AllowTypes.Definition; /** @@ -867,6 +868,11 @@ interface AssetLayerDefinition extends AssetCommonPropertiesGroupAssetLayer, Ass */ CopyLayerPoseMapping?: string; + /** + * A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files. + * + * By default files are expected for _all_ option indices associated with the key(s), unless the valid option set has been narrowed down according to {@link AssetLayerDefinition.AllowTypes}. + */ CreateLayerTypes?: string[]; /* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */ HideForAttribute?: AssetAttribute[]; diff --git a/BondageClub/Assets/Female3DCG/ItemHandheld/Kyosensu_FanStencil.png b/BondageClub/Assets/Female3DCG/ItemHandheld/Kyosensu_FanStencil.png deleted file mode 100644 index 64349c5e31..0000000000 Binary files a/BondageClub/Assets/Female3DCG/ItemHandheld/Kyosensu_FanStencil.png and /dev/null differ diff --git a/BondageClub/Assets/Female3DCG/ItemHandheld/Uchiwa_FanStencil.png b/BondageClub/Assets/Female3DCG/ItemHandheld/Uchiwa_FanStencil.png deleted file mode 100644 index 686704de3e..0000000000 Binary files a/BondageClub/Assets/Female3DCG/ItemHandheld/Uchiwa_FanStencil.png and /dev/null differ diff --git a/BondageClub/CSS/FriendList.css b/BondageClub/CSS/FriendList.css index 5745a58e42..1029007321 100644 --- a/BondageClub/CSS/FriendList.css +++ b/BondageClub/CSS/FriendList.css @@ -33,6 +33,7 @@ padding: var(--small-gap); width: 20%; text-align: center; + user-select: none; } #friend-list-search-input { @@ -61,17 +62,19 @@ /* #endregion */ /* #region HEADER */ + #friend-list-header { - min-height: var(--row-height); - display: flex; - align-items: center; - color: var(--text-color); + user-select: none; } #friend-list-header .friend-list-link { text-decoration: none; } +#friend-list-header .friend-list-row:hover { + color: var(--text-color); +} + #friend-list-header-hr { width: 80%; } @@ -105,6 +108,60 @@ #friend-list-beep-dialog:not([data-received]) .fl-beep-sent-content { display: block; } + +.ChatRoomType { + display: flex; + justify-content: center; + gap: 0.15em; + user-select: none; +} + +.friend-list-icon-container { + position: relative; + height: var(--row-height); + width: var(--row-height); + max-width: 86px; + max-height: 86px; + aspect-ratio: 1 / 1; +} + +.friend-list-icon-container > .button-tooltip { + --tooltip-gap: 0.15em; +} + +.friend-list-icon { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +@media (hover: hover) { + @supports selector(:has(*)) { + .friend-list-icon-container:hover:not(:has(.button-tooltip:hover)) > .button-tooltip { + visibility: visible; + } + } + + @supports not selector(:has(*)) { + .friend-list-icon-container:hover > .button-tooltip { + visibility: visible; + } + } +} + +.friend-list-icon-small { + pointer-events: none; + height: var(--row-height); + width: var(--row-height); + max-width: 50px; + max-height: 50px; + margin-inline: 0.15em; + aspect-ratio: 1 / 1; +} + /* #endregion */ /* #region FRIENDLIST */ @@ -124,17 +181,27 @@ width: calc(100% - var(--button-size)); } -.friend-list-row { - color: var(--text-color); - display: flex; - flex-direction: row; - justify-content: space-evenly; +#friend-list-table th { + font-weight: normal; } -.friend-list-row * { +.friend-list-row { + color: var(--text-color); + min-height: var(--row-height); + display: flex; + align-items: center; + flex-direction: row; + justify-content: space-evenly; + padding: unset; margin: var(--small-gap) 0; } +.friend-list-row > * { + vertical-align: middle; + text-justify: center; + padding: unset; +} + .friend-list-row:hover { color: yellow; } @@ -147,10 +214,6 @@ white-space: preserve; } -#friend-list .friend-list-column { - height: 100%; -} - #friend-list-member-number, .MemberNumber { width: 10%; @@ -168,8 +231,8 @@ gap: var(--small-gap); } -.RelationType img { - height: min(5dvh, 2.5dvw); +.RelationType { + user-select: none; } .friend-list-link { diff --git a/BondageClub/CSS/Styles.css b/BondageClub/CSS/Styles.css index 2a5633327e..891e1bca3c 100644 --- a/BondageClub/CSS/Styles.css +++ b/BondageClub/CSS/Styles.css @@ -223,41 +223,58 @@ select:invalid:not(:disabled):not(:read-only) { height: 1.2em; } -input[type="checkbox"] { +.checkbox { + /* Default background color */ + --checkbox-color: white; + /* Background color for disabled buttons */ + --disabled-color: grey; + /* Background color for hovered and/or active buttons */ + --hover-color: cyan; + -webkit-appearance: none; appearance: none; - background-color: #fff; - margin: 0; + background-color: var(--checkbox-color); + position: relative; font: inherit; color: black; width: min(6vh, 3vw); height: min(6vh, 3vw); - border: min(0.3vh, 0.15vw) solid black; - display: grid; - place-content: center; + border: min(0.2vh, 0.1vw) solid black; cursor: pointer; } -input[type="checkbox"]:hover { - background-color: cyan; +.checkbox:hover { + background-color: var(--hover-color); } -input[type="checkbox"]:disabled { - background-color: lightgray; +.checkbox:disabled { + background-color: var(--disabled-color); cursor: auto; } -input[type="checkbox"]::before { +.checkbox::before { content: ""; - width: min(4.6vh, 2.3vw); - height: min(4.6vh, 2.3vw); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); - transform: scale(0); background-color: black; + width: 90%; + height: 90%; + visibility: hidden; } -input[type="checkbox"]:checked::before { - transform: scale(1); +.checkbox:checked::before { + visibility: visible; +} + +@supports (height: 100dvh) { + .checkbox { + width: min(6dvh, 3dvw); + height: min(6dvh, 3dvw); + border-width: min(0.2dvh, 0.1dvw); + } } /* Dropdown */ diff --git a/BondageClub/Icons/FemaleInverted.png b/BondageClub/Icons/FemaleInverted.png new file mode 100644 index 0000000000..1d89fee7c7 Binary files /dev/null and b/BondageClub/Icons/FemaleInverted.png differ diff --git a/BondageClub/Icons/GenderInvert.png b/BondageClub/Icons/GenderInvert.png new file mode 100644 index 0000000000..60a4145fc9 Binary files /dev/null and b/BondageClub/Icons/GenderInvert.png differ diff --git a/BondageClub/Icons/MaleInverted.png b/BondageClub/Icons/MaleInverted.png new file mode 100644 index 0000000000..f32bddc6b9 Binary files /dev/null and b/BondageClub/Icons/MaleInverted.png differ diff --git a/BondageClub/Icons/PrivateInvert.png b/BondageClub/Icons/PrivateInvert.png new file mode 100644 index 0000000000..aea74c9ba4 Binary files /dev/null and b/BondageClub/Icons/PrivateInvert.png differ diff --git a/BondageClub/Screens/Character/FriendList/FriendList.d.ts b/BondageClub/Screens/Character/FriendList/FriendList.d.ts index dc71913876..f6ef4e1525 100644 --- a/BondageClub/Screens/Character/FriendList/FriendList.d.ts +++ b/BondageClub/Screens/Character/FriendList/FriendList.d.ts @@ -1,6 +1,6 @@ type FriendListModes = FriendListMode[]; type FriendListMode = "OnlineFriends" | "Beeps" | "AllFriends"; -type FriendListSortingMode = 'None' | 'MemberName' | 'MemberNickname' | 'MemberNumber' | 'ChatRoomName' | 'RelationType'; +type FriendListSortingMode = 'None' | 'MemberName' | 'MemberNickname' | 'MemberNumber' | 'ChatRoomName' | 'RelationType' | 'ChatRoomType'; type FriendListSortingDirection = 'Asc' | 'Desc'; type FriendListReturn<T extends ModuleType> = { Screen: ModuleScreens[T], Module: T, IsInChatRoom?: boolean, hasScrolledChat?: boolean }; @@ -19,6 +19,7 @@ type FriendRawRoom = { name?: string; caption: string; canSearchRoom: boolean; + types: FriendListIcon[]; }; type FriendRawBeep = { @@ -27,3 +28,12 @@ type FriendRawBeep = { hasMessage?: boolean; canBeep?: boolean; }; + +interface FriendListIcon { + /** The {@link HTMLImageElement.src} of the icon */ + src: string; + /** The `Character/FriendList` {@link TextGet} key of the icon's tooltip */ + tooltipKey: string; + /** A string to-be used for sorting the icon-containing column cells */ + sortKey: string; +} diff --git a/BondageClub/Screens/Character/FriendList/FriendList.js b/BondageClub/Screens/Character/FriendList/FriendList.js index accc1b3117..a4db0c57dd 100644 --- a/BondageClub/Screens/Character/FriendList/FriendList.js +++ b/BondageClub/Screens/Character/FriendList/FriendList.js @@ -90,7 +90,7 @@ function FriendListLoad() { tag: 'input', attributes: { id: FriendListIDs.searchInput, - type: 'text', + type: 'search', maxLength: 100, }, eventListeners: { @@ -198,71 +198,87 @@ function FriendListLoad() { } ), ElementCreate({ - tag: 'div', + tag: 'table', attributes: { - id: FriendListIDs.friendListTable + id: FriendListIDs.friendListTable, + "aria-labelledby": FriendListIDs.modeTitle, }, children: [ { - tag: 'div', + tag: 'thead', attributes: { id: FriendListIDs.header }, - classList: ['friend-list-row'], children: [ - ElementButton.Create( - "friend-list-member-name", - () => FriendListChangeSortingMode("MemberName"), - { noStyling: true }, - { button: { - classList: ['friend-list-column', 'friend-list-link'], - }}, - ), - ElementButton.Create( - "friend-list-member-number", - () => FriendListChangeSortingMode("MemberNumber"), - { noStyling: true }, - { button: { - classList: ['friend-list-column', 'friend-list-link'], - }}, - ), - ElementButton.Create( - "friend-list-chat-room-name", - () => FriendListChangeSortingMode("ChatRoomName"), - { noStyling: true }, - { button: { - classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'], - }}, - ), - ElementButton.Create( - "friend-list-relation-type", - () => FriendListChangeSortingMode("RelationType"), - { noStyling: true }, - { button: { - classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'], - }}, - ), { - tag: "span", - classList: ['friend-list-column', 'mode-specific-content', 'fl-online-friends-content'], + tag: "tr", + classList: ["friend-list-row"], children: [ - TextGet("ActionFriends") + ElementButton.Create( + "friend-list-member-name", + () => FriendListChangeSortingMode("MemberName"), + { noStyling: true }, + { button: { + classList: ['friend-list-column', 'friend-list-link'], + attributes: { role: "columnheader" }, + }}, + ), + ElementButton.Create( + "friend-list-member-number", + () => FriendListChangeSortingMode("MemberNumber"), + { noStyling: true }, + { button: { + classList: ['friend-list-column', 'friend-list-link'], + attributes: { role: "columnheader" }, + }}, + ), + ElementButton.Create( + "friend-list-chat-room-type", + () => FriendListChangeSortingMode("ChatRoomType"), + { noStyling: true }, + { button: { + classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'], + attributes: { role: "columnheader" }, + }}, + ), + ElementButton.Create( + "friend-list-chat-room-name", + () => FriendListChangeSortingMode("ChatRoomName"), + { noStyling: true }, + { button: { + classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'], + attributes: { role: "columnheader" }, + }}, + ), + ElementButton.Create( + "friend-list-relation-type", + () => FriendListChangeSortingMode("RelationType"), + { noStyling: true }, + { button: { + classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'], + attributes: { role: "columnheader" }, + }}, + ), + { + tag: "th", + classList: ['friend-list-column', 'mode-specific-content', 'fl-online-friends-content'], + attributes: { scope: "col" }, + children: [TextGet("ActionFriends")], + }, + { + tag: "th", + classList: ['friend-list-column', 'mode-specific-content', 'fl-beeps-content'], + attributes: { scope: "col" }, + children: [TextGet("ActionRead")], + }, + { + tag: "th", + classList: ['friend-list-column', 'mode-specific-content', 'fl-all-friends-content'], + attributes: { scope: "col" }, + children: [TextGet("ActionDelete")], + }, ], }, - { - tag: "span", - classList: ['friend-list-column', 'mode-specific-content', 'fl-beeps-content'], - children: [ - TextGet("ActionRead") - ], - }, - { - tag: "span", - classList: ['friend-list-column', 'mode-specific-content', 'fl-all-friends-content'], - children: [ - TextGet("ActionDelete") - ], - } ] }, { @@ -272,7 +288,7 @@ function FriendListLoad() { } }, { - tag: 'div', + tag: 'tbody', classList: ["scroll-box"], attributes: { id: FriendListIDs.friendList @@ -504,7 +520,7 @@ function FriendListBeepMenuSend() { ChatRoomName: FriendListBeepShowRoom ? ChatRoomData?.Name : undefined, ChatRoomSpace: FriendListBeepShowRoom ? ChatRoomData?.Space : undefined, Sent: true, - Private: false, + Private: FriendListBeepShowRoom ? !ChatRoomData?.Visibility.includes("All") : undefined, Time: new Date(), Message: msg || undefined }); @@ -537,6 +553,15 @@ function FriendListChatSearch(room) { ElementValue("InputSearch", ChatSearchMuffle(room)); } +/** @satisfies {{ [key in (ServerChatRoomSpace | "Private")]: FriendListIcon }} */ +const FriendListIconMapping = { + "": { src: "./Icons/FemaleInvert.png", tooltipKey: "TypeFemale", sortKey: "F " }, + M: { src: "./Icons/MaleInvert.png", tooltipKey: "TypeMale", sortKey: "M " }, + X: { src: "./Icons/GenderInvert.png", tooltipKey: "TypeMixed", sortKey: "FM" }, + Asylum: { src: "./Icons/Asylum.png", tooltipKey: "TypeAsylum", sortKey: "A " }, + Private: { src: "./Icons/PrivateInvert.png", tooltipKey: "TypePrivate", sortKey: "P" }, +}; + /** * Loads the friend list data into the HTML div element. * @param {ServerFriendInfo[]} data - An array of data, we receive from the server @@ -559,7 +584,6 @@ function FriendListLoadFriendList(data) { const BeepCaption = InterfaceTextGet("Beep"); const DeleteCaption = InterfaceTextGet("Delete"); const ConfirmDeleteCaption = InterfaceTextGet("ConfirmDelete"); - const PrivateRoomCaption = InterfaceTextGet("PrivateRoom"); const SentCaption = InterfaceTextGet("SentBeep"); const ReceivedCaption = InterfaceTextGet("ReceivedBeep"); const MailCaption = InterfaceTextGet("BeepWithMail"); @@ -601,15 +625,28 @@ function FriendListLoadFriendList(data) { }); if (infoChanged) ServerPlayerRelationsSync(); + /** @satisfies {Record<string, FriendListSortingMode>} */ const columnHeaders = { - "friend-list-member-name": `${TextGet("MemberName")} ${FriendListSortingMode === "MemberName" ? sortingSymbol : "↕"}`, - "friend-list-member-number": `${TextGet("MemberNumber")} ${FriendListSortingMode === "MemberNumber" ? sortingSymbol : "↕"}`, - "friend-list-chat-room-name": `${TextGet("ChatRoomName")} ${FriendListSortingMode === "ChatRoomName" ? sortingSymbol : "↕"}`, - "friend-list-relation-type": `${TextGet("FriendType")} ${FriendListSortingMode === "RelationType" ? sortingSymbol : "↕"}`, + "friend-list-member-name": "MemberName", + "friend-list-member-number": "MemberNumber", + "friend-list-chat-room-name": "ChatRoomName", + "friend-list-chat-room-type": "ChatRoomType", + "friend-list-relation-type": "RelationType", }; - CommonEntries(columnHeaders).forEach(([id, textContent]) => { + CommonEntries(columnHeaders).forEach(([id, modeName]) => { const elem = document.getElementById(id); - elem.textContent = textContent; + const elemSortingSymbol = FriendListSortingMode === modeName ? sortingSymbol : "↕"; + elem.textContent = `${TextGet(modeName)} ${elemSortingSymbol}`; + switch (elemSortingSymbol) { + case "↑": + elem.setAttribute("aria-sort", "ascending"); + break; + case "↓": + elem.setAttribute("aria-sort", "descending"); + break; + default: + elem.setAttribute("aria-sort", "none"); + } }); /** @type {FriendRawData[]} */ @@ -621,25 +658,20 @@ function FriendListLoadFriendList(data) { const originalChatRoomName = friend.ChatRoomName || ''; const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`); const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined); - let caption = ''; const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (friend.ChatRoomSpace || ''); const canBeep = true; - const rawCaption = []; - if (chatRoomSpaceCaption && chatRoomName) rawCaption.push(`<i>${chatRoomSpaceCaption}</i>`); - if (friend.Private) rawCaption.push(PrivateRoomCaption); - if (chatRoomName) rawCaption.push(chatRoomName); - if (rawCaption.length === 0) rawCaption.push('-'); - - caption = rawCaption.join(' - '); - friendRawData.push({ memberName: friend.MemberName, memberNumber: friend.MemberNumber, chatRoom: { name: originalChatRoomName, - caption: caption, + caption: chatRoomName || "-", canSearchRoom: canSearchRoom, + types: [ + chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[friend.ChatRoomSpace ?? ""] : null, + friend.Private ? FriendListIconMapping.Private : null, + ].filter(Boolean), }, beep: { canBeep: canBeep, @@ -653,18 +685,9 @@ function FriendListLoadFriendList(data) { const beepData = FriendListBeepLog[i]; const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${beepData.ChatRoomSpace || "F"}`); const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined); - let chatRoomCaption = ''; let beepCaption = ''; const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (beepData.ChatRoomSpace || ''); - const rawRoomCaption = []; - if (chatRoomSpaceCaption && chatRoomName) rawRoomCaption.push(`<i>${chatRoomSpaceCaption}</i>`); - if (beepData.Private) rawRoomCaption.push(PrivateRoomCaption); - if (chatRoomName) rawRoomCaption.push(chatRoomName); - if (rawRoomCaption.length === 0) rawRoomCaption.push('-'); - - chatRoomCaption = rawRoomCaption.join(' - '); - const rawBeepCaption = []; if (beepData.Sent) { rawBeepCaption.push(SentCaption); @@ -683,8 +706,12 @@ function FriendListLoadFriendList(data) { memberNumber: beepData.MemberNumber, chatRoom: { name: beepData.ChatRoomName, - caption: chatRoomCaption, + caption: chatRoomName || "-", canSearchRoom: canSearchRoom, + types: [ + chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[beepData.ChatRoomSpace ?? ""] : null, + beepData.Private ? FriendListIconMapping.Private : null, + ].filter(Boolean), }, beep: { beepIndex: i, @@ -718,18 +745,18 @@ function FriendListLoadFriendList(data) { friendRawData.forEach(friend => { const row = ElementCreate({ - tag: "div", + tag: "tr", classList: ['friend-list-row'], children: [ { - tag: "span", + tag: "td", classList: ['friend-list-column', 'MemberName'], children: [ friend.memberName ], }, { - tag: "span", + tag: "td", classList: ['friend-list-column', 'MemberNumber'], children: [ friend.memberNumber.toString() @@ -740,20 +767,96 @@ function FriendListLoadFriendList(data) { if (friend.chatRoom) { if (!friend.chatRoom.name || !friend.chatRoom.canSearchRoom) { - row.appendChild(ElementCreate({ - tag: "span", - classList: ['friend-list-column', 'ChatRoomName'], - innerHTML: friend.chatRoom.caption, - })); + // Sorting is performed via each cell's `textContent`, + // so explicitly prepend an invisible node with some sorting key + let totalSortKey = ""; + const imgContainer = ElementCreate({ + tag: "td", + classList: ['friend-list-column', 'ChatRoomType'], + children: [ + { tag: "span", style: { display: "none" }, classList: ["friend-list-sorting-node"] }, + ...friend.chatRoom.types.map(({ src, tooltipKey, sortKey }) => { + totalSortKey += sortKey; + return { + tag: /** @type {const} */("div"), + classList: ["friend-list-icon-container"], + children: [ + { + tag: /** @type {const} */("img"), + attributes: { src, decoding: "async", loading: "lazy", alt: TextGet(tooltipKey) }, + classList: ["friend-list-icon"], + }, + { + tag: /** @type {const} */("div"), + attributes: { role: "tooltip", "aria-hidden": "true" }, + children: [TextGet(tooltipKey)], + classList: ["button-tooltip", "button-tooltip-right"], + }, + ], + }; + }), + ], + }); + imgContainer.children[0].textContent = totalSortKey + " "; + if (imgContainer.children.length === 1) { + imgContainer.append("-"); + } + row.append( + imgContainer, + ElementCreate({ + tag: "td", + classList: ['friend-list-column', 'ChatRoomName'], + children: [friend.chatRoom.caption], + style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined }, + }), + ); } else if (friend.chatRoom.canSearchRoom) { - row.appendChild(ElementCreate({ - tag: "button", - classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'], - innerHTML: friend.chatRoom.caption, - eventListeners: { - click: () => FriendListChatSearch(friend.chatRoom.name), - }, - })); + // Sorting is performed via each cell's `textContent`, + // so explicitly prepend an invisible node with some sorting key + let totalSortKey = ""; + const imgContainer = ElementCreate({ + tag: "td", + classList: ['friend-list-column', 'ChatRoomType'], + children: [ + { tag: "span", style: { display: "none" }, classList: ["friend-list-sorting-node"] }, + ...friend.chatRoom.types.map(({ src, tooltipKey, sortKey }) => { + totalSortKey += sortKey; + return { + tag: /** @type {const} */("div"), + classList: ["friend-list-icon-container"], + children: [ + { + tag: /** @type {const} */("img"), + attributes: { src, decoding: "async", loading: "lazy", alt: TextGet(tooltipKey) }, + classList: ["friend-list-icon"], + }, + { + tag: /** @type {const} */("div"), + attributes: { role: "tooltip", "aria-hidden": "true" }, + children: [TextGet(tooltipKey)], + classList: ["button-tooltip", "button-tooltip-right"], + }, + ], + }; + }), + ], + }); + imgContainer.children[0].textContent = totalSortKey + " "; + if (imgContainer.children.length === 1) { + imgContainer.append("-"); + } + row.append( + imgContainer, + ElementCreate({ + tag: "td", + classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'], + innerHTML: friend.chatRoom.caption, + style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined }, + eventListeners: { + click: () => FriendListChatSearch(friend.chatRoom.name), + }, + }), + ); } } @@ -767,6 +870,7 @@ function FriendListLoadFriendList(data) { { button: { classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content'], children: [friend.beep.caption], + attributes: { role: "cell" }, }}, ), ElementButton.Create( @@ -776,12 +880,13 @@ function FriendListLoadFriendList(data) { { button: { classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-beeps-content'], children: [friend.beep.caption], + attributes: { role: "cell" }, }}, ), ); } else { row.appendChild(ElementCreate({ - tag: "span", + tag: "td", classList: ['friend-list-column'], children: [ friend.beep.caption @@ -792,14 +897,18 @@ function FriendListLoadFriendList(data) { if (friend.relationType) { row.appendChild(ElementCreate({ - tag: "span", + tag: "td", classList: ['friend-list-column', 'RelationType', 'mode-specific-content', 'fl-all-friends-content'], children: [ { - tag: 'img', + tag: "img", attributes: { src: relationTypeIcons[friend.relationType], - } + decoding: "async", + loading: "lazy", + "aria-hidden": "true", + }, + classList: ["friend-list-icon-small"], }, FriendTypeCaption[friend.relationType] ], @@ -813,7 +922,7 @@ function FriendListLoadFriendList(data) { { button: { classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'], children: [FriendListConfirmDelete.includes(friend.memberNumber) ? ConfirmDeleteCaption : DeleteCaption], - attributes: { disabled: !friend.canDelete }, + attributes: { disabled: !friend.canDelete, role: "cell" }, }} )); diff --git a/BondageClub/Screens/Character/FriendList/Text_FriendList.csv b/BondageClub/Screens/Character/FriendList/Text_FriendList.csv index 4dbfda8561..74eb3b78ce 100644 --- a/BondageClub/Screens/Character/FriendList/Text_FriendList.csv +++ b/BondageClub/Screens/Character/FriendList/Text_FriendList.csv @@ -5,7 +5,8 @@ MemberName,Name MemberNickname,Nickname MemberNumber,Member number ChatRoomName,Chat room -FriendType,Relation type +ChatRoomType,Room type +RelationType,Relation type ActionFriends,Send a Beep ActionRead,Read a Beep ActionDelete,Delete a Friend @@ -21,3 +22,8 @@ TypeOwner,Owner TypeLover,Lover TypeSubmissive,Submissive TypeFriend,Friend +TypeFemale,Femaly-only room +TypeMale,Male-only room +TypeMixed,Mixed male/female room +TypeAsylum,Asylum room +TypePrivate,Private room diff --git a/BondageClub/Screens/Character/Preference/Arousal.js b/BondageClub/Screens/Character/Preference/Arousal.js index e1281be1f7..c9773756a5 100644 --- a/BondageClub/Screens/Character/Preference/Arousal.js +++ b/BondageClub/Screens/Character/Preference/Arousal.js @@ -276,8 +276,11 @@ function PreferenceDecrementArousalFactor(factor) { * Exits the preference screen */ function PreferenceSubscreenArousalExit() { + return true; +} + +function PreferenceSubscreenArousalUnload() { Player.FocusGroup = null; CharacterAppearanceForceUpCharacter = -1; CharacterLoadCanvas(Player); - return true; } diff --git a/BondageClub/Screens/Character/Preference/Audio.js b/BondageClub/Screens/Character/Preference/Audio.js index b7974f1098..9c349a4fd6 100644 --- a/BondageClub/Screens/Character/Preference/Audio.js +++ b/BondageClub/Screens/Character/Preference/Audio.js @@ -78,6 +78,10 @@ function PreferenceSubscreenAudioClick() { * Exits the preference screen. */ function PreferenceSubscreenAudioExit() { + return true; +} + +function PreferenceSubscreenAudioUnload() { // If audio has been disabled for notifications, disable each individual notification audio setting if (!Player.AudioSettings.Notifications) { for (const setting in Player.NotificationSettings) { @@ -85,5 +89,4 @@ function PreferenceSubscreenAudioExit() { if (typeof audio === 'number' && audio > 0) Player.NotificationSettings[setting].Audio = NotificationAudioType.NONE; } } - return true; } diff --git a/BondageClub/Screens/Character/Preference/CensoredWords.js b/BondageClub/Screens/Character/Preference/CensoredWords.js index 4eaa4b0704..e03a50075d 100644 --- a/BondageClub/Screens/Character/Preference/CensoredWords.js +++ b/BondageClub/Screens/Character/Preference/CensoredWords.js @@ -88,7 +88,10 @@ function PreferenceSubscreenCensoredWordsClick() { * Exits the preference screen */ function PreferenceSubscreenCensoredWordsExit() { - ElementRemove("InputWord"); - Player.ChatSettings.CensoredWordsList = PreferenceCensoredWordsList.join("|"); return true; } + +function PreferenceSubscreenCensoredWordsUnload() { + ElementRemove("InputWord"); + Player.ChatSettings.CensoredWordsList = PreferenceCensoredWordsList.join("|"); +} diff --git a/BondageClub/Screens/Character/Preference/Controller.js b/BondageClub/Screens/Character/Preference/Controller.js index b820313d1d..aa9d7a7483 100644 --- a/BondageClub/Screens/Character/Preference/Controller.js +++ b/BondageClub/Screens/Character/Preference/Controller.js @@ -82,6 +82,9 @@ function PreferenceSubscreenControllerClick() { * Exits the preference screen */ function PreferenceSubscreenControllerExit() { - ControllerStopCalibration(true); return true; } + +function PreferenceSubscreenControllerUnload() { + ControllerStopCalibration(true); +} diff --git a/BondageClub/Screens/Character/Preference/Extensions.js b/BondageClub/Screens/Character/Preference/Extensions.js index caab2f4a21..1f600cb060 100644 --- a/BondageClub/Screens/Character/Preference/Extensions.js +++ b/BondageClub/Screens/Character/Preference/Extensions.js @@ -68,17 +68,12 @@ function PreferenceSubscreenExtensionsClick() { }); } - -function PreferenceSubscreenExtensionsUnload() { - PreferenceExtensionsCurrent?.unload?.(); -} - function PreferenceSubscreenExtensionsExit() { if (PreferenceExtensionsCurrent) { - if (PreferenceExtensionsCurrent.exit() ?? true) { - PreferenceSubscreenExtensionsClear(); - return false; - } + const validExit = PreferenceExtensionsCurrent.exit(); + if (validExit === false) return false; + PreferenceSubscreenExtensionsClear(); + return false; } return true; } @@ -95,3 +90,13 @@ function PreferenceSubscreenExtensionsClear() { // Reload the extension settings PreferenceSubscreenExtensionsLoad(); } + +/** + * Unloads the preference subscreen for extensions + * Cleans up the current extension, and reset the current extension to null + */ +function PreferenceSubscreenExtensionsUnload() { + PreferenceExtensionsCurrent?.unload?.(); + PreferenceExtensionsCurrent = null; +} + diff --git a/BondageClub/Screens/Character/Preference/General.js b/BondageClub/Screens/Character/Preference/General.js index 409e32e5cf..112b478501 100644 --- a/BondageClub/Screens/Character/Preference/General.js +++ b/BondageClub/Screens/Character/Preference/General.js @@ -103,8 +103,8 @@ function PreferenceSubscreenGeneralClick() { } /** - * Exits the preference screen. Cleans up elements that are not needed anymore - * If the selected color is invalid, the player cannot leave the screen. + * Exits the preference screen. Block exit when the color picker is active. + * @returns {boolean} - Returns false if the color picker is active and input is not valid */ function PreferenceSubscreenGeneralExit() { if (PreferenceSubscreenGeneralColorPicker) return false; @@ -115,13 +115,23 @@ function PreferenceSubscreenGeneralExit() { return false; } - if (color !== Player.LabelColor) { - Player.LabelColor = color; - const elems = /** @type {HTMLElement[]} */(Array.from(document.querySelectorAll(`[style*="--label-color"][data-sender="${Player.MemberNumber}"]`))); - elems.forEach(e => e.style.setProperty("--label-color", color)); - } - - PreferenceMessage = ""; - ElementRemove("InputCharacterLabelColor"); return true; } + +/** + * Cleans up elements that are not needed anymore + * If the selected color is invalid, the player cannot leave the screen. + */ +function PreferenceSubscreenGeneralUnload() { + const color = ElementValue("InputCharacterLabelColor"); + if (CommonIsColor(color)) { + if (color !== Player.LabelColor) { + Player.LabelColor = color; + const elems = /** @type {HTMLElement[]} */(Array.from(document.querySelectorAll(`[style*="--label-color"][data-sender="${Player.MemberNumber}"]`))); + elems.forEach(e => e.style.setProperty("--label-color", color)); + } + } + PreferenceMessage = ""; + ElementRemove("InputCharacterLabelColor"); +} + diff --git a/BondageClub/Screens/Character/Preference/Graphics.js b/BondageClub/Screens/Character/Preference/Graphics.js index 7ba950b7bd..0234c38363 100644 --- a/BondageClub/Screens/Character/Preference/Graphics.js +++ b/BondageClub/Screens/Character/Preference/Graphics.js @@ -211,6 +211,13 @@ function PreferenceSubscreenGraphicsClick() { } function PreferenceSubscreenGraphicsExit() { + return true; +} + +/** + * Finalize graphics setting when the screen is unloaded + */ +function PreferenceSubscreenGraphicsUnload() { // Reload WebGL if graphic settings have changed. const currentOptions = GLDrawGetOptions(); if ( @@ -223,5 +230,4 @@ function PreferenceSubscreenGraphicsExit() { GLDrawSetOptions(PreferenceGraphicsWebGLOptions); GLDrawResetCanvas(); } - return true; } diff --git a/BondageClub/Screens/Character/Preference/Notifications.js b/BondageClub/Screens/Character/Preference/Notifications.js index 6a99c12b9f..a63328e8c8 100644 --- a/BondageClub/Screens/Character/Preference/Notifications.js +++ b/BondageClub/Screens/Character/Preference/Notifications.js @@ -185,7 +185,13 @@ function PreferenceNotificationsClickSetting(Left, Top, Setting, EventType) { * Exits the preference screen. Resets the test notifications. */ function PreferenceSubscreenNotificationsExit() { + return true; +} +/** + * Finalize notification setting when the screen is unloaded + */ +function PreferenceSubscreenNotificationsUnload() { // If any of the settings now have audio enabled, enable the AudioSettings setting as well let enableAudio = false; for (const setting in Player.NotificationSettings) { @@ -195,5 +201,4 @@ function PreferenceSubscreenNotificationsExit() { if (enableAudio) Player.AudioSettings.Notifications = true; NotificationReset(NotificationEventType.TEST); - return true; } diff --git a/BondageClub/Screens/Character/Preference/Preference.js b/BondageClub/Screens/Character/Preference/Preference.js index ae375612bd..47a16e314e 100644 --- a/BondageClub/Screens/Character/Preference/Preference.js +++ b/BondageClub/Screens/Character/Preference/Preference.js @@ -40,6 +40,7 @@ const PreferenceSubscreens = [ run: () => PreferenceSubscreenGeneralRun(), click: () => PreferenceSubscreenGeneralClick(), exit: () => PreferenceSubscreenGeneralExit(), + unload: () => PreferenceSubscreenGeneralUnload(), }, { name: "Difficulty", @@ -64,6 +65,7 @@ const PreferenceSubscreens = [ run: () => PreferenceSubscreenCensoredWordsRun(), click: () => PreferenceSubscreenCensoredWordsClick(), exit: () => PreferenceSubscreenCensoredWordsExit(), + unload: () => PreferenceSubscreenCensoredWordsUnload(), }, { name: "Audio", @@ -71,6 +73,7 @@ const PreferenceSubscreens = [ run: () => PreferenceSubscreenAudioRun(), click: () => PreferenceSubscreenAudioClick(), exit: () => PreferenceSubscreenAudioExit(), + unload: () => PreferenceSubscreenAudioUnload(), }, { name: "Arousal", @@ -78,6 +81,7 @@ const PreferenceSubscreens = [ run: () => PreferenceSubscreenArousalRun(), click: () => PreferenceSubscreenArousalClick(), exit: () => PreferenceSubscreenArousalExit(), + unload: () => PreferenceSubscreenArousalUnload(), }, { name: "Security", @@ -85,6 +89,7 @@ const PreferenceSubscreens = [ run: () => PreferenceSubscreenSecurityRun(), click: () => PreferenceSubscreenSecurityClick(), exit: () => PreferenceSubscreenSecurityExit(), + unload: () => PreferenceSubscreenSecurityUnload(), }, { name: "Online", @@ -97,6 +102,7 @@ const PreferenceSubscreens = [ run: () => PreferenceSubscreenVisibilityRun(), click: () => PreferenceSubscreenVisibilityClick(), exit: () => PreferenceSubscreenVisibilityExit(), + unload: () => PreferenceSubscreenVisibilityUnload(), }, { name: "Immersion", @@ -110,12 +116,14 @@ const PreferenceSubscreens = [ run: () => PreferenceSubscreenGraphicsRun(), click: () => PreferenceSubscreenGraphicsClick(), exit: () => PreferenceSubscreenGraphicsExit(), + unload: () => PreferenceSubscreenGraphicsUnload(), }, { name: "Controller", run: () => PreferenceSubscreenControllerRun(), click: () => PreferenceSubscreenControllerClick(), exit: () => PreferenceSubscreenControllerExit(), + unload: () => PreferenceSubscreenControllerUnload(), }, { name: "Notifications", @@ -123,6 +131,7 @@ const PreferenceSubscreens = [ run: () => PreferenceSubscreenNotificationsRun(), click: () => PreferenceSubscreenNotificationsClick(), exit: () => PreferenceSubscreenNotificationsExit(), + unload: () => PreferenceSubscreenNotificationsUnload(), }, { name: "Gender", @@ -135,6 +144,7 @@ const PreferenceSubscreens = [ run: () => PreferenceSubscreenScriptsRun(), click: () => PreferenceSubscreenScriptsClick(), exit: () => PreferenceSubscreenScriptsExit(), + unload: () => PreferenceSubscreenScriptsUnload(), }, { name: "Extensions", @@ -142,6 +152,7 @@ const PreferenceSubscreens = [ run: () => PreferenceSubscreenExtensionsRun(), click: () => PreferenceSubscreenExtensionsClick(), exit: () => PreferenceSubscreenExtensionsExit(), + unload: () => PreferenceSubscreenExtensionsUnload(), }, ]; @@ -217,11 +228,14 @@ function PreferenceClick() { */ function PreferenceExit() { if (PreferenceSubscreen.name !== "Main") { - if (PreferenceSubscreenExit()) - return; + // If we are in a subscreen, the only exit is to the main preference screen + PreferenceSubscreenExit(); + return; } // Exit the preference menus + // Only a normal exit triggers an update to server. so we don't send data in unload function, + // which could be called from disconnects const P = { ArousalSettings: Player.ArousalSettings, ChatSettings: Player.ChatSettings, @@ -245,19 +259,31 @@ function PreferenceExit() { CommonSetScreen("Character", "InformationSheet"); } +/** + * Clear all GUI data and DOM elements creates by the preference screen load function + * We don't do this in exit function for disconnects do not trigger the exit function + */ +function PreferenceUnload() { + if (PreferenceSubscreen.name !== "Main") { + PreferenceSubscreen.unload?.(); + } +} + /** * Exit from a specific subscreen by running its handler and checking its validity */ function PreferenceSubscreenExit() { - let valid = true; - if (PreferenceSubscreen.exit) - valid = PreferenceSubscreen.exit(); + const validExit = PreferenceSubscreen.exit?.(); - if (!valid) return valid; + // Only when the results is false (not undefined) + // The exit is just a exit of the subscreen's substate, return to block more exit. + if(validExit === false) return; + + // The exit is a full exit of the subscreen, unload resources + PreferenceSubscreen.unload?.(); PreferenceMessage = ""; PreferenceSubscreen = PreferenceSubscreens.find(s => s.name === "Main"); - return valid; } /** diff --git a/BondageClub/Screens/Character/Preference/Scripts.js b/BondageClub/Screens/Character/Preference/Scripts.js index d555333efb..ea98fd97bb 100644 --- a/BondageClub/Screens/Character/Preference/Scripts.js +++ b/BondageClub/Screens/Character/Preference/Scripts.js @@ -190,6 +190,10 @@ function PreferenceSubscreenScriptsClick() { } function PreferenceSubscreenScriptsExit() { + return true; +} + +function PreferenceSubscreenScriptsUnload() { if (PreferenceScriptTimeoutHandle != null) { clearTimeout(PreferenceScriptTimeoutHandle); PreferenceScriptTimeoutHandle = null; @@ -215,7 +219,6 @@ function PreferenceSubscreenScriptsExit() { } } } - return true; } function PreferenceSubscreenScriptsWarningClick() { diff --git a/BondageClub/Screens/Character/Preference/Security.js b/BondageClub/Screens/Character/Preference/Security.js index 4adde3c055..19850bfde7 100644 --- a/BondageClub/Screens/Character/Preference/Security.js +++ b/BondageClub/Screens/Character/Preference/Security.js @@ -54,7 +54,10 @@ function PreferenceSubscreenSecurityClick() { * Exits the preference screen */ function PreferenceSubscreenSecurityExit() { - ElementRemove("InputEmailOld"); - ElementRemove("InputEmailNew"); return true; } + +function PreferenceSubscreenSecurityUnload() { + ElementRemove("InputEmailOld"); + ElementRemove("InputEmailNew"); +} diff --git a/BondageClub/Screens/Character/Preference/Visibility.js b/BondageClub/Screens/Character/Preference/Visibility.js index 0be66f37e7..9beb081970 100644 --- a/BondageClub/Screens/Character/Preference/Visibility.js +++ b/BondageClub/Screens/Character/Preference/Visibility.js @@ -233,9 +233,13 @@ function PreferenceVisibilityCheckboxChanged(permissionRecord, CheckSetting, Typ * Exits the preference screen */ function PreferenceSubscreenVisibilityExit() { + return true; +} + + +function PreferenceSubscreenVisibilityUnload() { PreferenceVisibilityGroupList = []; PreferenceVisibilityRecord = {}; - return true; } /** diff --git a/BondageClub/Screens/Inventory/ItemHandheld/Kyosensu/kyosensuScreenbase.png b/BondageClub/Screens/Inventory/ItemHandheld/Kyosensu/kyosensuScreenbase.png deleted file mode 100644 index 1078a7ffc7..0000000000 Binary files a/BondageClub/Screens/Inventory/ItemHandheld/Kyosensu/kyosensuScreenbase.png and /dev/null differ diff --git a/BondageClub/Screens/Inventory/ItemHandheld/Uchiwa/UchiwaScreenbase.png b/BondageClub/Screens/Inventory/ItemHandheld/Uchiwa/UchiwaScreenbase.png deleted file mode 100644 index ae84f33f81..0000000000 Binary files a/BondageClub/Screens/Inventory/ItemHandheld/Uchiwa/UchiwaScreenbase.png and /dev/null differ diff --git a/BondageClub/Screens/Room/Crafting/Crafting.js b/BondageClub/Screens/Room/Crafting/Crafting.js index 0062237f4e..6d6b7d9e56 100644 --- a/BondageClub/Screens/Room/Crafting/Crafting.js +++ b/BondageClub/Screens/Room/Crafting/Crafting.js @@ -1159,11 +1159,7 @@ function CraftingLoad() { attributes: { id: CraftingID.privateLabel }, classList: ["crafting-label"], children: [ - { - tag: "input", - attributes: { id: CraftingID.privateCheckbox, type: "checkbox" }, - eventListeners: { click: CraftingEventListeners._ClickPrivate }, - }, + ElementCheckbox.Create(CraftingID.privateCheckbox, CraftingEventListeners._ClickPrivate), { tag: "span", children: [TextGet("EnterPrivate")] }, ], }, @@ -1190,11 +1186,7 @@ function CraftingLoad() { attributes: { id: CraftingID.asciidescriptionLabel }, classList: ["crafting-label"], children: [ - { - tag: "input", - attributes: { id: CraftingID.asciiDescriptionCheckbox, type: "checkbox" }, - eventListeners: { click: CraftingEventListeners._ClickAsciiDescription }, - }, + ElementCheckbox.Create(CraftingID.asciiDescriptionCheckbox, CraftingEventListeners._ClickAsciiDescription), { tag: "span", children: [TextGet("EnterExtendedDescription")] }, ], }, diff --git a/BondageClub/Scripts/Activity.js b/BondageClub/Scripts/Activity.js index 6b352d915d..bda6defd73 100644 --- a/BondageClub/Scripts/Activity.js +++ b/BondageClub/Scripts/Activity.js @@ -258,7 +258,7 @@ function ActivityCheckPrerequisites(activity, acting, acted, group) { * @param {ItemActivity[]} allowed * @param {Character} acting * @param {Character} acted - * @param {string} needsItem + * @param {ActivityNameItem} needsItem * @param {Activity} activity * @param {AssetGroup} targetGroup */ @@ -342,7 +342,8 @@ function ActivityAllowedForGroup(character, groupname) { let needsItem = activity.Prerequisite.find(p => p.startsWith("Needs-")); if (needsItem) { - handled = ActivityGenerateItemActivitiesFromNeed(allowed, Player, character, needsItem.substring(6), activity, group); + const needsItemActivity = /** @type {ActivityNameItem} */(needsItem.substring(6)); + handled = ActivityGenerateItemActivitiesFromNeed(allowed, Player, character, needsItemActivity, activity, group); } if (activity.Name === "ShockItem" && InventoryItemHasEffect(targetedItem, "ReceiveShock")) { diff --git a/BondageClub/Scripts/Character.js b/BondageClub/Scripts/Character.js index 3167d87298..31ae89cfac 100644 --- a/BondageClub/Scripts/Character.js +++ b/BondageClub/Scripts/Character.js @@ -1930,7 +1930,7 @@ function CharacterHasItemWithAttribute(C, Attribute) { /** * Checks if the character is wearing an item that allows for a specific activity * @param {Character} C - The character to test for - * @param {string} Activity - The name of the activity that must be allowed + * @param {ActivityName} Activity - The name of the activity that must be allowed * @returns {Item[]} - A list of items allowing that activity */ function CharacterItemsForActivity(C, Activity) { @@ -1942,7 +1942,7 @@ function CharacterItemsForActivity(C, Activity) { /** * Checks if the character is wearing an item that allows for a specific activity * @param {Character} C - The character to test for - * @param {String} Activity - The name of the activity that must be allowed + * @param {ActivityName} Activity - The name of the activity that must be allowed * @returns {boolean} - TRUE if at least one item allows that activity */ function CharacterHasItemForActivity(C, Activity) { diff --git a/BondageClub/Scripts/Dialog.js b/BondageClub/Scripts/Dialog.js index 9ddf5e4003..6b3cbb3eef 100644 --- a/BondageClub/Scripts/Dialog.js +++ b/BondageClub/Scripts/Dialog.js @@ -2731,6 +2731,23 @@ function DialogDrawItemMenu(C) { } /** + * Abstract base class for a simplistic DOM subscreen with three-ish components: + * - A menubar with a set of buttons which are generally heterogeneous in function (_e.g._ perform arbitrary, unrelated task #1, #2 or #3) + * - A status message of some sort + * - A grid with some type of misc element, generally a set of buttons homogeneous in function (e.g. equip item #1, #2 or #3). See below for more details. + * + * Grid button clicks + * ------------------ + * Grid button clicks in the {@link ids|ids.grid}-referenced element generally involve the following four steps: + * 1) The click listener (see {@link eventListeners|eventListeners._ClickButton}) performs some basic generic validation, like checking whether the character has been initialized. + * A validation failure is considered an internal error, and will lead to a premature termination of the click event. + * 2) The click listener retrieves some type of underlying object associated with the grid button, like an item or activity (see {@link _GetClickedObject}). + * 3) The click listener performs more extensive, subscreen-/class-specific validation (see {@link GetClickStatus}), like checking whether an item has not been blacklisted. + * A validation failure here will trigger a soft reload, updating the status message and re-evaluating the enabled/disabled state of _all_ pre-existing grid buttons. + * 4) The click listener finally performs a subscreen-/class-specific action based on the grid button click, like equipping an item (see {@link _ClickButton}). + * + * Parameters + * ---------- * @abstract * @template {string} [ModeType=string] - The name of the mode associated with this instance (_e.g._ {@link DialogMenuMode}) * @template [ClickedObj=any] - The underlying item or activity object of the clicked grid buttons (if applicable) diff --git a/BondageClub/Scripts/Drawing.js b/BondageClub/Scripts/Drawing.js index dd8e7f5ddd..fb155a8305 100644 --- a/BondageClub/Scripts/Drawing.js +++ b/BondageClub/Scripts/Drawing.js @@ -338,7 +338,7 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) { // If we must dark the Canvas characters if (!C.IsPlayer() && !OverrideDark && (Player.IsBlind() || Player.HasTints())) { TempCanvas.canvas.width = CanvasDrawWidth; - TempCanvas.canvas.height = CanvasDrawHeight; + TempCanvas.canvas.height = CanvasDrawHeight; TempCanvas.globalCompositeOperation = "copy"; TempCanvas.drawImage(Canvas, 0, 0); @@ -352,7 +352,7 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) { } const Tints = Player.GetTints(); - for (const {r, g, b, a} of Tints) { + for (const { r, g, b, a } of Tints) { TempCanvas.fillStyle = `rgba(${r},${g},${b},${a})`; TempCanvas.fillRect(0, 0, Canvas.width, Canvas.height); } @@ -376,7 +376,8 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) { // Apply blur filter if needed const BlurLevel = Player.GetBlurLevel(); - if (!C.IsPlayer() && !OverrideDark && BlurLevel > 0) { + const needsBlur = !C.IsPlayer() && !OverrideDark && BlurLevel > 0; + if (needsBlur) { MainCanvas.filter = `blur(${BlurLevel}px)`; } // Draw the character @@ -387,7 +388,10 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) { Invert: IsInverted, Mirror: IsInverted }); - MainCanvas.filter = 'none'; + // Resetting the filter is expensive, even if there's no change. Only change the filter if we need to. + if (needsBlur) { + MainCanvas.filter = 'none'; + } // Draw the arousal meter & game images on certain conditions if (CurrentScreen != "ChatRoom" || ChatRoomHideIconState <= 1) { @@ -683,24 +687,21 @@ function DrawImageEx( } // Blit the transformed image to the main canvas, applying opacity and zoom - Canvas.save(); + + // Instead of expensive save/restore, we store the relevant state info and restore it manually + let savedCompositeOperation = Canvas.globalCompositeOperation; + let savedAlpha = Canvas.globalAlpha; Canvas.globalCompositeOperation = BlendingMode; Canvas.globalAlpha = Alpha; - Canvas.translate(X, Y); + // Performance benefits from combining transforms is usually minimal to none but in cases with multiple transforms it adds up + const scaleHoriz = Zoom * (Mirror ? -1 : 1); // Scaling and horizontal mirroring + const scaleVert = Zoom * (Invert ? -1 : 1); // Scaling and vertical inversion + const translateX = X + (Mirror ? Width : 0); // Translation in x + const translateY = Y + (Invert ? Height : 0); // Translation in y - if (Zoom != 1) { - Canvas.scale(Zoom, Zoom); - } - - if (Invert) { - Canvas.transform(1, 0, 0, -1, 0, Height); - } - - if (Mirror) { - Canvas.transform(-1, 0, 0, 1, Width, 0); - } + Canvas.transform(scaleHoriz, 0, 0, scaleVert, translateX, translateY); if (SourcePos) { Canvas.drawImage(destCanvas, SourcePos[0], SourcePos[1], SourcePos[2], SourcePos[3], 0, 0, Width, Height); @@ -710,7 +711,9 @@ function DrawImageEx( Canvas.drawImage(destCanvas, 0, 0); } - Canvas.restore(); + Canvas.globalCompositeOperation = savedCompositeOperation; + Canvas.globalAlpha = savedAlpha; + Canvas.resetTransform(); return true; } @@ -799,7 +802,7 @@ function GetWrapTextSize(Text, Width, MaxLine) { * @param {"Center" | "Top"} Alignment - How the text should be alligned w.r.t. the Y position when wrapped over multiple lines * @returns {void} - Nothing */ -function DrawTextWrap(Text, X, Y, Width, Height, ForeColor, BackColor, MaxLine, LineSpacing=23, Alignment="Center") { +function DrawTextWrap(Text, X, Y, Width, Height, ForeColor, BackColor, MaxLine, LineSpacing = 23, Alignment = "Center") { ControllerAddActiveArea(X, Y); // Draw the rectangle if we need too @@ -912,7 +915,7 @@ function DrawTextFit(Text, X, Y, Width, Color, BackColor) { const DrawingGetTextSize = CommonMemoize( /** @type {(Text: string, width: number) => [text: string, size: number]} */ (Text, Width) => { - // If it doesn't fit, test with smaller and smaller fonts until it fits + // If it doesn't fit, test with smaller and smaller fonts until it fits let S; for (S = 36; S >= 10; S = S - 2) { MainCanvas.font = CommonGetFont(S.toString()); @@ -1277,7 +1280,10 @@ function DrawRoomBackground(URL, bounds, opts) { DrawRect(...destRect, ChatRoomCustomFilter); } - MainCanvas.filter = 'none'; + // Resetting the filter is expensive, even if there's no change. Only change the filter if we need to. + if (blur > 0) { + MainCanvas.filter = 'none'; + } // Draw an overlay if the character is partially blinded if (darken < 1) { @@ -1288,7 +1294,7 @@ function DrawRoomBackground(URL, bounds, opts) { DrawRect(...RectGetFrame(bounds), "#000"); } - for (const {r, g, b, a} of tints) { + for (const { r, g, b, a } of tints) { DrawRect(bounds.x, bounds.y, bounds.w, bounds.h, `rgba(${r},${g},${b},${a})`); } } @@ -1325,7 +1331,7 @@ function DrawFlashScreen(Color, Duration, Intensity) { * @returns {string} - alpha of screen flash */ function DrawGetScreenFlashAlpha(FlashTime) { - let alpha = Math.max(0, Math.min(255, Math.floor(DrawScreenFlashStrength * (1 - Math.exp(-FlashTime/2500))))).toString(16); + let alpha = Math.max(0, Math.min(255, Math.floor(DrawScreenFlashStrength * (1 - Math.exp(-FlashTime / 2500))))).toString(16); if (alpha.length < 2) alpha = "0" + alpha; return alpha; } @@ -1457,7 +1463,7 @@ function DrawProcessScreenFlash() { if (BlindFlash == true && CurrentTime < DrawingBlindFlashTimer) { if (Player.GetBlindLevel() == 0) { let FlashTime = DrawingBlindFlashTimer - CurrentTime; - DrawRect(0, 0, 2000, 1000, "#ffffff" + DrawGetScreenFlashAlpha(FlashTime/Math.max(1, 4 - DrawLastDarkFactor))); + DrawRect(0, 0, 2000, 1000, "#ffffff" + DrawGetScreenFlashAlpha(FlashTime / Math.max(1, 4 - DrawLastDarkFactor))); } } @@ -1527,7 +1533,7 @@ function DrawAssetPreview(X, Y, A, Options) { if (Description == null) Description = C ? A.DynamicDescription(C) : A.Description; - DrawPreviewBox(X, Y, Path, Description, { Background, Foreground, Vibrating, Border, Hover, HoverBackground, Disabled, Icons, Width, Height}); + DrawPreviewBox(X, Y, Path, Description, { Background, Foreground, Vibrating, Border, Hover, HoverBackground, Disabled, Icons, Width, Height }); } /** The default width of item previews */ @@ -1679,7 +1685,7 @@ function DrawCharacterSegment(C, Left, Top, Width, Height) { * @param {number} [y] - The y-position on the target canvas that the final image should be drawn at */ function DrawImageTrapezify(image, targetCanvas, topToBottomRatio, x = 0, y = 0) { - const {width, height} = image; + const { width, height } = image; let xStartTop = 0; let xStartBottom = 0; diff --git a/BondageClub/Scripts/Element.js b/BondageClub/Scripts/Element.js index 4a06362f66..dc7379e24d 100644 --- a/BondageClub/Scripts/Element.js +++ b/BondageClub/Scripts/Element.js @@ -686,12 +686,7 @@ var ElementCheckboxDropdown = { classList: ["dropdown-checkbox-grid"], attributes: { id: `${idPrefix}-pair-${idSuffix}` }, children: [ - { - tag: "input", - classList: ["dropdown-checkbox"], - attributes: { id: `${idPrefix}-checkbox-${idSuffix}`, type: "checkbox", checked }, - eventListeners: { click: listener }, - }, + ElementCheckbox.Create(`${idPrefix}-checkbox-${idSuffix}`, listener, { checked }), { tag: "span", classList: ["dropdown-checkbox-label"], @@ -807,6 +802,12 @@ function ElementCreateSearchInput(id, dataCallback, options=null) { * @namespace */ var ElementButton = { + /** + * A unique element ID-suffix to-be assigned to buttons without an explicit ID. + * @private + */ + _idCounter: 0, + /** * @private * @readonly @@ -1217,6 +1218,7 @@ var ElementButton = { * @returns {HTMLButtonElement} - The created button */ Create: function Create(id, onClick, options=null, htmlOptions=null) { + id ??= `button-${ElementButton._idCounter++}`; let elem = /** @type {HTMLButtonElement | null} */(document.getElementById(id)); if (elem) { console.error(`Element "${id}" already exists`); @@ -1227,7 +1229,6 @@ var ElementButton = { const buttonOptions = htmlOptions.button ?? {}; const tooltipOptions = htmlOptions.tooltip ?? {}; - id ??= `button-${Date.now()}`; options ??= {}; const image = this._ParseImage(id, options.image, htmlOptions.img); const label = this._ParseLabel(id, options.label, options.labelPosition, htmlOptions.label); @@ -1798,6 +1799,53 @@ var ElementMenu = { }, }; +/** + * Namespace for creating DOM checkboxes. + */ +var ElementCheckbox = { + /** + * A unique element ID-suffix to-be assigned to checkboxes without an explicit ID. + * @private + */ + _idCounter: 0, + + /** + * Construct an return a DOM checkbox element (`<input type="checkbox">`) + * @param {null | string} id - The ID of the element, or `null` if one must be assigned automatically + * @param {(this: HTMLInputElement, ev: Event) => any} onChange - The change event listener to-be fired upon checkbox clicks + * @param {null | ElementCheckbox.Options} options - High level options for the to-be created checkbox + * @param {null | Partial<Record<"checkbox", Omit<HTMLOptions<any>, "tag">>>} htmlOptions - Additional {@link ElementCreate} options to-be applied to the respective (child) element + */ + Create: function Create(id, onChange, options=null, htmlOptions=null) { + id ??= `checkbox-${ElementCheckbox._idCounter++}`; + const checkbox = document.getElementById(id); + if (checkbox) { + console.error(`Element "${id}" already exists`); + return checkbox; + } + + options ??= {}; + const checkboxOptions = htmlOptions?.checkbox ?? {}; + return ElementCreate({ + ...checkboxOptions, + tag: "input", + attributes: { + id, + type: "checkbox", + disabled: options.disabled, + checked: options.checked, + value: options.value, + ...(checkboxOptions.attributes ?? {}), + }, + classList: ["checkbox", ...(checkboxOptions.classList ?? [])], + eventListeners: { + change: onChange, + ...(checkboxOptions.eventListeners ?? {}), + }, + }); + }, +}; + /** * Return whether an element is visible or not. * diff --git a/BondageClub/Scripts/Typedef.d.ts b/BondageClub/Scripts/Typedef.d.ts index 28d64d6ef9..311a12d784 100644 --- a/BondageClub/Scripts/Typedef.d.ts +++ b/BondageClub/Scripts/Typedef.d.ts @@ -179,6 +179,23 @@ declare namespace ElementButton { } } +declare namespace ElementCheckbox { + + /** Various options that can be passed along to {@link ElementCheckbox.Create} */ + interface Options { + /** Whether the checkbox is checked by default or not */ + checked?: boolean; + /** Whether the checkbox should be disabled or not */ + disabled?: boolean; + /** + * The {@link HTMLInputElement.value}/{@link HTMLInputElement.valueAsNumber} associated with the checkbox when checked. + * + * Defaults to `"on"` if not specified. + */ + value?: string | number; + } +} + type Rect = { x: number, y: number, w: number, h: number }; /** A 4-tuple with X & Y coordinates, width and height */ @@ -568,6 +585,7 @@ interface PreferenceSubscreen { run: () => void; click: () => void; exit?: () => boolean; + unload?: () => void; } interface PreferenceGenderSetting { @@ -883,7 +901,7 @@ interface IFriendListBeepLogMessage { MemberName: string; ChatRoomName?: string; Private: boolean; - ChatRoomSpace?: string; + ChatRoomSpace?: ServerChatRoomSpace; Sent: boolean; Time: Date; Message?: string; @@ -1056,6 +1074,11 @@ interface AssetLayer { readonly GroupAlpha?: readonly Alpha.Data[]; /** @deprecated - Superceded by {@link CreateLayerTypes} */ readonly ModuleType?: never; + /** + * A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files. + * + * By default files are expected for _all_ option indices associated with the key(s), unless the valid option set has been narrowed down according to {@link AssetLayer.AllowTypes}. + */ readonly CreateLayerTypes: readonly string[]; /* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */ readonly HideForAttribute: readonly AssetAttribute[] | null; @@ -1213,10 +1236,7 @@ interface Asset { readonly DynamicScriptDraw: boolean; /** @deprecated - superceded by {@link CreateLayerTypes} */ readonly HasType?: never; - /** - * A module for which the layer can have types. - * Allows one to define different module-specific assets for a single layer. - */ + /** A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files. */ readonly CreateLayerTypes: readonly string[]; /** A record that maps {@link ExtendedItemData.name} to a set with all option indices that support locks */ readonly AllowLockType: null | Record<string, Set<number>>; @@ -2810,7 +2830,7 @@ interface AssetDefinitionProperties { * A list of allowed activities * @see {@link Asset.AllowActivity} */ - AllowActivity?: string[]; + AllowActivity?: ActivityName[]; /** * A list of groups allowed activities * @see {@link Asset.AllowActivityOn} @@ -2917,7 +2937,7 @@ interface AssetDefinitionProperties { type PartialType = `${string}${number}`; /** - * A record mapping screen names to option indices. + * A record mapping extended item data/module names (see {@link ExtendedItemData.Name} and {@link ModularItemModule.Key}) to option indices. * @see {@link PartialType} A concatenation of a single `TypeRecord` key/value pair. */ type TypeRecord = Record<string, number>; @@ -4359,6 +4379,8 @@ interface PreferenceExtensionsSettingItem { * Called when the extension screen is about to exit. * * Happens either through a click of the exit button, or the ESC key. + * This will **not** be called if a disconnect happens, so clean up should be + * done in {@link PreferenceExtensionsSettingItem.unload}. * * @returns {boolean | void} If you have some validation that needs to happen * (for example, ensuring that a textfield contains a valid color code), return false; @@ -4367,7 +4389,7 @@ interface PreferenceExtensionsSettingItem { * either through your own means or by setting `PreferenceMessage` to a string. * * If you return true or nothing, your screen's {@link PreferenceExtensionsSettingItem.unload} handler - * will be called afterward. + * will be called afterward. And the setting screen for the extension will be closed. */ exit: () => boolean | void; }