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>
264 lines
11 KiB
GDScript
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)
|