feat: dm_view — top-down map, floor switch, player markers, cam feeds, overlay toggles

This commit is contained in:
2026-04-14 03:13:14 +02:00
parent f847facd9e
commit 30f5d57640
2 changed files with 275 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/dm_view.gd" id="1"]
[node name="DmView" type="Node"]
script = ExtResource("1")
[node name="RootLayout" type="VBoxContainer" parent="."]
[node name="TopSection" type="HSplitContainer" parent="RootLayout"]
size_flags_vertical = 3
[node name="MapContainer" type="SubViewportContainer" parent="RootLayout/TopSection"]
custom_minimum_size = Vector2(700, 500)
size_flags_horizontal = 3
stretch = true
[node name="MapViewport" type="SubViewport" parent="RootLayout/TopSection/MapContainer"]
size = Vector2i(700, 500)
[node name="MapRoot" type="Node3D" parent="RootLayout/TopSection/MapContainer/MapViewport"]
[node name="PlayerMarkers" type="Node3D" parent="RootLayout/TopSection/MapContainer/MapViewport/MapRoot"]
[node name="TopDownCam" type="Camera3D" parent="RootLayout/TopSection/MapContainer/MapViewport/MapRoot"]
transform = Transform3D(1, 0, 0, 0, 0, -1, 0, 1, 0, 2, 25, 0)
current = true
[node name="SidePanel" type="VBoxContainer" parent="RootLayout/TopSection"]
custom_minimum_size = Vector2(220, 0)
[node name="LblFloor" type="Label" parent="RootLayout/TopSection/SidePanel"]
text = "Etage"
[node name="FloorButtons" type="HBoxContainer" parent="RootLayout/TopSection/SidePanel"]
[node name="BtnEG" type="Button" parent="RootLayout/TopSection/SidePanel/FloorButtons"]
text = "Erdgeschoss"
toggle_mode = true
button_pressed = true
[node name="BtnOG" type="Button" parent="RootLayout/TopSection/SidePanel/FloorButtons"]
text = "Obergeschoss"
toggle_mode = true
[node name="HSeparator" type="HSeparator" parent="RootLayout/TopSection/SidePanel"]
[node name="LblOverlay" type="Label" parent="RootLayout/TopSection/SidePanel"]
text = "Overlays"
[node name="OverlayScroll" type="ScrollContainer" parent="RootLayout/TopSection/SidePanel"]
size_flags_vertical = 3
[node name="PlayerList" type="VBoxContainer" parent="RootLayout/TopSection/SidePanel/OverlayScroll"]
[node name="HSeparator2" type="HSeparator" parent="RootLayout"]
[node name="PlayerCamsRow" type="HBoxContainer" parent="RootLayout"]
custom_minimum_size = Vector2(0, 200)

View File

