mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2025-04-14 12:29:15 +00:00
Merge branch 'bra-port' of ssh.gitgud.io:sin1337/Bondage-College into bra-port
This commit is contained in:
commit
1af19885b9
36 changed files with 669 additions and 278 deletions
BondageClub
Assets/Female3DCG
CSS
Icons
Screens
Character
FriendList
Preference
Inventory/ItemHandheld
Room/Crafting
Scripts
|
@ -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.
|
|
@ -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",
|
||||
|
|
|
@ -9067,7 +9067,7 @@ var AssetFemale3DCGExtended = {
|
|||
// Semi-Hood
|
||||
Property: {
|
||||
Effect: [E.BlockMouth],
|
||||
Hide: ["HairFront","HairAccessory1"],
|
||||
Hide: ["HairFront", "HairAccessory1"],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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 |
|
@ -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 {
|
||||
|
|
|
@ -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 */
|
||||
|
|
BIN
BondageClub/Icons/FemaleInverted.png
Normal file
BIN
BondageClub/Icons/FemaleInverted.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 3.5 KiB |
BIN
BondageClub/Icons/GenderInvert.png
Normal file
BIN
BondageClub/Icons/GenderInvert.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 2 KiB |
BIN
BondageClub/Icons/MaleInverted.png
Normal file
BIN
BondageClub/Icons/MaleInverted.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 3.6 KiB |
BIN
BondageClub/Icons/PrivateInvert.png
Normal file
BIN
BondageClub/Icons/PrivateInvert.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 2.1 KiB |
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ function FriendListLoad() {
|
|||
tag: 'input',
|
||||
attributes: {
|
||||
id: FriendListIDs.searchInput,
|
||||
type: 'text',
|
||||
type: 'search',
|
||||
maxLength: 100,
|
||||
},
|
||||
eventListeners: {
|
||||
|
@ -198,71 +198,87 @@ function FriendListLoad() {
|
|||
}
|
||||
),
|
||||
ElementCreate({
|
||||
tag: 'div',
|
||||
tag: 'table',
|
||||
attributes: {
|
||||
id: FriendListIDs.friendListTable
|
||||
id: FriendListIDs.friendListTable,
|
||||
"aria-labelledby": FriendListIDs.modeTitle,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
tag: 'div',
|
||||
tag: 'thead',
|
||||
attributes: {
|
||||
id: FriendListIDs.header
|
||||
},
|
||||
classList: ['friend-list-row'],
|
||||
children: [
|
||||
ElementButton.Create(
|
||||
"friend-list-member-name",
|
||||
() => FriendListChangeSortingMode("MemberName"),
|
||||
{ noStyling: true },
|
||||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link'],
|
||||
}},
|
||||
),
|
||||
ElementButton.Create(
|
||||
"friend-list-member-number",
|
||||
() => FriendListChangeSortingMode("MemberNumber"),
|
||||
{ noStyling: true },
|
||||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link'],
|
||||
}},
|
||||
),
|
||||
ElementButton.Create(
|
||||
"friend-list-chat-room-name",
|
||||
() => FriendListChangeSortingMode("ChatRoomName"),
|
||||
{ noStyling: true },
|
||||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'],
|
||||
}},
|
||||
),
|
||||
ElementButton.Create(
|
||||
"friend-list-relation-type",
|
||||
() => FriendListChangeSortingMode("RelationType"),
|
||||
{ noStyling: true },
|
||||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
|
||||
}},
|
||||
),
|
||||
{
|
||||
tag: "span",
|
||||
classList: ['friend-list-column', 'mode-specific-content', 'fl-online-friends-content'],
|
||||
tag: "tr",
|
||||
classList: ["friend-list-row"],
|
||||
children: [
|
||||
TextGet("ActionFriends")
|
||||
ElementButton.Create(
|
||||
"friend-list-member-name",
|
||||
() => FriendListChangeSortingMode("MemberName"),
|
||||
{ noStyling: true },
|
||||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link'],
|
||||
attributes: { role: "columnheader" },
|
||||
}},
|
||||
),
|
||||
ElementButton.Create(
|
||||
"friend-list-member-number",
|
||||
() => FriendListChangeSortingMode("MemberNumber"),
|
||||
{ noStyling: true },
|
||||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link'],
|
||||
attributes: { role: "columnheader" },
|
||||
}},
|
||||
),
|
||||
ElementButton.Create(
|
||||
"friend-list-chat-room-type",
|
||||
() => FriendListChangeSortingMode("ChatRoomType"),
|
||||
{ noStyling: true },
|
||||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'],
|
||||
attributes: { role: "columnheader" },
|
||||
}},
|
||||
),
|
||||
ElementButton.Create(
|
||||
"friend-list-chat-room-name",
|
||||
() => FriendListChangeSortingMode("ChatRoomName"),
|
||||
{ noStyling: true },
|
||||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'],
|
||||
attributes: { role: "columnheader" },
|
||||
}},
|
||||
),
|
||||
ElementButton.Create(
|
||||
"friend-list-relation-type",
|
||||
() => FriendListChangeSortingMode("RelationType"),
|
||||
{ noStyling: true },
|
||||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
|
||||
attributes: { role: "columnheader" },
|
||||
}},
|
||||
),
|
||||
{
|
||||
tag: "th",
|
||||
classList: ['friend-list-column', 'mode-specific-content', 'fl-online-friends-content'],
|
||||
attributes: { scope: "col" },
|
||||
children: [TextGet("ActionFriends")],
|
||||
},
|
||||
{
|
||||
tag: "th",
|
||||
classList: ['friend-list-column', 'mode-specific-content', 'fl-beeps-content'],
|
||||
attributes: { scope: "col" },
|
||||
children: [TextGet("ActionRead")],
|
||||
},
|
||||
{
|
||||
tag: "th",
|
||||
classList: ['friend-list-column', 'mode-specific-content', 'fl-all-friends-content'],
|
||||
attributes: { scope: "col" },
|
||||
children: [TextGet("ActionDelete")],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tag: "span",
|
||||
classList: ['friend-list-column', 'mode-specific-content', 'fl-beeps-content'],
|
||||
children: [
|
||||
TextGet("ActionRead")
|
||||
],
|
||||
},
|
||||
{
|
||||
tag: "span",
|
||||
classList: ['friend-list-column', 'mode-specific-content', 'fl-all-friends-content'],
|
||||
children: [
|
||||
TextGet("ActionDelete")
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -272,7 +288,7 @@ function FriendListLoad() {
|
|||
}
|
||||
},
|
||||
{
|
||||
tag: 'div',
|
||||
tag: 'tbody',
|
||||
classList: ["scroll-box"],
|
||||
attributes: {
|
||||
id: FriendListIDs.friendList
|
||||
|
@ -504,7 +520,7 @@ function FriendListBeepMenuSend() {
|
|||
ChatRoomName: FriendListBeepShowRoom ? ChatRoomData?.Name : undefined,
|
||||
ChatRoomSpace: FriendListBeepShowRoom ? ChatRoomData?.Space : undefined,
|
||||
Sent: true,
|
||||
Private: false,
|
||||
Private: FriendListBeepShowRoom ? !ChatRoomData?.Visibility.includes("All") : undefined,
|
||||
Time: new Date(),
|
||||
Message: msg || undefined
|
||||
});
|
||||
|
@ -537,6 +553,15 @@ function FriendListChatSearch(room) {
|
|||
ElementValue("InputSearch", ChatSearchMuffle(room));
|
||||
}
|
||||
|
||||
/** @satisfies {{ [key in (ServerChatRoomSpace | "Private")]: FriendListIcon }} */
|
||||
const FriendListIconMapping = {
|
||||
"": { src: "./Icons/FemaleInvert.png", tooltipKey: "TypeFemale", sortKey: "F " },
|
||||
M: { src: "./Icons/MaleInvert.png", tooltipKey: "TypeMale", sortKey: "M " },
|
||||
X: { src: "./Icons/GenderInvert.png", tooltipKey: "TypeMixed", sortKey: "FM" },
|
||||
Asylum: { src: "./Icons/Asylum.png", tooltipKey: "TypeAsylum", sortKey: "A " },
|
||||
Private: { src: "./Icons/PrivateInvert.png", tooltipKey: "TypePrivate", sortKey: "P" },
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads the friend list data into the HTML div element.
|
||||
* @param {ServerFriendInfo[]} data - An array of data, we receive from the server
|
||||
|
@ -559,7 +584,6 @@ function FriendListLoadFriendList(data) {
|
|||
const BeepCaption = InterfaceTextGet("Beep");
|
||||
const DeleteCaption = InterfaceTextGet("Delete");
|
||||
const ConfirmDeleteCaption = InterfaceTextGet("ConfirmDelete");
|
||||
const PrivateRoomCaption = InterfaceTextGet("PrivateRoom");
|
||||
const SentCaption = InterfaceTextGet("SentBeep");
|
||||
const ReceivedCaption = InterfaceTextGet("ReceivedBeep");
|
||||
const MailCaption = InterfaceTextGet("BeepWithMail");
|
||||
|
@ -601,15 +625,28 @@ function FriendListLoadFriendList(data) {
|
|||
});
|
||||
if (infoChanged) ServerPlayerRelationsSync();
|
||||
|
||||
/** @satisfies {Record<string, FriendListSortingMode>} */
|
||||
const columnHeaders = {
|
||||
"friend-list-member-name": `${TextGet("MemberName")} ${FriendListSortingMode === "MemberName" ? sortingSymbol : "↕"}`,
|
||||
"friend-list-member-number": `${TextGet("MemberNumber")} ${FriendListSortingMode === "MemberNumber" ? sortingSymbol : "↕"}`,
|
||||
"friend-list-chat-room-name": `${TextGet("ChatRoomName")} ${FriendListSortingMode === "ChatRoomName" ? sortingSymbol : "↕"}`,
|
||||
"friend-list-relation-type": `${TextGet("FriendType")} ${FriendListSortingMode === "RelationType" ? sortingSymbol : "↕"}`,
|
||||
"friend-list-member-name": "MemberName",
|
||||
"friend-list-member-number": "MemberNumber",
|
||||
"friend-list-chat-room-name": "ChatRoomName",
|
||||
"friend-list-chat-room-type": "ChatRoomType",
|
||||
"friend-list-relation-type": "RelationType",
|
||||
};
|
||||
CommonEntries(columnHeaders).forEach(([id, textContent]) => {
|
||||
CommonEntries(columnHeaders).forEach(([id, modeName]) => {
|
||||
const elem = document.getElementById(id);
|
||||
elem.textContent = textContent;
|
||||
const elemSortingSymbol = FriendListSortingMode === modeName ? sortingSymbol : "↕";
|
||||
elem.textContent = `${TextGet(modeName)} ${elemSortingSymbol}`;
|
||||
switch (elemSortingSymbol) {
|
||||
case "↑":
|
||||
elem.setAttribute("aria-sort", "ascending");
|
||||
break;
|
||||
case "↓":
|
||||
elem.setAttribute("aria-sort", "descending");
|
||||
break;
|
||||
default:
|
||||
elem.setAttribute("aria-sort", "none");
|
||||
}
|
||||
});
|
||||
|
||||
/** @type {FriendRawData[]} */
|
||||
|
@ -621,25 +658,20 @@ function FriendListLoadFriendList(data) {
|
|||
const originalChatRoomName = friend.ChatRoomName || '';
|
||||
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`);
|
||||
const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined);
|
||||
let caption = '';
|
||||
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (friend.ChatRoomSpace || '');
|
||||
const canBeep = true;
|
||||
|
||||
const rawCaption = [];
|
||||
if (chatRoomSpaceCaption && chatRoomName) rawCaption.push(`<i>${chatRoomSpaceCaption}</i>`);
|
||||
if (friend.Private) rawCaption.push(PrivateRoomCaption);
|
||||
if (chatRoomName) rawCaption.push(chatRoomName);
|
||||
if (rawCaption.length === 0) rawCaption.push('-');
|
||||
|
||||
caption = rawCaption.join(' - ');
|
||||
|
||||
friendRawData.push({
|
||||
memberName: friend.MemberName,
|
||||
memberNumber: friend.MemberNumber,
|
||||
chatRoom: {
|
||||
name: originalChatRoomName,
|
||||
caption: caption,
|
||||
caption: chatRoomName || "-",
|
||||
canSearchRoom: canSearchRoom,
|
||||
types: [
|
||||
chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[friend.ChatRoomSpace ?? ""] : null,
|
||||
friend.Private ? FriendListIconMapping.Private : null,
|
||||
].filter(Boolean),
|
||||
},
|
||||
beep: {
|
||||
canBeep: canBeep,
|
||||
|
@ -653,18 +685,9 @@ function FriendListLoadFriendList(data) {
|
|||
const beepData = FriendListBeepLog[i];
|
||||
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${beepData.ChatRoomSpace || "F"}`);
|
||||
const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined);
|
||||
let chatRoomCaption = '';
|
||||
let beepCaption = '';
|
||||
const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (beepData.ChatRoomSpace || '');
|
||||
|
||||
const rawRoomCaption = [];
|
||||
if (chatRoomSpaceCaption && chatRoomName) rawRoomCaption.push(`<i>${chatRoomSpaceCaption}</i>`);
|
||||
if (beepData.Private) rawRoomCaption.push(PrivateRoomCaption);
|
||||
if (chatRoomName) rawRoomCaption.push(chatRoomName);
|
||||
if (rawRoomCaption.length === 0) rawRoomCaption.push('-');
|
||||
|
||||
chatRoomCaption = rawRoomCaption.join(' - ');
|
||||
|
||||
const rawBeepCaption = [];
|
||||
if (beepData.Sent) {
|
||||
rawBeepCaption.push(SentCaption);
|
||||
|
@ -683,8 +706,12 @@ function FriendListLoadFriendList(data) {
|
|||
memberNumber: beepData.MemberNumber,
|
||||
chatRoom: {
|
||||
name: beepData.ChatRoomName,
|
||||
caption: chatRoomCaption,
|
||||
caption: chatRoomName || "-",
|
||||
canSearchRoom: canSearchRoom,
|
||||
types: [
|
||||
chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[beepData.ChatRoomSpace ?? ""] : null,
|
||||
beepData.Private ? FriendListIconMapping.Private : null,
|
||||
].filter(Boolean),
|
||||
},
|
||||
beep: {
|
||||
beepIndex: i,
|
||||
|
@ -718,18 +745,18 @@ function FriendListLoadFriendList(data) {
|
|||
|
||||
friendRawData.forEach(friend => {
|
||||
const row = ElementCreate({
|
||||
tag: "div",
|
||||
tag: "tr",
|
||||
classList: ['friend-list-row'],
|
||||
children: [
|
||||
{
|
||||
tag: "span",
|
||||
tag: "td",
|
||||
classList: ['friend-list-column', 'MemberName'],
|
||||
children: [
|
||||
friend.memberName
|
||||
],
|
||||
},
|
||||
{
|
||||
tag: "span",
|
||||
tag: "td",
|
||||
classList: ['friend-list-column', 'MemberNumber'],
|
||||
children: [
|
||||
friend.memberNumber.toString()
|
||||
|
@ -740,20 +767,96 @@ function FriendListLoadFriendList(data) {
|
|||
|
||||
if (friend.chatRoom) {
|
||||
if (!friend.chatRoom.name || !friend.chatRoom.canSearchRoom) {
|
||||
row.appendChild(ElementCreate({
|
||||
tag: "span",
|
||||
classList: ['friend-list-column', 'ChatRoomName'],
|
||||
innerHTML: friend.chatRoom.caption,
|
||||
}));
|
||||
// Sorting is performed via each cell's `textContent`,
|
||||
// so explicitly prepend an invisible node with some sorting key
|
||||
let totalSortKey = "";
|
||||
const imgContainer = ElementCreate({
|
||||
tag: "td",
|
||||
classList: ['friend-list-column', 'ChatRoomType'],
|
||||
children: [
|
||||
{ tag: "span", style: { display: "none" }, classList: ["friend-list-sorting-node"] },
|
||||
...friend.chatRoom.types.map(({ src, tooltipKey, sortKey }) => {
|
||||
totalSortKey += sortKey;
|
||||
return {
|
||||
tag: /** @type {const} */("div"),
|
||||
classList: ["friend-list-icon-container"],
|
||||
children: [
|
||||
{
|
||||
tag: /** @type {const} */("img"),
|
||||
attributes: { src, decoding: "async", loading: "lazy", alt: TextGet(tooltipKey) },
|
||||
classList: ["friend-list-icon"],
|
||||
},
|
||||
{
|
||||
tag: /** @type {const} */("div"),
|
||||
attributes: { role: "tooltip", "aria-hidden": "true" },
|
||||
children: [TextGet(tooltipKey)],
|
||||
classList: ["button-tooltip", "button-tooltip-right"],
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
],
|
||||
});
|
||||
imgContainer.children[0].textContent = totalSortKey + " ";
|
||||
if (imgContainer.children.length === 1) {
|
||||
imgContainer.append("-");
|
||||
}
|
||||
row.append(
|
||||
imgContainer,
|
||||
ElementCreate({
|
||||
tag: "td",
|
||||
classList: ['friend-list-column', 'ChatRoomName'],
|
||||
children: [friend.chatRoom.caption],
|
||||
style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined },
|
||||
}),
|
||||
);
|
||||
} else if (friend.chatRoom.canSearchRoom) {
|
||||
row.appendChild(ElementCreate({
|
||||
tag: "button",
|
||||
classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'],
|
||||
innerHTML: friend.chatRoom.caption,
|
||||
eventListeners: {
|
||||
click: () => FriendListChatSearch(friend.chatRoom.name),
|
||||
},
|
||||
}));
|
||||
// Sorting is performed via each cell's `textContent`,
|
||||
// so explicitly prepend an invisible node with some sorting key
|
||||
let totalSortKey = "";
|
||||
const imgContainer = ElementCreate({
|
||||
tag: "td",
|
||||
classList: ['friend-list-column', 'ChatRoomType'],
|
||||
children: [
|
||||
{ tag: "span", style: { display: "none" }, classList: ["friend-list-sorting-node"] },
|
||||
...friend.chatRoom.types.map(({ src, tooltipKey, sortKey }) => {
|
||||
totalSortKey += sortKey;
|
||||
return {
|
||||
tag: /** @type {const} */("div"),
|
||||
classList: ["friend-list-icon-container"],
|
||||
children: [
|
||||
{
|
||||
tag: /** @type {const} */("img"),
|
||||
attributes: { src, decoding: "async", loading: "lazy", alt: TextGet(tooltipKey) },
|
||||
classList: ["friend-list-icon"],
|
||||
},
|
||||
{
|
||||
tag: /** @type {const} */("div"),
|
||||
attributes: { role: "tooltip", "aria-hidden": "true" },
|
||||
children: [TextGet(tooltipKey)],
|
||||
classList: ["button-tooltip", "button-tooltip-right"],
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
],
|
||||
});
|
||||
imgContainer.children[0].textContent = totalSortKey + " ";
|
||||
if (imgContainer.children.length === 1) {
|
||||
imgContainer.append("-");
|
||||
}
|
||||
row.append(
|
||||
imgContainer,
|
||||
ElementCreate({
|
||||
tag: "td",
|
||||
classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'],
|
||||
innerHTML: friend.chatRoom.caption,
|
||||
style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined },
|
||||
eventListeners: {
|
||||
click: () => FriendListChatSearch(friend.chatRoom.name),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -767,6 +870,7 @@ function FriendListLoadFriendList(data) {
|
|||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content'],
|
||||
children: [friend.beep.caption],
|
||||
attributes: { role: "cell" },
|
||||
}},
|
||||
),
|
||||
ElementButton.Create(
|
||||
|
@ -776,12 +880,13 @@ function FriendListLoadFriendList(data) {
|
|||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-beeps-content'],
|
||||
children: [friend.beep.caption],
|
||||
attributes: { role: "cell" },
|
||||
}},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
row.appendChild(ElementCreate({
|
||||
tag: "span",
|
||||
tag: "td",
|
||||
classList: ['friend-list-column'],
|
||||
children: [
|
||||
friend.beep.caption
|
||||
|
@ -792,14 +897,18 @@ function FriendListLoadFriendList(data) {
|
|||
|
||||
if (friend.relationType) {
|
||||
row.appendChild(ElementCreate({
|
||||
tag: "span",
|
||||
tag: "td",
|
||||
classList: ['friend-list-column', 'RelationType', 'mode-specific-content', 'fl-all-friends-content'],
|
||||
children: [
|
||||
{
|
||||
tag: 'img',
|
||||
tag: "img",
|
||||
attributes: {
|
||||
src: relationTypeIcons[friend.relationType],
|
||||
}
|
||||
decoding: "async",
|
||||
loading: "lazy",
|
||||
"aria-hidden": "true",
|
||||
},
|
||||
classList: ["friend-list-icon-small"],
|
||||
},
|
||||
FriendTypeCaption[friend.relationType]
|
||||
],
|
||||
|
@ -813,7 +922,7 @@ function FriendListLoadFriendList(data) {
|
|||
{ button: {
|
||||
classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'],
|
||||
children: [FriendListConfirmDelete.includes(friend.memberNumber) ? ConfirmDeleteCaption : DeleteCaption],
|
||||
attributes: { disabled: !friend.canDelete },
|
||||
attributes: { disabled: !friend.canDelete, role: "cell" },
|
||||
}}
|
||||
));
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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("|");
|
||||
}
|
||||
|
|
|
@ -82,6 +82,9 @@ function PreferenceSubscreenControllerClick() {
|
|||
* Exits the preference screen
|
||||
*/
|
||||
function PreferenceSubscreenControllerExit() {
|
||||
ControllerStopCalibration(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
function PreferenceSubscreenControllerUnload() {
|
||||
ControllerStopCalibration(true);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -54,7 +54,10 @@ function PreferenceSubscreenSecurityClick() {
|
|||
* Exits the preference screen
|
||||
*/
|
||||
function PreferenceSubscreenSecurityExit() {
|
||||
ElementRemove("InputEmailOld");
|
||||
ElementRemove("InputEmailNew");
|
||||
return true;
|
||||
}
|
||||
|
||||
function PreferenceSubscreenSecurityUnload() {
|
||||
ElementRemove("InputEmailOld");
|
||||
ElementRemove("InputEmailNew");
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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")] },
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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")) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
38
BondageClub/Scripts/Typedef.d.ts
vendored
38
BondageClub/Scripts/Typedef.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue