Merge branch 'bra-port' of ssh.gitgud.io:sin1337/Bondage-College into bra-port

This commit is contained in:
Sin 2025-03-29 22:50:43 -04:00
commit 1af19885b9
36 changed files with 669 additions and 278 deletions

View file

@ -6448,7 +6448,8 @@ ItemHoodCreepyIronMaskSelectBase,Creepy Iron Mask main menu:
ItemHoodCreepyIronMaskModuleMode,Mask style ItemHoodCreepyIronMaskModuleMode,Mask style
ItemHoodCreepyIronMaskSelectMode,Mask menu: ItemHoodCreepyIronMaskSelectMode,Mask menu:
ItemHoodCreepyIronMaskOptionm0,Just mask ItemHoodCreepyIronMaskOptionm0,Just mask
ItemHoodCreepyIronMaskOptionm1,Full hood ItemHoodCreepyIronMaskOptionm1,Semi-hood
ItemHoodCreepyIronMaskOptionm2,Full hood
ItemHoodCreepyIronMaskModuleBlindfold,Blindfold ItemHoodCreepyIronMaskModuleBlindfold,Blindfold
ItemHoodCreepyIronMaskSelectBlindfold,Metal blindfold option: ItemHoodCreepyIronMaskSelectBlindfold,Metal blindfold option:
ItemHoodCreepyIronMaskOptionb0,None ItemHoodCreepyIronMaskOptionb0,None
@ -6462,8 +6463,13 @@ ItemHoodCreepyIronMaskModuleNose,Nose guard
ItemHoodCreepyIronMaskSelectNose,Nose guard option: ItemHoodCreepyIronMaskSelectNose,Nose guard option:
ItemHoodCreepyIronMaskOptionn0,None ItemHoodCreepyIronMaskOptionn0,None
ItemHoodCreepyIronMaskOptionn1,Add nose guard ItemHoodCreepyIronMaskOptionn1,Add nose guard
ItemHoodCreepyIronMaskModuleSpeech,Speech
ItemHoodCreepyIronMaskSelectSpeech,Speech option:
ItemHoodCreepyIronMaskOptionp0,Loose
ItemHoodCreepyIronMaskOptionp1,Tight
ItemHoodCreepyIronMaskSetm0,SourceCharacter puts the Creepy Iron Mask on DestinationCharacterName head. ItemHoodCreepyIronMaskSetm0,SourceCharacter puts the Creepy Iron Mask on DestinationCharacterName head.
ItemHoodCreepyIronMaskSetm1,SourceCharacter locks DestinationCharacterName head with the Creepy Iron Hood version. 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. ItemHoodCreepyIronMaskSetb0,SourceCharacter removes a metal blindfold from DestinationCharacterName Creepy Iron Mask.
ItemHoodCreepyIronMaskSetb1,SourceCharacter equips a perforated metal blindfold on 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. 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. ItemHoodCreepyIronMaskSets1,SourceCharacter equips a spiked collar on DestinationCharacterName neck.
ItemHoodCreepyIronMaskSetn0,SourceCharacter removes a nose guard from DestinationCharacterName Creepy Iron Mask. ItemHoodCreepyIronMaskSetn0,SourceCharacter removes a nose guard from DestinationCharacterName Creepy Iron Mask.
ItemHoodCreepyIronMaskSetn1,SourceCharacter equips a nose guard on 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: ItemMouthHorrorMuzzleSelect,Horror Muzzle style option menu:
ItemMouthHorrorMuzzleNone,Plain ItemMouthHorrorMuzzleNone,Plain
ItemMouthHorrorMuzzleRivets,With rivets ItemMouthHorrorMuzzleRivets,With rivets
@ -6521,12 +6529,12 @@ ItemHandheldKyosensuType3,Sun
ItemHandheldKyosensuType4,Wave art ItemHandheldKyosensuType4,Wave art
ItemHandheldKyosensuType5,Moon ItemHandheldKyosensuType5,Moon
ItemHandheldKyosensuType6,Flowers ItemHandheldKyosensuType6,Flowers
ItemItemHandheldKyosensuSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Kyo-sensu fan. ItemHandheldKyosensuSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Kyo-sensu fan.
ItemItemHandheldKyosensuSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Kyo-sensu fan. ItemHandheldKyosensuSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Kyo-sensu fan.
ItemItemHandheldKyosensuSetType3,SourceCharacter chooses a sun art for DestinationCharacter Kyo-sensu fan. ItemHandheldKyosensuSetType3,SourceCharacter chooses a sun art for DestinationCharacter Kyo-sensu fan.
ItemItemHandheldKyosensuSetType4,SourceCharacter chooses a wave art for DestinationCharacter Kyo-sensu fan. ItemHandheldKyosensuSetType4,SourceCharacter chooses a wave art for DestinationCharacter Kyo-sensu fan.
ItemItemHandheldKyosensuSetType5,SourceCharacter chooses a moon art for DestinationCharacter Kyo-sensu fan. ItemHandheldKyosensuSetType5,SourceCharacter chooses a moon art for DestinationCharacter Kyo-sensu fan.
ItemItemHandheldKyosensuSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Kyo-sensu fan. ItemHandheldKyosensuSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Kyo-sensu fan.
ItemHandheldUchiwaSelect,Choose your Uchiwa paper art: ItemHandheldUchiwaSelect,Choose your Uchiwa paper art:
ItemHandheldUchiwaType1,Cherry blossoms ItemHandheldUchiwaType1,Cherry blossoms
ItemHandheldUchiwaType2,Lightning bolt ItemHandheldUchiwaType2,Lightning bolt
@ -6534,9 +6542,9 @@ ItemHandheldUchiwaType3,Sun
ItemHandheldUchiwaType4,Wave art ItemHandheldUchiwaType4,Wave art
ItemHandheldUchiwaType5,Moon ItemHandheldUchiwaType5,Moon
ItemHandheldUchiwaType6,Flowers ItemHandheldUchiwaType6,Flowers
ItemItemHandheldUchiwaSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Uchiwa fan. ItemHandheldUchiwaSetType1,SourceCharacter chooses a cherry blossooms art for DestinationCharacter Uchiwa fan.
ItemItemHandheldUchiwaSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Uchiwa fan. ItemHandheldUchiwaSetType2,SourceCharacter chooses a lightning bolt art for DestinationCharacter Uchiwa fan.
ItemItemHandheldUchiwaSetType3,SourceCharacter chooses a sun art for DestinationCharacter Uchiwa fan. ItemHandheldUchiwaSetType3,SourceCharacter chooses a sun art for DestinationCharacter Uchiwa fan.
ItemItemHandheldUchiwaSetType4,SourceCharacter chooses a wave art for DestinationCharacter Uchiwa fan. ItemHandheldUchiwaSetType4,SourceCharacter chooses a wave art for DestinationCharacter Uchiwa fan.
ItemItemHandheldUchiwaSetType5,SourceCharacter chooses a moon art for DestinationCharacter Uchiwa fan. ItemHandheldUchiwaSetType5,SourceCharacter chooses a moon art for DestinationCharacter Uchiwa fan.
ItemItemHandheldUchiwaSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Uchiwa fan. ItemHandheldUchiwaSetType6,SourceCharacter chooses a flowers art for DestinationCharacter Uchiwa fan.

