Neustart mit LFS

This commit is contained in:
torsten 2026-02-21 10:07:53 +01:00
commit 5362a6280b
54 changed files with 1689 additions and 0 deletions

7
.gitattributes vendored Normal file
View file

@ -0,0 +1,7 @@
*.blend filter=lfs diff=lfs merge=lfs -text
*.glb filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
# ---> Godot
# Godot 4+ specific ignores
.godot/
# Godot-specific ignores
.import/
export.cfg
export_presets.cfg
# Imported translations (automatically generated from CSV files)
*.translation
# Mono-specific ignores
.mono/
data_*/
mono_crash.*.json

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# weltenauge
NWN-ähnliche Multiplayer-Welt.

82
prompts/main.txt Normal file
View file

@ -0,0 +1,82 @@
Für mein NWN/Godot Projekt brauche ich eine Referenzimplementierung für die Client/Server Logik.
Client und Server sind in der gleichen Codebase/Projekt. Normal aufgerufen, kann der Anwender
Join und Host auswählen. Stellt das Programm fest, dass es headless läuft, wird automatisch als Host gestartet.
Die Szenen(Areas) in denen die Spieler laden, werden nach bedarf geladen. Wenn Spieler A in Area 1 ist, hat sein
Client nur Area 1 geladen. Wenn Spieler B in Area 2 ist, hat sein Client nur Area 2 geladen. Nur
Host hat beide Areas geladen, und synchronisiert seine Area 1 mit Area 1 des Clients von Spieler A und
seine Area 2 mit der Area 2 des Clients von Spieler B. Ein Übergang zu einer anderen Area kann vom Host veranlasst
werden . Er lädt die neue Area(Szene) und erzeugt den Avatar des Spielers. Der Client des betroffenen Spielers
muss die neue Area(Szene) auf seiner Seite ebenfalls laden, damit die Szenenobjekte synchronisiert werden können.
Sorge dafür, dass die Areas auf dem Server sich nicht überlagern. Jeder MultiPlayerSynchronizer sollte nur
position der Spieler synchronisieren, aber nicht global_position.
Jede Area soll auf dem Server einen eigenen SubViewport haben. (Godot Multi-World Physics)
Sorge mit Visibility Filtering dafür, dass die MultiplayerSynchronizer nur die Objekte in ihrer jeweiligen Area synchronisieren.
Aus Performancegründen soll die Click-to-Move Funktionalität über AStar implementiert werden.
Das Spielermodell liegt unter res://systems/player/player.tres
Area 1 liegt unter res://maps/outdoor/grass/keuwin_grasland.tscn
Area 2 liegt unter res://maps/planes/fugue/halle_der_ahnen.tscn
Alle Scripte müssen in einem thematisch passenden Subfolder unterhalb res://systems/ abgelegt werden.
Dies ist die Struktur des Projektes:
/assets/ # REINE ROHDATEN (Dumm, keine Spiellogik)
/characters/
/humanoid/
/base_meshes/ # .glb Dateien und .tres (Mesh-Resources)
/materials/ # .tres (Material-Resources)
/environment/
/dungeon/ # Tileset-Modelle
/nature/ # Bäume, Steine
/items/
/armor/
/weapons/
/consumables/
/systems/ # DAS GEHIRN (Logik, muss auf dem Server laufen)
/core/ # Game-Loop, Signale, State-Machine
/network/ # Multiplayer, Peer-Handling, Headless-Logik
/player/ # Player-Controller (ohne Kamera/UI)
/ai/ # NPC-Verhalten, Monster-Logik
/combat/ # Regelwerk, Würfelwürfe, Stats
/dialogue/ # Gesprächs-Logik, Quests
/inventory/ # Item-Management, Container
/interaction/ # Türen, Hebel, Portale
/navigation/ # AStar-Management, Pathfinder
/simulation/ # Tag-Nacht-Zyklus, Fraktionen
/data/ # Speicher-Logik, DB-Anbindung
/fx/ # Wetter-Logik, Sound-Trigger
/ui/ # DAS GESICHT (Nur für den Client)
/hud/ # Permanente Anzeigen (HP, Chat)
/menus/ # Startmenü, Optionen
/windows/ # Inventar-Fenster, Charakterbogen
/components/ # Buttons, Slots, wiederverwendbare Teile
/themes/ # Schriften, Styles, Visual Design
/maps/ # DIE WELT (Szenen, in denen man spielt)
/templates/ # Basis-Setups für neue Karten
/outdoor/ # Wald, Wüste, Gebirge
/indoor/ # Häuser, Burgen
/dungeons/ # Krypten, Höhlen
/planes/ # Andere Existenzebenen
/audio/ # KLANGWELT
/music/ # Hintergrundmusik (.ogg)
/sfx/ # Geräusche (Schwert, Schritte)
/ambience/ # Atmosphäre-Loops (Wind, Feuer)
/data/ # DIE DEFINITIONEN (Inhaltliche Datenkarten)
/items/ # .tres Dateien für jedes Schwert/Schild
/tables/ # SQLite .db oder JSON-Tabellen
/licencing/ # Rechtliches (Asset-Lizenzen)
Erstelle bitte eine Referenzimplementierung, die folgende Funktionen abdeckt: Host Game, Join Game, Scenenwechsel, Server-Authority
Erzeuge so wenige Dateien wie möglich.

3
tools/start_server.sh Executable file
View file

@ -0,0 +1,3 @@
#/opt/godot/Godot_v4.6-stable_linux.x86_64 --path /home/torsten/repos/weltenauge/weltenauge --headless -- --server
#/opt/godot/Godot_v4.6-stable_linux.x86_64 --path /home/torsten/Dokumente/Gamedev/Projekt_Weltenauge/Projekt_Weltenauge --headless -- --server
/opt/godot/Godot_v4.6-stable_linux.x86_64 --path /home/tschneider/Dokumente/GameDev/Projekt_Weltenauge/Projekt_Weltenauge --headless -- --server

1
weltenauge/.cursorrules Normal file
View file

@ -0,0 +1 @@
Always use Godot 4.x GDScript syntax. Prefer static typing (e.g., var x: int = 5) and use the @onready annotation for nodes.

4
weltenauge/.editorconfig Normal file
View file

@ -0,0 +1,4 @@
root = true
[*]
charset = utf-8

3
weltenauge/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"godotTools.editorPath.godot4": "/opt/godot/godot_v4.6-stable_linux.x86_64"
}

View file

@ -0,0 +1,35 @@
class_name CharacterData
var id: String = "" # UUID des Charakters
var name: String = "Neuer Held"
var level: int = 1
var area: String = "keuwin_grasland"
var position: Vector3 = Vector3.ZERO
# Erstellt ein Dictionary – Vector3 bleibt ein Vector3!
func to_dict() -> Dictionary:
return {
"id": id,
"name": name,
"level": level,
"area": area,
"position": position
}
func from_dict(data: Dictionary) -> void:
id = data.get("id", id)
name = data.get("name", name)
level = data.get("level", level)
area = data.get("area", area)
position = data.get("position", position)
## Auf dem Client
#var data = mein_charakter.to_dict()
#update_server_pos.rpc(data)
## Die RPC Funktion
#@rpc("any_peer")
#func update_server_pos(data: Dictionary):
# var server_char = CharacterData.new()
# server_char.from_dict(data)
# print(server_char.position) # Funktioniert sofort als Vector3

View file

@ -0,0 +1 @@
uid://dsylf8nx1e2wc

View file

@ -0,0 +1,33 @@
class_name SaveLoad
const SAVE_PATH = "user://player_data.json"
static func save_character(data: CharacterData):
var save_dict = data.to_dict()
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if file:
file.store_string(JSON.stringify(save_dict, "\t"))
file.close()
print("Saved player data to ", SAVE_PATH)
else:
printerr("Error saving to ", SAVE_PATH)
static func load_character() -> CharacterData:
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
if file:
var json_string = file.get_as_text()
file.close()
var data = JSON.parse_string(json_string)
if data == null:
printerr("JSON Parse Error")
return null
var char_data = CharacterData.new()
char_data.from_dict(data)
print("Loaded player data from ", SAVE_PATH)
return char_data
else:
print("No savefile found, returning default CharacterData.")
return null

View file

@ -0,0 +1 @@
uid://dhettikhnfdt4

View file

@ -0,0 +1,12 @@
extends Camera3D
var target: Node3D = null
#var offset = Vector3(0, 12, 8) # Klassische NWN Perspektive
var offset = Vector3(0, 3, 4) # Klassische NWN Perspektive
func _process(delta):
if is_instance_valid(target) and target.is_inside_tree():
# Sanftes Folgen (Lerp) für smoothe Bewegungen
var target_pos = target.global_position + offset
global_position = global_position.lerp(target_pos, delta * 5.0)
look_at(target.global_position)

View file

@ -0,0 +1 @@
uid://ctxhm624vs1ya

View file

@ -0,0 +1,26 @@
extends Node
# Signal für die UI, um neue Nachrichten anzuzeigen
signal network_message_received(sender_name: String, message: String, channel: String)
@rpc("any_peer", "call_remote", "reliable")
func send_message(message: String):
var peer_id = multiplayer.get_remote_sender_id()
# Serverseitige Validierung (Spam-Schutz, Filter)
if message.length() > 256:
message = message.left(256)
var sender_name = "Player " + str(peer_id) # Hier später Name aus DB laden
# Ermittle die Area des Spielers (Sichtbarkeits-Logik)
var area = NetworkManager.player_area_map.get(peer_id, "")
# Broadcast: Nur an Spieler in der gleichen Area senden (NWN-Style)
for id in NetworkManager.player_area_map:
if NetworkManager.player_area_map[id] == area:
cl_receive_message.rpc_id(id, sender_name, message, "local")
@rpc("authority", "call_remote", "reliable")
func cl_receive_message(sender_name: String, message: String, channel: String):
network_message_received.emit(sender_name, message, channel)

View file

@ -0,0 +1 @@
uid://c351ysuym2mdm

View file

@ -0,0 +1,13 @@
extends Node
signal ui_join_requested(ip: String, username: String, password: String)
signal ui_host_requested()
signal ui_create_character_screen()
signal ui_character_created(character: CharacterData)
signal ui_character_select()
signal ui_area_join_requested(area_key: String)
signal player_area_join_requested(area_key: String)
signal network_show_characters(characters:Array)
signal network_new_character_received(character: CharacterData)

View file

@ -0,0 +1 @@
uid://4xof67e363t4

View file

