Add modular character customization

This commit is contained in:
Alexander Sellite 2026-08-04 22:34:43 -04:00
parent 70d6205ec1
commit 2bcb4a3c93
34 changed files with 761 additions and 76 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dyjwgkgw0syo8"
path.s3tc="res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "c2f7c29aaaa95809e140eb308d9bb132"
}
[deps]
source_file="res://art/exported/characters/base/netfishing_base_character_eyes_simple_shine.png"
dest_files=["res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.s3tc.ctex", "res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.etc2.ctex"]
[params]
compress/mode=2
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=true
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=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dt5gitdtjdvcd"
path.s3tc="res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "f8bf25a1006c5cc2ab5323f4a758eec7"
}
[deps]
source_file="res://art/exported/characters/base/netfishing_base_character_mouth_three.png"
dest_files=["res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.s3tc.ctex", "res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.etc2.ctex"]
[params]
compress/mode=2
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=true
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=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bmho1ryst3smt"
path.s3tc="res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "3134f170152f9fa1512b41b61ecb01fc"
}
[deps]
source_file="res://art/exported/characters/base/netfishing_base_character_nose_dog_round.png"
dest_files=["res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.s3tc.ctex", "res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.etc2.ctex"]
[params]
compress/mode=2
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=true
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=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b345yhfv8w1j5"
path="res://.godot/imported/eyes_simple_shine.png-33edcf4bb163d7f2bde9a662105cf788.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/faces/eyes/eyes_simple_shine.png"
dest_files=["res://.godot/imported/eyes_simple_shine.png-33edcf4bb163d7f2bde9a662105cf788.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: 8.1 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://8wg4k4us4baa"
path="res://.godot/imported/eyes_simple_shine_eyebrows.png-d2147df0247f280b8f63b23d590204ab.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/faces/eyes/eyes_simple_shine_eyebrows.png"
dest_files=["res://.godot/imported/eyes_simple_shine_eyebrows.png-d2147df0247f280b8f63b23d590204ab.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: 6.1 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://crrw5vvop2gj7"
path="res://.godot/imported/mouth_three.png-b0b3775e32dfd17b6cfcac16b5c64d58.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/faces/mouth/mouth_three.png"
dest_files=["res://.godot/imported/mouth_three.png-b0b3775e32dfd17b6cfcac16b5c64d58.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: 7 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bbx0ywjiwymrm"
path="res://.godot/imported/nose_dog_round.png-e1d92a8a7b661901752b05898a3f7c8b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/faces/noses/nose_dog_round.png"
dest_files=["res://.godot/imported/nose_dog_round.png-e1d92a8a7b661901752b05898a3f7c8b.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: 7 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://du4u6meapay05"
path.s3tc="res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "c2f7c29aaaa95809e140eb308d9bb132"
}
[deps]
source_file="res://art/source/characters/base/netfishing_character_source_eyes_simple_shine.png"
dest_files=["res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.s3tc.ctex", "res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.etc2.ctex"]
[params]
compress/mode=2
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=true
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=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://di20c1x8vksi1"
path.s3tc="res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "f8bf25a1006c5cc2ab5323f4a758eec7"
}
[deps]
source_file="res://art/source/characters/base/netfishing_character_source_mouth_three.png"
dest_files=["res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.s3tc.ctex", "res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.etc2.ctex"]
[params]
compress/mode=2
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=true
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=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

View file

@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b0p55hwdry2cr"
path.s3tc="res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.s3tc.ctex"
path.etc2="res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "3134f170152f9fa1512b41b61ecb01fc"
}
[deps]
source_file="res://art/source/characters/base/netfishing_character_source_nose_dog_round.png"
dest_files=["res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.s3tc.ctex", "res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.etc2.ctex"]
[params]
compress/mode=2
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=true
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=0

View file

@ -533,6 +533,8 @@ func _initialize_after_data_root() -> void:
_world_time,
_world_weather,
_player_jobs,
_test_world.get_world_environment(),
_test_world.get_sun(),
)
_game_ui.setup_data_and_identity(
_data_root,

View file

@ -44,18 +44,18 @@
[ext_resource type="Shader" uid="uid://byw3iub66qvog" path="res://ui/player_menu_pattern.gdshader" id="42_menu_pattern"]
[ext_resource type="Texture2D" uid="uid://b6xws1d2dnbnn" path="res://art/patterns/pattern_moneyfish.png" id="43_shop_pattern"]
[ext_resource type="PackedScene" uid="uid://w7n4gjqq1juc" path="res://art/exported/characters/base/netfishing_base_character.glb" id="44_4pcu1"]
[ext_resource type="Script" path="res://network/network_fish_showcase_service.gd" id="45_fish_showcase"]
[ext_resource type="Script" path="res://network/network_surface_drawing_service.gd" id="46_surface_drawing"]
[ext_resource type="Script" path="res://world/world_time_service.gd" id="47_world_time"]
[ext_resource type="Script" path="res://world/world_time_visual_controller.gd" id="48_world_time_visuals"]
[ext_resource type="Script" path="res://network/network_world_time_service.gd" id="49_network_world_time"]
[ext_resource type="Script" path="res://world/world_weather_service.gd" id="50_world_weather"]
[ext_resource type="Script" path="res://network/network_world_weather_service.gd" id="51_network_world_weather"]
[ext_resource type="Script" path="res://jobs/player_job_service.gd" id="52_player_jobs"]
[ext_resource type="Script" path="res://network/network_job_service.gd" id="53_network_jobs"]
[ext_resource type="Script" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"]
[ext_resource type="AudioStream" path="res://audio/ambience/waves.wav" id="55_waves"]
[ext_resource type="Script" path="res://settings/controller_mapping_manager.gd" id="56_controller_mapping"]
[ext_resource type="Script" uid="uid://m4n0bn38f34i" path="res://network/network_fish_showcase_service.gd" id="45_fish_showcase"]
[ext_resource type="Script" uid="uid://bkiepgu8j5r6t" path="res://network/network_surface_drawing_service.gd" id="46_surface_drawing"]
[ext_resource type="Script" uid="uid://5lceyulaglpf" path="res://world/world_time_service.gd" id="47_world_time"]
[ext_resource type="Script" uid="uid://svirs8vbypad" path="res://world/world_time_visual_controller.gd" id="48_world_time_visuals"]
[ext_resource type="Script" uid="uid://blig76aes25j" path="res://network/network_world_time_service.gd" id="49_network_world_time"]
[ext_resource type="Script" uid="uid://v4c54isq6w74" path="res://world/world_weather_service.gd" id="50_world_weather"]
[ext_resource type="Script" uid="uid://llj46fx0oco7" path="res://network/network_world_weather_service.gd" id="51_network_world_weather"]
[ext_resource type="Script" uid="uid://daqtje84gqgu0" path="res://jobs/player_job_service.gd" id="52_player_jobs"]
[ext_resource type="Script" uid="uid://drlyytr2o3uw8" path="res://network/network_job_service.gd" id="53_network_jobs"]
[ext_resource type="Script" uid="uid://b0c3o2vy76fg3" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"]
[ext_resource type="AudioStream" uid="uid://dck3tf8qkadea" path="res://audio/ambience/waves.wav" id="55_waves"]
[ext_resource type="Script" uid="uid://c80x8jkdnx0xy" path="res://settings/controller_mapping_manager.gd" id="56_controller_mapping"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
shader = ExtResource("40_title_water")
@ -123,31 +123,31 @@ mouse_filter = 2
unique_name_in_owner = true
script = ExtResource("39_fonts")
[node name="WorldTimeService" type="Node" parent="."]
[node name="WorldTimeService" type="Node" parent="." unique_id=1774610576]
unique_name_in_owner = true
script = ExtResource("47_world_time")
[node name="WorldTimeVisualController" type="Node" parent="."]
[node name="WorldTimeVisualController" type="Node" parent="." unique_id=1861087008]
unique_name_in_owner = true
script = ExtResource("48_world_time_visuals")
[node name="NetworkWorldTimeService" type="Node" parent="."]
[node name="NetworkWorldTimeService" type="Node" parent="." unique_id=1136962511]
unique_name_in_owner = true
script = ExtResource("49_network_world_time")
[node name="WorldWeatherService" type="Node" parent="."]
[node name="WorldWeatherService" type="Node" parent="." unique_id=1812156280]
unique_name_in_owner = true
script = ExtResource("50_world_weather")
[node name="NetworkWorldWeatherService" type="Node" parent="."]
[node name="NetworkWorldWeatherService" type="Node" parent="." unique_id=266238117]
unique_name_in_owner = true
script = ExtResource("51_network_world_weather")
[node name="PlayerJobService" type="Node" parent="."]
[node name="PlayerJobService" type="Node" parent="." unique_id=2062127535]
unique_name_in_owner = true
script = ExtResource("52_player_jobs")
[node name="NetworkJobService" type="Node" parent="."]
[node name="NetworkJobService" type="Node" parent="." unique_id=556458360]
unique_name_in_owner = true
script = ExtResource("53_network_jobs")
@ -223,11 +223,11 @@ script = ExtResource("23_network_shop")
unique_name_in_owner = true
script = ExtResource("24_network_item")
[node name="NetworkFishShowcaseService" type="Node" parent="."]
[node name="NetworkFishShowcaseService" type="Node" parent="." unique_id=148288598]
unique_name_in_owner = true
script = ExtResource("45_fish_showcase")
[node name="NetworkSurfaceDrawingService" type="Node" parent="."]
[node name="NetworkSurfaceDrawingService" type="Node" parent="." unique_id=80122665]
unique_name_in_owner = true
script = ExtResource("46_surface_drawing")
@ -252,11 +252,11 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0024132729, 0, -0.00093126
[node name="Players" type="Node3D" parent="." unique_id=550630144]
[node name="SurfaceDrawings" type="Node3D" parent="."]
[node name="Player" parent="Players" unique_id=485429332 instance=ExtResource("2_player")]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 16)
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.3999481, 12.07081)
[node name="SurfaceDrawings" type="Node3D" parent="." unique_id=234671307]
[node name="FishingSpot" parent="." unique_id=1461328864 instance=ExtResource("4_spot")]
unique_name_in_owner = true
@ -274,7 +274,7 @@ script = ExtResource("9_save")
unique_name_in_owner = true
script = ExtResource("10_settings")
[node name="ControllerMappingManager" type="Node" parent="."]
[node name="ControllerMappingManager" type="Node" parent="." unique_id=1198390067]
unique_name_in_owner = true
script = ExtResource("56_controller_mapping")
@ -284,11 +284,11 @@ stream = ExtResource("13_title_music")
volume_db = -80.0
bus = &"Music"
[node name="ShorelineAmbience" type="Node" parent="."]
[node name="ShorelineAmbience" type="Node" parent="." unique_id=1948037177]
unique_name_in_owner = true
script = ExtResource("54_shoreline_ambience")
[node name="WavesAudio" type="AudioStreamPlayer" parent="ShorelineAmbience"]
[node name="WavesAudio" type="AudioStreamPlayer" parent="ShorelineAmbience" unique_id=193530658]
unique_name_in_owner = true
stream = ExtResource("55_waves")
bus = &"SFX"

View file

@ -13,7 +13,7 @@ roughness = 0.8
bone_name = "hand.R"
[node name="FishingRod" type="Node3D" parent="."]
rotation = Vector3(-0.785398, -0.007863, 3.141593)
rotation_degrees = Vector3(322.6, -31.2, 248.9)
[node name="RodMesh" type="MeshInstance3D" parent="FishingRod"]
position = Vector3(0, 0.42, 0)

View file

@ -171,6 +171,7 @@ var _controller_mapping_manager: ControllerMappingManagerType
func _ready() -> void:
PlayerVisualPresenter.apply_appearance(_visuals, appearance_snapshot)
_initialize_fishing_rod()
_target_zoom = clampf(_spring_arm.spring_length, minimum_zoom, maximum_zoom)
_spring_arm.spring_length = _target_zoom

View file

@ -2,6 +2,25 @@ class_name PlayerVisualPresenter
extends RefCounted
const PlayerScene: PackedScene = preload("res://player/player.tscn")
const EYE_TEXTURES: Dictionary = {
"simple_shine": preload(
"res://art/exported/characters/faces/eyes/eyes_simple_shine.png"
),
"simple_shine_eyebrows": preload(
"res://art/exported/characters/faces/eyes/eyes_simple_shine_eyebrows.png"
),
}
const MOUTH_TEXTURES: Dictionary = {
"three": preload(
"res://art/exported/characters/faces/mouth/mouth_three.png"
),
}
const NOSE_TEXTURES: Dictionary = {
"dog_round": preload(
"res://art/exported/characters/faces/noses/nose_dog_round.png"
),
}
const RUNTIME_MATERIAL_META: StringName = &"netfishing_runtime_material"
static func instantiate_visuals() -> Node3D:
@ -32,10 +51,115 @@ static func instantiate_visuals() -> Node3D:
static func apply_appearance(
_visuals: Node3D,
visuals: Node3D,
snapshot: Dictionary,
) -> void:
# Gameplay and preview deliberately share this presentation seam. Modular
# visual parts can be applied here without coupling Profile to Player.
if not CharacterCustomizationCatalog.validate_snapshot(snapshot):
return
# Gameplay and preview deliberately share this presentation seam. Keep all
# mesh selection here so profile previews and network avatars stay identical.
var skeleton: Skeleton3D = visuals.find_child("Skeleton3D", true, false) as Skeleton3D
if skeleton == null:
return
var species_id: String = CharacterCustomizationCatalog.canonical_option_id(
"species", str(snapshot.get("species", "round"))
)
var head_id: String = "head_pointy" if species_id == "pointy" else "head_round"
var head_ids: Array[String] = ["head_pointy", "head_round"]
for head_name: String in head_ids:
var head: Node3D = skeleton.get_node_or_null(head_name) as Node3D
if head != null:
head.visible = head_name == head_id
for feature: String in ["eyes", "mouth", "nose"]:
var decal: Node3D = skeleton.get_node_or_null(
"%s_%s" % [head_name, feature]
) as Node3D
if decal != null:
decal.visible = head_name == head_id
var selected_ears: String = CharacterCustomizationCatalog.canonical_option_id(
"ears", str(snapshot.get("ears", "none"))
)
var ear_names: Array[String] = [
"ears_pointy_long",
"ears_pointy_short",
"ears_pointy_wide",
]
for ear_name: String in ear_names:
var ears: Node3D = skeleton.get_node_or_null(ear_name) as Node3D
if ears != null:
ears.visible = selected_ears == ear_name.trim_prefix("ears_")
var fur_color: Color = CharacterCustomizationCatalog.option_color(
"fur_pattern", str(snapshot.get("fur_pattern", "white"))
)
_set_mesh_color(skeleton, "body_arms", fur_color)
_set_mesh_color(skeleton, "body_main", fur_color)
_set_mesh_color(skeleton, "head_pointy", fur_color)
_set_mesh_color(skeleton, "head_round", fur_color)
for ear_name: String in ear_names:
_set_mesh_color(skeleton, ear_name, fur_color)
var eye_id: String = CharacterCustomizationCatalog.canonical_option_id(
"eyes", str(snapshot.get("eyes", "simple_shine"))
)
var mouth_id: String = CharacterCustomizationCatalog.canonical_option_id(
"mouth", str(snapshot.get("mouth", "three"))
)
var nose_id: String = CharacterCustomizationCatalog.canonical_option_id(
"nose", str(snapshot.get("nose", "dog_round"))
)
_set_feature_texture(skeleton, "eyes", EYE_TEXTURES.get(eye_id) as Texture2D)
_set_feature_texture(
skeleton, "mouth", MOUTH_TEXTURES.get(mouth_id) as Texture2D
)
_set_feature_texture(skeleton, "nose", NOSE_TEXTURES.get(nose_id) as Texture2D)
static func _set_feature_texture(
skeleton: Skeleton3D,
feature_name: String,
texture: Texture2D,
) -> void:
if texture == null:
return
for head_name: String in ["head_pointy", "head_round"]:
var mesh_instance: MeshInstance3D = skeleton.get_node_or_null(
"%s_%s" % [head_name, feature_name]
) as MeshInstance3D
var material: StandardMaterial3D = _runtime_material(mesh_instance)
if material != null:
material.albedo_color = Color.WHITE
material.albedo_texture = texture
static func _set_mesh_color(
skeleton: Skeleton3D,
mesh_name: String,
color: Color,
) -> void:
var mesh_instance: MeshInstance3D = skeleton.get_node_or_null(
mesh_name
) as MeshInstance3D
var material: StandardMaterial3D = _runtime_material(mesh_instance)
if material != null:
material.albedo_color = color
static func _runtime_material(
mesh_instance: MeshInstance3D,
) -> StandardMaterial3D:
if mesh_instance == null or mesh_instance.mesh == null:
return null
if mesh_instance.has_meta(RUNTIME_MATERIAL_META):
return mesh_instance.material_override as StandardMaterial3D
var source: StandardMaterial3D = mesh_instance.mesh.surface_get_material(
0
) as StandardMaterial3D
if source == null:
return null
var material: StandardMaterial3D = source.duplicate() as StandardMaterial3D
mesh_instance.material_override = material
mesh_instance.set_meta(RUNTIME_MATERIAL_META, true)
return material

View file

@ -12,8 +12,8 @@ const CATEGORY_IDS: PackedStringArray = [
]
const CATEGORY_LABELS: Dictionary = {
"species": "species",
"fur_pattern": "fur pattern",
"species": "head",
"fur_pattern": "fur color",
"ears": "ears",
"eyes": "eyes",
"nose": "nose",
@ -22,24 +22,59 @@ const CATEGORY_LABELS: Dictionary = {
}
const OPTIONS: Dictionary = {
"species": [{"id": "default", "label": "default"}],
"fur_pattern": [{"id": "solid", "label": "solid"}],
"ears": [{"id": "default", "label": "default"}],
"eyes": [{"id": "default", "label": "default"}],
"nose": [{"id": "default", "label": "default"}],
"mouth": [{"id": "default", "label": "default"}],
"species": [
{"id": "round", "label": "round"},
{"id": "pointy", "label": "pointy"},
],
# The serialized field remains `fur_pattern` for save and network
# compatibility. It holds the initial solid fur-color selection until the
# authored pattern layer is ready.
"fur_pattern": [
{"id": "white", "label": "white", "color": Color("f2f0e8")},
{"id": "gray", "label": "gray", "color": Color("819398")},
{"id": "charcoal", "label": "charcoal", "color": Color("34444a")},
{"id": "brown", "label": "brown", "color": Color("7b4a32")},
{"id": "orange", "label": "orange", "color": Color("c86c36")},
{"id": "yellow", "label": "yellow", "color": Color("d8c545")},
{"id": "green", "label": "green", "color": Color("6f913c")},
{"id": "teal", "label": "teal", "color": Color("3a8790")},
],
"ears": [
{"id": "none", "label": "none"},
{"id": "pointy_long", "label": "long"},
{"id": "pointy_short", "label": "short"},
{"id": "pointy_wide", "label": "wide"},
],
"eyes": [
{"id": "simple_shine", "label": "simple shine"},
{
"id": "simple_shine_eyebrows",
"label": "simple shine + eyebrows",
},
],
"nose": [{"id": "dog_round", "label": "round"}],
"mouth": [{"id": "three", "label": "three"}],
"tail": [{"id": "none", "label": "none"}],
}
const LEGACY_OPTION_ALIASES: Dictionary = {
"species": {"default": "round"},
"fur_pattern": {"solid": "white"},
"ears": {"default": "none"},
"eyes": {"default": "simple_shine"},
"nose": {"default": "dog_round"},
"mouth": {"default": "three"},
}
static func default_snapshot() -> Dictionary:
return {
"species": "default",
"fur_pattern": "solid",
"ears": "default",
"eyes": "default",
"nose": "default",
"mouth": "default",
"species": "round",
"fur_pattern": "white",
"ears": "none",
"eyes": "simple_shine",
"nose": "dog_round",
"mouth": "three",
"tail": "none",
}
@ -53,12 +88,26 @@ 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)
for option: Dictionary in options_for(category_id):
if str(option.get("id", "")) == option_id:
return true
return false
static func canonical_option_id(category_id: String, option_id: String) -> String:
var aliases: Dictionary = LEGACY_OPTION_ALIASES.get(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)
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 validate_snapshot(value: Variant) -> bool:
if typeof(value) != TYPE_DICTIONARY:
return false
@ -80,7 +129,9 @@ static func sanitized_snapshot(value: Variant) -> Dictionary:
return result
var snapshot: Dictionary = value
for category_id: String in CATEGORY_IDS:
var option_id := str(snapshot.get(category_id, ""))
var option_id: String = canonical_option_id(
category_id, str(snapshot.get(category_id, ""))
)
if is_valid_option(category_id, option_id):
result[category_id] = option_id
return result

View file

@ -227,6 +227,8 @@ func setup(
world_time: WorldTimeServiceType,
world_weather: WorldWeatherServiceType,
player_jobs: PlayerJobService,
world_environment: WorldEnvironment,
world_sun: DirectionalLight3D,
) -> void:
_player = player
_fishing_spot = fishing_spot
@ -285,6 +287,8 @@ func setup(
network_player_list,
player_jobs,
world_time,
world_environment,
world_sun,
)
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot, inventory)
_fishing_shop.setup(

View file

@ -508,6 +508,8 @@ func setup(
network_player_list: NetworkPlayerListService,
player_jobs: PlayerJobService,
world_time: WorldTimeService,
world_environment: WorldEnvironment,
world_sun: DirectionalLight3D,
) -> void:
_player = player
_inventory = inventory
@ -530,7 +532,12 @@ func setup(
_mail_page.setup(
network_mail_service, reservations, inventory, wallet, bag, item_catalog
)
_profile_page.setup(network_profile_service, experience)
_profile_page.setup(
network_profile_service,
experience,
world_environment,
world_sun,
)
_players_page.setup(network_player_list)
_catalog_logbook.setup(collection_log, inventory, catalog)
_the_net_page.setup(player_jobs, world_time)

View file

@ -27,6 +27,7 @@ var _revert_button: Button
var _discard_confirmation: PanelContainer
var _confirmation_label: Label
var _confirmation_confirm: Button
var _keep_editing_button: Button
var _confirmation_action: String = ""
var _debounce: Timer
var _experience_level: Label
@ -47,6 +48,8 @@ func _ready() -> void:
func setup(
service: NetworkProfileService,
experience: PlayerExperience,
world_environment: WorldEnvironment,
world_sun: DirectionalLight3D,
) -> void:
_service = service
_experience = experience
@ -62,6 +65,7 @@ func setup(
_experience.experience_changed.connect(_on_experience_changed)
_load_persisted()
_refresh_experience()
_preview.setup_world_lighting(world_environment, world_sun)
var identity_value := find_child("IdentityFingerprint", true, false) as Label
if identity_value != null:
var fingerprint := _service.get_identity_fingerprint()
@ -344,16 +348,14 @@ func _build_ui() -> void:
_confirmation_confirm.pressed.connect(_confirm_pending_action)
UtilityPageStyle.apply_ocean_button(_confirmation_confirm)
confirm_buttons.add_child(_confirmation_confirm)
var keep := Button.new()
keep.name = "KeepEditing"
keep.unique_name_in_owner = true
keep.text = "keep editing"
keep.pressed.connect(func() -> void:
_keep_editing_button = Button.new()
_keep_editing_button.text = "keep editing"
_keep_editing_button.pressed.connect(func() -> void:
_discard_confirmation.visible = false
_name_edit.grab_focus()
)
UtilityPageStyle.apply_ocean_button(keep)
confirm_buttons.add_child(keep)
UtilityPageStyle.apply_ocean_button(_keep_editing_button)
confirm_buttons.add_child(_keep_editing_button)
_build_categories()
@ -394,13 +396,16 @@ func _refresh_options() -> void:
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
_option_list.add_child(title)
var options := CharacterCustomizationCatalog.options_for(_category_id)
var options: Array = CharacterCustomizationCatalog.options_for(_category_id)
if options.is_empty():
var empty := Label.new()
empty.text = "More options coming later."
empty.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_option_list.add_child(empty)
return
if _category_id == "fur_pattern":
_build_fur_color_options(options)
return
for option: Dictionary in options:
var option_id := str(option["id"])
var button := Button.new()
@ -413,6 +418,49 @@ func _refresh_options() -> void:
_option_list.add_child(button)
func _build_fur_color_options(options: Array) -> void:
var grid := GridContainer.new()
grid.columns = 4
grid.add_theme_constant_override("h_separation", 10)
grid.add_theme_constant_override("v_separation", 10)
_option_list.add_child(grid)
var selected_id: String = CharacterCustomizationCatalog.canonical_option_id(
"fur_pattern", str(_draft_appearance.get("fur_pattern", "white"))
)
for option: Dictionary in options:
var option_id: String = str(option.get("id", ""))
var color: Color = CharacterCustomizationCatalog.option_color(
"fur_pattern", option_id
)
var button := Button.new()
button.text = ""
button.tooltip_text = str(option.get("label", option_id))
button.toggle_mode = true
button.custom_minimum_size = Vector2(44.0, 44.0)
button.button_pressed = selected_id == option_id
button.pressed.connect(_select_option.bind("fur_pattern", option_id))
_apply_fur_swatch_style(button, color, button.button_pressed)
grid.add_child(button)
func _apply_fur_swatch_style(
button: Button,
color: Color,
selected: bool,
) -> void:
var normal: StyleBoxFlat = UtilityPageStyle.rounded_style(color, 999)
var hover: StyleBoxFlat = UtilityPageStyle.rounded_style(
color.lightened(0.12), 999
)
var selected_style: StyleBoxFlat = UtilityPageStyle.rounded_style(color, 999)
selected_style.border_color = UtilityPageStyle.OCEAN_TEXT_PRIMARY
selected_style.set_border_width_all(3 if selected else 2)
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
button.add_theme_stylebox_override("pressed", selected_style)
button.add_theme_stylebox_override("focus", selected_style)
func _select_option(category_id: String, option_id: String) -> void:
_draft_appearance[category_id] = option_id
_preview.apply_appearance_profile(_draft_appearance)
@ -546,7 +594,7 @@ func _show_confirmation(action: String) -> void:
else:
_confirmation_label.text = "Discard unsaved profile changes?"
_confirmation_confirm.text = "discard changes"
_discard_confirmation.get_node("%KeepEditing").grab_focus()
_keep_editing_button.grab_focus()
func _confirm_pending_action() -> void:

View file

@ -4,15 +4,20 @@ extends SubViewportContainer
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const FRONT_FACING_YAW: float = PI
@export_range(0.1, 2.0, 0.05) var drag_sensitivity: float = 0.012
@export_range(0.1, 4.0, 0.1) var keyboard_speed: float = 1.8
@onready var _preview_root: Node3D = %PreviewRoot
@onready var _preview_environment: WorldEnvironment = %WorldEnvironment
@onready var _preview_sun: DirectionalLight3D = %KeyLight
var _dragging: bool = false
var _visuals: Node3D
var _controller_mapping_manager: ControllerMappingManagerType
var _source_environment: WorldEnvironment
var _source_sun: DirectionalLight3D
func setup_controller_mapping(
@ -21,9 +26,19 @@ func setup_controller_mapping(
_controller_mapping_manager = mapping_manager
func setup_world_lighting(
world_environment: WorldEnvironment,
world_sun: DirectionalLight3D,
) -> void:
_source_environment = world_environment
_source_sun = world_sun
_sync_world_lighting()
func _ready() -> void:
focus_mode = Control.FOCUS_ALL
gui_input.connect(_on_gui_input)
reset_view()
_visuals = PlayerVisualPresenter.instantiate_visuals()
_preview_root.add_child(_visuals)
var rod := _visuals.find_child("FishingRod", true, false) as Node3D
@ -40,10 +55,11 @@ func apply_appearance_profile(profile: Dictionary) -> void:
func reset_view() -> void:
_preview_root.rotation.y = 0.0
_preview_root.rotation.y = FRONT_FACING_YAW
func _process(delta: float) -> void:
_sync_world_lighting()
if not has_focus():
return
var axis := Input.get_axis("ui_left", "ui_right")
@ -78,3 +94,19 @@ func _notification(what: int) -> void:
func _rotate(amount: float) -> void:
_preview_root.rotation.y = fposmod(_preview_root.rotation.y + amount, TAU)
func _sync_world_lighting() -> void:
if (
_preview_environment == null
or _preview_sun == null
or _source_environment == null
or _source_sun == null
):
return
_preview_environment.environment = _source_environment.environment
_preview_sun.rotation = _source_sun.global_rotation
_preview_sun.light_color = _source_sun.light_color
_preview_sun.light_energy = _source_sun.light_energy
_preview_sun.light_indirect_energy = _source_sun.light_indirect_energy
_preview_sun.shadow_enabled = _source_sun.shadow_enabled

View file

@ -1,16 +1,8 @@
[gd_scene load_steps=4 format=3]
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://ui/profile_preview.gd" id="1"]
[sub_resource type="Environment" id="Environment"]
background_mode = 1
background_color = Color(0.075, 0.13, 0.16, 1)
ambient_light_source = 3
ambient_light_color = Color(0.72, 0.82, 0.86, 1)
ambient_light_energy = 0.65
[sub_resource type="World3D" id="PreviewWorld"]
environment = SubResource("Environment")
[node name="ProfilePreview" type="SubViewportContainer"]
custom_minimum_size = Vector2(300, 220)
@ -24,18 +16,12 @@ render_target_update_mode = 4
world_3d = SubResource("PreviewWorld")
[node name="WorldEnvironment" type="WorldEnvironment" parent="Viewport"]
environment = SubResource("Environment")
unique_name_in_owner = true
[node name="KeyLight" type="DirectionalLight3D" parent="Viewport"]
rotation_degrees = Vector3(-38, -32, 0)
light_energy = 1.15
shadow_enabled = true
[node name="FillLight" type="OmniLight3D" parent="Viewport"]
position = Vector3(-1.8, 1.6, 2.2)
light_color = Color(0.72, 0.84, 1, 1)
omni_range = 5.0
light_energy = 1.3
unique_name_in_owner = true
light_energy = 0.1
shadow_enabled = false
[node name="PreviewRoot" type="Node3D" parent="Viewport"]
unique_name_in_owner = true