Files
2026-09-05 22:54:54 +02:00

270 lines
9.2 KiB
GDScript

extends Node3D
## Run CharacterWorkshop.tscn to combine parts, inspect motion, and save authored profiles.
const PRESETS: Array[String] = [
"reference_cat",
"fox_rambler",
"rabbit_scholar",
"badger_baker",
"otter_courier",
"lop_gardener"
]
const PROFILE_DIR := "res://assets/characters/profiles/"
var profile: CharacterAppearanceProfile
var actor: Node3D
var camera: Camera3D
var controls: VBoxContainer
var ui: CanvasLayer
var walk := false
var phase := 0.0
var turn := -0.32
var dragging := false
var status: Label
func _ready() -> void:
_build_stage()
_build_ui()
_select_preset(0)
func _process(delta: float) -> void:
actor.rotation.y = turn
phase += delta * 5.0
var swing := sin(phase) if walk else 0.0
for side in ["Left", "Right"]:
var direction := -1.0 if side == "Left" else 1.0
actor.get_node("Leg" + side).rotation.x = swing * direction * 0.44
actor.get_node("Arm" + side).rotation.x = (
-swing * direction * 0.30 * float(actor.get_meta("arm_swing", 1.0))
)
var rest := AnimalAppearance.rest_heights(actor)
var bob := absf(sin(phase * 2.0)) * 0.035 if walk else 0.0
actor.get_node("Body").position.y = rest.x + bob
actor.get_node("Head").position.y = rest.y + bob
actor.get_node("Hair").position.y = rest.z + bob
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
dragging = event.pressed
if event is InputEventMouseMotion and dragging:
turn += event.relative.x * 0.008
if event is InputEventMouseButton and event.pressed:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
camera.size = maxf(2.8, camera.size - 0.2)
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
camera.size = minf(6.0, camera.size + 0.2)
func _build_stage() -> void:
var environment := WorldEnvironment.new()
var env := Environment.new()
environment.environment = env
env.background_mode = Environment.BG_COLOR
env.background_color = Color("e2e4d8")
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
env.ambient_light_color = Color("d0ddd8")
env.ambient_light_energy = 0.38
env.tonemap_mode = Environment.TONE_MAPPER_FILMIC
# Calibrated for this preview stage to retain cream fabric highlights on GLES.
if RenderingServer.get_current_rendering_method() == "gl_compatibility":
env.tonemap_exposure = 0.25
add_child(environment)
var light := DirectionalLight3D.new()
light.rotation_degrees = Vector3(-38, -32, 0)
light.light_color = Color("fff6e8")
light.light_energy = 1.1
light.shadow_enabled = true
add_child(light)
var floor_mesh := MeshInstance3D.new()
var plane := PlaneMesh.new()
plane.size = Vector2(200, 200)
var material := StandardMaterial3D.new()
material.albedo_color = Color("d0d6c3")
material.roughness = 1.0
plane.material = material
floor_mesh.mesh = plane
add_child(floor_mesh)
actor = load("res://player/PlayerVisual.tscn").instantiate()
add_child(actor)
actor.set_process(false)
actor.get_node("Sword").hide()
actor.position.x = 0.80
camera = Camera3D.new()
add_child(camera)
camera.position = Vector3(0, 2.45, 7)
camera.look_at(Vector3(0, 1.36, 0))
camera.projection = Camera3D.PROJECTION_ORTHOGONAL
camera.size = 3.9
camera.current = true
get_viewport().msaa_3d = Viewport.MSAA_4X
func _build_ui() -> void:
ui = CanvasLayer.new()
add_child(ui)
var panel := PanelContainer.new()
panel.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
panel.offset_right = 320
var theme := Theme.new()
theme.default_font_size = 15
theme.set_color("font_color", "Label", Color("37433a"))
theme.set_color("font_color", "CheckButton", Color("37433a"))
theme.set_color("font_pressed_color", "CheckButton", Color("37433a"))
theme.set_color("font_hover_color", "CheckButton", Color("37433a"))
theme.set_color("font_hover_pressed_color", "CheckButton", Color("37433a"))
for type in ["Button", "OptionButton"]:
for state in ["normal", "hover", "pressed", "focus"]:
var button_style := StyleBoxFlat.new()
button_style.bg_color = Color("d6dccd") if state == "hover" else Color("e2e4da")
button_style.content_margin_left = 8
button_style.content_margin_right = 12
button_style.content_margin_top = 6
button_style.content_margin_bottom = 6
theme.set_stylebox(state, type, button_style)
for state in ["font_color", "font_hover_color", "font_pressed_color", "font_focus_color"]:
theme.set_color(state, type, Color("37433a"))
panel.theme = theme
var paper := StyleBoxFlat.new()
paper.bg_color = Color("f2f0e7")
paper.content_margin_left = 24
paper.content_margin_right = 24
paper.content_margin_top = 24
paper.content_margin_bottom = 20
panel.add_theme_stylebox_override("panel", paper)
ui.add_child(panel)
var scroll := ScrollContainer.new()
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
panel.add_child(scroll)
controls = VBoxContainer.new()
controls.size_flags_horizontal = Control.SIZE_EXPAND_FILL
controls.add_theme_constant_override("separation", 10)
scroll.add_child(controls)
func _select_preset(index: int) -> void:
profile = load(PROFILE_DIR + PRESETS[index] + ".tres").duplicate()
_rebuild_controls(index)
_apply()
func _rebuild_controls(selected: int) -> void:
for child in controls.get_children():
controls.remove_child(child)
child.queue_free()
_label("THE WARDROBE", 23)
_label("Woodland travelers", 16)
var presets := OptionButton.new()
for preset in PRESETS:
presets.add_item(preset.replace("_", " ").capitalize())
presets.selected = selected
presets.item_selected.connect(_select_preset)
controls.add_child(presets)
_select("Species", "species", ["Cat", "Fox", "Rabbit", "Badger", "Otter"])
_slider("Body fullness", "body_fullness", 0.0, 1.0)
_slider("Height", "height", 0.80, 1.15)
_slider("Head size", "head_size", 0.88, 1.12)
_slider("Leg length", "leg_length", 0.85, 1.15)
_slider("Ear length", "ear_length", 0.65, 1.20)
_select("Ears", "ears", ["Species default", "Cat", "Fox", "Rabbit", "Lop", "Badger", "Otter"])
_select("Clothes", "outfit", ["Vest", "Long coat", "Work tunic"])
_select("Arms", "arm_pose", ["Relaxed", "Holding pack straps"])
_select("Bag", "bag", ["None", "Satchel", "Travel pack"])
_select("Hat", "hat", ["None", "Beret", "Brimmed hat"])
for field in ["scarf", "spectacles"]:
var toggle := CheckButton.new()
toggle.text = field.capitalize()
toggle.button_pressed = profile.get(field)
toggle.toggled.connect(func(value: bool) -> void: _change(field, value))
controls.add_child(toggle)
for field in ["fur_color", "outfit_color", "scarf_color", "trousers_color"]:
var row := HBoxContainer.new()
controls.add_child(row)
var label := Label.new()
label.text = field.trim_suffix("_color").capitalize()
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(label)
var color := ColorPickerButton.new()
color.custom_minimum_size = Vector2(72, 28)
color.edit_alpha = false
color.color = profile.get(field)
color.color_changed.connect(func(value: Color) -> void: _change(field, value))
row.add_child(color)
var motion := CheckButton.new()
motion.text = "Walk preview"
motion.button_pressed = walk
motion.toggled.connect(func(value: bool) -> void: walk = value)
controls.add_child(motion)
var save := Button.new()
save.text = "Save profile…"
save.pressed.connect(_save_profile)
controls.add_child(save)
status = _label("Drag to turn · scroll to zoom", 13)
func _label(text: String, size: int) -> Label:
var label := Label.new()
label.text = text
label.add_theme_font_size_override("font_size", size)
controls.add_child(label)
return label
func _select(title: String, field: String, options: Array) -> void:
var row := HBoxContainer.new()
controls.add_child(row)
var label := Label.new()
label.text = title
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(label)
var select := OptionButton.new()
for option: String in options:
select.add_item(option)
select.selected = profile.get(field)
select.item_selected.connect(func(value: int) -> void: _change(field, value))
row.add_child(select)
func _slider(title: String, field: String, minimum: float, maximum: float) -> void:
var label := _label(title + " %.2f" % float(profile.get(field)), 14)
var slider := HSlider.new()
slider.min_value = minimum
slider.max_value = maximum
slider.step = 0.01
slider.value = profile.get(field)
slider.value_changed.connect(
func(value: float) -> void:
label.text = title + " %.2f" % value
_change(field, value)
)
controls.add_child(slider)
func _change(field: String, value: Variant) -> void:
profile.set(field, value)
_apply()
func _apply() -> void:
AnimalAppearance.apply_to(actor, 0, false, profile)
func _save_profile() -> void:
var dialog := FileDialog.new()
dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
dialog.access = FileDialog.ACCESS_FILESYSTEM
dialog.filters = PackedStringArray(["*.tres ; Character appearance"])
dialog.current_dir = ProjectSettings.globalize_path(PROFILE_DIR)
dialog.current_file = "custom_traveler.tres"
dialog.file_selected.connect(
func(path: String) -> void:
var error := ResourceSaver.save(profile, path)
status.text = "Profile saved." if error == OK else "Could not save profile."
dialog.queue_free()
)
dialog.canceled.connect(dialog.queue_free)
ui.add_child(dialog)
dialog.popup_centered(Vector2i(720, 500))