@ -0,0 +1,249 @@
extends Node
const PORT = 7777
const MAX_CLIENTS = 32
# Kamera (wird nur auf dem Client gebraucht)
var local_camera: Camera3D = null
# Mapping der Areas
var area_paths = {
"keuwin_grasland": "res://05_worlds/area/outdoor/grass/keuwin_grasland.tscn",
"ahnenhalle": "res://05_worlds/area/planes/fugue/halle_der_ahnen.tscn"
}
# Server-seitiges Tracking: peer_id -> SubViewportContainer
var player_worlds = {}
func _ready():
GameEvents.ui_host_requested.connect(host_game)
GameEvents.ui_join_requested.connect(join_game)
GameEvents.ui_character_created.connect(request_character_store)
if DisplayServer.get_name() == "headless" or OS.get_cmdline_args().has("--server"):
host_game()
func host_game():
var peer = ENetMultiplayerPeer.new()
peer.create_server(PORT, MAX_CLIENTS)
multiplayer.multiplayer_peer = peer
multiplayer.peer_connected.connect(_on_player_connected)
multiplayer.peer_disconnected.connect(_on_player_disconnected)
print("Server gestartet. id=" + str(multiplayer.get_unique_id()))
func join_game(ip: String, username: String, password: String):
print("NetworkManager.join_game(", ip, "/", username, "/", password)
var peer = ENetMultiplayerPeer.new()
peer.create_client(ip, PORT)
multiplayer.multiplayer_peer = peer
func _on_player_connected(id):
print("Player connected with id:", id)
cl_show_characters.rpc_id(id, [])
@rpc("authority", "call_remote", "reliable")
func cl_show_characters(characters):
print("Got client_show_characters")
GameEvents.network_show_characters.emit(characters)
func _on_player_disconnected(id):
if not multiplayer.is_server(): return
print("Player disconnected with id: ", id)
# Der MultiplayerSpawner entfernt den Node automatisch aus dem Szenenbaum.
# Unsere einzige Aufgabe ist es, unseren internen Zustand aufzuräumen.
if player_worlds.has(id):
print("Removing player ", id, " from internal tracking.")
player_worlds.erase(id)
func change_player_area(id: int, area_key: String):
if not multiplayer.is_server(): return
print("Server is loading ", area_key)
var path = area_paths[area_key]
var container_name = area_key + "_container"
var viewport_name = area_key + "_viewport"
# Server: Erstelle SubViewportContainer + SubViewport für physikalische Trennung, falls nicht existent
var container_node = get_node_or_null(container_name)
if not container_node:
#container_node = SubViewportContainer.new()
container_node = Node3D.new()
container_node.name = container_name
add_child(container_node)
var viewport_node = container_node.get_node_or_null(viewport_name)
var map_instance = null
if not viewport_node:
viewport_node = SubViewport.new()
viewport_node.name = viewport_name
viewport_node.own_world_3d = true # Multi-World Physics!
container_node.add_child(viewport_node)
else:
map_instance = viewport_node.get_node_or_null(area_key)
map_instance = load(path).instantiate()
map_instance.name = area_key
viewport_node.add_child(map_instance)
# --- ALTEN ZUSTAND AUFRÄUMEN ---
if player_worlds.has(id):
player_worlds.erase(id)
# --- NEUEN ZUSTAND REGISTRIEREN ---
player_worlds[id] = container_node
# Client anweisen, Szene zu laden
cl_load_area.rpc_id(id, area_key)
# Avatar auf Server erstellen erfolgt erst wenn Client Vollzug meldet (client_ready_to_spawn)
@rpc("any_peer", "call_local", "reliable")
func client_ready_to_spawn(area_key:String):
var id = multiplayer.get_remote_sender_id()
if not multiplayer.is_server(): return
if player_worlds.has(id):
var container_node = player_worlds[id]
var viewport_node = container_node.get_node(area_key+"_viewport")
var spawn_target_node = viewport_node.get_node(area_key).get_node("SpawnPoints")
print("Container Node:", container_node)
print("Viewport Node:", viewport_node)
print("spawn target Node:", spawn_target_node)
print("Client ", id, " bereits für Spawning.")
_spawn_player_in_world(id, spawn_target_node)
@rpc("authority", "call_remote", "reliable")
func cl_load_area(area_key):
print("Client loads area")
# Client löscht alte Map, lädt neue lokal in das Root
for child in get_children():
if child.is_in_group("maps"): child.queue_free()
var container_name = area_key + "_container"
var viewport_name = area_key + "_viewport"
var container_node = SubViewportContainer.new()
container_node.name = container_name
container_node.stretch = true
container_node.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(container_node)
container_node.add_to_group("maps")
var viewport_node = SubViewport.new()
viewport_node.own_world_3d = true
viewport_node.name = viewport_name
viewport_node.size = Vector2i(1280,720)
container_node.add_child(viewport_node)
var map = load(area_paths[area_key]).instantiate()
map.name = area_key
viewport_node.add_child(map)
# Kamera-Setup nur beim ersten Laden oder falls sie gelöscht wurde
if not multiplayer.is_server(): #and not is_instance_valid(local_camera):
print("Setting up camera")
setup_client_camera(viewport_node)
# Jetzt können wir dem Server ok fürs Spawnen geben
client_ready_to_spawn.rpc_id(1, area_key) # Server ist immer id=1
func setup_client_camera(node):
local_camera = Camera3D.new()
local_camera.name = "Camera3D"
local_camera.set_script(load("res://01_core/components/camera_logic.gd"))
local_camera.current = true
node.add_child(local_camera)
# Ein AudioListener gehört auf dem Client auch an die Kamera
local_camera.add_child(AudioListener3D.new())
func _spawn_player_in_world(id, spawn_target_node):
var player_scene = load("res://systems/player/player.tscn")
var player = player_scene.instantiate()
player.name = str(id)
player.get_node("PlayerNameLabel").text = str(id)
#player.set_multiplayer_authority(id)
player.set_multiplayer_authority(1)
spawn_target_node.add_child(player, true)
# SCHRITT 3: Jetzt ist der Spieler garantiert bereit und konfiguriert.
# Alle folgenden Operationen sind sicher.
var sync = player.get_node("MultiplayerSynchronizer")
# Visibility Filtering: Nur Spieler im gleichen Viewport sehen sich
for other in spawn_target_node.get_children():
if other.is_in_group("players"):
var other_id = other.name.to_int()
# Der neue Spieler sieht den alten
other.get_node("MultiplayerSynchronizer").set_visibility_for(id, true)
# Der alte Spieler sieht den neuen
sync.set_visibility_for(other_id, true)
# Stelle sicher, dass der Spieler IMMER für seinen eigenen Client sichtbar ist.
# Dies ist die wichtigste Regel, damit der Client seine eigenen Updates erhält.
sync.set_visibility_for(id, true)
# Wir schicken dem Client eine Info, welchen Node er verfolgen soll
# (Da der Name des Player-Nodes die ID ist, weiß der Client welcher seiner ist)
cl_assign_camera_target.rpc_id(id, str(id))
@rpc("authority", "call_remote", "reliable")
func cl_assign_camera_target(node_name: String):
# Der Client sucht seinen Player-Node im Baum
var p = get_tree().root.find_child(node_name, true, false)
if p and local_camera:
local_camera.target = p
# var camera = get_viewport().get_camera_3d()
@rpc("any_peer", "call_remote", "reliable")
func srv_handle_area_change_request(area_key: String):
var id = multiplayer.get_remote_sender_id()
if not player_worlds.has(id): return
print("Server: Player ", id, " requests area change to ", area_key)
# 1. Alten Spieler finden und entfernen
var old_container = player_worlds[id]
print("container name=", old_container)
var old_viewport = old_container.get_child(0)
print("Viewport name=", old_viewport)
var player_node = old_viewport.get_child(0).get_node("SpawnPoints").get_node(str(id))
if player_node:
player_node.queue_free()
else:
print("Old viewport:", old_viewport)
print("Player for erasing not found:", id)
# 2. Bestehende Funktion für den Wechsel aufrufen
change_player_area(id, area_key)
func request_character_store(new_character: CharacterData):
var character_dict = new_character.to_dict()
srv_store_new_character.rpc_id(1,character_dict)
@rpc("any_peer", "call_remote", "reliable")
func srv_store_new_character(character_dict: Dictionary):
var new_character = CharacterData.new()
new_character.from_dict(character_dict)
# TODO: In Zukunft erhalten neue Charaktere einen area_key und position gesetzt
var client_id = multiplayer.get_remote_sender_id()
cl_receive_new_character.rpc_id(client_id, new_character.to_dict())
@rpc("authority", "call_remote", "reliable")
func cl_receive_new_character(char_dict: Dictionary):
var new_character = CharacterData.new()
new_character.from_dict(char_dict)
GameEvents.network_new_character_received.emit(new_character)

View file

@ -0,0 +1 @@
uid://tniwwfk5703p

