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

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: [
"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",

View file

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

View file

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

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

View file

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

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

View file

@ -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('<', '&lt;').replaceAll('>', '&gt;') || 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('<', '&lt;').replaceAll('>', '&gt;') || 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" },
}}
));

View file

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

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
*/
function PreferenceSubscreenArousalExit() {
return true;
}
function PreferenceSubscreenArousalUnload() {
Player.FocusGroup = null;
CharacterAppearanceForceUpCharacter = -1;
CharacterLoadCanvas(Player);
return true;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -233,9 +233,13 @@ function PreferenceVisibilityCheckboxChanged(permissionRecord, CheckSetting, Typ
* Exits the preference screen
*/
function PreferenceSubscreenVisibilityExit() {
return true;
}
function PreferenceSubscreenVisibilityUnload() {
PreferenceVisibilityGroupList = [];
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 },
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")] },
],
},

View file

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

View file

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

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

View file

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

View file

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

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