diff --git a/art/exported/characters/base/netfishing_base_character.glb b/art/exported/characters/base/netfishing_base_character.glb index c7fcf83..b4bb5d1 100644 Binary files a/art/exported/characters/base/netfishing_base_character.glb and b/art/exported/characters/base/netfishing_base_character.glb differ diff --git a/art/source/characters/base/netfishing_character_source.blend b/art/source/characters/base/netfishing_character_source.blend index 2b345d1..8fde03b 100644 Binary files a/art/source/characters/base/netfishing_character_source.blend and b/art/source/characters/base/netfishing_character_source.blend differ diff --git a/art/source/characters/base/netfishing_character_source.blend1 b/art/source/characters/base/netfishing_character_source.blend1 index c6f3af2..e4808eb 100644 Binary files a/art/source/characters/base/netfishing_character_source.blend1 and b/art/source/characters/base/netfishing_character_source.blend1 differ diff --git a/fishing/fishing_presentation.gd b/fishing/fishing_presentation.gd index 8a5ecb0..e19fb0f 100644 --- a/fishing/fishing_presentation.gd +++ b/fishing/fishing_presentation.gd @@ -59,7 +59,6 @@ var _cast_arrival_position: Vector3 var _cast_tracks_water_surface: bool = false var _bobber_surface_position: Vector3 var _bobber_idle_elapsed: float = 0.0 -var _bobber_base_scale: Vector3 = Vector3.ONE var _active_tween: Tween var _rod_tween: Tween var _bite_tween: Tween @@ -95,13 +94,11 @@ func begin_aim( minimum_target: Vector3, target_normal: Vector3, target_is_fishable: bool, - character_scale: float = 1.0, ) -> void: cleanup() if rod_tip == null or rod == null: return - _bobber_base_scale = Vector3.ONE * maxf(character_scale, 0.01) _mode = VisualMode.AIMING _rod = rod _rod_tip = rod_tip @@ -161,7 +158,7 @@ func begin_cast( _mode = VisualMode.CASTING _target_marker.visible = false _bobber.visible = true - _bobber.scale = _bobber_base_scale + _bobber.scale = Vector3.ONE _cast_arrival_position = ( blocked_landing_position if cast_is_blocked else target ) @@ -191,26 +188,26 @@ func show_bite() -> void: return _kill_bite_tween() - _bobber.scale = _bobber_base_scale + _bobber.scale = Vector3.ONE _bite_tween = create_tween() _bite_tween.set_trans(Tween.TRANS_QUAD) _bite_tween.set_ease(Tween.EASE_IN_OUT) _bite_tween.tween_property( _bobber, "scale", - _bobber_base_scale * 0.68, + Vector3.ONE * 0.68, 0.08 ) _bite_tween.tween_property( _bobber, "scale", - _bobber_base_scale * 1.2, + Vector3.ONE * 1.2, 0.1 ) _bite_tween.tween_property( _bobber, "scale", - _bobber_base_scale, + Vector3.ONE, 0.1 ) _bite_tween.finished.connect(_on_bite_tween_finished) @@ -250,7 +247,7 @@ func play_outcome(outcome: StringName) -> void: # Withdrawal is a visible return, not an instant cancellation. Keep the # bobber present even if the preceding reel update hid or reset it. _bobber.visible = true - _bobber.scale = _bobber_base_scale + _bobber.scale = Vector3.ONE _active_tween = create_tween() _active_tween.set_trans(Tween.TRANS_QUAD) _active_tween.set_ease(Tween.EASE_IN) @@ -299,7 +296,6 @@ func cleanup() -> void: _invalid_target_marker.visible = false _bobber.visible = false _bobber.scale = Vector3.ONE - _bobber_base_scale = Vector3.ONE _bobber.rotation = Vector3.ZERO _cast_arrival_position = Vector3.ZERO _cast_tracks_water_surface = false @@ -479,7 +475,7 @@ func _kill_bite_tween() -> void: _bite_tween.kill() _bite_tween = null if is_instance_valid(_bobber): - _bobber.scale = _bobber_base_scale + _bobber.scale = Vector3.ONE func _on_bite_tween_finished() -> void: diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index b275bcb..6c7b434 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -609,8 +609,7 @@ func _begin_aiming(player: PlayerType) -> void: _active_player.get_fishing_rod(), preview_position, _aim_surface_sample.normal, - target_is_fishable, - _active_player.get_character_visual_scale(), + target_is_fishable ) @@ -953,7 +952,6 @@ func _activate_bite() -> void: return state = FishingState.FIGHTING - _active_player.set_fighting_visual(true) _state_time_remaining = 0.0 _withdrawal_input_held = false _pending_catch = _fish_selector.create_catch(_selected_fish) @@ -1042,7 +1040,6 @@ func _on_catch_completed() -> void: _cancel_attempt() return _stop_fight_audio() - _active_player.set_fighting_visual(false) state = FishingState.SHOWING_CATCH _showcase_ready = false _showcase_outcome_completed = false @@ -1165,8 +1162,6 @@ func _cleanup_attempt( ) -> void: _stop_fight_audio() _stop_reeling_audio() - if _active_player != null: - _active_player.set_fighting_visual(false) if not visual_outcome.is_empty(): if state == FishingState.RETURNING: return @@ -1195,7 +1190,6 @@ func _cleanup_attempt( func _finalize_attempt_cleanup(cooldown_message: String) -> void: _showcase_restore_generation += 1 if _active_player != null: - _active_player.set_fighting_visual(false) _active_player.end_catch_showcase() _active_player.set_movement_enabled(true) _active_player = null @@ -1483,8 +1477,6 @@ func _on_network_bite_started(_attempt_id: String) -> void: return _stop_reeling_audio() state = FishingState.FIGHTING - if _active_player != null: - _active_player.set_fighting_visual(true) _withdrawal_input_held = false _network_primary_input_held = Input.is_action_pressed("fish_primary") _network_input_resend_elapsed = 0.0 @@ -1575,8 +1567,6 @@ func _on_network_catch_received(fish_catch: FishCatchType) -> void: return _stop_fight_audio() _pending_catch = fish_catch - if _active_player != null: - _active_player.set_fighting_visual(false) state = FishingState.SHOWING_CATCH _showcase_ready = false _showcase_outcome_completed = false diff --git a/fishing/remote_fishing_presentation.gd b/fishing/remote_fishing_presentation.gd index 22b1859..8dab308 100644 --- a/fishing/remote_fishing_presentation.gd +++ b/fishing/remote_fishing_presentation.gd @@ -19,7 +19,6 @@ var _return_tween: Tween var _showcase_tween: Tween var _return_showcase_catch: FishCatch var _bobber_idle_elapsed: float = 0.0 -var _bobber_base_scale: Vector3 = Vector3.ONE func setup(owning_player: Player) -> void: @@ -53,12 +52,11 @@ func show_cast(origin: Vector3, target: Vector3) -> void: return _kill_cast_tween() _active = true - _bobber_base_scale = Vector3.ONE * _owner.get_character_visual_scale() _target = origin _pending_target = target _bobber_idle_elapsed = 0.0 _bobber.global_position = origin - _bobber.scale = _bobber_base_scale + _bobber.scale = Vector3.ONE _bobber.visible = true _line.visible = true _owner.set_active_item_is_rod(true) @@ -89,16 +87,12 @@ func update_bobber(world_position: Vector3) -> void: func show_bite() -> void: if not _active: return - if _owner != null and is_instance_valid(_owner): - _owner.set_fighting_visual(true) var tween: Tween = create_tween() - tween.tween_property(_bobber, "scale", _bobber_base_scale * 0.7, 0.08) - tween.tween_property(_bobber, "scale", _bobber_base_scale, 0.12) + tween.tween_property(_bobber, "scale", Vector3.ONE * 0.7, 0.08) + tween.tween_property(_bobber, "scale", Vector3.ONE, 0.12) func play_return(showcase_catch: FishCatch = null) -> void: - if _owner != null and is_instance_valid(_owner): - _owner.set_fighting_visual(false) if not _active or _bobber == null: cleanup() return_completed.emit() @@ -134,12 +128,10 @@ func cleanup() -> void: _return_showcase_catch = null _bobber_idle_elapsed = 0.0 if _owner != null and is_instance_valid(_owner): - _owner.set_fighting_visual(false) _owner.end_catch_showcase(Callable(), true) if _bobber != null: _bobber.visible = false _bobber.scale = Vector3.ONE - _bobber_base_scale = Vector3.ONE if _line != null: _line.visible = false _line_mesh.clear_surfaces() diff --git a/player/player.gd b/player/player.gd index f006af4..d4c0515 100644 --- a/player/player.gd +++ b/player/player.gd @@ -41,7 +41,6 @@ const CHARACTER_IDLE_SIT_ANIMATION: StringName = &"idle_sit" const CHARACTER_IDLE_SIT_SHOW_ANIMATION: StringName = &"idle_sit_show" const CHARACTER_WALKING_ANIMATION: StringName = &"walking" const CHARACTER_WALKING_SHOW_ANIMATION: StringName = &"walking_show" -const FIGHTING_EYES_ID: String = "alligator_eyes" const BASE_REEL_SPEED: float = 0.16 # The target Android handheld exposes its physical right trigger through # Godot's left-trigger axis. Keep the role named here so the platform mapping @@ -76,33 +75,7 @@ func unequip_bait() -> void: func apply_appearance_snapshot(snapshot: Dictionary) -> void: if CharacterCustomizationCatalog.validate_snapshot(snapshot): appearance_snapshot = snapshot.duplicate(true) - _apply_presented_appearance() - - -func get_character_visual_scale() -> float: - return CharacterCustomizationCatalog.character_scale( - appearance_snapshot.get( - CharacterCustomizationCatalog.SCALE_CATEGORY_ID, - CharacterCustomizationCatalog.DEFAULT_CHARACTER_SCALE, - ) - ) - - -func set_fighting_visual(active: bool) -> void: - if _fighting_visual_active == active: - return - _fighting_visual_active = active - _apply_presented_appearance() - - -func _apply_presented_appearance() -> void: - if not is_node_ready() or _visuals == null: - return - var presented_appearance := appearance_snapshot - if _fighting_visual_active: - presented_appearance = appearance_snapshot.duplicate(true) - presented_appearance["eyes"] = FIGHTING_EYES_ID - PlayerVisualPresenter.apply_appearance(_visuals, presented_appearance) + PlayerVisualPresenter.apply_appearance(_visuals, appearance_snapshot) func apply_animalese_voice_id(voice_id: String) -> void: @@ -245,7 +218,6 @@ var _sitting_intent_pending: bool = false var _sitting_intent_sequence: int = -1 var _held_fish_visible: bool = false var _showcase_animation_active: bool = false -var _fighting_visual_active: bool = false var _fishing_rod: Node3D var _fishing_rod_tip: Marker3D var _controller_mapping_manager: ControllerMappingManagerType @@ -254,7 +226,7 @@ var _controller_mapping_manager: ControllerMappingManagerType func _ready() -> void: if not bag.contents_changed.is_connected(_on_bag_contents_changed): bag.contents_changed.connect(_on_bag_contents_changed) - _apply_presented_appearance() + 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 diff --git a/player/player_visual_presenter.gd b/player/player_visual_presenter.gd index f29cff7..8dd9f93 100644 --- a/player/player_visual_presenter.gd +++ b/player/player_visual_presenter.gd @@ -3,22 +3,6 @@ extends RefCounted const PlayerScene: PackedScene = preload("res://player/player.tscn") const RUNTIME_MATERIAL_META: StringName = &"netfishing_runtime_material" -const EAR_MESH_NAMES: Array[String] = [ - "ears_antlers_round", - "ears_bear", - "ears_bunny", - "ears_pointy_long", - "ears_pointy_short", - "ears_pointy_wide", -] -const TAIL_MESH_NAMES: Array[String] = [ - "tails_bear", - "tails_bunny", - "tails_cat", - "tails_fox", - "tails_gator", - "tails_pointy", -] static var _feature_uv_aspects: Dictionary = {} @@ -129,17 +113,15 @@ static func apply_appearance( var selected_ears: String = CharacterCustomizationCatalog.canonical_option_id( "ears", str(snapshot.get("ears", "none")) ) - for ear_name: String in EAR_MESH_NAMES: + 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 selected_tail: String = CharacterCustomizationCatalog.canonical_option_id( - "tail", str(snapshot.get("tail", "none")) - ) - for tail_name: String in TAIL_MESH_NAMES: - var tail: Node3D = skeleton.get_node_or_null(tail_name) as Node3D - if tail != null: - tail.visible = selected_tail == tail_name.trim_prefix("tails_") var fur_color: Color = CharacterCustomizationCatalog.option_color( "fur_pattern", str(snapshot.get("fur_pattern", "white")) @@ -148,10 +130,8 @@ static func apply_appearance( _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_MESH_NAMES: + for ear_name: String in ear_names: _set_mesh_color(skeleton, ear_name, fur_color) - for tail_name: String in TAIL_MESH_NAMES: - _set_mesh_color(skeleton, tail_name, fur_color) var eye_id: String = CharacterCustomizationCatalog.canonical_option_id( "eyes", str(snapshot.get("eyes", "simple_shine")) diff --git a/progression/character_customization_catalog.gd b/progression/character_customization_catalog.gd index 7350425..ddcf0b3 100644 --- a/progression/character_customization_catalog.gd +++ b/progression/character_customization_catalog.gd @@ -24,8 +24,8 @@ const CATEGORY_LABELS: Dictionary = { } const SCALE_CATEGORY_ID: String = "scale" -const MIN_CHARACTER_SCALE: float = 0.5 -const MAX_CHARACTER_SCALE: float = 1.5 +const MIN_CHARACTER_SCALE: float = 0.75 +const MAX_CHARACTER_SCALE: float = 1.25 const DEFAULT_CHARACTER_SCALE: float = 1.0 const CHARACTER_SCALE_STEP: float = 0.05 @@ -51,24 +51,16 @@ const OPTIONS: Dictionary = { # authored pattern layer is ready. "fur_pattern": [ {"id": "white", "label": "white", "color": Color("f2f0e8")}, - {"id": "cream", "label": "cream", "color": Color("e6d39b")}, {"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": "red", "label": "red", "color": Color("a9433f")}, - {"id": "pink", "label": "pink", "color": Color("d8899e")}, {"id": "yellow", "label": "yellow", "color": Color("d8c545")}, {"id": "green", "label": "green", "color": Color("6f913c")}, {"id": "teal", "label": "teal", "color": Color("3a8790")}, - {"id": "blue", "label": "blue", "color": Color("4d76a8")}, - {"id": "purple", "label": "purple", "color": Color("76558f")}, ], "ears": [ {"id": "none", "label": "none"}, - {"id": "antlers_round", "label": "antlers"}, - {"id": "bear", "label": "bear"}, - {"id": "bunny", "label": "bunny"}, {"id": "pointy_long", "label": "long"}, {"id": "pointy_short", "label": "short"}, {"id": "pointy_wide", "label": "wide"}, @@ -82,22 +74,13 @@ const OPTIONS: Dictionary = { ], "nose": [{"id": "dog_round", "label": "round"}], "mouth": [{"id": "three", "label": "three"}], - "tail": [ - {"id": "none", "label": "none"}, - {"id": "bear", "label": "bear"}, - {"id": "bunny", "label": "bunny"}, - {"id": "cat", "label": "cat"}, - {"id": "fox", "label": "fox"}, - {"id": "gator", "label": "gator"}, - {"id": "pointy", "label": "pointy"}, - ], + "tail": [{"id": "none", "label": "none"}], } const LEGACY_OPTION_ALIASES: Dictionary = { "species": {"default": "round"}, "fur_pattern": {"solid": "white"}, "ears": {"default": "none"}, - "tail": {"default": "none"}, "eyes": {"default": "simple_shine"}, "nose": {"default": "dog_round"}, "mouth": {"default": "three"}, @@ -144,82 +127,6 @@ static func options_for(category_id: String) -> Array: return OPTIONS.get(category_id, []) -static func feature_option_groups(category_id: String) -> Array: - if category_id not in FEATURE_CATEGORIES: - return [] - var options: Array = options_for(category_id) - var option_ids: Array[String] = [] - for option: Dictionary in options: - option_ids.append(str(option.get("id", ""))) - var grouped_options: Dictionary = {} - var group_order: Array[String] = [] - for option: Dictionary in options: - var option_id := str(option.get("id", "")) - var group_id := _feature_variant_root(option_id, option_ids) - if not grouped_options.has(group_id): - grouped_options[group_id] = [] - group_order.append(group_id) - var group: Array = grouped_options[group_id] as Array - group.append(option) - grouped_options[group_id] = group - var result: Array = [] - for group_id: String in group_order: - result.append({ - "id": group_id, - "options": grouped_options[group_id], - }) - return result - - -static func _feature_variant_root( - option_id: String, - option_ids: Array[String], -) -> String: - if option_id == "none": - return option_id - # Prefer the shortest authored base option. This keeps nested names such as - # `beady_extra_shine` in the `beady` drawer rather than creating drawers - # inside drawers. - var authored_root := "" - for candidate_id: String in option_ids: - if candidate_id == "none": - continue - if ( - option_id != candidate_id - and not option_id.begins_with(candidate_id + "_") - ): - continue - var has_descendant := false - for descendant_id: String in option_ids: - if descendant_id.begins_with(candidate_id + "_"): - has_descendant = true - break - if ( - has_descendant - and ( - authored_root.is_empty() - or candidate_id.length() < authored_root.length() - ) - ): - authored_root = candidate_id - if not authored_root.is_empty(): - return authored_root - - # Some families have no unqualified base asset. Group those siblings by - # their leading name segment, making the sorted first asset representative. - var separator_index := option_id.find("_") - if separator_index <= 0: - return option_id - var leading_segment := option_id.substr(0, separator_index) - var sibling_count := 0 - for candidate_id: String in option_ids: - if candidate_id.begins_with(leading_segment + "_"): - sibling_count += 1 - if sibling_count >= 2: - return leading_segment - return option_id - - static func texture_for(category_id: String, option_id: String) -> Texture2D: if category_id not in FEATURE_CATEGORIES: return null diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index b8690cb..3268b28 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -104,7 +104,6 @@ var _prior_movement: bool = true var _prior_camera: bool = true var _controller_refocused: bool = false var _input_lock_applied: bool = false -var _last_submit_frame: int = -1 var _output_scale: float = 1.0 var _dock_right: bool = false var _mobile_mode: bool = false @@ -187,10 +186,10 @@ func open_chat() -> void: if _presentation_state == PresentationState.COLLAPSED: _set_presentation_state(_visible_state_before_collapse, true, true) _controller_refocused = false - _prior_movement = _player.is_movement_enabled() - _prior_camera = _player.is_camera_input_enabled() _opened = true text_entry_ownership_changed.emit(true) + _prior_movement = _player.is_movement_enabled() + _prior_camera = _player.is_camera_input_enabled() _input_lock_applied = true _player.set_movement_enabled(false) _player.set_camera_input_enabled(false) @@ -207,15 +206,6 @@ func open_chat() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE -func open_command_chat() -> void: - open_chat() - if not _opened: - return - if not _entry.text.begins_with("/"): - _entry.text = "/" + _entry.text - _entry.caret_column = _entry.text.length() - - func set_available(value: bool) -> void: _available = value if not value: @@ -305,26 +295,16 @@ func _unhandled_input(event: InputEvent) -> void: if event is InputEventKey and event.pressed and not event.echo: if event.keycode in [KEY_ENTER, KEY_KP_ENTER]: if _opened: - _last_submit_frame = Engine.get_process_frames() _send() elif ( _available and get_viewport().gui_get_focus_owner() is not LineEdit - and Engine.get_process_frames() != _last_submit_frame ): open_chat() get_viewport().set_input_as_handled() elif event.keycode == KEY_T and not _opened and _available: open_chat() get_viewport().set_input_as_handled() - elif ( - event.unicode == 47 - and not _opened - and _available - and get_viewport().gui_get_focus_owner() is not LineEdit - ): - open_command_chat() - get_viewport().set_input_as_handled() elif event.keycode == KEY_ESCAPE and _opened: close_chat() get_viewport().set_input_as_handled() @@ -408,7 +388,7 @@ func _build_ui() -> void: _entry.name = "ChatEntry" _entry.placeholder_text = "type a message…" _entry.max_length = NetworkChatProtocol.MAX_VISIBLE_CHARACTERS - _entry.text_submitted.connect(_on_entry_text_submitted) + _entry.text_submitted.connect(func(_value: String) -> void: _send()) _entry.text_changed.connect(_on_draft_changed) UtilityPageStyle.apply_ocean_line_edit(_entry) _entry.add_theme_font_size_override("font_size", INPUT_FONT_SIZE) @@ -685,11 +665,6 @@ func _create_speech_pointer() -> Polygon2D: return pointer -func _on_entry_text_submitted(_value: String) -> void: - _last_submit_frame = Engine.get_process_frames() - _send() - - func _send() -> void: if _send_pending: return diff --git a/ui/icons/tab_menu/close.png b/ui/icons/tab_menu/close.png deleted file mode 100644 index 010564e..0000000 Binary files a/ui/icons/tab_menu/close.png and /dev/null differ diff --git a/ui/icons/tab_menu/close.png.import b/ui/icons/tab_menu/close.png.import deleted file mode 100644 index 97421f4..0000000 --- a/ui/icons/tab_menu/close.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://i0di6nvyfgwq" -path="res://.godot/imported/close.png-f592282e3f82852ae6231bd14b5fd099.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://ui/icons/tab_menu/close.png" -dest_files=["res://.godot/imported/close.png-f592282e3f82852ae6231bd14b5fd099.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/ui/icons/tab_menu/fishnet_simple.png b/ui/icons/tab_menu/fishnet_simple.png deleted file mode 100644 index 00f9a75..0000000 Binary files a/ui/icons/tab_menu/fishnet_simple.png and /dev/null differ diff --git a/ui/icons/tab_menu/fishnet_simple.png.import b/ui/icons/tab_menu/fishnet_simple.png.import deleted file mode 100644 index 3264172..0000000 --- a/ui/icons/tab_menu/fishnet_simple.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://cjbdqfnyvsnm4" -path="res://.godot/imported/fishnet_simple.png-4b51afc915ee521b23b2244427b7733d.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://ui/icons/tab_menu/fishnet_simple.png" -dest_files=["res://.godot/imported/fishnet_simple.png-4b51afc915ee521b23b2244427b7733d.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/ui/icons/tab_menu/inventory_simple.png b/ui/icons/tab_menu/inventory_simple.png deleted file mode 100644 index 94a41ef..0000000 Binary files a/ui/icons/tab_menu/inventory_simple.png and /dev/null differ diff --git a/ui/icons/tab_menu/inventory_simple.png.import b/ui/icons/tab_menu/inventory_simple.png.import deleted file mode 100644 index c570770..0000000 --- a/ui/icons/tab_menu/inventory_simple.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://cjcbjd5edx5x4" -path="res://.godot/imported/inventory_simple.png-f67cb963aaeb25aa9f4d1cdb8c686d15.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://ui/icons/tab_menu/inventory_simple.png" -dest_files=["res://.godot/imported/inventory_simple.png-f67cb963aaeb25aa9f4d1cdb8c686d15.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/ui/icons/tab_menu/logbook_icon_simple.png b/ui/icons/tab_menu/logbook_icon_simple.png deleted file mode 100644 index 0455fdd..0000000 Binary files a/ui/icons/tab_menu/logbook_icon_simple.png and /dev/null differ diff --git a/ui/icons/tab_menu/logbook_icon_simple.png.import b/ui/icons/tab_menu/logbook_icon_simple.png.import deleted file mode 100644 index 3b6c86a..0000000 --- a/ui/icons/tab_menu/logbook_icon_simple.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://bbrhsth08pra" -path="res://.godot/imported/logbook_icon_simple.png-19496dc73a295b5d47ceff1151998546.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://ui/icons/tab_menu/logbook_icon_simple.png" -dest_files=["res://.godot/imported/logbook_icon_simple.png-19496dc73a295b5d47ceff1151998546.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/ui/icons/tab_menu/mail_simple.png b/ui/icons/tab_menu/mail_simple.png deleted file mode 100644 index 0c5569c..0000000 Binary files a/ui/icons/tab_menu/mail_simple.png and /dev/null differ diff --git a/ui/icons/tab_menu/mail_simple.png.import b/ui/icons/tab_menu/mail_simple.png.import deleted file mode 100644 index 6ed128e..0000000 --- a/ui/icons/tab_menu/mail_simple.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://jlg77ov0slgm" -path="res://.godot/imported/mail_simple.png-95f4049e43a5076c9411faf284044682.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://ui/icons/tab_menu/mail_simple.png" -dest_files=["res://.godot/imported/mail_simple.png-95f4049e43a5076c9411faf284044682.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/ui/icons/tab_menu/online_simple.png b/ui/icons/tab_menu/online_simple.png deleted file mode 100644 index e332392..0000000 Binary files a/ui/icons/tab_menu/online_simple.png and /dev/null differ diff --git a/ui/icons/tab_menu/online_simple.png.import b/ui/icons/tab_menu/online_simple.png.import deleted file mode 100644 index 51ea7ab..0000000 --- a/ui/icons/tab_menu/online_simple.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://br2rfqaxc0sjl" -path="res://.godot/imported/online_simple.png-8eb00fe8c066e2f85263d1fb847d94d6.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://ui/icons/tab_menu/online_simple.png" -dest_files=["res://.godot/imported/online_simple.png-8eb00fe8c066e2f85263d1fb847d94d6.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/ui/icons/tab_menu/profile_simple.png b/ui/icons/tab_menu/profile_simple.png deleted file mode 100644 index de13f5d..0000000 Binary files a/ui/icons/tab_menu/profile_simple.png and /dev/null differ diff --git a/ui/icons/tab_menu/profile_simple.png.import b/ui/icons/tab_menu/profile_simple.png.import deleted file mode 100644 index 1a7ab07..0000000 --- a/ui/icons/tab_menu/profile_simple.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://cd08b16j75wep" -path="res://.godot/imported/profile_simple.png-4d3096990c6fb1ca6619433b80d957a2.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://ui/icons/tab_menu/profile_simple.png" -dest_files=["res://.godot/imported/profile_simple.png-4d3096990c6fb1ca6619433b80d957a2.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/ui/player_menu.tscn b/ui/player_menu.tscn index f990a93..4fcf0c0 100644 --- a/ui/player_menu.tscn +++ b/ui/player_menu.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=33 format=3] +[gd_scene load_steps=27 format=3] [ext_resource type="Script" path="res://ui/player_menu.gd" id="1_menu"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] @@ -24,13 +24,7 @@ [ext_resource type="Script" path="res://ui/components/inventory_notepad.gd" id="22_inventory_notepad"] [ext_resource type="Script" path="res://ui/components/organizer_tab.gd" id="23_organizer_tab"] [ext_resource type="PackedScene" path="res://ui/the_net_page.tscn" id="24_the_net_page"] -[ext_resource type="Texture2D" path="res://ui/icons/tab_menu/inventory_simple.png" id="25_inventory_simple"] -[ext_resource type="Texture2D" path="res://ui/icons/tab_menu/logbook_icon_simple.png" id="26_logbook_simple"] -[ext_resource type="Texture2D" path="res://ui/icons/tab_menu/fishnet_simple.png" id="27_fishnet_simple"] -[ext_resource type="Texture2D" path="res://ui/icons/tab_menu/mail_simple.png" id="28_mail_simple"] -[ext_resource type="Texture2D" path="res://ui/icons/tab_menu/profile_simple.png" id="29_profile_simple"] -[ext_resource type="Texture2D" path="res://ui/icons/tab_menu/online_simple.png" id="30_online_simple"] -[ext_resource type="Texture2D" path="res://ui/icons/tab_menu/close.png" id="31_close"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/online_dark.png" id="25_online_dark"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_collection"] @@ -1397,8 +1391,7 @@ compact_reference_size = Vector2(840, 100) unique_name_in_owner = true layout_mode = 0 toggle_mode = true -icon = ExtResource("25_inventory_simple") -tooltip_text = "stuff" +text = "stuff" accessibility_name = "stuff" script = ExtResource("6_bubble") profile = ExtResource("4_profile") @@ -1419,9 +1412,7 @@ deformation_period = 6.4 unique_name_in_owner = true layout_mode = 0 toggle_mode = true -icon = ExtResource("26_logbook_simple") -tooltip_text = "logbook" -accessibility_name = "logbook" +text = "logbook" script = ExtResource("6_bubble") profile = ExtResource("4_profile") neutral_size = Vector2(112, 108) @@ -1441,8 +1432,7 @@ deformation_period = 6.7 unique_name_in_owner = true layout_mode = 0 toggle_mode = true -icon = ExtResource("27_fishnet_simple") -tooltip_text = "fishnet" +text = "fishnet" accessibility_name = "net" script = ExtResource("6_bubble") profile = ExtResource("4_profile") @@ -1463,9 +1453,7 @@ deformation_period = 6.45 unique_name_in_owner = true layout_mode = 0 toggle_mode = true -icon = ExtResource("28_mail_simple") -tooltip_text = "mail" -accessibility_name = "mail" +text = "mail" script = ExtResource("6_bubble") profile = ExtResource("4_profile") neutral_size = Vector2(112, 108) @@ -1501,9 +1489,7 @@ vertical_alignment = 1 unique_name_in_owner = true layout_mode = 0 toggle_mode = true -icon = ExtResource("29_profile_simple") -tooltip_text = "profile" -accessibility_name = "profile" +text = "profile" script = ExtResource("6_bubble") profile = ExtResource("4_profile") neutral_size = Vector2(112, 108) @@ -1524,7 +1510,7 @@ unique_name_in_owner = true layout_mode = 0 toggle_mode = true texture_filter = 1 -icon = ExtResource("30_online_simple") +icon = ExtResource("25_online_dark") tooltip_text = "online" accessibility_name = "online" script = ExtResource("6_bubble") @@ -1545,9 +1531,7 @@ deformation_period = 6.4 [node name="CloseButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/NavigationCluster"] unique_name_in_owner = true layout_mode = 0 -icon = ExtResource("31_close") -tooltip_text = "close" -accessibility_name = "close" +text = "close" script = ExtResource("6_bubble") profile = ExtResource("4_profile") neutral_size = Vector2(96, 94) diff --git a/ui/profile_page.gd b/ui/profile_page.gd index 4a9f074..b895bac 100644 --- a/ui/profile_page.gd +++ b/ui/profile_page.gd @@ -3,7 +3,6 @@ extends Control const CHECK_DEBOUNCE_SECONDS: float = 0.4 const OPTION_GRID_COLUMNS: int = 6 -const FEATURE_DRAWER_ANIMATION_SECONDS: float = 0.16 const ControllerMappingManagerType = preload( "res://settings/controller_mapping_manager.gd" ) @@ -46,8 +45,6 @@ var _experience_level: Label var _experience_progress: ProgressBar var _experience_value: Label var _feature_preview_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 @@ -633,7 +630,7 @@ func _build_scale_option() -> void: _option_list.add_child(gameplay_note) -func _build_feature_preview_options(_options: Array) -> void: +func _build_feature_preview_options(options: Array) -> void: var scroll := ScrollContainer.new() scroll.custom_minimum_size = Vector2(190.0, 0.0) scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL @@ -649,153 +646,45 @@ func _build_feature_preview_options(_options: Array) -> void: var selected_id := CharacterCustomizationCatalog.canonical_option_id( _category_id, str(_draft_appearance.get(_category_id, "")) ) - var groups: Array = CharacterCustomizationCatalog.feature_option_groups( - _category_id - ) - var expanded_drawer_id := str( - _expanded_feature_drawers.get(_category_id, "") - ) - if expanded_drawer_id.is_empty(): - for group: Dictionary in groups: - var group_options: Array = group.get("options", []) as Array - if group_options.size() <= 1: - continue - var representative_id := str( - (group_options[0] as Dictionary).get("id", "") - ) - for option: Dictionary in group_options: - if ( - str(option.get("id", "")) == selected_id - and selected_id != representative_id - ): - expanded_drawer_id = str(group.get("id", "")) - _expanded_feature_drawers[_category_id] = ( - expanded_drawer_id - ) - break - if not expanded_drawer_id.is_empty(): - break - - var animation_key := _feature_drawer_animation_key - for group: Dictionary in groups: - var group_id := str(group.get("id", "")) - var group_options: Array = group.get("options", []) as Array - if group_options.is_empty(): - continue - var has_variants := group_options.size() > 1 - var is_expanded := has_variants and group_id == expanded_drawer_id - for option_index: int in range(group_options.size()): - if option_index > 0 and not is_expanded: - continue - var option: Dictionary = group_options[option_index] as Dictionary - var button := _build_feature_option_button( - option, - selected_id, - group_id, - option_index == 0, - has_variants, - is_expanded, - ) - grid.add_child(button) - if ( - option_index > 0 - and animation_key == "%s:%s" % [_category_id, group_id] - ): - _animate_feature_drawer_button(button) - _feature_drawer_animation_key = "" - - -func _build_feature_option_button( - option: Dictionary, - selected_id: String, - drawer_id: String, - is_representative: bool, - has_variants: bool, - is_expanded: bool, -) -> Button: - var option_id := str(option.get("id", "")) - var button := Button.new() - var is_none := option_id == "none" - button.text = "×" if is_none else "" - button.tooltip_text = "none" if is_none else str( - option.get("label", option_id) - ) - if is_representative and has_variants: - button.tooltip_text += ( - " (hide variants)" if is_expanded else " (show variants)" + for option: Dictionary in options: + var option_id := str(option.get("id", "")) + var button := Button.new() + var is_none := option_id == "none" + button.text = "×" if is_none else "" + button.tooltip_text = "none" if is_none else str( + option.get("label", option_id) ) - button.toggle_mode = true - button.button_pressed = selected_id == option_id - button.custom_minimum_size = Vector2(84.0, 64.0) - button.alignment = HORIZONTAL_ALIGNMENT_CENTER - button.clip_contents = true - if is_representative and has_variants: - button.pressed.connect( - _select_feature_drawer.bind(_category_id, drawer_id, option_id) - ) - else: + button.toggle_mode = true + button.button_pressed = selected_id == option_id + button.custom_minimum_size = Vector2(84.0, 64.0) + button.alignment = HORIZONTAL_ALIGNMENT_CENTER + button.clip_contents = true button.pressed.connect(_select_option.bind(_category_id, option_id)) - UtilityPageStyle.apply_compact_ocean_button(button) - button.custom_minimum_size = Vector2(84.0, 64.0) - if is_none: - button.add_theme_font_size_override("font_size", 28) - else: - var preview := TextureRect.new() - preview.texture = _feature_preview_texture(_category_id, option_id) - preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - preview.mouse_filter = Control.MOUSE_FILTER_IGNORE - var preview_height := 50.0 - var preview_width := clampf( - preview_height * PlayerVisualPresenter.feature_uv_aspect(_category_id), - 32.0, - 74.0, - ) - preview.custom_minimum_size = Vector2(preview_width, preview_height) - preview.size = Vector2(preview_width, preview_height) - preview.position = Vector2((84.0 - preview_width) * 0.5, 7.0) - button.add_child(preview) - if is_representative and has_variants: - var indicator := Label.new() - indicator.text = "<" if is_expanded else ">" - indicator.position = Vector2(66.0, 1.0) - indicator.size = Vector2(14.0, 16.0) - indicator.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - indicator.mouse_filter = Control.MOUSE_FILTER_IGNORE - indicator.add_theme_font_size_override("font_size", 13) - indicator.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY - ) - button.add_child(indicator) - return button - - -func _select_feature_drawer( - category_id: String, - drawer_id: String, - option_id: String, -) -> void: - if str(_expanded_feature_drawers.get(category_id, "")) == drawer_id: - _expanded_feature_drawers.erase(category_id) - else: - _expanded_feature_drawers[category_id] = drawer_id - _feature_drawer_animation_key = "%s:%s" % [category_id, drawer_id] - _select_option(category_id, option_id) - - -func _animate_feature_drawer_button(button: Button) -> void: - button.scale = Vector2(0.05, 1.0) - var faded := button.modulate - faded.a = 0.0 - button.modulate = faded - var tween := button.create_tween() - tween.set_parallel(true) - tween.tween_property( - button, "scale", Vector2.ONE, FEATURE_DRAWER_ANIMATION_SECONDS - ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) - tween.tween_property( - button, "modulate:a", 1.0, FEATURE_DRAWER_ANIMATION_SECONDS - ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + UtilityPageStyle.apply_compact_ocean_button(button) + button.custom_minimum_size = Vector2(84.0, 64.0) + if is_none: + button.add_theme_font_size_override("font_size", 28) + else: + var preview := TextureRect.new() + preview.texture = _feature_preview_texture( + _category_id, option_id + ) + preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + preview.mouse_filter = Control.MOUSE_FILTER_IGNORE + var preview_height := 50.0 + var preview_width := clampf( + preview_height * PlayerVisualPresenter.feature_uv_aspect( + _category_id + ), + 32.0, + 74.0, + ) + preview.custom_minimum_size = Vector2(preview_width, preview_height) + preview.size = Vector2(preview_width, preview_height) + preview.position = Vector2((84.0 - preview_width) * 0.5, 7.0) + button.add_child(preview) + grid.add_child(button) func _feature_preview_texture( diff --git a/ui/profile_preview.gd b/ui/profile_preview.gd index 713a6ba..5076cd3 100644 --- a/ui/profile_preview.gd +++ b/ui/profile_preview.gd @@ -9,7 +9,6 @@ const UIReferencePresentationType = preload( ) const FRONT_FACING_YAW: float = PI const DEFAULT_CAMERA_DISTANCE: float = 1.9 -const DEFAULT_CAMERA_PITCH: float = 0.12 const MIN_CAMERA_DISTANCE: float = 1.05 const MAX_CAMERA_DISTANCE: float = 2.8 const CAMERA_ZOOM_STEP: float = 0.14 @@ -26,7 +25,7 @@ const CAMERA_TARGET_HEIGHT: float = 0.95 var _dragging: bool = false var _camera_yaw: float = 0.0 -var _camera_pitch: float = DEFAULT_CAMERA_PITCH +var _camera_pitch: float = 0.0 var _camera_distance: float = DEFAULT_CAMERA_DISTANCE var _visuals: Node3D var _controller_mapping_manager: ControllerMappingManagerType @@ -86,9 +85,11 @@ func apply_appearance_profile(profile: Dictionary) -> void: func reset_view() -> void: _preview_root.rotation.y = FRONT_FACING_YAW _camera_yaw = 0.0 - _camera_pitch = DEFAULT_CAMERA_PITCH + _camera_pitch = 0.0 _camera_distance = DEFAULT_CAMERA_DISTANCE _apply_camera_orbit() + if _preview_camera != null: + _preview_camera.position.z = DEFAULT_CAMERA_DISTANCE func _process(delta: float) -> void: diff --git a/world/interactables/fishing_shop_world.tscn b/world/interactables/fishing_shop_world.tscn index 7221005..a1eaaa9 100644 --- a/world/interactables/fishing_shop_world.tscn +++ b/world/interactables/fishing_shop_world.tscn @@ -15,7 +15,6 @@ script = ExtResource("3_display") species_id = "pointy" fur_color_id = "white" ears_id = "pointy_wide" -tail_id = "fox" eyes_id = "punk_purple" nose_id = "triangle_small" mouth_id = "slanted" diff --git a/world/world_character_display.gd b/world/world_character_display.gd index a145f3b..024551d 100644 --- a/world/world_character_display.gd +++ b/world/world_character_display.gd @@ -25,15 +25,6 @@ const PlayerVisualPresenterType = preload( "pointy_short", "pointy_wide", ) var ears_id: String = "none" -@export_enum( - "none", - "bear", - "bunny", - "cat", - "fox", - "gator", - "pointy", -) var tail_id: String = "none" @export var eyes_id: String = "simple_shine" @export var nose_id: String = "dog_round" @export var mouth_id: String = "three" @@ -57,7 +48,6 @@ func _ready() -> void: appearance["species"] = species_id appearance["fur_pattern"] = fur_color_id appearance["ears"] = ears_id - appearance["tail"] = tail_id appearance["eyes"] = eyes_id appearance["nose"] = nose_id appearance["mouth"] = mouth_id