@@ -0,0 +1,216 @@
extends Node
const TAVERN_SCENE := "res://scenes/tavern.tscn"
const CAM_EG_Y := 25.0
const CAM_OG_Y := 30.0
const OVERLAY_CYCLE := ["default", "spore_active"]
var _top_down_cam: Camera3D
var _player_markers: Node3D
var _markers_by_id: Dictionary = {} # peer_id → MeshInstance3D
var _player_cam_cams: Dictionary = {} # peer_id → Camera3D (in SubViewport)
var _showing_upper_floor: bool = false
func _ready() -> void:
$RootLayout.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_load_tavern_into_viewport()
_setup_floor_buttons()
_setup_player_cams()
_setup_overlay_panel()
func _load_tavern_into_viewport() -> void:
var packed := ResourceLoader.load(TAVERN_SCENE, "PackedScene") as PackedScene
if packed == null:
push_error("[DmView] tavern.tscn nicht gefunden — wird nach Schritt 5 verfügbar sein")
return
var map_root := $RootLayout/TopSection/MapContainer/MapViewport/MapRoot as Node3D
_top_down_cam = map_root.get_node("TopDownCam") as Camera3D
_player_markers = map_root.get_node("PlayerMarkers") as Node3D
var tavern := packed.instantiate()
tavern.name = "Tavern"
map_root.add_child(tavern)
print("[DmView] Tavern in SubViewport geladen")
func _setup_floor_buttons() -> void:
var btn_eg := $RootLayout/TopSection/SidePanel/FloorButtons/BtnEG as Button
var btn_og := $RootLayout/TopSection/SidePanel/FloorButtons/BtnOG as Button
btn_eg.pressed.connect(func() -> void: _switch_floor(false))
btn_og.pressed.connect(func() -> void: _switch_floor(true))
func _switch_floor(upper: bool) -> void:
_showing_upper_floor = upper
if _top_down_cam == null:
return
_top_down_cam.global_position.y = CAM_OG_Y if upper else CAM_EG_Y
print("[DmView] Etage gewechselt → %s" % ("OG" if upper else "EG"))
# --- Player Cam Feeds (Task 7) ---
func _setup_player_cams() -> void:
NetworkManager.player_joined.connect(_on_player_joined_cam)
NetworkManager.player_left.connect(_on_player_left_cam)
for peer_id in NetworkManager.players.keys():
var info: Dictionary = NetworkManager.players[peer_id]
if info.get("role", "") == "player":
_create_player_cam(peer_id, info.get("name", "???"))
func _on_player_joined_cam(peer_id: int, player_name: String, role: String) -> void:
if role == "player":
_create_player_cam(peer_id, player_name)
func _on_player_left_cam(peer_id: int) -> void:
var row := $RootLayout/PlayerCamsRow
var panel := row.get_node_or_null("CamPanel_%d" % peer_id)
if panel != null:
panel.queue_free()
_player_cam_cams.erase(peer_id)
func _create_player_cam(peer_id: int, player_name: String) -> void:
var packed := ResourceLoader.load(TAVERN_SCENE, "PackedScene") as PackedScene
if packed == null:
push_error("[DmView] tavern.tscn nicht gefunden für Player-Cam")
return
var viewport := SubViewport.new()
viewport.size = Vector2i(320, 180)
var scene_root := Node3D.new()
var tavern := packed.instantiate()
scene_root.add_child(tavern)
var cam := Camera3D.new()
cam.name = "PlayerCam"
cam.current = true
scene_root.add_child(cam)
viewport.add_child(scene_root)
var container := SubViewportContainer.new()
container.name = "CamPanel_%d" % peer_id
container.custom_minimum_size = Vector2(320, 180)
container.stretch = true
container.add_child(viewport)
var wrapper := VBoxContainer.new()
var label := Label.new()
label.text = player_name
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
wrapper.add_child(label)
wrapper.add_child(container)
$RootLayout/PlayerCamsRow.add_child(wrapper)
_player_cam_cams[peer_id] = cam
print("[DmView] Player-Cam erstellt für %s (Peer %d)" % [player_name, peer_id])
# --- Overlay Panel (Task 8) ---
func _setup_overlay_panel() -> void:
NetworkManager.player_joined.connect(_on_player_joined)
NetworkManager.player_left.connect(_on_player_left)
for peer_id in NetworkManager.players.keys():
var info: Dictionary = NetworkManager.players[peer_id]
if info.get("role", "") == "player":
_add_overlay_row(peer_id, info.get("name", "???"))
func _on_player_joined(peer_id: int, player_name: String, role: String) -> void:
if role == "player":
_add_overlay_row(peer_id, player_name)
func _on_player_left(peer_id: int) -> void:
var list := $RootLayout/TopSection/SidePanel/OverlayScroll/PlayerList as VBoxContainer
var row := list.get_node_or_null("Row_%d" % peer_id)
if row != null:
row.queue_free()
func _add_overlay_row(peer_id: int, player_name: String) -> void:
var list := $RootLayout/TopSection/SidePanel/OverlayScroll/PlayerList as VBoxContainer
var row := HBoxContainer.new()
row.name = "Row_%d" % peer_id
var label := Label.new()
label.text = player_name
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var btn := Button.new()
btn.text = "● default"
btn.pressed.connect(func() -> void: _toggle_overlay(peer_id, btn))
row.add_child(label)
row.add_child(btn)
list.add_child(row)
func _toggle_overlay(peer_id: int, btn: Button) -> void:
var current := GameState.get_overlay(peer_id)
var idx := OVERLAY_CYCLE.find(current)
var next: String = OVERLAY_CYCLE[(idx + 1) % OVERLAY_CYCLE.size()]
NetworkManager.request_set_overlay.rpc_id(1, peer_id, next)
GameState.set_overlay_local(peer_id, next)
btn.text = "%s" % next
print("[DmView] Overlay-Toggle → Spieler %d: %s" % [peer_id, next])
# --- _process: Marker + Cam updates ---
func _process(_delta: float) -> void:
_update_player_markers()
_update_player_cams()
func _update_player_markers() -> void:
if _player_markers == null:
return
for peer_id in GameState.player_positions.keys():
var data: Dictionary = GameState.player_positions[peer_id]
var pos: Vector3 = data.get("position", Vector3.ZERO)
var rot: Vector3 = data.get("rotation", Vector3.ZERO)
var marker := _get_or_create_marker(peer_id)
marker.global_position = pos + Vector3(0, 0.2, 0)
marker.rotation.y = rot.y
var is_upper := pos.y > 3.5
marker.visible = (is_upper == _showing_upper_floor)
func _get_or_create_marker(peer_id: int) -> MeshInstance3D:
if peer_id in _markers_by_id:
return _markers_by_id[peer_id]
var mesh := MeshInstance3D.new()
var sphere := SphereMesh.new()
sphere.radius = 0.25
sphere.height = 0.5
mesh.mesh = sphere
var mat := StandardMaterial3D.new()
mat.albedo_color = _peer_color(peer_id)
mat.emission_enabled = true
mat.emission = mat.albedo_color
mat.emission_energy_multiplier = 0.5
mesh.material_override = mat
_player_markers.add_child(mesh)
_markers_by_id[peer_id] = mesh
print("[DmView] Marker erstellt für Peer %d" % peer_id)
return mesh
func _peer_color(peer_id: int) -> Color:
var colors := [Color.RED, Color.CYAN, Color.YELLOW, Color.MAGENTA, Color.GREEN]
return colors[peer_id % colors.size()]
func _update_player_cams() -> void:
for peer_id in _player_cam_cams.keys():
if not GameState.player_positions.has(peer_id):
continue
var data: Dictionary = GameState.player_positions[peer_id]
var pos: Vector3 = data.get("position", Vector3.ZERO)
var rot: Vector3 = data.get("rotation", Vector3.ZERO)
var cam: Camera3D = _player_cam_cams[peer_id]
cam.global_position = pos
cam.global_rotation = rot