Expand character customization and refine fishing flow

This commit is contained in:
Alexander Sellite 2026-08-05 16:25:16 -04:00
parent 8cc4c0985d
commit 43f57e56a0
144 changed files with 3108 additions and 189 deletions

View file

@ -21,6 +21,9 @@ const HANDLE_GAP: float = 6.0
const COLLAPSED_REVEAL_WIDTH: float = 8.0
const HINT_EDGE_MARGIN: float = 4.0
const SPEECH_BUBBLE_WIDTH: float = 320.0
const SPEECH_POINTER_HALF_WIDTH: float = 12.0
const SPEECH_POINTER_HEIGHT: float = 14.0
const SPEECH_POINTER_OVERLAP: float = 3.0
const MOBILE_COMPACT_WIDTH: float = 620.0
const MOBILE_EXPANDED_WIDTH: float = 820.0
const MOBILE_COMPACT_HEIGHT: float = 220.0
@ -571,20 +574,31 @@ func _apply_flat_chat_button(button: Button) -> void:
func _speech_panel_style() -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color(UtilityPageStyle.PAPER_ALT, 0.96)
style.border_color = Color(UtilityPageStyle.BORDER, 0.88)
style.set_border_width_all(2)
style.bg_color = Color(UtilityPageStyle.OCEAN_PANEL_MID, 0.96)
style.set_border_width_all(0)
style.set_corner_radius_all(12)
style.anti_aliasing = false
style.content_margin_left = 10
style.content_margin_right = 10
style.content_margin_top = 6
style.content_margin_bottom = 6
style.shadow_color = Color(0, 0, 0, 0.32)
style.shadow_size = 4
style.shadow_offset = Vector2(2, 3)
return style
func _create_speech_pointer() -> Polygon2D:
var pointer := Polygon2D.new()
pointer.name = "SpeechPointer"
pointer.polygon = PackedVector2Array([
Vector2(-SPEECH_POINTER_HALF_WIDTH, 0.0),
Vector2(SPEECH_POINTER_HALF_WIDTH, 0.0),
Vector2(0.0, SPEECH_POINTER_HEIGHT),
])
pointer.color = Color(UtilityPageStyle.OCEAN_PANEL_MID, 0.96)
pointer.antialiased = false
pointer.z_index = -1
return pointer
func _send() -> void:
if _send_pending:
return
@ -630,6 +644,8 @@ func _on_message(message: Dictionary) -> void:
bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
bubble.custom_minimum_size = Vector2(SPEECH_BUBBLE_WIDTH, 0.0)
bubble.add_theme_stylebox_override("panel", _speech_panel_style())
var pointer := _create_speech_pointer()
bubble.add_child(pointer)
var label := Label.new()
label.text = str(message["body"])
label.custom_minimum_size = Vector2(
@ -641,11 +657,14 @@ func _on_message(message: Dictionary) -> void:
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
label.add_theme_color_override("font_color", UtilityPageStyle.INK)
label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
bubble.add_child(label)
_speech_layer.add_child(bubble)
_speech[peer_id] = {
"bubble": bubble,
"pointer": pointer,
"expires": Time.get_ticks_msec() / 1000.0 + SPEECH_SECONDS,
"fingerprint": str(message.get("sender_fingerprint", "")),
}
@ -863,12 +882,30 @@ func _update_speech() -> void:
bubble.hide()
continue
var desired := screen_position - Vector2(
bubble.size.x * 0.5, bubble.size.y + 8.0
bubble.size.x * 0.5,
bubble.size.y + SPEECH_POINTER_HEIGHT,
)
bubble.position = Vector2(
clampf(desired.x, 8.0, viewport_size.x - bubble.size.x - 8.0),
clampf(desired.y, 8.0, viewport_size.y - bubble.size.y - 8.0),
clampf(
desired.y,
8.0,
viewport_size.y
- bubble.size.y
- SPEECH_POINTER_HEIGHT
- 8.0,
),
)
var pointer := state.get("pointer") as Polygon2D
if pointer != null:
pointer.position = Vector2(
clampf(
screen_position.x - bubble.position.x,
SPEECH_POINTER_HALF_WIDTH + 12.0,
bubble.size.x - SPEECH_POINTER_HALF_WIDTH - 12.0,
),
bubble.size.y - SPEECH_POINTER_OVERLAP,
)
bubble.show()

View file

@ -78,6 +78,10 @@ const CONTROLLER_MENU_SCROLL_SPEED: float = 660.0
const VIRTUAL_MOUSE_TRIGGER_AXIS: JoyAxis = JOY_AXIS_TRIGGER_RIGHT
const VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS: JoyAxis = JOY_AXIS_TRIGGER_LEFT
const VIRTUAL_MOUSE_SECONDARY_CLICK_AXIS: JoyAxis = JOY_AXIS_TRIGGER_LEFT
const FISHING_PANEL_DEFAULT_TOP_OFFSET: float = -210.0
const FISHING_PANEL_DEFAULT_BOTTOM_OFFSET: float = -140.0
const FISHING_PANEL_SHOWCASE_TOP_OFFSET: float = -174.0
const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0
@onready var _status_label: Label = %StatusLabel
@onready var _gameplay_transient_hud: Control = %GameplayTransientHUD
@ -155,6 +159,7 @@ var _virtual_mouse_stick: Vector2 = Vector2.ZERO
var _virtual_mouse_trigger_rest_by_device: Dictionary[int, float] = {}
var _shared_trigger_rest_by_device: Dictionary[int, float] = {}
var _controller_mapping_manager: ControllerMappingManagerType
var _settings_manager: PlayerSettingsManagerType
func _ready() -> void:
@ -231,6 +236,7 @@ func setup(
world_sun: DirectionalLight3D,
) -> void:
_player = player
_settings_manager = settings_manager
_fishing_spot = fishing_spot
_item_effects = item_effects
_experience = experience
@ -290,6 +296,15 @@ func setup(
world_environment,
world_sun,
)
if (
_settings_manager != null
and not _settings_manager.settings_changed.is_connected(
_on_player_settings_changed
)
):
_settings_manager.settings_changed.connect(_on_player_settings_changed)
if _settings_manager != null:
_on_player_settings_changed(_settings_manager.current_settings)
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot, inventory)
_fishing_shop.setup(
player,
@ -347,7 +362,11 @@ func _input(event: InputEvent) -> void:
)
if (
_surface_drawing != null
and _surface_drawing.handle_input(event, drawing_can_open)
and _surface_drawing.handle_input(
event,
drawing_can_open,
_drawing_pointer_window_position(),
)
):
get_viewport().set_input_as_handled()
return
@ -959,7 +978,13 @@ func _toggle_surface_drawing() -> void:
if _surface_drawing.is_active():
_surface_drawing.deactivate()
elif _surface_drawing.can_activate():
_surface_drawing.activate()
_surface_drawing.activate(_drawing_pointer_window_position())
func _drawing_pointer_window_position() -> Vector2:
if _virtual_mouse_active:
return _virtual_mouse_window_position
return get_window().get_mouse_position()
func setup_data_and_identity(
@ -1229,6 +1254,13 @@ func set_effective_ui_pixel_size(pixel_size: int) -> void:
_pause_settings_panel.set_effective_ui_pixel_size(pixel_size)
func _on_player_settings_changed(settings: PlayerSettings) -> void:
if settings != null:
_player_menu.set_profile_preview_world_pixel_size(
settings.world_pixel_size
)
func set_edge_docks(
chat_dock_right: bool,
paint_dock_right: bool,
@ -1389,11 +1421,13 @@ func _on_showcase_changed(
) -> void:
_showcase_active = visible
if not visible:
_set_fishing_panel_showcase_position(false)
_showcase_details.text = ""
_showcase_details.visible = false
_set_fishing_status("")
call_deferred("_start_next_experience_animation")
return
_set_fishing_panel_showcase_position(true)
_catch_track.visible = false
_barrier_prompt_panel.visible = false
_barrier_prompt.visible = false
@ -1411,6 +1445,19 @@ func _on_showcase_changed(
_refresh_fishing_panel_visibility()
func _set_fishing_panel_showcase_position(showcase: bool) -> void:
_fishing_panel.offset_top = (
FISHING_PANEL_SHOWCASE_TOP_OFFSET
if showcase
else FISHING_PANEL_DEFAULT_TOP_OFFSET
)
_fishing_panel.offset_bottom = (
FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET
if showcase
else FISHING_PANEL_DEFAULT_BOTTOM_OFFSET
)
func _on_experience_awarded(
amount: int,
previous_total: int,

View file

@ -582,6 +582,10 @@ func setup_controller_mapping(
_profile_page.setup_controller_mapping(_controller_mapping_manager)
func set_profile_preview_world_pixel_size(pixel_size: int) -> void:
_profile_page.set_world_pixel_size(pixel_size)
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.echo:
return

View file

@ -33,6 +33,8 @@ var _debounce: Timer
var _experience_level: Label
var _experience_progress: ProgressBar
var _experience_value: Label
var _feature_preview_cache: Dictionary = {}
var _scale_value_label: Label
func _ready() -> void:
@ -84,6 +86,11 @@ func setup_controller_mapping(
_preview.setup_controller_mapping(mapping_manager)
func set_world_pixel_size(pixel_size: int) -> void:
if _preview != null:
_preview.set_world_pixel_size(pixel_size)
func activate() -> void:
visible = true
UtilityPageStyle.animate_in(self)
@ -281,48 +288,51 @@ func _build_ui() -> void:
_category_list.add_theme_constant_override("separation", 5)
body.add_child(_category_list)
_option_list = VBoxContainer.new()
_option_list.custom_minimum_size = Vector2(170, 0)
_option_list.custom_minimum_size = Vector2(360, 0)
_option_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_option_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
_option_list.add_theme_constant_override("separation", 5)
body.add_child(_option_list)
var body_spacer := Control.new()
body_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
body_spacer.custom_minimum_size.x = 12.0
body_spacer.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
body.add_child(body_spacer)
var preview_stack := VBoxContainer.new()
preview_stack.custom_minimum_size.x = 180.0
preview_stack.custom_minimum_size.x = 260.0
preview_stack.size_flags_vertical = Control.SIZE_EXPAND_FILL
preview_stack.alignment = BoxContainer.ALIGNMENT_CENTER
preview_stack.add_theme_constant_override("separation", 7)
body.add_child(preview_stack)
var preview_frame := PanelContainer.new()
preview_frame.custom_minimum_size = Vector2(180.0, 220.0)
preview_frame.custom_minimum_size = Vector2(260.0, 0.0)
preview_frame.size_flags_vertical = Control.SIZE_EXPAND_FILL
preview_frame.add_theme_stylebox_override(
"panel", UtilityPageStyle.rounded_style(
UtilityPageStyle.OCEAN_FIELD, 18
)
)
preview_stack.add_child(preview_frame)
var preview_layer := Control.new()
preview_layer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
preview_layer.size_flags_vertical = Control.SIZE_EXPAND_FILL
preview_layer.mouse_filter = Control.MOUSE_FILTER_PASS
preview_frame.add_child(preview_layer)
_preview = preload("res://ui/profile_preview.tscn").instantiate()
_preview.custom_minimum_size = Vector2(180.0, 220.0)
preview_frame.add_child(_preview)
var preview_actions := VBoxContainer.new()
preview_actions.alignment = BoxContainer.ALIGNMENT_CENTER
preview_actions.add_theme_constant_override("separation", 4)
preview_stack.add_child(preview_actions)
var preview_note := Label.new()
preview_note.text = "drag or use left / right"
preview_note.custom_minimum_size.x = 180.0
preview_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
preview_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
preview_note.add_theme_font_size_override("font_size", 12)
preview_note.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
preview_actions.add_child(preview_note)
_preview.custom_minimum_size = Vector2(260.0, 0.0)
_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_preview.size_flags_vertical = Control.SIZE_EXPAND_FILL
preview_layer.add_child(_preview)
_preview.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var reset_view := Button.new()
reset_view.text = "reset view"
reset_view.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
reset_view.text = ""
reset_view.tooltip_text = "reset view"
reset_view.custom_minimum_size = Vector2(36.0, 36.0)
reset_view.position = Vector2(-8.0, -8.0)
reset_view.z_index = 2
reset_view.pressed.connect(_preview.reset_view)
UtilityPageStyle.apply_compact_ocean_button(reset_view)
preview_actions.add_child(reset_view)
reset_view.add_theme_font_size_override("font_size", 22)
preview_layer.add_child(reset_view)
_discard_confirmation = PanelContainer.new()
_discard_confirmation.visible = false
@ -396,6 +406,9 @@ func _refresh_options() -> void:
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
_option_list.add_child(title)
if _category_id == CharacterCustomizationCatalog.SCALE_CATEGORY_ID:
_build_scale_option()
return
var options: Array = CharacterCustomizationCatalog.options_for(_category_id)
if options.is_empty():
var empty := Label.new()
@ -406,18 +419,176 @@ func _refresh_options() -> void:
if _category_id == "fur_pattern":
_build_fur_color_options(options)
return
if _category_id in CharacterCustomizationCatalog.FEATURE_CATEGORIES:
_build_feature_preview_options(options)
return
for option: Dictionary in options:
var option_id := str(option["id"])
var button := Button.new()
button.text = str(option["label"])
button.text = "×" if option_id == "none" else str(option["label"])
button.tooltip_text = "none" if option_id == "none" else str(
option["label"]
)
button.toggle_mode = true
button.custom_minimum_size.x = 170.0
button.button_pressed = _draft_appearance.get(_category_id) == option_id
button.pressed.connect(_select_option.bind(_category_id, option_id))
UtilityPageStyle.apply_compact_ocean_button(button)
if option_id == "none":
button.add_theme_font_size_override("font_size", 24)
_option_list.add_child(button)
func _build_scale_option() -> void:
var description := Label.new()
description.text = "Adjust your character's visual size."
description.add_theme_font_size_override("font_size", 14)
description.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
_option_list.add_child(description)
var value_row := HBoxContainer.new()
value_row.add_theme_constant_override("separation", 10)
value_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_option_list.add_child(value_row)
var small_label := Label.new()
small_label.text = "small"
small_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
small_label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
value_row.add_child(small_label)
var slider := HSlider.new()
slider.name = "CharacterScaleSlider"
slider.min_value = CharacterCustomizationCatalog.MIN_CHARACTER_SCALE
slider.max_value = CharacterCustomizationCatalog.MAX_CHARACTER_SCALE
slider.step = CharacterCustomizationCatalog.CHARACTER_SCALE_STEP
slider.custom_minimum_size = Vector2(220.0, 40.0)
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
slider.tooltip_text = "character size"
slider.value = CharacterCustomizationCatalog.character_scale(
_draft_appearance.get(
CharacterCustomizationCatalog.SCALE_CATEGORY_ID,
CharacterCustomizationCatalog.DEFAULT_CHARACTER_SCALE,
)
)
slider.value_changed.connect(_select_scale)
value_row.add_child(slider)
var large_label := Label.new()
large_label.text = "large"
large_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
large_label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
value_row.add_child(large_label)
_scale_value_label = Label.new()
_scale_value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_scale_value_label.add_theme_font_size_override("font_size", 20)
_scale_value_label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
_option_list.add_child(_scale_value_label)
_update_scale_value_label(float(slider.value))
var gameplay_note := Label.new()
gameplay_note.text = "Movement and collision stay the same."
gameplay_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
gameplay_note.add_theme_font_size_override("font_size", 12)
gameplay_note.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
_option_list.add_child(gameplay_note)
func _build_feature_preview_options(options: Array) -> void:
var scroll := ScrollContainer.new()
scroll.custom_minimum_size = Vector2(190.0, 0.0)
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
_option_list.add_child(scroll)
var grid := GridContainer.new()
grid.columns = 4
grid.add_theme_constant_override("h_separation", 8)
grid.add_theme_constant_override("v_separation", 8)
grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.add_child(grid)
var selected_id := CharacterCustomizationCatalog.canonical_option_id(
_category_id, str(_draft_appearance.get(_category_id, ""))
)
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
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)
grid.add_child(button)
func _feature_preview_texture(
category_id: String,
option_id: String,
) -> Texture2D:
var cache_key := "%s:%s" % [category_id, option_id]
if _feature_preview_cache.has(cache_key):
return _feature_preview_cache[cache_key] as Texture2D
var source := CharacterCustomizationCatalog.texture_for(
category_id, option_id
)
if source == null:
return null
var image := source.get_image()
var used := image.get_used_rect()
if used.size.x <= 0 or used.size.y <= 0:
_feature_preview_cache[cache_key] = source
return source
var padding := 12
used.position -= Vector2i(padding, padding)
used.size += Vector2i(padding * 2, padding * 2)
used = used.intersection(Rect2i(Vector2i.ZERO, image.get_size()))
var cropped := image.get_region(used)
var preview := ImageTexture.create_from_image(cropped)
_feature_preview_cache[cache_key] = preview
return preview
func _build_fur_color_options(options: Array) -> void:
var grid := GridContainer.new()
grid.columns = 4
@ -469,6 +640,26 @@ func _select_option(category_id: String, option_id: String) -> void:
_refresh_actions()
func _select_scale(value: float) -> void:
var resolved_scale: float = CharacterCustomizationCatalog.character_scale(
value
)
_draft_appearance[CharacterCustomizationCatalog.SCALE_CATEGORY_ID] = (
resolved_scale
)
_update_scale_value_label(resolved_scale)
_preview.apply_appearance_profile(_draft_appearance)
_dirty = _draft_differs()
_refresh_actions()
func _update_scale_value_label(value: float) -> void:
if _scale_value_label != null:
_scale_value_label.text = "%d%%" % (
CharacterCustomizationCatalog.character_scale_percent(value)
)
func _on_name_changed(value: String) -> void:
_draft_name = value
_allow_duplicate = false

View file

@ -4,7 +4,15 @@ extends SubViewportContainer
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const UIReferencePresentationType = preload(
"res://ui/ui_reference_presentation.gd"
)
const FRONT_FACING_YAW: float = PI
const DEFAULT_CAMERA_DISTANCE: float = 1.9
const MIN_CAMERA_DISTANCE: float = 1.05
const MAX_CAMERA_DISTANCE: float = 2.8
const CAMERA_ZOOM_STEP: float = 0.14
const CAMERA_TARGET_HEIGHT: float = 0.95
@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
@ -12,12 +20,18 @@ const FRONT_FACING_YAW: float = PI
@onready var _preview_root: Node3D = %PreviewRoot
@onready var _preview_environment: WorldEnvironment = %WorldEnvironment
@onready var _preview_sun: DirectionalLight3D = %KeyLight
@onready var _preview_camera: Camera3D = $Viewport/Camera3D
@onready var _pixelation_material: ShaderMaterial = material as ShaderMaterial
var _dragging: bool = false
var _camera_yaw: float = 0.0
var _camera_pitch: float = 0.0
var _camera_distance: float = DEFAULT_CAMERA_DISTANCE
var _visuals: Node3D
var _controller_mapping_manager: ControllerMappingManagerType
var _source_environment: WorldEnvironment
var _source_sun: DirectionalLight3D
var _world_pixel_size: int = PlayerSettings.DEFAULT_WORLD_PIXEL_SIZE
func setup_controller_mapping(
@ -35,9 +49,22 @@ func setup_world_lighting(
_sync_world_lighting()
func set_world_pixel_size(pixel_size: int) -> void:
_world_pixel_size = clampi(
pixel_size,
PlayerSettings.MIN_WORLD_PIXEL_SIZE,
PlayerSettings.MAX_WORLD_PIXEL_SIZE,
)
_refresh_pixelation_filter()
func _ready() -> void:
focus_mode = Control.FOCUS_ALL
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
gui_input.connect(_on_gui_input)
resized.connect(_refresh_pixelation_filter)
get_window().size_changed.connect(_refresh_pixelation_filter)
_configure_preview_lighting()
reset_view()
_visuals = PlayerVisualPresenter.instantiate_visuals()
_preview_root.add_child(_visuals)
@ -47,6 +74,7 @@ func _ready() -> void:
var catch_display := _visuals.find_child("CatchDisplay", true, false) as Node3D
if catch_display != null:
catch_display.visible = false
call_deferred("_refresh_pixelation_filter")
func apply_appearance_profile(profile: Dictionary) -> void:
@ -56,6 +84,12 @@ func apply_appearance_profile(profile: Dictionary) -> void:
func reset_view() -> void:
_preview_root.rotation.y = FRONT_FACING_YAW
_camera_yaw = 0.0
_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:
@ -77,13 +111,30 @@ func _process(delta: float) -> void:
func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event is InputEventMouseButton and event.shift_pressed:
if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
_zoom_preview(-CAMERA_ZOOM_STEP)
grab_focus()
accept_event()
return
if event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed:
_zoom_preview(CAMERA_ZOOM_STEP)
grab_focus()
accept_event()
return
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
_dragging = event.pressed
if event.pressed:
grab_focus()
accept_event()
elif event is InputEventMouseMotion and _dragging:
_rotate(event.relative.x * drag_sensitivity)
_camera_pitch = clampf(
_camera_pitch + event.relative.y * drag_sensitivity,
-0.55,
0.55,
)
_apply_camera_orbit()
accept_event()
@ -93,20 +144,96 @@ func _notification(what: int) -> void:
func _rotate(amount: float) -> void:
_preview_root.rotation.y = fposmod(_preview_root.rotation.y + amount, TAU)
_camera_yaw = fposmod(_camera_yaw + amount, TAU)
_apply_camera_orbit()
func _zoom_preview(amount: float) -> void:
if _preview_camera == null:
return
_camera_distance = clampf(
_camera_distance + amount,
MIN_CAMERA_DISTANCE,
MAX_CAMERA_DISTANCE,
)
_apply_camera_orbit()
func _apply_camera_orbit() -> void:
if _preview_camera == null:
return
var horizontal_distance := cos(_camera_pitch) * _camera_distance
_preview_camera.position = Vector3(
sin(_camera_yaw) * horizontal_distance,
CAMERA_TARGET_HEIGHT + sin(_camera_pitch) * _camera_distance,
cos(_camera_yaw) * horizontal_distance,
)
_preview_camera.look_at(
Vector3(0.0, CAMERA_TARGET_HEIGHT, 0.0),
Vector3.UP,
)
func _sync_world_lighting() -> void:
if (
_preview_environment == null
or _preview_sun == null
or _source_environment == null
or _source_sun == null
):
# The customization viewport deliberately does not follow gameplay time of
# day. Keep this method as a compatibility seam for existing setup callers.
pass
func _configure_preview_lighting() -> void:
if _preview_environment == null or _preview_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
var environment := Environment.new()
environment.background_mode = Environment.BG_COLOR
environment.background_color = Color("082431")
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
environment.ambient_light_color = Color("b9d8d4")
environment.ambient_light_energy = 1.15
environment.reflected_light_source = Environment.REFLECTION_SOURCE_DISABLED
_preview_environment.environment = environment
_preview_sun.rotation_degrees = Vector3(-35.0, -25.0, 0.0)
_preview_sun.light_color = Color("fff1d0")
_preview_sun.light_energy = 1.1
_preview_sun.light_indirect_energy = 0.0
_preview_sun.shadow_enabled = false
func _refresh_pixelation_filter() -> void:
if not is_node_ready() or _pixelation_material == null:
return
_pixelation_material.set_shader_parameter(
"logical_grid_size",
Vector2(calculate_pixelated_grid_size(
size,
Vector2(get_window().size),
_world_pixel_size,
)),
)
static func calculate_pixelated_grid_size(
control_size: Vector2,
window_size: Vector2,
pixel_size: int,
) -> Vector2i:
var safe_window_size := Vector2(
maxf(window_size.x, 1.0),
maxf(window_size.y, 1.0),
)
var world_grid_size: Vector2i = PlayerSettings.get_world_grid_size(
pixel_size,
Vector2i(roundi(safe_window_size.x), roundi(safe_window_size.y)),
)
var world_pixel_extent: float = (
safe_window_size.y / float(maxi(world_grid_size.y, 1))
)
var presentation_scale: float = UIReferencePresentationType.get_scale(
safe_window_size
)
var grid_size: Vector2 = (
control_size * presentation_scale / maxf(world_pixel_extent, 0.001)
)
return Vector2i(
maxi(roundi(grid_size.x), 1),
maxi(roundi(grid_size.y), 1),
)

View file

@ -1,11 +1,17 @@
[gd_scene load_steps=3 format=3]
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://ui/profile_preview.gd" id="1"]
[ext_resource type="Shader" path="res://ui/profile_preview_pixelation.gdshader" id="2_pixelation"]
[sub_resource type="World3D" id="PreviewWorld"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_pixelation"]
shader = ExtResource("2_pixelation")
[node name="ProfilePreview" type="SubViewportContainer"]
material = SubResource("ShaderMaterial_pixelation")
custom_minimum_size = Vector2(300, 220)
texture_filter = 1
stretch = true
script = ExtResource("1")
@ -27,5 +33,6 @@ shadow_enabled = false
unique_name_in_owner = true
[node name="Camera3D" type="Camera3D" parent="Viewport"]
unique_name_in_owner = true
position = Vector3(0, 0.95, 1.9)
current = true

View file

@ -0,0 +1,11 @@
shader_type canvas_item;
render_mode unshaded;
uniform vec2 logical_grid_size = vec2(160.0, 165.0);
void fragment() {
vec2 safe_grid = max(logical_grid_size, vec2(1.0));
vec2 snapped_uv = (floor(UV * safe_grid) + vec2(0.5)) / safe_grid;
COLOR = texture(TEXTURE, snapped_uv);
}

View file

@ -0,0 +1 @@
uid://dlbc0pxkqo028