BIN
weltenauge/02_library/meshes/humanoid/dude.glb (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,54 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://do451iws7munv"
path="res://.godot/imported/dude.glb-ff042c8f74305db1da7e47c7a83b2cea.scn"
[deps]
source_file="res://02_library/meshes/humanoid/dude.glb"
dest_files=["res://.godot/imported/dude.glb-ff042c8f74305db1da7e47c7a83b2cea.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={
"meshes": {
"Dude_Cube_002": {
"generate/lightmap_uv": 0,
"generate/lods": 0,
"generate/shadow_meshes": 0,
"lods/normal_merge_angle": 20.0,
"save_to_file/enabled": true,
"save_to_file/fallback_path": "res://assets/characters/humanoid/Dude.tres",
"save_to_file/path": "uid://c6h0s0by6gg3n"
}
}
}
gltf/naming_version=2
gltf/embedded_image_handling=1

View file

@ -0,0 +1,99 @@
[gd_resource type="ArrayMesh" format=4 uid="uid://bi22s15yeh6mw"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_h8qyk"]
resource_name = "pink.002"
cull_mode = 2
albedo_color = Color(0.8201731, 0.17311497, 0.9063355, 1)
roughness = 0.5
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_c6dnh"]
resource_name = "schwarz"
cull_mode = 2
albedo_color = Color(0, 0, 0, 1)
roughness = 0.5
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_i8imj"]
resource_name = "weiss"
cull_mode = 2
albedo_color = Color(0.9063318, 0.9063318, 0.9063318, 1)
roughness = 0.5
[sub_resource type="ArrayMesh" id="ArrayMesh_wt8sa"]
_surfaces = [{
"aabb": AABB(-0.28403106, 0.008408189, -0.083770275, 0.6, 0.6000001, 0.60000014),
"format": 34896613377,
"index_count": 582,
"index_data": PackedByteArray("PQAGAD4APQAEAAYAPwAGAAQAQAAEAD0APwAEAEAAHAAGAD8AHAA+AAYAQQA/AEAAQgA9AD4AQAA9AEMAQwA9AEIARABAAEMAQQBAAEQAGAA/AEEAGAAcAD8ANABBAEQAHAAYABoAHAAaAB0AGAAZABoAHgAcAB0AHgA+ABwAHgAdABsAFgAbABkAGAAWABkAFgAeABsAEQAYAEEAEQBBADQARQAWABgARQAYABEARQARABIAEQANABIARQASABMAEQBGAA0ARgARADQARgALAA0ARwBFABMARwAWAEUARwATABUARwAfABYAHwAeABYAFABHABUAFAAfAEcAFAAVABAARgAIAAsACAAKAAsARgA0AEgACABGAEgASAA0AEkANABEAEkASQBEAEoARABDAEoASABJAEsASgBDAEwAQwBCAEwASQBKAE0ASwBJAE0ASgBMADUATQBKADUATgBIAEsACABIAE4ASwBNAE8ATQA1AFAATwBNAFAAUQBLAE8ATgBLAFEABwBOAFEAUgAIAE4AUgBOAAcABwBRAAAAAABSAAcAUQBPAFMAAABRAFMAUwBPAFQATwBQAFQAAABTAFUAAABVAFIAUwBUAFYAVQBTAFYAVABQAFcAVQBYAFIAWAAIAFIAVQBWAFkAVQBZAFgAVgBUAFoAVABXAFoAWQBWAFsAVgBaAFsAWABcAAgACABcAAoAXAAMAAoAXQBcAFgAWQBdAFgAXABeAAwAXQBeAFwAXgAOAAwAWQBfAF0AWQBbAF8AYABeAF0AXwBgAF0AXgAJAA4AYAAJAF4ACQAPAA4AXwBhAGAACQBiAA8AYgAQAA8AAgAJAGAAYQACAGAAYgAUABAAYQABAAIAAgAwAAkAMABiAAkAAQAwAAIAMgAUAGIAMAAyAGIAAQAxADAAMQAyADAAYQAxAAEAXwBjAGEAYQBjADEAXwBbAGMAMQAzADIAYwAzADEAWwBkAGMAYwBkADMAWwBaAGQAMwA8ADIAMgA8ABQAZABlADMAMwBlADwAWgBmAGQAZABmAGUAWgBXAGYAPAAXABQAZQA7ADwAPAA7ABcAZgBnAGUAZQBnADsAVwBoAGYAZgBoAGcAFAAXAB8AVwBpAGgAUABpAFcAUAA1AGkANQBqAGkANQBMAGoAaQBrAGgAaQBqAGsAaABrADgAaAA4AGcATAA2AGoATABCADYAagBsAGsAagA2AGwAawADADgAawBsAAMAOAADAAUAZwA4ADoAOAAFADoAZwA6ADsAAwA5AAUAAwBsADkAOgAFADkAOwA6ACEAOgA5ACEAbAA3ADkAbAA2ADcAIQA5ADcAOwAhABcANgBtADcANgBCAG0AQgA+AG0AHgBtAD4AIAA3AG0AIABtAB4AIQA3ACAAHwAgAB4AKgAhACsAKwAhACwAIQAqACkAIAAsACEAIQApABcAIAAtACwAIAAuAC0ALgAgAC8ALwAgACYAHwAmACAAHwAkACYAJAAfACMAIwAfACIAFwAiAB8AFwAlACIAFwAnACUAJwAXACgAKAAXACkA"),
"lods": [0.00012957268, PackedByteArray("AAABAAIAAQAwAAIAAQAxADAAAAAxAAEAAgAwAAkAMQAyADAAMAAyAAkAMQAzADIAAAAzADEAAAACAAcAAgAIAAcAAgAJAAgABwAEAAAACAA0AAcABwA0AAQAAAAEADUAAAA1ADMANAAGAAQANQAEADYANgAEAAYANgADADUAEQAGADQACAARADQANgAGADcAAwA2ADcANQADADgANQA4ADMAOAADAAUAAwA3ADkAAwA5AAUAOAAFADoAOgAFADkAMwA4ADoAIQA5ADcAOgA5ACEAMwA6ADsAOwA6ACEAMwA7ADwAMwA8ADIAPAA7ABcAOwAhABcAMgA8ABQAPAAXABQAMgAUAAkAJwAXACgAFwAnACUAKAAXACkAIQApABcAFwAlACIAIQAqACkAKgAhACsAKwAhACwAFwAiAB8AFAAXAB8AIwAfACIAJAAfACMAIAAsACEAHwAkACYAHwAmACAALwAgACYAIQA3ACAALgAgAC8AIAAtACwAIAAuAC0AIAA3AAYAHwAgAB4AIAAGAB4AHwAeABYAFAAfABYAHgAGABwAHgAcAB0AHgAdABsAFgAeABsAHAAaAB0AFgAbABkAGAAcAAYAHAAYABoAGAAZABoAGAAWABkAEQAYAAYAEQAWABgAFAAWABEAFAARABMAEQASABMAFAATABUAEQANABIAFAAVABAAEQAIAA0ACAALAA0ACAAKAAsACAAMAAoACQAUABAACAAJAAwACQAQAA8ACQAOAAwACQAPAA4A"), 0.00025139967, PackedByteArray("AAABAAIAAAADAAEAAAAEAAMAAQADAAUAAQAFAAIAAwAEAAYAAwAGAAUABwAEAAAAAAACAAcABwAGAAQAAgAIAAcACAAGAAcAAgAJAAgAAgAFAAkACAAKAAsACAAMAAoACAALAA0ACAAJAAwACQAOAAwACQAPAA4ACQAQAA8AEQAIAA0AEQANABIACAARAAYAEQASABMACQAUABAAFAARABMAFAAVABAAFAATABUABQAUAAkAFAAWABEABQAXABQAEQAWABgAEQAYAAYAGAAWABkAGAAZABoAFgAbABkAHAAYABoAGAAcAAYAHAAaAB0AHgAdABsAHgAcAB0AFgAeABsAHgAGABwAFAAfABYAHwAeABYAFAAXAB8AIAAGAB4AIAAFAAYAHwAgAB4ABQAhABcAIQAFACAAFwAiAB8AIwAfACIAJAAfACMAFwAlACIAHwAkACYAHwAmACAAFwAnACUAJwAXACgAKAAXACkAIQApABcAIQAqACkAKgAhACsAKwAhACwAIAAsACEAIAAtACwAIAAuAC0ALgAgAC8ALwAgACYA")],
"name": "pink.002",
"primitive": 3,
"uv_scale": Vector4(0, 0, 0, 0),
"vertex_count": 110,
"vertex_data": PackedByteArray("AAAAAAAAAAD//wAAAAAAAP//AAD//wAA/////wAAAAAAAP//AAAAAP///////wAAAAD/////AAAAAAAA//8AADIzqir//wAAzMyqKv//AABlZqoqefAAADIzqip58AAAmZmqKnnwAAAyM1RVefAAAMzMqip58AAAzMxUVXnwAADMzP9/efAAADIz/3///wAAMjP/f3nwAABlZv9/efAAAMzM/3///wAAmZn/f3nwAABlZqqq//8AAMzMqqr//wAAMjOqqv//AABlZqqq+PMAADIzqqr48wAAZWZU1fjzAAAyM1TV//8AADIzVNX48wAAZWZU1f//AACZmaqq//8AAJmZVNX//wAAzMxU1f//AACZmaqqKOwAAJmZ/78o7AAAmZmpyijsAAAhoqqqKOwAAJmZVNUo7AAAqaqqqijsAAAys6qqKOwAAMzMqqoo7AAAzMz/vyjsAADMzKnKKOwAAMzMVNUo7AAAMrNU1SjsAACpqlTVKOwAACGiVNUo7AAA//+qKv//AAD//6oqAAAAAP//VFX//wAA//9UVQAAAAAAAP9///8AAGVmqqoAAAAAmZn//wAAAACZmf////8AAP//VNUAAAAAzMz/////AAD//1TV//8AAP//qqr//wAA////f///AAAyM///AAAAADIz/////wAAAABU1f//AAAAAFTVAAAAAAAAqqr//wAAZWb//wAAAAAyM1TVAAAAAAAAqqoAAAAAZWb/f///AAAyM1RV//8AAJmZ/3///wAAAABVVf//AAAAAP9/AAAAADIzqqoAAAAAAABUVQAAAABlZlTVAAAAADIz/38AAAAAAACqKv//AAAyM1RVAAAAAGVm/38AAAAAAACqKgAAAAAyMwAA//8AADIzqioAAAAAZWZUVQAAAAAyMwAAAAAAAGVmqioAAAAAmZn/fwAAAABlZgAA//8AAGVmAAAAAAAAmZlUVQAAAACZmaoqAAAAAGVmqir//wAAmZkAAP//AACZmaoq//8AAJmZAAAAAAAAzMwAAP//AADMzAAAAAAAAMzMVFX//wAAzMyqKgAAAADMzFRVAAAAAP///38AAAAAzMz/fwAAAAD//6qqAAAAAMzMqqoAAAAAmZmqqgAAAACZmVTVAAAAAMzMVNUAAAAAzMz//wAAAABlZv////8AAA==")
}, {
"aabb": AABB(-0.16403107, 0.10840821, 0.46973133, 0.36, 0.39999998, 0.03833282),
"format": 34896613377,
"index_count": 174,
"index_data": PackedByteArray("CgALAAwACgANAAsACgAOAA0ADwAMAAsADQAOABAAEAAMAA8ADQARAAsACwARABIACwASAA8ADQATABEADwASABQAEAAVAA0AEAAPABUAFQATAA0ADwAUABUAFQAUABMAFgAXABgAFgAZABcAGgAWABgAGgAYABsAHAAZABYAHAAdABkAHgAbAB8AHgAaABsAIAAaAB4AGgAhABYAFgAhACIAFgAiABwAGgAjACEAIAAkABoAJAAjABoAJQAkACAAJAAmACMAHAAiACYAHAAmACQAJQAnACQAJwAcACQAKAAnACUAKAApACcAJwAqABwAKQAqACcAKgAdABwAKQArACoAKgAsAB0AKwAsACoAKwAtACwAAAABAAIAAAAuAAEAAwABAC4ABQAuAAAAAwAuAAQABQAvAC4ALwAEAC4ACQAvAAUALwAGAAQACQAHAC8ABwAGAC8ABwAIAAYA"),
"lods": [0.1056379, PackedByteArray("AAABAAIAAwABAAAAAwAAAAQABQAEAAAABQAGAAQABwAGAAUABwAIAAYACQAHAAUA")],
"name": "schwarz",
"primitive": 3,
"uv_scale": Vector4(0, 0, 0, 0),
"vertex_count": 48,
"vertex_data": PackedByteArray("VFUAAJdDAAAAAP8/l0MAAAAAAACXQwAAAAD/f5dDAABUVf9/l0MAAKqqAACXQwAAqqr/f5dDAAD///8/l0MAAP///3+XQwAA//8AAJdDAAAAAP+/THoAAK8MKORMegAAAAD//0x6AABtIrbkTHoAAFRV/79MegAA0gzr8kx6AABUVf//THoAAK8MKOT//wAA0gzr8v//AABtIrbk//8AAOUgD/P//wAA5SAP80x6AADiuP/vAAAAAKqq//8AAAAAqqr/7wAAAADiuP//AAAAAOK4/98AAAAAqqr/3wAAAAAxzP/vAAAAABvH//8AAAAA4rj/vwAAAACqqv+/AAAAABvH/78AAAAA4rj/72FgAAAxzP/vYWAAAOK4/99hYAAAMcz/3wAAAABU1f+/AAAAADHM/99hYAAAVNX/3wAAAAD///+/AAAAAP///98AAAAAVNX/7wAAAAD////vAAAAAFTV//8AAAAA/////wAAAABUVf8/l0MAAKqq/z+XQwAA")
}, {
"aabb": AABB(-0.14619195, 0.45840824, 0.48416355, 0.26931208, 0.029790878, 0.023900598),
"format": 34896613377,
"index_count": 12,
"index_data": PackedByteArray("AAABAAIAAAADAAEABAAFAAYABAAHAAUA"),
"name": "weiss",
"primitive": 3,
"uv_scale": Vector4(0, 0, 0, 0),
"vertex_count": 8,
"vertex_data": PackedByteArray("//8AAAAAAAAv5tPWAAAAAC/mAAAAAAAA///T1gAAAAAuABr+//8AABAdVj///wAABBv/////AAAAAOU3//8AAA==")
}]
blend_shape_mode = 0
[resource]
resource_name = "Dude_Cube_002"
_surfaces = [{
"aabb": AABB(-0.28403106, 0.008408189, -0.083770275, 0.6, 0.6000001, 0.60000014),
"attribute_data": PackedByteArray("x6zHrOH9x6zh/eH9x6zh/cesclfg/XJX4P2MqMesjKhVEvZkHQJyV44i9mRVEntyN1NyV1US/3+OIntyVRIImx0CjKj/QgibN1OMqMYy9mTGMnty/0L/f/9C9mT/QntyjKjh/XJXx6yMqMesclfh/WEhjiJMJo4iTCbGMmEhN1NMJv9CTCY3UzxlN1NRYDdTUWCzRTxlLjhRYC44h5M3U1aX/0JWlzdTh5P/QnEvN1OHKjdThyr/QnEvjiKHKsYyhyqOIn9+s0XIhDdTf343U8iEdUzIhBRJyISzRR0Cx6w3U8esN1Ph/R0C4f1yV3JXjKiMqHJXjKiMqHJXYZ//QpGbN1ORm/9CYZ83U5yjN1Nrp7NFa6c3U5yjs0V1r7NFpqs3U6ars0V1rzdTRHo3U/tz/0JEev9C+3MbS/tzz037c4NQ+3M3U0yPN1MDibNFTI+zRQOJFEkDiXVMA4k3U3dp/0LAbzdTd2k3U8Bvg1DAb89NwG8bS8Bv/0IrVzdTFlwuOBZcs0UrVy44Flw3U1zwcldc8Iyoz8dyV9jijKhU1Yyoz8eMqEu6cldLuoyoLjg4zbNF4f03U3DdJh3h/aEP4f0dAvZkVRJyV44iclcdAntyHQL/fx0Cg40dAgibN1P/f1SYx6wbiMesG4jh/f9/cleqZ+H9qmfHrON34f3jd8esVJjh/TdT9mT/QnJXN1N7csYyclfGMv9//0KDjTdTg42OIv9/N1MIm/9CjKjGMoyoxjIIm44ijKiOIgibVRKMqFUSg40Im3JXCJuMqIONcleDjYyo/3+MqHtycld7coyo9mRyV/ZkjKip7cesqe3h/XDdx6xw3eH9OM3HrDjN4f3/vMes/7zh/djicldU1XJXs0Wp7TdTqe0uOKntLjjh/bNFcN0uOHDdqiqp7aoq4f2qKnDds0U4zTdTOM2qKjjNs0X/vDdT/7yzRcesLjjHrC44/7yqKsesqir/vCYdx6wmHf+8oQ/HrCYdOM2hD/+8HQL/vKEPOM0dAjjNJh1w3aEPcN0dAnDdJh2p7aEPqe0dAqntYSHGMmEh/0I8ZbNFcS//QnEvxjIrV7NF"),
"format": 34896613399,
"index_count": 582,
"index_data": PackedByteArray("fQAZAH4AfQAbABkAfwB9AH4AfwB+AIAAewB/AIAAewCAAHoAgQB7AHoAgQB6AHkAGACBAHkAGAB5ABoAFgAMAIIAFgCDAAwAFwAWAIIAEwCDABYAFwCCAIQAEwCFAIMAFQAXAIQAFQCEAHgAhgAXABUAhwAVAHgAhgAUABcAhwB4AIgAiQAUAIYAEQCHAIgAEQCIAIoAiwARAIoAiwCKABIAjAARAIsAjACNABEAjgCNAIwAjgCPAI0AkACPAI4AkAAPAI8AEAAPAJAAEAB3AA8AdwCRAA8AdwB2AJEAdgANAJEAdgB1AA0AdQALAA0AdQB0AAsADQALAA4ADQAOAIkAiQAOABQADgATABQADgAKABMACgCFABMACgBzAIUAdAAIAAsACABzAAoAdABxAAgACAByAHMAcQByAAgAcQAJAHIAkgA8AJMAkgA+ADwAlACSAJMAlACTAJUAfACUAJUAfACVAJYAlwB8AJYAlwCWAJgAmQCXAJgAmQCYAJoAOwCZAJoAOwCaAD0AmwACAJwAmwABAAIAnQCbAJwAnQCcAJ4AnwCdAJ4AnwCeAKAAoQCfAKAAoQCgAKIAAAChAKIAAACiAAMAZAAGAGUAZAAFAAYAowBkAGUAowBlAGcApACjAGcApABnAGgAZgCkAGgAZgBoAGkAagBmAGkAagBpAGsABABqAGsABABrAAcApQA5AG0ApQCmADkApwClAG0ApwBtAKgAqQCmAKUAqQBuAKYAqgClAKcAqgCpAKUAqwCnAKgAqwCoAKwArQCqAKcArQCnAKsArgBuAKkArgCvAG4AbACpAKoAbACuAKkAsACqAK0AsABsAKoAsQCvAK4AsQCyAK8AswCyALEAswA4ALIAtACzALEAtQCxAK4AtACxALUAtQCuAGwAtgC0ALUAtwC1AGwAtgC1ALcAtwBsALAAuAC2ALcAuAC3ALkAuQC3ALAAugC4ALkAuQCwALsAugC5ALwAvAC5ALsANwC6ALwAuwCwAK0ANwC8AL0AvQC8AL4AvAC7AL4AvQC+AL8AuwCtAMAAvgC7AMAAwACtAKsAvwC+AMEAvgDAAMEAvwDBAMIAwACrAMMAwQDAAMMAwwCrAKwAwwCsAG8AwQDDAMQAwgDBAMQAxADDAG8AwgDEAMUAxABvAHAAxQDEAHAAxQBwADoAxgAdAB4AxgAcAB0AxwDGAB4AxwAeACAAHwDHACAAHwAgACEAyAAjACQAyAAiACMAJQDIACQAJQAkACYAJwAoACkAJwAqACgAyQAsAC0AyQArACwAygDJAC0AygAtAC8ALgDKAC8ALgAvADAAMQAyADMAMQA0ADIANAAxADUANQAxADYAPwBAAEEAPwBCAEAAQwBEAEUAQwBGAEQARwBIAEkARwBKAEgASwBMAE0ASwBOAEwASwBPAE4ATwBLAFAAUABLAFEAUgBTAFQAUgBVAFMAVQBSAFYAVgBSAFcAWABZAFoAWABbAFkAWABcAFsAXABYAF0AXQBYAF4AywBgAGEAywBiAGAAXwDLAGEAXwBhAGMA"),
"lods": [0.00012957268, PackedByteArray("AAABAAIAAAACAAMAZAAGAGUAZAAFAAYAZgBkAGUAZgBlAGcAZgBnAGgAZgBoAGkAagBmAGkAagBpAGsABABqAGsABABrAAcAbAA5AG0AbgA5AGwAbAA4AG4AbABtAG8ANwA4AGwANwBsAG8ANwBvAHAANwBwADoAcQByAAgAcQAJAHIACAByAHMAdABxAAgACABzAAoAdAAIAAsAdQB0AAsACgBzAAwAdQALAA0AdgB1AA0ADQALAA4AdgANAA8AdwB2AA8AEAB3AA8AEAAPABEAEAARABIAEQB4ABIAEQAVAHgAFQAMAHgADQAUABUADQAOABQAFQAUABcAFQAXAAwADgATABQADgAKABMACgAMABMAFwAWAAwAEwAMABYAGAB5ABoAGAB6AHkAGAB7AHoAewAZAHoAewAbABkAHAAdAB4AHwAcAB4AHwAeACAAHwAgACEAIgAjACQAJQAiACQAJQAkACYAJwAoACkAJwAqACgAKwAsAC0ALgArAC0ALgAtAC8ALgAvADAAMQAyADMAMQA0ADIANAAxADUANQAxADYAOwA8AD0AOwB8ADwAfAA+ADwAPwBAAEEAPwBCAEAAQwBEAEUAQwBGAEQARwBIAEkARwBKAEgASwBMAE0ASwBOAEwASwBPAE4ATwBLAFAAUABLAFEAUgBTAFQAUgBVAFMAVQBSAFYAVgBSAFcAWABZAFoAWABbAFkAWABcAFsAXABYAF0AXQBYAF4AXwBgAGEAXwBiAGAAXwBhAGMA"), 0.00025139967, PackedByteArray("AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACQAIAAsACgAJAAwACQALAA0ADQALAA4ACQANAA8AEAAJAA8AEAAPABEAEAARABIAEQAMABIADgAKABMACgAMABMADQAOABQADgATABQADQAUABUAEQAVAAwAEwAMABYAFQAUABcAFwAWAAwAFQAXAAwAGAAZABoAGAAbABkAHAAdAB4AHwAcAB4AHwAeACAAHwAgACEAIgAjACQAJQAiACQAJQAkACYAJwAoACkAJwAqACgAKwAsAC0ALgArAC0ALgAtAC8ALgAvADAAMQAyADMAMQA0ADIANAAxADUANQAxADYANwA4ADkANwA5ADoAOwA8AD0AOwA+ADwAPwBAAEEAPwBCAEAAQwBEAEUAQwBGAEQARwBIAEkARwBKAEgASwBMAE0ASwBOAEwASwBPAE4ATwBLAFAAUABLAFEAUgBTAFQAUgBVAFMAVQBSAFYAVgBSAFcAWABZAFoAWABbAFkAWABcAFsAXABYAF0AXQBYAF4AXwBgAGEAXwBiAGAAXwBhAGMA")],
"material": SubResource("StandardMaterial3D_h8qyk"),
"name": "pink.002",
"primitive": 3,
"uv_scale": Vector4(0, 0, 0, 0),
"vertex_count": 204,
"vertex_data": PackedByteArray("AAAAAAAA/7///wAAAAD/v///AAD///+/AAAAAP///7///wAAAABU1f////8AAFTV////////VNX//wAA//9U1czMVNX///////////////+ZmVTV/////8zMqqr/////AAD////////MzP9//////5mZqqr/////zMyqKv///////wAA/////zIzqir/////AAAAAP////9lZlTV/////2Vmqqr/////MjP/f/////8yM1TV/////zIzqqr//////////wAA/78AAP//////v/////////+/AAD//wAA/7/MzKoq//9U1czMqip58FTVmZmqKnnwVNUyM6oq//9U1WVmqip58FTVMjOqKnnwVNUyM6oq////vzIzqip58P+/MjNUVXnw/78yM/9/////vzIz/3958P+/MjOqqv//VNVlZqqq+PNU1TIzqqr481TVZWaqqv//VNUyM/9///9U1TIz/3958FTVZWb/f3nwVNXMzP9///9U1ZmZ/3958FTVzMz/f3nwVNXMzFTV////v8zMqqoo7P+/zMyqqv///7/MzP+/KOz/v8zMqcoo7P+/zMxU1Sjs/78AAAAAAAD/vwAA//8AAP+//////wAA/7///wAAAAD/vwAAAAD//1TVAAD//wAAVNUAAAAAAABU1QAA/////1TVZWZU1f//VNUyM1TV+PNU1WVmVNX481TVMjNU1f//VNVlZqqq////v2VmVNX48/+/ZWaqqvjz/79lZlTV////vzIzVNX///+/MjOqqvjz/78yM1TV+PP/vzIzqqr///+/mZlU1f//VNXMzFTVKOxU1czMVNX//1TVMrNU1SjsVNWpqlTVKOxU1SGiVNUo7FTVmZlU1SjsVNWZmaqq////v5mZVNUo7P+/mZlU1f///7+ZmanKKOz/v5mZ/78o7P+/mZmqqijs/7/MzKqq//9U1ZmZqqoo7FTVmZmqqv//VNUhoqqqKOxU1amqqqoo7FTVMrOqqijsVNXMzKqqKOxU1czMqir///+/zMz/f3nw/7/MzFRVefD/v8zM/3////+/zMyqKnnw/7///1TVAABU1f//VNX//1TV//9UVQAAVNX//6qq//9U1f///3///1TV//9UVf//VNX//6oqAABU1f//qir//1TVZWaqqgAA/7///1TVAAD/v5mZ//8AAP+///9UVQAA/7///6oqAAD/v///VNX/////zMz///////+Zmf//////////qqr/////////f////////1RV////////qir/////AAD/f//////MzP//////v5mZ//////+/mZn//wAA/78AAP9///9U1TIz//8AAP+/MjP//////79lZv//AAD/v2Vm//////+/zMz//wAA/78AAFTV/////zIz////////AACqqv////9lZv///////2Vm/3//////MjNUVf////8AAFVV/////5mZ/3//////AACqKv////8yMwAA/////2VmAAD/////ZWaqKv////+ZmQAA/////5mZqir/////zMwAAP/////MzFRV/////wAAVNX//1TVAABU1QAAVNUAAKqq//9U1QAAqqoAAFTVAAD/fwAAVNUAAFVV//9U1QAAVFUAAFTVAACqKv//VNUAAKoqAABU1czMAAAAAP+/zMwAAP///7+ZmQAAAAD/v5mZAAD///+/ZWYAAAAA/79lZgAA////vzIzAAAAAP+/MjMAAP///7///6qqAABU1f///38AAFTVzMxU1QAA/7/MzP//AAD/v8zMqqoAAP+///+qqgAA/7+ZmVTVAAD/v5mZqqoAAP+/zMz/fwAA/7////9/AAD/v5mZ/38AAP+/ZWZU1QAA/79lZv//AAD/v2Vm/38AAP+/MjNU1QAA/78yM///AAD/vwAAVNUAAP+/AACqqgAA/78yM6qqAAD/vwAA/38AAP+/MjP/fwAA/78AAFRVAAD/vzIzVFUAAP+/AACqKgAA/79lZlRVAAD/vzIzqioAAP+/MjMAAAAA/79lZqoqAAD/v2VmAAAAAP+/mZlUVQAA/7+ZmaoqAAD/v5mZAAAAAP+/zMxUVQAA/7/MzKoqAAD/v8zMAAAAAP+/mZmqKv//VNVlZqoq//9U1TIzVFX///+/ZWb/f///VNWZmf9///9U1czMVFX///+/////f////3////9/////f1TVVNVU1VTVVNVU1VTVVNX/f////3////9/////f////3////9/////f////3////9/////f/7//3////9/////f////3////9/////f///AAD/fwAA/38AAP9/AAD/f1RVVFVUVVRVVFVUVVRVVFVUVVRVVFVUVf9/////f////3////9/////f///VFVUVVRVVFVUVVRVVFVUVaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv9/AAD/fwAA/38AAP9/AAD/fwAA/38AAP////////////////////+qKqoqqiqqKqoqqiqqKqoqqqqqqqqqqqqqqqqqqqqqqv9/AAD/fwAA/38AAP9/AAD/f////3////9/////f///qqqpqqqqqqqqqqqqqqqpqqqqqaqpqqqqqqqqqv9//v//f/7//3////9//v//f/7//3///1VVVFVUVVVVVFVUVVRVVVVVVVRVVVVUVVRVVFX/fwAA/38AAP9/AAD/fwAA/38AAFTVVNVU1VTVVNVU1VTVVNVU1VTVVNVU1VTVVNVU1VTV////////////////////////////f////3////9/////f////3////9/////f////3/+/wAA/38AAP9/AAD/f6oqqioAAP9/AAD/fwAA/38AAP9/AAD/f/9/////f////3////9/////f////3/+//9//v//f////3////9/////f////3////9/////f////3////9///+qKqoqqiqqKqoqqiqqKqoqqiqqKqoqqiqqKqoqqiqqKqoqqir///9/////f////3////9/////f////3////9/////f1TVVNVU1VTV////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VFVUVVRVVFX/f///qqqqqqqqqqr/fwAA")
}, {
"aabb": AABB(-0.16403107, 0.10840821, 0.46973133, 0.36, 0.39999998, 0.03833282),
"attribute_data": PackedByteArray("HQL/QqEPN1MdAjdTJh03UyYd/0IdAsYyJh3GMqEPjiImHY4iHQKOIqwzN1NQO85QMUE3U24+x1BuO6xMMUH/Qqwz/0J1PvZMj0+DUPBSN1OPTzdT8FKDUC5Mg1CPT9dMLkw3U/BSz01sRYNQbEU3U2xFz00uTNdMbEUbSy5MG0tsRf9CLkz/Qo9PG0uPT/9C8FIbS/BS/0KhD/9CoQ/GMljgN1Nk3otPWOCLT2TeN1Pw8NZP4/I3U/DwN1Pj8tZPk+SLT4bmN1OT5DdThuaLT7XsN1PB6tZPtezWT8HqN1NKzDdTlclmT0rMZk+VyTdTpcIVT1rFN1OlwjdTWsUVT3XXL1Ap2jdTddc3UynaL1A60zdThdAZUDrTGVCF0DdT"),
"format": 34896613399,
"index_count": 174,
"index_data": PackedByteArray("CgALAAwADQAMAAsACgAOAAsADwAMAA0ACgAQAA4ADgAQAA8ADwARAA4ADwANABEAEgATABQAEgAVABMAFgASABQAFwAVABIAFgAUABgAFwAZABUAGgAWABgAGgAYABsAHAAWABoAHAAdABYAHgAdABwAHgAfAB0AHwAXAB0AIAAfAB4AIAAhAB8AHwAiABcAIQAiAB8AIgAZABcAIQAjACIAIgAkABkAIwAkACIAIwAlACQAAAABAAIAAAAmAAEAAwABACYABQAmAAAAAwAmAAQABQAnACYAJwAEACYACQAnAAUAJwAGAAQACQAHACcABwAGACcABwAIAAYAKAApACoAKAArACkALAAtAC4ALAAvAC0AMAAxADIAMAAzADEANAA1ADYANAA3ADUAOAA5ADoAOAA7ADkAPAA9AD4APAA/AD0AQABBAEIAQABDAEEARABFAEYARABHAEUA"),
"lods": [0.1056379, PackedByteArray("AAABAAIAAwABAAAAAwAAAAQABQAEAAAABQAGAAQABwAGAAUABwAIAAYACQAHAAUA")],
"material": SubResource("StandardMaterial3D_c6dnh"),
"name": "schwarz",
"primitive": 3,
"uv_scale": Vector4(0, 0, 0, 0),
"vertex_count": 72,
"vertex_data": PackedByteArray("VFUAAJdD//8AAP8/l0P//wAAAACXQ///AAD/f5dD//9UVf9/l0P//6qqAACXQ///qqr/f5dD//////8/l0P//////3+XQ/////8AAJdD//8AAP+/THr//68MKORMev//AAD//0x6///SDOvyTHr//20ituRMev//VFX//0x6//9UVf+/THr//+UgD/NMev//4rj/7wAA//+qqv//AAD//6qq/+8AAP//4rj//wAA///iuP/fAAD//zHM/+8AAP//qqr/3wAA//8bx///AAD//+K4/78AAP//qqr/vwAA//8bx/+/AAD//zHM/98AAP//VNX/vwAA//9U1f/fAAD//////78AAP//////3wAA//9U1f/vAAD//////+8AAP//VNX//wAA////////AAD//1RV/z+XQ///qqr/P5dD///iuP/vAABU1THM/+9hYFTVMcz/7wAAVNXiuP/vYWBU1THM/+8AAP+/Mcz/32Fg/78xzP/fAAD/vzHM/+9hYP+/Mcz/3wAAVNXiuP/fYWBU1eK4/98AAFTVMcz/32FgVNXiuP/fAAD/v+K4/+9hYP+/4rj/7wAA/7/iuP/fYWD/v9IM6/JMeiXV5SAP8///JdXlIA/zTHol1dIM6/L//yXVbSK25Ex6qtSvDCjk//+q1K8MKORMeqrUbSK25P//qtTlIA/zTHoXwG0ituT//xfAbSK25Ex6F8DlIA/z//8XwK8MKORMev+/0gzr8v///7/SDOvyTHr/v68MKOT///+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v/+//7//v1RVVFVUVVRVVFVUVVRVVFX/f////3////9/////f///qqqqqqqqqqqqqqqqqqqqqv9/AAD/fwAA/38AAP9/AABxVRxVcVUcVXFVHFVxVRxVQqp5q0KqeatCqnmrQqp5q850ZvrOdGb6znRm+s50Zvp4fw0BeH8NAXh/DQF4fw0B")
}, {
"aabb": AABB(-0.14619195, 0.45840824, 0.48416355, 0.26931208, 0.029790878, 0.023900598),
"attribute_data": PackedByteArray("CbuLT2q+N1MJuzdTar6LT862GlOwsxRPurZJT7CzN1M="),
"format": 34896613399,
"index_count": 12,
"index_data": PackedByteArray("AAABAAIAAAADAAEABAAFAAYABAAHAAUA"),
"material": SubResource("StandardMaterial3D_i8imj"),
"name": "weiss",
"primitive": 3,
"uv_scale": Vector4(0, 0, 0, 0),
"vertex_count": 8,
"vertex_data": PackedByteArray("//8AAAAA//8v5tPWAAD//y/mAAAAAP/////T1gAA//8uABr+/////xAdVj//////BBv///////8AAOU3//////+//7//v/+//7//v/+//78Xv+fAF7/nwBe/58AXv+fA")
}]
blend_shape_mode = 0
shadow_mesh = SubResource("ArrayMesh_wt8sa")

View file

@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://dgjgtr82ye25d"]
[ext_resource type="ArrayMesh" uid="uid://bi22s15yeh6mw" path="res://02_library/meshes/humanoid/dude.tres" id="1_jchph"]
[node name="Dude" type="MeshInstance3D" unique_id=1185543431]
mesh = ExtResource("1_jchph")

Binary file not shown.

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d1cyxxj8ypfu"
path="res://.godot/imported/logo_projekt_weltenauge.png-050b0b56f8f1da7cb817f7c3bf842b44.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://02_library/ui_assets/icons/program/logo_projekt_weltenauge.png"
dest_files=["res://.godot/imported/logo_projekt_weltenauge.png-050b0b56f8f1da7cb817f7c3bf842b44.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -0,0 +1,38 @@
[gd_scene format=3 uid="uid://bjurf8vfd1fg8"]
[ext_resource type="Script" uid="uid://bhym64rc7xtin" path="res://04_entities/player/player_controller.gd" id="1_pnayr"]
[ext_resource type="PackedScene" uid="uid://dgjgtr82ye25d" path="res://02_library/meshes/humanoid/dude.tscn" id="2_11qf6"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_ktl23"]
[sub_resource type="SystemFont" id="SystemFont_ktl23"]
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_fqfg4"]
[node name="CharacterBody3D" type="CharacterBody3D" unique_id=682006733 groups=["players"]]
script = ExtResource("1_pnayr")
interpolation_speed = 5.0
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=159496934]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.0185384, -0.18054307)
shape = SubResource("CylinderShape3D_ktl23")
[node name="Visuals" type="Node3D" parent="." unique_id=1840405751]
transform = Transform3D(-0.99988204, 0, 0.015358234, 0, 1, 0, -0.015358234, 0, -0.99988204, 0, 0, 0)
[node name="Dude" parent="Visuals" unique_id=1185543431 instance=ExtResource("2_11qf6")]
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1343500738]
root_node = NodePath("../..")
[node name="PlayerNameLabel" type="Label3D" parent="." unique_id=461683617]
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0.73133814, 0)
text = "-label-"
font = SubResource("SystemFont_ktl23")
vertical_alignment = 2
uppercase = true
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." unique_id=656518492]
replication_interval = 0.05
replication_config = SubResource("SceneReplicationConfig_fqfg4")
public_visibility = false

