diff --git a/scenes/Main.tscn b/scenes/Main.tscn index 540cf26..e661f9e 100644 --- a/scenes/Main.tscn +++ b/scenes/Main.tscn @@ -462,7 +462,14 @@ texture = ExtResource("3_yddbk") [sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_hwjm3"] texture = ExtResource("5_bqev6") +0:0/next_alternative_id = 4 0:0/0 = 0 +0:0/1 = 1 +0:0/1/flip_h = true +0:0/2 = 2 +0:0/2/flip_v = true +0:0/3 = 3 +0:0/3/transpose = true 1:0/0 = 0 2:0/0 = 0 3:0/0 = 0 @@ -1514,7 +1521,6 @@ offset_right = 936.0 offset_bottom = 256.0 horizontal_alignment = 2 -[connection signal="set_camera_position" from="World" to="World/CameraZoom2D" method="_on_world_set_camera_position"] [connection signal="button_pressed" from="UILayer/Control" to="World" method="_on_control_button_pressed"] [connection signal="pressed" from="UILayer/Control/ConstructionPanel/button_residental" to="UILayer/Control" method="_on_button_residental_pressed"] [connection signal="pressed" from="UILayer/Control/ConstructionPanel/button_commercial" to="UILayer/Control" method="_on_button_commercial_pressed"] diff --git a/scripts/Globals.gd b/scripts/Globals.gd index 838bfb2..b1c0227 100644 --- a/scripts/Globals.gd +++ b/scripts/Globals.gd @@ -3,6 +3,16 @@ extends Node +enum {TILE_WATER, TILE_TERRAIN, TILE_FOREST, TILE_BOG} + +func are_coords_valid(value:int, bounds:Vector2i, errmsg:String) -> bool: + if bounds.x > value or value > bounds.y: + errmsg = errmsg % [value, bounds.x, bounds.y] + push_error(errmsg) + return false + + return true + var world_map: TileMap var map_image_size:Vector2i diff --git a/scripts/Main.gd b/scripts/Main.gd index 59f46ad..7328520 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -6,25 +6,28 @@ # - Then click and drag to draw the lines of grey cubes. # - etc. +class_name Main extends Node # The idea is for the user to be able to choose the map from GUI later -var map_file_name: String = "res://maps/tampere_200px_crop.png" +var map_filename: String = "res://maps/tampere_10x10km_1000px.png" +var _world := World.new() +var _world_generator := WorldGeneration.new() func _init(): DisplayServer.window_set_size( - Vector2i(Globals.DEFAULT_X_RES, Globals.DEFAULT_Y_RES) + #Vector2i(Globals.DEFAULT_X_RES, Globals.DEFAULT_Y_RES) + Vector2i(2560,1440) ) # Called when the node enters the scene tree for the first time. func _ready(): - Globals.world_map = get_node("World") - if !Globals.world_map: + if !_world: push_error(Globals.ERROR_MAKING_WORLD_INSTANCE) quit_game() # generate terrain. quit game if generation fails. - if !Globals.world_map.generate_terrain(map_file_name): + if !_world_generator.generate_world(map_filename): push_error(Globals.ERROR_WHILE_GENERATING_MAP) quit_game() diff --git a/scripts/World.gd b/scripts/World.gd index d2180cb..cec4a31 100644 --- a/scripts/World.gd +++ b/scripts/World.gd @@ -1,12 +1,13 @@ +class_name World extends TileMap -signal set_camera_position(pos:Vector2) - var has_placeable_building: bool = false var building var building_type: String var scene -var image:Image = Image.new() + +func _init(): + Globals.world_map = self # Called when the node enters the scene tree for the first time. func _ready(): @@ -57,7 +58,7 @@ func _on_control_button_pressed(type): self.building_type = type # create new building, in Building node it is attached to mouse cursor - var building_properties = get_building_properties() + #var building_properties = get_building_properties() scene = load(Globals.SCENE_PATH + "Building.tscn") building = scene.instantiate() #building.set_cell(0, Vector2i(0,0), building_properties[0], building_properties[1], 0) @@ -75,29 +76,21 @@ func _input(event): if event.is_action_pressed("cancel"): if has_placeable_building: pass - + func calculate_grid_coordinates(map_position: Vector2) -> Vector2: return (map_position).floor() -func are_coords_valid(value:int, bounds:Vector2i, errmsg:String) -> bool: - if bounds.x > value or value > bounds.y: - errmsg = errmsg % [value, bounds.x, bounds.y] - push_error(errmsg) - return false - - return true - func place_building_to_map(): var building_properties = get_building_properties() var tile_on_mouse = local_to_map(get_global_mouse_position()) - if !are_coords_valid( + if !Globals.are_coords_valid( tile_on_mouse.y, Vector2i(0, Globals.map_image_size.y), Globals.ERROR_TILE_Y_COORDS_OUT_OF_BOUNDS ): return false - elif !are_coords_valid( + elif !Globals.are_coords_valid( tile_on_mouse.x, Vector2i(0, Globals.map_image_size.x), Globals.ERROR_TILE_X_COORDS_OUT_OF_BOUNDS @@ -105,109 +98,4 @@ func place_building_to_map(): return false set_cell(Globals.LAYER_BUILDINGS, tile_on_mouse, building_properties[0], building_properties[1], 0) - -func generate_terrain(filename) -> bool: - # Try to load the image which we used to place water & ground to world map - image = load(filename) - if image == null: - var errmsg = Globals.ERROR_FAILED_TO_LOAD_FILE - push_error(errmsg % filename) - return false - - # Check if image is too small or too large - Globals.map_image_size = image.get_size() - if !validate_mapgen_params(): - return false - - generate_water_and_land() - generate_shorelines() - - # center camera to world map - emit_signal( - "set_camera_position", - Vector2(Globals.map_image_size.x / 2.0 * Globals.TILE_SIZE_X, - Globals.map_image_size.y / 2.0 * Globals.TILE_SIZE_Y) - ) - return true - -func validate_mapgen_params() -> bool: - if !are_coords_valid( - Globals.map_image_size.y, - Vector2i(Globals.MAP_MIN_HEIGHT, Globals.MAP_MAX_HEIGHT), - Globals.ERROR_IMAGE_HEIGHT_INCORRECT): - return false - elif !are_coords_valid( - Globals.map_image_size.x, - Vector2i(Globals.MAP_MIN_WIDTH, Globals.MAP_MAX_WIDTH), - Globals.ERROR_IMAGE_WIDTH_INCORRECT): - return false - - # Try to load the world tilemap where we place the tiles - if (Globals.world_map == null): - var errmsg = Globals.ERROR_WORLD_TILEMAP_NODE_MISSING % Globals.WORLD_NODE - push_error(errmsg) - return false - - return true - -func generate_water_and_land() -> void: - for x in Globals.map_image_size.x: - for y in Globals.map_image_size.y: - # layer | position coords | tilemap id | coords of the tile at tilemap | alternative tile - if image.get_pixel(x, y) == Globals.WATER_TILE_COLOR_IN_MAP_FILE: - Globals.world_map.set_cell(Globals.LAYER_TERRAIN, Vector2i(x, y), 2, Vector2i(15,5), 0) - else: - Globals.world_map.set_cell(Globals.LAYER_TERRAIN, Vector2i(x, y), 2, Vector2i(0,0), 0) - -func generate_shorelines() -> void: - # for testing avoid map borders to make it simpler to implement - var directions:Array = [ - Vector2i(0,1), # south - Vector2i(1,0), # east - Vector2i(0,-1), # north - Vector2i(-1,0) # west - ] - - for x in range(1, Globals.map_image_size.x-1): - for y in range(1, Globals.map_image_size.y-1): - # skip tiles with water - if image.get_pixel(x, y) == Globals.WATER_TILE_COLOR_IN_MAP_FILE: - continue - - # now we are supposed to be inspecting a tile with land - # 1 = water 0 = land - var surrounding_water_tiles:Array = [] - - # determine which directions have water around the tile - for dir in directions: - if image.get_pixel(x+dir.x, y+dir.y) == Globals.WATER_TILE_COLOR_IN_MAP_FILE: - surrounding_water_tiles.append(1) - continue - surrounding_water_tiles.append(0) - - var selected_tile:Vector2i = Vector2i(0,0) - - match surrounding_water_tiles: - [1,1,0,0]: # south & east - selected_tile = Vector2i(19,0) # or 20 - [0,1,1,0]: # north & east - selected_tile = Vector2i(15,0) # or 60 - [0,0,1,1]: # north & west - selected_tile = Vector2i(11,0) # or 12 - [1,0,0,1]: # south & west - selected_tile = Vector2i(7,0) # or 8 - [0,0,0,1]: # water in west only - selected_tile = Vector2i(9,0) # or 10 - [0,0,1,0]: # water in north only - selected_tile = Vector2i(13,0) # or 14 - [0,1,0,0]: # water in east only - selected_tile = Vector2i(17,0) # or 18 - [1,0,0,0]: # water in south only - selected_tile = Vector2i(5,0) # or 6 - _: # otherwise skip drawing - continue - - # layer | position coords | tilemap id | coords of the tile at tilemap | alternative tile - Globals.world_map.set_cell(Globals.LAYER_TERRAIN, Vector2i(x, y), 2, selected_tile, 0) - diff --git a/scripts/WorldGeneration.gd b/scripts/WorldGeneration.gd new file mode 100644 index 0000000..a0e7355 --- /dev/null +++ b/scripts/WorldGeneration.gd @@ -0,0 +1,215 @@ +class_name WorldGeneration +extends RefCounted + +signal set_camera_position(pos:Vector2) + +var image:Image = Image.new() +var map_tile_data:Array[Array] = [[]] # store map tile info to a 2d array +var directions:Array = [ + Vector2i(0,1), # south + Vector2i(1,0), # east + Vector2i(0,-1), # north + Vector2i(-1,0) # west + ] + +var count:int = 0 + +func choose_randomly(list_of_entries:Array[int]) -> int: + return list_of_entries[randi() % list_of_entries.size()] + +# +# Generates biomes, like forest and bog +# +func generate_biomes() -> void: + pass + +func generate_world(filename) -> bool: + # Try to load the image which we used to place water & ground to world map + image = load(filename) + if image == null: + var errmsg = Globals.ERROR_FAILED_TO_LOAD_FILE + push_error(errmsg % filename) + return false + + # Check if image is too small or too large + Globals.map_image_size = image.get_size() + if !validate_mapgen_params(): + return false + + read_image_pixel_data() + smooth_land_features() + generate_biomes() + set_tilemap_tiles() + set_shorelines() + print(count) + + # center camera to world map + emit_signal( + "set_camera_position", + Vector2(Globals.map_image_size.x / 2.0 * Globals.TILE_SIZE_X, + Globals.map_image_size.y / 2.0 * Globals.TILE_SIZE_Y) + ) + return true + +func match_tile(surrounding_tiles) -> Vector2i: + match surrounding_tiles: + # 3 land tiles around water + [1,1,1,0]: + return Vector2i(0,0) # land tile + [1,1,0,1]: + return Vector2i(0,0) # land tile + [1,0,1,1]: + return Vector2i(0,0) # land tile + [0,1,1,1]: + return Vector2i(0,0) # land tile + + # 2 land tiles around water + [1,1,0,0]: # south & east + return Vector2i(choose_randomly([11,12]),0) + [0,1,1,0]: # north & east + return Vector2i(choose_randomly([7,8]),0) + [0,0,1,1]: # north & west + return Vector2i(choose_randomly([19,20]),0) + [1,0,0,1]: # south & west + return Vector2i(choose_randomly([15,16]),0) + + # 1 land tile around water + [0,0,0,1]: # west only + return Vector2i(choose_randomly([17,18]),0) + [0,0,1,0]: # north only + return Vector2i(choose_randomly([5,6]),0) + [0,1,0,0]: # east only + return Vector2i(choose_randomly([9,10]),0) + [1,0,0,0]: # south only + return Vector2i(choose_randomly([13,14]),0) + + _: # otherwise skip drawing + return Vector2i(-1,-1) + +func read_image_pixel_data(): + # initialize the array to have enough rows + map_tile_data.resize(Globals.map_image_size.y) + + for y in Globals.map_image_size.y: + #initialize the row to have enough columns + map_tile_data[y].resize(Globals.map_image_size.y) + + for x in Globals.map_image_size.x: + if image.get_pixel(x, y) == Globals.WATER_TILE_COLOR_IN_MAP_FILE: + map_tile_data[y][x] = Globals.TILE_WATER + else: + map_tile_data[y][x] = Globals.TILE_TERRAIN + +func set_shorelines() -> void: + # for testing avoid map borders to make it simpler to implement + for y in range(1, Globals.map_image_size.y-1): + for x in range(1, Globals.map_image_size.x-1): + # skip tiles with land + if map_tile_data[y][x] != Globals.TILE_WATER: + continue + + # now we are supposed to be inspecting a tile with land + # 1 = water 0 = land + var surrounding_tiles:Array = [] + + # determine which directions have land around the tile + for dir in directions: + if map_tile_data[y+dir.y][x+dir.x] == Globals.TILE_TERRAIN: + surrounding_tiles.append(Globals.TILE_TERRAIN) + continue + surrounding_tiles.append(Globals.TILE_WATER) + + var selected_tile = match_tile(surrounding_tiles) + if selected_tile.x == -1 or selected_tile.y == -1: + continue + + # layer | position coords | tilemap id | coords of the tile at tilemap | alternative tile + Globals.world_map.set_cell(Globals.LAYER_TERRAIN, Vector2i(x, y), 2, selected_tile, 0) + +func set_tilemap_tiles() -> void: + for y in map_tile_data.size(): + for x in map_tile_data[y].size(): + # layer | position coords | tilemap id | coords of the tile at tilemap | alternative tile + # set water or ground + match map_tile_data[y][x]: + Globals.TILE_WATER: + Globals.world_map.set_cell( + Globals.LAYER_TERRAIN, + Vector2i(x, y), + 2, + Vector2i(15,5), + 0 + ) + Globals.TILE_TERRAIN: + Globals.world_map.set_cell( + Globals.LAYER_TERRAIN, + Vector2i(x, y), + 2, + Vector2i(0,0), + choose_randomly([0,1,2,3]) + ) + _: #default + pass + +# Fill water tiles, surrounded in 3-4 sides by land, with land. +# Do it recursively with limit of n recursions! +func smooth_land_features() -> void: + # for testing avoid map borders to make it simpler to implement + for y in range(1, Globals.map_image_size.y-1): + for x in range(1, Globals.map_image_size.x-1): + if map_tile_data[y][x] != Globals.TILE_WATER: + continue + + smooth_recursively(Vector2i(x, y)) + +func smooth_recursively(pos:Vector2i) -> void: + # now we are supposed to be inspecting a tile with land + # 1 = water 0 = land + var surrounding_tiles:Array = [] + count += 1 + + # determine which directions have land around the tile + for dir in directions: + if map_tile_data[pos.y+dir.y][pos.x+dir.x] == Globals.TILE_TERRAIN: + surrounding_tiles.append(Globals.TILE_TERRAIN) + elif map_tile_data[pos.y+dir.y][pos.x+dir.x] == Globals.TILE_WATER: + surrounding_tiles.append(Globals.TILE_WATER) + + match surrounding_tiles: + [1,1,1,0]: #west + map_tile_data[pos.y][pos.x] = Globals.TILE_TERRAIN + pos.x -= 1 + [1,1,0,1]: #north + map_tile_data[pos.y][pos.x] = Globals.TILE_TERRAIN + pos.y -= 1 + [1,0,1,1]: #east + map_tile_data[pos.y][pos.x] = Globals.TILE_TERRAIN + pos.x += 1 + [0,1,1,1]: #south + map_tile_data[pos.y][pos.x] = Globals.TILE_TERRAIN + pos.y += 1 + _: + return + + smooth_recursively(pos) + +func validate_mapgen_params() -> bool: + if !Globals.are_coords_valid( + Globals.map_image_size.y, + Vector2i(Globals.MAP_MIN_HEIGHT, Globals.MAP_MAX_HEIGHT), + Globals.ERROR_IMAGE_HEIGHT_INCORRECT): + return false + + elif !Globals.are_coords_valid( + Globals.map_image_size.x, + Vector2i(Globals.MAP_MIN_WIDTH, Globals.MAP_MAX_WIDTH), + Globals.ERROR_IMAGE_WIDTH_INCORRECT): + return false + + # Try to load the world tilemap where we place the tiles + if (Globals.world_map == null): + var errmsg = Globals.ERROR_WORLD_TILEMAP_NODE_MISSING % Globals.WORLD_NODE + push_error(errmsg) + return false + + return true