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

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