Add customizable fur patterns

This commit is contained in:
Alexander Sellite 2026-08-12 16:29:58 -04:00
parent 9729c278dc
commit 1940ae13e5
21 changed files with 705 additions and 49 deletions

View file

@ -3,6 +3,8 @@ extends Control
const CHECK_DEBOUNCE_SECONDS: float = 0.4
const OPTION_GRID_COLUMNS: int = 6
const FUR_PALETTE_GRID_COLUMNS: int = 10
const FUR_CHANNEL_ICON_SIZE: int = 20
const FEATURE_DRAWER_ANIMATION_SECONDS: float = 0.16
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
@ -48,11 +50,13 @@ var _experience_level: Label
var _experience_progress: ProgressBar
var _experience_value: Label
var _feature_preview_cache: Dictionary = {}
var _fur_swatch_icon_cache: Dictionary = {}
var _expanded_feature_drawers: Dictionary = {}
var _feature_drawer_animation_key: String = ""
var _scale_value_label: Label
var _voice_preview: AnimaleseVoiceType
var _voice_preview_tween: Tween
var _active_fur_color_id: String = "fur_pattern"
func _ready() -> void:
@ -882,30 +886,167 @@ func _feature_preview_texture(
func _build_fur_color_options(options: Array) -> void:
var grid := GridContainer.new()
grid.columns = OPTION_GRID_COLUMNS
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"))
var pattern_label := Label.new()
pattern_label.text = "pattern"
pattern_label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
_option_list.add_child(pattern_label)
var pattern_row := HBoxContainer.new()
pattern_row.add_theme_constant_override("separation", 8)
_option_list.add_child(pattern_row)
var selected_style_id := str(_draft_appearance.get(
CharacterCustomizationCatalog.FUR_STYLE_ID,
CharacterCustomizationCatalog.DEFAULT_FUR_STYLE,
))
for pattern_option: Dictionary in CharacterCustomizationCatalog.options_for(
CharacterCustomizationCatalog.FUR_STYLE_ID
):
var pattern_id := str(pattern_option.get("id", ""))
var pattern_button := Button.new()
pattern_button.text = str(pattern_option.get("label", pattern_id))
pattern_button.toggle_mode = true
pattern_button.button_pressed = selected_style_id == pattern_id
pattern_button.custom_minimum_size = Vector2(118.0, 32.0)
pattern_button.pressed.connect(_select_option.bind(
CharacterCustomizationCatalog.FUR_STYLE_ID,
pattern_id,
))
UtilityPageStyle.apply_compact_ocean_button(pattern_button)
pattern_row.add_child(pattern_button)
var channel_label := Label.new()
channel_label.text = "color channel"
channel_label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
_option_list.add_child(channel_label)
var channel_grid := GridContainer.new()
channel_grid.columns = 2
channel_grid.add_theme_constant_override("h_separation", 8)
channel_grid.add_theme_constant_override("v_separation", 8)
_option_list.add_child(channel_grid)
for color_category_id: String in CharacterCustomizationCatalog.FUR_COLOR_IDS:
var selected_color_id := str(_draft_appearance.get(
color_category_id,
"white",
))
var selected_color := CharacterCustomizationCatalog.option_color(
color_category_id,
selected_color_id,
)
var channel_button := Button.new()
channel_button.text = CharacterCustomizationCatalog.fur_color_label(
color_category_id
)
channel_button.icon = _fur_swatch_icon(selected_color)
channel_button.icon_alignment = HORIZONTAL_ALIGNMENT_LEFT
channel_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
channel_button.add_theme_constant_override(
"icon_max_width", FUR_CHANNEL_ICON_SIZE
)
channel_button.add_theme_constant_override("h_separation", 8)
channel_button.tooltip_text = "Edit %s: %s" % [
CharacterCustomizationCatalog.fur_color_label(color_category_id),
_fur_option_label(options, selected_color_id),
]
channel_button.toggle_mode = true
channel_button.custom_minimum_size = Vector2(140.0, 34.0)
channel_button.button_pressed = (
_active_fur_color_id == color_category_id
)
channel_button.pressed.connect(
_select_fur_color_slot.bind(color_category_id)
)
UtilityPageStyle.apply_compact_ocean_button(channel_button)
channel_grid.add_child(channel_button)
_build_fur_palette(_active_fur_color_id, options)
func _build_fur_palette(category_id: String, options: Array) -> void:
var selected_id: String = CharacterCustomizationCatalog.canonical_option_id(
category_id,
str(_draft_appearance.get(category_id, "white")),
)
var label := Label.new()
label.text = "%s palette" % CharacterCustomizationCatalog.fur_color_label(
category_id
)
label.add_theme_font_size_override("font_size", 14)
label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
_option_list.add_child(label)
var grid := GridContainer.new()
grid.columns = FUR_PALETTE_GRID_COLUMNS
grid.add_theme_constant_override("h_separation", 6)
grid.add_theme_constant_override("v_separation", 6)
_option_list.add_child(grid)
for option: Dictionary in options:
var option_id: String = str(option.get("id", ""))
var color: Color = CharacterCustomizationCatalog.option_color(
"fur_pattern", option_id
category_id, option_id
)
var button := Button.new()
button.text = ""
button.tooltip_text = str(option.get("label", option_id))
button.tooltip_text = "%s: %s" % [
CharacterCustomizationCatalog.fur_color_label(category_id),
str(option.get("label", option_id)),
]
button.toggle_mode = true
button.custom_minimum_size = Vector2(44.0, 44.0)
button.custom_minimum_size = Vector2(30.0, 30.0)
button.button_pressed = selected_id == option_id
button.pressed.connect(_select_option.bind("fur_pattern", option_id))
button.pressed.connect(_select_option.bind(category_id, option_id))
_apply_fur_swatch_style(button, color, button.button_pressed)
grid.add_child(button)
func _fur_option_label(options: Array, option_id: String) -> String:
for option: Dictionary in options:
if str(option.get("id", "")) == option_id:
return str(option.get("label", option_id))
return option_id
func _fur_swatch_icon(color: Color) -> Texture2D:
var cache_id := color.to_html(true)
if _fur_swatch_icon_cache.has(cache_id):
return _fur_swatch_icon_cache[cache_id] as Texture2D
var image := Image.create(
FUR_CHANNEL_ICON_SIZE,
FUR_CHANNEL_ICON_SIZE,
false,
Image.FORMAT_RGBA8,
)
var center := Vector2.ONE * (float(FUR_CHANNEL_ICON_SIZE) * 0.5)
var radius := float(FUR_CHANNEL_ICON_SIZE) * 0.39
for y: int in range(FUR_CHANNEL_ICON_SIZE):
for x: int in range(FUR_CHANNEL_ICON_SIZE):
var sample_position := Vector2(float(x) + 0.5, float(y) + 0.5)
var alpha := clampf(
radius + 0.8 - sample_position.distance_to(center),
0.0,
1.0,
)
image.set_pixel(
x,
y,
Color(color.r, color.g, color.b, color.a * alpha),
)
var texture := ImageTexture.create_from_image(image)
_fur_swatch_icon_cache[cache_id] = texture
return texture
func _select_fur_color_slot(category_id: String) -> void:
if category_id not in CharacterCustomizationCatalog.FUR_COLOR_IDS:
return
_active_fur_color_id = category_id
_refresh_options()
func _apply_fur_swatch_style(
button: Button,
color: Color,

View file

@ -143,7 +143,7 @@ func _notification(what: int) -> void:
func _rotate(amount: float) -> void:
_camera_yaw = fposmod(_camera_yaw + amount, TAU)
_camera_yaw = fposmod(_camera_yaw - amount, TAU)
_apply_camera_orbit()