saumwacht-godot/scripts/ui/model_inspector.gd
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

264 lines
11 KiB
GDScript

extends ColorRect
## In-game close-up of the actual animated model; the match pauses underneath.
signal closed
var _previous_pause: bool = false
var _view: SubViewport
var _root: Node3D
var _turntable: Node3D
var _figure: Node3D
var _camera: Camera3D
var _time: float = 0
var _walking: bool = false
var _attacking: bool = false
var _rotate: bool = true
var _close_button: Button
var _caption: Label
var _details: Label
var _previous_focus: Control
var _selector: OptionButton
var _rotate_button: CheckButton
var model_id: String = ""
var _legacy_buttons: Array[Control] = []
var _clip_row: HBoxContainer
var _clip_selector: OptionButton
var _clip_time: float = 0.0
var _clip_paused: bool = false
var _clip_slider: HSlider
var _clip_readout: Label
const CLIP_LABELS = {"idle":"Ruhe", "start":"Anlaufen", "walk":"Laufen", "stop":"Anhalten", "attack_a":"Angriff links", "attack_b":"Angriff rechts", "hit":"Treffer", "ability":"Fähigkeit", "death":"Tod"}
const WORKER_CLIP_LABELS = {"idle":"Ruhe", "start":"Anlaufen", "walk":"Tragen", "stop":"Anhalten", "strike":"Werkzeugkontakt", "pickup":"Aufnehmen", "deposit":"Einfüllen", "return":"Rückkehr", "hit":"Treffer", "death":"Tod"}
const RINGBORKE_CLIP_LABELS = {"idle":"Ruhe", "start":"Anlaufen", "walk":"Laufen", "stop":"Anhalten", "attack_a":"Schildstoß links", "attack_b":"Hieb rechts", "hit":"Treffer", "ability":"Heilung", "death":"Tod"}
const BASALT_CLIP_LABELS = {"idle":"Ruhe", "start":"Anlaufen", "walk":"Laufen", "stop":"Anhalten", "attack_a":"Schlag links", "attack_b":"Schlag rechts", "hit":"Treffer", "ability":"Schutzhaltung", "death":"Tod"}
const QUELL_CLIP_LABELS = {"idle":"Ruhe", "start":"Anlaufen", "walk":"Laufen", "stop":"Anhalten", "attack_a":"Ranke links", "attack_b":"Ranke rechts", "hit":"Treffer", "ability":"Mehrheilung", "death":"Tod"}
const FUNKEN_CLIP_LABELS = {"idle":"Ruhe", "start":"Anlaufen", "walk":"Laufen", "stop":"Anhalten", "attack_a":"Freier Schuss", "attack_b":"Angelegter Schuss", "hit":"Treffer", "ability":"Durchschlagschuss", "death":"Tod"}
const GLASS_CLIP_LABELS = {"idle":"Ruhe", "start":"Anlaufen", "walk":"Laufen", "stop":"Anhalten", "attack_a":"Freier Schuss", "attack_b":"Angelegter Schuss", "hit":"Treffer", "ability":"Panzerbrecherschuss", "death":"Tod"}
func _ready() -> void:
color = Color(.015, .025, .04, 1)
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var margin := MarginContainer.new()
margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
for side: String in ["left","right","top","bottom"]:
margin.add_theme_constant_override("margin_"+side,32)
add_child(margin)
var layout := VBoxContainer.new()
margin.add_child(layout)
var header := HBoxContainer.new()
layout.add_child(header)
_caption = Label.new()
_caption.add_theme_font_size_override("font_size",28)
_caption.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(_caption)
_selector = OptionButton.new()
for id: String in DataCatalog.UNITS:
_selector.add_item(str(DataCatalog.UNITS[id].display_name))
_selector.set_item_metadata(_selector.item_count-1,id)
_selector.add_item("Funkenleser")
_selector.set_item_metadata(_selector.item_count-1,"worker")
_selector.item_selected.connect(func(index: int): _select_model(str(_selector.get_item_metadata(index))))
header.add_child(_selector)
var close := Button.new()
_close_button = close
close.text = "ZURÜCK [Esc / V]"
close.pressed.connect(hide_model)
header.add_child(close)
_details = Label.new()
layout.add_child(_details)
var container := SubViewportContainer.new()
container.stretch = true
container.size_flags_vertical = Control.SIZE_EXPAND_FILL
container.mouse_filter = Control.MOUSE_FILTER_STOP
container.gui_input.connect(_drag)
layout.add_child(container)
_view = SubViewport.new()
_view.own_world_3d = true
_view.msaa_3d = Viewport.MSAA_4X
_view.render_target_update_mode = SubViewport.UPDATE_DISABLED
container.add_child(_view)
_root = Node3D.new()
_view.add_child(_root)
var environment := WorldEnvironment.new()
environment.environment = Environment.new()
preload("res://scripts/presentation/lighting.gd").configure(environment.environment,true)
_root.add_child(environment)
for entry: Array in [[Vector3(-40,-30,0),Color("fff0dc"),1.35],[Vector3(-30,140,0),Color("bdcee8"),.42]]:
var light := DirectionalLight3D.new()
light.rotation_degrees = entry[0]
light.light_color = entry[1]
light.light_energy = entry[2]
light.shadow_enabled = entry[2] > 1.0
light.light_angular_distance = 1.5
_root.add_child(light)
_turntable = Node3D.new()
_root.add_child(_turntable)
var pedestal := MeshInstance3D.new()
var disc := CylinderMesh.new()
disc.top_radius = 1.12
disc.bottom_radius = 1.16
disc.height = .075
disc.radial_segments = 64
var stone := StandardMaterial3D.new()
stone.albedo_color = Color("1a2430")
stone.roughness = .55
stone.metallic = .25
disc.material = stone
pedestal.mesh = disc
pedestal.position.y = -.04
_root.add_child(pedestal)
var rim := MeshInstance3D.new()
var rim_mesh := TorusMesh.new()
rim_mesh.inner_radius = 1.05
rim_mesh.outer_radius = 1.10
rim_mesh.rings = 48
rim_mesh.ring_segments = 4
var glow := StandardMaterial3D.new()
glow.albedo_color = Color("7fe8c8")
glow.emission_enabled = true
glow.emission = Color("4ec9a8")
glow.emission_energy_multiplier = 1.8
glow.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
rim.mesh = rim_mesh
rim.material_override = glow
rim.position.y = -.005
_root.add_child(rim)
_camera = Camera3D.new()
_camera.projection = Camera3D.PROJECTION_ORTHOGONAL
_camera.size = 2.8
_camera.position = Vector3(3,2.0,4.8)
_root.add_child(_camera)
_camera.look_at(Vector3(0,.85,0),Vector3.UP)
var row := HBoxContainer.new()
row.alignment = BoxContainer.ALIGNMENT_CENTER
layout.add_child(row)
for title: String in ["Automatisch drehen","Gehen","Angreifen"]:
var button := CheckButton.new()
button.text = title
row.add_child(button)
if title == "Automatisch drehen":
_rotate_button = button
button.button_pressed = true
button.toggled.connect(func(value: bool): _rotate = value)
elif title == "Gehen":
_legacy_buttons.append(button)
button.toggled.connect(func(value: bool): _walking = value)
else:
_legacy_buttons.append(button)
button.toggled.connect(func(value: bool): _attacking = value)
_clip_row = HBoxContainer.new()
_clip_row.alignment = BoxContainer.ALIGNMENT_CENTER
layout.add_child(_clip_row)
_clip_selector = OptionButton.new()
for clip: String in CLIP_LABELS:
_clip_selector.add_item(CLIP_LABELS[clip])
_clip_selector.set_item_metadata(_clip_selector.item_count-1,clip)
_clip_selector.item_selected.connect(func(_index: int): _clip_time = 0.0)
_clip_row.add_child(_clip_selector)
var pause := CheckButton.new()
pause.text = "Animation pausieren"
pause.toggled.connect(func(value: bool): _clip_paused = value)
_clip_row.add_child(pause)
var restart := Button.new()
restart.text = "Von vorn"
restart.pressed.connect(func(): _clip_time = 0.0)
_clip_row.add_child(restart)
_clip_slider = HSlider.new()
_clip_slider.custom_minimum_size.x = 180
_clip_slider.min_value = 0
_clip_slider.max_value = 1
_clip_slider.step = .001
_clip_slider.value_changed.connect(func(value: float):
if is_instance_valid(_figure) and _figure.has_method("clip_length"):
_clip_time = value*_figure.clip_length(str(_clip_selector.get_selected_metadata())))
_clip_row.add_child(_clip_slider)
_clip_readout = Label.new()
_clip_readout.custom_minimum_size.x = 120
_clip_row.add_child(_clip_readout)
var hint := Label.new()
hint.text = "Ziehen: drehen · Mausrad: vergrößern · Die laufende Partie ist pausiert."
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
layout.add_child(hint)
hide()
func show_model(id: String) -> void:
if visible or not (DataCatalog.UNITS.has(id) or id == "worker"):
return
_previous_pause = GameState.paused
_previous_focus = get_viewport().gui_get_focus_owner()
GameState.paused = true
Soundscape.stop_combat()
_select_model(id)
_view.render_target_update_mode = SubViewport.UPDATE_ALWAYS
show()
_close_button.grab_focus()
func _select_model(id: String) -> void:
model_id = id
for index: int in range(_selector.item_count):
if _selector.get_item_metadata(index) == id:
_selector.select(index)
if is_instance_valid(_figure):
_figure.free()
_figure = AssetLibrary.instantiate_fighter(id)
_turntable.add_child(_figure)
var bounds: AABB = _figure.get_aabb()
var factor: float = 1.8/maxf(bounds.size.y,bounds.size.x*.80)
_figure.scale = Vector3.ONE*factor
_figure.position = -Vector3(bounds.get_center().x,bounds.position.y,bounds.get_center().z)*factor
_turntable.rotation.y = 0
_camera.size = 2.8
_time = 0
_caption.text = "Funkenleser" if id == "worker" else str(DataCatalog.UNITS[id].display_name)
var record: Dictionary = AssetLibrary._models[id]
_details.text = "%s · %d Dreiecke · Tatsächliches Spielmodell" % [id.to_upper(),int(record.triangles)]
var skeletal: bool = record.get("skeletal",false)
_clip_row.visible = skeletal
for button: Control in _legacy_buttons:
button.visible = not skeletal
_clip_time = 0.0
_clip_selector.clear()
var labels: Dictionary = WORKER_CLIP_LABELS if id == "worker" else (RINGBORKE_CLIP_LABELS if id == "w04" else (BASALT_CLIP_LABELS if id == "g04" else (QUELL_CLIP_LABELS if id == "w10" else (FUNKEN_CLIP_LABELS if id == "g05" else (GLASS_CLIP_LABELS if id == "g06" else CLIP_LABELS)))))
for clip: String in labels:
_clip_selector.add_item(labels[clip])
_clip_selector.set_item_metadata(_clip_selector.item_count-1,clip)
_clip_selector.select(0)
if skeletal:
var clip_count: int = int(record.get("clips", labels.keys()).size()) if record.has("clips") else labels.size()
_details.text += " · %d Knochen · %d Bewegungen" % [int(record.bones), clip_count]
func hide_model() -> void:
if not visible:
return
hide()
_view.render_target_update_mode = SubViewport.UPDATE_DISABLED
GameState.paused = _previous_pause
if is_instance_valid(_previous_focus):
_previous_focus.grab_focus()
closed.emit()
func _process(delta: float) -> void:
if not visible:
return
_time += delta
if _rotate:
_turntable.rotation.y += delta*.28
if _figure.has_method("preview"):
var clip: String = str(_clip_selector.get_selected_metadata())
var duration: float = _figure.clip_length(clip)
if not _clip_paused:
_clip_time += delta
if _clip_time > duration + .55:
_clip_time = 0.0
_figure.preview(clip,minf(_clip_time,duration))
_clip_slider.set_value_no_signal(minf(_clip_time/duration,1.0))
_clip_readout.text = "%.2f / %.2f s" % [minf(_clip_time,duration),duration]
elif _figure.has_method("pose"):
_figure.pose(_time,_walking,maxf(0,.28-fmod(_time,1.25)) if _attacking else 0.0,0,0)
func _drag(event: InputEvent) -> void:
if event is InputEventMouseMotion and event.button_mask & MOUSE_BUTTON_MASK_LEFT:
_turntable.rotation.y += event.relative.x*.012
if event is InputEventMouseButton and event.pressed:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
_camera.size = maxf(1.8,_camera.size-.15)
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_camera.size = minf(4.2,_camera.size+.15)