Adding custom tile functionality written by Cursor

This commit is contained in:
Torsten 2026-01-10 10:24:33 +01:00
parent cf2a9aaecf
commit ef53ed543f
5 changed files with 246 additions and 3 deletions

1
hexmapper/.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.

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

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

View file

@ -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

View file

@ -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("========================")

View file

@ -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