View file

@ -0,0 +1,137 @@
extends CharacterBody3D
@onready var synchronizer = $MultiplayerSynchronizer
@export var target_position: Vector3
@export var sync_position: Vector3
@export var sync_rotation: Vector3
@export var interpolation_speed = 15.0 # Geschwindigkeit für die Glättung
var path: PackedVector3Array = []
var speed = 5.0
func _enter_tree():
# Da der Name des Nodes die Peer-ID ist, setzen wir die Authority einfach selbst
if name.is_valid_int():
#set_multiplayer_authority(name.to_int())
set_multiplayer_authority(1)
func _ready():
# --- START: Dynamische Replikations-Konfiguration ---
# Eine neue Konfigurations-Ressource im Code erstellen.
var config = SceneReplicationConfig.new()
# Die zu synchronisierenden Eigenschaften hinzufügen. Der Pfad '..:sync_position' bedeutet:
# '..': Gehe zum Eltern-Node (dem CharacterBody3D).
# ':': Greife auf eine Eigenschaft dieses Nodes zu.
# 'sync_position': Der Name der Eigenschaft.
config.add_property(NodePath(":sync_position"))
config.add_property(NodePath(":sync_rotation"))
config.add_property(NodePath("PlayerNameLabel:text"))
# Die generierte Konfiguration dem Synchronizer zuweisen. Dies überschreibt alle Editor-Einstellungen.
synchronizer.replication_config = config
# --- ENDE: Dynamische Replikations-Konfiguration ---
target_position = global_position
# Wir schalten die automatische Physik-Synchronisation aus
set_physics_process(multiplayer.is_server())
func _physics_process(delta):
# Dieser Teil läuft NUR auf dem Server
if path.size() > 0:
var next_point = path[0]
var direction = (next_point - global_position).normalized()
# Richte den Avatar in die Bewegungsrichtung aus.
look_at(next_point, Vector3.UP)
# Wir nutzen move_and_slide, damit Kollisionen (Wände) funktionieren
velocity = direction * speed
move_and_slide()
# Nach der Bewegung speichern wir das Ergebnis für die Clients
sync_position = global_position
sync_rotation = rotation
if global_position.distance_to(next_point) < 0.2:
path.remove_at(0)
else:
velocity = Vector3.ZERO
sync_position = global_position # Sicherstellen, dass Standbild syncron bleibt
@rpc("any_peer", "call_remote", "reliable")
func srv_request_move(target: Vector3):
if not multiplayer.is_server(): return
#print("Server: Request Move received from client")
# Sicherheits-Check: Nur der Besitzer des Avatars darf ihn bewegen
#if multiplayer.get_remote_sender_id() != get_multiplayer_authority():
# return
# Hier später: path = astar.get_point_path(global_position, target)
#path = PackedVector3Array([target])
print("Server Move to Target: ", target)
path = [target]
target_position = target
# ERSETZT die alte _input(event)-Funktion
func _input(event):
# Ist dieser Avatar nicht der des lokalen Spielers? Wenn ja, sofort abbrechen.
if name.to_int() != multiplayer.get_unique_id(): return
if DisplayServer.get_name() == "headless": return # Server ignoriert Klicks
if multiplayer.is_server(): return # Kein Input vom Server
# Wir reagieren nur auf den Klick der linken Maustaste
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
var click_result = _get_click_result()
if click_result: # Prüfen, ob der Strahl überhaupt etwas getroffen hat
var collider = click_result.collider
# PRIORITÄT 1: Haben wir ein Portal angeklickt?
# Wir prüfen, ob das getroffene Objekt gültig ist und zur Gruppe "portals" gehört.
if is_instance_valid(collider) and collider.is_in_group("portals"):
print("Portal angeklickt. Fordere Area-Wechsel an zu: ", collider.target_area_key)
# Sende RPC direkt an den NetworkManager mit dem Ziel des Portals
NetworkManager.srv_handle_area_change_request.rpc_id(1, collider.target_area_key)
return # Wichtig: Aktion ist beendet, nichts weiter tun.
# PRIORITÄT 2 (Fallback): Wir haben etwas anderes getroffen (vermutlich den Boden), also bewegen.
var target_position = click_result.position
srv_request_move.rpc_id(1, target_position)
# ERSETZT die alte _get_click_floor_pos()-Funktion
func _get_click_result():
var camera = get_viewport().get_camera_3d()
if not camera: return null
var mouse_pos = get_viewport().get_mouse_position()
var space_state = get_world_3d().direct_space_state
var ray_origin = camera.project_ray_origin(mouse_pos)
var ray_dir = camera.project_ray_normal(mouse_pos)
var query = PhysicsRayQueryParameters3D.create(ray_origin, ray_origin + ray_dir * 1000)
# WICHTIG: Schließe den eigenen Körper von der Abfrage aus, um nicht auf sich selbst zu klicken.
query.exclude = [self]
# NEU: Sage dem Raycast, dass er auch mit Area3D-Nodes kollidieren soll.
query.collide_with_areas = true
var result = space_state.intersect_ray(query)
return result # Gibt das komplette Ergebnis-Dictionary zurück
func _process(_delta):
# Dieser Teil läuft auf dem Client (besonders dem, der die Authority hat)
if not multiplayer.is_server():
# Bewege die lokale Position sanft in Richtung der vom Server vorgegebenen Position.
global_position = global_position.lerp(sync_position, _delta * interpolation_speed)
# Rotiere sanft in Richtung der vom Server vorgegebenen Rotation.
rotation = rotation.lerp(sync_rotation, _delta * interpolation_speed)

