Expand per-part fur customization

Load authored pattern masks by body component, add custom fur colors and the themed picker UI, and advance the signed profile protocol to version 6.
This commit is contained in:
Alexander Sellite 2026-08-15 14:27:53 -04:00
parent 64c2acb773
commit 84695c473b
27 changed files with 1337 additions and 327 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://m1ujlrpjghq2"
path="res://.godot/imported/body_main_calico.png-c5644e883b1aaedc3c3114ce40eaae02.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/calico/body_main_calico.png"
dest_files=["res://.godot/imported/body_main_calico.png-c5644e883b1aaedc3c3114ce40eaae02.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bawjitj68qe5s"
path="res://.godot/imported/ears_pointy_short_fox.png-a42bd3e51180136284f946a738c44a1d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/fox/ears_pointy_short_fox.png"
dest_files=["res://.godot/imported/ears_pointy_short_fox.png-a42bd3e51180136284f946a738c44a1d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dmqpy81ves1g8"
path="res://.godot/imported/body_arms_paws.png-06ae28eef9d8a116856aac8b0c511466.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/paws/body_arms_paws.png"
dest_files=["res://.godot/imported/body_arms_paws.png-06ae28eef9d8a116856aac8b0c511466.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://nl50my4whybb"
path="res://.godot/imported/body_main_paws.png-7262be81b419a66af0dc6fa26e01a7f8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/paws/body_main_paws.png"
dest_files=["res://.godot/imported/body_main_paws.png-7262be81b419a66af0dc6fa26e01a7f8.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cmmemafdvgw5y"
path="res://.godot/imported/body_main_stripes.png-2ff942bebaea90b5e13ff6f122820689.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/stripes/body_main_stripes.png"
dest_files=["res://.godot/imported/body_main_stripes.png-2ff942bebaea90b5e13ff6f122820689.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dbakek50irjdp"
path="res://.godot/imported/body_main_tiger.png-30e81e0d87ce0d9af9d678c10fd2e5bd.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/tiger/body_main_tiger.png"
dest_files=["res://.godot/imported/body_main_tiger.png-30e81e0d87ce0d9af9d678c10fd2e5bd.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cnpjuifjm0h4i"
path="res://.godot/imported/head_round_tiger.png-fe9ec3d30703aa477bff0311b12fc252.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/tiger/head_round_tiger.png"
dest_files=["res://.godot/imported/head_round_tiger.png-fe9ec3d30703aa477bff0311b12fc252.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dv1bran6gqbd5"
path="res://.godot/imported/body_main_tummy.png-74bd92fbb462e4dbe1c1bf841db71eb0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/tummy/body_main_tummy.png"
dest_files=["res://.godot/imported/body_main_tummy.png-74bd92fbb462e4dbe1c1bf841db71eb0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -176,11 +176,19 @@ authored `mesh_style.png` filenames.
| Bengal spots, body and arms | `body_arms_bengal.png` | `art/exported/characters/patterns/bengal/body_arms_bengal.png` | `fa90bd4e74b0b770ce97bf69d069676f5d4bd1f5d649f89e09638a66b61a8470` |
| Bengal spots, main body | `body_main_bengal.png` | `art/exported/characters/patterns/bengal/body_main_bengal.png` | `8662fa198220cfdbe0aa83bdeeef7b855e20124d74f8481467f750d32b67ee8b` |
| Bengal spots, round head | `head_round_bengal.png` | `art/exported/characters/patterns/bengal/head_round_bengal.png` | `f397ed8d152e3fda8181757a2bbd01925b7b8c2c4362f515d91c7f0fbf3ddfb6` |
| Calico, main body | `body_main_calico.png` | `art/exported/characters/patterns/calico/body_main_calico.png` | `25b2389ee729a6bafafbe6b5440541b18f3c5db2808ca8d49c2d5c4d012d390c` |
| Fox, body and arms | `body_arms_fox.png` | `art/exported/characters/patterns/fox/body_arms_fox.png` | `9930808718f31d84b415902f1ba55ac2a508bb2ac127300e793d73a8a013022d` |
| Fox, main body | `body_main_fox.png` | `art/exported/characters/patterns/fox/body_main_fox.png` | `c65e4390173b8c31c9396edd053f940f0f809e47e01d18042cd7babc712dde9d` |
| Fox, short pointy ears | `ears_pointy_short_fox.png` | `art/exported/characters/patterns/fox/ears_pointy_short_fox.png` | `13cb35192d25c0136df287434fa056ce990698fc47dc50445c941f92a4ff42fb` |
| Fox, pointy head | `head_pointy_fox.png` | `art/exported/characters/patterns/fox/head_pointy_fox.png` | `71a9d2383e62884dbd3981460fcd985b18805fa907f61ffb16dce08ff9418ddf` |
| Fox, round head | `head_round_fox.png` | `art/exported/characters/patterns/fox/head_round_fox.png` | `86dd7af8a2346faf67359ad668a117a74e2408676137d6b0626403cfcf2aba73` |
| Fox, round head | `head_round_fox.png` | `art/exported/characters/patterns/fox/head_round_fox.png` | `1ac3d503bc1e47a5ae3c1a8af719c9d1be06aab2d6d4d30c179c2a021fe7d34d` |
| Fox tail | `tails_fox_fox.png` | `art/exported/characters/patterns/fox/tails_fox_fox.png` | `72da9673f038e2c3bb8aca19ed99932085fa3b162cab95366901c4e97153dd3e` |
| Paws, body and arms | `body_arms_paws.png` | `art/exported/characters/patterns/paws/body_arms_paws.png` | `38f31ecf7045510501bdd08cbc243065c2fadec481462671dd22b13e8a196e66` |
| Paws, main body | `body_main_paws.png` | `art/exported/characters/patterns/paws/body_main_paws.png` | `d52012d4a1ea3ed544582be714469ed8fec148ccd59417dda21e89c18509321b` |
| Stripes, main body | `body_main_stripes.png` | `art/exported/characters/patterns/stripes/body_main_stripes.png` | `4b1e5b412a7af3e6acd0a5bfb1b6ffd6e0814850a311fc8cc3061419a8f23555` |
| Tiger, main body | `body_main_tiger.png` | `art/exported/characters/patterns/tiger/body_main_tiger.png` | `64b6f47871802e72ba6fe15577f14949a760c6d44bb962bfbbdcddc991b2105b` |
| Tiger, round head | `head_round_tiger.png` | `art/exported/characters/patterns/tiger/head_round_tiger.png` | `6af27056b9827391a73b4ea6f3736b308fa93a03127570f90cc4d3701afd3d80` |
| Tummy, main body | `body_main_tummy.png` | `art/exported/characters/patterns/tummy/body_main_tummy.png` | `dad404614c3feae4082537c02556781ea46c664327e0dc4528804f76619af86a` |
### Tekgator shop icon contribution record

View file

@ -43,27 +43,14 @@ static func valid_apply_request(data: Variant) -> bool:
static func signature_fields(data: Dictionary) -> Array:
var appearance: Dictionary = data.get("appearance", {})
return [
var result: Array = [
str(data.get("session_id", "")),
str(data.get("request_id", "")),
str(data.get("sender_fingerprint", "")),
str(data.get("display_name", "")),
str(appearance.get("species", "")),
CharacterCustomizationCatalog.character_scale_percent(
appearance.get(
CharacterCustomizationCatalog.SCALE_CATEGORY_ID,
CharacterCustomizationCatalog.DEFAULT_CHARACTER_SCALE,
)
),
str(appearance.get("fur_pattern", "")),
str(appearance.get("fur_style", "")),
str(appearance.get("fur_color_2", "")),
str(appearance.get("fur_color_3", "")),
str(appearance.get("fur_color_4", "")),
str(appearance.get("ears", "")),
str(appearance.get("eyes", "")),
str(appearance.get("nose", "")),
str(appearance.get("mouth", "")),
str(appearance.get("tail", "")),
bool(data.get("use_anyway", false)),
]
result.append_array(
CharacterCustomizationCatalog.appearance_signature_values(appearance)
)
result.append(bool(data.get("use_anyway", false)))
return result

View file

@ -1,7 +1,7 @@
class_name NetworkProtocol
extends RefCounted
const PROTOCOL_VERSION: int = 5
const PROTOCOL_VERSION: int = 6
const GAME_BUILD: String = "prealpha"
const MAX_GAME_VERSION_LENGTH: int = 64
const MAX_DISPLAY_NAME_LENGTH: int = 24
@ -256,30 +256,17 @@ static func validate_client_hello(data: Variant) -> String:
static func client_profile_fields(data: Dictionary) -> Array:
var appearance: Dictionary = data.get("cosmetic_snapshot", {})
return [
var result: Array = [
str(data.get("game_version", "")),
str(data.get("client_nonce", "")),
str(data.get("identity_fingerprint", "")),
str(data.get("local_profile_id", "")),
str(data.get("display_name", "")),
str(appearance.get("species", "")),
CharacterCustomizationCatalog.character_scale_percent(
appearance.get(
CharacterCustomizationCatalog.SCALE_CATEGORY_ID,
CharacterCustomizationCatalog.DEFAULT_CHARACTER_SCALE,
)
),
str(appearance.get("fur_pattern", "")),
str(appearance.get("fur_style", "")),
str(appearance.get("fur_color_2", "")),
str(appearance.get("fur_color_3", "")),
str(appearance.get("fur_color_4", "")),
str(appearance.get("ears", "")),
str(appearance.get("eyes", "")),
str(appearance.get("nose", "")),
str(appearance.get("mouth", "")),
str(appearance.get("tail", "")),
]
result.append_array(
CharacterCustomizationCatalog.appearance_signature_values(appearance)
)
return result
static func make_server_hello(

View file

@ -218,10 +218,13 @@ static func _set_fur_mesh(
var mesh_instance := skeleton.get_node_or_null(mesh_name) as MeshInstance3D
if mesh_instance == null:
return
var style_field := (
CharacterCustomizationCatalog.fur_style_field_for_component(mesh_name)
)
var style_id := CharacterCustomizationCatalog.canonical_option_id(
CharacterCustomizationCatalog.FUR_STYLE_ID,
style_field,
str(snapshot.get(
CharacterCustomizationCatalog.FUR_STYLE_ID,
style_field,
CharacterCustomizationCatalog.DEFAULT_FUR_STYLE,
)),
)

View file

@ -20,6 +20,10 @@ const SNAPSHOT_IDS: PackedStringArray = [
"scale",
"fur_pattern",
"fur_style",
"fur_style_arms",
"fur_style_head",
"fur_style_ears",
"fur_style_tail",
"fur_color_2",
"fur_color_3",
"fur_color_4",
@ -47,6 +51,24 @@ const MAX_CHARACTER_SCALE: float = 1.5
const DEFAULT_CHARACTER_SCALE: float = 1.0
const CHARACTER_SCALE_STEP: float = 0.05
const FUR_STYLE_ID: String = "fur_style"
const FUR_STYLE_ARMS_ID: String = "fur_style_arms"
const FUR_STYLE_HEAD_ID: String = "fur_style_head"
const FUR_STYLE_EARS_ID: String = "fur_style_ears"
const FUR_STYLE_TAIL_ID: String = "fur_style_tail"
const FUR_STYLE_IDS: PackedStringArray = [
FUR_STYLE_ID,
FUR_STYLE_ARMS_ID,
FUR_STYLE_HEAD_ID,
FUR_STYLE_EARS_ID,
FUR_STYLE_TAIL_ID,
]
const FUR_STYLE_LABELS: Dictionary = {
FUR_STYLE_ID: "body",
FUR_STYLE_ARMS_ID: "arms",
FUR_STYLE_HEAD_ID: "head",
FUR_STYLE_EARS_ID: "ears",
FUR_STYLE_TAIL_ID: "tail",
}
const FUR_COLOR_IDS: PackedStringArray = [
"fur_pattern",
"fur_color_2",
@ -56,10 +78,14 @@ const FUR_COLOR_IDS: PackedStringArray = [
const FUR_COLOR_LABELS: Dictionary = {
"fur_pattern": "primary",
"fur_color_2": "secondary",
"fur_color_3": "accent",
"fur_color_4": "detail",
"fur_color_3": "tertiary",
"fur_color_4": "quaternary",
}
const CUSTOM_FUR_COLOR_PREFIX: String = "custom_"
const DEFAULT_FUR_STYLE: String = "solid"
const FUR_PATTERN_ASSET_ROOT: String = (
"res://art/exported/characters/patterns"
)
const FEATURE_TEXTURE_CACHE_LIMIT: int = 24
const FEATURE_CATEGORIES: PackedStringArray = ["eyes", "nose", "mouth"]
@ -117,53 +143,6 @@ const OPTIONS: Dictionary = {
"fur_color_2": FUR_COLOR_OPTIONS,
"fur_color_3": FUR_COLOR_OPTIONS,
"fur_color_4": FUR_COLOR_OPTIONS,
"fur_style": [
{"id": "solid", "label": "solid"},
{
"id": "spots_bengal",
"label": "bengal spots",
"textures": {
"body_main": (
"res://art/exported/characters/patterns/"
+ "bengal/body_main_bengal.png"
),
"body_arms": (
"res://art/exported/characters/patterns/"
+ "bengal/body_arms_bengal.png"
),
"head_round": (
"res://art/exported/characters/patterns/"
+ "bengal/head_round_bengal.png"
),
},
},
{
"id": "fox",
"label": "fox",
"textures": {
"body_main": (
"res://art/exported/characters/patterns/"
+ "fox/body_main_fox.png"
),
"body_arms": (
"res://art/exported/characters/patterns/"
+ "fox/body_arms_fox.png"
),
"head_pointy": (
"res://art/exported/characters/patterns/"
+ "fox/head_pointy_fox.png"
),
"head_round": (
"res://art/exported/characters/patterns/"
+ "fox/head_round_fox.png"
),
"tails_fox": (
"res://art/exported/characters/patterns/"
+ "fox/tails_fox_fox.png"
),
},
},
],
"ears": [
{"id": "none", "label": "none"},
{"id": "antlers_round", "label": "antlers"},
@ -196,7 +175,7 @@ const OPTIONS: Dictionary = {
const LEGACY_OPTION_ALIASES: Dictionary = {
"species": {"default": "round"},
"fur_pattern": {"solid": "white"},
"fur_style": {"bengal": "spots_bengal"},
"fur_style": {"spots_bengal": "bengal"},
"ears": {"default": "none"},
"tail": {"default": "none"},
"eyes": {"default": "simple_shine"},
@ -209,6 +188,9 @@ static var _feature_options: Dictionary = {}
static var _feature_resource_paths: Dictionary = {}
static var _feature_textures: Dictionary = {}
static var _feature_texture_cache_order: Array[String] = []
static var _fur_pattern_assets_ready: bool = false
static var _fur_pattern_options: Array = []
static var _fur_pattern_resource_paths: Dictionary = {}
static var _fur_pattern_textures: Dictionary = {}
@ -219,6 +201,10 @@ static func default_snapshot() -> Dictionary:
"scale": DEFAULT_CHARACTER_SCALE,
"fur_pattern": "white",
"fur_style": DEFAULT_FUR_STYLE,
"fur_style_arms": DEFAULT_FUR_STYLE,
"fur_style_head": DEFAULT_FUR_STYLE,
"fur_style_ears": DEFAULT_FUR_STYLE,
"fur_style_tail": DEFAULT_FUR_STYLE,
"fur_color_2": "cream",
"fur_color_3": "brown",
"fur_color_4": "red",
@ -249,6 +235,9 @@ static func options_for(category_id: String) -> Array:
if category_id in FEATURE_CATEGORIES:
_ensure_feature_assets()
return _feature_options.get(category_id, []) as Array
if category_id in FUR_STYLE_IDS:
_ensure_fur_pattern_assets()
return _fur_pattern_options
return OPTIONS.get(category_id, [])
@ -386,6 +375,98 @@ static func refresh_feature_assets() -> void:
_ensure_feature_assets()
static func refresh_fur_pattern_assets() -> void:
_fur_pattern_assets_ready = false
_fur_pattern_options.clear()
_fur_pattern_resource_paths.clear()
_fur_pattern_textures.clear()
_ensure_fur_pattern_assets()
static func _ensure_fur_pattern_assets() -> void:
if _fur_pattern_assets_ready:
return
_fur_pattern_assets_ready = true
_fur_pattern_options = [{"id": DEFAULT_FUR_STYLE, "label": "solid"}]
var root_directory := DirAccess.open(FUR_PATTERN_ASSET_ROOT)
if root_directory == null:
return
var directory_names := root_directory.get_directories()
directory_names.sort()
for directory_name: String in directory_names:
var pattern_id := _normalized_asset_id(directory_name)
if pattern_id.is_empty() or pattern_id == DEFAULT_FUR_STYLE:
continue
var pattern_directory_path := FUR_PATTERN_ASSET_ROOT.path_join(
directory_name
)
var pattern_directory := DirAccess.open(pattern_directory_path)
if pattern_directory == null:
continue
var file_names := pattern_directory.get_files()
file_names.sort()
var canonical_files: Dictionary = {}
var found_pattern_texture := false
for file_name: String in file_names:
var canonical_file_name := _canonical_feature_filename(file_name)
if (
canonical_file_name.is_empty()
or canonical_files.has(canonical_file_name)
):
continue
canonical_files[canonical_file_name] = true
var file_base_name := canonical_file_name.get_basename().to_lower()
var pattern_suffix := "_" + pattern_id
if not file_base_name.ends_with(pattern_suffix):
continue
var component_id := file_base_name.substr(
0,
file_base_name.length() - pattern_suffix.length(),
)
component_id = _normalized_asset_id(component_id)
if component_id.is_empty():
continue
var resource_path := pattern_directory_path.path_join(
canonical_file_name
)
if not ResourceLoader.exists(resource_path, "Texture2D"):
push_warning(
"Ignoring fur mask without a loadable texture: "
+ resource_path
)
continue
_fur_pattern_resource_paths[
_fur_pattern_cache_id(pattern_id, component_id)
] = resource_path
found_pattern_texture = true
if found_pattern_texture:
_fur_pattern_options.append({
"id": pattern_id,
"label": pattern_id.replace("_", " "),
})
static func _normalized_asset_id(value: String) -> String:
var result := value.to_lower()
result = result.replace("-", "_")
result = result.replace(" ", "_")
result = result.replace(".", "_")
while result.contains("__"):
result = result.replace("__", "_")
while result.begins_with("_"):
result = result.substr(1)
while result.ends_with("_"):
result = result.substr(0, result.length() - 1)
return result
static func _fur_pattern_cache_id(
pattern_id: String,
component_id: String,
) -> String:
return "%s:%s" % [pattern_id, component_id]
static func _ensure_feature_assets() -> void:
if _feature_assets_ready:
return
@ -473,6 +554,8 @@ static func category_label(category_id: String) -> String:
static func is_valid_option(category_id: String, option_id: String) -> bool:
option_id = canonical_option_id(category_id, option_id)
if category_id in FUR_COLOR_IDS and is_custom_fur_color_id(option_id):
return true
for option: Dictionary in options_for(category_id):
if str(option.get("id", "")) == option_id:
return true
@ -480,44 +563,149 @@ static func is_valid_option(category_id: String, option_id: String) -> bool:
static func canonical_option_id(category_id: String, option_id: String) -> String:
var aliases: Dictionary = LEGACY_OPTION_ALIASES.get(category_id, {})
if (
category_id in FUR_COLOR_IDS
and option_id.to_lower().begins_with(CUSTOM_FUR_COLOR_PREFIX)
):
return option_id.to_lower()
var alias_category_id := (
FUR_STYLE_ID if category_id in FUR_STYLE_IDS else category_id
)
var aliases: Dictionary = LEGACY_OPTION_ALIASES.get(alias_category_id, {})
return str(aliases.get(option_id, option_id))
static func option_color(category_id: String, option_id: String) -> Color:
var canonical_id: String = canonical_option_id(category_id, option_id)
if category_id in FUR_COLOR_IDS and is_custom_fur_color_id(canonical_id):
return Color(canonical_id.trim_prefix(CUSTOM_FUR_COLOR_PREFIX))
for option: Dictionary in options_for(category_id):
if str(option.get("id", "")) == canonical_id:
return option.get("color", Color.WHITE) as Color
return Color.WHITE
static func custom_fur_color_id(color: Color) -> String:
var opaque_color := Color(color.r, color.g, color.b, 1.0)
return CUSTOM_FUR_COLOR_PREFIX + opaque_color.to_html(false)
static func is_custom_fur_color_id(option_id: String) -> bool:
var canonical_id := option_id.to_lower()
if (
canonical_id.length() != CUSTOM_FUR_COLOR_PREFIX.length() + 6
or not canonical_id.begins_with(CUSTOM_FUR_COLOR_PREFIX)
):
return false
var hex_value := canonical_id.trim_prefix(CUSTOM_FUR_COLOR_PREFIX)
for character_index: int in range(hex_value.length()):
if hex_value[character_index] not in "0123456789abcdef":
return false
return true
static func fur_color_label(category_id: String) -> String:
return str(FUR_COLOR_LABELS.get(category_id, category_id))
static func fur_style_label(category_id: String) -> String:
return str(FUR_STYLE_LABELS.get(category_id, category_id))
static func fur_style_field_for_component(component_id: String) -> String:
if component_id == "body_arms":
return FUR_STYLE_ARMS_ID
if component_id.begins_with("head_"):
return FUR_STYLE_HEAD_ID
if component_id.begins_with("ears_"):
return FUR_STYLE_EARS_ID
if component_id.begins_with("tails_"):
return FUR_STYLE_TAIL_ID
return FUR_STYLE_ID
static func fur_component_for_style_field(
category_id: String,
snapshot: Dictionary,
) -> String:
match category_id:
FUR_STYLE_ID:
return "body_main"
FUR_STYLE_ARMS_ID:
return "body_arms"
FUR_STYLE_HEAD_ID:
var species_id := canonical_option_id(
"species", str(snapshot.get("species", "round"))
)
return "head_pointy" if species_id == "pointy" else "head_round"
FUR_STYLE_EARS_ID:
var ears_id := canonical_option_id(
"ears", str(snapshot.get("ears", "none"))
)
return "" if ears_id == "none" else "ears_" + ears_id
FUR_STYLE_TAIL_ID:
var tail_id := canonical_option_id(
"tail", str(snapshot.get("tail", "none"))
)
return "" if tail_id == "none" else "tails_" + tail_id
return ""
static func fur_pattern_options_for_field(
category_id: String,
snapshot: Dictionary,
) -> Array:
if category_id not in FUR_STYLE_IDS:
return []
_ensure_fur_pattern_assets()
var component_id := fur_component_for_style_field(category_id, snapshot)
var result: Array = []
for option: Dictionary in _fur_pattern_options:
var pattern_id := str(option.get("id", ""))
if (
pattern_id == DEFAULT_FUR_STYLE
or (
not component_id.is_empty()
and _fur_pattern_resource_paths.has(
_fur_pattern_cache_id(pattern_id, component_id)
)
)
):
result.append(option)
return result
static func fur_pattern_texture(
option_id: String,
component_id: String = "body_main",
) -> Texture2D:
_ensure_fur_pattern_assets()
var canonical_id := canonical_option_id(FUR_STYLE_ID, option_id)
if canonical_id == DEFAULT_FUR_STYLE:
return null
var cache_id := "%s:%s" % [canonical_id, component_id]
var cache_id := _fur_pattern_cache_id(canonical_id, component_id)
if _fur_pattern_textures.has(cache_id):
return _fur_pattern_textures[cache_id] as Texture2D
for option: Dictionary in options_for(FUR_STYLE_ID):
if str(option.get("id", "")) != canonical_id:
continue
var textures := option.get("textures", {}) as Dictionary
var resource_path := str(textures.get(component_id, ""))
var resource_path := str(_fur_pattern_resource_paths.get(cache_id, ""))
if resource_path.is_empty():
return null
var texture := ResourceLoader.load(resource_path) as Texture2D
var texture := ResourceLoader.load(resource_path, "Texture2D") as Texture2D
if texture != null:
_fur_pattern_textures[cache_id] = texture
return texture
return null
static func appearance_signature_values(snapshot: Dictionary) -> Array:
var result: Array = []
for category_id: String in SNAPSHOT_IDS:
if category_id == SCALE_CATEGORY_ID:
result.append(character_scale_percent(snapshot.get(
category_id,
DEFAULT_CHARACTER_SCALE,
)))
else:
result.append(str(snapshot.get(category_id, "")))
return result
static func validate_snapshot(value: Variant) -> bool:
@ -550,8 +738,15 @@ static func sanitized_snapshot(value: Variant) -> Dictionary:
snapshot.get(category_id, DEFAULT_CHARACTER_SCALE)
)
continue
var raw_option: Variant = snapshot.get(category_id, "")
if (
category_id in FUR_STYLE_IDS
and category_id != FUR_STYLE_ID
and not snapshot.has(category_id)
):
raw_option = snapshot.get(FUR_STYLE_ID, DEFAULT_FUR_STYLE)
var option_id: String = canonical_option_id(
category_id, str(snapshot.get(category_id, ""))
category_id, str(raw_option)
)
if is_valid_option(category_id, option_id):
result[category_id] = option_id

View file

@ -181,7 +181,7 @@ func _run() -> void:
var invalid_state: Dictionary = valid_state.duplicate(true)
invalid_state["display_scale"] = 1000.0
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
assert(NetworkProtocol.PROTOCOL_VERSION == 5)
assert(NetworkProtocol.PROTOCOL_VERSION == 6)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
assert(
NetworkProtocol.FISH_QUALITY_CAPABILITY

View file

@ -25,7 +25,7 @@ func _run() -> void:
_validate_mail_round_trip()
_validate_collection_mastery()
_validate_version_four_migration()
assert(NetworkProtocol.PROTOCOL_VERSION == 5)
assert(NetworkProtocol.PROTOCOL_VERSION == 6)
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
print("Fish quality validation: PASS")

View file

@ -6,19 +6,23 @@ func _initialize() -> void:
func _run() -> void:
CharacterCustomizationCatalog.refresh_fur_pattern_assets()
var legacy_snapshot := CharacterCustomizationCatalog.default_snapshot()
legacy_snapshot[CharacterCustomizationCatalog.FUR_STYLE_ID] = "spots_bengal"
for added_id: String in [
"fur_style",
"fur_color_2",
"fur_color_3",
"fur_color_4",
CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID,
CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID,
CharacterCustomizationCatalog.FUR_STYLE_EARS_ID,
CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID,
]:
legacy_snapshot.erase(added_id)
var migrated := CharacterCustomizationCatalog.sanitized_snapshot(
legacy_snapshot
)
assert(CharacterCustomizationCatalog.validate_snapshot(migrated))
assert(migrated["fur_style"] == "solid")
for style_field: String in CharacterCustomizationCatalog.FUR_STYLE_IDS:
assert(migrated[style_field] == "bengal")
var fur_colors := CharacterCustomizationCatalog.options_for("fur_pattern")
assert(fur_colors.size() == 30)
for natural_color_id: String in [
@ -38,72 +42,128 @@ func _run() -> void:
"fur_pattern",
natural_color_id,
))
var custom_color := Color("2b91c7")
var custom_color_id := CharacterCustomizationCatalog.custom_fur_color_id(
custom_color
)
assert(custom_color_id == "custom_2b91c7")
assert(
CharacterCustomizationCatalog.custom_fur_color_id(
Color(custom_color.r, custom_color.g, custom_color.b, 0.2)
)
== custom_color_id
)
for color_field: String in CharacterCustomizationCatalog.FUR_COLOR_IDS:
assert(CharacterCustomizationCatalog.is_valid_option(
color_field,
custom_color_id,
))
assert(
CharacterCustomizationCatalog.option_color(
color_field, custom_color_id
).is_equal_approx(custom_color)
)
assert(not CharacterCustomizationCatalog.is_valid_option(
"fur_pattern", "custom_2b91cg"
))
assert(not CharacterCustomizationCatalog.is_valid_option(
"fur_pattern", "custom_2b91c7ff"
))
var pattern_texture := CharacterCustomizationCatalog.fur_pattern_texture(
"spots_bengal"
)
assert(pattern_texture != null)
assert(pattern_texture.get_width() == 1024)
assert(pattern_texture.get_height() == 1024)
var arm_pattern_texture := CharacterCustomizationCatalog.fur_pattern_texture(
"spots_bengal",
"body_arms",
)
assert(arm_pattern_texture != null)
assert(arm_pattern_texture.get_width() == 1024)
assert(arm_pattern_texture.get_height() == 1024)
var fox_pattern_texture := CharacterCustomizationCatalog.fur_pattern_texture(
var pattern_ids: Array[String] = []
for option: Dictionary in CharacterCustomizationCatalog.options_for(
CharacterCustomizationCatalog.FUR_STYLE_ID
):
pattern_ids.append(str(option.get("id", "")))
assert(pattern_ids == [
"solid",
"bengal",
"calico",
"fox",
"body_main",
)
assert(fox_pattern_texture != null)
assert(fox_pattern_texture.get_width() == 1024)
assert(fox_pattern_texture.get_height() == 1024)
var fox_arm_pattern_texture := (
CharacterCustomizationCatalog.fur_pattern_texture(
"fox", "body_arms"
)
)
assert(fox_arm_pattern_texture != null)
assert(fox_arm_pattern_texture.get_width() == 1024)
assert(fox_arm_pattern_texture.get_height() == 1024)
var bengal_head_texture := (
CharacterCustomizationCatalog.fur_pattern_texture(
"spots_bengal", "head_round"
)
)
assert(bengal_head_texture != null)
assert(bengal_head_texture.get_width() == 1024)
assert(bengal_head_texture.get_height() == 1024)
var fox_head_texture := CharacterCustomizationCatalog.fur_pattern_texture(
"fox", "head_pointy"
)
assert(fox_head_texture != null)
assert(fox_head_texture.get_width() == 1024)
assert(fox_head_texture.get_height() == 1024)
var fox_round_head_texture := (
CharacterCustomizationCatalog.fur_pattern_texture(
"fox", "head_round"
)
)
assert(fox_round_head_texture != null)
assert(fox_round_head_texture.get_width() == 1024)
assert(fox_round_head_texture.get_height() == 1024)
var fox_tail_texture := CharacterCustomizationCatalog.fur_pattern_texture(
"fox", "tails_fox"
)
assert(fox_tail_texture != null)
assert(fox_tail_texture.get_width() == 1024)
assert(fox_tail_texture.get_height() == 1024)
"paws",
"stripes",
"tiger",
"tummy",
])
var patterned := migrated.duplicate(true)
var patterned := CharacterCustomizationCatalog.default_snapshot()
patterned["species"] = "round"
patterned["ears"] = "pointy_short"
patterned["tail"] = "fox"
patterned["fur_pattern"] = "orange"
patterned["fur_style"] = "spots_bengal"
patterned[CharacterCustomizationCatalog.FUR_STYLE_ID] = "calico"
patterned[CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID] = "paws"
patterned[CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID] = "tiger"
patterned[CharacterCustomizationCatalog.FUR_STYLE_EARS_ID] = "fox"
patterned[CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID] = "fox"
patterned["fur_color_2"] = "cream"
patterned["fur_color_3"] = "brown"
patterned["fur_color_4"] = "red"
assert(CharacterCustomizationCatalog.validate_snapshot(patterned))
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_ID, patterned
),
["solid", "bengal", "calico", "fox", "paws", "stripes", "tiger", "tummy"],
)
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID, patterned
),
["solid", "bengal", "fox", "paws"],
)
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID, patterned
),
["solid", "bengal", "fox", "tiger"],
)
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_EARS_ID, patterned
),
["solid", "fox"],
)
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID, patterned
),
["solid", "fox"],
)
var pointy_snapshot := patterned.duplicate(true)
pointy_snapshot["species"] = "pointy"
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID, pointy_snapshot
),
["solid", "fox"],
)
var no_accessories := patterned.duplicate(true)
no_accessories["ears"] = "none"
no_accessories["tail"] = "none"
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_EARS_ID, no_accessories
),
["solid"],
)
_assert_option_ids(
CharacterCustomizationCatalog.fur_pattern_options_for_field(
CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID, no_accessories
),
["solid"],
)
var body_texture := _assert_pattern_texture("calico", "body_main")
var arms_texture := _assert_pattern_texture("paws", "body_arms")
var head_texture := _assert_pattern_texture("tiger", "head_round")
var ears_texture := _assert_pattern_texture("fox", "ears_pointy_short")
var tail_texture := _assert_pattern_texture("fox", "tails_fox")
_assert_pattern_texture("tummy", "body_main")
_assert_pattern_texture("stripes", "body_main")
_assert_pattern_texture("fox", "head_pointy")
var visuals := PlayerVisualPresenter.instantiate_visuals()
root.add_child(visuals)
PlayerVisualPresenter.apply_appearance(visuals, patterned)
@ -121,72 +181,224 @@ func _run() -> void:
uv_max = uv_max.max(uv)
assert(uv_min.x < 0.05 and uv_min.y < 0.05)
assert(uv_max.x > 0.95 and uv_max.y > 0.95)
var material := body.material_override as ShaderMaterial
assert(material != null)
var body_material := _assert_mesh_pattern(
skeleton, "body_main", body_texture
)
var arms_material := _assert_mesh_pattern(
skeleton, "body_arms", arms_texture
)
var head_material := _assert_mesh_pattern(
skeleton, "head_round", head_texture
)
var ears_material := _assert_mesh_pattern(
skeleton, "ears_pointy_short", ears_texture
)
var tail_material := _assert_mesh_pattern(
skeleton, "tails_fox", tail_texture
)
for material: ShaderMaterial in [
body_material,
arms_material,
head_material,
ears_material,
tail_material,
]:
assert(material.shader == preload("res://player/fur_pattern.gdshader"))
assert(material.get_shader_parameter("pattern_texture") == pattern_texture)
assert(material.get_shader_parameter("base_color") == Color("c86c36"))
assert(material.get_shader_parameter("red_zone_color") == Color("e6d39b"))
assert(material.get_shader_parameter("green_zone_color") == Color("7b4a32"))
assert(material.get_shader_parameter("blue_zone_color") == Color("a9433f"))
var arms := skeleton.get_node_or_null("body_arms") as MeshInstance3D
assert(arms != null)
var arm_material := arms.material_override as ShaderMaterial
assert(arm_material != null)
assert(
arm_material.get_shader_parameter("pattern_texture")
== arm_pattern_texture
)
var round_head := skeleton.get_node_or_null("head_round") as MeshInstance3D
assert(round_head != null)
var round_head_material := round_head.material_override as ShaderMaterial
assert(round_head_material != null)
assert(
round_head_material.get_shader_parameter("pattern_texture")
== bengal_head_texture
)
var fox_snapshot := patterned.duplicate(true)
fox_snapshot["species"] = "pointy"
fox_snapshot["fur_style"] = "fox"
fox_snapshot["tail"] = "fox"
PlayerVisualPresenter.apply_appearance(visuals, fox_snapshot)
var pointy_head := (
skeleton.get_node_or_null("head_pointy") as MeshInstance3D
)
assert(pointy_head != null)
var pointy_head_material := pointy_head.material_override as ShaderMaterial
assert(pointy_head_material != null)
assert(
pointy_head_material.get_shader_parameter("pattern_texture")
== fox_head_texture
)
fox_snapshot["species"] = "round"
PlayerVisualPresenter.apply_appearance(visuals, fox_snapshot)
assert(
round_head_material.get_shader_parameter("pattern_texture")
== fox_round_head_texture
)
var fox_tail := skeleton.get_node_or_null("tails_fox") as MeshInstance3D
assert(fox_tail != null)
var fox_tail_material := fox_tail.material_override as ShaderMaterial
assert(fox_tail_material != null)
assert(
fox_tail_material.get_shader_parameter("pattern_texture")
== fox_tail_texture
)
var recolored := patterned.duplicate(true)
recolored["fur_color_3"] = "teal"
PlayerVisualPresenter.apply_appearance(visuals, recolored)
for material: ShaderMaterial in [
body_material,
arms_material,
head_material,
ears_material,
tail_material,
]:
assert(material.get_shader_parameter("green_zone_color") == Color("3a8790"))
assert(
NetworkProfileProtocol.signature_fields({"appearance": patterned})
!= NetworkProfileProtocol.signature_fields({"appearance": recolored})
)
var other_arms := patterned.duplicate(true)
other_arms[CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID] = "fox"
assert(
NetworkProfileProtocol.signature_fields({"appearance": patterned})
!= NetworkProfileProtocol.signature_fields({"appearance": other_arms})
)
var custom_snapshot := patterned.duplicate(true)
custom_snapshot["fur_pattern"] = custom_color_id
assert(CharacterCustomizationCatalog.validate_snapshot(custom_snapshot))
PlayerVisualPresenter.apply_appearance(visuals, custom_snapshot)
for material: ShaderMaterial in [
body_material,
arms_material,
head_material,
ears_material,
tail_material,
]:
assert(
(material.get_shader_parameter("base_color") as Color).is_equal_approx(
custom_color
)
)
assert(
NetworkProfileProtocol.signature_fields({"appearance": patterned})
!= NetworkProfileProtocol.signature_fields({"appearance": custom_snapshot})
)
PlayerVisualPresenter.apply_appearance(visuals, migrated)
var solid := CharacterCustomizationCatalog.default_snapshot()
PlayerVisualPresenter.apply_appearance(visuals, solid)
assert(body.material_override is StandardMaterial3D)
await _validate_fur_color_ui(patterned)
print("Fur pattern validation: PASS")
visuals.queue_free()
await process_frame
quit()
func _validate_fur_color_ui(snapshot: Dictionary) -> void:
var profile_page := ProfilePage.new()
profile_page.size = Vector2(1280.0, 720.0)
root.add_child(profile_page)
await process_frame
profile_page.set("_draft_appearance", snapshot.duplicate(true))
profile_page.call("_select_category", "fur_pattern")
profile_page.call("_select_fur_section", "colors")
await process_frame
await process_frame
var color_panel := profile_page.find_child(
"FurColorOptionsPanel", true, false
) as PanelContainer
var channel_grid := profile_page.find_child(
"FurColorChannelGrid", true, false
) as GridContainer
var palette_grid := profile_page.find_child(
"FurPaletteGrid", true, false
) as GridContainer
var picker := profile_page.find_child(
"FurCustomColorPicker", true, false
) as ColorPickerButton
var picker_popup := picker.get_popup()
var picker_control := picker.get_picker()
assert(color_panel != null and color_panel.size.x > 360.0)
assert(channel_grid != null and channel_grid.columns == 4)
assert(channel_grid.get_child_count() == 4)
assert(palette_grid != null and palette_grid.columns == 10)
assert(palette_grid.get_child_count() == 30)
for option_button: Button in palette_grid.get_children():
assert(
option_button.custom_minimum_size
== Vector2.ONE * ProfilePage.FUR_PALETTE_SWATCH_SIZE
)
for color_index: int in CharacterCustomizationCatalog.FUR_COLOR_IDS.size():
var category_id: String = (
CharacterCustomizationCatalog.FUR_COLOR_IDS[color_index]
)
var channel_button := profile_page.find_child(
"FurColorSlot%d" % (color_index + 1), true, false
) as Button
assert(channel_button != null)
var swatch := channel_button.get_node(
"ChannelContents/ActiveColorSwatch"
) as Panel
var channel_label := channel_button.get_node(
"ChannelContents/ChannelNumber"
) as Label
assert(swatch.size.is_equal_approx(
Vector2.ONE * ProfilePage.FUR_CHANNEL_SWATCH_SIZE
))
assert(swatch.size.y <= channel_label.size.y)
var swatch_style := swatch.get_theme_stylebox(
"panel"
) as StyleBoxFlat
assert(swatch_style != null)
assert(
swatch_style.corner_radius_top_left
== roundi(ProfilePage.FUR_CHANNEL_SWATCH_SIZE * 0.5)
)
assert(swatch_style.bg_color.is_equal_approx(
CharacterCustomizationCatalog.option_color(
category_id,
str(snapshot[category_id]),
)
))
var picker_style := picker.get_theme_stylebox("normal") as StyleBoxFlat
assert(picker_style != null and picker_style.corner_radius_top_left == 14)
assert(picker_popup.min_size == ProfilePage.FUR_COLOR_PICKER_POPUP_SIZE)
assert(picker_popup.theme.default_font == UtilityPageStyle.TuffyFont)
var popup_style := picker_popup.get_theme_stylebox(
"panel"
) as StyleBoxFlat
assert(popup_style != null)
assert(popup_style.bg_color.a == 1.0)
assert(popup_style.corner_radius_top_left == 20)
assert(
picker_control.get_theme_constant("sv_width")
== ProfilePage.FUR_COLOR_PICKER_SV_SIZE.x
)
assert(
picker_control.get_theme_constant("sv_height")
== ProfilePage.FUR_COLOR_PICKER_SV_SIZE.y
)
assert(not picker_control.edit_alpha)
assert(not picker_control.edit_intensity)
var picker_display := picker.get_node("FurCustomColorDisplay") as Panel
var display_style := picker_display.get_theme_stylebox(
"panel"
) as StyleBoxFlat
assert(display_style != null and display_style.corner_radius_top_left == 11)
var live_custom_color := Color("2b91c7")
profile_page.call(
"_select_custom_fur_color",
live_custom_color,
CharacterCustomizationCatalog.FUR_COLOR_IDS[0],
)
var first_swatch := profile_page.find_child(
"FurColorSlot1", true, false
).get_node("ChannelContents/ActiveColorSwatch") as Panel
var first_swatch_style := first_swatch.get_theme_stylebox(
"panel"
) as StyleBoxFlat
assert(first_swatch_style.bg_color.is_equal_approx(live_custom_color))
profile_page.free()
await process_frame
func _assert_option_ids(options: Array, expected: Array[String]) -> void:
var actual: Array[String] = []
for option: Dictionary in options:
actual.append(str(option.get("id", "")))
assert(actual == expected)
func _assert_pattern_texture(
pattern_id: String,
component_id: String,
) -> Texture2D:
var texture := CharacterCustomizationCatalog.fur_pattern_texture(
pattern_id, component_id
)
assert(texture != null)
assert(texture.get_width() == 1024)
assert(texture.get_height() == 1024)
return texture
func _assert_mesh_pattern(
skeleton: Skeleton3D,
mesh_name: String,
expected_texture: Texture2D,
) -> ShaderMaterial:
var mesh := skeleton.get_node_or_null(mesh_name) as MeshInstance3D
assert(mesh != null)
var material := mesh.material_override as ShaderMaterial
assert(material != null)
assert(material.get_shader_parameter("pattern_texture") == expected_texture)
return material

View file

@ -7,7 +7,7 @@ const FishCatalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
func _initialize() -> void:
assert(NetworkProtocol.PROTOCOL_VERSION == 5)
assert(NetworkProtocol.PROTOCOL_VERSION == 6)
assert(
NetworkWorldSpawnProtocol.CAPABILITY
== NetworkProtocol.WORLD_SPAWN_CAPABILITY

View file

@ -4,7 +4,13 @@ extends Control
const CHECK_DEBOUNCE_SECONDS: float = 0.4
const OPTION_GRID_COLUMNS: int = 6
const FUR_PALETTE_GRID_COLUMNS: int = 10
const FUR_CHANNEL_ICON_SIZE: int = 20
const FUR_PATTERN_GRID_COLUMNS: int = 4
const FUR_PALETTE_SWATCH_SIZE: float = 38.0
const FUR_CHANNEL_SWATCH_SIZE: float = 22.0
const FUR_COLOR_PICKER_POPUP_SIZE: Vector2i = Vector2i(720, 560)
const FUR_COLOR_PICKER_SV_SIZE: Vector2i = Vector2i(520, 300)
const FUR_SECTION_PATTERNS: String = "patterns"
const FUR_SECTION_COLORS: String = "colors"
const FEATURE_DRAWER_ANIMATION_SECONDS: float = 0.16
const FEATURE_PREVIEW_SCAN_MAX_SIZE: int = 256
const FEATURE_PREVIEW_TEXTURE_MAX_SIZE: int = 128
@ -12,6 +18,8 @@ const FEATURE_PREVIEW_PADDING: int = 3
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const OrganizerTabType = preload("res://ui/components/organizer_tab.gd")
const GameTheme: Theme = preload("res://ui/game_theme.tres")
const AnimaleseVoiceType = preload("res://ui/animalese_voice.gd")
const TypewriterRevealType = preload("res://ui/typewriter_reveal.gd")
const VoiceProfilesType = preload(
@ -56,13 +64,18 @@ var _feature_preview_cache: Dictionary = {}
var _feature_preview_requests: Array[Dictionary] = []
var _feature_preview_generation: int = 0
var _feature_preview_worker_active: bool = false
var _fur_swatch_icon_cache: Dictionary = {}
var _expanded_feature_drawers: Dictionary = {}
var _feature_drawer_animation_key: String = ""
var _scale_value_label: Label
var _voice_preview: AnimaleseVoiceType
var _voice_preview_tween: Tween
var _active_fur_section: String = FUR_SECTION_PATTERNS
var _active_fur_pattern_id: String = CharacterCustomizationCatalog.FUR_STYLE_ID
var _active_fur_color_id: String = "fur_pattern"
var _fur_color_channel_buttons: Dictionary[String, Button] = {}
var _fur_color_channel_swatches: Dictionary[String, Panel] = {}
var _fur_custom_color_display: Panel
var _fur_custom_color_label: Label
func _ready() -> void:
@ -125,7 +138,6 @@ func set_world_pixel_size(pixel_size: int) -> void:
func activate() -> void:
visible = true
UtilityPageStyle.animate_in(self)
if _service != null:
_load_persisted()
# Keep controller page switches on the Profile navigation bubble. Focusing
@ -318,10 +330,17 @@ func _build_ui() -> void:
body.alignment = BoxContainer.ALIGNMENT_BEGIN
body.add_theme_constant_override("separation", 16)
body_margin.add_child(body)
var category_scroll := ScrollContainer.new()
category_scroll.name = "CategoryScroll"
category_scroll.custom_minimum_size.x = 120.0
category_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
category_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
category_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
body.add_child(category_scroll)
_category_list = VBoxContainer.new()
_category_list.custom_minimum_size = Vector2(120, 0)
_category_list.add_theme_constant_override("separation", 5)
body.add_child(_category_list)
category_scroll.add_child(_category_list)
_option_list = VBoxContainer.new()
_option_list.custom_minimum_size = Vector2(360, 0)
_option_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@ -402,9 +421,6 @@ func _build_ui() -> void:
UtilityPageStyle.apply_ocean_button(_keep_editing_button)
confirm_buttons.add_child(_keep_editing_button)
_voice_preview = AnimaleseVoiceType.new()
_voice_preview.name = "ProfileVoicePreview"
add_child(_voice_preview)
_build_categories()
@ -440,15 +456,12 @@ func _select_category(category_id: String) -> void:
func _refresh_options() -> void:
_cancel_feature_preview_requests()
_fur_color_channel_buttons.clear()
_fur_color_channel_swatches.clear()
_fur_custom_color_display = null
_fur_custom_color_label = null
for child: Node in _option_list.get_children():
child.queue_free()
var title := Label.new()
title.text = _category_label(_category_id)
title.add_theme_font_size_override("font_size", 18)
title.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
_option_list.add_child(title)
if _category_id == VOICE_CATEGORY_ID:
_build_voice_options()
return
@ -463,7 +476,7 @@ func _refresh_options() -> void:
_option_list.add_child(empty)
return
if _category_id == "fur_pattern":
_build_fur_color_options(options)
_build_fur_options(options)
return
if _category_id in CharacterCustomizationCatalog.FEATURE_CATEGORIES:
_build_feature_preview_options(options)
@ -630,6 +643,10 @@ func _select_call_option(call_id: String) -> void:
func _play_voice_preview() -> void:
if _voice_preview_tween != null and _voice_preview_tween.is_valid():
_voice_preview_tween.kill()
if not is_instance_valid(_voice_preview):
_voice_preview = AnimaleseVoiceType.new()
_voice_preview.name = "ProfileVoicePreview"
add_child(_voice_preview)
_voice_preview_tween = _voice_preview.speak_text(
self,
"hello there!",
@ -695,15 +712,6 @@ func _build_scale_option() -> void:
_option_list.add_child(_scale_value_label)
_update_scale_value_label(float(slider.value))
var gameplay_note := Label.new()
gameplay_note.text = "Movement and collision stay the same."
gameplay_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
gameplay_note.add_theme_font_size_override("font_size", 12)
gameplay_note.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
_option_list.add_child(gameplay_note)
func _build_feature_preview_options(_options: Array) -> void:
var scroll := ScrollContainer.new()
@ -984,48 +992,166 @@ func _resize_feature_preview_image(image: Image, max_dimension: int) -> void:
)
func _build_fur_color_options(options: Array) -> void:
var pattern_label := Label.new()
pattern_label.text = "pattern"
pattern_label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
func _build_fur_options(options: Array) -> void:
var section_tabs := HBoxContainer.new()
section_tabs.add_theme_constant_override("separation", 8)
_option_list.add_child(section_tabs)
for section_id: String in [FUR_SECTION_PATTERNS, FUR_SECTION_COLORS]:
var section_button := Button.new()
section_button.text = section_id
section_button.toggle_mode = true
section_button.button_pressed = _active_fur_section == section_id
section_button.custom_minimum_size = Vector2(132.0, 34.0)
section_button.pressed.connect(_select_fur_section.bind(section_id))
UtilityPageStyle.apply_compact_ocean_button(section_button)
section_tabs.add_child(section_button)
if _active_fur_section == FUR_SECTION_COLORS:
_build_fur_color_channels(options)
return
_build_fur_pattern_options()
func _build_fur_pattern_options() -> void:
if _active_fur_pattern_id not in CharacterCustomizationCatalog.FUR_STYLE_IDS:
_active_fur_pattern_id = CharacterCustomizationCatalog.FUR_STYLE_ID
var pattern_stack := VBoxContainer.new()
pattern_stack.name = "FurPatternStack"
pattern_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
pattern_stack.size_flags_vertical = Control.SIZE_EXPAND_FILL
pattern_stack.add_theme_constant_override("separation", -10)
_option_list.add_child(pattern_stack)
var part_tab_margin := MarginContainer.new()
part_tab_margin.name = "FurPatternPartTabMargin"
part_tab_margin.custom_minimum_size.y = 38.0
part_tab_margin.add_theme_constant_override("margin_left", 14)
pattern_stack.add_child(part_tab_margin)
var part_tabs := HBoxContainer.new()
part_tabs.name = "FurPatternPartTabs"
part_tabs.custom_minimum_size.y = 38.0
part_tabs.add_theme_constant_override("separation", 4)
part_tab_margin.add_child(part_tabs)
for style_index: int in range(
CharacterCustomizationCatalog.FUR_STYLE_IDS.size()
):
var style_field: String = (
CharacterCustomizationCatalog.FUR_STYLE_IDS[style_index]
)
_option_list.add_child(pattern_label)
var pattern_row := HBoxContainer.new()
pattern_row.add_theme_constant_override("separation", 8)
_option_list.add_child(pattern_row)
var part_tab: OrganizerTab = OrganizerTabType.new()
part_tab.text = CharacterCustomizationCatalog.fur_style_label(
style_field
)
part_tab.palette_index = mini(style_index, 2)
part_tab.custom_minimum_size = Vector2(90.0, 38.0)
part_tab.focus_mode = Control.FOCUS_ALL
part_tab.mouse_filter = Control.MOUSE_FILTER_STOP
part_tab.pressed.connect(_select_fur_pattern_part.bind(style_field))
part_tabs.add_child(part_tab)
part_tab.set_selected(
_active_fur_pattern_id == style_field,
false,
)
var pattern_panel := PanelContainer.new()
pattern_panel.name = "FurPatternOptionsPanel"
pattern_panel.custom_minimum_size.y = 180.0
pattern_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
pattern_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
pattern_panel.add_theme_stylebox_override(
"panel",
UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_FIELD, 12),
)
pattern_stack.add_child(pattern_panel)
var pattern_margin := MarginContainer.new()
pattern_margin.add_theme_constant_override("margin_left", 12)
pattern_margin.add_theme_constant_override("margin_top", 10)
pattern_margin.add_theme_constant_override("margin_right", 12)
pattern_margin.add_theme_constant_override("margin_bottom", 10)
pattern_panel.add_child(pattern_margin)
var pattern_scroll := ScrollContainer.new()
pattern_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
pattern_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
pattern_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
pattern_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
pattern_margin.add_child(pattern_scroll)
var pattern_grid := GridContainer.new()
pattern_grid.columns = FUR_PATTERN_GRID_COLUMNS
pattern_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
pattern_grid.add_theme_constant_override("h_separation", 8)
pattern_grid.add_theme_constant_override("v_separation", 8)
pattern_scroll.add_child(pattern_grid)
var selected_style_id := str(_draft_appearance.get(
CharacterCustomizationCatalog.FUR_STYLE_ID,
_active_fur_pattern_id,
CharacterCustomizationCatalog.DEFAULT_FUR_STYLE,
))
for pattern_option: Dictionary in CharacterCustomizationCatalog.options_for(
CharacterCustomizationCatalog.FUR_STYLE_ID
):
var pattern_options := (
CharacterCustomizationCatalog.fur_pattern_options_for_field(
_active_fur_pattern_id,
_draft_appearance,
)
)
var selected_style_available := false
for pattern_option: Dictionary in pattern_options:
if str(pattern_option.get("id", "")) == selected_style_id:
selected_style_available = true
break
var displayed_style_id := (
selected_style_id
if selected_style_available
else CharacterCustomizationCatalog.DEFAULT_FUR_STYLE
)
for pattern_option: Dictionary in pattern_options:
var pattern_id := str(pattern_option.get("id", ""))
var pattern_button := Button.new()
pattern_button.text = str(pattern_option.get("label", pattern_id))
pattern_button.toggle_mode = true
pattern_button.button_pressed = selected_style_id == pattern_id
pattern_button.button_pressed = displayed_style_id == pattern_id
pattern_button.custom_minimum_size = Vector2(118.0, 32.0)
pattern_button.pressed.connect(_select_option.bind(
CharacterCustomizationCatalog.FUR_STYLE_ID,
_active_fur_pattern_id,
pattern_id,
))
UtilityPageStyle.apply_compact_ocean_button(pattern_button)
pattern_row.add_child(pattern_button)
pattern_grid.add_child(pattern_button)
var channel_label := Label.new()
channel_label.text = "color channel"
channel_label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
func _build_fur_color_channels(options: Array) -> void:
var color_panel := PanelContainer.new()
color_panel.name = "FurColorOptionsPanel"
color_panel.custom_minimum_size.y = 250.0
color_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
color_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
color_panel.add_theme_stylebox_override(
"panel",
UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_FIELD, 12),
)
_option_list.add_child(channel_label)
_option_list.add_child(color_panel)
var color_margin := MarginContainer.new()
color_margin.add_theme_constant_override("margin_left", 14)
color_margin.add_theme_constant_override("margin_top", 12)
color_margin.add_theme_constant_override("margin_right", 14)
color_margin.add_theme_constant_override("margin_bottom", 12)
color_panel.add_child(color_margin)
var color_stack := VBoxContainer.new()
color_stack.name = "FurColorStack"
color_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
color_stack.size_flags_vertical = Control.SIZE_EXPAND_FILL
color_stack.add_theme_constant_override("separation", 10)
color_margin.add_child(color_stack)
var channel_grid := GridContainer.new()
channel_grid.columns = 2
channel_grid.add_theme_constant_override("h_separation", 8)
channel_grid.name = "FurColorChannelGrid"
channel_grid.columns = CharacterCustomizationCatalog.FUR_COLOR_IDS.size()
channel_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
channel_grid.add_theme_constant_override("h_separation", 10)
channel_grid.add_theme_constant_override("v_separation", 8)
_option_list.add_child(channel_grid)
for color_category_id: String in CharacterCustomizationCatalog.FUR_COLOR_IDS:
color_stack.add_child(channel_grid)
for color_index: int in range(
CharacterCustomizationCatalog.FUR_COLOR_IDS.size()
):
var color_category_id: String = (
CharacterCustomizationCatalog.FUR_COLOR_IDS[color_index]
)
var selected_color_id := str(_draft_appearance.get(
color_category_id,
"white",
@ -1035,22 +1161,16 @@ func _build_fur_color_options(options: Array) -> void:
selected_color_id,
)
var channel_button := Button.new()
channel_button.text = CharacterCustomizationCatalog.fur_color_label(
color_category_id
)
channel_button.icon = _fur_swatch_icon(selected_color)
channel_button.icon_alignment = HORIZONTAL_ALIGNMENT_LEFT
channel_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
channel_button.add_theme_constant_override(
"icon_max_width", FUR_CHANNEL_ICON_SIZE
)
channel_button.add_theme_constant_override("h_separation", 8)
channel_button.name = "FurColorSlot%d" % (color_index + 1)
channel_button.text = ""
channel_button.accessibility_name = "fur color %d" % (color_index + 1)
channel_button.tooltip_text = "Edit %s: %s" % [
CharacterCustomizationCatalog.fur_color_label(color_category_id),
_fur_option_label(options, selected_color_id),
]
channel_button.toggle_mode = true
channel_button.custom_minimum_size = Vector2(140.0, 34.0)
channel_button.custom_minimum_size = Vector2(104.0, 42.0)
channel_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
channel_button.button_pressed = (
_active_fur_color_id == color_category_id
)
@ -1059,30 +1179,61 @@ func _build_fur_color_options(options: Array) -> void:
)
UtilityPageStyle.apply_compact_ocean_button(channel_button)
channel_grid.add_child(channel_button)
var channel_contents := HBoxContainer.new()
channel_contents.name = "ChannelContents"
channel_contents.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
channel_contents.mouse_filter = Control.MOUSE_FILTER_IGNORE
channel_contents.alignment = BoxContainer.ALIGNMENT_CENTER
channel_contents.add_theme_constant_override("separation", 8)
channel_button.add_child(channel_contents)
var channel_label := Label.new()
channel_label.name = "ChannelNumber"
channel_label.text = str(color_index + 1)
channel_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
channel_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
channel_label.add_theme_font_override(
"font", UtilityPageStyle.TuffyFont
)
channel_label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
channel_contents.add_child(channel_label)
var channel_swatch := Panel.new()
channel_swatch.name = "ActiveColorSwatch"
channel_swatch.custom_minimum_size = (
Vector2.ONE * FUR_CHANNEL_SWATCH_SIZE
)
channel_swatch.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
channel_swatch.size_flags_vertical = Control.SIZE_SHRINK_CENTER
channel_swatch.mouse_filter = Control.MOUSE_FILTER_IGNORE
_set_fur_color_swatch(channel_swatch, selected_color)
channel_contents.add_child(channel_swatch)
_fur_color_channel_buttons[color_category_id] = channel_button
_fur_color_channel_swatches[color_category_id] = channel_swatch
_build_fur_palette(_active_fur_color_id, options)
_build_fur_palette(_active_fur_color_id, options, color_stack)
func _build_fur_palette(category_id: String, options: Array) -> void:
func _build_fur_palette(
category_id: String,
options: Array,
parent: VBoxContainer,
) -> void:
var selected_id: String = CharacterCustomizationCatalog.canonical_option_id(
category_id,
str(_draft_appearance.get(category_id, "white")),
)
var label := Label.new()
label.text = "%s palette" % CharacterCustomizationCatalog.fur_color_label(
category_id
)
label.add_theme_font_size_override("font_size", 14)
label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
_option_list.add_child(label)
var palette_center := CenterContainer.new()
palette_center.name = "FurPaletteCenter"
palette_center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
palette_center.size_flags_vertical = Control.SIZE_EXPAND_FILL
parent.add_child(palette_center)
var grid := GridContainer.new()
grid.name = "FurPaletteGrid"
grid.columns = FUR_PALETTE_GRID_COLUMNS
grid.add_theme_constant_override("h_separation", 6)
grid.add_theme_constant_override("v_separation", 6)
_option_list.add_child(grid)
grid.add_theme_constant_override("h_separation", 8)
grid.add_theme_constant_override("v_separation", 8)
palette_center.add_child(grid)
for option: Dictionary in options:
var option_id: String = str(option.get("id", ""))
var color: Color = CharacterCustomizationCatalog.option_color(
@ -1095,50 +1246,73 @@ func _build_fur_palette(category_id: String, options: Array) -> void:
str(option.get("label", option_id)),
]
button.toggle_mode = true
button.custom_minimum_size = Vector2(30.0, 30.0)
button.custom_minimum_size = (
Vector2.ONE * FUR_PALETTE_SWATCH_SIZE
)
button.button_pressed = selected_id == option_id
button.pressed.connect(_select_option.bind(category_id, option_id))
_apply_fur_swatch_style(button, color, button.button_pressed)
grid.add_child(button)
var selected_color := CharacterCustomizationCatalog.option_color(
category_id,
selected_id,
)
var custom_picker := ColorPickerButton.new()
custom_picker.name = "FurCustomColorPicker"
custom_picker.text = ""
custom_picker.accessibility_name = "custom color"
custom_picker.color = selected_color
custom_picker.edit_alpha = false
custom_picker.edit_intensity = false
custom_picker.custom_minimum_size = Vector2(0.0, 44.0)
custom_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
custom_picker.tooltip_text = "Choose a custom color for channel %d." % (
CharacterCustomizationCatalog.FUR_COLOR_IDS.find(category_id) + 1
)
custom_picker.color_changed.connect(
_select_custom_fur_color.bind(category_id)
)
custom_picker.popup_closed.connect(_refresh_options)
UtilityPageStyle.apply_compact_ocean_button(custom_picker)
_round_fur_color_picker_button(custom_picker)
_theme_fur_color_picker_popup(custom_picker)
parent.add_child(custom_picker)
var picker_display := Panel.new()
picker_display.name = "FurCustomColorDisplay"
picker_display.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
picker_display.offset_left = 4.0
picker_display.offset_top = 4.0
picker_display.offset_right = -4.0
picker_display.offset_bottom = -4.0
picker_display.mouse_filter = Control.MOUSE_FILTER_IGNORE
picker_display.z_index = 1
custom_picker.add_child(picker_display)
var picker_label := Label.new()
picker_label.name = "FurCustomColorLabel"
picker_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
picker_label.text = "custom color"
picker_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
picker_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
picker_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
picker_label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
picker_display.add_child(picker_label)
_fur_custom_color_display = picker_display
_fur_custom_color_label = picker_label
_update_fur_custom_color_display(selected_color)
func _fur_option_label(options: Array, option_id: String) -> String:
if CharacterCustomizationCatalog.is_custom_fur_color_id(option_id):
return "custom #%s" % option_id.trim_prefix(
CharacterCustomizationCatalog.CUSTOM_FUR_COLOR_PREFIX
)
for option: Dictionary in options:
if str(option.get("id", "")) == option_id:
return str(option.get("label", option_id))
return option_id
func _fur_swatch_icon(color: Color) -> Texture2D:
var cache_id := color.to_html(true)
if _fur_swatch_icon_cache.has(cache_id):
return _fur_swatch_icon_cache[cache_id] as Texture2D
var image := Image.create(
FUR_CHANNEL_ICON_SIZE,
FUR_CHANNEL_ICON_SIZE,
false,
Image.FORMAT_RGBA8,
)
var center := Vector2.ONE * (float(FUR_CHANNEL_ICON_SIZE) * 0.5)
var radius := float(FUR_CHANNEL_ICON_SIZE) * 0.39
for y: int in range(FUR_CHANNEL_ICON_SIZE):
for x: int in range(FUR_CHANNEL_ICON_SIZE):
var sample_position := Vector2(float(x) + 0.5, float(y) + 0.5)
var alpha := clampf(
radius + 0.8 - sample_position.distance_to(center),
0.0,
1.0,
)
image.set_pixel(
x,
y,
Color(color.r, color.g, color.b, color.a * alpha),
)
var texture := ImageTexture.create_from_image(image)
_fur_swatch_icon_cache[cache_id] = texture
return texture
func _select_fur_color_slot(category_id: String) -> void:
if category_id not in CharacterCustomizationCatalog.FUR_COLOR_IDS:
return
@ -1146,6 +1320,38 @@ func _select_fur_color_slot(category_id: String) -> void:
_refresh_options()
func _select_fur_section(section_id: String) -> void:
if section_id not in [FUR_SECTION_PATTERNS, FUR_SECTION_COLORS]:
return
_active_fur_section = section_id
_refresh_options()
func _select_custom_fur_color(color: Color, category_id: String) -> void:
if category_id not in CharacterCustomizationCatalog.FUR_COLOR_IDS:
return
var option_id := CharacterCustomizationCatalog.custom_fur_color_id(color)
_draft_appearance[category_id] = option_id
_update_fur_channel_color(
category_id,
color,
"custom #%s" % option_id.trim_prefix(
CharacterCustomizationCatalog.CUSTOM_FUR_COLOR_PREFIX
),
)
_update_fur_custom_color_display(color)
_preview.apply_appearance_profile(_draft_appearance)
_dirty = _draft_differs()
_refresh_actions()
func _select_fur_pattern_part(category_id: String) -> void:
if category_id not in CharacterCustomizationCatalog.FUR_STYLE_IDS:
return
_active_fur_pattern_id = category_id
_refresh_options()
func _apply_fur_swatch_style(
button: Button,
color: Color,
@ -1164,6 +1370,98 @@ func _apply_fur_swatch_style(
button.add_theme_stylebox_override("focus", selected_style)
func _set_fur_color_swatch(swatch: Panel, color: Color) -> void:
var swatch_style := UtilityPageStyle.rounded_style(
color,
roundi(FUR_CHANNEL_SWATCH_SIZE * 0.5),
)
swatch_style.anti_aliasing = true
swatch.add_theme_stylebox_override(
"panel", swatch_style
)
func _update_fur_channel_color(
category_id: String,
color: Color,
option_label: String,
) -> void:
var swatch: Panel = _fur_color_channel_swatches.get(category_id)
if swatch != null and is_instance_valid(swatch):
_set_fur_color_swatch(swatch, color)
var button: Button = _fur_color_channel_buttons.get(category_id)
if button != null and is_instance_valid(button):
button.tooltip_text = "Edit %s: %s" % [
CharacterCustomizationCatalog.fur_color_label(category_id),
option_label,
]
func _round_fur_color_picker_button(button: ColorPickerButton) -> void:
for state: StringName in [
&"normal", &"hover", &"pressed", &"focus", &"disabled",
]:
var style := button.get_theme_stylebox(state).duplicate() as StyleBoxFlat
if style == null:
continue
style.set_corner_radius_all(14)
style.content_margin_left = 14.0
style.content_margin_top = 14.0
style.content_margin_right = 14.0
style.content_margin_bottom = 14.0
button.add_theme_stylebox_override(state, style)
func _theme_fur_color_picker_popup(button: ColorPickerButton) -> void:
var popup: PopupPanel = button.get_popup()
var picker: ColorPicker = button.get_picker()
popup.accessibility_name = "custom fur color picker"
popup.theme = GameTheme
popup.min_size = FUR_COLOR_PICKER_POPUP_SIZE
popup.size = FUR_COLOR_PICKER_POPUP_SIZE
var popup_style := UtilityPageStyle.rounded_style(
UtilityPageStyle.OCEAN_PANEL_DEEP,
20,
)
popup_style.content_margin_left = 24.0
popup_style.content_margin_top = 24.0
popup_style.content_margin_right = 24.0
popup_style.content_margin_bottom = 24.0
popup.add_theme_stylebox_override("panel", popup_style)
picker.custom_minimum_size = Vector2(
FUR_COLOR_PICKER_POPUP_SIZE.x - 48,
FUR_COLOR_PICKER_POPUP_SIZE.y - 48,
)
picker.edit_alpha = false
picker.edit_intensity = false
picker.can_add_swatches = false
picker.presets_visible = false
picker.add_theme_constant_override("margin", 16)
picker.add_theme_constant_override("sv_width", FUR_COLOR_PICKER_SV_SIZE.x)
picker.add_theme_constant_override("sv_height", FUR_COLOR_PICKER_SV_SIZE.y)
picker.add_theme_constant_override("h_width", 38)
picker.add_theme_constant_override("label_width", 28)
func _update_fur_custom_color_display(color: Color) -> void:
if (
_fur_custom_color_display == null
or not is_instance_valid(_fur_custom_color_display)
or _fur_custom_color_label == null
or not is_instance_valid(_fur_custom_color_label)
):
return
_fur_custom_color_display.add_theme_stylebox_override(
"panel", UtilityPageStyle.rounded_style(color, 11)
)
_fur_custom_color_label.add_theme_color_override(
"font_color",
UtilityPageStyle.NAVY
if color.get_luminance() >= 0.52
else UtilityPageStyle.OCEAN_TEXT_PRIMARY,
)
func _select_option(category_id: String, option_id: String) -> void:
_draft_appearance[category_id] = option_id
_preview.apply_appearance_profile(_draft_appearance)