diff --git a/hexmapper/.cursorrules b/hexmapper/.cursorrules new file mode 100644 index 0000000..29b7410 --- /dev/null +++ b/hexmapper/.cursorrules @@ -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. \ No newline at end of file diff --git a/hexmapper/.vscode/settings.json b/hexmapper/.vscode/settings.json new file mode 100644 index 0000000..14cf1f9 --- /dev/null +++ b/hexmapper/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "godotTools.editorPath.godot4": "/opt/godot/Godot_v4.5.1-stable_linux.x86_64" +} \ No newline at end of file diff --git a/hexmapper/scenes/MainMap/_MainMap.tscn b/hexmapper/scenes/MainMap/_MainMap.tscn index a94c346..79c2f0e 100644 --- a/hexmapper/scenes/MainMap/_MainMap.tscn +++ b/hexmapper/scenes/MainMap/_MainMap.tscn @@ -255,11 +255,13 @@ unique_name_in_owner = true custom_minimum_size = Vector2(125, 120) layout_mode = 2 size_flags_vertical = 3 -item_count = 4 +item_count = 6 item_0/text = "Neu" item_1/text = "Laden" item_2/text = "Speichern" item_3/text = "Speichern als" +item_4/text = "---" +item_5/text = "Tile hinzufügen" [node name="PaletteButton" type="Button" parent="UI/MenuTop"] unique_name_in_owner = true diff --git a/hexmapper/scenes/MainMap/map/map_manager.gd b/hexmapper/scenes/MainMap/map/map_manager.gd index a613bf8..a8f11b1 100644 --- a/hexmapper/scenes/MainMap/map/map_manager.gd +++ b/hexmapper/scenes/MainMap/map/map_manager.gd @@ -90,10 +90,11 @@ func get_tile_texcoord(str_palette:String, tile_name:String) -> Vector2i: return ret_v2 -func _on_map_item_selected(index: int) -> void: # 1 laden, 2 speichern, 3 speichern als +func _on_map_item_selected(index: int) -> void: # 0 neu, 1 laden, 2 speichern, 3 speichern als, 4 separator, 5 tile hinzufügen %MapItemList.deselect_all() match index: - 0: print("Neue Karte erstellen") + 0: + print("Neue Karte erstellen") 1: print("Karte laden") load_map_from() @@ -103,6 +104,12 @@ func _on_map_item_selected(index: int) -> void: # 1 laden, 2 speichern, 3 speich 3: print("Karte speichern als...") save_map_as() + 4: + # Separator - do nothing + pass + 5: + print("Custom Tile hinzufügen...") + add_custom_tile_from_file() map_menu_container.hide() @@ -194,3 +201,75 @@ func scroll_left(): func scroll_right(): var tile_width = ground_map.tile_set.tile_size.x camera_2d.position.x += tile_width + +## Opens a file dialog for the user to select and add a custom tile at runtime +func add_custom_tile_from_file(): + %FileDialog.mode_overrides_title = false + %FileDialog.title = "Custom Tile hinzufügen" + %FileDialog.access = FileDialog.ACCESS_FILESYSTEM + %FileDialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE + %FileDialog.set_filters(PackedStringArray(["*.png", "*.jpg", "*.jpeg"])) + + # Set initial directory to user's home or documents + var initial_dir: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS) + if DirAccess.dir_exists_absolute(initial_dir): + %FileDialog.set_current_dir(initial_dir) + + %FileDialog.popup_centered() + %FileDialog.file_selected.connect(_on_custom_tile_file_selected, CONNECT_ONE_SHOT) + +## Called when user selects a tile image file +func _on_custom_tile_file_selected(file_path: String): + var source_id: int = TileSetHelper.add_custom_single_tile( + ground_map.tile_set, + file_path, + Vector2i(72, 72) # Default tile size - could be made configurable + ) + + if source_id >= 0: + print("Custom tile added successfully! Source ID: %d" % source_id) + # Optionally add to palette or notify user + # You could emit a signal here to update the UI + else: + print("Failed to add custom tile from: %s" % file_path) + +## Opens a directory dialog for the user to add multiple tiles at once +func add_custom_tiles_from_directory(): + %FileDialog.mode_overrides_title = false + %FileDialog.title = "Custom Tiles aus Ordner hinzufügen" + %FileDialog.access = FileDialog.ACCESS_FILESYSTEM + %FileDialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR + + var initial_dir: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS) + if DirAccess.dir_exists_absolute(initial_dir): + %FileDialog.set_current_dir(initial_dir) + + %FileDialog.popup_centered() + %FileDialog.dir_selected.connect(_on_custom_tiles_directory_selected, CONNECT_ONE_SHOT) + +## Called when user selects a directory containing tile images +func _on_custom_tiles_directory_selected(dir_path: String): + var custom_tiles: Dictionary = TileSetHelper.add_custom_tiles_from_directory( + ground_map.tile_set, + dir_path, + Vector2i(72, 72) + ) + + if custom_tiles.size() > 0: + print("Added %d custom tiles from directory: %s" % [custom_tiles.size(), dir_path]) + for file_path in custom_tiles.keys(): + print(" - %s (source_id: %d)" % [file_path, custom_tiles[file_path]]) + else: + print("No valid tile images found in directory: %s" % dir_path) + +## Lists all tile sources (useful for debugging) +func list_all_tile_sources(): + var all_sources: Array = TileSetHelper.list_all_sources(ground_map.tile_set) + print("=== All Tile Sources ===") + for source_info in all_sources: + print("Source ID: %d, Texture: %s, Tiles: %d" % [ + source_info.get("source_id", -1), + source_info.get("texture_path", ""), + source_info.get("tile_count", 0) + ]) + print("========================") diff --git a/hexmapper/scenes/MainMap/map/tile_set_helper.gd b/hexmapper/scenes/MainMap/map/tile_set_helper.gd new file mode 100644 index 0000000..a365c9c --- /dev/null +++ b/hexmapper/scenes/MainMap/map/tile_set_helper.gd @@ -0,0 +1,158 @@ +extends RefCounted +class_name TileSetHelper + +## Helper class for managing custom single tiles in TileSet +## This allows adding individual tile images as separate sources alongside atlas-based tiles +## +## EASIEST WAY TO ADD CUSTOM SINGLE TILES: +## +## METHOD 1 - Manual (Editor): +## 1. Select GroundMapLayer in the scene +## 2. In Inspector, click on the TileSet property to open TileSet editor +## 3. Click "Add Texture(s) to TileSet" button +## 4. Select your custom tile image(s) +## 5. Each image will be added as a new source with a unique source_id +## 6. The tile will be at atlas_coords (0, 0) in that source +## +## METHOD 2 - Programmatic (Runtime): +## Use the helper functions below to add tiles from code: +## - add_custom_single_tile() - Add one tile from a file path +## - add_custom_tiles_from_directory() - Add all PNG files from a directory +## +## USAGE NOTES: +## - Your existing save/load system already supports multiple sources via source_id +## - Custom tiles use source_id > 1 (atlas tiles use source_id = 1) +## - Custom tiles always use atlas_coords (0, 0) since they're single-tile sources +## - Example: ground_map.set_cell(cell_pos, custom_source_id, Vector2i(0, 0)) + +## Adds a custom single tile image to a TileSet from a filesystem path (runtime) +## Supports both res:// paths and user filesystem paths +## Returns the source_id of the newly added tile, or -1 on failure +static func add_custom_single_tile(tile_set: TileSet, tile_texture_path: String, tile_size: Vector2i = Vector2i(72, 72)) -> int: + if tile_set == null: + push_error("TileSetHelper: tile_set is null") + return -1 + + # Load the texture - supports both res:// and filesystem paths + var texture: Texture2D = _load_texture_from_path(tile_texture_path) + if texture == null: + push_error("TileSetHelper: Failed to load texture from path: %s" % tile_texture_path) + return -1 + + # Create a new TileSetAtlasSource + var atlas_source: TileSetAtlasSource = TileSetAtlasSource.new() + atlas_source.texture = texture + atlas_source.texture_region_size = tile_size + + # Create a single tile at position (0, 0) + atlas_source.create_tile(Vector2i(0, 0)) + + # Find the next available source_id + var next_source_id: int = _get_next_available_source_id(tile_set) + + # Add the source to the TileSet + tile_set.add_source(atlas_source, next_source_id) + + print("TileSetHelper: Added custom tile from '%s' with source_id %d" % [tile_texture_path, next_source_id]) + return next_source_id + +## Loads a texture from either res:// or filesystem path +static func _load_texture_from_path(path: String) -> Texture2D: + # Try res:// path first (for project resources) + if path.begins_with("res://"): + return load(path) as Texture2D + + # For filesystem paths, load the image and create a texture + var image: Image = Image.new() + var error: Error = image.load_from_file(path) + if error != OK: + push_error("TileSetHelper: Failed to load image from file: %s (Error: %d)" % [path, error]) + return null + + var texture: ImageTexture = ImageTexture.create_from_image(image) + return texture + +## Adds multiple custom single tiles from a directory +## Supports both res:// and filesystem paths +## Returns a dictionary mapping file paths to their source_ids +static func add_custom_tiles_from_directory(tile_set: TileSet, directory_path: String, tile_size: Vector2i = Vector2i(72, 72)) -> Dictionary: + var result: Dictionary = {} + + if tile_set == null: + push_error("TileSetHelper: tile_set is null") + return result + + var dir: DirAccess + if directory_path.begins_with("res://"): + dir = DirAccess.open(directory_path) + else: + dir = DirAccess.open(directory_path) + + if dir == null: + push_error("TileSetHelper: Failed to open directory: %s" % directory_path) + return result + + dir.list_dir_begin() + var file_name: String = dir.get_next() + + while file_name != "": + if not dir.current_is_dir(): + # Check for common image extensions + var lower_name: String = file_name.to_lower() + if lower_name.ends_with(".png") or lower_name.ends_with(".jpg") or lower_name.ends_with(".jpeg"): + var full_path: String + if directory_path.begins_with("res://"): + full_path = directory_path.path_join(file_name) + else: + full_path = directory_path.path_join(file_name) + + var source_id: int = add_custom_single_tile(tile_set, full_path, tile_size) + if source_id >= 0: + result[full_path] = source_id + file_name = dir.get_next() + + return result + +## Gets the next available source_id in the TileSet +static func _get_next_available_source_id(tile_set: TileSet) -> int: + var max_id: int = 0 + for i in range(tile_set.get_source_count()): + var source_id: int = tile_set.get_source_id(i) + max_id = max(max_id, source_id) + return max_id + 1 + +## Gets information about a tile source +## Returns a dictionary with source_id, texture_path, and tile_count +static func get_source_info(tile_set: TileSet, source_id: int) -> Dictionary: + if tile_set == null: + return {} + + var source: TileSetSource = tile_set.get_source(source_id) + if source == null: + return {} + + var info: Dictionary = { + "source_id": source_id, + "tile_count": 0 + } + + if source is TileSetAtlasSource: + var atlas_source: TileSetAtlasSource = source as TileSetAtlasSource + info["texture_path"] = atlas_source.texture.resource_path if atlas_source.texture else "" + info["tile_count"] = atlas_source.get_tiles_count() + + return info + +## Lists all sources in the TileSet +static func list_all_sources(tile_set: TileSet) -> Array: + var sources: Array = [] + + if tile_set == null: + return sources + + for i in range(tile_set.get_source_count()): + var source_id: int = tile_set.get_source_id(i) + var info: Dictionary = get_source_info(tile_set, source_id) + sources.append(info) + + return sources