View file

@ -0,0 +1 @@
uid://bhym64rc7xtin

View file

@ -0,0 +1,16 @@
extends Control
@onready var ip_input: TextEdit = $IPInput
func _on_host_button_pressed() -> void:
print("host pressed")
GameEvents.ui_host_requested.emit()
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
hide()
func _on_join_button_pressed():
var username = $UsernameLineEdit.text
var password = $PasswordLineEdit.text # TODO: Handle Password
GameEvents.ui_join_requested.emit(ip_input.text, username, password)
hide()

View file

@ -0,0 +1 @@
uid://c6yngwnpqsnpi

View file

@ -0,0 +1,82 @@
[gd_scene format=3 uid="uid://lb6btb6426mi"]
[ext_resource type="Script" uid="uid://c6yngwnpqsnpi" path="res://04_entities/ui/windows/account/login.gd" id="1_x74xt"]
[node name="Login" type="Control" unique_id=1694671237]
layout_mode = 3
anchors_preset = 0
offset_left = 23.0
offset_top = 102.0
offset_right = 23.0
offset_bottom = 102.0
script = ExtResource("1_x74xt")
[node name="IPInput" type="TextEdit" parent="." unique_id=1340335140]
custom_minimum_size = Vector2(150, 40)
layout_mode = 0
offset_left = 375.0
offset_top = -70.0
offset_right = 525.0
offset_bottom = -30.0
text = "127.0.0.1"
editable = false
[node name="Join_Button" type="Button" parent="." unique_id=811892478]
layout_mode = 0
offset_left = 30.0
offset_top = 12.0
offset_right = 231.0
offset_bottom = 43.0
text = "Join"
[node name="Host_Button" type="Button" parent="." unique_id=1465654892]
layout_mode = 0
offset_left = 313.0
offset_top = 11.0
offset_right = 530.0
offset_bottom = 42.0
text = "Host"
[node name="lb_User" type="Label" parent="." unique_id=1356001317]
layout_mode = 0
offset_left = 21.0
offset_top = -72.0
offset_right = 62.0
offset_bottom = -49.0
text = "User:"
[node name="lb_User" type="Label" parent="lb_User" unique_id=463394394]
layout_mode = 0
offset_left = 286.0
offset_top = 11.0
offset_right = 340.0
offset_bottom = 34.0
text = "Server:"
[node name="lb_Password" type="Label" parent="." unique_id=1027088176]
layout_mode = 0
offset_left = 18.0
offset_top = -39.0
offset_right = 94.0
offset_bottom = -16.0
text = "Passwort:"
[node name="UsernameLineEdit" type="LineEdit" parent="." unique_id=1896094376]
layout_mode = 0
offset_left = 122.0
offset_top = -72.0
offset_right = 231.0
offset_bottom = -41.0
text = "test_user"
[node name="PasswordLineEdit" type="LineEdit" parent="." unique_id=214865529]
layout_mode = 0
offset_left = 121.0
offset_top = -38.0
offset_right = 230.0
offset_bottom = -7.0
text = "test_pw"
[connection signal="pressed" from="Join_Button" to="." method="_on_join_button_pressed"]
[connection signal="button_up" from="Host_Button" to="." method="_on_host_pressed"]
[connection signal="pressed" from="Host_Button" to="." method="_on_host_button_pressed"]

