feat(characters): add modular woodland traveler kit
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
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))
|
||||
Reference in New Issue
Block a user