# World.gd # ───────────────────────────────────────────────────────────────────────────── # Spielwelt-Controller # # Verantwortlichkeiten: # • Overworld + Dungeon in einer Szene (kein Szenenwechsel) # • Prozeduraler Boden-Shader, Himmel # • Hauptmenü → Klassenauswahl → Spielinitialisierung # • Startausrüstung je nach Klasse # • Gegner-Setup, Loot-Weiterleitung, Respawn nach Tod # • Dungeon-Generierung und Portal-System # ───────────────────────────────────────────────────────────────────────────── extends Node3D const ENEMY_SCENE = preload("res://enemy.tscn") const MAIN_MENU = preload("res://main_menu.tscn") const CLASS_SELECTION_MENU = preload("res://class_selection_menu.tscn") const RESPAWN_TIME = 60.0 # Startausrüstung const STARTER_SWORD = preload("res://equipment/iron_sword.tres") const STARTER_STAFF = preload("res://equipment/wooden_staff.tres") const STARTER_CHEST = preload("res://equipment/leather_chest.tres") const STARTER_POTION = preload("res://consumables/small_hp_potion.tres") # Loot Tables const GOBLIN_LOOT = preload("res://loot_tables/goblin_loot.tres") # Dungeon-Konstanten const TILE_SIZE = 3.0 const WALL_HEIGHT = 6.0 const GRID_WIDTH = 60 const GRID_HEIGHT = 60 const MIN_ROOMS = 6 const MAX_ROOMS = 10 const MIN_ROOM_SIZE = 4 const MAX_ROOM_SIZE = 9 @onready var player = $Player @onready var floor_mesh = $Boden/MeshInstance3D @onready var gate_area = $DungeonGate/GateArea @onready var gate_label = $DungeonGate/GateLabel var in_dungeon: bool = false var dungeon_level: int = 0 # 0 = Overworld, 1+ = Dungeon-Ebene var overworld_nodes: Array = [] # Nodes die im Dungeon versteckt werden var dungeon_container: Node3D = null var dungeon_env: WorldEnvironment = null var overworld_env: WorldEnvironment = null var return_portal_area: Area3D = null var return_portal_label: Label3D = null var deeper_portal_area: Area3D = null var deeper_portal_label: Label3D = null var player_overworld_pos: Vector3 = Vector3.ZERO var portal_choice_panel: PanelContainer = null var overworld_env_resource: Environment = null # Dungeon-Daten var dungeon_grid: Array = [] var dungeon_rooms: Array = [] # Gespeicherte Dungeon-Ebenen (Level → {grid, rooms}) var saved_dungeons: Dictionary = {} func _ready(): _setup_floor_material() _setup_sky() # Hauptmenü anzeigen var main_menu = MAIN_MENU.instantiate() add_child(main_menu) main_menu.start_game.connect(_on_start_game) func _setup_sky(): var sky_mat = ProceduralSkyMaterial.new() sky_mat.sky_top_color = Color(0.15, 0.35, 0.75) sky_mat.sky_horizon_color = Color(0.55, 0.75, 1.0) sky_mat.ground_horizon_color = Color(0.35, 0.30, 0.25) sky_mat.ground_bottom_color = Color(0.1, 0.1, 0.1) sky_mat.sun_angle_max = 30.0 sky_mat.sun_curve = 0.15 var sky = Sky.new() sky.sky_material = sky_mat var env = Environment.new() env.background_mode = Environment.BG_SKY env.sky = sky env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY env.ambient_light_energy = 0.6 env.tonemap_mode = Environment.TONE_MAPPER_FILMIC overworld_env = WorldEnvironment.new() overworld_env.name = "OverworldEnv" overworld_env.environment = env add_child(overworld_env) func _setup_floor_material(): var shader = Shader.new() shader.code = """ shader_type spatial; uniform vec4 grass_color_a : source_color = vec4(0.18, 0.42, 0.12, 1.0); uniform vec4 grass_color_b : source_color = vec4(0.22, 0.50, 0.15, 1.0); uniform vec4 dirt_color : source_color = vec4(0.35, 0.25, 0.15, 1.0); uniform float noise_scale : hint_range(0.01, 0.5) = 0.08; uniform float dirt_threshold : hint_range(0.0, 1.0) = 0.72; float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f); float a = hash(i); float b = hash(i + vec2(1.0, 0.0)); float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0)); return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); } void fragment() { vec3 world_pos = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz; vec2 uv = world_pos.xz * noise_scale; float n1 = noise(uv * 3.0); float n2 = noise(uv * 7.0 + vec2(50.0)); float n3 = noise(uv * 15.0 + vec2(100.0)); float combined = n1 * 0.5 + n2 * 0.35 + n3 * 0.15; vec3 grass = mix(grass_color_a.rgb, grass_color_b.rgb, n2); vec3 col = mix(grass, dirt_color.rgb, step(dirt_threshold, combined)); ALBEDO = col; ROUGHNESS = 0.92; METALLIC = 0.0; NORMAL_MAP = vec3(n2 * 0.3, n3 * 0.3, 1.0); } """ var mat = ShaderMaterial.new() mat.shader = shader floor_mesh.material_override = mat # ───────────────────────────────────────────────────────────────────────────── # PROCESS # ───────────────────────────────────────────────────────────────────────────── func _process(_delta): if in_dungeon: _check_return_portal() _check_deeper_portal() else: _check_gate_proximity() func _check_gate_proximity(): if not player or not gate_area or not gate_label: return var dist = player.global_position.distance_to(gate_area.global_position) gate_label.visible = dist < 6.0 if dist < 6.0 and Input.is_action_just_pressed("interact"): _enter_dungeon() func _check_return_portal(): if not player or not return_portal_area or not return_portal_label: return if portal_choice_panel and is_instance_valid(portal_choice_panel): return # Menü ist offen var dist = player.global_position.distance_to(return_portal_area.global_position) return_portal_label.visible = dist < 6.0 if dist < 6.0 and Input.is_action_just_pressed("interact"): if dungeon_level <= 1: _exit_dungeon() else: _show_portal_choice() func _check_deeper_portal(): if not player or not deeper_portal_area or not deeper_portal_label: return var dist = player.global_position.distance_to(deeper_portal_area.global_position) deeper_portal_label.visible = dist < 6.0 if dist < 6.0 and Input.is_action_just_pressed("interact"): _go_deeper() # ───────────────────────────────────────────────────────────────────────────── # DUNGEON BETRETEN / VERLASSEN # ───────────────────────────────────────────────────────────────────────────── func _enter_dungeon(): in_dungeon = true dungeon_level = 1 player_overworld_pos = player.global_position # Overworld-Nodes verstecken _hide_overworld() # Ersten Dungeon generieren _generate_dungeon_level() func _go_deeper(): # Aktuelle Ebene speichern _save_current_dungeon() _clear_dungeon() dungeon_level += 1 _generate_dungeon_level() func _go_up(): # Aktuelle Ebene speichern _save_current_dungeon() _clear_dungeon() dungeon_level -= 1 _generate_dungeon_level() func _save_current_dungeon(): saved_dungeons[dungeon_level] = { "grid": dungeon_grid.duplicate(true), "rooms": dungeon_rooms.duplicate(true) } func _show_portal_choice(): get_tree().paused = true Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) portal_choice_panel = PanelContainer.new() portal_choice_panel.process_mode = Node.PROCESS_MODE_ALWAYS var vbox = VBoxContainer.new() vbox.add_theme_constant_override("separation", 10) var title = Label.new() title.text = "Wohin möchtest du gehen?" title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER vbox.add_child(title) var btn_up = Button.new() btn_up.text = "Eine Ebene höher (Ebene " + str(dungeon_level - 1) + ")" btn_up.pressed.connect(_on_portal_go_up) vbox.add_child(btn_up) var btn_out = Button.new() btn_out.text = "Zurück zur Oberwelt" btn_out.pressed.connect(_on_portal_exit) vbox.add_child(btn_out) var btn_cancel = Button.new() btn_cancel.text = "Abbrechen" btn_cancel.pressed.connect(_on_portal_cancel) vbox.add_child(btn_cancel) portal_choice_panel.add_child(vbox) player.hud.add_child(portal_choice_panel) portal_choice_panel.anchors_preset = Control.PRESET_CENTER portal_choice_panel.position -= portal_choice_panel.size / 2 func _on_portal_go_up(): _close_portal_choice() _go_up() func _on_portal_exit(): _close_portal_choice() _exit_dungeon() func _on_portal_cancel(): _close_portal_choice() func _close_portal_choice(): if portal_choice_panel and is_instance_valid(portal_choice_panel): portal_choice_panel.queue_free() portal_choice_panel = null get_tree().paused = false func _hide_overworld(): overworld_nodes.clear() for child in get_children(): if child == player: continue if child.name == "OverworldEnv": continue if child is Node3D or child is StaticBody3D: overworld_nodes.append(child) child.visible = false if child is StaticBody3D: child.process_mode = Node.PROCESS_MODE_DISABLED if overworld_env: overworld_env_resource = overworld_env.environment overworld_env.environment = null func _generate_dungeon_level(): dungeon_container = Node3D.new() dungeon_container.name = "DungeonContainer" add_child(dungeon_container) # Gespeichertes Level laden oder neu generieren if saved_dungeons.has(dungeon_level): dungeon_grid = saved_dungeons[dungeon_level]["grid"].duplicate(true) dungeon_rooms = saved_dungeons[dungeon_level]["rooms"].duplicate(true) else: _generate_dungeon() _build_dungeon_geometry() _build_dungeon_navmesh() # Edge-Connection-Margin erhöhen damit NavMesh-Zellen sich verbinden var map_rid = get_world_3d().navigation_map NavigationServer3D.map_set_edge_connection_margin(map_rid, 0.6) _place_return_portal() _place_deeper_portal() _setup_dungeon_lighting() _setup_dungeon_environment() # Spieler-Spawn: beim Hochgehen im letzten Raum, sonst im ersten var spawn_room: Rect2i if saved_dungeons.has(dungeon_level) and dungeon_level < _get_max_visited_level(): # Von unten zurückgekommen → Spawn am Deeper-Portal (letzter Raum) spawn_room = dungeon_rooms[dungeon_rooms.size() - 1] else: # Neu betreten oder von oben → Spawn am Eingang (erster Raum) spawn_room = dungeon_rooms[0] var spawn = Vector3( (spawn_room.position.x + spawn_room.size.x / 2.0) * TILE_SIZE, 0.5, (spawn_room.position.y + spawn_room.size.y / 2.0) * TILE_SIZE ) player.global_position = spawn # Gegner im Dungeon spawnen _spawn_dungeon_enemies() func _get_max_visited_level() -> int: var max_level = 0 for key in saved_dungeons.keys(): if key > max_level: max_level = key return max_level func _clear_dungeon(): if dungeon_container: dungeon_container.queue_free() dungeon_container = null if dungeon_env: dungeon_env.queue_free() dungeon_env = null return_portal_area = null return_portal_label = null deeper_portal_area = null deeper_portal_label = null dungeon_grid.clear() dungeon_rooms.clear() func _exit_dungeon(): in_dungeon = false dungeon_level = 0 _clear_dungeon() saved_dungeons.clear() # Overworld-Nodes wieder zeigen for node in overworld_nodes: if is_instance_valid(node): node.visible = true if node is StaticBody3D: node.process_mode = Node.PROCESS_MODE_INHERIT overworld_nodes.clear() if overworld_env: overworld_env.environment = overworld_env_resource # Spieler zurück zur Overworld player.global_position = player_overworld_pos # ───────────────────────────────────────────────────────────────────────────── # DUNGEON-GENERIERUNG # ───────────────────────────────────────────────────────────────────────────── func _generate_dungeon(): dungeon_grid.clear() dungeon_rooms.clear() for x in range(GRID_WIDTH): var col = [] for y in range(GRID_HEIGHT): col.append(0) dungeon_grid.append(col) var attempts = 0 var target_rooms = randi_range(MIN_ROOMS, MAX_ROOMS) while dungeon_rooms.size() < target_rooms and attempts < 200: attempts += 1 var w = randi_range(MIN_ROOM_SIZE, MAX_ROOM_SIZE) var h = randi_range(MIN_ROOM_SIZE, MAX_ROOM_SIZE) var x = randi_range(2, GRID_WIDTH - w - 2) var y = randi_range(2, GRID_HEIGHT - h - 2) var new_room = Rect2i(x, y, w, h) if _dungeon_room_fits(new_room): dungeon_rooms.append(new_room) _carve_room(new_room) for i in range(dungeon_rooms.size() - 1): _carve_corridor(dungeon_rooms[i], dungeon_rooms[i + 1]) func _dungeon_room_fits(new_room: Rect2i) -> bool: var expanded = Rect2i(new_room.position - Vector2i(2, 2), new_room.size + Vector2i(4, 4)) for room in dungeon_rooms: if expanded.intersects(room): return false return true func _carve_room(room: Rect2i): for x in range(room.position.x, room.position.x + room.size.x): for y in range(room.position.y, room.position.y + room.size.y): dungeon_grid[x][y] = 1 func _carve_corridor(room_a: Rect2i, room_b: Rect2i): var ax = room_a.position.x + room_a.size.x / 2 var ay = room_a.position.y + room_a.size.y / 2 var bx = room_b.position.x + room_b.size.x / 2 var by = room_b.position.y + room_b.size.y / 2 var corridor_width = 2 var start_x = min(ax, bx) var end_x = max(ax, bx) for x in range(start_x, end_x + 1): for w in range(corridor_width): if x >= 0 and x < GRID_WIDTH and ay + w >= 0 and ay + w < GRID_HEIGHT: dungeon_grid[x][ay + w] = 1 var start_y = min(ay, by) var end_y = max(ay, by) for y in range(start_y, end_y + 1): for w in range(corridor_width): if bx + w >= 0 and bx + w < GRID_WIDTH and y >= 0 and y < GRID_HEIGHT: dungeon_grid[bx + w][y] = 1 func _build_dungeon_geometry(): var floor_mat = StandardMaterial3D.new() floor_mat.albedo_color = Color(0.25, 0.22, 0.2) floor_mat.roughness = 0.9 var wall_mat = StandardMaterial3D.new() wall_mat.albedo_color = Color(0.35, 0.3, 0.28) wall_mat.roughness = 0.95 var ceiling_mat = StandardMaterial3D.new() ceiling_mat.albedo_color = Color(0.2, 0.18, 0.16) ceiling_mat.roughness = 0.95 for x in range(GRID_WIDTH): for y in range(GRID_HEIGHT): if dungeon_grid[x][y] == 1: # Boden var floor_tile = CSGBox3D.new() floor_tile.size = Vector3(TILE_SIZE, 0.3, TILE_SIZE) floor_tile.position = Vector3(x * TILE_SIZE, -0.15, y * TILE_SIZE) floor_tile.material = floor_mat floor_tile.use_collision = true dungeon_container.add_child(floor_tile) # Decke var ceil_tile = CSGBox3D.new() ceil_tile.size = Vector3(TILE_SIZE, 0.3, TILE_SIZE) ceil_tile.position = Vector3(x * TILE_SIZE, WALL_HEIGHT + 0.15, y * TILE_SIZE) ceil_tile.material = ceiling_mat dungeon_container.add_child(ceil_tile) # Wände var directions = [ Vector2i(-1, 0), Vector2i(1, 0), Vector2i(0, -1), Vector2i(0, 1) ] for dir in directions: var nx = x + dir.x var ny = y + dir.y if nx < 0 or nx >= GRID_WIDTH or ny < 0 or ny >= GRID_HEIGHT or dungeon_grid[nx][ny] == 0: var wall = CSGBox3D.new() if dir.x != 0: wall.size = Vector3(0.3, WALL_HEIGHT, TILE_SIZE) wall.position = Vector3( x * TILE_SIZE + dir.x * TILE_SIZE / 2.0, WALL_HEIGHT / 2.0, y * TILE_SIZE ) else: wall.size = Vector3(TILE_SIZE, WALL_HEIGHT, 0.3) wall.position = Vector3( x * TILE_SIZE, WALL_HEIGHT / 2.0, y * TILE_SIZE + dir.y * TILE_SIZE / 2.0 ) wall.material = wall_mat wall.use_collision = true dungeon_container.add_child(wall) func _place_return_portal(): var first = dungeon_rooms[0] var portal_pos = Vector3( (first.position.x + 1.0) * TILE_SIZE, 0, (first.position.y + 1.0) * TILE_SIZE ) var portal = Node3D.new() portal.name = "ReturnPortal" portal.position = portal_pos dungeon_container.add_child(portal) var portal_mat = StandardMaterial3D.new() portal_mat.albedo_color = Color(0.2, 0.4, 1.0) portal_mat.emission_enabled = true portal_mat.emission = Color(0.2, 0.4, 1.0) portal_mat.emission_energy_multiplier = 2.0 # Portal-Ring var frame = CSGTorus3D.new() frame.inner_radius = 1.2 frame.outer_radius = 1.6 frame.ring_sides = 16 frame.sides = 24 frame.position = Vector3(0, 2.0, 0) frame.material = portal_mat portal.add_child(frame) # Portal-Fläche var surface = CSGCylinder3D.new() surface.radius = 1.2 surface.height = 0.1 surface.sides = 24 surface.position = Vector3(0, 2.0, 0) surface.material = portal_mat portal.add_child(surface) # Trigger return_portal_area = Area3D.new() return_portal_area.name = "PortalArea" return_portal_area.position = Vector3(0, 1.5, 0) portal.add_child(return_portal_area) var col = CollisionShape3D.new() var shape = BoxShape3D.new() shape.size = Vector3(4, 4, 4) col.shape = shape return_portal_area.add_child(col) # Label return_portal_label = Label3D.new() return_portal_label.text = "Zurück zur Oberwelt [E]" return_portal_label.position = Vector3(0, 4.5, 0) return_portal_label.font_size = 48 return_portal_label.modulate = Color(0.3, 0.6, 1.0) return_portal_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED return_portal_label.visible = false portal.add_child(return_portal_label) func _place_deeper_portal(): var last = dungeon_rooms[dungeon_rooms.size() - 1] var portal_pos = Vector3( (last.position.x + last.size.x / 2.0) * TILE_SIZE, 0, (last.position.y + last.size.y / 2.0) * TILE_SIZE ) var portal = Node3D.new() portal.name = "DeeperPortal" portal.position = portal_pos dungeon_container.add_child(portal) var portal_mat = StandardMaterial3D.new() portal_mat.albedo_color = Color(1.0, 0.3, 0.1) portal_mat.emission_enabled = true portal_mat.emission = Color(1.0, 0.3, 0.1) portal_mat.emission_energy_multiplier = 2.0 # Portal-Ring (rot/orange) var frame = CSGTorus3D.new() frame.inner_radius = 1.2 frame.outer_radius = 1.6 frame.ring_sides = 16 frame.sides = 24 frame.position = Vector3(0, 2.0, 0) frame.material = portal_mat portal.add_child(frame) # Portal-Fläche var surface = CSGCylinder3D.new() surface.radius = 1.2 surface.height = 0.1 surface.sides = 24 surface.position = Vector3(0, 2.0, 0) surface.material = portal_mat portal.add_child(surface) # Trigger deeper_portal_area = Area3D.new() deeper_portal_area.name = "DeeperPortalArea" deeper_portal_area.position = Vector3(0, 1.5, 0) portal.add_child(deeper_portal_area) var col = CollisionShape3D.new() var shape = BoxShape3D.new() shape.size = Vector3(4, 4, 4) col.shape = shape deeper_portal_area.add_child(col) # Label deeper_portal_label = Label3D.new() deeper_portal_label.text = "Ebene " + str(dungeon_level + 1) + " betreten [E]" deeper_portal_label.position = Vector3(0, 4.5, 0) deeper_portal_label.font_size = 48 deeper_portal_label.modulate = Color(1.0, 0.5, 0.2) deeper_portal_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED deeper_portal_label.visible = false portal.add_child(deeper_portal_label) func _setup_dungeon_lighting(): # Licht in jedem Raum for room in dungeon_rooms: var center = Vector3( (room.position.x + room.size.x / 2.0) * TILE_SIZE, WALL_HEIGHT - 1.0, (room.position.y + room.size.y / 2.0) * TILE_SIZE ) var light = OmniLight3D.new() light.position = center light.omni_range = 15.0 light.light_energy = 1.2 light.light_color = Color(1.0, 0.85, 0.6) light.shadow_enabled = true dungeon_container.add_child(light) # Korridor-Beleuchtung for i in range(dungeon_rooms.size() - 1): var room_a = dungeon_rooms[i] var room_b = dungeon_rooms[i + 1] var ax = (room_a.position.x + room_a.size.x / 2.0) * TILE_SIZE var ay = (room_a.position.y + room_a.size.y / 2.0) * TILE_SIZE var bx = (room_b.position.x + room_b.size.x / 2.0) * TILE_SIZE var by = (room_b.position.y + room_b.size.y / 2.0) * TILE_SIZE # Licht am L-Knick var corner_light = OmniLight3D.new() corner_light.position = Vector3(bx, WALL_HEIGHT - 1.0, ay) corner_light.omni_range = 12.0 corner_light.light_energy = 1.0 corner_light.light_color = Color(1.0, 0.8, 0.5) dungeon_container.add_child(corner_light) # Mitte horizontaler Gang var mid_h = OmniLight3D.new() mid_h.position = Vector3((ax + bx) / 2.0, WALL_HEIGHT - 1.0, ay) mid_h.omni_range = 10.0 mid_h.light_energy = 0.8 mid_h.light_color = Color(1.0, 0.8, 0.5) dungeon_container.add_child(mid_h) # Mitte vertikaler Gang var mid_v = OmniLight3D.new() mid_v.position = Vector3(bx, WALL_HEIGHT - 1.0, (ay + by) / 2.0) mid_v.omni_range = 10.0 mid_v.light_energy = 0.8 mid_v.light_color = Color(1.0, 0.8, 0.5) dungeon_container.add_child(mid_v) func _setup_dungeon_environment(): var env = Environment.new() env.background_mode = Environment.BG_COLOR env.background_color = Color(0.02, 0.02, 0.04) env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color(0.08, 0.07, 0.1) env.ambient_light_energy = 0.3 env.tonemap_mode = Environment.TONE_MAPPER_FILMIC env.fog_enabled = true env.fog_light_color = Color(0.05, 0.05, 0.08) env.fog_density = 0.02 dungeon_env = WorldEnvironment.new() dungeon_env.name = "DungeonEnv" dungeon_env.environment = env add_child(dungeon_env) func _build_dungeon_navmesh(): var nav_region = NavigationRegion3D.new() nav_region.name = "DungeonNavRegion" var nav_mesh = NavigationMesh.new() var half = TILE_SIZE / 2.0 var margin = 0.5 # Abstand zu Wänden # Positions-basierte Vertex-Deduplizierung: # Gleiche Position → gleicher Index → NavMesh ist automatisch zusammenhängend var pos_to_idx: Dictionary = {} # "x_z" String → vertex index var vertices = PackedVector3Array() var cell_indices: Array = [] # [{i0, i1, i2, i3}, ...] for x in range(GRID_WIDTH): for y in range(GRID_HEIGHT): if dungeon_grid[x][y] != 1: continue var wall_left = (x <= 0 or dungeon_grid[x - 1][y] == 0) var wall_right = (x >= GRID_WIDTH - 1 or dungeon_grid[x + 1][y] == 0) var wall_top = (y <= 0 or dungeon_grid[x][y - 1] == 0) var wall_bot = (y >= GRID_HEIGHT - 1 or dungeon_grid[x][y + 1] == 0) var lx = x * TILE_SIZE - half + (margin if wall_left else 0.0) var rx = (x + 1) * TILE_SIZE - half - (margin if wall_right else 0.0) var tz = y * TILE_SIZE - half + (margin if wall_top else 0.0) var bz = (y + 1) * TILE_SIZE - half - (margin if wall_bot else 0.0) var corners = [ Vector3(lx, 0.0, tz), Vector3(rx, 0.0, tz), Vector3(rx, 0.0, bz), Vector3(lx, 0.0, bz) ] var indices = PackedInt32Array() for c in corners: var key = "%0.4f_%0.4f" % [c.x, c.z] if not pos_to_idx.has(key): pos_to_idx[key] = vertices.size() vertices.append(c) indices.append(pos_to_idx[key]) cell_indices.append(indices) nav_mesh.vertices = vertices for ci in cell_indices: nav_mesh.add_polygon(ci) nav_region.navigation_mesh = nav_mesh dungeon_container.add_child(nav_region) func _spawn_dungeon_enemies(): for i in range(1, dungeon_rooms.size()): # Letzter Raum = Deeper-Portal, keine Gegner if i == dungeon_rooms.size() - 1: continue var room = dungeon_rooms[i] var center = Vector3( (room.position.x + room.size.x / 2.0) * TILE_SIZE, 0.5, (room.position.y + room.size.y / 2.0) * TILE_SIZE ) var enemy = ENEMY_SCENE.instantiate() dungeon_container.add_child(enemy) enemy.global_position = center # Gegner-Level = Dungeon-Ebene enemy.mob_level = dungeon_level _setup_enemy(enemy) # ───────────────────────────────────────────────────────────────────────────── # MENÜ & SPIELER-SETUP # ───────────────────────────────────────────────────────────────────────────── # Nach Hauptmenü: Klassenauswahl anzeigen func _on_start_game(): var menu = CLASS_SELECTION_MENU.instantiate() add_child(menu) menu.class_selected.connect(_on_class_selected) get_tree().paused = true # Klasse ausgewählt: Spieler initialisieren func _on_class_selected(character_class: CharacterClass): player.character_class = character_class # Skills klassenabhängig aufbauen player._init_class_skills() for i in range(9): player._refresh_action_slot(i) # Startausrüstung klassenabhängig if character_class.resource_type == CharacterClass.ResourceType.MANA: player.equip_item(STARTER_STAFF) else: player.equip_item(STARTER_SWORD) player.equip_item(STARTER_CHEST) player._calculate_stats() player.current_hp = player.max_hp var potion = STARTER_POTION.duplicate() potion.stack_size = 3 player.inventory.add_item(potion) player.assign_to_action_bar(2, potion) player.current_resource = player.max_resource player.hud.update_health(player.current_hp, player.max_hp) player.hud.update_resource(player.current_resource, player.max_resource, player.get_resource_name()) print("Klasse gewählt: ", character_class.class_name_de) # Jetzt Gegner initialisieren for child in get_children(): if child.has_method("take_damage") and child != player: _setup_enemy(child) # ───────────────────────────────────────────────────────────────────────────── # GEGNER-SYSTEM # ───────────────────────────────────────────────────────────────────────────── func _setup_enemy(enemy): if enemy and player: enemy.target = player if enemy.loot_table == null: enemy.loot_table = GOBLIN_LOOT if not enemy.enemy_died.is_connected(_on_enemy_died): enemy.enemy_died.connect(_on_enemy_died) if not enemy.enemy_dropped_loot.is_connected(_on_enemy_dropped_loot): enemy.enemy_dropped_loot.connect(_on_enemy_dropped_loot) func _on_enemy_dropped_loot(loot: Dictionary, world_pos: Vector3): if player: player.receive_loot(loot, world_pos) func _on_enemy_died(spawn_position: Vector3, xp_reward: int): if player: player.gain_xp(xp_reward) print("Respawn in ", RESPAWN_TIME, " Sekunden...") await get_tree().create_timer(RESPAWN_TIME).timeout if in_dungeon and dungeon_container: var new_enemy = ENEMY_SCENE.instantiate() dungeon_container.add_child(new_enemy) new_enemy.global_position = spawn_position _setup_enemy(new_enemy) elif not in_dungeon: _spawn_enemy(spawn_position) func _spawn_enemy(position: Vector3): var new_enemy = ENEMY_SCENE.instantiate() add_child(new_enemy) new_enemy.global_position = position _setup_enemy(new_enemy)