View file

@ -0,0 +1,22 @@
extends Control
@onready var name_line_edit: LineEdit = $NameLineEdit
func _ready():
GameEvents.ui_create_character_screen.connect(show_creation_screen)
func show_creation_screen():
name_line_edit.text = ""
show()
func _on_create_button_pressed() -> void:
var new_character = CharacterData.new()
new_character.name = name_line_edit.text
print("Create Button pressed")
GameEvents.ui_character_created.emit(new_character)
hide()
func _on_cancel_button_pressed() -> void:
get_parent().get_node("CharacterSelect").show()
hide()

View file

@ -0,0 +1 @@
uid://dx24rxnsd0eoj

View file

@ -0,0 +1,42 @@
[gd_scene format=3 uid="uid://cdwkmx452hc4k"]
[ext_resource type="Script" uid="uid://dx24rxnsd0eoj" path="res://04_entities/ui/windows/character_create/character_create.gd" id="1_mdh5q"]
[node name="CharacterCreate" type="Control" unique_id=2056198803]
layout_mode = 3
anchors_preset = 0
script = ExtResource("1_mdh5q")
[node name="NameLabel" type="Label" parent="." unique_id=1413425912]
layout_mode = 0
offset_left = 24.0
offset_top = 32.0
offset_right = 82.0
offset_bottom = 61.0
text = "Name:"
[node name="CreateButton" type="Button" parent="." unique_id=50672893]
layout_mode = 0
offset_left = 29.0
offset_top = 81.0
offset_right = 140.0
offset_bottom = 117.0
text = "Erstellen"
[node name="CancelButton" type="Button" parent="." unique_id=861690836]
layout_mode = 0
offset_left = 175.0
offset_top = 80.0
offset_right = 286.0
offset_bottom = 116.0
text = "Abbrechen"
[node name="NameLineEdit" type="LineEdit" parent="." unique_id=2114380893]
layout_mode = 0
offset_left = 89.0
offset_top = 29.0
offset_right = 288.0
offset_bottom = 60.0
[connection signal="pressed" from="CreateButton" to="." method="_on_create_button_pressed"]
[connection signal="pressed" from="CancelButton" to="." method="_on_cancel_button_pressed"]

View file

@ -0,0 +1,31 @@
extends Control
@onready var character_item_list: ItemList = $CharacterItemList
var characters: Array[CharacterData] = []
func _ready():
GameEvents.network_show_characters.connect(show_list)
GameEvents.network_new_character_received.connect(receive_new_character)
func receive_new_character(character: CharacterData):
characters.append(character)
character_item_list.add_item(character.name)
show()
func show_list(characters: Array):
print("show_list triggered")
character_item_list.clear()
for character: CharacterData in characters:
character_item_list.add_item(character.name)
show()
func _on_create_character_button_pressed() -> void:
GameEvents.ui_create_character_screen.emit()
hide()
# Doubleclick on Character should start area transition
func _on_character_item_list_item_activated(index: int) -> void:
print("Doubleclick for index=", index)
var item = character_item_list.get_selected_items()[0]
print("=>",character_item_list.get_item_text(item))

View file

@ -0,0 +1 @@
uid://b5htxbkq23mo6

View file

@ -0,0 +1,33 @@
[gd_scene format=3 uid="uid://ca2bhph2k3xpy"]
[ext_resource type="Script" uid="uid://b5htxbkq23mo6" path="res://04_entities/ui/windows/character_select/character_select.gd" id="1_o7qwi"]
[node name="CharacterSelect" type="Control" unique_id=2056198803]
layout_mode = 3
anchors_preset = 0
script = ExtResource("1_o7qwi")
[node name="CharacterItemList" type="ItemList" parent="." unique_id=1442954519]
layout_mode = 0
offset_top = 32.0
offset_right = 368.0
offset_bottom = 571.0
[node name="Label" type="Label" parent="." unique_id=1413425912]
layout_mode = 0
offset_left = 111.0
offset_top = 2.0
offset_right = 217.0
offset_bottom = 31.0
text = "Charaktere:"
[node name="CreateCharacterButton" type="Button" parent="." unique_id=50672893]
layout_mode = 0
offset_left = 113.0
offset_top = 585.0
offset_right = 224.0
offset_bottom = 621.0
text = "Neu"
[connection signal="item_activated" from="CharacterItemList" to="." method="_on_character_item_list_item_activated"]
[connection signal="pressed" from="CreateCharacterButton" to="." method="_on_create_character_button_pressed"]

View file

@ -0,0 +1,61 @@
[gd_scene format=3 uid="uid://h5qt2pcidh8x"]
[ext_resource type="PackedScene" uid="uid://coq1j1cdrvqyn" path="res://05_worlds/prefabs/portal/portal.tscn" id="1_1yg6u"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_d3jw7"]
[sub_resource type="Sky" id="Sky_1yg6u"]
sky_material = SubResource("ProceduralSkyMaterial_d3jw7")
[sub_resource type="Environment" id="Environment_8r0t0"]
background_mode = 2
sky = SubResource("Sky_1yg6u")
tonemap_mode = 2
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_30klc"]
albedo_color = Color(9.3854965e-07, 0.4579985, 0.32070312, 1)
[sub_resource type="PlaneMesh" id="PlaneMesh_0iae2"]
material = SubResource("StandardMaterial3D_30klc")
[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_1yg6u"]
data = PackedVector3Array(1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, -1, 0, -1, 1, 0, -1)
[node name="keuwin_grasland" type="Node3D" unique_id=1953380732]
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1262732162]
transform = Transform3D(-0.8660254, -0.3535534, 0.3535534, 0, 0.70710677, 0.70710677, -0.50000006, 0.6123724, -0.6123724, 0, 0, 0)
light_color = Color(0.90022373, 0.9250896, 0.6778064, 1)
light_energy = 1.5
shadow_enabled = true
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1286599151]
environment = SubResource("Environment_8r0t0")
[node name="Architecture" type="Node3D" parent="." unique_id=97923552]
[node name="MeshInstance3D" type="MeshInstance3D" parent="Architecture" unique_id=1708107393]
transform = Transform3D(10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0)
mesh = SubResource("PlaneMesh_0iae2")
[node name="StaticBody3D" type="StaticBody3D" parent="Architecture/MeshInstance3D" unique_id=209028061]
[node name="CollisionShape3D" type="CollisionShape3D" parent="Architecture/MeshInstance3D/StaticBody3D" unique_id=1119853048]
shape = SubResource("ConcavePolygonShape3D_1yg6u")
[node name="Navigation" type="Node3D" parent="." unique_id=1472582667]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.01873529, 0)
[node name="SpawnPoints" type="Node3D" parent="." unique_id=438053421]
[node name="Interaction" type="Node3D" parent="." unique_id=1592705952]
[node name="Triggers" type="Node3D" parent="." unique_id=1507691477]
[node name="Portal" parent="." unique_id=988801188 instance=ExtResource("1_1yg6u")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -6)
target_area_key = "ahnenhalle"
[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="." unique_id=58641401]
_spawnable_scenes = PackedStringArray("uid://bjurf8vfd1fg8")
spawn_path = NodePath("../SpawnPoints")

View file

@ -0,0 +1,65 @@
[gd_scene format=3 uid="uid://bef0tj7n6g8uo"]
[ext_resource type="PackedScene" uid="uid://coq1j1cdrvqyn" path="res://05_worlds/prefabs/portal/portal.tscn" id="1_4r2hu"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_d3jw7"]
sky_top_color = Color(0.055328622, 0.3637527, 0.67898166, 1)
ground_bottom_color = Color(0.28193337, 0.24096331, 0.19489476, 1)
ground_horizon_color = Color(0.5439266, 0.5434616, 0.51488423, 1)
[sub_resource type="Sky" id="Sky_1yg6u"]
sky_material = SubResource("ProceduralSkyMaterial_d3jw7")
[sub_resource type="Environment" id="Environment_8r0t0"]
background_mode = 2
sky = SubResource("Sky_1yg6u")
tonemap_mode = 2
fog_enabled = true
fog_light_color = Color(0.061652586, 0.3592321, 0.673373, 1)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_30klc"]
albedo_color = Color(0.1800012, 0.004964735, 0.2305904, 1)
[sub_resource type="PlaneMesh" id="PlaneMesh_0iae2"]
material = SubResource("StandardMaterial3D_30klc")
[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_0hy1c"]
data = PackedVector3Array(1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, -1, 0, -1, 1, 0, -1)
[node name="halle_der_ahnen" type="Node3D" unique_id=1953380732]
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1262732162]
transform = Transform3D(-0.8660254, -0.12940952, 0.48296294, 0, 0.96592575, 0.258819, -0.50000006, 0.22414385, -0.8365162, 0, 0, 0)
light_color = Color(0.94507605, 0.7726238, 0.95401555, 1)
light_energy = 0.5
shadow_enabled = true
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1286599151]
environment = SubResource("Environment_8r0t0")
[node name="Architecture" type="Node3D" parent="." unique_id=97923552]
[node name="MeshInstance3D" type="MeshInstance3D" parent="Architecture" unique_id=1708107393]
transform = Transform3D(10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0)
mesh = SubResource("PlaneMesh_0iae2")
[node name="StaticBody3D" type="StaticBody3D" parent="Architecture/MeshInstance3D" unique_id=1395982254]
[node name="CollisionShape3D" type="CollisionShape3D" parent="Architecture/MeshInstance3D/StaticBody3D" unique_id=521000493]
shape = SubResource("ConcavePolygonShape3D_0hy1c")
[node name="Navigation" type="Node3D" parent="." unique_id=1472582667]
[node name="SpawnPoints" type="Node3D" parent="." unique_id=438053421]
[node name="Interaction" type="Node3D" parent="." unique_id=1592705952]
[node name="Triggers" type="Node3D" parent="." unique_id=1507691477]
[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="." unique_id=1610787579]
_spawnable_scenes = PackedStringArray("uid://bjurf8vfd1fg8")
spawn_path = NodePath("../SpawnPoints")
[node name="Portal" parent="." unique_id=988801188 instance=ExtResource("1_4r2hu")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.1332607, 0.014829755, 8.73144)
target_area_key = "keuwin_grasland"

View file