Can't render this file because it contains an unexpected character in line 6406 and column 135.

View file

@ -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: [ Color: [
"Default", "Default",
@ -38081,7 +38051,10 @@ var AssetFemale3DCG = [
AllowTighten: true, AllowTighten: true,
DrawLocks: false, DrawLocks: false,
Audio: "Buckle", 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: [ Layer: [
{ Name: "Main", Priority: 35, AllowColorize: true }, { Name: "Main", Priority: 35, AllowColorize: true },
{ {
@ -41813,7 +41786,10 @@ var AssetFemale3DCG = [
Effect: [E.BlockMouth], Effect: [E.BlockMouth],
Prerequisite: ["GagFlat"], Prerequisite: ["GagFlat"],
Hide: ["ItemNose"], 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: [ Layer: [
{ Name: "Main", Priority: 38, AllowColorize: true }, { Name: "Main", Priority: 38, AllowColorize: true },
{ {
@ -45775,7 +45751,10 @@ var AssetFemale3DCG = [
Effect: [E.BlockMouth], Effect: [E.BlockMouth],
Prerequisite: ["GagFlat"], Prerequisite: ["GagFlat"],
Hide: ["ItemNose"], 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: [ Layer: [
{ Name: "Main", Priority: 39, AllowColorize: true }, { 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: [ Color: [
"Default", "Default",

View file

@ -9067,7 +9067,7 @@ var AssetFemale3DCGExtended = {
// Semi-Hood // Semi-Hood
Property: { Property: {
Effect: [E.BlockMouth], Effect: [E.BlockMouth],
Hide: ["HairFront","HairAccessory1"], Hide: ["HairFront", "HairAccessory1"],
}, },
}, },
{ {

View file

@ -657,6 +657,7 @@ interface AssetDefinitionBase extends AssetCommonPropertiesGroupAsset, AssetComm
AllowHideItem?: string[]; AllowHideItem?: string[];
/** @deprecated */ /** @deprecated */
AllowTypes?: never; AllowTypes?: never;
/** A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files. */
CreateLayerTypes?: string[]; CreateLayerTypes?: string[];
/** /**
* Whether an item can be tightened or not. * 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. */ /** Whether the layer is hidden in the Color Picker UI. Defaults to false. */
HideColoring?: boolean; 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; AllowTypes?: AllowTypes.Definition;
/** /**
@ -867,6 +868,11 @@ interface AssetLayerDefinition extends AssetCommonPropertiesGroupAssetLayer, Ass
*/ */
CopyLayerPoseMapping?: string; 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[]; CreateLayerTypes?: string[];
/* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */ /* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */
HideForAttribute?: AssetAttribute[]; HideForAttribute?: AssetAttribute[];

Binary file not shown.

Before

(image error) Size: 19 KiB

Binary file not shown.

Before

(image error) Size: 12 KiB

View file

@ -33,6 +33,7 @@
padding: var(--small-gap); padding: var(--small-gap);
width: 20%; width: 20%;
text-align: center; text-align: center;
user-select: none;
} }
#friend-list-search-input { #friend-list-search-input {
@ -61,17 +62,19 @@
/* #endregion */ /* #endregion */
/* #region HEADER */ /* #region HEADER */
#friend-list-header { #friend-list-header {
min-height: var(--row-height); user-select: none;
display: flex;
align-items: center;
color: var(--text-color);
} }
#friend-list-header .friend-list-link { #friend-list-header .friend-list-link {
text-decoration: none; text-decoration: none;
} }
#friend-list-header .friend-list-row:hover {
color: var(--text-color);
}
#friend-list-header-hr { #friend-list-header-hr {
width: 80%; width: 80%;
} }
@ -105,6 +108,60 @@
#friend-list-beep-dialog:not([data-received]) .fl-beep-sent-content { #friend-list-beep-dialog:not([data-received]) .fl-beep-sent-content {
display: block; 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 */ /* #endregion */
/* #region FRIENDLIST */ /* #region FRIENDLIST */
@ -124,17 +181,27 @@
width: calc(100% - var(--button-size)); width: calc(100% - var(--button-size));
} }
.friend-list-row { #friend-list-table th {
color: var(--text-color); font-weight: normal;
display: flex;
flex-direction: row;
justify-content: space-evenly;
} }
.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; margin: var(--small-gap) 0;
} }
.friend-list-row > * {
vertical-align: middle;
text-justify: center;
padding: unset;
}
.friend-list-row:hover { .friend-list-row:hover {
color: yellow; color: yellow;
} }
@ -147,10 +214,6 @@
white-space: preserve; white-space: preserve;
} }
#friend-list .friend-list-column {
height: 100%;
}
#friend-list-member-number, #friend-list-member-number,
.MemberNumber { .MemberNumber {
width: 10%; width: 10%;
@ -168,8 +231,8 @@
gap: var(--small-gap); gap: var(--small-gap);
} }
.RelationType img { .RelationType {
height: min(5dvh, 2.5dvw); user-select: none;
} }
.friend-list-link { .friend-list-link {

View file

@ -223,41 +223,58 @@ select:invalid:not(:disabled):not(:read-only) {
height: 1.2em; 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; -webkit-appearance: none;
appearance: none; appearance: none;
background-color: #fff; background-color: var(--checkbox-color);
margin: 0; position: relative;
font: inherit; font: inherit;
color: black; color: black;
width: min(6vh, 3vw); width: min(6vh, 3vw);
height: min(6vh, 3vw); height: min(6vh, 3vw);
border: min(0.3vh, 0.15vw) solid black; border: min(0.2vh, 0.1vw) solid black;
display: grid;
place-content: center;
cursor: pointer; cursor: pointer;
} }
input[type="checkbox"]:hover { .checkbox:hover {
background-color: cyan; background-color: var(--hover-color);
} }
input[type="checkbox"]:disabled { .checkbox:disabled {
background-color: lightgray; background-color: var(--disabled-color);
cursor: auto; cursor: auto;
} }
input[type="checkbox"]::before { .checkbox::before {
content: ""; content: "";
width: min(4.6vh, 2.3vw); position: absolute;
height: min(4.6vh, 2.3vw); top: 50%;
left: 50%;
transform: translate(-50%, -50%);
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
transform: scale(0);
background-color: black; background-color: black;
width: 90%;
height: 90%;
visibility: hidden;
} }
input[type="checkbox"]:checked::before { .checkbox:checked::before {
transform: scale(1); visibility: visible;
}
@supports (height: 100dvh) {
.checkbox {
width: min(6dvh, 3dvw);
height: min(6dvh, 3dvw);
border-width: min(0.2dvh, 0.1dvw);
}
} }
/* Dropdown */ /* Dropdown */

Binary file not shown.

After

(image error) Size: 3.5 KiB

Binary file not shown.

After

(image error) Size: 2 KiB

Binary file not shown.

After

(image error) Size: 3.6 KiB

Binary file not shown.

After

(image error) Size: 2.1 KiB

View file

@ -1,6 +1,6 @@
type FriendListModes = FriendListMode[]; type FriendListModes = FriendListMode[];
type FriendListMode = "OnlineFriends" | "Beeps" | "AllFriends"; 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 FriendListSortingDirection = 'Asc' | 'Desc';
type FriendListReturn<T extends ModuleType> = { Screen: ModuleScreens[T], Module: T, IsInChatRoom?: boolean, hasScrolledChat?: boolean }; type FriendListReturn<T extends ModuleType> = { Screen: ModuleScreens[T], Module: T, IsInChatRoom?: boolean, hasScrolledChat?: boolean };
@ -19,6 +19,7 @@ type FriendRawRoom = {
name?: string; name?: string;
caption: string; caption: string;
canSearchRoom: boolean; canSearchRoom: boolean;
types: FriendListIcon[];
}; };
type FriendRawBeep = { type FriendRawBeep = {
@ -27,3 +28,12 @@ type FriendRawBeep = {
hasMessage?: boolean; hasMessage?: boolean;
canBeep?: 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;
}

View file

@ -90,7 +90,7 @@ function FriendListLoad() {
tag: 'input', tag: 'input',
attributes: { attributes: {
id: FriendListIDs.searchInput, id: FriendListIDs.searchInput,
type: 'text', type: 'search',
maxLength: 100, maxLength: 100,
}, },
eventListeners: { eventListeners: {
@ -198,71 +198,87 @@ function FriendListLoad() {
} }
), ),
ElementCreate({ ElementCreate({
tag: 'div', tag: 'table',
attributes: { attributes: {
id: FriendListIDs.friendListTable id: FriendListIDs.friendListTable,
"aria-labelledby": FriendListIDs.modeTitle,
}, },
children: [ children: [
{ {
tag: 'div', tag: 'thead',
attributes: { attributes: {
id: FriendListIDs.header id: FriendListIDs.header
}, },
classList: ['friend-list-row'],
children: [ 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", tag: "tr",
classList: ['friend-list-column', 'mode-specific-content', 'fl-online-friends-content'], classList: ["friend-list-row"],
children: [ 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"], classList: ["scroll-box"],
attributes: { attributes: {
id: FriendListIDs.friendList id: FriendListIDs.friendList
@ -504,7 +520,7 @@ function FriendListBeepMenuSend() {
ChatRoomName: FriendListBeepShowRoom ? ChatRoomData?.Name : undefined, ChatRoomName: FriendListBeepShowRoom ? ChatRoomData?.Name : undefined,
ChatRoomSpace: FriendListBeepShowRoom ? ChatRoomData?.Space : undefined, ChatRoomSpace: FriendListBeepShowRoom ? ChatRoomData?.Space : undefined,
Sent: true, Sent: true,
Private: false, Private: FriendListBeepShowRoom ? !ChatRoomData?.Visibility.includes("All") : undefined,
Time: new Date(), Time: new Date(),
Message: msg || undefined Message: msg || undefined
}); });
@ -537,6 +553,15 @@ function FriendListChatSearch(room) {
ElementValue("InputSearch", ChatSearchMuffle(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. * Loads the friend list data into the HTML div element.
* @param {ServerFriendInfo[]} data - An array of data, we receive from the server * @param {ServerFriendInfo[]} data - An array of data, we receive from the server
@ -559,7 +584,6 @@ function FriendListLoadFriendList(data) {
const BeepCaption = InterfaceTextGet("Beep"); const BeepCaption = InterfaceTextGet("Beep");
const DeleteCaption = InterfaceTextGet("Delete"); const DeleteCaption = InterfaceTextGet("Delete");
const ConfirmDeleteCaption = InterfaceTextGet("ConfirmDelete"); const ConfirmDeleteCaption = InterfaceTextGet("ConfirmDelete");
const PrivateRoomCaption = InterfaceTextGet("PrivateRoom");
const SentCaption = InterfaceTextGet("SentBeep"); const SentCaption = InterfaceTextGet("SentBeep");
const ReceivedCaption = InterfaceTextGet("ReceivedBeep"); const ReceivedCaption = InterfaceTextGet("ReceivedBeep");
const MailCaption = InterfaceTextGet("BeepWithMail"); const MailCaption = InterfaceTextGet("BeepWithMail");
@ -601,15 +625,28 @@ function FriendListLoadFriendList(data) {
}); });
if (infoChanged) ServerPlayerRelationsSync(); if (infoChanged) ServerPlayerRelationsSync();
/** @satisfies {Record<string, FriendListSortingMode>} */
const columnHeaders = { const columnHeaders = {
"friend-list-member-name": `${TextGet("MemberName")} ${FriendListSortingMode === "MemberName" ? sortingSymbol : "↕"}`, "friend-list-member-name": "MemberName",
"friend-list-member-number": `${TextGet("MemberNumber")} ${FriendListSortingMode === "MemberNumber" ? sortingSymbol : "↕"}`, "friend-list-member-number": "MemberNumber",
"friend-list-chat-room-name": `${TextGet("ChatRoomName")} ${FriendListSortingMode === "ChatRoomName" ? sortingSymbol : "↕"}`, "friend-list-chat-room-name": "ChatRoomName",
"friend-list-relation-type": `${TextGet("FriendType")} ${FriendListSortingMode === "RelationType" ? sortingSymbol : "↕"}`, "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); 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[]} */ /** @type {FriendRawData[]} */
@ -621,25 +658,20 @@ function FriendListLoadFriendList(data) {
const originalChatRoomName = friend.ChatRoomName || ''; const originalChatRoomName = friend.ChatRoomName || '';
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`); const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`);
const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') || undefined); const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') || undefined);
let caption = '';
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (friend.ChatRoomSpace || ''); const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (friend.ChatRoomSpace || '');
const canBeep = true; 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({ friendRawData.push({
memberName: friend.MemberName, memberName: friend.MemberName,
memberNumber: friend.MemberNumber, memberNumber: friend.MemberNumber,
chatRoom: { chatRoom: {
name: originalChatRoomName, name: originalChatRoomName,
caption: caption, caption: chatRoomName || "-",
canSearchRoom: canSearchRoom, canSearchRoom: canSearchRoom,
types: [
chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[friend.ChatRoomSpace ?? ""] : null,
friend.Private ? FriendListIconMapping.Private : null,
].filter(Boolean),
}, },
beep: { beep: {
canBeep: canBeep, canBeep: canBeep,
@ -653,18 +685,9 @@ function FriendListLoadFriendList(data) {
const beepData = FriendListBeepLog[i]; const beepData = FriendListBeepLog[i];
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${beepData.ChatRoomSpace || "F"}`); const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${beepData.ChatRoomSpace || "F"}`);
const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') || undefined); const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') || undefined);
let chatRoomCaption = '';
let beepCaption = ''; let beepCaption = '';
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (beepData.ChatRoomSpace || ''); 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 = []; const rawBeepCaption = [];
if (beepData.Sent) { if (beepData.Sent) {
rawBeepCaption.push(SentCaption); rawBeepCaption.push(SentCaption);
@ -683,8 +706,12 @@ function FriendListLoadFriendList(data) {
memberNumber: beepData.MemberNumber, memberNumber: beepData.MemberNumber,
chatRoom: { chatRoom: {
name: beepData.ChatRoomName, name: beepData.ChatRoomName,
caption: chatRoomCaption, caption: chatRoomName || "-",
canSearchRoom: canSearchRoom, canSearchRoom: canSearchRoom,
types: [
chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[beepData.ChatRoomSpace ?? ""] : null,
beepData.Private ? FriendListIconMapping.Private : null,
].filter(Boolean),
}, },
beep: { beep: {
beepIndex: i, beepIndex: i,
@ -718,18 +745,18 @@ function FriendListLoadFriendList(data) {
friendRawData.forEach(friend => { friendRawData.forEach(friend => {
const row = ElementCreate({ const row = ElementCreate({
tag: "div", tag: "tr",
classList: ['friend-list-row'], classList: ['friend-list-row'],
children: [ children: [
{ {
tag: "span", tag: "td",
classList: ['friend-list-column', 'MemberName'], classList: ['friend-list-column', 'MemberName'],
children: [ children: [
friend.memberName friend.memberName
], ],
}, },
{ {
tag: "span", tag: "td",
classList: ['friend-list-column', 'MemberNumber'], classList: ['friend-list-column', 'MemberNumber'],
children: [ children: [
friend.memberNumber.toString() friend.memberNumber.toString()
@ -740,20 +767,96 @@ function FriendListLoadFriendList(data) {
if (friend.chatRoom) { if (friend.chatRoom) {
if (!friend.chatRoom.name || !friend.chatRoom.canSearchRoom) { if (!friend.chatRoom.name || !friend.chatRoom.canSearchRoom) {
row.appendChild(ElementCreate({ // Sorting is performed via each cell's `textContent`,
tag: "span", // so explicitly prepend an invisible node with some sorting key
classList: ['friend-list-column', 'ChatRoomName'], let totalSortKey = "";
innerHTML: friend.chatRoom.caption, 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) { } else if (friend.chatRoom.canSearchRoom) {
row.appendChild(ElementCreate({ // Sorting is performed via each cell's `textContent`,
tag: "button", // so explicitly prepend an invisible node with some sorting key
classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'], let totalSortKey = "";
innerHTML: friend.chatRoom.caption, const imgContainer = ElementCreate({
eventListeners: { tag: "td",
click: () => FriendListChatSearch(friend.chatRoom.name), 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: { { button: {
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content'], classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content'],
children: [friend.beep.caption], children: [friend.beep.caption],
attributes: { role: "cell" },
}}, }},
), ),
ElementButton.Create( ElementButton.Create(
@ -776,12 +880,13 @@ function FriendListLoadFriendList(data) {
{ button: { { button: {
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-beeps-content'], classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-beeps-content'],
children: [friend.beep.caption], children: [friend.beep.caption],
attributes: { role: "cell" },
}}, }},
), ),
); );
} else { } else {
row.appendChild(ElementCreate({ row.appendChild(ElementCreate({
tag: "span", tag: "td",
classList: ['friend-list-column'], classList: ['friend-list-column'],
children: [ children: [
friend.beep.caption friend.beep.caption
@ -792,14 +897,18 @@ function FriendListLoadFriendList(data) {
if (friend.relationType) { if (friend.relationType) {
row.appendChild(ElementCreate({ row.appendChild(ElementCreate({
tag: "span", tag: "td",
classList: ['friend-list-column', 'RelationType', 'mode-specific-content', 'fl-all-friends-content'], classList: ['friend-list-column', 'RelationType', 'mode-specific-content', 'fl-all-friends-content'],
children: [ children: [
{ {
tag: 'img', tag: "img",
attributes: { attributes: {
src: relationTypeIcons[friend.relationType], src: relationTypeIcons[friend.relationType],
} decoding: "async",
loading: "lazy",
"aria-hidden": "true",
},
classList: ["friend-list-icon-small"],
}, },
FriendTypeCaption[friend.relationType] FriendTypeCaption[friend.relationType]
], ],
@ -813,7 +922,7 @@ function FriendListLoadFriendList(data) {
{ button: { { button: {
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'], classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
children: [FriendListConfirmDelete.includes(friend.memberNumber) ? ConfirmDeleteCaption : DeleteCaption], children: [FriendListConfirmDelete.includes(friend.memberNumber) ? ConfirmDeleteCaption : DeleteCaption],
attributes: { disabled: !friend.canDelete }, attributes: { disabled: !friend.canDelete, role: "cell" },
}} }}
)); ));

View file

@ -5,7 +5,8 @@ MemberName,Name
MemberNickname,Nickname MemberNickname,Nickname
MemberNumber,Member number MemberNumber,Member number
ChatRoomName,Chat room ChatRoomName,Chat room
FriendType,Relation type ChatRoomType,Room type
RelationType,Relation type
ActionFriends,Send a Beep ActionFriends,Send a Beep
ActionRead,Read a Beep ActionRead,Read a Beep
ActionDelete,Delete a Friend ActionDelete,Delete a Friend
@ -21,3 +22,8 @@ TypeOwner,Owner
TypeLover,Lover TypeLover,Lover
TypeSubmissive,Submissive TypeSubmissive,Submissive
TypeFriend,Friend TypeFriend,Friend
TypeFemale,Femaly-only room
TypeMale,Male-only room
TypeMixed,Mixed male/female room
TypeAsylum,Asylum room
TypePrivate,Private room

1 OnlineFriends Online friends
5 MemberNickname Nickname
6 MemberNumber Member number
7 ChatRoomName Chat room
8 FriendType ChatRoomType Relation type Room type
9 RelationType Relation type
10 ActionFriends Send a Beep
11 ActionRead Read a Beep
12 ActionDelete Delete a Friend
22 TypeLover Lover
23 TypeSubmissive Submissive
24 TypeFriend Friend
25 TypeFemale Femaly-only room
26 TypeMale Male-only room
27 TypeMixed Mixed male/female room
28 TypeAsylum Asylum room
29 TypePrivate Private room

View file

@ -276,8 +276,11 @@ function PreferenceDecrementArousalFactor(factor) {
* Exits the preference screen * Exits the preference screen
*/ */
function PreferenceSubscreenArousalExit() { function PreferenceSubscreenArousalExit() {
return true;
}
function PreferenceSubscreenArousalUnload() {
Player.FocusGroup = null; Player.FocusGroup = null;
CharacterAppearanceForceUpCharacter = -1; CharacterAppearanceForceUpCharacter = -1;
CharacterLoadCanvas(Player); CharacterLoadCanvas(Player);
return true;
} }

View file

@ -78,6 +78,10 @@ function PreferenceSubscreenAudioClick() {
* Exits the preference screen. * Exits the preference screen.
*/ */
function PreferenceSubscreenAudioExit() { function PreferenceSubscreenAudioExit() {
return true;
}
function PreferenceSubscreenAudioUnload() {
// If audio has been disabled for notifications, disable each individual notification audio setting // If audio has been disabled for notifications, disable each individual notification audio setting
if (!Player.AudioSettings.Notifications) { if (!Player.AudioSettings.Notifications) {
for (const setting in Player.NotificationSettings) { for (const setting in Player.NotificationSettings) {
@ -85,5 +89,4 @@ function PreferenceSubscreenAudioExit() {
if (typeof audio === 'number' && audio > 0) Player.NotificationSettings[setting].Audio = NotificationAudioType.NONE; if (typeof audio === 'number' && audio > 0) Player.NotificationSettings[setting].Audio = NotificationAudioType.NONE;
} }
} }
return true;
} }

View file

@ -88,7 +88,10 @@ function PreferenceSubscreenCensoredWordsClick() {
* Exits the preference screen * Exits the preference screen
*/ */
function PreferenceSubscreenCensoredWordsExit() { function PreferenceSubscreenCensoredWordsExit() {
ElementRemove("InputWord");
Player.ChatSettings.CensoredWordsList = PreferenceCensoredWordsList.join("|");
return true; return true;
} }
function PreferenceSubscreenCensoredWordsUnload() {
ElementRemove("InputWord");
Player.ChatSettings.CensoredWordsList = PreferenceCensoredWordsList.join("|");
}

View file

@ -82,6 +82,9 @@ function PreferenceSubscreenControllerClick() {
* Exits the preference screen * Exits the preference screen
*/ */
function PreferenceSubscreenControllerExit() { function PreferenceSubscreenControllerExit() {
ControllerStopCalibration(true);
return true; return true;
} }
function PreferenceSubscreenControllerUnload() {
ControllerStopCalibration(true);
}

View file

@ -68,17 +68,12 @@ function PreferenceSubscreenExtensionsClick() {
}); });
} }
function PreferenceSubscreenExtensionsUnload() {
PreferenceExtensionsCurrent?.unload?.();
}
function PreferenceSubscreenExtensionsExit() { function PreferenceSubscreenExtensionsExit() {
if (PreferenceExtensionsCurrent) { if (PreferenceExtensionsCurrent) {
if (PreferenceExtensionsCurrent.exit() ?? true) { const validExit = PreferenceExtensionsCurrent.exit();
PreferenceSubscreenExtensionsClear(); if (validExit === false) return false;
return false; PreferenceSubscreenExtensionsClear();
} return false;
} }
return true; return true;
} }
@ -95,3 +90,13 @@ function PreferenceSubscreenExtensionsClear() {
// Reload the extension settings // Reload the extension settings
PreferenceSubscreenExtensionsLoad(); 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;
}

View file

@ -103,8 +103,8 @@ function PreferenceSubscreenGeneralClick() {
} }
/** /**
* Exits the preference screen. Cleans up elements that are not needed anymore * Exits the preference screen. Block exit when the color picker is active.
* If the selected color is invalid, the player cannot leave the screen. * @returns {boolean} - Returns false if the color picker is active and input is not valid
*/ */
function PreferenceSubscreenGeneralExit() { function PreferenceSubscreenGeneralExit() {
if (PreferenceSubscreenGeneralColorPicker) return false; if (PreferenceSubscreenGeneralColorPicker) return false;
@ -115,13 +115,23 @@ function PreferenceSubscreenGeneralExit() {
return false; 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; 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");
}

View file

@ -211,6 +211,13 @@ function PreferenceSubscreenGraphicsClick() {
} }
function PreferenceSubscreenGraphicsExit() { function PreferenceSubscreenGraphicsExit() {
return true;
}
/**
* Finalize graphics setting when the screen is unloaded
*/
function PreferenceSubscreenGraphicsUnload() {
// Reload WebGL if graphic settings have changed. // Reload WebGL if graphic settings have changed.
const currentOptions = GLDrawGetOptions(); const currentOptions = GLDrawGetOptions();
if ( if (
@ -223,5 +230,4 @@ function PreferenceSubscreenGraphicsExit() {
GLDrawSetOptions(PreferenceGraphicsWebGLOptions); GLDrawSetOptions(PreferenceGraphicsWebGLOptions);
GLDrawResetCanvas(); GLDrawResetCanvas();
} }
return true;
} }

View file

@ -185,7 +185,13 @@ function PreferenceNotificationsClickSetting(Left, Top, Setting, EventType) {
* Exits the preference screen. Resets the test notifications. * Exits the preference screen. Resets the test notifications.
*/ */
function PreferenceSubscreenNotificationsExit() { 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 // If any of the settings now have audio enabled, enable the AudioSettings setting as well
let enableAudio = false; let enableAudio = false;
for (const setting in Player.NotificationSettings) { for (const setting in Player.NotificationSettings) {
@ -195,5 +201,4 @@ function PreferenceSubscreenNotificationsExit() {
if (enableAudio) Player.AudioSettings.Notifications = true; if (enableAudio) Player.AudioSettings.Notifications = true;
NotificationReset(NotificationEventType.TEST); NotificationReset(NotificationEventType.TEST);
return true;
} }

View file

@ -40,6 +40,7 @@ const PreferenceSubscreens = [
run: () => PreferenceSubscreenGeneralRun(), run: () => PreferenceSubscreenGeneralRun(),
click: () => PreferenceSubscreenGeneralClick(), click: () => PreferenceSubscreenGeneralClick(),
exit: () => PreferenceSubscreenGeneralExit(), exit: () => PreferenceSubscreenGeneralExit(),
unload: () => PreferenceSubscreenGeneralUnload(),
}, },
{ {
name: "Difficulty", name: "Difficulty",
@ -64,6 +65,7 @@ const PreferenceSubscreens = [
run: () => PreferenceSubscreenCensoredWordsRun(), run: () => PreferenceSubscreenCensoredWordsRun(),
click: () => PreferenceSubscreenCensoredWordsClick(), click: () => PreferenceSubscreenCensoredWordsClick(),
exit: () => PreferenceSubscreenCensoredWordsExit(), exit: () => PreferenceSubscreenCensoredWordsExit(),
unload: () => PreferenceSubscreenCensoredWordsUnload(),
}, },
{ {
name: "Audio", name: "Audio",
@ -71,6 +73,7 @@ const PreferenceSubscreens = [
run: () => PreferenceSubscreenAudioRun(), run: () => PreferenceSubscreenAudioRun(),
click: () => PreferenceSubscreenAudioClick(), click: () => PreferenceSubscreenAudioClick(),
exit: () => PreferenceSubscreenAudioExit(), exit: () => PreferenceSubscreenAudioExit(),
unload: () => PreferenceSubscreenAudioUnload(),
}, },
{ {
name: "Arousal", name: "Arousal",
@ -78,6 +81,7 @@ const PreferenceSubscreens = [
run: () => PreferenceSubscreenArousalRun(), run: () => PreferenceSubscreenArousalRun(),
click: () => PreferenceSubscreenArousalClick(), click: () => PreferenceSubscreenArousalClick(),
exit: () => PreferenceSubscreenArousalExit(), exit: () => PreferenceSubscreenArousalExit(),
unload: () => PreferenceSubscreenArousalUnload(),
}, },
{ {
name: "Security", name: "Security",
@ -85,6 +89,7 @@ const PreferenceSubscreens = [
run: () => PreferenceSubscreenSecurityRun(), run: () => PreferenceSubscreenSecurityRun(),
click: () => PreferenceSubscreenSecurityClick(), click: () => PreferenceSubscreenSecurityClick(),
exit: () => PreferenceSubscreenSecurityExit(), exit: () => PreferenceSubscreenSecurityExit(),
unload: () => PreferenceSubscreenSecurityUnload(),
}, },
{ {
name: "Online", name: "Online",
@ -97,6 +102,7 @@ const PreferenceSubscreens = [
run: () => PreferenceSubscreenVisibilityRun(), run: () => PreferenceSubscreenVisibilityRun(),
click: () => PreferenceSubscreenVisibilityClick(), click: () => PreferenceSubscreenVisibilityClick(),
exit: () => PreferenceSubscreenVisibilityExit(), exit: () => PreferenceSubscreenVisibilityExit(),
unload: () => PreferenceSubscreenVisibilityUnload(),
}, },
{ {
name: "Immersion", name: "Immersion",
@ -110,12 +116,14 @@ const PreferenceSubscreens = [
run: () => PreferenceSubscreenGraphicsRun(), run: () => PreferenceSubscreenGraphicsRun(),
click: () => PreferenceSubscreenGraphicsClick(), click: () => PreferenceSubscreenGraphicsClick(),
exit: () => PreferenceSubscreenGraphicsExit(), exit: () => PreferenceSubscreenGraphicsExit(),
unload: () => PreferenceSubscreenGraphicsUnload(),
}, },
{ {
name: "Controller", name: "Controller",
run: () => PreferenceSubscreenControllerRun(), run: () => PreferenceSubscreenControllerRun(),
click: () => PreferenceSubscreenControllerClick(), click: () => PreferenceSubscreenControllerClick(),
exit: () => PreferenceSubscreenControllerExit(), exit: () => PreferenceSubscreenControllerExit(),
unload: () => PreferenceSubscreenControllerUnload(),
}, },
{ {
name: "Notifications", name: "Notifications",
@ -123,6 +131,7 @@ const PreferenceSubscreens = [
run: () => PreferenceSubscreenNotificationsRun(), run: () => PreferenceSubscreenNotificationsRun(),
click: () => PreferenceSubscreenNotificationsClick(), click: () => PreferenceSubscreenNotificationsClick(),
exit: () => PreferenceSubscreenNotificationsExit(), exit: () => PreferenceSubscreenNotificationsExit(),
unload: () => PreferenceSubscreenNotificationsUnload(),
}, },
{ {
name: "Gender", name: "Gender",
@ -135,6 +144,7 @@ const PreferenceSubscreens = [
run: () => PreferenceSubscreenScriptsRun(), run: () => PreferenceSubscreenScriptsRun(),
click: () => PreferenceSubscreenScriptsClick(), click: () => PreferenceSubscreenScriptsClick(),
exit: () => PreferenceSubscreenScriptsExit(), exit: () => PreferenceSubscreenScriptsExit(),
unload: () => PreferenceSubscreenScriptsUnload(),
}, },
{ {
name: "Extensions", name: "Extensions",
@ -142,6 +152,7 @@ const PreferenceSubscreens = [
run: () => PreferenceSubscreenExtensionsRun(), run: () => PreferenceSubscreenExtensionsRun(),
click: () => PreferenceSubscreenExtensionsClick(), click: () => PreferenceSubscreenExtensionsClick(),
exit: () => PreferenceSubscreenExtensionsExit(), exit: () => PreferenceSubscreenExtensionsExit(),
unload: () => PreferenceSubscreenExtensionsUnload(),
}, },
]; ];
@ -217,11 +228,14 @@ function PreferenceClick() {
*/ */
function PreferenceExit() { function PreferenceExit() {
if (PreferenceSubscreen.name !== "Main") { if (PreferenceSubscreen.name !== "Main") {
if (PreferenceSubscreenExit()) // If we are in a subscreen, the only exit is to the main preference screen
return; PreferenceSubscreenExit();
return;
} }
// Exit the preference menus // 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 = { const P = {
ArousalSettings: Player.ArousalSettings, ArousalSettings: Player.ArousalSettings,
ChatSettings: Player.ChatSettings, ChatSettings: Player.ChatSettings,
@ -245,19 +259,31 @@ function PreferenceExit() {
CommonSetScreen("Character", "InformationSheet"); 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 * Exit from a specific subscreen by running its handler and checking its validity
*/ */
function PreferenceSubscreenExit() { function PreferenceSubscreenExit() {
let valid = true; const validExit = PreferenceSubscreen.exit?.();
if (PreferenceSubscreen.exit)
valid = 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 = ""; PreferenceMessage = "";
PreferenceSubscreen = PreferenceSubscreens.find(s => s.name === "Main"); PreferenceSubscreen = PreferenceSubscreens.find(s => s.name === "Main");
return valid;
} }
/** /**

View file

@ -190,6 +190,10 @@ function PreferenceSubscreenScriptsClick() {
} }
function PreferenceSubscreenScriptsExit() { function PreferenceSubscreenScriptsExit() {
return true;
}
function PreferenceSubscreenScriptsUnload() {
if (PreferenceScriptTimeoutHandle != null) { if (PreferenceScriptTimeoutHandle != null) {
clearTimeout(PreferenceScriptTimeoutHandle); clearTimeout(PreferenceScriptTimeoutHandle);
PreferenceScriptTimeoutHandle = null; PreferenceScriptTimeoutHandle = null;
@ -215,7 +219,6 @@ function PreferenceSubscreenScriptsExit() {
} }
} }
} }
return true;
} }
function PreferenceSubscreenScriptsWarningClick() { function PreferenceSubscreenScriptsWarningClick() {

View file

@ -54,7 +54,10 @@ function PreferenceSubscreenSecurityClick() {
* Exits the preference screen * Exits the preference screen
*/ */
function PreferenceSubscreenSecurityExit() { function PreferenceSubscreenSecurityExit() {
ElementRemove("InputEmailOld");
ElementRemove("InputEmailNew");
return true; return true;
} }
function PreferenceSubscreenSecurityUnload() {
ElementRemove("InputEmailOld");
ElementRemove("InputEmailNew");
}

View file

@ -233,9 +233,13 @@ function PreferenceVisibilityCheckboxChanged(permissionRecord, CheckSetting, Typ
* Exits the preference screen * Exits the preference screen
*/ */
function PreferenceSubscreenVisibilityExit() { function PreferenceSubscreenVisibilityExit() {
return true;
}
function PreferenceSubscreenVisibilityUnload() {
PreferenceVisibilityGroupList = []; PreferenceVisibilityGroupList = [];
PreferenceVisibilityRecord = {}; PreferenceVisibilityRecord = {};
return true;
} }
/** /**

Binary file not shown.

Before

(image error) Size: 53 KiB

Binary file not shown.

Before

(image error) Size: 91 KiB

View file

@ -1159,11 +1159,7 @@ function CraftingLoad() {
attributes: { id: CraftingID.privateLabel }, attributes: { id: CraftingID.privateLabel },
classList: ["crafting-label"], classList: ["crafting-label"],
children: [ children: [
{ ElementCheckbox.Create(CraftingID.privateCheckbox, CraftingEventListeners._ClickPrivate),
tag: "input",
attributes: { id: CraftingID.privateCheckbox, type: "checkbox" },
eventListeners: { click: CraftingEventListeners._ClickPrivate },
},
{ tag: "span", children: [TextGet("EnterPrivate")] }, { tag: "span", children: [TextGet("EnterPrivate")] },
], ],
}, },
@ -1190,11 +1186,7 @@ function CraftingLoad() {
attributes: { id: CraftingID.asciidescriptionLabel }, attributes: { id: CraftingID.asciidescriptionLabel },
classList: ["crafting-label"], classList: ["crafting-label"],
children: [ children: [
{ ElementCheckbox.Create(CraftingID.asciiDescriptionCheckbox, CraftingEventListeners._ClickAsciiDescription),
tag: "input",
attributes: { id: CraftingID.asciiDescriptionCheckbox, type: "checkbox" },
eventListeners: { click: CraftingEventListeners._ClickAsciiDescription },
},
{ tag: "span", children: [TextGet("EnterExtendedDescription")] }, { tag: "span", children: [TextGet("EnterExtendedDescription")] },
], ],
}, },

View file

@ -258,7 +258,7 @@ function ActivityCheckPrerequisites(activity, acting, acted, group) {
* @param {ItemActivity[]} allowed * @param {ItemActivity[]} allowed
* @param {Character} acting * @param {Character} acting
* @param {Character} acted * @param {Character} acted
* @param {string} needsItem * @param {ActivityNameItem} needsItem
* @param {Activity} activity * @param {Activity} activity
* @param {AssetGroup} targetGroup * @param {AssetGroup} targetGroup
*/ */
@ -342,7 +342,8 @@ function ActivityAllowedForGroup(character, groupname) {
let needsItem = activity.Prerequisite.find(p => p.startsWith("Needs-")); let needsItem = activity.Prerequisite.find(p => p.startsWith("Needs-"));
if (needsItem) { 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")) { if (activity.Name === "ShockItem" && InventoryItemHasEffect(targetedItem, "ReceiveShock")) {

View file

@ -1930,7 +1930,7 @@ function CharacterHasItemWithAttribute(C, Attribute) {
/** /**
* Checks if the character is wearing an item that allows for a specific activity * Checks if the character is wearing an item that allows for a specific activity
* @param {Character} C - The character to test for * @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 * @returns {Item[]} - A list of items allowing that activity
*/ */
function CharacterItemsForActivity(C, 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 * Checks if the character is wearing an item that allows for a specific activity
* @param {Character} C - The character to test for * @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 * @returns {boolean} - TRUE if at least one item allows that activity
*/ */
function CharacterHasItemForActivity(C, Activity) { function CharacterHasItemForActivity(C, Activity) {

View file

@ -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 * @abstract
* @template {string} [ModeType=string] - The name of the mode associated with this instance (_e.g._ {@link DialogMenuMode}) * @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) * @template [ClickedObj=any] - The underlying item or activity object of the clicked grid buttons (if applicable)

View file

@ -338,7 +338,7 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
// If we must dark the Canvas characters // If we must dark the Canvas characters
if (!C.IsPlayer() && !OverrideDark && (Player.IsBlind() || Player.HasTints())) { if (!C.IsPlayer() && !OverrideDark && (Player.IsBlind() || Player.HasTints())) {
TempCanvas.canvas.width = CanvasDrawWidth; TempCanvas.canvas.width = CanvasDrawWidth;
TempCanvas.canvas.height = CanvasDrawHeight; TempCanvas.canvas.height = CanvasDrawHeight;
TempCanvas.globalCompositeOperation = "copy"; TempCanvas.globalCompositeOperation = "copy";
TempCanvas.drawImage(Canvas, 0, 0); TempCanvas.drawImage(Canvas, 0, 0);
@ -352,7 +352,7 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
} }
const Tints = Player.GetTints(); 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.fillStyle = `rgba(${r},${g},${b},${a})`;
TempCanvas.fillRect(0, 0, Canvas.width, Canvas.height); 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 // Apply blur filter if needed
const BlurLevel = Player.GetBlurLevel(); const BlurLevel = Player.GetBlurLevel();
if (!C.IsPlayer() && !OverrideDark && BlurLevel > 0) { const needsBlur = !C.IsPlayer() && !OverrideDark && BlurLevel > 0;
if (needsBlur) {
MainCanvas.filter = `blur(${BlurLevel}px)`; MainCanvas.filter = `blur(${BlurLevel}px)`;
} }
// Draw the character // Draw the character
@ -387,7 +388,10 @@ function DrawCharacter(C, X, Y, Zoom, IsHeightResizeAllowed, DrawCanvas) {
Invert: IsInverted, Invert: IsInverted,
Mirror: 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 // Draw the arousal meter & game images on certain conditions
if (CurrentScreen != "ChatRoom" || ChatRoomHideIconState <= 1) { if (CurrentScreen != "ChatRoom" || ChatRoomHideIconState <= 1) {
@ -683,24 +687,21 @@ function DrawImageEx(
} }
// Blit the transformed image to the main canvas, applying opacity and zoom // 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.globalCompositeOperation = BlendingMode;
Canvas.globalAlpha = Alpha; 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.transform(scaleHoriz, 0, 0, scaleVert, translateX, translateY);
Canvas.scale(Zoom, Zoom);
}
if (Invert) {
Canvas.transform(1, 0, 0, -1, 0, Height);
}
if (Mirror) {
Canvas.transform(-1, 0, 0, 1, Width, 0);
}
if (SourcePos) { if (SourcePos) {
Canvas.drawImage(destCanvas, SourcePos[0], SourcePos[1], SourcePos[2], SourcePos[3], 0, 0, Width, Height); 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.drawImage(destCanvas, 0, 0);
} }
Canvas.restore(); Canvas.globalCompositeOperation = savedCompositeOperation;
Canvas.globalAlpha = savedAlpha;
Canvas.resetTransform();
return true; 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 * @param {"Center" | "Top"} Alignment - How the text should be alligned w.r.t. the Y position when wrapped over multiple lines
* @returns {void} - Nothing * @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); ControllerAddActiveArea(X, Y);
// Draw the rectangle if we need too // Draw the rectangle if we need too
@ -912,7 +915,7 @@ function DrawTextFit(Text, X, Y, Width, Color, BackColor) {
const DrawingGetTextSize = CommonMemoize( const DrawingGetTextSize = CommonMemoize(
/** @type {(Text: string, width: number) => [text: string, size: number]} */ /** @type {(Text: string, width: number) => [text: string, size: number]} */
(Text, Width) => { (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; let S;
for (S = 36; S >= 10; S = S - 2) { for (S = 36; S >= 10; S = S - 2) {
MainCanvas.font = CommonGetFont(S.toString()); MainCanvas.font = CommonGetFont(S.toString());
@ -1277,7 +1280,10 @@ function DrawRoomBackground(URL, bounds, opts) {
DrawRect(...destRect, ChatRoomCustomFilter); 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 // Draw an overlay if the character is partially blinded
if (darken < 1) { if (darken < 1) {
@ -1288,7 +1294,7 @@ function DrawRoomBackground(URL, bounds, opts) {
DrawRect(...RectGetFrame(bounds), "#000"); 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})`); 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 * @returns {string} - alpha of screen flash
*/ */
function DrawGetScreenFlashAlpha(FlashTime) { 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; if (alpha.length < 2) alpha = "0" + alpha;
return alpha; return alpha;
} }
@ -1457,7 +1463,7 @@ function DrawProcessScreenFlash() {
if (BlindFlash == true && CurrentTime < DrawingBlindFlashTimer) { if (BlindFlash == true && CurrentTime < DrawingBlindFlashTimer) {
if (Player.GetBlindLevel() == 0) { if (Player.GetBlindLevel() == 0) {
let FlashTime = DrawingBlindFlashTimer - CurrentTime; 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; 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 */ /** 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 * @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) { function DrawImageTrapezify(image, targetCanvas, topToBottomRatio, x = 0, y = 0) {
const {width, height} = image; const { width, height } = image;
let xStartTop = 0; let xStartTop = 0;
let xStartBottom = 0; let xStartBottom = 0;

View file

@ -686,12 +686,7 @@ var ElementCheckboxDropdown = {
classList: ["dropdown-checkbox-grid"], classList: ["dropdown-checkbox-grid"],
attributes: { id: `${idPrefix}-pair-${idSuffix}` }, attributes: { id: `${idPrefix}-pair-${idSuffix}` },
children: [ children: [
{ ElementCheckbox.Create(`${idPrefix}-checkbox-${idSuffix}`, listener, { checked }),
tag: "input",
classList: ["dropdown-checkbox"],
attributes: { id: `${idPrefix}-checkbox-${idSuffix}`, type: "checkbox", checked },
eventListeners: { click: listener },
},
{ {
tag: "span", tag: "span",
classList: ["dropdown-checkbox-label"], classList: ["dropdown-checkbox-label"],
@ -807,6 +802,12 @@ function ElementCreateSearchInput(id, dataCallback, options=null) {
* @namespace * @namespace
*/ */
var ElementButton = { var ElementButton = {
/**
* A unique element ID-suffix to-be assigned to buttons without an explicit ID.
* @private
*/
_idCounter: 0,
/** /**
* @private * @private
* @readonly * @readonly
@ -1217,6 +1218,7 @@ var ElementButton = {
* @returns {HTMLButtonElement} - The created button * @returns {HTMLButtonElement} - The created button
*/ */
Create: function Create(id, onClick, options=null, htmlOptions=null) { Create: function Create(id, onClick, options=null, htmlOptions=null) {
id ??= `button-${ElementButton._idCounter++}`;
let elem = /** @type {HTMLButtonElement | null} */(document.getElementById(id)); let elem = /** @type {HTMLButtonElement | null} */(document.getElementById(id));
if (elem) { if (elem) {
console.error(`Element "${id}" already exists`); console.error(`Element "${id}" already exists`);
@ -1227,7 +1229,6 @@ var ElementButton = {
const buttonOptions = htmlOptions.button ?? {}; const buttonOptions = htmlOptions.button ?? {};
const tooltipOptions = htmlOptions.tooltip ?? {}; const tooltipOptions = htmlOptions.tooltip ?? {};
id ??= `button-${Date.now()}`;
options ??= {}; options ??= {};
const image = this._ParseImage(id, options.image, htmlOptions.img); const image = this._ParseImage(id, options.image, htmlOptions.img);
const label = this._ParseLabel(id, options.label, options.labelPosition, htmlOptions.label); 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. * Return whether an element is visible or not.
* *

View file

@ -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 }; type Rect = { x: number, y: number, w: number, h: number };
/** A 4-tuple with X & Y coordinates, width and height */ /** A 4-tuple with X & Y coordinates, width and height */
@ -568,6 +585,7 @@ interface PreferenceSubscreen {
run: () => void; run: () => void;
click: () => void; click: () => void;
exit?: () => boolean; exit?: () => boolean;
unload?: () => void;
} }
interface PreferenceGenderSetting { interface PreferenceGenderSetting {
@ -883,7 +901,7 @@ interface IFriendListBeepLogMessage {
MemberName: string; MemberName: string;
ChatRoomName?: string; ChatRoomName?: string;
Private: boolean; Private: boolean;
ChatRoomSpace?: string; ChatRoomSpace?: ServerChatRoomSpace;
Sent: boolean; Sent: boolean;
Time: Date; Time: Date;
Message?: string; Message?: string;
@ -1056,6 +1074,11 @@ interface AssetLayer {
readonly GroupAlpha?: readonly Alpha.Data[]; readonly GroupAlpha?: readonly Alpha.Data[];
/** @deprecated - Superceded by {@link CreateLayerTypes} */ /** @deprecated - Superceded by {@link CreateLayerTypes} */
readonly ModuleType?: never; 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[]; readonly CreateLayerTypes: readonly string[];
/* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */ /* Specifies that this layer should not be drawn if the character is wearing any item with the given attributes */
readonly HideForAttribute: readonly AssetAttribute[] | null; readonly HideForAttribute: readonly AssetAttribute[] | null;
@ -1213,10 +1236,7 @@ interface Asset {
readonly DynamicScriptDraw: boolean; readonly DynamicScriptDraw: boolean;
/** @deprecated - superceded by {@link CreateLayerTypes} */ /** @deprecated - superceded by {@link CreateLayerTypes} */
readonly HasType?: never; readonly HasType?: never;
/** /** A list of {@link TypeRecord} keys for which a single layer expects multiple type-specific .png files. */
* A module for which the layer can have types.
* Allows one to define different module-specific assets for a single layer.
*/
readonly CreateLayerTypes: readonly string[]; readonly CreateLayerTypes: readonly string[];
/** A record that maps {@link ExtendedItemData.name} to a set with all option indices that support locks */ /** A record that maps {@link ExtendedItemData.name} to a set with all option indices that support locks */
readonly AllowLockType: null | Record<string, Set<number>>; readonly AllowLockType: null | Record<string, Set<number>>;
@ -2810,7 +2830,7 @@ interface AssetDefinitionProperties {
* A list of allowed activities * A list of allowed activities
* @see {@link Asset.AllowActivity} * @see {@link Asset.AllowActivity}
*/ */
AllowActivity?: string[]; AllowActivity?: ActivityName[];
/** /**
* A list of groups allowed activities * A list of groups allowed activities
* @see {@link Asset.AllowActivityOn} * @see {@link Asset.AllowActivityOn}
@ -2917,7 +2937,7 @@ interface AssetDefinitionProperties {
type PartialType = `${string}${number}`; 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. * @see {@link PartialType} A concatenation of a single `TypeRecord` key/value pair.
*/ */
type TypeRecord = Record<string, number>; type TypeRecord = Record<string, number>;
@ -4359,6 +4379,8 @@ interface PreferenceExtensionsSettingItem {
* Called when the extension screen is about to exit. * Called when the extension screen is about to exit.
* *
* Happens either through a click of the exit button, or the ESC key. * 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 * @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; * (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. * 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 * 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; exit: () => boolean | void;
} }