270 lines
9.2 KiB
GDScript
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))
|