@ -0,0 +1,4 @@
extends Area3D
# Dieser Wert muss einem Schlüssel in NetworkManager.area_paths entsprechen (z.B. "ahnenhalle").
@export var target_area_key: String = ""

View file

@ -0,0 +1 @@
uid://c23uu6akws8nj

View file

@ -0,0 +1,19 @@
[gd_scene format=3 uid="uid://coq1j1cdrvqyn"]
[ext_resource type="Script" uid="uid://c23uu6akws8nj" path="res://05_worlds/prefabs/portal/portal.gd" id="1_8dnr8"]
[sub_resource type="BoxShape3D" id="BoxShape3D_h1tlh"]
size = Vector3(3.2, 1, 1)
[sub_resource type="BoxMesh" id="BoxMesh_gtfyq"]
[node name="Portal" type="Area3D" unique_id=988801188 groups=["portals"]]
script = ExtResource("1_8dnr8")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1301383310]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.50266325, 0)
shape = SubResource("BoxShape3D_h1tlh")
[node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=1902207257]
transform = Transform3D(3.2577846, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.48517025, 0)
mesh = SubResource("BoxMesh_gtfyq")

View file

@ -0,0 +1,185 @@
## 1. Projektziel & Vision
"weltenauge" ist ein von Neverwinter Nights (NWN) inspiriertes Multiplayer-Rollenspiel. Das Kernstück ist eine robuste Client/Server-Architektur, die in einer einzigen Godot-Codebase umgesetzt wird.
## 2. Kernkonzepte der Architektur
### Client/Server-Modell
- **Eine Codebase:** Client und Server sind Teil desselben Godot-Projekts.
- **Start-Logik:**
- Im normalen Modus startet das Spiel mit einem Menü, das "Host" und "Beitreten" anbietet.
- Im Headless-Modus (`--headless`) startet das Programm automatisch als dedizierter Server (Host).
- **Verantwortlichkeiten:** Der Server (Host) ist die Autorität für den Spielzustand. Clients empfangen und visualisieren den Zustand.
### Dynamisches Area-Management
- **Serverseitig:** Der Server lädt **alle** von Spielern bewohnten Areas (Szenen) gleichzeitig.
- **Clientseitig:** Ein Client lädt **nur** die eine Area, in der sich sein Charakter gerade befindet. Dies spart Ressourcen auf den Client-Rechnern.
- **Area-Wechsel:** Ein Wechsel wird immer vom Server initiiert. Der Server lädt die neue Szene für den Spieler, platziert dessen Avatar und sendet ein Signal an den Client. Der Client lädt daraufhin ebenfalls die neue Szene.
### Isolierte Welten auf dem Server
## 3. Geplante Ordnerstruktur
Alle Skripte und die dazugehörige Logik sollen thematisch in Unterordnern unter `res://systems/` abgelegt werden:
/project_root
│
├── 01_core/                     # Die Logik (Script-only, Architektur)
│   ├── systems/                 # Globale Manager (Autoloads): network.gd, game_state.gd, signal_bus.gd
│   ├── components/              # Logik-Nodes: health_component.gd, interactor.gd, inventory_controller.gd
│   └── base_classes/            # Definitionen & Klassen: item_data.gd, spell_base.gd, entity_base.gd
│
├── 02_library/                  # Das Rohmaterial (Assets & Bausteine)
│   ├── meshes/                  # 3D-Importe (.glb, .obj) nach Kategorien: nature/, architecture/
│ ├── mesh_libraries/ # Deine fertigen GridMap-Kataloge (.tres)
│   ├── materials/               # Globale Materialien (.tres): wood.tres, stone.tres, metal.tres
│   ├── textures/                # Roh-Bilder für Materialien & Shader
│   ├── ui_assets/               # Visuelles Interface: icons/, fonts/, themes/
│   ├── shaders/                 # Shader-Dateien (.gdshader)
│   ├── vfx/                     # Partikelsysteme und visuelle Effekte
│   └── audio/                   # Sound-Dateien: sfx/, music/, ambient/
│
├── 03_database/                 # Die Spieldaten (Content-Definitionen)
│   ├── items/                   # Hunderte .tres Dateien (iron_sword.tres, health_potion.tres)
│   ├── spells/                  # Zaubersprüche als Resources (.tres)
│   ├── loot_tables/             # Definitionen für Drops und Wahrscheinlichkeiten
│   ├── dialogues/               # Dialog-Skripte oder JSON-Dateien
│   ├── localization/            # Übersetzungsdateien (.csv, .po)
│ └── spawn_tables/ # Definitionen: Welche Monster-Gruppe spawnt in welcher Area?
│
├── 04_entities/                 # Die Spielobjekte (Zusammengesetzte Szenen)
│   ├── player/                  # Spieler-Szene (player.tscn + player.gd)
│   ├── npcs/                    # Monster, Händler, KI-Gegner
│   ├── props/                   # Interaktive Welt-Objekte: chest.tscn, door.tscn
│   ├── pickups/                 # Das world_item.tscn (Visualisierung von Database-Items)
│   ├── ui/                      # UI-Szenen: hud/, windows/, menus/, shared/
│ └── markers/ # Unsichtbare Helfer: spawn_point.tscn, area_trigger.tscn
│
├── 05_worlds/                   # Die Level (Das "NWN Modul")
│   ├── areas/                   # Die eigentlichen Karten/Level (.tscn)
│   ├── prefabs/                 # Wiederverwendbare Szenen-Gruppen (Häuser, Lagerfeuer-Sets)
│   └── environments/            # Licht- & Welt-Einstellungen (.tres)
│
└── 06_tests/                    # Die Sandkästen (Isolierte Prototypen)
    ├── test_ui.tscn             # Nur UI-Fenster testen
    ├── test_combat.tscn         # Kampf-Mechanik ohne Level testen
    └── test_inventory.tscn      # Item-Logik prüfen
## 4. Wichtige Dateien & Szenen
- **Spieler-Modell:** `res://systems/player/player.tres`
- **Area 1:** `res://maps/outdoor/grass/keuwin_grasland.tscn`
- **Area 2:** `res://maps/planes/fugue/halle_der_ahnen.tscn`
- **Startpunkt:** `main.tscn` (vermutlich für das Hauptmenü)
- **Haupt-Logik:** `game_root.gd` / `game_root.tscn` (vermutlich die Wurzel des aktiven Spiels)
## 5. Implementierungsdetails & Konventionen
- **Click-to-Move:** Die Spielerbewegung soll aus Performancegründen über eine AStar-Implementierung (`systems/navigation/`) gesteuert werden.
- **Version:** Basis ist **Godot 4.6**. Nutze aktuelle APIs (besonders für Navigation und Multiplayer).
- **GDScript-Regel (No Async Keyword):** Das Schlüsselwort `async` darf **nicht** verwendet werden. Funktionen mit `await` sind automatisch asynchron.
- - *Korrekt:* `func move(): await get_tree().process_frame`
- **Statische Typisierung:** Nutze konsequent Typ-Hints (z.B. `var health: int = 100`), um Fehlerquellen zu minimieren.
- **Node-Referenzen:** Nutze `@onready var` und relative Pfade. Vermeide `/root/`-Pfade wegen der SubViewport-Struktur.
## 6. Globale Skripte (Autoloads)
Die folgenden Skripte sind als Singletons (Autoloads in Godot) konfiguriert und global verfügbar:
- **`GameEvents`** (`res://systems/core/game_events.gd`): Zentraler Event-Bus für die Kommunikation zwischen entkoppelten Systemen.
- `/data/`: Speicher-Logik, Daten-Wrapper (z.B. für JSON, SQLite).
- **`ChatManager`** (`res://systems/network/chat_manager.gd`): Kümmert sich um die Logik des In-Game-Chats.
## 7. Analyse der existierenden Skripte (Stand: Commit 30b6ade)
- `/data/`: Speicher-Logik, Daten-Wrapper (z.B. für JSON, SQLite).
Dieser Abschnitt beschreibt die Verantwortlichkeiten der zentralen Skripte im Projekt.
- **Rolle:** Absoluter Startpunkt des Spiels.
- **Verantwortlichkeit:** Hält die Instanz des Hauptmenüs (`main.tscn`). Seine einzige Aufgabe ist es, zu existieren und die initiale Szene zu laden. Die eigentliche Logik wird sofort an die globalen Manager und das UI delegiert.
### `/systems/core/game_events.gd` (Autoload: `GameEvents`)
- **Rolle:** Globaler Event-Bus.
### `/systems/network/network_manager.gd` (Autoload: `NetworkManager`)
- **Rolle:** Das Herzstück der gesamten Anwendung.
- **Verantwortlichkeit:**
- **Startlogik:** Prüft in `_ready()` selbst, ob das Spiel headless gestartet wurde und ruft dann `host_game()` auf.
- **Verbindungs-Management:** Startet den Server (`host_game`) oder verbindet als Client (`join_game`).
- **Area-Management:** Lädt und verwaltet die Spielwelten (Areas). Erstellt für jede Area auf dem Server einen eigenen `SubViewport` mit `own_world_3d = true`, um die Physikwelten zu trennen.
- **Spieler-Spawning:** Koordiniert den komplexen Prozess des Spieler-Spawns. Weist den Client an, eine Szene zu laden (`client_load_area`), wartet auf dessen Bereitschaftsmeldung (`client_ready_to_spawn`) und instanziiert dann den Spieler-Avatar serverseitig in der korrekten Sub-Welt.
### `/systems/player/player_controller.gd`
- **Rolle:** Steuerungslogik für den Spieler-Avatar.
- **Verantwortlichkeit:** Dieses Skript läuft auf dem Server und allen Clients, verhält sich aber je nach Autorität unterschiedlich:
- **Client (mit Autorität):** Fängt Mauseklicks ab (`_input`), berechnet die Zielposition auf dem Boden und sendet einen `request_move` RPC an den Server.
- **Server:** Empfängt `request_move`, berechnet den Pfad (aktuell nur direktes Ziel) und bewegt den `CharacterBody3D` mittels `move_and_slide()` im `_physics_process`. Die resultierende Position und Rotation werden in `sync_position` und `sync_rotation` gespeichert.
- **Client (ohne Autorität):** Liest im `_process` kontinuierlich `sync_position` und `sync_rotation` aus und setzt die `global_position` und `rotation` des Avatars hart auf diese Werte, um ihn mit dem Server synchron zu halten.
### `/systems/player/camera_logic.gd`
- **Rolle:** Kamera-Steuerung.
- **Verantwortlichkeit:** Ein einfaches Skript, das an einer `Camera3D` hängt. Es verfolgt sanft (`lerp`) ein `target` (den Spieler-Avatar) und behält dabei einen festen `offset` bei, um eine klassische Top-Down-Perspektive zu erzeugen. Die Zuweisung des Ziels erfolgt durch den `NetworkManager` über den `assign_camera_target` RPC.
### `/main.tscn`
- **Rolle:** Hauptmenü-UI.
- **Verantwortlichkeit:** Enthält die UI-Elemente wie "Host" und "Join" Buttons. Es wird angenommen, dass die `pressed()` Signale dieser Buttons direkt im Godot-Editor mit den entsprechenden Signalen im `GameEvents`-Singleton (`host_requested`, `join_requested`) verbunden sind.
## 8. Wichtige Erkenntnisse & Best Practices
* **MultiplayerSynchronizer:** Konfiguration **nur per Code** in `_ready()`.
* **Property-Syntax:** Pfade in der `SceneReplicationConfig` müssen mit Doppelpunkt starten, wenn sie sich auf den Root-Pfad beziehen (z.B. `config.add_property(":sync_position")`).
* **MultiplayerSpawner:** Nutze einen generischen Spawner in der Basis-Level-Szene für automatische Synchronisation bei Late-Joinern.
## 7. Anweisungen für die KI (Refactoring & Generierung)
* **Thinking-Phase:** Analysiere vor der Code-Ausgabe, ob Änderungen die SubViewport-Isolation oder die Server-Autorität beeinflussen könnten.
* **Minimaler Eingriff:** Verändere nur angeforderte Logik. Behalte Kommentare und Signal-Strukturen bei.
- **Spieler-Spawning:** Koordiniert den komplexen Prozess des Spieler-Spawns. Weist den Client an, eine Szene zu laden (`client_load_area`), wartet auf dessen Bereitschaftsmeldung (`client_ready_to_spawn`) und instanziiert dann den Spieler-Avatar serverseitig in der korrekten Sub-Welt.
Es gibt kein async Keyword, aber await funktioniert trotzdem. => await get_tree().process_frame
- **Spieler-Spawning:** Koordiniert den komplexen Prozess des Spieler-Spawns. Weist den Client an, eine Szene zu laden (`client_load_area`), wartet auf dessen Bereitschaftsmeldung (`client_ready_to_spawn`) und instanziiert dann den Spieler-Avatar serverseitig in der korrekten Sub-Welt.
Die Konfiguration des `MultiplayerSynchronizer` für wiederverwendbare Szenen (wie den Spieler) ist kritisch und muss korrekt gehandhabt werden, um Timing- und Pfad-Probleme zu vermeiden. Die folgenden vier Regeln sind entscheidend:
1. **Konfiguration nur per Code:** Die Konfiguration muss zur Laufzeit im Code erfolgen, nicht im Editor. Der Editor speichert absolute Pfade, die in einem dynamischen Szenen-Setup (mehrere Welten/Viewports) unbrauchbar sind.
2. **Ort der Konfiguration: `_ready()`:** Die Logik zur Erstellung der `SceneReplicationConfig` gehört in die `_ready()`-funktion des Skripts, dessen Eigenschaften synchronisiert werden sollen (z.B. `player_controller.gd`).
3. **Korrekte Eigenschafts-Syntax:** Der Pfad zu einer Eigenschaft, die sich direkt auf dem `root_path`-Node des Synchronizers befindet (standardmäßig sein Parent), **muss mit einem Doppelpunkt beginnen**.
- **Korrekt:** `config.add_property(":sync_position")`
- **Falsch:** `config.add_property("sync_position")` oder `config.add_property("..:sync_position")`
- **Falsch:** `config.add_property("sync_position")` oder `config.add_property("..:sync_position")`
### Dynamische Konfiguration des MultiplayerSpawner
- Handhabt nicht nur originäre Objekterzeugung in den Levels, sondern auch Objektaktualisierungen für "Late Joiner"
- Generischer MultiplayerSpawner in Basis Level, von dem alle Level erben, so dass die Spawnliste nur 1x bearbeitet werden muss
## Namenskonventionen
Signale:
Verwenden Sie einheitliche Präfixe, um die Quelle des Signals anzugeben (z.B. player_, ui_, network_).
Verwenden Sie Verben in der Vergangenheitsform, um anzuzeigen, dass ein Ereignis stattgefunden hat (z.B. player_moved, ui_buttonPressed, network_connected).
RPCs:
An den Server gerichtet RPCs beginnen mit srv_ und an den Client gerictete RPCs beginnen mit cl_
Variablen:
- snake_case für lokale Variablen und Funktionsnamen (z.B. player_health, calculate_damage).
- PascalCase für Klassennamen und Ressourcen (z.B. CharacterData, MyCustomShader).
- Konstanten in GROSSBUCHSTABEN (z.B. MAX_HEALTH, DEFAULT_SPEED).
## 9. Reihenfolge rpc Methoden
- Programmstart: Autoload Singletons: NetworkManager, GameEvents und ChatManager werden instanziert.
-- Wenn der NetworkManager erkennt, dass er headless ist, startet er automatisch den Server
- Die Haupt Szene startet: game_root.tscn - in ihr sind alle UI Scenen eingebettet. Bis auf eine ( Login ) sind alle hidden.
-- Login beinhaltet eine UI, in der man Host oder Join anwählen kann. Zu join gehört auch noch Username und Passwort
- Es wird Join geklickt:
* GameEvents.ui_join_requested.emit(ip_input.text, username, password)
* Der NetworkManager des Clients hat das Signal abonniert und startet join_game, das den MultiplayerPeer instanziert.
* Der NetworkManager des Servers empfängt das Signal _on_player_connected aktiviert auf dem Client
=> cl_show_characters.rpc_id(id, []) , was auf dem Client die Charakterliste anzeigt( die aktuell leer ist).
In der Charakterliste kann über einen Button ein neuer Charakter angelegt werden. Der Button
löst aus: GameEvents.create_character_screen.emit() , der von CharacterCreate abonniert ist, und dort die
Methode show_creation_screen auslöst.
** Wird das Formular ausgefüllt und Create angeklickt, löst dies die an den Server gerichtete rpc store_player_charakter aus.

