diff --git a/art/exported/characters/patterns/calico/body_main_calico.png b/art/exported/characters/patterns/calico/body_main_calico.png new file mode 100644 index 0000000..ef2acad Binary files /dev/null and b/art/exported/characters/patterns/calico/body_main_calico.png differ diff --git a/art/exported/characters/patterns/calico/body_main_calico.png.import b/art/exported/characters/patterns/calico/body_main_calico.png.import new file mode 100644 index 0000000..25f60c0 --- /dev/null +++ b/art/exported/characters/patterns/calico/body_main_calico.png.import @@ -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 diff --git a/art/exported/characters/patterns/fox/ears_pointy_short_fox.png b/art/exported/characters/patterns/fox/ears_pointy_short_fox.png new file mode 100644 index 0000000..03720dd Binary files /dev/null and b/art/exported/characters/patterns/fox/ears_pointy_short_fox.png differ diff --git a/art/exported/characters/patterns/fox/ears_pointy_short_fox.png.import b/art/exported/characters/patterns/fox/ears_pointy_short_fox.png.import new file mode 100644 index 0000000..70910ef --- /dev/null +++ b/art/exported/characters/patterns/fox/ears_pointy_short_fox.png.import @@ -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 diff --git a/art/exported/characters/patterns/fox/head_round_fox.png b/art/exported/characters/patterns/fox/head_round_fox.png index 2963975..427650f 100644 Binary files a/art/exported/characters/patterns/fox/head_round_fox.png and b/art/exported/characters/patterns/fox/head_round_fox.png differ diff --git a/art/exported/characters/patterns/paws/body_arms_paws.png b/art/exported/characters/patterns/paws/body_arms_paws.png new file mode 100644 index 0000000..4b3c2d7 Binary files /dev/null and b/art/exported/characters/patterns/paws/body_arms_paws.png differ diff --git a/art/exported/characters/patterns/paws/body_arms_paws.png.import b/art/exported/characters/patterns/paws/body_arms_paws.png.import new file mode 100644 index 0000000..877788b --- /dev/null +++ b/art/exported/characters/patterns/paws/body_arms_paws.png.import @@ -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 diff --git a/art/exported/characters/patterns/paws/body_main_paws.png b/art/exported/characters/patterns/paws/body_main_paws.png new file mode 100644 index 0000000..bd738b7 Binary files /dev/null and b/art/exported/characters/patterns/paws/body_main_paws.png differ diff --git a/art/exported/characters/patterns/paws/body_main_paws.png.import b/art/exported/characters/patterns/paws/body_main_paws.png.import new file mode 100644 index 0000000..4db3aa8 --- /dev/null +++ b/art/exported/characters/patterns/paws/body_main_paws.png.import @@ -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 diff --git a/art/exported/characters/patterns/stripes/body_main_stripes.png b/art/exported/characters/patterns/stripes/body_main_stripes.png new file mode 100644 index 0000000..1eac4f2 Binary files /dev/null and b/art/exported/characters/patterns/stripes/body_main_stripes.png differ diff --git a/art/exported/characters/patterns/stripes/body_main_stripes.png.import b/art/exported/characters/patterns/stripes/body_main_stripes.png.import new file mode 100644 index 0000000..28192d5 --- /dev/null +++ b/art/exported/characters/patterns/stripes/body_main_stripes.png.import @@ -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 diff --git a/art/exported/characters/patterns/tiger/body_main_tiger.png b/art/exported/characters/patterns/tiger/body_main_tiger.png new file mode 100644 index 0000000..e61d7ba Binary files /dev/null and b/art/exported/characters/patterns/tiger/body_main_tiger.png differ diff --git a/art/exported/characters/patterns/tiger/body_main_tiger.png.import b/art/exported/characters/patterns/tiger/body_main_tiger.png.import new file mode 100644 index 0000000..28f3b3d --- /dev/null +++ b/art/exported/characters/patterns/tiger/body_main_tiger.png.import @@ -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 diff --git a/art/exported/characters/patterns/tiger/head_round_tiger.png b/art/exported/characters/patterns/tiger/head_round_tiger.png new file mode 100644 index 0000000..e89fb56 Binary files /dev/null and b/art/exported/characters/patterns/tiger/head_round_tiger.png differ diff --git a/art/exported/characters/patterns/tiger/head_round_tiger.png.import b/art/exported/characters/patterns/tiger/head_round_tiger.png.import new file mode 100644 index 0000000..1b0c5e1 --- /dev/null +++ b/art/exported/characters/patterns/tiger/head_round_tiger.png.import @@ -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 diff --git a/art/exported/characters/patterns/tummy/body_main_tummy.png b/art/exported/characters/patterns/tummy/body_main_tummy.png new file mode 100644 index 0000000..241c2a8 Binary files /dev/null and b/art/exported/characters/patterns/tummy/body_main_tummy.png differ diff --git a/art/exported/characters/patterns/tummy/body_main_tummy.png.import b/art/exported/characters/patterns/tummy/body_main_tummy.png.import new file mode 100644 index 0000000..044bee3 --- /dev/null +++ b/art/exported/characters/patterns/tummy/body_main_tummy.png.import @@ -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 diff --git a/docs/ATTRIBUTION.md b/docs/ATTRIBUTION.md index 321b73d..0f5f410 100644 --- a/docs/ATTRIBUTION.md +++ b/docs/ATTRIBUTION.md @@ -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 diff --git a/network/network_profile_protocol.gd b/network/network_profile_protocol.gd index 32c44b6..104112d 100644 --- a/network/network_profile_protocol.gd +++ b/network/network_profile_protocol.gd @@ -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 diff --git a/network/network_protocol.gd b/network/network_protocol.gd index d1f8d2c..bb99d62 100644 --- a/network/network_protocol.gd +++ b/network/network_protocol.gd @@ -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( diff --git a/player/player_visual_presenter.gd b/player/player_visual_presenter.gd index 7a1ae1c..745fc3c 100644 --- a/player/player_visual_presenter.gd +++ b/player/player_visual_presenter.gd @@ -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, )), ) diff --git a/progression/character_customization_catalog.gd b/progression/character_customization_catalog.gd index d18fc7f..5a02f09 100644 --- a/progression/character_customization_catalog.gd +++ b/progression/character_customization_catalog.gd @@ -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, "")) - if resource_path.is_empty(): - return null - var texture := ResourceLoader.load(resource_path) as Texture2D - if texture != null: - _fur_pattern_textures[cache_id] = texture - return texture - return null + var resource_path := str(_fur_pattern_resource_paths.get(cache_id, "")) + if resource_path.is_empty(): + return null + var texture := ResourceLoader.load(resource_path, "Texture2D") as Texture2D + if texture != null: + _fur_pattern_textures[cache_id] = texture + return texture + + +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 diff --git a/tests/fish_hotbar_showcase_validation.gd b/tests/fish_hotbar_showcase_validation.gd index 35bb7c4..945155d 100644 --- a/tests/fish_hotbar_showcase_validation.gd +++ b/tests/fish_hotbar_showcase_validation.gd @@ -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 diff --git a/tests/fish_quality_validation.gd b/tests/fish_quality_validation.gd index 2020575..28224f4 100644 --- a/tests/fish_quality_validation.gd +++ b/tests/fish_quality_validation.gd @@ -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") diff --git a/tests/fur_pattern_validation.gd b/tests/fur_pattern_validation.gd index 0bd3eb2..40f6cdd 100644 --- a/tests/fur_pattern_validation.gd +++ b/tests/fur_pattern_validation.gd @@ -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) - 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 + var body_material := _assert_mesh_pattern( + skeleton, "body_main", body_texture ) - 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 + var arms_material := _assert_mesh_pattern( + skeleton, "body_arms", arms_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 head_material := _assert_mesh_pattern( + skeleton, "head_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 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("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 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 diff --git a/tests/world_spawn_protocol_validation.gd b/tests/world_spawn_protocol_validation.gd index 2819ba7..c9de51c 100644 --- a/tests/world_spawn_protocol_validation.gd +++ b/tests/world_spawn_protocol_validation.gd @@ -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 diff --git a/ui/profile_page.gd b/ui/profile_page.gd index 57a30cf..d380656 100644 --- a/ui/profile_page.gd +++ b/ui/profile_page.gd @@ -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] + ) + 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), ) - _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) + 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)