4
weltenauge/game_root.gd Normal file
View file

@ -0,0 +1,4 @@
extends Node
func _ready():
print("--- Start: Projekt Weltenauge ---")

View file

@ -0,0 +1 @@
uid://162cjpb4lac4

17
weltenauge/game_root.tscn Normal file
View file

@ -0,0 +1,17 @@
[gd_scene format=3 uid="uid://cpxxy0664jyjm"]
[ext_resource type="Script" uid="uid://162cjpb4lac4" path="res://game_root.gd" id="1_xj5yv"]
[ext_resource type="PackedScene" uid="uid://ca2bhph2k3xpy" path="res://04_entities/ui/windows/character_select/character_select.tscn" id="3_bxsma"]
[ext_resource type="PackedScene" uid="uid://lb6btb6426mi" path="res://04_entities/ui/windows/account/login.tscn" id="3_lkrbl"]
[ext_resource type="PackedScene" uid="uid://cdwkmx452hc4k" path="res://04_entities/ui/windows/character_create/character_create.tscn" id="4_lkrbl"]
[node name="Game_root" type="Node2D" unique_id=1336425327]
script = ExtResource("1_xj5yv")
[node name="Login" parent="." unique_id=1694671237 instance=ExtResource("3_lkrbl")]
[node name="CharacterSelect" parent="." unique_id=2079351175 instance=ExtResource("3_bxsma")]
visible = false
[node name="CharacterCreate" parent="." unique_id=2056198803 instance=ExtResource("4_lkrbl")]
visible = false

8
weltenauge/main.tscn Normal file
View file

@ -0,0 +1,8 @@
[gd_scene format=3 uid="uid://dvmffuxafo75u"]
[ext_resource type="ArrayMesh" uid="uid://bi22s15yeh6mw" path="res://02_library/meshes/humanoid/dude.tres" id="1_ig7tw"]
[node name="Node3D" type="Node3D" unique_id=1431241737]
[node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=231315459]
mesh = ExtResource("1_ig7tw")

46
weltenauge/project.godot Normal file
View file

@ -0,0 +1,46 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Project Weltenauge"
run/main_scene="uid://cpxxy0664jyjm"
config/features=PackedStringArray("4.6", "Forward Plus")
run/low_processor_mode=true
boot_splash/image="uid://d1cyxxj8ypfu"
config/icon="uid://d1cyxxj8ypfu"
boot_splash/minimum_display_time=1000
[autoload]
GameEvents="*uid://4xof67e363t4"
ChatManager="*uid://c351ysuym2mdm"
NetworkManager="*uid://tniwwfk5703p"
[display]
window/size/viewport_width=1280
window/size/viewport_height=720
window/size/window_width_override=1280
window/size/window_height_override=720
window/stretch/mode="canvas_items"
[global_group]
players="Verwaltung der Spieler Objekte"
portals="Area Übergänge"
[physics]
3d/physics_engine="Jolt Physics"
[rendering]
rendering_device/driver.windows="d3d12"

42
weltenauge_struktur.txt Normal file
View file

@ -0,0 +1,42 @@
/project_root
│
├── 01_core/                     # Die Logik (Script-only, Architektur)
│   ├── systems/                 # Globale Manager (Autoloads): network.gd, game_state.gd, signal_bus.gd
│   ├── components/              # Logik-Nodes: health_component.gd, interactor.gd, inventory_controller.gd
│   └── base_classes/            # Definitionen & Klassen: item_data.gd, spell_base.gd, entity_base.gd
│
├── 02_library/                  # Das Rohmaterial (Assets & Bausteine)
│   ├── meshes/                  # 3D-Importe (.glb, .obj) nach Kategorien: nature/, architecture/
│ ├── mesh_libraries/ # Deine fertigen GridMap-Kataloge (.tres)
│   ├── materials/               # Globale Materialien (.tres): wood.tres, stone.tres, metal.tres
│   ├── textures/                # Roh-Bilder für Materialien & Shader
│   ├── ui_assets/               # Visuelles Interface: icons/, fonts/, themes/
│   ├── shaders/                 # Shader-Dateien (.gdshader)
│   ├── vfx/                     # Partikelsysteme und visuelle Effekte
│   └── audio/                   # Sound-Dateien: sfx/, music/, ambient/
│
├── 03_database/                 # Die Spieldaten (Content-Definitionen)
│   ├── items/                   # Hunderte .tres Dateien (iron_sword.tres, health_potion.tres)
│   ├── spells/                  # Zaubersprüche als Resources (.tres)
│   ├── loot_tables/             # Definitionen für Drops und Wahrscheinlichkeiten
│   ├── dialogues/               # Dialog-Skripte oder JSON-Dateien
│   ├── localization/            # Übersetzungsdateien (.csv, .po)
│ └── spawn_tables/ # Definitionen: Welche Monster-Gruppe spawnt in welcher Area?
│
├── 04_entities/                 # Die Spielobjekte (Zusammengesetzte Szenen)
│   ├── player/                  # Spieler-Szene (player.tscn + player.gd)
│   ├── npcs/                    # Monster, Händler, KI-Gegner
│   ├── props/                   # Interaktive Welt-Objekte: chest.tscn, door.tscn
│   ├── pickups/                 # Das world_item.tscn (Visualisierung von Database-Items)
│   ├── ui/                      # UI-Szenen: hud/, windows/, menus/, shared/
│ └── markers/ # Unsichtbare Helfer: spawn_point.tscn, area_trigger.tscn
│
├── 05_worlds/                   # Die Level (Das "NWN Modul")
│   ├── areas/                   # Die eigentlichen Karten/Level (.tscn)
│   ├── prefabs/                 # Wiederverwendbare Szenen-Gruppen (Häuser, Lagerfeuer-Sets)
│   └── environments/            # Licht- & Welt-Einstellungen (.tres)
│
└── 06_tests/                    # Die Sandkästen (Isolierte Prototypen)
    ├── test_ui.tscn             # Nur UI-Fenster testen
    ├── test_combat.tscn         # Kampf-Mechanik ohne Level testen
    └── test_inventory.tscn      # Item-Logik prüfen

View file

@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zauberturm Projekt-Log</title>
<style>
:root {
--bg-color: #0f111a;
--card-bg: #1a1d2e;
--accent-color: #7c4dff;
--text-color: #e0e0e0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: 'Segoe UI', sans-serif;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
header {
width: 100%;
padding: 3rem 0;
text-align: center;
background: linear-gradient(to bottom, #161b33, var(--bg-color));
border-bottom: 2px solid var(--accent-color);
}
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
width: 90%;
max-width: 1200px;
padding: 3rem 0;
}
.screenshot-card {
background: var(--card-bg);
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.05);
transition: all 0.3s ease;
}
.screenshot-card:hover {
transform: translateY(-5px);
border-color: var(--accent-color);
box-shadow: 0 10px 20px rgba(0,0,0,0.5);
}
.screenshot-card img {
width: 100%;
height: 250px;
object-fit: cover;
display: block;
}
.info { padding: 1rem; text-align: center; border-top: 1px solid rgba(255,255,255,0.05); }
.info span { font-size: 0.85rem; color: #888; font-family: monospace; }
.info small { display: block; margin-top: 4px; color: #555; }
.empty-msg { grid-column: 1 / -1; text-align: center; padding: 5rem; color: #555; }
footer { margin-top: auto; padding: 2rem; color: #444; }
</style>
</head>
<body>
<header>
<h1>Zauberturm.de</h1>
<p>Projekt-Entwicklung: Screenshot Log</p>
</header>
<div class="gallery">
<?php
$log_dir = 'image_log';
$allowed_extensions = ['jpg', 'jpeg', 'png', 'webp'];
// Verzeichnis erstellen, falls es nicht existiert
if (!is_dir($log_dir)) {
mkdir($log_dir, 0755, true);
}
// Dateien im Unterordner scannen
$files = array_diff(scandir($log_dir), array('..', '.'));
if (count($files) > 0) {
// Sortieren: Neueste Dateien zuerst
usort($files, function($a, $b) use ($log_dir) {
return filemtime($log_dir . '/' . $b) - filemtime($log_dir . '/' . $a);
});
foreach ($files as $file) {
$file_path = $log_dir . '/' . $file;
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
if (in_array($ext, $allowed_extensions)) {
$date = date("d.m.Y H:i", filemtime($file_path));
echo "
<div class='screenshot-card'>
<a href='$file_path' target='_blank'>
<img src='$file_path' alt='Projekt Screenshot'>
</a>
<div class='info'>
<span>$file</span>
<small>$date</small>
</div>
</div>";
}
}
} else {
echo "<div class='empty-msg'>Keine Screenshots im Ordner /image_log/ gefunden.</div>";
}
?>
</div>
<footer>&copy; 2026 zauberturm.de</footer>
</body>
</html>

BIN
workbench/zauberturm_html/logo_projekt_weltenauge.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
workbench/zauberturm_html/logo_zauberturm.png (Stored with Git LFS) Normal file

Binary file not shown.